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 RunningMan v1.5.10
plugins\RunningMan\RunningMan.dll
Decompiled 2 days 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.Globalization; using System.IO; using System.Linq; 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 HarmonyLib; using Microsoft.CodeAnalysis; using RunningMan.Net; using RunningMan.Patches; using RunningMan.Storage; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("RunningMan")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Marathon race tracker for Valheim dedicated servers")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+0a508e319f966e579403952eda19df5d5ddd287a")] [assembly: AssemblyProduct("RunningMan")] [assembly: AssemblyTitle("RunningMan")] [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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunningMan { [Serializable] public sealed class RaceGate { public Vector3Data PointA = new Vector3Data(); public Vector3Data PointB = new Vector3Data(); public Vector3Data Forward = new Vector3Data(); public RaceGate() { } public RaceGate(Vector3 pointA, Vector3 pointB, Vector3 forward) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) PointA = ValheimUtil.ToData(pointA); PointB = ValheimUtil.ToData(pointB); Forward = ValheimUtil.ToData(forward); } public Vector3 GetPointA() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return ValheimUtil.FromData(PointA); } public Vector3 GetPointB() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return ValheimUtil.FromData(PointB); } public Vector3 GetForward() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return ValheimUtil.FromData(Forward); } public Vector3 GetMidpoint() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) return (GetPointA() + GetPointB()) * 0.5f; } public bool IsConfigured() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) if (PointA != null && PointB != null) { Vector3 val = GetPointA() - GetPointB(); return ((Vector3)(ref val)).sqrMagnitude > 0.01f; } return false; } public void SetEndpoint(bool pointA, Vector3 worldPosition, Vector3? preferredForward = null) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) if (pointA) { PointA = ValheimUtil.ToData(worldPosition); } else { PointB = ValheimUtil.ToData(worldPosition); } RecomputeForward(preferredForward); } public void RecomputeForward(Vector3? preferredForward = null) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: 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_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: 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) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: 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_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_006d: 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) Vector3 pointA = GetPointA(); Vector3 val = GetPointB() - pointA; val.y = 0f; if (((Vector3)(ref val)).sqrMagnitude < 0.0001f) { Vector3 val2 = (Vector3)(((??)preferredForward) ?? GetForward()); val2.y = 0f; if (((Vector3)(ref val2)).sqrMagnitude < 0.0001f) { val2 = Vector3.forward; } Forward = ValheimUtil.ToData(((Vector3)(ref val2)).normalized); return; } Vector3 val3 = Vector3.Cross(Vector3.up, ((Vector3)(ref val)).normalized); if (preferredForward.HasValue) { Vector3 value = preferredForward.Value; value.y = 0f; if (((Vector3)(ref value)).sqrMagnitude > 0.0001f && Vector3.Dot(val3, value) < 0f) { val3 = -val3; } } else { Vector3 forward = GetForward(); forward.y = 0f; if (((Vector3)(ref forward)).sqrMagnitude > 0.0001f && Vector3.Dot(val3, forward) < 0f) { val3 = -val3; } } Forward = ValheimUtil.ToData(((Vector3)(ref val3)).normalized); } public static RaceGate FromPositionAndForward(Vector3 position, Vector3 forward, float width) { //IL_001a: 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_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: 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) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0046: 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_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) forward.y = 0f; if (((Vector3)(ref forward)).sqrMagnitude < 0.0001f) { forward = Vector3.forward; } else { ((Vector3)(ref forward)).Normalize(); } Vector3 val = Vector3.Cross(Vector3.up, forward); Vector3 normalized = ((Vector3)(ref val)).normalized; float num = width * 0.5f; return new RaceGate(position - normalized * num, position + normalized * num, forward); } public static RaceGate FromPlayerPosition(Player player, float width) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) Vector3 position = ((Component)player).transform.position; Vector3 forward = ((Component)player).transform.forward; return FromPositionAndForward(position, forward, width); } public static RaceGate FromPeerPosition(ZNetPeer peer, float width) { //IL_001a: 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_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (peer != null && ValheimUtil.TryGetPeerWorldPosition(peer, out var position)) { return FromPositionAndForward(position, Vector3.forward, width); } return FromPositionAndForward(Vector3.zero, Vector3.forward, width); } } [Serializable] public sealed class Checkpoint { public int Index; public RaceGate Gate = new RaceGate(); public Checkpoint() { } public Checkpoint(int index, RaceGate gate) { Index = index; Gate = gate; } } [Serializable] public sealed class TrackConfig { public string Name = string.Empty; public RaceGate StartGate = new RaceGate(); public RaceGate FinishGate = new RaceGate(); public List<Checkpoint> Checkpoints = new List<Checkpoint>(); public bool HasStart { get { if (StartGate != null) { return StartGate.IsConfigured(); } return false; } } public bool HasFinish { get { if (FinishGate != null) { return FinishGate.IsConfigured(); } return false; } } public int CheckpointCount => Checkpoints?.Count ?? 0; } public static class CommandContext { [ThreadStatic] private static ZRpc _currentRpc; private static readonly List<string> PendingReplies = new List<string>(); public static long SenderPeerId { get; set; } public static bool ExecutingFromRemote { get; set; } public static bool HasOrigin { get; private set; } public static Vector3 OriginPosition { get; private set; } public static Vector3 OriginForward { get; private set; } public static ZRpc CurrentRpc { get { return _currentRpc; } set { _currentRpc = value; } } public static void SetOrigin(Vector3 position, Vector3 forward) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) HasOrigin = true; OriginPosition = position; OriginForward = forward; } public static bool TryGetOrigin(out Vector3 position, out Vector3 forward) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) position = OriginPosition; forward = OriginForward; return HasOrigin; } public static void ClearOrigin() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) HasOrigin = false; OriginPosition = Vector3.zero; OriginForward = Vector3.forward; } public static void Clear() { _currentRpc = null; SenderPeerId = 0L; ClearOrigin(); PendingReplies.Clear(); } public static void QueueReply(string message) { if (!string.IsNullOrEmpty(message)) { PendingReplies.Add(message); } } public static IReadOnlyList<string> TakePendingReplies() { string[] result = PendingReplies.ToArray(); PendingReplies.Clear(); return result; } } public static class Commands { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__HandleRunCommand; public static Action<ConsoleEventArgs> <1>__HandleStart; public static Action<ConsoleEventArgs> <2>__HandleDebug; public static Action<ConsoleEventArgs> <3>__HandleReset; public static Action<ConsoleEventArgs> <4>__HandleExport; public static Action<ConsoleEventArgs> <5>__HandleReload; public static Action<ConsoleEventArgs> <6>__HandleRegister; public static Action<ConsoleEventArgs> <7>__HandleRemove; public static Action<ConsoleEventArgs> <8>__HandleClear; public static Action<ConsoleEventArgs> <9>__HandleAutoDetect; public static Action<ConsoleEventArgs> <10>__HandleTrackName; public static Action<ConsoleEventArgs> <11>__HandleGearSet; public static Action<ConsoleEventArgs> <12>__HandleEdit; public static Action<ConsoleEventArgs> <13>__HandleSetPoint; } private static bool _registered; public static void Register() { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown if (!_registered) { _registered = true; object obj = <>O.<0>__HandleRunCommand; if (obj == null) { ConsoleEvent val = HandleRunCommand; <>O.<0>__HandleRunCommand = val; obj = (object)val; } new ConsoleCommand("run", "RunningMan marathon commands", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); RunningManPlugin.Log.LogInfo((object)"Registered /run chat command."); } } public static void ExecuteFromRemote(long senderPeerId, string argLine) { ZRpc rpc = null; if ((Object)(object)ZNet.instance != (Object)null) { rpc = ZNet.instance.GetPeer(senderPeerId)?.m_rpc; } CommandContext.ExecutingFromRemote = true; CommandContext.SenderPeerId = senderPeerId; try { Execute(argLine, rpc); } finally { CommandContext.ExecutingFromRemote = false; } } public static void Execute(string argLine, ZRpc rpc) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown CommandContext.CurrentRpc = rpc; if (rpc != null && (Object)(object)ZNet.instance != (Object)null) { ZNetPeer peer = ZNet.instance.GetPeer(rpc); if (peer != null) { CommandContext.SenderPeerId = peer.m_uid; } } HandleRunCommand(new ConsoleEventArgs("run" + (string.IsNullOrWhiteSpace(argLine) ? string.Empty : (" " + argLine.Trim())), (Terminal)(object)Console.instance)); } private static void HandleRunCommand(ConsoleEventArgs args) { try { string[] array = NormalizeArgs(args.Args); if (TryHandleClientLocalCommand(array, args)) { return; } if (!ValheimUtil.IsServerAuthority() && !CommandContext.ExecutingFromRemote && (Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer()) { ValheimUtil.RunCommand(string.Join(" ", array)); return; } if (array.Length == 0) { PrintHelp(args.Context); return; } string text = array[0].ToLowerInvariant(); switch (text) { case "status": HandleStatus(args); break; case "join": HandleJoin(args); break; case "leave": HandleLeave(args); break; case "gear": case "gearcheck": HandleGearCheck(args); break; case "loadout": ValheimUtil.Reply(args.Context, GearValidator.FormatRequiredLoadout()); break; case "admincheck": HandleAdminCheck(args); break; case "last": HandleLastRun(args); break; case "pb": HandlePersonalBest(args); break; case "open": RequireAdmin(args, delegate(ConsoleEventArgs a) { RaceManager.Instance.OpenRegistration(); ValheimUtil.Reply(a.Context, "Registration opened."); }); break; case "close": RequireAdmin(args, delegate(ConsoleEventArgs a) { RaceManager.Instance.CloseRegistration(); ValheimUtil.Reply(a.Context, "Registration closed."); }); break; case "start": RequireAdmin(args, HandleStart); break; case "cancel": RequireAdmin(args, delegate(ConsoleEventArgs a) { RaceManager.Instance.CancelEvent(); ValheimUtil.Reply(a.Context, "Event cancelled."); }); break; case "debug": RequireAdmin(args, HandleDebug); break; case "records": case "leaderboard": case "worldrecords": HandleWorldRecords(args); break; case "reset": RequireAdmin(args, HandleReset); break; case "export": RequireAdmin(args, HandleExport); break; case "reload": RequireAdmin(args, HandleReload); break; case "sync": HandleSync(args); break; case "register": RequireAdmin(args, HandleRegister); break; case "remove": RequireAdmin(args, HandleRemove); break; case "clear": RequireAdmin(args, HandleClear); break; case "autodetect": RequireAdmin(args, HandleAutoDetect); break; case "trackname": RequireAdmin(args, HandleTrackName); break; case "gearset": RequireAdmin(args, HandleGearSet); break; case "edit": RequireAdmin(args, HandleEdit); break; case "setpoint": RequireAdmin(args, HandleSetPoint); break; case "wrboard": case "bulletin": RequireAdmin(args, delegate(ConsoleEventArgs a) { HandleBulletinBoard(a, RaceBulletinKind.Records); }); break; case "rulesboard": case "rulesign": RequireAdmin(args, delegate(ConsoleEventArgs a) { HandleBulletinBoard(a, RaceBulletinKind.Rules); }); break; default: ValheimUtil.Reply(args.Context, "Unknown /run subcommand: " + text); PrintHelp(args.Context); break; } } catch (Exception ex) { ValheimUtil.Reply(args.Context, "RunningMan error: " + ex.Message); RunningManPlugin.Log.LogError((object)ex); } } private static string[] NormalizeArgs(string[] args) { if (args == null || args.Length == 0) { return Array.Empty<string>(); } if (args[0].Equals("run", StringComparison.OrdinalIgnoreCase)) { return args.Skip(1).ToArray(); } return args; } private static void HandleStatus(ConsoleEventArgs args) { if (!ValheimUtil.TryResolveCommandParticipant(out var participant)) { ValheimUtil.ReplyHud("Could not resolve player for status."); } else { ValheimUtil.ReplyHud(RaceManager.Instance.FormatStatus(participant.PlayerId, DateTime.UtcNow)); } } private static void HandleJoin(ConsoleEventArgs args) { if (!ValheimUtil.TryResolveCommandParticipant(out var participant)) { ValheimUtil.ReplyHud("Could not resolve player."); } else if (RaceManager.Instance.JoinPlayer(participant.PlayerId, participant.PlayerName)) { ValheimUtil.ReplyHud(participant.PlayerName + " joined the race."); } else { ValheimUtil.ReplyHud("Could not join — registration may be closed or you are already registered."); } } private static void HandleLeave(ConsoleEventArgs args) { if (!ValheimUtil.TryResolveCommandParticipant(out var participant)) { ValheimUtil.ReplyHud("Could not resolve player."); } else if (RaceManager.Instance.LeavePlayer(participant.PlayerId)) { ValheimUtil.ReplyHud(participant.PlayerName + " left the race."); } else { ValheimUtil.ReplyHud("You are not registered."); } } private static void HandleStart(ConsoleEventArgs args) { if (RaceManager.Instance.StartCountdown(out var failureReason)) { ValheimUtil.Reply(args.Context, $"Countdown started ({ModConfig.CountdownSeconds.Value}s)."); } else { ValheimUtil.Reply(args.Context, string.IsNullOrEmpty(failureReason) ? "Could not start countdown. Use /run open, have runners /run join, then /run start." : failureReason); } } private static void HandleDebug(ConsoleEventArgs args) { bool? flag = ResolveDebugToggle(NormalizeArgs(args.Args)); if (!flag.HasValue) { ValheimUtil.Reply(args.Context, "Usage: /run debug [on|off]"); return; } RaceManager.Instance.SetDebugMode(flag.Value); ValheimUtil.Reply(args.Context, "Debug markers: " + (flag.Value ? "ON" : "OFF")); } private static bool? ResolveDebugToggle(string[] subArgs) { if (subArgs.Length >= 2) { switch (subArgs[1].ToLowerInvariant()) { case "on": case "true": case "1": return true; case "off": case "false": case "0": return false; } } return !RaceNetSync.IsDebugModeActive(); } private static bool TryHandleClientLocalCommand(string[] subArgs, ConsoleEventArgs args) { if (subArgs.Length == 0 || (Object)(object)Player.m_localPlayer == (Object)null || CommandContext.ExecutingFromRemote) { return false; } if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return false; } switch (subArgs[0].ToLowerInvariant()) { case "gearcheck": case "gear": HandleGearCheck(args, Player.m_localPlayer); return true; case "edit": HandleEdit(args); return true; case "loadout": ValheimUtil.Reply(args.Context, GearValidator.FormatRequiredLoadout()); return true; default: return false; } } private static void HandleGearCheck(ConsoleEventArgs args, Player player = null) { if (player == null) { player = ValheimUtil.ResolvePlayerForGearCheck(); } if ((Object)(object)player == (Object)null) { ValheimUtil.Reply(args.Context, "Could not resolve player."); return; } GearValidator.GearCheckResult gearCheckResult = RaceManager.Instance.CheckPlayerGear(player); string title = (gearCheckResult.IsValid ? "Gear check passed" : "Gear check failed"); string body = (gearCheckResult.IsValid ? "Gear check passed." : ("- " + string.Join("\n- ", gearCheckResult.Issues))); RaceGui.ShowInfoPanel(title, body, gearCheckResult.IsValid ? 5f : 14f); ValheimUtil.Reply(args.Context, gearCheckResult.Format(multiline: false)); } private static void HandleAdminCheck(ConsoleEventArgs args) { if (ValheimUtil.IsServerAuthority()) { ZRpc currentRpc = CommandContext.CurrentRpc; ZNetPeer val = ((currentRpc != null) ? ZNet.instance.GetPeer(currentRpc) : null); string message = ValheimUtil.FormatAdminIdentity(val, currentRpc); string text = ((val != null) ? ValheimUtil.GetPlatformUserIdString(val) : null); bool flag = ValheimUtil.IsAdmin(currentRpc); ValheimUtil.Reply(args.Context, $"Server admin check: admin={flag}"); ValheimUtil.Reply(args.Context, message); if (!string.IsNullOrEmpty(text)) { ValheimUtil.Reply(args.Context, "Add this exact line to adminlist.txt on the server if crossplay is enabled: " + text); } ValheimUtil.ReplyHud(flag ? "Admin: true" : "Admin: false"); if (CommandContext.SenderPeerId != 0L) { RaceNetSync.SendAdminStatusToPeer(CommandContext.SenderPeerId, flag); } } else { bool num = ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.LocalPlayerIsAdminOrHost()) || RaceNetSync.ClientIsAdmin; ValheimUtil.Reply(args.Context, $"Client: Valheim admin={(Object)(object)ZNet.instance != (Object)null && ZNet.instance.LocalPlayerIsAdminOrHost()}, synced admin={RaceNetSync.ClientIsAdmin}"); ValheimUtil.ReplyHud(num ? "Admin: true" : "Admin: false"); } } private static void HandlePersonalBest(ConsoleEventArgs args) { if (!ValheimUtil.TryResolveCommandParticipant(out var participant)) { ValheimUtil.Reply(args.Context, "Could not resolve player."); } else { ValheimUtil.Reply(args.Context, Leaderboard.FormatPersonalBest(Leaderboard.GetPersonalBest(participant.PlayerId))); } } private static void HandleLastRun(ConsoleEventArgs args) { if (!ValheimUtil.TryResolveCommandParticipant(out var participant)) { ValheimUtil.Reply(args.Context, "Could not resolve player."); } else { ValheimUtil.Reply(args.Context, Leaderboard.FormatLastRun(Leaderboard.GetLastRun(participant.PlayerId))); } } private static void HandleWorldRecords(ConsoleEventArgs args) { string message = Leaderboard.FormatWorldRecords(JsonStorage.Track, ModConfig.LeaderboardLimit.Value); ValheimUtil.Reply(args.Context, message); ValheimUtil.ReplyHud(message); } private static void HandleTrackName(ConsoleEventArgs args) { string[] array = NormalizeArgs(args.Args); if (array.Length < 2) { ValheimUtil.Reply(args.Context, "Current track: " + TrackIdentity.GetDisplayName(JsonStorage.Track) + ". Usage: /run trackname <name>"); return; } string text = string.Join(" ", array.Skip(1)); RaceManager.Instance.SetTrackName(text); ValheimUtil.Reply(args.Context, "Track name set to: " + text); } private static void HandleGearSet(ConsoleEventArgs args) { string[] array = NormalizeArgs(args.Args); if (array.Length < 2) { ValheimUtil.Reply(args.Context, "Usage: /run gearset reset | /run gearset save <payload> (use F6 Allowed gear)"); ValheimUtil.Reply(args.Context, GearValidator.FormatRequiredLoadout()); return; } switch (array[1].ToLowerInvariant()) { case "reset": case "defaults": JsonStorage.ResetAllowedGearToDefaults(); RaceManager.Instance.BroadcastState(); ValheimUtil.Reply(args.Context, "Allowed gear reset to marathon defaults."); ValheimUtil.Reply(args.Context, GearValidator.FormatRequiredLoadout()); break; case "save": case "set": { if (!TryParseGearSetPayload(string.Join(" ", array.Skip(2)), out var rules)) { ValheimUtil.Reply(args.Context, "Invalid gearset payload. Use F6 Allowed gear → Save."); break; } JsonStorage.SetAllowedGear(rules); RaceManager.Instance.BroadcastState(); ValheimUtil.Reply(args.Context, "Allowed gear saved."); ValheimUtil.Reply(args.Context, GearValidator.FormatRequiredLoadout()); break; } default: ValheimUtil.Reply(args.Context, "Usage: /run gearset reset | /run gearset save <payload>"); break; } } private static bool TryParseGearSetPayload(string payload, out AllowedGearRules rules) { rules = null; if (string.IsNullOrWhiteSpace(payload)) { return false; } string[] array = payload.Split(new char[1] { '§' }, StringSplitOptions.None); if (array.Length < 11) { return false; } if (!int.TryParse(array[8], out var result) || !int.TryParse(array[9], out var result2) || !int.TryParse(array[10], out var _)) { return false; } AllowedGearRules allowedGearRules = JsonStorage.AllowedGear ?? AllowedGearRules.CreateDefaults(); rules = allowedGearRules.Clone(); rules.Helmet = array[0] ?? string.Empty; rules.Chest = array[1] ?? string.Empty; rules.Legs = array[2] ?? string.Empty; rules.Cape = array[3] ?? string.Empty; rules.AllowedHandItems = array[4] ?? string.Empty; if (!string.IsNullOrWhiteSpace(array[5])) { rules.AntiStingPrefab = array[5].Trim(); } if (!string.IsNullOrWhiteSpace(array[6])) { rules.RatatoskPrefab = array[6].Trim(); } rules.RequiredAntiSting = Math.Max(0, result); rules.RequiredRatatosk = Math.Max(0, result2); if (array.Length >= 17 && int.TryParse(array[14], out var result4) && int.TryParse(array[15], out var result5) && int.TryParse(array[16], out var result6)) { if (!string.IsNullOrWhiteSpace(array[11])) { rules.SaladPrefab = array[11].Trim(); } if (!string.IsNullOrWhiteSpace(array[12])) { rules.BloodPuddingPrefab = array[12].Trim(); } if (!string.IsNullOrWhiteSpace(array[13])) { rules.MushroomOmelettePrefab = array[13].Trim(); } rules.RequiredSalad = Math.Max(0, result4); rules.RequiredBloodPudding = Math.Max(0, result5); rules.RequiredMushroomOmelette = Math.Max(0, result6); } return true; } private static void HandleEdit(ConsoleEventArgs args) { string[] array = NormalizeArgs(args.Args); bool flag; if (array.Length >= 2) { switch (array[1].ToLowerInvariant()) { case "on": case "true": case "1": flag = true; break; case "off": case "false": case "0": flag = false; break; default: ValheimUtil.Reply(args.Context, "Usage: /run edit [on|off]"); return; } } else { flag = !RaceGateEditor.EditMode; } RaceGateEditor.SetEditMode(flag); ValheimUtil.Reply(args.Context, flag ? "Gate edit ON. Equip Hammer, look at endpoint dots, Use (E) to pick up / place." : "Gate edit OFF."); } private static void HandleSetPoint(ConsoleEventArgs args) { //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) string[] array = NormalizeArgs(args.Args); if (array.Length < 3) { ValheimUtil.Reply(args.Context, "Usage: /run setpoint start|finish a|b OR /run setpoint checkpoint <index> a|b"); return; } if (!TryResolveSearchOrigin(ResolvePlayer(args), out var origin)) { ValheimUtil.Reply(args.Context, "Could not resolve position for setpoint."); return; } string text = array[1].ToLowerInvariant(); int result = 0; RaceManager.GateKind gateKind; string text2; switch (text) { case "start": gateKind = RaceManager.GateKind.Start; text2 = array[2]; break; case "finish": gateKind = RaceManager.GateKind.Finish; text2 = array[2]; break; case "checkpoint": case "cp": if (array.Length < 4 || !int.TryParse(array[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) { ValheimUtil.Reply(args.Context, "Usage: /run setpoint checkpoint <index> a|b"); return; } gateKind = RaceManager.GateKind.Checkpoint; text2 = array[3]; break; default: ValheimUtil.Reply(args.Context, "Usage: /run setpoint start|finish a|b OR /run setpoint checkpoint <index> a|b"); return; } bool flag; switch (text2.ToLowerInvariant()) { case "a": case "1": case "left": flag = true; break; case "b": case "2": case "right": flag = false; break; default: ValheimUtil.Reply(args.Context, "Endpoint must be a or b."); return; } Vector3? preferredForward = null; if (CommandContext.TryGetOrigin(out var _, out var forward)) { preferredForward = forward; } if (RaceManager.Instance.SetGateEndpoint(gateKind, result, flag, origin, preferredForward)) { string text3 = ((gateKind == RaceManager.GateKind.Checkpoint) ? $"CP{result}" : gateKind.ToString().ToUpperInvariant()); ValheimUtil.Reply(args.Context, "Moved " + text3 + " point " + (flag ? "A" : "B") + "."); } else { ValheimUtil.Reply(args.Context, "Could not move that endpoint (gate missing?)."); } } private static void HandleReset(ConsoleEventArgs args) { string[] array = NormalizeArgs(args.Args); if (array.Length < 2) { ValheimUtil.Reply(args.Context, "Usage: /run reset <player>"); return; } Player val = ValheimUtil.FindPlayerByName(array[1]); if ((Object)(object)val == (Object)null) { ValheimUtil.Reply(args.Context, "Player not found: " + array[1]); } else if (RaceManager.Instance.ResetActiveRun(val.GetPlayerID())) { ValheimUtil.Reply(args.Context, "Reset active run for " + val.GetPlayerName() + "."); } else { ValheimUtil.Reply(args.Context, val.GetPlayerName() + " has no active run."); } } private static void HandleExport(ConsoleEventArgs args) { JsonStorage.ExportAll(); ValheimUtil.Reply(args.Context, "Exported race data to " + JsonStorage.ExportFilePath); } private static void HandleSync(ConsoleEventArgs args) { if (!ValheimUtil.IsServerAuthority()) { ValheimUtil.Reply(args.Context, "State sync must run on the server."); return; } RaceStateSnapshot snapshot = RaceManager.Instance.BuildClientSnapshot(DateTime.UtcNow); long senderPeerId = CommandContext.SenderPeerId; if (senderPeerId != 0L) { RaceNetSync.SendStateToPeer(senderPeerId, snapshot); RaceNetSync.SendTrackToPeer(senderPeerId, JsonStorage.Track); } else { RaceManager.Instance.BroadcastState(); ValheimUtil.Reply(args.Context, "RunningMan state refreshed."); } } private static void HandleReload(ConsoleEventArgs args) { ModConfig.ConfigFile.Reload(); ModConfig.Initialize(ModConfig.ConfigFile); JsonStorage.Initialize(Paths.ConfigPath); RaceManager.Instance.SyncTrack(); RaceNetSync.SendBulletins(); ValheimUtil.Reply(args.Context, "RunningMan configuration and data reloaded."); } private static void HandleBulletinBoard(ConsoleEventArgs args, RaceBulletinKind kind) { //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0237: Unknown result type (might be due to invalid IL or missing references) //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_026a: Unknown result type (might be due to invalid IL or missing references) string text = ((kind == RaceBulletinKind.Rules) ? "RULES" : "WR"); List<Vector3Data> list = ((kind == RaceBulletinKind.Rules) ? JsonStorage.RulesBoards : JsonStorage.WrBoards); string[] array = NormalizeArgs(args.Args); string text2 = ((array.Length >= 2) ? array[1].ToLowerInvariant() : "add"); string message = ((kind == RaceBulletinKind.Rules) ? "Usage: /run rulesboard [add|remove|list|clear]" : "Usage: /run wrboard [add|remove|list|clear]"); switch (text2) { case "list": ValheimUtil.Reply(args.Context, (list.Count == 0) ? ("No " + text + " bulletin boards marked.") : $"{list.Count} {text} bulletin board(s) marked."); break; case "clear": if (kind == RaceBulletinKind.Rules) { JsonStorage.SetRulesBoards(new List<Vector3Data>()); } else { JsonStorage.SetWrBoards(new List<Vector3Data>()); } RaceNetSync.SendBulletins(); ValheimUtil.Reply(args.Context, "All " + text + " bulletin boards cleared."); break; case "remove": { if (!TryResolveBoardPosition(args, out var position2)) { ValheimUtil.Reply(args.Context, "Look at a marked " + text + " Sign (or stand near one) and run the remove command."); break; } int num = -1; float num2 = float.MaxValue; for (int i = 0; i < list.Count; i++) { float num3 = Vector3.Distance(position2, ValheimUtil.FromData(list[i])); if (num3 < num2) { num2 = num3; num = i; } } if (num < 0 || num2 > 8f) { ValheimUtil.Reply(args.Context, "No " + text + " board within 8m to remove."); break; } list.RemoveAt(num); if (kind == RaceBulletinKind.Rules) { JsonStorage.SaveRulesBoards(); } else { JsonStorage.SaveWrBoards(); } RaceNetSync.SendBulletins(); ValheimUtil.Reply(args.Context, $"Removed {text} board ({list.Count} remaining)."); break; } default: { if (text2 != "add" && array.Length >= 2) { ValheimUtil.Reply(args.Context, message); break; } if (!TryResolveBoardPosition(args, out var position)) { ValheimUtil.Reply(args.Context, "Look at a Hammer Sign and run /run " + ((kind == RaceBulletinKind.Rules) ? "rulesboard" : "wrboard") + "."); break; } if (RaceWrBoards.TryFindBoardIndex(RaceBulletinKind.Records, position, 1.5f, out var index) || RaceWrBoards.TryFindBoardIndex(RaceBulletinKind.Rules, position, 1.5f, out index)) { ValheimUtil.Reply(args.Context, "That Sign is already marked as a bulletin."); break; } list.Add(ValheimUtil.ToData(position)); if (kind == RaceBulletinKind.Rules) { JsonStorage.SaveRulesBoards(); } else { JsonStorage.SaveWrBoards(); } RaceNetSync.SendBulletins(); ValheimUtil.Reply(args.Context, $"{text} Sign marked ({list.Count} total). Walk up to it to read."); break; } } } private static bool TryResolveBoardPosition(ConsoleEventArgs args, out Vector3 position) { //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_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (CommandContext.TryGetOrigin(out position, out var _)) { return true; } Player val = ResolvePlayer(args); if ((Object)(object)val != (Object)null) { position = ((Component)val).transform.position; return true; } position = Vector3.zero; return false; } private static void HandleRegister(ConsoleEventArgs args) { string[] array = NormalizeArgs(args.Args); if (array.Length < 2) { ValheimUtil.Reply(args.Context, "Usage: /run register start|finish|checkpoint [width]"); return; } Player player = ResolvePlayer(args); float num = ParseRegistrationWidth(array, 2); RaceGate raceGate = ValheimUtil.CreateRegistrationGate(player, ValheimUtil.ResolveCommandPeer(), num); if (raceGate == null) { ValheimUtil.Reply(args.Context, "Could not resolve player position for registration."); return; } switch (array[1].ToLowerInvariant()) { case "start": RaceManager.Instance.RegisterStartGate(raceGate); ValheimUtil.Reply(args.Context, $"Start gate registered (width {num:0.#}m). Enable debug to preview markers."); break; case "finish": RaceManager.Instance.RegisterFinishGate(raceGate); ValheimUtil.Reply(args.Context, $"Finish gate registered (width {num:0.#}m). Enable debug to preview markers."); break; case "checkpoint": { int num2 = RaceManager.Instance.RegisterCheckpoint(raceGate); ValheimUtil.Reply(args.Context, $"Checkpoint {num2} registered (width {num:0.#}m). Later checkpoints were renumbered. Enable debug to preview markers."); break; } default: ValheimUtil.Reply(args.Context, "Usage: /run register start|finish|checkpoint [width]"); break; } } private static void HandleRemove(ConsoleEventArgs args) { //IL_0126: Unknown result type (might be due to invalid IL or missing references) string[] array = NormalizeArgs(args.Args); if (array.Length < 2) { ValheimUtil.Reply(args.Context, "Usage: /run remove start|finish|checkpoint <index|last>"); return; } switch (array[1].ToLowerInvariant()) { case "start": ValheimUtil.Reply(args.Context, RaceManager.Instance.ClearStartGate() ? "Start gate removed." : "No start gate is registered."); break; case "finish": ValheimUtil.Reply(args.Context, RaceManager.Instance.ClearFinishGate() ? "Finish gate removed." : "No finish gate is registered."); break; case "checkpoint": { int result; if (array.Length < 3) { ValheimUtil.Reply(args.Context, "Usage: /run remove checkpoint <index|last|nearest>"); } else if (array[2].Equals("last", StringComparison.OrdinalIgnoreCase)) { ValheimUtil.Reply(args.Context, RaceManager.Instance.RemoveLastCheckpoint() ? "Last checkpoint removed." : "No checkpoints are registered."); } else if (array[2].Equals("nearest", StringComparison.OrdinalIgnoreCase)) { if (!TryResolveSearchOrigin(ResolvePlayer(args), out var origin)) { ValheimUtil.Reply(args.Context, "Could not resolve position for nearest checkpoint removal."); } else { ValheimUtil.Reply(args.Context, RaceManager.Instance.RemoveNearestCheckpoint(origin) ? "Nearest checkpoint removed." : "No checkpoints are registered."); } } else if (!int.TryParse(array[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) { ValheimUtil.Reply(args.Context, "Usage: /run remove checkpoint <index|last|nearest>"); } else { ValheimUtil.Reply(args.Context, RaceManager.Instance.RemoveCheckpoint(result) ? $"Checkpoint {result} removed." : $"Checkpoint {result} was not found."); } break; } default: ValheimUtil.Reply(args.Context, "Usage: /run remove start|finish|checkpoint <index|last|nearest>"); break; } } private static void HandleClear(ConsoleEventArgs args) { string[] array = NormalizeArgs(args.Args); if (array.Length < 2) { ValheimUtil.Reply(args.Context, "Usage: /run clear checkpoints|start|finish|track|records [all]"); return; } switch (array[1].ToLowerInvariant()) { case "records": case "worldrecords": if (array.Length >= 3 && array[2].Equals("all", StringComparison.OrdinalIgnoreCase)) { int num3 = RaceManager.Instance.ClearAllWorldRecords(); ValheimUtil.Reply(args.Context, $"Cleared {num3} world record(s) for all tracks."); } else { int num4 = RaceManager.Instance.ClearTrackWorldRecords(); ValheimUtil.Reply(args.Context, (num4 > 0) ? $"Cleared {num4} world record(s) for {TrackIdentity.GetDisplayName(JsonStorage.Track)}." : "No world records for this track."); } break; case "checkpoints": case "checkpoint": { int num2 = RaceManager.Instance.ClearCheckpoints(); ValheimUtil.Reply(args.Context, (num2 > 0) ? $"Removed {num2} checkpoint(s)." : "No checkpoints are registered."); break; } case "start": ValheimUtil.Reply(args.Context, RaceManager.Instance.ClearStartGate() ? "Start gate removed." : "No start gate is registered."); break; case "finish": ValheimUtil.Reply(args.Context, RaceManager.Instance.ClearFinishGate() ? "Finish gate removed." : "No finish gate is registered."); break; case "track": { RaceManager.Instance.ClearStartGate(); RaceManager.Instance.ClearFinishGate(); int num = RaceManager.Instance.ClearCheckpoints(); ValheimUtil.Reply(args.Context, $"Track cleared ({num} checkpoint(s) removed)."); break; } default: ValheimUtil.Reply(args.Context, "Usage: /run clear checkpoints|start|finish|track|records [all]"); break; } } private static float ParseRegistrationWidth(string[] subArgs, int widthArgIndex) { float value = ModConfig.GateRegistrationWidth.Value; if (subArgs.Length <= widthArgIndex) { return value; } if (float.TryParse(subArgs[widthArgIndex], NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return Mathf.Clamp(result, 1f, 50f); } return value; } private static void HandleAutoDetect(ConsoleEventArgs args) { //IL_020b: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) string[] array = NormalizeArgs(args.Args); if (!TryResolveSearchOrigin(ResolvePlayer(args), out var origin)) { ValheimUtil.Reply(args.Context, "Could not resolve player for auto-detect."); return; } float value = ModConfig.AutoDetectSearchRadius.Value; switch ((array.Length > 1) ? array[1].ToLowerInvariant() : "checkpoints") { case "start": { RaceGate raceGate3 = TriggerDetector.DetectGraustenGate(origin, value); if (raceGate3 == null) { ValheimUtil.Reply(args.Context, "No Grausten gate found nearby."); break; } RaceManager.Instance.RegisterStartGate(raceGate3); ValheimUtil.Reply(args.Context, "Start gate auto-detected."); break; } case "finish": { RaceGate raceGate2 = TriggerDetector.DetectGraustenGate(origin, value); if (raceGate2 == null) { ValheimUtil.Reply(args.Context, "No Grausten gate found nearby."); break; } RaceManager.Instance.RegisterFinishGate(raceGate2); ValheimUtil.Reply(args.Context, "Finish gate auto-detected."); break; } case "checkpoints": { List<Checkpoint> list = TriggerDetector.DetectTorchCheckpoints(origin, value, ModConfig.AutoDetectTorchPairMinDistance.Value, ModConfig.AutoDetectTorchPairMaxDistance.Value); if (list.Count == 0) { ValheimUtil.Reply(args.Context, "No torch checkpoint pairs found nearby."); break; } RaceManager.Instance.ReplaceCheckpoints(list); ValheimUtil.Reply(args.Context, $"Auto-detected {list.Count} checkpoint pair(s)."); break; } case "checkpoint": case "cp": case "nearest": case "here": { float value2 = ModConfig.AutoDetectNearestSearchRadius.Value; RaceGate raceGate = TriggerDetector.DetectNearestTorchPair(origin, value2, ModConfig.AutoDetectTorchPairMinDistance.Value, ModConfig.AutoDetectTorchPairMaxDistance.Value); if (raceGate == null) { ValheimUtil.Reply(args.Context, $"No valid Standing Iron Torch pair within {value2:0.#}m. Need two torches {ModConfig.AutoDetectTorchPairMinDistance.Value:0.#}-{ModConfig.AutoDetectTorchPairMaxDistance.Value:0.#}m apart."); break; } int num = RaceManager.Instance.RegisterCheckpoint(raceGate); ValheimUtil.Reply(args.Context, $"Added checkpoint {num} from the two nearest Standing Iron Torches (later CPs renumbered)."); break; } default: ValheimUtil.Reply(args.Context, "Usage: /run autodetect start|finish|checkpoints|checkpoint"); break; } } private static void RequireAdmin(ConsoleEventArgs args, Action<ConsoleEventArgs> action) { if (!ValheimUtil.IsServerAuthority()) { ValheimUtil.Reply(args.Context, "RunningMan admin commands must run on the server."); } else if (!ValheimUtil.IsAdmin(CommandContext.CurrentRpc)) { string text = ValheimUtil.FormatAdminIdentity(ValheimUtil.ResolveCommandPeer(), CommandContext.CurrentRpc); RunningManPlugin.Log.LogWarning((object)("RunningMan admin denied for " + text + ".")); ValheimUtil.Reply(args.Context, "Admin privileges required. If this server uses crossplay, adminlist.txt needs the Platform ID from F2, e.g. Steam_7656119..."); ValheimUtil.Reply(args.Context, "Server sees: " + text); } else { action(args); } } private static bool TryResolveSearchOrigin(Player player, out Vector3 origin) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player != (Object)null) { origin = ((Component)player).transform.position; return true; } if (CommandContext.TryGetOrigin(out origin, out var _)) { return true; } ZNetPeer val = ValheimUtil.ResolveCommandPeer(); if (val != null && ValheimUtil.TryGetPeerWorldPosition(val, out origin)) { return true; } origin = Vector3.zero; return false; } private static Player ResolvePlayer(ConsoleEventArgs args) { return ValheimUtil.ResolvePlayerForGearCheck(); } private static void PrintHelp(Terminal context) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("RunningMan — press F6 for GUI"); stringBuilder.AppendLine("Player:"); stringBuilder.AppendLine(" /run join | leave | status | gearcheck | loadout | admincheck | pb | last"); stringBuilder.AppendLine("Admin:"); stringBuilder.AppendLine(" /run open | close | start | cancel | debug"); stringBuilder.AppendLine(" /run register start|finish|checkpoint [width]"); stringBuilder.AppendLine(" /run remove start|finish|checkpoint <index|last>"); stringBuilder.AppendLine(" /run clear checkpoints|start|finish|track"); stringBuilder.AppendLine(" /run autodetect start|finish|checkpoints|checkpoint"); stringBuilder.AppendLine(" /run edit [on|off] (hammer + Use to move gate endpoints)"); stringBuilder.AppendLine(" /run setpoint start|finish a|b | checkpoint <n> a|b"); stringBuilder.AppendLine(" /run worldrecords | trackname <name> | gearset reset|save"); stringBuilder.AppendLine(" /run wrboard [add|remove|list|clear] (look at a Hammer Sign)"); stringBuilder.AppendLine(" /run rulesboard [add|remove|list|clear] (RULES Sign)"); stringBuilder.AppendLine(" /run clear records | clear records all"); stringBuilder.AppendLine(" /run leaderboard | reset <player> | export | reload"); ValheimUtil.Reply(context, stringBuilder.ToString()); } } public static class ModConfig { public static ConfigEntry<bool> EnableBroadcasts { get; private set; } public static ConfigEntry<bool> EnableHud { get; private set; } public static ConfigEntry<bool> DebugMode { get; private set; } public static ConfigEntry<KeyCode> GuiHotkey { get; private set; } public static ConfigEntry<int> CountdownSeconds { get; private set; } public static ConfigEntry<bool> RequireRegistration { get; private set; } public static ConfigEntry<float> StartTriggerDistance { get; private set; } public static ConfigEntry<float> CheckpointDistance { get; private set; } public static ConfigEntry<float> GateEndPadding { get; private set; } public static ConfigEntry<float> CheckpointVerticalDistance { get; private set; } public static ConfigEntry<float> GateVerticalDistance { get; private set; } public static ConfigEntry<float> FinishTriggerDistance { get; private set; } public static ConfigEntry<bool> EnableLiveStandings { get; private set; } public static ConfigEntry<int> LiveStandingsLimit { get; private set; } public static ConfigEntry<float> UpdateInterval { get; private set; } public static ConfigEntry<float> GateRegistrationWidth { get; private set; } public static ConfigEntry<int> CountdownFontSize { get; private set; } public static ConfigEntry<int> CheckpointHudFontSize { get; private set; } public static ConfigEntry<string> SavePath { get; private set; } public static ConfigEntry<string> ExportFormat { get; private set; } public static ConfigEntry<int> LeaderboardLimit { get; private set; } public static ConfigEntry<int> WorldRecordsLimit { get; private set; } public static ConfigEntry<bool> EnableRaceSounds { get; private set; } public static ConfigEntry<float> RaceSoundVolume { get; private set; } public static ConfigEntry<string> RaceStartSounds { get; private set; } public static ConfigEntry<string> CheckpointSound { get; private set; } public static ConfigEntry<string> FirstPlaceFinishSounds { get; private set; } public static ConfigEntry<bool> UseCustomRaceSounds { get; private set; } public static ConfigEntry<string> CustomSoundsFolder { get; private set; } public static ConfigEntry<string> CustomStartSoundFile { get; private set; } public static ConfigEntry<string> CustomCheckpointSoundFile { get; private set; } public static ConfigEntry<string> CustomFinishSoundFile { get; private set; } public static ConfigEntry<string> CustomCountdownSoundFile { get; private set; } public static ConfigEntry<string> CustomFalseStartSoundFile { get; private set; } public static ConfigEntry<float> StartingAreaOffset { get; private set; } public static ConfigEntry<float> StartingAreaDepth { get; private set; } public static ConfigEntry<float> StartingAreaSidePadding { get; private set; } public static ConfigEntry<bool> RequireStartingArea { get; private set; } public static ConfigEntry<bool> DisqualifyOnFalseStart { get; private set; } public static ConfigEntry<float> AutoDetectTorchPairMinDistance { get; private set; } public static ConfigEntry<float> AutoDetectTorchPairMaxDistance { get; private set; } public static ConfigEntry<float> AutoDetectSearchRadius { get; private set; } public static ConfigEntry<float> AutoDetectNearestSearchRadius { get; private set; } public static ConfigEntry<float> EndpointSnapRadius { get; private set; } public static ConfigEntry<float> EndpointSnapAngle { get; private set; } public static ConfigEntry<KeyCode> RegisterCheckpointHotkey { get; private set; } public static ConfigEntry<KeyCode> AutoDetectNearestCheckpointHotkey { get; private set; } public static ConfigEntry<bool> NormalizeRunSkill { get; private set; } public static ConfigEntry<float> NormalizedRunSkillLevel { get; private set; } public static ConfigEntry<bool> EnableGearCheck { get; private set; } public static ConfigEntry<bool> DisqualifyOnGearViolation { get; private set; } public static ConfigEntry<int> RequiredAntiStingCount { get; private set; } public static ConfigEntry<int> RequiredRatatoskCount { get; private set; } public static ConfigEntry<float> TriggerSweepStepDistance { get; private set; } public static ConfigEntry<string> AllowedHandItems { get; private set; } public static ConfigEntry<bool> MountainBiomeUnlocksFeatherCape { get; private set; } public static ConfigFile ConfigFile { get; private set; } public static void Initialize(ConfigFile config) { ConfigFile = config; EnableBroadcasts = config.Bind<bool>("General", "EnableBroadcasts", true, "Broadcast race events to all players in chat."); EnableHud = config.Bind<bool>("General", "EnableHud", true, "Show live race overlay with time and place."); EnableLiveStandings = config.Bind<bool>("General", "EnableLiveStandings", true, "Show a live ranking panel during races."); EnableRaceSounds = config.Bind<bool>("Audio", "EnableRaceSounds", true, "Play local sounds for race start, checkpoints, and 1st-place finishes."); RaceSoundVolume = config.Bind<float>("Audio", "RaceSoundVolume", 1f, "Volume multiplier for RunningMan race sounds."); RaceStartSounds = config.Bind<string>("Audio", "RaceStartSounds", "sfx_offering,sfx_demister_start", "Comma-separated Valheim SFX prefab names played when the race starts (GO!)."); CheckpointSound = config.Bind<string>("Audio", "CheckpointSound", "sfx_archery_target_hit", "Valheim SFX prefab name played when you reach a checkpoint."); FirstPlaceFinishSounds = config.Bind<string>("Audio", "FirstPlaceFinishSounds", "sfx_coins_placed,sfx_coins_placed,sfx_boar_love", "Comma-separated Valheim SFX prefab names played when you finish in 1st place (fallback)."); UseCustomRaceSounds = config.Bind<bool>("Audio", "UseCustomRaceSounds", true, "Prefer custom audio files from the plugin Sounds folder when present."); CustomSoundsFolder = config.Bind<string>("Audio", "CustomSoundsFolder", "Sounds", "Subfolder next to RunningMan.dll (…/BepInEx/plugins/RunningMan/Sounds)."); CustomStartSoundFile = config.Bind<string>("Audio", "CustomStartSoundFile", "start.mp3", "Race start sound file name (mp3, wav, or ogg)."); CustomCheckpointSoundFile = config.Bind<string>("Audio", "CustomCheckpointSoundFile", "checkpoint.mp3", "Checkpoint sound file name (mp3, wav, or ogg)."); CustomFinishSoundFile = config.Bind<string>("Audio", "CustomFinishSoundFile", "finish.mp3", "1st-place finish sound file name (mp3, wav, or ogg)."); CustomCountdownSoundFile = config.Bind<string>("Audio", "CustomCountdownSoundFile", "countdown.mp3", "Countdown voice file played when an admin starts the race (e.g. 5-4-3-2-1)."); CustomFalseStartSoundFile = config.Bind<string>("Audio", "CustomFalseStartSoundFile", "false_start.mp3", "Played only for a player who false-starts (stops their countdown audio)."); LiveStandingsLimit = config.Bind<int>("General", "LiveStandingsLimit", 6, "Maximum runners shown in the live ranking panel."); DebugMode = config.Bind<bool>("General", "DebugMode", false, "Show start/finish/checkpoint gate markers in the world (synced from server)."); GuiHotkey = config.Bind<KeyCode>("General", "GuiHotkey", (KeyCode)287, "Hotkey to open the RunningMan panel."); RegisterCheckpointHotkey = config.Bind<KeyCode>("General", "RegisterCheckpointHotkey", (KeyCode)288, "Hotkey to register a checkpoint at your position (admin). None disables."); AutoDetectNearestCheckpointHotkey = config.Bind<KeyCode>("General", "AutoDetectNearestCheckpointHotkey", (KeyCode)289, "Hotkey to auto-detect 1 checkpoint from the two nearest Standing Iron Torches (admin). None disables."); NormalizeRunSkill = config.Bind<bool>("General", "NormalizeRunSkill", true, "Clamp every runner's Run skill to NormalizedRunSkillLevel during a race."); NormalizedRunSkillLevel = config.Bind<float>("General", "NormalizedRunSkillLevel", 50f, "Run skill level used for all participants while a race is active (0-100)."); CountdownSeconds = config.Bind<int>("General", "CountdownSeconds", 5, "Seconds counted down before a registered race starts."); RequireRegistration = config.Bind<bool>("General", "RequireRegistration", true, "Only registered participants can race after the countdown."); StartTriggerDistance = config.Bind<float>("Detection", "StartTriggerDistance", 6f, "Maximum horizontal distance from the start gate line to trigger a start."); RequireStartingArea = config.Bind<bool>("Detection", "RequireStartingArea", true, "Block countdown unless every registered runner is inside the starting grid."); StartingAreaOffset = config.Bind<float>("Detection", "StartingAreaOffset", 1f, "How far before the start line (meters) the starting grid is centered."); StartingAreaDepth = config.Bind<float>("Detection", "StartingAreaDepth", 4f, "Depth of the starting grid along the approach (meters)."); StartingAreaSidePadding = config.Bind<float>("Detection", "StartingAreaSidePadding", 1.5f, "Extra width beyond the start gate endpoints for the starting grid."); DisqualifyOnFalseStart = config.Bind<bool>("Detection", "DisqualifyOnFalseStart", true, "Remove a runner from the event if they cross the start line during countdown."); CheckpointDistance = config.Bind<float>("Detection", "CheckpointDistance", 2.5f, "Maximum horizontal distance from a checkpoint gate line to register passage."); GateEndPadding = config.Bind<float>("Detection", "GateEndPadding", 0f, "How far past gate endpoints (meters) still counts as inside. 0 = must pass between A and B."); CheckpointVerticalDistance = config.Bind<float>("Detection", "CheckpointVerticalDistance", 15f, "Maximum vertical distance above/below a checkpoint gate line to register passage."); GateVerticalDistance = config.Bind<float>("Detection", "GateVerticalDistance", 8f, "Maximum vertical distance above/below start/finish gate lines."); FinishTriggerDistance = config.Bind<float>("Detection", "FinishTriggerDistance", 8f, "Maximum horizontal distance from the finish gate line to trigger completion."); UpdateInterval = config.Bind<float>("Detection", "UpdateInterval", 0.05f, "Seconds between race position checks (lower = better for fast runners)."); TriggerSweepStepDistance = config.Bind<float>("Detection", "TriggerSweepStepDistance", 1.5f, "Sub-step size (meters) when checking movement between ticks so fast runners do not skip gates."); GateRegistrationWidth = config.Bind<float>("Detection", "GateRegistrationWidth", 6f, "Default gate width when registering from player position."); CountdownFontSize = config.Bind<int>("General", "CountdownFontSize", 72, "Font size for the on-screen race countdown."); CheckpointHudFontSize = config.Bind<int>("General", "CheckpointHudFontSize", 11, "Font size for the checkpoint progress line on the race HUD."); SavePath = config.Bind<string>("Storage", "SavePath", "RunningMan/", "Subfolder under BepInEx/config/ for race JSON files."); ExportFormat = config.Bind<string>("Storage", "ExportFormat", "json", "Export format identifier."); LeaderboardLimit = config.Bind<int>("Storage", "LeaderboardLimit", 10, "Number of entries shown in /run worldrecords."); WorldRecordsLimit = config.Bind<int>("Storage", "WorldRecordsLimit", 5, "Number of world record entries shown on the HUD and WR bulletin Signs (default 5)."); AutoDetectTorchPairMinDistance = config.Bind<float>("AutoDetect", "TorchPairMinDistance", 3f, "Minimum spacing between paired Standing Iron Torches."); AutoDetectTorchPairMaxDistance = config.Bind<float>("AutoDetect", "TorchPairMaxDistance", 25f, "Maximum spacing between paired Standing Iron Torches."); AutoDetectSearchRadius = config.Bind<float>("AutoDetect", "SearchRadius", 500f, "Search radius for auto-detection commands."); AutoDetectNearestSearchRadius = config.Bind<float>("AutoDetect", "NearestSearchRadius", 40f, "Search radius when auto-detecting a single checkpoint from the two nearest torches."); EndpointSnapRadius = config.Bind<float>("AutoDetect", "EndpointSnapRadius", 14f, "How far endpoint edit mode looks for snap targets (Standing Iron Torches)."); EndpointSnapAngle = config.Bind<float>("AutoDetect", "EndpointSnapAngle", 22f, "Max look angle (degrees) for endpoint snap-to-torch."); EnableGearCheck = config.Bind<bool>("GearCheck", "EnableGearCheck", true, "Validate marathon loadout at start and during the race."); DisqualifyOnGearViolation = config.Bind<bool>("GearCheck", "DisqualifyOnViolation", true, "Remove runners who equip non-approved gear mid-race."); RequiredAntiStingCount = config.Bind<int>("GearCheck", "RequiredAntiStingCount", 1, "Anti-Sting Concoctions (MeadBugRepellent) required in inventory at start."); RequiredRatatoskCount = config.Bind<int>("GearCheck", "RequiredRatatoskCount", 2, "Tonic of Ratatosk (MeadHasty) required in inventory at start."); AllowedHandItems = config.Bind<string>("GearCheck", "AllowedHandItems", "", "Unused (kept for compatibility). Hand items are not restricted during races."); MountainBiomeUnlocksFeatherCape = config.Bind<bool>("GearCheck", "MountainBiomeUnlocksFeatherCape", false, "Unused (kept for config compatibility). Feather Cape is always allowed; Troll Hide is not."); } } public static class GearRules { public const string HelmetTroll = "HelmetTrollLeather"; public const string ChestTroll = "ArmorTrollLeatherChest"; public const string LegsTroll = "ArmorTrollLeatherLegs"; public const string CapeFeather = "CapeFeather"; public const string AntiSting = "MeadBugRepellent"; public const string TonicRatatosk = "MeadHasty"; public const string Salad = "Salad"; public const string BloodPudding = "BloodPudding"; public const string MushroomOmelette = "MushroomOmelette"; } public static class GearValidator { public sealed class GearCheckResult { public bool IsValid; public readonly List<string> Issues = new List<string>(); public string Format(bool multiline = true) { if (IsValid) { return "Gear check passed."; } if (multiline) { return "Gear check failed:\n- " + string.Join("\n- ", Issues); } return string.Join("; ", Issues); } } public static AllowedGearRules GetActiveRules() { if (ValheimUtil.IsServerAuthority()) { return JsonStorage.AllowedGear ?? AllowedGearRules.CreateDefaults(); } return RaceNetSync.ClientAllowedGear ?? AllowedGearRules.CreateDefaults(); } public static GearCheckResult CheckStartGear(Player player) { GearCheckResult gearCheckResult = new GearCheckResult { IsValid = true }; if ((Object)(object)player == (Object)null || !ModConfig.EnableGearCheck.Value) { return gearCheckResult; } AllowedGearRules activeRules = GetActiveRules(); ValidateArmor(player, activeRules, gearCheckResult); ValidateConsumables(player, activeRules, gearCheckResult, atStart: true); gearCheckResult.IsValid = gearCheckResult.Issues.Count == 0; return gearCheckResult; } public static GearCheckResult CheckRuntimeGear(Player player, bool ignored = true) { GearCheckResult gearCheckResult = new GearCheckResult { IsValid = true }; if ((Object)(object)player == (Object)null || !ModConfig.EnableGearCheck.Value) { return gearCheckResult; } AllowedGearRules activeRules = GetActiveRules(); ValidateArmor(player, activeRules, gearCheckResult); gearCheckResult.IsValid = gearCheckResult.Issues.Count == 0; return gearCheckResult; } private static void ValidateArmor(Player player, AllowedGearRules rules, GearCheckResult result) { RequireSlot(((Humanoid)player).m_helmetItem, rules.Helmet, "helmet", result); RequireSlot(((Humanoid)player).m_chestItem, rules.Chest, "chest", result); RequireSlot(((Humanoid)player).m_legItem, rules.Legs, "legs", result); ValidateCape(((Humanoid)player).m_shoulderItem, rules.Cape, result); } private static void ValidateCape(ItemData cape, string allowedCape, GearCheckResult result) { if (cape == null) { return; } string prefabName = GetPrefabName(cape); if (!string.IsNullOrEmpty(prefabName)) { if (string.IsNullOrWhiteSpace(allowedCape)) { result.Issues.Add("Disallowed cape: " + GetDisplayName(cape) + " (no cape allowed)."); } else if (!string.Equals(prefabName, allowedCape.Trim(), StringComparison.OrdinalIgnoreCase)) { result.Issues.Add("Disallowed cape: " + GetDisplayName(cape) + " (allowed: " + allowedCape.Trim() + " only, or no cape)."); } } } private static void RequireSlot(ItemData item, string requiredPrefab, string slot, GearCheckResult result) { if (string.IsNullOrWhiteSpace(requiredPrefab)) { if (item != null) { result.Issues.Add("Slot " + slot + " must be empty (have " + GetDisplayName(item) + ")."); } } else if (item == null) { result.Issues.Add("Missing required " + slot + ": " + requiredPrefab.Trim() + "."); } else if (!string.Equals(GetPrefabName(item), requiredPrefab.Trim(), StringComparison.OrdinalIgnoreCase)) { result.Issues.Add("Wrong " + slot + ": " + GetDisplayName(item) + " (required " + requiredPrefab.Trim() + ")."); } } private static void ValidateConsumables(Player player, AllowedGearRules rules, GearCheckResult result, bool atStart) { if (atStart) { Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory == null) { result.Issues.Add("Could not read inventory."); return; } RequireConsumableCount(inventory, rules.AntiStingPrefab, rules.RequiredAntiSting, "Anti-Sting", result); RequireConsumableCount(inventory, rules.RatatoskPrefab, rules.RequiredRatatosk, "Tonic of Ratatosk", result); RequireConsumableCount(inventory, rules.SaladPrefab, rules.RequiredSalad, "Salad", result); RequireConsumableCount(inventory, rules.BloodPuddingPrefab, rules.RequiredBloodPudding, "Blood Pudding", result); RequireConsumableCount(inventory, rules.MushroomOmelettePrefab, rules.RequiredMushroomOmelette, "Mushroom Omelette", result); } } private static void RequireConsumableCount(Inventory inventory, string prefab, int required, string label, GearCheckResult result) { if (required > 0 && !string.IsNullOrWhiteSpace(prefab)) { int num = CountPrefabInInventory(inventory, prefab.Trim()); if (num < required) { result.Issues.Add($"Need {required} {label} ({prefab.Trim()}) (have {num})."); } } } private static int CountPrefabInInventory(Inventory inventory, string prefabName) { if (inventory == null || string.IsNullOrEmpty(prefabName)) { return 0; } if ((Object)(object)ObjectDB.instance != (Object)null) { GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(prefabName); string text = ((!((Object)(object)itemPrefab != (Object)null)) ? null : itemPrefab.GetComponent<ItemDrop>()?.m_itemData?.m_shared?.m_name); if (!string.IsNullOrEmpty(text)) { return inventory.CountItems(text, -1, true); } } int num = 0; foreach (ItemData allItem in inventory.GetAllItems()) { if (allItem != null && string.Equals(GetPrefabName(allItem), prefabName, StringComparison.OrdinalIgnoreCase)) { num += Math.Max(1, allItem.m_stack); } } return num; } public static string GetPrefabName(ItemData item) { if (item == null) { return string.Empty; } if ((Object)(object)item.m_dropPrefab != (Object)null) { return ((Object)item.m_dropPrefab).name; } return item.m_shared?.m_name ?? string.Empty; } private static string GetDisplayName(ItemData item) { string prefabName = GetPrefabName(item); if (!string.IsNullOrEmpty(prefabName)) { return prefabName; } return item?.m_shared?.m_name ?? "unknown item"; } public static bool IsConsumableItem(ItemData item) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 if (item?.m_shared == null) { return false; } if ((int)item.m_shared.m_itemType == 2) { return true; } if (item.m_shared.m_food > 0.01f) { return true; } return (Object)(object)item.m_shared.m_consumeStatusEffect != (Object)null; } public static bool IsAllowedRaceConsumable(ItemData item) { if (!IsConsumableItem(item)) { return true; } AllowedGearRules activeRules = GetActiveRules(); return IsAllowedRaceConsumablePrefab(GetPrefabName(item), activeRules); } public static bool IsAllowedRaceConsumablePrefab(string prefabName, AllowedGearRules rules = null) { if (string.IsNullOrWhiteSpace(prefabName)) { return false; } if (rules == null) { rules = GetActiveRules(); } if (!string.Equals(prefabName, rules.AntiStingPrefab, StringComparison.OrdinalIgnoreCase) && !string.Equals(prefabName, rules.RatatoskPrefab, StringComparison.OrdinalIgnoreCase) && !string.Equals(prefabName, rules.SaladPrefab, StringComparison.OrdinalIgnoreCase) && !string.Equals(prefabName, rules.BloodPuddingPrefab, StringComparison.OrdinalIgnoreCase)) { return string.Equals(prefabName, rules.MushroomOmelettePrefab, StringComparison.OrdinalIgnoreCase); } return true; } public static bool IsFoodItem(ItemData item) { if (item?.m_shared != null) { return item.m_shared.m_food > 0.01f; } return false; } public static string FormatRequiredLoadout() { return GetActiveRules().Format(); } public static AllowedGearRules CaptureFromPlayer(Player player) { AllowedGearRules allowedGearRules = GetActiveRules().Clone(); if ((Object)(object)player == (Object)null) { return allowedGearRules; } allowedGearRules.Helmet = GetPrefabName(((Humanoid)player).m_helmetItem); allowedGearRules.Chest = GetPrefabName(((Humanoid)player).m_chestItem); allowedGearRules.Legs = GetPrefabName(((Humanoid)player).m_legItem); allowedGearRules.Cape = GetPrefabName(((Humanoid)player).m_shoulderItem); List<string> list = new List<string>(); AddUniqueHandPrefab(list, GetPrefabName(((Humanoid)player).m_rightItem)); AddUniqueHandPrefab(list, GetPrefabName(((Humanoid)player).m_leftItem)); allowedGearRules.AllowedHandItems = string.Join(",", list); Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory != null) { if (!string.IsNullOrWhiteSpace(allowedGearRules.AntiStingPrefab)) { allowedGearRules.RequiredAntiSting = CountPrefabInInventory(inventory, allowedGearRules.AntiStingPrefab); } if (!string.IsNullOrWhiteSpace(allowedGearRules.RatatoskPrefab)) { allowedGearRules.RequiredRatatosk = CountPrefabInInventory(inventory, allowedGearRules.RatatoskPrefab); } if (!string.IsNullOrWhiteSpace(allowedGearRules.SaladPrefab)) { allowedGearRules.RequiredSalad = CountPrefabInInventory(inventory, allowedGearRules.SaladPrefab); } if (!string.IsNullOrWhiteSpace(allowedGearRules.BloodPuddingPrefab)) { allowedGearRules.RequiredBloodPudding = CountPrefabInInventory(inventory, allowedGearRules.BloodPuddingPrefab); } if (!string.IsNullOrWhiteSpace(allowedGearRules.MushroomOmelettePrefab)) { allowedGearRules.RequiredMushroomOmelette = CountPrefabInInventory(inventory, allowedGearRules.MushroomOmelettePrefab); } } return allowedGearRules; } private static void AddUniqueHandPrefab(List<string> names, string prefab) { if (string.IsNullOrWhiteSpace(prefab)) { return; } foreach (string name in names) { if (string.Equals(name, prefab, StringComparison.OrdinalIgnoreCase)) { return; } } names.Add(prefab); } } internal enum HudPanelId { Runner, Standings, RaceBanner, Finish, Registered, WorldRecords } internal static class HudPanelLayout { private const float DragTitleHeight = 20f; private const int PanelCount = 6; private static readonly Rect[] Positions = (Rect[])(object)new Rect[6]; private static readonly bool[] HasPosition = new bool[6]; private static readonly bool[] PanelVisible = new bool[6]; private static int _draggingId = -1; private static GUIStyle _dragTitleStyle; private static GUIStyle _closeButtonStyle; private static bool _loaded; public static float TitleBarHeight => 20f; public static HudPanelId Normalize(HudPanelId id) { if (id != HudPanelId.Finish) { return id; } return HudPanelId.RaceBanner; } public static bool ShouldDrawPanel(HudPanelId id, bool layoutMode = false) { EnsureLoaded(); return PanelVisible[(int)Normalize(id)]; } public static bool IsPanelVisible(HudPanelId id) { EnsureLoaded(); return PanelVisible[(int)Normalize(id)]; } public static void SetPanelVisible(HudPanelId id, bool visible) { EnsureLoaded(); int num = (int)Normalize(id); PanelVisible[num] = visible; PlayerPrefs.SetInt(PrefKey(num, "Visible"), visible ? 1 : 0); PlayerPrefs.Save(); } public static void ShowAllPanels() { for (int i = 0; i < 6; i++) { if (i != 3) { SetPanelVisible((HudPanelId)i, visible: true); } } } public static string GetPanelTitle(HudPanelId id) { return Normalize(id) switch { HudPanelId.Runner => "Race stats", HudPanelId.Standings => "Live standings", HudPanelId.RaceBanner => "Race banner", HudPanelId.Registered => "Registration", _ => id.ToString(), }; } public static IEnumerable<HudPanelId> ToggleablePanels() { yield return HudPanelId.Runner; yield return HudPanelId.Standings; yield return HudPanelId.RaceBanner; yield return HudPanelId.Registered; } public static bool Resolve(HudPanelId id, Rect defaultRect, bool draggable, string title, out Rect panelRect) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0026: 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) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: 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_013a: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Expected I4, but got Unknown //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_0206: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) EnsureLoaded(); id = Normalize(id); int num = (int)id; Rect val = (HasPosition[num] ? Positions[num] : defaultRect); ((Rect)(ref val)).width = ((Rect)(ref defaultRect)).width; ((Rect)(ref val)).height = ((Rect)(ref defaultRect)).height + (draggable ? 20f : 0f); if (!draggable) { Positions[num] = val; panelRect = val; return true; } EnsureDragStyle(); Rect val2 = default(Rect); ((Rect)(ref val2))..ctor(((Rect)(ref val)).x, ((Rect)(ref val)).y, ((Rect)(ref val)).width, 20f); float num2 = 22f; GUI.Label(new Rect(((Rect)(ref val2)).x + 8f, ((Rect)(ref val2)).y, ((Rect)(ref val2)).width - num2 - 10f, ((Rect)(ref val2)).height), title + " (drag)", _dragTitleStyle); Rect val3 = default(Rect); ((Rect)(ref val3))..ctor(((Rect)(ref val2)).xMax - num2 - 2f, ((Rect)(ref val2)).y + 1f, num2, ((Rect)(ref val2)).height - 2f); if (GUI.Button(val3, "X", _closeButtonStyle)) { SetPanelVisible(id, visible: false); panelRect = val; return false; } Event current = Event.current; EventType type = current.type; switch ((int)type) { case 0: if (((Rect)(ref val2)).Contains(current.mousePosition) && !((Rect)(ref val3)).Contains(current.mousePosition)) { _draggingId = num; current.Use(); } break; case 3: if (_draggingId == num) { ((Rect)(ref val)).x = ((Rect)(ref val)).x + current.delta.x; ((Rect)(ref val)).y = ((Rect)(ref val)).y + current.delta.y; current.Use(); } break; case 1: if (_draggingId == num) { _draggingId = -1; SavePosition(num, val); current.Use(); } break; } Positions[num] = val; HasPosition[num] = true; panelRect = val; return true; } public static Rect ContentRect(Rect panelRect, bool draggable) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) if (!draggable) { return panelRect; } return new Rect(((Rect)(ref panelRect)).x, ((Rect)(ref panelRect)).y + 20f, ((Rect)(ref panelRect)).width, ((Rect)(ref panelRect)).height - 20f); } public static void ResetAll() { for (int i = 0; i < HasPosition.Length; i++) { HasPosition[i] = false; PanelVisible[i] = true; PlayerPrefs.DeleteKey(PrefKey(i, "X")); PlayerPrefs.DeleteKey(PrefKey(i, "Y")); PlayerPrefs.DeleteKey(PrefKey(i, "Visible")); } PlayerPrefs.Save(); } private static void EnsureLoaded() { //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) if (_loaded) { return; } _loaded = true; for (int i = 0; i < HasPosition.Length; i++) { PanelVisible[i] = !PlayerPrefs.HasKey(PrefKey(i, "Visible")) || PlayerPrefs.GetInt(PrefKey(i, "Visible")) == 1; if (PlayerPrefs.HasKey(PrefKey(i, "X"))) { Positions[i] = new Rect(PlayerPrefs.GetFloat(PrefKey(i, "X")), PlayerPrefs.GetFloat(PrefKey(i, "Y")), 0f, 0f); HasPosition[i] = true; } } } private static void SavePosition(int index, Rect rect) { PlayerPrefs.SetFloat(PrefKey(index, "X"), ((Rect)(ref rect)).x); PlayerPrefs.SetFloat(PrefKey(index, "Y"), ((Rect)(ref rect)).y); PlayerPrefs.Save(); } private static string PrefKey(int index, string suffix) { return $"RunningMan.Hud.{index}.{suffix}"; } private static void EnsureDragStyle() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0034: 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_0043: Expected O, but got Unknown //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown //IL_00af: Expected O, but got Unknown if (_dragTitleStyle == null) { GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = 12, fontStyle = (FontStyle)1, alignment = (TextAnchor)3, clipping = (TextClipping)0, padding = new RectOffset(0, 0, 2, 2) }; val.normal.textColor = new Color(0.75f, 0.85f, 1f); _dragTitleStyle = val; _closeButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 11, fontStyle = (FontStyle)1, alignment = (TextAnchor)4, padding = new RectOffset(0, 0, 0, 0), margin = new RectOffset(0, 0, 0, 0) }; } } } public static class Leaderboard { public static void RecordCompletedRun(RaceRecord record) { RaceDatabase database = JsonStorage.Database; if (string.IsNullOrEmpty(record.TrackId)) { record.TrackId = TrackIdentity.GetId(JsonStorage.Track); } database.Runs.Add(record); string playerId = record.PlayerId; database.LastRuns[playerId] = record; if (!database.PersonalBests.TryGetValue(playerId, out var value) || record.TotalTimeMs < value.TotalTimeMs) { database.PersonalBests[playerId] = record; } JsonStorage.SaveDatabase(); } public static RaceRecord GetPersonalBest(long playerId) { JsonStorage.Database.PersonalBests.TryGetValue(playerId.ToString(), out var value); return value; } public static RaceRecord GetLastRun(long playerId) { JsonStorage.Database.LastRuns.TryGetValue(playerId.ToString(), out var value); return value; } public static IReadOnlyList<RaceRecord> GetWorldRecords(TrackConfig track, int limit) { string trackId = TrackIdentity.GetId(track); if (string.IsNullOrEmpty(trackId)) { return new List<RaceRecord>(); } return (from record in JsonStorage.Database.Runs where string.Equals(record.TrackId, trackId, StringComparison.OrdinalIgnoreCase) orderby record.TotalTimeMs select record).Take(limit).ToList(); } public static int ClearRecordsForTrackId(string trackId) { if (string.IsNullOrEmpty(trackId)) { return 0; } RaceDatabase database = JsonStorage.Database; int result = database.Runs.RemoveAll((RaceRecord record) => string.Equals(record.TrackId, trackId, StringComparison.OrdinalIgnoreCase)); PruneDerivedRecords(database, trackId); JsonStorage.SaveDatabase(); return result; } public static int ClearWorldRecords(TrackConfig track) { return ClearRecordsForTrackId(TrackIdentity.GetId(track)); } public static int ClearAllWorldRecords() { RaceDatabase database = JsonStorage.Database; int count = database.Runs.Count; database.Runs.Clear(); database.PersonalBests.Clear(); database.LastRuns.Clear(); JsonStorage.SaveDatabase(); return count; } public static string FormatWorldRecords(TrackConfig track, int limit) { string displayName = TrackIdentity.GetDisplayName(track); IReadOnlyList<RaceRecord> worldRecords = GetWorldRecords(track, limit); if (worldRecords.Count == 0) { return "No world records for " + displayName + " yet."; } List<string> list = new List<string> { displayName + " — World Records:" }; for (int i = 0; i < worldRecords.Count; i++) { RaceRecord raceRecord = worldRecords[i]; list.Add($"{i + 1}. {raceRecord.Player} - {raceRecord.TotalTime} ({raceRecord.Date})"); } return string.Join("\n", list); } public static List<WorldRecordEntry> BuildWorldRecordEntries(TrackConfig track, int limit) { List<WorldRecordEntry> list = new List<WorldRecordEntry>(); IReadOnlyList<RaceRecord> worldRecords = GetWorldRecords(track, limit); for (int i = 0; i < worldRecords.Count; i++) { RaceRecord raceRecord = worldRecords[i]; list.Add(new WorldRecordEntry { Place = i + 1, PlayerName = raceRecord.Player, Time = raceRecord.TotalTime, TimeMs = raceRecord.TotalTimeMs, Date = raceRecord.Date }); } return list; } public static string FormatPersonalBest(RaceRecord record) { if (record == null) { return "No personal best recorded yet."; } return record.Player + " PB: " + record.TotalTime + " on " + record.Date; } public static string FormatLastRun(RaceRecord record) { if (record == null) { return "No completed runs recorded yet."; } return record.Player + " last run: " + record.TotalTime + " on " + record.Date; } private static void PruneDerivedRecords(RaceDatabase database, string clearedTrackId) { Dictionary<string, List<RaceRecord>> dictionary = (from record in database.Runs group record by record.PlayerId).ToDictionary((IGrouping<string, RaceRecord> group) => group.Key, (IGrouping<string, RaceRecord> group) => group.OrderBy((RaceRecord record) => record.TotalTimeMs).ToList()); Dictionary<string, RaceRecord> dictionary2 = new Dictionary<string, RaceRecord>(StringComparer.Ordinal); Dictionary<string, RaceRecord> dictionary3 = new Dictionary<string, RaceRecord>(StringComparer.Ordinal); foreach (KeyValuePair<string, List<RaceRecord>> item in dictionary) { if (item.Value.Count != 0) { dictionary2[item.Key] = item.Value[0]; dictionary3[item.Key] = item.Value[item.Value.Count - 1]; } } database.PersonalBests = dictionary2; database.LastRuns = dictionary3; } } public sealed class RaceClientGearMonitor : MonoBehaviour { private float _nextCheckTime; private bool _reportedViolation; private void Update() { if (!ModConfig.EnableGearCheck.Value || (Object)(object)Player.m_localPlayer == (Object)null || Time.time < _nextCheckTime) { return; } _nextCheckTime = Time.time + Mathf.Max(0.25f, ModConfig.UpdateInterval.Value); RunnerSnapshot localRunner = RaceNetSync.GetLocalRunner(); if (localRunner == null || localRunner.StartUtcTicks <= 0 || localRunner.Finished || localRunner.Disqualified) { _reportedViolation = false; } else { if (RaceNetSync.ClientState == null || RaceNetSync.ClientState.Phase != 3 || _reportedViolation) { return; } GearValidator.GearCheckResult gearCheckResult = GearValidator.CheckRuntimeGear(Player.m_localPlayer); if (!gearCheckResult.IsValid) { _reportedViolation = true; string text = ((gearCheckResult.Issues.Count > 0) ? gearCheckResult.Issues[0] : "illegal gear change"); localRunner.Disqualified = true; localRunner.DisqualifiedReason = text; if (localRunner.FinishTimeMs <= 0) { localRunner.FinishTimeMs = (long)(DateTime.UtcNow - new DateTime(localRunner.StartUtcTicks, DateTimeKind.Utc)).TotalMilliseconds; } RaceGui.ShowYellowHud("DISQUALIFIED: " + text); RaceGui.ShowInfoPanel("Disqualified", text, 12f); RaceNetSync.SendGearViolation(text); } } } } public sealed class RaceClientPositionReporter : MonoBehaviour { private float _nextSendTime; private void Update() { //IL_00bd: 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) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || Time.time < _nextSendTime) { return; } RaceStateSnapshot clientState = RaceNetSync.ClientState; if (clientState == null || clientState.Phase != 3) { _nextSendTime = Time.time + 0.5f; return; } RunnerSnapshot localRunner = RaceNetSync.GetLocalRunner(); if (localRunner == null || localRunner.StartUtcTicks <= 0 || localRunner.Finished || localRunner.Disqualified) { _nextSendTime = Time.time + 0.5f; return; } _nextSendTime = Time.time + Mathf.Clamp(ModConfig.UpdateInterval.Value, 0.05f, 0.2f); Vector3 position = ((Component)Player.m_localPlayer).transform.position; RaceNetSync.SendRunnerPosition(Player.m_localPlayer.GetPlayerID(), position); } } public enum RaceSoundSlot { Start, Checkpoint, Finish, Countdown, FalseStart } public static class RaceCustomAudio { private static readonly Dictionary<RaceSoundSlot, AudioClip> Clips = new Dictionary<RaceSoundSlot, AudioClip>(); private static readonly HashSet<RaceSoundSlot> LoadAttempted = new HashSet<RaceSoundSlot>(); private static readonly HashSet<RaceSoundSlot> Loading = new HashSet<RaceSoundSlot>(); private static MonoBehaviour _host; private static AudioSource _oneShotSource; private static AudioSource _streamSource; private static bool _folderEnsured; public static void Initialize(MonoBehaviour host) { _host = host; _oneShotSource = ((Component)host).gameObject.AddComponent<AudioSource>(); _oneShotSource.playOnAwake = false; _oneShotSource.spatialBlend = 0f; _oneShotSource.spatialize = false; _oneShotSource.bypassEffects = true; _oneShotSource.bypassListenerEffects = true; _oneShotSource.bypassReverbZones = true; _streamSource = ((Component)host).gameObject.AddComponent<AudioSource>(); _streamSource.playOnAwake = false; _streamSource.spatialBlend = 0f; _streamSource.spatialize = false; _streamSource.loop = false; _streamSource.bypassEffects = true; _streamSource.bypassListenerEffects = true; _streamSource.bypassReverbZones = true; EnsureSoundsFolder(); } public static string GetSoundsFolderPath() { string text = ModConfig.CustomSoundsFolder.Value?.Trim() ?? "Sounds"; text = text.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); string runningManPluginDirectory = GetRunningManPluginDirectory(); if (Path.IsPathRooted(text)) { return text; } return Path.Combine(runningManPluginDirectory, text); } private static string GetRunningManPluginDirectory() { try { string location = typeof(RunningManPlugin).Assembly.Location; if (!string.IsNullOrEmpty(location)) { string directoryName = Path.GetDirectoryName(location); if (!string.IsNullOrEmpty(directoryName)) { return directoryName; } } } catch { } return Path.Combine(Paths.PluginPath, "RunningMan"); } public static void PreloadAll() { if (!((Object)(object)_host == (Object)null) && ModConfig.UseCustomRaceSounds.Value) { _host.StartCoroutine(PreloadRoutine()); } } public static bool TryPlay(RaceSoundSlot slot, float volume, Action playFallback) { if (!ModConfig.UseCustomRaceSounds.Value) { return false; } if (Clips.TryGetValue(slot, out var value) && (Object)(object)value != (Object)null) { PlayClip(value, volume); return true; } if (LoadAttempted.Contains(slot)) { return false; } if ((Object)(object)_host == (Object)null) { return false; } if (!HasCustomFile(slot)) { LoadAttempted.Add(slot); return false; } _host.StartCoroutine(LoadAndPlay(slot, volume, playFallback)); return true; } public static bool HasCustomFile(RaceSoundSlot slot) { string path; return TryResolveFilePath(GetConfiguredFileName(slot), out path); } private static IEnumerator PreloadRoutine() { yield return LoadClip(RaceSoundSlot.Start); yield return LoadClip(RaceSoundSlot.Checkpoint); yield return LoadClip(RaceSoundSlot.Finish); yield return LoadClip(RaceSoundSlot.Countdown); yield return LoadClip(RaceSoundSlot.FalseStart); } private static IEnumerator LoadAndPlay(RaceSoundSlot slot, float volume, Action playFallback) { if (!Loading.Contains(slot)) { Loading.Add(slot); yield return LoadClip(slot); Loading.Remove(slot); if (Clips.TryGetValue(slot, out var value) && (Object)(object)value != (Object)null) { PlayClip(value, volume); } else { playFallback?.Invoke(); } } } private static IEnumerator LoadClip(RaceSoundSlot slot) { if (LoadAttempted.Contains(slot)) { yield break; } LoadAttempted.Add(slot); string configuredFileName = GetConfiguredFileName(slot); if (!TryResolveFilePath(configuredFileName, out var path)) { RunningManPlugin.Log.LogInfo((object)$"RunningMan: no custom sound for {slot} (expected {configuredFileName} in {GetSoundsFolderPath()})."); yield break; } string text = "file:///" + path.Replace("\\", "/"); AudioType audioType = GetAudioType(path); UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(text, audioType); try { yield return request.SendWebRequest(); if (!string.IsNullOrEmpty(request.error)) { RunningManPlugin.Log.LogWarning((object)("RunningMan: failed to load custom sound '" + path + "': " + request.error)); yield break; } AudioClip content = DownloadHandlerAudioClip.GetContent(request); if ((Object)(object)content == (Object)null) { RunningManPlugin.Log.LogWarning((object)("RunningMan: custom sound '" + path + "' loaded empty.")); yield break; } ((Object)content).name = Path.GetFileName(path); Clips[slot] = content; RunningManPlugin.Log.LogInfo((object)$"RunningMan: loaded custom sound '{((Object)content).name}' for {slot}."); } finally { ((IDisposable)request)?.Dispose(); } } public static bool TryPlayStream(RaceSoundSlot slot, float volume) { if (!ModConfig.UseCustomRaceSounds.Value || (Object)(object)_streamSource == (Object)null) { return false; } if (Clips.TryGetValue(slot, out var value) && (Object)(object)value != (Object)null) { PlayStream(value, volume); return true; } if (LoadAttempted.Contains(slot) || (Object)(object)_host == (Object)null || !HasCustomFile(slot)) { return false; } _host.StartCoroutine(LoadAndPlayStream(slot, volume)); return true; } public static void StopStream() { if ((Object)(object)_streamSource != (Object)null && _streamSource.isPlaying) { _streamSource.Stop(); } } private static IEnumerator LoadAndPlayStream(RaceSoundSlot slot, float volume) { if (!Loading.Contains(slot)) { Loading.Add(slot); yield return LoadClip(slot); Loading.Remove(slot); if (Clips.TryGetValue(slot, out var value) && (Object)(object)value != (Object)null) { PlayStream(value, volume); } } } private static void PlayStream(AudioClip clip, float volume) { if (!((Object)(object)_streamSource == (Object)null) && !((Object)(object)clip == (Object)null)) { _streamSource.Stop(); _streamSource.clip = clip; _streamSource.volume = Mathf.Clamp(volume, 0f, 2f); _streamSource.Play(); } } private static void PlayClip(AudioClip clip, float volume) { if (!((Object)(object)_oneShotSource == (Object)null) && !((Object)(object)clip == (Object)null)) { _oneShotSource.PlayOneShot(clip, Mathf.Clamp(volume, 0f, 2f)); } } private static string GetConfiguredFileName(RaceSoundSlot slot) { return slot switch { RaceSoundSlot.Start => ModConfig.CustomStartSoundFile.Value, RaceSoundSlot.Checkpoint => ModConfig.CustomCheckpointSoundFile.Value, RaceSoundSlot.Finish => ModConfig.CustomFinishSoundFile.Value, RaceSoundSlot.Countdown => ModConfig.CustomCountdownSoundFile.Value, RaceSoundSlot.FalseStart => ModConfig.CustomFalseStartSoundFile.Value, _ => string.Empty, }; } private static bool TryResolveFilePath(string configuredName, out string path) { path = null; if (string.IsNullOrWhiteSpace(configuredName)) { return false; } string soundsFolderPath = GetSoundsFolderPath(); string text = configuredName.Trim(); string text2 = Path.Combine(soundsFolderPath, text); if (File.Exists(text2)) { path = text2; return true; } string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text); string[] array = new string[3] { ".mp3", ".wav", ".ogg" }; foreach (string text3 in array) { text2 = Path.Combine(soundsFolderPath, fileNameWithoutExtension + text3); if (File.Exists(text2)) { path = text2; return true; } } return false; } private static AudioType GetAudioType(string path) { string text = Path.GetExtension(path)?.ToLowerInvariant(); if (!(text == ".ogg")) { if (text == ".wav") { return (AudioType)20; } return (AudioType)13; } return (AudioType)14; } private static void EnsureSoundsFolder() { if (_folderEnsured) { return; } _folderEnsured = true; try { string soundsFolderPath = GetSoundsFolderPath(); Directory.CreateDirectory(soundsFolderPath); RunningManPlugin.Log.LogInfo((object)("RunningMan custom sounds folder: " + soundsFolderPath + " (add start.mp3, checkpoint.mp3, finish.mp3, countdown.mp3, false_start.mp3)")); } catch (Exception ex) { RunningManPlugin.Log.LogWarning((object)("RunningMan: could not create sounds folder: " + ex.Message)); } } } public sealed class RaceDebugDrawer : MonoBehaviour { private static Texture2D _lineTexture; private static GUIStyle _labelStyle; private static readonly Color CheckpointColor = new Color(1f, 0.92f, 0.16f); private void OnEnable() { RaceNetSync.TrackUpdated += MarkDirty; RaceNetSync.StateUpdated += MarkDirty; } private void OnDisable() { RaceNetSync.TrackUpdated -= MarkDirty; RaceNetSync.StateUpdated -= MarkDirty; } private static void MarkDirty() { } private void OnGUI() { //IL_000d: Unkno