oni-priority-ux/mod/PLibCore/PRegistryComponent.cs

254 lines
8.6 KiB
C#

/*
* Copyright 2022 Peter Han
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using HarmonyLib;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace PeterHan.PLib.Core {
/// <summary>
/// A custom component added to manage shared data between mods, especially instances of
/// PForwardedComponent used by both PLib and other mods.
/// </summary>
internal sealed class PRegistryComponent : MonoBehaviour, IPLibRegistry {
/// <summary>
/// The Harmony instance name used when patching via PLib.
/// </summary>
internal const string PLIB_HARMONY = "PeterHan.PLib";
/// <summary>
/// A pointer to the active PLib registry.
/// </summary>
private static PRegistryComponent instance = null;
/// <summary>
/// true if the forwarded components have been instantiated, or false otherwise.
/// </summary>
private static bool instantiated = false;
/// <summary>
/// Applies the latest version of all forwarded components.
/// </summary>
private static void ApplyLatest() {
bool apply = false;
if (instance != null)
lock (instance) {
if (!instantiated)
apply = instantiated = true;
}
if (apply)
instance.Instantiate();
}
/// <summary>
/// Stores shared mod data which needs single instance existence. Available to all
/// PLib consumers through PLib API.
/// </summary>
public IDictionary<string, object> ModData { get; }
/// <summary>
/// The Harmony instance used by PLib patching.
/// </summary>
public Harmony PLibInstance { get; }
/// <summary>
/// The candidate components with versions, from multiple assemblies.
/// </summary>
private readonly ConcurrentDictionary<string, PVersionList> forwardedComponents;
/// <summary>
/// The components actually instantiated (latest version of each).
/// </summary>
private readonly ConcurrentDictionary<string, object> instantiatedComponents;
/// <summary>
/// The latest versions of each component.
/// </summary>
private readonly ConcurrentDictionary<string, PForwardedComponent> latestComponents;
internal PRegistryComponent() {
if (instance == null)
instance = this;
else {
#if DEBUG
PRegistry.LogPatchWarning("Multiple PLocalRegistry created!");
#endif
}
ModData = new ConcurrentDictionary<string, object>(2, 64);
forwardedComponents = new ConcurrentDictionary<string, PVersionList>(2, 32);
instantiatedComponents = new ConcurrentDictionary<string, object>(2, 32);
latestComponents = new ConcurrentDictionary<string, PForwardedComponent>(2, 32);
PLibInstance = new Harmony(PLIB_HARMONY);
}
public void AddCandidateVersion(PForwardedComponent instance) {
if (instance == null)
throw new ArgumentNullException(nameof(instance));
AddCandidateVersion(instance.ID, instance);
}
/// <summary>
/// Adds a remote or local forwarded component by ID.
/// </summary>
/// <param name="id">The real ID of the component.</param>
/// <param name="instance">The candidate instance to add.</param>
private void AddCandidateVersion(string id, PForwardedComponent instance) {
var versions = forwardedComponents.GetOrAdd(id, (_) => new PVersionList());
if (versions == null)
PRegistry.LogPatchWarning("Missing version info for component type " + id);
else {
var list = versions.Components;
bool first = list.Count < 1;
list.Add(instance);
#if DEBUG
PRegistry.LogPatchDebug("Candidate version of {0} from {1}".F(id, instance.
GetOwningAssembly()));
#endif
if (first)
instance.Bootstrap(PLibInstance);
}
}
/// <summary>
/// Applies a bootstrapper patch which will complete forwarded component initialization
/// before mods are post-loaded.
/// </summary>
internal void ApplyBootstrapper() {
try {
PLibInstance.Patch(typeof(KMod.Mod), nameof(KMod.Mod.PostLoad), prefix:
new HarmonyMethod(typeof(PRegistryComponent), nameof(ApplyLatest)));
} catch (AmbiguousMatchException e) {
PUtil.LogException(e);
} catch (ArgumentException e) {
PUtil.LogException(e);
} catch (TypeLoadException e) {
PUtil.LogException(e);
}
}
/// <summary>
/// Called from other mods to add a candidate version of a particular component.
/// </summary>
/// <param name="instance">The component to be added.</param>
internal void DoAddCandidateVersion(object instance) {
AddCandidateVersion(instance.GetType().FullName, new PRemoteComponent(instance));
}
/// <summary>
/// Called from other mods to get a list of all components with the given ID.
/// </summary>
/// <param name="id">The component ID to retrieve.</param>
/// <returns>The instantiated instance of that component, or null if no component by
/// that name was found or ever registered.</returns>
internal System.Collections.ICollection DoGetAllComponents(string id) {
if (!forwardedComponents.TryGetValue(id, out PVersionList all))
all = null;
return all?.Components;
}
/// <summary>
/// Called from other mods to get the instantiated version of a particular component.
/// </summary>
/// <param name="id">The component ID to retrieve.</param>
/// <returns>The instantiated instance of that component, or null if no component by
/// that name was found or successfully instantiated.</returns>
internal object DoGetLatestVersion(string id) {
if (!instantiatedComponents.TryGetValue(id, out object component))
component = null;
return component;
}
public IEnumerable<PForwardedComponent> GetAllComponents(string id) {
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id));
if (!forwardedComponents.TryGetValue(id, out PVersionList all))
all = null;
return all?.Components;
}
public PForwardedComponent GetLatestVersion(string id) {
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id));
if (!latestComponents.TryGetValue(id, out PForwardedComponent remoteComponent)) {
#if DEBUG
PRegistry.LogPatchWarning("Unable to find a component matching: " + id);
#endif
remoteComponent = null;
}
return remoteComponent;
}
public object GetSharedData(string id) {
if (!forwardedComponents.TryGetValue(id, out PVersionList all))
all = null;
return all?.SharedData;
}
/// <summary>
/// Goes through the forwarded components, and picks the latest version of each to
/// instantiate.
/// </summary>
public void Instantiate() {
foreach (var pair in forwardedComponents) {
// Sort value by version
var versions = pair.Value.Components;
int n = versions.Count;
if (n > 0) {
string id = pair.Key;
versions.Sort();
var component = versions[n - 1];
latestComponents.GetOrAdd(id, component);
#if DEBUG
PRegistry.LogPatchDebug("Instantiating component {0} using version {1} from assembly {2}".F(
id, component.Version, component.GetOwningAssembly().FullName));
#endif
try {
instantiatedComponents.GetOrAdd(id, component?.DoInitialize(
PLibInstance));
} catch (Exception e) {
PRegistry.LogPatchWarning("Error when instantiating component " + id +
":");
PUtil.LogException(e);
}
}
}
// Post initialize for component compatibility
foreach (var pair in latestComponents)
try {
pair.Value.PostInitialize(PLibInstance);
} catch (Exception e) {
PRegistry.LogPatchWarning("Error when instantiating component " +
pair.Key + ":");
PUtil.LogException(e);
}
}
public void SetSharedData(string id, object data) {
if (forwardedComponents.TryGetValue(id, out PVersionList all))
all.SharedData = data;
}
public override string ToString() {
return forwardedComponents.ToString();
}
}
}