Start ModManager dissemination....

This commit is contained in:
Ottermandias 2023-03-24 00:28:36 +01:00
parent 174e640c45
commit c8415e3079
34 changed files with 1305 additions and 1542 deletions

View file

@ -38,7 +38,7 @@ public class IpcTester : IDisposable
private readonly ModSettings _modSettings;
private readonly Temporary _temporary;
public IpcTester( DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders )
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager)
{
_ipcProviders = ipcProviders;
_pluginState = new PluginState(pi);
@ -51,7 +51,7 @@ public class IpcTester : IDisposable
_meta = new Meta(pi);
_mods = new Mods(pi);
_modSettings = new ModSettings(pi);
_temporary = new Temporary( pi );
_temporary = new Temporary(pi, modManager);
UnsubscribeEvents();
}
@ -183,15 +183,11 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Plugin State");
if (!_)
{
return;
}
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
void DrawList(string label, string text, List<DateTimeOffset> list)
{
@ -204,12 +200,10 @@ public class IpcTester : IDisposable
{
ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture));
if (list.Count > 1 && ImGui.IsItemHovered())
{
ImGui.SetTooltip(string.Join("\n",
list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture))));
}
}
}
DrawList(Ipc.Initialized.Label, "Last Initialized", _initializedList);
DrawList(Ipc.Disposed.Label, "Last Disposed", _disposedList);
@ -252,15 +246,11 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Configuration");
if (!_)
{
return;
}
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.GetModDirectory.Label, "Current Mod Directory");
ImGui.TextUnformatted(Ipc.GetModDirectory.Subscriber(_pi).Invoke());
@ -290,11 +280,9 @@ public class IpcTester : IDisposable
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{
ImGui.CloseCurrentPopup();
}
}
}
private void UpdateModDirectoryChanged(string path, bool valid)
=> (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now);
@ -331,30 +319,22 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("UI");
if (!_)
{
return;
}
using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString()))
{
if (combo)
{
foreach (var val in Enum.GetValues<TabType>())
{
if (ImGui.Selectable(val.ToString(), _selectTab == val))
{
_selectTab = val;
}
}
}
}
ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.PostSettingsDraw.Label, "Last Drawn Mod");
ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None");
@ -363,14 +343,10 @@ public class IpcTester : IDisposable
if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip))
{
if (_subscribedToTooltip)
{
Tooltip.Enable();
}
else
{
Tooltip.Disable();
}
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastHovered);
@ -379,32 +355,24 @@ public class IpcTester : IDisposable
if (ImGui.Checkbox("##click", ref _subscribedToClick))
{
if (_subscribedToClick)
{
Click.Enable();
}
else
{
Click.Disable();
}
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastClicked);
DrawIntro(Ipc.OpenMainWindow.Label, "Open Mod Window");
if (ImGui.Button("Open##window"))
{
_ec = Ipc.OpenMainWindow.Subscriber(_pi).Invoke(_selectTab, _modName, _modName);
}
ImGui.SameLine();
ImGui.TextUnformatted(_ec.ToString());
DrawIntro(Ipc.CloseMainWindow.Label, "Close Mod Window");
if (ImGui.Button("Close##window"))
{
Ipc.CloseMainWindow.Subscriber(_pi).Invoke();
}
}
private void UpdateLastDrawnMod(string name)
=> (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now);
@ -440,50 +408,36 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Redrawing");
if (!_)
{
return;
}
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.RedrawObjectByName.Label, "Redraw by Name");
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32);
ImGui.SameLine();
if (ImGui.Button("Redraw##Name"))
{
Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw);
}
DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character");
if (ImGui.Button("Redraw##pc") && DalamudServices.SClientState.LocalPlayer != null)
{
Ipc.RedrawObject.Subscriber(_pi).Invoke(DalamudServices.SClientState.LocalPlayer, RedrawType.Redraw);
}
DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index");
var tmp = _redrawIndex;
ImGui.SetNextItemWidth(100 * UiHelpers.Scale);
if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.SObjects.Length))
{
_redrawIndex = Math.Clamp(tmp, 0, DalamudServices.SObjects.Length);
}
ImGui.SameLine();
if (ImGui.Button("Redraw##Index"))
{
Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw);
}
DrawIntro(Ipc.RedrawAll.Label, "Redraw All");
if (ImGui.Button("Redraw##All"))
{
Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw);
}
DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:");
ImGui.TextUnformatted(_lastRedrawnString);
@ -491,10 +445,11 @@ public class IpcTester : IDisposable
private void SetLastRedrawn(IntPtr address, int index)
{
if( index < 0 || index > DalamudServices.SObjects.Length || address == IntPtr.Zero || DalamudServices.SObjects[ index ]?.Address != address )
{
if (index < 0
|| index > DalamudServices.SObjects.Length
|| address == IntPtr.Zero
|| DalamudServices.SObjects[index]?.Address != address)
_lastRedrawnString = "Invalid";
}
_lastRedrawnString = $"{DalamudServices.SObjects[index]!.Name} (0x{address:X}, {index})";
}
@ -531,24 +486,19 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Game State");
if (!_)
{
return;
}
if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16,
ImGuiInputTextFlags.CharsHexadecimal))
{
_currentDrawObject = IntPtr.TryParse( _currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var tmp )
_currentDrawObject = IntPtr.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out var tmp)
? tmp
: IntPtr.Zero;
}
ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.GetDrawObjectInfo.Label, "Draw Object Info");
if (_currentDrawObject == IntPtr.Zero)
@ -566,19 +516,15 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.CreatingCharacterBase.Label, "Last Drawobject created");
if (_lastCreatedGameObjectTime < DateTimeOffset.Now)
{
ImGui.TextUnformatted(_lastCreatedDrawObject != IntPtr.Zero
? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
: $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}");
}
DrawIntro(Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved");
if (_lastResolvedGamePathTime < DateTimeOffset.Now)
{
ImGui.TextUnformatted(
$"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}");
}
}
private void UpdateLastCreated(IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4)
{
@ -626,9 +572,7 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Resolving");
if (!_)
{
return;
}
ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength);
ImGui.InputTextWithHint("##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32);
@ -637,39 +581,27 @@ public class IpcTester : IDisposable
ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve");
if (_currentResolvePath.Length != 0)
{
ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(_pi).Invoke(_currentResolvePath));
}
DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve");
if (_currentResolvePath.Length != 0)
{
ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(_pi).Invoke(_currentResolvePath));
}
DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve");
if (_currentResolvePath.Length != 0)
{
ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(_pi).Invoke(_currentResolvePath));
}
DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve");
if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0)
{
ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentResolveCharacter));
}
DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve");
if (_currentResolvePath.Length != 0)
{
ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentReverseIdx));
}
DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths");
if (_currentReversePath.Length > 0)
@ -679,11 +611,9 @@ public class IpcTester : IDisposable
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
{
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
}
DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)");
if (_currentReversePath.Length > 0)
@ -693,11 +623,9 @@ public class IpcTester : IDisposable
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
{
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
}
DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)");
if (_currentReversePath.Length > 0)
@ -707,30 +635,34 @@ public class IpcTester : IDisposable
{
ImGui.TextUnformatted(list[0]);
if (list.Length > 1 && ImGui.IsItemHovered())
{
ImGui.SetTooltip(string.Join("\n", list.Skip(1)));
}
}
}
DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)");
if (_currentResolvePath.Length > 0 || _currentReversePath.Length > 0)
{
var forwardArray = _currentResolvePath.Length > 0 ? new[] { _currentResolvePath } : Array.Empty< string >();
var reverseArray = _currentReversePath.Length > 0 ? new[] { _currentReversePath } : Array.Empty< string >();
var forwardArray = _currentResolvePath.Length > 0
? new[]
{
_currentResolvePath,
}
: Array.Empty<string>();
var reverseArray = _currentReversePath.Length > 0
? new[]
{
_currentReversePath,
}
: Array.Empty<string>();
var ret = Ipc.ResolvePlayerPaths.Subscriber(_pi).Invoke(forwardArray, reverseArray);
var text = string.Empty;
if (ret.Item1.Length > 0)
{
if (ret.Item2.Length > 0)
{
text = $"Forward: {ret.Item1[0]} | Reverse: {string.Join("; ", ret.Item2[0])}.";
}
else
{
text = $"Forward: {ret.Item1[0]}.";
}
}
else if (ret.Item2.Length > 0)
{
text = $"Reverse: {string.Join("; ", ret.Item2[0])}.";
@ -765,9 +697,7 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Collections");
if (!_)
{
return;
}
ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName());
ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0);
@ -778,15 +708,11 @@ public class IpcTester : IDisposable
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro("Last Return Code", _returnCode.ToString());
if (_oldCollection != null)
{
ImGui.TextUnformatted(_oldCollection.Length == 0 ? "Created" : _oldCollection);
}
DrawIntro(Ipc.GetCurrentCollectionName.Label, "Current Collection");
ImGui.TextUnformatted(Ipc.GetCurrentCollectionName.Subscriber(_pi).Invoke());
@ -813,9 +739,8 @@ public class IpcTester : IDisposable
ImGui.TextUnformatted(name.Length == 0 ? "Unassigned" : name);
DrawIntro(Ipc.SetCollectionForType.Label, "Set Special Collection");
if (ImGui.Button("Set##TypeCollection"))
{
( _returnCode, _oldCollection ) = Ipc.SetCollectionForType.Subscriber( _pi ).Invoke( _type, _collectionName, _allowCreation, _allowDeletion );
}
(_returnCode, _oldCollection) =
Ipc.SetCollectionForType.Subscriber(_pi).Invoke(_type, _collectionName, _allowCreation, _allowDeletion);
DrawIntro(Ipc.GetCollectionForObject.Label, "Get Object Collection");
(var valid, var individual, name) = Ipc.GetCollectionForObject.Subscriber(_pi).Invoke(_objectIdx);
@ -823,14 +748,11 @@ public class IpcTester : IDisposable
$"{(valid ? "Valid" : "Invalid")} Object, {(name.Length == 0 ? "Unassigned" : name)}{(individual ? " (Individual Assignment)" : string.Empty)}");
DrawIntro(Ipc.SetCollectionForObject.Label, "Set Object Collection");
if (ImGui.Button("Set##ObjectCollection"))
{
( _returnCode, _oldCollection ) = Ipc.SetCollectionForObject.Subscriber( _pi ).Invoke( _objectIdx, _collectionName, _allowCreation, _allowDeletion );
}
(_returnCode, _oldCollection) = Ipc.SetCollectionForObject.Subscriber(_pi)
.Invoke(_objectIdx, _collectionName, _allowCreation, _allowDeletion);
if (_returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty())
{
_oldCollection = null;
}
DrawIntro(Ipc.GetChangedItems.Label, "Changed Item List");
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
@ -851,41 +773,29 @@ public class IpcTester : IDisposable
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Changed Item List");
if (!p)
{
return;
}
foreach (var item in _changedItems)
{
ImGui.TextUnformatted(item.Key);
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{
ImGui.CloseCurrentPopup();
}
}
private void DrawCollectionPopup()
{
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Collections");
if (!p)
{
return;
}
foreach (var collection in _collections)
{
ImGui.TextUnformatted(collection);
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{
ImGui.CloseCurrentPopup();
}
}
}
private class Meta
{
@ -901,17 +811,13 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Meta");
if (!_)
{
return;
}
ImGui.InputTextWithHint("##characterName", "Character Name...", ref _characterName, 64);
ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.GetMetaManipulations.Label, "Meta Manipulations");
if (ImGui.Button("Copy to Clipboard"))
@ -986,18 +892,14 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Mods");
if (!_)
{
return;
}
ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100);
ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100);
ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100);
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro(Ipc.GetMods.Label, "Mods");
if (ImGui.Button("Get##Mods"))
@ -1008,27 +910,21 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.ReloadMod.Label, "Reload Mod");
if (ImGui.Button("Reload"))
{
_lastReloadEc = Ipc.ReloadMod.Subscriber(_pi).Invoke(_modDirectory, _modName);
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastReloadEc.ToString());
DrawIntro(Ipc.AddMod.Label, "Add Mod");
if (ImGui.Button("Add"))
{
_lastAddEc = Ipc.AddMod.Subscriber(_pi).Invoke(_modDirectory);
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastAddEc.ToString());
DrawIntro(Ipc.DeleteMod.Label, "Delete Mod");
if (ImGui.Button("Delete"))
{
_lastDeleteEc = Ipc.DeleteMod.Subscriber(_pi).Invoke(_modDirectory, _modName);
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastDeleteEc.ToString());
@ -1039,30 +935,22 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.SetModPath.Label, "Set Path");
if (ImGui.Button("Set"))
{
_lastSetPathEc = Ipc.SetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName, _pathInput);
}
ImGui.SameLine();
ImGui.TextUnformatted(_lastSetPathEc.ToString());
DrawIntro(Ipc.ModDeleted.Label, "Last Mod Deleted");
if (_lastDeletedModTime > DateTimeOffset.UnixEpoch)
{
ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}");
}
DrawIntro(Ipc.ModAdded.Label, "Last Mod Added");
if (_lastAddedModTime > DateTimeOffset.UnixEpoch)
{
ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}");
}
DrawIntro(Ipc.ModMoved.Label, "Last Mod Moved");
if (_lastMovedModTime > DateTimeOffset.UnixEpoch)
{
ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}");
}
DrawModsPopup();
}
@ -1072,21 +960,15 @@ public class IpcTester : IDisposable
ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500));
using var p = ImRaii.Popup("Mods");
if (!p)
{
return;
}
foreach (var (modDir, modName) in _mods)
{
ImGui.TextUnformatted($"{modDir}: {modName}");
}
if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused())
{
ImGui.CloseCurrentPopup();
}
}
}
private class ModSettings
{
@ -1120,9 +1002,7 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Mod Settings");
if (!_)
{
return;
}
ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100);
ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100);
@ -1131,9 +1011,7 @@ public class IpcTester : IDisposable
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro("Last Error", _lastSettingsError.ToString());
DrawIntro(Ipc.ModSettingChanged.Label, "Last Mod Setting Changed");
@ -1151,7 +1029,8 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.GetCurrentModSettings.Label, "Get Current Settings");
if (ImGui.Button("Get##Current"))
{
var ret = Ipc.GetCurrentModSettings.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance );
var ret = Ipc.GetCurrentModSettings.Subscriber(_pi)
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance);
_lastSettingsError = ret.Item1;
if (ret.Item1 == PenumbraApiEc.Success)
{
@ -1170,40 +1049,33 @@ public class IpcTester : IDisposable
ImGui.Checkbox("##inherit", ref _settingsInherit);
ImGui.SameLine();
if (ImGui.Button("Set##Inherit"))
{
_lastSettingsError = Ipc.TryInheritMod.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit );
}
_lastSettingsError = Ipc.TryInheritMod.Subscriber(_pi)
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit);
DrawIntro(Ipc.TrySetMod.Label, "Set Enabled");
ImGui.Checkbox("##enabled", ref _settingsEnabled);
ImGui.SameLine();
if (ImGui.Button("Set##Enabled"))
{
_lastSettingsError = Ipc.TrySetMod.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled );
}
_lastSettingsError = Ipc.TrySetMod.Subscriber(_pi)
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled);
DrawIntro(Ipc.TrySetModPriority.Label, "Set Priority");
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
ImGui.DragInt("##Priority", ref _settingsPriority);
ImGui.SameLine();
if (ImGui.Button("Set##Priority"))
{
_lastSettingsError = Ipc.TrySetModPriority.Subscriber( _pi ).Invoke( _settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority );
}
_lastSettingsError = Ipc.TrySetModPriority.Subscriber(_pi)
.Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority);
DrawIntro(Ipc.CopyModSettings.Label, "Copy Mod Settings");
if (ImGui.Button("Copy Settings"))
{
_lastSettingsError = Ipc.CopyModSettings.Subscriber(_pi).Invoke(_settingsCollection, _settingsModDirectory, _settingsModName);
}
ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection.");
DrawIntro(Ipc.TrySetModSetting.Label, "Set Setting(s)");
if (_availableSettings == null)
{
return;
}
foreach (var (group, (list, type)) in _availableSettings)
{
@ -1218,48 +1090,36 @@ public class IpcTester : IDisposable
{
current = new List<string>();
if (_currentSettings != null)
{
_currentSettings[group] = current;
}
}
ImGui.SetNextItemWidth(200 * UiHelpers.Scale);
using (var c = ImRaii.Combo("##group", preview))
{
if (c)
{
foreach (var s in list)
{
var contained = current.Contains(s);
if (ImGui.Checkbox(s, ref contained))
{
if (contained)
{
current.Add(s);
}
else
{
current.Remove(s);
}
}
}
}
}
ImGui.SameLine();
if (ImGui.Button("Set##setting"))
{
if (type == GroupType.Single)
{
_lastSettingsError = Ipc.TrySetModSetting.Subscriber(_pi).Invoke(_settingsCollection,
_settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[0] : string.Empty);
}
else
{
_lastSettingsError = Ipc.TrySetModSettings.Subscriber(_pi).Invoke(_settingsCollection,
_settingsModDirectory, _settingsModName, group, current.ToArray());
}
}
ImGui.SameLine();
ImGui.TextUnformatted(group);
@ -1278,10 +1138,14 @@ public class IpcTester : IDisposable
private class Temporary
{
public readonly DalamudPluginInterface _pi;
private readonly DalamudPluginInterface _pi;
private readonly Mod.Manager _modManager;
public Temporary( DalamudPluginInterface pi )
=> _pi = pi;
public Temporary(DalamudPluginInterface pi, Mod.Manager modManager)
{
_pi = pi;
_modManager = modManager;
}
public string LastCreatedCollectionName = string.Empty;
@ -1299,9 +1163,7 @@ public class IpcTester : IDisposable
{
using var _ = ImRaii.TreeNode("Temporary");
if (!_)
{
return;
}
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
ImGui.InputTextWithHint("##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32);
@ -1315,53 +1177,41 @@ public class IpcTester : IDisposable
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
if (!table)
{
return;
}
DrawIntro("Last Error", _lastTempError.ToString());
DrawIntro("Last Created Collection", LastCreatedCollectionName);
DrawIntro(Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection");
#pragma warning disable 0612
if (ImGui.Button("Create##Collection"))
{
( _lastTempError, LastCreatedCollectionName ) = Ipc.CreateTemporaryCollection.Subscriber( _pi ).Invoke( _tempCollectionName, _tempCharacterName, _forceOverwrite );
}
(_lastTempError, LastCreatedCollectionName) = Ipc.CreateTemporaryCollection.Subscriber(_pi)
.Invoke(_tempCollectionName, _tempCharacterName, _forceOverwrite);
DrawIntro(Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection");
if (ImGui.Button("Create##NamedCollection"))
{
_lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName);
}
DrawIntro(Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character");
if (ImGui.Button("Delete##Collection"))
{
_lastTempError = Ipc.RemoveTemporaryCollection.Subscriber(_pi).Invoke(_tempCharacterName);
}
#pragma warning restore 0612
DrawIntro(Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection");
if (ImGui.Button("Delete##NamedCollection"))
{
_lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber(_pi).Invoke(_tempCollectionName);
}
DrawIntro(Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection");
if (ImGui.Button("Assign##NamedCollection"))
{
_lastTempError = Ipc.AssignTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName, _tempActorIndex, _forceOverwrite);
}
DrawIntro(Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection");
if (ImGui.Button("Add##Mod"))
{
_lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName,
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
}
DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection");
if( ImGuiUtil.DrawDisabledButton( "Copy##Collection", Vector2.Zero, "Copies the effective list from the collection named in Temporary Mod Name...",
if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
"Copies the effective list from the collection named in Temporary Mod Name...",
!Penumbra.CollectionManager.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
@ -1373,51 +1223,43 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections");
if (ImGui.Button("Add##All"))
{
_lastTempError = Ipc.AddTemporaryModAll.Subscriber( _pi ).Invoke( _tempModName, new Dictionary< string, string > { { _tempGamePath, _tempFilePath } },
_lastTempError = Ipc.AddTemporaryModAll.Subscriber(_pi).Invoke(_tempModName,
new Dictionary<string, string> { { _tempGamePath, _tempFilePath } },
_tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue);
}
DrawIntro(Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection");
if (ImGui.Button("Remove##Mod"))
{
_lastTempError = Ipc.RemoveTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, int.MaxValue);
}
DrawIntro(Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections");
if (ImGui.Button("Remove##ModAll"))
{
_lastTempError = Ipc.RemoveTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, int.MaxValue);
}
}
public void DrawCollections()
{
using var collTree = ImRaii.TreeNode("Collections##TempCollections");
if (!collTree)
{
return;
}
using var table = ImRaii.Table("##collTree", 5);
if (!table)
{
return;
}
foreach (var collection in Penumbra.TempCollections.Values)
{
ImGui.TableNextColumn();
var character = Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown";
var character = Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
if (ImGui.Button($"Save##{collection.Name}"))
{
Mod.TemporaryMod.SaveTempCollection( collection, character );
}
Mod.TemporaryMod.SaveTempCollection(_modManager, collection, character);
ImGuiUtil.DrawTableColumn(collection.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) );
ImGuiUtil.DrawTableColumn(string.Join(", ",
Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
}
}
@ -1425,9 +1267,7 @@ public class IpcTester : IDisposable
{
using var modTree = ImRaii.TreeNode("Mods##TempMods");
if (!modTree)
{
return;
}
using var table = ImRaii.Table("##modTree", 5);
@ -1447,10 +1287,8 @@ public class IpcTester : IDisposable
{
using var tt = ImRaii.Tooltip();
foreach (var (path, file) in mod.Default.Files)
{
ImGui.TextUnformatted($"{path} -> {file}");
}
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(mod.TotalManipulations.ToString());
@ -1458,21 +1296,17 @@ public class IpcTester : IDisposable
{
using var tt = ImRaii.Tooltip();
foreach (var manip in mod.Default.Manipulations)
{
ImGui.TextUnformatted(manip.ToString());
}
}
}
}
if (table)
{
PrintList("All", Penumbra.TempMods.ModsForAllCollections);
foreach (var (collection, list) in Penumbra.TempMods.Mods)
{
PrintList(collection.Name, list);
}
}
}
}
}

View file

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Mods;
namespace Penumbra.Api;
@ -111,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod;
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api )
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager )
{
Api = api;
@ -219,7 +220,7 @@ public class PenumbraIpcProviders : IDisposable
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider( pi, Api.RemoveTemporaryModAll );
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider( pi, Api.RemoveTemporaryMod );
Tester = new IpcTester( pi, this );
Tester = new IpcTester( pi, this, modManager );
Initialized.Invoke();
}

View file

@ -17,7 +17,7 @@ namespace Penumbra.Collections;
public partial class ModCollection
{
public sealed partial class Manager : ISaveable
public sealed partial class Manager : ISavable
{
public const int Version = 1;

View file

@ -11,7 +11,7 @@ using Penumbra.Util;
namespace Penumbra.Collections;
// File operations like saving, loading and deleting for a collection.
public partial class ModCollection : ISaveable
public partial class ModCollection : ISavable
{
// Since inheritances depend on other collections existing,
// we return them as a list to be applied after reading all collections.

View file

@ -14,18 +14,16 @@ using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.Util;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
[Serializable]
public class Configuration : IPluginConfiguration
public class Configuration : IPluginConfiguration, ISavable
{
[JsonIgnore]
private readonly string _fileName;
[JsonIgnore]
private readonly FrameworkManager _framework;
private readonly SaveService _saveService;
public int Version { get; set; } = Constants.CurrentVersion;
@ -101,14 +99,13 @@ public class Configuration : IPluginConfiguration
/// Load the current configuration.
/// Includes adding new colors and migrating from old versions.
/// </summary>
public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework)
public Configuration(FilenameService fileNames, ConfigMigrationService migrator, SaveService saveService)
{
_fileName = fileNames.ConfigFile;
_framework = framework;
Load(migrator);
_saveService = saveService;
Load(fileNames, migrator);
}
public void Load(ConfigMigrationService migrator)
public void Load(FilenameService fileNames, ConfigMigrationService migrator)
{
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{
@ -117,9 +114,9 @@ public class Configuration : IPluginConfiguration
errorArgs.ErrorContext.Handled = true;
}
if (File.Exists(_fileName))
if (File.Exists(fileNames.ConfigFile))
{
var text = File.ReadAllText(_fileName);
var text = File.ReadAllText(fileNames.ConfigFile);
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
{
Error = HandleDeserializationError,
@ -130,21 +127,8 @@ public class Configuration : IPluginConfiguration
}
/// <summary> Save the current configuration. </summary>
private void SaveConfiguration()
{
try
{
var text = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(_fileName, text);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not save plugin configuration:\n{e}");
}
}
public void Save()
=> _framework.RegisterDelayed(nameof(SaveConfiguration), SaveConfiguration);
=> _saveService.QueueSave(this);
/// <summary> Contains some default values or boundaries for config values. </summary>
public static class Constants
@ -192,4 +176,14 @@ public class Configuration : IPluginConfiguration
return mode;
}
}
public string ToFilename(FilenameService fileNames)
=> fileNames.ConfigFile;
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
}

View file

@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Penumbra.Api;
using Penumbra.Import.Structs;
using Penumbra.Mods;
using FileMode = System.IO.FileMode;
@ -33,22 +34,19 @@ public partial class TexToolsImporter : IDisposable
public ImporterState State { get; private set; }
public readonly List< (FileInfo File, DirectoryInfo? Mod, Exception? Error) > ExtractedMods;
public TexToolsImporter( DirectoryInfo baseDirectory, ICollection< FileInfo > files,
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor)
: this( baseDirectory, files.Count, files, handler, config, editor)
{ }
private readonly Configuration _config;
private readonly ModEditor _editor;
private readonly Mod.Manager _modManager;
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor)
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager)
{
_baseDirectory = baseDirectory;
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
_modPackFiles = modPackFiles;
_config = config;
_editor = editor;
_modManager = modManager;
_modPackCount = count;
ExtractedMods = new List< (FileInfo, DirectoryInfo?, Exception?) >( count );
_token = _cancellation.Token;

View file

@ -35,7 +35,7 @@ public partial class TexToolsImporter
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
// Create a new ModMeta from the TTMP mod list info
Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
// Open the mod data file from the mod pack as a SqPackStream
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
@ -90,7 +90,7 @@ public partial class TexToolsImporter
Penumbra.Log.Information( " -> Importing Simple V2 ModPack" );
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName );
Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
? "Mod imported from TexTools mod pack"
: modList.Description, modList.Version, modList.Url );
@ -135,7 +135,7 @@ public partial class TexToolsImporter
_currentModName = modList.Name;
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName );
Mod.Creator.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url );
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url );
if( _currentNumOptions == 0 )
{

View file

@ -243,7 +243,7 @@ public class DuplicateManager
try
{
var mod = new Mod(modDirectory);
mod.Reload(true, out _);
mod.Reload(_modManager, true, out _);
Finished = false;
_files.UpdateAll(mod, mod.Default);

View file

@ -154,7 +154,7 @@ public class ModFileEditor
if (deletions <= 0)
return;
mod.Reload(false, out _);
mod.Reload(_modManager, false, out _);
_files.UpdateAll(mod, option);
}

View file

@ -58,12 +58,12 @@ public partial class Mod
return;
}
MoveDataFile( oldDirectory, dir );
DataEditor.MoveDataFile(oldDirectory, dir);
new ModBackup(this, mod).Move(null, dir.Name);
dir.Refresh();
mod.ModPath = dir;
if( !mod.Reload( false, out var metaChange ) )
if (!mod.Reload(this, false, out var metaChange))
{
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
@ -71,20 +71,20 @@ public partial class Mod
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
if (metaChange != ModDataChangeType.None)
{
ModDataChanged?.Invoke( metaChange, mod, oldName );
}
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
// Reload a mod without changing its base directory.
// If the base directory does not exist anymore, the mod will be deleted.
/// <summary>
/// Reload a mod without changing its base directory.
/// If the base directory does not exist anymore, the mod will be deleted.
/// </summary>
public void ReloadMod(int idx)
{
var mod = this[idx];
var oldName = mod.Name;
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if( !mod.Reload( true, out var metaChange ) )
if (!mod.Reload(this, true, out var metaChange))
{
Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
@ -96,19 +96,18 @@ public partial class Mod
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
if (metaChange != ModDataChangeType.None)
{
ModDataChanged?.Invoke( metaChange, mod, oldName );
}
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
}
// Delete a mod by its index. The event is invoked before the mod is removed from the list.
// Deletes from filesystem as well as from internal data.
// Updates indices of later mods.
/// <summary>
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
/// Deletes from filesystem as well as from internal data.
/// Updates indices of later mods.
/// </summary>
public void DeleteMod(int idx)
{
var mod = this[idx];
if (Directory.Exists(mod.ModPath.FullName))
{
try
{
Directory.Delete(mod.ModPath.FullName, true);
@ -118,32 +117,25 @@ public partial class Mod
{
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
}
}
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
_mods.RemoveAt(idx);
foreach (var remainingMod in _mods.Skip(idx))
{
--remainingMod.Index;
}
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
}
// Load a new mod and add it to the manager if successful.
/// <summary> Load a new mod and add it to the manager if successful. </summary>
public void AddMod(DirectoryInfo modFolder)
{
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
{
return;
}
Creator.SplitMultiGroups(modFolder);
var mod = LoadMod( modFolder, true );
var mod = LoadMod(this, modFolder, true);
if (mod == null)
{
return;
}
mod.Index = _mods.Count;
_mods.Add(mod);
@ -162,47 +154,35 @@ public partial class Mod
Empty,
}
// Return the state of the new potential name of a directory.
/// <summary> Return the state of the new potential name of a directory. </summary>
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
{
directory = null;
if (newName.Length == 0)
{
return NewDirectoryState.Empty;
}
if (oldName == newName)
{
return NewDirectoryState.Identical;
}
var fixedNewName = Creator.ReplaceBadXivSymbols(newName);
if (fixedNewName != newName)
{
return NewDirectoryState.ContainsInvalidSymbols;
}
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
if (File.Exists(directory.FullName))
{
return NewDirectoryState.ExistsAsFile;
}
if (!Directory.Exists(directory.FullName))
{
return NewDirectoryState.NonExisting;
}
if (directory.EnumerateFileSystemInfos().Any())
{
return NewDirectoryState.ExistsNonEmpty;
}
return NewDirectoryState.ExistsEmpty;
}
// Add new mods to NewMods and remove deleted mods from NewMods.
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory)
{
@ -216,9 +196,7 @@ public partial class Mod
break;
case ModPathChangeType.Moved:
if (oldDirectory != null && newDirectory != null)
{
MoveDataFile( oldDirectory, newDirectory );
}
DataEditor.MoveDataFile(oldDirectory, newDirectory);
break;
}

View file

@ -7,67 +7,6 @@ public sealed partial class Mod
{
public partial class Manager
{
public void ChangeModFavorite( Index idx, bool state )
{
var mod = this[ idx ];
if( mod.Favorite != state )
{
mod.Favorite = state;
mod.SaveLocalData();
ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null );
}
}
public void ChangeModNote( Index idx, string newNote )
{
var mod = this[ idx ];
if( mod.Note != newNote )
{
mod.Note = newNote;
mod.SaveLocalData();
ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null );
}
}
private void ChangeTag( Index idx, int tagIdx, string newTag, bool local )
{
var mod = this[ idx ];
var which = local ? mod.LocalTags : mod.ModTags;
if( tagIdx < 0 || tagIdx > which.Count )
{
return;
}
ModDataChangeType flags = 0;
if( tagIdx == which.Count )
{
flags = mod.UpdateTags( local ? null : which.Append( newTag ), local ? which.Append( newTag ) : null );
}
else
{
var tmp = which.ToArray();
tmp[ tagIdx ] = newTag;
flags = mod.UpdateTags( local ? null : tmp, local ? tmp : null );
}
if( flags.HasFlag( ModDataChangeType.ModTags ) )
{
mod.SaveMeta();
}
if( flags.HasFlag( ModDataChangeType.LocalTags ) )
{
mod.SaveLocalData();
}
if( flags != 0 )
{
ModDataChanged?.Invoke( flags, mod, null );
}
}
public void ChangeLocalTag( Index idx, int tagIdx, string newTag )
=> ChangeTag( idx, tagIdx, newTag, true );
}
}

View file

@ -1,71 +0,0 @@
using System;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public partial class Manager
{
public delegate void ModDataChangeDelegate( ModDataChangeType type, Mod mod, string? oldName );
public event ModDataChangeDelegate? ModDataChanged;
public void ChangeModName( Index idx, string newName )
{
var mod = this[ idx ];
if( mod.Name.Text != newName )
{
var oldName = mod.Name;
mod.Name = newName;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Name, mod, oldName.Text );
}
}
public void ChangeModAuthor( Index idx, string newAuthor )
{
var mod = this[ idx ];
if( mod.Author != newAuthor )
{
mod.Author = newAuthor;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Author, mod, null );
}
}
public void ChangeModDescription( Index idx, string newDescription )
{
var mod = this[ idx ];
if( mod.Description != newDescription )
{
mod.Description = newDescription;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Description, mod, null );
}
}
public void ChangeModVersion( Index idx, string newVersion )
{
var mod = this[ idx ];
if( mod.Version != newVersion )
{
mod.Version = newVersion;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Version, mod, null );
}
}
public void ChangeModWebsite( Index idx, string newWebsite )
{
var mod = this[ idx ];
if( mod.Website != newWebsite )
{
mod.Website = newWebsite;
mod.SaveMeta();
ModDataChanged?.Invoke( ModDataChangeType.Website, mod, null );
}
}
public void ChangeModTag( Index idx, int tagIdx, string newTag )
=> ChangeTag( idx, tagIdx, newTag, false );
}
}

