From 35a06936a99e3526dc6c92668de37541f0078337 Mon Sep 17 00:00:00 2001 From: Andrew Gazelka Date: Thu, 7 Mar 2024 11:51:32 -0600 Subject: [PATCH] feat: player joins server (#8) --- Cargo.lock | 49 ++-- Cargo.toml | 3 +- DETAILS.md | 159 +++++++++++++ prototype/.github/workflows/build.yml | 117 ++++++++++ prototype/.gitignore | 1 + prototype/Cargo.toml | 76 ++++++ prototype/README.md | 0 prototype/clippy.toml | 3 + prototype/rustfmt.toml | 21 ++ prototype/src/global.rs | 30 +++ prototype/src/lib.rs | 160 +++++++++++++ prototype/src/messages.rs | 47 ++++ prototype/src/utils.rs | 80 +++++++ server/Cargo.toml | 5 + server/src/chunk.rs | 25 ++ server/src/main.rs | 321 +++++++++++++++----------- 16 files changed, 939 insertions(+), 158 deletions(-) create mode 100644 DETAILS.md create mode 100644 prototype/.github/workflows/build.yml create mode 100644 prototype/.gitignore create mode 100644 prototype/Cargo.toml create mode 100644 prototype/README.md create mode 100644 prototype/clippy.toml create mode 100644 prototype/rustfmt.toml create mode 100644 prototype/src/global.rs create mode 100644 prototype/src/lib.rs create mode 100644 prototype/src/messages.rs create mode 100644 prototype/src/utils.rs create mode 100644 server/src/chunk.rs diff --git a/Cargo.lock b/Cargo.lock index ca9e8095..4ee68e96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -723,6 +723,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "enum-as-inner" version = "0.6.0" @@ -977,6 +983,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -985,9 +1000,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1325,7 +1340,9 @@ dependencies = [ "azalea-buf", "azalea-world", "bytes", + "itertools", "mimalloc", + "rand", "serde_json", "sha2", "tokio", @@ -1802,9 +1819,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1812,9 +1829,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -1827,9 +1844,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -1839,9 +1856,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1849,9 +1866,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -1862,15 +1879,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 052b16db..c8b5257e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,8 @@ resolver = "2" members = [ -# "ser", -# "ser-macro", "server", +# "prototype" ] [workspace.dependencies] diff --git a/DETAILS.md b/DETAILS.md new file mode 100644 index 00000000..0e011477 --- /dev/null +++ b/DETAILS.md @@ -0,0 +1,159 @@ +Given a machine with `64` cores: + +The world is allocated into 64 `Partition`s where each partition is assigned to a certain region + +```rust + +struct Player { + partition_id: usize + inventory: Inventory +} + +struct Entity { + location: Location + partion_id: usize +} + +struct Packet { + // ... + group: Option +} + +enum PacketGroup { + PlayerLocal, + RegionLocal, + Global, + CrossRegion { + from: usize, + to: usize + }, + +} + +impl Packet { + fn group(&self, client: &ClientState, world: &World) -> PacketGroup { + if self.requires_global_mut() { + return PacketGroup::Global; + } + + if let Some(boundary) = self.crosses_boundary(client, world) { + return PacketGroup::CrossRegion(boundary) + } + + if self.no_region_modification() { + return PacketGroup::Player + } + + // Confirm Teleportation + // Chat Message + // Edit Book (modified player inv) + // Keep Alive + PacketGroup::Region + } + + fn crosses_boundary(&self, client: &ClientState, world: &World) -> Option { + match self.kind { + SetPlayerPositionAndRotation | MoveVehicle (new_loc, ...) => world.get_opt_cross(client.loc, new_loc) + _ => None + } + } + + /// do not care about region even + fn client(&self) { + // Confirm Teleportation + // Chat Message + // Edit Book (modified player inv) + // Keep Alive + } + + fn regional(&self, client: &Client, world: &World) { + // Click Container (assuming it does not cross regions) + // Close Container + // Interact (set to the partition of the entity) + } + + fn requires_global_mut(&self) -> bool { + // Confirm Teleportation + // + } +} + +struct Partition { + player_ids: Vec + chunk_ids: Vec + adjacent: Vec +} + +struct World { + partitions: [Partition; 64] + local_packets: Recv, + global_packes: Recv +} + +struct Thread { + partition: Partiton +} + +impl Thread { + // iterator of players not related to partition... should be equal split + // between threads + fn assigned_global_players(&mut self) -> impl Iterator<&mut Player> + + // iterator of entities not related to partition... should be equal split + // between threads + fn assigned_global_living_entities(&mut self) -> impl Iterator<&mut Entity> + + fn players_in_region(&mut self) -> impl Iterator<&mut Player> + + fn general(world: &WorldState, messages: &mut Messages) { + for entity in world.assigned_global_entities() { + if let Some(message) = entity.physics() { + // new location of the entity, + // the entity (say an arrow) hitting a player for instance + messages.push(message); + } + } + + for player in world.assigned_global_players() { + if let Some(message) = player.physics() { + // new location of the entity, + // the entity (say an arrow) hitting a player for instance + messages.push(message); + } + } + } + + fn apply_messages(world: &WorldState, input_messages: &Messages) { + + } + + fn run_cycle() { + + + for player in self.assigned_global_players() { + for packet in player.packets() { + let group = packet.assign_group(); + + if group == PlayerLocal { + // process + } + } + } + + for entity in self.assigned_global_living_entities() { + // we probably do not need to modify any blocks etc for basic entities + entity.physics() + } + + + + + + + } +} + + +``` + +```rust diff --git a/prototype/.github/workflows/build.yml b/prototype/.github/workflows/build.yml new file mode 100644 index 00000000..e5e727e7 --- /dev/null +++ b/prototype/.github/workflows/build.yml @@ -0,0 +1,117 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + merge_group: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + + +jobs: + udeps: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@nightly + + - uses: Swatinem/rust-cache@v2 + + - name: Install cargo-udeps + run: cargo install cargo-udeps + + - name: Run cargo udeps + run: | + cargo +nightly udeps --all + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@nextest + + - name: Run cargo nextest + run: cargo nextest run + fmt: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + + - uses: Swatinem/rust-cache@v2 + + - name: Run rustfmt + run: | + cargo +nightly fmt --all -- --check + clippy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@nightly + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Clippy check + run: cargo clippy + +# deploy: +# if: github.ref == 'refs/heads/main' +# runs-on: ubuntu-latest +# needs: +# - clippy +# - fmt +# - test +# - udeps +# +# steps: +# - uses: actions/checkout@v4 +# +# - name: Login to Docker Registry +# uses: azure/docker-login@v1 +# with: +# login-server: TODO.azurecr.io +# username: ${{ secrets.REGISTRY_USERNAME }} +# password: ${{ secrets.REGISTRY_PASSWORD }} +# +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v3 +# +# - name: Build and Push Docker image +# uses: docker/build-push-action@v5 +# with: +# context: . +# file: ./Dockerfile +# push: true +# tags: TODO.azurecr.io/TODO:${{ github.sha }} +# cache-from: type=gha +# cache-to: type=gha,mode=max +# +# - name: Deploy to Azure Web App +# uses: azure/webapps-deploy@v2 +# with: +# app-name: 'TODO' +# publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} +# images: 'TODO.azurecr.io/myapp:${{ github.sha }}' + diff --git a/prototype/.gitignore b/prototype/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/prototype/.gitignore @@ -0,0 +1 @@ +/target diff --git a/prototype/Cargo.toml b/prototype/Cargo.toml new file mode 100644 index 00000000..ba5fdcb6 --- /dev/null +++ b/prototype/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "prototype" +version = "0.1.0" +edition = "2021" +authors = ["Andrew Gazelka "] +readme = "README.md" + +[dependencies] +anyhow = "1.0.80" +flume = "0.11.0" +rayon = "1.9.0" + + +[lints.rust] +#warnings = "deny" + +[lints.clippy] +# cargo +cargo_common_metadata = "allow" +multiple_crate_versions = "warn" +negative_feature_names = "deny" +redundant_feature_names = "deny" +wildcard_dependencies = "deny" + +restriction = { level = "deny", priority = -1 } +missing_docs_in_private_items = "allow" +question_mark_used = "allow" +print_stdout = "allow" +implicit_return = "allow" +shadow_reuse = "allow" +absolute_paths = "allow" +use_debug = "allow" +unwrap_used = "allow" +std_instead_of_alloc = "allow" # consider denying +default_numeric_fallback = "allow" +as_conversions = "allow" +arithmetic_side_effects = "allow" +shadow_unrelated = "allow" +unseparated_literal_suffix = "allow" +else_if_without_else = "allow" +float_arithmetic = "allow" +single_call_fn = "allow" +missing_inline_in_public_items = "allow" +exhaustive_structs = "allow" +pub_use = "allow" +let_underscore_untyped = "allow" +infinite_loop = "allow" +single_char_lifetime_names = "allow" +min_ident_chars = "allow" +std_instead_of_core = "allow" +panic_in_result_fn = "allow" +panic = "allow" +missing_trait_methods = "allow" +todo = "allow" + +complexity = "deny" + +nursery = { level = "deny", priority = -1 } +future_not_send = "allow" + +pedantic = { level = "deny", priority = -1 } +uninlined_format_args = "allow" # consider denying; this is allowed because Copilot often generates code that triggers this lint +needless_pass_by_value = "allow" # consider denying +cast_lossless = "allow" +cast_possible_truncation = "allow" # consider denying +cast_precision_loss = "allow" # consider denying +missing_errors_doc = "allow" # consider denying +struct_excessive_bools = "allow" +wildcard_imports = "allow" + +perf = "deny" + +style = "deny" + +suspicious = { level = "deny", priority = -1 } +blanket_clippy_restriction_lints = "allow" diff --git a/prototype/README.md b/prototype/README.md new file mode 100644 index 00000000..e69de29b diff --git a/prototype/clippy.toml b/prototype/clippy.toml new file mode 100644 index 00000000..377098bf --- /dev/null +++ b/prototype/clippy.toml @@ -0,0 +1,3 @@ +# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html +cognitive-complexity-threshold = 5 +excessive-nesting-threshold = 5 diff --git a/prototype/rustfmt.toml b/prototype/rustfmt.toml new file mode 100644 index 00000000..7073741e --- /dev/null +++ b/prototype/rustfmt.toml @@ -0,0 +1,21 @@ +combine_control_expr = true +comment_width = 100 # https://lkml.org/lkml/2020/5/29/1038 +condense_wildcard_suffixes = true +control_brace_style = "AlwaysSameLine" +edition = "2021" +format_code_in_doc_comments = true +format_macro_bodies = true +format_macro_matchers = true +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +merge_derives = false +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +overflow_delimited_expr = true +reorder_impl_items = true +reorder_imports = true +unstable_features = true +wrap_comments = true + diff --git a/prototype/src/global.rs b/prototype/src/global.rs new file mode 100644 index 00000000..47eb82ff --- /dev/null +++ b/prototype/src/global.rs @@ -0,0 +1,30 @@ +use crate::{utils::split_into_mut, Entity, IdMap, Thread, World}; + +struct Global { + threads: Vec, + entities: IdMap, + world: World, +} + +impl Global { + + fn parallel_tasks(&mut self) { + rayon::scope(|s| { + let threads = &mut self.threads; + let entities = split_into_mut(threads.len(), &mut self.entities); + let world = &self.world; + + for (thread, entities) in threads.iter_mut().zip(entities) { + s.spawn(move |_| { + // unwrap not ideal + #[allow(clippy::unwrap_used)] + thread.process(entities, world).unwrap(); + }); + } + }); + } + + fn run_cycle(&mut self) { + self.parallel_tasks(); + } +} diff --git a/prototype/src/lib.rs b/prototype/src/lib.rs new file mode 100644 index 00000000..92e190a7 --- /dev/null +++ b/prototype/src/lib.rs @@ -0,0 +1,160 @@ +#![feature(split_at_checked)] + +use std::collections::HashMap; + +use crate::messages::{Message, MessageSender}; + +// todo +type PlayerInventory = (); + +type PartitionId = usize; +type PlayerId = usize; +type ChunkId = (i32, i32); +type MultiMap = HashMap>; +type Chunked = Vec; + +type IdMap = Vec; + +mod global; +mod messages; +mod utils; + +struct Location { + x: f64, + y: f64, + z: f64, +} + +struct Player { + partition_in: PartitionId, + inventory: PlayerInventory, + location: Location, +} + +struct Entity { + partition_in: PartitionId, + location: Location, +} + +struct Packet { + // ... + group: Option, +} + +enum PacketGroup { + PlayerLocal, + RegionLocal, + Global, + CrossRegion { from: usize, to: usize }, +} + +impl Packet { + // also take context of world + fn group(&self) -> PacketGroup { + todo!() + } +} + +struct World { + partitions: Vec, + players: Vec, + entities: Vec, +} + +struct Partition { + players_in: Vec, + chunk_ids: Vec, + adjacent: Vec, +} + +// struct World { +// partitions: [Partition; 64] +// local_packets: Recv, +// global_packes: Recv +// } +// +// struct Thread { +// partition: Partiton +// } + +struct Thread { + partition: Partition, + messages: MessageSender, +} + +impl Thread { + fn process(&mut self, entities: &mut [Entity], world: &World) -> anyhow::Result<()> { + for entity in entities { + + } + Ok(()) + } +} + +// impl Thread { +// fn sar +// +// +// +// +// +// } + +// impl Thread { +// // iterator of players not related to partition... should be equal split +// // between threads +// fn assigned_global_players(&mut self) -> impl Iterator<&mut Player> +// +// // iterator of entities not related to partition... should be equal split +// // between threads +// fn assigned_global_living_entities(&mut self) -> impl Iterator<&mut Entity> +// +// fn players_in_region(&mut self) -> impl Iterator<&mut Player> +// +// fn general(world: &WorldState, messages: &mut Messages) { +// for entity in world.assigned_global_entities() { +// if let Some(message) = entity.physics() { +// // new location of the entity, +// // the entity (say an arrow) hitting a player for instance +// messages.push(message); +// } +// } +// +// for player in world.assigned_global_players() { +// if let Some(message) = player.physics() { +// // new location of the entity, +// // the entity (say an arrow) hitting a player for instance +// messages.push(message); +// } +// } +// } +// +// fn apply_messages(world: &WorldState, input_messages: &Messages) { +// +// } +// +// fn run_cycle() { +// +// +// for player in self.assigned_global_players() { +// for packet in player.packets() { +// let group = packet.assign_group(); +// +// if group == PlayerLocal { +// // process +// } +// } +// } +// +// for entity in self.assigned_global_living_entities() { +// // we probably do not need to modify any blocks etc for basic entities +// entity.physics() +// } +// +// +// +// +// +// +// } +// } diff --git a/prototype/src/messages.rs b/prototype/src/messages.rs new file mode 100644 index 00000000..cb056a0a --- /dev/null +++ b/prototype/src/messages.rs @@ -0,0 +1,47 @@ +use crate::{IdMap, PartitionId}; + +pub enum Message { + Test, +} + +pub struct MessageSender { + for_partition: IdMap>, + for_global: flume::Sender, +} + +impl MessageSender { + pub fn send_partition(&self, id: PartitionId, message: Message) -> anyhow::Result<()> { + let Some(partition) = self.for_partition.get(id) else { + panic!("somehow tried to send a message to a partition that doesn't exist"); + }; + + partition.send(message)?; + Ok(()) + } + + pub fn send_global(&self, message: Message) -> anyhow::Result<()> { + self.for_global.send(message)?; + Ok(()) + } +} + +pub struct MessageReceiver { + for_partition: IdMap>, + for_global: flume::Receiver, +} + +impl MessageReceiver { + pub fn recv_partition(&self, id: PartitionId) -> anyhow::Result { + let Some(partition) = self.for_partition.get(id) else { + panic!("somehow tried to receive a message from a partition that doesn't exist"); + }; + + let v = partition.recv()?; + Ok(v) + } + + pub fn recv_global(&self) -> anyhow::Result { + let v = self.for_global.recv()?; + Ok(v) + } +} diff --git a/prototype/src/utils.rs b/prototype/src/utils.rs new file mode 100644 index 00000000..2777435f --- /dev/null +++ b/prototype/src/utils.rs @@ -0,0 +1,80 @@ +struct SplitIntoMut<'a, T> { + input: Option<&'a mut [T]>, + count: usize, + lower_count: usize, + num_chunks_with_rem: usize, +} + +impl<'a, T> SplitIntoMut<'a, T> { + fn new(count: usize, input: &'a mut [T]) -> Self { + let input_len = input.len(); + + // calculate div and remainder + #[allow(clippy::integer_division)] + let lower_count = input_len / count; + let num_chunks_with_rem = input_len % count; + + Self { + input: Some(input), + count, + lower_count, + num_chunks_with_rem, + } + } +} + +impl<'a, T> Iterator for SplitIntoMut<'a, T> { + type Item = &'a mut [T]; + + fn next(&mut self) -> Option { + let input = self.input.take()?; + + let amount_to_take = if self.num_chunks_with_rem > 0 { + self.num_chunks_with_rem -= 1; + self.lower_count + 1 + } else { + self.lower_count + }; + + let (split, rest) = input.split_at_mut(amount_to_take); + + if !rest.is_empty() { + self.input = Some(rest); + } + + Some(split) + } +} + +pub fn split_into_mut(count: usize, input: &mut [T]) -> impl Iterator { + SplitIntoMut::new(count, input) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_split_into_mut() { + let mut input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let mut iter = super::split_into_mut(3, &mut input); + + assert_eq!(iter.next().unwrap(), &[1, 2, 3, 4]); + assert_eq!(iter.next().unwrap(), &[5, 6, 7]); + assert_eq!(iter.next().unwrap(), &[8, 9, 10]); + assert!(iter.next().is_none()); + + let mut input = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut iter = super::split_into_mut(3, &mut input); + + assert_eq!(iter.next().unwrap(), &[1, 2, 3]); + assert_eq!(iter.next().unwrap(), &[4, 5, 6]); + assert_eq!(iter.next().unwrap(), &[7, 8, 9]); + + let mut input = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + let mut iter = super::split_into_mut(4, &mut input); + + assert_eq!(iter.next().unwrap(), &[1, 2, 3]); + assert_eq!(iter.next().unwrap(), &[4, 5]); + assert_eq!(iter.next().unwrap(), &[6, 7]); + assert_eq!(iter.next().unwrap(), &[8, 9]); + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index fe967d8c..8dbcf56f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,6 +18,8 @@ bytes = "1.5.0" #valence = { git = "https://github.com/valence-rs/valence" } valence_protocol = { git = "https://github.com/andrewgazelka/valence" } valence_registry = { git = "https://github.com/andrewgazelka/valence" } +#valence_server = { git = "https://github.com/andrewgazelka/valence" } + #azalea = { git = "https://github.com/azalea-rs/azalea", branch = "1.20.1" } azalea-buf = { git = "https://github.com/azalea-rs/azalea", branch = "1.20.1" } azalea-world = { git = "https://github.com/azalea-rs/azalea", branch = "1.20.1" } @@ -26,6 +28,8 @@ azalea-world = { git = "https://github.com/azalea-rs/azalea", branch = "1.20.1" # no secure alloc mimalloc = { version = "0.1.39" , default-features = false } sha2 = "0.10.8" +itertools = "0.12.1" +rand = "0.8.5" [lints.rust] @@ -65,6 +69,7 @@ infinite_loop = "allow" single_char_lifetime_names = "allow" min_ident_chars = "allow" std_instead_of_core = "allow" +items_after_statements = "allow" complexity = "deny" diff --git a/server/src/chunk.rs b/server/src/chunk.rs new file mode 100644 index 00000000..04ec2e02 --- /dev/null +++ b/server/src/chunk.rs @@ -0,0 +1,25 @@ +// Compound containing one long array named MOTION_BLOCKING, which is a heightmap for the highest +// solid block at each position in the chunk (as a compacted long array with 256 entries, with the +// number of bits per entry varying depending on the world's height, defined by the formula +// ceil(log2(height + 1))). The Notchian server also adds a WORLD_SURFACE long array, the purpose of +// which is unknown, but it's not required for the chunk to be accepted. + +use azalea_world::BitStorage; + +pub const fn ceil_log2(x: u32) -> u32 { + u32::BITS - x.leading_zeros() +} + +pub fn heightmap(max_height: u32, current_height: u32) -> Vec { + let bits = ceil_log2(max_height + 1); + let mut data = BitStorage::new(bits as usize, 16 * 16, None).unwrap(); + + for x in 0usize..16 { + for z in 0usize..16 { + let index = x + z * 16; + data.set(index, current_height as u64 + 1); + } + } + + data.data +} diff --git a/server/src/main.rs b/server/src/main.rs index 2a5ffe11..306b05b1 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,5 +1,7 @@ // #![allow(unused)] +mod chunk; + #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -8,6 +10,8 @@ use std::{borrow::Cow, collections::BTreeSet, io, io::ErrorKind}; use anyhow::{ensure, Context}; use azalea_buf::McBufWritable; use bytes::BytesMut; +use itertools::Itertools; +use rand::random; use serde_json::json; use sha2::Digest; use tokio::{ @@ -27,16 +31,18 @@ use valence_protocol::{ play::{ player_list_s2c::{PlayerListActions, PlayerListEntry}, player_position_look_s2c::PlayerPositionLookFlags, - FullC2s, SynchronizeTagsS2c, + SynchronizeTagsS2c, }, status, }, uuid::Uuid, BlockPos, Bounded, ChunkPos, Decode, Encode, GameMode, Ident, Packet, PacketDecoder, - PacketEncoder, RawBytes, VarInt, VarLong, + PacketEncoder, RawBytes, VarInt, }; use valence_registry::{BiomeRegistry, RegistryCodec, TagsRegistry}; +use crate::chunk::heightmap; + const READ_BUF_SIZE: usize = 4096; /// The Minecraft protocol version this library currently targets. @@ -57,6 +63,29 @@ struct Io { enc: PacketEncoder, frame: PacketFrame, } +// fn motion_blocking(chunk: &azalea_world::Chunk) -> Vec> { +// let mut heightmap: Vec> = vec![vec![0; 16]; 16]; +// +// let height = chunk.sections.len() as u32 * 16; +// +// for z in 0..16 { +// for x in 0..16 { +// for y in (0..height).rev() { +// let state = chunk.get(x as u32, y, z as u32); +// // let state = self.block_state(x as u32, y, z as u32); +// if state.blocks_motion() +// || state.is_liquid() +// || state.get(PropName::Waterlogged) == Some(PropValue::True) +// { +// heightmap[z][x] = y + 2; +// break; +// } +// } +// } +// } +// +// heightmap +// } impl Io { pub async fn recv_packet<'a, P>(&'a mut self) -> anyhow::Result

@@ -90,7 +119,7 @@ impl Io { pub async fn recv_packet_raw(&mut self) -> anyhow::Result { loop { if let Some(frame) = self.dec.try_next_packet()? { - info!("read packet id {:#x}", frame.id); + // info!("read packet id {:#x}", frame.id); return Ok(frame); } @@ -143,61 +172,9 @@ impl Io { self.stream.write_all(&bytes).await?; self.stream.flush().await?; // todo: remove - // info!("wrote {pkt:#?}"); - Ok(()) } - // async fn client_process(mut self) -> anyhow::Result<()> { - // use valence_protocol::packets::handshaking; - // - // let pkt = HandshakeC2s { - // protocol_version: PROTOCOL_VERSION.into(), - // server_address: "localhost:25565".into(), - // server_port: 25565, - // next_state: HandshakeNextState::Login, - // }; - // - // self.send_packet(&pkt).await?; - // - // self.client_login().await?; - // - // Ok(()) - // } - - // async fn client_login(mut self) -> anyhow::Result<()> { - // use valence_protocol::packets::login; - // - // let pkt = login::LoginHelloC2s { - // username: "Emerald_Explorer".into(), - // profile_id: Some(Uuid::from_u128(0)), - // }; - // - // self.send_packet(&pkt).await?; - // - // // let login::LoginCompressionS2c { threshold } = self.recv_packet().await?; - // // - // // let threshold = threshold.0; - // // info!("compression threshold {threshold}"); - // - // let pkt: login::LoginSuccessS2c = self.recv_packet().await?; - // - // // self.client_read_loop().await?; - // - // Ok(()) - // } - - // async fn client_read_loop(mut self) -> anyhow::Result<()> { - // let game_join: playGameJoinS2c = self.recv_packet().await?; - // - // loop { - // let frame = self.recv_packet_raw().await?; - // let id = frame.id; - // // hex - // info!("read packet id {id:#x}"); - // } - // } - async fn server_process(mut self, id: usize) -> anyhow::Result<()> { // self.stream.set_nodelay(true)?; @@ -283,13 +260,13 @@ impl Io { dimension_names: Cow::Owned(dimension_names), registry_codec: Cow::Borrowed(®istry_codec), max_players: 10_000.into(), - view_distance: 10.into(), + view_distance: 32.into(), // max view distance simulation_distance: 10.into(), reduced_debug_info: false, enable_respawn_screen: false, dimension_name: dimension_name.into(), hashed_seed: 0, - game_mode: GameMode::Survival, + game_mode: GameMode::Creative, is_flat: false, last_death_location: None, portal_cooldown: 60.into(), @@ -387,7 +364,7 @@ impl Io { chat_data: None, listed: true, // show on player list ping: 0, // ms - game_mode: GameMode::Survival, + game_mode: GameMode::Creative, display_name: None, }; @@ -407,15 +384,7 @@ impl Io { }) .await?; - // 25. Set Center Chunk - self.send_packet(&play::ChunkRenderDistanceCenterS2c { - chunk_x: VarInt(0), - chunk_z: VarInt(0), - }) - .await?; - // 26. Update light - let mut pkt = play::LightUpdateS2c { chunk_x: VarInt::default(), chunk_z: VarInt::default(), @@ -436,58 +405,18 @@ impl Io { } } - // 27. Chunk Data - - let mut chunk = azalea_world::Chunk::default(); - - #[allow(clippy::indexing_slicing)] - let first_section = &mut chunk.sections[0]; - - let states = &mut first_section.states; - - for x in 0..16 { - for z in 0..16 { - let id: u32 = 2; - states.set(x, 0, z, id); - } - } - - let mut bytes = Vec::new(); - - chunk.write_into(&mut bytes)?; - - let mut chunk = play::ChunkDataS2c { - pos: ChunkPos::new(0, 0), - heightmaps: Cow::Owned(Compound::new()), - blocks_and_biomes: &bytes, - block_entities: Cow::Borrowed(&[]), - sky_light_mask: Cow::Borrowed(&[]), - block_light_mask: Cow::Borrowed(&[]), - empty_sky_light_mask: Cow::Borrowed(&[]), - empty_block_light_mask: Cow::default(), - sky_light_arrays: Cow::default(), - block_light_arrays: Cow::Borrowed(&[]), - }; - - for x in -16..=16 { - for z in -16..=16 { - chunk.pos = ChunkPos::new(x, z); - self.send_packet(&chunk).await?; - } - } - - // 28. Initialize World Border - self.send_packet(&play::WorldBorderInitializeS2c { - x: 0.0, - z: 0.0, - old_diameter: 1000.0, - new_diameter: 1000.0, - duration_millis: VarLong::default(), - portal_teleport_boundary: VarInt::default(), - warning_blocks: VarInt::default(), - warning_time: VarInt::default(), - }) - .await?; + // // 28. Initialize World Border + // self.send_packet(&play::WorldBorderInitializeS2c { + // x: 0.0, + // z: 0.0, + // old_diameter: 10.0, + // new_diameter: 10.0, + // duration_millis: VarLong::default(), + // portal_teleport_boundary: VarInt::default(), + // warning_blocks: VarInt::default(), + // warning_time: VarInt::default(), + // }) + // .await?; // 29. S → C: Set Default Spawn Position (“home” spawn, not where the client will spawn on // login) @@ -498,12 +427,12 @@ impl Io { .await?; // 32. C → S: Set Player Position and Rotation (to confirm the spawn position) - let pkt = self.recv_packet::().await?; - info!("32. {pkt:#?}"); + let pkt = self.recv_packet::().await?; + info!("32. {pkt:?}"); // Set Player Position let pkt = self.recv_packet::().await?; - info!("32. {pkt:#?}"); + info!("32. {pkt:?}"); // 30.Synchronize Player Position (Required, tells the client they're ready to spawn) self.send_packet(&play::PlayerPositionLookS2c { @@ -530,12 +459,12 @@ impl Io { // Set Player Position if id == play::PositionAndOnGroundC2s::ID { let pkt = frame.decode::()?; - info!("32. {pkt:#?}"); + info!("32. {pkt:?}"); } }; let teleport_id = recv.teleport_id.0; - info!("read {recv:#?}"); + info!("read {recv:?}"); ensure!( teleport_id == 1, "expected teleport id 1, got {teleport_id}" @@ -543,7 +472,7 @@ impl Io { // 30.Synchronize Player Position (Required, tells the client they're ready to spawn) self.send_packet(&play::PlayerPositionLookS2c { - position: DVec3::new(0.0, 4.0, 0.0), + position: DVec3::new(0.0, 200.0, 0.0), yaw: 0.0, pitch: 0.0, flags: PlayerPositionLookFlags::default(), @@ -551,27 +480,139 @@ impl Io { }) .await?; - // 33. C → S: Client Command (sent either before or while receiving chunks, further testing - // needed, server handles correctly if not sent) - // let pkt = self.recv_packet::().await?; - // info!("33. {pkt:#?}"); + // 25. Set Center Chunk + self.send_packet(&play::ChunkRenderDistanceCenterS2c { + chunk_x: VarInt(0), + chunk_z: VarInt(0), + }) + .await?; + + // 27. Chunk Data + #[allow(clippy::integer_division)] + let mut chunk = azalea_world::Chunk::default(); + let dimension_height = 384; - // 34. S → C: inventory, entities, etc - // todo + // // blockstate + // #[allow(clippy::cast_possible_truncation)] + // let dirt = BlockState::GRAN.to_raw(); + // + // #[allow(clippy::indexing_slicing)] + // for section in &mut chunk.sections { + // + // let states = &mut section.states; + // for x in 0..16 { + // for z in 0..16 { + // for y in 0..16 { + // // let id: u32 = 2; + // states.set(x, y, z, 2); + // } + // } + // } + // } - // read packet - loop { - let frame = self.recv_packet_raw().await?; - let id = frame.id; - // hex - info!("read packet id {id:#x}"); + for section in chunk.sections.iter_mut().take(1) { + // Sections with a block count of 0 are not rendered + section.block_count = 4096; - // Set Player Position - if id == play::PositionAndOnGroundC2s::ID { - let pkt = frame.decode::()?; - info!("{pkt:#?}"); + // Set the Palette to be a single value + let states = &mut section.states; + states.palette = azalea_world::palette::Palette::SingleValue(2); + } + + let map = heightmap(dimension_height, dimension_height - 3); + let map: Vec<_> = map.into_iter().map(i64::try_from).try_collect()?; + + let mut bytes = Vec::new(); + chunk.write_into(&mut bytes)?; + + let mut pkt = play::ChunkDataS2c { + pos: ChunkPos::new(0, 0), + heightmaps: Cow::Owned(compound! { + "MOTION_BLOCKING" => List::Long(map), + }), + blocks_and_biomes: &bytes, + block_entities: Cow::Borrowed(&[]), + + sky_light_mask: Cow::Borrowed(&[]), + block_light_mask: Cow::Borrowed(&[]), + empty_sky_light_mask: Cow::Borrowed(&[]), + empty_block_light_mask: Cow::Borrowed(&[]), + sky_light_arrays: Cow::Borrowed(&[]), + block_light_arrays: Cow::Borrowed(&[]), + }; + for x in -16..=16 { + for z in -16..=16 { + pkt.pos = ChunkPos::new(x, z); + self.send_packet(&pkt).await?; } } + + // 25. Set Center Chunk + self.send_packet(&play::ChunkRenderDistanceCenterS2c { + chunk_x: VarInt(0), + chunk_z: VarInt(0), + }) + .await?; + + // // 28. Initialize World Border + // self.send_packet(&play::WorldBorderInitializeS2c { + // x: 10.0, + // z: 10.0, + // old_diameter: 10.0, + // new_diameter: 10.0, + // duration_millis: VarLong::default(), + // portal_teleport_boundary: VarInt::default(), + // warning_blocks: VarInt::default(), + // warning_time: VarInt::default(), + // }) + // .await?; + // + // // border center + // self.send_packet(&play::WorldBorderCenterChangedS2c { + // x_pos: 0.0, + // z_pos: 0.0, + // }).await?; + // + // // size + // self.send_packet(&play::WorldBorderSizeChangedS2c { + // diameter: 30.0, + // }).await?; + + // read packet + loop { + // schedule every 2 seconds + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + self.send_packet(&play::KeepAliveS2c { id: random() }) + .await?; + + // let frame_read = self.recv_packet_raw(); + // + // + // let id = frame.id; + // // Set Player Position + // match id { + // play::PositionAndOnGroundC2s::ID + // | play::LookAndOnGroundC2s::ID + // | play::FullC2s::ID => {} + // + // play::TeleportConfirmC2s::ID => { + // // 0x00 + // let pkt = frame.decode::()?; + // info!("{pkt:?}"); + // } + // + // play::UpdatePlayerAbilitiesC2s::ID => { + // // 0x1C + // let pkt = frame.decode::()?; + // info!("{pkt:?}"); + // } + // _ => { + // info!("ID: {id:#x}"); + // } + // + // } + } } async fn server_status(mut self) -> anyhow::Result<()> {