-
Notifications
You must be signed in to change notification settings - Fork 432
4.11 How to add packets
Currently, the project uses many incorrect packet structers, as a result many features in the game doesn't work. In this section we will learn how to implement packet correctly.
First you should know that every packet has its structure in LeaguePackets, so you can use the structure from there.
Every packet is either C2S (Client to Server) or S2C (similarly), we will denote these as Requests and Responses
There are 2 parts:
- GameServerLib - the framework issue the sending, called here 'the server'.
- PacketNotifier - the actual sender, called here 'the notifier'
In the server (GameServerLib) you just call Notify
function in PacketNotifier
, for example:
_game.PacketNotifier.NotifyNpcDie(this, killer);
As you can see, in the server we use just API object like IAttackableUnit.
In the notifier you will need to have of course the packetsending scheme that you want, for example:
public void NotifySkillUp(int userId, uint netId, byte skill, byte level, byte pointsLeft)
{
var skillUpResponse = new SkillUpResponse(netId, skill, level, pointsLeft);
_packetHandlerManager.SendPacket(userId, skillUpResponse, Channel.CHL_GAMEPLAY);
}
What is happening here is actualy sending of BasePacket
object that actually converted to byte[] later when Enet sending the packet (Enet - the network lib we use). We don't enforce to send BasePacket as you can see here:
public void NotifyMinionSpawned(IMinion minion, TeamId team)
{
var spawnPacket = new LeaguePackets.GamePackets.SpawnMinionS2C();
spawnPacket.SkinName = minion.Model;
spawnPacket.Name = minion.Name;
spawnPacket.VisibilitySize = minion.VisionRadius; // Might be incorrect
spawnPacket.IsTargetableToTeam = SpellFlags.TargetableToAll;
spawnPacket.IsTargetable = true;
spawnPacket.IsBot = minion.IsBot;
spawnPacket.IsLaneMinion = minion.IsLaneMinion;
spawnPacket.IsWard = minion.IsWard;
spawnPacket.IgnoreCollision = false;
spawnPacket.TeamID = (TeamID)minion.Team;
// CloneNetID, clones not yet implemented
spawnPacket.SkinID = 0;
spawnPacket.Position = new Vector3(minion.GetPosition().X, minion.GetZ(), minion.GetPosition().Y);
spawnPacket.SenderNetID = (NetID)minion.NetId;
spawnPacket.NetNodeID = NetNodeID.Spawned;
if (minion.IsLaneMinion) // Should probably change/optimize at some point
{
spawnPacket.OwnerNetID = (NetID)minion.Owner.NetId;
}
else
{
spawnPacket.OwnerNetID = (NetID)minion.NetId;
}
spawnPacket.NetID = (NetID)minion.NetId;
spawnPacket.InitialLevel = 1;
var visionPacket = new LeaguePackets.GamePackets.OnEnterVisiblityClient();
var vd = new LeaguePackets.CommonData.VisibilityDataAIMinion();
vd.LookAtPosition = new Vector3(1, 0, 0);
var md = new LeaguePackets.CommonData.MovementDataStop();
md.Position = minion.GetPosition();
md.Forward = new Vector2(0, 1);
vd.MovementSyncID = 0x0006E4CF;
vd.MovementData = md;
visionPacket.VisibilityData = vd;
visionPacket.Packets.Add(spawnPacket);
visionPacket.SenderNetID = (NetID)minion.NetId;
_packetHandlerManager.BroadcastPacketVision(minion, visionPacket.GetBytes(), Channel.CHL_S2C);
}
This is more complex packet, but you see how LeaguePackets handle all the field and then you use GetBytes()
method to get just byte[] to send with Enet.
In this case you have 3 parts:
- GameServerLib handler class - this is the class that get API RequestType (see here: Requests)
- GameServerLib registering - for activating your handler you need to register it (technical reasons)
- PacketReader actual recieving - this class recieving your packet as byte[] after verifying that the opcode is indeed your packet, so this is actually unique handling for this packet type
For the class you just create class like this in Handlers:
using GameServerCore;
using GameServerCore.Packets.Handlers;
using GameServerCore.Packets.PacketDefinitions.Requests;
using LeagueSandbox.GameServer.Items;
namespace LeagueSandbox.GameServer.Packets.PacketHandlers
{
public class HandleBuyItem : PacketHandlerBase<BuyItemRequest>
{
private readonly Game _game;
private readonly ItemManager _itemManager;
private readonly IPlayerManager _playerManager;
public HandleBuyItem(Game game)
{
_game = game;
_itemManager = game.ItemManager;
_playerManager = game.PlayerManager;
}
public override bool HandlePacket(int userId, BuyItemRequest req)
{
var champion = _playerManager.GetPeerInfo((ulong)userId).Champion;
return champion.Shop.HandleItemBuyRequest(req.ItemId);
}
}
}
To register your class so when this packet is handled after PacketReader just add line in this function: https://github.com/LeagueSandbox/GameServer/blob/b2ca11d9197a5800d58fdad36c40fdddd161967a/GameServerLib/Game.cs#L129
And in order that the packet will actually get parsed you need to add function like this to the PacketReader.cs:
[PacketType(PacketCmd.PKT_C2S_CURSOR_POSITION_ON_WORLD)]
public static CursorPositionOnWorldRequest ReadCursorPositionOnWorldRequest(byte[] data)
{
var rq = new CursorPositionOnWorld(data);
return new CursorPositionOnWorldRequest(rq.NetId, rq.Unk1, rq.X, rq.Z, rq.Y);
}
The PacketType attribute is very important, this is actually sorts the packets so the unique packet type that you want to parse will go to this function for parsing - without this it will never get parsed.
CursorPositionOnWorldRequest
here is a class that implements interface that called ICoreRequest
, for example:
namespace GameServerCore.Packets.PacketDefinitions.Requests
{
public class HeartbeatRequest : ICoreRequest
{
public int NetId { get; }
public float ReceiveTime { get; }
public float AckTime { get; }
public HeartbeatRequest(int netId, float receiveTime, float ackTime)
{
NetId = netId;
ReceiveTime = receiveTime;
AckTime = ackTime;
}
}
}
This class represent the actual API request that the server will get, so the server dont need to know about the structure or ever get byte[]. This is full decoupling.
Sometimes you will need to add your API request type and function in PacketReader to handle new type of packet.
You can use LeaguePacket for requests as well - you read the packet and then just take its fields into your new API object.
How that actually works:
Enet get packet -> follow the attribute PacketType
and move the packet to the correct function -> create API request object and return it -> follow some generic sorter that take this request and put it in the correct handler, in order to find this handler when it searches for it you need to register the handler before.
Note: The correct handler is an handler that uses the same request type. So if you used MyLongRequest API request type, you should also make the handler function to be with signature (int, MyLongRequest) like:
int HandleMyPacket(int userId, MyLongRequest myReq)
{
\\ My function for handling this type of request from client...
}
LeagueSandbox is a AGPL-3.0 open-source project