View file

@ -305,18 +305,18 @@ public sealed partial class Mod
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length == 0
|| mod.Groups.Any(o => !ReferenceEquals(o, group)
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
{
return true;
if (message)
_chat.NotificationMessage($"Could not name option {newName} because option with same filename {path} already exists.",
Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return false;
}
return true;
}
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)

View file

@ -97,7 +97,7 @@ public sealed partial class Mod
var queue = new ConcurrentQueue< Mod >();
Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir =>
{
var mod = LoadMod( dir, false );
var mod = LoadMod( this, dir, false );
if( mod != null )
{
queue.Enqueue( mod );

View file

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods;
@ -37,13 +38,15 @@ public sealed partial class Mod
=> GetEnumerator();
private readonly Configuration _config;
private readonly ChatService _chat;
private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor;
public Manager(StartTracker time, Configuration config, ChatService chat)
public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
{
using var timer = time.Measure(StartTimeType.Mods);
_config = config;
_chat = chat;
_communicator = communicator;
DataEditor = dataEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false);

View file

@ -0,0 +1,21 @@
using System;
namespace Penumbra.Mods;
[Flags]
public enum ModDataChangeType : ushort
{
None = 0x0000,
Name = 0x0001,
Author = 0x0002,
Description = 0x0004,
Version = 0x0008,
Website = 0x0010,
Deletion = 0x0020,
Migration = 0x0040,
ModTags = 0x0080,
ImportDate = 0x0100,
Favorite = 0x0200,
LocalTags = 0x0400,
Note = 0x0800,
}

View file

@ -0,0 +1,362 @@
using System;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods;
public class ModDataEditor
{
private readonly FilenameService _filenameService;
private readonly SaveService _saveService;
private readonly CommunicatorService _communicatorService;
public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService)
{
_filenameService = filenameService;
_saveService = saveService;
_communicatorService = communicatorService;
}
public string MetaFile(Mod mod)
=> _filenameService.ModMetaPath(mod);
public string DataFile(Mod mod)
=> _filenameService.LocalDataFile(mod);
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website)
{
var mod = new Mod(directory);
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name!);
mod.Author = author != null ? new LowerString(author) : mod.Author;
mod.Description = description ?? mod.Description;
mod.Version = version ?? mod.Version;
mod.Website = website ?? mod.Website;
_saveService.ImmediateSave(new ModMeta(mod));
}
public ModDataChangeType LoadLocalData(Mod mod)
{
var dataFile = _filenameService.LocalDataFile(mod);
var importDate = 0L;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var save = true;
if (File.Exists(dataFile))
{
save = false;
try
{
var text = File.ReadAllText(dataFile);
var json = JObject.Parse(text);
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = json[nameof(Mod.LocalTags)]?.Values<string>().OfType<string>() ?? localTags;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load local mod data:\n{e}");
}
}
if (importDate == 0)
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
ModDataChangeType changes = 0;
if (mod.ImportDate != importDate)
{
mod.ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= mod.UpdateTags(null, localTags);
if (mod.Favorite != favorite)
{
mod.Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if (mod.Note != note)
{
mod.Note = note;
changes |= ModDataChangeType.Note;
}
if (save)
_saveService.QueueSave(new ModData(mod));
return changes;
}
public ModDataChangeType LoadMeta(Mod mod)
{
var metaFile = _filenameService.ModMetaPath(mod);
if (!File.Exists(metaFile))
{
Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}.");
return ModDataChangeType.Deletion;
}
try
{
var text = File.ReadAllText(metaFile);
var json = JObject.Parse(text);
var newName = json[nameof(Mod.Name)]?.Value<string>() ?? string.Empty;
var newAuthor = json[nameof(Mod.Author)]?.Value<string>() ?? string.Empty;
var newDescription = json[nameof(Mod.Description)]?.Value<string>() ?? string.Empty;
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
var newFileVersion = json[nameof(Mod.FileVersion)]?.Value<uint>() ?? 0;
var importDate = json[nameof(Mod.ImportDate)]?.Value<long>();
var modTags = json[nameof(Mod.ModTags)]?.Values<string>().OfType<string>();
ModDataChangeType changes = 0;
if (mod.Name != newName)
{
changes |= ModDataChangeType.Name;
mod.Name = newName;
}
if (mod.Author != newAuthor)
{
changes |= ModDataChangeType.Author;
mod.Author = newAuthor;
}
if (mod.Description != newDescription)
{
changes |= ModDataChangeType.Description;
mod.Description = newDescription;
}
if (mod.Version != newVersion)
{
changes |= ModDataChangeType.Version;
mod.Version = newVersion;
}
if (mod.Website != newWebsite)
{
changes |= ModDataChangeType.Website;
mod.Website = newWebsite;
}
if (mod.FileVersion != newFileVersion)
{
mod.FileVersion = newFileVersion;
if (Mod.Migration.Migrate(mod, json))
{
changes |= ModDataChangeType.Migration;
_saveService.ImmediateSave(new ModMeta(mod));
}
}
if (importDate != null && mod.ImportDate != importDate.Value)
{
mod.ImportDate = importDate.Value;
changes |= ModDataChangeType.ImportDate;
}
changes |= mod.UpdateTags(modTags, null);
return changes;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not load mod meta:\n{e}");
return ModDataChangeType.Deletion;
}
}
public void ChangeModName(Mod mod, string newName)
{
if (mod.Name.Text == newName)
return;
var oldName = mod.Name;
mod.Name = newName;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text);
}
public void ChangeModAuthor(Mod mod, string newAuthor)
{
if (mod.Author == newAuthor)
return;
mod.Author = newAuthor;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null);
}
public void ChangeModDescription(Mod mod, string newDescription)
{
if (mod.Description == newDescription)
return;
mod.Description = newDescription;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null);
}
public void ChangeModVersion(Mod mod, string newVersion)
{
if (mod.Version == newVersion)
return;
mod.Version = newVersion;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null);
}
public void ChangeModWebsite(Mod mod, string newWebsite)
{
if (mod.Website == newWebsite)
return;
mod.Website = newWebsite;
_saveService.QueueSave(new ModMeta(mod));
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
}
public void ChangeModTag(Mod mod, int tagIdx, string newTag)
=> ChangeTag(mod, tagIdx, newTag, false);
public void ChangeLocalTag(Mod mod, int tagIdx, string newTag)
=> ChangeTag(mod, tagIdx, newTag, true);
public void ChangeModFavorite(Mod mod, bool state)
{
if (mod.Favorite == state)
return;
mod.Favorite = state;
_saveService.QueueSave(new ModData(mod));
;
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
}
public void ChangeModNote(Mod mod, string newNote)
{
if (mod.Note == newNote)
return;
mod.Note = newNote;
_saveService.QueueSave(new ModData(mod));
;
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
}
private void ChangeTag(Mod mod, int tagIdx, string newTag, bool local)
{
var which = local ? mod.LocalTags : mod.ModTags;
if (tagIdx < 0 || tagIdx > which.Count)
return;
ModDataChangeType flags = 0;
if (tagIdx == which.Count)
{
flags = mod.UpdateTags(local ? null : which.Append(newTag), local ? which.Append(newTag) : null);
}
else
{
var tmp = which.ToArray();
tmp[tagIdx] = newTag;
flags = mod.UpdateTags(local ? null : tmp, local ? tmp : null);
}
if (flags.HasFlag(ModDataChangeType.ModTags))
_saveService.QueueSave(new ModMeta(mod));
if (flags.HasFlag(ModDataChangeType.LocalTags))
_saveService.QueueSave(new ModData(mod));
if (flags != 0)
_communicatorService.ModDataChanged.Invoke(flags, mod, null);
}
public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod)
{
var oldFile = _filenameService.LocalDataFile(oldMod.Name);
var newFile = _filenameService.LocalDataFile(newMod.Name);
if (!File.Exists(oldFile))
return;
try
{
File.Move(oldFile, newFile, true);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}");
}
}
private readonly struct ModMeta : ISavable
{
private readonly Mod _mod;
public ModMeta(Mod mod)
=> _mod = mod;
public string ToFilename(FilenameService fileNames)
=> fileNames.ModMetaPath(_mod);
public void Save(StreamWriter writer)
{
var jObject = new JObject
{
{ nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) },
{ nameof(Mod.Name), JToken.FromObject(_mod.Name) },
{ nameof(Mod.Author), JToken.FromObject(_mod.Author) },
{ nameof(Mod.Description), JToken.FromObject(_mod.Description) },
{ nameof(Mod.Version), JToken.FromObject(_mod.Version) },
{ nameof(Mod.Website), JToken.FromObject(_mod.Website) },
{ nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) },
};
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObject.WriteTo(jWriter);
}
}
private readonly struct ModData : ISavable
{
private readonly Mod _mod;
public ModData(Mod mod)
=> _mod = mod;
public string ToFilename(FilenameService fileNames)
=> fileNames.LocalDataFile(_mod);
public void Save(StreamWriter writer)
{
var jObject = new JObject
{
{ nameof(Mod.FileVersion), JToken.FromObject(_mod.FileVersion) },
{ nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) },
{ nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) },
{ nameof(Mod.Note), JToken.FromObject(_mod.Note) },
{ nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) },
};
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
jObject.WriteTo(jWriter);
}
}
}

