水电费

This commit is contained in:
bob
2025-07-16 17:47:53 +08:00
parent 7cccd2a81c
commit 3bd1ffbb75
75 changed files with 2148 additions and 1265 deletions

View File

@@ -2,12 +2,14 @@ using Fantasy.Entitas;
using Fantasy.Entitas.Interface;
namespace Fantasy;
// 这个是一个自定义系统类型,用于决定系统类型。
// 也可以用枚举,看个人怎么使用了。
public static class CustomSystemType
{
public const int RunSystem = 1;
}
// 这个是一个自定义系统,用于处理自定义事件。
public abstract class RunSystem<T> : CustomSystem<T> where T : Entity
{
@@ -15,23 +17,26 @@ public abstract class RunSystem<T> : CustomSystem<T> where T : Entity
/// 自定义事件类型,用于决定事件的类型。
/// </summary>
public override int CustomEventType => CustomSystemType.RunSystem;
/// <summary>
/// 不知道为什么这样定义的,就照搬就可以了。
/// </summary>
/// <param name="self"></param>
protected abstract override void Custom(T self);
/// <summary>
/// 不知道为什么这样定义的,就照搬就可以了。
/// </summary>
/// <returns></returns>
public override Type EntitiesType() => typeof(T);
}
// 下面是一个测试自定义系统。
// 首先定义一个组件用来测试自定义系统。
public class TestCustomSystemComponent : Entity
{
}
// 现在给TestCustomSystemComponent组件添加一个自定义系统。
// 现在添加的就是上面定义的RunSystem自定义系统。
public class TestCustomSystemComponentRunSystem : RunSystem<TestCustomSystemComponent>
@@ -47,4 +52,4 @@ public class TestCustomSystemComponentRunSystem : RunSystem<TestCustomSystemComp
// 第一个参数是你要执行自定义系统的实体实例
// 第二个参数是自定义系统类型
// scene.EntityComponent.CustomSystem(testCustomSystemComponent, CustomSystemType.RunSystem);
// 你可以在OnCreateSceneEvent.cs的Handler方法里找到执行这个的例子。
// 你可以在OnCreateSceneEvent.cs的Handler方法里找到执行这个的例子。

47
Hotfix/EntityHelper.cs Normal file
View File

@@ -0,0 +1,47 @@
using System.Authentication;
using Fantasy;
using Fantasy.Async;
using Fantasy.Entitas;
using Fantasy.Network;
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
namespace System;
public static class EntityHelper
{
public static bool CheckInterval(this Entity entity, int interval)
{
var sessionTimeOutComponent = entity.GetComponent<EntityTimeOutComponent>();
if (sessionTimeOutComponent == null)
{
sessionTimeOutComponent = entity.AddComponent<EntityTimeOutComponent>();
sessionTimeOutComponent.SetInterval(interval);
return true;
}
return sessionTimeOutComponent.CheckInterval();
}
public static void SetTimeout(this Entity entity, int timeout = 3000, Func<FTask>? task = null)
{
var sessionTimeOutComponent = entity.GetComponent<EntityTimeOutComponent>();
if (sessionTimeOutComponent == null)
{
sessionTimeOutComponent = entity.AddComponent<EntityTimeOutComponent>();
}
sessionTimeOutComponent.TimeOut(timeout, task);
}
public static bool IsTimeOutComponent(this Entity entity)
{
return entity.GetComponent<EntityTimeOutComponent>() != null;
}
public static void CancelTimeout(this Entity entity)
{
entity.RemoveComponent<EntityTimeOutComponent>();
}
}

View File

@@ -0,0 +1,78 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Entitas.Interface;
using Fantasy.Helper;
namespace System.Authentication;
public sealed class EntityTimeOutComponentDestroySystem : DestroySystem<EntityTimeOutComponent>
{
protected override void Destroy(EntityTimeOutComponent self)
{
if (self.TimerId != 0)
{
self.Scene.TimerComponent.Net.Remove(ref self.TimerId);
}
self.NextTime = 0;
self.Interval = 0;
}
}
public static class EntityTimeOutComponentSystem
{
public static void SetInterval(this EntityTimeOutComponent self, int interval)
{
if (interval <= 0)
{
throw new ArgumentException("interval must be greater than 0", nameof(interval));
}
self.Interval = interval;
self.NextTime = TimeHelper.Now + interval;
}
public static bool CheckInterval(this EntityTimeOutComponent self)
{
if (self.NextTime > TimeHelper.Now)
{
Log.Warning("当前连接请求的间隔过小");
return false;
}
self.NextTime = TimeHelper.Now + self.Interval;
return true;
}
public static void TimeOut(this EntityTimeOutComponent self, int timeout, Func<FTask>? task = null)
{
var scene = self.Scene;
var parentRunTimeId = self.Parent.RuntimeId;
if (self.TimerId != 0)
{
self.Scene.TimerComponent.Net.Remove(ref self.TimerId);
}
self.TimerId =
scene.TimerComponent.Net.OnceTimer(timeout, () => { self.Handler(parentRunTimeId, task).Coroutine(); });
}
private static async FTask Handler(this EntityTimeOutComponent self, long parentRunTimeId, Func<FTask>? task = null)
{
var selfParent = self.Parent;
if (selfParent == null || parentRunTimeId != selfParent.RuntimeId)
{
return;
}
if (task != null)
{
await task();
}
self.TimerId = 0;
selfParent.Dispose();
}
}

View File

@@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Outer\Demo\" />
<Folder Include="Outer\Gate\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
using System.Authentication;
using Fantasy;
using Fantasy.Async;
using Fantasy.Authentication;
using Fantasy.Event;
using Fantasy.Gate;
namespace System;
public class OnSceneCreate_Init : AsyncEventSystem<OnCreateScene>
{
protected override async FTask Handler(OnCreateScene self)
{
var scene = self.Scene;
switch (scene.SceneType)
{
case SceneType.Authentication:
{
// 用于鉴权服务器注册和登录相关逻辑的组件
scene.AddComponent<AuthenticationComponent>().UpdatePosition();
// 用于颁发ToKen证书相关的逻辑。
scene.AddComponent<AuthenticationJwtComponent>();
break;
}
case SceneType.Gate:
{
// 用于验证JWT是否合法的组件
scene.AddComponent<GateJWTComponent>();
// 用于管理GameAccount的组件
scene.AddComponent<GameAccountManageComponent>();
break;
}
}
await FTask.CompletedTask;
}
}

View File

@@ -0,0 +1,38 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Authentication.Jwt;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Platform.Net;
namespace Fantasy.Authentication.Handler;
public class C2A_LoginRequestHandler : MessageRPC<C2A_LoginRequest, A2C_LoginResponse>
{
protected override async FTask Run(Session session, C2A_LoginRequest request, A2C_LoginResponse response,
Action reply)
{
var scene = session.Scene;
var result = await AuthenticationHelper.Login(scene, request.Username, request.Password);
if (result.ErrorCode == 0)
{
// 通过配置表或其他方式拿到Gate服务器组的信息
var gates = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Gate);
// 通过当前账号的ID拿到要分配Gate服务器
var gatePosition = result.AccountId % gates.Count;
// 通过计算出来的位置下标拿到Gate服务器的配置
var gateSceneConfig = gates[(int)gatePosition];
// 通过Gate的SceneConfig文件拿到外网的ID地址和端口
var outerPort = gateSceneConfig.OuterPort;
var processConfig = ProcessConfigData.Instance.Get(gateSceneConfig.ProcessConfigId);
var machineConfig = MachineConfigData.Instance.Get(processConfig.MachineId);
// 颁发一个ToKen令牌给客户端
response.ToKen = AuthenticationJwtHelper.GetToken(scene, result.AccountId,
$"{machineConfig.OuterIP}:{outerPort}", gateSceneConfig.Id);
}
response.ErrorCode = result.ErrorCode;
Log.Debug($"Login 当前的服务器是:{scene.SceneConfigId}");
}
}

