add update resources button in admin panel

This commit is contained in:
Mikhail Tyukin
2025-06-23 21:08:50 +04:00
parent 3bd955eebd
commit 1ba1a89110
14 changed files with 169 additions and 33 deletions

View File

@@ -63,7 +63,7 @@ namespace EpinelPS.Controllers
} }
[HttpPost("RunCmd")] [HttpPost("RunCmd")]
public RunCmdResponse RunCmd([FromBody] RunCmdRequest req) public async Task<RunCmdResponse> RunCmd([FromBody] RunCmdRequest req)
{ {
if (!AdminController.CheckAuth(HttpContext)) return new RunCmdResponse() { error = "bad token" }; if (!AdminController.CheckAuth(HttpContext)) return new RunCmdResponse() { error = "bad token" };
@@ -124,6 +124,10 @@ namespace EpinelPS.Controllers
var s = req.p2.Split("-"); var s = req.p2.Split("-");
return AdminCommands.AddItem(user, int.Parse(s[0]), int.Parse(s[1])); return AdminCommands.AddItem(user, int.Parse(s[0]), int.Parse(s[1]));
} }
case "updateServer":
{
return await AdminCommands.UpdateResources();
}
} }
return new RunCmdResponse() { error = "Not implemented" }; return new RunCmdResponse() { error = "Not implemented" };
} }

View File

@@ -22,10 +22,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ASodium" Version="0.6.1" /> <PackageReference Include="ASodium" Version="0.6.2" />
<PackageReference Include="DnsClient" Version="1.8.0" /> <PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.16.0" /> <PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.30.2" /> <PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" />
<PackageReference Include="Grpc.AspNetCore" Version="2.71.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

View File

@@ -13,7 +13,8 @@ namespace EpinelPS.LobbyServer.Event
ResEnterEventField response = new() ResEnterEventField response = new()
{ {
Field = new() Field = new(),
Json = "{}"
}; };
// Retrieve collected objects // Retrieve collected objects

View File

