Initial commit v2

This commit is contained in:
Martin Dvořák 2026-08-04 20:09:03 +02:00
commit 380a66e286
17 changed files with 1651 additions and 231 deletions

View file

@ -0,0 +1,30 @@
package one.eim.gravitycontrol;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
public class Compatibility {
public static Boolean IS_FOLIA = null;
private static boolean methodExist(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
try {
clazz.getDeclaredMethod(methodName, parameterTypes);
return true;
} catch (Throwable ignored) {}
return false;
}
public static Boolean isFolia() {
if (IS_FOLIA == null) IS_FOLIA = methodExist(Bukkit.class, "getGlobalRegionScheduler");
return IS_FOLIA;
}
public static void runTaskAsync(Plugin plugin, Runnable runnable) {
if (isFolia()) {
Bukkit.getAsyncScheduler().runNow(plugin, (task) -> runnable.run());
return;
}
Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable);
}
}

View file

@ -0,0 +1,173 @@
package one.eim.gravitycontrol;
import com.destroystokyo.paper.MaterialSetTag;
import com.destroystokyo.paper.MaterialTags;
import net.kyori.adventure.key.InvalidKeyException;
import net.kyori.adventure.key.Key;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.intellij.lang.annotations.Subst;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Configuration {
private final GravityControl plugin;
final double horizontalCoefficient;
final double verticalCoefficient;
final Set<Key> worlds;
final Set<Material> blocks;
final List<String> regions;
final boolean updateChecker;
Configuration(GravityControl plugin, FileConfiguration config) {
this.plugin = plugin;
if (!config.isSet(Fields._version)) {
migrate(config);
}
// These seem to make it about match vanilla, but where did these values come from??
// magic number much
this.horizontalCoefficient = config.getDouble("horizontal-coefficient", 1.46D);
this.verticalCoefficient = config.getDouble("vertical-coefficient", -2.4D);
this.worlds = worlds(config);
this.blocks = blocks(config);
this.regions = config.getStringList(Fields.regions);
this.updateChecker = config.getBoolean(Fields.updateChecker);
}
private Set<Material> blocks(final ConfigurationSection config) {
final Set<Material> materials = new HashSet<>();
for (final String keyString : config.getStringList(Fields.blocks)) {
final NamespacedKey key = NamespacedKey.fromString(keyString);
if (key == null) {
this.plugin.getSLF4JLogger().warn("Invalid key {} supplied in blocks configuration", keyString);
continue;
}
final Material material = Registry.MATERIAL.get(key);
if (material == null) {
this.plugin.getSLF4JLogger().warn("Unknown block {} supplied in blocks configuration", key.asString());
}
materials.add(material);
}
return materials;
}
private Set<Key> worlds(final ConfigurationSection config) {
final Set<Key> keys = new HashSet<>();
for (final @Subst("minecraft:overworld") String keyString : config.getStringList(Fields.worlds)) {
if (keyString.equals("*")) {
this.plugin.getServer().getWorlds().forEach(world -> keys.add(world.key()));
return keys;
}
try {
keys.add(Key.key(keyString));
} catch (InvalidKeyException e) {
this.plugin.getSLF4JLogger().warn("Invalid key {} supplied in worlds configuration", keyString);
}
}
return keys;
}
private void migrate(final FileConfiguration config) {
this.plugin.getSLF4JLogger().info("Performing config migration");
final List<String> oldWorlds = config.getStringList("worlds");
final List<String> newWorlds = new ArrayList<>();
for (final String worldName : oldWorlds) {
if (worldName.equals("*")) {
newWorlds.add(worldName);
} else {
final World world = this.plugin.getServer().getWorld(worldName);
if (world == null) {
this.plugin.getSLF4JLogger().warn("Unable to find world {}, discarding from configuration", worldName);
continue;
}
newWorlds.add(world.key().asString());
}
}
final List<String> newMaterials = new ArrayList<>();
if (config.getBoolean("enabled-blocks.sand")) {
newMaterials.add(Material.SAND.key().asString());
}
if (config.getBoolean("enabled-blocks.red-sand")) {
newMaterials.add(Material.RED_SAND.key().asString());
}
if (config.getBoolean("enabled-blocks.anvil")) {
MaterialSetTag.ANVIL.getValues().forEach(material -> newMaterials.add(material.key().asString()));
}
if (config.getBoolean("enabled-blocks.dragon-egg")) {
newMaterials.add(Material.DRAGON_EGG.key().asString());
}
if (config.getBoolean("enabled-blocks.gravel")) {
newMaterials.add(Material.GRAVEL.key().asString());
}
if (config.getBoolean("enabled-blocks.concrete-powder")) {
MaterialTags.CONCRETE_POWDER.getValues().forEach(material -> newMaterials.add(material.key().asString()));
}
config.set("enabled-blocks", null);
config.set(Fields._version, 2);
config.set(Fields.worlds, newWorlds);
config.set(Fields.blocks, newMaterials);
config.set(Fields.regions, List.of("*"));
config.set(Fields.updateChecker, true);
config.setComments(Fields._version, List.of(
"Do not touch. Used for configuration migration."
));
config.setComments(Fields.worlds, List.of(
"Worlds where gravity duplication is enabled.",
"`*` means all worlds.",
"WARNING: World key is used, NOT world name.",
"For example: `minecraft:overworld` rather than `world`."
));
config.setComments(Fields.blocks, List.of(
"Blocks for which duping is allowed.",
"NOTE: Uses Mojang item names, not bukkit Material names."
));
config.setComments(Fields.regions, List.of(
"List of WorldGuard regions where duping is allowed",
"`*` means all regions. Checked based on end portal coordinates."
));
config.setComments(Fields.updateChecker, List.of(
"If GravityControl should check for updates on startup"
));
try {
config.save(this.plugin.getDataFolder().toPath().resolve("config.yml").toFile());
} catch (final IOException e) {
this.plugin.getSLF4JLogger().warn("Failed to migrate configuration", e);
this.plugin.getPluginLoader().disablePlugin(this.plugin);
}
}
private static final class Fields {
static final String _version = "_version";
static final String worlds = "worlds";
static final String blocks = "blocks";
static final String regions = "regions";
static final String updateChecker = "update-checker";
}
}

View file

@ -0,0 +1,31 @@
package one.eim.gravitycontrol;
import org.bstats.bukkit.Metrics;
import org.bukkit.plugin.java.JavaPlugin;
public final class GravityControl extends JavaPlugin {
public Configuration config;
WorldGuardHook worldGuardHook = null;
@Override
public void onLoad() {
if (this.getServer().getPluginManager().getPlugin("WorldGuard") != null) {
this.worldGuardHook = new WorldGuardHook(this);
if (!this.worldGuardHook.register()) this.worldGuardHook = null;
}
}
@Override
public void onEnable() {
this.saveDefaultConfig();
this.config = new Configuration(this, this.getConfig());
this.getServer().getPluginManager().registerEvents(new GravityListener(this), this);
this.getCommand("gcr").setExecutor(new ReloadCommand(this));
if (this.config.updateChecker) new UpdateChecker(this).check();
new Metrics(this, 13384);
}
}

View file

@ -0,0 +1,77 @@
package one.eim.gravitycontrol;
import com.sk89q.worldguard.protection.flags.StateFlag.State;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.FallingBlock;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.util.Set;
public class GravityListener implements Listener {
private static final Set<Vector> DIRECTIONS = Set.of(
new Vector(0, 0, -1),
new Vector(1, 0, 0),
new Vector(0, 0, 1),
new Vector(-1, 0, 0)
);
private final GravityControl plugin;
public GravityListener(final GravityControl plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onEntityChangeBlock(final EntityChangeBlockEvent event) {
if (event.getTo() == Material.AIR
|| !(event.getEntity() instanceof final FallingBlock falling)
|| !this.plugin.config.worlds.contains(event.getBlock().getWorld().key())
|| !this.plugin.config.blocks.contains(falling.getBlockData().getMaterial())
) {
return;
}
final BoundingBox boundingBox = falling.getBoundingBox().expand(-0.01D);
for (Vector direction : DIRECTIONS) {
final Location loc = event.getBlock().getLocation().add(direction);
if (!loc.getWorld().isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) {
continue;
}
final Block block = loc.getBlock();
if (block.getType() != Material.END_PORTAL || !block.getBoundingBox().overlaps(boundingBox)) {
continue;
}
// Check end portal position. This seems to make more sense than checking block or entity location
// as this can vary by design
if (this.plugin.worldGuardHook != null && this.plugin.worldGuardHook.dupingAllowed(loc) != State.ALLOW) {
continue;
}
final Vector velocity = falling.getVelocity();
if (velocity.getX() == 0 && velocity.getZ() == 0) {
loc.getWorld().spawnFallingBlock(falling.getLocation().add(direction.getX() * 0.25D, 0.05D, direction.getZ() * 0.25D), falling.getBlockData());
} else {
final Vector endVelocity = velocity
.setX(velocity.getX() * this.plugin.config.horizontalCoefficient)
.setY(velocity.getY() * this.plugin.config.verticalCoefficient)
.setZ(velocity.getZ() * this.plugin.config.horizontalCoefficient);
falling.getWorld().spawnFallingBlock(
falling.getLocation().add(direction.getX() * 0.25D, direction.getY() * 0.25D, direction.getZ() * 0.25D),
falling.getBlockData()
).setVelocity(endVelocity);
}
}
}
}

View file

@ -0,0 +1,28 @@
package one.eim.gravitycontrol;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
public class ReloadCommand implements CommandExecutor {
private static final Component prefix = Component.text("[", NamedTextColor.GRAY)
.append(Component.text("GravityControl", NamedTextColor.DARK_GRAY))
.append(Component.text("]", NamedTextColor.GRAY))
.append(Component.space());
private final GravityControl plugin;
public ReloadCommand(GravityControl plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
sender.sendMessage(prefix.append(Component.text("Attempting to reload configuration...", NamedTextColor.WHITE)));
this.plugin.reloadConfig();
this.plugin.config = new Configuration(this.plugin, this.plugin.getConfig());
sender.sendMessage(prefix.append(Component.text("Successfully reloaded configuration!", NamedTextColor.GREEN)));
return true;
}
}

View file

@ -0,0 +1,68 @@
package one.eim.gravitycontrol;
import com.google.gson.Gson;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.logging.Level;
class UpdateChecker {
private final HttpClient httpClient;
private final HttpRequest request;
private final Gson gson;
private final GravityControl plugin;
UpdateChecker(GravityControl plugin) {
this.httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.connectTimeout(Duration.ofSeconds(2))
.build();
this.request = HttpRequest.newBuilder(URI.create("https://gravitycontrol.eim.one/version"))
.POST(HttpRequest.BodyPublishers.ofString(plugin.getDescription().getVersion()))
.build();
this.gson = new Gson();
this.plugin = plugin;
}
void check() {
Compatibility.runTaskAsync(this.plugin, () -> {
final Version version;
try {
version = this.gson.fromJson(
this.httpClient.send(this.request, HttpResponse.BodyHandlers.ofString()).body(),
Version.class
);
} catch (IOException | InterruptedException e) {
this.plugin.getSLF4JLogger().info("Failed to check for updates", e);
return;
}
if (version.outdated()) {
this.plugin.getLogger().log(Level.INFO, "GravityControl is out of date! Current version: " + this.plugin.getDescription().getVersion() + " Latest version: " + version.latest() + ".");
this.plugin.getLogger().log(Level.INFO, "Please download an updated version of GravityControl from https://dev.bukkit.org/projects/gravity-control");
}
});
}
private static final class Version {
private final boolean outdated;
private final String latest;
public Version(final boolean outdated, final String latest) {
this.outdated = outdated;
this.latest = latest;
}
public boolean outdated() {
return outdated;
}
public String latest() {
return latest;
}
}
}

View file

@ -0,0 +1,40 @@
package one.eim.gravitycontrol;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.StateFlag.State;
import com.sk89q.worldguard.protection.flags.registry.FlagConflictException;
import org.bukkit.Location;
class WorldGuardHook {
private static final String FLAG_NAME = "gravity-dupe";
private static final StateFlag FLAG = new StateFlag(FLAG_NAME, true);
private final GravityControl plugin;
WorldGuardHook(final GravityControl plugin) {
this.plugin = plugin;
}
WorldGuard wg() {
return WorldGuard.getInstance();
}
boolean register() {
try {
this.wg().getFlagRegistry().register(FLAG);
} catch (final FlagConflictException e) {
plugin.getSLF4JLogger().warn("Failed to enable WorldGuard hook. Flag with name {} already exists!", FLAG_NAME, e);
return false;
}
plugin.getSLF4JLogger().info("Enabled WorldGuard hook");
return true;
}
State dupingAllowed(final Location location) {
return this.wg().getPlatform().getRegionContainer().createQuery()
.getApplicableRegions(BukkitAdapter.adapt(location))
.queryState(null, FLAG);
}
}

View file

@ -0,0 +1,41 @@
# Do not touch. Used for configuration migration.
_version: 2
# Worlds where gravity duplication is enabled.
# `*` means all worlds.
# WARNING: World key is used, NOT world name.
# For example: `minecraft:overworld` rather than `world`.
worlds:
- "*"
# Blocks for which duping is allowed.
blocks:
- "sand"
- "red_sand"
- "gravel"
- "anvil"
- "dragon_egg"
- "white_concrete_powder"
- "orange_concrete_powder"
- "magenta_concrete_powder"
- "light_blue_concrete_powder"
- "yellow_concrete_powder"
- "lime_concrete_powder"
- "pink_concrete_powder"
- "gray_concrete_powder"
- "light_gray_concrete_powder"
- "cyan_concrete_powder"
- "purple_concrete_powder"
- "blue_concrete_powder"
- "brown_concrete_powder"
- "green_concrete_powder"
- "red_concrete_powder"
- "black_concrete_powder"
# List of WorldGuard regions where duping is allowed
# `*` means all regions. Checked based on end portal coordinates.
regions:
- "*"
# If GravityControl should check for updates on startup
update-checker: true

View file

@ -0,0 +1,19 @@
name: GravityControl
version: 1.0
author: laymanuel, sulu
website: https://github.com/sulu5890/GravityControl
main: one.eim.gravitycontrol.GravityControl
api-version: 26.1.2
folia-supported: true
softdepend: [WorldGuard]
commands:
gcr:
description: Reload GravityControl configuration
permission: gravitycontrol.reload
usage: /gcr
permissions:
gravitycontrol.reload:
description: Allows use of the /gcr command
default: op