use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream}; use std::time::Duration; use anyhow::Result; use craftio_rs::{CraftIo, CraftSyncReader, CraftSyncWriter, CraftTcpConnection}; use mcproto_rs::nbt::Tag; use mcproto_rs::protocol::{HasPacketKind, State}; use mcproto_rs::Serializer; use mcproto_rs::status::{StatusPlayersSpec, StatusSpec, StatusVersionSpec}; use mcproto_rs::types::{BytesSerializer, Chat, IntPosition, ItemStack, NamedNbtTag, RemainingBytes, Vec3}; use mcproto_rs::uuid::UUID4; use mcproto_rs::v1_19_3::{BitSet, BlobArray, BookSettings, ChunkDataAndUpdateLightSpec, ChunkDataSpec, ChunkSection, CommandNode, CommandNodeSpec, CommandsSpec, EntityEventSpec, InitializeWorldBorderSpec, KeepAliveClientBoundSpec, LightDataSpec, PalettedContainer, PluginMessageSpec, RecipeBookAction, RecipeBookInitSpec, SetCenterChunkSpec, SetContainerContentSpec, SetDefaultSpawnPositionSpec, SynchronizePlayerPositionSpec, TagType, TypedTagList, UpdateRecipeBookSpec}; use mcproto_rs::v1_19_3::{GameMode, HandshakeNextState, LoginPlaySpec, LoginSuccessSpec, Packet761, Packet761Kind, PingRequestSpec, PingResponseSpec, PreviousGameMode, RawPacket761, SetHeldItemClientSpec, StatusResponseSpec, UpdateRecipesSpec, UpdateTagsSpec}; use tokio; pub struct MinecraftClient { connection: CraftTcpConnection, } impl MinecraftClient { pub fn new(connection: CraftTcpConnection) -> Self { Self { connection } } pub fn from_stream(stream: TcpStream) -> Result { Ok(Self { connection: CraftTcpConnection::from_std( stream, mcproto_rs::protocol::PacketDirection::ServerBound, )?, }) } pub async fn read_next_packet(&mut self) -> Result> { if let Some(raw) = self.connection.read_packet::()? { println!("Client -> Server: {:?}", raw); Ok(Some(raw)) } else { Ok(None) } } pub async fn send_packet(&mut self, packet: Packet761) -> Result<()> { println!("Server -> Client: {:?}", packet); self.connection.write_packet(packet)?; Ok(()) } pub async fn wait_for_packet(&mut self, packet_kind: Packet761Kind) -> Result { loop { if let Some(packet) = self.read_next_packet().await? { if packet.kind() == packet_kind { return Ok(packet); } println!("Skipping packet {:?}", packet); } } } } macro_rules! await_packet { ($packet_type:ident, $conn:expr) => { assert_packet!( $packet_type, $conn.wait_for_packet(Packet761Kind::$packet_type).await? ) }; } macro_rules! assert_packet { ($packet_type:ident, $obj:expr) => { if let Packet761::$packet_type(packet_data) = $obj { packet_data } else { panic!("Expected packet of type {}", stringify!($packet_type)) } }; ($packet_type:ident, $conn:expr, $errmgs:literal) => { assert_packet!( $packet_type, $conn .read_next_packet() .await? .ok_or(anyhow::anyhow!("Missing packet"))? ) }; } trait ShittyToNbt { fn to_nbt(self) -> Tag; } impl ShittyToNbt for String { fn to_nbt(self) -> Tag { Tag::String(self) } } impl ShittyToNbt for &str { fn to_nbt(self) -> Tag { Tag::String(self.into()) } } impl ShittyToNbt for i32 { fn to_nbt(self) -> Tag { Tag::Int(self) } } impl ShittyToNbt for i64 { fn to_nbt(self) -> Tag { Tag::Long(self) } } impl ShittyToNbt for bool { fn to_nbt(self) -> Tag { Tag::Byte(if self { 1 } else { 0 }) } } impl ShittyToNbt for Tag { fn to_nbt(self) -> Tag { self } } impl ShittyToNbt for f32 { fn to_nbt(self) -> Tag { Tag::Float(self) } } impl ShittyToNbt for f64 { fn to_nbt(self) -> Tag { Tag::Double(self) } } macro_rules! nbt { ( {$($name:literal :: $value:tt),* $(,)? } ) => { Tag::Compound(vec![ $( nbt!($value).with_name($name) ),* ]) }; { $($name:literal :: $value:tt),* $(,)? } => { Tag::Compound(vec![ $( nbt!($value).with_name($name) ),* ]) }; ([ $($value:tt),* $(,)? ]) => { Tag::List(vec![ $( nbt!($value) ),* ]) }; (($e:expr)) => { ShittyToNbt::to_nbt($e) }; } #[tokio::main] async fn main() -> Result<()> { println!("Starting server"); let bind = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 25565))?; loop { if let Ok((socket, address)) = bind.accept() { println!("Connection accepted from {}", address); let client = MinecraftClient::from_stream(socket)?; tokio::spawn(async { if let Err(x) = handle_conn(client).await { println!("Error: {:?}", x); } }); } } } async fn handle_conn(mut client: MinecraftClient) -> Result<()> { let hs = assert_packet!(Handshake, client, "Missing packet"); if hs.next_state == HandshakeNextState::Login { client.connection.set_state(State::Login); let loginstart = assert_packet!( LoginStart, client .read_next_packet() .await? .ok_or(anyhow::anyhow!("Missing packet"))? ); println!("Login: {:?}", loginstart); let uuid = UUID4::random(); client .send_packet(Packet761::LoginSuccess(LoginSuccessSpec { uuid, username: loginstart.name, properties: vec![].into(), })) .await?; let min_build_height = -64i32; let max_build_height = 256i32; let world_height = max_build_height - min_build_height; let min_world_section = min_build_height >> 4; let max_world_section = ((max_build_height - 1) >> 4) + 1; let section_count = max_world_section - min_world_section; let overworld_spec = nbt! { "id" :: (0), "name" :: ("minecraft:overworld"), "element" :: { "ambient_light" :: (false), "bed_works" :: (true), "coordinate_scale" :: (1), "effects" :: ("minecraft:overworld"), "has_ceiling" :: (false), "has_skylight" :: (true), "has_raids" :: (false), "height" :: (world_height), "infiniburn" :: ("#minecraft:infiniburn_overworld"), "logical_height" :: (world_height), "min_y" :: (min_build_height), "monster_spawn_block_light_limit" :: (0), "monster_spawn_light_level" :: { "type" :: ("minecraft:uniform"), "value" :: { "max_inclusive" :: (7), "min_inclusive" :: (0), }, }, "natural" :: (true), "piglin_safe" :: (false), "respawn_anchor_works" :: (false), "ultrawarm" :: (false), }, }; let biome_spec = nbt!( [ { "id" :: (0), "name" :: ("minecraft:plains"), "element" :: { "downfall" :: (0.5), "precipitation" :: ("none"), "temperature" :: (0.5), "effects" :: { "fog_color" :: (12638463), "sky_color" :: (12638463), "water_color" :: (12638463), "water_fog_color" :: (12638463), "mood_sound" :: { "block_search_extent" :: (8), "offset" :: (2), "sound" :: ("minecraft:ambient.cave"), "tick_delay" :: (6000), }, } } } ] ); let registry_doex = nbt! { "minecraft:dimension_type" :: { "type" :: ("minecraft:dimension_type"), "value" :: [(overworld_spec)] }, "minecraft:worldgen/biome" :: { "type" :: ("minecraft:worldgen/biome"), "value" :: (biome_spec), } }; client.connection.set_state(State::Play); client .send_packet(Packet761::LoginPlay(LoginPlaySpec { entity_id: 0, is_hardcore: false, gamemode: GameMode::Survival, previous_gamemode: PreviousGameMode::NoPrevious, dimension_names: vec!["world".into()].into(), registry_codex: NamedNbtTag { root: registry_doex.with_name("root"), }, dimension_type: "minecraft:overworld".into(), dimension_name: "world".into(), hashed_seed: 0, max_players: 10.into(), view_distance: 10.into(), simulation_distance: 10.into(), reduced_debug_info: false, enable_respawn_screen: false, is_debug: false, is_flat: false, death_location: None, })) .await?; let client_information = await_packet!(ClientInformation, client); println!("Client Information: {:?}", client_information); client.send_packet(Packet761::SetHeldItemClient(SetHeldItemClientSpec { selected_slot: 0, })).await?; client.send_packet(Packet761::UpdateRecipes(UpdateRecipesSpec { recipes: vec![].into() })).await?; client.send_packet(Packet761::UpdateTags(UpdateTagsSpec { tags: vec![ TypedTagList { tag_type: TagType::Block, tags: vec![].into(), }, TypedTagList { tag_type: TagType::Item, tags: vec![].into(), }, TypedTagList { tag_type: TagType::EntityType, tags: vec![].into(), }, TypedTagList { tag_type: TagType::GameEvent, tags: vec![].into(), }, TypedTagList { tag_type: TagType::Fluid, tags: vec![].into(), }, ].into(), })).await?; client.send_packet(Packet761::EntityEvent(EntityEventSpec { entity_id: 0, entity_status: 24 /* OP permission level = 0 */, })).await?; client.send_packet(Packet761::Commands(CommandsSpec { nodes: vec![ CommandNodeSpec { children_indices: vec![].into(), redirect_node: None, is_executable: false, node: CommandNode::Root, } ].into(), root_index: 0.into(), })).await?; client.send_packet(Packet761::UpdateRecipeBook(UpdateRecipeBookSpec { action: RecipeBookAction::Init(RecipeBookInitSpec { book_settings: BookSettings::default(), known_recipes: vec![].into(), highlighted_recipes: vec![].into(), }) })).await?; client.send_packet(Packet761::SynchronizePlayerPosition(SynchronizePlayerPositionSpec { position: Vec3 { x: 0f64, y: 100f64, z: 0f64, }, yaw: 0.0, pitch: 0.0, flags: Default::default(), teleport_id: 0.into(), dismount_vehicle: false, })).await?; client.send_packet(Packet761::SetCenterChunk(SetCenterChunkSpec { chunk_x: 0.into(), chunk_z: 0.into(), })).await?; for i in -7..7 { for j in -7..7 { let mut heightmap = BlobArray::new(9); for cx in 0..(16 * 16) { heightmap.set_entry(cx, 2) } let mut chunk_dat_serializer = BytesSerializer::default(); for i in 0..section_count { let section = ChunkSection { block_count: if i < 4 { 16 * 16 * 16 } else { 0 }, block_state: PalettedContainer { singular_value: if i < 4 { 1.into() } else { 0.into() } }, biomes: PalettedContainer { singular_value: 0.into() }, }; chunk_dat_serializer.serialize_other(§ion)?; } let chunk_column_bytes = chunk_dat_serializer.into_bytes(); client.send_packet(Packet761::ChunkDataAndUpdateLight(ChunkDataAndUpdateLightSpec { chunk_x: i, chunk_z: j, chunk_data: ChunkDataSpec { heightmaps: NamedNbtTag { root: nbt! { "MOTION_BLOCKING" :: (Tag::LongArray(heightmap.data.iter().map(|n| n.to_owned() as _).collect())), "WORLD_SURFACE" :: (Tag::LongArray(heightmap.data.iter().map(|n| n.to_owned() as _).collect())), }.with_name("root") }, buffer: chunk_column_bytes.into(), block_entities: vec![].into(), }, light_data: LightDataSpec { trust_edges: true, sky_y_mask: BitSet::empty(), block_y_mask: BitSet::empty(), empty_sky_y_mask: BitSet::empty(), empty_block_y_mask: BitSet::empty(), sky_updates: vec![].into(), block_updates: vec![].into(), }, })).await?; } } client.send_packet(Packet761::InitializeWorldBorder(InitializeWorldBorderSpec { x: 0.0, z: 0.0, old_diameter: 10000000.0, new_diameter: 10000000.0, speed: 0.into(), portal_teleport_boundary: 29999984.into(), warning_blocks: 0.into(), warning_time: 0.into(), })).await?; client.send_packet(Packet761::SetDefaultSpawnPosition(SetDefaultSpawnPositionSpec { location: IntPosition { x: 0, y: 0, z: 0, }, angle: 0.0, })).await?; client.send_packet(Packet761::SynchronizePlayerPosition(SynchronizePlayerPositionSpec { position: Vec3 { x: 0f64, y: 100f64, z: 0f64, }, yaw: 0.0, pitch: 0.0, flags: Default::default(), teleport_id: 0.into(), dismount_vehicle: false, })).await?; client.send_packet(Packet761::SetContainerContent(SetContainerContentSpec { window_id: 0, state_id: 0.into(), slot_data: vec![Some(ItemStack { item_id: 1.into(), item_count: 1, nbt: None, }); 45].into(), carried_item: None, })).await?; let mut i = 0; loop { client.send_packet(Packet761::KeepAliveClientBound(KeepAliveClientBoundSpec { keep_alive_id: i, })).await?; if i == 2 { let mut brand_encoder = BytesSerializer::default(); brand_encoder.serialize_other(&String::from("MGASI"))?; client.send_packet(Packet761::PluginMessage(PluginMessageSpec { channel: "minecraft:brand".to_string(), data: RemainingBytes { data: brand_encoder.into_bytes() }, })).await?; } i += 1; tokio::time::sleep(Duration::from_secs(10)).await; } } else if hs.next_state == HandshakeNextState::Status { return handle_status(client).await; } else { anyhow::bail!("Unknown next status") } } async fn handle_status(mut client: MinecraftClient) -> Result<()> { client.connection.set_state(State::Status); loop { match client .read_next_packet() .await? .ok_or(anyhow::anyhow!("Missing packet"))? { Packet761::StatusRequest(_) => { client .send_packet(Packet761::StatusRespone(StatusResponseSpec { response: StatusSpec { version: Some(StatusVersionSpec { name: "1.19.3 mgasi".to_string(), protocol: 761, }), players: StatusPlayersSpec { max: 100, online: 0, sample: vec![], }, description: Chat::from_text("hehehe"), favicon: None, }, })) .await?; } Packet761::PingRequest(PingRequestSpec { payload }) => { client .send_packet(Packet761::PingResponse(PingResponseSpec { payload })) .await?; return Ok(()); } _ => anyhow::bail!("Unexpected packet during status communication"), } } }