diff --git a/README.md b/README.md
index ca66dd9..2283d49 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-![Logo](https://i.imgur.com/MV1mbYv.png)
+![Logo](https://i.imgur.com/DOl47Dn.png)
# Styled Player List
It's a simple mod that allows server owners to style their player list as they like!
-With full permission support, placeholder api support, multiple styles and player name overrides.
+With full permission/requirement support, placeholder api support, multiple styles and player list name overrides.
-*This mod works only on Fabric Mod Loader and compatible!*
+*This mod works only on Fabric and Quilt!*
If you have any questions, you can ask them on my [Discord](https://pb4.eu/discord)
@@ -21,48 +21,86 @@ If you have any questions, you can ask them on my [Discord](https://pb4.eu/disco
## Configuration:
You can find config file in `./config/styledplayerlist/`.
+Some config options allow for dynamic predicates (marked as `{/* PREDICATE */}`).
+See [this page](https://github.com/Patbox/PredicateAPI/blob/1.19.4/BUILTIN.md) for more details.
+[Formatting uses PlaceholderAPI's Text Parser for which docs you can find here](https://placeholders.pb4.eu/user/text-format/).
+
```json5
{
- "defaultStyle": "default", // allows to select id of default player list
- "updateRate": 20, // change how often player list is updated (20 = every 1 second)
- "...Message": "...", // allows to change messages
- "changePlayerName": false, // if true, names of players on player list will be changed
- "playerNameFormat": "%player:display_name%", // format of player name (uses Text Parser and placeholders)
- "updatePlayerNameEveryChatMessage": false, // if true, everytime player sends a message, theirs name will be updated
- "playerNameUpdateRate": -1 , // changes how often player name is updated (20 = every 1 second, -1 disables it)
- "permissionNameFormat": [ // Permission based overrides of name format
- {
- "permission": "some.permission", // Required permission
- "opLevel": -1, // Alternative OP level (-1 to disable)
- "style": "..." // format of player name (uses Text Parser and placeholders)
- }
- ]
+ // Config version, do not change. Used only for updating from one version to another
+ "config_version": 2,
+ // Allows selecting id of default player list style
+ "default_style": "default",
+ // Allows changing messages sent by this mods commands.
+ "messages": {
+ "switch": "Your player list style has been changed to: ${style}",
+ "unknown": "This style doesn't exist!",
+ "no_permission": "You don't have required permissions!"
+ },
+ // Modifies how player name is displayed
+ "player": {
+ // Toggles this feature.
+ "modify_name": false,
+ // Hides player name from player list. Doesn't have any effect on commands, suggestions or entity visibility!
+ "hidden": false,
+ // Disables this formatting, forcing it to use vanilla one.
+ "passthrough": false,
+ // Default format of player name
+ "format": "%player:displayname%",
+ // Enables sending updates when player sends a message
+ "update_on_chat_message": false,
+ // Enables sending updates every provided amount of ticks. -1 disables it
+ "update_tick_time": -1,
+ // Custom styles
+ "styles": [
+ {
+ // Requirement of style, used for applying
+ "require": {/* PREDICATE */},
+ // Applied formatting, same as one above
+ "format": "...",
+ // Optional. Disables this formatting, forcing it to use vanilla one.
+ "passthrough": false,
+ // Optional, hides player name from player list. Doesn't have any effect on commands, suggestions or entity visibility!
+ "hidden": false
+ }
+ ]
+ },
+ // Makes player list show in singleplayer without lan enabled
+ "client_show_in_singleplayer": true
}
```
### Styles:
This mod allows having multiple styles, that can be selected by players (just put them in `./config/styledplayerlist/styles/` and use `/styledplayerlist reload` command)
-[Formatting uses PlaceholderAPI's Text Parser for which docs you can find here](https://github.com/Patbox/FabricPlaceholderAPI/blob/1.17/TEXT_FORMATTING.md).
```json5
{
- "id": "default", // used internally and for commands
- "name": "Default", // used is messages
- "header": [ // header of player list, every element is in new line
- "",
- " Styled Player List ⛏ ",
- "",
- " [ %server:online%/%server:max_players% ] ",
- ""
- ],
- "footer": [ // footer of player list, every element is in new line
- "",
- " ",
- "",
- "TPS: %server:tps_colored% | Ping: %player:ping%",
- ""
+ // Predicate required for usage of this style, required by player
+ "require": {/* PREDICATE */},
+ // Style name used for display
+ "style_name": "Default",
+ // Time between updates of the style in ticks. 20 is 1 second. Used for formatting and placeholders
+ "update_tick_time": 20,
+ // Header of player list style, using simple/static definition (works in "list_footer" too). Allows formatting
+ "list_header": [
+ "...",
+ "..."
],
- "hidden": false, // hides in commands
- "permission": "" // required permission, leave empty if you want to allow everyone
+ // Footer of player list style, using animated definition (works in "list_header" too). Allows formatting
+ "list_footer": {
+ // Number of changes required to change into next frame. This means it updates every (change_rate * update_tick_time) ticks
+ "change_rate": 1,
+ // Frames of displayed text. There is no limit for amount of them
+ "values": [
+ [
+ "..."
+ ],
+ [
+ "..."
+ ]
+ ],
+ },
+ // Makes this style hidden from autocompletion, without changing requirements
+ "hidden_in_commands": false
}
```
diff --git a/build.gradle b/build.gradle
index 2b605a7..1bc5ce0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -32,9 +32,10 @@ dependencies {
modCompileOnly("net.fabricmc.fabric-api:fabric-api:${project.fabric_version}")
modLocalRuntime("net.fabricmc.fabric-api:fabric-api:${project.fabric_version}")
- modImplementation include("eu.pb4:placeholder-api:2.0.0-pre.2+1.19.3")
+ modImplementation include("eu.pb4:placeholder-api:2.1.0+1.19.4")
modImplementation include("eu.pb4:player-data-api:0.2.2+1.19.3")
modImplementation include("me.lucko:fabric-permissions-api:0.1-SNAPSHOT")
+ modImplementation include("eu.pb4:predicate-api:0.1.1+1.19.4")
modCompileOnly "carpet:fabric-carpet:${project.carpet_core_version}"
diff --git a/gradle.properties b/gradle.properties
index de6542e..2fcbdde 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,15 +3,15 @@ org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://fabricmc.net/use
-minecraft_version=1.19.3-rc1
-yarn_mappings=1.19.3-rc1+build.1
-loader_version=0.14.11
+minecraft_version=1.19.4
+yarn_mappings=1.19.4+build.1
+loader_version=0.14.14
#Fabric api
-fabric_version=0.68.1+1.19.3
+fabric_version=0.75.1+1.19.4
# Mod Properties
- mod_version = 2.3.0+1.19.3
+ mod_version = 3.0.0+1.19.4
maven_group = eu.pb4
archives_base_name = styledplayerlist
diff --git a/logo.png b/logo.png
index 9fba2a8..1557fa1 100644
Binary files a/logo.png and b/logo.png differ
diff --git a/logo.xcf b/logo.xcf
new file mode 100644
index 0000000..efe2026
Binary files /dev/null and b/logo.xcf differ
diff --git a/logo_512.png b/logo_512.png
new file mode 100644
index 0000000..1f042e2
Binary files /dev/null and b/logo_512.png differ
diff --git a/src/main/java/eu/pb4/styledplayerlist/CardboardWarning.java b/src/main/java/eu/pb4/styledplayerlist/CardboardWarning.java
new file mode 100644
index 0000000..905a18e
--- /dev/null
+++ b/src/main/java/eu/pb4/styledplayerlist/CardboardWarning.java
@@ -0,0 +1,31 @@
+package eu.pb4.styledplayerlist;
+
+import com.mojang.logging.LogUtils;
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
+import org.slf4j.Logger;
+
+public class CardboardWarning implements PreLaunchEntrypoint {
+ public static final Logger LOGGER = LogUtils.getLogger();
+ public static final boolean LOADED = FabricLoader.getInstance().isModLoaded("cardboard") || FabricLoader.getInstance().isModLoaded("banner");
+
+ @Override
+ public void onPreLaunch() {
+ checkAndAnnounce();
+ }
+
+ public static void checkAndAnnounce() {
+ if (LOADED) {
+ LOGGER.error("==============================================");
+ for (var i = 0; i < 4; i++) {
+ LOGGER.error("");
+ LOGGER.error("Cardboard/Banner detected! This mod doesn't work with it!");
+ LOGGER.error("You won't get any support as long as it's present!");
+ LOGGER.error("");
+ LOGGER.error("Read more at: https://gist.github.com/Patbox/e44844294c358b614d347d369b0fc3bf");
+ LOGGER.error("");
+ LOGGER.error("==============================================");
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/pb4/styledplayerlist/GenericModInfo.java b/src/main/java/eu/pb4/styledplayerlist/GenericModInfo.java
new file mode 100644
index 0000000..79c0781
--- /dev/null
+++ b/src/main/java/eu/pb4/styledplayerlist/GenericModInfo.java
@@ -0,0 +1,160 @@
+package eu.pb4.styledplayerlist;
+
+import net.fabricmc.loader.api.ModContainer;
+import net.minecraft.text.*;
+import net.minecraft.util.Formatting;
+
+import javax.imageio.ImageIO;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class GenericModInfo {
+ private static final int COLOR = 0x3d8eff;
+
+ private static Text[] icon = new Text[0];
+ private static Text[] about = new Text[0];
+ private static Text[] consoleAbout = new Text[0];
+
+ public static void build(ModContainer container) {
+ var github = container.getMetadata().getContact().get("sources").orElse("UNKNOWN");
+ {
+ final String chr = "█";
+ var icon = new ArrayList();
+ try {
+ var source = ImageIO.read(Files.newInputStream(container.getPath("assets/styled_player_list/icon_ingame.png")));
+
+ for (int y = 0; y < source.getHeight(); y++) {
+ var base = Text.literal("");
+ int line = 0;
+ int color = source.getRGB(0, y) & 0xFFFFFF;
+ for (int x = 0; x < source.getWidth(); x++) {
+ int colorPixel = source.getRGB(x, y) & 0xFFFFFF;
+
+ if (color == colorPixel) {
+ line++;
+ } else {
+ base.append(Text.literal(chr.repeat(line)).setStyle(Style.EMPTY.withColor(color)));
+ color = colorPixel;
+ line = 1;
+ }
+ }
+
+ base.append(Text.literal(chr.repeat(line)).setStyle(Style.EMPTY.withColor(color)));
+ icon.add(base);
+ }
+ GenericModInfo.icon = icon.toArray(new Text[0]);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ var contributors = new ArrayList();
+
+ container.getMetadata().getAuthors().forEach(x -> contributors.add(x.getName()));
+ container.getMetadata().getContributors().forEach(x -> contributors.add(x.getName()));
+
+ var about = new ArrayList();
+ var extraData = Text.empty();
+ try {
+ extraData.append(Text.literal("[")
+ .append(Text.literal("Contributors")
+ .setStyle(Style.EMPTY.withColor(Formatting.AQUA)
+ .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
+ Text.literal(String.join("\n", contributors))
+ ))
+ ))
+ .append("] ")
+ ).append(Text.literal("[")
+ .append(Text.literal("Github")
+ .setStyle(Style.EMPTY.withColor(Formatting.BLUE).withUnderline(true)
+ .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, github))
+ .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
+ Text.literal(github)
+ ))
+ ))
+ .append("]")).setStyle(Style.EMPTY.withColor(Formatting.DARK_GRAY));
+
+ about.add(Text.empty()
+ .append(Text.literal( container.getMetadata().getName() + " ").setStyle(Style.EMPTY.withColor(COLOR).withBold(true)))
+ .append(Text.literal(container.getMetadata().getVersion().getFriendlyString()).setStyle(Style.EMPTY.withColor(Formatting.WHITE))));
+
+ about.add(Text.literal("» " + container.getMetadata().getDescription()).setStyle(Style.EMPTY.withColor(Formatting.GRAY)));
+
+ about.add(extraData);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ GenericModInfo.consoleAbout = about.toArray(new Text[0]);
+
+ if (icon.length == 0) {
+ GenericModInfo.about = GenericModInfo.consoleAbout;
+ } else {
+ var output = new ArrayList();
+ about.clear();
+ try {
+ about.add(Text.literal(container.getMetadata().getName()).setStyle(Style.EMPTY.withColor(COLOR).withBold(true).withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, github))));
+ about.add(Text.literal("Version: ").setStyle(Style.EMPTY.withColor(0xf7e1a7))
+ .append(Text.literal(container.getMetadata().getVersion().getFriendlyString()).setStyle(Style.EMPTY.withColor(Formatting.WHITE))));
+
+ about.add(extraData);
+ about.add(Text.empty());
+
+ var desc = new ArrayList<>(List.of(container.getMetadata().getDescription().split(" ")));
+
+ if (desc.size() > 0) {
+ StringBuilder descPart = new StringBuilder();
+ while (!desc.isEmpty()) {
+ (descPart.isEmpty() ? descPart : descPart.append(" ")).append(desc.remove(0));
+
+ if (descPart.length() > 16) {
+ about.add(Text.literal(descPart.toString()).setStyle(Style.EMPTY.withColor(Formatting.GRAY)));
+ descPart = new StringBuilder();
+ }
+ }
+
+ if (descPart.length() > 0) {
+ about.add(Text.literal(descPart.toString()).setStyle(Style.EMPTY.withColor(Formatting.GRAY)));
+ }
+ }
+
+ if (icon.length > about.size() + 2) {
+ int a = 0;
+ for (int i = 0; i < icon.length; i++) {
+ if (i == (icon.length - about.size() - 1) / 2 + a && a < about.size()) {
+ output.add(icon[i].copy().append(" ").append(about.get(a++)));
+ } else {
+ output.add(icon[i]);
+ }
+ }
+ } else {
+ Collections.addAll(output, icon);
+ output.addAll(about);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ var invalid = Text.literal("/!\\ [ Invalid about mod info ] /!\\").setStyle(Style.EMPTY.withColor(0xFF0000).withItalic(true));
+
+ output.add(invalid);
+ about.add(invalid);
+ }
+
+ GenericModInfo.about = output.toArray(new Text[0]);
+ }
+ }
+
+ public static Text[] getIcon() {
+ return icon;
+ }
+
+ public static Text[] getAboutFull() {
+ return about;
+ }
+
+ public static Text[] getAboutConsole() {
+ return consoleAbout;
+ }
+}
diff --git a/src/main/java/eu/pb4/styledplayerlist/MicroScheduler.java b/src/main/java/eu/pb4/styledplayerlist/MicroScheduler.java
new file mode 100644
index 0000000..3fe7bb3
--- /dev/null
+++ b/src/main/java/eu/pb4/styledplayerlist/MicroScheduler.java
@@ -0,0 +1,70 @@
+package eu.pb4.styledplayerlist;
+
+import net.minecraft.server.MinecraftServer;
+import org.apache.commons.lang3.mutable.MutableLong;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class MicroScheduler {
+ private final List singleTasks = new CopyOnWriteArrayList<>();
+ private final List repeatingTasks = new CopyOnWriteArrayList<>();
+ private static MicroScheduler INSTANCE;
+ private final MinecraftServer server;
+ private final Thread thread;
+
+ public MicroScheduler(MinecraftServer s) {
+ this.server = s;
+ this.thread = new Thread(this::run);
+ this.thread.start();
+ }
+
+ private void run() {
+ while (!this.server.isStopped()) {
+ try {
+ singleTasks.removeIf(this::executeScheduled);
+ repeatingTasks.forEach(this::executeScheduled);
+ Thread.sleep(10);
+ } catch (Throwable e) {
+
+ }
+ }
+ }
+
+ private void executeScheduled(RepeatingTask repeatingTask) {
+ if (System.currentTimeMillis() >= repeatingTask.time.longValue()) {
+ this.server.execute(repeatingTask.runnable);
+ repeatingTask.time.add(repeatingTask.delay);
+ }
+ }
+
+ private boolean executeScheduled(ScheduledTask scheduledTask) {
+ if (System.currentTimeMillis() >= scheduledTask.time) {
+ this.server.execute(scheduledTask.runnable);
+ return true;
+ }
+ return false;
+ }
+
+ public void scheduleOnce(long delay, Runnable task) {
+ this.singleTasks.add(new ScheduledTask(System.currentTimeMillis() + delay, task));
+ }
+
+ public void scheduleRepeating(long delay, Runnable task) {
+ this.repeatingTasks.add(new RepeatingTask(delay, task));
+ }
+
+ public static MicroScheduler get(MinecraftServer server) {
+ if (INSTANCE == null || INSTANCE.server != server) {
+ INSTANCE = new MicroScheduler(server);
+ }
+ return INSTANCE;
+ }
+
+ private record ScheduledTask(long time, Runnable runnable) {}
+ private record RepeatingTask(long delay, MutableLong time, Runnable runnable) {
+ RepeatingTask(long delay, Runnable runnable) {
+ this(delay, new MutableLong(System.currentTimeMillis() + delay), runnable);
+ }
+ }
+}
diff --git a/src/main/java/eu/pb4/styledplayerlist/PlayerList.java b/src/main/java/eu/pb4/styledplayerlist/PlayerList.java
index d9cf4f1..1425b1e 100644
--- a/src/main/java/eu/pb4/styledplayerlist/PlayerList.java
+++ b/src/main/java/eu/pb4/styledplayerlist/PlayerList.java
@@ -1,15 +1,20 @@
package eu.pb4.styledplayerlist;
+import eu.pb4.placeholders.api.PlaceholderContext;
import eu.pb4.styledplayerlist.access.PlayerListViewerHolder;
import eu.pb4.styledplayerlist.command.Commands;
import eu.pb4.styledplayerlist.config.ConfigManager;
import eu.pb4.styledplayerlist.config.PlayerListStyle;
+import eu.pb4.styledplayerlist.config.data.ConfigData;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.network.packet.s2c.play.PlayerListHeaderS2CPacket;
+import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -20,16 +25,45 @@ public class PlayerList implements ModInitializer {
public static final Logger LOGGER = LogManager.getLogger("Styled Player List");
public static final String ID = "styledplayerlist";
- public static String VERSION = FabricLoader.getInstance().getModContainer(ID).get().getMetadata().getVersion().getFriendlyString();
-
@Override
public void onInitialize() {
- this.crabboardDetection();
+ GenericModInfo.build(FabricLoader.getInstance().getModContainer(ID).get());
Commands.register();
ServerLifecycleEvents.SERVER_STARTING.register((s) -> {
- this.crabboardDetection();
ConfigManager.loadConfig();
});
+
+ ServerLifecycleEvents.SERVER_STARTED.register(s -> {
+ CardboardWarning.checkAndAnnounce();
+ //MicroScheduler.get(s).scheduleRepeating(50, () -> tick(s));
+ });
+ }
+
+ private void tick(MinecraftServer server) {
+ if (ConfigManager.isEnabled()) {
+ ConfigData config = ConfigManager.getConfig().configData;
+ for (var player : server.getPlayerManager().getPlayerList()) {
+ var x = System.nanoTime();
+ if (!SPLHelper.shouldSendPlayerList(player) || player.networkHandler == null) {
+ continue;
+ }
+ var tick = server.getTicks();
+ var holder = (PlayerListViewerHolder) player.networkHandler;
+
+ var style = holder.styledPlayerList$getStyleObject();
+
+ if (tick % style.updateRate == 0) {
+ var context = PlaceholderContext.of(player, SPLHelper.PLAYER_LIST_VIEW);
+ var animationTick = holder.styledPlayerList$getAndIncreaseAnimationTick();
+ player.networkHandler.sendPacket(new PlayerListHeaderS2CPacket(style.getHeader(context, animationTick), style.getFooter(context, animationTick)));
+ }
+
+ if (config.playerName.playerNameUpdateRate > 0 && tick % config.playerName.playerNameUpdateRate == 0) {
+ holder.styledPlayerList$updateName();
+ }
+ player.sendMessage(Text.literal(tick + " | " + ((System.nanoTime() - x) / 1000000f)), true);
+ }
+ }
}
public static Identifier id(String path) {
@@ -75,15 +109,4 @@ public static void addUpdateSkipCheck(ModCompatibility check) {
public interface ModCompatibility {
boolean check(ServerPlayerEntity player);
}
-
- private void crabboardDetection() {
- if (FabricLoader.getInstance().isModLoaded("cardboard")) {
- LOGGER.error("");
- LOGGER.error("Cardboard detected! This mod doesn't work with it!");
- LOGGER.error("You won't get any support as long as it's present!");
- LOGGER.error("");
- LOGGER.error("Read more: https://gist.github.com/Patbox/e44844294c358b614d347d369b0fc3bf");
- LOGGER.error("");
- }
- }
}
diff --git a/src/main/java/eu/pb4/styledplayerlist/SPLHelper.java b/src/main/java/eu/pb4/styledplayerlist/SPLHelper.java
index 3715221..9cd56dc 100644
--- a/src/main/java/eu/pb4/styledplayerlist/SPLHelper.java
+++ b/src/main/java/eu/pb4/styledplayerlist/SPLHelper.java
@@ -1,20 +1,23 @@
package eu.pb4.styledplayerlist;
import carpet.logging.HUDController;
+import eu.pb4.placeholders.api.PlaceholderContext;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.Identifier;
import java.util.HashSet;
import java.util.Set;
public class SPLHelper {
+ public static final PlaceholderContext.ViewObject PLAYER_LIST_VIEW = PlaceholderContext.ViewObject.of(new Identifier("styled_player_list", "player_list"));
+ public static final PlaceholderContext.ViewObject PLAYER_NAME_VIEW = PlaceholderContext.ViewObject.of(new Identifier("styled_player_list", "player_name"));
public static Set COMPATIBILITY = new HashSet<>();
private static final Set BLOCKED_LAST_TIME = new HashSet<>();
static {
FabricLoader loader = FabricLoader.getInstance();
-
if (loader.getModContainer("carpet").isPresent()) {
SPLHelper.COMPATIBILITY.add(player -> {
boolean block = HUDController.player_huds.containsKey(player);
diff --git a/src/main/java/eu/pb4/styledplayerlist/access/PlayerListViewerHolder.java b/src/main/java/eu/pb4/styledplayerlist/access/PlayerListViewerHolder.java
index 7b5ea37..743fba1 100644
--- a/src/main/java/eu/pb4/styledplayerlist/access/PlayerListViewerHolder.java
+++ b/src/main/java/eu/pb4/styledplayerlist/access/PlayerListViewerHolder.java
@@ -1,7 +1,13 @@
package eu.pb4.styledplayerlist.access;
+import eu.pb4.styledplayerlist.config.PlayerListStyle;
+
public interface PlayerListViewerHolder {
void styledPlayerList$setStyle(String key);
String styledPlayerList$getStyle();
void styledPlayerList$updateName();
+ void styledPlayerList$reloadStyle();
+ int styledPlayerList$getAndIncreaseAnimationTick();
+
+ PlayerListStyle styledPlayerList$getStyleObject();
}
diff --git a/src/main/java/eu/pb4/styledplayerlist/command/Commands.java b/src/main/java/eu/pb4/styledplayerlist/command/Commands.java
index 76b5252..76abd03 100644
--- a/src/main/java/eu/pb4/styledplayerlist/command/Commands.java
+++ b/src/main/java/eu/pb4/styledplayerlist/command/Commands.java
@@ -5,7 +5,7 @@
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
-import eu.pb4.styledplayerlist.PlayerList;
+import eu.pb4.styledplayerlist.GenericModInfo;
import eu.pb4.styledplayerlist.access.PlayerListViewerHolder;
import eu.pb4.styledplayerlist.config.ConfigManager;
import eu.pb4.styledplayerlist.config.PlayerListStyle;
@@ -68,17 +68,18 @@ private static int reloadConfig(CommandContext context) {
context.getSource().sendFeedback(Text.literal("Reloaded config!"), false);
} else {
context.getSource().sendError(Text.literal("Error accrued while reloading config!").formatted(Formatting.RED));
-
}
+ for (var player : context.getSource().getServer().getPlayerManager().getPlayerList()) {
+ ((PlayerListViewerHolder) player.networkHandler).styledPlayerList$reloadStyle();
+ }
+
return 1;
}
private static int about(CommandContext context) {
- context.getSource().sendFeedback(Text.literal("Styled Player List")
- .formatted(Formatting.BLUE)
- .append(Text.literal(" - " + PlayerList.VERSION)
- .formatted(Formatting.WHITE)
- ), false);
+ for (var text : (context.getSource().getEntity() instanceof ServerPlayerEntity ? GenericModInfo.getAboutFull() : GenericModInfo.getAboutConsole())) {
+ context.getSource().sendFeedback(text, false);
+ };
return 1;
}
diff --git a/src/main/java/eu/pb4/styledplayerlist/config/Config.java b/src/main/java/eu/pb4/styledplayerlist/config/Config.java
index e652c03..1c4cd91 100644
--- a/src/main/java/eu/pb4/styledplayerlist/config/Config.java
+++ b/src/main/java/eu/pb4/styledplayerlist/config/Config.java
@@ -1,22 +1,32 @@
package eu.pb4.styledplayerlist.config;
+import eu.pb4.placeholders.api.ParserContext;
import eu.pb4.placeholders.api.PlaceholderContext;
import eu.pb4.placeholders.api.Placeholders;
-import eu.pb4.placeholders.api.TextParserUtils;
import eu.pb4.placeholders.api.node.TextNode;
+import eu.pb4.placeholders.api.parsers.NodeParser;
+import eu.pb4.placeholders.api.parsers.PatternPlaceholderParser;
+import eu.pb4.placeholders.api.parsers.StaticPreParser;
+import eu.pb4.placeholders.api.parsers.TextParserV1;
+import eu.pb4.predicate.api.BuiltinPredicates;
+import eu.pb4.predicate.api.MinecraftPredicate;
+import eu.pb4.predicate.api.PredicateContext;
+import eu.pb4.styledplayerlist.SPLHelper;
import eu.pb4.styledplayerlist.config.data.ConfigData;
-import me.lucko.fabric.api.permissions.v0.Permissions;
-import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.regex.Pattern;
public class Config {
- public static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{(?[^}]+)}");
+ public static final NodeParser PARSER = NodeParser.merge(
+ TextParserV1.DEFAULT, Placeholders.DEFAULT_PLACEHOLDER_PARSER,
+ new PatternPlaceholderParser(PatternPlaceholderParser.PREDEFINED_PLACEHOLDER_PATTERN, DynamicNode::of),
+ StaticPreParser.INSTANCE
+ );
public final ConfigData configData;
public final TextNode playerNameFormat;
@@ -24,39 +34,59 @@ public class Config {
public final Text unknownStyleMessage;
public final Text permissionMessage;
private final List permissionNameFormat;
+ public final boolean isHiddenDefault;
+ private final boolean passthroughDefault;
public Config(ConfigData data) {
this.configData = data;
- this.playerNameFormat = Placeholders.parseNodes(TextParserUtils.formatNodes(data.playerNameFormat));
- this.switchMessage = Placeholders.parseNodes(TextParserUtils.formatNodes(data.switchMessage));
- this.unknownStyleMessage = TextParserUtils.formatText(data.unknownStyleMessage);
- this.permissionMessage = TextParserUtils.formatText(data.permissionMessage);
+ this.playerNameFormat = parseText(data.playerName.playerNameFormat);
+ this.switchMessage = parseText(data.messages.switchMessage);
+ this.unknownStyleMessage = parseText(data.messages.unknownStyleMessage).toText();
+ this.permissionMessage = parseText(data.messages.permissionMessage).toText();
+ this.isHiddenDefault = data.playerName.hidePlayer;
+ this.passthroughDefault = data.playerName.ignoreFormatting;
this.permissionNameFormat = new ArrayList<>();
- for (ConfigData.PermissionNameFormat entry : data.permissionNameFormat) {
- this.permissionNameFormat.add(new PermissionNameFormat(entry.permission, entry.opLevel == -1 ? 5 : entry.opLevel, Placeholders.parseNodes(TextParserUtils.formatNodes(entry.style))));
+ for (ConfigData.PermissionNameFormat entry : data.playerName.permissionNameFormat) {
+ this.permissionNameFormat.add(new PermissionNameFormat(entry.require != null ? entry.require : BuiltinPredicates.operatorLevel(5),
+ parseText(entry.format), entry.ignoreFormatting, entry.hidePlayer != null ? entry.hidePlayer : isHiddenDefault));
}
}
+ public static TextNode parseText(String string) {
+ return PARSER.parseNode(string);
+ }
+
public Text getSwitchMessage(ServerPlayerEntity player, String target) {
- return Placeholders.parseText(this.switchMessage, PLACEHOLDER_PATTERN, Map.of("style", Text.literal(target)));
+ return this.switchMessage.toText(ParserContext.of(DynamicNode.NODES, Map.of("style", Text.literal(target))));
}
+ @Nullable
public Text formatPlayerUsername(ServerPlayerEntity player) {
- ServerCommandSource source = player.getCommandSource();
+ var context = PredicateContext.of(player);
+ for (PermissionNameFormat entry : this.permissionNameFormat) {
+ if (entry.predicate.test(context).success()) {
+ return entry.passthrough ? null : entry.style.toText(PlaceholderContext.of(player, SPLHelper.PLAYER_NAME_VIEW));
+ }
+ }
+
+ return this.passthroughDefault ? null :this.playerNameFormat.toText(PlaceholderContext.of(player, SPLHelper.PLAYER_NAME_VIEW));
+ }
+
+ public boolean isPlayerHidden(ServerPlayerEntity player) {
+ var context = PredicateContext.of(player);
for (PermissionNameFormat entry : this.permissionNameFormat) {
- if (Permissions.check(source, entry.permission, entry.opLevel)) {
- Text text = Placeholders.parseText(entry.style, PlaceholderContext.of(player));
- return text;
+ if (entry.predicate.test(context).success()) {
+ return entry.hidden;
}
}
- return Placeholders.parseText(this.playerNameFormat, PlaceholderContext.of(player));
+ return this.isHiddenDefault;
}
- record PermissionNameFormat(String permission, int opLevel, TextNode style) {
+ record PermissionNameFormat(MinecraftPredicate predicate, TextNode style, boolean passthrough, boolean hidden) {
}
}
diff --git a/src/main/java/eu/pb4/styledplayerlist/config/ConfigManager.java b/src/main/java/eu/pb4/styledplayerlist/config/ConfigManager.java
index 9726c0f..64676bb 100644
--- a/src/main/java/eu/pb4/styledplayerlist/config/ConfigManager.java
+++ b/src/main/java/eu/pb4/styledplayerlist/config/ConfigManager.java
@@ -2,21 +2,29 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParser;
+import eu.pb4.predicate.api.GsonPredicateSerializer;
+import eu.pb4.predicate.api.MinecraftPredicate;
import eu.pb4.styledplayerlist.PlayerList;
import eu.pb4.styledplayerlist.config.data.ConfigData;
import eu.pb4.styledplayerlist.config.data.StyleData;
+import eu.pb4.styledplayerlist.config.data.legacy.LegacyConfigData;
+import eu.pb4.styledplayerlist.config.data.legacy.LegacyStyleData;
import net.fabricmc.loader.api.FabricLoader;
import java.io.*;
-import java.nio.file.Paths;
+import java.nio.file.Files;
import java.util.Collection;
import java.util.LinkedHashMap;
public class ConfigManager {
- private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().setLenient().create();
+ public static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().setLenient()
+ .registerTypeHierarchyAdapter(MinecraftPredicate.class, GsonPredicateSerializer.INSTANCE)
+ .registerTypeAdapter(StyleData.ElementList.class, new StyleData.ElementList.Serializer())
+ .create();
private static Config CONFIG;
private static boolean ENABLED = false;
@@ -35,40 +43,83 @@ public static boolean loadConfig() {
CONFIG = null;
try {
+ var configDir = FabricLoader.getInstance().getConfigDir().resolve("styledplayerlist");
- File configStyle = Paths.get("", "config", "styledplayerlist", "styles").toFile();
- File configDir = Paths.get("", "config", "styledplayerlist").toFile();
+ var configStyle = configDir.resolve("styles");
+ var configStyleLegacy = configDir.resolve("styles_old");
- if (configStyle.mkdirs()) {
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(configStyle, "default.json")), "UTF-8"));
+ if (!Files.exists(configStyle)) {
+ Files.createDirectories(configStyle);
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(configStyle.resolve("default.json")), "UTF-8"));
writer.write(GSON.toJson(DefaultValues.exampleStyleData()));
writer.close();
+
+ writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(configStyle.resolve("animated.json")), "UTF-8"));
+ writer.write(GSON.toJson(DefaultValues.exampleAnimatedStyleData()));
+ writer.close();
}
ConfigData config;
- File configFile = new File(configDir, "config.json");
+ var configFile = configDir.resolve("config.json");
+ LegacyConfigData legacyConfigData = null;
+ if (Files.exists(configFile)) {
+ var data = JsonParser.parseString(Files.readString(configFile));
+ if (data.getAsJsonObject().has("CONFIG_VERSION_DONT_TOUCH_THIS")) {
+ legacyConfigData = GSON.fromJson(data, LegacyConfigData.class);
+ Files.writeString(configDir.resolve("config.json_old"), data.toString());
+ config = legacyConfigData.convert();
+ } else {
+ config = GSON.fromJson(data, ConfigData.class);
+ }
- if (configFile.exists()) {
- config = GSON.fromJson(new InputStreamReader(new FileInputStream(configFile), "UTF-8"), ConfigData.class);
} else {
config = new ConfigData();
}
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(configFile), "UTF-8"));
- writer.write(GSON.toJson(config));
- writer.close();
+ Files.writeString(configFile, GSON.toJson(config));
STYLES.clear();
- FilenameFilter filter = (dir, name) -> name.endsWith(".json");
-
- for (String fileName : configStyle.list(filter)) {
- PlayerListStyle style = new PlayerListStyle(GSON.fromJson(new InputStreamReader(new FileInputStream(new File(configStyle, fileName)), "UTF-8"), StyleData.class));
-
- STYLES.put(style.id, style);
- }
+ var finalLegacyConfigData = legacyConfigData;
+ Files.list(configStyle).filter((name) -> !name.endsWith(".json")).forEach((path) -> {
+ String data;
+ try {
+ data = Files.readString(path);;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+
+ var json = JsonParser.parseString(data);
+ StyleData styleData;
+
+ if (json.getAsJsonObject().has("permission")) {
+ styleData = GSON.fromJson(data, LegacyStyleData.class).convert(finalLegacyConfigData);
+
+ try {
+ Files.createDirectories(configStyleLegacy);
+ Files.writeString(configStyleLegacy.resolve(path.getFileName().toString()), data);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ styleData = GSON.fromJson(data, StyleData.class);
+ }
+
+ try {
+ Files.writeString(path, GSON.toJson(styleData));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ var name = path.getFileName().toString();
+ name = name.substring(0, name.length() - 5);
+
+ var style = new PlayerListStyle(name, styleData);
+ STYLES.put(name, style);
+ });
PlayerList.PLAYER_LIST_STYLE_LOAD.invoker().onPlayerListUpdate(new PlayerList.StyleHelper(STYLES));
CONFIG = new Config(config);
diff --git a/src/main/java/eu/pb4/styledplayerlist/config/DefaultValues.java b/src/main/java/eu/pb4/styledplayerlist/config/DefaultValues.java
index dd18122..9771949 100644
--- a/src/main/java/eu/pb4/styledplayerlist/config/DefaultValues.java
+++ b/src/main/java/eu/pb4/styledplayerlist/config/DefaultValues.java
@@ -2,23 +2,97 @@
import eu.pb4.styledplayerlist.config.data.StyleData;
-public class DefaultValues {
- public static PlayerListStyle EMPTY_STYLE = new PlayerListStyle(new StyleData());
+import java.util.List;
+public class DefaultValues {
+ public static PlayerListStyle EMPTY_STYLE = new PlayerListStyle("", new StyleData());
public static StyleData exampleStyleData() {
StyleData data = new StyleData();
- data.header.add("");
- data.header.add(" Styled Player List ⛏ ");
- data.header.add("");
- data.header.add(" [ %server:online%/%server:max_players% ] ");
- data.header.add("");
-
- data.footer.add("");
- data.footer.add(" ");
- data.footer.add("");
- data.footer.add("TPS: %server:tps_colored% | Ping: %player:ping%");
- data.footer.add("");
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+
+ data.footer.values.add(List.of("",
+ " ",
+ "",
+ "TPS: %server:tps_colored% | Ping: %player:ping%",
+ ""));
+
+ return data;
+ }
+
+ public static StyleData exampleAnimatedStyleData() {
+ StyleData data = new StyleData();
+ data.updateRate = 2;
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+ data.header.values.add(List.of("",
+ " Styled Player List ⛏ ",
+ "",
+ " [ %server:online%/%server:max_players% ] ",
+ ""));
+
+
+ data.footer.values.add(List.of("",
+ " ",
+ "",
+ "TPS: %server:tps_colored% | Ping: %player:ping%",
+ ""));
+
+ data.footer.values.add(List.of("",
+ " ",
+ "",
+ "Health: %player:health% | Playtime: %player:playtime%",
+ ""));
+
+ data.footer.values.add(List.of("",
+ " ",
+ "",
+ "Time: %world:time% | World: %world:name%",
+ ""));
+
+ data.footer.changeRate = 20;
return data;
}
diff --git a/src/main/java/eu/pb4/styledplayerlist/config/DynamicNode.java b/src/main/java/eu/pb4/styledplayerlist/config/DynamicNode.java
new file mode 100644
index 0000000..3f50632
--- /dev/null
+++ b/src/main/java/eu/pb4/styledplayerlist/config/DynamicNode.java
@@ -0,0 +1,25 @@
+package eu.pb4.styledplayerlist.config;
+
+import eu.pb4.placeholders.api.ParserContext;
+import eu.pb4.placeholders.api.node.TextNode;
+import net.minecraft.text.Text;
+
+import java.util.Map;
+
+public record DynamicNode(String key, Text text) implements TextNode {
+ public static DynamicNode of(String key) {
+ return new DynamicNode(key, Text.literal("${" + key + "}"));
+ }
+
+ public static final ParserContext.Key