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 Valheim Donations v5.1.0
ValheimDonationSystem.dll
Decompiled 13 hours agousing 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<T>(bool ok, T result, string error); public static IEnumerator Get<T>(string path, Callback<T> cb) { return Send("GET", path, null, cb); } public static IEnumerator Post<T>(string path, object body, Callback<T> cb) { return Send("POST", path, body, cb); } private static IEnumerator Send<T>(string method, string path, object body, Callback<T> 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<T>(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<string, Sku> Items { get; private set; } = new Dictionary<string, Sku>(); public static List<Sku> Order { get; private set; } = new List<Sku>(); 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<string, Sku>(); Order = new List<Sku>(); } } 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 <name> 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<string, Sku> items = new Dictionary<string, Sku>(); List<Sku> order = new List<Sku>(); bool flag = false; Sku sku = null; foreach (string text in lines) { if (string.IsNullOrWhiteSpace(text) || text.TrimStart(Array.Empty<char>()).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<string, Sku> items, List<Sku> 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<Sku> list = JsonConvert.DeserializeObject<List<Sku>>(json); if (list == null) { return; } Dictionary<string, Sku> dictionary = new Dictionary<string, Sku>(); 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>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((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<string>() : 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 <id>"); } private static void HandleBuy(string steam64, Player p, string[] args) { if (args.Length != 1) { Tell(p, "⚠\ufe0f Usage: /buy <sku> (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 <playerName> <amount>"); 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 <text> (or /title clear)" : ("Your title is currently: [" + text + "] Change with /title <text>, 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 <playerName> <amount>"); 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 <playerName> <amount>"); 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<ZNetPeer>)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((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>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((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<SharedCoroutineRunner>(); } return _instance; } } } public static class CoinManager { private class State { public Dictionary<string, int> balances = new Dictionary<string, int>(); public List<long> recentGrants = new List<long>(); } 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<long> _seen = new HashSet<long>(); public static void Load() { try { Directory.CreateDirectory(SaveDir); if (!File.Exists(SaveFile)) { return; } string text = File.ReadAllText(SaveFile); State state; try { state = JsonConvert.DeserializeObject<State>(text); if (state == null || state.balances == null) { throw new Exception("not new shape"); } } catch { Dictionary<string, int> balances = JsonConvert.DeserializeObject<Dictionary<string, int>>(text) ?? new Dictionary<string, int>(); state = new State { balances = balances }; } _state = state; State state2 = _state; if (state2.recentGrants == null) { state2.recentGrants = new List<long>(); } _seen = new HashSet<long>(_state.recentGrants); } catch (Exception ex) { Debug.LogError((object)("[CoinManager] Failed to load: " + ex.Message)); _state = new State(); _seen = new HashSet<long>(); } } 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<long> 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<TopEntry> _patrons = new List<TopEntry>(); 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<KeyCode>(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<TopEntry>(r.top_donors) : new List<TopEntry>()); } }); } 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<GUILayoutOption>()); GUILayout.Label("Donation Codex", _hdr, Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); DrawHr(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); 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<GUILayoutOption>()); GUILayout.Space(8f); GUILayout.Label("How Valcoins work", _sub, Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); Bullet("/donate — get your donation code + link"); Bullet("/coins — show your Valcoin balance + perks"); Bullet("/shop — list everything for sale"); Bullet("/buy <id> — purchase an item"); Bullet("/gift <player> <amount> — share Valcoins"); Bullet("/topdonors — the patron leaderboard"); Bullet("/title <text> — set a chat title (with the perk)"); GUILayout.Space(8f); GUILayout.Label($"Press {_toggleKey} any time to open this Codex.", _label, Array.Empty<GUILayoutOption>()); } 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<GUILayoutOption>()); return; } string text = ResolveLocalSteam64(); foreach (Catalog.Sku item in Catalog.Order) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); } } if (!string.IsNullOrEmpty(item.Description)) { GUILayout.Label(" " + item.Description, _label, Array.Empty<GUILayoutOption>()); } 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<GUILayoutOption>()); } } private void DrawPatrons() { GUILayout.Label("Top Patrons", _hdr, Array.Empty<GUILayoutOption>()); GUILayout.Space(4f); if (!_online) { GUILayout.Label("The patron leaderboard appears here once the donation service is online.", _label, Array.Empty<GUILayoutOption>()); return; } if (_patrons.Count == 0) { GUILayout.Label("No patrons yet — be the first. Head to the Donate tab!", _label, Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); } 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<GUILayoutOption>()); GUILayout.Label("Get a personal code, donate through the portal link, and your Valcoins are credited automatically within a few seconds.", _label, Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); } else { GUILayout.Label("Donations aren't connected yet.", _sub, Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); } } private void Bullet(string text) { GUILayout.Label("• " + text, _label, Array.Empty<GUILayoutOption>()); } 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<string> _perks = new List<string>(); private List<TopEntry> _topDonors = new List<TopEntry>(); private readonly List<string> _log = new List<string>(); 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<KeyCode>(Config.UiToggleKey, ignoreCase: true, out KeyCode result)) { _toggleKey = result; } RpcLayer.OnPanelMessage = (Action<string>)Delegate.Combine(RpcLayer.OnPanelMessage, new Action<string>(AddLog)); Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); } private void OnDestroy() { RpcLayer.OnPanelMessage = (Action<string>)Delegate.Remove(RpcLayer.OnPanelMessage, new Action<string>(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<TopEntry>(r.top_donors) : new List<TopEntry>()); } }); } 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<GUILayoutOption>()); GUILayout.Label("Valheim Donations", _hdr, Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); string text = string.Join(", ", PerksForLocalPlayer()); if (!string.IsNullOrEmpty(text)) { GUILayout.Label("Perks: " + text, _label, Array.Empty<GUILayoutOption>()); } if (!_online) { GUILayout.Label("\ud83d\udd0c Donation service offline — browse now; it activates when the operator connects it.", _label, Array.Empty<GUILayoutOption>()); } DrawHr(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); 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<GUILayoutOption>()); 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<GUILayoutOption>()); 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<GUILayoutOption>()); } GUILayout.Space(4f); } GUILayout.EndScrollView(); } private void DrawGift() { GUILayout.Label("Send Valcoins to another player on the server.", _label, Array.Empty<GUILayoutOption>()); GUILayout.Space(4f); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); 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<GUILayoutOption>()); 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<GUILayoutOption>()); _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<GUILayoutOption>()); GUILayout.Space(4f); if (_topDonors.Count == 0) { GUILayout.Label("(none yet — be the first!)", _label, Array.Empty<GUILayoutOption>()); } 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<GUILayoutOption>()); } } 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<GUILayoutOption>()); _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<GUILayoutOption>()); } GUILayout.EndScrollView(); } } private IEnumerable<string> 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<string> 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<string> 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<string> 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<Grant> grants; } private class AckRequest { public List<long> 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<long> list = new List<long>(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, $"<color=yellow>+{grant.coins} Valcoins!</color> 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<string> perks = new HashSet<string>(); public Dictionary<string, int> charges = new Dictionary<string, int>(); public string title; public Pos home; public string homeCooldownUntilUtc; } private class State { public Dictionary<string, PlayerPerks> players = new Dictionary<string, PlayerPerks>(); } 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<State>(File.ReadAllText(SaveFile)) ?? new State(); if (_state.players == null) { _state.players = new Dictionary<string, PlayerPerks>(); } } } 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<string> AdminSteamIDs = new HashSet<string>(); 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<GrantPoller>(); Object.DontDestroyOnLoad((Object)val); GameObject val2 = new GameObject("ValcoinCatalogSync"); val2.AddComponent<CatalogSync>(); 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<DonationPanel>(); Object.DontDestroyOnLoad((Object)val); GameObject val2 = new GameObject("ValcoinDonationCodex"); val2.AddComponent<DonationCodex>(); 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 <playerName> <amount>\n# /removecoins <playerName> <amount>\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<string> hashSet = new HashSet<string>(); 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<char>()); if (text.TrimStart(Array.Empty<char>()).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<string>(); } } } 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<string> OnPanelMessage; public static IEnumerator RegisterWhenReady(bool serverSide) { while (ZRoutedRpc.instance == null) { yield return null; } if (serverSide && !_registeredServer) { ZRoutedRpc.instance.Register<string>("vc_action", (Action<long, string>)HandleActionOnServer); _registeredServer = true; Debug.Log((object)"[Valcoin] RPC registered (server): vc_action"); } if (!serverSide && !_registeredClient) { ZRoutedRpc.instance.Register<string>("vc_panel", (Action<long, string>)HandlePanelOnClient); ZRoutedRpc.instance.Register<string>("vc_catalog", (Action<long, string>)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<char>())); } 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<ItemDrop>(); 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<GameObject>(prefab, val2, Quaternion.identity).GetComponent<ItemDrop>(); 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<ZNetPeer>)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((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<ZNetPeer>)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => FromPeer(p) == steam64)); if (val == null) { return null; } string name = val.m_playerName; return ((IEnumerable<Player>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((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<string> 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<string> 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<WelcomeBanner>(); 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(); } } }