From 6f93f3174a6be8caaa55c2e535cdd558fb71ddfd Mon Sep 17 00:00:00 2001 From: SamB440 Date: Sun, 10 Oct 2021 15:09:29 +0100 Subject: [PATCH] Added per-region biomes/fog --- .../rpgregions/effects/RegionEffect.java | 16 + .../effects/RegionEffectRegistry.java | 13 +- rpgregions/build.gradle | 6 +- .../islandearth/rpgregions/RPGRegions.java | 2 + .../rpgregions/effects/FogEffect.java | 135 +++++ .../effects/fog/nms/biome/Biome.java | 10 + .../nms/biome/BiomeBaseWrapper_1_17R1.java | 68 +++ .../effects/fog/tinyprotocol/Reflection.java | 403 ++++++++++++++ .../fog/tinyprotocol/TinyProtocol.java | 519 ++++++++++++++++++ .../rpgregions/gui/AddRegionElementGUI.java | 2 +- .../rpgregions/listener/MoveListener.java | 15 + .../managers/RPGRegionsManagers.java | 29 + 12 files changed, 1214 insertions(+), 4 deletions(-) create mode 100644 rpgregions/src/main/java/net/islandearth/rpgregions/effects/FogEffect.java create mode 100644 rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/Biome.java create mode 100644 rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/BiomeBaseWrapper_1_17R1.java create mode 100644 rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/Reflection.java create mode 100644 rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/TinyProtocol.java diff --git a/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffect.java b/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffect.java index 26bee81..8a6582d 100644 --- a/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffect.java +++ b/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffect.java @@ -4,6 +4,7 @@ import net.islandearth.rpgregions.api.IRPGRegionsAPI; import net.islandearth.rpgregions.gui.IGuiEditable; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; @@ -30,6 +31,21 @@ public abstract class RegionEffect implements IGuiEditable { */ public abstract void effect(Player player); + /** + * Called when a player exits all regions and effects should be entirely removed. + * @param player + */ + public void uneffect(Player player) {} + + /** + * Gets the required Minecraft version for this effect to function. + * @return minecraft version + */ + @Nullable + public String getRequiredVersion() { + return null; + } + /** * Whether the items are required to be worn in armour slots * @return whether items should be worn in armour slots diff --git a/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffectRegistry.java b/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffectRegistry.java index dc84e9c..dd64585 100644 --- a/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffectRegistry.java +++ b/api/src/main/java/net/islandearth/rpgregions/effects/RegionEffectRegistry.java @@ -2,6 +2,7 @@ package net.islandearth.rpgregions.effects; import net.islandearth.rpgregions.api.IRPGRegionsAPI; import net.islandearth.rpgregions.managers.registry.RPGRegionsRegistry; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.jetbrains.annotations.Nullable; @@ -14,8 +15,16 @@ public final class RegionEffectRegistry extends RPGRegionsRegistry @Override public @Nullable RegionEffect getNew(Class clazz, IRPGRegionsAPI plugin, Object... data) { try { - Constructor constructor = clazz.getConstructor(IRPGRegionsAPI.class); - return (RegionEffect) constructor.newInstance(plugin); + try { + Constructor constructor = clazz.getConstructor(IRPGRegionsAPI.class); + RegionEffect effect = (RegionEffect) constructor.newInstance(plugin); + if (effect.getRequiredVersion() != null && !Bukkit.getVersion().contains(effect.getRequiredVersion())) { + return null; + } + return effect; + } catch (NoClassDefFoundError e) { + return null; + } } catch (ReflectiveOperationException e) { e.printStackTrace(); } diff --git a/rpgregions/build.gradle b/rpgregions/build.gradle index a7a3015..bcdee3b 100644 --- a/rpgregions/build.gradle +++ b/rpgregions/build.gradle @@ -6,6 +6,10 @@ plugins { group = pluginGroup version = pluginVersion +repositories { + maven { url = "https://repo.codemc.io/repository/nms/" } +} + dependencies { testImplementation group: 'junit', name: 'junit', version: '4.5' testImplementation 'com.github.seeseemelk:MockBukkit-v1.17-SNAPSHOT' @@ -30,7 +34,7 @@ dependencies { exclude group: 'org.spigotmc' } //compileOnly 'com.zaxxer:HikariCP:2.4.1' // IMPLEMENTED VIA LIBRARIES - database - compileOnly 'org.spigotmc:spigot-api:1.17-R0.1-SNAPSHOT' // spigot + compileOnly 'org.spigotmc:spigot:1.17.1-R0.1-SNAPSHOT' // spigot compileOnly 'me.clip:placeholderapi:2.10.4' // PAPI compileOnly ('com.github.MilkBowl:VaultAPI:1.7') { // vault exclude group: 'org.bukkit' diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/RPGRegions.java b/rpgregions/src/main/java/net/islandearth/rpgregions/RPGRegions.java index 90f6bba..42c98aa 100644 --- a/rpgregions/src/main/java/net/islandearth/rpgregions/RPGRegions.java +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/RPGRegions.java @@ -17,6 +17,7 @@ import net.islandearth.rpgregions.api.integrations.rpgregions.region.RPGRegionsR import net.islandearth.rpgregions.commands.DiscoveriesCommand; import net.islandearth.rpgregions.commands.RPGRegionsCommand; import net.islandearth.rpgregions.commands.RPGRegionsDebugCommand; +import net.islandearth.rpgregions.effects.FogEffect; import net.islandearth.rpgregions.effects.PotionRegionEffect; import net.islandearth.rpgregions.effects.RegionEffect; import net.islandearth.rpgregions.effects.RegionEffectRegistry; @@ -254,6 +255,7 @@ public final class RPGRegions extends JavaPlugin implements IRPGRegionsAPI, Lang return; } registry.register(PotionRegionEffect.class); + registry.register(FogEffect.class); //registry.register(VanishEffect.class); //TODO } diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/effects/FogEffect.java b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/FogEffect.java new file mode 100644 index 0000000..621e5c0 --- /dev/null +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/FogEffect.java @@ -0,0 +1,135 @@ +package net.islandearth.rpgregions.effects; + +import com.mojang.serialization.Lifecycle; +import net.islandearth.rpgregions.api.IRPGRegionsAPI; +import net.islandearth.rpgregions.effects.fog.nms.biome.BiomeBaseWrapper_1_17R1; +import net.islandearth.rpgregions.gui.GuiEditable; +import net.minecraft.core.IRegistry; +import net.minecraft.core.IRegistryWritable; +import net.minecraft.network.protocol.game.PacketPlayOutMapChunk; +import net.minecraft.resources.MinecraftKey; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.world.level.biome.BiomeBase; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_17_R1.CraftChunk; +import org.bukkit.craftbukkit.v1_17_R1.CraftServer; +import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class FogEffect extends RegionEffect { + + @GuiEditable("Sky Colour") private final String skyColour; + @GuiEditable("Water Colour") private final String waterColour; + @GuiEditable("Water Fog Colour") private final String waterFogColour; + @GuiEditable("Fog Colour") private final String fogColour; + @GuiEditable("Grass Colour") private final String grassColour; + @GuiEditable("Foilage Colour") private final String foilageColour; + + private transient BiomeBase biomeBase; + private transient int biomeId; + private transient List effect; + + public FogEffect(IRPGRegionsAPI api) { + super(api); + this.skyColour = "FF0000"; + this.waterColour = "FF0000"; + this.waterFogColour = "FF0000"; + this.fogColour = "FF0000"; + this.grassColour = "FF0000"; + this.foilageColour = "FF0000"; + generateBiomes(); + } + + public void generateBiomes() { + ResourceKey key = ResourceKey.a(IRegistry.aO, new MinecraftKey(UUID.randomUUID().toString())); + BiomeBase biomeBase = new BiomeBaseWrapper_1_17R1() + .build(fogColour, + waterColour, + waterFogColour, + skyColour, + grassColour.equals("-1") ? null : grassColour, + foilageColour.equals("-1") ? null : foilageColour); + + DedicatedServer ds = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); + IRegistryWritable rw = ds.getCustomRegistry().b(IRegistry.aO); + rw.a(key, biomeBase, Lifecycle.stable()); + this.biomeId = ds.getCustomRegistry().d(IRegistry.aO).getId(biomeBase); + this.biomeBase = biomeBase; + } + + @Override + public void effect(Player player) { + if (this.effect == null) this.effect = new ArrayList<>(); + + if (!effect.contains(player.getUniqueId())) { + effect.add(player.getUniqueId()); + for (Chunk chunk : getChunkAround(player.getLocation().getChunk(), Bukkit.getServer().getViewDistance())) { + net.minecraft.world.level.chunk.Chunk c = ((CraftChunk)chunk).getHandle(); + ((CraftPlayer) player).getHandle().b.sendPacket(new PacketPlayOutMapChunk(c)); + } + } + } + + @Override + public void uneffect(Player player) { + if (this.effect == null) this.effect = new ArrayList<>(); + + if (effect.contains(player.getUniqueId())) { + effect.remove(player.getUniqueId()); + for (Chunk chunk : getChunkAround(player.getLocation().getChunk(), Bukkit.getServer().getViewDistance())) { + net.minecraft.world.level.chunk.Chunk c = ((CraftChunk)chunk).getHandle(); + ((CraftPlayer) player).getHandle().b.sendPacket(new PacketPlayOutMapChunk(c)); + } + } + } + + public Object onSendChunk(PacketPlayOutMapChunk packet, Player target) { + if (effect != null && effect.contains(target.getUniqueId())) { + int[] biomeIDs = packet.h(); + Arrays.fill(biomeIDs, biomeId); + try { + Field field = packet.getClass().getDeclaredField("f"); + field.setAccessible(true); + field.set(packet, biomeIDs); + return packet; + } catch (ReflectiveOperationException e) { + e.printStackTrace(); + } + } + return packet; + } + + @Override + public String getName() { + return "Fog"; + } + + private Collection getChunkAround(Chunk origin, int radius) { + World world = origin.getWorld(); + + int length = (radius * 2) + 1; + Set chunks = new HashSet<>(length * length); + + int cX = origin.getX(); + int cZ = origin.getZ(); + + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + if (world.isChunkLoaded(cX + x, cZ + z)) chunks.add(world.getChunkAt(cX + x, cZ + z)); + } + } + return chunks; + } +} diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/Biome.java b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/Biome.java new file mode 100644 index 0000000..9807f28 --- /dev/null +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/Biome.java @@ -0,0 +1,10 @@ +package net.islandearth.rpgregions.effects.fog.nms.biome; + +import net.minecraft.world.level.biome.BiomeBase; + +public interface Biome { + + BiomeBase build(String fogColor, String waterColor, String waterFogColor, String skyColor, String grassColor, String foliageColor); + + BiomeBase build(String fogColor, String waterColor, String waterFogColor, String skyColor); +} diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/BiomeBaseWrapper_1_17R1.java b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/BiomeBaseWrapper_1_17R1.java new file mode 100644 index 0000000..cc15ec3 --- /dev/null +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/nms/biome/BiomeBaseWrapper_1_17R1.java @@ -0,0 +1,68 @@ +package net.islandearth.rpgregions.effects.fog.nms.biome; + +import net.minecraft.data.worldgen.BiomeDecoratorGroups; +import net.minecraft.data.worldgen.WorldGenSurfaceComposites; +import net.minecraft.world.level.biome.BiomeBase; +import net.minecraft.world.level.biome.BiomeFog; +import net.minecraft.world.level.biome.BiomeSettingsGeneration; +import net.minecraft.world.level.biome.BiomeSettingsMobs; +import net.minecraft.world.level.levelgen.WorldGenStage; + +/** + * Thanks to ProdigySky for this + * @author cocoraid + */ +public class BiomeBaseWrapper_1_17R1 implements Biome { + + private String fogColor, waterColor, waterFogColor, skyColor; + private String grassColor,foliageColor; + + @Override + public BiomeBase build(String fogColor, String waterColor, String waterFogColor, String skyColor) { + return build(fogColor,waterColor,waterFogColor,skyColor,null,null); + } + + @Override + public BiomeBase build(String fogColor, String waterColor, String waterFogColor, String skyColor,String grassColor, String foliageColor) { + this.fogColor = fogColor; + this.waterColor = waterColor; + this.waterFogColor = waterFogColor; + this.skyColor = skyColor; + this.grassColor = grassColor; + this.foliageColor = foliageColor; + return build(); + } + + public BiomeBase build() { + //void generation + BiomeSettingsGeneration.a gen = (new BiomeSettingsGeneration.a()).a(WorldGenSurfaceComposites.p); + gen.a(WorldGenStage.Decoration.j, BiomeDecoratorGroups.W); + + BiomeFog.a biomeFogCodec = new BiomeFog.a() + .a(Integer.parseInt(fogColor, 16)) //fog color + .b(Integer.parseInt(waterColor, 16)) //water color + .c(Integer.parseInt(waterFogColor, 16)) //water fog color + .d(Integer.parseInt(skyColor, 16)); //skycolor + //.e() //foliage color (leaves, fines and more) + //.f() //grass blocks color + //.a(BiomeParticle) + //a(Music) + + if(foliageColor != null) + biomeFogCodec.e(Integer.parseInt(foliageColor, 16)); + if(grassColor != null) + biomeFogCodec.f(Integer.parseInt(grassColor, 16)); + + return new BiomeBase.a() + .a(BiomeBase.Precipitation.a) //none + .a(BiomeBase.Geography.a) //none + .a(0F) //depth ocean or not // var3 ? -1.8F : -1.0F + .b(0F) //scale Lower values produce flatter terrain + .c(0F) //temperature + .d(0F) //maybe important, foliage and grass color + .a(biomeFogCodec.a()) //biomefog + .a(BiomeSettingsMobs.c) //same as void biome + .a(gen.a()) //same as void biome + .a(); + } +} diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/Reflection.java b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/Reflection.java new file mode 100644 index 0000000..865a451 --- /dev/null +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/Reflection.java @@ -0,0 +1,403 @@ +package net.islandearth.rpgregions.effects.fog.tinyprotocol; + +import org.bukkit.Bukkit; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An utility class that simplifies reflection in Bukkit plugins. + * + * @author Kristian + */ +public final class Reflection { + /** + * An interface for invoking a specific constructor. + */ + public interface ConstructorInvoker { + /** + * Invoke a constructor for a specific class. + * + * @param arguments - the arguments to pass to the constructor. + * @return The constructed object. + */ + public Object invoke(Object... arguments); + } + + /** + * An interface for invoking a specific method. + */ + public interface MethodInvoker { + /** + * Invoke a method on a specific target object. + * + * @param target - the target object, or NULL for a static method. + * @param arguments - the arguments to pass to the method. + * @return The return value, or NULL if is void. + */ + public Object invoke(Object target, Object... arguments); + } + + /** + * An interface for retrieving the field content. + * + * @param - field type. + */ + public interface FieldAccessor { + /** + * Retrieve the content of a field. + * + * @param target - the target object, or NULL for a static field. + * @return The value of the field. + */ + public T get(Object target); + + /** + * Set the content of a field. + * + * @param target - the target object, or NULL for a static field. + * @param value - the new value of the field. + */ + public void set(Object target, Object value); + + /** + * Determine if the given object has this field. + * + * @param target - the object to test. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean hasField(Object target); + } + + // Deduce the net.minecraft.server.v* package + private static String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName(); + private static String NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server"); + private static String VERSION = OBC_PREFIX.replace("org.bukkit.craftbukkit", "").replace(".", ""); + + // Variable replacement + private static Pattern MATCH_VARIABLE = Pattern.compile("\\{([^\\}]+)\\}"); + + private Reflection() { + // Seal class + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param target - the target type. + * @param name - the name of the field, or NULL to ignore. + * @param fieldType - a compatible field type. + * @return The field accessor. + */ + public static FieldAccessor getField(Class target, String name, Class fieldType) { + return getField(target, name, fieldType, 0); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param name - the name of the field, or NULL to ignore. + * @param fieldType - a compatible field type. + * @return The field accessor. + */ + public static FieldAccessor getField(String className, String name, Class fieldType) { + return getField(getClass(className), name, fieldType, 0); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param target - the target type. + * @param fieldType - a compatible field type. + * @param index - the number of compatible fields to skip. + * @return The field accessor. + */ + public static FieldAccessor getField(Class target, Class fieldType, int index) { + return getField(target, null, fieldType, index); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param fieldType - a compatible field type. + * @param index - the number of compatible fields to skip. + * @return The field accessor. + */ + public static FieldAccessor getField(String className, Class fieldType, int index) { + return getField(getClass(className), fieldType, index); + } + + // Common method + private static FieldAccessor getField(Class target, String name, Class fieldType, int index) { + for (final Field field : target.getDeclaredFields()) { + if ((name == null || field.getName().equals(name)) && fieldType.isAssignableFrom(field.getType()) && index-- <= 0) { + field.setAccessible(true); + + // A function for retrieving a specific field value + return new FieldAccessor() { + + @Override + @SuppressWarnings("unchecked") + public T get(Object target) { + try { + return (T) field.get(target); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access reflection.", e); + } + } + + @Override + public void set(Object target, Object value) { + try { + field.set(target, value); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access reflection.", e); + } + } + + @Override + public boolean hasField(Object target) { + // target instanceof DeclaringClass + return field.getDeclaringClass().isAssignableFrom(target.getClass()); + } + }; + } + } + + // Search in parent classes + if (target.getSuperclass() != null) + return getField(target.getSuperclass(), name, fieldType, index); + + throw new IllegalArgumentException("Cannot find field with type " + fieldType); + } + + /** + * Search for the first publicly and privately defined method of the given name and parameter count. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getMethod(String className, String methodName, Class... params) { + return getTypedMethod(getClass(className), methodName, null, params); + } + + /** + * Search for the first publicly and privately defined method of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getMethod(Class clazz, String methodName, Class... params) { + return getTypedMethod(clazz, methodName, null, params); + } + + /** + * Search for the first publicly and privately defined method of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param methodName - the method name, or NULL to skip. + * @param returnType - the expected return type, or NULL to ignore. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { + for (final Method method : clazz.getDeclaredMethods()) { + if ((methodName == null || method.getName().equals(methodName)) + && (returnType == null || method.getReturnType().equals(returnType)) + && Arrays.equals(method.getParameterTypes(), params)) { + method.setAccessible(true); + + return new MethodInvoker() { + + @Override + public Object invoke(Object target, Object... arguments) { + try { + return method.invoke(target, arguments); + } catch (Exception e) { + throw new RuntimeException("Cannot invoke method " + method, e); + } + } + + }; + } + } + + // Search in every superclass + if (clazz.getSuperclass() != null) + return getMethod(clazz.getSuperclass(), methodName, params); + + throw new IllegalStateException(String.format("Unable to find method %s (%s).", methodName, Arrays.asList(params))); + } + + /** + * Search for the first publically and privately defined constructor of the given name and parameter count. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param params - the expected parameters. + * @return An object that invokes this constructor. + * @throws IllegalStateException If we cannot find this method. + */ + public static ConstructorInvoker getConstructor(String className, Class... params) { + return getConstructor(getClass(className), params); + } + + /** + * Search for the first publically and privately defined constructor of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param params - the expected parameters. + * @return An object that invokes this constructor. + * @throws IllegalStateException If we cannot find this method. + */ + public static ConstructorInvoker getConstructor(Class clazz, Class... params) { + for (final Constructor constructor : clazz.getDeclaredConstructors()) { + if (Arrays.equals(constructor.getParameterTypes(), params)) { + constructor.setAccessible(true); + + return new ConstructorInvoker() { + + @Override + public Object invoke(Object... arguments) { + try { + return constructor.newInstance(arguments); + } catch (Exception e) { + throw new RuntimeException("Cannot invoke constructor " + constructor, e); + } + } + + }; + } + } + + throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); + } + + /** + * Retrieve a class from its full name, without knowing its type on compile time. + *

+ * This is useful when looking up fields by a NMS or OBC type. + *

+ * + * @see {@link #getClass()} for more information. + * @param lookupName - the class name with variables. + * @return The class. + */ + public static Class getUntypedClass(String lookupName) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Class clazz = (Class) getClass(lookupName); + return clazz; + } + + /** + * Retrieve a class from its full name. + *

+ * Strings enclosed with curly brackets - such as {TEXT} - will be replaced according to the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
VariableContent
{nms}Actual package name of net.minecraft.server.VERSION
{obc}Actual pacakge name of org.bukkit.craftbukkit.VERSION
{version}The current Minecraft package VERSION, if any.
+ * + * @param lookupName - the class name with variables. + * @return The looked up class. + * @throws IllegalArgumentException If a variable or class could not be found. + */ + public static Class getClass(String lookupName) { + return getCanonicalClass(expandVariables(lookupName)); + } + + /** + * Retrieve a class in the net.minecraft.server.VERSION.* package. + * + * @param name - the name of the class, excluding the package. + * @throws IllegalArgumentException If the class doesn't exist. + */ + public static Class getMinecraftClass(String name) { + return getCanonicalClass(NMS_PREFIX + "." + name); + } + + /** + * Retrieve a class in the org.bukkit.craftbukkit.VERSION.* package. + * + * @param name - the name of the class, excluding the package. + * @throws IllegalArgumentException If the class doesn't exist. + */ + public static Class getCraftBukkitClass(String name) { + return getCanonicalClass(OBC_PREFIX + "." + name); + } + + /** + * Retrieve a class by its canonical name. + * + * @param canonicalName - the canonical name. + * @return The class. + */ + private static Class getCanonicalClass(String canonicalName) { + try { + return Class.forName(canonicalName); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot find " + canonicalName, e); + } + } + + /** + * Expand variables such as "{nms}" and "{obc}" to their corresponding packages. + * + * @param name - the full name of the class. + * @return The expanded string. + */ + private static String expandVariables(String name) { + StringBuffer output = new StringBuffer(); + Matcher matcher = MATCH_VARIABLE.matcher(name); + + while (matcher.find()) { + String variable = matcher.group(1); + String replacement = ""; + + // Expand all detected variables + if ("nms".equalsIgnoreCase(variable)) + replacement = NMS_PREFIX; + else if ("obc".equalsIgnoreCase(variable)) + replacement = OBC_PREFIX; + else if ("version".equalsIgnoreCase(variable)) + replacement = VERSION; + else + throw new IllegalArgumentException("Unknown variable: " + variable); + + // Assume the expanded variables are all packages, and append a dot + if (replacement.length() > 0 && matcher.end() < name.length() && name.charAt(matcher.end()) != '.') + replacement += "."; + matcher.appendReplacement(output, Matcher.quoteReplacement(replacement)); + } + + matcher.appendTail(output); + return output.toString(); + } +} \ No newline at end of file diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/TinyProtocol.java b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/TinyProtocol.java new file mode 100644 index 0000000..6223756 --- /dev/null +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/effects/fog/tinyprotocol/TinyProtocol.java @@ -0,0 +1,519 @@ +package net.islandearth.rpgregions.effects.fog.tinyprotocol; + +import com.google.common.collect.Lists; +import com.google.common.collect.MapMaker; +import com.mojang.authlib.GameProfile; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +/** + * Represents a very tiny alternative to ProtocolLib. + *

+ * It now supports intercepting packets during login and status ping (such as OUT_SERVER_PING)! + * + * @author Kristian + */ +public abstract class TinyProtocol { + private static final AtomicInteger ID = new AtomicInteger(0); + + // Used in order to lookup a channel + private static final Reflection.MethodInvoker getPlayerHandle = Reflection.getMethod("{obc}.entity.CraftPlayer", "getHandle"); + + private static final Reflection.FieldAccessor getConnection = Reflection.getField("net.minecraft.server.level.EntityPlayer", "b", Object.class); + private static final Reflection.FieldAccessor getManager = Reflection.getField("net.minecraft.server.network.PlayerConnection", "a", Object.class); + private static final Reflection.FieldAccessor getChannel = Reflection.getField("net.minecraft.network.NetworkManager", Channel.class, 0); + + // Looking up ServerConnection + private static final Class minecraftServerClass = Reflection.getUntypedClass("net.minecraft.server.MinecraftServer"); + private static final Class serverConnectionClass = Reflection.getUntypedClass("net.minecraft.server.network.ServerConnection"); + private static final Reflection.FieldAccessor getMinecraftServer = Reflection.getField("{obc}.CraftServer", minecraftServerClass, 0); + private static final Reflection.FieldAccessor getServerConnection = Reflection.getField(minecraftServerClass, serverConnectionClass, 0); + private static final Reflection.FieldAccessor getNetworkMarkers = Reflection.getField(serverConnectionClass, "g", List.class); + + // Packets we have to intercept + private static final Class PACKET_LOGIN_IN_START = Reflection.getClass("net.minecraft.network.protocol.login.PacketLoginInStart"); + private static final Reflection.FieldAccessor getGameProfile = Reflection.getField(PACKET_LOGIN_IN_START, GameProfile.class, 0); + + // Speedup channel lookup + private Map channelLookup = new MapMaker().weakValues().makeMap(); + private Listener listener; + + // Channels that have already been removed + private Set uninjectedChannels = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); + + // List of network markers + private List networkManagers; + + // Injected channel handlers + private List serverChannels = Lists.newArrayList(); + private ChannelInboundHandlerAdapter serverChannelHandler; + private ChannelInitializer beginInitProtocol; + private ChannelInitializer endInitProtocol; + + // Current handler name + private String handlerName; + + protected volatile boolean closed; + protected Plugin plugin; + + /** + * Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future clients. + *

+ * You can construct multiple instances per plugin. + * + * @param plugin - the plugin. + */ + public TinyProtocol(final Plugin plugin) { + this.plugin = plugin; + + // Compute handler name + this.handlerName = getHandlerName(); + + // Prepare existing players + registerBukkitEvents(); + + try { + registerChannelHandler(); + registerPlayers(plugin); + } catch (IllegalArgumentException ex) { + // Damn you, late bind + plugin.getLogger().info("[TinyProtocol] Delaying server channel injection due to late bind."); + + new BukkitRunnable() { + @Override + public void run() { + registerChannelHandler(); + registerPlayers(plugin); + plugin.getLogger().info("[TinyProtocol] Late bind injection successful."); + } + }.runTask(plugin); + } + } + + private void createServerChannelHandler() { + // Handle connected channels + endInitProtocol = new ChannelInitializer() { + + @Override + protected void initChannel(Channel channel) throws Exception { + try { + // This can take a while, so we need to stop the main thread from interfering + synchronized (networkManagers) { + // Stop injecting channels + if (!closed) { + channel.eventLoop().submit(() -> injectChannelInternal(channel)); + } + } + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Cannot inject incomming channel " + channel, e); + } + } + + }; + + // This is executed before Minecraft's channel handler + beginInitProtocol = new ChannelInitializer() { + + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addLast(endInitProtocol); + } + + }; + + serverChannelHandler = new ChannelInboundHandlerAdapter() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = (Channel) msg; + + // Prepare to initialize ths channel + channel.pipeline().addFirst(beginInitProtocol); + ctx.fireChannelRead(msg); + } + + }; + } + + /** + * Register bukkit events. + */ + private void registerBukkitEvents() { + listener = new Listener() { + + @EventHandler(priority = EventPriority.LOWEST) + public final void onPlayerLogin(PlayerLoginEvent e) { + if (closed) + return; + + Channel channel = getChannel(e.getPlayer()); + + // Don't inject players that have been explicitly uninjected + if (!uninjectedChannels.contains(channel)) { + injectPlayer(e.getPlayer()); + } + } + + @EventHandler + public final void onPluginDisable(PluginDisableEvent e) { + if (e.getPlugin().equals(plugin)) { + close(); + } + } + + }; + + plugin.getServer().getPluginManager().registerEvents(listener, plugin); + } + + @SuppressWarnings("unchecked") + private void registerChannelHandler() { + Object mcServer = getMinecraftServer.get(Bukkit.getServer()); + Object serverConnection = getServerConnection.get(mcServer); + boolean looking = true; + + // We need to synchronize against this list + networkManagers = (List) getNetworkMarkers.get(serverConnection); + createServerChannelHandler(); + + // Find the correct list, or implicitly throw an exception + for (int i = 0; looking; i++) { + List list = Reflection.getField(serverConnection.getClass(), List.class, i).get(serverConnection); + + for (Object item : list) { + if (!ChannelFuture.class.isInstance(item)) + break; + + // Channel future that contains the server connection + Channel serverChannel = ((ChannelFuture) item).channel(); + + serverChannels.add(serverChannel); + serverChannel.pipeline().addFirst(serverChannelHandler); + looking = false; + } + } + } + + private void unregisterChannelHandler() { + if (serverChannelHandler == null) + return; + + for (Channel serverChannel : serverChannels) { + final ChannelPipeline pipeline = serverChannel.pipeline(); + + // Remove channel handler + serverChannel.eventLoop().execute(new Runnable() { + + @Override + public void run() { + try { + pipeline.remove(serverChannelHandler); + } catch (NoSuchElementException e) { + // That's fine + } + } + + }); + } + } + + private void registerPlayers(Plugin plugin) { + for (Player player : plugin.getServer().getOnlinePlayers()) { + injectPlayer(player); + } + } + + /** + * Invoked when the server is starting to send a packet to a player. + *

+ * Note that this is not executed on the main thread. + * + * @param receiver - the receiving player, NULL for early login/status packets. + * @param channel - the channel that received the packet. Never NULL. + * @param packet - the packet being sent. + * @return The packet to send instead, or NULL to cancel the transmission. + */ + public Object onPacketOutAsync(Player receiver, Channel channel, Object packet) { + return packet; + } + + /** + * Invoked when the server has received a packet from a given player. + *

+ * Use {@link Channel#remoteAddress()} to get the remote address of the client. + * + * @param sender - the player that sent the packet, NULL for early login/status packets. + * @param channel - channel that received the packet. Never NULL. + * @param packet - the packet being received. + * @return The packet to recieve instead, or NULL to cancel. + */ + public Object onPacketInAsync(Player sender, Channel channel, Object packet) { + return packet; + } + + /** + * Send a packet to a particular player. + *

+ * Note that {@link #onPacketOutAsync(Player, Channel, Object)} will be invoked with this packet. + * + * @param player - the destination player. + * @param packet - the packet to send. + */ + public void sendPacket(Player player, Object packet) { + sendPacket(getChannel(player), packet); + } + + /** + * Send a packet to a particular client. + *

+ * Note that {@link #onPacketOutAsync(Player, Channel, Object)} will be invoked with this packet. + * + * @param channel - client identified by a channel. + * @param packet - the packet to send. + */ + public void sendPacket(Channel channel, Object packet) { + channel.pipeline().writeAndFlush(packet); + } + + /** + * Pretend that a given packet has been received from a player. + *

+ * Note that {@link #onPacketInAsync(Player, Channel, Object)} will be invoked with this packet. + * + * @param player - the player that sent the packet. + * @param packet - the packet that will be received by the server. + */ + public void receivePacket(Player player, Object packet) { + receivePacket(getChannel(player), packet); + } + + /** + * Pretend that a given packet has been received from a given client. + *

+ * Note that {@link #onPacketInAsync(Player, Channel, Object)} will be invoked with this packet. + * + * @param channel - client identified by a channel. + * @param packet - the packet that will be received by the server. + */ + public void receivePacket(Channel channel, Object packet) { + channel.pipeline().context("encoder").fireChannelRead(packet); + } + + /** + * Retrieve the name of the channel injector, default implementation is "tiny-" + plugin name + "-" + a unique ID. + *

+ * Note that this method will only be invoked once. It is no longer necessary to override this to support multiple instances. + * + * @return A unique channel handler name. + */ + protected String getHandlerName() { + return "tiny-" + plugin.getName() + "-" + ID.incrementAndGet(); + } + + /** + * Add a custom channel handler to the given player's channel pipeline, allowing us to intercept sent and received packets. + *

+ * This will automatically be called when a player has logged in. + * + * @param player - the player to inject. + */ + public void injectPlayer(Player player) { + injectChannelInternal(getChannel(player)).player = player; + } + + /** + * Add a custom channel handler to the given channel. + * + * @param channel - the channel to inject. + * @return The intercepted channel, or NULL if it has already been injected. + */ + public void injectChannel(Channel channel) { + injectChannelInternal(channel); + } + + /** + * Add a custom channel handler to the given channel. + * + * @param channel - the channel to inject. + * @return The packet interceptor. + */ + private PacketInterceptor injectChannelInternal(Channel channel) { + try { + PacketInterceptor interceptor = (PacketInterceptor) channel.pipeline().get(handlerName); + + // Inject our packet interceptor + if (interceptor == null) { + interceptor = new PacketInterceptor(); + channel.pipeline().addBefore("packet_handler", handlerName, interceptor); + uninjectedChannels.remove(channel); + } + + return interceptor; + } catch (IllegalArgumentException e) { + // Try again + return (PacketInterceptor) channel.pipeline().get(handlerName); + } + } + + /** + * Retrieve the Netty channel associated with a player. This is cached. + * + * @param player - the player. + * @return The Netty channel. + */ + public Channel getChannel(Player player) { + Channel channel = channelLookup.get(player.getName()); + + // Lookup channel again + if (channel == null) { + Object connection = getConnection.get(getPlayerHandle.invoke(player)); + Object manager = getManager.get(connection); + + channelLookup.put(player.getName(), channel = getChannel.get(manager)); + } + + return channel; + } + + /** + * Uninject a specific player. + * + * @param player - the injected player. + */ + public void uninjectPlayer(Player player) { + uninjectChannel(getChannel(player)); + } + + /** + * Uninject a specific channel. + *

+ * This will also disable the automatic channel injection that occurs when a player has properly logged in. + * + * @param channel - the injected channel. + */ + public void uninjectChannel(final Channel channel) { + // No need to guard against this if we're closing + if (!closed) { + uninjectedChannels.add(channel); + } + + // See ChannelInjector in ProtocolLib, line 590 + channel.eventLoop().execute(new Runnable() { + + @Override + public void run() { + channel.pipeline().remove(handlerName); + } + + }); + } + + /** + * Determine if the given player has been injected by TinyProtocol. + * + * @param player - the player. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean hasInjected(Player player) { + return hasInjected(getChannel(player)); + } + + /** + * Determine if the given channel has been injected by TinyProtocol. + * + * @param channel - the channel. + * @return TRUE if it is, FALSE otherwise. + */ + public boolean hasInjected(Channel channel) { + return channel.pipeline().get(handlerName) != null; + } + + /** + * Cease listening for packets. This is called automatically when your plugin is disabled. + */ + public final void close() { + if (!closed) { + closed = true; + + // Remove our handlers + for (Player player : plugin.getServer().getOnlinePlayers()) { + uninjectPlayer(player); + } + + // Clean up Bukkit + HandlerList.unregisterAll(listener); + unregisterChannelHandler(); + } + } + + /** + * Channel handler that is inserted into the player's channel pipeline, allowing us to intercept sent and received packets. + * + * @author Kristian + */ + private final class PacketInterceptor extends ChannelDuplexHandler { + // Updated by the login event + public volatile Player player; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // Intercept channel + final Channel channel = ctx.channel(); + handleLoginStart(channel, msg); + + try { + msg = onPacketInAsync(player, channel, msg); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error in onPacketInAsync().", e); + } + + if (msg != null) { + super.channelRead(ctx, msg); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + try { + msg = onPacketOutAsync(player, ctx.channel(), msg); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Error in onPacketOutAsync().", e); + } + + if (msg != null) { + super.write(ctx, msg, promise); + } + } + + private void handleLoginStart(Channel channel, Object packet) { + if (PACKET_LOGIN_IN_START.isInstance(packet)) { + GameProfile profile = getGameProfile.get(packet); + channelLookup.put(profile.getName(), channel); + } + } + } +} \ No newline at end of file diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/gui/AddRegionElementGUI.java b/rpgregions/src/main/java/net/islandearth/rpgregions/gui/AddRegionElementGUI.java index ebc36b4..5da9461 100644 --- a/rpgregions/src/main/java/net/islandearth/rpgregions/gui/AddRegionElementGUI.java +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/gui/AddRegionElementGUI.java @@ -68,7 +68,7 @@ public class AddRegionElementGUI extends RPGRegionsGUI { GuiItem guiItem = new GuiItem(item, click -> { Object newInstance = registry.getNew(name, plugin, region); if (newInstance == null) { - player.sendMessage(ChatColor.RED + "This requires a plugin which is not installed."); + player.sendMessage(ChatColor.RED + "This requires a newer Minecraft version or a plugin which is not installed."); player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_LAND, 1f, 1f); return; } diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/listener/MoveListener.java b/rpgregions/src/main/java/net/islandearth/rpgregions/listener/MoveListener.java index 130ba81..da793fa 100644 --- a/rpgregions/src/main/java/net/islandearth/rpgregions/listener/MoveListener.java +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/listener/MoveListener.java @@ -1,6 +1,8 @@ package net.islandearth.rpgregions.listener; import net.islandearth.rpgregions.RPGRegions; +import net.islandearth.rpgregions.managers.data.region.ConfiguredRegion; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerMoveEvent; @@ -10,11 +12,24 @@ public record MoveListener(RPGRegions plugin) implements Listener { @EventHandler public void onMove(PlayerMoveEvent event) { + final Player player = event.getPlayer(); plugin.getManagers().getIntegrationManager().handleMove(event); + + if (!plugin.getManagers().getIntegrationManager().isInRegion(event.getTo()) && plugin.getManagers().getIntegrationManager().isInRegion(event.getFrom())) { + for (ConfiguredRegion configuredRegion : plugin.getManagers().getRegionsCache().getConfiguredRegions().values()) { + configuredRegion.getEffects().forEach(effect -> effect.uneffect(player)); + } + } } @EventHandler public void onTeleport(PlayerTeleportEvent event) { + final Player player = event.getPlayer(); plugin.getManagers().getIntegrationManager().handleMove(event); + if (!plugin.getManagers().getIntegrationManager().isInRegion(event.getTo()) && plugin.getManagers().getIntegrationManager().isInRegion(event.getFrom())) { + for (ConfiguredRegion configuredRegion : plugin.getManagers().getRegionsCache().getConfiguredRegions().values()) { + configuredRegion.getEffects().forEach(effect -> effect.uneffect(player)); + } + } } } \ No newline at end of file diff --git a/rpgregions/src/main/java/net/islandearth/rpgregions/managers/RPGRegionsManagers.java b/rpgregions/src/main/java/net/islandearth/rpgregions/managers/RPGRegionsManagers.java index fd51b76..6dcfba4 100644 --- a/rpgregions/src/main/java/net/islandearth/rpgregions/managers/RPGRegionsManagers.java +++ b/rpgregions/src/main/java/net/islandearth/rpgregions/managers/RPGRegionsManagers.java @@ -1,13 +1,16 @@ package net.islandearth.rpgregions.managers; +import io.netty.channel.Channel; import net.islandearth.rpgregions.RPGRegions; import net.islandearth.rpgregions.api.integrations.IntegrationManager; import net.islandearth.rpgregions.api.integrations.IntegrationType; import net.islandearth.rpgregions.api.integrations.hooks.PlaceholderRegionHook; import net.islandearth.rpgregions.command.IconCommand; +import net.islandearth.rpgregions.effects.FogEffect; import net.islandearth.rpgregions.effects.PotionRegionEffect; import net.islandearth.rpgregions.effects.RegionEffect; import net.islandearth.rpgregions.effects.RegionEffectRegistry; +import net.islandearth.rpgregions.effects.fog.tinyprotocol.TinyProtocol; import net.islandearth.rpgregions.exception.CouldNotStartException; import net.islandearth.rpgregions.gui.element.BooleanGuiFieldElement; import net.islandearth.rpgregions.gui.element.CompareTypeGuiFieldElement; @@ -38,10 +41,12 @@ import net.islandearth.rpgregions.rewards.PlayerCommandReward; import net.islandearth.rpgregions.rewards.RegionRewardRegistry; import net.islandearth.rpgregions.thread.Blocking; import net.islandearth.rpgregions.utils.ItemStackBuilder; +import net.minecraft.network.protocol.game.PacketPlayOutMapChunk; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; @@ -74,6 +79,7 @@ public class RPGRegionsManagers implements IRPGRegionsManagers { private final IRegenerationManager regenerationManager; private final Map>, RPGRegionsRegistry> registry; private final IGuiFieldElementRegistry guiFieldElementRegistry; + private TinyProtocol tinyProtocol; public RPGRegionsManagers(RPGRegions plugin) throws ReflectiveOperationException, CouldNotStartException, IOException { StorageType.valueOf(plugin.getConfig().getString("settings.storage.mode").toUpperCase()) @@ -141,6 +147,10 @@ public class RPGRegionsManagers implements IRPGRegionsManagers { ConfiguredRegion region = plugin.getGson().fromJson(reader, ConfiguredRegion.class); if (!region.getId().equals("exampleconfig")) regionsCache.addConfiguredRegion(region); warnBlocking(plugin, region); + if (region.getEffects() != null) + for (RegionEffect effect : region.getEffects()) { + if (effect instanceof FogEffect fogEffect) fogEffect.generateBiomes(); + } } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Error loading region config " + file.getName() + ".", e); } @@ -165,6 +175,25 @@ public class RPGRegionsManagers implements IRPGRegionsManagers { guiFieldElementRegistry.register(new LocationGuiFieldElement()); guiFieldElementRegistry.register(new PotionEffectGuiFieldElement()); guiFieldElementRegistry.register(new CompareTypeGuiFieldElement()); + + if (Bukkit.getBukkitVersion().contains("1.17")) { + this.tinyProtocol = new TinyProtocol(plugin) { + @Override + public Object onPacketOutAsync(Player target, Channel channel, Object packet) { + if (packet instanceof PacketPlayOutMapChunk mapChunk) { + for (ConfiguredRegion region : regionsCache.getConfiguredRegions().values()) { + if (region.getEffects() == null) continue; + for (RegionEffect effect : region.getEffects()) { + if (effect instanceof FogEffect fogEffect) { + return fogEffect.onSendChunk(mapChunk, target); + } + } + } + } + return packet; + } + }; + } } private void warnBlocking(RPGRegions plugin, ConfiguredRegion region) {