mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
Add Cookies (#325)
* COKKIES PROTOCOL * Update pumpkin-protocol/src/client/config/c_store_cookie.rs Co-authored-by: DataModel <183248792+DataM0del@users.noreply.github.com> * Update pumpkin-protocol/src/server/config/s_cookie_response.rs Co-authored-by: DataModel <183248792+DataM0del@users.noreply.github.com> * Update pumpkin-protocol/src/server/play/s_cookie_response.rs Co-authored-by: DataModel <183248792+DataM0del@users.noreply.github.com> * Add read fn * Finished * Add to readme * remove async * use payload.len() * fix payload_length * fix all --------- Co-authored-by: DataModel <183248792+DataM0del@users.noreply.github.com>
This commit is contained in:
@@ -42,7 +42,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi
|
||||
- [x] Server Brand
|
||||
- [ ] Server Links
|
||||
- [x] Set Resource Pack
|
||||
- [ ] Cookies
|
||||
- [x] Cookies
|
||||
- World
|
||||
- [x] World Joining
|
||||
- [x] Player Tab-list
|
||||
|
||||
@@ -4,6 +4,13 @@ use crate::Identifier;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[client_packet("config:cookie_request")]
|
||||
pub struct CCookieRequest {
|
||||
key: Identifier,
|
||||
/// Requests a cookie that was previously stored.
|
||||
pub struct CCookieRequest<'a> {
|
||||
key: &'a Identifier,
|
||||
}
|
||||
|
||||
impl<'a> CCookieRequest<'a> {
|
||||
pub fn new(key: &'a Identifier) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
}
|
||||
|
||||
22
pumpkin-protocol/src/client/config/c_store_cookie.rs
Normal file
22
pumpkin-protocol/src/client/config/c_store_cookie.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use crate::{Identifier, VarInt};
|
||||
use pumpkin_macros::client_packet;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[client_packet("config:store_cookie")]
|
||||
/// Stores some arbitrary data on the client, which persists between server transfers.
|
||||
/// The Notchian (vanilla) client only accepts cookies of up to 5 kiB in size.
|
||||
pub struct CStoreCookie<'a> {
|
||||
key: &'a Identifier,
|
||||
payload_length: VarInt,
|
||||
payload: &'a [u8], // 5120,
|
||||
}
|
||||
|
||||
impl<'a> CStoreCookie<'a> {
|
||||
pub fn new(key: &'a Identifier, payload: &'a [u8]) -> Self {
|
||||
Self {
|
||||
key,
|
||||
payload_length: VarInt(payload.len() as i32),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
16
pumpkin-protocol/src/client/config/c_transfer.rs
Normal file
16
pumpkin-protocol/src/client/config/c_transfer.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::VarInt;
|
||||
use pumpkin_macros::client_packet;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[client_packet("config:transfer")]
|
||||
pub struct CTransfer<'a> {
|
||||
host: &'a str,
|
||||
port: &'a VarInt,
|
||||
}
|
||||
|
||||
impl<'a> CTransfer<'a> {
|
||||
pub fn new(host: &'a str, port: &'a VarInt) -> Self {
|
||||
Self { host, port }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ mod c_finish_config;
|
||||
mod c_known_packs;
|
||||
mod c_plugin_message;
|
||||
mod c_registry_data;
|
||||
mod c_store_cookie;
|
||||
mod c_transfer;
|
||||
|
||||
pub use c_add_resource_pack::*;
|
||||
pub use c_config_disconnect::*;
|
||||
@@ -13,3 +15,5 @@ pub use c_finish_config::*;
|
||||
pub use c_known_packs::*;
|
||||
pub use c_plugin_message::*;
|
||||
pub use c_registry_data::*;
|
||||
pub use c_store_cookie::*;
|
||||
pub use c_transfer::*;
|
||||
|
||||
16
pumpkin-protocol/src/client/login/c_cookie_request.rs
Normal file
16
pumpkin-protocol/src/client/login/c_cookie_request.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::Identifier;
|
||||
use pumpkin_macros::client_packet;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[client_packet("login:cookie_request")]
|
||||
/// Requests a cookie that was previously stored.
|
||||
pub struct CCookieRequest<'a> {
|
||||
key: &'a Identifier,
|
||||
}
|
||||
|
||||
impl<'a> CCookieRequest<'a> {
|
||||
pub fn new(key: &'a Identifier) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
mod c_cookie_request;
|
||||
mod c_encryption_request;
|
||||
mod c_login_disconnect;
|
||||
mod c_login_success;
|
||||
mod c_plugin_request;
|
||||
mod c_set_compression;
|
||||
|
||||
pub use c_cookie_request::*;
|
||||
pub use c_encryption_request::*;
|
||||
pub use c_login_disconnect::*;
|
||||
pub use c_login_success::*;
|
||||
|
||||
16
pumpkin-protocol/src/client/play/c_cookie_request.rs
Normal file
16
pumpkin-protocol/src/client/play/c_cookie_request.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::Identifier;
|
||||
use pumpkin_macros::client_packet;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[client_packet("play:cookie_request")]
|
||||
/// Requests a cookie that was previously stored.
|
||||
pub struct CCookieRequest<'a> {
|
||||
key: &'a Identifier,
|
||||
}
|
||||
|
||||
impl<'a> CCookieRequest<'a> {
|
||||
pub fn new(key: &'a Identifier) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
}
|
||||
23
pumpkin-protocol/src/client/play/c_store_cookie.rs
Normal file
23
pumpkin-protocol/src/client/play/c_store_cookie.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use crate::{Identifier, VarInt};
|
||||
use pumpkin_macros::client_packet;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Stores some arbitrary data on the client, which persists between server transfers.
|
||||
/// The Notchian client only accepts cookies of up to 5 kiB in size.
|
||||
#[derive(Serialize)]
|
||||
#[client_packet("play:store_cookie")]
|
||||
pub struct CStoreCookie<'a> {
|
||||
key: &'a Identifier,
|
||||
payload_length: VarInt,
|
||||
payload: &'a [u8], // 5120,
|
||||
}
|
||||
|
||||
impl<'a> CStoreCookie<'a> {
|
||||
pub fn new(key: &'a Identifier, payload: &'a [u8]) -> Self {
|
||||
Self {
|
||||
key,
|
||||
payload_length: VarInt(payload.len() as i32),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod c_close_container;
|
||||
mod c_combat_death;
|
||||
mod c_command_suggestions;
|
||||
mod c_commands;
|
||||
mod c_cookie_request;
|
||||
mod c_damage_event;
|
||||
mod c_disguised_chat_message;
|
||||
mod c_display_objective;
|
||||
@@ -47,6 +48,7 @@ mod c_set_held_item;
|
||||
mod c_set_title;
|
||||
mod c_sound_effect;
|
||||
mod c_spawn_entity;
|
||||
mod c_store_cookie;
|
||||
mod c_subtitle;
|
||||
mod c_sync_player_position;
|
||||
mod c_system_chat_message;
|
||||
@@ -72,6 +74,7 @@ pub use c_close_container::*;
|
||||
pub use c_combat_death::*;
|
||||
pub use c_command_suggestions::*;
|
||||
pub use c_commands::*;
|
||||
pub use c_cookie_request::*;
|
||||
pub use c_damage_event::*;
|
||||
pub use c_disguised_chat_message::*;
|
||||
pub use c_display_objective::*;
|
||||
@@ -110,6 +113,7 @@ pub use c_set_held_item::*;
|
||||
pub use c_set_title::*;
|
||||
pub use c_sound_effect::*;
|
||||
pub use c_spawn_entity::*;
|
||||
pub use c_store_cookie::*;
|
||||
pub use c_subtitle::*;
|
||||
pub use c_sync_player_position::*;
|
||||
pub use c_system_chat_message::*;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod s_acknowledge_finish_config;
|
||||
mod s_client_information;
|
||||
mod s_cookie_response;
|
||||
mod s_known_packs;
|
||||
mod s_plugin_message;
|
||||
|
||||
pub use s_acknowledge_finish_config::*;
|
||||
pub use s_client_information::*;
|
||||
pub use s_cookie_response::*;
|
||||
pub use s_known_packs::*;
|
||||
pub use s_plugin_message::*;
|
||||
|
||||
51
pumpkin-protocol/src/server/config/s_cookie_response.rs
Normal file
51
pumpkin-protocol/src/server/config/s_cookie_response.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use pumpkin_macros::server_packet;
|
||||
use serde::de;
|
||||
|
||||
use crate::bytebuf::{ByteBuffer, DeserializerError};
|
||||
use crate::{Identifier, ServerPacket, VarInt};
|
||||
|
||||
#[server_packet("config:cookie_response")]
|
||||
/// Response to a Cookie Request (configuration) from the server.
|
||||
/// The Notchian (vanilla) server only accepts responses of up to 5 kiB in size.
|
||||
pub struct SCookieResponse {
|
||||
pub key: Identifier,
|
||||
pub has_payload: bool,
|
||||
pub payload_length: Option<VarInt>,
|
||||
pub payload: Option<Vec<u8>>, // 5120,
|
||||
}
|
||||
|
||||
const MAX_PAYLOAD_SIZE: i32 = 5120;
|
||||
|
||||
impl ServerPacket for SCookieResponse {
|
||||
fn read(bytebuf: &mut ByteBuffer) -> Result<Self, DeserializerError> {
|
||||
let key = bytebuf.get_string()?;
|
||||
let has_payload = bytebuf.get_bool()?;
|
||||
|
||||
if !has_payload {
|
||||
return Ok(Self {
|
||||
key,
|
||||
has_payload,
|
||||
payload_length: None,
|
||||
payload: None,
|
||||
});
|
||||
}
|
||||
|
||||
let payload_length = bytebuf.get_var_int()?;
|
||||
let length = payload_length.0;
|
||||
|
||||
if length > MAX_PAYLOAD_SIZE {
|
||||
return Err(de::Error::custom(
|
||||
"Payload exceeds the maximum allowed size (5120 bytes)",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = bytebuf.copy_to_bytes(length as usize)?.to_vec();
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
has_payload,
|
||||
payload_length: Some(payload_length),
|
||||
payload: Some(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
mod s_cookie_response;
|
||||
mod s_encryption_response;
|
||||
mod s_login_response;
|
||||
mod s_login_start;
|
||||
mod s_plugin_response;
|
||||
|
||||
pub use s_cookie_response::*;
|
||||
pub use s_encryption_response::*;
|
||||
pub use s_login_response::*;
|
||||
pub use s_login_start::*;
|
||||
|
||||
50
pumpkin-protocol/src/server/login/s_cookie_response.rs
Normal file
50
pumpkin-protocol/src/server/login/s_cookie_response.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::bytebuf::{ByteBuffer, DeserializerError};
|
||||
use crate::{Identifier, ServerPacket, VarInt};
|
||||
use pumpkin_macros::server_packet;
|
||||
use serde::de;
|
||||
|
||||
#[server_packet("login:cookie_response")]
|
||||
/// Response to a Cookie Request (login) from the server.
|
||||
/// The Notchian server only accepts responses of up to 5 kiB in size.
|
||||
pub struct SCookieResponse {
|
||||
pub key: Identifier,
|
||||
pub has_payload: bool,
|
||||
pub payload_length: Option<VarInt>,
|
||||
pub payload: Option<Vec<u8>>, // 5120,
|
||||
}
|
||||
|
||||
const MAX_PAYLOAD_SIZE: i32 = 5120;
|
||||
|
||||
impl ServerPacket for SCookieResponse {
|
||||
fn read(bytebuf: &mut ByteBuffer) -> Result<Self, DeserializerError> {
|
||||
let key = bytebuf.get_string()?;
|
||||
let has_payload = bytebuf.get_bool()?;
|
||||
|
||||
if !has_payload {
|
||||
return Ok(Self {
|
||||
key,
|
||||
has_payload,
|
||||
payload_length: None,
|
||||
payload: None,
|
||||
});
|
||||
}
|
||||
|
||||
let payload_length = bytebuf.get_var_int()?;
|
||||
let length = payload_length.0;
|
||||
|
||||
if length > MAX_PAYLOAD_SIZE {
|
||||
return Err(de::Error::custom(
|
||||
"Payload exceeds the maximum allowed size (5120 bytes)",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = bytebuf.copy_to_bytes(length as usize)?.to_vec();
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
has_payload,
|
||||
payload_length: Some(payload_length),
|
||||
payload: Some(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod s_client_tick_end;
|
||||
mod s_close_container;
|
||||
mod s_command_suggestion;
|
||||
mod s_confirm_teleport;
|
||||
mod s_cookie_response;
|
||||
mod s_interact;
|
||||
mod s_keep_alive;
|
||||
mod s_ping_request;
|
||||
@@ -33,6 +34,7 @@ pub use s_client_tick_end::*;
|
||||
pub use s_close_container::*;
|
||||
pub use s_command_suggestion::*;
|
||||
pub use s_confirm_teleport::*;
|
||||
pub use s_cookie_response::*;
|
||||
pub use s_interact::*;
|
||||
pub use s_keep_alive::*;
|
||||
pub use s_ping_request::*;
|
||||
|
||||
50
pumpkin-protocol/src/server/play/s_cookie_response.rs
Normal file
50
pumpkin-protocol/src/server/play/s_cookie_response.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::bytebuf::{ByteBuffer, DeserializerError};
|
||||
use crate::{Identifier, ServerPacket, VarInt};
|
||||
use pumpkin_macros::server_packet;
|
||||
use serde::de;
|
||||
|
||||
#[server_packet("play:cookie_response")]
|
||||
/// Response to a Cookie Request (play) from the server.
|
||||
/// The Notchian (vanilla) server only accepts responses of up to 5 kiB in size.
|
||||
pub struct SCookieResponse {
|
||||
pub key: Identifier,
|
||||
pub has_payload: bool,
|
||||
pub payload_length: Option<VarInt>,
|
||||
pub payload: Option<Vec<u8>>, // 5120,
|
||||
}
|
||||
|
||||
const MAX_PAYLOAD_SIZE: i32 = 5120;
|
||||
|
||||
impl ServerPacket for SCookieResponse {
|
||||
fn read(bytebuf: &mut ByteBuffer) -> Result<Self, DeserializerError> {
|
||||
let key = bytebuf.get_string()?;
|
||||
let has_payload = bytebuf.get_bool()?;
|
||||
|
||||
if !has_payload {
|
||||
return Ok(Self {
|
||||
key,
|
||||
has_payload,
|
||||
payload_length: None,
|
||||
payload: None,
|
||||
});
|
||||
}
|
||||
|
||||
let payload_length = bytebuf.get_var_int()?;
|
||||
let length = payload_length.0;
|
||||
|
||||
if length > MAX_PAYLOAD_SIZE {
|
||||
return Err(de::Error::custom(
|
||||
"Payload exceeds the maximum allowed size (5120 bytes)",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = bytebuf.copy_to_bytes(length as usize)?.to_vec();
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
has_payload,
|
||||
payload_length: Some(payload_length),
|
||||
payload: Some(payload),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
use crate::{
|
||||
client::authentication::{self, offline_uuid, validate_textures, GameProfile},
|
||||
entity::player::{ChatMode, Hand},
|
||||
proxy::{
|
||||
bungeecord,
|
||||
velocity::{self, velocity_login},
|
||||
},
|
||||
server::{Server, CURRENT_MC_VERSION},
|
||||
};
|
||||
use num_traits::FromPrimitive;
|
||||
use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG};
|
||||
use pumpkin_core::text::TextComponent;
|
||||
use pumpkin_protocol::server::config::SCookieResponse as SCCookieResponse;
|
||||
use pumpkin_protocol::server::login::SCookieResponse as SLCookieResponse;
|
||||
use pumpkin_protocol::{
|
||||
client::{
|
||||
config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData},
|
||||
@@ -13,20 +24,10 @@ use pumpkin_protocol::{
|
||||
login::{SEncryptionResponse, SLoginPluginResponse, SLoginStart},
|
||||
status::SStatusPingRequest,
|
||||
},
|
||||
ConnectionState, KnownPack, CURRENT_MC_PROTOCOL,
|
||||
ConnectionState, KnownPack, VarInt, CURRENT_MC_PROTOCOL,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
client::authentication::{self, offline_uuid, validate_textures, GameProfile},
|
||||
entity::player::{ChatMode, Hand},
|
||||
proxy::{
|
||||
bungeecord,
|
||||
velocity::{self, velocity_login},
|
||||
},
|
||||
server::{Server, CURRENT_MC_VERSION},
|
||||
};
|
||||
|
||||
use super::{authentication::AuthError, Client, PlayerConfig};
|
||||
|
||||
/// Processes incoming Packets from the Client to the Server
|
||||
@@ -251,6 +252,16 @@ impl Client {
|
||||
Err(AuthError::MissingAuthClient)
|
||||
}
|
||||
|
||||
pub fn handle_login_cookie_response(&self, packet: SLCookieResponse) {
|
||||
// TODO: allow plugins to access this
|
||||
log::debug!(
|
||||
"Received cookie_response[login]: key: \"{}\", has_payload: \"{}\", payload_length: \"{}\"",
|
||||
packet.key,
|
||||
packet.has_payload,
|
||||
packet.payload_length.unwrap_or(VarInt::from(0)).0
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn handle_plugin_response(&self, plugin_response: SLoginPluginResponse) {
|
||||
log::debug!("Handling plugin");
|
||||
let velocity_config = &ADVANCED_CONFIG.proxy.velocity;
|
||||
@@ -342,6 +353,16 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_config_cookie_response(&self, packet: SCCookieResponse) {
|
||||
// TODO: allow plugins to access this
|
||||
log::debug!(
|
||||
"Received cookie_response[config]: key: \"{}\", has_payload: \"{}\", payload_length: \"{}\"",
|
||||
packet.key,
|
||||
packet.has_payload,
|
||||
packet.payload_length.unwrap_or(VarInt::from(0)).0
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn handle_known_packs(&self, server: &Server, _config_acknowledged: SKnownPacks) {
|
||||
log::debug!("Handling known packs");
|
||||
for registry in &server.cached_registry {
|
||||
|
||||
@@ -32,8 +32,9 @@ use pumpkin_protocol::{
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use pumpkin_protocol::server::config::SCookieResponse as SCCookieResponse;
|
||||
use pumpkin_protocol::server::login::SCookieResponse as SLCookieResponse;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod authentication;
|
||||
mod client_packet;
|
||||
pub mod combat;
|
||||
@@ -433,6 +434,9 @@ impl Client {
|
||||
SLoginAcknowledged::PACKET_ID => {
|
||||
self.handle_login_acknowledged(server).await;
|
||||
}
|
||||
SLCookieResponse::PACKET_ID => {
|
||||
self.handle_login_cookie_response(SLCookieResponse::read(bytebuf)?);
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to handle client packet id {} in Login State",
|
||||
@@ -467,6 +471,9 @@ impl Client {
|
||||
self.handle_known_packs(server, SKnownPacks::read(bytebuf)?)
|
||||
.await;
|
||||
}
|
||||
SCCookieResponse::PACKET_ID => {
|
||||
self.handle_config_cookie_response(SCCookieResponse::read(bytebuf)?);
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to handle client packet id {} in Config State",
|
||||
|
||||
@@ -16,9 +16,11 @@ use pumpkin_core::{
|
||||
GameMode,
|
||||
};
|
||||
use pumpkin_inventory::{InventoryError, WindowType};
|
||||
use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse;
|
||||
use pumpkin_protocol::{
|
||||
client::play::CCommandSuggestions,
|
||||
server::play::{SCloseContainer, SCommandSuggestion, SKeepAlive, SSetPlayerGround, SUseItem},
|
||||
VarInt,
|
||||
};
|
||||
use pumpkin_protocol::{
|
||||
client::play::{
|
||||
@@ -751,4 +753,14 @@ impl Player {
|
||||
|
||||
self.client.send_packet(&response).await;
|
||||
}
|
||||
|
||||
pub fn handle_cookie_response(&self, packet: SPCookieResponse) {
|
||||
// TODO: allow plugins to access this
|
||||
log::debug!(
|
||||
"Received cookie_response[play]: key: \"{}\", has_payload: \"{}\", payload_length: \"{}\"",
|
||||
packet.key,
|
||||
packet.has_payload,
|
||||
packet.payload_length.unwrap_or(VarInt::from(0)).0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use pumpkin_core::{
|
||||
use pumpkin_entity::{entity_type::EntityType, EntityId};
|
||||
use pumpkin_inventory::player::PlayerInventory;
|
||||
use pumpkin_macros::sound;
|
||||
use pumpkin_protocol::server::play::SCookieResponse as SPCookieResponse;
|
||||
use pumpkin_protocol::server::play::{SClickContainer, SKeepAlive};
|
||||
use pumpkin_protocol::{
|
||||
bytebuf::packet_id::Packet,
|
||||
@@ -793,6 +794,9 @@ impl Player {
|
||||
self.handle_command_suggestion(SCommandSuggestion::read(bytebuf)?, server)
|
||||
.await;
|
||||
}
|
||||
SPCookieResponse::PACKET_ID => {
|
||||
self.handle_cookie_response(SPCookieResponse::read(bytebuf)?);
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Failed to handle player packet id {}", packet.id.0);
|
||||
// TODO: We give an error if all play packets are implemented
|
||||
|
||||
Reference in New Issue
Block a user