/*
* 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 Newtonsoft.Json;
using System;
using System.IO;
using System.Reflection;
using System.Text;
namespace PeterHan.PLib.Core {
///
/// A library component that is forwarded across multiple assemblies, to allow only the
/// latest version available on the system to run. Provides methods to marshal some
/// objects across the assembly boundaries.
///
public abstract class PForwardedComponent : IComparable {
///
/// The default maximum serialization depth for marshaling data.
///
public const int MAX_DEPTH = 8;
///
/// The data stored in this object. It can be retrieved, with optional round trip
/// serialization, by the instantiated version of this component.
///
protected virtual object InstanceData { get; set; }
///
/// The ID used by PLib for this component.
///
/// This method is non-virtual for a reason, as the ID is sometimes only available
/// on methods of type object, so GetType().FullName is used directly there.
///
public string ID {
get {
return GetType().FullName;
}
}
///
/// The JSON serialization settings to be used if the Data is marshaled across
/// assembly boundaries.
///
protected JsonSerializer SerializationSettings { get; set; }
///
/// Retrieves the version of the component provided by this assembly.
///
public abstract Version Version { get; }
///
/// Whether this object has been registered.
///
private volatile bool registered;
///
/// Serializes access to avoid race conditions when registering this component.
///
private readonly object candidateLock;
protected PForwardedComponent() {
candidateLock = new object();
InstanceData = null;
registered = false;
SerializationSettings = new JsonSerializer() {
DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
Culture = System.Globalization.CultureInfo.InvariantCulture,
MaxDepth = MAX_DEPTH
};
}
///
/// Called only on the first instance of a particular component to be registered.
/// For some particular components that need very early patches, this call might be
/// required to initialize state before the rest of the forwarded components are
/// initialized. However, this call might occur on a version that is not the latest of
/// this component in the system, or on an instance that will not be instantiated or
/// initialized by the other callbacks.
///
/// The Harmony instance to use for patching if necessary.
public virtual void Bootstrap(Harmony plibInstance) { }
public int CompareTo(PForwardedComponent other) {
return Version.CompareTo(other.Version);
}
///
/// Initializes this component. Only called on the version that is selected as the
/// latest.
///
/// The Harmony instance to use for patching if necessary.
/// The initialized instance.
internal virtual object DoInitialize(Harmony plibInstance) {
Initialize(plibInstance);
return this;
}
///
/// Gets the data from this component as a specific type. Only works if the type is
/// shared across all mods (in some shared assembly's memory space) such as types in
/// System or the base game.
///
/// The data type to retrieve.
/// The default value if the instance data is unset.
/// The data, or defValue if the instance data has not been set.
public T GetInstanceData(T defValue = default) {
if (!(InstanceData is T outData))
outData = defValue;
return outData;
}
///
/// Gets the data from this component, serialized to the specified type. The data is
/// retrieved from the base component, serialized with JSON, and reconstituted as type
/// T in the memory space of the caller.
///
/// The target type must exist and be a [JsonObject] in both this assembly and the
/// target component's assembly.
///
/// This method is somewhat slow and memory intensive, and should be used sparingly.
///
/// The data type to retrieve and into which to convert.
/// The default value if the instance data is unset.
/// The data, or defValue if the instance data has not been set or cannot be serialized.
public T GetInstanceDataSerialized(T defValue = default) {
var remoteData = InstanceData;
T result = defValue;
using (var buffer = new MemoryStream(1024)) {
try {
var writer = new StreamWriter(buffer, Encoding.UTF8);
SerializationSettings.Serialize(writer, remoteData);
writer.Flush();
buffer.Position = 0L;
var reader = new StreamReader(buffer, Encoding.UTF8);
if (SerializationSettings.Deserialize(reader, typeof(T)) is T decoded)
result = decoded;
} catch (JsonException e) {
PUtil.LogError("Unable to serialize instance data for component " + ID +
":");
PUtil.LogException(e);
result = defValue;
}
}
return result;
}
///
/// Gets the shared data between components with this ID as a specific type. Only works
/// if the type is shared across all mods (in some shared assembly's memory space) such
/// as types in System or the base game.
///
/// The data type to retrieve.
/// The default value if the shared data is unset.
/// The data, or defValue if the shared data has not been set.
public T GetSharedData(T defValue = default) {
if (!(PRegistry.Instance.GetSharedData(ID) is T outData))
outData = defValue;
return outData;
}
///
/// Gets the shared data between components with this ID, serialized to the specified
/// type. The shared data is retrieved, serialized with JSON, and reconstituted as type
/// T in the memory space of the caller.
///
/// The target type must exist and be a [JsonObject] in both this assembly and the
/// target component's assembly.
///
/// This method is somewhat slow and memory intensive, and should be used sparingly.
///
/// The data type to retrieve and into which to convert.
/// The default value if the shared data is unset.
/// The data, or defValue if the shared data has not been set or cannot be serialized.
public T GetSharedDataSerialized(T defValue = default) {
var remoteData = PRegistry.Instance.GetSharedData(ID);
T result = defValue;
using (var buffer = new MemoryStream(1024)) {
try {
SerializationSettings.Serialize(new StreamWriter(buffer, Encoding.UTF8),
remoteData);
buffer.Position = 0L;
if (SerializationSettings.Deserialize(new StreamReader(buffer,
Encoding.UTF8), typeof(T)) is T decoded)
result = decoded;
} catch (JsonException e) {
PUtil.LogError("Unable to serialize shared data for component " + ID +
":");
PUtil.LogException(e);
result = defValue;
}
}
return result;
}
///
/// Gets the assembly which provides this component.
///
/// The assembly which owns this component.
public virtual Assembly GetOwningAssembly() {
return GetType().Assembly;
}
///
/// Initializes this component. Only called on the version that is selected as the
/// latest. Component initialization order is undefined, so anything relying on another
/// component cannot be used until PostInitialize.
///
/// The Harmony instance to use for patching if necessary.
public abstract void Initialize(Harmony plibInstance);
///
/// Invokes the Process method on all registered components of this type.
///
/// The operation to pass to Process.
/// The arguments to pass to Process.
protected void InvokeAllProcess(uint operation, object args) {
var allComponents = PRegistry.Instance.GetAllComponents(ID);
if (allComponents != null)
foreach (var component in allComponents)
component.Process(operation, args);
}
///
/// Gets a HarmonyMethod instance for manual patching using a method from this class.
///
/// The method name.
/// A reference to that method as a HarmonyMethod for patching.
public HarmonyMethod PatchMethod(string name) {
return new HarmonyMethod(GetType(), name);
}
///
/// Initializes this component. Only called on the version that is selected as the
/// latest. Other components have been initialized when this method is called.
///
/// The Harmony instance to use for patching if necessary.
public virtual void PostInitialize(Harmony plibInstance) { }
///
/// Called on demand by the initialized instance to run processing in all other
/// instances.
///
/// The operation to perform. The meaning of this parameter
/// varies by component.
/// The arguments for processing.
public virtual void Process(uint operation, object args) { }
///
/// Registers this component into the list of versions available for forwarding. This
/// method is thread safe. If this component instance is already registered, it will
/// not be registered again.
///
/// true if the component was registered, or false if it was already registered.
protected bool RegisterForForwarding() {
bool result = false;
lock (candidateLock) {
if (!registered) {
PUtil.InitLibrary(false);
PRegistry.Instance.AddCandidateVersion(this);
registered = result = true;
}
}
return result;
}
///
/// Sets the shared data between components with this ID. Only works if the type is
/// shared across all mods (in some shared assembly's memory space) such as types in
/// System or the base game.
///
/// The new value for the shared data.
public void SetSharedData(object value) {
PRegistry.Instance.SetSharedData(ID, value);
}
}
}