New actor assignment ui experiments

This commit is contained in:
RisaDev
2024-10-05 22:17:47 +03:00
parent 9cb105bf36
commit a7da74bb80
7 changed files with 335 additions and 122 deletions

View File

@@ -89,6 +89,7 @@ public static class ServiceManagerBuilder
services services
.AddSingleton<TemplateCombo>() .AddSingleton<TemplateCombo>()
.AddSingleton<PluginStateBlock>() .AddSingleton<PluginStateBlock>()
.AddSingleton<ActorAssignmentUi>()
.AddSingleton<SettingsTab>() .AddSingleton<SettingsTab>()
// template // template
.AddSingleton<TemplatesTab>() .AddSingleton<TemplatesTab>()

View File

@@ -21,13 +21,19 @@ namespace CustomizePlus.Profiles.Data;
/// </summary> /// </summary>
public sealed class Profile : ISavable public sealed class Profile : ISavable
{ {
public const int Version = 4;
private static int _nextGlobalId; private static int _nextGlobalId;
private readonly int _localId; private readonly int _localId;
public List<Armature> Armatures = new(); public List<Armature> Armatures = new();
[Obsolete("To be removed")]
public LowerString CharacterName { get; set; } = LowerString.Empty; public LowerString CharacterName { get; set; } = LowerString.Empty;
public ActorIdentifier Character { get; set; } = ActorIdentifier.Invalid;
public LowerString Name { get; set; } = LowerString.Empty; public LowerString Name { get; set; } = LowerString.Empty;
/// <summary> /// <summary>
@@ -35,8 +41,6 @@ public sealed class Profile : ISavable
/// </summary> /// </summary>
public bool LimitLookupToOwnedObjects { get; set; } = false; public bool LimitLookupToOwnedObjects { get; set; } = false;
public int Version { get; set; } = Constants.ConfigurationVersion;
public bool Enabled { get; set; } public bool Enabled { get; set; }
public DateTimeOffset CreationDate { get; set; } = DateTime.UtcNow; public DateTimeOffset CreationDate { get; set; } = DateTime.UtcNow;
public DateTimeOffset ModifiedDate { get; set; } = DateTime.UtcNow; public DateTimeOffset ModifiedDate { get; set; } = DateTime.UtcNow;
@@ -99,6 +103,7 @@ public sealed class Profile : ISavable
["CreationDate"] = CreationDate, ["CreationDate"] = CreationDate,
["ModifiedDate"] = ModifiedDate, ["ModifiedDate"] = ModifiedDate,
["CharacterName"] = CharacterName.Text, ["CharacterName"] = CharacterName.Text,
//["Character"] = Character.ToJson(),
["Name"] = Name.Text, ["Name"] = Name.Text,
["LimitLookupToOwnedObjects"] = LimitLookupToOwnedObjects, ["LimitLookupToOwnedObjects"] = LimitLookupToOwnedObjects,
["Enabled"] = Enabled, ["Enabled"] = Enabled,
@@ -124,62 +129,7 @@ public sealed class Profile : ISavable
#endregion #endregion
#region Deserialization //Loading is in ProfileManager
public static Profile Load(TemplateManager templateManager, JObject obj)
{
var version = obj["Version"]?.ToObject<int>() ?? 0;
return version switch
{
//Ignore everything below v4 for now
4 => LoadV4(templateManager, obj),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
private static Profile LoadV4(TemplateManager templateManager, JObject obj)
{
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
var profile = new Profile()
{
CreationDate = creationDate,
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
Name = new LowerString(obj["Name"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("Name")),
CharacterName = new LowerString(obj["CharacterName"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("CharacterName")),
LimitLookupToOwnedObjects = obj["LimitLookupToOwnedObjects"]?.ToObject<bool>() ?? throw new ArgumentNullException("LimitLookupToOwnedObjects"),
Enabled = obj["Enabled"]?.ToObject<bool>() ?? throw new ArgumentNullException("Enabled"),
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false,
Templates = new List<Template>()
};
if (profile.ModifiedDate < creationDate)
profile.ModifiedDate = creationDate;
if (obj["Templates"] is not JArray templateArray)
return profile;
foreach (var templateObj in templateArray)
{
if (templateObj is not JObject templateObjCast)
{
//todo: warning
continue;
}
var templateId = templateObjCast["TemplateId"]?.ToObject<Guid>();
if (templateId == null)
continue; //todo: error
var template = templateManager.GetTemplate((Guid)templateId);
if (template != null)
profile.Templates.Add(template);
}
return profile;
}
#endregion
#region ISavable #region ISavable

View File

@@ -0,0 +1,155 @@
using CustomizePlus.Profiles.Data;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Templates.Data;
using CustomizePlus.Templates;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.String;
using Penumbra.GameData.Structs;
namespace CustomizePlus.Profiles;
public partial class ProfileManager : IDisposable
{
public void LoadProfiles()
{
_logger.Information("Loading profiles...");
//todo: hot reload was not tested
//save temp profiles
var temporaryProfiles = Profiles.Where(x => x.IsTemporary).ToList();
Profiles.Clear();
List<(Profile, string)> invalidNames = new();
foreach (var file in _saveService.FileNames.Profiles())
{
_logger.Debug($"Reading profile {file.FullName}");
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var profile = LoadIndividualProfile(data);
if (profile.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((profile, file.FullName));
if (Profiles.Any(f => f.UniqueId == profile.UniqueId))
throw new Exception($"ID {profile.UniqueId} was not unique.");
Profiles.Add(profile);
}
catch (Exception ex)
{
_logger.Error($"Could not load profile, skipped:\n{ex}");
//++skipped;
}
}
foreach (var profile in Profiles)
{
//This will solve any issues if file on disk was manually edited and we have more than a single active profile
if (profile.Enabled)
SetEnabled(profile, true, true);
if (_configuration.DefaultProfile == profile.UniqueId)
DefaultProfile = profile;
}
//insert temp profiles back into profile list
if (temporaryProfiles.Count > 0)
{
Profiles.AddRange(temporaryProfiles);
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
}
var failed = MoveInvalidNames(invalidNames);
if (invalidNames.Count > 0)
_logger.Information(
$"Moved {invalidNames.Count - failed} profiles to correct names.{(failed > 0 ? $" Failed to move {failed} profiles to correct names." : string.Empty)}");
_logger.Information("Profiles load complete");
_event.Invoke(ProfileChanged.Type.ReloadedAll, null, null);
}
private Profile LoadIndividualProfile(JObject obj)
{
var version = obj["Version"]?.ToObject<int>() ?? 0;
return version switch
{
//Ignore everything below v4
// 4 => LoadV4(obj),
// 5 => LoadV5(obj),
4 => LoadV5(obj),
_ => throw new Exception("The profile to be loaded has no valid Version."),
};
}
private Profile LoadV4(JObject obj)
{
var characterName = new LowerString(obj["CharacterName"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("CharacterName"));
ByteString.FromString(characterName, out var nameByteString);
var character = _actorManager.CreatePlayer(nameByteString, WorldId.AnyWorld); //todo: detect type
obj["Character"] = character.ToJson();
var profile = LoadV5(obj);
profile.ModifiedDate = DateTimeOffset.UtcNow;
_saveService.ImmediateSave(profile);
return profile;
}
private Profile LoadV5(JObject obj)
{
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
/*var character = _actorManager.FromJson(obj["Character"] as JObject);
if (!character.IsValid)
throw new ArgumentException("Character");*/
var profile = new Profile()
{
CreationDate = creationDate,
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
Name = new LowerString(obj["Name"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("Name")),
//Character = character,
CharacterName = new LowerString(obj["CharacterName"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("CharacterName")),
LimitLookupToOwnedObjects = obj["LimitLookupToOwnedObjects"]?.ToObject<bool>() ?? throw new ArgumentNullException("LimitLookupToOwnedObjects"),
Enabled = obj["Enabled"]?.ToObject<bool>() ?? throw new ArgumentNullException("Enabled"),
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false,
Templates = new List<Template>()
};
if (profile.ModifiedDate < creationDate)
profile.ModifiedDate = creationDate;
if (obj["Templates"] is not JArray templateArray)
return profile;
foreach (var templateObj in templateArray)
{
if (templateObj is not JObject templateObjCast)
{
//todo: warning
continue;
}
var templateId = templateObjCast["TemplateId"]?.ToObject<Guid>();
if (templateId == null)
continue; //todo: error
var template = _templateManager.GetTemplate((Guid)templateId);
if (template != null)
profile.Templates.Add(template);
}
return profile;
}
}

View File

@@ -34,7 +34,7 @@ namespace CustomizePlus.Profiles;
/// <summary> /// <summary>
/// Container class for administrating <see cref="Profile" />s during runtime. /// Container class for administrating <see cref="Profile" />s during runtime.
/// </summary> /// </summary>
public class ProfileManager : IDisposable public partial class ProfileManager : IDisposable
{ {
private readonly TemplateManager _templateManager; private readonly TemplateManager _templateManager;
private readonly TemplateEditorManager _templateEditorManager; private readonly TemplateEditorManager _templateEditorManager;
@@ -93,65 +93,6 @@ public class ProfileManager : IDisposable
_templateChangedEvent.Unsubscribe(OnTemplateChange); _templateChangedEvent.Unsubscribe(OnTemplateChange);
} }
public void LoadProfiles()
{
_logger.Information("Loading profiles...");
//todo: hot reload was not tested
//save temp profiles
var temporaryProfiles = Profiles.Where(x => x.IsTemporary).ToList();
Profiles.Clear();
List<(Profile, string)> invalidNames = new();
foreach (var file in _saveService.FileNames.Profiles())
{
_logger.Debug($"Reading profile {file.FullName}");
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var profile = Profile.Load(_templateManager, data);
if (profile.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((profile, file.FullName));
if (Profiles.Any(f => f.UniqueId == profile.UniqueId))
throw new Exception($"ID {profile.UniqueId} was not unique.");
Profiles.Add(profile);
}
catch (Exception ex)
{
_logger.Error($"Could not load profile, skipped:\n{ex}");
//++skipped;
}
}
foreach (var profile in Profiles)
{
//This will solve any issues if file on disk was manually edited and we have more than a single active profile
if (profile.Enabled)
SetEnabled(profile, true, true);
if (_configuration.DefaultProfile == profile.UniqueId)
DefaultProfile = profile;
}
//insert temp profiles back into profile list
if (temporaryProfiles.Count > 0)
{
Profiles.AddRange(temporaryProfiles);
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
}
var failed = MoveInvalidNames(invalidNames);
if (invalidNames.Count > 0)
_logger.Information(
$"Moved {invalidNames.Count - failed} profiles to correct names.{(failed > 0 ? $" Failed to move {failed} profiles to correct names." : string.Empty)}");
_logger.Information("Profiles load complete");
_event.Invoke(ProfileChanged.Type.ReloadedAll, null, null);
}
/// <summary> /// <summary>
/// Main rendering function, called from rendering hook after calling ArmatureManager.OnRender /// Main rendering function, called from rendering hook after calling ArmatureManager.OnRender
/// </summary> /// </summary>

View File

@@ -16,9 +16,10 @@ namespace CustomizePlus.Templates.Data;
/// </summary> /// </summary>
public sealed class Template : ISavable public sealed class Template : ISavable
{ {
public const int Version = Constants.ConfigurationVersion;
public LowerString Name { get; internal set; } = "Template"; public LowerString Name { get; internal set; } = "Template";
public int Version { get; internal set; } = Constants.ConfigurationVersion;
public DateTimeOffset CreationDate { get; internal set; } = DateTime.UtcNow; public DateTimeOffset CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTimeOffset ModifiedDate { get; internal set; } = DateTime.UtcNow; public DateTimeOffset ModifiedDate { get; internal set; } = DateTime.UtcNow;

View File

@@ -0,0 +1,155 @@
using Dalamud.Game.ClientState.Objects.Enums;
using ImGuiNET;
using OtterGui.Custom;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui;
using Penumbra.GameData.Interop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.UI.Windows.Controls;
public class ActorAssignmentUi : IDisposable
{
private readonly ActorManager _actorManager;
private WorldCombo _worldCombo = null!;
private NpcCombo _mountCombo = null!;
private NpcCombo _companionCombo = null!;
private NpcCombo _ornamentCombo = null!;
private NpcCombo _bnpcCombo = null!;
private NpcCombo _enpcCombo = null!;
private bool _ready;
private string _newCharacterName = string.Empty;
private ObjectKind _newKind = ObjectKind.BattleNpc;
public ActorAssignmentUi(ActorManager actorManager)
{
_actorManager = actorManager;
_actorManager.Awaiter.ContinueWith(_ => SetupCombos(), TaskScheduler.Default);
}
public void DrawWorldCombo(float width)
{
if (_ready && _worldCombo.Draw(width))
UpdateIdentifiersInternal();
}
public void DrawPlayerInput(float width)
{
if (!_ready)
return;
ImGui.SetNextItemWidth(width);
if (ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32))
UpdateIdentifiersInternal();
}
public void DrawObjectKindCombo(float width)
{
if (_ready && IndividualHelpers.DrawObjectKindCombo(width, _newKind, out _newKind, ObjectKinds))
UpdateIdentifiersInternal();
}
public void DrawNpcInput(float width)
{
if (!_ready)
return;
var combo = GetNpcCombo(_newKind);
if (combo.Draw(width))
UpdateIdentifiersInternal();
}
private static readonly IReadOnlyList<ObjectKind> ObjectKinds = new[]
{
ObjectKind.BattleNpc,
ObjectKind.EventNpc,
ObjectKind.Companion,
ObjectKind.MountType,
ObjectKind.Ornament,
};
private NpcCombo GetNpcCombo(ObjectKind kind)
=> kind switch
{
ObjectKind.BattleNpc => _bnpcCombo,
ObjectKind.EventNpc => _enpcCombo,
ObjectKind.MountType => _mountCombo,
ObjectKind.Companion => _companionCombo,
ObjectKind.Ornament => _ornamentCombo,
_ => throw new NotImplementedException(),
};
private void SetupCombos()
{
_worldCombo = new WorldCombo(_actorManager.Data.Worlds, Plugin.Logger);
_mountCombo = new NpcCombo("##mountCombo", _actorManager.Data.Mounts, Plugin.Logger);
_companionCombo = new NpcCombo("##companionCombo", _actorManager.Data.Companions, Plugin.Logger);
_ornamentCombo = new NpcCombo("##ornamentCombo", _actorManager.Data.Ornaments, Plugin.Logger);
_bnpcCombo = new NpcCombo("##bnpcCombo", _actorManager.Data.BNpcs, Plugin.Logger);
_enpcCombo = new NpcCombo("##enpcCombo", _actorManager.Data.ENpcs, Plugin.Logger);
_ready = true;
}
private void UpdateIdentifiersInternal()
{
/* var combo = GetNpcCombo(_newKind);
PlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName,
_worldCombo.CurrentSelection.Key, ObjectKind.None, [], out _playerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
RetainerTooltip =
_collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, [],
out _retainerIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
if (combo.CurrentSelection.Ids != null)
{
NpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind,
combo.CurrentSelection.Ids, out _npcIdentifiers) switch
{
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
OwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName,
_worldCombo.CurrentSelection.Key, _newKind,
combo.CurrentSelection.Ids, out _ownedIdentifiers) switch
{
_ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty,
IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid,
IndividualCollections.AddResult.AlreadySet => AlreadyAssigned,
_ => string.Empty,
};
}
else
{
NpcTooltip = NewNpcTooltipEmpty;
OwnedTooltip = NewNpcTooltipEmpty;
_npcIdentifiers = [];
_ownedIdentifiers = [];
}*/
}
public void Dispose()
{
//throw new NotImplementedException();
}
}

View File

@@ -23,6 +23,7 @@ public class ProfilePanel
private readonly PluginConfiguration _configuration; private readonly PluginConfiguration _configuration;
private readonly TemplateCombo _templateCombo; private readonly TemplateCombo _templateCombo;
private readonly TemplateEditorManager _templateEditorManager; private readonly TemplateEditorManager _templateEditorManager;
private readonly ActorAssignmentUi _actorAssignmentUi;
private readonly TemplateEditorEvent _templateEditorEvent; private readonly TemplateEditorEvent _templateEditorEvent;
private string? _newName; private string? _newName;
@@ -42,6 +43,7 @@ public class ProfilePanel
PluginConfiguration configuration, PluginConfiguration configuration,
TemplateCombo templateCombo, TemplateCombo templateCombo,
TemplateEditorManager templateEditorManager, TemplateEditorManager templateEditorManager,
ActorAssignmentUi actorAssignmentUi,
TemplateEditorEvent templateEditorEvent) TemplateEditorEvent templateEditorEvent)
{ {
_selector = selector; _selector = selector;
@@ -49,6 +51,7 @@ public class ProfilePanel
_configuration = configuration; _configuration = configuration;
_templateCombo = templateCombo; _templateCombo = templateCombo;
_templateEditorManager = templateEditorManager; _templateEditorManager = templateEditorManager;
_actorAssignmentUi = actorAssignmentUi;
_templateEditorEvent = templateEditorEvent; _templateEditorEvent = templateEditorEvent;
} }
@@ -211,7 +214,7 @@ public class ProfilePanel
ImGui.TableNextRow(); ImGui.TableNextRow();
ImGuiUtil.DrawFrameColumn("Character Name"); ImGuiUtil.DrawFrameColumn("Character");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0); width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
name = _newCharacterName ?? _selector.Selected!.CharacterName; name = _newCharacterName ?? _selector.Selected!.CharacterName;
@@ -221,7 +224,7 @@ public class ProfilePanel
{ {
if (!_selector.IncognitoMode) if (!_selector.IncognitoMode)
{ {
if (ImGui.InputText("##CharacterName", ref name, 128)) /*if (ImGui.InputText("##CharacterName", ref name, 128))
{ {
_newCharacterName = name; _newCharacterName = name;
_changedProfile = _selector.Selected; _changedProfile = _selector.Selected;
@@ -232,7 +235,14 @@ public class ProfilePanel
_manager.ChangeCharacterName(_changedProfile, name); _manager.ChangeCharacterName(_changedProfile, name);
_newCharacterName = null; _newCharacterName = null;
_changedProfile = null; _changedProfile = null;
} }*/
_actorAssignmentUi.DrawWorldCombo(width.X / 2);
ImGui.SameLine();
_actorAssignmentUi.DrawPlayerInput(width.X);
_actorAssignmentUi.DrawObjectKindCombo(width.X / 2);
ImGui.SameLine();
_actorAssignmentUi.DrawNpcInput(width.X);
} }
else else
ImGui.TextUnformatted("Incognito active"); ImGui.TextUnformatted("Incognito active");