diff --git a/Memoria.FF1/Memoria.FF1.csproj b/Memoria.FF1/Memoria.FF1.csproj
index 8d6dd61..1e15926 100644
--- a/Memoria.FF1/Memoria.FF1.csproj
+++ b/Memoria.FF1/Memoria.FF1.csproj
@@ -14,7 +14,7 @@
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 1173770
$([MSBuild]::GetRegistryValueFromView('$(GameRegistryPath)', 'InstallLocation', null, RegistryView.Registry32))
$([MSBuild]::GetRegistryValueFromView('$(GameRegistryPath)', 'InstallLocation', null, RegistryView.Registry64))
- bin\FF3
+ bin\FF1
$(GamePath)\BepInEx\plugins\
true
full
@@ -102,6 +102,7 @@
+
@@ -110,6 +111,7 @@
+
@@ -121,6 +123,7 @@
+
diff --git a/Memoria.FF1/Shared/BeepInEx/ExtensionMethods.cs b/Memoria.FF1/Shared/BeepInEx/ExtensionMethods.cs
new file mode 100644
index 0000000..b8797d9
--- /dev/null
+++ b/Memoria.FF1/Shared/BeepInEx/ExtensionMethods.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+
+namespace Memoria.FFPR.BeepInEx;
+
+public static class ExtensionMethods
+{
+ public static void Deconstruct(this Il2CppSystem.Collections.Generic.KeyValuePair il2cpp, out TKey key, out TValue value)
+ {
+ key = il2cpp.Key;
+ value = il2cpp.Value;
+ }
+
+ public static Dictionary> ToManaged(
+ this Il2CppSystem.Collections.Generic.Dictionary> il2cpp)
+ {
+ return il2cpp.ToManaged(k => k, v => v.ToManaged());
+ }
+
+ public static Dictionary ToManaged(this Il2CppSystem.Collections.Generic.Dictionary il2cpp)
+ {
+ return il2cpp.ToManaged(k => k, v => v);
+ }
+
+ public static Dictionary ToManaged(
+ this Il2CppSystem.Collections.Generic.Dictionary il2cpp,
+ Func keySelector,
+ Func valueSelector)
+ {
+ if (il2cpp is null) throw new ArgumentNullException(nameof(il2cpp));
+ if (keySelector is null) throw new ArgumentNullException(nameof(keySelector));
+ if (valueSelector is null) throw new ArgumentNullException(nameof(valueSelector));
+
+ if (il2cpp.comparer.Pointer != Il2CppSystem.Collections.Generic.EqualityComparer.Default.Pointer)
+ throw new ArgumentException($"The IL2CPP Dictionary uses a non-standard Comparer ([{il2cpp.comparer}]) that cannot be converted to a Managed type.", nameof(il2cpp));
+
+ var result = new Dictionary(il2cpp.Count);
+
+ foreach ((TSourceKey k, TSourceValue v) in il2cpp)
+ result.Add(keySelector(k), valueSelector(v));
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/Memoria.FF1/Shared/Configuration/AssetsConfiguration.cs b/Memoria.FF1/Shared/Configuration/AssetsConfiguration.cs
index a548fc6..81b7f91 100644
--- a/Memoria.FF1/Shared/Configuration/AssetsConfiguration.cs
+++ b/Memoria.FF1/Shared/Configuration/AssetsConfiguration.cs
@@ -21,6 +21,9 @@ public sealed class AssetsConfiguration
private readonly ConfigEntry _importTextures;
// private readonly ConfigEntry _importBinary; // Cannot import :/
+ private readonly ConfigEntry ModsEnabled;
+ private readonly ConfigEntry _modsDirectory;
+
public AssetsConfiguration(ConfigFile file)
{
ExportEnabled = file.Bind(Section, nameof(ExportEnabled), false,
@@ -60,6 +63,13 @@ public AssetsConfiguration(ConfigFile file)
// _importBinary = file.Bind(Section, nameof(ImportBinary), true,
// "Import binary resources: .bytes, etc.");
+
+ ModsEnabled = file.Bind(Section, nameof(ModsEnabled), true,
+ $"Overwrite the supported resources from the {nameof(ModsDirectory)}.");
+
+ _modsDirectory = file.Bind(Section, nameof(ModsDirectory), "%StreamingAssets%/Mods",
+ $"Directory from which the supported resources will be updated.",
+ new AcceptableDirectoryPath(nameof(ModsDirectory)));
}
public String ExportDirectory => ExportEnabled.Value
@@ -81,6 +91,10 @@ public AssetsConfiguration(ConfigFile file)
public Boolean ImportText => _importText.Value;
public Boolean ImportTextures => _importTextures.Value;
public Boolean ImportBinary => false; // _importBinary.Value;
+
+ public String ModsDirectory => ModsEnabled.Value
+ ? AcceptableDirectoryPath.Preprocess(_modsDirectory.Value)
+ : String.Empty;
public void DisableExport() => ExportEnabled.Value = false;
}
diff --git a/Memoria.FF1/Shared/Core/CsvMerger.cs b/Memoria.FF1/Shared/Core/CsvMerger.cs
new file mode 100644
index 0000000..dfbd4f5
--- /dev/null
+++ b/Memoria.FF1/Shared/Core/CsvMerger.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using Memoria.FFPR.Configuration;
+using Memoria.FFPR.IL2CPP;
+
+namespace Memoria.FFPR.Core;
+
+public sealed class CsvMerger
+{
+ private const Char Separator = ',';
+
+ private readonly String[] _columnNames;
+ private readonly Dictionary _columnNameIndices;
+ private readonly List _rows;
+ private readonly Dictionary _rowIndices;
+ private readonly HashSet _removedRows = new HashSet();
+
+ public CsvMerger(String csvContent)
+ {
+ if (csvContent is null) throw new ArgumentNullException(nameof(csvContent));
+ if (csvContent == String.Empty) throw new ArgumentException(nameof(csvContent));
+
+ using (var sr = new StringReader(csvContent))
+ {
+ if (!TryReadContent(sr, out String[] parts))
+ parts = Array.Empty();
+ else if (parts[0] != "id")
+ throw new NotSupportedException($"Not supported CSV-format. Unexpected first column: [{parts[0]}]. Expected: [id]");
+
+ HashSet processedColumns = new();
+ _columnNames = parts;
+ _columnNameIndices = new(_columnNames.Length);
+ for (Int32 i = 0; i < _columnNames.Length; i++)
+ {
+ String columnName = _columnNames[i];
+ if (!processedColumns.Add(columnName))
+ throw new FormatException($"The header contains several columns with the same name: [{columnName}]");
+
+ _columnNameIndices.Add(columnName, i);
+ }
+
+ _rows = new List();
+ _rowIndices = new Dictionary();
+ while (TryReadContent(sr, out parts))
+ {
+ Int32 id = Int32.Parse(parts[0], CultureInfo.InvariantCulture);
+ AddNewRow(id, parts);
+ }
+ }
+ }
+
+ public void MergeFiles(IReadOnlyList filePaths)
+ {
+ if (filePaths is null) throw new ArgumentNullException(nameof(filePaths));
+
+ foreach (String fullPath in filePaths)
+ {
+ try
+ {
+ String shortPath = ApplicationPathConverter.ReturnPlaceholders(fullPath);
+ ModComponent.Log.LogInfo($"[Mod] Merging data from {shortPath}");
+
+ using (StreamReader sr = File.OpenText(fullPath))
+ MergeFile(sr);
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException($"Failed to merge data from {fullPath}", nameof(fullPath), ex);
+ }
+ }
+ }
+
+ private void AddNewRow(Int32 id, String[] row)
+ {
+ _rowIndices.Add(id, _rows.Count);
+ _rows.Add(row);
+ }
+
+ private void MergeFile(StreamReader sr)
+ {
+ if (!TryReadContent(sr, out String[] parts))
+ return;
+
+ if (parts[0] != "id")
+ throw new NotSupportedException($"Not supported CSV-format. Unexpected first column: [{parts[0]}]. Expected: [id]");
+
+ StringBuilder sb = new();
+
+ String[] columnNames = parts;
+ Int32[] columnIndices = new Int32[columnNames.Length];
+ HashSet processedColumns = new HashSet();
+ for (Int32 i = 0; i < columnNames.Length; i++)
+ {
+ String columnName = columnNames[i];
+ if (!processedColumns.Add(columnName))
+ throw new FormatException($"The header contains several columns with the same name: [{columnName}]");
+
+ if (!_columnNameIndices.TryGetValue(columnName, out Int32 columnIndex))
+ throw new FormatException($"Cannot find index of [{columnName}] column in the full CSV-file.");
+
+ columnIndices[i] = columnIndex;
+ }
+
+ while (TryReadContent(sr, out parts))
+ {
+ Boolean toRemove = false;
+ Int32 id = Int32.Parse(parts[0], CultureInfo.InvariantCulture);
+ if (id < 0)
+ {
+ toRemove = true;
+ id *= -1;
+ }
+
+ if (!_rowIndices.TryGetValue(id, out var rowIndex))
+ {
+ if (toRemove)
+ {
+ ModComponent.Log.LogWarning($"[Mod] Cannot find row with id [{id}] to remove it.");
+ continue;
+ }
+
+ if (parts.Length != _columnNames.Length)
+ throw new FormatException($"Cannot add row with id [{id}]. Expected {_columnNames.Length} columns, but there is {parts.Length}.");
+
+ String[] row = new String[_columnNames.Length];
+ for (Int32 i = 0; i < row.Length; i++)
+ {
+ Int32 columnIndex = columnIndices[i];
+ row[columnIndex] = parts[i];
+ }
+
+ AddNewRow(id, row);
+ ModComponent.Log.LogInfo($"[Mod] Added new row: {String.Join(",", row)}.");
+ continue;
+ }
+
+ if (toRemove)
+ {
+ if (_removedRows.Add(rowIndex))
+ {
+ String[] row = _rows[rowIndex];
+ ModComponent.Log.LogInfo($"[Mod] Removed existing row [{id}]. {String.Join(",", row)}.");
+ }
+
+ continue;
+ }
+
+ for (Int32 i = 1; i < parts.Length; i++)
+ {
+ Int32 columnIndex = columnIndices[i];
+ String columnName = columnNames[i];
+ String[] row = _rows[rowIndex];
+ String oldValue = row[columnIndex];
+ String newValue = parts[i];
+ if (oldValue != newValue)
+ {
+ row[columnIndex] = newValue;
+ sb.Append($" {columnName} ({oldValue} -> {newValue})");
+ }
+ }
+
+ if (sb.Length > 0)
+ {
+ ModComponent.Log.LogInfo($"[Mod] Changed row [{id}]. {sb.ToString()}");
+ sb.Clear();
+ }
+ }
+ }
+
+ public String BuildContent()
+ {
+ using (StringWriter sw = new StringWriter())
+ {
+ foreach (String columnName in _columnNames)
+ {
+ sw.Write(columnName);
+ sw.Write(Separator);
+ }
+
+ sw.WriteLine();
+
+ for (int i = 0; i < _rows.Count; i++)
+ {
+ if (_removedRows.Contains(i))
+ continue;
+
+ String[] row = _rows[i];
+ foreach (String data in row)
+ {
+ sw.Write(data);
+ sw.Write(Separator);
+ }
+
+ sw.WriteLine();
+ }
+
+ sw.Flush();
+ return sw.ToString();
+ }
+ }
+
+ private Boolean TryReadContent(TextReader reader, out String[] parts)
+ {
+ while (true)
+ {
+ String line = reader.ReadLine();
+ if (line is null)
+ {
+ parts = null;
+ return false;
+ }
+
+ if (!String.IsNullOrWhiteSpace(line))
+ {
+ parts = line.Split(Separator);
+ return true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Memoria.FF1/Shared/IL2CPP/EncounterLot_CheckEncount.cs b/Memoria.FF1/Shared/IL2CPP/EncounterLot_CheckEncount.cs
index 45bcc0c..9878dfa 100644
--- a/Memoria.FF1/Shared/IL2CPP/EncounterLot_CheckEncount.cs
+++ b/Memoria.FF1/Shared/IL2CPP/EncounterLot_CheckEncount.cs
@@ -1,6 +1,6 @@
using System;
+using System.Collections.Generic;
using HarmonyLib;
-using Il2CppSystem.Collections.Generic;
using Last.Management;
using Last.Map;
using Memoria.FFPR.Configuration;
@@ -12,7 +12,7 @@
using Exception = System.Exception;
using File = System.IO.File;
using IntPtr = System.IntPtr;
-using Object = Il2CppSystem.Object;
+using Object = System.Object;
using Path = System.IO.Path;
namespace Memoria.FFPR.IL2CPP
@@ -20,7 +20,7 @@ namespace Memoria.FFPR.IL2CPP
[HarmonyPatch(typeof(ContentCatalogData), nameof(ContentCatalogData.CreateLocator))]
public sealed class ContentCatalogData_CreateLocator : Il2CppSystem.Object
{
- private static readonly Dictionary