Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View 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) { }
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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; }
}
}

View 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);
}
}
}

View 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());
}
}
}
}

View 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;
}
}
}

View 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);
}
}

View 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();
}
}

View 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
View 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();
}
}
}

View 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>

View 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);
}
}
}

View 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
View 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

File diff suppressed because it is too large Load Diff

137
mod/PLibCore/PRegistry.cs Normal file
View 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);
}
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View 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
View 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
View 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";
}
}

View 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();
}
}
}

View 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);
}
}

View 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);
}
}

View 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)");
}
}
}
}

View 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;
}
}
}
}
}

View 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!");
}
}
}

View 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();
}
}
}

View 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&lt;<typeparamref name="T"/>&gt; with the default
/// initial capacity.
/// </summary>
public PriorityQueue() : this(32) { }
/// <summary>
/// Creates a new PriorityQueue&lt;<typeparamref name="T"/>&gt; 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&lt;<typeparamref name="T"/>&gt;.
/// </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&lt;<typeparamref name="T"/>&gt;.
///
/// 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&lt;<typeparamref name="T"/>&gt;.
/// </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&lt;<typeparamref name="T"/>&gt;
/// 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&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt; with the default initial capacity.
/// </summary>
public PriorityDictionary() : base() { }
/// <summary>
/// Creates a new PriorityDictionary&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt; 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&lt;<typeparamref name="K"/>, <typeparamref name="V"/>&gt;.
///
/// 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&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt;.
/// </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&lt;<typeparamref name="K"/>,
/// <typeparamref name="V"/>&gt; 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 + "]";
}
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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."

View 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Сравните эту версию с примечаниями к выпуску мода, чтобы узнать, не устарел ли он."

View 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 ""

View 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"
"请将此版本与模组的发行说明进行比较,以查看它是否过时。"