View file

@ -16,6 +16,8 @@ public enum ModPathChangeType
public partial class Mod
{
public DirectoryInfo ModPath { get; private set; }
public string Identifier
=> Index >= 0 ? ModPath.Name : Name;
public int Index { get; private set; } = -1;
public bool IsTemporary
@ -31,7 +33,7 @@ public partial class Mod
_default = new SubMod( this );
}
private static Mod? LoadMod( DirectoryInfo modPath, bool incorporateMetaChanges )
private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
{
modPath.Refresh();
if( !modPath.Exists )
@ -41,17 +43,16 @@ public partial class Mod
}
var mod = new Mod(modPath);
if( !mod.Reload( incorporateMetaChanges, out _ ) )
{
if (mod.Reload(modManager, incorporateMetaChanges, out _))
return mod;
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." );
return null;
}
return mod;
}
internal bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange )
internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{
modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh();
@ -60,13 +61,13 @@ public partial class Mod
return false;
}
modDataChange = LoadMeta();
modDataChange = modManager.DataEditor.LoadMeta(this);
if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 )
{
return false;
}
LoadLocalData();
modManager.DataEditor.LoadLocalData(this);
LoadDefaultOption();
LoadAllGroups();

View file

@ -64,19 +64,6 @@ public partial class Mod
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
}
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
public static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version,
string? website )
{
var mod = new Mod( directory );
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString( name! );
mod.Author = author != null ? new LowerString( author ) : mod.Author;
mod.Description = description ?? mod.Description;
mod.Version = version ?? mod.Version;
mod.Website = website ?? mod.Website;
mod.SaveMetaFile(); // Not delayed.
}
/// <summary> Create a file for an option group from given data. </summary>
public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name,
int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods )
@ -147,14 +134,12 @@ public partial class Mod
internal static void CreateDefaultFiles( DirectoryInfo directory )
{
var mod = new Mod( directory );
mod.Reload( false, out _ );
mod.Reload( Penumbra.ModManager, false, out _ );
foreach( var file in mod.FindUnusedFiles() )
{
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
{
mod._default.FileData.TryAdd( gamePath, file );
}
}
mod._default.IncorporateMetaChanges( directory, true );
mod.SaveDefaultMod();

View file

@ -3,139 +3,24 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Penumbra.Services;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi)
=> new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" ));
public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public IReadOnlyList<string> LocalTags { get; private set; } = Array.Empty<string>();
public string AllTagsLower { get; private set; } = string.Empty;
public string Note { get; private set; } = string.Empty;
public bool Favorite { get; private set; } = false;
public string Note { get; internal set; } = string.Empty;
public bool Favorite { get; internal set; } = false;
private FileInfo LocalDataFile
=> new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" ));
private ModDataChangeType LoadLocalData()
{
var dataFile = LocalDataFile;
var importDate = 0L;
var localTags = Enumerable.Empty< string >();
var favorite = false;
var note = string.Empty;
var save = true;
if( File.Exists( dataFile.FullName ) )
{
save = false;
try
{
var text = File.ReadAllText( dataFile.FullName );
var json = JObject.Parse( text );
importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? importDate;
favorite = json[ nameof( Favorite ) ]?.Value< bool >() ?? favorite;
note = json[ nameof( Note ) ]?.Value< string >() ?? note;
localTags = json[ nameof( LocalTags ) ]?.Values< string >().OfType< string >() ?? localTags;
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not load local mod data:\n{e}" );
}
}
if( importDate == 0 )
{
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
ModDataChangeType changes = 0;
if( ImportDate != importDate )
{
ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= UpdateTags( null, localTags );
if( Favorite != favorite )
{
Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if( Note != note )
{
Note = note;
changes |= ModDataChangeType.Note;
}
if( save )
{
SaveLocalDataFile();
}
return changes;
}
private void SaveLocalData()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveLocalData ) + ModPath.Name, SaveLocalDataFile );
private void SaveLocalDataFile()
{
var dataFile = LocalDataFile;
try
{
var jObject = new JObject
{
{ nameof( FileVersion ), JToken.FromObject( FileVersion ) },
{ nameof( ImportDate ), JToken.FromObject( ImportDate ) },
{ nameof( LocalTags ), JToken.FromObject( LocalTags ) },
{ nameof( Note ), JToken.FromObject( Note ) },
{ nameof( Favorite ), JToken.FromObject( Favorite ) },
};
dataFile.Directory!.Create();
File.WriteAllText( dataFile.FullName, jObject.ToString( Formatting.Indented ) );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write local data file for mod {Name} to {dataFile.FullName}:\n{e}" );
}
}
private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod )
{
var oldFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" );
var newFile = Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" );
if( File.Exists( oldFile ) )
{
try
{
File.Move( oldFile, newFile, true );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not move local data file {oldFile} to {newFile}:\n{e}" );
}
}
}
private ModDataChangeType UpdateTags( IEnumerable< string >? newModTags, IEnumerable< string >? newLocalTags )
internal ModDataChangeType UpdateTags(IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
{
if (newModTags == null && newLocalTags == null)
{
return 0;
}
ModDataChangeType type = 0;
if (newModTags != null)
@ -160,9 +45,7 @@ public sealed partial class Mod
}
if (type != 0)
{
AllTagsLower = string.Join('\0', ModTags.Concat(LocalTags).Select(s => s.ToLowerInvariant()));
}
return type;
}

