diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fe451fb079..027e62b5cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweWorldNativeAccess.java index 352e9b9583..c234f2544c 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_20_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R2/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -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() { @@ -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) diff --git a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweWorldNativeAccess.java index 4fb3e04851..bbc964adbb 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_20_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R3/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -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() { @@ -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) diff --git a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweWorldNativeAccess.java index 4fa9988b81..ed4ebdd674 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -59,7 +60,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() { @@ -95,7 +96,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) diff --git a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightPlatformAdapter.java index cbcb613b16..d8a672cbef 100644 --- a/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_20_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_20_R4/PaperweightPlatformAdapter.java @@ -25,7 +25,6 @@ import net.minecraft.core.IdMap; import net.minecraft.core.Registry; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; @@ -363,7 +362,7 @@ public static void sendChunk(IntPair pair, ServerLevel nmsWorld, int chunkX, int if (lockHolder.chunkLock == null) { return; } - MinecraftServer.getServer().execute(() -> { + TaskManager.taskManager().task(() -> { try { ClientboundLevelChunkWithLightPacket packet; if (PaperLib.isPaper()) { diff --git a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweWorldNativeAccess.java index f7c2dc8e84..bc45970fa7 100644 --- a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -59,7 +60,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() { @@ -95,7 +96,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) diff --git a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightPlatformAdapter.java index f91810ead5..c474af94b5 100644 --- a/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_R1/PaperweightPlatformAdapter.java @@ -25,7 +25,6 @@ import net.minecraft.core.IdMap; import net.minecraft.core.Registry; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; @@ -353,7 +352,7 @@ public static void sendChunk(IntPair pair, ServerLevel nmsWorld, int chunkX, int if (lockHolder.chunkLock == null) { return; } - MinecraftServer.getServer().execute(() -> { + TaskManager.taskManager().task(() -> { try { ClientboundLevelChunkWithLightPacket packet; if (PaperLib.isPaper()) { diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweWorldNativeAccess.java index 4c38811703..bb9850f131 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -62,7 +63,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() { @@ -98,7 +99,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.shouldApply(SideEffect.UPDATE) ? 0 : 512 diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightGetBlocks.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightGetBlocks.java index 15f6350617..96b483f88e 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightGetBlocks.java +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightGetBlocks.java @@ -195,9 +195,9 @@ public void removeSectionLighting(int layer, boolean sky) { @Override public FaweCompoundTag tile(final int x, final int y, final int z) { - BlockEntity blockEntity = getChunk().getBlockEntity(new BlockPos((x & 15) + ( - chunkX << 4), y, (z & 15) + ( - chunkZ << 4))); + BlockPos blockPos = new BlockPos((x & 15) + (chunkX << 4), y, (z & 15) + (chunkZ << 4)); + // Avoid LevelChunk#getBlockEntity(...) here: on Folia async command threads, getCurrentWorldData() can be null. + BlockEntity blockEntity = getChunk().getBlockEntities().get(blockPos); if (blockEntity == null) { return null; } @@ -721,10 +721,20 @@ protected > T internalCall( final BlockPos pos = new BlockPos(x, y, z); synchronized (nmsWorld) { - BlockEntity tileEntity = nmsWorld.getBlockEntity(pos); + BlockEntity tileEntity; + try { + tileEntity = nmsWorld.getBlockEntity(pos); + } catch (NullPointerException ignored) { + // Folia can invoke this from non-region threads where Level#getCurrentWorldData is null. + tileEntity = nmsChunk.getBlockEntities().get(pos); + } if (tileEntity == null || tileEntity.isRemoved()) { nmsWorld.removeBlockEntity(pos); - tileEntity = nmsWorld.getBlockEntity(pos); + try { + tileEntity = nmsWorld.getBlockEntity(pos); + } catch (NullPointerException ignored) { + tileEntity = nmsChunk.getBlockEntities().get(pos); + } } if (tileEntity != null) { ValueInput input = createInput(nativeTag.linTag().toBuilder() diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightPlatformAdapter.java index a2c9261d5a..4e62a974d3 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/PaperweightPlatformAdapter.java @@ -26,7 +26,6 @@ import net.minecraft.core.Registry; import net.minecraft.core.RegistryAccess; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; @@ -326,7 +325,7 @@ private static LevelChunk toLevelChunk(Chunk chunk) { private static void addTicket(ServerLevel serverLevel, int chunkX, int chunkZ) { // Ensure chunk is definitely loaded before applying a ticket - io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> serverLevel + TaskManager.taskManager().task(() -> serverLevel .getChunkSource() .addTicketWithRadius(ChunkHolderManager.UNLOAD_COOLDOWN, new ChunkPos(chunkX, chunkZ), 0)); } @@ -361,7 +360,7 @@ public static void sendChunk(IntPair pair, ServerLevel nmsWorld, int chunkX, int if (lockHolder.chunkLock == null) { return; } - MinecraftServer.getServer().execute(() -> { + TaskManager.taskManager().task(() -> { try { ChunkPos pos = levelChunk.getPos(); ClientboundLevelChunkWithLightPacket packet; diff --git a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/regen/PaperweightRegen.java b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/regen/PaperweightRegen.java index 549a021400..f46a31a36c 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/regen/PaperweightRegen.java +++ b/worldedit-bukkit/adapters/adapter-1_21_11/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_11/regen/PaperweightRegen.java @@ -92,7 +92,13 @@ public PaperweightRegen( @Override protected void runTasks(final BooleanSupplier shouldKeepTicking) { while (shouldKeepTicking.getAsBoolean()) { - if (!this.freshWorld.getChunkSource().pollTask()) { + try { + if (!this.freshWorld.getChunkSource().pollTask()) { + return; + } + } catch (NullPointerException ignored) { + // Folia global tasks can execute without a current regionized world context. + // In that case, pollTask() is not safe to call and will throw from NMS internals. return; } } diff --git a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweWorldNativeAccess.java index 60a1d5c13a..7e59fc1a48 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_21_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_4/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -60,7 +61,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() { @@ -96,7 +97,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) diff --git a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweWorldNativeAccess.java index f5a49fbb67..f38d05bb22 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_21_5/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_5/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -60,7 +61,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() { @@ -96,7 +97,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.shouldApply(SideEffect.UPDATE) ? 0 : 512 diff --git a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweWorldNativeAccess.java index 36370d286f..5cbb4b3726 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_21_6/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_6/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -62,7 +63,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() { @@ -98,7 +99,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, diff --git a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java new file mode 100644 index 0000000000..8fc63e1b2c --- /dev/null +++ b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/MinecraftReflection.java @@ -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, Object> CACHE = new ConcurrentHashMap<>(); + + private static final List FIELD_CANDIDATES = Arrays.asList( + "currentTick", + "tickCount", + "fullTick", + "fullTickCount", + "ticks", + "currentTicks", + "au", + "ac" + ); + + private static final List 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; + } +} diff --git a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweWorldNativeAccess.java index e778eecae8..8141d3fd87 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightFaweWorldNativeAccess.java @@ -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; @@ -62,7 +63,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() { @@ -98,7 +99,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.shouldApply(SideEffect.UPDATE) ? 0 : 512 diff --git a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightPlatformAdapter.java index 3bbe8c7bad..12852cd731 100644 --- a/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_21_9/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_21_9/PaperweightPlatformAdapter.java @@ -29,7 +29,6 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; @@ -376,7 +375,7 @@ public static void sendChunk(IntPair pair, ServerLevel nmsWorld, int chunkX, int if (lockHolder.chunkLock == null) { return; } - MinecraftServer.getServer().execute(() -> { + TaskManager.taskManager().task(() -> { try { ChunkPos pos = levelChunk.getPos(); ClientboundLevelChunkWithLightPacket packet; diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/BukkitTaskManager.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/BukkitTaskManager.java index 11b8565a67..46eb0a0094 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/BukkitTaskManager.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/BukkitTaskManager.java @@ -2,13 +2,21 @@ import com.fastasyncworldedit.core.util.TaskManager; import org.bukkit.Bukkit; +import org.bukkit.Server; import org.bukkit.plugin.Plugin; import javax.annotation.Nonnull; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; public class BukkitTaskManager extends TaskManager { private final Plugin plugin; + private final AtomicInteger foliaTaskCounter = new AtomicInteger(); + private final Map foliaTaskCancels = new ConcurrentHashMap<>(); public BukkitTaskManager(final Plugin plugin) { this.plugin = plugin; @@ -16,39 +24,239 @@ public BukkitTaskManager(final Plugin plugin) { @Override public int repeat(@Nonnull final Runnable runnable, final int interval) { - return this.plugin.getServer().getScheduler().scheduleSyncRepeatingTask(this.plugin, runnable, interval, interval); + try { + return this.plugin.getServer().getScheduler().scheduleSyncRepeatingTask(this.plugin, runnable, interval, interval); + } catch (UnsupportedOperationException ignored) { + return scheduleFoliaRepeatingTask(runnable, interval); + } } @Override public int repeatAsync(@Nonnull final Runnable runnable, final int interval) { - return this.plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(this.plugin, runnable, interval, interval); + try { + return this.plugin.getServer().getScheduler().scheduleAsyncRepeatingTask(this.plugin, runnable, interval, interval); + } catch (UnsupportedOperationException ignored) { + return scheduleFoliaAsyncRepeatingTask(runnable, interval); + } } @Override public void async(@Nonnull final Runnable runnable) { - this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, runnable).getTaskId(); + try { + this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, runnable).getTaskId(); + } catch (UnsupportedOperationException ignored) { + scheduleFoliaAsyncNow(runnable); + } + } + + @Override + public boolean isMainThread() { + if (Bukkit.isPrimaryThread()) { + return true; + } + return isFoliaGlobalTickThread(); } @Override public void task(@Nonnull final Runnable runnable) { - this.plugin.getServer().getScheduler().runTask(this.plugin, runnable).getTaskId(); + try { + this.plugin.getServer().getScheduler().runTask(this.plugin, runnable).getTaskId(); + } catch (UnsupportedOperationException ignored) { + executeFoliaNow(runnable); + } } @Override public void later(@Nonnull final Runnable runnable, final int delay) { - this.plugin.getServer().getScheduler().runTaskLater(this.plugin, runnable, delay).getTaskId(); + try { + this.plugin.getServer().getScheduler().runTaskLater(this.plugin, runnable, delay).getTaskId(); + } catch (UnsupportedOperationException ignored) { + scheduleFoliaDelayedTask(runnable, delay); + } } @Override public void laterAsync(@Nonnull final Runnable runnable, final int delay) { - this.plugin.getServer().getScheduler().runTaskLaterAsynchronously(this.plugin, runnable, delay); + try { + this.plugin.getServer().getScheduler().runTaskLaterAsynchronously(this.plugin, runnable, delay); + } catch (UnsupportedOperationException ignored) { + scheduleFoliaAsyncDelayedTask(runnable, delay); + } } @Override public void cancel(final int task) { if (task != -1) { - Bukkit.getScheduler().cancelTask(task); + Runnable foliaCancel = foliaTaskCancels.remove(task); + if (foliaCancel != null) { + foliaCancel.run(); + } else { + Bukkit.getScheduler().cancelTask(task); + } + } + } + + private void executeFoliaNow(final Runnable runnable) { + try { + Object globalRegionScheduler = getGlobalRegionScheduler(); + Method execute = globalRegionScheduler.getClass().getMethod("execute", Plugin.class, Runnable.class); + execute.invoke(globalRegionScheduler, this.plugin, runnable); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule sync task on Folia", e); + } + } + + private void scheduleFoliaDelayedTask(final Runnable runnable, final long delay) { + try { + Object globalRegionScheduler = getGlobalRegionScheduler(); + Method runDelayed = globalRegionScheduler.getClass() + .getMethod("runDelayed", Plugin.class, java.util.function.Consumer.class, long.class); + Object scheduledTask = runDelayed.invoke( + globalRegionScheduler, + this.plugin, + new FoliaTaskConsumer(runnable), + normalizeFoliaTickDelay(delay) + ); + storeFoliaTaskCancel(scheduledTask); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule delayed sync task on Folia", e); + } + } + + private int scheduleFoliaRepeatingTask(final Runnable runnable, final long interval) { + try { + Object globalRegionScheduler = getGlobalRegionScheduler(); + Method runAtFixedRate = globalRegionScheduler.getClass() + .getMethod("runAtFixedRate", Plugin.class, java.util.function.Consumer.class, long.class, long.class); + Object scheduledTask = runAtFixedRate.invoke( + globalRegionScheduler, + this.plugin, + new FoliaTaskConsumer(runnable), + normalizeFoliaTickDelay(interval), + normalizeFoliaTickDelay(interval) + ); + return storeFoliaTaskCancel(scheduledTask); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule repeating sync task on Folia", e); + } + } + + private void scheduleFoliaAsyncNow(final Runnable runnable) { + try { + Object asyncScheduler = getAsyncScheduler(); + Method runNow = asyncScheduler.getClass().getMethod("runNow", Plugin.class, java.util.function.Consumer.class); + Object scheduledTask = runNow.invoke(asyncScheduler, this.plugin, new FoliaTaskConsumer(runnable)); + storeFoliaTaskCancel(scheduledTask); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule async task on Folia", e); } } + private void scheduleFoliaAsyncDelayedTask(final Runnable runnable, final long delay) { + try { + Object asyncScheduler = getAsyncScheduler(); + Method runDelayed = asyncScheduler.getClass().getMethod( + "runDelayed", + Plugin.class, + java.util.function.Consumer.class, + long.class, + TimeUnit.class + ); + Object scheduledTask = runDelayed.invoke( + asyncScheduler, + this.plugin, + new FoliaTaskConsumer(runnable), + normalizeFoliaMillisDelay(delay), + TimeUnit.MILLISECONDS + ); + storeFoliaTaskCancel(scheduledTask); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule delayed async task on Folia", e); + } + } + + private int scheduleFoliaAsyncRepeatingTask(final Runnable runnable, final long interval) { + try { + Object asyncScheduler = getAsyncScheduler(); + Method runAtFixedRate = asyncScheduler.getClass().getMethod( + "runAtFixedRate", + Plugin.class, + java.util.function.Consumer.class, + long.class, + long.class, + TimeUnit.class + ); + long normalizedDelay = normalizeFoliaMillisDelay(interval); + Object scheduledTask = runAtFixedRate.invoke( + asyncScheduler, + this.plugin, + new FoliaTaskConsumer(runnable), + normalizedDelay, + normalizedDelay, + TimeUnit.MILLISECONDS + ); + return storeFoliaTaskCancel(scheduledTask); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule repeating async task on Folia", e); + } + } + + private int storeFoliaTaskCancel(final Object scheduledTask) { + try { + Method cancel = scheduledTask.getClass().getMethod("cancel"); + if (!cancel.canAccess(scheduledTask)) { + cancel.setAccessible(true); + } + int taskId = foliaTaskCounter.decrementAndGet(); + foliaTaskCancels.put(taskId, () -> { + try { + cancel.invoke(scheduledTask); + } catch (ReflectiveOperationException | RuntimeException e) { + throw new RuntimeException("Unable to cancel Folia task", e); + } + }); + return taskId; + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to wire Folia task cancellation", e); + } + } + + private Object getGlobalRegionScheduler() throws ReflectiveOperationException { + Server server = this.plugin.getServer(); + Method method = server.getClass().getMethod("getGlobalRegionScheduler"); + return method.invoke(server); + } + + private Object getAsyncScheduler() throws ReflectiveOperationException { + Server server = this.plugin.getServer(); + Method method = server.getClass().getMethod("getAsyncScheduler"); + return method.invoke(server); + } + + private boolean isFoliaGlobalTickThread() { + try { + Method method = Bukkit.class.getMethod("isGlobalTickThread"); + return Boolean.TRUE.equals(method.invoke(null)); + } catch (ReflectiveOperationException ignored) { + return false; + } + } + + private long normalizeFoliaTickDelay(final long ticks) { + return Math.max(1L, ticks); + } + + private long normalizeFoliaMillisDelay(final long ticks) { + return normalizeFoliaTickDelay(ticks) * 50L; + } + + private record FoliaTaskConsumer(Runnable runnable) implements java.util.function.Consumer { + + @Override + public void accept(final Object ignored) { + runnable.run(); + } + + } + } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java index 3c2e99f420..c99eb1aa59 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java @@ -195,14 +195,11 @@ public boolean isActive() { // we can update eagerly updateActive(); } else { - // we should update it eventually - Bukkit.getScheduler().callSyncMethod( - plugin, - () -> { - updateActive(); - return null; - } - ); + // Folia-safe synchronization for eventually consistent updates. + TaskManager.taskManager().sync(() -> { + updateActive(); + return null; + }); } return active; } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java index adf0ee8465..3d5a639394 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitEntity.java @@ -19,8 +19,8 @@ package com.sk89q.worldedit.bukkit; -import com.fastasyncworldedit.core.util.TaskManager; import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter; +import com.sk89q.worldedit.bukkit.util.FoliaEntityTask; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.entity.Player; @@ -83,11 +83,10 @@ public Location getLocation() { @Override public boolean setLocation(Location location) { org.bukkit.entity.Entity entity = entityRef.get(); - if (entity != null) { - return entity.teleport(BukkitAdapter.adapt(location)); - } else { + if (entity == null) { return false; } + return FoliaEntityTask.execute(entity, () -> entity.teleport(BukkitAdapter.adapt(location))); } @Override @@ -113,18 +112,17 @@ public BaseEntity getState() { public boolean remove() { // synchronize the whole method, not just the remove operation as we always need to synchronize and // can make sure the entity reference was not invalidated in the few milliseconds between the next available tick (lol) - return TaskManager.taskManager().sync(() -> { - org.bukkit.entity.Entity entity = entityRef.get(); - if (entity != null) { - try { - entity.remove(); - } catch (UnsupportedOperationException e) { - return false; - } - return entity.isDead(); - } else { - return true; + org.bukkit.entity.Entity entity = entityRef.get(); + if (entity == null) { + return true; + } + return FoliaEntityTask.execute(entity, () -> { + try { + entity.remove(); + } catch (UnsupportedOperationException e) { + return false; } + return entity.isDead(); }); } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java index a7a680b68b..e4dd4efb67 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitPlayer.java @@ -21,7 +21,7 @@ import com.fastasyncworldedit.core.configuration.Caption; import com.fastasyncworldedit.core.configuration.Settings; -import com.fastasyncworldedit.core.util.TaskManager; +import com.sk89q.worldedit.bukkit.util.FoliaEntityTask; import com.sk89q.util.StringUtil; import com.sk89q.wepif.VaultResolver; import com.sk89q.worldedit.WorldEdit; @@ -66,12 +66,16 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; public class BukkitPlayer extends AbstractPlayerActor { @@ -163,7 +167,7 @@ public String getDisplayName() { public void giveItem(BaseItemStack itemStack) { final PlayerInventory inv = player.getInventory(); ItemStack newItem = BukkitAdapter.adapt(itemStack); - TaskManager.taskManager().sync(() -> { + Runnable giveItem = () -> { if (itemStack.getType().id().equalsIgnoreCase(WorldEdit.getInstance().getConfiguration().wandItem)) { inv.remove(newItem); } @@ -184,9 +188,29 @@ public void giveItem(BaseItemStack itemStack) { } } player.updateInventory(); + }; + if (canAccessInventoryOnCurrentThread()) { + giveItem.run(); + return; + } + FoliaEntityTask.execute(player, () -> { + giveItem.run(); return null; }); } + + private boolean canAccessInventoryOnCurrentThread() { + if (Bukkit.isPrimaryThread()) { + return true; + } + try { + Method isOwnedByCurrentRegion = Bukkit.getServer().getClass() + .getMethod("isOwnedByCurrentRegion", org.bukkit.entity.Entity.class); + return (boolean) isOwnedByCurrentRegion.invoke(Bukkit.getServer(), player); + } catch (ReflectiveOperationException ignored) { + return false; + } + } //FAWE end @Deprecated @@ -242,14 +266,57 @@ public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { } org.bukkit.World finalWorld = world; //FAWE end - return TaskManager.taskManager().sync(() -> player.teleport(new Location( - finalWorld, - pos.x(), - pos.y(), - pos.z(), - yaw, - pitch - ))); + Location targetLocation = new Location(finalWorld, pos.x(), pos.y(), pos.z(), yaw, pitch); + + CompletableFuture asyncTeleport = tryTeleportAsync(targetLocation); + if (asyncTeleport != null) { + try { + return Boolean.TRUE.equals(asyncTeleport.get()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + + return FoliaEntityTask.execute(player, () -> player.teleport(targetLocation)); + } + + @Nullable + @SuppressWarnings("unchecked") + private CompletableFuture tryTeleportAsync(Location targetLocation) { + try { + for (Method method : player.getClass().getMethods()) { + if (!method.getName().equals("teleportAsync") || !Modifier.isPublic(method.getModifiers())) { + continue; + } + + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length == 0 || !Location.class.equals(parameterTypes[0])) { + continue; + } + + Object result; + if (parameterTypes.length == 1) { + result = method.invoke(player, targetLocation); + } else if (parameterTypes.length == 2 + && "org.bukkit.event.player.PlayerTeleportEvent$TeleportCause".equals(parameterTypes[1].getName())) { + Object commandCause = Enum.valueOf((Class) parameterTypes[1].asSubclass(Enum.class), "COMMAND"); + result = method.invoke(player, targetLocation, commandCause); + } else { + continue; + } + + if (result instanceof CompletableFuture) { + return (CompletableFuture) result; + } + } + + return null; + } catch (ReflectiveOperationException ignored) { + return null; + } } @Override @@ -269,7 +336,10 @@ public GameMode getGameMode() { @Override public void setGameMode(GameMode gameMode) { - player.setGameMode(org.bukkit.GameMode.valueOf(gameMode.id().toUpperCase(Locale.ROOT))); + FoliaEntityTask.execute(player, () -> { + player.setGameMode(org.bukkit.GameMode.valueOf(gameMode.id().toUpperCase(Locale.ROOT))); + return null; + }); } @Override @@ -363,7 +433,7 @@ public com.sk89q.worldedit.util.Location getLocation() { @Override public boolean setLocation(com.sk89q.worldedit.util.Location location) { - return player.teleport(BukkitAdapter.adapt(location)); + return FoliaEntityTask.execute(player, () -> player.teleport(BukkitAdapter.adapt(location))); } @Override diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java index 73a7421c34..1cd6fcfbfa 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java @@ -56,6 +56,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; @@ -135,7 +136,35 @@ public void reload() { @Override public int schedule(long delay, long period, Runnable task) { - return Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); + try { + return Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, task, delay, period); + } catch (UnsupportedOperationException ignored) { + scheduleFoliaRepeatingTask(task, delay, period); + return -1; + } + } + + private void scheduleFoliaRepeatingTask(final Runnable task, final long delay, final long period) { + try { + Method getGlobalRegionScheduler = server.getClass().getMethod("getGlobalRegionScheduler"); + Object globalRegionScheduler = getGlobalRegionScheduler.invoke(server); + Method runAtFixedRate = globalRegionScheduler.getClass().getMethod( + "runAtFixedRate", + org.bukkit.plugin.Plugin.class, + java.util.function.Consumer.class, + long.class, + long.class + ); + runAtFixedRate.invoke( + globalRegionScheduler, + plugin, + (java.util.function.Consumer) ignoredTask -> task.run(), + Math.max(1L, delay), + Math.max(1L, period) + ); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Unable to schedule repeating sync task on Folia", e); + } } @Override diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java index d0708887b0..8cddf73827 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java @@ -457,7 +457,11 @@ public void onDisable() { if (config != null) { config.unload(); } - this.getServer().getScheduler().cancelTasks(this); + try { + this.getServer().getScheduler().cancelTasks(this); + } catch (UnsupportedOperationException ignored) { + // Folia does not support legacy scheduler cancellation for all task types. + } } /** diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/util/FoliaEntityTask.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/util/FoliaEntityTask.java new file mode 100644 index 0000000000..2c0fa5d52a --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/util/FoliaEntityTask.java @@ -0,0 +1,102 @@ +package com.sk89q.worldedit.bukkit.util; + +import com.sk89q.worldedit.bukkit.WorldEditPlugin; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Utility methods to execute Bukkit entity interactions on a Folia-safe thread. + */ +public final class FoliaEntityTask { + + private FoliaEntityTask() { + } + + public static T execute(final Entity entity, final Supplier supplier) { + if (isEntityThread(entity)) { + return supplier.get(); + } + + CompletableFuture future = new CompletableFuture<>(); + Plugin plugin = WorldEditPlugin.getInstance(); + Runnable run = () -> { + try { + future.complete(supplier.get()); + } catch (Throwable t) { + future.completeExceptionally(t); + } + }; + + if (!scheduleOnEntityScheduler(entity, plugin, run, future)) { + com.fastasyncworldedit.core.util.TaskManager.taskManager().task(run); + } + + try { + return future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + + private static boolean scheduleOnEntityScheduler( + final Entity entity, + final Plugin plugin, + final Runnable run, + final CompletableFuture future + ) { + try { + Method getScheduler = entity.getClass().getMethod("getScheduler"); + Object scheduler = getScheduler.invoke(entity); + + try { + Method execute = scheduler.getClass().getMethod("execute", Plugin.class, Runnable.class, Runnable.class, long.class); + boolean scheduled = (boolean) execute.invoke(scheduler, plugin, run, (Runnable) () -> { + if (!future.isDone()) { + future.completeExceptionally(new IllegalStateException("Entity scheduler retired before execution")); + } + }, 1L); + return scheduled; + } catch (NoSuchMethodException ignored) { + Method runMethod = scheduler.getClass().getMethod( + "run", + Plugin.class, + Consumer.class, + Runnable.class, + long.class + ); + runMethod.invoke(scheduler, plugin, (Consumer) ignoredTask -> run.run(), (Runnable) () -> { + if (!future.isDone()) { + future.completeExceptionally(new IllegalStateException("Entity scheduler retired before execution")); + } + }, 1L); + return true; + } + } catch (ReflectiveOperationException ignored) { + return false; + } + } + + private static boolean isEntityThread(final Entity entity) { + if (Bukkit.isPrimaryThread()) { + return true; + } + try { + Method isOwnedByCurrentRegion = Bukkit.getServer().getClass() + .getMethod("isOwnedByCurrentRegion", Entity.class); + return Boolean.TRUE.equals(isOwnedByCurrentRegion.invoke(Bukkit.getServer(), entity)); + } catch (ReflectiveOperationException ignored) { + return false; + } + } + +} diff --git a/worldedit-bukkit/src/main/resources/plugin.yml b/worldedit-bukkit/src/main/resources/plugin.yml index 86f95e9ef7..61aa1137f3 100644 --- a/worldedit-bukkit/src/main/resources/plugin.yml +++ b/worldedit-bukkit/src/main/resources/plugin.yml @@ -3,6 +3,7 @@ main: com.sk89q.worldedit.bukkit.WorldEditPlugin version: "${internalVersion}" load: STARTUP api-version: 1.20 +folia-supported: true softdepend: [ Vault ] provides: [ WorldEdit ] website: https://modrinth.com/plugin/fastasyncworldedit/ diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java index 57a61d1f0e..3b496c86ee 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java @@ -207,7 +207,14 @@ public static void setupInjector() { } public static boolean isMainThread() { - return instance == null || instance.thread == Thread.currentThread(); + if (instance == null) { + return true; + } + TaskManager taskManager = TaskManager.IMP; + if (taskManager != null) { + return taskManager.isMainThread(); + } + return instance.thread == Thread.currentThread(); } /** diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java index dc2ba45059..b4c4c7dbb2 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java @@ -3,7 +3,6 @@ import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.configuration.Settings; import com.fastasyncworldedit.core.internal.exception.FaweClipboardVersionMismatchException; -import com.fastasyncworldedit.core.internal.io.ByteBufferInputStream; import com.fastasyncworldedit.core.jnbt.streamer.IntValueReader; import com.fastasyncworldedit.core.math.IntTriple; import com.fastasyncworldedit.core.nbt.FaweCompoundTag; @@ -146,7 +145,7 @@ public DiskOptimizedClipboard(BlockVector3 dimensions, File file) { long fileLength = (long) (getVolume() << 1) + (long) headerSize; braf.setLength(0); braf.setLength(fileLength); - this.nbtBytesRemaining = Integer.MAX_VALUE - (int) fileLength; + this.nbtBytesRemaining = nbtBytesRemaining(fileLength); init(); // write getLength() etc byteBuffer.putChar(2, (char) (VERSION)); @@ -189,7 +188,7 @@ public DiskOptimizedClipboard(File file, int versionOverride) { this.file = file; this.braf = new RandomAccessFile(file, "rw"); braf.setLength(file.length()); - this.nbtBytesRemaining = Integer.MAX_VALUE - (int) file.length(); + this.nbtBytesRemaining = nbtBytesRemaining(file.length()); init(); int biomeLength = ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1); @@ -248,42 +247,44 @@ private static BlockVector3 readSize(File file, int expectedVersion) { } private void loadNBTFromFileFooter(int nbtCount, int entitiesCount, long biomeLength) throws IOException { - int biomeBlocksLength = headerSize + (getVolume() << 1) + (hasBiomes ? (int) biomeLength : 0); - MappedByteBuffer tmp = fileChannel.map(FileChannel.MapMode.READ_ONLY, biomeBlocksLength, braf.length()); - try (NBTInputStream nbtIS = new NBTInputStream(MainUtil.getCompressedIS(new ByteBufferInputStream(tmp)))) { - Iterator iter = nbtIS.toIterator(); - while (nbtCount > 0 && iter.hasNext()) { // TileEntities are stored "before" entities - LinCompoundTag tag = iter.next().toLinTag(); - int x = tag.getTag("x", LinTagType.intTag()).valueAsInt(); - int y = tag.getTag("y", LinTagType.intTag()).valueAsInt(); - int z = tag.getTag("z", LinTagType.intTag()).valueAsInt(); - IntTriple pos = new IntTriple(x, y, z); - nbtMap.put(pos, FaweCompoundTag.of(tag)); - nbtCount--; - } - while (entitiesCount > 0 && iter.hasNext()) { - CompoundTag tag = iter.next(); - Tag posTag = tag.getValue().get("Pos"); - if (posTag == null) { - LOGGER.warn("Missing pos tag: {}", tag); - return; + long biomeBlocksLength = headerSize + ((long) getVolume() << 1) + (hasBiomes ? biomeLength : 0L); + try (FileInputStream fis = new FileInputStream(file)) { + fis.skipNBytes(biomeBlocksLength); + try (NBTInputStream nbtIS = new NBTInputStream(MainUtil.getCompressedIS(fis))) { + Iterator iter = nbtIS.toIterator(); + while (nbtCount > 0 && iter.hasNext()) { // TileEntities are stored "before" entities + LinCompoundTag tag = iter.next().toLinTag(); + int x = tag.getTag("x", LinTagType.intTag()).valueAsInt(); + int y = tag.getTag("y", LinTagType.intTag()).valueAsInt(); + int z = tag.getTag("z", LinTagType.intTag()).valueAsInt(); + IntTriple pos = new IntTriple(x, y, z); + nbtMap.put(pos, FaweCompoundTag.of(tag)); + nbtCount--; + } + while (entitiesCount > 0 && iter.hasNext()) { + CompoundTag tag = iter.next(); + Tag posTag = tag.getValue().get("Pos"); + if (posTag == null) { + LOGGER.warn("Missing pos tag: {}", tag); + return; + } + List pos = (List) posTag.getValue(); + double x = pos.get(0).getValue(); + double y = pos.get(1).getValue(); + double z = pos.get(2).getValue(); + BaseEntity entity = new BaseEntity(tag); + BlockArrayClipboard.ClipboardEntity clipboardEntity = new BlockArrayClipboard.ClipboardEntity( + this, + x, + y, + z, + 0f, + 0f, + entity + ); + this.entities.add(clipboardEntity); + entitiesCount--; } - List pos = (List) posTag.getValue(); - double x = pos.get(0).getValue(); - double y = pos.get(1).getValue(); - double z = pos.get(2).getValue(); - BaseEntity entity = new BaseEntity(tag); - BlockArrayClipboard.ClipboardEntity clipboardEntity = new BlockArrayClipboard.ClipboardEntity( - this, - x, - y, - z, - 0f, - 0f, - entity - ); - this.entities.add(clipboardEntity); - entitiesCount--; } } catch (Exception e) { e.printStackTrace(); @@ -329,8 +330,22 @@ private void init() throws IOException { throw e; } } - this.byteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, braf.length()); + this.byteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, getMappedLength()); + } + } + + private long getMappedLength() throws IOException { + long blockLength = headerSize + ((long) getVolume() << 1); + long biomeLength = (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1); + long maxDataLength = blockLength + (canHaveBiomes ? biomeLength : 0L); + return Math.min(braf.length(), maxDataLength); + } + + private int nbtBytesRemaining(long fileLength) { + if (fileLength >= Integer.MAX_VALUE) { + return 0; } + return Integer.MAX_VALUE - (int) fileLength; } private boolean initBiome() { @@ -347,7 +362,7 @@ private boolean initBiome() { long length = headerSize + ((long) getVolume() << 1) + (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1); this.braf.setLength(length); - this.nbtBytesRemaining = Integer.MAX_VALUE - (int) length; + this.nbtBytesRemaining = nbtBytesRemaining(length); init(); } catch (IOException e) { e.printStackTrace(); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/TaskManager.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/TaskManager.java index 176e02673b..3bb3ffab1f 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/TaskManager.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/TaskManager.java @@ -77,6 +77,12 @@ public static TaskManager taskManager() { */ public abstract void task(@Nonnull final Runnable runnable); + /** + * Returns whether the current thread is an execution context that should be treated as synchronous for platform + * operations. + */ + public abstract boolean isMainThread(); + /** * Get the public ForkJoinPool. * - ONLY SUBMIT SHORT LIVED TASKS
diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index a4843811ed..306f75fd27 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -1,5 +1,5 @@ { - "prefix": "&8(&4&lFAWE&8)&7 {0}", + "prefix": "&8(&5&lFAWE&8)&7 {0}", "fawe.worldedit.history.find.element": "&2{0} {1} &7ago &3{2}m &6{3} &c/{4}", "fawe.worldedit.history.find.element.more": " - Changes: {0}\n - Bounds: {1} -> {2}\n - Extra: {3}\n - Size on Disk: {4}", "fawe.worldedit.history.find.hover": "{0} blocks changed, click for more info",