Game Server
Overview
Egress
Buffers
The systems are running in multiple threads with entities partitioned across them. Suppose we have 8 threads[1].
This means that the egress system will send thread-local 8 bytes::BytesMut
[2] to the tokio egress task at the end of every tick.
The contents in the buffers are rkyv-encoded packets specified here:
#[derive(Archive, Deserialize, Serialize, Clone, PartialEq)]
pub struct BroadcastLocal<'a> {
pub center: ChunkPosition,
pub exclude: u64,
pub order: u32,
#[rkyv(with = InlineAsBox)]
pub data: &'a [u8],
}
#[derive(Archive, Deserialize, Serialize, Clone, PartialEq)]
pub struct Unicast<'a> {
pub stream: u64,
pub order: u32,
#[rkyv(with = InlineAsBox)]
pub data: &'a [u8],
}
Ingress
Tokio Async Task
The tokio async ingress task creates a data structure defined here.
#[derive(Default)]
pub struct ReceiveStateInner {
/// All players who have recently connected to the server.
pub player_connect: Vec<u64>,
/// All players who have recently disconnected from the server.
pub player_disconnect: Vec<u64>,
/// A map of stream ids to the corresponding [`BytesMut`] buffers. This represents data from the client to the server.
pub packets: HashMap<u64, BytesMut>,
}
Decoding System
Then, when it is time to run the ingress system, we lock the mutex for ReceiveStateInner
and process the data, decoding all the packets until we get
#[derive(Copy, Clone)]
pub struct BorrowedPacketFrame<'a> {
pub id: i32,
pub body: &'a [u8],
}
where the 'a
lifetime is the duration of the entire tick (the BytesMut
are deallocated at the end of the tick).
Event Generation
For each entity's packet, we have a switch statement over what we should do for each packet here.
Note: all packet-switch logic is done in parallel (based on the number of threads) and which entity is partitioned on that thread.
pub fn packet_switch(
raw: BorrowedPacketFrame<'_>,
query: &mut PacketSwitchQuery<'_>,
) -> anyhow::Result<()> {
let packet_id = raw.id;
let data = raw.body;
// ideally we wouldn't have to do this. The lifetime is the same as the entire tick.
// as the data is bump-allocated and reset occurs at the end of the tick
let data: &'static [u8] = unsafe { core::mem::transmute(data) };
match packet_id {
play::ChatMessageC2s::ID => chat_message(data, query)?,
play::ClickSlotC2s::ID => click_slot(data, query)?,
play::ClientCommandC2s::ID => client_command(data, query)?,
play::CommandExecutionC2s::ID => chat_command(data, query)?,
play::CreativeInventoryActionC2s::ID => creative_inventory_action(data, query)?,
play::CustomPayloadC2s::ID => custom_payload(data, query)?,
play::FullC2s::ID => full(query, data)?,
play::HandSwingC2s::ID => hand_swing(data, query)?,
play::LookAndOnGroundC2s::ID => look_and_on_ground(data, query)?,
play::PlayerActionC2s::ID => player_action(data, query)?,
play::PlayerInteractBlockC2s::ID => player_interact_block(data, query)?,
play::PlayerInteractEntityC2s::ID => player_interact_entity(data, query)?,
play::PlayerInteractItemC2s::ID => player_interact_item(data, query)?,
play::PositionAndOnGroundC2s::ID => position_and_on_ground(query, data)?,
play::RequestCommandCompletionsC2s::ID => request_command_completions(data, query)?,
play::UpdateSelectedSlotC2s::ID => update_selected_slot(data, query)?,
_ => trace!("unknown packet id: 0x{:02X}", packet_id),
}
Ok(())
}