using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Glamourer.Designs; using Glamourer.Events; using Glamourer.Interop; using Glamourer.Services; using Glamourer.Structs; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.GameData.Actors; namespace Glamourer.Automation; public class AutoDesignManager : ISavable, IReadOnlyList, IDisposable { public const int CurrentVersion = 1; private readonly SaveService _saveService; private readonly JobService _jobs; private readonly DesignManager _designs; private readonly ActorService _actors; private readonly AutomationChanged _event; private readonly DesignChanged _designEvent; private readonly List _data = new(); private readonly Dictionary _enabled = new(); public IReadOnlyDictionary EnabledSets => _enabled; public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event, FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent) { _jobs = jobs; _actors = actors; _saveService = saveService; _designs = designs; _event = @event; _designEvent = designEvent; _designEvent.Subscribe(OnDesignChange, DesignChanged.Priority.AutoDesignManager); Load(); migrator.ConsumeMigratedData(_actors, fileSystem, this); } public void Dispose() => _designEvent.Unsubscribe(OnDesignChange); public IEnumerator GetEnumerator() => _data.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Count => _data.Count; public AutoDesignSet this[int index] => _data[index]; public void AddDesignSet(string name, ActorIdentifier identifier) { if (!IdentifierValid(identifier, out var group) || name.Length == 0) return; var newSet = new AutoDesignSet(name, group) { Enabled = false }; _data.Add(newSet); Save(); Glamourer.Log.Debug($"Created new design set for {newSet.Identifiers[0].Incognito(null)}."); _event.Invoke(AutomationChanged.Type.AddedSet, newSet, (_data.Count - 1, name)); } public void DuplicateDesignSet(AutoDesignSet set) { var name = set.Name; var match = Regex.Match(name, @"\(Duplicate( (?\d+))?\)$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture); if (match.Success) { var number = match.Groups["Number"]; var replacement = number.Success ? $"(Duplicate {int.Parse(number.Value) + 1})" : "(Duplicate 2)"; name = name.Replace(match.Value, replacement); } else { name += " (Duplicate)"; } var newSet = new AutoDesignSet(name, set.Identifiers) { Enabled = false }; newSet.Designs.AddRange(set.Designs.Select(d => d.Clone())); _data.Add(newSet); Save(); Glamourer.Log.Debug( $"Duplicated new design set for {newSet.Identifiers[0].Incognito(null)} with {newSet.Designs.Count} auto designs from existing set."); _event.Invoke(AutomationChanged.Type.AddedSet, newSet, (_data.Count - 1, name)); } public void DeleteDesignSet(int whichSet) { if (whichSet >= _data.Count || whichSet < 0) return; var set = _data[whichSet]; if (set.Enabled) { set.Enabled = false; foreach (var id in set.Identifiers) _enabled.Remove(id); } _data.RemoveAt(whichSet); Save(); Glamourer.Log.Debug($"Deleted design set {whichSet + 1}."); _event.Invoke(AutomationChanged.Type.DeletedSet, set, whichSet); } public void Rename(int whichSet, string newName) { if (whichSet >= _data.Count || whichSet < 0 || newName.Length == 0) return; var set = _data[whichSet]; if (set.Name == newName) return; var old = set.Name; set.Name = newName; Save(); Glamourer.Log.Debug($"Renamed design set {whichSet + 1} from {old} to {newName}."); _event.Invoke(AutomationChanged.Type.RenamedSet, set, (old, newName)); } public void MoveSet(int whichSet, int toWhichSet) { if (!_data.Move(whichSet, toWhichSet)) return; Save(); Glamourer.Log.Debug($"Moved design set {whichSet + 1} to position {toWhichSet + 1}."); _event.Invoke(AutomationChanged.Type.MovedSet, _data[toWhichSet], (whichSet, toWhichSet)); } public void ChangeIdentifier(int whichSet, ActorIdentifier to) { if (whichSet >= _data.Count || whichSet < 0 || !IdentifierValid(to, out var group)) return; var set = _data[whichSet]; if (set.Identifiers.Any(id => id == to)) return; var old = set.Identifiers; set.Identifiers = group; AutoDesignSet? oldEnabled = null; if (set.Enabled) { foreach (var id in old) _enabled.Remove(id); if (_enabled.Remove(to, out oldEnabled)) { foreach (var id in oldEnabled.Identifiers) _enabled.Remove(id); oldEnabled.Enabled = false; } foreach (var id in group) _enabled.Add(id, set); } Save(); Glamourer.Log.Debug($"Changed Identifier of design set {whichSet + 1} from {old[0].Incognito(null)} to {to.Incognito(null)}."); _event.Invoke(AutomationChanged.Type.ChangeIdentifier, set, (old, to, oldEnabled)); } public void SetState(int whichSet, bool value) { if (whichSet >= _data.Count || whichSet < 0) return; var set = _data[whichSet]; if (set.Enabled == value) return; set.Enabled = value; AutoDesignSet? oldEnabled; if (value) { if (_enabled.Remove(set.Identifiers[0], out oldEnabled)) { foreach (var id in oldEnabled.Identifiers) _enabled.Remove(id); oldEnabled.Enabled = false; } foreach (var id in set.Identifiers) _enabled.Add(id, set); } else if (_enabled.Remove(set.Identifiers[0], out oldEnabled)) { foreach (var id in oldEnabled.Identifiers) _enabled.Remove(id); } Save(); Glamourer.Log.Debug($"Changed enabled state of design set {whichSet + 1} to {value}."); _event.Invoke(AutomationChanged.Type.ToggleSet, set, oldEnabled); } public void ChangeBaseState(int whichSet, AutoDesignSet.Base newBase) { if (whichSet >= _data.Count || whichSet < 0) return; var set = _data[whichSet]; if (newBase == set.BaseState) return; var old = set.BaseState; set.BaseState = newBase; Save(); Glamourer.Log.Debug($"Changed base state of set {whichSet + 1} from {old} to {newBase}."); _event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase)); } public void AddDesign(AutoDesignSet set, Design? design) { var newDesign = new AutoDesign() { Design = design, ApplicationType = AutoDesign.Type.All, Jobs = _jobs.JobGroups[1], }; set.Designs.Add(newDesign); Save(); Glamourer.Log.Debug( $"Added new associated design {design?.Identifier.ToString() ?? "Reverter"} as design {set.Designs.Count} to design set."); _event.Invoke(AutomationChanged.Type.AddedDesign, set, set.Designs.Count - 1); } /// Only used to move between sets. public void MoveDesignToSet(AutoDesignSet from, int idx, AutoDesignSet to) { if (ReferenceEquals(from, to)) return; var design = from.Designs[idx]; to.Designs.Add(design); from.Designs.RemoveAt(idx); Save(); Glamourer.Log.Debug($"Moved design {idx} from design set {from.Name} to design set {to.Name}."); _event.Invoke(AutomationChanged.Type.AddedDesign, to, to.Designs.Count - 1); _event.Invoke(AutomationChanged.Type.DeletedDesign, from, idx); } public void DeleteDesign(AutoDesignSet set, int which) { if (which >= set.Designs.Count || which < 0) return; set.Designs.RemoveAt(which); Save(); Glamourer.Log.Debug($"Removed associated design {which + 1} from design set."); _event.Invoke(AutomationChanged.Type.DeletedDesign, set, which); } public void MoveDesign(AutoDesignSet set, int from, int to) { if (!set.Designs.Move(from, to)) return; Save(); Glamourer.Log.Debug($"Moved design {from + 1} to {to + 1} in design set."); _event.Invoke(AutomationChanged.Type.MovedDesign, set, (from, to)); } public void ChangeDesign(AutoDesignSet set, int which, Design? newDesign) { if (which >= set.Designs.Count || which < 0) return; var design = set.Designs[which]; if (design.Design?.Identifier == newDesign?.Identifier) return; var old = design.Design; design.Design = newDesign; Save(); Glamourer.Log.Debug( $"Changed linked design from {old?.Identifier.ToString() ?? "Reverter"} to {newDesign?.Identifier.ToString() ?? "Reverter"} for associated design {which + 1} in design set."); _event.Invoke(AutomationChanged.Type.ChangedDesign, set, (which, old, newDesign)); } public void ChangeJobCondition(AutoDesignSet set, int which, JobGroup jobs) { if (which >= set.Designs.Count || which < 0) return; var design = set.Designs[which]; if (design.Jobs.Id == jobs.Id) return; var old = design.Jobs; design.Jobs = jobs; Save(); Glamourer.Log.Debug($"Changed job condition from {old.Id} to {jobs.Id} for associated design {which + 1} in design set."); _event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs)); } public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type) { if (which >= set.Designs.Count || which < 0) return; type &= AutoDesign.Type.All; var design = set.Designs[which]; if (design.ApplicationType == type) return; var old = design.ApplicationType; design.ApplicationType = type; Save(); Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set."); _event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type)); } public string ToFilename(FilenameService fileNames) => fileNames.AutomationFile; public void Save(StreamWriter writer) { using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented, }; Serialize().WriteTo(j); } private JObject Serialize() { var array = new JArray(); foreach (var set in _data) array.Add(set.Serialize()); return new JObject() { ["Version"] = CurrentVersion, ["Data"] = array, }; } private void Load() { var file = _saveService.FileNames.AutomationFile; _data.Clear(); if (!File.Exists(file)) return; try { var text = File.ReadAllText(file); var obj = JObject.Parse(text); var version = obj["Version"]?.ToObject() ?? 0; switch (version) { case < 1: case > CurrentVersion: Glamourer.Messager.NotificationMessage("Failure to load automated designs: No valid version available.", NotificationType.Error); break; case 1: LoadV1(obj["Data"]); break; } } catch (Exception ex) { Glamourer.Messager.NotificationMessage(ex, "Failure to load automated designs: Error during parsing.", "Failure to load automated designs", NotificationType.Error); } } private void LoadV1(JToken? data) { if (data is not JArray array) return; foreach (var obj in array) { var name = obj["Name"]?.ToObject() ?? string.Empty; if (name.Length == 0) { Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: No name provided.", NotificationType.Warning); continue; } var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject); if (!IdentifierValid(id, out var group)) { Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", NotificationType.Warning); continue; } var set = new AutoDesignSet(name, group) { Enabled = obj["Enabled"]?.ToObject() ?? false, BaseState = obj["BaseState"]?.ToObject() ?? AutoDesignSet.Base.Current, }; if (set.Enabled) { if (_enabled.TryAdd(group[0], set)) foreach (var id2 in group.Skip(1)) _enabled[id2] = set; else set.Enabled = false; } _data.Add(set); if (obj["Designs"] is not JArray designArray) continue; foreach (var designObj in designArray) { if (designObj is not JObject j) { Glamourer.Messager.NotificationMessage( $"Skipped loading design in Automation Set {name}: Unknown design.", NotificationType.Warning); continue; } var design = ToDesignObject(set.Name, j); if (design != null) set.Designs.Add(design); } } } private AutoDesign? ToDesignObject(string setName, JObject jObj) { var designIdentifier = jObj["Design"]?.ToObject(); Design? design = null; // designIdentifier == null means Revert-Design. if (designIdentifier != null) { if (designIdentifier.Length == 0) { Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", NotificationType.Warning); return null; } if (!Guid.TryParse(designIdentifier, out var guid)) { Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", NotificationType.Warning); return null; } design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid); if (design == null) { Glamourer.Messager.NotificationMessage( $"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", NotificationType.Warning); return null; } } var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject() ?? 0); var ret = new AutoDesign() { Design = design, ApplicationType = applicationType & AutoDesign.Type.All, }; var conditions = jObj["Conditions"]; if (conditions == null) return ret; var jobs = conditions["JobGroup"]?.ToObject() ?? -1; if (jobs >= 0) { if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup)) { Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", NotificationType.Warning); return null; } ret.Jobs = jobGroup; } return ret; } private void Save() => _saveService.DelaySave(this); private bool IdentifierValid(ActorIdentifier identifier, out ActorIdentifier[] group) { var validType = identifier.Type switch { IdentifierType.Player => true, IdentifierType.Retainer => true, IdentifierType.Npc => true, _ => false, }; if (!validType) { group = Array.Empty(); return false; } group = GetGroup(identifier); return group.Length > 0; } private ActorIdentifier[] GetGroup(ActorIdentifier identifier) { if (!identifier.IsValid) return Array.Empty(); static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) { var name = manager.Data.ToName(identifier.Kind, identifier.DataId); var table = identifier.Kind switch { ObjectKind.BattleNpc => manager.Data.BNpcs, ObjectKind.EventNpc => manager.Data.ENpcs, _ => new Dictionary(), }; return table.Where(kvp => kvp.Value == name) .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, identifier.Kind, kvp.Key)).ToArray(); } return identifier.Type switch { IdentifierType.Player => new[] { identifier.CreatePermanent(), }, IdentifierType.Retainer => new[] { _actors.AwaitedService.CreateRetainer(identifier.PlayerName, identifier.Retainer == ActorIdentifier.RetainerType.Mannequin ? ActorIdentifier.RetainerType.Mannequin : ActorIdentifier.RetainerType.Bell).CreatePermanent(), }, IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier), _ => Array.Empty(), }; } private void OnDesignChange(DesignChanged.Type type, Design design, object? data) { if (type is not DesignChanged.Type.Deleted) return; foreach (var (set, idx) in this.WithIndex()) { var deleted = 0; for (var i = 0; i < set.Designs.Count; ++i) { if (set.Designs[i].Design != design) continue; DeleteDesign(set, i--); ++deleted; } if (deleted > 0) Glamourer.Log.Information( $"Removed {deleted} automated designs from automated design set {idx} due to deletion of {design.Incognito}."); } } }