Upload from upload_mods.ps1

This commit is contained in:
Nathaniel Cosford
2025-06-04 16:13:32 +09:30
commit 7345f42201
470 changed files with 51966 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
using System;
//https://gist.github.com/Kryzarel/bba64622057f21a1d6d44879f9cd7bd4
namespace Kryz.Tweening
{
// Made with the help of this great post: https://joshondesign.com/2013/03/01/improvedEasingEquations
// --------------------------------- Other Related Links --------------------------------------------------------------------
// Original equations, bad formulation: https://github.com/danro/jquery-easing/blob/master/jquery.easing.js
// A few equations, very simplified: https://gist.github.com/gre/1650294
// Easings.net equations, simplified: https://github.com/ai/easings.net/blob/master/src/easings/easingsFunctions.ts
public static class EasingFunctions
{
public static float Linear(float t) => t;
public static float InQuad(float t) => t * t;
public static float OutQuad(float t) => 1 - InQuad(1 - t);
public static float InOutQuad(float t)
{
if (t < 0.5) return InQuad(t * 2) / 2;
return 1 - InQuad((1 - t) * 2) / 2;
}
public static float InCubic(float t) => t * t * t;
public static float OutCubic(float t) => 1 - InCubic(1 - t);
public static float InOutCubic(float t)
{
if (t < 0.5) return InCubic(t * 2) / 2;
return 1 - InCubic((1 - t) * 2) / 2;
}
public static float InQuart(float t) => t * t * t * t;
public static float OutQuart(float t) => 1 - InQuart(1 - t);
public static float InOutQuart(float t)
{
if (t < 0.5) return InQuart(t * 2) / 2;
return 1 - InQuart((1 - t) * 2) / 2;
}
public static float InQuint(float t) => t * t * t * t * t;
public static float OutQuint(float t) => 1 - InQuint(1 - t);
public static float InOutQuint(float t)
{
if (t < 0.5) return InQuint(t * 2) / 2;
return 1 - InQuint((1 - t) * 2) / 2;
}
public static float InSine(float t) => 1 - (float)Math.Cos(t * Math.PI / 2);
public static float OutSine(float t) => (float)Math.Sin(t * Math.PI / 2);
public static float InOutSine(float t) => (float)(Math.Cos(t * Math.PI) - 1) / -2;
public static float InExpo(float t) => (float)Math.Pow(2, 10 * (t - 1));
public static float OutExpo(float t) => 1 - InExpo(1 - t);
public static float InOutExpo(float t)
{
if (t < 0.5) return InExpo(t * 2) / 2;
return 1 - InExpo((1 - t) * 2) / 2;
}
public static float InCirc(float t) => -((float)Math.Sqrt(1 - t * t) - 1);
public static float OutCirc(float t) => 1 - InCirc(1 - t);
public static float InOutCirc(float t)
{
if (t < 0.5) return InCirc(t * 2) / 2;
return 1 - InCirc((1 - t) * 2) / 2;
}
public static float InElastic(float t) => 1 - OutElastic(1 - t);
public static float OutElastic(float t)
{
float p = 0.3f;
return (float)Math.Pow(2, -10 * t) * (float)Math.Sin((t - p / 4) * (2 * Math.PI) / p) + 1;
}
public static float InOutElastic(float t)
{
if (t < 0.5) return InElastic(t * 2) / 2;
return 1 - InElastic((1 - t) * 2) / 2;
}
public static float InBack(float t)
{
float s = 1.70158f;
return t * t * ((s + 1) * t - s);
}
public static float OutBack(float t) => 1 - InBack(1 - t);
public static float InOutBack(float t)
{
if (t < 0.5) return InBack(t * 2) / 2;
return 1 - InBack((1 - t) * 2) / 2;
}
public static float InBounce(float t) => 1 - OutBounce(1 - t);
public static float OutBounce(float t)
{
float div = 2.75f;
float mult = 7.5625f;
if (t < 1 / div)
{
return mult * t * t;
}
else if (t < 2 / div)
{
t -= 1.5f / div;
return mult * t * t + 0.75f;
}
else if (t < 2.5 / div)
{
t -= 2.25f / div;
return mult * t * t + 0.9375f;
}
else
{
t -= 2.625f / div;
return mult * t * t + 0.984375f;
}
}
public static float InOutBounce(float t)
{
if (t < 0.5) return InBounce(t * 2) / 2;
return 1 - InBounce((1 - t) * 2) / 2;
}
}
}

View File

@@ -0,0 +1,67 @@
public static class EntityInventoryExtension
{
public static void TryStackItem(this EntityAlive self, ItemStack stack)
{
if (self.bag.TryStackItem(stack))
return;
self.inventory.TryStackItem(stack);
}
public static void TryRemoveItem(this EntityAlive self, int count, ItemValue value)
{
int decFromInv = self.bag.DecItem(value, count) - count;
if (decFromInv > 0)
self.inventory.DecItem(value, decFromInv);
}
public static int GetItemCount(this EntityAlive self, ItemValue value)
{
return self.inventory.GetItemCount(value) + self.bag.GetItemCount(value);
}
public static bool TryStackItem(this Bag self, ItemStack stack)
{
ItemStack[] slots = self.GetSlots();
TryStackItem(slots, stack);
self.SetSlots(slots);
return stack.count == 0;
}
public static bool TryStackItem(this Inventory self, ItemStack stack)
{
ItemStack[] slots = self.GetSlots();
TryStackItem(slots, stack);
self.CallOnToolbeltChangedInternal();
return stack.count == 0;
}
public static void TryStackWith(this ItemStack self, ItemStack other)
{
int maxStackCount = other.itemValue.ItemClass.Stacknumber.Value;
if (self.IsEmpty())
{
self.itemValue = other.itemValue.Clone();
self.count = Utils.FastMin(maxStackCount, other.count);
other.count -= self.count;
return;
}
if (self.itemValue.type != other.itemValue.type || self.itemValue.Texture != other.itemValue.Texture || self.count >= maxStackCount)
return;
int add = Utils.FastMin(maxStackCount - self.count, other.count);
self.count += add;
other.count -= add;
}
private static void TryStackItem(ItemStack[] slots, ItemStack stack)
{
foreach (var slot in slots)
{
slot.TryStackWith(stack);
if (stack.count == 0)
return;
}
}
}

View File

