// Self-contained Unity v2 example, not a published package dependency. // Requires UnityWebRequest. Public config only; no external JSON package. using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Collections.Generic; using System.Globalization; using System.Text; using UnityEngine.Networking; namespace Ggbucks.PublicV2 { [Serializable] public sealed class Rule { public string eventType; public long rewardGgbucks; } [Serializable] public sealed class Thresholds { public long maxPayoutGgbucks, dailyPlayerCapGgbucks; } [Serializable] public sealed class Config { public string gameId, name, slug, platform, status, environment; public bool productionRewardsEnabled; public Rule[] rules; public Thresholds thresholds; } public sealed class PublicConfigClient { readonly string endpoint; public PublicConfigClient(string apiOrigin, string publicKey) { if (!Uri.TryCreate(apiOrigin, UriKind.Absolute, out var origin) || origin.Scheme != "https" || origin.UserInfo != "" || origin.AbsolutePath != "/" || origin.Query != "" || origin.Fragment != "") throw new ArgumentException("Use a fixed HTTPS API origin."); if (publicKey == null || publicKey.Length != 58 || !Regex.IsMatch(publicKey, @"\Aggb_pk_sandbox_[A-Za-z0-9_-]{43}\z")) throw new ArgumentException("Use a public game key; never embed a secret."); endpoint = origin.GetLeftPart(UriPartial.Authority) + "/api/game-sdk/v2/config?publicKey=" + Uri.EscapeDataString(publicKey); } public async Task GetConfigAsync() { using (var request = UnityWebRequest.Get(endpoint)) { request.redirectLimit = 0; request.timeout = 20; var operation = request.SendWebRequest(); while (!operation.isDone) await Task.Yield(); if (request.result != UnityWebRequest.Result.Success || request.responseCode < 200 || request.responseCode >= 300) throw new Exception("Game configuration is unavailable. Keep normal gameplay available."); return ParseResponse(request.downloadHandler.text); } } internal static Config ParseResponse(string json) { try { var envelope = Object(new JsonReader(json).Read()); if (envelope.TryGetValue("error", out var error) && error != null) throw new FormatException(); var data = Object(Required(envelope, "data")); var gameId = Text(data, "gameId"); var slug = Text(data, "slug"); if (!Guid.TryParseExact(gameId, "D", out var id) || id == Guid.Empty || slug != gameId || Text(data, "environment") != "sandbox" || !(Required(data, "productionRewardsEnabled") is bool enabled) || enabled) throw new FormatException(); var platform = Text(data, "platform"); var status = Text(data, "status"); if (!new HashSet { "android", "ios", "other", "unity", "unreal", "web" }.Contains(platform) || (status != "active" && status != "paused")) throw new FormatException(); var thresholds = Object(Required(data, "thresholds")); var rules = Required(data, "rules") as List; if (rules == null) throw new FormatException(); var parsedRules = new List(); foreach (var item in rules) { var rule = Object(item); var type = Text(rule, "eventType"); if (!Regex.IsMatch(type, @"\A[a-z][a-z0-9_.-]{0,63}\z")) throw new FormatException(); parsedRules.Add(new Rule { eventType = type, rewardGgbucks = Units(rule, "rewardGgbucks", 1000000000000L, true) }); } return new Config { gameId = gameId, slug = slug, name = Text(data, "name"), environment = "sandbox", productionRewardsEnabled = false, platform = platform, status = status, thresholds = new Thresholds { maxPayoutGgbucks = Units(thresholds, "maxPayoutGgbucks", 1000000000000L), dailyPlayerCapGgbucks = Units(thresholds, "dailyPlayerCapGgbucks", 1000000000000L) }, rules = parsedRules.ToArray() }; } catch { throw new FormatException("Invalid sandbox configuration."); } } static Dictionary Object(object value) => value as Dictionary ?? throw new FormatException(); static object Required(Dictionary data, string key) => data.TryGetValue(key, out var value) ? value : throw new FormatException(); static string Text(Dictionary data, string key) => Required(data, key) as string ?? throw new FormatException(); static long Units(Dictionary data, string key, long maximum, bool positive = false) { if (!(Required(data, key) is double number) || number < (positive ? 1 : 0) || number > maximum || number != Math.Truncate(number)) throw new FormatException(); return (long)number; } sealed class JsonReader { readonly string text; int at; public JsonReader(string value) { if (value == null || value.Length > 1048576) throw new FormatException(); text = value; } public object Read() { var value = Value(0); Space(); if (at != text.Length) throw new FormatException(); return value; } void Space() { while (at < text.Length && (text[at] == ' ' || text[at] == '\n' || text[at] == '\r' || text[at] == '\t')) at++; } bool Take(char c) { Space(); if (at < text.Length && text[at] == c) { at++; return true; } return false; } void Need(char c) { if (!Take(c)) throw new FormatException(); } object Value(int depth) { if (depth > 24) throw new FormatException(); Space(); if (at == text.Length) throw new FormatException(); if (text[at] == '"') return String(); if (Take('{')) { var result = new Dictionary(); if (Take('}')) return result; do { var key = String(); Need(':'); if (result.ContainsKey(key)) throw new FormatException(); result.Add(key, Value(depth + 1)); } while (Take(',')); Need('}'); return result; } if (Take('[')) { var result = new List(); if (Take(']')) return result; do { result.Add(Value(depth + 1)); } while (Take(',')); Need(']'); return result; } foreach (var literal in new[] { "true", "false", "null" }) if (at + literal.Length <= text.Length && text.Substring(at, literal.Length) == literal) { at += literal.Length; return literal == "null" ? null : (object)(literal == "true"); } int start = at; while (at < text.Length && "0123456789eE+-.".IndexOf(text[at]) >= 0) at++; var numeric = text.Substring(start, at - start); if (!Regex.IsMatch(numeric, @"\A-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\z") || !double.TryParse(numeric, NumberStyles.Float, CultureInfo.InvariantCulture, out var number) || double.IsNaN(number) || double.IsInfinity(number)) throw new FormatException(); return number; } string String() { Need('"'); var result = new StringBuilder(); while (at < text.Length) { var c = text[at++]; if (c == '"') { var value = result.ToString(); for (int i = 0; i < value.Length; i++) { if (char.IsHighSurrogate(value[i])) { if (++i == value.Length || !char.IsLowSurrogate(value[i])) throw new FormatException(); } else if (char.IsLowSurrogate(value[i])) throw new FormatException(); } return value; } if (c < 32) throw new FormatException(); if (c != '\\') { result.Append(c); continue; } if (at == text.Length) throw new FormatException(); var escape = text[at++]; if (escape == 'u') { if (at + 4 > text.Length || !ushort.TryParse(text.Substring(at, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var code)) throw new FormatException(); result.Append((char)code); at += 4; } else { int index = "\"\\/bfnrt".IndexOf(escape); if (index < 0) throw new FormatException(); result.Append("\"\\/\b\f\n\r\t"[index]); } } throw new FormatException(); } } } }