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

51
TcpSharp/BasePacket.cs Normal file
View File

@@ -0,0 +1,51 @@
using Google.Protobuf;
using MikuSB.Enums.Packet;
namespace MikuSB.TcpSharp;
public class BasePacket
{
public ushort CmdId { get; set; }
public byte[] Body { get; set; }
public ushort SeqNo { get; set; }
public ushort PushSeq { get; set; }
public long Timestamp { get; set; }
public IMessage? Message { get; set; }
public PacketFraming Framing { get; set; }
public BasePacket(ushort cmdId)
{
CmdId = cmdId;
Body = Array.Empty<byte>();
SeqNo = 0;
PushSeq = 0;
Timestamp = 0;
Framing = PacketFraming.FourByteLittleEndianLength;
}
public BasePacket(ushort cmdId, byte[] body, PacketFraming framing = PacketFraming.FourByteLittleEndianLength)
{
CmdId = cmdId;
Body = body ?? Array.Empty<byte>();
Framing = framing;
SeqNo = 0;
PushSeq = 0;
Timestamp = 0;
}
public void SetData(byte[] data)
{
Body = data;
}
public void SetData(IMessage message)
{
Body = message.ToByteArray();
Message = message;
}
public void SetData(string base64)
{
SetData(Convert.FromBase64String(base64));
}
}

308
TcpSharp/PacketCodec.cs Normal file
View File

