Skip to content
8 changes: 8 additions & 0 deletions NebulaModel/DataStructures/Chat/ChatCommandGiftType.cs
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions NebulaModel/Packets/Chat/ChatCommandGiftPacket.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
17 changes: 17 additions & 0 deletions NebulaModel/Packets/Routers/ClientRelayPacket.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
107 changes: 107 additions & 0 deletions NebulaNetwork/PacketProcessors/Chat/ChatCommandGiftProcessor.cs
Original file line number Diff line number Diff line change
@@ -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<ChatCommandGiftPacket>
{
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);
//}
}
}
1 change: 1 addition & 0 deletions NebulaWorld/Chat/ChatCommandRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
219 changes: 219 additions & 0 deletions NebulaWorld/Chat/Commands/GiftCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -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<ChatCommandGiftPacket> 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 ["<player name|id> <soil|item|energy> <quantity>"];
}

// 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<UserInfo> 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();
}
}
}
Loading