View File

@@ -0,0 +1,25 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
namespace Fantasy.Authentication.Handler;
public sealed class C2A_RegisterRequestHandler : MessageRPC<C2A_RegisterRequest, A2C_RegisterResponse>
{
protected override async FTask Run(Session session, C2A_RegisterRequest request, A2C_RegisterResponse response,
Action reply)
{
if (!session.CheckInterval(2000))
{
// 返回这个3代表操作过于频繁。
response.ErrorCode = 3;
return;
}
session.SetTimeout(3000);
response.ErrorCode =
await AuthenticationHelper.Register(session.Scene, request.Username, request.Password, "用户注册");
Log.Debug($"Register 当前的服务器是:{session.Scene.SceneConfigId}");
}
}

View File

@@ -0,0 +1,15 @@
using Fantasy;
using Fantasy.Entitas.Interface;
namespace Fantasy.Authentication;
public class AccountDestroySystem : DestroySystem<Account>
{
protected override void Destroy(Account self)
{
self.Username = null;
self.Password = null;
self.CreateTime = 0;
self.LoginTime = 0;
}
}

View File

@@ -0,0 +1,239 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Entitas;
using Fantasy.Entitas.Interface;
using Fantasy.Helper;
using Fantasy.Platform.Net;
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
#pragma warning disable CS8602 // Dereference of a possibly null reference.
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
namespace Fantasy.Authentication;
public sealed class AuthenticationComponentDestroySystem : DestroySystem<AuthenticationComponent>
{
protected override void Destroy(AuthenticationComponent self)
{
foreach (var (_, account) in self.Accounts.ToArray())
{
account.Dispose();
}
self.Accounts.Clear();
}
}
internal static class AuthenticationComponentSystem
{
public static void UpdatePosition(this AuthenticationComponent self)
{
// 1、通过远程接口或者本地文件来拿到鉴权组
// 2、通过配置文件来拿
var authentications = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Authentication);
// 拿到当前Scene的配置文件
var sceneConfig = SceneConfigData.Instance.Get(self.Scene.SceneConfigId);
// 获取到当前Scene在鉴权组的位置
self.Position = authentications.IndexOf(sceneConfig);
// 获得鉴权组的总数
self.AuthenticationCount = authentications.Count;
Log.Info($"鉴权服务器启动成功Position:{self.Position} AuthenticationCount:{self.AuthenticationCount}");
}
internal static async FTask<(uint ErrorCode, long AccountId)> Login(this AuthenticationComponent self, string userName, string password)
{
// 1、检查传递的参数是否完整
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
{
// 这个1代表的是参数不完整。
return (1, 0);
}
// 检查账号是否应该在当前鉴权服务器中处理
var position = HashCodeHelper.MurmurHash3(userName) % self.AuthenticationCount;
if (self.Position != position)
{
// 这个3代表的是当前账号不应该在这个鉴权服务器处理。
return (3, 0);
}
var scene = self.Scene;
var worldDateBase = scene.World.DataBase;//DateBase
var usernameHashCode = userName.GetHashCode();
using (var @lock = await scene.CoroutineLockComponent.Wait((int)LockType.AuthenticationLoginLock, usernameHashCode))
{
// 如果用户频繁发生登录的请求,导致服务器会频繁请求数据库或缓存。
// 针对这个问题咱们可以利用缓存来解决这个问题。
// 1、创建一个新的字典容器在AuthenticationComponent中存储登录的信息。
// 2、key:userName + password, Value = ?
// 3、为了防止缓存暴涨、肯定需要一个定期清理的过程Value肯定是要一个实体了。
// 4、因为这个实体下面咱们可以挂载一个组件这个组件的作用就是定时清理这个缓存。
// 问题
// 1、如果用户的密码改了怎么办?
// 因为缓存中有定时清除的,所以遇到改密码的情况下,最多等待这个缓存清除了,然后就可以登录了。
// 2、如果我不这样做还有什么其他办法
// 通过防火墙的策略来限制用户请求比如100ms请求一次。
// 作业:
// 在这个AccountCacheInfo下创建一个组件这个组件的功能就是定时清理这个缓存。
Account account = null;
var loginAccountsKey = userName + password;
if (self.LoginAccounts.TryGetValue(loginAccountsKey, out var accountCacheInfo))
{
account = accountCacheInfo.GetComponent<Account>();
if (account == null)
{
return (2, 0);
}
return (0, account.Id);
}
uint result = 0;
accountCacheInfo = Entity.Create<AccountCacheInfo>(scene, true, true);
account = await worldDateBase.First<Account>(d => d.Username == userName && d.Password == password);
if (account == null)
{
// 这个2代表的是该用户没有注册或者用户或密码错误
result = 2;
}
else
{
// 更新登录时间,并保存到数据库
account.LoginTime = TimeHelper.Now;
await worldDateBase.Save(account);
// 添加Account到缓存中
account.Deserialize(scene);
accountCacheInfo.AddComponent(account);
}
accountCacheInfo.AddComponent<AccountCacheInfoTimeOut>().TimeOut(loginAccountsKey, 5000);
self.LoginAccounts.Add(loginAccountsKey, accountCacheInfo);
if (result != 0)
{
return (result, 0);
}
return (0, account.Id);
}
}
/// <summary>
/// 鉴权注册接口
/// </summary>
/// <param name="self"></param>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="source"></param>
internal static async FTask<uint> Register(this AuthenticationComponent self, string username, string password, string source)
{
// 1、检查传递的参数是否完整
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
// 这个1代表的是参数不完整。
return 1;
}
// 检查账号是否应该在当前鉴权服务器中处理
var position = HashCodeHelper.MurmurHash3(username) % self.AuthenticationCount;
if (self.Position != position)
{
// 这个3代表的是当前账号不应该在这个鉴权服务器处理。
return 3;
}
var usernameHashCode = username.GetHashCode();
var scene = self.Scene;
// 利用协程锁来解决异步的原子问题
using (var @lock = await scene.CoroutineLockComponent.Wait((int)LockType.AuthenticationRegisterLock, usernameHashCode))
{
// 利用缓存来减少频繁请求数据库或缓存的压力。
if (self.Accounts.TryGetValue(username, out var account))
{
// 这个2代表的是该用户已经存在。
return 2;
}
// 2、数据库查询该账号是否存在
var worldDateBase = scene.World.DataBase;
var isExist = await worldDateBase.Exist<Account>(d => d.Username == username);
if (isExist)
{
// 这个2代表的是该用户已经存在。
return 2;
}
//3、执行到这里的话表示数据库或缓存没有该账号的注册信息需要咱们创建一个。
account = Entity.Create<Account>(scene, true, true);
account.Username = username;
account.Password = password;
account.CreateTime = TimeHelper.Now;
// 写入这个实体到数据中
await worldDateBase.Save(account);
var accountId = account.Id;
// 把当前账号添加到缓存字典中。
self.Accounts.Add(username, account);
// 添加AccountTimeOut组件用来定时清除缓存
account.AddComponent<AccountTimeOut>().TimeOut(4000);
// 这个0代表的是操作成功
Log.Info($"Register source:{source} username:{username} accountId:{accountId}");
return 0;
}
}
internal static void RemoveLoginAccounts(this AuthenticationComponent self, string key, bool isDispose)
{
if (!self.LoginAccounts.Remove(key, out var accountCacheInfo))
{
return;
}
if (isDispose)
{
accountCacheInfo.Dispose();
}
}
internal static void RemoveCache(this AuthenticationComponent self, string username, bool isDispose)
{
if (!self.Accounts.Remove(username, out var account))
{
return;
}
Log.Debug($"Remove cache username:{username} Count:{self.Accounts.Count}");
if (isDispose)
{
account.Dispose();
}
}
internal static async FTask<uint> Remove(this AuthenticationComponent self, long accountId, string source)
{
var scene = self.Scene;
// 其实呢,这里没必要加协程锁,这里加是为了给大家加深下这个协程锁的印象。
using (var @lock = await scene.CoroutineLockComponent.Wait((int)LockType.AuthenticationRemoveLock, accountId))
{
var worldDateBase = scene.World.DataBase;
await worldDateBase.Remove<Account>(accountId);
Log.Info($"Remove source:{source} accountId:{accountId}");
return 0;
}
}
}

