fix(login): F1+F2+F4 — void-death guard, retry recovery, AuthMe pre-empt
Closes the YOU500 inventory-loss path documented in AUDIT-2026-05-07.md. Bumps to 1.1.0 per CHANGELOG. F1 — void-damage guard: new EntityDamageEvent listener at HIGHEST, ignoreCancelled=true. While a UUID sits in pendingTransit, DamageCause.VOID is cancelled, the player is healed to 20.0, fall/fire reset, sync-TP back to limbo spawn. Console gets a WARN with intended TP target. This single guard would have saved YOU500's inventory. F2 — recovery on teleportAsync false / exception: replaces the previous log-only branch in doTeleport. On failure: sync-TP to limbo spawn, bump per-UUID retry counter, schedule retry after 30 ticks. After MAX_RETRIES=3 failures: leave at limbo spawn in GameMode.SPECTATOR, log SEVERE with full saved coords + reason, alert console for manual `/authlimbo tp` action. Player stays in pendingTransit across retries so F1 keeps protecting them. F4 — pre-empt AuthMe's broken teleport: new LoginEvent handler at EventPriority.LOWEST that adds the UUID to pendingTransit and sync-TPs the player to limbo spawn BEFORE AuthMe-ReReloaded's own internal post-login teleport runs. AuthMe's teleport then operates against an irrelevant location; the existing MONITOR handler still wins last with the authoritative restore. Net effect: closes the void-death window even when the saved chunk is far out and slow to load. Internal: ConcurrentHashMap-backed pendingTransit set + retryCounts map, both watchdog-timed-out after 5 s so we never leak entries on edge cases. No new Maven dependencies. Privacy invariant unchanged — F4 actually strengthens limbo-on-join by guaranteeing the limbo position is reasserted at LOGIN-LOWEST. Build: mvn -B clean package (in maven:3.9-eclipse-temurin-21 container) -> BUILD SUCCESS, AuthLimbo-1.1.0.jar produced.
This commit is contained in:
parent
1f9d4bb198
commit
afea34badc
3 changed files with 339 additions and 13 deletions
59
CHANGELOG.md
59
CHANGELOG.md
|
|
@ -4,6 +4,65 @@ All notable changes to AuthLimbo are documented here.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.1.0] - 2026-05-07
|
||||
|
||||
Data-loss bug fix release. Triggered by the YOU500 incident on
|
||||
`racked.ru` at 2026-05-07 17:13:39 UTC — full inventory void-death during
|
||||
AuthMe's post-login teleport. See `AUDIT-2026-05-07.md` for the full
|
||||
forensic trace and `ROADMAP.md` for tracking.
|
||||
|
||||
### Added
|
||||
- **F1 — VOID-damage guard during post-login restore.** New
|
||||
`EntityDamageEvent` listener at `EventPriority.HIGHEST,
|
||||
ignoreCancelled=true`. While a player UUID sits in `pendingTransit`
|
||||
(post-`LoginEvent`, pre-restore-success), `DamageCause.VOID` events are
|
||||
cancelled, the player is healed to full, and sync-teleported back to
|
||||
limbo spawn. Console gets a WARN with the player name + intended TP
|
||||
target. This single guard would have saved YOU500's inventory.
|
||||
- **F2 — Recovery on `teleportAsync` future == false.** The previous
|
||||
log-and-abandon branch in `LoginListener.doTeleport` is replaced with a
|
||||
retry loop. On any failure (false future, exceptional future, or
|
||||
chunk-load throw): synchronously snap the player back to limbo spawn,
|
||||
increment a per-UUID retry counter, schedule another `doTeleport` after
|
||||
30 ticks (~1.5 s). After 3 failures: leave the player at limbo spawn
|
||||
in `GameMode.SPECTATOR`, log SEVERE with full saved coords + retry
|
||||
count, and alert console with manual-intervention instructions
|
||||
(`/authlimbo tp <player>`). Players stay in `pendingTransit` across
|
||||
retries so F1 keeps protecting them.
|
||||
- **F4 — Pre-empt AuthMe's broken teleport.** New `LoginEvent` listener
|
||||
at `EventPriority.LOWEST` (runs BEFORE AuthMe-ReReloaded's own internal
|
||||
post-login teleport). Action: synchronously TP the player back to
|
||||
limbo spawn and add their UUID to `pendingTransit`. AuthMe's
|
||||
subsequent teleport then operates against an irrelevant location, and
|
||||
our existing MONITOR handler still wins last with the authoritative
|
||||
restore. Net effect: closes the void-death window even when the saved
|
||||
chunk is far out and slow to load.
|
||||
|
||||
### Internal
|
||||
- New `Set<UUID> pendingTransit` (ConcurrentHashMap.newKeySet) and
|
||||
`Map<UUID, Integer> retryCounts` on `LoginListener`. Both are
|
||||
watchdog-timed-out after 5 s so we never leak entries on edge cases.
|
||||
- Constants: `MAX_RETRIES=3`, `RETRY_DELAY_TICKS=30`,
|
||||
`PENDING_TIMEOUT_TICKS=100`.
|
||||
- No new Maven dependencies. No new public API.
|
||||
|
||||
### Privacy
|
||||
- Limbo-on-join invariant unchanged. F4 actually *strengthens* it by
|
||||
guaranteeing the limbo position is reasserted at LOGIN-LOWEST.
|
||||
|
||||
### Test plan (reproduces YOU500 in dev Paper 1.21.x + AuthMe-ReReloaded fork b49)
|
||||
- Set saved coord far out (e.g. X=10000, Z=10000) in `authme.db` for a
|
||||
test account so the chunk is unloaded at login. Restart server. Login.
|
||||
Expect: F4 sync-TPs to limbo spawn first; F2 retries on false future;
|
||||
F1 catches any VOID damage during transit; player ends up at saved
|
||||
coords with full inventory.
|
||||
- Set saved Y above world build limit (e.g. 5000). Login. Expect: F2
|
||||
recovery branch retries up to 3 times, then drops the player into
|
||||
spectator at limbo spawn with admin alert.
|
||||
- Trigger a synthetic VOID damage during transit (debug command).
|
||||
Expect: F1 cancels the damage, snaps player back to limbo spawn at
|
||||
full health, restore continues.
|
||||
|
||||
## [1.0.0] - 2026-04-30
|
||||
|
||||
Initial public release.
|
||||
|
|
|
|||
2
pom.xml
2
pom.xml
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<groupId>ru.authlimbo</groupId>
|
||||
<artifactId>AuthLimbo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>AuthLimbo</name>
|
||||
|
|
|
|||
|
|
@ -20,29 +20,45 @@ import fr.xephi.authme.events.AuthMeAsyncPreLoginEvent;
|
|||
import fr.xephi.authme.events.LoginEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
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.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Listens for AuthMe's two relevant events:
|
||||
* Listens for AuthMe's two relevant events plus a damage guard:
|
||||
*
|
||||
* 1. {@link AuthMeAsyncPreLoginEvent} — fired before AuthMe authenticates
|
||||
* the password. We pin the destination chunk via a plugin chunk-ticket
|
||||
* so it's fully loaded by the time the actual teleport runs.
|
||||
*
|
||||
* 2. {@link LoginEvent} — fired AFTER AuthMe successfully authenticates
|
||||
* and runs its own (often broken) post-login teleport. We listen at
|
||||
* MONITOR priority so we are LAST in the chain, then fire an
|
||||
* authoritative teleport that overrides whatever AuthMe / Paper safety
|
||||
* checks did to the player's location.
|
||||
* 2. {@link LoginEvent} at {@link EventPriority#LOWEST} (F4) — fired BEFORE
|
||||
* AuthMe-ReReloaded's own internal post-login teleport. We immediately
|
||||
* sync-TP the player back to limbo spawn so AuthMe's broken teleport
|
||||
* operates against an irrelevant location. The MONITOR handler then
|
||||
* runs the authoritative restore.
|
||||
*
|
||||
* 3. {@link LoginEvent} at {@link EventPriority#MONITOR} — fires LAST in
|
||||
* the chain, schedules the authoritative teleport to the saved
|
||||
* quit-location after a configurable tick delay.
|
||||
*
|
||||
* 4. {@link EntityDamageEvent} at {@link EventPriority#HIGHEST} (F1) —
|
||||
* while a player is in {@code pendingTransit}, cancels {@code VOID}
|
||||
* damage and snaps them back to limbo spawn at full health. This is
|
||||
* the YOU500-incident fix: even if AuthMe's broken teleport drops the
|
||||
* player into an unloaded section, the void death is intercepted.
|
||||
*
|
||||
* Threading:
|
||||
* - AuthMeAsyncPreLoginEvent fires async (AuthMe worker thread).
|
||||
|
|
@ -62,12 +78,32 @@ import java.util.Set;
|
|||
*/
|
||||
public final class LoginListener implements Listener {
|
||||
|
||||
/** Hard cap on teleportAsync retries before falling back to spectator at limbo spawn (F2). */
|
||||
private static final int MAX_RETRIES = 3;
|
||||
|
||||
/** Tick delay between a failed teleport attempt and the next retry (F2). 1500 ms. */
|
||||
private static final long RETRY_DELAY_TICKS = 30L;
|
||||
|
||||
/** Safety timeout before pendingTransit entries expire even if no callback fires (F1). 5 s. */
|
||||
private static final long PENDING_TIMEOUT_TICKS = 20L * 5L;
|
||||
|
||||
private final AuthLimbo plugin;
|
||||
private final AuthMeDatabase db;
|
||||
|
||||
/** Tracks active plugin-chunk-tickets so we don't double-add or fail to release. */
|
||||
private final Set<String> activeTickets = new HashSet<>();
|
||||
|
||||
/**
|
||||
* F1 — UUIDs of players currently in the post-login restore window.
|
||||
* While present, the player is protected from VOID damage by
|
||||
* {@link #onEntityDamage}. Removed on successful TP, on final retry
|
||||
* give-up, or by the {@link #PENDING_TIMEOUT_TICKS} watchdog.
|
||||
*/
|
||||
private final Set<UUID> pendingTransit = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/** F2 — per-UUID retry counter for failed teleportAsync attempts. */
|
||||
private final Map<UUID, Integer> retryCounts = new ConcurrentHashMap<>();
|
||||
|
||||
public LoginListener(AuthLimbo plugin, AuthMeDatabase db) {
|
||||
this.plugin = plugin;
|
||||
this.db = db;
|
||||
|
|
@ -108,6 +144,50 @@ public final class LoginListener implements Listener {
|
|||
});
|
||||
}
|
||||
|
||||
/* ---------------- F4: pre-empt AuthMe's broken teleport ---------------- */
|
||||
|
||||
/**
|
||||
* Runs at LOWEST priority — BEFORE AuthMe-ReReloaded's own LoginEvent
|
||||
* handler runs its broken teleport. We immediately sync-TP the player
|
||||
* back to limbo spawn so AuthMe's subsequent teleport runs against an
|
||||
* irrelevant location. Combined with F1, this closes the void-death
|
||||
* window even when the saved chunk is far out and slow to load.
|
||||
*
|
||||
* Adds the player to {@code pendingTransit} here too — F1 must protect
|
||||
* the player across the entire LOWEST→MONITOR window.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onLoginPreEmpt(LoginEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player == null) return;
|
||||
final UUID id = player.getUniqueId();
|
||||
|
||||
// Mark in-transit BEFORE AuthMe's teleport runs so F1 catches any
|
||||
// void damage from that teleport.
|
||||
pendingTransit.add(id);
|
||||
scheduleTransitTimeout(id);
|
||||
|
||||
Location limbo = plugin.limbo().spawn();
|
||||
if (limbo == null) {
|
||||
plugin.getLogger().warning("[AuthLimbo] limbo spawn is null at LOWEST pre-empt for "
|
||||
+ player.getName() + " — cannot pre-empt AuthMe teleport.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Synchronous teleport so we land BEFORE AuthMe's own MONITOR/HIGH
|
||||
// handler fires its own teleport. teleportAsync would be racy here.
|
||||
try {
|
||||
player.teleport(limbo, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[AuthLimbo][debug] Pre-empted AuthMe TP for "
|
||||
+ player.getName() + " — pinned at limbo spawn.");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[AuthLimbo] pre-empt teleport failed for "
|
||||
+ player.getName() + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- Post-login: authoritative teleport ---------------- */
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
|
|
@ -115,31 +195,93 @@ public final class LoginListener implements Listener {
|
|||
Player player = event.getPlayer();
|
||||
if (player == null) return;
|
||||
final String name = player.getName();
|
||||
final UUID id = player.getUniqueId();
|
||||
|
||||
final Location saved = db.getQuitLocation(name);
|
||||
if (saved == null) {
|
||||
plugin.getLogger().info("[AuthLimbo] No saved location for "
|
||||
+ name + " — leaving where AuthMe put them.");
|
||||
// No restore needed — clear transit guard.
|
||||
pendingTransit.remove(id);
|
||||
retryCounts.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Defensive: pre-empt handler should have added this already, but ensure.
|
||||
pendingTransit.add(id);
|
||||
retryCounts.putIfAbsent(id, 0);
|
||||
|
||||
long delay = Math.max(0, plugin.getConfig().getLong("authme.teleport-delay-ticks", 10L));
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> doTeleport(player, name, saved), delay);
|
||||
}
|
||||
|
||||
/* ---------------- F1: void-damage guard during transit ---------------- */
|
||||
|
||||
/**
|
||||
* F1 — while a player is mid-restore, intercept VOID damage and snap
|
||||
* them back to limbo spawn at full health. This is the primary fix for
|
||||
* the YOU500 incident: even if AuthMe's own broken teleport drops the
|
||||
* player into an unloaded section that triggers "left the confines of
|
||||
* this world", we cancel the damage and recover the player.
|
||||
*
|
||||
* HIGHEST + ignoreCancelled=true so we run last among damage handlers
|
||||
* and don't double-process events already cancelled by other plugins.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onEntityDamage(EntityDamageEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
final UUID id = player.getUniqueId();
|
||||
if (!pendingTransit.contains(id)) return;
|
||||
if (event.getCause() != DamageCause.VOID) return;
|
||||
|
||||
event.setCancelled(true);
|
||||
|
||||
Location limbo = plugin.limbo().spawn();
|
||||
plugin.getLogger().warning(String.format(
|
||||
"[AuthLimbo] VOID damage intercepted for %s during post-login restore"
|
||||
+ " (intended TP target: %s) — relocating to limbo spawn at %s.",
|
||||
player.getName(),
|
||||
describeIntendedTarget(player),
|
||||
limbo == null ? "<null>" : describe(limbo)));
|
||||
|
||||
Bukkit.getConsoleSender().sendMessage(
|
||||
"[AuthLimbo] WARN: void-damage guard saved " + player.getName()
|
||||
+ " from inventory loss during AuthMe restore.");
|
||||
|
||||
// Heal first so the next-tick snap doesn't carry residual damage state.
|
||||
try {
|
||||
player.setHealth(20.0);
|
||||
player.setFireTicks(0);
|
||||
player.setFallDistance(0f);
|
||||
} catch (Throwable t) {
|
||||
// best-effort
|
||||
}
|
||||
|
||||
if (limbo != null) {
|
||||
try {
|
||||
player.teleport(limbo, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[AuthLimbo] limbo recovery teleport failed for "
|
||||
+ player.getName() + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- Core teleport with chunk-prep ---------------- */
|
||||
|
||||
private void doTeleport(Player player, String name, Location saved) {
|
||||
if (!player.isOnline()) {
|
||||
plugin.getLogger().info("[AuthLimbo] " + name
|
||||
+ " went offline before restore — skipping.");
|
||||
clearTransit(player.getUniqueId());
|
||||
return;
|
||||
}
|
||||
World world = saved.getWorld();
|
||||
if (world == null) {
|
||||
plugin.getLogger().warning("[AuthLimbo] Saved world for "
|
||||
+ name + " is no longer loaded.");
|
||||
clearTransit(player.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -169,28 +311,118 @@ public final class LoginListener implements Listener {
|
|||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[AuthLimbo][debug] Teleport ok for " + name);
|
||||
}
|
||||
// F1/F2: restore complete — clear transit guard.
|
||||
clearTransit(player.getUniqueId());
|
||||
} else {
|
||||
plugin.getLogger().warning("[AuthLimbo] teleportAsync returned false for "
|
||||
+ name + " — Paper may have rejected the location.");
|
||||
handleTeleportFailure(player, name, saved,
|
||||
"teleportAsync returned false");
|
||||
}
|
||||
// Release the ticket 5s later — gives the client time to
|
||||
// download the chunk before we let it unload.
|
||||
scheduleTicketRelease(world, cx, cz, key);
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
plugin.getLogger().warning("[AuthLimbo] teleportAsync threw for "
|
||||
+ name + ": " + ex.getMessage());
|
||||
handleTeleportFailure(player, name, saved,
|
||||
"teleportAsync threw: " + ex.getMessage());
|
||||
scheduleTicketRelease(world, cx, cz, key);
|
||||
return null;
|
||||
});
|
||||
}).exceptionally(ex -> {
|
||||
plugin.getLogger().warning("[AuthLimbo] getChunkAtAsyncUrgently threw for "
|
||||
+ name + ": " + ex.getMessage());
|
||||
handleTeleportFailure(player, name, saved,
|
||||
"getChunkAtAsyncUrgently threw: " + ex.getMessage());
|
||||
scheduleTicketRelease(world, cx, cz, key);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- F2: failure recovery + retry ---------------- */
|
||||
|
||||
/**
|
||||
* F2 — on any failed teleport (false future, exceptional future, or
|
||||
* chunk-load throw), do not abandon the player.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Synchronously TP back to limbo spawn so they aren't sitting in
|
||||
* an unloaded chunk that may void-kill them next tick.
|
||||
* 2. Increment retry counter. Up to {@link #MAX_RETRIES} retries
|
||||
* schedule another doTeleport after {@link #RETRY_DELAY_TICKS}.
|
||||
* 3. After MAX_RETRIES failures: leave player at limbo spawn in
|
||||
* spectator gamemode, log ERROR with full coords + retry count,
|
||||
* alert console for manual `/authlimbo tp` intervention.
|
||||
*
|
||||
* The player remains in {@code pendingTransit} across all retries so
|
||||
* F1 still protects them from any void damage during the retry window.
|
||||
*/
|
||||
private void handleTeleportFailure(Player player, String name, Location saved, String reason) {
|
||||
final UUID id = player.getUniqueId();
|
||||
final int attempt = retryCounts.merge(id, 1, Integer::sum);
|
||||
|
||||
plugin.getLogger().warning(String.format(
|
||||
"[AuthLimbo] Restore attempt %d/%d failed for %s — %s. Recovering to limbo spawn.",
|
||||
attempt, MAX_RETRIES, name, reason));
|
||||
|
||||
// Step 1: snap back to limbo spawn synchronously so they don't
|
||||
// continue free-falling in an unloaded section.
|
||||
Location limbo = plugin.limbo().spawn();
|
||||
if (limbo != null && player.isOnline()) {
|
||||
// Run on main thread — teleportAsync future callbacks may not be there.
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
try {
|
||||
player.teleport(limbo, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
player.setHealth(20.0);
|
||||
player.setFireTicks(0);
|
||||
player.setFallDistance(0f);
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[AuthLimbo] sync recovery TP failed for "
|
||||
+ name + ": " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (attempt >= MAX_RETRIES) {
|
||||
// Step 3: give up gracefully.
|
||||
plugin.getLogger().severe(String.format(
|
||||
"[AuthLimbo] Failed to restore %s after %d retries — manual intervention needed."
|
||||
+ " Saved coords: %s(%.1f, %.1f, %.1f). Last reason: %s.",
|
||||
name, MAX_RETRIES,
|
||||
saved.getWorld() == null ? "<null>" : saved.getWorld().getName(),
|
||||
saved.getX(), saved.getY(), saved.getZ(), reason));
|
||||
|
||||
Bukkit.getConsoleSender().sendMessage(
|
||||
"[AuthLimbo] ERROR: failed to restore " + name + " after "
|
||||
+ MAX_RETRIES + " retries — manual intervention needed."
|
||||
+ " Run `/authlimbo tp " + name + "` after investigating.");
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
try {
|
||||
player.setGameMode(GameMode.SPECTATOR);
|
||||
player.sendMessage("§c[AuthLimbo] Could not restore your saved location."
|
||||
+ " Staff have been notified — please ping an admin for"
|
||||
+ " a manual /authlimbo tp.");
|
||||
} catch (Throwable t) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clearTransit(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: schedule a retry. Player stays in pendingTransit so F1
|
||||
// continues to protect them.
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (!player.isOnline()) {
|
||||
clearTransit(id);
|
||||
return;
|
||||
}
|
||||
doTeleport(player, name, saved);
|
||||
}, RETRY_DELAY_TICKS);
|
||||
}
|
||||
|
||||
/* ---------------- Helpers ---------------- */
|
||||
|
||||
private void scheduleTicketRelease(World world, int cx, int cz, String key) {
|
||||
if (!activeTickets.contains(key)) return;
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
|
|
@ -206,4 +438,39 @@ public final class LoginListener implements Listener {
|
|||
}
|
||||
}, 20L * 5L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a player from the transit set + retry counter. After this,
|
||||
* F1's void-damage guard no longer protects them.
|
||||
*/
|
||||
private void clearTransit(UUID id) {
|
||||
pendingTransit.remove(id);
|
||||
retryCounts.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watchdog: if no callback fires within {@link #PENDING_TIMEOUT_TICKS},
|
||||
* remove the UUID from pendingTransit so we don't leak entries.
|
||||
*/
|
||||
private void scheduleTransitTimeout(UUID id) {
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (pendingTransit.remove(id)) {
|
||||
retryCounts.remove(id);
|
||||
if (plugin.debug()) {
|
||||
plugin.getLogger().info("[AuthLimbo][debug] pendingTransit timeout for " + id);
|
||||
}
|
||||
}
|
||||
}, PENDING_TIMEOUT_TICKS);
|
||||
}
|
||||
|
||||
private static String describeIntendedTarget(Player player) {
|
||||
Location loc = player.getLocation();
|
||||
return describe(loc);
|
||||
}
|
||||
|
||||
private static String describe(Location loc) {
|
||||
if (loc == null || loc.getWorld() == null) return "<unknown>";
|
||||
return String.format("%s(%.1f, %.1f, %.1f)",
|
||||
loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue