enter intro cutscene

This commit is contained in:
Naruse
2026-04-20 12:40:38 +08:00
parent 2826239284
commit 279da58dc1
81 changed files with 7279 additions and 0 deletions

32
Common/Common.csproj Normal file
View 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>

View 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;
}

View 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; } = "";
}

View 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();
}
}

View 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
View File

@@ -0,0 +1,5 @@
namespace MikuSB.Data;
public static class GameData
{
}

View 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
}

View 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;
}
}

View 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
}

View File

@@ -0,0 +1,8 @@
using SqlSugar;
namespace MikuSB.Database;
public abstract class BaseDatabaseDataHelper
{
[SugarColumn(IsPrimaryKey = true)] public int Uid { get; set; }
}

View 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);
}
}

View 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);
}
}
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
namespace MikuSB.Enums.Language;
public enum ProgramLanguageTypeEnum
{
EN = 0,
CHS = 1,
CHT = 2,
JP = 3
}

View File

@@ -0,0 +1,9 @@
namespace MikuSB.Enums.Packet;
public enum PacketFraming
{
FourByteLittleEndianLength,
TwoByteBigEndianLength,
Control,
Unknown
}

View 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
}

View File

@@ -0,0 +1,7 @@
namespace MikuSB.Enums.Player;
public enum ServerEnum
{
Console = 0,
Chat = 1
}

View File

@@ -0,0 +1,9 @@
namespace MikuSB.Enums.Player;
public enum PermEnum
{
Trial = 0,
Support = 1,
Admin = 2,
Other = 10
}

View 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;
}
}

View 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

View 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

View 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

View 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;
}

View 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);
}
}
}

View 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
View 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;
}
}

View 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
}

View 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
View 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
View 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
View 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
}

View 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
View 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
// };
//}
}

View 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);
}
}
}