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 Bitencola Hot Fixes v1.2.1
BitencolaHotFixes.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using BitencolaHotFixes.Core; using BitencolaHotFixes.Features; using BitencolaHotFixes.Features.Hud; using BitencolaHotFixes.Features.Remains; using GameNetcodeStuff; using HarmonyLib; using LethalCompanyInputUtils.Api; using LethalLib.Modules; using Microsoft.CodeAnalysis; using TMPro; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("BitencolaHotFixes")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+3c2756e2c376a7a2ba573d090825e802e951661a")] [assembly: AssemblyProduct("BitencolaHotFixes")] [assembly: AssemblyTitle("BitencolaHotFixes")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BitencolaHotFixes { [BepInPlugin("Bitencola.HotFixes", "Bitencola Hot Fixes", "1.2.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "Bitencola.HotFixes"; public const string PluginName = "Bitencola Hot Fixes"; public const string PluginVersion = "1.2.1"; public static Plugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } public static PluginConfig Cfg { get; private set; } private void Awake() { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Cfg = new PluginConfig(((BaseUnityPlugin)this).Config); try { RemainsItems.CreateAndRegister(); } catch (Exception arg) { Log.LogError((object)$"Falha ao registrar os restos de monstro: {arg}"); } try { KillCreditPatches.Register(); } catch (Exception arg2) { Log.LogError((object)$"Falha ao ligar quem matou às caixas de restos: {arg2}"); } try { SetUpInputs(); } catch (Exception arg3) { Log.LogError((object)$"Falha ao criar as teclas do mod (1-4, C e X ficam sem efeito): {arg3}"); } PatchEachClass(new Harmony("Bitencola.HotFixes")); Log.LogInfo((object)"Bitencola Hot Fixes 1.2.1 carregado"); } private static void PatchEachClass(Harmony harmony) { Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(typeof(Plugin).Assembly); foreach (Type type in typesFromAssembly) { try { harmony.CreateClassProcessor(type).Patch(); } catch (Exception arg) { Log.LogError((object)$"Falha ao aplicar os patches de {type.Name}: {arg}"); } } } [MethodImpl(MethodImplOptions.NoInlining)] private static void SetUpInputs() { HotFixesInputs.Create(); SlotHotkeys.Bind(HotFixesInputs.Instance); DropAll.Bind(HotFixesInputs.Instance); DropScrap.Bind(HotFixesInputs.Instance); } } public class PluginConfig { private const string SecTeclas = "1. Teclas"; private const string SecCura = "2. Cura"; private const string SecNave = "3. Nave"; private const string SecMonstros = "4. Monstros"; private const string SecGrupo = "5. Grupo"; private readonly Dictionary<MonsterTier, (ConfigEntry<int> min, ConfigEntry<int> max)> _tierRanges = new Dictionary<MonsterTier, (ConfigEntry<int>, ConfigEntry<int>)>(); public ConfigEntry<bool> SlotHotkeysEnabled { get; } public ConfigEntry<float> SlotSwitchCooldownSeconds { get; } public ConfigEntry<bool> ShowSlotNumbers { get; } public ConfigEntry<bool> DropAllEnabled { get; } public ConfigEntry<bool> SellAllAtCounterEnabled { get; } public ConfigEntry<bool> DropScrapEnabled { get; } public ConfigEntry<bool> MoveEmotesEnabled { get; } public ConfigEntry<bool> ShowSlotValues { get; } public ConfigEntry<bool> WeightInKilograms { get; } public ConfigEntry<bool> RegenEnabled { get; } public ConfigEntry<float> RegenDelaySeconds { get; } public ConfigEntry<float> RegenSecondsPerPoint { get; } public ConfigEntry<bool> ShowHealthNumbers { get; } public ConfigEntry<bool> ShipHealEnabled { get; } public ConfigEntry<bool> ShipRechargeEnabled { get; } public ConfigEntry<float> ShipRechargeSecondsToFull { get; } public ConfigEntry<bool> RemainsEnabled { get; } public ConfigEntry<bool> ShowKillerEnabled { get; } public ConfigEntry<bool> DeathNoticesEnabled { get; } public ConfigEntry<bool> DailyScoreboardEnabled { get; } public PluginConfig(ConfigFile config) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0204: Unknown result type (might be due to invalid IL or missing references) //IL_020e: Expected O, but got Unknown //IL_02f8: Unknown result type (might be due to invalid IL or missing references) //IL_0302: Expected O, but got Unknown //IL_0333: Unknown result type (might be due to invalid IL or missing references) //IL_033d: Expected O, but got Unknown SlotHotkeysEnabled = config.Bind<bool>("1. Teclas", "Trocar slot com numeros", true, "As teclas 1, 2, 3 e 4 selecionam os slots do inventário. Dá para remapear no menu de Controles do jogo."); SlotSwitchCooldownSeconds = config.Bind<float>("1. Teclas", "Intervalo entre trocas (s)", 0.1f, new ConfigDescription("Tempo mínimo entre duas trocas pelas teclas 1 a 4. Um aperto dentro desse tempo não se perde: a troca acontece assim que o tempo passa. A rolagem do mouse continua com o tempo do jogo (0,3 s).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 1f), Array.Empty<object>())); ShowSlotNumbers = config.Bind<bool>("1. Teclas", "Mostrar numeros nos slots", true, "Mostra [1], [2], [3] e [4] em cima dos slots, como o [TAB] do slot extra. Se a tecla for trocada no menu de Controles, o texto acompanha."); DropAllEnabled = config.Bind<bool>("1. Teclas", "Largar tudo", true, "Uma tecla (C por padrão) larga todos os itens de uma vez. Dá para remapear no menu de Controles do jogo."); SellAllAtCounterEnabled = config.Bind<bool>("1. Teclas", "Vender tudo no balcao", true, "No balcão da Companhia, a tecla de largar tudo (C) põe toda a sucata no balcão, um item por vez, em vez de largar tudo no chão. As ferramentas ficam com você."); DropScrapEnabled = config.Bind<bool>("1. Teclas", "Largar so sucata", true, "Uma tecla (X por padrão) larga no chão só a sucata dos slots normais, um item por vez. Ferramentas e o item do slot extra ficam com você. Dá para remapear no menu de Controles do jogo."); MoveEmotesEnabled = config.Bind<bool>("1. Teclas", "Mover emotes para 5 e 6", true, "Move os emotes do jogo (dançar e apontar) das teclas 1 e 2 para 5 e 6, liberando 1 a 4 para os slots."); ShowSlotValues = config.Bind<bool>("1. Teclas", "Mostrar valor nos slots", true, "Mostra o valor de venda de cada sucata embaixo do slot em que ela está."); WeightInKilograms = config.Bind<bool>("1. Teclas", "Peso em kg", true, "Mostra o peso que você carrega em quilos (kg) em vez de libras (lb)."); RegenEnabled = config.Bind<bool>("2. Cura", "Cura gradual", true, "Recupera vida aos poucos depois de levar dano, até 100. A regeneração nativa do jogo (abaixo de 20) continua igual."); RegenDelaySeconds = config.Bind<float>("2. Cura", "Espera apos dano (s)", 5f, "Segundos sem levar dano antes de a cura gradual começar. Novo dano reinicia a contagem."); RegenSecondsPerPoint = config.Bind<float>("2. Cura", "Segundos por ponto de vida", 1.5f, "Quantos segundos leva para recuperar 1 ponto de vida durante a cura gradual."); ShowHealthNumbers = config.Bind<bool>("2. Cura", "Mostrar vida em numeros", true, "Mostra sua vida em números (por exemplo 75/100) perto do ícone do jogador."); ShipHealEnabled = config.Bind<bool>("3. Nave", "Curar ao entrar na nave", true, "Ao entrar na nave, a vida volta a 100 na hora e o estado de gravemente ferido é removido."); ShipRechargeEnabled = config.Bind<bool>("3. Nave", "Recarregar baterias na nave", true, "Itens com bateria que você carrega recarregam sozinhos dentro da nave. Quem hospeda a partida também recarrega os itens soltos na nave."); ShipRechargeSecondsToFull = config.Bind<float>("3. Nave", "Segundos para recarga completa", 20f, new ConfigDescription("Quantos segundos uma bateria vazia leva para chegar a 100% dentro da nave.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 300f), Array.Empty<object>())); RemainsEnabled = config.Bind<bool>("4. Monstros", "Monstros vendiveis", true, "Monstros mortos viram um item carregável que pode ser vendido na Companhia como sucata."); ShowKillerEnabled = config.Bind<bool>("4. Monstros", "Mostrar quem matou", true, "O scanner da caixa de restos mostra quem matou o monstro: o último jogador que acertou um golpe nele. Monstros que nenhum jogador acertou (mortos por mina, torreta ou queda) não mostram ninguém."); DeathNoticesEnabled = config.Bind<bool>("5. Grupo", "Avisar morte de amigos", true, "Mostra um aviso na tela com o nome e a causa da morte quando outro jogador morre."); DailyScoreboardEnabled = config.Bind<bool>("5. Grupo", "Placar do dia", true, "No fim do dia, quando a nave decola, mostra quantos monstros cada jogador matou e o valor dos restos."); AcceptableValueRange<int> val = new AcceptableValueRange<int>(1, 10000); foreach (MonsterTier value in Enum.GetValues(typeof(MonsterTier))) { TierSpec tierSpec = MonsterCatalog.Tiers[value]; string text = MonsterCatalog.ConfigPrefix(value); ConfigEntry<int> item = config.Bind<int>("4. Monstros", text + " - valor minimo", tierSpec.MinValue, new ConfigDescription("Valor mínimo de venda dos restos de monstros da categoria \"" + text + "\".", (AcceptableValueBase)(object)val, Array.Empty<object>())); ConfigEntry<int> item2 = config.Bind<int>("4. Monstros", text + " - valor maximo", tierSpec.MaxValue, new ConfigDescription("Valor máximo de venda dos restos de monstros da categoria \"" + text + "\".", (AcceptableValueBase)(object)val, Array.Empty<object>())); _tierRanges[value] = (item, item2); } } public (int min, int max) GetTierRange(MonsterTier tier) { var (val, val2) = _tierRanges[tier]; return (min: val.Value, max: val2.Value); } } } namespace BitencolaHotFixes.Features { [HarmonyPatch(typeof(HUDManager), "Update")] internal static class DeathNotices { private static readonly DeathWatcher Watcher = new DeathWatcher(); private static readonly List<PlayerLife> Lives = new List<PlayerLife>(); private static StartOfRound? _lastRound; private const string TipSource = "morte"; private static readonly List<(string name, string cause)> Recent = new List<(string, string)>(); [HarmonyPostfix] private static void UpdatePostfix() { //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Expected I4, but got Unknown try { if (!Plugin.Cfg.DeathNoticesEnabled.Value) { return; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null) { return; } if (instance != _lastRound) { _lastRound = instance; Recent.Clear(); Watcher.Reset(); } Lives.Clear(); PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null)) { Lives.Add(new PlayerLife((int)val.playerClientId, val.isPlayerControlled || val.isPlayerDead, val.isPlayerDead)); } } IReadOnlyList<int> readOnlyList = Watcher.Update(Lives); if (readOnlyList.Count == 0) { return; } PlayerControllerB val2 = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if (!TipQueue.IsQueuedOrOnScreen("morte")) { Recent.Clear(); } bool flag = false; for (int j = 0; j < readOnlyList.Count; j++) { PlayerControllerB val3 = null; for (int k = 0; k < allPlayerScripts.Length; k++) { if ((Object)(object)allPlayerScripts[k] != (Object)null && (int)allPlayerScripts[k].playerClientId == readOnlyList[j]) { val3 = allPlayerScripts[k]; break; } } if (!((Object)(object)val3 == (Object)null) && !((Object)(object)val3 == (Object)(object)val2)) { Recent.Add((val3.playerUsername, DeathCauseText.For((int)val3.causeOfDeath))); flag = true; } } if (flag) { var (header, body) = DeathNoticeText.Build(Recent); TipQueue.Show("morte", header, body); } } catch (Exception ex) { Plugin.Log.LogError((object)("DeathNotices: " + ex)); } } } internal static class DropAll { public static void Bind(HotFixesInputs inputs) { inputs.DropAll.performed += OnDropPressed; } private static void OnDropPressed(CallbackContext context) { try { if (((CallbackContext)(ref context)).performed && Plugin.Cfg.DropAllEnabled.Value && !InventoryUnloader.IsRunning && LocalPlayerInput.TryGet(out PlayerControllerB player, out PlayerInputState state) && InputGate.CanDropAll(in state)) { if ((Object)(object)ShipBuildModeManager.Instance != (Object)null) { ShipBuildModeManager.Instance.CancelBuildMode(true); } if (Plugin.Cfg.SellAllAtCounterEnabled.Value && InventoryUnloader.HasScrapToUnload(player) && InventoryUnloader.TryGetCounterInReach(player, out DepositItemsDesk _)) { InventoryUnloader.Start(player, UnloadMode.PlaceOnCounter); } else { player.DropAllHeldItemsAndSyncNonexact(); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao largar tudo: {arg}"); } } } internal static class DropScrap { public static void Bind(HotFixesInputs inputs) { inputs.DropScrap.performed += OnDropScrapPressed; } private static void OnDropScrapPressed(CallbackContext context) { try { if (((CallbackContext)(ref context)).performed && Plugin.Cfg.DropScrapEnabled.Value && !InventoryUnloader.IsRunning && LocalPlayerInput.TryGet(out PlayerControllerB player, out PlayerInputState state) && InputGate.CanUnloadScrap(in state, InventoryUnloader.InBuildMode())) { InventoryUnloader.Start(player, UnloadMode.DropOnFloor); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao largar só a sucata: {arg}"); } } } [HarmonyPatch] internal static class EmoteRebindPatches { private static readonly string[] EmoteActions = new string[2] { "Emote1", "Emote2" }; [HarmonyPostfix] [HarmonyPatch(typeof(IngamePlayerSettings), "LoadSettingsFromPrefs")] private static void AfterLoadSettings() { ApplyEmoteRemap(); } [HarmonyPostfix] [HarmonyPatch(typeof(IngamePlayerSettings), "DiscardChangedSettings")] private static void AfterDiscardChanges(IngamePlayerSettings __instance) { RefreshKeybindMenuIfChanged(__instance); } [HarmonyPostfix] [HarmonyPatch(typeof(IngamePlayerSettings), "ResetAllKeybinds")] private static void AfterResetKeybinds(IngamePlayerSettings __instance) { RefreshKeybindMenuIfChanged(__instance); } private static void RefreshKeybindMenuIfChanged(IngamePlayerSettings settings) { if (!ApplyEmoteRemap()) { return; } try { settings.UpdateAllKeybindOptions(); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao atualizar o menu de Controles: {arg}"); } } private static bool ApplyEmoteRemap() { //IL_0094: 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_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) try { if (!Plugin.Cfg.MoveEmotesEnabled.Value) { return false; } InputActionAsset actions = InputSystem.actions; if ((Object)(object)actions == (Object)null) { return false; } bool result = false; string[] emoteActions = EmoteActions; foreach (string text in emoteActions) { InputAction val = actions.FindAction(text, false); if (val == null) { continue; } for (int j = 0; j < val.bindings.Count; j++) { InputBinding val2 = val.bindings[j]; string text2 = EmoteRemap.NewPath(text, ((InputBinding)(ref val2)).effectivePath); if (text2 != null) { InputActionRebindingExtensions.ApplyBindingOverride(val, j, text2); result = true; } } } return result; } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mover os emotes para 5 e 6: {arg}"); return false; } } } [HarmonyPatch(typeof(PlayerControllerB), "Update")] internal static class HealthPatches { private const int MaxHealth = 100; private const int VanillaFloor = 20; private const float MinDelaySeconds = 0f; private const float MinSecondsPerPoint = 0.1f; private static readonly ShipEntryTracker Ship = new ShipEntryTracker(); private static RegenTracker? _regen; private static float _regenDelay; private static float _regenSecondsPerPoint; [HarmonyPostfix] private static void UpdatePostfix(PlayerControllerB __instance) { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)__instance != (Object)(object)instance.localPlayerController) { return; } if (__instance.isPlayerDead) { _regen?.Reset(); Ship.Update(__instance.isInHangarShipRoom, isAlive: false); } else if (__instance.isPlayerControlled) { PluginConfig cfg = Plugin.Cfg; if (Ship.Update(__instance.isInHangarShipRoom, isAlive: true) && cfg.ShipHealEnabled.Value) { HealToFullInShip(__instance); } if (cfg.RegenEnabled.Value) { RegenerateGradually(__instance, cfg); } else { _regen?.Reset(); } } } private static void HealToFullInShip(PlayerControllerB player) { if (player.health < 100 || player.criticallyInjured) { player.health = 100; if (player.criticallyInjured) { player.MakeCriticallyInjured(false); } HUDManager.Instance.UpdateHealthUI(100, false); Plugin.Log.LogInfo((object)"Cura na nave"); } } private static void RegenerateGradually(PlayerControllerB player, PluginConfig cfg) { int num = EnsureRegenMatchesConfig(cfg).Tick(player.health, Time.deltaTime); if (num > 0) { player.health += num; HUDManager.Instance.UpdateHealthUI(player.health, false); if (player.health >= 20 && player.criticallyInjured) { player.MakeCriticallyInjured(false); } } } private static RegenTracker EnsureRegenMatchesConfig(PluginConfig cfg) { float num = Mathf.Max(cfg.RegenDelaySeconds.Value, 0f); float num2 = Mathf.Max(cfg.RegenSecondsPerPoint.Value, 0.1f); if (_regen == null || num != _regenDelay || num2 != _regenSecondsPerPoint) { _regenDelay = num; _regenSecondsPerPoint = num2; _regen = new RegenTracker(num, num2); } return _regen; } } public class HotFixesInputs : LcInputActions { public static HotFixesInputs Instance { get; private set; } [InputAction("<Keyboard>/1", Name = "Slot 1")] public InputAction Slot1 { get; set; } [InputAction("<Keyboard>/2", Name = "Slot 2")] public InputAction Slot2 { get; set; } [InputAction("<Keyboard>/3", Name = "Slot 3")] public InputAction Slot3 { get; set; } [InputAction("<Keyboard>/4", Name = "Slot 4")] public InputAction Slot4 { get; set; } [InputAction("<Keyboard>/c", Name = "Largar tudo")] public InputAction DropAll { get; set; } [InputAction("<Keyboard>/x", Name = "Largar so sucata")] public InputAction DropScrap { get; set; } public static void Create() { Instance = new HotFixesInputs(); } } internal enum UnloadMode { DropOnFloor, PlaceOnCounter } internal static class InventoryUnloader { private const int UtilitySlot = 50; private const int CounterObjectLimit = 150; private const float SettleAfterSwitchSeconds = 0.15f; private const float PauseBetweenItemsSeconds = 0.15f; private const float StepTimeoutSeconds = 1f; private const float StaleAfterSeconds = 3f; private static int _lastSequenceId; private static int _runningId; private static PlayerControllerB? _runningOn; private static float _heartbeat; private static DepositItemsDesk? _counter; private static bool _counterSearched; private static int _counterScenesKey; public static bool IsRunning { get { if (_runningId != 0 && (Object)(object)_runningOn != (Object)null) { return Time.time - _heartbeat < 3f; } return false; } } public static void Start(PlayerControllerB player, UnloadMode mode) { if (IsRunning || (Object)(object)player == (Object)null || !((Behaviour)player).isActiveAndEnabled) { return; } int currentItemSlot = player.currentItemSlot; IReadOnlyList<int> readOnlyList = ScrapSelection.UnloadOrder(ClassifySlots(player), currentItemSlot); if (readOnlyList.Count == 0) { return; } DepositItemsDesk desk = null; if (mode == UnloadMode.PlaceOnCounter && !TryGetCounterInReach(player, out desk)) { return; } int id = (_runningId = ++_lastSequenceId); _runningOn = player; _heartbeat = Time.time; try { ((MonoBehaviour)player).StartCoroutine(Guarded(id, Run(player, mode, desk, readOnlyList, currentItemSlot))); } catch { Release(id); throw; } } public static bool HasScrapToUnload(PlayerControllerB player) { return ScrapSelection.SlotsToUnload(ClassifySlots(player)).Count > 0; } public static bool TryGetCounterInReach(PlayerControllerB player, [NotNullWhen(true)] out DepositItemsDesk? desk) { desk = FindCounter(); if ((Object)(object)desk != (Object)null) { return InReach(desk, player); } return false; } public static bool InBuildMode() { ShipBuildModeManager instance = ShipBuildModeManager.Instance; if ((Object)(object)instance != (Object)null) { return instance.InBuildMode; } return false; } private static IEnumerator Guarded(int id, IEnumerator steps) { try { while (true) { object current; try { if (_runningId != id) { break; } _heartbeat = Time.time; if (!steps.MoveNext()) { break; } current = steps.Current; } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao tirar a sucata dos slots; a sequência parou: {arg}"); break; } yield return current; } } finally { Release(id); } } private static IEnumerator Run(PlayerControllerB player, UnloadMode mode, DepositItemsDesk? desk, IReadOnlyList<int> order, int savedSlot) { bool canRestoreSlot = true; foreach (int slot in order) { if (!CanStartStep(player, mode, desk, out string reason)) { Stopped(reason); break; } if (player.currentItemSlot != slot) { if (player.twoHanded) { Stopped("item de duas mãos na mão"); break; } SlotSwitcher.SwitchNow(player, slot); } float deadline = Time.time + 1f; while (!SettledAfterSwitch(player) && Time.time < deadline && IsLocalPlayer(player)) { yield return null; } if (mode == UnloadMode.DropOnFloor) { yield return (object)new WaitForEndOfFrame(); } if (!CanActNow(player, mode, desk, out reason)) { Stopped(reason); break; } if (HoldsScrapFrom(player, slot)) { if (mode == UnloadMode.PlaceOnCounter) { desk.PlaceItemOnCounter(player); } else { player.DiscardHeldObject(false, (NetworkObject)null, default(Vector3), true); } deadline = Time.time + 1f; while (player.throwingObject && Time.time < deadline && IsLocalPlayer(player)) { yield return null; } if (player.throwingObject) { canRestoreSlot = false; Plugin.Log.LogWarning((object)"O jogo não confirmou a tempo que o item foi largado; a sequência parou."); break; } } yield return (object)new WaitForSeconds(0.15f); } if (canRestoreSlot) { RestoreSlot(player, savedSlot); } } private static bool CanStartStep(PlayerControllerB player, UnloadMode mode, DepositItemsDesk? desk, out string reason) { if (!LocalPlayerInput.TryGet(out PlayerControllerB player2, out PlayerInputState state) || (Object)(object)player2 != (Object)(object)player) { reason = "o jogador não é mais o jogador local"; return false; } if (!InputGate.CanUnloadScrap(in state, InBuildMode())) { reason = "o jogador está ocupado ou não pode agir"; return false; } return CounterStillAccepts(player, mode, desk, out reason); } private static bool CanActNow(PlayerControllerB player, UnloadMode mode, DepositItemsDesk? desk, out string reason) { if (!LocalPlayerInput.TryGet(out PlayerControllerB player2, out PlayerInputState state) || (Object)(object)player2 != (Object)(object)player) { reason = "o jogador não é mais o jogador local"; return false; } if (!state.CanAct || state.Busy || InBuildMode() || !SettledAfterSwitch(player)) { reason = "o jogador está ocupado ou não pode agir"; return false; } return CounterStillAccepts(player, mode, desk, out reason); } private static bool CounterStillAccepts(PlayerControllerB player, UnloadMode mode, DepositItemsDesk? desk, out string reason) { reason = ""; if (mode != UnloadMode.PlaceOnCounter) { return true; } if ((Object)(object)desk == (Object)null || !InReach(desk, player)) { reason = "o balcão saiu do alcance"; return false; } if (desk.inGrabbingObjectsAnimation || ObjectsOnCounter(desk) >= 150) { reason = "o balcão não está aceitando itens agora"; return false; } return true; } private static void RestoreSlot(PlayerControllerB player, int savedSlot) { if (player.currentItemSlot != savedSlot && (savedSlot == 50 || (savedSlot >= 0 && savedSlot < player.ItemSlots.Length)) && LocalPlayerInput.TryGet(out PlayerControllerB player2, out PlayerInputState state) && !((Object)(object)player2 != (Object)(object)player) && state.CanAct && !state.Busy && !state.TwoHanded && !InBuildMode()) { SlotSwitcher.SwitchNow(player, savedSlot); } } private static bool SettledAfterSwitch(PlayerControllerB player) { if (!player.isGrabbingObjectAnimation) { return player.timeSinceSwitchingSlots >= 0.15f; } return false; } private static bool IsLocalPlayer(PlayerControllerB player) { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)player != (Object)null && (Object)(object)instance != (Object)null && (Object)(object)instance.localPlayerController == (Object)(object)player) { return !player.isPlayerDead; } return false; } private static bool HoldsScrapFrom(PlayerControllerB player, int slot) { if (slot < 0 || slot >= player.ItemSlots.Length || player.currentItemSlot != slot || !player.isHoldingObject) { return false; } GrabbableObject currentlyHeldObjectServer = player.currentlyHeldObjectServer; if ((Object)(object)currentlyHeldObjectServer != (Object)null && (Object)(object)player.ItemSlots[slot] == (Object)(object)currentlyHeldObjectServer) { return Classify(currentlyHeldObjectServer) == SlotItemKind.Scrap; } return false; } private static SlotItemKind[] ClassifySlots(PlayerControllerB player) { GrabbableObject[] itemSlots = player.ItemSlots; SlotItemKind[] array = new SlotItemKind[itemSlots.Length]; for (int i = 0; i < itemSlots.Length; i++) { array[i] = Classify(itemSlots[i]); } return array; } private static SlotItemKind Classify(GrabbableObject? item) { if ((Object)(object)item == (Object)null) { return SlotItemKind.Empty; } if (!((Object)(object)item.itemProperties != (Object)null) || !item.itemProperties.isScrap) { return SlotItemKind.Tool; } return SlotItemKind.Scrap; } private static DepositItemsDesk? FindCounter() { int num = LoadedScenesKey(); bool flag = _counter != null && (Object)(object)_counter == (Object)null; if (!_counterSearched || num != _counterScenesKey || flag) { _counter = Object.FindObjectOfType<DepositItemsDesk>(); _counterScenesKey = num; _counterSearched = true; } if (!((Object)(object)_counter != (Object)null)) { return null; } return _counter; } private static int LoadedScenesKey() { //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) int num = 17; for (int i = 0; i < SceneManager.sceneCount; i++) { int num2 = num * 31; Scene sceneAt = SceneManager.GetSceneAt(i); num = num2 + ((Scene)(ref sceneAt)).handle; } return num; } private static bool InReach(DepositItemsDesk desk, PlayerControllerB player) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)desk.triggerCollider != (Object)null && (Object)(object)player.localItemHolder != (Object)null) { Bounds bounds = ((Collider)desk.triggerCollider).bounds; return ((Bounds)(ref bounds)).Contains(player.localItemHolder.position); } return false; } private static int ObjectsOnCounter(DepositItemsDesk desk) { if (!((Object)(object)desk.deskObjectsContainer != (Object)null)) { return int.MaxValue; } return ((Component)desk.deskObjectsContainer).GetComponentsInChildren<GrabbableObject>().Length; } private static void Stopped(string reason) { Plugin.Log.LogInfo((object)("Tirar a sucata dos slots parou no meio: " + reason + ".")); } private static void Release(int id) { if (_runningId == id) { _runningId = 0; _runningOn = null; } } } internal static class LocalPlayerInput { private const int JetpackItemId = 13; public static bool TryGet(out PlayerControllerB player, out PlayerInputState state) { GameNetworkManager instance = GameNetworkManager.Instance; player = (((Object)(object)instance != (Object)null) ? instance.localPlayerController : null); if ((Object)(object)player == (Object)null) { state = default(PlayerInputState); return false; } state = new PlayerInputState(CanAct(player), IsBusy(player), player.twoHanded, player.timeSinceSwitchingSlots, player.currentItemSlot, player.ItemSlots.Length, HasAnyItem(player)); return true; } private static bool CanAct(PlayerControllerB p) { if (((NetworkBehaviour)p).IsOwner && p.isPlayerControlled && !p.isPlayerDead) { if (((NetworkBehaviour)p).IsServer) { return p.isHostPlayerObject; } return true; } return false; } private static bool IsBusy(PlayerControllerB p) { if (!p.inTerminalMenu && !p.isTypingChat && (!((Object)(object)p.quickMenuManager != (Object)null) || !p.quickMenuManager.isMenuOpen) && !p.inSpecialMenu && !p.isGrabbingObjectAnimation && !p.inSpecialInteractAnimation && !p.throwingObject && !p.activatingItem) { return IsUsingJetpack(p); } return true; } private static bool IsUsingJetpack(PlayerControllerB p) { GrabbableObject currentlyHeldObjectServer = p.currentlyHeldObjectServer; if ((p.jetpackControls || p.disablingJetpackControls) && (Object)(object)currentlyHeldObjectServer != (Object)null) { return currentlyHeldObjectServer.itemProperties.itemId == 13; } return false; } private static bool HasAnyItem(PlayerControllerB p) { if ((Object)(object)p.ItemOnlySlot != (Object)null) { return true; } GrabbableObject[] itemSlots = p.ItemSlots; for (int i = 0; i < itemSlots.Length; i++) { if ((Object)(object)itemSlots[i] != (Object)null) { return true; } } return false; } } [HarmonyPatch(typeof(PlayerControllerB), "Update")] internal static class ShipRecharge { private sealed class Entry { public float LastSyncAt; public float LastSeenAt; } private const float FloorScanInterval = 1f; private const float PruneInterval = 10f; private const float StaleAfterSeconds = 5f; private static readonly Dictionary<int, Entry> Entries = new Dictionary<int, Entry>(); private static readonly List<int> ToRemove = new List<int>(); private static float _lastFloorScan; private static float _lastPrune; [HarmonyPostfix] private static void UpdatePostfix(PlayerControllerB __instance) { try { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)__instance != (Object)(object)instance.localPlayerController) { return; } PluginConfig cfg = Plugin.Cfg; if (!cfg.ShipRechargeEnabled.Value) { return; } float value = cfg.ShipRechargeSecondsToFull.Value; float time = Time.time; if (!__instance.isPlayerDead && __instance.isPlayerControlled && __instance.isInHangarShipRoom) { float deltaTime = Time.deltaTime; GrabbableObject[] itemSlots = __instance.ItemSlots; for (int i = 0; i < itemSlots.Length; i++) { RechargeCarried(itemSlots[i], deltaTime, value, time); } RechargeCarried(__instance.ItemOnlySlot, deltaTime, value, time); } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null && singleton.IsServer && time - _lastFloorScan >= 1f) { float val = time - _lastFloorScan; _lastFloorScan = time; RechargeFloorItems(Math.Min(val, 5f), value, time); } if (time - _lastPrune >= 10f) { _lastPrune = time; Prune(time); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha na recarga de baterias: {arg}"); } } private static void RechargeCarried(GrabbableObject? item, float dt, float secondsToFull, float now) { if (!((Object)(object)item == (Object)null) && CanRecharge(item)) { Step(item, dt, secondsToFull, now, ((NetworkBehaviour)item).IsOwner, isServerRpc: true); } } private static void RechargeFloorItems(float dt, float secondsToFull, float now) { GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); foreach (GrabbableObject val in array) { if (!((Object)(object)val == (Object)null) && !val.isHeld && val.isInShipRoom && CanRecharge(val)) { Step(val, dt, secondsToFull, now, canSend: true, isServerRpc: false); } } } private static bool CanRecharge(GrabbableObject item) { Item itemProperties = item.itemProperties; Battery insertedBattery = item.insertedBattery; if ((Object)(object)itemProperties != (Object)null && itemProperties.requiresBattery && insertedBattery != null && insertedBattery.charge < 1f) { return !item.isBeingUsed; } return false; } private static void Step(GrabbableObject item, float dt, float secondsToFull, float now, bool canSend, bool isServerRpc) { int instanceID = ((Object)item).GetInstanceID(); if (!Entries.TryGetValue(instanceID, out Entry value)) { value = new Entry { LastSyncAt = now - 1f }; Entries[instanceID] = value; } value.LastSeenAt = now; Battery insertedBattery = item.insertedBattery; float num = (insertedBattery.charge = BatteryRecharge.Next(insertedBattery.charge, dt, secondsToFull)); if (num > 0f) { insertedBattery.empty = false; } bool flag = num >= 1f; if (canSend && SyncThrottle.ShouldSync(now, value.LastSyncAt, flag)) { value.LastSyncAt = now; int num2 = Mathf.Clamp(Mathf.RoundToInt(num * 100f), 0, 100); if (isServerRpc) { item.SyncBatteryServerRpc(num2); } else { item.SyncBatteryClientRpc(num2); } } if (flag) { Entries.Remove(instanceID); } } private static void Prune(float now) { ToRemove.Clear(); foreach (KeyValuePair<int, Entry> entry in Entries) { if (now - entry.Value.LastSeenAt > 5f) { ToRemove.Add(entry.Key); } } foreach (int item in ToRemove) { Entries.Remove(item); } } } internal static class SlotHotkeys { public static void Bind(HotFixesInputs inputs) { inputs.Slot1.performed += delegate(CallbackContext context) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) OnSlotPressed(context, 0); }; inputs.Slot2.performed += delegate(CallbackContext context) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) OnSlotPressed(context, 1); }; inputs.Slot3.performed += delegate(CallbackContext context) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) OnSlotPressed(context, 2); }; inputs.Slot4.performed += delegate(CallbackContext context) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) OnSlotPressed(context, 3); }; } private static void OnSlotPressed(CallbackContext context, int slot) { if (((CallbackContext)(ref context)).performed && Plugin.Cfg.SlotHotkeysEnabled.Value) { SlotSwitcher.Request(slot); } } } [HarmonyPatch] internal static class SlotKeyLabels { private const string LabelName = "BitencolaSlotKey"; private static bool _listeningToConfig; private static bool _lastUsingController; [HarmonyPatch(typeof(HUDManager), "SetUtilitySlotKeyText")] [HarmonyPostfix] private static void AfterUtilityKeyText(HUDManager __instance) { try { ListenToConfigOnce(); Refresh(__instance); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar os números dos slots: {arg}"); } } [HarmonyPatch(typeof(HUDManager), "Update")] [HarmonyPostfix] private static void AfterHudUpdate(HUDManager __instance) { try { if (UsingController() != _lastUsingController) { Refresh(__instance); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao atualizar os números dos slots: {arg}"); } } private static void ListenToConfigOnce() { if (!_listeningToConfig) { _listeningToConfig = true; Plugin.Cfg.ShowSlotNumbers.SettingChanged += delegate { RefreshCurrentHud(); }; Plugin.Cfg.SlotHotkeysEnabled.SettingChanged += delegate { RefreshCurrentHud(); }; } } private static void RefreshCurrentHud() { try { if ((Object)(object)HUDManager.Instance != (Object)null) { Refresh(HUDManager.Instance); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao atualizar os números dos slots: {arg}"); } } private static bool UsingController() { if ((Object)(object)StartOfRound.Instance != (Object)null) { return StartOfRound.Instance.localPlayerUsingController; } return false; } private static void Refresh(HUDManager hud) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //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_00be: Unknown result type (might be due to invalid IL or missing references) bool usingController = (_lastUsingController = UsingController()); TextMeshProUGUI utilitySlotKeyText = hud.utilitySlotKeyText; Image itemOnlySlotIconFrame = hud.itemOnlySlotIconFrame; Image[] itemSlotIconFrames = hud.itemSlotIconFrames; if ((Object)(object)utilitySlotKeyText == (Object)null || (Object)(object)itemOnlySlotIconFrame == (Object)null || itemSlotIconFrames == null) { return; } Vector3 val = ((Transform)((TMP_Text)utilitySlotKeyText).rectTransform).position - ((Transform)((Graphic)itemOnlySlotIconFrame).rectTransform).position; for (int i = 0; i < itemSlotIconFrames.Length; i++) { Image val2 = itemSlotIconFrames[i]; if ((Object)(object)val2 == (Object)null) { continue; } if (i >= 4) { Transform val3 = ((Transform)((Graphic)val2).rectTransform).Find("BitencolaSlotKey"); if ((Object)(object)val3 != (Object)null) { ((Component)val3).gameObject.SetActive(false); } continue; } TextMeshProUGUI val4 = HudLabels.FindOrCloneTabLabel(hud, (Transform)(object)((Graphic)val2).rectTransform, "BitencolaSlotKey", ((Transform)((Graphic)val2).rectTransform).position + val); if (!((Object)(object)val4 == (Object)null)) { ((TMP_Text)val4).text = SlotKeyLabel.Format(BindingDisplay(i), i); ((Component)val4).gameObject.SetActive(SlotKeyLabel.IsVisible(i, Plugin.Cfg.ShowSlotNumbers.Value, Plugin.Cfg.SlotHotkeysEnabled.Value, usingController)); } } } private static string? BindingDisplay(int slot) { try { return SlotKeyBindings.Display(slot); } catch (Exception) { return null; } } } internal static class SlotKeyBindings { private const string UnboundMarker = "Not-Bound"; [MethodImpl(MethodImplOptions.NoInlining)] public static string? Display(int slot) { //IL_004f: 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) //IL_0061: 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_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) HotFixesInputs instance = HotFixesInputs.Instance; if (instance == null) { return null; } InputAction val = (InputAction)(slot switch { 0 => instance.Slot1, 1 => instance.Slot2, 2 => instance.Slot3, 3 => instance.Slot4, _ => null, }); if (val == null || val.bindings.Count == 0) { return null; } InputBinding val2 = val.bindings[0]; if (((InputBinding)(ref val2)).effectivePath.Contains("Not-Bound")) { return "-"; } return InputActionRebindingExtensions.GetBindingDisplayString(val, 0, (DisplayStringOptions)0); } } [HarmonyPatch] internal static class SlotSwitcher { private const float GrabSoundVolume = 0.6f; private const float MinCooldownSeconds = 0.05f; private static readonly PendingSlotRequest Pending = new PendingSlotRequest(); public static void Request(int slot) { if (!LocalPlayerInput.TryGet(out PlayerControllerB player, out PlayerInputState state)) { Pending.Clear(); return; } switch (InputGate.DecideSwitch(in state, slot, Cooldown())) { case SwitchDecision.Switch: Pending.Clear(); SwitchNow(player, slot); break; case SwitchDecision.Wait: Pending.Set(slot, Time.time); break; default: Pending.Clear(); break; } } [HarmonyPatch(typeof(PlayerControllerB), "Update")] [HarmonyPostfix] private static void ApplyPendingRequest(PlayerControllerB __instance) { try { float num = Cooldown(); if (Pending.TryGet(Time.time, PendingSlotRequest.MaxAgeFor(num), out var slot) && LocalPlayerInput.TryGet(out PlayerControllerB player, out PlayerInputState state) && !((Object)(object)__instance != (Object)(object)player)) { switch (InputGate.DecideSwitch(in state, slot, num)) { case SwitchDecision.Switch: Pending.Clear(); SwitchNow(player, slot); break; case SwitchDecision.Ignore: Pending.Clear(); break; } } } catch (Exception arg) { Pending.Clear(); Plugin.Log.LogError((object)$"Falha ao aplicar a troca de slot guardada: {arg}"); } } private static float Cooldown() { return Mathf.Max(Plugin.Cfg.SlotSwitchCooldownSeconds.Value, 0.05f); } internal static void SwitchNow(PlayerControllerB player, int slot) { if ((Object)(object)ShipBuildModeManager.Instance != (Object)null) { ShipBuildModeManager.Instance.CancelBuildMode(true); } player.playerBodyAnimator.SetBool("GrabValidated", false); player.SwitchToItemSlot(slot, (GrabbableObject)null); player.SwitchToSlotServerRpc(slot); GrabbableObject currentlyHeldObjectServer = player.currentlyHeldObjectServer; if ((Object)(object)currentlyHeldObjectServer != (Object)null) { AudioSource component = ((Component)currentlyHeldObjectServer).GetComponent<AudioSource>(); AudioClip grabSFX = currentlyHeldObjectServer.itemProperties.grabSFX; if ((Object)(object)component != (Object)null && (Object)(object)grabSFX != (Object)null) { component.PlayOneShot(grabSFX, 0.6f); } } player.timeSinceSwitchingSlots = 0f; } } } namespace BitencolaHotFixes.Features.Remains { [HarmonyPatch(typeof(GrabbableObject), "Start")] internal static class CorpseHider { private static readonly Dictionary<int, EnemyAI> Hidden = new Dictionary<int, EnemyAI>(); private static readonly List<int> Destroyed = new List<int>(); private static readonly List<EnemyAI> Enemies = new List<EnemyAI>(); private static readonly List<CorpseCandidate> Candidates = new List<CorpseCandidate>(); internal static event Action<GrabbableObject, EnemyAI>? CorpseMatched; [HarmonyPostfix] private static void StartPostfix(GrabbableObject __instance) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) EnemyAI val = null; try { if (RemainsItems.IsRemains(__instance.itemProperties)) { val = HideNearestCorpse(((Component)__instance).transform.position); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao esconder o corpo do monstro: {arg}"); } if ((Object)(object)val == (Object)null) { return; } try { CorpseHider.CorpseMatched?.Invoke(__instance, val); } catch (Exception arg2) { Plugin.Log.LogError((object)$"Falha ao avisar qual corpo a caixa de restos escondeu: {arg2}"); } } private static EnemyAI? HideNearestCorpse(Vector3 box) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0048: 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_0056: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) ForgetDestroyed(); Enemies.Clear(); Candidates.Clear(); EnemyAI[] array = Object.FindObjectsByType<EnemyAI>((FindObjectsSortMode)0); foreach (EnemyAI val in array) { Vector3 position = ((Component)val).transform.position; Enemies.Add(val); Candidates.Add(new CorpseCandidate(position.x, position.y, position.z, val.isEnemyDead, Hidden.ContainsKey(((Object)val).GetInstanceID()))); } int num = CorpsePicker.PickNearest(Candidates, box.x, box.y, box.z); EnemyAI val2 = ((num >= 0) ? Enemies[num] : null); Enemies.Clear(); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogDebug((object)"Restos sem corpo por perto para esconder."); return null; } Hidden[((Object)val2).GetInstanceID()] = val2; Hide(val2); Plugin.Log.LogInfo((object)("Corpo escondido: " + (((Object)(object)val2.enemyType != (Object)null) ? val2.enemyType.enemyName : ((Object)val2).name))); return val2; } private static void Hide(EnemyAI enemy) { enemy.overrideSettingEnemyMeshes = true; Renderer[] componentsInChildren = ((Component)enemy).GetComponentsInChildren<Renderer>(true); foreach (Renderer val in componentsInChildren) { if (BelongsToEnemy((Component)(object)val)) { val.enabled = false; } } Collider[] componentsInChildren2 = ((Component)enemy).GetComponentsInChildren<Collider>(true); foreach (Collider val2 in componentsInChildren2) { if (BelongsToEnemy((Component)(object)val2)) { val2.enabled = false; } } } private static bool BelongsToEnemy(Component component) { if ((Object)(object)component.GetComponentInParent<GrabbableObject>(true) == (Object)null && (Object)(object)component.GetComponentInParent<PlayerControllerB>(true) == (Object)null) { return (Object)(object)component.GetComponentInParent<DeadBodyInfo>(true) == (Object)null; } return false; } private static void ForgetDestroyed() { Destroyed.Clear(); foreach (KeyValuePair<int, EnemyAI> item in Hidden) { if ((Object)(object)item.Value == (Object)null) { Destroyed.Add(item.Key); } } foreach (int item2 in Destroyed) { Hidden.Remove(item2); } } } [HarmonyPatch] internal static class EnemyDeathPatch { [HarmonyPostfix] [HarmonyPatch(typeof(EnemyAI), "KillEnemy")] private static void KillEnemyPostfix(EnemyAI __instance, bool destroy) { RemainsSpawner.OnEnemyKilled(__instance, destroy); } [HarmonyPostfix] [HarmonyPatch(typeof(RoundManager), "UnloadSceneObjectsEarly")] private static void UnloadSceneObjectsEarlyPostfix() { RemainsSpawner.OnRoundEnded(); } } [HarmonyPatch] internal static class KillCreditPatches { internal static readonly KillCredits Credits = new KillCredits(); internal static readonly DailyScoreboard Scoreboard = new DailyScoreboard(); private static readonly Dictionary<int, string> KillerByBox = new Dictionary<int, string>(); private static readonly Dictionary<int, int> ValueByBox = new Dictionary<int, int>(); public static void Register() { CorpseHider.CorpseMatched += OnCorpseMatched; } [HarmonyPostfix] [HarmonyPatch(typeof(EnemyAI), "HitEnemy")] private static void HitEnemyPostfix(EnemyAI __instance, PlayerControllerB? playerWhoHit) { try { if (!((Object)(object)playerWhoHit == (Object)null)) { bool isEnemyDead = __instance.isEnemyDead; bool lateHitAllowed = isEnemyDead && KillCredits.IsLateHitAllowed(((NetworkBehaviour)__instance).IsOwner, ((NetworkBehaviour)playerWhoHit).OwnerClientId, ((NetworkBehaviour)__instance).OwnerClientId); Credits.RecordHit(((Object)__instance).GetInstanceID(), playerWhoHit.playerUsername, isEnemyDead, lateHitAllowed, Time.realtimeSinceStartup); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao registrar o golpe no monstro: {arg}"); } } [HarmonyPostfix] [HarmonyPatch(typeof(EnemyAI), "KillEnemy")] private static void KillEnemyPostfix(EnemyAI __instance) { try { if (__instance.isEnemyDead) { Credits.MarkDead(((Object)__instance).GetInstanceID(), Time.realtimeSinceStartup); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao registrar a morte do monstro: {arg}"); } } [HarmonyPostfix] [HarmonyPatch(typeof(GrabbableObject), "SetScrapValue")] private static void SetScrapValuePostfix(GrabbableObject __instance, int setValueTo) { try { if (RemainsItems.IsRemains(__instance.itemProperties)) { int instanceID = ((Object)__instance).GetInstanceID(); ValueByBox[instanceID] = setValueTo; if (KillerByBox.TryGetValue(instanceID, out string value)) { ApplyKiller(__instance, setValueTo, value); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar quem matou o monstro: {arg}"); } } public static void OnShipLeft() { Scoreboard.NewDay(); } public static void OnDayEnded() { Credits.Clear(); ValueByBox.Clear(); Scoreboard.NewDay(); } public static void OnSessionStarted() { Credits.Clear(); ValueByBox.Clear(); KillerByBox.Clear(); Scoreboard.Clear(); } private static void OnCorpseMatched(GrabbableObject box, EnemyAI corpse) { try { string text = Credits.KillerOf(((Object)corpse).GetInstanceID()); if (text != null) { int instanceID = ((Object)box).GetInstanceID(); KillerByBox[instanceID] = text; if (ValueByBox.TryGetValue(instanceID, out var value)) { ApplyKiller(box, value, text); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao ligar a caixa de restos a quem matou o monstro: {arg}"); } } private static void ApplyKiller(GrabbableObject box, int value, string killer) { if (Plugin.Cfg.ShowKillerEnabled.Value) { ScanNodeProperties componentInChildren = ((Component)box).GetComponentInChildren<ScanNodeProperties>(); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.subText = RemainsScanText.Format(value, killer); } } StartOfRound instance = StartOfRound.Instance; int instanceID = ((Object)box).GetInstanceID(); if ((Object)(object)instance != (Object)null && !instance.shipIsLeaving) { Scoreboard.TryAdd(instanceID, killer, value); } else { Scoreboard.Exclude(instanceID); } } } internal static class RemainsItems { private const int ItemIdBase = 41000; private const string PrefabNamePrefix = "BitencolaRemains_"; private const int FallbackPropsLayer = 6; private const int FallbackScanNodeLayer = 22; private static readonly Dictionary<MonsterEntry, Item> ItemsByEntry = new Dictionary<MonsterEntry, Item>(); private static readonly HashSet<Item> AllItems = new HashSet<Item>(); public static IEnumerable<Item> All => AllItems; public static void CreateAndRegister() { if (ItemsByEntry.Count <= 0) { int propsLayer = ResolveLayer("Props", 6); int scanNodeLayer = ResolveLayer("ScanNode", 22); (Mesh mesh, Material? material) tuple = CreateBoxVisuals(); Mesh item = tuple.mesh; Dictionary<MonsterTier, Material> dictionary = CreateTierMaterials(tuple.material); IReadOnlyList<MonsterEntry> entries = MonsterCatalog.Entries; for (int i = 0; i < entries.Count; i++) { MonsterEntry monsterEntry = entries[i]; TierSpec tier = MonsterCatalog.Tiers[monsterEntry.Tier]; Item val = CreateItem(monsterEntry, tier, 41000 + i); val.spawnPrefab = CreatePrefab(monsterEntry, tier, val, item, dictionary[monsterEntry.Tier], propsLayer, scanNodeLayer); Items.RegisterItem(val); ItemsByEntry.Add(monsterEntry, val); AllItems.Add(val); } Plugin.Log.LogInfo((object)$"{ItemsByEntry.Count} itens de restos de monstro registrados"); } } public static Item ItemFor(MonsterEntry entry) { return ItemsByEntry[entry]; } public static bool IsRemains(Item? item) { if ((Object)(object)item != (Object)null) { return AllItems.Contains(item); } return false; } private static Item CreateItem(MonsterEntry entry, TierSpec tier, int itemId) { Item obj = ScriptableObject.CreateInstance<Item>(); ((Object)obj).name = "BitencolaRemains_" + entry.Key; ((Object)obj).hideFlags = (HideFlags)32; obj.itemName = entry.ItemName; obj.itemId = itemId; obj.isScrap = true; obj.weight = ItemWeight.ToGameWeight(tier.WeightLb); obj.twoHanded = tier.TwoHanded; obj.twoHandedAnimation = tier.TwoHanded; obj.minValue = tier.MinValue; obj.maxValue = tier.MaxValue; obj.itemSpawnsOnGround = true; obj.canBeGrabbedBeforeGameStart = true; obj.requiresBattery = false; obj.verticalOffset = tier.BoxSize / 2f; obj.toolTips = Array.Empty<string>(); obj.meshVariants = Array.Empty<Mesh>(); obj.materialVariants = Array.Empty<Material>(); obj.clinkAudios = Array.Empty<AudioClip>(); obj.spawnPositionTypes = new List<ItemGroup>(); return obj; } private static GameObject CreatePrefab(MonsterEntry entry, TierSpec tier, Item item, Mesh mesh, Material? material, int propsLayer, int scanNodeLayer) { //IL_002e: 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_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown GameObject obj = NetworkPrefabs.CreateNetworkPrefab("BitencolaRemains_" + entry.Key); obj.tag = "PhysicsProp"; obj.layer = propsLayer; obj.transform.localScale = Vector3.one * tier.BoxSize; NetworkObject component = obj.GetComponent<NetworkObject>(); component.AutoObjectParentSync = false; component.DontDestroyWithOwner = true; obj.AddComponent<MeshFilter>().sharedMesh = mesh; MeshRenderer val = obj.AddComponent<MeshRenderer>(); ((Renderer)val).sharedMaterial = material; obj.AddComponent<BoxCollider>(); AudioSource obj2 = obj.AddComponent<AudioSource>(); obj2.playOnAwake = false; obj2.spatialBlend = 1f; PhysicsProp obj3 = obj.AddComponent<PhysicsProp>(); ((GrabbableObject)obj3).itemProperties = item; ((GrabbableObject)obj3).grabbable = true; ((GrabbableObject)obj3).mainObjectRenderer = val; ((GrabbableObject)obj3).insertedBattery = new Battery(true, 0f); AddScanNode(obj, item, scanNodeLayer); Utilities.FixMixerGroups(obj); return obj; } private static void AddScanNode(GameObject prefab, Item item, int scanNodeLayer) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_001c: 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) GameObject val = new GameObject("ScanNode"); val.transform.SetParent(prefab.transform, false); val.layer = scanNodeLayer; ((Collider)val.AddComponent<BoxCollider>()).isTrigger = true; ScanNodeProperties obj = val.AddComponent<ScanNodeProperties>(); obj.headerText = item.itemName; obj.subText = "Value: $0"; obj.nodeType = 2; obj.minRange = 1; obj.maxRange = 13; obj.requiresLineOfSight = true; } private static Dictionary<MonsterTier, Material?> CreateTierMaterials(Material? template) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: 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_006e: Expected O, but got Unknown //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) Dictionary<MonsterTier, Material> dictionary = new Dictionary<MonsterTier, Material>(); Color val2 = default(Color); foreach (MonsterTier value in Enum.GetValues(typeof(MonsterTier))) { if ((Object)(object)template == (Object)null) { dictionary[value] = null; continue; } Material val = new Material(template) { name = "BitencolaRemains_" + value, hideFlags = (HideFlags)32 }; var (num, num2, num3) = TierColors.For(value); ((Color)(ref val2))..ctor(num, num2, num3); if (val.HasProperty("_BaseColor")) { val.SetColor("_BaseColor", val2); } else if (val.HasProperty("_Color")) { val.color = val2; } dictionary[value] = val; } if ((Object)(object)template != (Object)null) { Object.Destroy((Object)(object)template); } return dictionary; } private static (Mesh mesh, Material? material) CreateBoxVisuals() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown GameObject obj = GameObject.CreatePrimitive((PrimitiveType)3); Mesh sharedMesh = obj.GetComponent<MeshFilter>().sharedMesh; Material sharedMaterial = ((Renderer)obj.GetComponent<MeshRenderer>()).sharedMaterial; Object.Destroy((Object)(object)obj); Material val = null; Shader val2 = Shader.Find("HDRP/Lit"); if ((Object)(object)val2 != (Object)null) { val = new Material(val2); } else { if (!((Object)(object)sharedMaterial != (Object)null)) { Plugin.Log.LogError((object)"Shader HDRP/Lit não encontrado e sem material padrão; os restos ficam sem material."); return (mesh: sharedMesh, material: null); } Plugin.Log.LogWarning((object)"Shader HDRP/Lit não encontrado; os restos usam o material padrão."); val = new Material(sharedMaterial); } return (mesh: sharedMesh, material: val); } private static int ResolveLayer(string name, int fallback) { int num = LayerMask.NameToLayer(name); if (num >= 0) { return num; } Plugin.Log.LogError((object)$"Layer \"{name}\" não encontrado (NameToLayer devolveu -1); usando {fallback}."); return fallback; } } [HarmonyPatch(typeof(RoundManager), "SyncScrapValuesClientRpc")] internal static class RemainsScrapSyncPatch { internal sealed class SavedTotals { public float TotalScrapValueInLevel; public int ScrapCollectedInLevel; public int ValueOfFoundScrapItems; public int RemainsValue; } [HarmonyPrefix] private static void Prefix(RoundManager __instance, NetworkObjectReference[] spawnedScrap, int[] allScrapValue, out SavedTotals? __state) { __state = null; try { if (TrySumOnlyRemains(spawnedScrap, allScrapValue, out var sum)) { __state = new SavedTotals { TotalScrapValueInLevel = __instance.totalScrapValueInLevel, ScrapCollectedInLevel = __instance.scrapCollectedInLevel, ValueOfFoundScrapItems = __instance.valueOfFoundScrapItems, RemainsValue = sum }; } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao preservar os contadores de sucata: {arg}"); } } [HarmonyPostfix] private static void Postfix(RoundManager __instance, SavedTotals? __state) { if (__state != null) { __instance.totalScrapValueInLevel = __state.TotalScrapValueInLevel + (float)__state.RemainsValue; __instance.scrapCollectedInLevel = __state.ScrapCollectedInLevel; __instance.valueOfFoundScrapItems = __state.ValueOfFoundScrapItems; } } private static bool TrySumOnlyRemains(NetworkObjectReference[]? spawnedScrap, int[]? values, out int sum) { sum = 0; if (spawnedScrap == null || values == null || spawnedScrap.Length == 0 || values.Length < spawnedScrap.Length) { return false; } NetworkObject val = default(NetworkObject); for (int i = 0; i < spawnedScrap.Length; i++) { if (!((NetworkObjectReference)(ref spawnedScrap[i])).TryGet(ref val, (NetworkManager)null) || (Object)(object)val == (Object)null) { return false; } GrabbableObject component = ((Component)val).GetComponent<GrabbableObject>(); if ((Object)(object)component == (Object)null || !RemainsItems.IsRemains(component.itemProperties)) { return false; } sum += values[i]; } return true; } } internal static class RemainsSpawner { private const float DeathAnimationSeconds = 3f; private const float ValueSyncDelaySeconds = 1f; private const float SpawnHeight = 0.5f; private static readonly RemainsLedger Ledger = new RemainsLedger(); private static readonly Random Rng = new Random(); private static int _round; public static void OnEnemyKilled(EnemyAI enemy, bool destroyed) { try { HandleKill(enemy, destroyed); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao criar restos de monstro: {arg}"); } } public static void OnRoundEnded() { Ledger.Clear(); _round++; } private static void HandleKill(EnemyAI enemy, bool destroyed) { //IL_0070: Unknown result type (might be due to invalid IL or missing references) EnemyType enemyType = enemy.enemyType; if ((Object)(object)enemyType == (Object)null) { return; } RemainsTiming remainsTiming = RemainsDeathRule.Decide(IsHost(), Plugin.Cfg.RemainsEnabled.Value, enemyType.canDie, destroyed, enemyType.canBeDestroyed, enemyType.destroyOnDeath); if (remainsTiming != RemainsTiming.None && Ledger.TryClaim(LedgerKey(enemy))) { MonsterEntry monsterEntry = MonsterCatalog.Resolve(TypeNameChain(((object)enemy).GetType())); if (remainsTiming == RemainsTiming.Immediately) { Spawn(monsterEntry, ((Component)enemy).transform.position, !enemy.isOutside); } else { StartOnRoundManager(SpawnAfterDeathAnimation(enemy, monsterEntry, _round), "restos de " + monsterEntry.DisplayName + " não criados"); } } } private static IEnumerator SpawnAfterDeathAnimation(EnemyAI enemy, MonsterEntry entry, int round) { Vector3 position = ((Component)enemy).transform.position; bool inFactory = !enemy.isOutside; yield return (object)new WaitForSeconds(3f); try { SpawnAtCorpse(enemy, entry, round, position, inFactory); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao criar restos de monstro: {arg}"); } } private static void SpawnAtCorpse(EnemyAI enemy, MonsterEntry entry, int round, Vector3 position, bool inFactory) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_001f: 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) if (round == _round && IsHost()) { if ((Object)(object)enemy != (Object)null) { position = ((Component)enemy).transform.position; inFactory = !enemy.isOutside; } Spawn(entry, position, inFactory); } } private static void Spawn(MonsterEntry entry, Vector3 position, bool inFactory) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { Plugin.Log.LogError((object)("RoundManager ausente: restos de " + entry.DisplayName + " não criados.")); return; } Item val = RemainsItems.ItemFor(entry); GameObject obj = Object.Instantiate<GameObject>(val.spawnPrefab, position + Vector3.up * 0.5f, Quaternion.identity, instance.spawnedScrapContainer); RemainsVisibility.MakeFindable(obj); GrabbableObject component = obj.GetComponent<GrabbableObject>(); component.isInFactory = inFactory; (int min, int max) tierRange = Plugin.Cfg.GetTierRange(entry.Tier); int item = tierRange.min; int item2 = tierRange.max; int num = (component.scrapValue = ValueRoller.Roll(item, item2, Rng)); NetworkObject component2 = obj.GetComponent<NetworkObject>(); component2.Spawn(false); Plugin.Log.LogInfo((object)$"Restos criados: {val.itemName} (${num})"); StartOnRoundManager(SyncValueLater(component2, num), $"valor de {val.itemName} (${num}) não sincronizado; os outros jogadores veem $0"); } private static IEnumerator SyncValueLater(NetworkObject remains, int value) { yield return (object)new WaitForSeconds(1f); try { RoundManager instance = RoundManager.Instance; if ((Object)(object)remains != (Object)null && remains.IsSpawned && (Object)(object)instance != (Object)null && IsHost()) { instance.SyncScrapValuesClientRpc((NetworkObjectReference[])(object)new NetworkObjectReference[1] { NetworkObjectReference.op_Implicit(remains) }, new int[1] { value }); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao sincronizar o valor dos restos de monstro: {arg}"); } } private static void StartOnRoundManager(IEnumerator routine, string consequence) { RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { Plugin.Log.LogError((object)("RoundManager ausente: " + consequence + ".")); return; } Coroutine val; try { val = ((MonoBehaviour)instance).StartCoroutine(routine); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao iniciar corrotina: {consequence}. {arg}"); return; } if (val == null) { Plugin.Log.LogError((object)("Corrotina não iniciada: " + consequence + ".")); } } private static bool IsHost() { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null) { return singleton.IsServer; } return false; } private static ulong LedgerKey(EnemyAI enemy) { return (ulong)((Object)enemy).GetInstanceID(); } private static IEnumerable<string> TypeNameChain(Type type) { Type t = type; while (t != null && t != typeof(EnemyAI)) { yield return t.Name; t = t.BaseType; } } } [HarmonyPatch(typeof(StartOfRound), "Awake")] internal static class RemainsTemplatePatch { [HarmonyPostfix] private static void AwakePostfix(StartOfRound __instance) { try { ApplyTemplates(__instance.allItemsList.itemsList); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao copiar ícone e sons para os restos de monstro: {arg}"); } } private static void ApplyTemplates(List<Item> allItems) { Item val = FindVanillaScrap(allItems, twoHanded: false); Item val2 = FindVanillaScrap(allItems, twoHanded: true); if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)"Sucata do jogo para servir de modelo não encontrada; alguns restos ficam sem ícone/sons."); } foreach (Item item in RemainsItems.All) { Item val3 = (item.twoHanded ? val2 : val); if ((Object)(object)val3 != (Object)null) { CopyFromTemplate(val3, item); } } } private static Item? FindVanillaScrap(List<Item> allItems, bool twoHanded) { Item val = null; foreach (Item allItem in allItems) { if (!((Object)(object)allItem == (Object)null) && allItem.isScrap && allItem.twoHanded == twoHanded && !((Object)(object)allItem.spawnPrefab == (Object)null) && !RemainsItems.IsRemains(allItem) && !Items.LethalLibItemList.Contains(allItem)) { GrabbableObject component = allItem.spawnPrefab.GetComponent<GrabbableObject>(); if ((Object)(object)component != (Object)null && ((object)component).GetType() == typeof(PhysicsProp)) { return allItem; } if (val == null) { val = allItem; } } } return val; } private static void CopyFromTemplate(Item template, Item item) { //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) item.itemIcon = template.itemIcon; item.grabSFX = template.grabSFX; item.dropSFX = template.dropSFX; item.pocketSFX = template.pocketSFX; item.throwSFX = template.throwSFX; item.grabAnim = template.grabAnim; item.restingRotation = template.restingRotation; CopyAudioSettings(template.spawnPrefab, item.spawnPrefab); } private static void CopyAudioSettings(GameObject templatePrefab, GameObject prefab) { //IL_0047: 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_0070: Invalid comparison between Unknown and I4 AudioSource component = templatePrefab.GetComponent<AudioSource>(); AudioSource component2 = prefab.GetComponent<AudioSource>(); if (!((Object)(object)component == (Object)null) && !((Object)(object)component2 == (Object)null)) { component2.outputAudioMixerGroup = component.outputAudioMixerGroup; component2.spatialBlend = component.spatialBlend; component2.dopplerLevel = component.dopplerLevel; component2.rolloffMode = component.rolloffMode; component2.minDistance = component.minDistance; component2.maxDistance = component.maxDistance; if ((int)component.rolloffMode == 2) { component2.SetCustomCurve((AudioSourceCurveType)0, component.GetCustomCurve((AudioSourceCurveType)0)); } } } } [HarmonyPatch] internal static class RemainsVisibility { public static void MakeFindable(GameObject root) { Transform[] componentsInChildren = root.GetComponentsInChildren<Transform>(true); foreach (Transform obj in componentsInChildren) { ((Object)((Component)obj).gameObject).hideFlags = (HideFlags)0; Component[] components = ((Component)obj).GetComponents<Component>(); foreach (Component val in components) { if ((Object)(object)val != (Object)null) { ((Object)val).hideFlags = (HideFlags)0; } } } } [HarmonyPatch(typeof(GrabbableObject), "Start")] [HarmonyPostfix] private static void StartPostfix(GrabbableObject __instance) { try { if ((Object)(object)__instance != (Object)null && RemainsItems.IsRemains(__instance.itemProperties)) { MakeFindable(((Component)__instance).gameObject); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao tornar os restos visíveis para o jogo: {arg}"); } } } [HarmonyPatch] internal static class RemainsWipePatch { [HarmonyPatch(typeof(RoundManager), "DespawnPropsAtEndOfRound")] [HarmonyPrefix] private static void BeforeDespawnProps(RoundManager __instance, bool despawnAllItems) { try { StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).IsServer && !((Object)(object)instance == (Object)null)) { Plugin.Log.LogInfo((object)$"Limpeza de fim de rodada: todos mortos={instance.allPlayersDead}, apagar tudo={despawnAllItems}, restos de monstro={CountRemains()}"); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao registrar a limpeza de fim de rodada: {arg}"); } } [HarmonyPatch(typeof(RoundManager), "DespawnPropsAtEndOfRound")] [HarmonyFinalizer] private static void AfterDespawnProps(Exception? __exception) { if (__exception != null) { Plugin.Log.LogError((object)$"A limpeza de itens do jogo falhou no fim da rodada: {__exception}"); } } private static int CountRemains() { int num = 0; GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); foreach (GrabbableObject val in array) { if ((Object)(object)val != (Object)null && RemainsItems.IsRemains(val.itemProperties)) { num++; } } return num; } } [HarmonyPatch] internal static class ScoreboardPatch { private const string Header = "Placar do dia"; private const string TipSource = "placar"; [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "ShipLeave")] private static void ShipLeavePostfix() { try { if (Plugin.Cfg.DailyScoreboardEnabled.Value) { string text = KillCreditPatches.Scoreboard.Format(); if (text.Length > 0) { TipQueue.Show("placar", "Placar do dia", text); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar o placar do dia: {arg}"); } finally { ClearSafely(KillCreditPatches.OnShipLeft); } } [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "EndOfGameClientRpc")] private static void EndOfGameClientRpcPostfix() { ClearSafely(KillCreditPatches.OnDayEnded); } [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "Start")] private static void StartPostfix() { ClearSafely(KillCreditPatches.OnSessionStarted); } private static void ClearSafely(Action clear) { try { clear(); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao zerar o placar do dia: {arg}"); } } } } namespace BitencolaHotFixes.Features.Hud { [HarmonyPatch] internal static class HealthLabel { private const string LabelName = "BitencolaHealthText"; [HarmonyPatch(typeof(HUDManager), "Update")] [HarmonyPostfix] private static void AfterHudUpdate(HUDManager __instance) { try { Refresh(__instance); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar a vida em números: {arg}"); } } private static void Refresh(HUDManager hud) { //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: 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_00db: Unknown result type (might be due to invalid IL or missing references) CanvasGroup selfRedCanvasGroup = hud.selfRedCanvasGroup; if ((Object)(object)selfRedCanvasGroup == (Object)null) { return; } Transform parent = ((Component)selfRedCanvasGroup).transform.parent; if ((Object)(object)parent == (Object)null) { return; } PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if (!Plugin.Cfg.ShowHealthNumbers.Value || !((Object)(object)val != (Object)null) || val.isPlayerDead) { Transform val2 = parent.Find("BitencolaHealthText"); if ((Object)(object)val2 != (Object)null && ((Component)val2).gameObject.activeSelf) { ((Component)val2).gameObject.SetActive(false); } return; } Vector3[] array = (Vector3[])(object)new Vector3[4]; ((RectTransform)((Component)selfRedCanvasGroup).transform).GetWorldCorners(array); Vector3 worldPosition = (array[0] + array[3]) * 0.5f; TextMeshProUGUI val3 = HudLabels.FindOrCloneTabLabel(hud, parent, "BitencolaHealthText", worldPosition); if (!((Object)(object)val3 == (Object)null)) { string text = HudText.Health(val.health); if (((TMP_Text)val3).text != text) { ((TMP_Text)val3).text = text; } if (!((Component)val3).gameObject.activeSelf) { ((Component)val3).gameObject.SetActive(true); } } } } internal static class HudLabels { public static TextMeshProUGUI? FindOrCloneTabLabel(HUDManager hud, Transform parent, string name, Vector3 worldPosition) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) Transform val = parent.Find(name); if ((Object)(object)val != (Object)null) { return ((Component)val).GetComponent<TextMeshProUGUI>(); } TextMeshProUGUI utilitySlotKeyText = hud.utilitySlotKeyText; if ((Object)(object)utilitySlotKeyText == (Object)null) { return null; } TextMeshProUGUI obj = Object.Instantiate<TextMeshProUGUI>(utilitySlotKeyText, parent, true); ((Object)obj).name = name; ((Transform)((TMP_Text)obj).rectTransform).position = worldPosition; ((TMP_Text)obj).enableWordWrapping = false; ((TMP_Text)obj).overflowMode = (TextOverflowModes)0; return obj; } } [HarmonyPatch] internal static class SlotValueLabels { private const string LabelName = "BitencolaSlotValue"; private const int SlotCount = 4; [HarmonyPatch(typeof(HUDManager), "Update")] [HarmonyPostfix] private static void AfterHudUpdate(HUDManager __instance) { try { Refresh(__instance); } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar o valor nos slots: {arg}"); } } private static void Refresh(HUDManager hud) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009b: 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_0175: Unknown result type (might be due to invalid IL or missing references) //IL_017a: 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) Image[] itemSlotIconFrames = hud.itemSlotIconFrames; Image itemOnlySlotIconFrame = hud.itemOnlySlotIconFrame; TextMeshProUGUI utilitySlotKeyText = hud.utilitySlotKeyText; if (itemSlotIconFrames == null || (Object)(object)itemOnlySlotIconFrame == (Object)null || (Object)(object)utilitySlotKeyText == (Object)null) { return; } PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); GrabbableObject[] array = (((Object)(object)val != (Object)null) ? val.ItemSlots : null); bool flag = Plugin.Cfg.ShowSlotValues.Value && array != null; Vector3 val2 = ((Transform)((TMP_Text)utilitySlotKeyText).rectTransform).position - ((Transform)((Graphic)itemOnlySlotIconFrame).rectTransform).position; Vector3 val3 = default(Vector3); ((Vector3)(ref val3))..ctor(val2.x, 0f - val2.y, val2.z); for (int i = 0; i < 4 && i < itemSlotIconFrames.Length; i++) { Image val4 = itemSlotIconFrames[i]; if ((Object)(object)val4 == (Object)null) { continue; } string text = ""; if (flag && array != null && i < array.Length) { GrabbableObject val5 = array[i]; if ((Object)(object)val5 != (Object)null && (Object)(object)val5.itemProperties != (Object)null) { text = HudText.ScrapValue(val5.scrapValue, val5.itemProperties.isScrap); } } if (text.Length == 0) { Transform val6 = ((Transform)((Graphic)val4).rectTransform).Find("BitencolaSlotValue"); if ((Object)(object)val6 != (Object)null && ((Component)val6).gameObject.activeSelf) { ((Component)val6).gameObject.SetActive(false); } continue; } TextMeshProUGUI val7 = HudLabels.FindOrCloneTabLabel(hud, (Transform)(object)((Graphic)val4).rectTransform, "BitencolaSlotValue", ((Transform)((Graphic)val4).rectTransform).position + val3); if (!((Object)(object)val7 == (Object)null)) { if (((TMP_Text)val7).text != text) { ((TMP_Text)val7).text = text; } if (!((Component)val7).gameObject.activeSelf) { ((Component)val7).gameObject.SetActive(true); } } } } } [HarmonyPatch(typeof(HUDManager), "Update")] internal static class TipQueue { private sealed class Tip { public string Source { get; } public string Header { get; set; } public string Body { get; set; } public Tip(string source, string header, string body) { Source = source; Header = header; Body = body; } } public const float MinSecondsOnScreen = 5f; private static readonly List<Tip> Pending = new List<Tip>(); private static HUDManager? _hud; private static float _lastShownAt = float.NegativeInfinity; private static string? _lastShownSource; public static void Show(string source, string header, string body) { try { HUDManager instance = HUDManager.Instance; ResetIfNewHud(instance); for (int i = 0; i < Pending.Count; i++) { if (Pending[i].Source == source) { Pending[i].Header = header; Pending[i].Body = body; return; } } float unscaledTime = Time.unscaledTime; bool flag = ((unscaledTime - _lastShownAt < 5f) ? (_lastShownSource == source) : (Pending.Count == 0)); if ((Object)(object)instance != (Object)null && flag) { Display(instance, source, header, body, unscaledTime); } else { Pending.Add(new Tip(source, header, body)); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar um aviso na tela: {arg}"); } } public static bool IsQueuedOrOnScreen(string source) { for (int i = 0; i < Pending.Count; i++) { if (Pending[i].Source == source) { return true; } } if (_lastShownSource == source) { return Time.unscaledTime - _lastShownAt < 5f; } return false; } [HarmonyPostfix] private static void UpdatePostfix(HUDManager __instance) { try { ResetIfNewHud(__instance); if (Pending.Count != 0) { float unscaledTime = Time.unscaledTime; if (!(unscaledTime - _lastShownAt < 5f)) { Tip tip = Pending[0]; Pending.RemoveAt(0); Display(__instance, tip.Source, tip.Header, tip.Body, unscaledTime); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar um aviso da fila: {arg}"); } } private static void ResetIfNewHud(HUDManager? hud) { if (!((Object)(object)hud == (Object)null) && hud != _hud) { if (_hud != null) { Pending.Clear(); _lastShownAt = float.NegativeInfinity; _lastShownSource = null; } _hud = hud; } } private static void Display(HUDManager hud, string source, string header, string body, float now) { hud.DisplayTip(header, body, false, false, "LC_Tip1"); _lastShownAt = now; _lastShownSource = source; } } [HarmonyPatch] internal static class WeightUnitPatch { private static bool _listeningToConfig; private static HUDManager? _startedHud; [HarmonyPatch(typeof(HUDManager), "Update")] [HarmonyPostfix] private static void AfterHudUpdate(HUDManager __instance) { try { if (_startedHud == __instance || (Object)(object)__instance.weightCounter == (Object)null) { return; } PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if (!((Object)(object)val == (Object)null)) { _startedHud = __instance; if (Plugin.Cfg.WeightInKilograms.Value) { ((TMP_Text)__instance.weightCounter).text = HudText.Weight(val.carryWeight, kilograms: true); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar o peso em kg no início: {arg}"); } } [HarmonyPatch(typeof(HUDManager), "UpdateWeightCounter")] [HarmonyPostfix] private static void AfterUpdateWeightCounter(HUDManager __instance) { try { ListenToConfigOnce(); if (Plugin.Cfg.WeightInKilograms.Value && !((Object)(object)__instance.weightCounter == (Object)null)) { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if (!((Object)(object)val == (Object)null)) { ((TMP_Text)__instance.weightCounter).text = HudText.Weight(val.carryWeight, kilograms: true); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao mostrar o peso em kg: {arg}"); } } private static void ListenToConfigOnce() { if (_listeningToConfig) { return; } _listeningToConfig = true; Plugin.Cfg.WeightInKilograms.SettingChanged += delegate { try { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if ((Object)(object)HUDManager.Instance != (Object)null && (Object)(object)val != (Object)null) { HUDManager.Instance.UpdateWeightCounter(); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Falha ao atualizar o peso: {arg}"); } }; } } } namespace BitencolaHotFixes.Core { public static class BatteryRecharge { public static float Next(float charge, float deltaSeconds, float secondsToFull) { if (secondsToFull <= 0f || float.IsNaN(secondsToFull)) { return 1f; } if (charge >= 1f) { return 1f; } float num = Math.Max(0f, deltaSeconds) / secondsToFull; return Math.Min(1f, Math.Max(0f, charge) + num); } } public static class SyncThrottle { public const float IntervalSeconds = 1f; public static bool ShouldSync(float now, float lastSyncAt, bool reachedFull) { if (!reachedFull) { return now - lastSyncAt >= 1f; } return true; } } public readonly struct CorpseCandidate { public float X { get; } public float Y { get; } public float Z { get; } public bool IsDead { get; } public bool IsHidden { get; } public CorpseCandidate(float x, float y, float z, bool isDead, bool isHidden) { X = x; Y = y; Z = z; IsDead = isDead; IsHidden = isHidden; } } public static class CorpsePicker { public const float MaxDistance = 3f; public static int PickNearest(IReadOnlyList<CorpseCandidate> candidates, float x, float y, float z, float maxDistance = 3f) { float num = maxDistance * maxDistance; int result = -1; float num2 = float.MaxValue; for (int i = 0; i < candidates.Count; i++) { CorpseCandidate corpseCandidate = candidates[i]; if (corpseCandidate.IsDead && !corpseCandidate.IsHidden) { float num3 = corpseCandidate.X - x; float num4 = corpseCandidate.Y - y; float num5 = corpseCandidate.Z - z; float num6 = num3 * num3 + num4 * num4 + num5 * num5; if (!(num6 > num) && !(num6 >= num2)) { result = i; num2 = num6; } } } return result; } } public readonly struct ScoreLine { public string Player { get; } public int Kills { get; } public int Value { get; } public ScoreLine(string player, int kills, int value) { Player = player; Kills = kills; Value = value; } } public sealed class DailyScoreboard { private readonly HashSet<int> _countedBoxes = new HashSet<int>(); private readonly Dictionary<string, (int kills, int value)> _byPlayer = new Dictionary<string, (int, int)>(StringComparer.Ordinal); public bool TryAdd(int boxInstanceId, string player, int value) { if (!_countedBoxes.Add(boxInstanceId)) { return false; } _byPlayer.TryGetValue(player, out (int, int) value2); _byPlayer[player] = (value2.Item1 + 1, value2.Item2 + value); return true; } public IReadOnlyList<ScoreLine> Lines() { List<ScoreLine> list = new List<ScoreLine>(_byPlayer.Count); foreach (KeyValuePair<string, (int, int)> item in _byPlayer) { list.Add(new ScoreLine(item.Key, item.Value.Item1, item.Value.Item2)); } list.Sort(delegate(ScoreLine a, ScoreLine b) { int num = b.Value.CompareTo(a.Value); return (num == 0) ? string.CompareOrdinal(a.Player, b.Player) : num; }); return list; } public string Format() { IReadOnlyList<ScoreLine> readOnlyList = Lines(); List<string> list = new List<string>(readOnlyList.Count); foreach (ScoreLine item in readOnlyList) { list.Add(new StringBuilder().Append(item.Player).Append(": ").Append(item.Kills) .Append((item.Kills == 1) ? " monstro" : " monstros") .Append(", $") .Append(item.Value) .ToString()); } return TipBody.Join(list); } public void Exclude(int boxInstanceId) { _countedBoxes.Add(boxInstanceId); } public void NewDay() { _byPlayer.Clear(); } public void Clear() { _countedBoxes.Clear(); _byPlayer.Clear(); } } public static class DeathCauseText { private const string Unknown = "causa desconhecida"; private static readonly string[] Texts = new string[19] { "causa desconhecida", "espancado", "queda", "explosão", "estrangulado", "sufocado", "dilacerado", "baleado", "esmagado", "afogado", "abandonado na lua", "eletrocutado", "chutado", "queimado", "esfaqueado", "ventilador", "inércia", "cortado ao meio", "arranhado" }; public static string For(int causeOfDeath) { if (causeOfDeath < 0 || causeOfDeath >= Texts.Length) { return "causa desconhecida"; } return Texts[causeOfDeath]; } } public static class DeathNoticeText { public static (string header, string body) Build(IReadOnlyList<(string name, string cause)> deaths) { if (deaths.Count == 1) { return (header: deaths[0].name + " morreu", body: "Causa: " + deaths[0].cause); } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < deaths.Count; i++) { if (i > 0) { stringBuilder.Append((i == deaths.Count - 1) ? " e " : ", "); } stringBuilder.Append(deaths[i].name); } stringBuilder.Append(" morreram"); List<string> list = new List<string>(deaths.Count); for (int j = 0; j < deaths.Count; j++) { list.Add(deaths[j].name + ": " + deaths[j].cause); } return (header: stringBuilder.ToString(), body: TipBody.Join(list)); } } public readonly struct PlayerLife { public int Id { get; } public bool Controlled { get; } public bool Dead { get; } public PlayerLife(int id, bool controlled, bool dead) { Id = id; Controlled = controlled; Dead = dead; } } public sealed class DeathWatcher { private readonly Dictionary<int, bool> _aliveLastTime = new Dictionary<int, bool>(); public IReadOnlyList<int> Update(IReadOnlyList<PlayerLife> players) { List<int> list = new List<int>(); bool value = default(bool); for (int i = 0; i < players.Count; i++) { PlayerLife playerLife = players[i]; if (playerLife.Controlled && playerLife.Dead && _aliveLastTime.TryGetValue(playerLife.Id, out value) && value) { list.Add(playerLife.Id); } _aliveLastTime[playerLife.Id] = playerLife.Controlled && !playerLife.Dead; } return list; } public void Reset() { _aliveLastTime.Clear(); } } public static class EmoteRemap { public static string? NewPath(string actionName, string effectivePath) { if (Matches(actionName, "Emote1") && Matches(effectivePath, "<Keyboard>/1")) { return "<Keyboard>/5"; } if (Matches(actionName, "Emote2") && Matches(effectivePath, "<Keyboard>/2")) { return "<Keyboard>/6"; } return null; } private static bool Matches(string value, string expected) { return string.Equals(value, expected, StringComparison.OrdinalIgnoreCase); } } public static class HudText { private const float PoundsPerWeightUnit = 105f; private const double KilogramsPerPound = 0.4536; public static string ScrapValue(int value, bool isScrap) { if (!isScrap || value <= 0) { return ""; } return "$" + value.ToString(CultureInfo.InvariantCulture); } public static string Health(int health, int maxHealth = 100) { return Math.Max(0, health).ToString(CultureInfo.InvariantCulture) + "/" + maxHealth.ToString(CultureInfo.InvariantCulture); } public static string Weight(float carryWeight, bool kilograms) { double num = Math.Round(Math.Min(Math.Max(carryWeight - 1f, 0f), 100f) * 105f, MidpointRounding.ToEven); if (!kilograms) { return num.ToString("0", CultureInfo.InvariantCulture) + " lb"; } return Math.Round(num * 0.4536, MidpointRounding.ToEven).ToString("0", CultureInfo.InvariantCulture) + " kg"; } } public enum SwitchDecision { Switch, Wait, Ignore } public static class InputGate { public const float DefaultSwitchCooldown = 0.1f; public const float DropCooldown = 0.2f; public static SwitchDecision DecideSwitch(in PlayerInputState s, int targetSlot, float cooldown) { if (!s.CanAct || s.Busy || s.TwoHanded) { return SwitchDecision.Ignore; } if (targetSlot < 0 || targetSlot >= s.SlotCount || targetSlot == s.CurrentSlot) { return SwitchDecision.Ignore; } if (!(s.TimeSinceSwitch < cooldown)) { return SwitchDecision.Switch; } return SwitchDecision.Wait; } public static bool CanDropAll(in PlayerInputState s) { if (s.CanAct && !s.Busy && s.HasAnyItem) { return s.TimeSinceSwitch >= 0.2f; } return false; } public static bool CanUnloadScrap(in PlayerInputState s, bool inBuildMode) { if (s.CanAct && !s.Busy && !inBuildMode) { return s.TimeSinceSwitch >= 0.2f; } return false; } } public static class ItemWeight { public static float ToGameWeight(int pounds) { return 1f + (float)pounds / 105f; } } public sealed class KillCredits { public const float LateHitWindowSeconds = 0.15f; private readonly Dictionary<int, string> _lastHitBy = new Dictionary<int, string>(); private readonly Dictionary<int, float> _diedAt = new Dictionary<int, float>(); private readonly HashSet<int> _lateHitTaken = new HashSet<int>(); public static bool IsLateHitAllowed(bool thisMachineOwnsEnemy, ulong hitterClientId, ulong enemyOwnerClientId) { if (!thisMachineOwnsEnemy) { return hitterClientId == enemyOwnerClientId; } return false; } public void RecordHit(int enemyInstanceId, string playerName) { _lastHitBy[enemyInstanceId] = playerName; } public void RecordHit(int enemyInstanceId, string playerName, bool enemyDead, bool lateHitAllowed, float time) { float value; if (!enemyDead) { RecordHit(enemyInstanceId, playerName); } else if (lateHitAllowed && _diedAt.TryGetValue(enemyInstanceId, out value) && time - value <= 0.15f && _lateHitTaken.Add(enemyInstanceId)) { _lastHitBy[enemyInstanceId] = playerName; } } public void MarkDead(int enemyInstanceId, float time) { _diedAt[enemyInstanceId] = time; _lateHitTaken.Remove(enemyInstanceId); } public string? KillerOf(int enemyInstanceId) { if (!_lastHitBy.TryGetValue(enemyInstanceId, out string value)) { return null; } return value; } public void Clear() { _lastHitBy.Clear(); _diedAt.Clear(); _lateHitTaken.Clear(); } } public static class RemainsScanText { public static string Format(int value, string? killer) { string text = "Value: $" + value; if (killer != null) { return text + "\nMorto por " + killer; } return text; } } public enum MonsterTier { Small, Medium, Dangerous, VeryDangerous, Giant, Other } public sealed class TierSpec { public MonsterTier Tier { get;