@@ -0,0 +1,424 @@
using System;
using System.Collections.Generic;
using UniLinq;
using Sprache;
using static Sprache.Parse;
namespace CodeWriter.ExpressionParser
{
public delegate T Expression<out T>();
public abstract class ExpressionParser<T>
{
private readonly Dictionary<string, ExprBuilder> _builderCached = new Dictionary<string, ExprBuilder>();
private Parser<ExprBuilder> _parserCached;
public Expression<bool> CompilePredicate(string input, ExpressionContext<T> context, bool cache)
{
var expr = Compile(input, context, cache);
return () => IsTrue(expr.Invoke());
}
public Expression<T> Compile(string input, ExpressionContext<T> context, bool cache)
{
context = context ?? new ExpressionContext<T>();
_parserCached = _parserCached ?? CreateParser();
ExprBuilder builder;
try
{
if (cache)
{
if (!_builderCached.TryGetValue(input, out builder))
{
builder = _parserCached.Parse(input);
_builderCached.Add(input, builder);
}
}
else
{
builder = _parserCached.Parse(input);
}
}
catch (ParseException parseException)
{
throw new ExpressionParseException(input, parseException);
}
return builder.Invoke(context);
}
private static Parser<BinaryFunc> Operator(string op, BinaryFunc fun)
{
return String(op).Token().Return(fun).Named(op);
}
private static Parser<ExprBuilder> BinaryOperator(Parser<BinaryFunc> op,
Parser<ExprBuilder> operand, Func<BinaryFunc, ExprBuilder, ExprBuilder, ExprBuilder> apply) =>
(
from l in operand
from rest in
(
from f in op
from r in operand
select apply(f, l, r)
).Optional()
select rest.IsEmpty ? l : rest.Get()
);
private Parser<ExprBuilder> CreateParser()
{
var letterOrUnderscore = Char(c => char.IsLetter(c) || c == '_',
"letter or underscore");
var letterOrDigitOrUnderscore = Char(c => char.IsLetterOrDigit(c) || c == '_',
"letter or digit or underscore");
var constant = (
from number in DecimalInvariant
select MakeConstant(number, Parse)
).Named("number");
var add = Operator("+", Add);
var sub = Operator("-", Sub);
var mul = Operator("*", Mul);
var div = Operator("/", Div);
var mod = Operator("%", Mod);
var pow = Operator("^", Pow);
var eq = Operator("=", Equal);
var neq = Operator("!=", NotEqual);
var and = Operator("AND", And);
var or = Operator("OR", Or);
var lt = Operator("<", LessThan);
var lte = Operator("<=", LessThanOrEqual);
var gt = Operator(">", GreaterThan);
var gte = Operator(">=", GreaterThanOrEqual);
var variable =
(
from nameHead in letterOrUnderscore.Once().Text()
from nameTail in letterOrDigitOrUnderscore.Many().Text()
select MakeVariable(nameHead + nameTail)
).Named("variable");
Parser<ExprBuilder> expression = null;
var function =
(
from name in Letter.AtLeastOnce().Text()
from lparen in Char('(')
// ReSharper disable once AccessToModifiedClosure
from expr in Ref(() => expression).DelimitedBy(Char(',').Token())
from rparen in Char(')')
select MakeFunction(name, expr.ToList())
).Named("function");
var factor =
(
from lparen in Char('(')
// ReSharper disable once AccessToModifiedClosure
from expr in Ref(() => expression)
from rparen in Char(')')
select expr
).Named("expression")
.XOr(constant)
.XOr(function)
.Or(variable);
var operand =
(
(
from sign in Char('-')
from fact in factor
select MakeUnary(Negate, fact)
)
.XOr(factor)
).Token();
var term1 = ChainRightOperator(pow, operand, MakeBinary);
var term2 = ChainOperator(mul.Or(div).Or(mod), term1, MakeBinary);
var term3 = ChainOperator(add.Or(sub), term2, MakeBinary);
var term4 = BinaryOperator(lte.Or(lt).Or(gte).Or(gt), term3, MakeBinary);
var term5 = BinaryOperator(eq.Or(neq), term4, MakeBinary);
var term6 = ChainOperator(and, term5, MakeBinary);
var term7 = ChainOperator(or, term6, MakeBinary);
expression = term7;
return expression.End();
}
protected abstract T False { get; }
protected abstract T True { get; }
protected abstract T Parse(string input);
protected abstract T Negate(T v);
protected abstract T Add(T a, T b);
protected abstract T Sub(T a, T b);
protected abstract T Mul(T a, T b);
protected abstract T Div(T a, T b);
protected abstract T Mod(T a, T b);
protected abstract T Pow(T a, T b);
protected abstract T Equal(T a, T b);
protected abstract T NotEqual(T a, T b);
protected abstract T LessThan(T a, T b);
protected abstract T LessThanOrEqual(T a, T b);
protected abstract T GreaterThan(T a, T b);
protected abstract T GreaterThanOrEqual(T a, T b);
protected abstract bool IsTrue(T v);
protected abstract T Round(T v);
protected abstract T Floor(T v);
protected abstract T Ceiling(T v);
protected abstract T Log10(T v);
protected virtual T Log(T v, T newBase) => Div(Log10(v), Log10(newBase));
private T Not(T v) => IsTrue(v) ? False : True;
private T And(T a, T b) => IsTrue(a) ? b : a;
private T Or(T a, T b) => IsTrue(a) ? a : b;
private T Min(T a, T b) => IsTrue(GreaterThan(a, b)) ? b : a;
private T Max(T a, T b) => IsTrue(GreaterThan(b, a)) ? b : a;
private delegate Expression<T> ExprBuilder(ExpressionContext<T> context);
private delegate T UnaryFunc(T a);
private delegate T BinaryFunc(T a, T b);
private ExprBuilder MakeFunction(string name, List<ExprBuilder> parameterBuilders)
{
switch (name)
{
case "NOT":
return MakeFunction1(Not);
case "ROUND":
return MakeFunction1(Round);
case "CEILING":
return MakeFunction1(Ceiling);
case "FLOOR":
return MakeFunction1(Floor);
case "LOG":
return parameterBuilders.Count == 2
? MakeBinary(Log, parameterBuilders[0], parameterBuilders[1])
: MakeFunction1(Log10);
case "MIN":
return MakeFunctionFold(Min);
case "MAX":
return MakeFunctionFold(Max);
case "IF":
if (parameterBuilders.Count < 3 ||
parameterBuilders.Count % 2 != 1)
{
throw new FunctionNotDefinedException(name, "Wrong parameters count");
}
return context =>
{
var conditions = new List<Expression<T>>();
var results = new List<Expression<T>>();
var defaultResult = parameterBuilders[parameterBuilders.Count - 1].Invoke(context);
for (var i = 0; i < parameterBuilders.Count - 1; i += 2)
{
conditions.Add(parameterBuilders[i].Invoke(context));
results.Add(parameterBuilders[i + 1].Invoke(context));
}
return () =>
{
for (var i = 0; i < conditions.Count; i++)
{
if (IsTrue(conditions[i].Invoke()))
{
return results[i].Invoke();
}
}
return defaultResult.Invoke();
};
};
default: throw new FunctionNotDefinedException(name, "Unknown name");
}
ExprBuilder MakeFunction1(Func<T, T> func)
{
if (parameterBuilders.Count != 1)
{
throw new FunctionNotDefinedException(name, "Wrong parameters count");
}
return context =>
{
var inner = parameterBuilders[0].Invoke(context);
return () => func(inner.Invoke());
};
}
ExprBuilder MakeFunctionFold(Func<T, T, T> func)
{
if (parameterBuilders.Count < 1)
{
throw new FunctionNotDefinedException(name, "Wrong parameters count");
}
return context =>
{
var inner = new List<Expression<T>>();
for (var i = 0; i < parameterBuilders.Count; i++)
{
inner.Add(parameterBuilders[i].Invoke(context));
}
return () =>
{
var result = inner[0].Invoke();
for (var i = 1; i < inner.Count; i++)
{
result = func(result, inner[i].Invoke());
}
return result;
};
};
}
}
private static ExprBuilder MakeUnary(UnaryFunc func, ExprBuilder innerBuilder)
{
return context =>
{
var inner = innerBuilder.Invoke(context);
return () => func(inner.Invoke());
};
}
private static ExprBuilder MakeBinary(BinaryFunc func, ExprBuilder l, ExprBuilder r)
{
return context =>
{
var left = l.Invoke(context);
var right = r.Invoke(context);
return () => func(left.Invoke(), right.Invoke());
};
}
private ExprBuilder MakeVariable(string name)
{
if (name.Equals("TRUE", StringComparison.Ordinal))
{
return context => () => True;
}
if (name.Equals("FALSE", StringComparison.Ordinal))
{
return context => () => False;
}
return context =>
{
var variable = context.GetVariable(name);
return variable;
};
}
private static ExprBuilder MakeConstant(string valueString, Func<string, T> parser)
{
var value = parser(valueString);
return context => () => value;
}
}
public class ExpressionContext<T>
{
private readonly ExpressionContext<T> _parent;
private readonly Func<string, Expression<T>> _unregisteredVariableResolver;
private readonly Dictionary<string, Expression<T>> _variables = new Dictionary<string, Expression<T>>();
public ExpressionContext(ExpressionContext<T> parent = null,
Func<string, Expression<T>> unregisteredVariableResolver = null)
{
_parent = parent;
_unregisteredVariableResolver = unregisteredVariableResolver;
}
public void RegisterVariable(string name, Expression<T> value)
{
if (_variables.ContainsKey(name))
{
throw new InvalidOperationException($"Variable {name} already registered");
}
_variables.Add(name, value);
}
public Expression<T> GetVariable(string name, bool nullIsOk = false)
{
if (_variables.TryGetValue(name, out var variable))
{
return variable;
}
if (_unregisteredVariableResolver != null)
{
variable = _unregisteredVariableResolver.Invoke(name);
if (variable != null)
{
return variable;
}
}
var parentVariable = _parent?.GetVariable(name, nullIsOk: true);
if (parentVariable != null)
{
return parentVariable;
}
if (nullIsOk)
{
return null;
}
throw new VariableNotDefinedException(name);
}
}
[Obsolete("ExpresionContext contains a typo. Use ExpressionContext instead", true)]
public class ExpresionContext<T> : ExpressionContext<T>
{
}
public class VariableNotDefinedException : Exception
{
public VariableNotDefinedException(string name)
: base($"Variable '{name}' not defined")
{
}
}
public class FunctionNotDefinedException : Exception
{
public FunctionNotDefinedException(string name, string reason) : base(
$"Function '{name}' not defined: {reason}")
{
}
}
public class ExpressionParseException : Exception
{
public ExpressionParseException(string expression, ParseException parseException)
: base($"Failed to parse expression '{expression}'{Environment.NewLine}{parseException.Message}")
{
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Globalization;
using UnityEngine;
namespace CodeWriter.ExpressionParser
{
public class FloatExpressionParser : ExpressionParser<float>
{
public static readonly ExpressionParser<float> Instance = new FloatExpressionParser();
protected override float False { get; } = 0f;
protected override float True { get; } = 1f;
protected override float Parse(string input) =>
float.Parse(input, NumberStyles.Any, CultureInfo.InvariantCulture);
protected override float Negate(float v) => -v;
protected override float Add(float a, float b) => a + b;
protected override float Sub(float a, float b) => a - b;
protected override float Mul(float a, float b) => a * b;
protected override float Div(float a, float b) => a / b;
protected override float Mod(float a, float b) => a % b;
protected override float Pow(float a, float b) => Mathf.Pow(a, b);
protected override float Equal(float a, float b) => Mathf.Approximately(a, b) ? 1 : 0;
protected override float NotEqual(float a, float b) => !Mathf.Approximately(a, b) ? 1 : 0;
protected override float LessThan(float a, float b) => a < b ? 1 : 0;
protected override float LessThanOrEqual(float a, float b) => a <= b ? 1 : 0;
protected override float GreaterThan(float a, float b) => a > b ? 1 : 0;
protected override float GreaterThanOrEqual(float a, float b) => a >= b ? 1 : 0;
protected override bool IsTrue(float v) => !Mathf.Approximately(v, 0);
protected override float Round(float v) => (float) Math.Round(v);
protected override float Ceiling(float v) => (float) Math.Ceiling(v);
protected override float Floor(float v) => (float) Math.Floor(v);
protected override float Log10(float v) => (float) Math.Log10(v);
protected override float Log(float v, float newBase) => (float) Math.Log(v, newBase);
}
}

View File

@@ -0,0 +1,124 @@
namespace Sprache
{
/// <summary>
/// Constructs customizable comment parsers.
/// </summary>
public class CommentParser : IComment
{
///<summary>
///Single-line comment header.
///</summary>
public string Single { get; set; }
///<summary>
///Newline character preference.
///</summary>
public string NewLine { get; set; }
///<summary>
///Multi-line comment opener.
///</summary>
public string MultiOpen { get; set; }
///<summary>
///Multi-line comment closer.
///</summary>
public string MultiClose { get; set; }
/// <summary>
/// Initializes a Comment with C-style headers and Windows newlines.
/// </summary>
public CommentParser()
{
Single = "//";
MultiOpen = "/*";
MultiClose = "*/";
NewLine = "\n";
}
/// <summary>
/// Initializes a Comment with custom multi-line headers and newline characters.
/// Single-line headers are made null, it is assumed they would not be used.
/// </summary>
/// <param name="multiOpen"></param>
/// <param name="multiClose"></param>
/// <param name="newLine"></param>
public CommentParser(string multiOpen, string multiClose, string newLine)
{
Single = null;
MultiOpen = multiOpen;
MultiClose = multiClose;
NewLine = newLine;
}
/// <summary>
/// Initializes a Comment with custom headers and newline characters.
/// </summary>
/// <param name="single"></param>
/// <param name="multiOpen"></param>
/// <param name="multiClose"></param>
/// <param name="newLine"></param>
public CommentParser(string single, string multiOpen, string multiClose, string newLine)
{
Single = single;
MultiOpen = multiOpen;
MultiClose = multiClose;
NewLine = newLine;
}
///<summary>
///Parse a single-line comment.
///</summary>
public Parser<string> SingleLineComment
{
get
{
if (Single == null)
throw new ParseException("Field 'Single' is null; single-line comments not allowed.");
return from first in Parse.String(Single)
from rest in Parse.CharExcept(NewLine).Many().Text()
select rest;
}
private set { }
}
///<summary>
///Parse a multi-line comment.
///</summary>
public Parser<string> MultiLineComment
{
get
{
if (MultiOpen == null)
throw new ParseException("Field 'MultiOpen' is null; multi-line comments not allowed.");
else if (MultiClose == null)
throw new ParseException("Field 'MultiClose' is null; multi-line comments not allowed.");
return from first in Parse.String(MultiOpen)
from rest in Parse.AnyChar
.Until(Parse.String(MultiClose)).Text()
select rest;
}
private set { }
}
///<summary>
///Parse a comment.
///</summary>
public Parser<string> AnyComment
{
get
{
if (Single != null && MultiOpen != null && MultiClose != null)
return SingleLineComment.Or(MultiLineComment);
else if (Single != null && (MultiOpen == null || MultiClose == null))
return SingleLineComment;
else if (Single == null && (MultiOpen != null && MultiClose != null))
return MultiLineComment;
else throw new ParseException("Unable to parse comment; check values of fields 'MultiOpen' and 'MultiClose'.");
}
private set { }
}
}
}

View File

@@ -0,0 +1,43 @@
namespace Sprache
{
/// <summary>
/// Represents a customizable comment parser.
/// </summary>
public interface IComment
{
///<summary>
/// Single-line comment header.
///</summary>
string Single { get; set; }
///<summary>
/// Newline character preference.
///</summary>
string NewLine { get; set; }
///<summary>
/// Multi-line comment opener.
///</summary>
string MultiOpen { get; set; }
///<summary>
/// Multi-line comment closer.
///</summary>
string MultiClose { get; set; }
///<summary>
/// Parse a single-line comment.
///</summary>
Parser<string> SingleLineComment { get; }
///<summary>
/// Parse a multi-line comment.
///</summary>
Parser<string> MultiLineComment { get; }
///<summary>
/// Parse a comment.
///</summary>
Parser<string> AnyComment { get; }
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace Sprache
{
/// <summary>
/// Represents a commented result with its leading and trailing comments.
/// </summary>
/// <typeparam name="T">Type of the matched result.</typeparam>
public interface ICommented<T>
{
/// <summary>
/// Gets the leading comments.
/// </summary>
IEnumerable<string> LeadingComments { get; }
/// <summary>
/// Gets the resulting value.
/// </summary>
T Value { get; }
/// <summary>
/// Gets the trailing comments.
/// </summary>
IEnumerable<string> TrailingComments { get; }
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
namespace Sprache
{
/// <summary>
/// Represents an input for parsing.
/// </summary>
public interface IInput : IEquatable<IInput>
{
/// <summary>
/// Advances the input.
/// </summary>
/// <returns>A new <see cref="IInput" /> that is advanced.</returns>
/// <exception cref="System.InvalidOperationException">The input is already at the end of the source.</exception>
IInput Advance();
/// <summary>
/// Gets the whole source.
/// </summary>
string Source { get; }
/// <summary>
/// Gets the current <see cref="System.Char" />.
/// </summary>
char Current { get; }
/// <summary>
/// Gets a value indicating whether the end of the source is reached.
/// </summary>
bool AtEnd { get; }
/// <summary>
/// Gets the current positon.
/// </summary>
int Position { get; }
/// <summary>
/// Gets the current line number.
/// </summary>
int Line { get; }
/// <summary>
/// Gets the current column.
/// </summary>
int Column { get; }
/// <summary>
/// Memos used by this input
/// </summary>
IDictionary<object, object> Memos { get; }
}
}

View File

@@ -0,0 +1,18 @@

namespace Sprache
{
/// <summary>
/// An interface for objects that have a source <see cref="Position"/>.
/// </summary>
/// <typeparam name="T">Type of the matched result.</typeparam>
public interface IPositionAware<out T>
{
/// <summary>
/// Set the start <see cref="Position"/> and the matched length.
/// </summary>
/// <param name="startPos">The start position</param>
/// <param name="length">The matched length.</param>
/// <returns>The matched result.</returns>
T SetPos(Position startPos, int length);
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace Sprache
{
/// <summary>
/// Represents a parsing result.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
public interface IResult<out T>
{
/// <summary>
/// Gets the resulting value.
/// </summary>
T Value { get; }
/// <summary>
/// Gets a value indicating whether wether parsing was successful.
/// </summary>
bool WasSuccessful { get; }
/// <summary>
/// Gets the error message.
/// </summary>
string Message { get; }
/// <summary>
/// Gets the parser expectations in case of error.
/// </summary>
IEnumerable<string> Expectations { get; }
/// <summary>
/// Gets the remainder of the input.
/// </summary>
IInput Remainder { get; }
}
}

View File

@@ -0,0 +1,29 @@
namespace Sprache
{
/// <summary>
/// Represents a text span of the matched result.
/// </summary>
/// <typeparam name="T">Type of the matched result.</typeparam>
public interface ITextSpan<T>
{
/// <summary>
/// Gets the resulting value.
/// </summary>
T Value { get; }
/// <summary>
/// Gets the starting <see cref="Position"/>.
/// </summary>
Position Start { get; }
/// <summary>
/// Gets the ending <see cref="Position"/>.
/// </summary>
Position End { get; }
/// <summary>
/// Gets the length of the text span.
/// </summary>
int Length { get; }
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
namespace Sprache
{
/// <summary>
/// Represents an input for parsing.
/// </summary>
public class Input : IInput
{
private readonly string _source;
private readonly int _position;
private readonly int _line;
private readonly int _column;
/// <summary>
/// Gets the list of memos assigned to the <see cref="Input" /> instance.
/// </summary>
public IDictionary<object, object> Memos { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="Input" /> class.
/// </summary>
/// <param name="source">The source.</param>
public Input(string source)
: this(source, 0)
{
}
internal Input(string source, int position, int line = 1, int column = 1)
{
_source = source;
_position = position;
_line = line;
_column = column;
Memos = new Dictionary<object, object>();
}
/// <summary>
/// Advances the input.
/// </summary>
/// <returns>A new <see cref="IInput" /> that is advanced.</returns>
/// <exception cref="System.InvalidOperationException">The input is already at the end of the source.</exception>
public IInput Advance()
{
if (AtEnd)
throw new InvalidOperationException("The input is already at the end of the source.");
return new Input(_source, _position + 1, Current == '\n' ? _line + 1 : _line, Current == '\n' ? 1 : _column + 1);
}
/// <summary>
/// Gets the whole source.
/// </summary>
public string Source { get { return _source; } }
/// <summary>
/// Gets the current <see cref="System.Char" />.
/// </summary>
public char Current { get { return _source[_position]; } }
/// <summary>
/// Gets a value indicating whether the end of the source is reached.
/// </summary>
public bool AtEnd { get { return _position == _source.Length; } }
/// <summary>
/// Gets the current positon.
/// </summary>
public int Position { get { return _position; } }
/// <summary>
/// Gets the current line number.
/// </summary>
public int Line { get { return _line; } }
/// <summary>
/// Gets the current column.
/// </summary>
public int Column { get { return _column; } }
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>
/// A string that represents the current object.
/// </returns>
public override string ToString()
{
return string.Format("Line {0}, Column {1}", _line, _column);
}
/// <summary>
/// Serves as a hash function for a particular type.
/// </summary>
/// <returns>
/// A hash code for the current <see cref="Input" />.
/// </returns>
public override int GetHashCode()
{
unchecked
{
return ((_source != null ? _source.GetHashCode() : 0) * 397) ^ _position;
}
}
/// <summary>
/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="Input" />.
/// </summary>
/// <returns>
/// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="Input" />; otherwise, false.
/// </returns>
/// <param name="obj">The object to compare with the current object. </param>
public override bool Equals(object obj)
{
return Equals(obj as IInput);
}
/// <summary>
/// Indicates whether the current <see cref="Input" /> is equal to another object of the same type.
/// </summary>
/// <returns>
/// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
/// </returns>
/// <param name="other">An object to compare with this object.</param>
public bool Equals(IInput other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(_source, other.Source) && _position == other.Position;
}
/// <summary>
/// Indicates whether the left <see cref="Input" /> is equal to the right <see cref="Input" />.
/// </summary>
/// <param name="left">The left <see cref="Input" />.</param>
/// <param name="right">The right <see cref="Input" />.</param>
/// <returns>true if both objects are equal.</returns>
public static bool operator ==(Input left, Input right)
{
return Equals(left, right);
}
/// <summary>
/// Indicates whether the left <see cref="Input" /> is not equal to the right <see cref="Input" />.
/// </summary>
/// <param name="left">The left <see cref="Input" />.</param>
/// <param name="right">The right <see cref="Input" />.</param>
/// <returns>true if the objects are not equal.</returns>
public static bool operator !=(Input left, Input right)
{
return !Equals(left, right);
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
namespace Sprache
{
/// <summary>
/// Represents an optional result.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
public interface IOption<out T>
{
/// <summary>
/// Gets a value indicating whether this instance is empty.
/// </summary>
bool IsEmpty { get; }
/// <summary>
/// Gets a value indicating whether this instance is defined.
/// </summary>
bool IsDefined { get; }
/// <summary>
/// Gets the matched result or a default value.
/// </summary>
/// <returns></returns>
T GetOrDefault();
/// <summary>
/// Gets the matched result.
/// </summary>
T Get();
}
/// <summary>
/// Extensions for <see cref="IOption&lt;T&gt;"/>.
/// </summary>
public static class OptionExtensions
{
/// <summary>
/// Gets the value or else returns a default value.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="option"></param>
/// <param name="defaultValue">The default value.</param>
/// <returns></returns>
public static T GetOrElse<T>(this IOption<T> option, T defaultValue)
{
if (option == null) throw new ArgumentNullException(nameof(option));
return option.IsEmpty ? defaultValue : option.Get();
}
/// <summary>
/// Maps a function over the value or else returns an empty option.
/// </summary>
/// <typeparam name="T">The input type.</typeparam>
/// <typeparam name="U">The output type.</typeparam>
/// <param name="option">The option containing the value to apply <paramref name="map" /> to.</param>
/// <param name="map">The function to apply to the value of <paramref name="option" />.</param>
/// <returns>An options result containing the result if there was an input value.</returns>
public static IOption<U> Select<T, U>(this IOption<T> option, Func<T,U> map)
{
if (option == null) throw new ArgumentNullException(nameof(option));
return option.IsDefined ? (IOption<U>) new Some<U>(map(option.Get())) : new None<U>();
}
/// <summary>
/// Binds the value to a function with optional result and flattens the result to a single optional.
/// A result projection is applied aftherwards.
/// </summary>
/// <typeparam name="T">The input type.</typeparam>
/// <typeparam name="U">The output type of <paramref name="bind" />.</typeparam>
/// <typeparam name="V">The final output type.</typeparam>
/// <param name="option">The option containing the value to bind to.</param>
/// <param name="bind">The function that receives the input values and returns an optional value.</param>
/// <param name="project">The function that is projects the result of <paramref name="bind" />.</param>
/// <returns>An option result containing the result if there were was an input value and bind result.</returns>
public static IOption<V> SelectMany<T,U,V>(this IOption<T> option, Func<T,IOption<U>> bind, Func<T,U,V> project)
{
if (option == null) throw new ArgumentNullException(nameof(option));
if (option.IsEmpty) return new None<V>();
var t = option.Get();
return bind(t).Select(u => project(t,u));
}
/// <summary>
/// Binds the value to a function with optional result and flattens the result to a single optional.
/// </summary>
/// <typeparam name="T">The input type.</typeparam>
/// <typeparam name="U">The output type.</typeparam>
/// <param name="option">The option containing the value to bind to.</param>
/// <param name="bind">The function that receives the input values and returns an optional value.</param>
/// <returns>An option result containing the result if there were was an input value and bind result.</returns>
public static IOption<U> SelectMany<T,U>(this IOption<T> option, Func<T,IOption<U>> bind) => option.SelectMany(bind, (_,x) => x);
}
internal abstract class AbstractOption<T> : IOption<T>
{
public abstract bool IsEmpty { get; }
public bool IsDefined
{
get { return !IsEmpty; }
}
public T GetOrDefault()
{
return IsEmpty ? default(T) : Get();
}
public abstract T Get();
}
internal sealed class Some<T> : AbstractOption<T>
{
private readonly T _value;
public Some(T value)
{
_value = value;
}
public override bool IsEmpty
{
get { return false; }
}
public override T Get()
{
return _value;
}
}
internal sealed class None<T> : AbstractOption<T>
{
public override bool IsEmpty
{
get { return true; }
}
public override T Get()
{
throw new InvalidOperationException("Cannot get value from None.");
}
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using UniLinq;
namespace Sprache
{
partial class Parse
{
/// <summary>
/// Represents a text span of the matched result.
/// </summary>
/// <typeparam name="T">Type of the matched result.</typeparam>
private class TextSpan<T> : ITextSpan<T>
{
public T Value { get; set; }
public Position Start { get; set; }
public Position End { get; set; }
public int Length { get; set; }
}
/// <summary>
/// Constructs a parser that returns the <see cref="ITextSpan{T}"/> of the parsed value.
/// </summary>
/// <typeparam name="T">The result type of the given parser.</typeparam>
/// <param name="parser">The parser to wrap.</param>
/// <returns>A parser for the text span of the given parser.</returns>
public static Parser<ITextSpan<T>> Span<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var r = parser(i);
if (r.WasSuccessful)
{
var span = new TextSpan<T>
{
Value = r.Value,
Start = Position.FromInput(i),
End = Position.FromInput(r.Remainder),
Length = r.Remainder.Position - i.Position,
};
return Result.Success(span, r.Remainder);
}
return Result.Failure<ITextSpan<T>>(r.Remainder, r.Message, r.Expectations);
};
}
/// <summary>
/// Represents a commented result with its leading and trailing comments.
/// </summary>
/// <typeparam name="T">Type of the matched result.</typeparam>
private class CommentedValue<T> : ICommented<T>
{
public CommentedValue(T value)
{
LeadingComments = TrailingComments = EmptyStringList;
Value = value;
}
public CommentedValue(IEnumerable<string> leading, T value, IEnumerable<string> trailing)
{
LeadingComments = leading ?? EmptyStringList;
Value = value;
TrailingComments = trailing ?? EmptyStringList;
}
public T Value { get; }
public IEnumerable<string> LeadingComments { get; }
public IEnumerable<string> TrailingComments { get; }
}
private static readonly string[] EmptyStringList = new string[0];
private static readonly IComment DefaultCommentParser = new CommentParser();
/// <summary>
/// Constructs a parser that consumes a whitespace and all comments
/// parsed by the commentParser.AnyComment parser, but parses only one trailing
/// comment that starts exactly on the last line of the parsed value.
/// </summary>
/// <typeparam name="T">The result type of the given parser.</typeparam>
/// <param name="parser">The parser to wrap.</param>
/// <param name="commentParser">The comment parser.</param>
/// <returns>An extended Token() version of the given parser.</returns>
public static Parser<ICommented<T>> Commented<T>(this Parser<T> parser, IComment commentParser = null)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
// consume any comment supported by the comment parser
var comment = (commentParser ?? DefaultCommentParser).AnyComment;
// parses any whitespace except for the new lines
var whiteSpaceExceptForNewLine = WhiteSpace.Except(Chars("\r\n")).Many().Text();
// returns true if the second span starts on the first span's last line
bool IsSameLine(ITextSpan<T> first, ITextSpan<string> second) =>
first.End.Line == second.Start.Line;
// single comment span followed by a whitespace
var commentSpan =
from cs in comment.Span()
from ws in whiteSpaceExceptForNewLine
select cs;
// add leading and trailing comments to the parser
return
from leadingWhiteSpace in WhiteSpace.Many()
from leadingComments in comment.Token().Many()
from valueSpan in parser.Span()
from trailingWhiteSpace in whiteSpaceExceptForNewLine
from trailingPreview in commentSpan.Many().Preview()
let trailingCount = trailingPreview.GetOrElse(Enumerable.Empty<ITextSpan<string>>())
.Where(c => IsSameLine(valueSpan, c)).Count()
from trailingComments in commentSpan.Repeat(trailingCount)
select new CommentedValue<T>(leadingComments, valueSpan.Value, trailingComments.Select(c => c.Value));
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
namespace Sprache
{
partial class Parse
{
/// <summary>
/// Construct a parser that indicates that the given parser
/// is optional. The returned parser will succeed on
/// any input no matter whether the given parser
/// succeeds or not.
/// </summary>
/// <typeparam name="T">The result type of the given parser.</typeparam>
/// <param name="parser">The parser to wrap.</param>
/// <returns>An optional version of the given parser.</returns>
public static Parser<IOption<T>> Optional<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var pr = parser(i);
if (pr.WasSuccessful)
return Result.Success(new Some<T>(pr.Value), pr.Remainder);
return Result.Success(new None<T>(), i);
};
}
/// <summary>
/// Constructs the eXclusive version of the Optional{T} parser.
/// </summary>
/// <typeparam name="T">The result type of the given parser</typeparam>
/// <param name="parser">The parser to wrap</param>
/// <returns>An eXclusive optional version of the given parser.</returns>
/// <seealso cref="XOr"/>
public static Parser<IOption<T>> XOptional<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var result = parser(i);
if (result.WasSuccessful)
return Result.Success(new Some<T>(result.Value), result.Remainder);
if (result.Remainder.Equals(i))
return Result.Success(new None<T>(), i);
return Result.Failure<IOption<T>>(result.Remainder, result.Message, result.Expectations);
};
}
/// <summary>
/// Construct a parser that indicates that the given parser is optional
/// and non-consuming. The returned parser will succeed on
/// any input no matter whether the given parser succeeds or not.
/// In any case, it won't consume any input, like a positive look-ahead in regex.
/// </summary>
/// <typeparam name="T">The result type of the given parser.</typeparam>
/// <param name="parser">The parser to wrap.</param>
/// <returns>A non-consuming version of the given parser.</returns>
public static Parser<IOption<T>> Preview<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var result = parser(i);
if (result.WasSuccessful)
return Result.Success(new Some<T>(result.Value), i);
return Result.Success(new None<T>(), i);
};
}
}
}

View File

@@ -0,0 +1,27 @@

namespace Sprache
{
partial class Parse
{
/// <summary>
/// Construct a parser that will set the position to the position-aware
/// T on succsessful match.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<T> Positioned<T>(this Parser<T> parser) where T : IPositionAware<T>
{
return i =>
{
var r = parser(i);
if (r.WasSuccessful)
{
return Result.Success(r.Value.SetPos(Position.FromInput(i), r.Remainder.Position - i.Position), r.Remainder);
}
return r;
};
}
}
}

View File

@@ -0,0 +1,34 @@
namespace Sprache
{
partial class Parse
{
/// <summary>
/// \n or \r\n
/// </summary>
public static Parser<string> LineEnd =
(from r in Char('\r').Optional()
from n in Char('\n')
select r.IsDefined ? r.Get().ToString() + n : n.ToString())
.Named("LineEnd");
/// <summary>
/// line ending or end of input
/// </summary>
public static Parser<string> LineTerminator =
Return("").End()
.Or(LineEnd.End())
.Or(LineEnd)
.Named("LineTerminator");
/// <summary>
/// Parser for identifier starting with <paramref name="firstLetterParser"/> and continuing with <paramref name="tailLetterParser"/>
/// </summary>
public static Parser<string> Identifier(Parser<char> firstLetterParser, Parser<char> tailLetterParser)
{
return
from firstLetter in firstLetterParser
from tail in tailLetterParser.Many().Text()
select firstLetter + tail;
}
}
}

View File

@@ -0,0 +1,158 @@
namespace Sprache
{
using System;
using System.Collections.Generic;
using UniLinq;
partial class Parse
{
/// <summary>
///
/// </summary>
/// <param name="parser"></param>
/// <param name="delimiter"></param>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static Parser<IEnumerable<T>> DelimitedBy<T, U>(this Parser<T> parser, Parser<U> delimiter)
{
return DelimitedBy(parser, delimiter, null, null);
}
/// <summary>
///
/// </summary>
/// <param name="parser"></param>
/// <param name="delimiter"></param>
/// <param name="minimumCount"></param>
/// <param name="maximumCount"></param>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static Parser<IEnumerable<T>> DelimitedBy<T, U>(this Parser<T> parser, Parser<U> delimiter, int? minimumCount, int? maximumCount)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (delimiter == null) throw new ArgumentNullException(nameof(delimiter));
return from head in parser.Once()
from tail in
(from separator in delimiter
from item in parser
select item).Repeat(minimumCount - 1, maximumCount - 1)
select head.Concat(tail);
}
/// <summary>
/// Fails on the first itemParser failure, if it reads at least one character.
/// </summary>
/// <param name="itemParser"></param>
/// <param name="delimiter"></param>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static Parser<IEnumerable<T>> XDelimitedBy<T, U>(this Parser<T> itemParser, Parser<U> delimiter)
{
if (itemParser == null) throw new ArgumentNullException(nameof(itemParser));
if (delimiter == null) throw new ArgumentNullException(nameof(delimiter));
return from head in itemParser.Once()
from tail in
(from separator in delimiter
from item in itemParser
select item).XMany()
select head.Concat(tail);
}
/// <summary>
///
/// </summary>
/// <param name="parser"></param>
/// <param name="count"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static Parser<IEnumerable<T>> Repeat<T>(this Parser<T> parser, int count)
{
return Repeat(parser, count, count);
}
/// <summary>
///
/// </summary>
/// <param name="parser"></param>
/// <param name="minimumCount"></param>
/// <param name="maximumCount"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static Parser<IEnumerable<T>> Repeat<T>(this Parser<T> parser, int? minimumCount, int? maximumCount)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var remainder = i;
var result = new List<T>();
var count = 0;
var r = parser(remainder);
while (r.WasSuccessful && (maximumCount == null || count < maximumCount.Value))
{
count++;
result.Add(r.Value);
remainder = r.Remainder;
r = parser(remainder);
}
if (minimumCount.HasValue && count < minimumCount.Value)
{
var what = r.Remainder.AtEnd
? "end of input"
: r.Remainder.Current.ToString();
var msg = $"Unexpected '{what}'";
string exp;
if (minimumCount == maximumCount)
exp = $"'{StringExtensions.Join(", ", r.Expectations)}' {minimumCount.Value} times, but found {count}";
else if (maximumCount == null)
exp = $"'{StringExtensions.Join(", ", r.Expectations)}' minimum {minimumCount.Value} times, but found {count}";
else
exp = $"'{StringExtensions.Join(", ", r.Expectations)}' between {minimumCount.Value} and {maximumCount.Value} times, but found {count}";
return Result.Failure<IEnumerable<T>>(i, msg, new[] { exp });
}
return Result.Success<IEnumerable<T>>(result, remainder);
};
}
/// <summary>
///
/// </summary>
/// <param name="parser"></param>
/// <param name="open"></param>
/// <param name="close"></param>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <typeparam name="V"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static Parser<T> Contained<T, U, V>(this Parser<T> parser, Parser<U> open, Parser<V> close)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (open == null) throw new ArgumentNullException(nameof(open));
if (close == null) throw new ArgumentNullException(nameof(close));
return from o in open
from item in parser
from c in close
select item;
}
}
}

View File

@@ -0,0 +1,782 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using UniLinq;
namespace Sprache
{
/// <summary>
/// Parsers and combinators.
/// </summary>
public static partial class Parse
{
/// <summary>
/// TryParse a single character matching 'predicate'
/// </summary>
/// <param name="predicate"></param>
/// <param name="description"></param>
/// <returns></returns>
public static Parser<char> Char(Predicate<char> predicate, string description)
{
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
if (description == null) throw new ArgumentNullException(nameof(description));
return i =>
{
if (!i.AtEnd)
{
if (predicate(i.Current))
return Result.Success(i.Current, i.Advance());
return Result.Failure<char>(i,
$"unexpected '{i.Current}'",
new[] { description });
}
return Result.Failure<char>(i,
"Unexpected end of input reached",
new[] { description });
};
}
/// <summary>
/// Parse a single character except those matching <paramref name="predicate"/>.
/// </summary>
/// <param name="predicate">Characters not to match.</param>
/// <param name="description">Description of characters that don't match.</param>
/// <returns>A parser for characters except those matching <paramref name="predicate"/>.</returns>
public static Parser<char> CharExcept(Predicate<char> predicate, string description)
{
return Char(c => !predicate(c), "any character except " + description);
}
/// <summary>
/// Parse a single character c.
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> Char(char c)
{
return Char(ch => c == ch, char.ToString(c));
}
/// <summary>
/// Parse a single character of any in c
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> Chars(params char[] c)
{
return Char(c.Contains, StringExtensions.Join("|", c));
}
/// <summary>
/// Parse a single character of any in c
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> Chars(string c)
{
return Char(c.ToEnumerable().Contains, StringExtensions.Join("|", c.ToEnumerable()));
}
/// <summary>
/// Parse a single character except c.
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> CharExcept(char c)
{
return CharExcept(ch => c == ch, char.ToString(c));
}
/// <summary>
/// Parses a single character except for those in the given parameters
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> CharExcept(IEnumerable<char> c)
{
var chars = c as char[] ?? c.ToArray();
return CharExcept(chars.Contains, StringExtensions.Join("|", chars));
}
/// <summary>
/// Parses a single character except for those in c
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> CharExcept(string c)
{
return CharExcept(c.ToEnumerable().Contains, StringExtensions.Join("|", c.ToEnumerable()));
}
/// <summary>
/// Parse a single character in a case-insensitive fashion.
/// </summary>
/// <param name="c"></param>
/// <returns></returns>
public static Parser<char> IgnoreCase(char c)
{
return Char(ch => char.ToLower(c) == char.ToLower(ch), char.ToString(c));
}
/// <summary>
/// Parse a string in a case-insensitive fashion.
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static Parser<IEnumerable<char>> IgnoreCase(string s)
{
if (s == null) throw new ArgumentNullException(nameof(s));
return s
.ToEnumerable()
.Select(IgnoreCase)
.Aggregate(Return(Enumerable.Empty<char>()),
(a, p) => a.Concat(p.Once()))
.Named(s);
}
/// <summary>
/// Parse any character.
/// </summary>
public static readonly Parser<char> AnyChar = Char(c => true, "any character");
/// <summary>
/// Parse a whitespace.
/// </summary>
public static readonly Parser<char> WhiteSpace = Char(char.IsWhiteSpace, "whitespace");
/// <summary>
/// Parse a digit.
/// </summary>
public static readonly Parser<char> Digit = Char(char.IsDigit, "digit");
/// <summary>
/// Parse a letter.
/// </summary>
public static readonly Parser<char> Letter = Char(char.IsLetter, "letter");
/// <summary>
/// Parse a letter or digit.
/// </summary>
public static readonly Parser<char> LetterOrDigit = Char(char.IsLetterOrDigit, "letter or digit");
/// <summary>
/// Parse a lowercase letter.
/// </summary>
public static readonly Parser<char> Lower = Char(char.IsLower, "lowercase letter");
/// <summary>
/// Parse an uppercase letter.
/// </summary>
public static readonly Parser<char> Upper = Char(char.IsUpper, "uppercase letter");
/// <summary>
/// Parse a numeric character.
/// </summary>
public static readonly Parser<char> Numeric = Char(char.IsNumber, "numeric character");
/// <summary>
/// Parse a string of characters.
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static Parser<IEnumerable<char>> String(string s)
{
if (s == null) throw new ArgumentNullException(nameof(s));
return s
.ToEnumerable()
.Select(Char)
.Aggregate(Return(Enumerable.Empty<char>()),
(a, p) => a.Concat(p.Once()))
.Named(s);
}
/// <summary>
/// Constructs a parser that will fail if the given parser succeeds,
/// and will succeed if the given parser fails. In any case, it won't
/// consume any input. It's like a negative look-ahead in regex.
/// </summary>
/// <typeparam name="T">The result type of the given parser</typeparam>
/// <param name="parser">The parser to wrap</param>
/// <returns>A parser that is the opposite of the given parser.</returns>
public static Parser<object> Not<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var result = parser(i);
if (result.WasSuccessful)
{
var msg = $"`{StringExtensions.Join(", ", result.Expectations)}' was not expected";
return Result.Failure<object>(i, msg, new string[0]);
}
return Result.Success<object>(null, i);
};
}
/// <summary>
/// Parse first, and if successful, then parse second.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns></returns>
public static Parser<U> Then<T, U>(this Parser<T> first, Func<T, Parser<U>> second)
{
if (first == null) throw new ArgumentNullException(nameof(first));
if (second == null) throw new ArgumentNullException(nameof(second));
return i => first(i).IfSuccess(s => second(s.Value)(s.Remainder));
}
/// <summary>
/// Parse a stream of elements.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
/// <remarks>Implemented imperatively to decrease stack usage.</remarks>
public static Parser<IEnumerable<T>> Many<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i =>
{
var remainder = i;
var result = new List<T>();
var r = parser(i);
while (r.WasSuccessful)
{
if (remainder.Equals(r.Remainder))
break;
result.Add(r.Value);
remainder = r.Remainder;
r = parser(remainder);
}
return Result.Success<IEnumerable<T>>(result, remainder);
};
}
/// <summary>
/// Parse a stream of elements, failing if any element is only partially parsed.
/// </summary>
/// <typeparam name="T">The type of element to parse.</typeparam>
/// <param name="parser">A parser that matches a single element.</param>
/// <returns>A <see cref="Parser{T}"/> that matches the sequence.</returns>
/// <remarks>
/// <para>
/// Using <seealso cref="XMany{T}(Parser{T})"/> may be preferable to <seealso cref="Many{T}(Parser{T})"/>
/// where the first character of each match identified by <paramref name="parser"/>
/// is sufficient to determine whether the entire match should succeed. The X*
/// methods typically give more helpful errors and are easier to debug than their
/// unqualified counterparts.
/// </para>
/// </remarks>
/// <seealso cref="XOr"/>
public static Parser<IEnumerable<T>> XMany<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return parser.Many().Then(m => parser.Once().XOr(Return(m)));
}
/// <summary>
/// TryParse a stream of elements with at least one item.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<IEnumerable<T>> AtLeastOnce<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return parser.Once().Then(t1 => parser.Many().Select(ts => t1.Concat(ts)));
}
/// <summary>
/// TryParse a stream of elements with at least one item. Except the first
/// item, all other items will be matched with the <code>XMany</code> operator.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<IEnumerable<T>> XAtLeastOnce<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return parser.Once().Then(t1 => parser.XMany().Select(ts => t1.Concat(ts)));
}
/// <summary>
/// Parse end-of-input.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<T> End<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return i => parser(i).IfSuccess(s =>
s.Remainder.AtEnd
? s
: Result.Failure<T>(
s.Remainder,
string.Format("unexpected '{0}'", s.Remainder.Current),
new[] { "end of input" }));
}
/// <summary>
/// Take the result of parsing, and project it onto a different domain.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="parser"></param>
/// <param name="convert"></param>
/// <returns></returns>
public static Parser<U> Select<T, U>(this Parser<T> parser, Func<T, U> convert)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (convert == null) throw new ArgumentNullException(nameof(convert));
return parser.Then(t => Return(convert(t)));
}
/// <summary>
/// Parse the token, embedded in any amount of whitespace characters.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<T> Token<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return from leading in WhiteSpace.Many()
from item in parser
from trailing in WhiteSpace.Many()
select item;
}
/// <summary>
/// Refer to another parser indirectly. This allows circular compile-time dependency between parsers.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reference"></param>
/// <returns></returns>
public static Parser<T> Ref<T>(Func<Parser<T>> reference)
{
if (reference == null) throw new ArgumentNullException(nameof(reference));
Parser<T> p = null;
return i =>
{
if (p == null)
p = reference();
if (i.Memos.ContainsKey(p))
{
var pResult = i.Memos[p] as IResult<T>;
if (pResult.WasSuccessful)
return pResult;
throw new ParseException(pResult.ToString());
}
i.Memos[p] = Result.Failure<T>(i,
"Left recursion in the grammar.",
new string[0]);
var result = p(i);
i.Memos[p] = result;
return result;
};
}
/// <summary>
/// Convert a stream of characters to a string.
/// </summary>
/// <param name="characters"></param>
/// <returns></returns>
public static Parser<string> Text(this Parser<IEnumerable<char>> characters)
{
return characters.Select(chs => new string(chs.ToArray()));
}
/// <summary>
/// Parse first, if it succeeds, return first, otherwise try second.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns></returns>
public static Parser<T> Or<T>(this Parser<T> first, Parser<T> second)
{
if (first == null) throw new ArgumentNullException(nameof(first));
if (second == null) throw new ArgumentNullException(nameof(second));
return i =>
{
var fr = first(i);
if (!fr.WasSuccessful)
{
return second(i).IfFailure(sf => DetermineBestError(fr, sf));
}
if (fr.Remainder.Equals(i))
return second(i).IfFailure(sf => fr);
return fr;
};
}
/// <summary>
/// Names part of the grammar for help with error messages.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <param name="name"></param>
/// <returns></returns>
public static Parser<T> Named<T>(this Parser<T> parser, string name)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (name == null) throw new ArgumentNullException(nameof(name));
return i => parser(i).IfFailure(f => f.Remainder.Equals(i) ?
Result.Failure<T>(f.Remainder, f.Message, new[] { name }) :
f);
}
/// <summary>
/// Parse first, if it succeeds, return first, otherwise try second.
/// Assumes that the first parsed character will determine the parser chosen (see Try).
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns></returns>
public static Parser<T> XOr<T>(this Parser<T> first, Parser<T> second)
{
if (first == null) throw new ArgumentNullException(nameof(first));
if (second == null) throw new ArgumentNullException(nameof(second));
return i => {
var fr = first(i);
if (!fr.WasSuccessful)
{
// The 'X' part
if (!fr.Remainder.Equals(i))
return fr;
return second(i).IfFailure(sf => DetermineBestError(fr, sf));
}
// This handles a zero-length successful application of first.
if (fr.Remainder.Equals(i))
return second(i).IfFailure(sf => fr);
return fr;
};
}
// Examines two results presumably obtained at an "Or" junction; returns the result with
// the most information, or if they apply at the same input position, a union of the results.
static IResult<T> DetermineBestError<T>(IResult<T> firstFailure, IResult<T> secondFailure)
{
if (secondFailure.Remainder.Position > firstFailure.Remainder.Position)
return secondFailure;
if (secondFailure.Remainder.Position == firstFailure.Remainder.Position)
return Result.Failure<T>(
firstFailure.Remainder,
firstFailure.Message,
firstFailure.Expectations.Union(secondFailure.Expectations));
return firstFailure;
}
/// <summary>
/// Parse a stream of elements containing only one item.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<IEnumerable<T>> Once<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return parser.Select(r => (IEnumerable<T>)new[] { r });
}
/// <summary>
/// Concatenate two streams of elements.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns></returns>
public static Parser<IEnumerable<T>> Concat<T>(this Parser<IEnumerable<T>> first, Parser<IEnumerable<T>> second)
{
if (first == null) throw new ArgumentNullException(nameof(first));
if (second == null) throw new ArgumentNullException(nameof(second));
return first.Then(f => second.Select(f.Concat));
}
/// <summary>
/// Succeed immediately and return value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <returns></returns>
public static Parser<T> Return<T>(T value)
{
return i => Result.Success(value, i);
}
/// <summary>
/// Version of Return with simpler inline syntax.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="parser"></param>
/// <param name="value"></param>
/// <returns></returns>
public static Parser<U> Return<T, U>(this Parser<T> parser, U value)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
return parser.Select(t => value);
}
/// <summary>
/// Attempt parsing only if the <paramref name="except"/> parser fails.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="parser"></param>
/// <param name="except"></param>
/// <returns></returns>
public static Parser<T> Except<T, U>(this Parser<T> parser, Parser<U> except)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (except == null) throw new ArgumentNullException(nameof(except));
// Could be more like: except.Then(s => s.Fail("..")).XOr(parser)
return i =>
{
var r = except(i);
if (r.WasSuccessful)
return Result.Failure<T>(i, "Excepted parser succeeded.", new[] { "other than the excepted input" });
return parser(i);
};
}
/// <summary>
/// Parse a sequence of items until a terminator is reached.
/// Returns the sequence, discarding the terminator.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="parser"></param>
/// <param name="until"></param>
/// <returns></returns>
public static Parser<IEnumerable<T>> Until<T, U>(this Parser<T> parser, Parser<U> until)
{
return parser.Except(until).Many().Then(r => until.Return(r));
}
/// <summary>
/// Succeed if the parsed value matches predicate.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <param name="predicate"></param>
/// <returns></returns>
public static Parser<T> Where<T>(this Parser<T> parser, Func<T, bool> predicate)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
return i => parser(i).IfSuccess(s =>
predicate(s.Value) ? s : Result.Failure<T>(i,
string.Format("Unexpected {0}.", s.Value),
new string[0]));
}
/// <summary>
/// Monadic combinator Then, adapted for Linq comprehension syntax.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <typeparam name="V"></typeparam>
/// <param name="parser"></param>
/// <param name="selector"></param>
/// <param name="projector"></param>
/// <returns></returns>
public static Parser<V> SelectMany<T, U, V>(
this Parser<T> parser,
Func<T, Parser<U>> selector,
Func<T, U, V> projector)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (selector == null) throw new ArgumentNullException(nameof(selector));
if (projector == null) throw new ArgumentNullException(nameof(projector));
return parser.Then(t => selector(t).Select(u => projector(t, u)));
}
/// <summary>
/// Chain a left-associative operator.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TOp"></typeparam>
/// <param name="op"></param>
/// <param name="operand"></param>
/// <param name="apply"></param>
/// <returns></returns>
public static Parser<T> ChainOperator<T, TOp>(
Parser<TOp> op,
Parser<T> operand,
Func<TOp, T, T, T> apply)
{
if (op == null) throw new ArgumentNullException(nameof(op));
if (operand == null) throw new ArgumentNullException(nameof(operand));
if (apply == null) throw new ArgumentNullException(nameof(apply));
return operand.Then(first => ChainOperatorRest(first, op, operand, apply, Or));
}
/// <summary>
/// Chain a left-associative operator.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TOp"></typeparam>
/// <param name="op"></param>
/// <param name="operand"></param>
/// <param name="apply"></param>
/// <returns></returns>
public static Parser<T> XChainOperator<T, TOp>(
Parser<TOp> op,
Parser<T> operand,
Func<TOp, T, T, T> apply)
{
if (op == null) throw new ArgumentNullException(nameof(op));
if (operand == null) throw new ArgumentNullException(nameof(operand));
if (apply == null) throw new ArgumentNullException(nameof(apply));
return operand.Then(first => ChainOperatorRest(first, op, operand, apply, XOr));
}
static Parser<T> ChainOperatorRest<T, TOp>(
T firstOperand,
Parser<TOp> op,
Parser<T> operand,
Func<TOp, T, T, T> apply,
Func<Parser<T>, Parser<T>, Parser<T>> or)
{
if (op == null) throw new ArgumentNullException(nameof(op));
if (operand == null) throw new ArgumentNullException(nameof(operand));
if (apply == null) throw new ArgumentNullException(nameof(apply));
return or(op.Then(opvalue =>
operand.Then(operandValue =>
ChainOperatorRest(apply(opvalue, firstOperand, operandValue), op, operand, apply, or))),
Return(firstOperand));
}
/// <summary>
/// Chain a right-associative operator.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TOp"></typeparam>
/// <param name="op"></param>
/// <param name="operand"></param>
/// <param name="apply"></param>
/// <returns></returns>
public static Parser<T> ChainRightOperator<T, TOp>(
Parser<TOp> op,
Parser<T> operand,
Func<TOp, T, T, T> apply)
{
if (op == null) throw new ArgumentNullException(nameof(op));
if (operand == null) throw new ArgumentNullException(nameof(operand));
if (apply == null) throw new ArgumentNullException(nameof(apply));
return operand.Then(first => ChainRightOperatorRest(first, op, operand, apply, Or));
}
/// <summary>
/// Chain a right-associative operator.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TOp"></typeparam>
/// <param name="op"></param>
/// <param name="operand"></param>
/// <param name="apply"></param>
/// <returns></returns>
public static Parser<T> XChainRightOperator<T, TOp>(
Parser<TOp> op,
Parser<T> operand,
Func<TOp, T, T, T> apply)
{
if (op == null) throw new ArgumentNullException(nameof(op));
if (operand == null) throw new ArgumentNullException(nameof(operand));
if (apply == null) throw new ArgumentNullException(nameof(apply));
return operand.Then(first => ChainRightOperatorRest(first, op, operand, apply, XOr));
}
static Parser<T> ChainRightOperatorRest<T, TOp>(
T lastOperand,
Parser<TOp> op,
Parser<T> operand,
Func<TOp, T, T, T> apply,
Func<Parser<T>, Parser<T>, Parser<T>> or)
{
if (op == null) throw new ArgumentNullException(nameof(op));
if (operand == null) throw new ArgumentNullException(nameof(operand));
if (apply == null) throw new ArgumentNullException(nameof(apply));
return or(op.Then(opvalue =>
operand.Then(operandValue =>
ChainRightOperatorRest(operandValue, op, operand, apply, or)).Then(r =>
Return(apply(opvalue, lastOperand, r)))),
Return(lastOperand));
}
/// <summary>
/// Parse a number.
/// </summary>
public static readonly Parser<string> Number = Numeric.AtLeastOnce().Text();
static Parser<string> DecimalWithoutLeadingDigits(CultureInfo ci = null)
{
return from nothing in Return("")
// dummy so that CultureInfo.CurrentCulture is evaluated later
from dot in String((ci ?? CultureInfo.CurrentCulture).NumberFormat.NumberDecimalSeparator).Text()
from fraction in Number
select dot + fraction;
}
static Parser<string> DecimalWithLeadingDigits(CultureInfo ci = null)
{
return Number.Then(n => DecimalWithoutLeadingDigits(ci).XOr(Return("")).Select(f => n + f));
}
/// <summary>
/// Parse a decimal number using the current culture's separator character.
/// </summary>
public static readonly Parser<string> Decimal = DecimalWithLeadingDigits().XOr(DecimalWithoutLeadingDigits());
/// <summary>
/// Parse a decimal number with separator '.'.
/// </summary>
public static readonly Parser<string> DecimalInvariant = DecimalWithLeadingDigits(CultureInfo.InvariantCulture)
.XOr(DecimalWithoutLeadingDigits(CultureInfo.InvariantCulture));
}
}

View File