View File

@@ -0,0 +1,66 @@
using Fantasy;
using Fantasy.Async;
namespace Fantasy.Authentication;
public static class AuthenticationHelper
{
/// <summary>
/// 登录账号
/// </summary>
/// <param name="scene"></param>
/// <param name="userName">用户名</param>
/// <param name="password">用户密码</param>
/// <returns></returns>
public static async FTask<(uint ErrorCode, long AccountId)> Login(Scene scene, string userName, string password)
{
return await scene.GetComponent<AuthenticationComponent>().Login(userName, password);
}
/// <summary>
/// 注册一个新的账号
/// </summary>
/// <param name="scene"></param>
/// <param name="username">用户名</param>
/// <param name="password">用户密码</param>
/// <param name="source">注册的来源/原因</param>
/// <returns></returns>
public static async FTask<uint> Register(Scene scene, string username, string password, string source)
{
return await scene.GetComponent<AuthenticationComponent>().Register(username, password, source);
}
/// <summary>
/// 移除一个账号
/// </summary>
/// <param name="scene"></param>
/// <param name="accountId">账号ID</param>
/// <param name="source">移除的来源/原因</param>
/// <returns></returns>
public static async FTask<uint> Remove(Scene scene, long accountId, string source)
{
return await scene.GetComponent<AuthenticationComponent>().Remove(accountId, source);
}
/// <summary>
/// 移除缓存中的Account
/// </summary>
/// <param name="scene"></param>
/// <param name="username">账号名字</param>
/// <param name="isDispose">是否销毁</param>
public static void RemoveCache(Scene scene, string username, bool isDispose)
{
scene.GetComponent<AuthenticationComponent>().RemoveCache(username, isDispose);
}
/// <summary>
/// 移除LoginAccounts缓存中的数据仅供内部调用不明白原理的不要调用否则后果自负。
/// </summary>
/// <param name="scene"></param>
/// <param name="key"></param>
/// <param name="isDispose"></param>
internal static void RemoveLoginAccounts(Scene scene, string key, bool isDispose)
{
scene.GetComponent<AuthenticationComponent>().RemoveLoginAccounts(key, isDispose);
}
}

View File

@@ -0,0 +1,55 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Fantasy;
using Fantasy.Entitas.Interface;
using Microsoft.IdentityModel.Tokens;
namespace Fantasy.Authentication.Jwt;
public sealed class AuthenticationJwtComponentAwakeSystem : AwakeSystem<AuthenticationJwtComponent>
{
protected override void Awake(AuthenticationJwtComponent self)
{
self.Awake();
}
}
public static class AuthenticationJwtComponentSystem
{
public static void Awake(this AuthenticationJwtComponent self)
{
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(self.PublicKeyPem), out _);
rsa.ImportRSAPrivateKey(Convert.FromBase64String(self.PrivateKeyPem), out _);
self.SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
// 创建 TokenValidationParameters 对象,用于配置验证参数
self.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = false, // 禁止令牌验证时间是否过期
ValidateIssuer = true, // 验证发行者
ValidateAudience = true, // 验证受众
ValidateIssuerSigningKey = true, // 验证签名密钥
ValidIssuer = "Fantasy", // 有效的发行者
ValidAudience = "Fantasy", // 有效的受众
IssuerSigningKey = new RsaSecurityKey(rsa) // RSA公钥作为签名密钥
};
}
public static string GetToken(this AuthenticationJwtComponent self, long aId, string address, uint sceneId)
{
var jwtPayload = new JwtPayload()
{
{ "aId", aId },
{ "Address", address },
{ "SceneId", sceneId }
};
var jwtSecurityToken = new JwtSecurityToken(
issuer: "Fantasy",
audience: "Fantasy",
claims: jwtPayload.Claims,
expires: DateTime.UtcNow.AddMilliseconds(3000),
signingCredentials: self.SigningCredentials);
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
}

View File

@@ -0,0 +1,19 @@
using Fantasy;
namespace Fantasy.Authentication.Jwt;
public static class AuthenticationJwtHelper
{
/// <summary>
/// 获取一个新的令牌
/// </summary>
/// <param name="scene"></param>
/// <param name="aId">AccountId</param>
/// <param name="address">目标服务器的地址</param>
/// <param name="sceneId">分配的Scene的Id</param>
/// <returns></returns>
public static string GetToken(Scene scene, long aId, string address, uint sceneId)
{
return scene.GetComponent<AuthenticationJwtComponent>().GetToken(aId, address, sceneId);
}
}

View File

@@ -0,0 +1,40 @@
using Fantasy;
using Fantasy.Entitas.Interface;
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
namespace Fantasy.Authentication;
public sealed class AccountCacheInfoTimeOutDestroySystem : DestroySystem<AccountCacheInfoTimeOut>
{
protected override void Destroy(AccountCacheInfoTimeOut self)
{
if (self.TimerId != 0)
{
self.Scene.TimerComponent.Net.Remove(ref self.TimerId);
}
self.Key = null;
}
}
public static class AccountCacheInfoTimeOutSystem
{
public static void TimeOut(this AccountCacheInfoTimeOut self, string key, int timeout)
{
self.Key = key;
// 创建一个任务计时器、用在timeout时间后执行并且要清楚掉当前鉴权服务器缓存
var scene = self.Scene;
var runTimeId = self.RuntimeId;
self.TimerId = scene.TimerComponent.Net.OnceTimer(timeout, () =>
{
if (runTimeId != self.RuntimeId)
{
return;
}
self.TimerId = 0;
AuthenticationHelper.RemoveLoginAccounts(scene, self.Key,true);
});
}
}

View File

@@ -0,0 +1,37 @@
using Fantasy;
using Fantasy.Entitas.Interface;
namespace Fantasy.Authentication;
public sealed class AccountTimeOutDestroySystem : DestroySystem<AccountTimeOut>
{
protected override void Destroy(AccountTimeOut self)
{
if (self.TimerId != 0)
{
self.Scene.TimerComponent.Net.Remove(ref self.TimerId);
}
}
}
public static class AccountTimeOutSystem
{
public static void TimeOut(this AccountTimeOut self, int timeout)
{
// 创建一个任务计时器、用在timeout时间后执行并且要清楚掉当前鉴权服务器缓存
var scene = self.Scene;
var account = (Account)self.Parent;
var accountRunTimeId = account.RuntimeId;
self.TimerId = scene.TimerComponent.Net.OnceTimer(timeout, () =>
{
if (accountRunTimeId != account.RuntimeId)
{
return;
}
self.TimerId = 0;
AuthenticationHelper.RemoveCache(scene, account.Username,true);
});
}
}

View File

