Minimal PS
This commit is contained in:
parent
228709bb6f
commit
c6680fcb43
35 changed files with 9875 additions and 229 deletions
|
@ -1,4 +1,7 @@
|
|||
[*.cs]
|
||||
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
|
||||
# IDE0290: Use primary constructor
|
||||
csharp_style_prefer_primary_constructors = false
|
14
RPG.GameCore/Excel/Attributes/ExcelTableAttribute.cs
Normal file
14
RPG.GameCore/Excel/Attributes/ExcelTableAttribute.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace RPG.GameCore.Excel.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
internal class ExcelTableAttribute : Attribute
|
||||
{
|
||||
public string Path { get; }
|
||||
public ExcelType Type { get; }
|
||||
|
||||
public ExcelTableAttribute(string path, ExcelType type)
|
||||
{
|
||||
Path = path;
|
||||
Type = type;
|
||||
}
|
||||
}
|
49
RPG.GameCore/Excel/AvatarExcelRow.cs
Normal file
49
RPG.GameCore/Excel/AvatarExcelRow.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using RPG.GameCore.Excel.Attributes;
|
||||
|
||||
namespace RPG.GameCore.Excel;
|
||||
|
||||
[ExcelTable("AvatarExcelTable.json", ExcelType.Avatar)]
|
||||
public class AvatarExcelRow : ExcelRow
|
||||
{
|
||||
public override uint Id => AvatarID;
|
||||
|
||||
public uint AvatarID { get; set; }
|
||||
public uint AdventurePlayerID { get; set; }
|
||||
public string AvatarVOTag { get; set; } = string.Empty;
|
||||
public uint Rarity { get; set; }
|
||||
public string JsonPath { get; set; } = string.Empty;
|
||||
public uint NatureID { get; set; }
|
||||
public string DamageType { get; set; } = string.Empty;
|
||||
public SPValue SPNeed { get; set; } = new();
|
||||
public uint ExpGroup { get; set; }
|
||||
public uint MaxPromotion { get; set; }
|
||||
public uint MaxRank { get; set; }
|
||||
public List<string> RankUpCostList { get; set; } = [];
|
||||
public uint MaxRankRepay { get; set; }
|
||||
public List<uint> SkillList { get; set; } = [];
|
||||
public string AvatarBaseType { get; set; } = string.Empty;
|
||||
public string DefaultAvatarImagePath { get; set; } = string.Empty;
|
||||
public string DefaultAvatarModelPath { get; set; } = string.Empty;
|
||||
public string DefaultAvatarHeadIconPath { get; set; } = string.Empty;
|
||||
public string DefaultAvatarHalfImagePath { get; set; } = string.Empty;
|
||||
public string AvatarSideIconPath { get; set; } = string.Empty;
|
||||
public string ActionAvatarHeadIconPath { get; set; } = string.Empty;
|
||||
public string DefaultAvatarQHeadIconPath { get; set; } = string.Empty;
|
||||
public string AvatarBaseTypeIconPath { get; set; } = string.Empty;
|
||||
public string AvatarDialogHalfImagePath { get; set; } = string.Empty;
|
||||
public string UltraSkillCutInPrefabPath { get; set; } = string.Empty;
|
||||
public string UIAvatarModelPath { get; set; } = string.Empty;
|
||||
public string ManikinJsonPath { get; set; } = string.Empty;
|
||||
public string AIPath { get; set; } = string.Empty;
|
||||
public string SkilltreePrefabPath { get; set; } = string.Empty;
|
||||
public bool Release { get; set; }
|
||||
public string SideAvatarHeadIconPath { get; set; } = string.Empty;
|
||||
public string WaitingAvatarHeadIconPath { get; set; } = string.Empty;
|
||||
public string AvatarCutinImgPath { get; set; } = string.Empty;
|
||||
public string AvatarCutinBgImgPath { get; set; } = string.Empty;
|
||||
|
||||
public class SPValue
|
||||
{
|
||||
public long RawValue { get; set; }
|
||||
}
|
||||
}
|
5
RPG.GameCore/Excel/ExcelRow.cs
Normal file
5
RPG.GameCore/Excel/ExcelRow.cs
Normal file
|
@ -0,0 +1,5 @@
|
|||
namespace RPG.GameCore.Excel;
|
||||
public abstract class ExcelRow
|
||||
{
|
||||
public abstract uint Id { get; }
|
||||
}
|
73
RPG.GameCore/Excel/ExcelTables.cs
Normal file
73
RPG.GameCore/Excel/ExcelTables.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RPG.GameCore.Excel.Attributes;
|
||||
|
||||
namespace RPG.GameCore.Excel;
|
||||
public class ExcelTables
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private ImmutableDictionary<ExcelType, ImmutableArray<ExcelRow>>? _tables;
|
||||
|
||||
public ExcelTables(ILogger<ExcelTables> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public TExcelRow? GetExcelRow<TExcelRow>(ExcelType type, int id) where TExcelRow : ExcelRow
|
||||
{
|
||||
if (_tables == null) throw new InvalidOperationException("GetExcelRow called when ExcelTables not loaded.");
|
||||
|
||||
if (_tables.TryGetValue(type, out ImmutableArray<ExcelRow> rows))
|
||||
{
|
||||
return rows.SingleOrDefault(row => row.Id == id) as TExcelRow;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"GetExcelRow: table for excel type not found {type}");
|
||||
}
|
||||
|
||||
public IEnumerable<ExcelRow> GetAllRows(ExcelType type)
|
||||
{
|
||||
if (_tables == null) throw new InvalidOperationException("GetAllRows called when ExcelTables not loaded.");
|
||||
|
||||
if (_tables.TryGetValue(type, out ImmutableArray<ExcelRow> rows))
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"GetAllRows: table for excel type not found {type}");
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
ImmutableDictionary<ExcelType, ImmutableArray<ExcelRow>>.Builder tables = ImmutableDictionary.CreateBuilder<ExcelType, ImmutableArray<ExcelRow>>();
|
||||
|
||||
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(type => type.GetCustomAttribute<ExcelTableAttribute>() != null);
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
ExcelTableAttribute attribute = type.GetCustomAttribute<ExcelTableAttribute>()!;
|
||||
|
||||
// TODO: asset provider
|
||||
|
||||
JsonDocument tableJson = JsonDocument.Parse(File.ReadAllText("data/excel/" + attribute.Path));
|
||||
ImmutableArray<ExcelRow>.Builder rows = ImmutableArray.CreateBuilder<ExcelRow>();
|
||||
|
||||
foreach (JsonProperty property in tableJson.RootElement.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind != JsonValueKind.Object)
|
||||
throw new ArgumentException($"Failed to load excel: expected an object, got {property.Value.ValueKind}");
|
||||
|
||||
ExcelRow row = (property.Value.Deserialize(type) as ExcelRow)!;
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
tables.Add(attribute.Type, rows.ToImmutable());
|
||||
}
|
||||
|
||||
_tables = tables.ToImmutable();
|
||||
_logger.LogInformation("Loaded {count} excel tables", _tables.Count);
|
||||
}
|
||||
}
|
6
RPG.GameCore/Excel/ExcelType.cs
Normal file
6
RPG.GameCore/Excel/ExcelType.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace RPG.GameCore.Excel;
|
||||
public enum ExcelType
|
||||
{
|
||||
Avatar,
|
||||
MainMission
|
||||
}
|
34
RPG.GameCore/Excel/MainMissionRow.cs
Normal file
34
RPG.GameCore/Excel/MainMissionRow.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using RPG.GameCore.Excel.Attributes;
|
||||
|
||||
namespace RPG.GameCore.Excel;
|
||||
|
||||
[ExcelTable("MainMissionExcelTable.json", ExcelType.MainMission)]
|
||||
public class MainMissionRow : ExcelRow
|
||||
{
|
||||
public override uint Id => MainMissionID;
|
||||
|
||||
public uint MainMissionID { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public bool IsLoop { get; set; }
|
||||
public List<uint> NextMainMissionList { get; set; } = [];
|
||||
public string TakeType { get; set; } = string.Empty;
|
||||
public uint TakeParamInt1 { get; set; }
|
||||
public List<uint> TakeParamIntList { get; set; } = [];
|
||||
public string BeginType { get; set; } = string.Empty;
|
||||
public uint BeginParamInt1 { get; set; }
|
||||
public List<uint> BeginParamIntList { get; set; } = [];
|
||||
public List<uint> StartSubMissionList { get; set; } = [];
|
||||
public List<uint> FinishSubMissionList { get; set; } = [];
|
||||
public uint NextTrackMainMission { get; set; }
|
||||
public uint TrackWeight { get; set; }
|
||||
public bool IsShowStartHint { get; set; }
|
||||
public bool IsShowFinishHint { get; set; }
|
||||
public int RewardID { get; set; }
|
||||
public int DisplayRewardID { get; set; }
|
||||
}
|
|
@ -10,4 +10,17 @@
|
|||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="data\excel\AvatarExcelTable.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="data\excel\MainMissionExcelTable.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
2403
RPG.GameCore/data/excel/AvatarExcelTable.json
Normal file
2403
RPG.GameCore/data/excel/AvatarExcelTable.json
Normal file
File diff suppressed because it is too large
Load diff
6472
RPG.GameCore/data/excel/MainMissionExcelTable.json
Normal file
6472
RPG.GameCore/data/excel/MainMissionExcelTable.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,218 +1,218 @@
|
|||
namespace RPG.Network.Proto;
|
||||
public static class CmdType
|
||||
public enum 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;
|
||||
CmdTypeNone = 0,
|
||||
CmdPlayerLoginCsReq = 1,
|
||||
CmdPlayerLoginScRsp = 2,
|
||||
CmdPlayerLogoutCsReq = 3,
|
||||
CmdPlayerLogoutScRsp = 4,
|
||||
CmdPlayerGetTokenCsReq = 5,
|
||||
CmdPlayerGetTokenScRsp = 6,
|
||||
CmdPlayerKeepAliveNotify = 7,
|
||||
CmdGmTalkScNotify = 8,
|
||||
CmdPlayerKickOutScNotify = 9,
|
||||
CmdGmTalkCsReq = 10,
|
||||
CmdGmTalkScRsp = 11,
|
||||
CmdGetStaminaExchangeCsReq = 12,
|
||||
CmdGetStaminaExchangeScRsp = 13,
|
||||
CmdExchangeStaminaCsReq = 14,
|
||||
CmdExchangeStaminaScRsp = 15,
|
||||
CmdGetAuthkeyCsReq = 16,
|
||||
CmdGetAuthkeyScRsp = 17,
|
||||
CmdRegionStopScNotify = 18,
|
||||
CmdAntiAddictScNotify = 19,
|
||||
CmdSetNicknameCsReq = 20,
|
||||
CmdSetNicknameScRsp = 21,
|
||||
CmdGetLevelRewardTakenListCsReq = 22,
|
||||
CmdGetLevelRewardTakenListScRsp = 23,
|
||||
CmdGetLevelRewardCsReq = 24,
|
||||
CmdGetLevelRewardScRsp = 25,
|
||||
CmdSyncTimeCsReq = 26,
|
||||
CmdSyncTimeScRsp = 27,
|
||||
CmdSetLanguageCsReq = 28,
|
||||
CmdSetLanguageScRsp = 29,
|
||||
CmdServerAnnounceNotify = 30,
|
||||
CmdPVEBattleResultCsReq = 101,
|
||||
CmdPVEBattleResultScRsp = 102,
|
||||
CmdQuitBattleCsReq = 103,
|
||||
CmdQuitBattleScRsp = 104,
|
||||
CmdGetCurBattleInfoCsReq = 105,
|
||||
CmdGetCurBattleInfoScRsp = 106,
|
||||
CmdSyncClientResVersionCsReq = 107,
|
||||
CmdSyncClientResVersionScRsp = 108,
|
||||
CmdGetStageDataCsReq = 201,
|
||||
CmdGetStageDataScRsp = 202,
|
||||
CmdStageBeginCsReq = 203,
|
||||
CmdStageBeginScRsp = 204,
|
||||
CmdGetAvatarDataCsReq = 301,
|
||||
CmdGetAvatarDataScRsp = 302,
|
||||
CmdAvatarExpUpCsReq = 303,
|
||||
CmdAvatarExpUpScRsp = 304,
|
||||
CmdUnlockSkilltreeCsReq = 305,
|
||||
CmdUnlockSkilltreeScRsp = 306,
|
||||
CmdPromoteAvatarCsReq = 307,
|
||||
CmdPromoteAvatarScRsp = 308,
|
||||
CmdDressAvatarCsReq = 309,
|
||||
CmdDressAvatarScRsp = 310,
|
||||
CmdTakeOffEquipmentCsReq = 311,
|
||||
CmdTakeOffEquipmentScRsp = 312,
|
||||
CmdAddAvatarScNotify = 313,
|
||||
CmdGetWaypointCsReq = 401,
|
||||
CmdGetWaypointScRsp = 402,
|
||||
CmdSetCurWaypointCsReq = 403,
|
||||
CmdSetCurWaypointScRsp = 404,
|
||||
CmdGetChapterCsReq = 405,
|
||||
CmdGetChapterScRsp = 406,
|
||||
CmdWaypointShowNewCsNotify = 407,
|
||||
CmdTakeChapterRewardCsReq = 408,
|
||||
CmdTakeChapterRewardScRsp = 409,
|
||||
CmdGetBagCsReq = 501,
|
||||
CmdGetBagScRsp = 502,
|
||||
CmdPromoteEquipmentCsReq = 503,
|
||||
CmdPromoteEquipmentScRsp = 504,
|
||||
CmdLockEquipmentCsReq = 505,
|
||||
CmdLockEquipmentScRsp = 506,
|
||||
CmdUseItemCsReq = 507,
|
||||
CmdUseItemScRsp = 508,
|
||||
CmdRankUpEquipmentCsReq = 509,
|
||||
CmdRankUpEquipmentScRsp = 510,
|
||||
CmdExpUpEquipmentCsReq = 511,
|
||||
CmdExpUpEquipmentScRsp = 512,
|
||||
CmdUseItemFoodCsReq = 513,
|
||||
CmdUseItemFoodScRsp = 514,
|
||||
CmdComposeItemCsReq = 515,
|
||||
CmdComposeItemScRsp = 516,
|
||||
CmdPlayerSyncScNotify = 601,
|
||||
CmdGetStageLineupCsReq = 701,
|
||||
CmdGetStageLineupScRsp = 702,
|
||||
CmdGetCurLineupDataCsReq = 703,
|
||||
CmdGetCurLineupDataScRsp = 704,
|
||||
CmdJoinLineupCsReq = 705,
|
||||
CmdJoinLineupScRsp = 706,
|
||||
CmdQuitLineupCsReq = 707,
|
||||
CmdQuitLineupScRsp = 708,
|
||||
CmdSwapLineupCsReq = 709,
|
||||
CmdSwapLineupScRsp = 710,
|
||||
CmdSyncLineupNotify = 711,
|
||||
CmdGetLineupAvatarDataCsReq = 712,
|
||||
CmdGetLineupAvatarDataScRsp = 713,
|
||||
CmdChangeLineupLeaderCsReq = 714,
|
||||
CmdChangeLineupLeaderScRsp = 715,
|
||||
CmdSwitchLineupIndexCsReq = 716,
|
||||
CmdSwitchLineupIndexScRsp = 717,
|
||||
CmdSetLineupNameCsReq = 718,
|
||||
CmdSetLineupNameScRsp = 719,
|
||||
CmdGetAllLineupDataCsReq = 720,
|
||||
CmdGetAllLineupDataScRsp = 721,
|
||||
CmdVirtualLineupDestroyNotify = 722,
|
||||
CmdGetMailCsReq = 801,
|
||||
CmdGetMailScRsp = 802,
|
||||
CmdMarkReadMailCsReq = 803,
|
||||
CmdMarkReadMailScRsp = 804,
|
||||
CmdDelMailCsReq = 805,
|
||||
CmdDelMailScRsp = 806,
|
||||
CmdTakeMailAttachmentCsReq = 807,
|
||||
CmdTakeMailAttachmentScRsp = 808,
|
||||
CmdNewMailScNotify = 809,
|
||||
CmdGetQuestDataCsReq = 901,
|
||||
CmdGetQuestDataScRsp = 902,
|
||||
CmdTakeQuestRewardCsReq = 903,
|
||||
CmdTakeQuestRewardScRsp = 904,
|
||||
CmdGetMazeCsReq = 1001,
|
||||
CmdGetMazeScRsp = 1002,
|
||||
CmdChooseMazeSeriesCsReq = 1003,
|
||||
CmdChooseMazeSeriesScRsp = 1004,
|
||||
CmdChooseMazeAbilityCsReq = 1005,
|
||||
CmdChooseMazeAbilityScRsp = 1006,
|
||||
CmdEnterMazeCsReq = 1007,
|
||||
CmdEnterMazeScRsp = 1008,
|
||||
CmdMazeBuffScNotify = 1011,
|
||||
CmdCastMazeSkillCsReq = 1012,
|
||||
CmdCastMazeSkillScRsp = 1013,
|
||||
CmdMazePlaneEventScNotify = 1014,
|
||||
CmdEnterMazeByServerScNotify = 1015,
|
||||
CmdGetMazeMapInfoCsReq = 1016,
|
||||
CmdGetMazeMapInfoScRsp = 1017,
|
||||
CmdFinishPlotCsReq = 1101,
|
||||
CmdFinishPlotScRsp = 1102,
|
||||
CmdGetMissionDataCsReq = 1201,
|
||||
CmdGetMissionDataScRsp = 1202,
|
||||
CmdFinishTalkMissionCsReq = 1203,
|
||||
CmdFinishTalkMissionScRsp = 1204,
|
||||
CmdMissionRewardScNotify = 1205,
|
||||
CmdSyncTaskCsReq = 1206,
|
||||
CmdSyncTaskScRsp = 1207,
|
||||
CmdDailyTaskDataScNotify = 1208,
|
||||
CmdTakeDailyTaskExtraRewardCsReq = 1209,
|
||||
CmdTakeDailyTaskExtraRewardScRsp = 1210,
|
||||
CmdDailyTaskRewardScNotify = 1211,
|
||||
CmdMissionGroupWarnScNotify = 1212,
|
||||
CmdFinishCosumeItemMissionCsReq = 1213,
|
||||
CmdFinishCosumeItemMissionScRsp = 1214,
|
||||
CmdEnterAdventureCsReq = 1301,
|
||||
CmdEnterAdventureScRsp = 1302,
|
||||
CmdSceneEntityMoveCsReq = 1401,
|
||||
CmdSceneEntityMoveScRsp = 1402,
|
||||
CmdInteractPropCsReq = 1403,
|
||||
CmdInteractPropScRsp = 1404,
|
||||
CmdSceneCastSkillCsReq = 1405,
|
||||
CmdSceneCastSkillScRsp = 1406,
|
||||
CmdGetCurSceneInfoCsReq = 1407,
|
||||
CmdGetCurSceneInfoScRsp = 1408,
|
||||
CmdSceneEntityUpdateScNotify = 1409,
|
||||
CmdSceneEntityDisappearScNotify = 1410,
|
||||
CmdSceneEntityMoveScNotify = 1411,
|
||||
CmdWaitCustomStringCsReq = 1412,
|
||||
CmdWaitCustomStringScRsp = 1413,
|
||||
CmdSpringTransferCsReq = 1414,
|
||||
CmdSpringTransferScRsp = 1415,
|
||||
CmdUpdateBuffScNotify = 1416,
|
||||
CmdDelBuffScNotify = 1417,
|
||||
CmdSpringRefreshCsReq = 1418,
|
||||
CmdSpringRefreshScRsp = 1419,
|
||||
CmdLastSpringRefreshTimeNotify = 1420,
|
||||
CmdReturnLastTownCsReq = 1421,
|
||||
CmdReturnLastTownScRsp = 1422,
|
||||
CmdSceneEnterStageCsReq = 1423,
|
||||
CmdSceneEnterStageScRsp = 1424,
|
||||
CmdEnterSectionCsReq = 1427,
|
||||
CmdEnterSectionScRsp = 1428,
|
||||
CmdSetCurInteractEntityCsReq = 1431,
|
||||
CmdSetCurInteractEntityScRsp = 1432,
|
||||
CmdRecoverAllLineupCsReq = 1433,
|
||||
CmdRecoverAllLineupScRsp = 1434,
|
||||
CmdSavePointsInfoNotify = 1435,
|
||||
CmdStartCocoonStageCsReq = 1436,
|
||||
CmdStartCocoonStageScRsp = 1437,
|
||||
CmdEntityBindPropCsReq = 1438,
|
||||
CmdEntityBindPropScRsp = 1439,
|
||||
CmdSetClientPausedCsReq = 1440,
|
||||
CmdSetClientPausedScRsp = 1441,
|
||||
CmdPropBeHitCsReq = 1442,
|
||||
CmdPropBeHitScRsp = 1443,
|
||||
CmdGetShopListCsReq = 1501,
|
||||
CmdGetShopListScRsp = 1502,
|
||||
CmdBuyGoodsCsReq = 1503,
|
||||
CmdBuyGoodsScRsp = 1504,
|
||||
CmdGetTutorialCsReq = 1601,
|
||||
CmdGetTutorialScRsp = 1602,
|
||||
CmdGetTutorialGuideCsReq = 1603,
|
||||
CmdGetTutorialGuideScRsp = 1604,
|
||||
CmdUnlockTutorialCsReq = 1605,
|
||||
CmdUnlockTutorialScRsp = 1606,
|
||||
CmdUnlockTutorialGuideCsReq = 1607,
|
||||
CmdUnlockTutorialGuideScRsp = 1608,
|
||||
CmdFinishTutorialCsReq = 1609,
|
||||
CmdFinishTutorialScRsp = 1610,
|
||||
CmdFinishTutorialGuideCsReq = 1611,
|
||||
CmdFinishTutorialGuideScRsp = 1612,
|
||||
CmdGetChallengeCsReq = 1701,
|
||||
CmdGetChallengeScRsp = 1702,
|
||||
CmdStartChallengeCsReq = 1703,
|
||||
CmdStartChallengeScRsp = 1704,
|
||||
CmdLeaveChallengeCsReq = 1705,
|
||||
CmdLeaveChallengeScRsp = 1706,
|
||||
CmdChallengeSettleNotify = 1707,
|
||||
CmdFinishChallengeCsReq = 1708,
|
||||
CmdFinishChallengeScRsp = 1709
|
||||
}
|
|
@ -4,7 +4,7 @@ using RPG.Services.Core.Network;
|
|||
using RPG.Services.Core.Network.Command;
|
||||
|
||||
namespace RPG.Services.Core.Session;
|
||||
public abstract class RPGSession
|
||||
public abstract class RPGSession : IDisposable
|
||||
{
|
||||
private readonly ServiceBox _serviceBox;
|
||||
|
||||
|
@ -44,4 +44,6 @@ public abstract class RPGSession
|
|||
Reason = reason
|
||||
});
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,9 @@ public class SessionManager
|
|||
|
||||
public void Remove(RPGSession session)
|
||||
{
|
||||
_ = _sessions.TryRemove(session.SessionId, out _);
|
||||
if (_sessions.TryRemove(session.SessionId, out _))
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RPG.Services.Gameserver.Modules;
|
||||
|
||||
namespace RPG.Services.Gameserver.Extensions;
|
||||
internal static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddModules(this IServiceCollection services)
|
||||
{
|
||||
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(type => type.IsAssignableTo(typeof(BaseModule)) && !type.IsAbstract);
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
services.AddScoped(type);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
26
RPG.Services.Gameserver/Modules/AdventureModule.cs
Normal file
26
RPG.Services.Gameserver/Modules/AdventureModule.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class AdventureModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetCurSceneInfoCsReq)]
|
||||
public Task OnCmdGetCurSceneInfoCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetCurSceneInfoScRsp, new GetCurSceneInfoScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
Scene = new SceneInfo
|
||||
{
|
||||
PlaneId = 20121,
|
||||
FloorId = 20121001,
|
||||
EntryId = 2012101,
|
||||
EntityList = { },
|
||||
LeaderEntityId = 0
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using RPG.Network.Proto;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
internal class OnCommandAttribute : Attribute
|
||||
{
|
||||
public CmdType CmdType { get; }
|
||||
|
||||
public OnCommandAttribute(CmdType cmdType)
|
||||
{
|
||||
CmdType = cmdType;
|
||||
}
|
||||
}
|
43
RPG.Services.Gameserver/Modules/AvatarModule.cs
Normal file
43
RPG.Services.Gameserver/Modules/AvatarModule.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using RPG.GameCore.Excel;
|
||||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class AvatarModule : BaseModule
|
||||
{
|
||||
private readonly ExcelTables _excelTables;
|
||||
|
||||
public AvatarModule(ExcelTables excelTables)
|
||||
{
|
||||
_excelTables = excelTables;
|
||||
}
|
||||
|
||||
[OnCommand(CmdType.CmdGetAvatarDataCsReq)]
|
||||
public Task OnCmdGetAvatarDataCsReq(PlayerSession session, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
GetAvatarDataCsReq req = GetAvatarDataCsReq.Parser.ParseFrom(body.Span);
|
||||
|
||||
GetAvatarDataScRsp rsp = new()
|
||||
{
|
||||
IsAll = req.IsGetAll
|
||||
};
|
||||
|
||||
foreach (ExcelRow row in _excelTables.GetAllRows(ExcelType.Avatar))
|
||||
{
|
||||
if (row is AvatarExcelRow avatarRow)
|
||||
{
|
||||
if (avatarRow.AvatarID >= 9000) continue;
|
||||
|
||||
rsp.AvatarList.Add(new Avatar
|
||||
{
|
||||
AvatarId = avatarRow.AvatarID,
|
||||
Level = 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Send(session, CmdType.CmdGetAvatarDataScRsp, rsp);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
17
RPG.Services.Gameserver/Modules/BaseModule.cs
Normal file
17
RPG.Services.Gameserver/Modules/BaseModule.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using Google.Protobuf;
|
||||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal abstract class BaseModule
|
||||
{
|
||||
protected static void Send<TBody>(PlayerSession session, CmdType cmdType, TBody body) where TBody : IMessage<TBody>
|
||||
{
|
||||
session.SendToService(RPGServiceType.Gateserver, ServiceCommandType.ForwardGameMessage, new CmdForwardGameMessage
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
CmdType = (ushort)cmdType,
|
||||
Payload = body.ToByteString()
|
||||
});
|
||||
}
|
||||
}
|
19
RPG.Services.Gameserver/Modules/ChallengeModule.cs
Normal file
19
RPG.Services.Gameserver/Modules/ChallengeModule.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class ChallengeModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetChallengeCsReq)]
|
||||
public Task OnCmdGetChallengeCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetChallengeScRsp, new GetChallengeScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
ChallengeList = { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
20
RPG.Services.Gameserver/Modules/InventoryModule.cs
Normal file
20
RPG.Services.Gameserver/Modules/InventoryModule.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class InventoryModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetBagCsReq)]
|
||||
public Task OnCmdGetBagCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetBagScRsp, new GetBagScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
EquipmentList = { },
|
||||
MaterialList = { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
27
RPG.Services.Gameserver/Modules/LoginModule.cs
Normal file
27
RPG.Services.Gameserver/Modules/LoginModule.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class LoginModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdPlayerLoginCsReq)]
|
||||
public Task OnCmdPlayerLoginCsReq(PlayerSession session, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
PlayerLoginCsReq req = PlayerLoginCsReq.Parser.ParseFrom(body.Span);
|
||||
Send(session, CmdType.CmdPlayerLoginScRsp, new PlayerLoginScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
LoginRandom = req.LoginRandom,
|
||||
Stamina = 160,
|
||||
ServerTimestampMs = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds(),
|
||||
BasicInfo = new()
|
||||
{
|
||||
Level = 5,
|
||||
Nickname = "ReversedRooms"
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
18
RPG.Services.Gameserver/Modules/MailModule.cs
Normal file
18
RPG.Services.Gameserver/Modules/MailModule.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class MailModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetMailCsReq)]
|
||||
public Task OnCmdGetMailCsReq(PlayerSession session, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
Send(session, CmdType.CmdGetMailScRsp, new GetMailScRsp
|
||||
{
|
||||
Retcode = 0
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
29
RPG.Services.Gameserver/Modules/MissionModule.cs
Normal file
29
RPG.Services.Gameserver/Modules/MissionModule.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using RPG.GameCore.Excel;
|
||||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class MissionModule : BaseModule
|
||||
{
|
||||
private readonly ExcelTables _excelTables;
|
||||
|
||||
public MissionModule(ExcelTables excelTables)
|
||||
{
|
||||
_excelTables = excelTables;
|
||||
}
|
||||
|
||||
[OnCommand(CmdType.CmdGetMissionDataCsReq)]
|
||||
public Task OnCmdGetMissionDataCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
GetMissionDataScRsp rsp = new();
|
||||
|
||||
foreach (ExcelRow row in _excelTables.GetAllRows(ExcelType.MainMission))
|
||||
{
|
||||
rsp.FinishedMainMissionIdList.Add(row.Id);
|
||||
}
|
||||
|
||||
Send(session, CmdType.CmdGetMissionDataScRsp, rsp);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
75
RPG.Services.Gameserver/Modules/ModuleManager.cs
Normal file
75
RPG.Services.Gameserver/Modules/ModuleManager.cs
Normal file
|
@ -0,0 +1,75 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class ModuleManager
|
||||
{
|
||||
private delegate Task ReqHandler(PlayerSession session, IServiceProvider serviceProvider, ReadOnlyMemory<byte> body);
|
||||
private static readonly ImmutableDictionary<CmdType, ReqHandler> s_handlers;
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
static ModuleManager()
|
||||
{
|
||||
s_handlers = MapHandlers();
|
||||
}
|
||||
|
||||
public ModuleManager(IServiceProvider serviceProvider, ILogger<ModuleManager> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(PlayerSession session, CmdType cmdType, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
if (s_handlers.TryGetValue(cmdType, out var handler))
|
||||
{
|
||||
await handler(session, _serviceProvider, body);
|
||||
_logger.LogInformation("Successfully handled command of type {cmdType}", cmdType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Handler for command of type {cmdType} not defined!", cmdType);
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<CmdType, ReqHandler> MapHandlers()
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<CmdType, ReqHandler>();
|
||||
|
||||
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(type => type.IsAssignableTo(typeof(BaseModule)) && !type.IsAbstract);
|
||||
|
||||
MethodInfo getServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod("GetRequiredService", [typeof(IServiceProvider)])!;
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
IEnumerable<MethodInfo> methods = type.GetMethods()
|
||||
.Where(method => method.GetCustomAttribute<OnCommandAttribute>() != null);
|
||||
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
OnCommandAttribute attribute = method.GetCustomAttribute<OnCommandAttribute>()!;
|
||||
|
||||
ParameterExpression sessionParam = Expression.Parameter(typeof(PlayerSession));
|
||||
ParameterExpression serviceProviderParam = Expression.Parameter(typeof(IServiceProvider));
|
||||
ParameterExpression bodyParam = Expression.Parameter(typeof(ReadOnlyMemory<byte>));
|
||||
|
||||
MethodCallExpression getServiceCall = Expression.Call(getServiceMethod.MakeGenericMethod(type), serviceProviderParam);
|
||||
MethodCallExpression handlerCall = Expression.Call(getServiceCall, method, sessionParam, bodyParam);
|
||||
|
||||
Expression<ReqHandler> lambda = Expression.Lambda<ReqHandler>(handlerCall, sessionParam, serviceProviderParam, bodyParam);
|
||||
builder.Add(attribute.CmdType, lambda.Compile());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
35
RPG.Services.Gameserver/Modules/PlayerModule.cs
Normal file
35
RPG.Services.Gameserver/Modules/PlayerModule.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class PlayerModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdSyncTimeCsReq)]
|
||||
public Task OnCmdSyncTimeCsReq(PlayerSession session, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
SyncTimeCsReq req = SyncTimeCsReq.Parser.ParseFrom(body.Span);
|
||||
|
||||
// TODO: TimeManager
|
||||
|
||||
Send(session, CmdType.CmdSyncTimeScRsp, new SyncTimeScRsp
|
||||
{
|
||||
ServerTimeMs = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds(),
|
||||
ClientTimeMs = req.ClientTimeMs,
|
||||
Retcode = 0
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[OnCommand(CmdType.CmdGetStaminaExchangeCsReq)]
|
||||
public Task OnCmdGetStaminaExchangeCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetStaminaExchangeScRsp, new GetStaminaExchangeScRsp
|
||||
{
|
||||
Retcode = 0
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
19
RPG.Services.Gameserver/Modules/QuestModule.cs
Normal file
19
RPG.Services.Gameserver/Modules/QuestModule.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class QuestModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetQuestDataCsReq)]
|
||||
public Task OnCmdGetQuestDataCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetQuestDataScRsp, new GetQuestDataScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
QuestList = { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
18
RPG.Services.Gameserver/Modules/ShopModule.cs
Normal file
18
RPG.Services.Gameserver/Modules/ShopModule.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class ShopModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetShopListCsReq)]
|
||||
public Task OnCmdGetShopListCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetShopListScRsp, new GetShopListScRsp
|
||||
{
|
||||
Retcode = 0
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
85
RPG.Services.Gameserver/Modules/TeamModule.cs
Normal file
85
RPG.Services.Gameserver/Modules/TeamModule.cs
Normal file
|
@ -0,0 +1,85 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class TeamModule : BaseModule
|
||||
{
|
||||
private static readonly uint[] StartingLineup = [1007, 1102, 1101, 1003];
|
||||
|
||||
[OnCommand(CmdType.CmdChangeLineupLeaderCsReq)]
|
||||
public Task OnCmdChangeLineupLeaderCsReq(PlayerSession session, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
ChangeLineupLeaderCsReq req = ChangeLineupLeaderCsReq.Parser.ParseFrom(body.Span);
|
||||
|
||||
Send(session, CmdType.CmdChangeLineupLeaderScRsp, new ChangeLineupLeaderScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
Slot = req.Slot
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[OnCommand(CmdType.CmdGetAllLineupDataCsReq)]
|
||||
public Task OnCmdGetAllLineupDataCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
GetAllLineupDataScRsp rsp = new();
|
||||
rsp.LineupList.Add(new LineupInfo
|
||||
{
|
||||
Name = "Test Squad",
|
||||
LeaderSlot = 0,
|
||||
ExtraLineupType = ExtraLineupType.LineupNone,
|
||||
Mp = 3
|
||||
});
|
||||
|
||||
for (uint i = 0; i < StartingLineup.Length; i++)
|
||||
{
|
||||
rsp.LineupList[0].AvatarList.Add(new LineupAvatar
|
||||
{
|
||||
Id = StartingLineup[i],
|
||||
Satiety = 100,
|
||||
Sp = 10000,
|
||||
Hp = 10000,
|
||||
Slot = i,
|
||||
AvatarType = AvatarType.AvatarFormalType
|
||||
});
|
||||
}
|
||||
|
||||
Send(session, CmdType.CmdGetAllLineupDataScRsp, rsp);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[OnCommand(CmdType.CmdGetCurLineupDataCsReq)]
|
||||
public Task OnCmdGetCurLineupDataCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
GetCurLineupDataScRsp rsp = new()
|
||||
{
|
||||
Lineup = new LineupInfo
|
||||
{
|
||||
Name = "Test Squad",
|
||||
LeaderSlot = 0,
|
||||
ExtraLineupType = ExtraLineupType.LineupNone,
|
||||
Mp = 3
|
||||
}
|
||||
};
|
||||
|
||||
for (uint i = 0; i < StartingLineup.Length; i++)
|
||||
{
|
||||
rsp.Lineup.AvatarList.Add(new LineupAvatar
|
||||
{
|
||||
Id = StartingLineup[i],
|
||||
Satiety = 100,
|
||||
Sp = 10000,
|
||||
Hp = 10000,
|
||||
Slot = i,
|
||||
AvatarType = AvatarType.AvatarFormalType
|
||||
});
|
||||
}
|
||||
|
||||
Send(session, CmdType.CmdGetCurLineupDataScRsp, rsp);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
31
RPG.Services.Gameserver/Modules/TutorialModule.cs
Normal file
31
RPG.Services.Gameserver/Modules/TutorialModule.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using RPG.Network.Proto;
|
||||
using RPG.Services.Gameserver.Modules.Attributes;
|
||||
using RPG.Services.Gameserver.Session;
|
||||
|
||||
namespace RPG.Services.Gameserver.Modules;
|
||||
internal class TutorialModule : BaseModule
|
||||
{
|
||||
[OnCommand(CmdType.CmdGetTutorialCsReq)]
|
||||
public Task OnCmdGetTutorialCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetTutorialScRsp, new GetTutorialScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
TutorialList = { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[OnCommand(CmdType.CmdGetTutorialGuideCsReq)]
|
||||
public Task OnCmdGetTutorialGuideCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
|
||||
{
|
||||
Send(session, CmdType.CmdGetTutorialGuideScRsp, new GetTutorialGuideScRsp
|
||||
{
|
||||
Retcode = 0,
|
||||
TutorialGuideList = { }
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
|
@ -40,4 +40,28 @@ internal class GameserverCommandHandler : ServiceCommandHandler
|
|||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[ServiceCommand(ServiceCommandType.UnbindContainer)]
|
||||
public Task OnCmdUnbindContainer(ServiceCommand command)
|
||||
{
|
||||
CmdUnbindContainer cmdUnbindContainer = CmdUnbindContainer.Parser.ParseFrom(command.Body.Span);
|
||||
|
||||
if (_sessionManager.TryGet(cmdUnbindContainer.SessionId, out PlayerSession? session))
|
||||
{
|
||||
_sessionManager.Remove(session);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[ServiceCommand(ServiceCommandType.ForwardGameMessage)]
|
||||
public async Task OnCmdForwardGameMessage(ServiceCommand command)
|
||||
{
|
||||
CmdForwardGameMessage cmd = CmdForwardGameMessage.Parser.ParseFrom(command.Body.Span);
|
||||
|
||||
if (_sessionManager.TryGet(cmd.SessionId, out PlayerSession? session))
|
||||
{
|
||||
await session.HandleGameCommand((ushort)cmd.CmdType, cmd.Payload.Memory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using RPG.GameCore.Excel;
|
||||
using RPG.Services.Core.Extensions;
|
||||
using RPG.Services.Gameserver.Extensions;
|
||||
using RPG.Services.Gameserver.Modules;
|
||||
using RPG.Services.Gameserver.Network.Command;
|
||||
|
||||
namespace RPG.Services.Gameserver;
|
||||
|
@ -13,6 +17,9 @@ internal static class Program
|
|||
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.SetupRPGService<RPGGameserver, GameserverCommandHandler>();
|
||||
builder.Services.AddModules()
|
||||
.AddScoped<ModuleManager>()
|
||||
.AddSingleton<ExcelTables>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
using RPG.Services.Core;
|
||||
using RPG.GameCore.Excel;
|
||||
using RPG.Services.Core;
|
||||
|
||||
namespace RPG.Services.Gameserver;
|
||||
internal class RPGGameserver : RPGServiceBase
|
||||
{
|
||||
public RPGGameserver(ServiceManager serviceManager) : base(serviceManager)
|
||||
private readonly ExcelTables _excelTables;
|
||||
|
||||
public RPGGameserver(ServiceManager serviceManager, ExcelTables excelTables) : base(serviceManager)
|
||||
{
|
||||
// RPGGameserver.
|
||||
_excelTables = excelTables;
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_excelTables.Load();
|
||||
await base.StartAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
using RPG.Services.Core.Network;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using RPG.Network.Proto;
|
||||
using RPG.Services.Core.Network;
|
||||
using RPG.Services.Core.Session;
|
||||
using RPG.Services.Gameserver.Modules;
|
||||
|
||||
namespace RPG.Services.Gameserver.Session;
|
||||
internal class PlayerSession : RPGSession
|
||||
{
|
||||
public PlayerSession(ulong sessionId, ServiceBox serviceBox) : base(sessionId, serviceBox)
|
||||
private readonly IServiceScope _scope;
|
||||
private readonly ModuleManager _moduleManager;
|
||||
|
||||
public PlayerSession(ulong sessionId, ServiceBox serviceBox, IServiceScopeFactory scopeFactory) : base(sessionId, serviceBox)
|
||||
{
|
||||
_scope = scopeFactory.CreateScope();
|
||||
_moduleManager = _scope.ServiceProvider.GetRequiredService<ModuleManager>();
|
||||
}
|
||||
|
||||
public async Task HandleGameCommand(ushort cmdType, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
await _moduleManager.HandleAsync(this, (CmdType)cmdType, body);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_scope.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,18 @@ internal class GateserverCommandHandler : ServiceCommandHandler
|
|||
};
|
||||
}
|
||||
|
||||
await session.SendAsync(CmdType.CmdPlayerGetTokenScRsp, rsp);
|
||||
await session.SendAsync((ushort)CmdType.CmdPlayerGetTokenScRsp, rsp);
|
||||
}
|
||||
}
|
||||
|
||||
[ServiceCommand(ServiceCommandType.ForwardGameMessage)]
|
||||
public async Task OnForwardGameMessage(ServiceCommand command)
|
||||
{
|
||||
CmdForwardGameMessage cmd = CmdForwardGameMessage.Parser.ParseFrom(command.Body.Span);
|
||||
|
||||
if (_sessionManager.TryGet(cmd.SessionId, out NetworkSession? session))
|
||||
{
|
||||
await session.SendAsync(new((ushort)cmd.CmdType, ReadOnlyMemory<byte>.Empty, cmd.Payload.Memory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
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;
|
||||
|
||||
|
@ -40,10 +39,10 @@ internal class NetworkSession : RPGSession
|
|||
{
|
||||
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;
|
||||
if (result == NetPacket.DeserializationResult.Corrupted) throw new Exception();
|
||||
|
||||
HandleSessionPacketAsync(packet!);
|
||||
Buffer.BlockCopy(_recvBuffer, recvBufferIdx, _recvBuffer, 0, recvBufferIdx -= bytesRead);
|
||||
Buffer.BlockCopy(_recvBuffer, bytesRead, _recvBuffer, 0, recvBufferIdx -= bytesRead);
|
||||
}
|
||||
while (recvBufferIdx >= NetPacket.Overhead);
|
||||
}
|
||||
|
@ -66,7 +65,7 @@ internal class NetworkSession : RPGSession
|
|||
|
||||
private void HandleSessionPacketAsync(NetPacket packet)
|
||||
{
|
||||
switch (packet.CmdType)
|
||||
switch ((CmdType)packet.CmdType)
|
||||
{
|
||||
case CmdType.CmdPlayerGetTokenCsReq:
|
||||
HandlePlayerGetTokenCsReq(PlayerGetTokenCsReq.Parser.ParseFrom(packet.Body.Span));
|
||||
|
@ -102,4 +101,9 @@ internal class NetworkSession : RPGSession
|
|||
CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(timeoutMs));
|
||||
return await socket.ReceiveAsync(buffer, cts.Token);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Socket?.Close();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue