Skip to content

Commit 73d8abb

Browse files
committed
onchatmessage filters
1 parent 67fad62 commit 73d8abb

File tree

8 files changed

+212
-7
lines changed

8 files changed

+212
-7
lines changed

SomethingNeedDoing/Core/MacroMetadata.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Dalamud.Game.Addon.Lifecycle;
2+
using Dalamud.Game.Text;
23
using SomethingNeedDoing.Core.Interfaces;
34

45
namespace SomethingNeedDoing.Core;
@@ -73,6 +74,17 @@ public class MacroMetadata
7374
/// Gets or sets the macro dependencies.
7475
/// </summary>
7576
public List<IMacroDependency> Dependencies { get; set; } = [];
77+
78+
/// <summary>
79+
/// Gets or sets the chat message filter configuration for macro-level OnChatMessage triggers.
80+
/// </summary>
81+
public ChatMessageFilterConfig? ChatMessageFilter { get; set; }
82+
83+
/// <summary>
84+
/// Gets or sets chat message filter configurations for function-level triggers.
85+
/// Key is the function name (e.g., "OnChatMessage"), value is the filter configuration.
86+
/// </summary>
87+
public Dictionary<string, ChatMessageFilterConfig> FunctionChatFilters { get; set; } = [];
7688
}
7789

7890
/// <summary>
@@ -90,3 +102,29 @@ public class AddonEventConfig
90102
/// </summary>
91103
public AddonEvent EventType { get; set; } = AddonEvent.PostSetup;
92104
}
105+
106+
/// <summary>
107+
/// Configuration for filtering chat messages.
108+
/// </summary>
109+
public class ChatMessageFilterConfig
110+
{
111+
/// <summary>
112+
/// Gets or sets the chat channels to filter by. If null or empty, all channels are allowed.
113+
/// </summary>
114+
public List<XivChatType>? Channels { get; set; }
115+
116+
/// <summary>
117+
/// Gets or sets a string that the message must contain. If null or empty, no message content filter is applied.
118+
/// </summary>
119+
public string? MessageContains { get; set; }
120+
121+
/// <summary>
122+
/// Gets or sets a string that the sender must contain. If null or empty, no sender filter is applied.
123+
/// </summary>
124+
public string? SenderContains { get; set; }
125+
126+
/// <summary>
127+
/// Gets or sets a regex pattern that the message must match. If null or empty, no regex filter is applied.
128+
/// </summary>
129+
public string? MessageRegex { get; set; }
130+
}

SomethingNeedDoing/Core/MetadataParser.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Dalamud.Game.Addon.Lifecycle;
2+
using Dalamud.Game.Text;
23
using SomethingNeedDoing.Core.Interfaces;
34
using System.Text.RegularExpressions;
45
using YamlDotNet.Serialization;
@@ -92,6 +93,21 @@ public MacroMetadata ParseMetadata(string content, MacroMetadata? previousMetada
9293
metadata.AddonEventConfig.EventType = parsedEventType;
9394
}
9495

96+
if (yaml.TryGetValue("chat_message_filter", out var chatFilter) && chatFilter is Dictionary<object, object> chatFilterDict)
97+
metadata.ChatMessageFilter = ParseChatMessageFilter(chatFilterDict);
98+
99+
if (yaml.TryGetValue("function_chat_filters", out var functionChatFilters) && functionChatFilters is Dictionary<object, object> functionFiltersDict)
100+
{
101+
foreach (var kvp in functionFiltersDict)
102+
{
103+
var functionName = kvp.Key?.ToString();
104+
if (string.IsNullOrEmpty(functionName)) continue;
105+
106+
if (kvp.Value is Dictionary<object, object> filterDict)
107+
metadata.FunctionChatFilters[functionName] = ParseChatMessageFilter(filterDict);
108+
}
109+
}
110+
95111
if (yaml.TryGetValue("dependencies", out var dependencies))
96112
metadata.Dependencies = ParseDependencies(dependencies);
97113

@@ -156,6 +172,17 @@ public bool WriteMetadata(ConfigMacro macro, Action? onContentUpdated = null)
156172
};
157173
}
158174

