Compare commits

...
59 Commits
Author SHA1 Message Date
asesidaa c21047024c Merge remote-tracking branch 'origin/master' 2022-08-01 21:52:05 +08:00
asesidaa 252f1da79f Update README 2022-08-01 21:51:50 +08:00
asesidaa 77fccdbac5 Update README.md 2022-08-01 21:42:00 +08:00
asesidaa 8711be388e Merge remote-tracking branch 'origin/master' 2022-08-01 21:40:45 +08:00
asesidaa 316fff969e Add relay server 2022-08-01 21:39:24 +08:00
asesidaa c185497502 Update online matching logic 2022-08-01 21:39:14 +08:00
asesidaa 1ab148a50a Clear online matching entries after one match 2022-08-01 16:29:18 +08:00
asesidaa 1de4d8c406 Add experimental online matching 2022-08-01 16:27:57 +08:00
asesidaa 16d8c25ea0 Fix empty response 2022-08-01 16:27:37 +08:00
DeepSource Bot 6b0095508d Add .deepsource.toml 2022-07-23 16:34:58 +00:00
asesidaa e5d2724f8c Disable download by default 2022-07-18 15:13:25 +08:00
asesidaa 0d1c3e60cd Update README.md 2022-07-16 16:11:29 +08:00
asesidaa a5fea98d59 Add online matching testing code
Add event download code
2022-07-16 16:03:56 +08:00
asesidaa 80b69400d5 Add back compression 2022-07-12 00:48:43 +08:00
asesidaa e5a22ece90 Use locally compiled new version of EmbedIO
Remove backports
Skip trimming of FilterOperator and FilterDefinition
2022-07-09 21:26:39 +08:00
asesidaa 01710f1857 Update wwwroot content
Use OnMappingFailed to deal with client routing
Disable content caching for update
2022-07-08 21:22:29 +08:00
jiych1 14261c8e29 Change avatar/navigator/title select method
Add the function to change player name
2022-07-07 21:46:17 +08:00
jiych1 4cd290aedc Add last play time support
Support setting avatar, navigator and titles on web interface (unoptimized)
2022-07-06 00:52:38 +08:00
jiych1 95d9d0be5f Add card register, restructure CardServiceController.cs 2022-07-02 23:00:40 +08:00
jiych1 c2258a5fd9 Add fake update 2022-07-02 21:00:49 +08:00
Yuchen Ji ccdb5d3754 Route 404 to index.html to fix client routing
Add 404 update check
Experiment with card register
2022-06-28 03:30:44 +08:00
Yuchen Ji 01a2f04289 Add dark mode, update appbar 2022-06-22 22:23:11 +08:00
asesidaa df47afcf32 Update README.md 2022-06-21 16:56:18 +08:00
asesidaa 82f3ab6d23 Update README.md 2022-06-21 16:36:15 +08:00
Yuchen Ji f3edade533 Update README.md 2022-06-21 16:35:33 +08:00
Yuchen Ji 28c5fdc461 Remove test pages, update index page 2022-06-21 15:45:18 +08:00
Yuchen Ji c8b8a2816e Finish prototype for web interface
Only unlock unlockable songs at first write
2022-06-21 03:40:42 +08:00
Yuchen Ji d3265dbc02 Update web interface 2022-06-21 01:49:47 +08:00
Yuchen Ji 4871c0bf6f Update to use server data instead of mock data 2022-06-20 02:10:34 +08:00
Yuchen Ji 003f655585 Update server for better query performance 2022-06-20 02:10:04 +08:00
Yuchen Ji 7afd41664c Try add stamp count for 3EX and before 2022-06-18 22:11:57 +08:00
Yuchen Ji 04be44cff1 Update admin pages (not complete) 2022-06-18 22:11:14 +08:00
Yuchen Ji 39c5920dc5 Update card_detail doc 2022-06-18 22:10:38 +08:00
Yuchen Ji b280438819 Add card_bdata doc 2022-06-18 22:10:21 +08:00
Yuchen Ji c142516b00 Add 471 support.
Updated config
Updated readme
Add default values, remove null
2022-06-11 20:22:28 +08:00
Yuchen Ji c7deb8f970 Unlock all songs in card_detail table during first time 2022-06-10 00:49:56 +08:00
Yuchen Ji e7a71ea07d Update music and music_aou xml fields 2022-06-10 00:49:18 +08:00
Yuchen Ji 2f185890e5 Add replay server for test 2022-06-10 00:48:55 +08:00
Yuchen Ji 47cdbb0315 Add card_detail doc 2022-06-10 00:48:13 +08:00
Yuchen Ji 4730846674 Add random play_num_rank for testing 2022-06-10 00:48:03 +08:00
asesidaa 9fad46fb08 Update README.md 2022-04-26 22:41:39 +08:00
asesidaa c3df11958d Update README.md 2022-04-26 22:36:15 +08:00
Yuchen Ji 09716e4d4b Merge remote-tracking branch 'origin/master' 2022-04-26 22:35:26 +08:00
Yuchen Ji aa0b7787a8 Update README 2022-04-26 22:35:18 +08:00
Yuchen Ji 5c6cc5228f Update to support event, change config to use json. 2022-04-26 22:35:09 +08:00
Yuchen Ji 7397102dc0 Add user detail display 2022-04-25 02:07:11 +08:00
Yuchen Ji d89065e58a Change project using mud template, add test user pages 2022-04-24 02:29:55 +08:00
asesidaa 8960040008 Update README.md 2022-04-13 11:27:24 +08:00
Yuchen Ji b486b5b36a Start front end develop 2022-04-12 03:08:03 +08:00
Yuchen Ji 01ba2ea90b Update README 2022-04-09 02:18:49 +08:00
Yuchen Ji d04a213e87 Update to support 4.65 and make it default 2022-04-09 02:17:14 +08:00
Yuchen Ji ebde41d2e7 Merge remote-tracking branch 'origin/master' 2022-04-08 03:41:57 +08:00
Yuchen Ji 219a90c640 Update to use fixed UTF8 with no bom 2022-04-08 03:41:51 +08:00
asesidaa 7cfc3d7e11 Update README.md for instructions on XP 2022-04-07 20:49:50 +08:00
Yuchen Ji 1d211d5f15 Save cert to disk 2022-04-07 00:05:31 +08:00
Yuchen Ji 3129170217 Disable gid check 2022-04-06 21:48:09 +08:00
Yuchen Ji 77f4cfcfbf Update README 2022-04-06 18:55:27 +08:00
Yuchen Ji b624ec53d8 Update README 2022-04-06 18:51:00 +08:00
Yuchen Ji 3a59c639a5 Add support for host redirect and set local network ip addrress 2022-04-06 18:42:49 +08:00
104 changed files with 15357 additions and 411 deletions
+5
View File
@@ -0,0 +1,5 @@
version = 1
[[analyzers]]
name = "csharp"
enabled = true
+2 -1
View File
@@ -439,4 +439,5 @@ FodyWeavers.xsd
/db/card.db3
.idea
.idea
identifier.sqlite
+18
View File
@@ -5,6 +5,12 @@ VisualStudioVersion = 16.0.32126.315
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GC-local-server-rewrite", "GC-local-server-rewrite\GC-local-server-rewrite.csproj", "{D5FFF2E8-6591-4967-8883-A28F453F0524}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedProject", "SharedProject\SharedProject.csproj", "{C01D0576-9D4E-4535-8055-C4905433A1EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MudAdmin", "MudAdmin\MudAdmin.csproj", "{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCRelayServer", "GCRelayServer\GCRelayServer.csproj", "{268178DF-6345-4D9E-A389-9E809CBF39C9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +21,18 @@ Global
{D5FFF2E8-6591-4967-8883-A28F453F0524}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D5FFF2E8-6591-4967-8883-A28F453F0524}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5FFF2E8-6591-4967-8883-A28F453F0524}.Release|Any CPU.Build.0 = Release|Any CPU
{C01D0576-9D4E-4535-8055-C4905433A1EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C01D0576-9D4E-4535-8055-C4905433A1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C01D0576-9D4E-4535-8055-C4905433A1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C01D0576-9D4E-4535-8055-C4905433A1EB}.Release|Any CPU.Build.0 = Release|Any CPU
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC8E30E9-F81E-4E28-A4D2-F4576C77FFBE}.Release|Any CPU.Build.0 = Release|Any CPU
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{268178DF-6345-4D9E-A389-9E809CBF39C9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+6
View File
@@ -0,0 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Adlibs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Keynum/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Touhou/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unlockable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Vocaloid/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+3 -1
View File
@@ -1,6 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=ADDR/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BDATA/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcol/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Incom/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pcol/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Respone/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Respone/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tenpo/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -11,22 +11,31 @@
<ItemGroup>
<PackageReference Include="CertificateManager" Version="1.0.8" />
<PackageReference Include="ChoETL" Version="1.2.1.44" />
<PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="sqlite-net2" Version="2.0.7" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
<PackageReference Include="ChoETL" Version="1.2.1.47" />
<PackageReference Include="ConcurrentHashSet" Version="1.3.0" />
<PackageReference Include="Config.Net" Version="4.19.0" />
<PackageReference Include="Config.Net.Json" Version="4.19.0" />
<PackageReference Include="EmbedIO" Version="3.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0-preview.5.22301.12" />
<PackageReference Include="sqlite-net2" Version="2.1.0-preB" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0" />
<PackageReference Include="System.Security.Cryptography.X509Certificates" Version="4.3.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="log" />
<Folder Include="event\" />
<None Remove="db\music.db3" />
<None Remove="db\music4MAX465.db3" />
<Content Include="db\music.db3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<None Remove="static\favicon.ico" />
<Content Include="db\music4MAX465.db3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Include="static\favicon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
@@ -42,15 +51,23 @@
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<None Remove="GC-local-server-rewrite.exe.config.xml" />
<None Include="GC-local-server-rewrite.exe.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Remove="db\music4MAX.db3" />
<Content Include="db\music4MAX.db3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Update="db\music471.db3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharedProject\SharedProject.csproj" />
</ItemGroup>
</Project>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="AvatarCount" value="294"/>
<add key="NavigatorCount" value="71"/>
<add key="ItemCount" value="21"/>
<add key="SkinCount" value="21"/>
<add key="SeCount" value="25"/>
<add key="TitleCount" value="4942"/>
<add key="MusicDBName" value="music.db3"/>
</appSettings>
</configuration>
+25 -24
View File
@@ -13,10 +13,11 @@ internal class Program
Batteries_V2.Init();
InitializeLogging();
LogConfigValues();
var urlPrefixes = args.Length > 0 ? args : new[] { "http://*:80", "https://*:443" };
var urlPrefixes =
args.Length > 0 ? new List<string>(args) : new List<string> { "http://+:80", "https://+:443" };
using (var cts = new CancellationTokenSource())
{
@@ -31,7 +32,7 @@ internal class Program
Terminal.Flush();
Console.WriteLine("Press any key to exit.");
WaitForKeypress();
Console.ReadKey(true);
}
private static void InitializeLogging()
@@ -50,7 +51,7 @@ internal class Program
/// </summary>
/// <param name="urlPrefixes"></param>
/// <param name="cancellationToken"></param>
private static async Task RunWebServerAsync(string[] urlPrefixes, CancellationToken cancellationToken)
private static async Task RunWebServerAsync(IEnumerable<string> urlPrefixes, CancellationToken cancellationToken)
{
using var server = Server.CreateWebServer(urlPrefixes);
await server.RunAsync(cancellationToken).ConfigureAwait(false);
@@ -66,7 +67,7 @@ internal class Program
// Be sure to run in parallel.
await Task.Yield();
"Press any key to stop the web server.".Info(nameof(Program));
"Press x to stop the web server.".Info(nameof(Program));
WaitForKeypress();
"Stopping...".Info(nameof(Program));
cancel();
@@ -77,30 +78,30 @@ internal class Program
/// </summary>
private static void WaitForKeypress()
{
while (Console.KeyAvailable)
{
Console.ReadKey(true);
}
ConsoleKeyInfo consoleKeyInfo;
Console.ReadKey(true);
do
{
while (Console.KeyAvailable == false)
{
Thread.Sleep(250);
}
consoleKeyInfo = Console.ReadKey(true);
} while (consoleKeyInfo.Key != ConsoleKey.X);
}
private static void LogConfigValues()
{
var configs = $"Config values: {nameof(Configs.SE_COUNT)} : {Configs.SE_COUNT}\n" +
$"{nameof(Configs.ITEM_COUNT)} : {Configs.ITEM_COUNT}\n" +
$"{nameof(Configs.SKIN_COUNT)} : {Configs.SKIN_COUNT}\n" +
$"{nameof(Configs.TITLE_COUNT)} : {Configs.TITLE_COUNT}\n" +
$"{nameof(Configs.AVATAR_COUNT)} : {Configs.AVATAR_COUNT}\n" +
$"{nameof(Configs.NAVIGATOR_COUNT)} : {Configs.NAVIGATOR_COUNT}\n" +
$"{nameof(Configs.MUSIC_DB_NAME)} : {Configs.MUSIC_DB_NAME}\n";
configs.Info();
var paths = $"Paths: {nameof(PathHelper.HtmlRootPath)}: {PathHelper.HtmlRootPath}\n" +
$"{nameof(PathHelper.LogRootPath)}: {PathHelper.LogRootPath}\n" +
$"{nameof(PathHelper.DataBaseRootPath)}: {PathHelper.DataBaseRootPath}\n" +
$"{nameof(PathHelper.ConfigFilePath)}: {PathHelper.ConfigFilePath}\n";
paths.Info();
var paths = $"Paths: {nameof(PathHelper.HtmlRootPath)}: {PathHelper.HtmlRootPath}\n" +
$"{nameof(PathHelper.LogRootPath)}: {PathHelper.LogRootPath}\n" +
$"{nameof(PathHelper.DataBaseRootPath)}: {PathHelper.DataBaseRootPath}\n" +
$"{nameof(PathHelper.ConfigFilePath)}: {PathHelper.ConfigFilePath}\n" +
$"{nameof(PathHelper.CertRootPath)}: {PathHelper.CertRootPath}\n";
paths.Info();
var configs = "Config values: \n" +
$"{Configs.SETTINGS.Stringify()}";
configs.Info();
}
}
@@ -1,74 +0,0 @@
using EmbedIO;
namespace GCLocalServerRewrite.backports;
/// <summary>
/// Provides custom response serializer callbacks.
/// </summary>
public static class CustomResponseSerializer
{
private static readonly ResponseSerializerCallback CHUNKED_ENCODING_BASE_SERIALIZER = GetBaseSerializer(false);
private static readonly ResponseSerializerCallback BUFFERING_BASE_SERIALIZER = GetBaseSerializer(true);
/// <summary>
/// Sends data in a HTTP response without serialization.
/// </summary>
/// <param name="bufferResponse">
/// <see langword="true" /> to write the response body to a memory buffer first,
/// then send it all together with a <c>Content-Length</c> header; <see langword="false" /> to use chunked
/// transfer encoding.
/// </param>
/// <returns>A <see cref="ResponseSerializerCallback" /> that can be used to serialize data to a HTTP response.</returns>
/// <remarks>
/// <para>
/// <see cref="string" />s and one-dimensional arrays of <see cref="byte" />s
/// are sent to the client unchanged; every other type is converted to a string.
/// </para>
/// <para>
/// The <see cref="IHttpResponse.ContentType">ContentType</see> set on the response is used to negotiate
/// a compression method, according to request headers.
/// </para>
/// <para>
/// Strings (and other types converted to strings) are sent with the encoding specified by
/// <see cref="IHttpResponse.ContentEncoding" />.
/// </para>
/// </remarks>
public static ResponseSerializerCallback None(bool bufferResponse)
{
return bufferResponse ? BUFFERING_BASE_SERIALIZER : CHUNKED_ENCODING_BASE_SERIALIZER;
}
private static ResponseSerializerCallback GetBaseSerializer(bool bufferResponse)
{
return async (context, data) =>
{
if (data is null)
{
return;
}
var isBinaryResponse = data is byte[];
if (!context.TryDetermineCompression(context.Response.ContentType, out var preferCompression))
{
preferCompression = true;
}
preferCompression = false;
if (isBinaryResponse)
{
var responseBytes = (byte[])data;
using var stream = context.OpenResponseStream(bufferResponse, preferCompression);
await stream.WriteAsync(responseBytes).ConfigureAwait(false);
}
else
{
var responseString = data is string stringData ? stringData : data.ToString() ?? string.Empty;
await using var text = context.OpenResponseText(context.Response.ContentEncoding, bufferResponse,
preferCompression);
await text.WriteAsync(responseString).ConfigureAwait(false);
}
};
}
}
@@ -15,8 +15,9 @@ public static class CertificateHelper
X509KeyUsageFlags.KeyEncipherment |
X509KeyUsageFlags.DigitalSignature;
private const X509KeyStorageFlags X509_KEY_STORAGE_FLAGS_MACHINE = X509KeyStorageFlags.PersistKeySet
| X509KeyStorageFlags.MachineKeySet;
private const X509KeyStorageFlags X509_KEY_STORAGE_FLAGS_MACHINE = X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.Exportable;
private const X509KeyUsageFlags CERT_X509_KEY_USAGE_FLAGS = X509KeyUsageFlags.DataEncipherment |
X509KeyUsageFlags.KeyEncipherment |
@@ -50,10 +51,8 @@ public static class CertificateHelper
private static readonly SubjectAlternativeName SUBJECT_ALTERNATIVE_NAME = new()
{
DnsName = new List<string>
{
"localhost"
}
DnsName = Configs.DOMAINS,
IpAddress = System.Net.IPAddress.Parse(Configs.SETTINGS.ServerIp)
};
private static readonly ValidityPeriod VALIDITY_PERIOD = new()
@@ -77,6 +76,7 @@ public static class CertificateHelper
{
return existingCert;
}
"Existing CN not found! Removing old certificates and genrate new ones...".Info();
}
RemovePreviousCert(StoreName.My, StoreLocation.LocalMachine);
@@ -129,10 +129,16 @@ public static class CertificateHelper
var certWithPrivateKey = new X509Certificate2(certPfxBytes, (string)null!,
X509_KEY_STORAGE_FLAGS_MACHINE);
AddCertToStore(rootCaWithPrivateKey, StoreName.My, StoreLocation.LocalMachine);
AddCertToStore(rootCaWithPrivateKey, StoreName.Root, StoreLocation.LocalMachine);
AddCertToStore(certWithPrivateKey, StoreName.My, StoreLocation.LocalMachine);
Directory.CreateDirectory(PathHelper.CertRootPath);
File.WriteAllBytes(Path.Combine(PathHelper.CertRootPath, "root.pfx"), rootCaWithPrivateKey.Export(X509ContentType.Pfx));
File.WriteAllBytes(Path.Combine(PathHelper.CertRootPath, "cert.pfx"), certWithPrivateKey.Export(X509ContentType.Pfx));
return certWithPrivateKey;
}
+51 -59
View File
@@ -1,6 +1,4 @@
using System.Configuration;
using Swan;
using Swan.Logging;
using Config.Net;
namespace GCLocalServerRewrite.common;
@@ -16,11 +14,19 @@ public static class Configs
public const string LOG_FOLDER = "log";
public const string CERT_FOLDER = "certs";
public const string LOG_BASE_NAME = "log";
public const string CONFIG_FILE_NAME = "config.json";
public const string STATIC_FOLDER = "static";
public const string CONFIG_FILE_NAME = "App.config";
public const string WWWROOT = "wwwroot";
public const string API_BASE_ROUTE = "/api";
public const string OPTION_SERVICE_BASE_ROUTE = "/service/option";
public const string CARD_SERVICE_BASE_ROUTE = "/service/card";
@@ -30,6 +36,8 @@ public static class Configs
public const string INCOM_SERVICE_BASE_ROUTE = "/service/incom";
public const string UPDATE_SERVICE_BASE_ROUTE = "/update/cgi";
public const string RANK_BASE_ROUTE = "/ranking";
public const string ALIVE_BASE_ROUTE = "/alive";
@@ -38,8 +46,6 @@ public static class Configs
public const string STATIC_BASE_ROUTE = "/static";
public const string CARD_DB_NAME = "card.db3";
public const string ROOT_XPATH = "/root";
public const string DATA_XPATH = $"{ROOT_XPATH}/data";
@@ -68,7 +74,7 @@ public static class Configs
public const string MUSIC_AOU = "music_aou";
public const string MUSIC_AOU_XPATH = $"{ROOT_XPATH}/{MUSIC_AOU}";
public const string MUSIC_AOU_XPATH = $"{ROOT_XPATH}/{MUSIC_AOU}/record";
public const string ITEM = "item";
@@ -119,64 +125,50 @@ public static class Configs
public const string SESSION_XPATH = $"{ROOT_XPATH}/session";
public const string RANK_STATUS_XPATH = $"{ROOT_XPATH}/ranking_status";
public const string ONLINE_MATCHING_XPATH = $"{ROOT_XPATH}/online_matching/record";
public const string ONLINE_BATTLE_RESULT_XPATH = $"{ROOT_XPATH}/online_battle_result";
public const int FIRST_CONFIG_PCOL1 = 0;
public const int SECOND_CONFIG_PCOL1 = 1;
public const int CONFIG_PCOL2 = 0;
public const int CONFIG_PCOL3 = 0;
public const int GC4_EX_GID = 303801;
public static readonly int AVATAR_COUNT;
public const int FAVORITE_PCOL1 = 10;
public static readonly int NAVIGATOR_COUNT;
public const int COUNT_PCOL1 = 20;
public static readonly int ITEM_COUNT;
public const int SCORE_PCOL1 = 21;
public static readonly int TITLE_COUNT;
public static readonly int SKIN_COUNT;
public static readonly int SE_COUNT;
public static readonly string MUSIC_DB_NAME;
private static readonly Configuration CONFIG = ConfigurationManager.OpenExeConfiguration(PathHelper.ConfigFilePath);
private const string AVATAR_COUNT_CONFIG_NAME = "AvatarCount";
private const string NAVIGATOR_COUNT_CONFIG_NAME = "NavigatorCount";
private const string ITEM_COUNT_CONFIG_NAME = "ItemCount";
private const string TITLE_COUNT_CONFIG_NAME = "TitleCount";
private const string SKIN_COUNT_CONFIG_NAME = "SkinCount";
private const string SE_COUNT_CONFIG_NAME = "SeCount";
private const string MUSIC_DB_CONFIG_NAME = "MusicDBName";
private const int AVATAR_DEFAULT_COUNT = 294;
private const int NAVIGATOR_DEFAULT_COUNT = 71;
private const int ITEM_DEFAULT_COUNT = 21;
private const int TITLE_DEFAULT_COUNT = 4942;
private const int SKIN_DEFAULT_COUNT = 21;
private const int SE_DEFAULT_COUNT = 25;
private const string MUSIC_DB_DEFAULT_NAME = "music.db3";
static Configs()
public static readonly List<string> DOMAINS = new()
{
void GetIntValueFromConfig(string configFieldName, int defaultValue, out int field )
{
var success = int.TryParse(CONFIG.AppSettings.Settings[configFieldName].Value, out var count);
"localhost",
"cert.nesys.jp",
"nesys.taito.co.jp",
"fjm170920zero.nesica.net"
};
field = success ? count:defaultValue;
}
GetIntValueFromConfig(AVATAR_COUNT_CONFIG_NAME, AVATAR_DEFAULT_COUNT, out AVATAR_COUNT);
GetIntValueFromConfig(NAVIGATOR_COUNT_CONFIG_NAME, NAVIGATOR_DEFAULT_COUNT, out NAVIGATOR_COUNT);
GetIntValueFromConfig(ITEM_COUNT_CONFIG_NAME, ITEM_DEFAULT_COUNT, out ITEM_COUNT);
GetIntValueFromConfig(TITLE_COUNT_CONFIG_NAME, TITLE_DEFAULT_COUNT, out TITLE_COUNT);
GetIntValueFromConfig(SKIN_COUNT_CONFIG_NAME, SKIN_DEFAULT_COUNT, out SKIN_COUNT);
GetIntValueFromConfig(SE_COUNT_CONFIG_NAME, SE_DEFAULT_COUNT, out SE_COUNT);
public static readonly IAppSettings SETTINGS =
new ConfigurationBuilder<IAppSettings>().UseJsonFile(PathHelper.ConfigFilePath).Build();
MUSIC_DB_NAME = CONFIG.AppSettings.Settings[MUSIC_DB_CONFIG_NAME].Value;
public const int DEFAULT_AVATAR_COUNT = 323;
public const int DEFAULT_NAVIGATOR_COUNT = 94;
public const int DEFAULT_ITEM_COUNT = 21;
public const int DEFAULT_TITLE_COUNT = 5273;
public const int DEFAULT_SKIN_COUNT = 21;
public const int DEFAULT_SE_COUNT = 26;
public const string DEFAULT_CARD_DB_NAME = "card.db3";
public const string DEFAULT_MUSIC_DB_NAME = "music4MAX465.db3";
public const string DEFAULT_SERVER_IP = "127.0.0.1";
public const string DEFAULT_RELAY_SERVER = "127.0.0.1";
public const int DEFAULT_RELAY_PORT = 54321;
public const string DEFAULT_EVENT_FOLDER = "event";
if (MUSIC_DB_NAME == string.Empty)
{
MUSIC_DB_NAME = MUSIC_DB_DEFAULT_NAME;
}
}
public static readonly IReadOnlyList<int> DEFAULT_UNLOCKABLE_SONGS = new[]
{
11, 13, 149, 273, 291, 320, 321, 371, 378, 384, 464, 471, 474, 475, 492, 494, 498, 520,
548, 551, 558, 561, 565, 570, 577, 583, 612, 615, 622, 632, 659, 666, 668, 670, 672, 676,
680, 682, 685, 686, 697, 700, 701, 711, 720, 749, 875, 876, 877
};
}
@@ -0,0 +1,50 @@
using Config.Net;
namespace GCLocalServerRewrite.common;
public interface IAppSettings
{
[Option(DefaultValue = Configs.DEFAULT_AVATAR_COUNT)]
int AvatarCount { get; }
[Option(DefaultValue = Configs.DEFAULT_NAVIGATOR_COUNT)]
int NavigatorCount { get; }
[Option(DefaultValue = Configs.DEFAULT_ITEM_COUNT)]
int ItemCount { get; }
[Option(DefaultValue = Configs.DEFAULT_TITLE_COUNT)]
int TitleCount { get; }
[Option(DefaultValue = Configs.DEFAULT_SKIN_COUNT)]
int SkinCount { get; }
[Option(DefaultValue = Configs.DEFAULT_SE_COUNT)]
int SeCount { get; }
[Option(DefaultValue = Configs.DEFAULT_MUSIC_DB_NAME)]
string MusicDbName { get; }
[Option(DefaultValue = Configs.DEFAULT_CARD_DB_NAME)]
string CardDbName { get; }
[Option(DefaultValue = Configs.DEFAULT_SERVER_IP)]
string ServerIp { get; }
[Option(DefaultValue = Configs.DEFAULT_EVENT_FOLDER)]
string EventFolder { get; }
[Option(DefaultValue = Configs.DEFAULT_RELAY_SERVER)]
string RelayServer { get; }
[Option(DefaultValue = Configs.DEFAULT_RELAY_PORT)]
int RelayPort { get; }
[Option(DefaultValue = false)]
bool DownloadEvents { get; }
[Option(DefaultValue = null)]
IEnumerable<int>? UnlockableSongIds { get; }
IEnumerable<IOptionServiceResponse> ResponseData { get; }
}
@@ -0,0 +1,14 @@
namespace GCLocalServerRewrite.common;
public interface IOptionServiceResponse
{
string FileName { get; }
long NotBeforeUnixTime { get; }
long NotAfterUnixTime { get; }
string Md5 { get; }
int Index { get; }
}
+4 -2
View File
@@ -8,7 +8,7 @@ public static class PathHelper
/// <summary>
/// Gets the local path of html/static files.
/// </summary>
public static string HtmlRootPath => Path.Combine(BasePath, Configs.STATIC_FOLDER);
public static string HtmlRootPath => Path.Combine(BasePath, Configs.STATIC_FOLDER, Configs.WWWROOT);
/// <summary>
/// Root path for database, when debug, it's under source root, when release, it's the exe dir
@@ -17,7 +17,9 @@ public static class PathHelper
public static string LogRootPath => Path.Combine(BasePath, Configs.LOG_FOLDER);
public static string ConfigFilePath => Environment.ProcessPath ?? string.Empty;
public static string CertRootPath => Path.Combine(BasePath, Configs.CERT_FOLDER);
public static string ConfigFilePath => Path.Combine(BasePath, Configs.CONFIG_FILE_NAME);
private static string BasePath
{
+124
View File
@@ -0,0 +1,124 @@
{
"AvatarCount": 356,
"NavigatorCount": 118,
"ItemCount": 21,
"SkinCount": 21,
"SeCount": 26,
"TitleCount": 5530,
"CardDbName": "card.db3",
"MusicDbName": "music471omni.db3",
"ServerIp": "127.0.0.1",
"EventFolder": "event",
"DownloadEvents": false,
"RelayServer": "127.0.0.1",
"RelayPort": 54321,
"UnlockableSongIds": [
11,
13,
149,
273,
291,
320,
321,
371,
378,
384,
464,
471,
474,
475,
492,
494,
498,
520,
548,
551,
558,
561,
565,
570,
577,
583,
612,
615,
622,
632,
659,
666,
668,
670,
672,
676,
680,
682,
685,
686,
697,
700,
701,
711,
720,
749,
875,
876,
877
],
"ResponseData": [
{
"FileName": "/event_103_20201125.evt",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "9ef6c4d5ca381583a2d99b626ce06b5e",
"Index": 0
},
{
"FileName": "/event_20201125_reg.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "8e3fe25bf50dcbed13dbb54cc18b1efa",
"Index": 1
},
{
"FileName": "/event_20201125_sgreg.png",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "e0abb0503fe0c530d8a68e36994264c6",
"Index": 2
},
{
"FileName": "/news_big_20201125_0.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "4a0f66431f6449279dc046149d1dd882",
"Index": 0
},
{
"FileName": "/news_big_20201125_2.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "8e3fe25bf50dcbed13dbb54cc18b1efa",
"Index": 2
},
{
"FileName": "/news_small_20201125_1.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "e20135bcd41c98875aec2b52eb9fcd06",
"Index": 1
},
{
"FileName": "/telop_20201125.txt",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "ee228de44d6656a9ec0bb7f1a0ca64e1",
"Index": 0
},
{
"FileName": "/event_unlock_20201125.cmp",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "534a253e3de8360c2beff49a5f120105",
"Index": 8
}
]
}
@@ -14,7 +14,7 @@ public class AliveController : WebApiController
public string Check()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Html;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "REMOTE ADDRESS: 127.0.0.1\n" +
@@ -28,7 +28,7 @@ public class AliveController : WebApiController
{
HttpContext.Response.SetEmptyResponse((int)HttpStatusCode.OK);
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
}
}
@@ -0,0 +1,340 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using GCLocalServerRewrite.common;
using GCLocalServerRewrite.models;
using SharedProject.common;
using SharedProject.enums;
using SharedProject.models;
using SQLite.Net2;
using Swan.Logging;
namespace GCLocalServerRewrite.controllers;
public class ApiController : WebApiController
{
private readonly SQLiteConnection cardSqLiteConnection;
private readonly Dictionary<int, Music> musics;
private readonly Dictionary<int, MusicExtra> musicExtras;
public ApiController()
{
cardSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.CardDbName);
var musicSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.MusicDbName);
musics = musicSqLiteConnection.Table<Music>().ToDictionary(music => music.MusicId);
musicExtras = musicSqLiteConnection.Table<MusicExtra>().ToDictionary(music => music.MusicId);
}
[Route(HttpVerbs.Get, "/Users")]
// ReSharper disable once UnusedMember.Global
public List<User> GetUsers()
{
var result = cardSqLiteConnection.Table<Card>().ToList().ConvertAll(card => new User
{
CardId = card.CardId,
PlayerName = card.PlayerName
});
return result;
}
[Route(HttpVerbs.Post, "/Users/SetPlayerName")]
public bool SetPlayerName([JsonData] User data)
{
var existing = cardSqLiteConnection.Table<Card>().Where(card => card.CardId == data.CardId);
if (!existing.Any())
{
$"Trying to update non existing user's name! Card id {data.CardId}".Warn();
return false;
}
var user = existing.First();
user.PlayerName = data.PlayerName;
return cardSqLiteConnection.Update(user) == 1;
}
[Route(HttpVerbs.Post, "/UserDetail/SetMusicFavorite")]
// ReSharper disable once UnusedMember.Global
public bool SetFavorite([JsonData] MusicFavoriteData data)
{
var existing = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == data.CardId
&& detail.Pcol1 == Configs.FAVORITE_PCOL1
&& detail.Pcol2 == data.MusicId);
if (!existing.Any())
{
$"Trying to update non existing song's favorite! Card id {data.CardId}, music id {data.MusicId}".Warn();
return false;
}
var cardDetail = existing.First();
cardDetail.Fcol1 = data.IsFavorite ? 1 : 0;
var result = cardSqLiteConnection.Update(cardDetail);
return result == 1;
}
[Route(HttpVerbs.Post, "/UserDetail/SetPlayOption")]
// ReSharper disable once UnusedMember.Global
public bool SetPlayOption([JsonData] PlayOption data)
{
var firstConfig = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == data.CardId
&& detail.Pcol1 == Configs.FIRST_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3);
var secondConfig = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == data.CardId
&& detail.Pcol1 == Configs.SECOND_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3);
if (!firstConfig.Any() || !secondConfig.Any())
{
$"Trying to update non existing card's config! Card id {data.CardId}".Warn();
return false;
}
var firstDetail = firstConfig.First();
firstDetail.ScoreUi1 = (long)data.FastSlowIndicator;
firstDetail.ScoreUi2 = (long)data.FeverTrance;
firstDetail.ScoreI1 = data.AvatarId;
firstDetail.Fcol2 = (int)data.TitleId;
var secondDetail = secondConfig.First();
secondDetail.ScoreI1 = data.NavigatorId;
var firstResult = cardSqLiteConnection.Update(firstDetail);
var secondResult = cardSqLiteConnection.Update(secondDetail);
return firstResult == 1 && secondResult == 1;
}
[Route(HttpVerbs.Get, "/UserDetail/{cardId}")]
// ReSharper disable once UnusedMember.Global
public UserDetail? GetUserDetail(long cardId)
{
var cardResult = cardSqLiteConnection.Table<Card>().Where(card => card.CardId == cardId);
if (!cardResult.Any())
{
$"Getting detail for non exisisting card! Card id is {cardId}".Warn();
return null;
}
var card = cardResult.First();
return ToUserDetail(card);
}
private UserDetail? ToUserDetail(Card card)
{
if (!cardSqLiteConnection.Table<CardDetail>().Select(detail => detail.CardId == card.CardId).Any())
{
return null;
}
var userDetail = new UserDetail
{
CardId = card.CardId,
PlayerName = card.PlayerName
};
var songPlayDataDict = new Dictionary<int, SongPlayData>();
ProcessCardDetail(userDetail, songPlayDataDict);
userDetail.SongPlayDataList = songPlayDataDict.Values.ToList();
userDetail.TotalSongCount = musics.Count;
userDetail.TotalStageCount = userDetail.TotalSongCount * 3 + musicExtras.Count;
userDetail.AverageScore = (int)(userDetail.TotalScore / userDetail.PlayedStageCount);
userDetail.PlayedSongCount = songPlayDataDict.Count;
return userDetail;
}
private void ProcessCardDetail(UserDetail userDetail, IDictionary<int, SongPlayData> songPlayDataDict)
{
var firstOption = cardSqLiteConnection.Table<CardDetail>()
.FirstOrDefault(detail => detail.CardId == userDetail.CardId
&& detail.Pcol1 == Configs.FIRST_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3
, new CardDetail
{
CardId = userDetail.CardId
});
var secondOption = cardSqLiteConnection.Table<CardDetail>()
.FirstOrDefault(detail => detail.CardId == userDetail.CardId
&& detail.Pcol1 == Configs.SECOND_CONFIG_PCOL1
&& detail.Pcol2 == Configs.CONFIG_PCOL2
&& detail.Pcol3 == Configs.CONFIG_PCOL3
, new CardDetail
{
CardId = userDetail.CardId
});
SetOptions(firstOption, secondOption, userDetail);
var songCounts = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == userDetail.CardId && detail.Pcol1 == Configs.COUNT_PCOL1);
foreach (var detail in songCounts)
{
SetCounts(detail, songPlayDataDict, userDetail);
}
var songScores = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == userDetail.CardId && detail.Pcol1 == Configs.SCORE_PCOL1);
foreach (var detail in songScores)
{
SetDetails(detail, songPlayDataDict, userDetail);
}
var favorites = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == userDetail.CardId && detail.Pcol1 == Configs.FAVORITE_PCOL1)
.ToDictionary(detail => detail.Pcol2);
foreach (var (musicId, songPlayData) in songPlayDataDict)
{
songPlayData.IsFavorite = favorites[musicId].Fcol1 != 0;
}
}
private static void SetOptions(CardDetail firstOptionCardDetail, CardDetail secondOptionCardDetail, UserDetail userDetail)
{
var fastSlow = (int)firstOptionCardDetail.ScoreUi1;
var feverTrance = (int)firstOptionCardDetail.ScoreUi2;
if (!Enum.IsDefined(typeof(PlayOptions.FastSlowIndicator), fastSlow))
{
fastSlow = (int)PlayOptions.FastSlowIndicator.NotUsed;
}
if (!Enum.IsDefined(typeof(PlayOptions.FeverTranceShow), feverTrance))
{
feverTrance = (int)PlayOptions.FeverTranceShow.Show;
}
userDetail.PlayOption = new PlayOption
{
CardId = firstOptionCardDetail.CardId,
FastSlowIndicator = (PlayOptions.FastSlowIndicator)fastSlow,
FeverTrance = (PlayOptions.FeverTranceShow)feverTrance,
AvatarId = firstOptionCardDetail.ScoreI1,
TitleId = firstOptionCardDetail.Fcol2,
NavigatorId = secondOptionCardDetail.ScoreI1
};
}
private void SetDetails(CardDetail cardDetail, IDictionary<int, SongPlayData> songPlayDataDict,
UserDetail userDetail)
{
var musicId = cardDetail.Pcol2;
AddSongPlayDataIfNotExist(songPlayDataDict, musicId);
for (var i = 0; i < SharedConstants.DIFFICULTY_COUNT; i++)
{
var songPlayDetailData = songPlayDataDict[musicId].SongPlaySubDataList[i];
songPlayDetailData.Difficulty = (Difficulty)i;
if (i != cardDetail.Pcol3)
{
continue;
}
songPlayDetailData.Score = (int)cardDetail.ScoreUi1;
songPlayDetailData.MaxChain = (int)cardDetail.ScoreUi3;
userDetail.TotalScore += cardDetail.ScoreUi1;
if (cardDetail.ScoreUi1 >= SharedConstants.S_SCORE_THRESHOLD)
{
userDetail.SAboveStageCount++;
}
if (cardDetail.ScoreUi1 >= SharedConstants.S_PLUS_SCORE_THRESHOLD)
{
userDetail.SPlusAboveStageCount++;
}
if (cardDetail.ScoreUi1 >= SharedConstants.S_PLUS_PLUS_SCORE_THRESHOLD)
{
userDetail.SPlusPlusAboveStageCount++;
}
}
}
private void SetCounts(CardDetail cardDetail, IDictionary<int, SongPlayData> songPlayDataDict, UserDetail userDetail)
{
var musicId = cardDetail.Pcol2;
AddSongPlayDataIfNotExist(songPlayDataDict, musicId);
for (var i = 0; i < SharedConstants.DIFFICULTY_COUNT; i++)
{
var songPlayDetailData = songPlayDataDict[musicId].SongPlaySubDataList[i];
songPlayDetailData.Difficulty = (Difficulty)i;
if (i != cardDetail.Pcol3)
{
continue;
}
songPlayDetailData.PlayCount = (int)cardDetail.ScoreUi1;
songPlayDetailData.LastPlayTime = cardDetail.LastPlayTime;
songPlayDetailData.ClearState = ClearState.Failed;
userDetail.PlayedStageCount++;
if (cardDetail.ScoreUi2 > 0)
{
userDetail.ClearedStageCount++;
songPlayDetailData.ClearState = ClearState.Clear;
}
if (cardDetail.ScoreUi3 > 0)
{
userDetail.NoMissStageCount++;
songPlayDetailData.ClearState = ClearState.NoMiss;
}
if (cardDetail.ScoreUi4 > 0)
{
userDetail.FullChainStageCount++;
songPlayDetailData.ClearState = ClearState.FullChain;
}
if (cardDetail.ScoreUi6 > 0)
{
userDetail.PerfectStageCount++;
songPlayDetailData.ClearState = ClearState.Perfect;
}
}
}
private void AddSongPlayDataIfNotExist(IDictionary<int, SongPlayData> songPlayDataDict, int musicId)
{
if (songPlayDataDict.ContainsKey(musicId))
{
return;
}
var musicData = musics[musicId];
var songPlayData = new SongPlayData
{
Artist = musicData.Artist ?? string.Empty,
Title = musicData.Title ?? string.Empty,
MusicId = musicId,
SongPlaySubDataList = new SongPlayDetailData[SharedConstants.DIFFICULTY_COUNT]
};
for (var i = 0; i < SharedConstants.DIFFICULTY_COUNT; i++)
{
songPlayData.SongPlaySubDataList[i] = new SongPlayDetailData();
}
songPlayDataDict[musicId] = songPlayData;
}
}
@@ -1,4 +1,4 @@
using System.Data;
using System.Collections.Concurrent;
using System.Net.Mime;
using System.Text;
using System.Xml.Linq;
@@ -9,6 +9,7 @@ using EmbedIO.WebApi;
using GCLocalServerRewrite.common;
using GCLocalServerRewrite.models;
using SQLite.Net2;
using Swan;
using Swan.Logging;
namespace GCLocalServerRewrite.controllers;
@@ -18,169 +19,170 @@ public class CardServiceController : WebApiController
private readonly SQLiteConnection cardSqLiteConnection;
private readonly SQLiteConnection musicSqLiteConnection;
private static readonly ConcurrentDictionary<long, List<OnlineMatchingEntry>> OnlineMatchingEntries = new()
{
[0xDEADBEEF] = new List<OnlineMatchingEntry>(),
[0xCAFEBABE] = new List<OnlineMatchingEntry>(),
[0xD0D0CACA] = new List<OnlineMatchingEntry>()
};
public CardServiceController()
{
cardSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.CARD_DB_NAME);
musicSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.MUSIC_DB_NAME);
cardSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.CardDbName);
musicSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.MusicDbName);
}
[Route(HttpVerbs.Post, "/cardn.cgi")]
// ReSharper disable once UnusedMember.Global
public string CardService([FormField] int gid, [FormField("mac_addr")] string mac, [FormField] int type,
[FormField("card_no")] long cardId, [FormField("data")] string xmlData)
[FormField("card_no")] long cardId, [FormField("data")] string xmlData, [FormField("cmd_str")] int cmdType)
{
HttpContext.Response.ContentType = MediaTypeNames.Application.Octet;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
if (gid != Configs.GC4_EX_GID)
return ProcessCommand(cmdType, mac, cardId, xmlData, type);
}
private string ProcessCommand(int cmdType, string mac, long cardId, string xmlData, int type)
{
if (!Enum.IsDefined(typeof(Command), cmdType))
{
throw new ArgumentOutOfRangeException(nameof(gid));
throw new ArgumentOutOfRangeException(nameof(cmdType), cmdType, $"Cmd type is unknown!\n Data is {xmlData}");
}
var command = (Command)cmdType;
return command switch
{
Command.CardReadRequest or Command.CardWriteRequest => ProcessCardRequest(mac, cardId, xmlData, type),
Command.ReissueRequest => ProcessReissueRequest(),
Command.RegisterRequest => ProcessRegisterRequest(cardId, xmlData),
_ => throw new ArgumentOutOfRangeException(nameof(command), command, "Command unknown, should never happen!")
};
}
private string ProcessRegisterRequest(long cardId, string xmlData)
{
$"Get card register request, data is \n{xmlData}".Info();
Write<Card>(cardId, xmlData);
return ConstructResponse(xmlData);
}
private static string ProcessReissueRequest()
{
"Get reissue request, returning not reissue".Info();
return ConstructResponse("", ReturnCode.NotReissue);
}
private string ProcessCardRequest(string mac, long cardId, string xmlData, int type)
{
if (!Enum.IsDefined(typeof(CardRequestType), type))
{
throw new ArgumentOutOfRangeException(nameof(type));
throw new ArgumentOutOfRangeException(nameof(type), type, "Card request type is unknown!");
}
var requestType = (CardRequestType)type;
$"Getting card request, type is {requestType}".Info();
switch (requestType)
{
#region ReadRequests
case CardRequestType.ReadCard:
{
$"Getting read request, type is {requestType}".Info();
var response = Card(cardId, out var returnCode);
return ConstructResponse(Card(cardId));
return ConstructResponse(response, returnCode);
}
case CardRequestType.ReadCardDetail:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(CardDetail(cardId, xmlData));
var cardDetail = CardDetail(cardId, xmlData);
return ConstructResponse(cardDetail);
}
case CardRequestType.ReadCardDetails:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(CardDetails(cardId));
}
case CardRequestType.ReadCardBData:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(CardBData(cardId));
}
case CardRequestType.ReadAvatar:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GetStaticCount<Avatar>(cardId, Configs.AVATAR_COUNT, Configs.AVATAR_XPATH));
GetStaticCount<Avatar>(cardId, Configs.SETTINGS.AvatarCount, Configs.AVATAR_XPATH));
}
case CardRequestType.ReadItem:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GetStaticCount<Item>(cardId, Configs.ITEM_COUNT, Configs.ITEM_XPATH));
GetStaticCount<Item>(cardId, Configs.SETTINGS.ItemCount, Configs.ITEM_XPATH));
}
case CardRequestType.ReadSkin:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GetStaticCount<Skin>(cardId, Configs.SKIN_COUNT, Configs.SKIN_XPATH));
GetStaticCount<Skin>(cardId, Configs.SETTINGS.SkinCount, Configs.SKIN_XPATH));
}
case CardRequestType.ReadTitle:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GetStaticCount<Title>(cardId, Configs.TITLE_COUNT, Configs.TITLE_XPATH));
GetStaticCount<Title>(cardId, Configs.SETTINGS.TitleCount, Configs.TITLE_XPATH));
}
case CardRequestType.ReadMusic:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(MusicUnlock());
}
case CardRequestType.ReadEventReward:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GenerateEmptyXML(Configs.EVENT_REWARD));
GenerateEmptyXml(Configs.EVENT_REWARD));
}
case CardRequestType.ReadNavigator:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GetStaticCount<Navigator>(cardId, Configs.NAVIGATOR_COUNT, Configs.NAVIGATOR_XPATH));
GetStaticCount<Navigator>(cardId, Configs.SETTINGS.NavigatorCount, Configs.NAVIGATOR_XPATH));
}
case CardRequestType.ReadMusicExtra:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(MusicExtra());
}
case CardRequestType.ReadMusicAou:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(GenerateEmptyXML(Configs.MUSIC_AOU));
return ConstructResponse(MusicAouUnlock());
}
case CardRequestType.ReadCoin:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(Coin(cardId));
}
case CardRequestType.ReadUnlockReward:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(UnlockReward(cardId));
}
case CardRequestType.ReadUnlockKeynum:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(UnlockKeynum(cardId));
}
case CardRequestType.ReadSoundEffect:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(
GetStaticCount<SoundEffect>(cardId, Configs.SE_COUNT, Configs.SE_XPATH));
GetStaticCount<SoundEffect>(cardId, Configs.SETTINGS.SeCount, Configs.SE_XPATH));
}
case CardRequestType.ReadGetMessage:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(GenerateEmptyXML(Configs.GET_MESSAGE));
return ConstructResponse(GenerateEmptyXml(Configs.GET_MESSAGE));
}
case CardRequestType.ReadCond:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(GenerateEmptyXML(Configs.COND));
return ConstructResponse(GenerateEmptyXml(Configs.COND));
}
case CardRequestType.ReadTotalTrophy:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(TotalTrophy(cardId));
}
case CardRequestType.SessionStart:
case CardRequestType.SessionGet:
case CardRequestType.StartSession:
case CardRequestType.GetSession:
{
$"Getting read request, type is {requestType}".Info();
return ConstructResponse(GetSession(cardId, mac));
}
@@ -188,27 +190,26 @@ public class CardServiceController : WebApiController
#region WriteRequests
case CardRequestType.WriteCard:
{
$"Card Write data is {xmlData}".Info();
Write<Card>(cardId, xmlData);
return ConstructResponse(xmlData);
}
case CardRequestType.WriteCardDetail:
{
$"Getting write request, type is {requestType}\n Data is {xmlData}".Info();
Write<CardDetail>(cardId, xmlData);
$"Card Detail Write data is {xmlData}".Info();
WriteCardDetail(cardId, xmlData);
return ConstructResponse(xmlData);
}
case CardRequestType.WriteCardBData:
{
$"Getting write request, type is {requestType}\n Data is {xmlData}".Info();
$"Card BData Write data is {xmlData}".Info();
Write<CardBData>(cardId, xmlData);
return ConstructResponse(xmlData);
}
case CardRequestType.WriteCard:
{
$"Getting write request, type is {requestType}\n Data is {xmlData}".Info();
Write<Card>(cardId, xmlData);
WriteCardPlayCount(cardId);
return ConstructResponse(xmlData);
}
// TODO: Maybe one day implement these
case CardRequestType.WriteAvatar:
case CardRequestType.WriteItem:
case CardRequestType.WriteTitle:
@@ -219,33 +220,52 @@ public class CardServiceController : WebApiController
case CardRequestType.WriteUnlockKeynum:
case CardRequestType.WriteSoundEffect:
{
$"Getting write request, type is {requestType}\n Data is {xmlData}".Info();
$"Card Write data is {xmlData}".Info();
return ConstructResponse(xmlData);
}
#endregion
#region OnlineMatching
case CardRequestType.StartOnlineMatching:
{
$"Start Online Matching, data is {xmlData}".Info();
return ConstructResponse(StartOnlineMatching(cardId, xmlData));
}
case CardRequestType.UpdateOnlineMatching:
{
$"Update Online Matching, data is {xmlData}".Info();
return ConstructResponse(UpdateOnlineMatching(cardId, xmlData));
}
case CardRequestType.UploadOnlineMatchingResult:
{
$"Get Online Matching result, data is {xmlData}".Info();
return ConstructResponse(UploadOnlineMatchingResult(cardId, xmlData));
}
#endregion
default:
#pragma warning disable CA2208
throw new ArgumentOutOfRangeException(nameof(requestType));
#pragma warning restore CA2208
throw new ArgumentOutOfRangeException(nameof(requestType), requestType, "Request type not captured, should never happen!");
}
}
#region ReadImplementation
private string Card(long cardId)
private string Card(long cardId, out ReturnCode returnCode)
{
var result = cardSqLiteConnection.Table<Card>().Where(card => card.CardId == cardId);
if (!result.Any())
{
return GenerateEmptyXML(Configs.CARD);
returnCode = ReturnCode.CardNotRegistered;
return string.Empty;
}
var card = result.First();
returnCode = ReturnCode.Ok;
return GenerateSingleXml(card, Configs.CARD_XPATH);
}
@@ -261,11 +281,10 @@ public class CardServiceController : WebApiController
if (!result.Any())
{
return GenerateEmptyXML(Configs.CARD_DETAIL);
return GenerateEmptyXml(Configs.CARD_DETAIL);
}
var cardDetail = result.First();
return GenerateSingleXml(cardDetail, Configs.CARD_DETAIL_SINGLE_XPATH);
}
@@ -276,7 +295,7 @@ public class CardServiceController : WebApiController
if (!result.Any())
{
return GenerateEmptyXML(Configs.CARD_DETAIL);
return GenerateEmptyXml(Configs.CARD_DETAIL);
}
var cardDetails = result.ToList();
@@ -291,7 +310,7 @@ public class CardServiceController : WebApiController
if (!result.Any())
{
return GenerateEmptyXML(Configs.CARD_BDATA);
return GenerateEmptyXml(Configs.CARD_BDATA);
}
var cardBData = result.First();
@@ -393,6 +412,13 @@ public class CardServiceController : WebApiController
return GenerateRecordsXml(result, Configs.MUSIC_XPATH);
}
private string MusicAouUnlock()
{
var result = musicSqLiteConnection.Table<MusicAou>().ToList();
return !result.Any() ? GenerateEmptyXml(Configs.MUSIC_AOU) : GenerateRecordsXml(result, Configs.MUSIC_AOU_XPATH);
}
private string MusicExtra()
{
var result = musicSqLiteConnection.Table<MusicExtra>().ToList();
@@ -404,23 +430,28 @@ public class CardServiceController : WebApiController
#region HelperMethods
private static string ConstructResponse(string xml)
private static string ConstructResponse(string xml, ReturnCode returnCode = ReturnCode.Ok)
{
return "1\n" +
"10,10\n" +
xml;
var returnCodeInt = (int)returnCode;
if (returnCodeInt == 1)
{
return $"{returnCodeInt}\n" +
"1,1\n" +
xml;
}
return $"{returnCodeInt}";
}
private static string GenerateEmptyXML(string fieldName)
private static string GenerateEmptyXml(string fieldName)
{
var xml = new XDocument(new XElement("root",
new XElement(fieldName)));
new XElement(fieldName)));
xml.Declaration = new XDeclaration("1.0", "UTF-8", null);
return xml.ToString();
}
private static string GenerateSingleXml<T>(T obj, string xpath) where T: class
private static string GenerateSingleXml<T>(T obj, string xpath) where T : class
{
var sb = new StringBuilder();
@@ -435,7 +466,7 @@ public class CardServiceController : WebApiController
return sb.ToString();
}
private static string GenerateRecordsXml<T>(IReadOnlyList<T> list, string xpath) where T : Record
{
var stringBuilder = new StringBuilder();
@@ -454,7 +485,6 @@ public class CardServiceController : WebApiController
writer.Write(list);
}
stringBuilder.Insert(stringBuilder.Length - 5, "/");
return stringBuilder.ToString();
}
@@ -483,6 +513,190 @@ public class CardServiceController : WebApiController
$"Updated {typeof(T)}".Info();
}
private void WriteCardPlayCount(long cardId)
{
var record = cardSqLiteConnection.Table<CardPlayCount>().Where(count => count.CardId == cardId);
if (!record.Any())
{
$"Created new play count data for card {cardId}".Info();
var playCount = new CardPlayCount
{
CardId = cardId,
PlayCount = 1,
LastPlayed = DateTime.Now
};
cardSqLiteConnection.InsertOrReplace(playCount);
return;
}
var data = record.First();
var now = DateTime.Now;
var lastPlayedTime = data.LastPlayed;
if (now <= lastPlayedTime)
{
$"Current time {now} is less than or equal to last played time! Clock skew detected!".Warn();
data.PlayCount = 0;
data.LastPlayed = DateTime.Now;
cardSqLiteConnection.InsertOrReplace(data);
return;
}
DateTime start;
DateTime end;
if (now.Hour >= 8)
{
start = DateTime.Today.AddHours(8);
end = start.AddHours(24);
}
else
{
end = DateTime.Today.AddHours(8);
start = end.AddHours(-24);
}
data.PlayCount = lastPlayedTime.IsBetween(start, end) ? data.PlayCount + 1 : 0;
cardSqLiteConnection.InsertOrReplace(data);
$"Updated card play count, current count is {data.PlayCount}".Info();
}
private void WriteCardDetail(long cardId, string xmlData)
{
var result = cardSqLiteConnection.Table<CardDetail>()
.Where(detail => detail.CardId == cardId);
// Unlock all unlockable songs in card details table when write card detail for the first time
if (!result.Any())
{
UnlockSongs(cardId);
}
var reader = new ChoXmlReader<CardDetail>(new StringReader(xmlData)).WithXPath(Configs.DATA_XPATH);
var cardDetail = reader.Read();
if (cardDetail is null)
{
throw new HttpRequestException("Write object is null");
}
cardDetail.SetCardId(cardId);
cardDetail.LastPlayTime = DateTime.Now;
var rowsAffected = cardSqLiteConnection.InsertOrReplace(cardDetail);
if (rowsAffected == 0)
{
throw new ApplicationException("Update database failed!");
}
"Updated card detail".Info();
}
private void UnlockSongs(long cardId)
{
var unlockableSongIds = Configs.SETTINGS.UnlockableSongIds;
if (unlockableSongIds is null)
{
unlockableSongIds = Configs.DEFAULT_UNLOCKABLE_SONGS;
}
var detailList = unlockableSongIds.Select(id => new CardDetail
{
CardId = cardId,
Pcol1 = 10,
Pcol2 = id,
Pcol3 = 0,
ScoreUi2 = 1,
ScoreUi6 = 1,
LastPlayTime = DateTime.Now
})
.ToList();
cardSqLiteConnection.InsertOrIgnoreAll(detailList);
}
#endregion
#region OnlineMatchingImplementation
private static string StartOnlineMatching(long cardId, string xmlData)
{
var reader = new ChoXmlReader<OnlineMatchingEntry>(new StringReader(xmlData)).WithXPath(Configs.DATA_XPATH);
var entry = reader.Read();
entry.CardId = cardId;
entry.MatchingId = 0xDEADBEEF;
entry.EntryNo = OnlineMatchingEntries[0xDEADBEEF].Count;
entry.EntryStart = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
entry.MatchingTimeout = 20;
entry.MatchingRemainingTime = 3;
entry.MatchingWaitTime = 10;
entry.Status = 1;
entry.Dump().Info();
var entries = OnlineMatchingEntries[0xDEADBEEF];
var existing = entries.Find(matchingEntry => matchingEntry.CardId == cardId);
if (existing is not null)
{
"Card id already exist in entry! Previous match is not cleaned up! Clearing now".Warn();
entries.Clear();
}
OnlineMatchingEntries[0xDEADBEEF].Add(entry);
return GenerateRecordsXml(OnlineMatchingEntries[0xDEADBEEF], Configs.ONLINE_MATCHING_XPATH);
}
private static string UpdateOnlineMatching(long cardId, string xmlData)
{
var reader = new ChoXmlReader<OnlineMatchingUpdateData>(new StringReader(xmlData));
var data = reader.Read();
var entries = OnlineMatchingEntries[0xDEADBEEF];
var entry = entries.Find(matchingEntry => matchingEntry.CardId == cardId);
if (entry is null)
{
throw new HttpException(400,"Entry for this card id does not exist!");
}
if (data.Action != 0)
{
$"Action is {data.Action}".Info();
}
entry.MessageId = data.MessageId;
if (entry.MatchingRemainingTime <= 0)
{
entry.Status = 3;
entry.Dump().Info();
return GenerateRecordsXml(OnlineMatchingEntries[0xDEADBEEF], Configs.ONLINE_MATCHING_XPATH);
}
entry.MatchingRemainingTime--;
entry.Dump().Info();
return GenerateRecordsXml(OnlineMatchingEntries[0xDEADBEEF], Configs.ONLINE_MATCHING_XPATH);
}
private static string UploadOnlineMatchingResult(long cardId, string xmlData)
{
var reader = new ChoXmlReader<OnlineMatchingResultData>(new StringReader(xmlData));
var data = reader.Read();
var entries = OnlineMatchingEntries[0xDEADBEEF];
var entry = entries.Find(matchingEntry => matchingEntry.CardId == cardId);
if (entry is null)
{
throw new HttpException(400,"Entry for this card id does not exist!");
}
if (entry.MatchingId != data.MatchingId)
{
throw new HttpException(400,"Matching Id mismatch!");
}
OnlineMatchingEntries[0xDEADBEEF].Remove(entry);
var result = new OnlineMatchingResult
{
Status = 1
};
return GenerateSingleXml(result, Configs.ONLINE_BATTLE_RESULT_XPATH);
}
#endregion
private enum CardRequestType
@@ -510,8 +724,8 @@ public class CardServiceController : WebApiController
ReadTotalTrophy = 8468,
// Sessions
SessionGet = 401,
SessionStart = 402,
GetSession = 401,
StartSession = 402,
// Write data
WriteCard = 771,
@@ -525,6 +739,37 @@ public class CardServiceController : WebApiController
WriteCoin = 980,
WriteSkin = 933,
WriteUnlockKeynum = 1020,
WriteSoundEffect = 8969
WriteSoundEffect = 8969,
// Online matching
StartOnlineMatching = 8705,
UpdateOnlineMatching = 8961,
UploadOnlineMatchingResult = 8709
}
private enum Command
{
CardReadRequest = 256,
CardWriteRequest = 768,
RegisterRequest = 512,
ReissueRequest = 1536
}
private enum ReturnCode
{
/// <summary>
/// 処理は正常に完了しました in debug string
/// </summary>
Ok = 1,
/// <summary>
/// 未登録のカードです in debug string
/// </summary>
CardNotRegistered = 23,
/// <summary>
/// 再発行予約がありません in debug string
/// </summary>
NotReissue = 27
}
}
@@ -13,7 +13,7 @@ public class IncomServiceController : WebApiController
public string IncomService()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "1+1";
@@ -24,7 +24,7 @@ public class IncomServiceController : WebApiController
public string IncomAllService()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "1+1";
@@ -0,0 +1,70 @@
using System.Net.Mime;
using System.Text;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using GCLocalServerRewrite.common;
using GCLocalServerRewrite.models;
using SQLite.Net2;
using Swan;
using Swan.Logging;
namespace GCLocalServerRewrite.controllers;
public class OptionServiceController : WebApiController
{
private readonly SQLiteConnection cardSqLiteConnection;
public OptionServiceController()
{
cardSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.CardDbName);
}
[Route(HttpVerbs.Get, "/PlayInfo.php")]
public string OptionService([QueryField("card_id")] long cardId)
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "1\n" +
$"{GetPlayCount(cardId)}";
}
private int GetPlayCount(long cardId)
{
var record = cardSqLiteConnection.Table<CardPlayCount>().Where(count => count.CardId == cardId);
if (!record.Any())
{
return 0;
}
var now = DateTime.Now;
var data = record.First();
var lastPlayedTime = data.LastPlayed;
if (now <= lastPlayedTime)
{
$"Current time {now} is less than or equal to last played time! Clock skew detected!".Warn();
return 0;
}
DateTime start;
DateTime end;
if (now.Hour >= 8)
{
start = DateTime.Today.AddHours(8);
end = start.AddHours(24);
}
else
{
end = DateTime.Today.AddHours(8);
start = end.AddHours(-24);
}
return lastPlayedTime.IsBetween(start, end) ? data.PlayCount : 0;
}
}
@@ -7,6 +7,7 @@ using EmbedIO.Routing;
using EmbedIO.WebApi;
using GCLocalServerRewrite.common;
using GCLocalServerRewrite.models;
using SQLite.Net2;
using Swan;
using Swan.Logging;
// ReSharper disable UnusedMember.Global
@@ -15,16 +16,26 @@ namespace GCLocalServerRewrite.controllers;
public class RankController : WebApiController
{
private readonly SQLiteConnection cardSqLiteConnection;
private readonly SQLiteConnection musicSqLiteConnection;
public RankController()
{
cardSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.CardDbName);
musicSqLiteConnection = DatabaseHelper.ConnectDatabase(Configs.SETTINGS.MusicDbName);
}
[Route(HttpVerbs.Get, "/ranking.php")]
public string Rank([QueryField("cmd_type")] int type)
{
HttpContext.Response.ContentType = MediaTypeNames.Application.Octet;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
if (!Enum.IsDefined(typeof(RankType), type))
{
throw new ArgumentOutOfRangeException(nameof(type));
throw new ArgumentOutOfRangeException(nameof(type), type, "Rank type out of range");
}
var requestType = (RankType)type;
@@ -41,7 +52,7 @@ public class RankController : WebApiController
case RankType.PlayNumRank:
$"Getting rank request, type is {requestType}".Info();
return ConstructResponse(RankTemp("play_num_rank"));
return ConstructResponse(PlayNumRank());
case RankType.EventRank:
$"Getting rank request, type is {requestType}".Info();
@@ -97,6 +108,64 @@ public class RankController : WebApiController
return stringWriter.ToString();
}
private string PlayNumRank()
{
var rankStatus = new RankStatus
{
Rows = 30,
TableName = "play_num_rank",
Status = 1,
EndDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
var playNumRankContainer = new PlayNumRankContainer
{
PlayNumRankRecords = GetPlayNumRankRecords(),
RankStatus = rankStatus
};
var sb = new StringBuilder();
using (var writer = new ChoXmlWriter<PlayNumRankContainer>(sb))
{
writer.Configuration.UseXmlSerialization = true;
writer.Configuration.OmitXmlDeclaration = false;
writer.Configuration.IgnoreRootName = true;
writer.Write(playNumRankContainer);
}
return sb.ToString();
}
private List<PlayNumRankRecord> GetPlayNumRankRecords()
{
var records = new List<PlayNumRankRecord>();
var musics = musicSqLiteConnection.Table<Music>().ToList().OrderBy(arg => Guid.NewGuid()).Take(30).ToList();
for (var i = 0; i < musics.Count; i++)
{
var music = musics[i];
var index = i + 1;
var record = new PlayNumRankRecord
{
Id = index,
Rank = index,
Rank2 = index + 1,
PrevRank = musics.Count - i,
PrevRank2 = index + 1,
Artist = music.Artist,
Pcol1 = music.MusicId,
ScoreBi1 = index,
Title = music.Title
};
records.Add(record);
}
return records;
}
private enum RankType
{
GlobalRank = 4119,
@@ -13,7 +13,7 @@ public class ResponeServiceController : WebApiController
public string ResponeService()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "1";
@@ -15,11 +15,13 @@ namespace GCLocalServerRewrite.controllers;
public class ServerController : WebApiController
{
private static readonly string DataUrl = $"https://{Configs.SETTINGS.ServerIp}/{Configs.STATIC_FOLDER}/{Configs.SETTINGS.EventFolder}";
[Route(HttpVerbs.Get, "/certify.php")]
public string Certify([QueryData] NameValueCollection parameters)
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
var gid = parameters["gid"];
@@ -63,14 +65,14 @@ public class ServerController : WebApiController
md5.ComputeHash(Encoding.UTF8.GetBytes(gid)).Select(b => b.ToString("x2")));
var response = "host=card_id=7020392000147361\n" +
var response = $"host=card_id=7020392000147361,relay_addr={Configs.SETTINGS.RelayServer},relay_port={Configs.SETTINGS.RelayPort}\n" +
"no=1337\n" +
"name=123\n" +
"pref=nesys\n" +
"addr=nesys@home\n" +
"x-next-time=15\n" +
$"x-img=http://localhost{Configs.STATIC_BASE_ROUTE}/news.png\n" +
$"x-ranking=http://localhost{Configs.RANK_BASE_ROUTE}/ranking.php\n" +
$"x-img=http://{Configs.SETTINGS.ServerIp}{Configs.STATIC_BASE_ROUTE}/news.png\n" +
$"x-ranking=http://{Configs.SETTINGS.ServerIp}{Configs.RANK_BASE_ROUTE}/ranking.php\n" +
$"ticket={ticket}";
return response;
@@ -80,7 +82,7 @@ public class ServerController : WebApiController
public string Cursel()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "1\n";
@@ -90,18 +92,34 @@ public class ServerController : WebApiController
public string Data()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "count=1\n" +
"nexttime=0\n";
var responseList = Configs.SETTINGS.ResponseData.ToList();
var count = responseList.Count;
var dataString = new StringBuilder();
for (var i = 0; i < count; i++)
{
var data = responseList[i];
var fileUrl = data.FileName;
if (Configs.SETTINGS.DownloadEvents)
{
fileUrl = data.FileName.StartsWith("/") ? $"{DataUrl}{data.FileName}" : $"{DataUrl}/{data.FileName}";
}
dataString.Append($"{i},{fileUrl},{data.NotBeforeUnixTime},{data.NotAfterUnixTime},{data.Md5},{data.Index}");
dataString.Append('\n');
}
return $"count={count}\n" +
"nexttime=1\n" +
dataString.ToString().TrimEnd('\n');
}
[Route(HttpVerbs.Get, "/gameinfo.php")]
public string GameInfo()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "0\n" +
@@ -0,0 +1,24 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Swan.Logging;
namespace GCLocalServerRewrite.controllers
{
// TODO: Add proper update check response
public class UpdateController : WebApiController
{
[Route(HttpVerbs.Get, "/check.php")]
public string CheckUpdate()
{
var parameters = HttpContext.GetRequestQueryData();
foreach (var key in parameters.AllKeys)
{
$"Key {key}: {parameters[key]}".Info();
}
HttpContext.Response.StatusCode = 404;
return "Not found";
}
}
}
@@ -13,7 +13,7 @@ public class UploadServiceController : WebApiController
public string UploadService()
{
HttpContext.Response.ContentType = MediaTypeNames.Text.Plain;
HttpContext.Response.ContentEncoding = Encoding.Default;
HttpContext.Response.ContentEncoding = new UTF8Encoding(false);
HttpContext.Response.KeepAlive = true;
return "1\n" +
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -11,10 +11,10 @@ public class Avatar : Record , IIdModel, ICardIdModel
public int AvatarId { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
[XmlElement("new_flag")]
public int NewFlag { get; set; } = 1;
+8 -8
View File
@@ -11,12 +11,12 @@ public class Card : ICardIdModel
[Column("card_id")]
[ChoXmlElementRecordField(FieldName = "card_id")]
[XmlElement(ElementName = "card_id")]
public long? CardId { get; set; }
public long CardId { get; set; }
[Column("player_name")]
[ChoXmlElementRecordField(FieldName = "player_name")]
[XmlElement(ElementName = "player_name")]
public string? PlayerName { get; set; }
public string PlayerName { get; set; } = string.Empty;
[Column("score_i1")]
[ChoXmlElementRecordField(FieldName = "score_i1")]
@@ -41,17 +41,17 @@ public class Card : ICardIdModel
[Column("achieve_status")]
[ChoXmlElementRecordField(FieldName = "achieve_status")]
[XmlElement("achieve_status")]
public string? AchieveStatus { get; set; } = string.Empty;
public string AchieveStatus { get; set; } = string.Empty;
[Column("created")]
[ChoXmlElementRecordField(FieldName = "created")]
[XmlElement("created")]
public string Created { get; set; } = "1";
public string Created { get; set; } = "2017-01-01 08:00:00";
[Column("updated")]
[ChoXmlElementRecordField(FieldName = "updated")]
[XmlElement("updated")]
public string Updated { get; set; } = "1";
[Column("modified")]
[ChoXmlElementRecordField(FieldName = "modified")]
[XmlElement("modified")]
public string Modified { get; set; } = "2017-01-01 08:00:00";
public void SetCardId(long cardId)
{
+1 -1
View File
@@ -11,7 +11,7 @@ public class CardBData : ICardIdModel
[Column("card_id")]
[ChoXmlElementRecordField(FieldName = "card_id")]
[XmlElement(ElementName = "card_id")]
public long? CardId { get; set; }
public long CardId { get; set; }
[Column("bdata")]
[ChoXmlElementRecordField(FieldName = "bdata")]
@@ -90,6 +90,10 @@ public class CardDetail : Record, ICardIdModel
[ChoXmlElementRecordField(FieldName = "fcol3")]
[XmlElement("fcol3")]
public int Fcol3 { get; set; }
[Column("last_play_time")]
[XmlIgnore]
public DateTime LastPlayTime { get; set; } = DateTime.MinValue;
public void SetCardId(long cardId)
{
@@ -0,0 +1,16 @@
using SQLite.Net2;
namespace GCLocalServerRewrite.models;
public class CardPlayCount
{
[PrimaryKey]
[Column("card_id")]
public long CardId { get; set; }
[Column("play_count")]
public int PlayCount { get; set; }
[Column("last_played_time")]
public DateTime LastPlayed { get; set; }
}
+2 -2
View File
@@ -17,8 +17,8 @@ public class Coin
public int MonthlyCoins { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
}
+3 -3
View File
@@ -14,13 +14,13 @@ public class Item : Record, IIdModel, ICardIdModel
public int ItemNum { get; set; } = 90;
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
[XmlElement("new_flag")]
public int NewFlag { get; set; } = 1;
public int NewFlag { get; set; }
[XmlElement("use_flag")]
public int UseFlag { get; set; } = 1;
+18 -8
View File
@@ -12,20 +12,26 @@ public class Music : Record
public int MusicId { get; set; }
[Column("title")]
[XmlElement(ElementName = "title", IsNullable = true)]
public string? Title { get; set; }
[XmlElement(ElementName = "title")]
public string? Title { get; set; } = string.Empty;
[Column("artist")]
[XmlElement(ElementName = "artist", IsNullable = true)]
public string? Artist { get; set; }
[XmlElement(ElementName = "artist")]
public string? Artist
{
get => _artist;
set => _artist = value ?? string.Empty;
}
private string? _artist = string.Empty;
[Column("release_date")]
[XmlElement(ElementName = "release_date", IsNullable = true)]
public string? ReleaseDate { get; set; }
[XmlElement(ElementName = "release_date")]
public string ReleaseDate { get; set; } = "2013-01-01 08:00:00";
[Column("end_date")]
[XmlElement(ElementName = "end_date", IsNullable = true)]
public string? EndDate { get; set; }
[XmlElement(ElementName = "end_date")]
public string EndDate { get; set; } = "2030-01-01 08:00:00";
[Column("new_flag")]
[XmlElement("new_flag")]
@@ -34,4 +40,8 @@ public class Music : Record
[Column("use_flag")]
[XmlElement("use_flag")]
public int UseFlag { get; set; }
[Ignore]
[XmlElement("calc_flag")]
public uint CalcFlag { get; set; } = 2;
}
@@ -11,26 +11,6 @@ public class MusicAou : Record
[XmlElement("music_id")]
public int MusicId { get; set; }
[Column("title")]
[XmlElement(ElementName = "title", IsNullable = true)]
public string? Title { get; set; }
[Column("artist")]
[XmlElement(ElementName = "artist", IsNullable = true)]
public string? Artist { get; set; }
[Column("release_date")]
[XmlElement(ElementName = "release_date", IsNullable = true)]
public string? ReleaseDate { get; set; }
[Column("end_date")]
[XmlElement(ElementName = "end_date", IsNullable = true)]
public string? EndDate { get; set; }
[Column("new_flag")]
[XmlElement("new_flag")]
public int NewFlag { get; set; }
[Column("use_flag")]
[XmlElement("use_flag")]
public int UseFlag { get; set; }
+3 -3
View File
@@ -11,13 +11,13 @@ public class Navigator : Record, IIdModel, ICardIdModel
public int NavigatorId { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
[XmlElement("new_flag")]
public int NewFlag { get; set; } = 1;
public int NewFlag { get; set; }
[XmlElement("use_flag")]
public int UseFlag { get; set; } = 1;
@@ -0,0 +1,96 @@
using System.Xml.Serialization;
using ChoETL;
namespace GCLocalServerRewrite.models;
public class OnlineMatchingEntry: Record
{
[XmlElement(ElementName = "machine_id")]
[ChoXmlElementRecordField(FieldName = "machine_id")]
public long MachineId { get; set; }
[XmlElement(ElementName = "event_id")]
[ChoXmlElementRecordField(FieldName = "event_id")]
public long EventId { get; set; }
[XmlElement(ElementName = "matching_id")]
[ChoXmlElementRecordField(FieldName = "matching_id")]
public long MatchingId { get; set; }
[XmlElement(ElementName = "entry_no")]
[ChoXmlElementRecordField(FieldName = "entry_no")]
public long EntryNo { get; set; }
[XmlElement(ElementName = "entry_start")]
[ChoXmlElementRecordField(FieldName = "entry_start")]
public string EntryStart { get; set; } = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
[XmlElement(ElementName = "status")]
[ChoXmlElementRecordField(FieldName = "status")]
public long Status { get; set; } = 1;
[XmlElement(ElementName = "card_id")]
[ChoXmlElementRecordField(FieldName = "card_id")]
public long CardId { get; set; }
[XmlElement(ElementName = "player_name")]
[ChoXmlElementRecordField(FieldName = "player_name")]
public string PlayerName { get; set; } = string.Empty;
[XmlElement(ElementName = "avatar_id")]
[ChoXmlElementRecordField(FieldName = "avatar_id")]
public long AvatarId { get; set; }
[XmlElement(ElementName = "title_id")]
[ChoXmlElementRecordField(FieldName = "title_id")]
public long TitleId { get; set; }
[XmlElement(ElementName = "class_id")]
[ChoXmlElementRecordField(FieldName = "class_id")]
public long ClassId { get; set; }
[XmlElement(ElementName = "group_id")]
[ChoXmlElementRecordField(FieldName = "group_id")]
public long GroupId { get; set; }
[XmlElement(ElementName = "tenpo_id")]
[ChoXmlElementRecordField(FieldName = "tenpo_id")]
public long TenpoId { get; set; }
[XmlElement(ElementName = "tenpo_name")]
[ChoXmlElementRecordField(FieldName = "tenpo_name")]
public string TenpoName { get; set; } = "1337";
[XmlElement(ElementName = "pref_id")]
[ChoXmlElementRecordField(FieldName = "pref_id")]
public long PrefId { get; set; }
[XmlElement(ElementName = "pref")]
[ChoXmlElementRecordField(FieldName = "pref")]
public string Pref { get; set; } = "nesys";
[XmlElement(ElementName = "message_id")]
[ChoXmlElementRecordField(FieldName = "message_id")]
public long MessageId { get; set; }
/// <summary>
/// Communication timeout?
/// </summary>
[XmlElement(ElementName = "matching_timeout")]
[ChoXmlElementRecordField(FieldName = "matching_timeout")]
public long MatchingTimeout { get; set; } = 99;
/// <summary>
/// Wait time
/// </summary>
[XmlElement(ElementName = "matching_wait_time")]
[ChoXmlElementRecordField(FieldName = "matching_wait_time")]
public long MatchingWaitTime { get; set; } = 10;
/// <summary>
/// Seems not used
/// </summary>
[XmlElement(ElementName = "matching_remaining_time")]
[ChoXmlElementRecordField(FieldName = "matching_remaining_time")]
public long MatchingRemainingTime { get; set; } = 89;
}
@@ -0,0 +1,9 @@
using System.Xml.Serialization;
namespace GCLocalServerRewrite.models;
public class OnlineMatchingResult
{
[XmlElement(ElementName = "status")]
public int Status { get; set; }
}
@@ -0,0 +1,56 @@
using ChoETL;
namespace GCLocalServerRewrite.models;
[ChoXmlRecordObject(XPath = "/root/data")]
public class OnlineMatchingResultData
{
[ChoXmlElementRecordField(FieldName = "event_id")]
public long EventId { get; set; }
[ChoXmlElementRecordField(FieldName = "matching_id")]
public long MatchingId { get; set; }
[ChoXmlElementRecordField(FieldName = "class_id")]
public long ClassId { get; set; }
[ChoXmlElementRecordField(FieldName = "group_id")]
public long GroupId { get; set; }
[ChoXmlElementRecordField(FieldName = "result_score")]
public long ResultScore { get; set; }
[ChoXmlElementRecordField(FieldName = "result_star")]
public long ResultStar { get; set; }
[ChoXmlElementRecordField(FieldName = "result_rank")]
public long ResultRank { get; set; }
[ChoXmlElementRecordField(FieldName = "music_id_1st")]
public long MusicIdFirst { get; set; }
[ChoXmlElementRecordField(FieldName = "music_id_2nd")]
public long MusicIdSecond { get; set; }
[ChoXmlElementRecordField(FieldName = "music_id_3rd")]
public long MusicIdThird { get; set; }
[ChoXmlElementRecordField(FieldName = "difficulty_lv_1st")]
public long DifficultyFirst { get; set; }
[ChoXmlElementRecordField(FieldName = "difficulty_lv_2nd")]
public long DifficultySecond { get; set; }
[ChoXmlElementRecordField(FieldName = "difficulty_lv_3rd")]
public long DifficultyThird { get; set; }
[ChoXmlElementRecordField(FieldName = "item_id_1st")]
public long ItemIdFirst { get; set; }
[ChoXmlElementRecordField(FieldName = "item_id_2nd")]
public long ItemIdSecond { get; set; }
[ChoXmlElementRecordField(FieldName = "item_id_3rd")]
public long ItemIdThird { get; set; }
}
@@ -0,0 +1,20 @@
using ChoETL;
namespace GCLocalServerRewrite.models;
[ChoXmlRecordObject(XPath = "/root/data")]
// ReSharper disable once ClassNeverInstantiated.Global
public class OnlineMatchingUpdateData
{
[ChoXmlElementRecordField(FieldName = "action")]
public long Action { get; set; }
[ChoXmlElementRecordField(FieldName = "event_id")]
public long EventId { get; set; }
[ChoXmlElementRecordField(FieldName = "matching_id")]
public long MatchingId { get; set; }
[ChoXmlElementRecordField(FieldName = "message_id")]
public long MessageId { get; set; }
}
@@ -0,0 +1,13 @@
using System.Xml.Serialization;
namespace GCLocalServerRewrite.models;
[XmlRoot("root")]
public class PlayNumRankContainer
{
[XmlArray("play_num_rank")]
public List<PlayNumRankRecord> PlayNumRankRecords { get; set; } = new();
[XmlElement("ranking_status")]
public RankStatus? RankStatus { get; set; }
}
@@ -0,0 +1,34 @@
using System.Xml.Serialization;
namespace GCLocalServerRewrite.models;
[XmlType("record")]
public class PlayNumRankRecord
{
[XmlAttribute(AttributeName = "id")]
public int Id { get; set; }
[XmlElement("rank")]
public int Rank { get; set; }
[XmlElement("rank2")]
public int Rank2 { get; set; }
[XmlElement("prev_rank")]
public int PrevRank { get; set; }
[XmlElement("prev_rank2")]
public int PrevRank2 { get; set; }
[XmlElement("pcol1")]
public int Pcol1 { get; set; }
[XmlElement("score_bi1")]
public int ScoreBi1 { get; set; }
[XmlElement("title")]
public string? Title { get; set; }
[XmlElement("artist")]
public string? Artist { get; set; }
}
+5 -4
View File
@@ -2,16 +2,17 @@
namespace GCLocalServerRewrite.models;
[XmlType("ranking_status")]
public class RankStatus
{
[XmlElement("table_name")]
public string? TableName { get; set; }
[XmlElement("table_name")]
public string TableName { get; set; } = "rank_table";
[XmlElement("start_date")]
public string StartDate { get; set; } = "2017-05-30";
public string StartDate { get; set; } = "2021-05-30";
[XmlElement("end_date")]
public string EndDate { get; set; } = "2018-05-30";
public string EndDate { get; set; } = "2022-06-08";
[XmlElement("status")]
public int Status { get; set; }
+2 -1
View File
@@ -1,10 +1,11 @@
using System.Xml.Serialization;
using ChoETL;
using SQLite.Net2;
namespace GCLocalServerRewrite.models;
public class Record
{
[XmlAttribute(AttributeName = "id")]
[Ignore]
public int RecordId { get; set; }
}
+3 -3
View File
@@ -7,11 +7,11 @@ public class Session
[XmlElement(ElementName = "card_id")]
public long CardId { get; set; }
[XmlElement(ElementName = "mac_addr")]
public string? Mac { get; set; }
[XmlElement(ElementName = "mac_addr")]
public string Mac { get; set; } = "000000000000";
[XmlElement(ElementName = "session_id")]
public string? SessionId { get; set; }
public string SessionId { get; set; } = "12345678901234567890123456789012";
[XmlElement(ElementName = "expires")]
public int Expires { get; set; }
+3 -3
View File
@@ -11,13 +11,13 @@ public class Skin : Record, IIdModel, ICardIdModel
public int SkinId { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
[XmlElement("new_flag")]
public int NewFlag { get; set; } = 1;
public int NewFlag { get; set; }
[XmlElement("use_flag")]
public int UseFlag { get; set; } = 1;
@@ -11,13 +11,13 @@ public class SoundEffect : Record, IIdModel, ICardIdModel
public int SeId { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
[XmlElement("new_flag")]
public int NewFlag { get; set; } = 1;
public int NewFlag { get; set; }
[XmlElement("use_flag")]
public int UseFlag { get; set; } = 1;
+3 -3
View File
@@ -11,13 +11,13 @@ public class Title : Record, IIdModel, ICardIdModel
public int TitleId { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
[XmlElement("new_flag")]
public int NewFlag { get; set; } = 1;
public int NewFlag { get; set; }
[XmlElement("use_flag")]
public int UseFlag { get; set; } = 1;
@@ -26,8 +26,8 @@ public class UnlockKeynum : Record
public int CashFlag { get; set; }
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
}
@@ -14,16 +14,16 @@ public class UnlockReward : Record
public int RewardType { get; set; }
[XmlElement(ElementName = "open_date")]
public string? OpenDate { get; set; } = "2021-05-30";
public string OpenDate { get; set; } = "2021-05-30";
[XmlElement(ElementName = "close_date")]
public string? CloseDate { get; set; } = "2030-05-30";
public string CloseDate { get; set; } = "2030-05-30";
[XmlElement(ElementName = "open_time")]
public string? OpenTime { get; set; } = "00:00:01";
public string OpenTime { get; set; } = "00:00:01";
[XmlElement(ElementName = "close_time")]
public string? CloseTime { get; set; } = "23:59:59";
public string CloseTime { get; set; } = "23:59:59";
[XmlElement(ElementName = "target_id")]
public int TargetId { get; set; }
@@ -50,8 +50,8 @@ public class UnlockReward : Record
public long CloseUnixTime { get; set; } = 1906387199;
[XmlElement("created")]
public string? Created { get; set; } = "1";
public string Created { get; set; } = "1";
[XmlElement("modified")]
public string? Modified { get; set; } = "1";
public string Modified { get; set; } = "1";
}
+36 -20
View File
@@ -2,17 +2,17 @@
using EmbedIO.Actions;
using EmbedIO.Files;
using EmbedIO.WebApi;
using GCLocalServerRewrite.backports;
using GCLocalServerRewrite.common;
using GCLocalServerRewrite.controllers;
using GCLocalServerRewrite.models;
using Swan.Logging;
using System.Text;
namespace GCLocalServerRewrite.server;
public class Server
{
public static WebServer CreateWebServer(string[] urlPrefixes)
public static WebServer CreateWebServer(IEnumerable<string> urlPrefixes)
{
InitializeDatabase();
var cert = CertificateHelper.InitializeCertificate();
@@ -22,32 +22,48 @@ public class Server
.WithCertificate(cert)
.WithMode(HttpListenerMode.EmbedIO))
.WithLocalSessionManager()
.WithCors(
"http://unosquare.github.io,http://run.plnkr.co",
"content-type, accept",
"post")
.WithWebApi(Configs.CARD_SERVICE_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithCors()
.WithWebApi(Configs.API_BASE_ROUTE, module => module.WithController<ApiController>())
.WithWebApi(Configs.CARD_SERVICE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<CardServiceController>())
.WithWebApi(Configs.UPLOAD_SERVICE_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithWebApi(Configs.OPTION_SERVICE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<OptionServiceController>())
.WithWebApi(Configs.UPLOAD_SERVICE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<UploadServiceController>())
.WithWebApi(Configs.RESPONE_SERVICE_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithWebApi(Configs.RESPONE_SERVICE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<ResponeServiceController>())
.WithWebApi(Configs.INCOM_SERVICE_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithWebApi(Configs.INCOM_SERVICE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<IncomServiceController>())
.WithWebApi(Configs.ALIVE_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithWebApi(Configs.ALIVE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<AliveController>())
.WithWebApi(Configs.SERVER_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithWebApi(Configs.SERVER_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<ServerController>())
.WithWebApi(Configs.RANK_BASE_ROUTE, CustomResponseSerializer.None(true),
.WithWebApi(Configs.RANK_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<RankController>())
.WithWebApi(Configs.UPDATE_SERVICE_BASE_ROUTE, ResponseSerializer.None(true),
module => module.WithController<UpdateController>())
.WithStaticFolder(Configs.STATIC_BASE_ROUTE, PathHelper.HtmlRootPath, true, m => m
.WithContentCaching(Configs.USE_FILE_CACHE))
.WithContentCaching(Configs.USE_FILE_CACHE))
// Add static files after other modules to avoid conflicts
.WithStaticFolder("/", PathHelper.HtmlRootPath, true, m => m
.WithContentCaching(Configs.USE_FILE_CACHE))
.WithStaticFolder("/", PathHelper.HtmlRootPath, true, m =>
{
m.WithContentCaching(Configs.USE_FILE_CACHE);
m.OnMappingFailed = async (context, info) =>
{
var htmlContents = await File.ReadAllTextAsync(Path.Combine(PathHelper.HtmlRootPath, "index.html"));
context.Response.StatusCode = 200;
await context.SendStringAsync(htmlContents, "text/html", Encoding.UTF8);
};
})
.WithModule(new ActionModule("/", HttpVerbs.Any,
ctx => ctx.SendDataAsync(new { Message = "Error" })));
server.AddCustomMimeType(".dll", "application/octet-stream");
server.AddCustomMimeType(".blat", "application/octet-stream");
server.AddCustomMimeType(".dat", "application/octet-stream");
server.AddCustomMimeType(".json", "application/json");
server.AddCustomMimeType(".wasm", "application/wasm");
server.AddCustomMimeType(".woff", "application/font-woff");
server.AddCustomMimeType(".woff2", "application/font-woff2");
// Listen for state changes.
server.StateChanged += (_, e) => $"WebServer New State - {e.NewState}".Info();
@@ -57,9 +73,9 @@ public class Server
private static void InitializeDatabase()
{
DatabaseHelper.InitializeLocalDatabase(Configs.CARD_DB_NAME,
typeof(Card), typeof(CardBData), typeof(CardDetail));
DatabaseHelper.InitializeLocalDatabase(Configs.MUSIC_DB_NAME,
DatabaseHelper.InitializeLocalDatabase(Configs.SETTINGS.CardDbName,
typeof(Card), typeof(CardBData), typeof(CardDetail), typeof(CardPlayCount));
DatabaseHelper.InitializeLocalDatabase(Configs.SETTINGS.MusicDbName,
typeof(Music), typeof(MusicAou), typeof(MusicExtra));
}
}
+23
View File
@@ -0,0 +1,23 @@
using System.Net;
namespace GCRelayServer;
public class DictEntry
{
public List<EndPoint> EndPoints { get; set; } = new();
public DateTime LastAccessTime { get; set; } = DateTime.Now;
public void AddEndpoint(EndPoint endPoint, bool shouldClear = false)
{
if (shouldClear)
{
EndPoints.Clear();
}
if (EndPoints.Contains(endPoint))
{
return;
}
EndPoints.Add(endPoint);
}
}
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BinarySerializer" Version="8.6.3-alpha" />
<PackageReference Include="NetCoreServer" Version="6.2.0" />
<PackageReference Include="Swan.Logging" Version="6.0.2-beta.69" />
</ItemGroup>
</Project>
+59
View File
@@ -0,0 +1,59 @@
using System.Net;
using Swan.Logging;
namespace GCRelayServer
{
public static class Program
{
public static void Main(string[] args)
{
#if DEBUG
ConsoleLogger.Instance.LogLevel = LogLevel.Debug;
#endif
// UDP server port
var port = 3333;
if (args.Length > 0)
{
port = int.Parse(args[0]);
}
$"UDP server port: {port}".Info();
// Create a new UDP echo server
var server = new RelayServer(IPAddress.Any, port);
// Start the server
"Server starting...".Info();
server.Start();
"Server started".Info();
"Press Enter to stop the server or '!' to restart the server...".Info();
// Perform text input
for (;;)
{
var line = Console.ReadLine();
if (string.IsNullOrEmpty(line))
{
break;
}
// Restart the server
if (line != "!")
{
continue;
}
"Server restarting...".Info();
server.Restart();
"Server restarted".Info();
}
// Stop the server
"Server stopping...".Info();
server.Stop();
"Server stopped, press any key to close".Info();
Console.ReadKey(true);
}
}
}
+57
View File
@@ -0,0 +1,57 @@
using BinarySerialization;
namespace GCRelayServer;
public class RelayPacket
{
[FieldOrder(0)]
[FieldEndianness(Endianness.Big)]
public ushort Magic;
[FieldOrder(1)]
[FieldEndianness(Endianness.Big)]
public ushort RemainingSize;
[FieldOrder(2)]
[FieldEndianness(Endianness.Big)]
public ushort RequestMainType;
[FieldOrder(3)]
[FieldCount(6)]
public byte[] Unknown0 = Array.Empty<byte>();
[FieldOrder(4)]
[FieldEndianness(Endianness.Big)]
public ushort RequestSubType;
[FieldOrder(5)]
[FieldEndianness(Endianness.Big)]
public ushort Unknown1;
[FieldOrder(6)]
[FieldEndianness(Endianness.Big)]
public ushort DataSize;
[FieldOrder(7)]
[FieldEndianness(Endianness.Big)]
public ushort Unknown2;
[FieldOrder(8)]
[FieldEndianness(Endianness.Big)]
public uint MatchingId;
[FieldOrder(9)]
[FieldEndianness(Endianness.Big)]
public uint EntryNo;
[FieldOrder(10)]
[FieldEndianness(Endianness.Big)]
public uint MachineId;
[FieldOrder(11)]
[FieldEndianness(Endianness.Big)]
public uint Unknown3;
[FieldOrder(12)]
public byte[] Data = Array.Empty<byte>();
}
+14
View File
@@ -0,0 +1,14 @@
namespace GCRelayServer;
public static class RelayPacketTypes
{
public const ushort HEART_BEAT = 0xB0;
public const ushort HEART_BEAT_RESPONSE = 0xB1;
public const ushort START_MATCHING = 0xA0;
public const ushort START_MATCHING_RESPONSE = 0xA1;
public const ushort REGISTER_MATCHING = 0xA6;
}
+160
View File
@@ -0,0 +1,160 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using BinarySerialization;
using Swan.Logging;
namespace GCRelayServer;
public class RelayServer : NetCoreServer.UdpServer
{
private ConcurrentDictionary<uint, DictEntry> matchingDictionary = new();
public RelayServer(IPAddress address, int port) : base(address, port) {}
protected override void OnStarted()
{
// Start receive datagrams
ReceiveAsync();
}
protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
{
var serializer = new BinarySerializer();
var inputStream = new MemoryStream(buffer, (int)offset, (int)size, false);
var packet = serializer.Deserialize<RelayPacket>(inputStream);
if (!IsValidPacket(packet))
{
"Received malformed packet!".Warn();
return;
}
$"Received packet from {endpoint}, type is 0x{packet.RequestSubType:X2}".Info();
switch (packet.RequestSubType)
{
case RelayPacketTypes.HEART_BEAT:
{
for (var i = 0; i < 2; i++)
{
SendPacketSingle(endpoint, packet, RelayPacketTypes.HEART_BEAT_RESPONSE);
}
break;
}
case RelayPacketTypes.START_MATCHING:
{
AddEntry(packet.MatchingId, endpoint);
for (var i = 0; i < 2; i++)
{
SendPacketSingle(endpoint, packet, RelayPacketTypes.START_MATCHING_RESPONSE);
}
break;
}
case RelayPacketTypes.REGISTER_MATCHING:
{
var entry = AddEntry(packet.MatchingId, endpoint);
SendPacketToOthers(entry.EndPoints, packet, endpoint);
break;
}
default:
{
var entry = GetEntry(packet.MatchingId);
if (entry is null)
{
break;
}
SendPacketToOthers(entry.EndPoints, packet, endpoint);
break;
}
}
ReceiveAsync();
}
private void SendPacketSingle(EndPoint endpoint, RelayPacket packet, ushort subType)
{
packet.RequestSubType = subType;
var serializer = new BinarySerializer();
var sendStream = new MemoryStream(1024);
serializer.Serialize(sendStream, packet);
$"Send packet to {endpoint}, type is 0x{packet.RequestSubType:X2}".Info();
SendAsync(endpoint, sendStream.GetBuffer(), 0, sendStream.Length);
}
private void SendPacketToOthers(IEnumerable<EndPoint> endPoints, RelayPacket packet, EndPoint owner)
{
if (owner is not IPEndPoint ipEndPoint)
{
"Endpoint is not IP endpoint! This should not happen!".Fatal();
throw new ApplicationException();
}
foreach (var endPoint in endPoints.Where(endPoint => !ipEndPoint.Equals(endPoint)))
{
SendPacketSingle(endPoint, packet, packet.RequestSubType);
}
}
protected override void OnError(SocketError error)
{
$"Relay server caught an error with code {error}".Error();
}
private static bool IsValidPacket(RelayPacket packet)
{
var totalSize = packet.Magic + packet.RemainingSize;
var actualSize = 36 + packet.Data.Length;
if (totalSize != actualSize)
{
return false;
}
return packet.Data.Length == packet.DataSize;
}
private DictEntry AddEntry(uint matchingId, EndPoint endPoint)
{
var now = DateTime.Now;
var entry = matchingDictionary.GetValueOrDefault(matchingId, new DictEntry());
var shouldClear = false;
if (entry.LastAccessTime <= now && now - entry.LastAccessTime >= TimeSpan.FromMinutes(10))
{
$"Entry for matching id {matchingId:X8} has expired! Clients will be cleared!".Info();
shouldClear = true;
}
if (entry.EndPoints.Count >= 4)
{
$"Entry for matching id {matchingId:X8} contains more than 4 clients! Clients will be cleared!".Warn();
shouldClear = true;
}
entry.AddEndpoint(endPoint, shouldClear);
entry.LastAccessTime = DateTime.Now;
matchingDictionary[matchingId] = entry;
return entry;
}
private DictEntry? GetEntry(uint matchingId)
{
var now = DateTime.Now;
if (!matchingDictionary.ContainsKey(matchingId))
{
$"Entry for matching id {matchingId:X8} does not exist!".Warn();
return null;
}
var entry = matchingDictionary[matchingId];
if (entry.LastAccessTime <= now && now - entry.LastAccessTime >= TimeSpan.FromMinutes(10))
{
$"Entry for matching id {matchingId:X8} has expired!".Warn();
return null;
}
entry.LastAccessTime = DateTime.Now;
return entry;
}
}
+11
View File
@@ -0,0 +1,11 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
+12
View File
@@ -0,0 +1,12 @@
<linker>
<assembly fullname="MudBlazor" preserve="all">
<type fullname="MudBlazor.FilterOperator" preserve="all" />
<type fullname="MudBlazor.FilterOperator/String" preserve="all" />
<type fullname="MudBlazor.FilterOperator/Number" preserve="all" />
<type fullname="MudBlazor.FilterOperator/Enum" preserve="all" />
<type fullname="MudBlazor.FilterOperator/Boolean" preserve="all" />
<type fullname="MudBlazor.FilterOperator/DateTime" preserve="all" />
<type fullname="MudBlazor.FilterDefinition`1" preserve="all" />
<type fullname="MudBlazor.Filter" preserve="all" />
</assembly>
</linker>
+81
View File
@@ -0,0 +1,81 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<RunAOTCompilation>False</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<RunAOTCompilation>True</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>
<ItemGroup>
<None Remove="ILLink.Descriptors.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ILLink.Descriptors.xml">
<LogicalName>ILLink.Descriptors.xml</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="GenFu" Version="1.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.6" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.0.11" />
<PackageReference Include="protobuf-net" Version="3.1.17" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharedProject\SharedProject.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\data\avatar.dat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\avatar.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\data\navigator.dat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\navigator.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\data\title.dat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\data\title.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\news.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="Utils" />
</ItemGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31717.71
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MudAdmin", "MudAdmin.csproj", "{D6DF8A15-5B8D-4302-90D2-FC71264B18CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D6DF8A15-5B8D-4302-90D2-FC71264B18CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6DF8A15-5B8D-4302-90D2-FC71264B18CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6DF8A15-5B8D-4302-90D2-FC71264B18CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6DF8A15-5B8D-4302-90D2-FC71264B18CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {58C61F27-917C-435B-8900-9B3CE8F8DCD4}
EndGlobalSection
EndGlobal
@@ -0,0 +1,73 @@
@using SharedProject.common
@using System.Text.RegularExpressions
@inject HttpClient Client
@inject ILogger<ChangePlayerNameDialog> Logger
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
Change Player Name
</MudText>
</TitleContent>
<DialogContent>
<MudForm @bind-IsValid="IsValid">
<MudTextField Value="@Data.CardId" Label="CardId" ReadOnly="true"/>
<MudTextField @bind-Value="Data.PlayerName"
Immediate="true"
Counter="SharedConstants.MAX_PLAYER_NAME_LENGTH"
MaxLength="SharedConstants.MAX_PLAYER_NAME_LENGTH"
Validation="ValidatePlayerName"/>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit" Disabled="@(!IsValid)">Confirm</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public SharedProject.models.User Data { get; set; } = null!;
private string originalName = string.Empty;
private bool IsValid { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
originalName = new string(Data.PlayerName);
}
async Task Submit()
{
Logger.LogInformation("Data is {cardId}, {name}", Data.CardId, Data.PlayerName);
var response = await Client.PostAsJsonAsync("api/Users/SetPlayerName", Data);
var result = await response.Content.ReadFromJsonAsync<bool>();
Logger.LogInformation("SetPlayerName result is {result}", result);
MudDialog.Close(DialogResult.Ok(result));
}
void Cancel()
{
Data.PlayerName = originalName;
MudDialog.Cancel();
}
private static string? ValidatePlayerName(string playerName)
{
const string pattern = @"^[a-zA-Z0-9!?,./\-+:<>_\\@*#&=() ]{1,8}$";
return playerName.Length switch
{
0 => "Player name cannot be empty!",
> 8 => "Player name cannot be longer than 8 characters!",
_ => !Regex.IsMatch(playerName, pattern) ? "Player name contains invalid character!" : null
};
}
}
+57
View File
@@ -0,0 +1,57 @@
@using SharedProject.models
@inject HttpClient Client
@inject ILogger<FavoriteDialog> Logger
<MudDialog>
<TitleContent>
@if (!Data.IsFavorite)
{
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.BookmarkAdd" Color="Color.Secondary"/>
Add to favorite?
</MudText>
}
else
{
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.BookmarkRemove" Color="Color.Secondary"/>
Remove from favorite?
</MudText>
}
</TitleContent>
<DialogContent>
<MudTextField Value="@Data.Title" Label="Song Title" ReadOnly="true"/>
<MudTextField Value="@Data.Artist" Label="Artist Name" ReadOnly="true"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Confirm</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public SongPlayData Data { get; set; } = null!;
[Parameter]
public long CardId { get; set; }
async Task Submit()
{
var postData = new MusicFavoriteData
{
CardId = CardId,
IsFavorite = !Data.IsFavorite,
MusicId = Data.MusicId
};
Logger.LogInformation("Data is {cardId}, {musicId}", CardId, Data.MusicId);
var response = await Client.PostAsJsonAsync("api/UserDetail/SetMusicFavorite", postData);
var result = await response.Content.ReadFromJsonAsync<bool>();
Logger.LogInformation("Favorite result is {result}", result);
MudDialog.Close(DialogResult.Ok(result));
}
void Cancel() => MudDialog.Cancel();
}
+6
View File
@@ -0,0 +1,6 @@
@page "/"
<PageTitle>Index</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Admin page for GC local server</MudText>
<MudText Class="mb-8">Powered by MudBlazor</MudText>
+305
View File
@@ -0,0 +1,305 @@
@page "/user/{CardId:long}"
@using SharedProject.models
@using SharedProject.enums
<PageTitle>User</PageTitle>
<MudContainer>
@if (pageLoading)
{
<MudSkeleton Width="1184px" Height="57px"/>
<MudSkeleton Width="1184px" Height="57px"/>
<MudSkeleton Width="1184px" Height="57px"/>
}
else
{
if (userDetail is null)
{
<MudText Typo="Typo.h3">No Data</MudText>
}
else
{
<MudExpansionPanels>
<MudExpansionPanel Text="Total Result">
<MudList>
<MudListSubheader>Player Name: @userDetail.PlayerName</MudListSubheader>
<MudListItem>Total Score: @userDetail.TotalScore</MudListItem>
<MudListItem>Average Score: @userDetail.AverageScore</MudListItem>
<MudListItem>Played Song Count: @userDetail.PlayedSongCount / @userDetail.TotalSongCount</MudListItem>
<MudListItem>Cleared Stage Count: @userDetail.ClearedStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>No Miss Stage Count: @userDetail.NoMissStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>Full Chain Stage Count: @userDetail.FullChainStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>Perfect Stage Count: @userDetail.PerfectStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S and Above Stage Count: @userDetail.SAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S+ and Above Stage Count: @userDetail.SPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
<MudListItem>S++ and Above Stage Count: @userDetail.SPlusPlusAboveStageCount / @userDetail.TotalStageCount</MudListItem>
</MudList>
</MudExpansionPanel>
<MudExpansionPanel Text="PlayOptions">
<MudStack>
<MudSelect @bind-Value="@playOption.FastSlowIndicator"
Label="FAST/SLOW show setting">
@foreach (var item in Enum.GetValues<PlayOptions.FastSlowIndicator>())
{
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="@playOption.FeverTrance"
Label="FEVER/TRANCE show setting">
@foreach (var item in Enum.GetValues<PlayOptions.FeverTranceShow>())
{
<MudSelectItem Value="@item">@item.GetHelpText()</MudSelectItem>
}
</MudSelect>
<MudStack Row="true">
<MudField Label="Navigator setting">
@NavigatorIdToString(playOption.NavigatorId)
</MudField>
<MudButton Variant="Variant.Text"
OnClick="OnChangeNavigatorButtonClick">
Change Navigator
</MudButton>
</MudStack>
<MudStack Row="true">
<MudField Label="Avatar setting">
@AvatarIdToString(playOption.AvatarId)
</MudField>
<MudButton Variant="Variant.Text"
OnClick="OnChangeAvatarButtonClick">
Change Avatar
</MudButton>
</MudStack>
<MudStack Row="true">
<MudField Label="Title setting">
@TitleIdToString(playOption.TitleId)
</MudField>
<MudButton Variant="Variant.Text"
OnClick="OnChangeTitleButtonClick">
Change Title
</MudButton>
</MudStack>
<MudOverlay @bind-Visible="IsNavigatorOverlayVisible"
DarkBackground="true">
<MudPaper>
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack>
<MudDataGrid T="Navigator"
MultiSelection="false" @bind-SelectedItem="SelectedNavigator"
FixedHeader="true" FixedFooter="true" Height="70vh"
Items="navigatorDictionary.Values" ReadOnly="true" Hover="true"
Sortable="true" Filterable="true" Hideable="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Navigators</MudText>
<MudSpacer/>
</ToolBarContent>
<Columns>
<Column T="Navigator" Field="@nameof(Navigator.Id)"/>
<Column T="Navigator" Field="@nameof(Navigator.IdString)"/>
<Column T="Navigator" Field="@nameof(Navigator.NameEntryString)" Title="Navigator Name"
SortBy="NameEntrySortByFunc"/>
<Column T="Navigator" Field="@nameof(Navigator.ToolTipJp)" Title="Navigator Description (Japanese)"/>
<Column T="Navigator" Field="@nameof(Navigator.ToolTipEn)" Title="Navigator Description (English)"/>
<Column T="Navigator" Field="@nameof(Navigator.Genre)" Title="Navigator Type"/>
</Columns>
<PagerContent>
<MudDataGridPager T="Navigator"/>
</PagerContent>
</MudDataGrid>
<MudText Align="Align.Left">
Currently selected navigator:<br>
@SelectedNavigator
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
OnClick="OnNavigatorOverlayClosed">
Close
</MudButton>
</MudStack>
</MudContainer>
</MudPaper>
</MudOverlay>
<MudOverlay @bind-Visible="IsAvatarOverlayVisible"
DarkBackground="true">
<MudPaper>
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack>
<MudDataGrid T="Avatar"
MultiSelection="false" @bind-SelectedItem="SelectedAvatar"
FixedHeader="true" FixedFooter="true" Height="70vh"
Items="avatarDictionary.Values" ReadOnly="true" Hover="true"
Sortable="true" Filterable="true" Hideable="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Avatars</MudText>
<MudSpacer/>
</ToolBarContent>
<Columns>
<Column T="Avatar" Field="@nameof(Avatar.Id)"/>
<Column T="Avatar" Field="@nameof(Avatar.IdString)"/>
<Column T="Avatar" Field="@nameof(Avatar.FullName)" Title="Avatar Name"/>
<Column T="Avatar" Field="@nameof(Avatar.AcquireMethodJp)" Title="Acquire Method (Japanese)"/>
<Column T="Avatar" Field="@nameof(Avatar.AcquireMethodEn)" Title="Acquire Method (English)"/>
</Columns>
<PagerContent>
<MudDataGridPager T="Avatar"/>
</PagerContent>
</MudDataGrid>
<MudText Align="Align.Left">
Currently selected avatar:<br>
@SelectedAvatar
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
OnClick="OnAvatarOverlayClosed">
Close
</MudButton>
</MudStack>
</MudContainer>
</MudPaper>
</MudOverlay>
<MudOverlay @bind-Visible="IsTitleOverlayVisible"
DarkBackground="true">
<MudPaper>
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack>
<MudDataGrid T="Title"
MultiSelection="false" @bind-SelectedItem="SelectedTitle"
FixedHeader="true" FixedFooter="true" Height="70vh"
Items="titleDictionary.Values" ReadOnly="true" Hover="true"
Sortable="true" Filterable="true" Hideable="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Titles</MudText>
<MudSpacer/>
</ToolBarContent>
<Columns>
<Column T="Title" Field="@nameof(Title.Id)"/>
<Column T="Title" Field="@nameof(Title.IdString)"/>
<Column T="Title" Field="@nameof(Title.NameJp)" Title="Title Name"/>
<Column T="Title" Field="@nameof(Title.UnlockRequirementJp)" Title="Unlock Requirement (Japanese)"/>
<Column T="Title" Field="@nameof(Title.UnlockRequirementEng)" Title="Unlock Requirement (English)"/>
<Column T="Title" Field="@nameof(Title.Type)" Title="Title Type"/>
</Columns>
<PagerContent>
<MudDataGridPager T="Title"/>
</PagerContent>
</MudDataGrid>
<MudText Align="Align.Left">
Currently selected title:<br>
@SelectedTitle
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
OnClick="OnTitleOverlayClosed">
Close
</MudButton>
</MudStack>
</MudContainer>
</MudPaper>
</MudOverlay>
<MudButton Disabled="@isSavingOptions"
OnClick="SaveOptions"
Variant="Variant.Filled"
Color="Color.Info">
@if (isSavingOptions)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/>
<MudText Class="ms-2">Saving...</MudText>
}
else
{
<MudIcon Icon="@Icons.Filled.Save"></MudIcon>
<MudText>Save</MudText>
}
</MudButton>
</MudStack>
</MudExpansionPanel>
<MudExpansionPanel Text="SongPlayData">
<MudDataGrid T="SongPlayData"
Items="@songPlayDataList"
Sortable="true"
Filterable="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Played Songs</MudText>
</ToolBarContent>
<Columns>
<Column T="SongPlayData" Sortable="false" Filterable="false">
<CellTemplate>
<MudButton Variant="Variant.Outlined" Size="Size.Small"
OnClick="@(() => OnShowDetailsClick(context.Item))">
@(context.Item.ShowDetails ? "Hide" : "Show") Song Play Details
</MudButton>
</CellTemplate>
</Column>
<Column T="SongPlayData" Field="@nameof(SongPlayData.IsFavorite)" Sortable="false" Title="Favorite">
<CellTemplate>
<MudToggleIconButton Toggled="@context.Item.IsFavorite"
ToggledChanged="@(() => OnFavoriteToggled(context.Item))"
Icon="@Icons.Material.Filled.FavoriteBorder"
Color="@Color.Secondary"
Title="Add to favorite"
ToggledIcon="@Icons.Material.Filled.Favorite"
ToggledColor="@Color.Secondary"
ToggledTitle="Remove from favorite"/>
</CellTemplate>
</Column>
<Column T="SongPlayData" Field="@nameof(SongPlayData.Title)" Title="Song Title"/>
<Column T="SongPlayData" Field="@nameof(SongPlayData.Artist)" Title="Artist"/>
<Column T="SongPlayData" Field="@nameof(SongPlayData.TotalPlayCount)" Title="Total Play Count"/>
<Column T="SongPlayData" Field="@nameof(SongPlayData.LastPlayTime)" Title="Last Play Time"/>
</Columns>
<ChildRowContent>
@if (context.ShowDetails)
{
<MudTr>
<td colspan="5">
<MudCard Elevation="0">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.body1">Song Play Details</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pa-0">
<MudTable Items="@context.SongPlaySubDataList"
Context="SongPlayDetail"
Elevation="0"
Filter="data => data.ClearState != ClearState.NotPlayed">
<HeaderContent>
<MudTh>Difficulty</MudTh>
<MudTh>Clear State</MudTh>
<MudTh>Play Count</MudTh>
<MudTh>Rating</MudTh>
<MudTh>Score</MudTh>
<MudTh>Max Chain</MudTh>
<MudTh>Last Play Time</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Difficulty">@SongPlayDetail.Difficulty</MudTd>
<MudTd DataLabel="Clear State">
@(SongPlayDetail.Score != 1000000 ? SongPlayDetail.ClearState : ClearState.Perfect)
</MudTd>
<MudTd DataLabel="Play Count">@SongPlayDetail.PlayCount</MudTd>
<MudTd DataLabel="Rating">@CalculateRating(SongPlayDetail.Score)</MudTd>
<MudTd DataLabel="Score">@SongPlayDetail.Score</MudTd>
<MudTd DataLabel="Max Chain">@SongPlayDetail.MaxChain</MudTd>
<MudTd DataLabel="Last Play Time">@SongPlayDetail.LastPlayTime</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
</MudCard>
</td>
</MudTr>
}
</ChildRowContent>
<PagerContent>
<MudDataGridPager T="SongPlayData"/>
</PagerContent>
</MudDataGrid>
</MudExpansionPanel>
</MudExpansionPanels>
}
}
</MudContainer>
+228
View File
@@ -0,0 +1,228 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using ProtoBuf;
using SharedProject.common;
using SharedProject.models;
namespace MudAdmin.Pages;
public partial class User
{
[Inject]
public HttpClient Client { get; set; } = null!;
[Inject]
public IDialogService DialogService { get; set; } = null!;
[Inject]
public ILogger<User> Logger { get; set; } = null!;
private Dictionary<long, Avatar> avatarDictionary = new();
private bool isSavingOptions;
private Dictionary<long, Navigator> navigatorDictionary = new();
private bool pageLoading = true;
private PlayOption playOption = new();
private List<SongPlayData> songPlayDataList = new();
private Dictionary<long, Title> titleDictionary = new();
private UserDetail? userDetail;
[Parameter]
public long CardId { get; set; }
private bool IsTitleOverlayVisible { get; set; }
private bool IsNavigatorOverlayVisible { get; set; }
private bool IsAvatarOverlayVisible { get; set; }
private Title? SelectedTitle { get; set; }
private Avatar? SelectedAvatar { get; set; }
private Navigator? SelectedNavigator { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
userDetail = await Client.GetFromJsonAsync<UserDetail>($"api/UserDetail/{CardId}");
if (userDetail is null)
{
pageLoading = false;
return;
}
songPlayDataList = userDetail.SongPlayDataList ?? new List<SongPlayData>();
playOption = userDetail.PlayOption;
var navigatorStream = await Client.GetStreamAsync(SharedConstants.NAVIGATOR_DAT_URI);
var navigators = Serializer.Deserialize<Navigators>(navigatorStream);
if (navigators?.NavigatorList != null)
{
navigatorDictionary = navigators.NavigatorList.ToDictionary(navigator => (long)navigator.Id);
}
var avatarStream = await Client.GetStreamAsync(SharedConstants.AVATAR_DAT_URI);
var avatars = Serializer.Deserialize<List<Avatar>>(avatarStream);
if (avatars != null)
{
avatarDictionary = avatars.ToDictionary(avatar => (long)avatar.Id);
}
var titleStream = await Client.GetStreamAsync(SharedConstants.TITLE_DAT_URI);
var titles = Serializer.Deserialize<List<Title>>(titleStream);
if (titles != null)
{
titleDictionary = titles.ToDictionary(title => (long)title.Id);
}
SetSelected();
pageLoading = false;
}
private void SetSelected()
{
SelectedTitle = titleDictionary.GetValueOrDefault(playOption.TitleId);
SelectedAvatar = avatarDictionary.GetValueOrDefault(playOption.AvatarId);
SelectedNavigator = navigatorDictionary.GetValueOrDefault(playOption.NavigatorId);
}
private void OnShowDetailsClick(SongPlayData data)
{
data.ShowDetails = !data.ShowDetails;
}
private async Task SaveOptions()
{
isSavingOptions = true;
var postData = new PlayOption
{
CardId = CardId,
FastSlowIndicator = playOption.FastSlowIndicator,
FeverTrance = playOption.FeverTrance,
AvatarId = playOption.AvatarId,
NavigatorId = playOption.NavigatorId,
TitleId = playOption.TitleId
};
var result = await Client.PostAsJsonAsync("api/UserDetail/SetPlayOption", postData);
isSavingOptions = false;
}
private static string CalculateRating(int score)
{
var grade = SharedConstants.Grades.Where(g => g.Score <= score).Select(g => g.Grade).Last();
return grade;
}
private async Task OnFavoriteToggled(SongPlayData data)
{
var options = new DialogOptions
{
CloseOnEscapeKey = false,
DisableBackdropClick = true,
FullWidth = true
};
var parameters = new DialogParameters();
parameters.Add("Data", data);
parameters.Add("CardId", CardId);
var dialog = DialogService.Show<FavoriteDialog>("Favorite", parameters, options);
var result = await dialog.Result;
if (result.Cancelled)
{
return;
}
if ((bool)result.Data)
{
Logger.LogInformation("Changed!");
data.IsFavorite = !data.IsFavorite;
}
}
private Task<IEnumerable<long>> SearchAvatar(string value)
{
var result = string.IsNullOrEmpty(value) ? avatarDictionary.Keys : avatarDictionary.Where(pair => pair.Value.ToString().Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(pair => pair.Key);
return Task.FromResult(result);
}
private Task<IEnumerable<long>> SearchTitle(string value)
{
var result = string.IsNullOrEmpty(value) ? titleDictionary.Keys : titleDictionary.Where(pair => pair.Value.ToString().Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(pair => pair.Key);
return Task.FromResult(result);
}
private Task<IEnumerable<long>> SearchNavigator(string value)
{
var result = string.IsNullOrEmpty(value) ? navigatorDictionary.Keys : navigatorDictionary.Where(pair => pair.Value.ToString().Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(pair => pair.Key);
return Task.FromResult(result);
}
private string AvatarIdToString(long id)
{
return avatarDictionary.ContainsKey(id) ? avatarDictionary[id].ToString() : $"No Data for {id}!";
}
private string NavigatorIdToString(long id)
{
return navigatorDictionary.ContainsKey(id) ? navigatorDictionary[id].ToString() : $"No Data for {id}!";
}
private string TitleIdToString(long id)
{
return titleDictionary.ContainsKey(id) ? titleDictionary[id].ToString() : $"No Data for {id}!";
}
private void OnChangeTitleButtonClick()
{
IsTitleOverlayVisible = true;
}
private void OnChangeNavigatorButtonClick()
{
IsNavigatorOverlayVisible = true;
}
private void OnChangeAvatarButtonClick()
{
IsAvatarOverlayVisible = true;
}
private void OnTitleOverlayClosed()
{
IsTitleOverlayVisible = false;
if (SelectedTitle != null)
{
playOption.TitleId = SelectedTitle.Id;
}
}
private void OnNavigatorOverlayClosed()
{
IsNavigatorOverlayVisible = false;
if (SelectedNavigator != null)
{
playOption.NavigatorId = SelectedNavigator.Id;
}
}
private void OnAvatarOverlayClosed()
{
IsAvatarOverlayVisible = false;
if (SelectedAvatar != null)
{
playOption.AvatarId = SelectedAvatar.Id;
}
}
private static object NameEntrySortByFunc(Navigator navigator)
{
return navigator.NameEntry1?.ToString().ToLowerInvariant() ?? string.Empty;
}
}
+98
View File
@@ -0,0 +1,98 @@
@page "/users"
@using models = SharedProject.models
@inject NavigationManager NavigationManager
@inject HttpClient Client
@inject IDialogService DialogService
@inject ILogger<Users> Logger
<PageTitle>Users</PageTitle>
<MudContainer>
@if (users is null)
{
<MudGrid>
@for (var i = 0; i < 5; i++)
{
<MudItem>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="32px"/>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSkeleton Width="80px" Height="32px"/>
<MudSkeleton Width="147px" Height="28px"/>
</MudCardContent>
<MudCardActions>
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Width="99px" Height="25px" Class="ml-2"/>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
}
else if (users.Count != 0)
{
<MudGrid>
@foreach (var user in users)
{
<MudItem>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">@user.PlayerName</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudIconButton Icon="@Icons.Filled.Edit" Color="Color.Default"
OnClick="() => OnEditPlayerNameClicked(user)"/>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.h6">Card ID</MudText>
<MudText>@user.CardId</MudText>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="() => OnCheckDetailClick(user)">Check detail</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
}
else
{
<MudText Typo="Typo.h3">No Data</MudText>
}
</MudContainer>
@code {
private List<models.User>? users;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
users = await Client.GetFromJsonAsync<List<models.User>>("api/Users") ?? new List<models.User>();
}
private void OnCheckDetailClick(models.User user)
{
NavigationManager.NavigateTo($"user/{user.CardId}");
}
private async Task OnEditPlayerNameClicked(models.User user)
{
var options = new DialogOptions
{
CloseOnEscapeKey = false,
DisableBackdropClick = true,
FullWidth = true
};
var parameters = new DialogParameters();
parameters.Add("Data", user);
var dialog = DialogService.Show<ChangePlayerNameDialog>("Favorite", parameters, options);
var result = await dialog.Result;
}
}
+17
View File
@@ -0,0 +1,17 @@
using System.Text.Json;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudAdmin;
using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseUrl"))
});
builder.Services.AddMudServices();
await builder.Build().RunAsync();
+30
View File
@@ -0,0 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:65283",
"sslPort": 44398
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Mudblazor.Template": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+60
View File
@@ -0,0 +1,60 @@
@inherits LayoutComponentBase
<MudThemeProvider @ref="@MudThemeProvider" @bind-IsDarkMode="@IsDarkMode"/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<MudLayout>
<MudAppBar Elevation="0" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start"
OnClick="@((e) => DrawerToggle())"/>
<MudSpacer/>
<MudTooltip Duration="1000" Text="Powered by MudBlazor">
<MudIconButton Icon="@Icons.Custom.Brands.MudBlazor" Color="Color.Inherit"
Link="https://mudblazor.com/" Target="_blank"/>
</MudTooltip>
<MudTooltip Duration="1000" Text="Github Repository">
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit"
Link="https://github.com/asesidaa/GC-local-server-rewrite/" Target="_blank"/>
</MudTooltip>
<MudTooltip Duration="1000" Text="@(IsDarkMode ? "Switch to Light Mode" : "Switch to DarkMode")">
<MudToggleIconButton @bind-Toggled="@IsDarkMode"
Icon="@Icons.Material.Outlined.DarkMode" Color="Color.Inherit"
ToggledIcon="@Icons.Material.Filled.LightMode"/>
</MudTooltip>
</MudAppBar>
<MudDrawer @bind-Open="drawerOpen" Elevation="1">
<MudDrawerHeader>
<MudText Typo="Typo.h6">GC local server Admin</MudText>
</MudDrawerHeader>
<NavMenu/>
</MudDrawer>
<MudMainContent>
<MudContainer MaxWidth="MaxWidth.Large" Class="my-16 pt-16">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
@code {
bool drawerOpen = true;
public bool IsDarkMode { get; set; }
public MudThemeProvider MudThemeProvider { get; set; } = null!;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
IsDarkMode = await MudThemeProvider.GetSystemPreference();
StateHasChanged();
}
}
void DrawerToggle()
{
drawerOpen = !drawerOpen;
}
}
+4
View File
@@ -0,0 +1,4 @@
<MudNavMenu>
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
<MudNavLink Href="users" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Users</MudNavLink>
</MudNavMenu>
+11
View File
@@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using MudBlazor
@using MudAdmin
@using MudAdmin.Shared
+10
View File
@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"BaseUrl": "https://127.0.0.1"
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>MudAdmin</title>
<base href="/" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

+190 -14
View File
@@ -4,36 +4,212 @@ This is a rewritten version of GC local server, original version is [here](http
Server functionality is basically the same, but the code should be now easier to work with, and can be extended esaily.
Now it supports card registration.
# Usage
## Usage
Download latest release from release page.
Extract anywhere.
If you don't have the modified NesysService.exe, modify the host file, add the following lines
```
127.0.0.1 cert.nesys.jp
127.0.0.1 data.nesys.jp
127.0.0.1 nesys.taito.co.jp
127.0.0.1 fjm170920zero.nesica.net
```
Open mmc.exe, delete any certificate in `LocalMachine/My` or `LocalMachine/Trusted root` named `Taito Arcade Machine CA` (if present, most likely not)
Open exe with **admin privileges** for certificate generating functionalities to work.
Open game using teknoparrot loader (you can find that in discord). The loader should be also opened with **admin privileges**, otherwise it will not work.
Open game using openparrot loader (you can find that in discord). The loader should be also opened with **admin privileges**, otherwise it will not work.
# Config
## Config
The config file is GC-local-server-rewrite.exe.config.
The config file is ~~GC-local-server-rewrite.exe.config~~ config.json
If you are using 4EX data, it should work out of box.
If you are using 4MAX 4.71 data, it should work out of box.
If you are using 4MAX data, first change MusicDBName to bundled music4MAX.db3
If you are using 4MAX 4.65 data, MusicDbName=music4MAX465.db3
To unlock new avatars/navgators/skins/sound effects/titles from 4MAX, change the corresponding count in config.
If you are using 4MAX 4.61 data, first change MusicDbName to bundled music4MAX.db3
You can get the corresponding count in data/boot/*.dat file.
If you are using 4EX 4.52 data, change MusicDbName to bundled music.db3
# Missing functions
To unlock new avatars/navgators/skins/sound effects/titles from other version, change the corresponding count in config.
- [ ] Item/coin comsuming
You can get the corresponding count in data/boot/*.dat file. It is the first 2 bytes (uint16) encoded in big endian.
## Event files
You can now add solo events. In the config file, change the following section
```
"ResponseData": [
{
"FileName": "/event_103_20201125.evt", // This is the file name of the file to be read
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127, // Be sure that NotBeforeUnixTime<= current time <= NotAfterUnixTime
"Md5": "9ef6c4d5ca381583a2d99b626ce06b5e", // File MD5, must match
"Index": 0 // Special value, must be 0 for .evt file, this is the file controlling event
},
{
"FileName": "/event_20201125_reg.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "8e3fe25bf50dcbed13dbb54cc18b1efa",
"Index": 1 // Special value, must be 1 for event_reg.jpg
},
{
"FileName": "/event_20201125_sgreg.png",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "e0abb0503fe0c530d8a68e36994264c6",
"Index": 2 // Special value, must be 2 for event_sreg.jpg, this is the event picture in mode selection
},
{
"FileName": "/news_big_20201125_0.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "4a0f66431f6449279dc046149d1dd882",
"Index": 0 // Special value, must be 0 for first news_big.jpg, this is the picture shown before demo
},
{
"FileName": "/news_big_20201125_2.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "8e3fe25bf50dcbed13dbb54cc18b1efa",
"Index": 2 // Special value, can be 2~9 for 2nd to 9th news_big.jpg, so there can be up to 9 of these
},
{
"FileName": "/news_small_20201125_1.jpg",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "e20135bcd41c98875aec2b52eb9fcd06",
"Index": 1 // Special value, must be 1 for news_small.jpg, this is shown after game over
},
{
"FileName": "/telop_20201125.txt",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "ee228de44d6656a9ec0bb7f1a0ca64e1",
"Index": 0 // Special value, must be 0 for telop, this is the text in scrolling banner
},
{
"FileName": "/event_unlock_20201125.cmp",
"NotBeforeUnixTime": 1335677127,
"NotAfterUnixTime": 1966397127,
"Md5": "623b0f10125cbe19c5394d992930ae8c",
"Index": 8 // Special value, must be 8 for .cmp file
}
]
```
There are several types of data. File name starts with `event` is event config file. Sample
```
event_id = 1
title = "Test event"
comment = ""
open_date = (2021, 10, 12)
close_date = (2022, 12, 24)
open_time = (8, 0, 0)
close_time = (23, 59, 0)
ver = "4.65.00"
play_open_time = (0, 0, 0)
play_close_time = (0, 0, 0)
type = 0
challenge_num = 0
play_mode = 1 // 1 is solo, 0 is online so currently not availabe
select_music_data = (185, 191, 257, 261, 263, 267, 357, 358, 363, 364, 412, 416, 419, 494, 517, 518, 558, 572, 578, 579, 580, 699, 712, 766, 775, 789, 811, 812, 813, 814) // Enabled music
// Reward is not implemented, also everything has already been unlocked
reward0.type = 2
reward0.start = 1
reward0.end = 50
reward0.title = 5159
reward0.avatar = 0
reward0.item = 21
reward0.item_num = 3
reward0.trophy_num = 2
reward0.music = 0
reward0.navigator = 0
```
File name starts with `telop` is the scrolling banner, it's just a plain text file with banner content.
Other files like `option` and `cap` should also be possible, but I have not tested these.
The configured file should be put into the `event` folder, which you can change the position by using jconfig (regedit->event path)
The event files can also be downloaded from the server. Put the event files into `static/wwwroot/event` folder and the game will download the specified event files from the server.
## Missing functions
- [ ] Item/coin consuming
- [ ] Unlocking system (I don't like these two so I just hardcode them, if you are interested you can implement that and add a switch to enable, PRs are welcome)
- [ ] Ranking system (Is this really needed for a local server?)
- [ ] Proper update check response (Now it will just throw 404)
- [x] ~~PlayInfo.php (Now it will just throw 404, I don't know what data it expext)~~
- [x] ~~Online matching (and online matching events)~~ Initial support
# Difficulty unlocking
## Difficulty unlocking
This is processed on client side, so if you like to unlock all difficuties, just use bemani patcher.
This is processed on client side, so if you like to unlock all difficulties, just use Bemani patcher with 4.52 exe (they are compatible).
## Deteled songs
If you see a lot of duplicate "Play Merrily" in the original tab, this is because in the songs db deleted songs are added back.
To enable these, try use the omnimixed version of stage_param.dat. That can fix this issue
## Local network
If your game and server is not on the same computer, try modify in config, change `ServerIp` to your server IP.
From server, open mmc.exe, export certificate named "Taito Arcade Machine CA" and "GC local server" with private key and import to game computer.
The cert "Taito Arcade Machine CA" goes into LocalMachine/My and Trusted root, the other only LocalMachine/My.
## Windows XP
If you are using Windows XP (e.g. using real machine), it will not recognize the generated certificate since it uses SHA256.
You will have to generate the certificates yourself.
The root certificate should have CN=Taito Arcade Machine CA, while the server certificate should have DNS entries for the domains in the host file.
The most important bit, **choose MD5 or SHA1 as signature algorithm**.
# Web interface
A basic web interface for check scores and set options.
If you want to use the interface besides localhost(127.0.0.1), change in appsettings.json:
```
"BaseUrl": "http://192.168.1.1" // Change to your server ip
```
Notice that due to certificate issues, you need to use http for `BaseUrl` and when accessing the web page, otherwise it will show insecure or fail.
If you really want https all along, make sure you have changed the `ServerIp` and have regenerated the certificate.
# Relay server
Relay server is simple, just run the executable. Default port is 3333, you can change it by provide a command line argument, e.g.
```
./GCRelayServer 12345
```
In main server's config.json, change the following fields to specify the relay server:
```
"RelayServer": "127.0.0.1", // Server IP
"RelayPort": 3333, // Server port
```
Currently the server implementation only allow one matching at a time. Notice that if one matching ends abnormally, you will need to restart the main server.
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>SharedProject</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="protobuf-net" Version="3.1.17" />
</ItemGroup>
</Project>
+9
View File
@@ -0,0 +1,9 @@
namespace SharedProject.common;
public record ScoreGradeMap(int Score, string Grade)
{
public override string ToString()
{
return $"{{ Score = {Score}, Grade = {Grade} }}";
}
}
+42
View File
@@ -0,0 +1,42 @@
namespace SharedProject.common;
public static class SharedConstants
{
public const int DIFFICULTY_COUNT = 4;
public const int E_SCORE_THRESHOLD = 0;
public const int D_SCORE_THRESHOLD = 300000;
public const int C_SCORE_THRESHOLD = 500000;
public const int B_SCORE_THRESHOLD = 700000;
public const int A_SCORE_THRESHOLD = 800000;
public const int S_SCORE_THRESHOLD = 900000;
public const int S_PLUS_SCORE_THRESHOLD = 950000;
public const int S_PLUS_PLUS_SCORE_THRESHOLD = 990000;
public const int MAX_PLAYER_NAME_LENGTH = 8;
public const string NAVIGATOR_DAT_URI = "data/navigator.dat";
public const string AVATAR_DAT_URI = "data/avatar.dat";
public const string TITLE_DAT_URI = "data/title.dat";
public static readonly ScoreGradeMap[] Grades =
{
new(E_SCORE_THRESHOLD, "E"),
new(D_SCORE_THRESHOLD, "D"),
new(C_SCORE_THRESHOLD, "C"),
new(B_SCORE_THRESHOLD, "B"),
new(A_SCORE_THRESHOLD, "A"),
new(S_SCORE_THRESHOLD, "S"),
new(S_PLUS_SCORE_THRESHOLD, "S+"),
new(S_PLUS_PLUS_SCORE_THRESHOLD, "S++")
};
}
+11
View File
@@ -0,0 +1,11 @@
namespace SharedProject.enums;
public enum ClearState
{
NotPlayed = 0,
Failed,
Clear,
NoMiss,
FullChain,
Perfect
}
+9
View File
@@ -0,0 +1,9 @@
namespace SharedProject.enums;
public enum Difficulty
{
Simple = 0,
Normal = 1,
Hard = 2,
Extra = 3
}
+25
View File
@@ -0,0 +1,25 @@
namespace SharedProject.enums;
public static class Extensions
{
public static string GetHelpText(this PlayOptions.FastSlowIndicator indicator)
{
return indicator switch
{
PlayOptions.FastSlowIndicator.NotUsed => "Do not show FAST/SLOW indicator",
PlayOptions.FastSlowIndicator.NearAvatar => "Show FAST/SLOW indicator near avatar",
PlayOptions.FastSlowIndicator.NearJudgement => "Show FAST/SLOW indicator near judgement text",
_ => throw new ArgumentOutOfRangeException(nameof(indicator), indicator, null)
};
}
public static string GetHelpText(this PlayOptions.FeverTranceShow show)
{
return show switch
{
PlayOptions.FeverTranceShow.NotUsed => "Do not show FEVER/TRANCE",
PlayOptions.FeverTranceShow.Show => "Show FEVER/TRANCE",
_ => throw new ArgumentOutOfRangeException(nameof(show), show, null)
};
}
}
@@ -0,0 +1,11 @@
using ProtoBuf;
namespace SharedProject.enums;
[ProtoContract]
public enum NavigatorDefaultAvailability
{
NotAvailable = 0,
Available = 1,
AvailableWithVoice = 2,
}
+14
View File
@@ -0,0 +1,14 @@
using ProtoBuf;
namespace SharedProject.enums;
[ProtoContract]
public enum NavigatorGenre
{
Default = 1,
Original = 2,
Game = 3,
Touhou = 4,
Vocaloid = 5,
Collab = 6,
}
+17
View File
@@ -0,0 +1,17 @@
namespace SharedProject.enums;
public static class PlayOptions
{
public enum FastSlowIndicator
{
NotUsed = 3,
NearAvatar = 1,
NearJudgement = 2
}
public enum FeverTranceShow
{
NotUsed = 2,
Show = 1
}
}
+39
View File
@@ -0,0 +1,39 @@
using ProtoBuf;
namespace SharedProject.enums;
[ProtoContract]
public enum TitleUnlockType
{
Invalid = 0,
Default = 1,
Clear = 2,
NoMiss = 3,
FullChain = 4,
SRankSimpleStages = 5,
SRankNormalStages = 6,
SRankHardStages = 7,
SRankExtraStages = 8,
SRankAllDifficulties = 9,
SPlusRankAllDifficulties = 10,
SPlusPlusRankAllDifficulties = 11,
Event = 12,
Prefecture = 13,
ChainMilestone = 14,
Adlibs = 15,
ConsecutiveNoMiss = 16,
ClearsUsingItems = 17,
Avatars = 18,
MultiplayerStarsTotal = 19,
SongSet20 = 20,
SongSet21 = 21,
SongSet22 = 22,
SongSet23 = 23,
SongSet24 = 24,
SongSet25 = 25,
SongSet26 = 26,
ProfileLevel = 27,
Perfect = 28,
OnlineMatching = 29,
Trophies = 30,
}
+32
View File
@@ -0,0 +1,32 @@
using ProtoBuf;
namespace SharedProject.models;
[ProtoContract]
public class Avatar
{
[ProtoMember(1)]
public uint Id { get; set; }
[ProtoMember(2)]
public string? IdString { get; set; }
[ProtoMember(3)]
public string? FullName { get; set; }
[ProtoMember(4)]
public string? Name { get; set; }
[ProtoMember(5)]
public string? Variant { get; set; }
[ProtoMember(6)]
public string? AcquireMethodJp { get; set; }
[ProtoMember(7)]
public string? AcquireMethodEn { get; set; }
public override string ToString() {
return $"{Id}: {FullName}, {AcquireMethodEn}";
}
}
+15
View File
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace SharedProject.models;
public class MusicFavoriteData
{
[JsonPropertyName(nameof(CardId))]
public long CardId { get; set; }
[JsonPropertyName(nameof(MusicId))]
public int MusicId { get; set; }
[JsonPropertyName(nameof(IsFavorite))]
public bool IsFavorite { get; set; }
}
+20
View File
@@ -0,0 +1,20 @@
using ProtoBuf;
namespace SharedProject.models;
[ProtoContract]
public class NameEntry
{
[ProtoMember(1)]
public string? NameWithVariant { get; set; }
[ProtoMember(2)]
public string? NameWithoutVariant{ get; set; }
[ProtoMember(3)]
public string? Variant{ get; set; }
[ProtoMember(4)]
public string? IllustrationCredit{ get; set; }
public override string ToString()
{
return $"{NameWithVariant}, {IllustrationCredit}";
}
}
+44
View File
@@ -0,0 +1,44 @@
using ProtoBuf;
using SharedProject.enums;
namespace SharedProject.models;
[ProtoContract]
public class Navigator
{
[ProtoMember(1)]
public uint Id { get; set; }
[ProtoMember(2)]
public string? IdString { get; set; }
[ProtoMember(3)]
public string? FileName { get; set; }
[ProtoMember(4)]
public NameEntry? NameEntry0 { get; set; }
[ProtoMember(5)]
public NameEntry? NameEntry1 { get; set; }
[ProtoMember(6)]
public NavigatorGenre Genre { get; set; }
[ProtoMember(7)]
public NavigatorDefaultAvailability DefaultAvailability { get; set; }
[ProtoMember(8)]
public string? ToolTipJp { get; set; }
[ProtoMember(9)]
public string? ToolTipEn { get; set; }
[ProtoIgnore]
public string? NameEntryString => NameEntry0?.ToString();
public override string ToString() {
return $"{Id}: {NameEntry1?.NameWithVariant}, {NameEntry1?.IllustrationCredit}";
}
}
+13
View File
@@ -0,0 +1,13 @@
using ProtoBuf;
namespace SharedProject.models;
[ProtoContract]
public class Navigators
{
[ProtoMember(1)]
public int Count { get; set; }
[ProtoMember(2)]
public List<Navigator>? NavigatorList { get; set; }
}
+25
View File
@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
using SharedProject.enums;
namespace SharedProject.models;
public class PlayOption
{
[JsonPropertyName(nameof(CardId))]
public long CardId { get; set; }
[JsonPropertyName(nameof(FastSlowIndicator))]
public PlayOptions.FastSlowIndicator FastSlowIndicator { get; set; }
[JsonPropertyName(nameof(FeverTrance))]
public PlayOptions.FeverTranceShow FeverTrance { get; set; }
[JsonPropertyName(nameof(AvatarId))]
public long AvatarId { get; set; }
[JsonPropertyName(nameof(NavigatorId))]
public long NavigatorId { get; set; }
[JsonPropertyName(nameof(TitleId))]
public long TitleId { get; set; }
}
+40
View File
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using SharedProject.common;
using SharedProject.enums;
namespace SharedProject.models;
public class SongPlayData
{
public string Title { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public int MusicId { get; set; }
public SongPlayDetailData[] SongPlaySubDataList { get; set; } = new SongPlayDetailData[SharedConstants.DIFFICULTY_COUNT];
public bool IsFavorite { get; set; }
public bool ShowDetails { get; set; }
[JsonIgnore]
public int TotalPlayCount
{
get
{
return SongPlaySubDataList.Sum(data => data.PlayCount);
}
}
[JsonIgnore]
public DateTime LastPlayTime
{
get
{
var songPlayDetailData = SongPlaySubDataList.Where(data => data.ClearState != ClearState.NotPlayed)
.MinBy(data => data.LastPlayTime);
return songPlayDetailData?.LastPlayTime ?? DateTime.MaxValue;
}
}
}
@@ -0,0 +1,18 @@
using SharedProject.enums;
namespace SharedProject.models;
public class SongPlayDetailData
{
public int Score { get; set; }
public int PlayCount { get; set; }
public int MaxChain { get; set; }
public Difficulty Difficulty { get; set; }
public ClearState ClearState { get; set; }
public DateTime LastPlayTime { get; set; }
}
+32
View File
@@ -0,0 +1,32 @@
using ProtoBuf;
using SharedProject.enums;
namespace SharedProject.models;
[ProtoContract]
public class Title
{
[ProtoMember(1)]
public uint Id { get; set; }
[ProtoMember(2)]
public string? IdString { get; set; }
[ProtoMember(3)]
public string? NameJp { get; set; }
[ProtoMember(4)]
public string? NameEng { get; set; }
[ProtoMember(5)]
public string? UnlockRequirementJp { get; set; }
[ProtoMember(6)]
public string? UnlockRequirementEng { get; set; }
[ProtoMember(7)]
public TitleUnlockType Type { get; set; }
public override string ToString() {
return $"{Id}: {NameEng}, {UnlockRequirementEng}";
}
}

Some files were not shown because too many files have changed in this diff Show More