diff --git a/UdpHosts/GameServer/Data/HardcodedCharacterData.cs b/UdpHosts/GameServer/Data/HardcodedCharacterData.cs index 094dbaf7..41183e6f 100644 --- a/UdpHosts/GameServer/Data/HardcodedCharacterData.cs +++ b/UdpHosts/GameServer/Data/HardcodedCharacterData.cs @@ -10,7 +10,7 @@ namespace GameServer.Data; public static class HardcodedCharacterData { - public static string ArmyTag = "[ARMY]"; + public static string ArmyTag = "ARMY"; public static ulong ArmyGUID = 1u; public static uint SelectedLoadout = 184538131; public static byte Level = 45; @@ -27,6 +27,9 @@ public static class HardcodedCharacterData Race = (uint)CharacterRace.Human, TitleId = 135, CurrentBattleframeSDBId = 76331, + ArmyTag = ArmyTag, + ArmyGuid = ArmyGUID, + ArmyIsOfficer = true, }, CharacterVisuals = new BasicCharacterVisuals() { @@ -951,6 +954,9 @@ public class BasicCharacterInfo public uint Race { get; set; } public ushort TitleId { get; set; } public uint CurrentBattleframeSDBId { get; set; } + public string ArmyTag { get; set; } + public ulong ArmyGuid { get; set; } + public bool ArmyIsOfficer { get; set; } } public class BasicCharacterVisuals diff --git a/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs b/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs index 10accc18..150da48f 100644 --- a/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs +++ b/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs @@ -15,6 +15,7 @@ using GameServer.Data.SDB; using GameServer.Data.SDB.Records.dbstats; using GameServer.Enums; +using GameServer.Test; using GrpcGameServerAPIClient; namespace GameServer.Entities.Character; @@ -99,6 +100,8 @@ public CharacterEntity(IShard shard, ulong eid) public ulong CurrentPermissionsValue => GetCurrentPermissionsValue(); public StaticInfoData StaticInfo { get; set; } + public ulong ArmyGUID { get; set; } + public byte ArmyIsOfficer { get; set; } public CharacterStateData CharacterState { get; set; } public HostilityInfoData HostilityInfo { get; set; } public MaxVital MaxShields { get; set; } @@ -372,6 +375,9 @@ public void LoadRemote(CharacterAndBattleframeVisuals remoteData) Race = (byte)remoteData.CharacterInfo.Race, TitleId = (ushort)remoteData.CharacterInfo.TitleId, CurrentBattleframeSDBId = remoteData.CharacterInfo.CurrentBattleframeSDBId, + ArmyGuid = remoteData.CharacterInfo.ArmyGuid, + ArmyTag = remoteData.CharacterInfo.ArmyTag, + ArmyIsOfficer = remoteData.CharacterInfo.ArmyIsOfficer, }, CharacterVisuals = new Data.BasicCharacterVisuals() { @@ -439,8 +445,11 @@ public void Load(BasicCharacterData data) MorphWeights = Array.Empty(), Overlays = Array.Empty() }, - ArmyTag = HardcodedCharacterData.ArmyTag + ArmyTag = DataUtils.FormatArmyTag(info.ArmyTag) }); + + SetArmyGUID(info.ArmyGuid); + SetArmyIsOfficer((byte)(info.ArmyIsOfficer ? 1 : 0)); } public void ApplyLoadout(CharacterLoadout loadout) @@ -694,6 +703,25 @@ public void SetStaticInfo(StaticInfoData value) } } + public void SetArmyGUID(ulong value) + { + ArmyGUID = value; + Character_ObserverView.ArmyGUIDProp = ArmyGUID; + if (Character_BaseController != null) + { + Character_BaseController.ArmyGUIDProp = ArmyGUID; + } + } + + public void SetArmyIsOfficer(byte value) + { + ArmyIsOfficer = value; + if (Character_BaseController != null) + { + Character_BaseController.ArmyIsOfficerProp = ArmyIsOfficer; + } + } + public void SetCurrentEquipment(EquipmentData value) { CurrentEquipment = value; @@ -1102,8 +1130,8 @@ private void InitControllers() SinFlagsPrivateProp = 0, SinFactionsAcquiredByProp = null, SinTeamsAcquiredByProp = null, - ArmyGUIDProp = HardcodedCharacterData.ArmyGUID, - ArmyIsOfficerProp = 0, + ArmyGUIDProp = ArmyGUID, + ArmyIsOfficerProp = ArmyIsOfficer, EncounterPartyTupleProp = null, DockedParamsProp = DockedParams, LookAtTargetProp = null, diff --git a/UdpHosts/GameServer/GRPC/EventHandlers/ArmyEventHandler.cs b/UdpHosts/GameServer/GRPC/EventHandlers/ArmyEventHandler.cs new file mode 100644 index 00000000..8139d91c --- /dev/null +++ b/UdpHosts/GameServer/GRPC/EventHandlers/ArmyEventHandler.cs @@ -0,0 +1,197 @@ +using AeroMessages.GSS.V66.Character.Event; +using GameServer.Test; +using Google.Protobuf.Collections; +using GrpcGameServerAPIClient; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GameServer.GRPC.EventHandlers; + +public static class ArmyEventHandler +{ + private static readonly JsonSerializerOptions SerializerOptions + = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + + public static void HandleEvent(ArmyApplicationApproved e, IDictionary clients) + { + SendMessageToCharacter(clients, e.CharacterGuid, "army_application_approve", e.InitiatorName); + } + + public static void HandleEvent(ArmyApplicationReceived e, IDictionary clients) + { + SendMessageToAuthorizedArmyMembers(clients, e.ArmyMemberGuids, "army_application", e.InitiatorName); + } + + public static void HandleEvent(ArmyApplicationRejected e, IDictionary clients) + { + SendMessageToCharacter(clients, e.CharacterGuid, "army_application_reject", e.InitiatorName); + } + + public static void HandleEvent(ArmyApplicationsUpdated e, IDictionary clients) + { + SendMessageToAuthorizedArmyMembers(clients, e.ArmyMemberGuids, "army_applications_change"); + } + + public static void HandleEvent(ArmyIdChanged e, IDictionary clients) + { + var player = clients.Values.FirstOrDefault(p => p.CharacterId + 0xFE == e.CharacterGuid); + + if (player == null) + { + return; + } + + var playerEntity = player.CharacterEntity; + + var staticInfo = playerEntity.StaticInfo; + staticInfo.ArmyTag = DataUtils.FormatArmyTag(e.ArmyTag); + + playerEntity.SetStaticInfo(staticInfo); + + playerEntity.Character_BaseController.ArmyGUIDProp = e.ArmyGuid; + playerEntity.Character_BaseController.ArmyIsOfficerProp = (byte)(e.IsOfficer ? 1 : 0); + } + + public static void HandleEvent(ArmyInfoUpdated e, IDictionary clients) + { + SendMessageToAllArmyMembers(clients, e.ArmyGuid, "army_info_change"); + } + + public static void HandleEvent(ArmyInviteApproved e, IDictionary clients) + { + SendMessageToAllArmyMembers(clients, e.ArmyGuid, "army_invite_approve", e.InitiatorName); + } + + public static void HandleEvent(ArmyInviteReceived e, IDictionary clients) + { + var player = clients.Values.FirstOrDefault(p => p.CharacterId + 0xFE == e.CharacterGuid); + + if (player == null) + { + return; + } + + var message = new ArmyMessage + { + message_type = "army_invite", + initiator = e.InitiatorName, + army = new Army { army_guid = e.ArmyGuid, name = e.ArmyName }, + application = new Application { id = e.Id, message = e.Message } + }; + + string json = JsonSerializer.Serialize(message, SerializerOptions); + + var msg = new ReceivedWebUIMessage() { Message = json }; + + player.NetChannels[ChannelType.ReliableGss].SendIAero(msg, player.CharacterId); + } + + public static void HandleEvent(ArmyInviteRejected e, IDictionary clients) + { + SendMessageToAuthorizedArmyMembers(clients, e.ArmyMemberGuids, "army_invite_reject", e.InitiatorName); + } + + public static void HandleEvent(ArmyMembersUpdated e, IDictionary clients) + { + SendMessageToAllArmyMembers(clients, e.ArmyGuid, "army_characters_change"); + + // Updating members could've changed officers displayed in army info + SendMessageToAllArmyMembers(clients, e.ArmyGuid, "army_info_change"); + } + + public static void HandleEvent(ArmyRanksUpdated e, IDictionary clients) + { + SendMessageToAllArmyMembers(clients, e.ArmyGuid, "army_rank_info_change"); + + // // Updating ranks could've changed officers displayed in army info + SendMessageToAllArmyMembers(clients, e.ArmyGuid, "army_info_change"); + } + + public static void HandleEvent(ArmyTagUpdated e, IDictionary clients) + { + foreach (var armyMember in GetArmyMembers(clients, e.ArmyGuid)) + { + var staticInfo = armyMember.CharacterEntity.StaticInfo; + staticInfo.ArmyTag = DataUtils.FormatArmyTag(e.ArmyTag); + + armyMember.CharacterEntity.SetStaticInfo(staticInfo); + } + } + + private static IEnumerable GetArmyMembers(IDictionary clients, ulong armyGuid) + { + return clients.Values.Where(p => p.CharacterEntity.Character_BaseController.ArmyGUIDProp == armyGuid); + } + + private static void SendMessageToCharacter( + IDictionary clients, ulong characterGuid, string messageType, string initiatorName = null) + { + var player = clients.Values.FirstOrDefault(p => p.CharacterId + 0xFE == characterGuid); + + if (player == null) + { + return; + } + + SendMessage(player, messageType, initiatorName); + } + + private static void SendMessageToAllArmyMembers( + IDictionary clients, + ulong armyGuid, + string messageType, + string initiatorName = null) + { + var armyMembers = GetArmyMembers(clients, armyGuid); + + foreach (var armyMember in armyMembers) + { + SendMessage(armyMember, messageType, initiatorName); + } + } + + private static void SendMessageToAuthorizedArmyMembers( + IDictionary clients, + RepeatedField armyMemberGuids, + string messageType, + string initiatorName = null) + { + var armyMembers = clients.Values.Where(p => armyMemberGuids.Contains(p.CharacterId + 0xFE)); + + foreach (var armyMember in armyMembers) + { + SendMessage(armyMember, messageType, initiatorName); + } + } + + private static void SendMessage(INetworkPlayer player, string messageType, string initiatorName = null) + { + var armyMessage = new ArmyMessage { message_type = messageType, initiator = initiatorName }; + + player.NetChannels[ChannelType.ReliableGss] + .SendIAero(new ReceivedWebUIMessage() { Message = JsonSerializer.Serialize(armyMessage, SerializerOptions) }, + player.CharacterId); + } + + private record ArmyMessage + { + public string message_type { get; init; } + public string initiator { get; init; } + public Army army { get; init; } + public Application application { get; init; } + } + + private record Army + { + public ulong army_guid { get; init; } + public string name { get; init; } + } + + private record Application + { + public ulong id { get; init; } + public string message { get; init; } + } +} diff --git a/UdpHosts/GameServer/GRPC/EventHandlers/CharacterEventHandler.cs b/UdpHosts/GameServer/GRPC/EventHandlers/CharacterEventHandler.cs new file mode 100644 index 00000000..0fde175b --- /dev/null +++ b/UdpHosts/GameServer/GRPC/EventHandlers/CharacterEventHandler.cs @@ -0,0 +1,14 @@ +using GrpcGameServerAPIClient; +using System.Collections.Generic; +using System.Linq; + +namespace GameServer.GRPC.EventHandlers; + +public static class CharacterEventHandler +{ + public static void HandleEvent(CharacterVisualsUpdated e, IDictionary clients) + { + clients.Values.FirstOrDefault(p => p.CharacterId + 0xFE == e.CharacterGuid) + ?.CharacterEntity.LoadRemote(e.CharacterAndBattleframeVisuals); + } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/GRPC/GRPCService.cs b/UdpHosts/GameServer/GRPC/GRPCService.cs index c228d833..c6be871c 100644 --- a/UdpHosts/GameServer/GRPC/GRPCService.cs +++ b/UdpHosts/GameServer/GRPC/GRPCService.cs @@ -1,6 +1,9 @@ -using System.Threading.Tasks; +using GameServer.GRPC.EventHandlers; +using Grpc.Core; using Grpc.Net.Client; using GrpcGameServerAPIClient; +using System.Collections.Concurrent; +using System.Threading.Tasks; namespace GameServer.GRPC; @@ -17,11 +20,68 @@ public static void Init(string address) public static bool IsAvailable() { - return _channel.State == Grpc.Core.ConnectivityState.Ready; + return _channel.State == ConnectivityState.Ready; } public static async Task GetCharacterAndBattleframeVisualsAsync(long characterId) { return await _client.GetCharacterAndBattleframeVisualsAsync(new CharacterID { ID = characterId }); } + + public static async Task ListenAsync(ConcurrentDictionary clientMap) + { + using var listen = _client.Listen(new EmptyReq()); + + var reader = + Task.Run(async () => + { + await foreach (var evt in listen.ResponseStream.ReadAllAsync()) + { + switch (evt.SubtypeCase) + { + case Event.SubtypeOneofCase.ArmyApplicationApproved: + ArmyEventHandler.HandleEvent(evt.ArmyApplicationApproved, clientMap); + break; + case Event.SubtypeOneofCase.ArmyApplicationReceived: + ArmyEventHandler.HandleEvent(evt.ArmyApplicationReceived, clientMap); + break; + case Event.SubtypeOneofCase.ArmyApplicationRejected: + ArmyEventHandler.HandleEvent(evt.ArmyApplicationRejected, clientMap); + break; + case Event.SubtypeOneofCase.ArmyApplicationsUpdated: + ArmyEventHandler.HandleEvent(evt.ArmyApplicationsUpdated, clientMap); + break; + case Event.SubtypeOneofCase.ArmyIdChanged: + ArmyEventHandler.HandleEvent(evt.ArmyIdChanged, clientMap); + break; + case Event.SubtypeOneofCase.ArmyInfoUpdated: + ArmyEventHandler.HandleEvent(evt.ArmyInfoUpdated, clientMap); + break; + case Event.SubtypeOneofCase.ArmyInviteApproved: + ArmyEventHandler.HandleEvent(evt.ArmyInviteApproved, clientMap); + break; + case Event.SubtypeOneofCase.ArmyInviteReceived: + ArmyEventHandler.HandleEvent(evt.ArmyInviteReceived, clientMap); + break; + case Event.SubtypeOneofCase.ArmyInviteRejected: + ArmyEventHandler.HandleEvent(evt.ArmyInviteRejected, clientMap); + break; + case Event.SubtypeOneofCase.ArmyMembersUpdated: + ArmyEventHandler.HandleEvent(evt.ArmyMembersUpdated, clientMap); + break; + case Event.SubtypeOneofCase.ArmyRanksUpdated: + ArmyEventHandler.HandleEvent(evt.ArmyRanksUpdated, clientMap); + break; + case Event.SubtypeOneofCase.ArmyTagUpdated: + ArmyEventHandler.HandleEvent(evt.ArmyTagUpdated, clientMap); + break; + case Event.SubtypeOneofCase.CharacterVisualsUpdated: + CharacterEventHandler.HandleEvent(evt.CharacterVisualsUpdated, clientMap); + break; + } + } + }); + + await reader; + } } \ No newline at end of file diff --git a/UdpHosts/GameServer/GRPC/GameServerAPI.proto b/UdpHosts/GameServer/GRPC/GameServerAPI.proto index d5859c40..ef9843ef 100644 --- a/UdpHosts/GameServer/GRPC/GameServerAPI.proto +++ b/UdpHosts/GameServer/GRPC/GameServerAPI.proto @@ -3,6 +3,56 @@ option csharp_namespace = "GrpcGameServerAPIClient"; package RIN; import "google/protobuf/timestamp.proto"; +message ArmyApplicationApproved { + uint64 CharacterGuid = 1; + string InitiatorName = 2; +} +message ArmyApplicationReceived { + repeated uint64 ArmyMemberGuids = 1 [packed = false]; + string InitiatorName = 2; +} +message ArmyApplicationRejected { + uint64 CharacterGuid = 1; + string InitiatorName = 2; +} +message ArmyApplicationsUpdated { + repeated uint64 ArmyMemberGuids = 1 [packed = false]; +} +message ArmyIdChanged { + uint64 ArmyGuid = 1; + uint64 CharacterGuid = 2; + bool IsOfficer = 3; + string ArmyTag = 4; +} +message ArmyInfoUpdated { + uint64 ArmyGuid = 1; +} +message ArmyInviteApproved { + uint64 ArmyGuid = 1; + string InitiatorName = 2; +} +message ArmyInviteReceived { + uint64 ArmyGuid = 1; + string ArmyName = 2; + uint64 CharacterGuid = 3; + uint64 Id = 4; + string Message = 5; + string InitiatorName = 6; +} +message ArmyInviteRejected { + repeated uint64 ArmyMemberGuids = 1 [packed = false]; + string InitiatorName = 2; +} +message ArmyMembersUpdated { + uint64 ArmyGuid = 1; +} +message ArmyRanksUpdated { + uint64 ArmyGuid = 1; +} +message ArmyTagUpdated { + uint64 ArmyGuid = 1; + string ArmyTag = 2; +} message BasicCharacterInfo { string Name = 1; uint32 Race = 2; @@ -10,6 +60,9 @@ message BasicCharacterInfo { int32 TitleId = 4; int64 CurrentBattleframeId = 5; uint32 CurrentBattleframeSDBId = 6; + string ArmyTag = 7; + uint64 ArmyGuid = 8; + bool ArmyIsOfficer = 9; } message CharacterAndBattleframeVisuals { BasicCharacterInfo CharacterInfo = 1; @@ -38,6 +91,29 @@ message CharacterVisuals { WebId glider = 16; WebId vehicle = 17; } +message CharacterVisualsUpdated { + uint64 CharacterGuid = 1; + CharacterAndBattleframeVisuals CharacterAndBattleframeVisuals = 2; +} +message EmptyReq { +} +message Event { + oneof subtype { + ArmyApplicationApproved ArmyApplicationApproved = 1; + ArmyApplicationReceived ArmyApplicationReceived = 2; + ArmyApplicationRejected ArmyApplicationRejected = 3; + ArmyApplicationsUpdated ArmyApplicationsUpdated = 4; + ArmyIdChanged ArmyIdChanged = 5; + ArmyInfoUpdated ArmyInfoUpdated = 6; + ArmyInviteApproved ArmyInviteApproved = 7; + ArmyInviteReceived ArmyInviteReceived = 8; + ArmyInviteRejected ArmyInviteRejected = 9; + ArmyMembersUpdated ArmyMembersUpdated = 10; + ArmyRanksUpdated ArmyRanksUpdated = 11; + ArmyTagUpdated ArmyTagUpdated = 12; + CharacterVisualsUpdated CharacterVisualsUpdated = 13; + } +} message PingReq { .google.protobuf.Timestamp SentTime = 1; } @@ -78,5 +154,6 @@ message WebIdValueColorId { } service GameServerAPI { rpc GetCharacterAndBattleframeVisuals (CharacterID) returns (CharacterAndBattleframeVisuals); + rpc Listen (EmptyReq) returns (stream Event); rpc Ping (PingReq) returns (PingResp); } diff --git a/UdpHosts/GameServer/GameServer.cs b/UdpHosts/GameServer/GameServer.cs index 9666c19b..b2973ee6 100644 --- a/UdpHosts/GameServer/GameServer.cs +++ b/UdpHosts/GameServer/GameServer.cs @@ -14,6 +14,7 @@ using GrpcGameServerAPIClient; using Serilog; using Shared.Udp; +using System.Threading.Tasks; namespace GameServer; @@ -52,6 +53,8 @@ protected override void Startup(CancellationToken ct) DataUtils.Init(); Factory.Init(); NewShard(ct); + + _ = ListenGrpcAsync(ct); } protected override async void ServerRunThreadAsync(CancellationToken ct) @@ -125,4 +128,20 @@ private IShard NewShard(CancellationToken ct) return shard; } + + private async Task ListenGrpcAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await GRPCService.ListenAsync(_clientMap); + } + catch (Exception) + { + Console.WriteLine("Failed to connect to GRPC, retrying in 5 minutes"); + await Task.Delay(TimeSpan.FromMinutes(5), ct); + } + } + } } \ No newline at end of file diff --git a/UdpHosts/GameServer/Test/DataUtils.cs b/UdpHosts/GameServer/Test/DataUtils.cs index 43e47388..1015da14 100644 --- a/UdpHosts/GameServer/Test/DataUtils.cs +++ b/UdpHosts/GameServer/Test/DataUtils.cs @@ -59,6 +59,11 @@ public static Zone GetZone(uint id) return _zones.TryGetValue(id, out var zone) ? zone : _zones[448]; } + public static string FormatArmyTag(string armyTag) + { + return string.IsNullOrEmpty(armyTag) ? string.Empty : "[" + armyTag + "]"; + } + private static void AddZone(uint id, string name, ulong timestamp, Vector3 spawn) { var zone = new Zone { ID = id, Name = name, Timestamp = timestamp, POIs = { { "origin", new Vector3(0.0f, 0.0f, 0.0f) }, { "spawn", spawn } } };