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 FishComp v1.2.0
FishComp.dll
Decompiled a month agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using FishComp.Commands; using FishComp.Competition; using FishComp.Networking; using FishComp.Patches; using FishComp.UI; using HarmonyLib; using Jotunn; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("FishComp")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+6cf0958373f80653656a83c12f040f294af5de17")] [assembly: AssemblyProduct("FishComp")] [assembly: AssemblyTitle("FishComp")] [assembly: AssemblyVersion("1.0.0.0")] [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 FishComp { [BepInPlugin("com.fishcomp.valheim", "FishComp", "1.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string ModGuid = "com.fishcomp.valheim"; public const string ModName = "FishComp"; public const string ModVersion = "1.2.0"; private static GameObject _updaterHost; private Harmony harmony; public static Plugin Instance { get; private set; } public static ConfigEntry<float> ConfigPositionX { get; private set; } public static ConfigEntry<float> ConfigPositionY { get; private set; } public static ConfigEntry<float> ConfigSyncInterval { get; private set; } public CompetitionManager CompetitionManager { get; private set; } public RpcManager RpcManager { get; private set; } public LeaderboardUI UI { get; private set; } private void Awake() { //IL_000b: 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) //IL_0016: Expected O, but got Unknown //IL_001b: Expected O, but got Unknown //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Expected O, but got Unknown //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Expected O, but got Unknown Instance = this; GameObject val = new GameObject("FishComp_UpdaterHost"); Object.DontDestroyOnLoad((Object)val); _updaterHost = val; _updaterHost.AddComponent<ServerTicker>(); ConfigPositionX = ((BaseUnityPlugin)this).Config.Bind<float>("UI", "PanelPositionX", 20f, "Horizontal position of the leaderboard panel from the left edge."); ConfigPositionY = ((BaseUnityPlugin)this).Config.Bind<float>("UI", "PanelPositionY", -180f, "Vertical position of the leaderboard panel from the top edge (negative = downward)."); ConfigSyncInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "SyncIntervalSeconds", 3f, "How often (in seconds) the server broadcasts competition state to all clients."); CompetitionManager = new CompetitionManager(); RpcManager = new RpcManager(); new FishCompCommands(); PrefabManager.OnVanillaPrefabsAvailable += InitUI; harmony = new Harmony("com.fishcomp.valheim"); MethodInfo methodInfo = AccessTools.Method(typeof(Humanoid), "Pickup", (Type[])null, (Type[])null); if (methodInfo != null) { MethodInfo methodInfo2 = AccessTools.Method(typeof(FishingPatch), "HumanoidPickupPostfix", (Type[])null, (Type[])null); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Logger.LogInfo((object)"Patched Humanoid.Pickup"); } else { Logger.LogError((object)"Humanoid.Pickup not found!"); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"FishComp v1.2.0 loaded."); } private void InitUI() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 PrefabManager.OnVanillaPrefabsAvailable -= InitUI; if ((int)SystemInfo.graphicsDeviceType == 4) { Logger.LogInfo((object)"Headless server detected — skipping LeaderboardUI creation."); return; } Logger.LogInfo((object)"InitUI called — creating LeaderboardUI"); UI = new LeaderboardUI(_updaterHost); } } } namespace FishComp.UI { public class LeaderboardUI { private class LeaderboardUpdater : MonoBehaviour { private LeaderboardUI leaderboardUI; public void Init(LeaderboardUI ui) { leaderboardUI = ui; } private void Update() { if (leaderboardUI != null) { leaderboardUI.UpdateFrame(); } } } private GameObject _canvasRoot; private GameObject panel; private Text timerText; private Transform fishCaughtList; private bool _showTestUI; private bool _listDirty; private bool PanelExists { get { if ((Object)(object)panel != (Object)null) { return Object.op_Implicit((Object)(object)panel); } return false; } } public LeaderboardUI(GameObject updaterHost) { if (GUIManager.Instance == null) { Logger.LogError((object)"LeaderboardUI created before GUIManager is available."); return; } CompetitionManager.Instance.OnLeaderboardChanged += OnLeaderboardChanged; updaterHost.AddComponent<LeaderboardUpdater>().Init(this); } private void OnLeaderboardChanged() { _listDirty = true; } private void CreatePanel() { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Expected O, but got Unknown //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Expected O, but got Unknown Logger.LogInfo((object)"CreatePanel() entered"); if ((Object)(object)_canvasRoot == (Object)null) { _canvasRoot = new GameObject("FishCompCanvasRoot", new Type[4] { typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster) }); Object.DontDestroyOnLoad((Object)(object)_canvasRoot); Canvas component = _canvasRoot.GetComponent<Canvas>(); component.renderMode = (RenderMode)0; component.sortingOrder = 100; CanvasScaler component2 = _canvasRoot.GetComponent<CanvasScaler>(); component2.uiScaleMode = (ScaleMode)1; component2.referenceResolution = new Vector2(1920f, 1080f); component2.screenMatchMode = (ScreenMatchMode)0; component2.matchWidthOrHeight = 0.5f; } if ((Object)(object)panel != (Object)null) { Object.Destroy((Object)(object)panel); panel = null; } panel = new GameObject("FishCompPanel", new Type[2] { typeof(RectTransform), typeof(Image) }); panel.transform.SetParent(_canvasRoot.transform, false); RectTransform component3 = panel.GetComponent<RectTransform>(); component3.anchorMin = new Vector2(0f, 1f); component3.anchorMax = new Vector2(0f, 1f); component3.pivot = new Vector2(0f, 1f); component3.anchoredPosition = new Vector2(Plugin.ConfigPositionX.Value, Plugin.ConfigPositionY.Value); component3.sizeDelta = new Vector2(200f, 0f); Image component4 = panel.GetComponent<Image>(); component4.sprite = null; ((Graphic)component4).color = new Color(0f, 0f, 0f, 0.6f); VerticalLayoutGroup obj = panel.AddComponent<VerticalLayoutGroup>(); ((LayoutGroup)obj).padding = new RectOffset(10, 10, 8, 10); ((HorizontalOrVerticalLayoutGroup)obj).spacing = 4f; ((LayoutGroup)obj).childAlignment = (TextAnchor)0; ((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)obj).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandWidth = true; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandHeight = false; ContentSizeFitter obj2 = panel.AddComponent<ContentSizeFitter>(); obj2.horizontalFit = (FitMode)0; obj2.verticalFit = (FitMode)2; Font font = (((Object)(object)GUIManager.Instance.AveriaSerifBold != (Object)null) ? GUIManager.Instance.AveriaSerifBold : GUIManager.Instance.AveriaSerif); CreateText("Title", panel.transform, "Fishing Competition", 15, (FontStyle)1, font, (TextAnchor)4); timerText = CreateText("Timer", panel.transform, "Time Remaining: 00:00", 12, (FontStyle)0, font, (TextAnchor)0); fishCaughtList = CreateList("FishCaughtList", panel.transform); Logger.LogInfo((object)"CreatePanel() completed"); _listDirty = true; } private static Text CreateText(string name, Transform parent, string text, int fontSize, FontStyle style, Font font, TextAnchor alignment = (TextAnchor)0) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[3] { typeof(RectTransform), typeof(Text), typeof(ContentSizeFitter) }); val.transform.SetParent(parent, false); Text component = val.GetComponent<Text>(); component.text = text; component.font = font; component.fontSize = fontSize; component.fontStyle = style; ((Graphic)component).color = Color.white; component.alignment = alignment; component.horizontalOverflow = (HorizontalWrapMode)0; component.verticalOverflow = (VerticalWrapMode)0; ContentSizeFitter component2 = val.GetComponent<ContentSizeFitter>(); component2.horizontalFit = (FitMode)0; component2.verticalFit = (FitMode)2; return component; } private static Transform CreateList(string name, Transform parent) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[3] { typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter) }); val.transform.SetParent(parent, false); VerticalLayoutGroup component = val.GetComponent<VerticalLayoutGroup>(); ((HorizontalOrVerticalLayoutGroup)component).spacing = 2f; ((LayoutGroup)component).childAlignment = (TextAnchor)0; ((HorizontalOrVerticalLayoutGroup)component).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)component).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)component).childForceExpandWidth = true; ((HorizontalOrVerticalLayoutGroup)component).childForceExpandHeight = false; ContentSizeFitter component2 = val.GetComponent<ContentSizeFitter>(); component2.horizontalFit = (FitMode)0; component2.verticalFit = (FitMode)2; return val.transform; } public void SetPosition(float x, float y) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (PanelExists) { panel.GetComponent<RectTransform>().anchoredPosition = new Vector2(x, y); } } public bool ToggleTestUI() { _showTestUI = !_showTestUI; Logger.LogInfo((object)$"ToggleTestUI: _showTestUI is now {_showTestUI}"); _listDirty = true; return _showTestUI; } public void UpdateFrame() { if (!PanelExists) { if (GUIManager.Instance == null) { return; } CreatePanel(); if (!PanelExists) { return; } } CompetitionManager instance = CompetitionManager.Instance; bool isRunning = instance.IsRunning; if (isRunning) { _showTestUI = false; } bool flag = isRunning || _showTestUI; panel.SetActive(flag); if (!flag) { return; } if (isRunning) { int num = Mathf.Max(0, Mathf.CeilToInt(instance.RemainingSeconds)); timerText.text = $"Time Remaining: {num / 60:00}:{num % 60:00}"; } if (!_listDirty) { return; } _listDirty = false; if (_showTestUI && !isRunning) { timerText.text = "Time Remaining: 05:00"; RebuildList(fishCaughtList, new List<LeaderboardEntry> { new LeaderboardEntry { PlayerId = "1", PlayerName = "Fisherman1", FishCaught = 7 }, new LeaderboardEntry { PlayerId = "2", PlayerName = "Fisherman2", FishCaught = 4 }, new LeaderboardEntry { PlayerId = "3", PlayerName = "Fisherman3", FishCaught = 1 } }); } else if (isRunning) { RebuildList(fishCaughtList, (from e in instance.Entries.Values orderby e.FishCaught descending, e.PlayerName select e).ToList()); } LayoutRebuilder.ForceRebuildLayoutImmediate(panel.GetComponent<RectTransform>()); } private void RebuildList(Transform list, List<LeaderboardEntry> entries) { for (int num = list.childCount - 1; num >= 0; num--) { Object.Destroy((Object)(object)((Component)list.GetChild(num)).gameObject); } Font font = (((Object)(object)GUIManager.Instance.AveriaSerifBold != (Object)null) ? GUIManager.Instance.AveriaSerifBold : GUIManager.Instance.AveriaSerif); if (entries.Count == 0) { CreateText("NoEntries", list, "No catches yet", 12, (FontStyle)0, font, (TextAnchor)0); return; } for (int i = 0; i < entries.Count; i++) { LeaderboardEntry leaderboardEntry = entries[i]; CreateText($"Entry{i + 1}", list, $"{i + 1}. {leaderboardEntry.PlayerName} -- {leaderboardEntry.FishCaught}", 12, (FontStyle)0, font, (TextAnchor)0); } } } } namespace FishComp.Patches { public static class FishingPatch { public static void HumanoidPickupPostfix(Humanoid __instance, GameObject go, bool __result) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Expected O, but got Unknown if (__result && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)go.GetComponent<Fish>() == (Object)null) && CompetitionManager.Instance != null && CompetitionManager.Instance.IsRunning) { Logger.LogInfo((object)"[FishComp][DIAG] HumanoidPickupPostfix counted=true"); Logger.LogInfo((object)"Local player caught a fish — sending FishComp_ReportCatch to server"); RpcManager.Instance.SendToServer("FishComp_ReportCatch", new ZPackage()); } } } } namespace FishComp.Networking { public class RpcManager { public const string RpcSyncState = "FishComp_SyncState"; public const string RpcAnnounceWinner = "FishComp_AnnounceWinner"; public const string RpcAnnounceStart = "FishComp_AnnounceStart"; public const string RpcRequestStart = "FishComp_RequestStart"; public const string RpcRequestEnd = "FishComp_RequestEnd"; public const string RpcRequestReset = "FishComp_RequestReset"; public const string RpcReportCatch = "FishComp_ReportCatch"; private bool registered; private ZRoutedRpc registeredOn; private bool isApplyingRemoteState; public static RpcManager Instance { get; private set; } public bool Registered { get { if (registered) { return registeredOn == ZRoutedRpc.instance; } return false; } } public RpcManager() { Instance = this; CompetitionManager.Instance.OnLeaderboardChanged += OnLeaderboardChanged; CompetitionManager.Instance.OnCompetitionEnded += OnCompetitionEnded; Logger.LogInfo((object)"RpcManager subscribed to OnCompetitionEnded/OnLeaderboardChanged"); PrefabManager.OnVanillaPrefabsAvailable += OnVanillaPrefabsAvailable; } private void OnVanillaPrefabsAvailable() { Register(); } public void Register() { if (!Registered && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.Register<ZPackage>("FishComp_SyncState", (Action<long, ZPackage>)RPC_SyncState); ZRoutedRpc.instance.Register<ZPackage>("FishComp_AnnounceWinner", (Action<long, ZPackage>)RPC_AnnounceWinner); ZRoutedRpc.instance.Register<ZPackage>("FishComp_AnnounceStart", (Action<long, ZPackage>)RPC_AnnounceStart); ZRoutedRpc.instance.Register<ZPackage>("FishComp_RequestStart", (Action<long, ZPackage>)RPC_RequestStart); ZRoutedRpc.instance.Register<ZPackage>("FishComp_RequestEnd", (Action<long, ZPackage>)RPC_RequestEnd); ZRoutedRpc.instance.Register<ZPackage>("FishComp_RequestReset", (Action<long, ZPackage>)RPC_RequestReset); ZRoutedRpc.instance.Register<ZPackage>("FishComp_ReportCatch", (Action<long, ZPackage>)RPC_ReportCatch); registeredOn = ZRoutedRpc.instance; registered = true; Logger.LogInfo((object)"RpcManager RPCs registered (SyncState, AnnounceWinner, AnnounceStart, RequestStart, RequestEnd, RequestReset, ReportCatch)"); } } public void SendToServer(string rpcName, ZPackage pkg) { if (ZRoutedRpc.instance != null) { Register(); Logger.LogInfo((object)("[FishComp][DIAG] SendToServer rpc=" + rpcName + " target=no-target")); ZRoutedRpc.instance.InvokeRoutedRPC(rpcName, new object[1] { pkg }); } } private bool IsSenderAdmin(long sender) { if ((Object)(object)ZNet.instance == (Object)null) { return false; } if (ZNet.instance.IsServer() && sender == ZNet.GetUID()) { Logger.LogInfo((object)"[FishComp][DIAG] IsSenderAdmin branch=self-routed-server playerName=N/A steamId=N/A"); return true; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer == null) { Logger.LogInfo((object)"[FishComp][DIAG] IsSenderAdmin branch=peer-null playerName=N/A steamId=N/A"); Logger.LogWarning((object)$"[FishComp] IsSenderAdmin: no peer for sender {sender} (not server UID {ZNet.GetUID()}) — rejecting"); return false; } if (peer.m_socket == null) { Logger.LogWarning((object)$"[FishComp] IsSenderAdmin: peer {sender} has no socket — rejecting"); return false; } string hostName = peer.m_socket.GetHostName(); bool flag = ZNet.instance.IsAdmin(hostName); Logger.LogInfo((object)("[FishComp][DIAG] IsSenderAdmin branch=" + (flag ? "peer-found-admin" : "peer-found-not-admin") + " playerName=" + peer.m_playerName + " steamId=" + hostName)); return flag; } private static bool IsFromServer(long sender) { if ((Object)(object)ZNet.instance == (Object)null) { return false; } if (ZNet.instance.IsServer()) { return sender == ZNet.GetUID(); } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null) { return sender == serverPeer.m_uid; } return false; } public void BroadcastState() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown if (ZRoutedRpc.instance == null) { return; } Register(); CompetitionManager instance = CompetitionManager.Instance; ZPackage val = new ZPackage(); val.Write(instance.IsRunning); val.Write(instance.RemainingSeconds); val.Write(instance.Entries.Count); foreach (LeaderboardEntry value in instance.Entries.Values) { val.Write(value.PlayerId); val.Write(value.PlayerName); val.Write(value.FishCaught); } Logger.LogInfo((object)"[FishComp][DIAG] BroadcastState rpc=FishComp_SyncState target=Everybody"); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "FishComp_SyncState", new object[1] { val }); } public void BroadcastWinner(CompetitionResult result) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown if (ZRoutedRpc.instance == null) { Logger.LogWarning((object)"BroadcastWinner called but ZRoutedRpc.instance is null — skipping"); return; } Register(); if (!Registered) { Logger.LogWarning((object)"BroadcastWinner called but RPCs are not registered — skipping"); return; } if (result == null || result.TopByFishCaught == null) { Logger.LogInfo((object)"BroadcastWinner — no catches recorded, skipping announcement"); return; } string playerName = result.TopByFishCaught.PlayerName; int fishCaught = result.TopByFishCaught.FishCaught; ZPackage val = new ZPackage(); val.Write(playerName); val.Write(fishCaught); Logger.LogInfo((object)$"BroadcastWinner — most fish: {playerName} ({fishCaught} fish)"); Logger.LogInfo((object)"[FishComp][DIAG] BroadcastWinner rpc=FishComp_AnnounceWinner target=Everybody"); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "FishComp_AnnounceWinner", new object[1] { val }); } private void BroadcastAnnounceStart(int minutes) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(minutes); Logger.LogInfo((object)"[FishComp][DIAG] BroadcastAnnounceStart rpc=FishComp_AnnounceStart target=Everybody"); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "FishComp_AnnounceStart", new object[1] { val }); } } private void RPC_SyncState(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_SyncState sender={sender}"); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return; } if (!IsFromServer(sender)) { Logger.LogWarning((object)$"RPC_SyncState rejected — sender {sender} is not the server"); return; } Logger.LogInfo((object)"RPC_SyncState received"); bool isRunning = pkg.ReadBool(); float remainingSeconds = pkg.ReadSingle(); int num = pkg.ReadInt(); List<LeaderboardEntry> list = new List<LeaderboardEntry>(num); for (int i = 0; i < num; i++) { list.Add(new LeaderboardEntry { PlayerId = pkg.ReadString(), PlayerName = pkg.ReadString(), FishCaught = pkg.ReadInt() }); } isApplyingRemoteState = true; CompetitionManager.Instance.ApplyRemoteState(isRunning, remainingSeconds, list); isApplyingRemoteState = false; } private void RPC_AnnounceWinner(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_AnnounceWinner sender={sender}"); if (!IsFromServer(sender)) { Logger.LogWarning((object)$"RPC_AnnounceWinner rejected — sender {sender} is not the server"); return; } Logger.LogInfo((object)"RPC_AnnounceWinner received"); string text = pkg.ReadString(); int num = pkg.ReadInt(); string text2 = (string.IsNullOrEmpty(text) ? "nobody" : $"{text} ({num} fish)"); string text3 = "FishComp Results - Most Fish Caught: " + text2; if ((Object)(object)MessageHud.instance == (Object)null) { Logger.LogDebug((object)"RPC_AnnounceWinner — MessageHud.instance is null, skipping display"); } else { MessageHud.instance.ShowMessage((MessageType)2, text3, 0, (Sprite)null, false, true); } } private void RPC_AnnounceStart(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_AnnounceStart sender={sender}"); if (!IsFromServer(sender)) { Logger.LogWarning((object)$"RPC_AnnounceStart rejected — sender {sender} is not the server"); return; } Logger.LogInfo((object)"RPC_AnnounceStart received"); int num = pkg.ReadInt(); if ((Object)(object)MessageHud.instance == (Object)null) { Logger.LogDebug((object)"RPC_AnnounceStart — MessageHud.instance is null, skipping display"); } else { MessageHud.instance.ShowMessage((MessageType)2, $"A fishing competition has started! You have {num} minutes. Good luck!", 0, (Sprite)null, false, true); } } private void RPC_RequestStart(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_RequestStart sender={sender}"); Logger.LogInfo((object)$"RPC_RequestStart from sender {sender}"); if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"RPC_RequestStart rejected — sender {sender} is not admin"); return; } int num = pkg.ReadInt(); if (num < 1) { Logger.LogWarning((object)$"RPC_RequestStart rejected — invalid duration {num}"); return; } if (CompetitionManager.Instance.IsRunning) { Logger.LogWarning((object)"RPC_RequestStart rejected — competition already running"); return; } Logger.LogInfo((object)$"Starting competition for {num} minutes"); CompetitionManager.Instance.StartCompetition(num); BroadcastAnnounceStart(num); } private void RPC_RequestEnd(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_RequestEnd sender={sender}"); Logger.LogInfo((object)$"RPC_RequestEnd from sender {sender}"); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"RPC_RequestEnd rejected — sender {sender} is not admin"); return; } if (!CompetitionManager.Instance.IsRunning) { Logger.LogWarning((object)"RPC_RequestEnd — no competition running"); return; } Logger.LogInfo((object)"Ending competition via admin request"); CompetitionManager.Instance.EndCompetition(); } } private void RPC_RequestReset(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_RequestReset sender={sender}"); Logger.LogInfo((object)$"RPC_RequestReset from sender {sender}"); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"RPC_RequestReset rejected — sender {sender} is not admin"); return; } Logger.LogInfo((object)"Resetting competition via admin request"); CompetitionManager.Instance.ResetCompetition(); } } private void RPC_ReportCatch(long sender, ZPackage pkg) { Logger.LogInfo((object)$"[FishComp][DIAG] RPC_ReportCatch sender={sender}"); Logger.LogInfo((object)$"RPC_ReportCatch from sender {sender}"); if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !CompetitionManager.Instance.IsRunning) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); string durableIdentity; string text; if (peer != null) { if (peer.m_socket == null) { Logger.LogWarning((object)$"[FishComp] RPC_ReportCatch: peer {sender} has no socket — rejecting"); return; } durableIdentity = peer.m_socket.GetHostName(); text = peer.m_playerName; } else { if (!ZNet.instance.IsServer() || sender != ZNet.GetUID()) { Logger.LogWarning((object)$"[FishComp] RPC_ReportCatch: no peer for sender {sender} (not server UID {ZNet.GetUID()}) — rejecting"); return; } if ((Object)(object)Player.m_localPlayer == (Object)null) { Logger.LogWarning((object)$"[FishComp] RPC_ReportCatch: self-routed but no local player — rejecting sender {sender}"); return; } durableIdentity = "#host"; text = Player.m_localPlayer.GetPlayerName(); } string opaqueKey = CompetitionManager.Instance.GetOpaqueKey(durableIdentity); if (string.IsNullOrEmpty(opaqueKey)) { Logger.LogWarning((object)$"[FishComp] RPC_ReportCatch: no durable identity for sender {sender} — rejecting"); return; } if (string.IsNullOrEmpty(text)) { text = "Unknown"; } Logger.LogInfo((object)$"[FishComp][DIAG] ReportCatch sender={sender} peerNull={peer == null} name={text} key={opaqueKey}"); CompetitionManager.Instance.RecordCatch(opaqueKey, text); } private void OnLeaderboardChanged() { if (!isApplyingRemoteState && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { BroadcastState(); } } private void OnCompetitionEnded(CompetitionResult result) { Logger.LogInfo((object)"RpcManager.OnCompetitionEnded invoked"); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { BroadcastWinner(result); BroadcastState(); } else { Logger.LogInfo((object)"OnCompetitionEnded — not server, skipping broadcast"); } } } } namespace FishComp.Competition { public class CompetitionResult { public LeaderboardEntry TopByFishCaught { get; set; } } public class CompetitionManager { public const string HostIdentity = "#host"; private readonly Dictionary<string, string> identityKeys = new Dictionary<string, string>(); private int nextIdentityKey; public static CompetitionManager Instance { get; private set; } public bool IsRunning { get; private set; } public float RemainingSeconds { get; private set; } public Dictionary<string, LeaderboardEntry> Entries { get; } = new Dictionary<string, LeaderboardEntry>(); public event Action<CompetitionResult> OnCompetitionEnded; public event Action OnLeaderboardChanged; public CompetitionManager() { Instance = this; } public void StartCompetition(int minutes) { IsRunning = true; RemainingSeconds = minutes * 60; Entries.Clear(); ClearIdentityKeys(); this.OnLeaderboardChanged?.Invoke(); } public CompetitionResult EndCompetition() { IsRunning = false; CompetitionResult competitionResult = new CompetitionResult { TopByFishCaught = Entries.Values.OrderByDescending((LeaderboardEntry e) => e.FishCaught).FirstOrDefault() }; this.OnCompetitionEnded?.Invoke(competitionResult); return competitionResult; } public void ResetCompetition() { IsRunning = false; Entries.Clear(); ClearIdentityKeys(); RemainingSeconds = 0f; this.OnLeaderboardChanged?.Invoke(); } public string GetOpaqueKey(string durableIdentity) { if (string.IsNullOrEmpty(durableIdentity)) { return null; } if (!identityKeys.TryGetValue(durableIdentity, out var value)) { int num = ++nextIdentityKey; value = "p" + num.ToString(CultureInfo.InvariantCulture); identityKeys[durableIdentity] = value; } return value; } private void ClearIdentityKeys() { identityKeys.Clear(); nextIdentityKey = 0; } public void RecordCatch(string playerId, string playerName) { if (IsRunning) { if (!Entries.TryGetValue(playerId, out var value)) { value = new LeaderboardEntry { PlayerId = playerId, PlayerName = playerName }; Entries[playerId] = value; } value.RecordCatch(); this.OnLeaderboardChanged?.Invoke(); } } public void ApplyRemoteState(bool isRunning, float remainingSeconds, IEnumerable<LeaderboardEntry> entries) { IsRunning = isRunning; RemainingSeconds = remainingSeconds; Entries.Clear(); foreach (LeaderboardEntry entry in entries) { Entries[entry.PlayerId] = entry; } this.OnLeaderboardChanged?.Invoke(); } public void Tick(float deltaTime) { if (!IsRunning) { return; } RemainingSeconds -= deltaTime; if (RemainingSeconds <= 0f) { RemainingSeconds = 0f; if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { EndCompetition(); } } } } [Serializable] public class LeaderboardEntry { public string PlayerId; public string PlayerName; public int FishCaught; public void RecordCatch() { FishCaught++; } } public class ServerTicker : MonoBehaviour { private float _syncTimer; private float _inWorldSeconds; private bool _loggedRegistrationTimeout; private bool _loggedWaitingForWorld; private void Start() { Logger.LogInfo((object)"ServerTicker started — competition tick active on all machines; periodic broadcast active on server"); } private void RetryRpcRegistration() { if (RpcManager.Instance == null || RpcManager.Instance.Registered) { return; } RpcManager.Instance.Register(); if (RpcManager.Instance.Registered) { Logger.LogInfo((object)"[FishComp][DIAG] ServerTicker: RpcManager registered"); _inWorldSeconds = 0f; _loggedRegistrationTimeout = false; _loggedWaitingForWorld = false; } else if ((Object)(object)ZNet.instance == (Object)null) { _inWorldSeconds = 0f; _loggedRegistrationTimeout = false; if (!_loggedWaitingForWorld) { _loggedWaitingForWorld = true; Logger.LogInfo((object)"[FishComp][DIAG] ServerTicker: not in a world yet, deferring RPC registration (normal at the main menu)"); } } else { _loggedWaitingForWorld = false; _inWorldSeconds += Time.deltaTime; if (_inWorldSeconds >= 30f && !_loggedRegistrationTimeout) { _loggedRegistrationTimeout = true; Logger.LogWarning((object)"[FishComp][DIAG] ServerTicker: RpcManager STILL UNREGISTERED 30s after joining a world. Inbound RPCs will not be received on this machine."); } } } private void Update() { RetryRpcRegistration(); if (CompetitionManager.Instance == null) { return; } if ((Object)(object)ZNet.instance == (Object)null) { if (CompetitionManager.Instance.IsRunning || CompetitionManager.Instance.Entries.Count > 0) { Logger.LogInfo((object)"[FishComp] Not in a world — clearing stale competition state"); CompetitionManager.Instance.ResetCompetition(); } return; } CompetitionManager.Instance.Tick(Time.deltaTime); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && CompetitionManager.Instance.IsRunning) { _syncTimer -= Time.deltaTime; if (_syncTimer <= 0f) { _syncTimer = Plugin.ConfigSyncInterval.Value; Logger.LogDebug((object)"[FishComp][DIAG] ServerTicker periodic broadcast"); RpcManager.Instance.BroadcastState(); } } } } } namespace FishComp.Commands { public class FishCompCommands { private class StartCommand : ConsoleCommand { public override string Name => "fishcomp_start"; public override string Help => "Start a fishing competition. Usage: fishcomp_start <minutes>"; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown Logger.LogInfo((object)("[FishComp][DIAG] " + ((ConsoleCommand)this).Name + ".Run args=[" + string.Join(",", args) + "]")); if (CheckAdmin()) { if (args.Length < 1 || !int.TryParse(args[0], out var result) || result < 1) { Console.instance.Print(((ConsoleCommand)this).Help); return; } Logger.LogInfo((object)$"fishcomp_start: requesting start for {result} minutes"); ZPackage val = new ZPackage(); val.Write(result); RpcManager.Instance.SendToServer("FishComp_RequestStart", val); Console.instance.Print($"Competition start request sent ({result} min). Announcement will appear when the server confirms."); } } } private class EndCommand : ConsoleCommand { public override string Name => "fishcomp_end"; public override string Help => "End the current fishing competition and announce winners."; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown Logger.LogInfo((object)("[FishComp][DIAG] " + ((ConsoleCommand)this).Name + ".Run args=[" + string.Join(",", args) + "]")); if (CheckAdmin()) { Logger.LogInfo((object)"fishcomp_end: sending request to server"); RpcManager.Instance.SendToServer("FishComp_RequestEnd", new ZPackage()); Console.instance.Print("Competition end request sent."); } } } private class ResetCommand : ConsoleCommand { public override string Name => "fishcomp_reset"; public override string Help => "Reset and hide the leaderboard UI."; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Expected O, but got Unknown Logger.LogInfo((object)("[FishComp][DIAG] " + ((ConsoleCommand)this).Name + ".Run args=[" + string.Join(",", args) + "]")); if (CheckAdmin()) { Logger.LogInfo((object)"fishcomp_reset: sending request to server"); RpcManager.Instance.SendToServer("FishComp_RequestReset", new ZPackage()); Console.instance.Print("Competition reset request sent."); } } } private class SetPosCommand : ConsoleCommand { public override string Name => "fishcomp_setpos"; public override string Help => "Set leaderboard panel position. Usage: fishcomp_setpos <x> <y>"; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { Logger.LogInfo((object)("[FishComp][DIAG] " + ((ConsoleCommand)this).Name + ".Run args=[" + string.Join(",", args) + "]")); if (args.Length < 2 || !float.TryParse(args[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || !float.TryParse(args[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { Console.instance.Print(((ConsoleCommand)this).Help); return; } Logger.LogInfo((object)$"fishcomp_setpos: x={result}, y={result2}"); Plugin.ConfigPositionX.Value = result; Plugin.ConfigPositionY.Value = result2; if (Plugin.Instance.UI != null) { Plugin.Instance.UI.SetPosition(result, result2); } Console.instance.Print($"Panel position set to ({result}, {result2})"); } } private class TestUICommand : ConsoleCommand { public override string Name => "fishcomp_testui"; public override string Help => "Toggle the leaderboard test panel (dummy data for positioning). Run again to hide."; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { Logger.LogInfo((object)("[FishComp][DIAG] " + ((ConsoleCommand)this).Name + ".Run args=[" + string.Join(",", args) + "]")); Logger.LogInfo((object)"fishcomp_testui called"); if (Plugin.Instance.UI == null) { Console.instance.Print("LeaderboardUI not initialised yet — load a world first."); return; } bool flag = Plugin.Instance.UI.ToggleTestUI(); Console.instance.Print(flag ? "Test UI shown. Use fishcomp_setpos <x> <y> to reposition. Run fishcomp_testui again to hide." : "Test UI hidden."); } } public FishCompCommands() { CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new StartCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new EndCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ResetCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new SetPosCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new TestUICommand()); } private static bool CheckAdmin() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return true; } if (!SynchronizationManager.Instance.PlayerIsAdmin) { Console.instance.Print("You must be a server admin to use this command."); return false; } return true; } } }