175+
if (macro.Metadata.ChatMessageFilter != null)
176+
metadataDict["chat_message_filter"] = SerializeChatMessageFilter(macro.Metadata.ChatMessageFilter);
177+
178+
if (macro.Metadata.FunctionChatFilters.Any())
179+
{
180+
metadataDict["function_chat_filters"] = macro.Metadata.FunctionChatFilters.ToDictionary(
181+
kvp => kvp.Key,
182+
kvp => SerializeChatMessageFilter(kvp.Value)
183+
);
184+
}
185+
159186
if (macro.Metadata.Dependencies.Any())
160187
{
161188
metadataDict["dependencies"] = macro.Metadata.Dependencies.Select(dep => new Dictionary<string, object>
@@ -389,4 +416,50 @@ private static Type InferTypeFromValue(object? value)
389416

390417
return typeof(string);
391418
}
419+
420+
private static ChatMessageFilterConfig ParseChatMessageFilter(Dictionary<object, object> filterDict)
421+
{
422+
var filter = new ChatMessageFilterConfig();
423+
424+
if (filterDict.TryGetValue("channels", out var channels))
425+
{
426+
if (channels is List<object> channelList)
427+
{
428+
filter.Channels = [];
429+
foreach (var channel in channelList)
430+
if (Enum.TryParse<XivChatType>(channel?.ToString(), true, out var chatType))
431+
filter.Channels.Add(chatType);
432+
}
433+
}
434+
435+
if (filterDict.TryGetValue("message_contains", out var messageContains))
436+
filter.MessageContains = messageContains?.ToString();
437+
438+
if (filterDict.TryGetValue("sender_contains", out var senderContains))
439+
filter.SenderContains = senderContains?.ToString();
440+
441+
if (filterDict.TryGetValue("message_regex", out var messageRegex))
442+
filter.MessageRegex = messageRegex?.ToString();
443+
444+
return filter;
445+
}
446+
447+
private static Dictionary<string, object> SerializeChatMessageFilter(ChatMessageFilterConfig filter)
448+
{
449+
var dict = new Dictionary<string, object>();
450+
451+
if (filter.Channels != null && filter.Channels.Count > 0)
452+
dict["channels"] = filter.Channels.Select(c => c.ToString().ToLower()).ToList();
453+
454+
if (!string.IsNullOrEmpty(filter.MessageContains))
455+
dict["message_contains"] = filter.MessageContains;
456+
457+
if (!string.IsNullOrEmpty(filter.SenderContains))
458+
dict["sender_contains"] = filter.SenderContains;
459+
460+
if (!string.IsNullOrEmpty(filter.MessageRegex))
461+
dict["message_regex"] = filter.MessageRegex;
462+
463+
return dict;
464+
}
392465
}

SomethingNeedDoing/External/AllaganTools.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using ECommons.EzIpcManager;
2-
using FFXIVClientStructs.FFXIV.Client.Game;
32
using SomethingNeedDoing.Core.Interfaces;
43

54
namespace SomethingNeedDoing.External;

SomethingNeedDoing/Globals.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
global using Dalamud.Game.ClientState.Conditions;
33
global using ECommons.DalamudServices;
44
global using ECommons.GameFunctions;
5-
global using ECommons.GameHelpers;
65
global using SomethingNeedDoing.Attributes;
76
global using SomethingNeedDoing.Core;
87
global using SomethingNeedDoing.Core.Exceptions;

SomethingNeedDoing/Gui/ChangelogWindow.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private void AddGeneralChangelogs()
8282
Add("13.41", "Fixed how some lists were handled in the configs, as well as string representations of other types");
8383
Add("13.51", "Fixed ipc enums not being registered");
8484
Add("13.59", "Added option to auto show status window on script start. Also fixed the carets not showing in the help menu for Axis fonts.");
85+
Add("14.02", "Added OnChatMessage metadata filters so it's actually usable. See the wiki for usage.");
8586
}
8687

8788
private void Add(string version, string description)

SomethingNeedDoing/LuaMacro/NLuaMacroEngine.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using SomethingNeedDoing.LuaMacro.Modules;
55
using SomethingNeedDoing.LuaMacro.Modules.Engines;
66
using SomethingNeedDoing.Managers;
7-
using SomethingNeedDoing.NativeMacro;
87
using System.Collections.Concurrent;
98
using System.Text;
109
using System.Threading;

SomethingNeedDoing/Managers/MacroScheduler.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class MacroScheduler : IMacroScheduler, IDisposable
3939
private readonly TriggerEventManager _triggerEventManager;
4040
private readonly MacroHierarchyManager _hierarchyManager;
4141
private readonly WindowSystem _windowSystem;
42+
private readonly Core.MetadataParser _metadataParser;
4243

