mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
617 lines
21 KiB
C#
617 lines
21 KiB
C#
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;
|
|
using Penumbra.GameData.Enums;
|
|
using Penumbra.GameData.Structs;
|
|
|
|
namespace Glamourer.Automation;
|
|
|
|
public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDisposable
|
|
{
|
|
public const int CurrentVersion = 1;
|
|
|
|
private readonly SaveService _saveService;
|
|
|
|
private readonly JobService _jobs;
|
|
private readonly DesignManager _designs;
|
|
private readonly ActorManager _actors;
|
|
private readonly AutomationChanged _event;
|
|
private readonly DesignChanged _designEvent;
|
|
|
|
private readonly List<AutoDesignSet> _data = [];
|
|
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = [];
|
|
|
|
public IReadOnlyDictionary<ActorIdentifier, AutoDesignSet> EnabledSets
|
|
=> _enabled;
|
|
|
|
public AutoDesignManager(JobService jobs, ActorManager 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<AutoDesignSet> 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( (?<Number>\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);
|
|
}
|
|
|
|
/// <remarks> Only used to move between sets. </remarks>
|
|
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 ChangeGearsetCondition(AutoDesignSet set, int which, short index)
|
|
{
|
|
if (which >= set.Designs.Count || which < 0)
|
|
return;
|
|
|
|
var design = set.Designs[which];
|
|
if (design.GearsetIndex == index)
|
|
return;
|
|
|
|
var old = design.GearsetIndex;
|
|
design.GearsetIndex = index;
|
|
Save();
|
|
Glamourer.Log.Debug($"Changed gearset condition from {old} to {index} for associated design {which + 1} in design set.");
|
|
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index));
|
|
}
|
|
|
|
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);
|
|
j.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<int>() ?? 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>() ?? string.Empty;
|
|
if (name.Length == 0)
|
|
{
|
|
Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: No name provided.", NotificationType.Warning);
|
|
continue;
|
|
}
|
|
|
|
var id = _actors.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<bool>() ?? false,
|
|
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? 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<string?>();
|
|
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<uint>() ?? 0);
|
|
|
|
var ret = new AutoDesign()
|
|
{
|
|
Design = design,
|
|
ApplicationType = applicationType & AutoDesign.Type.All,
|
|
};
|
|
return ParseConditions(setName, jObj, ret) ? ret : null;
|
|
}
|
|
|
|
private bool ParseConditions(string setName, JObject jObj, AutoDesign ret)
|
|
{
|
|
var conditions = jObj["Conditions"];
|
|
if (conditions == null)
|
|
return true;
|
|
|
|
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -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 false;
|
|
}
|
|
|
|
ret.Jobs = jobGroup;
|
|
}
|
|
|
|
ret.GearsetIndex = conditions["Gearset"]?.ToObject<short>() ?? -1;
|
|
return true;
|
|
}
|
|
|
|
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<ActorIdentifier>();
|
|
return false;
|
|
}
|
|
|
|
group = GetGroup(identifier);
|
|
return group.Length > 0;
|
|
}
|
|
|
|
private ActorIdentifier[] GetGroup(ActorIdentifier identifier)
|
|
{
|
|
if (!identifier.IsValid)
|
|
return Array.Empty<ActorIdentifier>();
|
|
|
|
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
|
|
{
|
|
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
|
|
var table = identifier.Kind switch
|
|
{
|
|
ObjectKind.BattleNpc => (IReadOnlyDictionary<NpcId, string>)manager.Data.BNpcs,
|
|
ObjectKind.EventNpc => manager.Data.ENpcs,
|
|
_ => new Dictionary<NpcId, string>(),
|
|
};
|
|
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.CreateRetainer(identifier.PlayerName,
|
|
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
|
|
? ActorIdentifier.RetainerType.Mannequin
|
|
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
|
|
},
|
|
IdentifierType.Npc => CreateNpcs(_actors, identifier),
|
|
_ => Array.Empty<ActorIdentifier>(),
|
|
};
|
|
}
|
|
|
|
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}.");
|
|
}
|
|
}
|
|
}
|