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

View File

@@ -0,0 +1,79 @@
using MikuSB.Database.Account;
using MikuSB.GameServer.Server;
using MikuSB.Internationalization;
namespace MikuSB.GameServer.Command;
public class CommandArg
{
public string RawArg { get; } = "";
public List<string> Args { get; } = [];
public List<string> Attributes { get; } = [];
public ICommandSender Sender { get; }
public int TargetUid { get; set; } = 0;
public Connection? Target { get; set; }
public CommandArg(string rawArg, ICommandSender sender)
{
Sender = sender;
RawArg = rawArg;
foreach (var arg in rawArg.Split(' '))
{
if (string.IsNullOrEmpty(arg)) continue;
Args.Add(arg);
}
}
public async ValueTask SendMsg(string msg)
{
await Sender.SendMsg(msg);
}
public int GetInt(int index)
{
if (Args.Count <= index) return 0;
if (int.TryParse(Args[index], out var res))
return res;
return 0;
}
public async ValueTask<int?> GetOption(char pre, string def = "1")
{
var opStr = Args.FirstOrDefault(x => x[0] == pre)?[1..] ?? def;
if (!int.TryParse(opStr, out var op))
{
await SendMsg(I18NManager.Translate("Game.Command.Notice.InvalidArguments"));
return null;
}
return op;
}
public async ValueTask<bool> CheckArgCnt(int start, int? end = null)
{
end ??= start;
if (Args.Count >= start && Args.Count <= end) return true;
await SendMsg(I18NManager.Translate("Game.Command.Notice.InvalidArguments"));
return false;
}
public async ValueTask<bool> CheckTarget()
{
if (AccountData.GetAccountByUid(TargetUid) == null)
{
await SendMsg(I18NManager.Translate("Game.Command.Notice.PlayerNotFound"));
return false;
}
return true;
}
public async ValueTask<bool> CheckOnlineTarget(bool sendMsg = true)
{
if (Target == null)
{
if (sendMsg)
await SendMsg(I18NManager.Translate("Game.Command.Notice.PlayerNotFound"));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,25 @@
using MikuSB.Enums.Player;
namespace MikuSB.GameServer.Command;
[AttributeUsage(AttributeTargets.Class)]
public class CommandInfoAttribute(
string name, string desc, string usage, string[] alias, PermEnum[] perm) : Attribute
{
public string Name { get; } = name;
public string Description { get; } = desc;
public string Usage { get; } = usage;
public PermEnum[] Perm { get; } = perm;
public string[] Alias { get; } = alias;
}
[AttributeUsage(AttributeTargets.Method)]
public class CommandDefaultAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class CommandMethodAttribute(string method) : Attribute
{
public string MethodName { get; } = method;
}

View File

@@ -0,0 +1,19 @@

namespace MikuSB.GameServer.Command;
public static class CommandExecutor
{
public delegate void RunCommand(ICommandSender sender, string cmd);
public static event RunCommand? OnRunCommand;
public static void ExecuteCommand(ICommandSender sender, string cmd)
{
OnRunCommand?.Invoke(sender, cmd);
}
public static void ConsoleExcuteCommand(string input)
{
CommandManager.HandleCommand(input, new ConsoleCommandSender(CommandManager.Logger));
}
}

View File

@@ -0,0 +1,3 @@
namespace MikuSB.GameServer.Command;
public interface ICommands;

View File

@@ -0,0 +1,126 @@
using MikuSB.Database.Account;
using MikuSB.Enums.Player;
using MikuSB.GameServer.Server;
using MikuSB.Internationalization;
using MikuSB.TcpSharp;
using MikuSB.Util;
using System.Reflection;
namespace MikuSB.GameServer.Command;
public class CommandManager
{
public static Logger Logger { get; } = new("CommandManager");
public static Dictionary<string, ICommands> Commands { get; } = [];
public static Dictionary<string, CommandInfoAttribute> CommandInfo { get; } = [];
public static Dictionary<string, string> CommandAlias { get; } = []; // <aliaName, fullName>
public static void RegisterCommands()
{
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
if (typeof(ICommands).IsAssignableFrom(type) && !type.IsAbstract)
RegisterCommand(type);
Logger.Info(I18NManager.Translate("Server.ServerInfo.RegisterItem", Commands.Count.ToString(),
I18NManager.Translate("Word.Command")));
}
public static void RegisterCommand(Type type)
{
var attr = type.GetCustomAttribute<CommandInfoAttribute>();
if (attr == null) return;
var instance = Activator.CreateInstance(type);
if (instance is not ICommands command) return;
Commands.Add(attr.Name, command);
CommandInfo.Add(attr.Name, attr);
// register alias
foreach (var alias in attr.Alias) // add alias
CommandAlias.Add(alias, attr.Name);
}
public static async void HandleCommand(string input, ICommandSender sender)
{
try
{
var argInfo = new CommandArg(input, sender);
var target = sender.GetSender();
foreach (var arg in argInfo.Args.ToList()) // Copy
{
switch (arg[0])
{
case '-':
argInfo.Attributes.Add(arg[1..]);
break;
case '@':
_ = int.TryParse(arg[1..], out target);
argInfo.Args.Remove(arg);
break;
}
}
argInfo.TargetUid = target;
if (SocketListener.Connections.Values.ToList().Find(item =>
(item as Connection)?.Player?.Uid == target) is Connection con)
argInfo.Target = con;
// find register cmd
var cmdName = argInfo.Args[0];
if (CommandAlias.TryGetValue(cmdName, out var fullName)) cmdName = fullName;
if (!Commands.TryGetValue(cmdName, out var command))
{
await sender.SendMsg(I18NManager.Translate("Game.Command.Notice.CommandNotFound"));
return;
}
argInfo.Args.RemoveAt(0);
var cmdInfo = CommandInfo[cmdName];
// Check cmd perms
if (!AccountData.HasPerm(cmdInfo.Perm, sender.GetSender()))
{
await sender.SendMsg(I18NManager.Translate("Game.Command.Notice.NoPermission"));
return;
}
if (argInfo.Target?.Player?.Uid != sender.GetSender() && !AccountData.HasPerm([PermEnum.Other], sender.GetSender()))
{
await sender.SendMsg(I18NManager.Translate("Game.Command.Notice.NoPermission"));
return;
}
// find CommandMethodAttribute
var isFound = false;
foreach (var methodInfo in command.GetType().GetMethods())
{
var attr = methodInfo.GetCustomAttribute<CommandMethodAttribute>();
if (attr == null) continue;
if (argInfo.Args.Count > 0 && attr.MethodName == argInfo.Args[0])
{
argInfo.Args.RemoveAt(0);
isFound = true;
methodInfo.Invoke(command, [argInfo]);
break;
}
}
if (isFound) return;
// find CommandDefaultAttribute
foreach (var methodInfo in command.GetType().GetMethods())
{
var attr = methodInfo.GetCustomAttribute<CommandDefaultAttribute>();
if (attr == null) continue;
isFound = true;
methodInfo.Invoke(command, [argInfo]);
break;
}
if (isFound) return;
// failed to find method
await sender.SendMsg(I18NManager.Translate(cmdInfo.Usage));
}
catch (Exception ex)
{
Logger.Error(I18NManager.Translate("Game.Command.Notice.InternalError", ex.ToString()));
}
}
}

View File

@@ -0,0 +1,41 @@
using MikuSB.Enums.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.Util;
namespace MikuSB.GameServer.Command;
public interface ICommandSender
{
public ValueTask SendMsg(string msg);
public int GetSender();
}
public class ConsoleCommandSender(Logger logger) : ICommandSender
{
public async ValueTask SendMsg(string msg)
{
logger.Info(msg);
await Task.CompletedTask;
}
public int GetSender()
{
return (int)ServerEnum.Console;
}
}
public class PlayerCommandSender(PlayerInstance player) : ICommandSender
{
public PlayerInstance Player = player;
public async ValueTask SendMsg(string msg)
{
// TODO
}
public int GetSender()
{
return Player.Uid;
}
}

View File

@@ -0,0 +1,52 @@
using MikuSB.Enums.Player;
using MikuSB.Internationalization;
using MikuSB.Util.Extensions;
namespace MikuSB.GameServer.Command.Commands;
[CommandInfo("help", "Game.Command.Help.Desc", "Game.Command.Help.Usage", ["h"], [PermEnum.Support, PermEnum.Trial])]
public class CommandHelp : ICommands
{
[CommandDefault]
public async static ValueTask Help(CommandArg arg)
{
if (arg.Args.Count == 1)
{
var cmd = arg.Args[0];
if (CommandManager.CommandInfo == null || !CommandManager.CommandInfo.TryGetValue(cmd, out var command))
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Notice.CommandNotFound"));
return;
}
var msg =
$"/{command.Name} - {I18NManager.Translate(command.Description)}\n{I18NManager.Translate(command.Usage)}";
if (command.Alias.Length > 0)
msg +=
$"\n{I18NManager.Translate("Game.Command.Help.CommandAlias")} {command.Alias.ToList().ToArrayString()}";
if (command.Perm != null)
msg += $"\n{I18NManager.Translate("Game.Command.Help.CommandPermission")} {string.Join(", ", command.Perm.Select(perm => perm.ToString()))}";
await arg.SendMsg(msg + "\n");
return;
}
else
{
await arg.SendMsg(I18NManager.Translate("Game.Command.Help.Commands"));
if (CommandManager.CommandInfo == null) return;
foreach (var command in CommandManager.CommandInfo.Values)
{
var msg =
$"/{command.Name} - {I18NManager.Translate(command.Description)}\n{I18NManager.Translate(command.Usage)}";
if (command.Alias.Length > 0)
msg +=
$"\n{I18NManager.Translate("Game.Command.Help.CommandAlias")} {command.Alias.ToList().ToArrayString()}";
if (command.Perm != null)
msg += $"\n{I18NManager.Translate("Game.Command.Help.CommandPermission")} {string.Join(", ", command.Perm.Select(perm => perm.ToString()))}";
await arg.SendMsg(msg + "\n");
}
}
}
}

View File

@@ -0,0 +1,8 @@
using MikuSB.GameServer.Game.Player;
namespace MikuSB.GameServer.Game;
public class BasePlayerManager(PlayerInstance player)
{
public PlayerInstance Player { get; private set; } = player;
}

View File

@@ -0,0 +1,98 @@
using MikuSB.Database;
using MikuSB.Database.Account;
using MikuSB.Database.Player;
using MikuSB.GameServer.Server;
using MikuSB.TcpSharp;
using MikuSB.Util.Extensions;
namespace MikuSB.GameServer.Game.Player;
public class PlayerInstance(PlayerGameData data)
{
#region Property
public Connection? Connection { get; set; }
public static readonly List<PlayerInstance> _playerInstances = [];
public int Uid { get; set; }
public bool Initialized { get; set; }
public bool IsNewPlayer { get; set; }
#endregion
#region Data & Manager
public PlayerGameData Data { get; set; } = data;
#endregion
#region Initializers
public PlayerInstance(int uid) : this(new PlayerGameData { Uid = uid })
{
// new player
IsNewPlayer = true;
Data.Name = AccountData.GetAccountByUid(uid)?.Username;
DatabaseHelper.CreateInstance(Data);
var t = Task.Run(async () =>
{
await InitialPlayerManager();
});
t.Wait();
Initialized = true;
}
private async ValueTask InitialPlayerManager()
{
Uid = Data.Uid;
Data.LastActiveTime = Extensions.GetUnixSec();
await Task.CompletedTask;
}
public T InitializeDatabase<T>() where T : BaseDatabaseDataHelper, new()
{
var instance = DatabaseHelper.GetInstanceOrCreateNew<T>(Uid);
return instance!;
}
#endregion
#region Network
public async ValueTask OnEnterGame()
{
if (!Initialized) await InitialPlayerManager();
}
public async ValueTask OnLogin()
{
_playerInstances.Add(this);
await Task.CompletedTask;
}
public static PlayerInstance? GetPlayerInstanceByUid(long uid)
=> _playerInstances.FirstOrDefault(player => player.Uid == uid);
public void OnLogoutAsync()
{
_playerInstances.Remove(this);
}
public async ValueTask SendPacket(BasePacket packet)
{
if (Connection?.IsOnline == true) await Connection.SendPacket(packet);
}
#endregion
#region Actions
public async ValueTask OnHeartBeat()
{
DatabaseHelper.ToSaveUidList.SafeAdd(Uid);
await Task.CompletedTask;
}
#endregion
#region Serialization
#endregion
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CETCompat>false</CETCompat>
<RootNamespace>MikuSB.GameServer</RootNamespace>
<StartupObject></StartupObject>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AssemblyName>MikuGameServer</AssemblyName>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Game\Hero\**" />
<EmbeddedResource Remove="Game\Hero\**" />
<None Remove="Game\Hero\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\TcpSharp\TcpSharp.csproj" />
<ProjectReference Include="..\Proto\Proto.csproj" />
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,142 @@
using MikuSB.Enums.Packet;
using MikuSB.GameServer.Game.Player;
using MikuSB.GameServer.Server.Packet;
using MikuSB.TcpSharp;
using MikuSB.Util;
using System.Buffers;
using System.Net;
using System.Net.Sockets;
namespace MikuSB.GameServer.Server;
public class Connection(Socket socket, IPEndPoint remote) : SocketConnection(socket, remote)
{
private static readonly Logger Logger = new("GameServer");
public PlayerInstance? Player { get; set; }
private static readonly HashSet<string> DummyPacketNames =
[
];
public override async void Start()
{
Logger.Info($"New connection from {RemoteEndPoint}.");
State = SessionStateEnum.WAITING_FOR_TOKEN;
await ReceiveLoop();
}
public override void Stop(bool isServerStop = false)
{
Player?.OnLogoutAsync();
SocketListener.UnregisterConnection(this);
base.Stop(isServerStop);
}
public static int GetInt32(byte[] buf, int index)
{
int networkValue = BitConverter.ToInt32(buf, index);
return IPAddress.NetworkToHostOrder((int)networkValue);
}
protected async Task ReceiveLoop()
{
try
{
var stream = new NetworkStream(Socket, ownsSocket: false);
while (SocketConnected())
{
var decodedPacket = await new PacketCodec().ReadPacketAsync(stream, CancelToken.Token);
if (decodedPacket == null)
{
Logger.Info("Client disconnected");
break;
}
switch (decodedPacket.Framing)
{
case PacketFraming.FourByteLittleEndianLength:
case PacketFraming.TwoByteBigEndianLength:
Framing = decodedPacket.Framing;
LogPacket("Recv", decodedPacket.CmdId, decodedPacket.Body.ToArray(),Framing);
await HandlePacket(decodedPacket.CmdId, decodedPacket.Body.ToArray());
break;
case PacketFraming.Control:
Logger.Info("Control packet received");
// Handle control packet if needed
break;
case PacketFraming.Unknown:
Logger.Warn("Unknown packet format received");
break;
}
}
}
catch (OperationCanceledException)
{
Logger.Info("ReceiveLoop cancelled");
}
catch (Exception ex)
{
Logger.Info($"ReceiveLoop error: {ex}");
}
finally
{
Socket.Close();
}
Stop();
}
private async Task HandlePacket(ushort opcode, byte[] payload)
{
var packetName = LogMap.GetValueOrDefault(opcode);
if (DummyPacketNames.Contains(packetName!))
{
await SendDummy(packetName!);
Logger.Info($"[Dummy] Send Dummy {packetName}");
return;
}
// Find the Handler for this opcode
var handler = HandlerManager.GetHandler(opcode);
if (handler != null)
{
// Handle
// Make sure session is ready for packets
var state = State;
try
{
await handler.OnHandle(this, payload, (ushort)DownStreamSeqNo);
}
catch (Exception e)
{
Logger.Error(e.Message, e);
}
return;
}
if (ConfigManager.Config.ServerOption.EnableDebug &&
ConfigManager.Config.ServerOption.DebugNoHandlerPacket && !IgnoreLog.Contains(opcode))
Logger.Error($"No handler found for {packetName}({opcode})");
//if (ConfigManager.Config.ServerOption.AutoSendResponseWhenNoHandler)
//{
// await SendDummy(packetName);
//}
}
private async Task SendDummy(string packetName)
{
var respName = packetName.Replace("Req", "Rsp"); // Get the response packet name
if (respName == packetName) return; // do not send rsp when resp name = recv name
var respOpcode = LogMap.FirstOrDefault(x => x.Value == respName).Key; // Get the response opcode
// Send Rsp
await SendPacket(respOpcode);
}
}

View File

@@ -0,0 +1,13 @@
using MikuSB.TcpSharp;
namespace MikuSB.GameServer.Server;
public class Listener : SocketListener
{
public static Connection? GetActiveConnection(int uid)
{
var con = Connections.Values.FirstOrDefault(c =>
(c as Connection)?.Player?.Uid == uid && c.State == SessionStateEnum.ACTIVE) as Connection;
return con;
}
}

View File

@@ -0,0 +1,6 @@
namespace MikuSB.GameServer.Server.Packet;
public abstract class Handler
{
public abstract Task OnHandle(Connection connection, byte[] data, ushort SeqNo = 0);
}

View File

@@ -0,0 +1,31 @@
using System.Reflection;
namespace MikuSB.GameServer.Server.Packet;
public static class HandlerManager
{
public static Dictionary<int, Handler> handlers = [];
public static void Init()
{
var classes = Assembly.GetExecutingAssembly().GetTypes(); // Get all classes in the assembly
foreach (var cls in classes)
{
var attribute = (Opcode?)Attribute.GetCustomAttribute(cls, typeof(Opcode));
if (attribute != null) handlers.Add(attribute.CmdId, (Handler)Activator.CreateInstance(cls)!);
}
}
public static Handler? GetHandler(int cmdId)
{
try
{
return handlers[cmdId];
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace MikuSB.GameServer.Server.Packet;
[AttributeUsage(AttributeTargets.Class)]
public class Opcode(int cmdId) : Attribute
{
public int CmdId = cmdId;
}

View File

@@ -0,0 +1,50 @@
using MikuSB.Data;
using MikuSB.Database;
using MikuSB.Database.Account;
using MikuSB.Database.Player;
using MikuSB.GameServer.Game.Player;
using MikuSB.GameServer.Server.Packet.Send.Login;
using MikuSB.Proto;
using MikuSB.TcpSharp;
using MikuSB.Util;
namespace MikuSB.GameServer.Server.Packet.Recv.Login;
[Opcode(CmdIds.ReqLogin)]
public class HandlerReqLogin : Handler
{
public override async Task OnHandle(Connection connection, byte[] data, ushort seqNo)
{
var req = ReqLogin.Parser.ParseFrom(data);
var account = AccountData.GetAccountByUid(1);
if (account == null)
{
AccountData.CreateAccount("MIKU", 0, "");
account = AccountData.GetAccountByUid(1);
if (account == null)
{
await connection.SendPacket(CmdIds.NtfLogout);
return;
}
}
if (!ResourceManager.IsLoaded)
// resource manager not loaded, return
return;
var prev = Listener.GetActiveConnection(account.Uid);
if (prev != null)
{
await connection.SendPacket(CmdIds.NtfLogout);
prev.Stop();
}
connection.State = SessionStateEnum.WAITING_FOR_LOGIN;
var pd = DatabaseHelper.GetInstance<PlayerGameData>(account.Uid);
connection.Player = pd == null ? new PlayerInstance(account.Uid) : new PlayerInstance(pd);
connection.DebugFile = Path.Combine(ConfigManager.Config.Path.LogPath, "Debug/", $"{account.Uid}/",
$"Debug-{DateTime.Now:yyyy-MM-dd HH-mm-ss}.log");
await connection.Player.OnEnterGame();
connection.Player.Connection = connection;
await connection.SendPacket(new PacketRspLogin(connection.Player!));
}
}

View File

@@ -0,0 +1,29 @@
using MikuSB.GameServer.Game.Player;
using MikuSB.TcpSharp;
using MikuSB.Proto;
using MikuSB.Util.Extensions;
namespace MikuSB.GameServer.Server.Packet.Send.Login;
public class PacketRspLogin : BasePacket
{
public PacketRspLogin(PlayerInstance player) : base(CmdIds.RspLogin)
{
var proto = new RspLogin
{
Timestamp = (uint)Extensions.GetUnixSec(),
WorldChannel = 1,
AreaId = 1,
Data = new Player
{
Pid = (ulong)player.Data.Uid,
Account = player.Data.Name,
Name = player.Data.Name,
Level = 80
},
NeedRename = false
};
SetData(proto);
}
}