diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f967e81..6badfbd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -48,6 +48,7 @@ jobs: strategy: matrix: game_version: [ # Update this when adding new game versions! + "1.21.11", "1.21.6", "1.21.5", "1.21.4", diff --git a/build.gradle.kts b/build.gradle.kts index 2a4629d..fde16cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ import xyz.srnyx.gradlegalaxy.utility.setupJava plugins { java - id("fabric-loom") version "1.11-SNAPSHOT" + id("fabric-loom") version "1.14-SNAPSHOT" id("xyz.srnyx.gradle-galaxy") version "2.0.2" } @@ -37,10 +37,12 @@ dependencies { if (hasProperty("deps.placeholder_api")) dependencies.modCompileOnly("eu.pb4", "placeholder-api", property("deps.placeholder_api").toString()) // Replacements for fabric.mod.json and config.json +val mixinConfig = if (stonecutter.current.version == "1.21.11") "eventutils-1.21.11.mixin.json" else "eventutils.mixin.json" addReplacementsTask(setOf("fabric.mod.json"), getDefaultReplacements() + mapOf( "mod_name" to property("mod.name").toString(), "mod_version" to property("mod.version").toString(), - "deps_minecraft" to property("deps.minecraft").toString())) + "deps_minecraft" to property("deps.minecraft").toString(), + "mixin_config" to mixinConfig)) base { archivesName = rootProject.name diff --git a/crop_sheet.py b/crop_sheet.py new file mode 100644 index 0000000..ab67395 --- /dev/null +++ b/crop_sheet.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Crop sheet.png (3 cols x 2 rows) into individual plus-tag icons with transparent background.""" +from pathlib import Path + +try: + from PIL import Image +except ImportError: + print("Install Pillow: pip install Pillow") + raise + +# Order: row0 = bee, white, linked; row1 = booster, contrib, admin +NAMES = ["bee", "white", "linked", "booster", "contrib", "admin"] +ICON_SIZE = 64 # match PlusTagRenderer.TEX_SIZE for sharp in-game scaling +# Pixels within this distance of the sheet background color become transparent (0–255 per channel) +BG_TOLERANCE = 25 + +SCRIPT_DIR = Path(__file__).resolve().parent +OUT_DIR = SCRIPT_DIR / "src" / "main" / "resources" / "assets" / "eventutils" / "textures" / "gui" +# Prefer sheet in project root, then plus_sheet in gui folder +SHEET_CANDIDATES = [SCRIPT_DIR / "sheet.png", OUT_DIR / "plus_sheet.png"] + + +def make_bg_transparent(img: Image.Image, bg_rgba: tuple, tolerance: int) -> Image.Image: + """Replace pixels matching the background color (within tolerance) with transparent.""" + data = img.getdata() + r0, g0, b0, a0 = bg_rgba + out = [] + for p in data: + if len(p) == 3: + r, g, b = p + if abs(r - r0) <= tolerance and abs(g - g0) <= tolerance and abs(b - b0) <= tolerance: + out.append((0, 0, 0, 0)) + else: + out.append((r, g, b, 255)) + else: + r, g, b, a = p + if abs(r - r0) <= tolerance and abs(g - g0) <= tolerance and abs(b - b0) <= tolerance: + out.append((0, 0, 0, 0)) + else: + out.append((r, g, b, a)) + img.putdata(out) + return img + + +def main(): + SHEET = next((p for p in SHEET_CANDIDATES if p.exists()), None) + if SHEET is None: + print(f"Not found: tried {SHEET_CANDIDATES}") + return 1 + print(f"Using sheet: {SHEET}") + img = Image.open(SHEET).convert("RGBA") + w, h = img.size + # Use top-left corner as background color + bg_rgba = img.getpixel((0, 0)) + if len(bg_rgba) == 3: + bg_rgba = (bg_rgba[0], bg_rgba[1], bg_rgba[2], 255) + print(f"Background color (will be made transparent): {bg_rgba}") + + col_w = w // 3 + row_h = h // 2 + OUT_DIR.mkdir(parents=True, exist_ok=True) + idx = 0 + for row in range(2): + for col in range(3): + x = col * col_w + y = row * row_h + cw = (w - x) if col == 2 else col_w + ch = (h - y) if row == 1 else row_h + crop = img.crop((x, y, x + cw, y + ch)) + crop = crop.resize((ICON_SIZE, ICON_SIZE), Image.Resampling.LANCZOS) + crop = make_bg_transparent(crop, bg_rgba, BG_TOLERANCE) + out_path = OUT_DIR / f"{NAMES[idx]}.png" + crop.save(out_path) + print(f"Saved {out_path.name} ({ICON_SIZE}x{ICON_SIZE}, transparent bg)") + idx += 1 + print("Done.") + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e11132..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index d0b80aa..028f65a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ stonecutter { centralScript = "build.gradle.kts" shared { versions( // Make sure to update .github/workflows/publish.yml when changing versions! + "1.21.11", "1.21.6", "1.21.5", "1.21.4", diff --git a/sheet.png b/sheet.png new file mode 100644 index 0000000..3848d16 Binary files /dev/null and b/sheet.png differ diff --git a/src/main/java/cc/aabss/eventutils/EventInfoScreen.java b/src/main/java/cc/aabss/eventutils/EventInfoScreen.java index 9f2da09..041c2ef 100644 --- a/src/main/java/cc/aabss/eventutils/EventInfoScreen.java +++ b/src/main/java/cc/aabss/eventutils/EventInfoScreen.java @@ -26,7 +26,11 @@ public class EventInfoScreen extends Screen { @NotNull private final JsonObject json; public EventInfoScreen(@NotNull JsonObject json) { + //? if >=1.21.11 { + /*super(Text.translatable(EventUtils.MOD.keybindManager.eventInfoKey.getId())); + *///?} else { super(Text.translatable(EventUtils.MOD.keybindManager.eventInfoKey.getTranslationKey())); + //?} this.json = json; } diff --git a/src/main/java/cc/aabss/eventutils/EventUtils.java b/src/main/java/cc/aabss/eventutils/EventUtils.java index d01906b..4b40628 100644 --- a/src/main/java/cc/aabss/eventutils/EventUtils.java +++ b/src/main/java/cc/aabss/eventutils/EventUtils.java @@ -5,6 +5,8 @@ import cc.aabss.eventutils.websocket.SocketEndpoint; import cc.aabss.eventutils.websocket.WebSocketClient; import cc.aabss.eventutils.config.EventConfig; +import cc.aabss.eventutils.config.PlayerGroup; +import cc.aabss.eventutils.plustag.EventAlertsApi; import com.google.gson.JsonObject; @@ -59,7 +61,8 @@ public class EventUtils implements ClientModInitializer { public KeybindManager keybindManager; @NotNull public final EventServerManager eventServerManager = new EventServerManager(this); @NotNull public final Map lastIps = new EnumMap<>(EventType.class); - public boolean hidePlayers = false; + /** 0 = first group (or hide-all when no groups), 1 = second group, ... ; groups.size() = players revealed */ + public int hidePlayersViewMode = 0; public EventUtils() { MOD = this; @@ -85,6 +88,21 @@ public void onInitializeClient() { // Update checker ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> updateChecker.checkUpdate()); + // Fetch Event Alerts plus tags for local player + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { + if (client.player != null) { + var uuid = client.player.getUuid(); + LOGGER.info("[EventUtils] JOIN: scheduling Event Alerts fetch for local player uuid={}", uuid); + EventAlertsApi.scheduleFetchIfNeeded(uuid.toString()); + } else { + LOGGER.info("[EventUtils] JOIN: client.player is null, skipping fetch (will retry when tab list is opened)"); + } + }); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + LOGGER.info("[EventUtils] DISCONNECT: clearing Event Alerts cache"); + EventAlertsApi.clearCache(); + }); + // Initialize keybind manager keybindManager = new KeybindManager(this); @@ -146,6 +164,49 @@ public static boolean isNPC(@NotNull String name) { return isNPC(name, false); } + /** Whether the current view mode is "players revealed" (show everyone). */ + public boolean isHidePlayersRevealed() { + final int n = config.groups.size(); + if (n == 0) return hidePlayersViewMode == 1; + return hidePlayersViewMode >= n; + } + + /** Whether we are in a "hide" mode (any group or hide-all). */ + public boolean isInHidePlayersMode() { + final int n = config.groups.size(); + if (n == 0) return hidePlayersViewMode == 0; + return hidePlayersViewMode < n; + } + + /** Current group when in group view mode, or null if revealed or no groups. */ + @Nullable + public PlayerGroup getCurrentViewGroup() { + final var groups = config.groups; + if (groups.isEmpty() || hidePlayersViewMode >= groups.size()) return null; + return groups.get(hidePlayersViewMode); + } + + /** + * True if the player (by lowercased name) should be visible with current view mode. + * Caller must exclude main player. + */ + public boolean isPlayerVisible(@NotNull String nameLower) { + if (isHidePlayersRevealed()) return true; + if (config.whitelistedPlayers.contains(nameLower) || isNPC(nameLower)) return true; + final PlayerGroup group = getCurrentViewGroup(); + if (group == null) return false; // no groups, hide mode: only whitelist/NPC + return group.containsPlayer(nameLower); + } + + /** True if the nametag for this visible player should be drawn (per-group setting when in group view). */ + public boolean shouldShowNametagFor(@NotNull String nameLower) { + if (!isInHidePlayersMode()) return true; + final PlayerGroup group = getCurrentViewGroup(); + if (group == null) return true; // hide-all with no groups: use default + if (!group.containsPlayer(nameLower)) return true; // whitelist/NPC visibility: show nametag + return group.isShowNametags(); + } + @Contract(pure = true) public static int max(int... values) { int max = Integer.MIN_VALUE; diff --git a/src/main/java/cc/aabss/eventutils/KeybindManager.java b/src/main/java/cc/aabss/eventutils/KeybindManager.java index 0232d61..24d30a4 100644 --- a/src/main/java/cc/aabss/eventutils/KeybindManager.java +++ b/src/main/java/cc/aabss/eventutils/KeybindManager.java @@ -10,6 +10,9 @@ import net.minecraft.client.util.InputUtil; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +//? if >=1.21.11 { +/*import net.minecraft.util.Identifier; +*///?} import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -19,6 +22,7 @@ import java.util.HashMap; import java.util.Map; +import static net.minecraft.text.Text.literal; import static net.minecraft.text.Text.translatable; @@ -31,22 +35,36 @@ public class KeybindManager { public KeybindManager(@NotNull EventUtils mod) { // Keybindings + //? if >=1.21.11 { + /*final KeyBinding.Category category = KeyBinding.Category.create(Identifier.of("eventutils", "key.category.eventutils")); + eventInfoKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.eventutils.eventinfo", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_RIGHT_SHIFT, + category)); + final KeyBinding hidePlayersKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.eventutils.hideplayers", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_F10, + category)); + *///?} else { eventInfoKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( "key.eventutils.eventinfo", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_SHIFT, CATEGORY)); + final KeyBinding hidePlayersKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.eventutils.hideplayers", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_F10, + CATEGORY)); + //?} // DEV: Uncomment to force test event // final KeyBindingMixin testEventKey = (KeyBindingMixin) KeyBindingHelper.registerKeyBinding(new KeyBinding( // "key.eventutils.testevent", // InputUtil.Type.KEYSYM, // GLFW.GLFW_KEY_SEMICOLON, // CATEGORY)); - final KeyBinding hidePlayersKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( - "key.eventutils.hideplayers", - InputUtil.Type.KEYSYM, - GLFW.GLFW_KEY_F10, - CATEGORY)); ClientTickEvents.END_CLIENT_TICK.register(client -> { if (windowHandle == null) windowHandle = client.getWindow().getHandle(); @@ -79,17 +97,31 @@ public KeybindManager(@NotNull EventUtils mod) { // In-game keybinds if (client.player == null) return; - // Hide players key + // Hide players key: cycle Group 1 -> Group 2 -> ... -> Players Revealed -> repeat if (hidePlayersKey.wasPressed()) { - mod.hidePlayers = !mod.hidePlayers; - client.player.sendMessage(translatable(mod.hidePlayers ? "eventutils.hideplayers.enabled" : "eventutils.hideplayers.disabled") - .formatted(mod.hidePlayers ? Formatting.GREEN : Formatting.RED), true); + final int groupCount = mod.config.groups.size(); + final int totalStates = groupCount == 0 ? 2 : groupCount + 1; + mod.hidePlayersViewMode = (mod.hidePlayersViewMode + 1) % totalStates; + final boolean revealed = EventUtils.MOD.isHidePlayersRevealed(); + final Text message; + if (revealed) { + message = translatable("eventutils.hideplayers.view_revealed").formatted(Formatting.GREEN); + } else { + final var group = EventUtils.MOD.getCurrentViewGroup(); + message = (group != null ? literal(group.getName()) : translatable("eventutils.hideplayers.view_whitelist_only")) + .formatted(Formatting.GREEN); + } + client.player.sendMessage(translatable("eventutils.hideplayers.view_prefix").append(message), true); } }); } private boolean canNotPress(@NotNull KeyBinding keyBinding) { + //? if >=1.21.11 { + /*final String translationKey = keyBinding.getId(); + *///?} else { final String translationKey = keyBinding.getTranslationKey(); + //?} final Long lastPressTime = lastKeyPresses.get(translationKey); final long now = System.currentTimeMillis(); if (lastPressTime != null && now - lastPressTime < 500) return true; diff --git a/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java b/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java index 1c85e2a..5d6d387 100644 --- a/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java +++ b/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java @@ -1,6 +1,8 @@ package cc.aabss.eventutils.commands; import cc.aabss.eventutils.EventType; +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.config.PlayerGroup; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.IntegerArgumentType; @@ -96,6 +98,19 @@ public static void register(@NotNull CommandDispatcher groupMsg = ClientCommandManager + .literal("groupmsg") + .then(ClientCommandManager.argument("group", StringArgumentType.word()) + .suggests((context, builder) -> { + for (final PlayerGroup g : EventUtils.MOD.config.groups) builder.suggest(g.getName()); + return builder.buildFuture(); + }) + .then(ClientCommandManager.argument("message", StringArgumentType.greedyString()) + .executes(context -> GroupMsgCmd.groupMsg(context, + StringArgumentType.getString(context, "group"), + StringArgumentType.getString(context, "message"))))) + .build(); + // Build command tree dispatcher.getRoot().addChild(main); main.addChild(config); @@ -103,5 +118,6 @@ public static void register(@NotNull CommandDispatcher cont return; } + //? if >=1.21.11 { + /*final List namesFiltered = client.getNetworkHandler().getPlayerList().stream() + .map(entry -> entry.getProfile().name()) + .filter(name -> name.toLowerCase().contains(filter.toLowerCase())) + .filter(name -> !EventUtils.isNPC(name, true)) + .toList(); + *///?} else { final List namesFiltered = client.getNetworkHandler().getPlayerList().stream() .map(entry -> entry.getProfile().getName()) .filter(name -> name.toLowerCase().contains(filter.toLowerCase())) .filter(name -> !EventUtils.isNPC(name, true)) .toList(); + //?} if (namesFiltered.isEmpty()) { context.getSource().sendFeedback(Text.translatable("eventutils.command.countname.noplayers", EventUtils.ERROR_MESSAGE_PREFIX, Text.literal(filter).formatted(Formatting.DARK_RED))); @@ -44,11 +52,19 @@ public static void list(@NotNull CommandContext conte return; } + //? if >=1.21.11 { + /*final List namesFiltered = client.getNetworkHandler().getPlayerList().stream() + .map(entry -> entry.getProfile().name()) + .filter(name -> name.toLowerCase().contains(filter.toLowerCase())) + .filter(name -> !EventUtils.isNPC(name, true)) + .toList(); + *///?} else { final List namesFiltered = client.getNetworkHandler().getPlayerList().stream() .map(entry -> entry.getProfile().getName()) .filter(name -> name.toLowerCase().contains(filter.toLowerCase())) .filter(name -> !EventUtils.isNPC(name, true)) .toList(); + //?} if (namesFiltered.isEmpty()) { context.getSource().sendFeedback(Text.translatable("eventutils.command.countname.noplayers", EventUtils.ERROR_MESSAGE_PREFIX, Text.literal(filter).formatted(Formatting.DARK_RED))); diff --git a/src/main/java/cc/aabss/eventutils/commands/GroupMsgCmd.java b/src/main/java/cc/aabss/eventutils/commands/GroupMsgCmd.java new file mode 100644 index 0000000..44615bc --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/commands/GroupMsgCmd.java @@ -0,0 +1,38 @@ +package cc.aabss.eventutils.commands; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.config.PlayerGroup; + +import com.mojang.brigadier.context.CommandContext; + +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; + +import net.minecraft.text.Text; + +import org.jetbrains.annotations.NotNull; + +import static net.minecraft.text.Text.translatable; + + +public class GroupMsgCmd { + public static int groupMsg(@NotNull CommandContext context, @NotNull String groupNameArg, @NotNull String message) { + final var groups = EventUtils.MOD.config.groups; + final String search = groupNameArg.toLowerCase(); + PlayerGroup group = null; + for (final PlayerGroup g : groups) { + if (g.getName().toLowerCase().equals(search)) { + group = g; + break; + } + } + if (group == null) { + context.getSource().sendError(translatable("eventutils.command.groupmsg.no_group", EventUtils.ERROR_MESSAGE_PREFIX, groupNameArg)); + return 0; + } + final String toSend = "[" + group.getName() + "] " + message; + if (context.getSource().getPlayer() != null && context.getSource().getPlayer().networkHandler != null) { + context.getSource().getPlayer().networkHandler.sendChatMessage(toSend); + } + return 1; + } +} diff --git a/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java b/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java index ced5eac..71dd83e 100644 --- a/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java +++ b/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java @@ -6,6 +6,8 @@ import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.*; +import net.minecraft.client.MinecraftClient; + import net.minecraft.client.gui.screen.Screen; import net.minecraft.entity.EntityType; import net.minecraft.registry.Registries; @@ -135,6 +137,15 @@ public static Screen getConfigScreen(@Nullable Screen parent) { .controller(ConfigScreen::getBooleanBuilder).build()) .build()); + // Groups + builder.category(ConfigCategory.createBuilder().name(translatable("eventutils.config.groups.category")) + .option(ButtonOption.createBuilder() + .name(translatable("eventutils.config.groups.manage_title")) + .description(OptionDescription.of(translatable("eventutils.config.groups.manage_description"))) + .action((yaclScreen, option) -> MinecraftClient.getInstance().setScreen(new GroupManagerScreen(yaclScreen))) + .build()) + .build()); + // Alerts & notification sounds final OptionGroup.Builder alertsGroup = OptionGroup.createBuilder() .name(translatable("eventutils.config.alerts.toggles")); diff --git a/src/main/java/cc/aabss/eventutils/config/EditGroupScreen.java b/src/main/java/cc/aabss/eventutils/config/EditGroupScreen.java new file mode 100644 index 0000000..fb792c2 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/config/EditGroupScreen.java @@ -0,0 +1,105 @@ +package cc.aabss.eventutils.config; + +import cc.aabss.eventutils.EventUtils; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; + +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static net.minecraft.text.Text.translatable; + + +public class EditGroupScreen extends Screen { + private static final int PADDING = 20; + private static final int ROW = 24; + private static final int FIELD_HEIGHT = 20; + + @Nullable private final Screen parent; + private final int groupIndex; + private TextFieldWidget nameField; + private TextFieldWidget playersField; + private boolean showNametags = true; + + public EditGroupScreen(@Nullable Screen parent, int groupIndex) { + super(translatable("eventutils.config.groups.edit_title")); + this.parent = parent; + this.groupIndex = groupIndex; + } + + @Override + protected void init() { + final EventConfig config = EventUtils.MOD.config; + if (groupIndex < 0 || groupIndex >= config.groups.size()) { + if (client != null) client.setScreen(parent); + return; + } + final PlayerGroup group = config.groups.get(groupIndex); + showNametags = group.isShowNametags(); + + final int centerX = width / 2; + final int fieldWidth = Math.min(320, width - PADDING * 4); + int y = 40; + + nameField = new TextFieldWidget(textRenderer, centerX - fieldWidth / 2, y, fieldWidth, FIELD_HEIGHT, translatable("eventutils.config.groups.name_hint")); + nameField.setMaxLength(64); + nameField.setText(group.getName()); + nameField.setPlaceholder(translatable("eventutils.config.groups.name_hint")); + addDrawableChild(nameField); + + y += ROW + 8; + playersField = new TextFieldWidget(textRenderer, centerX - fieldWidth / 2, y, fieldWidth, FIELD_HEIGHT, translatable("eventutils.config.groups.players_hint")); + playersField.setMaxLength(1024); + playersField.setText(String.join(", ", group.getPlayers())); + playersField.setPlaceholder(translatable("eventutils.config.groups.players_hint")); + addDrawableChild(playersField); + + y += ROW + 16; + addDrawableChild(ButtonWidget.builder(showNametags ? translatable("eventutils.config.groups.nametags_on") : translatable("eventutils.config.groups.nametags_off"), button -> { + showNametags = !showNametags; + button.setMessage(showNametags ? translatable("eventutils.config.groups.nametags_on") : translatable("eventutils.config.groups.nametags_off")); + }).dimensions(centerX - 100, y, 200, 20).build()); + + y += ROW + 24; + addDrawableChild(ButtonWidget.builder(translatable("gui.done"), button -> saveAndClose()) + .dimensions(centerX - 60, y, 120, 20).build()); + } + + private void saveAndClose() { + final EventConfig config = EventUtils.MOD.config; + if (groupIndex < 0 || groupIndex >= config.groups.size()) { + if (client != null) client.setScreen(parent); + return; + } + final PlayerGroup group = config.groups.get(groupIndex); + group.setName(nameField.getText().trim().isEmpty() ? "New Group" : nameField.getText().trim()); + final String playersText = playersField.getText().trim(); + final List players = playersText.isEmpty() + ? List.of() + : Arrays.stream(playersText.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(String::toLowerCase) + .collect(Collectors.toList()); + group.setPlayers(players); + group.setShowNametags(showNametags); + config.setSave("groups", config.groups); + if (client != null) client.setScreen(parent); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(0, 0, width, height, 0xC0101010); + context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 12, 0xFFFFFF); + final int centerX = width / 2; + context.drawTextWithShadow(textRenderer, translatable("eventutils.config.groups.name_label"), centerX - 160, 44, 0xA0A0A0); + context.drawTextWithShadow(textRenderer, translatable("eventutils.config.groups.players_label"), centerX - 160, 44 + ROW + 8, 0xA0A0A0); + super.render(context, mouseX, mouseY, delta); + } +} diff --git a/src/main/java/cc/aabss/eventutils/config/EventConfig.java b/src/main/java/cc/aabss/eventutils/config/EventConfig.java index 13a2431..93858fb 100644 --- a/src/main/java/cc/aabss/eventutils/config/EventConfig.java +++ b/src/main/java/cc/aabss/eventutils/config/EventConfig.java @@ -33,6 +33,7 @@ public class EventConfig extends FileLoader { @NotNull public String defaultFamousIp; @NotNull public List> hiddenEntityTypes; @NotNull public List whitelistedPlayers; + @NotNull public List groups; public boolean useTestingApi; @NotNull public final List eventTypes; @NotNull public final Map notificationSounds; @@ -63,6 +64,7 @@ public EventConfig() { hideNPCs = get("hide_npcs", Defaults.HIDE_NPCS); hiddenEntityTypes = get("hidden_entity_types", Defaults.hiddenEntityTypes(), new TypeToken>>(){}.getType()); whitelistedPlayers = get("whitelisted_players", Defaults.whitelistedPlayers(), new TypeToken>(){}.getType()); + groups = get("groups", Defaults.groups(), new TypeToken>(){}.getType()); useTestingApi = get("use_testing_api", Defaults.USE_TESTING_API); eventTypes = get("notifications", Defaults.eventTypes(), new TypeToken>(){}.getType()); notificationSounds = get("notification_sounds", Defaults.notificationSounds(), new TypeToken>(){}.getType()); @@ -164,6 +166,10 @@ public static List whitelistedPlayers() { return new ArrayList<>(WHITELISTED_PLAYERS); } @NotNull + public static List groups() { + return new ArrayList<>(); + } + @NotNull public static List eventTypes() { return new ArrayList<>(EVENT_TYPES); } diff --git a/src/main/java/cc/aabss/eventutils/config/GroupManagerScreen.java b/src/main/java/cc/aabss/eventutils/config/GroupManagerScreen.java new file mode 100644 index 0000000..969093e --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/config/GroupManagerScreen.java @@ -0,0 +1,80 @@ +package cc.aabss.eventutils.config; + +import cc.aabss.eventutils.EventUtils; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.TextColor; +import net.minecraft.util.Formatting; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +import static net.minecraft.text.Text.literal; +import static net.minecraft.text.Text.translatable; + + +public class GroupManagerScreen extends Screen { + private static final int ROW_HEIGHT = 24; + private static final int PADDING = 20; + private static final int BUTTON_WIDTH = 120; + private static final int REMOVE_WIDTH = 50; + + @Nullable private final Screen parent; + + public GroupManagerScreen(@Nullable Screen parent) { + super(translatable("eventutils.config.groups.manage_title")); + this.parent = parent; + } + + @Override + protected void init() { + final EventConfig config = EventUtils.MOD.config; + final int listTop = 40; + + for (int i = 0; i < config.groups.size(); i++) { + final int index = i; + final PlayerGroup group = config.groups.get(i); + final int y = listTop + i * ROW_HEIGHT; + if (y >= height - 60) break; + + final ButtonWidget editBtn = ButtonWidget.builder( + literal(group.getName()).fillStyle(Style.EMPTY.withColor(TextColor.fromRgb(0xE0E0E0))), + button -> client.setScreen(new EditGroupScreen(this, index)) + ).dimensions(PADDING, y, width - PADDING * 2 - REMOVE_WIDTH - 4, 20).build(); + addDrawableChild(editBtn); + + addDrawableChild(ButtonWidget.builder(literal("X").formatted(Formatting.RED), button -> { + config.groups.remove(index); + config.setSave("groups", config.groups); + if (client != null) client.setScreen(new GroupManagerScreen(parent)); + }).dimensions(width - PADDING - REMOVE_WIDTH, y, REMOVE_WIDTH, 20).build()); + } + + final ButtonWidget addBtn = ButtonWidget.builder(translatable("eventutils.config.groups.add"), button -> { + config.groups.add(new PlayerGroup()); + config.setSave("groups", config.groups); + client.setScreen(new GroupManagerScreen(parent)); + }).dimensions(width / 2 - BUTTON_WIDTH - 4, height - 32, BUTTON_WIDTH, 20).build(); + addDrawableChild(addBtn); + + addDrawableChild(ButtonWidget.builder(translatable("gui.done"), button -> goBack()) + .dimensions(width / 2 + 4, height - 32, BUTTON_WIDTH, 20).build()); + } + + private void goBack() { + if (client != null) client.setScreen(parent); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(0, 0, width, height, 0xC0101010); + context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 12, 0xFFFFFF); + super.render(context, mouseX, mouseY, delta); + } + +} diff --git a/src/main/java/cc/aabss/eventutils/config/PlayerGroup.java b/src/main/java/cc/aabss/eventutils/config/PlayerGroup.java new file mode 100644 index 0000000..a725286 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/config/PlayerGroup.java @@ -0,0 +1,61 @@ +package cc.aabss.eventutils.config; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + + +/** + * A named group of players with optional nametag visibility. + * Used for cycling visibility (hide players keybind) and group chat. + */ +public class PlayerGroup { + @NotNull private String name; + @NotNull private List players; + private boolean showNametags; + + public PlayerGroup(@NotNull String name, @NotNull List players, boolean showNametags) { + this.name = name; + this.players = new ArrayList<>(players); + this.showNametags = showNametags; + } + + /** For Gson deserialization */ + public PlayerGroup() { + this.name = "New Group"; + this.players = new ArrayList<>(); + this.showNametags = true; + } + + @NotNull + public String getName() { + return name; + } + + public void setName(@NotNull String name) { + this.name = name; + } + + @NotNull + public List getPlayers() { + return players; + } + + public void setPlayers(@NotNull List players) { + this.players = new ArrayList<>(players); + } + + public boolean isShowNametags() { + return showNametags; + } + + public void setShowNametags(boolean showNametags) { + this.showNametags = showNametags; + } + + /** Returns true if the given (lowercased) player name is in this group. */ + public boolean containsPlayer(@NotNull String nameLower) { + return players.contains(nameLower); + } +} diff --git a/src/main/java/cc/aabss/eventutils/mixin/EntityMixin.java b/src/main/java/cc/aabss/eventutils/mixin/EntityMixin.java index b8a1a9b..7c4bf22 100644 --- a/src/main/java/cc/aabss/eventutils/mixin/EntityMixin.java +++ b/src/main/java/cc/aabss/eventutils/mixin/EntityMixin.java @@ -19,12 +19,16 @@ @Mixin(Entity.class) public abstract class EntityMixin { @Shadow public abstract EntityType getType(); + //? if >=1.21.11 { + /*@Shadow public abstract Vec3d getSyncedPos(); + *///?} else { @Shadow public abstract Vec3d getPos(); + //?} @Shadow public abstract Text getName(); @Inject(method = "spawnSprintingParticles", at = @At("HEAD"), cancellable = true) private void spawnSprintingParticles(CallbackInfo ci) { - if (!EventUtils.MOD.hidePlayers) return; + if (!EventUtils.MOD.isInHidePlayersMode()) return; final ClientPlayerEntity mainPlayer = MinecraftClient.getInstance().player; if (mainPlayer == null) return; final EntityType type = getType(); @@ -32,7 +36,7 @@ private void spawnSprintingParticles(CallbackInfo ci) { if (type == EntityType.PLAYER) { // Players final String name = getName().getString().toLowerCase(); - if (mainPlayer.getName().getString().toLowerCase().equals(name) || EventUtils.MOD.config.whitelistedPlayers.contains(name) || EventUtils.isNPC(name)) return; + if (mainPlayer.getName().getString().toLowerCase().equals(name) || EventUtils.MOD.isPlayerVisible(name)) return; } else { // Non-players (mob) if (!EventUtils.MOD.config.hiddenEntityTypes.contains(type)) return; @@ -45,6 +49,10 @@ private void spawnSprintingParticles(CallbackInfo ci) { } // Specific radius + //? if >=1.21.11 { + /*if (mainPlayer.getSyncedPos().distanceTo(getSyncedPos()) <= EventUtils.MOD.config.hidePlayersRadius) ci.cancel(); + *///?} else { if (mainPlayer.getPos().distanceTo(getPos()) <= EventUtils.MOD.config.hidePlayersRadius) ci.cancel(); + //?} } } diff --git a/src/main/java/cc/aabss/eventutils/mixin/EntityRenderDispatcherMixin.java b/src/main/java/cc/aabss/eventutils/mixin/EntityRenderDispatcherMixin.java index 4031cb5..63a064e 100644 --- a/src/main/java/cc/aabss/eventutils/mixin/EntityRenderDispatcherMixin.java +++ b/src/main/java/cc/aabss/eventutils/mixin/EntityRenderDispatcherMixin.java @@ -5,9 +5,18 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.render.VertexConsumerProvider; +//? if >=1.21.11 { +/*import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.entity.EntityRenderManager; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.client.render.state.CameraRenderState; +import net.minecraft.util.math.Vec3d; +*///?} else { import net.minecraft.client.render.entity.EntityRenderDispatcher; +//?} import net.minecraft.client.util.math.MatrixStack; import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; import net.minecraft.entity.player.PlayerEntity; import org.spongepowered.asm.mixin.Mixin; @@ -16,34 +25,57 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//? if >=1.21.11 { +/*@Mixin(EntityRenderManager.class) +*///?} else { @Mixin(EntityRenderDispatcher.class) +//?} public class EntityRenderDispatcherMixin { - @Inject(method = "render", at = @At("HEAD"), cancellable = true) - //? if <=1.21.1 { - /*private void render(Entity entity, double x, double y, double z, float yaw, float tickDelta, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light, CallbackInfo ci) { + //? if >=1.21.11 { + /*@Inject(method = "render", at = @At("HEAD"), cancellable = true) + private void render(EntityRenderState renderState, CameraRenderState cameraRenderState, double x, double y, double z, MatrixStack matrixStack, OrderedRenderCommandQueue orderedRenderCommandQueue, CallbackInfo ci) { + if (!EventUtils.MOD.isInHidePlayersMode()) return; + + if (renderState.entityType == EntityType.PLAYER) { + final ClientPlayerEntity mainPlayer = MinecraftClient.getInstance().player; + if (mainPlayer != null && renderState.displayName != null && mainPlayer.getName().getString().equalsIgnoreCase(renderState.displayName.getString())) return; + final String name = renderState.displayName != null ? renderState.displayName.getString().toLowerCase() : ""; + if (EventUtils.MOD.isPlayerVisible(name)) return; + } else { + if (!EventUtils.MOD.config.hiddenEntityTypes.contains(renderState.entityType)) return; + } + + if (EventUtils.MOD.config.hidePlayersRadius == 0) { + ci.cancel(); + return; + } + + final ClientPlayerEntity mainPlayer = MinecraftClient.getInstance().player; + if (mainPlayer != null) { + final Vec3d entityPos = new Vec3d(renderState.x, renderState.y, renderState.z); + if (mainPlayer.getSyncedPos().distanceTo(entityPos) <= EventUtils.MOD.config.hidePlayersRadius) ci.cancel(); + } + } *///?} else { + @Inject(method = "render", at = @At("HEAD"), cancellable = true) private void render(E entity, double x, double y, double z, float tickDelta, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light, CallbackInfo ci) { - //?} - if (!EventUtils.MOD.hidePlayers) return; + if (!EventUtils.MOD.isInHidePlayersMode()) return; if (entity instanceof PlayerEntity player) { - // Players if (player.isMainPlayer()) return; final String name = player.getName().getString().toLowerCase(); - if (EventUtils.MOD.config.whitelistedPlayers.contains(name) || EventUtils.isNPC(name)) return; + if (EventUtils.MOD.isPlayerVisible(name)) return; } else { - // Non-players (mob) if (!EventUtils.MOD.config.hiddenEntityTypes.contains(entity.getType())) return; } - // Any radius if (EventUtils.MOD.config.hidePlayersRadius == 0) { ci.cancel(); return; } - // Specific radius final ClientPlayerEntity mainPlayer = MinecraftClient.getInstance().player; if (mainPlayer != null && mainPlayer.getPos().distanceTo(entity.getPos()) <= EventUtils.MOD.config.hidePlayersRadius) ci.cancel(); } + //?} } diff --git a/src/main/java/cc/aabss/eventutils/mixin/PlayerEntityRendererMixin.java b/src/main/java/cc/aabss/eventutils/mixin/PlayerEntityRendererMixin.java index 42a1f6f..f73d027 100644 --- a/src/main/java/cc/aabss/eventutils/mixin/PlayerEntityRendererMixin.java +++ b/src/main/java/cc/aabss/eventutils/mixin/PlayerEntityRendererMixin.java @@ -6,14 +6,18 @@ import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.entity.PlayerEntityRenderer; +//? if >=1.21.11 { +/*import net.minecraft.client.render.command.OrderedRenderCommandQueue; +import net.minecraft.client.render.state.CameraRenderState; +*///?} +//? if >=1.21.3 { +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +//?} else { +/*import net.minecraft.client.network.AbstractClientPlayerEntity; +*///?} import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; import net.minecraft.util.math.Vec3d; -//? if <1.21.3 { -/*import net.minecraft.client.network.AbstractClientPlayerEntity; -*///?} else { -import net.minecraft.client.render.entity.state.PlayerEntityRenderState; -//?} import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -23,6 +27,29 @@ @Mixin(PlayerEntityRenderer.class) public class PlayerEntityRendererMixin { + //? if >=1.21.11 { + /*@Inject(at = @At("HEAD"), method = "renderLabelIfPresent", cancellable = true) + public void renderLabelIfPresent(PlayerEntityRenderState player, MatrixStack matrixStack, OrderedRenderCommandQueue orderedRenderCommandQueue, CameraRenderState cameraRenderState, CallbackInfo ci) { + if (!EventUtils.MOD.isInHidePlayersMode()) return; + final ClientPlayerEntity clientPlayer = MinecraftClient.getInstance().player; + if (clientPlayer == null) return; + final Text nameText = player.playerName; + if (nameText == null) return; + final String name = nameText.getString().toLowerCase(); + if (name.equals(clientPlayer.getName().getString().toLowerCase())) return; + if (!EventUtils.MOD.isPlayerVisible(name)) { + ci.cancel(); + return; + } + if (!EventUtils.MOD.shouldShowNametagFor(name)) { + ci.cancel(); + return; + } + if (EventUtils.MOD.config.hidePlayersRadius == 0) return; + final Vec3d playerPos = new Vec3d(player.x, player.y, player.z); + if (clientPlayer.getSyncedPos().distanceTo(playerPos) <= EventUtils.MOD.config.hidePlayersRadius) ci.cancel(); + } + *///?} else { @Inject(at = {@At("HEAD")}, method = "renderLabelIfPresent*", cancellable = true) //? if <=1.20.4 { /*public void renderLabelIfPresent(AbstractClientPlayerEntity player, Text text, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { @@ -31,7 +58,7 @@ public class PlayerEntityRendererMixin { *///?} else { public void renderLabelIfPresent(PlayerEntityRenderState player, Text text, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { //?} - if (!EventUtils.MOD.hidePlayers) return; + if (!EventUtils.MOD.isInHidePlayersMode()) return; final ClientPlayerEntity clientPlayer = MinecraftClient.getInstance().player; if (clientPlayer == null) return; @@ -51,15 +78,20 @@ public void renderLabelIfPresent(PlayerEntityRenderState player, Text text, Matr if (name.equals(clientPlayer.getName().getString().toLowerCase())) return; //?} - // Checks - if (EventUtils.MOD.config.whitelistedPlayers.contains(name) || EventUtils.isNPC(name)) return; - - // Any radius - if (EventUtils.MOD.config.hidePlayersRadius == 0) { + // Not visible in current view mode -> hide nametag + if (!EventUtils.MOD.isPlayerVisible(name)) { + ci.cancel(); + return; + } + // Visible: respect per-group nametag setting + if (!EventUtils.MOD.shouldShowNametagFor(name)) { ci.cancel(); return; } + // Any radius + if (EventUtils.MOD.config.hidePlayersRadius == 0) return; + // Get player position //? if <1.21.3 { /*final Vec3d playerPos = player.getPos(); @@ -70,4 +102,5 @@ public void renderLabelIfPresent(PlayerEntityRenderState player, Text text, Matr // Radius-specific if (clientPlayer.getPos().distanceTo(playerPos) <= EventUtils.MOD.config.hidePlayersRadius) ci.cancel(); } + //?} } diff --git a/src/main/java/cc/aabss/eventutils/mixin/PlayerListHudMixin.java b/src/main/java/cc/aabss/eventutils/mixin/PlayerListHudMixin.java new file mode 100644 index 0000000..78650cf --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/mixin/PlayerListHudMixin.java @@ -0,0 +1,69 @@ +package cc.aabss.eventutils.mixin; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.plustag.EventAlertsApi; +import cc.aabss.eventutils.plustag.PlusTag; +import cc.aabss.eventutils.plustag.PlusTagRenderer; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.PlayerListHud; +import net.minecraft.client.network.PlayerListEntry; + +import java.util.EnumSet; +import java.util.UUID; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + + +@Mixin(PlayerListHud.class) +public abstract class PlayerListHudMixin { + @Shadow @Final private MinecraftClient client; + + /** Draw + icon for every tab list row (local player = selected tag, others = best unlocked from Event Alerts). */ + @Inject(method = "renderLatencyIcon", at = @At("TAIL")) + private void eventutils$drawPlusTagNextToName(DrawContext context, int width, int x, int y, PlayerListEntry entry, CallbackInfo ci) { + if (client.player == null) return; + + //? if >=1.21.11 { + /*String name = entry.getProfile().name(); + final boolean isLocal = entry.getProfile().id().equals(client.player.getUuid()); + UUID uuid = entry.getProfile().id(); + *///?} else { + String name = entry.getProfile().getName(); + final boolean isLocal = entry.getProfile().getId().equals(client.player.getUuid()); + UUID uuid = entry.getProfile().getId(); + //?} + + if (isLocal && EventAlertsApi.getCached(uuid) == null) { + // If JOIN ran before player was ready, trigger it when tab list is drawn + EventUtils.LOGGER.info("[EventUtils] Tab list: local player not cached, scheduling Event Alerts fetch uuid={}", uuid); + EventAlertsApi.scheduleFetchIfNeeded(uuid); + } + + EnumSet cached = EventAlertsApi.getCached(uuid); + if (cached == null) { + EventUtils.LOGGER.debug("[TabList] entry={} uuid={} cache MISS, scheduling fetch", name, uuid); + EventAlertsApi.scheduleFetchIfNeeded(uuid); + return; + } + + final PlusTag tag = PlusTag.pickBestForDisplay(cached); + EventUtils.LOGGER.debug("[TabList] entry={} uuid={} cache HIT unlocked={} pickBest={}", name, uuid, cached, tag); + + if (tag == null || tag == PlusTag.WHITE) { + EventUtils.LOGGER.debug("[TabList] entry={} skip draw: tag={} (null or WHITE)", name, tag); + return; + } + + int iconSize = 8; + int iconX = x - 8; + PlusTagRenderer.draw(context, tag, iconX, y, iconSize); + EventUtils.LOGGER.debug("[TabList] entry={} DRAW tag={} at ({}, {}) size={}", name, tag, iconX, y, iconSize); + } +} diff --git a/src/main/java/cc/aabss/eventutils/plustag/EventAlertsApi.java b/src/main/java/cc/aabss/eventutils/plustag/EventAlertsApi.java new file mode 100644 index 0000000..1238895 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/plustag/EventAlertsApi.java @@ -0,0 +1,214 @@ +package cc.aabss.eventutils.plustag; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.Versions; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.EnumSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Fetches player data from Event Alerts API to determine unlocked plus tags. + * API: GET https://eventalerts.gg/api/v1/players/minecraft/uuid/{uuid} + */ +public final class EventAlertsApi { + private static final String API_BASE = "https://eventalerts.gg"; + private static final String API_PATH = "/api/v1/players/minecraft/uuid/"; + + private static final HttpClient HTTP = HttpClient.newBuilder().build(); + /** Cache: Minecraft UUID -> unlocked tags. Cleared on world unload. */ + private static final ConcurrentHashMap> CACHE = new ConcurrentHashMap<>(); + /** UUIDs we've already scheduled a fetch for (avoid duplicate requests until cache clear). */ + private static final Set FETCH_SCHEDULED = ConcurrentHashMap.newKeySet(); + + private EventAlertsApi() {} + + @NotNull + public static String getApiBase() { + return EventUtils.MOD.config.useTestingApi ? "http://localhost:9090" : API_BASE; + } + + @Nullable + private static UUID parseMinecraftUuid(@NotNull String uuidString) { + try { + return UUID.fromString(uuidString); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + /** Fetch unlocked plus tags for a Minecraft UUID. Returns empty set on failure. */ + @NotNull + public static EnumSet fetchUnlockedTags(@NotNull UUID minecraftUuid) { + EnumSet cached = CACHE.get(minecraftUuid); + if (cached != null) { + EventUtils.LOGGER.debug("[EventAlerts] fetchUnlockedTags: cache HIT uuid={} tags={}", minecraftUuid, cached); + return cached; + } + + EventUtils.LOGGER.info("[EventAlerts] Fetching tags for uuid={}", minecraftUuid); + try { + String url = getApiBase() + API_PATH + minecraftUuid; + EventUtils.LOGGER.debug("[EventAlerts] GET {}", url); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("User-Agent", "EventUtils/" + Versions.EU_VERSION + " (Minecraft/" + Versions.MC_VERSION + ")") + .GET() + .build(); + HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + EventUtils.LOGGER.debug("[EventAlerts] response status={} bodyLength={}", response.statusCode(), response.body().length()); + if (response.statusCode() != 200) { + EventUtils.LOGGER.warn("[EventAlerts] API returned status={} body={}", response.statusCode(), response.body()); + return EnumSet.noneOf(PlusTag.class); + } + + JsonObject root = JsonParser.parseString(response.body()).getAsJsonObject(); + // API may wrap data in "player": { ... } (player can be null if not found) + JsonObject data; + if (root.has("player") && !root.get("player").isJsonNull() && root.get("player").isJsonObject()) { + data = root.getAsJsonObject("player"); + EventUtils.LOGGER.debug("[EventAlerts] using wrapped player object"); + } else if (!root.has("player")) { + data = root; + EventUtils.LOGGER.debug("[EventAlerts] using root as data (no player wrapper)"); + } else { + EventUtils.LOGGER.info("[EventAlerts] API response: player=null (not linked or not found)"); + return EnumSet.noneOf(PlusTag.class); // player: null + } + EnumSet unlocked = parseUnlockedTags(data); + EventUtils.LOGGER.debug("[EventAlerts] parsed unlocked tags={} for uuid={}", unlocked, minecraftUuid); + CACHE.put(minecraftUuid, unlocked); + EventUtils.LOGGER.info("[EventAlerts] Fetched uuid={} tags={}", minecraftUuid, unlocked); + return unlocked; + } catch (Exception e) { + EventUtils.LOGGER.warn("[EventAlerts] Fetch failed uuid={} error={}", minecraftUuid, e.getMessage(), e); + return EnumSet.noneOf(PlusTag.class); + } + } + + /** Parse API response into unlocked tags. Expects player object: id, discord, minecraft, subscription (1=premium/Bee), anniversaries, roles (optional). */ + @NotNull + private static EnumSet parseUnlockedTags(@NotNull JsonObject root) { + EnumSet out = EnumSet.noneOf(PlusTag.class); + + // Linked: has discord and minecraft + if (root.has("discord") && root.has("minecraft")) { + JsonObject mc = root.getAsJsonObject("minecraft"); + if (mc != null && mc.has("uuid")) { + out.add(PlusTag.NONE); // "Linked" icon + EventUtils.LOGGER.debug("[EventAlerts] parse: +NONE (linked, discord+minecraft.uuid)"); + } + } + + // Bee / Premium: subscription tier, premium flag, or subscription object + if (root.has("subscription")) { + JsonElement sub = root.get("subscription"); + if (sub != null && !sub.isJsonNull()) { + if (sub.isJsonPrimitive()) { + if (sub.getAsJsonPrimitive().isNumber()) { + int v = sub.getAsInt(); + if (v >= 1) { + out.add(PlusTag.ORANGE); + EventUtils.LOGGER.debug("[EventAlerts] parse: +ORANGE (subscription number={})", v); + } + } else if (sub.getAsJsonPrimitive().isBoolean() && sub.getAsBoolean()) { + out.add(PlusTag.ORANGE); + EventUtils.LOGGER.debug("[EventAlerts] parse: +ORANGE (subscription boolean true)"); + } + } else if (sub.isJsonObject()) { + JsonObject obj = sub.getAsJsonObject(); + if (obj.has("tier") && obj.get("tier").getAsInt() >= 1) { + out.add(PlusTag.ORANGE); + EventUtils.LOGGER.debug("[EventAlerts] parse: +ORANGE (subscription.tier)"); + } else if (obj.has("active") && obj.get("active").getAsBoolean()) { + out.add(PlusTag.ORANGE); + EventUtils.LOGGER.debug("[EventAlerts] parse: +ORANGE (subscription.active)"); + } + } + } + } + if (root.has("premium") && root.get("premium").getAsBoolean()) { + out.add(PlusTag.ORANGE); + EventUtils.LOGGER.debug("[EventAlerts] parse: +ORANGE (premium)"); + } + if (root.has("isPremium") && root.get("isPremium").getAsBoolean()) { + out.add(PlusTag.ORANGE); + EventUtils.LOGGER.debug("[EventAlerts] parse: +ORANGE (isPremium)"); + } + + // Booster: often a role or field. Check for "booster" boolean or roles array + if (root.has("booster") && root.get("booster").getAsBoolean()) { + out.add(PlusTag.PINK); + EventUtils.LOGGER.debug("[EventAlerts] parse: +PINK (booster)"); + } + if (root.has("roles")) { + JsonArray roles = root.getAsJsonArray("roles"); + for (JsonElement e : roles) { + String r = e.getAsString().toUpperCase(); + if (r.contains("BOOSTER")) { out.add(PlusTag.PINK); EventUtils.LOGGER.debug("[EventAlerts] parse: +PINK (roles contains BOOSTER)"); } + if (r.contains("ADMIN")) { out.add(PlusTag.RED); EventUtils.LOGGER.debug("[EventAlerts] parse: +RED (roles contains ADMIN)"); } + if (r.contains("DEV") || r.contains("CONTRIBUTOR")) { out.add(PlusTag.BLUE); EventUtils.LOGGER.debug("[EventAlerts] parse: +BLUE (roles DEV/CONTRIBUTOR)"); } + } + } + + // Some APIs use role names instead of IDs + if (root.has("rolesNamed")) { + JsonArray roles = root.getAsJsonArray("rolesNamed"); + for (JsonElement e : roles) { + String r = e.getAsString().toUpperCase(); + if ("BOOSTER".equals(r)) { out.add(PlusTag.PINK); EventUtils.LOGGER.debug("[EventAlerts] parse: +PINK (rolesNamed BOOSTER)"); } + if ("ADMIN".equals(r)) { out.add(PlusTag.RED); EventUtils.LOGGER.debug("[EventAlerts] parse: +RED (rolesNamed ADMIN)"); } + if ("DEVELOPER".equals(r) || "CONTRIBUTOR".equals(r)) { out.add(PlusTag.BLUE); EventUtils.LOGGER.debug("[EventAlerts] parse: +BLUE (rolesNamed)"); } + } + } + + return out; + } + + /** Clear cache (e.g. on disconnect). */ + public static void clearCache() { + int size = CACHE.size(); + CACHE.clear(); + FETCH_SCHEDULED.clear(); + EventUtils.LOGGER.info("[EventAlerts] Cache cleared (was {} entries)", size); + } + + /** Schedule a fetch for this UUID if not cached and not already scheduled. Call from main thread. */ + public static void scheduleFetchIfNeeded(@NotNull UUID minecraftUuid) { + if (CACHE.containsKey(minecraftUuid)) return; + if (!FETCH_SCHEDULED.add(minecraftUuid)) return; // already scheduled + EventUtils.LOGGER.info("[EventAlerts] Scheduling fetch for uuid={}", minecraftUuid); + EventUtils.MOD.scheduler.execute(() -> EventAlertsApi.fetchUnlockedTags(minecraftUuid)); + } + + public static void scheduleFetchIfNeeded(@NotNull String minecraftUuid) { + UUID uuid = parseMinecraftUuid(minecraftUuid); + if (uuid != null) scheduleFetchIfNeeded(uuid); + } + + /** Get cached unlocked tags for UUID, or null if not cached. (No per-call debug log to avoid spam from tab list.) */ + @Nullable + public static EnumSet getCached(@NotNull UUID minecraftUuid) { + return CACHE.get(minecraftUuid); + } + + @Nullable + public static EnumSet getCached(@NotNull String minecraftUuid) { + UUID uuid = parseMinecraftUuid(minecraftUuid); + return uuid != null ? CACHE.get(uuid) : null; + } +} diff --git a/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java b/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java new file mode 100644 index 0000000..2289c9f --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java @@ -0,0 +1,82 @@ +package cc.aabss.eventutils.plustag; + +import cc.aabss.eventutils.EventUtils; + +import net.minecraft.util.Identifier; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +/** + * Plus (+) icon tag displayed next to names. Unlocked by linking Discord and roles. + * Each tag uses its own texture file (no in-game slicing). + */ +public enum PlusTag { + /** Linked (gray) - from linking Discord */ + NONE("none", "linked", "eventutils.plustag.unlock.linked"), + /** Unused (white +) */ + WHITE("white", "white", null), + /** Admin */ + RED("red", "admin", "eventutils.plustag.unlock.admin"), + /** Developer / Contributor */ + BLUE("blue", "contrib", "eventutils.plustag.unlock.contributor"), + /** Bee subscription */ + ORANGE("orange", "bee", "eventutils.plustag.unlock.bee"), + /** Booster */ + PINK("pink", "booster", "eventutils.plustag.unlock.booster"); + + private final String key; + @NotNull private final Identifier textureId; + @NotNull private final String unlockKey; + + PlusTag(@NotNull String key, @NotNull String textureName, String unlockKey) { + this.key = key; + this.textureId = Identifier.of("eventutils", "textures/gui/" + textureName + ".png"); + this.unlockKey = unlockKey != null ? unlockKey : "eventutils.plustag.unlock.none"; + } + + @NotNull + public String getKey() { + return key; + } + + @NotNull + public Identifier getTextureId() { + return textureId; + } + + @NotNull + public String getUnlockKey() { + return unlockKey; + } + + /** Priority for "best" tag when we don't know the player's choice (higher = show first). */ + private int displayPriority() { + return switch (this) { + case RED -> 5; + case BLUE -> 4; + case ORANGE -> 3; + case PINK -> 2; + case NONE -> 1; + case WHITE -> 0; + }; + } + + /** Pick the best tag to show for another player from their unlocked set (admin > contrib > bee > booster > linked). */ + @Nullable + public static PlusTag pickBestForDisplay(@Nullable Set unlocked) { + if (unlocked == null || unlocked.isEmpty()) { + EventUtils.LOGGER.debug("[PlusTag] pickBestForDisplay: unlocked={} -> null", unlocked); + return null; + } + PlusTag best = null; + for (PlusTag tag : unlocked) { + if (tag == WHITE) continue; + if (best == null || tag.displayPriority() > best.displayPriority()) best = tag; + } + EventUtils.LOGGER.debug("[PlusTag] pickBestForDisplay: unlocked={} -> best={}", unlocked, best); + return best; + } +} diff --git a/src/main/java/cc/aabss/eventutils/plustag/PlusTagRenderer.java b/src/main/java/cc/aabss/eventutils/plustag/PlusTagRenderer.java new file mode 100644 index 0000000..e6d5386 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/plustag/PlusTagRenderer.java @@ -0,0 +1,33 @@ +package cc.aabss.eventutils.plustag; + +import net.minecraft.client.gui.DrawContext; +//? if >=1.21.6 { +/*import net.minecraft.client.gl.RenderPipelines; +*///?} else if >=1.21.4 { +import net.minecraft.client.render.RenderLayer; +//?} + +import org.jetbrains.annotations.NotNull; + +/** + * Draws the plus tag icon (full texture, no slicing). + * Expects 64x64 textures in assets/eventutils/textures/gui/ (drawn at the given size for a sharp look): + * linked.png, bee.png, booster.png, contrib.png, admin.png, white.png. + */ +public final class PlusTagRenderer { + private static final int TEX_SIZE = 64; + + private PlusTagRenderer() {} + + /** Draw the icon at (x, y) with the given size (e.g. 8 for tab list). Samples full 64x64 texture, scaled to size. */ + public static void draw(@NotNull DrawContext context, @NotNull PlusTag tag, int x, int y, int size) { + if (tag == PlusTag.WHITE) return; // unused, skip + //? if >=1.21.6 { + /*context.drawTexture(RenderPipelines.GUI_TEXTURED, tag.getTextureId(), x, y, 0f, 0f, size, size, TEX_SIZE, TEX_SIZE, TEX_SIZE, TEX_SIZE); + *///?} else if >=1.21.4 { + context.drawTexture(RenderLayer::getGuiTextured, tag.getTextureId(), x, y, 0f, 0f, size, size, TEX_SIZE, TEX_SIZE, TEX_SIZE, TEX_SIZE); + //?} else { + /*context.drawTexture(tag.getTextureId(), x, y, 0f, 0f, size, size, TEX_SIZE, TEX_SIZE); + *///?} + } +} diff --git a/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java b/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java index 081bdf5..e2fedbb 100644 --- a/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java +++ b/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java @@ -37,11 +37,13 @@ public static void connect(@NotNull String ip) { final ServerAddress address = ServerAddress.parse(ip); client.execute(() -> { try { - //? if >=1.21.6 { - /*client.disconnect(new MessageScreen(translatable("multiplayer.disconnect.generic")), false); - *///?} else { - client.disconnect(); - //?} + //? if >=1.21.11 { + /*client.disconnect(new MessageScreen(translatable("multiplayer.disconnect.generic")), false, false); + *///?} else if >=1.21 { + client.disconnect(new MessageScreen(translatable("multiplayer.disconnect.generic")), false); + //?} else { + /*client.disconnect(new MessageScreen(translatable("multiplayer.disconnect.generic"))); + *///?} //? if <=1.20.4 { /*ConnectScreen.connect(screen, client, address, new ServerInfo("EventUtils Event Server", ip, ServerInfo.ServerType.OTHER), true); diff --git a/src/main/resources/assets/eventutils/lang/de_de.json b/src/main/resources/assets/eventutils/lang/de_de.json new file mode 100644 index 0000000..7f11e2d --- /dev/null +++ b/src/main/resources/assets/eventutils/lang/de_de.json @@ -0,0 +1,143 @@ +{ + "key.category.eventutils": "EventUtils", + "key.eventutils.screen": "Bildschirm öffnen", + "subtitles.eventutils.alert": "Event Benachrichtigung", + + "eventutils.skeppy.display": "Skeppy Events", + "eventutils.potential_famous.display": "Mögliche Skeppy Events", + "eventutils.sighting.display": "Skeppy Sichtungen", + "eventutils.famous.display": "Berühmte Events", + "eventutils.partner.display": "Partner Events", + "eventutils.community.display": "Community Events", + "eventutils.money.display": "Geld Events", + "eventutils.fun.display": "Spaß Events", + "eventutils.housing.display": "Housing Events", + "eventutils.civilization.display": "Zivilisation Events", + + "eventutils.skeppy.new": "Neues Skeppy Event!", + "eventutils.potential_famous.new": "Neues Potentielles Skeppy Event!", + "eventutils.sighting.new": "Neue Skeppy Sichtung!", + "eventutils.famous.new": "Neues Berühmtes Event!", + "eventutils.partner.new": "Neues Partner Event!", + "eventutils.community.new": "Neues Community Event!", + "eventutils.money.new": "Neues Geld Event!", + "eventutils.fun.new": "Neues Spaß Event!", + "eventutils.housing.new": "Neues Housing Event!", + "eventutils.civilization.new": "Neues Zivilisation Event!", + + "eventutils.event.teleport": "Schreibe '{command}', um zu teleportieren!", + + "key.eventutils.hideplayers": "Verstecke Spieler", + "key.eventutils.eventinfo": "Event Details", + "eventutils.hideplayers.enabled": "Aktiviere Verstecke Spieler!", + "eventutils.hideplayers.disabled": "Deaktiviere Verstecke Spieler!", + "eventutils.hideplayers.view_prefix": "Ansicht: ", + "eventutils.hideplayers.view_revealed": "Alle Spieler sichtbar", + "eventutils.hideplayers.view_whitelist_only": "Nur Whitelist", + + "eventutils.updatechecker.new": "Neues Update verfügbar!", + "eventutils.updatechecker.hover": "Klicke, um den Download zu öffnen", + "eventutils.updatechecker.config": "Du kannst diese Nachricht in der Konfiguration deaktivieren", + + "eventutils.command.no_event_specified": "Kein Event Typ angegeben!", + "eventutils.command.no_event_found": "Kein Event/IP gefunden für ", + "eventutils.command.config": "Öffne EventUtils' Konfigurationsbildschirm", + "eventutils.command.teleport": "Teleportiere zum letzten geposteten Event dieses Typs", + + "eventutils.confirm_exit.title": "Spiel beenden bestätigen", + "eventutils.confirm_exit.message": "Bist du sicher, dass du das Spiel beenden möchtest?", + + "eventutils.confirm_disconnect.title": "Verbindung trennen bestätigen", + "eventutils.confirm_disconnect.message": "Bist du sicher, dass du die Verbindung trennen möchtest?", + + "eventutils.no_recent_event.message": "Kein Event ist vor kurzem passiert!", + + "eventutils.config.title": "EventUtils Mod Konfiguration", + "eventutils.config.general": "Generell", + "eventutils.config.alerts": "Alarme", + "eventutils.config.alerts.toggles": "Umschalter", + "eventutils.config.alerts.sounds": "Sounds", + "eventutils.config.event_description": "Ob du für {event} gepingt werden sollst", + "eventutils.config.sound_description": "Der Sound, der abgespielt werden soll, wenn {event} gepingt wird", + "eventutils.config.teleport.title": "Automatischer Teleport", + "eventutils.config.teleport.description": "Teleportiere automatisch zum Server, wenn ein Event startet", + "eventutils.config.queue.title": "Einfache Warteschlangennachricht", + "eventutils.config.queue.description": "Vereinfacht die Warteschlangennachricht für InvadedLands", + "eventutils.config.discord.title": "Discord RPC", + "eventutils.config.discord.description": "Ob die Discord Rich Presence angezeigt werden soll", + "eventutils.config.update.title": "Mod Update Checker", + "eventutils.config.update.description": "Ob der Mod Update Checker aktiviert werden soll", + "eventutils.config.window.title": "Spielfenster schließen bestätigen", + "eventutils.config.window.description": "Ob eine Bestätigung angezeigt werden soll, um zu bestätigen, dass du das Spielfenster schließen möchtest", + "eventutils.config.disconnect.title": "Verbindung trennen bestätigen", + "eventutils.config.disconnect.description": "Ob eine Bestätigung angezeigt werden soll, um zu bestätigen, dass du die Verbindung trennen möchtest", + "eventutils.config.famous.title": "Standard IP für berühmte Events", + "eventutils.config.famous.description": "Die Standard IP für berühmte Events, wenn keine IP eingegeben wird", + "eventutils.config.entity.title": "Verborgene Entity Typen", + "eventutils.config.entity.description": "Die Typen von Entitäten, die verborgen werden sollen", + "eventutils.config.players.title": "Erlaubte Spieler", + "eventutils.config.players.description": "Die Namen der Spieler, die du sehen kannst, wenn Spieler verborgen sind", + "eventutils.config.npchide.title": "Verstecke NPCs", + "eventutils.config.npchide.description": "Ob der Mod NPCs verstecken wird", + "eventutils.config.radius.title": "Verstecke Spieler in Radius", + "eventutils.config.radius.description": "Versteckt alle Spieler in dem angegebenen Radius, setzt auf 0, um die Entfernung zu ignorieren", + "eventutils.config.use_testing_api.title": "Testing API benutzen", + "eventutils.config.use_testing_api.description": "(FORTGESCHRITTEN) Ob die Testing API für den Mod benutzt werden soll", + "eventutils.config.groups.category": "Gruppen", + "eventutils.config.groups.manage_title": "Gruppen verwalten", + "eventutils.config.groups.manage_description": "Gruppen für Sichtbarkeit und Gruppenchat anlegen und bearbeiten. Nutze die Taste „Verstecke Spieler“, um zu wechseln: Gruppe 1 → Gruppe 2 → … → Alle sichtbar.", + "eventutils.config.groups.add": "Gruppe hinzufügen", + "eventutils.config.groups.edit_title": "Gruppe bearbeiten", + "eventutils.config.groups.name_label": "Name:", + "eventutils.config.groups.name_hint": "Gruppenname", + "eventutils.config.groups.players_label": "Spieler:", + "eventutils.config.groups.players_hint": "spieler1, spieler2, ...", + "eventutils.config.groups.nametags_on": "Namensschilder: An", + "eventutils.config.groups.nametags_off": "Namensschilder: Aus", + + "eventutils.sound.alarm": "Alarm", + "eventutils.sound.alert": "Benachrichtigung", + "eventutils.sound.calm": "Ruhe", + "eventutils.sound.cat": "Katze", + "eventutils.sound.chime": "Glocke", + "eventutils.sound.goofy": "Lächerlich", + "eventutils.sound.pluck": "Pflücken", + "eventutils.sound.reverb": "Echoverb", + "eventutils.sound.shakey": "Schütteln", + "eventutils.sound.time_of_war": "Zeit des Kriegs", + + "eventutils.screen.settings": "Einstellungen", + + "eventutils.command.priority.self": "%s§e Du hast Pickup Priorität %s §ebasiert auf den Leuten um dich herum.", + "eventutils.command.priority.player": "%s§e %s§e hat Pickup Priorität %s §ebasiert auf den Leuten um dich herum.", + "eventutils.command.priority.noplayer": "%s§c Spieler nicht gefunden!", + + "eventutils.command.prioritytop.page": "%s§e Seite %s §evon %s\n\n", + "eventutils.command.prioritytop.emptypage": "%s§c Keine Spieler gefunden!", + "eventutils.command.prioritytop.notapage": "%s§c Diese Seite wurde nicht gefunden! (Höchste Seite ist Seite %s§c)", + "eventutils.command.prioritytop.nextpage": "§6[§7>%s§6 Nächste Seite] ", + "eventutils.command.prioritytop.lastpage": "§6[§7%s<§6 Letzte Seite] ", + "eventutils.command.prioritytop.pagebutton": "%s§e %s%s§7(Klicken)", + + "eventutils.command.countname.noplayers": "%s§c Keine Spieler gefunden mit '%s§c' in ihrem Namen!", + "eventutils.command.countname.count": "%s§e Gefunden %s §eSpieler%s §ewith '%s§e' in ihrem Namen.", + "eventutils.command.countname.list": "%s§e Gefunden %s §eSpieler%s §emit '%s§e' im Namen§e: %s", + "eventutils.command.groupmsg.no_group": "%s§c Keine Gruppe mit dem Namen '%s§c' gefunden.", + + "eventutils.plustag.title": "Plus-Tag", + "eventutils.plustag.subtitle": "Wähle, welches + neben deinem Namen angezeigt wird", + "eventutils.plustag.none": "[Verknüpft]", + "eventutils.plustag.white": "[Weiß]", + "eventutils.plustag.red": "[Rot]", + "eventutils.plustag.blue": "[Blau]", + "eventutils.plustag.orange": "[Orange]", + "eventutils.plustag.pink": "[Pink]", + "eventutils.plustag.unlock.linked": "Freigeschaltet durch Verknüpfung deines Discord-Kontos (Event Alerts)", + "eventutils.plustag.unlock.none": "—", + "eventutils.plustag.unlock.admin": "Freigeschaltet als Admin.", + "eventutils.plustag.unlock.contributor": "Freigeschaltet als Entwickler/Mitarbeiter.", + "eventutils.plustag.unlock.bee": "Freigeschaltet mit Bee (Abonnement).", + "eventutils.plustag.unlock.booster": "Freigeschaltet durch Boosten des Event Alerts Discord-Servers.", + + "eventutils.word.plural": "" +} \ No newline at end of file diff --git a/src/main/resources/assets/eventutils/lang/en_us.json b/src/main/resources/assets/eventutils/lang/en_us.json index 89abe08..946a3c9 100644 --- a/src/main/resources/assets/eventutils/lang/en_us.json +++ b/src/main/resources/assets/eventutils/lang/en_us.json @@ -31,6 +31,9 @@ "key.eventutils.eventinfo": "Event Info", "eventutils.hideplayers.enabled": "Enabled hide players!", "eventutils.hideplayers.disabled": "Disabled hide players!", + "eventutils.hideplayers.view_prefix": "View: ", + "eventutils.hideplayers.view_revealed": "Players Revealed", + "eventutils.hideplayers.view_whitelist_only": "Whitelist Only", "eventutils.updatechecker.new": "There is a new update available!", "eventutils.updatechecker.hover": "Click to open download", @@ -80,6 +83,17 @@ "eventutils.config.radius.description": "Hides all the players in the specified radius, set to 0 to ignore distance", "eventutils.config.use_testing_api.title": "Use Testing API", "eventutils.config.use_testing_api.description": "(ADVANCED) Whether to use the testing API for the mod", + "eventutils.config.groups.category": "Groups", + "eventutils.config.groups.manage_title": "Manage Groups", + "eventutils.config.groups.manage_description": "Create and edit groups for player visibility and group chat. Use the Hide Players key to cycle: Group 1 → Group 2 → ... → Players Revealed.", + "eventutils.config.groups.add": "Add Group", + "eventutils.config.groups.edit_title": "Edit Group", + "eventutils.config.groups.name_label": "Name:", + "eventutils.config.groups.name_hint": "Group name", + "eventutils.config.groups.players_label": "Players:", + "eventutils.config.groups.players_hint": "player1, player2, ...", + "eventutils.config.groups.nametags_on": "Nametags: On", + "eventutils.config.groups.nametags_off": "Nametags: Off", "eventutils.sound.alarm": "Alarm", "eventutils.sound.alert": "Alert", @@ -108,6 +122,22 @@ "eventutils.command.countname.noplayers": "%s§c No players found with '%s§c' in their name!", "eventutils.command.countname.count": "%s§e Found %s §eplayer%s §ewith '%s§e' in their name.", "eventutils.command.countname.list": "%s§e Found %s §eplayer%s §ewith '%s§e' in their name§e: %s", + "eventutils.command.groupmsg.no_group": "%s§c No group named '%s§c' found.", + + "eventutils.plustag.title": "Plus Tag", + "eventutils.plustag.subtitle": "Choose which + to show next to your name", + "eventutils.plustag.none": "[Linked]", + "eventutils.plustag.white": "[White]", + "eventutils.plustag.red": "[Red]", + "eventutils.plustag.blue": "[Blue]", + "eventutils.plustag.orange": "[Orange]", + "eventutils.plustag.pink": "[Pink]", + "eventutils.plustag.unlock.linked": "Unlocked by linking your Discord account (Event Alerts)", + "eventutils.plustag.unlock.none": "—", + "eventutils.plustag.unlock.admin": "Unlocked by being an Admin.", + "eventutils.plustag.unlock.contributor": "Unlocked by being a developer/contributor.", + "eventutils.plustag.unlock.bee": "Unlocked with Bee (subscription).", + "eventutils.plustag.unlock.booster": "Unlocked by boosting the Event Alerts Discord server.", "eventutils.word.plural": "s" } \ No newline at end of file diff --git a/src/main/resources/assets/eventutils/textures/gui/README.txt b/src/main/resources/assets/eventutils/textures/gui/README.txt new file mode 100644 index 0000000..6e9bffd --- /dev/null +++ b/src/main/resources/assets/eventutils/textures/gui/README.txt @@ -0,0 +1,11 @@ +Plus tag icons (16x16 PNG each): + linked.png - gray/silver + (Discord linked) + bee.png - orange + (Bee subscription) + booster.png - pink + (Discord booster) + contrib.png - blue + (contributor/developer) + admin.png - red + (admin) + white.png - white + (unused) + +You can export these from your sheet.png (3 columns x 2 rows): + Row 0: bee, white, linked + Row 1: booster, contrib, admin diff --git a/src/main/resources/assets/eventutils/textures/gui/admin.png b/src/main/resources/assets/eventutils/textures/gui/admin.png new file mode 100644 index 0000000..50f2b1b Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/admin.png differ diff --git a/src/main/resources/assets/eventutils/textures/gui/bee.png b/src/main/resources/assets/eventutils/textures/gui/bee.png new file mode 100644 index 0000000..86282c8 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/bee.png differ diff --git a/src/main/resources/assets/eventutils/textures/gui/booster.png b/src/main/resources/assets/eventutils/textures/gui/booster.png new file mode 100644 index 0000000..b5eab0c Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/booster.png differ diff --git a/src/main/resources/assets/eventutils/textures/gui/contrib.png b/src/main/resources/assets/eventutils/textures/gui/contrib.png new file mode 100644 index 0000000..da72335 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/contrib.png differ diff --git a/src/main/resources/assets/eventutils/textures/gui/linked.png b/src/main/resources/assets/eventutils/textures/gui/linked.png new file mode 100644 index 0000000..3ed219d Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/linked.png differ diff --git a/src/main/resources/assets/eventutils/textures/gui/plus_sheet.png b/src/main/resources/assets/eventutils/textures/gui/plus_sheet.png new file mode 100644 index 0000000..3848d16 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/plus_sheet.png differ diff --git a/src/main/resources/assets/eventutils/textures/gui/white.png b/src/main/resources/assets/eventutils/textures/gui/white.png new file mode 100644 index 0000000..c631f07 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/gui/white.png differ diff --git a/src/main/resources/eventutils-1.21.11.mixin.json b/src/main/resources/eventutils-1.21.11.mixin.json new file mode 100644 index 0000000..134f8c2 --- /dev/null +++ b/src/main/resources/eventutils-1.21.11.mixin.json @@ -0,0 +1,20 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "cc.aabss.eventutils.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "EntityMixin" + ], + "client": [ + "ClientMixin", + "EntityRenderDispatcherMixin", + "EntryListWidgetAccessor", + "KeyBindingMixin", + "PlayerListHudMixin", + "PlayerEntityRendererMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/eventutils.mixin.json b/src/main/resources/eventutils.mixin.json index 753caf6..5d26e58 100644 --- a/src/main/resources/eventutils.mixin.json +++ b/src/main/resources/eventutils.mixin.json @@ -13,6 +13,7 @@ "EntryListWidgetAccessor", "KeyBindingMixin", "MultiplayerScreenMixin", + "PlayerListHudMixin", "PlayerEntityRendererMixin" ], "injectors": { diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 1d6f537..d11e515 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -13,7 +13,7 @@ "icon": "assets/eventutils/textures/icon.png", "environment": "*", "mixins": [ - "eventutils.mixin.json" + "${mixin_config}" ], "entrypoints": { "client": [ diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties new file mode 100644 index 0000000..5be05c9 --- /dev/null +++ b/versions/1.21.11/gradle.properties @@ -0,0 +1,6 @@ +deps.minecraft=1.21.11 +deps.yarn_mappings=1.21.11+build.1 +deps.fabric_api=0.141.0+1.21.11 + +deps.yacl=3.8.1+1.21.11-fabric +deps.modmenu=15.0.0-beta.3