diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..75e7a15
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+[*.cs]
+
+# IDE0290: Use primary constructor
+csharp_style_prefer_primary_constructors = false
diff --git a/RPG.GameCore/RPG.GameCore.csproj b/RPG.GameCore/RPG.GameCore.csproj
index fa71b7a..88c3a39 100644
--- a/RPG.GameCore/RPG.GameCore.csproj
+++ b/RPG.GameCore/RPG.GameCore.csproj
@@ -6,4 +6,8 @@
enable
+
+
+
+
diff --git a/RPG.Network.Proto/CmdType.cs b/RPG.Network.Proto/CmdType.cs
new file mode 100644
index 0000000..c055b01
--- /dev/null
+++ b/RPG.Network.Proto/CmdType.cs
@@ -0,0 +1,218 @@
+namespace RPG.Network.Proto;
+public static class CmdType
+{
+ public const ushort CmdTypeNone = 0;
+ public const ushort CmdPlayerLoginCsReq = 1;
+ public const ushort CmdPlayerLoginScRsp = 2;
+ public const ushort CmdPlayerLogoutCsReq = 3;
+ public const ushort CmdPlayerLogoutScRsp = 4;
+ public const ushort CmdPlayerGetTokenCsReq = 5;
+ public const ushort CmdPlayerGetTokenScRsp = 6;
+ public const ushort CmdPlayerKeepAliveNotify = 7;
+ public const ushort CmdGmTalkScNotify = 8;
+ public const ushort CmdPlayerKickOutScNotify = 9;
+ public const ushort CmdGmTalkCsReq = 10;
+ public const ushort CmdGmTalkScRsp = 11;
+ public const ushort CmdGetStaminaExchangeCsReq = 12;
+ public const ushort CmdGetStaminaExchangeScRsp = 13;
+ public const ushort CmdExchangeStaminaCsReq = 14;
+ public const ushort CmdExchangeStaminaScRsp = 15;
+ public const ushort CmdGetAuthkeyCsReq = 16;
+ public const ushort CmdGetAuthkeyScRsp = 17;
+ public const ushort CmdRegionStopScNotify = 18;
+ public const ushort CmdAntiAddictScNotify = 19;
+ public const ushort CmdSetNicknameCsReq = 20;
+ public const ushort CmdSetNicknameScRsp = 21;
+ public const ushort CmdGetLevelRewardTakenListCsReq = 22;
+ public const ushort CmdGetLevelRewardTakenListScRsp = 23;
+ public const ushort CmdGetLevelRewardCsReq = 24;
+ public const ushort CmdGetLevelRewardScRsp = 25;
+ public const ushort CmdSyncTimeCsReq = 26;
+ public const ushort CmdSyncTimeScRsp = 27;
+ public const ushort CmdSetLanguageCsReq = 28;
+ public const ushort CmdSetLanguageScRsp = 29;
+ public const ushort CmdServerAnnounceNotify = 30;
+ public const ushort CmdPVEBattleResultCsReq = 101;
+ public const ushort CmdPVEBattleResultScRsp = 102;
+ public const ushort CmdQuitBattleCsReq = 103;
+ public const ushort CmdQuitBattleScRsp = 104;
+ public const ushort CmdGetCurBattleInfoCsReq = 105;
+ public const ushort CmdGetCurBattleInfoScRsp = 106;
+ public const ushort CmdSyncClientResVersionCsReq = 107;
+ public const ushort CmdSyncClientResVersionScRsp = 108;
+ public const ushort CmdGetStageDataCsReq = 201;
+ public const ushort CmdGetStageDataScRsp = 202;
+ public const ushort CmdStageBeginCsReq = 203;
+ public const ushort CmdStageBeginScRsp = 204;
+ public const ushort CmdGetAvatarDataCsReq = 301;
+ public const ushort CmdGetAvatarDataScRsp = 302;
+ public const ushort CmdAvatarExpUpCsReq = 303;
+ public const ushort CmdAvatarExpUpScRsp = 304;
+ public const ushort CmdUnlockSkilltreeCsReq = 305;
+ public const ushort CmdUnlockSkilltreeScRsp = 306;
+ public const ushort CmdPromoteAvatarCsReq = 307;
+ public const ushort CmdPromoteAvatarScRsp = 308;
+ public const ushort CmdDressAvatarCsReq = 309;
+ public const ushort CmdDressAvatarScRsp = 310;
+ public const ushort CmdTakeOffEquipmentCsReq = 311;
+ public const ushort CmdTakeOffEquipmentScRsp = 312;
+ public const ushort CmdAddAvatarScNotify = 313;
+ public const ushort CmdGetWaypointCsReq = 401;
+ public const ushort CmdGetWaypointScRsp = 402;
+ public const ushort CmdSetCurWaypointCsReq = 403;
+ public const ushort CmdSetCurWaypointScRsp = 404;
+ public const ushort CmdGetChapterCsReq = 405;
+ public const ushort CmdGetChapterScRsp = 406;
+ public const ushort CmdWaypointShowNewCsNotify = 407;
+ public const ushort CmdTakeChapterRewardCsReq = 408;
+ public const ushort CmdTakeChapterRewardScRsp = 409;
+ public const ushort CmdGetBagCsReq = 501;
+ public const ushort CmdGetBagScRsp = 502;
+ public const ushort CmdPromoteEquipmentCsReq = 503;
+ public const ushort CmdPromoteEquipmentScRsp = 504;
+ public const ushort CmdLockEquipmentCsReq = 505;
+ public const ushort CmdLockEquipmentScRsp = 506;
+ public const ushort CmdUseItemCsReq = 507;
+ public const ushort CmdUseItemScRsp = 508;
+ public const ushort CmdRankUpEquipmentCsReq = 509;
+ public const ushort CmdRankUpEquipmentScRsp = 510;
+ public const ushort CmdExpUpEquipmentCsReq = 511;
+ public const ushort CmdExpUpEquipmentScRsp = 512;
+ public const ushort CmdUseItemFoodCsReq = 513;
+ public const ushort CmdUseItemFoodScRsp = 514;
+ public const ushort CmdComposeItemCsReq = 515;
+ public const ushort CmdComposeItemScRsp = 516;
+ public const ushort CmdPlayerSyncScNotify = 601;
+ public const ushort CmdGetStageLineupCsReq = 701;
+ public const ushort CmdGetStageLineupScRsp = 702;
+ public const ushort CmdGetCurLineupDataCsReq = 703;
+ public const ushort CmdGetCurLineupDataScRsp = 704;
+ public const ushort CmdJoinLineupCsReq = 705;
+ public const ushort CmdJoinLineupScRsp = 706;
+ public const ushort CmdQuitLineupCsReq = 707;
+ public const ushort CmdQuitLineupScRsp = 708;
+ public const ushort CmdSwapLineupCsReq = 709;
+ public const ushort CmdSwapLineupScRsp = 710;
+ public const ushort CmdSyncLineupNotify = 711;
+ public const ushort CmdGetLineupAvatarDataCsReq = 712;
+ public const ushort CmdGetLineupAvatarDataScRsp = 713;
+ public const ushort CmdChangeLineupLeaderCsReq = 714;
+ public const ushort CmdChangeLineupLeaderScRsp = 715;
+ public const ushort CmdSwitchLineupIndexCsReq = 716;
+ public const ushort CmdSwitchLineupIndexScRsp = 717;
+ public const ushort CmdSetLineupNameCsReq = 718;
+ public const ushort CmdSetLineupNameScRsp = 719;
+ public const ushort CmdGetAllLineupDataCsReq = 720;
+ public const ushort CmdGetAllLineupDataScRsp = 721;
+ public const ushort CmdVirtualLineupDestroyNotify = 722;
+ public const ushort CmdGetMailCsReq = 801;
+ public const ushort CmdGetMailScRsp = 802;
+ public const ushort CmdMarkReadMailCsReq = 803;
+ public const ushort CmdMarkReadMailScRsp = 804;
+ public const ushort CmdDelMailCsReq = 805;
+ public const ushort CmdDelMailScRsp = 806;
+ public const ushort CmdTakeMailAttachmentCsReq = 807;
+ public const ushort CmdTakeMailAttachmentScRsp = 808;
+ public const ushort CmdNewMailScNotify = 809;
+ public const ushort CmdGetQuestDataCsReq = 901;
+ public const ushort CmdGetQuestDataScRsp = 902;
+ public const ushort CmdTakeQuestRewardCsReq = 903;
+ public const ushort CmdTakeQuestRewardScRsp = 904;
+ public const ushort CmdGetMazeCsReq = 1001;
+ public const ushort CmdGetMazeScRsp = 1002;
+ public const ushort CmdChooseMazeSeriesCsReq = 1003;
+ public const ushort CmdChooseMazeSeriesScRsp = 1004;
+ public const ushort CmdChooseMazeAbilityCsReq = 1005;
+ public const ushort CmdChooseMazeAbilityScRsp = 1006;
+ public const ushort CmdEnterMazeCsReq = 1007;
+ public const ushort CmdEnterMazeScRsp = 1008;
+ public const ushort CmdMazeBuffScNotify = 1011;
+ public const ushort CmdCastMazeSkillCsReq = 1012;
+ public const ushort CmdCastMazeSkillScRsp = 1013;
+ public const ushort CmdMazePlaneEventScNotify = 1014;
+ public const ushort CmdEnterMazeByServerScNotify = 1015;
+ public const ushort CmdGetMazeMapInfoCsReq = 1016;
+ public const ushort CmdGetMazeMapInfoScRsp = 1017;
+ public const ushort CmdFinishPlotCsReq = 1101;
+ public const ushort CmdFinishPlotScRsp = 1102;
+ public const ushort CmdGetMissionDataCsReq = 1201;
+ public const ushort CmdGetMissionDataScRsp = 1202;
+ public const ushort CmdFinishTalkMissionCsReq = 1203;
+ public const ushort CmdFinishTalkMissionScRsp = 1204;
+ public const ushort CmdMissionRewardScNotify = 1205;
+ public const ushort CmdSyncTaskCsReq = 1206;
+ public const ushort CmdSyncTaskScRsp = 1207;
+ public const ushort CmdDailyTaskDataScNotify = 1208;
+ public const ushort CmdTakeDailyTaskExtraRewardCsReq = 1209;
+ public const ushort CmdTakeDailyTaskExtraRewardScRsp = 1210;
+ public const ushort CmdDailyTaskRewardScNotify = 1211;
+ public const ushort CmdMissionGroupWarnScNotify = 1212;
+ public const ushort CmdFinishCosumeItemMissionCsReq = 1213;
+ public const ushort CmdFinishCosumeItemMissionScRsp = 1214;
+ public const ushort CmdEnterAdventureCsReq = 1301;
+ public const ushort CmdEnterAdventureScRsp = 1302;
+ public const ushort CmdSceneEntityMoveCsReq = 1401;
+ public const ushort CmdSceneEntityMoveScRsp = 1402;
+ public const ushort CmdInteractPropCsReq = 1403;
+ public const ushort CmdInteractPropScRsp = 1404;
+ public const ushort CmdSceneCastSkillCsReq = 1405;
+ public const ushort CmdSceneCastSkillScRsp = 1406;
+ public const ushort CmdGetCurSceneInfoCsReq = 1407;
+ public const ushort CmdGetCurSceneInfoScRsp = 1408;
+ public const ushort CmdSceneEntityUpdateScNotify = 1409;
+ public const ushort CmdSceneEntityDisappearScNotify = 1410;
+ public const ushort CmdSceneEntityMoveScNotify = 1411;
+ public const ushort CmdWaitCustomStringCsReq = 1412;
+ public const ushort CmdWaitCustomStringScRsp = 1413;
+ public const ushort CmdSpringTransferCsReq = 1414;
+ public const ushort CmdSpringTransferScRsp = 1415;
+ public const ushort CmdUpdateBuffScNotify = 1416;
+ public const ushort CmdDelBuffScNotify = 1417;
+ public const ushort CmdSpringRefreshCsReq = 1418;
+ public const ushort CmdSpringRefreshScRsp = 1419;
+ public const ushort CmdLastSpringRefreshTimeNotify = 1420;
+ public const ushort CmdReturnLastTownCsReq = 1421;
+ public const ushort CmdReturnLastTownScRsp = 1422;
+ public const ushort CmdSceneEnterStageCsReq = 1423;
+ public const ushort CmdSceneEnterStageScRsp = 1424;
+ public const ushort CmdEnterSectionCsReq = 1427;
+ public const ushort CmdEnterSectionScRsp = 1428;
+ public const ushort CmdSetCurInteractEntityCsReq = 1431;
+ public const ushort CmdSetCurInteractEntityScRsp = 1432;
+ public const ushort CmdRecoverAllLineupCsReq = 1433;
+ public const ushort CmdRecoverAllLineupScRsp = 1434;
+ public const ushort CmdSavePointsInfoNotify = 1435;
+ public const ushort CmdStartCocoonStageCsReq = 1436;
+ public const ushort CmdStartCocoonStageScRsp = 1437;
+ public const ushort CmdEntityBindPropCsReq = 1438;
+ public const ushort CmdEntityBindPropScRsp = 1439;
+ public const ushort CmdSetClientPausedCsReq = 1440;
+ public const ushort CmdSetClientPausedScRsp = 1441;
+ public const ushort CmdPropBeHitCsReq = 1442;
+ public const ushort CmdPropBeHitScRsp = 1443;
+ public const ushort CmdGetShopListCsReq = 1501;
+ public const ushort CmdGetShopListScRsp = 1502;
+ public const ushort CmdBuyGoodsCsReq = 1503;
+ public const ushort CmdBuyGoodsScRsp = 1504;
+ public const ushort CmdGetTutorialCsReq = 1601;
+ public const ushort CmdGetTutorialScRsp = 1602;
+ public const ushort CmdGetTutorialGuideCsReq = 1603;
+ public const ushort CmdGetTutorialGuideScRsp = 1604;
+ public const ushort CmdUnlockTutorialCsReq = 1605;
+ public const ushort CmdUnlockTutorialScRsp = 1606;
+ public const ushort CmdUnlockTutorialGuideCsReq = 1607;
+ public const ushort CmdUnlockTutorialGuideScRsp = 1608;
+ public const ushort CmdFinishTutorialCsReq = 1609;
+ public const ushort CmdFinishTutorialScRsp = 1610;
+ public const ushort CmdFinishTutorialGuideCsReq = 1611;
+ public const ushort CmdFinishTutorialGuideScRsp = 1612;
+ public const ushort CmdGetChallengeCsReq = 1701;
+ public const ushort CmdGetChallengeScRsp = 1702;
+ public const ushort CmdStartChallengeCsReq = 1703;
+ public const ushort CmdStartChallengeScRsp = 1704;
+ public const ushort CmdLeaveChallengeCsReq = 1705;
+ public const ushort CmdLeaveChallengeScRsp = 1706;
+ public const ushort CmdChallengeSettleNotify = 1707;
+ public const ushort CmdFinishChallengeCsReq = 1708;
+ public const ushort CmdFinishChallengeScRsp = 1709;
+}
\ No newline at end of file
diff --git a/RPG.Network.Proto/RPG.Network.Proto.csproj b/RPG.Network.Proto/RPG.Network.Proto.csproj
index fcff061..3263c22 100644
--- a/RPG.Network.Proto/RPG.Network.Proto.csproj
+++ b/RPG.Network.Proto/RPG.Network.Proto.csproj
@@ -8,6 +8,7 @@
+
@@ -20,6 +21,7 @@
+
diff --git a/RPG.Network.Proto/server_only.proto b/RPG.Network.Proto/server_only.proto
index c892c55..b48b344 100644
--- a/RPG.Network.Proto/server_only.proto
+++ b/RPG.Network.Proto/server_only.proto
@@ -3,21 +3,49 @@ option csharp_namespace = "RPG.Network.Proto";
enum RPGServiceType
{
- SERVICE_TYPE_NONE = 0;
- SERVICE_TYPE_SDK = 1;
- SERVICE_TYPE_GATESERVER = 2;
- SERVICE_TYPE_GAMESERVER = 3;
+ RPG_SERVICE_TYPE_NONE = 0;
+ RPG_SERVICE_TYPE_SDK = 1;
+ RPG_SERVICE_TYPE_GATESERVER = 2;
+ RPG_SERVICE_TYPE_GAMESERVER = 3;
}
-message ActionMetadata
+enum ServiceCommandType
{
- RPGServiceType sender_type = 1;
- uint64 session_id = 2;
- uint32 player_uid = 3;
+ SERVICE_COMMAND_TYPE_NONE = 0;
+ SERVICE_COMMAND_TYPE_BIND_CONTAINER = 100;
+ SERVICE_COMMAND_TYPE_BIND_CONTAINER_RESULT = 101;
+ SERVICE_COMMAND_TYPE_UNBIND_CONTAINER = 102;
+ SERVICE_COMMAND_TYPE_FORWARD_GAME_MESSAGE = 103;
}
-message ForwardGameMessageNotify
+message CmdBindContainer
+{
+ uint64 session_id = 1;
+ uint32 player_uid = 2;
+}
+
+message CmdBindContainerResult
+{
+ uint32 retcode = 1;
+ uint64 session_id = 2;
+}
+
+message CmdUnbindContainer
+{
+ UnbindContainerReason reason = 1;
+ uint64 session_id = 2;
+
+ enum UnbindContainerReason
+ {
+ UNBIND_CONTAINER_REASON_NONE = 0;
+ UNBIND_CONTAINER_REASON_LOGOUT = 1;
+ UNBIND_CONTAINER_REASON_KICK = 2;
+ }
+}
+
+message CmdForwardGameMessage
{
uint32 cmd_type = 1;
bytes payload = 2;
+ uint64 session_id = 3;
}
\ No newline at end of file
diff --git a/RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs b/RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs
new file mode 100644
index 0000000..4cabbc7
--- /dev/null
+++ b/RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs
@@ -0,0 +1,30 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Network.Command;
+using RPG.Services.Core.Options;
+using RPG.Services.Core.Session;
+
+namespace RPG.Services.Core.Extensions;
+public static class HostApplicationBuilderExtensions
+{
+ public static HostApplicationBuilder SetupRPGService(this HostApplicationBuilder builder)
+ where TService : RPGServiceBase
+ where TCommandHandler : ServiceCommandHandler
+ {
+ IConfigurationSection serviceOptionsSection = builder.Configuration.GetRequiredSection("Service");
+ IConfigurationSection serviceNodesSection = builder.Configuration.GetRequiredSection("Nodes");
+
+ builder.Services.Configure(serviceOptionsSection)
+ .Configure(serviceNodesSection);
+
+ builder.Services.AddHostedService()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton();
+
+ return builder;
+ }
+}
diff --git a/RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs b/RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs
new file mode 100644
index 0000000..e2de492
--- /dev/null
+++ b/RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs
@@ -0,0 +1,14 @@
+using RPG.Network.Proto;
+
+namespace RPG.Services.Core.Network.Attributes;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class ServiceCommandAttribute : Attribute
+{
+ public ServiceCommandType CommandType { get; }
+
+ public ServiceCommandAttribute(ServiceCommandType commandType)
+ {
+ CommandType = commandType;
+ }
+}
diff --git a/RPG.Services.Core/Network/Command/ServiceCommand.cs b/RPG.Services.Core/Network/Command/ServiceCommand.cs
new file mode 100644
index 0000000..846fb25
--- /dev/null
+++ b/RPG.Services.Core/Network/Command/ServiceCommand.cs
@@ -0,0 +1,16 @@
+using RPG.Network.Proto;
+
+namespace RPG.Services.Core.Network.Command;
+public class ServiceCommand
+{
+ public RPGServiceType SenderType { get; }
+ public ServiceCommandType CommandType { get; }
+ public ReadOnlyMemory Body { get; }
+
+ public ServiceCommand(RPGServiceType sender, ServiceCommandType commandType, ReadOnlyMemory body)
+ {
+ SenderType = sender;
+ CommandType = commandType;
+ Body = body;
+ }
+}
diff --git a/RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs b/RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs
new file mode 100644
index 0000000..d37141c
--- /dev/null
+++ b/RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs
@@ -0,0 +1,27 @@
+using System.Buffers.Binary;
+using RPG.Network.Proto;
+
+namespace RPG.Services.Core.Network.Command;
+public static class ServiceCommandEncoder
+{
+ public static ServiceCommand DecodeCommand(ReadOnlyMemory buffer)
+ {
+ ReadOnlySpan span = buffer.Span;
+
+ RPGServiceType senderType = (RPGServiceType)span[0];
+ ServiceCommandType commandType = (ServiceCommandType)BinaryPrimitives.ReadUInt16BigEndian(span[1..3]);
+ ReadOnlyMemory body = buffer.Slice(7, BinaryPrimitives.ReadInt32BigEndian(span[3..7]));
+
+ return new(senderType, commandType, body);
+ }
+
+ public static void EncodeCommand(ServiceCommand command, Memory buffer)
+ {
+ Span span = buffer.Span;
+
+ span[0] = (byte)command.SenderType;
+ BinaryPrimitives.WriteUInt16BigEndian(span[1..3], (ushort)command.CommandType);
+ BinaryPrimitives.WriteInt32BigEndian(span[3..7], command.Body.Length);
+ command.Body.CopyTo(buffer[7..]);
+ }
+}
diff --git a/RPG.Services.Core/Network/Command/ServiceCommandHandler.cs b/RPG.Services.Core/Network/Command/ServiceCommandHandler.cs
new file mode 100644
index 0000000..a84539f
--- /dev/null
+++ b/RPG.Services.Core/Network/Command/ServiceCommandHandler.cs
@@ -0,0 +1,66 @@
+using System.Collections.Immutable;
+using System.Linq.Expressions;
+using System.Reflection;
+using Google.Protobuf;
+using Microsoft.Extensions.Logging;
+using RPG.Network.Proto;
+using RPG.Services.Core.Network.Attributes;
+
+namespace RPG.Services.Core.Network.Command;
+public abstract class ServiceCommandHandler
+{
+ private delegate Task HandlerDelegate(ServiceCommand command);
+ private readonly ImmutableDictionary _handlers;
+
+ private readonly ILogger _logger;
+ private readonly ServiceBox _services;
+
+ public ServiceCommandHandler(ILogger logger, ServiceBox services)
+ {
+ _logger = logger;
+ _services = services;
+ _handlers = MapHandlers();
+ }
+
+ public async Task HandleAsync(ServiceCommand command)
+ {
+ if (_handlers.TryGetValue(command.CommandType, out HandlerDelegate? handler))
+ {
+ await handler(command);
+ }
+ else
+ {
+ _logger.LogWarning("Handler for service command of type {type} not found!", command.CommandType);
+ }
+ }
+
+ protected void Send(ServiceCommandType commandType, TBody body, RPGServiceType target) where TBody : IMessage
+ {
+ ServiceCommand command = new(_services.CurrentType, commandType, body.ToByteArray());
+
+ byte[] buffer = GC.AllocateUninitializedArray(command.Body.Length + 7);
+ ServiceCommandEncoder.EncodeCommand(command, buffer);
+
+ _services.SendToService(target, buffer);
+ }
+
+ private ImmutableDictionary MapHandlers()
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ IEnumerable methods = GetType().GetMethods().Where(m => m.GetCustomAttribute() != null);
+ foreach (MethodInfo method in methods)
+ {
+ ServiceCommandAttribute attribute = method.GetCustomAttribute()!;
+
+ Expression self = Expression.Convert(Expression.Constant(this), GetType());
+ ParameterExpression commandParameter = Expression.Parameter(typeof(ServiceCommand));
+ MethodCallExpression call = Expression.Call(self, method, commandParameter);
+
+ Expression lambda = Expression.Lambda(call, commandParameter);
+ builder.Add(attribute.CommandType, lambda.Compile());
+ }
+
+ return builder.ToImmutable();
+ }
+}
diff --git a/RPG.Services.Core/Network/ServiceBox.cs b/RPG.Services.Core/Network/ServiceBox.cs
new file mode 100644
index 0000000..3706c33
--- /dev/null
+++ b/RPG.Services.Core/Network/ServiceBox.cs
@@ -0,0 +1,52 @@
+using System.Collections.Immutable;
+using Microsoft.Extensions.Options;
+using NetMQ;
+using NetMQ.Sockets;
+using RPG.Network.Proto;
+using RPG.Services.Core.Options;
+
+namespace RPG.Services.Core.Network;
+public class ServiceBox
+{
+ private readonly IOptions _nodeOptions;
+ private readonly IOptions _serviceOptions;
+
+ private ImmutableDictionary? _sockets;
+
+ public ServiceBox(IOptions nodeOptions, IOptions serviceOptions)
+ {
+ _nodeOptions = nodeOptions;
+ _serviceOptions = serviceOptions;
+ }
+
+ public RPGServiceType CurrentType => _serviceOptions.Value.ServiceType;
+
+ public void Construct()
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (ServiceNodeOptions.Entry entry in _nodeOptions.Value)
+ {
+ if (entry.Type == CurrentType) continue;
+
+ NetMQSocket socket = new PushSocket($">tcp://{entry.Host}:{entry.Port}");
+ socket.Options.SendHighWatermark = 10000;
+ builder.Add(entry.Type, socket);
+ }
+
+ _sockets = builder.ToImmutable();
+ }
+
+ public void SendToService(RPGServiceType serviceType, byte[] data)
+ {
+ if (_sockets == null) throw new InvalidOperationException("SendToService called when socket map not constructed!");
+
+ if (_sockets.TryGetValue(serviceType, out NetMQSocket? socket))
+ {
+ lock (socket)
+ {
+ socket.SendFrame(data);
+ }
+ }
+ }
+}
diff --git a/RPG.Services.Core/Network/ServiceEndPoint.cs b/RPG.Services.Core/Network/ServiceEndPoint.cs
new file mode 100644
index 0000000..52d9ed7
--- /dev/null
+++ b/RPG.Services.Core/Network/ServiceEndPoint.cs
@@ -0,0 +1,58 @@
+using NetMQ;
+using NetMQ.Sockets;
+using RPG.Services.Core.Network.Command;
+using RPG.Services.Core.Options;
+
+namespace RPG.Services.Core.Network;
+internal class ServiceEndPoint
+{
+ private readonly NetMQSocket _socket;
+
+ private CancellationTokenSource? _receiveCancellation;
+ private Task? _receiveTask;
+
+ public delegate Task CommandEventHandler(ServiceCommand command);
+ public event CommandEventHandler? OnCommand;
+
+ public ServiceEndPoint(ServiceNodeOptions.Entry optionsEntry)
+ {
+ _socket = new PullSocket($"@tcp://{optionsEntry.Host}:{optionsEntry.Port}");
+ _socket.Options.ReceiveHighWatermark = 10000;
+ }
+
+ public void Start()
+ {
+ _receiveCancellation = new();
+ _receiveTask = Task.Run(() => Receive(_receiveCancellation.Token));
+ }
+
+ private async Task Receive(CancellationToken cancellationToken)
+ {
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ NetMQMessage netMessage = _socket.ReceiveMultipartMessage();
+ while (!netMessage.IsEmpty)
+ {
+ byte[] buffer = netMessage.Pop().Buffer;
+ if (OnCommand != null)
+ await OnCommand(ServiceCommandEncoder.DecodeCommand(buffer));
+ }
+ }
+ }
+ catch (Exception exception) when (exception is not OperationCanceledException)
+ {
+ throw;
+ }
+ }
+
+ public async Task StopAsync()
+ {
+ if (_receiveCancellation != null && _receiveTask != null)
+ {
+ await _receiveCancellation.CancelAsync();
+ await _receiveTask;
+ }
+ }
+}
diff --git a/RPG.Services.Core/Options/RPGServiceOptions.cs b/RPG.Services.Core/Options/RPGServiceOptions.cs
new file mode 100644
index 0000000..20fac86
--- /dev/null
+++ b/RPG.Services.Core/Options/RPGServiceOptions.cs
@@ -0,0 +1,7 @@
+using RPG.Network.Proto;
+
+namespace RPG.Services.Core.Options;
+public class RPGServiceOptions
+{
+ public required RPGServiceType ServiceType { get; set; }
+}
diff --git a/RPG.Services.Core/Options/ServiceNodeOptions.cs b/RPG.Services.Core/Options/ServiceNodeOptions.cs
new file mode 100644
index 0000000..246d5ab
--- /dev/null
+++ b/RPG.Services.Core/Options/ServiceNodeOptions.cs
@@ -0,0 +1,17 @@
+using RPG.Network.Proto;
+
+namespace RPG.Services.Core.Options;
+public class ServiceNodeOptions : List
+{
+ public class Entry
+ {
+ public required RPGServiceType Type { get; set; }
+ public required string Host { get; set; }
+ public required int Port { get; set; }
+ }
+
+ public Entry GetEntry(RPGServiceType type)
+ {
+ return Find(e => e.Type == type) ?? throw new ArgumentException("Entry not found", nameof(type));
+ }
+}
diff --git a/RPG.Services.Core/RPG.Services.Core.csproj b/RPG.Services.Core/RPG.Services.Core.csproj
index 6b584d4..7ee551b 100644
--- a/RPG.Services.Core/RPG.Services.Core.csproj
+++ b/RPG.Services.Core/RPG.Services.Core.csproj
@@ -8,6 +8,7 @@
+
diff --git a/RPG.Services.Core/RPGServiceBase.cs b/RPG.Services.Core/RPGServiceBase.cs
index 6a29dd4..778e65d 100644
--- a/RPG.Services.Core/RPGServiceBase.cs
+++ b/RPG.Services.Core/RPGServiceBase.cs
@@ -3,13 +3,22 @@
namespace RPG.Services.Core;
public abstract class RPGServiceBase : IHostedService
{
- public virtual Task StartAsync(CancellationToken cancellationToken)
+ private readonly ServiceManager _serviceManager;
+
+ public RPGServiceBase(ServiceManager serviceManager)
{
- throw new NotImplementedException();
+ _serviceManager = serviceManager;
}
- public virtual Task StopAsync(CancellationToken cancellationToken)
+ public virtual Task StartAsync(CancellationToken cancellationToken)
{
- throw new NotImplementedException();
+ _serviceManager.Start();
+
+ return Task.CompletedTask;
+ }
+
+ public virtual async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await _serviceManager.ShutdownAsync();
}
}
diff --git a/RPG.Services.Core/ServiceManager.cs b/RPG.Services.Core/ServiceManager.cs
new file mode 100644
index 0000000..f717647
--- /dev/null
+++ b/RPG.Services.Core/ServiceManager.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.Options;
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Network.Command;
+using RPG.Services.Core.Options;
+
+namespace RPG.Services.Core;
+public class ServiceManager
+{
+ private readonly IOptions _nodeOptions;
+ private readonly IOptions _serviceOptions;
+ private readonly ServiceCommandHandler _handler;
+ private readonly ServiceBox _serviceBox;
+
+ private ServiceEndPoint? _serviceEndPoint;
+
+ public ServiceManager(IOptions options, IOptions serviceOptions, ServiceCommandHandler commandHandler, ServiceBox serviceBox)
+ {
+ _nodeOptions = options;
+ _serviceOptions = serviceOptions;
+ _handler = commandHandler;
+ _serviceBox = serviceBox;
+ }
+
+ public void Start()
+ {
+ ServiceNodeOptions nodeOptions = _nodeOptions.Value;
+ RPGServiceOptions serviceOptions = _serviceOptions.Value;
+
+ _serviceBox.Construct();
+
+ _serviceEndPoint = new(nodeOptions.GetEntry(serviceOptions.ServiceType));
+ _serviceEndPoint.OnCommand += _handler.HandleAsync;
+ _serviceEndPoint.Start();
+ }
+
+ public async Task ShutdownAsync()
+ {
+ if (_serviceEndPoint != null)
+ {
+ await _serviceEndPoint.StopAsync();
+ _serviceEndPoint.OnCommand -= _handler.HandleAsync;
+ }
+ }
+}
diff --git a/RPG.Services.Core/Session/RPGSession.cs b/RPG.Services.Core/Session/RPGSession.cs
new file mode 100644
index 0000000..5a608e9
--- /dev/null
+++ b/RPG.Services.Core/Session/RPGSession.cs
@@ -0,0 +1,47 @@
+using Google.Protobuf;
+using RPG.Network.Proto;
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Network.Command;
+
+namespace RPG.Services.Core.Session;
+public abstract class RPGSession
+{
+ private readonly ServiceBox _serviceBox;
+
+ public ulong SessionId { get; }
+ public uint PlayerUid { get; set; }
+
+ public RPGSession(ulong sessionId, ServiceBox serviceBox)
+ {
+ SessionId = sessionId;
+ _serviceBox = serviceBox;
+ }
+
+ public void SendToService(RPGServiceType target, ServiceCommandType commandType, TBody body) where TBody : IMessage
+ {
+ ServiceCommand command = new(_serviceBox.CurrentType, commandType, body.ToByteArray());
+
+ byte[] commandBuffer = GC.AllocateUninitializedArray(7 + command.Body.Length);
+ ServiceCommandEncoder.EncodeCommand(command, commandBuffer);
+
+ _serviceBox.SendToService(target, commandBuffer);
+ }
+
+ public void BindService(RPGServiceType service)
+ {
+ SendToService(service, ServiceCommandType.BindContainer, new CmdBindContainer
+ {
+ SessionId = SessionId,
+ PlayerUid = PlayerUid
+ });
+ }
+
+ public void UnbindService(RPGServiceType service, CmdUnbindContainer.Types.UnbindContainerReason reason = CmdUnbindContainer.Types.UnbindContainerReason.Logout)
+ {
+ SendToService(service, ServiceCommandType.UnbindContainer, new CmdUnbindContainer
+ {
+ SessionId = SessionId,
+ Reason = reason
+ });
+ }
+}
diff --git a/RPG.Services.Core/Session/SessionManager.cs b/RPG.Services.Core/Session/SessionManager.cs
new file mode 100644
index 0000000..0182389
--- /dev/null
+++ b/RPG.Services.Core/Session/SessionManager.cs
@@ -0,0 +1,43 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace RPG.Services.Core.Session;
+public class SessionManager
+{
+ private readonly ConcurrentDictionary _sessions;
+ private readonly IServiceProvider _serviceProvider;
+
+ public SessionManager(IServiceProvider serviceProvider)
+ {
+ _sessions = [];
+ _serviceProvider = serviceProvider;
+ }
+
+ public TSession? Create(ulong id) where TSession : RPGSession
+ {
+ if (_sessions.ContainsKey(id)) return null;
+
+ TSession session = ActivatorUtilities.CreateInstance(_serviceProvider, id);
+
+ _sessions[id] = session;
+ return session;
+ }
+
+ public bool TryGet(ulong id, [NotNullWhen(true)] out TSession? session) where TSession : RPGSession
+ {
+ if (_sessions.TryGetValue(id, out RPGSession? value))
+ {
+ session = (TSession)value;
+ return true;
+ }
+
+ session = null;
+ return false;
+ }
+
+ public void Remove(RPGSession session)
+ {
+ _ = _sessions.TryRemove(session.SessionId, out _);
+ }
+}
diff --git a/RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs b/RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs
new file mode 100644
index 0000000..da497d5
--- /dev/null
+++ b/RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs
@@ -0,0 +1,43 @@
+using Microsoft.Extensions.Logging;
+using RPG.Network.Proto;
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Network.Attributes;
+using RPG.Services.Core.Network.Command;
+using RPG.Services.Core.Session;
+using RPG.Services.Gameserver.Session;
+
+namespace RPG.Services.Gameserver.Network.Command;
+internal class GameserverCommandHandler : ServiceCommandHandler
+{
+ private readonly SessionManager _sessionManager;
+
+ public GameserverCommandHandler(ILogger logger, ServiceBox services, SessionManager sessionManager) : base(logger, services)
+ {
+ _sessionManager = sessionManager;
+ }
+
+ [ServiceCommand(ServiceCommandType.BindContainer)]
+ public Task OnCmdBindContainer(ServiceCommand command)
+ {
+ CmdBindContainer cmdBindContainer = CmdBindContainer.Parser.ParseFrom(command.Body.Span);
+ PlayerSession? session = _sessionManager.Create(cmdBindContainer.SessionId);
+ if (session == null)
+ {
+ Send(ServiceCommandType.BindContainerResult, new CmdBindContainerResult
+ {
+ Retcode = 1,
+ SessionId = cmdBindContainer.SessionId
+ }, command.SenderType);
+ return Task.CompletedTask;
+ }
+
+ session.PlayerUid = cmdBindContainer.PlayerUid;
+ Send(ServiceCommandType.BindContainerResult, new CmdBindContainerResult
+ {
+ Retcode = 0,
+ SessionId = cmdBindContainer.SessionId
+ }, command.SenderType);
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/RPG.Services.Gameserver/Program.cs b/RPG.Services.Gameserver/Program.cs
index 98f13a6..73192ad 100644
--- a/RPG.Services.Gameserver/Program.cs
+++ b/RPG.Services.Gameserver/Program.cs
@@ -1,9 +1,19 @@
-namespace RPG.Services.Gameserver;
+using Microsoft.Extensions.Hosting;
+using RPG.Services.Core.Extensions;
+using RPG.Services.Gameserver.Network.Command;
-internal class Program
+namespace RPG.Services.Gameserver;
+
+internal static class Program
{
- static void Main(string[] args)
+ private static async Task Main(string[] args)
{
- Console.WriteLine("Hello, World!");
+ Console.Title = "Snowflake | Gameserver";
+
+ HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+
+ builder.SetupRPGService();
+
+ await builder.Build().RunAsync();
}
}
diff --git a/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj b/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj
index 2150e37..39a3637 100644
--- a/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj
+++ b/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj
@@ -7,4 +7,14 @@
enable
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/RPG.Services.Gameserver/RPGGameserver.cs b/RPG.Services.Gameserver/RPGGameserver.cs
new file mode 100644
index 0000000..ccae1e9
--- /dev/null
+++ b/RPG.Services.Gameserver/RPGGameserver.cs
@@ -0,0 +1,10 @@
+using RPG.Services.Core;
+
+namespace RPG.Services.Gameserver;
+internal class RPGGameserver : RPGServiceBase
+{
+ public RPGGameserver(ServiceManager serviceManager) : base(serviceManager)
+ {
+ // RPGGameserver.
+ }
+}
diff --git a/RPG.Services.Gameserver/Session/PlayerSession.cs b/RPG.Services.Gameserver/Session/PlayerSession.cs
new file mode 100644
index 0000000..f54d9e8
--- /dev/null
+++ b/RPG.Services.Gameserver/Session/PlayerSession.cs
@@ -0,0 +1,10 @@
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Session;
+
+namespace RPG.Services.Gameserver.Session;
+internal class PlayerSession : RPGSession
+{
+ public PlayerSession(ulong sessionId, ServiceBox serviceBox) : base(sessionId, serviceBox)
+ {
+ }
+}
diff --git a/RPG.Services.Gameserver/appsettings.json b/RPG.Services.Gameserver/appsettings.json
new file mode 100644
index 0000000..33622e7
--- /dev/null
+++ b/RPG.Services.Gameserver/appsettings.json
@@ -0,0 +1,17 @@
+{
+ "Service": {
+ "ServiceType": "Gameserver"
+ },
+ "Nodes": [
+ {
+ "Type": "Gateserver",
+ "Host": "127.0.0.1",
+ "Port": "21051"
+ },
+ {
+ "Type": "Gameserver",
+ "Host": "127.0.0.1",
+ "Port": "21081"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs b/RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs
new file mode 100644
index 0000000..b7fd030
--- /dev/null
+++ b/RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.Logging;
+using RPG.Network.Proto;
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Network.Attributes;
+using RPG.Services.Core.Network.Command;
+using RPG.Services.Core.Session;
+using RPG.Services.Gateserver.Session;
+
+namespace RPG.Services.Gateserver.Network.Command;
+internal class GateserverCommandHandler : ServiceCommandHandler
+{
+ private readonly SessionManager _sessionManager;
+
+ public GateserverCommandHandler(ILogger logger, ServiceBox services, SessionManager sessionManager) : base(logger, services)
+ {
+ _sessionManager = sessionManager;
+ }
+
+ [ServiceCommand(ServiceCommandType.BindContainerResult)]
+ public async Task OnBindContainerResult(ServiceCommand command)
+ {
+ CmdBindContainerResult result = CmdBindContainerResult.Parser.ParseFrom(command.Body.Span);
+
+ if (_sessionManager.TryGet(result.SessionId, out NetworkSession? session))
+ {
+ PlayerGetTokenScRsp rsp;
+ if (result.Retcode != 0)
+ {
+ rsp = new() { Retcode = 1 };
+ }
+ else
+ {
+ rsp = new()
+ {
+ Retcode = 0,
+ Msg = "OK",
+ Uid = session.PlayerUid
+ };
+ }
+
+ await session.SendAsync(CmdType.CmdPlayerGetTokenScRsp, rsp);
+ }
+ }
+}
diff --git a/RPG.Services.Gateserver/Network/NetPacket.cs b/RPG.Services.Gateserver/Network/NetPacket.cs
new file mode 100644
index 0000000..f88b77f
--- /dev/null
+++ b/RPG.Services.Gateserver/Network/NetPacket.cs
@@ -0,0 +1,68 @@
+using System.Buffers.Binary;
+
+namespace RPG.Services.Gateserver.Network;
+internal class NetPacket
+{
+ public const int Overhead = 16;
+
+ private const uint HeadMagic = 0x01234567;
+ private const uint TailMagic = 0x89ABCDEF;
+
+ public ushort CmdType { get; }
+ public ReadOnlyMemory Head { get; }
+ public ReadOnlyMemory Body { get; }
+
+ public int Size => Overhead + Head.Length + Body.Length;
+
+ public NetPacket(ushort cmdType, ReadOnlyMemory head, ReadOnlyMemory body)
+ {
+ CmdType = cmdType;
+ Head = head;
+ Body = body;
+ }
+
+ public static DeserializationResult TryDeserialize(ReadOnlyMemory buffer, out NetPacket? packet, out int bytesRead)
+ {
+ packet = null;
+ bytesRead = 0;
+ if (buffer.Length < Overhead) return DeserializationResult.BufferExceeded;
+
+ ReadOnlySpan span = buffer.Span;
+
+ if (BinaryPrimitives.ReadUInt32BigEndian(span[..4]) != HeadMagic) return DeserializationResult.Corrupted;
+
+ ushort cmdType = BinaryPrimitives.ReadUInt16BigEndian(span[4..6]);
+ int headSize = BinaryPrimitives.ReadUInt16BigEndian(span[6..8]);
+ int bodySize = BinaryPrimitives.ReadInt32BigEndian(span[8..12]);
+
+ if (buffer.Length < Overhead + headSize + bodySize) return DeserializationResult.BufferExceeded;
+ if (BinaryPrimitives.ReadUInt32BigEndian(span[(12 + headSize + bodySize)..]) != TailMagic) return DeserializationResult.Corrupted;
+
+ packet = new(cmdType, buffer.Slice(12, headSize), buffer.Slice(12 + headSize, bodySize));
+ bytesRead = Overhead + headSize + bodySize;
+
+ return DeserializationResult.Success;
+ }
+
+ public void Serialize(Memory buffer)
+ {
+ Span span = buffer.Span;
+
+ BinaryPrimitives.WriteUInt32BigEndian(span[..4], HeadMagic);
+ BinaryPrimitives.WriteUInt16BigEndian(span[4..6], CmdType);
+ BinaryPrimitives.WriteUInt16BigEndian(span[6..8], (ushort)Head.Length);
+ BinaryPrimitives.WriteInt32BigEndian(span[8..12], Body.Length);
+
+ Head.CopyTo(buffer[12..]);
+ Body.CopyTo(buffer[(12 + Head.Length)..]);
+
+ BinaryPrimitives.WriteUInt32BigEndian(span[(12 + Head.Length + Body.Length)..], TailMagic);
+ }
+
+ public enum DeserializationResult
+ {
+ Success,
+ BufferExceeded,
+ Corrupted
+ }
+}
diff --git a/RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs b/RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs
new file mode 100644
index 0000000..1cd99da
--- /dev/null
+++ b/RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs
@@ -0,0 +1,87 @@
+using System.Net;
+using System.Net.Sockets;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using RPG.Services.Core.Session;
+using RPG.Services.Gateserver.Options;
+using RPG.Services.Gateserver.Session;
+
+namespace RPG.Services.Gateserver.Network.Tcp;
+internal class TcpGateway
+{
+ private readonly IOptions _options;
+ private readonly SessionManager _sessionManager;
+ private readonly ILogger _logger;
+
+ private Socket? _socket;
+
+ private CancellationTokenSource? _acceptCancellation;
+ private Task? _acceptTask;
+
+ private ulong _sessionIdCounter;
+
+ public TcpGateway(IOptions options, SessionManager sessionManager, ILogger logger)
+ {
+ _options = options;
+ _sessionManager = sessionManager;
+ _logger = logger;
+ }
+
+ public void Start()
+ {
+ IPEndPoint bindEndPoint = _options.Value.BindEndPoint;
+
+ _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ _socket.Bind(bindEndPoint);
+ _socket.Listen(100);
+
+ _acceptCancellation = new();
+ _acceptTask = RunAcceptLoopAsync(_acceptCancellation.Token);
+
+ _logger.LogInformation("Listening at tcp://{endPoint}", bindEndPoint);
+ }
+
+ public async Task StopAsync()
+ {
+ if (_acceptCancellation != null && _acceptTask != null)
+ {
+ await _acceptCancellation.CancelAsync();
+ await _acceptTask;
+ }
+
+ _socket?.Close();
+ }
+
+ private async Task RunAcceptLoopAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ Socket clientSocket = await _socket!.AcceptAsync(cancellationToken);
+ _ = RunSessionAsync(clientSocket);
+ }
+ }
+ catch (Exception exception) when (exception is not OperationCanceledException)
+ {
+ throw;
+ }
+ }
+
+ private async Task RunSessionAsync(Socket socket)
+ {
+ try
+ {
+ NetworkSession? session = _sessionManager.Create(Interlocked.Increment(ref _sessionIdCounter));
+ if (session == null) return;
+
+ session.Socket = socket;
+
+ await session.RunAsync();
+ }
+ catch (Exception exception) when (exception is not OperationCanceledException)
+ {
+ _logger.LogError("Unhandled exception occurred: {exception}", exception);
+ }
+ }
+}
diff --git a/RPG.Services.Gateserver/Options/GatewayOptions.cs b/RPG.Services.Gateserver/Options/GatewayOptions.cs
new file mode 100644
index 0000000..d52c435
--- /dev/null
+++ b/RPG.Services.Gateserver/Options/GatewayOptions.cs
@@ -0,0 +1,10 @@
+using System.Net;
+
+namespace RPG.Services.Gateserver.Options;
+internal class GatewayOptions
+{
+ public required string Host { get; set; }
+ public required int Port { get; set; }
+
+ public IPEndPoint BindEndPoint => new(IPAddress.Parse(Host), Port);
+}
diff --git a/RPG.Services.Gateserver/Program.cs b/RPG.Services.Gateserver/Program.cs
index 00230ad..ac5e29e 100644
--- a/RPG.Services.Gateserver/Program.cs
+++ b/RPG.Services.Gateserver/Program.cs
@@ -1,9 +1,29 @@
-namespace RPG.Services.Gateserver;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using RPG.Services.Core.Extensions;
+using RPG.Services.Gateserver.Network.Command;
+using RPG.Services.Gateserver.Network.Tcp;
+using RPG.Services.Gateserver.Options;
-internal class Program
+namespace RPG.Services.Gateserver;
+
+internal static class Program
{
- static void Main(string[] args)
+ private const string GatewayOptionsSectionName = "Gateway";
+
+ private static async Task Main(string[] args)
{
- Console.WriteLine("Hello, World!");
+ Console.Title = "Snowflake | Gateserver";
+
+ HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+ builder.SetupRPGService();
+
+ IConfigurationSection gatewaySection = builder.Configuration.GetRequiredSection(GatewayOptionsSectionName);
+
+ builder.Services.Configure(gatewaySection)
+ .AddSingleton();
+
+ await builder.Build().RunAsync();
}
}
diff --git a/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj b/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj
index 2150e37..39a3637 100644
--- a/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj
+++ b/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj
@@ -7,4 +7,14 @@
enable
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/RPG.Services.Gateserver/RPGGateserver.cs b/RPG.Services.Gateserver/RPGGateserver.cs
new file mode 100644
index 0000000..d6ad11e
--- /dev/null
+++ b/RPG.Services.Gateserver/RPGGateserver.cs
@@ -0,0 +1,25 @@
+using RPG.Services.Core;
+using RPG.Services.Gateserver.Network.Tcp;
+
+namespace RPG.Services.Gateserver;
+internal class RPGGateserver : RPGServiceBase
+{
+ private readonly TcpGateway _gateway;
+
+ public RPGGateserver(ServiceManager serviceManager, TcpGateway tcpGateway) : base(serviceManager)
+ {
+ _gateway = tcpGateway;
+ }
+
+ public override async Task StartAsync(CancellationToken cancellationToken)
+ {
+ await base.StartAsync(cancellationToken);
+ _gateway.Start();
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await base.StopAsync(cancellationToken);
+ await _gateway.StopAsync();
+ }
+}
diff --git a/RPG.Services.Gateserver/Session/NetworkSession.cs b/RPG.Services.Gateserver/Session/NetworkSession.cs
new file mode 100644
index 0000000..cca6745
--- /dev/null
+++ b/RPG.Services.Gateserver/Session/NetworkSession.cs
@@ -0,0 +1,105 @@
+using System.Net.Sockets;
+using Google.Protobuf;
+using RPG.Network.Proto;
+using RPG.Services.Core.Network;
+using RPG.Services.Core.Network.Command;
+using RPG.Services.Core.Session;
+using RPG.Services.Gateserver.Network;
+
+namespace RPG.Services.Gateserver.Session;
+internal class NetworkSession : RPGSession
+{
+ private const int ReadTimeoutMs = 30000;
+ private const int ReceiveBufferSize = 16384;
+
+ private readonly byte[] _recvBuffer;
+
+ public Socket? Socket { private get; set; }
+ public PlayerGetTokenCsReq? GetTokenCsReq { get; private set; }
+
+ public NetworkSession(ulong sessionId, ServiceBox serviceBox) : base(sessionId, serviceBox)
+ {
+ _recvBuffer = GC.AllocateUninitializedArray(ReceiveBufferSize);
+ }
+
+ public async Task RunAsync()
+ {
+ if (Socket == null) throw new InvalidOperationException("RunAsync called but socket was not set!");
+
+ int recvBufferIdx = 0;
+ Memory recvBufferMem = _recvBuffer.AsMemory();
+
+ while (true)
+ {
+ int readAmount = await ReadWithTimeoutAsync(Socket, recvBufferMem[recvBufferIdx..], ReadTimeoutMs);
+ if (readAmount == 0) break;
+
+ recvBufferIdx += readAmount;
+
+ do
+ {
+ NetPacket.DeserializationResult result = NetPacket.TryDeserialize(recvBufferMem[..recvBufferIdx], out NetPacket? packet, out int bytesRead);
+ if (result == NetPacket.DeserializationResult.BufferExceeded) break;
+ if (result == NetPacket.DeserializationResult.Corrupted) return;
+
+ HandleSessionPacketAsync(packet!);
+ Buffer.BlockCopy(_recvBuffer, recvBufferIdx, _recvBuffer, 0, recvBufferIdx -= bytesRead);
+ }
+ while (recvBufferIdx >= NetPacket.Overhead);
+ }
+ }
+
+ public async Task SendAsync(ushort cmdType, TBody body) where TBody : IMessage
+ {
+ await SendAsync(new(cmdType, ReadOnlyMemory.Empty, body.ToByteArray()));
+ }
+
+ public async Task SendAsync(NetPacket packet)
+ {
+ if (Socket == null) return;
+
+ byte[] buffer = GC.AllocateUninitializedArray(packet.Size);
+ packet.Serialize(buffer);
+
+ await Socket!.SendAsync(buffer);
+ }
+
+ private void HandleSessionPacketAsync(NetPacket packet)
+ {
+ switch (packet.CmdType)
+ {
+ case CmdType.CmdPlayerGetTokenCsReq:
+ HandlePlayerGetTokenCsReq(PlayerGetTokenCsReq.Parser.ParseFrom(packet.Body.Span));
+ break;
+ case CmdType.CmdPlayerKeepAliveNotify:
+ break;
+ default:
+ ForwardToGameserver(packet);
+ break;
+ }
+ }
+
+ private void HandlePlayerGetTokenCsReq(PlayerGetTokenCsReq req)
+ {
+ GetTokenCsReq = req;
+ PlayerUid = uint.Parse(req.AccountUid);
+
+ BindService(RPGServiceType.Gameserver);
+ }
+
+ private void ForwardToGameserver(NetPacket packet)
+ {
+ SendToService(RPGServiceType.Gameserver, ServiceCommandType.ForwardGameMessage, new CmdForwardGameMessage
+ {
+ SessionId = SessionId,
+ CmdType = packet.CmdType,
+ Payload = ByteString.CopyFrom(packet.Body.Span)
+ });
+ }
+
+ private static async ValueTask ReadWithTimeoutAsync(Socket socket, Memory buffer, int timeoutMs)
+ {
+ CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(timeoutMs));
+ return await socket.ReceiveAsync(buffer, cts.Token);
+ }
+}
diff --git a/RPG.Services.Gateserver/appsettings.json b/RPG.Services.Gateserver/appsettings.json
new file mode 100644
index 0000000..dbbf968
--- /dev/null
+++ b/RPG.Services.Gateserver/appsettings.json
@@ -0,0 +1,21 @@
+{
+ "Service": {
+ "ServiceType": "Gateserver"
+ },
+ "Nodes": [
+ {
+ "Type": "Gateserver",
+ "Host": "127.0.0.1",
+ "Port": "21051"
+ },
+ {
+ "Type": "Gameserver",
+ "Host": "127.0.0.1",
+ "Port": "21081"
+ }
+ ],
+ "Gateway": {
+ "Host": "0.0.0.0",
+ "Port": 20301
+ }
+}
\ No newline at end of file
diff --git a/Snowflake.sln b/Snowflake.sln
index 5b3e1f1..f90ed67 100644
--- a/Snowflake.sln
+++ b/Snowflake.sln
@@ -19,6 +19,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Gameserver", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Core", "RPG.Services.Core\RPG.Services.Core.csproj", "{1B434662-DEC9-40C9-A709-CE87026191D9}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D96091AB-B78F-4092-ADEF-7A4D9F1B90C6}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU