Major refactor to handle more edge cases

This commit is contained in:
NotAlexNoyle committed 2026-08-22 19:56:27 -04:00
1 parent 6d8626546c
commit 31070db577
9 files changed
+647 -284

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ kotlin { jvmToolchain(17) }
/* ----------------------------- Metadata ------------------------------ */
group = "net.trueog.launchpads-og" // Declare bundle identifier.
version = "1.7" // Declare plugin version (will be in .jar).
version = "1.8" // Declare plugin version (will be in .jar).
val apiVersion = "1.19" // Declare minecraft server target version.
@@ -2,6 +2,8 @@ package net.trueog.launchpadsog.Commands;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.Vector;
@@ -63,7 +65,7 @@ public class LaunchPadCommands implements CommandExecutor {
// check if there is already a pad in that location
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(world.getName())) {
if (world.getName().equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == playerX) {
@@ -123,14 +125,19 @@ public class LaunchPadCommands implements CommandExecutor {
int playerX = playerLocation.getBlockX();
int playerY = playerLocation.getBlockY();
int playerZ = playerLocation.getBlockZ();
double padXPower = Double.parseDouble(args[1]);
double padYPower = Double.parseDouble(args[2]);
double padZPower = Double.parseDouble(args[3]);
final Double padXPower = parseDoubleOrWarn(sender, args[1]);
final Double padYPower = parseDoubleOrWarn(sender, args[2]);
final Double padZPower = parseDoubleOrWarn(sender, args[3]);
if (padXPower == null || padYPower == null || padZPower == null) {
return true;
}
// check if there is already a pad in that location
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(world.getName())) {
if (world.getName().equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == playerX) {
@@ -190,17 +197,24 @@ public class LaunchPadCommands implements CommandExecutor {
else if (args.length == 7) {
int padNumber = padCoords.getInt("numberOfPads") + 1;
int padX = Integer.parseInt(args[1]);
int padY = Integer.parseInt(args[2]);
int padZ = Integer.parseInt(args[3]);
double padXPower = Double.parseDouble(args[4]);
double padYPower = Double.parseDouble(args[5]);
double padZPower = Double.parseDouble(args[6]);
final Integer padX = parseIntOrWarn(sender, args[1]);
final Integer padY = parseIntOrWarn(sender, args[2]);
final Integer padZ = parseIntOrWarn(sender, args[3]);
final Double padXPower = parseDoubleOrWarn(sender, args[4]);
final Double padYPower = parseDoubleOrWarn(sender, args[5]);
final Double padZPower = parseDoubleOrWarn(sender, args[6]);
if (padX == null || padY == null || padZ == null || padXPower == null
|| padYPower == null || padZPower == null)
{
return true;
}
// check if there is already a pad in that location
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(world.getName())) {
if (world.getName().equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == padX) {
@@ -250,6 +264,15 @@ public class LaunchPadCommands implements CommandExecutor {
+ ", " + padZPower).color(TextColor.color(0, 191, 255)))); // light blue
}
// wrong arity: say so instead of doing nothing
else {
sender.sendMessage(Component.text("[LaunchPad]").color(TextColor.color(0, 255, 0))
.append(Component.text(
" Usage: /launchpad set [<xpower> <ypower> <zpower>] or /launchpad set <x> <y> <z> <xpower> <ypower> <zpower>")
.color(TextColor.color(255, 0, 0))));
}
}
// if the player doesn't have permission to use this command,
@@ -273,75 +296,48 @@ public class LaunchPadCommands implements CommandExecutor {
LaunchPadsOG.getPlugin(LaunchPadsOG.class).reloadConfig();
padCoords = LaunchPadsOG.getConfiguration();
// check through the config to make sure that
// there are as many launchpads as numberOfPads
// if there is not, move all of the pad number down
// by one starting at the missing number
// The live accessor means the pre-reload count no
// longer matches the file; re-read before using it.
numberOfPads = Math.max(0, padCoords.getInt("numberOfPads"));
int maxNumberOfPads = numberOfPads + 1;
if (numberOfPads > 1) {
// Compact pad ids to run 1..n with no gaps, judged
// via isSet so jar defaults cannot mask a gap.
int writeId = 0;
for (int readId = 1; readId <= numberOfPads; readId++) {
for (int i = 1; i < maxNumberOfPads; i++) {
if (!padCoords.isSet(readId + ".world")) {
// if the world is null, then the pad is not set
if (padCoords.getString(i + ".world") == null) {
// move all other pads up one id number
for (int j = i; j < padCoords.getInt("numberOfPads") + 1; j++) {
padCoords.set("" + j + ".x", padCoords.getInt("" + (j + 1) + ".x"));
padCoords.set("" + j + ".y", padCoords.getInt("" + (j + 1) + ".y"));
padCoords.set("" + j + ".z", padCoords.getInt("" + (j + 1) + ".z"));
padCoords.set("" + j + ".world",
padCoords.getString("" + (j + 1) + ".world"));
padCoords.set("" + j + ".xpower",
padCoords.getDouble("" + (j + 1) + ".xpower"));
padCoords.set("" + j + ".ypower",
padCoords.getDouble("" + (j + 1) + ".ypower"));
padCoords.set("" + j + ".zpower",
padCoords.getDouble("" + (j + 1) + ".zpower"));
// this makes it easier to search the config file
// use CTRL+F and search for the coords of the pad
padCoords.set("" + j + ".searchID",
padCoords.getString("" + (j + 1) + ".searchID"));
}
padCoords.set("" + numberOfPads, null);
padCoords.set("numberOfPads", --numberOfPads);
}
continue;
}
// check if the last pad exists
if (padCoords.getString(numberOfPads + ".world") == null) {
writeId++;
if (readId != writeId) {
// decrement numberOfPads by one
padCoords.set("numberOfPads", --numberOfPads);
}
} else {
// if numberOfPads is 1, then there can only be one pad
// check if it exists. If not, set numberOfPads to 0
if (padCoords.getString("1.world") == null) {
padCoords.set("numberOfPads", 0);
padCoords.set(writeId + ".x", padCoords.getInt(readId + ".x"));
padCoords.set(writeId + ".y", padCoords.getInt(readId + ".y"));
padCoords.set(writeId + ".z", padCoords.getInt(readId + ".z"));
padCoords.set(writeId + ".world", padCoords.getString(readId + ".world"));
padCoords.set(writeId + ".xpower", padCoords.getDouble(readId + ".xpower"));
padCoords.set(writeId + ".ypower", padCoords.getDouble(readId + ".ypower"));
padCoords.set(writeId + ".zpower", padCoords.getDouble(readId + ".zpower"));
padCoords.set(writeId + ".searchID", padCoords.getString(readId + ".searchID"));
padCoords.set("" + readId, null);
}
}
// check if numberOfPads is below 0
// if so, set it to 0
if (padCoords.getInt("numberOfPads") < 0) {
// drop any leftover sections above the compacted count
for (int staleId = writeId + 1; staleId <= numberOfPads; staleId++) {
padCoords.set("numberOfPads", 0);
padCoords.set("" + staleId, null);
}
padCoords.set("numberOfPads", writeId);
numberOfPads = writeId;
/* check that everything has what it is supposed to have */
// check that numberOfPads is an integer
@@ -694,7 +690,7 @@ public class LaunchPadCommands implements CommandExecutor {
boolean found = false;
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(world.getName())) {
if (world.getName().equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == block.getX()) {
@@ -774,14 +770,19 @@ public class LaunchPadCommands implements CommandExecutor {
else if (args.length == 4) {
// check for a launchpad at the coords given in the player's world
int blockX = Integer.parseInt(args[1]);
int blockY = Integer.parseInt(args[2]);
int blockZ = Integer.parseInt(args[3]);
final Integer blockX = parseIntOrWarn(sender, args[1]);
final Integer blockY = parseIntOrWarn(sender, args[2]);
final Integer blockZ = parseIntOrWarn(sender, args[3]);
if (blockX == null || blockY == null || blockZ == null) {
return true;
}
boolean found = false;
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(world.getName())) {
if (world.getName().equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == blockX) {
@@ -828,6 +829,10 @@ public class LaunchPadCommands implements CommandExecutor {
+ blockY + ", " + blockZ)
.color(TextColor.color(255, 255, 0))));
// stop here: the ids just compacted, so
// continuing the scan reads dead sections
return true;
}
}
@@ -871,14 +876,19 @@ public class LaunchPadCommands implements CommandExecutor {
}
int blockX = Integer.parseInt(args[2]);
int blockY = Integer.parseInt(args[3]);
int blockZ = Integer.parseInt(args[4]);
final Integer blockX = parseIntOrWarn(sender, args[2]);
final Integer blockY = parseIntOrWarn(sender, args[3]);
final Integer blockZ = parseIntOrWarn(sender, args[4]);
if (blockX == null || blockY == null || blockZ == null) {
return true;
}
boolean found = false;
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(worldName)) {
if (worldName.equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == blockX) {
@@ -925,6 +935,10 @@ public class LaunchPadCommands implements CommandExecutor {
+ blockY + ", " + blockZ)
.color(TextColor.color(255, 255, 0))));
// stop here: the ids just compacted, so
// continuing the scan reads dead sections
return true;
}
}
@@ -944,6 +958,15 @@ public class LaunchPadCommands implements CommandExecutor {
}
}
// wrong arity: say so instead of doing nothing
else {
sender.sendMessage(Component.text("[LaunchPad]").color(TextColor.color(0, 255, 0))
.append(Component.text(
" Usage: /launchpad delete [<x> <y> <z>] or /launchpad delete <world> <x> <y> <z>")
.color(TextColor.color(255, 0, 0))));
}
}
// if the player doesn't have permission to use this command,
@@ -1072,6 +1095,18 @@ public class LaunchPadCommands implements CommandExecutor {
}
} else if (args[0].equalsIgnoreCase("reloadconfig")) {
// console path: reload needs no player context, and index
// recovery must not depend on a logged-in op
if (sender.hasPermission("launchpad.canreload")) {
LaunchPadsOG.getPlugin(LaunchPadsOG.class).reloadConfig();
sender.sendMessage(Component.text("[LaunchPad]").color(TextColor.color(0, 255, 0))
.append(Component.text(" Config Reloaded").color(TextColor.color(255, 255, 0))));
}
}
} else {
@@ -1088,18 +1123,69 @@ public class LaunchPadCommands implements CommandExecutor {
}
// Null when the argument is not a number; the caller bails after the warning.
private Integer parseIntOrWarn(CommandSender sender, String arg) {
try {
return Integer.parseInt(arg);
} catch (NumberFormatException e) {
warnNotANumber(sender, arg);
return null;
}
}
private Double parseDoubleOrWarn(CommandSender sender, String arg) {
try {
return Double.parseDouble(arg);
} catch (NumberFormatException e) {
warnNotANumber(sender, arg);
return null;
}
}
private void warnNotANumber(CommandSender sender, String arg) {
sender.sendMessage(Component.text("[LaunchPad]").color(TextColor.color(0, 255, 0))
.append(Component.text(" '" + arg + "' is not a number").color(TextColor.color(255, 0, 0))));
}
private void restoreOldConfig(FileConfiguration oldPadCoords) {
try {
File configFile = new File(LaunchPadsOG.getPlugin(LaunchPadsOG.class).getDataFolder(), "config.yml");
// keep the admin's hand-edited file recoverable before clobbering it
if (configFile.exists()) {
Files.copy(configFile.toPath(), configFile.toPath().resolveSibling("config.yml.bak"),
StandardCopyOption.REPLACE_EXISTING);
}
oldPadCoords.save(configFile);
LaunchPadsOG.getPlugin(LaunchPadsOG.class).reloadConfig();
} catch (IOException e) {
e.printStackTrace();
} finally {
// reload whatever is on disk so memory, index, and file agree
// even when the rollback write itself failed
LaunchPadsOG.getPlugin(LaunchPadsOG.class).reloadConfig();
}
}
@@ -1,6 +1,8 @@
package net.trueog.launchpadsog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@@ -15,19 +17,27 @@ import net.trueog.launchpadsog.Commands.LaunchPadCommands;
import net.trueog.launchpadsog.Listeners.OnBlockBreak;
import net.trueog.launchpadsog.Listeners.OnBlockPlace;
import net.trueog.launchpadsog.Listeners.OnEntityDamage;
import net.trueog.launchpadsog.Listeners.OnLaunchEnd;
import net.trueog.launchpadsog.Listeners.OnPlayerInteract;
import net.trueog.launchpadsog.Listeners.OnPlayerMove;
public final class LaunchPadsOG extends JavaPlugin {
private static LaunchPadsOG plugin;
private static FileConfiguration config;
// This list will hold the players that have used a launchpad and are flying
// through the air
// then, when they land, I remove them from the list, and stop them from taking
// fall damage.
private static final ArrayList<Player> launchedPlayers = new ArrayList<>();
// Only an AIRBORNE protection can be spent or dropped, so a slow client
// still reporting the pad position cannot burn it before liftoff.
public enum LaunchPhase {
AWAITING_LIFTOFF, AIRBORNE
}
// Players inside a live launch arc, tracked by id so relogs and repeated
// pad hits can neither stack protections nor strand stale entries.
private static final Map<UUID, LaunchPhase> launchedPlayers = new HashMap<>();
// Pad ids indexed by "world:x:y:z", so the move handler does one map
// lookup instead of scanning every configured pad on every step.
private static final Map<String, Integer> padsByBlock = new HashMap<>();
@Override
public void onEnable() {
@@ -35,12 +45,10 @@ public final class LaunchPadsOG extends JavaPlugin {
plugin = this;
saveDefaultConfig();
config = this.getConfig();
reloadConfig();
// If the launchpad is craftable, add the recipe to the server.
if ((boolean) config.get("cancraft")) {
if (getConfig().getBoolean("cancraft")) {
// Create the recipe for the launchpad
// make it really expensive by locking it
@@ -63,6 +71,7 @@ public final class LaunchPadsOG extends JavaPlugin {
getServer().getPluginManager().registerEvents(new OnBlockBreak(), this);
getServer().getPluginManager().registerEvents(new OnBlockPlace(), this);
getServer().getPluginManager().registerEvents(new OnEntityDamage(), this);
getServer().getPluginManager().registerEvents(new OnLaunchEnd(), this);
getCommand("launchpad").setExecutor(new LaunchPadCommands());
getCommand("launchpad").setTabCompleter(new LaunchPadCommandCompleter());
@@ -70,49 +79,146 @@ public final class LaunchPadsOG extends JavaPlugin {
// to signify that they are launchpads.
Bukkit.getScheduler().runTaskTimer(this, () -> {
for (int i = 1; i < config.getInt("numberOfPads") + 1; i++) {
final FileConfiguration padConfig = getConfig();
final int idleParticleCount = padConfig.getInt("idleParticleCount");
final double idleParticlePower = padConfig.getDouble("idleParticlePower");
for (int i = 1; i < padConfig.getInt("numberOfPads") + 1; i++) {
final String worldName = config.getString(i + ".world");
final World world = Bukkit.getWorld(worldName);
final double padX = config.getInt(i + ".x") + 0.5;
final double padY = config.getInt(i + ".y");
final double padZ = config.getInt(i + ".z") + 0.5;
final Location padLocation = new Location(world, padX, padY, padZ);
final int idleParticleCount = config.getInt("idleParticleCount");
final double idleParticlePower = config.getDouble("idleParticlePower");
final String worldName = padConfig.getString(i + ".world");
final World world = worldName == null ? null : Bukkit.getWorld(worldName);
if (world == null) {
// Spawn particles on the pad if the pad is loaded.
if (world.isChunkLoaded(padLocation.getChunk())) {
// Use PORTAL, what ViaRewind downgrades DRAGON_BREATH to, so 1.8 and modern
// clients match.
world.spawnParticle(Particle.PORTAL, padLocation, idleParticleCount, 0, 0, 0,
0.05 * idleParticlePower);
continue;
}
final int padX = padConfig.getInt(i + ".x");
final int padY = padConfig.getInt(i + ".y");
final int padZ = padConfig.getInt(i + ".z");
// Asked by chunk coordinates on purpose: getChunk() on a
// location loads and pins the chunk just to answer.
if (!world.isChunkLoaded(padX >> 4, padZ >> 4)) {
continue;
}
final Location padLocation = new Location(world, padX + 0.5, padY, padZ + 0.5);
// Use PORTAL, what ViaRewind downgrades DRAGON_BREATH to, so 1.8 and modern
// clients match.
world.spawnParticle(Particle.PORTAL, padLocation, idleParticleCount, 0, 0, 0, 0.05 * idleParticlePower);
}
}, 1, 30);
}
@Override
public void onDisable() {
launchedPlayers.clear();
padsByBlock.clear();
}
// Every save and reload funnels through these, so the pad index can never
// go stale against the configuration.
@Override
public void saveConfig() {
super.saveConfig();
rebuildPadIndex();
}
@Override
public void reloadConfig() {
super.reloadConfig();
rebuildPadIndex();
}
public static LaunchPadsOG getPlugin() {
return plugin;
}
// Always the live configuration: the old cached object was silently
// swapped out by reloadConfig(), losing /launchpad set edits on restart.
public static FileConfiguration getConfiguration() {
return config;
return plugin.getConfig();
}
public static ArrayList<Player> getLaunchedPlayers() {
public static void markLaunched(Player player) {
return launchedPlayers;
launchedPlayers.put(player.getUniqueId(), LaunchPhase.AWAITING_LIFTOFF);
}
}
public static LaunchPhase launchPhase(Player player) {
return launchedPlayers.get(player.getUniqueId());
}
public static void markAirborne(Player player) {
launchedPlayers.replace(player.getUniqueId(), LaunchPhase.AIRBORNE);
}
// Spends the protection of an arc in flight; damage before liftoff is the
// arrival fall onto the pad, which stays the player's own to eat.
public static boolean consumeAirborneLaunch(Player player) {
if (launchedPlayers.get(player.getUniqueId()) != LaunchPhase.AIRBORNE)
return false;
launchedPlayers.remove(player.getUniqueId());
return true;
}
public static void clearLaunch(Player player) {
launchedPlayers.remove(player.getUniqueId());
}
// The pad id at a block position, or null when no pad sits there.
public static Integer padAt(String worldName, int x, int y, int z) {
return padsByBlock.get(worldName + ":" + x + ":" + y + ":" + z);
}
private void rebuildPadIndex() {
padsByBlock.clear();
final FileConfiguration padConfig = getConfig();
for (int i = 1; i < padConfig.getInt("numberOfPads") + 1; i++) {
// isSet ignores the jar defaults; plain getters resolve through
// them and would resurrect every hand-deleted pad.
if (!padConfig.isSet(i + ".world")) {
continue;
}
// putIfAbsent keeps the old scan's first-match-wins order when two
// entries share one block.
padsByBlock.putIfAbsent(padConfig.getString(i + ".world") + ":" + padConfig.getInt(i + ".x") + ":"
+ padConfig.getInt(i + ".y") + ":" + padConfig.getInt(i + ".z"), i);
}
}
}
@@ -43,7 +43,8 @@ public class OnBlockBreak implements Listener {
for (int i = 1; i < numberOfPads + 1; i++) {
if (padCoords.getString("" + i + ".world").equals(world.getName())) {
// null-safe order: a gap in the ids must not abort every break event
if (world.getName().equals(padCoords.getString("" + i + ".world"))) {
if (padCoords.getInt("" + i + ".x") == block.getX()) {
@@ -2,6 +2,7 @@ package net.trueog.launchpadsog.Listeners;
import java.util.Vector;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.event.EventHandler;
@@ -14,12 +15,21 @@ import net.trueog.launchpadsog.LaunchPadsOG;
public class OnBlockPlace implements Listener {
@EventHandler
// ignoreCancelled: a placement a region plugin blocked must not become a
// working pad on disk anyway
@EventHandler(ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent event) {
ItemStack item = event.getItemInHand();
Block block = event.getBlockPlaced();
// the pad item is a pressure plate; lore alone must not qualify
if (item.getType() != Material.STONE_PRESSURE_PLATE) {
return;
}
Vector<Component> loreList = new Vector<>();
loreList.add(Component.text("Launches you in the"));
loreList.add(Component.text("direction you are facing"));
@@ -1,36 +1,60 @@
package net.trueog.launchpadsog.Listeners;
import java.util.ArrayList;
import net.trueog.launchpadsog.LaunchPadsOG;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import net.trueog.launchpadsog.LaunchPadsOG;
public class OnEntityDamage implements Listener {
// @EventHandler(priority = EventPriority.HIGHEST)
@EventHandler
// HIGHEST + ignoreCancelled: a cancelled event spends nothing, and little
// runs after us that could un-cancel our own cancellation.
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onEntityDamage(EntityDamageEvent event) {
// check if the damage was fall damage
if (event.getCause().equals(EntityDamageEvent.DamageCause.FALL)) {
// check that this is a landing hurting a player
if (!(event.getEntity() instanceof Player player) || !endsALaunch(event)) {
// check if the entity was a player
if (event.getEntity() instanceof Player) {
return;
// check the entity was in the launchedPlayers list
ArrayList<Player> launchedList = LaunchPadsOG.getLaunchedPlayers();
Player eventPlayer = (Player) event.getEntity();
if (launchedList.contains(eventPlayer)) {
}
event.setCancelled(true);
// Cancelled even at zero final damage, so the landing costs no hurt
// animation or absorption; pre-liftoff damage is never consumed.
if (LaunchPadsOG.consumeAirborneLaunch(player)) {
launchedList.remove(eventPlayer);
event.setCancelled(true);
}
}
// Landing on a stalagmite reports CONTACT rather than FALL, but it is
// still the launch arc ending, so both count.
private static boolean endsALaunch(EntityDamageEvent event) {
switch (event.getCause()) {
case FALL:
return true;
case CONTACT:
if (event instanceof EntityDamageByBlockEvent blockDamage) {
final Block damager = blockDamage.getDamager();
return damager != null && damager.getType() == Material.POINTED_DRIPSTONE;
}
return false;
}
default:
return false;
}
@@ -1,43 +0,0 @@
package net.trueog.launchpadsog.Listeners;
import net.trueog.launchpadsog.LaunchPadsOG;
import org.bukkit.GameEvent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.GenericGameEvent;
public class OnGenericGameEvent implements Listener {
// use this event to see if the player hit the ground
// if they did, then we can check if they were launched
// this way, even if they don't take fall damage from the fall
// say, they were using an elytra,
// this will ensure that the OnEntityDamage event is still called
// to remove them from the launched player List
@EventHandler
public void onGenericGameEvent(GenericGameEvent event) {
// check if the event was an entity hitting the ground
if (event.getEvent().equals(GameEvent.HIT_GROUND)) {
// if the entity was a player
if (event.getEntity() instanceof Player) {
Player player = (Player) event.getEntity();
// if the player was in the launched list
if (LaunchPadsOG.getLaunchedPlayers().contains(player)) {
// set the player's fall distance to 4
player.setFallDistance(4.0f);
}
}
}
}
}
@@ -0,0 +1,75 @@
package net.trueog.launchpadsog.Listeners;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityToggleGlideEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.spigotmc.event.entity.EntityMountEvent;
import net.trueog.launchpadsog.LaunchPadsOG;
// Every way of leaving a launch arc drops its protection, so normal
// fall damage applies again afterwards.
public class OnLaunchEnd implements Listener {
// opening an elytra turns the arc into controlled flight
@EventHandler(ignoreCancelled = true)
public void onGlide(EntityToggleGlideEvent event) {
if (event.isGliding() && event.getEntity() instanceof Player player) {
LaunchPadsOG.clearLaunch(player);
}
}
// a mount catches the player; later falls belong to the ride
@EventHandler(ignoreCancelled = true)
public void onMount(EntityMountEvent event) {
if (event.getEntity() instanceof Player player) {
LaunchPadsOG.clearLaunch(player);
}
}
// a teleport abandons the arc entirely
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTeleport(PlayerTeleportEvent event) {
LaunchPadsOG.clearLaunch(event.getPlayer());
}
// portal travel is dispatched on its own handler list, so the teleport
// handler above never sees it
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPortal(PlayerPortalEvent event) {
LaunchPadsOG.clearLaunch(event.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onDeath(PlayerDeathEvent event) {
LaunchPadsOG.clearLaunch(event.getEntity());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent event) {
LaunchPadsOG.clearLaunch(event.getPlayer());
}
}
@@ -1,10 +1,16 @@
package net.trueog.launchpadsog.Listeners;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.Tag;
import org.bukkit.block.Block;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -16,22 +22,13 @@ import net.trueog.launchpadsog.LaunchPadsOG;
public class OnPlayerMove implements Listener {
// One boost per pass: a glider re-arms a pad only by leaving its radius.
private final Map<UUID, Integer> glidePads = new HashMap<>();
@EventHandler
public void onPlayerMoveEvent(PlayerMoveEvent event) {
if (!event.hasChangedBlock()) {
return;
}
// Loop through every launch pad and look for one that has the same coordinates
// as
// the player.
final FileConfiguration padConfig = LaunchPadsOG.getConfiguration();
final Player player = event.getPlayer();
final String worldName = player.getWorld().getName();
final Location playerLocation = event.getTo();
if (playerLocation == null) {
@@ -39,147 +36,254 @@ public class OnPlayerMove implements Listener {
}
for (int i = 1; i < padConfig.getInt("numberOfPads") + 1; i++) {
final LaunchPadsOG.LaunchPhase phase = LaunchPadsOG.launchPhase(player);
if (phase == LaunchPadsOG.LaunchPhase.AWAITING_LIFTOFF) {
final String padWorld = padConfig.getString(i + ".world");
if (!worldName.equals(padWorld)) {
// A slow client reports the pad position for a full round trip, so
// the protection only arms once liftoff is actually seen.
if (player.getFallDistance() > 0.0F || !hasLandedAt(playerLocation))
LaunchPadsOG.markAirborne(player);
continue; // wrong world -> skip this pad
} else if (phase == LaunchPadsOG.LaunchPhase.AIRBORNE) {
// Arc over: held up again, not ascending (fallDistance stays 0 all
// ascent), no fall live. getTo(): this step's damage fires after us.
if (!player.isFlying() && player.getFallDistance() <= 0.0F
&& playerLocation.getY() <= event.getFrom().getY() && hasLandedAt(playerLocation))
LaunchPadsOG.clearLaunch(player);
}
if (!event.hasChangedBlock()) {
return;
}
// Riders are skipped: their move events carry the vehicle's position,
// and setVelocity cannot move a mounted player anyway.
final Integer padId = player.isInsideVehicle() ? null : padNear(player, playerLocation);
if (padId != null) {
// A glider skimming the radius matches on several packets in a
// row; only the first one fires the boost.
if (player.isGliding() && padId.equals(glidePads.put(player.getUniqueId(), padId))) {
return;
}
final boolean condition = padConfig.getInt(Integer.toString(i) + ".x") == playerLocation.getBlockX()
&& padConfig.getInt(Integer.toString(i) + ".y") == playerLocation.getBlockY()
&& padConfig.getInt(Integer.toString(i) + ".z") == playerLocation.getBlockZ();
// check y location
// check z location
// check each cardinal direction individually so that it can run faster
// check x location
if (condition) {
final FileConfiguration padConfig = LaunchPadsOG.getConfiguration();
final int i = padId;
// calculate the player's y-speed using getFrom() and getTo()
// y speed is the only one calculated so that a jump will affect
// how far you get launched
final Location from = event.getFrom();
final Location to = playerLocation;
// calculate the player's y-speed using getFrom() and getTo()
// y speed is the only one calculated so that a jump will affect
// how far you get launched
final Location from = event.getFrom();
final Location to = playerLocation;
// speed = distance/time
// distance = abs(from-to)
// time = 1 tick
double ySpeed = to.getY() - from.getY();
// speed = distance/time
// distance = abs(from-to)
// time = 1 tick
double ySpeed = to.getY() - from.getY();
// if the ySpeed is below a threshold of 0.15,
// then set the ySpeed to 0.15 so that there is some y movement
if (ySpeed < 0.15) {
// if the ySpeed is below a threshold of 0.15,
// then set the ySpeed to 0.15 so that there is some y movement
if (ySpeed < 0.15) {
ySpeed = 0.15;
ySpeed = 0.15;
}
}
// add 0.25 to the ySpeed so that there is more movement,
// without reducing the effect of a jump
ySpeed += 0.25;
// add 0.25 to the ySpeed so that there is more movement,
// without reducing the effect of a jump
ySpeed += 0.25;
// if the player is crouching, reduce the velocity to 1/3
if (player.isSneaking()) {
// if the player is crouching, reduce the velocity to 1/3
if (player.isSneaking()) {
ySpeed /= 3;
ySpeed /= 3;
}
}
// Multiply the velocity by how far the player is looking up or down.
final double velocityMult;
final double lookPitch = Location.normalizePitch(playerLocation.getPitch());
velocityMult = Math.abs(lookPitch - 90) / 90;
// Multiply the velocity by how far the player is looking up or down.
final double velocityMult;
final double lookPitch = Location.normalizePitch(playerLocation.getPitch());
velocityMult = Math.abs(lookPitch - 90) / 90;
// Get the unit vector for the x,z directions.
final Vector directionVector = playerLocation.getDirection();
// Get the unit vector for the x,z directions.
final Vector directionVector = playerLocation.getDirection();
// Get the xpower, ypower, and zpower.
final double xPower = padConfig.getDouble(i + ".xpower");
final double yPower = padConfig.getDouble(i + ".ypower");
final double zPower = padConfig.getDouble(i + ".zpower");
// Get the xpower, ypower, and zpower.
final double xPower = padConfig.getDouble(i + ".xpower");
final double yPower = padConfig.getDouble(i + ".ypower");
final double zPower = padConfig.getDouble(i + ".zpower");
final double vx = xPower * directionVector.getX() * velocityMult;
final double vy = yPower * ySpeed;
final double vz = zPower * directionVector.getZ() * velocityMult;
final double vx = xPower * directionVector.getX() * velocityMult;
final double vy = yPower * ySpeed;
final double vz = zPower * directionVector.getZ() * velocityMult;
player.setVelocity(new Vector(/* x */ vx, /* y */ vy, /* z */ vz));
player.setVelocity(new Vector(/* x */ vx, /* y */ vy, /* z */ vz));
// Spawn particles at the launch pad.
final Location padLocation = new Location(player.getWorld(), padConfig.getInt(i + ".x") + 0.5,
padConfig.getInt(i + ".y"), padConfig.getInt(i + ".z") + 0.5);
// Spawn particles at the launch pad.
final Location padLocation = new Location(player.getWorld(), padConfig.getInt(i + ".x") + 0.5,
padConfig.getInt(i + ".y"), padConfig.getInt(i + ".z") + 0.5);
// Hive java style launchpad woosh (audible to nearby players, fades with
// distance).
final float basePitch = (float) Math.max(1.62, Math.min(1.99, 1.80 + (velocityMult * 0.18)));
final float treblePitch = Math.min(2.0f, basePitch + 0.22f);
// Hive java style launchpad woosh (audible to nearby players, fades with
// distance).
final float basePitch = (float) Math.max(1.62, Math.min(1.99, 1.80 + (velocityMult * 0.18)));
final float treblePitch = Math.min(2.0f, basePitch + 0.22f);
for (int t = 0; t < 6; t++) {
for (int t = 0; t < 6; t++) {
final int delay = t;
final int delay = t;
Bukkit.getScheduler().runTaskLater(LaunchPadsOG.getPlugin(), () -> {
final float falloff = Math.max(0.0f, 1.0f - (delay * 0.16f));
final float jitter = ((delay & 1) == 0) ? 0.06f : -0.03f;
final float pMain = Math.max(0.5f, Math.min(2.0f, basePitch + jitter));
final float pTreble = Math.max(0.5f, Math.min(2.0f, treblePitch + jitter));
player.getWorld().playSound(padLocation, Sound.ENTITY_WITHER_SHOOT, 0.34f * falloff, pMain);
player.getWorld().playSound(padLocation, Sound.ENTITY_GHAST_SHOOT, 0.20f * falloff, pTreble);
player.getWorld().playSound(padLocation, Sound.ENTITY_BAT_TAKEOFF, 0.16f * falloff, 2.0f);
player.getWorld().playSound(padLocation, Sound.ENTITY_BLAZE_SHOOT, 0.10f * falloff, 2.0f);
}, delay);
}
final double padParticleCount = padConfig.getDouble("padLaunchParticleCount");
final double padParticlePower = padConfig.getDouble("padLaunchParticlePower");
player.spawnParticle(Particle.FIREWORKS_SPARK, padLocation, (int) padParticleCount, 0, 0, 0,
0.2 * padParticlePower);
final double playerParticleMult = padConfig.getDouble("playerLaunchParticleCountMultipier");
// Spawn particles on the player as they fly through the air
// This equation was gotten with a linear regression on different amounts of
// yPower
// to find what the vertex of the player's jump is
// maxes out at 25 ticks
// \/ --------- \/
for (int j = 0; j < Math.min(yPower * 8 - 15, 25); j++) {
// Max out the number of particles at 50
Bukkit.getScheduler().runTaskLater(LaunchPadsOG.getPlugin(),
() -> player.getWorld().spawnParticle(Particle.SPELL, player.getLocation(),
(int) Math.min(xPower * zPower * playerParticleMult, 50)),
j);
}
// Add the player to the launchedPlayers list
// 1 tick later so that the player is off the ground
// and isn't removed immediately,
Bukkit.getScheduler().runTaskLater(LaunchPadsOG.getPlugin(), () -> {
// Check if the player is in survival. Otherwise, don't add them
if (player.getGameMode().equals(GameMode.SURVIVAL)) {
final float falloff = Math.max(0.0f, 1.0f - (delay * 0.16f));
LaunchPadsOG.getLaunchedPlayers().add(player);
final float jitter = ((delay & 1) == 0) ? 0.06f : -0.03f;
final float pMain = Math.max(0.5f, Math.min(2.0f, basePitch + jitter));
final float pTreble = Math.max(0.5f, Math.min(2.0f, treblePitch + jitter));
}
player.getWorld().playSound(padLocation, Sound.ENTITY_WITHER_SHOOT, 0.34f * falloff, pMain);
player.getWorld().playSound(padLocation, Sound.ENTITY_GHAST_SHOOT, 0.20f * falloff, pTreble);
}, 1);
player.getWorld().playSound(padLocation, Sound.ENTITY_BAT_TAKEOFF, 0.16f * falloff, 2.0f);
player.getWorld().playSound(padLocation, Sound.ENTITY_BLAZE_SHOOT, 0.10f * falloff, 2.0f);
break;
}, delay);
}
final double padParticleCount = padConfig.getDouble("padLaunchParticleCount");
final double padParticlePower = padConfig.getDouble("padLaunchParticlePower");
// Visible to everyone nearby, matching the in-air trail below.
player.getWorld().spawnParticle(Particle.FIREWORKS_SPARK, padLocation, (int) padParticleCount, 0, 0, 0,
0.2 * padParticlePower);
final double playerParticleMult = padConfig.getDouble("playerLaunchParticleCountMultipier");
// Spawn particles on the player as they fly through the air
// This equation was gotten with a linear regression on different amounts of
// yPower
// to find what the vertex of the player's jump is
// maxes out at 25 ticks
// \/ --------- \/
for (int j = 0; j < Math.min(yPower * 8 - 15, 25); j++) {
// Max out the number of particles at 50
Bukkit.getScheduler()
.runTaskLater(
LaunchPadsOG.getPlugin(), () -> player.getWorld().spawnParticle(Particle.SPELL,
player.getLocation(), (int) Math.min(xPower * zPower * playerParticleMult, 50)),
j);
}
// Eligibility sampled at launch: adventure falls like survival,
// and an already-gliding player is in controlled flight.
final GameMode launchGamemode = player.getGameMode();
final boolean protectable = (launchGamemode == GameMode.SURVIVAL || launchGamemode == GameMode.ADVENTURE)
&& !player.isGliding();
if (protectable) {
if (phase == LaunchPadsOG.LaunchPhase.AIRBORNE && player.getFallDistance() > 0.0F) {
// Landing here out of a previous arc: that arc's landing
// damage spends first, so the new arc arms a tick later.
Bukkit.getScheduler().runTaskLater(LaunchPadsOG.getPlugin(), () -> {
if (player.isOnline() && !player.isDead()) {
LaunchPadsOG.markLaunched(player);
}
}, 1);
} else {
// Immediate: the pre-liftoff phase can neither be spent
// nor dropped, so there is nothing to wait for.
LaunchPadsOG.markLaunched(player);
}
}
} else {
// out of every pad's radius again: the glide debounce re-arms
glidePads.remove(player.getUniqueId());
}
}
// Gliders skim rather than stand, so a pad within one block still fires;
// everyone else must be inside the pad's own block, as always.
private static Integer padNear(Player player, Location location) {
final String worldName = location.getWorld().getName();
final int x = location.getBlockX();
final int y = location.getBlockY();
final int z = location.getBlockZ();
final Integer exact = LaunchPadsOG.padAt(worldName, x, y, z);
if (exact != null || !player.isGliding()) {
return exact;
}
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
final Integer pad = LaunchPadsOG.padAt(worldName, x + dx, y + dy, z + dz);
if (pad != null) {
return pad;
}
}
}
}
return null;
}
// Whether something at or right under this spot ends a fall: standing
// room on a block, water, or a climbable the player has grabbed.
private static boolean hasLandedAt(Location location) {
final Block feet = location.getBlock();
if (feet.isLiquid() || Tag.CLIMBABLE.isTagged(feet.getType())) {
return true;
}
// 0.4 covers blocks, slabs, and carpets; 1.6 covers fences and walls,
// whose standing surface sits above the block that holds them.
final Block shallow = location.clone().subtract(0.0, 0.4, 0.0).getBlock();
if (shallow.isLiquid() || !shallow.isPassable()) {
return true;
}
return !location.clone().subtract(0.0, 1.6, 0.0).getBlock().isPassable();
}
}