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> NODES = new ParserContext.Key<>("styled_player_list:dynamic", null); + + @Override + public Text toText(ParserContext context, boolean removeBackslashes) { + return context.get(NODES).getOrDefault(this.key, text); + } + + @Override + public boolean isDynamic() { + return true; + } +} diff --git a/src/main/java/eu/pb4/styledplayerlist/config/PlayerListStyle.java b/src/main/java/eu/pb4/styledplayerlist/config/PlayerListStyle.java index df335ef..f95eb8e 100644 --- a/src/main/java/eu/pb4/styledplayerlist/config/PlayerListStyle.java +++ b/src/main/java/eu/pb4/styledplayerlist/config/PlayerListStyle.java @@ -2,55 +2,89 @@ 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.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.config.data.StyleData; -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 java.util.ArrayList; +import java.util.List; + public class PlayerListStyle { + private static final NodeParser PARSER = NodeParser.merge(TextParserV1.DEFAULT, Placeholders.DEFAULT_PLACEHOLDER_PARSER, StaticPreParser.INSTANCE); + public final String id; public final String name; - public final TextNode header; - public final TextNode footer; + public final AnimatedText header; + public final AnimatedText footer; - public final Boolean hidden; - private final String permission; + public final int updateRate; + public final boolean hidden; + private final MinecraftPredicate require; - public PlayerListStyle(StyleData data) { - this.id = data.id; + public PlayerListStyle(String id, StyleData data) { + this.id = id; this.name = data.name; - this.header = Placeholders.parseNodes(TextParserUtils.formatNodes(String.join("\n", data.header))); - this.footer = Placeholders.parseNodes(TextParserUtils.formatNodes(String.join("\n", data.footer))); + this.updateRate = data.updateRate; + + this.header = AnimatedText.from(data.header, data.legacyJoinBehaviour == Boolean.TRUE); + this.footer = AnimatedText.from(data.footer, data.legacyJoinBehaviour == Boolean.TRUE); this.hidden = data.hidden; - this.permission = data.permission; + this.require = data.require != null ? data.require : BuiltinPredicates.operatorLevel(0); } public boolean hasPermission(ServerPlayerEntity player) { - if (this.permission.length() == 0) { - return true; - } else { - return Permissions.check(player, this.permission, 2); - } + return this.require.test(PredicateContext.of(player)).success(); } public boolean hasPermission(ServerCommandSource source) { - if (this.permission.length() == 0) { - return true; - } else { - return Permissions.check(source, this.permission, 2); - } + return this.require.test(PredicateContext.of(source)).success(); + } + + public Text getHeader(PlaceholderContext context, int tick) { + return this.header.getFor(tick).toText(context); } - public Text getHeader(ServerPlayerEntity player) { - return this.header.toText(PlaceholderContext.of(player).asParserContext(), true); + public Text getFooter(PlaceholderContext context, int tick) { + return this.footer.getFor(tick).toText(context); } - public Text getFooter(ServerPlayerEntity player) { - return this.footer.toText(PlaceholderContext.of(player).asParserContext(), true); + private interface AnimatedText { + TextNode getFor(int tick); + + static AnimatedText from(StyleData.ElementList elementList, boolean legacy) { + if (elementList.values.isEmpty()) { + return AnimatedText.of(TextNode.empty()); + } + + var joiner = legacy ? "\n" : "\n"; + + if (elementList.values.size() == 1) { + return AnimatedText.of(PARSER.parseNode(String.join(joiner, elementList.values.get(0)))); + } else { + var list = new ArrayList(); + for (var x : elementList.values) { + list.add(PARSER.parseNode(String.join(joiner, x))); + } + return of(list, Math.max(elementList.changeRate, 1)); + } + } + + static AnimatedText of(TextNode node) { + return x -> node; + } + + static AnimatedText of(List node, int time) { + return x -> node.get((x / time) % node.size()); + } } } diff --git a/src/main/java/eu/pb4/styledplayerlist/config/data/ConfigData.java b/src/main/java/eu/pb4/styledplayerlist/config/data/ConfigData.java index 0a6ded1..43964a0 100644 --- a/src/main/java/eu/pb4/styledplayerlist/config/data/ConfigData.java +++ b/src/main/java/eu/pb4/styledplayerlist/config/data/ConfigData.java @@ -1,26 +1,62 @@ package eu.pb4.styledplayerlist.config.data; +import com.google.gson.annotations.SerializedName; +import eu.pb4.predicate.api.MinecraftPredicate; + import java.util.ArrayList; import java.util.List; public class ConfigData { - public int CONFIG_VERSION_DONT_TOUCH_THIS = 1; + @SerializedName("config_version") + public int version = 2; + @SerializedName("__comment") public String _comment = "Before changing anything, see https://github.com/Patbox/StyledPlayerList#configuration"; + @SerializedName("default_style") public String defaultStyle = "default"; - public long updateRate = 20; + @SerializedName("messages") + public Messages messages = new Messages(); + @SerializedName("player") + public PlayerName playerName = new PlayerName(); + + @SerializedName("client_show_in_singleplayer") public boolean displayOnSingleplayer = true; - public String switchMessage = "Your player list style has been changed to: ${style}"; - public String unknownStyleMessage = "This style doesn't exist!"; - public String permissionMessage = "You don't have required permissions!"; - public boolean changePlayerName = false; - public String playerNameFormat = "%player:displayname%"; - public boolean updatePlayerNameEveryChatMessage = false; - public long playerNameUpdateRate = -1; - public List permissionNameFormat = new ArrayList<>(); + + public static class Messages { + @SerializedName("switch") + public String switchMessage = "Your player list style has been changed to: ${style}"; + @SerializedName("unknown") + public String unknownStyleMessage = "This style doesn't exist!"; + @SerializedName("no_permission") + public String permissionMessage = "You don't have required permissions!"; + } + + public static class PlayerName { + @SerializedName("modify_name") + public boolean changePlayerName = false; + @SerializedName("passthrough") + public boolean ignoreFormatting = false; + @SerializedName("hidden") + public boolean hidePlayer = false; + @SerializedName("format") + public String playerNameFormat = "%player:displayname%"; + @SerializedName("update_on_chat_message") + public boolean updatePlayerNameEveryChatMessage = false; + @SerializedName("update_tick_time") + public long playerNameUpdateRate = -1; + @SerializedName("styles") + public List permissionNameFormat = new ArrayList<>(); + + } public static class PermissionNameFormat { - public String permission = ""; - public int opLevel = -1; - public String style = ""; + @SerializedName("require") + public MinecraftPredicate require; + @SerializedName("format") + public String format = ""; + @SerializedName("passthrough") + public boolean ignoreFormatting = false; + @SerializedName("hidden") + public Boolean hidePlayer; + } } diff --git a/src/main/java/eu/pb4/styledplayerlist/config/data/StyleData.java b/src/main/java/eu/pb4/styledplayerlist/config/data/StyleData.java index 683e883..d5fe106 100644 --- a/src/main/java/eu/pb4/styledplayerlist/config/data/StyleData.java +++ b/src/main/java/eu/pb4/styledplayerlist/config/data/StyleData.java @@ -1,13 +1,75 @@ package eu.pb4.styledplayerlist.config.data; +import com.google.common.reflect.TypeToken; +import com.google.gson.*; +import com.google.gson.annotations.SerializedName; +import eu.pb4.predicate.api.MinecraftPredicate; +import org.jetbrains.annotations.Nullable; + +import javax.lang.model.util.Types; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; public class StyleData { - public String id = "default"; + @Nullable + @SerializedName("require") + public MinecraftPredicate require; + + @SerializedName("style_name") public String name = "Default"; - public List header = new ArrayList<>(); - public List footer = new ArrayList<>(); + @SerializedName("update_tick_time") + public int updateRate = 20; + @SerializedName("list_header") + public ElementList header = new ElementList(); + @SerializedName("list_footer") + public ElementList footer = new ElementList(); + @SerializedName("hidden_in_commands") public boolean hidden = false; - public String permission = ""; + + @Nullable + @SerializedName("legacy_line_joining (Remove once updating!)") + public Boolean legacyJoinBehaviour; + + + public static class ElementList { + @SerializedName("values") + public List> values = new ArrayList<>(); + @SerializedName("change_rate") + public int changeRate = 1; + + public static class Serializer implements JsonSerializer, JsonDeserializer { + static final Type LIST_TYPE = new TypeToken>() {}.getType(); + static final Type LIST_LIST_TYPE = new TypeToken>>() {}.getType(); + @Override + public ElementList deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + var e = new ElementList(); + if (jsonElement.isJsonArray()) { + e.values.add(jsonDeserializationContext.deserialize(jsonElement, LIST_TYPE)); + } else if (jsonElement.isJsonObject()) { + var obj = jsonElement.getAsJsonObject(); + e.changeRate = obj.get("change_rate").getAsInt(); + e.values = jsonDeserializationContext.deserialize(obj.get("values"), LIST_LIST_TYPE); + } else if (jsonElement.isJsonPrimitive()) { + e.values.add(List.of(jsonElement.getAsString())); + } + + return e; + } + + @Override + public JsonElement serialize(ElementList elementList, Type type, JsonSerializationContext jsonSerializationContext) { + if (elementList.values.isEmpty()) { + return new JsonArray(); + } else if (elementList.values.size() == 1) { + return jsonSerializationContext.serialize(elementList.values.get(0)); + } + var obj = new JsonObject(); + obj.addProperty("change_rate", elementList.changeRate); + obj.add("values", jsonSerializationContext.serialize(elementList.values)); + + return obj; + } + } + } } diff --git a/src/main/java/eu/pb4/styledplayerlist/config/data/legacy/LegacyConfigData.java b/src/main/java/eu/pb4/styledplayerlist/config/data/legacy/LegacyConfigData.java new file mode 100644 index 0000000..e79f4b5 --- /dev/null +++ b/src/main/java/eu/pb4/styledplayerlist/config/data/legacy/LegacyConfigData.java @@ -0,0 +1,52 @@ +package eu.pb4.styledplayerlist.config.data.legacy; + +import eu.pb4.predicate.api.BuiltinPredicates; +import eu.pb4.styledplayerlist.config.data.ConfigData; + +import java.util.ArrayList; +import java.util.List; + +@Deprecated +public class LegacyConfigData { + public int CONFIG_VERSION_DONT_TOUCH_THIS = 1; + public String _comment = "Before changing anything, see https://github.com/Patbox/StyledPlayerList#configuration"; + public String defaultStyle = "default"; + public int updateRate = 20; + public boolean displayOnSingleplayer = true; + public String switchMessage = "Your player list style has been changed to: ${style}"; + public String unknownStyleMessage = "This style doesn't exist!"; + public String permissionMessage = "You don't have required permissions!"; + public boolean changePlayerName = false; + public String playerNameFormat = "%player:displayname%"; + public boolean updatePlayerNameEveryChatMessage = false; + public long playerNameUpdateRate = -1; + public List permissionNameFormat = new ArrayList<>(); + + public ConfigData convert() { + var x = new ConfigData(); + x.displayOnSingleplayer = this.displayOnSingleplayer; + x.defaultStyle = this.defaultStyle; + x.messages.permissionMessage = this.permissionMessage; + x.messages.switchMessage = this.switchMessage; + x.messages.unknownStyleMessage = this.unknownStyleMessage; + x.playerName.changePlayerName = this.changePlayerName; + x.playerName.playerNameFormat = this.playerNameFormat; + x.playerName.updatePlayerNameEveryChatMessage = this.updatePlayerNameEveryChatMessage; + x.playerName.playerNameUpdateRate = this.playerNameUpdateRate; + + for (var perm : this.permissionNameFormat) { + var a = new ConfigData.PermissionNameFormat(); + a.require = BuiltinPredicates.modPermissionApi(perm.permission, perm.opLevel); + a.format = perm.style; + x.playerName.permissionNameFormat.add(a); + } + + return x; + } + + public static class PermissionNameFormat { + public String permission = ""; + public int opLevel = -1; + public String style = ""; + } +} diff --git a/src/main/java/eu/pb4/styledplayerlist/config/data/legacy/LegacyStyleData.java b/src/main/java/eu/pb4/styledplayerlist/config/data/legacy/LegacyStyleData.java new file mode 100644 index 0000000..e9d85fb --- /dev/null +++ b/src/main/java/eu/pb4/styledplayerlist/config/data/legacy/LegacyStyleData.java @@ -0,0 +1,37 @@ +package eu.pb4.styledplayerlist.config.data.legacy; + +import eu.pb4.predicate.api.BuiltinPredicates; +import eu.pb4.predicate.api.MinecraftPredicate; +import eu.pb4.styledplayerlist.config.data.StyleData; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +@Deprecated +public class LegacyStyleData { + public String id = "default"; + public String name = "Default"; + public List header = new ArrayList<>(); + public List footer = new ArrayList<>(); + public boolean hidden = false; + public String permission = ""; + + public StyleData convert(@Nullable LegacyConfigData configData) { + var style = new StyleData(); + style.header.values.add(this.header); + style.footer.values.add(this.footer); + style.name = this.name; + style.legacyJoinBehaviour = Boolean.TRUE; + + if (!this.permission.isEmpty()) { + style.require = BuiltinPredicates.modPermissionApi(this.permission); + } + + if (configData != null) { + style.updateRate = configData.updateRate; + } + + return style; + } +} diff --git a/src/main/java/eu/pb4/styledplayerlist/mixin/PlayerListS2CPacketEntryMixin.java b/src/main/java/eu/pb4/styledplayerlist/mixin/PlayerListS2CPacketEntryMixin.java new file mode 100644 index 0000000..d141b94 --- /dev/null +++ b/src/main/java/eu/pb4/styledplayerlist/mixin/PlayerListS2CPacketEntryMixin.java @@ -0,0 +1,16 @@ +package eu.pb4.styledplayerlist.mixin; + +import eu.pb4.styledplayerlist.config.ConfigManager; +import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(PlayerListS2CPacket.Entry.class) +public class PlayerListS2CPacketEntryMixin { + @ModifyConstant(method = "(Lnet/minecraft/server/network/ServerPlayerEntity;)V", constant = @Constant(intValue = 1, ordinal = 0)) + private static int styledPlayerList$hideRealPlayer(int constant, ServerPlayerEntity player) { + return ConfigManager.getConfig().isPlayerHidden(player) ? 0 : 1; + } +} diff --git a/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayNetworkManagerMixin.java b/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayNetworkManagerMixin.java index 7b493f3..f1d4933 100644 --- a/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayNetworkManagerMixin.java +++ b/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayNetworkManagerMixin.java @@ -1,15 +1,18 @@ package eu.pb4.styledplayerlist.mixin; +import eu.pb4.placeholders.api.PlaceholderContext; import eu.pb4.playerdata.api.PlayerDataApi; +import eu.pb4.styledplayerlist.PlayerList; import eu.pb4.styledplayerlist.SPLHelper; import eu.pb4.styledplayerlist.access.PlayerListViewerHolder; import eu.pb4.styledplayerlist.config.ConfigManager; +import eu.pb4.styledplayerlist.config.DefaultValues; import eu.pb4.styledplayerlist.config.PlayerListStyle; import eu.pb4.styledplayerlist.config.data.ConfigData; import net.minecraft.nbt.NbtString; import net.minecraft.network.ClientConnection; -import net.minecraft.network.Packet; import net.minecraft.network.message.SignedMessage; +import net.minecraft.network.packet.Packet; import net.minecraft.network.packet.s2c.play.PlayerListHeaderS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; import net.minecraft.server.MinecraftServer; @@ -23,6 +26,9 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.EnumSet; +import java.util.List; + import static eu.pb4.styledplayerlist.PlayerList.id; @Mixin(ServerPlayNetworkHandler.class) @@ -38,15 +44,20 @@ public abstract class ServerPlayNetworkManagerMixin implements PlayerListViewerH private String styledPlayerList$activeStyle = ConfigManager.getDefault(); @Unique - private long styledPlayerList$ticker = 0; + private PlayerListStyle styledPlayerList$style = DefaultValues.EMPTY_STYLE; + + @Unique + private int styledPlayerList$animationTick = 0; @Inject(method = "", at = @At("TAIL")) private void styledPlayerList$loadData(MinecraftServer server, ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci) { try { - NbtString nickname = PlayerDataApi.getGlobalDataFor(player, id("style"), NbtString.TYPE); + NbtString style = PlayerDataApi.getGlobalDataFor(player, id("style"), NbtString.TYPE); - if (nickname != null) { - this.styledPlayerList$setStyle(nickname.asString()); + if (style != null) { + this.styledPlayerList$setStyle(style.asString()); + } else { + this.styledPlayerList$reloadStyle(); } } catch (Exception e) { e.printStackTrace(); @@ -56,41 +67,41 @@ public abstract class ServerPlayNetworkManagerMixin implements PlayerListViewerH @Inject(method = "tick", at = @At("TAIL")) private void styledPlayerList$updatePlayerList(CallbackInfo ci) { if (ConfigManager.isEnabled() && SPLHelper.shouldSendPlayerList(this.player)) { + var tick = this.server.getTicks(); ConfigData config = ConfigManager.getConfig().configData; - if (this.styledPlayerList$ticker % config.updateRate == 0) { - PlayerListStyle style = ConfigManager.getStyle(this.styledPlayerList$activeStyle); - this.sendPacket(new PlayerListHeaderS2CPacket(style.getHeader(this.player), style.getFooter(this.player))); + + if (tick % this.styledPlayerList$style.updateRate == 0) { + var context = PlaceholderContext.of(this.player, SPLHelper.PLAYER_LIST_VIEW); + this.sendPacket(new PlayerListHeaderS2CPacket(this.styledPlayerList$style.getHeader(context, this.styledPlayerList$animationTick), this.styledPlayerList$style.getFooter(context, this.styledPlayerList$animationTick))); + this.styledPlayerList$animationTick += 1; } - if (config.playerNameUpdateRate > 0 && this.styledPlayerList$ticker % config.playerNameUpdateRate == 0) { + if (config.playerName.playerNameUpdateRate > 0 && tick % config.playerName.playerNameUpdateRate == 0) { this.styledPlayerList$updateName(); } - this.styledPlayerList$ticker += 1; } } - - @Inject(method = "handleDecoratedMessage", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayNetworkHandler;checkForSpam()V")) private void styledPlayerList$onMessage(SignedMessage signedMessage, CallbackInfo ci) { - if (ConfigManager.isEnabled() && ConfigManager.getConfig().configData.updatePlayerNameEveryChatMessage) { + if (ConfigManager.isEnabled() && ConfigManager.getConfig().configData.playerName.updatePlayerNameEveryChatMessage) { this.styledPlayerList$updateName(); } } - @Override public void styledPlayerList$setStyle(String key) { if (ConfigManager.isEnabled()) { if (ConfigManager.styleExist(key)) { this.styledPlayerList$activeStyle = key; } else { - this.styledPlayerList$activeStyle = ConfigManager.getConfig().configData.defaultStyle; + this.styledPlayerList$activeStyle = ConfigManager.getDefault(); } } else { this.styledPlayerList$activeStyle = "default"; } + styledPlayerList$reloadStyle(); PlayerDataApi.setGlobalDataFor(this.player, id("style"), NbtString.of(this.styledPlayerList$activeStyle)); } @@ -104,12 +115,31 @@ public abstract class ServerPlayNetworkManagerMixin implements PlayerListViewerH @Override public void styledPlayerList$updateName() { try { - if (ConfigManager.isEnabled() && ConfigManager.getConfig().configData.changePlayerName) { - PlayerListS2CPacket packet = new PlayerListS2CPacket(PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME, this.player); + if (ConfigManager.isEnabled() && ConfigManager.getConfig().configData.playerName.changePlayerName) { + PlayerListS2CPacket packet = new PlayerListS2CPacket(EnumSet.of(PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME, PlayerListS2CPacket.Action.UPDATE_LISTED), List.of(this.player)); this.server.getPlayerManager().sendToAll(packet); } } catch (Exception e) { } } + + @Override + public void styledPlayerList$reloadStyle() { + var style = ConfigManager.getStyle(this.styledPlayerList$activeStyle); + if (style != this.styledPlayerList$style) { + this.styledPlayerList$style = style; + this.styledPlayerList$animationTick = 0; + } + } + + @Override + public int styledPlayerList$getAndIncreaseAnimationTick() { + return this.styledPlayerList$animationTick++; + } + + @Override + public PlayerListStyle styledPlayerList$getStyleObject() { + return this.styledPlayerList$style; + } } diff --git a/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayerEntityMixin.java b/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayerEntityMixin.java index 659bb98..4c29142 100644 --- a/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayerEntityMixin.java +++ b/src/main/java/eu/pb4/styledplayerlist/mixin/ServerPlayerEntityMixin.java @@ -13,7 +13,7 @@ public class ServerPlayerEntityMixin { @Inject(method = "getPlayerListName", at = @At("HEAD"), cancellable = true) private void styledPlayerList$changePlayerListName(CallbackInfoReturnable cir) { try { - if (ConfigManager.isEnabled() && ConfigManager.getConfig().configData.changePlayerName) { + if (ConfigManager.isEnabled() && ConfigManager.getConfig().configData.playerName.changePlayerName) { cir.setReturnValue(ConfigManager.getConfig().formatPlayerUsername((ServerPlayerEntity) (Object) this)); } } catch (Exception e) { diff --git a/src/main/resources/assets/styled_player_list/icon_ingame.png b/src/main/resources/assets/styled_player_list/icon_ingame.png new file mode 100644 index 0000000..933a8f9 Binary files /dev/null and b/src/main/resources/assets/styled_player_list/icon_ingame.png differ diff --git a/src/main/resources/assets/styled_player_list/logo.png b/src/main/resources/assets/styled_player_list/logo.png new file mode 100644 index 0000000..1557fa1 Binary files /dev/null and b/src/main/resources/assets/styled_player_list/logo.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index e896ce6..d4501bf 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -12,20 +12,22 @@ "homepage": "https://pb4.eu", "sources": "https://github.com/Patbox/StyledPlayerList" }, - + "icon": "assets/styled_player_list/logo.png", "license": "LGPLv3", "environment": "*", "entrypoints": { "main": [ "eu.pb4.styledplayerlist.PlayerList" + ], + "preLaunch": [ + "eu.pb4.styledplayerlist.CardboardWarning" ] }, "mixins": [ "styledplayerlist.mixins.json" ], - "depends": { - "minecraft": ">=1.19.2" + "minecraft": ">=1.19.4" } } diff --git a/src/main/resources/styledplayerlist.mixins.json b/src/main/resources/styledplayerlist.mixins.json index a0bd9d5..d9b313e 100644 --- a/src/main/resources/styledplayerlist.mixins.json +++ b/src/main/resources/styledplayerlist.mixins.json @@ -4,6 +4,7 @@ "package": "eu.pb4.styledplayerlist.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ + "PlayerListS2CPacketEntryMixin", "ServerPlayerEntityMixin", "ServerPlayNetworkManagerMixin" ],