From 48ed96164837dced1c0dae172671afa14432838d Mon Sep 17 00:00:00 2001 From: Mikhail Thompson Date: Sat, 29 Jun 2024 20:57:44 +0300 Subject: [PATCH] parse static data to get quest data --- .../Msgs/Misc/GetResourceHosts2.cs | 2 +- .../Msgs/Misc/GetStaticDataPack.cs | 13 +- nksrv/LobbyServer/Msgs/Shop/GetProductList.cs | 4 +- nksrv/LobbyServer/Msgs/Stage/ClearStage.cs | 35 +-- nksrv/LobbyServer/Msgs/Stage/GetStage.cs | 20 -- .../Msgs/Trigger/FinishMainQuest.cs | 12 +- nksrv/Program.cs | 24 +- nksrv/Protos/allmsgs.proto | 17 ++ nksrv/StaticInfo/JsonStaticData.cs | 19 ++ nksrv/StaticInfo/StaticDataParser.cs | 259 ++++++++++++++++++ nksrv/nksrv.csproj | 1 + 11 files changed, 344 insertions(+), 62 deletions(-) create mode 100644 nksrv/StaticInfo/JsonStaticData.cs create mode 100644 nksrv/StaticInfo/StaticDataParser.cs diff --git a/nksrv/LobbyServer/Msgs/Misc/GetResourceHosts2.cs b/nksrv/LobbyServer/Msgs/Misc/GetResourceHosts2.cs index 7d7df74..0137023 100644 --- a/nksrv/LobbyServer/Msgs/Misc/GetResourceHosts2.cs +++ b/nksrv/LobbyServer/Msgs/Misc/GetResourceHosts2.cs @@ -16,7 +16,7 @@ namespace nksrv.LobbyServer.Msgs.Misc var r = new ResourceHostResponse(); r.BaseUrl = "https://cloud.nikke-kr.com/prdenv/121-b0630db21d/{Platform}"; - + WriteData(r); } } diff --git a/nksrv/LobbyServer/Msgs/Misc/GetStaticDataPack.cs b/nksrv/LobbyServer/Msgs/Misc/GetStaticDataPack.cs index e610c80..3a3ec97 100644 --- a/nksrv/LobbyServer/Msgs/Misc/GetStaticDataPack.cs +++ b/nksrv/LobbyServer/Msgs/Misc/GetStaticDataPack.cs @@ -1,4 +1,5 @@ using Google.Protobuf; +using nksrv.StaticInfo; using nksrv.Utils; namespace nksrv.LobbyServer.Msgs.Misc @@ -11,14 +12,14 @@ namespace nksrv.LobbyServer.Msgs.Misc var req = await ReadData(); var r = new StaticDataPackResponse(); - r.Url = "https://cloud.nikke-kr.com/prdenv/121-c5e64b1a1b/staticdata/data/qa-240620-05b-p1/307748/StaticData.pack"; - r.Version = "data/qa-240620-05b-p1/307748"; - r.Size = 11575712; + r.Url = StaticDataParser.StaticDataUrl; + r.Version = StaticDataParser.Version; + r.Size = StaticDataParser.Size; // TODO: Read the file and compute these values - r.Sha256Sum = ByteString.CopyFrom(Convert.FromBase64String("PBcDa3PoHR2MJQ+4Xc3/FUSgkqx2gY25MBJ0ih9FMsM=")); - r.Salt1 = ByteString.CopyFrom(Convert.FromBase64String("WqyrQ8MGtzwHN3AGPkqVKyjdfWZjBJXw9K7nGblv/SA=")); - r.Salt2 = ByteString.CopyFrom(Convert.FromBase64String("6Gf2jEvAX2mt5OWIxIU5uDdbjKtIc+VgTjKKSLuYnsI=")); + r.Sha256Sum = ByteString.CopyFrom(StaticDataParser.Sha256Sum); + r.Salt1 = ByteString.CopyFrom(StaticDataParser.Salt1); + r.Salt2 = ByteString.CopyFrom(StaticDataParser.Salt2); WriteData(r); } diff --git a/nksrv/LobbyServer/Msgs/Shop/GetProductList.cs b/nksrv/LobbyServer/Msgs/Shop/GetProductList.cs index b01b3d8..c4e8997 100644 --- a/nksrv/LobbyServer/Msgs/Shop/GetProductList.cs +++ b/nksrv/LobbyServer/Msgs/Shop/GetProductList.cs @@ -1,4 +1,5 @@ using nksrv.Utils; +using Swan.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -23,8 +24,9 @@ namespace nksrv.LobbyServer.Msgs.Shop } WriteData(response); } - catch + catch(Exception ex) { + Logger.Error("Error while handling GetProductList request. Have you replaced sodium library?" + ex); ; } } diff --git a/nksrv/LobbyServer/Msgs/Stage/ClearStage.cs b/nksrv/LobbyServer/Msgs/Stage/ClearStage.cs index 52ea2e6..2cbb84b 100644 --- a/nksrv/LobbyServer/Msgs/Stage/ClearStage.cs +++ b/nksrv/LobbyServer/Msgs/Stage/ClearStage.cs @@ -87,19 +87,16 @@ namespace nksrv.LobbyServer.Msgs.Stage private static void DoQuestSpecificUserOperations(Utils.User user, int clearedStageId) { - if (clearedStageId == 6000001) - { - user.SetQuest(2, true); - } - else if (clearedStageId == 6000002) - { - user.SetQuest(3, true); - } - else if (clearedStageId == 6000003) + var quest = StaticDataParser.Instance.GetMainQuestForStageClearCondition(clearedStageId); + if (quest == null) throw new Exception("quest not found for stage: " + clearedStageId); + user.SetQuest(quest.id, true); + user.SetQuest(quest.next_main_quest_id, false); + + if (clearedStageId == 6000003) { // TODO: Is this the right place to copy over default characters? // TODO: What is CSN and TID? Also need to add names for these - // Note: CSN appears to be a character ID, still not sure what TID is + // Note: TID is table index, not sure what CSN is user.Characters.Add(new Utils.Character() { Csn = 47263455, Tid = 201001 }); user.Characters.Add(new Utils.Character() { Csn = 47273456, Tid = 330501 }); user.Characters.Add(new Utils.Character() { Csn = 47263457, Tid = 130201 }); @@ -114,24 +111,6 @@ namespace nksrv.LobbyServer.Msgs.Stage user.TeamData.Slots.Add(new NetWholeTeamSlot { Slot = 3, Csn = 47263457, Tid = 130201, Lvl = 1 }); user.TeamData.Slots.Add(new NetWholeTeamSlot { Slot = 4, Csn = 47263458, Tid = 230101, Lvl = 1 }); user.TeamData.Slots.Add(new NetWholeTeamSlot { Slot = 5, Csn = 47263459, Tid = 301201, Lvl = 1 }); - - user.SetQuest(4, true); - } - else if (clearedStageId == 6001001) - { - user.SetQuest(5, true); - } - else if (clearedStageId == 6001003) - { - user.SetQuest(6, true); - } - else if (clearedStageId == 6001004) - { - user.SetQuest(7, true); - } - else if (clearedStageId == 6002001) - { - user.SetQuest(13, true); } } public static int GetChapterForStageId(int stageId) diff --git a/nksrv/LobbyServer/Msgs/Stage/GetStage.cs b/nksrv/LobbyServer/Msgs/Stage/GetStage.cs index 960cff0..9641c18 100644 --- a/nksrv/LobbyServer/Msgs/Stage/GetStage.cs +++ b/nksrv/LobbyServer/Msgs/Stage/GetStage.cs @@ -28,26 +28,6 @@ namespace nksrv.LobbyServer.Msgs.Stage WriteData(response); } - public static NetFieldObjectData CreateFieldInfoWithAllStages(int chapter) - { - var f = new NetFieldObjectData(); - switch(chapter) - { - case 1: - f.Stages.Add(new NetFieldStageData() { StageId = 6001001 }); - f.Stages.Add(new NetFieldStageData() { StageId = 6001002 }); - f.Stages.Add(new NetFieldStageData() { StageId = 6001003 }); - f.Stages.Add(new NetFieldStageData() { StageId = 6001004 }); - - // Objects are collected i think - break; - default: - Logger.Error("ERROR: CreateFieldInfoWithAllStages: TODO chapter " + chapter); - break; - } - return f; - } - public static NetFieldObjectData CreateFieldInfo(Utils.User user, int chapter) { var f = new NetFieldObjectData(); diff --git a/nksrv/LobbyServer/Msgs/Trigger/FinishMainQuest.cs b/nksrv/LobbyServer/Msgs/Trigger/FinishMainQuest.cs index 46ebb14..bc2bf9c 100644 --- a/nksrv/LobbyServer/Msgs/Trigger/FinishMainQuest.cs +++ b/nksrv/LobbyServer/Msgs/Trigger/FinishMainQuest.cs @@ -1,4 +1,5 @@ -using nksrv.Utils; +using nksrv.StaticInfo; +using nksrv.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -15,7 +16,14 @@ namespace nksrv.LobbyServer.Msgs.Trigger var req = await ReadData(); var user = GetUser(); Console.WriteLine("Complete quest: " + req.Tid); - user.SetQuest(req.Tid, true); // todo is this right? + user.SetQuest(req.Tid, true); + + var completedQuest = StaticDataParser.Instance.GetMainQuestByTableId(req.Tid); + if (completedQuest == null) throw new Exception("Quest not found"); + + // set next quest as available + user.SetQuest(completedQuest.next_main_quest_id, true); + JsonDb.Save(); var response = new ResFinMainQuest(); WriteData(response); diff --git a/nksrv/Program.cs b/nksrv/Program.cs index dfa289e..dfeaa84 100644 --- a/nksrv/Program.cs +++ b/nksrv/Program.cs @@ -19,19 +19,31 @@ using System.Net.Sockets; using Newtonsoft.Json.Linq; using Swan; using Google.Api; +using nksrv.StaticInfo; namespace nksrv { internal class Program { - private static readonly HttpClient AssetDownloader = new(); + public static readonly HttpClient AssetDownloader = new(); static async Task Main() { Logger.UnregisterLogger(); Logger.RegisterLogger(new GreatLogger()); + Logger.Info("Initializing database"); JsonDb.Save(); + + Logger.Info("Load static data"); + await StaticDataParser.Load(); + + Logger.Info("Parse static data"); + await StaticDataParser.Instance.Parse(); + + Logger.Info("Initialize handlers"); LobbyHandler.Init(); + Logger.Info("Start server"); + // Start Webserver using var server = CreateWebServer(); await server.RunAsync(); @@ -55,7 +67,7 @@ namespace nksrv .WithModule(new ActionModule("/$batch", HttpVerbs.Any, HandleBatchRequests)); // Listen for state changes. - server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info(); + //server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info(); return server; } @@ -137,13 +149,17 @@ namespace nksrv ctx.Response.StatusCode = 404; } } + public static string GetCachePathForPath(string path) + { + return AppDomain.CurrentDomain.BaseDirectory + "cache" + path; + } private static async Task HandleAsset(IHttpContext ctx) { - string targetFile = AppDomain.CurrentDomain.BaseDirectory + "cache" + ctx.RequestedPath; + string targetFile = GetCachePathForPath(ctx.Request.RawUrl); var targetDir = Path.GetDirectoryName(targetFile); if (targetDir == null) { - Logger.Error($"ERROR: Directory name cannot be null for request " + ctx.RequestedPath + ", file path is " + targetFile); + Logger.Error($"ERROR: Directory name cannot be null for request " + ctx.Request.RawUrl + ", file path is " + targetFile); return; } Directory.CreateDirectory(targetDir); diff --git a/nksrv/Protos/allmsgs.proto b/nksrv/Protos/allmsgs.proto index e6136d8..c192928 100644 --- a/nksrv/Protos/allmsgs.proto +++ b/nksrv/Protos/allmsgs.proto @@ -1613,4 +1613,21 @@ message ReqFinMainQuest { } message ResFinMainQuest { +} + + +enum SimRoomStatus { + SimroomStatus_Ready = 0; + SimroomStatus_Progress = 1; +} +message ReqGetSimRoom { + +} +message ResGetSimRoom { + SimRoomStatus status = 1; + int32 currentDifficulty = 2; + int64 nextRenewAt = 3; + //repeated NetSimRoomClearInfo clearInfos = 4; + //repeated NetSimRoomEvent events = 5; + } \ No newline at end of file diff --git a/nksrv/StaticInfo/JsonStaticData.cs b/nksrv/StaticInfo/JsonStaticData.cs new file mode 100644 index 0000000..cdbf686 --- /dev/null +++ b/nksrv/StaticInfo/JsonStaticData.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace nksrv.StaticInfo +{ + public class MainQuestCompletionData + { + public int id; + public int group_id; + public string category = ""; + public int condition_id; + public int next_main_quest_id = 0; + public int reward_id = 0; + public int target_chapter_id; + } +} diff --git a/nksrv/StaticInfo/StaticDataParser.cs b/nksrv/StaticInfo/StaticDataParser.cs new file mode 100644 index 0000000..0fbe0d6 --- /dev/null +++ b/nksrv/StaticInfo/StaticDataParser.cs @@ -0,0 +1,259 @@ +using nksrv.Utils; +using Swan.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip; +using Newtonsoft.Json.Linq; +using Swan.Parsers; +using Newtonsoft.Json; + +namespace nksrv.StaticInfo +{ + /// + /// "Static data" which is what the game calls it, contains data such as map info, characters, quests, rewards and a lot more. + /// + public class StaticDataParser + { + // Extracted from staticinfo api call + public const string StaticDataUrl = "https://cloud.nikke-kr.com/prdenv/121-c5e64b1a1b/staticdata/data/qa-240620-05b-p1/307748/StaticData.pack"; + public const string Version = "data/qa-240620-05b-p1/307748"; + public const int Size = 11575712; + public static byte[] Sha256Sum = Convert.FromBase64String("PBcDa3PoHR2MJQ+4Xc3/FUSgkqx2gY25MBJ0ih9FMsM="); + public static byte[] Salt1 = Convert.FromBase64String("WqyrQ8MGtzwHN3AGPkqVKyjdfWZjBJXw9K7nGblv/SA="); + public static byte[] Salt2 = Convert.FromBase64String("6Gf2jEvAX2mt5OWIxIU5uDdbjKtIc+VgTjKKSLuYnsI="); + + // These fields were extracted from the game. + public static byte[] PresharedKey = [0xCB, 0xC2, 0x1C, 0x6F, 0xF3, 0xF5, 0x07, 0xF5, 0x05, 0xBA, 0xCA, 0xD4, 0x98, 0x28, 0x84, 0x1F, 0xF0, 0xD1, 0x38, 0xC7, 0x61, 0xDF, 0xD6, 0xE6, 0x64, 0x9A, 0x85, 0x13, 0x3E, 0x1A, 0x6A, 0x0C, 0x68, 0x0E, 0x2B, 0xC4, 0xDF, 0x72, 0xF8, 0xC6, 0x55, 0xE4, 0x7B, 0x14, 0x36, 0x18, 0x3B, 0xA7, 0xD1, 0x20, 0x81, 0x22, 0xD1, 0xA9, 0x18, 0x84, 0x65, 0x13, 0x0B, 0xED, 0xA3, 0x00, 0xE5, 0xD9]; + public static RSAParameters RSAParameters = new RSAParameters() + { + Exponent = [0x01, 0x00, 0x01], + Modulus = [0x89, 0xD6, 0x66, 0x00, 0x7D, 0xFC, 0x7D, 0xCE, 0x83, 0xA6, 0x62, 0xE3, 0x1A, 0x5E, 0x9A, 0x53, 0xC7, 0x8A, 0x27, 0xF3, 0x67, 0xC1, 0xF3, 0xD4, 0x37, 0xFE, 0x50, 0x6D, 0x38, 0x45, 0xDF, 0x7E, 0x73, 0x5C, 0xF4, 0x9D, 0x40, 0x4C, 0x8C, 0x63, 0x21, 0x97, 0xDF, 0x46, 0xFF, 0xB2, 0x0D, 0x0E, 0xDB, 0xB2, 0x72, 0xB4, 0xA8, 0x42, 0xCD, 0xEE, 0x48, 0x06, 0x74, 0x4F, 0xE9, 0x56, 0x6E, 0x9A, 0xB1, 0x60, 0x18, 0xBC, 0x86, 0x0B, 0xB6, 0x32, 0xA7, 0x51, 0x00, 0x85, 0x7B, 0xC8, 0x72, 0xCE, 0x53, 0x71, 0x3F, 0x64, 0xC2, 0x25, 0x58, 0xEF, 0xB0, 0xC9, 0x1D, 0xE3, 0xB3, 0x8E, 0xFC, 0x55, 0xCF, 0x8B, 0x02, 0xA5, 0xC8, 0x1E, 0xA7, 0x0E, 0x26, 0x59, 0xA8, 0x33, 0xA5, 0xF1, 0x11, 0xDB, 0xCB, 0xD3, 0xA7, 0x1F, 0xB1, 0xC6, 0x10, 0x39, 0xC8, 0x31, 0x1D, 0x60, 0xDB, 0x0D, 0xA4, 0x13, 0x4B, 0x2B, 0x0E, 0xF3, 0x6F, 0x69, 0xCB, 0xA8, 0x62, 0x03, 0x69, 0xE6, 0x95, 0x6B, 0x8D, 0x11, 0xF6, 0xAF, 0xD9, 0xC2, 0x27, 0x3A, 0x32, 0x12, 0x05, 0xC3, 0xB1, 0xE2, 0x81, 0x4B, 0x40, 0xF8, 0x8B, 0x8D, 0xBA, 0x1F, 0x55, 0x60, 0x2C, 0x09, 0xC6, 0xED, 0x73, 0x96, 0x32, 0xAF, 0x5F, 0xEE, 0x8F, 0xEB, 0x5B, 0x93, 0xCF, 0x73, 0x13, 0x15, 0x6B, 0x92, 0x7B, 0x27, 0x0A, 0x13, 0xF0, 0x03, 0x4D, 0x6F, 0x5E, 0x40, 0x7B, 0x9B, 0xD5, 0xCE, 0xFC, 0x04, 0x97, 0x7E, 0xAA, 0xA3, 0x53, 0x2A, 0xCF, 0xD2, 0xD5, 0xCF, 0x52, 0xB2, 0x40, 0x61, 0x28, 0xB1, 0xA6, 0xF6, 0x78, 0xFB, 0x69, 0x9A, 0x85, 0xD6, 0xB9, 0x13, 0x14, 0x6D, 0xC4, 0x25, 0x36, 0x17, 0xDB, 0x54, 0x0C, 0xD8, 0x77, 0x80, 0x9A, 0x00, 0x62, 0x83, 0xDD, 0xB0, 0x06, 0x64, 0xD0, 0x81, 0x5B, 0x0D, 0x23, 0x9E, 0x88, 0xBD], + DP = null + }; + + // Fields + public static StaticDataParser Instance; + private ZipFile MainZip; + private MemoryStream ZipStream; + private JArray questDataRecords; + private JArray stageDataRecords; + + public StaticDataParser(string filePath) + { + if (!File.Exists(filePath)) throw new ArgumentException("Static data file must exist", nameof(filePath)); + DecryptStaticDataAndLoadZip(filePath); + if (MainZip == null) throw new Exception("failed to read zip file"); + } + #region Decryption + private void DecryptStaticDataAndLoadZip(string file) + { + using var fileStream = File.Open(file, FileMode.Open, FileAccess.Read); + + var keyDecryptor = new Rfc2898DeriveBytes(PresharedKey, Salt2, 10000, HashAlgorithmName.SHA256); + var key2 = keyDecryptor.GetBytes(32); + + byte[] decryptionKey = key2[0..16]; + byte[] iv = key2[16..32]; + var aes = Aes.Create(); + aes.KeySize = 128; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + aes.Key = decryptionKey; + aes.IV = iv; + var transform = aes.CreateDecryptor(); + + // Decryption layer 1 + using CryptoStream stream = new CryptoStream(fileStream, transform, CryptoStreamMode.Read); + + using MemoryStream ms = new MemoryStream(); + stream.CopyTo(ms); + + var bytes = ms.ToArray(); + + // Decryption of layer 2 + var zip = new ZipFile(ms, false); + + var signEntry = zip.GetEntry("sign"); + if (signEntry == null) throw new Exception("Sign entry not found in decrypted static data pack"); + var dataEntry = zip.GetEntry("data"); + if (dataEntry == null) throw new Exception("Data entry not found in decrypted static data pack"); + + var signStream = zip.GetInputStream(signEntry); + var dataStream = zip.GetInputStream(dataEntry); + + using MemoryStream signMs = new MemoryStream(); + signStream.CopyTo(signMs); + + using MemoryStream dataMs = new MemoryStream(); + dataStream.CopyTo(dataMs); + dataMs.Position = 0; + + var rsa = RSA.Create(RSAParameters); + if (!rsa.VerifyData(dataMs, signMs.ToArray(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)) + throw new Exception("failed to decrypt static data (round 2)"); + + dataMs.Position = 0; + + // Decryption of layer 3 + var keyDecryptor2 = new Rfc2898DeriveBytes(PresharedKey, Salt1, 10000, HashAlgorithmName.SHA256); + var key3 = keyDecryptor2.GetBytes(32); + + byte[] decryptionKey2 = key3[0..16]; + byte[] iv2 = key3[16..32]; + + ZipStream = new MemoryStream(); + AesCtrTransform(decryptionKey2, iv2, dataMs, ZipStream); + MainZip = new ZipFile(ZipStream, false); + } + + public static void AesCtrTransform( + byte[] key, byte[] salt, Stream inputStream, Stream outputStream) + { + SymmetricAlgorithm aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + + int blockSize = aes.BlockSize / 8; + + if (salt.Length != blockSize) + { + throw new ArgumentException( + "Salt size must be same as block size " + + $"(actual: {salt.Length}, expected: {blockSize})"); + } + + var counter = (byte[])salt.Clone(); + + var xorMask = new Queue(); + + var zeroIv = new byte[blockSize]; + ICryptoTransform counterEncryptor = aes.CreateEncryptor(key, zeroIv); + + int b; + while ((b = inputStream.ReadByte()) != -1) + { + if (xorMask.Count == 0) + { + var counterModeBlock = new byte[blockSize]; + + counterEncryptor.TransformBlock( + counter, 0, counter.Length, counterModeBlock, 0); + + for (var i2 = counter.Length - 1; i2 >= 0; i2--) + { + if (++counter[i2] != 0) + { + break; + } + } + + foreach (var b2 in counterModeBlock) + { + xorMask.Enqueue(b2); + } + } + + var mask = xorMask.Dequeue(); + outputStream.WriteByte((byte)(((byte)b) ^ mask)); + } + } + public static async Task Load() + { + string targetFile = Program.GetCachePathForPath(StaticDataUrl.Replace("https://cloud.nikke-kr.com", "")); + var targetDir = Path.GetDirectoryName(targetFile); + + Directory.CreateDirectory(targetDir); + + if (!File.Exists(targetFile)) + { + // TODO: Ip might change + var requestUri = new Uri("https://43.132.66.200/" + StaticDataUrl.Replace("https://cloud.nikke-kr.com", "")); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.TryAddWithoutValidation("host", "cloud.nikke-kr.com"); + + Logger.Info("Downloading static game data from server. Please wait."); + using var response = await Program.AssetDownloader.SendAsync(request); + if (response.StatusCode == HttpStatusCode.OK) + { + using var fss = new FileStream(targetFile, FileMode.CreateNew); + await response.Content.CopyToAsync(fss); + + fss.Close(); + } + else + { + throw new Exception("Failed to download static game data"); + } + } + + + Instance = new(targetFile); + } + #endregion + public async Task Parse() + { + var mainQuestData = MainZip.GetEntry("MainQuestTable.json"); + var campaignStageData = MainZip.GetEntry("CampaignStageTable.json"); + + if (mainQuestData == null) throw new Exception("MainQuestTable.json does not exist in static data"); + if (campaignStageData == null) throw new Exception("CampaignStageTable.json does not exist in static data"); + + using StreamReader mainQuestReader = new StreamReader(MainZip.GetInputStream(mainQuestData)); + var mainQuestDataString = await mainQuestReader.ReadToEndAsync(); + + using StreamReader campaignStageDataReader = new StreamReader(MainZip.GetInputStream(mainQuestData)); + var campaignStageDataString = await campaignStageDataReader.ReadToEndAsync(); + + var questdata = JObject.Parse(mainQuestDataString); + var stagedata = JObject.Parse(campaignStageDataString); + + questDataRecords = (JArray?)questdata["records"]; + stageDataRecords = (JArray?)stagedata["records"]; + if (questDataRecords == null) throw new Exception("MainQuestTable.json does not contain records array"); + if (stageDataRecords == null) throw new Exception("CampaignStageTable.json does not contain records array"); + } + + public MainQuestCompletionData? GetMainQuestForStageClearCondition(int stage) + { + foreach (JObject item in questDataRecords) + { + var id = item["condition_id"]; + if (id == null) throw new Exception("expected condition_id field in quest data"); + + int value = id.ToObject(); + if (value == stage) + { + MainQuestCompletionData? data = JsonConvert.DeserializeObject(item.ToString()); + if (data == null) throw new Exception("failed to deserialize main quest data item"); + return data; + } + } + + return null; + } + public MainQuestCompletionData? GetMainQuestByTableId(int tid) + { + foreach (JObject item in questDataRecords) + { + var id = item["id"]; + if (id == null) throw new Exception("expected condition_id field in quest data"); + + int value = id.ToObject(); + if (value == tid) + { + MainQuestCompletionData? data = JsonConvert.DeserializeObject(item.ToString()); + if (data == null) throw new Exception("failed to deserialize main quest data item"); + return data; + } + } + + return null; + } + } +} diff --git a/nksrv/nksrv.csproj b/nksrv/nksrv.csproj index 8903f67..d8e4523 100644 --- a/nksrv/nksrv.csproj +++ b/nksrv/nksrv.csproj @@ -16,6 +16,7 @@ +