@@ -21,7 +21,7 @@ namespace EpinelPS
{ {
Console.WriteLine($"EpinelPS v{Assembly.GetExecutingAssembly().GetName().Version} - https://github.com/EpinelPS/EpinelPS/"); Console.WriteLine($"EpinelPS v{Assembly.GetExecutingAssembly().GetName().Version} - https://github.com/EpinelPS/EpinelPS/");
Console.WriteLine("This software is licensed under the AGPL-3.0 License"); Console.WriteLine("This software is licensed under the AGPL-3.0 License");
Console.WriteLine("Targeting game version " + GameConfig.Root.GameMaxVer); Console.WriteLine("Targeting game version " + GameConfig.Root.TargetVersion);
GameData.Instance.GetAllCostumes(); // force static data to be loaded GameData.Instance.GetAllCostumes(); // force static data to be loaded
@@ -101,6 +101,7 @@ namespace EpinelPS
app.MapGet("/prdenv/{**all}", AssetDownloadUtil.HandleReq); app.MapGet("/prdenv/{**all}", AssetDownloadUtil.HandleReq);
app.MapGet("/PC/{**all}", AssetDownloadUtil.HandleReq); app.MapGet("/PC/{**all}", AssetDownloadUtil.HandleReq);
app.MapGet("/media/{**all}", AssetDownloadUtil.HandleReq); app.MapGet("/media/{**all}", AssetDownloadUtil.HandleReq);
app.MapPost("/rqd/sync", HandleRqd);
// NOTE: pub prefixes shows public (production server), local is local server (does not have any effect), dev is development server, etc. // NOTE: pub prefixes shows public (production server), local is local server (does not have any effect), dev is development server, etc.
// It does not have any effect, except for the publisher server, which adds a watermark? // It does not have any effect, except for the publisher server, which adds a watermark?
@@ -189,6 +190,11 @@ namespace EpinelPS
} }
} }
private static async Task HandleRqd(HttpContext context)
{
}
private static void CliLoop() private static void CliLoop()
{ {
ulong selectedUser = 0; ulong selectedUser = 0;

View File

@@ -1,15 +1,59 @@
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using DnsClient; using DnsClient;
using EpinelPS.Data; using EpinelPS.Data;
using EpinelPS.Database; using EpinelPS.Database;
using EpinelPS.LobbyServer.Stage; using EpinelPS.LobbyServer.Stage;
using EpinelPS.Models.Admin; using EpinelPS.Models.Admin;
using System.Net; using Google.Protobuf;
namespace EpinelPS.Utils namespace EpinelPS.Utils
{ {
public class AdminCommands public class AdminCommands
{ {
private static HttpClient client;
private static string serverUrl = "global-lobby.nikke-kr.com";
private static string connectingServer = serverUrl;
private static string? serverIp;
private static string? staticDataUrl;
private static string? resourcesUrl;
static AdminCommands()
{
// Use TLS 1.1 so that tencents cloudflare knockoff wont complain
var handler = new SocketsHttpHandler
{
ConnectCallback = async (context, cancellationToken) =>
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
var sslStream = new SslStream(new NetworkStream(socket, ownsSocket: true));
// When using HTTP/2, you must also keep in mind to set options like ApplicationProtocols
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
{
TargetHost = connectingServer,
EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls11
}, cancellationToken);
return sslStream;
}
catch
{
socket.Dispose();
throw;
}
}
};
client = new(handler);
client.DefaultRequestHeaders.Add("Accept", "application/octet-stream+protobuf");
}
public static RunCmdResponse CompleteStage(ulong userId, string input2) public static RunCmdResponse CompleteStage(ulong userId, string input2)
{ {
var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == userId); var user = JsonDb.Instance.Users.FirstOrDefault(x => x.ID == userId);
@@ -327,5 +371,72 @@ namespace EpinelPS.Utils
JsonDb.Save(); JsonDb.Save();
return RunCmdResponse.OK; return RunCmdResponse.OK;
} }
internal static async Task<RunCmdResponse> UpdateResources()
{
Logging.WriteLine("updating static data and resource info...", LogType.Info);
if (serverIp == null || staticDataUrl == null || resourcesUrl == null)
{
serverIp = await AssetDownloadUtil.GetIpAsync(serverUrl);
staticDataUrl = $"https://{serverIp}/v1/staticdatapack";
resourcesUrl = $"https://{serverIp}/v1/resourcehosts2";
}
if (serverIp == null)
return new RunCmdResponse() { error = "failed to get real server ip, check internet connection" };
// Get latest static data info from server
ResStaticDataPackInfo? staticData = await FetchProtobuf<ResStaticDataPackInfo>(staticDataUrl);
if (staticData == null)
{
Logging.WriteLine("failed to fetch static data", LogType.Error);
return new RunCmdResponse() { error = "failed to fetch static data"};
}
ResGetResourceHosts2? resources = await FetchProtobuf<ResGetResourceHosts2>(resourcesUrl);
if (resources == null)
{
Logging.WriteLine("failed to fetch resource data", LogType.Error);
return new RunCmdResponse() { error = "failed to fetch resource data" };
}
GameConfig.Root.ResourceBaseURL = resources.BaseUrl;
GameConfig.Root.StaticData.Salt1 = staticData.Salt1.ToBase64();
GameConfig.Root.StaticData.Salt2 = staticData.Salt2.ToBase64();
GameConfig.Root.StaticData.Version = staticData.Version;
GameConfig.Root.StaticData.Url = staticData.Url;
GameConfig.Save();
return RunCmdResponse.OK;
}
private static async Task<T?> FetchProtobuf<T>(string url) where T : IMessage, new()
{
ByteArrayContent staticDataContent = new([]);
client.DefaultRequestHeaders.Host = serverUrl;
staticDataContent.Headers.Add("Content-Type", "application/octet-stream+protobuf");
connectingServer = serverUrl;
HttpResponseMessage? staticDataHttpResponse = await client.PostAsync(url, staticDataContent);
if (staticDataHttpResponse == null)
{
Console.WriteLine($"failed to post {url}");
return default(T);
}
if (!staticDataHttpResponse.IsSuccessStatusCode)
{
Console.WriteLine($"POST {url} failed with {staticDataHttpResponse.StatusCode}");
return default(T);
}
byte[] staticDataHttpResponseBytes = await staticDataHttpResponse.Content.ReadAsByteArrayAsync();
// Parse response
T response = new();
response.MergeFrom(new CodedInputStream(staticDataHttpResponseBytes));
return response;
}
} }
} }

View File

@@ -26,7 +26,7 @@ namespace EpinelPS.Utils
if (CloudIp == null) if (CloudIp == null)
{ {
CloudIp = await GetCloudIpAsync(); CloudIp = await GetIpAsync("cloud.nikke-kr.com");
} }
var requestUri = new Uri("https://" + CloudIp + "/" + rawUrl); var requestUri = new Uri("https://" + CloudIp + "/" + rawUrl);
@@ -69,13 +69,13 @@ namespace EpinelPS.Utils
context.Response.StatusCode = 404; context.Response.StatusCode = 404;
} }
private static async Task<string> GetCloudIpAsync() public static async Task<string> GetIpAsync(string query)
{ {
var lookup = new LookupClient(); var lookup = new LookupClient();
var result = await lookup.QueryAsync("cloud.nikke-kr.com", QueryType.A); var result = await lookup.QueryAsync(query, QueryType.A);
var record = result.Answers.ARecords().FirstOrDefault(); var record = result.Answers.ARecords().FirstOrDefault();
var ip = record?.Address ?? throw new Exception("Failed to find IP address of cloud.nikke-kr.com, check your internet connection."); var ip = record?.Address ?? throw new Exception($"Failed to find IP address of {query}, check your internet connection.");
return ip.ToString(); return ip.ToString();
} }