@@ -0,0 +1,308 @@
using Google.Protobuf;
using MikuSB.Enums.Packet;
using MikuSB.Util;
using System.Buffers.Binary;
using System.Net.Sockets;
namespace MikuSB.TcpSharp
{
public class PacketCodec
{
private const int HeaderSize4Byte = 4;
private const int HeaderSize2Byte = 2;
private const int MaxPacketLength = 1024 * 1024;
private const ushort ClientMagic = 0x011F;
private const int ControlPacketSize = 35;
private static readonly Logger Logger = new("PacketCodec");
public PacketCodec()
{
}
public async Task<BasePacket?> ReadPacketAsync(
Stream stream,
CancellationToken cancellationToken = default)
{
try
{
var lengthBuffer = new byte[HeaderSize4Byte];
if (!await ReadExactAsync(stream, lengthBuffer, cancellationToken))
{
Logger.Debug("Connection closed before packet header");
return null;
}
var framing = DetectFraming(lengthBuffer);
switch (framing)
{
case PacketFraming.Control:
return await HandleControlPacket(stream, cancellationToken);
case PacketFraming.TwoByteBigEndianLength:
return await HandleTwoBytePacket(stream, lengthBuffer, cancellationToken);
case PacketFraming.FourByteLittleEndianLength:
return await HandleFourBytePacket(stream, lengthBuffer, cancellationToken);
default:
return await HandleUnknownPacket(stream, lengthBuffer, cancellationToken);
}
}
catch (OperationCanceledException)
{
Logger.Debug("Packet read cancelled");
return null;
}
catch (Exception ex)
{
Logger.Error($"Error reading packet {ex}");
return null;
}
}
public byte[] Encode(ushort packetId, byte[] payload, PacketFraming framing = PacketFraming.FourByteLittleEndianLength)
{
return framing switch
{
PacketFraming.TwoByteBigEndianLength => EncodeTwoByteFrame(packetId, payload),
PacketFraming.FourByteLittleEndianLength => EncodeFourByteFrame(packetId, payload),
_ => EncodeFourByteFrame(packetId, payload)
};
}
public byte[] EncodeRaw(ushort packetId, byte[] payload, PacketFraming framing = PacketFraming.FourByteLittleEndianLength)
{
return framing switch
{
PacketFraming.TwoByteBigEndianLength => EncodeTwoByteFrame(packetId, payload),
PacketFraming.FourByteLittleEndianLength => EncodeFourByteFrame(packetId, payload),
_ => EncodeFourByteFrame(packetId, payload)
};
}
#region Private Methods
private PacketFraming DetectFraming(byte[] header)
{
var firstTwoBytes = BinaryPrimitives.ReadUInt16BigEndian(header.AsSpan(0, 2));
var nextTwoBytes = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(2, 2));
if (firstTwoBytes == ClientMagic && nextTwoBytes == 0)
return PacketFraming.Control;
if (firstTwoBytes == ClientMagic && IsValidPacketId(nextTwoBytes))
return PacketFraming.TwoByteBigEndianLength;
if (IsValidTwoByteHeader(firstTwoBytes, (ushort)nextTwoBytes))
return PacketFraming.TwoByteBigEndianLength;
return PacketFraming.FourByteLittleEndianLength;
}
private async Task<BasePacket?> HandleControlPacket(Stream stream, CancellationToken cancellationToken)
{
var controlData = new byte[ControlPacketSize];
if (!await ReadExactAsync(stream, controlData, cancellationToken))
{
Logger.Debug("Connection closed during control packet read");
return null;
}
Logger.Debug("Control packet received");
return new BasePacket(0)
{
Framing = PacketFraming.Control,
Body = Array.Empty<byte>()
};
}
private async Task<BasePacket?> HandleTwoBytePacket(
Stream stream,
byte[] header,
CancellationToken cancellationToken)
{
var packetId = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(2, 2));
var wrapper = new byte[ControlPacketSize];
if (!await ReadExactAsync(stream, wrapper, cancellationToken))
{
Logger.Debug($"Connection closed during wrapper read for packet {packetId}");
return null;
}
var payloadLength = BinaryPrimitives.ReadUInt16LittleEndian(wrapper.AsSpan(6, 2));
var payload = await ReadPayloadAsync(stream, payloadLength, cancellationToken);
if (payload == null)
return null;
//Logger.Debug($"Packet received (2-byte framing): ID={packetId}, PayloadSize={payload.Length}");
return new BasePacket(packetId)
{
Framing = PacketFraming.TwoByteBigEndianLength,
Body = payload
};
}
private async Task<BasePacket?> HandleFourBytePacket(
Stream stream,
byte[] header,
CancellationToken cancellationToken)
{
var length = BinaryPrimitives.ReadUInt32LittleEndian(header);
if (length < 2 || length > MaxPacketLength)
{
Logger.Warn($"Invalid packet length: {length}");
return null;
}
var frame = new byte[length];
if (!await ReadExactAsync(stream, frame, cancellationToken))
{
Logger.Debug("Connection closed during packet body read");
return null;
}
var packetId = BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(0, 2));
var payload = frame[2..];
//Logger.Debug($"Packet received (4-byte framing): ID={packetId}, PayloadSize={payload.Length}");
return new BasePacket(packetId)
{
Framing = PacketFraming.FourByteLittleEndianLength,
Body = payload
};
}
private async Task<BasePacket?> HandleUnknownPacket(
Stream stream,
byte[] header,
CancellationToken cancellationToken)
{
var extraData = await ReadAvailableBytesAsync(stream, cancellationToken);
var combinedData = new byte[header.Length + extraData.Length];
header.CopyTo(combinedData, 0);
extraData.CopyTo(combinedData, header.Length);
Logger.Warn($"Unknown packet format detected, captured {combinedData.Length} bytes");
return new BasePacket(0)
{
Framing = PacketFraming.Unknown,
Body = combinedData
};
}
private async Task<byte[]?> ReadPayloadAsync(
Stream stream,
int length,
CancellationToken cancellationToken)
{
if (length <= 0)
return Array.Empty<byte>();
if (length > MaxPacketLength)
{
Logger.Warn($"Payload too large: {length}");
return null;
}
var payload = new byte[length];
if (!await ReadExactAsync(stream, payload, cancellationToken))
return null;
return payload;
}
private byte[] EncodeTwoByteFrame(ushort packetId, byte[] payload)
{
var wrappedPayload = WrapPayload(payload);
var buffer = new byte[HeaderSize4Byte + wrappedPayload.Length];
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), ClientMagic);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), packetId);
wrappedPayload.CopyTo(buffer.AsSpan(HeaderSize4Byte));
return buffer;
}
private byte[] EncodeFourByteFrame(ushort packetId, byte[] payload)
{
var buffer = new byte[HeaderSize4Byte + HeaderSize2Byte + payload.Length];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), (uint)(HeaderSize2Byte + payload.Length));
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(4, 2), packetId);
payload.CopyTo(buffer.AsSpan(HeaderSize4Byte + HeaderSize2Byte));
return buffer;
}
private byte[] WrapPayload(byte[] payload)
{
const int wrapperHeaderSize = 35;
var wrapped = new byte[wrapperHeaderSize + payload.Length];
BinaryPrimitives.WriteUInt16LittleEndian(wrapped.AsSpan(6, 2), (ushort)payload.Length);
wrapped[11] = 1;
payload.CopyTo(wrapped.AsSpan(wrapperHeaderSize));
return wrapped;
}
private static async Task<bool> ReadExactAsync(
Stream stream,
byte[] buffer,
CancellationToken cancellationToken)
{
var offset = 0;
while (offset < buffer.Length)
{
var read = await stream.ReadAsync(buffer.AsMemory(offset), cancellationToken);
if (read == 0)
return false;
offset += read;
}
return true;
}
private static async Task<byte[]> ReadAvailableBytesAsync(
Stream stream,
CancellationToken cancellationToken)
{
if (stream is not NetworkStream networkStream || !networkStream.DataAvailable)
return Array.Empty<byte>();
using var ms = new MemoryStream();
var buffer = new byte[4096];
while (networkStream.DataAvailable && ms.Length < 16384)
{
var read = await networkStream.ReadAsync(buffer, cancellationToken);
if (read <= 0)
break;
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
private static bool IsValidTwoByteHeader(int firstTwoBytes, ushort packetId)
{
return firstTwoBytes >= 2
&& firstTwoBytes <= ushort.MaxValue
&& IsValidPacketId(packetId);
}
private static bool IsValidPacketId(ushort packetId)
{
return packetId != 0;
}
#endregion
}
}

