mirror of
https://github.com/EpinelPS/EpinelPS.git
synced 2025-12-12 15:04:36 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
181d1433cf | ||
|
|
1c7292b68b | ||
|
|
3dfcac2cef | ||
|
|
5134ac187b | ||
|
|
367cd64b8e | ||
|
|
01f86a7d24 | ||
|
|
ecbb9dfe3b | ||
|
|
b63478f0b4 | ||
|
|
9e79ae0e80 | ||
|
|
b34383883b | ||
|
|
71f975b5ce | ||
|
|
20213153ab | ||
|
|
9356d347bd | ||
|
|
c1d783b1e7 | ||
|
|
3865b403b4 | ||
|
|
618619e36d |
2
.github/workflows/dotnet-desktop.yml
vendored
2
.github/workflows/dotnet-desktop.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
run: dotnet publish nksrv
|
||||
|
||||
- name: Copy to output
|
||||
run: echo ${{ github.workspace }} && md ${{ github.workspace }}/out/ && xcopy "${{ github.workspace }}\ServerSelector.Desktop\bin\Release\net8.0\win-x64\publish\" "${{ github.workspace }}\out\" && xcopy "${{ github.workspace }}\nksrv\bin\Release\net8.0\win-x64\publish\" "${{ github.workspace }}\out\" && copy "${{ github.workspace }}\ServerSelector.Desktop\sodium.dll" "${{ github.workspace }}\out\sodium.dll"
|
||||
run: echo ${{ github.workspace }} && md ${{ github.workspace }}/out/ && xcopy /s /e "${{ github.workspace }}\ServerSelector.Desktop\bin\Release\net8.0\win-x64\publish\" "${{ github.workspace }}\out\" && xcopy /s /e "${{ github.workspace }}\nksrv\bin\Release\net8.0\win-x64\publish\" "${{ github.workspace }}\out\" && copy "${{ github.workspace }}\ServerSelector.Desktop\sodium.dll" "${{ github.workspace }}\out\sodium.dll"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Mikhail Thompson
|
||||
Copyright (c) 2024 Mikhail
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -10,8 +10,8 @@ namespace ProtobufViewUtil
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
|
||||
ResGetOutpostData s = new ResGetOutpostData();
|
||||
var inn = File.ReadAllBytes(@"C:\Users\Misha\Downloads\getoutpostdatach2done");
|
||||
StaticDataPackResponse s = new StaticDataPackResponse();
|
||||
var inn = File.ReadAllBytes(@"C:\Users\Misha\Desktop\response");
|
||||
s.MergeFrom(inn);
|
||||
Console.WriteLine(s.ToString());
|
||||
var outt = s.ToByteArray();
|
||||
|
||||
19
README.md
19
README.md
@@ -1,5 +1,18 @@
|
||||
# nikke-server
|
||||
Private/local server for Nikke. NOTE: This project is in a very early state so many features in the game do not work.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/MishaProductions/nikke-server/issues)
|
||||
[](https://github.com/MishaProductions/nikke-server/pulls)
|
||||
[](https://github.com/MishaProductions/nikke-server/blob/main/LICENSE)
|
||||

|
||||

|
||||
[](https://discord.gg/Ztt6Y9vQjF)
|
||||
|
||||
</div>
|
||||
Private/local server for Nikke. NOTE: This project is in a very early state so many features in the game do not work. Discord server: https://discord.gg/Ztt6Y9vQjF
|
||||
|
||||
## Usage
|
||||
Download the latest release/GitHub actions build, and run ServerSelector.Desktop.exe as administrator (to modify DNS hosts file and install a CA cert). Make sure to close the game and launcher first. Select Local server, and then click save. After that, start nksrv.exe to start the actual server.
|
||||
@@ -12,6 +25,10 @@ If the game does not get past the title screen, open an issue and send %appdata%
|
||||
|
||||
Note that this was tested with the latest version (122.8.20c)
|
||||
|
||||
To access the admin panel, go to https://127.0.0.1/admin/ and sign in. Note that IsAdmin needs to be true for the user account. Note that this interface does not have anything yet.
|
||||
|
||||
To skip stages, a basic command line interface is implemented.
|
||||
|
||||
## Progress
|
||||
Stage, character, outpost and story information is saved and works, as well as player nickname.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Avalonia.Controls;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
@@ -24,6 +25,30 @@ public partial class MainView : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(txtGamePath.Text))
|
||||
{
|
||||
ShowWarningMsg("Game path folder does not exist", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(txtLauncherPath.Text))
|
||||
{
|
||||
ShowWarningMsg("Launcher folder does not exist", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(Path.Combine(txtLauncherPath.Text, "nikke_launcher.exe")))
|
||||
{
|
||||
ShowWarningMsg("Launcher path is invalid. Make sure that nikke_launcher.exe exists in the launcher folder", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(Path.Combine(txtGamePath.Text, "nikke.exe")))
|
||||
{
|
||||
ShowWarningMsg("Game path is invalid. Make sure that nikke.exe exists in the launcher folder", "Error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (CmbServerSelection.SelectedIndex == 1)
|
||||
{
|
||||
if (!IPAddress.TryParse(TxtIpAddress.Text, out _))
|
||||
@@ -34,6 +59,7 @@ public partial class MainView : UserControl
|
||||
}
|
||||
if (TxtIpAddress.Text == null) TxtIpAddress.Text = "";
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
ServerSwitcher.SaveCfg(CmbServerSelection.SelectedIndex == 0, txtGamePath.Text, txtLauncherPath.Text, TxtIpAddress.Text);
|
||||
|
||||
90
nksrv/AdminApiController.cs
Normal file
90
nksrv/AdminApiController.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace nksrv
|
||||
{
|
||||
public class AdminApiController : WebApiController
|
||||
{
|
||||
public static Dictionary<string, User> AdminAuthTokens = new();
|
||||
private static MD5 md5 = MD5.Create();
|
||||
[Route(HttpVerbs.Any, "/login")]
|
||||
public async Task Login()
|
||||
{
|
||||
var c = await HttpContext.GetRequestFormDataAsync();
|
||||
var username = c["username"];
|
||||
var password = c["password"];
|
||||
|
||||
if (HttpContext.Request.HttpMethod != "POST")
|
||||
{
|
||||
await HttpContext.SendStringAsync(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", ""), "text/html", Encoding.Unicode);
|
||||
return;
|
||||
}
|
||||
|
||||
User? user = null;
|
||||
bool nullusernames = false;
|
||||
if (username != null && password != null)
|
||||
{
|
||||
var passwordHash = Convert.ToHexString(md5.ComputeHash(Encoding.ASCII.GetBytes(password))).ToLower();
|
||||
foreach (var item in JsonDb.Instance.Users)
|
||||
{
|
||||
if (item.Username == username)
|
||||
{
|
||||
if (item.Password.ToLower() == passwordHash)
|
||||
{
|
||||
user = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nullusernames = true;
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
if (nullusernames == false)
|
||||
{
|
||||
await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", "Incorrect username or password"), "text/html", Encoding.Unicode);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", "Please enter a username or password"), "text/html", Encoding.Unicode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user.IsAdmin)
|
||||
{
|
||||
Response.Headers.Add("Set-Cookie", "token=" + CreateAuthToken(user) + ";path=/");
|
||||
HttpContext.Redirect("/admin/", 301);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HttpContext.SendStringAsync((string)File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/www/admin/index.html").Replace("<errormsg/>", "User does not have admin panel access."), "text/html", Encoding.Unicode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateAuthToken(User user)
|
||||
{
|
||||
var tok = RandomString(128);
|
||||
AdminAuthTokens.Add(tok, user);
|
||||
return tok;
|
||||
}
|
||||
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[new Random().Next(s.Length)]).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace nksrv.IntlServer
|
||||
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
if (ctx == null) throw new Exception("ctx cannot be null");
|
||||
Console.WriteLine("li-sg redirect in: " + Content);
|
||||
HttpClientHandler handler = new()
|
||||
{
|
||||
|
||||
@@ -19,18 +19,15 @@ namespace nksrv.IntlServer
|
||||
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
|
||||
RegisterEPReq? ep = JsonConvert.DeserializeObject<RegisterEPReq>(Content);
|
||||
if (ep != null)
|
||||
{
|
||||
string? seg = ctx.GetRequestQueryData().Get("seq");
|
||||
|
||||
// check if the account already exists
|
||||
foreach (var item in JsonDb.Instance.Users)
|
||||
{
|
||||
if (item.Username == ep.account)
|
||||
{
|
||||
await WriteJsonStringAsync("{\"msg\":\"send code failed; invalid account\",\"ret\":2112,\"seq\":\"" + seg + "\"}");
|
||||
await WriteJsonStringAsync("{\"msg\":\"send code failed; invalid account\",\"ret\":2112,\"seq\":\"" + Seq + "\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +48,7 @@ namespace nksrv.IntlServer
|
||||
JsonDb.Instance.Users.Add(user);
|
||||
|
||||
var tok = IntlHandler.CreateLauncherTokenForUser(user);
|
||||
await WriteJsonStringAsync("{\"expire\":" + tok.ExpirationTime + ",\"is_login\":false,\"msg\":\"Success\",\"register_time\":" + user.RegisterTime + ",\"ret\":0,\"seq\":\"" + seg + "\",\"token\":\"" + tok.Token + "\",\"uid\":\"" + user.ID + "\"}");
|
||||
await WriteJsonStringAsync("{\"expire\":" + tok.ExpirationTime + ",\"is_login\":false,\"msg\":\"Success\",\"register_time\":" + user.RegisterTime + ",\"ret\":0,\"seq\":\"" + Seq + "\",\"token\":\"" + tok.Token + "\",\"uid\":\"" + user.ID + "\"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -33,9 +33,7 @@ namespace nksrv.IntlServer
|
||||
}
|
||||
}
|
||||
|
||||
string? seg = ctx.GetRequestQueryData().Get("seq");
|
||||
|
||||
await WriteJsonStringAsync("{\"msg\":\"the account does not exists!\",\"ret\":2001,\"seq\":\"" + seg + "\"}");
|
||||
await WriteJsonStringAsync("{\"msg\":\"the account does not exists!\",\"ret\":2001,\"seq\":\"" + Seq + "\"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -12,17 +12,17 @@ namespace nksrv.IntlServer
|
||||
{
|
||||
public abstract class IntlMsgHandler
|
||||
{
|
||||
protected IHttpContext ctx;
|
||||
protected IHttpContext? ctx;
|
||||
protected string Content = "";
|
||||
protected User? User;
|
||||
protected string? Seq;
|
||||
protected string Seq = "";
|
||||
protected AccessToken? UsedToken;
|
||||
public abstract bool RequiresAuth { get; }
|
||||
public async Task HandleAsync(IHttpContext ctx)
|
||||
{
|
||||
this.ctx = ctx;
|
||||
Content = await ctx.GetRequestBodyAsStringAsync();
|
||||
Seq = ctx.GetRequestQueryData().Get("seq");
|
||||
Seq = ctx.GetRequestQueryData().Get("seq") ?? "";
|
||||
if (RequiresAuth)
|
||||
{
|
||||
var x = JsonConvert.DeserializeObject<AuthPkt>(Content);
|
||||
@@ -74,7 +74,6 @@ namespace nksrv.IntlServer
|
||||
await HandleAsync();
|
||||
}
|
||||
protected abstract Task HandleAsync();
|
||||
|
||||
protected async Task WriteJsonStringAsync(string data)
|
||||
{
|
||||
if (ctx != null)
|
||||
@@ -83,10 +82,11 @@ namespace nksrv.IntlServer
|
||||
ctx.Response.ContentEncoding = null;
|
||||
ctx.Response.ContentType = "application/json";
|
||||
ctx.Response.ContentLength64 = bt.Length;
|
||||
await ctx.Response.OutputStream.WriteAsync(bt, 0, bt.Length, ctx.CancellationToken);
|
||||
await ctx.Response.OutputStream.WriteAsync(bt, ctx.CancellationToken);
|
||||
await ctx.Response.OutputStream.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelInfo
|
||||
{
|
||||
public string openid { get; set; } = "";
|
||||
|
||||
@@ -20,10 +20,7 @@ namespace nksrv.IntlServer
|
||||
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var str = await ctx.GetRequestBodyAsStringAsync();
|
||||
|
||||
string? seg = ctx.GetRequestQueryData().Get("seq");
|
||||
await WriteJsonStringAsync(JsonToReturn.Replace("((SEGID))", seg));
|
||||
await WriteJsonStringAsync(JsonToReturn.Replace("((SEGID))", Seq));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,19 @@ namespace nksrv.IntlServer
|
||||
SendCodeRequest? ep = JsonConvert.DeserializeObject<SendCodeRequest>(Content);
|
||||
if (ep != null)
|
||||
{
|
||||
string? seg = ctx.GetRequestQueryData().Get("seq");
|
||||
|
||||
// check if the account already exists
|
||||
|
||||
foreach (var item in JsonDb.Instance.Users)
|
||||
{
|
||||
if (item.Username == ep.account)
|
||||
{
|
||||
await WriteJsonStringAsync("{\"msg\":\"send code failed; invalid account\",\"ret\":2112,\"seq\":\"" + seg + "\"}");
|
||||
await WriteJsonStringAsync("{\"msg\":\"send code failed; invalid account\",\"ret\":2112,\"seq\":\"" + Seq + "\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// pretend that we sent the code
|
||||
await WriteJsonStringAsync("{\"expire_time\":898,\"msg\":\"Success\",\"ret\":0,\"seq\":\"" + seg + "\"}");
|
||||
await WriteJsonStringAsync("{\"expire_time\":898,\"msg\":\"Success\",\"ret\":0,\"seq\":\"" + Seq + "\"}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
22
nksrv/LobbyServer/Msgs/Arena/ShowSpecialArenaReward.cs
Normal file
22
nksrv/LobbyServer/Msgs/Arena/ShowSpecialArenaReward.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.Arena
|
||||
{
|
||||
[PacketPath("/arena/special/showreward")]
|
||||
public class ShowSpecialArenaReward : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqShowSpecialArenaReward>();
|
||||
|
||||
var response = new ResShowSpecialArenaReward();
|
||||
// TODO
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.FavoriteItem
|
||||
{
|
||||
[PacketPath("/favoriteitem/library")]
|
||||
public class GetFavoriteItemLibrary : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqGetFavoriteItemLibrary>();
|
||||
|
||||
var response = new ResGetFavoriteItemLibrary();
|
||||
var user = GetUser();
|
||||
|
||||
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
nksrv/LobbyServer/Msgs/FavoriteItem/ListFavoriteItem.cs
Normal file
23
nksrv/LobbyServer/Msgs/FavoriteItem/ListFavoriteItem.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.FavoriteItem
|
||||
{
|
||||
[PacketPath("/favoriteitem/list")]
|
||||
public class ListFavoriteItem : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqListFavoriteItem>();
|
||||
var user = GetUser();
|
||||
|
||||
var response = new ResListFavoriteItem();
|
||||
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.FavoriteItem
|
||||
{
|
||||
[PacketPath("/favoriteitem/quest/list")]
|
||||
public class ListFavoriteItemQuests : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqListFavoriteItemQuest>();
|
||||
var user = GetUser();
|
||||
|
||||
var response = new ResListFavoriteItemQuest();
|
||||
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ namespace nksrv.LobbyServer.Msgs.Inventory
|
||||
{
|
||||
// update character id
|
||||
item.Csn = req.Csn;
|
||||
item.Position = NetUtils.GetItemPos(user, item.Isn);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace nksrv.LobbyServer.Msgs.Inventory
|
||||
if (item2 == item.Isn)
|
||||
{
|
||||
item.Csn = req.Csn;
|
||||
|
||||
item.Position = NetUtils.GetItemPos(user, item.Isn);
|
||||
response.Items.Add(NetUtils.ToNet(item));
|
||||
}
|
||||
}
|
||||
|
||||
25
nksrv/LobbyServer/Msgs/Liberate/GetLiberateData.cs
Normal file
25
nksrv/LobbyServer/Msgs/Liberate/GetLiberateData.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.Liberate
|
||||
{
|
||||
[PacketPath("/liberate/get")]
|
||||
public class GetLiberateData : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqGetLiberateData>();
|
||||
var user = GetUser();
|
||||
|
||||
var response = new ResGetLiberateData() { };
|
||||
|
||||
// TODO
|
||||
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
nksrv/LobbyServer/Msgs/Lostsector/GetLostSectorData.cs
Normal file
25
nksrv/LobbyServer/Msgs/Lostsector/GetLostSectorData.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.Lostsector
|
||||
{
|
||||
[PacketPath("/lostsector/get")]
|
||||
public class GetLostSectorData : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqGetLostSectorData>();
|
||||
var user = GetUser();
|
||||
|
||||
var response = new ResGetLostSectorData();
|
||||
|
||||
// TODO
|
||||
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,9 @@ namespace nksrv.LobbyServer.Msgs.Misc
|
||||
var req = await ReadData<ResourceHostRequest>();
|
||||
|
||||
var r = new ResourceHostResponse();
|
||||
r.BaseUrl = "https://cloud.nikke-kr.com/prdenv/122-b0255105e0/{Platform}";
|
||||
|
||||
await WriteDataAsync(r);
|
||||
r.BaseUrl = GameConfig.Root.ResourceBaseURL;
|
||||
|
||||
await WriteDataAsync(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,14 @@ namespace nksrv.LobbyServer.Msgs.Misc
|
||||
var req = await ReadData<StaticDataPackRequest>();
|
||||
|
||||
var r = new StaticDataPackResponse();
|
||||
r.Url = StaticDataParser.StaticDataUrl;
|
||||
r.Version = StaticDataParser.Version;
|
||||
r.Size = StaticDataParser.Size;
|
||||
r.Url = GameConfig.Root.StaticData.Url;
|
||||
r.Version = GameConfig.Root.StaticData.Version;
|
||||
r.Size = StaticDataParser.Instance.Size;
|
||||
r.Sha256Sum = ByteString.CopyFrom(StaticDataParser.Instance.Sha256Hash);
|
||||
r.Salt1 = ByteString.CopyFrom(Convert.FromBase64String(GameConfig.Root.StaticData.Salt1));
|
||||
r.Salt2 = ByteString.CopyFrom(Convert.FromBase64String(GameConfig.Root.StaticData.Salt2));
|
||||
|
||||
// TODO: Read the file and compute these values
|
||||
r.Sha256Sum = ByteString.CopyFrom(StaticDataParser.Sha256Sum);
|
||||
r.Salt1 = ByteString.CopyFrom(StaticDataParser.Salt1);
|
||||
r.Salt2 = ByteString.CopyFrom(StaticDataParser.Salt2);
|
||||
|
||||
await WriteDataAsync(r);
|
||||
await WriteDataAsync(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
nksrv/LobbyServer/Msgs/Sidestory/ListSideStory.cs
Normal file
25
nksrv/LobbyServer/Msgs/Sidestory/ListSideStory.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using nksrv.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.LobbyServer.Msgs.Sidestory
|
||||
{
|
||||
[PacketPath("/sidestory/list")]
|
||||
public class ListSideStory : LobbyMsgHandler
|
||||
{
|
||||
protected override async Task HandleAsync()
|
||||
{
|
||||
var req = await ReadData<ReqListSideStory>();
|
||||
var user = GetUser();
|
||||
|
||||
var response = new ResListSideStory();
|
||||
|
||||
// TODO
|
||||
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,61 +19,73 @@ namespace nksrv.LobbyServer.Msgs.Stage
|
||||
var response = new ResClearStage();
|
||||
var user = GetUser();
|
||||
|
||||
// TOOD: save to user info
|
||||
Console.WriteLine($"Stage " + req.StageId + " completed, result is " + req.BattleResult);
|
||||
|
||||
// TODO: check if user has already cleared this stage
|
||||
if (req.BattleResult == 1)
|
||||
{
|
||||
var clearedStage = StaticDataParser.Instance.GetStageData(req.StageId);
|
||||
if (clearedStage == null) throw new Exception("cleared stage cannot be null");
|
||||
|
||||
|
||||
if (user.FieldInfo.Count == 0)
|
||||
{
|
||||
user.FieldInfo.Add("0_" + clearedStage.chapter_mod, new FieldInfo() { });
|
||||
}
|
||||
|
||||
DoQuestSpecificUserOperations(user, req.StageId);
|
||||
var rewardData = StaticDataParser.Instance.GetRewardTableEntry(clearedStage.reward_id);
|
||||
|
||||
if (rewardData != null)
|
||||
response.StageClearReward = RegisterRewardsForUser(user, rewardData);
|
||||
else
|
||||
Logger.Warn("rewardId is null for stage " + req.StageId);
|
||||
|
||||
|
||||
if (clearedStage.stage_category == "Normal" || clearedStage.stage_category == "Boss" || clearedStage.stage_category == "Hard")
|
||||
{
|
||||
if (clearedStage.chapter_mod == "Hard")
|
||||
{
|
||||
user.LastHardStageCleared = req.StageId;
|
||||
}
|
||||
else if (clearedStage.chapter_mod == "Normal")
|
||||
{
|
||||
user.LastNormalStageCleared = req.StageId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warn("Unknown chapter mod " + clearedStage.chapter_mod);
|
||||
}
|
||||
}
|
||||
else if (clearedStage.stage_category == "Extra")
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warn("Unknown stage category " + clearedStage.stage_category);
|
||||
}
|
||||
|
||||
user.FieldInfo[(clearedStage.chapter_id - 1) + "_" + clearedStage.chapter_mod].CompletedStages.Add(new NetFieldStageData() { StageId = req.StageId });
|
||||
JsonDb.Save();
|
||||
response = CompleteStage(user, req.StageId);
|
||||
}
|
||||
|
||||
await WriteDataAsync(response);
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
|
||||
private NetRewardData RegisterRewardsForUser(Utils.User user, RewardTableRecord rewardData)
|
||||
|
||||
public static ResClearStage CompleteStage(Utils.User user, int StageId)
|
||||
{
|
||||
var response = new ResClearStage();
|
||||
var clearedStage = StaticDataParser.Instance.GetStageData(StageId);
|
||||
if (clearedStage == null) throw new Exception("cleared stage cannot be null");
|
||||
|
||||
|
||||
if (user.FieldInfo.Count == 0)
|
||||
{
|
||||
user.FieldInfo.Add("0_" + clearedStage.chapter_mod, new FieldInfo() { });
|
||||
}
|
||||
|
||||
DoQuestSpecificUserOperations(user, StageId);
|
||||
var rewardData = StaticDataParser.Instance.GetRewardTableEntry(clearedStage.reward_id);
|
||||
|
||||
if (rewardData != null)
|
||||
response.StageClearReward = RegisterRewardsForUser(user, rewardData);
|
||||
else
|
||||
Logger.Warn("rewardId is null for stage " + StageId);
|
||||
|
||||
|
||||
if (clearedStage.stage_category == "Normal" || clearedStage.stage_category == "Boss" || clearedStage.stage_category == "Hard")
|
||||
{
|
||||
if (clearedStage.chapter_mod == "Hard")
|
||||
{
|
||||
user.LastHardStageCleared = StageId;
|
||||
}
|
||||
else if (clearedStage.chapter_mod == "Normal")
|
||||
{
|
||||
user.LastNormalStageCleared = StageId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warn("Unknown chapter mod " + clearedStage.chapter_mod);
|
||||
}
|
||||
}
|
||||
else if (clearedStage.stage_category == "Extra")
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warn("Unknown stage category " + clearedStage.stage_category);
|
||||
}
|
||||
|
||||
var key = (clearedStage.chapter_id - 1) + "_" + clearedStage.chapter_mod;
|
||||
if (!user.FieldInfo.ContainsKey(key))
|
||||
user.FieldInfo.Add(key, new FieldInfo());
|
||||
|
||||
user.FieldInfo[key].CompletedStages.Add(new NetFieldStageData() { StageId = StageId });
|
||||
JsonDb.Save();
|
||||
return response;
|
||||
}
|
||||
|
||||
private static NetRewardData RegisterRewardsForUser(Utils.User user, RewardTableRecord rewardData)
|
||||
{
|
||||
NetRewardData ret = new();
|
||||
if (rewardData.rewards == null) return ret;
|
||||
@@ -81,11 +93,28 @@ namespace nksrv.LobbyServer.Msgs.Stage
|
||||
if (rewardData.user_exp != 0)
|
||||
{
|
||||
var newXp = rewardData.user_exp + user.userPointData.ExperiencePoint;
|
||||
var newLevel = StaticDataParser.Instance.GetUserLevelFromUserExp(newXp);
|
||||
|
||||
var oldXpData = StaticDataParser.Instance.GetUserLevelFromUserExp(user.userPointData.ExperiencePoint);
|
||||
var xpData = StaticDataParser.Instance.GetUserLevelFromUserExp(newXp);
|
||||
var newLevel = xpData.Item1;
|
||||
|
||||
if (newLevel == -1)
|
||||
{
|
||||
Logger.Warn("Unknown user level value for xp " + newXp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (newLevel > user.userPointData.UserLevel)
|
||||
{
|
||||
newXp -= oldXpData.Item2;
|
||||
if (user.Currency.ContainsKey(CurrencyType.FreeCash))
|
||||
user.Currency[CurrencyType.FreeCash] += 30;
|
||||
else
|
||||
user.Currency.Add(CurrencyType.FreeCash, 30);
|
||||
}
|
||||
|
||||
|
||||
// TODO: what is the difference between IncreaseExp and GainExp
|
||||
// NOTE: Current Exp/Lv refers to after XP was added.
|
||||
|
||||
@@ -103,10 +132,6 @@ namespace nksrv.LobbyServer.Msgs.Stage
|
||||
};
|
||||
user.userPointData.ExperiencePoint = newXp;
|
||||
|
||||
if (newLevel > user.userPointData.UserLevel)
|
||||
{
|
||||
// TODO: Commander Level up reward
|
||||
}
|
||||
user.userPointData.UserLevel = newLevel;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,6 @@ namespace nksrv.LobbyServer.Msgs.Stage
|
||||
|
||||
if (!found)
|
||||
{
|
||||
Console.WriteLine("chapter not found: " + key);
|
||||
|
||||
user.FieldInfo.Add(key, new FieldInfo());
|
||||
return CreateFieldInfo(user, chapter, mod);
|
||||
}
|
||||
|
||||
@@ -47,13 +47,15 @@ namespace nksrv.LobbyServer.Msgs.User
|
||||
{
|
||||
response.Currency.Add(new NetUserCurrencyData() { Type = (int)item.Key, Value = item.Value });
|
||||
}
|
||||
|
||||
foreach (var item in user.Characters)
|
||||
{
|
||||
response.Character.Add(new NetUserCharacterData() { Default = new() { Csn = item.Csn, Skill1Lv = item.Skill1Lvl, Skill2Lv = item.Skill2Lvl, CostumeId = item.CostumeId, Lv = item.Level, Grade = item.Grade, Tid = item.Tid, UltiSkillLv = item.UltimateLevel } });
|
||||
}
|
||||
foreach (var item in user.Items)
|
||||
|
||||
foreach (var item in NetUtils.GetUserItems(user))
|
||||
{
|
||||
response.Items.Add(new NetUserItemData() { Count = item.Count, Tid = item.ItemType, Csn = item.Csn, Lv = item.Level, Exp = item.Exp, Corporation = item.Corp, Isn = item.Isn, Position = item.Position });
|
||||
response.Items.Add(item);
|
||||
}
|
||||
|
||||
// Add squad data if there are characters
|
||||
@@ -85,7 +87,7 @@ namespace nksrv.LobbyServer.Msgs.User
|
||||
|
||||
response.LastClearedNormalMainStageId = user.LastNormalStageCleared;
|
||||
|
||||
await WriteDataAsync(response);
|
||||
await WriteDataAsync(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace nksrv.LobbyServer.Msgs.User
|
||||
response.RepresentationTeam = user.RepresentationTeamData;
|
||||
|
||||
response.LastClearedNormalMainStageId = user.LastNormalStageCleared;
|
||||
|
||||
|
||||
// Restore completed tutorials. GroupID is the first 4 digits of the Table ID.
|
||||
foreach (var item in user.ClearedTutorialData)
|
||||
{
|
||||
|
||||
270
nksrv/Program.cs
270
nksrv/Program.cs
@@ -20,6 +20,8 @@ using Newtonsoft.Json.Linq;
|
||||
using Swan;
|
||||
using Google.Api;
|
||||
using nksrv.StaticInfo;
|
||||
using EmbedIO.WebApi;
|
||||
using nksrv.LobbyServer.Msgs.Stage;
|
||||
|
||||
namespace nksrv
|
||||
{
|
||||
@@ -38,16 +40,202 @@ namespace nksrv
|
||||
LobbyHandler.Init();
|
||||
|
||||
Logger.Info("Starting server");
|
||||
new Thread(() =>
|
||||
{
|
||||
var server = CreateWebServer();
|
||||
server.RunAsync();
|
||||
}).Start();
|
||||
|
||||
using var server = CreateWebServer();
|
||||
await server.RunAsync();
|
||||
// cli interface
|
||||
|
||||
ulong selectedUser = 0;
|
||||
string prompt = "# ";
|
||||
while (true)
|
||||
{
|
||||
Console.Write(prompt);
|
||||
|
||||
var input = Console.ReadLine();
|
||||
var args = input.Split(' ');
|
||||
if (string.IsNullOrEmpty(input) || string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
|
||||
}
|
||||
else if (input == "?" || input == "help")
|
||||
{
|
||||
Console.WriteLine("Nikke Private Server CLI interface");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Commands:");
|
||||
Console.WriteLine(" help - show this help");
|
||||
Console.WriteLine(" ls /users - show all users");
|
||||
Console.WriteLine(" cd (user id) - select user by id");
|
||||
Console.WriteLine(" rmuser - delete selected user");
|
||||
Console.WriteLine(" ban - ban selected user from game");
|
||||
Console.WriteLine(" unban - unban selected user from game");
|
||||
Console.WriteLine(" exit - exit server application");
|
||||
Console.WriteLine(" completestage (chapter num)-(stage number) - complete selected stage and get rewards (and all previous ones). Example completestage 15-1. Note that the exact stage number cleared may not be exact.");
|
||||
}
|
||||
else if (input == "ls /users")
|
||||
{
|
||||
Console.WriteLine("Id,Username,Nickname");
|
||||
foreach (var item in JsonDb.Instance.Users)
|
||||
{
|
||||
Console.WriteLine($"{item.ID},{item.Username},{item.Nickname}");
|
||||
}
|
||||
}
|
||||
else if (input.StartsWith("cd"))
|
||||
{
|
||||
if (args.Length == 2)
|
||||
{
|
||||
if (ulong.TryParse(args[1], out ulong id))
|
||||
{
|
||||
// check if user id exists
|
||||
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == id);
|
||||
if (user != null)
|
||||
{
|
||||
selectedUser = user.ID;
|
||||
Console.WriteLine("Selected user: " + user.Username);
|
||||
prompt = "/users/" + user.Username + "# ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("User not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Argument #1 should be a number");
|
||||
Console.WriteLine("Usage: chroot (user id)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Incorrect number of arguments for chroot");
|
||||
Console.WriteLine("Usage: chroot (user id)");
|
||||
}
|
||||
}
|
||||
else if (input.StartsWith("rmuser"))
|
||||
{
|
||||
if (selectedUser == 0)
|
||||
{
|
||||
Console.WriteLine("No user selected");
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
|
||||
if (user == null)
|
||||
{
|
||||
Console.WriteLine("Selected user does not exist");
|
||||
selectedUser = 0;
|
||||
prompt = "# ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write("Are you sure you want to delete user " + user.Username + "? (y/n) ");
|
||||
var confirm = Console.ReadLine();
|
||||
if (confirm == "y")
|
||||
{
|
||||
JsonDb.Instance.Users.Remove(user);
|
||||
JsonDb.Save();
|
||||
Console.WriteLine("User deleted");
|
||||
selectedUser = 0;
|
||||
prompt = "# ";
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("User not deleted");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (input.StartsWith("completestage"))
|
||||
{
|
||||
if (selectedUser == 0)
|
||||
{
|
||||
Console.WriteLine("No user selected");
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == selectedUser);
|
||||
if (user == null)
|
||||
{
|
||||
Console.WriteLine("Selected user does not exist");
|
||||
selectedUser = 0;
|
||||
prompt = "# ";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (args.Length == 2)
|
||||
{
|
||||
var input2 = args[1];
|
||||
try
|
||||
{
|
||||
var chapter = int.TryParse(input2.Split('-')[0], out int chapterNumber);
|
||||
var stage = int.TryParse(input2.Split('-')[1], out int stageNumber);
|
||||
|
||||
if (chapter && stage)
|
||||
{
|
||||
for (int i = 0; i < chapterNumber + 1; i++)
|
||||
{
|
||||
var stages = StaticDataParser.Instance.GetStageIdsForChapter(i, true);
|
||||
int target = 1;
|
||||
foreach (var item in stages)
|
||||
{
|
||||
if (!user.IsStageCompleted(item, true))
|
||||
{
|
||||
Console.WriteLine("Completing stage " + item);
|
||||
ClearStage.CompleteStage(user, item);
|
||||
}
|
||||
|
||||
if (i == chapterNumber && target == stageNumber)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
target++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("chapter and stage number must be a 32 bit integer");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("exception:" + ex.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("invalid argument length, must be 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (input == "exit")
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
else if (input == "ban")
|
||||
{
|
||||
Console.WriteLine("Not implemented");
|
||||
}
|
||||
else if (input == "unban")
|
||||
{
|
||||
Console.WriteLine("Not implemented");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Unknown command");
|
||||
}
|
||||
}
|
||||
}
|
||||
private static WebServer CreateWebServer()
|
||||
{
|
||||
var cert = new X509Certificate2(new X509Certificate(AppDomain.CurrentDomain.BaseDirectory + @"site.pfx"));
|
||||
|
||||
var server = new WebServer(o => o
|
||||
.WithUrlPrefixes("https://*:443", "http://*:80")
|
||||
.WithUrlPrefixes("https://*:443")
|
||||
.WithMode(HttpListenerMode.EmbedIO).WithAutoLoadCertificate().WithCertificate(cert))
|
||||
// First, we will configure our web server by adding Modules.
|
||||
.WithLocalSessionManager()
|
||||
@@ -60,7 +248,10 @@ namespace nksrv
|
||||
.WithModule(new ActionModule("/media/", HttpVerbs.Any, HandleAsset))
|
||||
.WithModule(new ActionModule("/PC/", HttpVerbs.Any, HandleAsset))
|
||||
.WithModule(new ActionModule("/$batch", HttpVerbs.Any, HandleBatchRequests))
|
||||
.WithModule(new ActionModule("/nikke_launcher", HttpVerbs.Any, HandleLauncherUI));
|
||||
.WithStaticFolder("/nikke_launcher", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "launcher"), true)
|
||||
.WithStaticFolder("/admin/assets/", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "admin", "assets"), true)
|
||||
.WithModule(new ActionModule("/admin", HttpVerbs.Any, HandleAdminRequest))
|
||||
.WithWebApi("/adminapi", m => m.WithController(typeof(AdminApiController)));
|
||||
|
||||
// Listen for state changes.
|
||||
//server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info();
|
||||
@@ -68,28 +259,59 @@ namespace nksrv
|
||||
return server;
|
||||
}
|
||||
|
||||
private static async Task HandleLauncherUI(IHttpContext ctx)
|
||||
private static async Task HandleAdminRequest(IHttpContext context)
|
||||
{
|
||||
await ctx.SendStringAsync(@"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Private Nikke Server Launcher</title>
|
||||
<style>
|
||||
* {
|
||||
color:white;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>What's new in Nikke Private Server<h1>
|
||||
<p>This is the inital release, only story mode works except that you can't collect items and a few other things don't work.</p>
|
||||
<p>In order to level up characters, you manually have to edit db.json</p>
|
||||
</body>
|
||||
</html>
|
||||
", "text/html", Encoding.UTF8);
|
||||
//check if user is logged in
|
||||
if (context.Request.Cookies["token"] == null && context.Request.Url.PathAndQuery != "/api/login")
|
||||
{
|
||||
context.Redirect("/adminapi/login");
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if authenticated correctly
|
||||
User? currentUser = null;
|
||||
if (context.Request.Url.PathAndQuery != "/api/login")
|
||||
{
|
||||
//verify token
|
||||
foreach (var item in AdminApiController.AdminAuthTokens)
|
||||
{
|
||||
if (item.Key == context.Request.Cookies["token"].Value)
|
||||
{
|
||||
currentUser = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentUser == null)
|
||||
{
|
||||
context.Redirect("/adminapi/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Request.Url.PathAndQuery == "/admin/")
|
||||
{
|
||||
context.Redirect("/admin/dashboard");
|
||||
}
|
||||
else if (context.Request.Url.PathAndQuery == "/admin/dashboard")
|
||||
{
|
||||
await context.SendStringAsync(ProcessAdminPage("dashbrd.html", currentUser), "text/html", Encoding.Unicode);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
await context.SendStringAsync("404 not found", "text/html", Encoding.Unicode);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProcessAdminPage(string pg, User? currentUser)
|
||||
{
|
||||
var pgContent = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "admin", pg));
|
||||
var nav = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "www", "admin", "nav.html"));
|
||||
|
||||
//navbar
|
||||
pgContent = pgContent.Replace("{{navbar}}", nav);
|
||||
|
||||
return pgContent;
|
||||
}
|
||||
private static async Task HandleBatchRequests(IHttpContext ctx)
|
||||
{
|
||||
var theBytes = await PacketDecryption.DecryptOrReturnContentAsync(ctx);
|
||||
@@ -169,7 +391,7 @@ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
public static string GetCachePathForPath(string path)
|
||||
{
|
||||
return AppDomain.CurrentDomain.BaseDirectory + "cache" + path;
|
||||
return AppDomain.CurrentDomain.BaseDirectory + "cache/" + path;
|
||||
}
|
||||
private static async Task HandleAsset(IHttpContext ctx)
|
||||
{
|
||||
|
||||
@@ -2195,4 +2195,106 @@ message ReqAllClearEquipment {
|
||||
message ResAllClearEquipment {
|
||||
int64 Csn = 2;
|
||||
repeated NetUserItemData Items = 3;
|
||||
}
|
||||
|
||||
message NetFavoriteItemLibraryElement {
|
||||
int32 Tid = 1;
|
||||
int64 ReceivedAt = 2;
|
||||
}
|
||||
|
||||
message ReqGetFavoriteItemLibrary {
|
||||
|
||||
}
|
||||
message ResGetFavoriteItemLibrary {
|
||||
repeated NetFavoriteItemLibraryElement FavoriteItemLibrary = 1;
|
||||
}
|
||||
|
||||
|
||||
message ReqListFavoriteItem {
|
||||
|
||||
}
|
||||
message ResListFavoriteItem {
|
||||
repeated NetUserFavoriteItemData FavoriteItems = 1;
|
||||
}
|
||||
|
||||
message NetUserFavoriteItemQuestData {
|
||||
int32 QuestId = 1;
|
||||
bool Clear = 2;
|
||||
bool Received = 3;
|
||||
}
|
||||
message ReqListFavoriteItemQuest {
|
||||
|
||||
}
|
||||
message ResListFavoriteItemQuest {
|
||||
repeated NetUserFavoriteItemQuestData FavoriteItemQuests = 1;
|
||||
}
|
||||
|
||||
message NetUserLostSectorData {
|
||||
int32 SectorId = 1;
|
||||
int32 RewardCount = 2;
|
||||
bool IsFinalReward = 3;
|
||||
bool IsPlaying = 4;
|
||||
bool IsOpen = 5;
|
||||
int32 CurrentClearStageCount = 6;
|
||||
int32 MaxClearStageCount = 7;
|
||||
bool IsPerfectReward = 8;
|
||||
}
|
||||
message ReqGetLostSectorData {
|
||||
|
||||
}
|
||||
message ResGetLostSectorData {
|
||||
repeated NetUserLostSectorData LostSector = 2;
|
||||
int32 LastEnterSectorId = 3;
|
||||
repeated NetFieldStageData ClearedStages = 4;
|
||||
}
|
||||
|
||||
message NetSideStoryStageData {
|
||||
int32 SideStoryStageId = 1;
|
||||
google.protobuf.Timestamp ClearedAt = 2;
|
||||
}
|
||||
|
||||
message ReqListSideStory {}
|
||||
message ResListSideStory {
|
||||
repeated NetSideStoryStageData SideStoryStageDataList = 1;
|
||||
}
|
||||
|
||||
enum LiberateMissionState {
|
||||
LiberateMissionState_Running = 0;
|
||||
LiberateMissionState_Rewarded = 1;
|
||||
LiberateMissionState_Closed = 2;
|
||||
}
|
||||
|
||||
message NetLiberateMissionData {
|
||||
int64 Id = 1;
|
||||
int32 MissionTid = 2;
|
||||
int32 LiberateCharacterId = 3;
|
||||
LiberateMissionState MissionState = 4;
|
||||
int64 CreatedAt = 6;
|
||||
int64 TriggerStartAt = 7;
|
||||
int64 TriggerEndAt = 8;
|
||||
optional int64 ReceivedAt = 9;
|
||||
}
|
||||
|
||||
message NetLiberateData {
|
||||
int32 CharacterId = 2;
|
||||
int32 StepId = 3;
|
||||
int32 ProgressPoint = 4;
|
||||
repeated NetLiberateMissionData MissionData = 5;
|
||||
int32 RewardedCount = 6;
|
||||
bool IsCompleted = 7;
|
||||
}
|
||||
|
||||
message ReqGetLiberateData {}
|
||||
message ResGetLiberateData {
|
||||
repeated int32 OpenLiberateTypeIdList = 2;
|
||||
NetLiberateData LiberateData = 3;
|
||||
}
|
||||
|
||||
message ReqShowSpecialArenaReward {}
|
||||
message ResShowSpecialArenaReward {
|
||||
NetRewardData reward = 4;
|
||||
/*SpecialArenaContentsState SpecialArenaContentsState = 5;
|
||||
NetSpecialArenaRewardHistory History = 6;*/
|
||||
bool IsBan = 7;
|
||||
NetArenaBanInfo BanInfo = 8;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ namespace nksrv.StaticInfo
|
||||
/// Can be Normal or Hard
|
||||
/// </summary>
|
||||
public string chapter_mod = "";
|
||||
public string stage_type = "";
|
||||
}
|
||||
public class RewardTableRecord
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using ICSharpCode.SharpZipLib.Zip;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Swan.Parsers;
|
||||
using Newtonsoft.Json;
|
||||
using System.Drawing;
|
||||
|
||||
namespace nksrv.StaticInfo
|
||||
{
|
||||
@@ -20,14 +21,6 @@ namespace nksrv.StaticInfo
|
||||
/// </summary>
|
||||
public class StaticDataParser
|
||||
{
|
||||
// Extracted from staticinfo api call
|
||||
public const string StaticDataUrl = "https://cloud.nikke-kr.com/prdenv/122-c8cee37754/staticdata/data/qa-240704-07b/312528/StaticData.pack";
|
||||
public const string Version = "data/qa-240704-07b/312528";
|
||||
public const int Size = 11799792;
|
||||
public static byte[] Sha256Sum = Convert.FromBase64String("Wzy+AcGutLR6z1yM7lp+UpFkNuErf56Aj6e9taGH8j4=");
|
||||
public static byte[] Salt1 = Convert.FromBase64String("vZ3Nv6JwfaZJpHwmUc0kyV7Q3Yzm8ysPhyVE0R0GVTc=");
|
||||
public static byte[] Salt2 = Convert.FromBase64String("L29mjnvnlktQ1vLq+E56FkRECojiaHx9UmWzsurBfIU=");
|
||||
|
||||
// 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()
|
||||
@@ -38,7 +31,19 @@ namespace nksrv.StaticInfo
|
||||
};
|
||||
|
||||
// Fields
|
||||
public static StaticDataParser Instance;
|
||||
private static StaticDataParser? _instance;
|
||||
public static StaticDataParser Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = BuildAsync().Result;
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
private ZipFile MainZip;
|
||||
private MemoryStream ZipStream;
|
||||
private JArray questDataRecords;
|
||||
@@ -49,16 +54,22 @@ namespace nksrv.StaticInfo
|
||||
private JArray characterCostumeTable;
|
||||
private JArray characterTable;
|
||||
private JArray tutorialTable;
|
||||
private JArray itemEquipTable;
|
||||
|
||||
static StaticDataParser()
|
||||
public byte[] Sha256Hash;
|
||||
public int Size;
|
||||
|
||||
static async Task<StaticDataParser> BuildAsync()
|
||||
{
|
||||
Logger.Info("Loading static data");
|
||||
Load().Wait();
|
||||
if (Instance == null) throw new Exception("static data load fail");
|
||||
await Load();
|
||||
|
||||
Logger.Info("Parsing static data");
|
||||
Instance.Parse().Wait();
|
||||
await Instance.Parse();
|
||||
|
||||
return Instance;
|
||||
}
|
||||
|
||||
public StaticDataParser(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath)) throw new ArgumentException("Static data file must exist", nameof(filePath));
|
||||
@@ -73,6 +84,11 @@ namespace nksrv.StaticInfo
|
||||
characterTable = new();
|
||||
ZipStream = new();
|
||||
tutorialTable = new();
|
||||
itemEquipTable = new();
|
||||
|
||||
var rawBytes = File.ReadAllBytes(filePath);
|
||||
Sha256Hash = SHA256.HashData(rawBytes);
|
||||
Size = rawBytes.Length;
|
||||
|
||||
DecryptStaticDataAndLoadZip(filePath);
|
||||
if (MainZip == null) throw new Exception("failed to read zip file");
|
||||
@@ -82,7 +98,7 @@ namespace nksrv.StaticInfo
|
||||
{
|
||||
using var fileStream = File.Open(file, FileMode.Open, FileAccess.Read);
|
||||
|
||||
var keyDecryptor = new Rfc2898DeriveBytes(PresharedKey, Salt2, 10000, HashAlgorithmName.SHA256);
|
||||
var keyDecryptor = new Rfc2898DeriveBytes(PresharedKey, GameConfig.Root.StaticData.GetSalt2Bytes(), 10000, HashAlgorithmName.SHA256);
|
||||
var key2 = keyDecryptor.GetBytes(32);
|
||||
|
||||
byte[] decryptionKey = key2[0..16];
|
||||
@@ -128,7 +144,7 @@ namespace nksrv.StaticInfo
|
||||
dataMs.Position = 0;
|
||||
|
||||
// Decryption of layer 3
|
||||
var keyDecryptor2 = new Rfc2898DeriveBytes(PresharedKey, Salt1, 10000, HashAlgorithmName.SHA256);
|
||||
var keyDecryptor2 = new Rfc2898DeriveBytes(PresharedKey, GameConfig.Root.StaticData.GetSalt1Bytes(), 10000, HashAlgorithmName.SHA256);
|
||||
var key3 = keyDecryptor2.GetBytes(32);
|
||||
|
||||
byte[] decryptionKey2 = key3[0..16];
|
||||
@@ -142,8 +158,7 @@ namespace nksrv.StaticInfo
|
||||
MainZip = new ZipFile(ZipStream, false);
|
||||
}
|
||||
|
||||
public static void AesCtrTransform(
|
||||
byte[] key, byte[] salt, Stream inputStream, Stream outputStream)
|
||||
public static void AesCtrTransform(byte[] key, byte[] salt, Stream inputStream, Stream outputStream)
|
||||
{
|
||||
SymmetricAlgorithm aes = Aes.Create();
|
||||
aes.Mode = CipherMode.ECB;
|
||||
@@ -195,10 +210,10 @@ namespace nksrv.StaticInfo
|
||||
}
|
||||
public static async Task Load()
|
||||
{
|
||||
var targetFile = await AssetDownloadUtil.DownloadOrGetFileAsync(StaticDataUrl, CancellationToken.None);
|
||||
var targetFile = await AssetDownloadUtil.DownloadOrGetFileAsync(GameConfig.Root.StaticData.Url, CancellationToken.None);
|
||||
if (targetFile == null) throw new Exception("static data download fail");
|
||||
|
||||
Instance = new(targetFile);
|
||||
_instance = new(targetFile);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -229,6 +244,7 @@ namespace nksrv.StaticInfo
|
||||
characterCostumeTable = await LoadZip("CharacterCostumeTable.json");
|
||||
characterTable = await LoadZip("CharacterTable.json");
|
||||
tutorialTable = await LoadZip("ContentsTutorialTable.json");
|
||||
itemEquipTable = await LoadZip("ItemEquipTable.json");
|
||||
}
|
||||
|
||||
public MainQuestCompletionData? GetMainQuestForStageClearCondition(int stage)
|
||||
@@ -303,7 +319,13 @@ namespace nksrv.StaticInfo
|
||||
|
||||
return null;
|
||||
}
|
||||
public int GetUserLevelFromUserExp(int targetExp)
|
||||
/// <summary>
|
||||
/// Returns the level and its minimum value for XP value
|
||||
/// </summary>
|
||||
/// <param name="targetExp"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public (int, int) GetUserLevelFromUserExp(int targetExp)
|
||||
{
|
||||
int prevLevel = 0;
|
||||
int prevValue = 0;
|
||||
@@ -328,10 +350,10 @@ namespace nksrv.StaticInfo
|
||||
}
|
||||
else
|
||||
{
|
||||
return prevLevel;
|
||||
return (prevLevel, prevValue);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return (-1, -1);
|
||||
}
|
||||
public int GetNormalChapterNumberFromFieldName(string field)
|
||||
{
|
||||
@@ -403,5 +425,45 @@ namespace nksrv.StaticInfo
|
||||
|
||||
throw new Exception("tutorial not found: " + TableId);
|
||||
}
|
||||
|
||||
public string? GetItemSubType(int itemType)
|
||||
{
|
||||
foreach (JObject item in itemEquipTable)
|
||||
{
|
||||
var id = item["id"];
|
||||
if (id == null) throw new Exception("expected id field in reward data");
|
||||
|
||||
int? idValue = id.ToObject<int>();
|
||||
if (idValue == itemType)
|
||||
{
|
||||
var subtype = item["item_sub_type"];
|
||||
if (subtype == null)
|
||||
{
|
||||
throw new Exception("expected item_sub_type field in item equip data");
|
||||
}
|
||||
|
||||
return subtype.ToObject<string>();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal IEnumerable<int> GetStageIdsForChapter(int chapterNumber, bool normal)
|
||||
{
|
||||
string mod = normal ? "Normal" : "Hard";
|
||||
foreach (JObject item in stageDataRecords)
|
||||
{
|
||||
CampaignStageRecord? data = JsonConvert.DeserializeObject<CampaignStageRecord>(item.ToString());
|
||||
if (data == null) throw new Exception("failed to deserialize stage data");
|
||||
|
||||
int chVal = data.chapter_id - 1;
|
||||
|
||||
if (chapterNumber == chVal && data.chapter_mod == mod && data.stage_type == "Main")
|
||||
{
|
||||
yield return data.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Swan.Logging;
|
||||
using DnsClient;
|
||||
using Swan.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -11,9 +12,11 @@ namespace nksrv.Utils
|
||||
public class AssetDownloadUtil
|
||||
{
|
||||
public static readonly HttpClient AssetDownloader = new(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.All });
|
||||
|
||||
private static string? CloudIp = null;
|
||||
public static async Task<string?> DownloadOrGetFileAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var rawUrl = url.Replace("https://cloud.nikke-kr.com", "");
|
||||
var rawUrl = url.Replace("https://cloud.nikke-kr.com/", "");
|
||||
string targetFile = Program.GetCachePathForPath(rawUrl);
|
||||
var targetDir = Path.GetDirectoryName(targetFile);
|
||||
if (targetDir == null)
|
||||
@@ -27,12 +30,12 @@ namespace nksrv.Utils
|
||||
{
|
||||
Logger.Info("Download " + targetFile);
|
||||
|
||||
// TODO: Ip might change for cloud.nikke-kr.com
|
||||
string @base = rawUrl.StartsWith("/prdenv") ? "prdenv" : "media";
|
||||
if (rawUrl.StartsWith("/PC"))
|
||||
@base = "PC";
|
||||
if (CloudIp == null)
|
||||
{
|
||||
CloudIp = await GetCloudIpAsync();
|
||||
}
|
||||
|
||||
var requestUri = new Uri("https://35.190.17.65/" + @base + rawUrl);
|
||||
var requestUri = new Uri("https://" + CloudIp + "/" + rawUrl);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
|
||||
request.Headers.TryAddWithoutValidation("host", "cloud.nikke-kr.com");
|
||||
using var response = await AssetDownloader.SendAsync(request);
|
||||
@@ -48,11 +51,23 @@ namespace nksrv.Utils
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to download " + url + " with status code " + response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
private static async Task<string> GetCloudIpAsync()
|
||||
{
|
||||
var lookup = new LookupClient();
|
||||
var result = await lookup.QueryAsync("cloud.nikke-kr.com", QueryType.A);
|
||||
|
||||
var record = result.Answers.ARecords().FirstOrDefault();
|
||||
var ip = record?.Address;
|
||||
|
||||
return ip.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
nksrv/Utils/GameConfig.cs
Normal file
64
nksrv/Utils/GameConfig.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Newtonsoft.Json;
|
||||
using Swan.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace nksrv.Utils
|
||||
{
|
||||
public class GameConfigRoot
|
||||
{
|
||||
public StaticData StaticData { get; set; } = new();
|
||||
public string ResourceBaseURL { get; set; } = "";
|
||||
}
|
||||
|
||||
public class StaticData
|
||||
{
|
||||
public string Url { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string Salt1 { get; set; } = "";
|
||||
public string Salt2 { get; set; } = "";
|
||||
|
||||
|
||||
public byte[] GetSalt1Bytes()
|
||||
{
|
||||
return Convert.FromBase64String(Salt1);
|
||||
}
|
||||
public byte[] GetSalt2Bytes()
|
||||
{
|
||||
return Convert.FromBase64String(Salt2);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GameConfig
|
||||
{
|
||||
private static GameConfigRoot? _root;
|
||||
public static GameConfigRoot Root
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_root == null)
|
||||
{
|
||||
if (!File.Exists(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json"))
|
||||
{
|
||||
Logger.Error("Gameconfig.json is not found, the game WILL NOT work!");
|
||||
_root = new GameConfigRoot();
|
||||
}
|
||||
Logger.Info("Loaded game config");
|
||||
|
||||
|
||||
_root = JsonConvert.DeserializeObject<GameConfigRoot>(File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json"));
|
||||
|
||||
if (_root == null)
|
||||
{
|
||||
throw new Exception("Failed to read gameconfig.json");
|
||||
}
|
||||
}
|
||||
|
||||
return _root;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using ASodium;
|
||||
using Newtonsoft.Json;
|
||||
using nksrv.LobbyServer;
|
||||
using nksrv.LobbyServer.Msgs.Stage;
|
||||
using nksrv.StaticInfo;
|
||||
using Swan.Logging;
|
||||
using System;
|
||||
@@ -78,6 +79,7 @@ namespace nksrv.Utils
|
||||
public string Nickname = "SomePlayer";
|
||||
public int ProfileIconId = 39900;
|
||||
public bool ProfileIconIsPrism = false;
|
||||
public bool IsAdmin = false;
|
||||
|
||||
// Game data
|
||||
public List<string> CompletedScenarios = [];
|
||||
@@ -134,10 +136,28 @@ namespace nksrv.Utils
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
public bool IsStageCompleted(int id, bool isNorm)
|
||||
{
|
||||
foreach (var item in FieldInfo)
|
||||
{
|
||||
if (item.Key.Contains("hard") && isNorm) continue;
|
||||
if (item.Key.Contains("normal") && !isNorm) continue;
|
||||
foreach (var s in item.Value.CompletedStages)
|
||||
{
|
||||
if (s.StageId == id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public class CoreInfo
|
||||
{
|
||||
public int DbVersion = 0;
|
||||
public int DbVersion = 2;
|
||||
public List<User> Users = [];
|
||||
|
||||
public List<AccessToken> LauncherAccessTokens = [];
|
||||
@@ -187,6 +207,20 @@ namespace nksrv.Utils
|
||||
}
|
||||
Console.WriteLine("Database update completed");
|
||||
}
|
||||
else if (Instance.DbVersion == 1)
|
||||
{
|
||||
Console.WriteLine("Starting database update...");
|
||||
// there was a bug where equipment position was not saved, so remove all items from each characters
|
||||
Instance.DbVersion = 2;
|
||||
foreach (var user in Instance.Users)
|
||||
{
|
||||
foreach (var f in user.Items.ToList())
|
||||
{
|
||||
f.Csn = 0;
|
||||
}
|
||||
}
|
||||
Console.WriteLine("Database update completed");
|
||||
}
|
||||
Save();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
|
||||
|
||||
using nksrv.StaticInfo;
|
||||
using Swan.Logging;
|
||||
using System.Reflection;
|
||||
|
||||
namespace nksrv.Utils
|
||||
{
|
||||
public class NetUtils
|
||||
@@ -17,5 +22,61 @@ namespace nksrv.Utils
|
||||
Tid = item.ItemType
|
||||
};
|
||||
}
|
||||
|
||||
public static List<NetUserItemData> GetUserItems(User user)
|
||||
{
|
||||
List<NetUserItemData> ret = new();
|
||||
Dictionary<int, NetUserItemData> itemDictionary = new Dictionary<int, NetUserItemData>();
|
||||
|
||||
foreach (var item in user.Items.ToList())
|
||||
{
|
||||
if (item.Csn == 0)
|
||||
{
|
||||
if (itemDictionary.ContainsKey(item.ItemType))
|
||||
{
|
||||
itemDictionary[item.ItemType].Count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemDictionary[item.ItemType] = new NetUserItemData() { Count = item.Count, Tid = item.ItemType, Csn = item.Csn, Lv = item.Level, Exp = item.Exp, Corporation = item.Corp, Isn = item.Isn, Position = item.Position };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newItem = new NetUserItemData() { Count = item.Count, Tid = item.ItemType, Csn = item.Csn, Lv = item.Level, Exp = item.Exp, Corporation = item.Corp, Isn = item.Isn, Position = item.Position };
|
||||
itemDictionary[item.ItemType] = newItem;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static int GetItemPos(User user, long isn)
|
||||
{
|
||||
foreach (var item in user.Items)
|
||||
{
|
||||
if (item.Isn == isn)
|
||||
{
|
||||
var subType = StaticDataParser.Instance.GetItemSubType(item.ItemType);
|
||||
switch(subType)
|
||||
{
|
||||
case "Module_A":
|
||||
return 0;
|
||||
case "Module_B":
|
||||
return 1;
|
||||
case "Module_C":
|
||||
return 2;
|
||||
case "Module_D":
|
||||
return 3;
|
||||
default:
|
||||
Logger.Warn("Unknown item subtype: " + subType);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
nksrv/gameconfig.json
Normal file
13
nksrv/gameconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// Asset Urls for game version 122.8.20f
|
||||
// Extracted from POST https://global-lobby.nikke-kr.com/v1/staticdatapack
|
||||
"StaticData": {
|
||||
"Url": "https://cloud.nikke-kr.com/prdenv/122-c8cee37754/staticdata/data/qa-240704-07b/313275/StaticData.pack",
|
||||
"Version": "data/qa-240704-07b/313275",
|
||||
"Salt1": "7OpvuafRK67Rf0X2VJrzIAqZ0CBPbY4IWWdtbQ3LyV8=",
|
||||
"Salt2": "zR7nPjsRCPUfN9BViVkk5R/KOCkVimb8VSE+yOqey+g="
|
||||
},
|
||||
|
||||
// Extracted from POST https://global-lobby.nikke-kr.com/v1/resourcehosts2
|
||||
"ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/122-b0255105e0/{Platform}"
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ASodium" Version="0.6.1" />
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="EmbedIO" Version="3.5.2" />
|
||||
<PackageReference Include="Google.Api.CommonProtos" Version="2.15.0" />
|
||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.27.1" />
|
||||
@@ -29,8 +30,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="gameconfig.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="site.pfx">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="www\**\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
23
nksrv/www/admin/assets/login.css
Normal file
23
nksrv/www/admin/assets/login.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@ -0,0 +1,23 @@ html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: url('/admin/assets/login.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
/* Center child horizontally*/
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.LoginBox {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px 20px 20px 20px;
|
||||
}
|
||||
BIN
nksrv/www/admin/assets/login.jpg
Normal file
BIN
nksrv/www/admin/assets/login.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
239
nksrv/www/admin/assets/style.css
Normal file
239
nksrv/www/admin/assets/style.css
Normal file
@@ -0,0 +1,239 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin: 5px 0px 5px 0;
|
||||
}
|
||||
|
||||
|
||||
/* Tabs */
|
||||
/* Style the tab */
|
||||
.tab {
|
||||
float: left;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f1f1f1;
|
||||
width: 30%;
|
||||
height: 1000px;
|
||||
}
|
||||
|
||||
/* Style the buttons inside the tab */
|
||||
.tab button {
|
||||
display: block;
|
||||
background-color: inherit;
|
||||
color: black;
|
||||
padding: 22px 16px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
/* Change background color of buttons on hover */
|
||||
.tab button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
/* Create an active/current "tab button" class */
|
||||
.tab button.active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
/* Style the tab content */
|
||||
.tabcontent {
|
||||
float: left;
|
||||
padding: 5px 5px 5px 5px;
|
||||
border: 1px solid #ccc;
|
||||
width: 70%;
|
||||
border-left: none;
|
||||
height: 1000px;
|
||||
}
|
||||
|
||||
.navbar2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--main-nav-light-bg);
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
float: left;
|
||||
display: block;
|
||||
color: var(--main-nav-light-color);
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-item:hover {
|
||||
background-color: #029761;
|
||||
text-decoration: none;
|
||||
color: var(--main-nav-light-color);
|
||||
}
|
||||
|
||||
.navbar-item:not(.active):hover {
|
||||
background-color: var(--main-nav-light-hover);
|
||||
text-decoration: none;
|
||||
color: var(--main-nav-light-color);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #04AA6D;
|
||||
}
|
||||
|
||||
:root {
|
||||
--main-nav-dark-bg: #333;
|
||||
--main-nav-dark-color: white;
|
||||
--main-nav-dark-hover: #111;
|
||||
--main-nav-light-bg: #f1f1f1;
|
||||
--main-nav-light-color: black;
|
||||
--main-nav-light-hover: #dddddd;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
zonegroup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
zonegroup p {
|
||||
display: flex;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
zonegroup input {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
.dropdown2 {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown2 .dropbtn {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: black;
|
||||
padding: 14px 16px;
|
||||
background-color: inherit;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navbar a:hover, .dropdown2:hover .dropbtn, .dropbtn:focus {
|
||||
background-color: green;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px 12px 7px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Leave this at the end */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: rgb(25, 25, 25);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.app-bar {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
/* For browsers that do not support gradients */
|
||||
background-color: #A3A6A9;
|
||||
background-image: linear-gradient(to right, #A3A6A9, #666765);
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: darkgray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab {
|
||||
background-color: rgb(36,36,36);
|
||||
}
|
||||
|
||||
.tab button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab button:hover {
|
||||
background-color: #666765;
|
||||
}
|
||||
|
||||
.tab button.active {
|
||||
background-color: darkgray;
|
||||
}
|
||||
|
||||
.navbar2 {
|
||||
background-color: var(--main-nav-dark-bg);
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
color: var(--main-nav-dark-color);
|
||||
}
|
||||
|
||||
.navbar-item:hover {
|
||||
background-color: #029761;
|
||||
text-decoration: none;
|
||||
color: var(--main-nav-dark-color);
|
||||
}
|
||||
|
||||
.navbar-item:not(.active):hover {
|
||||
background-color: var(--main-nav-dark-hover);
|
||||
text-decoration: none;
|
||||
color: var(--main-nav-dark-color);
|
||||
}
|
||||
|
||||
.modal {
|
||||
--bs-modal-bg: black;
|
||||
}
|
||||
|
||||
.dropdown2 .dropbtn {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
21
nksrv/www/admin/dashbrd.html
Normal file
21
nksrv/www/admin/dashbrd.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Security System Controller</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/admin/assets/style.css">
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
{{navbar}}
|
||||
<div class="containter">
|
||||
<h1>Welcome to Nikke Private Server Admin Panel</h1>
|
||||
<p>There are no settings to display.</p>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
34
nksrv/www/admin/index.html
Normal file
34
nksrv/www/admin/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Security System Controller</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
|
||||
<link rel="stylesheet" href="/admin/assets/login.css">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<div class="LoginBox">
|
||||
<h1>Login</h1>
|
||||
<form action="/adminapi/login" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="UsernameBox" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="UsernameBox" name="username">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="PasswordBox" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="PasswordBox" name="password">
|
||||
</div>
|
||||
<errormsg/>
|
||||
<br />
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
3
nksrv/www/admin/nav.html
Normal file
3
nksrv/www/admin/nav.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="navbar2">
|
||||
<a href="/admin/dashboard" class="navbar-item">Overview</a>
|
||||
</div>
|
||||
26
nksrv/www/launcher/index.html
Normal file
26
nksrv/www/launcher/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Private Nikke Server Launcher</title>
|
||||
<style>
|
||||
* {
|
||||
color: white;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>What's new in Nikke Private Server - v0.1.2</h1>
|
||||
<ul>
|
||||
<li>Fixed static download fail</li>
|
||||
<li>Asset server IP address is no longer hardcoded.</li>
|
||||
<li>Skipping stages is now implemented via command line interface</li>
|
||||
<li>Added required messages for ch15+</li>
|
||||
<li>Fixed XP system</li>
|
||||
<li>Fixed game crash due to inventory system</li>
|
||||
<li>Check if game path / launcher path is correct in server selector</li>
|
||||
<li>Implemented commander level up reward</li>
|
||||
<li>Began work on admin panel</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user