Override Votifier /vote, fix world loading
This commit is contained in:
1 parent
22dfa1a737
commit
009dbe137c
17 files changed
+807
-26
No files matched your search
@@ -35,3 +35,6 @@ build/
|
||||
.project
|
||||
.classpath
|
||||
bin/
|
||||
|
||||
# Vendored compile-only APIs under libs/ are tracked on purpose.
|
||||
!libs/*/*.jar
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
**Splegg-OG** is a Splegg plugin originally made by MrLuangamer, updated for Worldedit 7.2 and Spigot 1.16.4 by Hraponssi, then updated for Purpur 1.19.4 for use at [TrueOG Network](https://true-og.net/) by [NotAlexNoyle](https://github.com/NotAlexNoyle/).
|
||||
|
||||
Runtime dependencies: WorldEdit, DiamondBank-OG, Essentials-OG, and MyWorlds.
|
||||
Runtime dependencies: WorldEdit, DiamondBank-OG, Essentials-OG, and MyWorlds. Chat-OG is optional and integrated when present.
|
||||
|
||||
Current feature state:
|
||||
|
||||
- Matches run in per-game MyWorlds copies of the configured template world, so multiple games can use the same map without sharing arena blocks, scoreboards, or player state.
|
||||
- Matches run in per-game MyWorlds copies of the configured template world, so multiple games can use the same map without sharing arena blocks, scoreboards, or player state. The template copied is the world the map's **terrain** is in, taken from its spawns and then its floors; a match lobby saved in some other world never decides it.
|
||||
- Splegg creates isolated MyWorlds inventory groups for configured lobby and in-game worlds. It does not manage the vanilla main-world inventory group.
|
||||
- Players can join by command, random queue, or registered lobby signs, then vote for the map while waiting in the lobby.
|
||||
- Players can join by lobby id with `/spjoin`, by map with `/splegg join`, by random queue, or through registered lobby signs, then vote for the map while waiting in the lobby.
|
||||
- Players can leave with `/splegg leave`, `/hub`, or the lobby slimeball.
|
||||
- Players who reconnect inside a Splegg world are returned to a protected main-world spawn instead of remaining in an abandoned match world.
|
||||
|
||||
@@ -51,7 +51,7 @@ Teleport to your in-game world (`/mw tp <world_name>`) and run:
|
||||
### 4. Define the Arena
|
||||
While standing in the map:
|
||||
1. **Set Spawns:** Run `/splegg setspawn <map-name>` at every player starting position. You need at least two. Use `next` or a number to modify them.
|
||||
2. **Set Lobby:** Run `/splegg setlobby <map-name>` where players should wait before the game starts.
|
||||
2. **Set Lobby:** Run `/splegg setlobby <map-name>` where players should wait before the game starts. You must be standing **inside the map's own world** -- the match lobby is rebased into every per-game copy, so coordinates surveyed anywhere else land in unrelated terrain. Splegg refuses the command from another world and tells you which one it wants. For a waiting area outside the map, use the global `/splegg setlobby` with no map name instead.
|
||||
3. **Define Floors:**
|
||||
- Use the WorldEdit wand (`//wand`) to select two parallel corners of a floor.
|
||||
- Run `/splegg addfloor <map-name>`. Repeat for as many floors as needed.
|
||||
@@ -64,20 +64,22 @@ Run `/splegg join <map-name>` to ensure everything is working correctly.
|
||||
### Management & Setup
|
||||
- `/splegg create <map>` - Initialize a new map.
|
||||
- `/splegg setspawn <map> [next|#]` - Define/modify player start points.
|
||||
- `/splegg setlobby <map>` - Set map-specific warm-up area.
|
||||
- `/splegg setlobby` - Set the global fallback lobby.
|
||||
- `/splegg setlobby <map>` - Set map-specific warm-up area. Must be run inside that map's world.
|
||||
- `/splegg setlobby` - Set the global fallback lobby. Any world.
|
||||
- `/splegg addfloor <map>` - Add selected WorldEdit region as a floor.
|
||||
- `/splegg info <map>` - Check setup status (spawns, floors, etc.).
|
||||
|
||||
### Gameplay
|
||||
- `/spjoin [lobby]` - Join a lobby by id (`SP1`, or just `1`). With no argument, joins the best open lobby.
|
||||
- `/splegg join <map>` - Join a specific map.
|
||||
- `/splegg random` - Join a random available map.
|
||||
- `/vote [#]` - View or cast votes for the current lobby's maps.
|
||||
- `/vote [#]` or `/v [#]` - View or cast votes for the current lobby's maps.
|
||||
- `/splegg leave` - Exit a lobby or match.
|
||||
- `/hub` - Return to the main world spawn.
|
||||
|
||||
### Administration
|
||||
- `/splegg start [map]` - Force start a match.
|
||||
- `/spforcestart [time]` - Start the match ignoring `Options.AutoStartPlayers`.
|
||||
- `/splegg start [map]` - Start a match that already has enough players.
|
||||
- `/splegg stop [map]` - End a match immediately.
|
||||
- `/splegg list` - List all maps, their status, and player counts.
|
||||
- `/splegg help` - Full command reference.
|
||||
@@ -90,6 +92,24 @@ When a player joins a Splegg lobby, the lobby picks up to `Options.VotingMaps` p
|
||||
|
||||
When the start countdown reaches `Options.EndVotingAt`, voting ends and the highest-voted map wins. If the winning map is different from the joined map, Splegg prepares a new per-game world copy for the winner, moves lobby players to that map's queue lobby, updates signs and scoreboards, and starts the match on the winning map.
|
||||
|
||||
`/v` and `/vote` are claimed inside Splegg territory before any other plugin sees them, so VotingPlugin cannot take the `/vote` label away from map voting. The claim covers a player who is in a Splegg game, and anyone standing in a configured lobby or in-game world or a per-match copy. Everywhere else `/vote` behaves normally.
|
||||
|
||||
**Force Starting:**
|
||||
|
||||
`/spforcestart [time]` restarts the lobby countdown ignoring `Options.AutoStartPlayers`, so a match can begin below the configured minimum. The countdown floor is `Options.EndVotingAt` so map voting still resolves before the match starts, and `[time]` raises it. Requires `splegg.admin`.
|
||||
|
||||
**Chat:**
|
||||
|
||||
When Chat-OG is installed, Splegg registers a chat formatter for its `Worlds.GamePrefix` key (`SP` by default), which covers both the configured lobby worlds and every per-match copy. Lobby chat shows the player count, in-game chat shows blocks broken, and eliminated players are greyed out. Chat-OG's `discord.games` must contain a matching `SP` key for Splegg worlds to get their own chat; without it they stay in global chat and the formatter never runs.
|
||||
|
||||
Chat-OG only routes worlds named `<letters><number>-<name>`, so a lobby world named anything else stays in global chat while Splegg still treats it as its own. Splegg warns at startup naming any such lobby world. `Worlds.InGame` entries are deliberately not checked: they are match templates, nobody queues or plays in one, and each per-game copy is named `<prefix><gameId>-<template>` and routes whatever the template is called.
|
||||
|
||||
**Match Lobbies:**
|
||||
|
||||
A map's match lobby is stored as bare coordinates and rebased into whichever per-game world copy the match runs in, so it only means anything when it was surveyed inside the map's own world. One saved elsewhere is ignored at runtime -- the queue falls through to the global lobby, then to spawn 1 -- and `/splegg info <map>` reports it as `ignored` with the command to re-run. Splegg logs this once per map on load rather than on every sign refresh.
|
||||
|
||||
If you are upgrading and a map's lobby was saved in a shared hub world, that map was previously copying the *hub* as its arena. Re-run `/splegg setlobby <map>` inside the map, or clear the entry and rely on the global lobby.
|
||||
|
||||
**Lobby Signs:**
|
||||
|
||||
Admins can create a join sign by placing a sign with these lines:
|
||||
|
||||
@@ -62,6 +62,7 @@ dependencies {
|
||||
compileOnly(
|
||||
files("libs/BKCommonLib/BKCommonLib-1.19.4-v2.jar")
|
||||
) // Import BKCommonLib API (local build, required by MyWorlds at compile time).
|
||||
compileOnly(files("libs/Chat-OG/Chat-OG.jar")) // Import Chat-OG API for world chat formatting.
|
||||
implementation(project(":libs:GxUI-OG")) // Import TrueOG Network GxUI-OG Java API (from source).
|
||||
compileOnlyApi(project(":libs:Utilities-OG")) // Import TrueOG Network Utilities-OG Java API (from source).
|
||||
compileOnlyApi(project(":libs:DiamondBank-OG")) {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
@@ -0,0 +1,82 @@
|
||||
package chat;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import nl.skbotnl.chatog.api.WorldChatFormatter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import main.SpleggOG;
|
||||
import managers.Game;
|
||||
import managers.Status;
|
||||
import utils.SpleggPlayer;
|
||||
import utils.UtilPlayer;
|
||||
import utils.Utils;
|
||||
|
||||
// Renders Splegg chat now that Chat-OG owns delivery and the Discord relay.
|
||||
//
|
||||
// The game is resolved from the sender rather than from the lobby id Chat-OG passes: a Splegg game
|
||||
// is a live object keyed by game id, and its worlds are per-match copies, so the player is the only
|
||||
// reliable handle. The lobby id is still the right thing for Chat-OG to route on, it just is not
|
||||
// what identifies a Game here.
|
||||
public class SpleggChatFormatter implements WorldChatFormatter {
|
||||
|
||||
@Override
|
||||
public Component format(Player sender, Component message, String worldName, String lobbyId) {
|
||||
|
||||
final SpleggOG plugin = SpleggOG.getPlugin();
|
||||
if (plugin == null || plugin.pm == null) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
final UtilPlayer trackedPlayer = plugin.pm.getPlayer(sender);
|
||||
final Game game = trackedPlayer == null ? null : trackedPlayer.getGame();
|
||||
|
||||
// Standing in a Splegg world without being in a game (staff, a stale copy):
|
||||
// let Chat-OG use its default format rather than inventing a game prefix.
|
||||
if (game == null) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
// The message is already sanitised by Chat-OG, so it is composed with rather
|
||||
// than re-parsed.
|
||||
return Component.join(JoinConfiguration.noSeparators(),
|
||||
Utils.legacySerializerAnyCase(buildPrefix(sender, trackedPlayer, game)), message);
|
||||
|
||||
}
|
||||
|
||||
private String buildPrefix(Player player, UtilPlayer trackedPlayer, Game game) {
|
||||
|
||||
final String name = "&9" + PlainTextComponentSerializer.plainText().serialize(player.displayName()) + "&8 » &r";
|
||||
|
||||
if (game.getStatus() == Status.LOBBY) {
|
||||
|
||||
final int max = game.getMap() == null ? 0 : game.getMap().getSpawnCount();
|
||||
return "&e" + game.getPlayers().size() + "&7/&e" + max + "&8 ▏ " + name;
|
||||
|
||||
}
|
||||
|
||||
if (game.getStatus() != Status.INGAME) {
|
||||
|
||||
return name;
|
||||
|
||||
}
|
||||
|
||||
if (!trackedPlayer.isAlive()) {
|
||||
|
||||
return "&8&lOUT &8▏ &7" + PlainTextComponentSerializer.plainText().serialize(player.displayName())
|
||||
+ "&8 » &7";
|
||||
|
||||
}
|
||||
|
||||
final SpleggPlayer spleggPlayer = game.getPlayers().get(player.getUniqueId());
|
||||
final int broken = spleggPlayer == null ? 0 : spleggPlayer.getBroken();
|
||||
return "&e" + broken + "&8 ▏ " + name;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package commands;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import main.SpleggOG;
|
||||
import managers.Game;
|
||||
import managers.Status;
|
||||
import utils.UtilPlayer;
|
||||
import utils.Utils;
|
||||
|
||||
// Starts the lobby countdown regardless of Options.AutoStartPlayers.
|
||||
public class ForceStartCommand implements CommandExecutor {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
|
||||
sender.sendMessage("You are unable to use this command.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (!player.hasPermission("splegg.admin")) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cYou do not have permission to use this command.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
final UtilPlayer trackedPlayer = SpleggOG.getPlugin().pm.getPlayer(player);
|
||||
final Game game = trackedPlayer != null ? trackedPlayer.getGame() : null;
|
||||
if (game == null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cYou must be in a lobby to do this.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (game.getStatus() != Status.LOBBY) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cYou cannot run this command right now.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (game.getPlayers().isEmpty()) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cThere is nobody in the lobby to start a game for.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
int startTime = 0;
|
||||
if (args.length > 0) {
|
||||
|
||||
try {
|
||||
|
||||
startTime = Integer.parseInt(args[0]);
|
||||
|
||||
} catch (NumberFormatException error) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cCorrect Usage: /spforcestart [time]");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final int countdown = game.forceStartCountdown(startTime);
|
||||
if (countdown < 0) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cYou cannot run this command right now.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
SpleggOG.getPlugin().chat.bc("&6The game will start in &b" + countdown + "&6 seconds.", game);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package commands;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import config.Map;
|
||||
import main.SpleggOG;
|
||||
import managers.Game;
|
||||
import utils.UtilPlayer;
|
||||
import utils.Utils;
|
||||
|
||||
// Joins a Splegg lobby by its id (SP1). Maps are not accepted; with no argument
|
||||
// the player is put into the best open lobby, creating one when none is up.
|
||||
public class JoinLobbyCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
|
||||
sender.sendMessage("You are unable to use this command.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (!player.hasPermission("splegg.join")) {
|
||||
|
||||
Utils.spleggOGMessage(player, SpleggOG.getPlugin().getConfig().getString("Messages.NoPermission"));
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
final UtilPlayer trackedPlayer = SpleggOG.getPlugin().pm.getPlayer(player);
|
||||
if (trackedPlayer == null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cERROR: You are not tracked by Splegg yet. Try again in a moment.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (trackedPlayer.getGame() != null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cERROR: You are already playing.");
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
|
||||
joinBestLobby(player, trackedPlayer);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
final Game game = SpleggOG.getPlugin().games.resolveLobby(args[0]);
|
||||
if (game == null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&c" + args[0] + " does not exist.");
|
||||
sendLobbyList(player);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
game.joinGame(trackedPlayer);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
|
||||
if (args.length != 1) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
final String prefix = args[0].toLowerCase(Locale.ROOT);
|
||||
final List<String> completions = new ArrayList<>();
|
||||
for (String id : SpleggOG.getPlugin().games.getLobbyIds()) {
|
||||
|
||||
if (id.toLowerCase(Locale.ROOT).startsWith(prefix)) {
|
||||
|
||||
completions.add(id);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return completions;
|
||||
|
||||
}
|
||||
|
||||
// No lobby was named, so reuse an open one before spinning a new map up.
|
||||
private void joinBestLobby(Player player, UtilPlayer trackedPlayer) {
|
||||
|
||||
final Map chosen = SpleggOG.getPlugin().maps.getRandomMap();
|
||||
if (chosen == null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cERROR: No playable maps are currently available.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
final Game game = SpleggOG.getPlugin().games.findOrCreateForMap(chosen);
|
||||
if (game == null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cERROR: Failed to start a game on &e" + chosen.getName() + "&c.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
Utils.spleggOGMessage(player, "&aJoining lobby &e" + SpleggOG.getPlugin().games.getLobbyId(game) + "&a.");
|
||||
game.joinGame(trackedPlayer);
|
||||
|
||||
}
|
||||
|
||||
private void sendLobbyList(Player player) {
|
||||
|
||||
final List<String> ids = SpleggOG.getPlugin().games.getLobbyIds();
|
||||
if (ids.isEmpty()) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&6No lobbies are open. Use &e/spjoin&6 to open one.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
Utils.spleggOGMessage(player, "&6Join a lobby with /spjoin <lobby>.");
|
||||
for (String id : ids) {
|
||||
|
||||
final Game game = SpleggOG.getPlugin().games.resolveLobby(id);
|
||||
if (game == null || game.getMap() == null) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
Utils.spleggOGMessage(player, "&6&l" + id + " &6- &b" + game.getMap().getName() + " &6(&b"
|
||||
+ game.getPlayers().size() + "&6/&b" + game.getMap().getSpawnCount() + "&6)");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -482,10 +482,34 @@ public class SpleggCommand implements CommandExecutor, TabCompleter {
|
||||
firstUserCommandArgument = args[1];
|
||||
if (SpleggOG.getPlugin().maps.mapExists(firstUserCommandArgument)) {
|
||||
|
||||
SpleggOG.getPlugin().maps.getMap(firstUserCommandArgument)
|
||||
.setLobby(player.getLocation());
|
||||
Utils.spleggOGMessage(player, "&aThe lobby for map: &e" + firstUserCommandArgument
|
||||
+ "&a has been set to: &2" + player.getLocation() + "&a.");
|
||||
final Map targetMap = SpleggOG.getPlugin().maps.getMap(firstUserCommandArgument);
|
||||
final String terrainWorld = targetMap.getTerrainWorldName();
|
||||
|
||||
// The match lobby is rebased into every per-game copy of the
|
||||
// map's world, so surveying it anywhere else stores coordinates
|
||||
// that mean nothing once they land in the copy.
|
||||
if (terrainWorld == null) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&cERROR: Map &e" + firstUserCommandArgument
|
||||
+ " &chas no spawns yet, so its world is unknown. &6Run &e/splegg setspawn "
|
||||
+ firstUserCommandArgument + " &6first.");
|
||||
|
||||
} else if (!terrainWorld.equalsIgnoreCase(player.getWorld().getName())) {
|
||||
|
||||
Utils.spleggOGMessage(player,
|
||||
"&cERROR: The match lobby for &e" + firstUserCommandArgument
|
||||
+ " &cmust be inside that map's world (&e" + terrainWorld
|
||||
+ "&c). &6You are in &e" + player.getWorld().getName()
|
||||
+ "&6. Use &e/splegg setlobby &6with no map name to set the "
|
||||
+ "global queue lobby instead.");
|
||||
|
||||
} else {
|
||||
|
||||
targetMap.setLobby(player.getLocation());
|
||||
Utils.spleggOGMessage(player, "&aThe lobby for map: &e" + firstUserCommandArgument
|
||||
+ "&a has been set to: &2" + player.getLocation() + "&a.");
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
@@ -772,8 +796,21 @@ public class SpleggCommand implements CommandExecutor, TabCompleter {
|
||||
+ (map.getWorldName() != null ? map.getWorldName() : "&cunset (set a spawn first)"));
|
||||
Utils.spleggOGMessage(player, "&6Spawn points: &f" + map.getSpawnCount());
|
||||
Utils.spleggOGMessage(player, "&6Floor regions: &f" + map.getFloors());
|
||||
Utils.spleggOGMessage(player,
|
||||
"&6Match lobby: " + (map.lobbySet() ? "&aset" : "&cunset (falls back to global lobby)"));
|
||||
if (!map.lobbySet()) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&6Match lobby: &cunset (falls back to global lobby)");
|
||||
|
||||
} else if (map.isLobbyInMapWorld()) {
|
||||
|
||||
Utils.spleggOGMessage(player, "&6Match lobby: &aset");
|
||||
|
||||
} else {
|
||||
|
||||
Utils.spleggOGMessage(player, "&6Match lobby: &cignored -- saved in a different world. &6Re-run &e/splegg "
|
||||
+ "setlobby " + map.getName() + " &6inside &e" + map.getTerrainWorldName() + "&6.");
|
||||
|
||||
}
|
||||
|
||||
Utils.spleggOGMessage(player, "&6Active games: &f" + games.size());
|
||||
|
||||
final boolean playable = map.isUsable(map);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package commands;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
|
||||
import main.SpleggOG;
|
||||
import managers.GameWorldManager;
|
||||
import utils.UtilPlayer;
|
||||
|
||||
// Routes /v and /vote to Splegg map voting inside Splegg games and worlds only.
|
||||
// Without this, VotingPlugin can own the /vote label and swallow map votes.
|
||||
public class VoteCommandListener implements Listener {
|
||||
|
||||
private final VoteCommand voteCommand;
|
||||
|
||||
public VoteCommandListener(VoteCommand voteCommand) {
|
||||
|
||||
this.voteCommand = voteCommand;
|
||||
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onCommandPreprocess(PlayerCommandPreprocessEvent event) {
|
||||
|
||||
final String message = event.getMessage();
|
||||
if (message == null || message.length() < 2 || message.charAt(0) != '/') {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
final String[] parts = message.substring(1).trim().split("\\s+");
|
||||
if (parts.length == 0 || parts[0].isEmpty()) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Drop any namespace prefix (e.g. votingplugin:v) before matching.
|
||||
String label = parts[0].toLowerCase(Locale.ROOT);
|
||||
final int colon = label.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
|
||||
label = label.substring(colon + 1);
|
||||
|
||||
}
|
||||
|
||||
if (!label.equals("v") && !label.equals("vote")) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
final Player player = event.getPlayer();
|
||||
if (!isSpleggTerritory(player)) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
final String[] args = parts.length > 1 ? Arrays.copyOfRange(parts, 1, parts.length) : new String[0];
|
||||
voteCommand.onCommand(player, null, label, args);
|
||||
event.setCancelled(true);
|
||||
|
||||
}
|
||||
|
||||
// Claims the command for a tracked Splegg player, or for anyone standing in a
|
||||
// Splegg lobby, in-game, or per-match world. Defers everywhere else.
|
||||
private boolean isSpleggTerritory(Player player) {
|
||||
|
||||
final SpleggOG plugin = SpleggOG.getPlugin();
|
||||
if (plugin == null) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
if (plugin.pm != null) {
|
||||
|
||||
final UtilPlayer trackedPlayer = plugin.pm.getPlayer(player);
|
||||
if (trackedPlayer != null && trackedPlayer.getGame() != null) {
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final String worldName = player.getWorld().getName();
|
||||
if (plugin.isSpleggWorld(worldName)) {
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
final GameWorldManager gameWorldManager = plugin.getGameWorldManager();
|
||||
return gameWorldManager != null && !SpleggOG.isProtectedMainWorld(worldName)
|
||||
&& gameWorldManager.isGameCopyName(worldName);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,11 @@ public class Map {
|
||||
int spawncount;
|
||||
int floorcount;
|
||||
boolean usable;
|
||||
// isUsable runs on every sign refresh and every voting-map pick, so the lobby
|
||||
// world warning below is latched instead of logged per call.
|
||||
// isUsable runs on every sign refresh and every voting-map pick, so the lobby
|
||||
// world warning is latched rather than logged each time. Cleared by load().
|
||||
private boolean warnedLobbyWorldMismatch;
|
||||
|
||||
// Enable the conversion of text from config.yml to objects.
|
||||
public FileConfiguration config = SpleggOG.getPlugin().getConfig();
|
||||
@@ -36,6 +41,7 @@ public class Map {
|
||||
public void load() {
|
||||
|
||||
SpleggOG.getPlugin().getLogger().info("Loading map " + this.name + "...");
|
||||
this.warnedLobbyWorldMismatch = false;
|
||||
|
||||
this.file = new File(SpleggOG.getPlugin().getDataFolder(), this.name + ".yml");
|
||||
try {
|
||||
@@ -122,6 +128,21 @@ public class Map {
|
||||
|
||||
}
|
||||
|
||||
// Not fatal: getQueueLobbyLocation ignores a lobby stored against the
|
||||
// wrong world and falls back, so the map still plays. Say so once, since
|
||||
// the operator's intended waiting area is silently not being used.
|
||||
if (!this.isLobbyInMapWorld() && !this.warnedLobbyWorldMismatch) {
|
||||
|
||||
this.warnedLobbyWorldMismatch = true;
|
||||
|
||||
Bukkit.getLogger()
|
||||
.warning("[Splegg-OG] Map '" + this.getName() + "' has its match lobby saved in world '"
|
||||
+ lobbyWorld + "' but its terrain is in '" + this.getTerrainWorldName()
|
||||
+ "'. The match lobby is ignored. Re-run /splegg setlobby " + this.getName()
|
||||
+ " while standing in the map, or use /splegg setlobby for a global queue lobby.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -360,13 +381,11 @@ public class Map {
|
||||
|
||||
}
|
||||
|
||||
public String getWorldName() {
|
||||
|
||||
if (this.lobbySet()) {
|
||||
|
||||
return this.config.getString("Spawns.lobby.world");
|
||||
|
||||
}
|
||||
// The world the map's terrain lives in, and therefore the template
|
||||
// GameWorldManager copies per match. Spawns and floors define it; the match
|
||||
// lobby never does. Reading the lobby first made a lobby set in another world
|
||||
// (a shared hub) the thing that got copied as the arena.
|
||||
public String getTerrainWorldName() {
|
||||
|
||||
if (this.config.isString("Spawns.1.world")) {
|
||||
|
||||
@@ -384,6 +403,37 @@ public class Map {
|
||||
|
||||
}
|
||||
|
||||
public String getWorldName() {
|
||||
|
||||
final String terrain = this.getTerrainWorldName();
|
||||
if (terrain != null) {
|
||||
|
||||
return terrain;
|
||||
|
||||
}
|
||||
|
||||
// Nothing but a lobby is configured. Such a map has no spawns, so it is
|
||||
// already unusable; returning the lobby keeps /splegg info informative.
|
||||
return this.lobbySet() ? this.config.getString("Spawns.lobby.world") : null;
|
||||
|
||||
}
|
||||
|
||||
// The match lobby is rebased into each per-game world copy, so its coordinates
|
||||
// are only meaningful when they were surveyed in the map's own world. A lobby
|
||||
// stored against any other world is a misconfiguration, not a shared hub.
|
||||
public boolean isLobbyInMapWorld() {
|
||||
|
||||
if (!this.lobbySet()) {
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
final String terrain = this.getTerrainWorldName();
|
||||
return terrain != null && terrain.equalsIgnoreCase(this.config.getString("Spawns.lobby.world"));
|
||||
|
||||
}
|
||||
|
||||
public void setLobby(Location l) {
|
||||
|
||||
int x = l.getBlockX();
|
||||
|
||||
@@ -149,8 +149,8 @@ public class PlayerListener implements Listener {
|
||||
|
||||
private boolean isAllowedGameCommand(String message) {
|
||||
|
||||
return isCommand(message, "/splegg") || isCommand(message, "/sp") || isCommand(message, "/hub")
|
||||
|| isCommand(message, "/vote") || isCommand(message, "/v");
|
||||
return isCommand(message, "/splegg") || isCommand(message, "/sp") || isCommand(message, "/spjoin")
|
||||
|| isCommand(message, "/hub") || isCommand(message, "/vote") || isCommand(message, "/v");
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,15 @@ import com.bergerkiller.bukkit.mw.WorldInventory;
|
||||
import com.earth2me.essentials.Essentials;
|
||||
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
|
||||
|
||||
import nl.skbotnl.chatog.api.ChatOGAPI;
|
||||
|
||||
import chat.SpleggChatFormatter;
|
||||
import commands.ForceStartCommand;
|
||||
import commands.HubCommand;
|
||||
import commands.JoinLobbyCommand;
|
||||
import commands.SpleggCommand;
|
||||
import commands.VoteCommand;
|
||||
import commands.VoteCommandListener;
|
||||
import commands.VoteCompleter;
|
||||
import config.MapUtilities;
|
||||
import events.Listeners;
|
||||
@@ -169,9 +175,16 @@ public class SpleggOG extends JavaPlugin {
|
||||
this.getCommand("splegg").setExecutor(spleggCommand);
|
||||
this.getCommand("splegg").setTabCompleter(spleggCommand);
|
||||
this.getCommand("hub").setExecutor(new HubCommand());
|
||||
this.getCommand("spforcestart").setExecutor(new ForceStartCommand());
|
||||
final JoinLobbyCommand joinLobbyCommand = new JoinLobbyCommand();
|
||||
this.getCommand("spjoin").setExecutor(joinLobbyCommand);
|
||||
this.getCommand("spjoin").setTabCompleter(joinLobbyCommand);
|
||||
final VoteCommand voteCommand = new VoteCommand();
|
||||
this.getCommand("vote").setExecutor(voteCommand);
|
||||
this.getCommand("vote").setTabCompleter(new VoteCompleter());
|
||||
// Claims /v and /vote inside Splegg territory before VotingPlugin sees them.
|
||||
this.getServer().getPluginManager().registerEvents(new VoteCommandListener(voteCommand), this);
|
||||
this.registerChatFormatter();
|
||||
|
||||
}
|
||||
|
||||
@@ -341,6 +354,116 @@ public class SpleggOG extends JavaPlugin {
|
||||
|
||||
}
|
||||
|
||||
// Chat-OG routes a world to a game by the letter prefix of its name, which is
|
||||
// the
|
||||
// same prefix Splegg builds per-game worlds from, so one registration covers
|
||||
// both
|
||||
// the configured lobby worlds and every match copy.
|
||||
private void registerChatFormatter() {
|
||||
|
||||
if (this.getServer().getPluginManager().getPlugin("Chat-OG") == null) {
|
||||
|
||||
this.getLogger().info("Chat-OG is not installed, so Splegg chat keeps its default formatting.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
final String prefix = this.getGameWorldPrefix();
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
|
||||
this.getLogger().warning("Worlds.GamePrefix is empty, so no Splegg chat formatter was registered.");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (!ChatOGAPI.setFormatter(prefix, new SpleggChatFormatter())) {
|
||||
|
||||
this.getLogger().warning("Chat-OG was not ready, so no chat formatter was registered for " + prefix + ".");
|
||||
|
||||
}
|
||||
|
||||
this.warnAboutUnroutedWorlds(prefix);
|
||||
|
||||
}
|
||||
|
||||
// Chat-OG only routes a world whose name is <letters><digits>-<rest>, so a
|
||||
// lobby
|
||||
// world named anything else silently stays in global chat while Splegg still
|
||||
// treats it as its own territory.
|
||||
//
|
||||
// Only Worlds.Lobby is checked. Worlds.InGame holds match templates that are
|
||||
// copied per game, and prepareWorld names each copy
|
||||
// <prefix><gameId>-<template>,
|
||||
// which routes regardless of what the template is called. Nobody queues or
|
||||
// plays
|
||||
// in a template, so warning about one would be a false alarm -- and renaming it
|
||||
// to satisfy the pattern would only make the copies read SP1-SP1-Loss.
|
||||
private void warnAboutUnroutedWorlds(String prefix) {
|
||||
|
||||
final List<String> unrouted = new ArrayList<>();
|
||||
for (String worldName : this.getLobbyWorlds()) {
|
||||
|
||||
if (!isProtectedMainWorld(worldName) && !isChatOGRoutable(worldName)) {
|
||||
|
||||
unrouted.add(worldName);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (unrouted.isEmpty()) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
this.getLogger()
|
||||
.warning("These Splegg lobby worlds are not named <" + prefix
|
||||
+ "><number>-<name>, so Chat-OG leaves them in global chat while Splegg still treats them as "
|
||||
+ "its own: " + String.join(", ", unrouted) + ".");
|
||||
|
||||
}
|
||||
|
||||
// Mirrors Chat-OG's own parse: letters, then at least one digit, then '-',
|
||||
// then a non-empty remainder.
|
||||
private static boolean isChatOGRoutable(String worldName) {
|
||||
|
||||
if (worldName == null) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
while (index < worldName.length() && Character.isLetter(worldName.charAt(index))) {
|
||||
|
||||
index++;
|
||||
|
||||
}
|
||||
|
||||
if (index == 0) {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
final int digitStart = index;
|
||||
while (index < worldName.length() && Character.isDigit(worldName.charAt(index))) {
|
||||
|
||||
index++;
|
||||
|
||||
}
|
||||
|
||||
if (index == digitStart || index >= worldName.length() || worldName.charAt(index) != '-') {
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
return index + 1 < worldName.length();
|
||||
|
||||
}
|
||||
|
||||
public List<String> getLobbyWorlds() {
|
||||
|
||||
return this.getConfig().getStringList("Worlds.Lobby");
|
||||
|
||||
@@ -815,7 +815,9 @@ public class Game {
|
||||
|
||||
public Location getQueueLobbyLocation() {
|
||||
|
||||
if (this.map.lobbySet()) {
|
||||
// A lobby surveyed in another world would be rebased into this match's world
|
||||
// copy, dropping the player at those coordinates in unrelated terrain.
|
||||
if (this.map.lobbySet() && this.map.isLobbyInMapWorld()) {
|
||||
|
||||
final Location mapLobby = this.map.getLobbyIn(this.gameWorld);
|
||||
if (mapLobby != null && mapLobby.getWorld() != null) {
|
||||
@@ -936,6 +938,37 @@ public class Game {
|
||||
|
||||
}
|
||||
|
||||
// Restarts the lobby countdown ignoring Options.AutoStartPlayers. The floor is
|
||||
// EndVotingAt so map voting still resolves before the game starts.
|
||||
// Returns the countdown actually used, or -1 when the game is not in a lobby.
|
||||
public int forceStartCountdown(int seconds) {
|
||||
|
||||
if (this.status != Status.LOBBY) {
|
||||
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().cancelTask(counter);
|
||||
|
||||
this.lobbycount = Math.max(seconds, getEndVotingAt());
|
||||
this.setStarting(true);
|
||||
|
||||
final Iterator<?> playersInGame = this.players.values().iterator();
|
||||
while (playersInGame.hasNext()) {
|
||||
|
||||
final SpleggPlayer sp = (SpleggPlayer) playersInGame.next();
|
||||
sp.getPlayer().setLevel(this.getLobbyCount());
|
||||
|
||||
}
|
||||
|
||||
counter = Bukkit.getScheduler().scheduleSyncRepeatingTask(this.splegg,
|
||||
new LobbyCountdown(splegg, this, this.getLobbyCount(), true), 0L, 20L);
|
||||
|
||||
return this.lobbycount;
|
||||
|
||||
}
|
||||
|
||||
public void leaveGame(UtilPlayer u) {
|
||||
|
||||
final Player player = u.getPlayer();
|
||||
|
||||
@@ -160,6 +160,51 @@ public class GameUtilities {
|
||||
|
||||
}
|
||||
|
||||
// Lobby ids are <Worlds.GamePrefix><gameId>, the same shape as the per-game
|
||||
// world names, so SP1 names the lobby whose world is SP1-<map>.
|
||||
public String getLobbyId(Game game) {
|
||||
|
||||
if (game == null)
|
||||
return null;
|
||||
return SpleggOG.getPlugin().getGameWorldPrefix() + game.getGameId();
|
||||
|
||||
}
|
||||
|
||||
// Accepts SP1, sp1 or a bare 1. Map names are deliberately not resolvable.
|
||||
public Game resolveLobby(String input) {
|
||||
|
||||
if (input == null)
|
||||
return null;
|
||||
|
||||
String query = input.trim();
|
||||
if (query.isEmpty())
|
||||
return null;
|
||||
|
||||
final String prefix = SpleggOG.getPlugin().getGameWorldPrefix();
|
||||
if (!prefix.isEmpty() && query.regionMatches(true, 0, prefix, 0, prefix.length()))
|
||||
query = query.substring(prefix.length());
|
||||
|
||||
if (query.isEmpty() || !query.chars().allMatch(Character::isDigit))
|
||||
return null;
|
||||
|
||||
for (Game g : GAMES.values())
|
||||
if (query.equals(g.getGameId()))
|
||||
return g;
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public List<String> getLobbyIds() {
|
||||
|
||||
final List<String> ids = new ArrayList<>();
|
||||
for (Game g : GAMES.values())
|
||||
ids.add(getLobbyId(g));
|
||||
Collections.sort(ids);
|
||||
return ids;
|
||||
|
||||
}
|
||||
|
||||
public int howManyOpenGames() {
|
||||
|
||||
int n = 0;
|
||||
|
||||
@@ -14,11 +14,20 @@ public class LobbyCountdown implements Runnable {
|
||||
|
||||
int lobbycount;
|
||||
Game game;
|
||||
// Set by /spforcestart so the countdown ignores Options.AutoStartPlayers.
|
||||
boolean ignorePlayerCount;
|
||||
|
||||
public LobbyCountdown(SpleggOG splegg, Game game, int lobbycount) {
|
||||
|
||||
this(splegg, game, lobbycount, false);
|
||||
|
||||
}
|
||||
|
||||
public LobbyCountdown(SpleggOG splegg, Game game, int lobbycount, boolean ignorePlayerCount) {
|
||||
|
||||
this.game = game;
|
||||
this.lobbycount = lobbycount;
|
||||
this.ignorePlayerCount = ignorePlayerCount;
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +86,7 @@ public class LobbyCountdown implements Runnable {
|
||||
|
||||
}
|
||||
|
||||
} else if ((this.game.getPlayers().size()) >= 2) {
|
||||
} else if ((this.game.getPlayers().size()) >= (this.ignorePlayerCount ? 1 : 2)) {
|
||||
|
||||
Bukkit.getScheduler().cancelTask(this.game.getCounterID());
|
||||
SpleggOG.getPlugin().game.startGame(this.game);
|
||||
|
||||
@@ -4,7 +4,7 @@ version: '${version}'
|
||||
authors: [Hraponssi, MrLuangamer, Theluangamer9416, NotAlexNoyle]
|
||||
api-version: '${apiVersion}'
|
||||
depend: [DiamondBank-OG, Essentials-OG, WorldEdit, MyWorlds]
|
||||
softdepend: [My_Worlds]
|
||||
softdepend: [My_Worlds, Chat-OG]
|
||||
|
||||
commands:
|
||||
splegg:
|
||||
@@ -16,6 +16,14 @@ commands:
|
||||
description: Vote for a Splegg map.
|
||||
usage: /<command> <map number>
|
||||
aliases: [v]
|
||||
spjoin:
|
||||
description: Join a Splegg lobby by its id.
|
||||
usage: /<command> [lobby]
|
||||
spforcestart:
|
||||
description: Start the Splegg game even if the player minimum is not met.
|
||||
permission: splegg.admin
|
||||
usage: /<command> [time]
|
||||
|
||||
|
||||
permissions:
|
||||
splegg.use:
|
||||
|
||||
Reference in new issue
Block a user