using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Harmony12; using Mono.Cecil; using Debug = UnityEngine.Debug; namespace UnityModManagerNet { public partial class UnityModManager { public partial class ModEntry { private static readonly Regex RequirementPattern = new Regex(@"(.*)-(\d\.\d\.\d).*", RegexOptions.Compiled); /// /// Required game version [0.15.0] /// public readonly Version GameVersion; public readonly ModInfo Info; public readonly ModLogger Logger; /// /// Required UMM version /// public readonly Version ManagerVersion; private readonly Dictionary mCache = new Dictionary(); /// /// Path to mod folder /// public readonly string Path; /// /// Required mods /// public readonly Dictionary Requirements = new Dictionary(); /// /// Version of a mod /// public readonly Version Version; /// /// Displayed in UMM UI. Add tag to change colors. Can be used when custom verification game version /// [0.15.0] /// public string CustomRequirements = string.Empty; /// /// UI checkbox /// public bool Enabled = true; /// /// Not used /// public bool HasUpdate = false; private bool mActive; private bool mFirstLoading = true; /// /// Not used /// public Version NewestVersion; /// /// Called by MonoBehaviour.FixedUpdate [0.13.0] /// public Action OnFixedUpdate; /// /// Called by MonoBehaviour.OnGUI /// public Action OnGUI; /// /// Called when closing mod GUI [0.16.0] /// public Action OnHideGUI; /// /// Called by MonoBehaviour.LateUpdate [0.13.0] /// public Action OnLateUpdate; /// /// Called when the UMM UI closes. /// public Action OnSaveGUI; /// /// Called when opening mod GUI [0.16.0] /// public Action OnShowGUI; /// /// Called to activate / deactivate the mod. /// public Func OnToggle; /// /// Called to unload old data for reloading mod [0.14.0] /// public Func OnUnload; /// /// Called by MonoBehaviour.Update [0.13.0] /// public Action OnUpdate; /// /// Show button to reload the mod [0.14.0] /// public bool CanReload { get; private set; } public Assembly Assembly { get; private set; } public bool Started { get; private set; } public bool ErrorOnLoading { get; private set; } /// /// If OnToggle exists /// public bool Toggleable => OnToggle != null; /// /// If Assembly is loaded [0.13.1] /// public bool Loaded => Assembly != null; public bool Active { get => mActive; set { if (!Started || ErrorOnLoading) return; try { if (value == mActive) return; if (value && !Loaded) { var stopwatch = Stopwatch.StartNew(); Load(); Logger.NativeLog($"Loading time {stopwatch.ElapsedMilliseconds / 1000f:f2} s."); return; } var toggled = OnToggle?.Invoke(this, value); if (toggled ?? true) { mActive = value; Logger.Log(value ? "Active." : "Inactive."); } else Logger.Log("Unsuccessfully."); } catch (Exception e) { Logger.Error($"OnToggle: {e.GetType().Name} - {e.Message}"); Debug.LogException(e); } } } public ModEntry(ModInfo info, string path) { Info = info; Path = path; Logger = new ModLogger(Info.Id); Version = ParseVersion(info.Version); ManagerVersion = !string.IsNullOrEmpty(info.ManagerVersion) ? ParseVersion(info.ManagerVersion) : new Version(); GameVersion = !string.IsNullOrEmpty(info.GameVersion) ? ParseVersion(info.GameVersion) : new Version(); if (info.Requirements == null || info.Requirements.Length <= 0) return; foreach (string id in info.Requirements) { var match = RequirementPattern.Match(id); if (match.Success) { Requirements.Add(match.Groups[1].Value, ParseVersion(match.Groups[2].Value)); continue; } if (!Requirements.ContainsKey(id)) Requirements.Add(id, null); } } public bool Load() { if (Loaded) return !ErrorOnLoading; ErrorOnLoading = false; Logger.Log($"Version '{Info.Version}'. Loading."); if (string.IsNullOrEmpty(Info.AssemblyName)) { ErrorOnLoading = true; Logger.Error($"{nameof(Info.AssemblyName)} is null."); } if (string.IsNullOrEmpty(Info.EntryMethod)) { ErrorOnLoading = true; Logger.Error($"{nameof(Info.EntryMethod)} is null."); } if (!string.IsNullOrEmpty(Info.ManagerVersion)) if (ManagerVersion > GetVersion()) { ErrorOnLoading = true; Logger.Error($"Mod Manager must be version '{Info.ManagerVersion}' or higher."); } if (Requirements.Count > 0) foreach (var item in Requirements) { string id = item.Key; var mod = FindMod(id); if (mod == null) { ErrorOnLoading = true; Logger.Error($"Required mod '{id}' missing."); } else if (!mod.Active) { mod.Enabled = true; mod.Active = true; if (!mod.Active) Logger.Log($"Required mod '{id}' inactive."); } else if (item.Value != null && item.Value > mod.Version) { ErrorOnLoading = true; Logger.Error($"Required mod '{id}' must be version '{item.Value}' or higher."); } } if (ErrorOnLoading) return false; string assemblyPath = System.IO.Path.Combine(Path, Info.AssemblyName); if (File.Exists(assemblyPath)) { try { string assemblyCachePath = assemblyPath; var cacheExists = false; if (mFirstLoading) { var fi = new FileInfo(assemblyPath); var hash = (ushort)((long)fi.LastWriteTimeUtc.GetHashCode() + version.GetHashCode() + ManagerVersion.GetHashCode()).GetHashCode(); assemblyCachePath = assemblyPath + $".{hash}.cache"; cacheExists = File.Exists(assemblyCachePath); if (!cacheExists) foreach (string filepath in Directory.GetFiles(Path, "*.cache")) try { File.Delete(filepath); } catch (Exception) { } } if (ManagerVersion >= VER_0_13) { if (mFirstLoading) { if (!cacheExists) File.Copy(assemblyPath, assemblyCachePath, true); Assembly = Assembly.LoadFile(assemblyCachePath); foreach (var type in Assembly.GetTypes()) if (type.GetCustomAttributes(typeof(EnableReloadingAttribute), true).Any()) { CanReload = true; break; } } else Assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); } else { using (var asm = AssemblyDefinition.ReadAssembly(assemblyPath)) { var asmName = AssemblyNameReference.Parse(Assembly.GetExecutingAssembly().FullName); asm.MainModule.AssemblyReferences.Add(asmName); foreach (var typeReference in asm.MainModule.GetTypeReferences()) if (typeReference.FullName == "UnityModManagerNet.UnityModManager") typeReference.Scope = asmName; asm.Write(assemblyCachePath); } Assembly.LoadFile(assemblyCachePath); } mFirstLoading = false; } catch (Exception exception) { ErrorOnLoading = true; Logger.Error($"Error loading file '{assemblyPath}'."); Debug.LogException(exception); return false; } try { object[] param = { this }; Type[] types = { typeof(ModEntry) }; if (FindMethod(Info.EntryMethod, types, false) == null) { param = null; types = null; } if (!Invoke(Info.EntryMethod, out var result, param, types) || result != null && (bool)result == false) { ErrorOnLoading = true; Logger.Log("Not loaded."); } } catch (Exception e) { ErrorOnLoading = true; Logger.Log(e.ToString()); return false; } Started = true; if (!ErrorOnLoading) { Active = true; return true; } } else { ErrorOnLoading = true; Logger.Error($"File '{assemblyPath}' not found."); } return false; } internal void Reload() { if (!Started || !CanReload) return; try { string assemblyPath = System.IO.Path.Combine(Path, Info.AssemblyName); var reflAssembly = Assembly.ReflectionOnlyLoad(File.ReadAllBytes(assemblyPath)); if (reflAssembly.GetName().Version == Assembly.GetName().Version) { Logger.Log("Reload is not needed. The version is exactly the same as the previous one."); return; } } catch (Exception e) { Logger.Error(e.ToString()); return; } if (OnSaveGUI != null) OnSaveGUI.Invoke(this); Logger.Log("Reloading..."); if (Toggleable) Active = false; else mActive = false; try { if (!Active && (OnUnload == null || OnUnload.Invoke(this))) { mCache.Clear(); typeof(Traverse).GetField("Cache", BindingFlags.Static | BindingFlags.NonPublic)?.SetValue(null, new AccessCache()); typeof(Harmony.Traverse).GetField("Cache", BindingFlags.Static | BindingFlags.NonPublic)?.SetValue(null, new Harmony.AccessCache()); var oldAssembly = Assembly; Assembly = null; Started = false; ErrorOnLoading = false; OnToggle = null; OnGUI = null; OnSaveGUI = null; OnUnload = null; OnUpdate = null; OnShowGUI = null; OnHideGUI = null; OnFixedUpdate = null; OnLateUpdate = null; CustomRequirements = null; if (!Load()) return; foreach (var type in oldAssembly.GetTypes()) { var t = Assembly.GetType(type.FullName); if (t == null) continue; foreach (var field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Where(f => f.GetCustomAttributes(typeof(SaveOnReloadAttribute), true).Any())) { var f = t.GetField(field.Name); if (f == null) continue; Logger.Log($"Copying field '{field.DeclaringType.Name}.{field.Name}'"); try { if (field.FieldType != f.FieldType) { if (field.FieldType.IsEnum && f.FieldType.IsEnum) f.SetValue(null, Convert.ToInt32(field.GetValue(null))); } else f.SetValue(null, field.GetValue(null)); } catch (Exception ex) { Logger.Error(ex.ToString()); } } } return; } if (Active) Logger.Log("Must be deactivated."); } catch (Exception e) { Logger.Error(e.ToString()); } Logger.Log("Reloading canceled."); } public bool Invoke(string namespaceClassnameMethodname, out object result, object[] param = null, Type[] types = null) { result = null; try { var methodInfo = FindMethod(namespaceClassnameMethodname, types); if (methodInfo != null) { result = methodInfo.Invoke(null, param); return true; } } catch (Exception exception) { Logger.Error($"Error trying to call '{namespaceClassnameMethodname}'."); Logger.Error($"{exception.GetType().Name} - {exception.Message}"); Debug.LogException(exception); } return false; } private MethodInfo FindMethod(string namespaceClassnameMethodname, Type[] types, bool showLog = true) { long key = namespaceClassnameMethodname.GetHashCode(); if (types != null) key = types.Aggregate(key, (current, val) => current + val.GetHashCode()); if (mCache.TryGetValue(key, out var methodInfo)) return methodInfo; if (Assembly != null) { string classString = null; string methodString = null; int pos = namespaceClassnameMethodname.LastIndexOf('.'); if (pos != -1) { classString = namespaceClassnameMethodname.Substring(0, pos); methodString = namespaceClassnameMethodname.Substring(pos + 1); } else { if (showLog) Logger.Error($"Function name error '{namespaceClassnameMethodname}'."); goto Exit; } var type = Assembly.GetType(classString); if (type != null) { if (types == null) types = new Type[0]; methodInfo = type.GetMethod(methodString, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, types, new ParameterModifier[0]); if (methodInfo == null) if (showLog) Logger.Log(types.Length > 0 ? $"Method '{namespaceClassnameMethodname}[{string.Join(", ", types.Select(x => x.Name).ToArray())}]' not found." : $"Method '{namespaceClassnameMethodname}' not found."); } else { if (showLog) Logger.Error($"Class '{classString}' not found."); } } else { if (showLog) UnityModManager.Logger.Error($"Can't find method '{namespaceClassnameMethodname}'. Mod '{Info.Id}' is not loaded."); } Exit: mCache[key] = methodInfo; return methodInfo; } } } }