using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace game.PlayerDataStorage { // Pure decoding; Valid is only issued after the complete 31-module graph is checked. internal static class PlayerKVSaveSchema { static readonly CultureInfo Invariant = CultureInfo.InvariantCulture; static readonly string[] CoreKeys = { "Lv", "Gold", "vipAllScore", "Energy", "Diamond", "Magnification", "EquipRodID", "LastEnergyRecoveryTime", "RewardNewMap", "EnergyRM", "GoldRM", "DiamondTrue", "CurrentMapId", "LastMapId", "FishingFrequency", "AllFishingFrequency", "HeistGameCount", "BombGameCount", "FishHuntGameCount" }; static readonly string[] Aliases = { "lv", "gold", "vipS", "energy", "pearl", "mag", "rodID", "EnergyTime", "rewardNewMap", "energyRM", "goldRM", "diamond", "cMapID", "lMapID", "FF", "AFF", "HGC", "BGC", "FHGC" }; internal static PlayerKVSchemaResult Decode(PlayerKVLoginPrepareResult prepared,PlayerKVAccountDescriptor account, string expectedId,long epoch,bool current,PlayerKVSchemaSnapshot snapshot,string expectedVersion,PlayerKVIdentitySnapshot identity) { var errors=new PlayerKVSchemaDiagnostics(); PlayerKVSchemaResult Fail(PlayerKVSchemaVerdict verdict) => new PlayerKVSchemaResult(verdict,errors); if(!current || prepared==null || !prepared.Prepared || account==null || prepared.PlayerId!=expectedId || account.PlayerId!=expectedId || prepared.Epoch!=epoch || account.Epoch!=epoch) return Fail(PlayerKVSchemaVerdict.Stale); var raw=prepared.Data; if(raw.Count==0) return Fail(account.NewlyCreated ? PlayerKVSchemaVerdict.EmptyNew : PlayerKVSchemaVerdict.EmptyExisting); if(snapshot==null || snapshot.Version!=expectedVersion || identity==null) return Fail(PlayerKVSchemaVerdict.UnsupportedSchema); var modern=DecodeModern(raw,errors); if(modern==null) return Fail(PlayerKVSchemaVerdict.Malformed); var player=DecodePlayer(raw,modern,snapshot,errors); var modules=new object[] { player,player?.Core,DecodeFish(raw,modern,snapshot,errors),DecodeShop(raw,snapshot,errors), DecodeEventBase(raw,modern,snapshot,errors),DecodeBuffs(raw,snapshot,errors),DecodeCamp(raw,errors), DecodeAchievements(raw,modern,snapshot,errors),DecodeFishingBox(raw,modern,errors),DecodeGuides(raw,errors), DecodeBattlePass(raw,snapshot,errors),DecodeMonthlySign(raw,errors),DecodeTriggers(raw,errors),DecodeClub(raw,snapshot,errors), DecodeChallenge(raw,snapshot,errors),DecodeOffer(raw,false,snapshot,errors),DecodeOffer(raw,true,snapshot,errors), DecodePartner(raw,snapshot,errors),DecodeCube(raw,snapshot,errors),DecodeEventPack(raw,snapshot,errors), DecodePiggyBank(raw,snapshot,errors),DecodeBargain(raw,snapshot,errors),DecodeFootPrint(raw,errors),DecodeShootingRange(raw,snapshot,errors), DecodePotion(raw,errors),DecodePinball(raw,snapshot,errors),DecodeScratch(raw,snapshot,errors), new PlayerKVProgressPackPair(DecodeProgressPack(raw,false,errors),DecodeProgressPack(raw,true,errors)), DecodePinata(raw,errors),PlayerKVEmptyModuleState.Instance,DecodeLogin(raw,identity,errors) }; if(errors.TotalErrors>0) { // Core classification has precedence; remaining failures are deterministic by module/field order. if(errors.Count(1,PlayerKVSchemaError.MissingCore)>0) return Fail(PlayerKVSchemaVerdict.MissingCore); if(errors.Count(1,PlayerKVSchemaError.ConflictingRepresentations)>0) return Fail(PlayerKVSchemaVerdict.ConflictingRepresentations); var code=errors.Details[0].Code; return Fail(code==PlayerKVSchemaError.UnsupportedSchema ? PlayerKVSchemaVerdict.UnsupportedSchema : code==PlayerKVSchemaError.ConflictingRepresentations ? PlayerKVSchemaVerdict.ConflictingRepresentations : PlayerKVSchemaVerdict.Malformed); } var topKeys=new HashSet(CoreKeys,StringComparer.Ordinal); topKeys.UnionWith(("NewPlayerDataDic NewRodList SeasonId IsOpenAppraise InfiniteBuildingData RodData RodLevelData FishSwimToMap FishSwimToMapCount MapProgress HonorLevelData InitRodAccessoriesData LuckDataDic "+ "ShopPackBuyCount ShopAllPackBuyCount ShopGiftBuyCount Voucher ChainPackDataNew VIPAdPackData TurntableAdData BuyData AdvertAllCount DiamondShopRefreshCount DiamondShopRefreshTime DiamondShopItemRecords BlackFridayCouponDate "+ "LimitedTimeEventDic FixedTimeEventDic ETDropAfterDropDic RankTargetCount WheelLevelCount TurntableRoll WheelOperateCount AllWheelOperateCount WheelLevelFreeCount PVPToken "+ "BuffDataList TargetBuffDataList FishDailyBuff GlobalBuffDataList IsFix IsOpenSkyscraper CampDataDic AchievementData FishingBoxData GuideSaveData GuideSaveDataNew BattlePassRecord BattleTask BattlePassRecordTask MonthlySignEvent "+ "TriggerPackDataList GiftVIPDData FixedTimeShopDic ClubSignin LureDonateData IsOpenClubRed FishingChallengeData OfferChainsChestData OfferDiscoPartyManager EventPartnerGatherData EventCubeMeltData EventPackData PiggyBankPackData GeneralEventPackData FootPrint EventShootingRangeData epdk epbdk estdk ThanksGivingPackData ThanksGivingPackMigrationData EventPinataPackData MMTest EAccoutUrl TapJoy_Diamond hwGoogle hwFacebook Apple").Split(' ')); if(identity.Channel!=PlayerKVIdentityChannel.none && identity.Channel!=PlayerKVIdentityChannel.hwGuest) topKeys.Add(identity.Channel.ToString()); var modernKeys=new HashSet(Aliases,StringComparer.Ordinal); modernKeys.UnionWith(new[]{"newData","vipL","LDD","ld","AD","ad","bd","ct","rt"}); int unconsumedTop=raw.Keys.Count(k=>!topKeys.Contains(k) && !System.Text.RegularExpressions.Regex.IsMatch(k,"^Map_(5_)?([0-9]+)$")); int unconsumedModern=modern.Keys.Count(k=>!modernKeys.Contains(k)); return new PlayerKVSchemaResult(PlayerKVSchemaVerdict.Valid,errors,new PlayerKVDecodedSave(expectedId,epoch,snapshot.Version,raw,modules,unconsumedTop,unconsumedModern)); } internal static PlayerKVLoginState DecodeLogin(IReadOnlyDictionary raw,PlayerKVIdentitySnapshot identity, PlayerKVSchemaDiagnostics errors) { if(identity==null) throw new ArgumentNullException(nameof(identity)); int before=errors.TotalErrors; var normalized=new Dictionary(StringComparer.Ordinal); int mm=errors.Capture(31,0,()=> { int value=0; bool normalize=true; if(raw.TryGetValue("MMTest",out var text)) { if(text==null) throw new PlayerKVSchemaException(); if(text=="MMFailParam" || text.StartsWith("MMFailParam#",StringComparison.Ordinal)) { var parts=text.Split('#'); if(parts.Length!=2) throw new PlayerKVSchemaException(); value=Int32(parts[1]); normalize=false; } } else if(identity.NewlyCreated) { if(string.IsNullOrEmpty(identity.UserId) || identity.UserId.Length>16 || identity.UserId.Any(c=>!Uri.IsHexDigit(c)) || !ulong.TryParse(identity.UserId,NumberStyles.AllowHexSpecifier,Invariant,out var id)) throw new PlayerKVSchemaException(); value=(int)(id%16); } if(normalize) normalized["MMTest"]="MMFailParam#"+value.ToString(Invariant); return value; }); var linked=new HashSet(identity.Linked); var channels=new HashSet{PlayerKVIdentityChannel.hwGoogle,PlayerKVIdentityChannel.hwFacebook,PlayerKVIdentityChannel.Apple}; bool hasChannel=identity.Channel!=PlayerKVIdentityChannel.hwGuest && identity.Channel!=PlayerKVIdentityChannel.none; if(hasChannel) channels.Add(identity.Channel); int field=1; foreach(var channel in channels.OrderBy(c=>(int)c)) { string key=channel.ToString(); if(raw.TryGetValue(key,out var text)) { int flag=errors.Capture(31,field,()=> { if(text!="0" && text!="1") throw new PlayerKVSchemaException(); return text=="1" ? 1 : 0; }); if(flag==1) linked.Add(channel); } field++; } if(hasChannel && !raw.ContainsKey(identity.Channel.ToString())) { normalized[identity.Channel.ToString()]="1"; linked.Add(identity.Channel); } var urls=raw.TryGetValue("EAccoutUrl",out var urlsText) ? errors.Capture(31,6,()=>StringMap(urlsText)) : new Dictionary(); int balance=raw.TryGetValue("TapJoy_Diamond",out var balanceText) ? errors.Capture(31,7,()=>Int32(balanceText)) : 0; return errors.TotalErrors==before ? new PlayerKVLoginState(mm,balance,urls,normalized,linked,identity.Channel) : null; } internal static int Int32(string value) { if (!int.TryParse(value, NumberStyles.AllowLeadingSign, Invariant, out var n)) throw new PlayerKVSchemaException(); return n; } internal static ulong UInt64(string value) { if (!ulong.TryParse(value, NumberStyles.None, Invariant, out var n)) throw new PlayerKVSchemaException(); return n; } internal static float Single(string value) { if (!float.TryParse(value, NumberStyles.Float, Invariant, out var n) || float.IsNaN(n) || float.IsInfinity(n)) throw new PlayerKVSchemaException(); return n; } internal static DateTime Date(string value, PlayerKVSchemaSnapshot snapshot) { if (System.DateTime.TryParseExact(value, "O", Invariant, DateTimeStyles.RoundtripKind, out var roundtrip)) return roundtrip; const DateTimeStyles styles = DateTimeStyles.RoundtripKind | DateTimeStyles.NoCurrentDateDefault; bool local = System.DateTime.TryParse(value, snapshot.Culture, styles, out var a); bool invariant = System.DateTime.TryParse(value, Invariant, styles, out var b); if (local && invariant && a != b) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); if (local) return a; if (invariant) return b; throw new PlayerKVSchemaException(); } internal static JToken Json(string value) { if (value == null) throw new PlayerKVSchemaException(); StrictJsonSyntax.Validate(value); try { // Json.NET's default parser accepts comments, constructors and non-finite literals. // Reject those before materializing tokens; never deserialize a runtime game class here. using (var check = new JsonTextReader(new StringReader(value)) { DateParseHandling = DateParseHandling.None }) while (check.Read()) { if (check.TokenType == JsonToken.Comment || check.TokenType == JsonToken.StartConstructor || check.TokenType == JsonToken.Undefined) throw new PlayerKVSchemaException(); if (check.TokenType == JsonToken.Float && check.Value is double d && (double.IsNaN(d) || double.IsInfinity(d))) throw new PlayerKVSchemaException(); } using (var reader = new JsonTextReader(new StringReader(value)) { DateParseHandling = DateParseHandling.None }) { var result = JToken.Load(reader, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error }); if (reader.Read() || result.Type == JTokenType.Null) throw new PlayerKVSchemaException(); return result; } } catch (JsonException) { throw new PlayerKVSchemaException(); } } internal static JObject Object(JToken token) { if (!(token is JObject obj)) throw new PlayerKVSchemaException(); return obj; } internal static string String(JToken token) { if (token == null || token.Type != JTokenType.String) throw new PlayerKVSchemaException(); return (string)token; } internal static int Integer(JToken token) { if (token == null || token.Type != JTokenType.Integer) throw new PlayerKVSchemaException(); return Int32(token.ToString(Formatting.None)); } internal static ulong Unsigned(JToken token) { if (token == null || token.Type != JTokenType.Integer) throw new PlayerKVSchemaException(); return UInt64(token.ToString(Formatting.None)); } internal static long Signed64(JToken token) { if (token == null || token.Type != JTokenType.Integer || !long.TryParse(token.ToString(Formatting.None), NumberStyles.AllowLeadingSign, Invariant, out var value)) throw new PlayerKVSchemaException(); return value; } internal static bool Boolean(JToken token) { if (token == null || token.Type != JTokenType.Boolean) throw new PlayerKVSchemaException(); return (bool)token; } internal static float Finite(JToken token) { if (token == null || (token.Type != JTokenType.Integer && token.Type != JTokenType.Float)) throw new PlayerKVSchemaException(); return Single(token.ToString(Formatting.None)); } internal static List IntegerList(JToken token) { if (!(token is JArray array)) throw new PlayerKVSchemaException(); return array.Select(Integer).ToList(); } internal static PlayerKVPiggyBankState DecodePiggyBank(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue("PiggyBankPackData", out var text)) return PlayerKVPiggyBankState.Missing(); return errors.Capture(21, 0, () => { var obj = Object(Json(text)); var currentEventID = Integer(obj["currentEventID"]); var purchaseCount = Integer(obj["purchaseCount"]); var VIPLvlWhenEventActivate = Integer(obj["VIPLvlWhenEventActivate"]); var savingProgress = Integer(obj["savingProgress"]); var redirectID = Integer(obj["redirectID"]); var doNeedTrigger = Boolean(obj["doNeedTrigger"]); var isTriggered = Boolean(obj["isTriggered"]); var inflation = Finite(obj["inflation"]); if (redirectID == 0) redirectID = 4055001; if (!snapshot.HasPack(redirectID)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); return new PlayerKVPiggyBankState(true, currentEventID, purchaseCount, VIPLvlWhenEventActivate, savingProgress, redirectID, doNeedTrigger, isTriggered, inflation); }); } internal static PlayerKVBargainState DecodeBargain(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue("GeneralEventPackData", out var text)) return PlayerKVBargainState.Missing(); return errors.Capture(22, 0, () => { var obj = Object(Json(text)); var currentEventID = Integer(obj["currentEventID"]); var redirectID = Integer(obj["redirectID"]); var VIPLvlWhenEventActivate = Integer(obj["VIPLvlWhenEventActivate"]); var purchaseCount = Integer(obj["purchaseCount"]); var savingProgress = Integer(obj["savingProgress"]); var discountLvl = Integer(obj["discountLvl"]); var visualProgress = Integer(obj["visualProgress"]); var visualDiscountLvl = Integer(obj["visualDiscountLvl"]); var rodID = Integer(obj["rodID"]); var lastPackIDBought = Integer(obj["lastPackIDBought"]); var packID = Integer(obj["packID"]); var doNeedTrigger = Boolean(obj["doNeedTrigger"]); var isTriggered = Boolean(obj["isTriggered"]); var inflation = Finite(obj["inflation"]); var selectionToken = obj["selections"]; var selections = selectionToken != null && selectionToken.Type == JTokenType.Null ? null : IntegerList(selectionToken); if (!snapshot.HasPackType(redirectID, 3)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); return new PlayerKVBargainState(true, currentEventID, redirectID, VIPLvlWhenEventActivate, purchaseCount, savingProgress, discountLvl, visualProgress, visualDiscountLvl, rodID, lastPackIDBought, packID, doNeedTrigger, isTriggered, inflation, selections); }); } internal static PlayerKVShootingRangeState DecodeShootingRange(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue("EventShootingRangeData", out var text)) return PlayerKVShootingRangeState.Missing(); return errors.Capture(24, 0, () => { var obj = Object(Json(text)); var EventId = Integer(obj["EventId"]); var CycleId = Integer(obj["CycleId"]); var RedirectId = Integer(obj["RedirectId"]); var AmmoCount = Integer(obj["AmmoCount"]); var CurrentStageIdx = Integer(obj["CurrentStageIdx"]); var ChainProgress = Integer(obj["ChainProgress"]); var RewardFlag = Signed64(obj["RewardFlag"]); var TargetFlag = Signed64(obj["TargetFlag"]); var HasBeenOpened = Boolean(obj["HasBeenOpened"]); var Inflation = Finite(obj["Inflation"]); return new PlayerKVShootingRangeState(true, EventId, CycleId, RedirectId, AmmoCount, CurrentStageIdx, ChainProgress, RewardFlag, TargetFlag, HasBeenOpened, Inflation); }); } internal static Dictionary StringMap(string text) { var result = new Dictionary(StringComparer.Ordinal); foreach (var p in Object(Json(text)).Properties()) result.Add(p.Name, String(p.Value)); return result; } internal static Dictionary DecodeModern(IReadOnlyDictionary raw, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue("NewPlayerDataDic", out var text)) return new Dictionary(StringComparer.Ordinal); return errors.Capture(1, 0, () => StringMap(text)); } static object CoreValue(int slot, string value, PlayerKVSchemaSnapshot snapshot) { if (slot == 1 || slot == 10) return UInt64(value); if (slot == 7) return Date(value, snapshot); int n = Int32(value); if (slot == 6) { if (!snapshot.HasRod(n)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); } else if (slot == 12 || slot == 13) { if (n != 0 && !snapshot.HasMap(n)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); } else if (slot != 8 && n < (slot == 0 ? 1 : 0)) throw new PlayerKVSchemaException(); return n; } internal static PlayerKVCoreState DecodeCore(IReadOnlyDictionary raw, IReadOnlyDictionary modern, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { if (modern == null) return null; // Outer representation already rejected; no legacy fallback. int before = errors.TotalErrors; object[] selected = null; if (modern.TryGetValue("newData", out var compact)) { var parts = compact?.Split(','); if (parts == null || parts.Length != 19) { errors.Add(1, 1, PlayerKVSchemaError.Malformed); return null; } selected = new object[19]; for (int i = 0; i < 19; i++) { try { selected[i] = CoreValue(i, parts[i], snapshot); } catch (PlayerKVSchemaException ex) { errors.Add(i >= 16 ? 2 : 1, i + 2, ex.Code); } } } bool completeLegacy = CoreKeys.All(raw.ContainsKey); if (selected == null && !completeLegacy) errors.Add(1, 1, PlayerKVSchemaError.MissingCore); var legacy = new object[19]; for (int i = 0; i < 19; i++) { try { if (raw.TryGetValue(CoreKeys[i], out var old)) { legacy[i] = CoreValue(i, old, snapshot); if (selected != null && selected[i] != null && !Equals(selected[i], legacy[i])) errors.Add(i >= 16 ? 2 : 1, 32 + i, PlayerKVSchemaError.ConflictingRepresentations); } } catch (PlayerKVSchemaException ex) { errors.Add(i >= 16 ? 2 : 1, 32 + i, ex.Code); } try { if (modern.TryGetValue(Aliases[i], out var alias)) { var value = CoreValue(i, alias, snapshot); var canonical = selected != null ? selected[i] : legacy[i]; if (canonical != null && !Equals(canonical, value)) errors.Add(i >= 16 ? 2 : 1, 64 + i, PlayerKVSchemaError.ConflictingRepresentations); } } catch (PlayerKVSchemaException ex) { errors.Add(i >= 16 ? 2 : 1, 64 + i, ex.Code); } } return errors.TotalErrors == before ? new PlayerKVCoreState(selected ?? legacy) : null; } static PlayerKVAchievementValue AchievementSlots(string text) { var parts = text?.Split('|'); if (parts == null || parts.Length != 3) throw new PlayerKVSchemaException(); return new PlayerKVAchievementValue(Int32(parts[0]), UInt64(parts[1]), Int32(parts[2])); } static void AddAchievement(Dictionary result, int key, PlayerKVAchievementValue value) { if (result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key, value); } static Dictionary Achievements(string text, int format, PlayerKVSchemaSnapshot snapshot) { var result = new Dictionary(); if (format == 2) { if (text == "") return result; if (text == null) throw new PlayerKVSchemaException(); foreach (var record in text.Split(';')) { var parts = record.Split(':'); if (parts.Length != 2) throw new PlayerKVSchemaException(); AddAchievement(result, snapshot.Condition(parts[0]), AchievementSlots(parts[1])); } } else foreach (var p in Object(Json(text)).Properties()) { PlayerKVAchievementValue value; if (format == 1) value = AchievementSlots(String(p.Value)); else { var obj = Object(p.Value); value = new PlayerKVAchievementValue(Integer(obj["Id"]), Unsigned(obj["Target"]), Integer(obj["day"])); } AddAchievement(result, snapshot.Condition(p.Name), value); } return result; } internal static PlayerKVAchievementState DecodeAchievements(IReadOnlyDictionary raw, IReadOnlyDictionary modern, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { if (modern == null) return null; int before = errors.TotalErrors; var representations = new List>(); string[] keys = { "AchievementData", "AD", "ad" }; for (int format = 0; format < 3; format++) { var source = format == 0 ? raw : modern; if (!source.TryGetValue(keys[format], out var text)) continue; int captured = format; var state = errors.Capture(8, format, () => Achievements(text, captured, snapshot)); if (state != null) representations.Add(state); } for (int a = 0; a < representations.Count; a++) for (int b = a + 1; b < representations.Count; b++) foreach (var item in representations[a].OrderBy(x => x.Key)) if (representations[b].TryGetValue(item.Key, out var other) && !item.Value.Same(other)) errors.Add(8, 3, PlayerKVSchemaError.ConflictingRepresentations); if (errors.TotalErrors != before) return null; return new PlayerKVAchievementState(representations.Count == 0 ? new Dictionary() : representations[representations.Count - 1]); } internal static Dictionary IntegerDictionary(JToken token,Func decode) { var result=new Dictionary(); foreach(var p in Object(token).Properties()) { int key=Int32(p.Name); if(result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key,decode(p.Value)); } return result; } internal static List ArrayValues(JToken token,Func decode) { if(!(token is JArray array)) throw new PlayerKVSchemaException(); return array.Select(decode).ToList(); } static PlayerKVBuffValue Buff(JToken token,PlayerKVSchemaSnapshot snapshot) { var obj=Object(token); // isNew/addDown are explicitly NonSerialized runtime fields, not saved fields. return new PlayerKVBuffValue(Integer(obj["SortID"]),Integer(obj["sourceID"]),Integer(obj["buffID"]), Boolean(obj["isEnd"]),Date(String(obj["buffEndTime"]),snapshot)); } internal static PlayerKVBuffState DecodeBuffs(IReadOnlyDictionary raw,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; var map=new Dictionary(); var global=new Dictionary(); var target=new List(); PlayerKVBuffValue daily=null; if(raw.TryGetValue("BuffDataList",out var text)) map=errors.Capture(6,0,()=>IntegerDictionary(Json(text),t=>Buff(t,snapshot))); if(raw.TryGetValue("TargetBuffDataList",out var targetText)) target=errors.Capture(6,1,()=>ArrayValues(Json(targetText),t=>Buff(t,snapshot))); if(raw.TryGetValue("FishDailyBuff",out var dailyText)) daily=errors.Capture(6,2,()=>Buff(Json(dailyText),snapshot)); if(raw.TryGetValue("GlobalBuffDataList",out var globalText)) global=errors.Capture(6,3,()=>IntegerDictionary(Json(globalText),t=>Buff(t,snapshot))); return errors.TotalErrors==before ? new PlayerKVBuffState(map,global,target,daily) : null; } static PlayerKVTriggerValue Trigger(JToken token) { var obj=Object(token); return new PlayerKVTriggerValue(Integer(obj["ID"]),Integer(obj["index"]),Integer(obj["lastID"]), Integer(obj["vipLevel"]),Integer(obj["mapID"]),String(obj["time"])); } static PlayerKVGiftVipValue GiftVip(JToken token) { var obj=Object(token); return new PlayerKVGiftVipValue(Integer(obj["oldVipLevel"]),Integer(obj["curVipLevel"]),Integer(obj["timer"])); } internal static PlayerKVFixedTimeValue FixedTime(JToken token) { var obj=Object(token); return new PlayerKVFixedTimeValue(Integer(obj["id"]),String(obj["endTime"])); } internal static PlayerKVTriggerState DecodeTriggers(IReadOnlyDictionary raw,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; var packs=new List(); var gifts=new Dictionary(StringComparer.Ordinal); var fixedValues=new Dictionary(); if(raw.TryGetValue("TriggerPackDataList",out var text)) packs=errors.Capture(13,0,()=>ArrayValues(Json(text),Trigger)); if(raw.TryGetValue("GiftVIPDData",out var giftText)) gifts=errors.Capture(13,1,()=>Object(Json(giftText)).Properties().ToDictionary(p=>p.Name,p=>GiftVip(p.Value),StringComparer.Ordinal)); if(raw.TryGetValue("FixedTimeShopDic",out var fixedText)) fixedValues=errors.Capture(13,2,()=>IntegerDictionary(Json(fixedText),FixedTime)); return errors.TotalErrors==before ? new PlayerKVTriggerState(packs,gifts,fixedValues) : null; } static PlayerKVDailyValue Daily(JToken token) { var o=Object(token); return new PlayerKVDailyValue(Integer(o["ID"]),Integer(o["index"]),Integer(o["lastID"]),String(o["time"])); } static PlayerKVAdvertValue Advert(JToken token) { var o=Object(token); return new PlayerKVAdvertValue(Integer(o["ID"]),Integer(o["index"]),Integer(o["day"]),o["cdTime"]?.Type == JTokenType.Null ? null : String(o["cdTime"]),Boolean(o["isOpen"])); } static PlayerKVShopItemValue ShopItem(JToken token) { var o=Object(token); return new PlayerKVShopItemValue(Integer(o["ItemID"]),Finite(o["Discount"]),Integer(o["hasBought"])); } static PlayerKVCouponState Coupon(string text) { if(text==null) throw new PlayerKVSchemaException(); var fields=text.Split(','); if(fields.Length!=7 && fields.Length!=9) throw new PlayerKVSchemaException(); var n=fields.Select(Int32).ToArray(); if(n.Length==9 && n[8]!=0 && n[8]!=1) throw new PlayerKVSchemaException(); DateTime date; try { date=new DateTime(n[0],n[1],n[2],n[3],n[4],n[5]); } catch(ArgumentOutOfRangeException) { throw new PlayerKVSchemaException(); } return new PlayerKVCouponState(date,n[6],n.Length==9 ? n[7] : 0,n.Length==9 && n[8]==1); } internal static PlayerKVShopState DecodeShop(IReadOnlyDictionary raw,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; T Read(string key,int field,Func decode,T missing) => raw.TryGetValue(key,out var text) ? errors.Capture(4,field,()=>decode(text)) : missing; var daily=Read("ShopPackBuyCount",0,t=>Object(Json(t)).Properties().ToDictionary(p=>p.Name,p=>Integer(p.Value),StringComparer.Ordinal),new Dictionary()); var all=Read("ShopAllPackBuyCount",1,t=>IntegerMap(Json(t)),new Dictionary()); var gift=Read("ShopGiftBuyCount",2,t=>IntegerMap(Json(t)),new Dictionary()); int voucher=Read("Voucher",3,Int32,0); var chain=Read("ChainPackDataNew",4,t=>Daily(Json(t)),(PlayerKVDailyValue)null); var vip=Read("VIPAdPackData",5,t=>Advert(Json(t)),(PlayerKVAdvertValue)null); var turntable=Read("TurntableAdData",6,t=>Daily(Json(t)),(PlayerKVDailyValue)null); var buy=Read("BuyData",7,t=>ArrayValues(Json(t),Finite),new List()); var advert=Read("AdvertAllCount",8,t=>IntegerMap(Json(t)),new Dictionary()); int count=Read("DiamondShopRefreshCount",9,Int32,0); var time=Read("DiamondShopRefreshTime",10,t=>Date(t,snapshot),default(DateTime)); var items=Read("DiamondShopItemRecords",11,t=>IntegerDictionary(Json(t),ShopItem),new Dictionary()); var coupon=Read("BlackFridayCouponDate",12,Coupon,new PlayerKVCouponState(default,0,0,false)); return errors.TotalErrors==before ? new PlayerKVShopState(daily,all,gift,advert,items,buy,voucher,count,time,chain,turntable,vip,coupon) : null; } static PlayerKVLimitedValue Limited(JToken token) { var o=Object(token); var t=o["duration"]; if(t==null || (t.Type!=JTokenType.Float && t.Type!=JTokenType.Integer) || !double.TryParse(t.ToString(Formatting.None),NumberStyles.Float,Invariant,out var d) || double.IsNaN(d) || double.IsInfinity(d)) throw new PlayerKVSchemaException(); return new PlayerKVLimitedValue(Integer(o["id"]),d,String(o["startTime"])); } static Dictionary PreferIntMap(IReadOnlyDictionary raw,string oldKey, IReadOnlyDictionary modern,string newKey,PlayerKVSchemaDiagnostics errors,int module,int field) { Dictionary old=null,current=null; if(raw.TryGetValue(oldKey,out var text)) old=errors.Capture(module,field,()=>IntegerMap(Json(text))); if(modern.TryGetValue(newKey,out var compact)) current=errors.Capture(module,field+1,()=>ColonIntegers(compact)); if(old!=null && current!=null && old.Any(p=>current.TryGetValue(p.Key,out var n) && n!=p.Value)) errors.Add(module,field,PlayerKVSchemaError.ConflictingRepresentations); return current ?? old ?? new Dictionary(); } internal static PlayerKVEventBaseState DecodeEventBase(IReadOnlyDictionary raw,IReadOnlyDictionary modern, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; T Read(string key,int field,Func decode,T missing) => raw.TryGetValue(key,out var text) ? errors.Capture(5,field,()=>decode(text)) : missing; var limited=Read("LimitedTimeEventDic",0,t=> { var all=IntegerDictionary(Json(t),Limited); // Validate even expired entries, then filter solely against the epoch's captured clock. return all.Where(p=>(snapshot.NowGame-Date(p.Value.StartTime,snapshot)).TotalMinutesp.Key,p=>p.Value); },new Dictionary()); var fixedValues=Read("FixedTimeEventDic",1,t=>IntegerDictionary(Json(t),FixedTime),new Dictionary()); var target=PreferIntMap(raw,"ETDropAfterDropDic",modern,"ct",errors,5,2); var rank=PreferIntMap(raw,"RankTargetCount",modern,"rt",errors,5,4); var wheel=Read("WheelLevelCount",6,t=>IntegerMap(Json(t)),new Dictionary()); int coupon=Read("TurntableRoll",7,Int32,snapshot.InitWheelTicket),operate=Read("WheelOperateCount",8,Int32,0), allOperate=Read("AllWheelOperateCount",9,Int32,0),free=Read("WheelLevelFreeCount",10,Int32,0),pvp=Read("PVPToken",11,Int32,0); return errors.TotalErrors==before ? new PlayerKVEventBaseState(limited,fixedValues,target,rank,wheel,coupon,operate,allOperate,free,pvp) : null; } static PlayerKVFishValue FishValue(JToken token,bool compact) { if(!compact) { var o=Object(token); return new PlayerKVFishValue(Integer(o["Level"]),Finite(o["MaxG"]),Integer(o["TotalCard"]),Integer(o["TotalNum"]),Boolean(o["NewFish"])); } var f=String(token).Split('|'); if(f.Length!=5 && f.Length!=9) throw new PlayerKVSchemaException(); int c=f.Length==5 ? 2 : 5; if(!bool.TryParse(f[c+2],out var isNew)) throw new PlayerKVSchemaException(); return new PlayerKVFishValue(Int32(f[0]),Single(f[1]),Int32(f[c]),Int32(f[c+1]),isNew, f.Length==9 ? new[] { f[2],f[3],f[4],f[8] } : Array.Empty()); } static PlayerKVRodAccessories Accessories(string text) { var o=Object(Json(text)); return new PlayerKVRodAccessories(Integer(o["GloveID"]),IntegerMap(o["RodToSkin"]),IntegerList(o["GloveIds"]), IntegerList(o["SkinIds"]),IntegerList(o["GloveNewIds"]),IntegerList(o["SkinNewIds"])); } static Dictionary Luck(string text,bool compact) { if(text==null) throw new PlayerKVSchemaException(); Dictionary result; if(compact) { var f=text.Split('|'); if(f.Length!=3) throw new PlayerKVSchemaException(); result=new Dictionary(StringComparer.Ordinal) { ["InitFishLuck"]=Single(f[0]),["InitBotLuck"]=Single(f[1]),["BotCount"]=Single(f[2]) }; } else result=Object(Json(text)).Properties().ToDictionary(p=>p.Name,p=>Finite(p.Value),StringComparer.Ordinal); if(result.TryGetValue("BotCount",out var count) && ((double)countint.MaxValue || Math.Truncate((double)count)!=count)) throw new PlayerKVSchemaException(); return result; } static float ConfigFinite(float value) { if(float.IsNaN(value) || float.IsInfinity(value)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); return value; } internal static float RoundFishWeight(float value) { float scaled=ConfigFinite(value*100f); return (float)Math.Round((double)scaled,MidpointRounding.ToEven)/100f; } static PlayerKVFishValue DeriveFish(int id,int map,PlayerKVFishValue raw,PlayerKVSchemaSnapshot snapshot,out int cardId) { var fish=snapshot.Fish(id); if(fish==null || fish.Id!=id || fish.Area!=map) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); cardId=fish.Card; var card=snapshot.Card(cardId); int level=raw.Level,count=raw.Cards; float weight=raw.Weight; if(fish.Quality<=5) { float limit=ConfigFinite(ConfigFinite(fish.Weight)*ConfigFinite(fish.Extra)); limit=ConfigFinite(limit*ConfigFinite(1f+ConfigFinite(fish.Random))); if(level>0) { if(card==null || card.Id!=cardId || level>card.Weights.Count) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); limit=ConfigFinite(limit*ConfigFinite(1f+ConfigFinite(card.Weights[level-1]))); } limit=RoundFishWeight(limit); if(weight>limit) weight=limit; } if(level>0) { if(card==null || card.Id!=cardId || card.MaxLevel<1) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); level=Math.Min(level,card.MaxLevel); if(level>card.Required.Count) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); count=Math.Max(count,card.Required[level-1]); } return new PlayerKVFishValue(level,weight,count,raw.Number,raw.New,raw.Compatibility); } internal static PlayerKVFishState DecodeFish(IReadOnlyDictionary raw,IReadOnlyDictionary modern, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; Dictionary Read(string key,int field) => raw.TryGetValue(key,out var text) ? errors.Capture(3,field,()=>IntegerMap(Json(text))) : new Dictionary(); var rods=Read("RodData",0); var levels=Read("RodLevelData",1); var swim=Read("FishSwimToMap",2); var swimCount=Read("FishSwimToMapCount",3); var progress=Read("MapProgress",4); var honor=Read("HonorLevelData",5); PlayerKVRodAccessories accessories=null; if(raw.TryGetValue("InitRodAccessoriesData",out var acc)) accessories=errors.Capture(3,6,()=>Accessories(acc)); var lucks=new List>(); if(raw.TryGetValue("LuckDataDic",out var top)) lucks.Add(errors.Capture(3,7,()=>Luck(top,false))); if(modern.TryGetValue("LDD",out var oldLuck)) lucks.Add(errors.Capture(3,8,()=>Luck(oldLuck,false))); if(modern.TryGetValue("ld",out var newLuck)) lucks.Add(errors.Capture(3,9,()=>Luck(newLuck,true))); for(int i=0;ilucks[j].TryGetValue(p.Key,out var v) && v!=p.Value)) errors.Add(3,7,PlayerKVSchemaError.ConflictingRepresentations); var luck=lucks.LastOrDefault() ?? new Dictionary(StringComparer.Ordinal); var legacy=new Dictionary>(); var current=new Dictionary>(); int field=100; foreach(var pair in raw.OrderBy(p=>p.Key,StringComparer.Ordinal)) { var match=System.Text.RegularExpressions.Regex.Match(pair.Key,"^Map_(5_)?([0-9]+)$"); if(!match.Success) continue; errors.Capture(3,field++,() => { string suffix=match.Groups[2].Value; int map=Int32(suffix); if(map<=0 || suffix!=map.ToString(Invariant)) throw new PlayerKVSchemaException(); if(!snapshot.HasMap(map)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); bool compact=match.Groups[1].Success; var target=compact ? current : legacy; target.Add(map,IntegerDictionary(Json(pair.Value),t=>FishValue(t,compact))); return true; }); } foreach(var map in legacy) if(current.TryGetValue(map.Key,out var other) && map.Value.Any(p=>other.TryGetValue(p.Key,out var v) && !p.Value.Same(v))) errors.Add(3,10,PlayerKVSchemaError.ConflictingRepresentations); var selected=legacy.ToDictionary(p=>p.Key,p=>p.Value); foreach(var map in current) selected[map.Key]=map.Value; var maps=new Dictionary>(); var fishIndex=new Dictionary(); var cards=new Dictionary(); // Conflicting raw representations cannot become valid after normalization. if(errors.TotalErrors==before) errors.Capture(3,11,() => { foreach(var map in selected.OrderBy(p=>p.Key)) { var values=new Dictionary(); foreach(var p in map.Value.OrderBy(p=>p.Key)) { var value=DeriveFish(p.Key,map.Key,p.Value,snapshot,out var card); if(fishIndex.ContainsKey(p.Key) || (cards.TryGetValue(card,out var n) && n!=value.Cards)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); values.Add(p.Key,value); fishIndex.Add(p.Key,value); cards[card]=value.Cards; } maps.Add(map.Key,values); } return true; }); return errors.TotalErrors==before ? new PlayerKVFishState(rods,levels,swim,swimCount,progress,honor,luck,accessories,selected,maps,fishIndex,cards) : null; } internal static PlayerKVPlayerState DecodePlayer(IReadOnlyDictionary raw, IReadOnlyDictionary modern,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; var core=DecodeCore(raw,modern,snapshot,errors); var scores=new Dictionary(); var rods=new List(); int season=0; string building="0"; bool appraise=raw.TryGetValue("IsOpenAppraise",out var appraiseText); if(appraise && appraiseText==null) errors.Add(1,100,PlayerKVSchemaError.Malformed); if(raw.TryGetValue("NewRodList",out var rodText)) rods=errors.Capture(1,101,()=>IntegerList(Json(rodText))); if(raw.TryGetValue("SeasonId",out var seasonText)) season=errors.Capture(1,102,()=>Int32(seasonText)); if(raw.TryGetValue("InfiniteBuildingData",out var buildingText)) { if(buildingText==null) errors.Add(1,103,PlayerKVSchemaError.Malformed); else building=buildingText; } if(modern!=null && modern.TryGetValue("vipL",out var vipText)) scores=errors.Capture(1,104,() => { if(vipText==null) throw new PlayerKVSchemaException(); var result=new Dictionary(); if(vipText.Length==0) return result; foreach(var item in vipText.Split('|')) { var fields=item.Split(','); if(fields.Length!=2) throw new PlayerKVSchemaException(); int key=Int32(fields[0]); if(result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key,Int32(fields[1])); } return result; }); return core!=null && modern!=null && errors.TotalErrors==before ? new PlayerKVPlayerState(core,modern.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal),scores,rods,season,appraise,building) : null; } static PlayerKVPotionItem PotionItem(JToken token) { var obj=Object(token); int type=Integer(obj["DataType"]); var shelves=IntegerList(obj["ShelfTypeList"]); var grids=IntegerList(obj["GridStateList"]); if(type<0 || type>2 || shelves.Any(n=>n<0 || n>1) || grids.Any(n=>n<0 || n>5)) throw new PlayerKVSchemaException(); return new PlayerKVPotionItem(String(obj["name"]),Integer(obj["index"]),Boolean(obj["isOver"]),type, IntegerList(obj["ColorTypeList"]),shelves,grids,IntegerList(obj["FromDisplayAreaIndexList"])); } internal static PlayerKVPotionState DecodePotion(IReadOnlyDictionary raw,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("epdk",out var text)) return PlayerKVPotionState.Missing(); return errors.Capture(25,0,() => { var obj=Object(Json(text)); return new PlayerKVPotionState(true, Integer(obj["EventID"]), Integer(obj["PotionID"]), Integer(obj["StageID"]), Integer(obj["Stage1TotalLevel"]), Integer(obj["Stage2TotalLevel"]), Integer(obj["LevelID"]), Integer(obj["LevelIndex"]), Integer(obj["CorrectNum"]), Integer(obj["SubmitNum"]), Integer(obj["Token"]), Integer(obj["ConsumedToken"]), Integer(obj["Rule"]), Integer(obj["PreEventID"]), Integer(obj["PrePotionID"]), Integer(obj["PreStageID"]), Integer(obj["PreLevelID"]), Integer(obj["Round"]), Integer(obj["FirstPlayedEventId"]), Boolean(obj["IsOver"]), String(obj["Name"]), Finite(obj["InflationRate"]), PotionItem(obj["LevelTargetData"]), PotionItem(obj["LevelDisplayData"]), ArrayValues(obj["LevelProcessedList"],PotionItem)); }); } static List CsvIntegers(string text) => text.Length==0 ? new List() : text.Split(',').Select(Int32).ToList(); static Dictionary ColonIntegers(string text) { if(text==null) throw new PlayerKVSchemaException(); var result=new Dictionary(); if(text.Length==0) return result; foreach(var part in text.Split(',')) { var fields=part.Split(':'); if(fields.Length!=2) throw new PlayerKVSchemaException(); int key=Int32(fields[0]); if(result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key,Int32(fields[1])); } return result; } static DateTime Ticks(string text) { if(!long.TryParse(text,NumberStyles.AllowLeadingSign,Invariant,out var value) || value<0 || value>System.DateTime.MaxValue.Ticks) throw new PlayerKVSchemaException(); return new DateTime(value); } static PlayerKVBattlePassState CompactBattlePass(string text) { var pair=text?.Split('#'); if(pair==null || pair.Length!=2) throw new PlayerKVSchemaException(); var record=pair[0].Split('|'); var task=pair[1].Split('|'); if(record.Length!=7 || task.Length!=5) throw new PlayerKVSchemaException(); return new PlayerKVBattlePassState(new PlayerKVPassRecord(Int32(record[0]),Int32(record[1]),Int32(record[2]), Int32(record[3]),Int32(record[4]),CsvIntegers(record[5]),CsvIntegers(record[6]),Array.Empty(),0), new PlayerKVPassTask(ColonIntegers(task[0]),ColonIntegers(task[1]),CsvIntegers(task[2]),Ticks(task[3]),Ticks(task[4]))); } static PlayerKVPassRecord OldPassRecord(string text) { var obj=Object(Json(text)); return new PlayerKVPassRecord(Integer(obj["ID"]),Integer(obj["Level"]),Integer(obj["Exp"]),Integer(obj["BuyVipToastIndex"]), Integer(obj["IsUnlockVip"]),IntegerList(obj["NormalRewardGet"]),IntegerList(obj["VipRewardGet"]), IntegerList(obj["LastDropIDs"]),Finite(obj["InflationRate"])); } static PlayerKVPassTask OldPassTask(string text,PlayerKVSchemaSnapshot snapshot) { var obj=Object(Json(text)); return new PlayerKVPassTask(IntegerMap(obj["dic_dailyTasks"]),IntegerMap(obj["dic_weeklyTasks"]),IntegerList(obj["loginDays"]), Date(String(obj["DailyTaskEndTime"]),snapshot),Date(String(obj["WeeklyTaskEndTime"]),snapshot)); } internal static PlayerKVBattlePassState DecodeBattlePass(IReadOnlyDictionary raw,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; PlayerKVPassRecord oldRecord=null; PlayerKVPassTask oldTask=null; PlayerKVBattlePassState current=null; if(raw.TryGetValue("BattlePassRecord",out var record)) oldRecord=errors.Capture(11,0,()=>OldPassRecord(record)); if(raw.TryGetValue("BattleTask",out var task)) oldTask=errors.Capture(11,1,()=>OldPassTask(task,snapshot)); if(raw.TryGetValue("BattlePassRecordTask",out var compact)) current=errors.Capture(11,2,()=>CompactBattlePass(compact)); if(current!=null && oldRecord!=null && !oldRecord.CommonEquals(current.Record)) errors.Add(11,3,PlayerKVSchemaError.ConflictingRepresentations); if(current!=null && oldTask!=null && !oldTask.Same(current.Task)) errors.Add(11,4,PlayerKVSchemaError.ConflictingRepresentations); return errors.TotalErrors==before ? current ?? new PlayerKVBattlePassState(oldRecord ?? PlayerKVPassRecord.Empty(),oldTask ?? PlayerKVPassTask.Empty()) : null; } internal static PlayerKVCubeState DecodeCube(IReadOnlyDictionary raw,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("EventCubeMeltData",out var text)) return PlayerKVCubeState.Missing(); return errors.Capture(19,0,() => { var obj=Object(Json(text)); var size=Object(obj["BoardViewSize"]); var items=ArrayValues(obj["CollectItems"],t => { var item=Object(t); return new PlayerKVCollectValue( Integer(item["ItemId"]),Integer(item["Count"]),Integer(item["MaxCount"]),Integer(item["ItemTrueId"])); }); var pieces=ArrayValues(obj["PieceDatas"],IntegerList); return new PlayerKVCubeState(true,Integer(obj["TicketCount"]),Boolean(obj["FirstOpen"]),Boolean(obj["IsFinished"]), Integer(obj["EventId"]),Boolean(obj["IsInGuide"]),Date(String(obj["StartTime"]),snapshot),Date(String(obj["EndTime"]),snapshot), Integer(obj["CurrentStagetId"]),items,IntegerList(obj["BoardData"]),Integer(size["x"]),Integer(size["y"]), IntegerList(obj["CollectRewardsId"]),Boolean(obj["IsFinalRewardGet"]),pieces,Integer(obj["Cycle"]),Finite(obj["InflationRate"])); }); } internal static PlayerKVPinataState DecodePinata(IReadOnlyDictionary raw,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("EventPinataPackData",out var text)) return new PlayerKVPinataState(0,0,0); return errors.Capture(29,0,() => { var obj=Object(Json(text)); return new PlayerKVPinataState(Integer(obj["EventID"]),Integer(obj["PinataMainID"]),Integer(obj["PurchasedCount"])); }); } internal static PlayerKVChallengeState DecodeChallenge(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("FishingChallengeData",out var text)) return PlayerKVChallengeState.Missing(); return errors.Capture(15,0,() => { // Json rejects root null before returning; recognize this one empty state before Object(Json(...)). if (text != null && text.Trim(' ', '\t', '\r', '\n') == "null") { StrictJsonSyntax.Validate(text); return PlayerKVChallengeState.ExplicitNull(); } var obj=Object(Json(text)); return new PlayerKVChallengeState(true, Boolean(obj["FirstOpen"]), Integer(obj["EventId"]), Integer(obj["RankTierLevel"]), Integer(obj["Finished"]), Integer(obj["RedirectID"]), Integer(obj["Score"]), Integer(obj["ProcessedScore"]), Integer(obj["step"]), Integer(obj["uiStep"]), Integer(obj["index"]), Date(String(obj["StartTime"]),snapshot), Date(String(obj["EndTime"]),snapshot), IntegerList(obj["eliminateNumber"]), IntegerList(obj["robotAccountList"]), Finite(obj["InflationRate"])); }); } internal static PlayerKVPinballState DecodePinball(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("epbdk",out var text)) return PlayerKVPinballState.Missing(); return errors.Capture(26,0,() => { var obj=Object(Json(text)); return new PlayerKVPinballState(true, Integer(obj["EventID"]), Integer(obj["PinballMainID"]), Integer(obj["SpinPoint"]), Integer(obj["Scores"]), Integer(obj["Multiple"]), Integer(obj["Token"]), Integer(obj["ConsumedToken"]), Integer(obj["RewardIndex"]), Finite(obj["Force"]), Finite(obj["LuckyValue"]), Finite(obj["InflationRate"])); }); } internal static PlayerKVScratchState DecodeScratch(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("estdk",out var text)) return PlayerKVScratchState.Missing(); return errors.Capture(27,0,() => { var obj=Object(Json(text)); return new PlayerKVScratchState(true, Integer(obj["EventID"]), Integer(obj["ScratchMainID"]), Integer(obj["ChainPackID"]), Integer(obj["Token"]), Integer(obj["ConsumedToken"]), Integer(obj["RewardTaskConsumedToken"]), Integer(obj["EventScratchRewardTaskID"]), Integer(obj["EventScratchCardID"]), Integer(obj["TotalConsumedToken"]), Integer(obj["ChainPackProgress"]), Integer(obj["magIndex"]), Boolean(obj["IsOver"]), Finite(obj["LuckyValue"]), Finite(obj["InflationRate"]), IntegerList(obj["StampRewardList"]), IntegerDictionary(obj["StampRewardDict"],t=> { var item=Object(t); return new PlayerKVStampValue(Integer(item["EventScratchCardID"]),Integer(item["Count"])); })); }); } internal static PlayerKVPartnerState DecodePartner(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { if(!raw.TryGetValue("EventPartnerGatherData",out var text)) return PlayerKVPartnerState.Missing(); return errors.Capture(18,0,()=> { var obj=Object(Json(text)); int spin=Integer(obj["SpinState"]); if(spin<0 || spin>2) throw new PlayerKVSchemaException(); var bonus=Object(obj["PartnerBonus"]); return new PlayerKVPartnerState(true,Boolean(obj["FirstOpen"]),Integer(obj["EventId"]), Date(String(obj["StartTime"]),snapshot),Date(String(obj["EndTime"]),snapshot), Integer(obj["CurrentPoints"]),Integer(obj["CurrentMiningProgress"]),Integer(obj["Token"]), Integer(obj["InvitedCount"]),Integer(obj["TicketCount"]),ArrayValues(obj["CompletedNodesList"],IntegerList), ArrayValues(obj["BuilderPartnerScore"],t=> { var score=Object(t); return (Integer(score["Item1"]),Integer(score["Item2"])); }), Finite(bonus["Item1"]),Date(String(bonus["Item2"]),snapshot),Integer(obj["FreeSpinNum"]), IntegerDictionary(obj["Bots"],t=> { var bot=Object(t); return new PlayerKVPartnerBot(String(bot["Id"]), Integer(bot["ScorePlayer"]),Integer(bot["ScoreBot"]),Integer(bot["Grade"]),Integer(bot["TargetScore"]), Date(String(bot["WakeStartTime"]),snapshot),Integer(bot["WakingSeconds"])); }), Boolean(obj["IsRecvFinalReward"]),ArrayValues(obj["UIFinishedPartners"],Boolean),Boolean(obj["IsFinished"]), spin,Finite(obj["InflationRate"]),Boolean(obj["IsEnableAutoMatch"]),Date(String(obj["LastAutoMatchTime"]),snapshot)); }); } internal static PlayerKVOfferState DecodeOffer(IReadOnlyDictionary raw,bool disco, PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue(disco ? "OfferDiscoPartyManager" : "OfferChainsChestData",out var text)) return PlayerKVOfferState.Missing(); return errors.Capture(disco ? 17 : 16,0,() => { var obj=Object(Json(text)); if (!(obj["Items"] is JArray array)) throw new PlayerKVSchemaException(); var items=new List(); foreach(var token in array) { var item=Object(token); int status=Integer(item["Status"]); if (disco && (status<0 || status>2)) throw new PlayerKVSchemaException(); items.Add(new PlayerKVOfferItem(Integer(item["Id"]),Integer(item["Item"]),Integer(item["Number"]),status)); } return new PlayerKVOfferState(true,Boolean(obj["FirstOpen"]),Integer(obj["EventId"]), Date(String(obj["StartTime"]),snapshot),Date(String(obj["EndTime"]),snapshot), Integer(obj["ItemId"]),Integer(obj["LotteryNum"]),items,IntegerList(obj["RewardList"]), !disco && Boolean(obj["Finished"]),disco ? 0 : Integer(obj["ChestReward"])); }); } internal static PlayerKVEventPackState DecodeEventPack(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue("EventPackData",out var text)) return new PlayerKVEventPackState(0,0,0,0); return errors.Capture(20,0,() => { var obj=Object(Json(text)); int eventId=Integer(obj["currentEventID"]), count=Integer(obj["purchaseCount"]), vip=Integer(obj["VIPLvlWhenEventActivate"]), redirect=Integer(obj["redirectID"]); var config=snapshot.Event(eventId); if (config != null) { // The legacy loader evaluates EndTime before any pack-table dereference. if (config.EndTime == null) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); if (Date(config.EndTime,snapshot) > snapshot.NowUtc) { if (!snapshot.HasPackType(config.RedirectId,1)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); if (redirect == 0) redirect=4054001; if (!snapshot.HasPack(redirect)) throw new PlayerKVSchemaException(PlayerKVSchemaError.UnsupportedSchema); } } return new PlayerKVEventPackState(eventId,count,vip,redirect); }); } internal static PlayerKVCampState DecodeCamp(IReadOnlyDictionary raw,PlayerKVSchemaDiagnostics errors) { int before=errors.TotalErrors; bool fix=false; string skyscraper="0"; var data=new Dictionary(StringComparer.Ordinal); if (raw.TryGetValue("IsFix",out var fixText)) fix=errors.Capture(7,0,() => { if (!bool.TryParse(fixText,out var value)) throw new PlayerKVSchemaException(); return value; }); if (raw.TryGetValue("IsOpenSkyscraper",out var skyText)) { if (skyText != "0" && skyText != "1") errors.Add(7,1,PlayerKVSchemaError.Malformed); else skyscraper=skyText; } if (raw.TryGetValue("CampDataDic",out var dataText)) data=errors.Capture(7,2,() => Object(Json(dataText)).Properties().ToDictionary(p=>p.Name,p=>Integer(p.Value),StringComparer.Ordinal)); return errors.TotalErrors == before ? new PlayerKVCampState(fix,skyscraper,data) : null; } internal static PlayerKVClubState DecodeClub(IReadOnlyDictionary raw, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors) { int before = errors.TotalErrors; DateTime? sign = null; DateTime donateTime = default, requestEnd = default; int count = 0; if (raw.TryGetValue("ClubSignin",out var signText)) sign = errors.Capture(14,0,() => signText == "" ? (DateTime?)null : Date(signText,snapshot)); if (raw.TryGetValue("LureDonateData",out var donateText)) errors.Capture(14,1,() => { var obj = Object(Json(donateText)); donateTime = Date(String(obj["DonateCountTime"]),snapshot); count = Integer(obj["ClubLureDonateCount"]); requestEnd = Date(String(obj["ClubLureRequestCDEnd"]),snapshot); return true; }); bool openRed = !raw.TryGetValue("IsOpenClubRed",out var redText); if (!openRed && redText == null) errors.Add(14,2,PlayerKVSchemaError.Malformed); return errors.TotalErrors == before ? new PlayerKVClubState(sign,donateTime,count,requestEnd,openRed) : null; } internal static PlayerKVProgressPackState DecodeProgressPack(IReadOnlyDictionary raw, bool migration, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue(migration ? "ThanksGivingPackMigrationData" : "ThanksGivingPackData",out var text)) return new PlayerKVProgressPackState(false,0,0,0,0); return errors.Capture(28,migration ? 1 : 0,() => { var parts = text?.Split(','); if (parts == null || parts.Length != 4) throw new PlayerKVSchemaException(); return new PlayerKVProgressPackState(true,Int32(parts[0]),Int32(parts[1]),Int32(parts[2]),Int32(parts[3])); }); } internal static Dictionary IntegerMap(JToken token) { var result = new Dictionary(); foreach (var p in Object(token).Properties()) { int key = Int32(p.Name); if (result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key, Integer(p.Value)); } return result; } static Dictionary IntegerPairs(string text) { if (text == null) throw new PlayerKVSchemaException(); var result = new Dictionary(); if (text.Length == 0) return result; foreach (var item in text.Split(';')) { var fields = item.Split(','); if (fields.Length != 2) throw new PlayerKVSchemaException(); int key = Int32(fields[0]); if (result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key, Int32(fields[1])); } return result; } internal static PlayerKVIntMapState DecodeFishingBox(IReadOnlyDictionary raw, IReadOnlyDictionary modern, PlayerKVSchemaDiagnostics errors) { if (modern == null) return null; int before = errors.TotalErrors; Dictionary old = null, current = null; if (raw.TryGetValue("FishingBoxData",out var legacy)) old = errors.Capture(9,0,()=>IntegerMap(Object(Json(legacy))["FishingBoxOpenNum"])); if (modern.TryGetValue("bd",out var compact)) current = errors.Capture(9,1,()=>IntegerPairs(compact)); if (old != null && current != null) foreach (var item in old.OrderBy(p=>p.Key)) if (current.TryGetValue(item.Key,out var value) && value != item.Value) errors.Add(9,2,PlayerKVSchemaError.ConflictingRepresentations); return errors.TotalErrors == before ? new PlayerKVIntMapState(current ?? old ?? new Dictionary()) : null; } static Dictionary GuideRecords(string text, bool compact) { var result = new Dictionary(StringComparer.Ordinal); if (compact) { if (text == null) throw new PlayerKVSchemaException(); if (text.Length == 0) return result; foreach (var item in text.Split(';')) { var parts = item.Split('|'); if (parts.Length != 3 || (parts[2] != "0" && parts[2] != "1") || result.ContainsKey(parts[0])) throw new PlayerKVSchemaException(); result.Add(parts[0], new PlayerKVGuideValue(Int32(parts[1]), parts[2] == "1", parts[0])); } } else foreach (var p in Object(Json(text)).Properties()) { var value = Object(p.Value); result.Add(p.Name, new PlayerKVGuideValue(Integer(value["guideSubIndex"]),Boolean(value["isFinish"]),String(value["groupName"]))); } return result; } internal static PlayerKVGuideState DecodeGuides(IReadOnlyDictionary raw, PlayerKVSchemaDiagnostics errors) { int before = errors.TotalErrors; Dictionary old = null, current = null; if (raw.TryGetValue("GuideSaveData",out var legacy)) old = errors.Capture(10,0,()=>GuideRecords(legacy,false)); if (raw.TryGetValue("GuideSaveDataNew",out var compact)) current = errors.Capture(10,1,()=>GuideRecords(compact,true)); if (old != null && current != null) foreach (var item in old.OrderBy(p=>p.Key,StringComparer.Ordinal)) if (current.TryGetValue(item.Key,out var value) && !value.Same(item.Value)) errors.Add(10,2,PlayerKVSchemaError.ConflictingRepresentations); return errors.TotalErrors == before ? new PlayerKVGuideState(current ?? old ?? new Dictionary()) : null; } internal static PlayerKVMonthlySignState DecodeMonthlySign(IReadOnlyDictionary raw, PlayerKVSchemaDiagnostics errors) { if (!raw.TryGetValue("MonthlySignEvent",out var text)) return new PlayerKVMonthlySignState(false,0,0,0,0,null); return errors.Capture(12,0,() => { var obj = Object(Json(text)); var daily = Object(obj["dailySignEvent"]); return new PlayerKVMonthlySignState(true,Integer(obj["index"]),Integer(daily["ID"]), Integer(daily["index"]),Integer(daily["lastID"]),String(daily["time"])); }); } internal static PlayerKVFootPrintState DecodeFootPrint(IReadOnlyDictionary raw, PlayerKVSchemaDiagnostics errors) => errors.Capture(23, 0, () => new PlayerKVFootPrintState(raw.TryGetValue("FootPrint", out var text) ? UInt64(text) : 0)); } // Validate RFC JSON syntax before Json.NET, which also accepts single quotes, unquoted names and trailing commas. internal sealed class StrictJsonSyntax { readonly string text; int position; StrictJsonSyntax(string text) { this.text = text; } internal static void Validate(string text) { var parser = new StrictJsonSyntax(text); parser.Value(0); parser.Space(); if (parser.position != text.Length) throw new PlayerKVSchemaException(); } char Peek => position < text.Length ? text[position] : '\0'; static bool Digit(char c) => c >= '0' && c <= '9'; void Space() { while (Peek == ' ' || Peek == '\t' || Peek == '\r' || Peek == '\n') position++; } void Require(char c) { if (Peek != c) throw new PlayerKVSchemaException(); position++; } void Value(int depth) { if (depth > 64) throw new PlayerKVSchemaException(); Space(); if (Peek == '{' || Peek == '[') { bool obj = Peek == '{'; position++; Space(); char end = obj ? '}' : ']'; if (Peek == end) { position++; return; } while (true) { if (obj) { Quoted(); Space(); Require(':'); } Value(depth + 1); Space(); if (Peek == end) { position++; return; } Require(','); Space(); } } else if (Peek == '"') Quoted(); else if (Peek == 't') Literal("true"); else if (Peek == 'f') Literal("false"); else if (Peek == 'n') Literal("null"); else { if (Peek == '-') position++; if (Peek == '0') position++; else { if (Peek < '1' || Peek > '9') throw new PlayerKVSchemaException(); while (Digit(Peek)) position++; } if (Peek == '.') { position++; Digits(); } if (Peek == 'e' || Peek == 'E') { position++; if (Peek == '+' || Peek == '-') position++; Digits(); } } } void Digits() { if (!Digit(Peek)) throw new PlayerKVSchemaException(); while (Digit(Peek)) position++; } void Literal(string value) { foreach (char c in value) Require(c); } void Quoted() { Require('"'); while (true) { char c = Peek; if (c == '"') { position++; return; } if (c < ' ') throw new PlayerKVSchemaException(); position++; if (c != '\\') continue; c = Peek; position++; if (c == 'u') { for (int i = 0; i < 4; i++) { char h = Peek; if (!Digit(h) && !(h >= 'a' && h <= 'f') && !(h >= 'A' && h <= 'F')) throw new PlayerKVSchemaException(); position++; } } else if (c != '"' && c != '\\' && c != '/' && c != 'b' && c != 'f' && c != 'n' && c != 'r' && c != 't') throw new PlayerKVSchemaException(); } } } }