diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/ChatZoneOverridesOptions.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/ChatZoneOverridesOptions.cs new file mode 100644 index 0000000..8a531c0 --- /dev/null +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/ChatZoneOverridesOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace LightlessSyncServer.Configuration; + +public sealed class ChatZoneOverridesOptions +{ + public List? Zones { get; set; } +} + +public sealed class ChatZoneOverride +{ + public string Key { get; set; } = string.Empty; + public string? DisplayName { get; set; } + public List? TerritoryNames { get; set; } + public List? TerritoryIds { get; set; } +} diff --git a/LightlessSyncServer/LightlessSyncServer/Models/ChatZoneDefinitions.cs b/LightlessSyncServer/LightlessSyncServer/Models/ChatZoneDefinitions.cs index ddc12f5..4d51848 100644 --- a/LightlessSyncServer/LightlessSyncServer/Models/ChatZoneDefinitions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Models/ChatZoneDefinitions.cs @@ -61,7 +61,160 @@ internal static class ChatZoneDefinitions }, TerritoryIds: TerritoryRegistry.GetIds( "Ul'dah - Steps of Nald", - "Ul'dah - Steps of Thal")) + "Ul'dah - Steps of Thal")), + new ZoneChannelDefinition( + Key: "ishgard", + DisplayName: "Ishgard", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "ishgard" + }, + TerritoryNames: new[] + { + "Foundation", + "The Pillars" + }, + TerritoryIds: TerritoryRegistry.GetIds( + "Foundation", + "The Pillars")), + new ZoneChannelDefinition( + Key: "kugane", + DisplayName: "Kugane", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "kugane" + }, + TerritoryNames: new[] + { + "Kugane" + }, + TerritoryIds: TerritoryRegistry.GetIds("Kugane")), + new ZoneChannelDefinition( + Key: "crystarium", + DisplayName: "The Crystarium", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "crystarium" + }, + TerritoryNames: new[] + { + "The Crystarium" + }, + TerritoryIds: TerritoryRegistry.GetIds("The Crystarium")), + new ZoneChannelDefinition( + Key: "oldsharlayan", + DisplayName: "Old Sharlayan", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "oldsharlayan" + }, + TerritoryNames: new[] + { + "Old Sharlayan" + }, + TerritoryIds: TerritoryRegistry.GetIds("Old Sharlayan")), + new ZoneChannelDefinition( + Key: "tuliyollal", + DisplayName: "Tuliyollal", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "tuliyollal" + }, + TerritoryNames: new[] + { + "Tuliyollal" + }, + TerritoryIds: TerritoryRegistry.GetIds("Tuliyollal")), + new ZoneChannelDefinition( + Key: "eulmore", + DisplayName: "Eulmore", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "eulmore" + }, + TerritoryNames: new[] + { + "Eulmore" + }, + TerritoryIds: TerritoryRegistry.GetIds("Eulmore")), + new ZoneChannelDefinition( + Key: "idyllshire", + DisplayName: "Idyllshire", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "idyllshire" + }, + TerritoryNames: new[] + { + "Idyllshire" + }, + TerritoryIds: TerritoryRegistry.GetIds("Idyllshire")), + new ZoneChannelDefinition( + Key: "rhalgrsreach", + DisplayName: "Rhalgr's Reach", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "rhalgrsreach" + }, + TerritoryNames: new[] + { + "Rhalgr's Reach" + }, + TerritoryIds: TerritoryRegistry.GetIds("Rhalgr's Reach")), + new ZoneChannelDefinition( + Key: "radzathan", + DisplayName: "Radz-at-Han", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "radzathan" + }, + TerritoryNames: new[] + { + "Radz-at-Han" + }, + TerritoryIds: TerritoryRegistry.GetIds("Radz-at-Han")), + new ZoneChannelDefinition( + Key: "solutionnine", + DisplayName: "Solution Nine", + Descriptor: new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = "solutionnine" + }, + TerritoryNames: new[] + { + "Solution Nine" + }, + TerritoryIds: TerritoryRegistry.GetIds("Solution Nine")) }; } diff --git a/LightlessSyncServer/LightlessSyncServer/Services/ChatChannelService.cs b/LightlessSyncServer/LightlessSyncServer/Services/ChatChannelService.cs index fb9746f..9d4990b 100644 --- a/LightlessSyncServer/LightlessSyncServer/Services/ChatChannelService.cs +++ b/LightlessSyncServer/LightlessSyncServer/Services/ChatChannelService.cs @@ -1,7 +1,9 @@ using System.Security.Cryptography; using LightlessSync.API.Data; using LightlessSync.API.Dto.Chat; +using LightlessSyncServer.Configuration; using LightlessSyncServer.Models; +using Microsoft.Extensions.Options; namespace LightlessSyncServer.Services; @@ -21,14 +23,132 @@ public sealed class ChatChannelService : IDisposable private static readonly TimeSpan InactiveParticipantCleanupInterval = TimeSpan.FromMinutes(1); private readonly Timer _inactiveParticipantCleanupTimer; - public ChatChannelService(ILogger logger) + public ChatChannelService(ILogger logger, IOptions? zoneOverrides = null) { _logger = logger; - _zoneDefinitions = ChatZoneDefinitions.Defaults - .ToDictionary(definition => definition.Key, StringComparer.OrdinalIgnoreCase); + _zoneDefinitions = BuildZoneDefinitions(zoneOverrides?.Value); _inactiveParticipantCleanupTimer = new Timer(_ => CleanupExpiredInactiveParticipants(), null, InactiveParticipantCleanupInterval, InactiveParticipantCleanupInterval); } + private Dictionary BuildZoneDefinitions(ChatZoneOverridesOptions? overrides) + { + var definitions = ChatZoneDefinitions.Defaults + .ToDictionary(definition => definition.Key, StringComparer.OrdinalIgnoreCase); + + if (overrides?.Zones is null || overrides.Zones.Count == 0) + { + return definitions; + } + + foreach (var entry in overrides.Zones) + { + if (entry is null) + { + continue; + } + + if (!TryCreateZoneDefinition(entry, out var definition)) + { + continue; + } + + definitions[definition.Key] = definition; + } + + return definitions; + } + + private bool TryCreateZoneDefinition(ChatZoneOverride entry, out ZoneChannelDefinition definition) + { + definition = default; + + var key = NormalizeZoneKey(entry.Key); + if (string.IsNullOrEmpty(key)) + { + _logger.LogWarning("Skipped chat zone override with missing key."); + return false; + } + + var territoryIds = new HashSet(); + if (entry.TerritoryIds is not null) + { + foreach (var candidate in entry.TerritoryIds) + { + if (candidate > 0) + { + territoryIds.Add(candidate); + } + } + } + + var territoryNames = new HashSet(StringComparer.OrdinalIgnoreCase); + if (entry.TerritoryNames is not null) + { + foreach (var name in entry.TerritoryNames) + { + if (string.IsNullOrWhiteSpace(name)) + continue; + + var trimmed = name.Trim(); + territoryNames.Add(trimmed); + if (TerritoryRegistry.TryGetIds(trimmed, out var ids)) + { + territoryIds.UnionWith(ids); + } + else + { + _logger.LogWarning("Chat zone override {Zone} references unknown territory '{Territory}'.", key, trimmed); + } + } + } + + if (territoryIds.Count == 0) + { + _logger.LogWarning("Skipped chat zone override for {Zone}: no territory IDs resolved.", key); + return false; + } + + if (territoryNames.Count == 0) + { + foreach (var territoryId in territoryIds) + { + if (TerritoryRegistry.ById.TryGetValue(territoryId, out var territory)) + { + territoryNames.Add(territory.Name); + } + } + } + + if (territoryNames.Count == 0) + { + territoryNames.Add("Territory"); + } + + var descriptor = new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = 0, + ZoneId = 0, + CustomKey = key + }; + + var displayName = string.IsNullOrWhiteSpace(entry.DisplayName) + ? key + : entry.DisplayName.Trim(); + + definition = new ZoneChannelDefinition( + key, + displayName, + descriptor, + territoryNames.ToArray(), + territoryIds); + + return true; + } + + private static string NormalizeZoneKey(string? value) => + string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + public IReadOnlyList GetZoneChannelInfos() => _zoneDefinitions.Values .Select(definition => new ZoneChatChannelInfoDto( diff --git a/LightlessSyncServer/LightlessSyncServer/Startup.cs b/LightlessSyncServer/LightlessSyncServer/Startup.cs index 8f043da..e16e24b 100644 --- a/LightlessSyncServer/LightlessSyncServer/Startup.cs +++ b/LightlessSyncServer/LightlessSyncServer/Startup.cs @@ -93,6 +93,7 @@ public class Startup services.Configure(Configuration.GetRequiredSection("LightlessSync")); services.Configure(Configuration.GetRequiredSection("LightlessSync")); services.Configure(Configuration.GetSection("Broadcast")); + services.Configure(Configuration.GetSection("ChatZoneOverrides")); services.AddSingleton(); services.AddSingleton(); diff --git a/LightlessSyncServer/LightlessSyncServer/appsettings.json b/LightlessSyncServer/LightlessSyncServer/appsettings.json index d3983ef..cf1ce10 100644 --- a/LightlessSyncServer/LightlessSyncServer/appsettings.json +++ b/LightlessSyncServer/LightlessSyncServer/appsettings.json @@ -41,6 +41,9 @@ "PairRequestRateLimit": 5, "PairRequestRateWindow": 60 }, + "ChatZoneOverrides": { + "Zones": [] + }, "AllowedHosts": "*", "Kestrel": { "Endpoints": {