diff --git a/NebulaModel/DataStructures/Chat/ChatCommandGiftType.cs b/NebulaModel/DataStructures/Chat/ChatCommandGiftType.cs new file mode 100644 index 000000000..e31724eb9 --- /dev/null +++ b/NebulaModel/DataStructures/Chat/ChatCommandGiftType.cs @@ -0,0 +1,8 @@ +namespace NebulaModel.DataStructures.Chat; + +public enum ChatCommandGiftType +{ + Soil = 0, // TODO: Since Soil Pile (item id 1099 ) is also an item, we can probably remove this all together and just use Item + Item = 1, + Energy = 2 +} diff --git a/NebulaModel/Packets/Chat/ChatCommandGiftPacket.cs b/NebulaModel/Packets/Chat/ChatCommandGiftPacket.cs new file mode 100644 index 000000000..1f7625096 --- /dev/null +++ b/NebulaModel/Packets/Chat/ChatCommandGiftPacket.cs @@ -0,0 +1,21 @@ +using NebulaModel.DataStructures.Chat; + +namespace NebulaModel.Packets.Chat; + +public class ChatCommandGiftPacket +{ + public ChatCommandGiftPacket() { } + + public ChatCommandGiftPacket(ushort senderUserId, ushort recipientUserId, ChatCommandGiftType type, long quantity) + { + SenderUserId = senderUserId; + RecipientUserId = recipientUserId; + Type = type; + Quantity = quantity; + } + + public ushort SenderUserId { get; set; } + public ushort RecipientUserId { get; set; } + public ChatCommandGiftType Type { get; set; } + public long Quantity { get; set; } +} diff --git a/NebulaModel/Packets/Routers/ClientRelayPacket.cs b/NebulaModel/Packets/Routers/ClientRelayPacket.cs new file mode 100644 index 000000000..4073ee540 --- /dev/null +++ b/NebulaModel/Packets/Routers/ClientRelayPacket.cs @@ -0,0 +1,17 @@ +namespace NebulaModel.Packets.Routers; + +public class ClientRelayPacket +{ + public ClientRelayPacket() { } + + public ClientRelayPacket(byte[] packetObject, ushort clientUserId) + { + // TODO: We should probably rename this to PacketObjectToRelay or something + PacketObject = packetObject; + // TODO: We should probably rename this to RecipientClientUserId or something + ClientUserId = clientUserId; + } + + public byte[] PacketObject { get; set; } + public ushort ClientUserId { get; set; } +} diff --git a/NebulaNetwork/PacketProcessors/Chat/ChatCommandGiftProcessor.cs b/NebulaNetwork/PacketProcessors/Chat/ChatCommandGiftProcessor.cs new file mode 100644 index 000000000..a4c452ba4 --- /dev/null +++ b/NebulaNetwork/PacketProcessors/Chat/ChatCommandGiftProcessor.cs @@ -0,0 +1,107 @@ +#region + +using System; +using NebulaAPI.Packets; +using NebulaModel.DataStructures.Chat; +using NebulaModel.Logger; +using NebulaModel.Networking; +using NebulaModel.Packets; +using NebulaModel.Packets.Chat; +using NebulaWorld; +using NebulaWorld.Chat.Commands; +using NebulaWorld.MonoBehaviours.Local.Chat; +using static UnityEngine.Analytics.Analytics; + +#endregion + +namespace NebulaNetwork.PacketProcessors.Chat; + +[RegisterPacketProcessor] +internal class ChatCommandGiftProcessor : PacketProcessor +{ + protected override void ProcessPacket(ChatCommandGiftPacket packet, NebulaConnection conn) + { + // If you are not the intended recipient of this packet do not process this packet + if (packet.RecipientUserId != Multiplayer.Session.LocalPlayer.Data.PlayerId) + { + // However if you are the host relay the packet to the recipient + if (IsHost) + { + var recipient = Multiplayer.Session.Network.PlayerManager.GetPlayerById(packet.RecipientUserId); + if (recipient != null) + { + recipient.SendPacket(packet); + } + else + { + Log.Warn($"Could not relay packet because recipient was not found with clientId: {packet.RecipientUserId}"); + // TODO: if the recipient is not found return the failure packet + //conn.SendPacket(giftFailedPacket); // the giftFailedPacket needs the same kind of handling as that can also need to be relayed + } + } + return; + } + + //window.SendLocalChatMessage("Invalid gift type".Translate(), ChatMessageType.CommandErrorMessage); + string senderUserName = null; + // TODO: Unify this into something + using (Multiplayer.Session.World.GetRemotePlayersModels(out var remotePlayersModels)) + { + foreach (var remotePlayerModel in remotePlayersModels) + { + var movement = remotePlayerModel.Value.Movement; + if (movement.PlayerID == packet.SenderUserId) + { + senderUserName = movement.Username; + break; + } + } + } + + switch (packet.Type) + { + case ChatCommandGiftType.Soil: + var mainPlayer = GameMain.data.mainPlayer; + lock (mainPlayer) + { + mainPlayer.SetSandCount(mainPlayer.sandCount + packet.Quantity); + // TODO: Do we need to do something with soil sync? + } + ChatManager.Instance.SendChatMessage($"[{DateTime.Now:HH:mm}] [{packet.SenderUserId}] {senderUserName} gifted you soil ({packet.Quantity})", ChatMessageType.SystemInfoMessage); + break; + // TODO: Implement Item and Energy variants. + default: + return; + } + + // TODO: Logic for adding the soil, items, energy etc (restransmission logic no longer needed with the ClientRelayPacket + + //if (IsClient) + //{ + // WhisperCommandHandler.SendWhisperToLocalPlayer(packet.SenderUsername, packet.Message); + //} + //else + //{ + // // two cases, simplest is that whisper is meant for host + // if (Multiplayer.Session.LocalPlayer.Data.Username == packet.RecipientUsername) + // { + // WhisperCommandHandler.SendWhisperToLocalPlayer(packet.SenderUsername, packet.Message); + // return; + // } + + // // second case, relay message to recipient + // var recipient = Multiplayer.Session.Network + // .PlayerManager.GetConnectedPlayerByUsername(packet.RecipientUsername); + // if (recipient == null) + // { + // Log.Warn($"Recipient not found {packet.RecipientUsername}"); + // var sender = Multiplayer.Session.Network.PlayerManager.GetPlayer(conn); + // sender.SendPacket(new ChatCommandWhisperPacket("SYSTEM".Translate(), packet.SenderUsername, + // string.Format("User not found {0}".Translate(), packet.RecipientUsername))); + // return; + // } + + // recipient.SendPacket(packet); + //} + } +} diff --git a/NebulaWorld/Chat/ChatCommandRegistry.cs b/NebulaWorld/Chat/ChatCommandRegistry.cs index d271d91d4..ca3addca1 100644 --- a/NebulaWorld/Chat/ChatCommandRegistry.cs +++ b/NebulaWorld/Chat/ChatCommandRegistry.cs @@ -27,6 +27,7 @@ static ChatCommandRegistry() RegisterCommand("system", new SystemCommandHandler(), "s"); RegisterCommand("reconnect", new ReconnectCommandHandler(), "r"); RegisterCommand("server", new ServerCommandHandler()); + RegisterCommand("gift", new GiftCommandHandler(), "g"); } private static void RegisterCommand(string commandName, IChatCommandHandler commandHandlerHandler, params string[] aliases) diff --git a/NebulaWorld/Chat/Commands/GiftCommandHandler.cs b/NebulaWorld/Chat/Commands/GiftCommandHandler.cs new file mode 100644 index 000000000..dd12a15a6 --- /dev/null +++ b/NebulaWorld/Chat/Commands/GiftCommandHandler.cs @@ -0,0 +1,219 @@ +#region + +using System; +using System.Collections.Generic; +using System.Linq; +using NebulaModel.DataStructures.Chat; +using NebulaModel.Logger; +using NebulaModel.Packets.Chat; +using NebulaWorld.MonoBehaviours.Local.Chat; + +#endregion + +namespace NebulaWorld.Chat.Commands; + +public class GiftCommandHandler : IChatCommandHandler +{ + private struct UserInfo + { + public ushort id; + public string name; + } + + public void Execute(ChatWindow window, string[] parameters) + { + if (parameters.Length < 3) + { + throw new ChatCommandUsageException("Not enough arguments!".Translate()); + } + + UserInfo sender; + { + if ( + Multiplayer.Session?.LocalPlayer?.Data?.PlayerId is ushort senderUserId + && Multiplayer.Session?.LocalPlayer?.Data?.Username is string senderUsername + ) + { + sender = new UserInfo + { + id = senderUserId, + name = senderUsername + }; + } + else + { + window.SendLocalChatMessage("Invalid sender (not connected), can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + }; + } + + UserInfo recipient; + { + var userIdOrNameParameter = parameters[0]; + var couldParseUserId = ushort.TryParse(userIdOrNameParameter, out var recipientUserId); + + UserInfo? recipientOrNull = null; + if (couldParseUserId) + { + recipientOrNull = getUserInfoById(recipientUserId); + } + + if (recipientOrNull is UserInfo recipientNotNull) + { + recipient = recipientNotNull; + } + else + { + var recipientsByUsername = getUserInfosByUsername(userIdOrNameParameter); + + if (recipientsByUsername.Count == 0) + { + window.SendLocalChatMessage("Invalid recipient (user id or username not found), can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + + if (recipientsByUsername.Count > 1) + { + window.SendLocalChatMessage("Ambiguous recipient (multiple recipients with same username), can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + + recipient = recipientsByUsername.First(); + } + } + + if (sender.id == recipient.id) + { + window.SendLocalChatMessage("Invalid recipient (self), can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + + ChatCommandGiftType type; + switch (parameters[1]) + { + case "soil": + case "sand": + case "s": + type = ChatCommandGiftType.Soil; + break; + // TODO: Implement Item and Energy variants. + default: + window.SendLocalChatMessage("Invalid gift type, can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + + // Add support for scientific notation and other notation types + if (!long.TryParse(parameters[2], out var quantity) || quantity == 0) + { + window.SendLocalChatMessage("Invalid gift quantity, can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + + Action sendPacket; + if (Multiplayer.Session.LocalPlayer.IsHost) + { + // If you are the host, you can directly send the packet to the recipient + var recipientConnection = Multiplayer.Session.Network.PlayerManager.GetPlayerById(recipient.id); + if (recipientConnection == null) + { + window.SendLocalChatMessage("Invalid recipient (no connection), can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + + sendPacket = (packet) => + { + recipientConnection.SendPacket(packet); + }; + } + else + { + // Else send it to the host who can relay it + sendPacket = (packet) => + { + Multiplayer.Session.Network.SendPacket(packet); + }; + } + + // Validate that you actually have the required soil/items/energy to gift + switch (type) + { + case ChatCommandGiftType.Soil: + var packet = new ChatCommandGiftPacket(sender.id, recipient.id, type, quantity); + var mainPlayer = GameMain.data.mainPlayer; + bool sufficient; + lock (mainPlayer) + { + var remainingSand = mainPlayer.sandCount - quantity; + sufficient = remainingSand >= 0; + if (sufficient) + { + sendPacket(packet); + mainPlayer.SetSandCount(remainingSand); + // TODO: Do we need to do something with soil sync? + } + } + + if (!sufficient) + { + window.SendLocalChatMessage("You dont have enough soil to send, can't send gift".Translate(), ChatMessageType.CommandErrorMessage); + return; + } + // TODO: I don't think this is translatable since it contains dynamic data, look into this + window.SendLocalChatMessage($"[{DateTime.Now:HH:mm}] You gifted [{recipient.id}] {recipient.name} soil ({quantity})".Translate(), ChatMessageType.SystemInfoMessage); + break; + // TODO: Implement Item and Energy variants. + } + } + + public string GetDescription() + { + return string.Format("Send gift to player. Use /who for valid user names. Valid types are soil (s), item (i), energy (e)".Translate()); + } + + public string[] GetUsage() + { + return [" "]; + } + + // TODO: We should add logic here that acctually adds the gifted materials (and devise something to substract the materials) + //public static void SendWhisperToLocalPlayer(string sender, string mesageBody) + //{ + // ChatManager.Instance.SendChatMessage($"[{DateTime.Now:HH:mm}] [{sender} whispered] : {mesageBody}", + // ChatMessageType.PlayerMessagePrivate); + //} + + private UserInfo? getUserInfoById(ushort userId) + { + using (Multiplayer.Session.World.GetRemotePlayersModels(out var remotePlayersModels)) + { + // TODO: This does not include self, perhaps we should include this + var result = remotePlayersModels + .Select(remotePlayerModel => remotePlayerModel.Value.Movement) + .Where(movement => movement.PlayerID == userId) + .Select(movement => new UserInfo + { + id = movement.PlayerID, + name = movement.Username + }); + + return result.Any() ? result.First() : null; + } + } + + private List getUserInfosByUsername(string username) + { + using (Multiplayer.Session.World.GetRemotePlayersModels(out var remotePlayersModels)) + { + // TODO: This does not include self, perhaps we should include this + return remotePlayersModels + .Select(remotePlayerModel => remotePlayerModel.Value.Movement) + .Where(movement => movement.Username == username) + .Select(movement => new UserInfo + { + id = movement.PlayerID, + name = movement.Username + }) + .ToList(); + } + } +} diff --git a/NebulaWorld/MonoBehaviours/Local/Chat/ChatWindow.cs b/NebulaWorld/MonoBehaviours/Local/Chat/ChatWindow.cs index dfddf42f8..aed710a03 100644 --- a/NebulaWorld/MonoBehaviours/Local/Chat/ChatWindow.cs +++ b/NebulaWorld/MonoBehaviours/Local/Chat/ChatWindow.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using NebulaModel; using NebulaModel.DataStructures.Chat; using NebulaModel.Utils; @@ -144,7 +145,13 @@ private void TrySendMessage() if (chatBox.text.StartsWith(ChatCommandRegistry.CommandPrefix)) { - var arguments = chatBox.text.Substring(1).Split(' '); + //var arguments = chatBox.text.Substring(1).Split(' '); + // this handles quoted arguments and supports first lvl escaping using \" + var arguments = Regex.Matches(chatBox.text.Substring(1), @"(?() + .Select(m => Regex.Replace(m.Value, @"^(? 0) { var commandName = arguments[0];