View File

@@ -60,5 +60,13 @@ namespace EpinelPS.Utils
return _root; return _root;
} }
} }
internal static void Save()
{
if (Root != null)
{
File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "/gameconfig.json", JsonConvert.SerializeObject(Root, Formatting.Indented));
}
}
} }
} }

View File

@@ -15,6 +15,9 @@
</dd> </dd>
</dl> </dl>
<button id="reloadDB" type="button" class="btn btn-danger" data-i18n-title="config.server.updateResourcesHint" onclick="runSimpleCmd('updateServer')" data-i18n="config.server.updateResources">Update resources</button>
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary" data-i18n="common.save"></button> <button type="submit" class="btn btn-primary" data-i18n="common.save"></button>
</div> </div>

View File

@@ -1,17 +1,12 @@
{ {
// Asset Urls for game version 134.8.13 "StaticData": {
// Extracted from POST https://global-lobby.nikke-kr.com/v1/staticdatapack "Url": "https://cloud.nikke-kr.com/prdenv/134-cd14ebd38f/staticdata/data/qa-250612-06b/410113/StaticData.pack",
"StaticData": { "Version": "data/qa-250612-06b/410113",
"Url": "https://cloud.nikke-kr.com/prdenv/134-cd14ebd38f/staticdata/data/qa-250612-06b/410113/StaticData.pack", "Salt1": "4JgIjnC3wkpnk2B9nqvSd5Ovd0LxiKEd2u5UOKjPPVM=",
"Version": "data/qa-250612-06b/410113", "Salt2": "8505DAwa5p0ojKq91jhX5ktAOOL+JoSh1H6XhGrQfts="
"Salt1": "4JgIjnC3wkpnk2B9nqvSd5Ovd0LxiKEd2u5UOKjPPVM=", },
"Salt2": "8505DAwa5p0ojKq91jhX5ktAOOL+JoSh1H6XhGrQfts=" "ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/134-bac9cf1d39/{Platform}",
}, "GameMinVer": "100.0.1",
"GameMaxVer": "150.0.2",
// Extracted from POST https://global-lobby.nikke-kr.com"/v1/resourcehosts2 "TargetVersion": "134.8.13"
"ResourceBaseURL": "https://cloud.nikke-kr.com/prdenv/134-bac9cf1d39/{Platform}",
// Allow all versions
"GameMinVer": "100.0.1",
"GameMaxVer": "150.0.2"
} }

View File

@@ -204,7 +204,9 @@
"config": { "config": {
"server": { "server": {
"title": "Server Configuration", "title": "Server Configuration",
"logLevel": "Log Level:" "logLevel": "Log Level:",
"updateResources": "Update Resources",
"updateResourcesHint": "Downloads asset and static data info from official server."
}, },
"database": { "database": {
"title": "Database Configuration", "title": "Database Configuration",

View File

@@ -204,7 +204,9 @@
"config": { "config": {
"server": { "server": {
"title": "サーバー設定", "title": "サーバー設定",
"logLevel": "ログレベル:" "logLevel": "ログレベル:",
"updateResources": "リソースを更新する",
"updateResourcesHint": "公式サーバーからアセットと静的データ情報をダウンロードします。"
}, },
"database": { "database": {
"title": "データベース設定", "title": "データベース設定",

View File

@@ -204,7 +204,9 @@
"config": { "config": {
"server": { "server": {
"title": "서버 구성", "title": "서버 구성",
"logLevel": "로그 레벨:" "logLevel": "로그 레벨:",
"updateResources": "리소스 업데이트",
"updateResourcesHint": "공식 서버에서 자산 및 정적 데이터 정보를 다운로드합니다."
}, },
"database": { "database": {
"title": "데이터베이스 구성", "title": "데이터베이스 구성",

View File

@@ -204,7 +204,9 @@
"config": { "config": {
"server": { "server": {
"title": "服务器配置", "title": "服务器配置",
"logLevel": "日志级别:" "logLevel": "日志级别:",
"updateResources": "Update Resources",
"updateResourcesHint": "Downloads asset and static data info from official server."
}, },
"database": { "database": {
"title": "数据库配置", "title": "数据库配置",

View File

@@ -1,7 +1,7 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification // Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets. // for details on configuring this project to bundle and minify static web assets.
// NIKKE管理控制台API通用工具函数 // EpinelPS管理控制台API通用工具函数
function runCmd(cmdName, cb, p1, p2) function runCmd(cmdName, cb, p1, p2)
{ {
@@ -28,9 +28,9 @@ function runSimpleCmd(cmdName, p1, p2)
{ {
runCmd(cmdName, function(json){ runCmd(cmdName, function(json){
if (json.ok) if (json.ok)
alert("操作已完成"); alert("Operation completed");
else else
alert("错误: " + json.error); alert("Error: " + json.error);
}, p1, p2); }, p1, p2);
} }