├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── blocks.json ├── dimension.snbt ├── dimension_codec.snbt ├── items.json ├── mappings.json ├── protocol.json └── types.json ├── chat ├── Cargo.toml └── src │ ├── builder.rs │ ├── color.rs │ ├── component.rs │ └── lib.rs ├── datapack ├── Cargo.toml ├── src │ ├── data │ │ ├── advancement │ │ │ ├── conditions.rs │ │ │ ├── display.rs │ │ │ └── mod.rs │ │ ├── datatypes.rs │ │ ├── functions.rs │ │ ├── item_modifiers.rs │ │ ├── loot_tables.rs │ │ ├── mod.rs │ │ ├── predicate.rs │ │ ├── recipe │ │ │ ├── cooking.rs │ │ │ ├── ingredient.rs │ │ │ ├── mod.rs │ │ │ ├── recipe.rs │ │ │ ├── shaped.rs │ │ │ ├── shapeless.rs │ │ │ ├── smithing.rs │ │ │ └── stonecutting.rs │ │ ├── structure.rs │ │ ├── tags.rs │ │ └── world_gen │ │ │ ├── biome.rs │ │ │ ├── carvers.rs │ │ │ ├── density_function.rs │ │ │ ├── dimension.rs │ │ │ ├── dimension_type.rs │ │ │ ├── features.rs │ │ │ ├── jigsaw_pool.rs │ │ │ ├── mod.rs │ │ │ ├── noise.rs │ │ │ ├── noise_settings.rs │ │ │ ├── processors.rs │ │ │ ├── structure_features.rs │ │ │ ├── structure_set.rs │ │ │ └── surface_builders.rs │ ├── datapack.rs │ └── lib.rs └── tests │ └── default_datapack.rs ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── macros_impl ├── Cargo.toml └── src │ ├── lib.rs │ └── packet │ ├── gen.rs │ ├── mod.rs │ └── parse.rs ├── network ├── Cargo.toml ├── build.rs └── src │ ├── bitmask.rs │ ├── lib.rs │ ├── netutil.rs │ └── packet_data.rs ├── qdat ├── Cargo.toml ├── build.rs ├── buildscript │ ├── blockstate.rs │ ├── item_info.rs │ ├── items.rs │ └── mod.rs └── src │ ├── block │ ├── behavior │ │ └── mod.rs │ ├── mod.rs │ └── states.rs │ ├── item │ ├── item.rs │ ├── item_info.rs │ └── mod.rs │ ├── lib.rs │ ├── uln.rs │ └── world │ ├── lighting.rs │ ├── location.rs │ └── mod.rs ├── quartz ├── Cargo.toml ├── build.rs ├── format.sh └── src │ ├── base │ ├── assets.rs │ ├── config.rs │ ├── diagnostic.rs │ ├── exec.rs │ ├── mod.rs │ ├── registries.rs │ ├── server.rs │ └── static_registry.rs │ ├── block │ ├── entities │ │ └── furnace_entity.rs │ ├── entity.rs │ ├── init.rs │ ├── mod.rs │ └── state.rs │ ├── command │ ├── context.rs │ ├── executor.rs │ └── mod.rs │ ├── entities │ ├── mod.rs │ └── player.rs │ ├── item │ ├── init.rs │ ├── inventory.rs │ ├── item_stack.rs │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── network │ ├── connection.rs │ ├── handler.rs │ ├── mod.rs │ └── packet.rs │ ├── scheduler │ ├── mod.rs │ └── task.rs │ └── world │ ├── chunk │ ├── chunk.rs │ ├── error.rs │ ├── gen │ │ ├── density_function.rs │ │ └── mod.rs │ ├── palette.rs │ ├── provider.rs │ ├── section.rs │ └── states.rs │ ├── mod.rs │ └── world.rs ├── rust-toolchain.toml └── util ├── Cargo.toml └── src ├── hash.rs ├── lib.rs ├── logging.rs ├── map.rs ├── math.rs ├── single_access.rs └── variant.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Decompiled, mapped Minecraft Java source 2 | source/ 3 | 4 | # Server generated files 5 | run/ 6 | logs/ 7 | plugins/ 8 | config.json 9 | 10 | # Pre-build artifacts 11 | node_modules/ 12 | parser.js 13 | 14 | # Cargo and rustc artifacts 15 | target/ 16 | Cargo.lock 17 | **/*.rs.bk 18 | 19 | # IDE files 20 | .vscode/ -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | tab_spaces = 4 3 | use_field_init_shorthand = true 4 | use_try_shorthand = true 5 | max_width = 100 6 | use_small_heuristics = "Default" 7 | fn_args_layout = "Tall" 8 | 9 | unstable_features = true 10 | 11 | blank_lines_upper_bound = 2 12 | brace_style = "SameLineWhere" 13 | condense_wildcard_suffixes = true 14 | control_brace_style = "AlwaysSameLine" 15 | format_strings = true 16 | imports_layout = "HorizontalVertical" 17 | match_arm_blocks = false 18 | imports_granularity = "Crate" 19 | overflow_delimited_expr = true 20 | reorder_impl_items = true 21 | space_after_colon = true 22 | spaces_around_ranges = true 23 | trailing_semicolon = true 24 | where_single_line = true -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "chat", 4 | "quartz", 5 | "macros", 6 | "macros_impl", 7 | "util", 8 | "qdat", 9 | "network", 10 | "datapack" 11 | ] 12 | 13 | [profile.release] 14 | lto = "fat" 15 | debug = true 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cassy343, maddymakesgames 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quartz 2 | A reimplementation of a minecraft server in rust. 3 | Currently working on supporting 1.18.2 4 | 5 | ## Mission Statement 6 | 7 | Be better than feather. 8 | 9 | ## Current Features 10 | - [x] Logging 11 | - [x] Console commands 12 | - [x] Server List ping 13 | - [x] Initial Join Game flow 14 | - [x] Chunk loading 15 | 16 | ## Planned Features 17 | We plan on trying to be as 1:1 to vanilla as we can with the exception of any current bugs in vanilla
18 | We also plan on having our own plugin system to allow extending the functionality of Quartz 19 | 20 | ## Related Repos 21 | [Quartz Proxy](https://github.com/Rusty-Quartz/quartz_proxy), used to test reading and writing of packets and to log data the vanilla server sends
22 | [Quartz Commands](https://github.com/Rusty-Quartz/quartz_commands), the command system used to handle console and in-game commands
23 | [Quartz NBT](https://github.com/Rusty-Quartz/quartz_nbt), our nbt and snbt crate
24 | [Quartz Data Generators](https://github.com/Rusty-Quartz/data-generator), a mod for Minecraft that exports JSON files to be used in our buildscripts 25 | 26 | ### Credits 27 | Packet info and minecraft datatypes from the [minecraft protocol wiki](https://wiki.vg/) 28 | Info about the format of datapacks taken from [Minsode's datapack generator](https://misode.github.io/) 29 | Other info taken from minecraft source deobfuscated using [parchment mappings](https://parchmentmc.org/), [quilt mappings](https://github.com/QuiltMC/quilt-mappings), and Mojang's own mappings 30 | -------------------------------------------------------------------------------- /assets/dimension.snbt: -------------------------------------------------------------------------------- 1 | { 2 | min_y: 0b, 3 | natural: 1b, 4 | has_raids: 1b, 5 | respawn_anchor_works: 0b, 6 | has_skylight: 1b, 7 | has_ceiling: 0b, 8 | logical_height: 256s, 9 | height: 256s, 10 | effects: "minecraft:overworld", 11 | bed_works: 1b, 12 | infiniburn: "minecraft:infiniburn_overworld", 13 | piglin_safe: 0b, 14 | ambient_light: 0.0f, 15 | coordinate_scale: 1.0f, 16 | ultrawarm: 0b 17 | } -------------------------------------------------------------------------------- /assets/mappings.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "varint": "i32", 4 | "varlong": "i64", 5 | "string": "String", 6 | "unlocalized_name": "qdat::UnlocalizedName", 7 | "uuid": "uuid::Uuid", 8 | "nbt_tag": "quartz_nbt::NbtCompound", 9 | "box_nbt_tag": "Box", 10 | "position": "qdat::world::location::BlockPosition", 11 | "slot": "quartz_net::packet_data::Slot", 12 | "chat": "Box", 13 | "tab_complete_match": "quartz_net::packet_data::TabCompleteMatch", 14 | "statistic": "quartz_net::packet_data::Statistic", 15 | "block_lights": "qdat::world::lighting::LightBuffer", 16 | "map_icon": "quartz_net::packet_data::MapIcon", 17 | "villager_trade": "quartz_net::packet_data::VillagerTrade", 18 | "entity_metadata_wrapper": "quartz_net::packet_data::EntityMetadataWrapper", 19 | "inventory_slot": "quartz_net::packet_data::InventorySlot", 20 | "equipment_slot": "quartz_net::packet_data::EquipmentSlot", 21 | "advancement_map_element": "quartz_net::packet_data::AdvancementMapElement", 22 | "advancement_progress_map_element": "quartz_net::packet_data::AdvancementProgressMapElement", 23 | "advancement": "quartz_net::packet_data::Advancement", 24 | "advancement_requirements": "quartz_net::packet_data::AdvancementRequirements", 25 | "advancement_progress": "quartz_net::packet_data::AdvancementProgress", 26 | "advancement_progress_criteria": "quartz_net::packet_data::AdvancementProgressCriteria", 27 | "criteria_progress": "quartz_net::packet_data::CriteriaProgress", 28 | "advancement_display": "quartz_net::packet_data::AdvancementDisplay", 29 | "entity_property": "quartz_net::packet_data::EntityProperty", 30 | "attribute_modifier": "quartz_net::packet_data::AttributeModifier", 31 | "recipe": "quartz_net::packet_data::Recipe", 32 | "entity_metadata": "quartz_net::packet_data::EntityMetadata", 33 | "particle": "quartz_net::packet_data::ParticleData", 34 | "wrapped_player_info_action": "quartz_net::packet_data::WrappedPlayerInfoAction", 35 | "player_info_action": "quartz_net::packet_data::PlayerInfoAction", 36 | "tag_array": "quartz_net::packet_data::TagArray", 37 | "section_data": "quartz_net::packet_data::SectionData", 38 | "jigsaw_update_data": "Box", 39 | "mask": "quartz_net::BitMask", 40 | "gamemode": "qdat::Gamemode", 41 | "facing": "qdat::world::location::BlockFace" 42 | }, 43 | "primitives": [ 44 | "bool", 45 | "i8", 46 | "u8", 47 | "i16", 48 | "u16", 49 | "i32", 50 | "i64", 51 | "f32", 52 | "f64", 53 | "u128", 54 | "usize", 55 | "varint", 56 | "varlong", 57 | "uuid" 58 | ], 59 | "variable_repr": [ 60 | "varint", 61 | "varlong" 62 | ] 63 | } -------------------------------------------------------------------------------- /chat/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz_chat" 3 | version = "0.1.0" 4 | authors = ["Ian Smith "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | bitflags = "1.3.2" 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | serde_with = "1.9" 12 | quartz_nbt = "0.2.5" 13 | 14 | [target.'cfg(unix)'.dependencies] 15 | termion = "1.5" -------------------------------------------------------------------------------- /chat/src/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{color::Color, component::*}; 2 | 3 | /// Utility struct for building text components. Components can have children, and those children 4 | /// can have children, etc., however this utility only allows for a base component with a list of 5 | /// standalone child components. A new child with different formatting can be appended via the `add` 6 | /// method, and the same methods for adjusting color and formatting can be used to modify that child 7 | /// component. 8 | pub struct ComponentBuilder { 9 | component: Component, 10 | current_empty: bool, 11 | } 12 | 13 | impl ComponentBuilder { 14 | /// Creates a builder whose base component has no color and an empty text field. 15 | pub const fn empty() -> Self { 16 | ComponentBuilder { 17 | component: Component::empty(), 18 | current_empty: false, 19 | } 20 | } 21 | 22 | /// Creates a builder whose base component has no text and the color white. 23 | pub fn new() -> Self { 24 | ComponentBuilder { 25 | component: Component::colored(String::new(), Color::White), 26 | current_empty: false, 27 | } 28 | } 29 | 30 | /// Retrieves the current component being built. 31 | fn current(&mut self) -> &mut Component { 32 | self.current_empty = false; 33 | 34 | if self.component.has_children() { 35 | // These unwraps are checked above 36 | self.component.extra.as_mut().unwrap().last_mut().unwrap() 37 | } else { 38 | &mut self.component 39 | } 40 | } 41 | 42 | /// Finish the current component and prepare a new component which can have a different color, formatting, etc. 43 | #[allow(clippy::should_implement_trait)] 44 | pub fn add(mut self, component_type: ComponentType) -> Self { 45 | self.current().component_type = component_type; 46 | self.add_empty() 47 | } 48 | 49 | /// Finish the current component, setting its text field to the given value, and prepare a new component 50 | /// which can have a different color, formatting, etc. 51 | pub fn add_text(self, text: T) -> Self { 52 | self.add(ComponentType::text(text)) 53 | } 54 | 55 | /// Finish the current component, setting its text field to an empty string, and prepare a new component 56 | /// which can have a different color, formatting, etc. 57 | pub fn add_empty(mut self) -> Self { 58 | if !self.current_empty { 59 | self.component.add_child(Component::empty()); 60 | self.current_empty = true; 61 | } 62 | 63 | self 64 | } 65 | 66 | /// Consumes this builder and returns a finished text component. 67 | pub fn build(mut self) -> Component { 68 | if self.current_empty { 69 | let _ = self.component.extra.as_mut().unwrap().pop(); 70 | } 71 | 72 | self.component 73 | } 74 | 75 | /// Consumes this builder and returns a vec of the base component's children, excluding the base 76 | /// component itself. 77 | pub fn build_children(mut self) -> Vec { 78 | if self.current_empty { 79 | let _ = self.component.extra.as_mut().unwrap().pop(); 80 | } 81 | 82 | match self.component.extra { 83 | Some(children) => children, 84 | None => Vec::new(), 85 | } 86 | } 87 | 88 | /// Set the color of the current component to the given color. 89 | pub fn color>(mut self, color: C) -> Self { 90 | self.current().color = Some(color.into()); 91 | self 92 | } 93 | 94 | /// Set the color of the current component to a custom color. 95 | pub fn custom_color(mut self, red: u8, green: u8, blue: u8) -> Self { 96 | self.current().color = Some(Color::Custom(red, green, blue)); 97 | self 98 | } 99 | 100 | /// Set the format of the current component according to the given flags. 101 | pub fn format(mut self, format: Format) -> Self { 102 | self.current().format = format; 103 | self 104 | } 105 | 106 | /// Set the font of this component. 107 | pub fn font(mut self, font: Font) -> Self { 108 | self.current().font = Some(font); 109 | self 110 | } 111 | 112 | /// Set the insertion text of this component (the text inserted into a player's chat when 113 | /// they shift-click this component). 114 | pub fn insertion(mut self, insertion: String) -> Self { 115 | self.current().insertion = Some(insertion); 116 | self 117 | } 118 | 119 | /// Set the click event of this component. 120 | pub fn click_event(mut self, event: ClickEvent) -> Self { 121 | self.current().click_event = Some(Box::new(event)); 122 | self 123 | } 124 | 125 | /// Set the hover event of this component. 126 | pub fn hover_event(mut self, event: HoverEvent) -> Self { 127 | self.current().hover_event = Some(Box::new(event)); 128 | self 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /chat/src/color.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::fmt::{self, Formatter}; 3 | 4 | use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 5 | 6 | #[cfg(unix)] 7 | use termion::{color, style}; 8 | 9 | /// Highest level definition of a chat color which can either be predefined or custom as of minecraft 10 | /// 1.16. 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 12 | #[doc(hidden)] 13 | pub enum Color { 14 | Black, 15 | DarkBlue, 16 | DarkGreen, 17 | DarkAqua, 18 | DarkRed, 19 | DarkPurple, 20 | Gold, 21 | Gray, 22 | DarkGray, 23 | Blue, 24 | Green, 25 | Aqua, 26 | Red, 27 | LightPurple, 28 | Yellow, 29 | White, 30 | Reset, 31 | /// A custom RBG color. 32 | Custom(u8, u8, u8), 33 | } 34 | 35 | impl Serialize for Color { 36 | fn serialize(&self, serializer: S) -> Result 37 | where S: Serializer { 38 | let string = match self { 39 | Color::Black => "black", 40 | Color::DarkBlue => "dark_blue", 41 | Color::DarkGreen => "dark_green", 42 | Color::DarkAqua => "dark_aqua", 43 | Color::DarkRed => "dark_red", 44 | Color::DarkPurple => "dark_purple", 45 | Color::Gold => "gold", 46 | Color::Gray => "gray", 47 | Color::DarkGray => "dark_gray", 48 | Color::Blue => "blue", 49 | Color::Green => "green", 50 | Color::Aqua => "aqua", 51 | Color::Red => "red", 52 | Color::LightPurple => "light_purple", 53 | Color::Yellow => "yellow", 54 | Color::White => "white", 55 | Color::Reset => "reset", 56 | &Color::Custom(r, g, b) => { 57 | return serializer.serialize_str(&format!( 58 | "#{:06X}", 59 | (r as u32) << 16 | (g as u32) << 8 | (b as u32) 60 | )); 61 | } 62 | }; 63 | 64 | serializer.serialize_str(string) 65 | } 66 | } 67 | 68 | impl<'de> Deserialize<'de> for Color { 69 | fn deserialize(deserializer: D) -> Result 70 | where D: Deserializer<'de> { 71 | let string: &'de str = Deserialize::deserialize(deserializer)?; 72 | 73 | match string { 74 | "black" => Ok(Color::Black), 75 | "dark_blue" => Ok(Color::DarkBlue), 76 | "dark_green" => Ok(Color::DarkGreen), 77 | "dark_aqua" => Ok(Color::DarkAqua), 78 | "dark_red" => Ok(Color::DarkRed), 79 | "dark_purple" => Ok(Color::DarkPurple), 80 | "gold" => Ok(Color::Gold), 81 | "gray" => Ok(Color::Gray), 82 | "dark_gray" => Ok(Color::DarkGray), 83 | "blue" => Ok(Color::Blue), 84 | "green" => Ok(Color::Green), 85 | "aqua" => Ok(Color::Aqua), 86 | "red" => Ok(Color::Red), 87 | "light_purple" => Ok(Color::LightPurple), 88 | "yellow" => Ok(Color::Yellow), 89 | "white" => Ok(Color::White), 90 | "reset" => Ok(Color::Reset), 91 | _ => { 92 | if string.is_empty() { 93 | return Err(de::Error::custom( 94 | "Expected hex color, found an empty string.", 95 | )); 96 | } 97 | 98 | if string.len() != 7 { 99 | return Err(de::Error::custom( 100 | "Expected hex color in the form of '#RRGGBB'", 101 | )); 102 | } 103 | 104 | if let Ok(rgb) = u32::from_str_radix(&string[1 ..], 16) { 105 | Ok(Color::Custom( 106 | (rgb >> 16) as u8, 107 | (rgb >> 8) as u8, 108 | rgb as u8, 109 | )) 110 | } else { 111 | Err(de::Error::custom( 112 | "Invalid hex color, expected 6 hexadecimal digits (0-F).", 113 | )) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | impl Color { 121 | /// Applies the color to the terminal (unix only). 122 | #[cfg(unix)] 123 | pub fn apply(&self, f: &mut Formatter) -> fmt::Result { 124 | match self { 125 | Color::Black => write!(f, "{}", color::Fg(color::Black)), 126 | Color::DarkBlue => write!(f, "{}", color::Fg(color::Blue)), 127 | Color::DarkGreen => write!(f, "{}", color::Fg(color::Green)), 128 | Color::DarkAqua => write!(f, "{}", color::Fg(color::Cyan)), 129 | Color::DarkRed => write!(f, "{}", color::Fg(color::Red)), 130 | Color::DarkPurple => write!(f, "{}", color::Fg(color::Magenta)), 131 | Color::Gold => write!(f, "{}", color::Fg(color::Yellow)), 132 | Color::Gray => write!(f, "{}", color::Fg(color::White)), 133 | Color::DarkGray => write!(f, "{}", color::Fg(color::LightBlack)), 134 | Color::Blue => write!(f, "{}", color::Fg(color::LightBlue)), 135 | Color::Green => write!(f, "{}", color::Fg(color::LightGreen)), 136 | Color::Aqua => write!(f, "{}", color::Fg(color::LightCyan)), 137 | Color::Red => write!(f, "{}", color::Fg(color::LightRed)), 138 | Color::LightPurple => write!(f, "{}", color::Fg(color::LightMagenta)), 139 | Color::Yellow => write!(f, "{}", color::Fg(color::LightYellow)), 140 | Color::White => write!(f, "{}", color::Fg(color::LightWhite)), 141 | Color::Reset => write!(f, "{}{}", color::Fg(color::Reset), style::Reset), 142 | // Dividing by 43 maps the color to the correct ANSI range of [0,5] 143 | &Color::Custom(r, g, b) => write!( 144 | f, 145 | "{}", 146 | color::Fg(color::AnsiValue::rgb(r / 43, g / 43, b / 43)) 147 | ), 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /chat/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! Provides support for minecraft chat components. 4 | 5 | /// Contains component builders for in-code component creation. 6 | mod builder; 7 | /// Defines and handles the application of chat colors. 8 | pub mod color; 9 | /// Defines chat components and their variants. 10 | pub mod component; 11 | 12 | pub use builder::ComponentBuilder; 13 | pub use component::Component; 14 | -------------------------------------------------------------------------------- /datapack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz_datapack" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | qdat = { path = "../qdat" } 10 | quartz_chat = { path = "../chat" } 11 | 12 | quartz_nbt = { version = "0.2.4", features = ["serde"]} 13 | 14 | serde = {version = "1", features = ["derive"]} 15 | serde_json = "1" -------------------------------------------------------------------------------- /datapack/src/data/advancement/display.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use quartz_chat::Component; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// The display element of an [Advancement] 6 | #[derive(Serialize, Deserialize)] 7 | pub struct AdvancementDisplay { 8 | pub icon: Option, 9 | // TODO: this is a text component 10 | pub title: AdvancementDisplayText, 11 | pub frame: Option, 12 | pub background: Option, 13 | // Same as title 14 | pub description: AdvancementDisplayText, 15 | pub show_toast: bool, 16 | pub announce_to_chat: bool, 17 | pub hidden: bool, 18 | } 19 | 20 | /// The icon of an [AdvancementDisplay] 21 | #[derive(Serialize, Deserialize)] 22 | pub struct AdvancementIcon { 23 | pub item: UnlocalizedName, 24 | // This is actual snbt supposidly? idk I'll deal with it later 25 | // TODO: make this actual nbt 26 | pub nbt: Option, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | pub enum AdvancementFrame { 31 | #[serde(rename = "task")] 32 | Task, 33 | #[serde(rename = "challenge")] 34 | Challenge, 35 | #[serde(rename = "goal")] 36 | Goal, 37 | } 38 | 39 | #[derive(Serialize, Deserialize)] 40 | #[serde(untagged)] 41 | pub enum AdvancementDisplayText { 42 | String(String), 43 | TextComponent(Component), 44 | TextComponentList(Vec), 45 | // WHY THE FUCK IS THIS VALID 46 | Number(f32), 47 | Boolean(bool), 48 | } 49 | -------------------------------------------------------------------------------- /datapack/src/data/advancement/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use qdat::UnlocalizedName; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub mod conditions; 8 | pub mod display; 9 | 10 | use crate::data::advancement::conditions::AdvancementConditions; 11 | 12 | use self::display::AdvancementDisplay; 13 | 14 | /// An advancement 15 | #[derive(Serialize, Deserialize)] 16 | pub struct Advancement { 17 | pub display: Option, 18 | pub parent: Option, 19 | pub criteria: HashMap, 20 | pub requirements: Option, 21 | pub rewards: Option, 22 | } 23 | 24 | #[derive(Serialize, Deserialize)] 25 | #[serde(untagged)] 26 | pub enum AdvancementRequirements { 27 | /// A list of the required criteria 28 | List(Vec), 29 | /// A list of lists of criteria 30 | /// 31 | /// All of the lists only have to have one of their criteria met 32 | /// 33 | /// Basically ANDing of OR groups 34 | LogicalList(Vec>), 35 | } 36 | 37 | /// The rewards of an [Advancement] 38 | #[derive(Serialize, Deserialize)] 39 | pub struct AdvancementRewards { 40 | pub recipes: Option>, 41 | pub loot: Option>, 42 | pub experience: Option, 43 | pub function: Option, 44 | } 45 | -------------------------------------------------------------------------------- /datapack/src/data/functions.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | 3 | /// A list of commands loaded from a mcfunction file 4 | /// 5 | /// This is just the contents of the file but with comments stripped 6 | pub type Function = Vec; 7 | 8 | /// Reads the lines of a string from `reader` and returns a Function after stripping the comments 9 | pub fn read_function(mut reader: T) -> std::io::Result { 10 | let mut buf = String::new(); 11 | reader.read_to_string(&mut buf)?; 12 | 13 | Ok(buf 14 | .lines() 15 | .filter_map(|l| { 16 | // Filter out all comment lines 17 | if !l.starts_with('#') { 18 | Some(l.to_owned()) 19 | } else { 20 | None 21 | } 22 | }) 23 | .collect()) 24 | } 25 | 26 | pub fn write_function(function: &Function, mut writer: T) -> std::io::Result<()> { 27 | write!(writer, "{}", function.join("\n")) 28 | } 29 | -------------------------------------------------------------------------------- /datapack/src/data/item_modifiers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use qdat::UnlocalizedName; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::{ 7 | datatypes::AmountOrRange, 8 | loot_tables::NumberProvider, 9 | noise_settings::BlockState, 10 | predicate::Predicate, 11 | }; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct ItemModifier { 15 | #[serde(flatten)] 16 | modifier: ItemModifierType, 17 | conditions: Vec, 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | #[serde(rename_all = "snake_case")] 22 | pub enum ItemModifierType { 23 | ApplyBonus { 24 | enchantment: UnlocalizedName, 25 | formula: String, 26 | parameters: ApplyBonusParameters, 27 | }, 28 | CopyName { 29 | source: String, 30 | }, 31 | CopyNbt { 32 | source: NbtSource, 33 | ops: Vec, 34 | }, 35 | CopyState(BlockState), 36 | EnchantRandomly { 37 | enchantments: Vec, 38 | }, 39 | EnchantWithLevels { 40 | treasure: bool, 41 | levels: NumberProvider, 42 | }, 43 | ExplorationMap { 44 | destination: String, 45 | decoration: String, 46 | zoom: Option, 47 | search_results: Option, 48 | skip_existing_chunks: Option, 49 | }, 50 | ExplosionDelay, 51 | FurnaceSmelt, 52 | FillPlayerHead { 53 | entity: String, 54 | }, 55 | LimitCount { 56 | limit: AmountOrRange>, 57 | }, 58 | LootingEnchant { 59 | count: NumberProvider, 60 | limit: i32, 61 | }, 62 | SetAttributes { 63 | modifiers: Vec, 64 | }, 65 | SetBannerPatern { 66 | patterns: Vec, 67 | #[serde(default = "Default::default")] 68 | append: bool, 69 | }, 70 | SetContents { 71 | // NOTE: no clue what datatype this is lol 72 | entries: Vec, 73 | }, 74 | SetCount { 75 | count: NumberProvider, 76 | #[serde(default = "Default::default")] 77 | add: bool, 78 | }, 79 | SetDamage { 80 | damage: NumberProvider, 81 | #[serde(default = "Default::default")] 82 | add: bool, 83 | }, 84 | SetEnchantments { 85 | enchantments: HashMap>, 86 | #[serde(default = "Default::default")] 87 | add: bool, 88 | }, 89 | SetLootTable { 90 | name: UnlocalizedName, 91 | #[serde(default = "Default::default")] 92 | seed: i32, 93 | }, 94 | SetLore { 95 | // TODO: this is a json text component 96 | lore: Vec, 97 | entry: String, 98 | replace: bool, 99 | }, 100 | SetName { 101 | // TODO: this is a json text component 102 | name: String, 103 | entity: String, 104 | }, 105 | SetNbt { 106 | tag: String, 107 | }, 108 | SetStewEffect { 109 | effects: Vec, 110 | }, 111 | } 112 | 113 | #[derive(Serialize, Deserialize)] 114 | pub struct ApplyBonusParameters { 115 | extra: i32, 116 | probability: f32, 117 | #[serde(rename = "bonusMultiplier")] 118 | bonus_multiplier: f32, 119 | } 120 | 121 | #[derive(Serialize, Deserialize)] 122 | #[serde(rename_all = "snake_case")] 123 | #[serde(tag = "type")] 124 | pub enum NbtSource { 125 | Context { target: String }, 126 | Storage { source: UnlocalizedName }, 127 | } 128 | 129 | #[derive(Serialize, Deserialize)] 130 | pub struct NbtOpData { 131 | source: String, 132 | target: String, 133 | op: NbtOp, 134 | } 135 | 136 | #[derive(Serialize, Deserialize)] 137 | #[serde(rename_all = "snake_case")] 138 | pub enum NbtOp { 139 | Replace, 140 | Append, 141 | Merge, 142 | } 143 | 144 | #[derive(Serialize, Deserialize)] 145 | pub struct AttributeModifier { 146 | name: String, 147 | atrribute: String, 148 | operation: AttributeModifierOperation, 149 | amount: NumberProvider, 150 | id: Option, 151 | slot: AttributeModifierSlot, 152 | } 153 | 154 | #[derive(Serialize, Deserialize)] 155 | #[serde(rename_all = "snake_case")] 156 | pub enum AttributeModifierOperation { 157 | Addition, 158 | MultiplyBase, 159 | MultiplyTotal, 160 | } 161 | 162 | #[derive(Serialize, Deserialize)] 163 | #[serde(untagged)] 164 | pub enum AttributeModifierSlots { 165 | Singleton(AttributeModifierSlot), 166 | List(Vec), 167 | } 168 | 169 | #[derive(Serialize, Deserialize)] 170 | #[serde(rename_all = "lowercase")] 171 | pub enum AttributeModifierSlot { 172 | MainHand, 173 | OffHand, 174 | Feet, 175 | Legs, 176 | Chest, 177 | Head, 178 | } 179 | 180 | #[derive(Serialize, Deserialize)] 181 | pub struct BannerPattern { 182 | pattern: String, 183 | color: BannerColor, 184 | } 185 | 186 | #[derive(Serialize, Deserialize)] 187 | pub enum BannerColor { 188 | White, 189 | Orange, 190 | Magenta, 191 | LightBlue, 192 | Yellow, 193 | Lime, 194 | Pink, 195 | Gray, 196 | LightGray, 197 | Cyan, 198 | Purple, 199 | Blue, 200 | Brown, 201 | Green, 202 | Red, 203 | Black, 204 | } 205 | 206 | #[derive(Serialize, Deserialize)] 207 | pub struct StewEffect { 208 | r#type: UnlocalizedName, 209 | duration: NumberProvider, 210 | } 211 | -------------------------------------------------------------------------------- /datapack/src/data/loot_tables.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use qdat::UnlocalizedName; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::data::{ 7 | datatypes::{AmountOrRange, Enchantment, Entity, EntityType, PredicateLocation}, 8 | predicate::Predicate, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub struct LootTable { 13 | pub r#type: LootTableType, 14 | #[serde(default)] 15 | pub functions: Vec, 16 | #[serde(default)] 17 | pub pools: Vec, 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub enum LootTableType { 22 | #[serde(rename = "minecraft:empty")] 23 | Empty, 24 | #[serde(rename = "minecraft:entity")] 25 | Entity, 26 | #[serde(rename = "minecraft:block")] 27 | Block, 28 | #[serde(rename = "minecraft:chest")] 29 | Chest, 30 | #[serde(rename = "minecraft:fishing")] 31 | Fishing, 32 | #[serde(rename = "minecraft:gift")] 33 | Gift, 34 | #[serde(rename = "minecraft:advancement_reward")] 35 | AdvancementReward, 36 | #[serde(rename = "minecraft:barter")] 37 | Barter, 38 | #[serde(rename = "minecraft:command")] 39 | Command, 40 | #[serde(rename = "minecraft:selector")] 41 | Selector, 42 | #[serde(rename = "minecraft:advancement_entity")] 43 | AdvancementEntity, 44 | #[serde(rename = "minecraft:generic")] 45 | Generic, 46 | } 47 | 48 | #[derive(Serialize, Deserialize)] 49 | pub struct LootTableFunction { 50 | pub function: UnlocalizedName, 51 | #[serde(default)] 52 | pub conditions: Vec, 53 | } 54 | 55 | #[derive(Serialize, Deserialize)] 56 | pub struct LootTablePool { 57 | #[serde(default)] 58 | pub conditions: Vec, 59 | #[serde(default)] 60 | pub functions: Vec, 61 | pub rolls: NumberProvider, 62 | pub bonus_rolls: NumberProvider, 63 | #[serde(default)] 64 | pub entries: Vec, 65 | } 66 | 67 | #[derive(Serialize, Deserialize)] 68 | pub struct LootTableEntry { 69 | #[serde(default)] 70 | pub conditions: Vec, 71 | #[serde(default)] 72 | pub functions: Vec, 73 | #[serde(flatten)] 74 | pub r#type: LootTableEntryType, 75 | } 76 | 77 | #[derive(Serialize, Deserialize)] 78 | #[serde(tag = "condition")] 79 | pub enum LootTableCondition { 80 | #[serde(rename = "minecraft:alternative")] 81 | Alternative { terms: Vec }, 82 | #[serde(rename = "minecraft:block_state_property")] 83 | BlockStateProperty { 84 | block: UnlocalizedName, 85 | properties: HashMap, 86 | }, 87 | #[serde(rename = "minecraft:entity_properties")] 88 | EntityProperties { 89 | // This has to be "this" 90 | entity: String, 91 | predicate: Box, 92 | }, 93 | #[serde(rename = "minecraft:entity_scores")] 94 | EntityScores { 95 | // Has to be "this" 96 | entity: String, 97 | scores: HashMap>, 98 | }, 99 | #[serde(rename = "minecraft:inverted")] 100 | Inverted { term: Predicate }, 101 | #[serde(rename = "minecraft:location_check")] 102 | LocationCheck { 103 | #[serde(rename = "offsetX")] 104 | x_offset: Option, 105 | #[serde(rename = "offsetY")] 106 | y_offset: Option, 107 | #[serde(rename = "offsetZ")] 108 | z_offset: Option, 109 | predicate: Option>, 110 | }, 111 | #[serde(rename = "minecraft:match_tool")] 112 | MatchTool { 113 | #[serde(default)] 114 | items: Vec, 115 | tag: Option, 116 | count: Option>, 117 | durability: Option>, 118 | potion: Option, 119 | nbt: Option, 120 | enchantments: Option, 121 | }, 122 | #[serde(rename = "minecraft:random_chance")] 123 | RandomChance { chance: f32 }, 124 | #[serde(rename = "minecraft:reference")] 125 | Reference { name: UnlocalizedName }, 126 | #[serde(rename = "minecraft:survives_explosion")] 127 | SurvivesExplosion, 128 | #[serde(rename = "minecraft:table_bonus")] 129 | TableBonus { 130 | enchantment: UnlocalizedName, 131 | #[serde(default)] 132 | chances: Vec, 133 | }, 134 | #[serde(rename = "minecraft:time_check")] 135 | TimeCheck { 136 | value: Option>, 137 | period: Option, 138 | }, 139 | #[serde(rename = "minecraft:value_check")] 140 | ValueCheck { 141 | value: Option>, 142 | range: Option>, 143 | }, 144 | #[serde(rename = "minecraft:weather_check")] 145 | WeatherCheck { 146 | raining: Option, 147 | thundering: Option, 148 | }, 149 | } 150 | 151 | #[derive(Serialize, Deserialize)] 152 | #[serde(tag = "type")] 153 | pub enum LootTableEntryType { 154 | #[serde(rename = "minecraft:item")] 155 | Item { name: UnlocalizedName }, 156 | #[serde(rename = "minecraft:tag")] 157 | Tag { 158 | name: UnlocalizedName, 159 | #[serde(default)] 160 | expand: bool, 161 | }, 162 | #[serde(rename = "minecraft:loot_table")] 163 | LootTable { name: UnlocalizedName }, 164 | #[serde(rename = "minecraft:group")] 165 | Group { childern: Vec> }, 166 | #[serde(rename = "minecraft:alternatives")] 167 | Alternatives { children: Vec> }, 168 | #[serde(rename = "minecraft:sequence")] 169 | Sequence { children: Vec> }, 170 | #[serde(rename = "minecraft:dynamic")] 171 | Dynamic { name: UnlocalizedName }, 172 | #[serde(rename = "minecraft:empty")] 173 | Empty, 174 | } 175 | 176 | #[derive(Serialize, Deserialize)] 177 | #[serde(untagged)] 178 | pub enum NumberProvider { 179 | Singleton(T), 180 | Object(NumberProviderInternal), 181 | } 182 | 183 | #[derive(Serialize, Deserialize)] 184 | #[serde(tag = "type")] 185 | pub enum NumberProviderInternal { 186 | #[serde(rename = "minecraft:constant")] 187 | Constant { value: T }, 188 | #[serde(rename = "minecraft:uniform")] 189 | Uniform { 190 | max: Box>, 191 | min: Box>, 192 | }, 193 | #[serde(rename = "minecraft:binomial")] 194 | Binomial { 195 | n: Box>, 196 | p: Box>, 197 | }, 198 | #[serde(rename = "minecraft:score")] 199 | Score { 200 | target: ScoreboardNumProviderTarget, 201 | score: String, 202 | scale: Option, 203 | }, 204 | } 205 | #[derive(Serialize, Deserialize)] 206 | #[serde(untagged)] 207 | pub enum ScoreboardNumProviderTarget { 208 | Constant(EntityType), 209 | Variable(ScoreboardNameProvider), 210 | } 211 | 212 | #[derive(Serialize, Deserialize)] 213 | #[serde(tag = "type")] 214 | #[serde(rename_all = "snake_case")] 215 | pub enum ScoreboardNameProvider { 216 | Fixed { name: String }, 217 | Context { target: EntityType }, 218 | } 219 | -------------------------------------------------------------------------------- /datapack/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | //! Stores the definitions of all the formats in the `data` folder of datapacks 2 | pub mod advancement; 3 | pub mod functions; 4 | pub mod loot_tables; 5 | pub mod predicate; 6 | pub mod recipe; 7 | pub mod structure; 8 | pub mod tags; 9 | mod world_gen; 10 | pub use world_gen::*; 11 | pub mod datatypes; 12 | pub mod item_modifiers; 13 | -------------------------------------------------------------------------------- /datapack/src/data/predicate.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::data::datatypes::*; 4 | use qdat::UnlocalizedName; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::loot_tables::NumberProvider; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | #[serde(tag = "condition")] 11 | pub enum Predicate { 12 | #[serde(rename = "minecraft:alternative")] 13 | Alternative { terms: Vec }, 14 | #[serde(rename = "minecraft:block_state_property")] 15 | BlockStateProperty { 16 | block: UnlocalizedName, 17 | properties: Option>, 18 | }, 19 | #[serde(rename = "minecraft:damage_source_properties")] 20 | DamageSourceProperties { predicate: Box }, 21 | #[serde(rename = "minecraft:entity_properties")] 22 | EntityPropreties { 23 | entity: EntityType, 24 | predicate: Box, 25 | }, 26 | #[serde(rename = "minecraft:entity_scores")] 27 | EntityScores { 28 | entity: EntityType, 29 | scores: HashMap>, 30 | }, 31 | #[serde(rename = "minecraft:inverted")] 32 | Inverted { term: Box }, 33 | #[serde(rename = "minecraft:killed_by_player")] 34 | KilledByPlayer { 35 | #[serde(default)] 36 | inverse: bool, 37 | }, 38 | #[serde(rename = "minecraft:location_check")] 39 | LocationCheck { 40 | #[serde(rename = "offsetX")] 41 | offset_x: Option, 42 | #[serde(rename = "offsetY")] 43 | offset_y: Option, 44 | #[serde(rename = "offsetZ")] 45 | offset_z: Option, 46 | predicate: Box, 47 | }, 48 | #[serde(rename = "minecraft:match_tool")] 49 | MatchTool { predicate: Item }, 50 | #[serde(rename = "minecraft:random_chance")] 51 | RandomChance { chance: f32 }, 52 | #[serde(rename = "minecraft:random_chance_with_looting")] 53 | RandomChanceWithLooting { 54 | chance: f32, 55 | looting_multiplier: f32, 56 | }, 57 | #[serde(rename = "minecraft:reference")] 58 | Reference { name: UnlocalizedName }, 59 | #[serde(rename = "minecraft:survives_explosion")] 60 | SurvivesExplosion, 61 | #[serde(rename = "minecraft:table_bonus")] 62 | TableBonus { 63 | enchantment: UnlocalizedName, 64 | chances: Vec, 65 | }, 66 | #[serde(rename = "minecraft:time_check")] 67 | TimeCheck { 68 | value: NumberProvider, 69 | period: i32, 70 | }, 71 | #[serde(rename = "minecraft:weather_check")] 72 | WeatherCheck { raining: bool, thundering: bool }, 73 | #[serde(rename = "minecraft:value_check")] 74 | ValueCheck { 75 | value: NumberProvider, 76 | range: AmountOrRange>, 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/cooking.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, marker::PhantomData}; 2 | 3 | use qdat::UnlocalizedName; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::data::recipe::ingredient::Ingredient; 7 | 8 | /// The generic format of a cooking recipe 9 | /// 10 | /// The only thing that changes based on the cooking type is the amount of time it takes to cook
11 | /// Custom cooking recipes can be implemented by implementing [CookingRecipeType] on a struct 12 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 13 | pub struct CookingRecipe { 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub group: Option, 16 | #[serde(rename = "ingredient")] 17 | pub input: Ingredient, 18 | pub result: UnlocalizedName, 19 | pub experience: f64, 20 | #[serde(default = "T::cook_time")] 21 | #[serde(rename = "cookingtime")] 22 | pub cook_time: u64, 23 | #[serde(skip)] 24 | pub _phatom: PhantomData, 25 | } 26 | 27 | /// Represents the different type of cooking recipes by changing the cooking time 28 | pub trait CookingRecipeType: Debug + PartialEq { 29 | /// Defines the default cooking time for this recipe type 30 | /// 31 | /// This is the only thing that differs between the cooking recipe types 32 | // NOTE: if they add const trait fns then change this to be const 33 | fn cook_time() -> u64; 34 | } 35 | 36 | #[macro_export] 37 | macro_rules! smelting_type { 38 | ($($name: ident, $time: literal),*) => { 39 | $( 40 | #[doc = "To be used as the type parameter for [CookingRecipeType](quartz_datapack::data::recipe::cooking::CookingRecipeType)"] 41 | #[derive(Debug, Eq, PartialEq)] 42 | pub struct $name; 43 | impl CookingRecipeType for $name { 44 | fn cook_time() -> u64 { 45 | $time 46 | } 47 | } 48 | )* 49 | }; 50 | } 51 | 52 | smelting_type! { 53 | SmeltingRecipe, 200, 54 | BlastingRecipe, 100, 55 | SmokingRecipe, 100, 56 | // All vanilla campfire recipies have a cook time of 600 but the default is 100 for some reason 57 | CampfireRecipe, 100 58 | } 59 | 60 | #[test] 61 | fn cooking_ser_test() { 62 | use crate::data::recipe::{ingredient::Ingredient, VanillaRecipeType}; 63 | 64 | let recipe = VanillaRecipeType::SmeltingRecipe(CookingRecipe { 65 | group: Some("copper_ingot".to_owned()), 66 | input: Ingredient::Item(UnlocalizedName::minecraft("raw_copper")), 67 | result: UnlocalizedName::minecraft("copper_ingot"), 68 | cook_time: 200, 69 | experience: 0.7, 70 | _phatom: PhantomData, 71 | }); 72 | 73 | let serialized = serde_json::to_string(&recipe).unwrap(); 74 | 75 | assert_eq!( 76 | &serialized, 77 | r#"{"type":"minecraft:smelting","group":"copper_ingot","ingredient":{"item":"minecraft:raw_copper"},"result":"minecraft:copper_ingot","experience":0.7,"cookingtime":200}"# 78 | ) 79 | } 80 | 81 | #[test] 82 | fn cooking_de_test() { 83 | use crate::data::recipe::{ingredient::Ingredient, VanillaRecipeType}; 84 | 85 | let input = r#"{ 86 | "type": "minecraft:smelting", 87 | "group": "copper_ingot", 88 | "ingredient": {"item": "minecraft:raw_copper"}, 89 | "result": "minecraft:copper_ingot", 90 | "experience": 0.7 91 | }"#; 92 | 93 | let recipe: VanillaRecipeType = serde_json::from_str(input).unwrap(); 94 | 95 | assert_eq!( 96 | recipe, 97 | VanillaRecipeType::SmeltingRecipe(CookingRecipe { 98 | group: Some("copper_ingot".to_owned()), 99 | input: Ingredient::Item(UnlocalizedName::minecraft("raw_copper")), 100 | result: UnlocalizedName::minecraft("copper_ingot"), 101 | cook_time: 200, 102 | experience: 0.7, 103 | _phatom: PhantomData, 104 | }) 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/ingredient.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{ 3 | de::Visitor, 4 | ser::{SerializeMap, SerializeSeq}, 5 | Deserialize, 6 | Serialize, 7 | }; 8 | 9 | /// Represents an ingredient in a recipe 10 | #[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Debug)] 11 | pub enum Ingredient { 12 | /// A single item 13 | Item(UnlocalizedName), 14 | /// A tag to use as the item provider 15 | Tag(UnlocalizedName), 16 | /// A list of ingredients to use as an ingredient provider 17 | /// # Note 18 | ///Do not have nested Ingredient::Lists 19 | List(Box<[Ingredient]>), 20 | } 21 | 22 | impl Ingredient { 23 | pub fn is_empty(&self) -> bool { 24 | match self { 25 | Ingredient::Item(i) => i == "minecraft:air", 26 | // As long as the UnlocalizedName is valid we need to resolve the tag 27 | Ingredient::Tag(_) => false, 28 | // the iter.any call can be expensive if the array is large 29 | // in practice lists should p much not be used cause tags exist sooooooo 30 | Ingredient::List(l) => l.len() == 0 || l.iter().any(|i| i.is_empty()), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Serialize, Deserialize)] 36 | struct ItemJSON { 37 | pub item: String, 38 | } 39 | #[derive(Serialize, Deserialize)] 40 | struct TagJSON { 41 | pub tag: String, 42 | } 43 | impl Serialize for Ingredient { 44 | fn serialize(&self, serializer: S) -> Result 45 | where S: serde::Serializer { 46 | match self { 47 | Ingredient::Item(i) => { 48 | let mut struc = serializer.serialize_map(Some(1))?; 49 | struc.serialize_entry("item", &i.to_string())?; 50 | struc.end() 51 | } 52 | Ingredient::Tag(t) => { 53 | let mut struc = serializer.serialize_map(Some(1))?; 54 | struc.serialize_entry("tag", &t.to_string())?; 55 | struc.end() 56 | } 57 | Ingredient::List(l) => { 58 | let mut arr = serializer.serialize_seq(Some(l.len()))?; 59 | for ingr in l.iter() { 60 | match ingr { 61 | Ingredient::Item(i) => { 62 | arr.serialize_element(&ItemJSON { 63 | item: i.to_string(), 64 | })?; 65 | } 66 | Ingredient::Tag(t) => { 67 | arr.serialize_element(&TagJSON { tag: t.to_string() })?; 68 | } 69 | Ingredient::List(_) => 70 | return Err(serde::ser::Error::custom( 71 | "You can't have a list of ingredients inside another list of \ 72 | ingredients", 73 | )), 74 | } 75 | } 76 | arr.end() 77 | } 78 | } 79 | } 80 | } 81 | 82 | impl<'de> Deserialize<'de> for Ingredient { 83 | fn deserialize(deserializer: D) -> Result 84 | where D: serde::Deserializer<'de> { 85 | deserializer.deserialize_any(IngredientVisitor) 86 | } 87 | } 88 | 89 | struct IngredientVisitor; 90 | 91 | impl<'de> Visitor<'de> for IngredientVisitor { 92 | type Value = Ingredient; 93 | 94 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 95 | formatter.write_str("An ingredient") 96 | } 97 | 98 | fn visit_map(self, mut map: A) -> Result 99 | where A: serde::de::MapAccess<'de> { 100 | let mut ingr = None; 101 | while let Some((key, value)) = map.next_entry::<&str, &str>()? { 102 | let uln = UnlocalizedName::from_str(value); 103 | if let Err(e) = uln { 104 | return Err(serde::de::Error::custom(format!( 105 | "Invalid Identifier provided: {e}" 106 | ))); 107 | } 108 | // This error message kinda sucks 109 | if ingr.is_some() { 110 | return Err(serde::de::Error::custom( 111 | "Multiple values specified in ingredient", 112 | )); 113 | } 114 | match key { 115 | "item" => ingr = Some(Ingredient::Item(uln.unwrap())), 116 | "tag" => ingr = Some(Ingredient::Tag(uln.unwrap())), 117 | _ => return Err(serde::de::Error::unknown_field(key, &["item", "tag"])), 118 | } 119 | } 120 | 121 | 122 | ingr.ok_or_else(|| serde::de::Error::missing_field("item or tag")) 123 | } 124 | 125 | fn visit_seq(self, mut seq: A) -> Result 126 | where A: serde::de::SeqAccess<'de> { 127 | let mut ingrs = Vec::new(); 128 | 129 | while let Some(ingr) = seq.next_element::()? { 130 | match ingr { 131 | Ingredient::List(_) => 132 | return Err(serde::de::Error::custom( 133 | "Cannot have an ingredient list inside a list", 134 | )), 135 | _ => { 136 | ingrs.push(ingr); 137 | } 138 | } 139 | } 140 | 141 | Ok(Ingredient::List(ingrs.into_boxed_slice())) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cooking; 2 | pub mod ingredient; 3 | #[allow(clippy::module_inception)] 4 | mod recipe; 5 | pub mod shaped; 6 | pub mod shapeless; 7 | pub mod smithing; 8 | pub mod stonecutting; 9 | 10 | pub use recipe::*; 11 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/recipe.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::data::recipe::{ 5 | cooking::*, 6 | shaped::ShapedCraftingRecipe, 7 | shapeless::ShapelessCraftingRecipe, 8 | smithing::SmithingRecipe, 9 | stonecutting::StonecuttingRecipe, 10 | }; 11 | 12 | 13 | /// The output type of most recipe types 14 | #[derive(Deserialize, Serialize, PartialEq, Eq, Debug)] 15 | pub struct RecipeOutput { 16 | pub item: UnlocalizedName, 17 | #[serde(default = "default_count")] 18 | pub count: u8, 19 | } 20 | 21 | const fn default_count() -> u8 { 22 | 1 23 | } 24 | 25 | /// The vanilla recipe types, returns Other if the type is unknown 26 | /// # Note 27 | /// Singleton variants are not defined in datapacks and are left to servers / clients to implement properly
28 | /// Recipes in datapacks with these types only either enable or disable that special recipe type 29 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 30 | #[serde(tag = "type")] 31 | pub enum VanillaRecipeType { 32 | #[serde(rename = "minecraft:crafting_shaped")] 33 | ShapedRecipe(ShapedCraftingRecipe), 34 | #[serde(rename = "minecraft:crafting_shapeless")] 35 | ShapelessRecipe(ShapelessCraftingRecipe), 36 | #[serde(rename = "minecraft:smelting")] 37 | SmeltingRecipe(CookingRecipe), 38 | #[serde(rename = "minecraft:blasting")] 39 | BlastingRecipe(CookingRecipe), 40 | #[serde(rename = "minecraft:smoking")] 41 | SmokingRecipe(CookingRecipe), 42 | #[serde(rename = "minecraft:campfire_cooking")] 43 | CampfireRecipe(CookingRecipe), 44 | #[serde(rename = "minecraft:smithing")] 45 | SmithingRecipe(SmithingRecipe), 46 | #[serde(rename = "minecraft:stonecutting")] 47 | StonecuttingRecipe(StonecuttingRecipe), 48 | 49 | // Special recipe types 50 | #[serde(rename = "minecraft:armordye")] 51 | ArmorDye, 52 | #[serde(rename = "minecraft:bannerduplicate")] 53 | BannerDuplicate, 54 | #[serde(rename = "minecraft:bookcloning")] 55 | BookClone, 56 | #[serde(rename = "minecraft:firework_rocket")] 57 | FireworkRocket, 58 | #[serde(rename = "minecraft:firework_star")] 59 | FireworkStar, 60 | #[serde(rename = "minecraft:firework_star_fade")] 61 | FireworkStarFade, 62 | #[serde(rename = "minecraft:mapcloning")] 63 | MapClone, 64 | #[serde(rename = "minecraft:mapextending")] 65 | MapExtend, 66 | #[serde(rename = "minecraft:repairitem")] 67 | RepairItem, 68 | #[serde(rename = "minecraft:shielddecoration")] 69 | ShieldDecorate, 70 | #[serde(rename = "minecraft:shulkerboxcoloring")] 71 | ShulkerBoxColoring, 72 | #[serde(rename = "minecraft:tippedarrow")] 73 | TippedArrow, 74 | #[serde(rename = "minecraft:suspiciousstew")] 75 | SuspiciousStew, 76 | 77 | // If we get unknown types 78 | #[serde(other)] 79 | Unknown, 80 | } 81 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/shapeless.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::{de::Visitor, ser::SerializeSeq, Deserialize, Serialize}; 4 | 5 | use crate::data::recipe::{ingredient::Ingredient, RecipeOutput}; 6 | 7 | /// A shapeless crafting recipe 8 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 9 | pub struct ShapelessCraftingRecipe { 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub group: Option, 12 | #[serde(rename = "ingredients")] 13 | pub inputs: ShapelessIngredients, 14 | pub result: RecipeOutput, 15 | } 16 | 17 | /// The ingredients list of a shapeless recipe 18 | #[derive(Debug, PartialEq, Eq)] 19 | pub struct ShapelessIngredients(Box<[(Ingredient, u8)]>); 20 | 21 | impl Serialize for ShapelessIngredients { 22 | fn serialize(&self, serializer: S) -> Result 23 | where S: serde::Serializer { 24 | let mut seq = serializer.serialize_seq(Some(self.0.len()))?; 25 | 26 | for (ingr, count) in self.0.iter() { 27 | for _ in 0 .. *count { 28 | seq.serialize_element(ingr)?; 29 | } 30 | } 31 | 32 | seq.end() 33 | } 34 | } 35 | 36 | impl<'de> Deserialize<'de> for ShapelessIngredients { 37 | fn deserialize(deserializer: D) -> Result 38 | where D: serde::Deserializer<'de> { 39 | deserializer.deserialize_seq(ShapelessIngredientsVisitor) 40 | } 41 | } 42 | 43 | struct ShapelessIngredientsVisitor; 44 | 45 | impl<'de> Visitor<'de> for ShapelessIngredientsVisitor { 46 | type Value = ShapelessIngredients; 47 | 48 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 49 | formatter.write_str("List of ingredients") 50 | } 51 | 52 | fn visit_seq
(self, mut seq: A) -> Result 53 | where A: serde::de::SeqAccess<'de> { 54 | let mut map = BTreeMap::new(); 55 | 56 | while let Some(i) = seq.next_element::()? { 57 | // Clippy's warning here just is not valid for this use case I think 58 | #[allow(clippy::map_entry)] 59 | if map.contains_key(&i) { 60 | *map.get_mut(&i).unwrap() += 1; 61 | } else { 62 | map.insert(i, 1); 63 | } 64 | } 65 | 66 | Ok(ShapelessIngredients( 67 | map.into_iter().collect::>().into_boxed_slice(), 68 | )) 69 | } 70 | } 71 | 72 | #[test] 73 | fn shapeless_ser_test() { 74 | use crate::data::recipe::{ingredient::Ingredient, RecipeOutput, VanillaRecipeType}; 75 | use qdat::UnlocalizedName; 76 | 77 | let recipe = VanillaRecipeType::ShapelessRecipe(ShapelessCraftingRecipe { 78 | inputs: ShapelessIngredients(Box::new([ 79 | (Ingredient::Item(UnlocalizedName::minecraft("sand")), 4), 80 | (Ingredient::Item(UnlocalizedName::minecraft("gravel")), 4), 81 | (Ingredient::Item(UnlocalizedName::minecraft("red_dye")), 1), 82 | ])), 83 | result: RecipeOutput { 84 | item: UnlocalizedName::minecraft("red_concrete_powder"), 85 | count: 8, 86 | }, 87 | group: Some("concrete_powder".to_owned()), 88 | }); 89 | 90 | let serialized = serde_json::to_string(&recipe).unwrap(); 91 | 92 | assert_eq!( 93 | r#"{"type":"minecraft:crafting_shapeless","group":"concrete_powder","ingredients":[{"item":"minecraft:sand"},{"item":"minecraft:sand"},{"item":"minecraft:sand"},{"item":"minecraft:sand"},{"item":"minecraft:gravel"},{"item":"minecraft:gravel"},{"item":"minecraft:gravel"},{"item":"minecraft:gravel"},{"item":"minecraft:red_dye"}],"result":{"item":"minecraft:red_concrete_powder","count":8}}"#, 94 | &serialized 95 | ) 96 | } 97 | 98 | #[test] 99 | fn shapeless_de_test() { 100 | use crate::data::recipe::{ingredient::Ingredient, RecipeOutput, VanillaRecipeType}; 101 | use qdat::UnlocalizedName; 102 | 103 | let input = r#"{ 104 | "type": "minecraft:crafting_shapeless", 105 | "group": "concrete_powder", 106 | "ingredients": [ 107 | {"item": "minecraft:red_dye"}, 108 | {"item": "minecraft:sand"}, 109 | {"item": "minecraft:sand"}, 110 | {"item": "minecraft:sand"}, 111 | {"item": "minecraft:sand"}, 112 | {"item": "minecraft:gravel"}, 113 | {"item": "minecraft:gravel"}, 114 | {"item": "minecraft:gravel"}, 115 | {"item": "minecraft:gravel"} 116 | ], 117 | "result": { 118 | "item": "minecraft:red_concrete_powder", 119 | "count": 8 120 | } 121 | }"#; 122 | 123 | let recipe: VanillaRecipeType = serde_json::from_str(input).unwrap(); 124 | 125 | assert_eq!( 126 | recipe, 127 | VanillaRecipeType::ShapelessRecipe(ShapelessCraftingRecipe { 128 | inputs: ShapelessIngredients(Box::new([ 129 | (Ingredient::Item(UnlocalizedName::minecraft("gravel")), 4), 130 | (Ingredient::Item(UnlocalizedName::minecraft("red_dye")), 1), 131 | (Ingredient::Item(UnlocalizedName::minecraft("sand")), 4), 132 | ])), 133 | result: RecipeOutput { 134 | item: UnlocalizedName::minecraft("red_concrete_powder"), 135 | count: 8, 136 | }, 137 | group: Some("concrete_powder".to_owned()), 138 | }) 139 | ) 140 | } 141 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/smithing.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::data::recipe::ingredient::Ingredient; 5 | 6 | /// A smithing recipe 7 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] 8 | pub struct SmithingRecipe { 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | pub group: Option, 11 | /// The base of the smithing recipe 12 | /// # Note 13 | /// This ingredient cannot be a list 14 | pub base: Ingredient, 15 | pub addition: Ingredient, 16 | /// In most cases, the nbt of enchantments will be carried over to the result 17 | pub result: SmithingOutput, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] 21 | // mojang I hate you please stop making things very close but not the same 22 | pub struct SmithingOutput { 23 | pub item: UnlocalizedName, 24 | } 25 | 26 | #[test] 27 | fn smithing_ser_test() { 28 | use crate::data::recipe::{ingredient::Ingredient, VanillaRecipeType}; 29 | 30 | let recipe = VanillaRecipeType::SmithingRecipe(SmithingRecipe { 31 | group: None, 32 | base: Ingredient::Item(UnlocalizedName::minecraft("diamond_pickaxe")), 33 | addition: Ingredient::Item(UnlocalizedName::minecraft("netherite_ingot")), 34 | result: SmithingOutput { 35 | item: UnlocalizedName::minecraft("netherite_pickaxe"), 36 | }, 37 | }); 38 | 39 | let serialized = serde_json::to_string(&recipe).unwrap(); 40 | 41 | assert_eq!( 42 | &serialized, 43 | r#"{"type":"minecraft:smithing","base":{"item":"minecraft:diamond_pickaxe"},"addition":{"item":"minecraft:netherite_ingot"},"result":{"item":"minecraft:netherite_pickaxe"}}"# 44 | ) 45 | } 46 | 47 | #[test] 48 | fn smithing_de_test() { 49 | use crate::data::recipe::{ingredient::Ingredient, VanillaRecipeType}; 50 | 51 | let input = r#"{ 52 | "type": "minecraft:smithing", 53 | "base": { 54 | "item": "minecraft:diamond_pickaxe" 55 | }, 56 | "addition": { 57 | "item": "minecraft:netherite_ingot" 58 | }, 59 | "result": { 60 | "item": "minecraft:netherite_pickaxe" 61 | } 62 | }"#; 63 | 64 | let recipe: VanillaRecipeType = serde_json::from_str(input).unwrap(); 65 | 66 | assert_eq!( 67 | recipe, 68 | VanillaRecipeType::SmithingRecipe(SmithingRecipe { 69 | group: None, 70 | base: Ingredient::Item(UnlocalizedName::minecraft("diamond_pickaxe")), 71 | addition: Ingredient::Item(UnlocalizedName::minecraft("netherite_ingot")), 72 | result: SmithingOutput { 73 | item: UnlocalizedName::minecraft("netherite_pickaxe") 74 | } 75 | }) 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /datapack/src/data/recipe/stonecutting.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize}; 3 | 4 | use crate::data::recipe::{ingredient::Ingredient, recipe::RecipeOutput}; 5 | 6 | /// A stone cutting recipe 7 | #[derive(Debug, Eq, PartialEq)] 8 | pub struct StonecuttingRecipe { 9 | pub group: Option, 10 | pub input: Ingredient, 11 | pub result: RecipeOutput, 12 | } 13 | 14 | impl Serialize for StonecuttingRecipe { 15 | fn serialize(&self, serializer: S) -> Result 16 | where S: serde::Serializer { 17 | let mut map = serializer.serialize_map(Some(4))?; 18 | 19 | if self.group.is_some() { 20 | map.serialize_entry("group", &self.group)?; 21 | } 22 | 23 | map.serialize_entry("ingredient", &self.input)?; 24 | map.serialize_entry("result", &self.result.item)?; 25 | map.serialize_entry("count", &self.result.count)?; 26 | 27 | map.end() 28 | } 29 | } 30 | 31 | impl<'de> Deserialize<'de> for StonecuttingRecipe { 32 | fn deserialize(deserializer: D) -> Result 33 | where D: serde::Deserializer<'de> { 34 | deserializer.deserialize_map(StonecuttingRecipeVisitor) 35 | } 36 | } 37 | 38 | 39 | struct StonecuttingRecipeVisitor; 40 | 41 | impl<'de> Visitor<'de> for StonecuttingRecipeVisitor { 42 | type Value = StonecuttingRecipe; 43 | 44 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 45 | formatter.write_str("a stonecutting recipe") 46 | } 47 | 48 | fn visit_map(self, mut map: A) -> Result 49 | where A: serde::de::MapAccess<'de> { 50 | map_visitor!( 51 | map, 52 | (group, "group", String), 53 | (ingredient, "ingredient", Ingredient), 54 | (result, "result", UnlocalizedName), 55 | (count, "count", u8) 56 | ); 57 | 58 | missing_field_error!(ingredient, "ingredient", result, "result", count, "count"); 59 | 60 | Ok(StonecuttingRecipe { 61 | group, 62 | input: ingredient, 63 | result: RecipeOutput { 64 | item: result, 65 | count, 66 | }, 67 | }) 68 | } 69 | } 70 | 71 | #[test] 72 | fn stonecutting_ser_test() { 73 | use crate::data::recipe::{ingredient::Ingredient, VanillaRecipeType}; 74 | 75 | let recipe = VanillaRecipeType::StonecuttingRecipe(StonecuttingRecipe { 76 | group: None, 77 | input: Ingredient::Item(UnlocalizedName::minecraft("stone")), 78 | result: RecipeOutput { 79 | item: UnlocalizedName::minecraft("stone_brick_slab"), 80 | count: 2, 81 | }, 82 | }); 83 | 84 | let serialized = serde_json::to_string(&recipe).unwrap(); 85 | 86 | assert_eq!( 87 | &serialized, 88 | r#"{"type":"minecraft:stonecutting","ingredient":{"item":"minecraft:stone"},"result":"minecraft:stone_brick_slab","count":2}"# 89 | ) 90 | } 91 | 92 | #[test] 93 | fn stonecutting_de_test() { 94 | use crate::data::recipe::{ingredient::Ingredient, VanillaRecipeType}; 95 | 96 | let input = r#"{ 97 | "type": "minecraft:stonecutting", 98 | "ingredient": { 99 | "item": "minecraft:stone" 100 | }, 101 | "result": "minecraft:stone_brick_slab", 102 | "count": 2 103 | }"#; 104 | 105 | let recipe: VanillaRecipeType = serde_json::from_str(input).unwrap(); 106 | 107 | assert_eq!( 108 | recipe, 109 | VanillaRecipeType::StonecuttingRecipe(StonecuttingRecipe { 110 | group: None, 111 | input: Ingredient::Item(UnlocalizedName::minecraft("stone")), 112 | result: RecipeOutput { 113 | item: UnlocalizedName::minecraft("stone_brick_slab"), 114 | count: 2 115 | } 116 | }) 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /datapack/src/data/structure.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use qdat::UnlocalizedName; 4 | use quartz_nbt::NbtCompound; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// A structure 8 | /// 9 | /// This is stored in NBT files in contrast to the rest of datapacks that are in json 10 | #[derive(Serialize, Deserialize)] 11 | pub struct Structure { 12 | #[serde(rename = "DataVersion")] 13 | pub data_version: i32, 14 | pub size: Vec, 15 | pub palette: Option>, 16 | pub blocks: Vec, 17 | pub entities: Vec, 18 | } 19 | 20 | /// An entry in the palette of a structure 21 | #[derive(Serialize, Deserialize)] 22 | pub struct StructurePaletteEntry { 23 | #[serde(rename = "Name")] 24 | pub name: UnlocalizedName, 25 | #[serde(rename = "Properties")] 26 | #[serde(default)] 27 | pub properties: HashMap, 28 | } 29 | 30 | /// A block of a structure 31 | #[derive(Serialize, Deserialize)] 32 | pub struct StructureBlock { 33 | pub state: i32, 34 | pub pos: Vec, 35 | pub nbt: Option, 36 | } 37 | 38 | /// An entity in a structure 39 | #[derive(Serialize, Deserialize)] 40 | pub struct StructureEntity { 41 | pub pos: Vec, 42 | #[serde(rename = "blockPos")] 43 | pub block_pos: Vec, 44 | pub nbt: NbtCompound, 45 | } 46 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/biome.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use qdat::UnlocalizedName; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::data::tags::IdsOrTag; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct Biome { 10 | pub category: BiomeCategory, 11 | pub precipitation: Precipitation, 12 | pub temperature: f32, 13 | pub tempearture_modifier: Option, 14 | pub downfall: f32, 15 | pub creature_spawn_probability: Option, 16 | pub effects: BiomeEffects, 17 | pub carvers: BiomeCarvers, 18 | pub features: Vec, 19 | pub spawners: HashMap>, 20 | // This is literally never used by vanilla 21 | pub spawn_costs: HashMap, 22 | } 23 | 24 | #[derive(Serialize, Deserialize)] 25 | #[serde(rename_all = "snake_case")] 26 | pub enum Precipitation { 27 | None, 28 | Rain, 29 | Snow, 30 | } 31 | 32 | #[derive(Serialize, Deserialize)] 33 | #[serde(rename_all = "snake_case")] 34 | pub enum BiomeCategory { 35 | None, 36 | Taiga, 37 | ExtremeHills, 38 | Jungle, 39 | Mesa, 40 | Plains, 41 | Savanna, 42 | Icy, 43 | TheEnd, 44 | Beach, 45 | Forest, 46 | Ocean, 47 | Desert, 48 | River, 49 | Swamp, 50 | Mushroom, 51 | Nether, 52 | Underground, 53 | Mountain, 54 | } 55 | 56 | #[derive(Serialize, Deserialize)] 57 | pub enum TemperatureModifier { 58 | None, 59 | Frozen, 60 | } 61 | 62 | #[derive(Serialize, Deserialize)] 63 | pub struct BiomeEffects { 64 | pub fog_color: Option, 65 | pub foliage_color: Option, 66 | pub grass_color: Option, 67 | pub sky_color: Option, 68 | pub water_color: Option, 69 | pub particle: Option, 70 | pub additions_sound: Option, 71 | pub ambient_sound: Option, 72 | pub mood_sound: Option, 73 | pub music: Option, 74 | } 75 | 76 | #[derive(Serialize, Deserialize)] 77 | #[serde(rename_all = "snake_case")] 78 | pub enum GrassColorModifier { 79 | None, 80 | DarkForest, 81 | Swamp, 82 | } 83 | 84 | #[derive(Serialize, Deserialize)] 85 | pub struct BiomeParticle { 86 | pub probability: f32, 87 | pub options: BiomeParticleOptions, 88 | } 89 | 90 | #[derive(Serialize, Deserialize)] 91 | pub struct BiomeParticleOptions { 92 | r#type: UnlocalizedName, 93 | } 94 | // Idk if this is still valid 95 | // #[derive(Serialize, Deserialize)] 96 | // #[serde(tag = "type")] 97 | // pub enum BiomeParticleOptions { 98 | // #[serde(rename = "minecraft:block")] 99 | // Block { 100 | // #[serde(rename = "Name")] 101 | // name: UnlocalizedName, 102 | // #[serde(rename = "Properties")] 103 | // properties: HashMap, 104 | // }, 105 | // #[serde(rename = "minecraft:falling_dust")] 106 | // FallingDust { 107 | // #[serde(rename = "Name")] 108 | // name: UnlocalizedName, 109 | // #[serde(rename = "Properties")] 110 | // properties: HashMap, 111 | // }, 112 | // #[serde(rename = "minecraft:dust")] 113 | // Dust { r: f32, g: f32, b: f32, scale: f32 }, 114 | // #[serde(rename = "minecraft:item")] 115 | // Item { 116 | // id: UnlocalizedName, 117 | // #[serde(rename = "Count")] 118 | // count: i32, 119 | // // TO DO: this is snbt 120 | // tag: String, 121 | // }, 122 | // } 123 | 124 | #[derive(Serialize, Deserialize)] 125 | pub struct MoodSound { 126 | pub sound: String, 127 | pub tick_delay: i32, 128 | pub block_search_extent: i32, 129 | pub offset: f64, 130 | } 131 | 132 | #[derive(Serialize, Deserialize)] 133 | pub struct AdditionalSound { 134 | pub sound: String, 135 | pub tick_chance: f64, 136 | } 137 | 138 | #[derive(Serialize, Deserialize)] 139 | pub struct BiomeMusic { 140 | pub sound: String, 141 | pub min_delay: i32, 142 | pub max_delay: i32, 143 | pub replace_current_music: bool, 144 | } 145 | 146 | #[derive(Serialize, Deserialize)] 147 | pub struct MobSpawners { 148 | pub r#type: UnlocalizedName, 149 | pub weight: i32, 150 | #[serde(rename = "minCount")] 151 | pub min_count: i32, 152 | #[serde(rename = "maxCount")] 153 | pub max_count: i32, 154 | } 155 | 156 | #[derive(Serialize, Deserialize)] 157 | pub struct SpawnCosts { 158 | pub energy_budget: f64, 159 | pub charge: f64, 160 | } 161 | 162 | #[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] 163 | #[serde(rename_all = "snake_case")] 164 | pub enum MobCategory { 165 | Monster, 166 | Creature, 167 | Ambient, 168 | WaterCreature, 169 | UndergroundWaterCreature, 170 | WaterAmbient, 171 | Axolotls, 172 | Misc, 173 | } 174 | 175 | #[derive(Serialize, Deserialize)] 176 | pub struct BiomeCarvers { 177 | pub air: Option, 178 | pub liquid: Option, 179 | } 180 | 181 | #[derive(Serialize, Deserialize)] 182 | #[serde(untagged)] 183 | pub enum BiomeCarver { 184 | Singleton(UnlocalizedName), 185 | List(#[serde(default)] Vec), 186 | } 187 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/carvers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct CarverSettings { 8 | pub probability: f32, 9 | pub y: HeightProvider, 10 | #[serde(rename = "yScale")] 11 | pub y_scale: FloatProvider, 12 | pub default_settings: Option>, 13 | pub lava_level: VerticalAnchor, 14 | #[serde(default = "Default::default")] 15 | pub aquifers_enabled: bool, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | #[serde(tag = "type", content = "config")] 20 | pub enum Carver { 21 | #[serde(rename = "minecraft:cave")] 22 | Cave { 23 | #[serde(flatten)] 24 | settings: CarverSettings, 25 | horizontal_radius_multiplier: FloatProvider, 26 | vertical_radius_multiplier: FloatProvider, 27 | floor_level: FloatProvider, 28 | }, 29 | #[serde(rename = "minecraft:nether_cave")] 30 | NetherCave { 31 | #[serde(flatten)] 32 | settings: CarverSettings, 33 | horizontal_radius_multiplier: FloatProvider, 34 | vertical_radius_multiplier: FloatProvider, 35 | floor_level: FloatProvider, 36 | }, 37 | #[serde(rename = "minecraft:underwater_cave")] 38 | UnderwaterCave { 39 | #[serde(flatten)] 40 | settings: CarverSettings, 41 | horizontal_radius_multiplier: FloatProvider, 42 | vertical_radius_multiplier: FloatProvider, 43 | floor_level: FloatProvider, 44 | }, 45 | #[serde(rename = "minecraft:canyon")] 46 | Canyon { 47 | #[serde(flatten)] 48 | settings: CarverSettings, 49 | vertical_rotation: FloatProvider, 50 | shape: CanyonShape, 51 | }, 52 | #[serde(rename = "minecraft:underwater_canyon")] 53 | UnderwaterCanyon { 54 | #[serde(flatten)] 55 | settings: CarverSettings, 56 | vertical_rotation: FloatProvider, 57 | shape: CanyonShape, 58 | }, 59 | } 60 | 61 | #[derive(Serialize, Deserialize)] 62 | pub struct CanyonShape { 63 | pub thickness: FloatProvider, 64 | pub width_smoothness: i32, 65 | pub horizontal_radius_factor: FloatProvider, 66 | pub vertical_radius_default_factor: FloatProvider, 67 | pub vertical_radius_center_factor: FloatProvider, 68 | } 69 | 70 | #[derive(Serialize, Deserialize)] 71 | #[serde(untagged)] 72 | pub enum FloatProvider { 73 | Constant(f32), 74 | Provider(FloatProviderInternal), 75 | } 76 | 77 | #[derive(Serialize, Deserialize)] 78 | #[serde(tag = "type", content = "value")] 79 | pub enum FloatProviderInternal { 80 | #[serde(rename = "minecraft:constant")] 81 | Constant(f32), 82 | #[serde(rename = "minecraft:uniform")] 83 | Uniform { 84 | min_inclusive: f32, 85 | max_exclusive: f32, 86 | }, 87 | #[serde(rename = "minecraft:clamped_normal")] 88 | ClampedNormal { 89 | mean: f32, 90 | deviation: f32, 91 | min: f32, 92 | max: f32, 93 | }, 94 | #[serde(rename = "minecraft:trapezoid")] 95 | Trapezoid { min: f32, max: f32, plateau: f32 }, 96 | } 97 | 98 | #[derive(Serialize, Deserialize)] 99 | #[serde(untagged)] 100 | pub enum HeightProvider { 101 | Constant { 102 | value: VerticalAnchor, 103 | }, 104 | Uniform { 105 | min_inclusive: VerticalAnchor, 106 | max_inclusive: VerticalAnchor, 107 | }, 108 | BiasedToBottom { 109 | min_inclusive: VerticalAnchor, 110 | max_inclusive: VerticalAnchor, 111 | /// # Note 112 | /// If present, has to be at least 1 113 | inner: Option, 114 | }, 115 | VeryBiasedToBottom { 116 | min_inclusive: VerticalAnchor, 117 | max_inclusive: VerticalAnchor, 118 | /// # Note 119 | /// If present, has to be at least 1 120 | inner: Option, 121 | }, 122 | Trapezoid { 123 | min_inclusive: VerticalAnchor, 124 | max_inclusive: VerticalAnchor, 125 | plateau: Option, 126 | }, 127 | } 128 | 129 | #[derive(Serialize, Deserialize)] 130 | #[serde(untagged)] 131 | pub enum VerticalAnchor { 132 | Absolute { absolute: i32 }, 133 | AboveBottom { above_bottom: i32 }, 134 | BelowTop { below_top: i32 }, 135 | } 136 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/density_function.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | #[serde(untagged)] 6 | pub enum DensityFunctionProvider { 7 | Constant(f64), 8 | Inline(Box), 9 | Reference(UnlocalizedName), 10 | } 11 | 12 | 13 | #[derive(Serialize, Deserialize)] 14 | #[serde(tag = "type")] 15 | pub enum DensityFunction { 16 | #[serde(rename = "minecraft:abs")] 17 | Abs { argument: DensityFunctionProvider }, 18 | #[serde(rename = "minecraft:add")] 19 | Add { 20 | argument1: DensityFunctionProvider, 21 | argument2: DensityFunctionProvider, 22 | }, 23 | #[serde(rename = "minecraft:beardifier")] 24 | Beardifier, 25 | #[serde(rename = "minecraft:blend_alpha")] 26 | BlendAlpha, 27 | #[serde(rename = "minecraft:blend_density")] 28 | BendDensity { argument: DensityFunctionProvider }, 29 | #[serde(rename = "minecraft:blend_offset")] 30 | BlendOffset, 31 | #[serde(rename = "minecraft:cache_2d")] 32 | Cache2d { argument: DensityFunctionProvider }, 33 | #[serde(rename = "minecraft:cache_all_in_cell")] 34 | CacheAllInCell { argument: DensityFunctionProvider }, 35 | #[serde(rename = "minecraft:cache_once")] 36 | CacheOnce { argument: DensityFunctionProvider }, 37 | #[serde(rename = "minecraft:clamp")] 38 | Clamp { 39 | input: DensityFunctionProvider, 40 | min: f64, 41 | max: f64, 42 | }, 43 | #[serde(rename = "minecraft:constant")] 44 | Constant { argument: f64 }, 45 | #[serde(rename = "minecraft:cube")] 46 | Cube { argument: DensityFunctionProvider }, 47 | #[serde(rename = "minecraft:end_islands")] 48 | EndIslands, 49 | #[serde(rename = "minecraft:flat_cache")] 50 | FlatCache { argument: DensityFunctionProvider }, 51 | #[serde(rename = "minecraft:half_negative")] 52 | HalfNegative { argument: DensityFunctionProvider }, 53 | #[serde(rename = "minecraft:interpolated")] 54 | Interpolated { argument: DensityFunctionProvider }, 55 | #[serde(rename = "minecraft:max")] 56 | Max { 57 | argument1: DensityFunctionProvider, 58 | argument2: DensityFunctionProvider, 59 | }, 60 | #[serde(rename = "minecraft:min")] 61 | Min { 62 | argument1: DensityFunctionProvider, 63 | argument2: DensityFunctionProvider, 64 | }, 65 | #[serde(rename = "minecraft:mul")] 66 | Mul { 67 | argument1: DensityFunctionProvider, 68 | argument2: DensityFunctionProvider, 69 | }, 70 | #[serde(rename = "minecraft:noise")] 71 | Noise { 72 | noise: UnlocalizedName, 73 | xz_scale: f64, 74 | y_scale: f64, 75 | }, 76 | #[serde(rename = "minecraft:old_blended_noise")] 77 | OldBlendedNoise, 78 | /*{ 79 | xz_scale: f64, 80 | y_scale: f64, 81 | xz_factor: f64, 82 | y_factor: f64, 83 | smear_scale_multiplier: f64, 84 | } */ 85 | #[serde(rename = "minecraft:quarter_negative")] 86 | QuarterNegative { argument: DensityFunctionProvider }, 87 | #[serde(rename = "minecraft:range_choice")] 88 | RangeChoice { 89 | input: DensityFunctionProvider, 90 | min_inclusive: f64, 91 | max_exclusive: f64, 92 | when_in_range: DensityFunctionProvider, 93 | when_out_of_range: DensityFunctionProvider, 94 | }, 95 | #[serde(rename = "minecraft:shift")] 96 | Shift { argument: UnlocalizedName }, 97 | #[serde(rename = "minecraft:shift_a")] 98 | ShiftA { argument: UnlocalizedName }, 99 | #[serde(rename = "minecraft:shift_b")] 100 | ShiftB { argument: UnlocalizedName }, 101 | #[serde(rename = "minecraft:shifted_noise")] 102 | ShiftedNoise { 103 | noise: UnlocalizedName, 104 | xz_scale: f64, 105 | y_scale: f64, 106 | shift_x: DensityFunctionProvider, 107 | shift_y: DensityFunctionProvider, 108 | shift_z: DensityFunctionProvider, 109 | }, 110 | #[serde(rename = "minecraft:slide")] 111 | Slide { argument: DensityFunctionProvider }, 112 | #[serde(rename = "minecraft:spline")] 113 | Spline { 114 | spline: SplineValue, 115 | min_value: f64, 116 | max_value: f64, 117 | }, 118 | #[serde(rename = "minecraft:square")] 119 | Square { argument: DensityFunctionProvider }, 120 | #[serde(rename = "minecraft:squeeze")] 121 | Squeeze { argument: DensityFunctionProvider }, 122 | #[serde(rename = "minecraft:terrain_shaper_spline")] 123 | TerrainShaperSpline { 124 | spline: TerrainShaperSplineType, 125 | min_value: f64, 126 | max_value: f64, 127 | continentalness: DensityFunctionProvider, 128 | erosion: DensityFunctionProvider, 129 | weirdness: DensityFunctionProvider, 130 | }, 131 | #[serde(rename = "minecraft:weird_scaled_sampler")] 132 | WeirdScaledSampler { 133 | rarity_value_mapper: String, 134 | noise: UnlocalizedName, 135 | input: DensityFunctionProvider, 136 | }, 137 | #[serde(rename = "minecraft:y_clamped_gradient")] 138 | YClampedGradient { 139 | from_y: f64, 140 | to_y: f64, 141 | from_value: f64, 142 | to_value: f64, 143 | }, 144 | } 145 | 146 | 147 | #[derive(Serialize, Deserialize)] 148 | pub struct Spline { 149 | coordinate: DensityFunctionProvider, 150 | points: Vec, 151 | } 152 | 153 | #[derive(Serialize, Deserialize)] 154 | pub struct SplinePoint { 155 | location: f64, 156 | value: SplineValue, 157 | derivative: f64, 158 | } 159 | 160 | #[derive(Serialize, Deserialize)] 161 | pub enum SplineValue { 162 | Constant(f64), 163 | Spline(Spline), 164 | } 165 | 166 | #[derive(Serialize, Deserialize)] 167 | #[serde(rename_all = "snake_case")] 168 | pub enum TerrainShaperSplineType { 169 | Offset, 170 | Factor, 171 | Jaggedness, 172 | } 173 | 174 | #[derive(Serialize, Deserialize)] 175 | #[serde(rename_all = "snake_case")] 176 | pub enum RarityValueType { 177 | Type1, 178 | Type2, 179 | } 180 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/dimension.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::data::{structure_set::StructureSetProvider, tags::IdsOrTag}; 5 | 6 | use super::noise_settings::NoiseSettings; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct Dimension { 10 | pub r#type: UnlocalizedName, 11 | pub generator: DimensionGenerator, 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | #[serde(tag = "type")] 16 | pub enum DimensionGenerator { 17 | #[serde(rename = "minecraft:noise")] 18 | Noise { 19 | seed: i32, 20 | settings: DimensionNoiseSettings, 21 | biome_source: BiomeSourceType, 22 | }, 23 | #[serde(rename = "minecraft:flat")] 24 | Flat { settings: SuperflatSettings }, 25 | #[serde(rename = "minecraft:debug")] 26 | Debug, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | #[serde(untagged)] 31 | pub enum DimensionNoiseSettings { 32 | Preset(String), 33 | Settings(Box), 34 | } 35 | 36 | #[derive(Serialize, Deserialize)] 37 | pub struct BiomeSource { 38 | // NOTE: this might not be an uln 39 | pub biomes: Vec, 40 | pub r#type: UnlocalizedName, 41 | } 42 | 43 | #[derive(Serialize, Deserialize)] 44 | #[serde(tag = "type")] 45 | pub enum BiomeSourceType { 46 | #[serde(rename = "minecraft:vanilla_layered")] 47 | VanillaLayer { 48 | large_biomes: bool, 49 | legacy_biome_init_layer: bool, 50 | }, 51 | #[serde(rename = "minecraft:multi_noise")] 52 | MultiNoise { biomes: Vec }, 53 | #[serde(rename = "minecraft:the_end")] 54 | TheEnd, 55 | #[serde(rename = "minecraft:fixed")] 56 | Fixed { biome: String }, 57 | #[serde(rename = "minecraft:checkerboard")] 58 | CheckerBoard { biomes: IdsOrTag, scale: i32 }, 59 | } 60 | 61 | #[derive(Serialize, Deserialize)] 62 | pub struct BiomeSourceBiome { 63 | // Wiki says this can be repeated 64 | // WHAT THE FUCK DOES THAT MEAN 65 | pub biome: UnlocalizedName, 66 | pub parameters: DimensionBiomeParameters, 67 | } 68 | 69 | #[derive(Serialize, Deserialize)] 70 | pub struct DimensionBiomeParameters { 71 | pub erosion: AmountOrRangeArray, 72 | pub depth: AmountOrRangeArray, 73 | pub weirdness: AmountOrRangeArray, 74 | pub offset: AmountOrRangeArray, 75 | pub temperature: AmountOrRangeArray, 76 | pub humidity: AmountOrRangeArray, 77 | pub continentalness: AmountOrRangeArray, 78 | } 79 | 80 | #[derive(Serialize, Deserialize)] 81 | #[serde(untagged)] 82 | pub enum AmountOrRangeArray { 83 | Singleton(f32), 84 | Array([f32; 2]), 85 | } 86 | 87 | #[derive(Serialize, Deserialize)] 88 | pub struct BiomeNoise { 89 | #[serde(rename = "firstOctive")] 90 | pub first_octive: i32, 91 | pub amplitudes: Vec, 92 | } 93 | 94 | #[derive(Serialize, Deserialize)] 95 | pub struct SuperflatSettings { 96 | pub layers: Vec, 97 | pub biome: String, 98 | #[serde(default = "Default::default")] 99 | pub lakes: bool, 100 | #[serde(default = "Default::default")] 101 | pub features: bool, 102 | #[serde(default = "Default::default")] 103 | pub structure_overrides: Vec, 104 | } 105 | 106 | 107 | #[derive(Serialize, Deserialize)] 108 | pub struct SuperflatLayer { 109 | pub height: i32, 110 | pub block: UnlocalizedName, 111 | } 112 | 113 | #[derive(Serialize, Deserialize)] 114 | pub struct StrongholdSettings { 115 | pub distance: i32, 116 | pub count: i32, 117 | pub spread: i32, 118 | } 119 | 120 | #[derive(Serialize, Deserialize)] 121 | #[serde(tag = "type")] 122 | pub enum StructureSettings { 123 | #[serde(rename = "minecraft:random_spread")] 124 | RandomSpread { 125 | spacing: i32, 126 | separation: i32, 127 | salt: i32, 128 | }, 129 | #[serde(rename = "minecraft:concentric_rings")] 130 | ConcentricRings { 131 | distance: i32, 132 | spread: i32, 133 | count: i32, 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/dimension_type.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::data::tags::TagProvider; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct DimensionType { 7 | pub ultrawarm: bool, 8 | pub natural: bool, 9 | pub coordinate_scale: f32, 10 | pub has_skylight: bool, 11 | pub has_ceiling: bool, 12 | pub ambient_light: f32, 13 | pub fixed_time: Option, 14 | pub piglin_safe: bool, 15 | pub bed_works: bool, 16 | pub respawn_anchor_works: bool, 17 | pub has_raids: bool, 18 | pub logical_height: i32, 19 | pub min_y: i32, 20 | pub height: i32, 21 | pub infiniburn: TagProvider, 22 | #[serde(default = "Default::default")] 23 | pub effects: DimensionEffects, 24 | } 25 | 26 | #[derive(Serialize, Deserialize)] 27 | pub enum DimensionEffects { 28 | #[serde(rename = "minecraft:overworld")] 29 | Overworld, 30 | #[serde(rename = "minecraft:the_nether")] 31 | TheNether, 32 | #[serde(rename = "minecraft:the_end")] 33 | TheEnd, 34 | } 35 | 36 | impl Default for DimensionEffects { 37 | fn default() -> Self { 38 | Self::Overworld 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/jigsaw_pool.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::data::tags::IdsOrTag; 5 | 6 | use super::processors::Processor; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct JigsawPool { 10 | pub name: UnlocalizedName, 11 | pub fallback: UnlocalizedName, 12 | pub elements: Vec, 13 | } 14 | 15 | 16 | #[derive(Serialize, Deserialize)] 17 | #[serde(rename_all = "snake_case")] 18 | pub enum JigsawProjection { 19 | Rigid, 20 | TerrainMatching, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | pub struct WeightedJigsawElement { 25 | weight: u32, 26 | element: JigsawElement, 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | #[serde(tag = "element_type")] 31 | pub enum JigsawElement { 32 | #[serde(rename = "minecraft:empty_pool_element")] 33 | EmptyPoolElement, 34 | #[serde(rename = "minecraft:list_pool_element")] 35 | ListPoolElement { 36 | projection: JigsawProjection, 37 | elements: Vec, 38 | }, 39 | #[serde(rename = "minecraft:feature_pool_element")] 40 | FeaturePoolElement { 41 | projection: JigsawProjection, 42 | feature: IdsOrTag, 43 | }, 44 | #[serde(rename = "minecraft:legacy_single_pool_element")] 45 | LegacySinglePoolElement { 46 | location: UnlocalizedName, 47 | projection: JigsawProjection, 48 | processors: JigsawProcessor, 49 | }, 50 | #[serde(rename = "minecraft:single_pool_element")] 51 | SinglePoolElement { 52 | location: UnlocalizedName, 53 | processors: JigsawProcessor, 54 | projection: JigsawProjection, 55 | }, 56 | } 57 | 58 | #[derive(Serialize, Deserialize)] 59 | #[serde(untagged)] 60 | pub enum JigsawProcessor { 61 | Singleton(Processor), 62 | Uln(UnlocalizedName), 63 | List(Vec), 64 | } 65 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod biome; 2 | pub mod carvers; 3 | pub mod density_function; 4 | pub mod dimension; 5 | pub mod dimension_type; 6 | pub mod features; 7 | pub mod jigsaw_pool; 8 | pub mod noise; 9 | pub mod noise_settings; 10 | pub mod processors; 11 | pub mod structure_features; 12 | pub mod structure_set; 13 | pub mod surface_builders; 14 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/noise.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct Noise { 5 | #[serde(rename = "firstOctave")] 6 | pub first_octave: i32, 7 | pub amplitudes: Vec, 8 | } 9 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/noise_settings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use qdat::UnlocalizedName; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::data::world_gen::density_function::DensityFunctionProvider; 7 | 8 | use super::features::SurfaceType; 9 | 10 | #[derive(Serialize, Deserialize)] 11 | pub struct NoiseSettings { 12 | pub sea_level: i32, 13 | pub disable_mob_generation: bool, 14 | pub ore_veins_enabled: bool, 15 | pub aquifers_enabled: bool, 16 | pub legacy_random_source: bool, 17 | pub default_block: BlockState, 18 | pub default_fluid: BlockState, 19 | pub noise: NoiseOptions, 20 | pub surface_rule: SurfaceRule, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | #[serde(tag = "type")] 25 | pub enum SurfaceRule { 26 | #[serde(rename = "minecraft:condition")] 27 | Condition { 28 | if_true: Box, 29 | then_run: Box, 30 | }, 31 | #[serde(rename = "minecraft:block")] 32 | Block { result_state: BlockState }, 33 | #[serde(rename = "minecraft:vertical_gradient")] 34 | VerticalGradient { 35 | random_name: String, 36 | true_at_and_below: HeightConditionProvider, 37 | false_at_and_above: HeightConditionProvider, 38 | }, 39 | #[serde(rename = "minecraft:above_preliminary_surface")] 40 | AbovePreliminarySurface, 41 | #[serde(rename = "minecraft:sequence")] 42 | Sequnce { sequence: Vec }, 43 | #[serde(rename = "minecraft:stone_depth")] 44 | StoneDepth { 45 | offset: i32, 46 | add_surface_depth: bool, 47 | secondary_depth_range: i32, 48 | surface_type: SurfaceType, 49 | }, 50 | #[serde(rename = "minecraft:water")] 51 | Water { 52 | offset: i32, 53 | surface_depth_multiplier: i32, 54 | add_stone_depth: bool, 55 | }, 56 | #[serde(rename = "minecraft:biome")] 57 | Biome { biome_is: Vec }, 58 | #[serde(rename = "minecraft:y_above")] 59 | YAbove { 60 | anchor: HeightConditionProvider, 61 | surface_depth_multiplier: i32, 62 | add_stone_depth: bool, 63 | }, 64 | #[serde(rename = "minecraft:not")] 65 | Not { invert: Box }, 66 | #[serde(rename = "minecraft:noise_threshold")] 67 | NoiseThreshold { 68 | noise: NoiseType, 69 | min_threshold: f64, 70 | max_threshold: f64, 71 | }, 72 | #[serde(rename = "minecraft:steep")] 73 | Steep, 74 | #[serde(rename = "minecraft:hole")] 75 | Hole, 76 | #[serde(rename = "minecraft:bandlands")] 77 | Bandlands, 78 | #[serde(rename = "minecraft:temperature")] 79 | Temperature, 80 | } 81 | 82 | #[derive(Serialize, Deserialize)] 83 | pub enum NoiseType { 84 | #[serde(rename = "minecraft:surface")] 85 | Surface, 86 | #[serde(rename = "minecraft:surface_swamp")] 87 | SurfaceSwamp, 88 | #[serde(rename = "minecraft:packed_ice")] 89 | PackedIce, 90 | #[serde(rename = "minecraft:ice")] 91 | Ice, 92 | #[serde(rename = "minecraft:powder_snow")] 93 | PowderSnow, 94 | #[serde(rename = "minecraft:calcite")] 95 | Calcite, 96 | #[serde(rename = "minecraft:gravel")] 97 | Gravel, 98 | #[serde(rename = "minecraft:patch")] 99 | Patch, 100 | #[serde(rename = "minecraft:nether_state_selector")] 101 | NetherStateSelector, 102 | #[serde(rename = "minecraft:netherrack")] 103 | Netherrack, 104 | #[serde(rename = "minecraft:nether_wart")] 105 | NetherWart, 106 | #[serde(rename = "minecraft:soul_sand_layer")] 107 | SoulSandLayer, 108 | #[serde(rename = "minecraft:gravel_layer")] 109 | GravelLayer, 110 | } 111 | 112 | 113 | #[derive(Serialize, Deserialize)] 114 | #[serde(untagged)] 115 | pub enum HeightConditionProvider { 116 | AboveBottom { above_bottom: i32 }, 117 | Absolute { absolute: i32 }, 118 | BelowTop { below_top: i32 }, 119 | } 120 | 121 | #[derive(Serialize, Deserialize)] 122 | pub struct NoiseOptions { 123 | pub min_y: i32, 124 | pub height: i32, 125 | pub size_horizontal: i32, 126 | pub size_vertical: i32, 127 | #[serde(default = "Default::default")] 128 | pub island_noise_override: bool, 129 | #[serde(default = "Default::default")] 130 | pub amplified: bool, 131 | #[serde(default = "Default::default")] 132 | pub large_biomes: bool, 133 | pub sampling: NoiseSampling, 134 | pub top_slide: NoiseCurve, 135 | pub bottom_slide: NoiseCurve, 136 | pub terrain_shaper: TerrainShaper, 137 | } 138 | 139 | #[derive(Serialize, Deserialize)] 140 | pub struct TerrainShaper { 141 | pub offset: TerrainShaperValue, 142 | pub factor: TerrainShaperValue, 143 | pub jaggedness: TerrainShaperValue, 144 | } 145 | 146 | #[derive(Serialize, Deserialize)] 147 | #[serde(untagged)] 148 | pub enum TerrainShaperValue { 149 | Spline { 150 | coordinate: TerrainSpineCoordinate, 151 | points: Vec, 152 | }, 153 | Constant(f32), 154 | } 155 | 156 | #[derive(Serialize, Deserialize)] 157 | pub struct TerrainSplinePoint { 158 | pub location: f32, 159 | pub value: TerrainShaperValue, 160 | pub derivative: f32, 161 | } 162 | 163 | #[derive(Serialize, Deserialize)] 164 | #[serde(rename_all = "snake_case")] 165 | pub enum TerrainSpineCoordinate { 166 | Continents, 167 | Erosion, 168 | Ridges, 169 | Weirdness, 170 | } 171 | 172 | #[derive(Serialize, Deserialize)] 173 | pub struct NoiseSampling { 174 | pub xz_scale: f64, 175 | pub xz_factor: f64, 176 | pub y_scale: f64, 177 | pub y_factor: f64, 178 | } 179 | 180 | #[derive(Serialize, Deserialize)] 181 | pub struct NoiseCurve { 182 | pub target: f32, 183 | pub size: i32, 184 | pub offset: i32, 185 | } 186 | 187 | 188 | #[derive(Serialize, Deserialize)] 189 | pub struct BlockState { 190 | #[serde(rename = "Name")] 191 | pub name: UnlocalizedName, 192 | #[serde(rename = "Properties")] 193 | #[serde(default = "Default::default")] 194 | pub properties: HashMap, 195 | } 196 | 197 | #[derive(Serialize, Deserialize)] 198 | pub struct NoiseRouter { 199 | barrier: DensityFunctionProvider, 200 | fluid_level_floodedness: DensityFunctionProvider, 201 | fluid_level_spread: DensityFunctionProvider, 202 | lava: DensityFunctionProvider, 203 | temperature: DensityFunctionProvider, 204 | vegetation: DensityFunctionProvider, 205 | continents: DensityFunctionProvider, 206 | erosion: DensityFunctionProvider, 207 | depth: DensityFunctionProvider, 208 | ridges: DensityFunctionProvider, 209 | initial_density_without_jaggedness: DensityFunctionProvider, 210 | final_density: DensityFunctionProvider, 211 | vein_toggle: DensityFunctionProvider, 212 | vein_ridged: DensityFunctionProvider, 213 | vein_gap: DensityFunctionProvider, 214 | } 215 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/processors.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::data::tags::TagProvider; 5 | 6 | use super::{features::HeightMaps, noise_settings::BlockState}; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct ProcessorList { 10 | processors: Vec, 11 | } 12 | 13 | #[derive(Serialize, Deserialize)] 14 | #[serde(tag = "processor_type")] 15 | pub enum Processor { 16 | #[serde(rename = "minecraft:rule")] 17 | Rule { rules: Vec }, 18 | #[serde(rename = "minecraft:block_age")] 19 | BlockAge { mossiness: f32 }, 20 | #[serde(rename = "minecraft:block_ignore")] 21 | BlockIgnore { blocks: Vec }, 22 | #[serde(rename = "minecraft:gravity")] 23 | Gravity { heightmap: HeightMaps, offset: i32 }, 24 | #[serde(rename = "minecraft:block_rot")] 25 | BlockRot { integrity: f32 }, 26 | #[serde(rename = "minecraft:blackstone_replace")] 27 | BlackstoneReplace, 28 | #[serde(rename = "minecraft:jigsaw_replacement")] 29 | JigsawReplacement, 30 | #[serde(rename = "minecraft:lava_submerged_block")] 31 | LavaSubmergedBlock, 32 | #[serde(rename = "minecraft:nop")] 33 | Nop, 34 | #[serde(rename = "minecraft:protected_blocks")] 35 | ProtectedBlocks { value: TagProvider }, 36 | } 37 | 38 | #[derive(Serialize, Deserialize)] 39 | pub struct ProcessorRule { 40 | pub position_predicate: Option, 41 | pub input_predicate: ProcessorPredicate, 42 | pub location_predicate: ProcessorPredicate, 43 | pub output_state: BlockState, 44 | /// Never used in vanilla 45 | // TODO: this is nbt 46 | pub output_nbt: Option, 47 | } 48 | 49 | #[derive(Serialize, Deserialize)] 50 | #[serde(tag = "predicate_type")] 51 | pub enum ProcessorPredicate { 52 | #[serde(rename = "minecraft:always_true")] 53 | AlwaysTrue, 54 | #[serde(rename = "minecraft:axis_aligned_linear_pos")] 55 | AxisAlignedLinearPos { 56 | axis: Axis, 57 | min_chance: f32, 58 | max_chance: f32, 59 | min_dist: i32, 60 | max_dist: i32, 61 | }, 62 | #[serde(rename = "minecraft:block_match")] 63 | BlockMatch { block: UnlocalizedName }, 64 | #[serde(rename = "minecraft:blockstate_match")] 65 | BlockstateMatch { block_state: BlockState }, 66 | #[serde(rename = "minecraft:random_block_match")] 67 | RandomBlockMatch { 68 | block: UnlocalizedName, 69 | probability: f32, 70 | }, 71 | #[serde(rename = "minecraft:tag_match")] 72 | TagMatch { tag: UnlocalizedName }, 73 | } 74 | 75 | #[derive(Serialize, Deserialize)] 76 | #[serde(tag = "predicate_type")] 77 | pub enum PositionPredicate { 78 | #[serde(rename = "minecraft:always_true")] 79 | AlwaysTrue, 80 | #[serde(rename = "minecraft:axis_aligned_linear_pos")] 81 | AxisAlignedLinearPos { 82 | axis: Axis, 83 | min_chance: f32, 84 | max_chance: f32, 85 | min_dist: i32, 86 | max_dist: i32, 87 | }, 88 | #[serde(rename = "minecraft:linear_pos")] 89 | LinearPos { 90 | min_chance: f32, 91 | max_chance: f32, 92 | min_dist: i32, 93 | max_dist: i32, 94 | }, 95 | } 96 | 97 | #[derive(Serialize, Deserialize)] 98 | pub enum Axis { 99 | #[serde(rename = "x")] 100 | X, 101 | #[serde(rename = "y")] 102 | Y, 103 | #[serde(rename = "z")] 104 | Z, 105 | } 106 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/structure_features.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | #[serde(tag = "type", content = "config")] 6 | pub enum StructureFeatures { 7 | #[serde(rename = "minecraft:bastion_remnant")] 8 | BastionRemnant { 9 | start_pool: UnlocalizedName, 10 | size: i32, 11 | }, 12 | #[serde(rename = "minecraft:buried_treasure")] 13 | BuriedTreasure { probability: f32 }, 14 | #[serde(rename = "minecraft:desert_pyramid")] 15 | DesertPyramid {}, 16 | #[serde(rename = "minecraft:endcity")] 17 | Endcity {}, 18 | #[serde(rename = "minecraft:fortress")] 19 | Fortress {}, 20 | #[serde(rename = "minecraft:igloo")] 21 | Igloo {}, 22 | #[serde(rename = "minecraft:jungle_pyramid")] 23 | JunglePyramid {}, 24 | #[serde(rename = "minecraft:mansion")] 25 | Mansion {}, 26 | #[serde(rename = "minecraft:mineshaft")] 27 | Mineshaft { 28 | r#type: MineshaftType, 29 | probability: f32, 30 | }, 31 | #[serde(rename = "minecraft:monument")] 32 | Monument {}, 33 | #[serde(rename = "minecraft:nether_fossil")] 34 | NetherFossil {}, 35 | #[serde(rename = "minecraft:ocean_ruin")] 36 | OceanRuin { 37 | biome_temp: BiomeTemperature, 38 | large_probability: f32, 39 | cluster_probability: f32, 40 | }, 41 | #[serde(rename = "minecraft:pillager_outpost")] 42 | PillagerOutpost { 43 | start_pool: UnlocalizedName, 44 | size: i32, 45 | }, 46 | #[serde(rename = "minecraft:ruined_portal")] 47 | RuinedPortal { portal_type: RuinedPortalType }, 48 | #[serde(rename = "minecraft:shipwreck")] 49 | Shipwreck { 50 | #[serde(default = "Default::default")] 51 | is_beached: bool, 52 | }, 53 | #[serde(rename = "minecraft:stronghold")] 54 | Stronghold {}, 55 | #[serde(rename = "minecraft:swamp_hut")] 56 | SwampHut {}, 57 | #[serde(rename = "minecraft:village")] 58 | Village { 59 | start_pool: UnlocalizedName, 60 | size: i32, 61 | }, 62 | } 63 | 64 | #[derive(Serialize, Deserialize)] 65 | #[serde(rename_all = "snake_case")] 66 | pub enum MineshaftType { 67 | Normal, 68 | Mesa, 69 | } 70 | 71 | #[derive(Serialize, Deserialize)] 72 | #[serde(rename_all = "snake_case")] 73 | pub enum BiomeTemperature { 74 | Warm, 75 | Cold, 76 | } 77 | 78 | #[derive(Serialize, Deserialize)] 79 | #[serde(rename_all = "snake_case")] 80 | pub enum RuinedPortalType { 81 | Standard, 82 | Desert, 83 | Jungle, 84 | Swamp, 85 | Mountain, 86 | Ocean, 87 | Nether, 88 | } 89 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/structure_set.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::data::tags::IdsOrTag; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | #[serde(untagged)] 8 | pub enum StructureSetProvider { 9 | Reference(UnlocalizedName), 10 | Inline(StructureSet), 11 | } 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct StructureSet { 15 | pub structures: Vec, 16 | pub placement: StructurePlacementModifier, 17 | } 18 | 19 | #[derive(Serialize, Deserialize)] 20 | pub struct WeightedStructure { 21 | structure: IdsOrTag, 22 | weight: i32, 23 | } 24 | 25 | #[derive(Serialize, Deserialize)] 26 | #[serde(tag = "type")] 27 | pub enum StructurePlacementModifier { 28 | #[serde(rename = "minecraft:random_spread")] 29 | RandomSpread { 30 | spread_type: Option, 31 | spacing: u16, 32 | separation: u16, 33 | salt: u64, 34 | locate_offset: Option<[i8; 3]>, 35 | }, 36 | #[serde(rename = "minecraft:concentric_rings")] 37 | ConcentricRings { 38 | distance: u16, 39 | spread: u16, 40 | count: u16, 41 | }, 42 | } 43 | 44 | #[derive(Serialize, Deserialize)] 45 | #[serde(rename_all = "snake_case")] 46 | pub enum SpreadType { 47 | Linear, 48 | Triangular, 49 | } 50 | -------------------------------------------------------------------------------- /datapack/src/data/world_gen/surface_builders.rs: -------------------------------------------------------------------------------- 1 | use qdat::UnlocalizedName; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::noise_settings::BlockState; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct SurfaceBuilder { 8 | pub r#type: UnlocalizedName, 9 | pub config: SurfaceBuilderConfig, 10 | } 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub struct SurfaceBuilderConfig { 14 | pub top_material: BlockState, 15 | pub under_material: BlockState, 16 | pub underwater_material: BlockState, 17 | } 18 | -------------------------------------------------------------------------------- /datapack/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! quartz_datapack is a crate to read and write minecraft datapacks 2 | //! 3 | //! Note: To avoid the amount of single-use datatypes some types have notes ensuring the usage is valid
4 | //! If these notes are not followed then the datapack will not be valid by vanilla's standards
5 | //! To help with this, manual Serialize and Deserialize implementations will enforce these rules
6 | //! Though, where custom implementations were not already necessary for the datatype these were not added 7 | 8 | /// Unwraps the fields and returns a missing_field error if they are `None` 9 | macro_rules! missing_field_error { 10 | ($($field: ident, $field_str: literal),*) => { 11 | $(let $field = match $field { 12 | Some(f) => f, 13 | None => return Err(serde::de::Error::missing_field($field_str)), 14 | };)* 15 | }; 16 | } 17 | 18 | /// Generates a map visitor to deserialize a map with the given fields 19 | macro_rules! map_visitor { 20 | ($map: ident, $(($field: ident, $field_str: literal, $field_type: ty)),*) => { 21 | $(let mut $field = None;)* 22 | 23 | while let Some(__key__) = $map.next_key()? { 24 | match __key__ { 25 | $($field_str => { 26 | if $field.is_some() { 27 | return Err(serde::de::Error::duplicate_field($field_str)) 28 | } 29 | 30 | $field = Some($map.next_value::<$field_type>()?); 31 | },)* 32 | _ => return Err(serde::de::Error::unknown_field(__key__, &[$($field_str),*])) 33 | } 34 | } 35 | }; 36 | } 37 | 38 | mod datapack; 39 | pub use datapack::*; 40 | pub mod data; 41 | -------------------------------------------------------------------------------- /datapack/tests/default_datapack.rs: -------------------------------------------------------------------------------- 1 | use quartz_datapack::{DataPack, VersionFilter}; 2 | 3 | #[ignore] 4 | #[test] 5 | fn default_datapack_test() { 6 | let datapack_path = "../run/datapacks/"; 7 | 8 | // We use a none filter because we don't want the filter to trigger 9 | let output = DataPack::read_datapacks(&datapack_path, VersionFilter::None); 10 | 11 | assert!(output.is_ok()); 12 | 13 | let output = output.unwrap(); 14 | 15 | for pack in output { 16 | let pack = pack.unwrap(); 17 | let mut path = "../run/out/datapacks/".to_owned(); 18 | path.push_str(pack.name()); 19 | pack.write_datapack(&path).unwrap(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz_macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | quartz_macros_impl = { path = '../macros_impl' } 11 | syn = { version = "1.0", features = ["full"] } 12 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use quartz_macros_impl::*; 4 | use syn::{parse_macro_input, DeriveInput}; 5 | 6 | #[proc_macro_derive(WriteToPacket, attributes(packet_serde))] 7 | pub fn derive_write_to_packet(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 8 | packet::derive_write_to_packet_impl(parse_macro_input!(item as DeriveInput)).into() 9 | } 10 | 11 | #[proc_macro_derive(ReadFromPacket, attributes(packet_serde))] 12 | pub fn derive_read_from_packet(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 13 | packet::derive_read_from_packet_impl(parse_macro_input!(item as DeriveInput)).into() 14 | } 15 | -------------------------------------------------------------------------------- /macros_impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz_macros_impl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | proc-macro2 = "1.0" 8 | proc-macro-crate = "1.0" 9 | quote = "1.0" 10 | syn = { version = "1.0", features = ["full"] } 11 | -------------------------------------------------------------------------------- /macros_impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod packet; 2 | 3 | use proc_macro2::{Span, TokenStream}; 4 | use proc_macro_crate::{crate_name, FoundCrate}; 5 | use quote::quote; 6 | use syn::{Error, GenericArgument, Ident, PathArguments, Result, Type}; 7 | 8 | pub fn quartz() -> TokenStream { 9 | match crate_name("quartz") { 10 | Ok(FoundCrate::Itself) => quote! { crate }, 11 | Ok(FoundCrate::Name(name)) => { 12 | let name = Ident::new(&name, Span::call_site()); 13 | quote! { ::#name } 14 | } 15 | Err(e) => Error::new(Span::call_site(), format!("{e}")).to_compile_error(), 16 | } 17 | } 18 | 19 | pub fn quartz_net() -> TokenStream { 20 | match crate_name("quartz_net") { 21 | Ok(FoundCrate::Itself) => quote! { crate }, 22 | Ok(FoundCrate::Name(name)) => { 23 | let name = Ident::new(&name, Span::call_site()); 24 | quote! { ::#name } 25 | } 26 | Err(e) => Error::new(Span::call_site(), format!("{e}")).to_compile_error(), 27 | } 28 | } 29 | 30 | pub fn is_boxed_slice(ty: &Type) -> bool { 31 | match ty { 32 | Type::Path(path) => 33 | path.qself.is_none() 34 | && path.path.leading_colon.is_none() 35 | && !path.path.segments.is_empty() 36 | && path.path.segments.last().unwrap().ident == "Box" 37 | && matches!(extract_type_from_container(ty), Ok(Type::Slice(_))), 38 | _ => false, 39 | } 40 | } 41 | 42 | pub fn is_option(ty: &Type) -> bool { 43 | match ty { 44 | Type::Path(path) => 45 | path.qself.is_none() 46 | && path.path.leading_colon.is_none() 47 | && !path.path.segments.is_empty() 48 | && path.path.segments.last().unwrap().ident == "Option", 49 | _ => false, 50 | } 51 | } 52 | 53 | pub fn extract_type_from_container(ty: &Type) -> Result { 54 | match ty { 55 | Type::Slice(slice) => Ok(slice.elem.as_ref().clone()), 56 | Type::Path(path) => { 57 | let type_params = &path.path.segments.last().unwrap().arguments; 58 | 59 | let generic_arg = match type_params { 60 | PathArguments::AngleBracketed(params) => params.args.first().unwrap(), 61 | tokens => return Err(Error::new_spanned(tokens, "Expected type parameter")), 62 | }; 63 | 64 | match generic_arg { 65 | GenericArgument::Type(ty) => Ok(ty.clone()), 66 | arg => Err(Error::new_spanned(arg, "Expected type parameter")), 67 | } 68 | } 69 | ty => Err(Error::new_spanned(ty, "Expected path type")), 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /macros_impl/src/packet/mod.rs: -------------------------------------------------------------------------------- 1 | mod gen; 2 | mod parse; 3 | 4 | pub use gen::*; 5 | pub use parse::*; 6 | 7 | use proc_macro2::TokenStream; 8 | use syn::{Data, DeriveInput}; 9 | 10 | pub fn derive_write_to_packet_impl(input: DeriveInput) -> TokenStream { 11 | match &input.data { 12 | Data::Enum(data) => { 13 | let variants = match parse_enum(data, Side::Write) { 14 | Ok(variants) => variants, 15 | Err(e) => return e.to_compile_error(), 16 | }; 17 | gen_enum_serializer_impl(input, &variants) 18 | } 19 | _ => { 20 | let fields = match parse_fields(&input, Side::Write) { 21 | Ok(fields) => fields, 22 | Err(e) => return e.to_compile_error(), 23 | }; 24 | gen_struct_serializer_impl(input, &fields) 25 | } 26 | } 27 | } 28 | 29 | pub fn derive_read_from_packet_impl(input: DeriveInput) -> TokenStream { 30 | let fields = match parse_fields(&input, Side::Read) { 31 | Ok(fields) => fields, 32 | Err(e) => return e.to_compile_error(), 33 | }; 34 | gen_struct_deserializer_impl(input, &fields) 35 | } 36 | 37 | #[derive(Clone, Copy, PartialEq, Eq)] 38 | pub(crate) enum Side { 39 | Read, 40 | Write, 41 | } 42 | -------------------------------------------------------------------------------- /network/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz_net" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | qdat = { path = "../qdat" } 10 | quartz_chat = { path = "../chat" } 11 | quartz_macros = { path = "../macros" } 12 | quartz_nbt = "0.2.5" 13 | 14 | uuid = { version = "1", features = ["serde", "v4"] } 15 | openssl = "0.10" 16 | byteorder = "1.4" 17 | serde = "1" 18 | serde_json = "1" 19 | log = "0.4" 20 | 21 | [build-dependencies] 22 | quartz_macros_impl = { path = '../macros_impl' } 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = { version = "1.0", features = ["preserve_order"]} 25 | indexmap = { version = "1.7", features = ["serde"] } 26 | proc-macro2 = "1.0.24" 27 | quote = "1.0.8" 28 | once_cell = "1.8.0" 29 | syn = { version = "1.0.55", features = ["full"] } -------------------------------------------------------------------------------- /network/src/bitmask.rs: -------------------------------------------------------------------------------- 1 | use crate::{PacketBuffer, PacketSerdeError, ReadFromPacket, WriteToPacket}; 2 | use log::warn; 3 | 4 | #[derive(Clone, Copy, Debug)] 5 | pub struct BitMask(u128); 6 | 7 | impl BitMask { 8 | pub fn new() -> Self { 9 | Self(0) 10 | } 11 | 12 | pub fn into_raw(self) -> u128 { 13 | self.0 14 | } 15 | 16 | pub fn set(&mut self, index: usize) { 17 | self.0 |= 1u128 << index; 18 | } 19 | 20 | pub fn as_empty(&self) -> Self { 21 | Self(!self.0 | 1) 22 | } 23 | } 24 | 25 | impl Default for BitMask { 26 | fn default() -> Self { 27 | Self::new() 28 | } 29 | } 30 | 31 | impl ReadFromPacket for BitMask { 32 | fn read_from(buffer: &mut PacketBuffer) -> Result { 33 | let len = buffer.read_varying::()? as usize; 34 | 35 | if len > 2 { 36 | warn!("Encountered bit mask containing more than 128 bits"); 37 | } 38 | 39 | let mut mask = 0; 40 | for _ in 0 .. len.min(2) { 41 | mask <<= 64; 42 | mask |= buffer.read::()? as u128; 43 | } 44 | 45 | Ok(Self(mask)) 46 | } 47 | } 48 | 49 | impl WriteToPacket for BitMask { 50 | fn write_to(&self, buffer: &mut PacketBuffer) { 51 | let lo = (self.0 & u64::MAX as u128) as u64; 52 | let hi = (self.0 >> 64) as u64; 53 | 54 | if hi == 0 { 55 | buffer.write_varying(&1i32); 56 | buffer.write(&lo); 57 | } else { 58 | buffer.write_varying(&2i32); 59 | buffer.write(&lo); 60 | buffer.write(&hi); 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /network/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod bitmask; 2 | mod netutil; 3 | pub mod packet_data; 4 | 5 | pub use bitmask::*; 6 | pub use netutil::*; 7 | 8 | /// All possible states of a client's connection to the server. 9 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 10 | pub enum ConnectionState { 11 | /// The handshake state of the connection in which the client selects the next state to enter: 12 | /// either the `Status` state or `Login` state. 13 | Handshake, 14 | /// The client is requesting a server status ping. 15 | Status, 16 | /// The client is logging into the server. 17 | Login, 18 | /// The client has successfully logged into the server and is playing the game. 19 | Play, 20 | /// The client has disconnected. 21 | Disconnected, 22 | } 23 | 24 | /// The numeric protocol version the server uses. 25 | pub const PROTOCOL_VERSION: i32 = 755; 26 | /// The ID for the legacy ping packet. 27 | pub const LEGACY_PING_PACKET_ID: i32 = 0xFE; 28 | 29 | mod build { 30 | #![allow(clippy::redundant_pattern, clippy::match_single_binding)] 31 | use super::*; 32 | include!(concat!(env!("OUT_DIR"), "/packet_def_output.rs")); 33 | } 34 | 35 | pub use build::*; 36 | -------------------------------------------------------------------------------- /qdat/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qdat" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde = {version = "1", features = ["derive"]} 10 | phf = { version = "0.11", features = ["macros"] } 11 | tinyvec = "1.3" 12 | serde_json = "1" 13 | flashmap = "0.1" 14 | 15 | [build-dependencies] 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = { version = "1.0", features = ["preserve_order"]} 18 | indexmap = { version = "1.7", features = ["serde"] } 19 | proc-macro2 = "1.0.24" 20 | quote = "1.0.8" 21 | syn = { version = "1.0.55", features = ["full"] } 22 | prettyplease = "0.1" -------------------------------------------------------------------------------- /qdat/build.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::ptr_arg)] 2 | mod buildscript; 3 | 4 | fn main() { 5 | buildscript::gen_blockstates(); 6 | buildscript::gen_items(); 7 | println!("cargo:rerun-if-changed=build.rs"); 8 | } 9 | -------------------------------------------------------------------------------- /qdat/buildscript/items.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::Path}; 2 | 3 | use indexmap::IndexMap; 4 | 5 | use proc_macro2::TokenStream; 6 | use serde::Deserialize; 7 | 8 | use quote::{format_ident, quote}; 9 | 10 | use crate::buildscript::item_info::ItemInfo; 11 | 12 | pub fn gen_items() { 13 | let out_dir = env::var_os("OUT_DIR").unwrap(); 14 | let dest_path = Path::new(&out_dir).join("items_output.rs"); 15 | 16 | let data = serde_json::from_str::>(include_str!( 17 | "../../assets/items.json" 18 | )) 19 | .expect("Error parsing items.json"); 20 | 21 | let item_defs = gen_const_item_structs(&data); 22 | let usize_fn = gen_item_from_usize(&data); 23 | let uln_fn = gen_item_from_id(&data); 24 | 25 | std::fs::write( 26 | &dest_path, 27 | quote! { 28 | use phf::phf_map; 29 | #item_defs 30 | 31 | #usize_fn 32 | #uln_fn 33 | } 34 | .to_string(), 35 | ) 36 | .unwrap(); 37 | super::format_in_place(dest_path.as_os_str()); 38 | 39 | println!("cargo:rerun-if-changed=../assets/items.json"); 40 | println!("cargo:rerun-if-changed=buildscript/items.rs"); 41 | println!("cargo:rerun-if-changed=buildscript/item_info.rs"); 42 | } 43 | 44 | /// Generates a const variable for each vanilla item 45 | fn gen_const_item_structs(data: &IndexMap) -> TokenStream { 46 | let mut streams = Vec::new(); 47 | 48 | for (i, (name, item)) in data.iter().enumerate() { 49 | let ident = format_ident!("{}_ITEM", name.to_uppercase()); 50 | let stack_size = item.stack_size; 51 | let rarity = item.rarity; 52 | let num_id = i as u16; 53 | 54 | streams.push(if let Some(info) = &item.info { 55 | quote! { 56 | const #ident: Item = Item { 57 | id: #name, 58 | num_id: #num_id, 59 | stack_size: #stack_size, 60 | rarity: #rarity, 61 | item_info: Some(#info) 62 | }; 63 | } 64 | } else { 65 | quote! { 66 | const #ident: Item = Item { 67 | id: #name, 68 | num_id: #num_id, 69 | stack_size: #stack_size, 70 | rarity: #rarity, 71 | item_info: None 72 | }; 73 | } 74 | }); 75 | } 76 | 77 | streams 78 | .into_iter() 79 | .reduce(|mut out, stream| { 80 | out.extend(stream); 81 | out 82 | }) 83 | .unwrap() 84 | } 85 | 86 | /// Generates a phf map to lookup from the i32 used in the network protocol to an item instance 87 | fn gen_item_from_usize(data: &IndexMap) -> TokenStream { 88 | let mut branches = Vec::new(); 89 | 90 | for (id, (name, _)) in data.iter().enumerate() { 91 | let name = format_ident!("{}_ITEM", name.to_uppercase()); 92 | let id = id as i32; 93 | branches.push(quote! { 94 | #id => #name 95 | }) 96 | } 97 | 98 | quote! { 99 | pub static ITEM_LOOKUP_BY_NUMERIC_ID: phf::Map = phf_map!{ 100 | #(#branches),* 101 | }; 102 | } 103 | } 104 | 105 | /// Generates a phf map to lookup from an identifier to an Item instance 106 | /// 107 | /// # Note 108 | /// Is explicitly not a ULN, it is just the identifier part 109 | fn gen_item_from_id(data: &IndexMap) -> TokenStream { 110 | let mut branches = Vec::new(); 111 | 112 | for (name, _) in data.iter() { 113 | let const_name = format_ident!("{}_ITEM", name.to_uppercase()); 114 | branches.push(quote! { 115 | #name => #const_name 116 | }) 117 | } 118 | 119 | quote! { 120 | pub static ITEM_LOOKUP_BY_NAME: phf::Map<&'static str, Item> = phf_map!{ 121 | #(#branches),* 122 | }; 123 | } 124 | } 125 | 126 | #[derive(Deserialize)] 127 | struct RawItemData { 128 | pub stack_size: u8, 129 | pub rarity: u8, 130 | pub info: Option, 131 | } 132 | -------------------------------------------------------------------------------- /qdat/buildscript/mod.rs: -------------------------------------------------------------------------------- 1 | mod blockstate; 2 | pub use blockstate::gen_blockstates; 3 | mod item_info; 4 | mod items; 5 | pub use items::gen_items; 6 | 7 | use std::{ffi::OsStr, process::Command}; 8 | 9 | pub(crate) fn format_in_place(file: &OsStr) { 10 | Command::new("rustfmt") 11 | .arg(file) 12 | .output() 13 | .unwrap_or_else(|_| panic!("Failed to format file: {file:?}")); 14 | } 15 | 16 | pub(crate) fn format_ast(code: String) -> syn::Result { 17 | let file = syn::parse_file(&code)?; 18 | Ok(prettyplease::unparse(&file)) 19 | } 20 | -------------------------------------------------------------------------------- /qdat/src/block/behavior/mod.rs: -------------------------------------------------------------------------------- 1 | // use crate::world::location::BlockPosition; 2 | 3 | // pub trait BlockBehavior { 4 | // // TO DO: add world argument 5 | // fn on_break(_position: BlockPosition, _state: &'static BlockState) {} 6 | // } 7 | 8 | pub struct BlockBehaviorSMT { 9 | // on_break: fn(position: BlockPosition, state: &'static BlockState), 10 | } 11 | 12 | impl BlockBehaviorSMT { 13 | // Allow new_without_default because we plan on yeeting this (or rewriting it) later anyway 14 | #[allow(clippy::new_without_default)] 15 | pub fn new() -> Self { 16 | BlockBehaviorSMT { 17 | // on_break: T::on_break, 18 | } 19 | } 20 | } 21 | 22 | impl Clone for BlockBehaviorSMT { 23 | fn clone(&self) -> Self { 24 | BlockBehaviorSMT { 25 | // on_break: self.on_break, 26 | } 27 | } 28 | } 29 | 30 | pub struct DefaultBehavior; 31 | 32 | // impl BlockBehavior for DefaultBehavior {} 33 | -------------------------------------------------------------------------------- /qdat/src/block/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod behavior; 2 | #[allow(missing_docs, nonstandard_style, dead_code)] 3 | pub mod states; 4 | 5 | use crate::UnlocalizedName; 6 | pub use behavior::BlockBehaviorSMT; 7 | use std::fmt::{self, Debug, Display, Formatter}; 8 | use tinyvec::ArrayVec; 9 | 10 | pub type StateID = u16; 11 | 12 | /// A specific block type, not to be confused with a block state which specifies variants of a type. This 13 | /// is used as a data handle for block states. 14 | pub struct Block { 15 | /// The namespaced key identifying this block. 16 | pub name: UnlocalizedName, 17 | /// All block state properties and their valid values. 18 | pub properties: ArrayVec<[(String, Vec); 16]>, 19 | /// The ID for the base state of this block. 20 | pub base_state: StateID, 21 | /// The ID for the default state of this block. 22 | pub default_state: StateID, 23 | /// The static method table defining the behavior of a block. 24 | pub behavior: BlockBehaviorSMT, 25 | } 26 | 27 | impl Display for Block { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 29 | Display::fmt(&self.name, f) 30 | } 31 | } 32 | 33 | impl Debug for Block { 34 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 35 | Display::fmt(self, f) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qdat/src/block/states.rs: -------------------------------------------------------------------------------- 1 | use crate::block::StateID; 2 | 3 | mod build { 4 | #![allow(clippy::should_implement_trait)] 5 | use super::*; 6 | include!(concat!(env!("OUT_DIR"), "/blockstate_output.rs")); 7 | } 8 | pub use build::*; 9 | 10 | pub const AIR: StateID = BlockStateData::Air.id(); 11 | pub const VOID_AIR: StateID = BlockStateData::VoidAir.id(); 12 | pub const CAVE_AIR: StateID = BlockStateData::CaveAir.id(); 13 | 14 | #[inline] 15 | pub const fn is_air(state: StateID) -> bool { 16 | state == AIR || state == VOID_AIR || state == CAVE_AIR 17 | } 18 | 19 | pub struct BlockStateMetadata { 20 | pub default_state_data: BlockStateData, 21 | pub internal_block_id: usize, 22 | } 23 | 24 | impl BlockStateMetadata { 25 | const fn new(default_state_data: BlockStateData, internal_block_id: usize) -> Self { 26 | BlockStateMetadata { 27 | default_state_data, 28 | internal_block_id, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /qdat/src/item/item.rs: -------------------------------------------------------------------------------- 1 | use crate::item::*; 2 | 3 | include!(concat!(env!("OUT_DIR"), "/items_output.rs")); 4 | 5 | /// Represents a minecraft item 6 | #[derive(Debug)] 7 | pub struct Item { 8 | /// The item id 9 | pub id: &'static str, 10 | pub num_id: u16, 11 | /// The max size a stack can be 12 | pub stack_size: u8, 13 | /// The rarity of the item 14 | pub rarity: u8, 15 | /// Holds extra info about the item 16 | pub item_info: Option, 17 | } 18 | -------------------------------------------------------------------------------- /qdat/src/item/item_info.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Debug, Deserialize)] 4 | #[serde(untagged)] 5 | /// Represents all possible extra data we need to store about items 6 | pub enum ItemInfo { 7 | /// The info needed for food items 8 | FoodInfo { 9 | /// The amount of hunger the item restores 10 | hunger: u32, 11 | /// The amount of saturation given 12 | saturation: f32, 13 | /// Weather the item is a meat item 14 | meat: bool, 15 | /// If the player can eat it while they are full 16 | eat_when_full: bool, 17 | /// If the player eats the food item faster than normal 18 | snack: bool, 19 | // TODO: Add status effects 20 | // status_effects: Vec 21 | }, 22 | 23 | /// The info needed for tools 24 | ToolInfo { 25 | /// The type of the tool 26 | tool_type: ToolType, 27 | /// The level of the tool 28 | level: ToolLevel, 29 | /// The damage done by the tool 30 | attack_damage: f32, // TODO: implements enchantments 31 | // possible_enchantments: Vec 32 | }, 33 | 34 | /// The info needed for armor items 35 | ArmorInfo { 36 | /// The type of the armor 37 | armor_type: ArmorType, 38 | // level: ArmorLevel, 39 | /// The protection given by the armor piece 40 | protection: u32, 41 | /// The amount of toughness given 42 | toughness: f32, 43 | /// The max durability of the item 44 | max_durability: u32, 45 | // TODO: implements enchantments 46 | // possible_enchantments: Vec 47 | }, 48 | 49 | /// Miscellaneous tools 50 | UsableInfo { 51 | /// Which tool type the item is 52 | usable_type: UsableType, 53 | /// The durability of the item 54 | max_durability: u32, 55 | }, 56 | 57 | /// Ranged weapon info 58 | RangedWeaponInfo { 59 | /// Which ranged weapon it is 60 | weapon_type: RangedWeapon, 61 | /// The max charge time 62 | max_charge_time: u32, 63 | /// The max durability 64 | max_durability: u32, 65 | }, 66 | } 67 | 68 | impl ItemInfo { 69 | /// Gets the max durability of an item 70 | pub const fn max_durability(&self) -> u32 { 71 | match self { 72 | ItemInfo::FoodInfo { .. } => 0, 73 | ItemInfo::ArmorInfo { max_durability, .. } => *max_durability, 74 | ItemInfo::UsableInfo { max_durability, .. } => *max_durability, 75 | ItemInfo::RangedWeaponInfo { max_durability, .. } => *max_durability, 76 | ItemInfo::ToolInfo { level, .. } => level.max_durability(), 77 | } 78 | } 79 | } 80 | 81 | /// The different types of tools 82 | #[derive(Debug, Deserialize)] 83 | #[serde(rename_all = "snake_case")] 84 | pub enum ToolType { 85 | Sword, 86 | Pickaxe, 87 | Shovel, 88 | Axe, 89 | Hoe, 90 | } 91 | 92 | /// The possible levels for tools 93 | #[derive(Debug, Deserialize)] 94 | #[serde(rename_all = "snake_case")] 95 | pub enum ToolLevel { 96 | Wood, 97 | Stone, 98 | Iron, 99 | Gold, 100 | Diamond, 101 | Netherite, 102 | } 103 | 104 | impl ToolLevel { 105 | /// The max durability of the tool based on its level 106 | pub const fn max_durability(&self) -> u32 { 107 | match self { 108 | ToolLevel::Wood => 59, 109 | ToolLevel::Gold => 32, 110 | ToolLevel::Stone => 131, 111 | ToolLevel::Iron => 250, 112 | ToolLevel::Diamond => 1561, 113 | ToolLevel::Netherite => 2031, 114 | } 115 | } 116 | } 117 | 118 | /// The possible armor pieces 119 | #[derive(Debug, Deserialize)] 120 | #[serde(rename_all = "snake_case")] 121 | pub enum ArmorType { 122 | Helmet, 123 | Chestplate, 124 | Leggings, 125 | Boots, 126 | } 127 | 128 | /// The possible usable items 129 | #[derive(Debug, Deserialize)] 130 | #[serde(rename_all = "snake_case")] 131 | pub enum UsableType { 132 | Shears, 133 | FishingRod, 134 | FlintAndSteel, 135 | Shield, 136 | CarrotStick, 137 | FungusStick, 138 | } 139 | 140 | /// The possible ranged weapons 141 | #[derive(Debug, Deserialize)] 142 | #[serde(rename_all = "snake_case")] 143 | pub enum RangedWeapon { 144 | Bow, 145 | Crossbow, 146 | Trident, 147 | } 148 | -------------------------------------------------------------------------------- /qdat/src/item/mod.rs: -------------------------------------------------------------------------------- 1 | // We allow module name to be the same because we don't expose the item module 2 | #[allow(clippy::module_inception)] 3 | mod item; 4 | #[allow(missing_docs)] 5 | mod item_info; 6 | 7 | pub use item::*; 8 | pub use item_info::*; 9 | 10 | use crate::block::states::BlockStateData; 11 | pub fn item_to_block(item: &'static Item) -> Option { 12 | crate::block::states::BLOCK_LOOKUP_BY_NAME 13 | .get(item.id) 14 | .map(|bsm| bsm.default_state_data) 15 | } 16 | -------------------------------------------------------------------------------- /qdat/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(new_uninit)] 2 | 3 | pub mod block; 4 | pub mod item; 5 | pub mod world; 6 | 7 | pub mod uln; 8 | 9 | pub use uln::*; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 14 | #[serde(rename = "snake_case")] 15 | pub enum Gamemode { 16 | /// None is only valid when sending the JoinGame packet previous gamemode packet 17 | None, 18 | Survival, 19 | Creative, 20 | Adventure, 21 | Specator, 22 | } 23 | -------------------------------------------------------------------------------- /qdat/src/world/lighting.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{self, Display, Formatter}, 4 | ptr, 5 | }; 6 | 7 | pub const LIGHTING_LENGTH: usize = 2048; 8 | type RawLightBuffer = [u8; LIGHTING_LENGTH]; 9 | 10 | pub struct Lighting { 11 | pub block: Option, 12 | pub sky: Option, 13 | } 14 | 15 | impl Lighting { 16 | pub const fn new() -> Self { 17 | Lighting { 18 | block: None, 19 | sky: None, 20 | } 21 | } 22 | 23 | #[inline] 24 | pub fn init_block(&mut self, source: &[u8]) -> Result<(), LightingInitError> { 25 | Self::init(source, &mut self.block) 26 | } 27 | 28 | #[inline] 29 | pub fn init_sky(&mut self, source: &[u8]) -> Result<(), LightingInitError> { 30 | Self::init(source, &mut self.sky) 31 | } 32 | 33 | #[inline] 34 | pub fn has_block_light(&self) -> bool { 35 | self.block.is_some() 36 | } 37 | 38 | #[inline] 39 | pub fn block_light(&self) -> Option<&LightBuffer> { 40 | self.block.as_ref() 41 | } 42 | 43 | #[inline] 44 | pub fn block_light_mut(&mut self) -> Option<&mut LightBuffer> { 45 | self.block.as_mut() 46 | } 47 | 48 | #[inline] 49 | pub fn has_sky_light(&self) -> bool { 50 | self.sky.is_some() 51 | } 52 | 53 | #[inline] 54 | pub fn sky_light(&self) -> Option<&LightBuffer> { 55 | self.sky.as_ref() 56 | } 57 | 58 | #[inline] 59 | pub fn sky_light_mut(&mut self) -> Option<&mut LightBuffer> { 60 | self.sky.as_mut() 61 | } 62 | 63 | fn init(source: &[u8], buffer: &mut Option) -> Result<(), LightingInitError> { 64 | if buffer.is_some() { 65 | return Err(LightingInitError::AlreadyInitialized); 66 | } 67 | 68 | *buffer = Some(LightBuffer::new(source)?); 69 | Ok(()) 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone)] 74 | #[repr(transparent)] 75 | pub struct LightBuffer { 76 | pub data: Box, 77 | } 78 | 79 | impl LightBuffer { 80 | pub fn new(source: &[u8]) -> Result { 81 | if source.len() != LIGHTING_LENGTH { 82 | return Err(LightingInitError::InvalidLength(source.len())); 83 | } 84 | 85 | let src = source.as_ptr(); 86 | let mut buf = Box::::new_uninit(); 87 | let dst = buf.as_mut_ptr() as *mut u8; 88 | 89 | // Safety: 90 | // - `src` is valid for `LIGHTING_LENGTH` u8s because of the length check at the beginning 91 | // of this function 92 | // - `dst` is valid for `LIGHTING_LENGTH` u8s because it points to a `LightBuffer` which 93 | // is a [u8; LIGHTING_LENGTH] 94 | // - `src` is aligned because it came from a valid slice, and `dst` is aligned because it 95 | // came from a Box 96 | // - `src` and `dst` do not overlap because `dst` was newly allocated 97 | unsafe { 98 | ptr::copy_nonoverlapping(src, dst, LIGHTING_LENGTH); 99 | } 100 | 101 | Ok(LightBuffer { 102 | // Safety: we properly initialized the array above 103 | data: unsafe { buf.assume_init() }, 104 | }) 105 | } 106 | } 107 | 108 | #[derive(Clone, Copy, Debug)] 109 | pub enum LightingInitError { 110 | InvalidLength(usize), 111 | AlreadyInitialized, 112 | } 113 | 114 | impl Display for LightingInitError { 115 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 116 | match self { 117 | Self::InvalidLength(len) => write!( 118 | f, 119 | "expected light buffer of length {LIGHTING_LENGTH} but found length of {len}" 120 | ), 121 | Self::AlreadyInitialized => write!(f, "light buffer already initialized"), 122 | } 123 | } 124 | } 125 | 126 | impl Error for LightingInitError {} 127 | -------------------------------------------------------------------------------- /qdat/src/world/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lighting; 2 | pub mod location; 3 | -------------------------------------------------------------------------------- /quartz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz" 3 | version = "0.1.0" 4 | authors = ["maddymakesgames", "Cassy343"] 5 | edition = "2021" 6 | 7 | [[bin]] 8 | name = "quartz_launcher" 9 | path = "src/main.rs" 10 | 11 | [lib] 12 | name = "quartz" 13 | path = "src/lib.rs" 14 | 15 | [dependencies] 16 | # Internal dependencies 17 | quartz_chat = { path = "../chat" } 18 | quartz_datapack = { path = "../datapack" } 19 | quartz_util = { path = "../util" } 20 | quartz_macros = { path = "../macros" } 21 | quartz_net = { path = "../network" } 22 | qdat = { path = "../qdat" } 23 | 24 | # External dependencies 25 | byteorder = "1.4.3" 26 | dashmap = "5.1.0" 27 | enum_dispatch = "0.3.7" 28 | flate2 = "1.0.20" 29 | flashmap = "0.1" 30 | futures-util = "0.3.16" 31 | hecs = {git = "https://github.com/Rusty-Quartz/checs", features = ["macros"]} 32 | hex = "0.4.3" 33 | linefeed = "0.6.0" 34 | log = "0.4.14" 35 | once_cell = "1.8.0" 36 | openssl = "0.10.34" 37 | parking_lot = { version = "0.12.0", features = ["nightly"] } 38 | quartz_commands = "0.1.0" 39 | quartz_nbt = { version = "0.2.5", features = ["serde"] } 40 | rand = "0.8.4" 41 | regex = "1.5" 42 | serde = { version = "1.0", features = ["derive"] } 43 | serde_json = "1.0" 44 | static_assertions = "1.1.0" 45 | tinyvec = "1.2.0" 46 | ureq = { version = "2.1.1", features = ["json"] } 47 | uuid = { version = "1", features = ["serde", "v4", "v3"] } 48 | noise = "0.7.0" 49 | 50 | [dependencies.tokio] 51 | version = "1.9.0" 52 | features = [ 53 | "io-util", 54 | "fs", 55 | "net", 56 | "parking_lot", 57 | "rt", 58 | "rt-multi-thread", 59 | "sync", 60 | "time" 61 | ] 62 | 63 | [build-dependencies] 64 | quartz_macros_impl = { path = '../macros_impl' } 65 | serde = { version = "1.0", features = ["derive"] } 66 | serde_json = { version = "1.0", features = ["preserve_order"]} 67 | indexmap = { version = "1.7", features = ["serde"] } 68 | proc-macro2 = "1.0.24" 69 | quote = "1.0.8" 70 | once_cell = "1.8.0" 71 | syn = { version = "1.0.55", features = ["full"] } 72 | 73 | [target.'cfg(unix)'.dependencies] 74 | termion = "2.0" -------------------------------------------------------------------------------- /quartz/format.sh: -------------------------------------------------------------------------------- 1 | # `cargo fmt` ignores some directories in here for some reason, so this file just invokes `rustfmt` 2 | # with the appropriate config on all rust source files manually 3 | 4 | find -name "*.rs" -exec rustfmt --config-path ../ {} \; 5 | -------------------------------------------------------------------------------- /quartz/src/base/assets.rs: -------------------------------------------------------------------------------- 1 | /// JSON data detailing block and state information. 2 | pub const BLOCK_INFO: &str = include_str!("../../../assets/blocks.json"); 3 | 4 | /// Item data detailing stack size, rarity, etc. 5 | pub const ITEM_INFO: &str = include_str!("../../../assets/items.json"); 6 | -------------------------------------------------------------------------------- /quartz/src/base/config.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use qdat::Gamemode; 3 | use quartz_chat::Component; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{ 6 | fs::{File, OpenOptions}, 7 | io::{self, prelude::*, Read, SeekFrom, Write}, 8 | path::Path, 9 | }; 10 | 11 | /// The main server configuration. 12 | #[derive(Serialize, Deserialize)] 13 | pub struct Config { 14 | /// The maximum number of players the server will allow, defaults to 50. 15 | pub max_players: u16, 16 | /// The server IP, defaults to "0.0.0.0" 17 | pub server_ip: String, 18 | /// The server port, defaults to 25565. 19 | pub port: u16, 20 | /// The server's message of the day, written using CFMT format (see `chat::cfmt::parse_cfmt`). 21 | pub motd: Component, 22 | /// Whether to run the server in online or offline mode 23 | /// Offline mode skips the login state of the connection flow 24 | pub online_mode: bool, 25 | /// The default gamemode for a player who joins the server 26 | pub default_gamemode: Gamemode, 27 | } 28 | 29 | // Instantiate a config with default values 30 | impl Default for Config { 31 | fn default() -> Self { 32 | Config { 33 | max_players: 50, 34 | server_ip: "0.0.0.0".to_owned(), 35 | port: 25565, 36 | motd: Component::text("A Minecraft Server".to_owned()), 37 | online_mode: true, 38 | default_gamemode: Gamemode::Survival, 39 | } 40 | } 41 | } 42 | 43 | /// Attempts to parse the server configuration at the given path. The config should be in JSON format. 44 | pub fn load_config(path: &Path) -> io::Result { 45 | let std_path = Path::new(path); 46 | 47 | if std_path.exists() { 48 | // Try to open the file 49 | let mut file = OpenOptions::new().read(true).write(true).open(std_path)?; 50 | 51 | // Read the file to a string 52 | let mut json = String::new(); 53 | file.read_to_string(&mut json)?; 54 | 55 | // Parse the json 56 | let config: Config = match serde_json::from_str(&json) { 57 | Ok(cfg) => cfg, 58 | Err(e) => { 59 | error!("Invalid config JSON: {}", e); 60 | return use_default(&mut file); 61 | } 62 | }; 63 | 64 | Ok(config) 65 | } else { 66 | info!("Config file not found, creating file"); 67 | use_default(&mut File::create(std_path)?) 68 | } 69 | } 70 | 71 | fn use_default(file: &mut File) -> io::Result { 72 | info!("Using default configurations"); 73 | 74 | let default = Config::default(); 75 | 76 | // Go to the beginning of the file 77 | file.seek(SeekFrom::Start(0))?; 78 | 79 | // Write the default JSON 80 | let json = serde_json::to_string_pretty(&default).unwrap(); 81 | let bytes = json.as_bytes(); 82 | file.write_all(bytes)?; 83 | 84 | // Reset the file length 85 | file.set_len(bytes.len() as u64)?; 86 | 87 | Ok(default) 88 | } 89 | -------------------------------------------------------------------------------- /quartz/src/base/diagnostic.rs: -------------------------------------------------------------------------------- 1 | pub struct Diagnostics { 2 | pub(crate) microseconds_per_tick: f64, 3 | } 4 | 5 | impl Diagnostics { 6 | pub const fn new() -> Self { 7 | Diagnostics { 8 | microseconds_per_tick: 0.0, 9 | } 10 | } 11 | 12 | /// Returns a buffered milliseconds per tick (MSPT) measurement. This reading is buffer for 100 13 | /// tick cycles. 14 | #[inline] 15 | pub fn mspt(&self) -> f64 { 16 | self.microseconds_per_tick / 1000.0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /quartz/src/base/exec.rs: -------------------------------------------------------------------------------- 1 | use linefeed::{DefaultTerminal, Interface}; 2 | use log::{error, info}; 3 | use once_cell::sync::OnceCell; 4 | use parking_lot::{Mutex, RwLock}; 5 | use std::{ 6 | fmt::Display, 7 | sync::{ 8 | atomic::{AtomicBool, Ordering}, 9 | Arc, 10 | }, 11 | time::{Duration, Instant}, 12 | }; 13 | use tokio::{runtime::Builder, task::LocalSet}; 14 | 15 | use crate::{CommandExecutor, Config, Diagnostics, QuartzServer}; 16 | 17 | /// The state variable that controls whether or not the server and its various sub-processes are running. 18 | /// If set to false then the server will gracefully stop. 19 | pub(crate) static RUNNING: AtomicBool = AtomicBool::new(false); 20 | static CONFIG: OnceCell> = OnceCell::new(); 21 | static RAW_CONSOLE: OnceCell>> = OnceCell::new(); 22 | pub static DIAGNOSTICS: Mutex = Mutex::new(Diagnostics::new()); 23 | static COMMAND_EXECUTOR: OnceCell = OnceCell::new(); 24 | 25 | /// Returns whether or not the server is running. 26 | #[inline] 27 | pub fn is_running() -> bool { 28 | RUNNING.load(Ordering::Acquire) 29 | } 30 | 31 | pub fn config() -> &'static RwLock { 32 | CONFIG.get().expect("Config not initialized yet") 33 | } 34 | 35 | pub fn raw_console() -> &'static Interface { 36 | &**RAW_CONSOLE.get().expect("Raw console not initialized") 37 | } 38 | 39 | pub unsafe fn raw_console_unchecked() -> &'static Interface { 40 | &**RAW_CONSOLE.get_unchecked() 41 | } 42 | 43 | pub fn display_to_console(message: &T) { 44 | match raw_console().lock_writer_erase() { 45 | Ok(mut writer) => 46 | if let Err(e) = writeln!(writer, "{message}") { 47 | error!("Failed to send message to console: {}", e); 48 | }, 49 | Err(e) => error!("Failed to lock console interface: {}", e), 50 | } 51 | } 52 | 53 | pub fn command_executor() -> &'static CommandExecutor { 54 | COMMAND_EXECUTOR 55 | .get() 56 | .expect("Command executor not initialized") 57 | } 58 | 59 | pub fn run(config: Config, raw_console: Arc>) { 60 | CONFIG 61 | .set(RwLock::new(config)) 62 | .ok() 63 | .expect("Config initialized before server was run."); 64 | RAW_CONSOLE 65 | .set(raw_console) 66 | .ok() 67 | .expect("Raw console initialized before server was run."); 68 | 69 | let rt = Builder::new_multi_thread() 70 | .enable_all() 71 | .thread_name("main-tick-thread") 72 | .build() 73 | .expect("Failed to build main-tick runtime"); 74 | let rt = Arc::new(rt); 75 | 76 | let mut server = QuartzServer::new(Arc::clone(&rt)); 77 | server.init(); 78 | COMMAND_EXECUTOR 79 | .set(CommandExecutor::new()) 80 | .ok() 81 | .expect("Command executor initialized unexpectedly"); 82 | 83 | info!("Started server thread"); 84 | 85 | let local_set = LocalSet::new(); 86 | local_set.block_on(&*rt, async move { 87 | let mut clock = ServerClock::new(); 88 | 89 | while RUNNING.load(Ordering::Acquire) { 90 | if let Some(mut guard) = DIAGNOSTICS.try_lock() { 91 | guard.microseconds_per_tick = clock.micros_ema; 92 | } 93 | 94 | clock.start(); 95 | server.tick().await; 96 | clock.finish_tick().await; 97 | } 98 | }); 99 | 100 | drop(local_set); 101 | 102 | let rt = Arc::try_unwrap(rt); 103 | match rt { 104 | Ok(rt) => rt.shutdown_timeout(Duration::from_secs(5)), 105 | Err(_) => error!("Failed to reclaim ownership of runtime"), 106 | } 107 | } 108 | 109 | const FULL_TICK_LENGTH: u64 = 50; 110 | const FULL_TICK: Duration = Duration::from_millis(FULL_TICK_LENGTH); 111 | 112 | /// Keeps track of the time each tick takes and regulates the server ticks per second (TPS). 113 | pub struct ServerClock { 114 | micros_ema: f64, 115 | time: Instant, 116 | } 117 | 118 | impl ServerClock { 119 | /// Creates a new clock with the given tick length in milliseconds. 120 | pub fn new() -> Self { 121 | ServerClock { 122 | micros_ema: 0.0, 123 | time: Instant::now(), 124 | } 125 | } 126 | 127 | /// Called at the start of a server tick. 128 | pub(crate) fn start(&mut self) { 129 | self.time = Instant::now(); 130 | } 131 | 132 | /// The tick code has finished executing, so record the time and sleep if extra time remains. 133 | async fn finish_tick(&mut self) -> f64 { 134 | let elapsed = self.time.elapsed(); 135 | let micros = elapsed.as_micros() as f64; 136 | self.micros_ema = (99.0 * self.micros_ema + micros) / 100.0; 137 | 138 | if elapsed < FULL_TICK { 139 | tokio::time::sleep(FULL_TICK - elapsed).await; 140 | } 141 | 142 | micros 143 | } 144 | 145 | /// Converts a milliseconds pet tick value to ticks per second. 146 | #[inline] 147 | pub fn as_tps(mspt: f64) -> f64 { 148 | if mspt < FULL_TICK_LENGTH as f64 { 149 | 1000.0 / FULL_TICK_LENGTH as f64 150 | } else { 151 | 1000.0 / mspt 152 | } 153 | } 154 | 155 | /// The maximum tps the server will tick at. 156 | #[inline] 157 | pub fn max_tps() -> f64 { 158 | 1000.0 / FULL_TICK_LENGTH as f64 159 | } 160 | } 161 | 162 | impl Default for ServerClock { 163 | fn default() -> Self { 164 | Self::new() 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /quartz/src/base/mod.rs: -------------------------------------------------------------------------------- 1 | // TODO: we might want to change the name of these to something else 2 | // depends on what we end up doing with StaticRegistry 3 | pub mod registries; 4 | pub(crate) mod static_registry; 5 | // TODO: move behind a compiler flag or something 6 | pub use super::static_registry::*; 7 | 8 | /// Contains quartz assets data. 9 | pub mod assets; 10 | /// Defines the server config. 11 | pub mod config; 12 | pub mod diagnostic; 13 | pub mod exec; 14 | /// Main server module. 15 | pub mod server; 16 | 17 | pub use config::Config; 18 | pub use diagnostic::Diagnostics; 19 | pub use exec::*; 20 | pub use server::QuartzServer; 21 | -------------------------------------------------------------------------------- /quartz/src/base/static_registry.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | block::{entity::StaticBlockEntity, StaticBlockState, *}, 3 | command::StaticCommandExecutor, 4 | }; 5 | use log::info; 6 | use once_cell::sync::OnceCell; 7 | use qdat::{ 8 | block::{states::BLOCK_LOOKUP_BY_NAME, Block}, 9 | UlnStr, 10 | }; 11 | 12 | static GLOBAL_STATIC_REGISTRY: OnceCell = OnceCell::new(); 13 | 14 | pub type BlockState = StaticBlockState; 15 | pub type StateID = qdat::block::StateID; 16 | pub type BlockEntity = StaticBlockEntity; 17 | pub type Registry = StaticRegistry; 18 | pub type CommandExecutor = StaticCommandExecutor; 19 | 20 | pub const fn null_state_id() -> StateID { 21 | 0u16 22 | } 23 | 24 | pub struct StaticRegistry { 25 | blocks: &'static [Block], 26 | global_palette: Box<[StaticBlockState]>, 27 | } 28 | 29 | impl StaticRegistry { 30 | pub(crate) fn init() -> Result<(), ()> { 31 | info!("Initializing static registry"); 32 | 33 | info!("Initializing blocks"); 34 | let mut raw = load_raw_block_data(); 35 | attach_behavior(&mut raw); 36 | let blocks = make_block_list(&raw).leak(); 37 | let global_palette = make_static_global_palette(&raw, blocks).into_boxed_slice(); 38 | 39 | GLOBAL_STATIC_REGISTRY 40 | .set(StaticRegistry { 41 | blocks, 42 | global_palette, 43 | }) 44 | .map_err(|_| ()) 45 | } 46 | 47 | #[inline] 48 | pub fn get() -> &'static Self { 49 | #[cfg(debug_assertions)] 50 | { 51 | GLOBAL_STATIC_REGISTRY 52 | .get() 53 | .expect("Global static registry not initialized") 54 | } 55 | 56 | #[cfg(not(debug_assertions))] 57 | { 58 | unsafe { GLOBAL_STATIC_REGISTRY.get_unchecked() } 59 | } 60 | } 61 | 62 | pub fn default_state(block_name: &UlnStr) -> Option { 63 | if block_name.namespace() != "minecraft" { 64 | return None; 65 | } 66 | 67 | BLOCK_LOOKUP_BY_NAME 68 | .get(block_name.identifier()) 69 | .map(|meta| StaticBlockState { 70 | // Safety: internal block IDs are guaranteed to be consistent and in-bounds 71 | handle: unsafe { Self::get().blocks.get_unchecked(meta.internal_block_id) }, 72 | data: meta.default_state_data, 73 | }) 74 | } 75 | 76 | pub fn state_for_id(id: StateID) -> Option<&'static BlockState> { 77 | Self::get().global_palette.get(id as usize) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /quartz/src/block/entities/furnace_entity.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | block::entity::BlockEntity, 3 | item::{get_item, Inventory, ItemStack}, 4 | }; 5 | use qdat::{world::location::BlockPosition, UlnStr}; 6 | 7 | use quartz_nbt::NbtCompound; 8 | 9 | // While this is somewhat accurate to how the Furnace BE will be implemented the tick method is no where near finished and some key fields are missing 10 | // Currently this is mostly for testing BEs 11 | 12 | pub struct FurnaceBlockEntity { 13 | #[allow(dead_code)] 14 | pos: BlockPosition, 15 | custom_name: String, 16 | lock: bool, 17 | items: Inventory, 18 | burn_time: i32, 19 | cook_time: i32, 20 | cook_time_total: i32, 21 | active: bool, 22 | } 23 | 24 | impl FurnaceBlockEntity { 25 | pub fn new(pos: BlockPosition, name: Option) -> Self { 26 | FurnaceBlockEntity { 27 | pos, 28 | custom_name: match name { 29 | Some(name) => name, 30 | _ => "Furnace".to_owned(), 31 | }, 32 | lock: false, 33 | items: Inventory::new(3), 34 | burn_time: 0, 35 | cook_time: 0, 36 | cook_time_total: 0, 37 | active: false, 38 | } 39 | } 40 | } 41 | 42 | impl BlockEntity for FurnaceBlockEntity { 43 | fn from_nbt(&mut self, nbt: &NbtCompound) { 44 | self.burn_time = nbt.get("BurnTime").unwrap_or(0); 45 | self.cook_time = nbt.get("CookTime").unwrap_or(0); 46 | self.cook_time_total = nbt.get("CookTimeTotal").unwrap_or(0); 47 | self.items.from_tag(nbt); 48 | 49 | if nbt.contains_key("CustomName") { 50 | self.custom_name = nbt.get("CustomName").unwrap_or("Furnace").to_owned(); 51 | } 52 | 53 | if nbt.contains_key("Lock") { 54 | self.lock = nbt.get("Lock").unwrap_or(false); 55 | } 56 | } 57 | 58 | fn write_nbt(&self, nbt: &mut NbtCompound) { 59 | nbt.insert("BurnTime".to_owned(), self.burn_time); 60 | nbt.insert("CookTime".to_owned(), self.cook_time); 61 | nbt.insert("CookTimeTotal".to_owned(), self.cook_time_total); 62 | self.items.write_tag(nbt); 63 | nbt.insert("CustomName".to_owned(), self.custom_name.clone()); 64 | nbt.insert("Lock".to_owned(), self.lock); 65 | } 66 | 67 | fn tick(&mut self) { 68 | // Currently just testing of inventories 69 | if self.active { 70 | self.cook_time += 1; 71 | if self.cook_time > self.cook_time_total { 72 | self.items.insert( 73 | 2, 74 | ItemStack::new(get_item(UlnStr::minecraft("stone")).unwrap()).into(), 75 | ); 76 | } 77 | } else if self.items.get(2).is_empty() { 78 | self.items.insert( 79 | 2, 80 | ItemStack::new(get_item(UlnStr::minecraft("stone")).unwrap()).into(), 81 | ); 82 | } else { 83 | self.items.increment(2); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /quartz/src/block/entity.rs: -------------------------------------------------------------------------------- 1 | use crate::block::entities::FurnaceBlockEntity; 2 | use enum_dispatch::enum_dispatch; 3 | use quartz_nbt::NbtCompound; 4 | 5 | // All block entities must impl this 6 | /// Trait for block entities 7 | #[enum_dispatch] 8 | pub trait BlockEntity { 9 | /// Writes the entity info to a compound tag 10 | fn write_nbt(&self, nbt: &mut NbtCompound); 11 | /// Reads info from a compound tag 12 | #[allow(clippy::wrong_self_convention)] 13 | fn from_nbt(&mut self, nbt: &NbtCompound); 14 | /// Ticks the block entity 15 | fn tick(&mut self); 16 | } 17 | 18 | #[enum_dispatch(BlockEntity)] 19 | pub enum StaticBlockEntity { 20 | FurnaceBlockEntity, 21 | } 22 | -------------------------------------------------------------------------------- /quartz/src/block/init.rs: -------------------------------------------------------------------------------- 1 | use qdat::{ 2 | block::{states::BLOCK_LOOKUP_BY_NAME, Block, BlockBehaviorSMT, StateID}, 3 | UnlocalizedName, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json; 7 | use std::collections::{BTreeMap, HashMap}; 8 | use tinyvec::ArrayVec; 9 | 10 | use crate::{ 11 | assets, 12 | block::{BlockStateImpl, StateBuilder, StaticBlockState, StaticStateBuilder}, 13 | }; 14 | 15 | pub(crate) fn load_raw_block_data() -> HashMap { 16 | serde_json::from_str::>(assets::BLOCK_INFO) 17 | .expect("assets/blocks.json is corrupted.") 18 | } 19 | 20 | pub(crate) fn attach_behavior(raw: &mut HashMap) { 21 | macro_rules! attach { 22 | ($behavior:ty, $( $block_name:literal ),+) => { 23 | $( 24 | raw.get_mut(concat!("minecraft:", $block_name)) 25 | .expect("Invalid block name during behavior attachment") 26 | .behavior = Some(BlockBehaviorSMT::new()); 27 | )+ 28 | }; 29 | } 30 | 31 | attach!(DefaultBehavior, "air", "stone"); 32 | } 33 | 34 | pub(crate) fn make_block_list(raw: &HashMap) -> Vec { 35 | let mut block_list = Vec::new(); 36 | 37 | let mut raw = raw.iter().collect::>(); 38 | raw.sort_by_key(|(_, info)| info.interm_id); 39 | 40 | for (name, block_info) in raw { 41 | let uln = UnlocalizedName::from_str(name) 42 | .expect("Invalid block name encountered during registration."); 43 | 44 | // This should never happen if the data integrity is not compromised 45 | if block_info.states.is_empty() { 46 | panic!("Invalid block encountered: {name}, no states found."); 47 | } 48 | 49 | block_list.push(Block { 50 | name: uln, 51 | properties: block_info 52 | .properties 53 | .clone() 54 | .into_iter() 55 | .collect::>(), 56 | base_state: block_info.states[0].id, 57 | default_state: block_info.default, 58 | behavior: block_info 59 | .behavior 60 | .clone() 61 | .unwrap_or_else(BlockBehaviorSMT::new), 62 | }); 63 | } 64 | 65 | block_list 66 | } 67 | 68 | // TODO: put behind feature flag if necessary 69 | pub(crate) fn make_static_global_palette( 70 | raw: &HashMap, 71 | blocks: &'static [Block], 72 | ) -> Vec { 73 | let mut global_palette = Vec::new(); 74 | 75 | for block in raw.values() { 76 | let handle: &'static Block = &blocks[block.interm_id]; 77 | 78 | for state_info in block.states.iter() { 79 | let default_state = StaticBlockState { 80 | handle, 81 | data: BLOCK_LOOKUP_BY_NAME 82 | .get(handle.name.identifier()) 83 | .unwrap() 84 | .default_state_data, 85 | }; 86 | let mut state_builder = StaticStateBuilder::new(default_state.clone()); 87 | 88 | for (key, value) in state_info.properties.iter() { 89 | state_builder 90 | .add_property(key.as_str(), value.as_str()) 91 | .unwrap(); 92 | } 93 | 94 | let state = state_builder.build(); 95 | 96 | // Make sure the computed ID matches the ID in the generated data 97 | assert_eq!( 98 | state_info.id, 99 | state.id(), 100 | "Computed ID for {} does not match stored ID.\nState is {:?}", 101 | state.handle.name, 102 | state.data 103 | ); 104 | 105 | global_palette.push(state); 106 | } 107 | } 108 | 109 | global_palette.sort_by_key(|state| state.id()); 110 | global_palette 111 | } 112 | 113 | #[derive(Serialize, Deserialize)] 114 | pub(crate) struct RawBlockInfo { 115 | // Use a BTreeMap for ordering so that we can compute state IDs 116 | #[serde(default = "BTreeMap::new")] 117 | properties: BTreeMap>, 118 | default: StateID, 119 | interm_id: usize, 120 | states: Vec>, 121 | #[serde(skip_serializing, skip_deserializing, default = "Option::default")] 122 | behavior: Option, 123 | } 124 | 125 | #[derive(Serialize, Deserialize)] 126 | struct RawStateInfo { 127 | id: T, 128 | #[serde(default = "BTreeMap::new")] 129 | properties: BTreeMap, 130 | } 131 | -------------------------------------------------------------------------------- /quartz/src/block/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod entity; 2 | mod init; 3 | pub mod state; 4 | pub(crate) use init::*; 5 | pub use state::*; 6 | 7 | #[allow(missing_docs)] 8 | pub mod entities { 9 | pub mod furnace_entity; 10 | pub use furnace_entity::FurnaceBlockEntity; 11 | } 12 | -------------------------------------------------------------------------------- /quartz/src/command/context.rs: -------------------------------------------------------------------------------- 1 | use crate::{display_to_console, network::AsyncWriteHandle, CommandExecutor, QuartzServer}; 2 | use quartz_chat::component::Component; 3 | use quartz_net::ClientBoundPacket; 4 | use uuid::Uuid; 5 | 6 | /// The context in which a command is executed. This has no use outside the lifecycle of a command. 7 | pub struct CommandContext<'ctx> { 8 | /// A shared reference to the server. 9 | pub server: &'ctx mut QuartzServer, 10 | /// A shared reference to the executor that created this context. 11 | pub executor: &'ctx CommandExecutor, 12 | /// The sender of the command. 13 | pub sender: CommandSender, 14 | } 15 | 16 | // Shortcut functions for getting argument values 17 | impl<'ctx> CommandContext<'ctx> { 18 | /// Creates a new command context with the given parameters. 19 | pub fn new( 20 | server: &'ctx mut QuartzServer, 21 | executor: &'ctx CommandExecutor, 22 | sender: CommandSender, 23 | ) -> Self { 24 | CommandContext { 25 | server, 26 | executor, 27 | sender, 28 | } 29 | } 30 | } 31 | 32 | /// A command sender, can be command block, player, or the console. 33 | pub enum CommandSender { 34 | /// The console sender type. 35 | Console, 36 | Client(AsyncWriteHandle), 37 | } 38 | 39 | impl CommandSender { 40 | /// Sends a message to the sender. 41 | pub fn send_message(&self, message: Component) { 42 | match self { 43 | CommandSender::Console => display_to_console(&message), 44 | CommandSender::Client(handle) => handle.send_packet(ClientBoundPacket::ChatMessage { 45 | sender: Uuid::from_u128(0), 46 | position: 1, 47 | json_data: Box::new(message), 48 | }), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /quartz/src/command/executor.rs: -------------------------------------------------------------------------------- 1 | use quartz_commands::CommandModule; 2 | 3 | use crate::command::CommandContext; 4 | 5 | pub struct StaticCommandExecutor; 6 | 7 | impl StaticCommandExecutor { 8 | pub fn new() -> Self { 9 | StaticCommandExecutor 10 | } 11 | } 12 | 13 | impl Default for StaticCommandExecutor { 14 | fn default() -> Self { 15 | Self::new() 16 | } 17 | } 18 | 19 | impl<'ctx> CommandModule> for StaticCommandExecutor { 20 | fn dispatch( 21 | &self, 22 | command: &str, 23 | context: CommandContext<'ctx>, 24 | ) -> Result<(), quartz_commands::Error> { 25 | (cmds::NativeCommandSet).dispatch(command, context) 26 | } 27 | 28 | fn get_suggestions(&self, command: &str, context: &CommandContext<'ctx>) -> Vec { 29 | (cmds::NativeCommandSet).get_suggestions(command, context) 30 | } 31 | } 32 | 33 | // NOTE: in order for the help command to work every command needs to have a Help<'cmd> argument that when executed outputs its help message 34 | // We have to wrap the commands in a module so we can disable clippy cause I can't find a way to do it any other way 35 | #[allow(clippy::redundant_pattern)] 36 | mod cmds { 37 | use crate::{command::CommandContext, ServerClock, DIAGNOSTICS, RUNNING}; 38 | use quartz_chat::{color::Color, Component, ComponentBuilder}; 39 | use quartz_commands::{self, module, CommandModule, Help}; 40 | use std::sync::atomic::Ordering; 41 | module! { 42 | pub mod native_command_set; 43 | type Context<'ctx> = CommandContext<'ctx>; 44 | 45 | command help 46 | where 47 | cmd: String 48 | help: Help<'cmd> 49 | { 50 | root executes |ctx| { 51 | ctx.sender.send_message(Component::colored( 52 | "-- Command List --".to_owned(), 53 | Color::Gold, 54 | )); 55 | 56 | let command_names = ctx.executor.get_suggestions("", &ctx); 57 | 58 | for command in command_names { 59 | ctx.sender.send_message(Component::colored( 60 | command.to_owned(), 61 | Color::Gray, 62 | )); 63 | } 64 | ctx.sender.send_message(Component::colored( 65 | "-- Use 'help [command]' to get more information --".to_owned(), 66 | Color::Gold, 67 | )); 68 | 69 | Ok(()) 70 | }; 71 | 72 | cmd executes |ctx| { 73 | ctx.executor.dispatch(&format!("{cmd} -h"), ctx) 74 | }; 75 | 76 | help executes |ctx| { 77 | ctx.sender.send_message(Component::text("Gives information on commands")); 78 | Ok(()) 79 | }; 80 | 81 | cmd suggests |ctx, arg| { 82 | ctx.executor.get_suggestions("", ctx) 83 | }; 84 | } 85 | 86 | command stop where 87 | help: Help<'cmd> { 88 | root executes |_ctx| { 89 | let _ = RUNNING.compare_exchange(true, false, Ordering::Acquire, Ordering::Relaxed); 90 | Ok(()) 91 | }; 92 | 93 | help executes |ctx| { 94 | ctx.sender.send_message(Component::text("Stops the server")); 95 | Ok(()) 96 | } 97 | } 98 | 99 | command tps where 100 | help: Help<'cmd> { 101 | root executes |ctx| { 102 | let mspt = DIAGNOSTICS.lock().mspt(); 103 | let tps = ServerClock::as_tps(mspt); 104 | let red: f64; 105 | let green: f64; 106 | 107 | // Shift from dark green to yellow 108 | if tps > 15.0 { 109 | green = 128.0 + 14.4 * (20.0 - tps); 110 | red = 40.0 * (20.0 - tps); 111 | } 112 | // Shift from yellow to light red 113 | else if tps > 10.0 { 114 | green = 200.0 - 40.0 * (15.0 - tps); 115 | red = 200.0 + 11.0 * (15.0 - tps); 116 | } 117 | // Shift from light red to dark red 118 | else if tps > 0.0 { 119 | green = 0.0; 120 | red = 255.0 - 15.5 * (10.0 - tps); 121 | } 122 | // If everything is working this should never run 123 | else { 124 | green = 128.0; 125 | red = 0.0; 126 | } 127 | 128 | ctx.sender.send_message( 129 | ComponentBuilder::new() 130 | .color(Color::Gold) 131 | .add_text("Server TPS: ") 132 | .custom_color(red as u8, green as u8, 0) 133 | .add_text(format!( 134 | "{:.2} ({}%), {:.3} mspt", 135 | tps, 136 | ((tps / ServerClock::max_tps()) * 100.0) as u32, 137 | mspt 138 | )) 139 | .build(), 140 | ); 141 | 142 | Ok(()) 143 | }; 144 | 145 | help executes |ctx| { 146 | ctx.sender.send_message(Component::text("Shows the server's TPS and MSPT")); 147 | Ok(()) 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /quartz/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | mod executor; 3 | 4 | pub use context::*; 5 | pub use executor::*; 6 | pub use quartz_commands::*; 7 | -------------------------------------------------------------------------------- /quartz/src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | use qdat::world::location::BlockPosition; 2 | 3 | pub mod player; 4 | 5 | 6 | pub struct Position { 7 | pub x: f64, 8 | pub y: f64, 9 | pub z: f64, 10 | } 11 | 12 | impl From for Position { 13 | fn from(coord: BlockPosition) -> Self { 14 | Self { 15 | x: coord.x as f64, 16 | y: coord.y as f64, 17 | z: coord.z as f64, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /quartz/src/entities/player.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::forget_non_drop)] 2 | use hecs::Bundle; 3 | use qdat::Gamemode; 4 | 5 | use crate::{ 6 | entities::Position, 7 | item::{Inventory, OptionalItemStack, EMPTY_ITEM_STACK}, 8 | network::AsyncWriteHandle, 9 | }; 10 | 11 | #[derive(Bundle)] 12 | pub struct Player { 13 | pub inventory: PlayerInventory, 14 | pub pos: Position, 15 | pub gamemode: Gamemode, 16 | pub write_handle: AsyncWriteHandle, 17 | pub state: PlayerState, 18 | } 19 | 20 | impl Player { 21 | pub fn new(gamemode: Gamemode, pos: Position, write_handle: AsyncWriteHandle) -> Self { 22 | Player { 23 | inventory: PlayerInventory::new(), 24 | pos, 25 | gamemode, 26 | write_handle, 27 | state: PlayerState::Spawning, 28 | } 29 | } 30 | } 31 | 32 | pub enum PlayerState { 33 | Spawning, 34 | Ready, 35 | Despawning, 36 | } 37 | 38 | #[derive(Clone)] 39 | pub struct PlayerInventory { 40 | current_slot: u8, 41 | inv: Inventory, 42 | offhand_slot: OptionalItemStack, 43 | } 44 | 45 | impl PlayerInventory { 46 | pub fn new() -> PlayerInventory { 47 | Self { 48 | current_slot: 36, 49 | inv: Inventory::new(46), 50 | offhand_slot: EMPTY_ITEM_STACK, 51 | } 52 | } 53 | 54 | pub fn set_curr_slot(&mut self, slot: u8) { 55 | self.current_slot = slot; 56 | } 57 | 58 | pub fn set_slot(&mut self, slot: usize, item: OptionalItemStack) -> OptionalItemStack { 59 | self.inv.insert(slot, item) 60 | } 61 | 62 | pub fn swap_hands(&mut self) { 63 | self.offhand_slot = self.inv.insert( 64 | self.current_slot as usize, 65 | std::mem::take(&mut self.offhand_slot), 66 | ); 67 | } 68 | 69 | pub fn current_slot(&self) -> OptionalItemStack { 70 | self.inv.get(self.current_slot as usize) 71 | } 72 | 73 | pub fn swap_slots(&mut self, a: usize, b: usize) { 74 | self.inv.swap(a, b); 75 | } 76 | } 77 | 78 | impl Default for PlayerInventory { 79 | fn default() -> Self { 80 | Self::new() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /quartz/src/item/init.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use once_cell::sync::OnceCell; 3 | use qdat::{ 4 | item::{Item, ItemInfo}, 5 | UlnStr, 6 | UnlocalizedName, 7 | }; 8 | use serde::Deserialize; 9 | use serde_json::from_str; 10 | use std::collections::BTreeMap; 11 | 12 | use crate::assets; 13 | 14 | static ITEM_LIST: OnceCell> = OnceCell::new(); 15 | 16 | /// Gets the whole item list 17 | #[inline(always)] 18 | pub fn get_item_list() -> &'static BTreeMap { 19 | ITEM_LIST.get().expect("Item list not initialized.") 20 | } 21 | 22 | /// Gets an item instance from a unlocalized name 23 | #[inline] 24 | pub fn get_item(item_name: &UlnStr) -> Option<&'static Item> { 25 | get_item_list().get(item_name) 26 | } 27 | 28 | /// Initializes the item list 29 | pub fn init_items() { 30 | info!("Loading item data"); 31 | 32 | // Load in assets/items.json generated from data-generator 33 | let raw_list = from_str::>(assets::ITEM_INFO) 34 | .expect("items.json is corrupt"); 35 | 36 | let mut item_list: BTreeMap = BTreeMap::new(); 37 | 38 | for (i, (name, raw_data)) in raw_list.into_iter().enumerate() { 39 | let uln = UnlocalizedName::from_str(name).expect("Invalid item name in items.json"); 40 | 41 | // This should never happen if the data integrity is not compromised 42 | assert_ne!( 43 | 0, raw_data.stack_size, 44 | "Item has max stack size of 0, {name}" 45 | ); 46 | 47 | // NOTE: this is disabled because I don't feel like trying to make the id an &'static str 48 | item_list.insert(uln.clone(), Item { 49 | id: name, 50 | num_id: i as u16, 51 | stack_size: raw_data.stack_size, 52 | rarity: raw_data.rarity, 53 | item_info: raw_data.info, 54 | }); 55 | } 56 | 57 | if ITEM_LIST.set(item_list).is_err() { 58 | panic!("ITEM_LIST already initialized.") 59 | } 60 | } 61 | 62 | // How the item info is stored in the json 63 | #[derive(Deserialize)] 64 | struct RawItemData { 65 | pub stack_size: u8, 66 | pub rarity: u8, 67 | pub info: Option, 68 | } 69 | -------------------------------------------------------------------------------- /quartz/src/item/inventory.rs: -------------------------------------------------------------------------------- 1 | use crate::item::{ItemStack, OptionalItemStack}; 2 | use quartz_nbt::{NbtCompound, NbtList}; 3 | 4 | /// Represents a basic inventory 5 | #[derive(Clone)] 6 | pub struct Inventory { 7 | /// The size of the inventory 8 | pub size: usize, 9 | /// The items in the inventory 10 | items: Box<[OptionalItemStack]>, 11 | } 12 | 13 | impl Inventory { 14 | /// Creates a new inventory with a specified size 15 | pub fn new(size: usize) -> Self { 16 | Inventory { 17 | size, 18 | items: vec![OptionalItemStack::new(None); size].into_boxed_slice(), 19 | } 20 | } 21 | 22 | // Assume index is always within bounds as items has a static size and all calls should be prefixed with a can_insert if slot number is not hard coded 23 | // Return the previous stack 24 | /// Inserts an ItemStack into the given slot 25 | /// # Panics 26 | /// Panics if the index is out of range for the inventory, all calls should be prefixed with a can_insert call 27 | pub fn insert(&mut self, index: usize, item: OptionalItemStack) -> OptionalItemStack { 28 | let current_item = self.items[index].clone(); 29 | self.items[index] = item; 30 | current_item 31 | } 32 | 33 | /// Increments the amount in a slot 34 | /// # Panics 35 | /// Panics if the index is out of range for the inventory, all calls should be prefixed with a can_insert call 36 | pub fn increment(&mut self, index: usize) { 37 | self.items[index].item().unwrap().count += 1; 38 | } 39 | 40 | /// Gets a clone of the OptionalItemStack in a slot 41 | /// # Panics 42 | /// Panics if the index is out of range for the inventory, all calls should be prefixed with a can_insert call 43 | pub fn get(&self, index: usize) -> OptionalItemStack { 44 | self.items.get(index).unwrap().clone() 45 | } 46 | 47 | /// Tests if a slot can be inserted into 48 | pub const fn can_insert(&self, index: usize) -> bool { 49 | self.size > index && index > 0 50 | } 51 | 52 | /// Swaps the items in indecies `a` and `b` 53 | pub fn swap(&mut self, a: usize, b: usize) { 54 | self.items.swap(a, b); 55 | } 56 | 57 | /// Creates a new Inventory from a NbtCompound 58 | /// 59 | /// # NBT Format 60 | /// ``` 61 | /// {Items: [{ 62 | /// Slot: Byte, 63 | /// id: String, 64 | /// Count: Byte 65 | /// tag: Compound, 66 | /// }]} 67 | /// ``` 68 | pub fn from_tag(&mut self, nbt: &NbtCompound) { 69 | let list = nbt.get::<_, &NbtList>("Items").unwrap(); 70 | 71 | for i in 0 .. list.len() { 72 | // List has to have a element at every index because even slots without items need to have an empty stack 73 | let compound = list.get::<&NbtCompound>(i).unwrap(); 74 | let slot = compound.get::<_, i32>("Slot").unwrap_or(0) as usize; 75 | 76 | if slot < self.size { 77 | self.items[slot] = 78 | OptionalItemStack::new(Some(ItemStack::from_nbt(compound.clone()))); 79 | } 80 | } 81 | } 82 | 83 | /// Writes the Inventory to a NbtCompound 84 | /// 85 | /// # NBT Format 86 | /// ``` 87 | /// {Items: [{ 88 | /// Slot: Byte, 89 | /// id: String, 90 | /// Count: Byte 91 | /// tag: Compound, 92 | /// }]} 93 | /// ``` 94 | pub fn write_tag(&self, tag: &mut NbtCompound) { 95 | let mut list = NbtList::new(); 96 | 97 | for i in 0 .. self.size { 98 | let mut slot_tag = NbtCompound::new(); 99 | 100 | slot_tag.insert("Slot".to_owned(), i as i8); 101 | // Every index must have a stack, even if it is empty 102 | let item = self.items.get(i).unwrap(); 103 | item.write_nbt(&mut slot_tag); 104 | list.push(slot_tag); 105 | } 106 | 107 | tag.insert("Items".to_owned(), list); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /quartz/src/item/item_stack.rs: -------------------------------------------------------------------------------- 1 | use qdat::{item::Item, UlnStr}; 2 | use quartz_nbt::NbtCompound; 3 | 4 | use super::get_item; 5 | 6 | /// Represents a stack of items 7 | #[derive(Clone)] 8 | pub struct ItemStack { 9 | /// The item in the stack 10 | pub item: &'static Item, 11 | /// The size of the stack 12 | pub count: u8, 13 | /// The damage of the itemstack 14 | pub damage: u32, 15 | /// Extra nbt info about the stack 16 | pub nbt: NbtCompound, 17 | } 18 | 19 | impl ItemStack { 20 | /// Represents a empty item stack 21 | pub fn empty() -> Self { 22 | ItemStack { 23 | item: get_item(UlnStr::minecraft("air")).expect("Item list not initialized"), 24 | count: 0, 25 | damage: 0, 26 | nbt: NbtCompound::new(), 27 | } 28 | } 29 | 30 | /// Creates a new itemstack 31 | pub fn new(item: &'static Item) -> Self { 32 | ItemStack { 33 | item, 34 | count: 1, 35 | damage: if item.item_info.is_some() { 36 | item.item_info.as_ref().unwrap().max_durability() 37 | } else { 38 | 0 39 | }, 40 | nbt: NbtCompound::new(), 41 | } 42 | } 43 | 44 | /// Write the stack to nbt tag 45 | /// 46 | /// # NBT Format 47 | /// ``` 48 | /// { 49 | /// id: String, 50 | /// Count: byte 51 | /// tag: Compound, 52 | /// } 53 | /// ``` 54 | /// For `tag` format check https://minecraft.gamepedia.com/Player.dat_format#Item_structure 55 | pub fn write_nbt(&self, tag: &mut NbtCompound) { 56 | tag.insert("Count".to_owned(), self.count as i8); 57 | tag.insert("Damage".to_owned(), self.damage as i8); 58 | tag.insert("id".to_owned(), self.item.id.to_string()); 59 | tag.insert("tag".to_owned(), self.nbt.clone()); 60 | } 61 | 62 | /// Create an ItemStack from a nbt tag 63 | /// 64 | /// # NBT Format 65 | /// ``` 66 | /// { 67 | /// id: String, 68 | /// Count: byte 69 | /// tag: Compound, 70 | /// } 71 | /// ``` 72 | /// For `tag` format check https://minecraft.gamepedia.com/Player.dat_format#Item_structure 73 | pub fn from_nbt(tag: NbtCompound) -> Self { 74 | let tag = match tag.contains_key("tag") { 75 | true => match tag.get::<_, &NbtCompound>("tag") { 76 | Ok(tag) => tag.clone(), 77 | _ => NbtCompound::new(), 78 | }, 79 | _ => NbtCompound::new(), 80 | }; 81 | 82 | let damage = if tag.contains_key("Damage") { 83 | tag.get::<_, i32>("Damage").unwrap_or(0) 84 | } else { 85 | 0 86 | } as u32; 87 | 88 | ItemStack { 89 | item: get_item(UlnStr::from_str(tag.get("id").unwrap_or("minecraft:air")).unwrap()) 90 | .unwrap(), 91 | count: tag.get::<_, i32>("Count").unwrap_or(0) as u8, 92 | damage, 93 | nbt: tag, 94 | } 95 | } 96 | 97 | /// Returns if the current stack is empty or not 98 | /// Any empty stack is any stack that has a count of 0 or is air 99 | pub fn is_empty(&self) -> bool { 100 | self.count == 0 || self.item.id == UlnStr::minecraft("air") 101 | } 102 | } 103 | 104 | /// An ItemStack wrapped in an Option to save memory when it is empty 105 | #[repr(transparent)] 106 | #[derive(Default, Clone)] 107 | pub struct OptionalItemStack(Option>); 108 | pub const EMPTY_ITEM_STACK: OptionalItemStack = OptionalItemStack(None); 109 | 110 | impl OptionalItemStack { 111 | /// Creates a new OptionalItemStack 112 | pub fn new(stack: Option) -> Self { 113 | if stack.is_none() { 114 | return OptionalItemStack(None); 115 | } 116 | OptionalItemStack(Some(Box::new(stack.unwrap()))) 117 | } 118 | 119 | /// Is the OptionalItemStack empty / existant 120 | pub fn is_empty(&self) -> bool { 121 | self.0.is_none() || self.0.clone().unwrap().is_empty() 122 | } 123 | 124 | /// Writes the stack to an nbt tag 125 | // TODO: make sure this works when reading / writing the world files 126 | pub fn write_nbt(&self, tag: &mut NbtCompound) { 127 | if self.0.is_some() { 128 | self.0.clone().unwrap().write_nbt(tag) 129 | } 130 | } 131 | 132 | /// Creates a new OptionalItemStack from a nbt tag 133 | pub fn from_nbt(tag: NbtCompound) -> Self { 134 | OptionalItemStack(Some(Box::new(ItemStack::from_nbt(tag)))) 135 | } 136 | 137 | /// Gets the inner data of the OptionalItemStack 138 | pub fn item(&self) -> Option> { 139 | self.0.clone() 140 | } 141 | 142 | /// Takes the stack and replaces it with an empty stack 143 | pub fn take(&mut self) -> OptionalItemStack { 144 | std::mem::take(self) 145 | } 146 | } 147 | 148 | impl From for OptionalItemStack { 149 | fn from(i: ItemStack) -> Self { 150 | Self(Some(Box::new(i))) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /quartz/src/item/mod.rs: -------------------------------------------------------------------------------- 1 | mod inventory; 2 | mod item_stack; 3 | pub use inventory::*; 4 | pub use item_stack::*; 5 | mod init; 6 | pub use init::*; 7 | -------------------------------------------------------------------------------- /quartz/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | #![allow( 3 | // We allow this because if we have modules with the same name, we don't publicly expose the inner one 4 | clippy::module_inception, 5 | // explicit_auto_deref is currently over active due to a rustc bug 6 | clippy::explicit_auto_deref 7 | )] 8 | // TODO: enable when ready 9 | // #![warn(missing_docs)] 10 | #![warn(clippy::undocumented_unsafe_blocks)] 11 | // when we enable warn missing_docs we will disable this 12 | #![allow(clippy::missing_safety_doc)] 13 | 14 | //! This crate contains virtually all of the code, APIs, and other malarkey that makes quartz run. The server 15 | //! code is launched through the separate `quartz_launcher` crate. Plugins should use this crate as a library 16 | 17 | // Expose sub-crates 18 | pub use quartz_chat as chat; 19 | pub use quartz_nbt as nbt; 20 | pub use quartz_util as util; 21 | 22 | mod base; 23 | /// Contains all relevant code to blocks and their implementations. 24 | pub mod block; 25 | /// Defines a brigadier-like command system for rust. 26 | pub mod command; 27 | 28 | 29 | pub mod entities; 30 | /// Contains all relevant code to items and their implementations. 31 | pub mod item; 32 | 33 | /// Contains packet definitions and connection handlers. 34 | pub mod network; 35 | pub mod scheduler; 36 | /// Contains world and chunk implementations, including chunk I/O utilities. 37 | pub mod world; 38 | 39 | 40 | pub use base::*; 41 | -------------------------------------------------------------------------------- /quartz/src/main.rs: -------------------------------------------------------------------------------- 1 | use linefeed::Interface; 2 | use log::error; 3 | use quartz::{config::load_config, run, util::logging}; 4 | use std::{error::Error, path::Path, sync::Arc}; 5 | 6 | fn main() -> Result<(), Box> { 7 | let console_interface = Arc::new(Interface::new("quartz-server")?); 8 | console_interface.set_prompt("> ")?; 9 | 10 | logging::init_logger( 11 | Some(|path| path.starts_with("quartz")), 12 | console_interface.clone(), 13 | )?; 14 | 15 | let config = match load_config(Path::new("./config.json")) { 16 | Ok(cfg) => cfg, 17 | Err(error) => { 18 | error!("Failed to load config: {}", error); 19 | return Ok(()); 20 | } 21 | }; 22 | 23 | run(config, console_interface); 24 | logging::cleanup(); 25 | 26 | // Move off of the command prompt 27 | println!(); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /quartz/src/network/mod.rs: -------------------------------------------------------------------------------- 1 | mod connection; 2 | mod handler; 3 | mod packet; 4 | 5 | pub use connection::*; 6 | pub use handler::*; 7 | pub use packet::*; 8 | pub use quartz_net::*; 9 | -------------------------------------------------------------------------------- /quartz/src/network/packet.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use crate::server::ClientId; 4 | 5 | use super::AsyncWriteHandle; 6 | use quartz_net::{ClientBoundPacket, PacketBuffer, ServerBoundPacket, WriteToPacket}; 7 | use uuid::Uuid; 8 | 9 | pub enum WrappedServerBoundPacket { 10 | External { 11 | sender: ClientId, 12 | packet: ServerBoundPacket, 13 | }, 14 | ClientConnected { 15 | id: ClientId, 16 | write_handle: AsyncWriteHandle, 17 | }, 18 | ClientDisconnected { 19 | id: ClientId, 20 | }, 21 | LoginSuccess { 22 | id: ClientId, 23 | uuid: Uuid, 24 | username: String, 25 | }, 26 | ConsoleCommand { 27 | command: String, 28 | }, 29 | ConsoleCompletion { 30 | command: String, 31 | response: Sender>, 32 | }, 33 | } 34 | 35 | impl WrappedServerBoundPacket { 36 | pub fn external(sender: ClientId, packet: ServerBoundPacket) -> Self { 37 | WrappedServerBoundPacket::External { sender, packet } 38 | } 39 | } 40 | 41 | /// A wraper for client-bound packets used internally for sending packets to the connection thread. 42 | pub enum WrappedClientBoundPacket { 43 | /// A single packet. 44 | Singleton(ClientBoundPacket), 45 | /// Multiple packets to be sent all at once. 46 | Multiple(Box<[Self]>), 47 | /// A raw byte-buffer. 48 | Buffer(PacketBuffer), 49 | /// A generic item which can we written to a packet buffer. 50 | Custom(Box), 51 | /// Enables compression synchronously on the client channel. 52 | EnableCompression { threshold: i32 }, 53 | /// Flushes the client channel. 54 | Flush, 55 | /// Specifies that the connection should be forcefully terminated. 56 | Disconnect, 57 | } 58 | 59 | impl From for WrappedClientBoundPacket { 60 | fn from(packet: ClientBoundPacket) -> Self { 61 | WrappedClientBoundPacket::Singleton(packet) 62 | } 63 | } 64 | 65 | impl From for WrappedClientBoundPacket { 66 | fn from(buffer: PacketBuffer) -> Self { 67 | WrappedClientBoundPacket::Buffer(buffer) 68 | } 69 | } 70 | 71 | impl From> for WrappedClientBoundPacket { 72 | fn from(packet: Box) -> Self { 73 | WrappedClientBoundPacket::Custom(packet) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /quartz/src/scheduler/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod task; 2 | -------------------------------------------------------------------------------- /quartz/src/scheduler/task.rs: -------------------------------------------------------------------------------- 1 | use crate::QuartzServer; 2 | 3 | pub trait Task { 4 | type Output; 5 | 6 | fn tick(&mut self, server: &mut QuartzServer) -> TaskState; 7 | 8 | fn complete(self, server: &mut QuartzServer) -> Self::Output; 9 | } 10 | 11 | pub enum TaskState { 12 | InProgress, 13 | Complete, 14 | Invalid, 15 | } 16 | 17 | pub struct Delayed { 18 | ticks_remaining: u32, 19 | func: F, 20 | } 21 | 22 | impl T> Delayed { 23 | pub fn new(delay: u32, func: F) -> Self { 24 | Delayed { 25 | ticks_remaining: delay, 26 | func, 27 | } 28 | } 29 | } 30 | 31 | impl T> Task for Delayed { 32 | type Output = T; 33 | 34 | fn tick(&mut self, _server: &mut QuartzServer) -> TaskState { 35 | if self.ticks_remaining > 0 { 36 | self.ticks_remaining -= 1; 37 | TaskState::InProgress 38 | } else { 39 | TaskState::Complete 40 | } 41 | } 42 | 43 | fn complete(self, server: &mut QuartzServer) -> Self::Output { 44 | (self.func)(server) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /quartz/src/world/chunk/error.rs: -------------------------------------------------------------------------------- 1 | use qdat::{ 2 | world::{lighting::LightingInitError, location::Coordinate}, 3 | UnlocalizedName, 4 | }; 5 | use quartz_nbt::{io::NbtIoError, NbtReprError, NbtStructureError}; 6 | use std::{ 7 | error::Error, 8 | fmt::{self, Display, Formatter}, 9 | io::Error as IoError, 10 | }; 11 | 12 | #[derive(Debug)] 13 | pub enum ChunkDecodeError { 14 | StdIo(IoError), 15 | NbtIo(NbtIoError), 16 | NbtRepr(NbtReprError), 17 | UnknownBlockState(UnlocalizedName), 18 | UnknownStateProperty(String), 19 | Lighting(LightingInitError), 20 | ChunkRegionDesync(Coordinate), 21 | UnknownCompression(u8), 22 | } 23 | 24 | impl Display for ChunkDecodeError { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 26 | match self { 27 | ChunkDecodeError::StdIo(error) => Display::fmt(error, f), 28 | ChunkDecodeError::NbtIo(error) => Display::fmt(error, f), 29 | ChunkDecodeError::NbtRepr(error) => Display::fmt(error, f), 30 | ChunkDecodeError::UnknownBlockState(state) => 31 | write!(f, "Unknown block state {state}"), 32 | ChunkDecodeError::UnknownStateProperty(msg) => Display::fmt(msg, f), 33 | ChunkDecodeError::Lighting(error) => Display::fmt(error, f), 34 | ChunkDecodeError::ChunkRegionDesync(coords) => 35 | write!(f, "Attempted to load chunk outside of region at {coords}"), 36 | ChunkDecodeError::UnknownCompression(id) => write!( 37 | f, 38 | "Encountered unknown compression scheme {id}, expected 1 or 2" 39 | ), 40 | } 41 | } 42 | } 43 | 44 | impl Error for ChunkDecodeError { 45 | fn source(&self) -> Option<&(dyn Error + 'static)> { 46 | match self { 47 | ChunkDecodeError::StdIo(error) => Some(error), 48 | ChunkDecodeError::NbtIo(error) => Some(error), 49 | ChunkDecodeError::Lighting(error) => Some(error), 50 | ChunkDecodeError::NbtRepr(error) => Some(error), 51 | _ => None, 52 | } 53 | } 54 | } 55 | 56 | impl From for ChunkDecodeError { 57 | fn from(x: IoError) -> Self { 58 | ChunkDecodeError::StdIo(x) 59 | } 60 | } 61 | 62 | impl From for ChunkDecodeError { 63 | fn from(x: NbtIoError) -> Self { 64 | ChunkDecodeError::NbtIo(x) 65 | } 66 | } 67 | 68 | impl From for ChunkDecodeError { 69 | fn from(x: NbtReprError) -> Self { 70 | ChunkDecodeError::NbtRepr(x) 71 | } 72 | } 73 | 74 | impl From for ChunkDecodeError { 75 | fn from(x: NbtStructureError) -> Self { 76 | ChunkDecodeError::NbtRepr(x.into()) 77 | } 78 | } 79 | 80 | impl From for ChunkDecodeError { 81 | fn from(x: LightingInitError) -> Self { 82 | ChunkDecodeError::Lighting(x) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /quartz/src/world/chunk/gen/density_function.rs: -------------------------------------------------------------------------------- 1 | pub trait DensityFunction { 2 | fn compute(context: impl FunctionContext) -> f64; 3 | fn fill_array(array: &mut [f64], context_provider: impl ContextProvider); 4 | fn map_all(visitor: impl FnMut(I) -> O); 5 | fn min_value() -> f64; 6 | fn max_value() -> f64; 7 | // fn clamp() 8 | } 9 | 10 | pub trait FunctionContext { 11 | fn block_x() -> i32; 12 | fn block_y() -> i32; 13 | fn block_z() -> i32; 14 | 15 | // fn get_blender() -> Blender {}; 16 | } 17 | 18 | pub trait ContextProvider { 19 | fn for_index(arr_index: i32) -> C; 20 | fn fill_all_directly(array: &mut [f64], density: impl DensityFunction); 21 | } 22 | -------------------------------------------------------------------------------- /quartz/src/world/chunk/gen/mod.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | 3 | use qdat::world::location::{BlockPosition, Coordinate}; 4 | use quartz_nbt::NbtCompound; 5 | 6 | use crate::world::chunk::{Section, SectionStore, MAX_SECTION_COUNT}; 7 | 8 | use noise::{NoiseFn, Perlin}; 9 | 10 | pub enum ChunkStatus { 11 | Empty, 12 | Shaping, 13 | Biomes, 14 | Features, 15 | Carving, 16 | } 17 | 18 | pub trait ChunkGenerator { 19 | fn start_chunk(coords: Coordinate) -> Self; 20 | fn shape_chunk(&mut self); 21 | fn finish_chunk(self) -> super::Chunk; 22 | } 23 | 24 | pub struct SimpleChunkGenerator { 25 | chunk: ProtoChunk, 26 | noise: Perlin, 27 | } 28 | 29 | impl ChunkGenerator for SimpleChunkGenerator { 30 | fn start_chunk(coords: Coordinate) -> Self { 31 | let chunk = ProtoChunk::new(coords.as_chunk()); 32 | let noise = Perlin::new(); 33 | 34 | Self { chunk, noise } 35 | } 36 | 37 | fn shape_chunk(&mut self) { 38 | let chunk = &mut self.chunk; 39 | for x in 0 .. 16 { 40 | for z in 0 .. 16 { 41 | let y = (self.noise.get([ 42 | (chunk.pos.as_block().x() + x) as f64 / 100.0, 43 | (chunk.pos.as_block().z() + z) as f64 / 100.0, 44 | ]) * 40.0 45 | + 60.0) as i16; 46 | // let y = 70; 47 | for i in 0 .. y { 48 | let curr_y = y - i; 49 | let section_index = curr_y >> 4; 50 | let block_index = 51 | chunk.section_index_absolute(BlockPosition { x, y: curr_y, z }); 52 | chunk 53 | .sections 54 | .get_mut(section_index as usize) 55 | .unwrap() 56 | .set_block_state_at( 57 | block_index, 58 | qdat::block::states::BlockStateData::Stone.id(), 59 | ); 60 | } 61 | } 62 | } 63 | chunk.state = ChunkState::Shaped; 64 | } 65 | 66 | fn finish_chunk(self) -> super::Chunk { 67 | self.chunk.into() 68 | } 69 | } 70 | 71 | pub enum ChunkState { 72 | Empty, 73 | Shaped, 74 | Done, 75 | } 76 | 77 | pub struct ProtoChunk { 78 | pub state: ChunkState, 79 | pub pos: Coordinate, 80 | pub sections: [Section; MAX_SECTION_COUNT], 81 | // TODO: figure out biomes 82 | pub biomes: Box<[i32]>, 83 | } 84 | 85 | impl ProtoChunk { 86 | pub fn new(pos: Coordinate) -> ProtoChunk { 87 | let mut sections: [MaybeUninit
; MAX_SECTION_COUNT] = 88 | // Safety: an array of maybe uninit does not need any initialization 89 | unsafe { MaybeUninit::uninit().assume_init() }; 90 | 91 | for (i, s) in sections.iter_mut().enumerate() { 92 | s.write(Section::empty(i as i8 - 1)); 93 | } 94 | 95 | // Safety: we initialized the sections in the loop above 96 | let sections = unsafe { std::mem::transmute::<_, [Section; MAX_SECTION_COUNT]>(sections) }; 97 | 98 | ProtoChunk { 99 | state: ChunkState::Empty, 100 | pos, 101 | sections, 102 | biomes: Box::new([0; 1024]), 103 | } 104 | } 105 | 106 | // x and z have to be in 0-16 107 | fn section_index_absolute(&self, pos: BlockPosition) -> usize { 108 | (pos.x + pos.z * 16 + (pos.y as i32 % 16) * 256) as usize 109 | } 110 | } 111 | 112 | #[allow(clippy::from_over_into)] 113 | impl Into for ProtoChunk { 114 | fn into(self) -> super::Chunk { 115 | let chunk_size = self.sections.iter().filter(|s| !s.is_empty()).count(); 116 | 117 | let mut section_store = SectionStore::new(chunk_size); 118 | 119 | for s in self.sections.into_iter().filter(|s| !s.is_empty()) { 120 | // assunme insertion cannot fail 121 | section_store.insert(s).unwrap(); 122 | } 123 | 124 | 125 | super::Chunk::new( 126 | self.pos.as_block().into(), 127 | section_store, 128 | NbtCompound::new(), 129 | self.biomes, 130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /quartz/src/world/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chunk { 2 | mod chunk; 3 | mod error; 4 | pub mod gen; 5 | mod palette; 6 | pub mod provider; 7 | mod section; 8 | mod states; 9 | 10 | pub use chunk::*; 11 | pub use error::*; 12 | pub use palette::*; 13 | pub use provider::ChunkProvider; 14 | pub use section::*; 15 | pub use states::*; 16 | } 17 | 18 | pub mod world; 19 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" -------------------------------------------------------------------------------- /util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quartz_util" 3 | version = "0.1.0" 4 | authors = ["Cassy343", "maddymakesgames"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | chrono = "0.4" 10 | flate2 = "1.0" 11 | linefeed = "0.6" 12 | log = "0.4" 13 | log4rs = "1.1" 14 | serde = "1.0.126" 15 | 16 | [target.'cfg(unix)'.dependencies] 17 | termion = "2.0" -------------------------------------------------------------------------------- /util/src/hash.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | hash::{BuildHasher, Hasher}, 3 | mem, 4 | }; 5 | 6 | /// The purpose of this hasher is to be extremely fast for hashing primitive integer types containing 7 | /// less than or equal to 64 bits. This hasher should not be used in any other context. 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 9 | pub struct NumHasher; 10 | 11 | impl BuildHasher for NumHasher { 12 | type Hasher = NumHashIsomorphism; 13 | 14 | fn build_hasher(&self) -> Self::Hasher { 15 | NumHashIsomorphism { state: 0 } 16 | } 17 | } 18 | 19 | /// This hasher is the fastest possible implementation of a hasher for the primitive integer types 20 | /// containing no more than 64 bits. It treats values of those types as their own hash, hence the 21 | /// hash isomorphism. For all types larger than (or potentially larger than) `u64`, the byte 22 | /// representation of those types is xor-ed into the internal state in 64-bit chunks. 23 | pub struct NumHashIsomorphism { 24 | state: u64, 25 | } 26 | 27 | impl Hasher for NumHashIsomorphism { 28 | fn finish(&self) -> u64 { 29 | self.state 30 | } 31 | 32 | fn write(&mut self, bytes: &[u8]) { 33 | let mut buf = [0u8; 8]; 34 | for window in bytes.windows(8) { 35 | (buf[.. window.len()]).copy_from_slice(window); 36 | (window.len() .. 8).for_each(|j| buf[j] = 0); 37 | self.state ^= unsafe { mem::transmute_copy::<_, u64>(&buf) }; 38 | } 39 | } 40 | 41 | fn write_i8(&mut self, i: i8) { 42 | self.state = i as u64; 43 | } 44 | 45 | fn write_u8(&mut self, i: u8) { 46 | self.state = i as u64; 47 | } 48 | 49 | fn write_i16(&mut self, i: i16) { 50 | self.state = i as u64; 51 | } 52 | 53 | fn write_u16(&mut self, i: u16) { 54 | self.state = i as u64; 55 | } 56 | 57 | fn write_i32(&mut self, i: i32) { 58 | self.state = i as u64; 59 | } 60 | 61 | fn write_u32(&mut self, i: u32) { 62 | self.state = i as u64; 63 | } 64 | 65 | fn write_i64(&mut self, i: i64) { 66 | self.state = i as u64; 67 | } 68 | 69 | fn write_u64(&mut self, i: u64) { 70 | self.state = i; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /util/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![feature(coerce_unsized, unsize, set_ptr_value, test, ptr_metadata)] 3 | 4 | //! Provides generic utilities for quartz, the minecraft server implementation in rust. 5 | 6 | /// Defines a fast hasher for numeric types. 7 | pub mod hash; 8 | /// Configures log4rs to copy minecraft's logging style. 9 | pub mod logging; 10 | /// Contains optimized maps where hash maps are insufficient. 11 | pub mod map; 12 | /// Contains fast math utilities. 13 | pub mod math; 14 | /// Contains an implementation of a single-access box allowing for interior mutability. 15 | pub mod single_access; 16 | /// An implementation of Minecraft's unlocalized name. 17 | // pub mod uln; 18 | /// Allows for downcasting of trait types. 19 | pub mod variant; 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | #[cfg(not(debug_assertions))] 24 | extern crate test; 25 | #[cfg(not(debug_assertions))] 26 | use test::{black_box, Bencher}; 27 | 28 | use super::*; 29 | use map::{IdList, Identify}; 30 | 31 | struct Identifiable { 32 | id: usize, 33 | value: i32, 34 | } 35 | 36 | impl Identifiable { 37 | fn new(value: i32) -> Self { 38 | Identifiable { id: 0, value } 39 | } 40 | } 41 | 42 | impl Identify for Identifiable { 43 | fn set_id(&mut self, id: usize) { 44 | self.id = id; 45 | } 46 | 47 | fn id(&self) -> usize { 48 | self.id 49 | } 50 | } 51 | 52 | #[test] 53 | fn id_list_test() { 54 | let value1 = Identifiable::new(1); 55 | let value2 = Identifiable::new(2); 56 | let value3 = Identifiable::new(3); 57 | 58 | let mut id_list: IdList = IdList::with_capacity(5); 59 | assert_eq!(id_list.insert(value1), 0, "Incorrect ID assigned."); 60 | assert_eq!( 61 | id_list.insert(value2), 62 | 1, 63 | "Incorrect ID assigned (value 2)." 64 | ); 65 | assert_eq!( 66 | id_list.insert(value3), 67 | 2, 68 | "Incorrect ID assigned (value 3)." 69 | ); 70 | assert!(id_list.get(1).is_some(), "ID lookup failed."); 71 | let value2 = id_list.remove(1); 72 | assert!( 73 | value2.is_some(), 74 | "Element removal failed: no value returned." 75 | ); 76 | assert!( 77 | id_list.get(1).is_none(), 78 | "Element removal failed: value remained in list." 79 | ); 80 | 81 | let value4 = Identifiable::new(4); 82 | 83 | assert_eq!( 84 | id_list.insert(value2.unwrap()), 85 | 1, 86 | "Incorrect ID assigned after element removal." 87 | ); 88 | assert_eq!( 89 | id_list.insert(value4), 90 | 3, 91 | "Incorrect ID assigned after element removal and readdition." 92 | ); 93 | 94 | id_list.remove(0); 95 | id_list.remove(2); 96 | 97 | let mut count: usize = 0; 98 | 99 | for element in id_list.iter() { 100 | assert_eq!( 101 | count, 102 | (element.id() - 1) / 2, 103 | "Element ID mismatch in Iter." 104 | ); 105 | count += 1; 106 | } 107 | 108 | assert_eq!( 109 | count, 2, 110 | "IdList iterator did not cover the correct number of elements." 111 | ); 112 | 113 | count = 0; 114 | for element in id_list.iter_mut() { 115 | element.value /= 4; 116 | assert_eq!( 117 | element.value as usize, count, 118 | "Element order incorrect in IterMut." 119 | ); 120 | count += 1; 121 | } 122 | 123 | assert_eq!( 124 | count, 2, 125 | "IdList iterator mut did not cover the correct number of elements." 126 | ); 127 | } 128 | 129 | #[bench] 130 | #[cfg(not(debug_assertions))] 131 | fn refcell(bencher: &mut Bencher) { 132 | use std::cell::RefCell; 133 | 134 | let cell1 = RefCell::new(0_i32); 135 | let cell2 = RefCell::new(0_i32); 136 | 137 | bencher.iter(move || { 138 | for _ in 0 .. 1000 { 139 | let mut ref1 = cell1.borrow_mut(); 140 | *ref1 += 1; 141 | assert!(cell1.try_borrow_mut().is_err()); 142 | *cell2.borrow_mut() -= *ref1; 143 | *ref1 += *cell2.borrow(); 144 | } 145 | }); 146 | } 147 | 148 | #[bench] 149 | #[cfg(not(debug_assertions))] 150 | fn single_accessor(bencher: &mut Bencher) { 151 | use single_access::SingleAccessor; 152 | 153 | let sa1 = SingleAccessor::new(0_i32); 154 | let sa2 = SingleAccessor::new(0_i32); 155 | 156 | bencher.iter(move || { 157 | for _ in 0 .. 1000 { 158 | let mut locmov1 = sa1.take().unwrap(); 159 | *locmov1 += 1; 160 | assert!(sa1.take().is_none()); 161 | *sa2.take().unwrap() -= *locmov1; 162 | *locmov1 += *sa2.take().unwrap(); 163 | } 164 | }); 165 | } 166 | 167 | #[bench] 168 | #[cfg(not(debug_assertions))] 169 | fn fast_inv_sqrt64(bencher: &mut Bencher) { 170 | use math::fast_inv_sqrt64; 171 | 172 | let mut x = 0.01f64; 173 | 174 | bencher.iter(move || { 175 | for _ in 0 .. 10000 { 176 | black_box(x * fast_inv_sqrt64(x)); 177 | x += 0.01; 178 | } 179 | }); 180 | } 181 | 182 | #[bench] 183 | #[cfg(not(debug_assertions))] 184 | fn std_inv_sqrt64(bencher: &mut Bencher) { 185 | let mut x = 0.01f64; 186 | 187 | bencher.iter(move || { 188 | for _ in 0 .. 10000 { 189 | black_box(x / x.sqrt()); 190 | x += 0.01; 191 | } 192 | }); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /util/src/map.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem, 3 | ops::{Index, IndexMut}, 4 | }; 5 | 6 | /// Represents an object which has a usize as a modifiable ID. 7 | pub trait Identify { 8 | /// Updates the ID of this object to the given new ID. 9 | fn set_id(&mut self, id: usize); 10 | 11 | /// Returns the ID of this object. 12 | fn id(&self) -> usize; 13 | } 14 | 15 | /// A map from IDs (usizes) to an object of a given type. Internally, this operates on vectors 16 | /// and indexing so it is more efficient than a hash map. 17 | pub struct IdList { 18 | inner: Vec>, 19 | free_ids: Vec, 20 | } 21 | 22 | impl IdList { 23 | /// Returns a new, empty ID list with an empty internal vector. 24 | pub fn new() -> Self { 25 | IdList { 26 | inner: Vec::new(), 27 | free_ids: Vec::new(), 28 | } 29 | } 30 | 31 | /// Returns a new ID list with an internal vector with the given initial capacity. 32 | pub fn with_capacity(capacity: usize) -> Self { 33 | IdList { 34 | inner: Vec::with_capacity(capacity), 35 | free_ids: Vec::new(), 36 | } 37 | } 38 | 39 | /// Returns an iterator over shared references to the values in this ID list. 40 | pub fn iter(&self) -> impl Iterator { 41 | self.inner.iter().flatten() 42 | } 43 | 44 | /// Returns an iterator over mutable references to the values in this ID list. 45 | pub fn iter_mut(&mut self) -> impl Iterator { 46 | self.inner.iter_mut().flatten() 47 | } 48 | 49 | /// Adds the given item to this list, setting its ID to the next open ID in this list and returning that ID. 50 | pub fn insert(&mut self, mut item: T) -> usize { 51 | match self.free_ids.pop() { 52 | Some(id) => { 53 | item.set_id(id); 54 | self.inner[id] = Some(item); 55 | id 56 | } 57 | None => { 58 | let id = self.inner.len(); 59 | item.set_id(id); 60 | self.inner.push(Some(item)); 61 | id 62 | } 63 | } 64 | } 65 | 66 | /// Returns a shared reference to the element with the given ID, or `None` if no element has the given ID. 67 | pub fn get(&self, id: usize) -> Option<&T> { 68 | self.inner.get(id)?.as_ref() 69 | } 70 | 71 | /// Returns a mutable reference to the element with the given ID, or `None` if no element has the given ID. 72 | pub fn get_mut(&mut self, id: usize) -> Option<&mut T> { 73 | self.inner.get_mut(id)?.as_mut() 74 | } 75 | 76 | /// Removes the item with the given ID returning that item if it exists, or None if it does not. 77 | pub fn remove(&mut self, id: usize) -> Option { 78 | if id >= self.inner.len() { 79 | return None; 80 | } 81 | 82 | self.free_ids.push(id); 83 | // Swaps out the value and returns the old value 84 | mem::replace(&mut self.inner[id], None) 85 | } 86 | } 87 | 88 | impl Index for IdList { 89 | type Output = T; 90 | 91 | fn index(&self, index: usize) -> &Self::Output { 92 | self.inner[index].as_ref().unwrap() 93 | } 94 | } 95 | 96 | impl IndexMut for IdList { 97 | fn index_mut(&mut self, index: usize) -> &mut Self::Output { 98 | self.inner[index].as_mut().unwrap() 99 | } 100 | } 101 | 102 | impl Default for IdList { 103 | fn default() -> Self { 104 | Self::new() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /util/src/math.rs: -------------------------------------------------------------------------------- 1 | const LOG2_TABLE_64: [u64; 64] = [ 2 | 63, 0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3, 61, 51, 37, 40, 49, 18, 28, 20, 55, 3 | 30, 34, 11, 43, 14, 22, 4, 62, 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21, 56, 4 | 45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5, 5 | ]; 6 | 7 | /// Computes the base-2 logarithm of a 64-bit value using a DeBruijn-like algorithm, flooring the result. 8 | /// Note that an input of zero will result in an output of `63` rather than an error. 9 | /// 10 | /// # Examples 11 | /// 12 | /// ``` 13 | /// # use quartz_util::math::fast_log2_64; 14 | /// for i in 0..64 { 15 | /// assert_eq!(fast_log2_64(1 << i), i as u64); 16 | /// } 17 | /// 18 | /// assert_eq!(fast_log2_64(15), 3); 19 | /// assert_eq!(fast_log2_64(17), 4); 20 | /// assert_eq!(fast_log2_64(651854213), 29); // Exact: 29.2799741 21 | /// assert_eq!(fast_log2_64(0), 63); 22 | /// ``` 23 | #[inline] 24 | pub const fn fast_log2_64(mut value: u64) -> u64 { 25 | value |= value >> 1; 26 | value |= value >> 2; 27 | value |= value >> 4; 28 | value |= value >> 8; 29 | value |= value >> 16; 30 | value |= value >> 32; 31 | 32 | LOG2_TABLE_64[((value - (value >> 1)) 33 | .overflowing_mul(0x07EDD5E59A4E28C2u64) 34 | .0 35 | >> 58) as usize] 36 | } 37 | 38 | /// Computes the base-2 logarithm of a 64-bit value using a DeBruijn-like algorithm, ceiling the result. 39 | /// Note that an input of zero will result in an output of `63` rather than an error. 40 | /// 41 | /// # Examples 42 | /// 43 | /// ``` 44 | /// # use quartz_util::math::fast_ceil_log2_64; 45 | /// for i in 0..64 { 46 | /// assert_eq!(fast_ceil_log2_64(1 << i), i as u64); 47 | /// } 48 | /// 49 | /// assert_eq!(fast_ceil_log2_64(15), 4); 50 | /// assert_eq!(fast_ceil_log2_64(17), 5); 51 | /// assert_eq!(fast_ceil_log2_64(651854213), 30); // Exact: 29.2799741 52 | /// assert_eq!(fast_ceil_log2_64(0), 63); 53 | /// ``` 54 | #[inline] 55 | pub const fn fast_ceil_log2_64(value: u64) -> u64 { 56 | fast_log2_64(value.overflowing_shl(1).0.overflowing_sub(1).0) 57 | } 58 | 59 | /// Computes the inverse square root of the given floating point number. The approximation produced by 60 | /// this algorithm is fairly rough, but generally speaking the relative error is less than plus or minus 61 | /// one-tenth of one percent. 62 | #[inline] 63 | pub fn fast_inv_sqrt64(mut value: f64) -> f64 { 64 | let i: u64 = 0x5FE6EB50C7B537A9 - (value.to_bits() >> 1); 65 | let x: f64 = value * 0.5; 66 | 67 | value = f64::from_bits(i); 68 | 69 | // This constant is a short-cut for another iteration of Newton's method. It is the optimal number to 70 | // reduce the mean squared relative error of this algorithm. 71 | 1.0009632777831923 * value * (1.5 - (x * value * value)) 72 | } 73 | -------------------------------------------------------------------------------- /util/src/single_access.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Cell, UnsafeCell}, 3 | marker::Unsize, 4 | ops::{CoerceUnsized, Deref, DerefMut}, 5 | ptr::{self, Thin}, 6 | }; 7 | 8 | /// Similar to [`SingleAccessorBox`], except that this structure allocates its value on the stack. 9 | /// 10 | /// [`SingleAccessorBox`]: crate::single_access::SingleAccessorBox 11 | pub struct SingleAccessor { 12 | value: UnsafeCell, 13 | taken: Cell, 14 | } 15 | 16 | unsafe impl Send for SingleAccessor {} 17 | 18 | impl SingleAccessor { 19 | /// Creates a new `SingleAccessor` with the given value. 20 | #[inline] 21 | pub const fn new(value: T) -> Self { 22 | SingleAccessor { 23 | value: UnsafeCell::new(value), 24 | taken: Cell::new(false), 25 | } 26 | } 27 | 28 | /// Attempts to take exclusive access to the data guarded by this structure. If this fails 29 | /// because access is already taken, then `None` is returned. 30 | #[inline] 31 | pub fn take(&self) -> Option> { 32 | if self.taken.replace(true) { 33 | return None; 34 | } 35 | 36 | Some(AccessGuard { 37 | value: self.value.get(), 38 | flag: &self.taken, 39 | }) 40 | } 41 | } 42 | 43 | /// A smart pointer created by the [`take`] method of [`SingleAccessor`] to enforce its access constraints. 44 | /// 45 | /// [`SingleAccessor`]: crate::single_access::SingleAccessorBox 46 | /// [`take`]: crate::single_access::SingleAccessor::take 47 | pub struct AccessGuard<'a, T> { 48 | value: *mut T, 49 | flag: &'a Cell, 50 | } 51 | 52 | impl<'a, T> Drop for AccessGuard<'a, T> { 53 | fn drop(&mut self) { 54 | self.flag.set(false); 55 | } 56 | } 57 | 58 | impl<'a, T> Deref for AccessGuard<'a, T> { 59 | type Target = T; 60 | 61 | fn deref(&self) -> &Self::Target { 62 | unsafe { &*self.value } 63 | } 64 | } 65 | 66 | impl<'a, T> DerefMut for AccessGuard<'a, T> { 67 | fn deref_mut(&mut self) -> &mut Self::Target { 68 | unsafe { &mut *self.value } 69 | } 70 | } 71 | 72 | /// Acts the same as a box in terms of memory management and provides an interface allowing for interior 73 | /// mutability. 74 | /// 75 | /// This type, unlike [`RefCell`], only allows one reference to its data at a time, and that reference is 76 | /// always mutable. In fact, when [`take`] is called on a single accessor box, its internal pointer is set to 77 | /// null until the smart pointer is dropped, at which point it is replaced. 78 | /// 79 | /// [`RefCell`]: std::cell::RefCell 80 | /// [`take`]: crate::single_access::SingleAccessorBox::take 81 | // TODO: Delete if unused 82 | #[repr(transparent)] 83 | pub struct SingleAccessorBox { 84 | value: Cell<*mut T>, 85 | } 86 | 87 | unsafe impl Send for SingleAccessorBox {} 88 | impl, U: ?Sized> CoerceUnsized> 89 | for SingleAccessorBox 90 | { 91 | } 92 | 93 | impl SingleAccessorBox { 94 | /// Allocates memory on the heap and places `x` into it. Allocation is skipped if `T` is a ZST. 95 | /// 96 | /// # Examples 97 | /// 98 | /// ``` 99 | /// # use quartz_util::single_access::SingleAccessorBox; 100 | /// let pi = SingleAccessorBox::new(3.141592653_f32); 101 | /// ``` 102 | #[inline] 103 | pub fn new(x: T) -> Self { 104 | SingleAccessorBox { 105 | value: Cell::new(Box::into_raw(Box::new(x))), 106 | } 107 | } 108 | } 109 | 110 | impl SingleAccessorBox { 111 | /// Attempts to take the value stored in this box, returning exclusive access to that value, or `None` 112 | /// if the value is already taken. 113 | /// 114 | /// # Examples 115 | /// 116 | /// ``` 117 | /// # use quartz_util::single_access::SingleAccessorBox; 118 | /// let x = SingleAccessorBox::new(5_i32); 119 | /// let mut guard = x.take(); 120 | /// 121 | /// // We can take it once 122 | /// assert!(guard.is_some()); 123 | /// // But not twice 124 | /// assert!(x.take().is_none()); 125 | /// 126 | /// // Modify the value and release our access 127 | /// *guard.unwrap() += 5; 128 | /// 129 | /// assert_eq!(x.take().as_mut().map(|guard| **guard), Some(10_i32)); 130 | /// ``` 131 | #[inline] 132 | pub fn take(&self) -> Option> { 133 | BoxAccessGuard::new(&self.value) 134 | } 135 | } 136 | 137 | impl Drop for SingleAccessorBox { 138 | fn drop(&mut self) { 139 | unsafe { 140 | drop(Box::from_raw(self.value.get())); 141 | } 142 | } 143 | } 144 | 145 | /// A smart pointer created by the [`take`] method of [`SingleAccessorBox`] to enforce its access constraints. 146 | /// 147 | /// [`SingleAccessorBox`]: crate::single_access::SingleAccessorBox 148 | /// [`take`]: crate::single_access::SingleAccessorBox::take 149 | pub struct BoxAccessGuard<'a, T: ?Sized> { 150 | value: *mut T, 151 | source: &'a Cell<*mut T>, 152 | } 153 | 154 | impl<'a, T: ?Sized + Thin> BoxAccessGuard<'a, T> { 155 | /// Creates a new access-guard smart pointer with the given cell. 156 | /// 157 | /// If the pointer in the cell is null, then `None` is returned, otherwise the data part of the pointer 158 | /// is set to null and the reference is constructed and returned. 159 | #[inline] 160 | fn new(source: &'a Cell<*mut T>) -> Option { 161 | let value = source.get(); 162 | 163 | // Ensure the value hasn't already been taken 164 | if value.is_null() { 165 | return None; 166 | } 167 | 168 | // Set the data part of the pointer in the cell to null 169 | source.set(value.with_metadata_of(ptr::null_mut())); 170 | 171 | Some(BoxAccessGuard { value, source }) 172 | } 173 | } 174 | 175 | impl<'a, T: ?Sized> Drop for BoxAccessGuard<'a, T> { 176 | fn drop(&mut self) { 177 | self.source.set(self.value); 178 | } 179 | } 180 | 181 | impl<'a, T: ?Sized> Deref for BoxAccessGuard<'a, T> { 182 | type Target = T; 183 | 184 | fn deref(&self) -> &Self::Target { 185 | unsafe { &*self.value } 186 | } 187 | } 188 | 189 | impl<'a, T: ?Sized> DerefMut for BoxAccessGuard<'a, T> { 190 | fn deref_mut(&mut self) -> &mut Self::Target { 191 | unsafe { &mut *self.value } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /util/src/variant.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | 3 | /// Downcasts the given shared reference to the new type. If the type cannot be safely downcasted, then 4 | /// `None` is returned. 5 | /// 6 | /// # Examples 7 | /// 8 | /// ``` 9 | /// # use quartz_util::variant::downcast_ref; 10 | /// use std::any::Any; 11 | /// 12 | /// fn test(shared: &T) { 13 | /// assert_eq!(shared.to_string(), "10"); 14 | /// assert_eq!(*downcast_ref::<_, i32>(shared).unwrap(), 10); 15 | /// assert!(downcast_ref::<_, f32>(shared).is_none()); 16 | /// } 17 | /// 18 | /// let x: i32 = 10; 19 | /// test(&x); 20 | /// ``` 21 | #[inline] 22 | pub fn downcast_ref(x: &T) -> Option<&U> { 23 | if x.type_id() == TypeId::of::() { 24 | Some(unsafe { &*(x as *const T as *const U) }) 25 | } else { 26 | None 27 | } 28 | } 29 | 30 | /// Downcasts the given mutable reference to the new type. If the type cannot be safely downcasted, then 31 | /// `None` is returned. 32 | /// 33 | /// # Examples 34 | /// 35 | /// ``` 36 | /// # use quartz_util::variant::downcast_mut; 37 | /// use std::any::Any; 38 | /// 39 | /// fn test(mutable: &mut T) { 40 | /// assert_eq!(mutable.to_string(), "10"); 41 | /// *downcast_mut::<_, i32>(&mut *mutable).unwrap() += 5; 42 | /// assert_eq!(mutable.to_string(), "15"); 43 | /// 44 | /// assert!(downcast_mut::<_, f32>(mutable).is_none()); 45 | /// } 46 | /// 47 | /// let mut x: i32 = 10; 48 | /// test(&mut x); 49 | /// ``` 50 | #[inline] 51 | pub fn downcast_mut(x: &mut T) -> Option<&mut U> { 52 | if (*x).type_id() == TypeId::of::() { 53 | Some(unsafe { &mut *(x as *mut T as *mut U) }) 54 | } else { 55 | None 56 | } 57 | } 58 | --------------------------------------------------------------------------------