View file

@ -13,59 +13,43 @@ namespace Penumbra.Mods;
public sealed partial class Mod
{
private static class Migration
public static partial class Migration
{
public static bool Migrate(Mod mod, JObject json)
{
var ret = MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod );
if( ret )
{
// Immediately save on migration.
mod.SaveMetaFile();
}
return ret;
}
=> MigrateV0ToV1(mod, json) || MigrateV1ToV2(mod) || MigrateV2ToV3(mod);
private static bool MigrateV2ToV3(Mod mod)
{
if (mod.FileVersion > 2)
{
return false;
}
// Remove import time.
mod.FileVersion = 3;
return true;
}
[GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)]
private static partial Regex GroupRegex();
private static readonly Regex GroupRegex = new( @"group_\d{3}_", RegexOptions.Compiled );
private static bool MigrateV1ToV2(Mod mod)
{
if (mod.FileVersion > 1)
{
return false;
}
if (!mod.GroupFiles.All( g => GroupRegex.IsMatch( g.Name )))
{
if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name)))
foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray())
{
var newName = Regex.Replace(group.Name, "^group_", $"group_{index + 1:D3}_", RegexOptions.Compiled);
try
{
if (newName != group.Name)
{
group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}");
}
}
}
mod.FileVersion = 2;
@ -75,9 +59,7 @@ public sealed partial class Mod
private static bool MigrateV0ToV1(Mod mod, JObject json)
{
if (mod.FileVersion > 0)
{
return false;
}
var swaps = json["FileSwaps"]?.ToObject<Dictionary<Utf8GamePath, FullPath>>()
?? new Dictionary<Utf8GamePath, FullPath>();
@ -85,31 +67,23 @@ public sealed partial class Mod
var priority = 1;
var seenMetaFiles = new HashSet<FullPath>();
foreach (var group in groups.Values)
{
ConvertGroup(mod, group, ref priority, seenMetaFiles);
}
foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
{
if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
&& !mod._default.FileData.TryAdd(gamePath, unusedFile))
{
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}.");
}
}
mod._default.FileSwapData.Clear();
mod._default.FileSwapData.EnsureCapacity(swaps.Count);
foreach (var (gamePath, swapPath) in swaps)
{
mod._default.FileSwapData.Add(gamePath, swapPath);
}
mod._default.IncorporateMetaChanges(mod.ModPath, true);
foreach (var (group, index) in mod.Groups.WithIndex())
{
IModGroup.Save(group, mod.ModPath, index);
}
// Delete meta files.
foreach (var file in seenMetaFiles.Where(f => f.Exists))
@ -127,7 +101,6 @@ public sealed partial class Mod
// Delete old meta files.
var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json");
if (File.Exists(oldMetaFile))
{
try
{
File.Delete(oldMetaFile);
@ -136,11 +109,9 @@ public sealed partial class Mod
{
Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}");
}
}
mod.FileVersion = 1;
mod.SaveDefaultMod();
mod.SaveMetaFile();
return true;
}
@ -148,9 +119,7 @@ public sealed partial class Mod
private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
{
if (group.Options.Count == 0)
{
return;
}
switch (group.SelectionType)
{
@ -165,9 +134,7 @@ public sealed partial class Mod
};
mod._groups.Add(newMultiGroup);
foreach (var option in group.Options)
{
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++));
}
break;
case GroupType.Single:
@ -185,9 +152,7 @@ public sealed partial class Mod
};
mod._groups.Add(newSingleGroup);
foreach (var option in group.Options)
{
newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles));
}
break;
}
@ -199,16 +164,12 @@ public sealed partial class Mod
{
var fullPath = new FullPath(basePath, relPath);
foreach (var gamePath in gamePaths)
{
mod.FileData.TryAdd(gamePath, fullPath);
}
if (fullPath.Extension is ".meta" or ".rgsp")
{
seenMetaFiles.Add(fullPath);
}
}
}
private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
{
@ -254,9 +215,7 @@ public sealed partial class Mod
var token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
{
return token.ToObject<HashSet<T>>() ?? new HashSet<T>();
}
var tmp = token.ToObject<T>();
return tmp != null
@ -274,10 +233,8 @@ public sealed partial class Mod
{
var v = (HashSet<T>)value;
foreach (var val in v)
{
serializer.Serialize(writer, val?.ToString());
}
}
writer.WriteEndArray();
}

