diff --git a/src/main/java/me/justindevb/replay/Replay.java b/src/main/java/me/justindevb/replay/Replay.java index c3fdb15..d56a4a7 100644 --- a/src/main/java/me/justindevb/replay/Replay.java +++ b/src/main/java/me/justindevb/replay/Replay.java @@ -19,6 +19,7 @@ import me.justindevb.replay.config.ReplayConfigManager; import me.justindevb.replay.config.ReplayConfigReloadResult; import me.justindevb.replay.config.ReplayConfigSetting; +import me.justindevb.replay.config.ReplayMessagesConfig; import me.justindevb.replay.debug.ReplayDebugCommand; import me.justindevb.replay.export.ReplayExportCommand; import me.justindevb.replay.metrics.BStatsCharts; @@ -57,6 +58,7 @@ public class Replay extends JavaPlugin { private ReplayRetentionService replayRetentionService; private ReplayViewerStateManager replayViewerStateManager; private ReplayTransferManager transferManager; + private ReplayMessagesConfig messages; @Override public void onLoad() { @@ -77,6 +79,7 @@ public void onEnable() { recorderManager = new RecorderManager(this); manager = new ReplayManagerImpl(this, recorderManager); initConfig(); + messages = new ReplayMessagesConfig(this); replayViewerStateManager = new ReplayViewerStateManager(this); getServer().getPluginManager().registerEvents(replayViewerStateManager, this); replayBenchmarkService = createReplayBenchmarkService(); @@ -142,6 +145,10 @@ public ReplayStorage getReplayStorage() { return storage; } + public ReplayMessagesConfig getMessages() { + return messages; + } + private void initConfig() { new ReplayConfigManager(this).initialize(); } @@ -199,6 +206,7 @@ public ReplayConfigReloadResult reloadRuntimeConfig() { EnumMap previousValues = snapshotConfigValues(previousConfig); new ReplayConfigManager(this).initialize(); + if (messages != null) messages.reload(); EnumMap currentValues = snapshotConfigValues(getConfig()); List changedSettings = new ArrayList<>(); @@ -283,4 +291,4 @@ private void initVelocityLogic() { getServer().getMessenger().registerOutgoingPluginChannel(this, ReplayTransferManager.CHANNEL); getServer().getMessenger().registerIncomingPluginChannel(this, ReplayTransferManager.CHANNEL, new ReplayLaunchMessageListener(this)); } -} \ No newline at end of file +} diff --git a/src/main/java/me/justindevb/replay/ReplaySession.java b/src/main/java/me/justindevb/replay/ReplaySession.java index c16e8d9..3c81492 100644 --- a/src/main/java/me/justindevb/replay/ReplaySession.java +++ b/src/main/java/me/justindevb/replay/ReplaySession.java @@ -109,7 +109,11 @@ public ReplaySession(ReplayPlaybackData replayData, Player viewer, Replay replay public void start() { if (timeline == null || timeline.isEmpty()) { - viewer.sendMessage("Replay is empty!"); + if (replay.getMessages() != null) { + viewer.sendMessage(replay.getMessages().component("replay.empty", "Replay is empty!")); + } else { + viewer.sendMessage("Replay is empty!"); + } return; } @@ -299,7 +303,11 @@ public void stop() { } if (!suppressStopMessage && viewer.isOnline()) { - viewer.sendMessage("Replay finished"); + if (replay.getMessages() != null) { + viewer.sendMessage(replay.getMessages().component("replay.finished", "Replay finished")); + } else { + viewer.sendMessage("Replay finished"); + } } } finally { ReplayRegistry.remove(this); @@ -633,11 +641,17 @@ private void sendActionBar() { Component bar; if (paused) { - bar = Component.text("\u23F8 Replay paused: ", NamedTextColor.YELLOW) + bar = replay.getMessages() != null + ? replay.getMessages().component("action-bar.paused", "\u23F8 Replay paused: %current% / %total%", + "current", current, "total", total) + : Component.text("\u23F8 Replay paused: ", NamedTextColor.YELLOW) .append(Component.text(current + " / " + total, NamedTextColor.GRAY)); } else { String speedText = String.format("%.1fx", playbackSpeed); - bar = Component.text("\u25B6 Replay: ", NamedTextColor.GREEN) + bar = replay.getMessages() != null + ? replay.getMessages().component("action-bar.playing", "\u25B6 Replay: %current% / %total% (%percent%%) [%speed%]", + "current", current, "total", total, "percent", String.valueOf(percent), "speed", speedText) + : Component.text("\u25B6 Replay: ", NamedTextColor.GREEN) .append(Component.text(current + " / " + total, NamedTextColor.GRAY)) .append(Component.text(" (" + percent + "%)", NamedTextColor.DARK_GRAY)) .append(Component.text(" [" + speedText + "]", NamedTextColor.AQUA)); diff --git a/src/main/java/me/justindevb/replay/config/ReplayMessagesConfig.java b/src/main/java/me/justindevb/replay/config/ReplayMessagesConfig.java new file mode 100644 index 0000000..e81c0c6 --- /dev/null +++ b/src/main/java/me/justindevb/replay/config/ReplayMessagesConfig.java @@ -0,0 +1,66 @@ +package me.justindevb.replay.config; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +public final class ReplayMessagesConfig { + + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private final JavaPlugin plugin; + private YamlConfiguration messages; + + public ReplayMessagesConfig(JavaPlugin plugin) { + this.plugin = plugin; + reload(); + } + + public void reload() { + File file = new File(plugin.getDataFolder(), "messages.yml"); + if (!file.exists()) plugin.saveResource("messages.yml", false); + + YamlConfiguration loaded = YamlConfiguration.loadConfiguration(file); + YamlConfiguration defaults = loadDefaults(); + boolean changed = false; + for (String key : defaults.getKeys(true)) { + if (defaults.isConfigurationSection(key) || loaded.contains(key)) continue; + loaded.set(key, defaults.get(key)); + changed = true; + } + + if (changed) { + try { + loaded.save(file); + } catch (IOException exception) { + plugin.getLogger().warning("Could not update missing messages.yml keys: " + exception.getMessage()); + } + } + messages = loaded; + } + + private YamlConfiguration loadDefaults() { + try (InputStream resource = plugin.getResource("messages.yml")) { + if (resource == null) return new YamlConfiguration(); + return YamlConfiguration.loadConfiguration(new InputStreamReader(resource, StandardCharsets.UTF_8)); + } catch (IOException exception) { + plugin.getLogger().warning("Could not load bundled messages.yml defaults: " + exception.getMessage()); + return new YamlConfiguration(); + } + } + + public Component component(String key, String fallback, String... replacements) { + String value = messages.getString(key, fallback); + for (int index = 0; index + 1 < replacements.length; index += 2) { + value = value.replace("%" + replacements[index] + "%", replacements[index + 1]); + } + return MINI_MESSAGE.deserialize(value); + } +} diff --git a/src/main/java/me/justindevb/replay/debug/ReplayDebugCommand.java b/src/main/java/me/justindevb/replay/debug/ReplayDebugCommand.java index 1750eda..9a42b41 100644 --- a/src/main/java/me/justindevb/replay/debug/ReplayDebugCommand.java +++ b/src/main/java/me/justindevb/replay/debug/ReplayDebugCommand.java @@ -10,6 +10,7 @@ import java.io.File; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -32,7 +33,7 @@ public final class ReplayDebugCommand { private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z") .withLocale(Locale.ROOT) .withZone(ZoneId.systemDefault()); - private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.00"); + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.ROOT)); private final Replay replay; private final ReplayManager replayManager; diff --git a/src/main/java/me/justindevb/replay/playback/ReplayInventoryUI.java b/src/main/java/me/justindevb/replay/playback/ReplayInventoryUI.java index b381a59..da9c052 100644 --- a/src/main/java/me/justindevb/replay/playback/ReplayInventoryUI.java +++ b/src/main/java/me/justindevb/replay/playback/ReplayInventoryUI.java @@ -1,11 +1,13 @@ package me.justindevb.replay.playback; import me.justindevb.replay.Replay; +import me.justindevb.replay.config.ReplayMessagesConfig; import me.justindevb.replay.entity.RecordedEntity; import me.justindevb.replay.entity.RecordedPlayer; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -20,6 +22,8 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataType; import java.util.List; import java.util.Map; @@ -32,6 +36,8 @@ */ public class ReplayInventoryUI implements Listener { + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + /** * Callback interface for actions that must be delegated back to ReplaySession. */ @@ -46,6 +52,7 @@ public interface SessionControl { private final Player viewer; private final Replay replay; + private final NamespacedKey controlKey; private final Supplier> recordedEntitiesSupplier; private final SessionControl sessionControl; @@ -58,6 +65,7 @@ public ReplayInventoryUI(Replay replay, Supplier> recordedEntitiesSupplier, SessionControl sessionControl) { this.replay = replay; + this.controlKey = new NamespacedKey("betterreplay", "replay-control"); this.viewer = viewer; this.recordedEntitiesSupplier = recordedEntitiesSupplier; this.sessionControl = sessionControl; @@ -91,28 +99,33 @@ public void restoreInventory() { public void giveReplayControls() { ItemStack pauseButton = new ItemStack(Material.RED_DYE); ItemMeta pauseMeta = pauseButton.getItemMeta(); - pauseMeta.displayName(Component.text("Pause / Play", NamedTextColor.RED)); + pauseMeta.displayName(message("items.pause-play", "Pause / Play")); pauseButton.setItemMeta(pauseMeta); + markControl(pauseButton, "pause-play"); ItemStack skipForward = new ItemStack(Material.LIME_DYE); ItemMeta forwardMeta = skipForward.getItemMeta(); - forwardMeta.displayName(Component.text("+5 seconds", NamedTextColor.GREEN)); + forwardMeta.displayName(message("items.skip-forward", "+5 seconds")); skipForward.setItemMeta(forwardMeta); + markControl(skipForward, "skip-forward"); ItemStack skipBackward = new ItemStack(Material.YELLOW_DYE); ItemMeta backwardMeta = skipBackward.getItemMeta(); - backwardMeta.displayName(Component.text("-5 seconds", NamedTextColor.YELLOW)); + backwardMeta.displayName(message("items.skip-backward", "-5 seconds")); skipBackward.setItemMeta(backwardMeta); + markControl(skipBackward, "skip-backward"); ItemStack stopReplay = new ItemStack(Material.BARRIER); ItemMeta stopMeta = stopReplay.getItemMeta(); - stopMeta.displayName(Component.text("Exit Replay", NamedTextColor.DARK_RED)); + stopMeta.displayName(message("items.exit", "Exit Replay")); stopReplay.setItemMeta(stopMeta); + markControl(stopReplay, "exit"); ItemStack playerMenu = new ItemStack(Material.PLAYER_HEAD); ItemMeta menuMeta = playerMenu.getItemMeta(); - menuMeta.displayName(Component.text("Players", NamedTextColor.AQUA)); + menuMeta.displayName(message("items.players", "Players")); playerMenu.setItemMeta(menuMeta); + markControl(playerMenu, "players"); viewer.getInventory().setItem(0, skipBackward); viewer.getInventory().setItem(1, pauseButton); @@ -126,13 +139,15 @@ public void giveReplayControls() { public void showStepControls() { ItemStack stepBack = new ItemStack(Material.CYAN_DYE); ItemMeta backMeta = stepBack.getItemMeta(); - backMeta.displayName(Component.text("\u25C0\u25C0 Previous Frame", NamedTextColor.AQUA)); + backMeta.displayName(message("items.previous-frame", "\u25C0\u25C0 Previous Frame")); stepBack.setItemMeta(backMeta); + markControl(stepBack, "previous-frame"); ItemStack stepForward = new ItemStack(Material.MAGENTA_DYE); ItemMeta fwdMeta = stepForward.getItemMeta(); - fwdMeta.displayName(Component.text("\u25B6\u25B6 Next Frame", NamedTextColor.LIGHT_PURPLE)); + fwdMeta.displayName(message("items.next-frame", "\u25B6\u25B6 Next Frame")); stepForward.setItemMeta(fwdMeta); + markControl(stepForward, "next-frame"); viewer.getInventory().setItem(5, stepBack); viewer.getInventory().setItem(6, stepForward); @@ -145,19 +160,21 @@ public void hideStepControls() { public void showSpeedControls(double currentSpeed) { String speedText = String.format("%.1fx", currentSpeed); - List speedLore = List.of(Component.text("Current: " + speedText, NamedTextColor.GRAY)); + List speedLore = List.of(message("items.speed-lore", "Current: %speed%", "speed", speedText)); ItemStack slower = new ItemStack(Material.ORANGE_DYE); ItemMeta slowerMeta = slower.getItemMeta(); - slowerMeta.displayName(Component.text("\u23EA Slower", NamedTextColor.GOLD)); + slowerMeta.displayName(message("items.slower", "\u23EA Slower")); slowerMeta.lore(speedLore); slower.setItemMeta(slowerMeta); + markControl(slower, "slower"); ItemStack faster = new ItemStack(Material.LIGHT_BLUE_DYE); ItemMeta fasterMeta = faster.getItemMeta(); - fasterMeta.displayName(Component.text("\u23E9 Faster", NamedTextColor.BLUE)); + fasterMeta.displayName(message("items.faster", "\u23E9 Faster")); fasterMeta.lore(speedLore); faster.setItemMeta(fasterMeta); + markControl(faster, "faster"); viewer.getInventory().setItem(5, slower); viewer.getInventory().setItem(6, faster); @@ -167,7 +184,7 @@ public void openPlayerMenu() { Inventory inv = Bukkit.createInventory( null, 27, - Component.text("Recorded Players", NamedTextColor.DARK_GRAY) + message("menus.recorded-players", "Recorded Players") ); for (RecordedEntity entity : recordedEntitiesSupplier.get().values()) { @@ -287,8 +304,7 @@ public void onPlayerInteract(PlayerInteractEvent e) { if (handItem == null || !handItem.hasItemMeta()) return; - Component displayName = handItem.getItemMeta().displayName(); - String name = displayName instanceof TextComponent tc ? tc.content() : ""; + String control = getControl(handItem); RecordedPlayer targetPlayer = getTargetedRecordedPlayer(player); if (targetPlayer != null) { @@ -297,16 +313,19 @@ public void onPlayerInteract(PlayerInteractEvent e) { return; } - switch (name) { - case "Pause / Play" -> sessionControl.togglePause(); - case "+5 seconds" -> sessionControl.skipSeconds(5); - case "-5 seconds" -> sessionControl.skipSeconds(-5); - case "\u25C0\u25C0 Previous Frame" -> sessionControl.stepTick(-1); - case "\u25B6\u25B6 Next Frame" -> sessionControl.stepTick(1); - case "\u23EA Slower" -> sessionControl.changeSpeed(-1); - case "\u23E9 Faster" -> sessionControl.changeSpeed(1); - case "Exit Replay" -> sessionControl.stop(); - case "Players" -> openPlayerMenu(); + if (control == null) return; + + switch (control) { + case "pause-play" -> sessionControl.togglePause(); + case "skip-forward" -> sessionControl.skipSeconds(5); + case "skip-backward" -> sessionControl.skipSeconds(-5); + case "previous-frame" -> sessionControl.stepTick(-1); + case "next-frame" -> sessionControl.stepTick(1); + case "slower" -> sessionControl.changeSpeed(-1); + case "faster" -> sessionControl.changeSpeed(1); + case "exit" -> sessionControl.stop(); + case "players" -> openPlayerMenu(); + default -> { return; } } e.setCancelled(true); @@ -385,11 +404,7 @@ public void onPlayerDropItem(PlayerDropItemEvent e) { if (item == null || !item.hasItemMeta()) return; - Component dropDisplayName = item.getItemMeta().displayName(); - String dropName = dropDisplayName instanceof TextComponent tc ? tc.content() : ""; - if (dropName.equals("Pause / Play") || dropName.equals("+5 seconds") || dropName.equals("-5 seconds") - || dropName.equals("\u25C0\u25C0 Previous Frame") || dropName.equals("\u25B6\u25B6 Next Frame") - || dropName.equals("\u23EA Slower") || dropName.equals("\u23E9 Faster")) { + if (getControl(item) != null) { e.setCancelled(true); } } @@ -404,4 +419,22 @@ public void onEntityPickupItem(EntityPickupItemEvent e) { e.setCancelled(true); } + + private Component message(String key, String fallback, String... replacements) { + ReplayMessagesConfig messages = replay.getMessages(); + if (messages != null) return messages.component(key, fallback, replacements); + String value = fallback; + for (int index = 0; index + 1 < replacements.length; index += 2) { + value = value.replace("%" + replacements[index] + "%", replacements[index + 1]); + } + return MINI_MESSAGE.deserialize(value); + } + + private void markControl(ItemStack item, String control) { + item.editPersistentDataContainer(pdc -> pdc.set(controlKey, PersistentDataType.STRING, control)); + } + + private String getControl(ItemStack item) { + return item.getPersistentDataContainer().get(controlKey, PersistentDataType.STRING); + } } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..0bb3dcf --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,26 @@ +# =========================================== +# BetterReplay Messages +# =========================================== + +replay: + empty: "Replay is empty!" + finished: "Replay finished" + +action-bar: + paused: "⏸ Replay paused: %current% / %total%" + playing: "▶ Replay: %current% / %total% (%percent%%) [%speed%]" + +items: + pause-play: "Pause / Play" + skip-forward: "+5 seconds" + skip-backward: "-5 seconds" + exit: "Exit Replay" + players: "Players" + previous-frame: "◀◀ Previous Frame" + next-frame: "▶▶ Next Frame" + slower: "⏪ Slower" + faster: "⏩ Faster" + speed-lore: "Current: %speed%" + +menus: + recorded-players: "Recorded Players"