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.Text; using System.Text.RegularExpressions; using BepInEx; using HarmonyLib; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; using UnityEngine.Networking; [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("ValheimDonationSystem")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+1afeb5969f5b2c052d17afc7d227ef32cdd445b1")] [assembly: AssemblyProduct("ValheimDonationSystem")] [assembly: AssemblyTitle("ValheimDonationSystem")] [assembly: AssemblyVersion("1.0.0.0")] public static class BackendClient { public delegate void Callback(bool ok, T result, string error); public static IEnumerator Get(string path, Callback cb) { return Send("GET", path, null, cb); } public static IEnumerator Post(string path, object body, Callback cb) { return Send("POST", path, body, cb); } private static IEnumerator Send(string method, string path, object body, Callback cb) { if (!Config.Ready) { cb?.Invoke(ok: false, default(T), "backend not configured (valcoin_config.json missing backend_url/plugin_token)"); yield break; } string text = Config.BackendUrl.TrimEnd(new char[1] { '/' }) + path; UnityWebRequest req = new UnityWebRequest(text, method); try { req.timeout = 15; req.SetRequestHeader("Authorization", "Bearer " + Config.PluginToken); req.SetRequestHeader("Accept", "application/json"); req.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); if (body != null) { string s = JsonConvert.SerializeObject(body); byte[] bytes = Encoding.UTF8.GetBytes(s); req.uploadHandler = (UploadHandler)new UploadHandlerRaw(bytes); req.SetRequestHeader("Content-Type", "application/json"); } yield return req.SendWebRequest(); if ((int)req.result != 1) { if (cb != null) { object arg = (int)req.responseCode; string error = req.error; DownloadHandler downloadHandler = req.downloadHandler; cb(ok: false, default(T), $"{arg} {error}: {((downloadHandler != null) ? downloadHandler.text : null)}"); } yield break; } T result; try { string text2 = req.downloadHandler.text; result = (string.IsNullOrEmpty(text2) ? default(T) : JsonConvert.DeserializeObject(text2)); } catch (Exception ex) { cb?.Invoke(ok: false, default(T), "json parse failed: " + ex.Message); yield break; } cb?.Invoke(ok: true, result, null); } finally { ((IDisposable)req)?.Dispose(); } } } public static class Catalog { public class Sku { public string Id; public string Name; public string Description; public int Price; public string Effect; public string Perk; public int Charges = 1; public string Item; public int WeeklyCap; public string RequiresBoss; } private static readonly string CatalogPath = Path.Combine(Paths.ConfigPath, "valcoin_shop.yaml"); private static readonly Regex KvRe = new Regex("^\\s*([a-zA-Z_]+)\\s*:\\s*(.*)$", RegexOptions.Compiled); private static readonly Regex SkuRe = new Regex("^ ([a-z0-9_]+)\\s*:\\s*$", RegexOptions.Compiled); private static readonly Regex FieldRe = new Regex("^ ([a-zA-Z_]+)\\s*:\\s*(.*)$", RegexOptions.Compiled); public static Dictionary Items { get; private set; } = new Dictionary(); public static List Order { get; private set; } = new List(); public static void Load() { EnsureFile(); try { Parse(File.ReadAllLines(CatalogPath)); Debug.Log((object)$"[Valcoin] Shop catalog loaded: {Items.Count} SKU(s)."); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Failed to parse shop catalog: " + ex.Message)); Items = new Dictionary(); Order = new List(); } } private static void EnsureFile() { if (File.Exists(CatalogPath)) { return; } try { File.WriteAllText(CatalogPath, "# Valcoin shop catalog\n# -----------------------------------------------------------------------\n# Each SKU has:\n# name: what shows in /shop\n# description: helper text\n# price: Valcoin cost\n# effect: grant_perk | add_charges | grant_item\n# perk: (perk effects) identifier the plugin understands\n# charges: (add_charges) how many uses each purchase grants\n# item: (grant_item) comma list of \"prefab\" or \"prefab:qty\"\n# weekly_cap: (grant_item) max purchases per player per week (0 = unlimited)\n# requires_boss: (grant_item) global boss key gate, e.g. defeated_bonemass\n# A full ecosystem-aware catalog is in examples/valcoin_shop.example.yaml.\n# Edit and restart the server to apply changes.\n\nshop:\n donor_badge:\n name: \"Donor Badge\"\n description: \"A star next to your name in chat. Forever.\"\n price: 500\n effect: grant_perk\n perk: donor_badge\n\n chat_title:\n name: \"Chat Title\"\n description: \"Unlocks /title to set a custom prefix (e.g. [Jarl])\"\n price: 1500\n effect: grant_perk\n perk: chat_title\n\n companion_flair:\n name: \"Patron's Companion Flair\"\n description: \"A donor-only badge colour and name style on your Dvergr companions. Cosmetic only.\"\n price: 2000\n effect: grant_perk\n perk: companion_flair\n"); Debug.LogWarning((object)("[Valcoin] Created shop catalog template at " + CatalogPath)); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Could not write catalog template: " + ex.Message)); } } private static void Parse(string[] lines) { Dictionary items = new Dictionary(); List order = new List(); bool flag = false; Sku sku = null; foreach (string text in lines) { if (string.IsNullOrWhiteSpace(text) || text.TrimStart(Array.Empty()).StartsWith("#")) { continue; } if (!flag) { if (Regex.IsMatch(text, "^shop\\s*:\\s*$")) { flag = true; } continue; } Match match = SkuRe.Match(text); if (match.Success) { if (sku != null) { Commit(sku, items, order); } sku = new Sku { Id = match.Groups[1].Value }; continue; } Match match2 = FieldRe.Match(text); if (sku != null && match2.Success) { string value = match2.Groups[1].Value; string text2 = StripQuotes(match2.Groups[2].Value.Trim()); switch (value) { case "name": sku.Name = text2; break; case "description": sku.Description = text2; break; case "price": int.TryParse(text2, out sku.Price); break; case "effect": sku.Effect = text2; break; case "perk": sku.Perk = text2; break; case "charges": int.TryParse(text2, out sku.Charges); break; case "item": sku.Item = text2; break; case "weekly_cap": int.TryParse(text2, out sku.WeeklyCap); break; case "requires_boss": sku.RequiresBoss = text2; break; } } else if (text.Length > 0 && text[0] != ' ' && KvRe.IsMatch(text)) { break; } } if (sku != null) { Commit(sku, items, order); } Items = items; Order = order; } private static void Commit(Sku s, Dictionary items, List order) { if (string.IsNullOrEmpty(s.Id) || string.IsNullOrEmpty(s.Effect)) { return; } if (s.Effect == "grant_item") { if (string.IsNullOrEmpty(s.Item)) { return; } } else if (string.IsNullOrEmpty(s.Perk)) { return; } if (string.IsNullOrEmpty(s.Name)) { s.Name = s.Id; } items[s.Id] = s; order.Add(s); } public static string Serialize() { try { return JsonConvert.SerializeObject((object)Order); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Catalog serialize failed: " + ex.Message)); return null; } } public static void ApplyRemote(string json) { if (string.IsNullOrEmpty(json)) { return; } try { List list = JsonConvert.DeserializeObject>(json); if (list == null) { return; } Dictionary dictionary = new Dictionary(); foreach (Sku item in list) { if (!string.IsNullOrEmpty(item.Id)) { dictionary[item.Id] = item; } } Items = dictionary; Order = list; } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Catalog ApplyRemote failed: " + ex.Message)); } } private static string StripQuotes(string v) { if (v.Length >= 2 && v[0] == '"' && v[v.Length - 1] == '"') { return v.Substring(1, v.Length - 2); } return v; } } public class CatalogSync : MonoBehaviour { private const float IntervalSeconds = 30f; private Coroutine _loop; private void Start() { _loop = ((MonoBehaviour)this).StartCoroutine(Loop()); } private void OnDestroy() { if (_loop != null) { ((MonoBehaviour)this).StopCoroutine(_loop); } } private IEnumerator Loop() { while ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { yield return (object)new WaitForSeconds(2f); } while (true) { if (ZRoutedRpc.instance != null) { RpcLayer.BroadcastCatalog(Catalog.Serialize()); } yield return (object)new WaitForSeconds(30f); } } } [HarmonyPatch] public static class ChatRpcSlashPatch { private static MethodBase TargetMethod() { return typeof(Chat).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "RPC_ChatMessage"); } private static bool Prefix(MethodBase __originalMethod, object[] __args) { try { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return true; } int num = -1; string text = null; string text2 = null; string senderName = null; long peerId = 0L; ParameterInfo[] parameters = __originalMethod.GetParameters(); for (int i = 0; i < parameters.Length; i++) { string name = parameters[i].Name; object obj = __args[i]; switch (name) { case "text": text = obj as string; num = i; break; case "sender": if (obj is long num2) { peerId = num2; } break; case "senderNetworkUserId": text2 = obj as string; break; case "userInfo": if (obj != null) { Type type = obj.GetType(); senderName = (type.GetField("Name")?.GetValue(obj) as string) ?? (type.GetProperty("Name")?.GetValue(obj) as string) ?? senderName; text2 = (type.GetField("NetworkUserId")?.GetValue(obj) as string) ?? (type.GetProperty("NetworkUserId")?.GetValue(obj) as string) ?? text2; } break; } } if (string.IsNullOrEmpty(text)) { return true; } string text3 = SteamIdResolver.FromNetworkUserId(text2) ?? SteamIdResolver.FromPeerId(peerId); if (text[0] != '/') { if (num >= 0) { string text4 = BuildChatPrefix(text3); if (text4 != null) { __args[num] = text4 + text; } } return true; } Player senderPlayer = ((!string.IsNullOrEmpty(text3)) ? SteamIdResolver.OnlinePlayerFor(text3) : null); if ((Object)(object)senderPlayer == (Object)null && !string.IsNullOrEmpty(senderName)) { senderPlayer = ((IEnumerable)Player.GetAllPlayers()).FirstOrDefault((Func)((Player p) => p.GetPlayerName().Equals(senderName, StringComparison.OrdinalIgnoreCase))); } string[] array = text.Split(new char[1] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries); string text5 = array[0].ToLowerInvariant(); string text6 = ((array.Length > 1) ? array[1].Trim() : ""); string[] args = ((text6.Length == 0) ? Array.Empty() : text6.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); switch (text5) { case "/coins": HandleCoins(text3, senderPlayer); return false; case "/donate": DonateFlow.Run(text3, senderName, delegate(string m) { Tell(senderPlayer, m); }); return false; case "/topdonors": TopDonorsFetcher.Fetch(delegate(string m) { Tell(senderPlayer, m); }); return false; case "/shop": HandleShop(text3, senderPlayer); return false; case "/buy": HandleBuy(text3, senderPlayer, args); return false; case "/gift": HandleGift(text3, senderName, senderPlayer, args); return false; case "/title": HandleTitle(text3, senderPlayer, text6); return false; case "/givecoins": HandleGiveCoins(text3, senderPlayer, args); return false; case "/removecoins": HandleRemoveCoins(text3, senderPlayer, args); return false; default: return true; } } catch (Exception arg) { Debug.LogError((object)$"[Valcoin] ChatRpcSlashPatch error: {arg}"); return true; } } private static string BuildChatPrefix(string steam64) { if (string.IsNullOrEmpty(steam64)) { return null; } bool flag = PerkManager.Has(steam64, "donor_badge"); string text = (PerkManager.Has(steam64, "chat_title") ? PerkManager.Title(steam64) : null); if (!flag && string.IsNullOrEmpty(text)) { return null; } StringBuilder stringBuilder = new StringBuilder(8 + (text?.Length ?? 0)); if (flag) { stringBuilder.Append("⭐ "); } if (!string.IsNullOrEmpty(text)) { stringBuilder.Append('[').Append(text).Append("] "); } return stringBuilder.ToString(); } private static void HandleCoins(string steam64, Player p) { if (string.IsNullOrEmpty(steam64)) { Tell(p, "⚠\ufe0f Couldn't resolve your Steam ID."); return; } int balance = CoinManager.GetBalance(steam64); PerkManager.PlayerPerks playerPerks = PerkManager.Get(steam64); string text = $"\ud83d\udcb0 You have {balance} Valcoins."; if (playerPerks.perks.Count > 0) { text = text + " Perks: " + string.Join(", ", playerPerks.perks); } Tell(p, text); } private static void HandleShop(string steam64, Player p) { if (Catalog.Order.Count == 0) { Tell(p, "\ud83d\uded2 The shop is empty. Ask an admin to set up valcoin_shop.yaml."); return; } int num = ((!string.IsNullOrEmpty(steam64)) ? CoinManager.GetBalance(steam64) : 0); Tell(p, $"\ud83d\uded2 Shop (your balance: {num} Valcoins)"); foreach (Catalog.Sku item in Catalog.Order) { string text = ""; if (item.Effect == "grant_item") { if (item.WeeklyCap > 0) { text += $" [max {item.WeeklyCap}/week]"; } if (!string.IsNullOrEmpty(item.RequiresBoss)) { text = text + " [needs " + item.RequiresBoss + "]"; } } else if (!string.IsNullOrEmpty(steam64)) { if (item.Effect == "grant_perk" && PerkManager.Has(steam64, item.Perk)) { text = " [owned]"; } else if (item.Effect == "add_charges") { text = $" [you have {PerkManager.Charges(steam64, item.Perk)}]"; } } Tell(p, $" {item.Id} · {item.Price}c · {item.Name}{text}"); if (!string.IsNullOrEmpty(item.Description)) { Tell(p, " " + item.Description); } } Tell(p, "Buy with: /buy "); } private static void HandleBuy(string steam64, Player p, string[] args) { if (args.Length != 1) { Tell(p, "⚠\ufe0f Usage: /buy (see /shop)"); return; } Player capturedPlayer = p; ShopHandler.Buy(steam64, args[0].ToLowerInvariant(), delegate(string msg) { Tell(capturedPlayer, msg); }); } private static void HandleGift(string fromSteam64, string fromName, Player fromPlayer, string[] args) { if (args.Length != 2 || !int.TryParse(args[1], out var result)) { Tell(fromPlayer, "⚠\ufe0f Usage: /gift "); return; } GiftFlow.Run(fromSteam64, fromName, args[0], result, delegate(string m) { Tell(fromPlayer, m); }); } private static void HandleTitle(string steam64, Player p, string rest) { if (string.IsNullOrEmpty(steam64)) { Tell(p, "⚠\ufe0f Couldn't resolve your Steam ID."); } else if (!PerkManager.Has(steam64, "chat_title")) { Tell(p, "\ud83d\udd12 You need the \"chat_title\" perk. /buy chat_title"); } else if (string.IsNullOrEmpty(rest)) { string text = PerkManager.Title(steam64); Tell(p, string.IsNullOrEmpty(text) ? "Usage: /title (or /title clear)" : ("Your title is currently: [" + text + "] Change with /title , remove with /title clear")); } else if (rest.Equals("clear", StringComparison.OrdinalIgnoreCase) || rest == "-") { PerkManager.SetTitle(steam64, null); Tell(p, "✅ Title cleared."); } else if (rest.Length > 16) { Tell(p, "⚠\ufe0f Title must be 16 characters or fewer."); } else if (!Regex.IsMatch(rest, "^[\\w \\-'\\.!?]+$")) { Tell(p, "⚠\ufe0f Letters, numbers and basic punctuation only."); } else { PerkManager.SetTitle(steam64, rest); Tell(p, "✅ Title set to [" + rest + "]."); } } private static void HandleGiveCoins(string callerSteam64, Player caller, string[] args) { if (string.IsNullOrEmpty(callerSteam64) || !Plugin.AdminSteamIDs.Contains(callerSteam64)) { Tell(caller, "\ud83d\udeab You are not authorized."); return; } if (args.Length != 2 || !int.TryParse(args[1], out var result) || result <= 0) { Tell(caller, "⚠\ufe0f Usage: /givecoins "); return; } if (!ResolveTargetByName(args[0], out var steam, out var player)) { Tell(caller, "⚠\ufe0f Player not found or their Steam ID couldn't be resolved."); return; } CoinManager.AddCoins(steam, result); Tell(caller, $"✅ Gave {result} Valcoins to {args[0]}."); if ((Object)(object)player != (Object)null) { ((Character)player).Message((MessageType)1, $"\ud83c\udf81 +{result} Valcoins from admin!", 0, (Sprite)null); } } private static void HandleRemoveCoins(string callerSteam64, Player caller, string[] args) { if (string.IsNullOrEmpty(callerSteam64) || !Plugin.AdminSteamIDs.Contains(callerSteam64)) { Tell(caller, "\ud83d\udeab You are not authorized."); return; } if (args.Length != 2 || !int.TryParse(args[1], out var result) || result <= 0) { Tell(caller, "⚠\ufe0f Usage: /removecoins "); return; } if (!ResolveTargetByName(args[0], out var steam, out var player)) { Tell(caller, "⚠\ufe0f Player not found or their Steam ID couldn't be resolved."); return; } int num = Math.Max(0, CoinManager.GetBalance(steam) - result); CoinManager.SetBalance(steam, num); Tell(caller, $"✅ Removed {result} from {args[0]} (new balance: {num})."); if ((Object)(object)player != (Object)null) { ((Character)player).Message((MessageType)1, $"❌ {result} Valcoins removed by admin.", 0, (Sprite)null); } } private static bool ResolveTargetByName(string name, out string steam64, out Player player) { steam64 = null; player = null; if ((Object)(object)ZNet.instance == (Object)null) { return false; } ZNetPeer val = ((IEnumerable)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func)((ZNetPeer p) => p.m_playerName != null && p.m_playerName.Equals(name, StringComparison.OrdinalIgnoreCase))); if (val == null) { return false; } steam64 = SteamIdResolver.FromPeer(val); if (string.IsNullOrEmpty(steam64)) { return false; } player = ((IEnumerable)Player.GetAllPlayers()).FirstOrDefault((Func)((Player pp) => pp.GetPlayerName().Equals(name, StringComparison.OrdinalIgnoreCase))); return true; } private static void Tell(Player player, string msg) { if ((Object)(object)player != (Object)null) { ((Character)player).Message((MessageType)1, msg, 0, (Sprite)null); } else { Debug.Log((object)("[Valcoin] " + msg)); } } } public class SharedCoroutineRunner : MonoBehaviour { private static SharedCoroutineRunner _instance; public static SharedCoroutineRunner Instance { get { //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_001d: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("ValcoinCoroutineRunner"); Object.DontDestroyOnLoad((Object)val); _instance = val.AddComponent(); } return _instance; } } } public static class CoinManager { private class State { public Dictionary balances = new Dictionary(); public List recentGrants = new List(); } private static readonly string SaveDir = Path.Combine(Paths.ConfigPath, "valcoin_data"); private static readonly string SaveFile = Path.Combine(SaveDir, "coin_balances.json"); private const int RecentGrantCap = 5000; private static State _state = new State(); private static HashSet _seen = new HashSet(); public static void Load() { try { Directory.CreateDirectory(SaveDir); if (!File.Exists(SaveFile)) { return; } string text = File.ReadAllText(SaveFile); State state; try { state = JsonConvert.DeserializeObject(text); if (state == null || state.balances == null) { throw new Exception("not new shape"); } } catch { Dictionary balances = JsonConvert.DeserializeObject>(text) ?? new Dictionary(); state = new State { balances = balances }; } _state = state; State state2 = _state; if (state2.recentGrants == null) { state2.recentGrants = new List(); } _seen = new HashSet(_state.recentGrants); } catch (Exception ex) { Debug.LogError((object)("[CoinManager] Failed to load: " + ex.Message)); _state = new State(); _seen = new HashSet(); } } public static void Save() { try { File.WriteAllText(SaveFile, JsonConvert.SerializeObject((object)_state, (Formatting)1)); } catch (Exception ex) { Debug.LogError((object)("[CoinManager] Failed to save: " + ex.Message)); } } public static int GetBalance(string steamId) { if (!_state.balances.TryGetValue(steamId, out var value)) { return 0; } return value; } public static void AddCoins(string steamId, int amount) { _state.balances[steamId] = GetBalance(steamId) + amount; Save(); } public static void SetBalance(string steamId, int amount) { _state.balances[steamId] = Math.Max(0, amount); Save(); } public static bool TryApplyGrant(long grantId, string steamId, int amount) { if (_seen.Contains(grantId)) { return false; } _seen.Add(grantId); _state.recentGrants.Add(grantId); if (_state.recentGrants.Count > 5000) { int count = _state.recentGrants.Count - 5000; List range = _state.recentGrants.GetRange(0, count); _state.recentGrants.RemoveRange(0, count); foreach (long item in range) { _seen.Remove(item); } } AddCoins(steamId, amount); return true; } } public static class Config { public static string BackendUrl { get; private set; } public static string PluginToken { get; private set; } public static float PollIntervalSeconds { get; private set; } = 10f; public static string UiToggleKey { get; private set; } = "F8"; public static string CodexToggleKey { get; private set; } = "F4"; public static bool WelcomeEnabled { get; private set; } = true; public static string WelcomeMessage { get; private set; } public static bool Ready { get { if (!string.IsNullOrEmpty(BackendUrl)) { return !string.IsNullOrEmpty(PluginToken); } return false; } } public static void Load() { BackendUrl = Environment.GetEnvironmentVariable("VALCOIN_BACKEND_URL"); PluginToken = Environment.GetEnvironmentVariable("VALCOIN_PLUGIN_TOKEN"); try { string text = Path.Combine(Paths.ConfigPath, "valcoin_config.json"); if (File.Exists(text)) { JObject val = JObject.Parse(File.ReadAllText(text)); BackendUrl = (string.IsNullOrEmpty(BackendUrl) ? ((string)val["backend_url"]) : BackendUrl); PluginToken = (string.IsNullOrEmpty(PluginToken) ? ((string)val["plugin_token"]) : PluginToken); if (val["poll_interval_seconds"] != null) { PollIntervalSeconds = (float)val["poll_interval_seconds"]; } if (val["ui_toggle_key"] != null) { UiToggleKey = (string)val["ui_toggle_key"]; } if (val["codex_toggle_key"] != null) { CodexToggleKey = (string)val["codex_toggle_key"]; } if (val["welcome_message_enabled"] != null) { WelcomeEnabled = (bool)val["welcome_message_enabled"]; } if (val["welcome_message"] != null) { WelcomeMessage = (string)val["welcome_message"]; } } else { File.WriteAllText(text, "{\n \"backend_url\": \"https://your-app.fly.dev\",\n \"plugin_token\": \"paste-the-PLUGIN_TOKEN-from-your-fly-secrets\",\n \"poll_interval_seconds\": 10,\n\n \"ui_toggle_key\": \"F8\",\n \"codex_toggle_key\": \"F4\",\n \"welcome_message_enabled\": true,\n \"welcome_message\": null\n}\n"); Debug.LogWarning((object)("[Valcoin] Created template config at " + text + ". Fill in backend_url + plugin_token.")); } } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Failed to load config: " + ex.Message)); } if (!Ready) { Debug.LogWarning((object)"[Valcoin] Backend not configured; /donate and grant polling are disabled."); } } } public class DonationCodex : MonoBehaviour { private enum Section { Overview, Perks, Patrons, Donate } private class StateResp { public int balance; public TopEntry[] top_donors; } private class TopEntry { public int rank; public string name; public int total_coins; } private const int PanelW = 560; private const int PanelH = 520; private Section _section; private bool _open; private KeyCode _toggleKey = (KeyCode)285; private bool _online; private int _balance = -1; private List _patrons = new List(); private float _lastFetch; private const float RefreshSeconds = 20f; private Vector2 _scroll; private GUIStyle _bg; private GUIStyle _hdr; private GUIStyle _sub; private GUIStyle _btn; private GUIStyle _btnActive; private GUIStyle _btnDim; private GUIStyle _line; private GUIStyle _label; private GUIStyle _pillOn; private GUIStyle _pillOff; private bool _stylesReady; private bool _wasOpen; private void Awake() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(Config.CodexToggleKey) && Enum.TryParse(Config.CodexToggleKey, ignoreCase: true, out KeyCode result)) { _toggleKey = result; } _online = Config.Ready; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); } private void Update() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (Input.GetKeyDown(_toggleKey)) { Toggle(); } if (_open) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; DonationUiState.SetMouseCapture(value: false); } else if (_wasOpen) { DonationUiState.SetMouseCapture(value: true); } if (_open != _wasOpen) { DonationUiState.CodexOpen = _open; _wasOpen = _open; } } private void OnDestroy() { DonationUiState.CodexOpen = false; } private void Toggle() { _open = !_open; if (_open) { RefreshSoon(force: true); } } private void RefreshSoon(bool force = false) { if (!Config.Ready) { _online = false; } else if (force || !(Time.realtimeSinceStartup - _lastFetch < 20f)) { _lastFetch = Time.realtimeSinceStartup; ((MonoBehaviour)this).StartCoroutine(Fetch()); } } private IEnumerator Fetch() { string steam64 = ResolveLocalSteam64(); string path = (string.IsNullOrEmpty(steam64) ? "/api/leaderboard/top?limit=5" : ("/api/state/" + steam64 + "?top=5")); yield return BackendClient.Get(path, delegate(bool ok, StateResp r, string err) { _online = ok && r != null; if (_online) { if (r.balance != 0 || !string.IsNullOrEmpty(steam64)) { _balance = r.balance; } _patrons = ((r.top_donors != null) ? new List(r.top_donors) : new List()); } }); } private string ResolveLocalSteam64() { try { object obj = Type.GetType("ZSteamMatchmaking, assembly_valheim")?.GetField("instance", BindingFlags.Static | BindingFlags.Public)?.GetValue(null); return (obj?.GetType().GetMethod("GetSteamID")?.Invoke(obj, null))?.ToString(); } catch { return null; } } private void OnGUI() { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_0209: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Unknown result type (might be due to invalid IL or missing references) //IL_0222: Unknown result type (might be due to invalid IL or missing references) if (!_open) { return; } if (!_stylesReady) { InitStyles(); } if (Menu.IsVisible() || ((Object)(object)InventoryGui.instance != (Object)null && InventoryGui.IsVisible()) || ((Object)(object)Minimap.instance != (Object)null && Minimap.IsOpen())) { _open = false; return; } Rect val = default(Rect); ((Rect)(ref val))..ctor((float)(Screen.width - 560) / 2f, (float)(Screen.height - 520) / 2f, 560f, 520f); GUI.Box(val, GUIContent.none, _bg); GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 14f, ((Rect)(ref val)).y + 14f, ((Rect)(ref val)).width - 28f, ((Rect)(ref val)).height - 28f)); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Donation Codex", _hdr, Array.Empty()); GUILayout.FlexibleSpace(); GUILayout.Label(_online ? "● Live" : "● Offline", _online ? _pillOn : _pillOff, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(22f) }); GUILayout.Space(6f); if (GUILayout.Button("✕", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(30f) })) { _open = false; } GUILayout.EndHorizontal(); GUILayout.Label(_online ? ("Balance: " + ((_balance < 0) ? "—" : _balance.ToString()) + " Valcoins") : "The donation service isn't connected yet — everything here still works to browse; it activates once the operator brings it online.", _sub, Array.Empty()); DrawHr(); GUILayout.BeginHorizontal(Array.Empty()); NavButton("Overview", Section.Overview); NavButton("Perks & Shop", Section.Perks); NavButton("Patrons", Section.Patrons); NavButton("Donate", Section.Donate); GUILayout.EndHorizontal(); DrawHr(); _scroll = GUILayout.BeginScrollView(_scroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(true) }); switch (_section) { case Section.Overview: DrawOverview(); break; case Section.Perks: DrawPerks(); break; case Section.Patrons: DrawPatrons(); break; case Section.Donate: DrawDonate(); break; } GUILayout.EndScrollView(); GUILayout.EndArea(); } private void NavButton(string label, Section s) { if (GUILayout.Button(label, (_section == s) ? _btnActive : _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { _section = s; } } private void DrawOverview() { //IL_00e6: Unknown result type (might be due to invalid IL or missing references) GUILayout.Label("Support the realm", _hdr, Array.Empty()); GUILayout.Label("This server is kept alight by its patrons. Donating is always optional — playing is free, and perks are cosmetic or weekly-limited supplies, never power.", _label, Array.Empty()); GUILayout.Space(8f); GUILayout.Label("How Valcoins work", _sub, Array.Empty()); Bullet("Run /donate to get a code + a portal link."); Bullet("Donate through the portal; Valcoins are credited automatically."); Bullet("Spend them in Perks & Shop; check your balance any time."); GUILayout.Space(8f); GUILayout.Label("Commands", _sub, Array.Empty()); Bullet("/donate — get your donation code + link"); Bullet("/coins — show your Valcoin balance + perks"); Bullet("/shop — list everything for sale"); Bullet("/buy — purchase an item"); Bullet("/gift — share Valcoins"); Bullet("/topdonors — the patron leaderboard"); Bullet("/title — set a chat title (with the perk)"); GUILayout.Space(8f); GUILayout.Label($"Press {_toggleKey} any time to open this Codex.", _label, Array.Empty()); } private void DrawPerks() { if (Catalog.Order.Count == 0) { GUILayout.Label("The shop is empty — the operator hasn't set up valcoin_shop.yaml yet.", _label, Array.Empty()); return; } string text = ResolveLocalSteam64(); foreach (Catalog.Sku item in Catalog.Order) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label($"{item.Name} · {item.Price}c", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); if (item.Effect == "grant_perk" && !string.IsNullOrEmpty(text) && PerkManager.Has(text, item.Perk)) { GUILayout.Label("✓ owned", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) }); } else if (_online) { if (GUILayout.Button("Buy", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) })) { RpcLayer.SendAction("buy:" + item.Id); } } else { GUILayout.Label("Buy", _btnDim, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) }); } GUILayout.EndHorizontal(); if (item.Effect == "grant_item") { string text2 = ""; if (item.WeeklyCap > 0) { text2 += $"max {item.WeeklyCap}/week"; } if (!string.IsNullOrEmpty(item.RequiresBoss)) { text2 = text2 + ((text2.Length > 0) ? " · " : "") + "needs " + item.RequiresBoss; } if (text2.Length > 0) { GUILayout.Label(" " + text2, _label, Array.Empty()); } } if (!string.IsNullOrEmpty(item.Description)) { GUILayout.Label(" " + item.Description, _label, Array.Empty()); } GUILayout.Space(6f); } if (!_online) { DrawHr(); GUILayout.Label("Purchasing activates once the donation service is online. You can browse the full catalog now.", _sub, Array.Empty()); } } private void DrawPatrons() { GUILayout.Label("Top Patrons", _hdr, Array.Empty()); GUILayout.Space(4f); if (!_online) { GUILayout.Label("The patron leaderboard appears here once the donation service is online.", _label, Array.Empty()); return; } if (_patrons.Count == 0) { GUILayout.Label("No patrons yet — be the first. Head to the Donate tab!", _label, Array.Empty()); return; } foreach (TopEntry patron in _patrons) { GUILayout.Label(string.Format(" {0}. {1} — {2} coins", patron.rank, patron.name ?? "Anonymous", patron.total_coins), _label, Array.Empty()); } GUILayout.Space(8f); if (GUILayout.Button("Refresh", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { _patrons.Clear(); RefreshSoon(force: true); } } private void DrawDonate() { GUILayout.Label("Make a donation", _hdr, Array.Empty()); GUILayout.Label("Get a personal code, donate through the portal link, and your Valcoins are credited automatically within a few seconds.", _label, Array.Empty()); GUILayout.Space(8f); if (_online) { if (GUILayout.Button("\ud83c\udf81 Get my donation code", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { RpcLayer.SendAction("donate"); } GUILayout.Space(4f); GUILayout.Label("Your code + link will appear in the top-left message area.", _label, Array.Empty()); } else { GUILayout.Label("Donations aren't connected yet.", _sub, Array.Empty()); GUILayout.Label("Once the operator brings the donation service online, this button will hand you a code and a link — no update needed on your side.", _label, Array.Empty()); } } private void Bullet(string text) { GUILayout.Label("• " + text, _label, Array.Empty()); } private void DrawHr() { GUILayout.Box(GUIContent.none, _line, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(1f), GUILayout.ExpandWidth(true) }); GUILayout.Space(4f); } private void InitStyles() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: 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_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Expected O, but got Unknown //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Expected O, but got Unknown //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Expected O, but got Unknown //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01af: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Expected O, but got Unknown //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: 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_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Expected O, but got Unknown //IL_0225: Unknown result type (might be due to invalid IL or missing references) //IL_023a: Unknown result type (might be due to invalid IL or missing references) //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0247: Unknown result type (might be due to invalid IL or missing references) //IL_0253: Expected O, but got Unknown //IL_026d: Unknown result type (might be due to invalid IL or missing references) //IL_027e: Unknown result type (might be due to invalid IL or missing references) //IL_0288: Expected O, but got Unknown //IL_02a2: Unknown result type (might be due to invalid IL or missing references) _bg = new GUIStyle(GUI.skin.box); _bg.normal.background = SolidTex(new Color(0.08f, 0.07f, 0.05f, 0.97f)); _bg.padding = new RectOffset(12, 12, 12, 12); _hdr = new GUIStyle(GUI.skin.label) { fontSize = 16, fontStyle = (FontStyle)1 }; _hdr.normal.textColor = new Color(0.85f, 0.7f, 0.4f); _sub = new GUIStyle(GUI.skin.label) { fontSize = 12, fontStyle = (FontStyle)2, wordWrap = true }; _sub.normal.textColor = new Color(0.75f, 0.72f, 0.62f); _btn = new GUIStyle(GUI.skin.button) { fontSize = 13 }; _btn.padding = new RectOffset(10, 10, 6, 6); _btnActive = new GUIStyle(_btn); _btnActive.normal.background = SolidTex(new Color(0.6f, 0.45f, 0.2f, 1f)); _btnActive.normal.textColor = Color.white; _btnDim = new GUIStyle(_btn); _btnDim.normal.textColor = new Color(0.5f, 0.48f, 0.42f); _line = new GUIStyle(); _line.normal.background = SolidTex(new Color(0.3f, 0.25f, 0.18f, 0.6f)); _label = new GUIStyle(GUI.skin.label) { fontSize = 12, wordWrap = true }; _label.normal.textColor = new Color(0.88f, 0.85f, 0.78f); _pillOn = new GUIStyle(GUI.skin.label) { fontSize = 12, fontStyle = (FontStyle)1 }; _pillOn.normal.textColor = new Color(0.5f, 0.85f, 0.45f); _pillOff = new GUIStyle(_pillOn); _pillOff.normal.textColor = new Color(0.85f, 0.6f, 0.3f); _stylesReady = true; } private static Texture2D SolidTex(Color c) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000a: 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_0017: Expected O, but got Unknown Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, c); val.Apply(); return val; } } public class DonationPanel : MonoBehaviour { private enum Tab { Donate, Shop, Gift, Top } private class StateResp { public int balance; public TopEntry[] top_donors; public int donor_count; public int total_donated_coins; } private class TopEntry { public int rank; public string name; public int total_coins; } private const int PanelW = 520; private const int PanelH = 480; private Tab _tab; private bool _open; private KeyCode _toggleKey = (KeyCode)289; private int _balance; private List _perks = new List(); private List _topDonors = new List(); private readonly List _log = new List(); private const int LogCap = 12; private Vector2 _logScroll; private string _giftTo = ""; private string _giftAmount = ""; private string _titleText = ""; private GUIStyle _bg; private GUIStyle _hdr; private GUIStyle _btn; private GUIStyle _btnActive; private GUIStyle _btnDim; private GUIStyle _line; private GUIStyle _logLine; private GUIStyle _label; private bool _stylesReady; private float _lastStateFetch; private const float StateRefreshSeconds = 15f; private bool _online; private const float AutoRefreshSeconds = 20f; private bool _wasOpen; private Vector2 _shopScroll; private void Awake() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(Config.UiToggleKey) && Enum.TryParse(Config.UiToggleKey, ignoreCase: true, out KeyCode result)) { _toggleKey = result; } RpcLayer.OnPanelMessage = (Action)Delegate.Combine(RpcLayer.OnPanelMessage, new Action(AddLog)); Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); } private void OnDestroy() { RpcLayer.OnPanelMessage = (Action)Delegate.Remove(RpcLayer.OnPanelMessage, new Action(AddLog)); DonationUiState.PanelOpen = false; } private void Update() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (Input.GetKeyDown(_toggleKey)) { Toggle(); } if (_open) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; DonationUiState.SetMouseCapture(value: false); RefreshStateSoon(); } else if (_wasOpen) { DonationUiState.SetMouseCapture(value: true); } if (_open != _wasOpen) { DonationUiState.PanelOpen = _open; _wasOpen = _open; } } private void Toggle() { _open = !_open; if (_open) { RefreshStateSoon(force: true); } } private void RefreshStateSoon(bool force = false) { if (!Config.Ready) { _online = false; return; } float num = (force ? 1f : 20f); if (!(Time.realtimeSinceStartup - _lastStateFetch < num)) { _lastStateFetch = Time.realtimeSinceStartup; ((MonoBehaviour)this).StartCoroutine(FetchState()); } } private IEnumerator FetchState() { string text = ResolveLocalSteam64(); if (string.IsNullOrEmpty(text)) { yield break; } yield return BackendClient.Get("/api/state/" + text + "?top=5", delegate(bool ok, StateResp r, string err) { _online = ok && r != null; if (_online) { _balance = r.balance; _topDonors = ((r.top_donors != null) ? new List(r.top_donors) : new List()); } }); } private string ResolveLocalSteam64() { try { object obj = Type.GetType("ZSteamMatchmaking, assembly_valheim")?.GetField("instance", BindingFlags.Static | BindingFlags.Public)?.GetValue(null); return (obj?.GetType().GetMethod("GetSteamID")?.Invoke(obj, null))?.ToString(); } catch { return null; } } private void InitStyles() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Expected O, but got Unknown //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Expected O, but got Unknown //IL_0189: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Expected O, but got Unknown //IL_01d6: Unknown result type (might be due to invalid IL or missing references) //IL_01eb: Unknown result type (might be due to invalid IL or missing references) //IL_01f0: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Expected O, but got Unknown //IL_0217: Unknown result type (might be due to invalid IL or missing references) _bg = new GUIStyle(GUI.skin.box); _bg.normal.background = SolidTex(new Color(0.08f, 0.07f, 0.05f, 0.97f)); _bg.padding = new RectOffset(12, 12, 12, 12); _hdr = new GUIStyle(GUI.skin.label) { fontSize = 16, fontStyle = (FontStyle)1 }; _hdr.normal.textColor = new Color(0.85f, 0.7f, 0.4f); _btn = new GUIStyle(GUI.skin.button) { fontSize = 13 }; _btn.padding = new RectOffset(10, 10, 6, 6); _btnActive = new GUIStyle(_btn); _btnActive.normal.background = SolidTex(new Color(0.6f, 0.45f, 0.2f, 1f)); _btnActive.normal.textColor = Color.white; _btnDim = new GUIStyle(_btn); _btnDim.normal.textColor = new Color(0.5f, 0.48f, 0.42f); _line = new GUIStyle(); _line.normal.background = SolidTex(new Color(0.3f, 0.25f, 0.18f, 0.6f)); _logLine = new GUIStyle(GUI.skin.label) { fontSize = 12, wordWrap = true }; _logLine.normal.textColor = new Color(0.9f, 0.9f, 0.85f); _label = new GUIStyle(GUI.skin.label) { fontSize = 12 }; _label.normal.textColor = new Color(0.88f, 0.85f, 0.78f); _stylesReady = true; } private static Texture2D SolidTex(Color c) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000a: 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_0017: Expected O, but got Unknown Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, c); val.Apply(); return val; } private void OnGUI() { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) if (!_open) { return; } if (!_stylesReady) { InitStyles(); } if (Menu.IsVisible() || ((Object)(object)InventoryGui.instance != (Object)null && InventoryGui.IsVisible()) || ((Object)(object)Minimap.instance != (Object)null && Minimap.IsOpen())) { _open = false; return; } Rect val = default(Rect); ((Rect)(ref val))..ctor((float)(Screen.width - 520) / 2f, (float)(Screen.height - 480) / 2f, 520f, 480f); GUI.Box(val, GUIContent.none, _bg); GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 12f, ((Rect)(ref val)).y + 12f, ((Rect)(ref val)).width - 24f, ((Rect)(ref val)).height - 24f)); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Valheim Donations", _hdr, Array.Empty()); GUILayout.FlexibleSpace(); if (GUILayout.Button("✕", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(30f) })) { _open = false; } GUILayout.EndHorizontal(); GUILayout.Label($"Balance: {_balance} Valcoins", _label, Array.Empty()); string text = string.Join(", ", PerksForLocalPlayer()); if (!string.IsNullOrEmpty(text)) { GUILayout.Label("Perks: " + text, _label, Array.Empty()); } if (!_online) { GUILayout.Label("\ud83d\udd0c Donation service offline — browse now; it activates when the operator connects it.", _label, Array.Empty()); } DrawHr(); GUILayout.BeginHorizontal(Array.Empty()); TabButton("Donate", Tab.Donate); TabButton("Shop", Tab.Shop); TabButton("Gift", Tab.Gift); TabButton("Top", Tab.Top); GUILayout.EndHorizontal(); DrawHr(); switch (_tab) { case Tab.Donate: DrawDonate(); break; case Tab.Shop: DrawShop(); break; case Tab.Gift: DrawGift(); break; case Tab.Top: DrawTop(); break; } DrawHr(); DrawLog(); GUILayout.EndArea(); } private void TabButton(string label, Tab t) { if (GUILayout.Button(label, (_tab == t) ? _btnActive : _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { _tab = t; } } private void DrawHr() { GUILayout.Box(GUIContent.none, _line, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(1f), GUILayout.ExpandWidth(true) }); GUILayout.Space(4f); } private void DrawDonate() { GUILayout.Label("Get a donation code, then donate via the link.\nFunds turn into Valcoins automatically.", _label, Array.Empty()); GUILayout.Space(6f); if (_online) { if (GUILayout.Button("\ud83c\udf81 Get my donation code", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) })) { RpcLayer.SendAction("donate"); } } else { GUILayout.Label("\ud83c\udf81 Get my donation code", _btnDim, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) }); } } private void DrawShop() { //IL_0024: 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) if (Catalog.Order.Count == 0) { GUILayout.Label("Shop is empty — ask the operator to set up valcoin_shop.yaml.", _label, Array.Empty()); return; } _shopScroll = GUILayout.BeginScrollView(_shopScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(true) }); string text = ResolveLocalSteam64(); foreach (Catalog.Sku item in Catalog.Order) { GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label($"{item.Name} — {item.Price}c", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); bool flag = item.Effect == "grant_perk" && !string.IsNullOrEmpty(text) && PerkManager.Has(text, item.Perk); int num = ((item.Effect == "add_charges" && !string.IsNullOrEmpty(text)) ? PerkManager.Charges(text, item.Perk) : 0); if (flag) { GUILayout.Label("✓ owned", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) }); } else if (num > 0) { GUILayout.Label($"x{num} held", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) }); } if (!flag) { if (_online) { if (GUILayout.Button("Buy", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) })) { RpcLayer.SendAction("buy:" + item.Id); } } else { GUILayout.Label("Buy", _btnDim, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(70f) }); } } GUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(item.Description)) { GUILayout.Label(" " + item.Description, _label, Array.Empty()); } GUILayout.Space(4f); } GUILayout.EndScrollView(); } private void DrawGift() { GUILayout.Label("Send Valcoins to another player on the server.", _label, Array.Empty()); GUILayout.Space(4f); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("To:", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(50f) }); _giftTo = GUILayout.TextField(_giftTo ?? "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(200f) }); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.Label("Amount:", _label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(50f) }); _giftAmount = GUILayout.TextField(_giftAmount ?? "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) }); GUILayout.EndHorizontal(); GUILayout.Space(6f); if (_online) { if (GUILayout.Button("\ud83c\udf81 Send gift", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) })) { if (string.IsNullOrWhiteSpace(_giftTo) || string.IsNullOrWhiteSpace(_giftAmount)) { AddLog("⚠\ufe0f Fill in both fields."); } else { RpcLayer.SendAction("gift:" + _giftTo.Trim() + ":" + _giftAmount.Trim()); } } } else { GUILayout.Label("\ud83c\udf81 Send gift", _btnDim, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(28f) }); } string text = ResolveLocalSteam64(); if (!string.IsNullOrEmpty(text) && PerkManager.Has(text, "chat_title")) { GUILayout.Space(10f); GUILayout.Label("Chat title (clear with empty + Set):", _label, Array.Empty()); _titleText = GUILayout.TextField(_titleText ?? "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(200f) }); if (GUILayout.Button("Set title", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) })) { RpcLayer.SendAction("title:" + (string.IsNullOrWhiteSpace(_titleText) ? "clear" : _titleText.Trim())); } } } private void DrawTop() { GUILayout.Label("Lifetime donor leaderboard:", _label, Array.Empty()); GUILayout.Space(4f); if (_topDonors.Count == 0) { GUILayout.Label("(none yet — be the first!)", _label, Array.Empty()); } else { foreach (TopEntry topDonor in _topDonors) { GUILayout.Label(string.Format(" {0}. {1} — {2} coins", topDonor.rank, topDonor.name ?? "Anonymous", topDonor.total_coins), _label, Array.Empty()); } } GUILayout.Space(6f); if (GUILayout.Button("Refresh", _btn, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { _topDonors.Clear(); RefreshStateSoon(); } } private void DrawLog() { //IL_0025: 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_0042: Unknown result type (might be due to invalid IL or missing references) if (_log.Count != 0) { GUILayout.Label("Messages", _label, Array.Empty()); _logScroll = GUILayout.BeginScrollView(_logScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(80f) }); for (int num = _log.Count - 1; num >= 0; num--) { GUILayout.Label("• " + _log[num], _logLine, Array.Empty()); } GUILayout.EndScrollView(); } } private IEnumerable PerksForLocalPlayer() { string s = ResolveLocalSteam64(); if (!string.IsNullOrEmpty(s)) { if (PerkManager.Has(s, "donor_badge")) { yield return "donor_badge"; } if (PerkManager.Has(s, "chat_title")) { yield return "chat_title"; } if (PerkManager.Has(s, "companion_flair")) { yield return "companion_flair"; } } } private void AddLog(string msg) { _log.Add(msg); if (_log.Count > 12) { _log.RemoveAt(0); } RefreshStateSoon(); } } public static class DonationUiState { public static bool CodexOpen; public static bool PanelOpen; private static FieldInfo _mouseCapture; public static bool AnyOpen { get { if (!CodexOpen) { return PanelOpen; } return true; } } public static void SetMouseCapture(bool value) { GameCamera instance = GameCamera.instance; if (!((Object)(object)instance == (Object)null)) { if (_mouseCapture == null) { _mouseCapture = typeof(GameCamera).GetField("m_mouseCapture", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } _mouseCapture?.SetValue(instance, value); } } } [HarmonyPatch(typeof(Player), "TakeInput")] internal static class DonationPlayerTakeInputPatch { private static void Postfix(ref bool __result) { if (DonationUiState.AnyOpen) { __result = false; } } } [HarmonyPatch(typeof(PlayerController), "TakeInput", new Type[] { typeof(bool) })] internal static class DonationPlayerControllerTakeInputPatch { private static void Postfix(ref bool __result) { if (DonationUiState.AnyOpen) { __result = false; } } } public static class DonateFlow { private class ClaimResp { public string code; public string expires_at; public string donation_url; public int ttl_minutes; } public static void Run(string steam64, string senderName, Action reply) { if (string.IsNullOrEmpty(steam64)) { reply("⚠\ufe0f Couldn't resolve your Steam ID."); return; } if (!Config.Ready) { reply("⚠\ufe0f Donations aren't configured on this server."); return; } reply("⌛ Generating your donation code..."); ((MonoBehaviour)SharedCoroutineRunner.Instance).StartCoroutine(BackendClient.Post("/api/claim", new { steam64 = steam64, name = senderName }, delegate(bool ok, ClaimResp r, string err) { if (!ok || r == null) { reply("❌ Couldn't reach the donations service. Try again later."); Debug.LogWarning((object)("[Valcoin] /donate failed: " + err)); } else { reply("\ud83d\udc9b Donate at " + r.donation_url); reply($" Enter code: {r.code} (expires in {r.ttl_minutes} min)"); } })); } } public static class GiftFlow { private class TransferResp { public string status; public int balance; public int transferred; } public static void Run(string fromSteam64, string fromName, string toName, int amount, Action reply) { if (string.IsNullOrEmpty(fromSteam64)) { reply("⚠\ufe0f Couldn't resolve your Steam ID."); return; } if (string.IsNullOrEmpty(toName)) { reply("⚠\ufe0f Specify a recipient."); return; } if (amount <= 0) { reply("⚠\ufe0f Amount must be positive."); return; } if (!ResolveTargetByName(toName, out var steam)) { reply("⚠\ufe0f Player \"" + toName + "\" not found or no Steam ID."); return; } if (steam == fromSteam64) { reply("⚠\ufe0f You can't gift yourself."); return; } int balance = CoinManager.GetBalance(fromSteam64); if (balance < amount) { reply($"\ud83d\udcb8 Not enough Valcoins ({balance} / {amount})."); return; } string idempotency_key = $"gift-{Guid.NewGuid():N}"; ((MonoBehaviour)SharedCoroutineRunner.Instance).StartCoroutine(BackendClient.Post("/api/transfer", new { from_steam64 = fromSteam64, to_steam64 = steam, coins = amount, idempotency_key = idempotency_key, from_name = fromName, to_name = toName }, delegate(bool ok, TransferResp r, string err) { if (!ok || r == null) { reply("❌ Gift failed. (" + (err ?? "unknown") + ")"); } else { CoinManager.SetBalance(fromSteam64, r.balance); reply($"\ud83c\udf81 Sent {amount} Valcoins to {toName}. Your balance: {r.balance}"); } })); } private static bool ResolveTargetByName(string name, out string steam64) { steam64 = null; if ((Object)(object)ZNet.instance == (Object)null) { return false; } foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers()) { if (connectedPeer.m_playerName != null && connectedPeer.m_playerName.Equals(name, StringComparison.OrdinalIgnoreCase)) { steam64 = SteamIdResolver.FromPeer(connectedPeer); return !string.IsNullOrEmpty(steam64); } } return false; } } public static class TopDonorsFetcher { private class TopResp { public Entry[] donors; } private class Entry { public int rank; public string name; public int total_coins; } public static void Fetch(Action emit, int limit = 5) { if (!Config.Ready) { emit("⚠\ufe0f Leaderboard unavailable."); return; } ((MonoBehaviour)SharedCoroutineRunner.Instance).StartCoroutine(BackendClient.Get($"/api/leaderboard/top?limit={limit}", delegate(bool ok, TopResp r, string err) { if (!ok || r?.donors == null) { emit("❌ Couldn't fetch leaderboard. (" + (err ?? "unknown") + ")"); } else if (r.donors.Length == 0) { emit("\ud83c\udfc6 No donors yet. Be the first — type /donate!"); } else { emit("\ud83c\udfc6 Top donors:"); Entry[] donors = r.donors; foreach (Entry entry in donors) { emit(string.Format(" {0}. {1} — {2} coins", entry.rank, entry.name ?? "Anonymous", entry.total_coins)); } } })); } } public class GrantPoller : MonoBehaviour { public class Grant { public long id; public string steam64; public int coins; public string source; public string note; public string created_at; } private class PendingResponse { public List grants; } private class AckRequest { public List ids; } private class AckResponse { public int acked; } private Coroutine _loop; private void Start() { _loop = ((MonoBehaviour)this).StartCoroutine(Loop()); } private void OnDestroy() { if (_loop != null) { ((MonoBehaviour)this).StopCoroutine(_loop); } } private IEnumerator Loop() { while ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { yield return (object)new WaitForSeconds(2f); } while (true) { yield return (object)new WaitForSeconds(Mathf.Max(2f, Config.PollIntervalSeconds)); if (Config.Ready) { yield return Tick(); } } } private IEnumerator Tick() { PendingResponse pending = null; string err = null; yield return BackendClient.Get("/api/grants/pending?limit=50", delegate(bool ok, PendingResponse r, string e) { if (ok) { pending = r; } else { err = e; } }); if (err != null) { Debug.LogWarning((object)("[Valcoin] poll failed: " + err)); } else { if (pending?.grants == null || pending.grants.Count == 0) { yield break; } List list = new List(pending.grants.Count); foreach (Grant grant in pending.grants) { try { bool num = CoinManager.TryApplyGrant(grant.id, grant.steam64, grant.coins); int balance = CoinManager.GetBalance(grant.steam64); if (num) { Player val = SteamIdResolver.OnlinePlayerFor(grant.steam64); if ((Object)(object)val != (Object)null) { ((Character)val).Message((MessageType)1, $"+{grant.coins} Valcoins! Balance: {balance}", 0, (Sprite)null); } else { Debug.Log((object)$"[Valcoin] +{grant.coins} to {grant.steam64} (offline). Balance: {balance}"); } } else { Debug.Log((object)$"[Valcoin] grant {grant.id} replay (already applied locally); will re-ack."); } list.Add(grant.id); } catch (Exception ex) { Debug.LogError((object)$"[Valcoin] failed to apply grant {grant.id}: {ex.Message}"); } } if (list.Count <= 0) { yield break; } yield return BackendClient.Post("/api/grants/ack", new AckRequest { ids = list }, delegate(bool ok, AckResponse r, string e) { if (!ok) { Debug.LogWarning((object)("[Valcoin] ack failed (will retry next tick): " + e)); } }); } } } public static class PerkManager { public class Pos { public float x; public float y; public float z; } public class PlayerPerks { public HashSet perks = new HashSet(); public Dictionary charges = new Dictionary(); public string title; public Pos home; public string homeCooldownUntilUtc; } private class State { public Dictionary players = new Dictionary(); } private static readonly string SaveDir = Path.Combine(Paths.ConfigPath, "valcoin_data"); private static readonly string SaveFile = Path.Combine(SaveDir, "perks.json"); private static State _state = new State(); public static void Load() { try { Directory.CreateDirectory(SaveDir); if (File.Exists(SaveFile)) { _state = JsonConvert.DeserializeObject(File.ReadAllText(SaveFile)) ?? new State(); if (_state.players == null) { _state.players = new Dictionary(); } } } catch (Exception ex) { Debug.LogError((object)("[PerkManager] Failed to load: " + ex.Message)); _state = new State(); } } public static void Save() { try { File.WriteAllText(SaveFile, JsonConvert.SerializeObject((object)_state, (Formatting)1)); } catch (Exception ex) { Debug.LogError((object)("[PerkManager] Failed to save: " + ex.Message)); } } public static PlayerPerks Get(string steam64) { if (string.IsNullOrEmpty(steam64)) { return new PlayerPerks(); } if (!_state.players.TryGetValue(steam64, out var value)) { value = new PlayerPerks(); _state.players[steam64] = value; } return value; } public static bool Has(string steam64, string perk) { if (string.IsNullOrEmpty(steam64)) { return false; } if (_state.players.TryGetValue(steam64, out var value)) { return value.perks.Contains(perk); } return false; } public static void Grant(string steam64, string perk) { Get(steam64).perks.Add(perk); Save(); } public static void AddCharges(string steam64, string perk, int n) { PlayerPerks playerPerks = Get(steam64); playerPerks.charges.TryGetValue(perk, out var value); playerPerks.charges[perk] = value + n; Save(); } public static int Charges(string steam64, string perk) { if (string.IsNullOrEmpty(steam64)) { return 0; } if (!_state.players.TryGetValue(steam64, out var value) || !value.charges.TryGetValue(perk, out var value2)) { return 0; } return value2; } public static bool ConsumeCharge(string steam64, string perk) { PlayerPerks playerPerks = Get(steam64); if (!playerPerks.charges.TryGetValue(perk, out var value) || value <= 0) { return false; } playerPerks.charges[perk] = value - 1; Save(); return true; } public static void SetTitle(string steam64, string title) { Get(steam64).title = title; Save(); } public static string Title(string steam64) { if (!_state.players.TryGetValue(steam64, out var value)) { return null; } return value.title; } public static void SetHome(string steam64, Vector3 pos) { //IL_000c: 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_0024: Unknown result type (might be due to invalid IL or missing references) Get(steam64).home = new Pos { x = pos.x, y = pos.y, z = pos.z }; Save(); } public static Vector3? Home(string steam64) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) if (!_state.players.TryGetValue(steam64, out var value) || value.home == null) { return null; } return new Vector3(value.home.x, value.home.y, value.home.z); } public static int HomeCooldownRemaining(string steam64) { PlayerPerks playerPerks = Get(steam64); if (string.IsNullOrEmpty(playerPerks.homeCooldownUntilUtc)) { return 0; } if (!DateTime.TryParse(playerPerks.homeCooldownUntilUtc, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { return 0; } int val = (int)(result - DateTime.UtcNow).TotalSeconds; return Math.Max(0, val); } public static void StartHomeCooldown(string steam64, int seconds) { Get(steam64).homeCooldownUntilUtc = DateTime.UtcNow.AddSeconds(seconds).ToString("o"); Save(); } } [BepInPlugin("com.taeguk.valheimdonations", "Valheim Donations", "5.1.0")] public class Plugin : BaseUnityPlugin { public static HashSet AdminSteamIDs = new HashSet(); private Harmony _harmony; private static readonly string AdminConfigPath = Path.Combine(Paths.ConfigPath, "valcoin_admins.yaml"); private void Awake() { //IL_0033: 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_0044: Expected O, but got Unknown //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_005a: Expected O, but got Unknown //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Valheim Donations] Plugin loaded"); Config.Load(); EnsureAdminFile(); LoadAdmins(); CoinManager.Load(); PerkManager.Load(); Catalog.Load(); GameObject val = new GameObject("ValcoinGrantPoller"); val.AddComponent(); Object.DontDestroyOnLoad((Object)val); GameObject val2 = new GameObject("ValcoinCatalogSync"); val2.AddComponent(); Object.DontDestroyOnLoad((Object)val2); _harmony = new Harmony("com.taeguk.valheimdonations"); _harmony.PatchAll(); ((MonoBehaviour)this).StartCoroutine(InitRpcsWhenReady()); SpawnClientUiIfNotServer(); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Startup complete. Admins: {AdminSteamIDs.Count}, Backend ready: {Config.Ready}"); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private IEnumerator InitRpcsWhenReady() { while ((Object)(object)ZNet.instance == (Object)null) { yield return null; } bool serverSide = ZNet.instance.IsServer(); yield return RpcLayer.RegisterWhenReady(serverSide); } private void SpawnClientUiIfNotServer() { ((MonoBehaviour)this).StartCoroutine(SpawnUiWhenZnetReady()); } private IEnumerator SpawnUiWhenZnetReady() { while ((Object)(object)ZNet.instance == (Object)null) { yield return null; } if (!ZNet.instance.IsServer() || !ZNet.instance.IsDedicated()) { GameObject val = new GameObject("ValcoinDonationPanel"); val.AddComponent(); Object.DontDestroyOnLoad((Object)val); GameObject val2 = new GameObject("ValcoinDonationCodex"); val2.AddComponent(); Object.DontDestroyOnLoad((Object)val2); } } private static void EnsureAdminFile() { try { Directory.CreateDirectory(Paths.ConfigPath); if (!File.Exists(AdminConfigPath)) { File.WriteAllText(AdminConfigPath, "# Valcoin Admins (Steam64 IDs)\n# ------------------------------------------------------------\n# Add Steam64 IDs here to grant admin permission for:\n# /givecoins \n# /removecoins \n#\n# Find your Steam64 at https://steamid.io\n# Restart the server after changes.\nadmins:\n - 76561198012345678 # <-- replace\n"); Debug.LogWarning((object)("[Valcoin] Created admin file template at: " + AdminConfigPath)); } } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Failed to create admin YAML: " + ex.Message)); } } private static void LoadAdmins() { try { HashSet hashSet = new HashSet(); if (!File.Exists(AdminConfigPath)) { AdminSteamIDs = hashSet; return; } bool flag = false; Regex regex = new Regex("^\\s*-\\s*(\\d{17})\\b", RegexOptions.Compiled); string[] array = File.ReadAllLines(AdminConfigPath); for (int i = 0; i < array.Length; i++) { string text = array[i].TrimEnd(Array.Empty()); if (text.TrimStart(Array.Empty()).StartsWith("#")) { continue; } if (!flag) { if (Regex.IsMatch(text, "^\\s*admins\\s*:\\s*$")) { flag = true; } continue; } Match match = regex.Match(text); if (match.Success) { hashSet.Add(match.Groups[1].Value); } else if (Regex.IsMatch(text, "^\\s*\\w+\\s*:\\s*$")) { break; } } AdminSteamIDs = hashSet; Debug.Log((object)$"[Valcoin] Loaded {hashSet.Count} admin Steam64 ID(s)."); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Failed to load admin YAML: " + ex.Message)); AdminSteamIDs = new HashSet(); } } } public static class RpcLayer { public const string ActionRpc = "vc_action"; public const string PanelRpc = "vc_panel"; public const string CatalogRpc = "vc_catalog"; private static bool _registeredServer; private static bool _registeredClient; public static Action OnPanelMessage; public static IEnumerator RegisterWhenReady(bool serverSide) { while (ZRoutedRpc.instance == null) { yield return null; } if (serverSide && !_registeredServer) { ZRoutedRpc.instance.Register("vc_action", (Action)HandleActionOnServer); _registeredServer = true; Debug.Log((object)"[Valcoin] RPC registered (server): vc_action"); } if (!serverSide && !_registeredClient) { ZRoutedRpc.instance.Register("vc_panel", (Action)HandlePanelOnClient); ZRoutedRpc.instance.Register("vc_catalog", (Action)HandleCatalogOnClient); _registeredClient = true; Debug.Log((object)"[Valcoin] RPC registered (client): vc_panel, vc_catalog"); } } public static void SendAction(string action) { if (ZRoutedRpc.instance == null) { return; } try { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "vc_action", new object[1] { action }); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] SendAction failed: " + ex.Message)); } } private static void HandleActionOnServer(long senderPeerID, string action) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } try { UiActionRouter.Execute(senderPeerID, action); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] HandleActionOnServer error: " + ex)); } } public static void PushPanelMessage(long peerID, string msg) { if (ZRoutedRpc.instance == null) { return; } try { ZRoutedRpc.instance.InvokeRoutedRPC(peerID, "vc_panel", new object[1] { msg }); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] PushPanelMessage failed: " + ex.Message)); } } private static void HandlePanelOnClient(long _from, string msg) { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return; } try { OnPanelMessage?.Invoke(msg); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] OnPanelMessage handler failed: " + ex)); } } public static void BroadcastCatalog(string json) { if (ZRoutedRpc.instance == null || string.IsNullOrEmpty(json)) { return; } try { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "vc_catalog", new object[1] { json }); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] BroadcastCatalog failed: " + ex.Message)); } } private static void HandleCatalogOnClient(long _from, string json) { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return; } try { Catalog.ApplyRemote(json); } catch (Exception ex) { Debug.LogError((object)("[Valcoin] Catalog apply failed: " + ex)); } } } public static class ShopHandler { public delegate void TellFn(string msg); private class SpendResp { public string status; public int balance; public int spent; } public static void Buy(string steam64, string skuId, TellFn tell, Action onSuccess = null) { if (string.IsNullOrEmpty(steam64)) { tell("⚠\ufe0f Couldn't resolve your Steam ID."); return; } if (!Config.Ready) { tell("⚠\ufe0f Shop is offline (backend not configured)."); return; } if (!Catalog.Items.TryGetValue(skuId, out var sku)) { tell("⚠\ufe0f Unknown SKU: " + skuId + ". Type /shop for the list."); return; } int balance = CoinManager.GetBalance(steam64); if (balance < sku.Price) { tell($"\ud83d\udcb8 Not enough Valcoins ({balance} / {sku.Price})."); return; } if (sku.Effect == "grant_perk" && PerkManager.Has(steam64, sku.Perk)) { tell("✅ You already own \"" + sku.Name + "\"."); return; } if (sku.Effect == "grant_item") { if (!string.IsNullOrEmpty(sku.RequiresBoss) && !BossGateSatisfied(sku.RequiresBoss)) { tell("\ud83d\udd12 \"" + sku.Name + "\" unlocks after a later boss. Keep progressing!"); return; } if (SteamIdResolver.ZdoFor(steam64) == null) { tell("⚠\ufe0f Couldn't find your character to deliver items. Spawn in, then try again."); return; } } string idempotency_key = $"buy-{skuId}-{Guid.NewGuid():N}"; var body = new { steam64 = steam64, sku = skuId, coins = sku.Price, idempotency_key = idempotency_key, weekly_cap = ((sku.Effect == "grant_item") ? sku.WeeklyCap : 0) }; ((MonoBehaviour)SharedCoroutineRunner.Instance).StartCoroutine(BackendClient.Post("/api/spend", body, delegate(bool ok, SpendResp r, string err) { if (!ok || r == null) { if (err != null && err.Contains("429")) { tell(("\ud83d\uddd3\ufe0f Weekly limit reached for \"" + sku.Name + "\". " + ExtractDetail(err)).TrimEnd(Array.Empty())); } else if (err != null && err.Contains("402")) { tell("\ud83d\udcb8 The server says you don't have enough Valcoins. Try /coins."); } else { tell("❌ Purchase failed. (" + (err ?? "unknown") + ")"); } } else { CoinManager.SetBalance(steam64, r.balance); if (r.status == "duplicate") { tell($"↩\ufe0f \"{sku.Name}\" was already processed. Balance: {r.balance}."); onSuccess?.Invoke(); } else { ApplyEffect(steam64, sku, tell); onSuccess?.Invoke(); } } })); } private static void ApplyEffect(string steam64, Catalog.Sku sku, TellFn tell) { switch (sku.Effect) { case "grant_perk": PerkManager.Grant(steam64, sku.Perk); tell("\ud83c\udf89 Purchased \"" + sku.Name + "\" — perk \"" + sku.Perk + "\" unlocked!"); break; case "add_charges": PerkManager.AddCharges(steam64, sku.Perk, sku.Charges); tell($"\ud83c\udf89 Purchased \"{sku.Name}\" — +{sku.Charges} {sku.Perk} charge(s)."); break; case "grant_item": { int num = GrantItems(steam64, sku.Item); if (num > 0) { tell($"\ud83c\udf81 Purchased \"{sku.Name}\" — {num} item stack(s) dropped at your feet."); } else { tell("⚠\ufe0f \"" + sku.Name + "\" was charged but no items could be spawned (bad prefab id?). Tell an admin."); } break; } default: Debug.LogWarning((object)("[Valcoin] Unknown effect type \"" + sku.Effect + "\" for SKU " + sku.Id)); tell("⚠\ufe0f \"" + sku.Name + "\" was charged but the effect couldn't be applied. Tell an admin."); break; } } private static int GrantItems(string steam64, string itemSpec) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0170: 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_0176: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrEmpty(itemSpec)) { return 0; } if ((Object)(object)ZNetScene.instance == (Object)null) { Debug.LogWarning((object)"[Valcoin] grant_item: no ZNetScene."); return 0; } ZDO val = SteamIdResolver.ZdoFor(steam64); if (val == null) { Debug.LogWarning((object)"[Valcoin] grant_item: no ZDO for buyer."); return 0; } Vector3 position = val.GetPosition(); int num = 0; string[] array = itemSpec.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } string text2 = text; int result = 1; int num2 = text.LastIndexOf(':'); if (num2 > 0) { text2 = text.Substring(0, num2).Trim(); if (!int.TryParse(text.Substring(num2 + 1), out result) || result < 1) { result = 1; } } GameObject prefab = ZNetScene.instance.GetPrefab(text2); if ((Object)(object)prefab == (Object)null) { Debug.LogWarning((object)("[Valcoin] grant_item: unknown prefab \"" + text2 + "\" — skipped.")); continue; } ItemDrop component = prefab.GetComponent(); int num3 = ((!((Object)(object)component != (Object)null) || component.m_itemData?.m_shared == null) ? 1 : Mathf.Max(1, component.m_itemData.m_shared.m_maxStackSize)); int num4 = result; while (num4 > 0) { int num5 = Mathf.Min(num4, num3); Vector3 val2 = position + new Vector3(Random.Range(-1f, 1f), 1.5f, Random.Range(-1f, 1f)); try { ItemDrop component2 = Object.Instantiate(prefab, val2, Quaternion.identity).GetComponent(); if ((Object)(object)component2 != (Object)null) { component2.m_itemData.m_stack = num5; } num++; } catch (Exception ex) { Debug.LogError((object)("[Valcoin] grant_item: failed to spawn " + text2 + ": " + ex.Message)); } num4 -= num5; } } return num; } private static bool BossGateSatisfied(string bossKey) { if (string.IsNullOrEmpty(bossKey)) { return true; } try { if ((Object)(object)ZoneSystem.instance == (Object)null) { return true; } return ZoneSystem.instance.GetGlobalKey(bossKey); } catch (Exception ex) { Debug.LogWarning((object)("[Valcoin] boss-gate check failed for \"" + bossKey + "\": " + ex.Message)); return true; } } private static string ExtractDetail(string err) { if (string.IsNullOrEmpty(err)) { return ""; } int num = err.IndexOf("\"detail\":\"", StringComparison.Ordinal); if (num < 0) { return ""; } num += "\"detail\":\"".Length; int num2 = err.IndexOf('"', num); if (num2 <= num) { return ""; } return err.Substring(num, num2 - num); } } public static class SteamIdResolver { private static readonly Regex Steam64Re = new Regex("^7656119\\d{10}$", RegexOptions.Compiled); private static readonly Regex PlayFabIdRe = new Regex("^[A-Za-z0-9]{8,32}$", RegexOptions.Compiled); private const string PlayFabPrefix = "PlayFab_"; public static string FromPeer(ZNetPeer peer) { if (peer == null) { return null; } try { ZRpc rpc = peer.m_rpc; ISocket val = ((rpc != null) ? rpc.GetSocket() : null); if (val == null) { return null; } string hostName = val.GetHostName(); if (!string.IsNullOrEmpty(hostName) && Steam64Re.IsMatch(hostName)) { return hostName; } if ((((object)val).GetType().Name ?? "").IndexOf("PlayFab", StringComparison.OrdinalIgnoreCase) >= 0 && !string.IsNullOrEmpty(hostName) && PlayFabIdRe.IsMatch(hostName)) { return "PlayFab_" + hostName; } } catch { } return null; } public static string FromNetworkUserId(string nuid) { if (string.IsNullOrEmpty(nuid)) { return null; } if (nuid.StartsWith("Steam_")) { string text = nuid.Substring("Steam_".Length); if (!Steam64Re.IsMatch(text)) { return null; } return text; } if (nuid.StartsWith("Pla_") || nuid.StartsWith("PlayFab_")) { string text2 = (nuid.StartsWith("Pla_") ? nuid.Substring("Pla_".Length) : nuid.Substring("PlayFab_".Length)); if (!PlayFabIdRe.IsMatch(text2)) { return null; } return "PlayFab_" + text2; } if (!Steam64Re.IsMatch(nuid)) { return null; } return nuid; } public static string FromPeerId(long peerId) { if (peerId == 0L || (Object)(object)ZNet.instance == (Object)null) { return null; } return FromPeer(ZNet.instance.GetPeer(peerId)); } public static ZNetPeer PeerFor(string steam64) { if (string.IsNullOrEmpty(steam64) || (Object)(object)ZNet.instance == (Object)null) { return null; } return ((IEnumerable)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func)((ZNetPeer p) => FromPeer(p) == steam64)); } public static ZDO ZdoFor(string steam64) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) ZNetPeer val = PeerFor(steam64); if (val == null || ZDOMan.instance == null) { return null; } return ZDOMan.instance.GetZDO(val.m_characterID); } public static Player OnlinePlayerFor(string steam64) { if (string.IsNullOrEmpty(steam64) || (Object)(object)ZNet.instance == (Object)null) { return null; } ZNetPeer val = ((IEnumerable)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func)((ZNetPeer p) => FromPeer(p) == steam64)); if (val == null) { return null; } string name = val.m_playerName; return ((IEnumerable)Player.GetAllPlayers()).FirstOrDefault((Func)((Player p) => (Object)(object)p != (Object)null && p.GetPlayerName().Equals(name, StringComparison.OrdinalIgnoreCase))); } } public static class UiActionRouter { public static void Execute(long senderPeerID, string action) { if (string.IsNullOrEmpty(action)) { return; } ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(senderPeerID) : null); if (val == null) { return; } string text = SteamIdResolver.FromPeer(val); string playerName = val.m_playerName; int num = action.IndexOf(':'); string text2; string text3; if (num < 0) { text2 = action; text3 = ""; } else { text2 = action.Substring(0, num); text3 = action.Substring(num + 1); } switch (text2) { case "donate": DonateFlow.Run(text, playerName, Reply); break; case "buy": ShopHandler.Buy(text, text3.Trim().ToLowerInvariant(), Reply); break; case "gift": DoGift(text, playerName, text3, Reply); break; case "title": DoTitle(text, text3, Reply); break; case "topdonors": TopDonorsFetcher.Fetch(delegate(string reply) { Reply(reply); }); break; default: Reply("⚠\ufe0f Unknown UI action: " + text2); break; } void Reply(string msg) { RpcLayer.PushPanelMessage(senderPeerID, msg); } } private static void DoGift(string fromSteam64, string fromName, string rest, Action reply) { string[] array = rest.Split(new char[1] { ':' }, 2); int result; if (array.Length != 2) { reply("⚠\ufe0f Bad gift format."); } else if (!int.TryParse(array[1].Trim(), out result) || result <= 0) { reply("⚠\ufe0f Amount must be a positive number."); } else { GiftFlow.Run(fromSteam64, fromName, array[0].Trim(), result, reply); } } private static void DoTitle(string steam64, string rest, Action reply) { if (string.IsNullOrEmpty(steam64)) { reply("⚠\ufe0f Couldn't resolve your Steam ID."); } else if (!PerkManager.Has(steam64, "chat_title")) { reply("\ud83d\udd12 You need the \"chat_title\" perk. Buy it from the shop tab."); } else if (rest.Equals("clear", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(rest)) { PerkManager.SetTitle(steam64, null); reply("✅ Title cleared."); } else if (rest.Length > 16) { reply("⚠\ufe0f 16 characters or fewer."); } else if (!Regex.IsMatch(rest, "^[\\w \\-'\\.!?]+$")) { reply("⚠\ufe0f Letters, numbers and basic punctuation only."); } else { PerkManager.SetTitle(steam64, rest); reply("✅ Title set to [" + rest + "]."); } } } public class WelcomeBanner : MonoBehaviour { private const float DelaySeconds = 5f; private static bool _shownThisSession; public static void ResetForNewSession() { _shownThisSession = false; } public static void Show() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (!_shownThisSession && Config.WelcomeEnabled) { GameObject val = new GameObject("ValcoinWelcome"); val.AddComponent(); Object.DontDestroyOnLoad((Object)val); } } private IEnumerator Start() { while ((Object)(object)Player.m_localPlayer == (Object)null) { yield return null; } yield return (object)new WaitForSeconds(5f); if (!((Object)(object)Player.m_localPlayer == (Object)null)) { string text = (string.IsNullOrEmpty(Config.WelcomeMessage) ? ("\ud83d\udc9b Press " + (Config.UiToggleKey ?? "F8") + " or type /donate to support the server") : Config.WelcomeMessage); ((Character)Player.m_localPlayer).Message((MessageType)1, text, 0, (Sprite)null); _shownThisSession = true; Object.Destroy((Object)(object)((Component)this).gameObject); } } } [HarmonyPatch(typeof(Player), "OnSpawned")] public static class WelcomeOnSpawn { private static void Postfix(Player __instance) { if (!((Object)(object)ZNet.instance == (Object)null) && (!ZNet.instance.IsServer() || !ZNet.instance.IsDedicated()) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { WelcomeBanner.Show(); } } }