@@ -1,33 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Network.Route;
using Fantasy.Platform.Net;
namespace Fantasy;
public sealed class
C2G_CreateAddressableRequestHandler : MessageRPC<C2G_CreateAddressableRequest, G2C_CreateAddressableResponse>
{
protected override async FTask Run(Session session, C2G_CreateAddressableRequest request,
G2C_CreateAddressableResponse response, Action reply)
{
var scene = session.Scene;
// 1、首先要通过SceneConfig配置文件拿到进行注册Addressable协议的服务器
// 实际开发的时候可能会根据一些规则来选择不同的Map服务器。
// 演示的例子里只有一个MapScene所以我就拿第一个Map服务器进行通讯了。
// 我这里仅是演示功能不是一定要这样拿Map
var sceneConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[0];
// 2、使用Scene.NetworkMessagingComponent.CallInnerRoute方法跟Gate服务器进行通讯。
// b这个Id在sceneConfig.RouteId可以获取到。
// 第二个参数是需要发送网络协议这个协议在Fantasy/Examples/Config/ProtoBuf里的InnerBson或Inner文件定义。
var responseAddressableId =
(M2G_ResponseAddressableId)await scene.NetworkMessagingComponent.CallInnerRoute(sceneConfig.RouteId,
new G2M_RequestAddressableId());
// 3、给session添加一个AddressableRouteComponent组件这个组件很重要、能否转发Addressable协议主要是通过这个。
var addressableRouteComponent = session.AddComponent<AddressableRouteComponent>();
// 4、拿到MapScene返回的AddressableId赋值给addressableRouteComponent.AddressableId。
addressableRouteComponent.AddressableId = responseAddressableId.AddressableId;
}
}

View File

@@ -1,28 +0,0 @@
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Network.Route;
// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
namespace Fantasy;
public sealed class C2G_SendAddressableToMapHandler : Message<C2G_SendAddressableToMap>
{
protected override async FTask Run(Session session, C2G_SendAddressableToMap message)
{
var addressableRouteComponent = session.GetComponent<AddressableRouteComponent>();
if (addressableRouteComponent == null)
{
return;
}
// Gate发送一个Addressable消息给MAP
await session.Scene.NetworkMessagingComponent.SendAddressable(addressableRouteComponent.AddressableId,
new G2M_SendAddressableMessage()
{
Tag = message.Tag
});
}
}

View File

@@ -1,40 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Entitas;
using Fantasy.Entitas.Interface;
using Fantasy.Network.Interface;
using Fantasy.Network.Route;
using Fantasy.Platform.Net;
using Fantasy.Serialize;
namespace Fantasy;
public class C2M_MoveToMapRequestHandler : AddressableRPC<Unit, C2M_MoveToMapRequest, M2C_MoveToMapResponse>
{
protected override async FTask Run(Unit unit, C2M_MoveToMapRequest request, M2C_MoveToMapResponse response, Action reply)
{
// 1、首先要通过SceneConfig配置文件拿到MapScene的配置文件。
// 这里Map[1]就是要发送的服务器,因为Map[0]是当前的Scene。
var scene = unit.Scene;
var mapSceneConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[1];
// 2、锁定Addressable防止在转移期间有消息发送过来。
// LockAndRelease方法是先锁定Addressable消息后再销毁这个组件。
// 注意:只有这个方法的销毁组件不会去Addressable里删除自己的位置信息。
// 其他的任何销毁这个AddressableMessageComponent方法都会去Addressable里删除自己的位置信息。
await unit.GetComponent<AddressableMessageComponent>().LockAndRelease();
// 3、通过NetworkMessagingComponent发送内部消息给Map的Scene。
var sendResponse = await scene.NetworkMessagingComponent.CallInnerRoute(mapSceneConfig.RouteId,
new M2M_SendUnitRequest()
{
Unit = unit
});
if (sendResponse.ErrorCode != 0)
{
Log.Error($"转移Unit到目标Map服务器失败 ErrorCode={sendResponse.ErrorCode}");
return;
}
// 这个Unit已经转移到另外的Map服务器了所以就不需要这个组件了。
unit.Dispose();
Log.Debug("转移Unit到目标Map服务器成功");
}
}

View File

@@ -1,13 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public sealed class C2M_TestMessageHandler : Addressable<Unit, C2M_TestMessage>
{
protected override async FTask Run(Unit unit, C2M_TestMessage message)
{
Log.Debug($"C2M_TestMessageHandler = {message.Tag} Scene:{unit.Scene.Scene.SceneConfigId}");
await FTask.CompletedTask;
}
}

View File

@@ -1,15 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public sealed class C2M_TestRequestHandler : AddressableRPC<Unit, C2M_TestRequest, M2C_TestResponse>
{
protected override async FTask Run(Unit unit, C2M_TestRequest request, M2C_TestResponse response, Action reply)
{
Log.Debug($"Receive C2M_TestRequest Tag = {request.Tag}");
response.Tag = "Hello M2C_TestResponse";
await FTask.CompletedTask;
}
}

View File

@@ -1,13 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public sealed class G2M_SendAddressableMessageHandler : Addressable<Unit, G2M_SendAddressableMessage>
{
protected override async FTask Run(Unit unit, G2M_SendAddressableMessage message)
{
Log.Debug($"收到Gate发送来的Addressable消息 message:{message.Tag}");
await FTask.CompletedTask;
}
}

View File

@@ -1,21 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Route;
namespace Fantasy;
public class M2M_SendUnitRequestHandler : RouteRPC<Scene, M2M_SendUnitRequest, M2M_SendUnitResponse>
{
protected override async FTask Run(Scene scene, M2M_SendUnitRequest request, M2M_SendUnitResponse response, Action reply)
{
var requestUnit = request.Unit;
// 反序列化Unit把Unit注册到框架中
requestUnit.Deserialize(scene);
// 解锁这个Unit的Addressable消息解锁后Gate上缓存的消息会发送到这里。
// 由于AddressableMessageComponent不支持存数据库所以在发送Unit的时候会自动把这个给忽略掉。
// 所以需要再次手动的添加下才可以。
await requestUnit.AddComponent<AddressableMessageComponent>().UnLock("M2M_SendUnitRequestHandler");
Log.Debug($"传送完成 {scene.SceneConfigId}");
}
}

View File

@@ -1,16 +0,0 @@
using Fantasy.Async;
using Fantasy.Entitas;
using Fantasy.Entitas.Interface;
using Fantasy.Network;
using Fantasy.Network.Interface;
namespace Fantasy.Gate;
public sealed class C2G_TestMessageHandler : Message<C2G_TestMessage>
{
protected override async FTask Run(Session session, C2G_TestMessage message)
{
Log.Debug($"Receive C2G_TestMessage Tag={message.Tag}");
await FTask.CompletedTask;
}
}

View File

@@ -1,16 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
namespace Fantasy.Gate;
public sealed class C2G_TestRequestHandler : MessageRPC<C2G_TestRequest, G2C_TestResponse>
{
protected override async FTask Run(Session session, C2G_TestRequest request, G2C_TestResponse response, Action reply)
{
Log.Debug($"Receive C2G_TestRequest Tag = {request.Tag}");
response.Tag = "Hello G2C_TestResponse";
await FTask.CompletedTask;
}
}

View File

@@ -1,20 +0,0 @@
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
namespace Fantasy.Gate;
public sealed class C2G_TestRequestPushMessageHandler : Message<C2G_TestRequestPushMessage>
{
protected override async FTask Run(Session session, C2G_TestRequestPushMessage message)
{
// 因为没有服务器的相关的逻辑,所以制作了一个协议来触发服务器发送消息给客户端的环境。
// 使用当前会话的Session.Send发送消息给客户端。
// 如果需要群发你可以用一个容器保存起来发送的时候遍历这个容器调用Send方法就可以了。
session.Send(new G2C_PushMessage()
{
Tag = "Hi G2C_PushMessage"
});
await FTask.CompletedTask;
}
}

View File