View file

@ -1,31 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
namespace Penumbra.Mods;
[Flags]
public enum ModDataChangeType : ushort
{
None = 0x0000,
Name = 0x0001,
Author = 0x0002,
Description = 0x0004,
Version = 0x0008,
Website = 0x0010,
Deletion = 0x0020,
Migration = 0x0040,
ModTags = 0x0080,
ImportDate = 0x0100,
Favorite = 0x0200,
LocalTags = 0x0400,
Note = 0x0800,
}
public sealed partial class Mod : IMod
{
public static readonly TemporaryMod ForcedFiles = new()
@ -36,122 +14,13 @@ public sealed partial class Mod : IMod
};
public const uint CurrentFileVersion = 3;
public uint FileVersion { get; private set; } = CurrentFileVersion;
public LowerString Name { get; private set; } = "New Mod";
public LowerString Author { get; private set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty;
public string Version { get; private set; } = string.Empty;
public string Website { get; private set; } = string.Empty;
public IReadOnlyList< string > ModTags { get; private set; } = Array.Empty< string >();
internal FileInfo MetaFile
=> new(Path.Combine( ModPath.FullName, "meta.json" ));
private ModDataChangeType LoadMeta()
{
var metaFile = MetaFile;
if( !File.Exists( metaFile.FullName ) )
{
Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." );
return ModDataChangeType.Deletion;
}
try
{
var text = File.ReadAllText( metaFile.FullName );
var json = JObject.Parse( text );
var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty;
var newAuthor = json[ nameof( Author ) ]?.Value< string >() ?? string.Empty;
var newDescription = json[ nameof( Description ) ]?.Value< string >() ?? string.Empty;
var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty;
var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty;
var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0;
var importDate = json[ nameof( ImportDate ) ]?.Value< long >();
var modTags = json[ nameof( ModTags ) ]?.Values< string >().OfType< string >();
ModDataChangeType changes = 0;
if( Name != newName )
{
changes |= ModDataChangeType.Name;
Name = newName;
}
if( Author != newAuthor )
{
changes |= ModDataChangeType.Author;
Author = newAuthor;
}
if( Description != newDescription )
{
changes |= ModDataChangeType.Description;
Description = newDescription;
}
if( Version != newVersion )
{
changes |= ModDataChangeType.Version;
Version = newVersion;
}
if( Website != newWebsite )
{
changes |= ModDataChangeType.Website;
Website = newWebsite;
}
if( FileVersion != newFileVersion )
{
FileVersion = newFileVersion;
if( Migration.Migrate( this, json ) )
{
changes |= ModDataChangeType.Migration;
}
}
if( importDate != null && ImportDate != importDate.Value )
{
ImportDate = importDate.Value;
changes |= ModDataChangeType.ImportDate;
}
changes |= UpdateTags( modTags, null );
return changes;
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not load mod meta:\n{e}" );
return ModDataChangeType.Deletion;
}
}
private void SaveMeta()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveMetaFile ) + ModPath.Name, SaveMetaFile );
private void SaveMetaFile()
{
var metaFile = MetaFile;
try
{
var jObject = new JObject
{
{ nameof( FileVersion ), JToken.FromObject( FileVersion ) },
{ nameof( Name ), JToken.FromObject( Name ) },
{ nameof( Author ), JToken.FromObject( Author ) },
{ nameof( Description ), JToken.FromObject( Description ) },
{ nameof( Version ), JToken.FromObject( Version ) },
{ nameof( Website ), JToken.FromObject( Website ) },
{ nameof( ModTags ), JToken.FromObject( ModTags ) },
};
File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" );
}
}
public uint FileVersion { get; internal set; } = CurrentFileVersion;
public LowerString Name { get; internal set; } = "New Mod";
public LowerString Author { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string Version { get; internal set; } = string.Empty;
public string Website { get; internal set; } = string.Empty;
public IReadOnlyList< string > ModTags { get; internal set; } = Array.Empty< string >();
public override string ToString()
=> Name.Text;

View file

@ -46,14 +46,14 @@ public sealed partial class Mod
_default.ManipulationData = manips;
}
public static void SaveTempCollection( ModCollection collection, string? character = null )
public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null )
{
DirectoryInfo? dir = null;
try
{
dir = Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name );
var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) );
Creator.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
var mod = new Mod( dir );
var defaultMod = mod._default;
@ -88,7 +88,7 @@ public sealed partial class Mod
}
mod.SaveDefaultMod();
Penumbra.ModManager.AddMod( dir );
modManager.AddMod( dir );
Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." );
}
catch( Exception e )

