1169 lines
74 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
}
|