@@ -1,15 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Roaming;
namespace Fantasy;
public class C2Chat_TestRPCRoamingRequestHandler : RoamingRPC<Terminus, C2Chat_TestRPCRoamingRequest, Chat2C_TestRPCRoamingResponse>
{
protected override async FTask Run(Terminus terminus, C2Chat_TestRPCRoamingRequest request, Chat2C_TestRPCRoamingResponse response, Action reply)
{
Log.Debug($"C2Chat_TestRPCRoamingRequestHandler message:{request.Tag} SceneType:{terminus.Scene.SceneType} SceneId:{terminus.Scene.RuntimeId}");
await FTask.CompletedTask;
}
}

View File

@@ -1,15 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Roaming;
namespace Fantasy;
public sealed class C2Chat_TestRoamingMessageHandler : Roaming<Terminus, C2Chat_TestRoamingMessage>
{
protected override async FTask Run(Terminus terminus, C2Chat_TestRoamingMessage message)
{
Log.Debug($"C2Chat_TestRoamingMessageHandler message:{message.Tag} SceneType:{terminus.Scene.SceneType} SceneId:{terminus.Scene.RuntimeId}");
await FTask.CompletedTask;
}
}

View File

@@ -1,18 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Roaming;
namespace Fantasy;
public sealed class C2Chat_TestSendMapMessageHandler : Roaming<Terminus, C2Chat_TestSendMapMessage>
{
protected override async FTask Run(Terminus terminus, C2Chat_TestSendMapMessage message)
{
terminus.Send(RoamingType.MapRoamingType, new Chat2M_TestMessage()
{
Tag = "Hi Inner Roaming Message!"
});
await FTask.CompletedTask;
}
}

View File

@@ -1,62 +0,0 @@
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Platform.Net;
using Fantasy.Roaming;
namespace Fantasy;
public sealed class C2G_ConnectRoamingRequestHandler : MessageRPC<C2G_ConnectRoamingRequest, G2C_ConnectRoamingResponse>
{
protected override async FTask Run(Session session, C2G_ConnectRoamingRequest request, G2C_ConnectRoamingResponse response, Action reply)
{
// 给session创建一个漫游功能。
// 这个功能很重要这个组件是整个Roaming系统最核心的组件这个组件会处理Roaming协议
// 这个功能会处理Roaming协议所以创建这个是必须的。
// CreateRoaming需要支持三个参数:
// roamingId:这个参数是RoamingIdRoamingId是Roaming的唯一标识不能重复。
// 指定了这个RoamingId后服务器其他漫游终端的Id会是你设置的RoamingId。
// 这样操作方便统一管理漫游协议。
// 一般这个RoamingId是一个角色的Id这样方便管理。
// isAutoDispose:是否在Session断开的时候自动断开漫游功能。
// delayRemove:如果开启了自定断开漫游功能需要设置一个延迟多久执行断开。
// 这里没有角色的Id所以这里使用1来代替。
// isAutoDispose我选择自动断开这个断开的时机是Session断开后执行。
// delayRemove断开漫游功能后Session会自动断开所以这里设置延迟1000毫秒执行断开。
// 这里创建的漫游功能会自动处理Roaming协议所以不需要手动处理Roaming协议。
var roaming = session.CreateRoaming(1,true,1000);
// 通过SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[0]拿到Map场景的配置信息
// 如果需要协议漫游其他Scene可以在配置中查找要漫游的服务器。
// 可以同时漫游多个Scene但每个Scene的漫游都有一个固定的类型不能重复。
var mapConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[0];
// 通过RoamingComponent.Link(session, mapConfig, 1, 1)链接Map场景
// 第一个参数是Session第二个参数是Map场景的配置信息第三个参数是Map场景的RouteId第四个参数是Map场景的RoamingType。
// 这个RoamingType是通过RoamingType.Config文件中定义的。
// RouteType.Config文件位置在你定义的网络文件协议文件夹下。如果找不到RoamingType.Config文件可以运行下导出协议工具导出一个协议后会自动创建。
// 该示例工程下文件位置在Config/NetworkProtocol/RoamingType.Config
// 执行完后漫游会自动把Session绑定到Map场景上。
// 后面发送该类型的消息到Session上会自动转发给Map场景。
var linkResponse = await roaming.Link(session, mapConfig, RoamingType.MapRoamingType);
if (linkResponse != 0)
{
response.ErrorCode = linkResponse;
return;
}
// 同样你可以创建多个漫游的场景但每个场景的RouteId和RoamingType不能重复。
// 这里创建Chat场景的漫游。
var chatConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Chat)[0];
linkResponse = await roaming.Link(session, chatConfig, RoamingType.ChatRoamingType);
if (linkResponse != 0)
{
response.ErrorCode = linkResponse;
return;
}
// 如果你觉的每次创建一个场景的漫游都麻烦你可以利用RoamingType.RoamingTypes遍历创建。
// 但这样的会把你在RoamingType.Config定义的都创建出来
foreach (var roamingType in RoamingType.RoamingTypes)
{
// 这里添加roaming.Link的方法进行创建。
}
}
}

View File

@@ -1,17 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
namespace Fantasy;
public sealed class C2Map_PushMessageToClientHandler : Roaming<Terminus, C2Map_PushMessageToClient>
{
protected override async FTask Run(Terminus terminus, C2Map_PushMessageToClient message)
{
terminus.Send(new Map2C_PushMessageToClient()
{
Tag = message.Tag
});
await FTask.CompletedTask;
}
}

View File

@@ -1,15 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Roaming;
namespace Fantasy;
public class C2Map_TestRoamingMessageHandler : Roaming<Terminus, C2Map_TestRoamingMessage>
{
protected override async FTask Run(Terminus terminus, C2Map_TestRoamingMessage message)
{
Log.Debug($"C2Map_TestRoamingMessageHandler message:{message.Tag} SceneType:{terminus.Scene.SceneType} SceneId:{terminus.Scene.RuntimeId}");
await FTask.CompletedTask;
}
}

View File

@@ -1,17 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Platform.Net;
using Fantasy.Roaming;
namespace Fantasy;
public class C2Map_TestTransferRequestHandler : RoamingRPC<Terminus, C2Map_TestTransferRequest, Map2C_TestTransferResponse>
{
protected override async FTask Run(Terminus terminus, C2Map_TestTransferRequest request, Map2C_TestTransferResponse response, Action reply)
{
Log.Debug($"C2Map_TestTransferRequestHandler1 terminus:{terminus.RuntimeId}");
var mapConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[1];
response.ErrorCode = await terminus.StartTransfer(mapConfig.RouteId);
}
}

View File

@@ -1,15 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
using Fantasy.Network.Roaming;
using Fantasy.Roaming;
namespace Fantasy;
public sealed class Chat2M_TestMessageHandler : Roaming<Terminus, Chat2M_TestMessage>
{
protected override async FTask Run(Terminus terminus, Chat2M_TestMessage message)
{
Log.Debug($"Chat2M_TestMessageHandler message:{message.Tag} SceneType:{terminus.Scene.SceneType} SceneId:{terminus.Scene.RuntimeId}");
await FTask.CompletedTask;
}
}

View File

@@ -1,13 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public sealed class C2Chat_TestMessageHandler : Route<ChatUnit, C2Chat_TestMessage>
{
protected override async FTask Run(ChatUnit entity, C2Chat_TestMessage message)
{
Log.Debug($"C2Chat_TestMessageHandler.c2Chat_TestMessage: {message}");
await FTask.CompletedTask;
}
}

View File

@@ -1,15 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public sealed class C2Chat_TestMessageRequestHandler : RouteRPC<ChatUnit, C2Chat_TestMessageRequest, Chat2C_TestMessageResponse>
{
protected override async FTask Run(ChatUnit entity, C2Chat_TestMessageRequest request, Chat2C_TestMessageResponse response, Action reply)
{
Log.Debug($"C2Chat_TestMessageRequestHandler request = {request}");
response.Tag = "Hello RouteRPC";
await FTask.CompletedTask;
}
}

