diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3d4d3e3
--- /dev/null
+++ b/.editorconfig
@@ -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
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4207fbf
--- /dev/null
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/Common/Common.csproj b/Common/Common.csproj
new file mode 100644
index 0000000..024f2ef
--- /dev/null
+++ b/Common/Common.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+ MikuSB
+ MikuCommon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Common/Configuration/ConfigContainer.cs b/Common/Configuration/ConfigContainer.cs
new file mode 100644
index 0000000..5472e47
--- /dev/null
+++ b/Common/Configuration/ConfigContainer.cs
@@ -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;
+}
\ No newline at end of file
diff --git a/Common/Configuration/HotfixContainer.cs b/Common/Configuration/HotfixContainer.cs
new file mode 100644
index 0000000..742f10c
--- /dev/null
+++ b/Common/Configuration/HotfixContainer.cs
@@ -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 Hotfixes { get; set; } = new();
+ public Dictionary 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 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 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; } = "";
+}
\ No newline at end of file
diff --git a/Common/Data/Config/DictionaryConverter.cs b/Common/Data/Config/DictionaryConverter.cs
new file mode 100644
index 0000000..cccb4e4
--- /dev/null
+++ b/Common/Data/Config/DictionaryConverter.cs
@@ -0,0 +1,43 @@
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json;
+
+namespace MikuSB.Data.Config;
+
+class IntDictionaryConverter : JsonConverter>
+{
+ public override Dictionary? ReadJson(JsonReader reader, Type objectType, Dictionary? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.StartArray)
+ {
+ JArray.Load(reader);
+ return new Dictionary();
+ }
+ else if (reader.TokenType == JsonToken.StartObject)
+ {
+ var obj = JObject.Load(reader);
+ var dict = new Dictionary();
+
+ foreach (var prop in obj.Properties())
+ {
+ if (int.TryParse(prop.Name, out var key))
+ {
+ dict[key] = prop.Value.ToObject();
+ }
+ }
+ return dict;
+ }
+
+ return new Dictionary();
+ }
+
+ public override void WriteJson(JsonWriter writer, Dictionary? value, JsonSerializer serializer)
+ {
+ writer.WriteStartObject();
+ foreach (var kv in value)
+ {
+ writer.WritePropertyName(kv.Key.ToString());
+ writer.WriteValue(kv.Value);
+ }
+ writer.WriteEndObject();
+ }
+}
diff --git a/Common/Data/ExcelResource.cs b/Common/Data/ExcelResource.cs
new file mode 100644
index 0000000..506a51e
--- /dev/null
+++ b/Common/Data/ExcelResource.cs
@@ -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()
+ {
+ }
+}
\ No newline at end of file
diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs
new file mode 100644
index 0000000..a0fc47d
--- /dev/null
+++ b/Common/Data/GameData.cs
@@ -0,0 +1,5 @@
+namespace MikuSB.Data;
+
+public static class GameData
+{
+}
\ No newline at end of file
diff --git a/Common/Data/ResourceEntity.cs b/Common/Data/ResourceEntity.cs
new file mode 100644
index 0000000..6ad6c87
--- /dev/null
+++ b/Common/Data/ResourceEntity.cs
@@ -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(fileName.Split(','));
+ else
+ FileName = [fileName];
+ IsCritical = isCritical;
+ }
+
+
+ public ResourceEntity(string fileName, bool isMultifile = false)
+ {
+ if (isMultifile)
+ FileName = new List(fileName.Split(','));
+ else
+ FileName = [fileName];
+ }
+
+ public ResourceEntity(string fileName)
+ {
+ FileName = [fileName];
+ }
+
+ public List FileName { get; private set; }
+
+ [Obsolete("No effect")] public bool IsCritical { get; private set; } // deprecated
+}
\ No newline at end of file
diff --git a/Common/Data/ResourceManager.cs b/Common/Data/ResourceManager.cs
new file mode 100644
index 0000000..c2c8f30
--- /dev/null
+++ b/Common/Data/ResourceManager.cs
@@ -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 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? LoadSingleExcel(Type cls) where T : ExcelResource, new()
+ {
+ return LoadSingleExcelResource(cls) as List;
+ }
+
+ public static List? 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 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(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(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(text);
+ customFile = json;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error("Error in reading " + file.Name, ex);
+ }
+
+ switch (customFile)
+ {
+ case Dictionary d:
+ Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItems", d.Count.ToString(), type));
+ break;
+ case Dictionary> 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;
+ }
+}
\ No newline at end of file
diff --git a/Common/Database/Account/AccountData.cs b/Common/Database/Account/AccountData.cs
new file mode 100644
index 0000000..48d43e7
--- /dev/null
+++ b/Common/Database/Account/AccountData.cs
@@ -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 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()?.ForEach(account =>
+ {
+ if (account.Username == username) result = account;
+ });
+ return result;
+ }
+
+ public static AccountData? GetAccountByUid(int uid, bool force = false)
+ {
+ var result = DatabaseHelper.GetInstance(uid, force);
+ return result;
+ }
+
+ public static AccountData? GetAccountByDispatchToken(string dispatchToken)
+ {
+ AccountData? result = null;
+ DatabaseHelper.GetAllInstance()?.ForEach(account =>
+ {
+ if (account.DispatchToken == dispatchToken) result = account;
+ });
+ return result;
+ }
+
+ public static AccountData? GetAccountByComboToken(string comboToken)
+ {
+ AccountData? result = null;
+ DatabaseHelper.GetAllInstance()?.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
+}
\ No newline at end of file
diff --git a/Common/Database/BaseDatabaseDataHelper.cs b/Common/Database/BaseDatabaseDataHelper.cs
new file mode 100644
index 0000000..36d53fa
--- /dev/null
+++ b/Common/Database/BaseDatabaseDataHelper.cs
@@ -0,0 +1,8 @@
+using SqlSugar;
+
+namespace MikuSB.Database;
+
+public abstract class BaseDatabaseDataHelper
+{
+ [SugarColumn(IsPrimaryKey = true)] public int Uid { get; set; }
+}
\ No newline at end of file
diff --git a/Common/Database/CustomSerializeService.cs b/Common/Database/CustomSerializeService.cs
new file mode 100644
index 0000000..75d52f2
--- /dev/null
+++ b/Common/Database/CustomSerializeService.cs
@@ -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(string value)
+ {
+ return JsonConvert.DeserializeObject(value)!;
+ }
+
+ public string SugarSerializeObject(object value)
+ {
+ return JsonConvert.SerializeObject(value, _jsonSettings);
+ }
+}
\ No newline at end of file
diff --git a/Common/Database/DatabaseHelper.cs b/Common/Database/DatabaseHelper.cs
new file mode 100644
index 0000000..1a9dc5f
--- /dev/null
+++ b/Common/Database/DatabaseHelper.cs
@@ -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> UidInstanceMap = [];
+ public static readonly List 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().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(int uid) where T : BaseDatabaseDataHelper, new()
+ {
+ var list = sqlSugarScope?.Queryable()
+ .Select(x => x)
+ .Select()
+ .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() where T : BaseDatabaseDataHelper, new()
+ {
+ try
+ {
+ sqlSugarScope?.CodeFirst.InitTables();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ public static T? GetInstance(int uid, bool forceReload = false) where T : BaseDatabaseDataHelper, new()
+ {
+ try
+ {
+ if (!forceReload && UidInstanceMap.TryGetValue(uid, out var value))
+ {
+ var instance = value.OfType().FirstOrDefault();
+ if (instance != null) return instance;
+ }
+ var t = sqlSugarScope?.Queryable()
+ .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();
+ 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(int uid) where T : BaseDatabaseDataHelper, new()
+ {
+ var instance = GetInstance(uid);
+ if (instance != null) return instance;
+
+ instance = new T
+ {
+ Uid = uid
+ };
+ CreateInstance(instance);
+
+ return instance;
+ }
+
+ public static List? GetAllInstance() where T : BaseDatabaseDataHelper, new()
+ {
+ try
+ {
+ return sqlSugarScope?.Queryable()
+ .Select(x => x)
+ .ToList();
+ }
+ catch (Exception e)
+ {
+ logger.Error("Unsupported type", e);
+ return null;
+ }
+ }
+
+ public static void UpdateInstance(T instance) where T : BaseDatabaseDataHelper, new()
+ {
+ sqlSugarScope?.Updateable(instance).ExecuteCommand();
+ }
+
+ public static void CreateInstance(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(int key) where T : BaseDatabaseDataHelper, new()
+ {
+ try
+ {
+ sqlSugarScope?.Deleteable().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 instance) where T : BaseDatabaseDataHelper, new()
+ {
+ try
+ {
+ sqlSugarScope?.Updateable(instance).ExecuteCommand();
+ }
+ catch (Exception e)
+ {
+ logger.Error("An error occurred while saving the database", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Common/Database/Player/PlayerGameData.cs b/Common/Database/Player/PlayerGameData.cs
new file mode 100644
index 0000000..c3c39e9
--- /dev/null
+++ b/Common/Database/Player/PlayerGameData.cs
@@ -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((int)uid);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/Common/Enums/Language/ProgramLanguageTypeEnum.cs b/Common/Enums/Language/ProgramLanguageTypeEnum.cs
new file mode 100644
index 0000000..623675c
--- /dev/null
+++ b/Common/Enums/Language/ProgramLanguageTypeEnum.cs
@@ -0,0 +1,9 @@
+namespace MikuSB.Enums.Language;
+
+public enum ProgramLanguageTypeEnum
+{
+ EN = 0,
+ CHS = 1,
+ CHT = 2,
+ JP = 3
+}
\ No newline at end of file
diff --git a/Common/Enums/Packet/PacketFraming.cs b/Common/Enums/Packet/PacketFraming.cs
new file mode 100644
index 0000000..2215458
--- /dev/null
+++ b/Common/Enums/Packet/PacketFraming.cs
@@ -0,0 +1,9 @@
+namespace MikuSB.Enums.Packet;
+
+public enum PacketFraming
+{
+ FourByteLittleEndianLength,
+ TwoByteBigEndianLength,
+ Control,
+ Unknown
+}
\ No newline at end of file
diff --git a/Common/Enums/Player/BanTypeEnum.cs b/Common/Enums/Player/BanTypeEnum.cs
new file mode 100644
index 0000000..e0ddc54
--- /dev/null
+++ b/Common/Enums/Player/BanTypeEnum.cs
@@ -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
+}
\ No newline at end of file
diff --git a/Common/Enums/Player/FriendEnum.cs b/Common/Enums/Player/FriendEnum.cs
new file mode 100644
index 0000000..c4e5c79
--- /dev/null
+++ b/Common/Enums/Player/FriendEnum.cs
@@ -0,0 +1,7 @@
+namespace MikuSB.Enums.Player;
+
+public enum ServerEnum
+{
+ Console = 0,
+ Chat = 1
+}
\ No newline at end of file
diff --git a/Common/Enums/Player/PermEnum.cs b/Common/Enums/Player/PermEnum.cs
new file mode 100644
index 0000000..fe48bbc
--- /dev/null
+++ b/Common/Enums/Player/PermEnum.cs
@@ -0,0 +1,9 @@
+namespace MikuSB.Enums.Player;
+
+public enum PermEnum
+{
+ Trial = 0,
+ Support = 1,
+ Admin = 2,
+ Other = 10
+}
\ No newline at end of file
diff --git a/Common/Internationalization/I18nManager.cs b/Common/Internationalization/I18nManager.cs
new file mode 100644
index 0000000..e6ab84c
--- /dev/null
+++ b/Common/Internationalization/I18nManager.cs
@@ -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> 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> pluginAssemblies)
+ {
+ foreach (var (pluginName, types) in pluginAssemblies)
+ {
+ var languageType = types.FindAll(x => x.GetCustomAttribute() != null);
+ if (languageType.Count == 0) // no language to use
+ continue;
+
+ PluginLanguages.Add(pluginName, []);
+ foreach (var type in languageType)
+ {
+ var attr = type.GetCustomAttribute();
+ 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(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().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