View File

@@ -0,0 +1,10 @@
namespace MikuSB.TcpSharp;
public enum SessionStateEnum
{
INACTIVE,
WAITING_FOR_TOKEN,
WAITING_FOR_LOGIN,
PICKING_CHARACTER,
ACTIVE
}

View File

@@ -0,0 +1,203 @@
using Google.Protobuf;
using Google.Protobuf.Reflection;
using MikuSB.Enums.Packet;
using MikuSB.Proto;
using MikuSB.Util;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
namespace MikuSB.TcpSharp;
public class SocketConnection
{
public static readonly ConcurrentBag<int> BannedPackets = [];
private static readonly Logger Logger = new("GameServer");
public static readonly ConcurrentDictionary<int, string> LogMap = [];
public static readonly ConcurrentBag<int> IgnoreLog =
[
];
protected readonly CancellationTokenSource CancelToken;
protected readonly Socket Socket;
public readonly IPEndPoint RemoteEndPoint;
public string DebugFile = "";
public bool IsOnline = true;
public StreamWriter? Writer;
public int DownStreamSeqNo;
public int UpStreamSeqNo;
public PacketFraming Framing;
public SocketConnection(Socket socket, IPEndPoint remote)
{
Socket = socket;
RemoteEndPoint = remote;
CancelToken = new CancellationTokenSource();
Start();
}
public SessionStateEnum State { get; set; } = SessionStateEnum.INACTIVE;
internal long ConnectionId { get; set; }
public virtual void Start()
{
Logger.Info($"New connection from {RemoteEndPoint}.");
State = SessionStateEnum.WAITING_FOR_TOKEN;
}
public virtual void Stop(bool isServerStop = false)
{
try
{
Socket?.Shutdown(SocketShutdown.Both);
}
catch { }
finally
{
Socket?.Close();
Socket?.Dispose();
}
try
{
CancelToken.Cancel();
CancelToken.Dispose();
}
catch
{
}
IsOnline = false;
}
public bool SocketConnected()
{
try
{
return !((Socket.Poll(1000, SelectMode.SelectRead) && (Socket.Available == 0)) || !Socket.Connected);
}
catch (Exception e)
{
return false;
}
}
public void LogPacket(string sendOrRecv, ushort opcode, byte[] payload, PacketFraming framing)
{
if (!ConfigManager.Config.ServerOption.EnableDebug) return;
try
{
//Logger.DebugWriteLine($"{sendOrRecv}: {Enum.GetName(typeof(OpCode), opcode)}({opcode})\r\n{Convert.ToHexString(payload)}");
if (IgnoreLog.Contains(opcode)) return;
if (!ConfigManager.Config.ServerOption.DebugDetailMessage) throw new Exception(); // go to catch block
var typ = AppDomain.CurrentDomain.GetAssemblies()
.SingleOrDefault(assembly => assembly.GetName().Name == "MikuProto")!.GetTypes()
.First(t => t.Name == $"{LogMap[opcode]}"); //get the type using the packet name
var descriptor =
typ.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static)?.GetValue(
null, null) as MessageDescriptor; // get the static property Descriptor
var packet = descriptor?.Parser.ParseFrom(payload);
var formatter = JsonFormatter.Default;
var asJson = formatter.Format(packet);
var output = $"{sendOrRecv}: {LogMap[opcode]}({opcode}) ({framing})\r\n{asJson}";
if (ConfigManager.Config.ServerOption.DebugMessage)
Logger.Debug(output);
if (DebugFile == "" || !ConfigManager.Config.ServerOption.SavePersonalDebugFile) return;
var sw = GetWriter();
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] [GameServer] [DEBUG] " + output);
sw.Flush();
}
catch
{
var output = $"{sendOrRecv}: {LogMap.GetValueOrDefault(opcode, "UnknownPacket")}({opcode})";
if (ConfigManager.Config.ServerOption.DebugMessage)
Logger.Debug(output);
if (DebugFile != "" && ConfigManager.Config.ServerOption.SavePersonalDebugFile)
{
var sw = GetWriter();
sw.WriteLine($"[{DateTime.Now:HH:mm:ss}] [GameServer] [DEBUG] " + output);
sw.Flush();
}
}
}
private StreamWriter GetWriter()
{
// Create the file if it doesn't exist
var file = new FileInfo(DebugFile);
if (!file.Exists)
{
Directory.CreateDirectory(file.DirectoryName!);
File.Create(DebugFile).Dispose();
}
Writer ??= new StreamWriter(DebugFile, true);
return Writer;
}
public async Task SendPacket(byte[] packet)
{
try
{
if (Socket.Connected)
{
await Socket.SendAsync(
new ArraySegment<byte>(packet),
SocketFlags.None,
CancelToken.Token
);
}
}
catch
{
// ignore
}
}
public async Task SendPacket(BasePacket packet, ushort seqNo = 0)
{
// Test
if (packet.CmdId <= 0)
{
Logger.Debug("Tried to send packet with missing cmd id!");
return;
}
// DO NOT REMOVE (unless we find a way to validate code before sending to client which I don't think we can)
if (BannedPackets.Contains(packet.CmdId)) return;
LogPacket("Send", packet.CmdId, packet.Body,Framing);
byte[] packetBytes = new PacketCodec().Encode(packet.CmdId, packet.Body,Framing);
try
{
await SendPacket(packetBytes);
}
catch
{
// ignore
}
}
public async Task SendPacket(int cmdId)
{
await SendPacket(new BasePacket((ushort)cmdId));
}
public async Task SendPacket(int cmdId, ushort seqNo)
{
var packet = new BasePacket((ushort)cmdId);
packet.SeqNo = seqNo;
await SendPacket(packet);
}
public async Task SendPacket(int cmdId, IMessage msg, ushort seqNo = 0)
{
var packet = new BasePacket((ushort)cmdId);
packet.SetData(msg);
packet.SeqNo = seqNo;
await SendPacket(packet);
}
}

