From 0df550b95e0d116f62938a52fcb3b056796879f5 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Thu, 15 Oct 2026 13:21:23 +0800 Subject: [PATCH 1/4] refactor: share nick detection and bind commands to feature state --- docs/ARCHITECTURE.md | 2 +- docs/DEBUGGING.md | 4 +- docs/IMPLEMENTATION.md | 12 +- .../java/pit12/bootstrap/ClientBootstrap.java | 33 +++-- .../feature/chatmacro/ChatMacroFeature.java | 6 +- .../pit12/feature/denick/DenickConfig.java | 2 +- .../pit12/feature/denick/DenickFeature.java | 93 +++++------- .../pit12/feature/denick/api/DenickNames.java | 2 - .../feature/eventlist/EventListFeature.java | 6 +- .../pit12/feature/nametag/NametagFeature.java | 9 +- .../feature/playerlist/PlayerListBuilder.java | 17 ++- .../feature/playerlist/PlayerListConfig.java | 7 + .../feature/playerlist/PlayerListFeature.java | 6 +- .../java/pit12/feature/swap/SwapFeature.java | 6 +- .../platform/command/ForgeCommandAdapter.java | 34 ++++- .../accessor/CommandHandlerAccessor.java | 31 ++++ .../runtime/command/CommandRegistry.java | 89 ++++++++++-- .../java/pit12/runtime/nick/NickListener.java | 26 ++++ .../java/pit12/runtime/nick/NickTracker.java | 132 ++++++++++++++++++ src/main/resources/mixins.pit12.json | 1 + .../architecture/BootstrapLayoutTest.java | 1 + 21 files changed, 407 insertions(+), 112 deletions(-) create mode 100644 src/main/java/pit12/platform/mixin/accessor/CommandHandlerAccessor.java create mode 100644 src/main/java/pit12/runtime/nick/NickListener.java create mode 100644 src/main/java/pit12/runtime/nick/NickTracker.java diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8935c9a..f0166cf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -14,7 +14,7 @@ Each package immediately under `feature` owns one user-facing capability. Its se `platform` adapts external mechanisms such as Forge commands and Mixin. Adapters route external requests to their owners through narrow interfaces. An adapter can implement `ClientLifecycle` to manage its own subscriptions and registrations. -Commands definitions and actions are owned by features. `runtime.command` owns routing, help, completion, and availability of commands. `platform.command` owns Forge registration. +Command definitions and actions are owned by features. `runtime.command` owns routing, help, completion, and availability of commands. `platform.command` owns Forge registration. `feature.webui` owns the local HTTP server and connects the configuration interface to config and feature APIs. `web-ui` owns the browser pages. diff --git a/docs/DEBUGGING.md b/docs/DEBUGGING.md index bb85d64..92fa5b1 100644 --- a/docs/DEBUGGING.md +++ b/docs/DEBUGGING.md @@ -64,8 +64,6 @@ If Gradle sync fails or reports a toolchain error, check that Gradle uses JDK 21 In case the Vite page is opened, but the requests fail, make sure that the game is launched and the `WEB_UI_TARGET` matches the current address of the game. If the configuration interface fails to launch, look for the "Configuration interface is unavailable" message in the game log. -While reporting a failure, provide the relevant log messages and steps to reproduce it. - The saved data is in `12pit/` directory in the Minecraft game directory. Profiles are stored in `12pit/config/`, relations are stored in `12pit/relations.json`, and swap bindings are stored in `12pit/swap-bindings.json`. -Create a backup copy of these files before changing them in order to diagnose a load error. Do not delete the saved data in order to reset a temporary session issue. +Create a backup copy of these files before changing them in order to diagnose a load error. \ No newline at end of file diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 0fbadee..553d82e 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -94,7 +94,7 @@ The following helpers create settings and register their metadata in the feature - `booleanSetting` creates a boolean. Use `booleanSetting(id, defaultValue)` for a saved flag that has no interface control. - `integerSliderSetting` and `doubleSliderSetting` create a numeric setting with default value, minimum, maximum and step parameters. -- `keybindSetting` creates a `KeybindSetting` for one key and an optional Shift, Ctrl, Alt, or Meta key. The main key may also be a modifier. The saved integer keeps the main key in the low byte and the modifier in the next byte. Existing single-key values stay valid. Features use `Keybind.matches` for key events and `Keybind.held` for held input. +- `keybindSetting` creates a `KeybindSetting` for one key and an optional Shift, Ctrl, Alt, or Meta key. The main key may also be a modifier. The saved integer keeps the main key in the low byte and the modifier in the next byte. Features use `Keybind.matches` for key events and `Keybind.held` for held input. - `colorSetting` stores RGB color as `0xRRGGBB`. `colorPickerSetting` stores ARGB color as `0xAARRGGBB`. - `choiceSetting` creates a choice with default id and `ChoiceSetting.Choice` instances with stable ids. - `hudConfig` adds grouped anchor, text shadow, and translation options. It also stores placement and scale for the HUD editor. See [HUD](#hud) for grouping and storage details. @@ -128,7 +128,7 @@ private final ConfigChangeListener configListener = changes -> { `ConfigChangeSet.affects(featureId, settingId)` checks which settings changed. Listeners run after all changes are applied. `snapshot()` copies all registered values. `apply(snapshot)` validates all known values before changing live settings, then sends one notification for the whole set. It applies a complete snapshot: missing known values reset to their defaults, and unknown IDs are ignored. An empty snapshot resets all settings to their defaults. No notification is sent if the resulting values are unchanged. -`recoverSavedValues(snapshot, problems)` restores default values for saved values that are not valid and returns them through the callback. It creates a normalized snapshot but does not apply it. Live input uses a normal validation instead. Profile feature handles saving and switching config snapshots. +`recoverSavedValues(snapshot, problems)` restores default values for saved values that are not valid and returns them through the callback. It creates a normalized snapshot but does not apply it. Profile feature handles saving and switching config snapshots. ## Command @@ -148,14 +148,14 @@ commands.register(command); Use `register(command)` to make `/12pit message` available. If `/message` is supposed to be available too, use `register(command, true)` instead; both will use the same tree. Do not register a command twice. Names and aliases use only lowercase letters, digits, `-` and `_`; `12pit` is reserved as a root name. Names and aliases of siblings mustn't coincide. +Use `register(command, config, standalone)` for commands that belong to a toggleable feature. Call `commands.setActive(config, true)` after the feature starts and `commands.setActive(config, false)` when it stops. These commands are available only while their owner is started and enabled. Otherwise standalone names and aliases are removed and the whole subtree is hidden from `/12pit` help and completion. + `arguments(usage, minimum, maximum)` describes the argument display usage and number range. Default is no arguments. `executes` gets an `ICommandSender` and an array of `String` arguments without the matched command path. Registry checks the argument number; feature can check the values, readiness and result of the action. A node with children is a group. Without arguments it displays auto-generated help, even if there's a handler. Leaf with a handler executes it, if the argument number is correct. Child's name or alias matches case-insensitively and overrides group's handler. Group handler can accept extra unmatched arguments. Adding a single `help` argument will show group help unless there's an explicit `help` child. `requires(Predicate)` restricts use of a node. Help and completion hide children that cannot be used by the sender. The suggestion callback gets the arguments after the command path, including the current word. Return the complete set of suggestions. The registry filters it by the current prefix case-insensitively and removes duplicates. Return an empty list for no suggestions. -Bootstrap provides [ForgeCommandAdapter](../src/main/java/pit12/platform/command/ForgeCommandAdapter.java) through the `Registrar` of the registry. Features don't register Forge command classes manually. There's no way to unregister commands in Forge. `stop()` disallows execution and completion and `start()` will reuse existing entry points. Registration is closed after the first start. - ## Chat feedback Use [ChatFeedback](../src/main/java/pit12/shared/chat/ChatFeedback.java) for local chat messages: `ChatFeedback.reply(sender, Tone.INFO, message)`. Import `ChatFeedback.Tone` and choose `INFO`, `SUCCESS`, `WARNING`, or `ERROR` based on the outcome. The helper adds the shared prefix and tone color. @@ -296,6 +296,10 @@ Subscribe to name changes with `addListener(Runnable)` in `start()`, then read t Use `bindDisplayNameObserver(UUID, Consumer)` to watch one current Tab entry. Each entry has one observer, called on the client thread after its display name changes. Name queries can also refresh an entry and call its observer. The name keeps formatting codes and may be `null`. Pass `null` as the observer to detach it. The subscription ends when the entry leaves Tab or the connection changes. +[NickTracker](../src/main/java/pit12/runtime/nick/NickTracker.java) provides `isNicked(UUID)` for players in Tab. NPCs are not Nicks. + +Keep a [NickListener](../src/main/java/pit12/runtime/nick/NickListener.java) in a field. Register it with `nickTracker.addListener(listener)` in `start()`, then read the current Tab state, because existing entries are not replayed. `onNickChanged(playerId, name, nicked)` runs on the client thread when the status changes. `name` is the profile name. A Nick leaving Tab reports `nicked=false`. Unregister the same listener with `nickTracker.removeListener(listener)` in `stop()`. + [SidebarCache](../src/main/java/pit12/runtime/scoreboard/SidebarCache.java) provides `lines()` for the server sidebar. It returns an immutable snapshot of up to 15 lines in display order. Lines include footer text and keep formatting codes. The objective title is excluded. Each [SidebarLine](../src/main/java/pit12/runtime/scoreboard/SidebarLine.java) provides the entry ID, text, and sorting score through `entry()`, `text()`, and `score()`. `formattedText(minecraft.fontRendererObj)` removes formatting codes and characters with a width of zero in the Minecraft font. Call it on the client thread. It computes the text on the first call and caches it for that line. Score changes retain the cache. Text changes create a new line with an empty cache. diff --git a/src/main/java/pit12/bootstrap/ClientBootstrap.java b/src/main/java/pit12/bootstrap/ClientBootstrap.java index 6c7937b..c489dd0 100644 --- a/src/main/java/pit12/bootstrap/ClientBootstrap.java +++ b/src/main/java/pit12/bootstrap/ClientBootstrap.java @@ -91,6 +91,7 @@ import pit12.runtime.input.InputBlockBinding; import pit12.runtime.input.InputBlocker; import pit12.runtime.languages.Languages; +import pit12.runtime.nick.NickTracker; import pit12.runtime.pit.PitContext; import pit12.runtime.pit.PitContextTracker; import pit12.runtime.player.PlayerEntityTracker; @@ -132,7 +133,7 @@ public ClientBootstrap() { new LwjglInputDevice()); components.add(input); CommandRegistry commands = - new CommandRegistry(client, ForgeCommandAdapter::register, language); + new CommandRegistry(client, configs, ForgeCommandAdapter::register, language); components.add(commands); PitContextTracker pitContext = new PitContextTracker(session); components.add(pitContext); @@ -144,6 +145,8 @@ public ClientBootstrap() { components.add(presence); PlayerNameCache playerNames = new PlayerNameCache(minecraft, session, presence); components.add(playerNames); + NickTracker nicks = new NickTracker(client, presence, playerNames); + components.add(nicks); SidebarCache sidebar = new SidebarCache(minecraft, session); components.add(sidebar); components.add(new DailyUsageReporter(client, pitContext)); @@ -162,7 +165,7 @@ public ClientBootstrap() { (SwapHooksBinding) minecraft, input); // Category: Misc DenickNames denickNames = registerDenick(minecraft, configs, session, pitContext, presence, - playerNames, players, playerEquipment, commands, + playerNames, nicks, players, playerEquipment, commands, new File(minecraft.mcDataDir, "12pit/denick-cache.json").toPath()); // Category: Interface BooleanSetting discordRpc = @@ -187,10 +190,10 @@ public ClientBootstrap() { registerEventList(configs, hudRegistry, commands); registerGamma(configs, (GammaBinding) minecraft.entityRenderer); registerItemEsp(configs, session); - registerNametag(minecraft, configs, session, playerEquipment, denickNames); + registerNametag(minecraft, configs, session, playerEquipment, denickNames, nicks); registerPlayerEsp(configs, session, presence, relations); registerPlayerList(configs, playerNames, players, playerEquipment, pitContext, hudRegistry, - relations, presence, denickNames); + relations, presence, denickNames, nicks); registerRingHelper(minecraft, configs, session, pitContext); registerTooltip(configs); // Category: Misc @@ -244,13 +247,13 @@ private SwapBindings registerSwap(Minecraft minecraft, ClientThread client, // Category: Misc private DenickNames registerDenick(Minecraft minecraft, ConfigCatalog configs, ClientSession session, PitContext pitContext, TabPresence presence, - PlayerNameCache playerNames, PlayerEntityTracker players, + PlayerNameCache playerNames, NickTracker nicks, PlayerEntityTracker players, PlayerEquipmentAccess equipment, CommandRegistry commands, Path path) { DenickConfig config = new DenickConfig(); configs.register(config); DenickFeature denick = new DenickFeature(minecraft, client, configs, config, session, - pitContext, presence, playerNames, players, equipment, commands, language, path, - names -> ((DenickNamesBinding) minecraft.ingameGUI.getTabList()) + pitContext, presence, playerNames, nicks, players, equipment, commands, language, + path, names -> ((DenickNamesBinding) minecraft.ingameGUI.getTabList()) .pit12$bindDenickNames(names)); components.add(denick); return denick; @@ -349,14 +352,14 @@ private void registerItemEsp(ConfigCatalog configs, ClientSession session) { } private void registerNametag(Minecraft minecraft, ConfigCatalog configs, ClientSession session, - PlayerEquipmentAccess equipment, DenickNames denickNames) { + PlayerEquipmentAccess equipment, DenickNames denickNames, NickTracker nicks) { NametagConfig config = new NametagConfig(); configs.register(config); - components - .add(new NametagFeature(minecraft, configs, config, session, equipment, denickNames, - names -> minecraft.getRenderManager().getSkinMap().values() - .forEach(renderer -> ((NametagNamesBinding) renderer) - .pit12$bindNametagNames(names)))); + components.add(new NametagFeature(minecraft, configs, config, session, equipment, + denickNames, nicks, + names -> minecraft.getRenderManager().getSkinMap().values() + .forEach(renderer -> ((NametagNamesBinding) renderer) + .pit12$bindNametagNames(names)))); } private void registerPlayerEsp(ConfigCatalog configs, ClientSession session, @@ -369,11 +372,11 @@ private void registerPlayerEsp(ConfigCatalog configs, ClientSession session, private void registerPlayerList(ConfigCatalog configs, PlayerNameCache playerNames, PlayerEntityTracker players, PlayerEquipmentAccess playerEquipment, PitContext pitContext, HudRegistry hudRegistry, RelationLookup relations, - TabPresence presence, DenickNames denickNames) { + TabPresence presence, DenickNames denickNames, NickTracker nicks) { PlayerListConfig config = new PlayerListConfig(); configs.register(config); components.add(new PlayerListFeature(configs, config, playerNames, players, playerEquipment, - pitContext, hudRegistry, relations, presence, language, denickNames)); + pitContext, hudRegistry, relations, presence, language, denickNames, nicks)); } private void registerRingHelper(Minecraft minecraft, ConfigCatalog configs, diff --git a/src/main/java/pit12/feature/chatmacro/ChatMacroFeature.java b/src/main/java/pit12/feature/chatmacro/ChatMacroFeature.java index 44b6b3a..fcaa195 100644 --- a/src/main/java/pit12/feature/chatmacro/ChatMacroFeature.java +++ b/src/main/java/pit12/feature/chatmacro/ChatMacroFeature.java @@ -50,6 +50,7 @@ public final class ChatMacroFeature private final Minecraft minecraft; private final ConfigCatalog configs; private final ChatMacroConfig config; + private final CommandRegistry commands; private final ClientSession session; private final SidebarCache sidebar; private final InputBlocker input; @@ -73,6 +74,7 @@ public ChatMacroFeature(Minecraft minecraft, ClientThread client, ConfigCatalog this.minecraft = minecraft; this.configs = configs; this.config = config; + this.commands = commands; this.session = session; this.sidebar = sidebar; this.input = input; @@ -81,7 +83,7 @@ public ChatMacroFeature(Minecraft minecraft, ClientThread client, ConfigCatalog if (minecraft.thePlayer != null) ChatFeedback.reply(minecraft.thePlayer, Tone.ERROR, language.translate(message)); }); - commands.register(new ChatMacroCommand(bindings, language).definition(), true); + commands.register(new ChatMacroCommand(bindings, language).definition(), config, true); } @Override @@ -96,12 +98,14 @@ public void start() { session.addListener(sessionListener); language.addListener(languageListener); MinecraftForge.EVENT_BUS.register(this); + commands.setActive(config, true); } @Override public void stop() { session.checkThread(); started = false; + commands.setActive(config, false); cancel(false); input.removeHandler(this); configs.removeListener(this); diff --git a/src/main/java/pit12/feature/denick/DenickConfig.java b/src/main/java/pit12/feature/denick/DenickConfig.java index 58ba051..234d662 100644 --- a/src/main/java/pit12/feature/denick/DenickConfig.java +++ b/src/main/java/pit12/feature/denick/DenickConfig.java @@ -42,6 +42,6 @@ public DenickConfig() { source("Queues equipment changes until a real name is found."), true); subsubcategory("display", source("Display")); nickMarker = booleanSetting("nick_marker", source("Show nick marker"), - source("Adds N to nicked players' names in Tab and Player List."), true); + source("Adds N to nicked players' names in Tab."), true); } } diff --git a/src/main/java/pit12/feature/denick/DenickFeature.java b/src/main/java/pit12/feature/denick/DenickFeature.java index 82c7623..1bb7363 100644 --- a/src/main/java/pit12/feature/denick/DenickFeature.java +++ b/src/main/java/pit12/feature/denick/DenickFeature.java @@ -25,13 +25,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -40,12 +38,10 @@ import java.util.function.BooleanSupplier; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Pattern; import net.minecraft.client.Minecraft; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; -import net.minecraft.util.EnumChatFormatting; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent; @@ -58,6 +54,8 @@ import pit12.runtime.config.ConfigChangeSet; import pit12.runtime.config.Setting; import pit12.runtime.languages.Languages; +import pit12.runtime.nick.NickListener; +import pit12.runtime.nick.NickTracker; import pit12.runtime.pit.PitContext; import pit12.runtime.pit.PitState; import pit12.runtime.player.PlayerEntityTracker; @@ -76,23 +74,23 @@ import pit12.shared.lifecycle.ClientLifecycle; public final class DenickFeature implements ClientLifecycle, DenickNames, TabPresenceListener, - PlayerEquipmentListener, ConfigChangeListener { + PlayerEquipmentListener, ConfigChangeListener, NickListener { private static final Logger LOGGER = Logger.getLogger(DenickFeature.class.getName()); - private static final Pattern NPC_TAB_NAME = Pattern.compile("[a-z0-9]{10}(?![A-Za-z0-9_])"); private final Minecraft minecraft; private final ClientThread client; private final ConfigCatalog configs; private final DenickConfig config; + private final CommandRegistry commands; private final ClientSession session; private final PitContext pit; private final TabPresence presence; private final PlayerNameCache names; + private final NickTracker nicks; private final PlayerEntityTracker players; private final PlayerEquipmentAccess equipment; private final Languages language; private final DenickNamesBinding binding; private final DenickStore store; - private final Set checkedPlayers = new HashSet<>(); private final Map targets = new LinkedHashMap<>(); private final Map mappings = new LinkedHashMap<>(); private final List listeners = new ArrayList<>(); @@ -109,16 +107,19 @@ public final class DenickFeature implements ClientLifecycle, DenickNames, TabPre public DenickFeature(Minecraft minecraft, ClientThread client, ConfigCatalog configs, DenickConfig config, ClientSession session, PitContext pit, TabPresence presence, - PlayerNameCache names, PlayerEntityTracker players, PlayerEquipmentAccess equipment, - CommandRegistry commands, Languages language, Path path, DenickNamesBinding binding) { + PlayerNameCache names, NickTracker nicks, PlayerEntityTracker players, + PlayerEquipmentAccess equipment, CommandRegistry commands, Languages language, + Path path, DenickNamesBinding binding) { this.minecraft = minecraft; this.client = client; this.configs = configs; this.config = config; + this.commands = commands; this.session = session; this.pit = pit; this.presence = presence; this.names = names; + this.nicks = nicks; this.players = players; this.equipment = equipment; this.language = language; @@ -129,7 +130,7 @@ public DenickFeature(Minecraft minecraft, ClientThread client, ConfigCatalog con .suggests((sender, args) -> args.length == 1 ? new ArrayList<>(presence.players().values()) : Collections.singletonList("force")) - .build(), true); + .build(), config, true); } @Override @@ -153,11 +154,13 @@ public void start() { return thread; }); configs.addListener(this); + nicks.addListener(this); presence.addListener(this); equipment.addListener(this); session.addListener(sessionChanged); MinecraftForge.EVENT_BUS.register(this); resetWorld(); + presence.players().forEach((id, name) -> onPlayerSeen(id, name, true)); ExecutorService loader = storageWorker; loader.execute(() -> { Map loaded = null; @@ -179,12 +182,14 @@ public void start() { ready = true; }); }); + commands.setActive(config, true); } @Override public void stop() { client.check(); started = false; + commands.setActive(config, false); generation++; if (namesBound) { binding.pit12$bindDenickNames(null); @@ -195,14 +200,11 @@ public void stop() { session.removeListener(sessionChanged); equipment.removeListener(this); presence.removeListener(this); + nicks.removeListener(this); for (Target target : targets.values()) { target.cancel(); - if (target.nick) { - names.bindDisplayNameObserver(target.id, null); - } } targets.clear(); - checkedPlayers.clear(); if (queryWorker != null) { queryWorker.shutdownNow(); queryWorker = null; @@ -227,9 +229,6 @@ public void stop() { private void command(ICommandSender sender, String[] args) { client.check(); - if (!config.enabled()) { - throw new IllegalArgumentException("Denick is disabled"); - } if (!ready) { throw new IllegalArgumentException("Denick cache is loading"); } @@ -274,52 +273,37 @@ private void resetWorld() { changed(); } - private boolean isNick(UUID id) { - if (id.version() == 4 && id.variant() == 2) { - return false; - } - String display = names.displayName(id); - return display != null && !isNpc(display); - } - - private boolean isNpc(String display) { - String plain = EnumChatFormatting.getTextWithoutFormattingCodes(display).trim(); - return plain.contains("[NPC]") || NPC_TAB_NAME.matcher(display).lookingAt(); - } - @Override public void onPlayerSeen(UUID playerId, String name, boolean joined) { - if (name == null || !checkedPlayers.add(playerId)) { + if (name == null || !nicks.isNicked(playerId)) { return; } - if (isNick(playerId)) { - Target target = targets.computeIfAbsent(playerId, id -> new Target(id, name)); - target.nick = true; - names.bindDisplayNameObserver(playerId, display -> { - if (display != null && isNpc(display)) { - // A repeated ADD packet must not mark this entry as Nick again. - Target removed = targets.remove(playerId); - removed.cancel(); - names.bindDisplayNameObserver(playerId, null); - changed(); - } - }); - if (config.enabled() && config.automatic.get()) { - begin(target, null, false); - } - changed(); + Target target = targets.computeIfAbsent(playerId, id -> new Target(id, name)); + // Tab and NickTracker both report the same player. + if (target.nick) { + return; + } + target.nick = true; + if (config.enabled() && config.automatic.get()) { + begin(target, null, false); + } + changed(); + } + + @Override + public void onNickChanged(UUID playerId, String name, boolean nicked) { + if (nicked) { + onPlayerSeen(playerId, name, true); + } else { + onPlayerLeft(playerId); } } @Override public void onPlayerLeft(UUID playerId) { - checkedPlayers.remove(playerId); Target target = targets.remove(playerId); if (target != null) { target.cancel(); - if (target.nick) { - names.bindDisplayNameObserver(playerId, null); - } changed(); } } @@ -611,13 +595,6 @@ private String displayName(Target target) { return displayName == null ? target.name : displayName; } - @Override - public boolean isNicked(UUID playerId) { - client.check(); - Target target = targets.get(playerId); - return config.enabled() && target != null && target.nick; - } - @Override public String listName(UUID playerId, String original) { client.check(); diff --git a/src/main/java/pit12/feature/denick/api/DenickNames.java b/src/main/java/pit12/feature/denick/api/DenickNames.java index 3621158..690605a 100644 --- a/src/main/java/pit12/feature/denick/api/DenickNames.java +++ b/src/main/java/pit12/feature/denick/api/DenickNames.java @@ -22,8 +22,6 @@ /** Use this API on the client thread. */ public interface DenickNames { - boolean isNicked(UUID playerId); - String listName(UUID playerId, String original); /** Returns null when Denick is disabled or no real name is known. */ diff --git a/src/main/java/pit12/feature/eventlist/EventListFeature.java b/src/main/java/pit12/feature/eventlist/EventListFeature.java index 26a1d50..a7a3659 100644 --- a/src/main/java/pit12/feature/eventlist/EventListFeature.java +++ b/src/main/java/pit12/feature/eventlist/EventListFeature.java @@ -50,6 +50,7 @@ public final class EventListFeature implements ClientLifecycle, ConfigChangeList private final ConfigCatalog configs; private final ClientThread client; private final EventListConfig config; + private final CommandRegistry commands; private final HudRegistry hudRegistry; private final EventListHud hud; private final FontListener fontListener = new FontListener(); @@ -76,9 +77,10 @@ public EventListFeature(ConfigCatalog configs, EventListConfig config, HudRegist this.configs = configs; client = configs.clientThread(); this.config = config; + this.commands = commands; this.hudRegistry = hudRegistry; hud = new EventListHud(config, hudRenderer, language); - commands.register(new EventListCommand(this, language).definition()); + commands.register(new EventListCommand(this, language).definition(), config, false); languageListener = () -> { hud.localize(); rebuild(System.currentTimeMillis()); @@ -109,6 +111,7 @@ public void start() { if (config.enabled()) { activate(); } + commands.setActive(config, true); } @Override @@ -118,6 +121,7 @@ public void stop() { return; } started = false; + commands.setActive(config, false); MinecraftForge.EVENT_BUS.unregister(fontListener); configs.removeListener(this); language.removeListener(languageListener); diff --git a/src/main/java/pit12/feature/nametag/NametagFeature.java b/src/main/java/pit12/feature/nametag/NametagFeature.java index bbaf3d6..b93ef40 100644 --- a/src/main/java/pit12/feature/nametag/NametagFeature.java +++ b/src/main/java/pit12/feature/nametag/NametagFeature.java @@ -37,6 +37,7 @@ import pit12.runtime.config.ConfigChangeListener; import pit12.runtime.config.ConfigChangeSet; import pit12.runtime.item.PitEnchantmentFormat; +import pit12.runtime.nick.NickTracker; import pit12.runtime.player.PlayerEquipmentAccess; import pit12.runtime.player.PlayerEquipmentCache; import pit12.runtime.player.PlayerEquipmentListener; @@ -52,6 +53,7 @@ public final class NametagFeature private final ClientSession session; private final PlayerEquipmentAccess equipment; private final DenickNames denickNames; + private final NickTracker nicks; private final NametagNamesBinding binding; private final NametagRenderer renderer = new NametagRenderer(); private final Map texts = new HashMap<>(); @@ -64,13 +66,14 @@ public final class NametagFeature public NametagFeature(Minecraft minecraft, ConfigCatalog configs, NametagConfig config, ClientSession session, PlayerEquipmentAccess equipment, DenickNames denickNames, - NametagNamesBinding binding) { + NickTracker nicks, NametagNamesBinding binding) { this.minecraft = minecraft; this.configs = configs; this.config = config; this.session = session; this.equipment = equipment; this.denickNames = denickNames; + this.nicks = nicks; this.binding = binding; } @@ -229,8 +232,8 @@ private String displayName(UUID playerId, String original) { if (!config.enabled()) { return original; } - String name = config.nickMarker() && denickNames.isNicked(playerId) ? original + " §e§lN" - : original; + String name = + config.nickMarker() && nicks.isNicked(playerId) ? original + " §e§lN" : original; if (config.realNames()) { String realName = denickNames.realDisplayName(playerId); if (realName != null) { diff --git a/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java b/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java index b389db8..e546c13 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java @@ -34,6 +34,7 @@ import pit12.feature.relation.api.RelationLookup; import pit12.runtime.item.PitEnchantment; import pit12.runtime.item.PitEnchantmentFormat; +import pit12.runtime.nick.NickTracker; import pit12.runtime.pit.PitContext; import pit12.runtime.pit.PitSnapshot; import pit12.runtime.pit.SpawnState; @@ -52,12 +53,14 @@ final class PlayerListBuilder { private final RelationLookup relations; private final PlayerNameCache playerNames; private final DenickNames denickNames; + private final NickTracker nicks; private final Map previousEntries = new HashMap<>(); private WorldClient previousWorld; PlayerListBuilder(Minecraft minecraft, PlayerEntityTracker players, PlayerEquipmentAccess equipment, PitContext pitContext, PlayerListConfig config, - RelationLookup relations, PlayerNameCache playerNames, DenickNames denickNames) { + RelationLookup relations, PlayerNameCache playerNames, DenickNames denickNames, + NickTracker nicks) { this.minecraft = minecraft; this.players = players; this.equipment = equipment; @@ -66,6 +69,7 @@ final class PlayerListBuilder { this.relations = relations; this.playerNames = playerNames; this.denickNames = denickNames; + this.nicks = nicks; } PlayerListSnapshot build(boolean realNames, boolean rebuildText) { @@ -104,8 +108,8 @@ PlayerListSnapshot build(boolean realNames, boolean rebuildText) { : relation == Relation.ENEMY ? PlayerListGroup.ENEMY : null; PlayerListGroup group = equipmentGroup(playerEquipment); boolean showSpecial = special != null && config.showGroup(special); - boolean showNick = - config.showGroup(PlayerListGroup.NICK) && denickNames.isNicked(playerId); + boolean nicked = nicks.isNicked(playerId); + boolean showNick = config.showGroup(PlayerListGroup.NICK) && nicked; boolean showEquipment = group != null && config.showGroup(group); if (!showSpecial && !showNick && !showEquipment) { continue; @@ -119,8 +123,11 @@ PlayerListSnapshot build(boolean realNames, boolean rebuildText) { } if (previous == null) { String realName = realNames ? denickNames.realDisplayName(playerId) : null; - name = realName == null ? denickNames.listName(playerId, name) - : PRESTIGE_PREFIX.matcher(realName).replaceFirst("["); + if (realName != null) { + name = PRESTIGE_PREFIX.matcher(realName).replaceFirst("["); + } else if (config.nickMarker() && nicked) { + name = name.trim() + " §e§lN"; + } } EntityPlayer player = players.loadedPlayer(playerId); boolean distanceKnown = localPlayer != null && player != null; diff --git a/src/main/java/pit12/feature/playerlist/PlayerListConfig.java b/src/main/java/pit12/feature/playerlist/PlayerListConfig.java index c035dbd..b99b458 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListConfig.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListConfig.java @@ -36,6 +36,7 @@ public final class PlayerListConfig extends FeatureConfig { private final ChoiceSetting enchantmentFormat; private final BooleanSetting showGem; private final BooleanSetting shortPlayerNames; + private final BooleanSetting nickMarker; private final KeybindSetting realNamesKey; private final BooleanSetting showDistance; private final BooleanSetting showDirection; @@ -67,6 +68,8 @@ public PlayerListConfig() { subsubcategory("player_information", source("Player information")); shortPlayerNames = booleanSetting("short_player_names", source("Remove bounty suffix"), source("Removes bounty suffixes from player names."), false); + nickMarker = booleanSetting("nick_marker", source("Show nick marker"), + source("Adds N to nicked players' names in Player List."), true); showDistance = booleanSetting("show_distance", source("Show player distance"), source("Shows the distance to each loaded player."), true); showDirection = booleanSetting("show_direction", source("Show player direction"), @@ -118,6 +121,10 @@ public int realNamesKey() { return realNamesKey.get(); } + public boolean nickMarker() { + return nickMarker.get(); + } + public boolean showDistance() { return showDistance.get(); } diff --git a/src/main/java/pit12/feature/playerlist/PlayerListFeature.java b/src/main/java/pit12/feature/playerlist/PlayerListFeature.java index fda7b13..d68a67b 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListFeature.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListFeature.java @@ -39,6 +39,7 @@ import pit12.runtime.hud.HudRegistry; import pit12.runtime.hud.HudRenderer; import pit12.runtime.languages.Languages; +import pit12.runtime.nick.NickTracker; import pit12.runtime.pit.PitContext; import pit12.runtime.player.PlayerEntityTracker; import pit12.runtime.player.PlayerEquipmentAccess; @@ -79,7 +80,7 @@ public PlayerListFeature(ConfigCatalog configs, PlayerListConfig config, PlayerNameCache playerNames, PlayerEntityTracker players, PlayerEquipmentAccess equipment, PitContext pitContext, HudRegistry hudRegistry, RelationLookup relations, TabPresence presence, Languages language, - DenickNames denickNames) { + DenickNames denickNames, NickTracker nicks) { this.language = language; this.configs = configs; this.config = config; @@ -90,7 +91,7 @@ public PlayerListFeature(ConfigCatalog configs, PlayerListConfig config, this.denickNames = denickNames; this.hudRegistry = hudRegistry; builder = new PlayerListBuilder(minecraft, players, equipment, pitContext, config, - relations, playerNames, denickNames); + relations, playerNames, denickNames, nicks); hud = new PlayerListHud(config, hudRenderer); languageListener = () -> hud.localize(language); } @@ -222,6 +223,7 @@ public void onConfigChanged(ConfigChangeSet changes) { || changes.affects("playerlist", "enchantment_format") || changes.affects("playerlist", "show_gem") || changes.affects("playerlist", "short_player_names") + || changes.affects("playerlist", "nick_marker") || changes.affects("playerlist", "real_names_key") || changes.affects("playerlist", "show_distance") || changes.affects("playerlist", "show_direction") diff --git a/src/main/java/pit12/feature/swap/SwapFeature.java b/src/main/java/pit12/feature/swap/SwapFeature.java index 7d65a9a..c111d1b 100644 --- a/src/main/java/pit12/feature/swap/SwapFeature.java +++ b/src/main/java/pit12/feature/swap/SwapFeature.java @@ -56,6 +56,7 @@ public final class SwapFeature private final Minecraft minecraft; private final ConfigCatalog configs; private final SwapConfig config; + private final CommandRegistry commands; private final ClientSession session; private final BindingBook bindings; private final SwapController controller; @@ -76,6 +77,7 @@ public SwapFeature(Minecraft minecraft, ClientThread client, ConfigCatalog confi this.minecraft = minecraft; this.configs = configs; this.config = config; + this.commands = commands; this.session = session; this.hookBinding = hookBinding; this.input = input; @@ -89,7 +91,7 @@ public SwapFeature(Minecraft minecraft, ClientThread client, ConfigCatalog confi overlay = new SwapOverlay(minecraft, bindings, config); commands.register( new SwapCommand(minecraft, bindings, config, automatic, language).definition(), - true); + config, true); } public SwapBindings bindings() { @@ -106,6 +108,7 @@ public void start() { session.addListener(sessionListener); bindings.start(); updateListening(); + commands.setActive(config, true); } @Override @@ -114,6 +117,7 @@ public void stop() { if (!started) return; started = false; + commands.setActive(config, false); controller.finish(false); configs.removeListener(this); session.removeListener(sessionListener); diff --git a/src/main/java/pit12/platform/command/ForgeCommandAdapter.java b/src/main/java/pit12/platform/command/ForgeCommandAdapter.java index 07d1ed4..dfed9be 100644 --- a/src/main/java/pit12/platform/command/ForgeCommandAdapter.java +++ b/src/main/java/pit12/platform/command/ForgeCommandAdapter.java @@ -18,12 +18,17 @@ */ package pit12.platform.command; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import net.minecraft.command.CommandBase; import net.minecraft.command.CommandException; +import net.minecraft.command.ICommand; import net.minecraft.command.ICommandSender; import net.minecraft.util.BlockPos; import net.minecraftforge.client.ClientCommandHandler; +import pit12.platform.mixin.accessor.CommandHandlerAccessor; import pit12.runtime.command.CommandNode; import pit12.runtime.command.CommandRegistry; @@ -36,8 +41,31 @@ private ForgeCommandAdapter(CommandRegistry registry, CommandNode command) { this.command = command; } - public static void register(CommandRegistry registry, CommandNode command) { - ClientCommandHandler.instance.registerCommand(new ForgeCommandAdapter(registry, command)); + public static Runnable register(CommandRegistry registry, CommandNode command) { + ClientCommandHandler handler = ClientCommandHandler.instance; + ForgeCommandAdapter adapter = new ForgeCommandAdapter(registry, command); + List names = new ArrayList<>(command.aliases()); + names.add(command.name()); + Map previous = new LinkedHashMap<>(); + Map commands = handler.getCommands(); + for (String name : names) { + previous.put(name, commands.get(name)); + } + handler.registerCommand(adapter); + return () -> { + for (Map.Entry entry : previous.entrySet()) { + // Someone may have taken this name since. Leave theirs alone. + if (commands.get(entry.getKey()) == adapter) { + if (entry.getValue() == null) { + commands.remove(entry.getKey()); + } else { + commands.put(entry.getKey(), entry.getValue()); + } + } + } + // Forge has no unregister. registerCommand also added it to commandSet. + ((CommandHandlerAccessor) handler).pit12$getCommandSet().remove(adapter); + }; } @Override @@ -62,7 +90,7 @@ public int getRequiredPermissionLevel() { @Override public boolean canCommandSenderUseCommand(ICommandSender sender) { - return command.canUse(sender); + return registry.available(command) && command.canUse(sender); } @Override diff --git a/src/main/java/pit12/platform/mixin/accessor/CommandHandlerAccessor.java b/src/main/java/pit12/platform/mixin/accessor/CommandHandlerAccessor.java new file mode 100644 index 0000000..5b928da --- /dev/null +++ b/src/main/java/pit12/platform/mixin/accessor/CommandHandlerAccessor.java @@ -0,0 +1,31 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.platform.mixin.accessor; + +import java.util.Set; +import net.minecraft.command.CommandHandler; +import net.minecraft.command.ICommand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(CommandHandler.class) +public interface CommandHandlerAccessor { + @Accessor("commandSet") + Set pit12$getCommandSet(); +} diff --git a/src/main/java/pit12/runtime/command/CommandRegistry.java b/src/main/java/pit12/runtime/command/CommandRegistry.java index 60a307c..8ffc72b 100644 --- a/src/main/java/pit12/runtime/command/CommandRegistry.java +++ b/src/main/java/pit12/runtime/command/CommandRegistry.java @@ -24,31 +24,43 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import net.minecraft.command.CommandException; +import net.minecraft.command.CommandNotFoundException; import net.minecraft.command.ICommandSender; +import pit12.runtime.config.ConfigCatalog; +import pit12.runtime.config.ConfigChangeListener; +import pit12.runtime.config.ConfigChangeSet; +import pit12.runtime.config.FeatureConfig; import pit12.runtime.languages.Languages; import pit12.shared.chat.ChatFeedback.Tone; import pit12.shared.concurrent.ClientThread; import pit12.shared.lifecycle.ClientLifecycle; -public final class CommandRegistry implements ClientLifecycle { +public final class CommandRegistry implements ClientLifecycle, ConfigChangeListener { private final ClientThread client; + private final ConfigCatalog configs; private final Languages language; private final Registrar registrar; private final CommandNode.Builder rootBuilder = CommandNode.command("12pit", source("12pit commands")); private final List entryPoints = new ArrayList<>(); private CommandNode root; - private int installed; + private final Map owners = new LinkedHashMap<>(); + private final Set activeOwners = new LinkedHashSet<>(); + private final Map installed = new LinkedHashMap<>(); private boolean started; - public CommandRegistry(ClientThread client, Registrar registrar, Languages language) { + public CommandRegistry(ClientThread client, ConfigCatalog configs, Registrar registrar, + Languages language) { this.language = language; this.client = client; + this.configs = configs; this.registrar = registrar; } @@ -57,6 +69,10 @@ public void register(CommandNode command) { } public void register(CommandNode command, boolean standalone) { + register(command, null, standalone); + } + + public void register(CommandNode command, FeatureConfig owner, boolean standalone) { client.check(); if (root != null) { throw new IllegalStateException("Commands must be registered before startup"); @@ -65,6 +81,9 @@ public void register(CommandNode command, boolean standalone) { throw new IllegalArgumentException("The command name 12pit is reserved"); } rootBuilder.child(command); + if (owner != null) { + owners.put(command, owner); + } if (standalone) { entryPoints.add(command); } @@ -80,18 +99,61 @@ public void start() { root = rootBuilder.build(); entryPoints.add(0, root); } - // Forge won't let us unregister these. Reuse them if start() runs again. - while (installed < entryPoints.size()) { - registrar.register(this, entryPoints.get(installed)); - installed++; - } started = true; + configs.addListener(this); + updateRegistrations(); } @Override public void stop() { client.check(); started = false; + configs.removeListener(this); + for (Runnable unregister : installed.values()) { + unregister.run(); + } + installed.clear(); + } + + public boolean available(CommandNode command) { + client.check(); + FeatureConfig owner = owners.get(command); + return started && (owner == null || (activeOwners.contains(owner) && owner.enabled())); + } + + public void setActive(FeatureConfig owner, boolean active) { + client.check(); + if (active) { + activeOwners.add(owner); + } else { + activeOwners.remove(owner); + } + updateRegistrations(); + } + + @Override + public void onConfigChanged(ConfigChangeSet changes) { + for (FeatureConfig owner : owners.values()) { + if (changes.affects(owner.id(), "enabled")) { + updateRegistrations(); + return; + } + } + } + + private void updateRegistrations() { + for (CommandNode command : entryPoints) { + if (available(command)) { + if (!installed.containsKey(command)) { + installed.put(command, registrar.register(this, command)); + } + } else { + Runnable unregister = installed.remove(command); + if (unregister != null) { + unregister.run(); + } + } + } } public String usage(CommandNode command) { @@ -110,6 +172,9 @@ public void execute(CommandNode command, ICommandSender sender, String[] args) private void execute(CommandNode command, ICommandSender sender, String[] args, String path) throws CommandException { + if (!available(command)) { + throw new CommandNotFoundException(); + } if (!command.canUse(sender)) { throw new CommandException("commands.generic.permission"); } @@ -143,7 +208,7 @@ private void execute(CommandNode command, ICommandSender sender, String[] args, public List complete(CommandNode command, ICommandSender sender, String[] args) { client.check(); - if (!started || args.length == 0 || !command.canUse(sender)) { + if (!available(command) || args.length == 0 || !command.canUse(sender)) { return Collections.emptyList(); } if (args.length > 1) { @@ -155,7 +220,7 @@ public List complete(CommandNode command, ICommandSender sender, String[ Set options = new LinkedHashSet<>(); if (args.length == 1) { for (CommandNode child : command.children) { - if (child.canUse(sender)) { + if (available(child) && child.canUse(sender)) { options.add(child.name); options.addAll(child.aliases); } @@ -184,7 +249,7 @@ private static String usage(CommandNode command, String path) { private void help(CommandNode command, ICommandSender sender, String path) { reply(sender, Tone.INFO, path + " - " + language.translate(command.description)); for (CommandNode child : command.children) { - if (child.canUse(sender)) { + if (available(child) && child.canUse(sender)) { reply(sender, Tone.INFO, usage(child, path + " " + child.name) + " - " + language.translate(child.description)); } @@ -197,6 +262,6 @@ private void help(CommandNode command, ICommandSender sender, String path) { @FunctionalInterface public interface Registrar { - void register(CommandRegistry registry, CommandNode command); + Runnable register(CommandRegistry registry, CommandNode command); } } diff --git a/src/main/java/pit12/runtime/nick/NickListener.java b/src/main/java/pit12/runtime/nick/NickListener.java new file mode 100644 index 0000000..2e234fd --- /dev/null +++ b/src/main/java/pit12/runtime/nick/NickListener.java @@ -0,0 +1,26 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.runtime.nick; + +import java.util.UUID; + +public interface NickListener { + /** nicked is false when the player leaves Tab or turns out to be an NPC. */ + void onNickChanged(UUID playerId, String name, boolean nicked); +} diff --git a/src/main/java/pit12/runtime/nick/NickTracker.java b/src/main/java/pit12/runtime/nick/NickTracker.java new file mode 100644 index 0000000..8060b36 --- /dev/null +++ b/src/main/java/pit12/runtime/nick/NickTracker.java @@ -0,0 +1,132 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.runtime.nick; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; +import net.minecraft.util.EnumChatFormatting; +import pit12.runtime.player.PlayerNameCache; +import pit12.runtime.player.TabPresence; +import pit12.runtime.player.TabPresenceListener; +import pit12.shared.concurrent.ClientThread; +import pit12.shared.event.Listeners; +import pit12.shared.lifecycle.ClientLifecycle; + +/** Use this API on the client thread. */ +public final class NickTracker implements ClientLifecycle, TabPresenceListener { + private static final Pattern NPC_TAB_NAME = Pattern.compile("[a-z0-9]{10}(?![A-Za-z0-9_])"); + private final ClientThread client; + private final TabPresence presence; + private final PlayerNameCache names; + private final Set checked = new HashSet<>(); + private final Map nicked = new HashMap<>(); + private final List listeners = new ArrayList<>(); + private boolean started; + + public NickTracker(ClientThread client, TabPresence presence, PlayerNameCache names) { + this.client = client; + this.presence = presence; + this.names = names; + } + + @Override + public void start() { + client.check(); + if (started) { + return; + } + started = true; + presence.addListener(this); + presence.players().forEach((id, name) -> onPlayerSeen(id, name, true)); + } + + @Override + public void stop() { + client.check(); + started = false; + presence.removeListener(this); + for (UUID playerId : nicked.keySet()) { + names.bindDisplayNameObserver(playerId, null); + } + nicked.clear(); + checked.clear(); + listeners.clear(); + } + + public boolean isNicked(UUID playerId) { + client.check(); + return nicked.containsKey(playerId); + } + + public void addListener(NickListener listener) { + client.check(); + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + public void removeListener(NickListener listener) { + client.check(); + listeners.remove(listener); + } + + @Override + public void onPlayerSeen(UUID playerId, String name, boolean joined) { + if (name == null || (playerId.version() == 4 && playerId.variant() == 2) + || !checked.add(playerId)) { + return; + } + String display = names.displayName(playerId); + if (display == null || isNpc(display)) { + return; + } + nicked.put(playerId, name); + names.bindDisplayNameObserver(playerId, updated -> { + if (updated != null && isNpc(updated)) { + // Stay in checked, or a repeated ADD packet marks this NPC as Nick again. + nicked.remove(playerId); + names.bindDisplayNameObserver(playerId, null); + Listeners.notify(listeners, + listener -> listener.onNickChanged(playerId, name, false)); + } + }); + Listeners.notify(listeners, listener -> listener.onNickChanged(playerId, name, true)); + } + + @Override + public void onPlayerLeft(UUID playerId) { + checked.remove(playerId); + String name = nicked.remove(playerId); + if (name != null) { + names.bindDisplayNameObserver(playerId, null); + Listeners.notify(listeners, listener -> listener.onNickChanged(playerId, name, false)); + } + } + + private boolean isNpc(String display) { + String plain = EnumChatFormatting.getTextWithoutFormattingCodes(display).trim(); + return plain.contains("[NPC]") || NPC_TAB_NAME.matcher(display).lookingAt(); + } +} diff --git a/src/main/resources/mixins.pit12.json b/src/main/resources/mixins.pit12.json index f8f2707..ffbc92b 100644 --- a/src/main/resources/mixins.pit12.json +++ b/src/main/resources/mixins.pit12.json @@ -7,6 +7,7 @@ "compatibilityLevel": "JAVA_8", "mixins": [], "client": [ + "accessor.CommandHandlerAccessor", "accessor.MinecraftAccessor", "accessor.FMLModContainerAccessor", "client.MinecraftMixin", diff --git a/src/test/java/pit12/architecture/BootstrapLayoutTest.java b/src/test/java/pit12/architecture/BootstrapLayoutTest.java index 5561527..3bc594e 100644 --- a/src/test/java/pit12/architecture/BootstrapLayoutTest.java +++ b/src/test/java/pit12/architecture/BootstrapLayoutTest.java @@ -122,6 +122,7 @@ public void sectionsAndRegistrationOrderMatchCategoriesAndDependencies() throws Matcher objects = Pattern.compile("\\b(\\w+)\\s*=\\s*new\\s+(\\w+)\\(([^;]*)\\);") .matcher(constructorBody); while (objects.find()) { + // An object is its own producer, so later calls can use its name. producers.put(objects.group(1), objects.group(1)); arguments.put(objects.group(1), objects.group(3)); String type = imports.get(objects.group(2)); From 61befff9f3b2c53cd9c91a85bc249c4d95acc8c2 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Thu, 15 Oct 2026 13:41:55 +0800 Subject: [PATCH 2/4] docs: l10n --- docs/zh-CN/ARCHITECTURE.md | 2 +- docs/zh-CN/DEBUGGING.md | 6 ++---- docs/zh-CN/IMPLEMENTATION.md | 14 +++++++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/zh-CN/ARCHITECTURE.md b/docs/zh-CN/ARCHITECTURE.md index e927d4b..a3bd537 100644 --- a/docs/zh-CN/ARCHITECTURE.md +++ b/docs/zh-CN/ARCHITECTURE.md @@ -1,4 +1,4 @@ - + # 12pit 架构 diff --git a/docs/zh-CN/DEBUGGING.md b/docs/zh-CN/DEBUGGING.md index 2a45c59..366bc03 100644 --- a/docs/zh-CN/DEBUGGING.md +++ b/docs/zh-CN/DEBUGGING.md @@ -1,4 +1,4 @@ - + # 运行和调试 12pit @@ -66,8 +66,6 @@ npm run format:check --prefix web-ui 如果 Vite 页面已打开,但请求失败,确保游戏已启动,且 `WEB_UI_TARGET` 与游戏当前地址一致。如果配置界面无法启动,在游戏日志中查找 "Configuration interface is unavailable" 消息。 -报告故障时,提供相关日志消息和复现步骤。 - 保存的数据位于 Minecraft 游戏目录中的 `12pit/` 目录。配置方案存储在 `12pit/config/`,关系存储在 `12pit/relations.json`,切换绑定存储在 `12pit/swap-bindings.json`。 -为诊断加载错误而修改这些文件前,先创建备份。不要为了重置临时会话问题而删除已保存的数据。 +为诊断加载错误而修改这些文件前,先创建备份。 diff --git a/docs/zh-CN/IMPLEMENTATION.md b/docs/zh-CN/IMPLEMENTATION.md index 3b56852..1635f81 100644 --- a/docs/zh-CN/IMPLEMENTATION.md +++ b/docs/zh-CN/IMPLEMENTATION.md @@ -1,4 +1,4 @@ - + # 12pit 实现 @@ -96,7 +96,7 @@ public final class StatusConfig extends FeatureConfig { - `booleanSetting` 创建布尔值。使用 `booleanSetting(id, defaultValue)` 创建需要保存但没有界面控件的布尔标志。 - `integerSliderSetting` 和 `doubleSliderSetting` 创建数值设置,参数包括默认值、最小值、最大值和步长。 -- `keybindSetting` 创建一个 `KeybindSetting`,支持一个主键和一个可选的 Shift、Ctrl、Alt 或 Meta 修饰键。主键也可以是修饰键。保存的整数用低字节存主键,下一字节存修饰键。现有单键值仍然有效。功能用 `Keybind.matches` 匹配按键事件,用 `Keybind.held` 判断按住状态。 +- `keybindSetting` 创建一个 `KeybindSetting`,支持一个主键和一个可选的 Shift、Ctrl、Alt 或 Meta 修饰键。主键也可以是修饰键。保存的整数用低字节存主键,下一字节存修饰键。功能用 `Keybind.matches` 匹配按键事件,用 `Keybind.held` 判断按住状态。 - `colorSetting` 将 RGB 颜色存储为 `0xRRGGBB`。`colorPickerSetting` 将 ARGB 颜色存储为 `0xAARRGGBB`。 - `choiceSetting` 使用默认 ID 和具有稳定 ID 的 `ChoiceSetting.Choice` 实例创建选项设置。 - `hudConfig` 为 HUD 添加分组后的锚点、文字阴影和翻译选项,并存储 HUD 编辑器使用的位置和缩放。分组和存储细节见 [HUD](#hud)。 @@ -130,7 +130,7 @@ private final ConfigChangeListener configListener = changes -> { `ConfigChangeSet.affects(featureId, settingId)` 判断哪些设置发生了变化。监听器在所有变更应用后被调用。`snapshot()` 复制所有已注册的值。`apply(snapshot)` 在修改实时设置前校验所有已知值,然后为整组变更发送一次通知。它应用完整快照,缺失的已知值会重置为默认值,未知 ID 会被忽略。空快照会将所有设置重置为默认值。只有最终值没有实际变化时,才不发送通知。 -`recoverSavedValues(snapshot, problems)` 将无效的已保存值恢复为默认值,并通过回调返回这些值。它创建规范化的快照,但不应用它。实时输入改用普通校验。配置方案功能负责保存和切换配置快照。 +`recoverSavedValues(snapshot, problems)` 将无效的已保存值恢复为默认值,并通过回调返回这些值。它创建规范化的快照,但不应用它。配置方案功能负责保存和切换配置快照。 ## 命令 @@ -150,14 +150,14 @@ commands.register(command); 使用 `register(command)` 使 `/12pit message` 可用。如果 `/message` 也应可用,改用 `register(command, true)`;两者会使用同一棵树。不要重复注册命令。名称和别名只能使用小写字母、数字、`-` 和 `_`;`12pit` 是保留的根名称。同级节点的名称和别名不得相同。 +属于可开关功能的命令使用 `register(command, config, standalone)`。功能启动完成后调用 `commands.setActive(config, true)`,停止时调用 `commands.setActive(config, false)`。只有所属功能已启动且开启时,这些命令才可用。否则会移除独立命令的名称和别名,并从 `/12pit` 的帮助和补全中隐藏整个子树。 + `arguments(usage, minimum, maximum)` 描述显示给用户的参数用法和数量范围。默认没有参数。`executes` 接收一个 `ICommandSender` 和一个不包含已匹配命令路径的 `String` 参数数组。注册表检查参数数量;功能可以检查值、就绪状态和操作结果。 有子节点的节点是分组。没有参数时,它显示自动生成的帮助,即使存在处理器。带有处理器的叶节点在参数数量正确时执行处理器。子节点的名称或别名匹配不区分大小写,并覆盖分组的处理器。分组处理器可以接收额外的未匹配参数。添加单个 `help` 参数会显示分组帮助,除非存在显式的 `help` 子节点。 `requires(Predicate)` 限制节点的使用。帮助和补全会隐藏发送者无法使用的子节点。补全回调接收命令路径之后的参数,包括当前单词。返回完整的补全建议集合。注册表按当前前缀过滤,不区分大小写,并去除重复项。没有建议时返回空列表。 -Bootstrap 通过注册表的 `Registrar` 提供 [ForgeCommandAdapter](../../src/main/java/pit12/platform/command/ForgeCommandAdapter.java)。功能不手动注册 Forge 命令类。Forge 无法注销命令。`stop()` 禁止执行和补全,`start()` 会复用现有入口。首次启动后关闭注册。 - ## 聊天回显 本地聊天回显统一使用 [ChatFeedback](../../src/main/java/pit12/shared/chat/ChatFeedback.java):`ChatFeedback.reply(sender, Tone.INFO, message)`。导入 `ChatFeedback.Tone`,按结果选择 `INFO`、`SUCCESS`、`WARNING` 或 `ERROR`。共享方法统一添加前缀和消息类型对应的颜色。 @@ -298,6 +298,10 @@ Bootstrap 为功能提供查询契约。其实时查询和订阅需要客户端 通过 `bindDisplayNameObserver(UUID, Consumer)` 监听一个当前 Tab 条目。每个条目只能绑定一个监听器,显示名变化后在客户端线程上调用。名称查询也可能刷新条目并调用监听器。名称保留格式化代码,也可能为 `null`。将监听器设为 `null` 可取消绑定。条目离开 Tab 或连接变化时,订阅结束。 +[NickTracker](../../src/main/java/pit12/runtime/nick/NickTracker.java) 通过 `isNicked(UUID)` 查询 Tab 中的玩家是否为 Nick。NPC 不是 Nick。 + +将 [NickListener](../../src/main/java/pit12/runtime/nick/NickListener.java) 保存为字段。在 `start()` 中调用 `nickTracker.addListener(listener)` 注册,再读取当前 Tab 状态,因为不会补发已有条目。状态变化时,`onNickChanged(playerId, name, nicked)` 在客户端线程执行。`name` 是档案名称。Nick 离开 Tab 时,`nicked` 为 `false`。在 `stop()` 中调用 `nickTracker.removeListener(listener)` 注销同一个监听器。 + [SidebarCache](../../src/main/java/pit12/runtime/scoreboard/SidebarCache.java) 通过 `lines()` 查询服务器侧栏。它返回按显示顺序排列的不可变快照,最多包含 15 行。内容保留底部文字和格式化代码,不包含目标标题。每个 [SidebarLine](../../src/main/java/pit12/runtime/scoreboard/SidebarLine.java) 通过 `entry()`、`text()` 和 `score()` 提供条目标识、文字和排序分数。 `formattedText(minecraft.fontRendererObj)` 会去掉格式化代码,以及 Minecraft 字体中宽度为 0 的字符。请在客户端线程调用。它在首次调用时计算结果,并为这一行缓存结果。分数变化时保留缓存。文本变化时创建新行,并清空缓存。 From cf443ca694986ba1c56d0404a462a80c1854adb9 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Thu, 15 Oct 2026 14:21:45 +0800 Subject: [PATCH 3/4] feat: add an option to hide spawn players in player list --- .../feature/playerlist/PlayerListBuilder.java | 17 +++++++++++------ .../feature/playerlist/PlayerListConfig.java | 7 +++++++ .../feature/playerlist/PlayerListFeature.java | 6 ++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java b/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java index e546c13..5053ea6 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListBuilder.java @@ -114,6 +114,13 @@ PlayerListSnapshot build(boolean realNames, boolean rebuildText) { if (!showSpecial && !showNick && !showEquipment) { continue; } + EntityPlayer player = players.loadedPlayer(playerId); + boolean spawn = (config.hideSpawn() || config.showSpawn()) && player != null + && pitSnapshot.spawnStateAt(player.posX, player.posY, + player.posZ) == SpawnState.IN_SPAWN; + if (config.hideSpawn() && spawn) { + continue; + } PlayerListEntry previous = previousEntries.get(playerId); String name = previous != null ? previous.name() : config.shortPlayerNames() ? playerNames.shortName(playerId) @@ -129,7 +136,6 @@ PlayerListSnapshot build(boolean realNames, boolean rebuildText) { name = name.trim() + " §e§lN"; } } - EntityPlayer player = players.loadedPlayer(playerId); boolean distanceKnown = localPlayer != null && player != null; boolean directionKnown = config.showDirection() && localPlayer != null && player != null; @@ -137,8 +143,6 @@ PlayerListSnapshot build(boolean realNames, boolean rebuildText) { float direction = directionKnown ? direction(localPlayer, player, forwardX, forwardZ, rightX, rightZ) : 0.0F; - boolean spawn = config.showSpawn() && player != null && pitSnapshot - .spawnStateAt(player.posX, player.posY, player.posZ) == SpawnState.IN_SPAWN; String leggingsText = previous != null ? previous.leggingsText() : config.showLeggings() && playerEquipment != null && playerEquipment.leggingsKnown() @@ -153,9 +157,10 @@ PlayerListSnapshot build(boolean realNames, boolean rebuildText) { playerEquipment.heldEnchantments(), config.enchantmentFormat(), config.showGem()) : null; - PlayerListEntry entry = new PlayerListEntry(playerId, - player == null ? 0 : player.getEntityId(), name, leggingsText, heldItemText, - distance, direction, distanceKnown, directionKnown, spawn); + PlayerListEntry entry = + new PlayerListEntry(playerId, player == null ? 0 : player.getEntityId(), name, + leggingsText, heldItemText, distance, direction, distanceKnown, + directionKnown, config.showSpawn() && spawn); previousEntries.put(playerId, entry); if (showSpecial) { add(groups, special, entry); diff --git a/src/main/java/pit12/feature/playerlist/PlayerListConfig.java b/src/main/java/pit12/feature/playerlist/PlayerListConfig.java index b99b458..aa8f5e6 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListConfig.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListConfig.java @@ -41,6 +41,7 @@ public final class PlayerListConfig extends FeatureConfig { private final BooleanSetting showDistance; private final BooleanSetting showDirection; private final BooleanSetting showSpawn; + private final BooleanSetting hideSpawn; private final BooleanSetting showGroupName; private final BooleanSetting showFriend; private final BooleanSetting showEnemy; @@ -77,6 +78,8 @@ public PlayerListConfig() { showSpawn = booleanSetting("show_spawn", source("Show spawn marker"), source("Shows SPAWN instead of distance and direction for players in spawn."), true); + hideSpawn = booleanSetting("hide_spawn", source("Hide players in spawn"), + source("Hides players in spawn from all groups."), false); realNamesKey = keybindSetting("real_names_key", source("Real names key"), source("Hold to show nicked players' real names in Player List."), Keyboard.KEY_TAB); @@ -137,6 +140,10 @@ public boolean showSpawn() { return showSpawn.get(); } + public boolean hideSpawn() { + return hideSpawn.get(); + } + public boolean showGroupName() { return showGroupName.get(); } diff --git a/src/main/java/pit12/feature/playerlist/PlayerListFeature.java b/src/main/java/pit12/feature/playerlist/PlayerListFeature.java index d68a67b..d8d27e3 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListFeature.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListFeature.java @@ -228,6 +228,7 @@ public void onConfigChanged(ConfigChangeSet changes) { || changes.affects("playerlist", "show_distance") || changes.affects("playerlist", "show_direction") || changes.affects("playerlist", "show_spawn") + || changes.affects("playerlist", "hide_spawn") || changes.affects("playerlist", "show_group_name") || changes.affects("playerlist", "show_friend") || changes.affects("playerlist", "show_enemy") @@ -261,8 +262,9 @@ public void onClientTick(ClientTickEvent event) { return; } ticksSinceUpdate = 0; - boolean movingColumns = config.showDistance() || config.showDirection(); - if (snapshotDirty || movingColumns) { + boolean updatePositions = + config.showDistance() || config.showDirection() || config.hideSpawn(); + if (snapshotDirty || updatePositions) { boolean rebuildText = snapshotDirty; snapshotDirty = false; snapshot = builder.build(realNamesHeld, rebuildText); From 1574465883a43145f38c8ca6a3a1e002df5bc37d Mon Sep 17 00:00:00 2001 From: ASKLL Date: Thu, 15 Oct 2026 15:09:57 +0800 Subject: [PATCH 4/4] chore: remove unneeded localization keys --- .../feature/chatmacro/ChatMacroCommand.java | 4 +- .../feature/chatmacro/MacroBindings.java | 28 +++++++------- .../pit12/feature/denick/DenickConfig.java | 2 +- .../pit12/feature/denick/DenickFeature.java | 8 ++-- .../pit12/feature/nametag/NametagConfig.java | 2 +- .../feature/playerlist/PlayerListGroup.java | 2 +- .../java/pit12/feature/swap/BindingBook.java | 6 +-- .../java/pit12/feature/swap/SwapBinding.java | 9 ++--- .../java/pit12/feature/swap/SwapCommand.java | 15 ++++---- .../java/pit12/feature/swap/SwapConfig.java | 2 +- .../pit12/feature/swap/SwapController.java | 37 +++++++++---------- .../pit12/feature/webui/SettingsTransfer.java | 36 ++++++++++-------- .../java/pit12/feature/webui/WebUiConfig.java | 2 +- .../java/pit12/feature/webui/WebUiServer.java | 25 +++++++------ .../pit12/runtime/config/FeatureConfig.java | 5 +-- web-ui/src/RelationsPage.vue | 2 +- web-ui/src/api.ts | 5 +-- web-ui/src/languages.ts | 2 +- 18 files changed, 94 insertions(+), 98 deletions(-) diff --git a/src/main/java/pit12/feature/chatmacro/ChatMacroCommand.java b/src/main/java/pit12/feature/chatmacro/ChatMacroCommand.java index 825b34f..ec2e718 100644 --- a/src/main/java/pit12/feature/chatmacro/ChatMacroCommand.java +++ b/src/main/java/pit12/feature/chatmacro/ChatMacroCommand.java @@ -63,7 +63,7 @@ private CommandNode.Builder operation(String name, String description, try { handler.execute(sender, args); } catch (IllegalArgumentException failure) { - reply(sender, Tone.ERROR, language.translate(failure.getMessage())); + reply(sender, Tone.ERROR, failure.getMessage()); } }); } @@ -94,7 +94,7 @@ private void remove(ICommandSender sender, String[] args) { private int key(String name) { int key = Keyboard.getKeyIndex(name.toUpperCase(Locale.ROOT)); if (key == Keyboard.KEY_NONE) - throw new IllegalArgumentException(language.format("Unknown keyboard key: {0}", name)); + throw new IllegalArgumentException("Unknown keyboard key: " + name); return key; } diff --git a/src/main/java/pit12/feature/chatmacro/MacroBindings.java b/src/main/java/pit12/feature/chatmacro/MacroBindings.java index 3822756..d812bc3 100644 --- a/src/main/java/pit12/feature/chatmacro/MacroBindings.java +++ b/src/main/java/pit12/feature/chatmacro/MacroBindings.java @@ -51,7 +51,7 @@ final class MacroBindings { private final Map bindings = new LinkedHashMap<>(); private ExecutorService worker; private boolean loading; - private String problem = source("Chat macros are unavailable"); + private boolean ready; MacroBindings(ClientThread client, Path path, Consumer report) { this.client = client; @@ -64,7 +64,7 @@ void start() { if (worker != null) return; loading = true; - problem = source("Chat macros are loading"); + ready = false; ExecutorService started = Executors.newSingleThreadExecutor(task -> { Thread thread = new Thread(task, "12pit-chat-macros"); thread.setDaemon(true); @@ -83,9 +83,8 @@ void start() { if (worker != started) return; loading = false; - problem = source( - "Chat macros could not be loaded. Use /macro clear to reset them"); - report.accept(problem); + report.accept(source( + "Chat macros could not be loaded. Use /macro clear to reset them")); }); return; } @@ -95,7 +94,7 @@ void start() { bindings.clear(); bindings.putAll(loaded); loading = false; - problem = null; + ready = true; }); }); } @@ -115,11 +114,11 @@ void stop() { Thread.currentThread().interrupt(); } loading = false; - problem = source("Chat macros are unavailable"); + ready = false; } String message(int key) { - return problem == null ? bindings.get(key) : null; + return ready ? bindings.get(key) : null; } Map entries() { @@ -144,16 +143,16 @@ boolean remove(int key) { void clear() { client.check(); if (worker == null || loading) - throw new IllegalArgumentException(problem); + throw new IllegalArgumentException("Chat macros are unavailable or still loading"); bindings.clear(); - problem = null; + ready = true; persist(); } void requireReady() { client.check(); - if (problem != null) - throw new IllegalArgumentException(problem); + if (!ready) + throw new IllegalArgumentException("Chat macros are not ready"); } private void persist() { @@ -196,12 +195,11 @@ private static Map decode(String text) { private static void checkMessage(String message) { if (message.trim().isEmpty() || message.length() > 100) - throw new IllegalArgumentException( - source("Chat messages must contain 1 to 100 characters")); + throw new IllegalArgumentException("Chat messages must contain 1 to 100 characters"); for (int index = 0; index < message.length(); index++) { if (!ChatAllowedCharacters.isAllowedCharacter(message.charAt(index))) throw new IllegalArgumentException( - source("The chat message contains an invalid character")); + "The chat message contains an invalid character"); } } } diff --git a/src/main/java/pit12/feature/denick/DenickConfig.java b/src/main/java/pit12/feature/denick/DenickConfig.java index 234d662..5e3f547 100644 --- a/src/main/java/pit12/feature/denick/DenickConfig.java +++ b/src/main/java/pit12/feature/denick/DenickConfig.java @@ -31,7 +31,7 @@ public final class DenickConfig extends FeatureConfig { final BooleanSetting nickMarker; public DenickConfig() { - super("denick", source("Denick"), new ConfigCategory("misc", source("Misc")), + super("denick", "Denick", new ConfigCategory("misc", source("Misc")), source("Finds nicked players using their current equipment.")); subsubcategory("queries", source("Queries")); automatic = booleanSetting("automatic", source("Auto Denick"), diff --git a/src/main/java/pit12/feature/denick/DenickFeature.java b/src/main/java/pit12/feature/denick/DenickFeature.java index 1bb7363..a5efdf9 100644 --- a/src/main/java/pit12/feature/denick/DenickFeature.java +++ b/src/main/java/pit12/feature/denick/DenickFeature.java @@ -527,9 +527,11 @@ private void complete(Target target, Search search, List items, Stri if (search.force || !Objects.equals(previousName, result.displayName) || !Objects.equals(previousOwner, result.owner)) { boolean corrected = cached != null && !Objects.equals(cached, result.owner); - reply(search.sender, Tone.SUCCESS, language.format( - corrected ? source("{0} §c-> {1} §c(updated)") : source("{0} §c-> {1}"), - display, result.displayName)); + reply(search.sender, Tone.SUCCESS, + corrected + ? language.format("{0} §c-> {1} §c(updated)", display, + result.displayName) + : display + " §c-> " + result.displayName); changed(); } if (!Objects.equals(mappings.put(target.name.toLowerCase(Locale.ROOT), result.owner), diff --git a/src/main/java/pit12/feature/nametag/NametagConfig.java b/src/main/java/pit12/feature/nametag/NametagConfig.java index 7899924..9614a9d 100644 --- a/src/main/java/pit12/feature/nametag/NametagConfig.java +++ b/src/main/java/pit12/feature/nametag/NametagConfig.java @@ -40,7 +40,7 @@ public final class NametagConfig extends FeatureConfig { public NametagConfig() { super("nametag", source("Nametag"), new ConfigCategory("render", source("Render")), source("Adds real names and Pit enchantments to player nametags.")); - subsubcategory("names", source("Denick")); + subsubcategory("names", "Denick"); nickMarker = booleanSetting("nick_marker", source("Show nick marker"), source("Adds N to nicked players' names."), true); realNames = booleanSetting("real_names", source("Show real names on nametags"), diff --git a/src/main/java/pit12/feature/playerlist/PlayerListGroup.java b/src/main/java/pit12/feature/playerlist/PlayerListGroup.java index 2b1bac3..6982ce5 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListGroup.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListGroup.java @@ -23,7 +23,7 @@ public enum PlayerListGroup { FRIEND(source("§a§lFriend")), ENEMY(source("§c§lEnemy")), - NICK(source("§e§lNick")), + NICK("Nick"), REGULARITY("§4§lREG"), DARK("§d§lDARK"), BOUNTY_HUNTER("§6§lBounty Hunter"); diff --git a/src/main/java/pit12/feature/swap/BindingBook.java b/src/main/java/pit12/feature/swap/BindingBook.java index 747d791..7f7aef7 100644 --- a/src/main/java/pit12/feature/swap/BindingBook.java +++ b/src/main/java/pit12/feature/swap/BindingBook.java @@ -168,17 +168,17 @@ int unbind(int key, ItemIdentity identity) { void clear() { client.check(); if (worker == null || loading) { - throw new IllegalArgumentException(problem); + throw new IllegalArgumentException("Swap bindings are unavailable or still loading"); } bindings.clear(); problem = null; persist(); } - private void requireReady() { + void requireReady() { client.check(); if (problem != null) - throw new IllegalArgumentException(problem); + throw new IllegalArgumentException("Swap bindings are not ready"); } private void persist() { diff --git a/src/main/java/pit12/feature/swap/SwapBinding.java b/src/main/java/pit12/feature/swap/SwapBinding.java index bd4bfee..c09a65e 100644 --- a/src/main/java/pit12/feature/swap/SwapBinding.java +++ b/src/main/java/pit12/feature/swap/SwapBinding.java @@ -18,8 +18,6 @@ */ package pit12.feature.swap; -import static pit12.runtime.languages.Languages.source; - import net.minecraft.item.ItemArmor; import net.minecraft.item.ItemStack; import pit12.runtime.item.PitEnchantmentReader; @@ -47,17 +45,16 @@ final class SwapBinding { static SwapBinding create(int key, ItemStack stack, int hotbarTarget) { if (stack == null) - throw new IllegalArgumentException(source("Hold an item to bind")); + throw new IllegalArgumentException("Hold an item to bind"); int armor = armorTarget(stack); if (hotbarTarget == 0 && armor == 0) { - throw new IllegalArgumentException( - source("Hold armor or specify a hotbar slot from 1 to 9")); + throw new IllegalArgumentException("Hold armor or specify a hotbar slot from 1 to 9"); } PitEnchantments enchantments = PitEnchantmentReader.read(stack); String details = enchantments.formatDisplayNames(false); ItemIdentity identity = ItemIdentity.read(stack); if (identity == null) - throw new IllegalArgumentException(source("Held item could not be identified")); + throw new IllegalArgumentException("Held item could not be identified"); String name = stack.getDisplayName(); return new SwapBinding(key, identity, hotbarTarget == 0, hotbarTarget == 0 ? armor : hotbarTarget, name.isEmpty() ? identity.item : name, diff --git a/src/main/java/pit12/feature/swap/SwapCommand.java b/src/main/java/pit12/feature/swap/SwapCommand.java index d608d01..06fd528 100644 --- a/src/main/java/pit12/feature/swap/SwapCommand.java +++ b/src/main/java/pit12/feature/swap/SwapCommand.java @@ -75,7 +75,7 @@ private CommandNode.Builder operation(String name, String description, handler.execute(sender, args); } catch (IllegalArgumentException failure) { // Bindings also use this exception when input is bad or they aren't ready yet. - reply(sender, Tone.ERROR, language.translate(failure.getMessage())); + reply(sender, Tone.ERROR, failure.getMessage()); } }); } @@ -87,10 +87,10 @@ private void bind(ICommandSender sender, String[] args) { try { target = Integer.parseInt(args[1]); } catch (NumberFormatException failure) { - throw new IllegalArgumentException(source("Hotbar slot must be from 1 to 9")); + throw new IllegalArgumentException("Hotbar slot must be from 1 to 9"); } if (target < 1 || target > 9) { - throw new IllegalArgumentException(source("Hotbar slot must be from 1 to 9")); + throw new IllegalArgumentException("Hotbar slot must be from 1 to 9"); } } SwapBinding binding = @@ -108,7 +108,7 @@ private void unbind(ICommandSender sender, String[] args) { requirePlayer(); held = ItemIdentity.read(minecraft.thePlayer.getHeldItem()); if (held == null) - throw new IllegalArgumentException(source("Hold an item to unbind")); + throw new IllegalArgumentException("Hold an item to unbind"); } int removed = bindings.unbind(args.length == 1 ? key(args[0]) : 0, held); if (removed == 0 || config.bindingMessages.get()) @@ -118,8 +118,7 @@ private void unbind(ICommandSender sender, String[] args) { } private void list(ICommandSender sender, String[] args) { - if (bindings.readinessProblem() != null) - throw new IllegalArgumentException(bindings.readinessProblem()); + bindings.requireReady(); reply(sender, Tone.INFO, language.format("Swap bindings: {0}", bindings.count())); for (SwapBinding entry : bindings.entries()) reply(sender, Tone.INFO, Keyboard.getKeyName(entry.key) + ": " @@ -145,7 +144,7 @@ private void status(ICommandSender sender, String[] args) { private void requirePlayer() { if (minecraft.thePlayer == null) - throw new IllegalArgumentException(source("Join a world first")); + throw new IllegalArgumentException("Join a world first"); } private String state(PitEnchantment type, boolean enabled) { @@ -161,7 +160,7 @@ private String state(PitEnchantment type, boolean enabled) { private int key(String name) { int code = Keyboard.getKeyIndex(name.toUpperCase(Locale.ROOT)); if (code == Keyboard.KEY_NONE) - throw new IllegalArgumentException(language.format("Unknown keyboard key: {0}", name)); + throw new IllegalArgumentException("Unknown keyboard key: " + name); return code; } diff --git a/src/main/java/pit12/feature/swap/SwapConfig.java b/src/main/java/pit12/feature/swap/SwapConfig.java index 189599d..d740720 100644 --- a/src/main/java/pit12/feature/swap/SwapConfig.java +++ b/src/main/java/pit12/feature/swap/SwapConfig.java @@ -161,7 +161,7 @@ public SwapConfig() { source("Restore threshold"), source("Health points needed to restore leggings in Health mode. Health must also exceed the trigger threshold."), 6, 0, 20, 1); - subsubcategory("dark", source("Dark")); + subsubcategory("dark", "Dark"); dark = booleanSetting("use_dark", source("Use dark"), source( "Equips Somber leggings against players with low health wearing Escape Pod or Phoenix."), false); diff --git a/src/main/java/pit12/feature/swap/SwapController.java b/src/main/java/pit12/feature/swap/SwapController.java index ce3b85d..e3b4760 100644 --- a/src/main/java/pit12/feature/swap/SwapController.java +++ b/src/main/java/pit12/feature/swap/SwapController.java @@ -76,7 +76,7 @@ final class SwapController { private boolean failed; private boolean sendingClick; private boolean waitingToResume; - private String rejection; + private boolean clickRejected; SwapController(Minecraft minecraft, ClientSession session, SwapConfig config, BindingBook bindings, BiConsumer report, InputBlocker input, @@ -185,8 +185,8 @@ boolean tick(int phase) { } private void advance(int phase) { - if (rejection != null) - throw new IllegalStateException(rejection); + if (clickRejected) + throw new IllegalStateException("Server rejected the inventory click"); if (waitingToResume) { if (tick >= resumeAt) finish(false); @@ -197,7 +197,7 @@ private void advance(int phase) { return; } if (!failed && (!acceptsInput() || screen != null && minecraft.currentScreen != screen)) { - throw new IllegalStateException(language.translate("Inventory changed")); + throw new IllegalStateException("Inventory changed"); } if (cancelling) { finishCancellation(phase); @@ -238,7 +238,7 @@ private void advance(int phase) { if (options == null || cancelling) return; if (!owns(minecraft.currentScreen) || !acceptsInput()) { - throw new IllegalStateException(language.translate("Inventory did not open")); + throw new IllegalStateException("Inventory did not open"); } lastClickTick = tick; } @@ -283,7 +283,7 @@ void confirmClick(int windowId, short actionNumber, boolean accepted) { || !pendingClicks.remove(actionNumber)) return; if (!accepted) - rejection = language.translate("Server rejected the inventory click"); + clickRejected = true; } private void prepare() { @@ -394,7 +394,7 @@ private int firstEmptyInventorySlot() { private void click(int slot, int button, int mode) { if (!owns(minecraft.currentScreen) || player.openContainer != player.inventoryContainer || player.inventory.getItemStack() != null) { - throw new IllegalStateException(language.translate("Inventory changed")); + throw new IllegalStateException("Inventory changed"); } sendingClick = true; try { @@ -476,7 +476,7 @@ private void fail(String message) { failed = true; restoreOnCancel = false; pendingClicks.clear(); - rejection = null; + clickRejected = false; waitingToResume = false; closeAt = tick + options.closeDelay; report.accept(Tone.ERROR, language.format("Swap failed: {0}", message)); @@ -496,8 +496,7 @@ private void finishCancellation(int phase) { click(action.shiftedArmorSlot, 0, 1); if (!ItemStack.areItemStacksEqual(stack(action.binding.guiTarget()), action.originalArmor)) { - throw new IllegalStateException( - language.translate("Armor could not be equipped")); + throw new IllegalStateException("Armor could not be equipped"); } } else action.swap(action.source, action.work); @@ -576,7 +575,7 @@ private void reset() { failed = false; sendingClick = false; waitingToResume = false; - rejection = null; + clickRejected = false; if (active) input.unlock(inputOwner, resume); } @@ -626,7 +625,7 @@ void step() { if (stage == ActionStage.EQUIP_WITH_SHIFT_CLICK) { if (stack(binding.guiTarget()) != null || !binding.identity.matches(stack(source))) { - throw new IllegalStateException(language.translate("Inventory changed")); + throw new IllegalStateException("Inventory changed"); } click(source, 0, 1); verifyAndComplete(); @@ -635,12 +634,11 @@ void step() { if (stage == ActionStage.EQUIP_FROM_HOTBAR) { if (!binding.identity.matches(stack(36 + work)) || !ItemStack.areItemStacksEqual(stack(source), originalWorkspace)) { - throw new IllegalStateException(language.translate("Transfer slot changed")); + throw new IllegalStateException("Transfer slot changed"); } swap(binding.guiTarget(), work); if (!binding.identity.matches(stack(binding.guiTarget()))) { - throw new IllegalStateException( - language.translate("Armor could not be equipped")); + throw new IllegalStateException("Armor could not be equipped"); } expectedWorkspace = copy(stack(36 + work)); stage = ActionStage.RESTORE_HOTBAR; @@ -650,7 +648,7 @@ void step() { } if (stage == ActionStage.RESTORE_HOTBAR) { if (!restorable()) - throw new IllegalStateException(language.translate("Transfer slot changed")); + throw new IllegalStateException("Transfer slot changed"); swap(source, work); complete(); return; @@ -683,8 +681,8 @@ void step() { click(binding.guiTarget(), 0, 1); if (stack(binding.guiTarget()) != null || !ItemStack .areItemStacksEqual(stack(shiftedArmorSlot), originalArmor)) { - throw new IllegalStateException(language - .translate("Armor could not be moved to the inventory")); + throw new IllegalStateException( + "Armor could not be moved to the inventory"); } stage = ActionStage.EQUIP_WITH_SHIFT_CLICK; return; @@ -732,8 +730,7 @@ private boolean restorable() { private void verifyAndComplete() { if (!binding.identity.matches(stack(binding.guiTarget()))) { - throw new IllegalStateException( - language.format("Item could not be swapped: {0}", binding.name)); + throw new IllegalStateException("Item could not be swapped: " + binding.name); } complete(); } diff --git a/src/main/java/pit12/feature/webui/SettingsTransfer.java b/src/main/java/pit12/feature/webui/SettingsTransfer.java index 0f7f8fb..bfb9c8b 100644 --- a/src/main/java/pit12/feature/webui/SettingsTransfer.java +++ b/src/main/java/pit12/feature/webui/SettingsTransfer.java @@ -43,6 +43,7 @@ import pit12.feature.profile.api.Profiles; import pit12.feature.relation.api.Relation; import pit12.feature.relation.api.RelationEntry; +import pit12.feature.relation.api.RelationReadiness; import pit12.feature.relation.api.Relations; import pit12.feature.swap.api.SwapBindings; import pit12.shared.result.OperationResult; @@ -70,7 +71,8 @@ JsonObject exportData(JsonObject request) { data.addProperty("schemaVersion", VERSION); JsonArray exported = new JsonArray(); if (!selected.isEmpty()) { - for (String text : requireSuccess(profiles.exportProfiles(selected))) { + for (String text : requireSuccess(profiles.exportProfiles(selected), + "export profiles")) { exported.add(new JsonParser().parse(text)); } } @@ -82,8 +84,8 @@ JsonObject exportData(JsonObject request) { } data.add("relations", groups); if (includeSwap) - data.add("swapBindings", - new JsonParser().parse(requireSuccess(swapBindings.exportBindings()))); + data.add("swapBindings", new JsonParser() + .parse(requireSuccess(swapBindings.exportBindings(), "export swap bindings"))); return data; } @@ -146,24 +148,25 @@ void apply(JsonObject request) { JsonObject decisions = object(request, "resolutions"); List resolved = resolve(bundle, selectedTypes, mode, decisions); if (!chosen.isEmpty()) { - requireSuccess(profiles.importProfiles(chosen)); + requireSuccess(profiles.importProfiles(chosen), "import profiles"); } if (!selectedTypes.isEmpty()) { - requireSuccess(relations.replaceAll(resolved)); + requireSuccess(relations.replaceAll(resolved), "replace relations"); } if (includeSwap) - requireSuccess(swapBindings.replaceBindings(bundle.swapBindings)); + requireSuccess(swapBindings.replaceBindings(bundle.swapBindings), + "import swap bindings"); } - private static T requireSuccess(OperationResult result) { + private static T requireSuccess(OperationResult result, String operation) { if (!result.succeeded()) - throw new IllegalArgumentException(result.message()); + throw new IllegalArgumentException("Could not " + operation + ": " + result.status()); return result.value(); } - private static void requireSuccess(ProfileMutationResult result) { + private static void requireSuccess(ProfileMutationResult result, String operation) { if (!result.succeeded()) - throw new IllegalArgumentException(result.message()); + throw new IllegalArgumentException("Could not " + operation + ": " + result.status()); } private Bundle parseData(JsonObject data) { @@ -180,7 +183,8 @@ private Bundle parseData(JsonObject data) { savedProfiles.add(gson.toJson(element)); } if (!savedProfiles.isEmpty()) { - requireSuccess(profiles.validateImportProfiles(savedProfiles)); + requireSuccess(profiles.validateImportProfiles(savedProfiles), + "validate profile import"); } EnumMap> groups = new EnumMap>(Relation.class); @@ -228,7 +232,7 @@ private Bundle parseData(JsonObject data) { JsonElement swapData = data.get("swapBindings"); String savedSwap = swapData == null ? null : gson.toJson(swapData); if (savedSwap != null) - requireSuccess(swapBindings.validateImport(savedSwap)); + requireSuccess(swapBindings.validateImport(savedSwap), "validate swap binding import"); if (savedProfiles.isEmpty() && groups.isEmpty() && savedSwap == null) { throw new IllegalArgumentException("File is empty"); } @@ -327,8 +331,8 @@ private String fingerprint() { for (ProfileSummary profile : profiles.snapshot().profiles()) { all.add(profile.id()); } - StringBuilder state = - new StringBuilder(gson.toJson(requireSuccess(profiles.exportProfiles(all)))); + StringBuilder state = new StringBuilder( + gson.toJson(requireSuccess(profiles.exportProfiles(all), "export profiles"))); state.append(profiles.snapshot().activeProfileId()); state.append("swapBindings:").append(swapBindings.revision()); for (Relation type : TYPES) { @@ -348,8 +352,8 @@ private String fingerprint() { } private void requireRelations() { - if (relations.readinessProblem() != null) { - throw new IllegalArgumentException(relations.readinessProblem()); + if (relations.readiness() != RelationReadiness.READY) { + throw new IllegalArgumentException("Relations are not ready"); } } diff --git a/src/main/java/pit12/feature/webui/WebUiConfig.java b/src/main/java/pit12/feature/webui/WebUiConfig.java index f9a85a2..94ffcde 100644 --- a/src/main/java/pit12/feature/webui/WebUiConfig.java +++ b/src/main/java/pit12/feature/webui/WebUiConfig.java @@ -52,7 +52,7 @@ public WebUiConfig(Languages languages) { 0x7BADE2); booleanSetting("show_details", source("Show details"), source("Shows descriptions for features and settings."), false); - discordRpc = booleanSetting("discord_rpc", source("Discord RPC"), + discordRpc = booleanSetting("discord_rpc", "Discord RPC", source("Shows 12pit and elapsed time on your Discord profile."), true); keybind = keybindSetting("keybind", source("Interface key"), source("Opens the HUD editor. Press again with no HUD selected to open settings."), diff --git a/src/main/java/pit12/feature/webui/WebUiServer.java b/src/main/java/pit12/feature/webui/WebUiServer.java index 181caaa..bf4916a 100644 --- a/src/main/java/pit12/feature/webui/WebUiServer.java +++ b/src/main/java/pit12/feature/webui/WebUiServer.java @@ -18,8 +18,6 @@ */ package pit12.feature.webui; -import static pit12.runtime.languages.Languages.source; - import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -62,6 +60,7 @@ import pit12.feature.profile.api.ProfilesSnapshot; import pit12.feature.relation.api.Relation; import pit12.feature.relation.api.RelationEntry; +import pit12.feature.relation.api.RelationReadiness; import pit12.feature.relation.api.Relations; import pit12.feature.swap.api.SwapBindings; import pit12.runtime.config.ChoiceSetting; @@ -175,11 +174,11 @@ private void handle(HttpExchange exchange) throws IOException { } catch (JsonParseException failure) { sendJson(exchange, 400, object("error", "Invalid JSON")); } catch (IllegalArgumentException failure) { - sendJson(exchange, 400, object("error", Languages.sourceText(failure.getMessage()))); + sendJson(exchange, 400, object("error", failure.getMessage())); } catch (ExecutionException failure) { Throwable cause = failure.getCause(); if (cause instanceof IllegalArgumentException) { - sendJson(exchange, 400, object("error", Languages.sourceText(cause.getMessage()))); + sendJson(exchange, 400, object("error", cause.getMessage())); } else { sendJson(exchange, 500, object("error", "Settings operation failed")); } @@ -252,7 +251,7 @@ private void handleApi(HttpExchange exchange, String path) if ("/api/language-hint/dismiss".equals(path)) { if (!preferences.ready()) throw new IllegalArgumentException( - source("Configuration interface settings are unavailable")); + "Configuration interface settings are unavailable"); config.languageHint().set(false); return state(); } @@ -266,7 +265,7 @@ private void handleApi(HttpExchange exchange, String path) throw new IllegalArgumentException("Expected a boolean"); if (!preferences.ready()) throw new IllegalArgumentException( - source("Configuration interface settings are unavailable")); + "Configuration interface settings are unavailable"); preferences.setFavorite(id, favorite.getAsBoolean()); } else if ("/api/setting".equals(path)) { changeSetting(request); @@ -378,7 +377,8 @@ private void changeProfile(JsonObject request) { if ("create".equals(action)) { result = profiles.beginCreate(); if (!result.succeeded()) { - throw new IllegalArgumentException(result.message()); + throw new IllegalArgumentException( + "Could not begin profile creation: " + result.status()); } ProfileCreateSession session = result.createSession(); try { @@ -408,14 +408,14 @@ private void changeProfile(JsonObject request) { } } if (!result.succeeded()) { - throw new IllegalArgumentException(result.message()); + throw new IllegalArgumentException( + "Could not " + action + " profile: " + result.status()); } } private List changeRelations(JsonObject request) { - String problem = relations.readinessProblem(); - if (problem != null) { - throw new IllegalArgumentException(problem); + if (relations.readiness() != RelationReadiness.READY) { + throw new IllegalArgumentException("Relations are not ready"); } Relation relation = Relation.valueOf(requiredString(request, "relation")); if (relation == Relation.NONE) { @@ -475,7 +475,8 @@ private List changeRelations(JsonObject request) { OperationResult>> batch = relations.changeMany(relation, action, changes); if (!batch.succeeded()) - throw new IllegalArgumentException(batch.message()); + throw new IllegalArgumentException( + "Could not " + action + " relations: " + batch.status()); List> messages = batch.value(); for (int index = 0; index < messages.size(); index++) { results.set(positions.get(index), object("name", changes.get(index).name(), "message", diff --git a/src/main/java/pit12/runtime/config/FeatureConfig.java b/src/main/java/pit12/runtime/config/FeatureConfig.java index 0b3d587..2a550db 100644 --- a/src/main/java/pit12/runtime/config/FeatureConfig.java +++ b/src/main/java/pit12/runtime/config/FeatureConfig.java @@ -151,7 +151,7 @@ protected final void subsubcategory(String id, String displayName) { protected final HudConfig hudConfig(String id, HudAnchor defaultAnchor, int defaultOffsetX, int defaultOffsetY, boolean defaultTextShadow) { return hudConfig(id, defaultAnchor, defaultOffsetX, defaultOffsetY, defaultTextShadow, - HudGroup.AUTO, source("HUD")); + HudGroup.AUTO, "HUD"); } protected final HudConfig hudConfig(String id, HudAnchor defaultAnchor, int defaultOffsetX, @@ -189,8 +189,7 @@ protected final HudConfig hudConfig(String id, HudAnchor defaultAnchor, int defa register(translateText, ConfigOption.Kind.BOOLEAN); register(anchor, ConfigOption.Kind.CHOICE); int lastOption = options.size(); - ConfigGroup group = - new ConfigGroup(id + ".hud", groupName == null ? source("HUD") : groupName); + ConfigGroup group = new ConfigGroup(id + ".hud", groupName == null ? "HUD" : groupName); ConfigGroup parent = currentSubcategory; hudGroups.add(() -> { boolean topLevel = grouping == HudGroup.SUBCATEGORY diff --git a/web-ui/src/RelationsPage.vue b/web-ui/src/RelationsPage.vue index dd1e748..6edb238 100644 --- a/web-ui/src/RelationsPage.vue +++ b/web-ui/src/RelationsPage.vue @@ -431,7 +431,7 @@ async function removeEntry(entry: RelationEntry) { @change="toggleVisible" /> {{ t('MC ID') }} - {{ t('UUID') }} + UUID
. */ -import { t, type SourceText } from './languages' +import type { SourceText } from './languages' export interface Option { id: string @@ -123,8 +123,7 @@ async function request(path: string, body?: object): Promise { cache: 'no-store', }) const payload = await response.json() - if (!response.ok) - throw new Error(payload.error ? t(payload.error) : t('Request failed')) + if (!response.ok) throw new Error(payload.error || 'Request failed') return payload as T } diff --git a/web-ui/src/languages.ts b/web-ui/src/languages.ts index 22f4078..22b435d 100644 --- a/web-ui/src/languages.ts +++ b/web-ui/src/languages.ts @@ -32,7 +32,7 @@ export async function loadLanguage(language: string) { cache: 'no-store', }) .then(async (response) => { - if (!response.ok) throw new Error(t('Could not load language')) + if (!response.ok) throw new Error('Could not load language') return (await response.json()) as Catalog }) .catch((cause) => {