@@ -0,0 +1,50 @@
using System;
namespace Sprache
{
/// <summary>
/// Represents an error that occurs during parsing.
/// </summary>
public class ParseException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ParseException" /> class.
/// </summary>
public ParseException() { }
/// <summary>
/// Initializes a new instance of the <see cref="ParseException" /> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public ParseException(string message) : base(message) { }
/// <summary>
/// Initializes a new instance of the <see cref="ParseException" /> class with a specified error message
/// and the position where the error occured.
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="position">The position where the error occured.</param>
public ParseException(string message, Position position) : base(message)
{
if (position == null) throw new ArgumentNullException(nameof(position));
Position = position;
}
/// <summary>
/// Initializes a new instance of the <see cref="ParseException" /> class with a specified error message
/// and a reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception,
/// or a null reference (Nothing in Visual Basic) if no inner exception is specified.</param>
public ParseException(string message, Exception innerException) : base(message, innerException) { }
/// <summary>
/// Gets the position of the parsing failure if one is available; otherwise, null.
/// </summary>
public Position Position {
get;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
namespace Sprache
{
/// <summary>
/// Represents a parser.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="input">The input to parse.</param>
/// <returns>The result of the parser.</returns>
public delegate IResult<T> Parser<out T>(IInput input);
/// <summary>
/// Contains some extension methods for <see cref="Parser&lt;T&gt;" />.
/// </summary>
public static class ParserExtensions
{
/// <summary>
/// Tries to parse the input without throwing an exception.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="parser">The parser.</param>
/// <param name="input">The input.</param>
/// <returns>The result of the parser</returns>
public static IResult<T> TryParse<T>(this Parser<T> parser, string input)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (input == null) throw new ArgumentNullException(nameof(input));
return parser(new Input(input));
}
/// <summary>
/// Parses the specified input string.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="parser">The parser.</param>
/// <param name="input">The input.</param>
/// <returns>The result of the parser.</returns>
/// <exception cref="Sprache.ParseException">It contains the details of the parsing error.</exception>
public static T Parse<T>(this Parser<T> parser, string input)
{
if (parser == null) throw new ArgumentNullException(nameof(parser));
if (input == null) throw new ArgumentNullException(nameof(input));
var result = parser.TryParse(input);
if(result.WasSuccessful)
return result.Value;
throw new ParseException(result.ToString(), Position.FromInput(result.Remainder));
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
namespace Sprache
{
/// <summary>
/// Represents a position in the input.
/// </summary>
public class Position : IEquatable<Position>
{
/// <summary>
/// Initializes a new instance of the <see cref="Position" /> class.
/// </summary>
/// <param name="pos">The position.</param>
/// <param name="line">The line number.</param>
/// <param name="column">The column.</param>
public Position(int pos, int line, int column)
{
Pos = pos;
Line = line;
Column = column;
}
/// <summary>
/// Creates an new <see cref="Position"/> instance from a given <see cref="IInput"/> object.
/// </summary>
/// <param name="input">The current input.</param>
/// <returns>A new <see cref="Position"/> instance.</returns>
public static Position FromInput(IInput input)
{
return new Position(input.Position, input.Line, input.Column);
}
/// <summary>
/// Gets the current positon.
/// </summary>
public int Pos
{
get;
private set;
}
/// <summary>
/// Gets the current line number.
/// </summary>
public int Line
{
get;
private set;
}
/// <summary>
/// Gets the current column.
/// </summary>
public int Column
{
get;
private set;
}
/// <summary>
/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="Position" />.
/// </summary>
/// <returns>
/// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="Position" />; otherwise, false.
/// </returns>
/// <param name="obj">The object to compare with the current object. </param>
public override bool Equals(object obj)
{
return Equals(obj as Position);
}
/// <summary>
/// Indicates whether the current <see cref="Position" /> is equal to another object of the same type.
/// </summary>
/// <returns>
/// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
/// </returns>
/// <param name="other">An object to compare with this object.</param>
public bool Equals(Position other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Pos == other.Pos
&& Line == other.Line
&& Column == other.Column;
}
/// <summary>
/// Indicates whether the left <see cref="Position" /> is equal to the right <see cref="Position" />.
/// </summary>
/// <param name="left">The left <see cref="Position" />.</param>
/// <param name="right">The right <see cref="Position" />.</param>
/// <returns>true if both objects are equal.</returns>
public static bool operator ==(Position left, Position right)
{
return Equals(left, right);
}
/// <summary>
/// Indicates whether the left <see cref="Position" /> is not equal to the right <see cref="Position" />.
/// </summary>
/// <param name="left">The left <see cref="Position" />.</param>
/// <param name="right">The right <see cref="Position" />.</param>
/// <returns>true if the objects are not equal.</returns>
public static bool operator !=(Position left, Position right)
{
return !Equals(left, right);
}
/// <summary>
/// Serves as a hash function for a particular type.
/// </summary>
/// <returns>
/// A hash code for the current <see cref="Position" />.
/// </returns>
public override int GetHashCode()
{
var h = 31;
h = h * 13 + Pos;
h = h * 13 + Line;
h = h * 13 + Column;
return h;
}
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>
/// A string that represents the current object.
/// </returns>
public override string ToString()
{
return string.Format("Line {0}, Column {1}", Line, Column);
}
}
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using UniLinq;
namespace Sprache
{
/// <summary>
/// Contains helper functions to create <see cref="IResult&lt;T&gt;"/> instances.
/// </summary>
public static class Result
{
/// <summary>
/// Creates a success result.
/// </summary>
/// <typeparam name="T">The type of the result (value).</typeparam>
/// <param name="value">The sucessfully parsed value.</param>
/// <param name="remainder">The remainder of the input.</param>
/// <returns>The new <see cref="IResult&lt;T&gt;"/>.</returns>
public static IResult<T> Success<T>(T value, IInput remainder)
{
return new Result<T>(value, remainder);
}
/// <summary>
/// Creates a failure result.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="remainder">The remainder of the input.</param>
/// <param name="message">The error message.</param>
/// <param name="expectations">The parser expectations.</param>
/// <returns>The new <see cref="IResult&lt;T&gt;"/>.</returns>
public static IResult<T> Failure<T>(IInput remainder, string message, IEnumerable<string> expectations)
{
return new Result<T>(remainder, message, expectations);
}
}
internal class Result<T> : IResult<T>
{
private readonly T _value;
private readonly IInput _remainder;
private readonly bool _wasSuccessful;
private readonly string _message;
private readonly IEnumerable<string> _expectations;
public Result(T value, IInput remainder)
{
_value = value;
_remainder = remainder;
_wasSuccessful = true;
_message = null;
_expectations = Enumerable.Empty<string>();
}
public Result(IInput remainder, string message, IEnumerable<string> expectations)
{
_value = default(T);
_remainder = remainder;
_wasSuccessful = false;
_message = message;
_expectations = expectations;
}
public T Value
{
get
{
if (!WasSuccessful)
throw new InvalidOperationException("No value can be computed.");
return _value;
}
}
public bool WasSuccessful { get { return _wasSuccessful; } }
public string Message { get { return _message; } }
public IEnumerable<string> Expectations { get { return _expectations; } }
public IInput Remainder { get { return _remainder; } }
public override string ToString()
{
if (WasSuccessful)
return string.Format("Successful parsing of {0}.", Value);
var expMsg = "";
if (Expectations.Any())
expMsg = " expected " + Expectations.Aggregate((e1, e2) => e1 + " or " + e2);
var recentlyConsumed = CalculateRecentlyConsumed();
return string.Format("Parsing failure: {0};{1} ({2}); recently consumed: {3}", Message, expMsg, Remainder, recentlyConsumed);
}
private string CalculateRecentlyConsumed()
{
const int windowSize = 10;
var totalConsumedChars = Remainder.Position;
var windowStart = totalConsumedChars - windowSize;
windowStart = windowStart < 0 ? 0 : windowStart;
var numberOfRecentlyConsumedChars = totalConsumedChars - windowStart;
return Remainder.Source.Substring(windowStart, numberOfRecentlyConsumedChars);
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace Sprache
{
internal static class ResultHelper
{
public static IResult<U> IfSuccess<T, U>(this IResult<T> result, Func<IResult<T>, IResult<U>> next)
{
if(result == null) throw new ArgumentNullException(nameof(result));
if (result.WasSuccessful)
return next(result);
return Result.Failure<U>(result.Remainder, result.Message, result.Expectations);
}
public static IResult<T> IfFailure<T>(this IResult<T> result, Func<IResult<T>, IResult<T>> next)
{
if (result == null) throw new ArgumentNullException(nameof(result));
return result.WasSuccessful
? result
: next(result);
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using UniLinq;
namespace Sprache
{
internal static class StringExtensions
{
public static IEnumerable<char> ToEnumerable(this string @this)
{
#if STRING_IS_ENUMERABLE
return @this;
#else
if (@this == null) throw new ArgumentNullException(nameof(@this));
for (var i = 0; i < @this.Length; ++i)
{
yield return @this[i];
}
#endif
}
public static string Join<T>(string separator, IEnumerable<T> values)
{
#if STRING_JOIN_ENUMERABLE
return string.Join(separator, values);
#else
return string.Join(separator, values.Select(v => v.ToString()).ToArray());
#endif
}
}
}

View File

@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2011 Nicholas Blumhardt
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.

View File

@@ -0,0 +1,19 @@
using KFCommonUtilityLib.Scripts.StaticManagers;
using Mono.Cecil.Cil;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KFCommonUtilityLib
{
public interface IModuleProcessor
{
void InitModules(ModuleManipulator manipulator, Type targetType, Type baseType, params Type[] moduleTypes);
bool BuildConstructor(ModuleManipulator manipulator, MethodDefinition mtddef_ctor);
Type GetModuleTypeByName(string name);
bool MatchSpecialArgs(ParameterDefinition par, MethodDefinition mtddef_target, MethodPatchInfo mtdpinf_derived, int moduleIndex, List<Instruction> list_inst_pars, ILProcessor il);
}
}

View File

@@ -0,0 +1,80 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
namespace KFCommonUtilityLib
{
public class ItemActionDataModuleProcessor : IModuleProcessor
{
TypeDefinition typedef_newAction;
Type[] arr_type_actions;
FieldDefinition[] arr_flddef_actions;
bool[] arr_hasdata;
FieldDefinition[] arr_flddef_actiondatas;
public ItemActionDataModuleProcessor(TypeDefinition typedef_newAction, Type[] arr_type_actions, FieldDefinition[] arr_flddef_actions, bool[] arr_hasdata, out FieldDefinition[] arr_flddef_actiondatas)
{
this.typedef_newAction = typedef_newAction;
this.arr_type_actions = arr_type_actions;
this.arr_flddef_actions = arr_flddef_actions;
this.arr_hasdata = arr_hasdata;
this.arr_flddef_actiondatas = new FieldDefinition[arr_type_actions.Length];
arr_flddef_actiondatas = this.arr_flddef_actiondatas;
}
public bool BuildConstructor(ModuleManipulator manipulator, MethodDefinition mtddef_ctor)
{
mtddef_ctor.Parameters.Add(new ParameterDefinition("_inventoryData", Mono.Cecil.ParameterAttributes.None, manipulator.module.ImportReference(typeof(ItemInventoryData))));
mtddef_ctor.Parameters.Add(new ParameterDefinition("_indexInEntityOfAction", Mono.Cecil.ParameterAttributes.None, manipulator.module.TypeSystem.Int32));
FieldReference fldref_invdata_item = manipulator.module.ImportReference(typeof(ItemInventoryData).GetField(nameof(ItemInventoryData.item)));
FieldReference fldref_item_actions = manipulator.module.ImportReference(typeof(ItemClass).GetField(nameof(ItemClass.Actions)));
var il = mtddef_ctor.Body.GetILProcessor();
il.Append(il.Create(OpCodes.Ldarg_0));
il.Append(il.Create(OpCodes.Ldarg_1));
il.Append(il.Create(OpCodes.Ldarg_2));
il.Append(il.Create(OpCodes.Call, manipulator.module.ImportReference(manipulator.targetType.GetConstructor(new Type[] { typeof(ItemInventoryData), typeof(int) }))));
il.Append(il.Create(OpCodes.Nop));
for (int i = 0, j = 0; i < arr_type_actions.Length; i++)
{
if (!arr_hasdata[i])
{
arr_flddef_actiondatas[i] = null;
continue;
}
arr_flddef_actiondatas[i] = manipulator.arr_flddef_modules[j];
il.Append(il.Create(OpCodes.Ldarg_0));
il.Append(il.Create(OpCodes.Ldarg_1));
il.Append(il.Create(OpCodes.Ldarg_2));
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldfld, fldref_invdata_item);
il.Emit(OpCodes.Ldfld, fldref_item_actions);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Castclass, typedef_newAction);
il.Emit(OpCodes.Ldfld, arr_flddef_actions[i]);
Log.Out($"data module {j} {manipulator.moduleTypes[j].FullName} action module {i} {arr_type_actions[i].FullName}");
il.Append(il.Create(OpCodes.Newobj, manipulator.module.ImportReference(manipulator.moduleTypes[j].GetConstructor(new Type[] { typeof(ItemInventoryData), typeof(int), arr_type_actions[i] }))));
il.Append(il.Create(OpCodes.Stfld, manipulator.arr_flddef_modules[j]));
il.Append(il.Create(OpCodes.Nop));
j++;
}
il.Append(il.Create(OpCodes.Ret));
return true;
}
public Type GetModuleTypeByName(string name)
{
throw new NotImplementedException();
}
public void InitModules(ModuleManipulator manipulator, Type targetType, Type baseType, params Type[] moduleTypes)
{
}
public bool MatchSpecialArgs(ParameterDefinition par, MethodDefinition mtddef_target, MethodPatchInfo mtdpinf_derived, int moduleIndex, List<Instruction> list_inst_pars, ILProcessor il)
{
return false;
}
}
}

View File

@@ -0,0 +1,104 @@
using KFCommonUtilityLib.Scripts.Attributes;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UniLinq;
namespace KFCommonUtilityLib
{
//public static class AssemblyLocator
//{
// private static Dictionary<string, Assembly> assemblies;
// public static void Init()
// {
// assemblies = new Dictionary<string, Assembly>();
// foreach (var assembly in ModManager.GetLoadedAssemblies())
// {
// assemblies.Add(assembly.FullName, assembly);
// }
// AppDomain.CurrentDomain.AssemblyLoad += new AssemblyLoadEventHandler(CurrentDomain_AssemblyLoad);
// AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
// }
// private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
// {
// assemblies.TryGetValue(args.Name, out Assembly assembly);
// if (assembly != null)
// Log.Out($"RESOLVING ASSEMBLY {assembly.FullName}");
// else
// Log.Error($"RESOLVING ASSEMBBLY {args.Name} FAILED!");
// return assembly;
// }
// private static void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
// {
// Assembly assembly = args.LoadedAssembly;
// assemblies[assembly.FullName] = assembly;
// Log.Out($"LOADING ASSEMBLY {assembly.FullName}");
// }
//}
public static class ItemActionModuleManager
{
private static readonly Dictionary<string, List<(string typename, int indexOfAction)>> dict_replacement_mapping = new Dictionary<string, List<(string typename, int indexOfAction)>>();
internal static void Init()
{
ModuleManagers.OnAssemblyCreated += static () => dict_replacement_mapping.Clear();
ModuleManagers.OnAssemblyLoaded += static () =>
{
//replace item actions
foreach (var pair in dict_replacement_mapping)
{
ItemClass item = ItemClass.GetItemClass(pair.Key, true);
foreach ((string typename, int indexOfAction) in pair.Value)
if (ModuleManagers.TryFindType(typename, out Type itemActionType))
{
//Log.Out($"Replace ItemAction {item.Actions[indexOfAction].GetType().FullName} with {itemActionType.FullName}");
ItemAction itemActionPrev = item.Actions[indexOfAction];
item.Actions[indexOfAction] = (ItemAction)Activator.CreateInstance(itemActionType);
item.Actions[indexOfAction].ActionIndex = indexOfAction;
item.Actions[indexOfAction].item = item;
item.Actions[indexOfAction].ExecutionRequirements = itemActionPrev.ExecutionRequirements;
item.Actions[indexOfAction].ReadFrom(itemActionPrev.Properties);
}
}
dict_replacement_mapping.Clear();
};
}
internal static void CheckItem(ItemClass item)
{
if (!ModuleManagers.Inited)
{
return;
}
for (int i = 0; i < item.Actions.Length; i++)
{
ItemAction itemAction = item.Actions[i];
if (itemAction != null && itemAction.Properties.Values.TryGetValue("ItemActionModules", out string str_modules))
{
try
{
if (ModuleManagers.PatchType<ItemActionModuleProcessor>(itemAction.GetType(), typeof(ItemAction), str_modules, out string typename))
{
if (!dict_replacement_mapping.TryGetValue(item.Name, out var list))
{
list = new List<(string typename, int indexOfAction)>();
dict_replacement_mapping.Add(item.Name, list);
}
list.Add((typename, i));
}
}
catch(Exception e)
{
Log.Error($"Error parsing ItemActionModules for {item.Name} action{i}:\n{e}");
continue;
}
}
}
}
}
}

View File

@@ -0,0 +1,157 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using UniLinq;
using System.Reflection;
using UnityEngine.Scripting;
using FieldAttributes = Mono.Cecil.FieldAttributes;
using MethodAttributes = Mono.Cecil.MethodAttributes;
using TypeAttributes = Mono.Cecil.TypeAttributes;
using KFCommonUtilityLib.Scripts.Attributes;
using Mono.Cecil.Rocks;
namespace KFCommonUtilityLib
{
public class ItemActionModuleProcessor : IModuleProcessor
{
TypeDefinition typedef_newActionData;
FieldDefinition[] arr_flddef_data;
public Type GetModuleTypeByName(string name)
{
return ReflectionHelpers.GetTypeWithPrefix("ActionModule", name);
}
public bool BuildConstructor(ModuleManipulator manipulator, MethodDefinition mtddef_ctor)
{
return false;
}
public void InitModules(ModuleManipulator manipulator, Type targetType, Type baseType, params Type[] moduleTypes)
{
ModuleDefinition module = manipulator.module;
//Find ItemActionData subtype
MethodInfo mtdinf_create_data = null;
{
Type type_itemActionBase = targetType;
while (baseType.IsAssignableFrom(type_itemActionBase))
{
mtdinf_create_data = type_itemActionBase.GetMethod(nameof(ItemAction.CreateModifierData), BindingFlags.Public | BindingFlags.Instance);
if (mtdinf_create_data != null)
break;
mtdinf_create_data = mtdinf_create_data.GetBaseDefinition();
}
}
//ACTION MODULE DATA TYPES
var arr_type_data = moduleTypes.Select(m => m.GetCustomAttribute<ActionDataTargetAttribute>()?.DataType).ToArray();
//Create new ItemActionData
//Find CreateModifierData
MethodDefinition mtddef_create_data = module.ImportReference(mtdinf_create_data).Resolve();
//ItemActionData subtype is the return type of CreateModifierData
TypeReference typeref_actiondata = ((MethodReference)mtddef_create_data.Body.Instructions[mtddef_create_data.Body.Instructions.Count - 2].Operand).DeclaringType;
//Get type by assembly qualified name since it might be from mod assembly
Type type_itemActionData = Type.GetType(Assembly.CreateQualifiedName(typeref_actiondata.Module.Assembly.Name.Name, typeref_actiondata.FullName));
MethodReference mtdref_data_ctor;
if (ModuleManagers.PatchType(type_itemActionData, typeof(ItemActionData), arr_type_data.Where(t => t != null).ToArray(), new ItemActionDataModuleProcessor(manipulator.typedef_newTarget, moduleTypes, manipulator.arr_flddef_modules, arr_type_data.Select(t => t != null).ToArray(), out arr_flddef_data), out var str_data_type_name) && ModuleManagers.TryFindInCur(str_data_type_name, out typedef_newActionData))
{
module.Types.Remove(typedef_newActionData);
manipulator.typedef_newTarget.NestedTypes.Add(typedef_newActionData);
mtdref_data_ctor = typedef_newActionData.GetConstructors().FirstOrDefault();
}
else
{
mtdref_data_ctor = module.ImportReference(type_itemActionData.GetConstructor(new Type[] { typeof(ItemInventoryData), typeof(int) }));
}
//typedef_newActionData = new TypeDefinition(null, ModuleUtils.CreateTypeName(type_itemActionData, arr_type_data), TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.NestedPublic | TypeAttributes.Sealed, module.ImportReference(typeref_actiondata));
//typedef_newActionData.CustomAttributes.Add(new CustomAttribute(module.ImportReference(typeof(PreserveAttribute).GetConstructor(Array.Empty<Type>()))));
//manipulator.typedef_newTarget.NestedTypes.Add(typedef_newActionData);
////Create ItemActionData field
//arr_flddef_data = new FieldDefinition[moduleTypes.Length];
//for (int i = 0; i < moduleTypes.Length; i++)
//{
// if (arr_type_data[i] != null)
// {
// Type type_data = arr_type_data[i];
// manipulator.MakeContainerFor(typedef_newActionData, type_data, out var flddef_data);
// arr_flddef_data[i] = flddef_data;
// }
//}
////Create ItemActionData constructor
//MethodDefinition mtddef_ctor_data = new MethodDefinition(".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, module.TypeSystem.Void);
//mtddef_ctor_data.Parameters.Add(new ParameterDefinition("_inventoryData", Mono.Cecil.ParameterAttributes.None, module.ImportReference(typeof(ItemInventoryData))));
//mtddef_ctor_data.Parameters.Add(new ParameterDefinition("_indexInEntityOfAction", Mono.Cecil.ParameterAttributes.None, module.TypeSystem.Int32));
//FieldReference fldref_invdata_item = module.ImportReference(typeof(ItemInventoryData).GetField(nameof(ItemInventoryData.item)));
//FieldReference fldref_item_actions = module.ImportReference(typeof(ItemClass).GetField(nameof(ItemClass.Actions)));
//var il = mtddef_ctor_data.Body.GetILProcessor();
//il.Append(il.Create(OpCodes.Ldarg_0));
//il.Append(il.Create(OpCodes.Ldarg_1));
//il.Append(il.Create(OpCodes.Ldarg_2));
//il.Append(il.Create(OpCodes.Call, module.ImportReference(type_itemActionData.GetConstructor(new Type[] { typeof(ItemInventoryData), typeof(int) }))));
//il.Append(il.Create(OpCodes.Nop));
//for (int i = 0; i < arr_flddef_data.Length; i++)
//{
// if (arr_type_data[i] == null)
// continue;
// il.Append(il.Create(OpCodes.Ldarg_0));
// il.Append(il.Create(OpCodes.Ldarg_1));
// il.Append(il.Create(OpCodes.Ldarg_2));
// il.Emit(OpCodes.Ldarg_1);
// il.Emit(OpCodes.Ldfld, fldref_invdata_item);
// il.Emit(OpCodes.Ldfld, fldref_item_actions);
// il.Emit(OpCodes.Ldarg_2);
// il.Emit(OpCodes.Ldelem_Ref);
// il.Emit(OpCodes.Castclass, manipulator.typedef_newTarget);
// il.Emit(OpCodes.Ldfld, manipulator.arr_flddef_modules[i]);
// il.Append(il.Create(OpCodes.Newobj, module.ImportReference(arr_type_data[i].GetConstructor(new Type[] { typeof(ItemInventoryData), typeof(int), moduleTypes[i] }))));
// il.Append(il.Create(OpCodes.Stfld, arr_flddef_data[i]));
// il.Append(il.Create(OpCodes.Nop));
//}
//il.Append(il.Create(OpCodes.Ret));
//typedef_newActionData.Methods.Add(mtddef_ctor_data);
//Create ItemAction.CreateModifierData override
MethodDefinition mtddef_create_modifier_data = new MethodDefinition(nameof(ItemAction.CreateModifierData), MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.ReuseSlot, module.ImportReference(typeof(ItemActionData)));
mtddef_create_modifier_data.Parameters.Add(new ParameterDefinition("_invData", Mono.Cecil.ParameterAttributes.None, module.ImportReference(typeof(ItemInventoryData))));
mtddef_create_modifier_data.Parameters.Add(new ParameterDefinition("_indexInEntityOfAction", Mono.Cecil.ParameterAttributes.None, module.TypeSystem.Int32));
var il = mtddef_create_modifier_data.Body.GetILProcessor();
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Newobj, mtdref_data_ctor);
il.Emit(OpCodes.Ret);
manipulator.typedef_newTarget.Methods.Add(mtddef_create_modifier_data);
}
public bool MatchSpecialArgs(ParameterDefinition par, MethodDefinition mtddef_target, MethodPatchInfo mtdpinf_derived, int moduleIndex, List<Instruction> list_inst_pars, ILProcessor il)
{
switch (par.Name)
{
//load injected data instance
case "__customData":
var flddef_data = arr_flddef_data[moduleIndex];
if (flddef_data == null)
throw new ArgumentNullException($"No Injected ItemActionData in {mtddef_target.DeclaringType.FullName}! module index {moduleIndex}");
int index = -1;
for (int j = 0; j < mtdpinf_derived.Method.Parameters.Count; j++)
{
if (mtdpinf_derived.Method.Parameters[j].ParameterType.Name == "ItemActionData")
{
index = j;
break;
}
}
if (index < 0)
throw new ArgumentException($"ItemActionData is not present in target method! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
list_inst_pars.Add(il.Create(OpCodes.Ldarg_S, mtdpinf_derived.Method.Parameters[index]));
list_inst_pars.Add(il.Create(OpCodes.Castclass, typedef_newActionData));
list_inst_pars.Add(il.Create(par.ParameterType.IsByReference ? OpCodes.Ldflda : OpCodes.Ldfld, flddef_data));
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
namespace KFCommonUtilityLib
{
public static class ItemClassModuleManager
{
private static readonly Dictionary<string, string> dict_classtypes = new Dictionary<string, string>();
internal static void Init()
{
ModuleManagers.OnAssemblyCreated += static () => dict_classtypes.Clear();
ModuleManagers.OnAssemblyLoaded += static () =>
{
foreach (var pair in dict_classtypes)
{
if (ModuleManagers.TryFindType(pair.Value, out Type classType))
{
var item = ItemClass.GetItemClass(pair.Key);
if (item != null)
{
var itemNew = (ItemClass)Activator.CreateInstance(classType);
item.PreInitCopyTo(itemNew);
if (item is ItemClassModifier mod)
{
mod.PreInitCopyToModifier((ItemClassModifier)itemNew);
}
itemNew.Init();
ItemClass.itemNames.RemoveAt(ItemClass.itemNames.Count - 1);
ItemClass.list[itemNew.Id] = itemNew;
}
}
}
dict_classtypes.Clear();
};
}
internal static void CheckItem(ItemClass item)
{
if (!ModuleManagers.Inited)
{
return;
}
if (item != null && item.Properties.Values.TryGetValue("ItemClassModules", out string str_modules))
{
if (ModuleManagers.PatchType<ItemClassModuleProcessor>(item.GetType(), typeof(ItemClass), str_modules, out string typename))
{
dict_classtypes[item.Name] = typename;
}
}
}
private static void PreInitCopyTo(this ItemClass from, ItemClass to)
{
to.Actions = from.Actions;
foreach (var action in to.Actions)
{
if (action != null)
{
action.item = to;
}
}
to.SetName(from.Name);
to.pId = from.pId;
to.Properties = from.Properties;
to.Effects = from.Effects;
to.setLocalizedItemName(from.localizedName);
to.Stacknumber = from.Stacknumber;
to.SetCanHold(from.bCanHold);
to.SetCanDrop(from.bCanDrop);
to.MadeOfMaterial = from.MadeOfMaterial;
to.MeshFile = from.MeshFile;
to.StickyOffset = from.StickyOffset;
to.StickyColliderRadius = from.StickyColliderRadius;
to.StickyColliderUp = from.StickyColliderUp;
to.StickyColliderLength = from.StickyColliderLength;
to.StickyMaterial = from.StickyMaterial;
to.ImageEffectOnActive = from.ImageEffectOnActive;
to.Active = from.Active;
to.IsSticky = from.IsSticky;
to.DropMeshFile = from.DropMeshFile;
to.HandMeshFile = from.HandMeshFile;
to.HoldType = from.HoldType;
to.RepairTools = from.RepairTools;
to.RepairAmount = from.RepairAmount;
to.RepairTime = from.RepairTime;
to.MaxUseTimes = from.MaxUseTimes;
to.MaxUseTimesBreaksAfter = from.MaxUseTimesBreaksAfter;
to.EconomicValue = from.EconomicValue;
to.Preview = from.Preview;
}
private static void PreInitCopyToModifier(this ItemClassModifier from, ItemClassModifier to)
{
to.CosmeticInstallChance = from.CosmeticInstallChance;
to.PropertyOverrides = from.PropertyOverrides;
to.InstallableTags = from.InstallableTags;
to.DisallowedTags = from.DisallowedTags;
to.ItemTags = from.ItemTags;
to.Type = from.Type;
}
}
}

View File

@@ -0,0 +1,33 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KFCommonUtilityLib
{
public struct ItemClassModuleProcessor : IModuleProcessor
{
public Type GetModuleTypeByName(string name)
{
return ReflectionHelpers.GetTypeWithPrefix("ItemModule", name);
}
public bool BuildConstructor(ModuleManipulator manipulator, MethodDefinition mtddef_ctor)
{
return false;
}
public void InitModules(ModuleManipulator manipulator, Type targetType, Type baseType, params Type[] moduleTypes)
{
}
public bool MatchSpecialArgs(ParameterDefinition par, MethodDefinition mtddef_target, MethodPatchInfo mtdpinf_derived, int moduleIndex, List<Instruction> list_inst_pars, ILProcessor il)
{
return false;
}
}
}

View File

@@ -0,0 +1,262 @@
using HarmonyLib;
using KFCommonUtilityLib.Scripts.Attributes;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
using UniLinq;
using UnityEngine;
namespace KFCommonUtilityLib
{
public static class ModuleManagers
{
private static class ModuleExtensions<T>
{
public readonly static List<Type> extensions = new List<Type>();
}
public static AssemblyDefinition WorkingAssembly { get; private set; } = null;
public static event Action OnAssemblyCreated;
public static event Action OnAssemblyLoaded;
public static bool Inited { get; private set; }
private static bool extensionScanned;
private static readonly HashSet<string> list_registered_path = new HashSet<string>();
private static readonly List<Assembly> list_created = new List<Assembly>();
private static DefaultAssemblyResolver resolver;
private static ModuleAttributes moduleAttributes;
private static ModuleCharacteristics moduleCharacteristics;
private static MethodInfo mtdinf = AccessTools.Method(typeof(ModuleManagers), nameof(ModuleManagers.AddModuleExtension));
public static void InitModuleExtensions()
{
if (extensionScanned)
{
return;
}
var assemblies = ModManager.GetLoadedAssemblies();
foreach (var assembly in assemblies)
{
foreach (var type in assembly.GetTypes())
{
var attr = type.GetCustomAttribute<TypeTargetExtensionAttribute>();
if (attr != null)
{
if ((bool)mtdinf.MakeGenericMethod(attr.ModuleType).Invoke(null, new object[] { type }))
{
Log.Out($"Found Module Extension {type.FullName}");
}
}
}
}
extensionScanned = true;
}
public static bool AddModuleExtension<T>(Type extType)
{
if (typeof(T).GetCustomAttribute<TypeTargetAttribute>() == null)
{
return false;
}
if (!ModuleExtensions<T>.extensions.Contains(extType))
ModuleExtensions<T>.extensions.Add(extType);
return true;
}
public static Type[] GetModuleExtensions<T>()
{
if (typeof(T).GetCustomAttribute<TypeTargetAttribute>() == null)
{
return Array.Empty<Type>();
}
return ModuleExtensions<T>.extensions.ToArray();
}
public static void AddAssemblySearchPath(string path)
{
if (Directory.Exists(path))
{
list_registered_path.Add(path);
}
}
internal static void ClearOutputFolder()
{
Mod self = ModManager.GetMod("CommonUtilityLib");
string path = Path.Combine(self.Path, "AssemblyOutput");
if (Directory.Exists(path))
Array.ForEach(Directory.GetFiles(path), File.Delete);
else
Directory.CreateDirectory(path);
}
internal static void InitNew()
{
if (Inited)
{
return;
}
InitModuleExtensions();
WorkingAssembly?.Dispose();
if (resolver == null)
{
resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(Path.Combine(Application.dataPath, "Managed"));
foreach (var mod in ModManager.GetLoadedMods())
{
resolver.AddSearchDirectory(mod.Path);
}
foreach (var path in list_registered_path)
{
resolver.AddSearchDirectory(path);
}
AssemblyDefinition assdef_main = AssemblyDefinition.ReadAssembly($"{Application.dataPath}/Managed/Assembly-CSharp.dll", new ReaderParameters() { AssemblyResolver = resolver });
moduleAttributes = assdef_main.MainModule.Attributes;
moduleCharacteristics = assdef_main.MainModule.Characteristics;
Log.Out("Reading Attributes from assembly: " + assdef_main.FullName);
}
string assname = "RuntimeAssembled" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
WorkingAssembly = AssemblyDefinition.CreateAssembly(new AssemblyNameDefinition(assname,
new Version(0, 0, 0, 0)),
assname + ".dll",
new ModuleParameters()
{
Kind = ModuleKind.Dll,
AssemblyResolver = resolver,
Architecture = TargetArchitecture.I386,
Runtime = TargetRuntime.Net_4_0,
});
WorkingAssembly.MainModule.Attributes = moduleAttributes;
WorkingAssembly.MainModule.Characteristics = moduleCharacteristics;
//write security attributes so that calling non-public patch methods from this assembly is allowed
Mono.Cecil.SecurityAttribute sattr_permission = new Mono.Cecil.SecurityAttribute(WorkingAssembly.MainModule.ImportReference(typeof(SecurityPermissionAttribute)));
Mono.Cecil.CustomAttributeNamedArgument caarg_SkipVerification = new Mono.Cecil.CustomAttributeNamedArgument(nameof(SecurityPermissionAttribute.SkipVerification), new CustomAttributeArgument(WorkingAssembly.MainModule.TypeSystem.Boolean, true));
sattr_permission.Properties.Add(caarg_SkipVerification);
SecurityDeclaration sdec = new SecurityDeclaration(Mono.Cecil.SecurityAction.RequestMinimum);
sdec.SecurityAttributes.Add(sattr_permission);
WorkingAssembly.SecurityDeclarations.Add(sdec);
OnAssemblyCreated?.Invoke();
Inited = true;
Log.Out("======Init New======");
}
public static bool PatchType<T>(Type targetType, Type baseType, string moduleNames, out string typename) where T : IModuleProcessor, new()
{
Type[] moduleTypes = moduleNames.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => new T().GetModuleTypeByName(s.Trim()))
.Where(t => t.GetCustomAttribute<TypeTargetAttribute>().BaseType.IsAssignableFrom(targetType)).ToArray();
return PatchType(targetType, baseType, moduleTypes, new T(), out typename);
}
public static bool PatchType<T>(Type targetType, Type baseType, Type[] moduleTypes, out string typename) where T : IModuleProcessor, new()
{
return PatchType(targetType, baseType, moduleTypes, new T(), out typename);
}
public static bool PatchType<T>(Type targetType, Type baseType, Type[] moduleTypes, T processor, out string typename) where T : IModuleProcessor
{
if (moduleTypes.Length == 0)
{
typename = string.Empty;
return false;
}
typename = ModuleUtils.CreateTypeName(targetType, moduleTypes);
//Log.Out(typename);
if (!ModuleManagers.TryFindType(typename, out _) && !ModuleManagers.TryFindInCur(typename, out _))
_ = new ModuleManipulator(ModuleManagers.WorkingAssembly, processor, targetType, baseType, moduleTypes);
return true;
}
internal static void FinishAndLoad()
{
if (!Inited)
{
return;
}
//output assembly
Mod self = ModManager.GetMod("CommonUtilityLib");
if (self == null)
{
Log.Warning("Failed to get mod!");
self = ModManager.GetModForAssembly(typeof(ItemActionModuleManager).Assembly);
}
if (self != null && WorkingAssembly != null)
{
if (WorkingAssembly.MainModule.Types.Count > 1)
{
Log.Out("Assembly is valid!");
using (MemoryStream ms = new MemoryStream())
{
try
{
WorkingAssembly.Write(ms);
}
catch (Exception)
{
new ConsoleCmdShutdown().Execute(new List<string>(), new CommandSenderInfo());
}
DirectoryInfo dirInfo = Directory.CreateDirectory(Path.Combine(self.Path, "AssemblyOutput"));
string filename = Path.Combine(dirInfo.FullName, WorkingAssembly.Name.Name + ".dll");
Log.Out("Output Assembly: " + filename);
using (FileStream fs = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Write))
{
ms.WriteTo(fs);
}
Assembly newAssembly = Assembly.LoadFile(filename);
list_created.Add(newAssembly);
}
}
Log.Out("======Finish and Load======");
Inited = false;
OnAssemblyLoaded?.Invoke();
Cleanup();
}
}
//cleanup
internal static void Cleanup()
{
Inited = false;
WorkingAssembly?.Dispose();
WorkingAssembly = null;
}
/// <summary>
/// Check if type is already generated in previous assemblies.
/// </summary>
/// <param name="name">Full type name.</param>
/// <param name="type">The retrieved type, null if not found.</param>
/// <returns>true if found.</returns>
public static bool TryFindType(string name, out Type type)
{
type = null;
foreach (var assembly in list_created)
{
type = assembly.GetType(name, false);
if (type != null)
return true;
}
return false;
}
/// <summary>
/// Check if type is already generated in current working assembly definition.
/// </summary>
/// <param name="name">Full type name.</param>
/// <param name="typedef">The retrieved type definition, null if not found.</param>
/// <returns>true if found.</returns>
public static bool TryFindInCur(string name, out TypeDefinition typedef)
{
typedef = WorkingAssembly?.MainModule.GetType(name);
return typedef != null;
}
}
}

View File

@@ -0,0 +1,646 @@
using HarmonyLib.Public.Patching;
using HarmonyLib;
using KFCommonUtilityLib.Scripts.Attributes;
using Mono.Cecil;
using Mono.Cecil.Cil;
using MonoMod.Cil;
using MonoMod.Utils;
using System;
using System.Collections.Generic;
using UniLinq;
using System.Reflection;
using KFCommonUtilityLib.Harmony;
using UnityEngine.Scripting;
using TypeAttributes = Mono.Cecil.TypeAttributes;
using MethodAttributes = Mono.Cecil.MethodAttributes;
using FieldAttributes = Mono.Cecil.FieldAttributes;
using Mono.Cecil.Rocks;
namespace KFCommonUtilityLib
{
public interface IModuleContainerFor<out T> where T : class
{
T Instance { get; }
}
public class TranspilerTarget
{
public TranspilerTarget(Type type_action, MethodInfo mtdinf_original, PatchInfo patchinf_harmony)
{
this.type_action = type_action;
this.mtdinf_original = mtdinf_original;
this.patchinf_harmony = patchinf_harmony;
}
public Type type_action;
public MethodInfo mtdinf_original;
public PatchInfo patchinf_harmony;
}
public class MethodPatchInfo
{
public readonly MethodDefinition Method;
public Instruction PrefixBegin;
public Instruction PostfixBegin;
public Instruction PostfixEnd;
public MethodPatchInfo(MethodDefinition mtddef, Instruction postfixEnd, Instruction prefixBegin)
{
Method = mtddef;
PostfixEnd = postfixEnd;
PrefixBegin = prefixBegin;
}
}
internal struct MethodOverrideInfo
{
public MethodInfo mtdinf_target;
public MethodInfo mtdinf_base;
public MethodReference mtdref_base;
public Type prefType;
public MethodOverrideInfo(MethodInfo mtdinf_target, MethodInfo mtdinf_base, MethodReference mtddef_base, Type prefType)
{
this.mtdinf_target = mtdinf_target;
this.mtdinf_base = mtdinf_base;
this.mtdref_base = mtddef_base;
this.prefType = prefType;
}
}
public class ModuleManipulator
{
public ModuleDefinition module;
public IModuleProcessor processor;
public Type targetType;
public Type baseType;
public Type[] moduleTypes;
public Type[][] moduleExtensionTypes;
public TypeDefinition typedef_newTarget;
public TypeReference typeref_interface;
public FieldDefinition[] arr_flddef_modules;
private static MethodInfo mtdinf = AccessTools.Method(typeof(ModuleManagers), nameof(ModuleManagers.GetModuleExtensions));
public ModuleManipulator(AssemblyDefinition workingAssembly, IModuleProcessor processor, Type targetType, Type baseType, params Type[] moduleTypes)
{
module = workingAssembly.MainModule;
this.processor = processor;
this.targetType = targetType;
this.baseType = baseType;
this.moduleTypes = moduleTypes;
moduleExtensionTypes = moduleTypes.Select(t => (Type[])mtdinf.MakeGenericMethod(t).Invoke(null, null)).ToArray();
Patch();
}
private void Patch()
{
typeref_interface = module.ImportReference(typeof(IModuleContainerFor<>));
//Create new override type
TypeReference typeref_target = module.ImportReference(targetType);
typedef_newTarget = new TypeDefinition(null, ModuleUtils.CreateTypeName(targetType, moduleTypes), TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.Public | TypeAttributes.Sealed, typeref_target);
typedef_newTarget.CustomAttributes.Add(new CustomAttribute(module.ImportReference(typeof(PreserveAttribute).GetConstructor(Array.Empty<Type>()))));
module.Types.Add(typedef_newTarget);
//Create fields
arr_flddef_modules = new FieldDefinition[moduleTypes.Length];
for (int i = 0; i < moduleTypes.Length; i++)
{
//Create ItemAction field
Type type_module = moduleTypes[i];
MakeContainerFor(typedef_newTarget, type_module, out var flddef_module);
arr_flddef_modules[i] = flddef_module;
}
//Create ItemAction constructor
MethodDefinition mtddef_ctor = new MethodDefinition(".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, module.TypeSystem.Void);
if (processor == null || !processor.BuildConstructor(this, mtddef_ctor))
{
var il = mtddef_ctor.Body.GetILProcessor();
il.Append(il.Create(OpCodes.Ldarg_0));
il.Append(il.Create(OpCodes.Call, module.ImportReference(targetType.GetConstructor(Array.Empty<Type>()))));
il.Append(il.Create(OpCodes.Nop));
for (int i = 0; i < arr_flddef_modules.Length; i++)
{
il.Append(il.Create(OpCodes.Ldarg_0));
il.Append(il.Create(OpCodes.Newobj, module.ImportReference(moduleTypes[i].GetConstructor(Array.Empty<Type>()))));
il.Append(il.Create(OpCodes.Stfld, arr_flddef_modules[i]));
il.Append(il.Create(OpCodes.Nop));
}
il.Append(il.Create(OpCodes.Ret));
}
typedef_newTarget.Methods.Add(mtddef_ctor);
processor?.InitModules(this, targetType, baseType, moduleTypes);
//<derived method name, method patch info>
Dictionary<string, MethodPatchInfo> dict_overrides = new Dictionary<string, MethodPatchInfo>();
//<derived method name, transpiler stub methods in inheritance order>
//TODO: USE TREE INSTEAD OF LIST
Dictionary<string, List<TranspilerTarget>> dict_transpilers = new Dictionary<string, List<TranspilerTarget>>();
//<derived method name, <module type name, local variable>>
Dictionary<string, Dictionary<string, VariableDefinition>> dict_all_states = new Dictionary<string, Dictionary<string, VariableDefinition>>();
//Get all transpilers
for (int i = 0; i < moduleTypes.Length; i++)
{
Type moduleType = moduleTypes[i];
const BindingFlags searchFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var mtd in moduleType.GetMethods(searchFlags).Concat(moduleExtensionTypes[i].SelectMany(t => t.GetMethods(searchFlags))))
{
var attr = mtd.GetCustomAttribute<MethodTargetTranspilerAttribute>();
foreach (var hp in mtd.GetCustomAttributes<HarmonyPatch>())
{
//make sure the transpiler has a target method to apply, otherwise skip it
if (attr != null && hp != null && hp.info.declaringType != null)
{
var hm = hp.info;
hm.methodType = hm.methodType ?? MethodType.Normal;
var mtdinf_target = hm.GetOriginalMethod() as MethodInfo;
if (mtdinf_target == null || mtdinf_target.IsAbstract || !mtdinf_target.IsVirtual)
{
continue;
}
string id = hm.GetTargetMethodIdentifier();
if (!dict_transpilers.TryGetValue(id, out var list))
{
dict_transpilers[id] = (list = new List<TranspilerTarget>());
Type nextType = targetType;
TranspilerTarget curNode = null;
var hm_next = hm.Clone();
while (hm.declaringType.IsAssignableFrom(nextType))
{
hm_next.declaringType = nextType;
var mtdinfo_cur = hm_next.GetOriginalMethod() as MethodInfo;
if (mtdinfo_cur != null)
{
var patchinf_harmony = mtdinfo_cur.ToPatchInfoDontAdd().Copy();
curNode = new TranspilerTarget(mtdinfo_cur.DeclaringType, mtdinfo_cur, patchinf_harmony);
list.Add(curNode);
}
nextType = nextType.BaseType;
}
if (curNode != null)
{
curNode.patchinf_harmony.AddTranspilers(CommonUtilityLibInit.HarmonyInstance.Id, new HarmonyMethod(mtd));
Log.Out($"Adding transpiler {mtd.FullDescription()}\nCurrent transpilers:\n{string.Join('\n', curNode.patchinf_harmony.transpilers.Select(p => p.PatchMethod.FullDescription()))}");
}
}
else
{
bool childFound = false;
foreach (var node in ((IEnumerable<TranspilerTarget>)list).Reverse())
{
if (node.type_action.Equals(hm.declaringType))
{
childFound = true;
node.patchinf_harmony.AddTranspilers(CommonUtilityLibInit.HarmonyInstance.Id, mtd);
Log.Out($"Adding transpiler {mtd.FullDescription()}\nCurrent transpilers:\n{string.Join('\n', node.patchinf_harmony.transpilers.Select(p => p.PatchMethod.FullDescription()))}");
break;
}
}
if (!childFound)
{
Type nextType = list[list.Count - 1].type_action.BaseType;
TranspilerTarget curNode = null;
var hm_next = hm.Clone();
while (hm.declaringType.IsAssignableFrom(nextType))
{
hm_next.declaringType = nextType;
var mtdinfo_cur = hm_next.GetOriginalMethod() as MethodInfo;
if (mtdinfo_cur != null)
{
var patchinf_harmony = mtdinfo_cur.ToPatchInfoDontAdd().Copy();
curNode = new TranspilerTarget(mtdinfo_cur.DeclaringType, mtdinfo_cur, patchinf_harmony);
list.Add(curNode);
}
nextType = nextType.BaseType;
}
if (curNode != null)
{
curNode.patchinf_harmony.AddTranspilers(CommonUtilityLibInit.HarmonyInstance.Id, new HarmonyMethod(mtd));
Log.Out($"Adding transpiler {mtd.FullDescription()}\nCurrent transpilers:\n{string.Join('\n', curNode.patchinf_harmony.transpilers.Select(p => p.PatchMethod.FullDescription()))}");
}
}
}
}
}
}
}
//apply transpilers and replace method calls on base methods with patched ones
Dictionary<string, MethodDefinition> dict_replacers = new Dictionary<string, MethodDefinition>();
foreach (var pair in dict_transpilers)
{
List<TranspilerTarget> list = pair.Value;
//the top copy to call in the override method
MethodDefinition mtddef_override_copy = null;
MethodReference mtdref_override_base = null;
for (int i = list.Count - 1; i >= 0; i--)
{
TranspilerTarget curNode = list[i];
MethodPatcher patcher = curNode.mtdinf_original.GetMethodPatcher();
DynamicMethodDefinition dmd = patcher.CopyOriginal();
ILContext context = new ILContext(dmd.Definition);
HarmonyManipulator.Manipulate(curNode.mtdinf_original, curNode.patchinf_harmony, context);
var mtdref_original = module.ImportReference(curNode.mtdinf_original);
var mtddef_copy = mtdref_original.Resolve().CloneToModuleAsStatic(context.Body, module.ImportReference(curNode.type_action), module);
dmd.Dispose();
context.Dispose();
if (mtddef_override_copy != null && mtdref_override_base != null)
{
//replace calls to the base
foreach (var ins in mtddef_copy.Body.Instructions)
{
if (ins.OpCode == OpCodes.Call && ((MethodReference)ins.Operand).FullName.Equals(mtdref_override_base.FullName))
{
Log.Out($"replacing call to {mtdref_override_base.FullName} to {mtddef_override_copy.FullName}");
ins.Operand = mtddef_override_copy;
}
}
}
//add patched copy to the class
typedef_newTarget.Methods.Add(mtddef_copy);
//the iteration is reversed so make sure we grab the latest method
mtddef_override_copy = mtddef_copy;
mtdref_override_base = mtdref_original;
}
//create the method override that calls the patched copy
if (mtddef_override_copy != null && mtdref_override_base != null)
{
GetOrCreateOverride(dict_overrides, pair.Key, mtdref_override_base, mtddef_override_copy);
}
}
//Apply Postfixes first so that Prefixes can jump to the right instruction
for (int i = 0; i < moduleTypes.Length; i++)
{
Type moduleType = moduleTypes[i];
Dictionary<string, MethodOverrideInfo> dict_targets = GetMethodOverrideTargets<MethodTargetPostfixAttribute>(i);
string moduleID = ModuleUtils.CreateFieldName(moduleType);
foreach (var pair in dict_targets)
{
MethodDefinition mtddef_root = module.ImportReference(pair.Value.mtdinf_base.GetBaseDefinition()).Resolve();
MethodDefinition mtddef_target = module.ImportReference(pair.Value.mtdinf_target).Resolve();
MethodPatchInfo mtdpinf_derived = GetOrCreateOverride(dict_overrides, pair.Key, pair.Value.mtdref_base);
MethodDefinition mtddef_derived = mtdpinf_derived.Method;
if (!dict_all_states.TryGetValue(pair.Key, out var dict_states))
{
dict_states = new Dictionary<string, VariableDefinition>();
dict_all_states.Add(pair.Key, dict_states);
}
var list_inst_pars = MatchArguments(mtddef_root, mtdpinf_derived, mtddef_target, i, true, dict_states, moduleID);
//insert invocation
var il = mtddef_derived.Body.GetILProcessor();
foreach (var ins in list_inst_pars)
{
il.InsertBefore(mtdpinf_derived.PostfixEnd, ins);
}
il.InsertBefore(mtdpinf_derived.PostfixEnd, il.Create(OpCodes.Call, module.ImportReference(mtddef_target)));
if (mtdpinf_derived.PostfixBegin == null)
mtdpinf_derived.PostfixBegin = list_inst_pars[0];
}
}
//Apply Prefixes
for (int i = moduleTypes.Length - 1; i >= 0; i--)
{
Type moduleType = moduleTypes[i];
Dictionary<string, MethodOverrideInfo> dict_targets = GetMethodOverrideTargets<MethodTargetPrefixAttribute>(i);
string moduleID = ModuleUtils.CreateFieldName(moduleType);
foreach (var pair in dict_targets)
{
MethodDefinition mtddef_root = module.ImportReference(pair.Value.mtdinf_base.GetBaseDefinition()).Resolve();
MethodDefinition mtddef_target = module.ImportReference(pair.Value.mtdinf_target).Resolve();
MethodPatchInfo mtdpinf_derived = GetOrCreateOverride(dict_overrides, pair.Key, pair.Value.mtdref_base);
MethodDefinition mtddef_derived = mtdpinf_derived.Method;
dict_all_states.TryGetValue(pair.Key, out var dict_states);
var list_inst_pars = MatchArguments(mtddef_root, mtdpinf_derived, mtddef_target, i, false, dict_states, moduleID);
//insert invocation
var il = mtdpinf_derived.Method.Body.GetILProcessor();
Instruction ins_insert = mtdpinf_derived.PrefixBegin;
foreach (var ins in list_inst_pars)
{
il.InsertBefore(ins_insert, ins);
}
il.InsertBefore(ins_insert, il.Create(OpCodes.Call, module.ImportReference(mtddef_target)));
il.InsertBefore(ins_insert, il.Create(OpCodes.Brfalse_S, mtdpinf_derived.PostfixBegin ?? mtdpinf_derived.PostfixEnd));
}
}
foreach (var pair in dict_all_states)
{
var dict_states = pair.Value;
if (dict_states.Count > 0)
{
Log.Error($"__state variable count does not match in prefixes and postfixes for {pair.Key}! check following modules:\n" + string.Join("\n", dict_states.Keys));
throw new Exception();
}
}
//Add all overrides to new type
foreach (var mtd in dict_overrides.Values)
{
typedef_newTarget.Methods.Add(mtd.Method);
//Log.Out($"Add method override to new action: {mtd.Method.Name}");
}
}
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="targetType"></param>
/// <param name="moduleType"></param>
/// <param name="module"></param>
/// <returns></returns>
private Dictionary<string, MethodOverrideInfo> GetMethodOverrideTargets<T>(int moduleIndex) where T : Attribute, IMethodTarget
{
Type moduleType = moduleTypes[moduleIndex];
Dictionary<string, MethodOverrideInfo> dict_overrides = new Dictionary<string, MethodOverrideInfo>();
const BindingFlags searchFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
const BindingFlags extensionFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var mtd in moduleType.GetMethods(searchFlags).Concat(moduleExtensionTypes[moduleIndex].SelectMany(t => t.GetMethods(extensionFlags))))
{
if (mtd.GetCustomAttribute<T>() != null)
{
foreach (HarmonyPatch hp in mtd.GetCustomAttributes<HarmonyPatch>())
{
if (hp != null && (hp.info.declaringType == null || hp.info.declaringType.IsAssignableFrom(targetType)))
{
var hm = hp.info;
hm.methodType = hm.methodType ?? MethodType.Normal;
var hmclone = hm.Clone();
hmclone.declaringType = targetType;
string id = hm.GetTargetMethodIdentifier();
MethodInfo mtdinf_base = hmclone.GetBaseMethod() as MethodInfo;
if (mtdinf_base == null || !mtdinf_base.IsVirtual || mtdinf_base.IsFinal)
{
Log.Error($"Method not found: {moduleType.FullName} on {hmclone.methodName}\n{hmclone.ToString()}");
continue;
}
MethodReference mtdref_base = module.ImportReference(mtdinf_base);
//Find preferred patch
if (dict_overrides.TryGetValue(id, out var info))
{
if (hm.declaringType == null)
continue;
//cur action type is sub or same class of cur preferred type
//cur preferred type is sub class of previous preferred type
//means cur preferred type is closer to the action type in inheritance hierachy than the previous one
if (hm.declaringType.IsAssignableFrom(targetType) && (info.prefType == null || hm.declaringType.IsSubclassOf(info.prefType)))
{
dict_overrides[id] = new MethodOverrideInfo(mtd, mtdinf_base, mtdref_base, hm.declaringType);
}
}
else
{
dict_overrides[id] = new MethodOverrideInfo(mtd, mtdinf_base, mtdref_base, hm.declaringType);
}
//Log.Out($"Add method override: {id} for {mtdref_base.FullName}/{mtdinf_base.Name}, action type: {itemActionType.Name}");
}
else
{
//Log.Out($"No override target found or preferred type not match on {mtd.Name}");
}
}
}
}
return dict_overrides;
}
/// <summary>
/// Get or create override MethodDefinition of mtdref_base.
/// </summary>
/// <param name="dict_overrides"></param>
/// <param name="id"></param>
/// <param name="mtdref_base"></param>
/// <param name="module"></param>
/// <returns></returns>
private MethodPatchInfo GetOrCreateOverride(Dictionary<string, MethodPatchInfo> dict_overrides, string id, MethodReference mtdref_base, MethodDefinition mtddef_base_override = null)
{
//if (mtddef_base.FullName == "CreateModifierData")
// throw new MethodAccessException($"YOU SHOULD NOT MANUALLY MODIFY CreateModifierData!");
if (dict_overrides.TryGetValue(id, out var mtdpinf_derived))
{
return mtdpinf_derived;
}
//when overriding, retain attributes of base but make sure to remove the 'new' keyword which presents if you are overriding the root method
MethodDefinition mtddef_base = mtdref_base.Resolve();
MethodDefinition mtddef_derived = new MethodDefinition(mtddef_base.Name, (mtddef_base.Attributes | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.ReuseSlot) & ~MethodAttributes.NewSlot, module.ImportReference(mtddef_base.ReturnType));
//Log.Out($"Create method override: {id} for {mtdref_base.FullName}");
foreach (var par in mtddef_base_override?.Parameters?.Skip(1) ?? mtddef_base.Parameters)
{
ParameterDefinition pardef = new ParameterDefinition(par.Name, par.Attributes, module.ImportReference(par.ParameterType));
if (par.HasConstant)
pardef.Constant = par.Constant;
mtddef_derived.Parameters.Add(pardef);
}
mtddef_derived.Body.Variables.Clear();
mtddef_derived.Body.InitLocals = true;
mtddef_derived.Body.Variables.Add(new VariableDefinition(module.TypeSystem.Boolean));
bool hasReturnVal = mtddef_derived.ReturnType.MetadataType != MetadataType.Void;
if (hasReturnVal)
{
mtddef_derived.Body.Variables.Add(new VariableDefinition(module.ImportReference(mtddef_base.ReturnType)));
}
var il = mtddef_derived.Body.GetILProcessor();
if (hasReturnVal)
{
il.Emit(OpCodes.Ldloca_S, mtddef_derived.Body.Variables[1]);
il.Emit(OpCodes.Initobj, module.ImportReference(mtddef_derived.ReturnType));
}
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stloc_S, mtddef_derived.Body.Variables[0]);
Instruction prefixBegin = il.Create(OpCodes.Ldc_I4_1);
il.Append(prefixBegin);
il.Emit(OpCodes.Stloc_S, mtddef_derived.Body.Variables[0]);
il.Emit(OpCodes.Ldarg_0);
for (int i = 0; i < mtddef_derived.Parameters.Count; i++)
{
var par = mtddef_derived.Parameters[i];
il.Emit(par.ParameterType.IsByReference ? OpCodes.Ldarga_S : OpCodes.Ldarg_S, par);
}
il.Emit(OpCodes.Call, mtddef_base_override ?? module.ImportReference(mtdref_base));
if (hasReturnVal)
{
il.Emit(OpCodes.Stloc_S, mtddef_derived.Body.Variables[1]);
il.Emit(OpCodes.Ldloc_S, mtddef_derived.Body.Variables[1]);
}
il.Emit(OpCodes.Ret);
mtdpinf_derived = new MethodPatchInfo(mtddef_derived, mtddef_derived.Body.Instructions[mtddef_derived.Body.Instructions.Count - (hasReturnVal ? 2 : 1)], prefixBegin);
dict_overrides.Add(id, mtdpinf_derived);
return mtdpinf_derived;
}
/// <summary>
/// Create a List<Instruction> that loads all arguments required to call the method onto stack.
/// </summary>
/// <param name="mtddef_root">The root definition of this method.</param>
/// <param name="mtdpinf_derived">The override method.</param>
/// <param name="mtddef_target">The patch method to be called.</param>
/// <param name="flddef_module">The injected module field.</param>
/// <param name="flddef_data">The injected data field.</param>
/// <param name="module">The assembly's main module.</param>
/// <param name="itemActionType">The base ItemAction type.</param>
/// <returns></returns>
/// <exception cref="MissingFieldException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
private List<Instruction> MatchArguments(MethodDefinition mtddef_root, MethodPatchInfo mtdpinf_derived, MethodDefinition mtddef_target, int moduleIndex, bool isPostfix, Dictionary<string, VariableDefinition> dict_states, string moduleID)
{
FieldDefinition flddef_module = arr_flddef_modules[moduleIndex];
var mtddef_derived = mtdpinf_derived.Method;
var il = mtddef_derived.Body.GetILProcessor();
//Match parameters
List<Instruction> list_inst_pars = new List<Instruction>();
list_inst_pars.Add(il.Create(OpCodes.Ldarg_0));
list_inst_pars.Add(il.Create(OpCodes.Ldfld, flddef_module));
foreach (var par in mtddef_target.IsStatic ? mtddef_target.Parameters.Skip(1) : mtddef_target.Parameters)
{
if (par.Name.StartsWith("___"))
{
//___ means non public fields
string str_fldname = par.Name.Substring(3);
FieldDefinition flddef_target = module.ImportReference(targetType.GetField(str_fldname, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)).Resolve();
if (flddef_target == null)
throw new MissingFieldException($"Field with name \"{str_fldname}\" not found! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
if (flddef_target.IsStatic)
{
list_inst_pars.Add(il.Create((par.ParameterType.IsByReference) ? OpCodes.Ldsflda : OpCodes.Ldsfld, module.ImportReference(flddef_target)));
}
else
{
list_inst_pars.Add(il.Create(OpCodes.Ldarg_0));
list_inst_pars.Add(il.Create(par.ParameterType.IsByReference ? OpCodes.Ldflda : OpCodes.Ldfld, module.ImportReference(flddef_target)));
}
}
else if (!MatchSpecialParameters(par, mtddef_target, mtdpinf_derived, moduleIndex, list_inst_pars, il, isPostfix, dict_states, moduleID))
{
//match param by name
int index = -1;
for (int j = 0; j < mtddef_root.Parameters.Count; j++)
{
if (mtddef_root.Parameters[j].Name == par.Name)
{
index = mtddef_root.Parameters[j].Index;
break;
}
}
if (index < 0)
throw new ArgumentException($"Parameter \"{par.Name}\" not found! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
try
{
//Log.Out($"Match Parameter {par.Name} to {mtddef_derived.Parameters[index].Name}/{mtddef_root.Parameters[index].Name} index: {index}");
}
catch (ArgumentOutOfRangeException e)
{
Log.Error($"index {index} parameter {par.Name}" +
$"root pars: {{{string.Join(",", mtddef_root.Parameters.Select(p => p.Name + "/" + p.Index).ToArray())}}}" +
$"derived pars: {{{string.Join(",", mtddef_derived.Parameters.Select(p => p.Name + "/" + p.Index).ToArray())}}}");
throw e;
}
if (!mtddef_derived.Parameters[index].ParameterType.IsByReference)
{
list_inst_pars.Add(il.Create(par.ParameterType.IsByReference ? OpCodes.Ldarga_S : OpCodes.Ldarg_S, mtddef_derived.Parameters[index]));
}
else
{
list_inst_pars.Add(il.Create(OpCodes.Ldarg_S, mtddef_derived.Parameters[index]));
if (!par.ParameterType.IsByReference)
{
list_inst_pars.Add(il.Create(OpCodes.Ldind_Ref));
}
}
}
}
return list_inst_pars;
}
private bool MatchSpecialParameters(ParameterDefinition par, MethodDefinition mtddef_target, MethodPatchInfo mtdpinf_derived, int moduleIndex, List<Instruction> list_inst_pars, ILProcessor il, bool isPostfix, Dictionary<string, VariableDefinition> dict_states, string moduleID)
{
MethodDefinition mtddef_derived = mtdpinf_derived.Method;
switch (par.Name)
{
//load ItemAction instance
case "__instance":
list_inst_pars.Add(il.Create(OpCodes.Ldarg_0));
break;
//load return value
case "__result":
list_inst_pars.Add(il.Create(par.ParameterType.IsByReference ? OpCodes.Ldloca_S : OpCodes.Ldloc_S, mtddef_derived.Body.Variables[1]));
break;
//for postfix only, indicates whether original method is executed
case "__runOriginal":
if (par.ParameterType.IsByReference)
throw new ArgumentException($"__runOriginal is readonly! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
list_inst_pars.Add(il.Create(OpCodes.Ldloc_S, mtddef_derived.Body.Variables[0]));
break;
case "__state":
if (dict_states == null)
{
throw new ArgumentNullException($"__state is found in prefix but no matching postfix exists! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
}
if (!isPostfix && !dict_states.TryGetValue(moduleID, out var vardef))
{
throw new KeyNotFoundException($"__state is found in prefix but not found in corresponding postfix! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
}
if (par.IsOut && isPostfix)
{
throw new ArgumentException($"__state is marked as out parameter in postfix! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
}
if (!par.IsOut && !isPostfix)
{
throw new ArgumentException($"__state is not marked as out in prefix! Patch method: {mtddef_target.DeclaringType.FullName}.{mtddef_target.Name}");
}
if (isPostfix)
{
vardef = new VariableDefinition(module.ImportReference(par.ParameterType));
mtddef_derived.Body.Variables.Add(vardef);
dict_states.Add(moduleID, vardef);
var ins = mtddef_derived.Body.Instructions[0];
il.InsertBefore(ins, il.Create(OpCodes.Ldloca_S, vardef));
il.InsertBefore(ins, il.Create(OpCodes.Initobj, module.ImportReference(par.ParameterType)));
list_inst_pars.Add(il.Create(OpCodes.Ldloc_S, vardef));
}
else
{
vardef = dict_states[moduleID];
dict_states.Remove(moduleID);
list_inst_pars.Add(il.Create(OpCodes.Ldloca_S, vardef));
}
break;
default:
return processor != null ? processor.MatchSpecialArgs(par, mtddef_target, mtdpinf_derived, moduleIndex, list_inst_pars, il) : false;
}
return true;
}
public void MakeContainerFor(TypeDefinition typedef_container, Type type_module, out FieldDefinition flddef_module)
{
var typeref_module = module.ImportReference(type_module);
flddef_module = new FieldDefinition(ModuleUtils.CreateFieldName(type_module), FieldAttributes.Public, typeref_module);
typedef_container.Fields.Add(flddef_module);
typedef_container.Interfaces.Add(new InterfaceImplementation(typeref_interface.MakeGenericInstanceType(typeref_module)));
PropertyDefinition propdef_instance = new PropertyDefinition("Instance", Mono.Cecil.PropertyAttributes.None, typeref_module);
MethodDefinition mtddef_instance_getter = new MethodDefinition("get_Instance", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final, typeref_module);
mtddef_instance_getter.Overrides.Add(module.ImportReference(AccessTools.Method(typeof(IModuleContainerFor<>).MakeGenericType(type_module), "get_Instance")));
typedef_container.Methods.Add(mtddef_instance_getter);
mtddef_instance_getter.Body = new Mono.Cecil.Cil.MethodBody(mtddef_instance_getter);
var generator = mtddef_instance_getter.Body.GetILProcessor();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldfld, flddef_module);
generator.Emit(OpCodes.Ret);
propdef_instance.GetMethod = mtddef_instance_getter;
typedef_container.Properties.Add(propdef_instance);
}
}
}

View File

@@ -0,0 +1,67 @@
using HarmonyLib;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KFCommonUtilityLib
{
public static class ModuleUtils
{
public static string CreateFieldName(Type moduleType)
{
return (moduleType.FullName + "_" + moduleType.Assembly.GetName().Name).ReplaceInvalidChar();
}
public static string CreateFieldName(TypeReference moduleType)
{
return (moduleType.FullName + "_" + moduleType.Module.Assembly.Name.Name).ReplaceInvalidChar();
}
public static string CreateTypeName(Type itemActionType, params Type[] moduleTypes)
{
string typeName = itemActionType.FullName + "_" + itemActionType.Assembly.GetName().Name;
foreach (Type type in moduleTypes)
{
if (type != null)
typeName += "__" + type.FullName + "_" + type.Assembly.GetName().Name;
}
typeName = typeName.ReplaceInvalidChar();
return typeName;
}
public static string CreateTypeName(TypeReference itemActionType, params TypeReference[] moduleTypes)
{
string typeName = itemActionType.FullName + "_" + itemActionType.Module.Assembly.Name.Name;
foreach (TypeReference type in moduleTypes)
{
if (type != null)
typeName += "__" + type.FullName + "_" + type.Module.Assembly.Name.Name;
}
typeName = typeName.ReplaceInvalidChar();
return typeName;
}
private static string ReplaceInvalidChar(this string self)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < self.Length; i++)
{
char c = self[i];
if (!char.IsLetterOrDigit(c) && c != '_')
{
sb.Append('_');
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,1685 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using MethodBody = Mono.Cecil.Cil.MethodBody;
/// <summary>
/// https://github.com/snaphat/MonoCecilExtensions
/// Provides extension methods for classes from the Mono.Cecil library, a library for reading and writing Intermediate Language (IL) code.
/// These extensions facilitate manipulation of IL code, providing functionality to clone, merge, and update types in collections, methods, fields, and other components of a .NET assembly.
/// </summary>
public static class MonoCecilExtensions
{
/// <summary>
/// Represents an information container for updating Mono.Cecil definitions.
/// </summary>
public class UpdateInfo
{
/// <summary>
/// A collection of CustomAttribute objects that have been updated.
/// </summary>
public readonly Collection<CustomAttribute> updatedAttributes = new Collection<CustomAttribute>();
/// <summary>
/// A collection of InterfaceImplementation objects that have been updated.
/// </summary>
public readonly Collection<InterfaceImplementation> updatedInterfaces = new Collection<InterfaceImplementation>();
/// <summary>
/// A collection of FieldDefinition objects that have been updated.
/// </summary>
public readonly Collection<FieldDefinition> updatedFields = new Collection<FieldDefinition>();
/// <summary>
/// A collection of PropertyDefinition objects that have been updated.
/// </summary>
public readonly Collection<PropertyDefinition> updatedProperties = new Collection<PropertyDefinition>();
/// <summary>
/// A collection of MethodDefinition objects that have been updated.
/// </summary>
public readonly Collection<MethodDefinition> updatedMethods = new Collection<MethodDefinition>();
/// <summary>
/// A collection of source TypeDefinition objects that are being merged.
/// </summary>
public readonly Collection<TypeDefinition> srcTypes = new Collection<TypeDefinition>();
/// <summary>
/// A collection of destination TypeDefinition objects where source objects are merged into.
/// </summary>
public readonly Collection<TypeDefinition> destTypes = new Collection<TypeDefinition>();
};
/// <summary>
/// A dictionary mapping from AssemblyDefinition objects to their corresponding UpdateInfo objects.
/// Used to keep track of the updates made to each assembly.
/// </summary>
public static readonly Dictionary<AssemblyDefinition, UpdateInfo> assemblyUpdateInfo = new Dictionary<AssemblyDefinition, UpdateInfo> ();
/// <summary>
/// Additional search directories for resolving assembly types.
/// </summary>
public static readonly Collection<string> additionalSearchDirectories = new Collection<string>();
// Basic extension methods for loading assemblies, adding elements to collections, and finding types, fields, and methods in Mono.Cecil objects.
#region Base
/// <summary>
/// This extension method loads an assembly from a given location.
/// </summary>
/// <param name="location">The location of the assembly to be loaded.</param>
/// <param name="readWrite">A boolean value to determine if the assembly is read-only or writable.</param>
/// <returns>The AssemblyDefinition object of the loaded assembly if successful</returns>
public static AssemblyDefinition LoadAssembly(this string location, bool readWrite = false)
{
// Create a new instance of the DefaultAssemblyResolver.
var resolver = new DefaultAssemblyResolver();
// Add search directories to the resolver.
foreach (var directory in additionalSearchDirectories)
resolver.AddSearchDirectory(directory);
resolver.AddSearchDirectory(Path.GetDirectoryName(typeof(int).Assembly.Location));
resolver.AddSearchDirectory(Path.Combine(Path.GetDirectoryName(typeof(int).Assembly.Location), "Facades"));
// Read and return the assembly using the provided location and reader parameters.
return AssemblyDefinition.ReadAssembly(location, new ReaderParameters()
{
AssemblyResolver = resolver,
ReadWrite = readWrite,
});
}
/// <summary>
/// This extension method finds a method of a given type in an assembly.
/// </summary>
/// <param name="assembly">The assembly where the type and method are located.</param>
/// <param name="typeSignature">The full or simple name of the type.</param>
/// <param name="methodSignature">The full or simple name of the method.</param>
/// <returns>The MethodDefinition object of the found method. Null if not found.</returns>
public static MethodDefinition FindMethodOfType(this AssemblyDefinition assembly, string typeSignature, string methodSignature)
{
// Find and return the method of the given type in the assembly.
return assembly.FindType(typeSignature)?.FindMethod(methodSignature);
}
/// <summary>
/// This extension method finds a type in an assembly using its full name or simple name.
/// </summary>
/// <param name="assembly">The assembly where the type is located.</param>
/// <param name="typeSignature">The full or simple name of the type.</param>
/// <returns>The TypeDefinition object of the found type. Null if not found.</returns>
public static TypeDefinition FindType(this AssemblyDefinition assembly, string typeSignature)
{
// Return the first type that matches the provided type signature.
return assembly.MainModule.Types.FirstOrDefault(type => type.FullName == typeSignature || type.Name == typeSignature);
}
/// <summary>
/// This extension method finds a type in an assembly using its full name or simple name.
/// </summary>
/// <param name="assembly">The assembly where the type is located.</param>
/// <param name="type">The type to locate.</param>
/// <returns>The TypeDefinition object of the found type. Null if not found.</returns>
public static TypeDefinition FindType(this AssemblyDefinition assembly, Type type)
{
// Return the first type that matches the provided type signature.
return assembly.MainModule.Types.FirstOrDefault(_type => _type.FullName == type.FullName || _type.Name == type.Name);
}
/// <summary>
/// This extension method finds a field in a type.
/// </summary>
/// <param name="type">The type where the field is located.</param>
/// <param name="fieldSignature">The full or simple name of the field.</param>
/// <returns>The FieldDefinition object of the found field. Null if not found.</returns>
public static FieldDefinition FindField(this TypeDefinition type, string fieldSignature)
{
// Return the first field that matches the provided field signature.
return type.Fields.FirstOrDefault(m => m.FullName == fieldSignature || m.Name == fieldSignature);
}
/// <summary>
/// This extension method finds a method in a type.
/// </summary>
/// <param name="type">The type where the method is located.</param>
/// <param name="methodSignature">The full or simple name of the method.</param>
/// <returns>The MethodDefinition object of the found method. Null if not found.</returns>
public static MethodDefinition FindMethod(this TypeDefinition type, string methodSignature)
{
// The function checks each method in the type's Methods collection,
// and returns the first method whose full name or simple name matches the provided method signature.
return type.Methods.FirstOrDefault(m => m.FullName == methodSignature || m.Name == methodSignature);
}
/// <summary>
/// This extension method finds all methods in a type that match a given method signature.
/// </summary>
/// <param name="type">The type where the methods are located.</param>
/// <param name="methodSignature">The full or simple name of the methods.</param>
/// <returns>A collection of MethodDefinition objects for the found methods. Empty collection if none found.</returns>
public static Collection<MethodDefinition> FindMethods(this TypeDefinition type, string methodSignature)
{
var collection = new Collection<MethodDefinition>();
// This function checks each method in the type's Methods collection,
// and adds those methods to the collection whose full name or simple name matches the provided method signature.
foreach (var item in type.Methods.Where(m => m.FullName == methodSignature || m.Name == methodSignature))
collection.Add(item);
return collection;
}
#endregion Base
// Extension method that handles adding types to an assembly.
#region AddType
/// <summary>
/// Adds a type to an assembly. This includes adding the type's fields, properties, and methods.
/// If the source type is nested, it will be added as a nested type within the parent type in the destination assembly.
/// </summary>
/// <param name="assembly">The assembly to which the type will be added.</param>
/// <param name="src">The source type that will be added to the assembly.</param>
/// <param name="avoidSignatureConflicts">Avoid name conflicts by adding a '_' suffix to the copied class name.</param>
public static void AddType(this AssemblyDefinition assembly, TypeDefinition src, bool avoidSignatureConflicts = false)
{
// Check for signature conflict avoidance
var srcName = src.Name;
if (avoidSignatureConflicts) src.Name += "_";
// Create a new TypeDefinition with the same properties as the source type
var dest = new TypeDefinition(src.Namespace, src.Name, src.Attributes);
// If the source type isn't nested, add the new type directly to the assembly's types
// Otherwise, find the declaring type in the assembly and add the new type as a nested type
if (!src.IsNested)
assembly.MainModule.Types.Add(dest);
else
assembly.FindType(src.DeclaringType.FullName).NestedTypes.Add(dest);
// Set the base type of the new type to match the base type of the source type
dest.BaseType = src.BaseType;
// Add the fields, properties, and methods from the source type to the new type
dest.AddFieldsPropertiesAndMethods(src);
// Restore name
if (avoidSignatureConflicts) src.Name = srcName;
}
#endregion AddType
// Extension method that handles the addition of fields, properties, and methods from a source type to a destination type.
// This is a key part of merging two types, ensuring the destination type includes all necessary components from the source type.
#region AddFieldsPropertiesAndMethods
/// <summary>
/// Merges the source type into the destination type by cloning the fields, properties, and methods of the source, updating their types and adding them to the destination.
/// </summary>
/// <param name="dest">The destination type definition where fields, properties, and methods from source will be added.</param>
/// <param name="src">The source type definition whose fields, properties, and methods will be cloned and added to the destination.</param>
public static void AddFieldsPropertiesAndMethods(this TypeDefinition dest, TypeDefinition src)
{
// Add nested types to the module
foreach (var subtype in src.NestedTypes)
dest.Module.Assembly.AddType(subtype);
// Clone attributes from the source and add to the destination
var clonedAttributes = new Collection<CustomAttribute>();
foreach (var attribute in src.CustomAttributes)
{
var clonedAttribute = attribute.Clone();
dest.CustomAttributes.Add(clonedAttribute);
clonedAttributes.Add(clonedAttribute);
}
// Clone interfaces from the source and add to the destination
var clonedInterfaces = new Collection<InterfaceImplementation>();
foreach (var @interface in src.Interfaces)
{
var clonedInterface = @interface.Clone();
dest.Interfaces.Add(clonedInterface);
clonedInterfaces.Add(clonedInterface);
}
// Clone fields from the source and add to the destination
var clonedFields = new Collection<FieldDefinition>();
foreach (var field in src.Fields)
{
var clonedField = field.Clone();
clonedFields.Add(clonedField);
dest.Fields.Add(clonedField);
}
// Clone properties from the source and add to the destination
var clonedProperties = new Collection<PropertyDefinition>();
foreach (var property in src.Properties)
{
var clonedProperty = property.Clone();
clonedProperties.Add(clonedProperty);
dest.Properties.Add(clonedProperty);
}
// Clone methods from the source (don't add to the destination yet)
var clonedMethods = new Collection<MethodDefinition>();
foreach (var method in src.Methods)
{
var clonedMethod = method.Clone();
clonedMethods.Add(clonedMethod);
}
// List for keeping track of methods that need further processing
var updatedMethods = new Collection<MethodDefinition>();
// Process each method
foreach (var clonedMethod in clonedMethods.ToList())
{
// Special handling for constructors
if (clonedMethod.Name is ".ctor" || clonedMethod.Name is ".cctor" || clonedMethod.Name is "Finalize")
{
// Temporarily set the declaring type of the cloned method to the destination type
// This is required to get the correct full name of the method for the FindMethod call
clonedMethod.DeclaringType = dest;
// Find an existing method in the destination type that matches the full name of the cloned method
// Note that the full name of a method includes the name of its declaring type
var destMethod = dest.FindMethod(clonedMethod.FullName);
// Reset the declaring type of the cloned method to null
// This is done because the cloned method hasn't been added to the destination type yet,
// and leaving the declaring type set will cause failures to add the method to the destination type
clonedMethod.DeclaringType = null;
// If destination already contains a constructor/destructor, merge the instructions
if (destMethod != null)
{
var clonedInstructions = clonedMethod.Body.Instructions;
var trimmedClonedInstructions = clonedInstructions.ToList();
// For constructors
if (clonedMethod.Name is ".ctor")
{
// Find the constructor call instruction and remove the instructions before it
// This is done to prevent calling the base class constructor twice when merging
var callIndex = trimmedClonedInstructions.FindIndex(x => x.OpCode == OpCodes.Call);
// Check if callIndex is within valid range
if (callIndex < 0 || callIndex >= trimmedClonedInstructions.Count)
throw new Exception("Invalid Call Instruction Index in cloned method.");
// Remove starting instructions
trimmedClonedInstructions.RemoveRange(0, callIndex + 1);
trimmedClonedInstructions.RemoveAt(trimmedClonedInstructions.Count - 1);
// Insert the trimmed instructions to the existing constructor, just before the last instruction (ret)
int insertIndex = destMethod.Body.Instructions.Count - 1;
foreach (var clonedInstruction in trimmedClonedInstructions)
{
destMethod.Body.Instructions.Insert(insertIndex, clonedInstruction);
insertIndex++;
}
}
// For static constructors
else if (clonedMethod.Name is ".cctor")
{
// Remove the last instruction (ret)
trimmedClonedInstructions.RemoveAt(trimmedClonedInstructions.Count - 1);
// Insert the trimmed instructions to the existing static constructor, just before the last instruction (ret)
int insertIndex = destMethod.Body.Instructions.Count - 1;
foreach (var clonedInstruction in trimmedClonedInstructions)
{
destMethod.Body.Instructions.Insert(insertIndex, clonedInstruction);
insertIndex++;
}
}
// For destructors
else if (clonedMethod.Name is "Finalize")
{
// Find the leave.s instruction and remove the instructions after it.
// This is done to prevent calling the base class destructor twice when merging.
var trimIndex = trimmedClonedInstructions.FindIndex(x => x.OpCode == OpCodes.Leave_S);
// Check if trimIndex is within valid range
if (trimIndex < 0 || trimIndex >= trimmedClonedInstructions.Count)
throw new Exception("Invalid trim index in cloned method.");
// Remove instructions after leave.s (inclusive)
trimmedClonedInstructions.RemoveRange(trimIndex, trimmedClonedInstructions.Count - trimIndex);
// Insert the trimmed instructions to the existing destructor, at the beginning
int insertionIndex = 0;
foreach (var clonedInstruction in trimmedClonedInstructions)
{
destMethod.Body.Instructions.Insert(insertionIndex, clonedInstruction);
insertionIndex++;
}
}
// Remove the cloned constructor or destructor from the list of methods to add to the destination type
_ = clonedMethods.Remove(clonedMethod);
// Add the method to the list of methods to update since it has been modified
updatedMethods.Add(destMethod);
}
else
{
// Add the cloned constructor to the destination type
updatedMethods.Add(clonedMethod);
}
}
else
{
// For non-constructor/non-destructor methods
updatedMethods.Add(clonedMethod);
}
}
// Add updated methods to the destination type
foreach (var method in clonedMethods) dest.Methods.Add(method);
// Add updated attributes, interfaces, fields, properties and methods to the update info
if (!assemblyUpdateInfo.TryGetValue(dest.Module.Assembly, out var updateInfo))
updateInfo = assemblyUpdateInfo[dest.Module.Assembly] = new UpdateInfo();
foreach (var attribute in clonedAttributes) updateInfo.updatedAttributes.Add(attribute);
foreach (var @interface in clonedInterfaces) updateInfo.updatedInterfaces.Add(@interface);
foreach (var field in clonedFields) updateInfo.updatedFields.Add(field);
foreach (var property in clonedProperties) updateInfo.updatedProperties.Add(property);
foreach (var method in updatedMethods) updateInfo.updatedMethods.Add(method);
// Add source and destination types to the update info
updateInfo.srcTypes.Add(src);
updateInfo.destTypes.Add(dest);
}
#endregion AddFieldsPropertiesAndMethods
// Extension methods that handle the updating of fields, properties, and methods within a destination type after they have been cloned from a source type.
// These methods ensure that the newly added components in the destination type correctly reference the destination type, rather than the original source type.
#region UpdateFieldsPropertiesAndMethods
/// <summary>
/// Updates the types of attributes, interfaces, fields, properties, and methods within a given assembly.
/// This includes updating the types in interfaces, fields, properties, and methods. It also updates the getter and setter methods for properties,
/// updates the instruction types for methods, imports references for attributes, interfaces, fields, properties, and methods,
/// imports base types of each destination type, and swaps any duplicate methods in the destination types.
/// </summary>
/// <param name="assembly">The assembly to be updated. This assembly's types are matched against the source types and replaced with the corresponding destination types, based on previously registered update information.</param>
/// <param name="avoidSignatureConflicts">Avoid signature conflicts by changing original method parameters to be base object types for duplicate methods</param>
public static void UpdateFieldsPropertiesAndMethods(this AssemblyDefinition assembly, bool avoidSignatureConflicts = false)
{
// Check if update information exists for the assembly
if (assemblyUpdateInfo.TryGetValue(assembly, out var updateInfo))
{
// Update types in interfaces, fields, properties, and methods
for (int i = 0; i < updateInfo.destTypes.Count; ++i)
foreach (var @interface in updateInfo.updatedInterfaces) @interface.UpdateTypes(updateInfo.srcTypes[i], updateInfo.destTypes[i]);
for (int i = 0; i < updateInfo.destTypes.Count; ++i)
foreach (var field in updateInfo.updatedFields) field.UpdateTypes(updateInfo.srcTypes[i], updateInfo.destTypes[i]);
for (int i = 0; i < updateInfo.destTypes.Count; ++i)
foreach (var property in updateInfo.updatedProperties) property.UpdateTypes(updateInfo.srcTypes[i], updateInfo.destTypes[i]);
for (int i = 0; i < updateInfo.destTypes.Count; ++i)
foreach (var method in updateInfo.updatedMethods) method.UpdateTypes(updateInfo.srcTypes[i], updateInfo.destTypes[i]);
// Update getter and setter methods for properties
for (int i = 0; i < updateInfo.destTypes.Count; ++i)
foreach (var property in updateInfo.updatedProperties) property.UpdateGettersAndSetters(updateInfo.srcTypes[i], updateInfo.destTypes[i]);
// Update instruction types for methods
for (int i = 0; i < updateInfo.destTypes.Count; ++i)
foreach (var method in updateInfo.updatedMethods) method.UpdateInstructionTypes(updateInfo.srcTypes[i], updateInfo.destTypes[i]);
// Check for optimization opportunities for methods
foreach (var method in updateInfo.updatedMethods) method.OptimizeInstructions();
// Import references for attributes, interfaces, fields, properties, and methods
foreach (var attribute in updateInfo.updatedAttributes) attribute.ImportReferences(assembly.MainModule);
foreach (var @interface in updateInfo.updatedInterfaces) @interface.ImportReferences(assembly.MainModule);
foreach (var field in updateInfo.updatedFields) field.ImportReferences(assembly.MainModule);
foreach (var property in updateInfo.updatedProperties) property.ImportReferences(assembly.MainModule);
foreach (var method in updateInfo.updatedMethods) method.ImportReferences(assembly.MainModule);
// Import base type of each dest type
foreach (var type in updateInfo.destTypes) type.BaseType = assembly.MainModule.ImportReference(type.BaseType);
// Swap any duplicate methods in the destination types
foreach (var type in updateInfo.destTypes) type.SwapDuplicateMethods(avoidSignatureConflicts);
// Remove the assembly from the update information collection
_ = assemblyUpdateInfo.Remove(assembly);
}
}
#endregion UpdateFieldsPropertiesAndMethods
// Extension methods for cloning various Mono.Cecil objects.
#region Clone
/// <summary>
/// Clones a CustomAttribute.
/// </summary>
/// <param name="attribute">The attribute to be cloned.</param>
/// <returns>A clone of the original attribute.</returns>
public static CustomAttribute Clone(this CustomAttribute attribute)
{
// Create a new CustomAttribute with the constructor of the original attribute.
var clonedAttribute = new CustomAttribute(attribute.Constructor);
// Add all constructor arguments from the original attribute to the cloned attribute.
foreach (var argument in attribute.ConstructorArguments) clonedAttribute.ConstructorArguments.Add(argument);
// Add all properties from the original attribute to the cloned attribute.
foreach (var property in attribute.Properties) clonedAttribute.Properties.Add(property);
// Add all fields from the original attribute to the cloned attribute.
foreach (var field in attribute.Fields) clonedAttribute.Fields.Add(field);
// Return the cloned attribute.
return clonedAttribute;
}
/// <summary>
/// Clones a InterfaceImplementation.
/// </summary>
/// <param name="interface">The interface to be cloned.</param>
/// <returns>A clone of the original interface.</returns>
public static InterfaceImplementation Clone(this InterfaceImplementation @interface)
{
// Create a new InterfaceImplementation with the type the original interface.
var clonedInterface = new InterfaceImplementation(@interface.InterfaceType);
// Copy all custom attributes from the original interface to the cloned interface.
foreach (var attribute in @interface.CustomAttributes) clonedInterface.CustomAttributes.Add(attribute.Clone());
// Return the cloned interface.
return clonedInterface;
}
/// <summary>
/// Clones a FieldDefinition.
/// </summary>
/// <param name="field">The field to be cloned.</param>
/// <returns>A clone of the original field.</returns>
public static FieldDefinition Clone(this FieldDefinition field)
{
// Create a new FieldDefinition with the same properties as the original field.
var clonedField = new FieldDefinition(field.Name, field.Attributes, field.FieldType);
// Copy all custom attributes from the original field to the cloned field.
foreach (var attribute in field.CustomAttributes) clonedField.CustomAttributes.Add(attribute.Clone());
// Copy the MarshalInfo if it exists.
clonedField.MarshalInfo = field.MarshalInfo != null ? new MarshalInfo(field.MarshalInfo.NativeType) : null;
// Copy the initial value of the field.
clonedField.InitialValue = field.InitialValue;
// Return the cloned field.
return clonedField;
}
/// <summary>
/// Clones a PropertyDefinition.
/// </summary>
/// <param name="property">The property to be cloned.</param>
/// <returns>A clone of the original property.</returns>
public static PropertyDefinition Clone(this PropertyDefinition property)
{
// Create a new PropertyDefinition with the same properties as the original property.
var clonedProperty = new PropertyDefinition(property.Name, property.Attributes, property.PropertyType);
// Copy all custom attributes from the original property to the cloned property.
foreach (var attribute in property.CustomAttributes) clonedProperty.CustomAttributes.Add(attribute.Clone());
// Clone the get and set methods if they exist.
clonedProperty.GetMethod = property.GetMethod?.Clone();
clonedProperty.SetMethod = property.SetMethod?.Clone();
// Return the cloned property.
return clonedProperty;
}
/// <summary>
/// Clones a ParameterDefinition.
/// </summary>
/// <param name="parameter">The parameter to be cloned.</param>
/// <returns>A clone of the original parameter.</returns>
public static ParameterDefinition Clone(this ParameterDefinition parameter)
{
// Create a new ParameterDefinition with the same properties as the original parameter.
var clonedParameter = new ParameterDefinition(parameter.Name, parameter.Attributes, parameter.ParameterType);
// Copy all custom attributes from the original parameter to the cloned parameter.
foreach (var attribute in parameter.CustomAttributes) clonedParameter.CustomAttributes.Add(attribute.Clone());
// Return the cloned parameter.
return clonedParameter;
}
/// <summary>
/// Clones a VariableDefinition.
/// </summary>
/// <param name="variable">The variable to be cloned.</param>
/// <returns>A clone of the original variable.</returns>
public static VariableDefinition Clone(this VariableDefinition variable)
{
// Create and return a new VariableDefinition with the same type as the original variable.
return new VariableDefinition(variable.VariableType);
}
/// <summary>
/// Clones an Instruction.
/// </summary>
/// <param name="instruction">The instruction to be cloned.</param>
/// <returns>A clone of the original instruction.</returns>
public static Instruction Clone(this Instruction instruction)
{
// Create a new Instruction with a default opcode.
var clonedInstruction = Instruction.Create(OpCodes.Nop);
// Copy the opcode and operand from the original instruction to the cloned instruction.
clonedInstruction.OpCode = instruction.OpCode;
clonedInstruction.Operand = instruction.Operand;
// Return the cloned instruction.
return clonedInstruction;
}
/// <summary>
/// Clones a MethodDefinition.
/// </summary>
/// <param name="method">The method to be cloned.</param>
/// <returns>A clone of the original method.</returns>
public static MethodDefinition Clone(this MethodDefinition method)
{
// Create a new MethodDefinition with the same properties as the original method.
var clonedMethod = new MethodDefinition(method.Name, method.Attributes, method.ReturnType)
{
ImplAttributes = method.ImplAttributes,
SemanticsAttributes = method.SemanticsAttributes
};
// Add all overides from the original method to the cloned method (references).
foreach (var @override in method.Overrides) clonedMethod.Overrides.Add(@override);
// Copy all custom attributes from the original method to the cloned method.
foreach (var attribute in method.CustomAttributes) clonedMethod.CustomAttributes.Add(attribute.Clone());
// Clone all parameters and add them to the cloned method.
foreach (var parameter in method.Parameters) clonedMethod.Parameters.Add(parameter.Clone());
// Create a new method body for the cloned method.
clonedMethod.Body = new MethodBody(clonedMethod);
// If the original method has a body, copy the relevant properties to the cloned method's body.
if (method.HasBody)
{
clonedMethod.Body.MaxStackSize = method.Body.MaxStackSize;
clonedMethod.Body.InitLocals = method.Body.InitLocals;
// Clone all variables and add them to the cloned method's body.
foreach (var variable in method.Body.Variables) clonedMethod.Body.Variables.Add(variable.Clone());
// Instruction mapping from old to new instructions used to update branch targets which is necessary after cloning
var instructionMapping = new Dictionary<Instruction, Instruction>();
// Clone all the instructions and create the mapping.
foreach (var instruction in method.Body.Instructions)
{
var clonedInstruction = instruction.Clone();
instructionMapping[instruction] = clonedInstruction;
clonedMethod.Body.Instructions.Add(clonedInstruction);
}
// Now fix up the branch targets.
foreach (var instruction in clonedMethod.Body.Instructions)
{
// If the instruction is a branch instruction, fix up its target.
if (instruction.OpCode.FlowControl == FlowControl.Branch ||
instruction.OpCode.FlowControl == FlowControl.Cond_Branch)
{
instruction.Operand = instructionMapping[(Instruction)instruction.Operand];
}
// If the instruction is a switch instruction, fix up its targets.
if (instruction.OpCode == OpCodes.Switch)
{
var oldTargets = (Instruction[])instruction.Operand;
var newTargets = new Instruction[oldTargets.Length];
for (int i = 0; i < oldTargets.Length; ++i)
{
newTargets[i] = instructionMapping[oldTargets[i]];
}
instruction.Operand = newTargets;
}
}
}
// Return the cloned method.
return clonedMethod;
}
public static MethodDefinition CloneToModuleAsStatic(this MethodDefinition method, TypeReference originalType, ModuleDefinition module)
{
// Create a new MethodDefinition with the same properties as the original method.
var clonedMethod = new MethodDefinition(method.Name, MethodAttributes.Static | MethodAttributes.Public | MethodAttributes.HideBySig, module.ImportReference(method.ReturnType))
{
ImplAttributes = method.ImplAttributes,
SemanticsAttributes = method.SemanticsAttributes
};
// Copy all custom attributes from the original method to the cloned method.
foreach (var attribute in method.CustomAttributes)
clonedMethod.CustomAttributes.Add(attribute.CloneToModule(module));
if (!method.Attributes.HasFlag(MethodAttributes.Static))
clonedMethod.Parameters.Add(new ParameterDefinition("self", ParameterAttributes.None, originalType));
// Clone all parameters and add them to the cloned method.
foreach (var parameter in method.Parameters)
clonedMethod.Parameters.Add(parameter.CloneToModule(module));
// Create a new method body for the cloned method.
clonedMethod.Body = new MethodBody(clonedMethod);
// If the original method has a body, copy the relevant properties to the cloned method's body.
if (method.HasBody)
{
clonedMethod.Body.MaxStackSize = method.Body.MaxStackSize;
clonedMethod.Body.InitLocals = method.Body.InitLocals;
// Clone all variables and add them to the cloned method's body.
foreach (var variable in method.Body.Variables) clonedMethod.Body.Variables.Add(variable.CloneToModule(module));
// Instruction mapping from old to new instructions used to update branch targets which is necessary after cloning
var instructionMapping = new Dictionary<Instruction, Instruction>();
// Clone all the instructions and create the mapping.
foreach (var instruction in method.Body.Instructions)
{
var clonedInstruction = instruction.Clone();
clonedInstruction.Resolve(clonedMethod, method, module);
instructionMapping[instruction] = clonedInstruction;
clonedMethod.Body.Instructions.Add(clonedInstruction);
}
// Now fix up the branch targets.
foreach (var instruction in clonedMethod.Body.Instructions)
{
// If the instruction is a branch instruction, fix up its target.
if (instruction.OpCode.FlowControl == FlowControl.Branch ||
instruction.OpCode.FlowControl == FlowControl.Cond_Branch)
{
instruction.Operand = instructionMapping[(Instruction)instruction.Operand];
}
// If the instruction is a switch instruction, fix up its targets.
if (instruction.OpCode == OpCodes.Switch)
{
var oldTargets = (Instruction[])instruction.Operand;
var newTargets = new Instruction[oldTargets.Length];
for (int i = 0; i < oldTargets.Length; ++i)
{
newTargets[i] = instructionMapping[oldTargets[i]];
}
instruction.Operand = newTargets;
}
}
// copy the exception handler blocks
foreach (ExceptionHandler eh in method.Body.ExceptionHandlers)
{
ExceptionHandler neh = new ExceptionHandler(eh.HandlerType);
neh.CatchType = module.ImportReference(eh.CatchType);
// we need to setup neh.Start and End; these are instructions; we need to locate it in the source by index
if (eh.TryStart != null)
{
int idx = method.Body.Instructions.IndexOf(eh.TryStart);
neh.TryStart = clonedMethod.Body.Instructions[idx];
}
if (eh.TryEnd != null)
{
int idx = method.Body.Instructions.IndexOf(eh.TryEnd);
neh.TryEnd = clonedMethod.Body.Instructions[idx];
}
clonedMethod.Body.ExceptionHandlers.Add(neh);
}
}
// Return the cloned method.
return clonedMethod;
}
public static MethodDefinition CloneToModuleAsStatic(this MethodDefinition method, MethodBody body, TypeReference originalType, ModuleDefinition module)
{
// Create a new MethodDefinition with the same properties as the original method.
var clonedMethod = new MethodDefinition(method.Name, MethodAttributes.Static | MethodAttributes.Public | MethodAttributes.HideBySig, module.ImportReference(method.ReturnType))
{
ImplAttributes = method.ImplAttributes,
SemanticsAttributes = method.SemanticsAttributes
};
// Copy all custom attributes from the original method to the cloned method.
foreach (var attribute in method.CustomAttributes)
clonedMethod.CustomAttributes.Add(attribute.CloneToModule(module));
if (!method.Attributes.HasFlag(MethodAttributes.Static))
clonedMethod.Parameters.Add(new ParameterDefinition("self", ParameterAttributes.None, originalType));
// Clone all parameters and add them to the cloned method.
foreach (var parameter in method.Parameters)
clonedMethod.Parameters.Add(parameter.CloneToModule(module));
// Create a new method body for the cloned method.
clonedMethod.Body = new MethodBody(clonedMethod);
// If the original method has a body, copy the relevant properties to the cloned method's body.
clonedMethod.Body.MaxStackSize = body.MaxStackSize;
clonedMethod.Body.InitLocals = body.InitLocals;
// Clone all variables and add them to the cloned method's body.
foreach (var variable in body.Variables) clonedMethod.Body.Variables.Add(variable.CloneToModule(module));
// Instruction mapping from old to new instructions used to update branch targets which is necessary after cloning
var instructionMapping = new Dictionary<Instruction, Instruction>();
// Clone all the instructions and create the mapping.
foreach (var instruction in body.Instructions)
{
var clonedInstruction = instruction.Clone();
clonedInstruction.Resolve(clonedMethod, method, module);
instructionMapping[instruction] = clonedInstruction;
clonedMethod.Body.Instructions.Add(clonedInstruction);
}
// Now fix up the branch targets.
foreach (var instruction in clonedMethod.Body.Instructions)
{
// If the instruction is a branch instruction, fix up its target.
if (instruction.OpCode.FlowControl == FlowControl.Branch ||
instruction.OpCode.FlowControl == FlowControl.Cond_Branch)
{
instruction.Operand = instructionMapping[(Instruction)instruction.Operand];
}
// If the instruction is a switch instruction, fix up its targets.
if (instruction.OpCode == OpCodes.Switch)
{
var oldTargets = (Instruction[])instruction.Operand;
var newTargets = new Instruction[oldTargets.Length];
for (int i = 0; i < oldTargets.Length; ++i)
{
newTargets[i] = instructionMapping[oldTargets[i]];
}
instruction.Operand = newTargets;
}
}
// copy the exception handler blocks
foreach (ExceptionHandler eh in body.ExceptionHandlers)
{
ExceptionHandler neh = new ExceptionHandler(eh.HandlerType);
neh.CatchType = module.ImportReference(eh.CatchType);
// we need to setup neh.Start and End; these are instructions; we need to locate it in the source by index
if (eh.TryStart != null)
{
int idx = body.Instructions.IndexOf(eh.TryStart);
neh.TryStart = clonedMethod.Body.Instructions[idx];
}
if (eh.TryEnd != null)
{
int idx = body.Instructions.IndexOf(eh.TryEnd);
neh.TryEnd = clonedMethod.Body.Instructions[idx];
}
clonedMethod.Body.ExceptionHandlers.Add(neh);
}
// Return the cloned method.
return clonedMethod;
}
public static VariableDefinition CloneToModule(this VariableDefinition variable, ModuleDefinition module)
{
// Create and return a new VariableDefinition with the same type as the original variable.
return new VariableDefinition(module.ImportReference(variable.VariableType));
}
public static ParameterDefinition CloneToModule(this ParameterDefinition parameter, ModuleDefinition module)
{
// Create a new ParameterDefinition with the same properties as the original parameter.
var clonedParameter = new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType));
// Copy all custom attributes from the original parameter to the cloned parameter.
foreach (var attribute in parameter.CustomAttributes)
{
module.ImportReference(attribute.GetType());
clonedParameter.CustomAttributes.Add(attribute.CloneToModule(module));
}
// Return the cloned parameter.
return clonedParameter;
}
public static CustomAttribute CloneToModule(this CustomAttribute attribute, ModuleDefinition module)
{
// Create a new CustomAttribute with the constructor of the original attribute.
var clonedAttribute = new CustomAttribute(module.ImportReference(attribute.Constructor));
// Add all constructor arguments from the original attribute to the cloned attribute.
foreach (var argument in attribute.ConstructorArguments)
{
module.ImportReference(argument.Type);
if (argument.Value != null)
{
module.ImportReference(argument.Value?.GetType());
}
clonedAttribute.ConstructorArguments.Add(argument);
}
// Add all properties from the original attribute to the cloned attribute.
foreach (var property in attribute.Properties)
{
module.ImportReference(property.Argument.Type);
if (property.Argument.Value != null)
{
module.ImportReference(property.Argument.Value?.GetType());
}
clonedAttribute.Properties.Add(property);
}
// Add all fields from the original attribute to the cloned attribute.
foreach (var field in attribute.Fields)
{
module.ImportReference(field.Argument.Type);
if (field.Argument.Value != null)
{
module.ImportReference(field.Argument.Value?.GetType());
}
clonedAttribute.Fields.Add(field);
}
// Return the cloned attribute.
return clonedAttribute;
}
private static void Resolve(this Instruction instruction, MethodDefinition method_new, MethodDefinition method_original, ModuleDefinition module)
{
var operand = instruction.Operand;
if (operand == null)
return;
if (operand is MethodDefinition opmtddef)
{
if (opmtddef.FullName.Equals(method_original.FullName))
instruction.Operand = method_new;
else
instruction.Operand = module.ImportReference(opmtddef);
return;
}
if (operand is MethodReference opmtdref)
{
instruction.Operand = module.ImportReference(opmtdref);
return;
}
if (operand is FieldReference opfldref)
{
instruction.Operand = module.ImportReference(opfldref);
return;
}
if (operand is TypeReference optyperef)
{
instruction.Operand = module.ImportReference(optyperef);
return;
}
}
#endregion Clone
// Extension methods for replacing references to a source type with references to a destination type within Mono.Cecil objects.
// This is used to ensure that copied fields, properties, and methods reference copied types instead of the originals.
#region UpdateTypes
/// <summary>
/// Updates the InterfaceType of the given InterfaceImplementation, if it matches the source type, to the destination type.
/// </summary>
/// <param name="interface">InterfaceImplementation that may have its InterfaceType updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
public static void UpdateTypes(this InterfaceImplementation @interface, TypeDefinition src, TypeDefinition dest)
{
// If the interface's type matches the source type, update it to the destination type
if (@interface.InterfaceType == src) @interface.InterfaceType = dest;
}
/// <summary>
/// Updates the FieldType of the given FieldDefinition, if it matches the source type, to the destination type.
/// </summary>
/// <param name="field">FieldDefinition that may have its FieldType updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
public static void UpdateTypes(this FieldDefinition field, TypeDefinition src, TypeDefinition dest)
{
// If the field's type matches the source type, update it to the destination type
if (field.FieldType == src) field.FieldType = dest;
}
/// <summary>
/// Updates the FieldReference and DeclaringType of the given FieldReference, if they match the source type, to the destination type.
/// If a matching field definition is found in the destination type, a reference to it is returned.
/// Otherwise, the original field reference is returned.
/// </summary>
/// <param name="field">FieldReference that may have its FieldType, and DeclaringType updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
/// <returns>A FieldReference with updated types, or the original FieldReference if no updates were made.</returns>
public static FieldReference UpdateTypes(this FieldReference field, TypeDefinition src, TypeDefinition dest)
{
// Check if the field's FieldType or DeclaringType matches the source type, and if so, replace them with the destination type
if (field.FieldType == src) field.FieldType = dest;
if (field.DeclaringType == src) field.DeclaringType = dest;
// Attempt to find a field in the destination type that matches the field's full name
// If a matching definition is found, return a reference to it otherwise return original reference
return dest.FindField(field.FullName) ?? field;
}
/// <summary>
/// Updates the PropertyType of the given PropertyDefinition, if it matches the source type, to the destination type.
/// </summary>
/// <param name="property">PropertyDefinition that may have its PropertyType updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
public static void UpdateTypes(this PropertyDefinition property, TypeDefinition src, TypeDefinition dest)
{
// If the property's type matches the source type, update it to the destination type
if (property.PropertyType == src) property.PropertyType = dest;
}
/// <summary>
/// Updates the ParameterType of the given ParameterDefinition, if it matches the source type, to the destination type.
/// </summary>
/// <param name="parameter">ParameterDefinition that may have its ParameterType updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
public static void UpdateTypes(this ParameterDefinition parameter, TypeDefinition src, TypeDefinition dest)
{
// If the parameter's type matches the source type, update it to the destination type
if (parameter.ParameterType == src) parameter.ParameterType = dest;
}
/// <summary>
/// Updates the VariableType of the given VariableDefinition, if it matches the source type, to the destination type.
/// </summary>
/// <param name="variable">VariableDefinition that may have its VariableType updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
public static void UpdateTypes(this VariableDefinition variable, TypeDefinition src, TypeDefinition dest)
{
// If the variable's type matches the source type, update it to the destination type
if (variable.VariableType == src) variable.VariableType = dest;
}
/// <summary>
/// Updates the ReturnType of the given MethodDefinition, if it matches the source type, to the destination type.
/// Also updates ParameterTypes and VariableTypes of the MethodDefinition using the same rule.
/// </summary>
/// <param name="method">MethodDefinition that may have its ReturnType, ParameterTypes, and VariableTypes updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
public static void UpdateTypes(this MethodDefinition method, TypeDefinition src, TypeDefinition dest)
{
// Update method overrides if they match the source type
for (int i = 0; i < method.Overrides.Count; i++) method.Overrides[i] = method.Overrides[i].UpdateTypes(src, dest);
// If the method's return type matches the source type, update it to the destination type
if (method.ReturnType == src) method.ReturnType = dest;
// Update method parameters and variables if they match the source type
foreach (var parameter in method.Parameters) parameter.UpdateTypes(src, dest);
if (method.HasBody) foreach (var variable in method.Body.Variables) variable.UpdateTypes(src, dest);
}
/// <summary>
/// Updates the ReturnType and DeclaringType of the given MethodReference, if they match the source type, to the destination type.
/// Also updates the ParameterTypes of the MethodReference using the same rule.
/// If a matching method definition is found in the destination type, a reference to it is returned.
/// Otherwise, the original method reference is returned.
/// </summary>
/// <param name="method">MethodReference that may have its ReturnType, DeclaringType and ParameterTypes updated.</param>
/// <param name="src">The source type which could be replaced.</param>
/// <param name="dest">The destination type which could replace the source type.</param>
/// <returns>A MethodReference with updated types, or the original MethodReference if no updates were made.</returns>
public static MethodReference UpdateTypes(this MethodReference method, TypeDefinition src, TypeDefinition dest)
{
// Update method parameters to destination type
foreach (var parameter in method.Parameters) parameter.UpdateTypes(src, dest);
// Check if the method's ReturnType or DeclaringType matches the source type, and if so, replace them with the destination type
if (method.ReturnType == src) method.ReturnType = dest;
if (method.DeclaringType == src) method.DeclaringType = dest;
// Attempt to find a method in the destination type that matches the method's full name
// If a matching definition is found, return a reference to it otherwise return original reference
return dest.FindMethod(method.FullName) ?? method;
}
/// <summary>
/// Updates the ReturnType and Parameters of the CallSite to the destination type, if they match the source type, to the destination type.
/// </summary>
/// <param name="callSite">CallSite that needs its return type and parameters updated.</param>
/// <param name="src">The original type which is being replaced.</param>
/// <param name="dest">The new type which is replacing the original type.</param>
public static void UpdateTypes(this CallSite callSite, TypeDefinition src, TypeDefinition dest)
{
// Update callsite parameters to destination type
foreach (var parameter in callSite.Parameters) parameter.UpdateTypes(src, dest);
// If the current return type is the source type, update it to destination type
if (callSite.ReturnType == src) callSite.ReturnType = dest;
}
#endregion UpdateTypes
// Extension methods for replacing references to a source type with references to a destination type within Mono.Cecil.Instruction objects.
// This is crucial for ensuring that the instructions within methods correctly reference the fields, properties, and methods of the destination type after cloning from the source type.
#region UpdateInstructionTypes
/// <summary>
/// Updates the Operand of an instruction when merging classes.
/// The update strategy depends on the type of the operand.
/// If the operand is a ParameterDefinition, VariableDefinition, FieldReference, MethodReference, CallSite, or TypeReference, it's updated accordingly.
/// </summary>
/// <param name="instruction">Instruction that needs its operand updated.</param>
/// <param name="src">The original type which is being replaced.</param>
/// <param name="dest">The new type which is replacing the original type.</param>
public static void UpdateInstructionTypes(this Instruction instruction, TypeDefinition src, TypeDefinition dest)
{
// Check operand type and update accordingly
if (instruction.Operand is ParameterDefinition parameter)
parameter.UpdateTypes(src, dest); // Update types in ParameterDefinition
else if (instruction.Operand is VariableDefinition variable)
variable.UpdateTypes(src, dest); // Update types in VariableDefinition
else if (instruction.Operand is TypeReference type && type == src)
instruction.Operand = dest; // Update type in TypeReference
else if (instruction.Operand is FieldReference field)
instruction.Operand = field.UpdateTypes(src, dest); // Update types in FieldReference
else if (instruction.Operand is MethodReference method)
instruction.Operand = method.UpdateTypes(src, dest); // Update types in MethodReference
else if (instruction.Operand is CallSite callSite)
callSite.UpdateTypes(src, dest); // Update types in CallSite
}
/// <summary>
/// Updates all instructions in the method's body.
/// </summary>
/// <param name="method">Method whose instructions are to be updated.</param>
/// <param name="src">The original type which is being replaced.</param>
/// <param name="dest">The new type which is replacing the original type.</param>
public static void UpdateInstructionTypes(this MethodDefinition method, TypeDefinition src, TypeDefinition dest)
{
// Update instructions in the method body to the destination type
if (method.HasBody) foreach (var instruction in method.Body.Instructions) UpdateInstructionTypes(instruction, src, dest);
}
#endregion UpdateInstructionTypes
// Extension methods for replacing references to a source type with references to a destination type within Mono.Cecil.Property getter and setter methods.
// This ensures that the properties of the destination type reference copied getters and setters instead of the originals.
#region UpdateGettersAndSetters
/// <summary>
/// Updates the getter and setter methods of a property to reference the destination type when merging classes.
/// This method does the following:
/// - Clones the existing getter/setter methods, so that any modifications do not affect the original methods
/// - Calls UpdateTypes to update all type references within the methods' bodies from src to dest
/// - Updates the declaring type of the methods to be dest
/// - Finds the equivalent methods in dest (if they exist), and updates the property's getter/setter methods to reference them
/// This ensures that the property correctly interacts with the destination type after merging.
/// </summary>
/// <param name="property">PropertyDefinition whose getter and setter need to be updated.</param>
/// <param name="src">The original type which is being replaced.</param>
/// <param name="dest">The new type which is replacing the original type.</param>
public static void UpdateGettersAndSetters(this PropertyDefinition property, TypeDefinition src, TypeDefinition dest)
{
// If the declaring type of the property is the destination type
if (property.DeclaringType == dest)
{
// If the property has a getter, clone and update it
if (property.GetMethod != null)
{
// Clone the getter
var clonedGetter = property.GetMethod.Clone();
// Update all type references within the getter from src to dest
clonedGetter.UpdateTypes(src, dest);
// Update the declaring type of the getter to be dest
clonedGetter.DeclaringType = dest;
// If an equivalent method exists in dest, update the property's getter to reference it
if (dest.FindMethod(clonedGetter.FullName) is MethodDefinition getMethod)
property.GetMethod = getMethod;
}
// If the property has a setter, clone and update it
if (property.SetMethod != null)
{
// Clone the setter
var clonedSetter = property.SetMethod.Clone();
// Update all type references within the setter from src to dest
clonedSetter.UpdateTypes(src, dest);
// Update the declaring type of the setter to be dest
clonedSetter.DeclaringType = dest;
// If an equivalent method exists in dest, update the property's setter to reference it
if (dest.FindMethod(clonedSetter.FullName) is MethodDefinition setMethod)
property.SetMethod = setMethod;
}
}
}
#endregion UpdateGettersAndSetters
// Extension methods to import references from one module to another.
// This is important when merging assemblies classes as it allows the destination to access types that may not have been referenced prior.
#region ImportReferences
/// <summary>
/// Imports the constructor reference for a given attribute into a module.
/// </summary>
/// <param name="attribute">The custom attribute whose constructor reference needs to be imported.</param>
/// <param name="module">The module type into whose module the reference should be imported.</param>
public static void ImportReferences(this CustomAttribute attribute, ModuleDefinition module)
{
// Import the constructor reference into the module
attribute.Constructor = module.ImportReference(attribute.Constructor);
}
/// <summary>
/// Imports the interface type and custom attributes references of an interface into a module.
/// </summary>
/// <param name="interface">The interface whose references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this InterfaceImplementation @interface, ModuleDefinition module)
{
// Import the custom attributes references into the module
foreach (var attribute in @interface.CustomAttributes) attribute.ImportReferences(module);
// Import the interface type reference into the module
@interface.InterfaceType = module.ImportReference(@interface.InterfaceType);
}
/// <summary>
/// Imports the field type and custom attributes references of a field into a module.
/// </summary>
/// <param name="field">The field whose references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this FieldDefinition field, ModuleDefinition module)
{
// Import the custom attributes references into the module
foreach (var attribute in field.CustomAttributes) attribute.ImportReferences(module);
// Import the field type reference into the module
field.FieldType = module.ImportReference(field.FieldType);
// Import the declaring type definition into the module
field.DeclaringType = module.ImportReference(field.DeclaringType).Resolve();
}
/// <summary>
/// Imports the property type and custom attributes references of a property into a module.
/// </summary>
/// <param name="property">The property whose references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this PropertyDefinition property, ModuleDefinition module)
{
// Import the custom attributes references into the module
foreach (var attribute in property.CustomAttributes) attribute.ImportReferences(module);
// Import the property type reference into the module
property.PropertyType = module.ImportReference(property.PropertyType);
// Import the declaring type definition into the module
property.DeclaringType = module.ImportReference(property.DeclaringType).Resolve();
}
/// <summary>
/// Imports the parameter type and custom attributes references of a parameter into a module.
/// </summary>
/// <param name="parameter">The parameter whose references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this ParameterDefinition parameter, ModuleDefinition module)
{
// Import the custom attributes references into the module
foreach (var attribute in parameter.CustomAttributes) attribute.ImportReferences(module);
// Import the parameter type reference into the module
parameter.ParameterType = module.ImportReference(parameter.ParameterType);
}
/// <summary>
/// Imports the variable type references of a variable into a module.
/// </summary>
/// <param name="variable">The variable whose type references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this VariableDefinition variable, ModuleDefinition module)
{
// Import the variable type reference into the module
variable.VariableType = module.ImportReference(variable.VariableType);
}
/// <summary>
/// Imports the method type references and the custom attributes of a method into a module.
/// </summary>
/// <param name="method">The method whose references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this MethodDefinition method, ModuleDefinition module)
{
// Import method overrides into the module
for (int i = 0; i < method.Overrides.Count; ++i) method.Overrides[i] = module.ImportReference(method.Overrides[i]);
// Import the custom attributes references into the module
foreach (var attribute in method.CustomAttributes) attribute.ImportReferences(module);
// Import the parameter type references into the module
foreach (var parameter in method.Parameters) parameter.ImportReferences(module);
// Import the return type reference into the module
method.ReturnType = module.ImportReference(method.ReturnType);
// Import the declaring type definition into the module
method.DeclaringType = module.ImportReference(method.DeclaringType).Resolve();
// If the method has a body, import references for each variable and instruction
if (method.HasBody)
{
// Import the variable type references in the method body into the module
foreach (var variable in method.Body.Variables) variable.ImportReferences(module);
// Import the instruction type references in the method body into the module
foreach (var instruction in method.Body.Instructions) instruction.ImportReferences(module);
}
}
/// <summary>
/// Imports the return type references of a CallSite into a module.
/// </summary>
/// <param name="callSite">The CallSite whose return type references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this CallSite callSite, ModuleDefinition module)
{
// Import the return type reference of the callSite into the module
callSite.ReturnType = module.ImportReference(callSite.ReturnType);
}
/// <summary>
/// Imports the operand type references of an instruction into a module.
/// </summary>
/// <param name="instruction">The instruction whose operand references need to be imported.</param>
/// <param name="module">The module type into whose module the references should be imported.</param>
public static void ImportReferences(this Instruction instruction, ModuleDefinition module)
{
// Import the operand references of the instruction into the module
if (instruction.Operand is ParameterDefinition parameter)
parameter.ImportReferences(module);
else if (instruction.Operand is VariableDefinition variable)
variable.ImportReferences(module);
else if (instruction.Operand is TypeReference type)
instruction.Operand = module.ImportReference(type);
else if (instruction.Operand is FieldReference field)
instruction.Operand = module.ImportReference(field);
else if (instruction.Operand is MethodReference method)
instruction.Operand = module.ImportReference(method);
else if (instruction.Operand is CallSite callSite)
callSite.ImportReferences(module);
}
#endregion ImportReferences
// Extension methods for swapping method implementations between different types.
// This can be used when wanting to replace method functionality in the destination type with the corresponding functionality from the source type.
#region SwapMethods
/// <summary>
/// Swaps the method references within the provided instruction between two given methods.
/// </summary>
/// <param name="instruction">The instruction to modify.</param>
/// <param name="leftMethod">The first method to swap.</param>
/// <param name="rightMethod">The second method to swap.</param>
public static void SwapMethodReferences(this Instruction instruction, MethodDefinition leftMethod, MethodDefinition rightMethod)
{
// If the instruction's operand is a method reference
if (instruction.Operand is MethodReference method)
{
// If the operand matches the left method, replace it with the right method
if (method == leftMethod)
instruction.Operand = rightMethod;
// If the operand matches the right method, replace it with the left method
else if (method == rightMethod)
instruction.Operand = leftMethod;
}
}
/// <summary>
/// Swaps the method references within the provided collection of instructions between two given methods.
/// </summary>
/// <param name="instructions">The collection of instructions to modify.</param>
/// <param name="leftMethod">The first method to swap.</param>
/// <param name="rightMethod">The second method to swap.</param>
public static void SwapMethodReferences(this Collection<Instruction> instructions, MethodDefinition leftMethod, MethodDefinition rightMethod)
{
// Swap method references for each instruction in the collection
foreach (var instruction in instructions)
instruction.SwapMethodReferences(leftMethod, rightMethod);
}
/// <summary>
/// Swaps the method references within the body of the provided method between two given methods.
/// </summary>
/// <param name="method">The method to modify.</param>
/// <param name="leftMethod">The first method to swap.</param>
/// <param name="rightMethod">The second method to swap.</param>
public static void SwapMethodReferences(this MethodDefinition method, MethodDefinition leftMethod, MethodDefinition rightMethod)
{
// Swap method references for each instruction in the method's body
if (method.HasBody) method.Body.Instructions.SwapMethodReferences(leftMethod, rightMethod);
}
/// <summary>
/// Swaps the attributes, parameters, custom attributes, and generic parameters between two given methods.
/// </summary>
/// <param name="leftMethod">The first method to swap.</param>
/// <param name="rightMethod">The second method to swap.</param>
public static void SwapMethods(this MethodDefinition leftMethod, MethodDefinition rightMethod)
{
// Save the left method's original details
var leftBody = leftMethod.Body;
var leftAttributes = leftMethod.Attributes;
var leftImplAttributes = leftMethod.ImplAttributes;
var leftSemanticsAttributes = leftMethod.SemanticsAttributes;
var leftParameters = new Collection<ParameterDefinition>(leftMethod.Parameters);
var leftCustomAttributes = new Collection<CustomAttribute>(leftMethod.CustomAttributes);
var leftGenericParameters = new Collection<GenericParameter>(leftMethod.GenericParameters);
// Swap the details from the right method to the left
leftMethod.Body = rightMethod.Body;
leftMethod.Body = rightMethod.Body;
leftMethod.Attributes = rightMethod.Attributes;
leftMethod.ImplAttributes = rightMethod.ImplAttributes;
leftMethod.SemanticsAttributes = rightMethod.SemanticsAttributes;
leftMethod.Parameters.Clear();
leftMethod.CustomAttributes.Clear();
leftMethod.GenericParameters.Clear();
foreach (var parameter in rightMethod.Parameters) leftMethod.Parameters.Add(parameter);
foreach (var attribute in rightMethod.CustomAttributes) leftMethod.CustomAttributes.Add(attribute);
foreach (var parameter in rightMethod.GenericParameters) leftMethod.GenericParameters.Add(parameter);
// Swap the details from the left method (which were saved) to the right
rightMethod.Body = leftBody;
rightMethod.Body = leftBody;
rightMethod.Attributes = leftAttributes;
rightMethod.ImplAttributes = leftImplAttributes;
rightMethod.SemanticsAttributes = leftSemanticsAttributes;
rightMethod.Parameters.Clear();
rightMethod.CustomAttributes.Clear();
rightMethod.GenericParameters.Clear();
foreach (var parameter in leftParameters) rightMethod.Parameters.Add(parameter);
foreach (var attribute in leftCustomAttributes) rightMethod.CustomAttributes.Add(attribute);
foreach (var parameter in leftGenericParameters) rightMethod.GenericParameters.Add(parameter);
// Swap method references within each method body
leftMethod.SwapMethodReferences(leftMethod, rightMethod);
rightMethod.SwapMethodReferences(rightMethod, leftMethod);
}
/// <summary>
/// Finds and swaps methods with the same full name within the given type.
/// </summary>
/// <param name="type">The type to modify.</param>
/// <param name="avoidSignatureConflicts">Avoid signature conflicts by changing original method parameters to be base object types</param>
public static void SwapDuplicateMethods(this TypeDefinition type, bool avoidSignatureConflicts = false)
{
// This HashSet is used for tracking the methods that have already been swapped.
var alreadySwapped = new HashSet<string>();
// Convert the method collection to list for efficient index-based access.
var methods = type.Methods.ToList();
// Iterate over each pair of methods in the type
for (int i = 0; i < methods.Count; i++)
{
for (int j = i + 1; j < methods.Count; j++)
{
var methodLeft = methods[i];
var methodRight = methods[j];
// If two methods have the same full name and haven't been swapped yet
if (methodLeft.FullName == methodRight.FullName && !alreadySwapped.Contains(methodLeft.FullName))
{
// Add the method full name to the set of already swapped methods
_ = alreadySwapped.Add(methodLeft.FullName);
// Swap the two methods
methodLeft.SwapMethods(methodRight);
// Change the original method types to be generic to avoid signature conflicts
if (avoidSignatureConflicts)
{
foreach (var parameter in methodRight.Parameters)
if (!parameter.ParameterType.IsValueType) parameter.ParameterType = type.Module.ImportReference(typeof(object));
}
}
}
}
}
#endregion SwapMethods
// Methods to do with instruction optimizations
#region InstructionOptimizations
#pragma warning disable RCS1003
/// <summary>
/// Determines if a given instruction within a method can be optimized out.
/// Specifically, this method looks for type conversion instructions (Isinst or Castclass)
/// that are unnecessary because the type of the value at the top of the stack is
/// already the target conversion type.
/// </summary>
/// <param name="instruction">The instruction to be checked for optimization.</param>
/// <param name="method">The method definition that contains the instruction.</param>
/// <returns>Returns true if the instruction can be optimized out. Otherwise, returns false.</returns>
/// <remarks>
/// This method works by examining the instructions before the given instruction in the method,
/// maintaining a conceptual "stack balance" and tracking the type of the value at the top of the stack.
/// The stack balance is a measure of the net effect of the instructions on the stack,
/// with a positive balance indicating more values have been pushed than popped,
/// and a negative balance indicating more values have been popped than pushed.
/// If the stack balance is zero and the type of the value at the top of the stack
/// matches the type conversion, the conversion is unnecessary and the method returns true.
/// </remarks>
public static bool CanBeOptimizedOut(this Instruction instruction, MethodDefinition method)
{
// Check if the instruction is a type conversion instruction (instance cast or class cast)
if (instruction.OpCode == OpCodes.Isinst || instruction.OpCode == OpCodes.Castclass)
{
// Get the type to which the conversion is being made
var typeConversionType = instruction.Operand as TypeReference;
// Initialize stack balance. This will help to determine the net stack effect of the instructions
int stackBalance = 0;
// Move to the previous instruction
instruction = instruction.Previous;
// Process previous instructions
while (instruction != null)
{
// Determine how the current instruction modifies the stack
var pushBehaviour = instruction.OpCode.StackBehaviourPush;
var popBehaviour = instruction.OpCode.StackBehaviourPop;
// Fullname of any type extracted from the instruction
string extractedFullName = null;
// This is an exhaustive check for control flow change instructions. These instructions will cause a jump
// in execution or a termination of the function, thus ending our analysis.
if (instruction.OpCode == OpCodes.Ret || // Return from the current method.
instruction.OpCode == OpCodes.Throw || // Throw an exception.
instruction.OpCode == OpCodes.Rethrow || // Rethrow the current exception.
instruction.OpCode == OpCodes.Endfilter || // End the filter clause of an exception block and branch to the exception handler.
instruction.OpCode == OpCodes.Endfinally || // Transfer control from the exception block of a try or catch block.
instruction.OpCode == OpCodes.Leave || instruction.OpCode == OpCodes.Leave_S || // Exit a protected region of code.
instruction.OpCode == OpCodes.Jmp || // Jump to the method pointed to by the method pointer loaded on the stack.
instruction.OpCode == OpCodes.Switch || // Switch control to one of several locations.
instruction.OpCode == OpCodes.Br || instruction.OpCode == OpCodes.Br_S || // Unconditional branch to target.
instruction.OpCode == OpCodes.Brfalse || instruction.OpCode == OpCodes.Brfalse_S || // Branch to target if value is zero (false).
instruction.OpCode == OpCodes.Brtrue || instruction.OpCode == OpCodes.Brtrue_S || // Branch to target if value is non-zero (true).
instruction.OpCode == OpCodes.Beq || instruction.OpCode == OpCodes.Beq_S || // Branch to target if two values are equal.
instruction.OpCode == OpCodes.Bne_Un || instruction.OpCode == OpCodes.Bne_Un_S || // Branch to target if two values are not equal.
instruction.OpCode == OpCodes.Bge || instruction.OpCode == OpCodes.Bge_S || instruction.OpCode == OpCodes.Bge_Un || instruction.OpCode == OpCodes.Bge_Un_S || // Branch to target if value1 >= value2 (unsigned or unordered).
instruction.OpCode == OpCodes.Bgt || instruction.OpCode == OpCodes.Bgt_S || instruction.OpCode == OpCodes.Bgt_Un || instruction.OpCode == OpCodes.Bgt_Un_S || // Branch to target if value1 > value2 (unsigned or unordered).
instruction.OpCode == OpCodes.Ble || instruction.OpCode == OpCodes.Ble_S || instruction.OpCode == OpCodes.Ble_Un || instruction.OpCode == OpCodes.Ble_Un_S || // Branch to target if value1 <= value2 (unsigned or unordered).
instruction.OpCode == OpCodes.Blt || instruction.OpCode == OpCodes.Blt_S || instruction.OpCode == OpCodes.Blt_Un || instruction.OpCode == OpCodes.Blt_Un_S) // Branch to target if value1 < value2 (unsigned or unordered).
return false; // Return from method
// Check if instruction is for loading a field onto the stack
// In this case, the type of the value is the type of the field.
else if (instruction.OpCode == OpCodes.Ldfld || // load field value onto stack
instruction.OpCode == OpCodes.Ldflda || // load field address onto stack
instruction.OpCode == OpCodes.Ldsfld || // load static field value onto stack
instruction.OpCode == OpCodes.Ldsflda) // load static field address onto stack
extractedFullName = ((FieldReference)instruction.Operand).FieldType.FullName;
// Check if instruction is for loading an argument onto the stack
// In this case, the type of the value is the type of the argument.
else if (instruction.OpCode == OpCodes.Ldarg || // load argument onto stack
instruction.OpCode == OpCodes.Ldarg_S) // short form for loading argument onto stack
extractedFullName = ((ParameterReference)instruction.Operand).ParameterType.FullName;
// Check for loading argument at index 0 onto stack
else if (instruction.OpCode == OpCodes.Ldarg_0) // load argument at index 0 onto stack
extractedFullName = (method.IsStatic ? method.Parameters[0].ParameterType : method.DeclaringType).FullName;
// Check for loading argument at index 1 onto stack
else if (instruction.OpCode == OpCodes.Ldarg_1) // load argument at index 1 onto stack
extractedFullName = (method.IsStatic ? method.Parameters[1].ParameterType : method.Parameters[0].ParameterType).FullName;
// Check for loading argument at index 2 onto stack
else if (instruction.OpCode == OpCodes.Ldarg_2) // load argument at index 2 onto stack
extractedFullName = (method.IsStatic ? method.Parameters[2].ParameterType : method.Parameters[1].ParameterType).FullName;
// Check for loading argument at index 3 onto stack
else if (instruction.OpCode == OpCodes.Ldarg_3) // load argument at index 3 onto stack
extractedFullName = (method.IsStatic ? method.Parameters[3].ParameterType : method.Parameters[2].ParameterType).FullName;
// Check for loading local variable onto stack
else if (instruction.OpCode == OpCodes.Ldloc || // load local variable onto stack
instruction.OpCode == OpCodes.Ldloc_S) // short form for loading local variable onto stack
extractedFullName = ((VariableReference)instruction.Operand).VariableType.FullName;
// Check for loading local variable at index 0 onto stack
else if (instruction.OpCode == OpCodes.Ldloc_0) // load local variable at index 0 onto stack
extractedFullName = method.Body.Variables[0].VariableType.FullName;
// Check for loading local variable at index 1 onto stack
else if (instruction.OpCode == OpCodes.Ldloc_1)// load local variable at index 1 onto stack
extractedFullName = method.Body.Variables[1].VariableType.FullName;
// Check for loading local variable at index 2 onto stack
else if (instruction.OpCode == OpCodes.Ldloc_2)// load local variable at index 2 onto stack
extractedFullName = method.Body.Variables[2].VariableType.FullName;
// Check for loading local variable at index 3 onto stack
else if (instruction.OpCode == OpCodes.Ldloc_3)// load local variable at index 3 onto stack
extractedFullName = method.Body.Variables[3].VariableType.FullName;
// Check for calling a method and pushing return value onto the stack, loading function pointer onto the stack
else if (instruction.OpCode == OpCodes.Callvirt || // call method virtually and push return value onto stack
instruction.OpCode == OpCodes.Call || // call method and push return value onto stack
instruction.OpCode == OpCodes.Ldftn || // load method pointer onto stack
instruction.OpCode == OpCodes.Ldvirtftn) // load virtual method pointer onto stack
extractedFullName = ((MethodReference)instruction.Operand).ReturnType.FullName;
// Check for calling a method indicated on the stack with arguments, pushing return value onto stack
else if (instruction.OpCode == OpCodes.Calli) // call method indicated on the stack with arguments, pushing return value onto stack
extractedFullName = ((CallSite)instruction.Operand).ReturnType.FullName;
// Check for creating a new object and pushing object reference onto stack
else if (instruction.OpCode == OpCodes.Newobj) // create a new object and push object reference onto stack
extractedFullName = ((MethodReference)instruction.Operand).DeclaringType.FullName;
// Check for loading an object, array element, or pointer onto stack, creating a new array, or creating a typed reference
else if (instruction.OpCode == OpCodes.Ldobj || // load object onto stack
instruction.OpCode == OpCodes.Ldelem_Any || // load element of an object array onto stack
instruction.OpCode == OpCodes.Newarr || // create a new array and push reference onto stack
instruction.OpCode == OpCodes.Mkrefany) // push a typed reference onto stack
extractedFullName = ((TypeReference)instruction.Operand).FullName;
// Check for loading a string onto stack
else if (instruction.OpCode == OpCodes.Ldstr) // load a string onto stack
extractedFullName = typeof(string).FullName;
// If the type of the value currently at the top of the stack matches the type conversion
// and the stack is balanced, the conversion is unnecessary
if (stackBalance == 0 && extractedFullName == typeConversionType.FullName)
return true;
// Dup doesn't change the type of the top of the stack, so adjust stack balance to ignore it
if (instruction.OpCode == OpCodes.Dup)
stackBalance--;
// Adjust stack balance according to the current instruction's push behavior
//if (pushBehaviour == StackBehaviour.Push0)
if (pushBehaviour == StackBehaviour.Push1 || pushBehaviour == StackBehaviour.Pushi || pushBehaviour == StackBehaviour.Pushref ||
pushBehaviour == StackBehaviour.Pushi8 || pushBehaviour == StackBehaviour.Pushr4 || pushBehaviour == StackBehaviour.Pushr8 ||
pushBehaviour == StackBehaviour.Varpush)
stackBalance++;
else if (pushBehaviour == StackBehaviour.Push1_push1)
stackBalance += 2;
// Adjust stack balance according to the current instruction's pop behavior
//if (popBehaviour == StackBehaviour.Pop0)
if (popBehaviour == StackBehaviour.Pop1 || popBehaviour == StackBehaviour.Popi || popBehaviour == StackBehaviour.Popref ||
popBehaviour == StackBehaviour.Varpop)
stackBalance--;
else if (popBehaviour == StackBehaviour.Pop1_pop1 || popBehaviour == StackBehaviour.Popi_popi || popBehaviour == StackBehaviour.Popi_pop1 ||
popBehaviour == StackBehaviour.Popi_popi8 || popBehaviour == StackBehaviour.Popi_popr4 || popBehaviour == StackBehaviour.Popi_popr8 ||
popBehaviour == StackBehaviour.Popref_pop1 || popBehaviour == StackBehaviour.Popref_popi)
stackBalance -= 2;
else if (popBehaviour == StackBehaviour.Popi_popi_popi || popBehaviour == StackBehaviour.Popref_popi_popi || popBehaviour == StackBehaviour.Popref_popi_popi8 ||
popBehaviour == StackBehaviour.Popref_popi_popr4 || popBehaviour == StackBehaviour.Popref_popi_popr8 || popBehaviour == StackBehaviour.Popref_popi_popref)
stackBalance -= 3;
// Move to previous instruction
instruction = instruction.Previous;
}
}
// Return that the instruction cannot be optimized
return false;
}
#pragma warning restore RCS1003
/// <summary>
/// Optimizes a given method by removing any instructions
/// that can be optimized out.
/// </summary>
/// <param name="method">
/// The MethodDefinition object to be optimized. This method contains a list
/// of instructions that are to be checked and potentially removed if they can be optimized out.
/// </param>
public static void OptimizeInstructions(this MethodDefinition method)
{
// If the method doesn't have a body (i.e., it's an abstract or external method), then exit
if (!method.HasBody) return;
// Iterate over each instruction in the method body
for (int i = 0; i < method.Body.Instructions.Count - 1; ++i)
{
// If the current instruction can be optimized out according to the CanBeOptimizedOut method, remove it
if (method.Body.Instructions[i].CanBeOptimizedOut(method))
{
// Remove the current instruction from the method body
method.Body.Instructions.RemoveAt(i);
// Decrement the loop index to account for the removal. This ensures that the next iteration doesn't skip any instructions.
i--;
}
}
}
#endregion InstructionOptimizations
}

View File

@@ -0,0 +1,738 @@
using KFCommonUtilityLib.Scripts.StaticManagers;
using System.Collections.Generic;
using UnityEngine;
namespace KFCommonUtilityLib.Scripts.Utilities
{
/// <summary>
/// This new script make following changes:
/// - projectile ItemValue fields are reused for custom passive calculation:
/// -- Meta => launcher ItemClass id
/// -- SelectedAmmoIndex => launcher action index
/// -- Activated => launcher data strain perc
/// -- cosmetics and mods reference launcher ItemValue
/// -- Quality and Durability is copied from launcher ItemValue
/// - MinEventParams.itemActionData is set to correct launcher data.
/// </summary>
public class CustomProjectileMoveScript : ProjectileMoveScript
{
public override void checkCollision()
{
if (this.firingEntity == null || state != State.Active || gameManager == null)
return;
World world = gameManager?.World;
if (world == null)
{
return;
}
Vector3 checkPos;
if (bOnIdealPos)
{
checkPos = transform.position + Origin.position;
}
else
{
checkPos = idealPosition;
}
Vector3 dir = checkPos - previousPosition;
float magnitude = dir.magnitude;
if (magnitude < 0.04f)
{
return;
}
EntityAlive firingEntity = (EntityAlive)this.firingEntity;
Ray ray = new Ray(previousPosition, dir.normalized);
waterCollisionParticles.CheckCollision(ray.origin, ray.direction, magnitude, (firingEntity != null) ? firingEntity.entityId : (-1));
int prevLayer = 0;
if (firingEntity != null && firingEntity.emodel != null)
{
prevLayer = firingEntity.GetModelLayer();
firingEntity.SetModelLayer(2);
}
int hitmask = ((hmOverride == 0) ? 80 : hmOverride);
bool flag = Voxel.Raycast(world, ray, magnitude, -538750997, hitmask, 0);
if (firingEntity != null && firingEntity.emodel != null)
{
firingEntity.SetModelLayer(prevLayer);
}
if (flag && (GameUtils.IsBlockOrTerrain(Voxel.voxelRayHitInfo.tag) || Voxel.voxelRayHitInfo.tag.StartsWith("E_")))
{
if (firingEntity != null && !firingEntity.isEntityRemote)
{
firingEntity.MinEventContext.Other = ItemActionAttack.FindHitEntity(Voxel.voxelRayHitInfo) as EntityAlive;
firingEntity.MinEventContext.ItemActionData = actionData;
firingEntity.MinEventContext.ItemValue = itemValueLauncher;
firingEntity.MinEventContext.Position = Voxel.voxelRayHitInfo.hit.pos;
ItemActionAttack.AttackHitInfo attackHitInfo = new ItemActionAttack.AttackHitInfo
{
WeaponTypeTag = ItemActionAttack.RangedTag
};
float strainPerc = itemValueProjectile.Activated / (float)byte.MaxValue;
MultiActionProjectileRewrites.ProjectileHit(Voxel.voxelRayHitInfo,
ProjectileOwnerID,
EnumDamageTypes.Piercing,
Mathf.Lerp(1f, MultiActionProjectileRewrites.GetProjectileDamageBlock(itemActionProjectile, itemValueProjectile, ItemActionAttack.GetBlockHit(world, Voxel.voxelRayHitInfo), firingEntity, actionData.indexInEntityOfAction), strainPerc),
Mathf.Lerp(1f, MultiActionProjectileRewrites.GetProjectileDamageEntity(itemActionProjectile, itemValueProjectile, firingEntity, actionData.indexInEntityOfAction), strainPerc),
1f,
1f,
MultiActionReversePatches.ProjectileGetValue(PassiveEffects.CriticalChance, itemValueProjectile, itemProjectile.CritChance.Value, firingEntity, null, itemProjectile.ItemTags, true, false),
ItemAction.GetDismemberChance(actionData, Voxel.voxelRayHitInfo),
itemProjectile.MadeOfMaterial.SurfaceCategory,
itemActionProjectile.GetDamageMultiplier(),
getBuffActions(),
attackHitInfo,
1,
itemActionProjectile.ActionExp,
itemActionProjectile.ActionExpBonusMultiplier,
null,
null,
ItemActionAttack.EnumAttackMode.RealNoHarvesting,
null,
-1,
itemValueProjectile,
itemValueLauncher);
if (firingEntity.MinEventContext.Other == null)
{
firingEntity.FireEvent(MinEventTypes.onSelfPrimaryActionMissEntity, false);
}
firingEntity.FireEvent(MinEventTypes.onProjectileImpact, false);
MinEventParams.CachedEventParam.Self = firingEntity;
MinEventParams.CachedEventParam.Position = Voxel.voxelRayHitInfo.hit.pos;
MinEventParams.CachedEventParam.ItemValue = itemValueProjectile;
MinEventParams.CachedEventParam.ItemActionData = actionData;
MinEventParams.CachedEventParam.Other = firingEntity.MinEventContext.Other;
itemProjectile.FireEvent(MinEventTypes.onProjectileImpact, MinEventParams.CachedEventParam);
if (itemActionProjectile.Explosion.ParticleIndex > 0)
{
Vector3 vector3 = Voxel.voxelRayHitInfo.hit.pos - dir.normalized * 0.1f;
Vector3i vector3i = World.worldToBlockPos(vector3);
if (!world.GetBlock(vector3i).isair)
{
BlockFace blockFace;
vector3i = Voxel.OneVoxelStep(vector3i, vector3, -dir.normalized, out vector3, out blockFace);
}
gameManager.ExplosionServer(Voxel.voxelRayHitInfo.hit.clrIdx, vector3, vector3i, Quaternion.identity, itemActionProjectile.Explosion, ProjectileOwnerID, 0f, false, itemValueProjectile);
SetState(State.Dead);
return;
}
if (itemProjectile.IsSticky)
{
GameRandom gameRandom = world.GetGameRandom();
if (GameUtils.IsBlockOrTerrain(Voxel.voxelRayHitInfo.tag))
{
if (gameRandom.RandomFloat < MultiActionReversePatches.ProjectileGetValue(PassiveEffects.ProjectileStickChance, itemValueProjectile, 0.5f, firingEntity, null, itemProjectile.ItemTags | FastTags<TagGroup.Global>.Parse(Voxel.voxelRayHitInfo.fmcHit.blockValue.Block.blockMaterial.SurfaceCategory), true, false))
{
RestoreProjectileValue();
ProjectileID = ProjectileManager.AddProjectileItem(transform, -1, Voxel.voxelRayHitInfo.hit.pos, dir.normalized, itemValueProjectile.type);
SetState(State.Sticky);
}
else
{
gameManager.SpawnParticleEffectServer(new ParticleEffect("impact_metal_on_wood", Voxel.voxelRayHitInfo.hit.pos, Utils.BlockFaceToRotation(Voxel.voxelRayHitInfo.fmcHit.blockFace), 1f, Color.white, string.Format("{0}hit{1}", Voxel.voxelRayHitInfo.fmcHit.blockValue.Block.blockMaterial.SurfaceCategory, itemProjectile.MadeOfMaterial.SurfaceCategory), null), firingEntity.entityId, false, false);
SetState(State.Dead);
}
}
else if (gameRandom.RandomFloat < MultiActionReversePatches.ProjectileGetValue(PassiveEffects.ProjectileStickChance, itemValueProjectile, 0.5f, firingEntity, null, itemProjectile.ItemTags, true, false))
{
RestoreProjectileValue();
ProjectileID = ProjectileManager.AddProjectileItem(transform, -1, Voxel.voxelRayHitInfo.hit.pos, dir.normalized, itemValueProjectile.type);
Utils.SetLayerRecursively(ProjectileManager.GetProjectile(ProjectileID).gameObject, 14, null);
SetState(State.Sticky);
}
else
{
gameManager.SpawnParticleEffectServer(new ParticleEffect("impact_metal_on_wood", Voxel.voxelRayHitInfo.hit.pos, Utils.BlockFaceToRotation(Voxel.voxelRayHitInfo.fmcHit.blockFace), 1f, Color.white, "bullethitwood", null), firingEntity.entityId, false, false);
SetState(State.Dead);
}
}
else
{
SetState(State.Dead);
}
}
else
{
SetState(State.Dead);
}
if (state == State.Active)
{
SetState(State.Dead);
}
}
previousPosition = checkPos;
}
private void RestoreProjectileValue()
{
itemValueProjectile.Modifications = ItemValue.emptyItemValueArray;
itemValueProjectile.CosmeticMods = ItemValue.emptyItemValueArray;
itemValueProjectile.Quality = 0;
itemValueProjectile.UseTimes = 0;
itemValueProjectile.Meta = 0;
itemValueProjectile.SelectedAmmoTypeIndex = 0;
}
}
public static class MultiActionProjectileRewrites
{
public static void ProjectileHit(WorldRayHitInfo hitInfo, int _attackerEntityId, EnumDamageTypes _damageType, float _blockDamage,
float _entityDamage, float _staminaDamageMultiplier, float _weaponCondition, float _criticalHitChanceOLD,
float _dismemberChance, string _attackingDeviceMadeOf, DamageMultiplier _damageMultiplier,
List<string> _buffActions, ItemActionAttack.AttackHitInfo _attackDetails, int _flags = 1, int _actionExp = 0,
float _actionExpBonus = 0f, ItemActionAttack rangeCheckedAction = null,
Dictionary<string, ItemActionAttack.Bonuses> _toolBonuses = null,
ItemActionAttack.EnumAttackMode _attackMode = ItemActionAttack.EnumAttackMode.RealNoHarvesting,
Dictionary<string, string> _hitSoundOverrides = null, int ownedEntityId = -1, ItemValue projectileValue = null, ItemValue launcherValue = null)
{
if (_attackDetails != null)
{
_attackDetails.hitPosition = Vector3i.zero;
_attackDetails.bKilled = false;
}
if (hitInfo == null || hitInfo.tag == null)
{
return;
}
World world = GameManager.Instance.World;
bool canHarvest = true;
if (_attackMode == ItemActionAttack.EnumAttackMode.RealNoHarvestingOrEffects)
{
canHarvest = false;
_attackMode = ItemActionAttack.EnumAttackMode.RealNoHarvesting;
}
if (_attackDetails != null)
{
_attackDetails.itemsToDrop = null;
_attackDetails.bBlockHit = false;
_attackDetails.entityHit = null;
}
string blockFaceParticle = null;
string surfaceCategory = null;
float lightValueAtBlockPos = 1f;
Color blockFaceColor = Color.white;
bool isProtectionApplied = false;
EntityAlive attackerEntity = world.GetEntity(_attackerEntityId) as EntityAlive;
bool isHoldingDamageItem = false;
if (attackerEntity != null)
{
if (launcherValue == null)
{
launcherValue = attackerEntity.inventory.holdingItemItemValue;
}
isHoldingDamageItem = launcherValue.Equals(attackerEntity.inventory.holdingItemItemValue);
}
bool isHitTargetPlayer = true;
//if hits block or terrain
if (GameUtils.IsBlockOrTerrain(hitInfo.tag))
{
if (ItemAction.ShowDebugDisplayHit)
{
DebugLines.Create(null, attackerEntity.RootTransform, Camera.main.transform.position + Origin.position, hitInfo.hit.pos, new Color(1f, 0.5f, 1f), new Color(1f, 0f, 1f), ItemAction.DebugDisplayHitSize * 2f, ItemAction.DebugDisplayHitSize, ItemAction.DebugDisplayHitTime);
}
ChunkCluster hittedChunk = world.ChunkClusters[hitInfo.hit.clrIdx];
if (hittedChunk == null)
{
return;
}
Vector3i hitBlockPos = hitInfo.hit.blockPos;
BlockValue hitBlockValue = hittedChunk.GetBlock(hitBlockPos);
if (hitBlockValue.isair && hitInfo.hit.blockValue.Block.IsDistantDecoration && hitInfo.hit.blockValue.damage >= hitInfo.hit.blockValue.Block.MaxDamage - 1)
{
hitBlockValue = hitInfo.hit.blockValue;
world.SetBlockRPC(hitBlockPos, hitBlockValue);
}
Block hitBlock = hitBlockValue.Block;
if (hitBlock == null)
{
return;
}
if (hitBlockValue.ischild)
{
hitBlockPos = hitBlock.multiBlockPos.GetParentPos(hitBlockPos, hitBlockValue);
hitBlockValue = hittedChunk.GetBlock(hitBlockPos);
hitBlock = hitBlockValue.Block;
if (hitBlock == null)
{
return;
}
}
if (hitBlockValue.isair)
{
return;
}
float landProtectionModifier = 0;
if (!world.IsWithinTraderArea(hitInfo.hit.blockPos) && hitBlock.blockMaterial.id != "Mbedrock")
{
landProtectionModifier = world.GetLandProtectionHardnessModifier(hitInfo.hit.blockPos, attackerEntity, world.GetGameManager().GetPersistentLocalPlayer());
}
if (landProtectionModifier != 1f)
{
if (attackerEntity && _attackMode != ItemActionAttack.EnumAttackMode.Simulate && attackerEntity is EntityPlayer && !launcherValue.ItemClass.ignoreKeystoneSound && !launcherValue.ToBlockValue().Block.IgnoreKeystoneOverlay)
{
attackerEntity.PlayOneShot("keystone_impact_overlay", false);
}
if (landProtectionModifier < 1f)
{
isProtectionApplied = true;
}
}
if (hitBlockPos != _attackDetails.hitPosition || landProtectionModifier != _attackDetails.hardnessScale || hitBlockValue.type != _attackDetails.blockBeingDamaged.type || (isHoldingDamageItem && projectileValue.SelectedAmmoTypeIndex != _attackDetails.ammoIndex))
{
float finalHardness = Mathf.Max(hitBlock.GetHardness(), 0.1f) * landProtectionModifier;
float finalBlockDamage = _blockDamage * ((_damageMultiplier != null) ? _damageMultiplier.Get(hitBlock.blockMaterial.DamageCategory) : 1f);
if (attackerEntity)
{
finalBlockDamage *= attackerEntity.GetBlockDamageScale();
}
if (_toolBonuses != null && _toolBonuses.Count > 0)
{
finalBlockDamage *= calculateHarvestToolDamageBonus(_toolBonuses, hitBlock.itemsToDrop);
_attackDetails.bHarvestTool = true;
}
_attackDetails.damagePerHit = isProtectionApplied ? 0f : (finalBlockDamage / finalHardness);
_attackDetails.damage = 0f;
_attackDetails.hardnessScale = landProtectionModifier;
_attackDetails.hitPosition = hitBlockPos;
_attackDetails.blockBeingDamaged = hitBlockValue;
if (isHoldingDamageItem)
{
_attackDetails.ammoIndex = projectileValue.SelectedAmmoTypeIndex;
}
}
_attackDetails.raycastHitPosition = hitInfo.hit.blockPos;
Block fmcHitBlock = hitInfo.fmcHit.blockValue.Block;
lightValueAtBlockPos = world.GetLightBrightness(hitInfo.fmcHit.blockPos);
blockFaceColor = fmcHitBlock.GetColorForSide(hitInfo.fmcHit.blockValue, hitInfo.fmcHit.blockFace);
blockFaceParticle = fmcHitBlock.GetParticleForSide(hitInfo.fmcHit.blockValue, hitInfo.fmcHit.blockFace);
MaterialBlock materialForSide = fmcHitBlock.GetMaterialForSide(hitInfo.fmcHit.blockValue, hitInfo.fmcHit.blockFace);
surfaceCategory = materialForSide.SurfaceCategory;
float modifiedBlockDamage = _attackDetails.damagePerHit * _staminaDamageMultiplier;
if (attackerEntity)
{
string blockFaceDamageCategory = materialForSide.DamageCategory ?? string.Empty;
modifiedBlockDamage = (int)MultiActionReversePatches.ProjectileGetValue(PassiveEffects.DamageModifier, projectileValue, modifiedBlockDamage, attackerEntity, null, FastTags<TagGroup.Global>.Parse(blockFaceDamageCategory) | _attackDetails.WeaponTypeTag | hitInfo.fmcHit.blockValue.Block.Tags, true, false);
}
modifiedBlockDamage = ItemActionAttack.DegradationModifier(modifiedBlockDamage, _weaponCondition);
modifiedBlockDamage = isProtectionApplied ? 0f : Utils.FastMax(1f, modifiedBlockDamage);
_attackDetails.damage += modifiedBlockDamage;
_attackDetails.bKilled = false;
_attackDetails.damageTotalOfTarget = hitBlockValue.damage + _attackDetails.damage;
if (_attackDetails.damage > 0f)
{
BlockFace blockFaceFromHitInfo = GameUtils.GetBlockFaceFromHitInfo(hitBlockPos, hitBlockValue, hitInfo.hitCollider, hitInfo.hitTriangleIdx, out _, out _);
int blockFaceTexture = hittedChunk.GetBlockFaceTexture(hitBlockPos, blockFaceFromHitInfo);
int blockCurDamage = hitBlockValue.damage;
bool isBlockBroken = blockCurDamage >= hitBlock.MaxDamage;
int ownerAttackerID = ((ownedEntityId != -1 && ownedEntityId != -2) ? ownedEntityId : _attackerEntityId);
int blockNextDamage = (_attackMode != ItemActionAttack.EnumAttackMode.Simulate) ? hitBlock.DamageBlock(world, hittedChunk.ClusterIdx, hitBlockPos, hitBlockValue, (int)_attackDetails.damage, ownerAttackerID, _attackDetails, _attackDetails.bHarvestTool, false) : 0;
if (blockNextDamage == 0)
{
_attackDetails.damage = 0f;
}
else
{
_attackDetails.damage -= blockNextDamage - blockCurDamage;
}
if (_attackMode != ItemActionAttack.EnumAttackMode.Simulate && canHarvest && attackerEntity is EntityPlayerLocal && blockFaceTexture > 0 && hitBlock.MeshIndex == 0 && blockNextDamage >= hitBlock.MaxDamage * 1f)
{
ParticleEffect particleEffect = new ParticleEffect("paint_block", hitInfo.fmcHit.pos - Origin.position, Utils.BlockFaceToRotation(hitInfo.fmcHit.blockFace), lightValueAtBlockPos, blockFaceColor, null, null)
{
opqueTextureId = BlockTextureData.list[blockFaceTexture].TextureID
};
GameManager.Instance.SpawnParticleEffectClient(particleEffect, _attackerEntityId, false, false);
}
_attackDetails.damageGiven = ((!isBlockBroken) ? (blockNextDamage - blockCurDamage) : 0);
_attackDetails.damageMax = hitBlock.MaxDamage;
_attackDetails.bKilled = !isBlockBroken && blockNextDamage >= hitBlock.MaxDamage;
_attackDetails.itemsToDrop = hitBlock.itemsToDrop;
_attackDetails.bBlockHit = true;
_attackDetails.materialCategory = hitBlock.blockMaterial.SurfaceCategory;
if (attackerEntity != null && _attackMode != ItemActionAttack.EnumAttackMode.Simulate)
{
attackerEntity.MinEventContext.BlockValue = hitBlockValue;
attackerEntity.MinEventContext.Tags = hitBlock.Tags;
if (_attackDetails.bKilled)
{
attackerEntity.FireEvent(MinEventTypes.onSelfDestroyedBlock, isHoldingDamageItem);
attackerEntity.NotifyDestroyedBlock(_attackDetails);
}
else
{
attackerEntity.FireEvent(MinEventTypes.onSelfDamagedBlock, isHoldingDamageItem);
}
}
}
}
else if (hitInfo.tag.StartsWith("E_"))
{
Entity hitEntity = ItemActionAttack.FindHitEntityNoTagCheck(hitInfo, out string hitBodyPart);
if (hitEntity == null)
{
return;
}
if (hitEntity.entityId == _attackerEntityId)
{
return;
}
if (!hitEntity.CanDamageEntity(_attackerEntityId))
{
return;
}
EntityAlive hitEntityAlive = hitEntity as EntityAlive;
DamageSourceEntity damageSourceEntity = new DamageSourceEntity(EnumDamageSource.External, _damageType, _attackerEntityId, hitInfo.ray.direction, hitInfo.transform.name, hitInfo.hit.pos, Voxel.phyxRaycastHit.textureCoord);
damageSourceEntity.AttackingItem = projectileValue;
damageSourceEntity.DismemberChance = _dismemberChance;
damageSourceEntity.CreatorEntityId = ownedEntityId;
bool isCriticalHit = _attackDetails.isCriticalHit;
int finalEntityDamage = (int)_entityDamage;
if (attackerEntity != null && hitEntityAlive != null)
{
FastTags<TagGroup.Global> equipmentTags = FastTags<TagGroup.Global>.none;
if (hitEntityAlive.Health > 0)
{
equipmentTags = FastTags<TagGroup.Global>.Parse(damageSourceEntity.GetEntityDamageEquipmentSlotGroup(hitEntityAlive).ToStringCached());
equipmentTags |= DamagePatches.GetBodyPartTags(damageSourceEntity.GetEntityDamageBodyPart(hitEntityAlive));
}
finalEntityDamage = (int)MultiActionReversePatches.ProjectileGetValue(PassiveEffects.DamageModifier, projectileValue, finalEntityDamage, attackerEntity, null, equipmentTags | _attackDetails.WeaponTypeTag | hitEntityAlive.EntityClass.Tags, true, false);
finalEntityDamage = (int)MultiActionReversePatches.ProjectileGetValue(PassiveEffects.InternalDamageModifier, projectileValue, finalEntityDamage, hitEntityAlive, null, equipmentTags | projectileValue.ItemClass.ItemTags, true, false);
}
if (!hitEntityAlive || hitEntityAlive.Health > 0)
{
finalEntityDamage = Utils.FastMax(1, ItemActionAttack.difficultyModifier(finalEntityDamage, world.GetEntity(_attackerEntityId), hitEntity));
}
else if (_toolBonuses != null)
{
finalEntityDamage = (int)(finalEntityDamage * calculateHarvestToolDamageBonus(_toolBonuses, EntityClass.list[hitEntity.entityClass].itemsToDrop));
}
//Log.Out("Final entity damage: " + finalEntityDamage);
bool isAlreadyDead = hitEntity.IsDead();
int deathHealth = (hitEntityAlive != null) ? hitEntityAlive.DeathHealth : 0;
if (_attackMode != ItemActionAttack.EnumAttackMode.Simulate)
{
if (attackerEntity != null)
{
MinEventParams minEventContext = attackerEntity.MinEventContext;
minEventContext.Other = hitEntityAlive;
minEventContext.StartPosition = hitInfo.ray.origin;
}
if (SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer && (attackerEntity as EntityPlayer == null || !attackerEntity.isEntityRemote) && hitEntity.isEntityRemote && rangeCheckedAction != null)
{
EntityPlayer hitPlayer = hitEntity as EntityPlayer;
if (hitPlayer != null)
{
isHitTargetPlayer = false;
Ray lookRay = attackerEntity.GetLookRay();
lookRay.origin -= lookRay.direction * 0.15f;
float range = Utils.FastMax(rangeCheckedAction.Range, rangeCheckedAction.BlockRange) * ItemActionAttack.attackRangeMultiplier;
string hitTransformPath = null;
List<string> list_buffs = _buffActions;
if (list_buffs != null)
{
if (hitEntityAlive)
{
hitTransformPath = (hitBodyPart != null) ? GameUtils.GetChildTransformPath(hitEntity.transform, hitInfo.transform) : null;
}
else
{
list_buffs = null;
}
}
if (attackerEntity != null)
{
attackerEntity.FireEvent(MinEventTypes.onSelfAttackedOther, isHoldingDamageItem);
if (hitEntityAlive != null && hitEntityAlive.RecordedDamage.Strength > 0)
{
attackerEntity.FireEvent(MinEventTypes.onSelfDamagedOther, isHoldingDamageItem);
}
}
if (!isAlreadyDead && hitEntity.IsDead() && attackerEntity != null)
{
attackerEntity.FireEvent(MinEventTypes.onSelfKilledOther, isHoldingDamageItem);
}
if (hitEntityAlive && hitEntityAlive.RecordedDamage.ArmorDamage > hitEntityAlive.RecordedDamage.Strength)
{
surfaceCategory = "metal";
}
else
{
surfaceCategory = EntityClass.list[hitEntity.entityClass].Properties.Values["SurfaceCategory"];
}
blockFaceParticle = surfaceCategory;
lightValueAtBlockPos = hitEntity.GetLightBrightness();
string hitParticle = string.Format("impact_{0}_on_{1}", _attackingDeviceMadeOf, blockFaceParticle);
string hitSound = (surfaceCategory != null) ? string.Format("{0}hit{1}", _attackingDeviceMadeOf, surfaceCategory) : null;
if (_hitSoundOverrides != null && _hitSoundOverrides.ContainsKey(surfaceCategory))
{
hitSound = _hitSoundOverrides[surfaceCategory];
}
ParticleEffect particleEffect2 = new ParticleEffect(hitParticle, hitInfo.fmcHit.pos, Utils.BlockFaceToRotation(hitInfo.fmcHit.blockFace), lightValueAtBlockPos, blockFaceColor, hitSound, null);
hitPlayer.ServerNetSendRangeCheckedDamage(lookRay.origin, range, damageSourceEntity, finalEntityDamage, isCriticalHit, list_buffs, hitTransformPath, particleEffect2);
}
}
if (isHitTargetPlayer)
{
int damageDealt = hitEntity.DamageEntity(damageSourceEntity, finalEntityDamage, isCriticalHit, 1f);
if (damageDealt != -1 && attackerEntity)
{
MinEventParams attackerMinEventParams = attackerEntity.MinEventContext;
attackerMinEventParams.Other = hitEntityAlive;
attackerMinEventParams.StartPosition = hitInfo.ray.origin;
if (ownedEntityId != -1)
{
launcherValue.FireEvent(MinEventTypes.onSelfAttackedOther, attackerEntity.MinEventContext);
}
attackerEntity.FireEvent(MinEventTypes.onSelfAttackedOther, isHoldingDamageItem);
if (hitEntityAlive && hitEntityAlive.RecordedDamage.Strength > 0)
{
attackerEntity.FireEvent(MinEventTypes.onSelfDamagedOther, isHoldingDamageItem);
}
}
if (!isAlreadyDead && hitEntity.IsDead() && attackerEntity)
{
attackerEntity.FireEvent(MinEventTypes.onSelfKilledOther, isHoldingDamageItem);
}
if (damageDealt != -1 && hitEntityAlive && _buffActions != null && _buffActions.Count > 0)
{
for (int i = 0; i < _buffActions.Count; i++)
{
BuffClass buff = BuffManager.GetBuff(_buffActions[i]);
if (buff != null)
{
float bufProcChance = MultiActionReversePatches.ProjectileGetValue(PassiveEffects.BuffProcChance, null, 1f, attackerEntity, null, FastTags<TagGroup.Global>.Parse(buff.Name), true, false);
if (hitEntityAlive.rand.RandomFloat <= bufProcChance)
{
hitEntityAlive.Buffs.AddBuff(_buffActions[i], attackerEntity.entityId, true, false, -1f);
}
}
}
}
}
}
if (hitEntityAlive && hitEntityAlive.RecordedDamage.ArmorDamage > hitEntityAlive.RecordedDamage.Strength)
{
surfaceCategory = "metal";
}
else
{
surfaceCategory = EntityClass.list[hitEntity.entityClass].Properties.Values["SurfaceCategory"];
}
blockFaceParticle = surfaceCategory;
lightValueAtBlockPos = hitEntity.GetLightBrightness();
EntityPlayer attackerPlayer = attackerEntity as EntityPlayer;
if (attackerPlayer)
{
if (isAlreadyDead && hitEntity.IsDead() && hitEntityAlive && hitEntityAlive.DeathHealth + finalEntityDamage > -1 * EntityClass.list[hitEntity.entityClass].DeadBodyHitPoints)
{
_attackDetails.damageTotalOfTarget = (float)(-1 * hitEntityAlive.DeathHealth);
_attackDetails.damageGiven = deathHealth + Mathf.Min(EntityClass.list[hitEntity.entityClass].DeadBodyHitPoints, Mathf.Abs(hitEntityAlive.DeathHealth));
_attackDetails.damageMax = EntityClass.list[hitEntity.entityClass].DeadBodyHitPoints;
_attackDetails.bKilled = -1 * hitEntityAlive.DeathHealth >= EntityClass.list[hitEntity.entityClass].DeadBodyHitPoints;
_attackDetails.itemsToDrop = EntityClass.list[hitEntity.entityClass].itemsToDrop;
_attackDetails.entityHit = hitEntity;
_attackDetails.materialCategory = surfaceCategory;
}
if (!isAlreadyDead && (hitEntityAlive.IsDead() || hitEntityAlive.Health <= 0) && EntityClass.list.ContainsKey(hitEntity.entityClass))
{
if ((_flags & 2) > 0)
{
float trapXP = MultiActionReversePatches.ProjectileGetValue(PassiveEffects.ElectricalTrapXP, attackerPlayer.inventory.holdingItemItemValue, 0f, attackerPlayer, null, default, true, false);
if (trapXP > 0f)
{
attackerPlayer.AddKillXP(hitEntityAlive, trapXP);
}
}
else
{
attackerPlayer.AddKillXP(hitEntityAlive, 1f);
}
}
}
if (hitEntity is EntityDrone)
{
_attackDetails.entityHit = hitEntity;
}
}
if ((_flags & 8) > 0)
{
canHarvest = false;
}
if (isHitTargetPlayer && _attackMode != ItemActionAttack.EnumAttackMode.Simulate && canHarvest && blockFaceParticle != null && ((_attackDetails.bBlockHit && !_attackDetails.bKilled) || !_attackDetails.bBlockHit))
{
string hitParticle = string.Format("impact_{0}_on_{1}", _attackingDeviceMadeOf, blockFaceParticle);
if (_attackMode == ItemActionAttack.EnumAttackMode.RealAndHarvesting && (_flags & 4) > 0 && ParticleEffect.IsAvailable(hitParticle + "_harvest"))
{
hitParticle += "_harvest";
}
string hitSound = (surfaceCategory != null) ? string.Format("{0}hit{1}", _attackingDeviceMadeOf, surfaceCategory) : null;
if (_hitSoundOverrides != null && _hitSoundOverrides.ContainsKey(surfaceCategory))
{
hitSound = _hitSoundOverrides[surfaceCategory];
}
world.GetGameManager().SpawnParticleEffectServer(new ParticleEffect(hitParticle, hitInfo.fmcHit.pos, Utils.BlockFaceToRotation(hitInfo.fmcHit.blockFace), lightValueAtBlockPos, blockFaceColor, hitSound, null), _attackerEntityId, false, true);
}
if ((_flags & 1) > 0 && attackerEntity != null && attackerEntity.inventory != null)
{
attackerEntity.inventory.CallOnToolbeltChangedInternal();
}
}
private static float calculateHarvestToolDamageBonus(Dictionary<string, ItemActionAttack.Bonuses> _toolBonuses, Dictionary<EnumDropEvent, List<Block.SItemDropProb>> _harvestItems)
{
if (!_harvestItems.ContainsKey(EnumDropEvent.Harvest))
{
return 1f;
}
List<Block.SItemDropProb> list = _harvestItems[EnumDropEvent.Harvest];
for (int i = 0; i < list.Count; i++)
{
if (list[i].toolCategory != null && _toolBonuses.ContainsKey(list[i].toolCategory))
{
return _toolBonuses[list[i].toolCategory].Damage;
}
}
return 1f;
}
public static float GetProjectileDamageBlock(this ItemActionAttack self, ItemValue _itemValue, BlockValue _blockValue, EntityAlive _holdingEntity = null, int actionIndex = 0)
{
FastTags<TagGroup.Global> tmpTag = ((actionIndex != 1) ? ItemActionAttack.PrimaryTag : ItemActionAttack.SecondaryTag);
ItemClass launcherClass = ItemClass.GetForId(_itemValue.Meta);
tmpTag |= ((launcherClass == null) ? ItemActionAttack.MeleeTag : launcherClass.ItemTags);
if (_holdingEntity != null)
{
tmpTag |= _holdingEntity.CurrentStanceTag | _holdingEntity.CurrentMovementTag;
}
tmpTag |= _blockValue.Block.Tags;
float value = MultiActionReversePatches.ProjectileGetValue(PassiveEffects.BlockDamage, _itemValue, self.damageBlock, _holdingEntity, null, tmpTag, true, false)/* * GetProjectileBlockDamagePerc(_itemValue, _holdingEntity)*/;
//Log.Out($"block damage {value} base damage {self.GetBaseDamageBlock(null)} action index {actionIndex} launcher {launcherClass.Name} projectile {_itemValue.ItemClass.Name}");
return value;
}
public static float GetProjectileDamageEntity(this ItemActionAttack self, ItemValue _itemValue, EntityAlive _holdingEntity = null, int actionIndex = 0)
{
FastTags<TagGroup.Global> tmpTag = ((actionIndex != 1) ? ItemActionAttack.PrimaryTag : ItemActionAttack.SecondaryTag);
ItemClass launcherClass = ItemClass.GetForId(_itemValue.Meta);
tmpTag |= ((launcherClass == null) ? ItemActionAttack.MeleeTag : launcherClass.ItemTags);
if (_holdingEntity != null)
{
tmpTag |= _holdingEntity.CurrentStanceTag | _holdingEntity.CurrentMovementTag;
}
var res = MultiActionReversePatches.ProjectileGetValue(PassiveEffects.EntityDamage, _itemValue, self.damageEntity, _holdingEntity, null, tmpTag, true, false)/* * GetProjectileEntityDamagePerc(_itemValue, _holdingEntity)*/;
#if DEBUG
Log.Out($"get projectile damage entity for action index {actionIndex}, item {launcherClass.Name}, result {res}");
#endif
return res;
}
//public static float GetProjectileBlockDamagePerc(ItemValue _itemValue, EntityAlive _holdingEntity)
//{
// float value = MultiActionReversePatches.ProjectileGetValue(CustomEnums.ProjectileImpactDamagePercentBlock, _itemValue, 1, _holdingEntity, null);
// //Log.Out("Block damage perc: " + value);
// return value;
//}
//public static float GetProjectileEntityDamagePerc(ItemValue _itemValue, EntityAlive _holdingEntity)
//{
// float value = MultiActionReversePatches.ProjectileGetValue(CustomEnums.ProjectileImpactDamagePercentEntity, _itemValue, 1, _holdingEntity, null);
// //Log.Out("Entity damage perc: " + value);
// return value;
//}
public static void ProjectileValueModifyValue(this ItemValue _projectileItemValue, EntityAlive _entity, ItemValue _originalItemValue, PassiveEffects _passiveEffect, ref float _originalValue, ref float _perc_value, FastTags<TagGroup.Global> _tags, bool _useMods = true, bool _useDurability = false)
{
if (_originalItemValue != null)
{
Log.Warning($"original item value present: item {_originalItemValue.ItemClass.Name}");
return;
}
int seed = MinEventParams.CachedEventParam.Seed;
if (_entity != null)
{
seed = _entity.MinEventContext.Seed;
}
ItemClass launcherClass = ItemClass.GetForId(_projectileItemValue.Meta);
int actionIndex = _projectileItemValue.SelectedAmmoTypeIndex;
if (launcherClass != null)
{
if (launcherClass.Actions != null && launcherClass.Actions.Length != 0 && launcherClass.Actions[actionIndex] is ItemActionRanged)
{
ItemClass ammoClass = _projectileItemValue.ItemClass;
if (ammoClass != null && ammoClass.Effects != null)
{
ammoClass.Effects.ModifyValue(_entity, _passiveEffect, ref _originalValue, ref _perc_value, 0f, _tags);
}
}
if (launcherClass.Effects != null)
{
MinEventParams.CachedEventParam.Seed = (int)_projectileItemValue.Seed + (int)((_projectileItemValue.Seed != 0) ? _passiveEffect : PassiveEffects.None);
if (_entity != null)
{
_entity.MinEventContext.Seed = MinEventParams.CachedEventParam.Seed;
}
float prevOriginal = _originalValue;
launcherClass.Effects.ModifyValue(_entity, _passiveEffect, ref _originalValue, ref _perc_value, _projectileItemValue.Quality, _tags);
if (_useDurability)
{
float percentUsesLeft = _projectileItemValue.PercentUsesLeft;
switch (_passiveEffect)
{
case PassiveEffects.PhysicalDamageResist:
if (percentUsesLeft < 0.5f)
{
float diff = _originalValue - prevOriginal;
_originalValue = prevOriginal + diff * percentUsesLeft * 2f;
}
break;
case PassiveEffects.ElementalDamageResist:
if (percentUsesLeft < 0.5f)
{
float diff = _originalValue - prevOriginal;
_originalValue = prevOriginal + diff * percentUsesLeft * 2f;
}
break;
case PassiveEffects.BuffResistance:
if (percentUsesLeft < 0.5f)
{
float diff = _originalValue - prevOriginal;
_originalValue = prevOriginal + diff * percentUsesLeft * 2f;
}
break;
}
}
}
}
else
{
Log.Warning($"launcher class not found: item id{_projectileItemValue.Meta}");
}
if (_useMods)
{
for (int i = 0; i < _projectileItemValue.CosmeticMods.Length; i++)
{
if (_projectileItemValue.CosmeticMods[i] != null && _projectileItemValue.CosmeticMods[i].ItemClass is ItemClassModifier && !MultiActionManager.ShouldExcludePassive(_projectileItemValue.type, _projectileItemValue.CosmeticMods[i].type, actionIndex))
{
_projectileItemValue.CosmeticMods[i].ModifyValue(_entity, _projectileItemValue, _passiveEffect, ref _originalValue, ref _perc_value, _tags);
}
}
for (int i = 0; i < _projectileItemValue.Modifications.Length; i++)
{
if (_projectileItemValue.Modifications[i] != null && _projectileItemValue.Modifications[i].ItemClass is ItemClassModifier && !MultiActionManager.ShouldExcludePassive(_projectileItemValue.type, _projectileItemValue.Modifications[i].type, actionIndex))
{
_projectileItemValue.Modifications[i].ModifyValue(_entity, _projectileItemValue, _passiveEffect, ref _originalValue, ref _perc_value, _tags);
}
}
}
}
}
}

View File

@@ -0,0 +1,445 @@
using KFCommonUtilityLib.Scripts.StaticManagers;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace KFCommonUtilityLib.Scripts.Utilities
{
public static class MultiActionUtils
{
public static readonly string[] ActionMetaNames = new string[]
{
"Meta0",
"Meta1",
"Meta2",
"Meta3",
"Meta4",
};
public static readonly string[] ActionSelectedAmmoNames = new string[]
{
"AmmoIndex0",
"AmmoIndex1",
"AmmoIndex2",
"AmmoIndex3",
"AmmoIndex4",
};
public static readonly int ExecutingActionIndexHash = Animator.StringToHash("ExecutingActionIndex");
public static void SetMinEventArrays()
{
MinEvent.Start = new[]
{
MinEventTypes.onSelfPrimaryActionStart,
MinEventTypes.onSelfSecondaryActionStart,
MinEventTypes.onSelfAction2Start,
MinEventTypes.onSelfPrimaryActionStart,
MinEventTypes.onSelfPrimaryActionStart,
};
MinEvent.Update = new[]
{
MinEventTypes.onSelfPrimaryActionUpdate,
MinEventTypes.onSelfSecondaryActionUpdate,
MinEventTypes.onSelfAction2Update,
MinEventTypes.onSelfPrimaryActionUpdate,
MinEventTypes.onSelfPrimaryActionUpdate,
};
MinEvent.End = new[]
{
MinEventTypes.onSelfPrimaryActionEnd,
MinEventTypes.onSelfSecondaryActionEnd,
MinEventTypes.onSelfAction2End,
MinEventTypes.onSelfPrimaryActionEnd,
MinEventTypes.onSelfPrimaryActionEnd,
};
}
//public static int GetActionIndexForItemValue(this ItemValue self)
//{
// object val = self.GetMetadata(MultiActionMapping.STR_MULTI_ACTION_INDEX);
// if (val is int index)
// return index;
// return 0;
//}
public static string GetPropertyName(int index, string prop)
{
return $"Action{index}.{prop}";
}
public static void FixedItemReloadServer(int entityId, int actionIndex)
{
if (GameManager.Instance.World == null)
{
return;
}
FixedItemReloadClient(entityId, actionIndex);
if (!SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer)
{
SingletonMonoBehaviour<ConnectionManager>.Instance.SendToServer(NetPackageManager.GetPackage<NetPackageFixedReload>().Setup(entityId, actionIndex), false);
return;
}
SingletonMonoBehaviour<ConnectionManager>.Instance.SendPackage(NetPackageManager.GetPackage<NetPackageFixedReload>().Setup(entityId, actionIndex), false, -1, entityId);
}
public static void FixedItemReloadClient(int entityId, int actionIndex)
{
if (GameManager.Instance.World == null)
{
return;
}
EntityAlive entityAlive = (EntityAlive)GameManager.Instance.World.GetEntity(entityId);
if (entityAlive != null && entityAlive.inventory.holdingItem.Actions[actionIndex] is ItemActionRanged actionRanged)
{
entityAlive.MinEventContext.ItemActionData = entityAlive.inventory.holdingItemData.actionData[actionIndex];
actionRanged.ReloadGun(entityAlive.inventory.holdingItemData.actionData[actionIndex]);
}
}
public static void CopyLauncherValueToProjectile(ItemValue launcherValue, ItemValue projectileValue, int index)
{
projectileValue.Meta = launcherValue.type;
projectileValue.SelectedAmmoTypeIndex = (byte)index;
projectileValue.UseTimes = launcherValue.UseTimes;
projectileValue.Quality = launcherValue.Quality;
projectileValue.Modifications = new ItemValue[launcherValue.Modifications.Length];
Array.Copy(launcherValue.Modifications, projectileValue.Modifications, launcherValue.Modifications.Length);
projectileValue.CosmeticMods = new ItemValue[launcherValue.CosmeticMods.Length];
Array.Copy(launcherValue.CosmeticMods, projectileValue.CosmeticMods, launcherValue.CosmeticMods.Length);
}
public static void SetMinEventParamsActionData(EntityAlive entity, int actionIndex)
{
if (actionIndex >= 0 && actionIndex < entity.inventory?.holdingItemData?.actionData?.Count)
entity.MinEventContext.ItemActionData = entity.inventory.holdingItemData.actionData[actionIndex];
}
public static string GetPropertyOverrideForAction(this ItemValue self, string _propertyName, string _originalValue, int actionIndex)
{
if (self.Modifications.Length == 0 && self.CosmeticMods.Length == 0)
{
return _originalValue;
}
string text = "";
ItemClass item = self.ItemClass;
for (int i = 0; i < self.Modifications.Length; i++)
{
ItemValue itemValue = self.Modifications[i];
if (itemValue != null)
{
if (itemValue.ItemClass is ItemClassModifier mod && GetPropertyOverrideForMod(mod, _propertyName, item, ref text, actionIndex))
{
return text;
}
}
}
text = "";
for (int j = 0; j < self.CosmeticMods.Length; j++)
{
ItemValue itemValue2 = self.CosmeticMods[j];
if (itemValue2 != null)
{
if (itemValue2.ItemClass is ItemClassModifier cos && GetPropertyOverrideForMod(cos, _propertyName, item, ref text, actionIndex))
{
return text;
}
}
}
return _originalValue;
}
public static IEnumerable<string> GetAllPropertyOverridesForAction(this ItemValue self, string _propertyName, int actionIndex)
{
if (self.Modifications.Length == 0 && self.CosmeticMods.Length == 0)
{
yield break;
}
string text = "";
ItemClass item = self.ItemClass;
for (int i = 0; i < self.Modifications.Length; i++)
{
ItemValue itemValue = self.Modifications[i];
if (itemValue != null)
{
if (itemValue.ItemClass is ItemClassModifier mod && GetPropertyOverrideForMod(mod, _propertyName, item, ref text, actionIndex))
{
yield return text;
}
}
}
text = "";
for (int j = 0; j < self.CosmeticMods.Length; j++)
{
ItemValue itemValue2 = self.CosmeticMods[j];
if (itemValue2 != null)
{
if (itemValue2.ItemClass is ItemClassModifier cos && GetPropertyOverrideForMod(cos, _propertyName, item, ref text, actionIndex))
{
yield return text;
}
}
}
}
public static bool GetPropertyOverrideForMod(ItemClassModifier mod, string _propertyName, ItemClass _item, ref string _value, int actionIndex)
{
//Log.Out($"Get property override for item {_item.Name} itemID{_item.Id} property {_propertyName} mod {mod.Name} modID {mod.Id} action {actionIndex} should exclude {MultiActionManager.ShouldExcludeMod(_item.Id, mod.Id, actionIndex)}");
if (MultiActionManager.ShouldExcludeProperty(_item.Id, mod.Id, actionIndex))
return false;
string _itemName = _item.GetItemName();
string itemNameWithActionIndex = $"{_itemName}_{actionIndex}";
if (mod.PropertyOverrides.ContainsKey(itemNameWithActionIndex) && mod.PropertyOverrides[itemNameWithActionIndex].Values.ContainsKey(_propertyName))
{
_value = mod.PropertyOverrides[itemNameWithActionIndex].Values[_propertyName];
return true;
}
if (mod.PropertyOverrides.ContainsKey(_itemName) && mod.PropertyOverrides[_itemName].Values.ContainsKey(_propertyName))
{
_value = mod.PropertyOverrides[_itemName].Values[_propertyName];
return true;
}
itemNameWithActionIndex = $"*_{actionIndex}";
if (mod.PropertyOverrides.ContainsKey(itemNameWithActionIndex) && mod.PropertyOverrides[itemNameWithActionIndex].Values.ContainsKey(_propertyName))
{
_value = mod.PropertyOverrides[itemNameWithActionIndex].Values[_propertyName];
return true;
}
if (mod.PropertyOverrides.ContainsKey("*") && mod.PropertyOverrides["*"].Values.ContainsKey(_propertyName))
{
_value = mod.PropertyOverrides["*"].Values[_propertyName];
return true;
}
return false;
}
public static int GetMode(this ItemValue self)
{
object mode = self.GetMetadata(MultiActionMapping.STR_MULTI_ACTION_INDEX);
if (mode is int v)
{
return v;
}
return 0;
}
public static int GetActionIndexByEntityEventParams(EntityAlive entity)
{
return GetActionIndexByEventParams(entity?.MinEventContext ?? MinEventParams.CachedEventParam);
}
public static int GetActionIndexByEventParams(MinEventParams pars)
{
if (pars?.ItemActionData == null)
return 0;
return pars.ItemActionData.indexInEntityOfAction;
}
public static int GetActionIndexByMetaData(this ItemValue self)
{
int mode = self.GetMode();
MultiActionIndice indice = MultiActionManager.GetActionIndiceForItemID(self.type);
return indice.GetActionIndexForMode(mode);
}
public static int GetSelectedAmmoIndexByMode(this ItemValue self, int mode)
{
MultiActionIndice indice = MultiActionManager.GetActionIndiceForItemID(self.type);
int metaIndex = indice.GetMetaIndexForMode(mode);
if (metaIndex >= 0)
{
object ammoIndex = self.GetMetadata(ActionSelectedAmmoNames[metaIndex]);
if (ammoIndex is int)
{
return (int)ammoIndex;
}
}
return self.SelectedAmmoTypeIndex;
}
public static int GetMetaByMode(this ItemValue self, int mode)
{
MultiActionIndice indice = MultiActionManager.GetActionIndiceForItemID(self.type);
int metaIndex = indice.GetMetaIndexForMode(mode);
if (metaIndex >= 0)
{
object meta = self.GetMetadata(ActionMetaNames[metaIndex]);
if (meta is int)
{
return (int)meta;
}
}
return self.Meta;
}
public static int GetSelectedAmmoIndexByActionIndex(this ItemValue self, int actionIndex)
{
MultiActionIndice indice = MultiActionManager.GetActionIndiceForItemID(self.type);
int mode = indice.GetModeForAction(actionIndex);
if (mode < 0)
return self.SelectedAmmoTypeIndex;
int metaIndex = indice.GetMetaIndexForMode(mode);
if (metaIndex >= 0)
{
object ammoIndex = self.GetMetadata(ActionSelectedAmmoNames[metaIndex]);
if (ammoIndex is int)
{
return (int)ammoIndex;
}
}
return self.SelectedAmmoTypeIndex;
}
public static int GetMetaByActionIndex(this ItemValue self, int actionIndex)
{
MultiActionIndice indice = MultiActionManager.GetActionIndiceForItemID(self.type);
int mode = indice.GetModeForAction(actionIndex);
if (mode < 0)
return self.Meta;
int metaIndex = indice.GetMetaIndexForMode(mode);
if (metaIndex >= 0)
{
object meta = self.GetMetadata(ActionMetaNames[metaIndex]);
if (meta is int)
{
//Log.Out($"GetMetaByActionIndex: mode: {mode}, action: {metaIndex}, meta: {(int)meta}\n{StackTraceUtility.ExtractStackTrace()}");
return (int)meta;
}
}
return self.Meta;
}
public static void SetMinEventParamsByEntityInventory(EntityAlive entity)
{
if (entity != null && entity.MinEventContext != null)
{
entity.MinEventContext.ItemActionData = entity.inventory?.holdingItemData?.actionData[MultiActionManager.GetActionIndexForEntity(entity)];
}
}
public static bool MultiActionRemoveAmmoFromItemStack(ItemStack stack, List<ItemStack> result)
{
ItemValue itemValue = stack.itemValue;
object mode = itemValue.GetMetadata(MultiActionMapping.STR_MULTI_ACTION_INDEX);
if (mode is false || mode is null)
{
return false;
}
MultiActionIndice indices = MultiActionManager.GetActionIndiceForItemID(itemValue.type);
ItemClass item = ItemClass.GetForId(itemValue.type);
for (int i = 0; i < MultiActionIndice.MAX_ACTION_COUNT; i++)
{
int metaIndex = indices.GetMetaIndexForMode(i);
if (metaIndex < 0)
{
break;
}
int actionIndex = indices.GetActionIndexForMode(i);
if (item.Actions[actionIndex] is ItemActionRanged ranged && !(ranged is ItemActionTextureBlock))
{
object meta = itemValue.GetMetadata(MultiActionUtils.ActionMetaNames[metaIndex]);
object ammoIndex = itemValue.GetMetadata(MultiActionUtils.ActionSelectedAmmoNames[metaIndex]);
if (meta is int && ammoIndex is int && (int)meta > 0)
{
itemValue.SetMetadata(MultiActionUtils.ActionMetaNames[metaIndex], 0, TypedMetadataValue.TypeTag.Integer);
ItemStack ammoStack = new ItemStack(ItemClass.GetItem(ranged.MagazineItemNames[(int)ammoIndex]), (int)meta);
result.Add(ammoStack);
Log.Out($"Remove ammo: metadata {MultiActionUtils.ActionMetaNames[metaIndex]}, meta {(int)meta}, left {itemValue.GetMetadata(MultiActionUtils.ActionMetaNames[metaIndex])}");
}
}
}
itemValue.Meta = 0;
return true;
}
public static readonly ItemActionData[] DummyActionDatas = new ItemActionData[]
{
new ItemActionData(null, 0),
new ItemActionData(null, 3),
new ItemActionData(null, 4)
};
public static int GetMultiActionInitialMetaData(this ItemClass itemClass, ItemValue itemValue)
{
MultiActionIndice indice = MultiActionManager.GetActionIndiceForItemID(itemClass.Id);
if (indice.modeCount <= 1)
{
return itemClass.GetInitialMetadata(itemValue);
}
var prevItemValue = MinEventParams.CachedEventParam.ItemValue;
var prevActionData = MinEventParams.CachedEventParam.ItemActionData;
MinEventParams.CachedEventParam.ItemValue = itemValue;
itemValue.SetMetadata(MultiActionMapping.STR_MULTI_ACTION_INDEX, 0, TypedMetadataValue.TypeTag.Integer);
int ret = 0;
for (int i = 0; i < indice.modeCount; i++)
{
MinEventParams.CachedEventParam.ItemActionData = DummyActionDatas[i];
ItemAction itemAction = itemClass.Actions[indice.GetActionIndexForMode(i)];
int meta = itemAction.GetInitialMeta(itemValue);
if (i == 0)
{
ret = meta;
}
else if (itemAction.Properties.Contains("ActionUnlocked") && !itemAction.Properties.GetBool("ActionUnlocked"))
{
meta = 0;
}
if (indice.GetMetaIndexForMode(i) == indice.GetActionIndexForMode(i))
itemValue.SetMetadata(MultiActionUtils.ActionMetaNames[indice.GetMetaIndexForMode(i)], meta, TypedMetadataValue.TypeTag.Integer);
}
MinEventParams.CachedEventParam.ItemValue = prevItemValue;
MinEventParams.CachedEventParam.ItemActionData = prevActionData;
return ret;
}
public static void SetCachedEventParamsDummyAction(ItemStack itemStack)
{
ItemClass itemClass = itemStack?.itemValue?.ItemClass;
if (itemClass != null)
{
MinEventParams.CachedEventParam.ItemActionData = MultiActionUtils.DummyActionDatas[itemStack.itemValue.GetMode()];
MinEventParams.CachedEventParam.ItemValue = itemStack.itemValue;
MinEventParams.CachedEventParam.Seed = itemStack.itemValue.Seed;
}
}
public static string GetDisplayTypeForAction(ItemStack itemStack)
{
return GetDisplayTypeForAction(itemStack?.itemValue);
}
public static string GetDisplayTypeForAction(ItemValue itemValue)
{
if (itemValue == null || itemValue.IsEmpty())
{
return "";
}
if (itemValue.ItemClass.Actions[itemValue.GetActionIndexByMetaData()] is IModuleContainerFor<ActionModuleMultiActionFix> module)
{
return module.Instance.GetDisplayType(itemValue);
}
return itemValue.ItemClass.DisplayType;
}
public static bool CanCompare(ItemValue itemValue1, ItemValue itemValue2)
{
if (itemValue1 == null || itemValue2 == null || itemValue1.IsEmpty() || itemValue2.IsEmpty())
{
return false;
}
string displayType1 = itemValue1.ItemClass.IsBlock() ? Block.list[itemValue1.ItemClass.Id].DisplayType : GetDisplayTypeForAction(itemValue1);
string displayType2 = itemValue2.ItemClass.IsBlock() ? Block.list[itemValue2.ItemClass.Id].DisplayType : GetDisplayTypeForAction(itemValue2);
ItemDisplayEntry displayStatsForTag = UIDisplayInfoManager.Current.GetDisplayStatsForTag(displayType1);
ItemDisplayEntry displayStatsForTag2 = UIDisplayInfoManager.Current.GetDisplayStatsForTag(displayType2);
return displayStatsForTag != null && displayStatsForTag2 != null && displayStatsForTag.DisplayGroup == displayStatsForTag2.DisplayGroup;
}
}
}

View File

@@ -0,0 +1,134 @@
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
public class SaveTextureToFileUtility
{
public enum SaveTextureFileFormat
{
EXR, JPG, PNG, TGA
};
/// <summary>
/// Saves a Texture2D to disk with the specified filename and image format
/// </summary>
/// <param name="tex"></param>
/// <param name="filePath"></param>
/// <param name="fileFormat"></param>
/// <param name="jpgQuality"></param>
static public void SaveTexture2DToFile(Texture2D tex, string filePath, SaveTextureFileFormat fileFormat, int jpgQuality = 95)
{
switch (fileFormat)
{
case SaveTextureFileFormat.EXR:
System.IO.File.WriteAllBytes(filePath + ".exr", tex.EncodeToEXR());
break;
case SaveTextureFileFormat.JPG:
System.IO.File.WriteAllBytes(filePath + ".jpg", tex.EncodeToJPG(jpgQuality));
break;
case SaveTextureFileFormat.PNG:
System.IO.File.WriteAllBytes(filePath + ".png", tex.EncodeToPNG());
break;
case SaveTextureFileFormat.TGA:
System.IO.File.WriteAllBytes(filePath + ".tga", tex.EncodeToTGA());
break;
}
}
/// <summary>
/// Saves a RenderTexture to disk with the specified filename and image format
/// </summary>
/// <param name="renderTexture"></param>
/// <param name="filePath"></param>
/// <param name="fileFormat"></param>
/// <param name="jpgQuality"></param>
static public void SaveRenderTextureToFile(RenderTexture renderTexture, string filePath, SaveTextureFileFormat fileFormat = SaveTextureFileFormat.PNG, int jpgQuality = 95)
{
Texture2D tex;
if (fileFormat != SaveTextureFileFormat.EXR)
tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.ARGB32, false, false);
else
tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBAFloat, false, true);
var oldRt = RenderTexture.active;
RenderTexture.active = renderTexture;
tex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
tex.Apply();
RenderTexture.active = oldRt;
SaveTexture2DToFile(tex, filePath, fileFormat, jpgQuality);
if (Application.isPlaying)
Object.Destroy(tex);
else
Object.DestroyImmediate(tex);
}
static public void SaveTextureToFile(Texture source,
string filePath,
int width,
int height,
SaveTextureFileFormat fileFormat = SaveTextureFileFormat.PNG,
int jpgQuality = 95,
bool asynchronous = true,
System.Action<bool> done = null)
{
// check that the input we're getting is something we can handle:
if (!(source is Texture2D || source is RenderTexture))
{
done?.Invoke(false);
return;
}
// use the original texture size in case the input is negative:
if (width < 0 || height < 0)
{
width = source.width;
height = source.height;
}
// resize the original image:
var resizeRT = RenderTexture.GetTemporary(width, height, 0);
Graphics.Blit(source, resizeRT);
// create a native array to receive data from the GPU:
var narray = new NativeArray<byte>(width * height * 4, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
// request the texture data back from the GPU:
var request = AsyncGPUReadback.RequestIntoNativeArray(ref narray, resizeRT, 0, (AsyncGPUReadbackRequest req) =>
{
// if the readback was successful, encode and write the results to disk
if (!req.hasError)
{
NativeArray<byte> encoded;
switch (fileFormat)
{
case SaveTextureFileFormat.EXR:
encoded = ImageConversion.EncodeNativeArrayToEXR(narray, resizeRT.graphicsFormat, (uint)width, (uint)height);
break;
case SaveTextureFileFormat.JPG:
encoded = ImageConversion.EncodeNativeArrayToJPG(narray, resizeRT.graphicsFormat, (uint)width, (uint)height, 0, jpgQuality);
break;
case SaveTextureFileFormat.TGA:
encoded = ImageConversion.EncodeNativeArrayToTGA(narray, resizeRT.graphicsFormat, (uint)width, (uint)height);
break;
default:
encoded = ImageConversion.EncodeNativeArrayToPNG(narray, resizeRT.graphicsFormat, (uint)width, (uint)height);
break;
}
System.IO.File.WriteAllBytes(filePath, encoded.ToArray());
encoded.Dispose();
}
narray.Dispose();
// notify the user that the operation is done, and its outcome.
done?.Invoke(!req.hasError);
});
if (!asynchronous)
request.WaitForCompletion();
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace KFCommonUtilityLib
{
public class TemporaryMuzzleFlash : TemporaryObject
{
private void OnDisable()
{
StopAllCoroutines();
if (destroyMaterials)
{
Utils.CleanupMaterialsOfRenderers<Renderer[]>(transform.GetComponentsInChildren<Renderer>());
}
Destroy(gameObject);
}
}
}

View File

@@ -0,0 +1,5 @@
public class TypeBasedUID<T>
{
private static int uid = 0;
public static int UID { get => uid++; }
}