├── .envrc ├── CNAME ├── .github ├── chart │ ├── README.md │ ├── .python-version │ ├── .gitignore │ └── pyproject.toml ├── workflows │ ├── conventional.yml │ └── deploy.yml ├── dependabot.yml ├── FUNDING.yml └── codecov.yml ├── crates ├── hyperion │ ├── README.md │ ├── src │ │ ├── common │ │ │ ├── util │ │ │ │ └── mod.rs │ │ │ ├── runtime.rs │ │ │ ├── mod.rs │ │ │ └── config.rs │ │ ├── storage │ │ │ ├── mod.rs │ │ │ ├── buf.rs │ │ │ └── db.rs │ │ ├── ingress │ │ │ └── data │ │ │ │ └── hyperion.png │ │ ├── simulation │ │ │ ├── data │ │ │ │ └── registries.nbt │ │ │ ├── metadata │ │ │ │ ├── item.rs │ │ │ │ ├── block_display.rs │ │ │ │ ├── status.rs │ │ │ │ ├── player.rs │ │ │ │ ├── entity │ │ │ │ │ └── flags.rs │ │ │ │ ├── entity.rs │ │ │ │ └── display.rs │ │ │ ├── blocks │ │ │ │ └── shared.rs │ │ │ ├── animation.rs │ │ │ ├── packet_state.rs │ │ │ ├── packet.rs │ │ │ ├── entity_kind.rs │ │ │ └── skin.rs │ │ ├── net │ │ │ ├── agnostic.rs │ │ │ ├── agnostic │ │ │ │ ├── chat.rs │ │ │ │ └── sound.rs │ │ │ └── packets.rs │ │ └── egress │ │ │ ├── metadata.rs │ │ │ ├── stats.rs │ │ │ └── mod.rs │ ├── run │ │ └── config.toml │ ├── benches │ │ └── set.rs │ ├── tests │ │ ├── entity.rs │ │ ├── collision.rs │ │ └── spatial.rs │ └── Cargo.toml ├── geometry │ ├── README.md │ ├── .gitignore │ ├── src │ │ └── lib.rs │ ├── build.rs │ └── Cargo.toml ├── hyperion-proto │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ ├── shared.rs │ │ └── proxy_to_server.rs │ └── Cargo.toml ├── hyperion-clap-macros │ ├── README.md │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── hyperion-clap │ ├── .gitignore │ ├── README.md │ ├── tests │ │ └── gamemode.rs │ └── Cargo.toml ├── hyperion-item │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ ├── builder │ │ └── book.rs │ │ └── lib.rs ├── hyperion-text │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ ├── font.rs │ │ ├── scoreboard.rs │ │ ├── helper.rs │ │ └── event.rs ├── simd-utils │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ ├── proptest-regressions │ │ └── lib.txt │ └── src │ │ └── one_bit_positions.rs ├── hyperion-command │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ └── component.rs │ └── Cargo.toml ├── hyperion-crafting │ ├── .gitignore │ ├── README.md │ └── Cargo.toml ├── hyperion-genmap │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── hyperion-inventory │ ├── .gitignore │ ├── README.md │ └── Cargo.toml ├── hyperion-nerd-font │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── hyperion-palette │ ├── .gitignore │ ├── README.md │ └── Cargo.toml ├── hyperion-scheduled │ ├── .gitignore │ ├── README.md │ └── Cargo.toml ├── hyperion-stats │ ├── .gitignore │ ├── Cargo.toml │ ├── benches │ │ └── parallel_stats.rs │ └── README.md ├── hyperion-utils │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ ├── prev.rs │ │ ├── cached_save.rs │ │ └── lib.rs ├── hyperion-packet-macros │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── state.rs ├── hyperion-permission │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ ├── storage.rs │ │ └── lib.rs ├── packet-channel │ ├── README.md │ ├── Cargo.toml │ └── benches │ │ └── packet_channel.rs ├── hyperion-minecraft-proto │ ├── .gitignore │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── hyperion-proxy │ ├── README.md │ ├── src │ │ ├── util.rs │ │ ├── data.rs │ │ ├── server_sender.rs │ │ └── egress.rs │ └── Cargo.toml ├── hyperion-proxy-module │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── bvh-region │ ├── src │ │ ├── query.rs │ │ ├── node.rs │ │ ├── utils.rs │ │ └── query │ │ │ └── closest.rs │ ├── README.md │ ├── Cargo.toml │ └── benches │ │ └── sort.rs └── hyperion-gui │ ├── Cargo.toml │ └── src │ └── lib.rs ├── events └── bedwars │ ├── README.md │ ├── src │ ├── plugin.rs │ ├── command │ │ ├── xp.rs │ │ ├── speed.rs │ │ ├── bow.rs │ │ ├── fly.rs │ │ ├── vanish.rs │ │ ├── chest.rs │ │ ├── shoot.rs │ │ └── gui.rs │ ├── command.rs │ ├── plugin │ │ ├── regeneration.rs │ │ ├── vanish.rs │ │ ├── damage.rs │ │ ├── chat.rs │ │ └── stats.rs │ └── main.rs │ ├── build.rs │ └── Cargo.toml ├── tools ├── rust-mc-bot │ ├── .gitignore │ ├── src │ │ └── states │ │ │ ├── mod.rs │ │ │ ├── status.rs │ │ │ └── login.rs │ ├── README.md │ └── Cargo.toml ├── antithesis-bot │ ├── src │ │ ├── main.rs │ │ └── lib.rs │ └── Cargo.toml └── packet-inspector │ ├── src │ ├── main.rs │ ├── app │ │ ├── connection.rs │ │ └── hex_viewer.rs │ └── main_cli.rs │ ├── Cargo.toml │ └── README.md ├── libvoidstar.so ├── .cargo └── config.toml ├── rust-toolchain.toml ├── clippy.toml ├── .devcontainer.json ├── .gitignore ├── docs ├── bedwars │ └── introduction.md ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ ├── custom.css │ │ └── components │ │ │ └── GithubSnippet.vue │ └── config.mts └── index.md ├── rustfmt.toml ├── .dockerignore ├── package.json ├── flake.lock └── docker-compose.yml /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | hyperion.rs -------------------------------------------------------------------------------- /.github/chart/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/hyperion/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/geometry/README.md: -------------------------------------------------------------------------------- 1 | # aabb -------------------------------------------------------------------------------- /crates/hyperion-proto/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/chart/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /crates/geometry/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-clap-macros/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /events/bedwars/README.md: -------------------------------------------------------------------------------- 1 | # Bedwars 2 | -------------------------------------------------------------------------------- /crates/hyperion-clap/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-item/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-text/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/simd-utils/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/simd-utils/README.md: -------------------------------------------------------------------------------- 1 | # simd-utils -------------------------------------------------------------------------------- /.github/chart/.gitignore: -------------------------------------------------------------------------------- 1 | performance.png 2 | -------------------------------------------------------------------------------- /crates/hyperion-clap/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-clap -------------------------------------------------------------------------------- /crates/hyperion-command/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-crafting/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-genmap/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-inventory/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-item/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-item -------------------------------------------------------------------------------- /crates/hyperion-nerd-font/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-palette/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-scheduled/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-stats/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-text/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-text -------------------------------------------------------------------------------- /crates/hyperion-utils/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-utils/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-utils -------------------------------------------------------------------------------- /tools/rust-mc-bot/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | -------------------------------------------------------------------------------- /crates/hyperion-clap-macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-command/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-command -------------------------------------------------------------------------------- /crates/hyperion-crafting/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-crafting -------------------------------------------------------------------------------- /crates/hyperion-genmap/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-genmap -------------------------------------------------------------------------------- /crates/hyperion-packet-macros/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-palette/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-palette -------------------------------------------------------------------------------- /crates/hyperion-permission/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/packet-channel/README.md: -------------------------------------------------------------------------------- 1 | # packet-channel 2 | -------------------------------------------------------------------------------- /crates/hyperion-inventory/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-inventory -------------------------------------------------------------------------------- /crates/hyperion-minecraft-proto/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/hyperion-nerd-font/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-nerd-font -------------------------------------------------------------------------------- /crates/hyperion-permission/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-permission -------------------------------------------------------------------------------- /crates/hyperion-proxy/README.md: -------------------------------------------------------------------------------- 1 | todo: write readme 2 | -------------------------------------------------------------------------------- /crates/hyperion-scheduled/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-temp-block -------------------------------------------------------------------------------- /crates/hyperion/src/common/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mojang; 2 | -------------------------------------------------------------------------------- /crates/geometry/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod aabb; 2 | pub mod ray; 3 | -------------------------------------------------------------------------------- /crates/hyperion-proxy-module/README.md: -------------------------------------------------------------------------------- 1 | todo: write readme 2 | -------------------------------------------------------------------------------- /crates/hyperion-minecraft-proto/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-minecraft-proto -------------------------------------------------------------------------------- /crates/hyperion-packet-macros/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-packet-macros 2 | -------------------------------------------------------------------------------- /crates/bvh-region/src/query.rs: -------------------------------------------------------------------------------- 1 | mod closest; 2 | mod range; 3 | mod ray; 4 | -------------------------------------------------------------------------------- /libvoidstar.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperion-mc/hyperion/HEAD/libvoidstar.so -------------------------------------------------------------------------------- /tools/rust-mc-bot/src/states/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod login; 2 | pub mod play; 3 | pub mod status; 4 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "tokio_unstable", "-Ctarget-cpu=native"] 3 | 4 | [env] 5 | RUST_LOG="debug" -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-02-22" 3 | components = ["rustfmt", "clippy"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /crates/hyperion/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | mod bits; 2 | mod buf; 3 | mod db; 4 | 5 | pub use bits::*; 6 | pub use buf::*; 7 | pub use db::*; 8 | -------------------------------------------------------------------------------- /crates/hyperion/src/ingress/data/hyperion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperion-mc/hyperion/HEAD/crates/hyperion/src/ingress/data/hyperion.png -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/data/registries.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperion-mc/hyperion/HEAD/crates/hyperion/src/simulation/data/registries.nbt -------------------------------------------------------------------------------- /crates/bvh-region/README.md: -------------------------------------------------------------------------------- 1 | # bvh 2 | 3 | This implements a Bounding Volume Hierarchy (BVH) that is used to accelerate collision detection and other spatial queries. -------------------------------------------------------------------------------- /crates/geometry/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // for tango-bench 3 | println!("cargo:rustc-link-arg-benches=-rdynamic"); 4 | println!("cargo:rerun-if-changed=build.rs"); 5 | } 6 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # https://doc.rust-lang.org/nightly/clippy/lint_configuration.html 2 | 3 | cognitive-complexity-threshold = 200 4 | excessive-nesting-threshold = 7 5 | too-many-lines-threshold = 200 6 | -------------------------------------------------------------------------------- /events/bedwars/src/plugin.rs: -------------------------------------------------------------------------------- 1 | pub mod attack; 2 | pub mod block; 3 | pub mod bow; 4 | pub mod chat; 5 | pub mod damage; 6 | pub mod regeneration; 7 | pub mod spawn; 8 | pub mod stats; 9 | pub mod vanish; 10 | -------------------------------------------------------------------------------- /crates/hyperion/src/net/agnostic.rs: -------------------------------------------------------------------------------- 1 | //! Agnostic networking primitives. Translates to correct protocol version. 2 | 3 | mod chat; 4 | pub use chat::{Chat, chat}; 5 | 6 | mod sound; 7 | pub use sound::{Sound, SoundBuilder, sound}; 8 | -------------------------------------------------------------------------------- /.github/chart/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | dependencies = [ 3 | "matplotlib>=3.9.2", 4 | "numpy>=2.1.2" 5 | ] 6 | description = "Add your description here" 7 | name = "chart" 8 | readme = "README.md" 9 | requires-python = ">=3.12" 10 | version = "0.1.0" 11 | -------------------------------------------------------------------------------- /crates/hyperion/run/config.toml: -------------------------------------------------------------------------------- 1 | border_diameter = 100.0 2 | max_players = 10000 3 | view_distance = 32 4 | simulation_distance = 10 5 | server_desc = "Hyperion Test Server" 6 | 7 | [spawn] 8 | kind = "Chebyshev" 9 | radius = 1000 10 | x = 0 11 | y = 64 12 | z = 0 13 | -------------------------------------------------------------------------------- /crates/hyperion-nerd-font/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | 3 | [lints] 4 | workspace = true 5 | 6 | [package] 7 | authors = ["Andrew Gazelka "] 8 | edition.workspace = true 9 | name = "hyperion-nerd-font" 10 | publish = false 11 | readme = "README.md" 12 | version.workspace = true 13 | -------------------------------------------------------------------------------- /crates/hyperion-scheduled/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | 3 | [lints] 4 | workspace = true 5 | 6 | [package] 7 | authors = ["Andrew Gazelka "] 8 | edition.workspace = true 9 | name = "hyperion-scheduled" 10 | publish = false 11 | readme = "README.md" 12 | version.workspace = true 13 | -------------------------------------------------------------------------------- /crates/hyperion-minecraft-proto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | 3 | [lints] 4 | workspace = true 5 | 6 | [package] 7 | authors = ["Andrew Gazelka "] 8 | edition.workspace = true 9 | name = "hyperion-minecraft-proto" 10 | publish = false 11 | readme = "README.md" 12 | version.workspace = true 13 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Debian", 3 | "forwardPorts": [ 4 | 25565 5 | ], 6 | "features": { 7 | "ghcr.io/devcontainers/features/rust:1": {} 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "extensions": [ 12 | "rust-lang.rust-analyzer" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/hyperion-proto/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::module_inception, 3 | clippy::module_name_repetitions, 4 | clippy::derive_partial_eq_without_eq, 5 | hidden_glob_reexports 6 | )] 7 | 8 | mod proxy_to_server; 9 | mod server_to_proxy; 10 | mod shared; 11 | 12 | pub use proxy_to_server::*; 13 | pub use server_to_proxy::*; 14 | pub use shared::*; 15 | -------------------------------------------------------------------------------- /crates/simd-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simd-utils" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Andrew Gazelka "] 6 | readme = "README.md" 7 | publish = false 8 | 9 | [dependencies] 10 | 11 | [dev-dependencies] 12 | proptest = "1.5.0" 13 | aligned-vec = "0.6.4" 14 | 15 | [lints] 16 | workspace = true 17 | -------------------------------------------------------------------------------- /crates/hyperion-nerd-font/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub const NERD_ROCKET: char = '\u{F14DE}'; 2 | pub const FAIL_ROCKET: char = '\u{ea87}'; 3 | 4 | #[cfg(test)] 5 | mod tests { 6 | use super::*; 7 | 8 | #[test] 9 | #[expect(clippy::print_stdout)] 10 | fn print_chars() { 11 | println!("Rocket: {NERD_ROCKET}"); 12 | println!("Fail: {FAIL_ROCKET}"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/hyperion-clap/tests/gamemode.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(name = "gamemode")] 5 | #[command(about = "Change the gamemode of a player")] 6 | struct Gamemode { 7 | /// The gamemode to set 8 | #[arg(value_enum)] 9 | mode: hyperion_clap::GameMode, 10 | 11 | /// The player to change the gamemode of 12 | player: Option, 13 | } 14 | -------------------------------------------------------------------------------- /crates/hyperion-genmap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperion-genmap" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Andrew Gazelka "] 6 | readme = "README.md" 7 | publish = false 8 | 9 | [dependencies] 10 | bevy = { workspace = true } 11 | hyperion = { workspace = true } 12 | hyperion-utils = { workspace = true } 13 | 14 | [lints] 15 | workspace = true 16 | -------------------------------------------------------------------------------- /crates/hyperion-clap-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | quote.workspace = true 3 | syn.workspace = true 4 | 5 | [lib] 6 | proc-macro = true 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [package] 12 | authors = ["Andrew Gazelka "] 13 | edition.workspace = true 14 | name = "hyperion-clap-macros" 15 | publish = false 16 | readme = "README.md" 17 | version.workspace = true 18 | -------------------------------------------------------------------------------- /crates/hyperion-palette/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | anyhow.workspace = true 3 | roaring.workspace = true 4 | valence_protocol.workspace = true 5 | 6 | [lints] 7 | workspace = true 8 | 9 | [package] 10 | authors = ["Andrew Gazelka "] 11 | edition.workspace = true 12 | name = "hyperion-palette" 13 | publish = false 14 | readme = "README.md" 15 | version.workspace = true 16 | -------------------------------------------------------------------------------- /tools/rust-mc-bot/README.md: -------------------------------------------------------------------------------- 1 | ## Disclaimer 2 | 3 | This should **ONLY** be used test your own server. We do not endorse the use of this for any other purposes than testing your own infrastructure. 4 | 5 | Please be aware that attempting to execute this with an external server as a target can be seen as **illegal** as it simulates a layer 7 DoS (denial-of-service) attack, which is against the law in most countries. -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/item.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use valence_protocol::ItemStack; 3 | 4 | use super::Metadata; 5 | use crate::define_and_register_components; 6 | 7 | // Example usage: 8 | define_and_register_components! { 9 | 8, Item -> ItemStack 10 | } 11 | 12 | impl Default for Item { 13 | fn default() -> Self { 14 | Self::new(ItemStack::EMPTY) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/hyperion-packet-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | proc-macro2.workspace = true 3 | quote.workspace = true 4 | 5 | [lib] 6 | proc-macro = true 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [package] 12 | authors = ["Andrew Gazelka "] 13 | edition.workspace = true 14 | name = "hyperion-packet-macros" 15 | publish = false 16 | readme = "README.md" 17 | version.workspace = true 18 | -------------------------------------------------------------------------------- /crates/hyperion-text/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | serde.workspace = true 3 | serde_json.workspace = true 4 | thiserror.workspace = true 5 | uuid.workspace = true 6 | valence_protocol.workspace = true 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [package] 12 | authors = ["Andrew Gazelka "] 13 | edition.workspace = true 14 | name = "hyperion-text" 15 | publish = false 16 | readme = "README.md" 17 | version.workspace = true 18 | -------------------------------------------------------------------------------- /crates/hyperion-proto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [build-dependencies] 2 | 3 | [dependencies] 4 | rkyv = {workspace = true} 5 | glam = {workspace = true} 6 | 7 | [lints] 8 | workspace = true 9 | 10 | [package] 11 | authors = ["Andrew Gazelka "] 12 | edition.workspace = true 13 | name = "hyperion-proto" 14 | publish = false 15 | readme = "README.md" 16 | version.workspace = true 17 | 18 | [package.metadata.cargo-machete] 19 | ignored = ["prost"] 20 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/block_display.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use valence_generated::block::BlockState; 3 | 4 | use super::Metadata; 5 | use crate::define_and_register_components; 6 | 7 | // Example usage: 8 | define_and_register_components! { 9 | 22, DisplayedBlockState -> BlockState, 10 | } 11 | 12 | impl Default for DisplayedBlockState { 13 | fn default() -> Self { 14 | Self::new(BlockState::EMERALD_BLOCK) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/hyperion-stats/Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bench]] 2 | harness = false 3 | name = "parallel_stats" 4 | 5 | [dependencies] 6 | 7 | [dev-dependencies] 8 | rand.workspace = true 9 | approx.workspace = true 10 | divan.workspace = true 11 | 12 | [lints] 13 | workspace = true 14 | 15 | [package] 16 | authors = ["Andrew Gazelka "] 17 | edition.workspace = true 18 | name = "hyperion-stats" 19 | publish = false 20 | readme = "README.md" 21 | version.workspace = true 22 | -------------------------------------------------------------------------------- /crates/hyperion-text/src/font.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The font of the text. 4 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] 5 | pub enum Font { 6 | /// The default font. 7 | #[serde(rename = "minecraft:default")] 8 | Default, 9 | /// Unicode font. 10 | #[serde(rename = "minecraft:uniform")] 11 | Uniform, 12 | /// Enchanting table font. 13 | #[serde(rename = "minecraft:alt")] 14 | Alt, 15 | } 16 | -------------------------------------------------------------------------------- /crates/hyperion-crafting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | anyhow = { workspace = true } 3 | derive-build = { workspace = true } 4 | slotmap = { workspace = true } 5 | valence_protocol = { workspace = true } 6 | bevy = {workspace = true} 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [package] 12 | authors = ["Andrew Gazelka "] 13 | edition.workspace = true 14 | name = "hyperion-crafting" 15 | publish = false 16 | readme = "README.md" 17 | version.workspace = true 18 | -------------------------------------------------------------------------------- /crates/hyperion-command/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(iter_intersperse)] 2 | 3 | use bevy::prelude::*; 4 | 5 | mod component; 6 | mod system; 7 | 8 | pub use component::{CommandHandler, CommandRegistry, ExecutableCommand}; 9 | 10 | pub struct CommandPlugin; 11 | 12 | impl Plugin for CommandPlugin { 13 | fn build(&self, app: &mut App) { 14 | app.add_plugins(( 15 | component::CommandComponentPlugin, 16 | system::CommandSystemPlugin, 17 | )); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tools/antithesis-bot/src/main.rs: -------------------------------------------------------------------------------- 1 | use antithesis_bot::LaunchArguments; 2 | use tracing::trace; 3 | 4 | #[tokio::main] 5 | async fn main() -> eyre::Result<()> { 6 | tracing_subscriber::fmt::init(); 7 | if let Err(e) = dotenvy::dotenv() { 8 | trace!("Failed to load .env file: {}", e); 9 | } 10 | 11 | // Deserialize environment variables into the struct 12 | let args: LaunchArguments = envy::from_env()?; 13 | 14 | antithesis_bot::start(args).await?; 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /crates/hyperion-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperion-gui" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | anyhow = { workspace = true } 8 | bevy = { workspace = true } 9 | hyperion = { workspace = true } 10 | hyperion-inventory = { workspace = true } 11 | hyperion-utils = { workspace = true } 12 | serde = { version = "1.0", features = ["derive"] } 13 | valence_protocol = { workspace = true } 14 | tracing = { workspace = true } 15 | 16 | [lints] 17 | workspace = true 18 | -------------------------------------------------------------------------------- /crates/hyperion-proxy-module/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | bevy = { workspace = true } 3 | hyperion-proxy = { workspace = true } 4 | hyperion = { workspace = true } 5 | tracing = { workspace = true } 6 | tokio = { workspace = true , features = ["full"]} 7 | 8 | [lints] 9 | workspace = true 10 | 11 | [package] 12 | authors = ["Andrew Gazelka "] 13 | edition.workspace = true 14 | name = "hyperion-proxy-module" 15 | publish = false 16 | readme = "README.md" 17 | version.workspace = true 18 | -------------------------------------------------------------------------------- /tools/rust-mc-bot/src/states/status.rs: -------------------------------------------------------------------------------- 1 | use crate::{Bot, Compression, packet_utils::Buf}; 2 | 3 | pub fn process_status_response(buffer: &mut Buf, _bot: &mut Bot, _compression: &mut Compression) { 4 | let server_response = buffer.read_sized_string(); 5 | tracing::info!("got response {server_response}"); 6 | } 7 | 8 | pub fn process_pong(buffer: &mut Buf, _bot: &mut Bot, _compression: &mut Compression) { 9 | let payload = buffer.read_sized_string(); 10 | tracing::info!("got pong {payload}"); 11 | } 12 | -------------------------------------------------------------------------------- /events/bedwars/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Get target architecture (e.g., x86_64, aarch64, etc.) 3 | let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| "unknown".to_string()); 4 | 5 | // Get profile (debug/release) 6 | let profile = if cfg!(debug_assertions) { 7 | "debug" 8 | } else { 9 | "release" 10 | }; 11 | 12 | // Combine them into RUN_MODE 13 | let run_mode = format!("{arch}-{profile}"); 14 | println!("cargo:rustc-env=RUN_MODE={run_mode}"); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File Types 2 | *.webm 3 | *.jar 4 | *.svg 5 | *.zip 6 | 7 | /result 8 | 9 | # IDE Configs 10 | /.vscode 11 | .idea 12 | 13 | # Temp or Autogenerated 14 | /run 15 | /target 16 | /profiling 17 | node_modules/ 18 | db/ 19 | docs/.vitepress/cache 20 | crates/run/config.toml 21 | .env 22 | .gradle 23 | .DS_Store 24 | perf.data 25 | 26 | # Misc 27 | /extractor/build 28 | /extractor/out 29 | vendor/ 30 | /extractor/classes 31 | /extractor/run 32 | /extractor/bin 33 | .trigger-release 34 | .trigger-debug 35 | .trigger 36 | valence/ 37 | -------------------------------------------------------------------------------- /crates/hyperion-command/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperion-command" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Andrew Gazelka "] 6 | readme = "README.md" 7 | publish = false 8 | 9 | [dependencies] 10 | bevy = { workspace = true } 11 | derive_more = { workspace = true } 12 | hyperion = { workspace = true } 13 | hyperion-utils = { workspace = true } 14 | indexmap = { workspace = true } 15 | tracing = { workspace = true } 16 | valence_bytes = { workspace = true } 17 | 18 | [lints] 19 | workspace = true 20 | -------------------------------------------------------------------------------- /docs/bedwars/introduction.md: -------------------------------------------------------------------------------- 1 | # 10,000 Player PvP Event 2 | 3 | Our pilot event is a 10,000 player PvP battle to break the Guinness World Record for the largest PvP battle ever. 4 | 5 | We're partnering with [TheMisterEpic](https://www.youtube.com/channel/UCJiFgnnYpwlnadzTzhMnX_Q) to run an initial 6 | proof-of-concept event with around 2k players. Following its success, we'll host the full-scale 10,000 player PvP battle 7 | alongside numerous YouTubers and streamers. 8 | 9 | Our event will be a massive Bed Wars game consisting of teams with hundreds of players each. 10 | -------------------------------------------------------------------------------- /crates/packet-channel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bench]] 2 | harness = false 3 | name = "packet_channel" 4 | 5 | [dependencies] 6 | arc-swap = { workspace = true } 7 | bevy = { workspace = true } 8 | more-asserts = { workspace = true } 9 | valence_protocol = { workspace = true } 10 | 11 | [dev-dependencies] 12 | divan = {workspace = true} 13 | proptest = {workspace = true} 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [package] 19 | authors = ["TestingPlant"] 20 | edition.workspace = true 21 | name = "packet-channel" 22 | publish = false 23 | readme = "README.md" 24 | version.workspace = true 25 | -------------------------------------------------------------------------------- /crates/hyperion-item/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperion-item" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Andrew Gazelka "] 6 | readme = "README.md" 7 | publish = false 8 | 9 | [dependencies] 10 | bevy = { workspace = true } 11 | bytemuck = "1.23.0" 12 | valence_protocol = { workspace = true } 13 | hyperion = { workspace = true } 14 | hyperion-inventory = { workspace = true } 15 | hyperion-utils = { workspace = true } 16 | derive_more = { workspace = true } 17 | tracing = { workspace = true } 18 | 19 | [lints] 20 | workspace = true 21 | -------------------------------------------------------------------------------- /crates/hyperion-inventory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | bevy = {workspace = true} 3 | hyperion-crafting = {workspace = true} 4 | roaring = {workspace = true} 5 | snafu = {workspace = true} 6 | valence_protocol = {workspace = true} 7 | valence_generated = {workspace = true} 8 | derive_more = {workspace = true} 9 | tracing = {workspace = true} 10 | 11 | [lints] 12 | workspace = true 13 | 14 | [package] 15 | authors = ["Andrew Gazelka "] 16 | edition.workspace = true 17 | name = "hyperion-inventory" 18 | publish = false 19 | readme = "README.md" 20 | version.workspace = true 21 | -------------------------------------------------------------------------------- /crates/hyperion-permission/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | anyhow = {workspace = true} 3 | clap = {workspace = true} 4 | bevy = {workspace = true} 5 | heed = {workspace = true} 6 | hyperion = {workspace = true} 7 | num-derive = {workspace = true} 8 | num-traits = {workspace = true} 9 | tracing = {workspace = true} 10 | uuid = {workspace = true} 11 | 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [package] 17 | authors = ["Andrew Gazelka "] 18 | edition.workspace = true 19 | name = "hyperion-permission" 20 | publish = false 21 | readme = "README.md" 22 | version.workspace = true 23 | -------------------------------------------------------------------------------- /.github/workflows/conventional.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeller 2 | 3 | on: 4 | pull_request_target: 5 | branches: [main] 6 | types: [opened, reopened, synchronize, edited, labeled, unlabeled] 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | 12 | jobs: 13 | validate_pr_title: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: PR Conventional Commit Validation 17 | uses: ytanikin/pr-conventional-commits@1.4.0 18 | with: 19 | task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' 20 | add_label: 'true' 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | open-pull-requests-limit: 15 11 | groups: 12 | all-cargo-dependencies: 13 | patterns: 14 | - "*" 15 | - package-ecosystem: "gradle" 16 | directory: "/extractor/" 17 | schedule: 18 | interval: "weekly" 19 | open-pull-requests-limit: 15 20 | -------------------------------------------------------------------------------- /tools/rust-mc-bot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-mc-bot" 3 | version.workspace = true 4 | authors = [ 5 | "Eoghanmc22 ", 6 | "Andrew Gazelka ", 7 | ] 8 | edition.workspace = true 9 | 10 | [dependencies] 11 | anyhow.workspace = true 12 | libdeflater.workspace = true 13 | mio.workspace = true 14 | num_cpus.workspace = true 15 | rand.workspace = true 16 | tracing = { workspace = true } 17 | tracing-subscriber = { workspace = true } 18 | serde = { version = "1.0", features = ["derive"] } 19 | envy = "0.4" 20 | dotenvy = "0.15" 21 | 22 | [lints] 23 | workspace = true 24 | -------------------------------------------------------------------------------- /tools/antithesis-bot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "antithesis-bot" 3 | version.workspace = true 4 | authors = ["Andrew Gazelka "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | antithesis_sdk.workspace = true 9 | bytes = "1.9.0" 10 | dotenvy.workspace = true 11 | envy.workspace = true 12 | eyre.workspace = true 13 | regex = { workspace = true } 14 | serde = { workspace = true, features = ["derive"] } 15 | tokio = { workspace = true, features = ["full"] } 16 | tracing-subscriber.workspace = true 17 | tracing.workspace = true 18 | valence_protocol.workspace = true 19 | 20 | [lints] 21 | workspace = true -------------------------------------------------------------------------------- /crates/geometry/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geometry" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Andrew Gazelka "] 6 | readme = "README.md" 7 | publish = false 8 | 9 | [dependencies] 10 | glam = { workspace = true, features = ["serde"] } 11 | serde = { workspace = true, features = ["derive"] } 12 | ordered-float = { workspace = true } 13 | 14 | [dev-dependencies] 15 | approx = { workspace = true } 16 | itertools = { workspace = true } 17 | rand = { workspace = true } 18 | tango-bench = { workspace = true } 19 | 20 | [lints] 21 | workspace = true 22 | 23 | [[bench]] 24 | name = "general" 25 | harness = false 26 | -------------------------------------------------------------------------------- /crates/hyperion/src/egress/metadata.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for working with the Entity Metadata packet. 2 | 3 | use valence_protocol::{RawBytes, VarInt, packets::play}; 4 | 5 | /// Packet to show all parts of the skin. 6 | #[must_use] 7 | pub fn show_all(id: i32) -> play::EntityTrackerUpdateS2c<'static> { 8 | // https://wiki.vg/Entity_metadata#Entity_Metadata_Format 9 | // https://wiki.vg/Entity_metadata#Player 10 | // 17 = Metadata, type = byte 11 | static BYTES: &[u8] = &[17, 0, 0xff, 0xff]; 12 | 13 | let entity_id = VarInt(id); 14 | 15 | play::EntityTrackerUpdateS2c { 16 | entity_id, 17 | tracked_values: RawBytes(BYTES.into()), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | combine_control_expr = true 2 | comment_width = 100 # https://lkml.org/lkml/2020/5/29/1038 3 | condense_wildcard_suffixes = true 4 | control_brace_style = "AlwaysSameLine" 5 | edition = "2024" 6 | format_code_in_doc_comments = true 7 | format_macro_bodies = true 8 | format_macro_matchers = true 9 | format_strings = true 10 | group_imports = "StdExternalCrate" 11 | imports_granularity = "Crate" 12 | merge_derives = false 13 | newline_style = "Unix" 14 | normalize_comments = true 15 | normalize_doc_attributes = true 16 | overflow_delimited_expr = true 17 | reorder_impl_items = true 18 | reorder_imports = true 19 | unstable_features = true 20 | 21 | # wrap_comments = true 22 | -------------------------------------------------------------------------------- /crates/hyperion-clap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperion-clap" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Andrew Gazelka "] 6 | readme = "README.md" 7 | publish = false 8 | 9 | [dependencies] 10 | clap = { workspace = true } 11 | bevy = { workspace = true } 12 | hyperion = { workspace = true } 13 | hyperion-clap-macros = { workspace = true } 14 | hyperion-command = { workspace = true } 15 | hyperion-permission = { workspace = true } 16 | hyperion-utils = { workspace = true } 17 | tracing = { workspace = true } 18 | valence_protocol = { workspace = true } 19 | valence_bytes = { workspace = true } 20 | 21 | [lints] 22 | workspace = true 23 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Rust build artifacts 2 | **/target/ 3 | 4 | # Git and development files 5 | .git/ 6 | .github/ 7 | .gitignore 8 | .DS_Store 9 | .idea/ 10 | .vscode/ 11 | .envrc 12 | .pre-commit-config.yaml 13 | .trigger* 14 | 15 | # Docker files 16 | Dockerfile 17 | docker-compose.yml 18 | 19 | # Configuration and environment 20 | .env 21 | deny.toml 22 | clippy.toml 23 | rustfmt.toml 24 | 25 | # Documentation and non-essential files 26 | docs/ 27 | CONTRIBUTING.md 28 | LICENSE 29 | README.md 30 | CNAME 31 | 32 | # Node.js 33 | node_modules/ 34 | pnpm-lock.yaml 35 | 36 | # Nix 37 | flake.nix 38 | flake.lock 39 | result/ 40 | 41 | # Project-specific 42 | run/ 43 | .devcontainer.json 44 | libvoidstar.so -------------------------------------------------------------------------------- /crates/hyperion-minecraft-proto/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Write}; 2 | 3 | pub enum EncodeError { 4 | Encode(E), 5 | Io(std::io::Error), 6 | } 7 | 8 | pub enum DecodeError { 9 | Decode(E), 10 | Io(std::io::Error), 11 | } 12 | 13 | impl From for EncodeError { 14 | fn from(e: std::io::Error) -> Self { 15 | Self::Io(e) 16 | } 17 | } 18 | 19 | pub trait Encode { 20 | type Error; 21 | 22 | fn encode(&self, w: impl Write) -> Result<(), EncodeError>; 23 | } 24 | 25 | pub trait Decode<'a> { 26 | type Error; 27 | 28 | fn decode(r: Cursor<&'a [u8]>) -> Result> 29 | where 30 | Self: Sized; 31 | } 32 | -------------------------------------------------------------------------------- /events/bedwars/src/command/xp.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::simulation::Xp; 4 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 5 | 6 | #[derive(Parser, CommandPermission, Debug)] 7 | #[command(name = "xp")] 8 | #[command_permission(group = "Admin")] 9 | pub struct XpCommand { 10 | amount: u16, 11 | } 12 | 13 | impl MinecraftCommand for XpCommand { 14 | type State = SystemState>; 15 | 16 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 17 | let mut commands = state.get(world); 18 | commands.entity(caller).insert(Xp { 19 | amount: self.amount, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs:dev": "vitepress dev docs", 4 | "docs:build": "vitepress build docs", 5 | "docs:preview": "vitepress preview docs" 6 | }, 7 | "devDependencies": { 8 | "@braintree/sanitize-url": "^7.1.1", 9 | "@nolebase/vitepress-plugin-inline-link-preview": "^2.14.0", 10 | "cytoscape": "^3.31.0", 11 | "cytoscape-cose-bilkent": "^4.1.0", 12 | "dayjs": "^1.11.13", 13 | "debug": "^4.4.0", 14 | "markdown-it-footnote": "^4.0.0", 15 | "markdown-it-mathjax3": "^4.3.2", 16 | "medium-zoom": "^1.1.0", 17 | "mermaid": "^11.4.1", 18 | "vitepress": "^1.6.3", 19 | "vitepress-plugin-mermaid": "^2.0.17" 20 | }, 21 | "dependencies": { 22 | "@fontsource/fira-code": "^5.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/hyperion-proto/src/shared.rs: -------------------------------------------------------------------------------- 1 | use glam::I16Vec2; 2 | use rkyv::{Archive, Deserialize, Serialize}; 3 | 4 | #[derive(Archive, Deserialize, Serialize, Clone, Copy, PartialEq, Debug)] 5 | #[rkyv(derive(Debug))] 6 | pub struct ChunkPosition { 7 | pub x: i16, 8 | pub z: i16, 9 | } 10 | 11 | impl ChunkPosition { 12 | #[must_use] 13 | pub const fn new(x: i16, z: i16) -> Self { 14 | Self { x, z } 15 | } 16 | } 17 | 18 | impl From for ChunkPosition { 19 | fn from(value: I16Vec2) -> Self { 20 | Self { 21 | x: value.x, 22 | z: value.y, 23 | } 24 | } 25 | } 26 | 27 | impl From for I16Vec2 { 28 | fn from(value: ChunkPosition) -> Self { 29 | Self::new(value.x, value.z) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // .vitepress/theme/index.js 2 | import DefaultTheme from 'vitepress/theme' 3 | 4 | import GithubSnippet from './components/GithubSnippet.vue' 5 | import './custom.css' 6 | 7 | // import {NolebaseInlineLinkPreviewPlugin} from '@nolebase/vitepress-plugin-inline-link-preview/client'; 8 | import '@fontsource/fira-code' 9 | 10 | import { onMounted } from 'vue'; 11 | 12 | // export default DefaultTheme 13 | 14 | import mediumZoom from 'medium-zoom'; 15 | 16 | export default { 17 | ...DefaultTheme, 18 | enhanceApp({app}) { 19 | app.component('GithubSnippet', GithubSnippet) 20 | }, 21 | setup() { 22 | onMounted(() => { 23 | mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' }); 24 | }); 25 | }, 26 | }; -------------------------------------------------------------------------------- /crates/bvh-region/src/node.rs: -------------------------------------------------------------------------------- 1 | use geometry::aabb::Aabb; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq)] 4 | pub struct BvhNode { 5 | pub aabb: Aabb, // f32 * 6 = 24 bytes 6 | 7 | // if positive then it is an internal node; if negative then it is a leaf node 8 | pub left: i32, 9 | pub right: i32, 10 | } 11 | 12 | impl BvhNode { 13 | #[allow(dead_code)] 14 | const EMPTY_LEAF: Self = Self { 15 | aabb: Aabb::NULL, 16 | left: -1, 17 | right: 0, 18 | }; 19 | 20 | pub const fn create_leaf(aabb: Aabb, idx_left: usize, len: usize) -> Self { 21 | let left = idx_left as i32; 22 | let right = len as i32; 23 | 24 | let left = -left; 25 | 26 | let left = left - 1; 27 | 28 | Self { aabb, left, right } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/hyperion-genmap/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use hyperion::{runtime::AsyncRuntime, simulation::blocks::Blocks}; 3 | 4 | pub struct GenMapPlugin; 5 | 6 | impl Plugin for GenMapPlugin { 7 | fn build(&self, app: &mut App) { 8 | const URL: &str = "https://github.com/andrewgazelka/maps/raw/main/GenMap.tar.gz"; 9 | 10 | let runtime = app 11 | .world() 12 | .get_resource::() 13 | .expect("AsyncRuntime resource must exist"); 14 | let f = hyperion_utils::cached_save(app.world(), URL); 15 | 16 | let save = runtime.block_on(f).unwrap_or_else(|e| { 17 | panic!("failed to download map {URL}: {e}"); 18 | }); 19 | 20 | app.insert_resource(Blocks::new(runtime, &save).unwrap()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/hyperion/src/common/runtime.rs: -------------------------------------------------------------------------------- 1 | //! See [`AsyncRuntime`]. 2 | 3 | use std::sync::Arc; 4 | 5 | use bevy::prelude::*; 6 | use derive_more::{Deref, DerefMut}; 7 | 8 | /// Wrapper around [`tokio::runtime::Runtime`] 9 | #[derive(Resource, Deref, DerefMut, Clone)] 10 | pub struct AsyncRuntime { 11 | runtime: Arc, 12 | } 13 | 14 | impl AsyncRuntime { 15 | pub(crate) fn new() -> Self { 16 | Self { 17 | runtime: Arc::new( 18 | tokio::runtime::Builder::new_multi_thread() 19 | // .worker_threads(2) 20 | .enable_all() 21 | // .thread_stack_size(1024 * 1024) // 1 MiB 22 | .build() 23 | .unwrap(), 24 | ), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/hyperion-text/src/scoreboard.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Scoreboard value. 6 | #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] 7 | pub struct ScoreboardValueContent<'a> { 8 | /// The name of the score holder whose score should be displayed. This 9 | /// can be a [`selector`] or an explicit name. 10 | /// 11 | /// [`selector`]: https://minecraft.wiki/w/Target_selectors 12 | pub name: Cow<'a, str>, 13 | /// The internal name of the objective to display the player's score in. 14 | pub objective: Cow<'a, str>, 15 | /// If present, this value is displayed regardless of what the score 16 | /// would have been. 17 | #[serde(default, skip_serializing_if = "Option::is_none")] 18 | pub value: Option>, 19 | } 20 | -------------------------------------------------------------------------------- /crates/hyperion-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | anyhow = { workspace = true } 3 | bevy = { workspace = true } 4 | bytemuck = { workspace = true } 5 | directories = { workspace = true } 6 | flate2 = { workspace = true } 7 | futures-util = { workspace = true } 8 | hex = { workspace = true } 9 | hyperion-packet-macros = { workspace = true } 10 | reqwest = { workspace = true } 11 | sha2 = { workspace = true } 12 | tar = { workspace = true } 13 | tokio = { workspace = true } 14 | tokio-util = { workspace = true } 15 | tracing = { workspace = true } 16 | valence_protocol = { workspace = true } 17 | 18 | [lints] 19 | workspace = true 20 | 21 | [package] 22 | authors = ["Andrew Gazelka "] 23 | edition.workspace = true 24 | name = "hyperion-utils" 25 | publish = false 26 | readme = "README.md" 27 | version.workspace = true 28 | -------------------------------------------------------------------------------- /crates/hyperion/src/net/agnostic/chat.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use valence_protocol::packets::play; 4 | use valence_text::IntoText; 5 | 6 | use crate::PacketBundle; 7 | 8 | pub struct Chat { 9 | raw: play::GameMessageS2c<'static>, 10 | } 11 | 12 | pub fn chat(chat: impl Into) -> Chat { 13 | let chat = chat.into(); 14 | Chat { 15 | raw: play::GameMessageS2c { 16 | chat: chat.into_cow_text(), 17 | overlay: false, 18 | }, 19 | } 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! chat { 24 | ($($arg:tt)*) => { 25 | $crate::net::agnostic::chat(format!($($arg)*)) 26 | }; 27 | } 28 | 29 | impl PacketBundle for &Chat { 30 | fn encode_including_ids(self, mut w: impl Write) -> anyhow::Result<()> { 31 | self.raw.encode_including_ids(&mut w) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /events/bedwars/src/command.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use hyperion_clap::MinecraftCommand; 3 | 4 | use crate::command::{ 5 | bow::BowCommand, chest::ChestCommand, fly::FlyCommand, gui::GuiCommand, 6 | raycast::RaycastCommand, shoot::ShootCommand, speed::SpeedCommand, vanish::VanishCommand, 7 | xp::XpCommand, 8 | }; 9 | 10 | mod bow; 11 | mod chest; 12 | mod fly; 13 | mod gui; 14 | mod raycast; 15 | mod shoot; 16 | mod speed; 17 | mod vanish; 18 | mod xp; 19 | 20 | pub fn register(world: &mut World) { 21 | BowCommand::register(world); 22 | FlyCommand::register(world); 23 | GuiCommand::register(world); 24 | RaycastCommand::register(world); 25 | ShootCommand::register(world); 26 | SpeedCommand::register(world); 27 | VanishCommand::register(world); 28 | XpCommand::register(world); 29 | ChestCommand::register(world); 30 | } 31 | -------------------------------------------------------------------------------- /crates/hyperion-stats/benches/parallel_stats.rs: -------------------------------------------------------------------------------- 1 | use divan::{Bencher, black_box}; 2 | use hyperion_stats::ParallelStats; 3 | use rand::Rng; 4 | 5 | fn main() { 6 | divan::main(); 7 | } 8 | 9 | fn generate_test_data(width: usize, updates: usize) -> Vec> { 10 | let mut rng = rand::rng(); 11 | (0..updates) 12 | .map(|_| (0..width).map(|_| rng.random::()).collect()) 13 | .collect() 14 | } 15 | 16 | #[divan::bench(args = [ 17 | 4, 8, 16, 32, 64 18 | ])] 19 | fn bench_parallel_stats(bencher: Bencher<'_, '_>, width: usize) { 20 | let updates = 1000; 21 | let test_data = generate_test_data(width, updates); 22 | 23 | bencher.bench(move || { 24 | let mut stats = ParallelStats::new(width); 25 | for values in &test_data { 26 | stats.update(black_box(values)); 27 | } 28 | stats 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Hyperion" 7 | text: "The most advanced Minecraft game engine built in Rust" 8 | tagline: 10,000 players in one world at 20 TPS 9 | actions: 10 | - theme: brand 11 | text: Architecture 12 | link: /architecture/introduction 13 | - theme: alt 14 | text: 10,000 Player PvP 15 | link: /bedwars/introduction 16 | 17 | features: 18 | - title: Run massive events with confidence 19 | details: Built in Rust, you can be highly confident your event will not crash from memory leaks or SEGFAULTS. 20 | - title: Vertical and horizontal scalability 21 | details: In our testing I/O is the main bottleneck in massive events. As such, we made it so I/O logic can be offloaded horizontally. The actual core game server is scaled vertically. 22 | --- 23 | 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: andrewgazelka 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | /* .vitepress/theme/custom.css */ 2 | .green-tps { 3 | color: #4ade80; /* You can change this to any green color you prefer */ 4 | } 5 | 6 | :root { 7 | --vp-font-family-mono: 'Fira Code', monospace; 8 | --vp-code-line-height: 1.35; 9 | } 10 | 11 | .medium-zoom-overlay { 12 | z-index: 999; 13 | } 14 | 15 | .medium-zoom-image { 16 | z-index: 1000; 17 | } 18 | 19 | .vp-doc ul { 20 | list-style: disc; 21 | } 22 | 23 | .vp-doc ul ul { 24 | list-style: circle; 25 | } 26 | 27 | .vp-doc ul ul ul { 28 | list-style: square; 29 | } 30 | 31 | .vp-doc ul ul ul ul { 32 | list-style: square; 33 | /* Create open square effect by adding a border and transparent background */ 34 | list-style: none; 35 | } 36 | 37 | .vp-doc ul ul ul ul li::before { 38 | content: "□"; 39 | display: inline-block; 40 | width: 1em; 41 | margin-left: -1em; 42 | } 43 | -------------------------------------------------------------------------------- /tools/packet-inspector/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | #![expect( 3 | clippy::significant_drop_tightening, 4 | reason = "todo: we should double check no significant drop tightening" 5 | )] 6 | 7 | use egui::ViewportBuilder; 8 | 9 | mod tri_checkbox; 10 | 11 | mod app; 12 | mod shared_state; 13 | 14 | #[tokio::main] 15 | async fn main() -> Result<(), Box> { 16 | let native_options = eframe::NativeOptions { 17 | viewport: ViewportBuilder::default().with_inner_size(egui::Vec2::new(1024.0, 768.0)), 18 | ..Default::default() 19 | }; 20 | 21 | eframe::run_native( 22 | "Hyperion Packet Inspector", 23 | native_options, 24 | Box::new(move |cc| { 25 | let gui_app = app::GuiApp::new(cc); 26 | 27 | Ok(Box::new(gui_app)) 28 | }), 29 | )?; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/blocks/shared.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, path::Path}; 2 | 3 | use anyhow::Context; 4 | use tokio::runtime::Runtime; 5 | use valence_protocol::Ident; 6 | use valence_registry::{BiomeRegistry, biome::BiomeId}; 7 | 8 | use super::manager::RegionManager; 9 | 10 | /// Inner state of the [`MinecraftWorld`] component. 11 | pub struct WorldShared { 12 | pub regions: RegionManager, 13 | pub biome_to_id: BTreeMap, 14 | } 15 | 16 | impl WorldShared { 17 | pub(crate) fn new( 18 | biomes: &BiomeRegistry, 19 | runtime: &Runtime, 20 | path: &Path, 21 | ) -> anyhow::Result { 22 | let regions = RegionManager::new(runtime, path).context("failed to get anvil data")?; 23 | 24 | let biome_to_id = biomes.iter().map(|(id, name, _)| (name, id)).collect(); 25 | 26 | Ok(Self { 27 | regions, 28 | biome_to_id, 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # basic 6 | target: auto 7 | threshold: 1% 8 | base: auto 9 | # advanced 10 | branches: 11 | - main 12 | if_ci_failed: error 13 | informational: true 14 | only_pulls: false 15 | 16 | patch: 17 | default: 18 | # basic 19 | target: auto 20 | threshold: 1% 21 | base: auto 22 | # advanced 23 | branches: 24 | - main 25 | if_ci_failed: error 26 | informational: true 27 | only_pulls: false 28 | 29 | ignore: 30 | - "**/benches/**/*" # Ignore benchmarks 31 | - "**/*_test.rs" # Ignore test files 32 | - "**/tests/**/*" # Ignore test directories 33 | 34 | github_checks: 35 | annotations: true 36 | 37 | comment: 38 | layout: "diff, flags, files" 39 | behavior: default 40 | require_changes: false 41 | require_base: false 42 | require_head: true 43 | hide_project_coverage: false -------------------------------------------------------------------------------- /tools/antithesis-bot/src/lib.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::serde_json::json; 2 | use serde::Deserialize; 3 | 4 | mod bot; 5 | 6 | #[derive(Deserialize, Debug)] 7 | pub struct LaunchArguments { 8 | address: String, 9 | 10 | #[serde(default = "default_bot_count")] 11 | bot_count: u32, 12 | } 13 | 14 | const fn default_bot_count() -> u32 { 15 | 1 16 | } 17 | 18 | pub async fn start(args: LaunchArguments) -> eyre::Result<()> { 19 | const UNUSUALLY_HIGH_BOT_THRESHOLD: u32 = 1_000; 20 | 21 | antithesis_sdk::antithesis_init(); 22 | 23 | let setup_complete = json!({ 24 | "pls_work": "plssssssss (1)" 25 | }); 26 | 27 | antithesis_sdk::lifecycle::setup_complete(&setup_complete); 28 | 29 | tracing::info!("args = {args:?}"); 30 | 31 | let LaunchArguments { address, bot_count } = args; 32 | 33 | if bot_count > UNUSUALLY_HIGH_BOT_THRESHOLD { 34 | tracing::warn!("bot_count {bot_count} is unusually high. This may cause issues."); 35 | } 36 | 37 | for _ in 0..bot_count { 38 | bot::launch(&address).await?; 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /crates/hyperion-packet-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod packet; 4 | mod replace; 5 | mod state; 6 | 7 | // TODO: for_each is a somewhat misleading name 8 | 9 | #[proc_macro] 10 | pub fn for_each_handshake_c2s_packet(input: TokenStream) -> TokenStream { 11 | packet::for_each_packet( 12 | input, 13 | "handshaking", 14 | packet::HANDSHAKE_C2S_PACKETS.iter().copied(), 15 | ) 16 | } 17 | 18 | #[proc_macro] 19 | pub fn for_each_status_c2s_packet(input: TokenStream) -> TokenStream { 20 | packet::for_each_packet(input, "status", packet::STATUS_C2S_PACKETS.iter().copied()) 21 | } 22 | 23 | #[proc_macro] 24 | pub fn for_each_login_c2s_packet(input: TokenStream) -> TokenStream { 25 | packet::for_each_packet(input, "login", packet::LOGIN_C2S_PACKETS.iter().copied()) 26 | } 27 | 28 | #[proc_macro] 29 | pub fn for_each_play_c2s_packet(input: TokenStream) -> TokenStream { 30 | packet::for_each_packet(input, "play", packet::PLAY_C2S_PACKETS.iter().copied()) 31 | } 32 | 33 | #[proc_macro] 34 | pub fn for_each_state(input: TokenStream) -> TokenStream { 35 | state::for_each_state(input) 36 | } 37 | -------------------------------------------------------------------------------- /crates/hyperion-proxy/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::io::IoSlice; 2 | 3 | use tokio::io::{AsyncWrite, AsyncWriteExt}; 4 | 5 | /// Extension trait for [`AsyncWrite`] to write all data from given IO vectors. 6 | pub trait AsyncWriteVectoredExt: AsyncWrite + Unpin { 7 | /// Writes all data from the given IO vectors to the writer. 8 | fn write_vectored_all( 9 | &mut self, 10 | mut io_vectors: &mut [IoSlice<'_>], 11 | ) -> impl std::future::Future> { 12 | async move { 13 | while !io_vectors.is_empty() { 14 | let bytes_written = self.write_vectored(io_vectors).await?; 15 | if bytes_written == 0 { 16 | return Err(std::io::Error::new( 17 | std::io::ErrorKind::WriteZero, 18 | "failed to write the entire buffer to the writer", 19 | )); 20 | } 21 | IoSlice::advance_slices(&mut io_vectors, bytes_written); 22 | } 23 | Ok(()) 24 | } 25 | } 26 | } 27 | 28 | impl AsyncWriteVectoredExt for T {} 29 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/animation.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use enumset::{EnumSet, EnumSetType}; 3 | use valence_protocol::{VarInt, packets::play::EntityAnimationS2c}; 4 | 5 | #[derive(EnumSetType)] 6 | #[repr(u8)] 7 | pub enum Kind { 8 | SwingMainArm = 0, 9 | UseItem = 1, 10 | LeaveBed = 2, 11 | SwingOffHand = 3, 12 | Critical = 4, 13 | MagicCritical = 5, 14 | } 15 | 16 | #[derive(Component)] 17 | pub struct ActiveAnimation { 18 | kind: EnumSet, 19 | } 20 | 21 | impl ActiveAnimation { 22 | pub const NONE: Self = Self { 23 | kind: EnumSet::empty(), 24 | }; 25 | 26 | pub fn packets( 27 | &mut self, 28 | entity_id: VarInt, 29 | ) -> impl Iterator + use<> { 30 | self.kind.iter().map(move |kind| { 31 | let kind = kind as u8; 32 | EntityAnimationS2c { 33 | entity_id, 34 | animation: kind, 35 | } 36 | }) 37 | } 38 | 39 | pub fn push(&mut self, kind: Kind) { 40 | self.kind.insert(kind); 41 | } 42 | 43 | pub fn clear(&mut self) { 44 | self.kind.clear(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/hyperion-text/src/helper.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::{Text, TextContent}; 4 | 5 | impl<'a> Text<'a> { 6 | /// Creates a new `Text` instance from a string slice. 7 | #[must_use] 8 | pub const fn new(s: &'a str) -> Self { 9 | Text { 10 | content: TextContent::Text { 11 | text: Cow::Borrowed(s), 12 | }, 13 | color: None, 14 | font: None, 15 | bold: None, 16 | italic: None, 17 | underlined: None, 18 | strikethrough: None, 19 | obfuscated: None, 20 | insertion: None, 21 | click_event: None, 22 | hover_event: None, 23 | extra: Vec::new(), 24 | } 25 | } 26 | } 27 | 28 | // Implement From trait for &str to Text conversion 29 | impl<'a> From<&'a str> for Text<'a> { 30 | fn from(s: &'a str) -> Self { 31 | Text::new(s) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_text_creation_and_conversion() { 41 | let text1 = Text::new("Hello, world!"); 42 | let text2: Text<'_> = "Hello, world!".into(); 43 | 44 | assert_eq!(text1, text2); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/hyperion/src/egress/stats.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{ 4 | net::Compose, 5 | simulation::{blocks::Blocks, packet_state}, 6 | }; 7 | 8 | pub struct StatsPlugin; 9 | 10 | impl Plugin for StatsPlugin { 11 | fn build(&self, app: &mut App) { 12 | app.add_systems(FixedUpdate, (global_update, load_pending)); 13 | app.add_observer(player_join_world); 14 | app.add_observer(player_leave_world); 15 | } 16 | } 17 | 18 | #[expect(clippy::missing_const_for_fn, reason = "false positive")] 19 | fn global_update(mut compose: ResMut<'_, Compose>) { 20 | let global = compose.global_mut(); 21 | 22 | global.tick += 1; 23 | } 24 | 25 | pub fn player_join_world(_: Trigger<'_, OnAdd, packet_state::Play>, compose: Res<'_, Compose>) { 26 | compose 27 | .global() 28 | .player_count 29 | .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 30 | } 31 | 32 | pub fn player_leave_world(_: Trigger<'_, OnRemove, packet_state::Play>, compose: Res<'_, Compose>) { 33 | compose 34 | .global() 35 | .player_count 36 | .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); 37 | } 38 | 39 | fn load_pending(mut blocks: ResMut<'_, Blocks>) { 40 | blocks.load_pending(); 41 | } 42 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/GithubSnippet.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 44 | 45 | -------------------------------------------------------------------------------- /crates/simd-utils/proptest-regressions/lib.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 5bf89a88a063e4fce4a9a0fa7acdfb22bac51014468b6525b4f09c4d5364de26 # shrinks to current = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 8 | cc cb6ec7ecdf1576b82fdb0b96a5848958e0f0c770ff70e886beb45eaed74c83a3 # shrinks to current = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 9 | cc cd3f3b35fcb137d6d75e64730a07b688f609462029f4d77c701d29bf02037486 # shrinks to current = [0, 0, 0, 0, 0, 0, 0, 0, 386, 0, 0, 0, 0, 0, 0, 0, 0] 10 | cc 53eb6c4354595cfc01285c48548bf6aed654ead10a265bf6f214855fbba561b2 # shrinks to current = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 11 | cc b61e48091cd302dd3112d76307d0031b55d0820463b1ec7506ddd338c196fead # shrinks to current = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 12 | cc 3fd746d4c176ec0a96d96095c45617318283ed570eac0f9dd6468c7cd59a8093 # shrinks to current = [0, 0, 0, 1585, 0, 0, 61523, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0] 13 | -------------------------------------------------------------------------------- /crates/bvh-region/Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bench]] 2 | harness = false 3 | name = "sort" 4 | 5 | #[[bench]] 6 | #harness = false 7 | #name = "bvh" 8 | # 9 | #[[bench]] 10 | #harness = false 11 | #name = "side_by_side" 12 | 13 | [dependencies] 14 | arrayvec = { workspace = true } 15 | derive_more = { workspace = true } 16 | fastrand = { workspace = true } 17 | geometry = { workspace = true } 18 | glam = { workspace = true, features = ["serde"] } 19 | num-traits = { workspace = true } 20 | ordered-float = { workspace = true } 21 | plotters = { workspace = true, features = ["plotters-bitmap", "image"], optional = true } 22 | plotters-bitmap = { workspace = true, optional = true } 23 | proptest = { workspace = true } 24 | rayon = { workspace = true } 25 | tracing = { workspace = true } 26 | 27 | [dev-dependencies] 28 | approx = { workspace = true } 29 | criterion = { workspace = true } 30 | rand = { workspace = true } 31 | #divan = {workspace = true} 32 | #tango-bench = {workspace = true} 33 | 34 | [features] 35 | default = [] 36 | plot = ["dep:plotters", "dep:plotters-bitmap"] 37 | 38 | [lints] 39 | workspace = true 40 | 41 | [package] 42 | authors = ["Andrew Gazelka "] 43 | edition.workspace = true 44 | name = "bvh-region" 45 | publish = false 46 | readme = "README.md" 47 | version.workspace = true 48 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1739866667, 6 | "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "rust-overlay": "rust-overlay" 23 | } 24 | }, 25 | "rust-overlay": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "nixpkgs" 29 | ] 30 | }, 31 | "locked": { 32 | "lastModified": 1740191166, 33 | "narHash": "sha256-WqRxO1Afx8jPYG4CKwkvDFWFvDLCwCd4mxb97hFGYPg=", 34 | "owner": "oxalica", 35 | "repo": "rust-overlay", 36 | "rev": "74a3fb71b0cc67376ab9e7c31abcd68c813fc226", 37 | "type": "github" 38 | }, 39 | "original": { 40 | "owner": "oxalica", 41 | "repo": "rust-overlay", 42 | "type": "github" 43 | } 44 | } 45 | }, 46 | "root": "root", 47 | "version": 7 48 | } 49 | -------------------------------------------------------------------------------- /crates/bvh-region/src/utils.rs: -------------------------------------------------------------------------------- 1 | use derive_more::Constructor; 2 | use geometry::aabb::Aabb; 3 | 4 | use crate::node::BvhNode; 5 | 6 | /// get number of threads that is pow of 2 7 | pub fn thread_count_pow2() -> usize { 8 | let max_threads_tentative = rayon::current_num_threads(); 9 | // let max 10 | 11 | // does not make sense to not have a power of two 12 | let mut max_threads = max_threads_tentative.next_power_of_two(); 13 | 14 | if max_threads != max_threads_tentative { 15 | max_threads >>= 1; 16 | } 17 | 18 | max_threads 19 | } 20 | 21 | pub trait GetAabb: Fn(&T) -> Aabb {} 22 | 23 | impl GetAabb for F where F: Fn(&T) -> Aabb {} 24 | 25 | #[derive(Constructor, Copy, Clone, Debug)] 26 | pub struct NodeOrd<'a, T> { 27 | pub node: &'a BvhNode, 28 | pub by: T, 29 | } 30 | 31 | impl PartialEq for NodeOrd<'_, T> { 32 | fn eq(&self, other: &Self) -> bool { 33 | self.by == other.by 34 | } 35 | } 36 | impl PartialOrd for NodeOrd<'_, T> { 37 | fn partial_cmp(&self, other: &Self) -> Option { 38 | self.by.partial_cmp(&other.by) 39 | } 40 | } 41 | 42 | impl Eq for NodeOrd<'_, T> {} 43 | 44 | impl Ord for NodeOrd<'_, T> { 45 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 46 | self.by.cmp(&other.by) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/hyperion-proxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | arrayvec = { workspace = true } 3 | colored = { workspace = true } 4 | kanal = { workspace = true } 5 | papaya = { workspace = true } 6 | rkyv = { workspace = true } 7 | rustc-hash = { workspace = true } 8 | rustls = { workspace = true } 9 | rustls-pki-types = { workspace = true } 10 | rustls-webpki = { workspace = true } 11 | tokio = { workspace = true, features = ["full", "tracing"] } 12 | tokio-rustls = { workspace = true } 13 | tokio-util = { workspace = true, features = ["full"] } 14 | anyhow = { workspace = true } 15 | bvh = { workspace = true } 16 | bytes = { workspace = true } 17 | clap = { workspace = true } 18 | glam = { workspace = true } 19 | heapless = { workspace = true } 20 | hyperion-proto = { workspace = true } 21 | more-asserts = { workspace = true } 22 | slotmap = { workspace = true } 23 | tracing = { workspace = true } 24 | tracing-subscriber = { workspace = true } 25 | serde = { version = "1.0", features = ["derive"] } 26 | envy = "0.4" 27 | dotenvy = "0.15" 28 | 29 | [lints] 30 | workspace = true 31 | 32 | [package] 33 | authors = ["Andrew Gazelka "] 34 | edition.workspace = true 35 | name = "hyperion-proxy" 36 | publish = false 37 | readme = "README.md" 38 | version.workspace = true 39 | 40 | [target.'cfg(not(target_os = "windows"))'.dependencies] 41 | tikv-jemallocator.workspace = true 42 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/status.rs: -------------------------------------------------------------------------------- 1 | use std::ops::BitOr; 2 | 3 | #[derive(Clone, Copy, PartialEq, Eq)] 4 | pub struct EntityStatus(pub u8); 5 | 6 | impl EntityStatus { 7 | pub const HAS_GLOWING_EFFECT: Self = Self(0x40); 8 | pub const IS_CROUCHING: Self = Self(0x02); 9 | pub const IS_FLYING_WITH_ELYTRA: Self = Self(0x80); 10 | pub const IS_INVISIBLE: Self = Self(0x20); 11 | pub const IS_ON_FIRE: Self = Self(0x01); 12 | pub const IS_SPRINTING: Self = Self(0x08); 13 | pub const IS_SWIMMING: Self = Self(0x10); 14 | 15 | pub const fn has_status(self, status: Self) -> bool { 16 | self.0 & status.0 != 0 17 | } 18 | 19 | pub const fn set_status(&mut self, status: Self) { 20 | self.0 |= status.0; 21 | } 22 | 23 | pub const fn clear_status(&mut self, status: Self) { 24 | self.0 &= !status.0; 25 | } 26 | 27 | pub const fn toggle_status(&mut self, status: Self) { 28 | self.0 ^= status.0; 29 | } 30 | } 31 | 32 | impl BitOr for EntityStatus { 33 | type Output = Self; 34 | 35 | fn bitor(self, rhs: Self) -> Self::Output { 36 | Self(self.0 | rhs.0) 37 | } 38 | } 39 | 40 | impl BitOr for &EntityStatus { 41 | type Output = EntityStatus; 42 | 43 | fn bitor(self, rhs: EntityStatus) -> Self::Output { 44 | EntityStatus(self.0 | rhs.0) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/packet_state.rs: -------------------------------------------------------------------------------- 1 | //! Components marking a player packet state. Players will have at most 1 state component at a time (they may have no components during state transitions) 2 | //! 3 | //! All players with a state component assigned are guaranteed to have the following components: 4 | /// - [`crate::ConnectionId`] 5 | /// - [`crate::PacketDecoder`] 6 | use bevy::prelude::*; 7 | 8 | /// Marks players who are in the handshake state. 9 | #[derive(Component)] 10 | pub struct Handshake(pub(crate) ()); 11 | 12 | /// Marks players who are in the status state. 13 | #[derive(Component)] 14 | pub struct Status(pub(crate) ()); 15 | 16 | /// Marks players who are in the login state. 17 | #[derive(Component)] 18 | pub struct Login(pub(crate) ()); 19 | 20 | /// Marks players who are in the play state. 21 | /// 22 | /// Players in this state are guaranteed to have the following components: 23 | /// - [`crate::simulation::Name`] 24 | /// - [`crate::simulation::Uuid`] 25 | /// - [`crate::simulation::AiTargetable`] 26 | /// - [`crate::simulation::ImmuneStatus`] 27 | /// - [`crate::simulation::ChunkPosition`] 28 | /// - [`crate::egress::sync_chunks::ChunkSendQueue`] 29 | /// - [`crate::simulation::Yaw`] 30 | /// - [`crate::simulation::Pitch`] 31 | /// - [`crate::simulation::skin::PlayerSkin`] 32 | /// - [`crate::simulation::Position`] 33 | #[derive(Component)] 34 | pub struct Play(pub(crate) ()); 35 | -------------------------------------------------------------------------------- /crates/hyperion-packet-macros/src/state.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{TokenStream, TokenTree}; 2 | use quote::quote; 3 | 4 | use crate::replace::{SpecialIdentReplacer, replace}; 5 | 6 | pub fn for_each_state(input: TokenStream) -> TokenStream { 7 | replace(input, STATES.iter().copied(), StateIdentReplacer) 8 | } 9 | 10 | #[derive(Copy, Clone)] 11 | struct State { 12 | name: &'static str, 13 | } 14 | 15 | #[derive(Copy, Clone)] 16 | struct StateIdentReplacer; 17 | 18 | impl SpecialIdentReplacer for StateIdentReplacer { 19 | fn replace(&self, ident: proc_macro::Ident, state: State) -> Option { 20 | let ident_str = format!("{ident}"); 21 | if ident_str == "for_each_packet" { 22 | let state_ident = proc_macro2::Ident::new( 23 | &format!("for_each_{}_c2s_packet", state.name), 24 | ident.span().into(), 25 | ); 26 | Some(quote!(::hyperion_packet_macros::#state_ident).into()) 27 | } else if ident_str == "state" { 28 | let state_ident = proc_macro::Ident::new(state.name, ident.span()); 29 | Some(TokenStream::from(TokenTree::Ident(state_ident))) 30 | } else { 31 | None 32 | } 33 | } 34 | } 35 | 36 | const STATES: &[State] = &[ 37 | State { name: "handshake" }, 38 | State { name: "status" }, 39 | State { name: "login" }, 40 | State { name: "play" }, 41 | ]; 42 | -------------------------------------------------------------------------------- /crates/hyperion-utils/src/prev.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use tracing::error; 3 | 4 | fn initialize_previous( 5 | trigger: Trigger<'_, OnAdd, T>, 6 | query: Query<'_, '_, &T>, 7 | mut commands: Commands<'_, '_>, 8 | ) { 9 | let value = match query.get(trigger.target()) { 10 | Ok(value) => value, 11 | Err(e) => { 12 | error!("could not initialize previous: query failed: {e}"); 13 | return; 14 | } 15 | }; 16 | 17 | commands 18 | .entity(trigger.target()) 19 | .insert(Prev(value.clone())); 20 | } 21 | 22 | fn update_previous(mut query: Query<'_, '_, (&mut Prev, &T)>) { 23 | for (mut prev, current) in &mut query { 24 | prev.set(current.clone()); 25 | } 26 | } 27 | 28 | pub fn track_prev(app: &mut App) { 29 | // TODO: There should be an error for calling this function for the same component twice 30 | app.add_observer(initialize_previous::); 31 | app.add_systems(FixedPreUpdate, update_previous::); 32 | } 33 | 34 | /// Component storing the value of a component in the previous frame. This is updated every 35 | /// `FixedPreUpdate`. 36 | #[derive(Component, Copy, Clone, Deref, PartialEq, Eq, Debug)] 37 | pub struct Prev(#[deref] T); 38 | 39 | impl Prev { 40 | fn set(&mut self, new: T) { 41 | self.0 = new; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tools/rust-mc-bot/src/states/login.rs: -------------------------------------------------------------------------------- 1 | use crate::{Bot, Compression, packet_utils::Buf}; 2 | 3 | // c2s 4 | pub fn write_handshake_packet( 5 | protocol_version: u32, 6 | server_address: &str, 7 | server_port: u16, 8 | next_state: u32, 9 | ) -> Buf { 10 | let mut buf = Buf::with_length((1 + 4 + server_address.len() + 2 + 4) as u32); 11 | buf.write_packet_id(0x00); 12 | 13 | buf.write_var_u32(protocol_version); 14 | buf.write_sized_str(server_address); 15 | buf.write_u16(server_port); 16 | buf.write_var_u32(next_state); 17 | 18 | buf 19 | } 20 | 21 | pub fn write_login_start_packet(username: &str) -> Buf { 22 | let mut buf = Buf::with_length(1 + username.len() as u32); 23 | buf.write_packet_id(0x00); 24 | 25 | buf.write_sized_str(username); 26 | buf.write_bool(false); 27 | 28 | buf 29 | } 30 | 31 | // s2c 32 | 33 | // 0x02 34 | pub fn process_login_success_packet( 35 | buffer: &mut Buf, 36 | bot: &mut Bot, 37 | _compression: &mut Compression, 38 | ) { 39 | let _uuid = buffer.read_u128(); 40 | let _name = buffer.read_sized_string(); 41 | let _properties = buffer.read_var_u32(); 42 | 43 | bot.state = 2; 44 | } 45 | 46 | // 0x03 47 | pub fn process_set_compression_packet( 48 | buf: &mut Buf, 49 | bot: &mut Bot, 50 | _compression: &mut Compression, 51 | ) { 52 | bot.compression_threshold = buf.read_var_u32().0 as i32; 53 | } 54 | -------------------------------------------------------------------------------- /events/bedwars/src/command/speed.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::{ 4 | net::{Compose, ConnectionId, agnostic}, 5 | simulation::FlyingSpeed, 6 | }; 7 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 8 | use tracing::error; 9 | 10 | #[derive(Parser, CommandPermission, Debug)] 11 | #[command(name = "speed")] 12 | #[command_permission(group = "Moderator")] 13 | pub struct SpeedCommand { 14 | amount: f32, 15 | } 16 | 17 | impl MinecraftCommand for SpeedCommand { 18 | type State = SystemState<( 19 | Query<'static, 'static, &'static ConnectionId>, 20 | Res<'static, Compose>, 21 | Commands<'static, 'static>, 22 | )>; 23 | 24 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 25 | let (query, compose, mut commands) = state.get(world); 26 | 27 | let &connection_id = match query.get(caller) { 28 | Ok(connection_id) => connection_id, 29 | Err(e) => { 30 | error!("speed command failed: query failed: {e}"); 31 | return; 32 | } 33 | }; 34 | 35 | let msg = format!("Setting speed to {}", self.amount); 36 | let chat = agnostic::chat(msg); 37 | compose.unicast(&chat, connection_id).unwrap(); 38 | 39 | commands 40 | .entity(caller) 41 | .insert(FlyingSpeed::new(self.amount)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/simd-utils/src/one_bit_positions.rs: -------------------------------------------------------------------------------- 1 | use std::iter::{FusedIterator, TrustedLen}; 2 | 3 | pub struct OneBitPositions { 4 | pub remaining: u64, 5 | } 6 | 7 | impl OneBitPositions { 8 | const fn new(number: u64) -> Self { 9 | Self { remaining: number } 10 | } 11 | } 12 | 13 | impl Iterator for OneBitPositions { 14 | type Item = usize; 15 | 16 | fn next(&mut self) -> Option { 17 | if self.remaining == 0 { 18 | None 19 | } else { 20 | // Get position of lowest set bit 21 | let pos = self.remaining.trailing_zeros(); 22 | // Clear the lowest set bit 23 | self.remaining &= self.remaining - 1; 24 | Some(pos as usize) 25 | } 26 | } 27 | 28 | fn size_hint(&self) -> (usize, Option) { 29 | let len = self.len(); 30 | (len, Some(len)) 31 | } 32 | } 33 | 34 | impl ExactSizeIterator for OneBitPositions { 35 | fn len(&self) -> usize { 36 | self.remaining.count_ones() as usize 37 | } 38 | } 39 | 40 | impl FusedIterator for OneBitPositions {} 41 | 42 | unsafe impl TrustedLen for OneBitPositions {} 43 | 44 | // Extension trait for more ergonomic usage 45 | pub trait OneBitPositionsExt { 46 | fn one_positions(self) -> OneBitPositions; 47 | } 48 | 49 | impl OneBitPositionsExt for u64 { 50 | fn one_positions(self) -> OneBitPositions { 51 | OneBitPositions::new(self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /events/bedwars/src/command/bow.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::{ItemKind, ItemStack}; 4 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 5 | use hyperion_inventory::PlayerInventory; 6 | use tracing::error; 7 | 8 | #[derive(Parser, CommandPermission, Debug)] 9 | #[command(name = "bow")] 10 | #[command_permission(group = "Normal")] 11 | pub struct BowCommand; 12 | 13 | impl MinecraftCommand for BowCommand { 14 | type State = SystemState>; 15 | 16 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 17 | let mut commands = state.get(world); 18 | commands 19 | .entity(caller) 20 | .queue(|mut caller: EntityWorldMut<'_>| { 21 | let Some(mut inventory) = caller.get_mut::() else { 22 | error!("bow command failed: player is missing PlayerInventory component"); 23 | return; 24 | }; 25 | 26 | inventory.try_add_item(ItemStack { 27 | item: ItemKind::Bow, 28 | count: 1, 29 | nbt: None, 30 | }); 31 | 32 | inventory.try_add_item(ItemStack { 33 | item: ItemKind::Arrow, 34 | count: 64, 35 | nbt: None, 36 | }); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/hyperion-proto/src/proxy_to_server.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{Archive, Deserialize, Serialize, with::InlineAsBox}; 2 | 3 | #[derive(Archive, Deserialize, Serialize, Clone, PartialEq, Debug)] 4 | pub struct PlayerPackets<'a> { 5 | pub stream: u64, 6 | 7 | #[rkyv(with = InlineAsBox)] 8 | pub data: &'a [u8], 9 | } 10 | 11 | #[derive(Archive, Deserialize, Serialize, Clone, Copy, PartialEq, Debug)] 12 | pub struct PlayerConnect { 13 | pub stream: u64, 14 | } 15 | 16 | #[derive(Archive, Deserialize, Serialize, Clone, Copy, PartialEq, Debug)] 17 | pub struct PlayerDisconnect<'a> { 18 | pub stream: u64, 19 | pub reason: PlayerDisconnectReason<'a>, 20 | } 21 | 22 | #[derive(Archive, Deserialize, Serialize, Clone, Copy, PartialEq, Debug)] 23 | #[non_exhaustive] 24 | pub enum PlayerDisconnectReason<'a> { 25 | /// If cannot receive packets fast enough 26 | CouldNotKeepUp, 27 | LostConnection, 28 | 29 | Other(#[rkyv(with = InlineAsBox)] &'a str), 30 | } 31 | 32 | #[derive(Archive, Deserialize, Serialize, Clone, PartialEq, Debug)] 33 | pub struct RequestSubscribeChannelPackets<'a> { 34 | #[rkyv(with = InlineAsBox)] 35 | pub channels: &'a [u32], 36 | } 37 | 38 | #[derive(Archive, Deserialize, Serialize, Clone, PartialEq, Debug)] 39 | pub enum ProxyToServerMessage<'a> { 40 | PlayerConnect(PlayerConnect), 41 | PlayerDisconnect(PlayerDisconnect<'a>), 42 | PlayerPackets(PlayerPackets<'a>), 43 | RequestSubscribeChannelPackets(RequestSubscribeChannelPackets<'a>), 44 | } 45 | -------------------------------------------------------------------------------- /tools/packet-inspector/src/app/connection.rs: -------------------------------------------------------------------------------- 1 | use super::{SharedState, Tab, View}; 2 | use crate::shared_state::Event; 3 | 4 | pub struct Connection; 5 | 6 | impl Tab for Connection { 7 | fn new() -> Self { 8 | Self {} 9 | } 10 | 11 | fn name(&self) -> &'static str { 12 | "Connection" 13 | } 14 | } 15 | 16 | impl View for Connection { 17 | fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) { 18 | ui.label("Listener Address"); 19 | 20 | if state.is_listening { 21 | ui.text_edit_singleline(&mut state.listener_addr.clone()); 22 | ui.label("Server Address"); 23 | ui.text_edit_singleline(&mut state.server_addr.clone()); 24 | 25 | ui.horizontal(|ui| { 26 | if ui.button("Stop Listening").clicked() { 27 | state.send_event(Event::StopListening); 28 | } 29 | ui.checkbox(&mut state.autostart, "Autostart"); 30 | }); 31 | } else { 32 | ui.label("Listener Address"); 33 | ui.text_edit_singleline(&mut state.listener_addr); 34 | ui.label("Server Address"); 35 | ui.text_edit_singleline(&mut state.server_addr); 36 | ui.horizontal(|ui| { 37 | if ui.button("Start Listening").clicked() { 38 | state.send_event(Event::StartListening); 39 | } 40 | ui.checkbox(&mut state.autostart, "Autostart"); 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | hyperion-proxy: 3 | image: ghcr.io/hyperion-mc/hyperion/hyperion-proxy:latest 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | target: hyperion-proxy 8 | ports: 9 | - "25565:25565" 10 | command: [] 11 | restart: "no" 12 | environment: 13 | - RUST_LOG=info 14 | - HYPERION_PROXY_PROXY_ADDR=0.0.0.0:25565 15 | - HYPERION_PROXY_SERVER=bedwars:35565 16 | networks: 17 | - proxy-network 18 | depends_on: 19 | - bedwars 20 | bedwars: 21 | image: ghcr.io/hyperion-mc/hyperion/bedwars:latest 22 | build: 23 | context: . 24 | dockerfile: Dockerfile 25 | target: bedwars 26 | ports: 27 | - "27750:27750" 28 | expose: 29 | - "35565" 30 | command: [] 31 | restart: "no" 32 | environment: 33 | - RUST_LOG=info 34 | - BEDWARS_IP=0.0.0.0 35 | - BEDWARS_PORT=35565 36 | networks: 37 | - proxy-network 38 | rust-mc-bot: 39 | # image: ghcr.io/hyperion-mc/hyperion/rust-mc-bot:latest 40 | build: 41 | context: . 42 | dockerfile: Dockerfile 43 | target: rust-mc-bot 44 | command: [] 45 | restart: "no" 46 | depends_on: 47 | - hyperion-proxy 48 | environment: 49 | - RUST_LOG=info 50 | - BOT_SERVER=hyperion-proxy:25565 51 | - BOT_BOT_COUNT=500 52 | - BOT_THREADS=2 53 | networks: 54 | - proxy-network 55 | profiles: 56 | - bot 57 | 58 | networks: 59 | proxy-network: 60 | driver: bridge 61 | -------------------------------------------------------------------------------- /tools/packet-inspector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "packet-inspector" 3 | description = "A simple Minecraft proxy for inspecting packets." 4 | version.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | documentation.workspace = true 8 | license.workspace = true 9 | publish = false 10 | 11 | [lints] 12 | workspace = true 13 | 14 | [dependencies] 15 | valence_protocol = { git = 'https://github.com/TestingPlant/valence', branch = 'feat-open', features = ["compression"] } 16 | anyhow.workspace = true 17 | bytes.workspace = true 18 | flate2.workspace = true 19 | flume.workspace = true 20 | tokio = { workspace = true, features = ["full"] } 21 | tracing.workspace = true 22 | time = { workspace = true, features = ["local-offset"] } 23 | egui.workspace = true 24 | eframe = { workspace = true, features = [ 25 | "persistence", 26 | "wgpu", 27 | ] } 28 | egui_dock = { workspace = true, features = ["serde"] } 29 | itertools.workspace = true 30 | syntect = { workspace = true, default-features = false, features = [ 31 | "default-fancy", 32 | ] } 33 | serde = { workspace = true, features = ["derive"] } 34 | 35 | [build-dependencies] 36 | anyhow.workspace = true 37 | syn = { workspace = true, features = ["full"] } 38 | valence_build_utils = { git = 'https://github.com/TestingPlant/valence', branch = 'feat-open' } 39 | quote = { workspace = true, features = ["proc-macro"] } 40 | serde_json = { workspace = true, features = ["raw_value"] } 41 | proc-macro2.workspace = true 42 | serde = { workspace = true, features = ["derive"] } 43 | -------------------------------------------------------------------------------- /crates/hyperion-item/src/builder/book.rs: -------------------------------------------------------------------------------- 1 | use hyperion::{ItemKind, ItemStack}; 2 | use valence_protocol::nbt; 3 | 4 | use crate::builder::ItemBuilder; 5 | 6 | #[derive(Clone, Debug)] 7 | #[must_use] 8 | pub struct BookBuilder { 9 | item: ItemBuilder, 10 | } 11 | 12 | // /give @p minecraft:written_book{author:"AuthorName",title:"BookTitle",pages:['{"text":"Page content"}']} 13 | 14 | impl BookBuilder { 15 | pub fn new(author: impl Into, title: impl Into) -> Self { 16 | let mut item = ItemBuilder::new(ItemKind::WrittenBook); 17 | 18 | let author = author.into(); 19 | let title = title.into(); 20 | 21 | let mut nbt = nbt::Compound::new(); 22 | 23 | nbt.insert("author", nbt::Value::String(author)); 24 | nbt.insert("resolved", nbt::Value::Byte(1)); 25 | nbt.insert("title", nbt::Value::String(title)); 26 | nbt.insert("pages", nbt::Value::List(nbt::List::String(Vec::new()))); 27 | 28 | item.nbt = Some(nbt); 29 | 30 | Self { item } 31 | } 32 | 33 | pub fn add_page(mut self, page: impl Into) -> Self { 34 | let page = page.into(); 35 | let json = format!(r#"{{"text":"{page}"}}"#); 36 | 37 | if let Some(nbt) = &mut self.item.nbt { 38 | if let nbt::Value::List(nbt::List::String(pages)) = nbt.get_mut("pages").unwrap() { 39 | pages.push(json); 40 | } 41 | } 42 | 43 | self 44 | } 45 | 46 | #[must_use] 47 | pub fn build(self) -> ItemStack { 48 | self.item.build() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | import { withMermaid } from 'vitepress-plugin-mermaid'; 4 | 5 | import footnote from 'markdown-it-footnote' 6 | 7 | // https://vitepress.dev/reference/site-config 8 | const config = defineConfig({ 9 | title: "Hyperion", 10 | description: "The most advanced Minecraft game engine built in Rust", 11 | markdown: { 12 | math: true, 13 | config: (md) => { 14 | md.use(footnote) 15 | } 16 | }, 17 | themeConfig: { 18 | // https://vitepress.dev/reference/default-theme-config 19 | nav: [ 20 | { text: 'Home', link: '/' }, 21 | { text: 'Architecture', link: '/architecture/introduction' }, 22 | { text: 'Tag', link: '/bedwars/introduction' }, 23 | ], 24 | 25 | sidebar: [ 26 | { 27 | text: 'Architecture', 28 | items: [ 29 | { text: 'Introduction', link: '/architecture/introduction' }, 30 | { text: 'Game Server', link: '/architecture/game-server' }, 31 | { text: 'Proxy', link: '/architecture/proxy' }, 32 | ] 33 | }, 34 | { 35 | text: 'Tag', 36 | items: [ 37 | { text: '10,000 Player PvP', link: '/bedwars/introduction' }, 38 | ] 39 | } 40 | ], 41 | socialLinks: [ 42 | { icon: 'github', link: 'https://github.com/hyperion-mc/hyperion' } 43 | ] 44 | } 45 | }) 46 | 47 | 48 | export default withMermaid(config); 49 | -------------------------------------------------------------------------------- /crates/hyperion-proxy-module/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, path::Path}; 2 | 3 | use bevy::prelude::*; 4 | use hyperion::runtime::AsyncRuntime; 5 | use tokio::net::TcpListener; 6 | 7 | pub struct HyperionProxyPlugin; 8 | 9 | #[derive(Event)] 10 | pub struct SetProxyAddress { 11 | pub proxy: String, 12 | pub server: String, 13 | } 14 | 15 | impl Default for SetProxyAddress { 16 | fn default() -> Self { 17 | Self { 18 | proxy: "0.0.0.0:25565".to_string(), 19 | server: "127.0.0.1:35565".to_string(), 20 | } 21 | } 22 | } 23 | 24 | impl Plugin for HyperionProxyPlugin { 25 | fn build(&self, app: &mut App) { 26 | app.add_event::(); 27 | app.add_observer(update_proxy_address); 28 | } 29 | } 30 | 31 | fn update_proxy_address(trigger: Trigger<'_, SetProxyAddress>, runtime: Res<'_, AsyncRuntime>) { 32 | let proxy = trigger.proxy.clone(); 33 | let server = trigger.server.clone(); 34 | 35 | runtime.spawn(async move { 36 | let listener = TcpListener::bind(&proxy).await.unwrap(); 37 | tracing::info!("Listening on {proxy}"); 38 | 39 | let addr: SocketAddr = tokio::net::lookup_host(&server) 40 | .await 41 | .unwrap() 42 | .next() 43 | .unwrap(); 44 | 45 | hyperion_proxy::run_proxy( 46 | listener, 47 | addr, 48 | server.clone(), 49 | Path::new("root_ca.crt"), 50 | Path::new("proxy.crt"), 51 | Path::new("proxy_private_key.pem"), 52 | ) 53 | .await 54 | .unwrap(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /crates/hyperion-permission/src/storage.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use heed::{Database, Env, byteorder::NativeEndian, types}; 3 | use hyperion::storage::LocalDb; 4 | use num_traits::{FromPrimitive, ToPrimitive}; 5 | 6 | use crate::Group; 7 | 8 | #[derive(Resource)] 9 | pub struct PermissionStorage { 10 | env: Env, 11 | perms: Database, types::U8>, 12 | } 13 | 14 | impl PermissionStorage { 15 | pub fn new(db: &LocalDb) -> anyhow::Result { 16 | // We open the default unnamed database 17 | let perms = { 18 | let mut wtxn = db.write_txn()?; 19 | let db = db.create_database(&mut wtxn, Some("uuid-to-perms"))?; 20 | wtxn.commit()?; 21 | db 22 | }; 23 | 24 | Ok(Self { 25 | env: (**db).clone(), 26 | perms, 27 | }) 28 | } 29 | 30 | pub fn get(&self, uuid: uuid::Uuid) -> Group { 31 | let uuid = uuid.as_u128(); 32 | let rtxn = self.env.read_txn().unwrap(); 33 | let Some(perms) = self.perms.get(&rtxn, &uuid).unwrap() else { 34 | return Group::default(); 35 | }; 36 | 37 | let Some(group) = Group::from_u8(perms) else { 38 | tracing::error!("invalid group {perms:?}"); 39 | return Group::default(); 40 | }; 41 | 42 | group 43 | } 44 | 45 | pub fn set(&self, uuid: uuid::Uuid, group: Group) -> anyhow::Result<()> { 46 | let uuid = uuid.as_u128(); 47 | let mut wtxn = self.env.write_txn()?; 48 | self.perms.put(&mut wtxn, &uuid, &group.to_u8().unwrap())?; 49 | wtxn.commit()?; 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /events/bedwars/src/command/fly.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::{ 4 | net::{Compose, ConnectionId, DataBundle, agnostic}, 5 | simulation::Flight, 6 | }; 7 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 8 | use tracing::error; 9 | 10 | #[derive(Parser, CommandPermission, Debug)] 11 | #[command(name = "fly")] 12 | #[command_permission(group = "Moderator")] 13 | pub struct FlyCommand; 14 | 15 | impl MinecraftCommand for FlyCommand { 16 | type State = SystemState<( 17 | Res<'static, Compose>, 18 | Query<'static, 'static, (&'static ConnectionId, &'static Flight)>, 19 | Commands<'static, 'static>, 20 | )>; 21 | 22 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 23 | let (compose, query, mut commands) = state.get(world); 24 | 25 | let (&connection_id, &(mut flight)) = match query.get(caller) { 26 | Ok(data) => data, 27 | Err(e) => { 28 | error!("fly command failed: query failed: {e}"); 29 | return; 30 | } 31 | }; 32 | 33 | flight.allow = !flight.allow; 34 | flight.is_flying = flight.allow && flight.is_flying; 35 | 36 | let allow_flight = flight.allow; 37 | 38 | let chat_packet = if allow_flight { 39 | agnostic::chat("§aFlying enabled") 40 | } else { 41 | agnostic::chat("§cFlying disabled") 42 | }; 43 | 44 | let mut bundle = DataBundle::new(&compose); 45 | bundle.add_packet(&chat_packet).unwrap(); 46 | bundle.unicast(connection_id).unwrap(); 47 | 48 | commands.entity(caller).insert(flight); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /events/bedwars/src/command/vanish.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::net::{Compose, ConnectionId}; 4 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 5 | use tracing::error; 6 | 7 | use crate::plugin::vanish::Vanished; 8 | 9 | #[derive(Parser, CommandPermission, Debug)] 10 | #[command(name = "vanish")] 11 | #[command_permission(group = "Admin")] 12 | pub struct VanishCommand; 13 | 14 | impl MinecraftCommand for VanishCommand { 15 | type State = SystemState<( 16 | Query< 17 | 'static, 18 | 'static, 19 | ( 20 | &'static ConnectionId, 21 | &'static Name, 22 | Option<&'static Vanished>, 23 | ), 24 | >, 25 | Res<'static, Compose>, 26 | Commands<'static, 'static>, 27 | )>; 28 | 29 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 30 | let (query, compose, mut commands) = state.get(world); 31 | 32 | let (&connection_id, name, vanished) = match query.get(caller) { 33 | Ok(data) => data, 34 | Err(e) => { 35 | error!("vanish command failed: query failed: {e}"); 36 | return; 37 | } 38 | }; 39 | 40 | let is_vanished = !vanished.is_some_and(Vanished::is_vanished); 41 | 42 | commands.entity(caller).insert(Vanished::new(is_vanished)); 43 | 44 | let packet = hyperion::net::agnostic::chat(format!( 45 | "§7[Admin] §f{name} §7is now {}", 46 | if is_vanished { "vanished" } else { "visible" } 47 | )); 48 | compose.unicast(&packet, connection_id).unwrap(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/bvh-region/benches/sort.rs: -------------------------------------------------------------------------------- 1 | use std::hint::black_box; 2 | 3 | use criterion::{Bencher, Criterion, criterion_group, criterion_main}; 4 | use ordered_float::OrderedFloat; 5 | 6 | const LEN: usize = 100; 7 | 8 | fn sort_unwrap(bencher: &mut Bencher<'_>) { 9 | let mut arr = vec![0.0; LEN]; 10 | bencher.iter(|| { 11 | // step 1: random arr of LEN, floats 12 | for elem in &mut arr { 13 | *elem = rand::random::(); 14 | } 15 | arr.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); 16 | 17 | black_box(&arr); 18 | }); 19 | } 20 | 21 | fn sort_unwrap_unchecked(bencher: &mut Bencher<'_>) { 22 | let mut arr = vec![0.0; LEN]; 23 | bencher.iter(|| { 24 | // step 1: random arr of LEN, floats 25 | for elem in &mut arr { 26 | *elem = rand::random::(); 27 | } 28 | arr.sort_unstable_by(|a, b| unsafe { a.partial_cmp(b).unwrap_unchecked() }); 29 | 30 | black_box(&arr); 31 | }); 32 | } 33 | 34 | fn sort_unwrap_unchecked_key(bencher: &mut Bencher<'_>) { 35 | let mut arr = vec![0.0; LEN]; 36 | bencher.iter(|| { 37 | // step 1: random arr of len, floats 38 | for elem in &mut arr { 39 | *elem = rand::random::(); 40 | } 41 | arr.sort_unstable_by_key(|a| OrderedFloat(*a)); 42 | 43 | black_box(&arr); 44 | }); 45 | } 46 | 47 | fn criterion_benchmark(c: &mut Criterion) { 48 | c.bench_function("sort_unwrap", sort_unwrap); 49 | c.bench_function("sort_unwrap_unchecked", sort_unwrap_unchecked); 50 | c.bench_function("sort_unwrap_unchecked_key", sort_unwrap_unchecked_key); 51 | } 52 | 53 | criterion_group!(benches, criterion_benchmark); 54 | criterion_main!(benches); 55 | -------------------------------------------------------------------------------- /events/bedwars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | anyhow = { workspace = true } 3 | bevy = { workspace = true } 4 | clap = { workspace = true } 5 | compact_str = { workspace = true } 6 | derive_more = { workspace = true } 7 | dotenvy = { workspace = true } 8 | envy = "0.4" 9 | fastrand = { workspace = true } 10 | geometry = { workspace = true } 11 | glam = { workspace = true } 12 | hyperion = { workspace = true } 13 | hyperion-clap = { workspace = true } 14 | hyperion-genmap = { workspace = true } 15 | hyperion-gui = { workspace = true } 16 | hyperion-inventory = { workspace = true } 17 | hyperion-item = { workspace = true } 18 | hyperion-permission = { workspace = true } 19 | hyperion-proxy-module = { workspace = true } 20 | hyperion-scheduled = { workspace = true } 21 | hyperion-text = { workspace = true } 22 | hyperion-utils = { workspace = true } 23 | rayon = { workspace = true } 24 | roaring = { workspace = true } 25 | rustc-hash = { workspace = true } 26 | serde = { version = "1.0", features = ["derive"] } 27 | tap = "1.0.1" 28 | tracing = { workspace = true } 29 | tracing-subscriber = { workspace = true } 30 | uuid = { workspace = true } 31 | valence_bytes = { workspace = true } 32 | valence_protocol = { workspace = true } 33 | valence_server = { workspace = true } 34 | valence_text = { workspace = true } 35 | 36 | [dev-dependencies] 37 | tracing = { workspace = true, features = ["release_max_level_info"] } 38 | 39 | [lints] 40 | workspace = true 41 | 42 | [package] 43 | authors = ["Andrew Gazelka "] 44 | edition.workspace = true 45 | name = "bedwars" 46 | publish = false 47 | readme = "README.md" 48 | version.workspace = true 49 | 50 | [target.'cfg(not(target_os = "windows"))'.dependencies] 51 | tikv-jemallocator.workspace = true 52 | -------------------------------------------------------------------------------- /crates/hyperion/benches/set.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeSet, HashSet, LinkedList, VecDeque}, 3 | hash::Hash, 4 | hint::black_box, 5 | }; 6 | 7 | use divan::Bencher; 8 | 9 | const LENS: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]; 10 | 11 | fn main() { 12 | divan::main(); 13 | } 14 | #[divan::bench( 15 | types = [ 16 | BTreeSet, 17 | HashSet, 18 | Vec, 19 | VecDeque, 20 | ], 21 | args = LENS, 22 | )] 23 | fn from_iter(bencher: Bencher<'_, '_>, len: usize) 24 | where 25 | T: FromIterator + Contains, 26 | { 27 | let max_elem = len * 4; 28 | 29 | let elems: T = (0..len).map(|_| fastrand::usize(..max_elem)).collect(); 30 | bencher.counter(len).bench_local(|| { 31 | let n = fastrand::usize(..max_elem); 32 | black_box(elems.contains_impl(&n)); 33 | }); 34 | } 35 | 36 | trait Contains { 37 | fn contains_impl(&self, t: &T) -> bool; 38 | } 39 | 40 | impl Contains for Vec { 41 | fn contains_impl(&self, t: &T) -> bool { 42 | self.contains(t) 43 | } 44 | } 45 | 46 | impl Contains for VecDeque { 47 | fn contains_impl(&self, t: &T) -> bool { 48 | self.contains(t) 49 | } 50 | } 51 | 52 | impl Contains for BTreeSet { 53 | fn contains_impl(&self, t: &T) -> bool { 54 | self.contains(t) 55 | } 56 | } 57 | 58 | impl Contains for HashSet { 59 | fn contains_impl(&self, t: &T) -> bool { 60 | self.contains(t) 61 | } 62 | } 63 | 64 | impl Contains for LinkedList { 65 | fn contains_impl(&self, t: &T) -> bool { 66 | self.contains(t) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/hyperion/src/storage/buf.rs: -------------------------------------------------------------------------------- 1 | //! Buffer implementations. todo: We might be able to scrap this for [`bytes::Buf`] in the future. 2 | 3 | /// # Safety 4 | /// - `get_contiguous` must return a slice of exactly `len` bytes long. 5 | /// - `advance` must advance the buffer by exactly `len` bytes. 6 | pub unsafe trait Buf { 7 | /// What type we get when we advance. For example, if we have a [`bytes::BytesMut`], we get a [`bytes::BytesMut`]. 8 | type Output; 9 | 10 | /// Get a contiguous slice of memory of length `len`. 11 | /// 12 | /// The returned slice must be exactly `len` bytes long. 13 | fn get_contiguous(&mut self, len: usize) -> &mut [u8]; 14 | 15 | /// Advance the buffer by exactly `len` bytes. 16 | fn advance(&mut self, len: usize) -> Self::Output; 17 | } 18 | 19 | unsafe impl Buf for bytes::BytesMut { 20 | type Output = Self; 21 | 22 | fn get_contiguous(&mut self, len: usize) -> &mut [u8] { 23 | // self.resize(len, 0); 24 | // self 25 | self.reserve(len); 26 | let cap = self.spare_capacity_mut(); 27 | let cap = unsafe { cap.assume_init_mut() }; 28 | cap 29 | } 30 | 31 | fn advance(&mut self, len: usize) -> Self::Output { 32 | unsafe { self.set_len(self.len() + len) }; 33 | self.split_to(len) 34 | } 35 | } 36 | 37 | unsafe impl Buf for Vec { 38 | type Output = (); 39 | 40 | fn get_contiguous(&mut self, len: usize) -> &mut [u8] { 41 | // self.resize(len, 0); 42 | // self 43 | self.reserve(len); 44 | let cap = self.spare_capacity_mut(); 45 | let cap = unsafe { cap.assume_init_mut() }; 46 | cap 47 | } 48 | 49 | fn advance(&mut self, len: usize) -> Self::Output { 50 | unsafe { self.set_len(self.len() + len) }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy VitePress site to Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: pages 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - name: Setup Node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | - name: Install pnpm 30 | uses: pnpm/action-setup@v2 31 | with: 32 | version: 9 33 | - name: Get pnpm store directory 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 37 | - name: Setup pnpm cache 38 | uses: actions/cache@v3 39 | with: 40 | path: ${{ env.STORE_PATH }} 41 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 42 | restore-keys: | 43 | ${{ runner.os }}-pnpm-store- 44 | - name: Setup Pages 45 | uses: actions/configure-pages@v4 46 | - name: Install dependencies 47 | run: pnpm install 48 | - name: Build with VitePress 49 | run: pnpm run docs:build 50 | - name: Upload artifact 51 | uses: actions/upload-pages-artifact@v3 52 | with: 53 | path: docs/.vitepress/dist 54 | 55 | deploy: 56 | environment: 57 | name: github-pages 58 | url: ${{ steps.deployment.outputs.page_url }} 59 | needs: build 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /crates/hyperion/src/net/agnostic/sound.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use glam::Vec3; 4 | use valence_protocol::{ 5 | packets::play, 6 | sound::{SoundCategory, SoundId}, 7 | }; 8 | 9 | use crate::PacketBundle; 10 | 11 | #[must_use] 12 | pub struct Sound { 13 | raw: play::PlaySoundS2c, 14 | } 15 | 16 | #[must_use] 17 | pub struct SoundBuilder { 18 | position: Vec3, 19 | pitch: f32, 20 | volume: f32, 21 | seed: Option, 22 | sound: valence_ident::Ident, 23 | } 24 | 25 | impl SoundBuilder { 26 | pub const fn pitch(mut self, pitch: f32) -> Self { 27 | self.pitch = pitch; 28 | self 29 | } 30 | 31 | pub const fn volume(mut self, volume: f32) -> Self { 32 | self.volume = volume; 33 | self 34 | } 35 | 36 | pub const fn seed(mut self, seed: i64) -> Self { 37 | self.seed = Some(seed); 38 | self 39 | } 40 | 41 | pub fn build(self) -> Sound { 42 | Sound { 43 | raw: play::PlaySoundS2c { 44 | id: SoundId::Direct { 45 | id: self.sound, 46 | range: None, 47 | }, 48 | position: (self.position * 8.0).as_ivec3(), 49 | volume: self.volume, 50 | pitch: self.pitch, 51 | seed: self.seed.unwrap_or_else(|| fastrand::i64(..)), 52 | category: SoundCategory::Master, 53 | }, 54 | } 55 | } 56 | } 57 | 58 | impl PacketBundle for &Sound { 59 | fn encode_including_ids(self, mut w: impl Write) -> anyhow::Result<()> { 60 | self.raw.encode_including_ids(&mut w) 61 | } 62 | } 63 | 64 | pub const fn sound(sound: valence_ident::Ident, position: Vec3) -> SoundBuilder { 65 | SoundBuilder { 66 | position, 67 | pitch: 1.0, 68 | volume: 1.0, 69 | seed: None, 70 | sound, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/packet-channel/benches/packet_channel.rs: -------------------------------------------------------------------------------- 1 | use std::{hint::black_box, sync::mpsc}; 2 | 3 | use divan::Bencher; 4 | 5 | const LENS: &[usize] = &[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]; 6 | 7 | fn main() { 8 | divan::main(); 9 | } 10 | 11 | // This isn't a completely fair comparison since mpsc::sync_channel is a bounded channel and 12 | // packet_channel is an unbounded channel and this mpsc variant is skipping the VarInt length prefix 13 | // decoding that packet_channel does, but packet_channel is still faster despite this 14 | 15 | #[divan::bench( 16 | args = LENS, 17 | )] 18 | fn send_packet_mpsc(bencher: Bencher<'_, '_>, len: usize) { 19 | let packet = [0u8; 64]; 20 | let (writer, reader) = mpsc::sync_channel(*LENS.last().unwrap()); 21 | 22 | bencher.counter(len).bench_local(|| { 23 | for _ in 0..black_box(len) { 24 | writer 25 | .send(Box::<[u8]>::from(black_box(packet).as_slice())) 26 | .unwrap(); 27 | } 28 | let mut total_packets = 0; 29 | while let Ok(packet) = reader.try_recv() { 30 | black_box(packet); 31 | total_packets += 1; 32 | } 33 | assert_eq!(total_packets, len); 34 | }); 35 | } 36 | 37 | #[divan::bench( 38 | args = LENS, 39 | )] 40 | fn send_packet_channel(bencher: Bencher<'_, '_>, len: usize) { 41 | let mut packet = [0u8; 65]; 42 | // Add the length prefix 43 | packet[0] = 64; 44 | 45 | let (mut writer, mut reader) = packet_channel::channel(4096); 46 | 47 | bencher.counter(len).bench_local(|| { 48 | for _ in 0..black_box(len) { 49 | writer.send(&black_box(packet)[..]).unwrap(); 50 | } 51 | let mut total_packets = 0; 52 | while let Some(packet) = reader.try_recv() { 53 | black_box(packet); 54 | total_packets += 1; 55 | } 56 | assert_eq!(total_packets, len); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /events/bedwars/src/plugin/regeneration.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use hyperion::{ 3 | net::Compose, 4 | simulation::{metadata::living_entity::Health, packet_state}, 5 | }; 6 | use hyperion_utils::Prev; 7 | 8 | const MAX_HEALTH: f32 = 20.0; 9 | 10 | pub struct RegenerationPlugin; 11 | 12 | #[derive(Component, Default, Copy, Clone, Debug)] 13 | pub struct LastDamaged { 14 | pub tick: i64, 15 | } 16 | 17 | fn initialize_player( 18 | trigger: Trigger<'_, OnAdd, packet_state::Play>, 19 | mut commands: Commands<'_, '_>, 20 | ) { 21 | commands 22 | .entity(trigger.target()) 23 | .insert(LastDamaged::default()); 24 | } 25 | 26 | fn regenerate( 27 | query: Query<'_, '_, (&mut LastDamaged, &Prev, &mut Health)>, 28 | compose: Res<'_, Compose>, 29 | ) { 30 | let current_tick = compose.global().tick; 31 | 32 | for (mut last_damaged, prev_health, mut health) in query { 33 | if *health < **prev_health { 34 | last_damaged.tick = current_tick; 35 | } 36 | 37 | let ticks_since_damage = current_tick - last_damaged.tick; 38 | 39 | if health.is_dead() { 40 | return; 41 | } 42 | 43 | // Calculate regeneration rate based on time since last damage 44 | let base_regen = 0.01; // Base regeneration per tick 45 | let ramp_factor = 0.0001_f32; // Increase in regeneration per tick 46 | let max_regen = 0.1; // Maximum regeneration per tick 47 | 48 | let regen_rate = ramp_factor 49 | .mul_add(ticks_since_damage as f32, base_regen) 50 | .min(max_regen); 51 | 52 | // Apply regeneration, capped at max health 53 | health.heal(regen_rate); 54 | **health = health.min(MAX_HEALTH); 55 | } 56 | } 57 | 58 | impl Plugin for RegenerationPlugin { 59 | fn build(&self, app: &mut App) { 60 | app.add_observer(initialize_player); 61 | app.add_systems(FixedPostUpdate, regenerate); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/player.rs: -------------------------------------------------------------------------------- 1 | // Index Type Meaning Default 2 | // 15 Float (3) Additional Hearts 0.0 3 | // 16 VarInt (1) Score 0 4 | // 17 Byte (0) The Displayed Skin Parts bit mask that is sent in Client Settings 0 5 | // Bit mask Meaning 6 | // 0x01 Cape enabled 7 | // 0x02 Jacket enabled 8 | // 0x04 Left sleeve enabled 9 | // 0x08 Right sleeve enabled 10 | // 0x10 Left pants leg enabled 11 | // 0x20 Right pants leg enabled 12 | // 0x40 Hat enabled 13 | // 0x80 Unused 14 | // 18 Byte (0) Main hand (0 : Left, 1 : Right) 1 15 | // 19 NBT (16) Left shoulder entity data (for occupying parrot) Empty 16 | // 20 NBT (16) Right shoulder entity data (for occupying parrot) Empty 17 | 18 | use bevy::prelude::*; 19 | use valence_protocol::VarInt; 20 | 21 | use super::Metadata; 22 | use crate::define_and_register_components; 23 | 24 | // Example usage: 25 | define_and_register_components! { 26 | // 15 Float (3) Additional Hearts 0.0 27 | 15, AdditionalHearts -> f32, 28 | 29 | // 16 VarInt (1) Score 0 30 | 16, Score -> VarInt, 31 | 32 | // 17 Byte (0) The Displayed Skin Parts bit mask that is sent in Client Settings 0 33 | 17, DisplayedSkinParts -> u8, 34 | 35 | // 18 Byte (0) Main hand (0 : Left, 1 : Right) 1 36 | 18, MainHand -> u8, 37 | 38 | // 19 NBT (16) Left shoulder entity data (for occupying parrot) Empty 39 | // 19, LeftShoulderEntityData -> Option>, 40 | 41 | // 20 NBT (16) Right shoulder entity data (for occupying parrot) Empty 42 | // 20, RightShoulderEntityData -> Option>, 43 | } 44 | 45 | impl Default for AdditionalHearts { 46 | fn default() -> Self { 47 | Self::new(0.0) 48 | } 49 | } 50 | 51 | impl Default for Score { 52 | fn default() -> Self { 53 | Self::new(VarInt(0)) 54 | } 55 | } 56 | 57 | impl Default for DisplayedSkinParts { 58 | fn default() -> Self { 59 | Self::new(0) 60 | } 61 | } 62 | 63 | impl Default for MainHand { 64 | fn default() -> Self { 65 | Self::new(1) // 1 = Right hand 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/entity/flags.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use derive_more::Deref; 3 | 4 | use crate::simulation::metadata::Metadata; 5 | 6 | // todo: can be u8 7 | #[derive(Component, PartialEq, Eq, Copy, Clone, Debug, Deref)] 8 | pub struct EntityFlags { 9 | value: u8, 10 | } 11 | 12 | impl Default for EntityFlags { 13 | fn default() -> Self { 14 | Self::new() 15 | } 16 | } 17 | 18 | impl EntityFlags { 19 | pub const CROUCHING: Self = Self { value: 0x02 }; 20 | pub const FLYING_WITH_ELYTRA: Self = Self { value: 0x80 }; 21 | pub const GLOWING: Self = Self { value: 0x40 }; 22 | pub const INVISIBLE: Self = Self { value: 0x20 }; 23 | pub const ON_FIRE: Self = Self { value: 0x01 }; 24 | // 0x04 skipped (previously riding) 25 | pub const SPRINTING: Self = Self { value: 0x08 }; 26 | pub const SWIMMING: Self = Self { value: 0x10 }; 27 | 28 | const fn new() -> Self { 29 | Self { value: 0 } 30 | } 31 | } 32 | 33 | impl std::ops::BitOrAssign for EntityFlags { 34 | fn bitor_assign(&mut self, rhs: Self) { 35 | self.value |= rhs.value; 36 | } 37 | } 38 | 39 | impl std::ops::BitAndAssign for EntityFlags { 40 | fn bitand_assign(&mut self, rhs: Self) { 41 | self.value &= rhs.value; 42 | } 43 | } 44 | 45 | impl std::ops::BitOr for EntityFlags { 46 | type Output = Self; 47 | 48 | fn bitor(self, rhs: Self) -> Self { 49 | Self { 50 | value: self.value | rhs.value, 51 | } 52 | } 53 | } 54 | 55 | impl std::ops::BitAnd for EntityFlags { 56 | type Output = Self; 57 | 58 | fn bitand(self, rhs: Self) -> Self { 59 | Self { 60 | value: self.value & rhs.value, 61 | } 62 | } 63 | } 64 | 65 | impl std::ops::Not for EntityFlags { 66 | type Output = Self; 67 | 68 | fn not(self) -> Self { 69 | Self { value: !self.value } 70 | } 71 | } 72 | 73 | impl Metadata for EntityFlags { 74 | type Type = u8; 75 | 76 | const INDEX: u8 = 0; 77 | 78 | fn to_type(self) -> Self::Type { 79 | self.value 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/hyperion-gui/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use bevy::prelude::*; 4 | use hyperion::{ 5 | simulation::{Uuid, entity_kind::EntityKind}, 6 | valence_protocol::packets::play::{ 7 | click_slot_c2s::ClickMode, close_screen_s2c::CloseScreenS2c, 8 | }, 9 | }; 10 | use hyperion_inventory::{Inventory, OpenInventory}; 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Clone, Debug, Serialize, Deserialize)] 14 | pub struct InventoryItem { 15 | pub id: String, 16 | pub name: String, 17 | pub lore: Option, 18 | pub quantity: u32, 19 | } 20 | 21 | #[derive(Debug, Clone, Copy)] 22 | pub enum ContainerType { 23 | Chest, 24 | ShulkerBox, 25 | Furnace, 26 | Dispenser, 27 | Hopper, 28 | } 29 | 30 | #[derive(Component, Clone)] 31 | pub struct Gui { 32 | entity: Entity, 33 | items: HashMap, 34 | pub id: u64, 35 | } 36 | 37 | impl Gui { 38 | #[must_use] 39 | pub fn new(inventory: Inventory, world: &mut World, id: u64) -> Self { 40 | let uuid = Uuid::new_v4(); 41 | 42 | let entity = world 43 | .spawn((EntityKind::BlockDisplay, uuid, inventory)) 44 | .id(); 45 | 46 | Self { 47 | entity, 48 | items: HashMap::new(), 49 | id, 50 | } 51 | } 52 | 53 | pub fn add_command(&mut self, slot: usize, on_click: fn(Entity, ClickMode)) { 54 | self.items.insert(slot, on_click); 55 | } 56 | 57 | pub fn init(&mut self, _world: &mut World) { 58 | todo!() 59 | } 60 | 61 | pub fn open(&self, world: &mut World, player: Entity) { 62 | world 63 | .entity_mut(player) 64 | .insert(OpenInventory::new(self.entity)); 65 | } 66 | 67 | pub fn open_deferred(&self, commands: &mut Commands<'_, '_>, player: Entity) { 68 | commands 69 | .entity(player) 70 | .insert(OpenInventory::new(self.entity)); 71 | } 72 | 73 | pub fn handle_close(&mut self, _player: Entity, _close_packet: CloseScreenS2c) { 74 | todo!() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tools/packet-inspector/README.md: -------------------------------------------------------------------------------- 1 | # Packet Inspector 2 | 3 | ![packet inspector screenshot](https://raw.githubusercontent.com/valence-rs/valence/main/assets/packet-inspector.png) 4 | 5 | The packet inspector is a Minecraft proxy for viewing the contents of packets as 6 | they are sent/received. It uses Valence's protocol facilities to display packet 7 | contents. This was made for three purposes: 8 | 9 | - Check that packets between Valence and client are matching your expectations. 10 | - Check that packets between vanilla server and client are parsed correctly by 11 | Valence. 12 | - Understand how the protocol works between the vanilla server and client. 13 | 14 | # Usage 15 | 16 | Firstly, we should have a server running that we're going to be 17 | proxying/inspecting. 18 | 19 | ```sh 20 | cargo r -r --example game_of_life 21 | ``` 22 | 23 | Next up, we need to run the proxy server, To launch in a GUI environment, simply run `packet_inspector`. 24 | 25 | ```sh 26 | cargo r -r -p packet_inspector 27 | ``` 28 | 29 | Then click the "Start Listening" button in the top left of the UI. 30 | 31 | The client can now connect to `localhost:25566`. You should see packets streaming in on the GUI. 32 | 33 | ## Quick start with Vanilla Server via Docker 34 | 35 | Start the server 36 | 37 | ```sh 38 | docker run -e EULA=TRUE -e ONLINE_MODE=false -e ANNOUNCE_PLAYER_ACHIEVEMENTS=false -e GENERATE_STRUCTURES=false -e SPAWN_ANIMALS=false -e SPAWN_MONSTERS=false -e SPAWN_NPCS=false -e SPAWN_PROTECTION=0 -e VIEW_DISTANCE=16 -e MODE=creative -e LEVEL_TYPE=flat -e RCON_CMDS_STARTUP="gamerule doWeatherCycle false" -d -p 25565:25565 --name mc itzg/minecraft-server 39 | ``` 40 | 41 | View server logs 42 | 43 | ```sh 44 | docker logs -f mc 45 | ``` 46 | 47 | Server Rcon 48 | 49 | ```sh 50 | docker exec -i mc rcon-cli 51 | ``` 52 | 53 | In a separate terminal, start the packet inspector. 54 | 55 | ```sh 56 | cargo r -r -p packet_inspector --no-default-features --features cli -- 127.0.0.1:25566 127.0.0.1:25565 57 | ``` 58 | 59 | Open Minecraft and connect to `localhost:25566`. 60 | 61 | Clean up 62 | 63 | ``` 64 | docker stop mc 65 | docker rm mc 66 | ``` 67 | -------------------------------------------------------------------------------- /crates/hyperion-text/src/event.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | 6 | use crate::Text; 7 | 8 | /// Action to take on click of the text. 9 | #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] 10 | #[serde(tag = "action", content = "value", rename_all = "snake_case")] 11 | pub enum ClickEvent<'a> { 12 | /// Opens an URL 13 | OpenUrl(Cow<'a, str>), 14 | /// Only usable by internal servers for security reasons. 15 | OpenFile(Cow<'a, str>), 16 | /// Sends a chat command. Doesn't actually have to be a command, can be a 17 | /// normal chat message. 18 | RunCommand(Cow<'a, str>), 19 | /// Replaces the contents of the chat box with the text, not necessarily a 20 | /// command. 21 | SuggestCommand(Cow<'a, str>), 22 | /// Only usable within written books. Changes the page of the book. Indexing 23 | /// starts at 1. 24 | ChangePage(i32), 25 | /// Copies the given text to clipboard 26 | CopyToClipboard(Cow<'a, str>), 27 | } 28 | 29 | /// Action to take when mouse-hovering on the text. 30 | #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] 31 | #[serde(tag = "action", content = "contents", rename_all = "snake_case")] 32 | #[expect(clippy::enum_variant_names)] 33 | pub enum HoverEvent<'a> { 34 | /// Displays a tooltip with the given text. 35 | ShowText(Text<'a>), 36 | /// Shows an item. 37 | ShowItem { 38 | /// Resource identifier of the item (ident) 39 | id: Cow<'a, str>, 40 | /// Number of the items in the stack 41 | count: Option, 42 | /// NBT information about the item (sNBT format) 43 | tag: Cow<'a, str>, 44 | }, 45 | /// Shows an entity. 46 | ShowEntity { 47 | /// The entity's UUID 48 | id: Uuid, 49 | /// Resource identifier of the entity 50 | #[serde(rename = "type")] 51 | #[serde(default, skip_serializing_if = "Option::is_none")] 52 | kind: Option>, 53 | /// Optional custom name for the entity 54 | #[serde(default, skip_serializing_if = "Option::is_none")] 55 | name: Option>, 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /crates/hyperion-proxy/src/data.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic, atomic::AtomicBool}; 2 | 3 | use anyhow::bail; 4 | use bytes::Bytes; 5 | use slotmap::{KeyData, new_key_type}; 6 | 7 | new_key_type! { 8 | pub struct PlayerId; 9 | } 10 | 11 | impl From for PlayerId { 12 | fn from(id: u64) -> Self { 13 | let raw = KeyData::from_ffi(id); 14 | Self::from(raw) 15 | } 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct PlayerHandle { 20 | writer: kanal::AsyncSender, 21 | 22 | /// Whether the player is allowed to send broadcasts. 23 | /// 24 | /// This exists because the player is not automatically in the play state, 25 | /// and if they are not in the play state and they receive broadcasts, 26 | /// they will get packets that it deems are invalid because the broadcasts are using the play 27 | /// state and play IDs. 28 | can_receive_broadcasts: AtomicBool, 29 | } 30 | 31 | impl PlayerHandle { 32 | #[must_use] 33 | pub const fn new(writer: kanal::AsyncSender) -> Self { 34 | Self { 35 | writer, 36 | can_receive_broadcasts: AtomicBool::new(false), 37 | } 38 | } 39 | 40 | pub fn shutdown(&self) { 41 | // Ignore error for if the channel is already closed 42 | let _ = self.writer.close(); 43 | } 44 | 45 | pub fn enable_receive_broadcasts(&self) { 46 | self.can_receive_broadcasts 47 | .store(true, atomic::Ordering::Relaxed); 48 | } 49 | 50 | pub fn can_receive_broadcasts(&self) -> bool { 51 | self.can_receive_broadcasts.load(atomic::Ordering::Relaxed) 52 | } 53 | 54 | pub fn send(&self, bytes: Bytes) -> anyhow::Result<()> { 55 | match self.writer.try_send(bytes) { 56 | Ok(true) => Ok(()), 57 | 58 | Ok(false) => { 59 | let is_full = self.writer.is_full(); 60 | self.shutdown(); 61 | bail!("failed to send packet to player, channel is full: {is_full}"); 62 | } 63 | Err(e) => { 64 | self.shutdown(); 65 | bail!("failed to send packet to player: {e}"); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/hyperion-item/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use derive_more::Deref; 3 | use hyperion::{ingress, simulation::event::InteractEvent}; 4 | use hyperion_inventory::PlayerInventory; 5 | use tracing::error; 6 | use valence_protocol::nbt; 7 | 8 | pub mod builder; 9 | 10 | pub struct ItemPlugin; 11 | 12 | /// Event sent when an item with an NBT is clicked in the hotbar 13 | #[derive(Event, Deref)] 14 | pub struct NbtInteractEvent { 15 | pub handler: Entity, 16 | 17 | #[deref] 18 | pub event: InteractEvent, 19 | } 20 | 21 | fn handle_interact( 22 | mut events: EventReader<'_, '_, InteractEvent>, 23 | query: Query<'_, '_, &PlayerInventory>, 24 | mut event_writer: EventWriter<'_, NbtInteractEvent>, 25 | ) { 26 | for event in events.read() { 27 | let inventory = match query.get(event.client) { 28 | Ok(inventory) => inventory, 29 | Err(e) => { 30 | error!("failed to handle interact event: query failed: {e}"); 31 | continue; 32 | } 33 | }; 34 | 35 | let stack = &inventory.get_cursor().stack; 36 | 37 | if stack.is_empty() { 38 | return; 39 | } 40 | 41 | let Some(nbt) = stack.nbt.as_ref() else { 42 | return; 43 | }; 44 | 45 | let Some(handler) = nbt.get("Handler") else { 46 | return; 47 | }; 48 | 49 | let nbt::Value::Long(id) = handler else { 50 | return; 51 | }; 52 | 53 | let id: u64 = bytemuck::cast(*id); 54 | 55 | let Ok(handler) = Entity::try_from_bits(id) else { 56 | error!( 57 | "failed to handle interact event: nbt handler field contains an invalid entity id \ 58 | {id}" 59 | ); 60 | return; 61 | }; 62 | 63 | event_writer.write(NbtInteractEvent { 64 | handler, 65 | event: event.clone(), 66 | }); 67 | } 68 | } 69 | 70 | impl Plugin for ItemPlugin { 71 | fn build(&self, app: &mut App) { 72 | app.add_event::(); 73 | app.add_systems(FixedUpdate, handle_interact.after(ingress::decode::play)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/hyperion/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Defined the [`Global`] struct which is used to store global data which defines a [`crate::HyperionCore`] 2 | use std::{ 3 | sync::{Arc, atomic::AtomicUsize}, 4 | time::Duration, 5 | }; 6 | 7 | use bevy::prelude::*; 8 | use libdeflater::CompressionLvl; 9 | use valence_protocol::CompressionThreshold; 10 | 11 | pub mod command_channel; 12 | pub mod config; 13 | pub mod runtime; 14 | pub mod util; 15 | 16 | /// Shared data that is shared between the ECS framework and the IO thread. 17 | pub struct Shared { 18 | /// The compression level to use for the server. This is how long a packet needs to be before it is compressed. 19 | pub compression_threshold: CompressionThreshold, 20 | 21 | /// The compression level to use for the server. This is the [`libdeflater`] compression level. 22 | pub compression_level: CompressionLvl, 23 | } 24 | 25 | #[derive(Component)] 26 | pub struct Global { 27 | /// The current tick of the game. This is incremented every 50 ms. 28 | pub tick: i64, 29 | 30 | /// The maximum amount of time a player is resistant to being hurt. This is weird as this is 20 in vanilla 31 | /// Minecraft. 32 | /// However, the check to determine if a player can be hurt actually looks at this value divided by 2 33 | pub max_hurt_resistant_time: u16, 34 | 35 | /// Data shared between the IO thread and the ECS framework. 36 | pub shared: Arc, 37 | 38 | /// The amount of time from the last packet a player has sent before the server will kick them. 39 | pub keep_alive_timeout: Duration, 40 | 41 | /// The amount of time the last tick took in milliseconds. 42 | pub ms_last_tick: f32, 43 | 44 | pub player_count: AtomicUsize, 45 | } 46 | 47 | impl Global { 48 | /// Creates a new [`Global`] with the given shared data. 49 | #[must_use] 50 | pub const fn new(shared: Arc) -> Self { 51 | Self { 52 | tick: 0, 53 | max_hurt_resistant_time: 20, // actually kinda like 10 vanilla mc is weird 54 | shared, 55 | keep_alive_timeout: Duration::from_secs(20), 56 | ms_last_tick: 0.0, 57 | player_count: AtomicUsize::new(0), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/hyperion-utils/src/cached_save.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Context; 4 | use bevy::prelude::*; 5 | use futures_util::stream::StreamExt; 6 | use sha2::Digest; 7 | use tar::Archive; 8 | use tokio_util::io::{StreamReader, SyncIoBridge}; 9 | use tracing::info; 10 | 11 | use crate::AppId; 12 | 13 | pub fn cached_save( 14 | world: &World, 15 | url: U, 16 | ) -> impl Future> + 'static { 17 | let project_dirs = world.resource::(); 18 | 19 | let cache = project_dirs.cache_dir(); 20 | 21 | let mut hasher = sha2::Sha256::new(); 22 | let url_str = url.as_str().to_string(); 23 | Digest::update(&mut hasher, url_str.as_bytes()); 24 | // Get the final hash result 25 | let url_sha = hasher.finalize(); 26 | let url_sha = hex::encode(url_sha); 27 | 28 | let directory = cache.join(url_sha); 29 | 30 | async move { 31 | if directory.exists() { 32 | info!("using cached NewYork load"); 33 | } else { 34 | // download 35 | let response = reqwest::get(url) 36 | .await 37 | .with_context(|| format!("failed to get {url_str}"))?; 38 | 39 | let byte_stream = response.bytes_stream(); 40 | // Convert the byte stream into an AsyncRead 41 | 42 | let reader = StreamReader::new(byte_stream.map(|result| { 43 | result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) 44 | })); 45 | 46 | let directory = directory.clone(); 47 | let handle = tokio::task::spawn_blocking(move || { 48 | let reader = SyncIoBridge::new(reader); 49 | let reader = std::io::BufReader::new(reader); 50 | let reader = flate2::read::GzDecoder::new(reader); 51 | 52 | // Create the archive in the blocking context 53 | let mut archive = Archive::new(reader); 54 | 55 | archive 56 | .unpack(&directory) 57 | .context("failed to unpack archive")?; 58 | 59 | anyhow::Ok(()) 60 | }); 61 | 62 | handle.await??; 63 | } 64 | 65 | Ok(directory) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crates/hyperion-proxy/src/server_sender.rs: -------------------------------------------------------------------------------- 1 | use std::io::IoSlice; 2 | 3 | use rkyv::util::AlignedVec; 4 | use tokio::io::AsyncWrite; 5 | use tracing::{Instrument, trace_span, warn}; 6 | 7 | use crate::util::AsyncWriteVectoredExt; 8 | 9 | pub type ServerSender = kanal::AsyncSender; 10 | 11 | // todo: probably makes sense for caller to encode bytes 12 | #[must_use] 13 | pub fn launch_server_writer(mut write: impl AsyncWrite + Unpin + Send + 'static) -> ServerSender { 14 | let (tx, rx) = kanal::bounded_async::(32_768); 15 | 16 | tokio::spawn( 17 | async move { 18 | let mut lengths: Vec<[u8; 8]> = Vec::new(); 19 | let mut messages = Vec::new(); 20 | 21 | // todo: remove allocation is there an easy way to do this? 22 | let mut io_slices = Vec::new(); 23 | 24 | while let Ok(message) = rx.recv().await { 25 | let len = message.len() as u64; 26 | 27 | lengths.push(len.to_be_bytes()); 28 | messages.push(message); 29 | 30 | while let Ok(Some(message)) = rx.try_recv() { 31 | let len = message.len() as u64; 32 | lengths.push(len.to_be_bytes()); 33 | messages.push(message); 34 | } 35 | 36 | for (message, length) in messages.iter().zip(lengths.iter()) { 37 | let len = IoSlice::new(length); 38 | let msg = IoSlice::new(message); 39 | 40 | // todo: is there a way around this? 41 | let len = unsafe { core::mem::transmute::, IoSlice<'static>>(len) }; 42 | let msg = unsafe { core::mem::transmute::, IoSlice<'static>>(msg) }; 43 | 44 | io_slices.push(len); 45 | io_slices.push(msg); 46 | } 47 | 48 | if let Err(e) = write.write_vectored_all(&mut io_slices).await { 49 | warn!("failed to write to server: {e}"); 50 | return; 51 | } 52 | 53 | lengths.clear(); 54 | messages.clear(); 55 | io_slices.clear(); 56 | } 57 | } 58 | .instrument(trace_span!("server_writer_loop")), 59 | ); 60 | 61 | tx 62 | } 63 | -------------------------------------------------------------------------------- /crates/hyperion-command/src/component.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | use bevy::prelude::*; 4 | use derive_more::{Deref, DerefMut}; 5 | use hyperion::simulation::packet::play; 6 | use hyperion_utils::ApplyWorld; 7 | use indexmap::IndexMap; 8 | 9 | pub trait ExecutableCommand: ApplyWorld { 10 | /// Executes a command triggered by a player 11 | fn execute(&mut self, world: &World, execution: &play::CommandExecution); 12 | } 13 | 14 | pub struct CommandHandler { 15 | pub executable: Box, 16 | pub tab_complete: fn(&World, &play::RequestCommandCompletions), 17 | pub has_permissions: fn(&World, Entity) -> bool, 18 | } 19 | 20 | pub struct CommandRegistryInner { 21 | pub(crate) commands: IndexMap, 22 | } 23 | 24 | impl CommandRegistryInner { 25 | pub fn register(&mut self, name: impl Into, handler: CommandHandler) { 26 | let name = name.into(); 27 | self.commands.insert(name, handler); 28 | } 29 | 30 | pub fn all(&self) -> impl Iterator { 31 | self.commands.keys().map(String::as_str) 32 | } 33 | 34 | /// Returns an iterator over the names of commands (`&str`) that the given entity (`caller`) 35 | /// has permission to execute. 36 | pub fn get_permitted(&self, world: &World, caller: Entity) -> impl Iterator { 37 | self.commands 38 | .iter() 39 | .filter_map(move |(cmd_name, handler)| { 40 | if (handler.has_permissions)(world, caller) { 41 | Some(cmd_name) 42 | } else { 43 | None 44 | } 45 | }) 46 | .map(String::as_str) 47 | } 48 | } 49 | 50 | /// Registry storing a list of commands. 51 | /// 52 | /// This registry is locked by a [`Mutex`]. See the `execute_commands` system for justification. 53 | /// Consider accessing this resource using [`ResMut`] and [`Mutex::get_mut`]. 54 | #[derive(Resource, Deref, DerefMut)] 55 | pub struct CommandRegistry(Mutex); 56 | 57 | pub struct CommandComponentPlugin; 58 | 59 | impl Plugin for CommandComponentPlugin { 60 | fn build(&self, app: &mut App) { 61 | app.insert_resource(CommandRegistry(Mutex::new(CommandRegistryInner { 62 | commands: IndexMap::default(), 63 | }))); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/hyperion/tests/entity.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | clippy::print_stdout, 3 | reason = "the purpose of not having printing to stdout is so that tracing is used properly \ 4 | for the core libraries. These are tests, so it doesn't matter" 5 | )] 6 | 7 | use bevy::{app::FixedMain, prelude::*}; 8 | use hyperion::{ 9 | HyperionCore, 10 | simulation::{Owner, Position, Uuid, Velocity, entity_kind::EntityKind}, 11 | }; 12 | use serial_test::serial; 13 | 14 | #[test] 15 | #[serial] 16 | fn arrow() { 17 | let mut app = App::new(); 18 | app.add_plugins(HyperionCore); 19 | 20 | let world = app.world_mut(); 21 | let owner = world.spawn_empty().id(); 22 | let mut arrow = world.spawn(EntityKind::Arrow); 23 | let arrow_id = arrow.id(); 24 | let arrow_uuid = arrow 25 | .get::() 26 | .expect("All entities should automatically be given a UUID"); 27 | 28 | assert_ne!( 29 | arrow_uuid.0, 30 | uuid::Uuid::nil(), 31 | "The UUID should not be nil" 32 | ); 33 | 34 | arrow.insert(( 35 | Velocity::new(0.0, 1.0, 0.0), 36 | Position::new(0.0, 20.0, 0.0), 37 | Owner::new(owner), 38 | )); 39 | 40 | FixedMain::run_fixed_main(world); 41 | 42 | // since velocity.y is 1.0, the arrow should be at y = 20.0 + (1.0 * drag - gravity) = 20.947525 43 | assert_eq!( 44 | *world.entity(arrow_id).get::().unwrap(), 45 | Position::new(0.0, 20.947_525, 0.0) 46 | ); 47 | 48 | FixedMain::run_fixed_main(world); 49 | 50 | // gravity! drag! this is what was returned from the test but I am unsure if it actually 51 | // what we should be getting 52 | // todo: make a bunch more tests and compare to the vanilla velocity and positions 53 | assert_eq!( 54 | *world.entity(arrow_id).get::().unwrap(), 55 | Position::new(0.0, 21.842_705, 0.0) 56 | ); 57 | } 58 | 59 | #[test] 60 | #[serial] 61 | fn with_uuid() { 62 | let mut app = App::new(); 63 | app.add_plugins(HyperionCore); 64 | 65 | let world = app.world_mut(); 66 | let uuid = Uuid::new_v4(); 67 | let arrow = world.spawn((uuid, EntityKind::Arrow)); 68 | let arrow_uuid = *arrow.get::().unwrap(); 69 | 70 | assert_eq!( 71 | arrow_uuid, uuid, 72 | "The entity UUID should not be overwritten with a randomly generated UUID" 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /events/bedwars/src/command/chest.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::{ItemKind, ItemStack, simulation::entity_kind::EntityKind}; 4 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 5 | use hyperion_gui::Gui; 6 | use hyperion_inventory::{Inventory, ItemSlot}; 7 | use tracing::debug; 8 | use valence_protocol::packets::play::open_screen_s2c::WindowType; 9 | 10 | #[derive(Parser, CommandPermission, Debug)] 11 | #[command(name = "chest")] 12 | #[command_permission(group = "Normal")] 13 | pub struct ChestCommand; 14 | 15 | impl MinecraftCommand for ChestCommand { 16 | type State = SystemState<( 17 | Query<'static, 'static, &'static Gui>, 18 | Commands<'static, 'static>, 19 | )>; 20 | 21 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 22 | let (query, mut commands) = state.get(world); 23 | 24 | for gui in &query { 25 | if gui.id == 28 { 26 | gui.open_deferred(&mut commands, caller); 27 | return; 28 | } 29 | } 30 | 31 | debug!("Creating new GUI"); 32 | let mut gui_inventory = 33 | Inventory::new(27, "Test Chest".to_string(), WindowType::Generic9x3, false); 34 | 35 | let item = ItemStack::new(ItemKind::GoldIngot, 64, None); 36 | 37 | gui_inventory.set(13, item).unwrap(); 38 | gui_inventory 39 | .set(14, ItemStack::new(ItemKind::Diamond, 64, None)) 40 | .unwrap(); 41 | gui_inventory 42 | .set(15, ItemStack::new(ItemKind::IronIngot, 64, None)) 43 | .unwrap(); 44 | gui_inventory 45 | .set(16, ItemStack::new(ItemKind::Coal, 64, None)) 46 | .unwrap(); 47 | gui_inventory 48 | .set(17, ItemStack::new(ItemKind::Emerald, 64, None)) 49 | .unwrap(); 50 | gui_inventory 51 | .set(18, ItemStack::new(ItemKind::GoldIngot, 64, None)) 52 | .unwrap(); 53 | gui_inventory 54 | .set_slot(19, ItemSlot::new(ItemKind::Diamond, 64, None, Some(true))) 55 | .unwrap(); 56 | 57 | commands.queue(move |world: &mut World| { 58 | let gui = Gui::new(gui_inventory, world, 28); 59 | 60 | gui.open(world, caller); 61 | 62 | world.spawn((EntityKind::Gui, gui)); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/hyperion/src/net/packets.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, io::Write}; 2 | 3 | use uuid::Uuid; 4 | use valence_protocol::{ 5 | Decode, DecodeBytesAuto, Encode, ItemStack, Packet, VarInt, 6 | packets::play::{ 7 | boss_bar_s2c::{BossBarColor, BossBarDivision, BossBarFlags}, 8 | entity_equipment_update_s2c::EquipmentEntry, 9 | }, 10 | }; 11 | 12 | #[derive(Clone, PartialEq, Debug, Packet, DecodeBytesAuto)] 13 | pub struct EntityEquipmentUpdateS2c<'a> { 14 | pub entity_id: VarInt, 15 | pub equipment: Cow<'a, [EquipmentEntry]>, 16 | } 17 | 18 | impl Encode for EntityEquipmentUpdateS2c<'_> { 19 | fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { 20 | self.entity_id.encode(&mut w)?; 21 | 22 | for i in 0..self.equipment.len() { 23 | let slot = self.equipment[i].slot; 24 | if i == self.equipment.len() - 1 { 25 | slot.encode(&mut w)?; 26 | } else { 27 | (slot | -128).encode(&mut w)?; 28 | } 29 | self.equipment[i].item.encode(&mut w)?; 30 | } 31 | 32 | Ok(()) 33 | } 34 | } 35 | 36 | impl Decode for EntityEquipmentUpdateS2c<'static> { 37 | fn decode(r: &mut &[u8]) -> anyhow::Result { 38 | let entity_id = VarInt::decode(r)?; 39 | 40 | let mut equipment = vec![]; 41 | 42 | loop { 43 | let slot = i8::decode(r)?; 44 | let item = ItemStack::decode(r)?; 45 | equipment.push(EquipmentEntry { 46 | slot: slot & 127, 47 | item, 48 | }); 49 | if slot & -128 == 0 { 50 | break; 51 | } 52 | } 53 | 54 | Ok(Self { 55 | entity_id, 56 | equipment: Cow::Owned(equipment), 57 | }) 58 | } 59 | } 60 | 61 | #[derive(Clone, Debug, Encode, Packet)] 62 | pub struct BossBarS2c<'a> { 63 | pub id: Uuid, 64 | pub action: BossBarAction<'a>, 65 | } 66 | 67 | #[derive(Clone, PartialEq, Debug, Encode)] 68 | pub enum BossBarAction<'a> { 69 | Add { 70 | title: hyperion_text::Text<'a>, 71 | health: f32, 72 | color: BossBarColor, 73 | division: BossBarDivision, 74 | flags: BossBarFlags, 75 | }, 76 | Remove, 77 | UpdateHealth(f32), 78 | UpdateTitle(hyperion_text::Text<'a>), 79 | UpdateStyle(BossBarColor, BossBarDivision), 80 | UpdateFlags(BossBarFlags), 81 | } 82 | -------------------------------------------------------------------------------- /tools/packet-inspector/src/app/hex_viewer.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use super::{SharedState, Tab, View}; 4 | 5 | pub struct HexView; 6 | 7 | impl Tab for HexView { 8 | fn new() -> Self { 9 | Self {} 10 | } 11 | 12 | fn name(&self) -> &'static str { 13 | "Hex Viewer" 14 | } 15 | } 16 | 17 | impl View for HexView { 18 | fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) { 19 | let mut buf = [0_u8; 16]; 20 | let mut count = 0; 21 | 22 | let packets = state.packets.read().unwrap(); 23 | let Some(packet_index) = state.selected_packet else { 24 | return; 25 | }; 26 | 27 | let bytes = &packets[packet_index].data.as_ref().unwrap(); 28 | let mut file = &(*bytes).clone()[..]; 29 | 30 | egui::Grid::new("hex_grid") 31 | .spacing([4.0, 1.5]) 32 | .striped(true) 33 | .min_col_width(0.0) 34 | .show(ui, |ui| { 35 | ui.label(" "); 36 | for i in 0..16 { 37 | ui.label(format!("{i:02X}")); 38 | } 39 | ui.end_row(); 40 | loop { 41 | let bytes_read = file.read(&mut buf).unwrap(); 42 | if bytes_read == 0 { 43 | break; 44 | } 45 | 46 | ui.label(format!("{count:08X}")); 47 | let text_color = if ui.style().visuals.dark_mode { 48 | egui::Color32::from_gray(255) 49 | } else { 50 | egui::Color32::from_gray(0) 51 | }; 52 | for b in buf.iter().take(bytes_read) { 53 | ui.colored_label(text_color, format!("{b:02X}")); 54 | } 55 | for _ in 0..16 - bytes_read { 56 | ui.label(" "); 57 | } 58 | ui.label(" "); 59 | for b in buf.iter().take(bytes_read) { 60 | if *b >= 0x20 && *b <= 0x7e { 61 | ui.label(format!("{}", *b as char)); 62 | } else { 63 | ui.label("."); 64 | } 65 | } 66 | 67 | ui.end_row(); 68 | count += bytes_read; 69 | } 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/hyperion-clap-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, Error, Ident, Lit, parse_macro_input}; 4 | 5 | #[proc_macro_derive(CommandPermission, attributes(command_permission))] 6 | pub fn derive_command_permission(input: TokenStream) -> TokenStream { 7 | // Parse the input as a DeriveInput (struct or enum) 8 | let input = parse_macro_input!(input as DeriveInput); 9 | let name = input.ident.clone(); // Clone the Ident to prevent moving 10 | 11 | // Extract the group from the `#[command_permission(group = "Admin")]` attribute 12 | let mut group = None; 13 | for attr in &input.attrs { 14 | if attr.path().is_ident("command_permission") { 15 | if let Err(err) = attr.parse_nested_meta(|meta| { 16 | if meta.path.is_ident("group") { 17 | if let Ok(Lit::Str(lit)) = meta.value()?.parse::() { 18 | group = Some(lit); 19 | } 20 | } 21 | Ok(()) 22 | }) { 23 | return Error::new_spanned(attr, format!("Failed to parse attribute: {err}")) 24 | .to_compile_error() 25 | .into(); 26 | } 27 | } 28 | } 29 | 30 | let group_ident = match group { 31 | Some(g) => Ident::new(&g.value(), g.span()), 32 | None => { 33 | return Error::new_spanned( 34 | input, 35 | "Missing required `#[command_permission(group = \"\")]` attribute.", 36 | ) 37 | .to_compile_error() 38 | .into(); 39 | } 40 | }; 41 | 42 | // Generate the trait implementation 43 | let expanded = quote! { 44 | impl CommandPermission for #name { 45 | fn has_required_permission(user_group: ::hyperion_permission::Group) -> bool { 46 | const REQUIRED_GROUP: ::hyperion_permission::Group = ::hyperion_permission::Group::#group_ident; 47 | 48 | if REQUIRED_GROUP == ::hyperion_permission::Group::Banned { 49 | // When checking for the group "Banned" we don't want to check for higher groups. 50 | return REQUIRED_GROUP == user_group; 51 | } else { 52 | return user_group as u32 >= REQUIRED_GROUP as u32 53 | } 54 | } 55 | } 56 | }; 57 | 58 | TokenStream::from(expanded) 59 | } 60 | -------------------------------------------------------------------------------- /crates/hyperion-proxy/src/egress.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use hyperion_proto::{ArchivedSetReceiveBroadcasts, ArchivedShutdown}; 3 | use rustc_hash::FxBuildHasher; 4 | use tracing::{error, instrument, warn}; 5 | 6 | use crate::{data::PlayerHandle, server_sender::ServerSender}; 7 | 8 | #[derive(Clone)] 9 | pub struct Egress { 10 | // todo: can we do some type of EntityId and SlotMap 11 | pub(crate) player_registry: &'static papaya::HashMap, 12 | pub(crate) server_sender: ServerSender, 13 | } 14 | 15 | impl Egress { 16 | #[must_use] 17 | pub const fn new( 18 | player_registry: &'static papaya::HashMap, 19 | server_sender: ServerSender, 20 | ) -> Self { 21 | Self { 22 | player_registry, 23 | server_sender, 24 | } 25 | } 26 | 27 | #[instrument(skip_all)] 28 | pub fn unicast(&self, stream: u64, data: Bytes) { 29 | let players = self.player_registry.pin(); 30 | 31 | let Some(player) = players.get(&stream) else { 32 | // expected to still happen infrequently 33 | warn!("Player not found for id {stream:?}"); 34 | return; 35 | }; 36 | 37 | // todo: handle error; kick player if cannot send (buffer full) 38 | if let Err(e) = player.send(data) { 39 | warn!("Failed to send data to player: {:?}", e); 40 | player.shutdown(); 41 | } 42 | } 43 | 44 | #[instrument(skip_all)] 45 | pub fn handle_set_receive_broadcasts(&self, pkt: &ArchivedSetReceiveBroadcasts) { 46 | let player_registry = self.player_registry; 47 | let players = player_registry.pin(); 48 | let Ok(stream) = rkyv::deserialize::(&pkt.stream); 49 | 50 | let Some(player) = players.get(&stream) else { 51 | error!("Player not found for stream {stream:?}"); 52 | return; 53 | }; 54 | 55 | player.enable_receive_broadcasts(); 56 | } 57 | 58 | #[instrument(skip_all)] 59 | pub fn handle_shutdown(&self, pkt: &ArchivedShutdown) { 60 | let player_registry = self.player_registry; 61 | let players = player_registry.pin(); 62 | let Ok(stream) = rkyv::deserialize::(&pkt.stream); 63 | 64 | if let Some(result) = players.get(&stream) { 65 | result.shutdown(); 66 | } else { 67 | error!("Player not found for stream {stream:?}"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/entity.rs: -------------------------------------------------------------------------------- 1 | //! Entity metadata. 2 | //! 3 | //! The base class 4 | //! 5 | //! ```text 6 | //! 0 -> Byte (0) EntityFlags 7 | //! 1 -> VarInt (1) AirTicks(300) 8 | //! 2 -> TextComponent? (6) CustomName("") 9 | //! 3 -> bool (8) CustomNameVisible(false) 10 | //! 4 -> bool (8) Silent(false) 11 | //! 5 -> bool (8) NoGravity(false) 12 | //! 6 -> Pose (21) Pose(STANDING) 13 | //! 7 -> VarInt (1) TicksFrozenInPowderSnow(0) 14 | //! ``` 15 | 16 | use bevy::prelude::*; 17 | use valence_protocol::{Encode, VarInt}; 18 | use valence_text::Text; 19 | 20 | use crate::{define_and_register_components, simulation::Metadata}; 21 | 22 | mod flags; 23 | pub use flags::EntityFlags; 24 | 25 | // Example usage: 26 | define_and_register_components! { 27 | 1, AirSupply -> VarInt, 28 | 2, CustomName -> Option, 29 | 3, CustomNameVisible -> bool, 30 | 4, Silent -> bool, 31 | 5, NoGravity -> bool, 32 | 7, TicksFrozenInPowderSnow -> VarInt, 33 | } 34 | 35 | impl Default for AirSupply { 36 | fn default() -> Self { 37 | Self::new(VarInt(300)) 38 | } 39 | } 40 | 41 | impl Default for CustomName { 42 | fn default() -> Self { 43 | Self::new(None) 44 | } 45 | } 46 | 47 | impl Default for CustomNameVisible { 48 | fn default() -> Self { 49 | Self::new(false) 50 | } 51 | } 52 | 53 | impl Default for Silent { 54 | fn default() -> Self { 55 | Self::new(false) 56 | } 57 | } 58 | 59 | impl Default for NoGravity { 60 | fn default() -> Self { 61 | Self::new(false) 62 | } 63 | } 64 | 65 | impl Default for TicksFrozenInPowderSnow { 66 | fn default() -> Self { 67 | Self::new(VarInt(0)) 68 | } 69 | } 70 | 71 | #[derive(Encode, Clone, Copy, Default, PartialEq, Eq, Debug)] 72 | #[derive(Component)] 73 | #[repr(C)] // ideally this would be u8 74 | pub enum Pose { 75 | #[default] 76 | Standing, 77 | FallFlying, 78 | Sleeping, 79 | Swimming, 80 | SpinAttack, 81 | Sneaking, 82 | LongJumping, 83 | Dying, 84 | Croaking, 85 | UsingTongue, 86 | Sitting, 87 | Roaring, 88 | Sniffing, 89 | Emerging, 90 | Digging, 91 | } 92 | 93 | impl Metadata for Pose { 94 | type Type = Self; 95 | 96 | const INDEX: u8 = 6; 97 | 98 | fn to_type(self) -> Self::Type { 99 | self 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /events/bedwars/src/command/shoot.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::{ 4 | glam::Vec3, 5 | net::Channel, 6 | simulation::{Pitch, Position, Uuid, Velocity, Yaw, entity_kind::EntityKind}, 7 | }; 8 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 9 | use tracing::{debug, error}; 10 | 11 | #[derive(Parser, CommandPermission, Debug)] 12 | #[command(name = "shoot")] 13 | #[command_permission(group = "Normal")] 14 | pub struct ShootCommand { 15 | #[arg(help = "Initial velocity of the arrow")] 16 | velocity: f32, 17 | } 18 | 19 | impl MinecraftCommand for ShootCommand { 20 | type State = SystemState<( 21 | Query<'static, 'static, (&'static Position, &'static Yaw, &'static Pitch)>, 22 | Commands<'static, 'static>, 23 | )>; 24 | 25 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 26 | const EYE_HEIGHT: f32 = 1.62; 27 | const BASE_VELOCITY: f32 = 3.0; // Base velocity multiplier for arrows 28 | 29 | let (query, mut commands) = state.get(world); 30 | 31 | let (pos, yaw, pitch) = match query.get(caller) { 32 | Ok(data) => data, 33 | Err(e) => { 34 | error!("shoot command failed: query failed: {e}"); 35 | return; 36 | } 37 | }; 38 | 39 | // Calculate direction vector from player's rotation 40 | let direction = super::raycast::get_direction_from_rotation(**yaw, **pitch); 41 | 42 | // Spawn arrow slightly in front of player to avoid self-collision 43 | let spawn_pos = Vec3::new(pos.x, pos.y + EYE_HEIGHT, pos.z) + direction * 1.0; 44 | 45 | // Calculate velocity with base multiplier 46 | let velocity = direction * (self.velocity * BASE_VELOCITY); 47 | 48 | debug!( 49 | "Arrow velocity: ({}, {}, {})", 50 | velocity.x, velocity.y, velocity.z 51 | ); 52 | 53 | debug!( 54 | "Arrow spawn position: ({}, {}, {})", 55 | spawn_pos.x, spawn_pos.y, spawn_pos.z 56 | ); 57 | 58 | let entity_id = Uuid::new_v4(); 59 | 60 | // Create arrow entity with velocity 61 | commands.spawn(( 62 | EntityKind::Arrow, 63 | entity_id, 64 | Position::new(spawn_pos.x, spawn_pos.y, spawn_pos.z), 65 | Velocity::new(velocity.x, velocity.y, velocity.z), 66 | Yaw::new(**yaw), 67 | Pitch::new(**pitch), 68 | Channel, 69 | )); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/hyperion-permission/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod storage; 2 | 3 | use bevy::{ecs::world::OnDespawn, prelude::*}; 4 | use clap::ValueEnum; 5 | use hyperion::{ 6 | net::{Compose, ConnectionId}, 7 | simulation::{Uuid, command::get_command_packet}, 8 | storage::LocalDb, 9 | }; 10 | use num_derive::{FromPrimitive, ToPrimitive}; 11 | use storage::PermissionStorage; 12 | use tracing::error; 13 | 14 | pub struct PermissionPlugin; 15 | 16 | #[derive( 17 | Default, 18 | Component, 19 | FromPrimitive, 20 | ToPrimitive, 21 | Copy, 22 | Clone, 23 | Debug, 24 | PartialEq, 25 | ValueEnum, 26 | Eq 27 | )] 28 | #[repr(C)] 29 | pub enum Group { 30 | Banned, 31 | #[default] 32 | Normal, 33 | Moderator, 34 | Admin, 35 | } 36 | 37 | // todo: 38 | 39 | fn load_permissions( 40 | trigger: Trigger<'_, OnAdd, Uuid>, 41 | query: Query<'_, '_, &Uuid, With>, 42 | permissions: Res<'_, PermissionStorage>, 43 | mut commands: Commands<'_, '_>, 44 | ) { 45 | let Ok(uuid) = query.get(trigger.target()) else { 46 | return; 47 | }; 48 | 49 | let group = permissions.get(**uuid); 50 | commands.entity(trigger.target()).insert(group); 51 | } 52 | 53 | fn store_permissions( 54 | trigger: Trigger<'_, OnDespawn, Group>, 55 | query: Query<'_, '_, (&Uuid, &Group)>, 56 | permissions: Res<'_, PermissionStorage>, 57 | ) { 58 | let (uuid, group) = match query.get(trigger.target()) { 59 | Ok(data) => data, 60 | Err(e) => { 61 | error!("failed to store permissions: query failed: {e}"); 62 | return; 63 | } 64 | }; 65 | 66 | permissions.set(**uuid, *group).unwrap(); 67 | } 68 | 69 | fn initialize_commands( 70 | trigger: Trigger<'_, OnInsert, Group>, 71 | query: Query<'_, '_, &ConnectionId>, 72 | compose: Res<'_, Compose>, 73 | world: &World, 74 | ) { 75 | let cmd_pkt = get_command_packet(world, Some(trigger.target())); 76 | let Ok(&connection_id) = query.get(trigger.target()) else { 77 | error!("failed to initialize commands: player is missing ConnectionId"); 78 | return; 79 | }; 80 | compose.unicast(&cmd_pkt, connection_id).unwrap(); 81 | } 82 | 83 | impl Plugin for PermissionPlugin { 84 | fn build(&self, app: &mut App) { 85 | let storage = storage::PermissionStorage::new(app.world().resource::()).unwrap(); 86 | app.insert_resource(storage); 87 | app.add_observer(load_permissions); 88 | app.add_observer(store_permissions); 89 | app.add_observer(initialize_commands); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /events/bedwars/src/plugin/vanish.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use hyperion::{ 3 | net::Compose, 4 | simulation::{Uuid, metadata::entity::EntityFlags}, 5 | }; 6 | use tracing::error; 7 | use valence_protocol::packets::play::{self, player_list_s2c::PlayerListActions}; 8 | use valence_server::GameMode; 9 | 10 | pub struct VanishPlugin; 11 | 12 | #[derive(Default, Component, Debug)] 13 | pub struct Vanished(bool); 14 | 15 | impl Vanished { 16 | #[must_use] 17 | pub const fn new(is_vanished: bool) -> Self { 18 | Self(is_vanished) 19 | } 20 | 21 | #[must_use] 22 | pub const fn is_vanished(&self) -> bool { 23 | self.0 24 | } 25 | } 26 | 27 | fn update_vanish( 28 | trigger: Trigger<'_, OnInsert, Vanished>, 29 | compose: Res<'_, Compose>, 30 | mut query: Query<'_, '_, (&Vanished, &Uuid, &mut EntityFlags)>, 31 | ) { 32 | let (vanished, uuid, mut flags) = match query.get_mut(trigger.target()) { 33 | Ok(data) => data, 34 | Err(e) => { 35 | error!("failed to update vanish: query failed: {e}"); 36 | return; 37 | } 38 | }; 39 | 40 | if vanished.is_vanished() { 41 | // Remove from player list and make them invisible 42 | let remove_packet = play::PlayerListS2c { 43 | actions: PlayerListActions::new() 44 | .with_update_listed(true) 45 | .with_update_game_mode(true), 46 | entries: vec![play::player_list_s2c::PlayerListEntry { 47 | player_uuid: uuid.0, 48 | listed: false, 49 | game_mode: GameMode::Survival, 50 | ..Default::default() 51 | }] 52 | .into(), 53 | }; 54 | compose.broadcast(&remove_packet).send().unwrap(); 55 | 56 | // Set entity flags to make them invisible 57 | *flags |= EntityFlags::INVISIBLE; 58 | } else { 59 | // Add back to player list and make them visible 60 | let add_packet = play::PlayerListS2c { 61 | actions: PlayerListActions::new() 62 | .with_update_listed(true) 63 | .with_update_game_mode(true), 64 | entries: vec![play::player_list_s2c::PlayerListEntry { 65 | player_uuid: uuid.0, 66 | listed: true, 67 | game_mode: GameMode::Survival, 68 | ..Default::default() 69 | }] 70 | .into(), 71 | }; 72 | compose.broadcast(&add_packet).send().unwrap(); 73 | 74 | // Clear invisible flag 75 | *flags &= !EntityFlags::INVISIBLE; 76 | } 77 | } 78 | 79 | impl Plugin for VanishPlugin { 80 | fn build(&self, app: &mut App) { 81 | app.add_observer(update_vanish); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /events/bedwars/src/command/gui.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ecs::system::SystemState, prelude::*}; 2 | use clap::Parser; 3 | use hyperion::{ItemKind, ItemStack, simulation::entity_kind::EntityKind}; 4 | use hyperion_clap::{CommandPermission, MinecraftCommand}; 5 | use hyperion_gui::Gui; 6 | use hyperion_inventory::Inventory; 7 | use tracing::debug; 8 | use valence_protocol::packets::play::{click_slot_c2s::ClickMode, open_screen_s2c::WindowType}; 9 | 10 | #[derive(Parser, CommandPermission, Debug)] 11 | #[command(name = "testgui")] 12 | #[command_permission(group = "Normal")] 13 | pub struct GuiCommand; 14 | 15 | impl MinecraftCommand for GuiCommand { 16 | type State = SystemState<( 17 | Query<'static, 'static, &'static Gui>, 18 | Commands<'static, 'static>, 19 | )>; 20 | 21 | fn execute(self, world: &World, state: &mut Self::State, caller: Entity) { 22 | let (query, mut commands) = state.get(world); 23 | 24 | for gui in &query { 25 | if gui.id == 27 { 26 | gui.open_deferred(&mut commands, caller); 27 | return; 28 | } 29 | } 30 | 31 | // The gui was not found, so create one 32 | let mut gui_inventory = 33 | Inventory::new(27, "Test GUI".to_string(), WindowType::Generic9x3, true); 34 | 35 | let item = ItemStack::new(ItemKind::GoldIngot, 1, None); 36 | 37 | gui_inventory.set(13, item).unwrap(); 38 | 39 | commands.queue(move |world: &mut World| { 40 | let mut gui = Gui::new(gui_inventory, world, 27); 41 | gui.add_command(13, |player, click_mode| match click_mode { 42 | ClickMode::Click => { 43 | debug!("Player {:?} clicked on slot 13", player); 44 | } 45 | ClickMode::DoubleClick => { 46 | debug!("Player {:?} double clicked on slot 13", player); 47 | } 48 | ClickMode::Drag => { 49 | debug!("Player {:?} dragged on slot 13", player); 50 | } 51 | ClickMode::DropKey => { 52 | debug!("Player {:?} dropped on slot 13", player); 53 | } 54 | ClickMode::Hotbar => { 55 | debug!("Player {:?} hotbar on slot 13", player); 56 | } 57 | ClickMode::ShiftClick => { 58 | debug!("Player {:?} shift clicked on slot 13", player); 59 | } 60 | ClickMode::CreativeMiddleClick => { 61 | debug!("Player {:?} creative middle clicked on slot 13", player); 62 | } 63 | }); 64 | 65 | gui.open(world, caller); 66 | 67 | world.spawn((EntityKind::Gui, gui)); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tools/packet-inspector/src/main_cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use packet_inspector::Packet; 3 | use packet_inspector::Proxy; 4 | use packet_inspector::ProxyLog; 5 | use packet_inspector::DisconnectionReason; 6 | use std::net::SocketAddr; 7 | use tracing::Level; 8 | 9 | #[derive(Parser, Clone, Debug)] 10 | #[clap(author, version, about)] 11 | struct CliArgs { 12 | /// The socket address to listen for connections on. This is the address clients should connect to 13 | listener_addr: SocketAddr, 14 | /// The socket address the proxy will connect to. This is the address of the server 15 | server_addr: SocketAddr, 16 | } 17 | 18 | #[tokio::main] 19 | async fn main() -> anyhow::Result<()> { 20 | tracing_subscriber::fmt() 21 | .with_max_level(Level::TRACE) 22 | .init(); 23 | 24 | let args = CliArgs::parse(); 25 | 26 | let proxy = Proxy::start(args.listener_addr, args.server_addr).await?; 27 | let receiver = proxy.subscribe().await; 28 | 29 | tokio::spawn(async move { 30 | proxy.main_task.await??; 31 | Ok::<(), anyhow::Error>(()) 32 | }); 33 | 34 | // consumer 35 | tokio::spawn(async move { 36 | while let Ok(packet) = receiver.recv_async().await { 37 | log(&packet); 38 | } 39 | }); 40 | 41 | tokio::spawn(async move { 42 | loop { 43 | let next = proxy.logs_rx.recv_async().await?; 44 | match next { 45 | ProxyLog::ClientConnected(addr) => { 46 | tracing::trace!("Accepted a new client {addr}."); 47 | } 48 | ProxyLog::ClientDisconnected(addr, DisconnectionReason::Error(_)) => { 49 | tracing::trace!("Client {addr} disconnected."); 50 | } 51 | ProxyLog::ClientDisconnected(addr, DisconnectionReason::OnlineModeRequired) => { 52 | tracing::error!( 53 | "Client {addr} was disconnected due to a server encryption request. \ 54 | The packet inspector does not support online mode." 55 | ); 56 | } 57 | } 58 | } 59 | 60 | #[allow(unreachable_code)] 61 | Ok::<(), anyhow::Error>(()) 62 | }); 63 | 64 | tokio::signal::ctrl_c().await.unwrap(); 65 | 66 | Ok(()) 67 | } 68 | 69 | fn log(packet: &Packet) { 70 | tracing::debug!( 71 | "{:?} -> [{:?}] 0x{:0>2X} \"{}\" {:?}", 72 | packet.side, 73 | packet.state, 74 | packet.id, 75 | packet.name, 76 | truncated(format!("{:?}", packet.data), 512) 77 | ); 78 | } 79 | 80 | fn truncated(string: String, max_len: usize) -> String { 81 | if string.len() > max_len { 82 | format!("{}...", &string[..max_len]) 83 | } else { 84 | string 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /events/bedwars/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, path::PathBuf}; 2 | 3 | use bedwars::init_game; 4 | use clap::Parser; 5 | use hyperion::Crypto; 6 | use serde::Deserialize; 7 | use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt}; 8 | // use tracing_tracy::TracyLayer; 9 | 10 | #[cfg(not(target_env = "msvc"))] 11 | #[global_allocator] 12 | static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 13 | 14 | /// The arguments to run the server 15 | #[derive(Parser, Deserialize, Debug)] 16 | struct Args { 17 | /// The IP address the server should listen on. Defaults to 0.0.0.0 18 | #[clap(short, long, default_value = "0.0.0.0")] 19 | #[serde(default = "default_ip")] 20 | ip: String, 21 | 22 | /// The port the server should listen on. Defaults to 25565 23 | #[clap(short, long, default_value = "35565")] 24 | #[serde(default = "default_port")] 25 | port: u16, 26 | 27 | /// The file path to the root certificate authority's certificate 28 | #[clap(long)] 29 | root_ca_cert: PathBuf, 30 | 31 | /// The file path to the game server's certificate 32 | #[clap(long)] 33 | cert: PathBuf, 34 | 35 | /// The file path to the game server's private key 36 | #[clap(long)] 37 | private_key: PathBuf, 38 | } 39 | 40 | fn default_ip() -> String { 41 | "0.0.0.0".to_string() 42 | } 43 | 44 | const fn default_port() -> u16 { 45 | 35565 46 | } 47 | 48 | fn setup_logging() { 49 | tracing::subscriber::set_global_default( 50 | Registry::default() 51 | .with(EnvFilter::from_default_env()) 52 | // .with(TracyLayer::default()) 53 | .with( 54 | tracing_subscriber::fmt::layer() 55 | .with_target(false) 56 | .with_thread_ids(false) 57 | .with_file(true) 58 | .with_line_number(true), 59 | ), 60 | ) 61 | .expect("setup tracing subscribers"); 62 | } 63 | 64 | fn main() { 65 | dotenvy::dotenv().ok(); 66 | 67 | setup_logging(); 68 | 69 | // Try to load config from environment variables 70 | let args = match envy::prefixed("BEDWARS_").from_env::() { 71 | Ok(args) => { 72 | tracing::info!("Loaded configuration from environment variables"); 73 | args 74 | } 75 | Err(e) => { 76 | tracing::info!( 77 | "Failed to load from environment: {}, falling back to command line arguments", 78 | e 79 | ); 80 | Args::parse() 81 | } 82 | }; 83 | 84 | let address = format!("{ip}:{port}", ip = args.ip, port = args.port); 85 | let address = address.parse::().unwrap(); 86 | let crypto = Crypto::new(&args.root_ca_cert, &args.cert, &args.private_key).unwrap(); 87 | 88 | init_game(address, crypto).unwrap(); 89 | } 90 | -------------------------------------------------------------------------------- /crates/hyperion/tests/collision.rs: -------------------------------------------------------------------------------- 1 | #![feature(assert_matches)] 2 | #![allow( 3 | clippy::print_stdout, 4 | reason = "the purpose of not having printing to stdout is so that tracing is used properly \ 5 | for the core libraries. These are tests, so it doesn't matter" 6 | )] 7 | 8 | use bevy::{app::FixedMain, prelude::*}; 9 | use glam::Vec3; 10 | use hyperion::{ 11 | HyperionCore, 12 | simulation::{EntitySize, Owner, Pitch, Position, Velocity, Yaw, entity_kind::EntityKind}, 13 | spatial::Spatial, 14 | }; 15 | 16 | #[test] 17 | fn test_get_first_collision() { 18 | /// Function to spawn arrows at different angles 19 | fn spawn_arrow(world: &mut World, position: Vec3, direction: Vec3, owner: Owner) -> Entity { 20 | tracing::debug!("Spawning arrow at position: {position:?} with direction: {direction:?}"); 21 | world 22 | .spawn(( 23 | EntityKind::Arrow, 24 | Spatial, 25 | Velocity::new(direction.x, direction.y, direction.z), 26 | Position::new(position.x, position.y, position.z), 27 | owner, 28 | )) 29 | .id() 30 | } 31 | 32 | let mut app = App::new(); 33 | 34 | app.add_plugins((HyperionCore, hyperion_genmap::GenMapPlugin)); 35 | 36 | let world = app.world_mut(); 37 | 38 | // Create a player entity 39 | let player = world 40 | .spawn(( 41 | EntityKind::Player, 42 | EntitySize::default(), 43 | Position::new(0.0, 21.0, 0.0), 44 | Yaw::new(0.0), 45 | Pitch::new(90.0), 46 | )) 47 | .id(); 48 | 49 | // Spawn arrows at different angles 50 | let arrow_velocities = [Vec3::new(0.0, -1.0, 0.0)]; 51 | 52 | let arrows: Vec = arrow_velocities 53 | .iter() 54 | .map(|velocity| { 55 | spawn_arrow( 56 | world, 57 | Vec3::new(0.0, 21.0, 0.0), 58 | *velocity, 59 | Owner::new(player), 60 | ) 61 | }) 62 | .collect(); 63 | 64 | // Progress the world to ensure that the index is updated 65 | FixedMain::run_fixed_main(world); 66 | 67 | let mut query = world.query::<(&Position, &Velocity)>(); 68 | 69 | // Get all entities with Position and Velocity components 70 | for arrow in &arrows { 71 | let (position, velocity) = query.get(world, *arrow).unwrap(); 72 | println!("position: {position:?}"); 73 | println!("velocity: {velocity:?}"); 74 | } 75 | 76 | FixedMain::run_fixed_main(world); 77 | 78 | // Get all entities with Position and Velocity components 79 | for arrow in &arrows { 80 | let (position, velocity) = query.get(world, *arrow).unwrap(); 81 | println!("position: {position:?}"); 82 | println!("velocity: {velocity:?}"); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/hyperion/tests/spatial.rs: -------------------------------------------------------------------------------- 1 | #![feature(assert_matches)] 2 | #![allow( 3 | clippy::print_stdout, 4 | reason = "the purpose of not having printing to stdout is so that tracing is used properly \ 5 | for the core libraries. These are tests, so it doesn't matter" 6 | )] 7 | 8 | use std::{assert_matches::assert_matches, collections::HashSet}; 9 | 10 | use approx::assert_relative_eq; 11 | use bevy::{app::FixedMain, prelude::*}; 12 | use geometry::{aabb::Aabb, ray::Ray}; 13 | use glam::Vec3; 14 | use hyperion::{ 15 | simulation::{EntitySize, Position}, 16 | spatial, 17 | }; 18 | use spatial::{Spatial, SpatialIndex, SpatialPlugin}; 19 | 20 | #[test] 21 | fn spatial() { 22 | let mut app = App::new(); 23 | app.add_plugins(SpatialPlugin); 24 | 25 | let zombie = app 26 | .world_mut() 27 | .spawn((EntitySize::default(), Position::new(0.0, 0.0, 0.0), Spatial)) 28 | .id(); 29 | 30 | let player = app 31 | .world_mut() 32 | .spawn(( 33 | EntitySize::default(), 34 | Position::new(10.0, 0.0, 0.0), 35 | Spatial, 36 | )) 37 | .id(); 38 | 39 | // progress one tick to ensure that the index is updated 40 | FixedMain::run_fixed_main(app.world_mut()); 41 | 42 | let system = app.register_system( 43 | move |spatial: Res<'_, SpatialIndex>, query: Query<'_, '_, (&Position, &EntitySize)>| { 44 | let closest = spatial 45 | .closest_to(Vec3::new(1.0, 2.0, 0.0), query) 46 | .expect("there to be a closest entity"); 47 | assert_eq!(closest, zombie); 48 | 49 | let closest = spatial 50 | .closest_to(Vec3::new(11.0, 2.0, 0.0), query) 51 | .expect("there to be a closest entity"); 52 | assert_eq!(closest, player); 53 | 54 | let big_aabb = Aabb::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(100.0, 100.0, 100.0)); 55 | 56 | let collisions: HashSet<_> = spatial.get_collisions(big_aabb, query).collect(); 57 | assert!( 58 | collisions.contains(&zombie), 59 | "zombie should be in collisions" 60 | ); 61 | assert!( 62 | collisions.contains(&player), 63 | "player should be in collisions" 64 | ); 65 | 66 | let ray = Ray::from_points(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0)); 67 | let (first, distance) = spatial.first_ray_collision(ray, query).unwrap(); 68 | assert_eq!(first, zombie); 69 | assert_relative_eq!(distance.into_inner(), 0.0); 70 | 71 | let ray = Ray::from_points(Vec3::new(12.0, 0.0, 0.0), Vec3::new(13.0, 1.0, 1.0)); 72 | assert_matches!(spatial.first_ray_collision(ray, query), None); 73 | }, 74 | ); 75 | app.world_mut().run_system(system).unwrap(); 76 | } 77 | -------------------------------------------------------------------------------- /crates/hyperion/src/storage/db.rs: -------------------------------------------------------------------------------- 1 | //! Constructs for connecting and working with a `Heed` database. 2 | 3 | use std::path::Path; 4 | 5 | use bevy::prelude::*; 6 | use byteorder::NativeEndian; 7 | use derive_more::Deref; 8 | use heed::{Database, Env, EnvOpenOptions, types}; 9 | use uuid::Uuid; 10 | 11 | use crate::simulation::skin::{ArchivedPlayerSkin, PlayerSkin}; 12 | 13 | /// A wrapper around a `Heed` database 14 | #[derive(Resource, Debug, Clone, Deref)] 15 | pub struct LocalDb { 16 | env: Env, 17 | } 18 | 19 | impl LocalDb { 20 | /// Creates a new [`LocalDb`] 21 | pub fn new() -> anyhow::Result { 22 | let path = Path::new("db").join("heed.mdb"); 23 | 24 | std::fs::create_dir_all(&path)?; 25 | 26 | let env = unsafe { 27 | EnvOpenOptions::new() 28 | .map_size(10 * 1024 * 1024) // 10MB 29 | .max_dbs(8) // todo: why is this needed/configurable? ideally would be infinite... 30 | .open(&path)? 31 | }; 32 | 33 | Ok(Self { env }) 34 | } 35 | } 36 | 37 | /// A handler for player skin operations 38 | #[derive(Resource, Debug, Clone)] 39 | pub struct SkinHandler { 40 | env: Env, 41 | skins: Database, types::Bytes>, 42 | } 43 | 44 | impl SkinHandler { 45 | /// Creates a new [`SkinHandler`] from a given [`LocalDb`]. 46 | pub fn new(db: &LocalDb) -> anyhow::Result { 47 | // We open the default unnamed database 48 | let skins = { 49 | let mut wtxn = db.write_txn()?; 50 | let db = db.create_database(&mut wtxn, Some("uuid-to-skins"))?; 51 | wtxn.commit()?; 52 | db 53 | }; 54 | 55 | Ok(Self { 56 | env: db.env.clone(), 57 | skins, 58 | }) 59 | } 60 | 61 | /// Finds a [`PlayerSkin`] by its UUID. 62 | pub fn find(&self, uuid: Uuid) -> anyhow::Result> { 63 | // We open a read transaction to check if those values are now available 64 | 65 | let uuid = uuid.as_u128(); 66 | 67 | let rtxn = self.env.read_txn()?; 68 | let skin = self.skins.get(&rtxn, &uuid); 69 | 70 | let Some(skin) = skin? else { 71 | return Ok(None); 72 | }; 73 | 74 | let skin = unsafe { rkyv::access_unchecked::(skin) }; 75 | let skin = rkyv::deserialize::<_, rkyv::rancor::Error>(skin).unwrap(); 76 | Ok(Some(skin)) 77 | } 78 | 79 | /// Inserts a [`PlayerSkin`] into the database. 80 | pub fn insert(&self, uuid: Uuid, skin: &PlayerSkin) -> anyhow::Result<()> { 81 | let uuid = uuid.as_u128(); 82 | 83 | let mut wtxn = self.env.write_txn()?; 84 | 85 | let skin = rkyv::to_bytes::(skin).unwrap(); 86 | 87 | self.skins.put(&mut wtxn, &uuid, &skin)?; 88 | wtxn.commit()?; 89 | 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/packet.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use bevy::prelude::*; 4 | use derive_more::{Deref, From, Into}; 5 | use hyperion_packet_macros::for_each_state; 6 | use hyperion_utils::EntityExt; 7 | 8 | use crate::net::ConnectionId; 9 | 10 | #[derive(Copy, Clone, Debug, Deref, Event)] 11 | pub struct Packet { 12 | sender: Entity, 13 | connection_id: ConnectionId, 14 | packet_id: u64, 15 | 16 | #[deref] 17 | body: T, 18 | } 19 | 20 | impl Packet { 21 | pub const fn new(sender: Entity, connection_id: ConnectionId, packet_id: u64, body: T) -> Self { 22 | Self { 23 | sender, 24 | connection_id, 25 | packet_id, 26 | body, 27 | } 28 | } 29 | 30 | /// Entity of the player who sent this packet 31 | pub const fn sender(&self) -> Entity { 32 | self.sender 33 | } 34 | 35 | /// Connection id of the player who sent this packet. This is included for convenience; it is 36 | /// the same connection id component in the [`Packet::sender`] entity. 37 | pub const fn connection_id(&self) -> ConnectionId { 38 | self.connection_id 39 | } 40 | 41 | /// Minecraft id of the player who sent this packet. This is included for convenience; it is 42 | /// the same Minecraft id in the [`Packet::sender`] entity. 43 | pub fn minecraft_id(&self) -> i32 { 44 | self.sender().minecraft_id() 45 | } 46 | 47 | /// Unique monotonically-increasing packet id 48 | pub const fn packet_id(&self) -> u64 { 49 | self.packet_id 50 | } 51 | } 52 | 53 | /// Packet ordered by the time it was received by the server 54 | #[derive(Copy, Clone, Debug, Deref, From, Into)] 55 | pub struct OrderedPacketRef<'a, T>(&'a Packet); 56 | 57 | impl PartialOrd> for OrderedPacketRef<'_, T> { 58 | fn partial_cmp(&self, other: &OrderedPacketRef<'_, U>) -> Option { 59 | self.packet_id().partial_cmp(&other.packet_id()) 60 | } 61 | } 62 | 63 | impl PartialEq> for OrderedPacketRef<'_, T> { 64 | fn eq(&self, other: &OrderedPacketRef<'_, U>) -> bool { 65 | self.partial_cmp(other) == Some(Ordering::Equal) 66 | } 67 | } 68 | 69 | for_each_state! { 70 | #{ 71 | pub mod #state { 72 | #for_each_packet! { 73 | #{ 74 | pub type #packet_name = super::Packet<#static_valence_packet>; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | pub struct PacketPlugin; 82 | 83 | impl Plugin for PacketPlugin { 84 | fn build(&self, app: &mut App) { 85 | for_each_state! { 86 | #{ 87 | #for_each_packet! { 88 | #{ 89 | app.add_event::<#state::#packet_name>(); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /crates/hyperion-stats/README.md: -------------------------------------------------------------------------------- 1 | # hyperion-stats 2 | 3 | A high-performance parallel statistics library optimized for real-time anti-cheat systems, particularly in Minecraft servers. Uses SIMD operations to efficiently track multiple statistical metrics simultaneously. 4 | 5 | ## Overview 6 | 7 | This library provides efficient parallel computation of running statistics (mean, variance, min/max) across multiple data streams simultaneously. This is particularly useful for anti-cheat systems that need to track many player metrics in real-time. 8 | 9 | ## Anti-Cheat Applications 10 | 11 | Below are common anti-cheat metrics that can be tracked using parallel statistics: 12 | 13 | | Metric | Description | Statistical Considerations | 14 | |--------|-------------|---------------------------| 15 | | Packets per Second | Track packet frequency per player | - Use sliding window
- Account for network jitter
- Track variance for burst detection | 16 | | Position Delta | Distance between consecutive positions | - Consider tick rate
- Account for teleports
- Track max speed violations | 17 | | Vertical Velocity | Changes in Y-coordinate | - Account for jump mechanics
- Consider block collisions
- Track unusual patterns | 18 | | Click Patterns | Time between clicks | - Track click distribution
- Detect auto-clickers
- Consider CPS limits | 19 | | Rotation Deltas | Changes in pitch/yaw | - Track smooth vs. snap movements
- Detect aimbot patterns
- Consider sensitivity | 20 | | Block Interaction | Time between block breaks/places | - Account for tool efficiency
- Track unusual patterns
- Consider game mechanics | 21 | | Combat Patterns | Hit timing and accuracy | - Track reach distances
- Consider ping/latency
- Detect impossible hits | 22 | | Movement Timing | Time between movement packets | - Account for client tick rate
- Detect timer modifications
- Consider server load | 23 | 24 | ## Future Work & Considerations 25 | 26 | 27 | ### Additional Statistics 28 | - Skewness and kurtosis for better pattern detection 29 | - Exponential moving averages for trend detection 30 | - Correlation between different metrics 31 | - Fourier analysis for periodic pattern detection 32 | - Entropy calculations for randomness assessment 33 | 34 | ### Performance Optimizations 35 | - GPU acceleration for large player counts 36 | - Adaptive sampling rates based on load 37 | - Efficient memory management for long sessions 38 | - Better SIMD utilization 39 | 40 | ### Anti-Cheat Specific Features 41 | - Built-in violation level tracking 42 | - Confidence scoring for detections 43 | - False positive reduction algorithms 44 | - Integration with common game mechanics 45 | - Latency compensation 46 | 47 | ### Challenges to Consider 48 | - Network conditions affecting measurements 49 | - Server performance impact on timing 50 | - Client-side modifications affecting data 51 | - Game mechanic edge cases 52 | - Balance between detection and false positives -------------------------------------------------------------------------------- /events/bedwars/src/plugin/damage.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use hyperion::{ 3 | net::{Compose, ConnectionId, agnostic}, 4 | simulation::{Position, event::HitGroundEvent, metadata::living_entity::Health}, 5 | }; 6 | use hyperion_utils::EntityExt; 7 | use tracing::error; 8 | use valence_protocol::{VarInt, packets::play, text::IntoText}; 9 | use valence_server::ident; 10 | 11 | fn apply_natural_damages( 12 | mut events: EventReader<'_, '_, HitGroundEvent>, 13 | mut query: Query<'_, '_, (&mut Health, &ConnectionId, &Position)>, 14 | compose: Res<'_, Compose>, 15 | ) { 16 | for event in events.read() { 17 | if event.fall_distance <= 3. { 18 | continue; 19 | } 20 | 21 | // TODO account for armor/effects and gamemode 22 | let damage = event.fall_distance.floor() - 3.; 23 | 24 | if damage <= 0. { 25 | continue; 26 | } 27 | 28 | let (mut health, &connection_id, position) = match query.get_mut(event.client) { 29 | Ok(data) => data, 30 | Err(e) => { 31 | error!("failed to apply natural damages: query failed: {e}"); 32 | continue; 33 | } 34 | }; 35 | 36 | health.damage(damage); 37 | 38 | let pkt_damage_event = play::EntityDamageS2c { 39 | entity_id: VarInt(event.client.minecraft_id()), 40 | source_cause_id: VarInt(0), 41 | source_direct_id: VarInt(0), 42 | source_type_id: VarInt(10), // 10 = fall damage 43 | source_pos: Option::None, 44 | }; 45 | 46 | let sound = agnostic::sound( 47 | if event.fall_distance > 7. { 48 | ident!("minecraft:entity.player.big_fall") 49 | } else { 50 | ident!("minecraft:entity.player.small_fall") 51 | }, 52 | **position, 53 | ) 54 | .volume(1.) 55 | .pitch(1.) 56 | .seed(fastrand::i64(..)) 57 | .build(); 58 | 59 | compose.unicast(&pkt_damage_event, connection_id).unwrap(); 60 | compose 61 | .broadcast_local(&sound, position.to_chunk()) 62 | .send() 63 | .unwrap(); 64 | 65 | if health.is_dead() { 66 | let pkt_death_screen = play::DeathMessageS2c { 67 | player_id: VarInt(event.client.minecraft_id()), 68 | message: (if event.fall_distance < 5.0 { 69 | "You hit the ground too hard" 70 | } else { 71 | "You fell from a high place" 72 | }) 73 | .to_string() 74 | .into_cow_text(), 75 | }; 76 | compose.unicast(&pkt_death_screen, connection_id).unwrap(); 77 | } 78 | } 79 | } 80 | 81 | pub struct DamagePlugin; 82 | 83 | impl Plugin for DamagePlugin { 84 | fn build(&self, app: &mut App) { 85 | app.add_systems(FixedUpdate, apply_natural_damages); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/hyperion/src/egress/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use tracing::error; 3 | use valence_protocol::{VarInt, packets::play::PlayerActionResponseS2c}; 4 | 5 | use crate::{ 6 | Blocks, 7 | net::{ 8 | Compose, ConnectionId, 9 | intermediate::{IntermediateServerToProxyMessage, UpdatePlayerPositions}, 10 | }, 11 | simulation::Position, 12 | }; 13 | mod channel; 14 | pub mod metadata; 15 | pub mod player_join; 16 | mod stats; 17 | pub mod sync_chunks; 18 | mod sync_entity_state; 19 | 20 | use channel::ChannelPlugin; 21 | use player_join::PlayerJoinPlugin; 22 | use stats::StatsPlugin; 23 | use sync_chunks::SyncChunksPlugin; 24 | use sync_entity_state::EntityStateSyncPlugin; 25 | 26 | fn send_chunk_positions( 27 | compose: Res<'_, Compose>, 28 | query: Query<'_, '_, (&ConnectionId, &Position)>, 29 | ) { 30 | let count = query.iter().count(); 31 | let mut stream = Vec::with_capacity(count); 32 | let mut positions = Vec::with_capacity(count); 33 | 34 | for (&io, pos) in query.iter() { 35 | stream.push(io); 36 | positions.push(hyperion_proto::ChunkPosition::from(pos.to_chunk())); 37 | } 38 | 39 | let packet = UpdatePlayerPositions { stream, positions }; 40 | 41 | let chunk_positions = IntermediateServerToProxyMessage::UpdatePlayerPositions(packet); 42 | 43 | compose.io_buf().add_proxy_message(&chunk_positions); 44 | } 45 | 46 | fn broadcast_chunk_deltas( 47 | compose: Res<'_, Compose>, 48 | mut blocks: ResMut<'_, Blocks>, 49 | query: Query<'_, '_, &ConnectionId>, 50 | ) { 51 | blocks.for_each_to_update_mut(|chunk| { 52 | for packet in chunk.delta_drain_packets() { 53 | if let Err(e) = compose.broadcast(packet).send() { 54 | error!("failed to send chunk delta packet: {e}"); 55 | return; 56 | } 57 | } 58 | }); 59 | blocks.clear_should_update(); 60 | 61 | for to_confirm in blocks.to_confirm.drain(..) { 62 | let connection_id = match query.get(to_confirm.entity) { 63 | Ok(connection_id) => *connection_id, 64 | Err(e) => { 65 | error!("failed to send player action response: query failed: {e}"); 66 | continue; 67 | } 68 | }; 69 | 70 | let pkt = PlayerActionResponseS2c { 71 | sequence: VarInt(to_confirm.sequence), 72 | }; 73 | 74 | if let Err(e) = compose.unicast(&pkt, connection_id) { 75 | error!("failed to send player action response: {e}"); 76 | } 77 | } 78 | } 79 | 80 | #[derive(Component)] 81 | pub struct EgressPlugin; 82 | 83 | impl Plugin for EgressPlugin { 84 | fn build(&self, app: &mut App) { 85 | app.add_systems(PostUpdate, (send_chunk_positions, broadcast_chunk_deltas)); 86 | app.add_plugins(( 87 | PlayerJoinPlugin, 88 | StatsPlugin, 89 | SyncChunksPlugin, 90 | EntityStateSyncPlugin, 91 | ChannelPlugin, 92 | )); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /events/bedwars/src/plugin/chat.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use hyperion::{ 3 | ingress, 4 | net::{Compose, ConnectionId}, 5 | simulation::{Position, packet, packet_state}, 6 | valence_protocol::{ 7 | packets::play, 8 | text::{Color, IntoText, Text}, 9 | }, 10 | }; 11 | use tracing::error; 12 | 13 | use crate::Team; 14 | 15 | const CHAT_COOLDOWN_SECONDS: i64 = 3; // 3 seconds 16 | const CHAT_COOLDOWN_TICKS: i64 = CHAT_COOLDOWN_SECONDS * 20; // Convert seconds to ticks 17 | 18 | #[derive(Default, Component)] 19 | pub struct ChatCooldown { 20 | pub expires: i64, 21 | } 22 | 23 | pub fn initialize_cooldown( 24 | trigger: Trigger<'_, OnAdd, packet_state::Play>, 25 | mut commands: Commands<'_, '_>, 26 | ) { 27 | commands 28 | .entity(trigger.target()) 29 | .insert(ChatCooldown::default()); 30 | } 31 | 32 | pub fn handle_chat_messages( 33 | mut packets: EventReader<'_, '_, packet::play::ChatMessage>, 34 | compose: Res<'_, Compose>, 35 | mut query: Query<'_, '_, (&Name, &Position, &mut ChatCooldown, &ConnectionId, &Team)>, 36 | ) { 37 | let current_tick = compose.global().tick; 38 | 39 | for packet in packets.read() { 40 | let (name, position, mut cooldown, io, team) = match query.get_mut(packet.sender()) { 41 | Ok(data) => data, 42 | Err(e) => { 43 | error!("could not process chat message: query failed: {e}"); 44 | continue; 45 | } 46 | }; 47 | 48 | // Check if player is still on cooldown 49 | if cooldown.expires > current_tick { 50 | let remaining_ticks = cooldown.expires - current_tick; 51 | let remaining_secs = remaining_ticks as f32 / 20.0; 52 | 53 | let cooldown_msg = 54 | format!("§cPlease wait {remaining_secs:.2} seconds before sending another message") 55 | .into_cow_text(); 56 | 57 | let packet = play::GameMessageS2c { 58 | chat: cooldown_msg, 59 | overlay: false, 60 | }; 61 | 62 | compose.unicast(&packet, *io).unwrap(); 63 | continue; 64 | } 65 | 66 | cooldown.expires = current_tick + CHAT_COOLDOWN_TICKS; 67 | 68 | let chat = Text::default() 69 | + "<".color(Color::DARK_GRAY) 70 | + name.as_str().to_owned().color(*team) 71 | + "> ".color(Color::DARK_GRAY) 72 | + (**packet.message).to_owned(); 73 | let packet = play::GameMessageS2c { 74 | chat: chat.into(), 75 | overlay: false, 76 | }; 77 | 78 | let center = position.to_chunk(); 79 | 80 | compose.broadcast_local(&packet, center).send().unwrap(); 81 | } 82 | } 83 | 84 | pub struct ChatPlugin; 85 | 86 | impl Plugin for ChatPlugin { 87 | fn build(&self, app: &mut App) { 88 | app.add_observer(initialize_cooldown); 89 | app.add_systems( 90 | FixedUpdate, 91 | handle_chat_messages.after(ingress::decode::play), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /crates/bvh-region/src/query/closest.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Reverse, collections::BinaryHeap, fmt::Debug}; 2 | 3 | use geometry::aabb::Aabb; 4 | use glam::Vec3; 5 | use num_traits::Zero; 6 | use ordered_float::NotNan; 7 | 8 | use crate::{Bvh, Node, utils::NodeOrd}; 9 | 10 | impl Bvh { 11 | /// Returns the closest element to the target and the distance squared to it. 12 | pub fn get_closest(&self, target: Vec3, get_aabb: impl Fn(&T) -> Aabb) -> Option<(&T, f64)> { 13 | let mut min_dist2 = f64::INFINITY; 14 | let mut min_node = None; 15 | 16 | let on = self.root(); 17 | 18 | let on = match on { 19 | Node::Internal(internal) => internal, 20 | Node::Leaf(leaf) => { 21 | return leaf 22 | .iter() 23 | .map(|elem| { 24 | let aabb = get_aabb(elem); 25 | let dist2 = aabb.dist2(target); 26 | (elem, dist2) 27 | }) 28 | .min_by_key(|(_, dist)| dist.to_bits()); 29 | } 30 | }; 31 | 32 | // let mut stack: SmallVec<&BvhNode, 64> = SmallVec::new(); 33 | let mut heap: BinaryHeap<_> = std::iter::once(on) 34 | .map(|node| { 35 | Reverse(NodeOrd { 36 | node, 37 | by: NotNan::zero(), 38 | }) 39 | }) 40 | .collect(); 41 | 42 | while let Some(on) = heap.pop() { 43 | let on = on.0.node; 44 | let dist2 = on.aabb.dist2(target); 45 | 46 | if dist2 > min_dist2 { 47 | break; 48 | } 49 | 50 | for child in on.children(self) { 51 | match child { 52 | Node::Internal(internal) => { 53 | let dist2 = internal.aabb.dist2(target); 54 | let dist2 = NotNan::new(dist2).unwrap(); 55 | 56 | heap.push(Reverse(NodeOrd { 57 | node: internal, 58 | by: dist2, 59 | })); 60 | } 61 | Node::Leaf(leaf) => { 62 | let Some((elem, dist2)) = leaf 63 | .iter() 64 | .map(|elem| { 65 | let aabb = get_aabb(elem); 66 | let dist2 = aabb.dist2(target); 67 | (elem, dist2) 68 | }) 69 | .min_by_key(|(_, dist)| dist.to_bits()) 70 | else { 71 | continue; 72 | }; 73 | 74 | if dist2 < min_dist2 { 75 | min_dist2 = dist2; 76 | min_node = Some(elem); 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | min_node.map(|elem| (elem, min_dist2)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/hyperion/src/common/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for the server. 2 | 3 | use std::{fmt::Debug, fs::File, io::Read, path::Path}; 4 | 5 | use bevy::prelude::*; 6 | use serde::{Deserialize, Serialize}; 7 | use tracing::{info, instrument, warn}; 8 | 9 | /// The configuration for the server representing a `toml` file. 10 | #[derive(Serialize, Deserialize, Debug, Resource)] 11 | pub struct Config { 12 | pub border_diameter: Option, 13 | pub max_players: i32, 14 | pub view_distance: i16, 15 | pub simulation_distance: i32, 16 | pub server_desc: String, 17 | pub spawn: Spawn, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug, Component)] 21 | pub struct Spawn { 22 | pub kind: Radius, 23 | pub radius: i32, 24 | pub x: i32, 25 | pub y: i32, 26 | pub z: i32, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Debug, Clone, Copy)] 30 | pub enum Radius { 31 | Chebyshev, 32 | Euclidean, 33 | } 34 | 35 | impl Default for Config { 36 | fn default() -> Self { 37 | Self { 38 | border_diameter: Some(100.0), 39 | max_players: 10_000, 40 | view_distance: 32, 41 | simulation_distance: 10, 42 | server_desc: "Hyperion Test Server".to_owned(), 43 | spawn: Spawn::default(), 44 | } 45 | } 46 | } 47 | 48 | impl Default for Spawn { 49 | fn default() -> Self { 50 | Self { 51 | radius: 1000, 52 | kind: Radius::Chebyshev, 53 | x: 0, 54 | y: 64, 55 | z: 0, 56 | } 57 | } 58 | } 59 | 60 | impl Config { 61 | #[instrument] 62 | pub fn load