View File

@@ -1,22 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public sealed class C2Chat_TestRequestPushMessageHandler : Route<ChatUnit, C2Chat_TestRequestPushMessage>
{
protected override async FTask Run(ChatUnit chatUnit, C2Chat_TestRequestPushMessage message)
{
// 因为没有服务器的相关的逻辑,所以制作了一个协议来触发服务器发送消息给客户端的环境。
// 使用当前Scene.NetworkMessagingComponent.SendInnerRoute发送消息给客户端。
// 只需要把消息发送给创建链接的Gate上就会自动转发消息到客户端上。
// 因为chatUnit.GateRouteId是在G2Chat_CreateRouteRequestHandler方法里记录的所以直接使用这个就可以了。
chatUnit.Scene.NetworkMessagingComponent.SendInnerRoute(chatUnit.GateRouteId, new Chat2C_PushMessage()
{
Tag = "Hi Route Chat2C_PushMessage"
});
await FTask.CompletedTask;
}
}

View File

@@ -1,46 +0,0 @@
using System;
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Platform.Net;
namespace Fantasy;
public sealed class C2G_CreateChatRouteRequestHandler : MessageRPC<C2G_CreateChatRouteRequest, G2C_CreateChatRouteResponse>
{
protected override async FTask Run(Session session, C2G_CreateChatRouteRequest request, G2C_CreateChatRouteResponse response, Action reply)
{
// 首先需要找到一个需要建立Route的Scene的SceneConfig。
// 例子演示的连接的ChatScene,所以这里我通过SceneConfigData拿到这个SceneConfig。
// 如果是其他Scene用法跟这个没有任何区别。
var chatSceneConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Chat)[0];
// 通过chatSceneConfig拿到这个Scene的RouteId
var chatRouteId = chatSceneConfig.RouteId;
// 通过Scene拿到当前Scene的NetworkMessagingComponent。
// NetworkMessagingComponent是服务器之间通讯的唯一手段。
var networkMessagingComponent = session.Scene.NetworkMessagingComponent;
// 通过CallInnerRoute方法发送一个RPC消息给ChatScene上。
// 任何一个实体的RunTimeId都可以做为RouteId使用。
// 所以这个传递了一个session.RunTimeId是方便Chat发送消息回Gate上。
var routeResponse = (Chat2G_CreateRouteResponse)await networkMessagingComponent.CallInnerRoute(chatRouteId,
new G2Chat_CreateRouteRequest()
{
GateRouteId = session.RouteId
});
if (routeResponse.ErrorCode != 0)
{
// 如果ErrorCode不是0表示请求的协议发生错误应该提示给客户端。
// 这里就不做这个了。
return;
}
// 要实现Route协议的转发需要给Session添加一个RouteComponent这个非常重要。
var routeComponent = session.AddComponent<RouteComponent>();
// 需要再Examples/Config/NetworkProtocol/RouteType.Config里添加一个ChatRoute
// 然后点击导表工具会自动生成一个RouteType.cs文件。
// 使用你定义的ChatRoute当routeType的参数传递进去。
// routeResponse会返回一个ChatRouteId这个就是Chat的RouteId。
routeComponent.AddAddress((int)RouteType.ChatRoute,routeResponse.ChatRouteId);
// 这些操作完成后就完成了Route消息的建立。
// 后面可以直接发送Route消息通过Gate自动中转给Chat了。
}
}

View File

@@ -1,21 +0,0 @@
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Network.Route;
namespace Fantasy;
public sealed class C2G_CreateSubSceneAddressableRequestHandler : MessageRPC<C2G_CreateSubSceneAddressableRequest, G2C_CreateSubSceneAddressableResponse>
{
protected override async FTask Run(Session session, C2G_CreateSubSceneAddressableRequest request, G2C_CreateSubSceneAddressableResponse response, Action reply)
{
var scene = session.Scene;
var subSceneRouteId = session.GetComponent<GateSubSceneFlagComponent>().SubSceneRouteId;
// 1、向SubScene请求AddressableId
var responseAddressableId = (SubScene2G_AddressableIdResponse)await scene.NetworkMessagingComponent.CallInnerRoute(subSceneRouteId, new G2SubScene_AddressableIdRequest());
// 2、给session添加一个AddressableRouteComponent组件这个组件很重要、能否转发Addressable协议主要是通过这个。
var addressableRouteComponent = session.AddComponent<AddressableRouteComponent>();
// 3、拿到SubScene返回的AddressableId赋值给addressableRouteComponent.AddressableId。
addressableRouteComponent.AddressableId = responseAddressableId.AddressableId;
}
}

View File

@@ -1,26 +0,0 @@
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
using Fantasy.Platform.Net;
namespace Fantasy;
public sealed class C2G_CreateSubSceneRequestHandler : MessageRPC<C2G_CreateSubSceneRequest, G2C_CreateSubSceneResponse>
{
protected override async FTask Run(Session session, C2G_CreateSubSceneRequest request, G2C_CreateSubSceneResponse response, Action reply)
{
var scene = session.Scene;
var sceneConfig = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Map)[0];
var createSubSceneResponse = (M2G_CreateSubSceneResponse)await scene.NetworkMessagingComponent.CallInnerRoute(sceneConfig.RouteId, new G2M_CreateSubSceneRequest());
if (createSubSceneResponse.ErrorCode != 0)
{
// 创建SubScene失败。
response.ErrorCode = createSubSceneResponse.ErrorCode;
return;
}
// 记录下这个RouteId以便后续的消息转发。
session.AddComponent<GateSubSceneFlagComponent>().SubSceneRouteId = createSubSceneResponse.SubSceneRouteId;
}
}

View File

@@ -1,18 +0,0 @@
using Fantasy.Async;
using Fantasy.Network;
using Fantasy.Network.Interface;
namespace Fantasy;
public class C2G_SendToSubSceneMessageHandler : Message<C2G_SendToSubSceneMessage>
{
protected override async FTask Run(Session session, C2G_SendToSubSceneMessage message)
{
var subSceneRouteId = session.GetComponent<GateSubSceneFlagComponent>().SubSceneRouteId;
session.Scene.NetworkMessagingComponent.SendInnerRoute(subSceneRouteId, new G2SubScene_SentMessage()
{
Tag = "Hi SubScene",
});
await FTask.CompletedTask;
}
}

View File

@@ -1,16 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public class C2SubScene_TestDisposeMessageHandler : Addressable<Unit, C2SubScene_TestDisposeMessage>
{
protected override async FTask Run(Unit unit, C2SubScene_TestDisposeMessage message)
{
var unitScene = unit.Scene;
var unitSceneSceneType = unitScene.SceneType;
unitScene.Dispose();
Log.Debug($"{unitSceneSceneType} {unitScene.RuntimeId} is Dispose!");
await FTask.CompletedTask;
}
}

View File

@@ -1,14 +0,0 @@
using Fantasy.Async;
using Fantasy.Network.Interface;
namespace Fantasy;
public class C2SubScene_TestMessageHandler : Addressable<Unit, C2SubScene_TestMessage>
{
protected override async FTask Run(Unit unit, C2SubScene_TestMessage message)
{
Log.Debug($"C2M_TestMessageHandler = {message.Tag} SceneType:{unit.Scene.SceneType} {unit.Scene.GetComponent<SubSceneTestComponent>() == null}");
await FTask.CompletedTask;
}
}

View File

@@ -1,14 +0,0 @@
// using Fantasy.Async;
// using Fantasy.Network;
// using Fantasy.Network.Interface;
//
// namespace Fantasy.Gate;
//
// public class C2G_LoginMessageHandler : Message<C2G_TestMessage>
// {
// protected override async FTask Run(Session session, C2G_TestMessage message)
// {
// Log.Debug($"Receive C2G_TestMessage Tag={message.Tag}");
// await FTask.CompletedTask;
// }
// }

