mirror of
https://github.com/WiIIiam278/Velocitab.git
synced 2025-03-13 13:30:01 +01:00
1.7.4 - Bug fixes and performance improvements (#248)
* Incremented join delay * Refactored the code to cache placeholders and then use them in sync * More cleanup * Sorting improvements * Improvements on server switch * Increased delay * Fixed problem with header & footer task Improved placeholder cache * Fixed some problems Not yet stable * Applied 250ms delay to tab group quit logic Reworked disconnect handling * Fixed problem, more stable * Removed debug messages * Recoded task manager Fixed possible synchronization problem Added possibility of using multiple tabgroups files * Fixes * Improvements * Removed mini expansion Changed placeholder internal logic * General improvements * Performance improvements Added not relational conditional placeholders Fixed problems with displayname * Fixed problem * Improved code style Removed useless lines * Optimized imports * Code cleanup * Code cleanup * Fixed possible when using relational placeholders * Added Toilet support * Improved toilet implementation * Added new DebugSystem removed old debug * Changed labels * Removed useless dependencies Bumped toilet * Changed from GeneralUtil to StringUtil * Added Locales * Fixed style * Reverted #238 * Added docs Removed locales * Removed confirm sub command from docs * remove comments * Fixed FileInclusionRule sorting * Fixed rowspan ---------
This commit is contained in:
parent
4920b738a0
commit
4191f13624
17
build.gradle
17
build.gradle
@ -50,8 +50,9 @@ dependencies {
|
||||
implementation 'net.william278:minedown:1.8.2'
|
||||
implementation 'org.bstats:bstats-velocity:3.1.0'
|
||||
implementation 'de.exlll:configlib-yaml:4.5.0'
|
||||
implementation 'org.apache.commons:commons-jexl3:3.4.0'
|
||||
implementation 'org.mvel:mvel2:2.5.2.Final'
|
||||
implementation 'net.jodah:expiringmap:0.5.11'
|
||||
implementation 'net.william278.toilet:toilet-velocity:1.0.12'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
}
|
||||
@ -88,24 +89,26 @@ java {
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
dependencies {
|
||||
exclude dependency(':slf4j-api')
|
||||
exclude dependency('org.json:json')
|
||||
exclude dependency('org.apache.commons:commons-lang3')
|
||||
}
|
||||
|
||||
relocate 'org.apache.commons.text', 'net.william278.velocitab.libraries.commons.text'
|
||||
relocate 'org.apache.commons.lang3', 'net.william278.velocitab.libraries.commons.lang3'
|
||||
relocate 'org.jetbrains', 'net.william278.velocitab.libraries'
|
||||
relocate 'org.intellij', 'net.william278.velocitab.libraries'
|
||||
relocate 'de.themoep', 'net.william278.velocitab.libraries'
|
||||
relocate 'net.william278.annotaml', 'net.william278.velocitab.libraries.annotaml'
|
||||
relocate 'net.william278.desertwell', 'net.william278.velocitab.libraries.desertwell'
|
||||
relocate 'net.william278.toilet', 'net.william278.velocitab.libraries.toilet'
|
||||
relocate 'org.bstats', 'net.william278.velocitab.libraries.bstats'
|
||||
relocate 'de.exlll.configlib', 'net.william278.velocitab.libraries.configlib'
|
||||
relocate 'org.snakeyaml', 'net.william278.velocitab.libraries.snakeyaml'
|
||||
relocate 'org.apache.commons.jexl3', 'net.william278.velocitab.libraries.commons.jexl3'
|
||||
relocate 'org.apache.commons.logging', 'net.william278.velocitab.libraries.commons.logging'
|
||||
relocate 'net.jodah.expiringmap', 'net.william278.velocitab.libraries.expiringmap'
|
||||
|
||||
dependencies {
|
||||
exclude dependency(':slf4j-api')
|
||||
exclude dependency('org.json:json')
|
||||
}
|
||||
relocate 'org.mvel2', 'net.william278.velocitab.libraries.mvel2'
|
||||
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
|
@ -11,7 +11,7 @@ This page contains the usage table and associated permissions for the `/velocita
|
||||
<tbody>
|
||||
<!-- /velocitab command -->
|
||||
<tr>
|
||||
<td rowspan="5"><code>/velocitab</code></td>
|
||||
<td rowspan="7"><code>/velocitab</code></td>
|
||||
<td><code>/velocitab</code></td>
|
||||
<td>View & manage plugin system information</td>
|
||||
<td><code>velocitab.command</code></td>
|
||||
@ -36,5 +36,15 @@ This page contains the usage table and associated permissions for the `/velocita
|
||||
<td>Change or reset your TAB display name</td>
|
||||
<td><code>velocitab.command.name</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/velocitab dump</code></td>
|
||||
<td>Generate a debug dump of the plugin state</td>
|
||||
<td><code>velocitab.command.dump</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/velocitab debug tablist <player></code></td>
|
||||
<td>Debug the TAB list for a specific player</td>
|
||||
<td><code>velocitab.command.debug</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
|
@ -1,14 +1,34 @@
|
||||
In order to use these placeholders, install MiniPlaceholders on your Velocity proxy, set the `formatter_type`
|
||||
to `MINIMESSAGE`, and ensure `enable_miniplaceholders_hook` is set to `true`.
|
||||
|
||||
Conditional placeholders allow you to display different values based on certain conditions. The format
|
||||
is `<velocitab_rel_condition|<condition>|<true>|<false>>`.
|
||||
is `<velocitab_condition|<condition>|<true>|<false>>` and the relational format is `<velocitab_rel_condition|<condition>|<true>|<false>>`.
|
||||
|
||||
Currently, this system is only available for the `format` and `nametag` fields in the tab groups configuration.
|
||||
|
||||
**Note:** The difference between the two is that relational placeholders are evaluated from the viewer's perspective, while the conditional placeholders are evaluated from the player's perspective.
|
||||
So if you have 200 players, if you use a conditional placeholder, the placeholder will be evaluated 200 times, while if you use a relational placeholder, it will be evaluated 200*200 = 40000 times.
|
||||
Using relational placeholders could be really slow, so it is recommended to not use them unless you need them.
|
||||
|
||||
## Table of Conditional Placeholders
|
||||
|
||||
| Placeholder Example | Description | Example Output |
|
||||
| Relational Placeholder Example | Description | Example Output |
|
||||
|----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| `<velocitab_condition:%vault_eco_balance% >= 10:rich:poor>` | Checks if the player's vault balance is greater than or equal to 10. If true, displays "rich", else "poor". | `rich` or `poor` |
|
||||
| `<velocitab_condition:%player_health% < 10:Low Health:Healthy>` | Checks if the player's health is below 10. If true, displays "Low Health", else "Healthy". | `Low Health` or `Healthy` |
|
||||
| `<velocitab_condition:%player_ping% <= 50:Good Ping:Bad Ping>` | Checks if the player's ping is 50 or below. If true, displays "Good Ping", else "Bad Ping". | `Good Ping` or `Bad Ping` |
|
||||
| `<velocitab_condition:%player_level% >= 30:High Level:Low Level>` | Checks if the player's level is 30 or above. If true, displays "High Level", else "Low Level". | `High Level` or `Low Level` |
|
||||
| `<velocitab_condition:%player_exp% >= 1000:XP Master:XP Novice>` | Checks if the player has 1000 or more experience points. If true, displays "XP Master", else "XP Novice". | `XP Master` or `XP Novice` |
|
||||
| `<velocitab_condition:"%player_name%" == ''AlexDev_'' OR "%player_name%" == ''William278_'':VelocitabDev:>` | Checks if the player's name is either "AlexDev_" or "William278". If true, displays "Developer", else "NotDev". | `Developer` or `NotDev` |
|
||||
| `<velocitab_condition:"%player_gamemode%" == ''CREATIVE'':Creative Mode:Not Creative Mode>` | Checks if the player is in creative mode. If true, displays "Creative Mode", else "Not Creative Mode". | `Creative Mode` or `Not Creative Mode` |
|
||||
| `<velocitab_condition:"%player_world%" == ''nether'':In Nether:Not in Nether>` | Checks if the player is in the Nether. If true, displays "In Nether", else "Not in Nether". | `In Nether` or `Not in Nether` |
|
||||
| `<velocitab_condition:"%player_biome%" == "DESERT":In Desert:Not in Desert>` | Checks if the player is in a desert biome. If true, displays "In Desert", else "Not in Desert". | `In Desert` or `Not in Desert` |
|
||||
| `<velocitab_condition:''%player_gamemode%''.contains(''S''):Survival or Spectator:Not Survival or Spectator> ` | Checks if the player is in survival or spectator mode. If true, displays "Survival or Spectator", else "Not Survival or Spectator". | `Survival or Spectator` or `Not Survival or Spectator` |
|
||||
| `<velocitab_condition:%player_health% == %target_player_health%:Same health:Not same health> ` | Checks if the player's health is the same as the target player's health. If true, displays "Same health", else "Not same health". | `Same health` or `Not same health` |
|
||||
|
||||
|
||||
## Table of Conditional Relational Placeholders
|
||||
|
||||
**Note:** In order to use relational placeholders you need to give a player the permission `velocitab.relational`. For performance reasons, this permission is not given by default.
|
||||
|
||||
| Relational Placeholder Example | Description | Example Output |
|
||||
|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| `<velocitab_rel_condition:%vault_eco_balance% >= 10:rich:poor>` | Checks if the player's vault balance is greater than or equal to 10. If true, displays "rich", else "poor". | `rich` or `poor` |
|
||||
| `<velocitab_rel_condition:%player_health% < 10:Low Health:Healthy>` | Checks if the player's health is below 10. If true, displays "Low Health", else "Healthy". | `Low Health` or `Healthy` |
|
||||
@ -16,8 +36,6 @@ Currently, this system is only available for the `format` and `nametag` fields i
|
||||
| `<velocitab_rel_condition:%player_level% >= 30:High Level:Low Level>` | Checks if the player's level is 30 or above. If true, displays "High Level", else "Low Level". | `High Level` or `Low Level` |
|
||||
| `<velocitab_rel_condition:%player_exp% >= 1000:XP Master:XP Novice>` | Checks if the player has 1000 or more experience points. If true, displays "XP Master", else "XP Novice". | `XP Master` or `XP Novice` |
|
||||
| `<velocitab_rel_condition:"%player_name%" == ''AlexDev_'' OR "%player_name%" == ''William278_'':VelocitabDev:>` | Checks if the player's name is either "AlexDev_" or "William278". If true, displays "Developer", else "NotDev". | `Developer` or `NotDev` |
|
||||
| `<velocitab_rel_condition:startsWith(''%player_name%'', ''AlexDe''):IsAlex:NotAlex>` | Checks if the player's name starts with "AlexDe". If true, displays "IsAlex", else "NotAlex". | `IsAlex` or `NotAlex` |
|
||||
| `<velocitab_rel_condition:endsWith(''%player_name%'', ''278''):EndsWith278:DoesNotEndWith278>` | Checks if the player's name ends with "278". If true, displays "EndsWith278", else "DoesNotEndWith278". | `EndsWith278` or `DoesNotEndWith278` |
|
||||
| `<velocitab_rel_condition:"%player_gamemode%" == ''CREATIVE'':Creative Mode:Not Creative Mode>` | Checks if the player is in creative mode. If true, displays "Creative Mode", else "Not Creative Mode". | `Creative Mode` or `Not Creative Mode` |
|
||||
| `<velocitab_rel_condition:"%player_world%" == ''nether'':In Nether:Not in Nether>` | Checks if the player is in the Nether. If true, displays "In Nether", else "Not in Nether". | `In Nether` or `Not in Nether` |
|
||||
| `<velocitab_rel_condition:"%player_biome%" == "DESERT":In Desert:Not in Desert>` | Checks if the player is in a desert biome. If true, displays "In Desert", else "Not in Desert". | `In Desert` or `Not in Desert` |
|
||||
|
@ -6,6 +6,9 @@ the server a player is on. You can also set formatting to use for [[Nametags]] a
|
||||
|
||||
Groups are defined in `tab_groups.yml`, as a list of TabGroup elements.
|
||||
|
||||
You can also add more tab groups by creating a folder called `tab_groups` in the `plugins/velocitab` folder, and adding
|
||||
a `other_tab_groups.yml` (you can use a custom name, it needs to be .yml) file to it.
|
||||
|
||||
Every group must have a unique name, and a list of servers to include in the group. You can also define a list of
|
||||
sorting placeholders to use when sorting players in the TAB list, and a header/footer update rate and placeholder update
|
||||
rate to use for the group.
|
||||
@ -123,6 +126,34 @@ information.
|
||||
You can define a header/footer update rate to use for each group, in milliseconds. This will determine how quickly the
|
||||
headers and footers will cycle through in the TAB list. The default is 1000 milliseconds (1 second).
|
||||
|
||||
## Format update rate
|
||||
|
||||
<details>
|
||||
<summary>Example of format update rate</summary>
|
||||
|
||||
```yaml
|
||||
format_update_rate: 1000
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
You can define a format update rate to use for each group, in milliseconds. This will determine how quickly the
|
||||
formats will update in the TAB list.
|
||||
|
||||
## Nametag update rate (sorting)
|
||||
|
||||
<details>
|
||||
<summary>Example of nametag update rate</summary>
|
||||
|
||||
```yaml
|
||||
nametag_update_rate: 1000
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
You can define a nametag update rate to use for each group, in milliseconds. This will determine how quickly the
|
||||
nametags will update in the TAB list. This will also determine how quickly the sorting will update.
|
||||
|
||||
## Placeholder update rate
|
||||
|
||||
<details>
|
||||
@ -175,6 +206,8 @@ groups:
|
||||
- '%role_weight%'
|
||||
- '%username_lower%'
|
||||
header_footer_update_rate: 1000
|
||||
format_update_rate: 1000
|
||||
nametag_update_rate: 1000
|
||||
placeholder_update_rate: 1000
|
||||
- name: survival
|
||||
headers:
|
||||
@ -188,6 +221,8 @@ groups:
|
||||
- '%role_weight%'
|
||||
- '%username_lower%'
|
||||
header_footer_update_rate: 1000
|
||||
format_update_rate: 1000
|
||||
nametag_update_rate: 1000
|
||||
placeholder_update_rate: 1000
|
||||
```
|
||||
|
||||
|
@ -3,7 +3,7 @@ javaVersion=17
|
||||
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
|
||||
plugin_version=1.7.3
|
||||
plugin_version=1.7.4
|
||||
plugin_archive=velocitab
|
||||
plugin_description=A beautiful and versatile TAB list plugin for Velocity proxies
|
||||
|
||||
|
@ -33,24 +33,20 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.toilet.Toilet;
|
||||
import net.william278.velocitab.api.PluginMessageAPI;
|
||||
import net.william278.velocitab.api.VelocitabAPI;
|
||||
import net.william278.velocitab.commands.VelocitabCommand;
|
||||
import net.william278.velocitab.config.ConfigProvider;
|
||||
import net.william278.velocitab.config.Formatter;
|
||||
import net.william278.velocitab.config.Settings;
|
||||
import net.william278.velocitab.config.TabGroups;
|
||||
import net.william278.velocitab.config.*;
|
||||
import net.william278.velocitab.hook.Hook;
|
||||
import net.william278.velocitab.hook.LuckPermsHook;
|
||||
import net.william278.velocitab.hook.MiniPlaceholdersHook;
|
||||
import net.william278.velocitab.packet.PacketEventManager;
|
||||
import net.william278.velocitab.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.providers.HookProvider;
|
||||
import net.william278.velocitab.providers.LoggerProvider;
|
||||
import net.william278.velocitab.providers.MetricProvider;
|
||||
import net.william278.velocitab.providers.ScoreboardProvider;
|
||||
import net.william278.velocitab.placeholder.PlaceholderManager;
|
||||
import net.william278.velocitab.providers.*;
|
||||
import net.william278.velocitab.sorting.SortingManager;
|
||||
import net.william278.velocitab.tab.PlayerTabList;
|
||||
import net.william278.velocitab.util.DebugSystem;
|
||||
import net.william278.velocitab.vanish.VanishManager;
|
||||
import org.bstats.velocity.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -62,12 +58,12 @@ import java.util.List;
|
||||
|
||||
@Plugin(id = "velocitab")
|
||||
@Getter
|
||||
public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProvider, HookProvider, MetricProvider {
|
||||
public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProvider, HookProvider, MetricProvider, DumpProvider {
|
||||
|
||||
@Setter
|
||||
private Settings settings;
|
||||
@Setter
|
||||
private TabGroups tabGroups;
|
||||
private TabGroupsManager tabGroupsManager;
|
||||
|
||||
private final ProxyServer server;
|
||||
private final Logger logger;
|
||||
@ -87,6 +83,9 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
private VanishManager vanishManager;
|
||||
private PacketEventManager packetEventManager;
|
||||
private PluginMessageAPI pluginMessageAPI;
|
||||
private PlaceholderManager placeholderManager;
|
||||
@Setter
|
||||
private Toilet toilet;
|
||||
|
||||
@Inject
|
||||
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
|
||||
@ -100,13 +99,16 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
checkCompatibility();
|
||||
loadConfigs();
|
||||
loadHooks();
|
||||
preparePlaceholderManager();
|
||||
prepareVanishManager();
|
||||
prepareChannelManager();
|
||||
prepareScoreboard();
|
||||
registerCommands();
|
||||
registerMetrics();
|
||||
checkForUpdates();
|
||||
prepareAPI();
|
||||
prepareChannelManager();
|
||||
initializeToilet();
|
||||
DebugSystem.initializeTask(this);
|
||||
logger.info("Successfully enabled Velocitab");
|
||||
}
|
||||
|
||||
@ -114,7 +116,6 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
public void onProxyShutdown(@NotNull ProxyShutdownEvent event) {
|
||||
disableScoreboardManager();
|
||||
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
|
||||
getMiniPlaceholdersHook().ifPresent(MiniPlaceholdersHook::unregisterExpansion);
|
||||
unregisterAPI();
|
||||
logger.info("Successfully disabled Velocitab");
|
||||
}
|
||||
@ -137,6 +138,10 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
|
||||
this.packetEventManager = new PacketEventManager(this);
|
||||
}
|
||||
|
||||
private void preparePlaceholderManager() {
|
||||
this.placeholderManager = new PlaceholderManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Velocitab getPlugin() {
|
||||
|
@ -29,6 +29,7 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -110,7 +111,7 @@ public class VelocitabAPI {
|
||||
public void setCustomPlayerName(@NotNull Player player, @Nullable String name) {
|
||||
getUser(player).ifPresent(tabPlayer -> {
|
||||
tabPlayer.setCustomName(name);
|
||||
plugin.getTabList().updatePlayerDisplayName(tabPlayer);
|
||||
plugin.getTabList().updateDisplayName(tabPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
@ -199,7 +200,7 @@ public class VelocitabAPI {
|
||||
*/
|
||||
@NotNull
|
||||
public List<Group> getServerGroups() {
|
||||
return plugin.getTabGroups().getGroups();
|
||||
return new ArrayList<>(plugin.getTabGroupsManager().getGroups());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -211,11 +212,11 @@ public class VelocitabAPI {
|
||||
*/
|
||||
@NotNull
|
||||
public Optional<Group> getGroup(@NotNull String name) {
|
||||
return plugin.getTabGroups().getGroup(name);
|
||||
return plugin.getTabGroupsManager().getGroup(name);
|
||||
}
|
||||
|
||||
public Optional<Group> getGroupFromServer(@NotNull String server) {
|
||||
return plugin.getTabGroups().getGroupFromServer(server, plugin);
|
||||
return plugin.getTabGroupsManager().getGroupFromServer(server, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,19 +27,35 @@ import com.velocitypowered.api.command.BrigadierCommand;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import net.william278.desertwell.about.AboutMenu;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class VelocitabCommand {
|
||||
|
||||
private static final TextColor MAIN_COLOR = TextColor.color(0x00FB9A);
|
||||
private static final TextColor ERROR_COLOR = TextColor.color(0xFF7E5E);
|
||||
|
||||
private static final String systemDumpConfirm = """
|
||||
<color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| Prepare a system dump? This will include:</color>
|
||||
<gray>• Your latest server logs and Velocitab config files</gray>
|
||||
<gray>• Current plugin system status information</gray>
|
||||
<gray>• Information about your Java & Minecraft server environment</gray>
|
||||
<gray>• A list of other currently installed plugins</gray>
|
||||
<click:run_command:/velocitab dump confirm><hover:show_text:'<gray>Click to prepare dump'><color:#00fb9a>To confirm click here or use: <italic>/velocitab dump confirm</italic></color></click>
|
||||
""";
|
||||
private static final String systemDumpStarted = "<color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| Preparing system status dump, please wait…</color>";
|
||||
private static final String systemDumpReady = "<click:open_url:%url%><color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| System status dump prepared! Click here to view</color></click>";
|
||||
private static final String systemDumpReadyConsole = "<color:#00fb9a><bold>Velocitab</bold></color> <color:#00fb9a>| System status dump prepared! Url: %url%</color>";
|
||||
|
||||
private final AboutMenu aboutMenu;
|
||||
private final Velocitab plugin;
|
||||
|
||||
@ -93,14 +109,13 @@ public final class VelocitabCommand {
|
||||
}
|
||||
|
||||
tabPlayer.get().setCustomName(name);
|
||||
plugin.getTabList().updatePlayerDisplayName(tabPlayer.get());
|
||||
plugin.getTabList().updateDisplayName(tabPlayer.get());
|
||||
|
||||
ctx.getSource().sendMessage(Component
|
||||
.text("Your TAB name has been updated!", MAIN_COLOR));
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
.requires(src -> src instanceof Player)
|
||||
.executes(ctx -> {
|
||||
final Player player = (Player) ctx.getSource();
|
||||
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(player);
|
||||
@ -119,7 +134,7 @@ public final class VelocitabCommand {
|
||||
}
|
||||
|
||||
tabPlayer.get().setCustomName(null);
|
||||
plugin.getTabList().updatePlayerDisplayName(tabPlayer.get());
|
||||
plugin.getTabList().updateDisplayName(tabPlayer.get());
|
||||
player.sendMessage(Component.text("Your name has been reset!", MAIN_COLOR));
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
@ -134,6 +149,61 @@ public final class VelocitabCommand {
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("debug")
|
||||
.requires(src -> hasPermission(src, "debug"))
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("tablist")
|
||||
.then(RequiredArgumentBuilder.<CommandSource, String>argument("player", StringArgumentType.string())
|
||||
.suggests((ctx, builder1) -> {
|
||||
final String input = ctx.getInput();
|
||||
if (input.isEmpty()) {
|
||||
return builder1.buildFuture();
|
||||
}
|
||||
plugin.getServer().getAllPlayers().stream()
|
||||
.map(Player::getUsername)
|
||||
.filter(s -> s.toLowerCase().startsWith(input.toLowerCase()))
|
||||
.forEach(builder1::suggest);
|
||||
return builder1.buildFuture();
|
||||
})
|
||||
.executes(ctx -> {
|
||||
final String input = ctx.getArgument("player", String.class);
|
||||
final Optional<Player> player = plugin.getServer().getPlayer(input);
|
||||
if (player.isEmpty()) {
|
||||
ctx.getSource().sendMessage(Component.text("Player not found!", ERROR_COLOR));
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}
|
||||
|
||||
player.get().getTabList().getEntries().forEach(entry -> {
|
||||
final String name = entry.getProfile().getName();
|
||||
final UUID uuid = entry.getProfile().getId();
|
||||
final String unformattedDisplayName = entry.getDisplayNameComponent().map(c -> PlainTextComponentSerializer.plainText().serialize(c)).orElse("empty");
|
||||
|
||||
ctx.getSource().sendMessage(Component.text("Name: %s, UUID: %s, Unformatted display name: %s".formatted(name, uuid, unformattedDisplayName)));
|
||||
});
|
||||
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
))
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("dump")
|
||||
.requires(src -> hasPermission(src, "dump"))
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().sendRichMessage(systemDumpConfirm.trim());
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("confirm")
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().sendRichMessage(systemDumpStarted);
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
final String dumpUrl = plugin.createDump(ctx.getSource());
|
||||
final Component component = MiniMessage.miniMessage().deserialize((ctx.getSource() instanceof Player
|
||||
? systemDumpReady
|
||||
: systemDumpReadyConsole)
|
||||
.replace("%url%", dumpUrl));
|
||||
ctx.getSource().sendMessage(component);
|
||||
}).schedule();
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
))
|
||||
.then(LiteralArgumentBuilder.<CommandSource>literal("update")
|
||||
.requires(src -> hasPermission(src, "update"))
|
||||
.executes(ctx -> {
|
||||
|
@ -82,7 +82,7 @@ public interface ConfigProvider {
|
||||
Settings.class,
|
||||
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
|
||||
));
|
||||
getSettings().validateConfig(getPlugin());
|
||||
getSettings().validateConfig(getPlugin(), "config.yml");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,15 +92,15 @@ public interface ConfigProvider {
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
TabGroups getTabGroups();
|
||||
TabGroupsManager getTabGroupsManager();
|
||||
|
||||
/**
|
||||
* Set the tab groups
|
||||
*
|
||||
* @param tabGroups The tab groups to set
|
||||
* @since 1.0
|
||||
* @param tabGroupsManager The tab groups to set
|
||||
* @since 1.7.4
|
||||
*/
|
||||
void setTabGroups(@NotNull TabGroups tabGroups);
|
||||
void setTabGroupsManager(@NotNull TabGroupsManager tabGroupsManager);
|
||||
|
||||
/**
|
||||
* Load the tab groups from the config file
|
||||
@ -108,12 +108,8 @@ public interface ConfigProvider {
|
||||
* @since 1.0
|
||||
*/
|
||||
default void loadTabGroups() {
|
||||
setTabGroups(YamlConfigurations.update(
|
||||
getConfigDirectory().resolve("tab_groups.yml"),
|
||||
TabGroups.class,
|
||||
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
|
||||
));
|
||||
getTabGroups().validateConfig(getPlugin());
|
||||
setTabGroupsManager(new TabGroupsManager(getPlugin()));
|
||||
getTabGroupsManager().loadGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,23 +166,6 @@ public interface ConfigProvider {
|
||||
@NotNull
|
||||
Version getVelocityVersion();
|
||||
|
||||
/**
|
||||
* Saves the tab groups to the "tab_groups.yml" config file.
|
||||
* Uses the YamlConfigurations#save method to write the tab groups object to the specified config file path.
|
||||
* This method assumes that the getConfigDirectory method returns a valid directory path.
|
||||
*
|
||||
* @throws IllegalStateException if the getConfigDirectory method returns null
|
||||
* @since 1.0
|
||||
*/
|
||||
default void saveTabGroups() {
|
||||
YamlConfigurations.save(
|
||||
getConfigDirectory().resolve("tab_groups.yml"),
|
||||
TabGroups.class,
|
||||
getTabGroups(),
|
||||
YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin config directory
|
||||
*
|
||||
|
@ -28,6 +28,6 @@ public interface ConfigValidator {
|
||||
* Validates the configuration settings.
|
||||
* @throws IllegalStateException if the configuration is invalid
|
||||
*/
|
||||
void validateConfig(@NotNull Velocitab plugin) throws IllegalStateException;
|
||||
void validateConfig(@NotNull Velocitab plugin, @NotNull String name) throws IllegalStateException;
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.util.MiniMessageUtil;
|
||||
import net.william278.velocitab.util.QuadFunction;
|
||||
import net.william278.velocitab.util.SerializationUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -49,8 +50,9 @@ public enum Formatter {
|
||||
MINIMESSAGE(
|
||||
(text, player, viewer, plugin) -> plugin.getMiniPlaceholdersHook()
|
||||
.filter(hook -> player != null)
|
||||
.map(hook -> hook.format(text, player.getPlayer(), viewer == null ? null : viewer.getPlayer()))
|
||||
.orElse(MiniMessage.miniMessage().deserialize(text)),
|
||||
.map(hook -> hook.format(MiniMessageUtil.getINSTANCE().checkForErrors(text),
|
||||
player.getPlayer(), viewer == null ? null : viewer.getPlayer()))
|
||||
.orElse(MiniMessage.miniMessage().deserialize(MiniMessageUtil.getINSTANCE().checkForErrors(text))),
|
||||
(text) -> MiniMessage.miniMessage().escapeTags(text),
|
||||
"MiniMessage",
|
||||
(text) -> MiniMessage.miniMessage().deserialize(text),
|
||||
|
@ -19,13 +19,15 @@
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.placeholder.PlaceholderReplacement;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import net.william278.velocitab.util.StringUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.event.Level;
|
||||
@ -50,19 +52,21 @@ public record Group(
|
||||
Map<String, List<PlaceholderReplacement>> placeholderReplacements,
|
||||
boolean collisions,
|
||||
int headerFooterUpdateRate,
|
||||
int formatUpdateRate,
|
||||
int nametagUpdateRate,
|
||||
int placeholderUpdateRate,
|
||||
boolean onlyListPlayersInSameServer
|
||||
) {
|
||||
|
||||
@NotNull
|
||||
public String getHeader(int index) {
|
||||
return headers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(headers
|
||||
return headers.isEmpty() ? "" : StringUtil.unescapeJava(headers
|
||||
.get(Math.max(0, Math.min(index, headers.size() - 1))));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getFooter(int index) {
|
||||
return footers.isEmpty() ? "" : StringEscapeUtils.unescapeJava(footers
|
||||
return footers.isEmpty() ? "" : StringUtil.unescapeJava(footers
|
||||
.get(Math.max(0, Math.min(index, footers.size() - 1))));
|
||||
}
|
||||
|
||||
@ -82,6 +86,7 @@ public record Group(
|
||||
(isDefault(plugin) && plugin.getSettings().isFallbackEnabled())) {
|
||||
return Sets.newHashSet(plugin.getServer().getAllServers());
|
||||
}
|
||||
|
||||
return getRegexServers(plugin);
|
||||
}
|
||||
|
||||
@ -99,6 +104,7 @@ public record Group(
|
||||
plugin.getServer().getServer(server).ifPresent(totalServers::add);
|
||||
}
|
||||
}
|
||||
|
||||
return totalServers;
|
||||
}
|
||||
|
||||
@ -108,10 +114,21 @@ public record Group(
|
||||
|
||||
@NotNull
|
||||
public Set<Player> getPlayers(@NotNull Velocitab plugin) {
|
||||
Set<Player> players = Sets.newHashSet();
|
||||
final Set<Player> players = Sets.newHashSet();
|
||||
for (RegisteredServer server : registeredServers(plugin)) {
|
||||
players.addAll(server.getPlayersConnected());
|
||||
}
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<Player> getPlayersAsList(@NotNull Velocitab plugin) {
|
||||
final List<Player> players = Lists.newArrayList();
|
||||
for (RegisteredServer server : registeredServers(plugin)) {
|
||||
players.addAll(server.getPlayersConnected());
|
||||
}
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
@ -120,11 +137,13 @@ public record Group(
|
||||
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
|
||||
return Sets.newHashSet(plugin.getServer().getAllPlayers());
|
||||
}
|
||||
|
||||
if (onlyListPlayersInSameServer) {
|
||||
return tabPlayer.getPlayer().getCurrentServer()
|
||||
.map(s -> Sets.newHashSet(s.getServer().getPlayersConnected()))
|
||||
.orElseGet(Sets::newHashSet);
|
||||
}
|
||||
|
||||
return getPlayers(plugin);
|
||||
}
|
||||
|
||||
@ -138,35 +157,86 @@ public record Group(
|
||||
@NotNull
|
||||
public Set<TabPlayer> getTabPlayers(@NotNull Velocitab plugin) {
|
||||
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
|
||||
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
|
||||
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
return plugin.getTabList().getPlayers()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(tabPlayer -> tabPlayer.getGroup().equals(this))
|
||||
.filter(tabPlayer -> tabPlayer.isLoaded() && tabPlayer.getGroup().equals(this))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public List<TabPlayer> getTabPlayersAsList(@NotNull Velocitab plugin) {
|
||||
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
|
||||
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return plugin.getTabList().getPlayers()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(tabPlayer -> tabPlayer.isLoaded() && tabPlayer.getGroup().equals(this))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Set<TabPlayer> getTabPlayers(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
|
||||
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
|
||||
return Sets.newHashSet(plugin.getTabList().getPlayers().values());
|
||||
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
if (onlyListPlayersInSameServer) {
|
||||
return plugin.getTabList().getPlayers()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(player -> player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
|
||||
.filter(player -> player.isLoaded() && player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
return getTabPlayers(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<TabPlayer> getTabPlayersAsList(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) {
|
||||
if (plugin.getSettings().isShowAllPlayersFromAllGroups()) {
|
||||
return plugin.getTabList().getPlayers().values().stream().filter(TabPlayer::isLoaded).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (onlyListPlayersInSameServer) {
|
||||
return plugin.getTabList().getPlayers()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(player -> player.isLoaded() && player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return getTabPlayersAsList(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getTextsWithPlaceholders(@NotNull Velocitab plugin) {
|
||||
final List<String> texts = Lists.newArrayList();
|
||||
texts.add(name);
|
||||
texts.add(format);
|
||||
texts.addAll(headers);
|
||||
texts.addAll(footers);
|
||||
texts.add(nametag.prefix());
|
||||
texts.add(nametag.suffix());
|
||||
texts.addAll(sortingPlaceholders);
|
||||
|
||||
if (plugin.getLuckPermsHook().isEmpty()) {
|
||||
texts.add("%luckperms_meta_weight%");
|
||||
}
|
||||
|
||||
return texts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof Group group)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return name.equals(group.name);
|
||||
}
|
||||
}
|
||||
|
@ -1,331 +0,0 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.function.TriFunction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public enum Placeholder {
|
||||
|
||||
PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getPlayerCount())),
|
||||
MAX_PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getConfiguration().getShowMaxPlayers())),
|
||||
LOCAL_PLAYERS_ONLINE((plugin, player) -> player.getPlayer().getCurrentServer()
|
||||
.map(ServerConnection::getServer)
|
||||
.map(RegisteredServer::getPlayersConnected)
|
||||
.map(players -> Integer.toString(players.size()))
|
||||
.orElse("")),
|
||||
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return Integer.toString(player.getGroup().getPlayers(plugin).size());
|
||||
}
|
||||
return plugin.getTabGroups().getGroup(param)
|
||||
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
|
||||
.orElse("Group " + param + " not found");
|
||||
}),
|
||||
CURRENT_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
|
||||
CURRENT_DATE_WEEKDAY((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("EEEE").format(LocalDateTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofPattern("EEEE").withLocale(locale).format(LocalDateTime.now());
|
||||
}),
|
||||
CURRENT_DATE_MONTH((plugin, player) -> DateTimeFormatter.ofPattern("MM").format(LocalDateTime.now())),
|
||||
CURRENT_DATE_YEAR((plugin, player) -> DateTimeFormatter.ofPattern("yyyy").format(LocalDateTime.now())),
|
||||
CURRENT_DATE((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("dd/MM/yyyy").format(LocalDateTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(LocalDateTime.now());
|
||||
}),
|
||||
CURRENT_TIME_HOUR((plugin, player) -> DateTimeFormatter.ofPattern("HH").format(LocalDateTime.now())),
|
||||
CURRENT_TIME_MINUTE((plugin, player) -> DateTimeFormatter.ofPattern("mm").format(LocalDateTime.now())),
|
||||
CURRENT_TIME_SECOND((plugin, player) -> DateTimeFormatter.ofPattern("ss").format(LocalDateTime.now())),
|
||||
CURRENT_TIME((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).format(LocalTime.now());
|
||||
}),
|
||||
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
|
||||
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
|
||||
SERVER((plugin, player) -> player.getServerName()),
|
||||
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
|
||||
PREFIX((plugin, player) -> player.getRole().getPrefix()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_prefix%"))),
|
||||
SUFFIX((plugin, player) -> player.getRole().getSuffix()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_suffix%"))),
|
||||
ROLE((plugin, player) -> player.getRole().getName()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
|
||||
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
|
||||
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_weight%"))),
|
||||
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
|
||||
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
|
||||
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
|
||||
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
|
||||
.map(hook -> hook.getMeta(player.getPlayer(), param))
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
|
||||
|
||||
private final static Pattern VELOCITAB_PATTERN = Pattern.compile("<velocitab_.*?>");
|
||||
private final static Pattern TEST = Pattern.compile("<.*?>");
|
||||
private final static Pattern CONDITION_REPLACER = Pattern.compile("<velocitab_rel_condition:[^:]*:");
|
||||
private final static Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
|
||||
private final static String DELIMITER = ":::";
|
||||
private final static Map<String, String> SYMBOL_SUBSTITUTES = Map.of(
|
||||
"<", "*LESS*",
|
||||
">", "*GREATER*"
|
||||
);
|
||||
private final static Map<String, String> SYMBOL_SUBSTITUTES_2 = Map.of(
|
||||
"*LESS*", "*LESS2*",
|
||||
"*GREATER*", "*GREATER2*"
|
||||
);
|
||||
private final static String VEL_PLACEHOLDER = "<vel";
|
||||
private final static String VELOCITAB_PLACEHOLDER = "<velocitab_rel";
|
||||
private final static String ELSE_PLACEHOLDER = "ELSE";
|
||||
|
||||
/**
|
||||
* Function to replace placeholders with a real value
|
||||
*/
|
||||
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
|
||||
private final boolean parameterised;
|
||||
private final Pattern pattern;
|
||||
|
||||
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
|
||||
this.parameterised = false;
|
||||
this.replacer = (text, player, plugin) -> replacer.apply(player, plugin);
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "%");
|
||||
}
|
||||
|
||||
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
|
||||
this.parameterised = true;
|
||||
this.replacer = parameterisedReplacer;
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
public static CompletableFuture<Nametag> replace(@NotNull Nametag nametag, @NotNull Velocitab plugin,
|
||||
@NotNull TabPlayer player) {
|
||||
return replace(nametag.prefix() + DELIMITER + nametag.suffix(), plugin, player)
|
||||
.thenApply(s -> s.split(DELIMITER, 2))
|
||||
.thenApply(v -> new Nametag(v[0], v.length > 1 ? v[1] : ""));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getPlaceholderFallback(@NotNull Velocitab plugin, @NotNull String fallback) {
|
||||
if (plugin.getPAPIProxyBridgeHook().isPresent() && plugin.getSettings().isFallbackToPapiIfPlaceholderBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Pair<String, Map<String, String>> replaceInternal(@NotNull String format, @NotNull Velocitab plugin, @Nullable TabPlayer player) {
|
||||
format = processRelationalPlaceholders(format, plugin);
|
||||
return replacePlaceholders(format, plugin, player);
|
||||
}
|
||||
|
||||
private static String processRelationalPlaceholders(@NotNull String format, @NotNull Velocitab plugin) {
|
||||
if (plugin.getFormatter().equals(Formatter.MINIMESSAGE) && format.contains(VEL_PLACEHOLDER)) {
|
||||
final Matcher conditionReplacer = CONDITION_REPLACER.matcher(format);
|
||||
while (conditionReplacer.find()) {
|
||||
|
||||
final String search = conditionReplacer.group().split(":")[1];
|
||||
String condition = search;
|
||||
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE.entrySet()) {
|
||||
condition = condition.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
for (Map.Entry<String, String> entry : MiniConditionManager.REPLACE_2.entrySet()) {
|
||||
condition = condition.replace(entry.getValue(), entry.getKey());
|
||||
}
|
||||
format = format.replace(search, condition);
|
||||
}
|
||||
|
||||
final Matcher testMatcher = TEST.matcher(format);
|
||||
while (testMatcher.find()) {
|
||||
if (testMatcher.group().startsWith(VELOCITAB_PLACEHOLDER)) {
|
||||
final Matcher second = TEST.matcher(testMatcher.group().substring(1));
|
||||
while (second.find()) {
|
||||
String s = second.group();
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
|
||||
s = s.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
format = format.replace(second.group(), s);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String s = testMatcher.group();
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
|
||||
s = s.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
format = format.replace(testMatcher.group(), s);
|
||||
}
|
||||
|
||||
final Matcher velocitabRelationalMatcher = VELOCITAB_PATTERN.matcher(format);
|
||||
while (velocitabRelationalMatcher.find()) {
|
||||
final String relationalPlaceholder = velocitabRelationalMatcher.group().substring(1, velocitabRelationalMatcher.group().length() - 1);
|
||||
String fixedString = relationalPlaceholder;
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES_2.entrySet()) {
|
||||
fixedString = fixedString.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
format = format.replace(relationalPlaceholder, fixedString);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : SYMBOL_SUBSTITUTES.entrySet()) {
|
||||
format = format.replace(entry.getValue(), entry.getKey());
|
||||
}
|
||||
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Pair<String, Map<String, String>> replacePlaceholders(@NotNull String format, @NotNull Velocitab plugin,
|
||||
@Nullable TabPlayer player) {
|
||||
final Map<String, String> replacedPlaceholders = Maps.newHashMap();
|
||||
for (Placeholder placeholder : values()) {
|
||||
final Matcher matcher = placeholder.pattern.matcher(format);
|
||||
if (placeholder.parameterised) {
|
||||
format = matcher.replaceAll(matchResult -> {
|
||||
final String replacement = placeholder.replacer.apply(StringUtils.chop(matchResult.group().replace("%" + placeholder.name().toLowerCase(), "")
|
||||
.replaceFirst("_", "")), plugin, player);
|
||||
replacedPlaceholders.put(matchResult.group(), replacement);
|
||||
return Matcher.quoteReplacement(replacement);
|
||||
});
|
||||
} else {
|
||||
format = matcher.replaceAll(matchResult -> {
|
||||
final String replacement = placeholder.replacer.apply(null, plugin, player);
|
||||
replacedPlaceholders.put(matchResult.group(), replacement);
|
||||
return Matcher.quoteReplacement(replacement);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Pair.of(format, replacedPlaceholders);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String applyPlaceholderReplacements(@NotNull String text, @NotNull TabPlayer player,
|
||||
@NotNull Map<String, String> parsed) {
|
||||
for (final Map.Entry<String, List<PlaceholderReplacement>> entry : player.getGroup().placeholderReplacements().entrySet()) {
|
||||
if (!parsed.containsKey(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String replaced = parsed.get(entry.getKey());
|
||||
final Optional<PlaceholderReplacement> replacement = entry.getValue().stream()
|
||||
.filter(r -> r.placeholder().equalsIgnoreCase(replaced))
|
||||
.findFirst();
|
||||
|
||||
if (replacement.isPresent()) {
|
||||
text = text.replace(entry.getKey(), replacement.get().replacement());
|
||||
} else {
|
||||
final Optional<PlaceholderReplacement> elseReplacement = entry.getValue().stream()
|
||||
.filter(r -> r.placeholder().equalsIgnoreCase(ELSE_PLACEHOLDER))
|
||||
.findFirst();
|
||||
if (elseReplacement.isPresent()) {
|
||||
text = text.replace(entry.getKey(), elseReplacement.get().replacement());
|
||||
}
|
||||
}
|
||||
}
|
||||
return applyPlaceholders(text, parsed);
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
|
||||
@NotNull TabPlayer player) {
|
||||
|
||||
if (format.equals(DELIMITER)) {
|
||||
return CompletableFuture.completedFuture("");
|
||||
}
|
||||
|
||||
final Pair<String, Map<String, String>> replaced = replaceInternal(format, plugin, player);
|
||||
if (!PLACEHOLDER_PATTERN.matcher(replaced.first()).find()) {
|
||||
return CompletableFuture.completedFuture(applyPlaceholderReplacements(format, player, replaced.second()));
|
||||
}
|
||||
|
||||
final List<String> placeholders = extractPlaceholders(replaced.first());
|
||||
return plugin.getPAPIProxyBridgeHook()
|
||||
.map(hook -> hook.parsePlaceholders(placeholders, player.getPlayer())
|
||||
.exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
|
||||
return Map.of();
|
||||
})
|
||||
)
|
||||
.orElse(CompletableFuture.completedFuture(Maps.newHashMap()))
|
||||
.exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "An error occurred whilst parsing placeholders: " + e.getMessage());
|
||||
return Map.of();
|
||||
})
|
||||
.thenApply(m -> applyPlaceholderReplacements(format, player, mergeMaps(m, replaced.second())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String applyPlaceholders(@NotNull String text, @NotNull Map<String, String> replacements) {
|
||||
for (Map.Entry<String, String> entry : replacements.entrySet()) {
|
||||
text = text.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, String> mergeMaps(@NotNull Map<String, String> map1, @NotNull Map<String, String> map2) {
|
||||
map1.putAll(map2);
|
||||
return map1;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> extractPlaceholders(@NotNull String text) {
|
||||
final List<String> placeholders = Lists.newArrayList();
|
||||
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
|
||||
while (matcher.find()) {
|
||||
placeholders.add(matcher.group());
|
||||
}
|
||||
return placeholders;
|
||||
}
|
||||
}
|
@ -19,14 +19,15 @@
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.velocitypowered.api.util.ServerLink;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public record ServerUrl(
|
||||
@NotNull String label,
|
||||
@ -40,26 +41,25 @@ public record ServerUrl(
|
||||
|
||||
// Resolve the built-in label or format the custom label, then wrap as a Velocity ServerLink
|
||||
@NotNull
|
||||
CompletableFuture<ServerLink> getServerLink(@NotNull Velocitab plugin, @NotNull TabPlayer player) {
|
||||
ServerLink getServerLink(@NotNull Velocitab plugin, @NotNull TabPlayer player) {
|
||||
return getBuiltInLabel().map(
|
||||
(type) -> CompletableFuture.completedFuture(ServerLink.serverLink(type, url()))
|
||||
(type) -> ServerLink.serverLink(type, url())
|
||||
).orElseGet(
|
||||
() -> Placeholder.replace(label(), plugin, player)
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin))
|
||||
.thenApply(formatted -> ServerLink.serverLink(formatted, url()))
|
||||
);
|
||||
() -> {
|
||||
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(player, label());
|
||||
final Component formatted = plugin.getFormatter().format(replaced, player, plugin);
|
||||
return ServerLink.serverLink(formatted, url());
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static CompletableFuture<List<ServerLink>> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
|
||||
@NotNull List<ServerUrl> urls) {
|
||||
final List<CompletableFuture<ServerLink>> futures = new ArrayList<>();
|
||||
public static List<ServerLink> resolve(@NotNull Velocitab plugin, @NotNull TabPlayer player,
|
||||
@NotNull List<ServerUrl> urls) {
|
||||
final List<ServerLink> serverLinks = Lists.newArrayList();
|
||||
for (ServerUrl url : urls) {
|
||||
futures.add(url.getServerLink(plugin, player));
|
||||
serverLinks.add(url.getServerLink(plugin, player));
|
||||
}
|
||||
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.thenApply(v -> futures.stream()
|
||||
.map(CompletableFuture::join).toList());
|
||||
return serverLinks;
|
||||
}
|
||||
|
||||
private Optional<ServerLink.Type> getBuiltInLabel() {
|
||||
|
@ -28,7 +28,6 @@ import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@ -40,7 +39,8 @@ public class Settings implements ConfigValidator {
|
||||
public static final String CONFIG_HEADER = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Velocitab Config ┃
|
||||
┃ Developed by William278 ┃
|
||||
┃ Developed by ┃
|
||||
┃ William278 & AlexDev03 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ Information: https://william278.net/project/velocitab
|
||||
┗╸ Documentation: https://william278.net/docs/velocitab""";
|
||||
@ -96,6 +96,9 @@ public class Settings implements ConfigValidator {
|
||||
@Comment("Whether to force sending tab list packets to all players, even if a packet for that action has already been sent. This could fix issues with some mods.")
|
||||
private boolean forceSendingTabListPackets = false;
|
||||
|
||||
@Comment("Whether to enable relational placeholders. With an high amount of players, this could cause lag.")
|
||||
private boolean enableRelationalPlaceholders = false;
|
||||
|
||||
@Comment({"A list of links that will be sent to display on player pause menus (Minecraft 1.21+ clients only).",
|
||||
"• Labels can be fully custom or built-in (one of 'bug_report', 'community_guidelines', 'support', 'status',",
|
||||
" 'feedback', 'community', 'website', 'forums', 'news', or 'announcements').",
|
||||
@ -116,7 +119,7 @@ public class Settings implements ConfigValidator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(@NotNull Velocitab plugin) {
|
||||
public void validateConfig(@NotNull Velocitab plugin, @NotNull String name) {
|
||||
if (papiCacheTime < 0) {
|
||||
throw new IllegalStateException("PAPI cache time must be greater than or equal to 0");
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.placeholder.PlaceholderReplacement;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -64,6 +65,8 @@ public class TabGroups implements ConfigValidator {
|
||||
false,
|
||||
1000,
|
||||
1000,
|
||||
1000,
|
||||
1000,
|
||||
false
|
||||
);
|
||||
|
||||
@ -84,42 +87,15 @@ public class TabGroups implements ConfigValidator {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
|
||||
final List<Group> groups = new ArrayList<>(this.groups);
|
||||
final Optional<Group> defaultGroup = getGroup("default");
|
||||
if (defaultGroup.isEmpty()) {
|
||||
throw new IllegalStateException("No default group found");
|
||||
}
|
||||
// Ensure the default group is always checked last
|
||||
groups.remove(defaultGroup.get());
|
||||
groups.add(defaultGroup.get());
|
||||
for (Group group : groups) {
|
||||
if (group.registeredServers(plugin, false)
|
||||
.stream()
|
||||
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
|
||||
return Optional.of(group);
|
||||
}
|
||||
}
|
||||
|
||||
if (!plugin.getSettings().isFallbackEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return defaultGroup;
|
||||
}
|
||||
|
||||
public int getPosition(@NotNull Group group) {
|
||||
return groups.indexOf(group) + 1;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void validateConfig(@NotNull Velocitab plugin) {
|
||||
if (groups.isEmpty()) {
|
||||
throw new IllegalStateException("No tab groups defined in config");
|
||||
}
|
||||
if (groups.stream().noneMatch(group -> group.name().equals("default"))) {
|
||||
throw new IllegalStateException("No default tab group defined in config");
|
||||
public void validateConfig(@NotNull Velocitab plugin, @NotNull String name) {
|
||||
if(name.equals("tab_groups")) {
|
||||
if (groups.isEmpty()) {
|
||||
throw new IllegalStateException("No tab groups defined in config " + name);
|
||||
}
|
||||
if (groups.stream().noneMatch(group -> group.name().equals("default"))) {
|
||||
throw new IllegalStateException("No default tab group defined in config " + name);
|
||||
}
|
||||
}
|
||||
|
||||
final Multimap<Group, String> missingKeys = getMissingKeys();
|
||||
@ -127,7 +103,7 @@ public class TabGroups implements ConfigValidator {
|
||||
return;
|
||||
}
|
||||
|
||||
fixMissingKeys(plugin, missingKeys);
|
||||
fixMissingKeys(plugin, missingKeys, name);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -151,12 +127,28 @@ public class TabGroups implements ConfigValidator {
|
||||
if (group.placeholderReplacements() == null) {
|
||||
missingKeys.put(group, "placeholderReplacements");
|
||||
}
|
||||
|
||||
if (group.headerFooterUpdateRate() == 0) {
|
||||
missingKeys.put(group, "headerFooterUpdateRate");
|
||||
}
|
||||
|
||||
if (group.formatUpdateRate() == 0) {
|
||||
missingKeys.put(group, "formatUpdateRate");
|
||||
}
|
||||
|
||||
if (group.nametagUpdateRate() == 0) {
|
||||
missingKeys.put(group, "nametagUpdateRate");
|
||||
}
|
||||
|
||||
if (group.placeholderUpdateRate() == 0) {
|
||||
missingKeys.put(group, "placeholderUpdateRate");
|
||||
}
|
||||
}
|
||||
|
||||
return missingKeys;
|
||||
}
|
||||
|
||||
private void fixMissingKeys(@NotNull Velocitab plugin, @NotNull Multimap<Group, String> missingKeys) {
|
||||
private void fixMissingKeys(@NotNull Velocitab plugin, @NotNull Multimap<Group, String> missingKeys, @NotNull String name) {
|
||||
missingKeys.forEach((group, keys) -> {
|
||||
plugin.log("Missing required key(s) " + keys + " for group " + group.name());
|
||||
plugin.log("Using default values for group " + group.name());
|
||||
@ -173,14 +165,16 @@ public class TabGroups implements ConfigValidator {
|
||||
group.sortingPlaceholders() == null ? DEFAULT_GROUP.sortingPlaceholders() : group.sortingPlaceholders(),
|
||||
group.placeholderReplacements() == null ? DEFAULT_GROUP.placeholderReplacements() : group.placeholderReplacements(),
|
||||
group.collisions(),
|
||||
group.headerFooterUpdateRate(),
|
||||
group.placeholderUpdateRate(),
|
||||
group.headerFooterUpdateRate() == 0 ? DEFAULT_GROUP.headerFooterUpdateRate() : group.headerFooterUpdateRate(),
|
||||
group.formatUpdateRate() == 0 ? DEFAULT_GROUP.formatUpdateRate() : group.formatUpdateRate(),
|
||||
group.nametagUpdateRate() == 0 ? DEFAULT_GROUP.nametagUpdateRate() : group.nametagUpdateRate(),
|
||||
group.placeholderUpdateRate() == 0 ? DEFAULT_GROUP.placeholderUpdateRate() : group.placeholderUpdateRate(),
|
||||
group.onlyListPlayersInSameServer()
|
||||
);
|
||||
|
||||
groups.add(group);
|
||||
});
|
||||
|
||||
plugin.saveTabGroups();
|
||||
plugin.getTabGroupsManager().saveGroup(this);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import de.exlll.configlib.YamlConfigurationProperties;
|
||||
import de.exlll.configlib.YamlConfigurations;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TabGroupsManager {
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final Map<String, Group> groups;
|
||||
private final Map<TabGroups, String> groupsFiles;
|
||||
private List<Group> groupsList;
|
||||
|
||||
public TabGroupsManager(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.groups = Maps.newConcurrentMap();
|
||||
this.groupsFiles = Maps.newConcurrentMap();
|
||||
this.groupsList = Lists.newArrayList();
|
||||
}
|
||||
|
||||
public void loadGroups() {
|
||||
groups.clear();
|
||||
groupsFiles.clear();
|
||||
final Path configDirectory = plugin.getConfigDirectory();
|
||||
final File defaultFile = configDirectory.resolve("tab_groups.yml").toFile();
|
||||
final YamlConfigurationProperties properties = ConfigProvider.YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build();
|
||||
final TabGroups defaultTagGroupsFile = YamlConfigurations.update(
|
||||
configDirectory.resolve("tab_groups.yml"),
|
||||
TabGroups.class,
|
||||
properties
|
||||
);
|
||||
|
||||
final String defaultName = defaultFile.getAbsolutePath().replace(".yml", "");
|
||||
if (!validateGroups(defaultTagGroupsFile, defaultName)) {
|
||||
throw new IllegalStateException("Failed to load default tab groups file");
|
||||
}
|
||||
|
||||
final File folder = plugin.getConfigDirectory().resolve("tab_groups").toFile();
|
||||
if (folder.exists()) {
|
||||
final File[] filesArray = folder.listFiles();
|
||||
final List<File> files = filesArray == null ? List.of() : Arrays.asList(filesArray);
|
||||
for (File file : files) {
|
||||
if (!file.getName().endsWith(".yml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final TabGroups preCheck = YamlConfigurations.load(file.toPath(), TabGroups.class, properties);
|
||||
preCheck.groups.removeIf(g -> g.name().equals("default"));
|
||||
YamlConfigurations.save(file.toPath(), TabGroups.class, preCheck, properties);
|
||||
|
||||
final TabGroups group = YamlConfigurations.update(
|
||||
file.toPath(),
|
||||
TabGroups.class,
|
||||
properties
|
||||
);
|
||||
final String name = folder.getAbsoluteFile() + "/" + file.getName().replace(".yml", "");
|
||||
if (!validateGroups(group, name)) {
|
||||
throw new IllegalStateException("Failed to load tab groups file " + file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.groupsList = Lists.newArrayList(getGroups());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<File> getGroupsFiles() {
|
||||
return groupsFiles.values().stream().map(f -> new File(f+".yml")).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isDefaultFile(@NotNull File file) {
|
||||
return plugin.getConfigDirectory().resolve("tab_groups.yml").toFile().getAbsolutePath().equals(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
private boolean validateGroups(@NotNull TabGroups group, @NotNull String name) {
|
||||
this.groupsFiles.put(group, name);
|
||||
group.validateConfig(plugin, name);
|
||||
|
||||
final List<Group> eligibleGroups = Lists.newArrayList();
|
||||
final Set<RegisteredServer> registeredServers = Sets.newHashSet();
|
||||
|
||||
outer:
|
||||
for (Group group1 : group.groups) {
|
||||
final Set<RegisteredServer> current = group1.registeredServers(plugin, false);
|
||||
|
||||
if (groups.containsKey(group1.name())) {
|
||||
plugin.getLogger().warn("Group {} is already defined in {} tab groups file. Skipping.", group1.name(), name);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (RegisteredServer registeredServer : current) {
|
||||
if (registeredServers.contains(registeredServer)) {
|
||||
plugin.getLogger().warn("Server {} is already registered for group {} in {}, the same tabgroups file. Skipping.", registeredServer.getServerInfo().getName(), group1.name(), name);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
registeredServers.addAll(current);
|
||||
eligibleGroups.add(group1);
|
||||
}
|
||||
|
||||
outer:
|
||||
for (Group group1 : groups.values()) {
|
||||
final Set<RegisteredServer> current = group1.registeredServers(plugin, false);
|
||||
|
||||
for (Group loadingGroup : eligibleGroups) {
|
||||
final Set<RegisteredServer> loadingGroupServers = loadingGroup.registeredServers(plugin, false);
|
||||
|
||||
for (RegisteredServer registeredServer : loadingGroupServers) {
|
||||
if (current.contains(registeredServer)) {
|
||||
plugin.getLogger().warn("Server {} in {} tab groups file is already registered for group {}. Skipping.", registeredServer.getServerInfo().getName(), name, group1.name());
|
||||
eligibleGroups.remove(loadingGroup);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Group group1 : eligibleGroups) {
|
||||
groups.put(group1.name(), group1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void saveGroup(@NotNull TabGroups group) {
|
||||
final String name = groupsFiles.get(group);
|
||||
YamlConfigurations.save(
|
||||
new File("plugins/Velocitab").toPath().resolve(name + ".yml"),
|
||||
TabGroups.class,
|
||||
group,
|
||||
ConfigProvider.YAML_CONFIGURATION_PROPERTIES.header(TabGroups.CONFIG_HEADER).build()
|
||||
);
|
||||
}
|
||||
|
||||
public Optional<Group> getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) {
|
||||
final List<Group> groups = new ArrayList<>(this.groups.values());
|
||||
final Optional<Group> defaultGroup = getGroup("default");
|
||||
if (defaultGroup.isEmpty()) {
|
||||
throw new IllegalStateException("No default tab group defined");
|
||||
}
|
||||
// Ensure the default group is always checked last
|
||||
groups.remove(defaultGroup.get());
|
||||
groups.add(defaultGroup.get());
|
||||
for (Group group : groups) {
|
||||
if (group.registeredServers(plugin, false)
|
||||
.stream()
|
||||
.anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) {
|
||||
return Optional.of(group);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<Group> getGroup(@NotNull String name) {
|
||||
return Optional.ofNullable(groups.get(name));
|
||||
}
|
||||
|
||||
public int getGroupPosition(@NotNull Group group) {
|
||||
return groupsList.indexOf(group) + 1;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Collection<Group> getGroups() {
|
||||
return groups.values();
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
|
||||
package net.william278.velocitab.hook;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.event.Level;
|
||||
@ -66,9 +67,12 @@ public abstract class Hook {
|
||||
);
|
||||
|
||||
protected final Velocitab plugin;
|
||||
@Getter
|
||||
protected final String name;
|
||||
|
||||
public Hook(@NotNull Velocitab plugin) {
|
||||
public Hook(@NotNull Velocitab plugin, @NotNull String name) {
|
||||
this.plugin = plugin;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private static boolean isPluginAvailable(@NotNull Velocitab plugin, @NotNull String id) {
|
||||
|
@ -48,7 +48,7 @@ public class LuckPermsHook extends Hook {
|
||||
private boolean enabled;
|
||||
|
||||
public LuckPermsHook(@NotNull Velocitab plugin) throws IllegalStateException {
|
||||
super(plugin);
|
||||
super(plugin, "LuckPerms");
|
||||
this.api = LuckPermsProvider.get();
|
||||
this.lastUpdate = Maps.newConcurrentMap();
|
||||
this.event = api.getEventBus().subscribe(
|
||||
@ -116,11 +116,11 @@ public class LuckPermsHook extends Hook {
|
||||
}
|
||||
|
||||
tabPlayer.setRole(newRole);
|
||||
tabList.updatePlayerDisplayName(tabPlayer);
|
||||
tabList.updateDisplayName(tabPlayer);
|
||||
tabList.getVanishTabList().recalculateVanishForPlayer(tabPlayer);
|
||||
checkRoleUpdate(tabPlayer, oldRole);
|
||||
})
|
||||
.delay(500, TimeUnit.MILLISECONDS)
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.schedule());
|
||||
}
|
||||
|
||||
@ -145,6 +145,7 @@ public class LuckPermsHook extends Hook {
|
||||
if (oldRole.equals(player.getRole())) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getTabList().updatePlayer(player, false);
|
||||
}
|
||||
|
||||
|
@ -19,34 +19,48 @@
|
||||
|
||||
package net.william278.velocitab.hook;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import io.github.miniplaceholders.api.MiniPlaceholders;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.jodah.expiringmap.ExpiringMap;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class MiniPlaceholdersHook extends Hook {
|
||||
|
||||
private final VelocitabMiniExpansion expansion;
|
||||
private final Map<UUID, TagResolver> cache;
|
||||
|
||||
public MiniPlaceholdersHook(@NotNull Velocitab plugin) {
|
||||
super(plugin);
|
||||
this.expansion = new VelocitabMiniExpansion(plugin);
|
||||
expansion.registerExpansion();
|
||||
super(plugin, "MiniPlaceholders");
|
||||
this.cache = ExpiringMap.builder()
|
||||
.expiration(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component format(@NotNull String text, @NotNull Audience player, @Nullable Audience viewer) {
|
||||
private TagResolver getResolver(@NotNull Player player, @Nullable Player viewer) {
|
||||
if (viewer == null) {
|
||||
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getAudienceGlobalPlaceholders(player));
|
||||
return cache.computeIfAbsent(player.getUniqueId(), u -> MiniPlaceholders.getAudienceGlobalPlaceholders(player));
|
||||
}
|
||||
return MiniMessage.miniMessage().deserialize(text, MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
|
||||
|
||||
final UUID merged = new UUID(player.getUniqueId().getMostSignificantBits(), viewer.getUniqueId().getMostSignificantBits());
|
||||
return cache.computeIfAbsent(merged, u -> MiniPlaceholders.getRelationalGlobalPlaceholders(player, viewer));
|
||||
}
|
||||
|
||||
public void unregisterExpansion() {
|
||||
expansion.unregisterExpansion();
|
||||
@NotNull
|
||||
public Component format(@NotNull String text, @NotNull Player player, @Nullable Player viewer) {
|
||||
if (viewer == null) {
|
||||
return MiniMessage.miniMessage().deserialize(text, getResolver(player, null));
|
||||
}
|
||||
|
||||
return MiniMessage.miniMessage().deserialize(text, getResolver(player, viewer));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,15 +19,11 @@
|
||||
|
||||
package net.william278.velocitab.hook;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.william278.papiproxybridge.api.PlaceholderAPI;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class PAPIProxyBridgeHook extends Hook {
|
||||
@ -35,7 +31,7 @@ public class PAPIProxyBridgeHook extends Hook {
|
||||
private final PlaceholderAPI api;
|
||||
|
||||
public PAPIProxyBridgeHook(@NotNull Velocitab plugin) {
|
||||
super(plugin);
|
||||
super(plugin, "PAPIProxyBridge");
|
||||
this.api = PlaceholderAPI.createInstance();
|
||||
this.api.setCacheExpiry(Math.max(0, plugin.getSettings().getPapiCacheTime()));
|
||||
this.api.setRequestTimeout(1500);
|
||||
@ -45,17 +41,4 @@ public class PAPIProxyBridgeHook extends Hook {
|
||||
return api.formatPlaceholders(input, player.getUniqueId());
|
||||
}
|
||||
|
||||
public CompletableFuture<Map<String, String>> parsePlaceholders(@NotNull List<String> input, @NotNull Player player) {
|
||||
final Map<String, String> map = Maps.newConcurrentMap();
|
||||
final List<CompletableFuture<String>> futures = Lists.newArrayList();
|
||||
|
||||
for (String s : input) {
|
||||
final CompletableFuture<String> future = formatPlaceholders(s, player);
|
||||
futures.add(future);
|
||||
future.thenAccept(r -> map.put(s, r));
|
||||
}
|
||||
|
||||
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).thenApply(v -> map);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.hook;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import io.github.miniplaceholders.api.Expansion;
|
||||
import io.github.miniplaceholders.api.MiniPlaceholders;
|
||||
import io.github.miniplaceholders.api.utils.TagsUtils;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.Tag;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.hook.miniconditions.MiniConditionManager;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class VelocitabMiniExpansion {
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final MiniConditionManager miniConditionManager;
|
||||
private Expansion expansion;
|
||||
|
||||
public VelocitabMiniExpansion(Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.miniConditionManager = new MiniConditionManager(plugin);
|
||||
}
|
||||
|
||||
public void registerExpansion() {
|
||||
final Expansion.Builder builder = Expansion.builder("velocitab");
|
||||
builder.relationalPlaceholder("condition", ((a1, a2, queue, ctx) -> {
|
||||
if (!(a2 instanceof Player target)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
if (!(a1 instanceof Player audience)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
return Tag.selfClosingInserting(miniConditionManager.checkConditions(target, audience, queue));
|
||||
}));
|
||||
builder.relationalPlaceholder("who-is-seeing", ((a1, a2, queue, ctx) -> {
|
||||
if (!(a2 instanceof Player target)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
if (!(a1 instanceof Player)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
return Tag.selfClosingInserting(Component.text(target.getUsername()));
|
||||
}));
|
||||
builder.relationalPlaceholder("perm", ((a1, a2, queue, ctx) -> {
|
||||
if (!(a2 instanceof Player target)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
if (!(a1 instanceof Player audience)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
final Optional<TabPlayer> targetOptional = plugin.getTabList().getTabPlayer(audience);
|
||||
if (targetOptional.isEmpty()) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
final TabPlayer targetPlayer = targetOptional.get();
|
||||
|
||||
if (!queue.hasNext()) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
final String permission = queue.pop().value();
|
||||
|
||||
if (!queue.hasNext()) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
if (!target.hasPermission(permission)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
final String value = fixValue(popAll(queue));
|
||||
final String replaced = Placeholder.replaceInternal(value, plugin, targetPlayer).first();
|
||||
|
||||
return Tag.selfClosingInserting(MiniMessage.miniMessage().deserialize(replaced, MiniPlaceholders.getAudienceGlobalPlaceholders(audience)));
|
||||
}));
|
||||
builder.relationalPlaceholder("vanish", ((a1, otherAudience, queue, ctx) -> {
|
||||
if (!(otherAudience instanceof Player target)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
if (!(a1 instanceof Player audience)) {
|
||||
return TagsUtils.EMPTY_TAG;
|
||||
}
|
||||
|
||||
return Tag.selfClosingInserting(Component.text(plugin.getVanishManager().getIntegration().canSee(audience.getUsername(), target.getUsername())));
|
||||
}));
|
||||
plugin.getLogger().info("Registered Velocitab MiniExpansion");
|
||||
expansion = builder.build();
|
||||
expansion.register();
|
||||
}
|
||||
|
||||
public void unregisterExpansion() {
|
||||
expansion.unregister();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String popAll(@NotNull ArgumentQueue queue) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
int i = 0;
|
||||
while (queue.hasNext()) {
|
||||
if (i > 0) {
|
||||
builder.append(":");
|
||||
}
|
||||
builder.append(queue.pop().value());
|
||||
i++;
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String fixValue(@NotNull String value) {
|
||||
return value.replace("*LESS2*", "<").replace("*GREATER2*", ">");
|
||||
}
|
||||
|
||||
}
|
@ -1,216 +0,0 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.hook.miniconditions;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.jodah.expiringmap.ExpiringMap;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.apache.commons.jexl3.JexlBuilder;
|
||||
import org.apache.commons.jexl3.JexlContext;
|
||||
import org.apache.commons.jexl3.JexlEngine;
|
||||
import org.apache.commons.jexl3.MapContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MiniConditionManager {
|
||||
|
||||
public final static Map<String, String> REPLACE = Map.of(
|
||||
"\"", "-q-",
|
||||
"'", "-a-"
|
||||
);
|
||||
|
||||
public final static Map<String, String> REPLACE_2 = Map.of(
|
||||
"*LESS3*", "<",
|
||||
"*GREATER3*", ">",
|
||||
"*LESS2*", "<",
|
||||
"*GREATER2*", ">"
|
||||
);
|
||||
|
||||
private final static Map<String, String> REPLACE_3 = Map.of(
|
||||
"?dp?", ":"
|
||||
);
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final JexlEngine jexlEngine;
|
||||
private final JexlContext jexlContext;
|
||||
private final Pattern targetPlaceholderPattern;
|
||||
private final Pattern miniEscapeEndTags;
|
||||
private final Map<String, Object> cachedExpressions;
|
||||
|
||||
public MiniConditionManager(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.jexlEngine = createJexlEngine();
|
||||
this.jexlContext = createJexlContext();
|
||||
this.targetPlaceholderPattern = Pattern.compile("%target_(\\w+)?%");
|
||||
this.miniEscapeEndTags = Pattern.compile("</(\\w+)>");
|
||||
this.cachedExpressions = ExpiringMap.builder()
|
||||
.expiration(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private JexlEngine createJexlEngine() {
|
||||
return new JexlBuilder().create();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private JexlContext createJexlContext() {
|
||||
final JexlContext jexlContext = new MapContext();
|
||||
jexlContext.set("startsWith", new StartsWith());
|
||||
jexlContext.set("endsWith", new EndsWith());
|
||||
return jexlContext;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component checkConditions(@NotNull Player target, @NotNull Player audience, @NotNull ArgumentQueue queue) {
|
||||
final List<String> parameters = collectParameters(queue);
|
||||
if (parameters.isEmpty()) {
|
||||
plugin.getLogger().warn("Empty condition");
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
|
||||
String condition = decodeCondition(parameters.get(0));
|
||||
if (parameters.size() < 3) {
|
||||
plugin.getLogger().warn("Invalid condition: Missing true/false values for condition: {}", condition);
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(target);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
|
||||
condition = Placeholder.replaceInternal(condition, plugin, tabPlayer.get()).first();
|
||||
final String falseValue = processFalseValue(parameters.get(2));
|
||||
final String expression = buildExpression(condition);
|
||||
return evaluateAndFormatCondition(expression, target, audience, parameters.get(1), falseValue);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<String> collectParameters(@NotNull ArgumentQueue queue) {
|
||||
final List<String> parameters = Lists.newArrayList();
|
||||
while (queue.hasNext()) {
|
||||
String param = queue.pop().value();
|
||||
for (Map.Entry<String, String> entry : REPLACE_2.entrySet()) {
|
||||
param = param.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
for (Map.Entry<String, String> entry : REPLACE_3.entrySet()) {
|
||||
param = param.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
parameters.add(param);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String decodeCondition(@NotNull String condition) {
|
||||
for (Map.Entry<String, String> entry : REPLACE.entrySet()) {
|
||||
condition = condition.replace(entry.getValue(), entry.getKey());
|
||||
condition = condition.replace(entry.getKey() + entry.getKey(), entry.getKey());
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String processFalseValue(@NotNull String falseValue) {
|
||||
final Matcher matcher = miniEscapeEndTags.matcher(falseValue);
|
||||
if (matcher.find()) {
|
||||
final String tag = matcher.group(1);
|
||||
if (falseValue.startsWith("</" + tag + ">")) {
|
||||
falseValue = falseValue.substring(tag.length() + 3);
|
||||
}
|
||||
}
|
||||
return falseValue;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String buildExpression(@NotNull String condition) {
|
||||
return condition.replace("and", "&&").replace("or", "||")
|
||||
.replace("AND", "&&").replace("OR", "||");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Component evaluateAndFormatCondition(@NotNull String expression, @NotNull Player target, @NotNull Player audience, @NotNull String trueValue, @NotNull String falseValue) {
|
||||
final String targetString = parseTargetPlaceholders(expression, target);
|
||||
try {
|
||||
final Object result = evaluateExpression(targetString);
|
||||
if (result instanceof Boolean) {
|
||||
final boolean boolResult = (Boolean) result;
|
||||
final String value = boolResult ? trueValue : falseValue;
|
||||
return plugin.getMiniPlaceholdersHook().orElseThrow().format(value, target, audience);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warn("Failed to evaluate condition: {} error: {}", expression, e.getMessage());
|
||||
}
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Object evaluateExpression(@NotNull String expression) {
|
||||
return cachedExpressions.computeIfAbsent(expression, key -> jexlEngine.createExpression(key).evaluate(jexlContext));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String parseTargetPlaceholders(@NotNull String input, @NotNull Player target) {
|
||||
final Optional<TabPlayer> tabPlayer = plugin.getTabList().getTabPlayer(target);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return targetPlaceholderPattern.matcher(input).replaceAll(match -> {
|
||||
final String placeholder = match.group(1);
|
||||
if (placeholder == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final String text = "%" + placeholder + "%";
|
||||
final Optional<String> placeholderValue = tabPlayer.get().getCachedPlaceholderValue(text);
|
||||
return placeholderValue.orElse(text);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class StartsWith {
|
||||
public boolean startsWith(String str, String prefix) {
|
||||
return str != null && str.startsWith(prefix);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class EndsWith {
|
||||
public boolean endsWith(String str, String suffix) {
|
||||
return str != null && str.endsWith(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -32,6 +32,8 @@ import io.netty.channel.DefaultChannelPipeline;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PacketEventManager {
|
||||
|
||||
private static final String KEY = "velocitab";
|
||||
@ -45,7 +47,10 @@ public class PacketEventManager {
|
||||
}
|
||||
|
||||
private void loadPlayers() {
|
||||
plugin.getServer().getAllPlayers().forEach(this::injectPlayer);
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> plugin.getServer().getAllPlayers().forEach(this::injectPlayer))
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
private void loadListeners() {
|
||||
@ -72,6 +77,10 @@ public class PacketEventManager {
|
||||
.addBefore(Connections.HANDLER, KEY, handler);
|
||||
}
|
||||
|
||||
public void removeAllPlayers() {
|
||||
plugin.getServer().getAllPlayers().forEach(this::removePlayer);
|
||||
}
|
||||
|
||||
public void removePlayer(@NotNull Player player) {
|
||||
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
|
||||
final Channel channel = connectedPlayer.getConnection().getChannel();
|
||||
|
@ -75,7 +75,7 @@ public class PlayerChannelHandler extends ChannelDuplexHandler {
|
||||
// This is to prevent conflicts with Velocitab teams.
|
||||
plugin.getLogger().warn("Cancelled team \"{}\" packet from backend for player {}. " +
|
||||
"We suggest disabling \"send_scoreboard_packets\" in Velocitab's config.yml file, " +
|
||||
"but note this will disable TAB sorting",
|
||||
"but note this will disable TAB sorting. If you want to use sorting you have to disable team handling on your backend servers (plugin or vanilla scoreboard teams)",
|
||||
updateTeamsPacket.teamName(), player.getUsername());
|
||||
return;
|
||||
}
|
||||
|
@ -37,11 +37,11 @@ import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.sorting.SortedSet;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import net.william278.velocitab.util.DebugSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.*;
|
||||
|
||||
@ -100,7 +100,7 @@ public class ScoreboardManager {
|
||||
@NotNull
|
||||
public TeamsPacketAdapter getPacketAdapter(@NotNull ProtocolVersion version) {
|
||||
return Optional.ofNullable(versions.get(version))
|
||||
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version));
|
||||
.orElseThrow(() -> new IllegalArgumentException("No adapter found for protocol version " + version + ". Are you sure you're using the latest version of Velocitab and latest build of Velocity?"));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@ -129,7 +129,7 @@ public class ScoreboardManager {
|
||||
private void removeSortedTeam(@NotNull String teamName) {
|
||||
final boolean result = sortedTeams.removeTeam(teamName);
|
||||
if (!result) {
|
||||
plugin.log(Level.ERROR, "Failed to remove team " + teamName + " from sortedTeams");
|
||||
DebugSystem.log(DebugSystem.DebugLevel.ERROR, "Failed to remove team " + teamName + " from sortedTeams");
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,46 +173,39 @@ public class ScoreboardManager {
|
||||
* @param role The new role of the player. Must not be null.
|
||||
* @param force Whether to force the update even if the player's nametag is the same.
|
||||
*/
|
||||
public CompletableFuture<Void> updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
|
||||
public void updateRole(@NotNull TabPlayer tabPlayer, @NotNull String role, boolean force) {
|
||||
final Player player = tabPlayer.getPlayer();
|
||||
if (!player.isActive()) {
|
||||
plugin.getTabList().removeOfflinePlayer(player);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
plugin.getLogger().info("Player {} is not active, removing from tab list", player.getUsername());
|
||||
return;
|
||||
}
|
||||
final String name = player.getUsername();
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
tabPlayer.getNametag(plugin).thenAccept(newTag -> {
|
||||
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
|
||||
if (createdTeams.containsKey(player.getUniqueId())) {
|
||||
dispatchGroupPacket(
|
||||
UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())),
|
||||
tabPlayer
|
||||
);
|
||||
}
|
||||
final String oldRole = createdTeams.remove(player.getUniqueId());
|
||||
if (oldRole != null) {
|
||||
removeSortedTeam(oldRole);
|
||||
}
|
||||
createdTeams.put(player.getUniqueId(), role);
|
||||
final boolean a = sortedTeams.addTeam(role);
|
||||
if (!a) {
|
||||
plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams");
|
||||
}
|
||||
this.nametags.put(role, newTag);
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, role, newTag, name);
|
||||
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(newTag))) {
|
||||
this.nametags.put(role, newTag);
|
||||
dispatchGroupChangePacket(plugin, tabPlayer, role, newTag);
|
||||
} else {
|
||||
updatePlaceholders(tabPlayer);
|
||||
final Nametag nametag = tabPlayer.getNametag(plugin);
|
||||
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
|
||||
if (createdTeams.containsKey(player.getUniqueId())) {
|
||||
dispatchGroupPacket(
|
||||
UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())),
|
||||
tabPlayer
|
||||
);
|
||||
}
|
||||
future.complete(null);
|
||||
}).exceptionally(e -> {
|
||||
plugin.log(Level.ERROR, "Failed to update role for " + player.getUsername(), e);
|
||||
return null;
|
||||
});
|
||||
|
||||
return future;
|
||||
final String oldRole = createdTeams.remove(player.getUniqueId());
|
||||
if (oldRole != null) {
|
||||
removeSortedTeam(oldRole);
|
||||
}
|
||||
createdTeams.put(player.getUniqueId(), role);
|
||||
final boolean a = sortedTeams.addTeam(role);
|
||||
if (!a) {
|
||||
plugin.log(Level.ERROR, "Failed to add team " + role + " to sortedTeams");
|
||||
}
|
||||
this.nametags.put(role, nametag);
|
||||
dispatchGroupCreatePacket(plugin, tabPlayer, role, nametag, name);
|
||||
} else if (force || (this.nametags.containsKey(role) && !this.nametags.get(role).equals(nametag))) {
|
||||
this.nametags.put(role, nametag);
|
||||
dispatchGroupChangePacket(plugin, tabPlayer, role, nametag);
|
||||
} else {
|
||||
updatePlaceholders(tabPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePlaceholders(@NotNull TabPlayer tabPlayer) {
|
||||
|
@ -0,0 +1,199 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.placeholder;
|
||||
|
||||
import net.jodah.expiringmap.ExpiringMap;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.mvel2.MVEL;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ConditionManager {
|
||||
|
||||
private static final String VELOCITAB_REL_CONDITION = "velocitab_rel_condition:";
|
||||
private static final String VELOCITAB_CONDITION = "velocitab_condition:";
|
||||
private static final String VELOCITAB_REL_PLACEHOLDER_PERM = "velocitab_rel_perm:";
|
||||
private static final String VELOCITAB_REL_WHO_IS_SEEING = "velocitab_rel_who-is-seeing";
|
||||
private static final String VELOCITAB_REL_VANISH = "velocitab_rel_vanish";
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final Pattern targetPlaceholderPattern;
|
||||
private final Pattern miniEscapeEndTags;
|
||||
private final Map<String, Object> cachedExpressions;
|
||||
|
||||
private static final Map<String, String> REPLACE_CHARS = Map.of(
|
||||
"?dp?", ":"
|
||||
);
|
||||
|
||||
public ConditionManager(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.targetPlaceholderPattern = Pattern.compile("%target_(\\w+)?%");
|
||||
this.miniEscapeEndTags = Pattern.compile("</(\\w+)>");
|
||||
this.cachedExpressions = ExpiringMap.builder()
|
||||
.expiration(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String checkConditions(@NotNull TabPlayer target, @NotNull String argument) {
|
||||
final List<String> parameters = collectParameters(argument);
|
||||
if (parameters.isEmpty()) {
|
||||
plugin.getLogger().warn("Empty condition");
|
||||
return "";
|
||||
}
|
||||
|
||||
String condition = parameters.get(0);
|
||||
if (parameters.size() < 3) {
|
||||
plugin.getLogger().warn("Invalid condition: Missing true/false values for condition: {}", condition);
|
||||
return "";
|
||||
}
|
||||
|
||||
condition = plugin.getPlaceholderManager().applyPlaceholders(target, condition);
|
||||
final String falseValue = processFalseValue(parameters.get(2));
|
||||
final String expression = buildExpression(condition);
|
||||
return evaluateAndFormatCondition(expression, target, parameters.get(1), falseValue);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<String> collectParameters(@NotNull String argument) {
|
||||
for (Map.Entry<String, String> entry : REPLACE_CHARS.entrySet()) {
|
||||
argument = argument.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return Arrays.stream(argument.split(":"))
|
||||
.map(s -> s.replace("''", "\""))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String processFalseValue(@NotNull String falseValue) {
|
||||
final Matcher matcher = miniEscapeEndTags.matcher(falseValue);
|
||||
if (matcher.find()) {
|
||||
final String tag = matcher.group(1);
|
||||
if (falseValue.startsWith("</" + tag + ">")) {
|
||||
falseValue = falseValue.substring(tag.length() + 3);
|
||||
}
|
||||
}
|
||||
return falseValue;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String buildExpression(@NotNull String condition) {
|
||||
return condition.replace("and", "&&").replace("or", "||")
|
||||
.replace("AND", "&&").replace("OR", "||");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String evaluateAndFormatCondition(@NotNull String expression, @NotNull TabPlayer target,
|
||||
@NotNull String trueValue, @NotNull String falseValue) {
|
||||
final String targetString = parseTargetPlaceholders(expression, target).trim();
|
||||
try {
|
||||
final Object result = evaluateExpression(targetString);
|
||||
if (result instanceof Boolean) {
|
||||
final boolean boolResult = (Boolean) result;
|
||||
return boolResult ? trueValue : falseValue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warn("Failed to evaluate condition: {} error: {}", expression, e.getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Object evaluateExpression(@NotNull String expression) {
|
||||
return cachedExpressions.computeIfAbsent(expression, MVEL::eval);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String parseTargetPlaceholders(@NotNull String input, @NotNull TabPlayer target) {
|
||||
return targetPlaceholderPattern.matcher(input).replaceAll(match -> {
|
||||
final String placeholder = match.group(1);
|
||||
if (placeholder == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final String text = "%" + placeholder + "%";
|
||||
final Optional<String> placeholderValue = plugin.getPlaceholderManager().getCachedPlaceholderValue(text, target.getPlayer().getUniqueId());
|
||||
return placeholderValue.orElse(text);
|
||||
});
|
||||
}
|
||||
|
||||
public String handleVelocitabPlaceholders(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer viewer) {
|
||||
if (viewer == null) {
|
||||
return handleConditionPlaceholders(text, player);
|
||||
}
|
||||
|
||||
return handleRelPlaceholders(text, player, viewer);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String handleRelPlaceholders(@NotNull String text, @NotNull TabPlayer player, @NotNull TabPlayer viewer) {
|
||||
switch (text) {
|
||||
case VELOCITAB_REL_WHO_IS_SEEING -> viewer.getPlayer().getUsername();
|
||||
case VELOCITAB_REL_VANISH -> {
|
||||
if (plugin.getVanishManager().isVanished(viewer.getPlayer().getUsername())) {
|
||||
return "true";
|
||||
}
|
||||
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length() < VELOCITAB_REL_CONDITION.length()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (text.startsWith(VELOCITAB_REL_CONDITION)) {
|
||||
return checkConditions(player, text.substring(VELOCITAB_REL_CONDITION.length()));
|
||||
}
|
||||
|
||||
if (text.startsWith(VELOCITAB_REL_PLACEHOLDER_PERM)) {
|
||||
final String cleaned = text.substring(VELOCITAB_REL_PLACEHOLDER_PERM.length());
|
||||
final int firstSeparator = cleaned.indexOf(':');
|
||||
if (firstSeparator == -1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final String permission = cleaned.substring(0, firstSeparator);
|
||||
final String trueValue = cleaned.substring(firstSeparator + 1);
|
||||
return viewer.getPlayer().hasPermission(permission) ? trueValue : "";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String handleConditionPlaceholders(@NotNull String text, @NotNull TabPlayer player) {
|
||||
if (text.startsWith(VELOCITAB_CONDITION)) {
|
||||
return checkConditions(player, text.substring(VELOCITAB_CONDITION.length()));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.placeholder;
|
||||
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import lombok.Getter;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.util.TriFunction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
public enum Placeholder {
|
||||
|
||||
PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getPlayerCount())),
|
||||
MAX_PLAYERS_ONLINE((plugin, player) -> Integer.toString(plugin.getServer().getConfiguration().getShowMaxPlayers())),
|
||||
LOCAL_PLAYERS_ONLINE((plugin, player) -> player.getPlayer().getCurrentServer()
|
||||
.map(ServerConnection::getServer)
|
||||
.map(RegisteredServer::getPlayersConnected)
|
||||
.map(players -> Integer.toString(players.size()))
|
||||
.orElse("")),
|
||||
GROUP_PLAYERS_ONLINE((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return Integer.toString(player.getGroup().getPlayers(plugin).size());
|
||||
}
|
||||
return plugin.getTabGroupsManager().getGroup(param)
|
||||
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
|
||||
.orElse("Group " + param + " not found");
|
||||
}),
|
||||
CURRENT_DATE_DAY((plugin, player) -> DateTimeFormatter.ofPattern("dd").format(LocalDateTime.now())),
|
||||
CURRENT_DATE_WEEKDAY((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("EEEE").format(LocalDateTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofPattern("EEEE").withLocale(locale).format(LocalDateTime.now());
|
||||
}),
|
||||
CURRENT_DATE_MONTH((plugin, player) -> DateTimeFormatter.ofPattern("MM").format(LocalDateTime.now())),
|
||||
CURRENT_DATE_YEAR((plugin, player) -> DateTimeFormatter.ofPattern("yyyy").format(LocalDateTime.now())),
|
||||
CURRENT_DATE((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("dd/MM/yyyy").format(LocalDateTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale).format(LocalDateTime.now());
|
||||
}),
|
||||
CURRENT_TIME_HOUR((plugin, player) -> DateTimeFormatter.ofPattern("HH").format(LocalDateTime.now())),
|
||||
CURRENT_TIME_MINUTE((plugin, player) -> DateTimeFormatter.ofPattern("mm").format(LocalDateTime.now())),
|
||||
CURRENT_TIME_SECOND((plugin, player) -> DateTimeFormatter.ofPattern("ss").format(LocalDateTime.now())),
|
||||
CURRENT_TIME((param, plugin, player) -> {
|
||||
if (param.isEmpty()) {
|
||||
return DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
|
||||
}
|
||||
|
||||
final String countryCode = param.toUpperCase();
|
||||
final Locale locale = Locale.forLanguageTag(countryCode);
|
||||
return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).format(LocalTime.now());
|
||||
}),
|
||||
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),
|
||||
USERNAME_LOWER((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername()).toLowerCase()),
|
||||
SERVER((plugin, player) -> player.getServerName()),
|
||||
PING((plugin, player) -> Long.toString(player.getPlayer().getPing())),
|
||||
PREFIX((plugin, player) -> player.getRole().getPrefix()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_prefix%"))),
|
||||
SUFFIX((plugin, player) -> player.getRole().getSuffix()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_suffix%"))),
|
||||
ROLE((plugin, player) -> player.getRole().getName()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
|
||||
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName()
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_primary_group_name%"))),
|
||||
ROLE_WEIGHT((plugin, player) -> plugin.getLuckPermsHook()
|
||||
.map(hook -> player.getRoleWeightString())
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_weight%"))),
|
||||
SERVER_GROUP((plugin, player) -> player.getGroup().name()),
|
||||
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
|
||||
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))),
|
||||
LUCKPERMS_META((param, plugin, player) -> plugin.getLuckPermsHook()
|
||||
.map(hook -> hook.getMeta(player.getPlayer(), param))
|
||||
.orElse(getPlaceholderFallback(plugin, "%luckperms_meta_" + param + "%")));
|
||||
|
||||
|
||||
private static final List<Placeholder> VALUES = Arrays.asList(values());
|
||||
private static final Map<String, Placeholder> BY_NAME = VALUES.stream().collect(Collectors.toMap(p -> p.name().toLowerCase(), Function.identity()));
|
||||
@Getter
|
||||
private static final List<Placeholder> PARAMETERISED = VALUES.stream().filter(p -> p.parameterised).toList();
|
||||
|
||||
/**
|
||||
* Function to replace placeholders with a real value
|
||||
*/
|
||||
private final TriFunction<String, Velocitab, TabPlayer, String> replacer;
|
||||
private final boolean parameterised;
|
||||
private final Pattern pattern;
|
||||
|
||||
Placeholder(@NotNull BiFunction<Velocitab, TabPlayer, String> replacer) {
|
||||
this.parameterised = false;
|
||||
this.replacer = (text, player, plugin) -> replacer.apply(player, plugin);
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "%");
|
||||
}
|
||||
|
||||
Placeholder(@NotNull TriFunction<String, Velocitab, TabPlayer, String> parameterisedReplacer) {
|
||||
this.parameterised = true;
|
||||
this.replacer = parameterisedReplacer;
|
||||
this.pattern = Pattern.compile("%" + this.name().toLowerCase() + "[^%]*%", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getPlaceholderFallback(@NotNull Velocitab plugin, @NotNull String fallback) {
|
||||
if (plugin.getPAPIProxyBridgeHook().isPresent() && plugin.getSettings().isFallbackToPapiIfPlaceholderBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static Optional<Placeholder> byName(@NotNull String name) {
|
||||
return Optional.ofNullable(BY_NAME.get(name.toLowerCase().replace("%", "")));
|
||||
}
|
||||
}
|
@ -0,0 +1,334 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.placeholder;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import lombok.Setter;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.player.Role;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class PlaceholderManager {
|
||||
|
||||
@Setter
|
||||
private boolean debug = false;
|
||||
|
||||
private static final String ELSE_PLACEHOLDER = "ELSE";
|
||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%.*?%", Pattern.DOTALL);
|
||||
private static final Pattern VELOCITAB_PLACEHOLDERS = Pattern.compile("<velocitab[^<>]*(?:<(?!v)[^<>]*>[^<>]*)*>");
|
||||
private static final Pattern VELOCITAB_REL_PLACEHOLDERS = Pattern.compile("<velocitab_rel[^<>]*(?:<(?!v)[^<>]*>[^<>]*)*>");
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final Map<UUID, Map<String, String>> placeholders;
|
||||
private final Map<UUID, Set<CompletableFuture<?>>> requests;
|
||||
private final Map<Group, List<String>> cachedTexts;
|
||||
private final Set<UUID> blocked;
|
||||
private final ConditionManager conditionManager;
|
||||
private Map<Group, Map<String, Map<String, String>>> placeholdersReplacements;
|
||||
|
||||
public PlaceholderManager(Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.placeholders = Maps.newConcurrentMap();
|
||||
this.requests = Maps.newConcurrentMap();
|
||||
this.blocked = Sets.newConcurrentHashSet();
|
||||
this.cachedTexts = Maps.newConcurrentMap();
|
||||
this.conditionManager = new ConditionManager(plugin);
|
||||
this.placeholdersReplacements = Maps.newConcurrentMap();
|
||||
this.preparePlaceholdersReplacements();
|
||||
}
|
||||
|
||||
public void preparePlaceholdersReplacements() {
|
||||
placeholdersReplacements = Maps.newConcurrentMap();
|
||||
for (Group group : plugin.getTabGroupsManager().getGroups()) {
|
||||
final Map<String, Map<String, String>> map = Maps.newHashMap();
|
||||
placeholdersReplacements.put(group, map);
|
||||
for (String placeholder : group.placeholderReplacements().keySet()) {
|
||||
final Map<String, String> repMap = Maps.newHashMap();
|
||||
map.put(placeholder, repMap);
|
||||
for (PlaceholderReplacement replacement : group.placeholderReplacements().get(placeholder)) {
|
||||
repMap.put(replacement.placeholder(), replacement.replacement());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchPlaceholders(@NotNull Group group) {
|
||||
final List<String> texts = cachedTexts.computeIfAbsent(group, g -> g.getTextsWithPlaceholders(plugin));
|
||||
group.getPlayersAsList(plugin).forEach(player -> fetchPlaceholders(player.getUniqueId(), texts, group));
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
cachedTexts.clear();
|
||||
}
|
||||
|
||||
public void fetchPlaceholders(@NotNull UUID uuid, @NotNull List<String> texts, @NotNull Group group) {
|
||||
final Player player = plugin.getServer().getPlayer(uuid).orElse(null);
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blocked.contains(uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, String> parsed = placeholders.computeIfAbsent(uuid, k -> Maps.newConcurrentMap());
|
||||
final TabPlayer tabPlayer = plugin.getTabList().getTabPlayer(player)
|
||||
.orElse(new TabPlayer(plugin, player,
|
||||
plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE),
|
||||
plugin.getTabList().getGroupOrDefault(player)));
|
||||
|
||||
final List<String> placeholders = texts.stream()
|
||||
.map(PlaceholderManager::extractPlaceholders)
|
||||
.flatMap(List::stream)
|
||||
.map(s -> s.replace("%target_", "%"))
|
||||
.toList();
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
|
||||
placeholders.forEach(placeholder -> replaceSingle(placeholder, plugin, tabPlayer)
|
||||
.ifPresentOrElse(replacement -> parsed.put(placeholder, replacement),
|
||||
() -> plugin.getPAPIProxyBridgeHook().ifPresent(hook -> {
|
||||
final CompletableFuture<String> future = hook.formatPlaceholders(placeholder, player);
|
||||
requests.computeIfAbsent(player.getUniqueId(), u -> Sets.newConcurrentHashSet()).add(future);
|
||||
future.thenAccept(replacement -> {
|
||||
if (replacement == null || replacement.equals(placeholder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blocked.contains(player.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
plugin.getLogger().info("Placeholder {} replaced with {} in {}ms", placeholder, replacement, System.currentTimeMillis() - start);
|
||||
}
|
||||
|
||||
final long diff = System.currentTimeMillis() - start;
|
||||
if (diff > group.placeholderUpdateRate()) {
|
||||
final long increase = diff + 100;
|
||||
plugin.getLogger().warn("""
|
||||
Placeholder {} took more than group placeholder update rate of {} ms to update. This may cause a thread leak.
|
||||
Please fix the issue of the plugin providing the placeholder.
|
||||
If you can't fix it, increase the placeholder update rate of the group to at least {} ms.
|
||||
"""
|
||||
, placeholder, group.placeholderUpdateRate(), increase);
|
||||
}
|
||||
|
||||
parsed.put(placeholder, replacement);
|
||||
requests.get(player.getUniqueId()).remove(future);
|
||||
});
|
||||
})));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String applyPlaceholders(@NotNull TabPlayer player, @NotNull String text) {
|
||||
final Map<String, String> parsed = placeholders.computeIfAbsent(player.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
|
||||
return applyPlaceholderReplacements(text, player, parsed);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String applyPlaceholders(@NotNull TabPlayer player, @NotNull String text, @NotNull TabPlayer viewer) {
|
||||
final Map<String, String> parsed = placeholders.computeIfAbsent(player.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
|
||||
final String applied = applyPlaceholderReplacements(text, player, parsed);
|
||||
|
||||
final Map<String, String> targetParsed = placeholders.computeIfAbsent(viewer.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
|
||||
return applyPlaceholderReplacements(applied.replace("%target_", "%"), viewer, targetParsed);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String applyViewerPlaceholders(@NotNull TabPlayer viewer, @NotNull String text) {
|
||||
final Map<String, String> parsed = placeholders.computeIfAbsent(viewer.getPlayer().getUniqueId(), uuid -> Maps.newConcurrentMap());
|
||||
return applyPlaceholderReplacements(text.replace("%target_", "%"), viewer, parsed);
|
||||
}
|
||||
|
||||
public void clearPlaceholders(@NotNull UUID uuid) {
|
||||
blocked.add(uuid);
|
||||
placeholders.remove(uuid);
|
||||
Optional.ofNullable(requests.get(uuid)).ifPresent(set -> set.forEach(c -> c.cancel(true)));
|
||||
}
|
||||
|
||||
public void unblockPlayer(@NotNull UUID uuid) {
|
||||
blocked.remove(uuid);
|
||||
requests.remove(uuid);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> extractPlaceholders(@NotNull String text) {
|
||||
final List<String> placeholders = Lists.newArrayList();
|
||||
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
|
||||
while (matcher.find()) {
|
||||
placeholders.add(matcher.group());
|
||||
}
|
||||
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private String getReplacement(@NotNull Group group, @NotNull String placeholder, @NotNull String text) {
|
||||
final Map<String, Map<String, String>> replacements = placeholdersReplacements.get(group);
|
||||
if (replacements == null) {
|
||||
return null;
|
||||
}
|
||||
final Map<String, String> replacementMap = replacements.get(placeholder);
|
||||
if (replacementMap == null) {
|
||||
return null;
|
||||
}
|
||||
final String replacement = replacementMap.get(text);
|
||||
if (replacement == null) {
|
||||
return replacementMap.get(ELSE_PLACEHOLDER);
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String applyPlaceholderReplacements(@NotNull String text, @NotNull TabPlayer player,
|
||||
@NotNull Map<String, String> parsed) {
|
||||
for (final Map.Entry<String, List<PlaceholderReplacement>> entry : player.getGroup().placeholderReplacements().entrySet()) {
|
||||
final String replaced = parsed.get(entry.getKey());
|
||||
final String replacement = getReplacement(player.getGroup(), entry.getKey(), replaced);
|
||||
if (replacement != null) {
|
||||
text = text.replace(entry.getKey(), replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return applyPlaceholders(text, parsed);
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
private String applyPlaceholders(@NotNull String text, @NotNull Map<String, String> replacements) {
|
||||
for (Map.Entry<String, String> entry : replacements.entrySet()) {
|
||||
text = text.replace(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public Optional<String> getCachedPlaceholderValue(@NotNull String text, @NotNull UUID uuid) {
|
||||
if (!placeholders.containsKey(uuid)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(placeholders.get(uuid).get(text));
|
||||
}
|
||||
|
||||
private Optional<String> replaceSingle(@NotNull String placeholder, @NotNull Velocitab plugin, @NotNull TabPlayer player) {
|
||||
final Optional<Placeholder> optionalPlaceholder = Placeholder.byName(placeholder);
|
||||
if (optionalPlaceholder.isEmpty()) {
|
||||
//check if it's parameterised
|
||||
for (Placeholder placeholderType : Placeholder.getPARAMETERISED()) {
|
||||
final java.util.regex.Matcher matcher = placeholderType.getPattern().matcher(placeholder);
|
||||
if (matcher.find()) {
|
||||
final String s = chop(matcher.group().replace("%" + placeholderType.name().toLowerCase(), "")
|
||||
.replaceFirst("_", ""));
|
||||
return Optional.of(placeholderType.getReplacer().apply(s, plugin, player));
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (optionalPlaceholder.get().isParameterised()) {
|
||||
throw new IllegalArgumentException("Placeholder " + placeholder + " is parameterised");
|
||||
}
|
||||
|
||||
final Placeholder placeholderType = optionalPlaceholder.get();
|
||||
return Optional.of(placeholderType.getReplacer().apply(null, plugin, player));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String chop(@NotNull String text) {
|
||||
int strLen = text.length();
|
||||
if (strLen < 2) {
|
||||
return "";
|
||||
} else {
|
||||
int lastIdx = strLen - 1;
|
||||
String ret = text.substring(0, lastIdx);
|
||||
char last = text.charAt(lastIdx);
|
||||
return last == '\n' && ret.charAt(lastIdx - 1) == '\r' ? ret.substring(0, lastIdx - 1) : ret;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String formatVelocitabPlaceholders(@NotNull String text, @NotNull TabPlayer player, @Nullable TabPlayer viewer) {
|
||||
final Matcher matcher = VELOCITAB_PLACEHOLDERS.matcher(text);
|
||||
if (!matcher.find()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
final StringBuilder result = new StringBuilder(text.length());
|
||||
int lastEnd = 0;
|
||||
|
||||
do {
|
||||
String placeholder = matcher.group();
|
||||
String cleanedPlaceholder = placeholder.substring(1, placeholder.length() - 1);
|
||||
|
||||
String replacement;
|
||||
try {
|
||||
replacement = conditionManager.handleVelocitabPlaceholders(cleanedPlaceholder, player, viewer);
|
||||
if (replacement.equals(cleanedPlaceholder)) {
|
||||
continue;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warn("Failed to calculate condition for {}", cleanedPlaceholder, e);
|
||||
replacement = placeholder;
|
||||
}
|
||||
|
||||
result.append(text, lastEnd, matcher.start()).append(replacement);
|
||||
lastEnd = matcher.end();
|
||||
} while (matcher.find());
|
||||
|
||||
result.append(text.substring(lastEnd));
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String stripVelocitabRelPlaceholders(@NotNull String text) {
|
||||
final Matcher matcher = VELOCITAB_REL_PLACEHOLDERS.matcher(text);
|
||||
final StringBuilder result = new StringBuilder();
|
||||
int lastEnd = 0;
|
||||
while (matcher.find()) {
|
||||
String placeholder = matcher.group();
|
||||
String cleanedPlaceholder = placeholder.substring(1, placeholder.length() - 1);
|
||||
try {
|
||||
String replacement = "";
|
||||
result.append(text, lastEnd, matcher.start()).append(replacement);
|
||||
lastEnd = matcher.end();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warn("Failed to calculate condition for {}", cleanedPlaceholder, e);
|
||||
}
|
||||
}
|
||||
result.append(text.substring(lastEnd));
|
||||
return result.toString();
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.config;
|
||||
package net.william278.velocitab.placeholder;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
@ -30,7 +30,7 @@ import java.util.Optional;
|
||||
@RequiredArgsConstructor
|
||||
public class Role implements Comparable<Role> {
|
||||
|
||||
public static final int DEFAULT_WEIGHT = -1;
|
||||
public static final int DEFAULT_WEIGHT = 0;
|
||||
public static final Role DEFAULT_ROLE = new Role(DEFAULT_WEIGHT, null, null, null, null);
|
||||
@Getter
|
||||
private final int weight;
|
||||
@ -64,12 +64,8 @@ public class Role implements Comparable<Role> {
|
||||
return Optional.ofNullable(suffix);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Optional<String> getWeightString() {
|
||||
if (weight == -1) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(Integer.toString(weight));
|
||||
protected String getWeightString() {
|
||||
return Integer.toString(weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,7 +20,6 @@
|
||||
package net.william278.velocitab.player;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@ -28,29 +27,20 @@ import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.packet.UpdateTeamsPacket;
|
||||
import net.william278.velocitab.tab.Nametag;
|
||||
import net.william278.velocitab.tab.PlayerTabList;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
|
||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("%([^%]+)%");
|
||||
private static final String PLACEHOLDER_DELIMITER = "<-DELIMITER->";
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final Player player;
|
||||
@Setter
|
||||
@ -60,9 +50,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
// Each TabPlayer contains the components for each TabPlayer it's currently viewing this player
|
||||
private final Map<UUID, Component> relationalDisplayNames;
|
||||
private final Map<UUID, Component[]> relationalNametags;
|
||||
private final Map<String, String> cachedPlaceholders;
|
||||
private final Map<UUID, Integer> cachedListOrders;
|
||||
private String lastDisplayName;
|
||||
private Component lastHeader;
|
||||
private Component lastFooter;
|
||||
private String teamName;
|
||||
@ -91,12 +78,10 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
this.group = group;
|
||||
this.relationalDisplayNames = Maps.newConcurrentMap();
|
||||
this.relationalNametags = Maps.newConcurrentMap();
|
||||
this.cachedPlaceholders = Maps.newConcurrentMap();
|
||||
this.cachedListOrders = Maps.newConcurrentMap();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Optional<String> getRoleWeightString() {
|
||||
public String getRoleWeightString() {
|
||||
return getRole().getWeightString();
|
||||
}
|
||||
|
||||
@ -110,7 +95,7 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
public String getServerName() {
|
||||
return player.getCurrentServer()
|
||||
.map(serverConnection -> serverConnection.getServerInfo().getName())
|
||||
.orElse(ObjectUtils.firstNonNull(lastServer, "unknown"));
|
||||
.orElse(lastServer != null ? lastServer : "unknown");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -120,77 +105,31 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
* @return The ordinal position of the server group
|
||||
*/
|
||||
public int getServerGroupPosition(@NotNull Velocitab plugin) {
|
||||
return plugin.getTabGroups().getPosition(group);
|
||||
return plugin.getTabGroupsManager().getGroupPosition(group);
|
||||
}
|
||||
|
||||
public Nametag getNametag(@NotNull Velocitab plugin) {
|
||||
final String prefix = plugin.getPlaceholderManager().applyPlaceholders(this, group.nametag().prefix());
|
||||
final String suffix = plugin.getPlaceholderManager().applyPlaceholders(this, group.nametag().suffix());
|
||||
return new Nametag(prefix, suffix);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<String> getDisplayName(@NotNull Velocitab plugin) {
|
||||
final String format = formatGroup();
|
||||
return Placeholder.replace(format, plugin, this)
|
||||
.thenApply(d -> cacheDisplayName(d, format));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String formatGroup() {
|
||||
final Set<String> placeholders = Sets.newHashSet();
|
||||
final Matcher matcher = PLACEHOLDER_PATTERN.matcher(group.format());
|
||||
while (matcher.find()) {
|
||||
placeholders.add("%" + matcher.group(1) + "%");
|
||||
}
|
||||
|
||||
return String.join(PLACEHOLDER_DELIMITER, placeholders);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String cacheDisplayName(@NotNull String placeholders, @NotNull String keys) {
|
||||
String displayName = group.format();
|
||||
final String[] placeholderArray = placeholders.split(PLACEHOLDER_DELIMITER);
|
||||
final String[] keyArray = keys.split(PLACEHOLDER_DELIMITER);
|
||||
|
||||
for (int i = 0; i < placeholderArray.length; i++) {
|
||||
final String placeholder = keyArray[i];
|
||||
final String value = placeholderArray[i];
|
||||
cachedPlaceholders.put(placeholder, value);
|
||||
displayName = displayName.replace(placeholder, value);
|
||||
}
|
||||
|
||||
displayName = Placeholder.replaceInternal(displayName, plugin, this).first();
|
||||
return lastDisplayName = displayName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<Nametag> getNametag(@NotNull Velocitab plugin) {
|
||||
return Placeholder.replace(group.nametag(), plugin, this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<String> getTeamName(@NotNull Velocitab plugin) {
|
||||
return plugin.getSortingManager().getTeamName(this)
|
||||
.thenApply(teamName -> this.teamName = teamName);
|
||||
public String getTeamName(@NotNull Velocitab plugin) {
|
||||
final String teamName = plugin.getSortingManager().getTeamName(this);
|
||||
return this.teamName = teamName;
|
||||
}
|
||||
|
||||
public Optional<String> getLastTeamName() {
|
||||
return Optional.ofNullable(teamName);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
|
||||
return tabList.getHeader(this).thenCompose(header -> tabList.getFooter(this).thenAccept(footer -> {
|
||||
final boolean disabled = plugin.getSettings().isDisableHeaderFooterIfEmpty();
|
||||
if (disabled) {
|
||||
if ((!Component.empty().equals(header) && !header.equals(lastHeader)) ||
|
||||
(!Component.empty().equals(footer) && !footer.equals(lastFooter))) {
|
||||
lastHeader = header;
|
||||
lastFooter = footer;
|
||||
player.sendPlayerListHeaderAndFooter(header, footer);
|
||||
}
|
||||
} else {
|
||||
if (!header.equals(lastHeader) || !footer.equals(lastFooter)) {
|
||||
lastHeader = header;
|
||||
lastFooter = footer;
|
||||
player.sendPlayerListHeaderAndFooter(header, footer);
|
||||
}
|
||||
}
|
||||
}));
|
||||
public void sendHeaderAndFooter(@NotNull PlayerTabList tabList) {
|
||||
final Component header = tabList.getHeader(this);
|
||||
final Component footer = tabList.getFooter(this);
|
||||
lastHeader = header;
|
||||
lastFooter = footer;
|
||||
player.sendPlayerListHeaderAndFooter(header, footer);
|
||||
}
|
||||
|
||||
public void incrementIndexes() {
|
||||
@ -232,10 +171,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
relationalNametags.remove(target);
|
||||
}
|
||||
|
||||
public void unsetTabListOrder(@NotNull UUID target) {
|
||||
cachedListOrders.remove(target);
|
||||
}
|
||||
|
||||
public Optional<Component[]> getRelationalNametag(@NotNull UUID target) {
|
||||
return Optional.ofNullable(relationalNametags.get(target));
|
||||
}
|
||||
@ -248,7 +183,6 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
lastFooter = null;
|
||||
role = Role.DEFAULT_ROLE;
|
||||
teamName = null;
|
||||
cachedListOrders.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -273,8 +207,4 @@ public final class TabPlayer implements Comparable<TabPlayer> {
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
|
||||
}
|
||||
|
||||
public Optional<String> getCachedPlaceholderValue(@NotNull String placeholder) {
|
||||
return Optional.ofNullable(cachedPlaceholders.get(placeholder));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.providers;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import net.william278.toilet.DumpOptions;
|
||||
import net.william278.toilet.Toilet;
|
||||
import net.william278.toilet.dump.*;
|
||||
import net.william278.toilet.velocity.VelocityToilet;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.hook.Hook;
|
||||
import net.william278.velocitab.util.DebugSystem;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static net.william278.toilet.DumpOptions.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface DumpProvider {
|
||||
|
||||
@NotNull PluginInfo.Label VANISH_INCOMPATIBLE = new PluginInfo.Label("Vanish Incompatible", "#bd3b01");
|
||||
|
||||
@NotNull Map<String, PluginInfo.Label> INCOMPATIBLE_PLUGINS = Map.ofEntries(
|
||||
Map.entry("tab", PluginInfo.INCOMPATIBLE_LABEL),
|
||||
Map.entry("premiumvanish", VANISH_INCOMPATIBLE)
|
||||
);
|
||||
|
||||
@NotNull List<Integer> COLORS = Arrays.asList(
|
||||
Color.RED.getRGB(),
|
||||
Color.GREEN.getRGB(),
|
||||
Color.BLUE.getRGB(),
|
||||
Color.YELLOW.getRGB(),
|
||||
Color.ORANGE.getRGB(),
|
||||
Color.PINK.getRGB(),
|
||||
Color.CYAN.getRGB(),
|
||||
Color.MAGENTA.getRGB(),
|
||||
Color.GRAY.getRGB(),
|
||||
Color.LIGHT_GRAY.getRGB(),
|
||||
Color.DARK_GRAY.getRGB(),
|
||||
Color.BLACK.getRGB(),
|
||||
Color.WHITE.getRGB(),
|
||||
new Color(255, 165, 0).getRGB(), // Orange
|
||||
new Color(128, 0, 128).getRGB(), // Purple
|
||||
new Color(0, 128, 128).getRGB(), // Teal
|
||||
new Color(128, 128, 0).getRGB(), // Olive
|
||||
new Color(255, 192, 203).getRGB(), // Pink
|
||||
new Color(0, 255, 255).getRGB(), // Aqua
|
||||
new Color(255, 215, 0).getRGB() // Gold
|
||||
);
|
||||
|
||||
@NotNull String BYTEBIN_URL = "https://bytebin.lucko.me";
|
||||
@NotNull String VIEWER_URL = "https://william278.net/dump";
|
||||
|
||||
@NotNull
|
||||
Toilet getToilet();
|
||||
|
||||
void setToilet(@NotNull Toilet toilet);
|
||||
|
||||
default void initializeToilet() {
|
||||
final Toilet toilet = VelocityToilet.create(getDumpOptions(), getPlugin().getServer());
|
||||
setToilet(toilet);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
default String createDump(@NotNull CommandSource u) {
|
||||
return getToilet().dump(getPluginStatus(), u instanceof Player o
|
||||
? new DumpUser(o.getUsername(), o.getUniqueId()) : null, getDebugLog()).toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default DumpOptions getDumpOptions() {
|
||||
return builder()
|
||||
.bytebinUrl(BYTEBIN_URL)
|
||||
.viewerUrl(VIEWER_URL)
|
||||
.projectMeta(ProjectMeta.builder()
|
||||
.id("velocitab")
|
||||
.name("Velocitab")
|
||||
.version(getPlugin().getVersion().toString())
|
||||
.md5("unknown")
|
||||
.author("William278, AlexDev03")
|
||||
.sourceCode("https://github.com/WiIIiam278/Velocitab")
|
||||
.website("https://william278.net/project/velocitab")
|
||||
.support("https://discord.gg/tVYhJfyDWG")
|
||||
.build())
|
||||
.compatibilityRules(getCompatibilityRules())
|
||||
.fileInclusionRules(getFileInclusionRules())
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ExtraFile getDebugLog() {
|
||||
return new ExtraFile("debug-log", "Internal Debugger", DebugSystem.getLogsAsString());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<FileInclusionRule> getFileInclusionRules() {
|
||||
final List<File> tabGroupsFiles = getPlugin().getTabGroupsManager().getGroupsFiles();
|
||||
final List<FileInclusionRule> rules = Lists.newArrayList();
|
||||
rules.add(FileInclusionRule.configFile(getPlugin().getConfigDirectory().resolve("config.yml").toFile().getAbsolutePath(), "Config File"));
|
||||
|
||||
for (File tabGroupsFile : tabGroupsFiles) {
|
||||
final boolean isDefault = getPlugin().getTabGroupsManager().isDefaultFile(tabGroupsFile);
|
||||
final String name = "Tab Groups File (" + (isDefault ? "default" : tabGroupsFile.getName()) + ")";
|
||||
final FileInclusionRule rule = FileInclusionRule.configFile(tabGroupsFile.getAbsolutePath(), name);
|
||||
if (isDefault) {
|
||||
rules.add(1, rule);
|
||||
} else {
|
||||
rules.add(rule);
|
||||
}
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<CompatibilityRule> getCompatibilityRules() {
|
||||
return INCOMPATIBLE_PLUGINS.entrySet().stream()
|
||||
.filter(e -> getPlugin().getServer().getPluginManager().getPlugin(e.getKey()).isPresent())
|
||||
.map(e -> CompatibilityRule.builder()
|
||||
.resourceName(e.getKey())
|
||||
.labelToApply(e.getValue())
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
private PluginStatus getPluginStatus() {
|
||||
return PluginStatus.builder()
|
||||
.blocks(List.of(getSystemStatus(), getServersInEachGroup(), getPlayersInEachGroup(), getHookStatus()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private PluginStatus.MapStatusBlock getSystemStatus() {
|
||||
return new PluginStatus.MapStatusBlock(
|
||||
Map.ofEntries(
|
||||
Map.entry("RemoveNameTags", Boolean.toString(getPlugin().getSettings().isRemoveNametags())),
|
||||
Map.entry("DisableHeaderFooterIfEmpty", Boolean.toString(getPlugin().getSettings().isDisableHeaderFooterIfEmpty())),
|
||||
Map.entry("Formatter", getPlugin().getSettings().getFormatter().name()),
|
||||
Map.entry("FallbackGroupEnabled", Boolean.toString(getPlugin().getSettings().isFallbackEnabled())),
|
||||
Map.entry("FallbackGroup", getPlugin().getSettings().getFallbackGroup()),
|
||||
Map.entry("PapiProxyBridge", Boolean.toString(getPlugin().getSettings().isEnablePapiHook())),
|
||||
Map.entry("PapiCacheTime", Long.toString(getPlugin().getSettings().getPapiCacheTime())),
|
||||
Map.entry("MiniPlaceholders", Boolean.toString(getPlugin().getSettings().isEnableMiniPlaceholdersHook())),
|
||||
Map.entry("SendScoreboardPackets", Boolean.toString(getPlugin().getSettings().isSendScoreboardPackets())),
|
||||
Map.entry("SortPlayers", Boolean.toString(getPlugin().getSettings().isSortPlayers())),
|
||||
Map.entry("RelationalPlaceholders", Boolean.toString(getPlugin().getSettings().isEnableRelationalPlaceholders())),
|
||||
Map.entry("VanishIntegration", getPlugin().getVanishManager().getIntegration().getClass().getName())
|
||||
),
|
||||
"Plugin Status", "fa6-solid:wrench"
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private PluginStatus.ListStatusBlock getHookStatus() {
|
||||
return new PluginStatus.ListStatusBlock(
|
||||
getPlugin().getHooks().stream().map(Hook::getName).toList(),
|
||||
"Loaded Hooks", "fa6-solid:plug"
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private PluginStatus.ChartStatusBlock getPlayersInEachGroup() {
|
||||
final AtomicInteger colorIndex = new AtomicInteger(0);
|
||||
final Map<PluginStatus.ChartKey, Integer> players = getPlugin().getTabGroupsManager().getGroups().stream()
|
||||
.collect(Collectors.toMap(
|
||||
g -> new PluginStatus.ChartKey(g.name(), "fa6-solid:server", COLORS.get(colorIndex.getAndIncrement() % COLORS.size())),
|
||||
group -> group.getTabPlayersAsList(getPlugin()).size()
|
||||
));
|
||||
return new PluginStatus.ChartStatusBlock(
|
||||
players,
|
||||
PluginStatus.ChartType.PIE,
|
||||
"Online players per group",
|
||||
"fa6-solid:users"
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private PluginStatus.MapStatusBlock getServersInEachGroup() {
|
||||
final Map<String, String> servers = getPlugin().getTabGroupsManager().getGroups().stream()
|
||||
.sorted(getGroupComparator(getPlugin()).reversed())
|
||||
.collect(Collectors.toMap(
|
||||
Group::name,
|
||||
g -> g.registeredServers(getPlugin()).stream()
|
||||
.map(RegisteredServer::getServerInfo)
|
||||
.map(ServerInfo::getName)
|
||||
.collect(Collectors.joining(", ")),
|
||||
(e1, e2) -> e1,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
return new PluginStatus.MapStatusBlock(
|
||||
servers,
|
||||
"Servers in each group",
|
||||
"fa6-solid:network-wired"
|
||||
);
|
||||
}
|
||||
|
||||
default Comparator<Group> getGroupComparator(@NotNull Velocitab plugin) {
|
||||
return (g1, g2) -> {
|
||||
final int servers1 = g1.registeredServers(plugin).size();
|
||||
final int servers2 = g2.registeredServers(plugin).size();
|
||||
if (servers1 != servers2) {
|
||||
return servers1 - servers2;
|
||||
}
|
||||
return g1.servers().size() - g2.servers().size();
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Velocitab getPlugin();
|
||||
}
|
@ -91,7 +91,7 @@ public interface ScoreboardProvider {
|
||||
setTabList(tabList);
|
||||
getPlugin().getServer().getEventManager().register(this, tabList);
|
||||
|
||||
getPlugin().getServer().getScheduler().buildTask(this, tabList::load).delay(1, TimeUnit.SECONDS).schedule();
|
||||
getPlugin().getServer().getScheduler().buildTask(this, tabList::load).delay(250, TimeUnit.MILLISECONDS).schedule();
|
||||
|
||||
final SortingManager sortingManager = new SortingManager(getPlugin());
|
||||
setSortingManager(sortingManager);
|
||||
|
@ -20,12 +20,15 @@
|
||||
package net.william278.velocitab.sorting;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
|
||||
@ToString
|
||||
public class SortedSet {
|
||||
|
||||
private final ConcurrentSkipListSet<String> sortedTeams;
|
||||
@ -36,39 +39,37 @@ public class SortedSet {
|
||||
positionMap = Maps.newConcurrentMap();
|
||||
}
|
||||
|
||||
public synchronized boolean addTeam(@NotNull String teamName) {
|
||||
final boolean result = sortedTeams.add(teamName);
|
||||
if (!result) {
|
||||
public boolean addTeam(@NotNull String teamName) {
|
||||
if (!sortedTeams.add(teamName)) {
|
||||
return false;
|
||||
}
|
||||
updatePositions();
|
||||
updatePositions(teamName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean removeTeam(@NotNull String teamName) {
|
||||
final boolean result = sortedTeams.remove(teamName);
|
||||
if (!result) {
|
||||
public boolean removeTeam(@NotNull String teamName) {
|
||||
if (!sortedTeams.remove(teamName)) {
|
||||
return false;
|
||||
}
|
||||
updatePositions();
|
||||
updatePositions(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private synchronized void updatePositions() {
|
||||
int index = 0;
|
||||
positionMap.clear();
|
||||
for (final String team : sortedTeams) {
|
||||
positionMap.put(team, index);
|
||||
index++;
|
||||
private void updatePositions(@Nullable String newTeam) {
|
||||
if (newTeam != null) {
|
||||
int newPosition = sortedTeams.headSet(newTeam).size();
|
||||
positionMap.put(newTeam, newPosition);
|
||||
sortedTeams.tailSet(newTeam).forEach(team -> positionMap.put(team, sortedTeams.headSet(team).size()));
|
||||
} else {
|
||||
int index = 0;
|
||||
positionMap.clear();
|
||||
for (String team : sortedTeams) {
|
||||
positionMap.put(team, index++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized int getPosition(@NotNull String teamName) {
|
||||
public int getPosition(@NotNull String teamName) {
|
||||
return positionMap.getOrDefault(teamName, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return sortedTeams.toString();
|
||||
}
|
||||
}
|
||||
|
@ -22,20 +22,16 @@ package net.william278.velocitab.sorting;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SortingManager {
|
||||
|
||||
private final Velocitab plugin;
|
||||
private static final String DELIMITER = ":::";
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("^-?[0-9]\\d*(\\.\\d+)?$");
|
||||
|
||||
public SortingManager(@NotNull Velocitab plugin) {
|
||||
@ -43,15 +39,18 @@ public class SortingManager {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<String> getTeamName(@NotNull TabPlayer player) {
|
||||
public String getTeamName(@NotNull TabPlayer player) {
|
||||
if (!plugin.getSettings().isSortPlayers()) {
|
||||
return CompletableFuture.completedFuture("");
|
||||
return "";
|
||||
}
|
||||
|
||||
return Placeholder.replace(String.join(DELIMITER, player.getGroup().sortingPlaceholders()), plugin, player)
|
||||
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
|
||||
.thenApply(v -> v.stream().map(s -> adaptValue(s, player)).collect(Collectors.toList()))
|
||||
.thenApply(v -> handleList(player, v));
|
||||
final List<String> placeholders = player.getGroup().sortingPlaceholders()
|
||||
.stream()
|
||||
.map(s -> plugin.getPlaceholderManager().applyPlaceholders(player, s))
|
||||
.map(s -> adaptValue(s, player))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return handleList(player, placeholders);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
@ -21,7 +21,6 @@ package net.william278.velocitab.tab;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -32,13 +31,13 @@ public record Nametag(@NotNull String prefix, @NotNull String suffix) {
|
||||
|
||||
@NotNull
|
||||
public Component getPrefixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
|
||||
final String formatted = Placeholder.replaceInternal(prefix, plugin, tabPlayer).first();
|
||||
final String formatted = plugin.getPlaceholderManager().applyPlaceholders(tabPlayer, prefix, target);
|
||||
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getSuffixComponent(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer, @NotNull TabPlayer target) {
|
||||
final String formatted = Placeholder.replaceInternal(suffix, plugin, tabPlayer).first();
|
||||
final String formatted = plugin.getPlaceholderManager().applyPlaceholders(tabPlayer, suffix, target);
|
||||
return plugin.getFormatter().format(formatted, tabPlayer, target, plugin);
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,9 @@ import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.player.TabList;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.util.ServerLink;
|
||||
import com.velocitypowered.proxy.tablist.KeyedVelocityTabList;
|
||||
import com.velocitypowered.proxy.tablist.VelocityTabList;
|
||||
import lombok.AccessLevel;
|
||||
@ -37,33 +37,32 @@ import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.api.PlayerAddedToTabEvent;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.config.Placeholder;
|
||||
import net.william278.velocitab.config.ServerUrl;
|
||||
import net.william278.velocitab.packet.ScoreboardManager;
|
||||
import net.william278.velocitab.player.Role;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.util.DebugSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER;
|
||||
|
||||
/**
|
||||
* The main class for tracking the server TAB list for a map of {@link TabPlayer}s
|
||||
*/
|
||||
public class PlayerTabList {
|
||||
|
||||
private static final String RELATIONAL_PERMISSION = "velocitab.relational";
|
||||
|
||||
private final Velocitab plugin;
|
||||
@Getter
|
||||
private final VanishTabList vanishTabList;
|
||||
@Getter(value = AccessLevel.PUBLIC)
|
||||
private final Map<UUID, TabPlayer> players;
|
||||
@Getter(value = AccessLevel.PROTECTED)
|
||||
private final TaskManager taskManager;
|
||||
private final Map<Class<?>, Field> entriesFields;
|
||||
|
||||
@ -73,7 +72,6 @@ public class PlayerTabList {
|
||||
this.players = Maps.newConcurrentMap();
|
||||
this.taskManager = new TaskManager(plugin);
|
||||
this.entriesFields = Maps.newHashMap();
|
||||
this.reloadUpdate();
|
||||
this.registerListener();
|
||||
this.ensureDisplayNameTask();
|
||||
this.registerFields();
|
||||
@ -140,8 +138,26 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
joinPlayer(p, group.get());
|
||||
loadPlayer(p, group.get(), 400);
|
||||
});
|
||||
|
||||
reloadUpdate();
|
||||
}
|
||||
|
||||
protected void loadPlayer(@NotNull Player player, @NotNull Group group, int delay) {
|
||||
final ScheduledTask task = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> plugin.getPlaceholderManager().fetchPlaceholders(player.getUniqueId(), group.getTextsWithPlaceholders(plugin), group))
|
||||
.delay(150, TimeUnit.MILLISECONDS)
|
||||
.repeat(50, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
//After updating papiproxybridge we can check if redis is used
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
task.cancel();
|
||||
joinPlayer(player, group);
|
||||
})
|
||||
.delay(delay, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,7 +165,7 @@ public class PlayerTabList {
|
||||
* Removes the player's entry from the tab list of all other players on the same group servers.
|
||||
*/
|
||||
public void close() {
|
||||
taskManager.cancelAllTasks();
|
||||
taskManager.close();
|
||||
plugin.getServer().getAllPlayers().forEach(p -> {
|
||||
final Optional<ServerConnection> server = p.getCurrentServer();
|
||||
if (server.isEmpty()) return;
|
||||
@ -167,13 +183,13 @@ public class PlayerTabList {
|
||||
serversInGroup.remove(server.get().getServer());
|
||||
serversInGroup.forEach(s -> s.getPlayersConnected().forEach(t -> t.getTabList().removeEntry(p.getUniqueId())));
|
||||
});
|
||||
plugin.getPacketEventManager().removeAllPlayers();
|
||||
}
|
||||
|
||||
protected void clearCachedData(@NotNull Player player) {
|
||||
players.values().forEach(p -> {
|
||||
p.unsetRelationalDisplayName(player.getUniqueId());
|
||||
p.unsetRelationalNametag(player.getUniqueId());
|
||||
p.unsetTabListOrder(player.getUniqueId());
|
||||
});
|
||||
}
|
||||
|
||||
@ -186,6 +202,7 @@ public class PlayerTabList {
|
||||
tabPlayerOptional.get().setGroup(group);
|
||||
tabPlayerOptional.get().setRole(plugin.getLuckPermsHook().map(hook -> hook.getPlayerRole(joined)).orElse(Role.DEFAULT_ROLE));
|
||||
}
|
||||
|
||||
final TabPlayer tabPlayer = tabPlayerOptional.orElseGet(() -> createTabPlayer(joined, group));
|
||||
final String serverName = getServerName(joined);
|
||||
// Store last server, so it's possible to have the last server on disconnect
|
||||
@ -194,19 +211,7 @@ public class PlayerTabList {
|
||||
// Send server URLs (1.21 clients)
|
||||
sendPlayerServerLinks(tabPlayer);
|
||||
|
||||
// Set the player as not loaded until the display name is set
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(d -> {
|
||||
if (d == null) {
|
||||
plugin.log(Level.ERROR, "Failed to get display name for " + joined.getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
handleDisplayLoad(tabPlayer);
|
||||
}).exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to set display name for %s (UUID: %s)",
|
||||
joined.getUsername(), joined.getUniqueId()), throwable);
|
||||
return null;
|
||||
});
|
||||
handleDisplayLoad(tabPlayer);
|
||||
}
|
||||
|
||||
private void handleDisplayLoad(@NotNull TabPlayer tabPlayer) {
|
||||
@ -214,20 +219,14 @@ public class PlayerTabList {
|
||||
final Group group = tabPlayer.getGroup();
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername());
|
||||
players.putIfAbsent(joined.getUniqueId(), tabPlayer);
|
||||
tabPlayer.sendHeaderAndFooter(this)
|
||||
.thenAccept(v -> tabPlayer.setLoaded(true))
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.ERROR, String.format("Failed to send header and footer for %s (UUID: %s)",
|
||||
joined.getUsername(), joined.getUniqueId()), throwable);
|
||||
return null;
|
||||
});
|
||||
|
||||
final Set<TabPlayer> tabPlayers = group.getTabPlayers(plugin, tabPlayer);
|
||||
tabPlayer.sendHeaderAndFooter(this);
|
||||
tabPlayer.setLoaded(true);
|
||||
final List<TabPlayer> tabPlayers = group.getTabPlayersAsList(plugin, tabPlayer);
|
||||
updateTabListOnJoin(tabPlayer, group, tabPlayers, isVanished);
|
||||
}
|
||||
|
||||
private void updateTabListOnJoin(@NotNull TabPlayer tabPlayer, @NotNull Group group,
|
||||
@NotNull Set<TabPlayer> tabPlayers, boolean isJoinedVanished) {
|
||||
@NotNull List<TabPlayer> tabPlayers, boolean isJoinedVanished) {
|
||||
final Player joined = tabPlayer.getPlayer();
|
||||
final String serverName = getServerName(joined);
|
||||
final Set<UUID> uuids = tabPlayers.stream().map(p -> p.getPlayer().getUniqueId()).collect(Collectors.toSet());
|
||||
@ -268,11 +267,11 @@ public class PlayerTabList {
|
||||
final String observableUsername = observableTabPlayer.getPlayer().getUsername();
|
||||
final TabList observableTabPlayerTabList = observableTabPlayer.getPlayer().getTabList();
|
||||
|
||||
if (isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
|
||||
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) {
|
||||
if ((isObservablePlayerVanished && !plugin.getVanishManager().canSee(observableUsername, observedUsername) &&
|
||||
!observableUUID.equals(observedPlayer.getPlayer().getUniqueId())) || !observedPlayer.getPlayer().isActive()) {
|
||||
observableTabPlayerTabList.removeEntry(observedPlayer.getPlayer().getUniqueId());
|
||||
} else {
|
||||
updateDisplayName(observedPlayer, observableTabPlayer);
|
||||
calculateAndSetDisplayName(observedPlayer, observableTabPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,18 +283,14 @@ public class PlayerTabList {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
|
||||
@NotNull Component single, @NotNull String toParse) {
|
||||
if (plugin.getMiniPlaceholdersHook().isEmpty()) {
|
||||
return single;
|
||||
}
|
||||
public Component formatRelationalComponent(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
|
||||
@NotNull String toParse) {
|
||||
return plugin.getFormatter().format(toParse, player, viewer, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component getRelationalPlaceholder(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull String toParse) {
|
||||
final Component single = plugin.getFormatter().format(toParse, player, viewer, plugin);
|
||||
return getRelationalPlaceholder(player, viewer, single, toParse);
|
||||
public Component formatComponent(@NotNull TabPlayer player, @NotNull String toParse) {
|
||||
return plugin.getFormatter().format(toParse, player, plugin);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -317,10 +312,6 @@ public class PlayerTabList {
|
||||
}
|
||||
}
|
||||
|
||||
protected void removePlayer(@NotNull Player target) {
|
||||
removePlayer(target, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a player from the tab list
|
||||
*
|
||||
@ -332,29 +323,30 @@ public class PlayerTabList {
|
||||
));
|
||||
}
|
||||
|
||||
protected void removePlayer(@NotNull Player target, @Nullable RegisteredServer server) {
|
||||
protected void removePlayer(@NotNull Player target) {
|
||||
final UUID uuid = target.getUniqueId();
|
||||
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
|
||||
final Optional<TabPlayer> tabPlayer = getTabPlayer(target.getUniqueId());
|
||||
if (tabPlayer.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Group group = tabPlayer.get().getGroup();
|
||||
tabPlayer.get().setLoaded(false);
|
||||
|
||||
final Set<Player> currentServerPlayers = Optional.ofNullable(server)
|
||||
.map(RegisteredServer::getPlayersConnected)
|
||||
.map(HashSet::new)
|
||||
.orElseGet(HashSet::new);
|
||||
currentServerPlayers.add(target);
|
||||
|
||||
// Update the tab list of all players
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> getPlayers().values().stream()
|
||||
.filter(p -> currentServerPlayers.isEmpty() || !currentServerPlayers.contains(p.getPlayer()))
|
||||
.forEach(player -> {
|
||||
player.getPlayer().getTabList().removeEntry(uuid);
|
||||
player.sendHeaderAndFooter(this);
|
||||
updatePlayerDisplayName(player);
|
||||
}))
|
||||
.buildTask(plugin, () -> {
|
||||
final List<TabPlayer> list = group.getTabPlayersAsList(plugin);
|
||||
list.forEach(player -> {
|
||||
player.getPlayer().getTabList().removeEntry(uuid);
|
||||
player.sendHeaderAndFooter(this);
|
||||
});
|
||||
})
|
||||
.delay(250, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
|
||||
// Delete player team
|
||||
plugin.getScoreboardManager().resetCache(target);
|
||||
|
||||
//remove player from tab list cache
|
||||
getPlayers().remove(uuid);
|
||||
}
|
||||
@ -374,8 +366,9 @@ public class PlayerTabList {
|
||||
if (!viewer.getPlayer().getTabList().equals(tabList)) {
|
||||
throw new IllegalArgumentException("TabList of viewer is not the same as the TabList of the entry");
|
||||
}
|
||||
final Component single = plugin.getFormatter().format(player.getLastDisplayName(), player, viewer, plugin);
|
||||
final Component displayName = getRelationalPlaceholder(player, viewer, single, player.getGroup().format());
|
||||
|
||||
final String displayNameUnformatted = plugin.getPlaceholderManager().applyPlaceholders(player, player.getGroup().format(), viewer);
|
||||
final Component displayName = formatRelationalComponent(player, viewer, displayNameUnformatted);
|
||||
player.setRelationalDisplayName(viewer.getPlayer().getUniqueId(), displayName);
|
||||
return TabListEntry.builder()
|
||||
.profile(player.getPlayer().getGameProfile())
|
||||
@ -385,12 +378,22 @@ public class PlayerTabList {
|
||||
.build();
|
||||
}
|
||||
|
||||
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
|
||||
final Component displayName = getRelationalPlaceholder(player, viewer, player.getLastDisplayName());
|
||||
updateDisplayName(player, viewer, displayName);
|
||||
protected void calculateAndSetDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer) {
|
||||
final String withPlaceholders = plugin.getPlaceholderManager().applyPlaceholders(player, player.getGroup().format());
|
||||
final String unformatted = plugin.getPlaceholderManager().formatVelocitabPlaceholders(withPlaceholders, player, null);
|
||||
if (!plugin.getSettings().isEnableRelationalPlaceholders()) {
|
||||
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(unformatted);
|
||||
final Component displayName = plugin.getFormatter().format(stripped, player, plugin);
|
||||
updateEntryDisplayName(player, viewer, displayName);
|
||||
return;
|
||||
}
|
||||
|
||||
final String withRelationalPlaceholders = plugin.getPlaceholderManager().formatVelocitabPlaceholders(unformatted, player, viewer);
|
||||
final Component displayName = plugin.getFormatter().format(withRelationalPlaceholders, player, viewer, plugin);
|
||||
updateEntryDisplayName(player, viewer, displayName);
|
||||
}
|
||||
|
||||
protected void updateDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull Component displayName) {
|
||||
protected void updateEntryDisplayName(@NotNull TabPlayer player, @NotNull TabPlayer viewer, @NotNull Component displayName) {
|
||||
final Optional<Component> cached = player.getRelationalDisplayName(viewer.getPlayer().getUniqueId());
|
||||
if (cached.isPresent() && cached.get().equals(displayName) &&
|
||||
viewer.getPlayer().getTabList().getEntry(player.getPlayer().getUniqueId())
|
||||
@ -418,6 +421,10 @@ public class PlayerTabList {
|
||||
);
|
||||
}
|
||||
|
||||
public void updateHeaderFooter(@NotNull Group group) {
|
||||
group.getTabPlayers(plugin).forEach(p -> p.sendHeaderAndFooter(this));
|
||||
}
|
||||
|
||||
// Update a player's name in the tab list and scoreboard team
|
||||
public void updatePlayer(@NotNull TabPlayer tabPlayer, boolean force) {
|
||||
if (!tabPlayer.getPlayer().isActive()) {
|
||||
@ -425,58 +432,180 @@ public class PlayerTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSorting(tabPlayer, force);
|
||||
plugin.getPlaceholderManager().fetchPlaceholders(tabPlayer.getPlayer().getUniqueId(), tabPlayer.getGroup().sortingPlaceholders(), tabPlayer.getGroup());
|
||||
|
||||
//to make sure that role placeholder is updated even for a backend placeholder
|
||||
plugin.getServer().getScheduler().buildTask(plugin,
|
||||
() -> updateSorting(tabPlayer, force))
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
|
||||
public void updateSorting(@NotNull Group group) {
|
||||
final List<TabPlayer> players = group.getTabPlayersAsList(plugin);
|
||||
players.forEach(p -> updateSorting(p, false, players));
|
||||
}
|
||||
|
||||
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force) {
|
||||
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
|
||||
if (teamName.isBlank()) {
|
||||
return;
|
||||
}
|
||||
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force).thenAccept(v -> {
|
||||
final int order = plugin.getScoreboardManager().getPosition(teamName);
|
||||
if (order == -1) {
|
||||
plugin.log(Level.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
|
||||
return;
|
||||
}
|
||||
final List<TabPlayer> players = tabPlayer.getGroup().getTabPlayersAsList(plugin, tabPlayer);
|
||||
updateSorting(tabPlayer, force, players);
|
||||
}
|
||||
|
||||
tabPlayer.setListOrder(order);
|
||||
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
|
||||
players.forEach(p -> recalculateSortingForPlayer(p, players));
|
||||
});
|
||||
});
|
||||
private void updateSorting(@NotNull TabPlayer tabPlayer, boolean force, @NotNull List<TabPlayer> players) {
|
||||
final String teamName = tabPlayer.getTeamName(plugin);
|
||||
if (teamName.isBlank() || !tabPlayer.getPlayer().isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getScoreboardManager().updateRole(tabPlayer, teamName, force);
|
||||
final int order = plugin.getScoreboardManager().getPosition(teamName);
|
||||
if (order == -1) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.ERROR, "Failed to get position for " + tabPlayer.getPlayer().getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
tabPlayer.setListOrder(order);
|
||||
recalculateSortingForPlayers(tabPlayer, players, order);
|
||||
}
|
||||
|
||||
private boolean hasListOrder(TabPlayer tabPlayer) {
|
||||
return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2);
|
||||
}
|
||||
|
||||
private void updateSorting(TabPlayer tabPlayer, UUID uuid, int position) {
|
||||
tabPlayer.getPlayer().getTabList().getEntry(uuid)
|
||||
.filter(entry -> entry.getListOrder() != position)
|
||||
.ifPresent(entry -> entry.setListOrder(position));
|
||||
}
|
||||
|
||||
public synchronized void recalculateSortingForPlayers(@NotNull TabPlayer tabPlayer, @NotNull List<TabPlayer> players, int order) {
|
||||
players.stream()
|
||||
.filter(this::hasListOrder)
|
||||
.forEach(p -> updateSorting(p, tabPlayer.getPlayer().getUniqueId(), order));
|
||||
}
|
||||
|
||||
public void sendPlayerServerLinks(@NotNull TabPlayer player) {
|
||||
if (player.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final List<ServerUrl> urls = plugin.getSettings().getUrlsForGroup(player.getGroup());
|
||||
ServerUrl.resolve(plugin, player, urls).thenAccept(player.getPlayer()::setServerLinks);
|
||||
final List<ServerLink> serverLinks = ServerUrl.resolve(plugin, player, urls);
|
||||
player.getPlayer().setServerLinks(serverLinks);
|
||||
}
|
||||
|
||||
public void updatePlayerDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
|
||||
if (displayName == null) {
|
||||
plugin.log(Level.ERROR, "Failed to get display name for " + tabPlayer.getPlayer().getUsername());
|
||||
public void updateGroupNames(@NotNull Group group) {
|
||||
final List<TabPlayer> players = group.getTabPlayersAsList(plugin);
|
||||
if (plugin.getSettings().isEnableRelationalPlaceholders()) {
|
||||
updateRelationalGroupNames(players, group);
|
||||
return;
|
||||
}
|
||||
|
||||
updateNormalGroupNames(players, group);
|
||||
}
|
||||
|
||||
private void updateNormalGroupNames(List<TabPlayer> players, @NotNull Group group) {
|
||||
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(group.format());
|
||||
checkStrippedString(stripped, group);
|
||||
|
||||
for (TabPlayer player : players) {
|
||||
final String displayName = plugin.getPlaceholderManager().applyPlaceholders(player, stripped);
|
||||
final String displayNameConditional = plugin.getPlaceholderManager().formatVelocitabPlaceholders(displayName, player, null);
|
||||
final Component displayNameComponent = formatComponent(player, displayNameConditional);
|
||||
players.forEach(viewer -> updateEntryDisplayName(player, viewer, displayNameComponent));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRelationalGroupNames(@NotNull List<TabPlayer> players, @NotNull Group group) {
|
||||
for (TabPlayer p1 : players) {
|
||||
if (!p1.getPlayer().isActive() || !p1.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
final Component single = plugin.getFormatter().format(displayName, tabPlayer, plugin);
|
||||
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername());
|
||||
final Set<TabPlayer> players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer);
|
||||
final boolean isVanished = plugin.getVanishManager().isVanished(p1.getPlayer().getUsername());
|
||||
final String formatPlaceholders = plugin.getPlaceholderManager().applyPlaceholders(p1, group.format());
|
||||
final String formatConditionalPlaceholders = plugin.getPlaceholderManager().formatVelocitabPlaceholders(formatPlaceholders, p1, null);
|
||||
|
||||
players.forEach(player -> {
|
||||
if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) {
|
||||
// Handles the case where the player is not
|
||||
final String formatConditionalPlaceholdersWithoutRelational = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(formatConditionalPlaceholders);
|
||||
final Component relationalPlaceholder = formatComponent(p1, formatConditionalPlaceholdersWithoutRelational);
|
||||
|
||||
for (TabPlayer player : players) {
|
||||
if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), p1.getPlayer().getUsername())) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Component relationalPlaceholder = getRelationalPlaceholder(tabPlayer, player, single, displayName);
|
||||
updateDisplayName(tabPlayer, player, relationalPlaceholder);
|
||||
});
|
||||
if (!player.getPlayer().isActive() || !player.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.getPlayer().hasPermission(RELATIONAL_PERMISSION)) {
|
||||
updateEntryDisplayName(p1, player, relationalPlaceholder);
|
||||
continue;
|
||||
}
|
||||
|
||||
final String withPlaceholders = plugin.getPlaceholderManager().applyViewerPlaceholders(player, formatConditionalPlaceholders);
|
||||
final String unformatted = plugin.getPlaceholderManager().formatVelocitabPlaceholders(withPlaceholders, p1, player);
|
||||
|
||||
final Component displayName = formatRelationalComponent(p1, player, unformatted);
|
||||
updateEntryDisplayName(p1, player, displayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
if (plugin.getSettings().isEnableRelationalPlaceholders()) {
|
||||
updateRelationalDisplayName(tabPlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
updateNormalDisplayName(tabPlayer);
|
||||
}
|
||||
|
||||
private void updateNormalDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
final Group group = tabPlayer.getGroup();
|
||||
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(group.format());
|
||||
checkStrippedString(stripped, group);
|
||||
|
||||
final List<TabPlayer> players = group.getTabPlayersAsList(plugin, tabPlayer);
|
||||
players.forEach(player -> {
|
||||
if (!player.getPlayer().hasPermission(RELATIONAL_PERMISSION)) {
|
||||
final String displayName = plugin.getPlaceholderManager().applyPlaceholders(player, stripped);
|
||||
final String displayNameConditional = plugin.getPlaceholderManager().formatVelocitabPlaceholders(displayName, player, null);
|
||||
final Component displayNameComponent = formatComponent(player, displayNameConditional);
|
||||
updateEntryDisplayName(player, tabPlayer, displayNameComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
final String withPlaceholders = plugin.getPlaceholderManager().applyViewerPlaceholders(player, stripped);
|
||||
final String unformatted = plugin.getPlaceholderManager().formatVelocitabPlaceholders(withPlaceholders, tabPlayer, player);
|
||||
|
||||
final Component displayName = formatRelationalComponent(tabPlayer, player, unformatted);
|
||||
updateEntryDisplayName(tabPlayer, player, displayName);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateRelationalDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
final Group group = tabPlayer.getGroup();
|
||||
final String stripped = plugin.getPlaceholderManager().stripVelocitabRelPlaceholders(group.format());
|
||||
checkStrippedString(stripped, group);
|
||||
|
||||
final List<TabPlayer> players = group.getTabPlayersAsList(plugin, tabPlayer);
|
||||
players.forEach(player -> {
|
||||
final String displayName = plugin.getPlaceholderManager().applyPlaceholders(player, stripped);
|
||||
final String displayNameConditional = plugin.getPlaceholderManager().formatVelocitabPlaceholders(displayName, player, null);
|
||||
final Component displayNameComponent = formatRelationalComponent(player, tabPlayer, displayNameConditional);
|
||||
updateEntryDisplayName(player, tabPlayer, displayNameComponent);
|
||||
});
|
||||
}
|
||||
|
||||
private void checkStrippedString(@NotNull String text, @NotNull Group group) {
|
||||
if (text.length() != group.format().length()) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.WARNING, "Found relational placeholder in group {} format even though relational placeholders are disabled", group.name());
|
||||
}
|
||||
}
|
||||
|
||||
public void checkCorrectDisplayName(@NotNull TabPlayer tabPlayer) {
|
||||
if (!tabPlayer.isLoaded()) {
|
||||
return;
|
||||
@ -499,12 +628,6 @@ public class PlayerTabList {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// Update the display names of all listed players
|
||||
public void updateDisplayNames() {
|
||||
players.values().forEach(this::updatePlayerDisplayName);
|
||||
}
|
||||
|
||||
public void checkCorrectDisplayNames() {
|
||||
players.values().forEach(this::checkCorrectDisplayName);
|
||||
}
|
||||
@ -513,24 +636,24 @@ public class PlayerTabList {
|
||||
plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, this::checkCorrectDisplayNames)
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(2, TimeUnit.SECONDS)
|
||||
.repeat(5, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
// Get the component for the TAB list header
|
||||
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
|
||||
public Component getHeader(@NotNull TabPlayer player) {
|
||||
final String header = player.getGroup().getHeader(player.getHeaderIndex());
|
||||
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(player, header);
|
||||
|
||||
return Placeholder.replace(header, plugin, player)
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
|
||||
return plugin.getFormatter().format(replaced, player, plugin);
|
||||
}
|
||||
|
||||
// Get the component for the TAB list footer
|
||||
public CompletableFuture<Component> getFooter(@NotNull TabPlayer player) {
|
||||
public Component getFooter(@NotNull TabPlayer player) {
|
||||
final String footer = player.getGroup().getFooter(player.getFooterIndex());
|
||||
final String replaced = plugin.getPlaceholderManager().applyPlaceholders(player, footer);
|
||||
|
||||
return Placeholder.replace(footer, plugin, player)
|
||||
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
|
||||
return plugin.getFormatter().format(replaced, player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -538,33 +661,47 @@ public class PlayerTabList {
|
||||
*/
|
||||
public void reloadUpdate() {
|
||||
taskManager.cancelAllTasks();
|
||||
plugin.getTabGroups().getGroups().forEach(taskManager::updatePeriodically);
|
||||
plugin.getPlaceholderManager().reload();
|
||||
plugin.getPlaceholderManager().preparePlaceholdersReplacements();
|
||||
plugin.getTabGroupsManager().getGroups().forEach(g -> {
|
||||
plugin.getPlaceholderManager().fetchPlaceholders(g);
|
||||
taskManager.updatePeriodically(g);
|
||||
});
|
||||
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the update time is set to 0 do not schedule the updater
|
||||
players.values().forEach(player -> {
|
||||
final Optional<ServerConnection> server = player.getPlayer().getCurrentServer();
|
||||
if (server.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final String serverName = server.get().getServerInfo().getName();
|
||||
final Optional<Group> group = getGroup(serverName);
|
||||
if (group.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
player.setGroup(group.get());
|
||||
this.sendPlayerServerLinks(player);
|
||||
this.updatePlayer(player, true);
|
||||
player.sendHeaderAndFooter(this);
|
||||
});
|
||||
updateDisplayNames();
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
// If the update time is set to 0 do not schedule the updater
|
||||
players.values().forEach(player -> {
|
||||
final Optional<ServerConnection> server = player.getPlayer().getCurrentServer();
|
||||
if (server.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final String serverName = server.get().getServerInfo().getName();
|
||||
final Optional<Group> group = getGroup(serverName);
|
||||
if (group.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
player.setGroup(group.get());
|
||||
this.sendPlayerServerLinks(player);
|
||||
this.updatePlayer(player, true);
|
||||
player.sendHeaderAndFooter(this);
|
||||
});
|
||||
plugin.getTabGroupsManager().getGroups().forEach(this::updateGroupNames);
|
||||
}).delay(500, TimeUnit.MILLISECONDS).schedule();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Optional<Group> getGroup(@NotNull String serverName) {
|
||||
return plugin.getTabGroups().getGroupFromServer(serverName, plugin);
|
||||
return plugin.getTabGroupsManager().getGroupFromServer(serverName, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Group getGroupOrDefault(@NotNull Player player) {
|
||||
final Optional<Group> group = getGroup(player.getCurrentServer().map(ServerConnection::getServerInfo).map(ServerInfo::getName).orElse(""));
|
||||
return group.orElse(plugin.getTabGroupsManager().getGroup("default").orElseThrow());
|
||||
}
|
||||
|
||||
public void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
|
||||
@ -580,45 +717,4 @@ public class PlayerTabList {
|
||||
public void removeOfflinePlayer(@NotNull Player player) {
|
||||
players.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the player can use server-side specified TAB list ordering (Minecraft 1.21.2+)
|
||||
*
|
||||
* @param tabPlayer player to check
|
||||
* @return {@code true} if the user is on Minecraft 1.21.2+; {@code false}
|
||||
*/
|
||||
private boolean hasListOrder(@NotNull TabPlayer tabPlayer) {
|
||||
return tabPlayer.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_2);
|
||||
}
|
||||
|
||||
private void updateSorting(@NotNull TabPlayer tabPlayer, @NotNull UUID uuid, int position) {
|
||||
if (!tabPlayer.getPlayer().getTabList().containsEntry(uuid)) {
|
||||
return;
|
||||
}
|
||||
if (tabPlayer.getCachedListOrders().containsKey(uuid) && tabPlayer.getCachedListOrders().get(uuid) == position) {
|
||||
return;
|
||||
}
|
||||
tabPlayer.getCachedListOrders().put(uuid, position);
|
||||
final UpsertPlayerInfoPacket packet = new UpsertPlayerInfoPacket(UPDATE_LIST_ORDER);
|
||||
final UpsertPlayerInfoPacket.Entry entry = new UpsertPlayerInfoPacket.Entry(uuid);
|
||||
entry.setListOrder(position);
|
||||
packet.addEntry(entry);
|
||||
((ConnectedPlayer) tabPlayer.getPlayer()).getConnection().write(packet);
|
||||
}
|
||||
|
||||
private String getPlayerName(UUID uuid) {
|
||||
return plugin.getServer().getPlayer(uuid).map(Player::getUsername).orElse("Unknown");
|
||||
}
|
||||
|
||||
public synchronized void recalculateSortingForPlayer(@NotNull TabPlayer tabPlayer, @NotNull Set<TabPlayer> players) {
|
||||
if (!hasListOrder(tabPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
players.forEach(p -> {
|
||||
final int order = p.getListOrder();
|
||||
updateSorting(tabPlayer, p.getPlayer().getUniqueId(), order);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import com.velocitypowered.api.event.player.ServerPostConnectEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
@ -39,6 +40,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The TabListListener class is responsible for handling events related to the player tab list.
|
||||
@ -59,7 +61,11 @@ public class TabListListener {
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onKick(@NotNull KickedFromServerEvent event) {
|
||||
private void onKick(@NotNull KickedFromServerEvent event) {
|
||||
plugin.getTabList().getTaskManager().run(() -> handleKick(event));
|
||||
}
|
||||
|
||||
private void handleKick(@NotNull KickedFromServerEvent event) {
|
||||
event.getPlayer().getTabList().getEntries().stream()
|
||||
.filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(event.getPlayer().getUniqueId()))
|
||||
.forEach(entry -> event.getPlayer().getTabList().removeEntry(entry.getProfile().getId()));
|
||||
@ -68,7 +74,7 @@ public class TabListListener {
|
||||
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) {
|
||||
tabList.removePlayer(event.getPlayer());
|
||||
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) {
|
||||
tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer());
|
||||
tabList.removePlayer(event.getPlayer());
|
||||
} else if (event.getResult() instanceof KickedFromServerEvent.Notify notify) {
|
||||
return;
|
||||
}
|
||||
@ -83,16 +89,23 @@ public class TabListListener {
|
||||
.schedule();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
@Subscribe
|
||||
public void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
|
||||
@Subscribe(priority = Short.MIN_VALUE)
|
||||
private void onPlayerJoin(@NotNull ServerPostConnectEvent event) {
|
||||
plugin.getTabList().getTaskManager().run(() -> handlePlayerJoin(event));
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
private void handlePlayerJoin(@NotNull ServerPostConnectEvent event) {
|
||||
final Player joined = event.getPlayer();
|
||||
final String serverName = joined.getCurrentServer()
|
||||
.map(ServerConnection::getServerInfo)
|
||||
.map(ServerInfo::getName)
|
||||
.orElse("");
|
||||
|
||||
final Optional<Group> previousGroup = tabList.getTabPlayer(joined)
|
||||
final Optional<TabPlayer> previousTabPlayer = tabList.getTabPlayer(joined);
|
||||
final Optional<Group> previousGroup = previousTabPlayer
|
||||
.map(TabPlayer::getGroup);
|
||||
|
||||
// Get the group the player should now be in
|
||||
@ -101,18 +114,35 @@ public class TabListListener {
|
||||
|
||||
// Removes cached relational data of the joined player from all other players
|
||||
plugin.getTabList().clearCachedData(joined);
|
||||
plugin.getPlaceholderManager().clearPlaceholders(joined.getUniqueId());
|
||||
|
||||
// Mark the previous tab player as unloaded
|
||||
previousTabPlayer.ifPresent(player -> player.setLoaded(false));
|
||||
|
||||
// If the player was in a group and the new group is different or not set, remove the old entry
|
||||
if (!plugin.getSettings().isShowAllPlayersFromAllGroups() && previousGroup.isPresent()
|
||||
&& (groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())
|
||||
|| groupOptional.isEmpty())) {
|
||||
tabList.removeOldEntry(previousGroup.get(), joined.getUniqueId());
|
||||
&& ((groupOptional.isPresent() && !previousGroup.get().equals(groupOptional.get())) || groupOptional.isEmpty())
|
||||
) {
|
||||
tabList.getPlayers().remove(joined.getUniqueId());
|
||||
removeOldEntry(previousGroup.get(), joined.getUniqueId());
|
||||
|
||||
// If a player moved to a server without a group, remove possible entries
|
||||
if (groupOptional.isEmpty() && joined.getCurrentServer().isPresent()) {
|
||||
plugin.getTabList().getTaskManager().runDelayed(() -> {
|
||||
final RegisteredServer server = joined.getCurrentServer().get().getServer();
|
||||
final Set<UUID> players = server.getPlayersConnected().stream()
|
||||
.map(Player::getUniqueId).collect(Collectors.toSet());
|
||||
final Set<UUID> tabPlayers = Sets.newHashSet(tabList.getPlayers().keySet());
|
||||
tabPlayers.removeAll(players);
|
||||
tabPlayers.forEach(u -> joined.getTabList().removeEntry(u));
|
||||
}, 250, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
// If the server is not in a group, use fallback.
|
||||
// If fallback is disabled, permit the player to switch excluded servers without a header or footer override
|
||||
if (isDefault && !plugin.getSettings().isFallbackEnabled() && !groupOptional.map(g -> g.containsServer(plugin, serverName)).orElse(false)) {
|
||||
final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
|
||||
if (tabPlayer.isEmpty()) {
|
||||
if (previousTabPlayer.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -120,22 +150,7 @@ public class TabListListener {
|
||||
return;
|
||||
}
|
||||
|
||||
final Component header = tabPlayer.get().getLastHeader();
|
||||
final Component footer = tabPlayer.get().getLastFooter();
|
||||
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
final Component currentHeader = joined.getPlayerListHeader();
|
||||
final Component currentFooter = joined.getPlayerListFooter();
|
||||
if ((header.equals(currentHeader) && footer.equals(currentFooter)) ||
|
||||
(currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty()))
|
||||
) {
|
||||
joined.sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
|
||||
joined.getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player ->
|
||||
player.getTabList().getEntry(joined.getUniqueId())
|
||||
.ifPresent(entry -> entry.setDisplayName(Component.text(joined.getUsername())))));
|
||||
}
|
||||
}).delay(500, TimeUnit.MILLISECONDS).schedule();
|
||||
|
||||
cleanOldHeadersAndFooters(previousTabPlayer.get());
|
||||
tabList.getPlayers().remove(event.getPlayer().getUniqueId());
|
||||
return;
|
||||
}
|
||||
@ -147,26 +162,59 @@ public class TabListListener {
|
||||
final Group group = groupOptional.get();
|
||||
plugin.getScoreboardManager().resetCache(joined, group);
|
||||
|
||||
final int delay = justQuit.contains(joined.getUniqueId()) ? 100 : 250;
|
||||
plugin.getServer().getScheduler().buildTask(plugin,
|
||||
() -> tabList.joinPlayer(joined, group))
|
||||
.delay(delay, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
plugin.getPlaceholderManager().unblockPlayer(joined.getUniqueId());
|
||||
}).delay(10, TimeUnit.MILLISECONDS).schedule();
|
||||
|
||||
tabList.loadPlayer(joined, group, justQuit.contains(joined.getUniqueId()) ? 400 : 500);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Subscribe(order = PostOrder.CUSTOM, priority = Short.MIN_VALUE)
|
||||
public void onPlayerQuit(@NotNull DisconnectEvent event) {
|
||||
private void onPlayerQuit(@NotNull DisconnectEvent event) {
|
||||
plugin.getTabList().getTaskManager().run(() -> handlePlayerQuit(event));
|
||||
}
|
||||
|
||||
private void handlePlayerQuit(@NotNull DisconnectEvent event) {
|
||||
if (event.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN) {
|
||||
return;
|
||||
}
|
||||
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
|
||||
checkDelayedDisconnect(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the player from the tab list of all other players
|
||||
tabList.removePlayer(event.getPlayer());
|
||||
plugin.getPlaceholderManager().clearPlaceholders(event.getPlayer().getUniqueId());
|
||||
plugin.getPlaceholderManager().unblockPlayer(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
private void proxyReload(@NotNull ProxyReloadEvent event) {
|
||||
plugin.loadConfigs();
|
||||
tabList.reloadUpdate();
|
||||
plugin.log("Velocitab has been reloaded!");
|
||||
}
|
||||
|
||||
private void removeOldEntry(@NotNull Group group, @NotNull UUID uuid) {
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> tabList.removeOldEntry(group, uuid))
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
}
|
||||
|
||||
private void cleanOldHeadersAndFooters(@NotNull TabPlayer tabPlayer) {
|
||||
final Component header = tabPlayer.getLastHeader();
|
||||
final Component footer = tabPlayer.getLastFooter();
|
||||
|
||||
plugin.getServer().getScheduler().buildTask(plugin, () -> {
|
||||
final Component currentHeader = tabPlayer.getPlayer().getPlayerListHeader();
|
||||
final Component currentFooter = tabPlayer.getPlayer().getPlayerListFooter();
|
||||
if ((header.equals(currentHeader) && footer.equals(currentFooter)) ||
|
||||
(currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty()))
|
||||
) {
|
||||
tabPlayer.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
|
||||
tabPlayer.getPlayer().getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player ->
|
||||
player.getTabList().getEntry(tabPlayer.getPlayer().getUniqueId())
|
||||
.ifPresent(entry -> entry.setDisplayName(Component.text(tabPlayer.getPlayer().getUsername())))));
|
||||
}
|
||||
}).delay(500, TimeUnit.MILLISECONDS).schedule();
|
||||
}
|
||||
|
||||
private void checkDelayedDisconnect(@NotNull DisconnectEvent event) {
|
||||
@ -185,11 +233,5 @@ public class TabListListener {
|
||||
}).delay(750, TimeUnit.MILLISECONDS).schedule();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void proxyReload(@NotNull ProxyReloadEvent event) {
|
||||
plugin.loadConfigs();
|
||||
tabList.reloadUpdate();
|
||||
plugin.log("Velocitab has been reloaded!");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,88 +19,143 @@
|
||||
|
||||
package net.william278.velocitab.tab;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.config.Group;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.util.DebugSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TaskManager {
|
||||
|
||||
private final Velocitab plugin;
|
||||
private final Map<Group, GroupTasks> groupTasks;
|
||||
private final Map<Group, List<ScheduledFuture<?>>> groupTasks;
|
||||
private final Map<Group, List<ScheduledTask>> groupTasksOld;
|
||||
private final ScheduledExecutorService processThread;
|
||||
|
||||
public TaskManager(@NotNull Velocitab plugin) {
|
||||
this.plugin = plugin;
|
||||
this.groupTasksOld = Maps.newConcurrentMap();
|
||||
this.groupTasks = Maps.newConcurrentMap();
|
||||
this.processThread = Executors.newSingleThreadScheduledExecutor();
|
||||
}
|
||||
|
||||
protected void cancelAllTasks() {
|
||||
groupTasks.values().forEach(GroupTasks::cancel);
|
||||
groupTasksOld.values().forEach(c -> c.forEach(ScheduledTask::cancel));
|
||||
groupTasksOld.clear();
|
||||
groupTasks.values().forEach(c -> c.forEach(t -> t.cancel(true)));
|
||||
groupTasks.clear();
|
||||
}
|
||||
|
||||
protected void updatePeriodically(@NotNull Group group) {
|
||||
ScheduledTask headerFooterTask = null;
|
||||
ScheduledTask updateTask = null;
|
||||
ScheduledTask latencyTask;
|
||||
public void close() {
|
||||
try {
|
||||
cancelAllTasks();
|
||||
processThread.shutdownNow();
|
||||
} catch (Throwable e) {
|
||||
plugin.getLogger().error("Failed to close task manager", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updatePeriodically(@NotNull Group group) {
|
||||
final List<ScheduledFuture<?>> tasks = groupTasks.computeIfAbsent(group, g -> Lists.newArrayList());
|
||||
if (group.headerFooterUpdateRate() > 0) {
|
||||
headerFooterTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateGroupPlayers(group, false, true))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(Math.max(200, group.headerFooterUpdateRate()), TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
final ScheduledFuture<?> headerFooterTask = processThread.scheduleAtFixedRate(() -> {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
plugin.getTabList().updateHeaderFooter(group);
|
||||
final long endTime = System.currentTimeMillis();
|
||||
final long time = endTime - startTime;
|
||||
if (time > 30) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated header/footer for group {} took {}ms", group.name(), time);
|
||||
}
|
||||
},
|
||||
250,
|
||||
Math.max(200, group.headerFooterUpdateRate()),
|
||||
TimeUnit.MILLISECONDS);
|
||||
tasks.add(headerFooterTask);
|
||||
}
|
||||
|
||||
if (group.formatUpdateRate() > 0) {
|
||||
final ScheduledFuture<?> formatTask = processThread.scheduleAtFixedRate(() -> {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
plugin.getTabList().updateGroupNames(group);
|
||||
final long endTime = System.currentTimeMillis();
|
||||
final long time = endTime - startTime;
|
||||
if (time > 50) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated format for group {} took {}ms", group.name(), time);
|
||||
}
|
||||
},
|
||||
500,
|
||||
Math.max(200, group.formatUpdateRate()),
|
||||
TimeUnit.MILLISECONDS);
|
||||
tasks.add(formatTask);
|
||||
}
|
||||
|
||||
if (group.nametagUpdateRate() > 0) {
|
||||
final ScheduledFuture<?> nametagTask = processThread.scheduleAtFixedRate(() -> {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
plugin.getTabList().updateSorting(group);
|
||||
final long endTime = System.currentTimeMillis();
|
||||
final long time = endTime - startTime;
|
||||
if (time > 50) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated nametags/sorting for group {} took {}ms", group.name(), time);
|
||||
}
|
||||
},
|
||||
750,
|
||||
Math.max(200, group.nametagUpdateRate()),
|
||||
TimeUnit.MILLISECONDS);
|
||||
tasks.add(nametagTask);
|
||||
}
|
||||
|
||||
if (group.placeholderUpdateRate() > 0) {
|
||||
updateTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateGroupPlayers(group, true, false))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(Math.max(200, group.placeholderUpdateRate()), TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
final ScheduledFuture<?> updateTask = processThread.scheduleAtFixedRate(() -> {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
updatePlaceholders(group);
|
||||
final long endTime = System.currentTimeMillis();
|
||||
final long time = endTime - startTime;
|
||||
if (time > 10) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated placeholders for group {} took {}ms", group.name(), time);
|
||||
}
|
||||
},
|
||||
1000,
|
||||
Math.max(200, group.placeholderUpdateRate()),
|
||||
TimeUnit.MILLISECONDS);
|
||||
tasks.add(updateTask);
|
||||
}
|
||||
|
||||
latencyTask = plugin.getServer().getScheduler()
|
||||
.buildTask(plugin, () -> updateLatency(group))
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.repeat(3, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
final ScheduledFuture<?> latencyTask = processThread.scheduleAtFixedRate(() -> {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
updateLatency(group);
|
||||
final long endTime = System.currentTimeMillis();
|
||||
final long time = endTime - startTime;
|
||||
if (time > 10) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Updated latency for group {} took {}ms", group.name(), time);
|
||||
}
|
||||
},
|
||||
1250,
|
||||
2500,
|
||||
TimeUnit.MILLISECONDS);
|
||||
|
||||
groupTasks.put(group, new GroupTasks(headerFooterTask, updateTask, latencyTask));
|
||||
tasks.add(latencyTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the players in the given group.
|
||||
*
|
||||
* @param group The group whose players should be updated.
|
||||
* @param all Whether to update all player properties, or just the header and footer.
|
||||
* @param incrementIndexes Whether to increment the header and footer indexes.
|
||||
*/
|
||||
private void updateGroupPlayers(@NotNull Group group, boolean all, boolean incrementIndexes) {
|
||||
final Set<TabPlayer> groupPlayers = group.getTabPlayers(plugin);
|
||||
if (groupPlayers.isEmpty()) {
|
||||
private void updatePlaceholders(@NotNull Group group) {
|
||||
final List<TabPlayer> players = group.getTabPlayersAsList(plugin);
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
groupPlayers.stream()
|
||||
.filter(player -> player.getPlayer().isActive())
|
||||
.forEach(player -> {
|
||||
if (incrementIndexes) {
|
||||
player.incrementIndexes();
|
||||
}
|
||||
if (all) {
|
||||
plugin.getTabList().updatePlayer(player, false);
|
||||
}
|
||||
player.sendHeaderAndFooter(plugin.getTabList());
|
||||
});
|
||||
if (all) {
|
||||
plugin.getTabList().updateDisplayNames();
|
||||
}
|
||||
|
||||
final List<String> texts = group.getTextsWithPlaceholders(plugin);
|
||||
players.forEach(player -> plugin.getPlaceholderManager().fetchPlaceholders(player.getPlayer().getUniqueId(), texts, group));
|
||||
}
|
||||
|
||||
private void updateLatency(@NotNull Group group) {
|
||||
@ -108,6 +163,7 @@ public class TaskManager {
|
||||
if (groupPlayers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
groupPlayers.stream()
|
||||
.filter(player -> player.getPlayer().isActive())
|
||||
.forEach(player -> {
|
||||
@ -118,4 +174,11 @@ public class TaskManager {
|
||||
});
|
||||
}
|
||||
|
||||
public void run(@NotNull Runnable runnable) {
|
||||
processThread.execute(runnable);
|
||||
}
|
||||
|
||||
public void runDelayed(@NotNull Runnable runnable, long delay, @NotNull TimeUnit timeUnit) {
|
||||
processThread.schedule(runnable, delay, timeUnit);
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ public class VanishTabList {
|
||||
if (!p.getPlayer().getTabList().containsEntry(uuid)) {
|
||||
tabList.createEntry(tabPlayer, p.getPlayer().getTabList(), p);
|
||||
} else {
|
||||
tabList.updateDisplayName(tabPlayer, p);
|
||||
tabList.calculateAndSetDisplayName(tabPlayer, p);
|
||||
}
|
||||
});
|
||||
|
||||
@ -93,6 +93,10 @@ public class VanishTabList {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!p.isActive() || !target.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) ||
|
||||
plugin.getVanishManager().canSee(player.getUsername(), p.getUsername());
|
||||
|
||||
|
122
src/main/java/net/william278/velocitab/util/DebugSystem.java
Normal file
122
src/main/java/net/william278/velocitab/util/DebugSystem.java
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.util;
|
||||
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.Iterator;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class DebugSystem {
|
||||
|
||||
private static final ConcurrentLinkedQueue<LogEntry> logs = new ConcurrentLinkedQueue<>();
|
||||
private static final int MAX_LOGS = 10000;
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss");
|
||||
private static final int REMOVE_HOURS = 6;
|
||||
|
||||
public enum DebugLevel {
|
||||
INFO, WARNING, ERROR, DEBUG
|
||||
}
|
||||
|
||||
private static class LogEntry {
|
||||
final long timestamp;
|
||||
final String threadName;
|
||||
final DebugLevel level;
|
||||
final String message;
|
||||
|
||||
LogEntry(@NotNull final String threadName, @NotNull final DebugLevel level, @NotNull final String message) {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
this.threadName = threadName;
|
||||
this.level = level;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String format() {
|
||||
return "[" + DATE_FORMAT.format(new Date(timestamp)) + "] [" + threadName + "/" + level + "]: " + message;
|
||||
}
|
||||
}
|
||||
|
||||
public static void log(@NotNull final DebugLevel level, @NotNull final String message) {
|
||||
logs.add(new LogEntry(Thread.currentThread().getName(), level, message));
|
||||
if (logs.size() > MAX_LOGS) {
|
||||
logs.poll();
|
||||
}
|
||||
}
|
||||
|
||||
public static void log(@NotNull DebugLevel level, @NotNull String message, Object... args) {
|
||||
logs.add(new LogEntry(Thread.currentThread().getName(), level, formatMessage(message, args)));
|
||||
if (logs.size() > MAX_LOGS) {
|
||||
logs.poll();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String formatMessage(@NotNull final String message, @NotNull final Object... args) {
|
||||
final StringBuilder formattedMessage = new StringBuilder();
|
||||
int argIndex = 0;
|
||||
for (int i = 0; i < message.length(); i++) {
|
||||
if (message.charAt(i) == '{' && i + 1 < message.length() && message.charAt(i + 1) == '}') {
|
||||
formattedMessage.append(argIndex < args.length ? args[argIndex++] : "{}");
|
||||
i++;
|
||||
} else {
|
||||
formattedMessage.append(message.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
return formattedMessage.toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getLogsAsString() {
|
||||
final StringBuilder logBuilder = new StringBuilder();
|
||||
for (final LogEntry entry : logs) {
|
||||
logBuilder.append(entry.format()).append("\n");
|
||||
}
|
||||
return logBuilder.toString();
|
||||
}
|
||||
|
||||
private static void removeLogsOlderThan() {
|
||||
final long cutoffTime = System.currentTimeMillis() - (DebugSystem.REMOVE_HOURS * 3600L * 1000L);
|
||||
final Iterator<LogEntry> iterator = logs.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
final LogEntry entry = iterator.next();
|
||||
if (entry.timestamp < cutoffTime) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void initializeTask(@NotNull Velocitab plugin) {
|
||||
plugin.getServer().getScheduler().buildTask(plugin, DebugSystem::removeLogsOlderThan)
|
||||
.delay(REMOVE_HOURS, TimeUnit.HOURS)
|
||||
.repeat(REMOVE_HOURS, TimeUnit.HOURS)
|
||||
.schedule();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.util;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class MiniMessageUtil {
|
||||
|
||||
@Getter
|
||||
private static final MiniMessageUtil INSTANCE = new MiniMessageUtil();
|
||||
|
||||
private final Pattern legacyRGBPattern = Pattern.compile("#&[0-9a-fA-F]{6}");
|
||||
private final Pattern legacyPattern = Pattern.compile("&[0-9a-fA-F]");
|
||||
private final Pattern legacySectionPattern = Pattern.compile("§[0-9a-fA-F]");
|
||||
private int errorsCount;
|
||||
|
||||
private MiniMessageUtil() {
|
||||
errorsCount = 0;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String checkForErrors(@NotNull String text) {
|
||||
final List<String> errors = Lists.newArrayList();
|
||||
String copy = text;
|
||||
copy = processLegacySections(errors, copy, legacyRGBPattern);
|
||||
copy = processLegacySections(errors, copy, legacyPattern);
|
||||
copy = processLegacySections(errors, copy, legacySectionPattern);
|
||||
|
||||
if (errorsCount > 0 && errorsCount % 10 == 0) {
|
||||
errorsCount++;
|
||||
DebugSystem.log(DebugSystem.DebugLevel.WARNING, "Found legacy formatting which is not supported if the formatter is set to MINIMESSAGE." +
|
||||
" Remove the following characters from your config or make sure placeholders don't contain them: " + errors + ". & and § are replaced with * to prevent issues with MINIMESSAGE.");
|
||||
if(errorsCount > 100000) {
|
||||
errorsCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String processLegacySections(@NotNull List<String> errors, @NotNull String copy, @NotNull Pattern legacySectionPattern) {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
final Matcher legacySectionMatcher = legacySectionPattern.matcher(copy);
|
||||
|
||||
while (legacySectionMatcher.find()) {
|
||||
errors.add(legacySectionMatcher.group());
|
||||
String matched = legacySectionMatcher.group();
|
||||
String replaced = "*" + matched.substring(1);
|
||||
legacySectionMatcher.appendReplacement(result, Matcher.quoteReplacement(replaced));
|
||||
errorsCount++;
|
||||
}
|
||||
|
||||
legacySectionMatcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
}
|
@ -23,7 +23,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
public final class SerializationUtil {
|
||||
|
||||
public final static LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
|
||||
public static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.builder()
|
||||
.hexCharacter('#')
|
||||
.character('&')
|
||||
.hexColors()
|
||||
|
92
src/main/java/net/william278/velocitab/util/StringUtil.java
Normal file
92
src/main/java/net/william278/velocitab/util/StringUtil.java
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of Velocitab, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class StringUtil {
|
||||
|
||||
private static final Pattern UNICODE_PATTERN = Pattern.compile("\\\\u([0-9a-fA-F]{4})");
|
||||
|
||||
@NotNull
|
||||
public static String unescapeJava(@NotNull String input) {
|
||||
final StringBuilder output = new StringBuilder();
|
||||
int length = input.length();
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = input.charAt(i);
|
||||
if (c == '\\' && i + 1 < length) {
|
||||
char next = input.charAt(i + 1);
|
||||
switch (next) {
|
||||
case 'b' -> { output.append('\b'); i++; }
|
||||
case 't' -> { output.append('\t'); i++; }
|
||||
case 'n' -> { output.append('\n'); i++; }
|
||||
case 'f' -> { output.append('\f'); i++; }
|
||||
case 'r' -> { output.append('\r'); i++; }
|
||||
case '\"' -> { output.append('\"'); i++; }
|
||||
case '\'' -> { output.append('\''); i++; }
|
||||
case '\\' -> { output.append('\\'); i++; }
|
||||
case 'u' -> {
|
||||
if (i + 5 < length) {
|
||||
String hex = input.substring(i + 2, i + 6);
|
||||
try {
|
||||
int unicode = Integer.parseInt(hex, 16);
|
||||
output.append((char) unicode);
|
||||
i += 5;
|
||||
} catch (NumberFormatException e) {
|
||||
output.append("\\u").append(hex);
|
||||
i += 5;
|
||||
}
|
||||
} else {
|
||||
output.append(c);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
output.append(c).append(next);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
output.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return replaceUnicodeEscapes(output.toString());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String replaceUnicodeEscapes(@NotNull String input) {
|
||||
final Matcher matcher = UNICODE_PATTERN.matcher(input);
|
||||
final StringBuilder result = new StringBuilder();
|
||||
|
||||
while (matcher.find()) {
|
||||
final String unicodeStr = matcher.group(1);
|
||||
final int unicode = Integer.parseInt(unicodeStr, 16);
|
||||
matcher.appendReplacement(result, Character.toString(unicode));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
}
|
@ -17,24 +17,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.velocitab.tab;
|
||||
package net.william278.velocitab.util;
|
||||
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@FunctionalInterface
|
||||
public interface TriFunction<T, U, V, R> {
|
||||
|
||||
public record GroupTasks(@Nullable ScheduledTask updateTask, @Nullable ScheduledTask headerFooterTask,
|
||||
@Nullable ScheduledTask latencyTask) {
|
||||
/**
|
||||
* Applies this function to the given arguments.
|
||||
*
|
||||
* @param t the first function argument
|
||||
* @param u the second function argument
|
||||
* @param v the third function argument
|
||||
* @return the function result
|
||||
*/
|
||||
R apply(T t, U u, V v);
|
||||
|
||||
public void cancel() {
|
||||
if (updateTask != null) {
|
||||
updateTask.cancel();
|
||||
}
|
||||
if (headerFooterTask != null) {
|
||||
headerFooterTask.cancel();
|
||||
}
|
||||
if (latencyTask != null) {
|
||||
latencyTask.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ package net.william278.velocitab.vanish;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.william278.velocitab.Velocitab;
|
||||
import net.william278.velocitab.player.TabPlayer;
|
||||
import net.william278.velocitab.util.DebugSystem;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -46,11 +47,23 @@ public class VanishManager {
|
||||
}
|
||||
|
||||
public boolean canSee(@NotNull String name, @NotNull String otherName) {
|
||||
return integration.canSee(name, otherName);
|
||||
final long start = System.currentTimeMillis();
|
||||
final boolean result = integration.canSee(name, otherName);
|
||||
final long end = System.currentTimeMillis();
|
||||
if (end - start > 2) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Vanish isVanished check took " + (end - start) + "ms");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isVanished(@NotNull String name) {
|
||||
return integration.isVanished(name);
|
||||
final long start = System.currentTimeMillis();
|
||||
final boolean result = integration.isVanished(name);
|
||||
final long end = System.currentTimeMillis();
|
||||
if (end - start > 2) {
|
||||
DebugSystem.log(DebugSystem.DebugLevel.DEBUG, "Vanish isVanished check took " + (end - start) + "ms");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void vanishPlayer(@NotNull Player player) {
|
||||
|
Loading…
Reference in New Issue
Block a user