(path: P) -> anyhow::Result 63 | where 64 | P: AsRef + Debug, 65 | { 66 | info!("loading configuration file"); 67 | 68 | if path.as_ref().exists() { 69 | let mut file = File::open(path)?; 70 | let mut contents = String::default(); 71 | file.read_to_string(&mut contents)?; 72 | let config = toml::from_str::(contents.as_str())?; 73 | return Ok(config); 74 | } 75 | 76 | info!("configuration file not found, using defaults"); 77 | 78 | // make required folders 79 | if let Some(parent) = path.as_ref().parent() { 80 | if let Err(e) = std::fs::create_dir_all(parent) { 81 | // this might happen on a read-only filesystem (i.e., 82 | // when running on a CI, profiling in Instruments, etc.) 83 | warn!( 84 | "failed to create parent directories for {:?}: {}, using defaults", 85 | path.as_ref(), 86 | e 87 | ); 88 | return Ok(Self::default()); 89 | } 90 | } 91 | 92 | // write default config to file 93 | let default_config = Self::default(); 94 | std::fs::write(&path, toml::to_string(&default_config)?.as_bytes())?; 95 | 96 | info!("wrote default configuration to {:?}", path.as_ref()); 97 | 98 | Ok(Self::default()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/entity_kind.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | #[derive(Component, Copy, Clone, Debug, PartialEq, Eq, Hash)] 4 | #[repr(C)] 5 | pub enum EntityKind { 6 | Allay = 0, 7 | AreaEffectCloud = 1, 8 | ArmorStand = 2, 9 | Arrow = 3, 10 | Axolotl = 4, 11 | Bat = 5, 12 | Bee = 6, 13 | Blaze = 7, 14 | BlockDisplay = 8, 15 | Boat = 9, 16 | Camel = 10, 17 | Cat = 11, 18 | CaveSpider = 12, 19 | ChestBoat = 13, 20 | ChestMinecart = 14, 21 | Chicken = 15, 22 | Cod = 16, 23 | CommandBlockMinecart = 17, 24 | Cow = 18, 25 | Creeper = 19, 26 | Dolphin = 20, 27 | Donkey = 21, 28 | DragonFireball = 22, 29 | Drowned = 23, 30 | Egg = 24, 31 | ElderGuardian = 25, 32 | EndCrystal = 26, 33 | EnderDragon = 27, 34 | EnderPearl = 28, 35 | Enderman = 29, 36 | Endermite = 30, 37 | Evoker = 31, 38 | EvokerFangs = 32, 39 | ExperienceBottle = 33, 40 | ExperienceOrb = 34, 41 | EyeOfEnder = 35, 42 | FallingBlock = 36, 43 | FireworkRocket = 37, 44 | Fox = 38, 45 | Frog = 39, 46 | FurnaceMinecart = 40, 47 | Ghast = 41, 48 | Giant = 42, 49 | GlowItemFrame = 43, 50 | GlowSquid = 44, 51 | Goat = 45, 52 | Guardian = 46, 53 | Hoglin = 47, 54 | HopperMinecart = 48, 55 | Horse = 49, 56 | Husk = 50, 57 | Illusioner = 51, 58 | Interaction = 52, 59 | IronGolem = 53, 60 | Item = 54, 61 | ItemDisplay = 55, 62 | ItemFrame = 56, 63 | Fireball = 57, 64 | LeashKnot = 58, 65 | Lightning = 59, 66 | Llama = 60, 67 | LlamaSpit = 61, 68 | MagmaCube = 62, 69 | Marker = 63, 70 | Minecart = 64, 71 | Mooshroom = 65, 72 | Mule = 66, 73 | Ocelot = 67, 74 | Painting = 68, 75 | Panda = 69, 76 | Parrot = 70, 77 | Phantom = 71, 78 | Pig = 72, 79 | Piglin = 73, 80 | PiglinBrute = 74, 81 | Pillager = 75, 82 | PolarBear = 76, 83 | Potion = 77, 84 | Pufferfish = 78, 85 | Rabbit = 79, 86 | Ravager = 80, 87 | Salmon = 81, 88 | Sheep = 82, 89 | Shulker = 83, 90 | ShulkerBullet = 84, 91 | Silverfish = 85, 92 | Skeleton = 86, 93 | SkeletonHorse = 87, 94 | Slime = 88, 95 | SmallFireball = 89, 96 | Sniffer = 90, 97 | SnowGolem = 91, 98 | Snowball = 92, 99 | SpawnerMinecart = 93, 100 | SpectralArrow = 94, 101 | Spider = 95, 102 | Squid = 96, 103 | Stray = 97, 104 | Strider = 98, 105 | Tadpole = 99, 106 | TextDisplay = 100, 107 | Tnt = 101, 108 | TntMinecart = 102, 109 | TraderLlama = 103, 110 | Trident = 104, 111 | TropicalFish = 105, 112 | Turtle = 106, 113 | Vex = 107, 114 | Villager = 108, 115 | Vindicator = 109, 116 | WanderingTrader = 110, 117 | Warden = 111, 118 | Witch = 112, 119 | Wither = 113, 120 | WitherSkeleton = 114, 121 | WitherSkull = 115, 122 | Wolf = 116, 123 | Zoglin = 117, 124 | Zombie = 118, 125 | ZombieHorse = 119, 126 | ZombieVillager = 120, 127 | ZombifiedPiglin = 121, 128 | Player = 122, 129 | FishingBobber = 123, 130 | Gui = 124, 131 | } 132 | -------------------------------------------------------------------------------- /events/bedwars/src/plugin/stats.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use bevy::prelude::*; 4 | use hyperion::{ 5 | net::Compose, 6 | valence_protocol::{packets::play, text::IntoText}, 7 | }; 8 | use tracing::info_span; 9 | 10 | #[derive(Resource)] 11 | struct UpdateStart(Instant); 12 | 13 | #[derive(Resource)] 14 | struct TicksElapsed(u64); 15 | 16 | pub struct StatsPlugin; 17 | 18 | impl Plugin for StatsPlugin { 19 | #[allow(clippy::excessive_nesting)] 20 | fn build(&self, app: &mut App) { 21 | let mode = env!("RUN_MODE"); 22 | 23 | let mut tick_times = Vec::with_capacity(20 * 60); // 20 ticks per second, 60 seconds 24 | 25 | app.insert_resource(UpdateStart(Instant::now())); 26 | app.insert_resource(TicksElapsed(0)); 27 | 28 | // PreUpdate runs before FixedUpdate, which runs before Update 29 | app.add_systems(PreUpdate, |mut start: ResMut<'_, UpdateStart>| { 30 | start.0 = Instant::now(); 31 | }); 32 | 33 | app.add_systems(FixedUpdate, |mut elapsed: ResMut<'_, TicksElapsed>| { 34 | elapsed.0 += 1; 35 | }); 36 | 37 | app.add_systems( 38 | Update, 39 | move |compose: Res<'_, Compose>, 40 | start: Res<'_, UpdateStart>, 41 | mut elapsed: ResMut<'_, TicksElapsed>| { 42 | if elapsed.0 == 0 { 43 | // No ticks occured on this frame 44 | return; 45 | } 46 | 47 | let ticks_elapsed = elapsed.0; 48 | elapsed.0 = 0; 49 | 50 | let span = info_span!("stats"); 51 | let _enter = span.enter(); 52 | let player_count = compose 53 | .global() 54 | .player_count 55 | .load(std::sync::atomic::Ordering::Relaxed); 56 | 57 | let ms_per_tick = start.0.elapsed().as_secs_f32() * 1000.0 / (ticks_elapsed as f32); 58 | 59 | // If ticks_elapsed > 1, this inserts the average tick time multiple times for 60 | // more accurate data 61 | for _ in 0..ticks_elapsed { 62 | tick_times.push(ms_per_tick); 63 | } 64 | 65 | if tick_times.len() > 20 * 60 { 66 | tick_times.remove(0); 67 | } 68 | 69 | let avg_s05 = tick_times.iter().rev().take(20 * 5).sum::() / (20.0 * 5.0); 70 | let avg_s15 = tick_times.iter().rev().take(20 * 15).sum::() / (20.0 * 15.0); 71 | let avg_s60 = tick_times.iter().sum::() / tick_times.len() as f32; 72 | 73 | let title = format!( 74 | "§b{mode}§r\n§aµ/5s: {avg_s05:.2} ms §r| §eµ/15s: {avg_s15:.2} ms §r| §cµ/1m: \ 75 | {avg_s60:.2} ms" 76 | ); 77 | 78 | let footer = format!("§d§l{player_count} players online"); 79 | 80 | let pkt = play::PlayerListHeaderS2c { 81 | header: title.into_cow_text(), 82 | footer: footer.into_cow_text(), 83 | }; 84 | 85 | compose.broadcast(&pkt).send().unwrap(); 86 | }, 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/skin.rs: -------------------------------------------------------------------------------- 1 | //! Constructs for obtaining a player's skin. 2 | use anyhow::Context; 3 | use base64::{Engine as _, engine::general_purpose}; 4 | use bevy::prelude::*; 5 | use rkyv::Archive; 6 | use tracing::info; 7 | 8 | use crate::{storage::SkinHandler, util::mojang::MojangClient}; 9 | 10 | /// A signed player skin. 11 | #[derive( 12 | Debug, 13 | Clone, 14 | Archive, 15 | Component, 16 | rkyv::Deserialize, 17 | rkyv::Serialize, 18 | serde::Serialize, 19 | serde::Deserialize 20 | )] 21 | pub struct PlayerSkin { 22 | /// The textures of the player skin, usually obtained from the [`MojangClient`] as a base64 string. 23 | pub textures: String, 24 | /// The signature of the player skin, usually obtained from the [`MojangClient`] as a base64 string. 25 | pub signature: String, 26 | } 27 | 28 | impl PlayerSkin { 29 | pub const EMPTY: Self = Self { 30 | textures: String::new(), 31 | signature: String::new(), 32 | }; 33 | 34 | /// Creates a new [`PlayerSkin`] 35 | #[must_use] 36 | pub const fn new(textures: String, signature: String) -> Self { 37 | Self { 38 | textures, 39 | signature, 40 | } 41 | } 42 | 43 | /// Gets a skin from a Mojang UUID. 44 | /// 45 | /// # Arguments 46 | /// * `uuid` - A Mojang UUID. 47 | /// 48 | /// # Returns 49 | /// A `PlayerSkin` based on the UUID, or `None` if not found. 50 | pub async fn from_uuid( 51 | uuid: uuid::Uuid, 52 | mojang: &MojangClient, 53 | skins: &SkinHandler, 54 | ) -> anyhow::Result> { 55 | if let Some(skin) = skins.find(uuid)? { 56 | info!("Returning cached skin"); 57 | return Ok(Some(skin)); 58 | } 59 | 60 | info!("player skin cache miss for {uuid}"); 61 | 62 | let json_object = mojang.data_from_uuid(&uuid).await?; 63 | let properties_array = json_object["properties"] 64 | .as_array() 65 | .with_context(|| format!("no properties on {json_object:?}"))?; 66 | for property_object in properties_array { 67 | let name = property_object["name"] 68 | .as_str() 69 | .with_context(|| format!("no name on {property_object:?}"))?; 70 | if name != "textures" { 71 | continue; 72 | } 73 | let textures = property_object["value"] 74 | .as_str() 75 | .with_context(|| format!("no value on {property_object:?}"))?; 76 | let signature = property_object["signature"] 77 | .as_str() 78 | .with_context(|| format!("no signature on {property_object:?}"))?; 79 | 80 | // Validate base64 encoding 81 | general_purpose::STANDARD 82 | .decode(textures) 83 | .context("invalid texture value")?; 84 | general_purpose::STANDARD 85 | .decode(signature) 86 | .context("invalid signature value")?; 87 | 88 | let res = Self { 89 | textures: textures.to_string(), 90 | signature: signature.to_string(), 91 | }; 92 | skins.insert(uuid, &res)?; 93 | return Ok(Some(res)); 94 | } 95 | Ok(None) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/hyperion-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod cached_save; 2 | pub mod iterator; 3 | pub mod prev; 4 | use std::path::PathBuf; 5 | 6 | use bevy::{ 7 | ecs::system::{SystemParam, SystemState}, 8 | prelude::*, 9 | }; 10 | pub use cached_save::cached_save; 11 | pub use prev::{Prev, track_prev}; 12 | 13 | pub trait EntityExt: Sized { 14 | fn id(&self) -> u32; 15 | fn from_id(id: u32, world: &World) -> anyhow::Result; 16 | 17 | fn minecraft_id(&self) -> i32; 18 | fn from_minecraft_id(id: i32, world: &World) -> anyhow::Result; 19 | } 20 | 21 | impl EntityExt for Entity { 22 | fn id(&self) -> u32 { 23 | self.index() 24 | } 25 | 26 | fn from_id(id: u32, world: &World) -> anyhow::Result { 27 | // TODO: According to the docs, this should check if the returned entity is freed 28 | world 29 | .entities() 30 | .resolve_from_id(id) 31 | .ok_or_else(|| anyhow::anyhow!("minecraft id is invalid")) 32 | } 33 | 34 | fn minecraft_id(&self) -> i32 { 35 | bytemuck::cast(self.id()) 36 | } 37 | 38 | fn from_minecraft_id(id: i32, world: &World) -> anyhow::Result { 39 | Self::from_id(bytemuck::cast(id), world) 40 | } 41 | } 42 | 43 | pub trait ApplyWorld { 44 | fn apply(&mut self, world: &mut World); 45 | } 46 | 47 | impl ApplyWorld for SystemState 48 | where 49 | Param: SystemParam + 'static, 50 | { 51 | fn apply(&mut self, world: &mut World) { 52 | self.apply(world); 53 | } 54 | } 55 | 56 | impl ApplyWorld for () { 57 | fn apply(&mut self, _: &mut World) {} 58 | } 59 | 60 | /// Represents application identification information used for caching and other system-level operations 61 | #[derive(Resource)] 62 | pub struct AppId { 63 | /// The qualifier/category of the application (e.g. "com", "org", "hyperion") 64 | pub qualifier: String, 65 | /// The organization that created the application (e.g. "andrewgazelka") 66 | pub organization: String, 67 | /// The specific application name (e.g. "proof-of-concept") 68 | pub application: String, 69 | } 70 | 71 | impl AppId { 72 | #[must_use] 73 | pub fn cache_dir(&self) -> PathBuf { 74 | let project_dirs = directories::ProjectDirs::from( 75 | self.qualifier.as_str(), 76 | self.organization.as_str(), 77 | self.application.as_str(), 78 | ) 79 | .unwrap(); 80 | project_dirs.cache_dir().to_path_buf() 81 | } 82 | } 83 | 84 | pub struct HyperionUtilsPlugin; 85 | 86 | impl Plugin for HyperionUtilsPlugin { 87 | fn build(&self, app: &mut App) { 88 | app.insert_resource(AppId { 89 | qualifier: "github".to_string(), 90 | organization: "hyperion-mc".to_string(), 91 | application: "generic".to_string(), 92 | }); 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_entity_id() { 102 | let mut world = World::new(); 103 | let entity_id = world.spawn_empty().id(); 104 | let minecraft_id = entity_id.minecraft_id(); 105 | assert_eq!( 106 | Entity::from_minecraft_id(minecraft_id, &world).unwrap(), 107 | entity_id 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crates/hyperion/Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bench]] 2 | harness = false 3 | name = "set" 4 | 5 | [dependencies] 6 | anyhow = { workspace = true } 7 | arc-swap = { workspace = true } 8 | base64 = { workspace = true } 9 | bitfield-struct = { workspace = true } 10 | bitvec = { workspace = true } 11 | boxcar = { workspace = true } 12 | bumpalo = { workspace = true } 13 | bvh-region = { workspace = true } 14 | bytemuck = { workspace = true } 15 | byteorder = { workspace = true } 16 | bytes = { workspace = true } 17 | colored = { workspace = true } 18 | derive_more = { workspace = true } 19 | enumset = { workspace = true } 20 | bevy.workspace = true 21 | fastrand = { workspace = true } 22 | flate2 = { workspace = true, features = ["zlib-ng"] } 23 | geometry = { workspace = true } 24 | glam = { workspace = true, features = ["serde"] } 25 | heapless = { workspace = true } 26 | heed = { workspace = true } 27 | humantime = { workspace = true } 28 | hyperion-crafting = { workspace = true } 29 | hyperion-inventory = { workspace = true } 30 | hyperion-nerd-font = { workspace = true } 31 | hyperion-packet-macros = { workspace = true } 32 | hyperion-palette = { workspace = true } 33 | hyperion-proto = { workspace = true } 34 | hyperion-text = { workspace = true } 35 | hyperion-utils = { workspace = true } 36 | indexmap = { workspace = true } 37 | itertools = { workspace = true } 38 | kanal = { workspace = true } 39 | libc = { workspace = true } 40 | libdeflater = { workspace = true } 41 | memmap2 = { workspace = true } 42 | more-asserts = { workspace = true } 43 | ndarray = { workspace = true } 44 | once_cell = { workspace = true } 45 | packet-channel = { workspace = true } 46 | parking_lot = { workspace = true } 47 | paste = { workspace = true } 48 | rayon = { workspace = true } 49 | reqwest = { workspace = true } 50 | rkyv = { workspace = true } 51 | roaring = { workspace = true, features = ["simd"] } 52 | rustc-hash = { workspace = true } 53 | rustls = { workspace = true } 54 | rustls-pki-types = { workspace = true } 55 | rustls-webpki = { workspace = true } 56 | serde = { workspace = true, features = ["derive"] } 57 | serde_json = { workspace = true } 58 | sha2 = { workspace = true } 59 | simd-utils = { workspace = true } 60 | thiserror = { workspace = true } 61 | thread_local = { workspace = true } 62 | tokio = { workspace = true, features = ["full", "tracing"] } 63 | tokio-rustls = { workspace = true } 64 | toml = { workspace = true } 65 | tracing = { workspace = true } 66 | tracing-tracy = { workspace = true } 67 | uuid = { workspace = true } 68 | valence_anvil = { workspace = true } 69 | valence_bytes = { workspace = true } 70 | valence_generated = { workspace = true } 71 | valence_ident = { workspace = true } 72 | valence_nbt = { workspace = true } 73 | valence_protocol = { workspace = true } 74 | valence_registry = { workspace = true } 75 | valence_server = { workspace = true } 76 | valence_text = { workspace = true } 77 | ordered-float = { workspace = true } 78 | 79 | [dev-dependencies] 80 | approx = { workspace = true } 81 | divan = { workspace = true } 82 | fastrand = { workspace = true } 83 | hyperion-genmap = { workspace = true } 84 | serial_test = { workspace = true } 85 | tracing-appender = { workspace = true } 86 | tracing-subscriber = { workspace = true } 87 | 88 | [lints] 89 | workspace = true 90 | 91 | [package] 92 | authors = ["Andrew Gazelka "] 93 | edition.workspace = true 94 | name = "hyperion" 95 | publish = false 96 | readme = "README.md" 97 | version.workspace = true 98 | -------------------------------------------------------------------------------- /crates/hyperion/src/simulation/metadata/display.rs: -------------------------------------------------------------------------------- 1 | // Extends Entity. 2 | // 3 | // Index Type Meaning Default 4 | // 8 VarInt (1) Interpolation delay 0 5 | // 9 VarInt (1) Transformation interpolation duration 0 6 | // 10 VarInt (1) Position/Rotation interpolation duration 0 7 | // 11 Vector3 (28) Translation (0.0, 0.0, 0.0) 8 | // 12 Vector3 (28) Scale (1.0, 1.0, 1.0) 9 | // 13 Quaternion (29) Rotation left (0.0, 0.0, 0.0, 1.0) 10 | // 14 Quaternion (29) Rotation right (0.0, 0.0, 0.0, 1.0) 11 | // 15 Byte (0) Billboard Constraints (0 = FIXED, 1 = VERTICAL, 2 = HORIZONTAL, 3 = CENTER) 0 12 | // 16 VarInt (1) Brightness override (blockLight << 4 | skyLight << 20) -1 13 | // 17 Float (3) View range 1.0 14 | // 18 Float (3) Shadow radius 0.0 15 | // 19 Float (3) Shadow strength 1.0 16 | // 20 Float (3) Width 0.0 17 | // 21 Float (3) Height 0.0 18 | // 22 VarInt (1) Glow color override -1 19 | 20 | use bevy::prelude::*; 21 | use valence_protocol::VarInt; 22 | 23 | use super::Metadata; 24 | use crate::define_and_register_components; 25 | 26 | // Example usage: 27 | define_and_register_components! { 28 | 8, InterpolationDelay -> VarInt, 29 | 9, InterpolationDuration -> VarInt, 30 | 10, Translation -> glam::Vec3, 31 | 11, Scale -> glam::Vec3, 32 | 12, RotationLeft -> glam::Quat, 33 | 13, RotationRight -> glam::Quat, 34 | 14, BillboardConstraints -> u8, 35 | 15, BrightnessOverride -> VarInt, 36 | 16, ViewRange -> f32, 37 | 17, ShadowRadius -> f32, 38 | 18, ShadowStrength -> f32, 39 | 19, Width -> f32, 40 | 20, Height -> f32, 41 | 21, GlowColorOverride -> VarInt, 42 | } 43 | 44 | impl Default for InterpolationDelay { 45 | fn default() -> Self { 46 | Self::new(VarInt(0)) 47 | } 48 | } 49 | 50 | impl Default for InterpolationDuration { 51 | fn default() -> Self { 52 | Self::new(VarInt(0)) 53 | } 54 | } 55 | 56 | impl Default for Translation { 57 | fn default() -> Self { 58 | Self::new(glam::Vec3::ZERO) 59 | } 60 | } 61 | 62 | impl Default for Scale { 63 | fn default() -> Self { 64 | Self::new(glam::Vec3::ONE) 65 | } 66 | } 67 | 68 | impl Default for RotationLeft { 69 | fn default() -> Self { 70 | Self::new(glam::Quat::IDENTITY) 71 | } 72 | } 73 | 74 | impl Default for RotationRight { 75 | fn default() -> Self { 76 | Self::new(glam::Quat::IDENTITY) 77 | } 78 | } 79 | 80 | impl Default for BillboardConstraints { 81 | fn default() -> Self { 82 | Self::new(0) 83 | } 84 | } 85 | 86 | impl Default for BrightnessOverride { 87 | fn default() -> Self { 88 | Self::new(VarInt(-1)) 89 | } 90 | } 91 | 92 | impl Default for ViewRange { 93 | fn default() -> Self { 94 | Self::new(1.0) 95 | } 96 | } 97 | 98 | impl Default for ShadowRadius { 99 | fn default() -> Self { 100 | Self::new(0.0) 101 | } 102 | } 103 | 104 | impl Default for ShadowStrength { 105 | fn default() -> Self { 106 | Self::new(1.0) 107 | } 108 | } 109 | 110 | impl Default for Width { 111 | fn default() -> Self { 112 | Self::new(0.0) 113 | } 114 | } 115 | 116 | impl Default for Height { 117 | fn default() -> Self { 118 | Self::new(0.0) 119 | } 120 | } 121 | 122 | impl Default for GlowColorOverride { 123 | fn default() -> Self { 124 | Self::new(VarInt(-1)) 125 | } 126 | } 127 | --------------------------------------------------------------------------------