Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of MonsterDanceBreak v0.2.1
BepInEx/plugins/MonsterDanceBreak/MonsterDanceBreak.dll
Decompiled 16 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using EmotesAPI; using MonoMod.RuntimeDetour; using On.RoR2; using R2API.Networking; using R2API.Networking.Interfaces; using R2API.Utils; using RoR2; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.0")] namespace MonsterDanceBreak; public sealed class DanceMessage : INetMessage, ISerializableObject { public NetworkInstanceId BodyId; public uint Sequence; public string Scene; public string Emote; public int Variant; public int AudioEvent; public float Duration; public bool Stop; public void Serialize(NetworkWriter writer) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) writer.Write(BodyId); writer.Write(Sequence); writer.Write(Scene); writer.Write(Emote); writer.Write(Variant); writer.Write(AudioEvent); writer.Write(Duration); writer.Write(Stop); } public void Deserialize(NetworkReader reader) { //IL_0002: 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) BodyId = reader.ReadNetworkId(); Sequence = reader.ReadUInt32(); Scene = reader.ReadString(); Emote = reader.ReadString(); Variant = reader.ReadInt32(); AudioEvent = reader.ReadInt32(); Duration = reader.ReadSingle(); Stop = reader.ReadBoolean(); } public void OnReceived() { if (!NetworkServer.active && NetworkClient.active && Object.op_Implicit((Object)(object)Plugin.Instance)) { Plugin.Instance.Receive(this); } } } internal static class DancePolicy { public const float HardLimit = 60f; public static float Bound(float value, float min, float max, float fallback) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(min, Math.Min(max, value)); } return fallback; } public static float Duration(float requested, bool looping, bool hasSecondary, float clipLength) { float num = Bound(requested, 1f, 60f, 25f); if (!(looping || hasSecondary)) { return Math.Min(num, Bound(clipLength, 0.1f, 60f, 1f)); } return num; } public static int Choose(Random random, int count, int previous) { if (count <= 0) { return -1; } if (count == 1) { return 0; } if (previous < 0 || previous >= count) { return random.Next(count); } int num = random.Next(count - 1); if (num < previous) { return num; } return num + 1; } } internal sealed class DanceLifetime { public readonly float Deadline; public readonly float ReadyDeadline; public bool Started { get; private set; } public DanceLifetime(float now, float duration) { Deadline = now + DancePolicy.Bound(duration, 0.1f, 60f, 25f); ReadyDeadline = now + 2f; } public void Start() { Started = true; } public bool Expired(float now) { if (!(now >= Deadline)) { if (!Started) { return now >= ReadyDeadline; } return false; } return true; } } internal static class EnemyAnatomy { internal static readonly string[] BlockedFamilies = new string[54] { "Wisp", "GreaterWisp", "LunarWisp", "FlyingVermin", "Vulture", "Vagrant", "Jellyfish", "BellBody", "RoboBall", "SuperRoboBall", "MagmaWorm", "ElectricWorm", "Scorchling", "AcidLarva", "Vermin", "Bison", "Gup", "Geep", "Gip", "BeetleQueen", "ClayBoss", "Gravekeeper", "HermitCrab", "MiniMushroom", "LunarExploder", "Nullifier", "VoidInfestor", "VoidBarnacle", "VoidJailer", "VoidMegaCrab", "VoidRaidCrab", "MiniVoidRaidCrab", "MinorConstruct", "MegaConstruct", "BackupDrone", "Drone1", "Drone2", "EmergencyDrone", "EquipmentDrone", "FlameDrone", "MegaDrone", "MissileDrone", "DroneCommander", "Turret1", "EngiTurret", "EngiWalkerTurret", "DefectiveUnit", "ExtractorUnit", "IronHauler", "SolusAmalgamator", "SolusHeart", "SolusWing", "Tanker", "WorkerUnit" }; internal static string Normalize(string name) { return (name ?? "").Replace("(Clone)", "").Trim(); } internal static bool Blocked(string bodyName, bool flying, string extra) { string text = Normalize(bodyName); if (flying || text.Length == 0) { return true; } string[] blockedFamilies = BlockedFamilies; foreach (string value in blockedFamilies) { if (text.StartsWith(value, StringComparison.OrdinalIgnoreCase)) { return true; } } blockedFamilies = (extra ?? "").Split(';'); for (int i = 0; i < blockedFamilies.Length; i++) { string text2 = Normalize(blockedFamilies[i]); if (text2.EndsWith("*", StringComparison.Ordinal)) { if (text.StartsWith(text2.Substring(0, text2.Length - 1), StringComparison.OrdinalIgnoreCase)) { return true; } } else if (string.Equals(text2, text, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } } internal sealed class PingBudget { private float next; private readonly Dictionary<uint, float> ready = new Dictionary<uint, float>(); public bool CanStart(uint id, float now, bool alreadyDancing) { if (id != 0 && !alreadyDancing && now >= next) { if (ready.TryGetValue(id, out var value)) { return now >= value; } return true; } return false; } public void Commit(uint id, float now, float deadline) { next = now + 1f; ready[id] = deadline + 12f; } public void Clear() { next = 0f; ready.Clear(); } } internal sealed class CrowdMemory { private float lastCrowd = float.NegativeInfinity; private bool attempted; public bool Observe(int livingNearbyIncludingSelf, float now) { if (livingNearbyIncludingSelf >= 3) { lastCrowd = now; } if (attempted || livingNearbyIncludingSelf != 1 || now - lastCrowd > 10f) { return false; } attempted = true; return true; } } [BepInPlugin("com.rodanius.monsterdancebreak", "Monster Dance Break", "0.2.1")] [BepInDependency("com.weliveinasociety.CustomEmotesAPI", "2.11.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { private sealed class Dance { public DanceMessage Message; public DanceLifetime Lifetime; public CharacterBody Body; public BoneMapper Mapper; public CustomAnimationClip Clip; } private sealed class Entrance { public CharacterBody Body; public float Deadline; public bool Boss; } private sealed class Reaction { public Vector3 Origin; public float Radius; public float Deadline; public string Reason; public string Emote; public int Maximum; public CharacterBody Target; public BoneMapper PlayerMapper; } public const string Guid = "com.rodanius.monsterdancebreak"; internal static Plugin Instance; private readonly Random random = new Random(); private readonly Dictionary<uint, Dance> dances = new Dictionary<uint, Dance>(); private readonly Dictionary<uint, uint> received = new Dictionary<uint, uint>(); private readonly HashSet<uint> danced = new HashSet<uint>(); private ConfigEntry<bool> modEnabled; private ConfigEntry<bool> lineOfSight; private ConfigEntry<bool> allowInteractive; private ConfigEntry<bool> diagnostics; private ConfigEntry<float> intervalMin; private ConfigEntry<float> intervalMax; private ConfigEntry<float> durationMin; private ConfigEntry<float> durationMax; private ConfigEntry<float> radius; private ConfigEntry<float> groupChance; private ConfigEntry<float> groupRadius; private ConfigEntry<string> excluded; private ConfigEntry<string> excludedBodies; private float nextEvent; private float nextDiagnostic; private uint sequence; private string previousEmote; private ConfigEntry<bool> pingEnabled; private ConfigEntry<float> spawnChance; private ConfigEntry<float> bossChance; private ConfigEntry<float> shrineChance; private ConfigEntry<float> encoreChance; private ConfigEntry<float> lastStandChance; private ConfigEntry<float> afterpartyChance; private ConfigEntry<float> tauntChance; private ConfigEntry<float> quietMin; private ConfigEntry<float> quietMax; private readonly List<Entrance> entrances = new List<Entrance>(); private readonly Dictionary<CharacterBody, CrowdMemory> crowds = new Dictionary<CharacterBody, CrowdMemory>(); private readonly Dictionary<uint, float> hitChecks = new Dictionary<uint, float>(); private readonly PingBudget pingBudget = new PingBudget(); private float nextAutomatic; private float nextEntranceCheck; private float nextCrowdCheck; private Reaction reaction; private bool HostReady { get { if (Object.op_Implicit((Object)(object)Run.instance) && NetworkServer.active && NetworkClient.active && modEnabled.Value) { return !PauseManager.isPaused; } return false; } } private bool AutomaticReady { get { if (HostReady && dances.Count == 0) { return Time.time >= nextAutomatic; } return false; } } private void Awake() { SolusWingGuard.Install(((BaseUnityPlugin)this).Logger); Instance = this; modEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Host controls all dance scenarios. Turning off ends current events."); intervalMin = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "AmbientMinimumQuietSeconds", 90f, "Rare mid-combat fallback delay after an event ends. Spawn entrances are the main trigger."); intervalMax = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "AmbientMaximumQuietSeconds", 150f, "Maximum randomized quiet time after an event ends."); durationMin = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "MinimumDanceSeconds", 20f, "Looping emote minimum; short emotes finish naturally."); durationMax = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "MaximumDanceSeconds", 45f, "Looping emote maximum. Absolute safety limit is 60 seconds."); radius = ((BaseUnityPlugin)this).Config.Bind<float>("Selection", "PlayerDistance", 60f, "Maximum distance from a living player."); lineOfSight = ((BaseUnityPlugin)this).Config.Bind<bool>("Selection", "RequireLineOfSight", true, "Prefer moments a player could see; checks world geometry, not camera direction."); groupChance = ((BaseUnityPlugin)this).Config.Bind<float>("Groups", "Chance", 0.25f, "Chance to include nearby enemies. Everyone uses the same random emote and variant."); groupRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Groups", "NeighborDistance", 16f, "Neighbors of the first dancer; maximum three dancers per event."); allowInteractive = ((BaseUnityPlugin)this).Config.Bind<bool>("Selection", "IncludeInteractiveEmotes", false, "Include emotes with partner join spots. Experimental: some callbacks move or alter characters."); excluded = ((BaseUnityPlugin)this).Config.Bind<string>("Selection", "ExcludedEmotes", "", "Exact registered emote names separated by semicolons."); diagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Diagnostics", true, "Log events and occasional reasons no candidate was available."); excludedBodies = ((BaseUnityPlugin)this).Config.Bind<string>("Selection", "ExcludedBodies", "", "Additional body prefab IDs separated by semicolons; a trailing * matches a family. Built-in anatomy and flying exclusions always apply."); ConfigureScenarios(); NetworkingAPI.RegisterMessageType<DanceMessage>(); SceneManager.activeSceneChanged += SceneChanged; Run.onRunDestroyGlobal += RunEnded; SubscribeScenarios(); ResetSchedule(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"MDB_READY 0.2.1: entrances, ping spotlight and contextual moments; anatomy blacklist active."); } private float Range(float low, float high, float floor, float ceiling, float fallback) { low = DancePolicy.Bound(low, floor, ceiling, fallback); high = DancePolicy.Bound(high, low, ceiling, low); return low + (float)random.NextDouble() * (high - low); } private void ResetSchedule() { nextAutomatic = Time.time; nextEvent = Time.time + Range(intervalMin.Value, intervalMax.Value, 60f, 600f, 90f); } private void ScheduleQuiet() { nextAutomatic = Time.time + Range(quietMin.Value, quietMax.Value, 10f, 120f, 20f); nextEvent = Time.time + Range(intervalMin.Value, intervalMax.Value, 60f, 600f, 90f); } private void SceneChanged(Scene oldScene, Scene newScene) { Clear(broadcast: false); danced.Clear(); received.Clear(); ClearScenarios(); ResetSchedule(); } private void RunEnded(Run run) { Clear(broadcast: false); danced.Clear(); received.Clear(); ClearScenarios(); } private void Update() { if (!Object.op_Implicit((Object)(object)Run.instance)) { if (dances.Count > 0) { Clear(broadcast: false); } } else { if (PauseManager.isPaused) { return; } uint[] array = dances.Keys.ToArray(); foreach (uint num in array) { Dance dance = dances[num]; try { if (dance.Lifetime.Expired(Time.time) || (NetworkServer.active && !modEnabled.Value)) { End(num, broadcast: true); } else if (!dance.Lifetime.Started) { TryStart(dance); } else if (!AliveEnemy(dance.Body) || !Object.op_Implicit((Object)(object)dance.Mapper) || dance.Mapper.ragdolling) { End(num, broadcast: true); } else if (dance.Mapper.currentClip != dance.Clip || dance.Mapper.currentClipName != dance.Message.Emote) { End(num, broadcast: true); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("MDB_END_ERROR " + ex)); End(num, broadcast: true); } } if (!NetworkServer.active || !modEnabled.Value) { return; } if (NetworkClient.active) { try { UpdateScenarios(); if (entrances.Count == 0 && AutomaticReady && Time.time >= nextEvent) { SelectEvent(); } return; } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("MDB_SELECTION_ERROR " + ex2)); ScheduleQuiet(); return; } } nextEvent = Time.time + 60f; } } private static bool AliveEnemy(CharacterBody body) { //IL_0060: 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_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Invalid comparison between Unknown and I4 //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Invalid comparison between Unknown and I4 //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Invalid comparison between Unknown and I4 if (!Object.op_Implicit((Object)(object)body) || !Object.op_Implicit((Object)(object)body.healthComponent) || !body.healthComponent.alive || body.isPlayerControlled || !Object.op_Implicit((Object)(object)body.teamComponent) || !Object.op_Implicit((Object)(object)body.master)) { return false; } if (Object.op_Implicit((Object)(object)body.master.playerCharacterMasterController)) { return false; } TeamIndex teamIndex = body.teamComponent.teamIndex; if ((int)teamIndex != 2 && (int)teamIndex != 3) { return (int)teamIndex == 4; } return true; } private static bool ReadyMapper(CharacterBody body, out BoneMapper mapper) { mapper = null; if (!Object.op_Implicit((Object)(object)body) || !Object.op_Implicit((Object)(object)body.modelLocator) || !Object.op_Implicit((Object)(object)body.modelLocator.modelTransform)) { return false; } mapper = ((Component)body.modelLocator.modelTransform).GetComponentInChildren<BoneMapper>(); if (Object.op_Implicit((Object)(object)mapper) && ((Behaviour)mapper).isActiveAndEnabled && (Object)(object)mapper.mapperBody == (Object)(object)body && Object.op_Implicit((Object)(object)mapper.a1) && Object.op_Implicit((Object)(object)mapper.a2) && Object.op_Implicit((Object)(object)mapper.a2.runtimeAnimatorController)) { return !mapper.ragdolling; } return false; } private bool NearPlayer(CharacterBody body) { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) float num = DancePolicy.Bound(radius.Value, 5f, 150f, 60f); foreach (TeamComponent teamMember in TeamComponent.GetTeamMembers((TeamIndex)1)) { CharacterBody body2 = teamMember.body; if (!Object.op_Implicit((Object)(object)body2) || !body2.isPlayerControlled || !Object.op_Implicit((Object)(object)body2.healthComponent) || !body2.healthComponent.alive) { continue; } Vector3 val = body2.corePosition - body.corePosition; if (((Vector3)(ref val)).sqrMagnitude > num * num) { continue; } if (lineOfSight.Value) { Vector3 corePosition = body2.corePosition; Vector3 corePosition2 = body.corePosition; LayerIndex world = LayerIndex.world; if (Physics.Linecast(corePosition, corePosition2, LayerMask.op_Implicit(((LayerIndex)(ref world)).mask), (QueryTriggerInteraction)1)) { continue; } } return true; } return false; } private bool AllowedBody(CharacterBody body) { if (Object.op_Implicit((Object)(object)body)) { return !EnemyAnatomy.Blocked(((Object)((Component)body).gameObject).name, body.isFlying, excludedBodies.Value); } return false; } private bool Candidate(CharacterBody body, bool ping = false) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0057: 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) if (!AliveEnemy(body) || !AllowedBody(body)) { return false; } NetworkIdentity component = ((Component)body).GetComponent<NetworkIdentity>(); if (Object.op_Implicit((Object)(object)component)) { NetworkInstanceId netId = component.netId; if (((NetworkInstanceId)(ref netId)).Value != 0) { Dictionary<uint, Dance> dictionary = dances; netId = component.netId; if (!dictionary.ContainsKey(((NetworkInstanceId)(ref netId)).Value)) { if (!ping) { HashSet<uint> hashSet = danced; netId = component.netId; if (hashSet.Contains(((NetworkInstanceId)(ref netId)).Value)) { goto IL_006b; } } if (!ReadyMapper(body, out var mapper) || mapper.currentClip != null) { return false; } if (!ping) { return NearPlayer(body); } return true; } } } goto IL_006b; IL_006b: return false; } private List<string> Emotes() { HashSet<string> hashSet = new HashSet<string>(from s in (excluded.Value ?? "").Split(';') select s.Trim(), StringComparer.OrdinalIgnoreCase); List<string> list = new List<string>(); for (int num = 0; num < CustomEmotesAPI.allClipNames.Count; num++) { string text = CustomEmotesAPI.allClipNames[num]; if (!(text == "none") && !hashSet.Contains(text) && !CustomEmotesAPI.blacklistedClips.Contains(num) && BoneMapper.animClips.TryGetValue(text, out var value) && value != null && value.visibility && ValidVariants(value).Count != 0 && (allowInteractive.Value || value.joinSpots == null || value.joinSpots.Length == 0) && !list.Contains(text)) { list.Add(text); } } return list; } private static List<int> ValidVariants(CustomAnimationClip clip) { List<int> list = new List<int>(); if (clip == null || clip.clip == null) { return list; } for (int i = 0; i < clip.clip.Length; i++) { if (Object.op_Implicit((Object)(object)clip.clip[i]) && (clip.secondaryClip == null || clip.secondaryClip.Length == 0 || (i < clip.secondaryClip.Length && Object.op_Implicit((Object)(object)clip.secondaryClip[i])))) { list.Add(i); } } return list; } private List<CharacterBody> Candidates() { return CharacterBody.readOnlyInstancesList.Where((CharacterBody b) => Candidate(b)).ToList(); } private List<CharacterBody> Group(CharacterBody leader, IEnumerable<CharacterBody> pool, float distance, int maximum = 3) { List<CharacterBody> list = new List<CharacterBody> { leader }; List<CharacterBody> list2 = pool.Where(delegate(CharacterBody b) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0024: 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_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)b != (Object)(object)leader && b.teamComponent.teamIndex == leader.teamComponent.teamIndex) { Vector3 val = b.corePosition - leader.corePosition; return ((Vector3)(ref val)).sqrMagnitude <= distance * distance; } return false; }).ToList(); while (list.Count < maximum && list2.Count > 0) { int index = random.Next(list2.Count); list.Add(list2[index]); list2.RemoveAt(index); } return list; } private void SelectEvent() { List<CharacterBody> list = Candidates(); if (list.Count == 0) { nextEvent = Time.time + 5f; if (diagnostics.Value && Time.time >= nextDiagnostic) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"MDB_WAIT no visible eligible ground enemy ready"); nextDiagnostic = Time.time + 60f; } return; } CharacterBody val = list[random.Next(list.Count)]; List<CharacterBody> bodies = ((random.NextDouble() < (double)DancePolicy.Bound(groupChance.Value, 0f, 1f, 0.25f)) ? Group(val, list, DancePolicy.Bound(groupRadius.Value, 2f, 40f, 16f)) : new List<CharacterBody> { val }); if (!StartEvent(bodies, "spontaneous")) { nextEvent = Time.time + 5f; } } private bool StartEvent(IEnumerable<CharacterBody> bodies, string reason, string coordinatedEmote = null, bool ping = false) { //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) List<string> list = Emotes(); if (list.Count == 0 || (coordinatedEmote != null && !list.Contains(coordinatedEmote))) { return false; } string text = coordinatedEmote ?? list[DancePolicy.Choose(random, list.Count, list.IndexOf(previousEmote))]; CustomAnimationClip val = BoneMapper.animClips[text]; List<int> list2 = ValidVariants(val); if (list2.Count == 0) { return false; } int num = list2[random.Next(list2.Count)]; string[] array = ((val.syncPos >= 0 && val.syncPos < BoneMapper.startEvents.Count) ? BoneMapper.startEvents[val.syncPos] : null); int audioEvent = ((array != null && array.Length != 0) ? random.Next(array.Length) : 0); bool hasSecondary = val.secondaryClip != null && val.secondaryClip.Length > num && Object.op_Implicit((Object)(object)val.secondaryClip[num]); float num2 = DancePolicy.Duration(Range(durationMin.Value, durationMax.Value, 1f, 60f, 25f), val.looping, hasSecondary, val.clip[num].length); int num3 = 0; foreach (CharacterBody item in bodies.Take(3)) { if (!Candidate(item, ping) || dances.Count >= 3) { continue; } DanceMessage obj = new DanceMessage { BodyId = ((Component)item).GetComponent<NetworkIdentity>().netId, Sequence = ++sequence }; Scene activeScene = SceneManager.GetActiveScene(); obj.Scene = ((Scene)(ref activeScene)).name; obj.Emote = text; obj.Variant = num; obj.AudioEvent = audioEvent; obj.Duration = num2; DanceMessage danceMessage = obj; Dance dance = new Dance { Message = danceMessage, Lifetime = new DanceLifetime(Time.time, num2), Body = item }; dances.Add(((NetworkInstanceId)(ref danceMessage.BodyId)).Value, dance); try { if (!TryStart(dance)) { End(((NetworkInstanceId)(ref danceMessage.BodyId)).Value, broadcast: false); continue; } danced.Add(((NetworkInstanceId)(ref danceMessage.BodyId)).Value); NetMessageExtensions.Send((INetMessage)(object)danceMessage, (NetworkDestination)1); if (ping) { pingBudget.Commit(((NetworkInstanceId)(ref danceMessage.BodyId)).Value, Time.time, dance.Lifetime.Deadline); } num3++; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("MDB_START_ERROR " + ex)); End(((NetworkInstanceId)(ref danceMessage.BodyId)).Value, broadcast: true); } } if (num3 == 0) { return false; } previousEmote = text; if (diagnostics.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)$"MDB_EVENT scenario={reason} count={num3} emote={text} variant={num} seconds={num2:F1}"); } return true; } internal void Receive(DanceMessage packet) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)Run.instance)) { return; } string scene = packet.Scene; Scene activeScene = SceneManager.GetActiveScene(); if (scene != ((Scene)(ref activeScene)).name || ((NetworkInstanceId)(ref packet.BodyId)).Value == 0) { return; } uint value = ((NetworkInstanceId)(ref packet.BodyId)).Value; if (!received.TryGetValue(value, out var value2) || packet.Sequence > value2) { received[value] = packet.Sequence; if (packet.Stop) { End(value, broadcast: false); } else if (!string.IsNullOrEmpty(packet.Emote) && packet.Emote.Length <= 256 && !(packet.Duration <= 0f) && !float.IsNaN(packet.Duration) && !float.IsInfinity(packet.Duration)) { End(value, broadcast: false); dances[value] = new Dance { Message = packet, Lifetime = new DanceLifetime(Time.time, packet.Duration) }; } } } private bool TryStart(Dance dance) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) DanceMessage message = dance.Message; if (!Object.op_Implicit((Object)(object)dance.Body)) { GameObject val = Util.FindNetworkObject(message.BodyId); if (Object.op_Implicit((Object)(object)val)) { dance.Body = val.GetComponent<CharacterBody>(); } } if (!AliveEnemy(dance.Body) || !AllowedBody(dance.Body) || !ReadyMapper(dance.Body, out var mapper)) { return false; } if (mapper.currentClip != null) { return false; } if (!BoneMapper.animClips.TryGetValue(message.Emote, out var value) || value == null || value.clip == null || !ValidVariants(value).Contains(message.Variant)) { return false; } dance.Mapper = mapper; dance.Clip = value; dance.Lifetime.Start(); mapper.PlayAnim(message.Emote, message.Variant, message.AudioEvent); return true; } private void End(uint id, bool broadcast) { //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) if (!dances.TryGetValue(id, out var value)) { return; } dances.Remove(id); try { if (value.Lifetime.Started && Object.op_Implicit((Object)(object)value.Mapper) && value.Mapper.currentClip == value.Clip && value.Mapper.currentClipName == value.Message.Emote) { value.Mapper.PlayAnim("none", 0, 0); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("MDB_CLEANUP_ERROR " + ex)); } finally { if (NetworkServer.active && broadcast) { DanceMessage danceMessage = new DanceMessage { BodyId = value.Message.BodyId, Sequence = ++sequence, Scene = value.Message.Scene, Emote = "", Stop = true }; try { NetMessageExtensions.Send((INetMessage)(object)danceMessage, (NetworkDestination)1); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("MDB_STOP_SEND_ERROR " + ex2)); } } if (NetworkServer.active && dances.Count == 0) { ScheduleQuiet(); } } } private void Clear(bool broadcast) { uint[] array = dances.Keys.ToArray(); foreach (uint id in array) { End(id, broadcast); } } private void OnDestroy() { Clear(broadcast: true); UnsubscribeScenarios(); ClearScenarios(); SceneManager.activeSceneChanged -= SceneChanged; Run.onRunDestroyGlobal -= RunEnded; if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private bool Roll(ConfigEntry<float> chance) { return random.NextDouble() < (double)DancePolicy.Bound(chance.Value, 0f, 1f, 0f); } private void ConfigureScenarios() { pingEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Scenarios", "PingSpotlight", true, "Ping an eligible enemy to make it dance. Does not extend active dances."); spawnChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "EntranceChance", 0.35f, "One roll per newly spawned enemy, with a 3-second rig readiness window. Main automatic trigger."); bossChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "BossEntranceChance", 0.65f, "A newly spawned eligible boss may arrive with up to two backing dancers."); shrineChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "ShrineHecklerChance", 0.6f, "Nearby monster reacts to a failed Shrine of Chance purchase."); encoreChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "PlayerEncoreChance", 0.4f, "Nearby monsters join a player's compatible emote."); lastStandChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "LastStandingChance", 0.4f, "A recently outnumbering monster dances when it becomes the sole nearby survivor."); afterpartyChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "TeleporterAfterpartyChance", 0.75f, "Surviving monsters near a fully charged teleporter may dance together."); tauntChance = ((BaseUnityPlugin)this).Config.Bind<float>("Scenarios", "DamageTauntChance", 0.06f, "Rare taunt after hurting a living player. Direct hits only; at most one roll per attacker per 5 seconds."); quietMin = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "AutomaticMinimumQuietSeconds", 20f, "Shared quiet period after all dancers finish; applies to every automatic scenario."); quietMax = ((BaseUnityPlugin)this).Config.Bind<float>("Timing", "AutomaticMaximumQuietSeconds", 35f, "Maximum shared automatic quiet period. Pings alone bypass this wait."); } private void SubscribeScenarios() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown CharacterBody.onBodyStartGlobal += BodyStarted; ShrineChanceBehavior.onShrineChancePurchaseGlobal += ShrinePurchased; TeleporterInteraction.onTeleporterChargedGlobal += TeleporterCharged; CustomEmotesAPI.animChanged += new AnimationChanged(PlayerEmoted); GlobalEventManager.onServerDamageDealt += DamageDealt; PingerController.CmdPing += new hook_CmdPing(ObservePing); } private void UnsubscribeScenarios() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown CharacterBody.onBodyStartGlobal -= BodyStarted; ShrineChanceBehavior.onShrineChancePurchaseGlobal -= ShrinePurchased; TeleporterInteraction.onTeleporterChargedGlobal -= TeleporterCharged; CustomEmotesAPI.animChanged -= new AnimationChanged(PlayerEmoted); GlobalEventManager.onServerDamageDealt -= DamageDealt; PingerController.CmdPing -= new hook_CmdPing(ObservePing); } private void ClearScenarios() { entrances.Clear(); crowds.Clear(); hitChecks.Clear(); pingBudget.Clear(); reaction = null; nextEntranceCheck = (nextCrowdCheck = 0f); } private void BodyStarted(CharacterBody body) { ObserveSafely(delegate { if (HostReady && Object.op_Implicit((Object)(object)body) && AllowedBody(body) && !body.isPlayerControlled) { bool flag = body.isBoss || body.isChampion; if (Roll(flag ? bossChance : spawnChance) && AutomaticReady) { entrances.Add(new Entrance { Body = body, Boss = flag, Deadline = Time.time + 3f }); } } }); } private void UpdateScenarios() { if (Time.time >= nextEntranceCheck) { nextEntranceCheck = Time.time + 0.1f; entrances.RemoveAll((Entrance e) => !Object.op_Implicit((Object)(object)e.Body) || Time.time >= e.Deadline); if (AutomaticReady) { List<Entrance> source = entrances.Where((Entrance e) => Candidate(e.Body)).ToList(); Entrance entrance = source.FirstOrDefault((Entrance e) => e.Boss) ?? source.FirstOrDefault(); if (entrance != null) { List<CharacterBody> bodies = new List<CharacterBody> { entrance.Body }; if (entrance.Boss || Roll(groupChance)) { CharacterBody body = entrance.Body; IEnumerable<CharacterBody> pool; if (!entrance.Boss) { pool = source.Select((Entrance e) => e.Body); } else { IEnumerable<CharacterBody> enumerable = Candidates(); pool = enumerable; } bodies = Group(body, pool, DancePolicy.Bound(groupRadius.Value, 2f, 40f, 16f)); } StartEvent(bodies, entrance.Boss ? "opening-act" : "entrance"); entrances.Clear(); } } else { entrances.Clear(); } } if (entrances.Count > 0 && AutomaticReady) { return; } if (reaction != null) { Reaction pending = reaction; reaction = null; if (AutomaticReady && Time.time <= pending.Deadline && (pending.PlayerMapper == null || (Object.op_Implicit((Object)(object)pending.PlayerMapper) && LivingPlayer(pending.PlayerMapper.mapperBody) && pending.PlayerMapper.currentClipName == pending.Emote))) { List<CharacterBody> list = Candidates().Where(delegate(CharacterBody b) { //IL_0001: 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_0016: Unknown result type (might be due to invalid IL or missing references) Vector3 val2 = b.corePosition - pending.Origin; return ((Vector3)(ref val2)).sqrMagnitude <= pending.Radius * pending.Radius; }).ToList(); CharacterBody val = ((!Object.op_Implicit((Object)(object)pending.Target)) ? ((list.Count > 0) ? list[random.Next(list.Count)] : null) : (list.Contains(pending.Target) ? pending.Target : null)); if (Object.op_Implicit((Object)(object)val)) { StartEvent(Group(val, list, pending.Radius, pending.Maximum), pending.Reason, pending.Emote); } } } if (Time.time < nextCrowdCheck) { return; } nextCrowdCheck = Time.time + 1f; CharacterBody[] array = crowds.Keys.Where((CharacterBody b) => !Object.op_Implicit((Object)(object)b) || !AliveEnemy(b)).ToArray(); foreach (CharacterBody key in array) { crowds.Remove(key); } List<CharacterBody> list2 = CharacterBody.readOnlyInstancesList.Where(AliveEnemy).ToList(); foreach (CharacterBody body2 in list2) { if (!AllowedBody(body2) || !NearPlayer(body2)) { continue; } if (!crowds.TryGetValue(body2, out var value)) { value = (crowds[body2] = new CrowdMemory()); } int livingNearbyIncludingSelf = list2.Count(delegate(CharacterBody b) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) if (b.teamComponent.teamIndex == body2.teamComponent.teamIndex) { Vector3 val2 = b.corePosition - body2.corePosition; return ((Vector3)(ref val2)).sqrMagnitude <= 400f; } return false; }); if (value.Observe(livingNearbyIncludingSelf, Time.time) && AutomaticReady && Candidate(body2) && Roll(lastStandChance)) { StartEvent((IEnumerable<CharacterBody>)(object)new CharacterBody[1] { body2 }, "last-one-standing"); } } } private void QueueReaction(Vector3 origin, float distance, string reason, int maximum = 1, string emote = null, CharacterBody target = null, BoneMapper playerMapper = null) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (AutomaticReady && reaction == null) { reaction = new Reaction { Origin = origin, Radius = distance, Reason = reason, Maximum = maximum, Emote = emote, Target = target, PlayerMapper = playerMapper, Deadline = Time.time + 1f }; } } private void ShrinePurchased(bool failed, Interactor interactor) { ObserveSafely(delegate { //IL_0056: Unknown result type (might be due to invalid IL or missing references) if (failed && AutomaticReady && Object.op_Implicit((Object)(object)interactor) && Roll(shrineChance)) { CharacterBody component = ((Component)interactor).GetComponent<CharacterBody>(); if (LivingPlayer(component)) { QueueReaction(component.corePosition, 30f, "shrine-heckler"); } } }); } private void TeleporterCharged(TeleporterInteraction teleporter) { ObserveSafely(delegate { //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (AutomaticReady && Object.op_Implicit((Object)(object)teleporter) && Roll(afterpartyChance)) { QueueReaction(((Component)teleporter).transform.position, 40f, "teleporter-afterparty", 3); } }); } private void PlayerEmoted(string name, BoneMapper mapper) { ObserveSafely(delegate { //IL_0080: Unknown result type (might be due to invalid IL or missing references) if (AutomaticReady && Object.op_Implicit((Object)(object)mapper) && LivingPlayer(mapper.mapperBody) && !(name == "none") && Roll(encoreChance) && Emotes().Contains(name)) { QueueReaction(mapper.mapperBody.corePosition, 24f, "follow-my-lead", 3, name, null, mapper); } }); } private void DamageDealt(DamageReport report) { ObserveSafely(delegate { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) if (AutomaticReady && report != null && !(report.damageDealt <= 0f) && (int)report.dotType == -1 && LivingPlayer(report.victimBody) && Candidate(report.attackerBody)) { NetworkInstanceId netId = ((Component)report.attackerBody).GetComponent<NetworkIdentity>().netId; uint value = ((NetworkInstanceId)(ref netId)).Value; if (!hitChecks.TryGetValue(value, out var value2) || !(Time.time < value2)) { hitChecks[value] = Time.time + 5f; if (Roll(tauntChance)) { QueueReaction(report.attackerBody.corePosition, 60f, "damage-taunt", 1, null, report.attackerBody); } } } }); } private static bool LivingPlayer(CharacterBody body) { if (Object.op_Implicit((Object)(object)body) && body.isPlayerControlled && Object.op_Implicit((Object)(object)body.healthComponent)) { return body.healthComponent.alive; } return false; } private void ObservePing(orig_CmdPing orig, PingerController self, PingInfo info) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) orig.Invoke(self, info); ObserveSafely(delegate { //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) if (HostReady && pingEnabled.Value && info.active && Object.op_Implicit((Object)(object)self)) { CharacterMaster component = ((Component)self).GetComponent<CharacterMaster>(); if (Object.op_Implicit((Object)(object)component) && Object.op_Implicit((Object)(object)component.playerCharacterMasterController) && LivingPlayer(component.GetBody())) { GameObject targetGameObject = ((PingInfo)(ref info)).targetGameObject; if (Object.op_Implicit((Object)(object)targetGameObject)) { CharacterBody val = targetGameObject.GetComponent<CharacterBody>(); if (!Object.op_Implicit((Object)(object)val)) { val = targetGameObject.GetComponentInParent<CharacterBody>(); } if (Candidate(val, ping: true)) { NetworkInstanceId netId = ((Component)val).GetComponent<NetworkIdentity>().netId; uint value = ((NetworkInstanceId)(ref netId)).Value; if (pingBudget.CanStart(value, Time.time, dances.ContainsKey(value)) && Emotes().Count != 0) { if (dances.Count >= 3) { End(dances.OrderBy((KeyValuePair<uint, Dance> kv) => kv.Value.Message.Sequence).First().Key, broadcast: true); } StartEvent((IEnumerable<CharacterBody>)(object)new CharacterBody[1] { val }, "ping-spotlight", null, ping: true); } } } } } }); } private void ObserveSafely(Action action) { try { action(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("MDB_SCENARIO_ERROR " + ex)); } } } internal static class SolusWingGuard { private delegate void Play2(BoneMapper mapper, string animation, int position); private delegate void Play3(BoneMapper mapper, string animation, int position, int audio); private delegate void Request(string animation, BoneMapper mapper, int position); private delegate void Join(BoneMapper mapper); private delegate void Prepare(BoneMapper mapper, JoinSpot[] spots); private static readonly List<Hook> hooks = new List<Hook>(); private static ManualLogSource log; private static bool installed; private static bool reported; internal static void Install(ManualLogSource logger) { if (!installed) { log = logger; Add(typeof(BoneMapper), "PlayAnim", new Type[2] { typeof(string), typeof(int) }, new Action<Play2, BoneMapper, string, int>(BlockPlay2)); Add(typeof(BoneMapper), "PlayAnim", new Type[3] { typeof(string), typeof(int), typeof(int) }, new Action<Play3, BoneMapper, string, int, int>(BlockPlay3)); Add(typeof(CustomEmotesAPI), "PlayAnimation", new Type[3] { typeof(string), typeof(BoneMapper), typeof(int) }, new Action<Request, string, BoneMapper, int>(BlockRequest)); Add(typeof(BoneMapper), "JoinEmoteSpot", Type.EmptyTypes, new Action<Join, BoneMapper>(BlockJoin)); Add(typeof(BoneMapper), "NewAnimation", new Type[1] { typeof(JoinSpot[]) }, new Action<Prepare, BoneMapper, JoinSpot[]>(BlockPrepare)); installed = true; log.LogInfo((object)"MDB_SOLUS_WING_GUARD_READY 0.2.1: five API entry points protected on this peer; always active."); } } private static void Add(Type owner, string name, Type[] arguments, Delegate guard) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown MethodInfo method = owner.GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, arguments, null); if (method == null) { throw new MissingMethodException(owner.FullName, name); } Hook val = new Hook((MethodBase)method, guard); hooks.Add(val); if (!val.IsApplied) { throw new InvalidOperationException("Solus Wing safety hook did not apply: " + method); } } internal static bool IsWingIdentity(string identity) { string text = (identity ?? "").Trim(); if (!text.StartsWith("SolusWing", StringComparison.OrdinalIgnoreCase)) { return text.StartsWith("mdlSolusWing", StringComparison.OrdinalIgnoreCase); } return true; } private static bool NamedWing(Object target) { if (Object.op_Implicit(target)) { return IsWingIdentity(target.name); } return false; } private static bool Blocked(BoneMapper mapper) { if (!Object.op_Implicit((Object)(object)mapper)) { return true; } bool flag = NamedWing((Object)(object)mapper.mapperBody) || NamedWing((Object)(object)mapper.bodyPrefab) || NamedWing((Object)(object)mapper.model); if (!flag) { Transform val = ((Component)mapper).transform; while (Object.op_Implicit((Object)(object)val)) { if (NamedWing((Object)(object)val)) { flag = true; break; } val = val.parent; } } if (flag && !reported) { reported = true; ManualLogSource obj = log; if (obj != null) { obj.LogWarning((object)"MDB_SOLUS_WING_BLOCKED: rejected an upstream emote request before animation, audio, props or callbacks."); } } return flag; } private static void BlockPlay2(Play2 orig, BoneMapper mapper, string animation, int position) { if (!Blocked(mapper)) { orig(mapper, animation, position); } } private static void BlockPlay3(Play3 orig, BoneMapper mapper, string animation, int position, int audio) { if (!Blocked(mapper)) { orig(mapper, animation, position, audio); } } private static void BlockRequest(Request orig, string animation, BoneMapper mapper, int position) { if (!Blocked(mapper)) { orig(animation, mapper, position); } } private static void BlockJoin(Join orig, BoneMapper mapper) { if (!Blocked(mapper)) { orig(mapper); } } private static void BlockPrepare(Prepare orig, BoneMapper mapper, JoinSpot[] spots) { if (!Blocked(mapper)) { orig(mapper, spots); } } }