/* * 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); } } }