Decompiled source of RoR2Enemies v0.3.0
plugins/MuckRor2Enemies.dll
Decompiled 20 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.AI; using UnityEngine.Rendering; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("MuckRor2Enemies")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Risk of Rain 2 enemies as variants of vanilla Muck mobs. Host-authoritative; vanilla clients can join.")] [assembly: AssemblyFileVersion("0.3.0.0")] [assembly: AssemblyInformationalVersion("0.3.0+879fc7ac66fec639f2f84b4b7a65d5c61c9ed734")] [assembly: AssemblyProduct("RoR2 Enemies")] [assembly: AssemblyTitle("MuckRor2Enemies")] [assembly: InternalsVisibleTo("MuckRor2Enemies.Tests")] [assembly: AssemblyVersion("0.3.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } } namespace MuckRor2Enemies { internal static class ConfigFingerprint { public static ulong Of(RulesSnapshot rules) { ulong hash = 0uL; foreach (KindSpec item in KindRegistry.All) { KindRules value = default(KindRules); if (rules != null && rules.Kinds != null) { rules.Kinds.TryGetValue(item.Kind, out value); } hash = Mix(hash, (ulong)(value.Enabled ? 1 : 0)); hash = Mix(hash, (ulong)value.FirstDay); hash = Mix(hash, Bits(value.Chance)); hash = Mix(hash, (ulong)value.MaxAlive); hash = Mix(hash, (ulong)value.Sources); } float value2 = rules?.TitanShrineChance ?? 0f; return Mix(hash, Bits(value2)); } private static ulong Mix(ulong hash, ulong value) { return Hash01.SplitMix64(hash ^ value); } private static ulong Bits(float value) { return BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); } } internal static class DamageTuning { public static int ProjectileDamage(int attackDamage, float multiplier) { return (int)((float)attackDamage * multiplier); } public static int FxDamage(int baseDamage, float multiplier) { return (int)((float)baseDamage * multiplier); } public static int HostScaled(int damage, float mobDamageMultiplier) { return (int)((float)damage * mobDamageMultiplier); } public static float MeleeRefineMultiplier(float mobMultiplier, float damageScale, float meleeFactor) { return mobMultiplier / damageScale * meleeFactor; } public static int MaxHp(int prefabHp, float hpMult, float multiplier, float bossMultiplier) { return (int)((float)prefabHp * hpMult * multiplier * bossMultiplier); } } internal enum Difficulty { Easy, Normal, Gamer } internal static class DifficultyCurve { public static float MobHpMultiplier(Difficulty d, int day) { Coeff(d, hp: true, out var a, out var b, out var p); return Curve(a, b, p, day); } public static float MobDamageMultiplier(Difficulty d, int day) { Coeff(d, hp: false, out var a, out var b, out var p); return Curve(a, b, p, day); } public static float BossNightMultiplier(int playersAlive) { return 0.85f + 0.15f * (float)playersAlive; } public static float ShrineBossMultiplier(int playersAlive) { return 0.9f + 0.1f * (float)playersAlive; } public static int BossDay(Difficulty d) { return d switch { Difficulty.Easy => 6, Difficulty.Normal => 4, Difficulty.Gamer => 3, _ => 5, }; } public static float BuffChance(int day) { float num = 0.01f * (float)day; if (num < 0.05f) { num = 0.05f; } if (num > 0.3f) { num = 0.3f; } return 0.01f + num; } private static float Curve(float a, float b, float p, int day) { float num = b * (float)day; return a + (float)Math.Pow(num, p); } private static void Coeff(Difficulty d, bool hp, out float a, out float b, out float p) { switch (d) { case Difficulty.Easy: if (hp) { a = 0.9f; b = 0.2f; p = 1.32f; } else { a = 0.35f; b = 0.2f; p = 1.33f; } break; case Difficulty.Gamer: if (hp) { a = 1.3f; b = 0.3f; p = 1.8f; } else { a = 1.75f; b = 0.25f; p = 2.3f; } break; default: if (hp) { a = 1.05f; b = 0.24f; p = 1.5f; } else { a = 0.9f; b = 0.26f; p = 1.55f; } break; } } } internal static class EnemyArt { public const string JellyBell = "Hip__bell"; public const string TitanEye = "Head__eye"; private static readonly Dictionary<string, MaterialSpec> Empty = new Dictionary<string, MaterialSpec>(); private static readonly Dictionary<string, MaterialSpec> Lemurian = Palette(Entry("lem_scale", 0.45f, 0.33f, 0.62f, 0f), Entry("lem_belly", 0.88f, 0.8f, 0.62f, 0f), Entry("lem_dark", 0.14f, 0.11f, 0.16f, 0f), Entry("lem_mouth", 0.7f, 0.16f, 0.16f, 0f), Entry("lem_eye", 1f, 0.55f, 0.1f, 1.5f), Entry("lem_scale_dark", 0.33f, 0.23f, 0.47f, 0f)); private static readonly Dictionary<string, MaterialSpec> Jellyfish = Palette(Entry("jel_bell", 0.55f, 0.86f, 0.96f, 0.5f), Entry("jel_rim", 0.32f, 0.62f, 0.9f, 0.3f), Entry("jel_core", 0.95f, 0.98f, 1f, 2f), Entry("jel_tentacle", 0.45f, 0.75f, 0.95f, 0.3f), Entry("jel_frill", 0.72f, 0.93f, 1f, 0.9f)); private static readonly Dictionary<string, MaterialSpec> Wisp = Palette(Entry("wisp_mask", 0.92f, 0.89f, 0.8f, 0f), Entry("wisp_dark", 0.2f, 0.17f, 0.15f, 0f), Entry("wisp_flame", 1f, 0.52f, 0.12f, 2f), Entry("wisp_core", 1f, 0.86f, 0.4f, 3f), Entry("wisp_ember", 0.98f, 0.36f, 0.07f, 2f)); private static readonly Dictionary<string, MaterialSpec> Titan = Palette(Entry("tit_stone", 0.46f, 0.45f, 0.42f, 0f), Entry("tit_dark", 0.27f, 0.27f, 0.28f, 0f), Entry("tit_moss", 0.34f, 0.5f, 0.22f, 0f), Entry("tit_eye", 1f, 0.18f, 0.1f, 3f), Entry("tit_rune", 0.95f, 0.42f, 0.18f, 1.2f), Entry("tit_stone_hi", 0.56f, 0.55f, 0.51f, 0f)); public static string Asset(EnemyKind kind) { return kind switch { EnemyKind.Lemurian => "lemurian", EnemyKind.Jellyfish => "jellyfish", EnemyKind.LesserWisp => "lesser_wisp", EnemyKind.StoneTitan => "stone_titan", _ => null, }; } public static string ObjResource(string asset) { return "MuckRor2Enemies.art." + asset + ".obj"; } public static string PartsResource(string asset) { return "MuckRor2Enemies.art." + asset + ".parts.json"; } public static EnemyModel Load(EnemyKind kind, Func<string, string> read, out string error) { error = null; string text = Asset(kind); if (text == null) { return null; } try { EnemyModel enemyModel = EnemyModel.Parse(read(ObjResource(text)), read(PartsResource(text))); Validate(kind, enemyModel); return enemyModel; } catch (Exception ex) { error = ex.Message; return null; } } private static void Validate(EnemyKind kind, EnemyModel model) { if (!KindRegistry.TryGet((int)kind, out var spec)) { throw new FormatException($"{model.Name}: {kind} has no kind row."); } IReadOnlyList<string> names = MobRigs.Bones(spec.Rig); IReadOnlyDictionary<string, MaterialSpec> readOnlyDictionary = Materials(kind); foreach (EnemyPart part in model.Parts) { if (!Contains(names, part.Bone)) { throw new FormatException($"{model.Name}: part {part.Object} rides {part.Bone}, which the {spec.Rig} rig lacks."); } if (!readOnlyDictionary.ContainsKey(part.Material)) { throw new FormatException(model.Name + ": part " + part.Object + " uses " + part.Material + ", which the palette lacks."); } } int num = TriangleBudget(kind); if (model.TriangleCount > num) { throw new FormatException($"{model.Name}: {model.TriangleCount} triangles, over the budget of {num}."); } } private static bool Contains(IReadOnlyList<string> names, string name) { for (int i = 0; i < names.Count; i++) { if (names[i] == name) { return true; } } return false; } public static IReadOnlyDictionary<string, MaterialSpec> Materials(EnemyKind kind) { return kind switch { EnemyKind.Lemurian => Lemurian, EnemyKind.Jellyfish => Jellyfish, EnemyKind.LesserWisp => Wisp, EnemyKind.StoneTitan => Titan, _ => Empty, }; } public static IReadOnlyList<string> HiddenRenderers(EnemyKind kind) { if (KindRegistry.TryGet((int)kind, out var spec) && spec.HiddenRenderers != null) { return spec.HiddenRenderers; } return Array.Empty<string>(); } public static int TriangleBudget(EnemyKind kind) { return kind switch { EnemyKind.Lemurian => 1400, EnemyKind.Jellyfish => 1100, EnemyKind.LesserWisp => 1000, EnemyKind.StoneTitan => 2000, _ => 0, }; } private static Dictionary<string, MaterialSpec> Palette(params KeyValuePair<string, MaterialSpec>[] entries) { Dictionary<string, MaterialSpec> dictionary = new Dictionary<string, MaterialSpec>(); for (int i = 0; i < entries.Length; i++) { dictionary[entries[i].Key] = entries[i].Value; } return dictionary; } private static KeyValuePair<string, MaterialSpec> Entry(string name, float r, float g, float b, float emission) { return new KeyValuePair<string, MaterialSpec>(name, new MaterialSpec { R = r, G = g, B = b, Emission = emission }); } } internal sealed class EnemyPart { public readonly string Object; public readonly string Bone; public readonly string Material; public readonly Vec3 Pivot; public readonly MeshData Mesh; public int TriangleCount => Mesh.Positions.Length / 3; public EnemyPart(string obj, string bone, string material, Vec3 pivot, MeshData mesh) { Object = obj; Bone = bone; Material = material; Pivot = pivot; Mesh = mesh; } } internal sealed class EnemyModel { private sealed class ObjObject { public readonly List<Vec3> Positions = new List<Vec3>(); public readonly List<Vec3> Normals = new List<Vec3>(); public readonly HashSet<string> Materials = new HashSet<string>(); } public readonly string Name; public readonly string Rig; public readonly float RootScale; public readonly IReadOnlyList<EnemyPart> Parts; public readonly IReadOnlyDictionary<string, MaterialSpec> Materials; public int TriangleCount { get { int num = 0; for (int i = 0; i < Parts.Count; i++) { num += Parts[i].TriangleCount; } return num; } } private EnemyModel(string name, string rig, float rootScale, IReadOnlyList<EnemyPart> parts, IReadOnlyDictionary<string, MaterialSpec> materials) { Name = name; Rig = rig; RootScale = rootScale; Parts = parts; Materials = materials; } public EnemyPart Find(string objectName) { for (int i = 0; i < Parts.Count; i++) { if (Parts[i].Object == objectName) { return Parts[i]; } } return null; } public static EnemyModel Parse(string objText, string partsJson) { if (!(JsonReader.Parse(partsJson) is Dictionary<string, object> obj)) { throw new FormatException("parts.json: top level is not an object."); } string text = Str(obj, "name"); Dictionary<string, MaterialSpec> materials = ReadMaterials(Obj(obj, "materials")); Dictionary<string, ObjObject> dictionary = ReadObj(objText, text); List<EnemyPart> list = new List<EnemyPart>(); HashSet<string> hashSet = new HashSet<string>(); foreach (object item in Arr(obj, "parts")) { if (!(item is Dictionary<string, object> dictionary2)) { throw new FormatException(text + ": a part is not an object."); } string text2 = Str(dictionary2, "object"); if (!dictionary.TryGetValue(text2, out var value)) { throw new FormatException(text + ": part " + text2 + " is missing from the OBJ."); } if (!hashSet.Add(text2)) { throw new FormatException(text + ": part " + text2 + " is listed twice."); } list.Add(BuildPart(text, text2, dictionary2, value, materials)); } foreach (string key in dictionary.Keys) { if (!hashSet.Contains(key)) { throw new FormatException(text + ": OBJ object " + key + " is not in parts.json."); } } EnemyModel enemyModel = new EnemyModel(text, Str(obj, "rig"), (float)Num(obj, "root_scale"), list, materials); int num = (int)Num(obj, "tris"); if (enemyModel.TriangleCount != num) { throw new FormatException($"{text}: OBJ has {enemyModel.TriangleCount} triangles, parts.json says {num}."); } return enemyModel; } private static EnemyPart BuildPart(string name, string obj, Dictionary<string, object> entry, ObjObject mesh, Dictionary<string, MaterialSpec> materials) { List<object> list = Arr(entry, "materials"); if (list.Count != 1 || !(list[0] is string text)) { throw new FormatException(name + ": part " + obj + " must list exactly one material."); } if (!materials.ContainsKey(text)) { throw new FormatException(name + ": part " + obj + " uses undefined material '" + text + "'."); } if (mesh.Materials.Count != 1 || !mesh.Materials.Contains(text)) { throw new FormatException(name + ": part " + obj + " faces use " + string.Join(", ", mesh.Materials) + ", parts.json says " + text + "."); } int num = mesh.Positions.Count / 3; int num2 = (int)Num(entry, "tris"); if (num != num2) { throw new FormatException($"{name}: part {obj} has {num} triangles, parts.json says {num2}."); } Vec3 pivot = ReadVec3(entry, "pivot_unity"); Vec3[] positions = mesh.Positions.ToArray(); return new EnemyPart(obj, Str(entry, "bone"), text, pivot, new MeshData(BoundsCentre(positions), positions, mesh.Normals.ToArray())); } private static Vec3 BoundsCentre(Vec3[] positions) { if (positions.Length == 0) { return new Vec3(0f, 0f, 0f); } Vec3 vec = positions[0]; Vec3 vec2 = positions[0]; for (int i = 0; i < positions.Length; i++) { Vec3 vec3 = positions[i]; vec = new Vec3(Math.Min(vec.X, vec3.X), Math.Min(vec.Y, vec3.Y), Math.Min(vec.Z, vec3.Z)); vec2 = new Vec3(Math.Max(vec2.X, vec3.X), Math.Max(vec2.Y, vec3.Y), Math.Max(vec2.Z, vec3.Z)); } return (vec + vec2) * 0.5f; } private static Dictionary<string, ObjObject> ReadObj(string text, string name) { if (text == null) { throw new FormatException(name + ": OBJ text is null."); } List<Vec3> list = new List<Vec3>(); List<Vec3> list2 = new List<Vec3>(); Dictionary<string, ObjObject> dictionary = new Dictionary<string, ObjObject>(); ObjObject objObject = null; string text2 = null; string[] array = text.Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (array2.Length == 0) { continue; } switch (array2[0]) { case "v": { Vec3 vec2 = ReadVec(array2, name, i); list.Add(new Vec3(0f - vec2.X, vec2.Y, vec2.Z)); break; } case "vn": { Vec3 vec = ReadVec(array2, name, i); list2.Add(new Vec3(0f - vec.X, vec.Y, vec.Z)); break; } case "o": { string text3 = string.Join(" ", array2, 1, array2.Length - 1); if (dictionary.ContainsKey(text3)) { throw new FormatException(name + ": OBJ object " + text3 + " appears twice."); } objObject = new ObjObject(); dictionary.Add(text3, objObject); text2 = null; break; } case "usemtl": text2 = ((array2.Length > 1) ? array2[1] : null); break; case "f": { if (objObject == null || text2 == null) { throw new FormatException($"{name}: OBJ line {i + 1} has a face outside an object or material."); } if (array2.Length != 4) { throw new FormatException($"{name}: OBJ line {i + 1} is not a triangle."); } objObject.Materials.Add(text2); int[] array3 = new int[3] { 1, 3, 2 }; foreach (int num in array3) { string[] array4 = array2[num].Split(new char[1] { '/' }); if (array4.Length < 3 || array4[2].Length == 0) { throw new FormatException($"{name}: OBJ line {i + 1} has a corner without a normal."); } objObject.Positions.Add(list[Index(array4[0], list.Count, name, i)]); objObject.Normals.Add(list2[Index(array4[2], list2.Count, name, i)]); } break; } } } return dictionary; } private static int Index(string raw, int count, string name, int line) { if (!int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result == 0) { throw new FormatException($"{name}: OBJ line {line + 1} has a bad index '{raw}'."); } int num = ((result > 0) ? (result - 1) : (count + result)); if (num < 0 || num >= count) { throw new FormatException($"{name}: OBJ line {line + 1} index {raw} is out of range."); } return num; } private static Vec3 ReadVec(string[] tokens, string name, int line) { if (tokens.Length < 4) { throw new FormatException($"{name}: OBJ line {line + 1} needs three numbers."); } return new Vec3(Float(tokens[1], name, line), Float(tokens[2], name, line), Float(tokens[3], name, line)); } private static float Float(string raw, string name, int line) { if (!float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw new FormatException($"{name}: OBJ line {line + 1} has a bad number '{raw}'."); } return result; } private static Dictionary<string, MaterialSpec> ReadMaterials(Dictionary<string, object> raw) { Dictionary<string, MaterialSpec> dictionary = new Dictionary<string, MaterialSpec>(); foreach (KeyValuePair<string, object> item in raw) { if (!(item.Value is Dictionary<string, object> obj)) { throw new FormatException("parts.json: material " + item.Key + " is not an object."); } Vec3 vec = ReadVec3(obj, "rgb"); dictionary.Add(item.Key, new MaterialSpec { R = vec.X, G = vec.Y, B = vec.Z, Emission = (float)Num(obj, "emission") }); } return dictionary; } private static Vec3 ReadVec3(Dictionary<string, object> obj, string key) { List<object> list = Arr(obj, key); if (list.Count != 3 || !(list[0] is double num) || !(list[1] is double num2) || !(list[2] is double num3)) { throw new FormatException("parts.json: '" + key + "' is not three numbers."); } return new Vec3((float)num, (float)num2, (float)num3); } private static object Field(Dictionary<string, object> obj, string key) { if (!obj.TryGetValue(key, out var value)) { throw new FormatException("parts.json: missing '" + key + "'."); } return value; } private static string Str(Dictionary<string, object> obj, string key) { return (Field(obj, key) as string) ?? throw new FormatException("parts.json: '" + key + "' is not a string."); } private static double Num(Dictionary<string, object> obj, string key) { object obj2 = Field(obj, key); if (obj2 is double) { return (double)obj2; } throw new FormatException("parts.json: '" + key + "' is not a number."); } private static List<object> Arr(Dictionary<string, object> obj, string key) { return (Field(obj, key) as List<object>) ?? throw new FormatException("parts.json: '" + key + "' is not an array."); } private static Dictionary<string, object> Obj(Dictionary<string, object> obj, string key) { return (Field(obj, key) as Dictionary<string, object>) ?? throw new FormatException("parts.json: '" + key + "' is not an object."); } } internal static class FxLifetime { public const float Seconds = 5f; private static readonly string[] LeakyVanilla = new string[2] { "GolemGroundAttack(Clone)", "TitanGroundAttack(Clone)" }; public static bool ShouldExpire(string rootName, bool rootHasOwnLifetime, bool fromTaggedMob, bool cleanVanilla) { if (rootHasOwnLifetime) { return false; } if (fromTaggedMob) { return true; } if (cleanVanilla) { return IsLeakyVanilla(rootName); } return false; } public static bool IsLeakyVanilla(string rootName) { for (int i = 0; i < LeakyVanilla.Length; i++) { if (rootName == LeakyVanilla[i]) { return true; } } return false; } } internal sealed class TaggedFxLedger { private readonly struct Entry { public readonly string CloneName; public readonly Vec3 At; public readonly float Time; public Entry(string cloneName, Vec3 at, float time) { CloneName = cloneName; At = at; Time = time; } } public const float ClaimSeconds = 1f; public const float ClaimDistance = 0.5f; private readonly List<Entry> pending = new List<Entry>(); public int Count => pending.Count; public void Note(string cloneName, Vec3 at, float now) { Prune(now); pending.Add(new Entry(cloneName, at, now)); } public bool TryClaim(string rootName, Vec3 at, float now) { Prune(now); for (int i = 0; i < pending.Count; i++) { Entry entry = pending[i]; if (!(entry.CloneName != rootName) && !(Vec3.Length(entry.At - at) > 0.5f)) { pending.RemoveAt(i); return true; } } return false; } public void Clear() { pending.Clear(); } private void Prune(float now) { pending.RemoveAll((Entry entry) => now - entry.Time > 1f); } } internal static class Hash01 { public static ulong SplitMix64(ulong x) { x += 11400714819323198485uL; x = (x ^ (x >> 30)) * 13787848793156543929uL; x = (x ^ (x >> 27)) * 10723151780598845931uL; return x ^ (x >> 31); } public static double Of(int a, int b) { return ToUnit(SplitMix64(((ulong)(uint)a << 32) | (uint)b)); } private static double ToUnit(ulong h) { return (double)(h >> 11) * 1.1102230246251565E-16; } } internal struct HoverSample { public EnemyKind Kind; public int Id; public float Age; public float BaseOffset; public float RootY; public float GroundY; public string LowerBone; public float LowerBoneY; public string UpperBone; public float UpperBoneY; public float PartsLowY; public float PartsHighY; public float ColliderLowY; public float ColliderHighY; } internal static class HoverLog { public const int Samples = 6; public const float IntervalSeconds = 1f; public const string UpperBone = "Head"; public static bool Watches(EnemyKind kind) { if (kind != EnemyKind.Jellyfish) { return kind == EnemyKind.LesserWisp; } return true; } public static bool Due(float age, int logged) { if (logged < 6) { return age >= (float)logged * 1f; } return false; } public static bool Done(int logged) { return logged >= 6; } public static string LowerBone(RigId rig) { if (rig != RigId.LilDave) { return "Hips"; } return "Hip"; } public static float Clearance(float partsLowY, float groundY) { return partsLowY - groundY; } public static string Line(HoverSample s) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("RoR2 Enemies hover ").Append(s.Kind).Append(" #") .Append(s.Id.ToString(CultureInfo.InvariantCulture)) .Append(" t=") .Append(F(s.Age)) .Append("s baseOffset=") .Append(F(s.BaseOffset)) .Append(" root.y=") .Append(F(s.RootY)) .Append(" ground.y=") .Append(F(s.GroundY)) .Append(' ') .Append(s.LowerBone) .Append(".y=") .Append(F(s.LowerBoneY)) .Append(' ') .Append(s.UpperBone) .Append(".y=") .Append(F(s.UpperBoneY)) .Append(" parts.y=") .Append(F(s.PartsLowY)) .Append("..") .Append(F(s.PartsHighY)) .Append(" collider.y=") .Append(F(s.ColliderLowY)) .Append("..") .Append(F(s.ColliderHighY)) .Append(" clearance=") .Append(F(Clearance(s.PartsLowY, s.GroundY))); return stringBuilder.ToString(); } private static string F(float value) { if (!float.IsNaN(value)) { return value.ToString("0.00", CultureInfo.InvariantCulture); } return "n/a"; } } internal enum FuseState { Idle, Glowing, Detonated, Cancelled } internal enum FuseEvent { None, Detonate, HostDespawn } internal sealed class JellyFuse { public const float GlowSeconds = 1f; public const float HostDespawnDelay = 0.25f; public const int BuildTarget = -1; private float start; private bool hostDespawned; public FuseState State { get; private set; } public bool BlocksHostAttacks => State != FuseState.Idle; public bool BitingBuild { get; private set; } public void Attack(int targetPlayerId, float now) { BitingBuild = targetPlayerId == -1; if (!BitingBuild) { Start(now); } } public void Start(float now) { if (State == FuseState.Idle) { start = now; State = FuseState.Glowing; } } public FuseEvent Tick(float now, bool isHost) { if (State == FuseState.Glowing && now >= start + 1f) { State = FuseState.Detonated; return FuseEvent.Detonate; } if (State == FuseState.Detonated && isHost && !hostDespawned && now >= start + 1f + 0.25f) { hostDespawned = true; return FuseEvent.HostDespawn; } return FuseEvent.None; } public void Cancel() { if (State == FuseState.Glowing) { State = FuseState.Cancelled; } } public float GlowProgress(float now) { if (State == FuseState.Idle) { return 0f; } float num = (now - start) / 1f; if (num <= 0f) { return 0f; } if (num >= 1f) { return 1f; } return num; } } internal static class JellyHover { public const float VanillaBaseOffset = -0.15f; public const float DefaultOffset = 1f; public const float RetiredDefaultOffset = 1.5f; public const float MinOffset = 0f; public const float MaxOffset = 6f; public static float Clamp(float hoverOffset) { if (float.IsNaN(hoverOffset)) { return 1f; } return Math.Max(0f, Math.Min(6f, hoverOffset)); } public static float BaseOffset(float hoverOffset) { return -0.15f + Clamp(hoverOffset); } public static float? CarryRetired(string retiredValue) { if (retiredValue == null) { return null; } if (!float.TryParse(retiredValue.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return null; } if (float.IsNaN(result) || result == 1.5f) { return null; } return Clamp(result); } } internal sealed class JsonReader { private readonly string text; private int pos; private JsonReader(string text) { this.text = text; } public static object Parse(string text) { if (text == null) { throw new FormatException("JSON text is null."); } JsonReader jsonReader = new JsonReader(text); object result = jsonReader.ReadValue(); jsonReader.SkipWhitespace(); if (jsonReader.pos != text.Length) { throw jsonReader.Error("trailing characters"); } return result; } private object ReadValue() { SkipWhitespace(); if (pos >= text.Length) { throw Error("unexpected end"); } char c = text[pos]; switch (c) { case '{': return ReadObject(); case '[': return ReadArray(); case '"': return ReadString(); case 't': Expect("true"); return true; case 'f': Expect("false"); return false; case 'n': Expect("null"); return null; default: switch (c) { case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return ReadNumber(); default: throw Error($"unexpected '{c}'"); } } } private Dictionary<string, object> ReadObject() { Dictionary<string, object> dictionary = new Dictionary<string, object>(); pos++; SkipWhitespace(); if (Peek() == '}') { pos++; return dictionary; } while (true) { SkipWhitespace(); if (Peek() != '"') { throw Error("expected a key"); } string key = ReadString(); SkipWhitespace(); if (Peek() != ':') { break; } pos++; dictionary[key] = ReadValue(); SkipWhitespace(); char c = Peek(); pos++; switch (c) { case ',': break; case '}': return dictionary; default: throw Error("expected ',' or '}'"); } } throw Error("expected ':'"); } private List<object> ReadArray() { List<object> list = new List<object>(); pos++; SkipWhitespace(); if (Peek() == ']') { pos++; return list; } while (true) { list.Add(ReadValue()); SkipWhitespace(); char c = Peek(); pos++; switch (c) { case ',': break; case ']': return list; default: throw Error("expected ',' or ']'"); } } } private string ReadString() { pos++; StringBuilder stringBuilder = new StringBuilder(); while (pos < text.Length) { char c = text[pos++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); break; case '\\': { if (pos >= text.Length) { throw Error("unterminated escape"); } char c2 = text[pos++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': if (pos + 4 > text.Length) { throw Error("short \\u escape"); } stringBuilder.Append((char)int.Parse(text.Substring(pos, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture)); pos += 4; break; default: throw Error($"bad escape '\\{c2}'"); } break; } } } throw Error("unterminated string"); } private double ReadNumber() { int num = pos; while (pos < this.text.Length && "+-0123456789.eE".IndexOf(this.text[pos]) >= 0) { pos++; } string text = this.text.Substring(num, pos - num); if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw Error("bad number '" + text + "'"); } return result; } private void Expect(string word) { if (string.CompareOrdinal(text, pos, word, 0, word.Length) != 0) { throw Error("expected '" + word + "'"); } pos += word.Length; } private char Peek() { if (pos >= text.Length) { return '\0'; } return text[pos]; } private void SkipWhitespace() { while (pos < text.Length && char.IsWhiteSpace(text[pos])) { pos++; } } private FormatException Error(string what) { return new FormatException($"JSON: {what} at offset {pos}."); } } internal enum EnemyKind { None = 0, Lemurian = 1, Jellyfish = 2, LesserWisp = 3, StoneTitan = 5, Beetle = 6, WanderingVagrant = 7 } internal struct AiOverrides { public float Speed; public float RangedCooldown; public bool ApplyRangedCooldown; public float StartAttackDistance; public float MaxAttackDistance; public bool ApplyMaxAttackDistance; public float MinAttackAngle; public float KnockbackThreshold; } internal sealed class KindSpec { public EnemyKind Kind; public string DisplayName; public string SourceMobName; public string SpawnedMobName; public int PrefabHp; public float HpFactor; public float DamageScale; public float MeleeFactor; public bool Implemented; public RigId Rig; public AiOverrides Ai; public LootPlanRow[] Loot; public LootMode LootMode; public string RequiredItem; public string[] HiddenRenderers; } internal static class KindRegistry { private sealed class Restorer : IDisposable { private readonly HashSet<EnemyKind> previous; private bool done; public Restorer(HashSet<EnemyKind> previous) { this.previous = previous; } public void Dispose() { if (!done) { done = true; implementedOverride = previous; } } } public const int GlacialCode = 4; private static readonly KindSpec[] Rows = new KindSpec[6] { Spec(EnemyKind.Lemurian, "Lemurian", "Goblin", "Goblin", 60, 1.0833334f, 0.7f, 1.4f, RigId.Goblin, Ai(7f, applyRanged: true, 3f, 3.3f, applyMax: true, 30f, 40f, 0.25f), LootMode.Inherit, new LootPlanRow[1] { new LootPlanRow("Fireball", 1, 2, 0.15f) }, "Fireball", new string[2] { "Bone", "Trail" }, implemented: true), Spec(EnemyKind.Jellyfish, "Jellyfish", "lil Dave", "lil Dave", 40, 1.25f, 1f, 0f, RigId.LilDave, Ai(9f, applyRanged: false, 0f, 6f, applyMax: false, 0f, 180f, 0.2f), LootMode.None, Array.Empty<LootPlanRow>(), null, Array.Empty<string>(), implemented: true), Spec(EnemyKind.LesserWisp, "Lesser Wisp", "lil Dave", "Wyvern", 100, 0.4f, 0.25f, 0f, RigId.Wyvern, Ai(8f, applyRanged: false, 0f, 32f, applyMax: false, 0f, 90f, 0.1f), LootMode.Replace, new LootPlanRow[2] { new LootPlanRow("Coin", 4, 8, 1f), new LootPlanRow("Fireball", 1, 1, 0.1f) }, "Fireball", Array.Empty<string>(), implemented: true), Spec(EnemyKind.StoneTitan, "Stone Titan", "Big Chunk", "Big Chunk", 700, 1.1f, 1f, 0f, RigId.StoneGolem, Ai(11f, applyRanged: false, 0f, 32f, applyMax: true, 150f, 50f, 1f), LootMode.None, Array.Empty<LootPlanRow>(), "Spike Attack", Array.Empty<string>(), implemented: true), Spec(EnemyKind.Beetle, "Beetle", "Goblin", "Goblin", 0, 0f, 0f, 0f, RigId.Goblin, default(AiOverrides), LootMode.None, Array.Empty<LootPlanRow>(), null, Array.Empty<string>(), implemented: false), Spec(EnemyKind.WanderingVagrant, "Vagrant", "Wyvern", "Wyvern", 0, 0f, 0f, 0f, RigId.Wyvern, default(AiOverrides), LootMode.None, Array.Empty<LootPlanRow>(), null, Array.Empty<string>(), implemented: false) }; private static HashSet<EnemyKind> implementedOverride; internal static bool LemurianAvailable = true; internal static bool JellyfishAvailable = true; internal static bool WispAvailable = true; internal static bool TitanAvailable = true; public static IReadOnlyList<KindSpec> All => Rows; public static IEnumerable<KindSpec> Implemented { get { KindSpec[] rows = Rows; foreach (KindSpec kindSpec in rows) { if (IsImplemented(kindSpec)) { yield return kindSpec; } } } } public static bool TryGet(int code, out KindSpec spec) { for (int i = 0; i < Rows.Length; i++) { if (Rows[i].Kind == (EnemyKind)code) { spec = Rows[i]; return true; } } spec = null; return false; } public static bool IsImplemented(KindSpec spec) { if (spec == null) { return false; } if (implementedOverride != null) { return implementedOverride.Contains(spec.Kind); } return spec.Implemented; } internal static IDisposable WithImplemented(params EnemyKind[] kinds) { HashSet<EnemyKind> previous = implementedOverride; implementedOverride = new HashSet<EnemyKind>(kinds); return new Restorer(previous); } private static KindSpec Spec(EnemyKind kind, string display, string source, string spawned, int prefabHp, float hpFactor, float damageScale, float meleeFactor, RigId rig, AiOverrides ai, LootMode lootMode, LootPlanRow[] loot, string requiredItem, string[] hidden, bool implemented) { return new KindSpec { Kind = kind, DisplayName = display, SourceMobName = source, SpawnedMobName = spawned, PrefabHp = prefabHp, HpFactor = hpFactor, DamageScale = damageScale, MeleeFactor = meleeFactor, Implemented = implemented, Rig = rig, Ai = ai, Loot = loot, LootMode = lootMode, RequiredItem = requiredItem, HiddenRenderers = hidden }; } private static AiOverrides Ai(float speed, bool applyRanged, float ranged, float start, bool applyMax, float max, float angle, float knockback) { return new AiOverrides { Speed = speed, ApplyRangedCooldown = applyRanged, RangedCooldown = ranged, StartAttackDistance = start, ApplyMaxAttackDistance = applyMax, MaxAttackDistance = max, MinAttackAngle = angle, KnockbackThreshold = knockback }; } } internal static class LaserAim { public const float ChargeTurnDegPerSec = 90f; public const float FireTurnDegPerSec = 20f; public const float TargetHeight = 1f; public static Vec3 Step(Vec3 current, Vec3 desired, float maxDegPerSec, float dt) { Vec3 vec = Vec3.Normalize(current); Vec3 vec2 = Vec3.Normalize(desired); if (Vec3.Length(vec) < 1E-08f) { if (!(Vec3.Length(vec2) < 1E-08f)) { return vec2; } return new Vec3(0f, 0f, 1f); } if (Vec3.Length(vec2) < 1E-08f) { return vec; } float num = maxDegPerSec * dt; if (num < 0f) { num = 0f; } float num2 = (float)(Math.Acos(ClampUnit(Vec3.Dot(vec, vec2))) * (180.0 / Math.PI)); if (num2 <= num || num2 < 0.0001f) { return vec2; } Vec3 vec3 = Vec3.Cross(vec, vec2); if (Vec3.Length(vec3) < 1E-06f) { vec3 = ((Math.Abs(vec.Y) < 0.9f) ? Vec3.Cross(vec, new Vec3(0f, 1f, 0f)) : Vec3.Cross(vec, new Vec3(1f, 0f, 0f))); } return Rotate(vec, vec3, num); } private static float ClampUnit(float dot) { if (dot > 1f) { return 1f; } if (dot < -1f) { return -1f; } return dot; } private static Vec3 Rotate(Vec3 v, Vec3 axis, float degrees) { Vec3 vec = Vec3.Normalize(axis); if (Vec3.Length(vec) < 1E-08f) { return v; } double num = (double)degrees * (Math.PI / 180.0); double num2 = Math.Cos(num); double num3 = Math.Sin(num); Vec3 vec2 = Vec3.Cross(vec, v); double num4 = Vec3.Dot(vec, v); double num5 = 1.0 - num2; return new Vec3((float)((double)v.X * num2 + (double)vec2.X * num3 + (double)vec.X * num4 * num5), (float)((double)v.Y * num2 + (double)vec2.Y * num3 + (double)vec.Y * num4 * num5), (float)((double)v.Z * num2 + (double)vec2.Z * num3 + (double)vec.Z * num4 * num5)); } } internal enum LaserPhase { Charge, Fire, Done } internal static class LaserTimeline { public const float ChargeSeconds = 3.2f; public const float FireSeconds = 3f; public const int Ticks = 24; public const int TickDamage = 3; public static LaserPhase PhaseAt(float t) { if (t < 3.2f) { return LaserPhase.Charge; } if (t < 6.2f) { return LaserPhase.Fire; } return LaserPhase.Done; } public static int TicksBetween(float tPrev, float tNow) { int num = 0; for (int i = 0; i < 24; i++) { float num2 = 3.2f + 0.125f * (float)i; if (num2 > tPrev && num2 <= tNow) { num++; } } return num; } public static int TickDamageFor(float mobMultiplier) { int num = DamageTuning.FxDamage(3, mobMultiplier); if (num >= 1) { return num; } return 1; } } internal enum LootMode { None, Inherit, Replace } internal readonly struct LootPlanRow { public readonly string ItemName; public readonly int Min; public readonly int Max; public readonly float Chance; public LootPlanRow(string itemName, int min, int max, float chance) { ItemName = itemName; Min = min; Max = max; Chance = chance; } } internal static class LootPlan { public static bool IsValid(LootPlanRow row) { if (!string.IsNullOrEmpty(row.ItemName) && row.Min >= 0 && row.Min <= row.Max && row.Chance > 0f) { return row.Chance <= 1f; } return false; } public static IReadOnlyList<LootPlanRow> Merge(IReadOnlyList<LootPlanRow> baseRows, LootMode mode, IReadOnlyList<LootPlanRow> extra) { if (baseRows == null) { baseRows = Array.Empty<LootPlanRow>(); } if (mode == LootMode.None) { return baseRows; } List<LootPlanRow> list = new List<LootPlanRow>(); if (extra != null) { for (int i = 0; i < extra.Count; i++) { if (IsValid(extra[i])) { list.Add(extra[i]); } } } if (mode == LootMode.Replace) { return list; } List<LootPlanRow> list2 = new List<LootPlanRow>(baseRows.Count + list.Count); for (int j = 0; j < baseRows.Count; j++) { list2.Add(baseRows[j]); } list2.AddRange(list); return list2; } } internal sealed class MaterialSpec { public float R; public float G; public float B; public float Emission; public bool Glows => Emission > 0f; } internal sealed class MeshData { public readonly Vec3 Origin; public readonly Vec3[] Positions; public readonly Vec3[] Normals; public MeshData(Vec3 origin, Vec3[] positions, Vec3[] normals) { Origin = origin; Positions = positions; Normals = normals; } } internal enum RigId { Goblin, LilDave, Wyvern, StoneGolem } internal static class MobRigs { private static readonly string[] GoblinBones = new string[18] { "Hips", "Torso", "Neck", "Head", "Shoulder.L", "Arm.L", "Hand.L", "Fingers.L", "Shoulder.R", "Arm.R", "Hand.R", "Fingers.R", "Foot.L", "Leg.L", "Toe.L", "Foot.R", "Leg.R", "Toe.R" }; private static readonly string[] LilDaveBones = new string[13] { "Hip", "Foot.L", "Knee.L", "Toe.L", "Foot.R", "Knee.R", "Toe.R", "Neck", "Head", "Peck_bot", "Peck_top", "Tail", "Tail1" }; private static readonly string[] WyvernBones = new string[22] { "Hips", "Foot_L", "Knee.L", "Toes.L", "Foot_R", "Knee.R", "Toes.R", "Tail", "Tail1", "Tail2", "Tail3", "Torso", "Neck", "Head", "JAwLower", "JawUpper", "Shoulder.L", "Wing1.L", "Wing2.L", "Shoulder.R", "Wing1.R", "Wing2.R" }; private static readonly string[] StoneBones = new string[13] { "Hips", "Leg.L", "Knee.L", "Toe.L", "Leg.R", "Knee.R", "Toe.R", "Torso", "Head", "Shoulder.L", "Arm.L", "Shoulder.R", "Arm.R" }; public static IReadOnlyList<string> Bones(RigId rig) { return rig switch { RigId.Goblin => GoblinBones, RigId.LilDave => LilDaveBones, RigId.Wyvern => WyvernBones, _ => StoneBones, }; } public static string RendererName(RigId rig) { return rig switch { RigId.Goblin => "GoblinRender", RigId.LilDave => "DinoRenderer", RigId.Wyvern => "WyvernRenderer", _ => "StoneGolem", }; } } internal static class NovaFalloff { public const float Radius = 8f; public const float SweetSpot = 0.5f; public const float OuterFraction = 0.25f; public const int BaseDamage = 40; public static float Fraction(float distance) { if (distance <= 4f) { return 1f; } if (distance <= 8f) { return 0.25f; } return 0f; } public static int Damage(float mobMultiplier, float distance) { float num = Fraction(distance); if (num <= 0f) { return 0; } int num2 = (int)((float)DamageTuning.FxDamage(40, mobMultiplier) * num); if (num2 >= 1) { return num2; } return 1; } } internal static class RigMetrics { public static (Vec3 min, Vec3 max) Aabb(IReadOnlyList<Vec3> corners) { if (corners == null || corners.Count == 0) { return (min: new Vec3(0f, 0f, 0f), max: new Vec3(0f, 0f, 0f)); } Vec3 item = corners[0]; Vec3 item2 = corners[0]; for (int i = 1; i < corners.Count; i++) { Vec3 vec = corners[i]; item = new Vec3(Min(item.X, vec.X), Min(item.Y, vec.Y), Min(item.Z, vec.Z)); item2 = new Vec3(Max(item2.X, vec.X), Max(item2.Y, vec.Y), Max(item2.Z, vec.Z)); } return (min: item, max: item2); } public static float BodyHeight(IReadOnlyList<Vec3> corners) { (Vec3 min, Vec3 max) tuple = Aabb(corners); var (vec, _) = tuple; return tuple.max.Y - vec.Y; } private static float Min(float a, float b) { if (!(a < b)) { return b; } return a; } private static float Max(float a, float b) { if (!(a > b)) { return b; } return a; } } internal static class RoR2Marker { public const float GridStep = 0.0625f; public const float KindStep = 0.00390625f; public const float NudgeStep = 0.00048828125f; public const double Tolerance = 0.001; public const float MaxH = 16f; public static float Encode(float h, EnemyKind kind) { float num = h; if (num < 0f) { num = 0f; } if (num > 16f) { num = 16f; } double num2 = Math.Floor(16.0 * (double)num) / 16.0; if (num2 < 0.0625) { num2 = 0.0625; } return (float)(num2 + (double)kind / 256.0); } public static bool TryDecodeCode(float bossMultiplier, out int code) { double num = bossMultiplier; double num2 = Math.Floor(num * 16.0) / 16.0; double num3 = (num - num2) * 256.0; int num4 = (int)Math.Round(num3); if (Math.Abs(num3 - (double)num4) < 0.001 && num4 >= 1 && num4 <= 15) { code = num4; return true; } code = 0; return false; } public static bool TryDecode(float bossMultiplier, string spawnedMobName, out EnemyKind kind) { kind = EnemyKind.None; if (!TryDecodeCode(bossMultiplier, out var code)) { return false; } if (code == 4) { return false; } if (!KindRegistry.TryGet(code, out var spec)) { return false; } if (!KindRegistry.IsImplemented(spec)) { return false; } if (spec.SpawnedMobName != spawnedMobName) { return false; } kind = spec.Kind; return true; } public static bool NeedsNudge(float bossMultiplier) { KindSpec spec; if (TryDecodeCode(bossMultiplier, out var code)) { return KindRegistry.TryGet(code, out spec); } return false; } public static float Nudge(float bossMultiplier) { return bossMultiplier + 0.00048828125f; } } [Flags] internal enum SpawnSource { None = 0, Director = 1, BossNight = 2, BossShrine = 4, BattleTotem = 8, Debug = 0x10 } internal readonly struct KindRules { public readonly bool Enabled; public readonly int FirstDay; public readonly float Chance; public readonly int MaxAlive; public readonly SpawnSource Sources; private KindRules(bool enabled, int firstDay, float chance, int maxAlive, SpawnSource sources) { Enabled = enabled; FirstDay = firstDay; Chance = chance; MaxAlive = maxAlive; Sources = sources; } public static KindRules Clamped(bool enabled, int firstDay, float chance, int maxAlive, SpawnSource sources) { if (firstDay < 0) { firstDay = 0; } if (float.IsNaN(chance) || chance < 0f) { chance = 0f; } if (chance > 1f) { chance = 1f; } if (maxAlive < 0) { maxAlive = 0; } sources &= ~SpawnSource.Debug; return new KindRules(enabled, firstDay, chance, maxAlive, sources); } } internal sealed class RulesSnapshot { public IReadOnlyDictionary<EnemyKind, KindRules> Kinds; public float TitanShrineChance; public ulong Fingerprint() { return ConfigFingerprint.Of(this); } public bool TryGet(EnemyKind kind, out KindRules rules) { if (Kinds != null && Kinds.TryGetValue(kind, out rules)) { return true; } rules = KindRules.Clamped(enabled: false, 0, 0f, 0, SpawnSource.None); return false; } } internal enum ShaderKind { Plain, DoubledTint } internal readonly struct Rgba { public readonly float R; public readonly float G; public readonly float B; public readonly float A; public Rgba(float r, float g, float b, float a) { R = r; G = g; B = b; A = a; } public Rgba Scaled(float s) { return new Rgba(R * s, G * s, B * s, A * s); } public override string ToString() { return $"({R}, {G}, {B}, {A})"; } } internal readonly struct TintWrite { public readonly Rgba TintColor; public readonly Rgba Color; public TintWrite(Rgba tintColor, Rgba color) { TintColor = tintColor; Color = color; } } internal static class ShaderTint { public static ShaderKind KindOf(string shaderName) { if (shaderName == null) { return ShaderKind.Plain; } bool num = shaderName.IndexOf("Particles/", StringComparison.Ordinal) >= 0; bool flag = shaderName.IndexOf("Standard", StringComparison.Ordinal) >= 0; if (!num || flag) { return ShaderKind.Plain; } return ShaderKind.DoubledTint; } public static TintWrite For(ShaderKind kind, Rgba intended) { return new TintWrite((kind == ShaderKind.DoubledTint) ? intended.Scaled(0.5f) : intended, intended); } public static Rgba Intended(ShaderKind kind, Rgba tintColor) { if (kind != ShaderKind.DoubledTint) { return tintColor; } return tintColor.Scaled(2f); } } internal readonly struct SpawnRequest { public readonly SpawnSource Source; public readonly int Day; public readonly string SourceMobName; public readonly float IncomingMultiplier; public readonly float IncomingBossMultiplier; public readonly int GuardianType; public readonly double Roll; public readonly bool ShrineIsTitan; public readonly EnemyKind ForcedKind; public readonly Func<EnemyKind, int> AliveCount; public SpawnRequest(SpawnSource source, int day, string sourceMobName, float incomingMultiplier, float incomingBossMultiplier, int guardianType, double roll, bool shrineIsTitan, EnemyKind forcedKind, Func<EnemyKind, int> aliveCount) { Source = source; Day = day; SourceMobName = sourceMobName; IncomingMultiplier = incomingMultiplier; IncomingBossMultiplier = incomingBossMultiplier; GuardianType = guardianType; Roll = roll; ShrineIsTitan = shrineIsTitan; ForcedKind = forcedKind; AliveCount = aliveCount; } } internal enum SpawnReason { Converted, NoContext, Guardian, NoCandidate, RollMissed, Nudged } internal readonly struct SpawnDecision { public readonly EnemyKind Kind; public readonly string SpawnedMobName; public readonly float Multiplier; public readonly float BossMultiplier; public readonly SpawnReason Reason; public SpawnDecision(EnemyKind kind, string spawnedMobName, float multiplier, float bossMultiplier, SpawnReason reason) { Kind = kind; SpawnedMobName = spawnedMobName; Multiplier = multiplier; BossMultiplier = bossMultiplier; Reason = reason; } } internal static class SpawnPlanner { private readonly struct Candidate { public readonly KindSpec Spec; public readonly float Chance; public Candidate(KindSpec spec, float chance) { Spec = spec; Chance = chance; } } public static bool KeepsBuff(float incomingMultiplier, float damageScale) { if (!(incomingMultiplier > 1f)) { return true; } return incomingMultiplier * damageScale > 1f; } public static SpawnDecision Decide(in SpawnRequest req, RulesSnapshot rules) { if (req.GuardianType != -1) { return Vanilla(in req, SpawnReason.Guardian); } if (req.Source == SpawnSource.None) { return Vanilla(in req, SpawnReason.NoContext); } List<Candidate> list = new List<Candidate>(); if (req.Source == SpawnSource.Debug) { if (rules != null && KindRegistry.TryGet((int)req.ForcedKind, out var spec) && Accept(in req, rules, spec, skipDay: true, skipSources: true) && spec.SourceMobName == req.SourceMobName) { list.Add(new Candidate(spec, 1f)); } } else { foreach (KindSpec item in KindRegistry.All) { if (Accept(in req, rules, item, skipDay: false, skipSources: false) && !(item.SourceMobName != req.SourceMobName)) { float num = Chance(in req, rules, item); if (!(num <= 0f)) { list.Add(new Candidate(item, num)); } } } } float num2 = 0f; for (int i = 0; i < list.Count; i++) { num2 += list[i].Chance; } if (num2 > 0f) { float num3 = ((num2 > 1f) ? (1f / num2) : 1f); float num4 = 0f; for (int j = 0; j < list.Count; j++) { num4 += list[j].Chance * num3; if (req.Roll < (double)num4) { return Convert(in req, list[j].Spec); } } } SpawnReason reason = ((list.Count == 0) ? SpawnReason.NoCandidate : SpawnReason.RollMissed); if (RoR2Marker.NeedsNudge(req.IncomingBossMultiplier)) { return new SpawnDecision(EnemyKind.None, null, req.IncomingMultiplier, RoR2Marker.Nudge(req.IncomingBossMultiplier), SpawnReason.Nudged); } return Vanilla(in req, reason); } private static bool Accept(in SpawnRequest req, RulesSnapshot rules, KindSpec spec, bool skipDay, bool skipSources) { if (spec == null || rules == null) { return false; } if (!KindRegistry.IsImplemented(spec)) { return false; } if (spec.Kind == EnemyKind.Lemurian && !KindRegistry.LemurianAvailable) { return false; } if (spec.Kind == EnemyKind.Jellyfish && !KindRegistry.JellyfishAvailable) { return false; } if (spec.Kind == EnemyKind.LesserWisp && !KindRegistry.WispAvailable) { return false; } if (spec.Kind == EnemyKind.StoneTitan && !KindRegistry.TitanAvailable) { return false; } if (!rules.TryGet(spec.Kind, out var rules2) || !rules2.Enabled) { return false; } if (!skipSources && (rules2.Sources & req.Source) != req.Source) { return false; } if (!skipDay && req.Day < rules2.FirstDay) { return false; } int num = ((req.AliveCount != null) ? req.AliveCount(spec.Kind) : 0); if (req.Source != SpawnSource.BossShrine && num >= rules2.MaxAlive) { return false; } if (!KeepsBuff(req.IncomingMultiplier, spec.DamageScale)) { return false; } if (req.Source == SpawnSource.BossShrine && !req.ShrineIsTitan) { return false; } return spec.DamageScale > 0f; } private static float Chance(in SpawnRequest req, RulesSnapshot rules, KindSpec spec) { if (req.Source == SpawnSource.BossShrine) { return 1f; } rules.TryGet(spec.Kind, out var rules2); return rules2.Chance; } private static SpawnDecision Convert(in SpawnRequest req, KindSpec spec) { float multiplier = req.IncomingMultiplier * spec.DamageScale; float bossMultiplier = RoR2Marker.Encode(spec.HpFactor / spec.DamageScale * req.IncomingBossMultiplier, spec.Kind); return new SpawnDecision(spec.Kind, spec.SpawnedMobName, multiplier, bossMultiplier, SpawnReason.Converted); } private static SpawnDecision Vanilla(in SpawnRequest req, SpawnReason reason) { return new SpawnDecision(EnemyKind.None, null, req.IncomingMultiplier, req.IncomingBossMultiplier, reason); } } internal static class SpawnSources { public const string Accepted = "Director, BossNight, BossShrine, BattleTotem"; public static SpawnSource Parse(string text, ICollection<string> rejected) { SpawnSource spawnSource = SpawnSource.None; if (string.IsNullOrWhiteSpace(text)) { return spawnSource; } string[] array = text.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0) { if (TryName(text2, out var bit)) { spawnSource |= bit; } else { rejected?.Add(text2); } } } return spawnSource; } private static bool TryName(string token, out SpawnSource bit) { bit = SpawnSource.None; if (!char.IsLetter(token[0])) { return false; } if (!Enum.TryParse<SpawnSource>(token, ignoreCase: true, out bit) || !Enum.IsDefined(typeof(SpawnSource), bit)) { return false; } if (bit != SpawnSource.None) { return bit != SpawnSource.Debug; } return false; } } internal static class StoneFistAim { public static Vec3 Predict(Vec3 targetPos, Vec3 targetVelocity, float timeToImpact) { return new Vec3(targetPos.X + targetVelocity.X * timeToImpact, targetPos.Y, targetPos.Z + targetVelocity.Z * timeToImpact); } } internal enum TitanSkill { None, EyeLaser, StoneFist, Leap } internal enum LaserGate { Ready, Cooldown, OutOfRange, NoLineOfSight } internal struct TitanCooldowns { public float LaserReadyAt; public float FistReadyAt; public float LeapReadyAt; public static TitanCooldowns AtSpawn(float now) { return new TitanCooldowns { LaserReadyAt = now + 10f, FistReadyAt = now + 5f, LeapReadyAt = now }; } } internal readonly struct TitanInputs { public readonly float Now; public readonly float Distance; public readonly float AngleToTarget; public readonly bool HasLineOfSight; public TitanInputs(float now, float distance, float angleToTarget, bool hasLineOfSight) { Now = now; Distance = distance; AngleToTarget = angleToTarget; HasLineOfSight = hasLineOfSight; } } internal static class TitanScheduler { public const float LaserCooldown = 20f; public const float LaserMinRange = 20f; public const float LaserMaxRange = 80f; public const float FistCooldown = 10f; public const float FistMaxRange = 120f; public const float LeapCooldown = 12f; public const float LeapMaxRange = 32f; public const float LeapMaxAngle = 50f; public static TitanSkill Pick(in TitanInputs input, ref TitanCooldowns cd, out LaserGate laser) { laser = Laser(in input, cd); if (laser == LaserGate.Ready) { cd.LaserReadyAt = input.Now + 20f; return TitanSkill.EyeLaser; } if (FistReady(in input, cd)) { cd.FistReadyAt = input.Now + 10f; return TitanSkill.StoneFist; } if (LeapReady(in input, cd)) { cd.LeapReadyAt = input.Now + 12f; return TitanSkill.Leap; } return TitanSkill.None; } public static int AttackIndex(TitanSkill skill) { return skill switch { TitanSkill.EyeLaser => 0, TitanSkill.StoneFist => 1, TitanSkill.Leap => 2, _ => -1, }; } private static LaserGate Laser(in TitanInputs input, TitanCooldowns cd) { if (input.Now < cd.LaserReadyAt) { return LaserGate.Cooldown; } if (input.Distance < 20f || input.Distance > 80f) { return LaserGate.OutOfRange; } if (!input.HasLineOfSight) { return LaserGate.NoLineOfSight; } return LaserGate.Ready; } private static bool FistReady(in TitanInputs input, TitanCooldowns cd) { if (input.Now >= cd.FistReadyAt) { return input.Distance <= 120f; } return false; } private static bool LeapReady(in TitanInputs input, TitanCooldowns cd) { if (input.Now >= cd.LeapReadyAt && input.Distance <= 32f) { return input.AngleToTarget <= 50f; } return false; } } internal struct LaserSkipLog { public const float Interval = 5f; private float nextAt; public bool ShouldLog(LaserGate gate, float now) { if (gate == LaserGate.Ready || now < nextAt) { return false; } nextAt = now + 5f; return true; } } internal static class TitanShrineRoll { public static bool IsTitan(int seed, int shrineId, float chance) { return Hash01.Of(seed ^ 0x54495441, shrineId) < (double)chance; } } internal readonly struct Vec3 { public readonly float X; public readonly float Y; public readonly float Z; public Vec3(float x, float y, float z) { X = x; Y = y; Z = z; } public static Vec3 operator +(Vec3 a, Vec3 b) { return new Vec3(a.X + b.X, a.Y + b.Y, a.Z + b.Z); } public static Vec3 operator -(Vec3 a, Vec3 b) { return new Vec3(a.X - b.X, a.Y - b.Y, a.Z - b.Z); } public static Vec3 operator *(Vec3 a, float s) { return new Vec3(a.X * s, a.Y * s, a.Z * s); } public static Vec3 operator *(float s, Vec3 a) { return a * s; } public static Vec3 Scale(Vec3 a, Vec3 b) { return new Vec3(a.X * b.X, a.Y * b.Y, a.Z * b.Z); } public static float Dot(Vec3 a, Vec3 b) { return a.X * b.X + a.Y * b.Y + a.Z * b.Z; } public static Vec3 Cross(Vec3 a, Vec3 b) { return new Vec3(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); } public static float Length(Vec3 v) { return (float)Math.Sqrt(Dot(v, v)); } public static Vec3 Normalize(Vec3 v) { float num = Length(v); if (num < 1E-08f) { return new Vec3(0f, 0f, 0f); } return v * (1f / num); } public override string ToString() { return $"({X}, {Y}, {Z})"; } } internal static class WaveRunSpawn { public const string HostType = "MuckWaveRun.Game.WaveHost"; public const string Method = "TrySpawn"; private const int BossNight = 1; public static SpawnSource SourceFor(int bossType) { if (bossType != 1) { return SpawnSource.Director; } return SpawnSource.BossNight; } } internal static class WispHover { public const float DefaultHeight = 5.6f; public const float RatePerSecond = 2f; public const float MaxNavGap = 2f; public static float TargetOffset(float hoverHeight, float currentOffset, float heightAboveGround) { if (float.IsNaN(heightAboveGround)) { return hoverHeight; } float num = heightAboveGround - currentOffset; if (num > 2f) { num = 2f; } if (num < -2f) { num = -2f; } return hoverHeight - num; } public static float Step(float currentOffset, float targetOffset, float deltaTime) { float num = deltaTime * 2f; if (!(num > 0f)) { return currentOffset; } if (num > 1f) { num = 1f; } return currentOffset + (targetOffset - currentOffset) * num; } public static float Clamp(float hoverHeight) { if (float.IsNaN(hoverHeight)) { return 5.6f; } return Math.Max(1f, Math.Min(20f, hoverHeight)); } } internal readonly struct VolleyBolt { public readonly Vec3 From; public readonly Vec3 Dir; public VolleyBolt(Vec3 from, Vec3 dir) { From = from; Dir = dir; } } internal static class WispVolley { public const int Bolts = 3; public const float LateralSpacing = 1f; public const float Stagger = 1.5f; public const float FullSpacingDistance = 3f; public const float JitterMeters = 0.3f; private static readonly Vec3 Up = new Vec3(0f, 1f, 0f); private static readonly Vec3 FallbackAxis = new Vec3(1f, 0f, 0f); public static VolleyBolt[] Plan(Vec3 mouth, Vec3 target, float jitterAcross0, float jitterUp0, float jitterAcross1, float jitterUp1, float jitterAcross2, float jitterUp2) { Vec3 vec = target - mouth; float num = Vec3.Length(vec); Vec3 vec2 = ((num < 0.0001f) ? new Vec3(0f, 0f, 1f) : (vec * (1f / num))); Vec3 vec3 = Vec3.Normalize(Vec3.Cross(YawAxis(vec2), vec2)); Vec3 up = Vec3.Cross(vec2, vec3); float num2 = ((num < 3f) ? (num / 3f) : 1f); Vec3 vec4 = vec3 * (1f * num2); Vec3 vec5 = vec2 * (1.5f * num2); return new VolleyBolt[3] { Bolt(mouth - vec4 - vec5, target, vec2, vec3, up, jitterAcross0, jitterUp0), Bolt(mouth, target, vec2, vec3, up, jitterAcross1, jitterUp1), Bolt(mouth + vec4 + vec5, target, vec2, vec3, up, jitterAcross2, jitterUp2) }; } private static Vec3 YawAxis(Vec3 forward) { if (!(Vec3.Length(Vec3.Cross(Up, forward)) < 0.0001f)) { return Up; } return FallbackAxis; } private static VolleyBolt Bolt(Vec3 from, Vec3 target, Vec3 forward, Vec3 across, Vec3 up, float jitterAcross, float jitterUp) { Vec3 vec = Vec3.Normalize(target + across * (Clamp(jitterAcross) * 0.3f) + up * (Clamp(jitterUp) * 0.3f) - from); return new VolleyBolt(from, (Vec3.Length(vec) == 0f) ? forward : vec); } private static float Clamp(float value) { if (value > 1f) { return 1f; } if (value < -1f) { return -1f; } return value; } } [HarmonyPatch] internal static class CheckMobSpawnsContextPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(GameLoop), "CheckMobSpawns", Type.EmptyTypes, (Type[])null); } private static void Prefix(out SpawnSource __state) { __state = SpawnContext.Current; try { SpawnContext.Current = SpawnSource.Director; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-director", $"RoR2 Enemies: director context failed. {arg}"); } } private static Exception Finalizer(SpawnSource __state, Exception __exception) { try { SpawnContext.Current = __state; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-director-restore", $"RoR2 Enemies: director context restore failed. {arg}"); } return __exception; } } [HarmonyPatch] internal static class StartBossContextPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(GameLoop), "StartBoss", new Type[1] { typeof(MobType) }, (Type[])null); } private static void Prefix(out SpawnSource __state) { __state = SpawnContext.Current; try { SpawnContext.Current = SpawnSource.BossNight; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-boss", $"RoR2 Enemies: boss-night context failed. {arg}"); } } private static Exception Finalizer(SpawnSource __state, Exception __exception) { try { SpawnContext.Current = __state; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-boss-restore", $"RoR2 Enemies: boss-night context restore failed. {arg}"); } return __exception; } } [HarmonyPatch] internal static class ShrineBossContextPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ShrineBoss), "SpawnBoss", Type.EmptyTypes, (Type[])null); } private static void Prefix(ShrineBoss __instance, out SpawnContextSaved __state) { __state = new SpawnContextSaved { Source = SpawnContext.Current, Shrine = SpawnContext.Shrine }; try { SpawnContext.Current = SpawnSource.BossShrine; SpawnContext.Shrine = __instance; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-shrine", $"RoR2 Enemies: shrine context failed. {arg}"); } } private static Exception Finalizer(SpawnContextSaved __state, Exception __exception) { try { if (__state != null) { SpawnContext.Current = __state.Source; SpawnContext.Shrine = __state.Shrine; } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-shrine-restore", $"RoR2 Enemies: shrine context restore failed. {arg}"); } return __exception; } } [HarmonyPatch] internal static class BattleTotemContextPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ShrineInteractable), "ServerExecute", new Type[1] { typeof(int) }, (Type[])null); } private static void Prefix(out SpawnSource __state) { __state = SpawnContext.Current; try { SpawnContext.Current = SpawnSource.BattleTotem; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-totem", $"RoR2 Enemies: totem context failed. {arg}"); } } private static Exception Finalizer(SpawnSource __state, Exception __exception) { try { SpawnContext.Current = __state; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-totem-restore", $"RoR2 Enemies: totem context restore failed. {arg}"); } return __exception; } } [HarmonyPatch] internal static class FxLifetimePatch { private static readonly TaggedFxLedger Tagged = new TaggedFxLedger(); private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ImpactDamage), "Start", Type.EmptyTypes, (Type[])null); } public static void NoteTaggedSpawn(SpawnAttackFx fx, int n) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)fx == (Object)null) && fx.attackFx != null && n >= 0 && n < fx.attackFx.Length) { GameObject val = fx.attackFx[n]; if (!((Object)(object)val == (Object)null) && !((Object)(object)fx.spawnPos == (Object)null)) { Vector3 position = fx.spawnPos.position; Tagged.Note(((Object)val).name + "(Clone)", new Vec3(position.x, position.y, position.z), Time.time); } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("fx-note", $"RoR2 Enemies: could not track an attack effect. {arg}"); } } public static void Reset() { Tagged.Clear(); } private static void Prefix(ImpactDamage __instance) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)__instance == (Object)null)) { Transform root = ((Component)__instance).transform.root; Vector3 position = root.position; bool fromTaggedMob = Tagged.TryClaim(((Object)root).name, new Vec3(position.x, position.y, position.z), Time.time); bool rootHasOwnLifetime = (Object)(object)((Component)root).GetComponent<DestroyObject>() != (Object)null; bool cleanVanilla = Ror2EnemiesPlugin.Cfg == null || Ror2EnemiesPlugin.Cfg.CleanVanillaGroundFx.Value; if (FxLifetime.ShouldExpire(((Object)root).name, rootHasOwnLifetime, fromTaggedMob, cleanVanilla)) { Object.Destroy((Object)(object)((Component)root).gameObject, 5f); } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("fx-lifetime", $"RoR2 Enemies: attack effect cleanup failed. {arg}"); } } } [HarmonyPatch] internal static class JellyAttackGatePatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(MobServerEnemy), "AttackBehaviour", Type.EmptyTypes, (Type[])null); } private static bool Prefix(MobServerEnemy __instance) { try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null || component.Kind != EnemyKind.Jellyfish || component.Fuse == null) { return true; } if (component.Fuse.BlocksHostAttacks) { return false; } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("jelly-gate", $"RoR2 Enemies: Jellyfish attack gate failed, left the attack. {arg}"); } return true; } } [HarmonyPatch] internal static class LootPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(LootExtra), "DropMobLoot", new Type[4] { typeof(Transform), typeof(LootDrop), typeof(int), typeof(float) }, (Type[])null); } private static void Prefix(Transform __0, ref LootDrop __1) { try { if (!((Object)(object)__0 == (Object)null)) { Ror2Enemy component = ((Component)__0).GetComponent<Ror2Enemy>(); if (!((Object)(object)component == (Object)null) && component.Spec != null && component.Spec.LootMode != LootMode.None) { __1 = LootTables.For(component.Spec, __1); } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("loot", $"RoR2 Enemies: loot plan failed, left the vanilla table. {arg}"); } } } [HarmonyPatch] internal static class MobAttackPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(Mob), "Attack", new Type[2] { typeof(int), typeof(int) }, (Type[])null); } private static void Postfix(Mob __instance, int __0, int __1) { try { if ((Object)(object)__instance == (Object)null) { return; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if (!((Object)(object)component == (Object)null)) { component.AttackIndex = __1; if (component.Kind == EnemyKind.Jellyfish && component.Fuse != null) { component.Fuse.Attack(__0, Time.time); } if (component.Kind == EnemyKind.StoneTitan && __1 == 0) { TitanLaser.Begin(component, __0); } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("mob-attack", $"RoR2 Enemies: attack follow-up failed. {arg}"); } } } [HarmonyPatch] internal static class RagdollPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(TestRagdoll), "MakeRagdoll", new Type[1] { typeof(Vector3) }, (Type[])null); } private static bool Prefix(TestRagdoll __instance) { try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy componentInParent = ((Component)__instance).GetComponentInParent<Ror2Enemy>(); if ((Object)(object)componentInParent != (Object)null && componentInParent.Fuse != null && componentInParent.Fuse.State == FuseState.Detonated) { return false; } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("jelly-ragdoll", $"RoR2 Enemies: Jellyfish ragdoll skip failed. {arg}"); } return true; } } [HarmonyPatch] internal static class ServerSpawnNewMobPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(MobSpawner), "ServerSpawnNewMob", new Type[7] { typeof(int), typeof(int), typeof(Vector3), typeof(float), typeof(float), typeof(BossType), typeof(int) }, (Type[])null); } [HarmonyAfter(new string[] { "evan.muck.artifactofthenight" })] [HarmonyPriority(200)] private static void Prefix(MobSpawner __instance, ref int __1, ref float __3, ref float __4, int __6) { try { if (!LocalClient.serverOwner || (Object)(object)__instance == (Object)null || __instance.allMobs == null || __1 < 0 || __1 >= __instance.allMobs.Length || (Object)(object)__instance.allMobs[__1] == (Object)null || (Object)(object)GameManager.instance == (Object)null || Ror2EnemiesPlugin.Cfg == null) { return; } MobCatalog.Ensure(); Items.Ensure(); SpawnRequest req = new SpawnRequest(SpawnContext.Current, GameManager.instance.currentDay, __instance.allMobs[__1].name, __3, __4, __6, Random.value, TitanShrines.ShrineConverts(SpawnContext.Shrine), DebugSpawner.Forced, Ror2Enemy.LiveCount); SpawnDecision spawnDecision = SpawnPlanner.Decide(in req, Ror2EnemiesPlugin.Cfg.Rules()); if (spawnDecision.Kind != EnemyKind.None) { if (!MobCatalog.TryId(spawnDecision.SpawnedMobName, out var id)) { Ror2EnemiesPlugin.LogOnce("spawn-id-" + spawnDecision.SpawnedMobName, "RoR2 Enemies: no id for " + spawnDecision.SpawnedMobName + ". Left vanilla."); return; } __1 = id; __3 = spawnDecision.Multiplier; __4 = spawnDecision.BossMultiplier; if (Ror2EnemiesPlugin.Cfg.VerboseLogging.Value) { Ror2EnemiesPlugin.Log.LogInfo((object)$"RoR2 Enemies: converted {req.SourceMobName} to {spawnDecision.Kind}."); } } else if (spawnDecision.Reason == SpawnReason.Nudged) { __4 = spawnDecision.BossMultiplier; if (Ror2EnemiesPlugin.Cfg.VerboseLogging.Value) { Ror2EnemiesPlugin.Log.LogInfo((object)"RoR2 Enemies: nudged a bossMultiplier that already decoded."); } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("spawn", $"RoR2 Enemies: spawn conversion failed, left vanilla. {arg}"); } } } [HarmonyPatch] internal static class ShrineGetNamePatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ShrineBoss), "GetName", Type.EmptyTypes, (Type[])null); } private static void Postfix(ShrineBoss __instance, ref string __result) { try { if ((Object)(object)__instance != (Object)null && TitanShrines.IsTitan(__instance)) { __result = "Challenge Stone Titan"; } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("titan-shrine-name", $"RoR2 Enemies: shrine name failed. {arg}"); } } } [HarmonyPatch] internal static class ShrineServerExecutePatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ShrineBoss), "ServerExecute", new Type[1] { typeof(int) }, (Type[])null); } private static void Prefix(ShrineBoss __instance) { try { if (LocalClient.serverOwner && !((Object)(object)__instance == (Object)null) && TitanShrines.IsTitan(__instance)) { MobType label = TitanShrines.Label; if (!((Object)(object)label == (Object)null)) { __instance.boss = label; } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("titan-shrine", $"RoR2 Enemies: shrine label failed. {arg}"); } } } [HarmonyPatch] internal static class SpawnFxPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(SpawnAttackFx), "SpawnFx", new Type[1] { typeof(int) }, (Type[])null); } private static bool Prefix(SpawnAttackFx __instance, int n, ref float __state) { __state = float.NaN; try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null) { return true; } if (component.Kind == EnemyKind.Jellyfish) { return component.Fuse != null && component.Fuse.BitingBuild; } if (component.Kind == EnemyKind.StoneTitan && component.AttackIndex == 0) { return false; } FxLifetimePatch.NoteTaggedSpawn(__instance, n); if (component.Kind != EnemyKind.Lemurian || (Object)(object)component.Mob == (Object)null || component.Spec == null) { return true; } __state = component.Mob.multiplier; component.Mob.multiplier = DamageTuning.MeleeRefineMultiplier(__state, component.Spec.DamageScale, component.Spec.MeleeFactor); } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("lemurian-bite", $"RoR2 Enemies: Lemurian bite refine failed, left the Goblin bite. {arg}"); } return true; } private static Exception Finalizer(SpawnAttackFx __instance, float __state, Exception __exception) { try { if (float.IsNaN(__state) || (Object)(object)__instance == (Object)null) { return __exception; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component != (Object)null && (Object)(object)component.Mob != (Object)null) { component.Mob.multiplier = __state; } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("lemurian-bite-restore", $"RoR2 Enemies: Lemurian bite restore failed. {arg}"); } return __exception; } } [HarmonyPatch] internal static class SpawnMobAdoptPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(MobSpawner), "SpawnMob", new Type[7] { typeof(Vector3), typeof(int), typeof(int), typeof(float), typeof(float), typeof(BossType), typeof(int) }, (Type[])null); } private static void Prefix(MobSpawner __instance, int __1, float __4, out EnemyKind __state) { __state = EnemyKind.None; try { if (!((Object)(object)__instance == (Object)null) && __instance.allMobs != null && __1 >= 0 && __1 < __instance.allMobs.Length && !((Object)(object)__instance.allMobs[__1] == (Object)null)) { RoR2Marker.TryDecode(__4, __instance.allMobs[__1].name, out __state); } } catch (Exception arg) { __state = EnemyKind.None; Ror2EnemiesPlugin.LogOnce("decode", $"RoR2 Enemies: decode failed. {arg}"); } } private static void Postfix(int __2, EnemyKind __state) { if (__state == EnemyKind.None) { return; } try { Mob value = null; if (!((Object)(object)MobManager.Instance != (Object)null) || MobManager.Instance.mobs == null || !MobManager.Instance.mobs.TryGetValue(__2, out value) || !((Object)(object)value != (Object)null)) { Ror2EnemiesPlugin.LogOnce("adopt-missing", $"RoR2 Enemies: mob {__2} was not in MobManager.mobs."); } else if (!((Object)(object)((Component)value).GetComponent<Ror2Enemy>() != (Object)null)) { ((Component)value).gameObject.AddComponent<Ror2Enemy>().Adopt(__state); HostPresence.Confirm(); if (Ror2EnemiesPlugin.Cfg != null && Ror2EnemiesPlugin.Cfg.VerboseLogging.Value) { Ror2EnemiesPlugin.Log.LogInfo((object)$"RoR2 Enemies: adopted {__state} mob {__2}."); } } } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("adopt", $"RoR2 Enemies: adopt failed. {arg}"); } } } [HarmonyPatch] internal static class StoneFistPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ProjectileAttackNoGravity), "SpawnProjectilePredictionTrajectory", Type.EmptyTypes, (Type[])null); } private static bool Prefix(ProjectileAttackNoGravity __instance) { //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) bool flag = false; try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null || component.Kind != EnemyKind.StoneTitan) { return true; } flag = true; if (!LocalClient.serverOwner) { return false; } Mob mob = component.Mob; if ((Object)(object)mob == (Object)null || (Object)(object)mob.target == (Object)null) { return false; } Items.Ensure(); InventoryItem spikeAttack = Items.SpikeAttack; if ((Object)(object)spikeAttack == (Object)null || (Object)(object)spikeAttack.bowComponent == (Object)null || (Object)(object)ProjectileController.Instance == (Object)null) { Ror2EnemiesPlugin.LogOnce("titan-fist", "RoR2 Enemies: Stone Titan could not throw a fist. Left the rock unfired."); return false; } Vector3 position = mob.target.position; Rigidbody component2 = ((Component)mob.target).GetComponent<Rigidbody>(); Vector3 val = (((Object)(object)component2 != (Object)null) ? component2.velocity : Vector3.zero); Vec3 vec = StoneFistAim.Predict(new Vec3(position.x, position.y, position.z), new Vec3(val.x, val.y, val.z), spikeAttack.bowComponent.timeToImpact); Vector3 point = default(Vector3); ((Vector3)(ref point))..ctor(vec.X, vec.Y, vec.Z); RaycastHit val2 = default(RaycastHit); if (Physics.Raycast(point + Vector3.up * 50f, Vector3.down, ref val2, 200f, 512)) { point = ((RaycastHit)(ref val2)).point; } ServerSend.MobSpawnProjectile(point, Vector3.forward, 0f, spikeAttack.id, mob.id); ProjectileController.Instance.SpawnMobProjectile(point, Vector3.forward, 0f, spikeAttack.id, mob.id); return false; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("titan-fist", $"RoR2 Enemies: Stone Fist failed. {arg}"); return !flag; } } } [HarmonyPatch] internal static class TitanAttackPatch { private static FieldRef<MobServerEnemy, bool> serverReady; private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(MobServerEnemyMeleeAndRanged), "AttackBehaviour", Type.EmptyTypes, (Type[])null); } private static bool Prefix(MobServerEnemyMeleeAndRanged __instance) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) bool flag = false; try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null || component.Kind != EnemyKind.StoneTitan) { return true; } Mob mob = component.Mob; TitanInputs input = new TitanInputs(Time.time, Vector3.Distance(mob.target.position, ((Component)__instance).transform.position), VanillaAngle(((Component)__instance).transform, mob.target), component.HostLineOfSight()); LaserGate laser; TitanSkill titanSkill = TitanScheduler.Pick(in input, ref component.TitanCooldowns, out laser); if (component.LaserSkips.ShouldLog(laser, input.Now)) { Ror2EnemiesPlugin.Log.LogInfo((object)($"RoR2 Enemies: Titan {mob.GetId()} held the laser: {laser} at {input.Distance:F1} m, " + $"sight {input.HasLineOfSight}, ready in {Mathf.Max(0f, component.TitanCooldowns.LaserReadyAt - input.Now):F1} s. Picked {titanSkill}.")); } if (titanSkill == TitanSkill.None) { return false; } int num = TitanScheduler.AttackIndex(titanSkill); float num2 = mob.attackTimes[num] + Random.Range(0f, mob.attackCooldown); mob.Attack(mob.targetPlayerId, num); flag = true; ServerSend.MobAttack(mob.GetId(), mob.targetPlayerId, num); HoldFire((MobServerEnemy)(object)__instance); ((MonoBehaviour)__instance).Invoke("GetReady", num2); return false; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("titan-ai", $"RoR2 Enemies: Titan AI failed, using vanilla. {arg}"); return !flag; } } private static void HoldFire(MobServerEnemy server) { if (serverReady == null) { serverReady = AccessTools.FieldRefAccess<MobServerEnemy, bool>("serverReadyToAttack"); } serverReady.Invoke(server) = false; } private static float VanillaAngle(Transform body, Transform target) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) Vector3 val = target.position - body.position; Vector3 forward = body.forward; val.y = 0f; forward.y = 0f; if (((Vector3)(ref val)).sqrMagnitude < 1E-08f || ((Vector3)(ref forward)).sqrMagnitude < 1E-08f) { return 0f; } return Vector3.Angle(forward, val); } } [HarmonyPatch] internal static class TitanDeathPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(HitableMob), "OnKill", new Type[1] { typeof(Vector3) }, (Type[])null); } private static Exception Finalizer(HitableMob __instance, Exception __exception) { try { if ((Object)(object)__instance == (Object)null) { return __exception; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null || component.Kind != EnemyKind.StoneTitan || (Object)(object)component.Mob == (Object)null) { return __exception; } if ((Object)(object)BossUI.Instance != (Object)null && (Object)(object)BossUI.Instance.currentBoss == (Object)(object)component.Mob) { BossUI.Instance.currentBoss = null; } if (__exception == null) { return null; } Ror2EnemiesPlugin.LogOnce("titan-death", $"RoR2 Enemies: Titan death threw before the despawn. Finished it. {__exception}"); if ((Object)(object)MobManager.Instance != (Object)null) { MobManager.Instance.RemoveMob(component.Mob.id); } Object.Destroy((Object)(object)((Component)__instance).gameObject); return null; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("titan-death-bar", $"RoR2 Enemies: Titan boss bar release failed. {arg}"); return __exception; } } } [HarmonyPatch] internal static class WaveRunContextPatch { private static readonly Type[] Parameters = new Type[6] { typeof(int), typeof(Vector3), typeof(float), typeof(float), typeof(BossType), typeof(int).MakeByRefType() }; internal static MethodBase TargetMethod() { Type type = HostType(); if (!(type == null)) { return AccessTools.DeclaredMethod(type, "TrySpawn", Parameters, (Type[])null); } return null; } internal static Type HostType() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type type; try { type = assembly.GetType("MuckWaveRun.Game.WaveHost", throwOnError: false); } catch (Exception) { continue; } if (type != null) { return type; } } return null; } private static void Prefix(BossType __4, out SpawnSource __state) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected I4, but got Unknown __state = SpawnContext.Current; try { SpawnContext.Current = WaveRunSpawn.SourceFor((int)__4); } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-waverun", $"RoR2 Enemies: Wave Run context failed. {arg}"); } } private static Exception Finalizer(SpawnSource __state, Exception __exception) { try { SpawnContext.Current = __state; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("ctx-waverun-restore", $"RoR2 Enemies: Wave Run context restore failed. {arg}"); } return __exception; } } [HarmonyPatch] internal static class WispHoverPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(MobFlying), "ExtraUpdate", Type.EmptyTypes, (Type[])null); } private static bool Prefix(MobFlying __instance) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null || component.Kind != EnemyKind.LesserWisp) { return true; } NavMeshAgent agent = ((Mob)__instance).agent; if ((Object)(object)agent == (Object)null) { return false; } float heightAboveGround = float.NaN; Vector3 position = ((Component)__instance).transform.position; RaycastHit val = default(RaycastHit); if (Physics.Raycast(position, Vector3.down, ref val, 5000f, LayerMask.op_Implicit(__instance.whatIsGround))) { heightAboveGround = position.y - ((RaycastHit)(ref val)).point.y; } float targetOffset = WispHover.TargetOffset(Ror2EnemiesPlugin.WispHoverHeight(), agent.baseOffset, heightAboveGround); agent.baseOffset = WispHover.Step(agent.baseOffset, targetOffset, Time.deltaTime); return false; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("wisp-hover", $"RoR2 Enemies: Lesser Wisp hover failed, left the Wyvern hover. {arg}"); return true; } } } [HarmonyPatch] internal static class WispVolleyPatch { private static MethodBase TargetMethod() { return AccessTools.DeclaredMethod(typeof(ProjectileAttackNoGravity), "SpawnProjectile", Type.EmptyTypes, (Type[])null); } private static bool Prefix(ProjectileAttackNoGravity __instance) { //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_021e: Unknown result type (might be due to invalid IL or missing references) //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_023a: Unknown result type (might be due to invalid IL or missing references) //IL_023c: Unknown result type (might be due to invalid IL or missing references) bool flag = false; try { if ((Object)(object)__instance == (Object)null) { return true; } Ror2Enemy component = ((Component)__instance).GetComponent<Ror2Enemy>(); if ((Object)(object)component == (Object)null || component.Kind != EnemyKind.LesserWisp) { return true; } flag = true; if (!LocalClient.serverOwner) { return false; } Mob mob = component.Mob; if ((Object)(object)mob == (Object)null || (Object)(object)mob.target == (Object)null || (Object)(object)__instance.spawnPos == (Object)null) { return false; } Items.Ensure(); InventoryItem fireball = Items.Fireball; if ((Object)(object)fireball == (Object)null || (Object)(object)fireball.bowComponent == (Object)null || (Object)(object)ProjectileController.Instance == (Object)null) { Ror2EnemiesPlugin.LogOnce("wisp-shot", "RoR2 Enemies: Lesser Wisp could not fire a volley. Left the shot unfired."); return false; } Vector3 position = __instance.spawnPos.position; Vector3 val = mob.target.position + Vector3.up * 1f; float num = fireball.bowComponent.projectileSpeed * __instance.attackForce; VolleyBolt[] array = WispVolley.Plan(new Vec3(position.x, position.y, position.z), new Vec3(val.x, val.y, val.z), Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f), Random.Range(-1f, 1f)); Vector3 val2 = default(Vector3); Vector3 val3 = default(Vector3); for (int i = 0; i < array.Length; i++) { ((Vector3)(ref val2))..ctor(array[i].Dir.X, array[i].Dir.Y, array[i].Dir.Z); ((Vector3)(ref val3))..ctor(array[i].From.X, array[i].From.Y, array[i].From.Z); ServerSend.MobSpawnProjectile(val3, val2, num, fireball.id, mob.id); ProjectileController.Instance.SpawnMobProjectile(val3, val2, num, fireball.id, mob.id); } return false; } catch (Exception arg) { Ror2EnemiesPlugin.LogOnce("wisp-volley", $"RoR2 Enemies: Lesser Wisp volley failed. {arg}"); return !flag; } } } internal static class PatchGroups { internal static bool CoreApplied; internal static void Apply(Harmony harmony) { try { CoreApplied = TryGroup(harmony, "core", typeof(ServerSpawnNewMobPatch), typeof(CheckMobSpawnsContextPatch), typeof(StartBossContextPatch), typeof(ShrineBossContextPatch), typeof(BattleTotemContextPatch), typeof(SpawnMobAdoptPatch), typeof(LootPatch)); if (!CoreApplied) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: mod disabled."); return; } KindRegistry.LemurianAvailable = TryGroup(harmony, "Lemurian", typeof(SpawnFxPatch)); if (!KindRegistry.LemurianAvailable) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Lemurian stays a Goblin."); } bool flag = TryGroup(harmony, "Jellyfish", typeof(MobAttackPatch), typeof(JellyAttackGatePatch), typeof(RagdollPatch)); KindRegistry.JellyfishAvailable = flag && KindRegistry.LemurianAvailable; if (!KindRegistry.JellyfishAvailable) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Jellyfish stays a lil Dave."); } KindRegistry.WispAvailable = TryGroup(harmony, "Wisp", typeof(WispVolleyPatch)); if (!KindRegistry.WispAvailable) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Lesser Wisp stays a lil Dave."); } if (!TryGroup(harmony, "Wisp hover", typeof(WispHoverPatch))) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Lesser Wisps keep the Wyvern hover height."); } if (!TryGroup(harmony, "FX cleanup", typeof(FxLifetimePatch))) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: ground attack effects stay in the scene, as in vanilla."); } KindRegistry.TitanAvailable = TryGroup(harmony, "Titan", typeof(TitanAttackPatch), typeof(StoneFistPatch), typeof(ShrineGetNamePatch), typeof(ShrineServerExecutePatch)) && KindRegistry.LemurianAvailable && flag; if (!KindRegistry.TitanAvailable) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Stone Titan stays a Big Chunk."); } if (!TryGroup(harmony, "Titan death", typeof(TitanDeathPatch))) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: the boss bar closes only through the vanilla path."); } ApplyWaveRun(harmony); } catch (Exception arg) { CoreApplied = false; Ror2EnemiesPlugin.Log.LogError((object)$"RoR2 Enemies: hook install failed and the mod is disabled. {arg}"); } } private static void ApplyWaveRun(Harmony harmony) { try { if (WaveRunContextPatch.HostType() == null) { Ror2EnemiesPlugin.Log.LogInfo((object)"RoR2 Enemies: Wave Run is not installed. Only vanilla spawns convert."); } else if (WaveRunContextPatch.TargetMethod() == null) { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Wave Run's WaveHost.TrySpawn has changed. Wave Run rounds stay vanilla."); } else if (TryGroup(harmony, "Wave Run", typeof(WaveRunContextPatch))) { Ror2EnemiesPlugin.Log.LogInfo((object)"RoR2 Enemies: Wave Run rounds and bosses convert too."); } else { Ror2EnemiesPlugin.Log.LogError((object)"RoR2 Enemies: Wave Run rounds stay vanilla."); } } catch (Exception arg) { Ror2EnemiesPlugin.Log.LogError((object)$"RoR2 Enemies: Wave Run hook failed. Wave Run rounds stay vanilla. {arg}"); } } private static bool TryGroup(Harmony harmony, string name, params Type[] patchClasses) { List<MethodBase> list = new List<MethodBase>(); try { foreach (Type type in patchClasses) { MethodInfo methodInfo = AccessTools.Method(type, "TargetMethod", (Type[])null, (Type[])null); if (methodInfo == null) { throw new MissingMethodException(type.Name + ": TargetMethod n