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 RepoFfaMod v0.1.0
plugins\RepoFfaMod\RepoFfaMod.dll
Decompiled 4 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RepoFfaMod.Config; using RepoFfaMod.Gameplay; using RepoFfaMod.UI; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("RepoFfaMod")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("RepoFfaMod")] [assembly: AssemblyTitle("RepoFfaMod")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 RepoFfaMod { [BepInPlugin("repo.ffa.lastmanstanding", "REPO FFA Last Man Standing", "0.1.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "repo.ffa.lastmanstanding"; public const string PluginName = "REPO FFA Last Man Standing"; public const string PluginVersion = "0.1.0"; private Harmony? _harmony; private RoundDirector? _roundDirector; private CrouchBudgetSystem? _crouchSystem; private HudOverlay? _hudOverlay; private LeaderboardSystem? _leaderboard; private float _tickAccumulator; internal static Plugin? Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal RoundDirector? RoundDirector => _roundDirector; internal CrouchBudgetSystem? CrouchSystem => _crouchSystem; internal FfaConfig? FfaConfig { get; private set; } private void Awake() { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; FfaConfig config = (FfaConfig = new FfaConfig(((BaseUnityPlugin)this).Config)); RepoGameBridge bridge = new RepoGameBridge(((BaseUnityPlugin)this).Logger, config); _leaderboard = new LeaderboardSystem(); SpawnDirector spawnDirector = new SpawnDirector(config, bridge, ((BaseUnityPlugin)this).Logger); LoomDirector loomDirector = new LoomDirector(config, bridge, ((BaseUnityPlugin)this).Logger); _roundDirector = new RoundDirector(config, bridge, ((BaseUnityPlugin)this).Logger, _leaderboard, spawnDirector, loomDirector); _crouchSystem = new CrouchBudgetSystem(config); _hudOverlay = new HudOverlay(config, _leaderboard, _roundDirector); _harmony = new Harmony("repo.ffa.lastmanstanding"); _harmony.PatchAll(); _roundDirector.StartRound(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"REPO FFA Last Man Standing loaded."); } private void Update() { if (_roundDirector == null || _crouchSystem == null) { return; } float deltaTime = Time.deltaTime; _tickAccumulator += deltaTime; if (_tickAccumulator < 0.05f) { return; } float tickAccumulator = _tickAccumulator; _tickAccumulator = 0f; _roundDirector.Tick(tickAccumulator); foreach (PlayerState value in _roundDirector.CurrentRound.Players.Values) { _crouchSystem.TickPlayer(value, tickAccumulator); } } private void OnGUI() { _hudOverlay?.Draw(); } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } } namespace RepoFfaMod.UI { internal sealed class HudOverlay { private readonly FfaConfig _config; private readonly LeaderboardSystem _leaderboard; private readonly RoundDirector _roundDirector; public HudOverlay(FfaConfig config, LeaderboardSystem leaderboard, RoundDirector roundDirector) { _config = config; _leaderboard = leaderboard; _roundDirector = roundDirector; } public void Draw() { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) if (!_roundDirector.RoundStarted) { return; } GUI.Box(new Rect(20f, 20f, 310f, 180f), "R.E.P.O FFA"); PlayerState playerState = _roundDirector.CurrentRound.Players.Values.FirstOrDefault(); if (playerState != null) { GUI.Label(new Rect(30f, 50f, 290f, 22f), $"Crouch: {playerState.CrouchBudgetRemaining:0.0}s / {_config.MaxCrouchSeconds.Value:0}s"); } GUI.Label(new Rect(30f, 72f, 290f, 22f), $"Round Time: {_roundDirector.CurrentRound.ElapsedSeconds:0}s"); float num = 96f; foreach (var row in _leaderboard.GetRows(_roundDirector.CurrentRound)) { string item = row.Name; bool item2 = row.Alive; int item3 = row.Wins; string arg = (item2 ? "ALIVE" : "OUT"); GUI.Label(new Rect(30f, num, 290f, 20f), $"{item}: {arg} | Wins {item3}"); num += 18f; } } } } namespace RepoFfaMod.Patches { internal static class RuntimeReaders { public static string ReadString(object instance, string name) { return (instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance) as string) ?? string.Empty; } public static object? ReadField(object instance, string name) { return instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance); } } [HarmonyPatch] internal static class PlayerStatePatches { private static MethodBase? TargetMethod() { Type type = AccessTools.TypeByName("PlayerAvatar"); if (!(type == null)) { return AccessTools.Method(type, "SetState", (Type[])null, (Type[])null); } return null; } private static void Prefix(object __instance, ref bool crouching) { Plugin instance = Plugin.Instance; if (instance?.RoundDirector != null && instance.CrouchSystem != null && instance.FfaConfig != null) { string text = RuntimeReaders.ReadString(__instance, "steamID"); string text2 = RuntimeReaders.ReadString(__instance, "playerName"); if (string.IsNullOrWhiteSpace(text)) { text = $"player_{__instance.GetHashCode()}"; } PlayerState orCreatePlayer = instance.RoundDirector.GetOrCreatePlayer(text, string.IsNullOrWhiteSpace(text2) ? text : text2); orCreatePlayer.RuntimeRef = __instance; orCreatePlayer.IsCrouching = crouching; if (crouching && !instance.CrouchSystem.CanCrouch(orCreatePlayer)) { crouching = false; orCreatePlayer.IsCrouching = false; } } } } [HarmonyPatch] internal static class PlayerDeathPatches { private static MethodBase? TargetMethod() { Type type = AccessTools.TypeByName("PlayerHealth"); if (!(type == null)) { return AccessTools.Method(type, "Death", (Type[])null, (Type[])null); } return null; } private static void Postfix(object __instance) { Plugin instance = Plugin.Instance; if (instance?.RoundDirector == null) { return; } object obj = RuntimeReaders.ReadField(__instance, "playerAvatar"); if (obj != null) { string text = RuntimeReaders.ReadString(obj, "steamID"); if (string.IsNullOrWhiteSpace(text)) { text = $"player_{obj.GetHashCode()}"; } instance.RoundDirector.MarkDead(text); } } } } namespace RepoFfaMod.Gameplay { internal sealed class CrouchBudgetSystem { private readonly FfaConfig _config; public CrouchBudgetSystem(FfaConfig config) { _config = config; } public void EnsurePlayerRegistered(PlayerState player) { if (player.CrouchBudgetRemaining <= 0f) { player.CrouchBudgetRemaining = _config.MaxCrouchSeconds.Value; } } public bool CanCrouch(PlayerState player) { return player.CrouchBudgetRemaining > 0f; } public void TickPlayer(PlayerState player, float deltaTime) { float value = _config.MaxCrouchSeconds.Value; float num = value / _config.CrouchRechargeSeconds.Value; if (!player.IsAlive) { return; } if (player.IsCrouching) { player.CrouchBudgetRemaining -= deltaTime; if (player.CrouchBudgetRemaining < 0f) { player.CrouchBudgetRemaining = 0f; player.IsCrouching = false; } } else { player.CrouchBudgetRemaining += num * deltaTime; if (player.CrouchBudgetRemaining > value) { player.CrouchBudgetRemaining = value; } } } } internal sealed class PlayerState { public string PlayerId { get; } public string DisplayName { get; } public bool IsAlive { get; set; } = true; public bool IsCrouching { get; set; } public float CrouchBudgetRemaining { get; set; } public object? RuntimeRef { get; set; } public PlayerState(string playerId, string displayName) { PlayerId = playerId; DisplayName = displayName; CrouchBudgetRemaining = 20f; } } internal sealed class RoundState { public readonly Dictionary<string, PlayerState> Players = new Dictionary<string, PlayerState>(); public float ElapsedSeconds { get; set; } public bool ExtraWaveSpawned { get; set; } public bool LoomPhaseTriggered { get; set; } public string? WinnerPlayerId { get; set; } } internal interface IGameBridge { IReadOnlyCollection<PlayerState> SnapshotPlayers(); bool IsHost(); void ForceStage(int stageIndex, bool superMoonEnabled); void SpawnMonsters(int count); void SpawnMeleeWeapons(int count); void SpawnLoomForPlayer(string playerId, float speedMultiplier); void UpdateLoomSpeed(float speedMultiplier); } internal sealed class LeaderboardSystem { private readonly Dictionary<string, int> _winsByPlayer = new Dictionary<string, int>(); public void SyncPlayers(RoundState round) { foreach (PlayerState value in round.Players.Values) { if (!_winsByPlayer.ContainsKey(value.PlayerId)) { _winsByPlayer[value.PlayerId] = 0; } } } public void MarkPlayerDead(RoundState round, string playerId) { if (round.Players.TryGetValue(playerId, out PlayerState value)) { value.IsAlive = false; } } public void RecordWin(string playerId) { if (!_winsByPlayer.ContainsKey(playerId)) { _winsByPlayer[playerId] = 0; } _winsByPlayer[playerId]++; } public IReadOnlyList<(string PlayerId, string Name, bool Alive, int Wins)> GetRows(RoundState round) { int value; return (from player in round.Players.Values select (player.PlayerId, player.DisplayName, player.IsAlive, _winsByPlayer.TryGetValue(player.PlayerId, out value) ? value : 0) into row orderby row.Item4 descending, row.IsAlive descending, row.DisplayName select row).ToList(); } } internal sealed class LoomDirector { private readonly FfaConfig _config; private readonly IGameBridge _bridge; private readonly ManualLogSource _log; private float _loomPhaseSeconds; private readonly HashSet<string> _loomTargets = new HashSet<string>(); public LoomDirector(FfaConfig config, IGameBridge bridge, ManualLogSource log) { _config = config; _bridge = bridge; _log = log; } public void TriggerFinalThreePhase(RoundState round) { if (!_bridge.IsHost()) { return; } _loomPhaseSeconds = 0f; _loomTargets.Clear(); foreach (PlayerState item in round.Players.Values.Where((PlayerState p) => p.IsAlive).Take(3).ToList()) { _loomTargets.Add(item.PlayerId); _bridge.SpawnLoomForPlayer(item.PlayerId, _config.LoomBaseSpeedMultiplier.Value); } _log.LogInfo((object)$"Final-3 Loom phase activated with {_config.FinalThreeLoomCount.Value} Looms."); } public void Tick(RoundState round, float deltaTime) { if (_bridge.IsHost() && round.LoomPhaseTriggered) { _loomPhaseSeconds += deltaTime; float speedMultiplier = _config.LoomBaseSpeedMultiplier.Value + _loomPhaseSeconds * _config.LoomSpeedIncreasePerSecond.Value; _bridge.UpdateLoomSpeed(speedMultiplier); } } } internal sealed class RoundDirector { private readonly FfaConfig _config; private readonly IGameBridge _bridge; private readonly ManualLogSource _log; private readonly LeaderboardSystem _leaderboard; private readonly SpawnDirector _spawnDirector; private readonly LoomDirector _loomDirector; public RoundState CurrentRound { get; } = new RoundState(); public bool RoundStarted { get; private set; } public RoundDirector(FfaConfig config, IGameBridge bridge, ManualLogSource log, LeaderboardSystem leaderboard, SpawnDirector spawnDirector, LoomDirector loomDirector) { _config = config; _bridge = bridge; _log = log; _leaderboard = leaderboard; _spawnDirector = spawnDirector; _loomDirector = loomDirector; } public void StartRound() { RoundStarted = true; CurrentRound.ElapsedSeconds = 0f; CurrentRound.ExtraWaveSpawned = false; CurrentRound.LoomPhaseTriggered = false; CurrentRound.WinnerPlayerId = null; CurrentRound.Players.Clear(); foreach (PlayerState item in _bridge.SnapshotPlayers()) { MergePlayer(item); } if (_bridge.IsHost()) { _bridge.ForceStage(_config.StartStage.Value, superMoonEnabled: true); _spawnDirector.SpawnOpeningWave(CurrentRound.Players.Count); _log.LogInfo((object)$"FFA round started at stage {_config.StartStage.Value}."); } } public void Tick(float deltaTime) { if (!RoundStarted) { return; } CurrentRound.ElapsedSeconds += deltaTime; foreach (PlayerState item in _bridge.SnapshotPlayers()) { MergePlayer(item); } _leaderboard.SyncPlayers(CurrentRound); if (_bridge.IsHost()) { if (!CurrentRound.ExtraWaveSpawned && CurrentRound.ElapsedSeconds >= _config.ExtraWaveDelaySeconds.Value) { _spawnDirector.SpawnExtraWave(); CurrentRound.ExtraWaveSpawned = true; } int num = CurrentRound.Players.Values.Count((PlayerState p) => p.IsAlive); if (num <= 3 && !CurrentRound.LoomPhaseTriggered) { _loomDirector.TriggerFinalThreePhase(CurrentRound); CurrentRound.LoomPhaseTriggered = true; } if (num == 1 && CurrentRound.WinnerPlayerId == null) { PlayerState playerState = CurrentRound.Players.Values.First((PlayerState p) => p.IsAlive); CurrentRound.WinnerPlayerId = playerState.PlayerId; _leaderboard.RecordWin(playerState.PlayerId); _log.LogInfo((object)("Round winner: " + playerState.DisplayName)); } } _loomDirector.Tick(CurrentRound, deltaTime); } public PlayerState GetOrCreatePlayer(string playerId, string displayName) { if (!CurrentRound.Players.TryGetValue(playerId, out PlayerState value)) { value = new PlayerState(playerId, displayName); CurrentRound.Players[playerId] = value; } return value; } public void MarkDead(string playerId) { if (CurrentRound.Players.TryGetValue(playerId, out PlayerState value)) { value.IsAlive = false; } } private void MergePlayer(PlayerState incoming) { PlayerState orCreatePlayer = GetOrCreatePlayer(incoming.PlayerId, incoming.DisplayName); orCreatePlayer.IsAlive = incoming.IsAlive; orCreatePlayer.IsCrouching = incoming.IsCrouching; orCreatePlayer.RuntimeRef = incoming.RuntimeRef; } } internal sealed class SpawnDirector { private readonly FfaConfig _config; private readonly IGameBridge _bridge; private readonly ManualLogSource _log; public SpawnDirector(FfaConfig config, IGameBridge bridge, ManualLogSource log) { _config = config; _bridge = bridge; _log = log; } public void SpawnOpeningWave(int playerCount) { if (_bridge.IsHost()) { _bridge.SpawnMonsters(_config.InitialMonsterCount.Value); int num = Math.Max(1, (int)Math.Ceiling((float)playerCount * _config.MeleeSpawnPerPlayer.Value)); _bridge.SpawnMeleeWeapons(num); _log.LogInfo((object)$"Spawned opening wave ({_config.InitialMonsterCount.Value}) and {num} melee weapons."); } } public void SpawnExtraWave() { if (_bridge.IsHost()) { _bridge.SpawnMonsters(_config.ExtraWaveCount.Value); _log.LogInfo((object)$"Spawned extra wave (+{_config.ExtraWaveCount.Value} monsters)."); } } } internal sealed class RepoGameBridge : IGameBridge { private readonly ManualLogSource _log; private readonly FfaConfig _config; private readonly Assembly _gameAssembly; private readonly Type _playerControllerType; private readonly Type _playerAvatarType; private readonly Type _playerHealthType; private readonly Type _runManagerType; private readonly Type _levelGeneratorType; private readonly Type _enemyParentType; private readonly Type _itemMeleeType; private readonly Type _spawnPointType; private readonly Type _photonNetworkType; public RepoGameBridge(ManualLogSource log, FfaConfig config) { _log = log; _config = config; _gameAssembly = AppDomain.CurrentDomain.GetAssemblies().First((Assembly a) => a.GetName().Name == "Assembly-CSharp"); _playerControllerType = _gameAssembly.GetType("PlayerController"); _playerAvatarType = _gameAssembly.GetType("PlayerAvatar"); _playerHealthType = _gameAssembly.GetType("PlayerHealth"); _runManagerType = _gameAssembly.GetType("RunManager"); _levelGeneratorType = _gameAssembly.GetType("LevelGenerator"); _enemyParentType = _gameAssembly.GetType("EnemyParent"); _itemMeleeType = _gameAssembly.GetType("ItemMelee"); _spawnPointType = _gameAssembly.GetType("SpawnPoint"); _photonNetworkType = (from a in AppDomain.CurrentDomain.GetAssemblies() select a.GetType("Photon.Pun.PhotonNetwork")).FirstOrDefault((Type t) => t != null); } public IReadOnlyCollection<PlayerState> SnapshotPlayers() { List<PlayerState> list = new List<PlayerState>(); Object[] array = Object.FindObjectsOfType(_playerControllerType); foreach (object obj in array) { object field = GetField(obj, "playerAvatarScript"); if (field != null) { string text = ReadString(field, "steamID"); string text2 = ReadString(field, "playerName"); bool isCrouching = ReadBool(field, "isCrouching"); object field2 = GetField(field, "playerHealth"); bool isAlive = field2 == null || ReadInt(field2, "health") > 0; string text3 = (string.IsNullOrWhiteSpace(text) ? $"player_{obj.GetHashCode()}" : text); string displayName = (string.IsNullOrWhiteSpace(text2) ? text3 : text2); list.Add(new PlayerState(text3, displayName) { IsAlive = isAlive, IsCrouching = isCrouching, CrouchBudgetRemaining = _config.MaxCrouchSeconds.Value, RuntimeRef = field }); } } return list; } public bool IsHost() { PropertyInfo property = _photonNetworkType.GetProperty("IsMasterClient", BindingFlags.Static | BindingFlags.Public); if (!(property == null)) { return (bool)(property.GetValue(null) ?? ((object)true)); } return true; } public void ForceStage(int stageIndex, bool superMoonEnabled) { object singleton = GetSingleton(_runManagerType, "instance"); if (singleton != null) { int num = Mathf.Max(0, stageIndex - 1); SetField(singleton, "levelsCompleted", num); _runManagerType.GetMethod("UpdateMoonLevel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(singleton, Array.Empty<object>()); _log.LogInfo((object)$"Stage force applied: stage={stageIndex}, levelsCompleted={num}, superMoon={superMoonEnabled}"); } } public void SpawnMonsters(int count) { object singleton = GetSingleton(_levelGeneratorType, "Instance"); if (singleton != null) { int num = ReadInt(singleton, "EnemiesSpawnTarget"); SetField(singleton, "EnemiesSpawnTarget", num + count); _log.LogInfo((object)$"Adjusted enemy spawn target by +{count} (now {num + count})."); } } public void SpawnMeleeWeapons(int count) { //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) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) object[] array = Object.FindObjectsOfType(_spawnPointType); object[] array2 = array; if (array2.Length == 0) { _log.LogWarning((object)"No SpawnPoint objects found for melee placement."); return; } Object[] array3 = Object.FindObjectsOfType(_itemMeleeType); if (array3.Length == 0) { _log.LogWarning((object)"No ItemMelee prefab instance found in scene for cloning."); return; } Object val = array3[0]; Vector3 val3 = default(Vector3); for (int i = 0; i < count; i++) { object obj = array2[i % array2.Length]; object? obj2 = obj.GetType().GetProperty("transform")?.GetValue(obj); Transform val2 = (Transform)((obj2 is Transform) ? obj2 : null); if (!((Object)(object)val2 == (Object)null)) { ((Vector3)(ref val3))..ctor(Random.Range(-1.5f, 1.5f), 0.3f, Random.Range(-1.5f, 1.5f)); Object.Instantiate(val, val2.position + val3, Random.rotation); } } _log.LogInfo((object)$"Spawned {count} melee weapons around map points."); } public void SpawnLoomForPlayer(string playerId, float speedMultiplier) { SpawnMonsters(1); AssignNewestEnemyToPlayer(playerId, speedMultiplier); } public void UpdateLoomSpeed(float speedMultiplier) { object[] array = Object.FindObjectsOfType(_enemyParentType); array = array; for (int i = 0; i < array.Length; i++) { SetField(array[i], "actionMultiplier", speedMultiplier); } } private void AssignNewestEnemyToPlayer(string playerId, float speedMultiplier) { string playerId2 = playerId; object[] source = Object.FindObjectsOfType(_playerAvatarType); object obj = source.FirstOrDefault((object a) => ReadString(a, "steamID") == playerId2); if (obj == null) { return; } source = Object.FindObjectsOfType(_enemyParentType); object[] array = source; if (array.Length != 0) { object instance = array[^1]; SetField(instance, "actionMultiplier", speedMultiplier); object field = GetField(instance, "Enemy"); if (field != null) { field.GetType().GetMethod("SetChaseTarget", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(field, new object[1] { obj }); _log.LogInfo((object)$"Assigned Loom target to player {playerId2} at speed {speedMultiplier:0.00}"); } } } private static object? GetSingleton(Type type, string fieldName) { return type.GetField(fieldName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); } private static object? GetField(object instance, string name) { return instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance); } private static void SetField(object instance, string name, object value) { instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(instance, value); } private static string ReadString(object instance, string name) { return (GetField(instance, name) as string) ?? string.Empty; } private static bool ReadBool(object instance, string name) { object field = GetField(instance, name); bool flag = default(bool); int num; if (field is bool) { flag = (bool)field; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } private static int ReadInt(object instance, string name) { object field = GetField(instance, name); if (field is int) { return (int)field; } return 0; } } } namespace RepoFfaMod.Config { internal sealed class FfaConfig { public ConfigEntry<float> MaxCrouchSeconds { get; } public ConfigEntry<float> CrouchRechargeSeconds { get; } public ConfigEntry<int> StartStage { get; } public ConfigEntry<int> InitialMonsterCount { get; } public ConfigEntry<float> ExtraWaveDelaySeconds { get; } public ConfigEntry<int> ExtraWaveCount { get; } public ConfigEntry<int> FinalThreeLoomCount { get; } public ConfigEntry<float> LoomBaseSpeedMultiplier { get; } public ConfigEntry<float> LoomSpeedIncreasePerSecond { get; } public ConfigEntry<float> MeleeSpawnPerPlayer { get; } public ConfigEntry<bool> VerboseLogging { get; } public FfaConfig(ConfigFile config) { MaxCrouchSeconds = config.Bind<float>("Crouch", "MaxCrouchSeconds", 20f, "Maximum crouch stamina in seconds."); CrouchRechargeSeconds = config.Bind<float>("Crouch", "RechargeDurationSeconds", 5f, "Time to recharge from empty to full crouch stamina."); StartStage = config.Bind<int>("Round", "StartStage", 20, "Stage to force for this mode."); InitialMonsterCount = config.Bind<int>("Round", "InitialMonsterCount", 11, "Initial monster count on round start."); ExtraWaveDelaySeconds = config.Bind<float>("Round", "ExtraWaveDelaySeconds", 180f, "Delay until the second enemy wave."); ExtraWaveCount = config.Bind<int>("Round", "ExtraWaveCount", 6, "Additional monsters to spawn at wave time."); FinalThreeLoomCount = config.Bind<int>("Loom", "CountAtFinalThree", 3, "Loom count when only three players remain."); LoomBaseSpeedMultiplier = config.Bind<float>("Loom", "BaseSpeedMultiplier", 1.25f, "Initial speed multiplier for Looms."); LoomSpeedIncreasePerSecond = config.Bind<float>("Loom", "SpeedIncreasePerSecond", 0.015f, "Extra Loom speed per second after spawn."); MeleeSpawnPerPlayer = config.Bind<float>("Weapons", "MeleeSpawnPerPlayer", 1.5f, "How many melee weapons to spawn per alive player."); VerboseLogging = config.Bind<bool>("Debug", "VerboseLogging", false, "Enable verbose mode logs."); } } }