From db32bfd756f355d3f9a268b5237bedee29c3c8d8 Mon Sep 17 00:00:00 2001 From: M2B5 Date: Fri, 16 Jan 2026 21:29:39 +0000 Subject: [PATCH 1/7] First test version --- .../makkuusen/timing/system/TSListener.java | 12 +- .../me/makkuusen/timing/system/Tasks.java | 10 +- .../makkuusen/timing/system/TimingSystem.java | 1 + .../system/TimingSystemConfiguration.java | 24 ++ .../timing/system/commands/CommandHeat.java | 19 + .../system/commands/CommandTimingSystem.java | 36 ++ .../timing/system/database/MySQLDatabase.java | 6 +- .../system/database/SQLiteDatabase.java | 7 +- .../system/database/updates/Version14.java | 28 ++ .../timing/system/drs/PushToPass.java | 328 ++++++++++++++++++ .../me/makkuusen/timing/system/heat/Heat.java | 21 +- src/main/resources/config.yml | 5 + 12 files changed, 491 insertions(+), 6 deletions(-) create mode 100644 src/main/java/me/makkuusen/timing/system/database/updates/Version14.java create mode 100644 src/main/java/me/makkuusen/timing/system/drs/PushToPass.java diff --git a/src/main/java/me/makkuusen/timing/system/TSListener.java b/src/main/java/me/makkuusen/timing/system/TSListener.java index d237b461..f9bff164 100644 --- a/src/main/java/me/makkuusen/timing/system/TSListener.java +++ b/src/main/java/me/makkuusen/timing/system/TSListener.java @@ -9,6 +9,7 @@ import me.makkuusen.timing.system.database.EventDatabase; import me.makkuusen.timing.system.database.TSDatabase; import me.makkuusen.timing.system.database.TrackDatabase; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.heat.Heat; import me.makkuusen.timing.system.heat.HeatState; import me.makkuusen.timing.system.heat.Lap; @@ -191,7 +192,16 @@ public void onVehicleExit(VehicleExitEvent event) { } var maybeDriver = EventDatabase.getDriverFromRunningHeat(player.getUniqueId()); if (maybeDriver.isPresent()) { - if (maybeDriver.get().getState() == DriverState.LOADED || maybeDriver.get().getState() == DriverState.STARTING || maybeDriver.get().getState() == DriverState.RUNNING || maybeDriver.get().getState() == DriverState.RESET || maybeDriver.get().getState() == DriverState.LAPRESET) { + Driver driver = maybeDriver.get(); + + if (driver.getHeat().getPushToPass() != null && driver.getHeat().getPushToPass() + && driver.getState() == DriverState.RUNNING) { + PushToPass.togglePushToPass(player); + event.setCancelled(true); + return; + } + + if (driver.getState() == DriverState.LOADED || driver.getState() == DriverState.STARTING || driver.getState() == DriverState.RUNNING || driver.getState() == DriverState.RESET || driver.getState() == DriverState.LAPRESET) { event.setCancelled(true); return; } diff --git a/src/main/java/me/makkuusen/timing/system/Tasks.java b/src/main/java/me/makkuusen/timing/system/Tasks.java index 474cdaa7..b5bbe86b 100644 --- a/src/main/java/me/makkuusen/timing/system/Tasks.java +++ b/src/main/java/me/makkuusen/timing/system/Tasks.java @@ -5,6 +5,7 @@ import me.makkuusen.timing.system.database.TSDatabase; import me.makkuusen.timing.system.database.TrackDatabase; import me.makkuusen.timing.system.drs.DrsManager; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.heat.QualifyHeat; import me.makkuusen.timing.system.participant.Driver; import me.makkuusen.timing.system.participant.DriverState; @@ -164,9 +165,9 @@ private static void displayDriverTimer(Player player, Driver driver) { } private static String getPositionOrDrsDisplay(Driver driver) { + UUID playerId = driver.getTPlayer().getUniqueId(); + if (driver.getHeat().getDrs() != null && driver.getHeat().getDrs()) { - UUID playerId = driver.getTPlayer().getUniqueId(); - if (DrsManager.hasDrsActive(playerId)) { return "&s&lDRS"; } @@ -390,6 +391,11 @@ private void drawPolyRegion(TrackPolyRegion polyRegion, Player player, Particle public void startDrsCleanup(TimingSystem plugin) { Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, DrsManager::cleanupOldDetections, 100, 100); } + + public void startPushToPassUpdater(TimingSystem plugin) { + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, + PushToPass::updateAllCharges, 2, 2); + } } diff --git a/src/main/java/me/makkuusen/timing/system/TimingSystem.java b/src/main/java/me/makkuusen/timing/system/TimingSystem.java index 64acfa53..3a61a025 100644 --- a/src/main/java/me/makkuusen/timing/system/TimingSystem.java +++ b/src/main/java/me/makkuusen/timing/system/TimingSystem.java @@ -160,6 +160,7 @@ public void onEnable() { tasks.startParticleSpawner(plugin); tasks.generateTotalTime(plugin); tasks.startDrsCleanup(plugin); + tasks.startPushToPassUpdater(plugin); // Small check to make sure that PlaceholderAPI is installed if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { diff --git a/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java b/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java index dd468323..7084dbac 100644 --- a/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java +++ b/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java @@ -31,6 +31,10 @@ public class TimingSystemConfiguration { private Integer drsMaxDelta; private Integer drsDuration; private Double drsForwardAccel; + private Integer pushToPassMaxUseTime; + private Integer pushToPassFullChargeTime; + private Double pushToPassForwardAccel; + private Integer pushToPassStartingCharge; private final boolean frostHexAddOnEnabled; private final boolean medalsAddOnEnabled; private final boolean medalsShowNextMedal; @@ -74,6 +78,10 @@ public class TimingSystemConfiguration { drsMaxDelta = plugin.getConfig().getInt("drs.maxDelta", 1150); drsDuration = plugin.getConfig().getInt("drs.duration", 2000); drsForwardAccel = plugin.getConfig().getDouble("drs.forwardAccel", 0.06); + pushToPassMaxUseTime = plugin.getConfig().getInt("pushtopass.maxUseTime", 2000); + pushToPassFullChargeTime = plugin.getConfig().getInt("pushtopass.fullChargeTime", 30000); + pushToPassForwardAccel = plugin.getConfig().getDouble("pushtopass.forwardAccel", 0.06); + pushToPassStartingCharge = plugin.getConfig().getInt("pushtopass.startingCharge", 0); frostHexAddOnEnabled = plugin.getConfig().getBoolean("frosthexaddon.enabled"); medalsAddOnEnabled = plugin.getConfig().getBoolean("medalsaddon.enabled"); medalsShowNextMedal = plugin.getConfig().getBoolean("medalsaddon.showNextMedal"); @@ -148,6 +156,22 @@ public void setDrsForwardAccel(double value) { drsForwardAccel = value; } + public void setPushToPassMaxUseTime(int value) { + pushToPassMaxUseTime = value; + } + + public void setPushToPassFullChargeTime(int value) { + pushToPassFullChargeTime = value; + } + + public void setPushToPassForwardAccel(double value) { + pushToPassForwardAccel = value; + } + + public void setPushToPassStartingCharge(int value) { + pushToPassStartingCharge = value; + } + public T getDatabaseType() { // This could maybe be improved but I have no idea :P return (T) databaseType; diff --git a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java index 7cc32714..1a48993e 100644 --- a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java +++ b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java @@ -151,6 +151,17 @@ public static void onHeatInfo(Player player, Heat heat) { } player.sendMessage(drsMessage); + var pushToPassMessage = Component.text("Push to Pass: ").color(theme.getPrimary()); + + if (!heat.isFinished() && player.hasPermission("timingsystem.packs.eventadmin")) { + String p2pValue = (heat.getPushToPass() != null && heat.getPushToPass()) ? "true" : "false"; + pushToPassMessage = pushToPassMessage.append(theme.getEditButton(player, p2pValue, theme).clickEvent(ClickEvent.suggestCommand("/heat set pushtopass " + heat.getName() + " "))); + } else { + String p2pValue = (heat.getPushToPass() != null && heat.getPushToPass()) ? "enabled" : "disabled"; + pushToPassMessage = pushToPassMessage.append(theme.highlight(p2pValue)); + } + player.sendMessage(pushToPassMessage); + if (heat.getFastestLapUUID() != null) { Driver d = heat.getDrivers().get(heat.getFastestLapUUID()); player.sendMessage(Text.get(player, Info.HEAT_INFO_FASTEST_LAP, "%time%", ApiUtilities.formatAsTime(d.getBestLap().get().getLapTime()), "%player%", d.getTPlayer().getName())); @@ -397,6 +408,14 @@ public static void onHeatSetDrsDowntime(Player player, Heat heat, Integer laps) Text.send(player, Success.SAVED); } + @Subcommand("set pushtopass|p2p") + @CommandCompletion("@heat true|false") + @CommandPermission("%permissionheat_set_pushtopass") + public static void onHeatSetPushToPass(Player player, Heat heat, Boolean pushToPass) { + heat.setPushToPass(pushToPass); + Text.send(player, Success.SAVED); + } + @Subcommand("set lonely") @CommandCompletion("@heat true|false") @CommandPermission("%permissionheat_set_lonely") diff --git a/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java b/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java index ef048288..a3a1d5bc 100644 --- a/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java +++ b/src/main/java/me/makkuusen/timing/system/commands/CommandTimingSystem.java @@ -135,6 +135,42 @@ public static void onDrsForwardAccelChange(CommandSender sender, double value) { Text.send(sender, Success.SAVED); } + @Subcommand("pushtopass|p2p maxusetime") + @CommandCompletion("") + @CommandPermission("%permissiontimingsystem_pushtopass_set_maxusetime") + public static void onPushToPassMaxUseTimeChange(CommandSender sender, int value) { + TimingSystem.configuration.setPushToPassMaxUseTime(value); + Text.send(sender, Success.SAVED); + } + + @Subcommand("pushtopass|p2p fullchargetime") + @CommandCompletion("") + @CommandPermission("%permissiontimingsystem_pushtopass_set_fullchargetime") + public static void onPushToPassFullChargeTimeChange(CommandSender sender, int value) { + TimingSystem.configuration.setPushToPassFullChargeTime(value); + Text.send(sender, Success.SAVED); + } + + @Subcommand("pushtopass|p2p forwardaccel") + @CommandCompletion("") + @CommandPermission("%permissiontimingsystem_pushtopass_set_forwardaccel") + public static void onPushToPassForwardAccelChange(CommandSender sender, double value) { + TimingSystem.configuration.setPushToPassForwardAccel(value); + Text.send(sender, Success.SAVED); + } + + @Subcommand("pushtopass|p2p startingcharge") + @CommandCompletion("<0-100>") + @CommandPermission("%permissiontimingsystem_pushtopass_set_startingcharge") + public static void onPushToPassStartingChargeChange(CommandSender sender, int value) { + if (value < 0 || value > 100) { + Text.send(sender, Error.GENERIC); + return; + } + TimingSystem.configuration.setPushToPassStartingCharge(value); + Text.send(sender, Success.SAVED); + } + @Subcommand("shortname") @CommandCompletion(" @players") @CommandPermission("%permissiontimingsystem_shortname_others") diff --git a/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java b/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java index 4c46dd5b..c860961d 100644 --- a/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java +++ b/src/main/java/me/makkuusen/timing/system/database/MySQLDatabase.java @@ -60,7 +60,7 @@ public boolean update() { try { var row = DB.getFirstRow("SELECT * FROM `ts_version` ORDER BY `date` DESC;"); - int databaseVersion = 13; + int databaseVersion = 14; if (row == null) { // First startup DB.executeInsert("INSERT INTO `ts_version` (`version`, `date`) VALUES(?, ?);", databaseVersion, @@ -147,6 +147,9 @@ private static void updateDatabase(int previousVersion) throws SQLException { if (previousVersion < 13) { Version13.updateMySQL(); } + if (previousVersion < 14) { + Version14.updateMySQL(); + } } @@ -281,6 +284,7 @@ PRIMARY KEY (`id`) `boatSwitching` tinyint(1) DEFAULT NULL, `drs` tinyint(1) NOT NULL DEFAULT '0', `drsDowntime` int(11) DEFAULT NULL, + `pushToPass` tinyint(1) NOT NULL DEFAULT '0', `isRemoved` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"""); diff --git a/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java b/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java index 1d7a8485..8f71ffb5 100644 --- a/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java +++ b/src/main/java/me/makkuusen/timing/system/database/SQLiteDatabase.java @@ -31,7 +31,7 @@ public boolean update() { try { var row = DB.getFirstRow("SELECT * FROM `ts_version` ORDER BY `date` DESC;"); - int databaseVersion = 13; + int databaseVersion = 14; if (row == null) { // First startup DB.executeInsert("INSERT INTO `ts_version` (`version`, `date`) VALUES(?, ?);", databaseVersion, @@ -110,6 +110,10 @@ private static void updateDatabase(int previousVersion) throws SQLException { if (previousVersion < 13) { Version13.updateSQLite(); } + + if (previousVersion < 14) { + Version14.updateSQLite(); + } } @@ -235,6 +239,7 @@ public boolean createTables() { `collisionMode` TEXT DEFAULT NULL, `drs` INTEGER NOT NULL DEFAULT 0, `drsDowntime` INTEGER DEFAULT NULL, + `pushToPass` INTEGER NOT NULL DEFAULT 0, `isRemoved` INTEGER NOT NULL DEFAULT '0' );"""); diff --git a/src/main/java/me/makkuusen/timing/system/database/updates/Version14.java b/src/main/java/me/makkuusen/timing/system/database/updates/Version14.java new file mode 100644 index 00000000..d07f1280 --- /dev/null +++ b/src/main/java/me/makkuusen/timing/system/database/updates/Version14.java @@ -0,0 +1,28 @@ +package me.makkuusen.timing.system.database.updates; + +import co.aikar.idb.DB; + +import java.sql.SQLException; + +public class Version14 { + + public static void updateMySQL() throws SQLException { + try { + DB.executeUpdate("ALTER TABLE `ts_heats` ADD COLUMN `pushToPass` tinyint(1) NOT NULL DEFAULT 0"); + } catch (SQLException e) { + if (e.getErrorCode() != 1060) { + throw e; + } + } + } + + public static void updateSQLite() throws SQLException { + try { + DB.executeUpdate("ALTER TABLE `ts_heats` ADD COLUMN `pushToPass` INTEGER NOT NULL DEFAULT 0"); + } catch (SQLException e) { + if (!e.getMessage().toLowerCase().contains("duplicate column")) { + throw e; + } + } + } +} diff --git a/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java new file mode 100644 index 00000000..58a3a026 --- /dev/null +++ b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java @@ -0,0 +1,328 @@ +package me.makkuusen.timing.system.drs; + +import lombok.Getter; +import me.makkuusen.timing.system.TimingSystem; +import me.makkuusen.timing.system.api.TimingSystemAPI; +import me.makkuusen.timing.system.boatutils.BoatUtilsManager; +import me.makkuusen.timing.system.boatutils.CustomBoatUtilsMode; +import me.makkuusen.timing.system.database.EventDatabase; +import me.makkuusen.timing.system.heat.Heat; +import me.makkuusen.timing.system.loneliness.LonelinessController; +import me.makkuusen.timing.system.participant.Driver; +import me.makkuusen.timing.system.track.Track; +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.entity.Player; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class PushToPass { + + private static final Map pushToPassPlayers = new HashMap<>(); + private static final Map toggleCooldowns = new HashMap<>(); + private static final short PACKET_ID_SET_FORWARD_ACCELERATION = 11; + private static final long TOGGLE_COOLDOWN_MS = 500; + + /** + * Activates push to pass for a player if they have charge available + */ + public static void activatePushToPass(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data == null || data.isActive()) { + return; + } + + data.updateCharge(); + + if (data.getChargePercent() <= 0) { + return; + } + + data.setActive(true); + double forwardAccel = TimingSystem.configuration.getPushToPassForwardAccel(); + sendForwardAccelerationPacket(player, (float) forwardAccel); + + updateDriverScoreboard(playerId); + player.playSound(player.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_LAUNCH, 1.0f, 2.0f); + } + + /** + * Deactivates push to pass for a player + */ + public static void deactivatePushToPass(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data == null || !data.isActive()) { + return; + } + + data.updateCharge(); + data.setActive(false); + + resetToTrackSettings(player); + updateDriverScoreboard(playerId); + } + + /** + * Toggles push to pass on/off + */ + public static void togglePushToPass(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data == null) { + return; + } + + long currentTime = System.currentTimeMillis(); + Long lastToggle = toggleCooldowns.get(playerId); + if (lastToggle != null && (currentTime - lastToggle) < TOGGLE_COOLDOWN_MS) { + return; + } + + toggleCooldowns.put(playerId, currentTime); + + if (data.isActive()) { + deactivatePushToPass(player); + } else { + activatePushToPass(player); + } + } + + /** + * Initializes push to pass for a player (called when heat starts) + */ + public static void initializePushToPass(UUID playerId) { + int startingCharge = TimingSystem.configuration.getPushToPassStartingCharge(); + PushToPassData data = new PushToPassData(startingCharge); + pushToPassPlayers.put(playerId, data); + + Player player = Bukkit.getPlayer(playerId); + if (player != null) { + data.addPlayer(player); + } + } + + /** + * Cleans up push to pass data for a player + */ + public static void cleanupPlayer(UUID playerId) { + PushToPassData data = pushToPassPlayers.remove(playerId); + toggleCooldowns.remove(playerId); + if (data != null) { + if (data.isActive()) { + Player player = Bukkit.getPlayer(playerId); + if (player != null) { + resetToTrackSettings(player); + } + } + data.cleanup(); + } + } + + /** + * Gets the current charge percentage for a player (0-100) + */ + public static double getChargePercent(UUID playerId) { + PushToPassData data = pushToPassPlayers.get(playerId); + if (data == null) { + return 0; + } + data.updateCharge(); + return data.getChargePercent(); + } + + /** + * Checks if push to pass is currently active for a player + */ + public static boolean isPushToPassActive(UUID playerId) { + PushToPassData data = pushToPassPlayers.get(playerId); + return data != null && data.isActive(); + } + + /** + * Updates charge for all active players (should be called periodically) + */ + public static void updateAllCharges() { + for (Map.Entry entry : pushToPassPlayers.entrySet()) { + PushToPassData data = entry.getValue(); + data.updateCharge(); + double newCharge = data.getChargePercent(); + + if (data.isActive() && newCharge <= 0) { + UUID playerId = entry.getKey(); + Bukkit.getScheduler().runTask(TimingSystem.getPlugin(), () -> { + Player player = Bukkit.getPlayer(playerId); + if (player != null) { + deactivatePushToPass(player); + player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.5f); + } + }); + } + } + } + + private static void updateDriverScoreboard(UUID playerId) { + var maybeDriver = TimingSystemAPI.getDriverFromRunningHeat(playerId); + if (maybeDriver.isPresent()) { + Driver driver = maybeDriver.get(); + Heat heat = driver.getHeat(); + heat.getDrivers().values().forEach(Driver::updateScoreboard); + } + } + + private static void sendForwardAccelerationPacket(Player player, float acceleration) { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteStream)) { + out.writeShort(PACKET_ID_SET_FORWARD_ACCELERATION); + out.writeFloat(acceleration); + player.sendPluginMessage(TimingSystem.getPlugin(), "openboatutils:settings", byteStream.toByteArray()); + } catch (IOException e) { + TimingSystem.getPlugin().getLogger().warning("Failed to send Push to Pass forward acceleration packet to " + player.getName()); + e.printStackTrace(); + } + } + + private static void resetToTrackSettings(Player player) { + Optional maybeDriver = EventDatabase.getDriverFromRunningHeat(player.getUniqueId()); + if (maybeDriver.isPresent()) { + Heat heat = maybeDriver.get().getHeat(); + Track track = heat.getEvent().getTrack(); + if (track != null) { + Integer customModeId = track.getCustomBoatUtilsModeId(); + if (customModeId != null) { + CustomBoatUtilsMode bume = TimingSystem.getTrackDatabase().getCustomBoatUtilsModeFromId(customModeId); + if (bume != null && bume.applyToPlayer(player)) { + BoatUtilsManager.playerCustomBoatUtilsModeId.put(player.getUniqueId(), customModeId); + } else { + CustomBoatUtilsMode.resetPlayer(player); + BoatUtilsManager.playerCustomBoatUtilsModeId.remove(player.getUniqueId()); + var mode = track.getBoatUtilsMode(); + BoatUtilsManager.sendBoatUtilsModePluginMessage(player, mode, track, false); + } + } else { + CustomBoatUtilsMode.resetPlayer(player); + BoatUtilsManager.playerCustomBoatUtilsModeId.remove(player.getUniqueId()); + var mode = track.getBoatUtilsMode(); + BoatUtilsManager.sendBoatUtilsModePluginMessage(player, mode, track, false); + } + LonelinessController.updatePlayersVisibility(player); + LonelinessController.updatePlayerVisibility(player); + } + return; + } + + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteStream)) { + out.writeShort(0); + player.sendPluginMessage(TimingSystem.getPlugin(), "openboatutils:settings", byteStream.toByteArray()); + } catch (IOException e) { + TimingSystem.getPlugin().getLogger().warning("Failed to reset BoatUtils for " + player.getName()); + e.printStackTrace(); + } + } + + @Getter + private static class PushToPassData { + private double chargePercent; + private boolean active; + private Instant lastUpdate; + private BossBar bossBar; + + public PushToPassData(double startingCharge) { + this.chargePercent = Math.max(0, Math.min(100, startingCharge)); + this.active = false; + this.lastUpdate = Instant.now(); + this.bossBar = Bukkit.createBossBar("Push to Pass", BarColor.GREEN, BarStyle.SOLID); + this.bossBar.setProgress(startingCharge / 100.0); + } + + public void setActive(boolean active) { + this.active = active; + this.lastUpdate = Instant.now(); + updateBossBar(); + } + + public void addPlayer(Player player) { + if (bossBar != null && !bossBar.getPlayers().contains(player)) { + bossBar.addPlayer(player); + } + } + + public void cleanup() { + if (bossBar != null) { + bossBar.removeAll(); + bossBar = null; + } + } + + private void updateBossBar() { + if (bossBar == null) { + return; + } + + // Update progress (0.0 to 1.0) + bossBar.setProgress(Math.max(0.0, Math.min(1.0, chargePercent / 100.0))); + + if (active) { + bossBar.setColor(BarColor.PINK); + bossBar.setTitle(String.format("Push to Pass: ACTIVE (%.0f%%)", chargePercent)); + } else { + if (chargePercent >= 67) { + bossBar.setColor(BarColor.GREEN); + } else if (chargePercent >= 33) { + bossBar.setColor(BarColor.YELLOW); + } else { + bossBar.setColor(BarColor.RED); + } + bossBar.setTitle(String.format("Push to Pass: %.0f%%", chargePercent)); + } + } + + /** + * Updates the charge based on time elapsed since last update + */ + public void updateCharge() { + Instant now = Instant.now(); + long elapsedMillis = now.toEpochMilli() - lastUpdate.toEpochMilli(); + + if (elapsedMillis <= 0) { + return; + } + + if (active) { + // Draining + int maxUseTime = TimingSystem.configuration.getPushToPassMaxUseTime(); + double drainRate = 100.0 / maxUseTime; // percent per millisecond + chargePercent -= drainRate * elapsedMillis; + if (chargePercent < 0) { + chargePercent = 0; + } + } else { + // Charging + int fullChargeTime = TimingSystem.configuration.getPushToPassFullChargeTime(); + double chargeRate = 100.0 / fullChargeTime; // percent per millisecond + chargePercent += chargeRate * elapsedMillis; + if (chargePercent > 100) { + chargePercent = 100; + } + } + + lastUpdate = now; + updateBossBar(); + } + } +} diff --git a/src/main/java/me/makkuusen/timing/system/heat/Heat.java b/src/main/java/me/makkuusen/timing/system/heat/Heat.java index 4c02b841..315be07c 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/Heat.java +++ b/src/main/java/me/makkuusen/timing/system/heat/Heat.java @@ -10,6 +10,7 @@ import me.makkuusen.timing.system.api.events.driver.DriverPlacedOnGrid; import me.makkuusen.timing.system.database.EventDatabase; import me.makkuusen.timing.system.database.TSDatabase; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.event.Event; import me.makkuusen.timing.system.event.EventAnnouncements; import me.makkuusen.timing.system.event.EventResults; @@ -72,6 +73,7 @@ public class Heat { private Boolean boatSwitching; private Boolean drs; private Integer drsDowntime; + private Boolean pushToPass; private SpectatorScoreboard scoreboard; private Instant lastScoreboardUpdate = Instant.now(); @@ -101,6 +103,7 @@ public Heat(DbRow data, Round round) { boatSwitching = data.get("boatSwitching") instanceof Boolean ? data.get("boatSwitching") : data.get("boatSwitching") == null ? null : data.get("boatSwitching").equals(1); drs = data.get("drs") instanceof Boolean ? data.get("drs") : data.get("drs") == null ? false : data.get("drs").equals(1); drsDowntime = data.get("drsDowntime") == null ? 1 : data.getInt("drsDowntime"); + pushToPass = data.get("pushToPass") instanceof Boolean ? data.get("pushToPass") : data.get("pushToPass") == null ? false : data.get("pushToPass").equals(1); startDelay = data.get("startDelay") == null ? round instanceof FinalRound ? TimingSystem.configuration.getFinalStartDelayInMS() : TimingSystem.configuration.getQualyStartDelayInMS() : data.getInt("startDelay"); fastestLapUUID = data.getString("fastestLapUUID") == null ? null : UUID.fromString(data.getString("fastestLapUUID")); gridManager = new GridManager(round instanceof QualificationRound); @@ -212,6 +215,12 @@ public void startHeat() { } } + if (getPushToPass() != null && getPushToPass()) { + getDrivers().values().forEach(driver -> + me.makkuusen.timing.system.drs.PushToPass.initializePushToPass(driver.getTPlayer().getUniqueId()) + ); + } + if (round instanceof QualificationRound) { gridManager.startDriversWithDelay(getStartDelay(), true, getStartPositions()); return; @@ -268,6 +277,9 @@ public boolean finishHeat() { getDrivers().values().forEach(driver -> { EventDatabase.removePlayerFromRunningHeat(driver.getTPlayer().getUniqueId()); + + PushToPass.cleanupPlayer(driver.getTPlayer().getUniqueId()); + if (driver.getEndTime() == null) { driver.removeUnfinishedLap(); if (!driver.getLaps().isEmpty()) { @@ -287,7 +299,6 @@ public boolean finishHeat() { } }); - //Dump all laps to database getDrivers().values().forEach(driver -> driver.getLaps().forEach(EventDatabase::lapNew)); var heatResults = EventResults.generateHeatResults(this); @@ -338,6 +349,9 @@ public boolean resetHeat() { getDrivers().values().forEach(driver -> { driver.reset(); EventDatabase.removePlayerFromRunningHeat(driver.getTPlayer().getUniqueId()); + + PushToPass.cleanupPlayer(driver.getTPlayer().getUniqueId()); + if (driver.getTPlayer().getPlayer() != null) { LonelinessController.updatePlayersVisibility(driver.getTPlayer().getPlayer()); if (!LonelinessController.unghost(driver.getTPlayer().getUniqueId())) { @@ -595,6 +609,11 @@ public void setDrsDowntime(Integer drsDowntime) { TimingSystem.getEventDatabase().heatSet(getId(), "drsDowntime", drsDowntime); } + public void setPushToPass(Boolean pushToPass) { + this.pushToPass = pushToPass; + TimingSystem.getEventDatabase().heatSet(getId(), "pushToPass", pushToPass); + } + public void setCollisionMode(CollisionMode collisionMode) { this.collisionMode = collisionMode; TimingSystem.getEventDatabase().heatSet(getId(), "collisionMode", collisionMode.name()); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index ea8d728b..756adaf2 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -71,6 +71,11 @@ drs: maxDelta: 1150 duration: 2000 forwardAccel: 0.06 +pushtopass: + maxUseTime: 10000 + fullChargeTime: 30000 + forwardAccel: 0.08 + startingCharge: 0 sql: databaseType: SQLite # Options: MySQL, MariaDB, SQLite host: '127.0.0.1' From a7c590470c1fcbf6401b74cad0fa583dd5ac868d Mon Sep 17 00:00:00 2001 From: M2B5 Date: Fri, 16 Jan 2026 21:40:46 +0000 Subject: [PATCH 2/7] update default values --- .../makkuusen/timing/system/TimingSystemConfiguration.java | 6 +++--- src/main/resources/config.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java b/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java index 7084dbac..60357b41 100644 --- a/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java +++ b/src/main/java/me/makkuusen/timing/system/TimingSystemConfiguration.java @@ -78,9 +78,9 @@ public class TimingSystemConfiguration { drsMaxDelta = plugin.getConfig().getInt("drs.maxDelta", 1150); drsDuration = plugin.getConfig().getInt("drs.duration", 2000); drsForwardAccel = plugin.getConfig().getDouble("drs.forwardAccel", 0.06); - pushToPassMaxUseTime = plugin.getConfig().getInt("pushtopass.maxUseTime", 2000); - pushToPassFullChargeTime = plugin.getConfig().getInt("pushtopass.fullChargeTime", 30000); - pushToPassForwardAccel = plugin.getConfig().getDouble("pushtopass.forwardAccel", 0.06); + pushToPassMaxUseTime = plugin.getConfig().getInt("pushtopass.maxUseTime", 5000); + pushToPassFullChargeTime = plugin.getConfig().getInt("pushtopass.fullChargeTime", 60000); + pushToPassForwardAccel = plugin.getConfig().getDouble("pushtopass.forwardAccel", 0.05); pushToPassStartingCharge = plugin.getConfig().getInt("pushtopass.startingCharge", 0); frostHexAddOnEnabled = plugin.getConfig().getBoolean("frosthexaddon.enabled"); medalsAddOnEnabled = plugin.getConfig().getBoolean("medalsaddon.enabled"); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 756adaf2..5b73d371 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -72,9 +72,9 @@ drs: duration: 2000 forwardAccel: 0.06 pushtopass: - maxUseTime: 10000 - fullChargeTime: 30000 - forwardAccel: 0.08 + maxUseTime: 5000 + fullChargeTime: 60000 + forwardAccel: 0.05 startingCharge: 0 sql: databaseType: SQLite # Options: MySQL, MariaDB, SQLite From 2e4c6a5430601488d1490a9efff95a2bb950b34a Mon Sep 17 00:00:00 2001 From: M2B5 Date: Fri, 16 Jan 2026 23:01:17 +0000 Subject: [PATCH 3/7] Fix push to pass in boat swapping heats --- .../makkuusen/timing/system/heat/DriverSwapHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java index d33d2b18..f9246bd2 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java +++ b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java @@ -6,6 +6,7 @@ import me.makkuusen.timing.system.TimingSystem; import me.makkuusen.timing.system.api.events.driver.DriverSwapEvent; import me.makkuusen.timing.system.database.TSDatabase; +import me.makkuusen.timing.system.drs.PushToPass; import me.makkuusen.timing.system.participant.Driver; import me.makkuusen.timing.system.participant.DriverState; import me.makkuusen.timing.system.team.Team; @@ -255,6 +256,8 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, EventDatabase.removePlayerFromRunningHeat(oldDriverUUID); + PushToPass.cleanupPlayer(oldDriverUUID); + TPlayer tOldDriver = TSDatabase.getPlayer(oldDriverUUID); if (tOldDriver != null) { tOldDriver.clearScoreboard(); @@ -292,6 +295,11 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, EventDatabase.addPlayerToRunningHeat(newDriverObj); + // Initialize push to pass for the new driver if this is a push to pass heat + if (heat.getPushToPass() != null && heat.getPushToPass()) { + PushToPass.initializePushToPass(newDriverUUID); + } + // Update TeamHeatEntry with new active driver entry.swapDriver(newDriverUUID); From 48d025d73d82b55c3cbc3ae87e09754ffb19dca5 Mon Sep 17 00:00:00 2001 From: M2B5 Date: Fri, 23 Jan 2026 14:43:54 +0000 Subject: [PATCH 4/7] Qualifying for boat switching heats --- .../timing/system/commands/CommandHeat.java | 2 +- .../timing/system/drs/DrsManager.java | 2 +- .../timing/system/drs/PushToPass.java | 61 ++++--------------- .../timing/system/event/EventResults.java | 23 +++++++ .../me/makkuusen/timing/system/heat/Heat.java | 1 + .../timing/system/heat/TeamHeatEntry.java | 16 +++++ .../makkuusen/timing/system/round/Round.java | 46 +++++++++++++- src/main/resources/plugin.yml | 2 +- 8 files changed, 100 insertions(+), 53 deletions(-) diff --git a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java index 1a48993e..0921a06a 100644 --- a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java +++ b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java @@ -297,7 +297,7 @@ public static void onHeatRemove(Player player, Heat heat) { } @Subcommand("swap") - @CommandPermission("%permissionheat_driver_swap") + @CommandPermission("%permissionheat_driverswap") @Description("Take over for your team's offline driver") public static void onDriverSwap(Player player) { DriverSwapHandler.handleOfflineReplacement(player); diff --git a/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java b/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java index 23578daf..d7390301 100644 --- a/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java +++ b/src/main/java/me/makkuusen/timing/system/drs/DrsManager.java @@ -219,7 +219,7 @@ private static void sendForwardAccelerationPacket(Player player, float accelerat } } - private static void resetToTrackSettings(Player player) { + public static void resetToTrackSettings(Player player) { Optional maybeDriver = EventDatabase.getDriverFromRunningHeat(player.getUniqueId()); if (maybeDriver.isPresent()) { Heat heat = maybeDriver.get().getHeat(); diff --git a/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java index 58a3a026..3e74a82f 100644 --- a/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java +++ b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java @@ -3,13 +3,8 @@ import lombok.Getter; import me.makkuusen.timing.system.TimingSystem; import me.makkuusen.timing.system.api.TimingSystemAPI; -import me.makkuusen.timing.system.boatutils.BoatUtilsManager; -import me.makkuusen.timing.system.boatutils.CustomBoatUtilsMode; -import me.makkuusen.timing.system.database.EventDatabase; import me.makkuusen.timing.system.heat.Heat; -import me.makkuusen.timing.system.loneliness.LonelinessController; import me.makkuusen.timing.system.participant.Driver; -import me.makkuusen.timing.system.track.Track; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.boss.BarColor; @@ -23,7 +18,6 @@ import java.time.Instant; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.UUID; public class PushToPass { @@ -55,7 +49,7 @@ public static void activatePushToPass(Player player) { sendForwardAccelerationPacket(player, (float) forwardAccel); updateDriverScoreboard(playerId); - player.playSound(player.getLocation(), Sound.ENTITY_FIREWORK_ROCKET_LAUNCH, 1.0f, 2.0f); + player.playSound(player.getLocation(), Sound.BLOCK_BEACON_ACTIVATE, 1.0f, 2.0f); } /** @@ -72,8 +66,9 @@ public static void deactivatePushToPass(Player player) { data.updateCharge(); data.setActive(false); - resetToTrackSettings(player); + DrsManager.resetToTrackSettings(player); updateDriverScoreboard(playerId); + player.playSound(player.getLocation(), Sound.BLOCK_BEACON_DEACTIVATE, 1.0f, 0.5f); } /** @@ -126,7 +121,7 @@ public static void cleanupPlayer(UUID playerId) { if (data.isActive()) { Player player = Bukkit.getPlayer(playerId); if (player != null) { - resetToTrackSettings(player); + DrsManager.resetToTrackSettings(player); } } data.cleanup(); @@ -195,46 +190,7 @@ private static void sendForwardAccelerationPacket(Player player, float accelerat e.printStackTrace(); } } - - private static void resetToTrackSettings(Player player) { - Optional maybeDriver = EventDatabase.getDriverFromRunningHeat(player.getUniqueId()); - if (maybeDriver.isPresent()) { - Heat heat = maybeDriver.get().getHeat(); - Track track = heat.getEvent().getTrack(); - if (track != null) { - Integer customModeId = track.getCustomBoatUtilsModeId(); - if (customModeId != null) { - CustomBoatUtilsMode bume = TimingSystem.getTrackDatabase().getCustomBoatUtilsModeFromId(customModeId); - if (bume != null && bume.applyToPlayer(player)) { - BoatUtilsManager.playerCustomBoatUtilsModeId.put(player.getUniqueId(), customModeId); - } else { - CustomBoatUtilsMode.resetPlayer(player); - BoatUtilsManager.playerCustomBoatUtilsModeId.remove(player.getUniqueId()); - var mode = track.getBoatUtilsMode(); - BoatUtilsManager.sendBoatUtilsModePluginMessage(player, mode, track, false); - } - } else { - CustomBoatUtilsMode.resetPlayer(player); - BoatUtilsManager.playerCustomBoatUtilsModeId.remove(player.getUniqueId()); - var mode = track.getBoatUtilsMode(); - BoatUtilsManager.sendBoatUtilsModePluginMessage(player, mode, track, false); - } - LonelinessController.updatePlayersVisibility(player); - LonelinessController.updatePlayerVisibility(player); - } - return; - } - - try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - DataOutputStream out = new DataOutputStream(byteStream)) { - out.writeShort(0); - player.sendPluginMessage(TimingSystem.getPlugin(), "openboatutils:settings", byteStream.toByteArray()); - } catch (IOException e) { - TimingSystem.getPlugin().getLogger().warning("Failed to reset BoatUtils for " + player.getName()); - e.printStackTrace(); - } - } - + @Getter private static class PushToPassData { private double chargePercent; @@ -315,10 +271,17 @@ public void updateCharge() { // Charging int fullChargeTime = TimingSystem.configuration.getPushToPassFullChargeTime(); double chargeRate = 100.0 / fullChargeTime; // percent per millisecond + double oldCharge = chargePercent; chargePercent += chargeRate * elapsedMillis; if (chargePercent > 100) { chargePercent = 100; } + + if (oldCharge < 100 && chargePercent >= 100) { + for (Player player : bossBar.getPlayers()) { + player.playSound(player.getLocation(), Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.5f); + } + } } lastUpdate = now; diff --git a/src/main/java/me/makkuusen/timing/system/event/EventResults.java b/src/main/java/me/makkuusen/timing/system/event/EventResults.java index 338e8907..be779d1e 100644 --- a/src/main/java/me/makkuusen/timing/system/event/EventResults.java +++ b/src/main/java/me/makkuusen/timing/system/event/EventResults.java @@ -4,10 +4,13 @@ import me.makkuusen.timing.system.heat.Heat; import me.makkuusen.timing.system.participant.Driver; import me.makkuusen.timing.system.round.QualificationRound; +import me.makkuusen.timing.system.team.Team; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Getter public class EventResults { @@ -34,4 +37,24 @@ public static List generateRoundResults(List heats) { return results; } + + public static List generateTeamRoundResults(List heats) { + List driverResults = generateRoundResults(heats); + + List teams = new ArrayList<>(); + Set addedTeamIds = new HashSet<>(); + + for (Driver driver : driverResults) { + var teamEntry = driver.getHeat().getTeamEntryByPlayer(driver.getTPlayer().getUniqueId()); + if (teamEntry.isPresent() && teamEntry.get().getTeam() != null) { + int teamId = teamEntry.get().getTeam().getId(); + if (!addedTeamIds.contains(teamId)) { + teams.add(teamEntry.get().getTeam()); + addedTeamIds.add(teamId); + } + } + } + + return teams; + } } diff --git a/src/main/java/me/makkuusen/timing/system/heat/Heat.java b/src/main/java/me/makkuusen/timing/system/heat/Heat.java index 315be07c..18ce6633 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/Heat.java +++ b/src/main/java/me/makkuusen/timing/system/heat/Heat.java @@ -713,6 +713,7 @@ public void addTeamToHeat(Team team, int startPosition) { if (firstOnlinePlayer != null) { entry.setActiveDriver(firstOnlinePlayer); + EventDatabase.heatDriverNew(firstOnlinePlayer, this, startPosition); } teamEntries.put(team.getId(), entry); diff --git a/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java b/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java index 84560365..a5f140da 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java +++ b/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java @@ -136,4 +136,20 @@ public void setEndTime(Instant endTime) { this.endTime = endTime; TimingSystem.getEventDatabase().teamHeatEntrySet(id, "endTime", endTime == null ? null : endTime.toEpochMilli()); } + + public java.util.Optional getBestLap() { + if (laps.isEmpty()) { + return java.util.Optional.empty(); + } + if (laps.get(0).getLapTime() == -1) { + return java.util.Optional.empty(); + } + Lap bestLap = laps.get(0); + for (Lap lap : laps) { + if (lap.getLapTime() != -1 && lap.getLapTime() < bestLap.getLapTime()) { + bestLap = lap; + } + } + return java.util.Optional.of(bestLap); + } } diff --git a/src/main/java/me/makkuusen/timing/system/round/Round.java b/src/main/java/me/makkuusen/timing/system/round/Round.java index 877a9daf..e9c485af 100644 --- a/src/main/java/me/makkuusen/timing/system/round/Round.java +++ b/src/main/java/me/makkuusen/timing/system/round/Round.java @@ -9,6 +9,7 @@ import me.makkuusen.timing.system.heat.Heat; import me.makkuusen.timing.system.heat.HeatState; import me.makkuusen.timing.system.participant.Driver; +import me.makkuusen.timing.system.team.Team; import me.makkuusen.timing.system.track.locations.TrackLocation; import java.util.ArrayList; @@ -102,7 +103,15 @@ public boolean finish(Event event) { return true; } - getEvent().getEventSchedule().getNextRound().get().initRound(drivers); + Round nextRound = getEvent().getEventSchedule().getNextRound().get(); + + if (allHeatsHaveBoatSwapping() && nextRound.allHeatsHaveBoatSwapping()) { + List teams = EventResults.generateTeamRoundResults(getHeats()); + nextRound.initRoundWithTeams(teams); + } else { + nextRound.initRound(drivers); + } + event.eventSchedule.nextRound(); return true; } @@ -122,6 +131,41 @@ public void addDriversToHeats(List drivers) { } } + public boolean allHeatsHaveBoatSwapping() { + if (getHeats().isEmpty()) { + return false; + } + return getHeats().stream().allMatch(Heat::isBoatSwitchingEnabled); + } + + public void initRoundWithTeams(List teams) { + if (getHeats().isEmpty()) { + int maxDrivers = getEvent().getTrack().getTrackLocations().getLocations(TrackLocation.Type.GRID).size(); + int heats = teams.size() / maxDrivers; + if (teams.size() % maxDrivers != 0) { + heats++; + } + for (int i = 0; i < heats; i++) { + createHeat(i + 1); + } + } + addTeamsToHeats(teams); + setState(RoundState.RUNNING); + } + + public void addTeamsToHeats(List teams) { + int i = 0; + for (Heat heat : getHeats()) { + int startPos = 1; + for (; i < teams.size(); i++) { + if (startPos > heat.getMaxDrivers()) { + break; + } + heat.addTeamToHeat(teams.get(i), startPos++); + } + } + } + public void setState(RoundState state) { this.state = state; TimingSystem.getEventDatabase().roundSet(getId(), "state", state.name()); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5190d2f2..81307e84 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -79,7 +79,7 @@ permissions: timingsystem.heat.list: true timingsystem.heat.results: true timingsystem.heat.quit: true - timingsystem.heat.driver.swap: true + timingsystem.heat.driverswap: true timingsystem.packs.trackbuilder: description: Player has the tools to build a track From 21758ac1376605918c99ef8444f512aae5545414 Mon Sep 17 00:00:00 2001 From: M2B5 Date: Fri, 23 Jan 2026 15:10:05 +0000 Subject: [PATCH 5/7] disable push to pass in inpit regions, transfer push to pass charge on driver swap --- .../makkuusen/timing/system/TSListener.java | 3 ++ .../timing/system/drs/PushToPass.java | 51 +++++++++++++++++++ .../timing/system/heat/DriverSwapHandler.java | 13 +++-- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/main/java/me/makkuusen/timing/system/TSListener.java b/src/main/java/me/makkuusen/timing/system/TSListener.java index f9bff164..46820b9a 100644 --- a/src/main/java/me/makkuusen/timing/system/TSListener.java +++ b/src/main/java/me/makkuusen/timing/system/TSListener.java @@ -780,6 +780,9 @@ private static void handleHeat(Driver driver, PlayerMoveEvent e) { if (trackRegion.contains(player.getLocation()) && !inPits.contains(player.getUniqueId())) { inPits.add(player.getUniqueId()); heat.updatePositions(); + if (driver.getHeat().getPushToPass() != null && driver.getHeat().getPushToPass()) { + PushToPass.handleInpitEntry(player); + } } else if (!trackRegion.contains(player.getLocation()) && inPits.contains(player.getUniqueId())) { inPits.remove(player.getUniqueId()); heat.updatePositions(); diff --git a/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java index 3e74a82f..7d15d877 100644 --- a/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java +++ b/src/main/java/me/makkuusen/timing/system/drs/PushToPass.java @@ -38,6 +38,10 @@ public static void activatePushToPass(Player player) { return; } + if (isPlayerInInpitRegion(player)) { + return; + } + data.updateCharge(); if (data.getChargePercent() <= 0) { @@ -128,6 +132,53 @@ public static void cleanupPlayer(UUID playerId) { } } + /** + * Transfers push to pass charge from one player to another (used during driver swaps) + */ + public static void transferPushToPass(UUID fromPlayerId, UUID toPlayerId) { + PushToPassData fromData = pushToPassPlayers.get(fromPlayerId); + if (fromData == null) { + return; + } + + fromData.updateCharge(); + + PushToPassData toData = new PushToPassData(fromData.getChargePercent()); + pushToPassPlayers.put(toPlayerId, toData); + + Player toPlayer = Bukkit.getPlayer(toPlayerId); + if (toPlayer != null) { + toData.addPlayer(toPlayer); + } + + cleanupPlayer(fromPlayerId); + } + + /** + * Deactivates push to pass if player enters an inpit region + */ + public static void handleInpitEntry(Player player) { + UUID playerId = player.getUniqueId(); + PushToPassData data = pushToPassPlayers.get(playerId); + + if (data != null && data.isActive()) { + deactivatePushToPass(player); + } + } + + /** + * Checks if a player is currently in an inpit region + */ + private static boolean isPlayerInInpitRegion(Player player) { + var maybeDriver = TimingSystemAPI.getDriverFromRunningHeat(player.getUniqueId()); + if (maybeDriver.isEmpty()) { + return false; + } + + Driver driver = maybeDriver.get(); + return driver.isInPit(player.getLocation()); + } + /** * Gets the current charge percentage for a player (0-100) */ diff --git a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java index f9246bd2..442483e2 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java +++ b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java @@ -256,7 +256,11 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, EventDatabase.removePlayerFromRunningHeat(oldDriverUUID); - PushToPass.cleanupPlayer(oldDriverUUID); + if (heat.getPushToPass() != null && heat.getPushToPass()) { + PushToPass.transferPushToPass(oldDriverUUID, newDriverUUID); + } else { + PushToPass.cleanupPlayer(oldDriverUUID); + } TPlayer tOldDriver = TSDatabase.getPlayer(oldDriverUUID); if (tOldDriver != null) { @@ -294,12 +298,7 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, Driver::getPosition)); EventDatabase.addPlayerToRunningHeat(newDriverObj); - - // Initialize push to pass for the new driver if this is a push to pass heat - if (heat.getPushToPass() != null && heat.getPushToPass()) { - PushToPass.initializePushToPass(newDriverUUID); - } - + // Update TeamHeatEntry with new active driver entry.swapDriver(newDriverUUID); From e2f4a87a6e0c70a0c26ed1433789c8d0913bc3ae Mon Sep 17 00:00:00 2001 From: M2B5 Date: Fri, 23 Jan 2026 15:38:35 +0000 Subject: [PATCH 6/7] Make boat switches award pits, prevent multiple boat switches in the same lap --- .../java/me/makkuusen/timing/system/TSListener.java | 12 +++++++----- .../timing/system/heat/DriverSwapHandler.java | 10 ++++++++++ .../makkuusen/timing/system/heat/TeamHeatEntry.java | 7 ++++++- .../timing/system/theme/messages/Error.java | 6 +++--- src/main/resources/lang/en_us.yml | 1 + 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/main/java/me/makkuusen/timing/system/TSListener.java b/src/main/java/me/makkuusen/timing/system/TSListener.java index 46820b9a..e488a252 100644 --- a/src/main/java/me/makkuusen/timing/system/TSListener.java +++ b/src/main/java/me/makkuusen/timing/system/TSListener.java @@ -748,11 +748,13 @@ private static void handleHeat(Driver driver, PlayerMoveEvent e) { if (driver.getHeat().getRound() instanceof FinalRound) { // Check for pitstop - for (var r : track.getTrackRegions().getRegions(TrackRegion.RegionType.PIT)) { - if (r.contains(player.getLocation())) { - if (driver.passPit()) { - heat.updatePositions(); - break; + if (!heat.isBoatSwitchingEnabled()) { + for (var r : track.getTrackRegions().getRegions(TrackRegion.RegionType.PIT)) { + if (r.contains(player.getLocation())) { + if (driver.passPit()) { + heat.updatePositions(); + break; + } } } } diff --git a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java index 442483e2..0561bdb4 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java +++ b/src/main/java/me/makkuusen/timing/system/heat/DriverSwapHandler.java @@ -181,6 +181,10 @@ private static SwapValidation validateSwap(Player requester, Player target, bool return SwapValidation.invalid(Error.DRIVER_SWAP_HEAT_FINISHED, true); } + if (entry.hasSwappedThisLap()) { + return SwapValidation.invalid(Error.DRIVER_SWAP_ALREADY_SWAPPED_THIS_LAP, true); + } + if (requirePitRegion) { Track track = heat.getEvent().getTrack(); @@ -302,6 +306,12 @@ private static void transferDriverState(TeamHeatEntry entry, UUID oldDriverUUID, // Update TeamHeatEntry with new active driver entry.swapDriver(newDriverUUID); + // Award a pit for driver swaps (/heat swap doesn't count) + if (swapType == DriverSwapEvent.SwapType.RIGHT_CLICK) { + entry.incrementPits(); + newDriverObj.setPits(entry.getPits()); + } + // If old driver held the fastest lap, transfer it to new driver if (heat.getFastestLapUUID() != null && heat.getFastestLapUUID().equals(oldDriverUUID)) { heat.setFastestLapUUID(newDriverUUID); diff --git a/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java b/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java index a5f140da..09d3f756 100644 --- a/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java +++ b/src/main/java/me/makkuusen/timing/system/heat/TeamHeatEntry.java @@ -32,7 +32,7 @@ public class TeamHeatEntry { private int pits; private List laps; private boolean finished; - + private int lastSwapLap = -1; public TeamHeatEntry(DbRow data, Heat heat) { this.id = data.getInt("id"); this.heat = heat; @@ -74,6 +74,11 @@ public boolean isPlayerInTeam(UUID playerUUID) { public void swapDriver(UUID newDriverUUID) { setActiveDriver(newDriverUUID); + this.lastSwapLap = this.currentLap; + } + + public boolean hasSwappedThisLap() { + return this.lastSwapLap == this.currentLap; } public Location getLastCheckpointLocation() { diff --git a/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java b/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java index 88a1c170..3a37a2ab 100644 --- a/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java +++ b/src/main/java/me/makkuusen/timing/system/theme/messages/Error.java @@ -111,8 +111,8 @@ public enum Error implements Message { DRIVER_SWAP_NOT_IN_PIT, DRIVER_SWAP_ALREADY_IN_HEAT, DRIVER_SWAP_DRIVER_ONLINE, - DRIVER_SWAP_NO_ONLINE_MEMBERS - ; + DRIVER_SWAP_NO_ONLINE_MEMBERS, + DRIVER_SWAP_ALREADY_SWAPPED_THIS_LAP; Error() {} @@ -120,4 +120,4 @@ public enum Error implements Message { public String getKey() { return "error." + this.name().toLowerCase(); } -} \ No newline at end of file +} diff --git a/src/main/resources/lang/en_us.yml b/src/main/resources/lang/en_us.yml index 986c1af6..2136f4c1 100644 --- a/src/main/resources/lang/en_us.yml +++ b/src/main/resources/lang/en_us.yml @@ -122,6 +122,7 @@ error: driver_swap_not_same_team: "&eYou are not on the same team as that driver." driver_swap_not_in_active_heat: "&eYou are not in an active team heat." driver_swap_boat_switching_disabled: "&eDriver swapping is only available in boat switching mode." + driver_swap_already_swapped_this_lap: "&eYour team has already swapped drivers this lap." warning: dangerous_command: "&wThis command is dangerous, Type \"%command%\" to use it anyways" From fc4d37ef652f1ce66e1680d7008d3157030b5822 Mon Sep 17 00:00:00 2001 From: M2B5 Date: Sun, 1 Feb 2026 22:48:21 +0000 Subject: [PATCH 7/7] update boat switching perms, fix delta ghosting --- .../timing/system/commands/CommandHeat.java | 2 +- .../loneliness/LonelinessController.java | 22 +++++++++++++++++-- .../system/permissions/PermissionHeat.java | 3 ++- .../permissions/PermissionTimingSystem.java | 4 ++++ src/main/resources/plugin.yml | 16 +++++++++++++- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java index 0921a06a..1a48993e 100644 --- a/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java +++ b/src/main/java/me/makkuusen/timing/system/commands/CommandHeat.java @@ -297,7 +297,7 @@ public static void onHeatRemove(Player player, Heat heat) { } @Subcommand("swap") - @CommandPermission("%permissionheat_driverswap") + @CommandPermission("%permissionheat_driver_swap") @Description("Take over for your team's offline driver") public static void onDriverSwap(Player player) { DriverSwapHandler.handleOfflineReplacement(player); diff --git a/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java b/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java index b0d97f51..d369ea19 100644 --- a/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java +++ b/src/main/java/me/makkuusen/timing/system/loneliness/LonelinessController.java @@ -174,7 +174,16 @@ private static void showHeatPlayersOnly(Player player, Heat heat) { } } - if (shouldShow && !ghostedPlayers.contains(otherPlayer.getUniqueId())) { + boolean isManuallyGhosted = ghostedPlayers.contains(otherPlayer.getUniqueId()); + boolean isDeltaGhosted = false; + + Driver viewingDriver = heat.getDrivers().get(player.getUniqueId()); + Driver otherDriver = heat.getDrivers().get(otherPlayer.getUniqueId()); + if (viewingDriver != null && otherDriver != null) { + isDeltaGhosted = DeltaGhostingController.isDeltaGhosted(viewingDriver, otherDriver); + } + + if (shouldShow && !isManuallyGhosted && !isDeltaGhosted) { showPlayerAndCustomBoat(player, otherPlayer); } else { hidePlayerAndCustomBoat(player, otherPlayer); @@ -268,7 +277,16 @@ private static void processPlayerVisibilityForOther(Player targetPlayer, Player return; } - if (ghostedPlayers.contains(targetPlayer.getUniqueId())) { + boolean isManuallyGhosted = ghostedPlayers.contains(targetPlayer.getUniqueId()); + boolean isDeltaGhosted = false; + + if (viewingIsDriver && targetMaybeDriver.isPresent()) { + Driver viewingDriver = viewingMaybeDriver.get(); + Driver targetDriver = targetMaybeDriver.get(); + isDeltaGhosted = DeltaGhostingController.isDeltaGhosted(viewingDriver, targetDriver); + } + + if (isManuallyGhosted || isDeltaGhosted) { hidePlayerAndCustomBoat(viewingPlayer, targetPlayer); return; } diff --git a/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java b/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java index 832fd9f2..87d5dfe4 100644 --- a/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java +++ b/src/main/java/me/makkuusen/timing/system/permissions/PermissionHeat.java @@ -34,7 +34,8 @@ public enum PermissionHeat implements Permissions { SORT_TT, SORT_RANDOM, ADD_STREAKER, - REMOVESTREAKER; + REMOVESTREAKER, + DRIVER_SWAP; @Override public String getNode() { diff --git a/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java b/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java index fdc354ab..67d1f6a2 100644 --- a/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java +++ b/src/main/java/me/makkuusen/timing/system/permissions/PermissionTimingSystem.java @@ -19,6 +19,10 @@ public enum PermissionTimingSystem implements Permissions { DRS_SET_MAXDELTA, DRS_SET_DURATION, DRS_SET_FORWARDACCEL, + PUSHTOPASS_SET_MAXUSETIME, + PUSHTOPASS_SET_FULLCHARGETIME, + PUSHTOPASS_SET_FORWARDACCEL, + PUSHTOPASS_SET_STARTINGCHARGE, COLOR_SET_NAMED, COLOR_SET_HEX, GHOST; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 81307e84..fd68c52e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -79,7 +79,7 @@ permissions: timingsystem.heat.list: true timingsystem.heat.results: true timingsystem.heat.quit: true - timingsystem.heat.driverswap: true + timingsystem.heat.driver.swap: true timingsystem.packs.trackbuilder: description: Player has the tools to build a track @@ -170,6 +170,7 @@ permissions: timingsystem.heat.set.maxdrivers: true timingsystem.heat.set.driverposition: true timingsystem.heat.set.reversegrid: true + timingsystem.heat.set.boatswitching: true timingsystem.heat.sort.tt: true timingsystem.heat.sort.random: true timingsystem.shortname.others: true @@ -188,6 +189,19 @@ permissions: timingsystem.packs.eventhoster: true timingsystem.packs.racehoster: true timingsystem.event.delete: true + timingsystem.drs.set.mindelta: true + timingsystem.drs.set.maxdelta: true + timingsystem.drs.set.duration: true + timingsystem.drs.set.forwardaccel: true + timingsystem.pushtopass.set.maxusetime: true + timingsystem.pushtopass.set.fullchargetime: true + timingsystem.pushtopass.set.forwardaccel: true + timingsystem.pushtopass.set.startingcharge: true + timingsystem.team.create: true + timingsystem.team.delete: true + timingsystem.team.manage: true + timingsystem.team.info: true + timingsystem.team.list: true timingsystem.packs.admin: description: Player has access to most commands