View File

@@ -0,0 +1,33 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Gate;
using Fantasy.Network;
using Fantasy.Network.Interface;
namespace System.Gate;
public sealed class C2G_GetAccountInfoRequestHandler : MessageRPC<C2G_GetAccountInfoRequest, G2C_GetAccountInfoResponse>
{
protected override async FTask Run(Session session, C2G_GetAccountInfoRequest request, G2C_GetAccountInfoResponse response, Action reply)
{
var gameAccountFlagComponent = session.GetComponent<GameAccountFlagComponent>();
if (gameAccountFlagComponent == null)
{
// 表示不应该访问这个接口,要先访问登录的接口。
// response.ErrorCode = 1;
session.Dispose();
return;
}
GameAccount account = gameAccountFlagComponent.Account;
if (account == null)
{
// 表示这个Account已经被销毁过了。不是咱们想要的了
}
response.GameAccountInfo = account.GetGameAccountInfo();
await FTask.CompletedTask;
}
}

View File

@@ -0,0 +1,102 @@
using System.Gate.System;
using Fantasy;
using Fantasy.Async;
using Fantasy.Gate;
using Fantasy.Network;
using Fantasy.Network.Interface;
#pragma warning disable CS8604 // Possible null reference argument.
namespace System.Gate.Handler;
public sealed class C2G_LoginRequestHandler : MessageRPC<C2G_LoginRequest, G2C_LoginResponse>
{
protected override async FTask Run(Session session, C2G_LoginRequest request, G2C_LoginResponse response,
Action reply)
{
if (string.IsNullOrEmpty(request.ToKen))
{
// 1、客户端漏传了 response.ErrorCode = 1;
// 2、恶意攻击导致的 session.Dispose();
session.Dispose();
return;
}
var scene = session.Scene;
if (!GateJWTHelper.ValidateToken(scene, request.ToKen, out var accountId))
{
// 如果失败,表示肯定是恶意攻击、所以毫不犹疑,直接断开当前会话。
session.Dispose();
return;
}
// 在缓存中检查该账号是否存在
var gameAccountManageComponent = scene.GetComponent<GameAccountManageComponent>();
Log.Debug("检查账号是否在缓存中");
if (!gameAccountManageComponent.TryGet(accountId, out var account))
{
// 首先要先到数据库中查询是否有这个账号
account = await GameAccountHelper.LoadDataBase(scene, accountId);
// 如果有的话,就直接加入在缓存中就可以了
if (account == null)
{
Log.Debug("检查到账号没有在数据库中,需要创建一个新的账号并且保存到数据库中");
// 如果没有,就要创建一个新的并且保存到数据库。
// 如果不存在,表示这是一个新的账号,需要创建一下这个账号。
account = await GameAccountFactory.Create(scene, accountId);
}
else
{
Log.Debug("检查到账号在数据库中");
}
Log.Debug("把当前账号添加到缓存中");
// 把创建完成的Account放入到缓存中
gameAccountManageComponent.Add(account);
}
else
{
Log.Debug("检测到当前账号已经在缓存中了");
// 如果有延迟下线的计划任务,那就先取消一下。
account.CancelTimeout();
// 如果在Gate的缓存中已经存在了该账号那只能以下几种可能:
// 1、同一客户端发送了重复登录的请求数据。
// 2、客户端经历的断线然后又重新连接到这个服务器上了断线重连
// 3、多个客户端同时登录了这个账号顶号
if (session.RuntimeId == account.SessionRunTimeId)
{
// 如果执行到这里,说明是客户端发送了多次登录的请求,这样的情况下,直接返回就可以了,不需要做任何操作。
return;
}
Log.Debug("检测到当前账号的Session不是同一个");
if (scene.TryGetEntity<Session>(account.SessionRunTimeId, out var oldSession))
{
Log.Debug("当前账号的Session在当前的系统中所以需要发送一个重复登录的命令并且要断开这个Session");
// 如果这个Session在当前框架中可以查询到。
// 那表示就是当前的会话还是在存在的,有如下几个可能:
// 1、客户端断线重连要给这个Session发送一个消息通知它有人登录了。
// 2、其他的客户端登录了这个账号要给这个Session发送一个消息通知它有人登录了。
var gameAccountFlagComponent = oldSession.GetComponent<GameAccountFlagComponent>();
gameAccountFlagComponent.AccountID = 0;
gameAccountFlagComponent.Account = null;
// 给客户端发送一个重复登录的消息,如果当前客户端是自己上次登录的,发送也不会收到。
oldSession.Send(new G2C_RepeatLogin());
// 给当前Session做一个定时销毁的任务因为不做这个定时销毁直接销毁的话有可能消息还没有发送过去就销毁了
oldSession.SetTimeout(3000);
}
}
// 给当前Session添加一个组件当Session销毁的时候会销毁这个组件。
var accountFlagComponent = session.AddComponent<GameAccountFlagComponent>();
accountFlagComponent.AccountID = accountId;
accountFlagComponent.Account = account;
account.SessionRunTimeId = session.RuntimeId;
response.GameAccountInfo = account.GetGameAccountInfo();
Log.Debug($"当前的Gate服务器:{session.Scene.SceneConfigId} accountId:{accountId}");
}
}

View File

@@ -0,0 +1,30 @@
using Fantasy;
using Fantasy.Async;
using Fantasy.Entitas;
using Fantasy.Gate;
using Fantasy.Helper;
namespace System.Gate;
public static class GameAccountFactory
{
/// <summary>
/// 创建一个新的GameAccount
/// </summary>
/// <param name="scene"></param>
/// <param name="aId">ToKen令牌传递过来的aId</param>
/// <param name="isSaveDataBase">是否在创建的过程中保存到数据库</param>
/// <returns></returns>
public static async FTask<GameAccount> Create(Scene scene, long aId, bool isSaveDataBase = true)
{
var gameAccount = Entity.Create<GameAccount>(scene, aId, false, false);
gameAccount.LoginTime = gameAccount.CreateTime = TimeHelper.Now;
if (isSaveDataBase)
{
await gameAccount.SaveDataBase();
}
return gameAccount;
}
}

View File

@@ -0,0 +1,20 @@
using Fantasy.Entitas.Interface;
using Fantasy.Gate;
namespace System.Gate;
public sealed class GameAccountFlagComponentDestroySystem : DestroySystem<GameAccountFlagComponent>
{
protected override void Destroy(GameAccountFlagComponent self)
{
if (self.AccountID != 0)
{
// 执行下线过程、并且要求在5分钟后完成缓存清理。也就是5分钟后会保存数据到数据库。
// 由于5分钟太长了、咱们测试的时候不方便测试也可以把这个时间改短一些比如10秒。
GameAccountHelper.Disconnect(self.Scene, self.AccountID, 1000 * 60 * 5).Coroutine();
self.AccountID = 0;
}
self.Account = null;
}
}

View File

