Files
ft/Client/Assets/Scripts/Services/PlayerData/PlayerKVSaveSchema.cs

1169 lines
74 KiB
C#

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<string>(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<string>(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<string,string> raw,PlayerKVIdentitySnapshot identity,
PlayerKVSchemaDiagnostics errors)
{
if(identity==null) throw new ArgumentNullException(nameof(identity));
int before=errors.TotalErrors;
var normalized=new Dictionary<string,string>(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<PlayerKVIdentityChannel>(identity.Linked);
var channels=new HashSet<PlayerKVIdentityChannel>{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<string,string>();
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<int> IntegerList(JToken token)
{
if (!(token is JArray array)) throw new PlayerKVSchemaException();
return array.Select(Integer).ToList();
}
internal static PlayerKVPiggyBankState DecodePiggyBank(IReadOnlyDictionary<string, string> 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<string, string> 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<string, string> 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<string, string> StringMap(string text)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var p in Object(Json(text)).Properties()) result.Add(p.Name, String(p.Value));
return result;
}
internal static Dictionary<string, string> DecodeModern(IReadOnlyDictionary<string, string> raw, PlayerKVSchemaDiagnostics errors)
{
if (!raw.TryGetValue("NewPlayerDataDic", out var text)) return new Dictionary<string, string>(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<string, string> raw,
IReadOnlyDictionary<string, string> 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<int, PlayerKVAchievementValue> result, int key, PlayerKVAchievementValue value)
{ if (result.ContainsKey(key)) throw new PlayerKVSchemaException(); result.Add(key, value); }
static Dictionary<int, PlayerKVAchievementValue> Achievements(string text, int format, PlayerKVSchemaSnapshot snapshot)
{
var result = new Dictionary<int, PlayerKVAchievementValue>();
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<string, string> raw,
IReadOnlyDictionary<string, string> modern, PlayerKVSchemaSnapshot snapshot, PlayerKVSchemaDiagnostics errors)
{
if (modern == null) return null;
int before = errors.TotalErrors;
var representations = new List<Dictionary<int, PlayerKVAchievementValue>>();
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<int, PlayerKVAchievementValue>() : representations[representations.Count - 1]);
}
internal static Dictionary<int,T> IntegerDictionary<T>(JToken token,Func<JToken,T> decode)
{
var result=new Dictionary<int,T>();
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<T> ArrayValues<T>(JToken token,Func<JToken,T> 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<string,string> raw,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
var map=new Dictionary<int,PlayerKVBuffValue>(); var global=new Dictionary<int,PlayerKVBuffValue>();
var target=new List<PlayerKVBuffValue>(); 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<string,string> raw,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
var packs=new List<PlayerKVTriggerValue>(); var gifts=new Dictionary<string,PlayerKVGiftVipValue>(StringComparer.Ordinal);
var fixedValues=new Dictionary<int,PlayerKVFixedTimeValue>();
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<string,string> raw,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
T Read<T>(string key,int field,Func<string,T> 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<string,int>());
var all=Read("ShopAllPackBuyCount",1,t=>IntegerMap(Json(t)),new Dictionary<int,int>());
var gift=Read("ShopGiftBuyCount",2,t=>IntegerMap(Json(t)),new Dictionary<int,int>());
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<float>());
var advert=Read("AdvertAllCount",8,t=>IntegerMap(Json(t)),new Dictionary<int,int>());
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<int,PlayerKVShopItemValue>());
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<int,int> PreferIntMap(IReadOnlyDictionary<string,string> raw,string oldKey,
IReadOnlyDictionary<string,string> modern,string newKey,PlayerKVSchemaDiagnostics errors,int module,int field)
{
Dictionary<int,int> 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<int,int>();
}
internal static PlayerKVEventBaseState DecodeEventBase(IReadOnlyDictionary<string,string> raw,IReadOnlyDictionary<string,string> modern,
PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
T Read<T>(string key,int field,Func<string,T> 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)).TotalMinutes<p.Value.Duration).ToDictionary(p=>p.Key,p=>p.Value);
},new Dictionary<int,PlayerKVLimitedValue>());
var fixedValues=Read("FixedTimeEventDic",1,t=>IntegerDictionary(Json(t),FixedTime),new Dictionary<int,PlayerKVFixedTimeValue>());
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,int>());
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<string>());
}
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<string,float> Luck(string text,bool compact)
{
if(text==null) throw new PlayerKVSchemaException();
Dictionary<string,float> result;
if(compact)
{
var f=text.Split('|'); if(f.Length!=3) throw new PlayerKVSchemaException();
result=new Dictionary<string,float>(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)count<int.MinValue || (double)count>int.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<string,string> raw,IReadOnlyDictionary<string,string> modern,
PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
Dictionary<int,int> Read(string key,int field) => raw.TryGetValue(key,out var text) ? errors.Capture(3,field,()=>IntegerMap(Json(text))) : new Dictionary<int,int>();
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<Dictionary<string,float>>();
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;i<lucks.Count;i++) for(int j=i+1;j<lucks.Count;j++)
if(lucks[i]!=null && lucks[j]!=null && lucks[i].Any(p=>lucks[j].TryGetValue(p.Key,out var v) && v!=p.Value))
errors.Add(3,7,PlayerKVSchemaError.ConflictingRepresentations);
var luck=lucks.LastOrDefault() ?? new Dictionary<string,float>(StringComparer.Ordinal);
var legacy=new Dictionary<int,Dictionary<int,PlayerKVFishValue>>(); var current=new Dictionary<int,Dictionary<int,PlayerKVFishValue>>();
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<int,Dictionary<int,PlayerKVFishValue>>(); var fishIndex=new Dictionary<int,PlayerKVFishValue>(); var cards=new Dictionary<int,int>();
// 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<int,PlayerKVFishValue>();
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<string,string> raw,
IReadOnlyDictionary<string,string> modern,PlayerKVSchemaSnapshot snapshot,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
var core=DecodeCore(raw,modern,snapshot,errors);
var scores=new Dictionary<int,int>(); var rods=new List<int>(); 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<int,int>();
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<string,string> 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<int> CsvIntegers(string text) => text.Length==0 ? new List<int>() : text.Split(',').Select(Int32).ToList();
static Dictionary<int,int> ColonIntegers(string text)
{
if(text==null) throw new PlayerKVSchemaException();
var result=new Dictionary<int,int>();
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<int>(),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<string,string> 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<string,string> 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<string,string> 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<string,string> 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<string,string> 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<string,string> 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<string,string> 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<string,string> 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<PlayerKVOfferItem>();
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<string,string> 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<string,string> raw,PlayerKVSchemaDiagnostics errors)
{
int before=errors.TotalErrors;
bool fix=false;
string skyscraper="0";
var data=new Dictionary<string,int>(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<string,string> 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<DateTime?>(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<string,string> 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<int, int> IntegerMap(JToken token)
{
var result = new Dictionary<int, int>();
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<int, int> IntegerPairs(string text)
{
if (text == null) throw new PlayerKVSchemaException();
var result = new Dictionary<int,int>();
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<string,string> raw,
IReadOnlyDictionary<string,string> modern, PlayerKVSchemaDiagnostics errors)
{
if (modern == null) return null;
int before = errors.TotalErrors;
Dictionary<int,int> 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<int,int>()) : null;
}
static Dictionary<string, PlayerKVGuideValue> GuideRecords(string text, bool compact)
{
var result = new Dictionary<string,PlayerKVGuideValue>(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<string,string> raw, PlayerKVSchemaDiagnostics errors)
{
int before = errors.TotalErrors;
Dictionary<string,PlayerKVGuideValue> 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<string,PlayerKVGuideValue>()) : null;
}
internal static PlayerKVMonthlySignState DecodeMonthlySign(IReadOnlyDictionary<string,string> 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<string, string> 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();
}
}
}
}