View file

@ -10,20 +10,22 @@ using Penumbra.Util;
namespace Penumbra.Mods;
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISaveable
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
{
private readonly Mod.Manager _modManager;
private readonly CommunicatorService _communicator;
private readonly FilenameService _files;
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
public ModFileSystem(Mod.Manager modManager, FilenameService files)
public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files)
{
_modManager = modManager;
_communicator = communicator;
_files = files;
Reload();
Changed += OnChange;
_modManager.ModDiscoveryFinished += Reload;
_modManager.ModDataChanged += OnDataChange;
_communicator.ModDataChanged.Event += OnDataChange;
_modManager.ModPathChanged += OnModPathChange;
}
@ -31,7 +33,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISaveable
{
_modManager.ModPathChanged -= OnModPathChange;
_modManager.ModDiscoveryFinished -= Reload;
_modManager.ModDataChanged -= OnDataChange;
_communicator.ModDataChanged.Event -= OnDataChange;
}
public struct ImportDate : ISortMode<Mod>

View file

@ -92,6 +92,7 @@ public class PenumbraNew
// Add Mod Services
services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>()
.AddSingleton<Mod.Manager>()
.AddSingleton<ModFileSystem>();

View file

@ -45,6 +45,13 @@ public class CommunicatorService : IDisposable
/// </list> </summary>
public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase));
/// <summary><list type="number">
/// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary>
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModDataChanged = new(nameof(ModDataChanged));
public void Dispose()
{
CollectionChange.Dispose();
@ -52,5 +59,6 @@ public class CommunicatorService : IDisposable
ModMetaChange.Dispose();
CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose();
ModDataChanged.Dispose();
}
}