@@ -0,0 +1,114 @@
using System.Gate.System;
using Fantasy;
using Fantasy.Async;
using Fantasy.Gate;
using Fantasy.Network;
namespace System.Gate;
public static class GameAccountHelper
{
/// <summary>
/// 从数据库中读取GameAccount
/// </summary>
/// <param name="scene"></param>
/// <param name="accountId">账号Id</param>
/// <returns></returns>
public static async FTask<GameAccount?> LoadDataBase(Scene scene, long accountId)
{
var account = await scene.World.DataBase.First<GameAccount>(d => d.Id == accountId);
if (account == null)
{
return null;
}
account.Deserialize(scene);
return account;
}
/// <summary>
/// 保存账号到数据库中
/// </summary>
/// <param name="self"></param>
public static async FTask SaveDataBase(this GameAccount self)
{
await self.Scene.World.DataBase.Save(self);
}
/// <summary>
/// 执行该账号的断开逻辑,不要非必要不要使用这个接口,这个接口是内部使用。
/// </summary>
/// <param name="self"></param>
public static async FTask Disconnect(this GameAccount self)
{
// 保存该账号信息到数据库中。
await SaveDataBase(self);
// 在缓存中移除自己并且执行自己的Dispose方法。
self.Scene.GetComponent<GameAccountManageComponent>().Remove(self.Id);
}
/// <summary>
/// 账号完整的断开逻辑,执行了这个接口后,该账号会完全下线。
/// </summary>
/// <param name="scene"></param>
/// <param name="accountId"></param>
/// <param name="timeOut"></param>
public static async FTask Disconnect(Scene scene, long accountId, int timeOut = 1000 * 60 * 3)
{
// 调用该方法有如下几种情况:
// 1、客户端主动断开如:退出游戏、切换账号、等。 客户端会主动发送一个协议给服务器通知服务器断开。
// 2、客户端断断线 客户端不会主动发送一个协议给服务器,是由服务器的心跳来检测是否断开了。
// 如果是心跳检测断开的Session我怎么能拿到当前的这个账号来进行下线处理呢
// 通过给当前的Session挂载一个组件当销毁这个Session时候呢也会销毁这个组件。
// 这样的话是不是可以在登录的时候给这个组件把AccountId存到这个组件呢
// 要检查当前缓存中是否存在该账号的数据
var gameAccountManageComponent = scene.GetComponent<GameAccountManageComponent>();
if (!gameAccountManageComponent.TryGet(accountId, out var account))
{
// 如果缓存中没有、那表示已经下线或者根本不存在该账号,应该在打印一个警告,因为正常的情况下是不会出现的。
Log.Warning($"GameAccountHelper Disconnect accountId : {accountId} not found");
return;
}
// 为了防止逻辑的错误,加一个警告来排除下
if (!scene.TryGetEntity<Session>(account.SessionRunTimeId, out var session))
{
// 如果没有找到对应的Session那只有一种可能就是当前的链接会话已经断开了一般的情况下也不会出现的所以咱们也要打印一个警告。
Log.Warning($"GameAccountHelper Disconnect accountId : {accountId} SessionRunTimeId : {account.SessionRunTimeId} not found");
return;
}
// 如果不存在定时任务的组件,那就添加并设置定时任务
if (account.IsTimeOutComponent())
{
// 如果已经存在了,那就表示当然已经有一个延时断开的任务了,那就不需要重复添加了
return;
}
// 立即下线处理
if (timeOut <= 0)
{
// 如果timeOut销毁或等会0的情况下执行立即下线。
await account.Disconnect();
return;
}
// 设置延迟下线
account.SetTimeout(timeOut, account.Disconnect);
}
/// <summary>
/// 获得GameAccountInfo
/// </summary>
/// <param name="self"></param>
/// <returns></returns>
public static GameAccountInfo GetGameAccountInfo(this GameAccount self)
{
// 其实可以不用每次都NEW一个新的GameAccountInfo
// 可以在当前账号下创建一个GameAccountInfo每次变动会提前通知这个GameAccountInfo
// 又或者每次调用该方法的时候,把值重新赋值一下。
return new GameAccountInfo()
{
CreateTime = self.CreateTime,
LoginTime = self.LoginTime
};
}
}

View File

@@ -0,0 +1,14 @@
using Fantasy.Entitas.Interface;
using Fantasy.Gate;
namespace System.Gate;
public sealed class GameAccountDestroySystem : DestroySystem<GameAccount>
{
protected override void Destroy(GameAccount self)
{
self.CreateTime = 0;
self.LoginTime = 0;
self.SessionRunTimeId = 0;
}
}

View File

@@ -0,0 +1,50 @@
using Fantasy.Entitas.Interface;
using Fantasy.Gate;
namespace System.Gate.System;
public sealed class GameAccountManageComponentDestroySystem : DestroySystem<GameAccountManageComponent>
{
protected override void Destroy(GameAccountManageComponent self)
{
foreach (var (_, gameAccount) in self.Accounts)
{
gameAccount.Dispose();
}
self.Accounts.Clear();
}
}
public static class GameAccountManageComponentSystem
{
public static void Add(this GameAccountManageComponent self, GameAccount account)
{
self.Accounts.Add(account.Id, account);
}
public static GameAccount? Get(this GameAccountManageComponent self, long accountId)
{
return self.Accounts.GetValueOrDefault(accountId);
}
public static bool TryGet(this GameAccountManageComponent self, long accountId, out GameAccount? account)
{
return self.Accounts.TryGetValue(accountId, out account);
}
public static void Remove(this GameAccountManageComponent self, long accountId, bool isDispose = true)
{
if (!self.Accounts.Remove(accountId, out var account))
{
return;
}
if (!isDispose)
{
return;
}
account.Dispose();
}
}

View File

@@ -0,0 +1,65 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using Fantasy.Entitas.Interface;
using Fantasy.Gate;
using Microsoft.IdentityModel.Tokens;
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
namespace System.Gate;
public sealed class GateJWTComponentAwakeSystem : AwakeSystem<GateJWTComponent>
{
protected override void Awake(GateJWTComponent self)
{
self.Awake();
}
}
public static class GateJWTComponentSystem
{
public static void Awake(this GateJWTComponent self)
{
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(self.PublicKeyPem), out _);
self.SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
// 创建 TokenValidationParameters 对象,用于配置验证参数
self.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = false, // 禁止令牌验证时间是否过期
ValidateIssuer = true, // 验证发行者
ValidateAudience = true, // 验证受众
ValidateIssuerSigningKey = true, // 验证签名密钥
ValidIssuer = "Fantasy", // 有效的发行者
ValidAudience = "Fantasy", // 有效的受众
IssuerSigningKey = new RsaSecurityKey(rsa) // RSA公钥作为签名密钥
};
}
public static bool ValidateToken(this GateJWTComponent self, string token, out JwtPayload payload)
{
payload = null;
try
{
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
jwtSecurityTokenHandler.ValidateToken(token, self.TokenValidationParameters, out _);
payload = jwtSecurityTokenHandler.ReadJwtToken(token).Payload;
return true;
}
catch (SecurityTokenInvalidAudienceException)
{
Console.WriteLine("验证受众失败!");
return false;
}
catch (SecurityTokenInvalidIssuerException)
{
Console.WriteLine("验证发行者失败!");
return false;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.IdentityModel.Tokens.Jwt;
using Fantasy;
using Fantasy.Gate;
namespace System.Gate;
public static class GateJWTHelper
{
/// <summary>
/// 验证令牌是否合法
/// </summary>
/// <param name="scene"></param>
/// <param name="token">ToKen</param>
/// <param name="accountId">如果验证成功会返回正常的AccountId</param>
/// <returns>如果是True表示验证成功False表示验证失败</returns>
public static bool ValidateToken(Scene scene, string token, out long accountId)
{
if (!ValidateToken(scene, token, out JwtPayload payload))
{
// 如果令牌验证失败表示当前令牌不合法、那就返回为false让上层处理。
accountId = 0;
return false;
}
// 如果不等于当前Scene的ConfigId的话把就表示该连接不应该连接到当前的Gate里。
// 所以理应把当前连接关闭掉。
var sceneId = Convert.ToInt64(payload["SceneId"]);
accountId = Convert.ToInt64(payload["aId"]);
return sceneId == scene.SceneConfigId;
}
private static bool ValidateToken(Scene scene, string token, out JwtPayload payload)
{
return scene.GetComponent<GateJWTComponent>().ValidateToken(token, out payload);
}
}