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 KnownCatalogs = new(); + private static readonly Dictionary> KnownCatalogs = new(); public ContentCatalogData_CreateLocator(IntPtr ptr) : base(ptr) { @@ -30,17 +30,14 @@ public static String GetFileExtension(String assetAddress) { return GetFileExtension("AddressablesMainContentCatalog", assetAddress); } - + public static String GetFileExtension(String catalogName, String assetAddress) { - if (!KnownCatalogs.ContainsKey(catalogName)) - return String.Empty; - - Dictionary catalog = KnownCatalogs[catalogName].Cast>(); - if (!catalog.ContainsKey(assetAddress)) - return String.Empty; - - return catalog[assetAddress].ToString(); + return + KnownCatalogs.TryGetValue(catalogName, out var dictionary) + && dictionary.TryGetValue(assetAddress, out var extension) + ? extension + : String.Empty; } public static void Prefix(ContentCatalogData __instance) @@ -49,7 +46,7 @@ public static void Prefix(ContentCatalogData __instance) if (KnownCatalogs.ContainsKey(locatorId)) return; - Dictionary extensions = new(); + Dictionary extensions = new(); KnownCatalogs.Add(locatorId, extensions); try diff --git a/Memoria.FF1/Shared/IL2CPP/ModComponent.cs b/Memoria.FF1/Shared/IL2CPP/ModComponent.cs index 4f4d127..6706744 100644 --- a/Memoria.FF1/Shared/IL2CPP/ModComponent.cs +++ b/Memoria.FF1/Shared/IL2CPP/ModComponent.cs @@ -2,10 +2,12 @@ using BepInEx.Logging; using Memoria.FFPR.Configuration; using Memoria.FFPR.Core; +using Memoria.FFPR.Mods; using UnityEngine; using Exception = System.Exception; using IntPtr = System.IntPtr; using Logger = BepInEx.Logging.Logger; +using Object = System.Object; namespace Memoria.FFPR.IL2CPP { @@ -17,6 +19,8 @@ public sealed class ModComponent : MonoBehaviour [field: NonSerialized] public ModConfiguration Config { get; private set; } [field: NonSerialized] public GameSpeedControl SpeedControl { get; private set; } [field: NonSerialized] public GameEncountersControl EncountersControl { get; private set; } + [field: NonSerialized] public ModFileResolver ModFiles { get; private set; } + public ModComponent(IntPtr ptr) : base(ptr) { @@ -35,6 +39,7 @@ public void Awake() Config = new ModConfiguration(); SpeedControl = new GameSpeedControl(); EncountersControl = new GameEncountersControl(); + ModFiles = new ModFileResolver(); gameObject.AddComponent(); @@ -47,7 +52,7 @@ public void Awake() throw; } } - + public void OnDestroy() { Log.LogInfo($"[{nameof(ModComponent)}].{nameof(OnDestroy)}()"); diff --git a/Memoria.FF1/Shared/IL2CPP/ResourceExporter.cs b/Memoria.FF1/Shared/IL2CPP/ResourceExporter.cs index 6376a1c..34ebd63 100644 --- a/Memoria.FF1/Shared/IL2CPP/ResourceExporter.cs +++ b/Memoria.FF1/Shared/IL2CPP/ResourceExporter.cs @@ -1,13 +1,13 @@ using System; +using System.Collections.Generic; using System.IO; -using Il2CppSystem.Collections.Generic; using Last.Management; +using Memoria.FFPR.BeepInEx; using Memoria.FFPR.Configuration; using Memoria.FFPR.Core; using UnityEngine; using Exception = System.Exception; using IntPtr = System.IntPtr; -using Object = System.Object; namespace Memoria.FFPR.IL2CPP { @@ -18,6 +18,7 @@ public sealed class ResourceExporter : MonoBehaviour private String _exportDirectory; private Int32 _currentIndex; private Int32 _totalCount = 1; + private Boolean _enumeratorIsNull = true; private Dictionary>.Enumerator _enumerator; private KeyValuePair> _currentGroup; private DateTime _loadingStartTime; @@ -41,18 +42,18 @@ public void Awake() Destroy(this); return; } - + Time.timeScale = 0.0f; ModComponent.Log.LogInfo($"[Export] Game stopped. Export started. Directory: {_exportDirectory}"); ModComponent.Log.LogInfo($"[Export] Waiting for ResourceManager initialization."); _extensionResolver = new AssetExtensionResolver(); - + // OnGui _blackTexture = new Texture2D(1, 1); _blackTexture.SetPixel(0, 0, Color.black); _blackTexture.Apply(); - + _guiStyle = new GUIStyle(); _guiStyle.fontSize = 48; _guiStyle.normal.textColor = Color.white; @@ -80,7 +81,7 @@ public void Update() } // Waiting for AssetsPath loading - if (_enumerator is null) + if (_enumeratorIsNull) { if (!_resourceManager.CheckLoadAssetCompleted("AssetsPath")) return; @@ -88,15 +89,16 @@ public void Update() Dictionary> dic = GetAssetsPath(); _totalCount = dic.Count; _enumerator = dic.GetEnumerator(); + _enumeratorIsNull = false; ModComponent.Log.LogInfo($"[Export] Exporting assets {_totalCount} listed in AssetsPath..."); } - + // Must have to export not readable textures if (Camera.main is null) return; - if (_currentGroup != null) + if (_currentGroup.Key != null) { String assetGroup = _currentGroup.Key; TimeSpan elapsedTime = DateTime.Now - _loadingLogTime; @@ -111,12 +113,12 @@ public void Update() return; } - + elapsedTime = DateTime.Now - _loadingStartTime; Dictionary assets = _currentGroup.Value; ModComponent.Log.LogInfo($"[Export ({_currentIndex} / {_totalCount})] Loaded {assets.Count} assets from [{assetGroup}] in {elapsedTime.TotalSeconds} sec. Exporting..."); - Dictionary loaded = _resourceManager.completeAssetDic; + Il2CppSystem.Collections.Generic.Dictionary loaded = _resourceManager.completeAssetDic; foreach (var pair in assets) { String assetName = pair.Key; @@ -131,21 +133,21 @@ public void Update() String extension = _extensionResolver.GetFileExtension(assetPath); String type = _extensionResolver.GetAssetType(asset); - + String exportPath = assetPath + extension; ExportAsset(asset, type, assetName, exportPath); } _resourceManager.DestroyGroupAsset(assetGroup); - _currentGroup = null; + _currentGroup = default; } if (_enumerator.MoveNext()) { _loadingStartTime = DateTime.Now; _loadingLogTime = _loadingStartTime; - _currentGroup = _enumerator.current; + _currentGroup = _enumerator.Current; try { @@ -193,11 +195,11 @@ public void OnDisable() OnExportError(ex); } } - + private void ExportAsset(Il2CppSystem.Object asset, String type, String assetName, String assetPath) { String fullPath = Path.Combine(_exportDirectory, assetPath); - + Boolean overwrite = ModComponent.Instance.Config.Assets.ExportOverwrite; if (!overwrite && File.Exists(fullPath)) { @@ -304,7 +306,7 @@ void ExportDummy(String type, String assetName) { ModComponent.Log.LogInfo($"[Export ({_currentIndex} / {_totalCount})] \tSkip {assetName}. Not supported type: {type}"); } - + private static void PrepareDirectory(String fullPath) { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); @@ -322,7 +324,8 @@ private void OnExportError(Exception exception) if (assetPathAsset is null) throw new Exception("[Export] Cannot find text resource AssetsPath."); - return AssetPathUtilty.Parse(assetPathAsset.text); + var il2cppDic = AssetPathUtilty.Parse(assetPathAsset.text); + return il2cppDic.ToManaged(); } } } \ No newline at end of file diff --git a/Memoria.FF1/Shared/IL2CPP/ResourceManager_IsLoadAssetCompleted.cs b/Memoria.FF1/Shared/IL2CPP/ResourceManager_IsLoadAssetCompleted.cs index 80ab40b..4839430 100644 --- a/Memoria.FF1/Shared/IL2CPP/ResourceManager_IsLoadAssetCompleted.cs +++ b/Memoria.FF1/Shared/IL2CPP/ResourceManager_IsLoadAssetCompleted.cs @@ -1,6 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; using HarmonyLib; -using Il2CppSystem.Collections.Generic; using Last.Management; using Last.Map; using Memoria.FFPR.Configuration; @@ -26,41 +27,41 @@ public ResourceManager_IsLoadAssetCompleted(IntPtr ptr) : base(ptr) } // Don't use other Dictionaries. It must be present in IL2CPP - private static readonly Dictionary KnownAssets = new(); + private static readonly Dictionary KnownAssets = new(); private static readonly AssetExtensionResolver ExtensionResolver = new(); public static void Postfix(String addressName, ResourceManager __instance, Boolean __result) { if (!__result) return; - + // Skip scenes or the game will crash if (addressName.StartsWith("Assets/Scenes")) return; - + try { AssetsConfiguration config = ModComponent.Instance.Config.Assets; String importDirectory = config.ImportDirectory; String exportDirectory = config.ExportDirectory; + String modsDirectory = config.ModsDirectory; // Skip import if disabled - if (importDirectory == String.Empty) + if (importDirectory == String.Empty && modsDirectory == String.Empty) return; // Skip import if export is enabled to avoid race condition if (exportDirectory != String.Empty) return; - // Don't use TryGetValue to avoid MissingMethod exception IntPtr knownAsset = IntPtr.Zero; - if (KnownAssets.ContainsKey(addressName)) - knownAsset = KnownAssets[addressName].Pointer; + if (KnownAssets.TryGetValue(addressName, out var ka)) + knownAsset = ka.Pointer; - Dictionary dic = ResourceManager.Instance.completeAssetDic; + Il2CppSystem.Collections.Generic.Dictionary dic = ResourceManager.Instance.completeAssetDic; if (!dic.ContainsKey(addressName)) return; - + Object assetObject = dic[addressName]; if (assetObject is null) return; @@ -68,61 +69,31 @@ public static void Postfix(String addressName, ResourceManager __instance, Boole // Skip if asset was already processed if (knownAsset == assetObject.Pointer) return; - - KnownAssets[addressName] = assetObject; String type = ExtensionResolver.GetAssetType(assetObject); String extension = ExtensionResolver.GetFileExtension(addressName); - String fullPath = Path.Combine(importDirectory, addressName) + extension; - if (!File.Exists(fullPath)) - return; + String addressNameWithExtension = addressName + extension; - Object newAsset = null; - switch (type) + String fullPath = Path.Combine(importDirectory, addressNameWithExtension); + if (File.Exists(fullPath)) { - case "UnityEngine.AnimationClip": - case "UnityEngine.AnimatorOverrideController": - case "UnityEngine.GameObject": - case "UnityEngine.Material": - case "UnityEngine.RenderTexture": - case "UnityEngine.RuntimeAnimatorController": - case "UnityEngine.Shader": - case "UnityEngine.Sprite": - { - if (!config.ImportTextures) - return; - newAsset = ImportSprite(assetObject.Cast(), fullPath); - break; - } - case "UnityEngine.TextAsset": + if (TryImportAsset(type, config, assetObject, fullPath, out var newAsset)) { - if (!config.ImportText) - return; - newAsset = ImportTextAsset(fullPath); - break; - } - case "System.Byte[]": - { - if (!config.ImportBinary) - return; - newAsset = ImportBinaryAsset(assetObject.Cast().name, fullPath); - break; + assetObject = newAsset; + String shortPath = ApplicationPathConverter.ReturnPlaceholders(fullPath); + ModComponent.Log.LogInfo($"[Import] File imported: {shortPath}"); } - case "UnityEngine.Texture2D": - if (!config.ImportTextures) - return; - newAsset = ImportTextures(fullPath); - break; - case "UnityEngine.U2D.SpriteAtlas": - throw new NotSupportedException(type); } - dic[addressName] = newAsset; - - KnownAssets[addressName] = newAsset; + IReadOnlyList modPath = ModComponent.Instance.ModFiles.FindAll(addressNameWithExtension); + if (modPath.Count > 0) + { + if (TryModAsset(type, config, assetObject, modPath, out var newAsset)) + assetObject = newAsset; + } - String shortPath = ApplicationPathConverter.ReturnPlaceholders(fullPath); - ModComponent.Log.LogInfo($"[Import] File imported: {shortPath}"); + dic[addressName] = assetObject; + KnownAssets[addressName] = assetObject; } catch (Exception ex) { @@ -130,11 +101,57 @@ public static void Postfix(String addressName, ResourceManager __instance, Boole } } + private static Boolean TryImportAsset(String type, AssetsConfiguration config, Object assetObject, String fullPath, out Object newAsset) + { + newAsset = null; + switch (type) + { + case "UnityEngine.AnimationClip": + case "UnityEngine.AnimatorOverrideController": + case "UnityEngine.GameObject": + case "UnityEngine.Material": + case "UnityEngine.RenderTexture": + case "UnityEngine.RuntimeAnimatorController": + case "UnityEngine.Shader": + throw new NotSupportedException(type); + case "UnityEngine.Sprite": + { + if (!config.ImportTextures) + return false; + newAsset = ImportSprite(assetObject.Cast(), fullPath); + break; + } + case "UnityEngine.TextAsset": + { + if (!config.ImportText) + return false; + newAsset = ImportTextAsset(fullPath); + break; + } + case "System.Byte[]": + { + if (!config.ImportBinary) + return false; + newAsset = ImportBinaryAsset(assetObject.Cast().name, fullPath); + break; + } + case "UnityEngine.Texture2D": + if (!config.ImportTextures) + return false; + newAsset = ImportTextures(fullPath); + break; + case "UnityEngine.U2D.SpriteAtlas": + throw new NotSupportedException(type); + } + + return true; + } + private static Object ImportTextAsset(String fullPath) { return new TextAsset(File.ReadAllText(fullPath)); } - + private static Object ImportTextures(String fullPath) { return TextureHelper.ReadTextureFromFile(fullPath); @@ -171,7 +188,7 @@ private static Object ImportSprite(Sprite asset, String fullPath) private static Object ImportBinaryAsset(String assetName, String fullPath) { // Il2CppStructArray sourceBytes = Il2CppSystem.IO.File.ReadAllBytes(fullPath); - + // Not working // TextAsset result = new TextAsset(new String('a', sourceBytes.Length)); // result.name = assetName + ".bytes"; @@ -190,5 +207,72 @@ private static Object ImportBinaryAsset(String assetName, String fullPath) throw new NotSupportedException(); } + + private static Boolean TryModAsset(String type, AssetsConfiguration config, Object assetObject, IReadOnlyList modPath, out Object newAsset) + { + newAsset = null; + switch (type) + { + case "UnityEngine.AnimationClip": + case "UnityEngine.AnimatorOverrideController": + case "UnityEngine.GameObject": + case "UnityEngine.Material": + case "UnityEngine.RenderTexture": + case "UnityEngine.RuntimeAnimatorController": + case "UnityEngine.Shader": + { + throw new NotSupportedException(type); + } + case "UnityEngine.Sprite": + { + String fullPath = modPath.Last(); + newAsset = ImportSprite(assetObject.Cast(), fullPath); + + String shortPath = ApplicationPathConverter.ReturnPlaceholders(fullPath); + ModComponent.Log.LogInfo($"[Mod] Sprite replaced: {shortPath}"); + + break; + } + case "UnityEngine.TextAsset": + { + String fullPath = modPath.Last(); + if (Path.GetExtension(fullPath) == ".csv") + { + TextAsset textAsset = assetObject.Cast(); + + CsvMerger merger = new(textAsset.text); + merger.MergeFiles(modPath); + + newAsset = new TextAsset(merger.BuildContent()); + } + else + { + newAsset = ImportTextAsset(fullPath); + String shortPath = ApplicationPathConverter.ReturnPlaceholders(fullPath); + ModComponent.Log.LogInfo($"[Mod] Text replaced: {shortPath}"); + } + break; + } + case "System.Byte[]": + { + return false; + } + case "UnityEngine.Texture2D": + { + String fullPath = modPath.Last(); + newAsset = ImportTextures(fullPath); + + String shortPath = ApplicationPathConverter.ReturnPlaceholders(fullPath); + ModComponent.Log.LogInfo($"[Mod] Texture replaced: {shortPath}"); + break; + } + case "UnityEngine.U2D.SpriteAtlas": + { + throw new NotSupportedException(type); + } + } + + return true; + } } } \ No newline at end of file diff --git a/Memoria.FF1/Shared/Mods/ModIndex.cs b/Memoria.FF1/Shared/Mods/ModIndex.cs new file mode 100644 index 0000000..a1f9b0e --- /dev/null +++ b/Memoria.FF1/Shared/Mods/ModIndex.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Memoria.FFPR.Configuration; +using Memoria.FFPR.IL2CPP; +using UnhollowerBaseLib; +using UnityEngine; +using Object = Il2CppSystem.Object; + +namespace Memoria.FFPR.Mods; + +public sealed class ModFileResolver +{ + private readonly String _modsRoot; + private readonly Dictionary> _catalog; + + public ModFileResolver() + { + _modsRoot = ModComponent.Instance.Config.Assets.ModsDirectory; + _catalog = IndexMods(_modsRoot); + } + + public IReadOnlyList FindAll(String assetAddress) + { + if (!_catalog.TryGetValue(assetAddress, out List modNames)) + return Array.Empty(); + + return modNames.Select(n => Path.Combine(_modsRoot, n, assetAddress)).ToArray(); + } + + private static Dictionary> IndexMods(String modsRoot) + { + Dictionary> catalog = new(StringComparer.InvariantCultureIgnoreCase); + + if (!Directory.Exists(modsRoot)) + { + ModComponent.Log.LogInfo($"[Mods] Mods indexing skipped. Mods directory is not defined."); + return catalog; + } + + String[] mods = Directory.GetDirectories(modsRoot); + foreach (String modDirectory in mods) + { + String modName = Path.GetFileName(modDirectory); + + String shortPath = ApplicationPathConverter.ReturnPlaceholders(modDirectory); + ModComponent.Log.LogInfo($"[Mods.{modName}] Indexing mod. Directory: {shortPath}."); + String[] files = Directory.GetFiles(modDirectory, "*", SearchOption.AllDirectories); + foreach (String file in files) + { + // Before: C:\Mods\My\Assets\GameAssets\File.txt + // After: Assets\GameAssets\File.txt + String assetAddress = file.Substring(modDirectory.Length + 1); + + // Before: Assets\GameAssets\File.txt + // After : Assets/GameAssets/File.txt + assetAddress = assetAddress.Replace("\\", "/"); + if (!catalog.TryGetValue(assetAddress, out var modNames)) + { + modNames = new List(); + catalog.Add(assetAddress, modNames); + } + + modNames.Add(modName); + ModComponent.Log.LogInfo($"[Mods.{modName}] {assetAddress}."); + } + } + + return catalog; + } +} \ No newline at end of file diff --git a/Memoria.FF1/Shared/TypeRegister.cs b/Memoria.FF1/Shared/TypeRegister.cs index 92bc4c8..d5bc78a 100644 --- a/Memoria.FF1/Shared/TypeRegister.cs +++ b/Memoria.FF1/Shared/TypeRegister.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using BepInEx.Logging; +using Il2CppSystem.Collections.Generic; using UnhollowerRuntimeLib; namespace Memoria.FFPR @@ -16,15 +17,13 @@ public TypeRegister(ManualLogSource logSource) public void RegisterRequiredTypes() { - - try { // Not supported :( // _log.LogInfo("Registering required types..."); // - // ClassInjector.RegisterTypeInIl2Cpp>(); + // ClassInjector.RegisterTypeInIl2Cpp>(); // // _log.LogInfo($"1 additional types required successfully."); } @@ -40,12 +39,8 @@ public void RegisterAssemblyTypes() { _log.LogInfo("Registering assembly types..."); - MethodInfo registrator = typeof(ClassInjector).GetMethod("RegisterTypeInIl2Cpp", new Type[0]); - if (registrator == null) - throw new Exception("Cannot find method RegisterTypeInIl2Cpp."); - Assembly assembly = Assembly.GetExecutingAssembly(); - Int32 count = RegisterTypes(assembly, registrator); + Int32 count = RegisterTypes(assembly); _log.LogInfo($"{count} assembly types registered successfully."); } @@ -55,19 +50,25 @@ public void RegisterAssemblyTypes() } } - private Int32 RegisterTypes(Assembly assembly, MethodInfo registrator) + private Int32 RegisterTypes(Assembly assembly) { Int32 count = 0; - var parameters = new object[0]; foreach (Type type in assembly.GetTypes()) { if (!IsImportableType(type)) continue; - MethodInfo genericMethod = registrator.MakeGenericMethod(type); - genericMethod.Invoke(null, parameters); - count++; + try + { + ClassInjector.RegisterTypeInIl2Cpp(type); + count++; + } + catch (Exception ex) + { + _log.LogError($"Failed to register type {type.FullName}. Error: {ex}"); + throw; + } } return count; @@ -75,6 +76,9 @@ private Int32 RegisterTypes(Assembly assembly, MethodInfo registrator) private static Boolean IsImportableType(Type type) { + if (!type.IsClass) + return false; + return type.Namespace?.EndsWith(".IL2CPP") ?? false; } }