View file

@ -38,7 +38,7 @@ public class FilenameService
/// <summary> Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. </summary>
public string LocalDataFile(Mod mod)
=> mod.IsTemporary ? string.Empty : LocalDataFile(mod.ModPath.FullName);
=> LocalDataFile(mod.ModPath.FullName);
/// <summary> Obtain the path of the local data file given a mod directory. </summary>
public string LocalDataFile(string modDirectory)
@ -66,7 +66,7 @@ public class FilenameService
/// <summary> Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. </summary>
public string ModMetaPath(Mod mod)
=> mod.IsTemporary ? string.Empty : ModMetaPath(mod.ModPath.FullName);
=> ModMetaPath(mod.ModPath.FullName);
/// <summary> Obtain the path of the meta file given a mod directory. </summary>
public string ModMetaPath(string modDirectory)

View file

@ -267,7 +267,7 @@ public class ItemSwapTab : IDisposable, ITab
private void CreateMod()
{
var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName);
Mod.Creator.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
_modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir);
_modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager.Last(),

View file

@ -150,7 +150,7 @@ public partial class ModEditWindow : Window, IDisposable
_itemSwapTab.DrawContent();
}
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
/// <summary> A row of three buttonSizes and a help marker that can be used for material suffix changing. </summary>
private static class MaterialSuffix
{
private static string _materialSuffixFrom = string.Empty;

View file

@ -78,7 +78,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
_communicator.CollectionChange.Event += OnCollectionChange;
_collectionManager.Current.ModSettingChanged += OnSettingChange;
_collectionManager.Current.InheritanceChanged += OnInheritanceChange;
_modManager.ModDataChanged += OnModDataChange;
_communicator.ModDataChanged.Event += OnModDataChange;
_modManager.ModDiscoveryStarted += StoreCurrentSelection;
_modManager.ModDiscoveryFinished += RestoreLastSelection;
OnCollectionChange(CollectionType.Current, null, _collectionManager.Current, "");
@ -89,7 +89,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
base.Dispose();
_modManager.ModDiscoveryStarted -= StoreCurrentSelection;
_modManager.ModDiscoveryFinished -= RestoreLastSelection;
_modManager.ModDataChanged -= OnModDataChange;
_communicator.ModDataChanged.Event -= OnModDataChange;
_collectionManager.Current.ModSettingChanged -= OnSettingChange;
_collectionManager.Current.InheritanceChanged -= OnInheritanceChange;
_communicator.CollectionChange.Event -= OnCollectionChange;
@ -127,7 +127,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
try
{
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
_modManager.DataEditor.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir);
Penumbra.ModManager.AddMod(newDir);
_newModName = string.Empty;
@ -187,30 +187,30 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
private void ToggleLeafFavorite(FileSystem<Mod>.Leaf mod)
{
if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite"))
_modManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite);
_modManager.DataEditor.ChangeModFavorite(mod.Value, !mod.Value.Favorite);
}
private void SetDefaultImportFolder(ModFileSystem.Folder folder)
{
if (ImGui.MenuItem("Set As Default Import Folder"))
{
if (!ImGui.MenuItem("Set As Default Import Folder"))
return;
var newName = folder.FullName();
if (newName != _config.DefaultImportFolder)
{
if (newName == _config.DefaultImportFolder)
return;
_config.DefaultImportFolder = newName;
_config.Save();
}
}
}
private void ClearDefaultImportFolder()
{
if (ImGui.MenuItem("Clear Default Import Folder") && _config.DefaultImportFolder.Length > 0)
{
if (!ImGui.MenuItem("Clear Default Import Folder") || _config.DefaultImportFolder.Length <= 0)
return;
_config.DefaultImportFolder = string.Empty;
_config.Save();
}
}
private string _newModName = string.Empty;
@ -241,7 +241,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
return;
_import = new TexToolsImporter(_modManager.BasePath, f.Count, f.Select(file => new FileInfo(file)),
AddNewMod, _config, _modEditor);
AddNewMod, _config, _modEditor, _modManager);
ImGui.OpenPopup("Import Status");
}, 0, modPath, _config.AlwaysOpenDefaultImport);
}