4344
private readonly HashSet<string> _functionTriggersRegistered = [];
4445

@@ -54,7 +55,7 @@ public class MacroScheduler : IMacroScheduler, IDisposable
5455
[Signature("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 30 4C 8B 74 24 ?? 48 8B D9", DetourName = nameof(OnEmoteFuncDetour))]
5556
private readonly Hook<OnEmoteFuncDelegate> OnEmoteFuncHook = null!;
5657

57-
public MacroScheduler(NativeMacroEngine nativeEngine, NLuaMacroEngine luaEngine, TriggerEventManager triggerEventManager, MacroHierarchyManager hierarchyManager, WindowSystem windowSystem, IEnumerable<IDisableable> disableablePlugins)
58+
public MacroScheduler(NativeMacroEngine nativeEngine, NLuaMacroEngine luaEngine, TriggerEventManager triggerEventManager, MacroHierarchyManager hierarchyManager, WindowSystem windowSystem, Core.MetadataParser metadataParser, IEnumerable<IDisableable> disableablePlugins)
5859
{
5960
Svc.Hook.InitializeFromAttributes(this);
6061
OnEmoteFuncHook?.Enable();
@@ -64,6 +65,7 @@ public MacroScheduler(NativeMacroEngine nativeEngine, NLuaMacroEngine luaEngine,
6465
_triggerEventManager = triggerEventManager;
6566
_hierarchyManager = hierarchyManager;
6667
_windowSystem = windowSystem;
68+
_metadataParser = metadataParser;
6769

6870
_nativeEngine.MacroError += OnEngineError;
6971
_luaEngine.MacroError += OnEngineError;
@@ -690,6 +692,13 @@ private void SubscribeToTriggerEvents()
690692
{
691693
foreach (var macro in C.Macros)
692694
{
695+
// Parse metadata from content to ensure filters and triggers are loaded
696+
if (macro is ConfigMacro configMacro)
697+
{
698+
var parsedMetadata = _metadataParser.ParseMetadata(configMacro.Content, configMacro.Metadata);
699+
configMacro.Metadata = parsedMetadata;
700+
}
701+
693702
foreach (var triggerEvent in macro.Metadata.TriggerEvents)
694703
SubscribeToTriggerEvent(macro, triggerEvent);
695704

SomethingNeedDoing/Managers/TriggerEventManager.cs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using Dalamud.Game.Text;
12
using SomethingNeedDoing.Core.Events;
23
using SomethingNeedDoing.Core.Interfaces;
4+
using System.Text.RegularExpressions;
35
using System.Threading.Tasks;
46

57
namespace SomethingNeedDoing.Managers;
@@ -13,7 +15,8 @@ namespace SomethingNeedDoing.Managers;
1315
/// <param name="macro">The macro containing the function.</param>
1416
/// <param name="functionName">The name of the function.</param>
1517
/// <param name="eventType">The trigger event this function handles.</param>
16-
public class TriggerFunction(IMacro macro, string functionName, TriggerEvent eventType)
18+
/// <param name="chatMessageFilter">Optional chat message filter configuration.</param>
19+
public class TriggerFunction(IMacro macro, string functionName, TriggerEvent eventType, ChatMessageFilterConfig? chatMessageFilter = null)
1720
{
1821
/// <summary>
1922
/// Gets the macro containing this function.
@@ -44,6 +47,11 @@ public class TriggerFunction(IMacro macro, string functionName, TriggerEvent eve
4447
? functionName.Split('_')[2]
4548
: null;
4649

50+
/// <summary>
51+
/// Gets the chat message filter configuration for OnChatMessage triggers.
52+
/// </summary>
53+
public ChatMessageFilterConfig? ChatMessageFilter { get; } = chatMessageFilter;
54+
4755
/// <inheritdoc/>
4856
public override bool Equals(object? obj)
4957
=> obj is TriggerFunction other && Macro.Id == other.Macro.Id && FunctionName == other.FunctionName;
@@ -79,7 +87,11 @@ public void RegisterTrigger(IMacro macro, TriggerEvent eventType)
7987
if (!EventHandlers.ContainsKey(eventType))
8088
EventHandlers[eventType] = [];
8189

82-
var triggerFunction = new TriggerFunction(macro, string.Empty, eventType);
90+
ChatMessageFilterConfig? chatFilter = null;
91+
if (eventType == TriggerEvent.OnChatMessage && macro.Metadata.ChatMessageFilter != null)
92+
chatFilter = macro.Metadata.ChatMessageFilter;
93+
94+
var triggerFunction = new TriggerFunction(macro, string.Empty, eventType, chatFilter);
8395
if (!EventHandlers[eventType].Contains(triggerFunction))
8496
EventHandlers[eventType].Add(triggerFunction);
8597
}
@@ -122,7 +134,11 @@ public void RegisterFunctionTrigger(IMacro macro, string functionName)
122134
if (!EventHandlers.ContainsKey(eventType))
123135
EventHandlers[eventType] = [];
124136

125-
var triggerFunction = new TriggerFunction(macro, functionName, eventType);
137+
ChatMessageFilterConfig? chatFilter = null;
138+
if (eventType == TriggerEvent.OnChatMessage && macro.Metadata.FunctionChatFilters.TryGetValue(functionName, out var filter))
139+
chatFilter = filter;
140+
141+
var triggerFunction = new TriggerFunction(macro, functionName, eventType, chatFilter);
126142
if (EventHandlers[eventType].Contains(triggerFunction))
127143
{
128144
FrameworkLogger.Debug($"Function trigger already registered for macro {macro.Name} function {functionName} event {eventType}");
@@ -222,6 +238,11 @@ public async Task RaiseTriggerEvent(TriggerEvent eventType, object? data = null)
222238
continue;
223239
}
224240

241+
// For OnChatMessage, check if the message matches the filter
242+
if (eventType == TriggerEvent.OnChatMessage && data is Dictionary<string, object> chatData)
243+
if (!MatchesChatMessageFilter(chatData, triggerFunction.ChatMessageFilter))
244+
continue;
245+
225246
if (string.IsNullOrEmpty(triggerFunction.FunctionName))
226247
{
227248
// Macro-level trigger: raise the event for the entire macro
@@ -248,6 +269,72 @@ public async Task RaiseTriggerEvent(TriggerEvent eventType, object? data = null)
248269
}
249270
}
250271

272+
/// <summary>
273+
/// Checks if a chat message matches the given filter configuration.
274+
/// </summary>
275+
/// <param name="chatData">The chat message data dictionary.</param>
276+
/// <param name="filter">The filter configuration to check against.</param>
277+
/// <returns>True if the message matches the filter (or if no filter is specified), false otherwise.</returns>
278+
private static bool MatchesChatMessageFilter(Dictionary<string, object> chatData, ChatMessageFilterConfig? filter)
279+
{
280+
if (filter == null)
281+
return true;
282+
283+
if (filter.Channels != null && filter.Channels.Count > 0)
284+
{
285+
if (chatData.TryGetValue("type", out var typeObj) && typeObj is XivChatType chatType)
286+
{
287+
if (!filter.Channels.Contains(chatType))
288+
return false;
289+
}
290+
else
291+
return false;
292+
}
293+
294+
if (!string.IsNullOrEmpty(filter.MessageContains))
295+
{
296+
if (chatData.TryGetValue("message", out var messageObj) && messageObj is string message)
297+
{
298+
if (!message.Contains(filter.MessageContains, StringComparison.OrdinalIgnoreCase))
299+
return false;
300+
}
301+
else
302+
return false;
303+
}
304+
305+
if (!string.IsNullOrEmpty(filter.SenderContains))
306+
{
307+
if (chatData.TryGetValue("sender", out var senderObj) && senderObj is string sender)
308+
{
309+
if (!sender.Contains(filter.SenderContains, StringComparison.OrdinalIgnoreCase))
310+
return false;
311+
}
312+
else
313+
return false;
314+
}
315+
316+
if (!string.IsNullOrEmpty(filter.MessageRegex))
317+
{
318+
if (chatData.TryGetValue("message", out var messageObj) && messageObj is string message)
319+
{
320+
try
321+
{
322+
if (!Regex.IsMatch(message, filter.MessageRegex, RegexOptions.IgnoreCase))
323+
return false;
324+
}
325+
catch (ArgumentException)
326+
{
327+
FrameworkLogger.Warning($"Invalid regex pattern in chat message filter: {filter.MessageRegex}");
328+
return false;
329+
}
330+
}
331+
else
332+
return false;
333+
}
334+
335+
return true;
336+
}
337+
251338
/// <summary>
252339
/// Disposes of the trigger event manager.
253340
/// </summary>

0 commit comments

Comments
 (0)