105
TcpSharp/SocketListener.cs Normal file
View File

@@ -0,0 +1,105 @@
using System.Net.Sockets;
using System.Net;
using MikuSB.Util;
using MikuSB.Internationalization;
namespace MikuSB.TcpSharp;
public class SocketListener
{
private static IPEndPoint? ListenAddress;
private static readonly Logger Logger = new("GameServer");
private static Socket? serverSocket;
public static readonly SortedList<long, SocketConnection> Connections = [];
public static Type BaseConnection { get; set; } = typeof(SocketConnection);
private static int PORT => ConfigManager.Config.GameServer.Port;
private static long _nextId = 0;
public static void StartListener()
{
if (serverSocket != null)
throw new InvalidOperationException("SocketListener already started.");
ListenAddress = new IPEndPoint(IPAddress.Parse(ConfigManager.Config.GameServer.BindAddress), PORT);
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
serverSocket.Bind(ListenAddress);
serverSocket.Listen(100);
Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerRunning",
I18NManager.Translate("Word.Game"),
ConfigManager.Config.GameServer.GetDisplayAddress()));
_ = Task.Run(AcceptLoop);
}
private static async Task AcceptLoop()
{
if (serverSocket == null)
throw new InvalidOperationException("Server socket not initialized.");
try
{
while (true)
{
Socket clientSocket = await serverSocket.AcceptAsync();
var remote = clientSocket.RemoteEndPoint as IPEndPoint;
if (remote == null)
{
clientSocket.Close();
continue;
}
try
{
var connection = (SocketConnection?)Activator.CreateInstance(BaseConnection, clientSocket, remote);
if (connection == null)
{
Logger.Error($"Failed to create connection instance from {BaseConnection.Name}");
clientSocket.Close();
continue;
}
var id = Interlocked.Increment(ref _nextId);
connection.ConnectionId = id;
Connections[id] = connection;
Logger.Info($"Accepted connection #{id} from {remote}");
}
catch (Exception ex)
{
Logger.Error($"Error creating connection: {ex}");
clientSocket.Close();
}
}
}
catch (ObjectDisposedException)
{
Logger.Info("Server stopped listening.");
}
}
public static SocketConnection? GetConnectionByEndPoint(IPEndPoint ep)
{
Connections.TryGetValue(ep.GetHashCode(), out var conn);
return conn;
}
public static void UnregisterConnection(SocketConnection socket)
{
if (socket == null) return;
if (Connections.Remove(socket.ConnectionId))
{
Logger.Info($"Connection #{socket.ConnectionId} with {socket.RemoteEndPoint} has been closed");
}
}
}

21
TcpSharp/TcpSharp.csproj Normal file
View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CETCompat>false</CETCompat>
<AssemblyName>TcpSharp</AssemblyName>
<RootNamespace>MikuSB.TcpSharp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
<PackageReference Include="System.IO.Pipelines" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>