From 3caec014ed0d12a0ed3b6f88eeeb9175c23ec09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:03:26 +0100 Subject: [PATCH 01/12] Add chat commands --- UdpHosts/GameServer/IShard.cs | 1 + UdpHosts/GameServer/Shard.cs | 1 + .../GameServer/Systems/Chat/ChatCommand.cs | 62 ++++++++++++++ .../Systems/Chat/ChatCommandAttribute.cs | 18 +++++ .../Systems/Chat/ChatCommandContext.cs | 8 ++ .../Systems/Chat/ChatCommandService.cs | 80 +++++++++++++++++++ .../GameServer/Systems/Chat/ChatService.cs | 20 +++-- .../Systems/Chat/Commands/HelpChatCommand.cs | 21 +++++ .../Chat/Commands/LeaderboardChatCommand.cs | 22 +++++ 9 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 UdpHosts/GameServer/Systems/Chat/ChatCommand.cs create mode 100644 UdpHosts/GameServer/Systems/Chat/ChatCommandAttribute.cs create mode 100644 UdpHosts/GameServer/Systems/Chat/ChatCommandContext.cs create mode 100644 UdpHosts/GameServer/Systems/Chat/ChatCommandService.cs create mode 100644 UdpHosts/GameServer/Systems/Chat/Commands/HelpChatCommand.cs create mode 100644 UdpHosts/GameServer/Systems/Chat/Commands/LeaderboardChatCommand.cs diff --git a/UdpHosts/GameServer/IShard.cs b/UdpHosts/GameServer/IShard.cs index 0f3aab3..4f7535c 100644 --- a/UdpHosts/GameServer/IShard.cs +++ b/UdpHosts/GameServer/IShard.cs @@ -5,6 +5,7 @@ using GameServer.Entities; using GameServer.Entities.Outpost; using GameServer.Physics; +using GameServer.Systems.Chat; using GameServer.Systems.Encounters; using Serilog; using Shared.Udp; diff --git a/UdpHosts/GameServer/Shard.cs b/UdpHosts/GameServer/Shard.cs index b5fb1ee..f0066e1 100644 --- a/UdpHosts/GameServer/Shard.cs +++ b/UdpHosts/GameServer/Shard.cs @@ -11,6 +11,7 @@ using GameServer.Entities; using GameServer.Entities.Outpost; using GameServer.Physics; +using GameServer.Systems.Chat; using GameServer.Systems.Encounters; using Microsoft.Extensions.Logging; using Serilog; diff --git a/UdpHosts/GameServer/Systems/Chat/ChatCommand.cs b/UdpHosts/GameServer/Systems/Chat/ChatCommand.cs new file mode 100644 index 0000000..bd608a1 --- /dev/null +++ b/UdpHosts/GameServer/Systems/Chat/ChatCommand.cs @@ -0,0 +1,62 @@ +using System; +using System.Numerics; + +namespace GameServer.Systems.Chat; + +public abstract class ChatCommand +{ + public abstract void Execute(string[] parameters, ChatCommandContext context); + public virtual void SourceFeedback(string message, ChatCommandContext context) + { + Console.WriteLine(message); + context.SourcePlayer?.SendDebugChat(message); + } + + public uint ParseUIntParameter(string value) + { + if (uint.TryParse(value, out uint result)) + { + return result; + } + else + { + Console.WriteLine($"Invalid format: {value}"); + return 0; + } + } + + public ulong ParseULongParameter(string value) + { + if (ulong.TryParse(value, out ulong result)) + { + return result; + } + else + { + Console.WriteLine($"Invalid format: {value}"); + return 0; + } + } + + public Vector3? ParseVector3Parameters(string[] parameters, int startIndex = 0) + { + if (startIndex < 0 || startIndex >= parameters.Length) + { + Console.WriteLine($"Invalid start index: {startIndex}"); + return null; + } + + if (startIndex + 2 < parameters.Length && + float.TryParse(parameters[startIndex], out float x) && + float.TryParse(parameters[startIndex + 1], out float y) && + float.TryParse(parameters[startIndex + 2], out float z)) + { + return new Vector3(x, y, z); + } + else + { + Console.WriteLine("Invalid format for Vector3 parameters"); + return null; + } + } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Chat/ChatCommandAttribute.cs b/UdpHosts/GameServer/Systems/Chat/ChatCommandAttribute.cs new file mode 100644 index 0000000..4a77610 --- /dev/null +++ b/UdpHosts/GameServer/Systems/Chat/ChatCommandAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace GameServer.Systems.Chat; + +[AttributeUsage(AttributeTargets.Class)] +public class ChatCommandAttribute : Attribute +{ + public ChatCommandAttribute(string description, string usage, params string[] names) + { + Description = description; + Names = names; + Usage = usage; + } + + public string Description { get; } + public string[] Names { get; } + public string Usage { get; } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Chat/ChatCommandContext.cs b/UdpHosts/GameServer/Systems/Chat/ChatCommandContext.cs new file mode 100644 index 0000000..4d318b1 --- /dev/null +++ b/UdpHosts/GameServer/Systems/Chat/ChatCommandContext.cs @@ -0,0 +1,8 @@ +namespace GameServer.Systems.Chat; + +public class ChatCommandContext +{ + public ChatCommandService Service { get; set; } + public IShard Shard { get; set; } + public INetworkPlayer SourcePlayer { get; set; } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Chat/ChatCommandService.cs b/UdpHosts/GameServer/Systems/Chat/ChatCommandService.cs new file mode 100644 index 0000000..1ed3f9a --- /dev/null +++ b/UdpHosts/GameServer/Systems/Chat/ChatCommandService.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace GameServer.Systems.Chat; + +public class ChatCommandService +{ + private readonly Dictionary _commandDictionary; + private Shard _shard; + + public ChatCommandService(Shard shard) + { + _shard = shard; + _commandDictionary = new Dictionary(); + + LoadCommands(); + } + + public string GetCommandList() + { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("Available Commands:"); + + foreach (var commandType in _commandDictionary.Values.Distinct()) + { + var command = Activator.CreateInstance(commandType) as ChatCommand; + var attribute = commandType.GetCustomAttribute(); + stringBuilder.AppendLine($"{attribute.Names[0]}: {attribute.Description}\n\tUsage: {attribute.Usage}\n\tAliases: {string.Join(", ", attribute.Names)}"); + } + + return stringBuilder.ToString(); + } + + public void ExecuteCommand(string input, INetworkPlayer sourcePlayer) + { + var (commandName, parameters) = ParseCommand(input); + + if (_commandDictionary.TryGetValue(commandName.ToLower(), out var commandType)) + { + var command = Activator.CreateInstance(commandType) as ChatCommand; + command.Execute(parameters, new() { Service = this, Shard = _shard, SourcePlayer = sourcePlayer }); + } + else + { + Console.WriteLine($"Unknown command: {commandName}"); + sourcePlayer?.SendDebugChat($"Unknown command: {commandName}"); + } + } + + private (string commandName, string[] parameters) ParseCommand(string input) + { + var parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var commandName = parts[0]; + var parameters = parts.Skip(1).ToArray(); + + return (commandName, parameters); + } + + private void LoadCommands() + { + var commandTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.IsSubclassOf(typeof(ChatCommand))) + .ToList(); + + foreach (var commandType in commandTypes) + { + var attribute = commandType.GetCustomAttribute(); + if (attribute != null) + { + foreach (var name in attribute.Names) + { + _commandDictionary.Add(name.ToLower(), commandType); + } + } + } + } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Chat/ChatService.cs b/UdpHosts/GameServer/Systems/Chat/ChatService.cs index 84fda50..c9cae4a 100644 --- a/UdpHosts/GameServer/Systems/Chat/ChatService.cs +++ b/UdpHosts/GameServer/Systems/Chat/ChatService.cs @@ -1,17 +1,14 @@ using System; using System.Linq; -using System.Threading; using AeroMessages.Common; using AeroMessages.GSS.V66; -using AeroMessages.GSS.V66.Character; using AeroMessages.GSS.V66.Character.Command; -using AeroMessages.GSS.V66.Character.Event; using AeroMessages.GSS.V66.Generic; using GameServer.Entities; using GameServer.Entities.Character; using GameServer.Enums; -namespace GameServer; +namespace GameServer.Systems.Chat; public class ChatService { @@ -24,10 +21,12 @@ public class ChatService }; private Shard Shard; + private ChatCommandService CommandService; public ChatService(Shard shard) { Shard = shard; + CommandService = new ChatCommandService(shard); } public void CharacterPerformTextChat(INetworkClient client, IEntity entity, PerformTextChat query) @@ -40,7 +39,13 @@ public void CharacterPerformTextChat(INetworkClient client, IEntity entity, Perf ChatChannel queryChannel = (ChatChannel)query.Channel; - if (PublicBroadcastChannels.Contains(queryChannel)) + var trimmed = query.Message.Trim(); + if (trimmed.StartsWith('\\')) + { + CommandService.ExecuteCommand(trimmed[1..], ((CharacterEntity)entity).Player); + Console.WriteLine(query.Message); + } + else if (PublicBroadcastChannels.Contains(queryChannel)) { SendToAll(query.Message, queryChannel, entity); } @@ -73,6 +78,11 @@ public void SendToAll(string message, ChatChannel channel, IEntity sender) } } + public string GetCommandList() + { + return CommandService.GetCommandList(); + } + private ChatMessageList PrepareSingleMessage(string message, ChatChannel channel, IEntity sender) { var senderId = sender != null ? sender.AeroEntityId : new EntityId() { Backing = Shard.InstanceId }; diff --git a/UdpHosts/GameServer/Systems/Chat/Commands/HelpChatCommand.cs b/UdpHosts/GameServer/Systems/Chat/Commands/HelpChatCommand.cs new file mode 100644 index 0000000..ab63b0c --- /dev/null +++ b/UdpHosts/GameServer/Systems/Chat/Commands/HelpChatCommand.cs @@ -0,0 +1,21 @@ +using System; + +namespace GameServer.Systems.Chat.Commands; + +[ChatCommand("Print a list of all chat commands", "help", "help", "listcmd", "listcmds", "cmdlist", "cmds")] +public class HelpChatCommand : ChatCommand +{ + public override void Execute(string[] parameters, ChatCommandContext context) + { + var message = context.Shard.Chat.GetCommandList(); + if (context.SourcePlayer != null) + { + context.SourcePlayer.SendDebugLog(message); + context.SourcePlayer.SendDebugChat("Command list printed to console"); + } + else + { + Console.WriteLine(message); + } + } +} diff --git a/UdpHosts/GameServer/Systems/Chat/Commands/LeaderboardChatCommand.cs b/UdpHosts/GameServer/Systems/Chat/Commands/LeaderboardChatCommand.cs new file mode 100644 index 0000000..b9247a7 --- /dev/null +++ b/UdpHosts/GameServer/Systems/Chat/Commands/LeaderboardChatCommand.cs @@ -0,0 +1,22 @@ +using AeroMessages.GSS.V66.Character.Controller; + +namespace GameServer.Systems.Chat.Commands; + +[ChatCommand("Opens leaderboard", "leaderboard [id]", "leaderboard", "scoreboard")] +public class LeaderboardChatCommand : ChatCommand +{ + public override void Execute(string[] parameters, ChatCommandContext context) + { + if (parameters.Length != 1) + { + SourceFeedback("Provide id of the leaderboard you'd like to open", context); + return; + } + + context.SourcePlayer.CharacterEntity.SetAuthorizedTerminal( + new AuthorizedTerminalData + { + TerminalId = ParseUIntParameter(parameters[0]), TerminalType = 14, TerminalEntityId = 0 + }); + } +} From f44c9a2cde7f2f35451e26aa0bdba5d61664757c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:04:55 +0100 Subject: [PATCH 02/12] Update settings after namespace changes, add ZoneId to config --- UdpHosts/GameServer/App.Default.config | 1 + UdpHosts/GameServer/GameServer.cs | 3 ++- UdpHosts/GameServer/GameServerModule.cs | 13 +++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/UdpHosts/GameServer/App.Default.config b/UdpHosts/GameServer/App.Default.config index e435143..7f06c11 100644 --- a/UdpHosts/GameServer/App.Default.config +++ b/UdpHosts/GameServer/App.Default.config @@ -5,6 +5,7 @@ + diff --git a/UdpHosts/GameServer/GameServer.cs b/UdpHosts/GameServer/GameServer.cs index a78c262..2ff3652 100644 --- a/UdpHosts/GameServer/GameServer.cs +++ b/UdpHosts/GameServer/GameServer.cs @@ -12,6 +12,7 @@ using GameServer.Test; using Serilog; using Shared.Udp; +using SDB = FauFau.Formats.StaticDB; namespace GameServer; @@ -31,7 +32,7 @@ internal class GameServer : PacketServer public GameServer(GameServerSettings serverSettings, ILogger logger, - StaticDB sdb) + SDB sdb) : base(serverSettings.Port, logger) { _clientMap = new ConcurrentDictionary(); diff --git a/UdpHosts/GameServer/GameServerModule.cs b/UdpHosts/GameServer/GameServerModule.cs index 2fd5534..d4ea27f 100644 --- a/UdpHosts/GameServer/GameServerModule.cs +++ b/UdpHosts/GameServer/GameServerModule.cs @@ -1,10 +1,10 @@ using System; using System.Configuration; using Autofac; -using FauFau.Formats; using GameServer.Physics.ZoneLoader; using Serilog; using Shared.Common; +using SDB = FauFau.Formats.StaticDB; namespace GameServer; @@ -21,7 +21,7 @@ protected override void Load(ContainerBuilder builder) private static void RegisterTypes(ContainerBuilder builder) { builder.RegisterType().SingleInstance(); - builder.RegisterType().SingleInstance(); + builder.RegisterType().SingleInstance(); builder.RegisterType(); } @@ -46,6 +46,11 @@ private static void RegisterInstances(ContainerBuilder builder) settings.StaticDBPath = ConfigurationManager.AppSettings["StaticDBPath"]; } + if (ConfigurationManager.AppSettings["ZoneId"] != null) + { + settings.ZoneId = uint.Parse(ConfigurationManager.AppSettings["ZoneId"]); + } + if (ConfigurationManager.AppSettings["MapsPath"] != null) { settings.MapsPath = ConfigurationManager.AppSettings["MapsPath"]; @@ -101,11 +106,11 @@ private static void RegisterInstances(ContainerBuilder builder) var settings = ctx.Resolve(); Console.WriteLine($"Opening SDB from {settings.StaticDBPath}"); - StaticDB sdb = new StaticDB(); + var sdb = new SDB(); sdb.Read(settings.StaticDBPath); return sdb; }) - .As().SingleInstance(); + .As().SingleInstance(); } } \ No newline at end of file From 60a0b91460069a3e236b8bc0fd2afd9495f2ca40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:06:28 +0100 Subject: [PATCH 03/12] Add basic AreaVisualData --- .../AreaVisualData/AreaVisualDataEntity.cs | 32 +++ .../Systems/EntityManager/EntityManager.cs | 196 +++++++++++++++++- 2 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 UdpHosts/GameServer/Entities/AreaVisualData/AreaVisualDataEntity.cs diff --git a/UdpHosts/GameServer/Entities/AreaVisualData/AreaVisualDataEntity.cs b/UdpHosts/GameServer/Entities/AreaVisualData/AreaVisualDataEntity.cs new file mode 100644 index 0000000..e783079 --- /dev/null +++ b/UdpHosts/GameServer/Entities/AreaVisualData/AreaVisualDataEntity.cs @@ -0,0 +1,32 @@ +using AeroMessages.Common; +using AeroMessages.GSS.V66.AreaVisualData.View; + +namespace GameServer.Entities.AreaVisualData; + +public sealed class AreaVisualDataEntity : BaseEntity +{ + public AreaVisualDataEntity(IShard shard, ulong eid) + : base(shard, eid) + { + AeroEntityId = new EntityId() { Backing = EntityId, ControllerId = Controller.AreaVisualData }; + InitFields(); + InitViews(); + } + + public ObserverView AreaVisualData_ObserverView { get; set; } + public ParticleEffectsView AreaVisualData_ParticleEffectsView { get; set; } + public MapMarkerView AreaVisualData_MapMarkerView { get; set; } + public TinyObjectView AreaVisualData_TinyObjectView { get; set; } + public LootObjectView AreaVisualData_LootObjectView { get; set; } + public ForceShieldView AreaVisualData_ForceShieldView { get; set; } + + private void InitFields() + { + } + + private void InitViews() + { + AreaVisualData_ObserverView = new ObserverView() { PositionProp = Position }; + AreaVisualData_ParticleEffectsView = new ParticleEffectsView(); + } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs b/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs index 83ad96c..249a2b6 100644 --- a/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs +++ b/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs @@ -20,6 +20,7 @@ using GameServer.Data.SDB.Records.aptfs; using GameServer.Data.SDB.Records.customdata; using GameServer.Entities; +using GameServer.Entities.AreaVisualData; using GameServer.Entities.Carryable; using GameServer.Entities.Character; using GameServer.Entities.Deployable; @@ -229,6 +230,16 @@ public MeldingEntity SpawnMelding(string perimiterSetName, ActiveDataStruct acti return meldingEntity; } + public AreaVisualDataEntity SpawnAreaVisualData(Vector3 position, ScopingComponent scoping) + { + var areaVisualData = new AreaVisualDataEntity(Shard, Shard.GetNextGuid()) + { + Scoping = scoping, Position = position, + }; + Add(areaVisualData.EntityId, areaVisualData); + return areaVisualData; + } + public OutpostEntity SpawnOutpost(Outpost outpost) { var outpostEntity = new OutpostEntity(Shard, Shard.GetNextGuid(), outpost); @@ -693,9 +704,105 @@ public void KeyframeRequest(INetworkClient client, IPlayer player, IEntity entit } break; + case AreaVisualDataEntity areaVisualData: + switch (typecode) + { + case Enums.GSS.Controllers.AreaVisualData_ObserverView: + if (areaVisualData.AreaVisualData_ObserverView != null) + { + uint ourChecksum = areaVisualData.AreaVisualData_ObserverView.SerializeToChecksum(); + if (clientChecksum == ourChecksum) + { + client.NetChannels[ChannelType.ReliableGss].SendChecksum(entity.EntityId, typecode, clientChecksum); + } + else + { + client.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(areaVisualData.AreaVisualData_ObserverView, entity.EntityId); + } + } + + break; + case Enums.GSS.Controllers.AreaVisualData_ParticleEffectsView: + if (areaVisualData.AreaVisualData_ParticleEffectsView != null) + { + uint ourChecksum = areaVisualData.AreaVisualData_ParticleEffectsView.SerializeToChecksum(); + if (clientChecksum == ourChecksum) + { + client.NetChannels[ChannelType.ReliableGss].SendChecksum(entity.EntityId, typecode, clientChecksum); + } + else + { + client.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(areaVisualData.AreaVisualData_ParticleEffectsView, entity.EntityId); + } + } + + break; + case Enums.GSS.Controllers.AreaVisualData_MapMarkerView: + if (areaVisualData.AreaVisualData_MapMarkerView != null) + { + uint ourChecksum = areaVisualData.AreaVisualData_MapMarkerView.SerializeToChecksum(); + if (clientChecksum == ourChecksum) + { + client.NetChannels[ChannelType.ReliableGss].SendChecksum(entity.EntityId, typecode, clientChecksum); + } + else + { + client.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(areaVisualData.AreaVisualData_MapMarkerView, entity.EntityId); + } + } + + break; + case Enums.GSS.Controllers.AreaVisualData_TinyObjectView: + if (areaVisualData.AreaVisualData_TinyObjectView != null) + { + uint ourChecksum = areaVisualData.AreaVisualData_TinyObjectView.SerializeToChecksum(); + if (clientChecksum == ourChecksum) + { + client.NetChannels[ChannelType.ReliableGss].SendChecksum(entity.EntityId, typecode, clientChecksum); + } + else + { + client.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(areaVisualData.AreaVisualData_TinyObjectView, entity.EntityId); + } + } + + break; + case Enums.GSS.Controllers.AreaVisualData_LootObjectView: + if (areaVisualData.AreaVisualData_LootObjectView != null) + { + uint ourChecksum = areaVisualData.AreaVisualData_LootObjectView.SerializeToChecksum(); + if (clientChecksum == ourChecksum) + { + client.NetChannels[ChannelType.ReliableGss].SendChecksum(entity.EntityId, typecode, clientChecksum); + } + else + { + client.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(areaVisualData.AreaVisualData_LootObjectView, entity.EntityId); + } + } - // TODO: AreaVisualData - // -- + break; + case Enums.GSS.Controllers.AreaVisualData_ForceShieldView: + if (areaVisualData.AreaVisualData_ForceShieldView != null) + { + uint ourChecksum = areaVisualData.AreaVisualData_ForceShieldView.SerializeToChecksum(); + if (clientChecksum == ourChecksum) + { + client.NetChannels[ChannelType.ReliableGss].SendChecksum(entity.EntityId, typecode, clientChecksum); + } + else + { + client.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(areaVisualData.AreaVisualData_ForceShieldView, entity.EntityId); + } + } + + break; + default: + Console.WriteLine($"Unhandled KeyframeRequest for {typecode}"); + break; + } + + break; case VehicleEntity vehicle: bool isVehicleController = vehicle.IsPlayerControlled && vehicle.ControllingPlayer == player; switch (typecode) @@ -1108,6 +1215,44 @@ public void ScopeIn(INetworkPlayer player, IEntity entity) player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(observer, entity.EntityId); } } + else if (entity is AreaVisualDataEntity avd) + { + var observer = avd.AreaVisualData_ObserverView; + if (observer != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(observer, entity.EntityId); + } + + var particleEffects = avd.AreaVisualData_ParticleEffectsView; + if (particleEffects != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(particleEffects, entity.EntityId); + } + + var mapMarker = avd.AreaVisualData_MapMarkerView; + if (mapMarker != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(mapMarker, entity.EntityId); + } + + var tinyObject = avd.AreaVisualData_TinyObjectView; + if (tinyObject != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(tinyObject, entity.EntityId); + } + + var lootObject = avd.AreaVisualData_LootObjectView; + if (lootObject != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(lootObject, entity.EntityId); + } + + var forceShield = avd.AreaVisualData_ForceShieldView; + if (forceShield != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewKeyframe(forceShield, entity.EntityId); + } + } } public void ScopeOut(INetworkPlayer player, IEntity entity) @@ -1332,6 +1477,44 @@ public void ScopeOut(INetworkPlayer player, IEntity entity) player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(observer, entity.EntityId); } } + else if (entity is AreaVisualDataEntity avd) + { + var observer = avd.AreaVisualData_ObserverView; + if (observer != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(observer, entity.EntityId); + } + + var particleEffects = avd.AreaVisualData_ParticleEffectsView; + if (particleEffects != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(particleEffects, entity.EntityId); + } + + var mapMarker = avd.AreaVisualData_MapMarkerView; + if (mapMarker != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(mapMarker, entity.EntityId); + } + + var tinyObject = avd.AreaVisualData_TinyObjectView; + if (tinyObject != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(tinyObject, entity.EntityId); + } + + var lootObject = avd.AreaVisualData_LootObjectView; + if (lootObject != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(lootObject, entity.EntityId); + } + + var forceShield = avd.AreaVisualData_ForceShieldView; + if (forceShield != null) + { + player.NetChannels[ChannelType.ReliableGss].SendViewScopeOut(forceShield, entity.EntityId); + } + } } public void RemoveControllers(INetworkPlayer player, IEntity entity) @@ -1446,6 +1629,15 @@ public void FlushChanges(IEntity entity) var thumper = entity as ThumperEntity; FlushViewChangesToEveryone(thumper.ResourceNode_ObserverView, thumper.EntityId); } + else if (entity is AreaVisualDataEntity avd) + { + FlushViewChangesToEveryone(avd.AreaVisualData_ObserverView, avd.EntityId); + FlushViewChangesToEveryone(avd.AreaVisualData_ParticleEffectsView, avd.EntityId); + FlushViewChangesToEveryone(avd.AreaVisualData_MapMarkerView, avd.EntityId); + FlushViewChangesToEveryone(avd.AreaVisualData_TinyObjectView, avd.EntityId); + FlushViewChangesToEveryone(avd.AreaVisualData_LootObjectView, avd.EntityId); + FlushViewChangesToEveryone(avd.AreaVisualData_ForceShieldView, avd.EntityId); + } } public void FlushViewChangesToPlayer(TPacket view, ulong entityId, INetworkPlayer player) From a3018c33b0db3ed163c5b0a789f63632855f71c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:09:04 +0100 Subject: [PATCH 04/12] Update grpc stream --- UdpHosts/GameServer/GRPC/GRPCService.cs | 125 ++++++++++--------- UdpHosts/GameServer/GRPC/GameServerAPI.proto | 26 ++-- UdpHosts/GameServer/GameServer.cs | 6 +- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/UdpHosts/GameServer/GRPC/GRPCService.cs b/UdpHosts/GameServer/GRPC/GRPCService.cs index 8a29573..3519f44 100644 --- a/UdpHosts/GameServer/GRPC/GRPCService.cs +++ b/UdpHosts/GameServer/GRPC/GRPCService.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Concurrent; +using System.Threading; using System.Threading.Tasks; using GameServer.GRPC.EventHandlers; using Grpc.Core; @@ -11,6 +13,7 @@ public static class GRPCService { private static GrpcChannel _channel; private static GameServerAPI.GameServerAPIClient _client; + private static AsyncDuplexStreamingCall _stream; public static void Init(string address) { @@ -28,71 +31,73 @@ public static async Task GetCharacterAndBattlefr return await _client.GetCharacterAndBattleframeVisualsAsync(new CharacterID { ID = characterId }); } - public static async Task SaveCharacterSessionDataAsync(ulong characterId, uint zoneId, uint outpostId, uint timePlayed) + public static async Task SaveCharacterSessionDataAsync(ulong characterId, uint zoneId, uint outpostId, uint timePlayed) { - return await _client.SaveCharacterGameSessionDataAsync(new GameSessionData() - { - CharacterId = characterId, - ZoneId = zoneId, - OutpostId = outpostId, - TimePlayed = timePlayed - }); + var data = new SaveGameSessionData() + { + CharacterId = characterId, ZoneId = zoneId, OutpostId = outpostId, TimePlayed = timePlayed + }; + + await SendCommandAsync(new Command() { SaveGameSessionData = data }); } - public static async Task ListenAsync(ConcurrentDictionary clientMap) + public static async Task SendCommandAsync(Command command) { - using var listen = _client.Listen(new Empty()); + await _stream.RequestStream.WriteAsync(command); + } - 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; - } - } - }); + public static async Task ListenAsync(ConcurrentDictionary clientMap, CancellationToken ct) + { + _stream = _client.Stream(); - await reader; + await foreach (var evt in _stream.ResponseStream.ReadAllAsync(ct)) + { + Console.WriteLine(evt); + 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; + case Event.SubtypeOneofCase.None: + default: + break; + } + } } } \ No newline at end of file diff --git a/UdpHosts/GameServer/GRPC/GameServerAPI.proto b/UdpHosts/GameServer/GRPC/GameServerAPI.proto index c847329..9ee1521 100644 --- a/UdpHosts/GameServer/GRPC/GameServerAPI.proto +++ b/UdpHosts/GameServer/GRPC/GameServerAPI.proto @@ -98,7 +98,11 @@ message CharacterVisualsUpdated { uint64 CharacterGuid = 1; CharacterAndBattleframeVisuals CharacterAndBattleframeVisuals = 2; } -message Empty { +message Command { + oneof subtype { + SaveGameSessionData SaveGameSessionData = 1; + SaveLgvRaceFinish SaveLgvRaceFinish = 2; + } } message Event { oneof subtype { @@ -117,12 +121,6 @@ message Event { CharacterVisualsUpdated CharacterVisualsUpdated = 13; } } -message GameSessionData { - uint64 CharacterId = 1; - uint32 ZoneId = 2; - uint32 OutpostId = 3; - uint32 TimePlayed = 4; -} message PingReq { .google.protobuf.Timestamp SentTime = 1; } @@ -138,6 +136,17 @@ message PlayerBattleframeVisuals { repeated int32 warpaint_patterns = 5 [packed = false]; repeated int32 visual_overrides = 6 [packed = false]; } +message SaveGameSessionData { + uint64 CharacterId = 1; + uint32 ZoneId = 2; + uint32 OutpostId = 3; + uint32 TimePlayed = 4; +} +message SaveLgvRaceFinish { + uint64 CharacterGuid = 1; + uint32 LeaderboardId = 2; + uint64 TimeMs = 3; +} message WebColor { uint32 color = 1; } @@ -163,7 +172,6 @@ message WebIdValueColorId { } service GameServerAPI { rpc GetCharacterAndBattleframeVisuals (CharacterID) returns (CharacterAndBattleframeVisuals); - rpc Listen (Empty) returns (stream Event); rpc Ping (PingReq) returns (PingResp); - rpc SaveCharacterGameSessionData (GameSessionData) returns (Empty); + rpc Stream (stream Command) returns (stream Event); } diff --git a/UdpHosts/GameServer/GameServer.cs b/UdpHosts/GameServer/GameServer.cs index 2ff3652..0eb11ef 100644 --- a/UdpHosts/GameServer/GameServer.cs +++ b/UdpHosts/GameServer/GameServer.cs @@ -140,12 +140,12 @@ private async Task ListenGrpcAsync(CancellationToken ct) { try { - await GRPCService.ListenAsync(_clientMap); + await GRPCService.ListenAsync(_clientMap, ct); } catch (Exception) { - Console.WriteLine("Failed to connect to GRPC, retrying in 5 minutes"); - await Task.Delay(TimeSpan.FromMinutes(5), ct); + Console.WriteLine("Failed to establish GRPC stream, retrying in 30 seconds"); + await Task.Delay(TimeSpan.FromSeconds(30), ct); } } } From d18cead1a9a03f8271735150413e54a1745c299a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:11:11 +0100 Subject: [PATCH 05/12] Allow checking if entity is scoped per player, automounting vehicles --- .../Systems/EntityManager/EntityManager.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs b/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs index 249a2b6..bed6365 100644 --- a/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs +++ b/UdpHosts/GameServer/Systems/EntityManager/EntityManager.cs @@ -64,9 +64,12 @@ public EntityManager(Shard shard) public int GetNumberOfScopedEntities(IPlayer player) { - return ScopedPlayersByEntity.Values - .Where((set) => set.Contains(player)) - .Count(); + return ScopedPlayersByEntity.Values.Count(set => set.Contains(player)); + } + + public bool HasScopedInEntity(ulong entityId, INetworkPlayer player) + { + return ScopedPlayersByEntity.TryGetValue(entityId, out var players) && players.Contains(player); } public CharacterEntity SpawnCharacter(uint typeId, Vector3 position) @@ -107,23 +110,19 @@ public VehicleEntity SpawnVehicle(ushort typeId, Vector3 position, Quaternion or { vehicleEntity.SetOwner(owner as BaseEntity); - if (owner.GetType() == typeof(CharacterEntity)) + if (owner is CharacterEntity { IsPlayerControlled: true } character) { - var character = owner as CharacterEntity; - if (character.IsPlayerControlled) - { - vehicleEntity.SetOwningPlayer(character.Player); - } - } - - if (autoMount) - { - // TODO: + vehicleEntity.SetOwningPlayer(character.Player); } } Add(vehicleEntity.EntityId, vehicleEntity); + if (owner is CharacterEntity c && autoMount) + { + vehicleEntity.AddOccupant(c); + } + if (vehicleEntity.SpawnAbility != 0) { Shard.Abilities.HandleActivateAbility(Shard, vehicleEntity, vehicleEntity.SpawnAbility, Shard.CurrentTime, new AptitudeTargets()); From acb9aa648111d1e4f5257d57ccebe337bf9c645d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:11:45 +0100 Subject: [PATCH 06/12] Fix typos --- UdpHosts/GameServer/Enums/GSS/Character/Commands.cs | 2 +- UdpHosts/GameServer/Enums/GSS/Character/Events.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UdpHosts/GameServer/Enums/GSS/Character/Commands.cs b/UdpHosts/GameServer/Enums/GSS/Character/Commands.cs index a4c8c55..005c2f2 100644 --- a/UdpHosts/GameServer/Enums/GSS/Character/Commands.cs +++ b/UdpHosts/GameServer/Enums/GSS/Character/Commands.cs @@ -136,7 +136,7 @@ internal enum Commands : byte RequestTeleport = 210, RequestFrameLevelReset = 211, LeaveEncounterParty = 212, - JoinSquadLeadersAr = 213, + JoinSquadLeadersArc = 213, LeaveArc = 214, JobLedgerOperation = 215, SeatChangeRequest = 216, diff --git a/UdpHosts/GameServer/Enums/GSS/Character/Events.cs b/UdpHosts/GameServer/Enums/GSS/Character/Events.cs index 858e027..2ae24a2 100644 --- a/UdpHosts/GameServer/Enums/GSS/Character/Events.cs +++ b/UdpHosts/GameServer/Enums/GSS/Character/Events.cs @@ -64,7 +64,7 @@ internal enum Events ResourceNodeCompletedEvent = 137, FoundResourceAreas = 138, GeographicalReportResponse = 139, - ResourceLocationInfosRespons = 140, + ResourceLocationInfosResponse = 140, UiNamedVariableUpdate = 141, DuelNotification = 142, NewUiQuery = 143, From df4e5d84342341c9ad7dd194d32eb6962efac1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:14:11 +0100 Subject: [PATCH 07/12] Add lgv races, update naming in repulsor --- .../Controllers/Character/BaseController.cs | 13 +- .../GameServer/Entities/EncounterComponent.cs | 5 + .../Entities/Vehicle/VehicleEntity.cs | 7 +- .../GameServer/StaticDB/CustomDBInterface.cs | 9 +- .../StaticDB/CustomData/lgv_race.json | 262 ++++++++++++++++++ .../StaticDB/Loaders/CustomDBLoader.cs | 12 +- .../customdata/Encounters/IEncounterDef.cs | 5 + .../customdata/Encounters/LgvRaceDef.cs | 23 ++ ...ldingRepulsor.cs => MeldingRepulsorDef.cs} | 5 +- .../Interaction/EndInteractionCommand.cs | 5 + .../Systems/Encounters/BaseEncounter.cs | 15 + .../Systems/Encounters/EncounterManager.cs | 31 ++- .../Systems/Encounters/Encounters/LgvRace.cs | 246 ++++++++++++++++ .../Encounters/Encounters/MeldingRepulsor.cs | 13 +- .../GameServer/Systems/Encounters/Factory.cs | 21 ++ .../Systems/Encounters/IEncounterHandlers.cs | 12 + 16 files changed, 667 insertions(+), 17 deletions(-) create mode 100644 UdpHosts/GameServer/StaticDB/CustomData/lgv_race.json create mode 100644 UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/IEncounterDef.cs create mode 100644 UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/LgvRaceDef.cs rename UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/{MeldingRepulsor.cs => MeldingRepulsorDef.cs} (72%) create mode 100644 UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs create mode 100644 UdpHosts/GameServer/Systems/Encounters/Factory.cs diff --git a/UdpHosts/GameServer/Controllers/Character/BaseController.cs b/UdpHosts/GameServer/Controllers/Character/BaseController.cs index 0fe453f..333d097 100644 --- a/UdpHosts/GameServer/Controllers/Character/BaseController.cs +++ b/UdpHosts/GameServer/Controllers/Character/BaseController.cs @@ -12,12 +12,14 @@ using GameServer.Data; using GameServer.Data.SDB; using GameServer.Data.SDB.Records.customdata; +using GameServer.Entities; using GameServer.Entities.Character; using GameServer.Entities.Turret; using GameServer.Entities.Vehicle; using GameServer.Enums.GSS.Character; using GameServer.Extensions; using GameServer.Packets; +using GameServer.Systems.Encounters; using Serilog; using static AeroMessages.GSS.V66.Character.Command.NonDevDebugCommand; using LoadoutVisualType = AeroMessages.GSS.V66.Character.LoadoutConfig_Visual.LoadoutVisualType; @@ -371,12 +373,13 @@ public void ExitAttachmentRequest(INetworkClient client, IPlayer player, ulong e } var character = player.CharacterEntity; + var entity = character.AttachedToEntity; - if (character.AttachedToEntity is VehicleEntity vehicle) + if (entity is VehicleEntity vehicle) { vehicle.RemoveOccupant(character); } - else if (character.AttachedToEntity is TurretEntity turret) + else if (entity is TurretEntity turret) { if (turret.Parent is VehicleEntity parentVehicle) { @@ -393,6 +396,12 @@ public void ExitAttachmentRequest(INetworkClient client, IPlayer player, ulong e var response = new ExitingAttachment() { Direction = new Vector3(-0.5f, -0.5f, -0.47f) }; client.NetChannels[ChannelType.ReliableGss].SendMessage(response, character.EntityId); + + if (entity is BaseEntity { Encounter.Instance: IExitAttachmentHandler handler } baseEntity + && baseEntity.Encounter.Handles(EncounterComponent.Event.ExitAttachment)) + { + handler.OnExitAttachment(baseEntity, (INetworkPlayer)player); + } } [MessageID((byte)Commands.SeatChangeRequest)] diff --git a/UdpHosts/GameServer/Entities/EncounterComponent.cs b/UdpHosts/GameServer/Entities/EncounterComponent.cs index 6421327..14fbdd9 100644 --- a/UdpHosts/GameServer/Entities/EncounterComponent.cs +++ b/UdpHosts/GameServer/Entities/EncounterComponent.cs @@ -1,4 +1,5 @@ using System; +using GameServer.StaticDB.Records.customdata.Encounters; using GameServer.Systems.Encounters; namespace GameServer.Entities; @@ -11,10 +12,14 @@ public enum Event : uint Signal = 1 << 0, Interaction = 1 << 1, Donation = 1 << 2, + ExitAttachment = 1 << 3, + Proximity = 1 << 4, } public ulong EncounterId { get; set; } public IEncounter Instance { get; set; } + public uint ProximityDistance { get; set; } = 0; + public IEncounterDef SpawnDef { get; set; } public Event Events { get; set; } public bool Handles(Event type) => Events.HasFlag(type); public void StartHandling(Event type) => Events |= type; diff --git a/UdpHosts/GameServer/Entities/Vehicle/VehicleEntity.cs b/UdpHosts/GameServer/Entities/Vehicle/VehicleEntity.cs index b03fa78..85fc748 100644 --- a/UdpHosts/GameServer/Entities/Vehicle/VehicleEntity.cs +++ b/UdpHosts/GameServer/Entities/Vehicle/VehicleEntity.cs @@ -51,7 +51,7 @@ public class SeatConfig public byte TurretIndex; } -public class VehicleEntity : BaseAptitudeEntity, IAptitudeTarget +public sealed class VehicleEntity : BaseAptitudeEntity, IAptitudeTarget { public VehicleEntity(IShard shard, ulong eid) : base(shard, eid) @@ -503,6 +503,11 @@ public bool IsEntitySeated(IEntity entity) public override bool IsInteractable() { + if (Encounter != null && !Encounter.Handles(EncounterComponent.Event.Interaction)) + { + return false; + } + foreach (var seat in Occupants.Values) { if (seat.Occupant == null && seat.Role != AttachmentRole.None) diff --git a/UdpHosts/GameServer/StaticDB/CustomDBInterface.cs b/UdpHosts/GameServer/StaticDB/CustomDBInterface.cs index ef39eee..1233ace 100644 --- a/UdpHosts/GameServer/StaticDB/CustomDBInterface.cs +++ b/UdpHosts/GameServer/StaticDB/CustomDBInterface.cs @@ -1,3 +1,5 @@ +using GameServer.StaticDB.Records.customdata.Encounters; + namespace GameServer.Data.SDB; using System.Collections.Generic; @@ -154,7 +156,8 @@ public class CustomDBInterface private static Dictionary> Deployable; private static Dictionary> Melding; private static Dictionary> Outpost; - private static Dictionary> MeldingRepulsor; + private static Dictionary> MeldingRepulsor; + private static Dictionary> LgvRace; public static void Init() { @@ -310,6 +313,7 @@ public static void Init() Melding = loader.LoadMelding(); Outpost = loader.LoadOutpost(); MeldingRepulsor = loader.LoadMeldingRepulsor(); + LgvRace = loader.LoadLgvRace(); } // aptgss @@ -461,5 +465,6 @@ public static void Init() public static Dictionary GetZoneDeployables(uint zoneId) => Deployable.GetValueOrDefault(zoneId); public static Dictionary GetZoneMeldings(uint zoneId) => Melding.GetValueOrDefault(zoneId); public static Dictionary GetZoneOutposts(uint zoneId) => Outpost.GetValueOrDefault(zoneId); - public static Dictionary GetZoneMeldingRepulsors(uint zoneId) => MeldingRepulsor.GetValueOrDefault(zoneId); + public static Dictionary GetZoneMeldingRepulsors(uint zoneId) => MeldingRepulsor.GetValueOrDefault(zoneId); + public static Dictionary GetZoneLgvRaces(uint zoneId) => LgvRace.GetValueOrDefault(zoneId); } diff --git a/UdpHosts/GameServer/StaticDB/CustomData/lgv_race.json b/UdpHosts/GameServer/StaticDB/CustomData/lgv_race.json new file mode 100644 index 0000000..42f6196 --- /dev/null +++ b/UdpHosts/GameServer/StaticDB/CustomData/lgv_race.json @@ -0,0 +1,262 @@ +[ + { + "id": 1, + "zone_id": 448, + "comment": "Copacabana - Trans-Hub", + "label": 156505, + "leaderboard_id": 1, + "start_dialog": 11156, + "start": { + "orientation": { + "IsIdentity": false, + "W": -0.17295577, + "X": 0.002591103, + "Y": 0.0435862, + "Z": 0.9839613 + }, + "position": { + "X": -357.18137, + "Y": -467.0791, + "Z": 418.691528 + } + }, + "finish": { + "orientation": { + "IsIdentity": false, + "W": 0.1801588, + "X": 0, + "Y": 0, + "Z": 0.9836375 + }, + "position": { + "X": -1020.095704, + "Y": -908.493591, + "Z": 430.220825 + } + }, + "terminal": { + "orientation": { + "IsIdentity": false, + "W": 0.7175571, + "X": 0, + "Y": 0, + "Z": 0.6964997 + }, + "position": { + "X": -360.3101, + "Y": -465.41077, + "Z": 418.7193 + } + }, + "time_limit_ms": 80000, + "bonus_time_limit_ms": 50000 + }, + { + "id": 2, + "zone_id": 448, + "comment": "Trans-Hub - Sunken Harbor", + "label": 141983, + "leaderboard_id": 2, + "start_dialog": 11387, + "start": { + "orientation": { + "IsIdentity": false, + "W": 0.3392906, + "X": 0.014921068, + "Y": 0.041290212, + "Z": 0.9396565 + }, + "position": { + "X": -1023.555908, + "Y": -910.066528, + "Z": 429.218689 + } + }, + "finish": { + "orientation": { + "IsIdentity": false, + "W": -0.50619423, + "X": 0, + "Y": 0, + "Z": 0.8624195 + }, + "position": { + "X": 627.695129, + "Y": -1452.656860, + "Z": 411.751312 + } + }, + "terminal": { + "orientation": { + "IsIdentity": false, + "W": -0.97638285, + "X": 0, + "Y": 0, + "Z": 0.2160475 + }, + "position": { + "X": -1028.4078, + "Y": -907.1568, + "Z": 428.6798 + } + }, + "time_limit_ms": 180000, + "bonus_time_limit_ms": 120000 + }, + { + "id": 3, + "zone_id": 448, + "comment": "Sunken Harbor - Copacabana", + "label": 135929, + "leaderboard_id": 3, + "start_dialog": 11012, + "start": { + "orientation": { + "IsIdentity": false, + "W": 0.7648511, + "X": -0.012024249, + "Y": -0.010168955, + "Z": -0.6440147 + }, + "position": { + "X": 642.516479, + "Y": -1483.968628, + "Z": 414.576904 + } + }, + "finish": { + "orientation": { + "IsIdentity": false, + "W": 0.044414524, + "X": 0, + "Y": 0, + "Z": 0.9990132 + }, + "position": { + "X": -350.430237, + "Y": -421.046906, + "Z": 415.034119 + } + }, + "terminal": { + "orientation": { + "IsIdentity": false, + "W": 0.9999156, + "X": 0, + "Y": 0, + "Z": 0.012992493 + }, + "position": { + "X": 646.72455, + "Y": -1488.7458, + "Z": 414.7206 + } + }, + "time_limit_ms": 160000, + "bonus_time_limit_ms": 100000 + }, + { + "id": 4, + "zone_id": 448, + "comment": "Copacabana - Thump Dump", + "label": 151665, + "leaderboard_id": 4, + "start_dialog": 11585, + "start": { + "orientation": { + "IsIdentity": false, + "W": 0.9566996, + "X": 0.071605094, + "Y": 0.026196381, + "Z": -0.28091335 + }, + "position": { + "X": -667.258972, + "Y": -329.081451, + "Z": 442.637207 + } + }, + "finish": { + "orientation": { + "IsIdentity": false, + "W": -0.6432496, + "X": 0.0, + "Y": 0.0, + "Z": 0.76565653 + }, + "position": { + "X": -1644.012207, + "Y": 1294.188232, + "Z": 492.192047 + } + }, + "terminal": { + "orientation": { + "IsIdentity": false, + "W": -0.31347674, + "X": 0, + "Y": 0, + "Z": 0.94959587 + }, + "position": { + "X": -667.1653, + "Y": -324.08643, + "Z": 441.51447 + } + }, + "time_limit_ms": 180000, + "bonus_time_limit_ms": 120000 + }, + { + "id": 5, + "zone_id": 448, + "comment": "Thump Dump - Copacabana", + "label": 135929, + "leaderboard_id": 5, + "start_dialog": 11012, + "start": { + "orientation": { + "IsIdentity": false, + "W": 0.5659935, + "X": -0.03600099, + "Y": 0.05213954, + "Z": 0.82197124 + }, + "position": { + "X": -1653.038086, + "Y": 1294.078857, + "Z": 490.870789 + } + }, + "finish": { + "orientation": { + "IsIdentity": false, + "W": 0.9220468, + "X": 0, + "Y": 0, + "Z": 0.38707846 + }, + "position": { + "X": -683.523499, + "Y": -322.812042, + "Z": 441.364685 + } + }, + "terminal": { + "orientation": { + "IsIdentity": false, + "W": -0.8908104, + "X": 0, + "Y": 0, + "Z":0.45437527 + }, + "position": { + "X": -1649.3523, + "Y": 1289.0253, + "Z": 491.0055 + } + }, + "time_limit_ms": 180000, + "bonus_time_limit_ms": 120000 + } +] \ No newline at end of file diff --git a/UdpHosts/GameServer/StaticDB/Loaders/CustomDBLoader.cs b/UdpHosts/GameServer/StaticDB/Loaders/CustomDBLoader.cs index ce644fb..b8065c8 100644 --- a/UdpHosts/GameServer/StaticDB/Loaders/CustomDBLoader.cs +++ b/UdpHosts/GameServer/StaticDB/Loaders/CustomDBLoader.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json; using GameServer.Data.SDB.Records.customdata; +using GameServer.StaticDB.Records.customdata.Encounters; using Shared.Common; public class CustomDBLoader @@ -895,9 +896,16 @@ public Dictionary> LoadOutpost() .ToDictionary(group => group.Key, group => group.ToDictionary(row => row.Id, row => row)); } - public Dictionary> LoadMeldingRepulsor() + public Dictionary> LoadMeldingRepulsor() { - return LoadJSON("./StaticDB/CustomData/meldingRepulsor.json") + return LoadJSON("./StaticDB/CustomData/meldingRepulsor.json") + .GroupBy(row => row.ZoneId) + .ToDictionary(group => group.Key, group => group.ToDictionary(row => row.Id, row => row)); + } + + public Dictionary> LoadLgvRace() + { + return LoadJSON("./StaticDB/CustomData/lgv_race.json") .GroupBy(row => row.ZoneId) .ToDictionary(group => group.Key, group => group.ToDictionary(row => row.Id, row => row)); } diff --git a/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/IEncounterDef.cs b/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/IEncounterDef.cs new file mode 100644 index 0000000..59b4ef3 --- /dev/null +++ b/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/IEncounterDef.cs @@ -0,0 +1,5 @@ +namespace GameServer.StaticDB.Records.customdata.Encounters; + +public interface IEncounterDef +{ +} \ No newline at end of file diff --git a/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/LgvRaceDef.cs b/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/LgvRaceDef.cs new file mode 100644 index 0000000..caf97a9 --- /dev/null +++ b/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/LgvRaceDef.cs @@ -0,0 +1,23 @@ +using System.Numerics; + +namespace GameServer.StaticDB.Records.customdata.Encounters; + +public record LgvRaceDef : IEncounterDef +{ + public uint Id { get; set; } + public uint ZoneId { get; set; } + public uint LeaderboardId { get; set; } + public uint Label { get; set; } + public uint StartDialog { get; set; } + public SpawnPose Start { get; set; } + public SpawnPose Finish { get; set; } + public SpawnPose Terminal { get; set; } + public uint TimeLimitMs { get; set; } + public uint BonusTimeLimitMs { get; set; } +} + +public record SpawnPose +{ + public Vector3 Position { get; set; } + public Quaternion Orientation { get; set; } +} diff --git a/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/MeldingRepulsor.cs b/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/MeldingRepulsorDef.cs similarity index 72% rename from UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/MeldingRepulsor.cs rename to UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/MeldingRepulsorDef.cs index ca14fad..516e838 100644 --- a/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/MeldingRepulsor.cs +++ b/UdpHosts/GameServer/StaticDB/Records/customdata/Encounters/MeldingRepulsorDef.cs @@ -1,8 +1,9 @@ using System.Numerics; +using GameServer.Data.SDB.Records.customdata; -namespace GameServer.Data.SDB.Records.customdata; +namespace GameServer.StaticDB.Records.customdata.Encounters; -public record MeldingRepulsor +public record MeldingRepulsorDef : IEncounterDef { public uint Id { get; set; } public uint ZoneId { get; set; } diff --git a/UdpHosts/GameServer/Systems/Aptitude/Commands/Interaction/EndInteractionCommand.cs b/UdpHosts/GameServer/Systems/Aptitude/Commands/Interaction/EndInteractionCommand.cs index 68ff4de..439bb9d 100644 --- a/UdpHosts/GameServer/Systems/Aptitude/Commands/Interaction/EndInteractionCommand.cs +++ b/UdpHosts/GameServer/Systems/Aptitude/Commands/Interaction/EndInteractionCommand.cs @@ -46,6 +46,11 @@ public bool Execute(Context context) return true; } + if (hack.Encounter is { SpawnDef: { } spawnData }) + { + context.Shard.EncounterMan.Factory.SpawnEncounter(spawnData, (CharacterEntity)actingEntity); + } + var abilityId = hack.Interaction.CompletedAbilityId; if (abilityId != 0) { diff --git a/UdpHosts/GameServer/Systems/Encounters/BaseEncounter.cs b/UdpHosts/GameServer/Systems/Encounters/BaseEncounter.cs index ef2ca68..5b3e0ee 100644 --- a/UdpHosts/GameServer/Systems/Encounters/BaseEncounter.cs +++ b/UdpHosts/GameServer/Systems/Encounters/BaseEncounter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Aero.Gen; using AeroMessages.Common; using AeroMessages.GSS.V66.Generic; @@ -18,11 +19,17 @@ protected BaseEncounter(IShard shard, ulong entityId, HashSet pa Participants = participants; } + protected BaseEncounter(IShard shard, ulong entityId, INetworkPlayer soloParticipant) + : this(shard, entityId, new HashSet() { soloParticipant }) + { + } + public IShard Shard { get; protected set; } public ulong EntityId { get; } public EntityId AeroEntityId { get; protected set; } public HashSet Participants { get; } + public INetworkPlayer SoloParticipant => Participants.Single(); public virtual IAeroEncounter View => null; public virtual void OnUpdate(ulong currentTime) @@ -53,4 +60,12 @@ protected void PlayDialog(uint id) p.NetChannels[ChannelType.ReliableGss].SendMessage(msg, p.CharacterEntity.EntityId); } } + + protected void RewardWithResource(uint resourceId, uint quantity) + { + foreach (var p in Participants) + { + p.Inventory.AddResource(resourceId, quantity); + } + } } \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs b/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs index 55d0b9f..5e17c32 100644 --- a/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs +++ b/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs @@ -17,7 +17,9 @@ namespace GameServer.Systems.Encounters; public class EncounterManager { - private const ulong _updateFlushIntervalMs = 20; + public Factory Factory; + + private const ulong _updateFlushIntervalMs = 40; private const ulong _lifetimeCheckIntervalMs = 1000; private readonly Shard Shard; @@ -25,6 +27,7 @@ public class EncounterManager private ulong _lastLifetimeCheck = 0; private bool _hasSpawnedZoneEncounters = false; + public Dictionary EntitiesToCheckProximity = new Dictionary(); private Dictionary UiQueries = new Dictionary(); private HashSet EncountersToUpdate = new HashSet(); private ConcurrentDictionary LifetimeByEncounter = new ConcurrentDictionary(); @@ -32,6 +35,7 @@ public class EncounterManager public EncounterManager(Shard shard) { Shard = shard; + Factory = new Factory(shard); } public void SendUiQuery(NewUiQuery uiQuery, INetworkPlayer target, IEncounter encounter) @@ -81,7 +85,14 @@ public void SpawnZoneEncounters(uint zoneId) foreach (var entry in CustomDBInterface.GetZoneMeldingRepulsors(zoneId)) { var guid = Shard.GetNextGuid((byte)Controller.Encounter); - Shard.Encounters.Add(guid, new MeldingRepulsor(Shard, guid, new HashSet(), entry.Value)); + Add(guid, new MeldingRepulsor(Shard, guid, new HashSet(), entry.Value)); + } + + foreach (var entry in CustomDBInterface.GetZoneLgvRaces(zoneId)) + { + var t = entry.Value.Terminal; + var terminal = Shard.EntityMan.SpawnDeployable(820, t.Position, t.Orientation); + terminal.Encounter = new EncounterComponent() { SpawnDef = entry.Value, Events = EncounterComponent.Event.Interaction }; } } @@ -126,6 +137,22 @@ public void Tick(double deltaTime, ulong currentTime, CancellationToken ct) // FlushChanges(encounter); } + + foreach (var (entity, encounter) in EntitiesToCheckProximity) + { + foreach (var p in encounter.Participants) + { + if (!Shard.EntityMan.HasScopedInEntity(entity.EntityId, p)) + { + continue; + } + + if (Vector3.Distance(entity.Position, p.CharacterEntity.Position) < entity.Encounter.ProximityDistance) + { + encounter.OnProximity(entity, p); + } + } + } } if (currentTime > _lastLifetimeCheck + _lifetimeCheckIntervalMs) diff --git a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs new file mode 100644 index 0000000..7e5c98d --- /dev/null +++ b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs @@ -0,0 +1,246 @@ +using System.Threading; +using AeroMessages.Common; +using AeroMessages.GSS.V66.AreaVisualData; +using AeroMessages.GSS.V66.Character; +using AeroMessages.GSS.V66.Character.Controller; +using AeroMessages.GSS.V66.Character.Event; +using AeroMessages.GSS.V66.Generic.Event.EncounterView; +using GameServer.Entities; +using GameServer.Entities.AreaVisualData; +using GameServer.Entities.Vehicle; +using GameServer.GRPC; +using GameServer.StaticDB.Records.customdata.Encounters; +using GrpcGameServerAPIClient; +using Command = GrpcGameServerAPIClient.Command; +using SinCardTimer = AeroMessages.GSS.V66.Timer; + +namespace GameServer.Systems.Encounters.Encounters; + +public class LgvRace : BaseEncounter, IExitAttachmentHandler, IProximityHandler, ICanTimeout +{ + private const ushort _terromotoCobra = 48; + private const uint _crystite = 10; + private const uint _mustangDoubloon = 77324; + private const uint _lgvRace = 183671; + private const uint _finalTime = 152919; + private const uint _pfxFinishLine = 212597; + private const ushort _mapMarkerRace = 26; + + public override HudTimerView View { get; } + + private ulong startTime; + private uint leaderboardId; + private uint bonusTimeMs; + private VehicleEntity vehicle; + private AreaVisualDataEntity finishLine; + + public LgvRace(IShard shard, ulong entityId, INetworkPlayer soloParticipant, LgvRaceDef data) + : base(shard, entityId, soloParticipant) + { + startTime = Shard.CurrentTimeLong; + Shard.EncounterMan.SetRemainingLifetime(this, data.TimeLimitMs); + + leaderboardId = data.LeaderboardId; + + vehicle = Shard.EntityMan.SpawnVehicle( + _terromotoCobra, + data.Start.Position, + data.Start.Orientation, + SoloParticipant.CharacterEntity, + true); + vehicle.Encounter = new EncounterComponent() + { + EncounterId = entityId, Instance = this, Events = EncounterComponent.Event.ExitAttachment + }; + + View = new HudTimerView() + { + hudtimer_labelProp = data.Label, + hudtimer_timerProp = new SinCardTimer() + { + Micro = (startTime + data.TimeLimitMs) * 1000, + State = SinCardTimer.TimerState.CountingDown, + }, + }; + + PlayDialog(data.StartDialog); + + bonusTimeMs = data.BonusTimeLimitMs; + var bonusTimeDialog = (data.TimeLimitMs - data.BonusTimeLimitMs) switch + { + 30_000 => 11155u, + 45_000 => 11060u, + 60_000 => 11133u, + _ => 0u, + }; + + var timer = new Timer(state => + { + if (shard.Encounters.ContainsKey(EntityId)) + { + PlayDialog(bonusTimeDialog); + } + + ((Timer)state)?.Dispose(); + }); + timer.Change(30_000, Timeout.Infinite); + + finishLine = Shard.EntityMan.SpawnAreaVisualData(data.Finish.Position, new ScopingComponent() { Range = 90 }); + finishLine.AreaVisualData_ParticleEffectsView.ParticleEffects_0Prop = new ParticleEffect() + { + PfxEntityId = AeroEntityId, + PfxAssetId = _pfxFinishLine, + Position = data.Finish.Position, + Rotation = data.Finish.Orientation, + Unk9 = 1, + Unk10 = 1, + Scale = 0.7f, + HaveUnk4 = 0, + HaveUnk12 = 0, + }; + finishLine.Encounter = new EncounterComponent() + { + EncounterId = entityId, + Instance = this, + Events = EncounterComponent.Event.Proximity, + ProximityDistance = 3, + }; + Shard.EncounterMan.EntitiesToCheckProximity.Add(finishLine, this); + + SoloParticipant.CharacterEntity.AddMapMarker( + EntityId, + new PersonalMapMarkerData() + { + EncounterId = AeroEntityId, + EncounterMarkerId = + new EntityId() + { + Backing = AeroEntityId.Backing, + ControllerId = Controller.Generic, + }, + TitleId = data.Label, + HasDuration = 0, + Position = data.Finish.Position, + MarkerType = _mapMarkerRace, + }); + } + + public void OnExitAttachment(BaseEntity targetEntity, INetworkPlayer player) + { + OnFailure(); + } + + public void OnProximity(BaseEntity sourceEntity, INetworkPlayer player) + { + if (player != SoloParticipant) + { + return; + } + + OnSuccess(); + } + + public void OnTimeOut() + { + var dialogs = new uint[] { 11015, 11587, 19278 }; + PlayDialog(dialogs[Rng.Next(dialogs.Length)]); + + OnFailure(); + } + + public override void OnSuccess() + { + RemoveEntities(); + + uint exp; + + var time = Shard.CurrentTimeLong - startTime; + + if (time < bonusTimeMs) + { + exp = 5000; + PlayDialog(11014); + RewardWithResource(_mustangDoubloon, 1); + } + else + { + exp = 3500; + PlayDialog(11586); + } + + var crystite = (uint)Rng.Next(70, 90); + + RewardWithResource(_crystite, crystite); + + var msg = new DisplayRewards() + { + ResourceTargetId = new EntityId() { Backing = 0 }, + Experience = exp, + Reputations = [], + Rewards1 = new RewardInfoData[] + { + new RewardInfoData() + { + Unk = 0, + Boosted = 0, + SdbId = _crystite, + Quantity = (ushort)crystite, + Quality = 0, + Module1 = 0, + Module2 = 0, + }, + new RewardInfoData() + { + Boosted = 0, + SdbId = _mustangDoubloon, + Quantity = 1, + Quality = 0, + Unk = 0, + Module1 = 0, + Module2 = 0 + } + }, + Rewards2 = [], + Stats = new StatInfo[] + { + new StatInfo() + { + TitleId = _finalTime, Type = 2, Value = time / 1000f, Unk3 = string.Empty, + } + }, + Unk1 = 0, + IndexId = 1, + DisplayQuality = 0, + ScreenType = 0, + ResourceTargetType = 0, + TitleTextId = _lgvRace, + }; + + SoloParticipant.NetChannels[ChannelType.ReliableGss].SendMessage(msg, SoloParticipant.CharacterEntity.EntityId); + + _ = GRPCService.SendCommandAsync(new Command() + { + SaveLgvRaceFinish = new SaveLgvRaceFinish() + { + CharacterGuid = SoloParticipant.CharacterId + 0xFE, + LeaderboardId = leaderboardId, + TimeMs = time, + } + }); + + base.OnSuccess(); + } + + public override void OnFailure() + { + RemoveEntities(); + + base.OnFailure(); + } + + private void RemoveEntities() + { + Shard.EntityMan.Remove(finishLine); + SoloParticipant.CharacterEntity.RemoveEncounterMapMarkers(EntityId); + } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Encounters/Encounters/MeldingRepulsor.cs b/UdpHosts/GameServer/Systems/Encounters/Encounters/MeldingRepulsor.cs index 4609106..a7c7108 100644 --- a/UdpHosts/GameServer/Systems/Encounters/Encounters/MeldingRepulsor.cs +++ b/UdpHosts/GameServer/Systems/Encounters/Encounters/MeldingRepulsor.cs @@ -10,6 +10,7 @@ using GameServer.Entities.Character; using GameServer.Entities.Deployable; using GameServer.Entities.Melding; +using GameServer.StaticDB.Records.customdata.Encounters; namespace GameServer.Systems.Encounters.Encounters; @@ -35,14 +36,14 @@ public class MeldingRepulsor : BaseEncounter, IInteractionHandler, IDonationHand private ulong StartTime = 0; private uint MeldedCrystite = 0; - public MeldingRepulsor(IShard shard, ulong entityId, HashSet participants, Data.SDB.Records.customdata.MeldingRepulsor repulsor) + public MeldingRepulsor(IShard shard, ulong entityId, HashSet participants, MeldingRepulsorDef repulsorDef) : base(shard, entityId, participants) { - var r = repulsor.Repulsor; + var r = repulsorDef.Repulsor; _repulsor = Shard.EntityMan.SpawnDeployable(r.Type, r.Position, r.Orientation); _repulsor.Encounter = new EncounterComponent { EncounterId = entityId, Instance = this, Events = EncounterComponent.Event.Signal }; - var t = repulsor.Terminal; + var t = repulsorDef.Terminal; _terminal = Shard.EntityMan.SpawnDeployable(t.Type, t.Position, t.Orientation); _terminal.Encounter = new EncounterComponent() { EncounterId = entityId, Instance = this, Events = EncounterComponent.Event.Interaction }; @@ -56,13 +57,13 @@ public MeldingRepulsor(IShard shard, ulong entityId, HashSet par }; _melding = (MeldingEntity)Shard.Entities.Values.First(e => e is MeldingEntity meldingEntity - && meldingEntity.PerimiterSetName == repulsor.PerimiterSetName); + && meldingEntity.PerimiterSetName == repulsorDef.PerimiterSetName); var adp = _melding.Melding_ObserverView.ActiveDataProp; - _controlPointIndex = repulsor.MeldingPosition.ControlPointIndex; + _controlPointIndex = repulsorDef.MeldingPosition.ControlPointIndex; - _startPosition = repulsor.MeldingPosition.Position; + _startPosition = repulsorDef.MeldingPosition.Position; _endPosition = adp.ControlPoints_1[_controlPointIndex]; adp.ControlPoints_1[_controlPointIndex] = _startPosition; diff --git a/UdpHosts/GameServer/Systems/Encounters/Factory.cs b/UdpHosts/GameServer/Systems/Encounters/Factory.cs new file mode 100644 index 0000000..1b1ec58 --- /dev/null +++ b/UdpHosts/GameServer/Systems/Encounters/Factory.cs @@ -0,0 +1,21 @@ +using AeroMessages.Common; +using GameServer.Entities.Character; +using GameServer.StaticDB.Records.customdata.Encounters; +using LgvRace = GameServer.Systems.Encounters.Encounters.LgvRace; + +namespace GameServer.Systems.Encounters; + +public class Factory(Shard shard) +{ + public void SpawnEncounter(IEncounterDef def, CharacterEntity initiator) + { + var guid = shard.GetNextGuid((byte)Controller.Encounter); + + switch (def) + { + case LgvRaceDef lgvRace: + shard.EncounterMan.Add(guid, new LgvRace(shard, guid, initiator.Player, lgvRace)); + break; + } + } +} \ No newline at end of file diff --git a/UdpHosts/GameServer/Systems/Encounters/IEncounterHandlers.cs b/UdpHosts/GameServer/Systems/Encounters/IEncounterHandlers.cs index 1cf7e1c..83ce8f2 100644 --- a/UdpHosts/GameServer/Systems/Encounters/IEncounterHandlers.cs +++ b/UdpHosts/GameServer/Systems/Encounters/IEncounterHandlers.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using AeroMessages.GSS.V66.Character.Command; using GameServer.Entities; @@ -12,3 +13,14 @@ public interface IDonationHandler { void OnDonation(UiQueryResponse response, INetworkPlayer player); } + +public interface IProximityHandler +{ + public HashSet Participants { get; } + void OnProximity(BaseEntity sourceEntity, INetworkPlayer player); +} + +public interface IExitAttachmentHandler +{ + void OnExitAttachment(BaseEntity targetEntity, INetworkPlayer player); +} From fbff88b46954688a7e1633cd0310fec86d0a5ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:22:19 +0100 Subject: [PATCH 08/12] Handle adding personal map markers --- .../Entities/Character/CharacterEntity.cs | 57 ++++++++++++++++++- .../Systems/Encounters/MapMarkerState.cs | 9 +++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 UdpHosts/GameServer/Systems/Encounters/MapMarkerState.cs diff --git a/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs b/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs index 30dd10e..dd56cb6 100644 --- a/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs +++ b/UdpHosts/GameServer/Entities/Character/CharacterEntity.cs @@ -13,6 +13,7 @@ using GameServer.Data.SDB; using GameServer.Data.SDB.Records.customdata; using GameServer.Enums; +using GameServer.Systems.Encounters; using GameServer.Test; using GrpcGameServerAPIClient; using LoadoutVisualType = AeroMessages.GSS.V66.Character.LoadoutConfig_Visual.LoadoutVisualType; @@ -22,8 +23,11 @@ namespace GameServer.Entities.Character; /// /// Base Character /// -public partial class CharacterEntity : BaseAptitudeEntity, IAptitudeTarget +public sealed partial class CharacterEntity : BaseAptitudeEntity, IAptitudeTarget { + public const byte MaxMapMarkerCount = 64; + private MapMarkerState[] MapMarkers = new MapMarkerState[MaxMapMarkerCount]; + public CharacterEntity(IShard shard, ulong eid) : base(shard, eid) { @@ -1043,6 +1047,51 @@ public void HackClearAllStatusEffects() Shard.EntityMan.FlushChanges(this); } + public void AddMapMarker(ulong encounterId, PersonalMapMarkerData data) + { + byte firstFreeIndex = InvalidIndex; + for (byte i = 0; i < MaxMapMarkerCount; i++) + { + if (MapMarkers[i] == null) + { + firstFreeIndex = i; + break; + } + } + + if (firstFreeIndex == InvalidIndex) + { + Console.WriteLine("AddMapMarkers but there are too many active map markers!"); + firstFreeIndex = MaxMapMarkerCount - 1; // Lets not crash + } + + var state = new MapMarkerState + { + EncounterId = data.EncounterId, + EncounterMarkerId = data.EncounterMarkerId, + }; + + MapMarkers[firstFreeIndex] = state; + + SetMapMarker(firstFreeIndex, data); + } + + public void RemoveEncounterMapMarkers(ulong encounterId) + { + for (byte i = 0; i < MapMarkers.Length; i++) + { + if (MapMarkers[i] == null) + { + continue; + } + + if (MapMarkers[i].EncounterId.Backing == encounterId) + { + SetMapMarker(i, null); + } + } + } + public void SetCombatFlags(CombatFlagsData value) { Character_CombatController.CombatFlagsProp = value; @@ -1551,6 +1600,12 @@ private ulong GetCurrentPermissionsValue() return result; } + private void SetMapMarker(byte index, PersonalMapMarkerData? data) + { + Character_MissionAndMarkerController?.GetType().GetProperty($"PersonalMapMarkers_{index}Prop") + ?.SetValue(Character_MissionAndMarkerController, data); + } + public class ActiveStatModifier { public StatModifierIdentifier Stat { get; set; } diff --git a/UdpHosts/GameServer/Systems/Encounters/MapMarkerState.cs b/UdpHosts/GameServer/Systems/Encounters/MapMarkerState.cs new file mode 100644 index 0000000..a26f054 --- /dev/null +++ b/UdpHosts/GameServer/Systems/Encounters/MapMarkerState.cs @@ -0,0 +1,9 @@ +using AeroMessages.Common; + +namespace GameServer.Systems.Encounters; + +public class MapMarkerState +{ + public EntityId EncounterId; + public EntityId EncounterMarkerId; +} \ No newline at end of file From d82dda12a2caacea2e1ad0e79949e3c9bf376485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sat, 21 Dec 2024 22:24:25 +0100 Subject: [PATCH 09/12] Update field name --- UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs index 7e5c98d..44f3617 100644 --- a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs +++ b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs @@ -205,7 +205,7 @@ public override void OnSuccess() { new StatInfo() { - TitleId = _finalTime, Type = 2, Value = time / 1000f, Unk3 = string.Empty, + NameId = _finalTime, Type = 2, Value = time / 1000f, Unk3 = string.Empty, } }, Unk1 = 0, From 11a30ca32cf28ebd7041a18152303e1adf2e2e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sun, 22 Dec 2024 09:20:48 +0100 Subject: [PATCH 10/12] Fix CI warnings --- ...aponArmed.cs => RequireWeaponArmedCommand.cs} | 0 .../Systems/Encounters/EncounterManager.cs | 7 ++++++- .../Systems/Encounters/Encounters/LgvRace.cs | 16 +++++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) rename UdpHosts/GameServer/Systems/Aptitude/Commands/Requirement/{RequireWeaponArmed.cs => RequireWeaponArmedCommand.cs} (100%) diff --git a/UdpHosts/GameServer/Systems/Aptitude/Commands/Requirement/RequireWeaponArmed.cs b/UdpHosts/GameServer/Systems/Aptitude/Commands/Requirement/RequireWeaponArmedCommand.cs similarity index 100% rename from UdpHosts/GameServer/Systems/Aptitude/Commands/Requirement/RequireWeaponArmed.cs rename to UdpHosts/GameServer/Systems/Aptitude/Commands/Requirement/RequireWeaponArmedCommand.cs diff --git a/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs b/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs index 5e17c32..6cf25bd 100644 --- a/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs +++ b/UdpHosts/GameServer/Systems/Encounters/EncounterManager.cs @@ -27,7 +27,7 @@ public class EncounterManager private ulong _lastLifetimeCheck = 0; private bool _hasSpawnedZoneEncounters = false; - public Dictionary EntitiesToCheckProximity = new Dictionary(); + private Dictionary EntitiesToCheckProximity = new Dictionary(); private Dictionary UiQueries = new Dictionary(); private HashSet EncountersToUpdate = new HashSet(); private ConcurrentDictionary LifetimeByEncounter = new ConcurrentDictionary(); @@ -114,6 +114,11 @@ public void StopUpdatingEncounter(IEncounter encounter) EncountersToUpdate.Remove(encounter); } + public void AddCheckingOfProximity(BaseEntity entity, IProximityHandler encounter) + { + EntitiesToCheckProximity.Add(entity, encounter); + } + public void Tick(double deltaTime, ulong currentTime, CancellationToken ct) { if (!_hasSpawnedZoneEncounters && currentTime != 0) diff --git a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs index 44f3617..94f3664 100644 --- a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs +++ b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs @@ -7,7 +7,6 @@ using AeroMessages.GSS.V66.Generic.Event.EncounterView; using GameServer.Entities; using GameServer.Entities.AreaVisualData; -using GameServer.Entities.Vehicle; using GameServer.GRPC; using GameServer.StaticDB.Records.customdata.Encounters; using GrpcGameServerAPIClient; @@ -26,12 +25,9 @@ public class LgvRace : BaseEncounter, IExitAttachmentHandler, IProximityHandler, private const uint _pfxFinishLine = 212597; private const ushort _mapMarkerRace = 26; - public override HudTimerView View { get; } - private ulong startTime; private uint leaderboardId; private uint bonusTimeMs; - private VehicleEntity vehicle; private AreaVisualDataEntity finishLine; public LgvRace(IShard shard, ulong entityId, INetworkPlayer soloParticipant, LgvRaceDef data) @@ -42,7 +38,7 @@ public LgvRace(IShard shard, ulong entityId, INetworkPlayer soloParticipant, Lgv leaderboardId = data.LeaderboardId; - vehicle = Shard.EntityMan.SpawnVehicle( + var vehicle = Shard.EntityMan.SpawnVehicle( _terromotoCobra, data.Start.Position, data.Start.Orientation, @@ -105,7 +101,7 @@ public LgvRace(IShard shard, ulong entityId, INetworkPlayer soloParticipant, Lgv Events = EncounterComponent.Event.Proximity, ProximityDistance = 3, }; - Shard.EncounterMan.EntitiesToCheckProximity.Add(finishLine, this); + Shard.EncounterMan.AddCheckingOfProximity(finishLine, this); SoloParticipant.CharacterEntity.AddMapMarker( EntityId, @@ -125,6 +121,8 @@ public LgvRace(IShard shard, ulong entityId, INetworkPlayer soloParticipant, Lgv }); } + public override HudTimerView View { get; } + public void OnExitAttachment(BaseEntity targetEntity, INetworkPlayer player) { OnFailure(); @@ -176,7 +174,7 @@ public override void OnSuccess() { ResourceTargetId = new EntityId() { Backing = 0 }, Experience = exp, - Reputations = [], + Reputations = new ReputationInfo[] { }, Rewards1 = new RewardInfoData[] { new RewardInfoData() @@ -200,12 +198,12 @@ public override void OnSuccess() Module2 = 0 } }, - Rewards2 = [], + Rewards2 = new RewardInfoData[] { }, Stats = new StatInfo[] { new StatInfo() { - NameId = _finalTime, Type = 2, Value = time / 1000f, Unk3 = string.Empty, + NameId = _finalTime, Type = StatInfo.StatType.Time, Value = time / 1000f, Unk3 = string.Empty, } }, Unk1 = 0, From a259697e621d70d89e9077cd6349a00d85eb8d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sun, 22 Dec 2024 09:25:44 +0100 Subject: [PATCH 11/12] Update AeroMessages --- Lib/AeroMessages | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/AeroMessages b/Lib/AeroMessages index 6712d05..61de410 160000 --- a/Lib/AeroMessages +++ b/Lib/AeroMessages @@ -1 +1 @@ -Subproject commit 6712d05e5e13516113bb783e63a0725ed0fe9421 +Subproject commit 61de410c85ade361fd6dd20305c932e02302adfb From e11dce6bcc8669f2ecf7438a788c37b8dadc1ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kami=C5=84ski?= Date: Sun, 22 Dec 2024 11:39:52 +0100 Subject: [PATCH 12/12] Increase scoping of finish line to fix sh -> copa --- UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs index 94f3664..f37222e 100644 --- a/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs +++ b/UdpHosts/GameServer/Systems/Encounters/Encounters/LgvRace.cs @@ -81,7 +81,7 @@ public LgvRace(IShard shard, ulong entityId, INetworkPlayer soloParticipant, Lgv }); timer.Change(30_000, Timeout.Infinite); - finishLine = Shard.EntityMan.SpawnAreaVisualData(data.Finish.Position, new ScopingComponent() { Range = 90 }); + finishLine = Shard.EntityMan.SpawnAreaVisualData(data.Finish.Position, new ScopingComponent() { Range = 150 }); finishLine.AreaVisualData_ParticleEffectsView.ParticleEffects_0Prop = new ParticleEffect() { PfxEntityId = AeroEntityId,