mirror of
https://github.com/MikuLeaks/MikuSB.git
synced 2026-06-04 01:43:57 +00:00
enter intro cutscene
This commit is contained in:
125
.editorconfig
Normal file
125
.editorconfig
Normal file
@@ -0,0 +1,125 @@
|
||||
[*.cs]
|
||||
|
||||
# IDE0022: 使用方法的程序块主体
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
|
||||
[*.cs]
|
||||
#### 命名样式 ####
|
||||
|
||||
# 命名规则
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# 符号规范
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# 命名样式
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
csharp_style_var_for_built_in_types = false:silent
|
||||
csharp_style_var_when_type_is_apparent = false:silent
|
||||
csharp_style_var_elsewhere = false:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_indent_labels = one_less_than_current
|
||||
|
||||
[*.vb]
|
||||
#### 命名样式 ####
|
||||
|
||||
# 命名规则
|
||||
|
||||
dotnet_naming_rule.interface_should_be_以_i_开始.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_以_i_开始.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_以_i_开始.style = 以_i_开始
|
||||
|
||||
dotnet_naming_rule.类型_should_be_帕斯卡拼写法.severity = suggestion
|
||||
dotnet_naming_rule.类型_should_be_帕斯卡拼写法.symbols = 类型
|
||||
dotnet_naming_rule.类型_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
|
||||
|
||||
dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.severity = suggestion
|
||||
dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.symbols = 非字段成员
|
||||
dotnet_naming_rule.非字段成员_should_be_帕斯卡拼写法.style = 帕斯卡拼写法
|
||||
|
||||
# 符号规范
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.类型.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.类型.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
||||
dotnet_naming_symbols.类型.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.非字段成员.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.非字段成员.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
||||
dotnet_naming_symbols.非字段成员.required_modifiers =
|
||||
|
||||
# 命名样式
|
||||
|
||||
dotnet_naming_style.以_i_开始.required_prefix = I
|
||||
dotnet_naming_style.以_i_开始.required_suffix =
|
||||
dotnet_naming_style.以_i_开始.word_separator =
|
||||
dotnet_naming_style.以_i_开始.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.帕斯卡拼写法.required_prefix =
|
||||
dotnet_naming_style.帕斯卡拼写法.required_suffix =
|
||||
dotnet_naming_style.帕斯卡拼写法.word_separator =
|
||||
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.帕斯卡拼写法.required_prefix =
|
||||
dotnet_naming_style.帕斯卡拼写法.required_suffix =
|
||||
dotnet_naming_style.帕斯卡拼写法.word_separator =
|
||||
dotnet_naming_style.帕斯卡拼写法.capitalization = pascal_case
|
||||
|
||||
[*.{cs,vb}]
|
||||
end_of_line = crlf
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
370
.gitignore
vendored
Normal file
370
.gitignore
vendored
Normal file
@@ -0,0 +1,370 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
.idea/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
[Ll]aunchSettings.json
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
# *.pubxml
|
||||
# *.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Special Files
|
||||
/SdkServer/Properties
|
||||
/GameServer/OriginalProto
|
||||
*.rar
|
||||
32
Common/Common.csproj
Normal file
32
Common/Common.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
<RootNamespace>MikuSB</RootNamespace>
|
||||
<AssemblyName>MikuCommon</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EastAsianWidth" Version="1.2.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.29.2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.49.1" />
|
||||
<PackageReference Include="SQLitePCLRaw.core" Version="2.1.10" />
|
||||
<PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.10" />
|
||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.172" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
|
||||
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Proto\Proto.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
76
Common/Configuration/ConfigContainer.cs
Normal file
76
Common/Configuration/ConfigContainer.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace MikuSB.Configuration;
|
||||
|
||||
public class ConfigContainer
|
||||
{
|
||||
public HttpServerConfig HttpServer { get; set; } = new();
|
||||
public GameServerConfig GameServer { get; set; } = new();
|
||||
public PathConfig Path { get; set; } = new();
|
||||
public ServerOption ServerOption { get; set; } = new();
|
||||
}
|
||||
|
||||
public class HttpServerConfig
|
||||
{
|
||||
public string BindAddress { get; set; } = "0.0.0.0";
|
||||
public string PublicAddress { get; set; } = "127.0.0.1";
|
||||
public int Port { get; set; } = 21500;
|
||||
|
||||
public string GetDisplayAddress()
|
||||
{
|
||||
return "http" + "://" + PublicAddress + ":" + Port;
|
||||
}
|
||||
|
||||
public string GetBindDisplayAddress()
|
||||
{
|
||||
return "http" + "://" + BindAddress + ":" + Port;
|
||||
}
|
||||
}
|
||||
|
||||
public class GameServerConfig
|
||||
{
|
||||
public string BindAddress { get; set; } = "0.0.0.0";
|
||||
public string PublicAddress { get; set; } = "127.0.0.1";
|
||||
public int Port { get; set; } = 21000;
|
||||
public int KcpAliveMs { get; set; } = 45000;
|
||||
public string DatabaseName { get; set; } = "Miku.db";
|
||||
public string GameServerId { get; set; } = "MikuSB";
|
||||
public string GameServerName { get; set; } = "MikuSB";
|
||||
public string GetDisplayAddress()
|
||||
{
|
||||
return PublicAddress + ":" + Port;
|
||||
}
|
||||
}
|
||||
|
||||
public class PathConfig
|
||||
{
|
||||
public string ResourcePath { get; set; } = "Resources";
|
||||
public string ConfigPath { get; set; } = "Config";
|
||||
public string DatabasePath { get; set; } = "Config/Database";
|
||||
public string HandbookPath { get; set; } = "Config/Handbook";
|
||||
public string LogPath { get; set; } = "Config/Logs";
|
||||
public string DataPath { get; set; } = "Config/Data";
|
||||
}
|
||||
|
||||
public class ServerOption
|
||||
{
|
||||
public string Language { get; set; } = "EN";
|
||||
public string FallbackLanguage { get; set; } = "EN";
|
||||
public string[] DefaultPermissions { get; set; } = ["Admin"];
|
||||
public ServerProfile ServerProfile { get; set; } = new();
|
||||
public bool AutoCreateUser { get; set; } = true;
|
||||
public bool SavePersonalDebugFile { get; set; } = false;
|
||||
public bool AutoSendResponseWhenNoHandler { get; set; } = true;
|
||||
#if DEBUG
|
||||
public bool EnableDebug { get; set; } = true;
|
||||
#else
|
||||
public bool EnableDebug { get; set; } = false;
|
||||
#endif
|
||||
public bool DebugMessage { get; set; } = true;
|
||||
public bool DebugDetailMessage { get; set; } = true;
|
||||
public bool DebugNoHandlerPacket { get; set; } = true;
|
||||
}
|
||||
|
||||
public class ServerProfile
|
||||
{
|
||||
public string Name { get; set; } = "Miku-chan";
|
||||
public int Uid { get; set; } = 80;
|
||||
}
|
||||
75
Common/Configuration/HotfixContainer.cs
Normal file
75
Common/Configuration/HotfixContainer.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MikuSB.Configuration;
|
||||
|
||||
public class HotfixContainer
|
||||
{
|
||||
public bool UseLocalCache { get; set; } = false;
|
||||
public Dictionary<string, HotfixManfiset> Hotfixes { get; set; } = new();
|
||||
public Dictionary<string, string> AesKeys { get; set; } = new ();
|
||||
|
||||
public static string ExtractVersionNumber(string? version)
|
||||
{
|
||||
try
|
||||
{
|
||||
return version == null ? "" : version[..version.IndexOf('_')];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class HotfixManfiset
|
||||
{
|
||||
[JsonPropertyName("Asb")] public AsbData Asb { get; set; } = new();
|
||||
[JsonPropertyName("AsbPreDownload")] public AsbPreDownloadData AsbPreDownload { get; set; } = new();
|
||||
[JsonPropertyName("Audio")] public AudioData Audio { get; set; } = new();
|
||||
[JsonPropertyName("AudioPreDownload")] public AudioPreDownloadData AudioPreDownload { get; set; } = new();
|
||||
[JsonPropertyName("VideoEncrypt")] public VideoEncryptData VideoEncrypt { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AsbData
|
||||
{
|
||||
[JsonPropertyName("android")] public PlatformInfo Android { get; set; } = new();
|
||||
[JsonPropertyName("iphone")] public PlatformInfo Iphone { get; set; } = new();
|
||||
[JsonPropertyName("pc")] public PlatformInfo Pc { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AsbPreDownloadData
|
||||
{
|
||||
[JsonPropertyName("android")] public PlatformEncryptedInfo Android { get; set; } = new();
|
||||
[JsonPropertyName("iphone")] public PlatformEncryptedInfo Iphone { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AudioData
|
||||
{
|
||||
[JsonPropertyName("platform")] public Dictionary<string, string> Platform { get; set; } = new();
|
||||
[JsonPropertyName("revision")] public int Revision { get; set; }
|
||||
}
|
||||
|
||||
public class AudioPreDownloadData
|
||||
{
|
||||
[JsonPropertyName("enable_time")] public long EnableTime { get; set; }
|
||||
[JsonPropertyName("platform")] public Dictionary<string, string> Platform { get; set; } = new();
|
||||
[JsonPropertyName("revision")] public int Revision { get; set; }
|
||||
}
|
||||
|
||||
public class VideoEncryptData
|
||||
{
|
||||
[JsonPropertyName("filename")] public string FileName { get; set; } = "";
|
||||
}
|
||||
|
||||
public class PlatformInfo
|
||||
{
|
||||
[JsonPropertyName("enable_time")] public long EnableTime { get; set; }
|
||||
[JsonPropertyName("revision")] public string Revision { get; set; } = "";
|
||||
[JsonPropertyName("suffix")] public string Suffix { get; set; } = "";
|
||||
}
|
||||
|
||||
public class PlatformEncryptedInfo : PlatformInfo
|
||||
{
|
||||
[JsonPropertyName("encrypt_key")] public string EncryptKey { get; set; } = "";
|
||||
}
|
||||
43
Common/Data/Config/DictionaryConverter.cs
Normal file
43
Common/Data/Config/DictionaryConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MikuSB.Data.Config;
|
||||
|
||||
class IntDictionaryConverter : JsonConverter<Dictionary<int, int>>
|
||||
{
|
||||
public override Dictionary<int, int>? ReadJson(JsonReader reader, Type objectType, Dictionary<int, int>? existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.StartArray)
|
||||
{
|
||||
JArray.Load(reader);
|
||||
return new Dictionary<int, int>();
|
||||
}
|
||||
else if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
var obj = JObject.Load(reader);
|
||||
var dict = new Dictionary<int, int>();
|
||||
|
||||
foreach (var prop in obj.Properties())
|
||||
{
|
||||
if (int.TryParse(prop.Name, out var key))
|
||||
{
|
||||
dict[key] = prop.Value.ToObject<int>();
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
return new Dictionary<int, int>();
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, Dictionary<int, int>? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach (var kv in value)
|
||||
{
|
||||
writer.WritePropertyName(kv.Key.ToString());
|
||||
writer.WriteValue(kv.Value);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
18
Common/Data/ExcelResource.cs
Normal file
18
Common/Data/ExcelResource.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace MikuSB.Data;
|
||||
|
||||
public abstract class ExcelResource
|
||||
{
|
||||
public abstract uint GetId();
|
||||
|
||||
public virtual void Loaded()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Finalized()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void AfterAllDone()
|
||||
{
|
||||
}
|
||||
}
|
||||
5
Common/Data/GameData.cs
Normal file
5
Common/Data/GameData.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace MikuSB.Data;
|
||||
|
||||
public static class GameData
|
||||
{
|
||||
}
|
||||
33
Common/Data/ResourceEntity.cs
Normal file
33
Common/Data/ResourceEntity.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace MikuSB.Data;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class ResourceEntity : Attribute
|
||||
{
|
||||
[Obsolete("No effect")]
|
||||
public ResourceEntity(string fileName, bool isCritical = false, bool isMultifile = false)
|
||||
{
|
||||
if (isMultifile)
|
||||
FileName = new List<string>(fileName.Split(','));
|
||||
else
|
||||
FileName = [fileName];
|
||||
IsCritical = isCritical;
|
||||
}
|
||||
|
||||
|
||||
public ResourceEntity(string fileName, bool isMultifile = false)
|
||||
{
|
||||
if (isMultifile)
|
||||
FileName = new List<string>(fileName.Split(','));
|
||||
else
|
||||
FileName = [fileName];
|
||||
}
|
||||
|
||||
public ResourceEntity(string fileName)
|
||||
{
|
||||
FileName = [fileName];
|
||||
}
|
||||
|
||||
public List<string> FileName { get; private set; }
|
||||
|
||||
[Obsolete("No effect")] public bool IsCritical { get; private set; } // deprecated
|
||||
}
|
||||
169
Common/Data/ResourceManager.cs
Normal file
169
Common/Data/ResourceManager.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.Util;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.Data;
|
||||
|
||||
public class ResourceManager
|
||||
{
|
||||
public static Logger Logger { get; } = new("ResourceManager");
|
||||
public static bool IsLoaded { get; set; }
|
||||
|
||||
public static void LoadGameData()
|
||||
{
|
||||
LoadExcel();
|
||||
}
|
||||
|
||||
public static void LoadExcel()
|
||||
{
|
||||
var classes = Assembly.GetExecutingAssembly().GetTypes(); // Get all classes in the assembly
|
||||
List<ExcelResource> resList = [];
|
||||
|
||||
foreach (var cls in classes.Where(x => x.IsSubclassOf(typeof(ExcelResource))))
|
||||
{
|
||||
var res = LoadSingleExcelResource(cls);
|
||||
if (res != null) resList.AddRange(res);
|
||||
}
|
||||
|
||||
foreach (var cls in resList) cls.AfterAllDone();
|
||||
}
|
||||
|
||||
public static List<T>? LoadSingleExcel<T>(Type cls) where T : ExcelResource, new()
|
||||
{
|
||||
return LoadSingleExcelResource(cls) as List<T>;
|
||||
}
|
||||
|
||||
public static List<ExcelResource>? LoadSingleExcelResource(Type cls)
|
||||
{
|
||||
var attribute = (ResourceEntity?)Attribute.GetCustomAttribute(cls, typeof(ResourceEntity));
|
||||
|
||||
if (attribute == null) return null;
|
||||
var resource = (ExcelResource)Activator.CreateInstance(cls)!;
|
||||
var count = 0;
|
||||
List<ExcelResource> resList = [];
|
||||
foreach (var fileName in attribute.FileName)
|
||||
try
|
||||
{
|
||||
var path = ConfigManager.Config.Path.ResourcePath + "/ExcelOutput/" + fileName;
|
||||
var file = new FileInfo(path);
|
||||
if (!file.Exists)
|
||||
{
|
||||
Logger.Error(I18NManager.Translate("Server.ServerInfo.FailedToReadItem", fileName,
|
||||
I18NManager.Translate("Word.NotFound")));
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = file.OpenText().ReadToEnd();
|
||||
using (var reader = new JsonTextReader(new StringReader(json)))
|
||||
{
|
||||
reader.Read();
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.StartArray:
|
||||
{
|
||||
// array
|
||||
var jArray = JArray.Parse(json);
|
||||
foreach (var item in jArray)
|
||||
{
|
||||
var res = JsonConvert.DeserializeObject(item.ToString(), cls);
|
||||
resList.Add((ExcelResource)res!);
|
||||
((ExcelResource?)res)?.Loaded();
|
||||
count++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case JsonToken.StartObject:
|
||||
{
|
||||
// dictionary
|
||||
var jObject = JObject.Parse(json);
|
||||
foreach (var (_, obj) in jObject)
|
||||
{
|
||||
var instance = JsonConvert.DeserializeObject(obj!.ToString(), cls);
|
||||
|
||||
if (((ExcelResource?)instance)?.GetId() == 0 || (ExcelResource?)instance == null)
|
||||
{
|
||||
// Deserialize as JObject to handle nested dictionaries
|
||||
var nestedObject = JsonConvert.DeserializeObject<JObject>(obj.ToString());
|
||||
|
||||
foreach (var nestedItem in nestedObject ?? [])
|
||||
{
|
||||
var nestedInstance =
|
||||
JsonConvert.DeserializeObject(nestedItem.Value!.ToString(), cls);
|
||||
resList.Add((ExcelResource)nestedInstance!);
|
||||
((ExcelResource?)nestedInstance)?.Loaded();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resList.Add((ExcelResource)instance);
|
||||
((ExcelResource)instance).Loaded();
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource.Finalized();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToReadItem", fileName,
|
||||
I18NManager.Translate("Word.Error")), ex);
|
||||
}
|
||||
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItems", count.ToString(), cls.Name));
|
||||
|
||||
return resList;
|
||||
}
|
||||
|
||||
public static T? LoadCustomFile<T>(string filetype, string filename)
|
||||
{
|
||||
var type = I18NManager.Translate("Word." + filetype);
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", type));
|
||||
FileInfo file = new(ConfigManager.Config.Path.DataPath + $"/{filename}.json");
|
||||
T? customFile = default;
|
||||
if (!file.Exists)
|
||||
{
|
||||
Logger.Warn(I18NManager.Translate("Server.ServerInfo.ConfigMissing", type,
|
||||
$"{ConfigManager.Config.Path.DataPath}/{filename}.json", type));
|
||||
return customFile;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = file.OpenRead();
|
||||
using StreamReader reader2 = new(reader);
|
||||
var text = reader2.ReadToEnd();
|
||||
var json = JsonConvert.DeserializeObject<T>(text);
|
||||
customFile = json;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error in reading " + file.Name, ex);
|
||||
}
|
||||
|
||||
switch (customFile)
|
||||
{
|
||||
case Dictionary<int, int> d:
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItems", d.Count.ToString(), type));
|
||||
break;
|
||||
case Dictionary<int, List<int>> di:
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItems", di.Count.ToString(), type));
|
||||
break;
|
||||
default:
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItem", filetype));
|
||||
break;
|
||||
}
|
||||
|
||||
return customFile;
|
||||
}
|
||||
}
|
||||
181
Common/Database/Account/AccountData.cs
Normal file
181
Common/Database/Account/AccountData.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using MikuSB.Enums.Player;
|
||||
using MikuSB.Util;
|
||||
using MikuSB.Util.Extensions;
|
||||
using MikuSB.Util.Security;
|
||||
using SqlSugar;
|
||||
|
||||
namespace MikuSB.Database.Account;
|
||||
|
||||
[SugarTable("Account")]
|
||||
public class AccountData : BaseDatabaseDataHelper
|
||||
{
|
||||
public string Username { get; set; } = "";
|
||||
public string Password { get; set; } = "";
|
||||
public BanTypeEnum BanType { get; set; }
|
||||
public string Phone { get; set; } = "";
|
||||
|
||||
[SugarColumn(IsJson = true)] public List<PermEnum> Permissions { get; set; } = [];
|
||||
|
||||
[SugarColumn(IsNullable = true)] public string? ComboToken { get; set; }
|
||||
[SugarColumn(IsNullable = true)] public string? DispatchToken { get; set; }
|
||||
|
||||
#region GetAccount
|
||||
|
||||
public static AccountData? GetAccountByUserName(string username)
|
||||
{
|
||||
AccountData? result = null;
|
||||
DatabaseHelper.GetAllInstance<AccountData>()?.ForEach(account =>
|
||||
{
|
||||
if (account.Username == username) result = account;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public static AccountData? GetAccountByUid(int uid, bool force = false)
|
||||
{
|
||||
var result = DatabaseHelper.GetInstance<AccountData>(uid, force);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static AccountData? GetAccountByDispatchToken(string dispatchToken)
|
||||
{
|
||||
AccountData? result = null;
|
||||
DatabaseHelper.GetAllInstance<AccountData>()?.ForEach(account =>
|
||||
{
|
||||
if (account.DispatchToken == dispatchToken) result = account;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public static AccountData? GetAccountByComboToken(string comboToken)
|
||||
{
|
||||
AccountData? result = null;
|
||||
DatabaseHelper.GetAllInstance<AccountData>()?.ForEach(account =>
|
||||
{
|
||||
if (account.ComboToken == comboToken) result = account;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Account
|
||||
|
||||
public static void CreateAccount(string username, int uid, string password)
|
||||
{
|
||||
var newUid = uid;
|
||||
if (uid == 0)
|
||||
{
|
||||
newUid = 1;
|
||||
while (GetAccountByUid(newUid) != null) newUid++;
|
||||
}
|
||||
|
||||
var account = new AccountData
|
||||
{
|
||||
Uid = newUid,
|
||||
Username = username,
|
||||
Password = password,
|
||||
Phone = "123456",
|
||||
Permissions = [.. ConfigManager.Config.ServerOption.DefaultPermissions
|
||||
.Select(perm => Enum.TryParse(perm, out PermEnum result) ? result : (PermEnum?)null)
|
||||
.Where(result => result.HasValue).Select(result => result!.Value)]
|
||||
|
||||
};
|
||||
SetPassword(account, password);
|
||||
|
||||
DatabaseHelper.CreateInstance(account);
|
||||
}
|
||||
|
||||
public static void DeleteAccount(int uid)
|
||||
{
|
||||
if (GetAccountByUid(uid) == null) return;
|
||||
DatabaseHelper.DeleteAllInstance(uid);
|
||||
}
|
||||
|
||||
public static void SetPassword(AccountData account, string password)
|
||||
{
|
||||
if (password.Length > 0)
|
||||
account.Password = Extensions.GetSha256Hash(password);
|
||||
else
|
||||
account.Password = "";
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(AccountData account, string password)
|
||||
=> account.Password == Extensions.GetSha256Hash(password);
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Permission
|
||||
|
||||
public static bool HasPerm(PermEnum[] perms, int uid)
|
||||
{
|
||||
if (uid == (int)ServerEnum.Console) return true;
|
||||
var account = GetAccountByUid(uid);
|
||||
if (account?.Permissions == null) return false;
|
||||
if (account.Permissions.Contains(PermEnum.Admin)) return true;
|
||||
|
||||
return perms.Any(account.Permissions.Contains);
|
||||
}
|
||||
|
||||
public static void AddPerm(PermEnum[] perms, int uid)
|
||||
{
|
||||
if (uid == (int)ServerEnum.Console) return;
|
||||
var account = GetAccountByUid(uid);
|
||||
if (account == null) return;
|
||||
|
||||
account.Permissions ??= [];
|
||||
foreach (var perm in perms)
|
||||
{
|
||||
if (!account.Permissions.Contains(perm))
|
||||
{
|
||||
account.Permissions = [.. account.Permissions, perm];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemovePerm(PermEnum[] perms, int uid)
|
||||
{
|
||||
if (uid == (int)ServerEnum.Console) return;
|
||||
var account = GetAccountByUid(uid);
|
||||
if (account == null) return;
|
||||
if (account.Permissions == null) return;
|
||||
|
||||
foreach (var perm in perms)
|
||||
{
|
||||
if (account.Permissions.Contains(perm))
|
||||
{
|
||||
account.Permissions = account.Permissions.Except([perm]).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CleanPerm(int uid)
|
||||
{
|
||||
if (uid == (int)ServerEnum.Console) return;
|
||||
var account = GetAccountByUid(uid);
|
||||
if (account == null) return;
|
||||
|
||||
account.Permissions = [];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Token
|
||||
|
||||
public string GenerateDispatchToken()
|
||||
{
|
||||
DispatchToken = Crypto.CreateSessionKey(Uid.ToString());
|
||||
DatabaseHelper.UpdateInstance(this);
|
||||
return DispatchToken;
|
||||
}
|
||||
|
||||
public string GenerateComboToken()
|
||||
{
|
||||
ComboToken = Crypto.CreateSessionKey(Uid.ToString());
|
||||
DatabaseHelper.UpdateInstance(this);
|
||||
return ComboToken;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
8
Common/Database/BaseDatabaseDataHelper.cs
Normal file
8
Common/Database/BaseDatabaseDataHelper.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace MikuSB.Database;
|
||||
|
||||
public abstract class BaseDatabaseDataHelper
|
||||
{
|
||||
[SugarColumn(IsPrimaryKey = true)] public int Uid { get; set; }
|
||||
}
|
||||
32
Common/Database/CustomSerializeService.cs
Normal file
32
Common/Database/CustomSerializeService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Newtonsoft.Json;
|
||||
using SqlSugar;
|
||||
|
||||
namespace MikuSB.Database;
|
||||
|
||||
public class CustomSerializeService : ISerializeService
|
||||
{
|
||||
private readonly JsonSerializerSettings _jsonSettings;
|
||||
|
||||
public CustomSerializeService()
|
||||
{
|
||||
_jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore // ignore default values
|
||||
};
|
||||
}
|
||||
|
||||
public string SerializeObject(object value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, _jsonSettings);
|
||||
}
|
||||
|
||||
public T DeserializeObject<T>(string value)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(value)!;
|
||||
}
|
||||
|
||||
public string SugarSerializeObject(object value)
|
||||
{
|
||||
return JsonConvert.SerializeObject(value, _jsonSettings);
|
||||
}
|
||||
}
|
||||
305
Common/Database/DatabaseHelper.cs
Normal file
305
Common/Database/DatabaseHelper.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
using MikuSB.Database.Account;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.Util;
|
||||
using SqlSugar;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MikuSB.Database;
|
||||
|
||||
public class DatabaseHelper
|
||||
{
|
||||
public static Logger logger = new("Database");
|
||||
public static SqlSugarScope? sqlSugarScope;
|
||||
public static readonly ConcurrentDictionary<int, List<BaseDatabaseDataHelper>> UidInstanceMap = [];
|
||||
public static readonly List<int> ToSaveUidList = [];
|
||||
public static long LastSaveTick = DateTime.UtcNow.Ticks;
|
||||
public static Thread? SaveThread;
|
||||
public static bool LoadAccount;
|
||||
public static bool LoadAllData;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.Database")));
|
||||
var f = new FileInfo(ConfigManager.Config.Path.DatabasePath + "/" + ConfigManager.Config.GameServer.DatabaseName);
|
||||
if (!f.Exists && f.Directory != null) f.Directory.Create();
|
||||
|
||||
sqlSugarScope = new SqlSugarScope(new ConnectionConfig
|
||||
{
|
||||
ConnectionString = $"Data Source={f.FullName};",
|
||||
DbType = DbType.Sqlite,
|
||||
IsAutoCloseConnection = true,
|
||||
ConfigureExternalServices = new ConfigureExternalServices
|
||||
{
|
||||
SerializeService = new CustomSerializeService()
|
||||
}
|
||||
});
|
||||
|
||||
InitializeSqlite();
|
||||
|
||||
var baseType = typeof(BaseDatabaseDataHelper);
|
||||
var assembly = typeof(BaseDatabaseDataHelper).Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsSubclassOf(baseType));
|
||||
|
||||
var list = sqlSugarScope.Queryable<AccountData>().ToList();
|
||||
foreach (var inst in list)
|
||||
{
|
||||
if (!UidInstanceMap.TryGetValue(inst.Uid, out var value))
|
||||
{
|
||||
value = [];
|
||||
UidInstanceMap[inst.Uid] = value;
|
||||
}
|
||||
|
||||
value.Add(inst); // add to the map
|
||||
}
|
||||
|
||||
// start dispatch server
|
||||
LoadAccount = true;
|
||||
|
||||
var res = Parallel.ForEach(list, account =>
|
||||
{
|
||||
Parallel.ForEach(types, t =>
|
||||
{
|
||||
if (t == typeof(AccountData)) return; // skip the account data
|
||||
|
||||
try
|
||||
{
|
||||
typeof(DatabaseHelper).GetMethod(nameof(InitializeTable))?.MakeGenericMethod(t)
|
||||
.Invoke(null, [account.Uid]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Database initialization error: ", e);
|
||||
}
|
||||
|
||||
}); // cache the data
|
||||
});
|
||||
|
||||
while (!res.IsCompleted)
|
||||
{
|
||||
}
|
||||
|
||||
LastSaveTick = DateTime.UtcNow.Ticks;
|
||||
|
||||
SaveThread = new Thread(() =>
|
||||
{
|
||||
while (true) CalcSaveDatabase();
|
||||
});
|
||||
SaveThread.Start();
|
||||
|
||||
LoadAllData = true;
|
||||
}
|
||||
|
||||
public static void InitializeTable<T>(int uid) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
var list = sqlSugarScope?.Queryable<T>()
|
||||
.Select(x => x)
|
||||
.Select<T>()
|
||||
.Where(x => x.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
foreach (var inst in list!.Select(instance => (instance as BaseDatabaseDataHelper)!))
|
||||
{
|
||||
if (!UidInstanceMap.TryGetValue(inst.Uid, out var value))
|
||||
{
|
||||
value = [];
|
||||
UidInstanceMap[inst.Uid] = value;
|
||||
}
|
||||
|
||||
value.Add(inst); // add to the map
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitializeSqlite()
|
||||
{
|
||||
var baseType = typeof(BaseDatabaseDataHelper);
|
||||
var assembly = typeof(BaseDatabaseDataHelper).Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsSubclassOf(baseType));
|
||||
foreach (var type in types)
|
||||
typeof(DatabaseHelper).GetMethod("InitializeSqliteTable")?.MakeGenericMethod(type).Invoke(null, null);
|
||||
}
|
||||
|
||||
// DO NOT DEL ReSharper disable once UnusedMember.Global
|
||||
public static void InitializeSqliteTable<T>() where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
sqlSugarScope?.CodeFirst.InitTables<T>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
public static T? GetInstance<T>(int uid, bool forceReload = false) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!forceReload && UidInstanceMap.TryGetValue(uid, out var value))
|
||||
{
|
||||
var instance = value.OfType<T>().FirstOrDefault();
|
||||
if (instance != null) return instance;
|
||||
}
|
||||
var t = sqlSugarScope?.Queryable<T>()
|
||||
.Where(x => x.Uid == uid)
|
||||
.ToList();
|
||||
|
||||
if (t is { Count: > 0 })
|
||||
{
|
||||
var instance = t[0];
|
||||
|
||||
if (!UidInstanceMap.TryGetValue(uid, out var list))
|
||||
{
|
||||
list = new List<BaseDatabaseDataHelper>();
|
||||
UidInstanceMap[uid] = list;
|
||||
}
|
||||
else
|
||||
{
|
||||
list.RemoveAll(i => i is T);
|
||||
}
|
||||
|
||||
list.Add(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Unsupported type", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static T GetInstanceOrCreateNew<T>(int uid) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
var instance = GetInstance<T>(uid);
|
||||
if (instance != null) return instance;
|
||||
|
||||
instance = new T
|
||||
{
|
||||
Uid = uid
|
||||
};
|
||||
CreateInstance(instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static List<T>? GetAllInstance<T>() where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
return sqlSugarScope?.Queryable<T>()
|
||||
.Select(x => x)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Unsupported type", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateInstance<T>(T instance) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
sqlSugarScope?.Updateable(instance).ExecuteCommand();
|
||||
}
|
||||
|
||||
public static void CreateInstance<T>(T instance) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
sqlSugarScope?.Insertable(instance).ExecuteCommand();
|
||||
if (!UidInstanceMap.TryGetValue(instance.Uid, out var value))
|
||||
{
|
||||
value = [];
|
||||
UidInstanceMap[instance.Uid] = value;
|
||||
}
|
||||
value.Add(instance);
|
||||
}
|
||||
|
||||
public static void DeleteInstance<T>(int key) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
sqlSugarScope?.Deleteable<T>().Where(x => x.Uid == key).ExecuteCommand();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("An error occurred while delete the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteAllInstance(int key)
|
||||
{
|
||||
|
||||
var value = UidInstanceMap[key];
|
||||
var baseType = typeof(BaseDatabaseDataHelper);
|
||||
var assembly = typeof(BaseDatabaseDataHelper).Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsSubclassOf(baseType));
|
||||
foreach (var type in types)
|
||||
{
|
||||
var instance = value.Find(x => x.GetType() == type);
|
||||
if (instance != null)
|
||||
typeof(DatabaseHelper).GetMethod("DeleteInstance")?.MakeGenericMethod(type)
|
||||
.Invoke(null, [key]);
|
||||
}
|
||||
|
||||
if (UidInstanceMap.TryRemove(key, out var instances))
|
||||
ToSaveUidList.RemoveAll(x => x == key);
|
||||
}
|
||||
|
||||
// Auto save per 5 min
|
||||
public static void CalcSaveDatabase()
|
||||
{
|
||||
if (LastSaveTick + TimeSpan.TicksPerMinute * 5 > DateTime.UtcNow.Ticks) return;
|
||||
SaveDatabase();
|
||||
}
|
||||
|
||||
public static void SaveDatabase()
|
||||
{
|
||||
try
|
||||
{
|
||||
var prev = DateTime.Now;
|
||||
var list = ToSaveUidList.ToList(); // copy the list to avoid the exception
|
||||
foreach (var uid in list)
|
||||
{
|
||||
var value = UidInstanceMap[uid];
|
||||
var baseType = typeof(BaseDatabaseDataHelper);
|
||||
var assembly = typeof(BaseDatabaseDataHelper).Assembly;
|
||||
var types = assembly.GetTypes().Where(t => t.IsSubclassOf(baseType));
|
||||
foreach (var type in types)
|
||||
{
|
||||
var instance = value.Find(x => x.GetType() == type);
|
||||
if (instance != null)
|
||||
typeof(DatabaseHelper).GetMethod("SaveDatabaseType")?.MakeGenericMethod(type)
|
||||
.Invoke(null, [instance]);
|
||||
}
|
||||
}
|
||||
|
||||
var t = (DateTime.Now - prev).TotalSeconds;
|
||||
logger.Info(I18NManager.Translate("Server.ServerInfo.SaveDatabase",
|
||||
Math.Round(t, 2).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
ToSaveUidList.Clear();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("An error occurred while saving the database", e);
|
||||
}
|
||||
|
||||
LastSaveTick = DateTime.UtcNow.Ticks;
|
||||
}
|
||||
|
||||
// DO NOT DEL ReSharper save database from cache
|
||||
public static void SaveDatabaseType<T>(T instance) where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
sqlSugarScope?.Updateable(instance).ExecuteCommand();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("An error occurred while saving the database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Common/Database/Player/PlayerGameData.cs
Normal file
23
Common/Database/Player/PlayerGameData.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using MikuSB.Common.Util;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.Util.Extensions;
|
||||
using SqlSugar;
|
||||
|
||||
namespace MikuSB.Database.Player;
|
||||
|
||||
[SugarTable("Player")]
|
||||
public class PlayerGameData : BaseDatabaseDataHelper
|
||||
{
|
||||
public string? Name { get; set; } = "";
|
||||
public string? Signature { get; set; } = "MikuPS";
|
||||
public uint Level { get; set; } = 1;
|
||||
public int Exp { get; set; } = 0;
|
||||
public long RegisterTime { get; set; } = Extensions.GetUnixSec();
|
||||
public long LastActiveTime { get; set; }
|
||||
|
||||
public static PlayerGameData? GetPlayerByUid(long uid)
|
||||
{
|
||||
var result = DatabaseHelper.GetInstance<PlayerGameData>((int)uid);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
9
Common/Enums/Language/ProgramLanguageTypeEnum.cs
Normal file
9
Common/Enums/Language/ProgramLanguageTypeEnum.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MikuSB.Enums.Language;
|
||||
|
||||
public enum ProgramLanguageTypeEnum
|
||||
{
|
||||
EN = 0,
|
||||
CHS = 1,
|
||||
CHT = 2,
|
||||
JP = 3
|
||||
}
|
||||
9
Common/Enums/Packet/PacketFraming.cs
Normal file
9
Common/Enums/Packet/PacketFraming.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MikuSB.Enums.Packet;
|
||||
|
||||
public enum PacketFraming
|
||||
{
|
||||
FourByteLittleEndianLength,
|
||||
TwoByteBigEndianLength,
|
||||
Control,
|
||||
Unknown
|
||||
}
|
||||
13
Common/Enums/Player/BanTypeEnum.cs
Normal file
13
Common/Enums/Player/BanTypeEnum.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace MikuSB.Enums.Player;
|
||||
|
||||
public enum BanTypeEnum
|
||||
{
|
||||
None = 0,
|
||||
UseThirdPartySoftware = 1,
|
||||
ThirdPartySoftware = 2,
|
||||
AbnormalLogin = 4,
|
||||
AbnormalAccount = 5,
|
||||
ViolationTermsService = 6,
|
||||
AccountRisk = 7,
|
||||
Unknown = 8
|
||||
}
|
||||
7
Common/Enums/Player/FriendEnum.cs
Normal file
7
Common/Enums/Player/FriendEnum.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MikuSB.Enums.Player;
|
||||
|
||||
public enum ServerEnum
|
||||
{
|
||||
Console = 0,
|
||||
Chat = 1
|
||||
}
|
||||
9
Common/Enums/Player/PermEnum.cs
Normal file
9
Common/Enums/Player/PermEnum.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MikuSB.Enums.Player;
|
||||
|
||||
public enum PermEnum
|
||||
{
|
||||
Trial = 0,
|
||||
Support = 1,
|
||||
Admin = 2,
|
||||
Other = 10
|
||||
}
|
||||
102
Common/Internationalization/I18nManager.cs
Normal file
102
Common/Internationalization/I18nManager.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using MikuSB.Enums.Language;
|
||||
using MikuSB.Internationalization.Message;
|
||||
using MikuSB.Util;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.Internationalization;
|
||||
|
||||
public static class I18NManager
|
||||
{
|
||||
public static Logger Logger = new("I18nManager");
|
||||
|
||||
public static object Language { get; set; } = new LanguageEN();
|
||||
public static Dictionary<string, Dictionary<ProgramLanguageTypeEnum, object>> PluginLanguages { get; } = [];
|
||||
|
||||
public static void LoadLanguage()
|
||||
{
|
||||
var languageStr = "MikuSB.Internationalization.Message.Language" +
|
||||
ConfigManager.Config.ServerOption.Language;
|
||||
var languageType = Type.GetType(languageStr);
|
||||
if (languageType == null)
|
||||
{
|
||||
Logger.Warn("Language not found, fallback to EN");
|
||||
// fallback to English
|
||||
languageType = Type.GetType("MikuSB.Internationalization.Message.LanguageEN")!;
|
||||
}
|
||||
|
||||
var language = Activator.CreateInstance(languageType) ?? throw new Exception("Language not found");
|
||||
Language = language;
|
||||
|
||||
Logger.Info(Translate("Server.ServerInfo.LoadedItem", Translate("Word.Language")));
|
||||
}
|
||||
|
||||
public static void LoadPluginLanguage(Dictionary<string, List<Type>> pluginAssemblies)
|
||||
{
|
||||
foreach (var (pluginName, types) in pluginAssemblies)
|
||||
{
|
||||
var languageType = types.FindAll(x => x.GetCustomAttribute<PluginLanguageAttribute>() != null);
|
||||
if (languageType.Count == 0) // no language to use
|
||||
continue;
|
||||
|
||||
PluginLanguages.Add(pluginName, []);
|
||||
foreach (var type in languageType)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<PluginLanguageAttribute>();
|
||||
if (attr == null) continue;
|
||||
|
||||
var language = Activator.CreateInstance(type);
|
||||
if (language == null) continue;
|
||||
PluginLanguages[pluginName].Add(attr.LanguageType, language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string Translate(string key, params string[] args)
|
||||
{
|
||||
var pluginLangs = PluginLanguages.Values;
|
||||
var langs = (from pluginLang in pluginLangs
|
||||
from o in pluginLang
|
||||
where o.Key == Enum.Parse<ProgramLanguageTypeEnum>(ConfigManager.Config.ServerOption.Language)
|
||||
select o.Value).ToList(); // get all plugin languages
|
||||
langs.Add(Language); // add server language
|
||||
|
||||
var result = langs.Select(lang => GetNestedPropertyValue(lang, key)).OfType<string>().FirstOrDefault() ?? key;
|
||||
|
||||
var index = 0;
|
||||
|
||||
return args.Aggregate(result, (current, arg) => current.Replace("{" + index++ + "}", arg));
|
||||
}
|
||||
|
||||
public static string TranslateAsCertainLang(string langStr, string key, params string[] args)
|
||||
{
|
||||
var languageStr = "MikuSB.Internationalization.Message.Language" +
|
||||
langStr;
|
||||
var languageType = Type.GetType(languageStr) ??
|
||||
Type.GetType("MikuSB.Internationalization.Message.LanguageEN")!;
|
||||
var language = Activator.CreateInstance(languageType) ?? throw new Exception("Language not found");
|
||||
|
||||
List<object> langs = [language];
|
||||
|
||||
var result = langs.Select(lang => GetNestedPropertyValue(lang, key)).OfType<string>().FirstOrDefault() ?? key;
|
||||
|
||||
var index = 0;
|
||||
|
||||
return args.Aggregate(result, (current, arg) => current.Replace("{" + index++ + "}", arg));
|
||||
}
|
||||
|
||||
public static string? GetNestedPropertyValue(object? obj, string propertyName)
|
||||
{
|
||||
foreach (var part in propertyName.Split('.'))
|
||||
{
|
||||
if (obj == null) return null;
|
||||
|
||||
var type = obj.GetType();
|
||||
var property = type.GetProperty(part);
|
||||
if (property == null) return null;
|
||||
|
||||
obj = property.GetValue(obj, null);
|
||||
}
|
||||
|
||||
return (string?)obj;
|
||||
}
|
||||
}
|
||||
527
Common/Internationalization/Message/LanguageCHS.cs
Normal file
527
Common/Internationalization/Message/LanguageCHS.cs
Normal file
@@ -0,0 +1,527 @@
|
||||
namespace MikuSB.Internationalization.Message;
|
||||
|
||||
#region Root
|
||||
|
||||
public class LanguageCHS
|
||||
{
|
||||
public GameTextCHS Game { get; } = new();
|
||||
public ServerTextCHS Server { get; } = new();
|
||||
public WordTextCHS Word { get; } = new(); // a placeholder for the actual word text
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 1
|
||||
|
||||
/// <summary>
|
||||
/// path: Game
|
||||
/// </summary>
|
||||
public class GameTextCHS
|
||||
{
|
||||
public CommandTextCHS Command { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Server
|
||||
/// </summary>
|
||||
public class ServerTextCHS
|
||||
{
|
||||
public WebTextCHS Web { get; } = new();
|
||||
public ServerInfoTextCHS ServerInfo { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Word
|
||||
/// </summary>
|
||||
public class WordTextCHS
|
||||
{
|
||||
public string Rank => "星魂";
|
||||
public string Avatar => "角色";
|
||||
public string Material => "材料";
|
||||
public string Pet => "宠物";
|
||||
public string Relic => "遗器";
|
||||
public string Equipment => "光锥";
|
||||
public string Talent => "行迹";
|
||||
public string Banner => "卡池";
|
||||
public string Activity => "活动";
|
||||
public string CdKey => "兑换码";
|
||||
public string VideoKey => "过场动画密钥";
|
||||
public string Buff => "祝福";
|
||||
public string Miracle => "奇物";
|
||||
public string Unlock => "奢侈品";
|
||||
public string TrainParty => "派对车厢";
|
||||
|
||||
// server info
|
||||
public string Config => "配置文件";
|
||||
public string Language => "语言";
|
||||
public string Log => "日志";
|
||||
public string GameData => "游戏数据";
|
||||
public string Cache => "资源缓存";
|
||||
public string CustomData => "自定义数据";
|
||||
public string Database => "数据库";
|
||||
public string Command => "命令";
|
||||
public string SSL => "SSL";
|
||||
public string Ec2b => "Ec2b";
|
||||
public string SdkServer => "Web服务器";
|
||||
public string Handler => "包处理器";
|
||||
public string Dispatch => "全局分发";
|
||||
public string Game => "游戏";
|
||||
public string Handbook => "手册";
|
||||
public string NotFound => "未找到";
|
||||
public string Error => "错误";
|
||||
public string FloorInfo => "区域文件";
|
||||
public string FloorGroupInfo => "区域组文件";
|
||||
public string FloorMissingResult => "传送与世界生成";
|
||||
public string FloorGroupMissingResult => "传送、怪物战斗与世界生成";
|
||||
public string Mission => "任务";
|
||||
public string MissionInfo => "任务文件";
|
||||
public string SubMission => "子任务";
|
||||
public string SubMissionInfo => "子任务文件";
|
||||
public string MazeSkill => "角色秘技";
|
||||
public string MazeSkillInfo => "角色秘技文件";
|
||||
public string Dialogue => "模拟宇宙事件";
|
||||
public string DialogueInfo => "模拟宇宙事件文件";
|
||||
public string Performance => "剧情操作";
|
||||
public string PerformanceInfo => "剧情操作文件";
|
||||
public string RogueChestMap => "模拟宇宙地图";
|
||||
public string RogueChestMapInfo => "模拟宇宙地图文件";
|
||||
public string ChessRogueRoom => "模拟宇宙DLC";
|
||||
public string ChessRogueRoomInfo => "模拟宇宙DLC文件";
|
||||
public string SummonUnit => "秘技生成";
|
||||
public string SummonUnitInfo => "秘技生成文件";
|
||||
public string RogueTournRoom => "差分宇宙";
|
||||
public string RogueTournRoomInfo => "差分宇宙房间文件";
|
||||
public string TypesOfRogue => "类型的模拟宇宙";
|
||||
public string RogueMagicRoom => "不可知域";
|
||||
public string RogueMagicRoomInfo => "不可知域房间文件";
|
||||
public string RogueDiceSurface => "骰面效果";
|
||||
public string RogueDiceSurfaceInfo => "骰面效果文件";
|
||||
public string AdventureModifier => "AdventureModifier";
|
||||
public string AdventureModifierInfo => "AdventureModifier文件";
|
||||
public string RogueMapGen => "RogueMapGen文件";
|
||||
public string RogueMiracleGroup => "RogueMiracleGroup文件";
|
||||
public string RogueMiracleEffectGen => "RogueMiracleEffectGen文件";
|
||||
|
||||
public string DatabaseAccount => "数据库账号";
|
||||
public string Tutorial => "教程";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 2
|
||||
|
||||
#region GameText
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command
|
||||
/// </summary>
|
||||
public class CommandTextCHS
|
||||
{
|
||||
public NoticeTextCHS Notice { get; } = new();
|
||||
|
||||
public GenderTextCHS Gender { get; } = new();
|
||||
public AvatarTextCHS Avatar { get; } = new();
|
||||
public AnnounceTextCHS Announce { get; } = new();
|
||||
public BanTextCHS Ban { get; } = new();
|
||||
public GiveTextCHS Give { get; } = new();
|
||||
public GiveAllTextCHS GiveAll { get; } = new();
|
||||
public LineupTextCHS Lineup { get; } = new();
|
||||
public HelpTextCHS Help { get; } = new();
|
||||
public KickTextCHS Kick { get; } = new();
|
||||
public MissionTextCHS Mission { get; } = new();
|
||||
public RelicTextCHS Relic { get; } = new();
|
||||
public ReloadTextCHS Reload { get; } = new();
|
||||
public RogueTextCHS Rogue { get; } = new();
|
||||
public SceneTextCHS Scene { get; } = new();
|
||||
public UnlockAllTextCHS UnlockAll { get; } = new();
|
||||
public MailTextCHS Mail { get; } = new();
|
||||
public RaidTextCHS Raid { get; } = new();
|
||||
public AccountTextCHS Account { get; } = new();
|
||||
public UnstuckTextCHS Unstuck { get; } = new();
|
||||
public SetlevelTextCHS Setlevel { get; } = new();
|
||||
public PermissionTextCHS Permission { get; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServerText
|
||||
|
||||
/// <summary>
|
||||
/// path: Server.Web
|
||||
/// </summary>
|
||||
public class WebTextCHS
|
||||
{
|
||||
public string Maintain => "服务器正在维修, 请稍后尝试。";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Server.ServerInfo
|
||||
/// </summary>
|
||||
public class ServerInfoTextCHS
|
||||
{
|
||||
public string Shutdown => "关闭中…";
|
||||
public string CancelKeyPressed => "已按下取消键 (Ctrl + C), 服务器即将关闭…";
|
||||
public string StartingServer => "正在启动 MikuSB";
|
||||
public string CurrentVersion => "当前服务端支持的版本: {0}";
|
||||
public string InvalidVersion => "当前为不受支持的游戏版本 {0}\n请更新游戏到 {1}";
|
||||
public string LoadingItem => "正在加载 {0}…";
|
||||
public string GeneratingItem => "正在生成 {0}…";
|
||||
public string WaitingItem => "正在等待进程 {0} 完成…";
|
||||
public string RegisterItem => "注册了 {0} 个 {1}。";
|
||||
public string FailedToLoadItem => "加载 {0} 失败。";
|
||||
public string NewClientSecretKey => "客户端密钥不存在, 正在生成新的客户端密钥。";
|
||||
public string FailedToInitializeItem => "初始化 {0} 失败。";
|
||||
public string FailedToReadItem => "读取 {0} 失败, 文件{1}";
|
||||
public string GeneratedItem => "已生成 {0}。";
|
||||
public string LoadedItem => "已加载 {0}。";
|
||||
public string LoadedItems => "已加载 {0} 个 {1}。";
|
||||
public string ServerRunning => "{0} 服务器正在监听 {1}";
|
||||
public string ServerStarted => "启动完成!用时 {0}s, 击败了99%的用户, 输入 ‘help’ 来获取命令帮助"; // 玩梗, 考虑英语版本将其本土化
|
||||
public string MissionEnabled => "任务系统已启用, 此功能仍在开发中, 且可能不会按预期工作, 如果遇见任何bug, 请汇报给开发者。";
|
||||
public string KeyStoreError => "SSL证书不存在, 已关闭SSL功能。";
|
||||
public string CacheLoadSkip => "已跳过缓存加载。";
|
||||
|
||||
public string ConfigMissing => "{0} 缺失, 请检查你的资源文件夹: {1}, {2} 可能不能使用。";
|
||||
public string UnloadedItems => "卸载了所有 {0}。";
|
||||
public string SaveDatabase => "已保存数据库, 用时 {0}s";
|
||||
public string WaitForAllDone => "现在还不可以进入游戏, 请等待所有项目加载完成后再试";
|
||||
|
||||
public string UnhandledException => "发生未经处理的异常: {0}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 3
|
||||
|
||||
#region CommandText
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Notice
|
||||
/// </summary>
|
||||
public class NoticeTextCHS
|
||||
{
|
||||
public string PlayerNotFound => "未找到玩家!";
|
||||
public string InvalidArguments => "无效的参数!";
|
||||
public string NoPermission => "你没有权限这么做!";
|
||||
public string CommandNotFound => "未找到命令! 输入 '/help' 来获取帮助";
|
||||
public string TargetOffline => "目标 {0}({1}) 离线了!清除当前目标";
|
||||
public string TargetFound => "找到目标 {0}({1}), 下一次命令将默认对其执行";
|
||||
public string TargetNotFound => "未找到目标 {0}!";
|
||||
public string InternalError => "在处理命令时发生了内部错误: {0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Gender
|
||||
/// </summary>
|
||||
public class GenderTextCHS
|
||||
{
|
||||
public string Desc => "切换主角的性别";
|
||||
public string Usage => "用法: /gender [man/woman]";
|
||||
public string GenderNotSpecified => "性别不存在!";
|
||||
public string GenderChanged => "性别已更改!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.UnlockAll
|
||||
/// </summary>
|
||||
public class UnlockAllTextCHS
|
||||
{
|
||||
public string Desc =>
|
||||
"解锁所有在类别内的对象\n" +
|
||||
"使用 /unlockall mission 以完成所有任务, 使用后会被踢出, 重新登录后可能会被教程卡住, 请谨慎使用\n" +
|
||||
"使用 /unlockall tutorial 以解锁所有教程, 使用后会被踢出, 用于部分界面卡住无法行动的情况\n" +
|
||||
"使用 /unlockall rogue 以解锁所有类型模拟宇宙, 使用后会被踢出, 建议与 /unlockall tutorial 搭配使用以获取更好效果";
|
||||
public string Usage => "用法: /unlockall [mission/tutorial/rogue]";
|
||||
public string UnlockedAll => "已解锁/完成所有{0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Avatar
|
||||
/// </summary>
|
||||
public class AvatarTextCHS
|
||||
{
|
||||
public string Desc => "设定玩家已有角色的属性, -1为所有已拥有角色";
|
||||
public string Usage =>
|
||||
"用法: /avatar talent [角色ID/-1] [行迹等级]\n" +
|
||||
"用法: /avatar rank [角色ID/-1] [星魂]\n" +
|
||||
"用法: /avatar level [角色ID/-1] [角色等级]";
|
||||
public string InvalidLevel => "{0} 等级无效!";
|
||||
public string AllAvatarsLevelSet => "已将全部角色 {0} 等级设置为 {1}.";
|
||||
public string AvatarLevelSet => "已将 {0} 角色 {1} 等级设置为 {2}.";
|
||||
public string AvatarNotFound => "角色不存在!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Give
|
||||
/// </summary>
|
||||
public class GiveTextCHS
|
||||
{
|
||||
public string Desc => "给予玩家物品";
|
||||
public string Usage => "用法: /give [物品ID] l[等级] x[数量] r[叠影]";
|
||||
public string ItemNotFound => "未找到物品!";
|
||||
public string GiveItem => "已给予 {0} {1} 个物品 {2}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.GiveAll
|
||||
/// </summary>
|
||||
public class GiveAllTextCHS
|
||||
{
|
||||
public string Desc => "给予玩家全部指定类型的物品";
|
||||
public string Usage =>
|
||||
"用法: /giveall avatar r[星魂] l[等级]\n" +
|
||||
"用法: /giveall material x[数量]\n" +
|
||||
"用法: /giveall equipment r[叠影] l[等级] x[数量]\n" +
|
||||
"用法: /giveall relic x[数量]\n" +
|
||||
"用法: /giveall unlock\n" +
|
||||
"用法: /giveall train\n" +
|
||||
"用法: /giveall path";
|
||||
public string GiveAllItems => "已给予所有 {0}, 各 {1} 个.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Lineup
|
||||
/// </summary>
|
||||
public class LineupTextCHS
|
||||
{
|
||||
public string Desc => "管理玩家的队伍信息";
|
||||
public string Usage =>
|
||||
"用法: /lineup mp\n" +
|
||||
"用法: /lineup sp\n" +
|
||||
"用法: /lineup heal";
|
||||
public string GainedMp => "成功恢复秘技点!";
|
||||
public string GainedSp => "成功恢复能量!";
|
||||
public string HealedAllAvatars => "成功治愈当前队伍中的所有角色!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Help
|
||||
/// </summary>
|
||||
public class HelpTextCHS
|
||||
{
|
||||
public string Desc => "显示帮助信息";
|
||||
public string Usage =>
|
||||
"用法: /help\n" +
|
||||
"用法: /help [命令]";
|
||||
public string Commands => "命令: ";
|
||||
public string CommandPermission => "所需权限: ";
|
||||
public string CommandAlias => "命令别名: ";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Kick
|
||||
/// </summary>
|
||||
public class KickTextCHS
|
||||
{
|
||||
public string Desc => "踢出玩家";
|
||||
public string Usage => "用法: /kick";
|
||||
public string PlayerKicked => "玩家 {0} 已被踢出!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Mission
|
||||
/// </summary>
|
||||
public class MissionTextCHS
|
||||
{
|
||||
public string Desc =>
|
||||
"管理玩家的任务\n" +
|
||||
"使用 pass 完成当前正在进行的所有任务, 此命令易造成严重卡顿, 请尽量使用 /mission finish 替代\n" +
|
||||
"使用 finish [子任务ID] 完成指定子任务, 请浏览 handbook 来获取子任务ID\n" +
|
||||
"使用 finishmain [主任务ID] 完成指定主任务, 请浏览 handbook 来获取主任务ID\n" +
|
||||
"使用 running [-all] 获取正在追踪的任务, 增加'-all'则显示所有正在进行的任务以及可能卡住的任务, 使用后可能会出现较长任务列表, 请注意甄别\n" +
|
||||
"使用 reaccept [主任务ID] 可重新进行指定主任务, 请浏览 handbook 来获取主任务ID";
|
||||
public string Usage =>
|
||||
"用法: /mission pass\n" +
|
||||
"用法: /mission finish [子任务ID]\n" +
|
||||
"用法: /mission running [-all]\n" +
|
||||
"用法: /mission reaccept [主任务ID]\n" +
|
||||
"用法: /mission finishmain [主任务ID]";
|
||||
public string AllMissionsFinished => "所有任务已完成!";
|
||||
public string AllRunningMissionsFinished => "共 {0} 个进行中的任务已完成!";
|
||||
public string MissionFinished => "任务 {0} 已完成!";
|
||||
public string InvalidMissionId => "无效的任务ID!";
|
||||
public string NoRunningMissions => "没有正在进行的任务!";
|
||||
public string RunningMissions => "正在进行的任务: ";
|
||||
public string PossibleStuckMissions => "可能卡住的任务: ";
|
||||
public string MainMission => "主任务";
|
||||
public string MissionReAccepted => "重新接受任务 {0}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Relic
|
||||
/// </summary>
|
||||
public class RelicTextCHS
|
||||
{
|
||||
public string Desc => "管理玩家的遗器, 等级限制: 1 ≤ 等级 ≤ 9999";
|
||||
public string Usage => "用法: /relic [遗器ID] [主词条ID] [ID1:等级] [ID2:等级] l[等级] x[数量]";
|
||||
public string RelicNotFound => "遗器不存在!";
|
||||
public string InvalidMainAffixId => "主词条ID无效!";
|
||||
public string InvalidSubAffixId => "副词条ID无效!";
|
||||
public string RelicGiven => "给予玩家 {0} {1} 个遗器 {2}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Reload
|
||||
/// </summary>
|
||||
public class ReloadTextCHS
|
||||
{
|
||||
public string Desc => "重新加载指定的配置";
|
||||
public string Usage => "用法: /reload [banner/activity]";
|
||||
public string ConfigReloaded => "配置 {0} 已重新加载!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Rogue
|
||||
/// </summary>
|
||||
public class RogueTextCHS
|
||||
{
|
||||
public string Desc => "管理模拟宇宙数据, -1意为所有已拥有祝福, buff获取祝福, enhance强化祝福";
|
||||
public string Usage =>
|
||||
"用法: /rogue money [宇宙碎片数量]\n" +
|
||||
"用法: /rogue buff [祝福ID/-1]\n" +
|
||||
"用法: /rogue miracle [奇物ID]\n" +
|
||||
"用法: /rogue enhance [祝福ID/-1]\n" +
|
||||
"用法: /rogue unstuck - 脱离事件";
|
||||
public string PlayerGainedMoney => "已获得 {0} 宇宙碎片.";
|
||||
public string PlayerGainedAllItems => "已获得所有{0}.";
|
||||
public string PlayerGainedItem => "已获得{0} {1}.";
|
||||
public string PlayerEnhancedBuff => "已强化祝福 {0}.";
|
||||
public string PlayerEnhancedAllBuffs => "已强化所有祝福.";
|
||||
public string PlayerUnstuck => "已脱离事件.";
|
||||
public string NotFoundItem => "未找到 {0}!";
|
||||
public string PlayerNotInRogue => "玩家不在模拟宇宙中!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Scene
|
||||
/// </summary>
|
||||
public class SceneTextCHS
|
||||
{
|
||||
public string Desc =>
|
||||
"管理玩家场景\n" +
|
||||
"使用 PlaneId 默认进入指定场景\n" +
|
||||
"使用 group 来获取组, 使用 prop 来设置道具状态, 在 PropStateEnum 获取状态列表\n" +
|
||||
"使用 unlockall 来解锁场景内所有道具(open状态), 可能导致游戏加载卡条, 使用 /scene reset 解决\n" +
|
||||
"使用 reload 来重新加载当前场景, 并回到初始位置\n" +
|
||||
"使用 reset 来重置指定场景所有道具状态";
|
||||
public string Usage =>
|
||||
"用法: /scene [PlaneId]\n" +
|
||||
"用法: /scene cur\n" +
|
||||
"用法: /scene reload\n" +
|
||||
"用法: /scene group\n" +
|
||||
"用法: /scene unlockall\n" +
|
||||
"用法: /scene reset [PlaneId]" +
|
||||
"用法: /scene prop [组ID] [道具ID] [状态]\n" +
|
||||
"用法: /scene remove [实体ID]\n";
|
||||
|
||||
public string LoadedGroups => "已加载组: {0}.";
|
||||
public string PropStateChanged => "道具: {0} 的状态已设置为 {1}.";
|
||||
public string PropNotFound => "未找到道具!";
|
||||
public string EntityRemoved => "实体 {0} 已被移除.";
|
||||
public string EntityNotFound => "未找到实体!";
|
||||
public string AllPropsUnlocked => "所有道具已解锁!";
|
||||
public string SceneChanged => "已进入场景 {0}.";
|
||||
public string SceneReloaded => "场景已重新加载!";
|
||||
public string SceneReset => "已重置场景 {0} 中所有道具状态!";
|
||||
public string CurrentScene => "当前场景 EntryId: {0}, PlaneId: {1}, FloorId: {2}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Mail
|
||||
/// </summary>
|
||||
public class MailTextCHS
|
||||
{
|
||||
public string Desc => "发送邮件";
|
||||
public string Usage => "用法: /mail [发送名称] [标题] [内容] [ID1:数量,ID2:数量]";
|
||||
public string MailSent => "邮件已发送!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Raid
|
||||
/// </summary>
|
||||
public class RaidTextCHS
|
||||
{
|
||||
public string Desc => "管理玩家的任务临时场景";
|
||||
public string Usage => "用法: /raid leave";
|
||||
public string Leaved => "已离开临时场景!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Account
|
||||
/// </summary>
|
||||
public class AccountTextCHS
|
||||
{
|
||||
public string Desc => "管理数据库账号";
|
||||
public string Usage =>
|
||||
"用法: /account create [用户名] [UID] [密码]\n" +
|
||||
"用法: /account delete [UID]";
|
||||
public string InvalidUid => "UID无效!";
|
||||
public string InvalidAccount => "账号 {0} 无效!";
|
||||
public string CreateSuccess => "账号 {0} 创建成功!";
|
||||
public string DeleteSuccess => "账号 {0} 删除成功!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Announce
|
||||
/// </summary>
|
||||
public class AnnounceTextCHS
|
||||
{
|
||||
public string Desc => "发送弹窗公告";
|
||||
public string Usage => "用法: /announce [Text] [Color]";
|
||||
public string SendSuccess => "发送成功!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Ban
|
||||
/// </summary>
|
||||
public class BanTextCHS
|
||||
{
|
||||
public string Desc => "封禁或解封用户";
|
||||
public string Usage => "用法: /ban [add/delete]";
|
||||
public string BanSuccess => "账号已封禁!";
|
||||
public string UnBanSuccess => "账号已解封!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Unstuck
|
||||
/// </summary>
|
||||
public class UnstuckTextCHS
|
||||
{
|
||||
public string Desc => "将玩家传送回默认场景";
|
||||
public string Usage => "用法: /unstuck [UID]";
|
||||
public string UnstuckSuccess => "已成功将该玩家传送回默认场景.";
|
||||
public string UidNotExist => "该UID不存在!";
|
||||
public string PlayerIsOnline => "该玩家目前在线上!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Setlevel
|
||||
/// </summary>
|
||||
public class SetlevelTextCHS
|
||||
{
|
||||
public string Desc => "设定玩家等级";
|
||||
public string Usage => "用法: /setlevel [等级]";
|
||||
public string SetlevelSuccess => "等级设定成功!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Permission
|
||||
/// </summary>
|
||||
public class PermissionTextCHS
|
||||
{
|
||||
public string Desc => "管理玩家权限";
|
||||
public string Usage =>
|
||||
"用法: /permission add [权限]\n" +
|
||||
"用法: /permission remove [权限]\n" +
|
||||
"用法: /permission clean [权限]";
|
||||
public string InvalidPerm => "权限 {0} 不存在!";
|
||||
public string Added => "已添加权限 {0} 到玩家 {1}!";
|
||||
public string Removed => "已移除玩家 {0} 的权限 {1}!";
|
||||
public string Cleaned => "已清除玩家 {0} 的所有权限!";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
530
Common/Internationalization/Message/LanguageCHT.cs
Normal file
530
Common/Internationalization/Message/LanguageCHT.cs
Normal file
@@ -0,0 +1,530 @@
|
||||
namespace MikuSB.Internationalization.Message;
|
||||
|
||||
#region Root
|
||||
|
||||
public class LanguageCHT
|
||||
{
|
||||
public GameTextCHT Game { get; } = new();
|
||||
public ServerTextCHT Server { get; } = new();
|
||||
public WordTextCHT Word { get; } = new(); // a placeholder for the actual word text
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 1
|
||||
|
||||
/// <summary>
|
||||
/// path: Game
|
||||
/// </summary>
|
||||
public class GameTextCHT
|
||||
{
|
||||
public CommandTextCHT Command { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Server
|
||||
/// </summary>
|
||||
public class ServerTextCHT
|
||||
{
|
||||
public WebTextCHT Web { get; } = new();
|
||||
public ServerInfoTextCHT ServerInfo { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Word
|
||||
/// </summary>
|
||||
public class WordTextCHT
|
||||
{
|
||||
public string Rank => "星魂";
|
||||
public string Avatar => "角色";
|
||||
public string Material => "材料";
|
||||
public string Pet => "寵物";
|
||||
public string Relic => "遺器";
|
||||
public string Equipment => "光錐";
|
||||
public string Talent => "行跡";
|
||||
public string Banner => "卡池";
|
||||
public string Activity => "活動";
|
||||
public string CdKey => "兌換碼";
|
||||
public string VideoKey => "過場動畫金鑰";
|
||||
public string Buff => "祝福";
|
||||
public string Miracle => "奇物";
|
||||
public string Unlock => "奢侈品";
|
||||
public string TrainParty => "派對車廂";
|
||||
|
||||
// server info
|
||||
public string Config => "配置文件";
|
||||
public string Language => "語言";
|
||||
public string Log => "日誌";
|
||||
public string GameData => "遊戲數據";
|
||||
public string Cache => "資源緩存";
|
||||
public string CustomData => "自定義數據";
|
||||
public string Database => "數據庫";
|
||||
public string Command => "命令";
|
||||
public string SSL => "SSL";
|
||||
public string Ec2b => "Ec2b";
|
||||
public string SdkServer => "Web服務器";
|
||||
public string Handler => "包處理器";
|
||||
public string Dispatch => "全局分發";
|
||||
public string Game => "遊戲";
|
||||
public string Handbook => "手冊";
|
||||
public string NotFound => "未找到";
|
||||
public string Error => "錯誤";
|
||||
public string FloorInfo => "區域文件";
|
||||
public string FloorGroupInfo => "區域組文件";
|
||||
public string FloorMissingResult => "傳送與世界生成";
|
||||
public string FloorGroupMissingResult => "傳送、怪物戰鬥與世界生成";
|
||||
public string Mission => "任務";
|
||||
public string MissionInfo => "任務文件";
|
||||
public string SubMission => "子任務";
|
||||
public string SubMissionInfo => "子任務文件";
|
||||
public string MazeSkill => "角色秘技";
|
||||
public string MazeSkillInfo => "角色秘技文件";
|
||||
public string Dialogue => "模擬宇宙事件";
|
||||
public string DialogueInfo => "模擬宇宙事件文件";
|
||||
public string Performance => "劇情操作";
|
||||
public string PerformanceInfo => "劇情操作文件";
|
||||
public string RogueChestMap => "模擬宇宙地圖";
|
||||
public string RogueChestMapInfo => "模擬宇宙地圖文件";
|
||||
public string ChessRogueRoom => "模擬宇宙DLC";
|
||||
public string ChessRogueRoomInfo => "模擬宇宙DLC文件";
|
||||
public string SummonUnit => "秘技生成";
|
||||
public string SummonUnitInfo => "秘技生成文件";
|
||||
public string RogueTournRoom => "差分宇宙";
|
||||
public string RogueTournRoomInfo => "差分宇宙房間文件";
|
||||
public string TypesOfRogue => "類型的模擬宇宙";
|
||||
public string RogueMagicRoom => "不可知域";
|
||||
public string RogueMagicRoomInfo => "不可知域房間文件";
|
||||
public string RogueDiceSurface => "骰面效果";
|
||||
public string RogueDiceSurfaceInfo => "骰面效果文件";
|
||||
public string AdventureModifier => "AdventureModifier";
|
||||
public string AdventureModifierInfo => "AdventureModifier文件";
|
||||
public string RogueMapGen => "RogueMapGen文件";
|
||||
public string RogueMiracleGroup => "RogueMiracleGroup文件";
|
||||
public string RogueMiracleEffectGen => "RogueMiracleEffectGen文件";
|
||||
|
||||
public string DatabaseAccount => "數據庫賬號";
|
||||
public string Tutorial => "教程";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 2
|
||||
|
||||
#region GameText
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command
|
||||
/// </summary>
|
||||
public class CommandTextCHT
|
||||
{
|
||||
public NoticeTextCHT Notice { get; } = new();
|
||||
|
||||
public GenderTextCHT Gender { get; } = new();
|
||||
public AvatarTextCHT Avatar { get; } = new();
|
||||
public AnnounceTextCHT Announce { get; } = new();
|
||||
public BanTextCHT Ban { get; } = new();
|
||||
public GiveTextCHT Give { get; } = new();
|
||||
public GiveAllTextCHT GiveAll { get; } = new();
|
||||
public LineupTextCHT Lineup { get; } = new();
|
||||
public HelpTextCHT Help { get; } = new();
|
||||
public KickTextCHT Kick { get; } = new();
|
||||
public MissionTextCHT Mission { get; } = new();
|
||||
public RelicTextCHT Relic { get; } = new();
|
||||
public ReloadTextCHT Reload { get; } = new();
|
||||
public RogueTextCHT Rogue { get; } = new();
|
||||
public SceneTextCHT Scene { get; } = new();
|
||||
public UnlockAllTextCHT UnlockAll { get; } = new();
|
||||
public MailTextCHT Mail { get; } = new();
|
||||
public RaidTextCHT Raid { get; } = new();
|
||||
public AccountTextCHT Account { get; } = new();
|
||||
public UnstuckTextCHT Unstuck { get; } = new();
|
||||
public SetlevelTextCHT Setlevel { get; } = new();
|
||||
public PermissionTextCHT Permission { get; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServerText
|
||||
|
||||
/// <summary>
|
||||
/// path: Server.Web
|
||||
/// </summary>
|
||||
public class WebTextCHT
|
||||
{
|
||||
public string Maintain => "服務器正在維修, 請稍後嘗試。";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Server.ServerInfo
|
||||
/// </summary>
|
||||
public class ServerInfoTextCHT
|
||||
{
|
||||
public string Shutdown => "關閉中…";
|
||||
public string CancelKeyPressed => "已按下取消鍵 (Ctrl + C), 服務器即將關閉…";
|
||||
public string StartingServer => "正在啟動 MikuSB";
|
||||
public string CurrentVersion => "當前服務端支援的版本: {0}";
|
||||
public string InvalidVersion => "目前為不受支援的遊戲版本 {0}\n請更新遊戲到 {1}";
|
||||
public string LoadingItem => "正在加載 {0}…";
|
||||
public string GeneratingItem => "正在生成 {0}…";
|
||||
public string WaitingItem => "正在等待進程 {0} 完成…";
|
||||
public string RegisterItem => "註冊了 {0} 個 {1}。";
|
||||
public string FailedToLoadItem => "加載 {0} 失敗。";
|
||||
public string NewClientSecretKey => "客戶端密鑰不存在, 正在生成新的客戶端密鑰。";
|
||||
public string FailedToInitializeItem => "初始化 {0} 失敗。";
|
||||
public string FailedToReadItem => "讀取 {0} 失敗, 文件{1}";
|
||||
public string GeneratedItem => "已生成 {0}。";
|
||||
public string LoadedItem => "已加載 {0}。";
|
||||
public string LoadedItems => "已加載 {0} 個 {1}。";
|
||||
public string ServerRunning => "{0} 服務器正在監聽 {1}";
|
||||
public string ServerStarted => "啟動完成!用時 {0}s, 擊敗了99%的用戶, 輸入 『help』 來獲取命令幫助"; // 玩梗, 考慮英語版本將其本土化
|
||||
public string MissionEnabled => "任務系統已啟用, 此功能仍在開發中, 且可能不會按預期工作, 如果遇見任何bug, 請匯報給開發者。";
|
||||
public string KeyStoreError => "SSL證書不存在, 已關閉SSL功能。";
|
||||
public string CacheLoadSkip => "已跳過緩存加載。";
|
||||
|
||||
public string ConfigMissing => "{0} 缺失, 請檢查你的資源文件夾: {1}, {2} 可能不能使用。";
|
||||
public string UnloadedItems => "卸載了所有 {0}。";
|
||||
public string SaveDatabase => "已保存數據庫, 用時 {0}s";
|
||||
public string WaitForAllDone => "現在還不可以進入遊戲, 請等待所有項目加載完成後再試";
|
||||
|
||||
public string UnhandledException => "發生未經處理的異常: {0}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 3
|
||||
|
||||
#region CommandText
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Notice
|
||||
/// </summary>
|
||||
public class NoticeTextCHT
|
||||
{
|
||||
public string PlayerNotFound => "未找到玩家!";
|
||||
public string InvalidArguments => "無效的參數!";
|
||||
public string NoPermission => "你沒有權限這麽做!";
|
||||
public string CommandNotFound => "未找到命令! 輸入 '/help' 來獲取幫助";
|
||||
public string TargetOffline => "目標 {0}({1}) 離線了!清除當前目標";
|
||||
public string TargetFound => "找到目標 {0}({1}), 下一次命令將默認對其執行";
|
||||
public string TargetNotFound => "未找到目標 {0}!";
|
||||
public string InternalError => "在處理命令時發生了內部錯誤: {0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Gender
|
||||
/// </summary>
|
||||
public class GenderTextCHT
|
||||
{
|
||||
public string Desc => "切換主角的性別";
|
||||
public string Usage => "用法: /gender [man/woman]";
|
||||
public string GenderNotSpecified => "性別不存在!";
|
||||
public string GenderChanged => "性別已更改!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.UnlockAll
|
||||
/// </summary>
|
||||
public class UnlockAllTextCHT
|
||||
{
|
||||
public string Desc =>
|
||||
"解鎖所有在類別內的對象\n" +
|
||||
"使用 /unlockall mission 以完成所有任務, 使用後會被踢出, 重新登錄後可能會被教程卡住, 請謹慎使用\n" +
|
||||
"使用 /unlockall tutorial 以解鎖所有教程, 使用後會被踢出, 用於部分界面卡住無法行動的情況\n" +
|
||||
"使用 /unlockall rogue 以解鎖所有類型模擬宇宙, 使用後會被踢出, 建議與 /unlockall tutorial 搭配使用以獲取更好效果";
|
||||
|
||||
public string Usage => "用法: /unlockall [mission/tutorial/rogue]";
|
||||
public string UnlockedAll => "已解鎖/完成所有{0}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Avatar
|
||||
/// </summary>
|
||||
public class AvatarTextCHT
|
||||
{
|
||||
public string Desc => "設定玩家已有角色的屬性, -1意為所有已擁有角色";
|
||||
|
||||
public string Usage =>
|
||||
"用法: /avatar talent [角色ID/-1] [行跡等級]\n" +
|
||||
"用法: /avatar rank [角色ID/-1] [星魂]\n" +
|
||||
"用法: /avatar level [角色ID/-1] [角色等級]";
|
||||
public string InvalidLevel => "{0}等級無效!";
|
||||
public string AllAvatarsLevelSet => "已將全部角色 {0}等級設置為 {1}.";
|
||||
public string AvatarLevelSet => "已將 {0} 角色 {1}等級設置為 {2}.";
|
||||
public string AvatarNotFound => "角色不存在!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Give
|
||||
/// </summary>
|
||||
public class GiveTextCHT
|
||||
{
|
||||
public string Desc => "給予玩家物品";
|
||||
public string Usage => "用法: /give [物品ID] l[等級] x[數量] r[疊影]";
|
||||
public string ItemNotFound => "未找到物品!";
|
||||
public string GiveItem => "給予 @{0} {1} 個物品 {2}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.GiveAll
|
||||
/// </summary>
|
||||
public class GiveAllTextCHT
|
||||
{
|
||||
public string Desc => "給予玩家全部指定類型的物品";
|
||||
public string Usage =>
|
||||
"用法: /giveall avatar r[星魂] l[等級]\n" +
|
||||
"用法: /giveall material x[數量]\n" +
|
||||
"用法: /giveall equipment r[叠影] l[等級] x[數量]\n" +
|
||||
"用法: /giveall relic l<等級> x<數量>\n" +
|
||||
"用法: /giveall unlock\n" +
|
||||
"用法: /giveall train\n" +
|
||||
"用法: /giveall path";
|
||||
public string GiveAllItems => "已給予所有 {0}, 各 {1} 個.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Lineup
|
||||
/// </summary>
|
||||
public class LineupTextCHT
|
||||
{
|
||||
public string Desc => "管理玩家的隊伍資訊";
|
||||
public string Usage =>
|
||||
"用法: /lineup mp\n" +
|
||||
"用法: /lineup sp\n" +
|
||||
"用法: /lineup heal";
|
||||
public string GainedMp => "成功恢復秘技點!";
|
||||
public string GainedSp => "成功恢復能量!";
|
||||
public string HealedAllAvatars => "成功治愈當前隊伍中的所有角色!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Help
|
||||
/// </summary>
|
||||
public class HelpTextCHT
|
||||
{
|
||||
public string Desc => "顯示幫助信息";
|
||||
public string Usage =>
|
||||
"用法: /help\n" +
|
||||
"用法: /help [命令]";
|
||||
public string Commands => "命令: ";
|
||||
public string CommandPermission => "所需權限: ";
|
||||
public string CommandAlias => "命令別名: ";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Kick
|
||||
/// </summary>
|
||||
public class KickTextCHT
|
||||
{
|
||||
public string Desc => "踢出玩家";
|
||||
public string Usage => "用法: /kick";
|
||||
public string PlayerKicked => "玩家 {0} 已被踢出!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Mission
|
||||
/// </summary>
|
||||
public class MissionTextCHT
|
||||
{
|
||||
public string Desc =>
|
||||
"管理玩家的任務\n" +
|
||||
"使用 pass 完成當前正在進行的所有任務, 此命令易造成嚴重卡頓, 請盡量使用 /mission finish 替代\n" +
|
||||
"使用 finish [子任務ID] 完成指定子任務, 請流覽 handbook 來獲取子任務ID\n" +
|
||||
"使用 finishmain [主任務ID] 完成指定主任務, 請流覽 handbook 來獲取主任務ID\n" +
|
||||
"使用 running [-all] 獲取正在追蹤的任務, 增加'-all'則顯示所有正在進行的任務以及可能卡住的任務, 使用後可能會出現較長任務清單, 請注意甄別\n" +
|
||||
"使用 reaccept [主任務ID] 可重新進行指定主任務, 請流覽 handbook 來獲取主任務ID";
|
||||
|
||||
public string Usage =>
|
||||
"用法: /mission pass\n" +
|
||||
"用法: /mission finish [子任務]\n" +
|
||||
"用法: /mission running [-all]\n" +
|
||||
"用法: /mission reaccept [主任務ID]\n" +
|
||||
"用法: /mission finishmain [主任務ID]";
|
||||
public string AllMissionsFinished => "所有任務已完成!";
|
||||
public string AllRunningMissionsFinished => "共 {0} 個進行中的任務已完成!";
|
||||
public string MissionFinished => "任務 {0} 已完成!";
|
||||
public string InvalidMissionId => "無效的任務ID!";
|
||||
public string NoRunningMissions => "沒有正在進行的任務!";
|
||||
public string RunningMissions => "正在進行的任務: ";
|
||||
public string PossibleStuckMissions => "可能卡住的任務: ";
|
||||
public string MainMission => "主任務";
|
||||
public string MissionReAccepted => "重新接受任務 {0}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Relic
|
||||
/// </summary>
|
||||
public class RelicTextCHT
|
||||
{
|
||||
public string Desc => "管理玩家的遺器, 等級限製: 1 ≤ 等級 ≤ 9999";
|
||||
public string Usage => "用法: /relic [遺器ID] [主詞條ID] [ID1:等級] [ID2:等級] l[等級] x[數量]";
|
||||
public string RelicNotFound => "遺器不存在!";
|
||||
public string InvalidMainAffixId => "主詞條ID無效!";
|
||||
public string InvalidSubAffixId => "副詞條ID無效!";
|
||||
public string RelicGiven => "給予玩家 @{0} {1} 個遺器 {2}, 主詞條 {3}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Reload
|
||||
/// </summary>
|
||||
public class ReloadTextCHT
|
||||
{
|
||||
public string Desc => "重新加載指定的配置";
|
||||
public string Usage => "用法: /reload [banner/activity]";
|
||||
public string ConfigReloaded => "配置 {0} 已重新加載!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Rogue
|
||||
/// </summary>
|
||||
public class RogueTextCHT
|
||||
{
|
||||
public string Desc => "管理模擬宇宙數據, -1意為所有已擁有祝福, buff來獲取祝福, enhance強化祝福";
|
||||
|
||||
public string Usage =>
|
||||
"用法: /rogue money [宇宙碎片數量]\n" +
|
||||
"用法: /rogue buff [祝福ID/-1]\n" +
|
||||
"用法: /rogue miracle [奇物ID]\n" +
|
||||
"用法: /rogue enhance [祝福ID/-1]\n" +
|
||||
"用法: /rogue unstuck - 脫離事件";
|
||||
public string PlayerGainedMoney => "已獲得 {0} 宇宙碎片.";
|
||||
public string PlayerGainedAllItems => "已獲得所有{0}.";
|
||||
public string PlayerGainedItem => "已獲得{0} {1}.";
|
||||
public string PlayerEnhancedBuff => "已強化祝福 {0}.";
|
||||
public string PlayerEnhancedAllBuffs => "已強化所有祝福.";
|
||||
public string PlayerUnstuck => "已脫離事件.";
|
||||
public string NotFoundItem => "未找到 {0}!";
|
||||
public string PlayerNotInRogue => "玩家不在模擬宇宙中!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Scene
|
||||
/// </summary>
|
||||
public class SceneTextCHT
|
||||
{
|
||||
public string Desc =>
|
||||
"管理玩家場景\n" +
|
||||
"使用 PlaneId 預設進入指定場景\n" +
|
||||
"使用 group 來獲取組, 使用 prop 來設置道具狀態, 在 PropStateEnum 獲取狀態列表\n" +
|
||||
"使用 unlockall 來解鎖場景內所有道具(open狀態), 可能導致遊戲加載卡條, 使用 /scene reset 解決\n" +
|
||||
"使用 reload 來重新加載當前場景, 並回到初始位置\n" +
|
||||
"使用 reset 來重置指定場景所有道具狀態";
|
||||
public string Usage =>
|
||||
"用法: /scene [entryId]\n" +
|
||||
"用法: /scene cur\n" +
|
||||
"用法: /scene reload\n" +
|
||||
"用法: /scene group\n" +
|
||||
"用法: /scene unlockall\n" +
|
||||
"用法: /scene reset [floorId]" +
|
||||
"用法: /scene prop [組ID] [道具ID] [狀態]\n" +
|
||||
"用法: /scene remove [實體ID]\n";
|
||||
public string LoadedGroups => "已加載組: {0}.";
|
||||
public string PropStateChanged => "道具: {0} 的狀態已設置為 {1}.";
|
||||
public string PropNotFound => "未找到道具!";
|
||||
public string EntityRemoved => "實體 {0} 已被移除.";
|
||||
public string EntityNotFound => "未找到實體!";
|
||||
public string AllPropsUnlocked => "所有道具已解鎖!";
|
||||
public string SceneChanged => "已進入場景 {0}.";
|
||||
public string SceneReloaded => "場景已重新加載!";
|
||||
public string SceneReset => "已重置場景 {0} 中所有道具狀態!";
|
||||
public string CurrentScene => "當前場景Entry Id: {0}, Plane Id: {1}, Floor Id: {2}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Mail
|
||||
/// </summary>
|
||||
public class MailTextCHT
|
||||
{
|
||||
public string Desc => "發送郵件";
|
||||
public string Usage => "用法: /mail [發送名稱] [標題] [內容] [ID1:數量,ID2:數量]";
|
||||
public string MailSent => "郵件已發送!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Raid
|
||||
/// </summary>
|
||||
public class RaidTextCHT
|
||||
{
|
||||
public string Desc => "管理玩家的任務臨時場景";
|
||||
public string Usage => "用法: /raid leave";
|
||||
public string Leaved => "已離開臨時場景!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Account
|
||||
/// </summary>
|
||||
public class AccountTextCHT
|
||||
{
|
||||
public string Desc => "管理資料庫帳號";
|
||||
public string Usage =>
|
||||
"用法: /account create [用户名] [UID] [密碼]\n" +
|
||||
"用法: /account delete [UID]";
|
||||
public string InvalidUid => "UID無效!";
|
||||
public string InvalidAccount => "帳號 {0} 無效!";
|
||||
public string CreateSuccess => "賬號 {0} 創建成功!";
|
||||
public string DeleteSuccess => "賬號 {0} 刪除成功!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Announce
|
||||
/// </summary>
|
||||
public class AnnounceTextCHT
|
||||
{
|
||||
public string Desc => "發送彈窗公告";
|
||||
public string Usage => "用法: /announce [Text] [Color]";
|
||||
public string SendSuccess => "發送成功!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Ban
|
||||
/// </summary>
|
||||
public class BanTextCHT
|
||||
{
|
||||
public string Desc => "封禁或解封用户";
|
||||
public string Usage => "用法: /ban [add/delete]";
|
||||
public string BanSuccess => "帳號已封禁!";
|
||||
public string UnBanSuccess => "帳號已解封!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Unstuck
|
||||
/// </summary>
|
||||
public class UnstuckTextCHT
|
||||
{
|
||||
public string Desc => "將玩家傳送回默認場景";
|
||||
public string Usage => "用法: /unstuck [UID]";
|
||||
public string UnstuckSuccess => "已成功將該玩家傳送回默認場景";
|
||||
public string UidNotExist => "該UID不存在!";
|
||||
public string PlayerIsOnline => "該玩家目前在線上!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Setlevel
|
||||
/// </summary>
|
||||
public class SetlevelTextCHT
|
||||
{
|
||||
public string Desc => "設定玩家等級";
|
||||
public string Usage => "用法: /setlevel [等級]";
|
||||
public string SetlevelSuccess => "等級設定成功!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Permission
|
||||
/// </summary>
|
||||
public class PermissionTextCHT
|
||||
{
|
||||
public string Desc => "管理玩家權限";
|
||||
public string Usage =>
|
||||
"用法: /permission add [權限]\n" +
|
||||
"用法: /permission remove [權限]\n" +
|
||||
"用法: /permission clean [權限]";
|
||||
public string InvalidPerm => "權限 {0} 不存在!";
|
||||
public string Added => "已添加權限 {0} 到玩家 {1}!";
|
||||
public string Removed => "已移除玩家 {0} 的權限 {1}!";
|
||||
public string Cleaned => "已清除玩家 {0} 的所有權限!";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
277
Common/Internationalization/Message/LanguageEN.cs
Normal file
277
Common/Internationalization/Message/LanguageEN.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
namespace MikuSB.Internationalization.Message;
|
||||
|
||||
#region Root
|
||||
|
||||
public class LanguageEN
|
||||
{
|
||||
public GameTextEN Game { get; } = new();
|
||||
public ServerTextEN Server { get; } = new();
|
||||
public WordTextEN Word { get; } = new(); // a placeholder for the actual word text
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 1
|
||||
|
||||
/// <summary>
|
||||
/// path: Game
|
||||
/// </summary>
|
||||
public class GameTextEN
|
||||
{
|
||||
public CommandTextEN Command { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Server
|
||||
/// </summary>
|
||||
public class ServerTextEN
|
||||
{
|
||||
public WebTextEN Web { get; } = new();
|
||||
public ServerInfoTextEN ServerInfo { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Word
|
||||
/// </summary>
|
||||
public class WordTextEN
|
||||
{
|
||||
public string Star => "Star";
|
||||
public string Valk => "Valkyrie";
|
||||
public string Material => "Material";
|
||||
public string Stigmata => "Stigmata";
|
||||
public string Weapon => "Weapon";
|
||||
public string Banner => "Gacha";
|
||||
public string Activity => "Activity";
|
||||
public string Elf => "Elf";
|
||||
public string Dress => "Outfit";
|
||||
public string Bracket => "Bracket";
|
||||
public string Disturbance => "Disturbance";
|
||||
public string Site => "Site";
|
||||
|
||||
// server info
|
||||
public string Config => "Config File";
|
||||
public string Language => "Language";
|
||||
public string Log => "Log";
|
||||
public string GameData => "Game Data";
|
||||
public string Cache => "Resource Cache";
|
||||
public string CustomData => "Custom Data";
|
||||
public string Database => "Database";
|
||||
public string Command => "Command";
|
||||
public string SdkServer => "Web Server";
|
||||
public string Handler => "Packet Handler";
|
||||
public string Dispatch => "Global Dispatch";
|
||||
public string Game => "Game";
|
||||
public string Handbook => "Handbook";
|
||||
public string NotFound => "Not Found";
|
||||
public string Error => "Error";
|
||||
public string DatabaseAccount => "Database Account";
|
||||
public string Tutorial => "Tutorial";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 2
|
||||
|
||||
#region GameText
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command
|
||||
/// </summary>
|
||||
public class CommandTextEN
|
||||
{
|
||||
public NoticeTextEN Notice { get; } = new();
|
||||
public HelpTextEN Help { get; } = new();
|
||||
public ValkTextEN Valk { get; } = new();
|
||||
public GiveAllTextEN GiveAll { get; } = new();
|
||||
public ElfTextEN Elf { get; } = new();
|
||||
public AbyssTextEN Abyss { get; } = new();
|
||||
public EndlessTextEN Endless { get; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ServerTextEN
|
||||
|
||||
/// <summary>
|
||||
/// path: Server.Web
|
||||
/// </summary>
|
||||
public class WebTextEN
|
||||
{
|
||||
public string Maintain => "The server is undergoing maintenance, please try again later.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Server.ServerInfo
|
||||
/// </summary>
|
||||
public class ServerInfoTextEN
|
||||
{
|
||||
public string Shutdown => "Shutting down...";
|
||||
public string CancelKeyPressed => "Cancel key pressed (Ctrl + C), server shutting down...";
|
||||
public string StartingServer => "Starting MikuSB";
|
||||
public string CurrentVersion => "Server supported versions: {0}";
|
||||
public string InvalidVersion => "Unsupported game version {0}\nPlease update game to {1}";
|
||||
public string LoadingItem => "Loading {0}...";
|
||||
public string GeneratingItem => "Building {0}...";
|
||||
public string WaitingItem => "Waiting for process {0} to complete...";
|
||||
public string RegisterItem => "Registered {0} {1}(s).";
|
||||
public string FailedToLoadItem => "Failed to load {0}.";
|
||||
public string NewClientSecretKey => "Client Secret Key does not exist and a new Client Secret Key is being generated.";
|
||||
public string FailedToInitializeItem => "Failed to initialize {0}.";
|
||||
public string FailedToReadItem => "Failed to read {0}, file {1}";
|
||||
public string GeneratedItem => "Generated {0}.";
|
||||
public string LoadedItem => "Loaded {0}.";
|
||||
public string LoadedItems => "Loaded {0} {1}(s).";
|
||||
public string ServerRunning => "{0} server listening on {1}";
|
||||
|
||||
public string ServerStarted =>
|
||||
"Startup complete! Took {0}s, better than 99% of users. Type 'help' for command help"; // This is a meme, consider localpermissiong in English
|
||||
|
||||
public string MissionEnabled =>
|
||||
"Mission system enabled. This feature is still in development and may not work as expected. Please report any bugs to the developers.";
|
||||
public string KeyStoreError => "The SSL certificate does not exist, SSL functionality has been disabled.";
|
||||
public string CacheLoadSkip => "Skipped cache loading.";
|
||||
|
||||
public string ConfigMissing => "{0} is missing. Please check your resource folder: {1}, {2} may not be available.";
|
||||
public string UnloadedItems => "Unloaded all {0}.";
|
||||
public string SaveDatabase => "Database saved in {0}s";
|
||||
|
||||
public string WaitForAllDone =>
|
||||
"You cannot enter the game yet. Please wait for all items to load before trying again";
|
||||
|
||||
public string UnhandledException => "An unhandled exception occurred: {0}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer 3
|
||||
|
||||
#region CommandText
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Notice
|
||||
/// </summary>
|
||||
public class NoticeTextEN
|
||||
{
|
||||
public string PlayerNotFound => "Player not found!";
|
||||
public string InvalidArguments => "Invalid arguments!";
|
||||
public string NoPermission => "You don't have permission!";
|
||||
public string CommandNotFound => "Command not found! Type '/help' for assistance";
|
||||
public string TargetOffline => "Target {0}({1}) is offline! Clearing current target";
|
||||
public string TargetFound => "Target {0}({1}) found. Next command will default to this target";
|
||||
public string TargetNotFound => "Target {0} not found!";
|
||||
public string InternalError => "Internal error occurred while processing command!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Help
|
||||
/// </summary>
|
||||
public class HelpTextEN
|
||||
{
|
||||
public string Desc => "Show help information";
|
||||
public string Usage =>
|
||||
"Usage: /help\n" +
|
||||
"Usage: /help [cmd]";
|
||||
public string Commands => "Commands: ";
|
||||
public string CommandUsage => "Usage: ";
|
||||
public string CommandPermission => "Level Permission For Access: ";
|
||||
public string CommandAlias => "Command Alias:";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Valk
|
||||
/// </summary>
|
||||
public class ValkTextEN
|
||||
{
|
||||
public string Desc => "Set attributes for owned characters\n" +
|
||||
"Note: -1 means all owned characters\n";
|
||||
|
||||
public string Usage =>
|
||||
"Usage: /valk add [ValkID/-1] l<Level> s<Star>\n\n" +
|
||||
"Usage: /valk level [ValkID/-1] [Level]\n\n" +
|
||||
"Usage: /valk star [ValkID/-1] [Star]\n\n" +
|
||||
"Usage: /valk skill [ValkID/-1] for max skill level";
|
||||
|
||||
public string ValkNotFound => "Character does not exist!";
|
||||
public string ValkAddedAll => "Granted all characters to player!";
|
||||
public string ValkAdded => "Granted character {0} to player!";
|
||||
public string ValkSetLevelAll => "Set all characters to level {0}!";
|
||||
public string ValkSetLevel => "Set character {0} to level {1}!";
|
||||
public string ValkSetStarAll => "Set all characters' Resonance to {0}!";
|
||||
public string ValkSetStar => "Set character {0}'s Resonance to {1}!";
|
||||
public string ValkSetSkillLevelAll => "Set all characters' skill levels to max!";
|
||||
public string ValkSetSkillLevel => "Set character {0}'s skill levels to max!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.GiveAll
|
||||
/// </summary>
|
||||
public class GiveAllTextEN
|
||||
{
|
||||
public string Desc => "Give all items of specified type\n" +
|
||||
"weapon,stigmata";
|
||||
|
||||
public string Usage =>
|
||||
"Usage: /giveall weapon\n\n" +
|
||||
"Usage: /giveall stigmata\n\n" +
|
||||
"Usage: /giveall material\n\n" +
|
||||
"Usage: /giveall dress\n";
|
||||
|
||||
public string GiveAllItems => "Granted all {0}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Elf
|
||||
/// </summary>
|
||||
public class ElfTextEN
|
||||
{
|
||||
public string Desc => "Set attributes for owned elfs\n" +
|
||||
"Note: -1 means all owned elfs\n";
|
||||
|
||||
public string Usage =>
|
||||
"Usage: /elf add [ElfID/-1] l<Level> s<Star>\n\n";
|
||||
|
||||
public string ElfNotFound => "Elf does not exist!";
|
||||
public string ElfAddedAll => "Granted all Elfs to player!";
|
||||
public string ElfAdded => "Granted Elf {0} to player!";
|
||||
public string ElfSetLevelAll => "Set all Elfs to level {0}!";
|
||||
public string ElfSetLevel => "Set Elf {0} to level {1}!";
|
||||
public string ElfSetStarAll => "Set all Elf's Resonance to {0}!";
|
||||
public string ElfSetStar => "Set Elf {0}'s Resonance to {1}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Abyss
|
||||
/// </summary>
|
||||
public class AbyssTextEN
|
||||
{
|
||||
public string Desc => "Set abyss disturbance,bracket,site \n";
|
||||
|
||||
public string Usage =>
|
||||
"Usage: /abyss bracket [1-9]\n\n" +
|
||||
"Usage: /abyss temp [value]\n\n" +
|
||||
"Usage: /abyss site [siteId]\n";
|
||||
|
||||
public string Success => "Success set {0}";
|
||||
public string AreaNotFound => "SiteId Not Found";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// path: Game.Command.Endless
|
||||
/// </summary>
|
||||
public class EndlessTextEN
|
||||
{
|
||||
public string Desc => "Set Memorial Arena boss \n";
|
||||
|
||||
public string Usage =>
|
||||
"Usage: /endless [bossid1] [bossid2] [bossid3]\n\n" +
|
||||
"/endless 1001 1002 1003";
|
||||
|
||||
public string Success => "Success set Memorial Arena Boss";
|
||||
public string NotFound => "BossId Not Found";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
9
Common/Internationalization/PluginLanguageAttribute.cs
Normal file
9
Common/Internationalization/PluginLanguageAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using MikuSB.Enums.Language;
|
||||
|
||||
namespace MikuSB.Internationalization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PluginLanguageAttribute(ProgramLanguageTypeEnum languageType) : Attribute
|
||||
{
|
||||
public ProgramLanguageTypeEnum LanguageType { get; } = languageType;
|
||||
}
|
||||
99
Common/Util/ConfigManager.cs
Normal file
99
Common/Util/ConfigManager.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using MikuSB.Configuration;
|
||||
using MikuSB.Internationalization;
|
||||
using Newtonsoft.Json;
|
||||
using MikuSB.Util.Extensions;
|
||||
|
||||
namespace MikuSB.Util;
|
||||
|
||||
public static class ConfigManager
|
||||
{
|
||||
public static readonly Logger Logger = new("ConfigManager");
|
||||
public static ConfigContainer Config { get; private set; } = new();
|
||||
private static readonly string ConfigFilePath = Config.Path.ConfigPath + "/Config.json";
|
||||
public static HotfixContainer Hotfix { get; private set; } = new();
|
||||
private static readonly string HotfixFilePath = Config.Path.ConfigPath + "/Hotfix.json";
|
||||
|
||||
public static void LoadConfig()
|
||||
{
|
||||
LoadConfigData();
|
||||
//LoadHotfixData();
|
||||
}
|
||||
|
||||
private static void LoadConfigData()
|
||||
{
|
||||
var file = new FileInfo(ConfigFilePath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
Config = new()
|
||||
{
|
||||
ServerOption =
|
||||
{
|
||||
Language = Extensions.Extensions.GetCurrentLanguage()
|
||||
}
|
||||
};
|
||||
|
||||
Logger.Info("Current Language is " + Config.ServerOption.Language);
|
||||
SaveData(Config, ConfigFilePath);
|
||||
}
|
||||
|
||||
using (var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var json = reader.ReadToEnd();
|
||||
Config = JsonConvert.DeserializeObject<ConfigContainer>(json)!;
|
||||
}
|
||||
|
||||
SaveData(Config, ConfigFilePath);
|
||||
}
|
||||
|
||||
private static void LoadHotfixData()
|
||||
{
|
||||
var file = new FileInfo(HotfixFilePath);
|
||||
|
||||
// Generate all necessary versions
|
||||
var verList = Extensions.Extensions.GetSupportVersions();
|
||||
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.CurrentVersion",
|
||||
verList.Aggregate((current, next) => $"{current}, {next}")));
|
||||
|
||||
if (!file.Exists)
|
||||
{
|
||||
Hotfix = new HotfixContainer();
|
||||
SaveData(Hotfix, HotfixFilePath);
|
||||
file.Refresh();
|
||||
}
|
||||
|
||||
using (var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var json = reader.ReadToEnd();
|
||||
Hotfix = JsonConvert.DeserializeObject<HotfixContainer>(json)!;
|
||||
}
|
||||
|
||||
foreach (var version in verList)
|
||||
if (!Hotfix.Hotfixes.TryGetValue(version, out var _))
|
||||
Hotfix.Hotfixes[version] = new();
|
||||
|
||||
SaveData(Hotfix, HotfixFilePath);
|
||||
}
|
||||
|
||||
private static void SaveData(object data, string path)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(data, Formatting.Indented);
|
||||
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
|
||||
using var writer = new StreamWriter(stream);
|
||||
writer.Write(json);
|
||||
}
|
||||
|
||||
public static void InitDirectories()
|
||||
{
|
||||
foreach (var property in Config.Path.GetType().GetProperties())
|
||||
{
|
||||
var dir = property.GetValue(Config.Path)?.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Common/Util/Crpyto/DispatchEncryption.cs
Normal file
35
Common/Util/Crpyto/DispatchEncryption.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MikuSB.Util.Crypto;
|
||||
|
||||
public static class DispatchEncryption
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
};
|
||||
|
||||
public static string? EncryptDispatchContent(string version, object? data)
|
||||
{
|
||||
if (!ConfigManager.Hotfix.AesKeys.TryGetValue(version, out var aesKey))
|
||||
return null;
|
||||
|
||||
var serializedData = JsonSerializer.Serialize(data, JsonSerializerOptions);
|
||||
var keyBytes = aesKey.Split(' ')
|
||||
.Select(b => Convert.ToByte(b, 16))
|
||||
.ToArray();
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = keyBytes;
|
||||
|
||||
var encryptor = aes.CreateEncryptor();
|
||||
var dataBytes = Encoding.UTF8.GetBytes(serializedData);
|
||||
var encryptedBytes = encryptor.TransformFinalBlock(dataBytes, 0, dataBytes.Length);
|
||||
|
||||
return Convert.ToBase64String(encryptedBytes);
|
||||
}
|
||||
}
|
||||
14
Common/Util/DateTime.cs
Normal file
14
Common/Util/DateTime.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
namespace MikuSB.Util;
|
||||
|
||||
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public static long ToUnixTimestampMilliseconds(this DateTime dateTime)
|
||||
{
|
||||
return (long)(dateTime - UnixEpoch).TotalMilliseconds;
|
||||
}
|
||||
|
||||
}
|
||||
232
Common/Util/Extensions/Extensions.cs
Normal file
232
Common/Util/Extensions/Extensions.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
using MikuSB.Proto;
|
||||
using Newtonsoft.Json;
|
||||
using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MikuSB.Util.Extensions;
|
||||
|
||||
public static partial class Extensions
|
||||
{
|
||||
#region Regex
|
||||
|
||||
[GeneratedRegex(@"CN|OS|BETA|PROD|CECREATION|Android|Win|iOS")]
|
||||
public static partial Regex VersionRegex();
|
||||
|
||||
[GeneratedRegex(@"(?<=Avatar_)(.*?)(?=_Config)")]
|
||||
public static partial Regex AvatarConfigRegex();
|
||||
|
||||
[GeneratedRegex(@"(?<=Avatar_RogueBattleevent)(.*?)(?=_Config.json)")]
|
||||
public static partial Regex BattleEventDataRegex();
|
||||
|
||||
[GeneratedRegex(@"coin(\d+)tier")]
|
||||
public static partial Regex ProductRegex();
|
||||
|
||||
#endregion
|
||||
|
||||
public static string GetCurrentLanguage()
|
||||
{
|
||||
var uiCulture = CultureInfo.CurrentUICulture;
|
||||
return uiCulture.Name switch
|
||||
{
|
||||
"zh-CN" => "CHS",
|
||||
"zh-TW" => "CHT",
|
||||
"ja-JP" => "JP",
|
||||
_ => "EN"
|
||||
};
|
||||
}
|
||||
|
||||
public static List<string> GetSupportVersions()
|
||||
{
|
||||
var verList = new List<string>();
|
||||
if (GameConstants.GAME_VERSION[^1] == '5')
|
||||
for (var i = 1; i < 6; i++)
|
||||
verList.Add(GameConstants.GAME_VERSION + i.ToString());
|
||||
else
|
||||
verList.Add(GameConstants.GAME_VERSION);
|
||||
|
||||
return verList;
|
||||
}
|
||||
|
||||
public static T RandomElement<T>(this List<T> values)
|
||||
{
|
||||
var index = new Random().Next(values.Count);
|
||||
return values[index];
|
||||
}
|
||||
|
||||
public static string RandomKey(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var random = new Random();
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
|
||||
public static ICollection<T> Clone<T>(this ICollection<T> values)
|
||||
{
|
||||
List<T> list = [.. values];
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static int RandomInt(int from, int to)
|
||||
{
|
||||
return new Random().Next(from, to);
|
||||
}
|
||||
|
||||
public static string GetSha256Hash(string input)
|
||||
{
|
||||
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
var builder = new StringBuilder();
|
||||
for (int i = 0; i < bytes.Length; i++) builder.Append(bytes[i].ToString("x2"));
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static void SafeAdd<T>(this List<T> list, T item)
|
||||
{
|
||||
if (!list.Contains(item)) list.Add(item);
|
||||
}
|
||||
|
||||
public static void SafeAddRange<T>(this List<T> list, List<T> item)
|
||||
{
|
||||
foreach (var i in item) list.SafeAdd(i);
|
||||
}
|
||||
|
||||
public static long GetUnixSec()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public static long ToUnixSec(this DateTime dt)
|
||||
{
|
||||
return new DateTimeOffset(dt).ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public static long GetUnixMs()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
public static string ToArrayString<T>(this List<T> list)
|
||||
{
|
||||
return list.JoinFormat(", ", "");
|
||||
}
|
||||
|
||||
public static string ToJsonString<TK, TV>(this Dictionary<TK, TV> dic) where TK : notnull
|
||||
{
|
||||
return JsonConvert.SerializeObject(dic);
|
||||
}
|
||||
|
||||
public static byte[] StringToByteArray(string hex)
|
||||
{
|
||||
if (hex.Length % 2 == 1)
|
||||
throw new Exception("The binary key cannot have an odd number of digits");
|
||||
|
||||
byte[] arr = new byte[hex.Length >> 1];
|
||||
|
||||
for (int i = 0; i < hex.Length >> 1; ++i)
|
||||
{
|
||||
arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1])));
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static int GetHexVal(char hex)
|
||||
{
|
||||
int val = (int)hex;
|
||||
//For uppercase A-F letters:
|
||||
//return val - (val < 58 ? 48 : 55);
|
||||
//For lowercase a-f letters:
|
||||
//return val - (val < 58 ? 48 : 87);
|
||||
//Or the two combined, but a bit slower:
|
||||
return val - (val < 58 ? 48 : (val < 97 ? 55 : 87));
|
||||
}
|
||||
|
||||
#region Kcp Utils
|
||||
|
||||
public static string JoinFormat<T>(this IEnumerable<T> list, string separator,
|
||||
string formatString)
|
||||
{
|
||||
formatString = string.IsNullOrWhiteSpace(formatString) ? "{0}" : formatString;
|
||||
return string.Join(separator,
|
||||
list.Select(item => string.Format(formatString, item)));
|
||||
}
|
||||
|
||||
public static void WriteConvID(this BinaryWriter bw, long convId)
|
||||
{
|
||||
//bw.Write(convId);
|
||||
bw.Write((int)(convId >> 32));
|
||||
bw.Write((int)(convId & 0xFFFFFFFF));
|
||||
}
|
||||
|
||||
public static long GetNextAvailableIndex<T>(this SortedList<long, T> sortedList)
|
||||
{
|
||||
long key = 1;
|
||||
long count = sortedList.Count;
|
||||
long counter = 0;
|
||||
do
|
||||
{
|
||||
if (count == 0) break;
|
||||
var nextKeyInList = sortedList.Keys.ElementAt((Index)counter++);
|
||||
if (key != nextKeyInList) break;
|
||||
key = nextKeyInList + 1;
|
||||
} while (count != 1 && counter != count && key == sortedList.Keys.ElementAt((Index)counter));
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public static long AddNext<T>(this SortedList<long, T> sortedList, T item)
|
||||
{
|
||||
var key = sortedList.GetNextAvailableIndex();
|
||||
sortedList.Add(key, item);
|
||||
return key;
|
||||
}
|
||||
|
||||
public static int ReadInt32BE(this BinaryReader br)
|
||||
{
|
||||
return BinaryPrimitives.ReadInt32BigEndian(br.ReadBytes(sizeof(int)));
|
||||
}
|
||||
|
||||
public static uint ReadUInt32BE(this BinaryReader br)
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(br.ReadBytes(sizeof(uint)));
|
||||
}
|
||||
|
||||
public static ushort ReadUInt16BE(this BinaryReader br)
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(br.ReadBytes(sizeof(ushort)));
|
||||
}
|
||||
|
||||
public static void WriteUInt16BE(this BinaryWriter bw, ushort value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
|
||||
public static void WriteInt32BE(this BinaryWriter bw, int value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
|
||||
public static void WriteUInt32BE(this BinaryWriter bw, uint value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
|
||||
public static void WriteUInt64BE(this BinaryWriter bw, ulong value)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64BigEndian(data, value);
|
||||
bw.Write(data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
11
Common/Util/GameConstants.cs
Normal file
11
Common/Util/GameConstants.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace MikuSB.Util;
|
||||
|
||||
public static class GameConstants
|
||||
{
|
||||
public const string GAME_VERSION = "BETA V1.0";
|
||||
public const int MAX_STAMINA = 300;
|
||||
public const int STAMINA_RECOVERY_TIME = 360; // 6 minutes
|
||||
public const int STAMINA_RESERVE_RECOVERY_TIME = 1080; // 18 minutes
|
||||
public const int INVENTORY_MAX_EQUIPMENT = 1000;
|
||||
public const int MAX_LINEUP_COUNT = 9;
|
||||
}
|
||||
11
Common/Util/Guid64.cs
Normal file
11
Common/Util/Guid64.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace MikuSB.Util;
|
||||
|
||||
public static class Guid64
|
||||
{
|
||||
public static ulong NewGuid64()
|
||||
{
|
||||
byte[] guidBytes = Guid.NewGuid().ToByteArray();
|
||||
return (ulong)BitConverter.ToUInt32(guidBytes, 0);
|
||||
}
|
||||
}
|
||||
|
||||
185
Common/Util/IConsole.cs
Normal file
185
Common/Util/IConsole.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using Kodnix.Character;
|
||||
|
||||
namespace MikuSB.Util;
|
||||
|
||||
public class IConsole
|
||||
{
|
||||
public static readonly string PrefixContent = "[MikuSB]> ";
|
||||
public static readonly string Prefix = $"\u001b[38;2;255;192;203m{PrefixContent}\u001b[0m";
|
||||
private static readonly int HistoryMaxCount = 10;
|
||||
|
||||
public static List<char> Input { get; set; } = [];
|
||||
private static int CursorIndex { get; set; } = 0;
|
||||
private static readonly List<string> InputHistory = [];
|
||||
private static int HistoryIndex = -1;
|
||||
|
||||
public static event Action<string>? OnConsoleExcuteCommand;
|
||||
|
||||
public static void InitConsole()
|
||||
{
|
||||
Console.Title = ConfigManager.Config.GameServer.GameServerName;
|
||||
}
|
||||
|
||||
public static int GetWidth(string str)
|
||||
=> str.ToCharArray().Sum(EastAsianWidth.GetLength);
|
||||
|
||||
public static void RedrawInput(List<char> input, bool hasPrefix = true)
|
||||
=> RedrawInput(new string([.. input]), hasPrefix);
|
||||
|
||||
public static void RedrawInput(string input, bool hasPrefix = true)
|
||||
{
|
||||
var length = GetWidth(input);
|
||||
if (hasPrefix)
|
||||
{
|
||||
input = Prefix + input;
|
||||
length += GetWidth(PrefixContent);
|
||||
}
|
||||
|
||||
if (Console.GetCursorPosition().Left > 0)
|
||||
Console.SetCursorPosition(0, Console.CursorTop);
|
||||
|
||||
Console.Write(input + new string(' ', Console.BufferWidth - length));
|
||||
Console.SetCursorPosition(length, Console.CursorTop);
|
||||
}
|
||||
|
||||
#region Handlers
|
||||
|
||||
public static void HandleEnter()
|
||||
{
|
||||
var input = new string([.. Input]);
|
||||
if (string.IsNullOrWhiteSpace(input)) return;
|
||||
|
||||
// New line
|
||||
Console.WriteLine();
|
||||
Input = [];
|
||||
CursorIndex = 0;
|
||||
if (InputHistory.Count >= HistoryMaxCount)
|
||||
InputHistory.RemoveAt(0);
|
||||
InputHistory.Add(input);
|
||||
HistoryIndex = InputHistory.Count;
|
||||
|
||||
// Handle command
|
||||
if (input.StartsWith('/')) input = input[1..].Trim();
|
||||
OnConsoleExcuteCommand?.Invoke(input);
|
||||
}
|
||||
|
||||
public static void HandleBackspace()
|
||||
{
|
||||
if (CursorIndex <= 0) return;
|
||||
CursorIndex--;
|
||||
var targetWidth = GetWidth(Input[CursorIndex].ToString());
|
||||
Input.RemoveAt(CursorIndex);
|
||||
|
||||
var (left, _) = Console.GetCursorPosition();
|
||||
Console.SetCursorPosition(left - targetWidth, Console.CursorTop);
|
||||
var remain = new string([.. Input.Skip(CursorIndex)]);
|
||||
Console.Write(remain + new string(' ', targetWidth));
|
||||
Console.SetCursorPosition(left - targetWidth, Console.CursorTop);
|
||||
}
|
||||
|
||||
public static void HandleUpArrow()
|
||||
{
|
||||
if (InputHistory.Count == 0) return;
|
||||
|
||||
if (HistoryIndex > 0)
|
||||
{
|
||||
HistoryIndex--;
|
||||
var history = InputHistory[HistoryIndex];
|
||||
Input = [.. history];
|
||||
CursorIndex = Input.Count;
|
||||
RedrawInput(Input);
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleDownArrow()
|
||||
{
|
||||
if (HistoryIndex >= InputHistory.Count) return;
|
||||
|
||||
HistoryIndex++;
|
||||
if (HistoryIndex >= InputHistory.Count)
|
||||
{
|
||||
HistoryIndex = InputHistory.Count;
|
||||
Input = [];
|
||||
CursorIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var history = InputHistory[HistoryIndex];
|
||||
Input = [.. history];
|
||||
CursorIndex = Input.Count;
|
||||
}
|
||||
RedrawInput(Input);
|
||||
}
|
||||
|
||||
public static void HandleLeftArrow()
|
||||
{
|
||||
if (CursorIndex <= 0) return;
|
||||
|
||||
var (left, _) = Console.GetCursorPosition();
|
||||
CursorIndex--;
|
||||
Console.SetCursorPosition(left - GetWidth(Input[CursorIndex].ToString()), Console.CursorTop);
|
||||
}
|
||||
|
||||
public static void HandleRightArrow()
|
||||
{
|
||||
if (CursorIndex >= Input.Count) return;
|
||||
|
||||
var (left, _) = Console.GetCursorPosition();
|
||||
CursorIndex++;
|
||||
Console.SetCursorPosition(left + GetWidth(Input[CursorIndex - 1].ToString()), Console.CursorTop);
|
||||
}
|
||||
|
||||
public static void HandleInput(ConsoleKeyInfo keyInfo)
|
||||
{
|
||||
if (char.IsControl(keyInfo.KeyChar)) return;
|
||||
if (Input.Count >= (Console.BufferWidth - PrefixContent.Length)) return;
|
||||
HandleInput(keyInfo.KeyChar);
|
||||
}
|
||||
|
||||
public static void HandleInput(char keyChar)
|
||||
{
|
||||
Input.Insert(CursorIndex, keyChar);
|
||||
CursorIndex++;
|
||||
|
||||
var (left, _) = Console.GetCursorPosition();
|
||||
Console.Write(new string([.. Input.Skip(CursorIndex - 1)]));
|
||||
Console.SetCursorPosition(left + GetWidth(keyChar.ToString()), Console.CursorTop);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static string ListenConsole()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ConsoleKeyInfo keyInfo;
|
||||
try { keyInfo = Console.ReadKey(true); }
|
||||
catch (InvalidOperationException) { continue; }
|
||||
|
||||
switch (keyInfo.Key)
|
||||
{
|
||||
case ConsoleKey.Enter:
|
||||
HandleEnter();
|
||||
break;
|
||||
case ConsoleKey.Backspace:
|
||||
HandleBackspace();
|
||||
break;
|
||||
case ConsoleKey.LeftArrow:
|
||||
HandleLeftArrow();
|
||||
break;
|
||||
case ConsoleKey.RightArrow:
|
||||
HandleRightArrow();
|
||||
break;
|
||||
case ConsoleKey.UpArrow:
|
||||
HandleUpArrow();
|
||||
break;
|
||||
case ConsoleKey.DownArrow:
|
||||
HandleDownArrow();
|
||||
break;
|
||||
default:
|
||||
HandleInput(keyInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
Common/Util/Logger.cs
Normal file
109
Common/Util/Logger.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Spectre.Console;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MikuSB.Util;
|
||||
|
||||
public class Logger(string moduleName)
|
||||
{
|
||||
private static FileInfo? LogFile;
|
||||
private static readonly object _lock = new();
|
||||
private readonly string ModuleName = moduleName;
|
||||
|
||||
public void Log(string message, LoggerLevel level)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var savedInput = IConsole.Input.ToList(); // Copy
|
||||
IConsole.RedrawInput("", false);
|
||||
AnsiConsole.MarkupLine($"[[[bold deepskyblue3_1]{DateTime.Now:HH:mm:ss}[/]]] " +
|
||||
$"[[[gray]{ModuleName}[/]]] [[[{(ConsoleColor)level}]{level}[/]]] " +
|
||||
$"{message.Replace("[", "[[").Replace("]", "]]")}");
|
||||
IConsole.RedrawInput(savedInput);
|
||||
|
||||
var logMessage = $"[{DateTime.Now:HH:mm:ss}] [{ModuleName}] [{level}] {message}";
|
||||
WriteToFile(logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public void Info(string message, Exception? e = null)
|
||||
{
|
||||
Log(message, LoggerLevel.INFO);
|
||||
if (e != null)
|
||||
{
|
||||
Log(e.Message, LoggerLevel.INFO);
|
||||
Log(e.StackTrace!, LoggerLevel.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
public void Warn(string message, Exception? e = null)
|
||||
{
|
||||
Log(message, LoggerLevel.WARN);
|
||||
if (e != null)
|
||||
{
|
||||
Log(e.Message, LoggerLevel.WARN);
|
||||
Log(e.StackTrace!, LoggerLevel.WARN);
|
||||
}
|
||||
}
|
||||
|
||||
public void Error(string message, Exception? e = null)
|
||||
{
|
||||
Log(message, LoggerLevel.ERROR);
|
||||
if (e != null)
|
||||
{
|
||||
Log(e.Message, LoggerLevel.ERROR);
|
||||
Log(e.StackTrace!, LoggerLevel.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public void Fatal(string message, Exception? e = null)
|
||||
{
|
||||
Log(message, LoggerLevel.FATAL);
|
||||
if (e != null)
|
||||
{
|
||||
Log(e.Message, LoggerLevel.FATAL);
|
||||
Log(e.StackTrace!, LoggerLevel.FATAL);
|
||||
}
|
||||
}
|
||||
|
||||
public void Debug(string message, Exception? e = null)
|
||||
{
|
||||
Log(message, LoggerLevel.DEBUG);
|
||||
if (e != null)
|
||||
{
|
||||
Log(e.Message, LoggerLevel.DEBUG);
|
||||
Log(e.StackTrace!, LoggerLevel.DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetLogFile(FileInfo file)
|
||||
{
|
||||
LogFile = file;
|
||||
}
|
||||
|
||||
public static void WriteToFile(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (LogFile == null) throw new Exception("LogFile is not set");
|
||||
using var sw = LogFile.AppendText();
|
||||
sw.WriteLine(message);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static Logger GetByClassName()
|
||||
{
|
||||
return new Logger(new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public enum LoggerLevel
|
||||
{
|
||||
INFO = ConsoleColor.Cyan,
|
||||
WARN = ConsoleColor.Yellow,
|
||||
ERROR = ConsoleColor.Red,
|
||||
FATAL = ConsoleColor.DarkRed,
|
||||
DEBUG = ConsoleColor.Blue
|
||||
}
|
||||
34
Common/Util/LoggingMiddleware.cs
Normal file
34
Common/Util/LoggingMiddleware.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using MikuSB.Util;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MikuSB.SdkServer.Utils;
|
||||
|
||||
public class RequestLoggingMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, Logger logger)
|
||||
{
|
||||
var request = context.Request;
|
||||
var method = request.Method;
|
||||
var path = request.Path + request.QueryString;
|
||||
|
||||
await next(context);
|
||||
|
||||
var statusCode = context.Response.StatusCode;
|
||||
|
||||
if (path.StartsWith("/report") || path.Contains("/log/") || path == "/alive")
|
||||
return;
|
||||
|
||||
if (statusCode == 200)
|
||||
{
|
||||
logger.Info($"{method} {path} => {statusCode}");
|
||||
}
|
||||
else if (statusCode == 404)
|
||||
{
|
||||
logger.Warn($"{method} {path} => {statusCode}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"{method} {path} => {statusCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Common/Util/Position.cs
Normal file
129
Common/Util/Position.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
namespace MikuSB.Common.Util;
|
||||
|
||||
public class Position
|
||||
{
|
||||
public Position(float x, float y, float z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
//public Position(Proto.Position position)
|
||||
//{
|
||||
// X = position.X;
|
||||
// Y = position.Y;
|
||||
// Z = position.Z;
|
||||
//}
|
||||
|
||||
public Position()
|
||||
{
|
||||
X = 0;
|
||||
Y = 0;
|
||||
Z = 0;
|
||||
}
|
||||
|
||||
public Position(Position position)
|
||||
{
|
||||
X = position.X;
|
||||
Y = position.Y;
|
||||
Z = position.Z;
|
||||
}
|
||||
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Z { get; set; }
|
||||
|
||||
public void Set(float x, float y, float z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
public void Set(Position position)
|
||||
{
|
||||
X = position.X;
|
||||
Y = position.Y;
|
||||
Z = position.Z;
|
||||
}
|
||||
|
||||
//public void Set(Vector vector)
|
||||
//{
|
||||
// X = vector.X;
|
||||
// Y = vector.Y;
|
||||
// Z = vector.Z;
|
||||
//}
|
||||
|
||||
public void Add(float x, float y, float z)
|
||||
{
|
||||
X += x;
|
||||
Y += y;
|
||||
Z += z;
|
||||
}
|
||||
|
||||
public void Add(Position position)
|
||||
{
|
||||
X += position.X;
|
||||
Y += position.Y;
|
||||
Z += position.Z;
|
||||
}
|
||||
|
||||
public void Sub(float x, float y, float z)
|
||||
{
|
||||
X -= x;
|
||||
Y -= y;
|
||||
Z -= z;
|
||||
}
|
||||
|
||||
public void Sub(Position position)
|
||||
{
|
||||
X -= position.X;
|
||||
Y -= position.Y;
|
||||
Z -= position.Z;
|
||||
}
|
||||
|
||||
public void Mul(float x, float y, float z)
|
||||
{
|
||||
X *= x;
|
||||
Y *= y;
|
||||
Z *= z;
|
||||
}
|
||||
|
||||
public void Mul(Position position)
|
||||
{
|
||||
X *= position.X;
|
||||
Y *= position.Y;
|
||||
Z *= position.Z;
|
||||
}
|
||||
|
||||
public void Div(float x, float y, float z)
|
||||
{
|
||||
X /= x;
|
||||
Y /= y;
|
||||
Z /= z;
|
||||
}
|
||||
|
||||
public void Div(Position position)
|
||||
{
|
||||
X /= position.X;
|
||||
Y /= position.Y;
|
||||
Z /= position.Z;
|
||||
}
|
||||
|
||||
public double Distance(Position position)
|
||||
{
|
||||
return Math.Sqrt((X - position.X) * (X - position.X) + (Y - position.Y) * (Y - position.Y) +
|
||||
(Z - position.Z) * (Z - position.Z));
|
||||
}
|
||||
|
||||
//public Proto.Position ToProto()
|
||||
//{
|
||||
// return new Proto.Position
|
||||
// {
|
||||
// X = (int)X,
|
||||
// Y = (int)Y,
|
||||
// Z = (int)Z
|
||||
// };
|
||||
//}
|
||||
}
|
||||
29
Common/Util/Security/Crypto.cs
Normal file
29
Common/Util/Security/Crypto.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace MikuSB.Util.Security;
|
||||
|
||||
public class Crypto
|
||||
{
|
||||
private static readonly Random SecureRandom = new();
|
||||
|
||||
// Simple way to create a unique session key
|
||||
public static string CreateSessionKey(string accountUid)
|
||||
{
|
||||
var random = new byte[64];
|
||||
SecureRandom.NextBytes(random);
|
||||
|
||||
var temp = accountUid + "." + DateTime.Now.Ticks + "." + SecureRandom;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = SHA512.HashData(Encoding.UTF8.GetBytes(temp));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
var bytes = SHA512.HashData(Encoding.UTF8.GetBytes(temp));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Config/Config.json
Normal file
42
Config/Config.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"HttpServer": {
|
||||
"BindAddress": "0.0.0.0",
|
||||
"PublicAddress": "127.0.0.1",
|
||||
"Port": 21500
|
||||
},
|
||||
"GameServer": {
|
||||
"BindAddress": "0.0.0.0",
|
||||
"PublicAddress": "127.0.0.1",
|
||||
"Port": 21000,
|
||||
"KcpAliveMs": 45000,
|
||||
"DatabaseName": "Miku.db",
|
||||
"GameServerId": "MikuSB",
|
||||
"GameServerName": "MikuSB"
|
||||
},
|
||||
"Path": {
|
||||
"ResourcePath": "Resources",
|
||||
"ConfigPath": "Config",
|
||||
"DatabasePath": "Config/Database",
|
||||
"HandbookPath": "Config/Handbook",
|
||||
"LogPath": "Config/Logs",
|
||||
"DataPath": "Config/Data"
|
||||
},
|
||||
"ServerOption": {
|
||||
"Language": "EN",
|
||||
"FallbackLanguage": "EN",
|
||||
"DefaultPermissions": [
|
||||
"Admin"
|
||||
],
|
||||
"ServerProfile": {
|
||||
"Name": "Miku-chan",
|
||||
"Uid": 80
|
||||
},
|
||||
"AutoCreateUser": true,
|
||||
"SavePersonalDebugFile": false,
|
||||
"AutoSendResponseWhenNoHandler": true,
|
||||
"EnableDebug": true,
|
||||
"DebugMessage": true,
|
||||
"DebugDetailMessage": true,
|
||||
"DebugNoHandlerPacket": true
|
||||
}
|
||||
}
|
||||
51
Config/Hotfix.json
Normal file
51
Config/Hotfix.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"UseLocalCache": false,
|
||||
"Hotfixes": {
|
||||
"BETA V1.0": {
|
||||
"Asb": {
|
||||
"Android": {
|
||||
"EnableTime": 0,
|
||||
"Revision": "",
|
||||
"Suffix": ""
|
||||
},
|
||||
"Iphone": {
|
||||
"EnableTime": 0,
|
||||
"Revision": "",
|
||||
"Suffix": ""
|
||||
},
|
||||
"Pc": {
|
||||
"EnableTime": 0,
|
||||
"Revision": "",
|
||||
"Suffix": ""
|
||||
}
|
||||
},
|
||||
"AsbPreDownload": {
|
||||
"Android": {
|
||||
"EncryptKey": "",
|
||||
"EnableTime": 0,
|
||||
"Revision": "",
|
||||
"Suffix": ""
|
||||
},
|
||||
"Iphone": {
|
||||
"EncryptKey": "",
|
||||
"EnableTime": 0,
|
||||
"Revision": "",
|
||||
"Suffix": ""
|
||||
}
|
||||
},
|
||||
"Audio": {
|
||||
"Platform": {},
|
||||
"Revision": 0
|
||||
},
|
||||
"AudioPreDownload": {
|
||||
"EnableTime": 0,
|
||||
"Platform": {},
|
||||
"Revision": 0
|
||||
},
|
||||
"VideoEncrypt": {
|
||||
"FileName": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"AesKeys": {}
|
||||
}
|
||||
79
GameServer/Command/CommandArg.cs
Normal file
79
GameServer/Command/CommandArg.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using MikuSB.Database.Account;
|
||||
using MikuSB.GameServer.Server;
|
||||
using MikuSB.Internationalization;
|
||||
|
||||
namespace MikuSB.GameServer.Command;
|
||||
|
||||
public class CommandArg
|
||||
{
|
||||
public string RawArg { get; } = "";
|
||||
public List<string> Args { get; } = [];
|
||||
public List<string> Attributes { get; } = [];
|
||||
public ICommandSender Sender { get; }
|
||||
public int TargetUid { get; set; } = 0;
|
||||
public Connection? Target { get; set; }
|
||||
|
||||
public CommandArg(string rawArg, ICommandSender sender)
|
||||
{
|
||||
Sender = sender;
|
||||
RawArg = rawArg;
|
||||
foreach (var arg in rawArg.Split(' '))
|
||||
{
|
||||
if (string.IsNullOrEmpty(arg)) continue;
|
||||
Args.Add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask SendMsg(string msg)
|
||||
{
|
||||
await Sender.SendMsg(msg);
|
||||
}
|
||||
|
||||
public int GetInt(int index)
|
||||
{
|
||||
if (Args.Count <= index) return 0;
|
||||
if (int.TryParse(Args[index], out var res))
|
||||
return res;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async ValueTask<int?> GetOption(char pre, string def = "1")
|
||||
{
|
||||
var opStr = Args.FirstOrDefault(x => x[0] == pre)?[1..] ?? def;
|
||||
if (!int.TryParse(opStr, out var op))
|
||||
{
|
||||
await SendMsg(I18NManager.Translate("Game.Command.Notice.InvalidArguments"));
|
||||
return null;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CheckArgCnt(int start, int? end = null)
|
||||
{
|
||||
end ??= start;
|
||||
if (Args.Count >= start && Args.Count <= end) return true;
|
||||
await SendMsg(I18NManager.Translate("Game.Command.Notice.InvalidArguments"));
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CheckTarget()
|
||||
{
|
||||
if (AccountData.GetAccountByUid(TargetUid) == null)
|
||||
{
|
||||
await SendMsg(I18NManager.Translate("Game.Command.Notice.PlayerNotFound"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CheckOnlineTarget(bool sendMsg = true)
|
||||
{
|
||||
if (Target == null)
|
||||
{
|
||||
if (sendMsg)
|
||||
await SendMsg(I18NManager.Translate("Game.Command.Notice.PlayerNotFound"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
GameServer/Command/CommandAttribute.cs
Normal file
25
GameServer/Command/CommandAttribute.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using MikuSB.Enums.Player;
|
||||
|
||||
namespace MikuSB.GameServer.Command;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class CommandInfoAttribute(
|
||||
string name, string desc, string usage, string[] alias, PermEnum[] perm) : Attribute
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public string Description { get; } = desc;
|
||||
public string Usage { get; } = usage;
|
||||
public PermEnum[] Perm { get; } = perm;
|
||||
public string[] Alias { get; } = alias;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class CommandDefaultAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class CommandMethodAttribute(string method) : Attribute
|
||||
{
|
||||
public string MethodName { get; } = method;
|
||||
}
|
||||
19
GameServer/Command/CommandExecutor.cs
Normal file
19
GameServer/Command/CommandExecutor.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
namespace MikuSB.GameServer.Command;
|
||||
|
||||
public static class CommandExecutor
|
||||
{
|
||||
public delegate void RunCommand(ICommandSender sender, string cmd);
|
||||
|
||||
public static event RunCommand? OnRunCommand;
|
||||
|
||||
public static void ExecuteCommand(ICommandSender sender, string cmd)
|
||||
{
|
||||
OnRunCommand?.Invoke(sender, cmd);
|
||||
}
|
||||
|
||||
public static void ConsoleExcuteCommand(string input)
|
||||
{
|
||||
CommandManager.HandleCommand(input, new ConsoleCommandSender(CommandManager.Logger));
|
||||
}
|
||||
}
|
||||
3
GameServer/Command/CommandInterface.cs
Normal file
3
GameServer/Command/CommandInterface.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace MikuSB.GameServer.Command;
|
||||
|
||||
public interface ICommands;
|
||||
126
GameServer/Command/CommandManager.cs
Normal file
126
GameServer/Command/CommandManager.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using MikuSB.Database.Account;
|
||||
using MikuSB.Enums.Player;
|
||||
using MikuSB.GameServer.Server;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.GameServer.Command;
|
||||
|
||||
public class CommandManager
|
||||
{
|
||||
public static Logger Logger { get; } = new("CommandManager");
|
||||
|
||||
public static Dictionary<string, ICommands> Commands { get; } = [];
|
||||
public static Dictionary<string, CommandInfoAttribute> CommandInfo { get; } = [];
|
||||
public static Dictionary<string, string> CommandAlias { get; } = []; // <aliaName, fullName>
|
||||
|
||||
public static void RegisterCommands()
|
||||
{
|
||||
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
|
||||
if (typeof(ICommands).IsAssignableFrom(type) && !type.IsAbstract)
|
||||
RegisterCommand(type);
|
||||
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.RegisterItem", Commands.Count.ToString(),
|
||||
I18NManager.Translate("Word.Command")));
|
||||
}
|
||||
|
||||
public static void RegisterCommand(Type type)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<CommandInfoAttribute>();
|
||||
if (attr == null) return;
|
||||
var instance = Activator.CreateInstance(type);
|
||||
if (instance is not ICommands command) return;
|
||||
Commands.Add(attr.Name, command);
|
||||
CommandInfo.Add(attr.Name, attr);
|
||||
|
||||
// register alias
|
||||
foreach (var alias in attr.Alias) // add alias
|
||||
CommandAlias.Add(alias, attr.Name);
|
||||
}
|
||||
|
||||
public static async void HandleCommand(string input, ICommandSender sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
var argInfo = new CommandArg(input, sender);
|
||||
var target = sender.GetSender();
|
||||
|
||||
foreach (var arg in argInfo.Args.ToList()) // Copy
|
||||
{
|
||||
switch (arg[0])
|
||||
{
|
||||
case '-':
|
||||
argInfo.Attributes.Add(arg[1..]);
|
||||
break;
|
||||
case '@':
|
||||
_ = int.TryParse(arg[1..], out target);
|
||||
argInfo.Args.Remove(arg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
argInfo.TargetUid = target;
|
||||
if (SocketListener.Connections.Values.ToList().Find(item =>
|
||||
(item as Connection)?.Player?.Uid == target) is Connection con)
|
||||
argInfo.Target = con;
|
||||
|
||||
// find register cmd
|
||||
var cmdName = argInfo.Args[0];
|
||||
if (CommandAlias.TryGetValue(cmdName, out var fullName)) cmdName = fullName;
|
||||
if (!Commands.TryGetValue(cmdName, out var command))
|
||||
{
|
||||
await sender.SendMsg(I18NManager.Translate("Game.Command.Notice.CommandNotFound"));
|
||||
return;
|
||||
}
|
||||
argInfo.Args.RemoveAt(0);
|
||||
var cmdInfo = CommandInfo[cmdName];
|
||||
|
||||
// Check cmd perms
|
||||
if (!AccountData.HasPerm(cmdInfo.Perm, sender.GetSender()))
|
||||
{
|
||||
await sender.SendMsg(I18NManager.Translate("Game.Command.Notice.NoPermission"));
|
||||
return;
|
||||
}
|
||||
if (argInfo.Target?.Player?.Uid != sender.GetSender() && !AccountData.HasPerm([PermEnum.Other], sender.GetSender()))
|
||||
{
|
||||
await sender.SendMsg(I18NManager.Translate("Game.Command.Notice.NoPermission"));
|
||||
return;
|
||||
}
|
||||
|
||||
// find CommandMethodAttribute
|
||||
var isFound = false;
|
||||
foreach (var methodInfo in command.GetType().GetMethods())
|
||||
{
|
||||
var attr = methodInfo.GetCustomAttribute<CommandMethodAttribute>();
|
||||
if (attr == null) continue;
|
||||
if (argInfo.Args.Count > 0 && attr.MethodName == argInfo.Args[0])
|
||||
{
|
||||
argInfo.Args.RemoveAt(0);
|
||||
isFound = true;
|
||||
methodInfo.Invoke(command, [argInfo]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isFound) return;
|
||||
|
||||
// find CommandDefaultAttribute
|
||||
foreach (var methodInfo in command.GetType().GetMethods())
|
||||
{
|
||||
var attr = methodInfo.GetCustomAttribute<CommandDefaultAttribute>();
|
||||
if (attr == null) continue;
|
||||
isFound = true;
|
||||
methodInfo.Invoke(command, [argInfo]);
|
||||
break;
|
||||
}
|
||||
if (isFound) return;
|
||||
|
||||
// failed to find method
|
||||
await sender.SendMsg(I18NManager.Translate(cmdInfo.Usage));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(I18NManager.Translate("Game.Command.Notice.InternalError", ex.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
41
GameServer/Command/CommandSender.cs
Normal file
41
GameServer/Command/CommandSender.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using MikuSB.Enums.Player;
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
using MikuSB.Util;
|
||||
|
||||
namespace MikuSB.GameServer.Command;
|
||||
|
||||
public interface ICommandSender
|
||||
{
|
||||
public ValueTask SendMsg(string msg);
|
||||
|
||||
public int GetSender();
|
||||
}
|
||||
|
||||
public class ConsoleCommandSender(Logger logger) : ICommandSender
|
||||
{
|
||||
public async ValueTask SendMsg(string msg)
|
||||
{
|
||||
logger.Info(msg);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public int GetSender()
|
||||
{
|
||||
return (int)ServerEnum.Console;
|
||||
}
|
||||
}
|
||||
|
||||
public class PlayerCommandSender(PlayerInstance player) : ICommandSender
|
||||
{
|
||||
public PlayerInstance Player = player;
|
||||
|
||||
public async ValueTask SendMsg(string msg)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
public int GetSender()
|
||||
{
|
||||
return Player.Uid;
|
||||
}
|
||||
}
|
||||
52
GameServer/Command/Commands/CommandHelp.cs
Normal file
52
GameServer/Command/Commands/CommandHelp.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using MikuSB.Enums.Player;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.Util.Extensions;
|
||||
|
||||
namespace MikuSB.GameServer.Command.Commands;
|
||||
|
||||
[CommandInfo("help", "Game.Command.Help.Desc", "Game.Command.Help.Usage", ["h"], [PermEnum.Support, PermEnum.Trial])]
|
||||
public class CommandHelp : ICommands
|
||||
{
|
||||
[CommandDefault]
|
||||
public async static ValueTask Help(CommandArg arg)
|
||||
{
|
||||
if (arg.Args.Count == 1)
|
||||
{
|
||||
var cmd = arg.Args[0];
|
||||
if (CommandManager.CommandInfo == null || !CommandManager.CommandInfo.TryGetValue(cmd, out var command))
|
||||
{
|
||||
await arg.SendMsg(I18NManager.Translate("Game.Command.Notice.CommandNotFound"));
|
||||
return;
|
||||
}
|
||||
|
||||
var msg =
|
||||
$"/{command.Name} - {I18NManager.Translate(command.Description)}\n{I18NManager.Translate(command.Usage)}";
|
||||
if (command.Alias.Length > 0)
|
||||
msg +=
|
||||
$"\n{I18NManager.Translate("Game.Command.Help.CommandAlias")} {command.Alias.ToList().ToArrayString()}";
|
||||
if (command.Perm != null)
|
||||
msg += $"\n{I18NManager.Translate("Game.Command.Help.CommandPermission")} {string.Join(", ", command.Perm.Select(perm => perm.ToString()))}";
|
||||
|
||||
await arg.SendMsg(msg + "\n");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
await arg.SendMsg(I18NManager.Translate("Game.Command.Help.Commands"));
|
||||
if (CommandManager.CommandInfo == null) return;
|
||||
|
||||
foreach (var command in CommandManager.CommandInfo.Values)
|
||||
{
|
||||
var msg =
|
||||
$"/{command.Name} - {I18NManager.Translate(command.Description)}\n{I18NManager.Translate(command.Usage)}";
|
||||
if (command.Alias.Length > 0)
|
||||
msg +=
|
||||
$"\n{I18NManager.Translate("Game.Command.Help.CommandAlias")} {command.Alias.ToList().ToArrayString()}";
|
||||
|
||||
if (command.Perm != null)
|
||||
msg += $"\n{I18NManager.Translate("Game.Command.Help.CommandPermission")} {string.Join(", ", command.Perm.Select(perm => perm.ToString()))}";
|
||||
await arg.SendMsg(msg + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
GameServer/Game/BasePlayerManager.cs
Normal file
8
GameServer/Game/BasePlayerManager.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
|
||||
namespace MikuSB.GameServer.Game;
|
||||
|
||||
public class BasePlayerManager(PlayerInstance player)
|
||||
{
|
||||
public PlayerInstance Player { get; private set; } = player;
|
||||
}
|
||||
98
GameServer/Game/Player/PlayerInstance.cs
Normal file
98
GameServer/Game/Player/PlayerInstance.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Database.Account;
|
||||
using MikuSB.Database.Player;
|
||||
using MikuSB.GameServer.Server;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util.Extensions;
|
||||
|
||||
namespace MikuSB.GameServer.Game.Player;
|
||||
|
||||
public class PlayerInstance(PlayerGameData data)
|
||||
{
|
||||
#region Property
|
||||
public Connection? Connection { get; set; }
|
||||
|
||||
public static readonly List<PlayerInstance> _playerInstances = [];
|
||||
public int Uid { get; set; }
|
||||
public bool Initialized { get; set; }
|
||||
public bool IsNewPlayer { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data & Manager
|
||||
|
||||
public PlayerGameData Data { get; set; } = data;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initializers
|
||||
public PlayerInstance(int uid) : this(new PlayerGameData { Uid = uid })
|
||||
{
|
||||
// new player
|
||||
IsNewPlayer = true;
|
||||
Data.Name = AccountData.GetAccountByUid(uid)?.Username;
|
||||
|
||||
DatabaseHelper.CreateInstance(Data);
|
||||
|
||||
var t = Task.Run(async () =>
|
||||
{
|
||||
await InitialPlayerManager();
|
||||
});
|
||||
t.Wait();
|
||||
|
||||
Initialized = true;
|
||||
|
||||
}
|
||||
private async ValueTask InitialPlayerManager()
|
||||
{
|
||||
Uid = Data.Uid;
|
||||
Data.LastActiveTime = Extensions.GetUnixSec();
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
public T InitializeDatabase<T>() where T : BaseDatabaseDataHelper, new()
|
||||
{
|
||||
var instance = DatabaseHelper.GetInstanceOrCreateNew<T>(Uid);
|
||||
return instance!;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network
|
||||
public async ValueTask OnEnterGame()
|
||||
{
|
||||
if (!Initialized) await InitialPlayerManager();
|
||||
}
|
||||
|
||||
public async ValueTask OnLogin()
|
||||
{
|
||||
_playerInstances.Add(this);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static PlayerInstance? GetPlayerInstanceByUid(long uid)
|
||||
=> _playerInstances.FirstOrDefault(player => player.Uid == uid);
|
||||
public void OnLogoutAsync()
|
||||
{
|
||||
_playerInstances.Remove(this);
|
||||
}
|
||||
public async ValueTask SendPacket(BasePacket packet)
|
||||
{
|
||||
if (Connection?.IsOnline == true) await Connection.SendPacket(packet);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Actions
|
||||
public async ValueTask OnHeartBeat()
|
||||
{
|
||||
DatabaseHelper.ToSaveUidList.SafeAdd(Uid);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Serialization
|
||||
|
||||
#endregion
|
||||
}
|
||||
28
GameServer/GameServer.csproj
Normal file
28
GameServer/GameServer.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
<RootNamespace>MikuSB.GameServer</RootNamespace>
|
||||
<StartupObject></StartupObject>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AssemblyName>MikuGameServer</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Game\Hero\**" />
|
||||
<EmbeddedResource Remove="Game\Hero\**" />
|
||||
<None Remove="Game\Hero\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
|
||||
<ProjectReference Include="..\Proto\Proto.csproj" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
142
GameServer/Server/Connection.cs
Normal file
142
GameServer/Server/Connection.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using MikuSB.Enums.Packet;
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
using MikuSB.GameServer.Server.Packet;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MikuSB.GameServer.Server;
|
||||
|
||||
public class Connection(Socket socket, IPEndPoint remote) : SocketConnection(socket, remote)
|
||||
{
|
||||
private static readonly Logger Logger = new("GameServer");
|
||||
|
||||
public PlayerInstance? Player { get; set; }
|
||||
|
||||
private static readonly HashSet<string> DummyPacketNames =
|
||||
[
|
||||
|
||||
];
|
||||
|
||||
public override async void Start()
|
||||
{
|
||||
Logger.Info($"New connection from {RemoteEndPoint}.");
|
||||
State = SessionStateEnum.WAITING_FOR_TOKEN;
|
||||
await ReceiveLoop();
|
||||
}
|
||||
|
||||
public override void Stop(bool isServerStop = false)
|
||||
{
|
||||
Player?.OnLogoutAsync();
|
||||
SocketListener.UnregisterConnection(this);
|
||||
base.Stop(isServerStop);
|
||||
}
|
||||
|
||||
public static int GetInt32(byte[] buf, int index)
|
||||
{
|
||||
int networkValue = BitConverter.ToInt32(buf, index);
|
||||
return IPAddress.NetworkToHostOrder((int)networkValue);
|
||||
}
|
||||
|
||||
protected async Task ReceiveLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = new NetworkStream(Socket, ownsSocket: false);
|
||||
|
||||
while (SocketConnected())
|
||||
{
|
||||
var decodedPacket = await new PacketCodec().ReadPacketAsync(stream, CancelToken.Token);
|
||||
|
||||
if (decodedPacket == null)
|
||||
{
|
||||
Logger.Info("Client disconnected");
|
||||
break;
|
||||
}
|
||||
|
||||
switch (decodedPacket.Framing)
|
||||
{
|
||||
case PacketFraming.FourByteLittleEndianLength:
|
||||
case PacketFraming.TwoByteBigEndianLength:
|
||||
Framing = decodedPacket.Framing;
|
||||
LogPacket("Recv", decodedPacket.CmdId, decodedPacket.Body.ToArray(),Framing);
|
||||
await HandlePacket(decodedPacket.CmdId, decodedPacket.Body.ToArray());
|
||||
break;
|
||||
|
||||
case PacketFraming.Control:
|
||||
Logger.Info("Control packet received");
|
||||
// Handle control packet if needed
|
||||
break;
|
||||
|
||||
case PacketFraming.Unknown:
|
||||
Logger.Warn("Unknown packet format received");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.Info("ReceiveLoop cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Info($"ReceiveLoop error: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Socket.Close();
|
||||
}
|
||||
Stop();
|
||||
}
|
||||
|
||||
private async Task HandlePacket(ushort opcode, byte[] payload)
|
||||
{
|
||||
var packetName = LogMap.GetValueOrDefault(opcode);
|
||||
if (DummyPacketNames.Contains(packetName!))
|
||||
{
|
||||
await SendDummy(packetName!);
|
||||
Logger.Info($"[Dummy] Send Dummy {packetName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the Handler for this opcode
|
||||
var handler = HandlerManager.GetHandler(opcode);
|
||||
if (handler != null)
|
||||
{
|
||||
// Handle
|
||||
// Make sure session is ready for packets
|
||||
var state = State;
|
||||
try
|
||||
{
|
||||
await handler.OnHandle(this, payload, (ushort)DownStreamSeqNo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e.Message, e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConfigManager.Config.ServerOption.EnableDebug &&
|
||||
ConfigManager.Config.ServerOption.DebugNoHandlerPacket && !IgnoreLog.Contains(opcode))
|
||||
Logger.Error($"No handler found for {packetName}({opcode})");
|
||||
|
||||
//if (ConfigManager.Config.ServerOption.AutoSendResponseWhenNoHandler)
|
||||
//{
|
||||
// await SendDummy(packetName);
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
private async Task SendDummy(string packetName)
|
||||
{
|
||||
var respName = packetName.Replace("Req", "Rsp"); // Get the response packet name
|
||||
if (respName == packetName) return; // do not send rsp when resp name = recv name
|
||||
var respOpcode = LogMap.FirstOrDefault(x => x.Value == respName).Key; // Get the response opcode
|
||||
|
||||
// Send Rsp
|
||||
await SendPacket(respOpcode);
|
||||
}
|
||||
}
|
||||
13
GameServer/Server/Listener.cs
Normal file
13
GameServer/Server/Listener.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using MikuSB.TcpSharp;
|
||||
|
||||
namespace MikuSB.GameServer.Server;
|
||||
|
||||
public class Listener : SocketListener
|
||||
{
|
||||
public static Connection? GetActiveConnection(int uid)
|
||||
{
|
||||
var con = Connections.Values.FirstOrDefault(c =>
|
||||
(c as Connection)?.Player?.Uid == uid && c.State == SessionStateEnum.ACTIVE) as Connection;
|
||||
return con;
|
||||
}
|
||||
}
|
||||
6
GameServer/Server/Packet/Handler.cs
Normal file
6
GameServer/Server/Packet/Handler.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MikuSB.GameServer.Server.Packet;
|
||||
|
||||
public abstract class Handler
|
||||
{
|
||||
public abstract Task OnHandle(Connection connection, byte[] data, ushort SeqNo = 0);
|
||||
}
|
||||
31
GameServer/Server/Packet/HandlerManager.cs
Normal file
31
GameServer/Server/Packet/HandlerManager.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.GameServer.Server.Packet;
|
||||
|
||||
public static class HandlerManager
|
||||
{
|
||||
public static Dictionary<int, Handler> handlers = [];
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
var classes = Assembly.GetExecutingAssembly().GetTypes(); // Get all classes in the assembly
|
||||
foreach (var cls in classes)
|
||||
{
|
||||
var attribute = (Opcode?)Attribute.GetCustomAttribute(cls, typeof(Opcode));
|
||||
|
||||
if (attribute != null) handlers.Add(attribute.CmdId, (Handler)Activator.CreateInstance(cls)!);
|
||||
}
|
||||
}
|
||||
|
||||
public static Handler? GetHandler(int cmdId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return handlers[cmdId];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
GameServer/Server/Packet/Opcode.cs
Normal file
7
GameServer/Server/Packet/Opcode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MikuSB.GameServer.Server.Packet;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class Opcode(int cmdId) : Attribute
|
||||
{
|
||||
public int CmdId = cmdId;
|
||||
}
|
||||
50
GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs
Normal file
50
GameServer/Server/Packet/Recv/Login/HandlerReqLogin.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using MikuSB.Data;
|
||||
using MikuSB.Database;
|
||||
using MikuSB.Database.Account;
|
||||
using MikuSB.Database.Player;
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
using MikuSB.GameServer.Server.Packet.Send.Login;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
|
||||
namespace MikuSB.GameServer.Server.Packet.Recv.Login;
|
||||
|
||||
[Opcode(CmdIds.ReqLogin)]
|
||||
public class HandlerReqLogin : Handler
|
||||
{
|
||||
public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo)
|
||||
{
|
||||
var req = ReqLogin.Parser.ParseFrom(data);
|
||||
var account = AccountData.GetAccountByUid(1);
|
||||
if (account == null)
|
||||
{
|
||||
AccountData.CreateAccount("MIKU", 0, "");
|
||||
account = AccountData.GetAccountByUid(1);
|
||||
if (account == null)
|
||||
{
|
||||
await connection.SendPacket(CmdIds.NtfLogout);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!ResourceManager.IsLoaded)
|
||||
// resource manager not loaded, return
|
||||
return;
|
||||
var prev = Listener.GetActiveConnection(account.Uid);
|
||||
if (prev != null)
|
||||
{
|
||||
await connection.SendPacket(CmdIds.NtfLogout);
|
||||
prev.Stop();
|
||||
}
|
||||
|
||||
connection.State = SessionStateEnum.WAITING_FOR_LOGIN;
|
||||
var pd = DatabaseHelper.GetInstance<PlayerGameData>(account.Uid);
|
||||
connection.Player = pd == null ? new PlayerInstance(account.Uid) : new PlayerInstance(pd);
|
||||
|
||||
connection.DebugFile = Path.Combine(ConfigManager.Config.Path.LogPath, "Debug/", $"{account.Uid}/",
|
||||
$"Debug-{DateTime.Now:yyyy-MM-dd HH-mm-ss}.log");
|
||||
await connection.Player.OnEnterGame();
|
||||
connection.Player.Connection = connection;
|
||||
await connection.SendPacket(new PacketRspLogin(connection.Player!));
|
||||
}
|
||||
}
|
||||
29
GameServer/Server/Packet/Send/Login/PacketRspLogin.cs
Normal file
29
GameServer/Server/Packet/Send/Login/PacketRspLogin.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MikuSB.GameServer.Game.Player;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.Util.Extensions;
|
||||
|
||||
namespace MikuSB.GameServer.Server.Packet.Send.Login;
|
||||
|
||||
public class PacketRspLogin : BasePacket
|
||||
{
|
||||
public PacketRspLogin(PlayerInstance player) : base(CmdIds.RspLogin)
|
||||
{
|
||||
var proto = new RspLogin
|
||||
{
|
||||
Timestamp = (uint)Extensions.GetUnixSec(),
|
||||
WorldChannel = 1,
|
||||
AreaId = 1,
|
||||
Data = new Player
|
||||
{
|
||||
Pid = (ulong)player.Data.Uid,
|
||||
Account = player.Data.Name,
|
||||
Name = player.Data.Name,
|
||||
Level = 80
|
||||
},
|
||||
NeedRename = false
|
||||
};
|
||||
|
||||
SetData(proto);
|
||||
}
|
||||
}
|
||||
60
MikuSB.sln
Normal file
60
MikuSB.sln
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.9.34616.47
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SdkServer", "SdkServer\SdkServer.csproj", "{A84C0D8D-BF2E-449A-A46C-F5BE6FD2F42F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{0690883A-D749-42F3-88CB-41D2F627C862}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameServer", "GameServer\GameServer.csproj", "{8E3A0EA5-F4BC-4478-AEB9-CAAC07F10BD3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C822679-4BCC-497A-AF15-F441EC750CCE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MikuSB", "MikuSB\MikuSB.csproj", "{71D8488F-CAED-48EE-BD5C-F325FBAB991F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proto", "Proto\Proto.csproj", "{8A0ECA1A-167B-4B97-BF79-3665AF654A52}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TcpSharp", "TcpSharp\TcpSharp.csproj", "{CD7EFAA3-C655-40EE-8F6A-A8E2DA3B0FCB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A84C0D8D-BF2E-449A-A46C-F5BE6FD2F42F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A84C0D8D-BF2E-449A-A46C-F5BE6FD2F42F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A84C0D8D-BF2E-449A-A46C-F5BE6FD2F42F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A84C0D8D-BF2E-449A-A46C-F5BE6FD2F42F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0690883A-D749-42F3-88CB-41D2F627C862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0690883A-D749-42F3-88CB-41D2F627C862}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0690883A-D749-42F3-88CB-41D2F627C862}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0690883A-D749-42F3-88CB-41D2F627C862}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8E3A0EA5-F4BC-4478-AEB9-CAAC07F10BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8E3A0EA5-F4BC-4478-AEB9-CAAC07F10BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8E3A0EA5-F4BC-4478-AEB9-CAAC07F10BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8E3A0EA5-F4BC-4478-AEB9-CAAC07F10BD3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{71D8488F-CAED-48EE-BD5C-F325FBAB991F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{71D8488F-CAED-48EE-BD5C-F325FBAB991F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{71D8488F-CAED-48EE-BD5C-F325FBAB991F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{71D8488F-CAED-48EE-BD5C-F325FBAB991F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8A0ECA1A-167B-4B97-BF79-3665AF654A52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8A0ECA1A-167B-4B97-BF79-3665AF654A52}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A0ECA1A-167B-4B97-BF79-3665AF654A52}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A0ECA1A-167B-4B97-BF79-3665AF654A52}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CD7EFAA3-C655-40EE-8F6A-A8E2DA3B0FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CD7EFAA3-C655-40EE-8F6A-A8E2DA3B0FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CD7EFAA3-C655-40EE-8F6A-A8E2DA3B0FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CD7EFAA3-C655-40EE-8F6A-A8E2DA3B0FCB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {05A94C2B-B569-45D2-AB39-3F26D02E421A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
21
MikuSB/MikuSB.csproj
Normal file
21
MikuSB/MikuSB.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
<RootNamespace>MikuSB.MikuSB</RootNamespace>
|
||||
<AssemblyName>MikuSB</AssemblyName>
|
||||
<ApplicationIcon>Source\Kiana.ico</ApplicationIcon>
|
||||
<SatelliteResourceLanguages>false</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\GameServer\GameServer.csproj" />
|
||||
<ProjectReference Include="..\SdkServer\SdkServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
178
MikuSB/Program/LoaderManager.cs
Normal file
178
MikuSB/Program/LoaderManager.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MikuSB.Data;
|
||||
using MikuSB.Database;
|
||||
using MikuSB.GameServer.Command;
|
||||
using MikuSB.GameServer.Server;
|
||||
using MikuSB.GameServer.Server.Packet;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.MikuSB.Tool;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.SdkServer;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
using MikuSB.Util.Security;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.MikuSB.Program;
|
||||
|
||||
public class LoaderManager : MikuSB
|
||||
{
|
||||
public static void InitConfig()
|
||||
{
|
||||
// Initialize log
|
||||
var counter = 0;
|
||||
FileInfo file;
|
||||
while (true)
|
||||
{
|
||||
file = new FileInfo(ConfigManager.Config.Path.LogPath + $"/{DateTime.Now:yyyy-MM-dd}-{++counter}.log");
|
||||
if (file is not { Exists: false, Directory: not null }) continue;
|
||||
file.Directory.Create();
|
||||
break;
|
||||
}
|
||||
Logger.SetLogFile(file);
|
||||
|
||||
// Init all directories
|
||||
try
|
||||
{
|
||||
ConfigManager.InitDirectories();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(I18NManager.Translate("Server.ServerInfo.FailedToLoadItem", I18NManager.Translate("Word.Config")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
|
||||
// Starting the server
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.StartingServer"));
|
||||
|
||||
// Load the config
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.Config")));
|
||||
try
|
||||
{
|
||||
ConfigManager.LoadConfig();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToLoadItem", I18NManager.Translate("Word.Config")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the language
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.Language")));
|
||||
try
|
||||
{
|
||||
I18NManager.LoadLanguage();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToLoadItem", I18NManager.Translate("Word.Language")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitDatabase()
|
||||
{
|
||||
// Initialize the database
|
||||
try
|
||||
{
|
||||
_ = Task.Run(DatabaseHelper.Initialize); // do not wait
|
||||
|
||||
while (!DatabaseHelper.LoadAccount) Thread.Sleep(100);
|
||||
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItem",
|
||||
I18NManager.Translate("Word.DatabaseAccount")));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToLoadItem", I18NManager.Translate("Word.Database")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task InitSdkServer()
|
||||
{
|
||||
SdkServer.SdkServer.Start([]);
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerRunning", I18NManager.Translate("Word.Dispatch"),
|
||||
ConfigManager.Config.HttpServer.GetDisplayAddress()));
|
||||
|
||||
//KcpListener.BaseConnection = typeof(Connection);
|
||||
//KcpListener.StartListener();
|
||||
SocketListener.BaseConnection = typeof(Connection);
|
||||
SocketListener.StartListener();
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static void InitPacket()
|
||||
{
|
||||
// get opcode from CmdIds
|
||||
var opcodes = typeof(CmdIds).GetFields().Where(x => x.FieldType == typeof(int)).ToList();
|
||||
foreach (var opcode in opcodes)
|
||||
{
|
||||
var name = opcode.Name;
|
||||
var value = (int)opcode.GetValue(null)!;
|
||||
SocketConnection.LogMap.TryAdd(value, name);
|
||||
}
|
||||
|
||||
HandlerManager.Init();
|
||||
}
|
||||
|
||||
public static async Task InitResource()
|
||||
{
|
||||
// Init custom files
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.GeneratingItem", I18NManager.Translate("Word.CustomData")));
|
||||
try
|
||||
{
|
||||
await AssemblyGenerater.LoadCustomData(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToLoadItem", I18NManager.Translate("Word.CustomData")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the game data
|
||||
try
|
||||
{
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.GameData")));
|
||||
ResourceManager.LoadGameData();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToLoadItem", I18NManager.Translate("Word.GameData")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitCommand()
|
||||
{
|
||||
// Register the command handlers
|
||||
try
|
||||
{
|
||||
CommandManager.RegisterCommands();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(
|
||||
I18NManager.Translate("Server.ServerInfo.FailedToInitializeItem",
|
||||
I18NManager.Translate("Word.Command")), e);
|
||||
Console.ReadLine();
|
||||
return;
|
||||
}
|
||||
IConsole.OnConsoleExcuteCommand += CommandExecutor.ConsoleExcuteCommand;
|
||||
CommandExecutor.OnRunCommand += (sender, e) => { CommandManager.HandleCommand(e, sender); };
|
||||
|
||||
IConsole.ListenConsole();
|
||||
}
|
||||
}
|
||||
90
MikuSB/Program/MikuSB.cs
Normal file
90
MikuSB/Program/MikuSB.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using MikuSB.Data;
|
||||
using MikuSB.Database;
|
||||
using MikuSB.MikuSB.Tool;
|
||||
using MikuSB.GameServer.Command;
|
||||
using MikuSB.GameServer.Server;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.TcpSharp;
|
||||
using MikuSB.Util;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MikuSB.MikuSB.Program;
|
||||
|
||||
public class MikuSB
|
||||
{
|
||||
public static readonly Logger Logger = new("MikuSB");
|
||||
public static readonly DatabaseHelper DatabaseHelper = new();
|
||||
public static readonly Listener Listener = new();
|
||||
public static readonly CommandManager CommandManager = new();
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
var time = DateTime.Now;
|
||||
RegisterExitEvent();
|
||||
IConsole.InitConsole();
|
||||
LoaderManager.InitConfig();
|
||||
await LoaderManager.InitSdkServer();
|
||||
LoaderManager.InitPacket();
|
||||
|
||||
LoaderManager.InitDatabase();
|
||||
if (!DatabaseHelper.LoadAllData)
|
||||
{
|
||||
var t = Task.Run(() =>
|
||||
{
|
||||
while (!DatabaseHelper.LoadAllData) // wait for all data to be loaded
|
||||
Thread.Sleep(100);
|
||||
});
|
||||
|
||||
await t.WaitAsync(new CancellationToken());
|
||||
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItem", I18NManager.Translate("Word.Database")));
|
||||
}
|
||||
|
||||
Logger.Warn(I18NManager.Translate("Server.ServerInfo.WaitForAllDone"));
|
||||
|
||||
await LoaderManager.InitResource();
|
||||
ResourceManager.IsLoaded = true;
|
||||
|
||||
HandbookGenerator.GenerateAll();
|
||||
LoaderManager.InitCommand();
|
||||
|
||||
var elapsed = DateTime.Now - time;
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerStarted",
|
||||
Math.Round(elapsed.TotalSeconds, 2).ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
# region Exit
|
||||
|
||||
private static void RegisterExitEvent()
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
{
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.Shutdown"));
|
||||
ProcessExit();
|
||||
};
|
||||
AppDomain.CurrentDomain.UnhandledException += (obj, arg) =>
|
||||
{
|
||||
Logger.Error(I18NManager.Translate("Server.ServerInfo.UnhandledException", obj.GetType().Name),
|
||||
(Exception)arg.ExceptionObject);
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.Shutdown"));
|
||||
ProcessExit();
|
||||
Environment.Exit(1);
|
||||
};
|
||||
|
||||
Console.CancelKeyPress += (_, eventArgs) =>
|
||||
{
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.CancelKeyPressed"));
|
||||
eventArgs.Cancel = true;
|
||||
Environment.Exit(0);
|
||||
};
|
||||
}
|
||||
|
||||
private static void ProcessExit()
|
||||
{
|
||||
SocketListener.Connections.Values.ToList().ForEach(x => x.Stop(true));
|
||||
DatabaseHelper.SaveThread?.Interrupt();
|
||||
DatabaseHelper.SaveDatabase();
|
||||
}
|
||||
|
||||
# endregion
|
||||
}
|
||||
18
MikuSB/Properties/PublishProfiles/MikuSB-Win64-Debug.pubxml
Normal file
18
MikuSB/Properties/PublishProfiles/MikuSB-Win64-Debug.pubxml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\MikuSB-Win64-Debug</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\MikuSB-MultiFile\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>bin\MikuSB-OneFile\</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
BIN
MikuSB/Source/Kiana.ico
Normal file
BIN
MikuSB/Source/Kiana.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
38
MikuSB/Tool/AssemblyGenerater.cs
Normal file
38
MikuSB/Tool/AssemblyGenerater.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using MikuSB.Util;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.MikuSB.Tool;
|
||||
|
||||
public class AssemblyGenerater
|
||||
{
|
||||
private static readonly string SourceSpace = "MikuSB.MikuSB.Source.";
|
||||
|
||||
public static async ValueTask LoadCustomData(Assembly assembly)
|
||||
{
|
||||
string[] embededRes = assembly.GetManifestResourceNames();
|
||||
foreach (var res in embededRes)
|
||||
{
|
||||
var stream = assembly.GetManifestResourceStream(res);
|
||||
if (stream != null && res.Contains(ConfigManager.Config.Path.DataPath.Split("/").Last()))
|
||||
await WriteOutputFiles(stream, res);
|
||||
}
|
||||
}
|
||||
|
||||
private async static ValueTask WriteOutputFiles(Stream stream, string resSpace)
|
||||
{
|
||||
if (stream == null) return;
|
||||
|
||||
string relativePath = resSpace.Replace(SourceSpace, "");
|
||||
int lastDotIndex = relativePath.LastIndexOf('.');
|
||||
string outputPath = string.Concat(
|
||||
ConfigManager.Config.Path.ConfigPath, "/",
|
||||
relativePath[..lastDotIndex].Replace('.', '/'),
|
||||
relativePath.AsSpan(lastDotIndex));
|
||||
|
||||
if (File.Exists(outputPath)) return; // Check if file exist
|
||||
|
||||
using var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
|
||||
stream.Position = 0;
|
||||
await stream.CopyToAsync(fileStream);
|
||||
}
|
||||
}
|
||||
102
MikuSB/Tool/HandbookGenerator.cs
Normal file
102
MikuSB/Tool/HandbookGenerator.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using MikuSB.Data;
|
||||
using MikuSB.GameServer.Command;
|
||||
using MikuSB.Internationalization;
|
||||
using MikuSB.Util;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MikuSB.MikuSB.Tool;
|
||||
|
||||
public static class HandbookGenerator
|
||||
{
|
||||
public static void GenerateAll()
|
||||
{
|
||||
var directory = new DirectoryInfo(ConfigManager.Config.Path.ResourcePath + "/TextMap");
|
||||
var handbook = new DirectoryInfo(ConfigManager.Config.Path.HandbookPath);
|
||||
if (!handbook.Exists) handbook.Create();
|
||||
if (!directory.Exists) return;
|
||||
|
||||
foreach (var langFile in directory.GetFiles())
|
||||
{
|
||||
if (langFile.Extension != ".json") continue;
|
||||
var lang = langFile.Name.Replace("TextMap", "").Replace(".json", "");
|
||||
|
||||
// Check if handbook needs to regenerate
|
||||
var handbookPath = $"{ConfigManager.Config.Path.HandbookPath}/Handbook{lang}.txt";
|
||||
if (File.Exists(handbookPath))
|
||||
{
|
||||
var handbookInfo = new FileInfo(handbookPath);
|
||||
if (handbookInfo.LastWriteTime >= langFile.LastWriteTime)
|
||||
continue; // Skip if handbook is newer than language file
|
||||
}
|
||||
|
||||
Generate(lang);
|
||||
}
|
||||
|
||||
Logger.GetByClassName()
|
||||
.Info(I18NManager.Translate("Server.ServerInfo.GeneratedItem", I18NManager.Translate("Word.Handbook")));
|
||||
}
|
||||
|
||||
public static void Generate(string lang)
|
||||
{
|
||||
var textMapPath = ConfigManager.Config.Path.ResourcePath + "/TextMap/TextMap" + lang + ".json";
|
||||
|
||||
if (!File.Exists(textMapPath))
|
||||
{
|
||||
Logger.GetByClassName().Error(I18NManager.Translate("Server.ServerInfo.FailedToReadItem", textMapPath,
|
||||
I18NManager.Translate("Word.NotFound")));
|
||||
return;
|
||||
}
|
||||
|
||||
List<TextMapEntry> textMapList = JsonConvert.DeserializeObject<List<TextMapEntry>>(File.ReadAllText(textMapPath))!;
|
||||
|
||||
if (textMapList == null)
|
||||
{
|
||||
Logger.GetByClassName().Error(I18NManager.Translate("Server.ServerInfo.FailedToReadItem", textMapPath,
|
||||
I18NManager.Translate("Word.Error")));
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<long, string> textMap = [];
|
||||
|
||||
foreach (var map in textMapList) textMap.Add(map.Value!.Hash, map.Text!);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("#Handbook generated in " + DateTime.Now.ToString("yyyy/MM/dd HH:mm"));
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("#Command");
|
||||
builder.AppendLine();
|
||||
GenerateCmd(builder, lang);
|
||||
|
||||
builder.AppendLine();
|
||||
WriteToFile(lang, builder.ToString());
|
||||
}
|
||||
|
||||
public static void GenerateCmd(StringBuilder builder, string lang)
|
||||
{
|
||||
foreach (var cmd in CommandManager.CommandInfo)
|
||||
{
|
||||
builder.Append("\t" + cmd.Key);
|
||||
var desc = I18NManager.TranslateAsCertainLang(lang, cmd.Value.Description).Replace("\n", "\n\t\t");
|
||||
builder.AppendLine(": " + desc);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteToFile(string lang, string content)
|
||||
{
|
||||
File.WriteAllText($"{ConfigManager.Config.Path.HandbookPath}/Handbook{lang}.txt", content);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TextMapEntry
|
||||
{
|
||||
[JsonPropertyName("value")] public ValueEntry? Value { get; set; }
|
||||
[JsonPropertyName("text")] public string? Text { get; set; }
|
||||
}
|
||||
|
||||
public class ValueEntry
|
||||
{
|
||||
[JsonPropertyName("hash")] public int Hash { get; set; }
|
||||
}
|
||||
136
Proto/CmdIds.cs
Normal file
136
Proto/CmdIds.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
namespace MikuSB.Proto;
|
||||
|
||||
public class CmdIds
|
||||
{
|
||||
public const int None = 0;
|
||||
public const int ReqLogin = 1;
|
||||
public const int RspLogin = 2;
|
||||
public const int ReqReconnect = 3;
|
||||
public const int RspReconnect = 4;
|
||||
public const int ReqRename = 5;
|
||||
public const int RspRename = 6;
|
||||
public const int ReqCallGs = 7;
|
||||
public const int RspCallGs = 8;
|
||||
public const int ReqUseItem = 9;
|
||||
public const int RspUseItem = 10;
|
||||
public const int ReqReadMail = 11;
|
||||
public const int RspReadMail = 12;
|
||||
public const int ReqMailAttachment = 13;
|
||||
public const int RspMailAttachment = 14;
|
||||
public const int ReqDelMail = 15;
|
||||
public const int RspDelMail = 16;
|
||||
public const int ReqSetNewGuide = 17;
|
||||
public const int RspSetNewGuide = 18;
|
||||
public const int ReqAccountInfo = 19;
|
||||
public const int RspAccountInfo = 20;
|
||||
public const int ReqResign = 23;
|
||||
public const int RspResign = 24;
|
||||
public const int ReqRecord = 25;
|
||||
public const int RspRecord = 26;
|
||||
public const int ReqAddFriendReq = 27;
|
||||
public const int RspAddFriendReq = 28;
|
||||
public const int ReqAgreeFriendReq = 29;
|
||||
public const int RspAgreeFriendReq = 30;
|
||||
public const int ReqRefuseFriendReq = 31;
|
||||
public const int RspRefuseFriendReq = 32;
|
||||
public const int ReqRemoveFriend = 33;
|
||||
public const int RspRemoveFriend = 34;
|
||||
public const int ReqGiveFriendVigor = 35;
|
||||
public const int RspGiveFriendVigor = 36;
|
||||
public const int ReqRecvFriendVigor = 37;
|
||||
public const int RspRecvFriendVigor = 38;
|
||||
public const int ReqPlayerRecommend = 39;
|
||||
public const int RspPlayerRecommend = 40;
|
||||
public const int ReqAddBlockList = 41;
|
||||
public const int RspAddBlockList = 42;
|
||||
public const int ReqDelBlockList = 43;
|
||||
public const int RspDelBlockList = 44;
|
||||
public const int ReqFindPlayer = 45;
|
||||
public const int RspFindPlayer = 46;
|
||||
public const int ReqPlayerProfile = 47;
|
||||
public const int RspPlayerProfile = 48;
|
||||
public const int ReqGetVersion = 49;
|
||||
public const int RspGetVersion = 50;
|
||||
public const int ReqRankList = 51;
|
||||
public const int RspRankList = 52;
|
||||
public const int ReqRank = 53;
|
||||
public const int RspRank = 54;
|
||||
public const int ReqBlockFriendReq = 55;
|
||||
public const int RspBlockFriendReq = 56;
|
||||
public const int ReqWordFilter = 57;
|
||||
public const int RspWordFilter = 58;
|
||||
public const int ReqSetCustomRoster = 59;
|
||||
public const int RspSetCustomRoster = 60;
|
||||
public const int ReqGlobalCounter = 61;
|
||||
public const int RspGlobalCounter = 62;
|
||||
public const int ReqMatch = 301;
|
||||
public const int RspMatch = 302;
|
||||
public const int ReqOnlineRoom = 303;
|
||||
public const int RspOnlineRoom = 304;
|
||||
public const int ReqOnlineRoomStart = 305;
|
||||
public const int RspOnlineRoomStart = 306;
|
||||
public const int ReqOnlineRoomExit = 307;
|
||||
public const int RspOnlineRoomExit = 308;
|
||||
public const int ReqOnlineRoomInvite = 309;
|
||||
public const int RspOnlineRoomInvite = 310;
|
||||
public const int ReqOnlineRoomAccept = 311;
|
||||
public const int RspOnlineRoomAccept = 312;
|
||||
public const int ReqOnlineRoomUpdate = 313;
|
||||
public const int RspOnlineRoomUpdate = 314;
|
||||
public const int ReqOnlineRoomReconnect = 315;
|
||||
public const int RspOnlineRoomReconnect = 316;
|
||||
public const int ReqOnlineRoomChatAccept = 317;
|
||||
public const int RspOnlineRoomChatAccept = 318;
|
||||
public const int ReqOnlineRoomUpdateMap = 319;
|
||||
public const int RspOnlineRoomUpdateMap = 320;
|
||||
public const int ReqChangeWorldChannel = 321;
|
||||
public const int RspChangeWorldChannel = 322;
|
||||
public const int ReqWorldChat = 323;
|
||||
public const int RspWorldChat = 324;
|
||||
public const int ReqFriendChat = 325;
|
||||
public const int RspFriendChat = 326;
|
||||
public const int ReqOnlineChat = 327;
|
||||
public const int RspOnlineChat = 328;
|
||||
public const int ReqOnlineRecruit = 329;
|
||||
public const int RspOnlineRecruit = 330;
|
||||
public const int NtfLog = 1001;
|
||||
public const int NtfKickout = 1002;
|
||||
public const int NtfBroadcast = 1003;
|
||||
public const int NtfSyncAttr = 1004;
|
||||
public const int NtfSyncLineup = 1005;
|
||||
public const int NtfSyncNewMail = 1006;
|
||||
public const int NtfSyncDelMail = 1007;
|
||||
public const int NtfPlayerMsg = 1008;
|
||||
public const int NtfLogout = 1009;
|
||||
public const int NtfScript = 1010;
|
||||
public const int NtfSetAttr = 1011;
|
||||
public const int NtfSetStrAttr = 1012;
|
||||
public const int NtfOnlineStart = 1013;
|
||||
public const int NtfOnlineOver = 1014;
|
||||
public const int NtfReadItem = 1015;
|
||||
public const int NtfUpdateFriend = 1016;
|
||||
public const int NtfDelFriend = 1017;
|
||||
public const int NtfFriendReq = 1018;
|
||||
public const int NtfFriendVigor = 1019;
|
||||
public const int NtfBlacklist = 1020;
|
||||
public const int NtfGlobalAttrs = 1021;
|
||||
public const int NtfAntiData = 1022;
|
||||
public const int NtfBlockFriendReq = 1023;
|
||||
public const int NtfCustomRoster = 1024;
|
||||
public const int NtfOnlineRoomInfo = 1031;
|
||||
public const int NtfOnlineLoad = 1032;
|
||||
public const int NtfOnlineKickout = 1033;
|
||||
public const int NtfOnlineInvite = 1034;
|
||||
public const int NtfOnlineState = 1035;
|
||||
public const int NtfWorldChat = 1041;
|
||||
public const int NtfFriendChat = 1042;
|
||||
public const int NtfOnlineChat = 1043;
|
||||
public const int NtfOnlineRecruit = 1044;
|
||||
public const int NtfOnlinePlayerCheat = 1045;
|
||||
public const int ReqRoomStart = 2001;
|
||||
public const int RspRoomStart = 2002;
|
||||
public const int NtfRoomReady = 2003;
|
||||
public const int NtfRoomOver = 2004;
|
||||
public const int NtfStopRoom = 2005;
|
||||
public const int NtfRoomPlayerExit = 2006;
|
||||
}
|
||||
302
Proto/Core.proto
Normal file
302
Proto/Core.proto
Normal file
@@ -0,0 +1,302 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package core;
|
||||
|
||||
option csharp_namespace = "MikuSB.Proto";
|
||||
|
||||
enum Sex {
|
||||
MALE = 0;
|
||||
FEMALE = 1;
|
||||
}
|
||||
|
||||
enum PlayerCoreAttribute {
|
||||
LEVEL = 0;
|
||||
EXP = 1;
|
||||
VIGOR = 2;
|
||||
CHARGED = 3;
|
||||
VIGOR_TIME = 4;
|
||||
}
|
||||
|
||||
enum MailStat {
|
||||
New = 0;
|
||||
Readed = 1;
|
||||
Geted = 2;
|
||||
Removed = 3;
|
||||
}
|
||||
|
||||
enum GlobalMailStat {
|
||||
Default = 0;
|
||||
Banned = 1;
|
||||
Deleted = 2;
|
||||
}
|
||||
|
||||
enum ChatType {
|
||||
SYSTEM = 0;
|
||||
WORLD = 1;
|
||||
FRIEND = 2;
|
||||
ONLINE = 3;
|
||||
}
|
||||
|
||||
message Empty {
|
||||
}
|
||||
|
||||
message SimpleBoolean {
|
||||
bool data = 1;
|
||||
}
|
||||
|
||||
message SimpleUint {
|
||||
uint64 data = 1;
|
||||
}
|
||||
|
||||
message SimpleString {
|
||||
string data = 1;
|
||||
}
|
||||
|
||||
message IDArray {
|
||||
repeated uint64 ids = 1;
|
||||
}
|
||||
|
||||
message StringArray {
|
||||
repeated string list = 1;
|
||||
}
|
||||
|
||||
message PlayerProfileArray {
|
||||
repeated PlayerProfile list = 1;
|
||||
}
|
||||
|
||||
message ChannelOpt {
|
||||
string channel = 1;
|
||||
repeated string subchannels = 2;
|
||||
}
|
||||
|
||||
message SimpleItem {
|
||||
uint32 G = 1;
|
||||
uint32 D = 2;
|
||||
uint32 P = 3;
|
||||
uint32 L = 4;
|
||||
uint32 Count = 5;
|
||||
}
|
||||
|
||||
message Enhance {
|
||||
uint32 level = 1;
|
||||
uint32 exp = 2;
|
||||
uint32 break = 3;
|
||||
uint32 evolue = 4;
|
||||
uint32 trust = 5;
|
||||
uint32 pro_level = 6;
|
||||
repeated uint64 spines = 11;
|
||||
repeated uint32 affixs = 12;
|
||||
}
|
||||
|
||||
message Item {
|
||||
uint64 id = 1;
|
||||
uint64 template = 2;
|
||||
uint32 count = 3;
|
||||
uint32 flag = 4;
|
||||
uint32 userdata = 5;
|
||||
uint32 expiration = 6;
|
||||
Enhance enhance = 7;
|
||||
map<uint32, uint64> slots = 8;
|
||||
}
|
||||
|
||||
message Lineup {
|
||||
uint32 index = 1;
|
||||
string name = 2;
|
||||
uint64 member1 = 3;
|
||||
uint64 member2 = 4;
|
||||
uint64 member3 = 5;
|
||||
}
|
||||
|
||||
message Player {
|
||||
uint64 pid = 1;
|
||||
string account = 2;
|
||||
string provider = 3;
|
||||
string channel = 4;
|
||||
string subchannel = 5;
|
||||
string name = 11;
|
||||
string sign = 12;
|
||||
Sex sex = 13;
|
||||
uint32 level = 14;
|
||||
uint32 exp = 15;
|
||||
uint32 vigor = 16;
|
||||
map<string, int32> money = 17;
|
||||
uint32 charged = 18;
|
||||
uint32 create_time = 31;
|
||||
uint32 last_login_time = 32;
|
||||
uint32 last_vigor_time = 33;
|
||||
uint64 item_id_alloc = 41;
|
||||
repeated Item items = 42;
|
||||
repeated Lineup solutions = 43;
|
||||
map<uint32, uint32> attrs = 44;
|
||||
map<uint32, string> str_attrs = 45;
|
||||
repeated uint64 show_items = 46;
|
||||
repeated uint32 show_attrs = 47;
|
||||
map<uint64, FriendPieces> friend_pieces = 48;
|
||||
uint64 last_pieces = 49;
|
||||
map<uint64, PlayerMail> mail_box = 50;
|
||||
uint32 last_global_mail_time = 51;
|
||||
uint64 last_person_mid = 52;
|
||||
repeated uint64 badges = 53;
|
||||
map<string, PlayerOrder> order_box = 60;
|
||||
repeated uint64 tags = 96;
|
||||
uint64 serial = 97;
|
||||
uint32 ban_type = 98;
|
||||
uint32 ban_expr = 99;
|
||||
}
|
||||
|
||||
message FriendPieces {
|
||||
uint64 index = 1;
|
||||
uint64 pid = 2;
|
||||
uint32 shape = 3;
|
||||
uint32 expr = 4;
|
||||
bool deleted = 5;
|
||||
}
|
||||
|
||||
message PlayerMail {
|
||||
uint64 mid = 1;
|
||||
MailStat stat = 2;
|
||||
uint32 time = 3;
|
||||
uint32 expiration = 4;
|
||||
}
|
||||
|
||||
message Mail {
|
||||
uint64 mid = 1;
|
||||
string title = 2;
|
||||
string message = 3;
|
||||
repeated SimpleItem attachments = 4;
|
||||
string sender = 5;
|
||||
uint64 pid = 6;
|
||||
uint32 time = 7;
|
||||
uint32 expiration = 8;
|
||||
uint32 life = 9;
|
||||
MailStat stat = 10;
|
||||
bool is_deleted = 99;
|
||||
}
|
||||
|
||||
message GlobalMail {
|
||||
uint64 mid = 1;
|
||||
string sender = 2;
|
||||
string title = 3;
|
||||
string message = 4;
|
||||
repeated SimpleItem attachments = 5;
|
||||
uint32 start_time = 6;
|
||||
uint32 end_time = 7;
|
||||
uint32 life = 8;
|
||||
uint32 expiration = 9;
|
||||
uint32 min_level = 10;
|
||||
uint32 max_level = 12;
|
||||
uint32 create_begin = 13;
|
||||
uint32 create_end = 14;
|
||||
repeated ChannelOpt channels = 15;
|
||||
GlobalMailStat stat = 99;
|
||||
}
|
||||
|
||||
message Order {
|
||||
string trade_no = 1;
|
||||
string third_trade_no = 2;
|
||||
uint64 pid = 3;
|
||||
string product_id = 4;
|
||||
uint32 product_quantity = 5;
|
||||
uint32 total_price = 6;
|
||||
uint32 paid_price = 7;
|
||||
string finish_time = 8;
|
||||
bool status = 9;
|
||||
bool refund_status = 10;
|
||||
string refund_time = 11;
|
||||
string subchannel = 12;
|
||||
string priceunit = 13;
|
||||
bool supplement_status = 14;
|
||||
string extendinfo = 15;
|
||||
}
|
||||
|
||||
message PlayerOrder {
|
||||
string trade_no = 1;
|
||||
string subchannel = 2;
|
||||
uint32 done_time = 3;
|
||||
uint32 refund_time = 4;
|
||||
bool is_unreal = 5;
|
||||
uint32 supplement = 6;
|
||||
}
|
||||
|
||||
message PlayerProfile {
|
||||
uint64 pid = 1;
|
||||
string account = 2;
|
||||
uint32 create_time = 3;
|
||||
string name = 4;
|
||||
string sign = 5;
|
||||
Sex sex = 6;
|
||||
uint32 level = 7;
|
||||
uint32 logout_time = 8;
|
||||
uint32 friend_count = 9;
|
||||
repeated Item show_items = 10;
|
||||
repeated uint32 show_attrs = 11;
|
||||
repeated Item badges = 12;
|
||||
repeated uint64 tags = 13;
|
||||
}
|
||||
|
||||
message ClientProfile {
|
||||
uint32 plat_id = 1;
|
||||
string version = 2;
|
||||
string os_version = 3;
|
||||
string os_hardware = 4;
|
||||
string telecom_oper = 5;
|
||||
string network = 6;
|
||||
uint32 screen_width = 7;
|
||||
uint32 screen_height = 8;
|
||||
float density = 9;
|
||||
string cpu_profile = 10;
|
||||
uint32 ram = 11;
|
||||
string gl_render = 12;
|
||||
string gl_version = 13;
|
||||
string device_id = 14;
|
||||
string resource_version = 15;
|
||||
string language = 16;
|
||||
}
|
||||
|
||||
message OnlinePlayer {
|
||||
uint64 pid = 1;
|
||||
string name = 2;
|
||||
uint64 face = 3;
|
||||
uint64 faceframe = 4;
|
||||
uint32 level = 5;
|
||||
Lineup lineup = 6;
|
||||
repeated Item items = 7;
|
||||
bool captain = 8;
|
||||
uint32 stateflag = 9;
|
||||
repeated uint32 girllovelevel = 10;
|
||||
map<uint32, uint32> attrs = 11;
|
||||
}
|
||||
|
||||
message OnlineEndData {
|
||||
uint64 pid = 1;
|
||||
string infodata = 2;
|
||||
uint32 status = 3;
|
||||
}
|
||||
|
||||
message AccountInfo {
|
||||
string account = 1;
|
||||
uint64 pid = 2;
|
||||
uint32 new_guide = 3;
|
||||
repeated uint32 error_info = 4;
|
||||
}
|
||||
|
||||
message ChatMsg {
|
||||
ChatType type = 1;
|
||||
uint32 channel_id = 2;
|
||||
uint64 sender = 3;
|
||||
uint64 recver = 4;
|
||||
uint32 time_stamp = 5;
|
||||
uint64 emoji = 11;
|
||||
string text = 12;
|
||||
PlayerProfile profile = 21;
|
||||
}
|
||||
|
||||
message CustomRoster {
|
||||
uint64 pid = 1;
|
||||
map<string, string> roster = 2;
|
||||
}
|
||||
|
||||
message GlobalAttrs {
|
||||
map<string, uint32> attrs = 1;
|
||||
map<string, string> str_attrs = 2;
|
||||
}
|
||||
26
Proto/Proto.csproj
Normal file
26
Proto/Proto.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
<AssemblyName>MikuProto</AssemblyName>
|
||||
<RootNamespace>MikuSB.Proto</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.29.2" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.68.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="*.proto" ProtoRoot="." GrpcServices="None" />
|
||||
<Protobuf Include="./Protos/*.proto" ProtoRoot="./Protos" GrpcServices="None" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
381
Proto/Snowbreak.proto
Normal file
381
Proto/Snowbreak.proto
Normal file
@@ -0,0 +1,381 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package snowbreak;
|
||||
|
||||
import "core.proto";
|
||||
|
||||
option csharp_namespace = "MikuSB.Proto";
|
||||
|
||||
enum PF {
|
||||
NONE = 0;
|
||||
REQ_LOGIN = 1;
|
||||
RSP_LOGIN = 2;
|
||||
REQ_RECONNECT = 3;
|
||||
RSP_RECONNECT = 4;
|
||||
REQ_RENAME = 5;
|
||||
RSP_RENAME = 6;
|
||||
REQ_CALLGS = 7;
|
||||
RSP_CALLGS = 8;
|
||||
REQ_USEITEM = 9;
|
||||
RSP_USEITEM = 10;
|
||||
REQ_READMAIL = 11;
|
||||
RSP_READMAIL = 12;
|
||||
REQ_MAIL_ATTACHMENT = 13;
|
||||
RSP_MAIL_ATTACHMENT = 14;
|
||||
REQ_DELMAIL = 15;
|
||||
RSP_DELMAIL = 16;
|
||||
REQ_SET_NEWGUIDE = 17;
|
||||
RSP_SET_NEWGUIDE = 18;
|
||||
REQ_ACCOUNTINFO = 19;
|
||||
RSP_ACCOUNTINFO = 20;
|
||||
REQ_RESIGN = 23;
|
||||
RSP_RESIGN = 24;
|
||||
REQ_RECORD = 25;
|
||||
RSP_RECORD = 26;
|
||||
REQ_ADD_FRIENDREQ = 27;
|
||||
RSP_ADD_FRIENDREQ = 28;
|
||||
REQ_AGREE_FRIENDREQ = 29;
|
||||
RSP_AGREE_FRIENDREQ = 30;
|
||||
REQ_REFUSE_FRIENDREQ = 31;
|
||||
RSP_REFUSE_FRIENDREQ = 32;
|
||||
REQ_REMOVE_FRIEND = 33;
|
||||
RSP_REMOVE_FRIEND = 34;
|
||||
REQ_GIVE_FRIENDVIGOR = 35;
|
||||
RSP_GIVE_FRIENDVIGOR = 36;
|
||||
REQ_RECV_FRIENDVIGOR = 37;
|
||||
RSP_RECV_FRIENDVIGOR = 38;
|
||||
REQ_PLAYER_RECOMMEND = 39;
|
||||
RSP_PLAYER_RECOMMEND = 40;
|
||||
REQ_ADD_BLOCKLIST = 41;
|
||||
RSP_ADD_BLOCKLIST = 42;
|
||||
REQ_DEL_BLOCKLIST = 43;
|
||||
RSP_DEL_BLOCKLIST = 44;
|
||||
REQ_FIND_PLAYER = 45;
|
||||
RSP_FIND_PLAYER = 46;
|
||||
REQ_PLAYER_PROFILE = 47;
|
||||
RSP_PLAYER_PROFILE = 48;
|
||||
REQ_GET_VERSION = 49;
|
||||
RSP_GET_VERSION = 50;
|
||||
REQ_RANKLIST = 51;
|
||||
RSP_RANKLIST = 52;
|
||||
REQ_RANK = 53;
|
||||
RSP_RANK = 54;
|
||||
REQ_BLOCK_FRIENDREQ = 55;
|
||||
RSP_BLOCK_FRIENDREQ = 56;
|
||||
REQ_WORD_FILTER = 57;
|
||||
RSP_WORD_FILTER = 58;
|
||||
REQ_SET_CUSTOMROSTER = 59;
|
||||
RSP_SET_CUSTOMROSTER = 60;
|
||||
REQ_GLOBALCOUNTER = 61;
|
||||
RSP_GLOBALCOUNTER = 62;
|
||||
REQ_MATCH = 301;
|
||||
RSP_MATCH = 302;
|
||||
REQ_ONLINE_ROOM = 303;
|
||||
RSP_ONLINE_ROOM = 304;
|
||||
REQ_ONLINE_ROOM_START = 305;
|
||||
RSP_ONLINE_ROOM_START = 306;
|
||||
REQ_ONLINE_ROOM_EXIT = 307;
|
||||
RSP_ONLINE_ROOM_EXIT = 308;
|
||||
REQ_ONLINE_ROOM_INVITE = 309;
|
||||
RSP_ONLINE_ROOM_INVITE = 310;
|
||||
REQ_ONLINE_ROOM_ACCEPT = 311;
|
||||
RSP_ONLINE_ROOM_ACCEPT = 312;
|
||||
REQ_ONLINE_ROOM_UPDATE = 313;
|
||||
RSP_ONLINE_ROOM_UPDATE = 314;
|
||||
REQ_ONLINE_ROOM_RECONNECT = 315;
|
||||
RSP_ONLINE_ROOM_RECONNECT = 316;
|
||||
REQ_ONLINE_ROOM_CHATACCEPT = 317;
|
||||
RSP_ONLINE_ROOM_CHATACCEPT = 318;
|
||||
REQ_ONLINE_ROOM_UPDATEMAP = 319;
|
||||
RSP_ONLINE_ROOM_UPDATEMAP = 320;
|
||||
REQ_CHANGE_WORLD_CHANNEL = 321;
|
||||
RSP_CHANGE_WORLD_CHANNEL = 322;
|
||||
REQ_WORLD_CHAT = 323;
|
||||
RSP_WORLD_CHAT = 324;
|
||||
REQ_FRIEND_CHAT = 325;
|
||||
RSP_FRIEND_CHAT = 326;
|
||||
REQ_ONLINE_CHAT = 327;
|
||||
RSP_ONLINE_CHAT = 328;
|
||||
REQ_ONLINE_RECRUIT = 329;
|
||||
RSP_ONLINE_RECRUIT = 330;
|
||||
NTF_LOG = 1001;
|
||||
NTF_KICKOUT = 1002;
|
||||
NTF_BROADCAST = 1003;
|
||||
NTF_SYNCATTR = 1004;
|
||||
NTF_SYNCLINEUP = 1005;
|
||||
NTF_SYNC_NEW_MAIL = 1006;
|
||||
NTF_SYNC_DEL_MAIL = 1007;
|
||||
NTF_PLAYERMSG = 1008;
|
||||
NTF_LOGOUT = 1009;
|
||||
NTF_SCRIPT = 1010;
|
||||
NTF_SETATTR = 1011;
|
||||
NTF_SETSTRATTR = 1012;
|
||||
NTF_ONLINE_START = 1013;
|
||||
NTF_ONLINE_OVER = 1014;
|
||||
NTF_READITEM = 1015;
|
||||
NTF_UPDATE_FRIEND = 1016;
|
||||
NTF_DEL_FRIEND = 1017;
|
||||
NTF_FRIEND_REQ = 1018;
|
||||
NTF_FRIEND_VIGOR = 1019;
|
||||
NTF_BLACKLIST = 1020;
|
||||
NTF_GLOBALATTRS = 1021;
|
||||
NTF_ANTI_DATA = 1022;
|
||||
NTF_BLOCK_FRIENDREQ = 1023;
|
||||
NTF_CUSTOMROSTER = 1024;
|
||||
NTF_ONLINE_ROOMINFO = 1031;
|
||||
NTF_ONLINE_LOAD = 1032;
|
||||
NTF_ONLINE_KICKOUT = 1033;
|
||||
NTF_ONLINE_INVITE = 1034;
|
||||
NTF_ONLINE_STATE = 1035;
|
||||
NTF_WORLD_CHAT = 1041;
|
||||
NTF_FRIEND_CHAT = 1042;
|
||||
NTF_ONLINE_CHAT = 1043;
|
||||
NTF_ONLINE_RECRUIT = 1044;
|
||||
NTF_ONLINE_PLAYERCHEAT = 1045;
|
||||
REQ_ROOM_START = 2001;
|
||||
RSP_ROOM_START = 2002;
|
||||
NTF_ROOM_READY = 2003;
|
||||
NTF_ROOM_OVER = 2004;
|
||||
NTF_STOP_ROOM = 2005;
|
||||
NTF_ROOM_PLAYEREXIT = 2006;
|
||||
NTF_ROOM_PLAYERCHEAT = 2007;
|
||||
NTF_ROOM_PLAYERFINAL = 2008;
|
||||
}
|
||||
|
||||
message ReqLogin {
|
||||
string provider = 1;
|
||||
string token = 2;
|
||||
core.ClientProfile client_profile = 3;
|
||||
}
|
||||
|
||||
message RspLogin {
|
||||
string session_id = 1;
|
||||
core.Player data = 2;
|
||||
bool need_rename = 3;
|
||||
uint32 area_id = 4;
|
||||
int32 time_zone = 5;
|
||||
uint32 timestamp = 6;
|
||||
int32 certification = 7;
|
||||
map<string, uint32> global_attrs = 8;
|
||||
uint32 world_channel = 9;
|
||||
map<string, string> global_str_attrs = 10;
|
||||
uint32 error_code = 98;
|
||||
repeated uint32 error_info = 99;
|
||||
}
|
||||
|
||||
message ReqReconnect {
|
||||
uint64 pid = 1;
|
||||
string session_id = 2;
|
||||
uint32 world_channel = 3;
|
||||
string language = 4;
|
||||
}
|
||||
|
||||
message RspReconnect {
|
||||
string session_id = 1;
|
||||
core.Player data = 2;
|
||||
bool need_rename = 3;
|
||||
int32 time_zone = 4;
|
||||
uint32 timestamp = 5;
|
||||
uint32 world_channel = 6;
|
||||
}
|
||||
|
||||
message ReqAccountInfo {
|
||||
string provider = 1;
|
||||
string token = 2;
|
||||
}
|
||||
|
||||
message ReqCallGS {
|
||||
string api = 1;
|
||||
string param = 2;
|
||||
uint32 clicknum = 3;
|
||||
repeated string dependent_params = 4;
|
||||
}
|
||||
|
||||
message ReqUseItem {
|
||||
uint64 id = 1;
|
||||
uint32 count = 2;
|
||||
}
|
||||
|
||||
message ReqOnlineCreateRoom {
|
||||
uint32 onlineid = 1;
|
||||
uint32 lineup_index = 2;
|
||||
}
|
||||
|
||||
message RspOnlineCreateRoom {
|
||||
uint32 onlineid = 1;
|
||||
uint32 lineup_index = 2;
|
||||
uint64 roomid = 3;
|
||||
repeated uint32 buffinfo = 4;
|
||||
}
|
||||
|
||||
message ReqOnlineAccept {
|
||||
uint64 otherid = 1;
|
||||
uint32 onlineid = 2;
|
||||
}
|
||||
|
||||
message ReqOnlineChatAccept {
|
||||
uint64 otherid = 1;
|
||||
uint32 onlineid = 2;
|
||||
uint64 roomid = 3;
|
||||
}
|
||||
|
||||
message ReqOnlineRecruit {
|
||||
uint64 room_id = 1;
|
||||
uint32 online_id = 2;
|
||||
}
|
||||
|
||||
message FriendVigor {
|
||||
uint64 pid = 1;
|
||||
bool have_vigor = 2;
|
||||
bool vigor_got = 3;
|
||||
bool return_vigor = 4;
|
||||
}
|
||||
|
||||
message FriendVigorList {
|
||||
repeated FriendVigor list = 1;
|
||||
}
|
||||
|
||||
message RankList {
|
||||
message ListItem {
|
||||
string member_name = 1;
|
||||
uint32 score = 2;
|
||||
string info = 3;
|
||||
}
|
||||
string rank_name = 1;
|
||||
repeated RankList.ListItem list = 2;
|
||||
}
|
||||
|
||||
message RankInfo {
|
||||
uint32 score = 1;
|
||||
uint32 rank = 2;
|
||||
uint32 sum = 3;
|
||||
string info = 4;
|
||||
}
|
||||
|
||||
message GlobalCounterInfo {
|
||||
string counter_name = 1;
|
||||
uint32 value = 2;
|
||||
}
|
||||
|
||||
message NtfLog {
|
||||
string action = 1;
|
||||
string detail = 2;
|
||||
}
|
||||
|
||||
message NtfBroadcast {
|
||||
string msg = 1;
|
||||
uint32 duration = 2;
|
||||
uint32 start_time = 3;
|
||||
uint32 end_time = 4;
|
||||
bool clean = 5;
|
||||
repeated core.ChannelOpt channels = 6;
|
||||
}
|
||||
|
||||
message NtfSyncPlayer {
|
||||
string sign = 1;
|
||||
map<uint32, uint32> core = 2;
|
||||
map<uint32, uint32> custom = 3;
|
||||
map<uint32, string> custom_str = 4;
|
||||
repeated core.Item items = 5;
|
||||
repeated uint64 show_items = 6;
|
||||
repeated uint32 show_attrs = 7;
|
||||
map<string, int32> money = 8;
|
||||
repeated core.FriendPieces pieces = 9;
|
||||
repeated uint64 badges = 10;
|
||||
repeated uint64 tags = 11;
|
||||
}
|
||||
|
||||
message NtfSyncLineup {
|
||||
core.Lineup lineup = 1;
|
||||
bool remove = 2;
|
||||
}
|
||||
|
||||
message NtfCallScript {
|
||||
string api = 1;
|
||||
string arg = 2;
|
||||
NtfSyncPlayer extra_sync = 3;
|
||||
}
|
||||
|
||||
message NtfSetAttr {
|
||||
uint32 gid = 1;
|
||||
uint32 sid = 2;
|
||||
uint32 val = 3;
|
||||
}
|
||||
|
||||
message NtfSetStrAttr {
|
||||
uint32 gid = 1;
|
||||
uint32 sid = 2;
|
||||
string val = 3;
|
||||
}
|
||||
|
||||
message NtfOnlineStart {
|
||||
uint64 room_id = 1;
|
||||
string room_addr = 2;
|
||||
}
|
||||
|
||||
message NtfOnlineRoom {
|
||||
uint64 room_id = 1;
|
||||
uint32 onlineid = 2;
|
||||
repeated core.OnlinePlayer players = 3;
|
||||
bool bmatch = 4;
|
||||
repeated uint32 buffinfo = 5;
|
||||
}
|
||||
|
||||
message NtfOnlineInvite {
|
||||
uint64 room_id = 1;
|
||||
uint32 onlineid = 2;
|
||||
uint64 playerid = 3;
|
||||
string name = 4;
|
||||
uint64 face = 5;
|
||||
uint64 faceframe = 6;
|
||||
uint32 level = 7;
|
||||
}
|
||||
|
||||
message NtfOnlineRecruit {
|
||||
uint64 room_id = 1;
|
||||
uint32 online_id = 2;
|
||||
core.PlayerProfile sender_profile = 3;
|
||||
}
|
||||
|
||||
message NtfOnlineState {
|
||||
uint64 room_id = 1;
|
||||
uint32 onlineid = 2;
|
||||
uint32 matchflag = 3;
|
||||
repeated uint64 players = 4;
|
||||
repeated uint32 stateflag = 5;
|
||||
uint64 nowtime = 6;
|
||||
uint64 levelid = 7;
|
||||
}
|
||||
|
||||
message ReqRoomStart {
|
||||
uint64 room_id = 1;
|
||||
bool is_reday = 2;
|
||||
string error = 3;
|
||||
}
|
||||
|
||||
message RspRoomStart {
|
||||
repeated core.OnlinePlayer players = 1;
|
||||
repeated uint32 buffinfo = 2;
|
||||
uint32 pollingweek = 3;
|
||||
}
|
||||
|
||||
message NtfStopRoom {
|
||||
uint64 room_id = 1;
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
message NtfRoomOver {
|
||||
uint64 room_id = 1;
|
||||
repeated core.OnlineEndData playerinfo = 2;
|
||||
}
|
||||
|
||||
message ReqAntiData {
|
||||
uint32 data_type = 1;
|
||||
bytes mtpData = 2;
|
||||
int32 plat_id = 4;
|
||||
}
|
||||
|
||||
message NtfRoomPlayerCheat {
|
||||
uint64 room_id = 1;
|
||||
uint64 playerid = 2;
|
||||
}
|
||||
285
SdkServer/Handlers/RouteController.cs
Normal file
285
SdkServer/Handlers/RouteController.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MikuSB.Configuration;
|
||||
using MikuSB.SdkServer.Models;
|
||||
using MikuSB.Util;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MikuSB.SdkServer.Handlers;
|
||||
|
||||
[ApiController]
|
||||
public class RouteController : ControllerBase
|
||||
{
|
||||
public static ConfigContainer Config = ConfigManager.Config;
|
||||
|
||||
public static object BuildServerList(string version = "")
|
||||
{
|
||||
return new
|
||||
{
|
||||
code = 0,
|
||||
ret = 0,
|
||||
msg = "ok",
|
||||
message = "ok",
|
||||
version,
|
||||
server_time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
servers = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = 1,
|
||||
server_id = 1,
|
||||
name = Config.GameServer.GameServerName,
|
||||
title = Config.GameServer.GameServerName,
|
||||
host = Config.GameServer.PublicAddress,
|
||||
ip = Config.GameServer.PublicAddress,
|
||||
port = Config.GameServer.Port,
|
||||
status = 1,
|
||||
state = 1,
|
||||
is_open = true,
|
||||
open = true,
|
||||
recommend = true
|
||||
}
|
||||
},
|
||||
game_server = new
|
||||
{
|
||||
host = Config.GameServer.PublicAddress,
|
||||
ip = Config.GameServer.PublicAddress,
|
||||
port = Config.GameServer.Port
|
||||
},
|
||||
http_server = new
|
||||
{
|
||||
host = Config.HttpServer.PublicAddress,
|
||||
port = Config.HttpServer.Port
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ExtractUid(string? authInfo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(authInfo))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var normalized = Uri.UnescapeDataString(authInfo).Trim();
|
||||
var padding = normalized.Length % 4;
|
||||
if (padding > 0)
|
||||
normalized = normalized.PadRight(normalized.Length + (4 - padding), '=');
|
||||
|
||||
var json = Encoding.UTF8.GetString(Convert.FromBase64String(normalized));
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return document.RootElement.TryGetProperty("uid", out var uid) ? uid.GetString() : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/getGameConfig")]
|
||||
[HttpPost("/getGameConfig")]
|
||||
public IActionResult GetGameConfig()
|
||||
{
|
||||
object rsp = new
|
||||
{
|
||||
code = "0",
|
||||
data = new
|
||||
{
|
||||
agreementUpdateTime = "1728552600000",
|
||||
appDownLoadUrl = "",
|
||||
enableReportDataToDouyin = false,
|
||||
loginType = new[] { "channel" },
|
||||
openActivationCode = false,
|
||||
qqGroup = (string?)null
|
||||
},
|
||||
msg = "success"
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpGet("/seasun/config")]
|
||||
[HttpPost("/seasun/config")]
|
||||
public IActionResult GetSeasunConfig()
|
||||
{
|
||||
object rsp = new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
agreementUpdateTime = "1728552600000",
|
||||
appDownLoadUrl = "",
|
||||
enableReportDataToDouyin = false,
|
||||
loginType = new[] { "channel" },
|
||||
openActivationCode = false,
|
||||
qqGroup = (string?)null,
|
||||
privacyUpdateTime = "1728552600000",
|
||||
realNameAuth = false
|
||||
},
|
||||
msg = "success"
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpGet("/seasun/loginByToken")]
|
||||
[HttpPost("/seasun/loginByToken")]
|
||||
public IActionResult LoginByToken(
|
||||
[FromQuery] string? uid,
|
||||
[FromQuery] string? token,
|
||||
[FromForm] string? form_uid,
|
||||
[FromForm] string? form_token
|
||||
)
|
||||
{
|
||||
string finalUid = uid ?? form_uid ?? "10001";
|
||||
string finalToken = token ?? form_token ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
object rsp = new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
associatedAccounts = new[]
|
||||
{
|
||||
new { bindStatus = false, nickname = "", thirdPartyType = "mail" },
|
||||
new { bindStatus = true, nickname = Config.GameServer.GameServerName, thirdPartyType = "google" },
|
||||
new { bindStatus = false, nickname = "", thirdPartyType = "twitter" },
|
||||
new { bindStatus = false, nickname = "", thirdPartyType = "guest" },
|
||||
new { bindStatus = false, nickname = "", thirdPartyType = "steam" }
|
||||
},
|
||||
isFirstLogin = false,
|
||||
isNeedKoreaSciAuth = false,
|
||||
ksOpenId = $"ks_{finalUid}",
|
||||
nickname = Config.GameServer.GameServerName,
|
||||
passportId = finalUid.Length > 10 ? finalUid[^10..] : finalUid,
|
||||
playerFillAgeUrl = "",
|
||||
status = 0,
|
||||
thirdPartyUid = "",
|
||||
finalToken,
|
||||
type = "google",
|
||||
uid = finalUid
|
||||
},
|
||||
msg = "操作成功"
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpGet("/seasun/getAccountInfoForGame")]
|
||||
[HttpPost("/seasun/getAccountInfoForGame")]
|
||||
public IActionResult GetAccountInfoForGame(
|
||||
[FromQuery] string? uid,
|
||||
[FromForm] string? form_uid
|
||||
)
|
||||
{
|
||||
string uidString = uid ?? form_uid ?? "10001";
|
||||
var finalUid = int.TryParse(uidString, out int parsedUid) ? parsedUid : 10001;
|
||||
|
||||
object rsp = new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
bindAccountTypes = new[] { "google" },
|
||||
channelUid = uidString,
|
||||
loginAccountType = "google",
|
||||
nickName = Config.GameServer.GameServerName,
|
||||
passportId = uidString.Length > 10 ? uidString[^10..] : uidString,
|
||||
uid = $"seasun__{uid}"
|
||||
},
|
||||
msg = "操作成功"
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpPost("/bisdk/batchpush")]
|
||||
public IActionResult GetBatchPush()
|
||||
{
|
||||
object rsp = new
|
||||
{
|
||||
code = 0,
|
||||
ret = 0,
|
||||
msg = "ok",
|
||||
message = "ok"
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpGet("/query")]
|
||||
public IActionResult GetQuery([FromQuery] string? version, [FromQuery] string? platform)
|
||||
{
|
||||
object rsp = new
|
||||
{
|
||||
platform,
|
||||
version,
|
||||
host = Config.GameServer.PublicAddress,
|
||||
port = Config.GameServer.Port
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpGet("/query_version={version}")]
|
||||
public IActionResult GetQueryVersionV1(string version)
|
||||
{
|
||||
return Ok(BuildServerList(version));
|
||||
}
|
||||
|
||||
[HttpGet("/query_version")]
|
||||
public IActionResult GetQueryVersionV2([FromQuery] string version)
|
||||
{
|
||||
return Ok(BuildServerList(version));
|
||||
}
|
||||
|
||||
[HttpGet("/api/serverlist")]
|
||||
public IActionResult GetServerList()
|
||||
{
|
||||
return Ok(BuildServerList());
|
||||
}
|
||||
|
||||
[HttpGet("/account/query-uid/{appId}")]
|
||||
public IActionResult QueryUid(string appId, [FromQuery] string authInfo)
|
||||
{
|
||||
var uid = ExtractUid(authInfo) ?? "10001";
|
||||
|
||||
object rsp = new
|
||||
{
|
||||
code = "0",
|
||||
msg = "success",
|
||||
data = new
|
||||
{
|
||||
uid = $"seasun__{uid}"
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpGet("/health")]
|
||||
public IActionResult HealthCheck()
|
||||
{
|
||||
object rsp = new
|
||||
{
|
||||
status = "ok",
|
||||
service = Config.GameServer.GameServerName
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
|
||||
[HttpPost("/api/auth/guest")]
|
||||
public IActionResult AuthGuest([FromQuery] string? Token)
|
||||
{
|
||||
object rsp = new
|
||||
{
|
||||
Provider = "Guest",
|
||||
Token = Token,
|
||||
Account = "Account",
|
||||
Pid = "123813131321312"
|
||||
};
|
||||
|
||||
return Ok(rsp);
|
||||
}
|
||||
}
|
||||
9
SdkServer/Models/ResponseBase.cs
Normal file
9
SdkServer/Models/ResponseBase.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MikuSB.SdkServer.Models;
|
||||
|
||||
public class ResponseBase
|
||||
{
|
||||
public string Msg { get; set; } = "OK";
|
||||
public bool Success { get; set; } = true;
|
||||
public int Code { get; set; }
|
||||
public object? Data { get; set; }
|
||||
}
|
||||
95
SdkServer/SdkServer.cs
Normal file
95
SdkServer/SdkServer.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MikuSB.SdkServer.Handlers;
|
||||
using MikuSB.SdkServer.Utils;
|
||||
using MikuSB.Util;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MikuSB.SdkServer;
|
||||
|
||||
public static class SdkServer
|
||||
{
|
||||
public static void Start(string[] args)
|
||||
{
|
||||
BuildWebHost(args).RunAsync();
|
||||
}
|
||||
|
||||
private static IWebHost BuildWebHost(string[] args)
|
||||
{
|
||||
var builder = WebHost.CreateDefaultBuilder(args)
|
||||
.UseStartup<Startup>()
|
||||
.ConfigureLogging((_, logging) => { logging.ClearProviders(); })
|
||||
.UseUrls(ConfigManager.Config.HttpServer.GetDisplayAddress());
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
|
||||
public class Startup
|
||||
{
|
||||
private static bool LooksLikeServerListRequest(string path, string? query)
|
||||
{
|
||||
var value = $"{path}?{query}".ToLowerInvariant();
|
||||
return value.Contains("server")
|
||||
|| value.Contains("version")
|
||||
|| value.Contains("query_version")
|
||||
|| value.Contains("serverlist");
|
||||
}
|
||||
|
||||
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors("AllowAll");
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapFallback(async context =>
|
||||
{
|
||||
var path = context.Request.Path.Value ?? "";
|
||||
if (LooksLikeServerListRequest(path, context.Request.QueryString.Value))
|
||||
{
|
||||
var response = RouteController.BuildServerList("");
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
|
||||
return;
|
||||
}
|
||||
var fallbackResponse = new
|
||||
{
|
||||
code = 0,
|
||||
message = "ok",
|
||||
service = ConfigManager.Config.GameServer.GameServerName,
|
||||
path = path,
|
||||
query = context.Request.QueryString.Value ?? ""
|
||||
};
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(fallbackResponse));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll",
|
||||
builder => { builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); });
|
||||
});
|
||||
services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
});
|
||||
services.AddSingleton<Logger>(_ => new Logger("HttpServer"));
|
||||
}
|
||||
}
|
||||
19
SdkServer/SdkServer.csproj
Normal file
19
SdkServer/SdkServer.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>MikuSB.SdkServer</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="3.0.0-preview3-19153-02" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
24
SdkServer/Utils/JsonStringToObjectConverter.cs
Normal file
24
SdkServer/Utils/JsonStringToObjectConverter.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MikuSB.SdkServer.Utils;
|
||||
|
||||
public class JsonStringToObjectConverter<T> : JsonConverter<T> where T : class
|
||||
{
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
return JsonSerializer.Deserialize<T>(ref reader, options);
|
||||
|
||||
var jsonString = reader.GetString();
|
||||
return !string.IsNullOrEmpty(jsonString)
|
||||
? JsonSerializer.Deserialize<T>(jsonString, options)
|
||||
: null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value, options);
|
||||
writer.WriteStringValue(json);
|
||||
}
|
||||
}
|
||||
34
SdkServer/Utils/LoggingMiddleware.cs
Normal file
34
SdkServer/Utils/LoggingMiddleware.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using MikuSB.Util;
|
||||
|
||||
namespace MikuSB.SdkServer.Utils;
|
||||
|
||||
public class RequestLoggingMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, Logger logger)
|
||||
{
|
||||
var request = context.Request;
|
||||
var method = request.Method;
|
||||
var path = request.Path + request.QueryString;
|
||||
|
||||
await next(context);
|
||||
|
||||
var statusCode = context.Response.StatusCode;
|
||||
|
||||
if (path.StartsWith("/report") || path.Contains("/log/") || path == "/alive")
|
||||
return;
|
||||
|
||||
if (statusCode == 200)
|
||||
{
|
||||
logger.Info($"{method} {path} => {statusCode}");
|
||||
}
|
||||
else if (statusCode == 404)
|
||||
{
|
||||
logger.Warn($"{method} {path} => {statusCode}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"{method} {path} => {statusCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
51
TcpSharp/BasePacket.cs
Normal file
51
TcpSharp/BasePacket.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Google.Protobuf;
|
||||
using MikuSB.Enums.Packet;
|
||||
|
||||
namespace MikuSB.TcpSharp;
|
||||
|
||||
public class BasePacket
|
||||
{
|
||||
public ushort CmdId { get; set; }
|
||||
public byte[] Body { get; set; }
|
||||
public ushort SeqNo { get; set; }
|
||||
public ushort PushSeq { get; set; }
|
||||
public long Timestamp { get; set; }
|
||||
public IMessage? Message { get; set; }
|
||||
public PacketFraming Framing { get; set; }
|
||||
|
||||
public BasePacket(ushort cmdId)
|
||||
{
|
||||
CmdId = cmdId;
|
||||
Body = Array.Empty<byte>();
|
||||
SeqNo = 0;
|
||||
PushSeq = 0;
|
||||
Timestamp = 0;
|
||||
Framing = PacketFraming.FourByteLittleEndianLength;
|
||||
}
|
||||
|
||||
public BasePacket(ushort cmdId, byte[] body, PacketFraming framing = PacketFraming.FourByteLittleEndianLength)
|
||||
{
|
||||
CmdId = cmdId;
|
||||
Body = body ?? Array.Empty<byte>();
|
||||
Framing = framing;
|
||||
SeqNo = 0;
|
||||
PushSeq = 0;
|
||||
Timestamp = 0;
|
||||
}
|
||||
|
||||
public void SetData(byte[] data)
|
||||
{
|
||||
Body = data;
|
||||
}
|
||||
|
||||
public void SetData(IMessage message)
|
||||
{
|
||||
Body = message.ToByteArray();
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public void SetData(string base64)
|
||||
{
|
||||
SetData(Convert.FromBase64String(base64));
|
||||
}
|
||||
}
|
||||
308
TcpSharp/PacketCodec.cs
Normal file
308
TcpSharp/PacketCodec.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using Google.Protobuf;
|
||||
using MikuSB.Enums.Packet;
|
||||
using MikuSB.Util;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MikuSB.TcpSharp
|
||||
{
|
||||
public class PacketCodec
|
||||
{
|
||||
private const int HeaderSize4Byte = 4;
|
||||
private const int HeaderSize2Byte = 2;
|
||||
private const int MaxPacketLength = 1024 * 1024;
|
||||
private const ushort ClientMagic = 0x011F;
|
||||
private const int ControlPacketSize = 35;
|
||||
|
||||
private static readonly Logger Logger = new("PacketCodec");
|
||||
|
||||
public PacketCodec()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<BasePacket?> ReadPacketAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lengthBuffer = new byte[HeaderSize4Byte];
|
||||
if (!await ReadExactAsync(stream, lengthBuffer, cancellationToken))
|
||||
{
|
||||
Logger.Debug("Connection closed before packet header");
|
||||
return null;
|
||||
}
|
||||
|
||||
var framing = DetectFraming(lengthBuffer);
|
||||
|
||||
switch (framing)
|
||||
{
|
||||
case PacketFraming.Control:
|
||||
return await HandleControlPacket(stream, cancellationToken);
|
||||
|
||||
case PacketFraming.TwoByteBigEndianLength:
|
||||
return await HandleTwoBytePacket(stream, lengthBuffer, cancellationToken);
|
||||
|
||||
case PacketFraming.FourByteLittleEndianLength:
|
||||
return await HandleFourBytePacket(stream, lengthBuffer, cancellationToken);
|
||||
|
||||
default:
|
||||
return await HandleUnknownPacket(stream, lengthBuffer, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.Debug("Packet read cancelled");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Error reading packet {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Encode(ushort packetId, byte[] payload, PacketFraming framing = PacketFraming.FourByteLittleEndianLength)
|
||||
{
|
||||
return framing switch
|
||||
{
|
||||
PacketFraming.TwoByteBigEndianLength => EncodeTwoByteFrame(packetId, payload),
|
||||
PacketFraming.FourByteLittleEndianLength => EncodeFourByteFrame(packetId, payload),
|
||||
_ => EncodeFourByteFrame(packetId, payload)
|
||||
};
|
||||
}
|
||||
|
||||
public byte[] EncodeRaw(ushort packetId, byte[] payload, PacketFraming framing = PacketFraming.FourByteLittleEndianLength)
|
||||
{
|
||||
return framing switch
|
||||
{
|
||||
PacketFraming.TwoByteBigEndianLength => EncodeTwoByteFrame(packetId, payload),
|
||||
PacketFraming.FourByteLittleEndianLength => EncodeFourByteFrame(packetId, payload),
|
||||
_ => EncodeFourByteFrame(packetId, payload)
|
||||
};
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private PacketFraming DetectFraming(byte[] header)
|
||||
{
|
||||
var firstTwoBytes = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(0, 2));
|
||||
var nextTwoBytes = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(2, 2));
|
||||
|
||||
if (firstTwoBytes == ClientMagic && nextTwoBytes == 0)
|
||||
return PacketFraming.Control;
|
||||
|
||||
if (firstTwoBytes == ClientMagic && IsValidPacketId(nextTwoBytes))
|
||||
return PacketFraming.TwoByteBigEndianLength;
|
||||
|
||||
if (IsValidTwoByteHeader(firstTwoBytes, (ushort)nextTwoBytes))
|
||||
return PacketFraming.TwoByteBigEndianLength;
|
||||
|
||||
return PacketFraming.FourByteLittleEndianLength;
|
||||
}
|
||||
|
||||
private async Task<BasePacket?> HandleControlPacket(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var controlData = new byte[ControlPacketSize];
|
||||
if (!await ReadExactAsync(stream, controlData, cancellationToken))
|
||||
{
|
||||
Logger.Debug("Connection closed during control packet read");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.Debug("Control packet received");
|
||||
return new BasePacket(0)
|
||||
{
|
||||
Framing = PacketFraming.Control,
|
||||
Body = Array.Empty<byte>()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<BasePacket?> HandleTwoBytePacket(
|
||||
Stream stream,
|
||||
byte[] header,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var packetId = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(2, 2));
|
||||
|
||||
var wrapper = new byte[ControlPacketSize];
|
||||
if (!await ReadExactAsync(stream, wrapper, cancellationToken))
|
||||
{
|
||||
Logger.Debug($"Connection closed during wrapper read for packet {packetId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var payloadLength = BinaryPrimitives.ReadUInt16LittleEndian(wrapper.AsSpan(6, 2));
|
||||
var payload = await ReadPayloadAsync(stream, payloadLength, cancellationToken);
|
||||
|
||||
if (payload == null)
|
||||
return null;
|
||||
|
||||
//Logger.Debug($"Packet received (2-byte framing): ID={packetId}, PayloadSize={payload.Length}");
|
||||
|
||||
return new BasePacket(packetId)
|
||||
{
|
||||
Framing = PacketFraming.TwoByteBigEndianLength,
|
||||
Body = payload
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<BasePacket?> HandleFourBytePacket(
|
||||
Stream stream,
|
||||
byte[] header,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var length = BinaryPrimitives.ReadUInt32LittleEndian(header);
|
||||
|
||||
if (length < 2 || length > MaxPacketLength)
|
||||
{
|
||||
Logger.Warn($"Invalid packet length: {length}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var frame = new byte[length];
|
||||
if (!await ReadExactAsync(stream, frame, cancellationToken))
|
||||
{
|
||||
Logger.Debug("Connection closed during packet body read");
|
||||
return null;
|
||||
}
|
||||
|
||||
var packetId = BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(0, 2));
|
||||
var payload = frame[2..];
|
||||
|
||||
//Logger.Debug($"Packet received (4-byte framing): ID={packetId}, PayloadSize={payload.Length}");
|
||||
|
||||
return new BasePacket(packetId)
|
||||
{
|
||||
Framing = PacketFraming.FourByteLittleEndianLength,
|
||||
Body = payload
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<BasePacket?> HandleUnknownPacket(
|
||||
Stream stream,
|
||||
byte[] header,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var extraData = await ReadAvailableBytesAsync(stream, cancellationToken);
|
||||
var combinedData = new byte[header.Length + extraData.Length];
|
||||
header.CopyTo(combinedData, 0);
|
||||
extraData.CopyTo(combinedData, header.Length);
|
||||
|
||||
Logger.Warn($"Unknown packet format detected, captured {combinedData.Length} bytes");
|
||||
|
||||
return new BasePacket(0)
|
||||
{
|
||||
Framing = PacketFraming.Unknown,
|
||||
Body = combinedData
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<byte[]?> ReadPayloadAsync(
|
||||
Stream stream,
|
||||
int length,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (length <= 0)
|
||||
return Array.Empty<byte>();
|
||||
|
||||
if (length > MaxPacketLength)
|
||||
{
|
||||
Logger.Warn($"Payload too large: {length}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = new byte[length];
|
||||
if (!await ReadExactAsync(stream, payload, cancellationToken))
|
||||
return null;
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
private byte[] EncodeTwoByteFrame(ushort packetId, byte[] payload)
|
||||
{
|
||||
var wrappedPayload = WrapPayload(payload);
|
||||
var buffer = new byte[HeaderSize4Byte + wrappedPayload.Length];
|
||||
|
||||
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), ClientMagic);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), packetId);
|
||||
wrappedPayload.CopyTo(buffer.AsSpan(HeaderSize4Byte));
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[] EncodeFourByteFrame(ushort packetId, byte[] payload)
|
||||
{
|
||||
var buffer = new byte[HeaderSize4Byte + HeaderSize2Byte + payload.Length];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), (uint)(HeaderSize2Byte + payload.Length));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(4, 2), packetId);
|
||||
payload.CopyTo(buffer.AsSpan(HeaderSize4Byte + HeaderSize2Byte));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private byte[] WrapPayload(byte[] payload)
|
||||
{
|
||||
const int wrapperHeaderSize = 35;
|
||||
var wrapped = new byte[wrapperHeaderSize + payload.Length];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(wrapped.AsSpan(6, 2), (ushort)payload.Length);
|
||||
wrapped[11] = 1;
|
||||
payload.CopyTo(wrapped.AsSpan(wrapperHeaderSize));
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private static async Task<bool> ReadExactAsync(
|
||||
Stream stream,
|
||||
byte[] buffer,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < buffer.Length)
|
||||
{
|
||||
var read = await stream.ReadAsync(buffer.AsMemory(offset), cancellationToken);
|
||||
if (read == 0)
|
||||
return false;
|
||||
|
||||
offset += read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task<byte[]> ReadAvailableBytesAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (stream is not NetworkStream networkStream || !networkStream.DataAvailable)
|
||||
return Array.Empty<byte>();
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
var buffer = new byte[4096];
|
||||
|
||||
while (networkStream.DataAvailable && ms.Length < 16384)
|
||||
{
|
||||
var read = await networkStream.ReadAsync(buffer, cancellationToken);
|
||||
if (read <= 0)
|
||||
break;
|
||||
|
||||
ms.Write(buffer, 0, read);
|
||||
}
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static bool IsValidTwoByteHeader(int firstTwoBytes, ushort packetId)
|
||||
{
|
||||
return firstTwoBytes >= 2
|
||||
&& firstTwoBytes <= ushort.MaxValue
|
||||
&& IsValidPacketId(packetId);
|
||||
}
|
||||
|
||||
private static bool IsValidPacketId(ushort packetId)
|
||||
{
|
||||
return packetId != 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
10
TcpSharp/SessionStateEnum.cs
Normal file
10
TcpSharp/SessionStateEnum.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace MikuSB.TcpSharp;
|
||||
|
||||
public enum SessionStateEnum
|
||||
{
|
||||
INACTIVE,
|
||||
WAITING_FOR_TOKEN,
|
||||
WAITING_FOR_LOGIN,
|
||||
PICKING_CHARACTER,
|
||||
ACTIVE
|
||||
}
|
||||
203
TcpSharp/SocketConnection.cs
Normal file
203
TcpSharp/SocketConnection.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using MikuSB.Enums.Packet;
|
||||
using MikuSB.Proto;
|
||||
using MikuSB.Util;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MikuSB.TcpSharp;
|
||||
|
||||
public class SocketConnection
|
||||
{
|
||||
public static readonly ConcurrentBag<int> BannedPackets = [];
|
||||
private static readonly Logger Logger = new("GameServer");
|
||||
public static readonly ConcurrentDictionary<int, string> LogMap = [];
|
||||
|
||||
public static readonly ConcurrentBag<int> IgnoreLog =
|
||||
[
|
||||
|
||||
];
|
||||
protected readonly CancellationTokenSource CancelToken;
|
||||
protected readonly Socket Socket;
|
||||
public readonly IPEndPoint RemoteEndPoint;
|
||||
|
||||
public string DebugFile = "";
|
||||
public bool IsOnline = true;
|
||||
public StreamWriter? Writer;
|
||||
|
||||
public int DownStreamSeqNo;
|
||||
public int UpStreamSeqNo;
|
||||
|
||||
public PacketFraming Framing;
|
||||
|
||||
public SocketConnection(Socket socket, IPEndPoint remote)
|
||||
{
|
||||
Socket = socket;
|
||||
RemoteEndPoint = remote;
|
||||
CancelToken = new CancellationTokenSource();
|
||||
|
||||
Start();
|
||||
}
|
||||
public SessionStateEnum State { get; set; } = SessionStateEnum.INACTIVE;
|
||||
internal long ConnectionId { get; set; }
|
||||
|
||||
public virtual void Start()
|
||||
{
|
||||
Logger.Info($"New connection from {RemoteEndPoint}.");
|
||||
State = SessionStateEnum.WAITING_FOR_TOKEN;
|
||||
}
|
||||
|
||||
public virtual void Stop(bool isServerStop = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Socket?.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
Socket?.Close();
|
||||
Socket?.Dispose();
|
||||
}
|
||||
try
|
||||
{
|
||||
CancelToken.Cancel();
|
||||
CancelToken.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
IsOnline = false;
|
||||
}
|
||||
|
||||
public bool SocketConnected()
|
||||
{
|
||||
try
|
||||
{
|
||||
return !((Socket.Poll(1000, SelectMode.SelectRead) && (Socket.Available == 0)) || !Socket.Connected);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void LogPacket(string sendOrRecv, ushort opcode, byte[] payload, PacketFraming framing)
|
||||
{
|
||||
if (!ConfigManager.Config.ServerOption.EnableDebug) return;
|
||||
try
|
||||
{
|
||||
//Logger.DebugWriteLine($"{sendOrRecv}: {Enum.GetName(typeof(OpCode), opcode)}({opcode})\r\n{Convert.ToHexString(payload)}");
|
||||
if (IgnoreLog.Contains(opcode)) return;
|
||||
if (!ConfigManager.Config.ServerOption.DebugDetailMessage) throw new Exception(); // go to catch block
|
||||
var typ = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SingleOrDefault(assembly => assembly.GetName().Name == "MikuProto")!.GetTypes()
|
||||
.First(t => t.Name == $"{LogMap[opcode]}"); //get the type using the packet name
|
||||
var descriptor =
|
||||
typ.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static)?.GetValue(
|
||||
null, null) as MessageDescriptor; // get the static property Descriptor
|
||||
var packet = descriptor?.Parser.ParseFrom(payload);
|
||||
var formatter = JsonFormatter.Default;
|
||||
var asJson = formatter.Format(packet);
|
||||
var output = $"{sendOrRecv}: {LogMap[opcode]}({opcode}) ({framing})\r\n{asJson}";
|
||||
if (ConfigManager.Config.ServerOption.DebugMessage)
|
||||
Logger.Debug(output);
|
||||
if (DebugFile == "" || !ConfigManager.Config.ServerOption.SavePersonalDebugFile) return;
|
||||
var sw = GetWriter();
|
||||
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] [GameServer] [DEBUG] " + output);
|
||||
sw.Flush();
|
||||
}
|
||||
catch
|
||||
{
|
||||
var output = $"{sendOrRecv}: {LogMap.GetValueOrDefault(opcode, "UnknownPacket")}({opcode})";
|
||||
if (ConfigManager.Config.ServerOption.DebugMessage)
|
||||
Logger.Debug(output);
|
||||
if (DebugFile != "" && ConfigManager.Config.ServerOption.SavePersonalDebugFile)
|
||||
{
|
||||
var sw = GetWriter();
|
||||
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] [GameServer] [DEBUG] " + output);
|
||||
sw.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private StreamWriter GetWriter()
|
||||
{
|
||||
// Create the file if it doesn't exist
|
||||
var file = new FileInfo(DebugFile);
|
||||
if (!file.Exists)
|
||||
{
|
||||
Directory.CreateDirectory(file.DirectoryName!);
|
||||
File.Create(DebugFile).Dispose();
|
||||
}
|
||||
|
||||
Writer ??= new StreamWriter(DebugFile, true);
|
||||
return Writer;
|
||||
}
|
||||
|
||||
public async Task SendPacket(byte[] packet)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Socket.Connected)
|
||||
{
|
||||
await Socket.SendAsync(
|
||||
new ArraySegment<byte>(packet),
|
||||
SocketFlags.None,
|
||||
CancelToken.Token
|
||||
);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendPacket(BasePacket packet, ushort seqNo = 0)
|
||||
{
|
||||
// Test
|
||||
if (packet.CmdId <= 0)
|
||||
{
|
||||
Logger.Debug("Tried to send packet with missing cmd id!");
|
||||
return;
|
||||
}
|
||||
|
||||
// DO NOT REMOVE (unless we find a way to validate code before sending to client which I don't think we can)
|
||||
if (BannedPackets.Contains(packet.CmdId)) return;
|
||||
LogPacket("Send", packet.CmdId, packet.Body,Framing);
|
||||
byte[] packetBytes = new PacketCodec().Encode(packet.CmdId, packet.Body,Framing);
|
||||
try
|
||||
{
|
||||
await SendPacket(packetBytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendPacket(int cmdId)
|
||||
{
|
||||
await SendPacket(new BasePacket((ushort)cmdId));
|
||||
}
|
||||
|
||||
public async Task SendPacket(int cmdId, ushort seqNo)
|
||||
{
|
||||
var packet = new BasePacket((ushort)cmdId);
|
||||
packet.SeqNo = seqNo;
|
||||
await SendPacket(packet);
|
||||
}
|
||||
|
||||
public async Task SendPacket(int cmdId, IMessage msg, ushort seqNo = 0)
|
||||
{
|
||||
var packet = new BasePacket((ushort)cmdId);
|
||||
packet.SetData(msg);
|
||||
packet.SeqNo = seqNo;
|
||||
await SendPacket(packet);
|
||||
}
|
||||
}
|
||||
105
TcpSharp/SocketListener.cs
Normal file
105
TcpSharp/SocketListener.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Net;
|
||||
using MikuSB.Util;
|
||||
using MikuSB.Internationalization;
|
||||
|
||||
namespace MikuSB.TcpSharp;
|
||||
|
||||
public class SocketListener
|
||||
{
|
||||
private static IPEndPoint? ListenAddress;
|
||||
private static readonly Logger Logger = new("GameServer");
|
||||
|
||||
private static Socket? serverSocket;
|
||||
|
||||
public static readonly SortedList<long, SocketConnection> Connections = [];
|
||||
|
||||
public static Type BaseConnection { get; set; } = typeof(SocketConnection);
|
||||
|
||||
private static int PORT => ConfigManager.Config.GameServer.Port;
|
||||
|
||||
private static long _nextId = 0;
|
||||
|
||||
public static void StartListener()
|
||||
{
|
||||
if (serverSocket != null)
|
||||
throw new InvalidOperationException("SocketListener already started.");
|
||||
|
||||
ListenAddress = new IPEndPoint(IPAddress.Parse(ConfigManager.Config.GameServer.BindAddress), PORT);
|
||||
|
||||
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
serverSocket.Bind(ListenAddress);
|
||||
serverSocket.Listen(100);
|
||||
|
||||
Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerRunning",
|
||||
I18NManager.Translate("Word.Game"),
|
||||
ConfigManager.Config.GameServer.GetDisplayAddress()));
|
||||
|
||||
_ = Task.Run(AcceptLoop);
|
||||
}
|
||||
|
||||
private static async Task AcceptLoop()
|
||||
{
|
||||
if (serverSocket == null)
|
||||
throw new InvalidOperationException("Server socket not initialized.");
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Socket clientSocket = await serverSocket.AcceptAsync();
|
||||
var remote = clientSocket.RemoteEndPoint as IPEndPoint;
|
||||
|
||||
if (remote == null)
|
||||
{
|
||||
clientSocket.Close();
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var connection = (SocketConnection?)Activator.CreateInstance(BaseConnection, clientSocket, remote);
|
||||
|
||||
if (connection == null)
|
||||
{
|
||||
Logger.Error($"Failed to create connection instance from {BaseConnection.Name}");
|
||||
clientSocket.Close();
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextId);
|
||||
connection.ConnectionId = id;
|
||||
|
||||
Connections[id] = connection;
|
||||
Logger.Info($"Accepted connection #{id} from {remote}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Error creating connection: {ex}");
|
||||
clientSocket.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Info("Server stopped listening.");
|
||||
}
|
||||
}
|
||||
|
||||
public static SocketConnection? GetConnectionByEndPoint(IPEndPoint ep)
|
||||
{
|
||||
Connections.TryGetValue(ep.GetHashCode(), out var conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
public static void UnregisterConnection(SocketConnection socket)
|
||||
{
|
||||
if (socket == null) return;
|
||||
|
||||
if (Connections.Remove(socket.ConnectionId))
|
||||
{
|
||||
Logger.Info($"Connection #{socket.ConnectionId} with {socket.RemoteEndPoint} has been closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
21
TcpSharp/TcpSharp.csproj
Normal file
21
TcpSharp/TcpSharp.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CETCompat>false</CETCompat>
|
||||
<AssemblyName>TcpSharp</AssemblyName>
|
||||
<RootNamespace>MikuSB.TcpSharp</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user