Initial commit
This commit is contained in:
28
mod/PLibCore/Detours/DetourException.cs
Normal file
28
mod/PLibCore/Detours/DetourException.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Detours {
|
||||
/// <summary>
|
||||
/// An exception thrown when constructing a detour.
|
||||
/// </summary>
|
||||
public class DetourException : ArgumentException {
|
||||
public DetourException(string message) : base(message) { }
|
||||
}
|
||||
}
|
||||
53
mod/PLibCore/Detours/DetouredField.cs
Normal file
53
mod/PLibCore/Detours/DetouredField.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Detours {
|
||||
/// <summary>
|
||||
/// Stores delegates used to read and write fields or properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The containing type of the field or property.</typeparam>
|
||||
/// <typeparam name="T">The element type of the field or property.</typeparam>
|
||||
internal sealed class DetouredField<P, T> : IDetouredField<P, T> {
|
||||
/// <summary>
|
||||
/// Invoke to get the field/property value.
|
||||
/// </summary>
|
||||
public Func<P, T> Get { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The field name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoke to set the field/property value. Null if the field is const or readonly.
|
||||
/// </summary>
|
||||
public Action<P, T> Set { get; }
|
||||
|
||||
internal DetouredField(string name, Func<P, T> get, Action<P, T> set) {
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Get = get;
|
||||
Set = set;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("DetouredField[name={0}]", Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
mod/PLibCore/Detours/DetouredMethod.cs
Normal file
73
mod/PLibCore/Detours/DetouredMethod.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Detours {
|
||||
/// <summary>
|
||||
/// Stores a detoured method, only performing the expensive reflection when the detour is
|
||||
/// first used.
|
||||
///
|
||||
/// This class is not thread safe.
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// </summary>
|
||||
public sealed class DetouredMethod<D> where D : Delegate {
|
||||
/// <summary>
|
||||
/// Emulates the ability of Delegate.Invoke to actually call the method.
|
||||
/// </summary>
|
||||
public D Invoke {
|
||||
get {
|
||||
Initialize();
|
||||
return delg;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The method name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The delegate method which will be called.
|
||||
/// </summary>
|
||||
private D delg;
|
||||
|
||||
/// <summary>
|
||||
/// The target type.
|
||||
/// </summary>
|
||||
private readonly Type type;
|
||||
|
||||
internal DetouredMethod(Type type, string name) {
|
||||
this.type = type ?? throw new ArgumentNullException(nameof(type));
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
delg = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the getter and setter functions immediately if necessary.
|
||||
/// </summary>
|
||||
public void Initialize() {
|
||||
if (delg == null)
|
||||
delg = PDetours.Detour<D>(type, Name);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("LazyDetouredMethod[type={1},name={0}]", Name, type.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
mod/PLibCore/Detours/IDetouredField.cs
Normal file
44
mod/PLibCore/Detours/IDetouredField.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Detours {
|
||||
/// <summary>
|
||||
/// An interface that describes a detoured field, which stores delegates used to read and
|
||||
/// write fields or properties.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The containing type of the field or property.</typeparam>
|
||||
/// <typeparam name="T">The element type of the field or property.</typeparam>
|
||||
public interface IDetouredField<P, T> {
|
||||
/// <summary>
|
||||
/// Invoke to get the field/property value.
|
||||
/// </summary>
|
||||
Func<P, T> Get { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The field name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoke to set the field/property value.
|
||||
/// </summary>
|
||||
Action<P, T> Set { get; }
|
||||
}
|
||||
}
|
||||
93
mod/PLibCore/Detours/LazyDetouredField.cs
Normal file
93
mod/PLibCore/Detours/LazyDetouredField.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Detours {
|
||||
/// <summary>
|
||||
/// Stores delegates used to read and write fields or properties. This version is lazy and
|
||||
/// only calculates the destination when it is first used.
|
||||
///
|
||||
/// This class is not thread safe.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The containing type of the field or property.</typeparam>
|
||||
/// <typeparam name="T">The element type of the field or property.</typeparam>
|
||||
internal sealed class LazyDetouredField<P, T> : IDetouredField<P, T> {
|
||||
/// <summary>
|
||||
/// Invoke to get the field/property value.
|
||||
/// </summary>
|
||||
public Func<P, T> Get {
|
||||
get {
|
||||
Initialize();
|
||||
return getter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The field name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoke to set the field/property value.
|
||||
/// </summary>
|
||||
public Action<P, T> Set {
|
||||
get {
|
||||
Initialize();
|
||||
return setter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The function to get the field value.
|
||||
/// </summary>
|
||||
private Func<P, T> getter;
|
||||
|
||||
/// <summary>
|
||||
/// The function to set the field value.
|
||||
/// </summary>
|
||||
private Action<P, T> setter;
|
||||
|
||||
/// <summary>
|
||||
/// The target type.
|
||||
/// </summary>
|
||||
private readonly Type type;
|
||||
|
||||
internal LazyDetouredField(Type type, string name) {
|
||||
this.type = type ?? throw new ArgumentNullException(nameof(type));
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
getter = null;
|
||||
setter = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the getter and setter functions immediately if necessary.
|
||||
/// </summary>
|
||||
public void Initialize() {
|
||||
if (getter == null && setter == null) {
|
||||
var dt = PDetours.DetourField<P, T>(Name);
|
||||
getter = dt.Get;
|
||||
setter = dt.Set;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("LazyDetouredField[type={1},name={0}]", Name, type.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
600
mod/PLibCore/Detours/PDetours.cs
Normal file
600
mod/PLibCore/Detours/PDetours.cs
Normal file
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
* 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 PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace PeterHan.PLib.Detours {
|
||||
/// <summary>
|
||||
/// Efficiently detours around many changes in the game by creating detour methods and
|
||||
/// accessors which are resilient against many types of source compatible but binary
|
||||
/// incompatible changes.
|
||||
/// </summary>
|
||||
public static class PDetours {
|
||||
/// <summary>
|
||||
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||
/// method with the same name as the delegate type. The dynamic method will
|
||||
/// automatically adapt if optional parameters are added, filling in their default
|
||||
/// values.
|
||||
/// </summary>
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// <param name="type">The target type.</param>
|
||||
/// <returns>The detour that will call the method with the name of the delegate type.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||
public static D Detour<D>(this Type type) where D : Delegate {
|
||||
return Detour<D>(type, typeof(D).Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||
/// method with the specified name. The dynamic method will automatically adapt if
|
||||
/// optional parameters are added, filling in their default values.
|
||||
/// </summary>
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// <param name="type">The target type.</param>
|
||||
/// <param name="name">The method name.</param>
|
||||
/// <returns>The detour that will call that method.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||
public static D Detour<D>(this Type type, string name) where D : Delegate {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
var methods = type.GetMethods(PPatchTools.BASE_FLAGS | BindingFlags.Static |
|
||||
BindingFlags.Instance);
|
||||
// Determine delegate return type
|
||||
var expected = DelegateInfo.Create(typeof(D));
|
||||
MethodInfo bestMatch = null;
|
||||
int bestParamCount = int.MaxValue;
|
||||
foreach (var method in methods)
|
||||
if (method.Name == name) {
|
||||
try {
|
||||
var result = ValidateDelegate(expected, method, method.ReturnType);
|
||||
int n = result.Length;
|
||||
// Choose overload with fewest parameters to substitute
|
||||
if (n < bestParamCount) {
|
||||
bestParamCount = n;
|
||||
bestMatch = method;
|
||||
}
|
||||
} catch (DetourException) {
|
||||
// Keep looking
|
||||
}
|
||||
}
|
||||
if (bestMatch == null)
|
||||
throw new DetourException("No match found for {1}.{0}".F(name, type.FullName));
|
||||
return Detour<D>(bestMatch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||
/// constructor. The dynamic method will automatically adapt if optional parameters
|
||||
/// are added, filling in their default values.
|
||||
/// </summary>
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// <param name="type">The target type.</param>
|
||||
/// <returns>The detour that will call that type's constructor.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||
public static D DetourConstructor<D>(this Type type) where D : Delegate {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
var constructors = type.GetConstructors(PPatchTools.BASE_FLAGS | BindingFlags.
|
||||
Instance);
|
||||
// Determine delegate return type
|
||||
var expected = DelegateInfo.Create(typeof(D));
|
||||
ConstructorInfo bestMatch = null;
|
||||
int bestParamCount = int.MaxValue;
|
||||
foreach (var constructor in constructors)
|
||||
try {
|
||||
var result = ValidateDelegate(expected, constructor, type);
|
||||
int n = result.Length;
|
||||
// Choose overload with fewest parameters to substitute
|
||||
if (n < bestParamCount) {
|
||||
bestParamCount = n;
|
||||
bestMatch = constructor;
|
||||
}
|
||||
} catch (DetourException) {
|
||||
// Keep looking
|
||||
}
|
||||
if (bestMatch == null)
|
||||
throw new DetourException("No match found for {0} constructor".F(type.
|
||||
FullName));
|
||||
return Detour<D>(bestMatch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||
/// method with the specified name. The dynamic method will automatically adapt if
|
||||
/// optional parameters are added, filling in their default values.
|
||||
///
|
||||
/// This overload creates a lazy detour that only performs the expensive reflection
|
||||
/// when it is first used.
|
||||
/// </summary>
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// <param name="type">The target type.</param>
|
||||
/// <param name="name">The method name.</param>
|
||||
/// <returns>The detour that will call that method.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match any valid target method.</exception>
|
||||
public static DetouredMethod<D> DetourLazy<D>(this Type type, string name)
|
||||
where D : Delegate {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
return new DetouredMethod<D>(type, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||
/// method with the specified name. The dynamic method will automatically adapt if
|
||||
/// optional parameters are added, filling in their default values.
|
||||
/// </summary>
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// <param name="target">The target method to be called.</param>
|
||||
/// <returns>The detour that will call that method.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match the target.</exception>
|
||||
public static D Detour<D>(this MethodInfo target) where D : Delegate {
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
if (target.ContainsGenericParameters)
|
||||
throw new ArgumentException("Generic types must have all parameters defined");
|
||||
var expected = DelegateInfo.Create(typeof(D));
|
||||
var parentType = target.DeclaringType;
|
||||
var expectedParamTypes = expected.parameterTypes;
|
||||
var actualParams = ValidateDelegate(expected, target, target.ReturnType);
|
||||
int offset = target.IsStatic ? 0 : 1;
|
||||
if (parentType == null)
|
||||
throw new ArgumentException("Method is not declared by an actual type");
|
||||
// Method will be "declared" in the type of the target, as we are detouring around
|
||||
// a method of that type
|
||||
var caller = new DynamicMethod(target.Name + "_Detour", expected.returnType,
|
||||
expectedParamTypes, parentType, true);
|
||||
var generator = caller.GetILGenerator();
|
||||
LoadParameters(generator, actualParams, expectedParamTypes, offset);
|
||||
if (parentType.IsValueType || target.IsStatic)
|
||||
generator.Emit(OpCodes.Call, target);
|
||||
else
|
||||
generator.Emit(OpCodes.Callvirt, target);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
FinishDynamicMethod(caller, actualParams, expectedParamTypes, offset);
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Created delegate {0} for method {1}.{2} with parameters [{3}]".
|
||||
F(caller.Name, parentType.FullName, target.Name, actualParams.Join(",")));
|
||||
#endif
|
||||
return caller.CreateDelegate(typeof(D)) as D;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
|
||||
/// constructor. The dynamic method will automatically adapt if optional parameters
|
||||
/// are added, filling in their default values.
|
||||
/// </summary>
|
||||
/// <typeparam name="D">The delegate type to be used to call the detour.</typeparam>
|
||||
/// <param name="target">The target constructor to be called.</param>
|
||||
/// <returns>The detour that will call that constructor.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match the target.</exception>
|
||||
public static D Detour<D>(this ConstructorInfo target) where D : Delegate {
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
if (target.ContainsGenericParameters)
|
||||
throw new ArgumentException("Generic types must have all parameters defined");
|
||||
if (target.IsStatic)
|
||||
throw new ArgumentException("Static constructors cannot be called manually");
|
||||
var expected = DelegateInfo.Create(typeof(D));
|
||||
var parentType = target.DeclaringType;
|
||||
var expectedParamTypes = expected.parameterTypes;
|
||||
var actualParams = ValidateDelegate(expected, target, parentType);
|
||||
if (parentType == null)
|
||||
throw new ArgumentException("Method is not declared by an actual type");
|
||||
// Method will be "declared" in the type of the target, as we are detouring around
|
||||
// a constructor of that type
|
||||
var caller = new DynamicMethod("Constructor_Detour", expected.returnType,
|
||||
expectedParamTypes, parentType, true);
|
||||
var generator = caller.GetILGenerator();
|
||||
LoadParameters(generator, actualParams, expectedParamTypes, 0);
|
||||
generator.Emit(OpCodes.Newobj, target);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
FinishDynamicMethod(caller, actualParams, expectedParamTypes, 0);
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Created delegate {0} for constructor {1} with parameters [{2}]".
|
||||
F(caller.Name, parentType.FullName, actualParams.Join(",")));
|
||||
#endif
|
||||
return caller.CreateDelegate(typeof(D)) as D;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates dynamic detour methods to wrap a base game field or property with the
|
||||
/// specified name. The detour will still work even if the field is converted to a
|
||||
/// source compatible property and vice versa.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||
/// <typeparam name="T">The type of the field or property element.</typeparam>
|
||||
/// <param name="name">The name of the field or property to be accessed.</param>
|
||||
/// <returns>A detour element that wraps the field or property with common getter and
|
||||
/// setter delegates which will work on both types.</returns>
|
||||
public static IDetouredField<P, T> DetourField<P, T>(string name) {
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
var pt = typeof(P);
|
||||
var field = pt.GetField(name, PPatchTools.BASE_FLAGS | BindingFlags.
|
||||
Static | BindingFlags.Instance);
|
||||
IDetouredField<P, T> d;
|
||||
if (field == null) {
|
||||
try {
|
||||
var property = pt.GetProperty(name, PPatchTools.BASE_FLAGS |
|
||||
BindingFlags.Static | BindingFlags.Instance);
|
||||
if (property == null)
|
||||
throw new DetourException("Unable to find {0} on type {1}".
|
||||
F(name, typeof(P).FullName));
|
||||
d = DetourProperty<P, T>(property);
|
||||
} catch (AmbiguousMatchException) {
|
||||
throw new DetourException("Unable to find {0} on type {1}".
|
||||
F(name, typeof(P).FullName));
|
||||
}
|
||||
} else {
|
||||
if (pt.IsValueType || (pt.IsByRef && (pt.GetElementType()?.IsValueType ??
|
||||
false)))
|
||||
throw new ArgumentException("For accessing struct fields, use DetourStructField");
|
||||
d = DetourField<P, T>(field);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates dynamic detour methods to wrap a base game field or property with the
|
||||
/// specified name. The detour will still work even if the field is converted to a
|
||||
/// source compatible property and vice versa.
|
||||
///
|
||||
/// This overload creates a lazy detour that only performs the expensive reflection
|
||||
/// when it is first used.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||
/// <typeparam name="T">The type of the field or property element.</typeparam>
|
||||
/// <param name="name">The name of the field or property to be accessed.</param>
|
||||
/// <returns>A detour element that wraps the field or property with common getter and
|
||||
/// setter delegates which will work on both types.</returns>
|
||||
public static IDetouredField<P, T> DetourFieldLazy<P, T>(string name) {
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
return new LazyDetouredField<P, T>(typeof(P), name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates dynamic detour methods to wrap a base game field with the specified name.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||
/// <typeparam name="T">The type of the field element.</typeparam>
|
||||
/// <param name="target">The field which will be accessed.</param>
|
||||
/// <returns>A detour element that wraps the field with a common interface matching
|
||||
/// that of a detoured property.</returns>
|
||||
private static IDetouredField<P, T> DetourField<P, T>(FieldInfo target) {
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
var parentType = target.DeclaringType;
|
||||
string name = target.Name;
|
||||
if (parentType != typeof(P))
|
||||
throw new ArgumentException("Parent type does not match delegate to be created");
|
||||
var getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
|
||||
typeof(P)
|
||||
}, true);
|
||||
var generator = getter.GetILGenerator();
|
||||
// Getter will load the first argument and use ldfld/ldsfld
|
||||
if (target.IsStatic)
|
||||
generator.Emit(OpCodes.Ldsfld, target);
|
||||
else {
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
generator.Emit(OpCodes.Ldfld, target);
|
||||
}
|
||||
generator.Emit(OpCodes.Ret);
|
||||
DynamicMethod setter;
|
||||
if (target.IsInitOnly)
|
||||
// Handle readonly fields
|
||||
setter = null;
|
||||
else {
|
||||
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
|
||||
typeof(P), typeof(T)
|
||||
}, true);
|
||||
generator = setter.GetILGenerator();
|
||||
// Setter will load both arguments and use stfld/stsfld (argument 1 is ignored
|
||||
// for static fields)
|
||||
if (target.IsStatic) {
|
||||
generator.Emit(OpCodes.Ldarg_1);
|
||||
generator.Emit(OpCodes.Stsfld, target);
|
||||
} else {
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
generator.Emit(OpCodes.Ldarg_1);
|
||||
generator.Emit(OpCodes.Stfld, target);
|
||||
}
|
||||
generator.Emit(OpCodes.Ret);
|
||||
}
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Created delegate for field {0}.{1} with type {2}".
|
||||
F(parentType.FullName, target.Name, typeof(T).FullName));
|
||||
#endif
|
||||
return new DetouredField<P, T>(name, getter.CreateDelegate(typeof(Func<P, T>)) as
|
||||
Func<P, T>, setter?.CreateDelegate(typeof(Action<P, T>)) as Action<P, T>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates dynamic detour methods to wrap a base game property with the specified name.
|
||||
/// </summary>
|
||||
/// <typeparam name="P">The type of the parent class.</typeparam>
|
||||
/// <typeparam name="T">The type of the property element.</typeparam>
|
||||
/// <param name="target">The property which will be accessed.</param>
|
||||
/// <returns>A detour element that wraps the property with a common interface matching
|
||||
/// that of a detoured field.</returns>
|
||||
/// <exception cref="DetourException">If the property has indexers.</exception>
|
||||
private static IDetouredField<P, T> DetourProperty<P, T>(PropertyInfo target) {
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
var parentType = target.DeclaringType;
|
||||
string name = target.Name;
|
||||
if (parentType != typeof(P))
|
||||
throw new ArgumentException("Parent type does not match delegate to be created");
|
||||
var indexes = target.GetIndexParameters();
|
||||
if (indexes != null && indexes.Length > 0)
|
||||
throw new DetourException("Cannot detour on properties with index arguments");
|
||||
DynamicMethod getter, setter;
|
||||
var getMethod = target.GetGetMethod(true);
|
||||
if (target.CanRead && getMethod != null) {
|
||||
getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
|
||||
typeof(P)
|
||||
}, true);
|
||||
var generator = getter.GetILGenerator();
|
||||
// Getter will load the first argument and call the property getter
|
||||
if (!getMethod.IsStatic)
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
generator.Emit(OpCodes.Call, getMethod);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
} else
|
||||
getter = null;
|
||||
var setMethod = target.GetSetMethod(true);
|
||||
if (target.CanWrite && setMethod != null) {
|
||||
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
|
||||
typeof(P), typeof(T)
|
||||
}, true);
|
||||
var generator = setter.GetILGenerator();
|
||||
// Setter will load both arguments and call property setter (argument 1 is
|
||||
// ignored for static properties)
|
||||
if (!setMethod.IsStatic)
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
generator.Emit(OpCodes.Ldarg_1);
|
||||
generator.Emit(OpCodes.Call, setMethod);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
} else
|
||||
setter = null;
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Created delegate for property {0}.{1} with type {2}".
|
||||
F(parentType.FullName, target.Name, typeof(T).FullName));
|
||||
#endif
|
||||
return new DetouredField<P, T>(name, getter?.CreateDelegate(typeof(Func<P, T>)) as
|
||||
Func<P, T>, setter?.CreateDelegate(typeof(Action<P, T>)) as Action<P, T>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates dynamic detour methods to wrap a base game struct field with the specified
|
||||
/// name. For static struct fields, use the regular DetourField.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the field element.</typeparam>
|
||||
/// <param name="parentType">The struct type which will be accessed.</param>
|
||||
/// <param name="name">The name of the struct field to be accessed.</param>
|
||||
/// <returns>A detour element that wraps the field with a common interface matching
|
||||
/// that of a detoured property.</returns>
|
||||
public static IDetouredField<object, T> DetourStructField<T>(this Type parentType,
|
||||
string name) {
|
||||
if (parentType == null)
|
||||
throw new ArgumentNullException(nameof(parentType));
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
var target = parentType.GetField(name, PPatchTools.BASE_FLAGS | BindingFlags.
|
||||
Instance);
|
||||
if (target == null)
|
||||
throw new DetourException("Unable to find {0} on type {1}".F(name, parentType.
|
||||
FullName));
|
||||
var getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
|
||||
typeof(object)
|
||||
}, true);
|
||||
var generator = getter.GetILGenerator();
|
||||
// Getter will load the first argument, unbox. and use ldfld
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
generator.Emit(OpCodes.Unbox, parentType);
|
||||
generator.Emit(OpCodes.Ldfld, target);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
DynamicMethod setter;
|
||||
if (target.IsInitOnly)
|
||||
// Handle readonly fields
|
||||
setter = null;
|
||||
else {
|
||||
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
|
||||
typeof(object), typeof(T)
|
||||
}, true);
|
||||
generator = setter.GetILGenerator();
|
||||
// Setter will load both arguments, unbox and use stfld
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
generator.Emit(OpCodes.Unbox, parentType);
|
||||
generator.Emit(OpCodes.Ldarg_1);
|
||||
generator.Emit(OpCodes.Stfld, target);
|
||||
generator.Emit(OpCodes.Ret);
|
||||
}
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Created delegate for struct field {0}.{1} with type {2}".
|
||||
F(parentType.FullName, target.Name, typeof(T).FullName));
|
||||
#endif
|
||||
return new DetouredField<object, T>(name, getter.CreateDelegate(typeof(
|
||||
Func<object, T>)) as Func<object, T>, setter?.CreateDelegate(typeof(
|
||||
Action<object, T>)) as Action<object, T>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the required method parameters for the dynamic detour method.
|
||||
/// </summary>
|
||||
/// <param name="caller">The method where the parameters will be defined.</param>
|
||||
/// <param name="actualParams">The actual parameters required.</param>
|
||||
/// <param name="expectedParams">The parameters provided.</param>
|
||||
/// <param name="offset">The offset to start loading (0 = static, 1 = instance).</param>
|
||||
private static void FinishDynamicMethod(DynamicMethod caller,
|
||||
ParameterInfo[] actualParams, Type[] expectedParams, int offset) {
|
||||
int n = expectedParams.Length;
|
||||
if (offset > 0)
|
||||
caller.DefineParameter(1, ParameterAttributes.None, "this");
|
||||
for (int i = offset; i < n; i++) {
|
||||
var oldParam = actualParams[i - offset];
|
||||
caller.DefineParameter(i + 1, oldParam.Attributes, oldParam.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates instructions to load arguments or default values onto the stack in a
|
||||
/// detour method.
|
||||
/// </summary>
|
||||
/// <param name="generator">The method where the calls will be added.</param>
|
||||
/// <param name="actualParams">The actual parameters required.</param>
|
||||
/// <param name="expectedParams">The parameters provided.</param>
|
||||
/// <param name="offset">The offset to start loading (0 = static, 1 = instance).</param>
|
||||
private static void LoadParameters(ILGenerator generator, ParameterInfo[] actualParams,
|
||||
Type[] expectedParams, int offset) {
|
||||
// Load the known method arguments onto the stack
|
||||
int n = expectedParams.Length, need = actualParams.Length + offset;
|
||||
if (n > 0)
|
||||
generator.Emit(OpCodes.Ldarg_0);
|
||||
if (n > 1)
|
||||
generator.Emit(OpCodes.Ldarg_1);
|
||||
if (n > 2)
|
||||
generator.Emit(OpCodes.Ldarg_2);
|
||||
if (n > 3)
|
||||
generator.Emit(OpCodes.Ldarg_3);
|
||||
for (int i = 4; i < n; i++)
|
||||
generator.Emit(OpCodes.Ldarg_S, i);
|
||||
// Load the rest as defaults
|
||||
for (int i = n; i < need; i++) {
|
||||
var param = actualParams[i - offset];
|
||||
PTranspilerTools.GenerateDefaultLoad(generator, param.ParameterType, param.
|
||||
DefaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the delegate signature provided in dst can be dynamically mapped to
|
||||
/// the method provided by src, with the possible addition of optional parameters set
|
||||
/// to their default values.
|
||||
/// </summary>
|
||||
/// <param name="expected">The method return type and parameter types expected.</param>
|
||||
/// <param name="actual">The method to be called.</param>
|
||||
/// <param name="actualReturn">The type of the method or constructor's return value.</param>
|
||||
/// <returns>The parameters used in the call to the actual method.</returns>
|
||||
/// <exception cref="DetourException">If the delegate does not match the target.</exception>
|
||||
private static ParameterInfo[] ValidateDelegate(DelegateInfo expected,
|
||||
MethodBase actual, Type actualReturn) {
|
||||
var parameterTypes = expected.parameterTypes;
|
||||
var returnType = expected.returnType;
|
||||
// Validate return types
|
||||
if (!returnType.IsAssignableFrom(actualReturn))
|
||||
throw new DetourException("Return type {0} cannot be converted to type {1}".
|
||||
F(actualReturn.FullName, returnType.FullName));
|
||||
// Do not allow methods declared in not yet closed generic types
|
||||
var baseType = actual.DeclaringType;
|
||||
if (baseType == null)
|
||||
throw new ArgumentException("Method is not declared by an actual type");
|
||||
if (baseType.ContainsGenericParameters)
|
||||
throw new DetourException(("Method parent type {0} must have all " +
|
||||
"generic parameters defined").F(baseType.FullName));
|
||||
// Validate parameter types
|
||||
string actualName = baseType.FullName + "." + actual.Name;
|
||||
var actualParams = actual.GetParameters();
|
||||
int n = actualParams.Length, check = parameterTypes.Length;
|
||||
Type[] actualParamTypes, currentTypes = new Type[n];
|
||||
bool noThisPointer = actual.IsStatic || actual.IsConstructor;
|
||||
for (int i = 0; i < n; i++)
|
||||
currentTypes[i] = actualParams[i].ParameterType;
|
||||
if (noThisPointer)
|
||||
actualParamTypes = currentTypes;
|
||||
else {
|
||||
actualParamTypes = PTranspilerTools.PushDeclaringType(currentTypes, baseType);
|
||||
n++;
|
||||
}
|
||||
if (check > n)
|
||||
throw new DetourException(("Method {0} has only {1:D} parameters, but " +
|
||||
"{2:D} were supplied").F(actual.ToString(), n, check));
|
||||
// Check up to the number we have
|
||||
for (int i = 0; i < check; i++) {
|
||||
Type have = actualParamTypes[i], want = parameterTypes[i];
|
||||
if (!have.IsAssignableFrom(want))
|
||||
throw new DetourException(("Argument {0:D} for method {3} cannot be " +
|
||||
"converted from {1} to {2}").F(i, have.FullName, want.FullName,
|
||||
actualName));
|
||||
}
|
||||
// Any remaining parameters must be optional
|
||||
int offset = noThisPointer ? 0 : 1;
|
||||
for (int i = check; i < n; i++) {
|
||||
var cParam = actualParams[i - offset];
|
||||
if (!cParam.IsOptional)
|
||||
throw new DetourException(("New argument {0:D} for method {1} ({2}) " +
|
||||
"is not optional").F(i, actualName, cParam.ParameterType.FullName));
|
||||
}
|
||||
return actualParams;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores information about a delegate.
|
||||
/// </summary>
|
||||
private sealed class DelegateInfo {
|
||||
/// <summary>
|
||||
/// Creates delegate information on the specified delegate type.
|
||||
/// </summary>
|
||||
/// <param name="delegateType">The delegate type to wrap.</param>
|
||||
/// <returns>Information about that delegate's return and parameter types.</returns>
|
||||
public static DelegateInfo Create(Type delegateType) {
|
||||
if (delegateType == null)
|
||||
throw new ArgumentNullException(nameof(delegateType));
|
||||
var expected = delegateType.GetMethodSafe("Invoke", false, PPatchTools.
|
||||
AnyArguments);
|
||||
if (expected == null)
|
||||
throw new ArgumentException("Invalid delegate type: " + delegateType);
|
||||
return new DelegateInfo(delegateType, expected.GetParameterTypes(),
|
||||
expected.ReturnType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delegate's type.
|
||||
/// </summary>
|
||||
private readonly Type delegateType;
|
||||
|
||||
/// <summary>
|
||||
/// The delegate's parameter types.
|
||||
/// </summary>
|
||||
public readonly Type[] parameterTypes;
|
||||
|
||||
/// <summary>
|
||||
/// The delegate's return types.
|
||||
/// </summary>
|
||||
public readonly Type returnType;
|
||||
|
||||
private DelegateInfo(Type delegateType, Type[] parameterTypes, Type returnType) {
|
||||
this.delegateType = delegateType;
|
||||
this.parameterTypes = parameterTypes;
|
||||
this.returnType = returnType;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "DelegateInfo[delegate={0},return={1},parameters={2}]".F(delegateType,
|
||||
returnType, parameterTypes.Join());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
285
mod/PLibCore/ExtensionMethods.cs
Normal file
285
mod/PLibCore/ExtensionMethods.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Extension methods to make life easier!
|
||||
/// </summary>
|
||||
public static class ExtensionMethods {
|
||||
/// <summary>
|
||||
/// Shorthand for string.Format() which can be invoked directly on the message.
|
||||
/// </summary>
|
||||
/// <param name="message">The format template message.</param>
|
||||
/// <param name="args">The substitutions to be included.</param>
|
||||
/// <returns>The formatted string.</returns>
|
||||
public static string F(this string message, params object[] args) {
|
||||
return string.Format(message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a component, but returns null if the GameObject is disposed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The component type to retrieve.</typeparam>
|
||||
/// <param name="obj">The GameObject that hosts the component.</param>
|
||||
/// <returns>The requested component, or null if it does not exist</returns>
|
||||
public static T GetComponentSafe<T>(this GameObject obj) where T : Component {
|
||||
#pragma warning disable IDE0031 // Use null propagation
|
||||
// == operator is overloaded on GameObject to be equal to null if destroyed
|
||||
return (obj == null) ? null : obj.GetComponent<T>();
|
||||
#pragma warning restore IDE0031 // Use null propagation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly name of an assembly.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to query.</param>
|
||||
/// <returns>The assembly name, or null if assembly is null.</returns>
|
||||
public static string GetNameSafe(this Assembly assembly) {
|
||||
return assembly?.GetName()?.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file version of the specified assembly.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to query</param>
|
||||
/// <returns>The AssemblyFileVersion of that assembly, or null if it could not be determined.</returns>
|
||||
public static string GetFileVersion(this Assembly assembly) {
|
||||
// Mod version
|
||||
var fileVersions = assembly.GetCustomAttributes(typeof(
|
||||
AssemblyFileVersionAttribute), true);
|
||||
string modVersion = null;
|
||||
if (fileVersions != null && fileVersions.Length > 0) {
|
||||
// Retrieves the "File Version" attribute
|
||||
var assemblyFileVersion = (AssemblyFileVersionAttribute)fileVersions[0];
|
||||
if (assemblyFileVersion != null)
|
||||
modVersion = assemblyFileVersion.Version;
|
||||
}
|
||||
return modVersion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coerces a floating point number into the specified range.
|
||||
/// </summary>
|
||||
/// <param name="value">The original number.</param>
|
||||
/// <param name="min">The minimum value (inclusive).</param>
|
||||
/// <param name="max">The maximum value (inclusive).</param>
|
||||
/// <returns>The nearest value between minimum and maximum inclusive to value.</returns>
|
||||
public static double InRange(this double value, double min, double max) {
|
||||
double result = value;
|
||||
if (result < min) result = min;
|
||||
if (result > max) result = max;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coerces a floating point number into the specified range.
|
||||
/// </summary>
|
||||
/// <param name="value">The original number.</param>
|
||||
/// <param name="min">The minimum value (inclusive).</param>
|
||||
/// <param name="max">The maximum value (inclusive).</param>
|
||||
/// <returns>The nearest value between minimum and maximum inclusive to value.</returns>
|
||||
public static float InRange(this float value, float min, float max) {
|
||||
float result = value;
|
||||
if (result < min) result = min;
|
||||
if (result > max) result = max;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coerces an integer into the specified range.
|
||||
/// </summary>
|
||||
/// <param name="value">The original number.</param>
|
||||
/// <param name="min">The minimum value (inclusive).</param>
|
||||
/// <param name="max">The maximum value (inclusive).</param>
|
||||
/// <returns>The nearest value between minimum and maximum inclusive to value.</returns>
|
||||
public static int InRange(this int value, int min, int max) {
|
||||
int result = value;
|
||||
if (result < min) result = min;
|
||||
if (result > max) result = max;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if an object is falling.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to check.</param>
|
||||
/// <returns>true if it is falling, or false otherwise.</returns>
|
||||
public static bool IsFalling(this GameObject obj) {
|
||||
int cell = Grid.PosToCell(obj);
|
||||
return obj.TryGetComponent(out Navigator navigator) && !navigator.IsMoving() &&
|
||||
Grid.IsValidCell(cell) && Grid.IsValidCell(Grid.CellBelow(cell)) &&
|
||||
!navigator.NavGrid.NavTable.IsValid(cell, navigator.CurrentNavType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if a floating point value is NaN or infinite.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check.</param>
|
||||
/// <returns>true if it is NaN, PositiveInfinity, or NegativeInfinity, or false otherwise.</returns>
|
||||
public static bool IsNaNOrInfinity(this double value) {
|
||||
return double.IsNaN(value) || double.IsInfinity(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if a floating point value is NaN or infinite.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check.</param>
|
||||
/// <returns>true if it is NaN, PositiveInfinity, or NegativeInfinity, or false otherwise.</returns>
|
||||
public static bool IsNaNOrInfinity(this float value) {
|
||||
return float.IsNaN(value) || float.IsInfinity(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if a building is usable.
|
||||
/// </summary>
|
||||
/// <param name="building">The building component to check.</param>
|
||||
/// <returns>true if it is usable (enabled, not broken, not overheated), or false otherwise.</returns>
|
||||
public static bool IsUsable(this GameObject building) {
|
||||
return building.TryGetComponent(out Operational op) && op.IsFunctional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a string joining the members of an enumerable.
|
||||
/// </summary>
|
||||
/// <param name="values">The values to join.</param>
|
||||
/// <param name="delimiter">The delimiter to use between values.</param>
|
||||
/// <returns>A string consisting of each value in order, with the delimiter in between.</returns>
|
||||
public static string Join(this System.Collections.IEnumerable values,
|
||||
string delimiter = ",") {
|
||||
var ret = new StringBuilder(128);
|
||||
bool first = true;
|
||||
// Append all, but skip comma if the first time
|
||||
foreach (var value in values) {
|
||||
if (!first)
|
||||
ret.Append(delimiter);
|
||||
ret.Append(value);
|
||||
first = false;
|
||||
}
|
||||
return ret.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches a method manually.
|
||||
/// </summary>
|
||||
/// <param name="instance">The Harmony instance.</param>
|
||||
/// <param name="type">The class to modify.</param>
|
||||
/// <param name="methodName">The method to patch.</param>
|
||||
/// <param name="prefix">The prefix to apply, or null if none.</param>
|
||||
/// <param name="postfix">The postfix to apply, or null if none.</param>
|
||||
public static void Patch(this Harmony instance, Type type, string methodName,
|
||||
HarmonyMethod prefix = null, HarmonyMethod postfix = null) {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
if (string.IsNullOrEmpty(methodName))
|
||||
throw new ArgumentNullException(nameof(methodName));
|
||||
// Fetch the method
|
||||
try {
|
||||
var method = type.GetMethod(methodName, PPatchTools.BASE_FLAGS | BindingFlags.
|
||||
Static | BindingFlags.Instance);
|
||||
if (method != null)
|
||||
instance.Patch(method, prefix, postfix);
|
||||
else
|
||||
PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName,
|
||||
type.FullName));
|
||||
} catch (AmbiguousMatchException e) {
|
||||
#if DEBUG
|
||||
PUtil.LogWarning("When patching candidate method {0}.{1}:".F(type.FullName,
|
||||
methodName));
|
||||
#endif
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches a constructor manually.
|
||||
/// </summary>
|
||||
/// <param name="instance">The Harmony instance.</param>
|
||||
/// <param name="type">The class to modify.</param>
|
||||
/// <param name="arguments">The constructor's argument types.</param>
|
||||
/// <param name="prefix">The prefix to apply, or null if none.</param>
|
||||
/// <param name="postfix">The postfix to apply, or null if none.</param>
|
||||
public static void PatchConstructor(this Harmony instance, Type type,
|
||||
Type[] arguments, HarmonyMethod prefix = null, HarmonyMethod postfix = null) {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
// Fetch the constructor
|
||||
try {
|
||||
var cons = type.GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Static |
|
||||
BindingFlags.Instance, null, arguments, null);
|
||||
if (cons != null)
|
||||
instance.Patch(cons, prefix, postfix);
|
||||
else
|
||||
PUtil.LogWarning("Unable to find constructor on type {0}".F(type.
|
||||
FullName));
|
||||
} catch (ArgumentException e) {
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches a method manually with a transpiler.
|
||||
/// </summary>
|
||||
/// <param name="instance">The Harmony instance.</param>
|
||||
/// <param name="type">The class to modify.</param>
|
||||
/// <param name="methodName">The method to patch.</param>
|
||||
/// <param name="transpiler">The transpiler to apply.</param>
|
||||
public static void PatchTranspile(this Harmony instance, Type type,
|
||||
string methodName, HarmonyMethod transpiler) {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
if (string.IsNullOrEmpty(methodName))
|
||||
throw new ArgumentNullException(nameof(methodName));
|
||||
// Fetch the method
|
||||
try {
|
||||
var method = type.GetMethod(methodName, PPatchTools.BASE_FLAGS |
|
||||
BindingFlags.Static | BindingFlags.Instance);
|
||||
if (method != null)
|
||||
instance.Patch(method, null, null, transpiler);
|
||||
else
|
||||
PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName,
|
||||
type.FullName));
|
||||
} catch (AmbiguousMatchException e) {
|
||||
PUtil.LogException(e);
|
||||
} catch (FormatException e) {
|
||||
PUtil.LogWarning("Unable to transpile method {0}: {1}".F(methodName,
|
||||
e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a game object's parent.
|
||||
/// </summary>
|
||||
/// <param name="child">The game object to modify.</param>
|
||||
/// <param name="parent">The new parent object.</param>
|
||||
/// <returns>The game object, for call chaining.</returns>
|
||||
public static GameObject SetParent(this GameObject child, GameObject parent) {
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
#pragma warning disable IDE0031 // Use null propagation
|
||||
child.transform.SetParent((parent == null) ? null : parent.transform, false);
|
||||
#pragma warning restore IDE0031 // Use null propagation
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
mod/PLibCore/IPLibRegistry.cs
Normal file
68
mod/PLibCore/IPLibRegistry.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// An interface used for both local and remote PLib registry instances.
|
||||
/// </summary>
|
||||
public interface IPLibRegistry {
|
||||
/// <summary>
|
||||
/// Data shared between mods in key value pairs.
|
||||
/// </summary>
|
||||
IDictionary<string, object> ModData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a candidate version of a forwarded component.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance of the component to add.</param>
|
||||
void AddCandidateVersion(PForwardedComponent instance);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest version of a forwarded component of PLib (or another mod).
|
||||
/// </summary>
|
||||
/// <param name="id">The component ID to look up.</param>
|
||||
/// <returns>The latest version of that component, or a forwarded proxy of the
|
||||
/// component if functionality is provided by another mod.</returns>
|
||||
PForwardedComponent GetLatestVersion(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared data for a particular component.
|
||||
/// </summary>
|
||||
/// <param name="id">The component ID that holds the data.</param>
|
||||
/// <returns>The shared data for components with that ID, or null if no component by
|
||||
/// that name was found, or if the data is unset.</returns>
|
||||
object GetSharedData(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered forwarded components for the given ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The component ID to look up.</param>
|
||||
/// <returns>All registered components with that ID, with forwarded proxies for any
|
||||
/// whose functionality is provided by another mod.</returns>
|
||||
IEnumerable<PForwardedComponent> GetAllComponents(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the shared data for a particular component.
|
||||
/// </summary>
|
||||
/// <param name="id">The component ID that holds the data.</param>
|
||||
/// <param name="data">The new shared data value.</param>
|
||||
void SetSharedData(string id, object data);
|
||||
}
|
||||
}
|
||||
31
mod/PLibCore/IRefreshUserMenu.cs
Normal file
31
mod/PLibCore/IRefreshUserMenu.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Implemented by classes which want to use the utility user menu refresh to save some
|
||||
/// boilerplate code.
|
||||
/// </summary>
|
||||
public interface IRefreshUserMenu {
|
||||
/// <summary>
|
||||
/// Called when the user button menu in the info panel is refreshed. Since the
|
||||
/// arguments are always null, no parameter is passed.
|
||||
/// </summary>
|
||||
void OnRefreshUserMenu();
|
||||
}
|
||||
}
|
||||
292
mod/PLibCore/PForwardedComponent.cs
Normal file
292
mod/PLibCore/PForwardedComponent.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 {
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public abstract class PForwardedComponent : IComparable<PForwardedComponent> {
|
||||
/// <summary>
|
||||
/// The default maximum serialization depth for marshaling data.
|
||||
/// </summary>
|
||||
public const int MAX_DEPTH = 8;
|
||||
|
||||
/// <summary>
|
||||
/// The data stored in this object. It can be retrieved, with optional round trip
|
||||
/// serialization, by the instantiated version of this component.
|
||||
/// </summary>
|
||||
protected virtual object InstanceData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public string ID {
|
||||
get {
|
||||
return GetType().FullName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The JSON serialization settings to be used if the Data is marshaled across
|
||||
/// assembly boundaries.
|
||||
/// </summary>
|
||||
protected JsonSerializer SerializationSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the version of the component provided by this assembly.
|
||||
/// </summary>
|
||||
public abstract Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this object has been registered.
|
||||
/// </summary>
|
||||
private volatile bool registered;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes access to avoid race conditions when registering this component.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||
public virtual void Bootstrap(Harmony plibInstance) { }
|
||||
|
||||
public int CompareTo(PForwardedComponent other) {
|
||||
return Version.CompareTo(other.Version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this component. Only called on the version that is selected as the
|
||||
/// latest.
|
||||
/// </summary>
|
||||
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||
/// <returns>The initialized instance.</returns>
|
||||
internal virtual object DoInitialize(Harmony plibInstance) {
|
||||
Initialize(plibInstance);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type to retrieve.</typeparam>
|
||||
/// <param name="defValue">The default value if the instance data is unset.</param>
|
||||
/// <returns>The data, or defValue if the instance data has not been set.</returns>
|
||||
public T GetInstanceData<T>(T defValue = default) {
|
||||
if (!(InstanceData is T outData))
|
||||
outData = defValue;
|
||||
return outData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type to retrieve and into which to convert.</typeparam>
|
||||
/// <param name="defValue">The default value if the instance data is unset.</param>
|
||||
/// <returns>The data, or defValue if the instance data has not been set or cannot be serialized.</returns>
|
||||
public T GetInstanceDataSerialized<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type to retrieve.</typeparam>
|
||||
/// <param name="defValue">The default value if the shared data is unset.</param>
|
||||
/// <returns>The data, or defValue if the shared data has not been set.</returns>
|
||||
public T GetSharedData<T>(T defValue = default) {
|
||||
if (!(PRegistry.Instance.GetSharedData(ID) is T outData))
|
||||
outData = defValue;
|
||||
return outData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type to retrieve and into which to convert.</typeparam>
|
||||
/// <param name="defValue">The default value if the shared data is unset.</param>
|
||||
/// <returns>The data, or defValue if the shared data has not been set or cannot be serialized.</returns>
|
||||
public T GetSharedDataSerialized<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly which provides this component.
|
||||
/// </summary>
|
||||
/// <returns>The assembly which owns this component.</returns>
|
||||
public virtual Assembly GetOwningAssembly() {
|
||||
return GetType().Assembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||
public abstract void Initialize(Harmony plibInstance);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the Process method on all registered components of this type.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation to pass to Process.</param>
|
||||
/// <param name="args">The arguments to pass to Process.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a HarmonyMethod instance for manual patching using a method from this class.
|
||||
/// </summary>
|
||||
/// <param name="name">The method name.</param>
|
||||
/// <returns>A reference to that method as a HarmonyMethod for patching.</returns>
|
||||
public HarmonyMethod PatchMethod(string name) {
|
||||
return new HarmonyMethod(GetType(), name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this component. Only called on the version that is selected as the
|
||||
/// latest. Other components have been initialized when this method is called.
|
||||
/// </summary>
|
||||
/// <param name="plibInstance">The Harmony instance to use for patching if necessary.</param>
|
||||
public virtual void PostInitialize(Harmony plibInstance) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called on demand by the initialized instance to run processing in all other
|
||||
/// instances.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation to perform. The meaning of this parameter
|
||||
/// varies by component.</param>
|
||||
/// <param name="args">The arguments for processing.</param>
|
||||
public virtual void Process(uint operation, object args) { }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>true if the component was registered, or false if it was already registered.</returns>
|
||||
protected bool RegisterForForwarding() {
|
||||
bool result = false;
|
||||
lock (candidateLock) {
|
||||
if (!registered) {
|
||||
PUtil.InitLibrary(false);
|
||||
PRegistry.Instance.AddCandidateVersion(this);
|
||||
registered = result = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="value">The new value for the shared data.</param>
|
||||
public void SetSharedData(object value) {
|
||||
PRegistry.Instance.SetSharedData(ID, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
143
mod/PLibCore/PGameUtils.cs
Normal file
143
mod/PLibCore/PGameUtils.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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 System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Utility and helper functions to perform common game-related (not UI) tasks.
|
||||
/// </summary>
|
||||
public static class PGameUtils {
|
||||
/// <summary>
|
||||
/// Centers and selects an entity.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to center and focus.</param>
|
||||
public static void CenterAndSelect(KMonoBehaviour entity) {
|
||||
if (entity != null && entity.TryGetComponent(out KSelectable select))
|
||||
SelectTool.Instance.SelectAndFocus(entity.transform.position, select, Vector3.
|
||||
zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the sounds from one animation to another animation.
|
||||
/// </summary>
|
||||
/// <param name="dstAnim">The destination anim file name.</param>
|
||||
/// <param name="srcAnim">The source anim file name.</param>
|
||||
public static void CopySoundsToAnim(string dstAnim, string srcAnim) {
|
||||
if (string.IsNullOrEmpty(dstAnim))
|
||||
throw new ArgumentNullException(nameof(dstAnim));
|
||||
if (string.IsNullOrEmpty(srcAnim))
|
||||
throw new ArgumentNullException(nameof(srcAnim));
|
||||
var anim = Assets.GetAnim(dstAnim);
|
||||
if (anim != null) {
|
||||
var audioSheet = GameAudioSheets.Get();
|
||||
var animData = anim.GetData();
|
||||
// For each anim in the kanim, look for existing sound events under the old
|
||||
// anim's file name
|
||||
for (int i = 0; i < animData.animCount; i++) {
|
||||
string animName = animData.GetAnim(i)?.name ?? "";
|
||||
var events = audioSheet.GetEvents(srcAnim + "." + animName);
|
||||
if (events != null) {
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Adding {0:D} audio event(s) to anim {1}.{2}".F(events.
|
||||
Count, dstAnim, animName));
|
||||
#endif
|
||||
audioSheet.events[dstAnim + "." + animName] = events;
|
||||
}
|
||||
}
|
||||
} else
|
||||
PUtil.LogWarning("Destination animation \"{0}\" not found!".F(dstAnim));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a popup message at the specified cell location on the Move layer.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to display, likely from PopFXManager.Instance.</param>
|
||||
/// <param name="text">The text to display.</param>
|
||||
/// <param name="cell">The cell location to create the message.</param>
|
||||
public static void CreatePopup(Sprite image, string text, int cell) {
|
||||
CreatePopup(image, text, Grid.CellToPosCBC(cell, Grid.SceneLayer.Move));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a popup message at the specified location.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to display, likely from PopFXManager.Instance.</param>
|
||||
/// <param name="text">The text to display.</param>
|
||||
/// <param name="position">The position to create the message.</param>
|
||||
public static void CreatePopup(Sprite image, string text, Vector3 position) {
|
||||
PopFXManager.Instance.SpawnFX(image, text, null, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default user menu handler for a class implementing IRefreshUserMenu.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class to handle events.</typeparam>
|
||||
/// <returns>A handler which can be used to Subscribe for RefreshUserMenu events.</returns>
|
||||
public static EventSystem.IntraObjectHandler<T> CreateUserMenuHandler<T>()
|
||||
where T : Component, IRefreshUserMenu {
|
||||
return new Action<T, object>((T target, object ignore) => {
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("OnRefreshUserMenu<{0}> on {1}".F(typeof(T).Name, target));
|
||||
#endif
|
||||
target.OnRefreshUserMenu();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an object layer by its name, resolving the value at runtime to handle
|
||||
/// differences in the layer enum. This method is slower than a direct lookup -
|
||||
/// consider caching the result.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the layer (use nameof()!)</param>
|
||||
/// <param name="defValue">The default value (use the value at compile time)</param>
|
||||
/// <returns>The value to use for this object layer.</returns>
|
||||
public static ObjectLayer GetObjectLayer(string name, ObjectLayer defValue) {
|
||||
if (!Enum.TryParse(name, out ObjectLayer value))
|
||||
value = defValue;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlights an entity. Use Color.black to unhighlight it.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity to highlight.</param>
|
||||
/// <param name="highlightColor">The color to highlight it.</param>
|
||||
public static void HighlightEntity(Component entity, Color highlightColor) {
|
||||
if (entity != null && entity.TryGetComponent(out KAnimControllerBase kbac))
|
||||
kbac.HighlightColour = highlightColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sound effect.
|
||||
/// </summary>
|
||||
/// <param name="name">The sound effect name to play.</param>
|
||||
/// <param name="position">The position where the sound is generated.</param>
|
||||
public static void PlaySound(string name, Vector3 position) {
|
||||
SoundEvent.PlayOneShot(GlobalAssets.GetSound(name), position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current list of mods.
|
||||
/// </summary>
|
||||
public static void SaveMods() {
|
||||
Global.Instance.modManager.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
mod/PLibCore/PLibCore.csproj
Normal file
24
mod/PLibCore/PLibCore.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Title>PLib Core</Title>
|
||||
<AssemblyTitle>PLib.Core</AssemblyTitle>
|
||||
<Version>4.11.0.0</Version>
|
||||
<UsesPLib>false</UsesPLib>
|
||||
<RootNamespace>PeterHan.PLib.Core</RootNamespace>
|
||||
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
|
||||
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
|
||||
<DistributeMod>false</DistributeMod>
|
||||
<PLibCore>true</PLibCore>
|
||||
<Platforms>Vanilla;Mergedown</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\$(Platform)\Release\PLibCore.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Utils\**" />
|
||||
<EmbeddedResource Remove="Utils\**" />
|
||||
<None Remove="Utils\**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
103
mod/PLibCore/PLibCorePatches.cs
Normal file
103
mod/PLibCore/PLibCorePatches.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// A small component which applies core patches used by PLib.
|
||||
/// </summary>
|
||||
internal sealed class PLibCorePatches : PForwardedComponent {
|
||||
/// <summary>
|
||||
/// The version of this component. Uses the running PLib version.
|
||||
/// </summary>
|
||||
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||
|
||||
/// <summary>
|
||||
/// Localizes all mods to the current locale.
|
||||
/// </summary>
|
||||
private static void Initialize_Postfix() {
|
||||
var locale = Localization.GetLocale();
|
||||
if (locale != null) {
|
||||
int n = 0;
|
||||
string locCode = locale.Code;
|
||||
if (string.IsNullOrEmpty(locCode))
|
||||
locCode = Localization.GetCurrentLanguageCode();
|
||||
var libLocal = PRegistry.Instance.GetAllComponents(typeof(PLibCorePatches).
|
||||
FullName);
|
||||
if (libLocal != null)
|
||||
foreach (var component in libLocal)
|
||||
component.Process(0, locale);
|
||||
var modLocal = PRegistry.Instance.GetAllComponents(
|
||||
"PeterHan.PLib.Database.PLocalization");
|
||||
if (modLocal != null)
|
||||
foreach (var component in modLocal) {
|
||||
component.Process(0, locale);
|
||||
n++;
|
||||
}
|
||||
if (n > 0)
|
||||
PRegistry.LogPatchDebug("Localized {0:D} mod(s) to locale {1}".F(
|
||||
n, locCode));
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CodeInstruction> LoadPreviewImage_Transpile(
|
||||
IEnumerable<CodeInstruction> body) {
|
||||
var target = typeof(Debug).GetMethodSafe(nameof(Debug.LogFormat), true,
|
||||
typeof(string), typeof(object[]));
|
||||
return (target == null) ? body : PPatchTools.RemoveMethodCall(body, target);
|
||||
}
|
||||
|
||||
public override Version Version => VERSION;
|
||||
|
||||
public override void Initialize(Harmony plibInstance) {
|
||||
UI.TextMeshProPatcher.Patch();
|
||||
var ugc = PPatchTools.GetTypeSafe("SteamUGCService", "Assembly-CSharp");
|
||||
if (ugc != null)
|
||||
try {
|
||||
plibInstance.PatchTranspile(ugc, "LoadPreviewImage", PatchMethod(nameof(
|
||||
LoadPreviewImage_Transpile)));
|
||||
} catch (Exception e) {
|
||||
#if DEBUG
|
||||
PUtil.LogExcWarn(e);
|
||||
#endif
|
||||
}
|
||||
plibInstance.Patch(typeof(Localization), nameof(Localization.Initialize),
|
||||
postfix: PatchMethod(nameof(Initialize_Postfix)));
|
||||
}
|
||||
|
||||
public override void Process(uint operation, object _) {
|
||||
var locale = Localization.GetLocale();
|
||||
if (locale != null && operation == 0)
|
||||
PLibLocalization.LocalizeItself(locale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers this instance of the PLib core patches.
|
||||
/// </summary>
|
||||
/// <param name="instance">The registry instance to use (since PRegistry.Instance
|
||||
/// is not yet fully initialized).</param>
|
||||
internal void Register(IPLibRegistry instance) {
|
||||
if (instance == null)
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
instance.AddCandidateVersion(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
mod/PLibCore/PLibLocalization.cs
Normal file
78
mod/PLibCore/PLibLocalization.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Handles localization of PLib for mods by automatically loading po files stored as
|
||||
/// EmbeddedResources in PLibCore.dll and ILMerged with the mod assembly.
|
||||
/// </summary>
|
||||
public static class PLibLocalization {
|
||||
/// <summary>
|
||||
/// The file extension used for localization files.
|
||||
/// </summary>
|
||||
public const string TRANSLATIONS_EXT = ".po";
|
||||
|
||||
/// <summary>
|
||||
/// The Prefix of LogicalName of EmbeddedResources that stores the content of po files.
|
||||
/// Must match the specified value in the Directory.Build.targets file.
|
||||
/// </summary>
|
||||
private const string TRANSLATIONS_RES_PATH = "PeterHan.PLib.Core.PLibStrings.";
|
||||
|
||||
/// <summary>
|
||||
/// Localizes the PLib strings.
|
||||
/// </summary>
|
||||
/// <param name="locale">The locale to use.</param>
|
||||
internal static void LocalizeItself(Localization.Locale locale) {
|
||||
if (locale == null)
|
||||
throw new ArgumentNullException(nameof(locale));
|
||||
Localization.RegisterForTranslation(typeof(PLibStrings));
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
string locCode = locale.Code;
|
||||
if (string.IsNullOrEmpty(locCode))
|
||||
locCode = Localization.GetCurrentLanguageCode();
|
||||
try {
|
||||
using (var stream = assembly.GetManifestResourceStream(
|
||||
TRANSLATIONS_RES_PATH + locCode + TRANSLATIONS_EXT)) {
|
||||
if (stream != null) {
|
||||
// File.ReadAllLines does not work on streams unfortunately
|
||||
var lines = new List<string>(128);
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8)) {
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
lines.Add(line);
|
||||
}
|
||||
Localization.OverloadStrings(Localization.ExtractTranslatedStrings(
|
||||
lines.ToArray()));
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Localizing PLib Core to locale {0}".F(locCode));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
PUtil.LogWarning("Failed to load {0} localization for PLib Core:".F(locCode));
|
||||
PUtil.LogExcWarn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
mod/PLibCore/PLibStrings.cs
Normal file
204
mod/PLibCore/PLibStrings.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Strings used in PLib.
|
||||
/// </summary>
|
||||
public static class PLibStrings {
|
||||
/// <summary>
|
||||
/// The button used to manually edit the mod configuration.
|
||||
/// </summary>
|
||||
public static LocString BUTTON_MANUAL = "MANUAL CONFIG";
|
||||
|
||||
/// <summary>
|
||||
/// The button used to reset the configuration to its default value.
|
||||
/// </summary>
|
||||
public static LocString BUTTON_RESET = "RESET TO DEFAULT";
|
||||
|
||||
/// <summary>
|
||||
/// The text shown on the Done button.
|
||||
/// </summary>
|
||||
public static LocString BUTTON_OK = STRINGS.UI.FRONTEND.OPTIONS_SCREEN.BACK;
|
||||
|
||||
/// <summary>
|
||||
/// The text shown on the Options button.
|
||||
/// </summary>
|
||||
public static LocString BUTTON_OPTIONS = STRINGS.UI.FRONTEND.MAINMENU.OPTIONS;
|
||||
|
||||
/// <summary>
|
||||
/// The dialog title used for options, where {0} is substituted with the mod friendly name.
|
||||
/// </summary>
|
||||
public static LocString DIALOG_TITLE = "Options for {0}";
|
||||
|
||||
// Utility key names
|
||||
public static LocString KEY_HOME = "Home";
|
||||
public static LocString KEY_END = "End";
|
||||
public static LocString KEY_DELETE = "Delete";
|
||||
public static LocString KEY_PAGEUP = "Page Up";
|
||||
public static LocString KEY_PAGEDOWN = "Page Down";
|
||||
public static LocString KEY_SYSRQ = "SysRq";
|
||||
public static LocString KEY_PRTSCREEN = "Print Screen";
|
||||
public static LocString KEY_PAUSE = "Pause";
|
||||
|
||||
// Arrow key names
|
||||
public static LocString KEY_ARROWLEFT = "Left Arrow";
|
||||
public static LocString KEY_ARROWUP = "Up Arrow";
|
||||
public static LocString KEY_ARROWRIGHT = "Right Arrow";
|
||||
public static LocString KEY_ARROWDOWN = "Down Arrow";
|
||||
|
||||
/// <summary>
|
||||
/// The title used for the PLib key bind category.
|
||||
/// </summary>
|
||||
public static LocString KEY_CATEGORY_TITLE = "Mods";
|
||||
|
||||
/// <summary>
|
||||
/// The abbreviation text shown on the Blue field.
|
||||
/// </summary>
|
||||
public static LocString LABEL_B = "B";
|
||||
|
||||
/// <summary>
|
||||
/// The abbreviation text shown on the Green field.
|
||||
/// </summary>
|
||||
public static LocString LABEL_G = "G";
|
||||
|
||||
/// <summary>
|
||||
/// The abbreviation text shown on the Red field.
|
||||
/// </summary>
|
||||
public static LocString LABEL_R = "R";
|
||||
|
||||
/// <summary>
|
||||
/// The mod version in Mod Options if retrieved from the default AssemblyVersion, where
|
||||
/// {0} is substituted with the version text.
|
||||
/// </summary>
|
||||
public static LocString MOD_ASSEMBLY_VERSION = "Assembly Version: {0}";
|
||||
|
||||
/// <summary>
|
||||
/// The button text which goes to the mod's home page when clicked.
|
||||
/// </summary>
|
||||
public static LocString MOD_HOMEPAGE = "Mod Homepage";
|
||||
|
||||
/// <summary>
|
||||
/// The mod version in Mod Options if specified via AssemblyFileVersion, where {0} is
|
||||
/// substituted with the version text.
|
||||
/// </summary>
|
||||
public static LocString MOD_VERSION = "Mod Version: {0}";
|
||||
|
||||
/// <summary>
|
||||
/// The cancel button in the restart dialog.
|
||||
/// </summary>
|
||||
public static LocString RESTART_CANCEL = STRINGS.UI.FRONTEND.MOD_DIALOGS.RESTART.CANCEL;
|
||||
|
||||
/// <summary>
|
||||
/// The OK button in the restart dialog.
|
||||
/// </summary>
|
||||
public static LocString RESTART_OK = STRINGS.UI.FRONTEND.MOD_DIALOGS.RESTART.OK;
|
||||
|
||||
/// <summary>
|
||||
/// The details tooltip when AVC detects a mod to be outdated.
|
||||
/// </summary>
|
||||
public static LocString OUTDATED_TOOLTIP = "This mod is out of date!\nNew version: <b>{0}</b>\n\nUpdate local mods manually, or use <b>Mod Updater</b> to force update Steam mods";
|
||||
|
||||
/// <summary>
|
||||
/// Displayed when AVC detects a mod to be outdated.
|
||||
/// </summary>
|
||||
public static LocString OUTDATED_WARNING = "<b><style=\"logic_off\">Outdated!</style></b>";
|
||||
|
||||
/// <summary>
|
||||
/// The message prompting the user to restart.
|
||||
/// </summary>
|
||||
public static LocString RESTART_REQUIRED = "Oxygen Not Included must be restarted " +
|
||||
"for these options to take effect.";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the BLUE field in color pickers.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_BLUE = "Blue";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the CANCEL button.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_CANCEL = "Discard changes.";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the GREEN field in color pickers.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_GREEN = "Green";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the Mod Homepage button.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_HOMEPAGE = "Visit the mod's website.";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the Hue slider in color pickers.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_HUE = "Hue";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the MANUAL CONFIG button.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_MANUAL = "Opens the folder containing the full mod configuration.";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for cycling to the next item.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_NEXT = "Next";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the OK button.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_OK = "Save these options. Some mods may require " +
|
||||
"a restart for the options to take effect.";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for cycling to the previous item.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_PREVIOUS = "Previous";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the RED field in color pickers.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_RED = "Red";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the RESET TO DEFAULT button.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_RESET = "Resets the mod configuration to default values.";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the Saturation slider in color pickers.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_SATURATION = "Saturation";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for each category visibility toggle.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_TOGGLE = "Show or hide this options category";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip on the Value slider in color pickers.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_VALUE = "Value";
|
||||
|
||||
/// <summary>
|
||||
/// The tooltip for the mod version.
|
||||
/// </summary>
|
||||
public static LocString TOOLTIP_VERSION = "The currently installed version of this mod.\n\nCompare this version with the mod's Release Notes to see if it is outdated.";
|
||||
}
|
||||
}
|
||||
1028
mod/PLibCore/PPatchTools.cs
Normal file
1028
mod/PLibCore/PPatchTools.cs
Normal file
File diff suppressed because it is too large
Load Diff
137
mod/PLibCore/PRegistry.cs
Normal file
137
mod/PLibCore/PRegistry.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Provides the user facing API to the PLib Registry.
|
||||
/// </summary>
|
||||
public static class PRegistry {
|
||||
/// <summary>
|
||||
/// The singleton instance of this class.
|
||||
/// </summary>
|
||||
public static IPLibRegistry Instance {
|
||||
get {
|
||||
lock (instanceLock) {
|
||||
if (instance == null)
|
||||
Init();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pointer to the active PLib registry.
|
||||
/// </summary>
|
||||
private static IPLibRegistry instance = null;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that PLib can only be initialized by one thread at a time.
|
||||
/// </summary>
|
||||
private static readonly object instanceLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a value from the single-instance share.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the desired data.</typeparam>
|
||||
/// <param name="key">The string key to retrieve. <i>Suggested key format: YourMod.
|
||||
/// Category.KeyName</i></param>
|
||||
/// <returns>The data associated with that key.</returns>
|
||||
public static T GetData<T>(string key) {
|
||||
T value = default;
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
var registry = Instance.ModData;
|
||||
if (registry != null && registry.TryGetValue(key, out object sval) && sval is
|
||||
T newVal)
|
||||
value = newVal;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the patch bootstrapper, creating a PRegistry if not yet present.
|
||||
/// </summary>
|
||||
private static void Init() {
|
||||
const string INTENDED_NAME = "PRegistryComponent";
|
||||
var obj = Global.Instance?.gameObject;
|
||||
if (obj != null) {
|
||||
var plr = obj.GetComponent(INTENDED_NAME);
|
||||
if (plr == null) {
|
||||
var localReg = obj.AddComponent<PRegistryComponent>();
|
||||
// If PLib is ILMerged more than once, PRegistry gets added with a weird
|
||||
// type name including a GUID which does not match GetComponent.Name!
|
||||
string typeName = localReg.GetType().Name;
|
||||
if (typeName != INTENDED_NAME)
|
||||
LogPatchWarning(INTENDED_NAME + " has the type name " + typeName +
|
||||
"; this may be the result of ILMerging PLib more than once!");
|
||||
#if DEBUG
|
||||
LogPatchDebug("Creating PLib Registry from " + System.Reflection.Assembly.
|
||||
GetExecutingAssembly()?.FullName ?? "?");
|
||||
#endif
|
||||
// Patch in the bootstrap method
|
||||
localReg.ApplyBootstrapper();
|
||||
instance = localReg;
|
||||
} else {
|
||||
instance = new PRemoteRegistry(plr);
|
||||
}
|
||||
} else {
|
||||
#if DEBUG
|
||||
LogPatchWarning("Attempted to initialize PLib Registry before Global created!");
|
||||
#endif
|
||||
instance = null;
|
||||
}
|
||||
if (instance != null)
|
||||
new PLibCorePatches().Register(instance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a debug message while patching in PLib patches.
|
||||
/// </summary>
|
||||
/// <param name="message">The debug message.</param>
|
||||
internal static void LogPatchDebug(string message) {
|
||||
Debug.LogFormat("[PLibPatches] {0}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning encountered while patching in PLib patches.
|
||||
/// </summary>
|
||||
/// <param name="message">The warning message.</param>
|
||||
internal static void LogPatchWarning(string message) {
|
||||
Debug.LogWarningFormat("[PLibPatches] {0}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a value into the single-instance share.
|
||||
/// </summary>
|
||||
/// <param name="key">The string key to set. <i>Suggested key format: YourMod.
|
||||
/// Category.KeyName</i></param>
|
||||
/// <param name="value">The data to be associated with that key.</param>
|
||||
public static void PutData(string key, object value) {
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
var registry = Instance.ModData;
|
||||
if (registry != null) {
|
||||
if (registry.ContainsKey(key))
|
||||
registry[key] = value;
|
||||
else
|
||||
registry.Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
mod/PLibCore/PRegistryComponent.cs
Normal file
253
mod/PLibCore/PRegistryComponent.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
105
mod/PLibCore/PStateMachines.cs
Normal file
105
mod/PLibCore/PStateMachines.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 System;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Contains tools for dealing with state machines.
|
||||
/// </summary>
|
||||
public static class PStateMachines {
|
||||
/// <summary>
|
||||
/// Creates and initializes a new state. This method should be used in a postfix patch
|
||||
/// on InitializeStates if new states are to be added.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The state machine type.</typeparam>
|
||||
/// <typeparam name="I">The state machine Instance type.</typeparam>
|
||||
/// <param name="sm">The base state machine.</param>
|
||||
/// <param name="name">The state name.</param>
|
||||
/// <returns>The new state.</returns>
|
||||
public static GameStateMachine<T, I>.State CreateState<T, I>(
|
||||
this GameStateMachine<T, I> sm, string name)
|
||||
where T : GameStateMachine<T, I, IStateMachineTarget, object> where I :
|
||||
GameStateMachine<T, I, IStateMachineTarget, object>.GameInstance {
|
||||
var state = new GameStateMachine<T, I>.State();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
name = "State";
|
||||
if (sm == null)
|
||||
throw new ArgumentNullException(nameof(sm));
|
||||
state.defaultState = sm.GetDefaultState();
|
||||
// Process any sub parameters
|
||||
sm.CreateStates(state);
|
||||
sm.BindState(sm.root, state, name);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and initializes a new state. This method should be used in a postfix patch
|
||||
/// on InitializeStates if new states are to be added.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The state machine type.</typeparam>
|
||||
/// <typeparam name="I">The state machine Instance type.</typeparam>
|
||||
/// <typeparam name="M">The state machine Target type.</typeparam>
|
||||
/// <param name="sm">The base state machine.</param>
|
||||
/// <param name="name">The state name.</param>
|
||||
/// <returns>The new state.</returns>
|
||||
public static GameStateMachine<T, I, M>.State CreateState<T, I, M>(
|
||||
this GameStateMachine<T, I, M> sm, string name) where M : IStateMachineTarget
|
||||
where T : GameStateMachine<T, I, M, object> where I :
|
||||
GameStateMachine<T, I, M, object>.GameInstance {
|
||||
var state = new GameStateMachine<T, I, M>.State();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
name = "State";
|
||||
if (sm == null)
|
||||
throw new ArgumentNullException(nameof(sm));
|
||||
state.defaultState = sm.GetDefaultState();
|
||||
// Process any sub parameters
|
||||
sm.CreateStates(state);
|
||||
sm.BindState(sm.root, state, name);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the existing Enter actions on a state.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to modify.</param>
|
||||
public static void ClearEnterActions(this StateMachine.BaseState state) {
|
||||
if (state != null)
|
||||
state.enterActions.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the existing Exit actions on a state.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to modify.</param>
|
||||
public static void ClearExitActions(this StateMachine.BaseState state) {
|
||||
if (state != null)
|
||||
state.exitActions.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the existing Transition actions on a state. Parameter transitions are not
|
||||
/// affected.
|
||||
/// </summary>
|
||||
/// <param name="state">The state to modify.</param>
|
||||
public static void ClearTransitions(this StateMachine.BaseState state) {
|
||||
if (state != null)
|
||||
state.transitions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
359
mod/PLibCore/PTranspilerTools.cs
Normal file
359
mod/PLibCore/PTranspilerTools.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* 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.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// A utility class with transpiler tools.
|
||||
/// </summary>
|
||||
internal static class PTranspilerTools {
|
||||
/// <summary>
|
||||
/// The opcodes that branch control conditionally.
|
||||
/// </summary>
|
||||
private static readonly ISet<OpCode> BRANCH_CODES;
|
||||
|
||||
/// <summary>
|
||||
/// Opcodes to load an integer onto the stack.
|
||||
/// </summary>
|
||||
internal static readonly OpCode[] LOAD_INT = {
|
||||
OpCodes.Ldc_I4_M1, OpCodes.Ldc_I4_0, OpCodes.Ldc_I4_1, OpCodes.Ldc_I4_2,
|
||||
OpCodes.Ldc_I4_3, OpCodes.Ldc_I4_4, OpCodes.Ldc_I4_5, OpCodes.Ldc_I4_6,
|
||||
OpCodes.Ldc_I4_7, OpCodes.Ldc_I4_8
|
||||
};
|
||||
|
||||
static PTranspilerTools() {
|
||||
// OpCode has a GetHashCode method!
|
||||
BRANCH_CODES = new HashSet<OpCode> {
|
||||
OpCodes.Beq,
|
||||
OpCodes.Beq_S,
|
||||
OpCodes.Bge,
|
||||
OpCodes.Bge_S,
|
||||
OpCodes.Bge_Un,
|
||||
OpCodes.Bge_Un_S,
|
||||
OpCodes.Bgt,
|
||||
OpCodes.Bgt_S,
|
||||
OpCodes.Bgt_Un,
|
||||
OpCodes.Bgt_Un_S,
|
||||
OpCodes.Ble,
|
||||
OpCodes.Ble_S,
|
||||
OpCodes.Ble_Un,
|
||||
OpCodes.Ble_Un_S,
|
||||
OpCodes.Blt,
|
||||
OpCodes.Blt_S,
|
||||
OpCodes.Blt_Un,
|
||||
OpCodes.Blt_Un_S,
|
||||
OpCodes.Bne_Un,
|
||||
OpCodes.Bne_Un_S,
|
||||
OpCodes.Brfalse,
|
||||
OpCodes.Brfalse_S,
|
||||
OpCodes.Brtrue,
|
||||
OpCodes.Brtrue_S,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the method parameters and throws ArgumentException if they do not match.
|
||||
/// </summary>
|
||||
/// <param name="victim">The victim method.</param>
|
||||
/// <param name="paramTypes">The method's parameter types.</param>
|
||||
/// <param name="newMethod">The replacement method.</param>
|
||||
internal static void CompareMethodParams(MethodInfo victim, Type[] paramTypes,
|
||||
MethodInfo newMethod) {
|
||||
var newTypes = GetParameterTypes(newMethod);
|
||||
if (!newMethod.IsStatic)
|
||||
newTypes = PushDeclaringType(newTypes, newMethod.DeclaringType);
|
||||
if (!victim.IsStatic)
|
||||
paramTypes = PushDeclaringType(paramTypes, victim.DeclaringType);
|
||||
int n = paramTypes.Length;
|
||||
// Argument count check
|
||||
if (newTypes.Length != n)
|
||||
throw new ArgumentException(("New method {0} ({1:D} arguments) does not " +
|
||||
"match method {2} ({3:D} arguments)").F(newMethod.Name, newTypes.Length,
|
||||
victim.Name, n));
|
||||
// Argument type check
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!newTypes[i].IsAssignableFrom(paramTypes[i]))
|
||||
throw new ArgumentException(("Argument {0:D}: New method type {1} does " +
|
||||
"not match old method type {2}").F(i, paramTypes[i].FullName,
|
||||
newTypes[i].FullName));
|
||||
if (!victim.ReturnType.IsAssignableFrom(newMethod.ReturnType))
|
||||
throw new ArgumentException(("New method {0} (returns {1}) does not match " +
|
||||
"method {2} (returns {3})").F(newMethod.Name, newMethod.
|
||||
ReturnType, victim.Name, victim.ReturnType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the specified value onto the evaluation stack. This method does not work on
|
||||
/// compound value types or by-ref types, as those need a local variable. If the value
|
||||
/// is DBNull.Value, then default(value) will be used instead.
|
||||
/// </summary>
|
||||
/// <param name="generator">The IL generator where the opcodes will be emitted.</param>
|
||||
/// <param name="type">The type of the value to generate.</param>
|
||||
/// <param name="value">The value to load.</param>
|
||||
/// <returns>true if instructions were pushed (all basic types and reference types),
|
||||
/// or false otherwise (by ref type or compound value type).</returns>
|
||||
private static bool GenerateBasicLoad(ILGenerator generator, Type type, object value) {
|
||||
bool ok = !type.IsByRef;
|
||||
if (ok) {
|
||||
if (type == typeof(int)) {
|
||||
// int
|
||||
if (value is int iVal)
|
||||
generator.Emit(OpCodes.Ldc_I4, iVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(char)) {
|
||||
// char
|
||||
if (value is char cVal)
|
||||
generator.Emit(OpCodes.Ldc_I4, cVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(short)) {
|
||||
// short
|
||||
if (value is short sVal)
|
||||
generator.Emit(OpCodes.Ldc_I4, sVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(uint)) {
|
||||
// uint
|
||||
if (value is uint uiVal)
|
||||
generator.Emit(OpCodes.Ldc_I4, (int)uiVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(ushort)) {
|
||||
// ushort
|
||||
if (value is ushort usVal)
|
||||
generator.Emit(OpCodes.Ldc_I4, usVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(byte)) {
|
||||
// byte (unsigned)
|
||||
if (value is byte bVal)
|
||||
generator.Emit(OpCodes.Ldc_I4_S, bVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(sbyte)) {
|
||||
// byte (signed)
|
||||
if (value is sbyte sbVal)
|
||||
generator.Emit(OpCodes.Ldc_I4, sbVal);
|
||||
else
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
} else if (type == typeof(bool))
|
||||
// bool
|
||||
generator.Emit((value is bool kVal && kVal) ? OpCodes.Ldc_I4_1 : OpCodes.
|
||||
Ldc_I4_0);
|
||||
else if (type == typeof(long))
|
||||
// long
|
||||
generator.Emit(OpCodes.Ldc_I8, (value is long lVal) ? lVal : 0L);
|
||||
else if (type == typeof(ulong))
|
||||
// ulong
|
||||
generator.Emit(OpCodes.Ldc_I8, (value is ulong ulVal) ? (long)ulVal : 0L);
|
||||
else if (type == typeof(float))
|
||||
// float
|
||||
generator.Emit(OpCodes.Ldc_R4, (value is float fVal) ? fVal : 0.0f);
|
||||
else if (type == typeof(double))
|
||||
// double
|
||||
generator.Emit(OpCodes.Ldc_R8, (value is double dVal) ? dVal : 0.0);
|
||||
else if (type == typeof(string))
|
||||
// string
|
||||
generator.Emit(OpCodes.Ldstr, (value is string sVal) ? sVal : "");
|
||||
else if (type.IsPointer)
|
||||
// All pointers
|
||||
generator.Emit(OpCodes.Ldc_I4_0);
|
||||
else if (!type.IsValueType)
|
||||
// All reference types (including Nullable)
|
||||
generator.Emit(OpCodes.Ldnull);
|
||||
else
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a local if necessary, and generates initialization code for the default
|
||||
/// value of the specified type. The resulting value ends up on the stack in a form
|
||||
/// that it would be used for the method argument.
|
||||
/// </summary>
|
||||
/// <param name="generator">The IL generator where the opcodes will be emitted.</param>
|
||||
/// <param name="type">The type to load and initialize.</param>
|
||||
/// <param name="defaultValue">The default value to load.</param>
|
||||
internal static void GenerateDefaultLoad(ILGenerator generator, Type type,
|
||||
object defaultValue) {
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
if (!GenerateBasicLoad(generator, type, defaultValue)) {
|
||||
// This method will fail if there are more than 255 local variables, oh no!
|
||||
if (type.IsByRef) {
|
||||
var baseType = type.GetElementType();
|
||||
var localVariable = generator.DeclareLocal(baseType);
|
||||
int index = localVariable.LocalIndex;
|
||||
if (GenerateBasicLoad(generator, baseType, defaultValue))
|
||||
// Reference type or basic type
|
||||
generator.Emit(OpCodes.Stloc_S, index);
|
||||
else {
|
||||
// Value types not handled by it, ref vars cannot have a default value
|
||||
generator.Emit(OpCodes.Ldloca_S, index);
|
||||
generator.Emit(OpCodes.Initobj, type);
|
||||
}
|
||||
generator.Emit(OpCodes.Ldloca_S, index);
|
||||
} else {
|
||||
var localVariable = generator.DeclareLocal(type);
|
||||
int index = localVariable.LocalIndex;
|
||||
// Is a value type, those cannot have default values other than default()
|
||||
// as it must be constant
|
||||
generator.Emit(OpCodes.Ldloca_S, index);
|
||||
generator.Emit(OpCodes.Initobj, type);
|
||||
generator.Emit(OpCodes.Ldloc_S, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the method's parameter types.
|
||||
/// </summary>
|
||||
/// <param name="method">The method to query.</param>
|
||||
/// <returns>The type of each parameter of the method.</returns>
|
||||
internal static Type[] GetParameterTypes(this MethodInfo method) {
|
||||
if (method == null)
|
||||
throw new ArgumentNullException(nameof(method));
|
||||
var pm = method.GetParameters();
|
||||
int n = pm.Length;
|
||||
var types = new Type[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
types[i] = pm[i].ParameterType;
|
||||
return types;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if an instruction opcode is a branch instruction.
|
||||
/// </summary>
|
||||
/// <param name="opcode">The opcode to check.</param>
|
||||
/// <returns>true if it is a branch, or false otherwise.</returns>
|
||||
internal static bool IsConditionalBranchInstruction(OpCode opcode) {
|
||||
return BRANCH_CODES.Contains(opcode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a logger to all unhandled exceptions.
|
||||
///
|
||||
/// Not for production use.
|
||||
/// </summary>
|
||||
internal static void LogAllExceptions() {
|
||||
AppDomain.CurrentDomain.UnhandledException += OnThrown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a logger to all failed assertions. The assertions will still fail, but a stack
|
||||
/// trace will be printed for each failed assertion.
|
||||
///
|
||||
/// Not for production use.
|
||||
/// </summary>
|
||||
internal static void LogAllFailedAsserts() {
|
||||
var inst = new Harmony("PeterHan.PLib.LogFailedAsserts");
|
||||
MethodBase assert;
|
||||
var handler = new HarmonyMethod(typeof(PTranspilerTools), nameof(OnAssertFailed));
|
||||
try {
|
||||
// Assert(bool)
|
||||
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool));
|
||||
if (assert != null)
|
||||
inst.Patch(assert, handler);
|
||||
// Assert(bool, object)
|
||||
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool), typeof(
|
||||
object));
|
||||
if (assert != null)
|
||||
inst.Patch(assert, handler);
|
||||
// Assert(bool, object, UnityEngine.Object)
|
||||
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool), typeof(
|
||||
object), typeof(UnityEngine.Object));
|
||||
if (assert != null)
|
||||
inst.Patch(assert, handler);
|
||||
} catch (Exception e) {
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modifies a load instruction to load the specified constant, using short forms if
|
||||
/// possible.
|
||||
/// </summary>
|
||||
/// <param name="instruction">The instruction to modify.</param>
|
||||
/// <param name="newValue">The new i4 constant to load.</param>
|
||||
internal static void ModifyLoadI4(CodeInstruction instruction, int newValue) {
|
||||
if (newValue >= -1 && newValue <= 8) {
|
||||
// Short form: constant
|
||||
instruction.opcode = LOAD_INT[newValue + 1];
|
||||
instruction.operand = null;
|
||||
} else if (newValue >= sbyte.MinValue && newValue <= sbyte.MaxValue) {
|
||||
// Short form: -128 to 127 -- looks like Harmony has issues with emitting
|
||||
// the operand as a Byte
|
||||
instruction.opcode = OpCodes.Ldc_I4_S;
|
||||
instruction.operand = newValue;
|
||||
} else {
|
||||
// Long form
|
||||
instruction.opcode = OpCodes.Ldc_I4;
|
||||
instruction.operand = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a failed assertion that is about to occur.
|
||||
/// </summary>
|
||||
internal static void OnAssertFailed(bool condition) {
|
||||
if (!condition) {
|
||||
Debug.LogError("Assert is about to fail:");
|
||||
Debug.LogError(new System.Diagnostics.StackTrace().ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An optional handler for all unhandled exceptions.
|
||||
/// </summary>
|
||||
internal static void OnThrown(object sender, UnhandledExceptionEventArgs e) {
|
||||
if (!e.IsTerminating) {
|
||||
Debug.LogError("Unhandled exception on Thread " + System.Threading.Thread.
|
||||
CurrentThread.Name);
|
||||
if (e.ExceptionObject is Exception ex)
|
||||
Debug.LogException(ex);
|
||||
else
|
||||
Debug.LogError(e.ExceptionObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the declaring instance type to the front of the specified array.
|
||||
/// </summary>
|
||||
/// <param name="types">The parameter types.</param>
|
||||
/// <param name="declaringType">The type which declared this method.</param>
|
||||
/// <returns>The types with declaringType inserted at the beginning.</returns>
|
||||
internal static Type[] PushDeclaringType(Type[] types, Type declaringType) {
|
||||
int n = types.Length;
|
||||
// Allow special case of passing "this" as first static arg
|
||||
var newParamTypes = new Type[n + 1];
|
||||
if (declaringType.IsValueType)
|
||||
declaringType = declaringType.MakeByRefType();
|
||||
newParamTypes[0] = declaringType;
|
||||
for (int i = 0; i < n; i++)
|
||||
newParamTypes[i + 1] = types[i];
|
||||
return newParamTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
252
mod/PLibCore/PUtil.cs
Normal file
252
mod/PLibCore/PUtil.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Static utility functions used across mods.
|
||||
/// </summary>
|
||||
public static class PUtil {
|
||||
/// <summary>
|
||||
/// Retrieves the current changelist version of the game. LU-371502 has a version of
|
||||
/// 371502u.
|
||||
///
|
||||
/// If the version cannot be determined, returns 0.
|
||||
/// </summary>
|
||||
public static uint GameVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether PLib has been initialized.
|
||||
/// </summary>
|
||||
private static volatile bool initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes attempts to initialize PLib.
|
||||
/// </summary>
|
||||
private static readonly object initializeLock;
|
||||
|
||||
/// <summary>
|
||||
/// The characters which are not allowed in file names.
|
||||
/// </summary>
|
||||
private static readonly HashSet<char> INVALID_FILE_CHARS;
|
||||
|
||||
static PUtil() {
|
||||
initialized = false;
|
||||
initializeLock = new object();
|
||||
INVALID_FILE_CHARS = new HashSet<char>(Path.GetInvalidFileNameChars());
|
||||
GameVersion = GetGameVersion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mapping of assembly names to Mod instances. Only works after all mods
|
||||
/// have been loaded.
|
||||
/// </summary>
|
||||
/// <returns>A mapping from assemblies to the Mod instance that owns them.</returns>
|
||||
public static IDictionary<Assembly, KMod.Mod> CreateAssemblyToModTable() {
|
||||
var allMods = Global.Instance?.modManager?.mods;
|
||||
var result = new Dictionary<Assembly, KMod.Mod>(32);
|
||||
if (allMods != null)
|
||||
foreach (var mod in allMods) {
|
||||
var dlls = mod?.loaded_mod_data?.dlls;
|
||||
if (dlls != null)
|
||||
foreach (var assembly in dlls)
|
||||
result[assembly] = mod;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the distance between two points.
|
||||
/// </summary>
|
||||
/// <param name="x1">The first X coordinate.</param>
|
||||
/// <param name="y1">The first Y coordinate.</param>
|
||||
/// <param name="x2">The second X coordinate.</param>
|
||||
/// <param name="y2">The second Y coordinate.</param>
|
||||
/// <returns>The non-taxicab (straight line) distance between the points.</returns>
|
||||
public static float Distance(float x1, float y1, float x2, float y2) {
|
||||
float dx = x2 - x1, dy = y2 - y1;
|
||||
return Mathf.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the distance between two points.
|
||||
/// </summary>
|
||||
/// <param name="x1">The first X coordinate.</param>
|
||||
/// <param name="y1">The first Y coordinate.</param>
|
||||
/// <param name="x2">The second X coordinate.</param>
|
||||
/// <param name="y2">The second Y coordinate.</param>
|
||||
/// <returns>The non-taxicab (straight line) distance between the points.</returns>
|
||||
public static double Distance(double x1, double y1, double x2, double y2) {
|
||||
double dx = x2 - x1, dy = y2 - y1;
|
||||
return Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current game version from the Klei code.
|
||||
/// </summary>
|
||||
/// <returns>The change list version of the game, or 0 if it cannot be determined.</returns>
|
||||
private static uint GetGameVersion() {
|
||||
/*
|
||||
* KleiVersion.ChangeList is a const which is substituted at compile time; if
|
||||
* accessed directly, PLib would have a version "baked in" and would never
|
||||
* update depending on the game version in use.
|
||||
*/
|
||||
var field = PPatchTools.GetFieldSafe(typeof(KleiVersion), nameof(KleiVersion.
|
||||
ChangeList), true);
|
||||
uint ver = 0U;
|
||||
if (field != null && field.GetValue(null) is uint newVer)
|
||||
ver = newVer;
|
||||
return ver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the mod directory for the specified assembly. If an archived version is
|
||||
/// running, the path to that version is reported.
|
||||
/// </summary>
|
||||
/// <param name="modDLL">The assembly used for a mod.</param>
|
||||
/// <returns>The directory where the mod is currently executing.</returns>
|
||||
public static string GetModPath(Assembly modDLL) {
|
||||
if (modDLL == null)
|
||||
throw new ArgumentNullException(nameof(modDLL));
|
||||
string dir = null;
|
||||
try {
|
||||
dir = Directory.GetParent(modDLL.Location)?.FullName;
|
||||
} catch (NotSupportedException e) {
|
||||
// Guess from the Klei strings
|
||||
LogExcWarn(e);
|
||||
} catch (System.Security.SecurityException e) {
|
||||
// Guess from the Klei strings
|
||||
LogExcWarn(e);
|
||||
} catch (IOException e) {
|
||||
// Guess from the Klei strings
|
||||
LogExcWarn(e);
|
||||
}
|
||||
if (dir == null)
|
||||
dir = Path.Combine(KMod.Manager.GetDirectory(), modDLL.GetName()?.Name ?? "");
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes PLib. While most components are initialized dynamically if used, some
|
||||
/// key infrastructure must be initialized first.
|
||||
/// </summary>
|
||||
/// <param name="logVersion">If true, the mod name and version is emitted to the log.</param>
|
||||
public static void InitLibrary(bool logVersion = true) {
|
||||
var assembly = Assembly.GetCallingAssembly();
|
||||
lock (initializeLock) {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
if (assembly != null && logVersion)
|
||||
Debug.LogFormat("[PLib] Mod {0} initialized, version {1}",
|
||||
assembly.GetNameSafe(), assembly.GetFileVersion() ?? "Unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the file is a valid file name. If the argument contains path
|
||||
/// separator characters, this method returns false, since that is not a valid file
|
||||
/// name.
|
||||
///
|
||||
/// Null and empty file names are not valid file names.
|
||||
/// </summary>
|
||||
/// <param name="file">The file name to check.</param>
|
||||
/// <returns>true if the name could be used to name a file, or false otherwise.</returns>
|
||||
public static bool IsValidFileName(string file) {
|
||||
bool valid = (file != null);
|
||||
if (valid) {
|
||||
// Cannot contain characters in INVALID_FILE_CHARS
|
||||
int len = file.Length;
|
||||
for (int i = 0; i < len && valid; i++)
|
||||
if (INVALID_FILE_CHARS.Contains(file[i]))
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a message to the debug log.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void LogDebug(object message) {
|
||||
Debug.LogFormat("[PLib/{0}] {1}", Assembly.GetCallingAssembly().GetNameSafe(),
|
||||
message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message to the debug log.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void LogError(object message) {
|
||||
// Cannot make a utility property or method for Assembly.GetCalling... because
|
||||
// its caller would then be the assembly PLib is in, not the assembly which
|
||||
// invoked LogXXX
|
||||
Debug.LogErrorFormat("[PLib/{0}] {1}", Assembly.GetCallingAssembly().
|
||||
GetNameSafe() ?? "?", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an exception message to the debug log.
|
||||
/// </summary>
|
||||
/// <param name="thrown">The exception to log.</param>
|
||||
public static void LogException(Exception thrown) {
|
||||
Debug.LogErrorFormat("[PLib/{0}] {1} {2} {3}", Assembly.GetCallingAssembly().
|
||||
GetNameSafe() ?? "?", thrown.GetType(), thrown.Message, thrown.StackTrace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an exception message to the debug log at WARNING level.
|
||||
/// </summary>
|
||||
/// <param name="thrown">The exception to log.</param>
|
||||
public static void LogExcWarn(Exception thrown) {
|
||||
Debug.LogWarningFormat("[PLib/{0}] {1} {2} {3}", Assembly.GetCallingAssembly().
|
||||
GetNameSafe() ?? "?", thrown.GetType(), thrown.Message, thrown.StackTrace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning message to the debug log.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void LogWarning(object message) {
|
||||
Debug.LogWarningFormat("[PLib/{0}] {1}", Assembly.GetCallingAssembly().
|
||||
GetNameSafe() ?? "?", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures how long the specified code takes to run. The result is logged to the
|
||||
/// debug log in microseconds.
|
||||
/// </summary>
|
||||
/// <param name="code">The code to execute.</param>
|
||||
/// <param name="header">The name used in the log to describe this code.</param>
|
||||
public static void Time(System.Action code, string header = "Code") {
|
||||
if (code == null)
|
||||
throw new ArgumentNullException(nameof(code));
|
||||
var watch = new System.Diagnostics.Stopwatch();
|
||||
watch.Start();
|
||||
code.Invoke();
|
||||
watch.Stop();
|
||||
LogDebug("{1} took {0:D} us".F(watch.ElapsedTicks * 1000000L / System.Diagnostics.
|
||||
Stopwatch.Frequency, header));
|
||||
}
|
||||
}
|
||||
}
|
||||
30
mod/PLibCore/PVersion.cs
Normal file
30
mod/PLibCore/PVersion.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib {
|
||||
/// <summary>
|
||||
/// Used to pass the PLib version in the ILMerged assembly since the PLib version will
|
||||
/// not be included in the file version.
|
||||
/// </summary>
|
||||
public static class PVersion {
|
||||
/// <summary>
|
||||
/// The PLib version.
|
||||
/// </summary>
|
||||
public const string VERSION = "4.11.0.0";
|
||||
}
|
||||
}
|
||||
45
mod/PLibCore/PVersionList.cs
Normal file
45
mod/PLibCore/PVersionList.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Stores a list of forwarded component versions and their shared data.
|
||||
/// </summary>
|
||||
internal sealed class PVersionList {
|
||||
/// <summary>
|
||||
/// The list of registered components.
|
||||
/// </summary>
|
||||
public List<PForwardedComponent> Components { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The data shared between all components.
|
||||
/// </summary>
|
||||
public object SharedData { get; set; }
|
||||
|
||||
public PVersionList() {
|
||||
Components = new List<PForwardedComponent>(32);
|
||||
SharedData = null;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return Components.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
mod/PLibCore/PatchManager/IPLibAnnotation.cs
Normal file
38
mod/PLibCore/PatchManager/IPLibAnnotation.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 System.Reflection;
|
||||
|
||||
namespace PeterHan.PLib.PatchManager {
|
||||
/// <summary>
|
||||
/// The commmon parent of [PLibPatch] and [PLibMethod].
|
||||
/// </summary>
|
||||
internal interface IPLibAnnotation {
|
||||
/// <summary>
|
||||
/// When this method is run.
|
||||
/// </summary>
|
||||
uint Runtime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new patch method instance.
|
||||
/// </summary>
|
||||
/// <param name="method">The method that was attributed.</param>
|
||||
/// <returns>An instance that can execute this patch.</returns>
|
||||
IPatchMethodInstance CreateInstance(MethodInfo method);
|
||||
}
|
||||
}
|
||||
33
mod/PLibCore/PatchManager/IPatchMethodInstance.cs
Normal file
33
mod/PLibCore/PatchManager/IPatchMethodInstance.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
namespace PeterHan.PLib.PatchManager {
|
||||
/// <summary>
|
||||
/// Refers to a single instance of the annotation, with its annotated method.
|
||||
/// </summary>
|
||||
public interface IPatchMethodInstance {
|
||||
/// <summary>
|
||||
/// Runs the patch or method if the conditions are met. This method should check its
|
||||
/// preconditions before executing the target.
|
||||
/// </summary>
|
||||
/// <param name="instance">The Harmony instance to use.</param>
|
||||
void Run(Harmony instance);
|
||||
}
|
||||
}
|
||||
115
mod/PLibCore/PatchManager/PLibMethodAttribute.cs
Normal file
115
mod/PLibCore/PatchManager/PLibMethodAttribute.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace PeterHan.PLib.PatchManager {
|
||||
/// <summary>
|
||||
/// Represents a method that will be run by PLib at a specific time to reduce the number
|
||||
/// of patches required and allow conditional integration with other mods.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class PLibMethodAttribute : Attribute, IPLibAnnotation {
|
||||
/// <summary>
|
||||
/// Requires the specified assembly to be loaded for this method to run. If RequireType
|
||||
/// is null or empty, no particular types need to be defined in the assembly. The
|
||||
/// assembly name is required, but the version is optional (strong named assemblies
|
||||
/// can never load in ONI, since neither Unity nor Klei types are strong named...)
|
||||
/// </summary>
|
||||
public string RequireAssembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Requires the specified type full name (not assembly qualified name) to exist for
|
||||
/// this method to run. If RequireAssembly is null or empty, a type in any assembly
|
||||
/// will satisfy the requirement.
|
||||
/// </summary>
|
||||
public string RequireType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this method is run.
|
||||
/// </summary>
|
||||
public uint Runtime { get; }
|
||||
|
||||
public PLibMethodAttribute(uint runtime) {
|
||||
Runtime = runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new patch method instance.
|
||||
/// </summary>
|
||||
/// <param name="method">The method that was attributed.</param>
|
||||
/// <returns>An instance that can execute this patch.</returns>
|
||||
public IPatchMethodInstance CreateInstance(MethodInfo method) {
|
||||
return new PLibMethodInstance(this, method);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "PLibMethod[RunAt={0}]".F(RunAt.ToString(Runtime));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refers to a single instance of the annotation, with its annotated method.
|
||||
/// </summary>
|
||||
internal sealed class PLibMethodInstance : IPatchMethodInstance {
|
||||
/// <summary>
|
||||
/// The attribute describing the method.
|
||||
/// </summary>
|
||||
public PLibMethodAttribute Descriptor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The method to run.
|
||||
/// </summary>
|
||||
public MethodInfo Method { get; }
|
||||
|
||||
public PLibMethodInstance(PLibMethodAttribute attribute, MethodInfo method) {
|
||||
Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute));
|
||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the method, passing the required parameters if any.
|
||||
/// </summary>
|
||||
/// <param name="instance">The Harmony instance to use if the method wants to
|
||||
/// perform a patch.</param>
|
||||
public void Run(Harmony instance) {
|
||||
if (PPatchManager.CheckConditions(Descriptor.RequireAssembly, Descriptor.
|
||||
RequireType, out Type requiredType)) {
|
||||
// Only runs once, no meaningful savings with a delegate
|
||||
var paramTypes = Method.GetParameterTypes();
|
||||
int len = paramTypes.Length;
|
||||
if (len <= 0)
|
||||
// No parameters, static method only
|
||||
Method.Invoke(null, null);
|
||||
else if (paramTypes[0] == typeof(Harmony)) {
|
||||
if (len == 1)
|
||||
// Harmony instance parameter
|
||||
Method.Invoke(null, new object[] { instance });
|
||||
else if (len == 2 && paramTypes[1] == typeof(Type))
|
||||
// Type parameter
|
||||
Method.Invoke(null, new object[] { instance, requiredType });
|
||||
} else
|
||||
PUtil.LogWarning("Invalid signature for PLibMethod - must have (), " +
|
||||
"(HarmonyInstance), or (HarmonyInstance, Type)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
312
mod/PLibCore/PatchManager/PLibPatchAttribute.cs
Normal file
312
mod/PLibCore/PatchManager/PLibPatchAttribute.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* 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 PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace PeterHan.PLib.PatchManager {
|
||||
/// <summary>
|
||||
/// Represents a method that will be patched by PLib at a specific time to allow
|
||||
/// conditional integration with other mods.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class PLibPatchAttribute : Attribute, IPLibAnnotation {
|
||||
/// <summary>
|
||||
/// The required argument types. If null, any matching method name is patched, or an
|
||||
/// exception thrown if more than one matches.
|
||||
/// </summary>
|
||||
public Type[] ArgumentTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this flag is set, the patch will emit only at DEBUG level if the target method
|
||||
/// is not found or matches ambiguously.
|
||||
/// </summary>
|
||||
public bool IgnoreOnFail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the method to patch.
|
||||
/// </summary>
|
||||
public string MethodName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of patch to apply through Harmony.
|
||||
/// </summary>
|
||||
public HarmonyPatchType PatchType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Requires the specified assembly to be loaded for this method to run. If RequireType
|
||||
/// is null or empty, no particular types need to be defined in the assembly. The
|
||||
/// assembly name is required, but the version is optional (strong named assemblies
|
||||
/// can never load in ONI, since neither Unity nor Klei types are strong named...)
|
||||
/// </summary>
|
||||
public string RequireAssembly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Requires the specified type full name (not assembly qualified name) to exist for
|
||||
/// this method to run. If RequireAssembly is null or empty, a type in any assembly
|
||||
/// will satisfy the requirement.
|
||||
/// </summary>
|
||||
public string RequireType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this method is run.
|
||||
/// </summary>
|
||||
public uint Runtime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type to patch. If null, the patcher will try to use the required type from the
|
||||
/// RequireType parameter.
|
||||
/// </summary>
|
||||
public Type TargetType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Patches a concrete type and method.
|
||||
///
|
||||
/// Passing null as the method name will attempt to patch a constructor. Only one
|
||||
/// declared constructor may be present, or the call will fail at patch time.
|
||||
/// </summary>
|
||||
/// <param name="runtime">When to apply the patch.</param>
|
||||
/// <param name="target">The type to patch.</param>
|
||||
/// <param name="method">The method name to patch.</param>
|
||||
public PLibPatchAttribute(uint runtime, Type target, string method) {
|
||||
ArgumentTypes = null;
|
||||
IgnoreOnFail = false;
|
||||
MethodName = method;
|
||||
PatchType = HarmonyPatchType.All;
|
||||
Runtime = runtime;
|
||||
TargetType = target ?? throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches a concrete type and overloaded method.
|
||||
///
|
||||
/// Passing null as the method name will attempt to patch a constructor.
|
||||
/// </summary>
|
||||
/// <param name="runtime">When to apply the patch.</param>
|
||||
/// <param name="target">The type to patch.</param>
|
||||
/// <param name="method">The method name to patch.</param>
|
||||
/// <param name="argTypes">The types of the overload to patch.</param>
|
||||
public PLibPatchAttribute(uint runtime, Type target, string method,
|
||||
params Type[] argTypes) {
|
||||
ArgumentTypes = argTypes;
|
||||
IgnoreOnFail = false;
|
||||
MethodName = method;
|
||||
PatchType = HarmonyPatchType.All;
|
||||
Runtime = runtime;
|
||||
TargetType = target ?? throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches a method only if a specified type is available. Use optional parameters to
|
||||
/// specify the type to patch using RequireType / RequireAssembly.
|
||||
///
|
||||
/// Passing null as the method name will attempt to patch a constructor. Only one
|
||||
/// declared constructor may be present, or the call will fail at patch time.
|
||||
/// </summary>
|
||||
/// <param name="runtime">When to apply the patch.</param>
|
||||
/// <param name="method">The method name to patch.</param>
|
||||
public PLibPatchAttribute(uint runtime, string method) {
|
||||
ArgumentTypes = null;
|
||||
IgnoreOnFail = false;
|
||||
MethodName = method;
|
||||
PatchType = HarmonyPatchType.All;
|
||||
Runtime = runtime;
|
||||
TargetType = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches an overloaded method only if a specified type is available. Use optional
|
||||
/// parameters to specify the type to patch using RequireType / RequireAssembly.
|
||||
///
|
||||
/// Passing null as the method name will attempt to patch a constructor.
|
||||
/// </summary>
|
||||
/// <param name="runtime">When to apply the patch.</param>
|
||||
/// <param name="method">The method name to patch.</param>
|
||||
/// <param name="argTypes">The types of the overload to patch.</param>
|
||||
public PLibPatchAttribute(uint runtime, string method, params Type[] argTypes) {
|
||||
ArgumentTypes = argTypes;
|
||||
IgnoreOnFail = false;
|
||||
MethodName = method;
|
||||
PatchType = HarmonyPatchType.All;
|
||||
Runtime = runtime;
|
||||
TargetType = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new patch method instance.
|
||||
/// </summary>
|
||||
/// <param name="method">The method that was attributed.</param>
|
||||
/// <returns>An instance that can execute this patch.</returns>
|
||||
public IPatchMethodInstance CreateInstance(MethodInfo method) {
|
||||
return new PLibPatchInstance(this, method);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "PLibPatch[RunAt={0},PatchType={1},MethodName={2}]".F(RunAt.ToString(
|
||||
Runtime), PatchType, MethodName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refers to a single instance of the annotation, with its annotated method.
|
||||
/// </summary>
|
||||
internal sealed class PLibPatchInstance : IPatchMethodInstance {
|
||||
/// <summary>
|
||||
/// The attribute describing the method.
|
||||
/// </summary>
|
||||
public PLibPatchAttribute Descriptor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The method to run.
|
||||
/// </summary>
|
||||
public MethodInfo Method { get; }
|
||||
|
||||
public PLibPatchInstance(PLibPatchAttribute attribute, MethodInfo method) {
|
||||
Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute));
|
||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the patch type to perform.
|
||||
/// </summary>
|
||||
/// <returns>The type of Harmony patch to use for this method.</returns>
|
||||
private HarmonyPatchType GetPatchType() {
|
||||
var patchType = Descriptor.PatchType;
|
||||
if (patchType == HarmonyPatchType.All) {
|
||||
// Auto-determine the patch type based on name, if possible
|
||||
string patchName = Method.Name;
|
||||
foreach (var value in Enum.GetValues(typeof(HarmonyPatchType)))
|
||||
if (value is HarmonyPatchType eval && eval != patchType && patchName.
|
||||
EndsWith(eval.ToString(), StringComparison.Ordinal)) {
|
||||
patchType = eval;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return patchType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified instance constructor.
|
||||
/// </summary>
|
||||
/// <param name="targetType">The type to be constructed.</param>
|
||||
/// <returns>The target constructor.</returns>
|
||||
/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
|
||||
/// and multiple declared constructors exist.</exception>
|
||||
private MethodBase GetTargetConstructor(Type targetType, Type[] argumentTypes) {
|
||||
MethodBase constructor;
|
||||
if (argumentTypes == null) {
|
||||
var cons = targetType.GetConstructors(PPatchManager.FLAGS | BindingFlags.
|
||||
Instance);
|
||||
if (cons == null || cons.Length != 1)
|
||||
throw new InvalidOperationException("No constructor for {0} found".F(
|
||||
targetType.FullName));
|
||||
constructor = cons[0];
|
||||
} else
|
||||
constructor = targetType.GetConstructor(PPatchManager.FLAGS | BindingFlags.
|
||||
Instance, null, argumentTypes, null);
|
||||
return constructor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the target method to patch.
|
||||
/// </summary>
|
||||
/// <param name="requiredType">The type to use if no type was specified.</param>
|
||||
/// <returns>The method to patch.</returns>
|
||||
/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
|
||||
/// and multiple options match the method name.</exception>
|
||||
/// <exception cref="InvalidOperationException">If the target method was not found.</exception>
|
||||
private MethodBase GetTargetMethod(Type requiredType) {
|
||||
var targetType = Descriptor.TargetType;
|
||||
var argumentTypes = Descriptor.ArgumentTypes;
|
||||
string name = Descriptor.MethodName;
|
||||
MethodBase method;
|
||||
if (targetType == null)
|
||||
targetType = requiredType;
|
||||
// Only allow non-inherited members, patching inherited members gets spooky on
|
||||
// Mac OS and Linux
|
||||
if (targetType == null)
|
||||
throw new InvalidOperationException("No type specified to patch");
|
||||
if (string.IsNullOrEmpty(name) || name == ".ctor")
|
||||
// Constructor
|
||||
method = GetTargetConstructor(targetType, argumentTypes);
|
||||
else
|
||||
// Method
|
||||
method = (argumentTypes == null) ? targetType.GetMethod(name, PPatchManager.
|
||||
FLAGS_EITHER) : targetType.GetMethod(name, PPatchManager.FLAGS_EITHER,
|
||||
null, argumentTypes, null);
|
||||
if (method == null)
|
||||
throw new InvalidOperationException("Method {0}.{1} not found".F(targetType.
|
||||
FullName, name));
|
||||
return method;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a message at debug level if Ignore On Patch Fail is enabled.
|
||||
/// </summary>
|
||||
/// <param name="e">The exception thrown during patching.</param>
|
||||
/// <returns>true to suppress the exception, or false to rethrow it.</returns>
|
||||
private bool LogIgnoreOnFail(Exception e) {
|
||||
bool ignore = Descriptor.IgnoreOnFail;
|
||||
if (ignore)
|
||||
PUtil.LogDebug("Patch for {0} not applied: {1}".F(Descriptor.
|
||||
MethodName, e.Message));
|
||||
return ignore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the patch.
|
||||
/// </summary>
|
||||
/// <param name="instance">The Harmony instance to use.</param>
|
||||
/// <exception cref="InvalidOperationException">If the </exception>
|
||||
/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
|
||||
/// and multiple options match the method name.</exception>
|
||||
public void Run(Harmony instance) {
|
||||
if (PPatchManager.CheckConditions(Descriptor.RequireAssembly, Descriptor.
|
||||
RequireType, out Type requiredType)) {
|
||||
var dest = new HarmonyMethod(Method);
|
||||
if (instance == null)
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
try {
|
||||
var method = GetTargetMethod(requiredType);
|
||||
switch (GetPatchType()) {
|
||||
case HarmonyPatchType.Postfix:
|
||||
instance.Patch(method, postfix: dest);
|
||||
break;
|
||||
case HarmonyPatchType.Prefix:
|
||||
instance.Patch(method, prefix: dest);
|
||||
break;
|
||||
case HarmonyPatchType.Transpiler:
|
||||
instance.Patch(method, transpiler: dest);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(HarmonyPatchType));
|
||||
}
|
||||
} catch (AmbiguousMatchException e) {
|
||||
// Multi catch or filtering is not available in this version of C#
|
||||
if (!LogIgnoreOnFail(e))
|
||||
throw;
|
||||
} catch (InvalidOperationException e) {
|
||||
if (!LogIgnoreOnFail(e))
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
268
mod/PLibCore/PatchManager/PPatchManager.cs
Normal file
268
mod/PLibCore/PatchManager/PPatchManager.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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 PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
using PrivateRunList = System.Collections.Generic.ICollection<PeterHan.PLib.PatchManager.
|
||||
IPatchMethodInstance>;
|
||||
|
||||
namespace PeterHan.PLib.PatchManager {
|
||||
/// <summary>
|
||||
/// Manages patches that PLib will conditionally apply.
|
||||
/// </summary>
|
||||
public sealed class PPatchManager : PForwardedComponent {
|
||||
/// <summary>
|
||||
/// The base flags to use when matching instance or static methods.
|
||||
/// </summary>
|
||||
internal const BindingFlags FLAGS = PPatchTools.BASE_FLAGS | BindingFlags.DeclaredOnly;
|
||||
|
||||
/// <summary>
|
||||
/// The flags to use when matching instance and static methods.
|
||||
/// </summary>
|
||||
internal const BindingFlags FLAGS_EITHER = FLAGS | BindingFlags.Static | BindingFlags.
|
||||
Instance;
|
||||
|
||||
/// <summary>
|
||||
/// The version of this component. Uses the running PLib version.
|
||||
/// </summary>
|
||||
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||
|
||||
/// <summary>
|
||||
/// The instantiated copy of this class.
|
||||
/// </summary>
|
||||
internal static PPatchManager Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// true if the AfterModsLoad patches have been run, or false otherwise.
|
||||
/// </summary>
|
||||
private static volatile bool afterModsLoaded = false;
|
||||
|
||||
private static void Game_DestroyInstances_Postfix() {
|
||||
Instance?.InvokeAllProcess(RunAt.OnEndGame, null);
|
||||
}
|
||||
|
||||
private static void Game_OnPrefabInit_Postfix() {
|
||||
Instance?.InvokeAllProcess(RunAt.OnStartGame, null);
|
||||
}
|
||||
|
||||
private static void Initialize_Prefix() {
|
||||
Instance?.InvokeAllProcess(RunAt.BeforeDbInit, null);
|
||||
}
|
||||
|
||||
private static void Initialize_Postfix() {
|
||||
Instance?.InvokeAllProcess(RunAt.AfterDbInit, null);
|
||||
}
|
||||
|
||||
private static void Instance_Postfix() {
|
||||
bool load = false;
|
||||
if (Instance != null)
|
||||
lock (VERSION) {
|
||||
if (!afterModsLoaded)
|
||||
load = afterModsLoaded = true;
|
||||
}
|
||||
if (load)
|
||||
Instance.InvokeAllProcess(RunAt.AfterLayerableLoad, null);
|
||||
}
|
||||
|
||||
private static void MainMenu_OnSpawn_Postfix() {
|
||||
Instance?.InvokeAllProcess(RunAt.InMainMenu, null);
|
||||
}
|
||||
|
||||
public override Version Version => VERSION;
|
||||
|
||||
/// <summary>
|
||||
/// The Harmony instance to use for patching.
|
||||
/// </summary>
|
||||
private readonly Harmony harmony;
|
||||
|
||||
/// <summary>
|
||||
/// Patches and delegates to be run at specific points in the runtime. Put the kibosh
|
||||
/// on patching Db.Initialize()!
|
||||
/// </summary>
|
||||
private readonly IDictionary<uint, PrivateRunList> patches;
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the conditions for a method running are met.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">The assembly name that must be present, or null if none is required.</param>
|
||||
/// <param name="typeName">The type full name that must be present, or null if none is required.</param>
|
||||
/// <param name="requiredType">The type that was required, if typeName was not null or empty.</param>
|
||||
/// <returns>true if the requirements are met, or false otherwise.</returns>
|
||||
internal static bool CheckConditions(string assemblyName, string typeName,
|
||||
out Type requiredType) {
|
||||
bool ok = false, emptyType = string.IsNullOrEmpty(typeName);
|
||||
if (string.IsNullOrEmpty(assemblyName)) {
|
||||
if (emptyType) {
|
||||
requiredType = null;
|
||||
ok = true;
|
||||
} else {
|
||||
requiredType = PPatchTools.GetTypeSafe(typeName);
|
||||
ok = requiredType != null;
|
||||
}
|
||||
} else if (emptyType) {
|
||||
requiredType = null;
|
||||
// Search for assembly only, by name
|
||||
foreach (var candidate in AppDomain.CurrentDomain.GetAssemblies())
|
||||
if (candidate.GetName().Name == assemblyName) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
requiredType = PPatchTools.GetTypeSafe(typeName, assemblyName);
|
||||
ok = requiredType != null;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a patch manager to execute patches at specific times.
|
||||
///
|
||||
/// Create this instance in OnLoad() and use RegisterPatchClass to register a
|
||||
/// patch class.
|
||||
/// </summary>
|
||||
/// <param name="harmony">The Harmony instance to use for patching.</param>
|
||||
public PPatchManager(Harmony harmony) {
|
||||
if (harmony == null) {
|
||||
PUtil.LogWarning("Use the Harmony instance from OnLoad to create PPatchManager");
|
||||
harmony = new Harmony("PLib.PostLoad." + Assembly.GetExecutingAssembly().
|
||||
GetNameSafe());
|
||||
}
|
||||
this.harmony = harmony;
|
||||
patches = new Dictionary<uint, PrivateRunList>(8);
|
||||
InstanceData = patches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules a patch method instance to be run.
|
||||
/// </summary>
|
||||
/// <param name="when">When to run the patch.</param>
|
||||
/// <param name="instance">The patch method instance to run.</param>
|
||||
/// <param name="harmony">The Harmony instance to use for patching.</param>
|
||||
private void AddHandler(uint when, IPatchMethodInstance instance) {
|
||||
if (!patches.TryGetValue(when, out PrivateRunList atTime))
|
||||
patches.Add(when, atTime = new List<IPatchMethodInstance>(16));
|
||||
atTime.Add(instance);
|
||||
}
|
||||
|
||||
public override void Initialize(Harmony plibInstance) {
|
||||
Instance = this;
|
||||
|
||||
// Db
|
||||
plibInstance.Patch(typeof(Db), nameof(Db.Initialize), prefix: PatchMethod(nameof(
|
||||
Initialize_Prefix)), postfix: PatchMethod(nameof(Initialize_Postfix)));
|
||||
|
||||
// Game
|
||||
plibInstance.Patch(typeof(Game), "DestroyInstances", postfix: PatchMethod(nameof(
|
||||
Game_DestroyInstances_Postfix)));
|
||||
plibInstance.Patch(typeof(Game), "OnPrefabInit", postfix: PatchMethod(nameof(
|
||||
Game_OnPrefabInit_Postfix)));
|
||||
|
||||
// GlobalResources
|
||||
plibInstance.Patch(typeof(GlobalResources), "Instance", postfix:
|
||||
PatchMethod(nameof(Instance_Postfix)));
|
||||
|
||||
// MainMenu
|
||||
plibInstance.Patch(typeof(MainMenu), "OnSpawn", postfix: PatchMethod(
|
||||
nameof(MainMenu_OnSpawn_Postfix)));
|
||||
}
|
||||
|
||||
public override void PostInitialize(Harmony plibInstance) {
|
||||
InvokeAllProcess(RunAt.AfterModsLoad, null);
|
||||
}
|
||||
|
||||
public override void Process(uint when, object _) {
|
||||
if (patches.TryGetValue(when, out PrivateRunList atTime) && atTime != null &&
|
||||
atTime.Count > 0) {
|
||||
string stage = RunAt.ToString(when);
|
||||
#if DEBUG
|
||||
PRegistry.LogPatchDebug("Executing {0:D} handler(s) from {1} for stage {2}".F(
|
||||
atTime.Count, Assembly.GetExecutingAssembly().GetNameSafe() ?? "?", stage));
|
||||
#endif
|
||||
foreach (var patch in atTime)
|
||||
try {
|
||||
patch.Run(harmony);
|
||||
} catch (TargetInvocationException e) {
|
||||
// Use the inner exception
|
||||
PUtil.LogError("Error running patches for stage " + stage + ":");
|
||||
PUtil.LogException(e.GetBaseException());
|
||||
} catch (Exception e) {
|
||||
// Say which mod's postload crashed
|
||||
PUtil.LogError("Error running patches for stage " + stage + ":");
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a single patch to be run by Patch Manager. Obviously, the patch must be
|
||||
/// registered before the time that it is used.
|
||||
/// </summary>
|
||||
/// <param name="when">The time when the method should be run.</param>
|
||||
/// <param name="patch">The patch to execute.</param>
|
||||
public void RegisterPatch(uint when, IPatchMethodInstance patch) {
|
||||
RegisterForForwarding();
|
||||
if (patch == null)
|
||||
throw new ArgumentNullException(nameof(patch));
|
||||
if (when == RunAt.Immediately)
|
||||
// Now now now!
|
||||
patch.Run(harmony);
|
||||
else
|
||||
AddHandler(when, patch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a class containing methods for [PLibPatch] and [PLibMethod] handlers.
|
||||
/// All methods, public and private, of the type will be searched for annotations.
|
||||
/// However, nested and derived types will not be searched, nor will inherited methods.
|
||||
///
|
||||
/// This method cannot be used to register a class from another mod, as the annotations
|
||||
/// on those methods would have a different assembly qualified name and would thus
|
||||
/// not be recognized.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to register.</param>
|
||||
/// <param name="harmony">The Harmony instance to use for immediate patches. Use
|
||||
/// the instance provided from UserMod2.OnLoad().</param>
|
||||
public void RegisterPatchClass(Type type) {
|
||||
int count = 0;
|
||||
if (type == null)
|
||||
throw new ArgumentNullException(nameof(type));
|
||||
RegisterForForwarding();
|
||||
foreach (var method in type.GetMethods(FLAGS | BindingFlags.Static))
|
||||
foreach (var attrib in method.GetCustomAttributes(true))
|
||||
if (attrib is IPLibAnnotation pm) {
|
||||
var when = pm.Runtime;
|
||||
var instance = pm.CreateInstance(method);
|
||||
if (when == RunAt.Immediately)
|
||||
// Now now now!
|
||||
instance.Run(harmony);
|
||||
else
|
||||
AddHandler(pm.Runtime, instance);
|
||||
count++;
|
||||
}
|
||||
if (count > 0)
|
||||
PRegistry.LogPatchDebug("Registered {0:D} handler(s) for {1}".F(count,
|
||||
Assembly.GetCallingAssembly().GetNameSafe() ?? "?"));
|
||||
else
|
||||
PRegistry.LogPatchWarning("RegisterPatchClass could not find any handlers!");
|
||||
}
|
||||
}
|
||||
}
|
||||
93
mod/PLibCore/PatchManager/RunAt.cs
Normal file
93
mod/PLibCore/PatchManager/RunAt.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.PatchManager {
|
||||
/// <summary>
|
||||
/// Describes when a PLibPatch or PLibMethod should be invoked.
|
||||
///
|
||||
/// Due to a bug in ILRepack an enum type in PLib cannot be used as a parameter for a
|
||||
/// custom attribute. ILmerge does not have this bug.
|
||||
/// </summary>
|
||||
public static class RunAt {
|
||||
/// <summary>
|
||||
/// Runs the method/patch now.
|
||||
///
|
||||
/// Note that mods may load in any order and thus not all mods may be initialized at
|
||||
/// this time.
|
||||
/// </summary>
|
||||
public const uint Immediately = 0U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs after all mods load, but before most other aspects of the game (including
|
||||
/// Assets, Db, and so forth) are initialized. This will run before any other mod
|
||||
/// has their UserMod2.AfterModsLoad executed. All PLib components will be initialized
|
||||
/// by this point.
|
||||
/// </summary>
|
||||
public const uint AfterModsLoad = 1U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs immediately before Db.Initialize.
|
||||
/// </summary>
|
||||
public const uint BeforeDbInit = 2U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs immediately after Db.Initialize.
|
||||
/// </summary>
|
||||
public const uint AfterDbInit = 3U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs when the main menu has loaded.
|
||||
/// </summary>
|
||||
public const uint InMainMenu = 4U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs when Game.OnPrefabInit has completed.
|
||||
/// </summary>
|
||||
public const uint OnStartGame = 5U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs when Game.DestroyInstances is executed.
|
||||
/// </summary>
|
||||
public const uint OnEndGame = 6U;
|
||||
|
||||
/// <summary>
|
||||
/// Runs after all mod data (including layerable files like world gen and codex/
|
||||
/// elements) are loaded. This comes after all UserMod2.AfterModsLoad handlers execute.
|
||||
/// All PLib components will be initialized by this point.
|
||||
/// </summary>
|
||||
public const uint AfterLayerableLoad = 7U;
|
||||
|
||||
/// <summary>
|
||||
/// The string equivalents of each constant for debugging.
|
||||
/// </summary>
|
||||
private static readonly string[] STRING_VALUES = new string[] {
|
||||
nameof(Immediately), nameof(AfterModsLoad), nameof(BeforeDbInit),
|
||||
nameof(AfterDbInit), nameof(InMainMenu), nameof(OnStartGame), nameof(OnEndGame),
|
||||
nameof(AfterLayerableLoad)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human readable representation of a run time constant.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The time when the patch should be run.</param>
|
||||
public static string ToString(uint runtime) {
|
||||
return (runtime < STRING_VALUES.Length) ? STRING_VALUES[runtime] : runtime.
|
||||
ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
271
mod/PLibCore/PriorityQueue.cs
Normal file
271
mod/PLibCore/PriorityQueue.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* 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 System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// A class similar to Queue<typeparamref name="T"/> that allows efficient access to its
|
||||
/// items in ascending order.
|
||||
/// </summary>
|
||||
public class PriorityQueue<T> where T : IComparable<T> {
|
||||
/// <summary>
|
||||
/// Returns the index of the specified item's first child. Its second child index is
|
||||
/// that index plus one.
|
||||
/// </summary>
|
||||
/// <param name="index">The item index.</param>
|
||||
/// <returns>The index of its first child.</returns>
|
||||
private static int ChildIndex(int index) {
|
||||
return 2 * index + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the specified item's parent.
|
||||
/// </summary>
|
||||
/// <param name="index">The item index.</param>
|
||||
/// <returns>The index of its parent.</returns>
|
||||
private static int ParentIndex(int index) {
|
||||
return (index - 1) / 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of elements in this queue.
|
||||
/// </summary>
|
||||
public int Count => heap.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The heap where the items are stored.
|
||||
/// </summary>
|
||||
private readonly IList<T> heap;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PriorityQueue<<typeparamref name="T"/>> with the default
|
||||
/// initial capacity.
|
||||
/// </summary>
|
||||
public PriorityQueue() : this(32) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PriorityQueue<<typeparamref name="T"/>> with the specified
|
||||
/// initial capacity.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial capacity of this queue.</param>
|
||||
public PriorityQueue(int capacity) {
|
||||
if (capacity < 1)
|
||||
throw new ArgumentException("capacity > 0");
|
||||
heap = new List<T>(Math.Max(capacity, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all objects from this PriorityQueue<<typeparamref name="T"/>>.
|
||||
/// </summary>
|
||||
public void Clear() {
|
||||
heap.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the specified key is present in this priority queue. This operation
|
||||
/// is fairly slow, use with caution.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to check.</param>
|
||||
/// <returns>true if it exists in this priority queue, or false otherwise.</returns>
|
||||
public bool Contains(T key) {
|
||||
return heap.Contains(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the smallest object in the
|
||||
/// PriorityQueue<<typeparamref name="T"/>>.
|
||||
///
|
||||
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||
/// </summary>
|
||||
/// <returns>The object that is removed from this PriorityQueue.</returns>
|
||||
/// <exception cref="InvalidOperationException">If this queue is empty.</exception>
|
||||
public T Dequeue() {
|
||||
int index = 0, length = heap.Count, childIndex;
|
||||
if (length == 0)
|
||||
throw new InvalidOperationException("Queue is empty");
|
||||
T top = heap[0];
|
||||
// Put the last element as the new head
|
||||
heap[0] = heap[--length];
|
||||
heap.RemoveAt(length);
|
||||
while ((childIndex = ChildIndex(index)) < length) {
|
||||
T first = heap[index], child = heap[childIndex];
|
||||
if (childIndex < length - 1) {
|
||||
var rightChild = heap[childIndex + 1];
|
||||
// Select the smallest child
|
||||
if (child.CompareTo(rightChild) > 0) {
|
||||
childIndex++;
|
||||
child = rightChild;
|
||||
}
|
||||
}
|
||||
if (first.CompareTo(child) < 0)
|
||||
break;
|
||||
heap[childIndex] = first;
|
||||
heap[index] = child;
|
||||
index = childIndex;
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an object to the PriorityQueue<<typeparamref name="T"/>>.
|
||||
/// </summary>
|
||||
/// <param name="item">The object to add to this PriorityQueue.</param>
|
||||
/// <exception cref="ArgumentNullException">If item is null.</exception>
|
||||
public void Enqueue(T item) {
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
int index = heap.Count;
|
||||
heap.Add(item);
|
||||
while (index > 0) {
|
||||
int parentIndex = ParentIndex(index);
|
||||
T first = heap[index], parent = heap[parentIndex];
|
||||
if (first.CompareTo(parent) > 0)
|
||||
break;
|
||||
heap[parentIndex] = first;
|
||||
heap[index] = parent;
|
||||
index = parentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the smallest object in the PriorityQueue<<typeparamref name="T"/>>
|
||||
/// without removing it.
|
||||
///
|
||||
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||
/// </summary>
|
||||
/// <returns>The smallest object in this PriorityQueue.</returns>
|
||||
/// <exception cref="InvalidOperationException">If this queue is empty.</exception>
|
||||
public T Peek() {
|
||||
if (Count == 0)
|
||||
throw new InvalidOperationException("Queue is empty");
|
||||
return heap[0];
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return heap.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A priority queue that includes a paired value.
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type to use for the sorting in the PriorityQueue.</typeparam>
|
||||
/// <typeparam name="V">The type to include as extra data.</typeparam>
|
||||
public sealed class PriorityDictionary<K, V> : PriorityQueue<PriorityDictionary<K, V>.
|
||||
PriorityQueuePair> where K : IComparable<K> {
|
||||
/// <summary>
|
||||
/// Creates a new PriorityDictionary<<typeparamref name="K"/>,
|
||||
/// <typeparamref name="V"/>> with the default initial capacity.
|
||||
/// </summary>
|
||||
public PriorityDictionary() : base() { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PriorityDictionary<<typeparamref name="K"/>,
|
||||
/// <typeparamref name="V"/>> with the specified initial capacity.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial capacity of this dictionary.</param>
|
||||
public PriorityDictionary(int capacity) : base(capacity) { }
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the smallest object in the
|
||||
/// PriorityDictionary<<typeparamref name="K"/>, <typeparamref name="V"/>>.
|
||||
///
|
||||
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the object removed.</param>
|
||||
/// <param name="value">The value of the object removed.</param>
|
||||
/// <exception cref="InvalidOperationException">If this dictionary is empty.</exception>
|
||||
public void Dequeue(out K key, out V value) {
|
||||
var pair = Dequeue();
|
||||
key = pair.Key;
|
||||
value = pair.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an object to the PriorityDictionary<<typeparamref name="K"/>,
|
||||
/// <typeparamref name="V"/>>.
|
||||
/// </summary>
|
||||
/// <param name="item">The object to add to this PriorityDictionary.</param>
|
||||
/// <exception cref="ArgumentNullException">If item is null.</exception>
|
||||
public void Enqueue(K key, V value) {
|
||||
Enqueue(new PriorityQueuePair(key, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the smallest object in the PriorityDictionary<<typeparamref name="K"/>,
|
||||
/// <typeparamref name="V"/>> without removing it.
|
||||
///
|
||||
/// If multiple objects are the smallest object, an unspecified one is returned.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the smallest object.</param>
|
||||
/// <param name="value">The value of the smallest object.</param>
|
||||
/// <exception cref="InvalidOperationException">If this dictionary is empty.</exception>
|
||||
public void Peek(out K key, out V value) {
|
||||
var pair = Peek();
|
||||
key = pair.Key;
|
||||
value = pair.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a value with the key that is used for comparison.
|
||||
/// </summary>
|
||||
public sealed class PriorityQueuePair : IComparable<PriorityQueuePair> {
|
||||
/// <summary>
|
||||
/// Retrieves the key of this QueueItem.
|
||||
/// </summary>
|
||||
public K Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of this QueueItem.
|
||||
/// </summary>
|
||||
public V Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new priority queue pair.
|
||||
/// </summary>
|
||||
/// <param name="key">The item key.</param>
|
||||
/// <param name="value">The item value.</param>
|
||||
public PriorityQueuePair(K key, V value) {
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public int CompareTo(PriorityQueuePair other) {
|
||||
if (other == null)
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
return Key.CompareTo(other.Key);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
return obj is PriorityQueuePair other && other.Key.Equals(Key);
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return Key.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "PriorityQueueItem[key=" + Key + ",value=" + Value + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
mod/PLibCore/Remote/PRemoteComponent.cs
Normal file
146
mod/PLibCore/Remote/PRemoteComponent.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.Reflection;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Delegates calls to forwarded components in other assemblies.
|
||||
/// </summary>
|
||||
internal sealed class PRemoteComponent : PForwardedComponent {
|
||||
/// <summary>
|
||||
/// The prototype used for delegates to remote Initialize.
|
||||
/// </summary>
|
||||
private delegate void InitializeDelegate(Harmony instance);
|
||||
|
||||
/// <summary>
|
||||
/// The prototype used for delegates to remote Process.
|
||||
/// </summary>
|
||||
private delegate void ProcessDelegate(uint operation, object args);
|
||||
|
||||
protected override object InstanceData {
|
||||
get {
|
||||
return getData?.Invoke();
|
||||
}
|
||||
set {
|
||||
setData?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
public override Version Version {
|
||||
get {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points to the component's version of Bootstrap.
|
||||
/// </summary>
|
||||
private readonly InitializeDelegate doBootstrap;
|
||||
|
||||
/// <summary>
|
||||
/// Points to the component's version of Initialize.
|
||||
/// </summary>
|
||||
private readonly InitializeDelegate doInitialize;
|
||||
|
||||
/// <summary>
|
||||
/// Points to the component's version of PostInitialize.
|
||||
/// </summary>
|
||||
private readonly InitializeDelegate doPostInitialize;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the component's data.
|
||||
/// </summary>
|
||||
private readonly Func<object> getData;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the processing method of the component.
|
||||
/// </summary>
|
||||
private readonly ProcessDelegate process;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the component's data.
|
||||
/// </summary>
|
||||
private readonly Action<object> setData;
|
||||
|
||||
/// <summary>
|
||||
/// The component's version.
|
||||
/// </summary>
|
||||
private readonly Version version;
|
||||
|
||||
/// <summary>
|
||||
/// The wrapped instance from the other mod.
|
||||
/// </summary>
|
||||
private readonly object wrapped;
|
||||
|
||||
internal PRemoteComponent(object wrapped) {
|
||||
this.wrapped = wrapped ?? throw new ArgumentNullException(nameof(wrapped));
|
||||
if (!PPatchTools.TryGetPropertyValue(wrapped, nameof(Version), out Version
|
||||
version))
|
||||
throw new ArgumentException("Remote component missing Version property");
|
||||
this.version = version;
|
||||
// Initialize
|
||||
var type = wrapped.GetType();
|
||||
doInitialize = type.CreateDelegate<InitializeDelegate>(nameof(PForwardedComponent.
|
||||
Initialize), wrapped, typeof(Harmony));
|
||||
if (doInitialize == null)
|
||||
throw new ArgumentException("Remote component missing Initialize");
|
||||
// Bootstrap
|
||||
doBootstrap = type.CreateDelegate<InitializeDelegate>(nameof(PForwardedComponent.
|
||||
Bootstrap), wrapped, typeof(Harmony));
|
||||
doPostInitialize = type.CreateDelegate<InitializeDelegate>(nameof(
|
||||
PForwardedComponent.PostInitialize), wrapped, typeof(Harmony));
|
||||
getData = type.CreateGetDelegate<object>(nameof(InstanceData), wrapped);
|
||||
setData = type.CreateSetDelegate<object>(nameof(InstanceData), wrapped);
|
||||
process = type.CreateDelegate<ProcessDelegate>(nameof(PForwardedComponent.
|
||||
Process), wrapped, typeof(uint), typeof(object));
|
||||
}
|
||||
|
||||
public override void Bootstrap(Harmony plibInstance) {
|
||||
doBootstrap?.Invoke(plibInstance);
|
||||
}
|
||||
|
||||
internal override object DoInitialize(Harmony plibInstance) {
|
||||
doInitialize.Invoke(plibInstance);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
public override Assembly GetOwningAssembly() {
|
||||
return wrapped.GetType().Assembly;
|
||||
}
|
||||
|
||||
public override void Initialize(Harmony plibInstance) {
|
||||
DoInitialize(plibInstance);
|
||||
}
|
||||
|
||||
public override void PostInitialize(Harmony plibInstance) {
|
||||
doPostInitialize?.Invoke(plibInstance);
|
||||
}
|
||||
|
||||
public override void Process(uint operation, object args) {
|
||||
process?.Invoke(operation, args);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "PRemoteComponent[ID={0},TargetType={1}]".F(ID, wrapped.GetType().
|
||||
AssemblyQualifiedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
150
mod/PLibCore/Remote/PRemoteRegistry.cs
Normal file
150
mod/PLibCore/Remote/PRemoteRegistry.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Core {
|
||||
/// <summary>
|
||||
/// Transparently provides the functionality of PRegistry, while the actual instance is
|
||||
/// from another mod's bootstrapper.
|
||||
/// </summary>
|
||||
internal sealed class PRemoteRegistry : IPLibRegistry {
|
||||
/// <summary>
|
||||
/// The prototype used for delegates to remote GetAllComponents.
|
||||
/// </summary>
|
||||
private delegate System.Collections.ICollection GetAllComponentsDelegate(string id);
|
||||
|
||||
/// <summary>
|
||||
/// The prototype used for delegates to remote GetLatestVersion and GetSharedData.
|
||||
/// </summary>
|
||||
private delegate object GetObjectDelegate(string id);
|
||||
|
||||
/// <summary>
|
||||
/// The prototype used for delegates to remote SetSharedData.
|
||||
/// </summary>
|
||||
private delegate void SetObjectDelegate(string id, object value);
|
||||
|
||||
/// <summary>
|
||||
/// Points to the local registry's version of AddCandidateVersion.
|
||||
/// </summary>
|
||||
private readonly Action<object> addCandidateVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Points to the local registry's version of GetAllComponents.
|
||||
/// </summary>
|
||||
private readonly GetAllComponentsDelegate getAllComponents;
|
||||
|
||||
/// <summary>
|
||||
/// Points to the local registry's version of GetLatestVersion.
|
||||
/// </summary>
|
||||
private readonly GetObjectDelegate getLatestVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Points to the local registry's version of GetSharedData.
|
||||
/// </summary>
|
||||
private readonly GetObjectDelegate getSharedData;
|
||||
|
||||
/// <summary>
|
||||
/// Points to the local registry's version of SetSharedData.
|
||||
/// </summary>
|
||||
private readonly SetObjectDelegate setSharedData;
|
||||
|
||||
public IDictionary<string, object> ModData { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The components actually instantiated (latest version of each).
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, PForwardedComponent> remoteComponents;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a remote registry wrapping the target object.
|
||||
/// </summary>
|
||||
/// <param name="instance">The PRegistryComponent instance to wrap.</param>
|
||||
internal PRemoteRegistry(object instance) {
|
||||
if (instance == null)
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
remoteComponents = new Dictionary<string, PForwardedComponent>(32);
|
||||
if (!PPatchTools.TryGetPropertyValue(instance, nameof(ModData), out
|
||||
IDictionary<string, object> modData))
|
||||
throw new ArgumentException("Remote instance missing ModData");
|
||||
ModData = modData;
|
||||
var type = instance.GetType();
|
||||
addCandidateVersion = type.CreateDelegate<Action<object>>(nameof(
|
||||
PRegistryComponent.DoAddCandidateVersion), instance, typeof(object));
|
||||
getAllComponents = type.CreateDelegate<GetAllComponentsDelegate>(nameof(
|
||||
PRegistryComponent.DoGetAllComponents), instance, typeof(string));
|
||||
getLatestVersion = type.CreateDelegate<GetObjectDelegate>(nameof(
|
||||
PRegistryComponent.DoGetLatestVersion), instance, typeof(string));
|
||||
if (addCandidateVersion == null || getLatestVersion == null ||
|
||||
getAllComponents == null)
|
||||
throw new ArgumentException("Remote instance missing candidate versions");
|
||||
getSharedData = type.CreateDelegate<GetObjectDelegate>(nameof(IPLibRegistry.
|
||||
GetSharedData), instance, typeof(string));
|
||||
setSharedData = type.CreateDelegate<SetObjectDelegate>(nameof(IPLibRegistry.
|
||||
SetSharedData), instance, typeof(string), typeof(object));
|
||||
if (getSharedData == null || setSharedData == null)
|
||||
throw new ArgumentException("Remote instance missing shared data");
|
||||
}
|
||||
|
||||
public void AddCandidateVersion(PForwardedComponent instance) {
|
||||
addCandidateVersion.Invoke(instance);
|
||||
}
|
||||
|
||||
public IEnumerable<PForwardedComponent> GetAllComponents(string id) {
|
||||
ICollection<PForwardedComponent> results = null;
|
||||
var all = getAllComponents.Invoke(id);
|
||||
if (all != null) {
|
||||
results = new List<PForwardedComponent>(all.Count);
|
||||
foreach (var component in all)
|
||||
if (component is PForwardedComponent local)
|
||||
results.Add(local);
|
||||
else
|
||||
results.Add(new PRemoteComponent(component));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public PForwardedComponent GetLatestVersion(string id) {
|
||||
if (!remoteComponents.TryGetValue(id, out PForwardedComponent remoteComponent)) {
|
||||
// Attempt to resolve it
|
||||
object instantiated = getLatestVersion.Invoke(id);
|
||||
if (instantiated == null) {
|
||||
#if DEBUG
|
||||
PRegistry.LogPatchWarning("Unable to find a component matching: " + id);
|
||||
#endif
|
||||
remoteComponent = null;
|
||||
} else if (instantiated is PForwardedComponent inThisMod)
|
||||
// Running the current version
|
||||
remoteComponent = inThisMod;
|
||||
else
|
||||
remoteComponent = new PRemoteComponent(instantiated);
|
||||
remoteComponents.Add(id, remoteComponent);
|
||||
}
|
||||
return remoteComponent;
|
||||
}
|
||||
|
||||
public object GetSharedData(string id) {
|
||||
return getSharedData.Invoke(id);
|
||||
}
|
||||
|
||||
public void SetSharedData(string id, object data) {
|
||||
setSharedData.Invoke(id, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
164
mod/PLibCore/TextMeshProPatcher.cs
Normal file
164
mod/PLibCore/TextMeshProPatcher.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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 PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.UI {
|
||||
/// <summary>
|
||||
/// Patches bugs in Text Mesh Pro.
|
||||
/// </summary>
|
||||
internal static class TextMeshProPatcher {
|
||||
/// <summary>
|
||||
/// The ID to use for Harmony patches.
|
||||
/// </summary>
|
||||
private const string HARMONY_ID = "TextMeshProPatch";
|
||||
|
||||
/// <summary>
|
||||
/// Tracks whether the TMP patches have been checked.
|
||||
/// </summary>
|
||||
private static volatile bool patchChecked = false;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes multiple thread access to the patch status.
|
||||
/// </summary>
|
||||
private static readonly object patchLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Applied to TMP_InputField to fix a bug that prevented auto layout from ever
|
||||
/// working.
|
||||
/// </summary>
|
||||
private static bool AssignPositioningIfNeeded_Prefix(TMP_InputField __instance,
|
||||
RectTransform ___caretRectTrans, TMP_Text ___m_TextComponent) {
|
||||
bool cont = true;
|
||||
var crt = ___caretRectTrans;
|
||||
if (___m_TextComponent != null && crt != null && __instance != null &&
|
||||
___m_TextComponent.isActiveAndEnabled) {
|
||||
var rt = ___m_TextComponent.rectTransform;
|
||||
if (crt.localPosition != rt.localPosition ||
|
||||
crt.localRotation != rt.localRotation ||
|
||||
crt.localScale != rt.localScale ||
|
||||
crt.anchorMin != rt.anchorMin ||
|
||||
crt.anchorMax != rt.anchorMax ||
|
||||
crt.anchoredPosition != rt.anchoredPosition ||
|
||||
crt.sizeDelta != rt.sizeDelta ||
|
||||
crt.pivot != rt.pivot) {
|
||||
__instance.StartCoroutine(ResizeCaret(crt, rt));
|
||||
cont = false;
|
||||
}
|
||||
}
|
||||
return cont;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if a patch with our class name has already been applied.
|
||||
/// </summary>
|
||||
/// <param name="patchList">The patch list to search.</param>
|
||||
/// <returns>true if a patch with this class has already patched the method, or false otherwise.</returns>
|
||||
private static bool HasOurPatch(IEnumerable<Patch> patchList) {
|
||||
bool found = false;
|
||||
if (patchList != null) {
|
||||
foreach (var patch in patchList) {
|
||||
string ownerName = patch.PatchMethod.DeclaringType.Name;
|
||||
// Avoid stomping ourselves, or legacy PLibs < 3.14
|
||||
if (ownerName == nameof(TextMeshProPatcher) || ownerName == "PLibPatches")
|
||||
{
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("TextMeshProPatcher found existing patch from: {0}".
|
||||
F(patch.owner));
|
||||
#endif
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches TMP_InputField with fixes, but only if necessary.
|
||||
/// </summary>
|
||||
/// <param name="tmpType">The type of TMP_InputField.</param>
|
||||
/// <param name="instance">The Harmony instance to use for patching.</param>
|
||||
private static void InputFieldPatches(Type tmpType) {
|
||||
var instance = new Harmony(HARMONY_ID);
|
||||
var aip = tmpType.GetMethodSafe("AssignPositioningIfNeeded", false,
|
||||
PPatchTools.AnyArguments);
|
||||
if (aip != null && !HasOurPatch(Harmony.GetPatchInfo(aip)?.Prefixes))
|
||||
instance.Patch(aip, prefix: new HarmonyMethod(typeof(TextMeshProPatcher),
|
||||
nameof(AssignPositioningIfNeeded_Prefix)));
|
||||
var oe = tmpType.GetMethodSafe("OnEnable", false, PPatchTools.AnyArguments);
|
||||
if (oe != null && !HasOurPatch(Harmony.GetPatchInfo(oe)?.Postfixes))
|
||||
instance.Patch(oe, postfix: new HarmonyMethod(typeof(TextMeshProPatcher),
|
||||
nameof(OnEnable_Postfix)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applied to TMPro.TMP_InputField to fix a clipping bug inside of Scroll Rects.
|
||||
///
|
||||
/// https://forum.unity.com/threads/textmeshpro-text-still-visible-when-using-nested-rectmask2d.537967/
|
||||
/// </summary>
|
||||
private static void OnEnable_Postfix(UnityEngine.UI.Scrollbar ___m_VerticalScrollbar,
|
||||
TMP_Text ___m_TextComponent) {
|
||||
var component = ___m_TextComponent;
|
||||
if (component != null)
|
||||
component.ignoreRectMaskCulling = ___m_VerticalScrollbar != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches Text Mesh Pro input fields to fix a variety of bugs. Should be used before
|
||||
/// any Text Mesh Pro objects are created.
|
||||
/// </summary>
|
||||
public static void Patch() {
|
||||
lock (patchLock) {
|
||||
if (!patchChecked) {
|
||||
var tmpType = PPatchTools.GetTypeSafe("TMPro.TMP_InputField");
|
||||
if (tmpType != null)
|
||||
try {
|
||||
InputFieldPatches(tmpType);
|
||||
} catch (Exception) {
|
||||
PUtil.LogWarning("Unable to patch TextMeshPro bug, text fields may display improperly inside scroll areas");
|
||||
}
|
||||
patchChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes the caret object to match the text. Used as an enumerator.
|
||||
/// </summary>
|
||||
/// <param name="caretTransform">The rectTransform of the caret.</param>
|
||||
/// <param name="textTransform">The rectTransform of the text.</param>
|
||||
private static System.Collections.IEnumerator ResizeCaret(
|
||||
RectTransform caretTransform, RectTransform textTransform) {
|
||||
yield return new WaitForEndOfFrame();
|
||||
caretTransform.localPosition = textTransform.localPosition;
|
||||
caretTransform.localRotation = textTransform.localRotation;
|
||||
caretTransform.localScale = textTransform.localScale;
|
||||
caretTransform.anchorMin = textTransform.anchorMin;
|
||||
caretTransform.anchorMax = textTransform.anchorMax;
|
||||
caretTransform.anchoredPosition = textTransform.anchoredPosition;
|
||||
caretTransform.sizeDelta = textTransform.sizeDelta;
|
||||
caretTransform.pivot = textTransform.pivot;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
mod/PLibCore/translations/fr.po
Normal file
96
mod/PLibCore/translations/fr.po
Normal file
@@ -0,0 +1,96 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Application: Oxygen Not IncludedPOT Version: 2.0\n"
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.4.1\n"
|
||||
"Last-Translator: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"Language: fr\n"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||
msgid "MANUAL CONFIG"
|
||||
msgstr "CONFIGURATION MANUELLE"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||
msgid "Options for {0}"
|
||||
msgstr "Options pour {0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||
msgid "Assembly Version: {0}"
|
||||
msgstr "Version d'assemblage: {0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||
msgid "Mod Homepage"
|
||||
msgstr "Page d'accueil du mod"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||
msgid "Mod Version: {0}"
|
||||
msgstr "Version du mod: {0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||
msgstr ""
|
||||
"Oxygen Not Included doit être relancé pour que ces options prennent effet."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||
msgid "Discard changes."
|
||||
msgstr "Annuler les changements."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||
msgid "Visit the mod's website."
|
||||
msgstr "Visiter le site web du mod."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||
msgid "Opens the folder containing the full mod configuration."
|
||||
msgstr "Ouvre le dossier contenant la configuration complète du mod."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||
msgid "Next"
|
||||
msgstr "Suivant"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||
msgid ""
|
||||
"Save these options. Some mods may require a restart for the options to take "
|
||||
"effect."
|
||||
msgstr ""
|
||||
"Conservez ces options. Certains mods peuvent nécessiter un redémarrage pour "
|
||||
"que les options prennent effet."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||
msgid "Previous"
|
||||
msgstr "Précédent"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||
msgid "Show or hide this options category"
|
||||
msgstr "Afficher ou masquer cette catégorie d'options"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||
msgid ""
|
||||
"The currently installed version of this mod.\n"
|
||||
"\n"
|
||||
"Compare this version with the mod's Release Notes to see if it is outdated."
|
||||
msgstr ""
|
||||
"La version actuellement installée de ce mod.\n"
|
||||
"\n"
|
||||
"Comparez cette version avec les notes de mise à jour du mod pour voir si "
|
||||
"elle n'est pas dépassée."
|
||||
163
mod/PLibCore/translations/ru.po
Normal file
163
mod/PLibCore/translations/ru.po
Normal file
@@ -0,0 +1,163 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Application: Oxygen Not Included\n"
|
||||
"POT Version: 2.0\n"
|
||||
"Project-Id-Version:\n"
|
||||
"POT-Creation-Date:\n"
|
||||
"PO-Revision-Date:\n"
|
||||
"Language-Team:\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"Last-Translator:\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"Language: ru\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||
msgid "MANUAL CONFIG"
|
||||
msgstr "РУЧНАЯ НАСТРОЙКА"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_RESET
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_RESET"
|
||||
msgid "RESET TO DEFAULT"
|
||||
msgstr "СБРОС ПО УМОЛЧАНИЮ"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||
msgid "Options for {0}"
|
||||
msgstr "Настройки для {0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN"
|
||||
msgid "Down Arrow"
|
||||
msgstr "Стрелка Вниз"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT"
|
||||
msgid "Left Arrow"
|
||||
msgstr "Стрелка Влево"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT"
|
||||
msgid "Right Arrow"
|
||||
msgstr "Стрелка Вправо"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP"
|
||||
msgid "Up Arrow"
|
||||
msgstr "Стрелка Вверх"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE"
|
||||
msgid "Mods"
|
||||
msgstr "Моды"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||
msgid "Assembly Version: {0}"
|
||||
msgstr "Версия сборки: {0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||
msgid "Mod Homepage"
|
||||
msgstr "Страница мода"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||
msgid "Mod Version: {0}"
|
||||
msgstr "Версия мода: {0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP"
|
||||
msgid "This mod is out of date!\nNew version: <b>{0}</b>\n\nUpdate local mods manually, or use <b>Mod Updater</b> to force update Steam mods"
|
||||
msgstr "Этот мод устарел!\nНовая версия: <b>{0}</b>\n\nОбновите локальные моды вручную или используйте <b>Mod Updater</b> для принудительного обновления модов Steam"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING"
|
||||
msgid "<b><style=\"logic_off\">Outdated!</style></b>"
|
||||
msgstr "<b><style=\"logic_off\">Устарел!</style></b>"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||
msgstr "Чтобы эти параметры вступили в силу, необходимо перезапустить Oxygen Not Included."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE"
|
||||
msgid "Blue"
|
||||
msgstr "Синий"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||
msgid "Discard changes."
|
||||
msgstr "Отменить изменения."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN"
|
||||
msgid "Green"
|
||||
msgstr "Зелёный"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||
msgid "Visit the mod's website."
|
||||
msgstr "Посетите сайт мода."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE"
|
||||
msgid "Hue"
|
||||
msgstr "Тон"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||
msgid "Opens the folder containing the full mod configuration."
|
||||
msgstr "Открыть папку, содержащую полную конфигурацию мода."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||
msgid "Next"
|
||||
msgstr "Следующий"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||
msgid "Save these options. Some mods may require a restart for the options to take effect."
|
||||
msgstr "Сохранить эти параметры. Для некоторых модов может потребоваться перезапуск, чтобы параметры вступили в силу."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||
msgid "Previous"
|
||||
msgstr "Предыдущий"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED"
|
||||
msgid "Red"
|
||||
msgstr "Красный"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET"
|
||||
msgid "Resets the mod configuration to default values."
|
||||
msgstr "Сбросить конфигурацию мода до значений по умолчанию."
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION"
|
||||
msgid "Saturation"
|
||||
msgstr "Насыщенность"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||
msgid "Show or hide this options category"
|
||||
msgstr "Показать или скрыть эту категорию настроек"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE"
|
||||
msgid "Value"
|
||||
msgstr "Яркость"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||
msgid "The currently installed version of this mod.\n\nCompare this version with the mod's Release Notes to see if it is outdated."
|
||||
msgstr "Текущая установленная версия этого мода.\n\nСравните эту версию с примечаниями к выпуску мода, чтобы узнать, не устарел ли он."
|
||||
|
||||
150
mod/PLibCore/translations/template.pot
Normal file
150
mod/PLibCore/translations/template.pot
Normal file
@@ -0,0 +1,150 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Application: Oxygen Not Included"
|
||||
"POT Version: 2.0"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||
msgid "MANUAL CONFIG"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_RESET
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_RESET"
|
||||
msgid "RESET TO DEFAULT"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||
msgid "Options for {0}"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN"
|
||||
msgid "Down Arrow"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT"
|
||||
msgid "Left Arrow"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT"
|
||||
msgid "Right Arrow"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP"
|
||||
msgid "Up Arrow"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE"
|
||||
msgid "Mods"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||
msgid "Assembly Version: {0}"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||
msgid "Mod Homepage"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||
msgid "Mod Version: {0}"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP"
|
||||
msgid "This mod is out of date!\nNew version: <b>{0}</b>\n\nUpdate local mods manually, or use <b>Mod Updater</b> to force update Steam mods"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING"
|
||||
msgid "<b><style=\"logic_off\">Outdated!</style></b>"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_BLUE"
|
||||
msgid "Blue"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||
msgid "Discard changes."
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_GREEN"
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||
msgid "Visit the mod's website."
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HUE"
|
||||
msgid "Hue"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||
msgid "Opens the folder containing the full mod configuration."
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||
msgid "Save these options. Some mods may require a restart for the options to take effect."
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||
msgid "Previous"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RED"
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET"
|
||||
msgid "Resets the mod configuration to default values."
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_SATURATION"
|
||||
msgid "Saturation"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||
msgid "Show or hide this options category"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VALUE"
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||
msgid "The currently installed version of this mod.\n\nCompare this version with the mod's Release Notes to see if it is outdated."
|
||||
msgstr ""
|
||||
|
||||
206
mod/PLibCore/translations/zh.po
Normal file
206
mod/PLibCore/translations/zh.po
Normal file
@@ -0,0 +1,206 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Ventulus\n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"Application: Oxygen Not Included\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_MANUAL"
|
||||
msgid "MANUAL CONFIG"
|
||||
msgstr "手动配置"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_OK
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_OK"
|
||||
msgid "Done"
|
||||
msgstr "完成"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_OPTIONS
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_OPTIONS"
|
||||
msgid "OPTIONS"
|
||||
msgstr "选项"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.BUTTON_RESET
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.BUTTON_RESET"
|
||||
msgid "RESET TO DEFAULT"
|
||||
msgstr "重置为默认"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE"
|
||||
msgid "Options for {0}"
|
||||
msgstr "{0}的选项"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWDOWN"
|
||||
msgid "Down Arrow"
|
||||
msgstr "↓"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWLEFT"
|
||||
msgid "Left Arrow"
|
||||
msgstr "←"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWRIGHT"
|
||||
msgid "Right Arrow"
|
||||
msgstr "→"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_ARROWUP"
|
||||
msgid "Up Arrow"
|
||||
msgstr "↑"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE"
|
||||
msgid "Mods"
|
||||
msgstr "模组"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_DELETE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_DELETE"
|
||||
msgid "Delete"
|
||||
msgstr "Delete"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_END
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_END"
|
||||
msgid "End"
|
||||
msgstr "End"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_HOME
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_HOME"
|
||||
msgid "Home"
|
||||
msgstr "Home"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_PAGEDOWN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PAGEDOWN"
|
||||
msgid "Page Down"
|
||||
msgstr "Page Down"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_PAGEUP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PAGEUP"
|
||||
msgid "Page Up"
|
||||
msgstr "Page Up"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_PAUSE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PAUSE"
|
||||
msgid "Pause"
|
||||
msgstr "Pause"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_PRTSCREEN
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_PRTSCREEN"
|
||||
msgid "Print Screen"
|
||||
msgstr "Print Screen"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.KEY_SYSRQ
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_SYSRQ"
|
||||
msgid "SysRq"
|
||||
msgstr "SysRq"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION"
|
||||
msgid "Assembly Version: {0}"
|
||||
msgstr "程序集版本:{0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_HOMEPAGE"
|
||||
msgid "Mod Homepage"
|
||||
msgstr "模组主页"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.MOD_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_VERSION"
|
||||
msgid "Mod Version: {0}"
|
||||
msgstr "模组版本:{0}"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_TOOLTIP"
|
||||
msgid ""
|
||||
"This mod is out of date!\n"
|
||||
"New version: <b>{0}</b>\n"
|
||||
"\n"
|
||||
"Update local mods manually, or use <b>Mod Updater</b> to force update Steam "
|
||||
"mods"
|
||||
msgstr ""
|
||||
"这个模组已经过时了!\n"
|
||||
"新版本:<b>{0}</b>\n"
|
||||
"\n"
|
||||
"手动更新本地模组,或使用<b>Mod Updater</b>来强制更新Steam模组"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.OUTDATED_WARNING"
|
||||
msgid "<b><style=\"logic_off\">Outdated!</style></b>"
|
||||
msgstr "<b><style=\"logic_off\">过时了!</style></b>"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.RESTART_CANCEL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_CANCEL"
|
||||
msgid "CONTINUE"
|
||||
msgstr "继续"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.RESTART_OK
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_OK"
|
||||
msgid "RESTART"
|
||||
msgstr "重启"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.RESTART_REQUIRED"
|
||||
msgid "Oxygen Not Included must be restarted for these options to take effect."
|
||||
msgstr "要使这些选项生效,必须重新启动游戏。"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_CANCEL"
|
||||
msgid "Discard changes."
|
||||
msgstr "放弃更改。"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_HOMEPAGE"
|
||||
msgid "Visit the mod's website."
|
||||
msgstr "访问模组网站。"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_MANUAL"
|
||||
msgid "Opens the folder containing the full mod configuration."
|
||||
msgstr "打开包含完整模组配置的文件夹。"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_NEXT"
|
||||
msgid "Next"
|
||||
msgstr "下一个"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_OK"
|
||||
msgid ""
|
||||
"Save these options. Some mods may require a restart for the options to take "
|
||||
"effect."
|
||||
msgstr "保存这些选项。某些模组可能需要重新启动才能使选项生效。"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_PREVIOUS"
|
||||
msgid "Previous"
|
||||
msgstr "上一个"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_RESET"
|
||||
msgid "Resets the mod configuration to default values."
|
||||
msgstr "将模组配置重置为默认值。"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_TOGGLE"
|
||||
msgid "Show or hide this options category"
|
||||
msgstr "显示或隐藏此选项类别"
|
||||
|
||||
#. PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION
|
||||
msgctxt "PeterHan.PLib.Core.PLibStrings.TOOLTIP_VERSION"
|
||||
msgid ""
|
||||
"The currently installed version of this mod.\n"
|
||||
"\n"
|
||||
"Compare this version with the mod's Release Notes to see if it is outdated."
|
||||
msgstr ""
|
||||
"此模组的当前安装版本。\n"
|
||||
"\n"
|
||||
"请将此版本与模组的发行说明进行比较,以查看它是否过时。"
|
||||
Reference in New Issue
Block a user