Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public class VanishConfig extends OkaeriConfig implements VanishSettings {
@Comment("Give night vision effect to vanished players")
public boolean nightVision = true;

@Comment("Allow vanished players to fly while vanish is enabled")
public boolean allowFlight = true;

@Comment("Should vanished players be able to silently view other players' inventories?")
public boolean silentInventoryAccess = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public interface VanishSettings {

boolean nightVision();

boolean allowFlight();

boolean silentInventoryAccess();

boolean glowEffect();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.eternalcode.core.feature.vanish.controller;

import com.eternalcode.core.feature.vanish.VanishService;
import com.eternalcode.core.feature.vanish.VanishSettings;
import com.eternalcode.core.feature.vanish.event.DisableVanishEvent;
import com.eternalcode.core.feature.vanish.event.EnableVanishEvent;
import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Controller;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

@Controller
class VanishFlightController implements Listener {

private final Map<UUID, FlightState> previousFlightStates = new HashMap<>();

private final VanishSettings settings;
private final Server server;

@Inject
VanishFlightController(VanishSettings settings, Server server) {
this.settings = settings;
this.server = server;
}

@EventHandler(ignoreCancelled = true)
void onEnable(EnableVanishEvent event) {
if (!this.settings.allowFlight()) {
return;
}

Player player = event.getPlayer();
UUID playerId = player.getUniqueId();

this.previousFlightStates.putIfAbsent(
playerId,
new FlightState(player.getAllowFlight(), player.isFlying())
);

player.setAllowFlight(true);
player.setFlying(true);
}

@EventHandler(ignoreCancelled = true)
void onDisable(DisableVanishEvent event) {
this.restoreFlightState(event.getPlayer());
}

void synchronize(VanishService vanishService) {
Iterator<Map.Entry<UUID, FlightState>> iterator = this.previousFlightStates.entrySet().iterator();

while (iterator.hasNext()) {
Map.Entry<UUID, FlightState> entry = iterator.next();
UUID playerId = entry.getKey();
Player player = this.server.getPlayer(playerId);

if (player == null || !player.isOnline()) {
iterator.remove();
continue;
}

if (!this.settings.allowFlight() || !vanishService.isVanished(playerId)) {
entry.getValue().apply(player);
iterator.remove();
continue;
}

if (!player.getAllowFlight()) {
player.setAllowFlight(true);
}
}
}

private void restoreFlightState(Player player) {
FlightState previousFlightState = this.previousFlightStates.remove(player.getUniqueId());
if (previousFlightState == null) {
return;
}

previousFlightState.apply(player);
}

private record FlightState(boolean allowFlight, boolean flying) {

private void apply(Player player) {
if (this.allowFlight) {
player.setAllowFlight(true);
player.setFlying(this.flying);
return;
}

player.setFlying(false);
player.setAllowFlight(false);
Comment on lines +98 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve current creative flight state on unvanish

If a player enters vanish from survival and is later switched to creative/spectator while still vanished, this restores the pre-vanish allowFlight=false snapshot when vanish ends, leaving them in the new game mode with flight disabled. Since gamemode changes are not rolled back by this controller, the stale flight snapshot should not blindly clear flight when the current game mode or another feature has legitimately granted it during vanish.

Useful? React with 👍 / 👎.

}
}
}
Comment on lines +1 to +102

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a critical bug/exploit in the current implementation: when a vanished player disconnects, they are removed from previousFlightStates in the watchdog task without their original flight state being restored. Since Bukkit saves player data on disconnect, their player data will be saved with allowFlight = true and flying = true, granting them permanent flight even after they reconnect and are no longer vanished.

Additionally, if a player joins the server already vanished (e.g., silent join or persistent vanish), they won't be added to previousFlightStates, meaning they won't be allowed to fly until they toggle vanish.

To fix these issues, we should:

  1. Listen to PlayerQuitEvent to immediately restore the player's flight state before they disconnect.
  2. Listen to PlayerJoinEvent to enable flight if they join vanished.
  3. Inject VanishService directly into the controller to simplify the synchronization logic and support these event handlers.
  4. Avoid stripping flight from players in creative or spectator mode when unvanishing.
package com.eternalcode.core.feature.vanish.controller;

import com.eternalcode.core.feature.vanish.VanishService;
import com.eternalcode.core.feature.vanish.VanishSettings;
import com.eternalcode.core.feature.vanish.event.DisableVanishEvent;
import com.eternalcode.core.feature.vanish.event.EnableVanishEvent;
import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Controller;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import org.bukkit.GameMode;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;

@Controller
class VanishFlightController implements Listener {

    private final Map<UUID, FlightState> previousFlightStates = new HashMap<>();

    private final VanishSettings settings;
    private final Server server;
    private final VanishService vanishService;

    @Inject
    VanishFlightController(VanishSettings settings, Server server, VanishService vanishService) {
        this.settings = settings;
        this.server = server;
        this.vanishService = vanishService;
    }

    @EventHandler(ignoreCancelled = true)
    void onEnable(EnableVanishEvent event) {
        if (!this.settings.allowFlight()) {
            return;
        }

        Player player = event.getPlayer();
        this.enableFlight(player);
    }

    @EventHandler(ignoreCancelled = true)
    void onDisable(DisableVanishEvent event) {
        this.restoreFlightState(event.getPlayer());
    }

    @EventHandler
    void onQuit(PlayerQuitEvent event) {
        this.restoreFlightState(event.getPlayer());
    }

    @EventHandler
    void onJoin(PlayerJoinEvent event) {
        if (!this.settings.allowFlight()) {
            return;
        }

        Player player = event.getPlayer();
        if (this.vanishService.isVanished(player)) {
            this.enableFlight(player);
        }
    }

    void synchronize() {
        for (UUID playerId : this.vanishService.getVanishedPlayers()) {
            Player player = this.server.getPlayer(playerId);
            if (player == null || !player.isOnline()) {
                continue;
            }

            if (this.settings.allowFlight()) {
                this.previousFlightStates.putIfAbsent(
                    playerId,
                    new FlightState(player.getAllowFlight(), player.isFlying())
                );

                if (!player.getAllowFlight()) {
                    player.setAllowFlight(true);
                }
            }
        }

        Iterator<Map.Entry<UUID, FlightState>> iterator = this.previousFlightStates.entrySet().iterator();

        while (iterator.hasNext()) {
            Map.Entry<UUID, FlightState> entry = iterator.next();
            UUID playerId = entry.getKey();
            Player player = this.server.getPlayer(playerId);

            if (player == null || !player.isOnline()) {
                iterator.remove();
                continue;
            }

            if (!this.settings.allowFlight() || !this.vanishService.isVanished(playerId)) {
                entry.getValue().apply(player);
                iterator.remove();
            }
        }
    }

    private void enableFlight(Player player) {
        this.previousFlightStates.putIfAbsent(
            player.getUniqueId(),
            new FlightState(player.getAllowFlight(), player.isFlying())
        );

        player.setAllowFlight(true);
        player.setFlying(true);
    }

    private void restoreFlightState(Player player) {
        FlightState previousFlightState = this.previousFlightStates.remove(player.getUniqueId());
        if (previousFlightState == null) {
            return;
        }

        previousFlightState.apply(player);
    }

    private record FlightState(boolean allowFlight, boolean flying) {

        private void apply(Player player) {
            if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) {
                return;
            }

            if (this.allowFlight) {
                player.setAllowFlight(true);
                player.setFlying(this.flying);
                return;
            }

            player.setFlying(false);
            player.setAllowFlight(false);
        }
    }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.eternalcode.core.feature.vanish.controller;

import com.eternalcode.core.feature.vanish.VanishService;
import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Task;
import java.util.concurrent.TimeUnit;

@Task(period = 5, delay = 5, unit = TimeUnit.SECONDS)
class VanishFlightWatchdogTask implements Runnable {

private final VanishService vanishService;
private final VanishFlightController vanishFlightController;

@Inject
VanishFlightWatchdogTask(VanishService vanishService, VanishFlightController vanishFlightController) {
this.vanishService = vanishService;
this.vanishFlightController = vanishFlightController;
}

@Override
public void run() {
this.vanishFlightController.synchronize(this.vanishService);
}
}
Comment on lines +1 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since VanishService is now injected directly into VanishFlightController, we can simplify this task by removing the redundant VanishService dependency and calling the parameterless synchronize() method.

package com.eternalcode.core.feature.vanish.controller;

import com.eternalcode.core.injector.annotations.Inject;
import com.eternalcode.core.injector.annotations.component.Task;
import java.util.concurrent.TimeUnit;

@Task(period = 5, delay = 5, unit = TimeUnit.SECONDS)
class VanishFlightWatchdogTask implements Runnable {

    private final VanishFlightController vanishFlightController;

    @Inject
    VanishFlightWatchdogTask(VanishFlightController vanishFlightController) {
        this.vanishFlightController = vanishFlightController;
    }

    @Override
    public void run() {
        this.vanishFlightController.synchronize();
    }
}

Loading