Replace WGamemode-OG
This commit is contained in:
1 parent
9dc8b9fa5e
commit
9ee50d6cf1
11 files changed
+1159
-122
No files matched your search
@@ -1,58 +1,64 @@
|
||||
# Spawn-OG
|
||||
|
||||
**Spawn-OG** is a Minecraft server plugin that allows server administrators to set world spawn points and manage player teleportation to those spawns. It provides functionality similar to EssentialsXSpawn – such as setting and teleporting to spawn points, and first-join spawn control, with additional configuration options from TrueOG Network. Spawn-OG works in conjunction with the [Essentials-OG](https://github.com/true-og/Essentials-OG) fork of EssentialsX. It handles the spawn-related features originally handled by EssentialsSpawn. This is one small step towards a post-Essentials TrueOG Network.
|
||||
Spawn-OG owns safe player entry, spawn and respawn behavior, and TrueOG's
|
||||
WorldGuard-based regional flight policy. It replaces EssentialsXSpawn and the
|
||||
flight and gamemode management portions of legacy WGamemode-OG on Purpur 1.19.4.
|
||||
|
||||
## Features
|
||||
## Login safety
|
||||
|
||||
- **Set Spawn Points:**
|
||||
```/setspawn```
|
||||
- **Teleport to Spawn:**
|
||||
```/spawn```
|
||||
- **Teleport Others to Spawn:**
|
||||
```/spawn <player```
|
||||
- **Async teleportation:**
|
||||
Minimal impact on server performance
|
||||
- **Teleport delay:**
|
||||
5 seconds
|
||||
- **First Join Spawn (Newbie Spawn):**
|
||||
- **Respawn Handling:**
|
||||
By default, if `respawn-at-home` is set to false, players will respawn at the spawn point. If `respawn-at-home` is true, players will respawn at their bed or home location (if defined) instead of spawn. The plugin respects beds and Essentials homes according to this setting.
|
||||
- **Spawn-on-Join:**
|
||||
Optionally, the plugin can force players to go to spawn every time they log in, by enabling `spawn-on-join` in the config.
|
||||
1. Preserve the player's current invulnerability state and make them
|
||||
invulnerable while repair is in progress.
|
||||
2. Load and teleport to the configured global spawn when relocation is needed.
|
||||
3. Change non-staff spectator, creative, or adventure players to survival.
|
||||
4. Successfully rescued autopsy players are told their original world and block coordinates in chat so they can identify the location they were browsing.
|
||||
|
||||
By default, login normalization is limited to `world`, `world_nether`, and
|
||||
`world_the_end`. Add other survival worlds explicitly rather than applying the
|
||||
rule to minigame or build worlds accidentally.
|
||||
|
||||
## Regional flight
|
||||
|
||||
Inside a `fly` region, players with `spawnog.flight` can toggle flight using
|
||||
`/fly`. Inside a `nofly` region, flight is suspended unless the player has
|
||||
`spawnog.flight.bypass`. Previous flight permission is restored on region exit
|
||||
or disconnect, and fall damage caused by forced landing is cancelled once.
|
||||
|
||||
## Regional item drops
|
||||
|
||||
```text
|
||||
/rg flag -w world spawn item-drop deny
|
||||
```
|
||||
|
||||
Repeat the above command with each additional world and region that should deny
|
||||
drops. Remove the explicit policy with `item-drop` and no value, or restore
|
||||
drops with `item-drop allow`. Staff bypass, when genuinely required, is the
|
||||
LuckPerms permission `worldguard.region.bypass.<world>`.
|
||||
|
||||
This replaces WGamemode-OG's `stopItemDrop` behavior with an explicit region
|
||||
policy that applies consistently whether or not the player has toggled flight.
|
||||
|
||||
## Commands
|
||||
|
||||
- **`/spawn`**
|
||||
Teleport yourself to the defined spawn location. If used by a player, it sends that player to the spawn (global or their group’s spawn):contentReference[oaicite:21]{index=21}. If used from console or by an admin with a target, see below.
|
||||
- **`/spawn <player>`**
|
||||
Teleport the specified player to spawn. The target player will be sent to the spawn point, provided the command executor has permission to send others. The target will receive a message (e.g. “<*Player*> teleported you to spawn.”).
|
||||
- **`/setspawn`**
|
||||
Set the spawn point for the current world/group to your current location. With no arguments, this sets the “default” global spawn point:contentReference[oaicite:22]{index=22}. This is typically the main spawn for your server (and is used for all players unless group-specific spawns are set).
|
||||
- **`/setspawn <group>`**
|
||||
Set a spawn point for a specific group name. For example, `/setspawn newbie` will set the spawn location used for new players (if your config’s `newbies.spawnpoint` is “newbie”), or `/setspawn builders` could set a special spawn for the “builders” permission group.
|
||||
- `/spawn` — Teleport to the global or LuckPerms-group spawn after a five-second
|
||||
warmup.
|
||||
- `/spawn <player>` — Teleport another player to their resolved spawn.
|
||||
- `/setspawn [group] [normalize-view] [normalize-position]` — Configure a spawn.
|
||||
- `/fly` — Toggle flight inside a configured `fly` region.
|
||||
|
||||
## Permissions
|
||||
|
||||
- **`essentials.spawn`** – Allows use of the `/spawn` command to teleport oneself to spawn. *Default:* true.
|
||||
- **`essentials.spawn.others`** – Allows sending *other players* to spawn with `/spawn <player>`. *Default:* false.
|
||||
- **`essentials.setspawn`** – Allows use of `/setspawn` to define spawn points. *Default:* false.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
[Essentials-OG](https://github.com/true-og/Essentials-OG)
|
||||
- `spawnog.spawn` — Use `/spawn`.
|
||||
- `spawnog.spawn.others` — Use `/spawn <player>`.
|
||||
- `spawnog.setspawn` — Configure spawns.
|
||||
- `spawnog.login-migration.bypass` — Explicitly exempt staff from login
|
||||
migration and normalization.
|
||||
- `spawnog.flight` — Use regional `/fly`; granted by default.
|
||||
- `spawnog.flight.bypass` — Ignore `nofly` regions; not granted by default.
|
||||
|
||||
## Building
|
||||
|
||||
Use the TrueOG Bootstrap to build automatically, or run:
|
||||
Use the TrueOG bootstrap, or run:
|
||||
|
||||
```./gradlew clean build eclipse```
|
||||
|
||||
## TODO:
|
||||
|
||||
- Cancel teleport if a player is in combat
|
||||
|
||||
- Cancel teleport if a player moves
|
||||
|
||||
## Licese
|
||||
|
||||
[MIT](https://raw.githubusercontent.com/true-og/Spawn-OG/refs/heads/main/LICENSE)
|
||||
```sh
|
||||
./gradlew clean build eclipse --warning-mode all
|
||||
```
|
||||
+4
-1
@@ -21,7 +21,7 @@ java {
|
||||
/* ----------------------------- Metadata ------------------------------ */
|
||||
group = "net.trueog.spawn-og" // Declare bundle identifier.
|
||||
|
||||
version = "1.0" // Declare plugin version (will be in .jar).
|
||||
version = "1.1" // Declare plugin version (will be in .jar).
|
||||
|
||||
val apiVersion = "1.19" // Declare minecraft server target version.
|
||||
|
||||
@@ -39,12 +39,15 @@ repositories {
|
||||
gradlePluginPortal() // Import the Gradle Plugin Portal Maven Repository.
|
||||
maven { url = uri("https://repo.purpurmc.org/snapshots") } // Import the PurpurMC Maven Repository.
|
||||
maven { url = uri("https://repo.codemc.io/repository/maven-public/") } // Import the CodeMC Maven Repository.
|
||||
maven { url = uri("https://maven.enginehub.org/repo/") } // Import the EngineHub Maven Repository.
|
||||
}
|
||||
|
||||
/* ---------------------- Java project deps ---------------------------- */
|
||||
dependencies {
|
||||
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged.
|
||||
compileOnly("net.luckperms:api:5.4") // Import LuckPerms API.
|
||||
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.0-SNAPSHOT")
|
||||
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.8") { exclude(group = "com.sk89q.worldedit") }
|
||||
}
|
||||
|
||||
/* ---------------------- Reproducible jars ---------------------------- */
|
||||
|
||||
@@ -7,6 +7,10 @@ import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import spawnog.commands.SetSpawnCommand;
|
||||
import spawnog.commands.SpawnCommand;
|
||||
import spawnog.flight.RegionFlightService;
|
||||
import spawnog.flight.ToggleRegionFlightCommand;
|
||||
import spawnog.login.AutopsyMigrationStore;
|
||||
import spawnog.login.LoginMigrationService;
|
||||
import spawnog.listener.SpawnListener;
|
||||
|
||||
@Slf4j
|
||||
@@ -18,6 +22,9 @@ public final class SpawnOG extends JavaPlugin {
|
||||
@Getter
|
||||
private LuckPerms luckPerms;
|
||||
|
||||
private LoginMigrationService loginMigrationService;
|
||||
private RegionFlightService regionFlightService;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
|
||||
@@ -28,10 +35,37 @@ public final class SpawnOG extends JavaPlugin {
|
||||
if (rsp != null)
|
||||
luckPerms = rsp.getProvider();
|
||||
|
||||
register("spawn", new SpawnCommand(), "essentials.spawn");
|
||||
register("setspawn", new SetSpawnCommand(), "essentials.setspawn");
|
||||
register("spawn", new SpawnCommand(), "spawnog.spawn");
|
||||
register("setspawn", new SetSpawnCommand(), "spawnog.setspawn");
|
||||
|
||||
getServer().getPluginManager().registerEvents(new SpawnListener(), this);
|
||||
AutopsyMigrationStore migrationStore = new AutopsyMigrationStore(this);
|
||||
loginMigrationService = new LoginMigrationService(this, migrationStore);
|
||||
getServer().getPluginManager().registerEvents(new SpawnListener(this, loginMigrationService), this);
|
||||
|
||||
if (getConfig().getBoolean("flight.enabled", true)
|
||||
&& getServer().getPluginManager().isPluginEnabled("WorldGuard")
|
||||
&& getServer().getPluginManager().isPluginEnabled("WorldEdit"))
|
||||
{
|
||||
|
||||
regionFlightService = new RegionFlightService(this, loginMigrationService);
|
||||
getServer().getPluginManager().registerEvents(regionFlightService, this);
|
||||
getCommand("fly").setExecutor(new ToggleRegionFlightCommand(regionFlightService));
|
||||
|
||||
} else if (getConfig().getBoolean("flight.enabled", true)) {
|
||||
|
||||
getLogger().warning("WorldGuard or WorldEdit is unavailable; regional flight management is disabled.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
if (loginMigrationService != null)
|
||||
loginMigrationService.close();
|
||||
if (regionFlightService != null)
|
||||
regionFlightService.close();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public class SpawnCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("essentials.spawn.others")) {
|
||||
if (!sender.hasPermission("spawnog.spawn.others")) {
|
||||
|
||||
sender.sendMessage(cfg.getString("locale.missingPermission", "You are lacking the required permissions."));
|
||||
return true;
|
||||
@@ -84,7 +84,7 @@ public class SpawnCommand implements CommandExecutor, TabCompleter {
|
||||
@NotNull String lbl, @NotNull String[] args)
|
||||
{
|
||||
|
||||
if (args.length == 1 && sender.hasPermission("essentials.spawn.others")) {
|
||||
if (args.length == 1 && sender.hasPermission("spawnog.spawn.others")) {
|
||||
|
||||
return Bukkit.getOnlinePlayers().stream().map(Player::getName)
|
||||
.filter(n -> n.toLowerCase().startsWith(args[0].toLowerCase())).collect(Collectors.toList());
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
package spawnog.flight;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter;
|
||||
import com.sk89q.worldedit.util.Location;
|
||||
import com.sk89q.worldguard.WorldGuard;
|
||||
import com.sk89q.worldguard.protection.ApplicableRegionSet;
|
||||
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
|
||||
import com.sk89q.worldguard.protection.regions.RegionContainer;
|
||||
import com.sk89q.worldguard.protection.regions.RegionQuery;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerKickEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import spawnog.SpawnOG;
|
||||
import spawnog.login.LoginMigrationService;
|
||||
|
||||
public final class RegionFlightService implements Listener {
|
||||
|
||||
public static final String FLIGHT_PERMISSION = "spawnog.flight";
|
||||
public static final String LEGACY_FLIGHT_PERMISSION = "wgamemode.fly";
|
||||
public static final String BYPASS_PERMISSION = "spawnog.flight.bypass";
|
||||
public static final String LEGACY_BYPASS_PERMISSION = "wgamemode.fly.bypass";
|
||||
|
||||
private static final long PARTICLE_INITIAL_DELAY_TICKS = 10L;
|
||||
private static final long PARTICLE_PERIOD_TICKS = 6L;
|
||||
private static final double PARTICLE_RADIUS = 0.12D;
|
||||
private static final double PARTICLE_Y_OFFSET = -0.12D;
|
||||
private static final double PARTICLE_BACK_OFFSET = 0.45D;
|
||||
private static final Color[] RAINBOW_TRAIL_COLORS = { Color.fromRGB(255, 82, 82), Color.fromRGB(255, 171, 64),
|
||||
Color.fromRGB(255, 238, 88), Color.fromRGB(105, 240, 174), Color.fromRGB(64, 196, 255),
|
||||
Color.fromRGB(124, 77, 255), Color.fromRGB(255, 64, 129) };
|
||||
|
||||
private final SpawnOG plugin;
|
||||
private final LoginMigrationService loginMigrationService;
|
||||
private final Map<String, RuleKind> regionRules = new HashMap<>();
|
||||
private final Map<UUID, FlightOverride> flightOverrides = new HashMap<>();
|
||||
private final Set<UUID> fallImmunity = new HashSet<>();
|
||||
private final Set<UUID> scheduledRefreshes = new HashSet<>();
|
||||
private int particleStep;
|
||||
|
||||
public RegionFlightService(SpawnOG plugin, LoginMigrationService loginMigrationService) {
|
||||
|
||||
this.plugin = plugin;
|
||||
this.loginMigrationService = loginMigrationService;
|
||||
loadRules();
|
||||
plugin.getServer().getScheduler().runTaskTimer(plugin, this::spawnFlightParticles, PARTICLE_INITIAL_DELAY_TICKS,
|
||||
PARTICLE_PERIOD_TICKS);
|
||||
|
||||
}
|
||||
|
||||
public boolean canToggleFlight(Player player) {
|
||||
|
||||
if (player == null
|
||||
|| !(player.hasPermission(FLIGHT_PERMISSION) || player.hasPermission(LEGACY_FLIGHT_PERMISSION)))
|
||||
return false;
|
||||
RegionRule rule = currentRule(player);
|
||||
return rule != null && rule.kind() == RuleKind.FLY;
|
||||
|
||||
}
|
||||
|
||||
public boolean toggleFlight(Player player) {
|
||||
|
||||
RegionRule rule = currentRule(player);
|
||||
if (rule == null || rule.kind() != RuleKind.FLY)
|
||||
return player.getAllowFlight();
|
||||
|
||||
UUID playerId = player.getUniqueId();
|
||||
FlightOverride previous = flightOverrides.get(playerId);
|
||||
boolean originalAllowFlight;
|
||||
if (previous == null || previous.kind() != RuleKind.FLY) {
|
||||
|
||||
restoreOverride(player, previous);
|
||||
originalAllowFlight = player.getAllowFlight();
|
||||
|
||||
} else {
|
||||
|
||||
originalAllowFlight = previous.originalAllowFlight();
|
||||
|
||||
}
|
||||
|
||||
boolean enable = !player.getAllowFlight();
|
||||
if (enable) {
|
||||
|
||||
player.setAllowFlight(true);
|
||||
|
||||
} else {
|
||||
|
||||
stopFlyingSafely(player);
|
||||
player.setAllowFlight(false);
|
||||
|
||||
}
|
||||
|
||||
if (player.getAllowFlight() == originalAllowFlight) {
|
||||
|
||||
flightOverrides.remove(playerId);
|
||||
|
||||
} else {
|
||||
|
||||
flightOverrides.put(playerId,
|
||||
new FlightOverride(originalAllowFlight, player.getAllowFlight(), rule.regionId(), RuleKind.FLY));
|
||||
|
||||
}
|
||||
|
||||
return player.getAllowFlight();
|
||||
|
||||
}
|
||||
|
||||
public void refresh(Player player) {
|
||||
|
||||
if (player == null || !player.isOnline())
|
||||
return;
|
||||
if (loginMigrationService.isPending(player)) {
|
||||
|
||||
scheduleRefresh(player);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
UUID playerId = player.getUniqueId();
|
||||
RegionRule rule = currentRule(player);
|
||||
FlightOverride previous = flightOverrides.get(playerId);
|
||||
|
||||
if (rule == null) {
|
||||
|
||||
restoreOverride(player, previous);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (rule.kind() == RuleKind.FLY) {
|
||||
|
||||
if (previous != null && previous.kind() == RuleKind.NOFLY)
|
||||
restoreOverride(player, previous);
|
||||
previous = flightOverrides.get(playerId);
|
||||
if (previous != null && previous.kind() == RuleKind.FLY) {
|
||||
|
||||
if (player.getAllowFlight() != previous.appliedAllowFlight()) {
|
||||
|
||||
if (!previous.appliedAllowFlight())
|
||||
stopFlyingSafely(player);
|
||||
player.setAllowFlight(previous.appliedAllowFlight());
|
||||
|
||||
}
|
||||
|
||||
if (!previous.regionId().equals(rule.regionId()))
|
||||
flightOverrides.put(playerId, new FlightOverride(previous.originalAllowFlight(),
|
||||
previous.appliedAllowFlight(), rule.regionId(), RuleKind.FLY));
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (hasBypass(player)) {
|
||||
|
||||
restoreOverride(player, previous);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (previous != null && previous.kind() == RuleKind.FLY) {
|
||||
|
||||
restoreOverride(player, previous);
|
||||
previous = null;
|
||||
|
||||
}
|
||||
|
||||
if (previous == null) {
|
||||
|
||||
previous = new FlightOverride(player.getAllowFlight(), false, rule.regionId(), RuleKind.NOFLY);
|
||||
flightOverrides.put(playerId, previous);
|
||||
|
||||
} else if (previous != null && previous.kind() == RuleKind.NOFLY
|
||||
&& !previous.regionId().equals(rule.regionId()))
|
||||
{
|
||||
|
||||
flightOverrides.put(playerId,
|
||||
new FlightOverride(previous.originalAllowFlight(), false, rule.regionId(), RuleKind.NOFLY));
|
||||
|
||||
}
|
||||
|
||||
if (player.getAllowFlight()) {
|
||||
|
||||
stopFlyingSafely(player);
|
||||
player.setAllowFlight(false);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void restore(Player player) {
|
||||
|
||||
if (player == null)
|
||||
return;
|
||||
restoreOverride(player, flightOverrides.get(player.getUniqueId()));
|
||||
fallImmunity.remove(player.getUniqueId());
|
||||
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
Set.copyOf(flightOverrides.keySet()).forEach(playerId -> {
|
||||
|
||||
Player player = plugin.getServer().getPlayer(playerId);
|
||||
if (player != null)
|
||||
restoreOverride(player, flightOverrides.get(playerId));
|
||||
|
||||
});
|
||||
flightOverrides.clear();
|
||||
fallImmunity.clear();
|
||||
scheduledRefreshes.clear();
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
|
||||
scheduleRefresh(event.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onTeleport(PlayerTeleportEvent event) {
|
||||
|
||||
scheduleRefresh(event.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onChangedWorld(PlayerChangedWorldEvent event) {
|
||||
|
||||
// Essentials-OG resets flight during this same event. Reconcile one tick
|
||||
// later so Spawn-OG remains the final authority for regional flight.
|
||||
scheduleRefresh(event.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
|
||||
scheduleRefresh(event.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
|
||||
if (event.getTo() == null)
|
||||
return;
|
||||
|
||||
Player player = event.getPlayer();
|
||||
boolean sameBlock = event.getFrom().getWorld() == event.getTo().getWorld()
|
||||
&& event.getFrom().getBlockX() == event.getTo().getBlockX()
|
||||
&& event.getFrom().getBlockY() == event.getTo().getBlockY()
|
||||
&& event.getFrom().getBlockZ() == event.getTo().getBlockZ();
|
||||
if (!sameBlock)
|
||||
refresh(player);
|
||||
|
||||
if (fallImmunity.contains(player.getUniqueId()) && isOnSafeSurface(player) && !player.isFlying())
|
||||
fallImmunity.remove(player.getUniqueId());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
|
||||
restore(event.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onKick(PlayerKickEvent event) {
|
||||
|
||||
restore(event.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDamage(EntityDamageEvent event) {
|
||||
|
||||
if (event.getCause() != EntityDamageEvent.DamageCause.FALL || !(event.getEntity() instanceof Player player))
|
||||
return;
|
||||
if (fallImmunity.remove(player.getUniqueId()))
|
||||
event.setCancelled(true);
|
||||
|
||||
}
|
||||
|
||||
private void loadRules() {
|
||||
|
||||
if (plugin.getConfig().getConfigurationSection("flight.regions") == null) {
|
||||
|
||||
regionRules.put("spawn", RuleKind.FLY);
|
||||
regionRules.put("warzone", RuleKind.NOFLY);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
plugin.getConfig().getConfigurationSection("flight.regions").getValues(false).forEach((regionId, value) -> {
|
||||
|
||||
RuleKind kind;
|
||||
try {
|
||||
|
||||
kind = RuleKind.valueOf(String.valueOf(value).toUpperCase(Locale.ROOT));
|
||||
|
||||
} catch (IllegalArgumentException error) {
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid flight rule for region '" + regionId + "'. Expected fly or nofly.", error);
|
||||
|
||||
}
|
||||
|
||||
regionRules.put(regionId.toLowerCase(Locale.ROOT), kind);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private RegionRule currentRule(Player player) {
|
||||
|
||||
RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer();
|
||||
RegionQuery query = container.createQuery();
|
||||
Location location = BukkitAdapter.adapt(player.getLocation());
|
||||
ApplicableRegionSet applicable = query.getApplicableRegions(location);
|
||||
RegionRule best = null;
|
||||
|
||||
for (ProtectedRegion region : applicable.getRegions()) {
|
||||
|
||||
String regionId = region.getId().toLowerCase(Locale.ROOT);
|
||||
RuleKind kind = regionRules.get(regionId);
|
||||
if (kind == null)
|
||||
continue;
|
||||
|
||||
RegionRule candidate = new RegionRule(regionId, kind, region.getPriority());
|
||||
if (best == null || candidate.priority() > best.priority()
|
||||
|| candidate.priority() == best.priority() && candidate.regionId().compareTo(best.regionId()) < 0)
|
||||
best = candidate;
|
||||
|
||||
}
|
||||
|
||||
return best;
|
||||
|
||||
}
|
||||
|
||||
private boolean hasBypass(Player player) {
|
||||
|
||||
return player.hasPermission(BYPASS_PERMISSION) || player.hasPermission(LEGACY_BYPASS_PERMISSION);
|
||||
|
||||
}
|
||||
|
||||
private void restoreOverride(Player player, FlightOverride state) {
|
||||
|
||||
if (state == null)
|
||||
return;
|
||||
if (player.isFlying() && !state.originalAllowFlight())
|
||||
stopFlyingSafely(player);
|
||||
if (player.getAllowFlight() != state.originalAllowFlight())
|
||||
player.setAllowFlight(state.originalAllowFlight());
|
||||
flightOverrides.remove(player.getUniqueId());
|
||||
|
||||
}
|
||||
|
||||
private void stopFlyingSafely(Player player) {
|
||||
|
||||
if (player.isFlying()) {
|
||||
|
||||
fallImmunity.add(player.getUniqueId());
|
||||
player.setFlying(false);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isOnSafeSurface(Player player) {
|
||||
|
||||
org.bukkit.Location feet = player.getLocation();
|
||||
return isSafeSurface(feet.clone().subtract(0.0D, 0.08D, 0.0D))
|
||||
|| isSafeSurface(feet.clone().subtract(0.0D, 0.51D, 0.0D));
|
||||
|
||||
}
|
||||
|
||||
private boolean isSafeSurface(org.bukkit.Location location) {
|
||||
|
||||
Block block = location.getBlock();
|
||||
return block.isLiquid() || !block.isPassable();
|
||||
|
||||
}
|
||||
|
||||
private void scheduleRefresh(Player player) {
|
||||
|
||||
UUID playerId = player.getUniqueId();
|
||||
if (!scheduledRefreshes.add(playerId))
|
||||
return;
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
|
||||
scheduledRefreshes.remove(playerId);
|
||||
refresh(player);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void spawnFlightParticles() {
|
||||
|
||||
particleStep++;
|
||||
plugin.getServer().getOnlinePlayers().forEach(player -> {
|
||||
|
||||
FlightOverride state = flightOverrides.get(player.getUniqueId());
|
||||
if (state == null || state.kind() != RuleKind.FLY || !player.getAllowFlight() || !player.isFlying())
|
||||
return;
|
||||
|
||||
org.bukkit.Location location = flightParticleLocation(player);
|
||||
if (hasBypass(player))
|
||||
spawnRainbowParticles(player, location);
|
||||
else
|
||||
player.getWorld().spawnParticle(Particle.CLOUD, location, 4, PARTICLE_RADIUS, 0.025D, PARTICLE_RADIUS,
|
||||
0.004D);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private org.bukkit.Location flightParticleLocation(Player player) {
|
||||
|
||||
org.bukkit.Location feet = player.getLocation();
|
||||
Vector behind = feet.getDirection();
|
||||
behind.setY(0.0D);
|
||||
if (behind.lengthSquared() > 0.0D)
|
||||
feet.add(behind.normalize().multiply(-PARTICLE_BACK_OFFSET));
|
||||
return feet.add(0.0D, PARTICLE_Y_OFFSET, 0.0D);
|
||||
|
||||
}
|
||||
|
||||
private void spawnRainbowParticles(Player player, org.bukkit.Location feet) {
|
||||
|
||||
int count = 12;
|
||||
double radius = 0.18D;
|
||||
Vector trailDirection = player.getLocation().getDirection();
|
||||
trailDirection.setY(0.0D);
|
||||
if (trailDirection.lengthSquared() > 0.0D)
|
||||
trailDirection.normalize().multiply(-0.14D);
|
||||
else
|
||||
trailDirection = null;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
||||
int colorIndex = Math.floorMod(particleStep + i, RAINBOW_TRAIL_COLORS.length);
|
||||
double angle = particleStep * 0.42D + i * Math.PI * 2.0D / count;
|
||||
org.bukkit.Location particleLocation = feet.clone().add(Math.cos(angle) * radius,
|
||||
Math.sin(angle * 2.0D) * 0.04D, Math.sin(angle) * radius);
|
||||
if (trailDirection != null)
|
||||
particleLocation.add(trailDirection.clone().multiply(i % 4));
|
||||
Particle.DustOptions dust = new Particle.DustOptions(RAINBOW_TRAIL_COLORS[colorIndex], 0.85F);
|
||||
player.getWorld().spawnParticle(Particle.REDSTONE, particleLocation, 1, 0.015D, 0.015D, 0.015D, 0.0D, dust);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private enum RuleKind {
|
||||
FLY, NOFLY
|
||||
}
|
||||
|
||||
private record RegionRule(String regionId, RuleKind kind, int priority) {
|
||||
}
|
||||
|
||||
private record FlightOverride(boolean originalAllowFlight, boolean appliedAllowFlight, String regionId,
|
||||
RuleKind kind)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package spawnog.flight;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class ToggleRegionFlightCommand implements CommandExecutor {
|
||||
|
||||
private final RegionFlightService flightService;
|
||||
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||
|
||||
public ToggleRegionFlightCommand(RegionFlightService flightService) {
|
||||
|
||||
this.flightService = flightService;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
|
||||
@NotNull String[] args)
|
||||
{
|
||||
|
||||
if (args.length != 0)
|
||||
return false;
|
||||
if (!(sender instanceof Player player)) {
|
||||
|
||||
sender.sendMessage("Only players can use /fly.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (!flightService.canToggleFlight(player)) {
|
||||
|
||||
player.sendMessage(miniMessage
|
||||
.deserialize("<red>Flight can only be toggled inside a region that allows flight.</red>"));
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
boolean enabled = flightService.toggleFlight(player);
|
||||
player.sendMessage(miniMessage
|
||||
.deserialize(enabled ? "<green>Flight enabled.</green>" : "<yellow>Flight disabled.</yellow>"));
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
package spawnog.listener;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.*;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerKickEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import spawnog.SpawnOG;
|
||||
import spawnog.login.LoginMigrationService;
|
||||
|
||||
public class SpawnListener implements Listener {
|
||||
|
||||
private final SpawnOG plugin = SpawnOG.getInstance();
|
||||
private final SpawnOG plugin;
|
||||
private final LoginMigrationService loginMigration;
|
||||
|
||||
public SpawnListener(SpawnOG plugin, LoginMigrationService loginMigration) {
|
||||
|
||||
this.plugin = plugin;
|
||||
this.loginMigration = loginMigration;
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent e) {
|
||||
@@ -23,15 +29,7 @@ public class SpawnListener implements Listener {
|
||||
boolean first = !p.hasPlayedBefore();
|
||||
boolean force = plugin.getConfig().getBoolean("spawn-on-join", false);
|
||||
|
||||
if (!first && !force) {
|
||||
|
||||
// Returning players keep their logout position; rescue them if it is unsafe.
|
||||
rescueIfUnsafe(p);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
Location dest;
|
||||
Location dest = null;
|
||||
if (first) {
|
||||
|
||||
String nb = plugin.getConfig().getString("newbies.spawnpoint", "newbie").toLowerCase();
|
||||
@@ -39,17 +37,31 @@ public class SpawnListener implements Listener {
|
||||
if (dest == null)
|
||||
dest = plugin.getConfig().getLocation("spawns.global.location");
|
||||
|
||||
} else {
|
||||
} else if (force) {
|
||||
|
||||
dest = plugin.getConfig().getLocation("spawns.global.location");
|
||||
|
||||
}
|
||||
|
||||
if (dest != null)
|
||||
if (!loginMigration.handleLogin(p, dest) && dest != null)
|
||||
p.teleportAsync(dest.clone());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent e) {
|
||||
|
||||
loginMigration.cancel(e.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onKick(PlayerKickEvent e) {
|
||||
|
||||
loginMigration.cancel(e.getPlayer());
|
||||
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRespawn(PlayerRespawnEvent e) {
|
||||
|
||||
@@ -59,11 +71,17 @@ public class SpawnListener implements Listener {
|
||||
if (atHome && (p.getBedSpawnLocation() != null))
|
||||
return;
|
||||
|
||||
if (atHome && plugin.getServer().getPluginManager().isPluginEnabled("Essentials")) {
|
||||
String essentialsName = null;
|
||||
if (plugin.getServer().getPluginManager().isPluginEnabled("Essentials-OG"))
|
||||
essentialsName = "Essentials-OG";
|
||||
else if (plugin.getServer().getPluginManager().isPluginEnabled("Essentials"))
|
||||
essentialsName = "Essentials";
|
||||
|
||||
if (atHome && essentialsName != null) {
|
||||
|
||||
try {
|
||||
|
||||
Object ess = plugin.getServer().getPluginManager().getPlugin("Essentials");
|
||||
Object ess = plugin.getServer().getPluginManager().getPlugin(essentialsName);
|
||||
Object user = ess.getClass().getMethod("getUser", Player.class).invoke(ess, p);
|
||||
Location home = (Location) user.getClass().getMethod("getHome", String.class).invoke(user, "home");
|
||||
if (home != null) {
|
||||
@@ -85,46 +103,4 @@ public class SpawnListener implements Listener {
|
||||
|
||||
}
|
||||
|
||||
// Teleport the player to spawn and report their old coordinates when their
|
||||
// login location is unsafe.
|
||||
private void rescueIfUnsafe(Player p) {
|
||||
|
||||
Location from = p.getLocation();
|
||||
if (!isUnsafe(from))
|
||||
return;
|
||||
|
||||
Location spawn = plugin.getConfig().getLocation("spawns.global.location");
|
||||
if (spawn == null)
|
||||
return;
|
||||
|
||||
String raw = plugin.getConfig().getString("locale.unsafeLogin",
|
||||
"<gold>You logged in at an unsafe location (<red><x>, <y>, <z></red> in <world>) and were teleported to spawn.</gold>");
|
||||
Component msg = MiniMessage.miniMessage().deserialize(raw,
|
||||
Placeholder.unparsed("x", String.valueOf(from.getBlockX())),
|
||||
Placeholder.unparsed("y", String.valueOf(from.getBlockY())),
|
||||
Placeholder.unparsed("z", String.valueOf(from.getBlockZ())),
|
||||
Placeholder.unparsed("world", from.getWorld() != null ? from.getWorld().getName() : "unknown"));
|
||||
p.sendMessage(msg);
|
||||
|
||||
p.teleportAsync(spawn.clone());
|
||||
|
||||
}
|
||||
|
||||
// Unsafe when an occluding block overlaps the player's body (suffocation) or
|
||||
// there is nothing solid underfoot (fall).
|
||||
private boolean isUnsafe(Location loc) {
|
||||
|
||||
Block feet = loc.getBlock();
|
||||
Block head = feet.getRelative(BlockFace.UP);
|
||||
Block ground = feet.getRelative(BlockFace.DOWN);
|
||||
|
||||
// Suffocation: head or feet inside a full solid block.
|
||||
if (feet.getType().isOccluding() || head.getType().isOccluding())
|
||||
return true;
|
||||
|
||||
// Air underfoot: nothing to stand on.
|
||||
return ground.isEmpty();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package spawnog.login;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import spawnog.SpawnOG;
|
||||
|
||||
public final class AutopsyMigrationStore {
|
||||
|
||||
private final SpawnOG plugin;
|
||||
private final File migrationFile;
|
||||
private final YamlConfiguration migrations;
|
||||
|
||||
public AutopsyMigrationStore(SpawnOG plugin) {
|
||||
|
||||
this.plugin = plugin;
|
||||
this.migrationFile = new File(plugin.getDataFolder(), "autopsy-migrations.yml");
|
||||
this.migrations = YamlConfiguration.loadConfiguration(migrationFile);
|
||||
|
||||
}
|
||||
|
||||
public boolean hasMigrated(UUID playerId) {
|
||||
|
||||
return migrations.getBoolean(path(playerId) + ".complete", false);
|
||||
|
||||
}
|
||||
|
||||
public boolean record(UUID playerId, String playerName, GameMode originalGamemode, Location originalLocation) {
|
||||
|
||||
String path = path(playerId);
|
||||
migrations.set(path + ".complete", true);
|
||||
migrations.set(path + ".player-name", playerName);
|
||||
migrations.set(path + ".migrated-at", Instant.now().toString());
|
||||
migrations.set(path + ".original-gamemode", originalGamemode.name());
|
||||
migrations.set(path + ".original-location.world",
|
||||
originalLocation.getWorld() == null ? null : originalLocation.getWorld().getName());
|
||||
migrations.set(path + ".original-location.x", originalLocation.getX());
|
||||
migrations.set(path + ".original-location.y", originalLocation.getY());
|
||||
migrations.set(path + ".original-location.z", originalLocation.getZ());
|
||||
migrations.set(path + ".original-location.yaw", originalLocation.getYaw());
|
||||
migrations.set(path + ".original-location.pitch", originalLocation.getPitch());
|
||||
|
||||
try {
|
||||
|
||||
saveAtomically();
|
||||
return true;
|
||||
|
||||
} catch (IOException error) {
|
||||
|
||||
migrations.set(path, null);
|
||||
plugin.getLogger()
|
||||
.severe("Unable to save the autopsy migration for " + playerName + ": " + error.getMessage());
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void saveAtomically() throws IOException {
|
||||
|
||||
File dataFolder = plugin.getDataFolder();
|
||||
if (!dataFolder.exists() && !dataFolder.mkdirs())
|
||||
throw new IOException("Unable to create " + dataFolder);
|
||||
|
||||
File temporaryFile = new File(dataFolder, migrationFile.getName() + ".tmp");
|
||||
migrations.save(temporaryFile);
|
||||
|
||||
try {
|
||||
|
||||
Files.move(temporaryFile.toPath(), migrationFile.toPath(), StandardCopyOption.ATOMIC_MOVE,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
} catch (AtomicMoveNotSupportedException error) {
|
||||
|
||||
Files.move(temporaryFile.toPath(), migrationFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String path(UUID playerId) {
|
||||
|
||||
return "players." + playerId;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package spawnog.login;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import spawnog.SpawnOG;
|
||||
|
||||
public final class LoginMigrationService {
|
||||
|
||||
private static final List<String> DEFAULT_STAFF_PERMISSIONS = List.of("spawnog.login-migration.bypass",
|
||||
"staffog.seebroadcast", "chat-og.staff", "sv.use");
|
||||
private static final Set<String> DEFAULT_WORLDS = Set.of("world", "world_nether", "world_the_end");
|
||||
private static final Set<Material> HAZARDOUS_BLOCKS = Set.of(Material.LAVA, Material.FIRE, Material.SOUL_FIRE,
|
||||
Material.POWDER_SNOW, Material.CACTUS, Material.SWEET_BERRY_BUSH, Material.MAGMA_BLOCK, Material.CAMPFIRE,
|
||||
Material.SOUL_CAMPFIRE, Material.WITHER_ROSE);
|
||||
|
||||
private final SpawnOG plugin;
|
||||
private final AutopsyMigrationStore migrationStore;
|
||||
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||
private final Map<UUID, PendingLogin> pendingLogins = new HashMap<>();
|
||||
|
||||
public LoginMigrationService(SpawnOG plugin, AutopsyMigrationStore migrationStore) {
|
||||
|
||||
this.plugin = plugin;
|
||||
this.migrationStore = migrationStore;
|
||||
|
||||
}
|
||||
|
||||
public boolean handleLogin(Player player, Location plannedDestination) {
|
||||
|
||||
if (!plugin.getConfig().getBoolean("login-safety.enabled", true))
|
||||
return false;
|
||||
|
||||
boolean managedWorld = isManagedWorld(player);
|
||||
GameMode originalGamemode = player.getGameMode();
|
||||
boolean staff = isStaff(player);
|
||||
boolean normalizeGamemode = managedWorld && !staff
|
||||
&& plugin.getConfig().getBoolean("login-safety.normalize-non-staff-gamemode", true)
|
||||
&& originalGamemode != GameMode.SURVIVAL;
|
||||
boolean autopsyMigration = managedWorld && !staff && originalGamemode == GameMode.SPECTATOR
|
||||
&& plugin.getConfig().getBoolean("login-safety.autopsy-migration.enabled", true)
|
||||
&& !migrationStore.hasMigrated(player.getUniqueId());
|
||||
boolean vulnerableAfterLogin = normalizeGamemode || originalGamemode == GameMode.SURVIVAL
|
||||
|| originalGamemode == GameMode.ADVENTURE;
|
||||
boolean unsafe = vulnerableAfterLogin && isUnsafe(player.getLocation());
|
||||
|
||||
Location destination = plannedDestination;
|
||||
if (destination == null && (autopsyMigration || unsafe))
|
||||
destination = globalSpawn();
|
||||
|
||||
boolean teleportRequired = destination != null;
|
||||
if (!teleportRequired && !normalizeGamemode)
|
||||
return false;
|
||||
|
||||
if ((autopsyMigration || unsafe) && destination == null) {
|
||||
|
||||
protectAfterFailedRescue(player, originalGamemode, unsafe);
|
||||
send(player, "locale.loginSafetyFailed",
|
||||
"<red>Your login location could not be made safe because the global spawn is not configured. Staff have been notified.</red>");
|
||||
plugin.getLogger().severe("Unable to safely migrate " + player.getName()
|
||||
+ " because spawns.global.location is not configured.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
beginTransaction(player, destination, originalGamemode, normalizeGamemode, autopsyMigration, unsafe);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public void cancel(Player player) {
|
||||
|
||||
PendingLogin pending = pendingLogins.remove(player.getUniqueId());
|
||||
if (pending != null)
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
|
||||
}
|
||||
|
||||
public boolean isPending(Player player) {
|
||||
|
||||
return player != null && pendingLogins.containsKey(player.getUniqueId());
|
||||
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
pendingLogins.forEach((playerId, pending) -> {
|
||||
|
||||
Player player = plugin.getServer().getPlayer(playerId);
|
||||
if (player != null)
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
|
||||
});
|
||||
pendingLogins.clear();
|
||||
|
||||
}
|
||||
|
||||
private void beginTransaction(Player player, Location destination, GameMode originalGamemode,
|
||||
boolean normalizeGamemode, boolean autopsyMigration, boolean unsafe)
|
||||
{
|
||||
|
||||
UUID playerId = player.getUniqueId();
|
||||
if (pendingLogins.containsKey(playerId))
|
||||
return;
|
||||
|
||||
PendingLogin pending = new PendingLogin(player.isInvulnerable(), player.getAllowFlight(), player.isFlying(),
|
||||
originalGamemode, player.getLocation().clone(), normalizeGamemode, autopsyMigration, unsafe);
|
||||
pendingLogins.put(playerId, pending);
|
||||
|
||||
player.setInvulnerable(true);
|
||||
player.setFallDistance(0.0F);
|
||||
player.setVelocity(new Vector());
|
||||
|
||||
if (destination == null) {
|
||||
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> complete(player, true, null));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
CompletableFuture<Boolean> teleport = player.teleportAsync(destination.clone());
|
||||
teleport.whenComplete((success, error) -> {
|
||||
|
||||
if (!plugin.isEnabled())
|
||||
return;
|
||||
plugin.getServer().getScheduler().runTask(plugin,
|
||||
() -> complete(player, Boolean.TRUE.equals(success), error));
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void complete(Player player, boolean teleportSucceeded, Throwable teleportError) {
|
||||
|
||||
PendingLogin pending = pendingLogins.remove(player.getUniqueId());
|
||||
if (pending == null)
|
||||
return;
|
||||
|
||||
if (!player.isOnline()) {
|
||||
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!teleportSucceeded || teleportError != null || isUnsafe(player.getLocation())) {
|
||||
|
||||
protectAfterFailedRescue(player, pending.originalGamemode(), true);
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
send(player, "locale.loginSafetyFailed",
|
||||
"<red>Your login safety migration could not finish. You were left in a non-damaging gamemode; please contact staff.</red>");
|
||||
plugin.getLogger().severe("Login safety transaction failed for " + player.getName()
|
||||
+ (teleportError == null ? "." : ": " + teleportError.getMessage()));
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (pending.normalizeGamemode()) {
|
||||
|
||||
if (player.isFlying())
|
||||
player.setFlying(false);
|
||||
player.setFallDistance(0.0F);
|
||||
player.setGameMode(GameMode.SURVIVAL);
|
||||
if (player.getGameMode() != GameMode.SURVIVAL) {
|
||||
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
send(player, "locale.loginSafetyFailed",
|
||||
"<red>Another plugin prevented your safe switch to survival. Staff have been notified.</red>");
|
||||
plugin.getLogger().severe(
|
||||
"Another plugin prevented Spawn-OG from normalizing " + player.getName() + " to survival.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (plugin.getConfig().getBoolean("login-safety.remove-flight-on-normalize", true))
|
||||
player.setAllowFlight(false);
|
||||
|
||||
}
|
||||
|
||||
if (pending.autopsyMigration()) {
|
||||
|
||||
boolean recorded = migrationStore.record(player.getUniqueId(), player.getName(), pending.originalGamemode(),
|
||||
pending.originalLocation());
|
||||
if (!recorded) {
|
||||
|
||||
rollbackAutopsyCommit(player, pending);
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
send(player, "locale.loginSafetyFailed",
|
||||
"<red>Your autopsy migration could not be recorded. You remain safely in spectator mode; please contact staff.</red>");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
Location from = pending.originalLocation();
|
||||
send(player, "locale.autopsyMigration",
|
||||
"<gold>Your OG:SMP autopsy state was safely migrated from <red><x>, <y>, <z></red> in <world>. You were moved to spawn and returned to survival.</gold>",
|
||||
Placeholder.unparsed("x", String.valueOf(from.getBlockX())),
|
||||
Placeholder.unparsed("y", String.valueOf(from.getBlockY())),
|
||||
Placeholder.unparsed("z", String.valueOf(from.getBlockZ())),
|
||||
Placeholder.unparsed("world", from.getWorld() == null ? "unknown" : from.getWorld().getName()));
|
||||
|
||||
} else if (pending.normalizeGamemode()) {
|
||||
|
||||
send(player, "locale.gamemodeNormalized",
|
||||
"<gold>Your login gamemode was safely returned to survival.</gold>");
|
||||
|
||||
} else if (pending.unsafe()) {
|
||||
|
||||
Location from = pending.originalLocation();
|
||||
send(player, "locale.unsafeLogin",
|
||||
"<gold>You logged in at an unsafe location (<red><x>, <y>, <z></red> in <world>) and were teleported to spawn.</gold>",
|
||||
Placeholder.unparsed("x", String.valueOf(from.getBlockX())),
|
||||
Placeholder.unparsed("y", String.valueOf(from.getBlockY())),
|
||||
Placeholder.unparsed("z", String.valueOf(from.getBlockZ())),
|
||||
Placeholder.unparsed("world", from.getWorld() == null ? "unknown" : from.getWorld().getName()));
|
||||
|
||||
}
|
||||
|
||||
player.setInvulnerable(pending.originalInvulnerable());
|
||||
|
||||
}
|
||||
|
||||
private void protectAfterFailedRescue(Player player, GameMode originalGamemode, boolean unsafe) {
|
||||
|
||||
if (unsafe && originalGamemode != GameMode.CREATIVE && originalGamemode != GameMode.SPECTATOR) {
|
||||
|
||||
player.setGameMode(GameMode.SPECTATOR);
|
||||
player.setFallDistance(0.0F);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void rollbackAutopsyCommit(Player player, PendingLogin pending) {
|
||||
|
||||
if (player.isFlying())
|
||||
player.setFlying(false);
|
||||
player.setGameMode(pending.originalGamemode());
|
||||
player.setAllowFlight(pending.originalAllowFlight());
|
||||
if (pending.originalFlying() && player.getAllowFlight())
|
||||
player.setFlying(true);
|
||||
player.setFallDistance(0.0F);
|
||||
|
||||
}
|
||||
|
||||
private boolean isManagedWorld(Player player) {
|
||||
|
||||
List<String> configuredWorlds = plugin.getConfig().getStringList("login-safety.worlds");
|
||||
if (configuredWorlds.isEmpty())
|
||||
return DEFAULT_WORLDS.contains(player.getWorld().getName());
|
||||
return configuredWorlds.stream().anyMatch(world -> world.equalsIgnoreCase(player.getWorld().getName()));
|
||||
|
||||
}
|
||||
|
||||
private boolean isStaff(Player player) {
|
||||
|
||||
List<String> permissions = plugin.getConfig().getStringList("login-safety.staff-bypass-permissions");
|
||||
if (permissions.isEmpty())
|
||||
permissions = DEFAULT_STAFF_PERMISSIONS;
|
||||
return permissions.stream().anyMatch(player::hasPermission);
|
||||
|
||||
}
|
||||
|
||||
private Location globalSpawn() {
|
||||
|
||||
Location spawn = plugin.getConfig().getLocation("spawns.global.location");
|
||||
return spawn == null ? null : spawn.clone();
|
||||
|
||||
}
|
||||
|
||||
private boolean isUnsafe(Location location) {
|
||||
|
||||
if (location.getWorld() == null)
|
||||
return true;
|
||||
if (location.getY() <= location.getWorld().getMinHeight()
|
||||
|| location.getY() >= location.getWorld().getMaxHeight())
|
||||
return true;
|
||||
if (!location.getWorld().getWorldBorder().isInside(location))
|
||||
return true;
|
||||
|
||||
Block feet = location.getBlock();
|
||||
Block head = feet.getRelative(BlockFace.UP);
|
||||
Block ground = feet.getRelative(BlockFace.DOWN);
|
||||
if (feet.getType().isOccluding() || head.getType().isOccluding())
|
||||
return true;
|
||||
if (HAZARDOUS_BLOCKS.contains(feet.getType()) || HAZARDOUS_BLOCKS.contains(head.getType())
|
||||
|| HAZARDOUS_BLOCKS.contains(ground.getType()))
|
||||
return true;
|
||||
return ground.isEmpty();
|
||||
|
||||
}
|
||||
|
||||
private void send(Player player, String path, String fallback, TagResolver... placeholders) {
|
||||
|
||||
String message = plugin.getConfig().getString(path, fallback);
|
||||
Component component = miniMessage.deserialize(message, placeholders);
|
||||
player.sendMessage(component);
|
||||
|
||||
}
|
||||
|
||||
private record PendingLogin(boolean originalInvulnerable, boolean originalAllowFlight, boolean originalFlying,
|
||||
GameMode originalGamemode, Location originalLocation, boolean normalizeGamemode, boolean autopsyMigration,
|
||||
boolean unsafe)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,35 @@
|
||||
spawn-on-join: default
|
||||
spawn-on-join: false
|
||||
respawn-at-home: true
|
||||
newbies:
|
||||
spawnpoint: newbie
|
||||
login-safety:
|
||||
enabled: true
|
||||
# Only these SMP dimensions are normalized. Add other survival worlds explicitly.
|
||||
worlds:
|
||||
- world
|
||||
- world_nether
|
||||
- world_the_end
|
||||
autopsy-migration:
|
||||
enabled: true
|
||||
normalize-non-staff-gamemode: true
|
||||
remove-flight-on-normalize: true
|
||||
# LuckPerms-backed permissions. Matching any entry exempts a player from
|
||||
# spectator migration and creative/adventure-to-survival normalization.
|
||||
staff-bypass-permissions:
|
||||
- spawnog.login-migration.bypass
|
||||
- staffog.seebroadcast
|
||||
- chat-og.staff
|
||||
- sv.use
|
||||
flight:
|
||||
enabled: true
|
||||
regions:
|
||||
spawn: fly
|
||||
warzone: nofly
|
||||
locale:
|
||||
unsafeLogin: "<gold>You logged in at an unsafe location (<red><x>, <y>, <z></red> in <world>) and were teleported to spawn.</gold>"
|
||||
autopsyMigration: "<gold>Your OG:SMP autopsy state was safely migrated from <red><x>, <y>, <z></red> in <world>. You were moved to spawn and returned to survival.</gold>"
|
||||
gamemodeNormalized: "<gold>Your login gamemode was safely returned to survival.</gold>"
|
||||
loginSafetyFailed: "<red>Your login safety migration could not finish safely. Please contact staff.</red>"
|
||||
spawns:
|
||||
global:
|
||||
location:
|
||||
|
||||
@@ -1,28 +1,59 @@
|
||||
name: Spawn-OG
|
||||
description: A Minecraft Plugin simplifies teleporting to the spawn location.
|
||||
description: A drop-in replacement for EssentialsSpawn
|
||||
description: Safe player entry, spawn management, and WorldGuard regional flight for TrueOG.
|
||||
version: '${version}'
|
||||
api-version: '${apiVersion}'
|
||||
authors: [ kasumabalidps, NotAlexNoyle ]
|
||||
website: https://github.com/true-og/Spawn-OG
|
||||
main: spawnog.SpawnOG
|
||||
api-version: 1.19
|
||||
softdepend: [LuckPerms, WorldEdit, WorldGuard]
|
||||
loadbefore: [Essentials, Essentials-OG]
|
||||
commands:
|
||||
spawn:
|
||||
description: Teleport yourself (or another player) to the server spawn
|
||||
usage: /spawn [player]
|
||||
permission: spawnog.teleport
|
||||
permission: spawnog.spawn
|
||||
setspawn:
|
||||
description: Set or clear the server spawn point
|
||||
usage: /setspawn [normalize-view] [normalize-position] | /setspawn unset
|
||||
permission: spawnog.admin
|
||||
permission: spawnog.setspawn
|
||||
fly:
|
||||
description: Toggle flight inside a WorldGuard region configured to allow it
|
||||
usage: /fly
|
||||
permission: spawnog.flight
|
||||
permissions:
|
||||
spawnog.teleport:
|
||||
spawnog.spawn:
|
||||
description: Use /spawn on yourself
|
||||
default: true
|
||||
spawn.others:
|
||||
spawnog.spawn.others:
|
||||
description: Use /spawn <player>
|
||||
default: op
|
||||
spawnog.admin:
|
||||
default: false
|
||||
spawnog.setspawn:
|
||||
description: Use /setspawn
|
||||
default: op
|
||||
default: false
|
||||
spawnog.login-migration.bypass:
|
||||
description: Exempt a staff member from autopsy migration and login gamemode normalization
|
||||
default: false
|
||||
spawnog.flight:
|
||||
description: Use /fly inside regions configured to allow flight
|
||||
default: true
|
||||
spawnog.flight.bypass:
|
||||
description: Exempt a player from regional no-flight rules
|
||||
default: false
|
||||
spawnog.flight.*:
|
||||
description: Grant regional flight use and bypass
|
||||
default: false
|
||||
children:
|
||||
spawnog.flight: true
|
||||
spawnog.flight.bypass: true
|
||||
wgamemode.fly:
|
||||
description: Legacy alias for spawnog.flight
|
||||
default: true
|
||||
wgamemode.fly.bypass:
|
||||
description: Legacy alias for spawnog.flight.bypass
|
||||
default: false
|
||||
wgamemode.*:
|
||||
description: Legacy aggregate permission for WGamemode-OG compatibility
|
||||
default: false
|
||||
children:
|
||||
wgamemode.fly: true
|
||||
wgamemode.fly.bypass: true
|
||||
Reference in new issue
Block a user