View file

@ -42,7 +42,7 @@ public class ModPanelDescriptionTab : ITab
out var editedTag);
_tutorial.OpenTutorial(BasicTutorialSteps.Tags);
if (tagIdx >= 0)
_modManager.ChangeLocalTag(_selector.Selected!.Index, tagIdx, editedTag);
_modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag);
if (_selector.Selected!.ModTags.Count > 0)
_modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.",

View file

@ -77,7 +77,7 @@ public class ModPanelEditTab : ITab
var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags,
out var editedTag);
if (tagIdx >= 0)
_modManager.ChangeModTag(_mod.Index, tagIdx, editedTag);
_modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag);
UiHelpers.DefaultLineSpace();
AddOptionGroup.Draw(_modManager, _mod);
@ -172,18 +172,18 @@ public class ModPanelEditTab : ITab
private void EditRegularMeta()
{
if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X))
_modManager.ChangeModName(_mod.Index, newName);
_modManager.DataEditor.ChangeModName(_mod, newName);
if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X))
Penumbra.ModManager.ChangeModAuthor(_mod.Index, newAuthor);
_modManager.DataEditor.ChangeModAuthor(_mod, newAuthor);
if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32,
UiHelpers.InputTextWidth.X))
_modManager.ChangeModVersion(_mod.Index, newVersion);
_modManager.DataEditor.ChangeModVersion(_mod, newVersion);
if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256,
UiHelpers.InputTextWidth.X))
_modManager.ChangeModWebsite(_mod.Index, newWebsite);
_modManager.DataEditor.ChangeModWebsite(_mod, newWebsite);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3));
@ -192,13 +192,13 @@ public class ModPanelEditTab : ITab
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description));
ImGui.SameLine();
var fileExists = File.Exists(_mod.MetaFile.FullName);
var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod));
var tt = fileExists
? "Open the metadata json file in the text editor of your choice."
: "The metadata json file does not exist.";
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt,
!fileExists, true))
Process.Start(new ProcessStartInfo(_mod.MetaFile.FullName) { UseShellExecute = true });
Process.Start(new ProcessStartInfo(_modManager.DataEditor.MetaFile(_mod)) { UseShellExecute = true });
}
/// <summary> Do some edits outside of iterations. </summary>
@ -349,7 +349,7 @@ public class ModPanelEditTab : ITab
switch (_newDescriptionIdx)
{
case Input.Description:
modManager.ChangeModDescription(_mod.Index, _newDescription);
modManager.DataEditor.ChangeModDescription(_mod, _newDescription);
break;
case >= 0:
if (_newDescriptionOptionIdx < 0)

View file

@ -140,7 +140,7 @@ public class ModPanelTabBar
ImGui.SetCursorPos(newPos);
if (ImGui.Button(FontAwesomeIcon.Star.ToIconString()))
_modManager.ChangeModFavorite(mod.Index, !mod.Favorite);
_modManager.DataEditor.ChangeModFavorite(mod, !mod.Favorite);
}
var hovered = ImGui.IsItemHovered();

View file

@ -1,10 +1,8 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.Api;
using Penumbra.Services;
namespace Penumbra.Util;
@ -12,7 +10,7 @@ namespace Penumbra.Util;
/// <summary>
/// Any file type that we want to save via SaveService.
/// </summary>
public interface ISaveable
public interface ISavable
{
/// <summary> The full file name of a given object. </summary>
public string ToFilename(FilenameService fileNames);
@ -42,7 +40,7 @@ public class SaveService
}
/// <summary> Queue a save for the next framework tick. </summary>
public void QueueSave(ISaveable value)
public void QueueSave(ISavable value)
{
var file = value.ToFilename(_fileNames);
_framework.RegisterDelayed(value.GetType().Name + file, () =>
@ -52,7 +50,7 @@ public class SaveService
}
/// <summary> Immediately trigger a save. </summary>
public void ImmediateSave(ISaveable value)
public void ImmediateSave(ISavable value)
{
var name = value.ToFilename(_fileNames);
try
@ -75,7 +73,7 @@ public class SaveService
}
}
public void ImmediateDelete(ISaveable value)
public void ImmediateDelete(ISavable value)
{
var name = value.ToFilename(_fileNames);
try