diff --git a/src/main/java/dev/phyce/naturalspeech/SpeechEventHandler.java b/src/main/java/dev/phyce/naturalspeech/SpeechEventHandler.java index 78faa24..752659a 100644 --- a/src/main/java/dev/phyce/naturalspeech/SpeechEventHandler.java +++ b/src/main/java/dev/phyce/naturalspeech/SpeechEventHandler.java @@ -13,7 +13,9 @@ import dev.phyce.naturalspeech.tts.VoiceID; import dev.phyce.naturalspeech.tts.VoiceManager; import dev.phyce.naturalspeech.utils.TextUtil; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import lombok.extern.slf4j.Slf4j; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; @@ -21,6 +23,7 @@ import net.runelite.api.Player; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.OverheadTextChanged; +import net.runelite.api.events.WidgetClosed; import net.runelite.api.events.WidgetLoaded; import net.runelite.api.widgets.ComponentID; import net.runelite.api.widgets.InterfaceID; @@ -41,6 +44,10 @@ public class SpeechEventHandler { private final ClientThread clientThread; + // NPC names that have spoken in the current dialog (tag-stripped, lowercased). + // Multi-NPC dialogs append; cleared when no dialog widget remains. Client thread only. + private final Set currentDialogNpcNames = new HashSet<>(); + @Inject public SpeechEventHandler(Client client, TextToSpeech textToSpeech, NaturalSpeechConfig config, VoiceManager voiceManager, MuteManager muteManager, SpamDetection spamDetection, ClientThread clientThread) { @@ -155,6 +162,22 @@ private void onWidgetLoaded(WidgetLoaded event) { textToSpeech.speak(voiceID, text, 0, MagicUsernames.DIALOG); }); } else if (event.getGroupId() == InterfaceID.DIALOG_NPC) { + // Add the NPC synchronously so the very first OverheadTextChanged in this tick + // is suppressed. The name widget isn't filled in yet, but the player's interaction + // target was set the moment they clicked "Talk-to". + Player local = client.getLocalPlayer(); + if (local != null && local.getInteracting() instanceof NPC) { + String n = ((NPC) local.getInteracting()).getName(); + if (n != null) currentDialogNpcNames.add(Text.removeTags(n).toLowerCase()); + } + // Backup: read the dialog name widget at tick end, in case interacting wasn't this NPC + // (e.g. multi-NPC scripted conversation where the talking NPC changes). + clientThread.invokeAtTickEnd(() -> { + Widget nw = client.getWidget(ComponentID.DIALOG_NPC_NAME); + if (nw != null && nw.getText() != null) { + currentDialogNpcNames.add(Text.removeTags(nw.getText()).toLowerCase()); + } + }); if (!config.npcDialogEnabled()) return; // InvokeAtTickEnd to wait until the text has loaded in clientThread.invokeAtTickEnd(() -> { @@ -205,6 +228,7 @@ private void onOverheadTextChanged(OverheadTextChanged event) { if (!config.npcOverheadEnabled()) return; if (isAreaDisabled()) return; NPC npc = (NPC) event.getActor(); + if (config.suppressOverheadDuringDialog() && isInDialogWith(npc)) return; if (!muteManager.isNpcAllowed(npc)) return; int distance = PluginHelper.getActorDistance(event.getActor()); @@ -457,6 +481,28 @@ private boolean checkMuteLevelThreshold(ChatMessage message) { } + private boolean isInDialogWith(NPC npc) { + if (currentDialogNpcNames.isEmpty()) return false; + String npcName = npc.getName(); + if (npcName == null) return false; + return currentDialogNpcNames.contains(Text.removeTags(npcName).toLowerCase()); + } + + @Subscribe + private void onWidgetClosed(WidgetClosed event) { + int group = event.getGroupId(); + if (group != InterfaceID.DIALOG_NPC && group != InterfaceID.DIALOG_PLAYER) return; + // A pane closing may just be a transition to the other pane (NPC line ↔ player reply); + // defer to tick end so the new pane has had a chance to load before we decide. + clientThread.invokeAtTickEnd(() -> { + Widget npcText = client.getWidget(ComponentID.DIALOG_NPC_TEXT); + Widget playerText = client.getWidget(ComponentID.DIALOG_PLAYER_TEXT); + boolean stillOpen = (npcText != null && !npcText.isHidden()) + || (playerText != null && !playerText.isHidden()); + if (!stillOpen) currentDialogNpcNames.clear(); + }); + } + private static int getGroupId(int component) { return component >> 16; } diff --git a/src/main/java/dev/phyce/naturalspeech/configs/NaturalSpeechConfig.java b/src/main/java/dev/phyce/naturalspeech/configs/NaturalSpeechConfig.java index 5b2785c..6d747c5 100644 --- a/src/main/java/dev/phyce/naturalspeech/configs/NaturalSpeechConfig.java +++ b/src/main/java/dev/phyce/naturalspeech/configs/NaturalSpeechConfig.java @@ -46,6 +46,7 @@ final class ConfigKeys { public static final String HOLD_SHIFT_RIGHT_CLICK_MENU = "holdShiftRightClickMenu"; public static final String MUTE_GRAND_EXCHANGE_NPC_SPAM = "muteGrandExchangeNpcSpam"; public static final String FRIENDS_VOLUME_BOOST = "friendsVolumeBoost"; + public static final String SUPPRESS_OVERHEAD_DURING_DIALOG = "suppressOverheadDuringDialog"; } // @@ -159,6 +160,17 @@ default boolean holdShiftRightClickMenu() { return false; } + @ConfigItem( + position=10, + keyName=ConfigKeys.SUPPRESS_OVERHEAD_DURING_DIALOG, + name="Mute overhead text during dialogs", + description="Silence overhead chatter when dialog is open", + section=generalSettingsSection + ) + default boolean suppressOverheadDuringDialog() { + return true; + } + // @ConfigItem( // position=5, // keyName="playbackSpeed",