├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── mc173-server ├── Cargo.toml └── src │ ├── chunk.rs │ ├── command.rs │ ├── config.rs │ ├── entity.rs │ ├── main.rs │ ├── net.rs │ ├── offline.rs │ ├── player.rs │ ├── proto.rs │ ├── server.rs │ └── world.rs └── mc173 ├── Cargo.toml ├── examples └── sizeof.rs └── src ├── biome.rs ├── block ├── bed.rs ├── button.rs ├── dispenser.rs ├── door.rs ├── fluid.rs ├── ladder.rs ├── lever.rs ├── material.rs ├── mod.rs ├── piston.rs ├── pumpkin.rs ├── repeater.rs ├── sapling.rs ├── sign.rs ├── stair.rs ├── torch.rs └── trapdoor.rs ├── block_entity ├── chest.rs ├── dispenser.rs ├── furnace.rs ├── jukebox.rs ├── mod.rs ├── note_block.rs ├── piston.rs ├── sign.rs └── spawner.rs ├── chunk.rs ├── entity ├── common.rs ├── mod.rs ├── tick.rs ├── tick_ai.rs ├── tick_attack.rs └── tick_state.rs ├── gen ├── cave.rs ├── dungeon.rs ├── liquid.rs ├── math.rs ├── mod.rs ├── nether.rs ├── noise.rs ├── overworld.rs ├── plant.rs ├── tree.rs └── vein.rs ├── geom.rs ├── item ├── attack.rs ├── craft.rs ├── inv.rs ├── mod.rs └── smelt.rs ├── java ├── io.rs ├── mod.rs └── rand.rs ├── lib.rs ├── serde ├── chunk.rs ├── chunk │ ├── block_entity_nbt.rs │ ├── chunk_nbt.rs │ ├── entity_kind_nbt.rs │ ├── entity_nbt.rs │ ├── item_stack_nbt.rs │ ├── painting_art_nbt.rs │ └── slot_nbt.rs ├── mod.rs ├── nbt.rs └── region.rs ├── storage.rs ├── util.rs └── world ├── bound.rs ├── break.rs ├── explode.rs ├── interact.rs ├── loot.rs ├── material.rs ├── mod.rs ├── notify.rs ├── path.rs ├── place.rs ├── power.rs ├── tick.rs └── use.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /test_world 3 | /tracing* 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["mc173", "mc173-server"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.2.0" 7 | edition = "2024" 8 | readme = "README.md" 9 | homepage = "https://github.com/mindstorm38/mc173" 10 | repository = "https://github.com/mindstorm38/mc173" 11 | license = "Apache-2.0" 12 | categories = ["games"] 13 | 14 | [workspace.dependencies] 15 | mc173 = { path = "mc173", version = "0.2.0" } 16 | 17 | # Network 18 | mio = { version = "1.0.2", features = ["os-poll", "net"] } 19 | 20 | # Sync 21 | crossbeam-channel = "0.5.14" 22 | once_cell = "1.20.3" 23 | 24 | # Data structures 25 | indexmap = "2.7.1" 26 | flate2 = "1.0.35" 27 | glam = "0.30.0" 28 | arcstr = "1.2.0" 29 | 30 | # Utilities 31 | thiserror = "2.0.11" 32 | byteorder = "1.5.0" 33 | 34 | # OS 35 | ctrlc = "3.4.1" 36 | 37 | # Tracing 38 | tracing = "0.1.41" 39 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecraft Beta 1.7.3 2 | A work-in-progress Minecraft beta 1.7.3 server made in Rust. This project split the server 3 | crate from data structure and logic crate, the latter is made to be used be developers. 4 | 5 | 1. [Logic crate](#logic-crate) 6 | 2. [Server crate](#server-crate) 7 | 3. [Contributing](#contributing) 8 | 4. [Roadmap](#roadmap) 9 | 10 | ## Logic crate 11 | 12 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/mc173?style=flat-square)](https://crates.io/crates/mc173) 13 | 14 | The logic crate [mc173](/mc173/) provides the core data structures such as worlds, chunks 15 | and entities, but also the behaviors for blocks, items and entities. It also provides a 16 | lot of utilities related to Minecraft. 17 | 18 | ## Server crate 19 | 20 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/mc173-server?style=flat-square)](https://crates.io/crates/mc173-server) 21 | 22 | The server crate [mc173-server](/mc173-server/) is an implementation of the *Notchian* 23 | server protocol, it is built on top of the logic crate and has threaded networking, it 24 | also defines protocol structures. 25 | 26 | ## Contributing 27 | 28 | If you're willing to contribute or fork this code, this sections presents the different 29 | tools that can be used to understand the *Notchian* implementation of Minecraft beta 30 | 1.7.3 and how to implement it into Rust. 31 | 32 | The most important tool is [RetroMCP], which is a modern remake of *MCP* (one of the most important software in Minecraft's modding history). It can be used to automatically 33 | decompile and deobfuscate the original archive of Minecraft beta 1.7.3. It can also be 34 | used to recompile and reobfuscate the game and then run it, which can be useful to add 35 | debugging code, but fortunately it's rare to get to that point. You can read the project's 36 | README, it is really well designed and its CLI is intuitive, you just have to choose the 37 | b1.7.3 version for both client and server. 38 | 39 | Choosing both client and server is really important as these two have slightly different 40 | source codes. For example, you have to choose the client or server source code depending 41 | on which side of the network protocol you want to understand. 42 | 43 | The next step is just to explore the source code, and try to understand how it works! This 44 | can be quite challenging sometimes due to the object oriented nature of it, so you should 45 | also use a IDE or text editor that support the Java langage and a few important features 46 | such as *goto definition* and *class hierarchy* (VSCode, IDEA, Eclipse...). 47 | 48 | Use the following [roadmap](#roadmap) either to understand how the completed components 49 | have been adapted from Java to Rust, or if you want to contribute and add a feature. 50 | The Rust code is also documented as most as possible, so please read the doc comments 51 | to really understand how to contribute to the documented code. If you think that the 52 | roadmap is incomplete, you can add items as needed. 53 | 54 | A tool that can also be useful is a Minecraft CLI launcher that I *(Théo Rozier)* made, 55 | it's called [PortableMC] and it has really good support for b1.7.3 and the game starts 56 | really fast compared to the Mojang launcher. It also fixes in-game skin and some other 57 | legacy-related issues. 58 | 59 | > [!NOTE] 60 | > The logic crate is intentionally not designed to be modular and scale with the time, 61 | > one example of this is how every logic is hardcoded in its own module (redstone power, 62 | > block placing, breaking). This is intentional in order to be more efficient developing 63 | > and make the code clearer: you want to understand how redstone power works? Simply go 64 | > into power module. 65 | 66 | [RetroMCP]: https://github.com/MCPHackers/RetroMCP-Java 67 | [PortableMC]: https://github.com/mindstorm38/portablemc 68 | 69 | ## Roadmap 70 | There is a lot of work to be done in order to provide a fully functional server on 71 | parity with the *Notchian* server, in order to properly achieve this work, the following 72 | roadmap summarize implemented and missing components and in which order we should work 73 | on them. The priority of each feature is defined by its order in the list. 74 | 75 | - [x] World and chunk data structures 76 | - [ ] World serialization 77 | - [x] Chunk data 78 | - [x] Block entity data 79 | - [x] Entity data 80 | - [ ] Level data 81 | - [ ] Blocks 82 | - [x] Definitions 83 | - [x] Item drop 84 | - [x] Tick scheduling 85 | - [x] Placing 86 | - [x] Breaking 87 | - [x] Piston 88 | - [ ] Rail 89 | - [ ] Redstone (partial) 90 | - [ ] Clicking (partial) 91 | - [ ] Items 92 | - [x] Definitions 93 | - [x] Inventory data structure 94 | - [x] Crafting 95 | - [x] Definitions 96 | - [x] Tracker 97 | - [ ] Use/place behaviors 98 | - [x] Break behaviors 99 | - [ ] Entities 100 | - [x] Entity data structures 101 | - [ ] Entity behaviors (95%) 102 | - [ ] Server 103 | - [x] Protocol 104 | - [x] Network threading 105 | - [x] Block breaking 106 | - [x] Long block breaking 107 | - [x] Instant block breaking 108 | - [x] Block breaking duration check 109 | - [x] Players inventory is stored server-side 110 | - [x] Players can be linked to any entity type 111 | - [ ] Worlds serialization 112 | - [ ] Non-persistent player entities 113 | - [ ] Player entities saved appart 114 | - [x] Player window 115 | - [x] Left and right click support 116 | - [x] Player inventory crafting grid 117 | - [x] Crafting table 118 | - [x] Chest 119 | - [x] Furnace 120 | - [x] Dispenser 121 | - [x] Shift-click on items 122 | - [x] Entity tracking 123 | - [x] Client-side spawn 124 | - [ ] Move world loading handling to the logic crate *(with something like AutoWorld)* 125 | - [x] Lighting engine 126 | - [x] World generation 127 | -------------------------------------------------------------------------------- /mc173-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mc173-server" 3 | description = "Minecraft beta 1.7.3 protocol and server implementation compatible with Notchian client" 4 | keywords = ["minecraft", "game", "server"] 5 | version.workspace = true 6 | edition.workspace = true 7 | readme.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | 13 | [dependencies] 14 | mc173.workspace = true 15 | 16 | mio.workspace = true 17 | 18 | crossbeam-channel.workspace = true 19 | once_cell.workspace = true 20 | 21 | byteorder.workspace = true 22 | flate2.workspace = true 23 | glam.workspace = true 24 | arcstr.workspace = true 25 | 26 | ctrlc.workspace = true 27 | 28 | tracing.workspace = true 29 | tracing-subscriber.workspace = true 30 | -------------------------------------------------------------------------------- /mc173-server/src/config.rs: -------------------------------------------------------------------------------- 1 | //! The configuration for the server, given from environment variables and lazy 2 | //! initialized when needed. 3 | 4 | use std::env; 5 | 6 | use once_cell::race::OnceBool; 7 | use glam::DVec3; 8 | 9 | 10 | /// Return true if fast entity tracking is enabled on the server. 11 | /// 12 | /// To enable this feature, set `MC173_FAST_ENTITY=1`. 13 | pub fn fast_entity() -> bool { 14 | static ENV: OnceBool = OnceBool::new(); 15 | ENV.get_or_init(|| { 16 | env::var_os("MC173_FAST_ENTITY") 17 | .map(|s| s.as_encoded_bytes() == b"1") 18 | .unwrap_or(false) 19 | }) 20 | } 21 | 22 | /// Return true if the client-side piston execution is enabled, when enabled (default) 23 | /// the piston extension/retraction animation is send to the client in order to have a 24 | /// client-side animation. This can be disabled to debug pistons. 25 | /// 26 | /// To disable this feature, set `MC173_CLIENT_PISTON=0`. 27 | pub fn client_piston() -> bool { 28 | static ENV: OnceBool = OnceBool::new(); 29 | ENV.get_or_init(|| { 30 | env::var_os("MC173_CLIENT_PISTON") 31 | .map(|s| s.as_encoded_bytes() == b"0") 32 | .unwrap_or(true) 33 | }) 34 | } 35 | 36 | /// Server world seed is currently hardcoded. 37 | pub const SEED: i64 = 9999; 38 | 39 | /// The spawn position is currently hardcoded. 40 | pub const SPAWN_POS: DVec3 = DVec3::new(0.0, 100.0, 0.0); 41 | // pub const SPAWN_POS: DVec3 = DVec3::new(12550800.0, 100.0, 12550800.0); 42 | -------------------------------------------------------------------------------- /mc173-server/src/main.rs: -------------------------------------------------------------------------------- 1 | //! A Minecraft beta 1.7.3 server in Rust. 2 | 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | 5 | use mc173::world::Dimension; 6 | 7 | // The common configuration of the server. 8 | pub mod config; 9 | 10 | // The network modules, net is generic and proto is the implementation for b1.7.3. 11 | pub mod net; 12 | pub mod proto; 13 | 14 | // This modules use each others, this is usually a bad design but here this was too huge 15 | // for a single module and it will be easier to maintain like this. 16 | pub mod world; 17 | pub mod chunk; 18 | pub mod entity; 19 | pub mod offline; 20 | pub mod player; 21 | pub mod command; 22 | 23 | // This module link the previous ones to make a fully functional, multi-world server. 24 | pub mod server; 25 | 26 | /// Storing true while the server should run. 27 | static RUNNING: AtomicBool = AtomicBool::new(true); 28 | 29 | 30 | /// Entrypoint! 31 | pub fn main() { 32 | 33 | init_tracing(); 34 | 35 | ctrlc::set_handler(|| RUNNING.store(false, Ordering::Relaxed)).unwrap(); 36 | 37 | let mut server = server::Server::bind("127.0.0.1:25565".parse().unwrap()).unwrap(); 38 | server.register_world(format!("overworld"), Dimension::Overworld); 39 | 40 | while RUNNING.load(Ordering::Relaxed) { 41 | server.tick_padded().unwrap(); 42 | } 43 | 44 | server.stop(); 45 | 46 | } 47 | 48 | /// Initialize tracing to output into the console. 49 | fn init_tracing() { 50 | 51 | use tracing_subscriber::util::SubscriberInitExt; 52 | use tracing_subscriber::layer::SubscriberExt; 53 | use tracing_subscriber::EnvFilter; 54 | 55 | let filter_layer = EnvFilter::try_from_default_env() 56 | .or_else(|_| EnvFilter::try_new("debug")) 57 | .unwrap(); 58 | 59 | let fmt_layer = tracing_subscriber::fmt::layer() 60 | .with_target(false); 61 | 62 | tracing_subscriber::registry() 63 | .with(filter_layer) 64 | .with(fmt_layer) 65 | .init(); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /mc173-server/src/offline.rs: -------------------------------------------------------------------------------- 1 | //! Offline player data. 2 | 3 | use glam::{DVec3, Vec2}; 4 | 5 | 6 | /// An offline player defines the saved data of a player that is not connected. 7 | #[derive(Debug)] 8 | pub struct OfflinePlayer { 9 | /// World name. 10 | pub world: String, 11 | /// Last saved position of the player. 12 | pub pos: DVec3, 13 | /// Last saved look of the player. 14 | pub look: Vec2, 15 | } 16 | -------------------------------------------------------------------------------- /mc173-server/src/server.rs: -------------------------------------------------------------------------------- 1 | //! The network server managing connected players and dispatching incoming packets. 2 | 3 | use std::time::{Duration, Instant}; 4 | use std::collections::HashMap; 5 | use std::net::SocketAddr; 6 | use std::io; 7 | 8 | use glam::Vec2; 9 | 10 | use tracing::{warn, info}; 11 | 12 | use mc173::world::{Dimension, Weather}; 13 | use mc173::entity::{self as e}; 14 | 15 | use crate::config; 16 | use crate::proto::{self, Network, NetworkEvent, NetworkClient, InPacket, OutPacket}; 17 | use crate::offline::OfflinePlayer; 18 | use crate::player::ServerPlayer; 19 | use crate::world::ServerWorld; 20 | 21 | 22 | /// Target tick duration. Currently 20 TPS, so 50 ms/tick. 23 | const TICK_DURATION: Duration = Duration::from_millis(50); 24 | 25 | 26 | /// This structure manages a whole server and its clients, dispatching incoming packets 27 | /// to correct handlers. The server is responsible of associating clients 28 | pub struct Server { 29 | /// Packet server handle. 30 | net: Network, 31 | /// Clients of this server, these structures track the network state of each client. 32 | clients: HashMap, 33 | /// Worlds list. 34 | worlds: Vec, 35 | /// Offline players database. 36 | offline_players: HashMap, 37 | } 38 | 39 | impl Server { 40 | 41 | /// Bind this server's TCP listener to the given address. 42 | pub fn bind(addr: SocketAddr) -> io::Result { 43 | 44 | info!("server bound to {addr}"); 45 | 46 | Ok(Self { 47 | net: Network::bind(addr)?, 48 | clients: HashMap::new(), 49 | worlds: vec![], 50 | offline_players: HashMap::new(), 51 | }) 52 | 53 | } 54 | 55 | /// Register a world in this server. 56 | pub fn register_world(&mut self, name: String, dimension: Dimension) { 57 | self.worlds.push(WorldState { 58 | world: ServerWorld::new(name, dimension), 59 | players: Vec::new(), 60 | }); 61 | } 62 | 63 | /// Force save this server and block waiting for all resources to be saved. 64 | pub fn stop(&mut self) { 65 | 66 | for state in &mut self.worlds { 67 | state.world.stop(); 68 | } 69 | 70 | } 71 | 72 | /// Run a single tick on the server network and worlds. This function also waits for 73 | /// this function to approximately last for 50 ms (20 TPS), there is no sleep of the 74 | /// tick was too long, in such case a warning is logged. 75 | pub fn tick_padded(&mut self) -> io::Result<()> { 76 | 77 | let start = Instant::now(); 78 | self.tick()?; 79 | let elapsed = start.elapsed(); 80 | 81 | if let Some(missing) = TICK_DURATION.checked_sub(elapsed) { 82 | std::thread::sleep(missing); 83 | } else { 84 | warn!("tick too long {:?}, expected {:?}", elapsed, TICK_DURATION); 85 | } 86 | 87 | Ok(()) 88 | 89 | } 90 | 91 | /// Run a single tick on the server network and worlds. 92 | pub fn tick(&mut self) -> io::Result<()> { 93 | 94 | // Start by ticking the network, we receive and process all packets from clients. 95 | // All client-world interactions happens here. 96 | self.tick_net()?; 97 | 98 | // Then we tick each world. 99 | for state in &mut self.worlds { 100 | state.world.tick(&mut state.players); 101 | } 102 | 103 | Ok(()) 104 | 105 | } 106 | 107 | /// Tick the network and accept incoming events. 108 | fn tick_net(&mut self) -> io::Result<()> { 109 | 110 | // Poll all network events. 111 | while let Some(event) = self.net.poll()? { 112 | match event { 113 | NetworkEvent::Accept { client } => 114 | self.handle_accept(client), 115 | NetworkEvent::Lost { client, error } => 116 | self.handle_lost(client, error), 117 | NetworkEvent::Packet { client, packet } => 118 | self.handle_packet(client, packet), 119 | } 120 | } 121 | 122 | Ok(()) 123 | 124 | } 125 | 126 | /// Handle new client accepted by the network. 127 | fn handle_accept(&mut self, client: NetworkClient) { 128 | info!("accept client #{}", client.id()); 129 | self.clients.insert(client, ClientState::Handshaking); 130 | } 131 | 132 | /// Handle a lost client. 133 | fn handle_lost(&mut self, client: NetworkClient, error: Option) { 134 | 135 | info!("lost client #{}: {:?}", client.id(), error); 136 | 137 | let state = self.clients.remove(&client).unwrap(); 138 | 139 | if let ClientState::Playing { world_index, player_index } = state { 140 | // If the client was playing, remove it from its world. 141 | let state = &mut self.worlds[world_index]; 142 | // Swap remove the player and tell the world. 143 | let mut player = state.players.swap_remove(player_index); 144 | state.world.handle_player_leave(&mut player, true); 145 | // If a player has been swapped in place of this new one, redefine its state. 146 | if let Some(swapped_player) = state.players.get(player_index) { 147 | self.clients.insert(swapped_player.client, ClientState::Playing { 148 | world_index, 149 | player_index, 150 | }).expect("swapped player should have a previous state"); 151 | } 152 | } 153 | 154 | } 155 | 156 | fn handle_packet(&mut self, client: NetworkClient, packet: InPacket) { 157 | 158 | // println!("[{client:?}] Packet: {packet:?}"); 159 | 160 | match *self.clients.get(&client).unwrap() { 161 | ClientState::Handshaking => { 162 | self.handle_handshaking(client, packet); 163 | } 164 | ClientState::Playing { world_index, player_index } => { 165 | let state = &mut self.worlds[world_index]; 166 | let player = &mut state.players[player_index]; 167 | player.handle(&mut state.world, packet); 168 | } 169 | } 170 | 171 | } 172 | 173 | /// Handle a packet for a client that is in handshaking state. 174 | fn handle_handshaking(&mut self, client: NetworkClient, packet: InPacket) { 175 | match packet { 176 | InPacket::KeepAlive => {} 177 | InPacket::Handshake(_) => 178 | self.handle_handshake(client), 179 | InPacket::Login(packet) => 180 | self.handle_login(client, packet), 181 | _ => self.send_disconnect(client, format!("Invalid packet: {packet:?}")) 182 | } 183 | } 184 | 185 | /// Handle a handshake from a client that is still handshaking, there is no 186 | /// restriction. 187 | fn handle_handshake(&mut self, client: NetworkClient) { 188 | self.net.send(client, OutPacket::Handshake(proto::OutHandshakePacket { 189 | server: "-".to_string(), 190 | })); 191 | } 192 | 193 | /// Handle a login after handshake. 194 | fn handle_login(&mut self, client: NetworkClient, packet: proto::InLoginPacket) { 195 | 196 | if packet.protocol_version != 14 { 197 | self.send_disconnect(client, format!("Protocol version mismatch!")); 198 | return; 199 | } 200 | 201 | let spawn_pos = config::SPAWN_POS; 202 | 203 | // Get the offline player, if not existing we create a new one with the 204 | let offline_player = self.offline_players.entry(packet.username.clone()) 205 | .or_insert_with(|| { 206 | let state = &self.worlds[0]; 207 | OfflinePlayer { 208 | world: state.world.name.clone(), 209 | pos: spawn_pos, 210 | look: Vec2::ZERO, 211 | } 212 | }); 213 | 214 | let (world_index, state) = self.worlds.iter_mut() 215 | .enumerate() 216 | .filter(|(_, state)| state.world.name == offline_player.world) 217 | .next() 218 | .expect("invalid offline player world name"); 219 | 220 | let entity = e::Human::new_with(|base, living, player| { 221 | base.pos = offline_player.pos; 222 | base.look = offline_player.look; 223 | base.persistent = false; 224 | base.can_pickup = true; 225 | living.artificial = true; 226 | living.health = 200; // FIXME: Lot of HP for testing. 227 | player.username = packet.username.clone(); 228 | }); 229 | 230 | let entity_id = state.world.world.spawn_entity(entity); 231 | state.world.world.set_player_entity(entity_id, true); 232 | 233 | // Confirm the login by sending same packet in response. 234 | self.net.send(client, OutPacket::Login(proto::OutLoginPacket { 235 | entity_id, 236 | random_seed: state.world.seed, 237 | dimension: match state.world.world.get_dimension() { 238 | Dimension::Overworld => 0, 239 | Dimension::Nether => -1, 240 | }, 241 | })); 242 | 243 | // The standard server sends the spawn position just after login response. 244 | self.net.send(client, OutPacket::SpawnPosition(proto::SpawnPositionPacket { 245 | pos: spawn_pos.as_ivec3(), 246 | })); 247 | 248 | // Send the initial position for the client. 249 | self.net.send(client, OutPacket::PositionLook(proto::PositionLookPacket { 250 | pos: offline_player.pos, 251 | stance: offline_player.pos.y + 1.62, 252 | look: offline_player.look, 253 | on_ground: false, 254 | })); 255 | 256 | // Time must be sent once at login to conclude the login phase. 257 | self.net.send(client, OutPacket::UpdateTime(proto::UpdateTimePacket { 258 | time: state.world.world.get_time(), 259 | })); 260 | 261 | if state.world.world.get_weather() != Weather::Clear { 262 | self.net.send(client, OutPacket::Notification(proto::NotificationPacket { 263 | reason: 1, 264 | })); 265 | } 266 | 267 | // Finally insert the player tracker. 268 | let mut player = ServerPlayer::new(&self.net, client, entity_id, packet.username, &offline_player); 269 | state.world.handle_player_join(&mut player); 270 | let player_index = state.players.len(); 271 | state.players.push(player); 272 | 273 | // Replace the previous state with a playing state containing the world and 274 | // player indices, used to get to the player instance. 275 | let previous_state = self.clients.insert(client, ClientState::Playing { 276 | world_index, 277 | player_index, 278 | }); 279 | 280 | // Just a sanity check... 281 | debug_assert_eq!(previous_state, Some(ClientState::Handshaking)); 282 | 283 | // TODO: Broadcast chat joining chat message. 284 | 285 | } 286 | 287 | /// Send disconnect (a.k.a. kick) to a client. 288 | fn send_disconnect(&mut self, client: NetworkClient, reason: String) { 289 | self.net.send(client, OutPacket::Disconnect(proto::DisconnectPacket { 290 | reason, 291 | })) 292 | } 293 | 294 | } 295 | 296 | /// Track state of a network client in the server. 297 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 298 | enum ClientState { 299 | /// This client is not yet connected to the world. 300 | Handshaking, 301 | /// This client is actually playing into a world. 302 | Playing { 303 | /// Index of the world this player is in. 304 | world_index: usize, 305 | /// Index of the player within the server world. 306 | player_index: usize, 307 | } 308 | } 309 | 310 | /// A server world registered in the server, it is associated to a list of players. 311 | struct WorldState { 312 | /// The inner server world. 313 | world: ServerWorld, 314 | /// The players currently in this world. 315 | players: Vec, 316 | } 317 | -------------------------------------------------------------------------------- /mc173/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mc173" 3 | description = "Minecraft beta 1.7.3 base data structures and logic for running a world" 4 | keywords = ["minecraft", "game", "backend"] 5 | version.workspace = true 6 | edition.workspace = true 7 | readme.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | 13 | [dependencies] 14 | crossbeam-channel.workspace = true 15 | 16 | indexmap.workspace = true 17 | flate2.workspace = true 18 | glam.workspace = true 19 | 20 | thiserror.workspace = true 21 | byteorder.workspace = true 22 | 23 | tracing.workspace = true 24 | -------------------------------------------------------------------------------- /mc173/examples/sizeof.rs: -------------------------------------------------------------------------------- 1 | //! This example is just used internally to debug structures sizes. 2 | 3 | use std::mem::size_of; 4 | 5 | pub fn main() { 6 | 7 | println!("mc173::chunk::Chunk: {}", size_of::()); 8 | println!("mc173::world::World: {}", size_of::()); 9 | println!("mc173::entity::Entity: {}", size_of::()); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /mc173/src/biome.rs: -------------------------------------------------------------------------------- 1 | //! This modules provide the biome enumeration, it is stored in each chunk on the 2D grid. 2 | //! The Notchian implementation doesn't store the biomes, so they are generated on each 3 | //! chunk load, biomes are also not sent to the client, so it is also recomputed 4 | //! client-side in order to have the proper foliage color. 5 | 6 | use crate::entity::{EntityCategory, EntityKind}; 7 | 8 | 9 | /// Possible biomes, only used server-side for natural mob spawning. 10 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] 11 | pub enum Biome { 12 | #[default] 13 | Void, 14 | RainForest, 15 | Swampland, 16 | SeasonalForest, 17 | Forest, 18 | Savanna, 19 | ShrubLand, 20 | Taiga, 21 | Desert, 22 | Plains, 23 | IceDesert, 24 | Tundra, 25 | Nether, 26 | Sky, 27 | } 28 | 29 | impl Biome { 30 | 31 | /// Return true if it is possible to rain in a chunk. 32 | #[inline] 33 | pub fn has_rain(self) -> bool { 34 | match self { 35 | Biome::Desert | 36 | Biome::IceDesert | 37 | Biome::Nether | 38 | Biome::Sky => false, 39 | _ => true 40 | } 41 | } 42 | 43 | /// Return true if this is snowing in the biome. 44 | #[inline] 45 | pub fn has_snow(self) -> bool { 46 | match self { 47 | Biome::Taiga | 48 | Biome::IceDesert | 49 | Biome::Tundra => true, 50 | _ => false 51 | } 52 | } 53 | 54 | /// Get the natural entity kinds for the given category and this current biome. 55 | pub fn natural_entity_kinds(self, category: EntityCategory) -> &'static [NaturalEntityKind] { 56 | 57 | const ANIMALS: &'static [NaturalEntityKind] = &[ 58 | NaturalEntityKind::new(EntityKind::Sheep, 12), 59 | NaturalEntityKind::new(EntityKind::Pig, 10), 60 | NaturalEntityKind::new(EntityKind::Chicken, 10), 61 | NaturalEntityKind::new(EntityKind::Cow, 8), 62 | // Only in Forest/Taiga: 63 | NaturalEntityKind::new(EntityKind::Wolf, 2), 64 | ]; 65 | 66 | const WATER_ANIMALS: &'static [NaturalEntityKind] = &[ 67 | NaturalEntityKind::new(EntityKind::Squid, 10), 68 | ]; 69 | 70 | const MOBS: &'static [NaturalEntityKind] = &[ 71 | NaturalEntityKind::new(EntityKind::Spider, 10), 72 | NaturalEntityKind::new(EntityKind::Zombie, 10), 73 | NaturalEntityKind::new(EntityKind::Skeleton, 10), 74 | NaturalEntityKind::new(EntityKind::Creeper, 10), 75 | NaturalEntityKind::new(EntityKind::Slime, 10), 76 | ]; 77 | 78 | const NETHER_MOBS: &'static [NaturalEntityKind] = &[ 79 | NaturalEntityKind::new(EntityKind::Ghast, 10), 80 | NaturalEntityKind::new(EntityKind::PigZombie, 10), 81 | ]; 82 | 83 | const SKY_ANIMALS: &'static [NaturalEntityKind] = &[ 84 | NaturalEntityKind::new(EntityKind::Chicken, 10), 85 | ]; 86 | 87 | match self { 88 | Biome::Void => &[], 89 | Biome::RainForest | 90 | Biome::Swampland | 91 | Biome::SeasonalForest | 92 | Biome::Savanna | 93 | Biome::ShrubLand | 94 | Biome::Desert | 95 | Biome::Plains | 96 | Biome::IceDesert | 97 | Biome::Tundra => { 98 | match category { 99 | EntityCategory::Animal => &ANIMALS[..ANIMALS.len() - 1], // Skip wolf 100 | EntityCategory::WaterAnimal => WATER_ANIMALS, 101 | EntityCategory::Mob => MOBS, 102 | EntityCategory::Other => &[] 103 | } 104 | } 105 | Biome::Forest | 106 | Biome::Taiga => { 107 | match category { 108 | EntityCategory::Animal => ANIMALS, // Don't skip wolf 109 | EntityCategory::WaterAnimal => WATER_ANIMALS, 110 | EntityCategory::Mob => MOBS, 111 | EntityCategory::Other => &[] 112 | } 113 | } 114 | Biome::Nether => { 115 | match category { 116 | EntityCategory::Mob => NETHER_MOBS, 117 | _ => &[] 118 | } 119 | } 120 | Biome::Sky => { 121 | match category { 122 | EntityCategory::Animal => SKY_ANIMALS, 123 | _ => &[] 124 | } 125 | } 126 | 127 | } 128 | } 129 | 130 | } 131 | 132 | 133 | /// Describe a natural 134 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 135 | pub struct NaturalEntityKind { 136 | /// The entity kind. 137 | pub kind: EntityKind, 138 | /// The higher the rate is, the higher probability is to spawn. 139 | pub chance: u16, 140 | } 141 | 142 | impl NaturalEntityKind { 143 | 144 | #[inline] 145 | pub const fn new(kind: EntityKind, chance: u16) -> Self { 146 | Self { kind, chance } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /mc173/src/block/bed.rs: -------------------------------------------------------------------------------- 1 | //! Bed special functions for metadata. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the facing of a bed. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Face { 9 | match metadata & 3 { 10 | 0 => Face::PosZ, 11 | 1 => Face::NegX, 12 | 2 => Face::NegZ, 13 | 3 => Face::PosX, 14 | _ => unreachable!() 15 | } 16 | } 17 | 18 | /// Set the facing of a bed. 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata &= !3; 22 | *metadata |= match face { 23 | Face::PosZ => 0, 24 | Face::NegX => 1, 25 | Face::NegZ => 2, 26 | Face::PosX => 3, 27 | _ => 0, 28 | } 29 | } 30 | 31 | /// Return true if the bed is occupied. 32 | #[inline] 33 | pub fn is_occupied(metadata: u8) -> bool { 34 | metadata & 4 != 0 35 | } 36 | 37 | /// Set if the bed is occupied or not. 38 | #[inline] 39 | pub fn set_occupied(metadata: &mut u8, occupied: bool) { 40 | *metadata &= !4; 41 | *metadata |= (occupied as u8) << 2; 42 | } 43 | 44 | /// Return true if the bed block is the head piece. 45 | #[inline] 46 | pub fn is_head(metadata: u8) -> bool { 47 | metadata & 8 != 0 48 | } 49 | 50 | /// Set if the bed block is the head piece or not. 51 | #[inline] 52 | pub fn set_head(metadata: &mut u8, head: bool) { 53 | *metadata &= !8; 54 | *metadata |= (head as u8) << 3; 55 | } 56 | -------------------------------------------------------------------------------- /mc173/src/block/button.rs: -------------------------------------------------------------------------------- 1 | //! Button special functions for metadata. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// The the face the button is connected to. In b1.7.3, buttons can only attach to X/Z 7 | /// faces, not neg/pos Y. 8 | #[inline] 9 | pub fn get_face(metadata: u8) -> Option { 10 | Some(match metadata & 7 { 11 | 1 => Face::NegX, 12 | 2 => Face::PosX, 13 | 3 => Face::NegZ, 14 | 4 => Face::PosZ, 15 | _ => return None 16 | }) 17 | } 18 | 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata &= !7; 22 | *metadata |= match face { 23 | Face::NegY => 0, 24 | Face::PosY => 0, 25 | Face::NegZ => 3, 26 | Face::PosZ => 4, 27 | Face::NegX => 1, 28 | Face::PosX => 2, 29 | } 30 | } 31 | 32 | /// Return true if the button is currently active. 33 | #[inline] 34 | pub fn is_active(metadata: u8) -> bool { 35 | metadata & 8 != 0 36 | } 37 | 38 | /// Set the button active or not. 39 | #[inline] 40 | pub fn set_active(metadata: &mut u8, active: bool) { 41 | *metadata &= !8; 42 | *metadata |= (active as u8) << 3; 43 | } 44 | -------------------------------------------------------------------------------- /mc173/src/block/dispenser.rs: -------------------------------------------------------------------------------- 1 | //! Common metadata functions. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get facing of the dispenser. 7 | pub fn get_face(metadata: u8) -> Option { 8 | Some(match metadata { 9 | 2 => Face::NegZ, 10 | 3 => Face::PosZ, 11 | 4 => Face::NegX, 12 | 5 => Face::PosX, 13 | _ => return None 14 | }) 15 | } 16 | 17 | /// Set facing of the dispenser. 18 | pub fn set_face(metadata: &mut u8, face: Face) { 19 | *metadata = match face { 20 | Face::NegY => 0, 21 | Face::PosY => 1, 22 | Face::NegZ => 2, 23 | Face::PosZ => 3, 24 | Face::NegX => 4, 25 | Face::PosX => 5, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mc173/src/block/door.rs: -------------------------------------------------------------------------------- 1 | //! Door block specific logic. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the face of this door. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Face { 9 | match metadata & 3 { 10 | 0 => Face::NegX, 11 | 1 => Face::NegZ, 12 | 2 => Face::PosX, 13 | 3 => Face::PosZ, 14 | _ => unreachable!() 15 | } 16 | } 17 | 18 | #[inline] 19 | pub fn set_face(metadata: &mut u8, face: Face) { 20 | *metadata &= !3; 21 | *metadata |= match face { 22 | Face::NegY => 0, 23 | Face::PosY => 0, 24 | Face::NegX => 0, 25 | Face::PosX => 2, 26 | Face::NegZ => 1, 27 | Face::PosZ => 3, 28 | } 29 | } 30 | 31 | /// If the block is a door (iron/wood), get if it's in open state. 32 | #[inline] 33 | pub fn is_open(metadata: u8) -> bool { 34 | metadata & 4 != 0 35 | } 36 | 37 | #[inline] 38 | pub fn set_open(metadata: &mut u8, open: bool) { 39 | *metadata &= !4; 40 | *metadata |= (open as u8) << 2; 41 | } 42 | 43 | /// Return true if this door block is the upper part. 44 | #[inline] 45 | pub fn is_upper(metadata: u8) -> bool { 46 | metadata & 8 != 0 47 | } 48 | 49 | #[inline] 50 | pub fn set_upper(metadata: &mut u8, upper: bool) { 51 | *metadata &= !8; 52 | *metadata |= (upper as u8) << 3; 53 | } 54 | 55 | /// Get the actual face of this door, depending on its face and open state. 56 | #[inline] 57 | pub fn get_actual_face(metadata: u8) -> Face { 58 | let face = get_face(metadata); 59 | if is_open(metadata) { 60 | face.rotate_right() 61 | } else { 62 | face 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mc173/src/block/fluid.rs: -------------------------------------------------------------------------------- 1 | //! Fluid block special functions (mostly for water). 2 | 3 | 4 | /// Return true if this still/moving fluid block acts like a source. 5 | #[inline] 6 | pub fn is_source(metadata: u8) -> bool { 7 | metadata == 0 8 | } 9 | 10 | /// Force this metadata to be a source fluid block. This basically just overwrite metadata 11 | /// with a 0, which means that distance is 0 and fluid is not falling. 12 | #[inline] 13 | pub fn set_source(metadata: &mut u8) { 14 | *metadata = 0; 15 | } 16 | 17 | /// Get the distance to source of a fluid block. The distance can go up to 7, but does 18 | /// not account for the falling state. 19 | #[inline] 20 | pub fn get_distance(metadata: u8) -> u8 { 21 | metadata & 7 22 | } 23 | 24 | #[inline] 25 | pub fn set_distance(metadata: &mut u8, distance: u8) { 26 | debug_assert!(distance <= 7); 27 | *metadata &= !7; 28 | *metadata |= distance; 29 | } 30 | 31 | /// Get if this fluid block is falling and therefore should not spread on sides. 32 | #[inline] 33 | pub fn is_falling(metadata: u8) -> bool { 34 | metadata & 8 != 0 35 | } 36 | 37 | #[inline] 38 | pub fn set_falling(metadata: &mut u8, falling: bool) { 39 | *metadata &= !8; 40 | *metadata |= (falling as u8) << 3; 41 | } 42 | 43 | /// This function get the actual distance to the source of a fluid block, this account 44 | /// both the distance stored in the lower 3 bits, but also for the falling state: if a 45 | /// fluid is falling, it acts like a source block for propagation. 46 | #[inline] 47 | pub fn get_actual_distance(metadata: u8) -> u8 { 48 | if is_falling(metadata) { 49 | 0 50 | } else { 51 | get_distance(metadata) 52 | } 53 | } 54 | 55 | /// Calculate the actual height of a fluid block depending on its metadata. 56 | #[inline] 57 | pub fn get_actual_height(metadata: u8) -> f32 { 58 | 1.0 - (get_actual_distance(metadata) + 1) as f32 / 9.0 59 | } 60 | -------------------------------------------------------------------------------- /mc173/src/block/ladder.rs: -------------------------------------------------------------------------------- 1 | //! LAdder special functions for metadata. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// The the face the button is connected to. In b1.7.3, buttons can only attach to X/Z 7 | /// faces, not neg/pos Y. 8 | #[inline] 9 | pub fn get_face(metadata: u8) -> Option { 10 | Some(match metadata { 11 | 2 => Face::PosZ, 12 | 3 => Face::NegZ, 13 | 4 => Face::PosX, 14 | 5 => Face::NegX, 15 | _ => return None 16 | }) 17 | } 18 | 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata = match face { 22 | Face::PosZ => 2, 23 | Face::NegZ => 3, 24 | Face::PosX => 4, 25 | Face::NegX => 5, 26 | _ => 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mc173/src/block/lever.rs: -------------------------------------------------------------------------------- 1 | //! Lever special functions for metadata. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// The the face the lever is connected to. In b1.7.3, levers can only attach to X/Z and 7 | /// bottom Y. This function also returns the secondary face where this lever's stick 8 | /// points to when not active. 9 | #[inline] 10 | pub fn get_face(metadata: u8) -> Option<(Face, Face)> { 11 | Some(match metadata & 7 { 12 | 1 => (Face::NegX, Face::PosY), 13 | 2 => (Face::PosX, Face::PosY), 14 | 3 => (Face::NegZ, Face::PosY), 15 | 4 => (Face::PosZ, Face::PosY), 16 | 5 => (Face::NegY, Face::PosZ), 17 | 6 => (Face::NegY, Face::PosX), 18 | _ => return None 19 | }) 20 | } 21 | 22 | /// Set the face the lever is connected to and the direction of the lever's stick when 23 | /// not active, not that X/Z faces forces the direction to positive Y. Only positive X/Z 24 | /// should be used when facing bottom, other values will be forced to positive Z. 25 | #[inline] 26 | pub fn set_face(metadata: &mut u8, face: Face, dir: Face) { 27 | *metadata &= !7; 28 | *metadata |= match (face, dir) { 29 | (Face::NegY, Face::PosZ) => 5, 30 | (Face::NegY, _) => 6, 31 | (Face::PosY, _) => 0, 32 | (Face::NegZ, _) => 3, 33 | (Face::PosZ, _) => 4, 34 | (Face::NegX, _) => 1, 35 | (Face::PosX, _) => 2, 36 | } 37 | } 38 | 39 | /// Return true if the lever is currently active. 40 | #[inline] 41 | pub fn is_active(metadata: u8) -> bool { 42 | metadata & 8 != 0 43 | } 44 | 45 | /// Set the lever active or not. 46 | #[inline] 47 | pub fn set_active(metadata: &mut u8, active: bool) { 48 | *metadata &= !8; 49 | *metadata |= (active as u8) << 3; 50 | } 51 | -------------------------------------------------------------------------------- /mc173/src/block/mod.rs: -------------------------------------------------------------------------------- 1 | //! Block enumeration and functions to query their metadata state. 2 | 3 | // Block behaviors. 4 | pub mod material; 5 | 6 | // Block specific functions for their metadata. 7 | pub mod dispenser; 8 | pub mod trapdoor; 9 | pub mod repeater; 10 | pub mod pumpkin; 11 | pub mod sapling; 12 | pub mod button; 13 | pub mod ladder; 14 | pub mod piston; 15 | pub mod lever; 16 | pub mod stair; 17 | pub mod torch; 18 | pub mod fluid; 19 | pub mod door; 20 | pub mod sign; 21 | pub mod bed; 22 | 23 | 24 | /// Internal macro to easily define blocks registry. 25 | macro_rules! blocks { 26 | ( 27 | $($ident:ident / $id:literal : $name:literal),* $(,)? 28 | ) => { 29 | 30 | static NAMES: [&'static str; 256] = { 31 | let mut arr = [""; 256]; 32 | $(arr[$id as usize] = $name;)* 33 | arr 34 | }; 35 | 36 | $(pub const $ident: u8 = $id;)* 37 | 38 | }; 39 | } 40 | 41 | blocks! { 42 | AIR/0: "air", 43 | STONE/1: "stone", 44 | GRASS/2: "grass", 45 | DIRT/3: "dirt", 46 | COBBLESTONE/4: "cobblestone", 47 | WOOD/5: "wood", 48 | SAPLING/6: "sapling", 49 | BEDROCK/7: "bedrock", 50 | WATER_MOVING/8: "water_moving", 51 | WATER_STILL/9: "water_still", 52 | LAVA_MOVING/10: "lava_moving", 53 | LAVA_STILL/11: "lava_still", 54 | SAND/12: "sand", 55 | GRAVEL/13: "gravel", 56 | GOLD_ORE/14: "gold_ore", 57 | IRON_ORE/15: "iron_ore", 58 | COAL_ORE/16: "coal_ore", 59 | LOG/17: "log", 60 | LEAVES/18: "leaves", 61 | SPONGE/19: "sponge", 62 | GLASS/20: "glass", 63 | LAPIS_ORE/21: "lapis_ore", 64 | LAPIS_BLOCK/22: "lapis_block", 65 | DISPENSER/23: "dispenser", 66 | SANDSTONE/24: "sandstone", 67 | NOTE_BLOCK/25: "note_block", 68 | BED/26: "bed", 69 | POWERED_RAIL/27: "powered_rail", 70 | DETECTOR_RAIL/28: "detector_rail", 71 | STICKY_PISTON/29: "sticky_piston", 72 | COBWEB/30: "cobweb", 73 | TALL_GRASS/31: "tall_grass", 74 | DEAD_BUSH/32: "dead_bush", 75 | PISTON/33: "piston", 76 | PISTON_EXT/34: "piston_ext", 77 | WOOL/35: "wool", 78 | PISTON_MOVING/36: "piston_moving", 79 | DANDELION/37: "dandelion", 80 | POPPY/38: "poppy", 81 | BROWN_MUSHROOM/39: "brown_mushroom", 82 | RED_MUSHROOM/40: "red_mushroom", 83 | GOLD_BLOCK/41: "gold_block", 84 | IRON_BLOCK/42: "iron_block", 85 | DOUBLE_SLAB/43: "double_slab", 86 | SLAB/44: "slab", 87 | BRICK/45: "brick", 88 | TNT/46: "tnt", 89 | BOOKSHELF/47: "bookshelf", 90 | MOSSY_COBBLESTONE/48: "mossy_cobblestone", 91 | OBSIDIAN/49: "obsidian", 92 | TORCH/50: "torch", 93 | FIRE/51: "fire", 94 | SPAWNER/52: "spawner", 95 | WOOD_STAIR/53: "wood_stair", 96 | CHEST/54: "chest", 97 | REDSTONE/55: "redstone", 98 | DIAMOND_ORE/56: "diamond_ore", 99 | DIAMOND_BLOCK/57: "diamond_block", 100 | CRAFTING_TABLE/58: "crafting_table", 101 | WHEAT/59: "wheat", 102 | FARMLAND/60: "farmland", 103 | FURNACE/61: "furnace", 104 | FURNACE_LIT/62: "furnace_lit", 105 | SIGN/63: "sign", 106 | WOOD_DOOR/64: "wood_door", 107 | LADDER/65: "ladder", 108 | RAIL/66: "rail", 109 | COBBLESTONE_STAIR/67: "cobblestone_stair", 110 | WALL_SIGN/68: "wall_sign", 111 | LEVER/69: "lever", 112 | STONE_PRESSURE_PLATE/70: "stone_pressure_plate", 113 | IRON_DOOR/71: "iron_door", 114 | WOOD_PRESSURE_PLATE/72: "wood_pressure_plate", 115 | REDSTONE_ORE/73: "redstone_ore", 116 | REDSTONE_ORE_LIT/74: "redstone_ore_lit", 117 | REDSTONE_TORCH/75: "redstone_torch", 118 | REDSTONE_TORCH_LIT/76: "redstone_torch_lit", 119 | BUTTON/77: "button", 120 | SNOW/78: "snow", 121 | ICE/79: "ice", 122 | SNOW_BLOCK/80: "snow_block", 123 | CACTUS/81: "cactus", 124 | CLAY/82: "clay", 125 | SUGAR_CANES/83: "sugar_canes", 126 | JUKEBOX/84: "jukebox", 127 | FENCE/85: "fence", 128 | PUMPKIN/86: "pumpkin", 129 | NETHERRACK/87: "netherrack", 130 | SOULSAND/88: "soulsand", 131 | GLOWSTONE/89: "glowstone", 132 | PORTAL/90: "portal", 133 | PUMPKIN_LIT/91: "pumpkin_lit", 134 | CAKE/92: "cake", 135 | REPEATER/93: "repeater", 136 | REPEATER_LIT/94: "repeater_lit", 137 | LOCKED_CHEST/95: "locked_chest", 138 | TRAPDOOR/96: "trapdoor", 139 | } 140 | 141 | /// Find a block name from its id. 142 | #[inline] 143 | pub const fn name(id: u8) -> &'static str { 144 | NAMES[id as usize] 145 | } 146 | 147 | /// Find a block id from its name. 148 | pub fn from_name(name: &str) -> Option { 149 | NAMES.iter() 150 | .position(|&n| n == name) 151 | .map(|n| n as u8) 152 | } 153 | -------------------------------------------------------------------------------- /mc173/src/block/piston.rs: -------------------------------------------------------------------------------- 1 | //! Piston behaviors. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the facing of the piston base or extension. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Option { 9 | Some(match metadata & 7 { 10 | 0 => Face::NegY, 11 | 1 => Face::PosY, 12 | 2 => Face::NegZ, 13 | 3 => Face::PosZ, 14 | 4 => Face::NegX, 15 | 5 => Face::PosX, 16 | _ => return None 17 | }) 18 | } 19 | 20 | /// Set the facing of the piston base or extension. 21 | #[inline] 22 | pub fn set_face(metadata: &mut u8, face: Face) { 23 | *metadata &= !7; 24 | *metadata |= face as u8; 25 | } 26 | 27 | /// Get if a piston base has extended or not. 28 | #[inline] 29 | pub fn is_base_extended(metadata: u8) -> bool { 30 | metadata & 8 != 0 31 | } 32 | 33 | /// Set if a piston base has extended or not. 34 | #[inline] 35 | pub fn set_base_extended(metadata: &mut u8, extended: bool) { 36 | *metadata &= !8; 37 | *metadata |= (extended as u8) << 3; 38 | } 39 | 40 | /// Get if a piston extension is sticky or not. 41 | #[inline] 42 | pub fn is_ext_sticky(metadata: u8) -> bool { 43 | is_base_extended(metadata) // Same bit so we use same function 44 | } 45 | 46 | /// Set a piston extension to be sticky or not. 47 | #[inline] 48 | pub fn set_ext_sticky(metadata: &mut u8, sticky: bool) { 49 | set_base_extended(metadata, sticky) 50 | } 51 | -------------------------------------------------------------------------------- /mc173/src/block/pumpkin.rs: -------------------------------------------------------------------------------- 1 | //! Pumpkin block metadata functions. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the face where the pumpkin is carved. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Face { 9 | match metadata & 3 { 10 | 0 => Face::PosZ, 11 | 1 => Face::NegX, 12 | 2 => Face::NegZ, 13 | 3 => Face::PosX, 14 | _ => unreachable!() 15 | } 16 | } 17 | 18 | /// Set the face where the pumpkin is carved. 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata &= !3; 22 | *metadata |= match face { 23 | Face::PosZ => 0, 24 | Face::NegX => 1, 25 | Face::NegZ => 2, 26 | Face::PosX => 3, 27 | _ => 0 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /mc173/src/block/repeater.rs: -------------------------------------------------------------------------------- 1 | //! Redstone repeater metadata functions. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the face where the repeater send power. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Face { 9 | match metadata & 3 { 10 | 0 => Face::NegZ, 11 | 1 => Face::PosX, 12 | 2 => Face::PosZ, 13 | 3 => Face::NegX, 14 | _ => unreachable!() 15 | } 16 | } 17 | 18 | /// Set the face where the repeater send power. 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata &= !3; 22 | *metadata |= match face { 23 | Face::NegZ => 0, 24 | Face::PosX => 1, 25 | Face::PosZ => 2, 26 | Face::NegX => 3, 27 | _ => 0 28 | }; 29 | } 30 | 31 | /// Get the delay of the repeater. 32 | #[inline] 33 | pub fn get_delay(metadata: u8) -> u8 { 34 | (metadata & 0b1100) >> 2 35 | } 36 | 37 | /// Set the delay of the repeater. 38 | #[inline] 39 | pub fn set_delay(metadata: &mut u8, delay: u8) { 40 | *metadata &= !0b1100; 41 | *metadata |= (delay & 0b11) << 2; 42 | } 43 | 44 | /// Get the delay of the repeater in ticks. 45 | #[inline] 46 | pub fn get_delay_ticks(metadata: u8) -> u64 { 47 | (get_delay(metadata) as u64 + 1) * 2 48 | } 49 | -------------------------------------------------------------------------------- /mc173/src/block/sapling.rs: -------------------------------------------------------------------------------- 1 | //! Sapling block metadata functions. 2 | 3 | 4 | /// Kind of tree for sapling, logs and leaves. 5 | #[derive(Debug, )] 6 | pub enum TreeKind { 7 | Oak, 8 | Birch, 9 | Spruce, 10 | } 11 | 12 | 13 | /// Get the kind of tree for this sapling. 14 | #[inline] 15 | pub fn get_kind(metadata: u8) -> TreeKind { 16 | match metadata & 3 { 17 | 0 | 3 => TreeKind::Oak, 18 | 1 => TreeKind::Spruce, 19 | 2 => TreeKind::Birch, 20 | _ => unreachable!() 21 | } 22 | } 23 | 24 | /// Set the face where the pumpkin is carved. 25 | #[inline] 26 | pub fn set_kind(metadata: &mut u8, kind: TreeKind) { 27 | *metadata &= !3; 28 | *metadata |= match kind { 29 | TreeKind::Oak | 30 | TreeKind::Spruce => 1, 31 | TreeKind::Birch => 2, 32 | }; 33 | } 34 | 35 | /// Return true if the sapling is growing and will grow on the next random tick. 36 | #[inline] 37 | pub fn is_growing(metadata: u8) -> bool { 38 | metadata & 8 != 0 39 | } 40 | 41 | /// Set if a sapling is growing. 42 | #[inline] 43 | pub fn set_growing(metadata: &mut u8, growing: bool) { 44 | *metadata &= !8; 45 | *metadata |= (growing as u8) << 3; 46 | } 47 | -------------------------------------------------------------------------------- /mc173/src/block/sign.rs: -------------------------------------------------------------------------------- 1 | //! Sign (post/wall) block metadata functions. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the face where the stair leads to. 7 | #[inline] 8 | pub fn get_wall_face(metadata: u8) -> Option { 9 | Some(match metadata { 10 | 5 => Face::PosX, 11 | 4 => Face::NegX, 12 | 3 => Face::PosZ, 13 | 2 => Face::NegZ, 14 | _ => return None 15 | }) 16 | } 17 | 18 | #[inline] 19 | pub fn set_wall_face(metadata: &mut u8, face: Face) { 20 | *metadata = match face { 21 | Face::PosX => 5, 22 | Face::NegX => 4, 23 | Face::PosZ => 3, 24 | Face::NegZ => 2, 25 | _ => panic!("invalid wall face") 26 | }; 27 | } 28 | 29 | /// Get the sign post yaw angle. 30 | #[inline] 31 | pub fn get_post_yaw(metadata: u8) -> f32 { 32 | (metadata as f32 - 0.5) / 16.0 * std::f32::consts::TAU 33 | } 34 | 35 | /// Set the sign post yaw angle, approximated due to metadata being in range 0..16. 36 | #[inline] 37 | pub fn set_post_yaw(metadata: &mut u8, yaw: f32) { 38 | *metadata = (yaw / std::f32::consts::TAU * 16.0 + 0.5) as i32 as u8 & 15; 39 | } 40 | -------------------------------------------------------------------------------- /mc173/src/block/stair.rs: -------------------------------------------------------------------------------- 1 | //! Stair block metadata functions. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the face where the stair leads to. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Face { 9 | match metadata & 3 { 10 | 0 => Face::PosX, 11 | 1 => Face::NegX, 12 | 2 => Face::PosZ, 13 | 3 => Face::NegZ, 14 | _ => unreachable!() 15 | } 16 | } 17 | 18 | #[inline] 19 | pub fn set_face(metadata: &mut u8, face: Face) { 20 | *metadata &= !3; 21 | *metadata = match face { 22 | Face::PosX => 0, 23 | Face::NegX => 1, 24 | Face::PosZ => 2, 25 | Face::NegZ => 3, 26 | _ => 0 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /mc173/src/block/torch.rs: -------------------------------------------------------------------------------- 1 | //! Torch (including redstone torch) metadata functions. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// Get the face this torch is attached to. 7 | #[inline] 8 | pub fn get_face(metadata: u8) -> Option { 9 | Some(match metadata { 10 | 1 => Face::NegX, 11 | 2 => Face::PosX, 12 | 3 => Face::NegZ, 13 | 4 => Face::PosZ, 14 | 5 => Face::NegY, 15 | _ => return None 16 | }) 17 | } 18 | 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata = match face { 22 | Face::NegX => 1, 23 | Face::PosX => 2, 24 | Face::NegZ => 3, 25 | Face::PosZ => 4, 26 | Face::NegY => 5, 27 | _ => 5, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mc173/src/block/trapdoor.rs: -------------------------------------------------------------------------------- 1 | //! LAdder special functions for metadata. 2 | 3 | use crate::geom::Face; 4 | 5 | 6 | /// The the face the button is connected to. In b1.7.3, buttons can only attach to X/Z 7 | /// faces, not neg/pos Y. 8 | #[inline] 9 | pub fn get_face(metadata: u8) -> Face { 10 | match metadata & 3 { 11 | 0 => Face::PosZ, 12 | 1 => Face::NegZ, 13 | 2 => Face::PosX, 14 | 3 => Face::NegX, 15 | _ => unreachable!() 16 | } 17 | } 18 | 19 | #[inline] 20 | pub fn set_face(metadata: &mut u8, face: Face) { 21 | *metadata &= !3; 22 | *metadata |= match face { 23 | Face::PosZ => 0, 24 | Face::NegZ => 1, 25 | Face::PosX => 2, 26 | Face::NegX => 3, 27 | _ => 0 28 | } 29 | } 30 | 31 | /// Return true if the trapdoor is currently open. 32 | #[inline] 33 | pub fn is_open(metadata: u8) -> bool { 34 | metadata & 4 != 0 35 | } 36 | 37 | /// Set the trapdoor open or not. 38 | #[inline] 39 | pub fn set_open(metadata: &mut u8, active: bool) { 40 | *metadata &= !4; 41 | *metadata |= (active as u8) << 2; 42 | } 43 | -------------------------------------------------------------------------------- /mc173/src/block_entity/chest.rs: -------------------------------------------------------------------------------- 1 | //! Chest block entity. 2 | 3 | use crate::item::ItemStack; 4 | 5 | 6 | #[derive(Debug, Clone, Default)] 7 | pub struct ChestBlockEntity { 8 | /// The inventory of the chest. 9 | pub inv: Box<[ItemStack; 27]>, 10 | } 11 | -------------------------------------------------------------------------------- /mc173/src/block_entity/dispenser.rs: -------------------------------------------------------------------------------- 1 | //! Dispenser block entity. 2 | 3 | use crate::java::JavaRandom; 4 | use crate::item::ItemStack; 5 | 6 | 7 | #[derive(Debug, Clone, Default)] 8 | pub struct DispenserBlockEntity { 9 | /// The inventory of the dispenser. 10 | pub inv: Box<[ItemStack; 9]>, 11 | /// The dispenser has its own RNG. 12 | pub rand: JavaRandom, 13 | } 14 | 15 | impl DispenserBlockEntity { 16 | 17 | /// Randomly pick a non-empty stack in this dispenser, returning its index if any, 18 | /// none if there are only empty stacks in the inventory. 19 | pub fn pick_random_index(&mut self) -> Option { 20 | 21 | let mut bound = 0; 22 | let mut selected_index = None; 23 | 24 | for (index, stack) in self.inv.iter_mut().enumerate() { 25 | if !stack.is_empty() { 26 | bound += 1; 27 | if self.rand.next_int_bounded(bound) == 0 { 28 | selected_index = Some(index); 29 | } 30 | } 31 | } 32 | 33 | selected_index 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /mc173/src/block_entity/furnace.rs: -------------------------------------------------------------------------------- 1 | //! Furnace block entity. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::item::{self, ItemStack}; 6 | use crate::world::{World, Event, BlockEntityEvent, BlockEntityStorage, BlockEntityProgress}; 7 | use crate::{item::smelt, block}; 8 | 9 | 10 | #[derive(Debug, Clone, Default)] 11 | pub struct FurnaceBlockEntity { 12 | /// Input stack of the furnace. 13 | pub input_stack: ItemStack, 14 | /// Item stack for fueling the furnace. 15 | pub fuel_stack: ItemStack, 16 | /// Output stack of the furnace. 17 | pub output_stack: ItemStack, 18 | /// Max burn ticks for the current fuel being consumed. 19 | pub burn_max_ticks: u16, 20 | /// Current burn remaining ticks until a fuel item need to be consumed again. 21 | pub burn_remaining_ticks: u16, 22 | /// Current ticks count since the current item has been added. 23 | pub smelt_ticks: u16, 24 | /// Last input stack, used to compare to new one and update the current recipe. 25 | last_input_stack: ItemStack, 26 | /// Last output stack, used to compare to new one and update the current recipe. 27 | last_output_stack: ItemStack, 28 | /// If some recipe has been found for the current input stack, this contains the 29 | /// future output stack that will be assigned to output stack. 30 | active_output_stack: Option, 31 | } 32 | 33 | impl FurnaceBlockEntity { 34 | 35 | /// Internal function to compute the new recipe depending on the current input item. 36 | /// None is returned if the input stack is empty, if no recipe can be found, or if 37 | /// the recipe's output do not fit in the output stack. 38 | fn find_new_output_stack(&self) -> Option { 39 | 40 | if self.input_stack.size == 0 { 41 | return None; 42 | } 43 | 44 | let input_id = self.input_stack.id; 45 | let input_damage = self.input_stack.damage; 46 | let mut output_stack = smelt::find_smelting_output(input_id, input_damage)?; 47 | 48 | if !self.output_stack.is_empty() { 49 | if (self.output_stack.id, self.output_stack.damage) != (output_stack.id, output_stack.damage) { 50 | return None; 51 | } else if self.output_stack.size + output_stack.size > item::from_id(output_stack.id).max_stack_size { 52 | return None; 53 | } else { 54 | output_stack.size += self.output_stack.size; 55 | } 56 | } 57 | 58 | Some(output_stack) 59 | 60 | } 61 | 62 | /// Tick the furnace block entity. 63 | pub fn tick(&mut self, world: &mut World, pos: IVec3) { 64 | 65 | // If the input stack have changed since last update, get the new recipe. 66 | // TODO: Also update of output stack have changed. 67 | if self.input_stack != self.last_input_stack || self.output_stack != self.last_output_stack { 68 | self.active_output_stack = self.find_new_output_stack(); 69 | self.last_input_stack = self.input_stack; 70 | self.last_output_stack = self.output_stack; 71 | } 72 | 73 | let mut smelt_modified = false; 74 | let mut fuel_modified = false; 75 | 76 | let initial_burning = self.burn_remaining_ticks != 0; 77 | if initial_burning { 78 | self.burn_remaining_ticks -= 1; 79 | fuel_modified = true; 80 | } 81 | 82 | if let Some(active_output_stack) = &self.active_output_stack { 83 | 84 | if self.burn_remaining_ticks == 0 && !self.fuel_stack.is_empty() { 85 | 86 | self.burn_max_ticks = smelt::get_burn_ticks(self.fuel_stack.id); 87 | self.burn_remaining_ticks = self.burn_max_ticks; 88 | 89 | if self.burn_max_ticks != 0 { 90 | 91 | self.fuel_stack.size -= 1; 92 | fuel_modified = true; 93 | 94 | world.push_event(Event::BlockEntity { 95 | pos, 96 | inner: BlockEntityEvent::Storage { 97 | storage: BlockEntityStorage::FurnaceFuel, 98 | stack: self.fuel_stack, 99 | }, 100 | }); 101 | 102 | world.push_event(Event::BlockEntity { 103 | pos, 104 | inner: BlockEntityEvent::Progress { 105 | progress: BlockEntityProgress::FurnaceBurnMaxTime, 106 | value: self.burn_max_ticks, 107 | }, 108 | }); 109 | 110 | } 111 | 112 | } 113 | 114 | if self.burn_remaining_ticks == 0 { 115 | if self.smelt_ticks != 0 { 116 | self.smelt_ticks = 0; 117 | smelt_modified = true; 118 | } 119 | } else { 120 | 121 | self.smelt_ticks += 1; 122 | if self.smelt_ticks == 200 { 123 | 124 | self.smelt_ticks = 0; 125 | // This should not underflow because if input stack is empty, not 126 | // active output stack can be set. 127 | // NOTE: Modifying both of these will trigger an update of the active 128 | // output stack on the next tick. 129 | self.input_stack.size -= 1; 130 | self.output_stack = *active_output_stack; 131 | 132 | world.push_event(Event::BlockEntity { 133 | pos, 134 | inner: BlockEntityEvent::Storage { 135 | storage: BlockEntityStorage::FurnaceInput, 136 | stack: self.input_stack, 137 | }, 138 | }); 139 | 140 | world.push_event(Event::BlockEntity { 141 | pos, 142 | inner: BlockEntityEvent::Storage { 143 | storage: BlockEntityStorage::FurnaceOutput, 144 | stack: self.output_stack, 145 | }, 146 | }); 147 | 148 | } 149 | 150 | smelt_modified = true; 151 | 152 | } 153 | 154 | } else if self.smelt_ticks != 0 { 155 | self.smelt_ticks = 0; 156 | smelt_modified = true; 157 | } 158 | 159 | if smelt_modified { 160 | world.push_event(Event::BlockEntity { 161 | pos, 162 | inner: BlockEntityEvent::Progress { 163 | progress: BlockEntityProgress::FurnaceSmeltTime, 164 | value: self.smelt_ticks, 165 | }, 166 | }); 167 | } 168 | 169 | if fuel_modified { 170 | world.push_event(Event::BlockEntity { 171 | pos, 172 | inner: BlockEntityEvent::Progress { 173 | progress: BlockEntityProgress::FurnaceBurnRemainingTime, 174 | value: self.burn_remaining_ticks, 175 | }, 176 | }); 177 | } 178 | 179 | if initial_burning != (self.burn_remaining_ticks != 0) { 180 | let (_id, metadata) = world.get_block(pos).expect("should not be ticking if not loaded"); 181 | if initial_burning { 182 | // No longer burning. 183 | world.set_block_notify(pos, block::FURNACE, metadata); 184 | } else { 185 | // Now burning. 186 | world.set_block_notify(pos, block::FURNACE_LIT, metadata); 187 | } 188 | } 189 | 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /mc173/src/block_entity/jukebox.rs: -------------------------------------------------------------------------------- 1 | //! Sign block entity. 2 | 3 | 4 | #[derive(Debug, Clone, Default)] 5 | pub struct JukeboxBlockEntity { 6 | /// The record currently playing in the jukebox. 7 | pub record: u32, 8 | } 9 | -------------------------------------------------------------------------------- /mc173/src/block_entity/mod.rs: -------------------------------------------------------------------------------- 1 | //! Block entities structures and logic implementation. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::world::World; 6 | 7 | pub mod chest; 8 | pub mod furnace; 9 | pub mod dispenser; 10 | pub mod spawner; 11 | pub mod note_block; 12 | pub mod piston; 13 | pub mod sign; 14 | pub mod jukebox; 15 | 16 | 17 | /// All kinds of block entities. 18 | #[derive(Debug, Clone)] 19 | pub enum BlockEntity { 20 | Chest(chest::ChestBlockEntity), 21 | Furnace(furnace::FurnaceBlockEntity), 22 | Dispenser(dispenser::DispenserBlockEntity), 23 | Spawner(spawner::SpawnerBlockEntity), 24 | NoteBlock(note_block::NoteBlockBlockEntity), 25 | Piston(piston::PistonBlockEntity), 26 | Sign(sign::SignBlockEntity), 27 | Jukebox(jukebox::JukeboxBlockEntity), 28 | } 29 | 30 | impl BlockEntity { 31 | 32 | /// Tick the block entity at its position in the world. 33 | pub fn tick(&mut self, world: &mut World, pos: IVec3) { 34 | match self { 35 | BlockEntity::Chest(_) => (), 36 | BlockEntity::Furnace(furnace) => furnace.tick(world, pos), 37 | BlockEntity::Dispenser(_) => (), 38 | BlockEntity::Spawner(spawner) => spawner.tick(world, pos), 39 | BlockEntity::NoteBlock(_) => (), 40 | BlockEntity::Piston(piston) => piston.tick(world, pos), 41 | BlockEntity::Sign(_) => (), 42 | BlockEntity::Jukebox(_) => (), 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /mc173/src/block_entity/note_block.rs: -------------------------------------------------------------------------------- 1 | //! Note block block entity. 2 | 3 | 4 | #[derive(Debug, Clone, Default)] 5 | pub struct NoteBlockBlockEntity { 6 | /// The note to play. 7 | pub note: u8, 8 | /// True if the note block is currently powered. 9 | pub powered: bool, 10 | } 11 | -------------------------------------------------------------------------------- /mc173/src/block_entity/piston.rs: -------------------------------------------------------------------------------- 1 | //! Moving piston block entity. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block; 6 | use crate::world::World; 7 | use crate::geom::Face; 8 | 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct PistonBlockEntity { 12 | /// The block id of the moving piston block. 13 | pub block: u8, 14 | /// The block metadata of the moving piston block. 15 | pub metadata: u8, 16 | /// Face toward the block is moving. 17 | pub face: Face, 18 | /// Progress of the move animation. 19 | pub progress: f32, 20 | /// True when the piston is extending, false when retracting. 21 | pub extending: bool, 22 | } 23 | 24 | impl Default for PistonBlockEntity { 25 | fn default() -> Self { 26 | Self { 27 | block: 0, 28 | metadata: 0, 29 | face: Face::PosY, 30 | progress: 0.0, 31 | extending: false, 32 | } 33 | } 34 | } 35 | 36 | impl PistonBlockEntity { 37 | 38 | pub fn tick(&mut self, world: &mut World, pos: IVec3) { 39 | 40 | if self.progress >= 1.0 { 41 | // TODO: Handle entity push 42 | world.remove_block_entity(pos); 43 | if world.is_block(pos, block::PISTON_MOVING) { 44 | world.set_block_notify(pos, self.block, self.metadata); 45 | } 46 | } else { 47 | self.progress += 0.5; 48 | if self.extending { 49 | // TODO: Handle entity push 50 | } 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /mc173/src/block_entity/sign.rs: -------------------------------------------------------------------------------- 1 | //! Sign block entity. 2 | 3 | 4 | #[derive(Debug, Clone, Default)] 5 | pub struct SignBlockEntity { 6 | /// Text line of this sign block. 7 | pub lines: Box<[String; 4]>, 8 | } 9 | -------------------------------------------------------------------------------- /mc173/src/block_entity/spawner.rs: -------------------------------------------------------------------------------- 1 | //! Spawner block entity. 2 | 3 | use glam::{IVec3, DVec3}; 4 | 5 | use tracing::trace; 6 | 7 | use crate::entity::{EntityKind, Entity}; 8 | use crate::geom::BoundingBox; 9 | use crate::world::World; 10 | 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct SpawnerBlockEntity { 14 | /// Remaining ticks to spawn the entity. 15 | pub remaining_time: u16, 16 | /// Kind of entity. 17 | pub entity_kind: EntityKind, 18 | } 19 | 20 | impl Default for SpawnerBlockEntity { 21 | 22 | #[inline] 23 | fn default() -> Self { 24 | Self { 25 | remaining_time: 20, 26 | entity_kind: EntityKind::Pig, 27 | } 28 | } 29 | 30 | } 31 | 32 | impl SpawnerBlockEntity { 33 | 34 | /// Tick the furnace block entity. 35 | pub fn tick(&mut self, world: &mut World, pos: IVec3) { 36 | 37 | /// Maximum distance for a player to load the spawner. 38 | const LOAD_DIST_SQUARED: f64 = 16.0 * 16.0; 39 | 40 | let center = pos.as_dvec3() + 0.5; 41 | let loaded = world.iter_entities() 42 | .filter(|(_, entity)| entity.kind() == EntityKind::Human) 43 | .any(|(_, Entity(base, _))| base.pos.distance_squared(center) < LOAD_DIST_SQUARED); 44 | 45 | if !loaded { 46 | return; 47 | } 48 | 49 | if self.remaining_time > 0 { 50 | self.remaining_time -= 1; 51 | return; 52 | } 53 | 54 | self.remaining_time = 200 + world.get_rand_mut().next_int_bounded(600) as u16; 55 | trace!("spawner {pos}, reached spawn time, next time in: {}", self.remaining_time); 56 | 57 | // Count the number of entities of the spawner type in its box. 58 | let bb = BoundingBox::CUBE + pos.as_dvec3(); 59 | let mut same_count = world.iter_entities_colliding(bb.inflate(DVec3::new(8.0, 4.0, 8.0))) 60 | .filter(|(_, entity)| entity.kind() == self.entity_kind) 61 | .count(); 62 | 63 | trace!("spawner {pos}, same entity count: {same_count}"); 64 | 65 | for _ in 0..4 { 66 | 67 | // If more than 5 entities of the same type exists, abort. 68 | if same_count > 5 { 69 | break; 70 | } 71 | 72 | let rand = world.get_rand_mut(); 73 | let pos = pos.as_dvec3() + DVec3 { 74 | x: (rand.next_double() - rand.next_double()) * 4.0, 75 | y: (rand.next_int_bounded(3) - 1) as f64, 76 | z: (rand.next_double() - rand.next_double()) * 4.0, 77 | }; 78 | 79 | let mut entity = self.entity_kind.new_default(pos); 80 | entity.0.look.x = rand.next_float(); 81 | 82 | if entity.can_natural_spawn(world) { 83 | world.spawn_entity(entity); 84 | same_count += 1; 85 | } 86 | 87 | } 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /mc173/src/entity/common.rs: -------------------------------------------------------------------------------- 1 | //! Common functions to apply modifications to entities. 2 | 3 | use std::cell::RefCell; 4 | use std::ops::Sub; 5 | 6 | use glam::{DVec3, IVec3, Vec2, Vec3Swizzles}; 7 | 8 | use crate::world::bound::RayTraceKind; 9 | use crate::block::material::Material; 10 | use crate::geom::{Face, BoundingBox}; 11 | use crate::world::{World, Light}; 12 | use crate::block; 13 | 14 | use super::{Entity, LivingKind, Base}; 15 | 16 | 17 | /// Internal macro to make a refutable pattern assignment that just panic if refuted. 18 | macro_rules! let_expect { 19 | ( $pat:pat = $expr:expr ) => { 20 | #[allow(irrefutable_let_patterns)] 21 | let $pat = $expr else { 22 | unreachable!("invalid argument for this function"); 23 | }; 24 | }; 25 | } 26 | 27 | pub(super) use let_expect as let_expect; 28 | 29 | 30 | // Thread local variables internally used to reduce allocation overhead. 31 | thread_local! { 32 | /// Temporary entity id storage. 33 | pub(super) static ENTITY_ID: RefCell> = const { RefCell::new(Vec::new()) }; 34 | /// Temporary bounding boxes storage. 35 | pub(super) static BOUNDING_BOX: RefCell> = const { RefCell::new(Vec::new()) }; 36 | } 37 | 38 | /// Calculate the eye position of the given entity. 39 | pub fn calc_eye_pos(base: &Base) -> DVec3 { 40 | let mut pos = base.pos; 41 | pos.y += base.eye_height as f64; 42 | pos 43 | } 44 | 45 | /// Return true if the given bounding box is colliding with any fluid (given material). 46 | pub fn has_fluids_colliding(world: &World, bb: BoundingBox, material: Material) -> bool { 47 | debug_assert!(material.is_fluid()); 48 | world.iter_blocks_in_box(bb) 49 | .filter(|&(_, block, _)| block::material::get_material(block) == material) 50 | .any(|(pos, _, metadata)| { 51 | let dist = block::fluid::get_actual_distance(metadata); 52 | let height = 1.0 - dist as f64 / 8.0; 53 | pos.y as f64 + height >= bb.min.y 54 | }) 55 | } 56 | 57 | /// Calculate the velocity of a fluid at given position, this depends on neighbor blocks. 58 | /// This calculation will only take the given material into account, this material should 59 | /// be a fluid material (water/lava), and the given metadata should be the one of the 60 | /// current block the the position. 61 | pub fn calc_fluid_vel(world: &World, pos: IVec3, material: Material, metadata: u8) -> DVec3 { 62 | 63 | debug_assert!(material.is_fluid()); 64 | 65 | let distance = block::fluid::get_actual_distance(metadata); 66 | let mut vel = DVec3::ZERO; 67 | 68 | for face in Face::HORIZONTAL { 69 | 70 | let face_delta = face.delta(); 71 | let face_pos = pos + face_delta; 72 | let (face_block, face_metadata) = world.get_block(face_pos).unwrap_or_default(); 73 | let face_material = block::material::get_material(face_block); 74 | 75 | if face_material == material { 76 | let face_distance = block::fluid::get_actual_distance(face_metadata); 77 | let delta = face_distance as i32 - distance as i32; 78 | vel += (face_delta * delta).as_dvec3(); 79 | } else if !face_material.is_solid() { 80 | let below_pos = face_pos - IVec3::Y; 81 | let (below_block, below_metadata) = world.get_block(below_pos).unwrap_or_default(); 82 | let below_material = block::material::get_material(below_block); 83 | if below_material == material { 84 | let below_distance = block::fluid::get_actual_distance(below_metadata); 85 | let delta = below_distance as i32 - (distance as i32 - 8); 86 | vel += (face_delta * delta).as_dvec3(); 87 | } 88 | } 89 | 90 | } 91 | 92 | // TODO: Things with falling water. 93 | 94 | vel.normalize() 95 | 96 | } 97 | 98 | /// Calculate the light levels for an entity given its base component. 99 | pub fn get_entity_light(world: &World, base: &Base) -> Light { 100 | let mut check_pos = base.bb.min; 101 | check_pos.y += base.bb.size_y() * 0.66; 102 | world.get_light(check_pos.floor().as_ivec3()) 103 | } 104 | 105 | /// Find a the closest player entity (as defined in [`World`]) within the given radius. 106 | pub fn find_closest_player_entity(world: &World, center: DVec3, max_dist: f64) -> Option<(u32, &Entity, f64)> { 107 | let max_dist_sq = max_dist.powi(2); 108 | world.iter_player_entities() 109 | .map(|(entity_id, entity)| (entity_id, entity, entity.0.pos.distance_squared(center))) 110 | .filter(|&(_, _, dist_sq)| dist_sq <= max_dist_sq) 111 | .min_by(|(_, _, a), (_, _, b)| a.total_cmp(b)) 112 | .map(|(entity_id, entity, dist_sq)| (entity_id, entity, dist_sq.sqrt())) 113 | } 114 | 115 | /// Modify the look angles of this entity, limited to the given step. 116 | /// We need to call this function many time to reach the desired look. 117 | pub fn update_look_by_step(base: &mut Base, look: Vec2, step: Vec2) { 118 | 119 | let look_norm = Vec2 { 120 | // Yaw can be normalized between 0 and tau 121 | x: look.x.rem_euclid(std::f32::consts::TAU), 122 | // Pitch however is not normalized. 123 | y: look.y, 124 | }; 125 | 126 | base.look += look_norm.sub(base.look).min(step); 127 | 128 | } 129 | 130 | /// Modify the look angles to point to a given target step by step. The eye height is 131 | /// included in the calculation in order to make the head looking at target. 132 | pub fn update_look_at_by_step(base: &mut Base, target: DVec3, step: Vec2) { 133 | let delta = target - calc_eye_pos(base); 134 | let yaw = f64::atan2(delta.z, delta.x) as f32 - std::f32::consts::FRAC_PI_2; 135 | let pitch = -f64::atan2(delta.y, delta.xz().length()) as f32; 136 | update_look_by_step(base, Vec2::new(yaw, pitch), step); 137 | } 138 | 139 | /// Almost the same as [`update_look_at_by_step`] but the target is another entity base, 140 | /// this function will make the entity look at the eyes of the target one. 141 | pub fn update_look_at_entity_by_step(base: &mut Base, target_base: &Base, step: Vec2) { 142 | update_look_at_by_step(base, calc_eye_pos(target_base), step); 143 | } 144 | 145 | /// Apply knock back to this entity's velocity. 146 | pub fn update_knock_back(base: &mut Base, dir: DVec3) { 147 | 148 | let mut accel = dir.normalize_or_zero(); 149 | accel.y -= 1.0; 150 | 151 | base.vel /= 2.0; 152 | base.vel -= accel * 0.4; 153 | base.vel.y = base.vel.y.min(0.4); 154 | 155 | } 156 | 157 | /// Return true if the entity can eye track the target entity, this use ray tracing. 158 | pub fn can_eye_track(world: &World, base: &Base, target_base: &Base) -> bool { 159 | let origin = calc_eye_pos(base); 160 | let ray = calc_eye_pos(target_base) - origin; 161 | world.ray_trace_blocks(origin, ray, RayTraceKind::Overlay).is_none() 162 | } 163 | 164 | /// Get the path weight function for the given living entity kind. 165 | pub fn path_weight_func(living_kind: &LivingKind) -> fn(&World, IVec3) -> f32 { 166 | match living_kind { 167 | LivingKind::Pig(_) | 168 | LivingKind::Chicken(_) | 169 | LivingKind::Cow(_) | 170 | LivingKind::Sheep(_) | 171 | LivingKind::Wolf(_) => path_weight_animal, 172 | LivingKind::Creeper(_) | 173 | LivingKind::PigZombie(_) | 174 | LivingKind::Skeleton(_) | 175 | LivingKind::Spider(_) | 176 | LivingKind::Zombie(_) => path_weight_mob, 177 | LivingKind::Giant(_) => path_weight_giant, 178 | // We should not match other entities but we never known... 179 | _ => path_weight_default, 180 | } 181 | } 182 | 183 | /// Path weight function for animals. 184 | fn path_weight_animal(world: &World, pos: IVec3) -> f32 { 185 | if world.is_block(pos - IVec3::Y, block::GRASS) { 186 | 10.0 187 | } else { 188 | world.get_light(pos).brightness() - 0.5 189 | } 190 | } 191 | 192 | /// Path weight function for mobs. 193 | fn path_weight_mob(world: &World, pos: IVec3) -> f32 { 194 | 0.5 - world.get_light(pos).brightness() 195 | } 196 | 197 | /// Path weight function for Giant. 198 | fn path_weight_giant(world: &World, pos: IVec3) -> f32 { 199 | world.get_light(pos).brightness() - 0.5 200 | } 201 | 202 | /// Path weight function by default. 203 | fn path_weight_default(_world: &World, _pos: IVec3) -> f32 { 204 | 0.0 205 | } 206 | -------------------------------------------------------------------------------- /mc173/src/entity/tick_attack.rs: -------------------------------------------------------------------------------- 1 | //! Tick entity attack from AI. 2 | 3 | use glam::{Vec3Swizzles, DVec3}; 4 | 5 | use crate::entity::{Hurt, Arrow}; 6 | use crate::world::{World, Event, EntityEvent}; 7 | 8 | use super::{Entity, BaseKind, LivingKind}; 9 | use super::common::{self, let_expect}; 10 | 11 | 12 | /// Tick an attack from the entity to its targeted entity. The targeted entity id is given 13 | /// as argument and the entity is guaranteed to be present in the world as living entity. 14 | /// 15 | /// REF: EntityCreature::attackEntity 16 | pub(super) fn tick_attack(world: &mut World, id: u32, entity: &mut Entity, target_id: u32, dist_squared: f64, eye_track: bool, should_strafe: &mut bool) { 17 | match entity { 18 | Entity(_, BaseKind::Living(_, LivingKind::Spider(_))) => tick_spider_attack(world, id, entity, target_id, dist_squared, eye_track, should_strafe), 19 | Entity(_, BaseKind::Living(_, LivingKind::Creeper(_))) => tick_creeper_attack(world, id, entity, target_id, dist_squared, eye_track, should_strafe), 20 | Entity(_, BaseKind::Living(_, LivingKind::Skeleton(_))) => tick_skeleton_attack(world, id, entity, target_id, dist_squared, eye_track, should_strafe), 21 | Entity(_, BaseKind::Living(_, _)) => tick_mob_attack(world, id, entity, target_id, dist_squared, eye_track, should_strafe), 22 | _ => unreachable!("expected a living entity for this function") 23 | } 24 | } 25 | 26 | /// REF: EntityMob::attackEntity 27 | fn tick_mob_attack(world: &mut World, id: u32, entity: &mut Entity, target_id: u32, dist_squared: f64, eye_track: bool, _should_strafe: &mut bool) { 28 | 29 | /// Maximum distance for the mob to attack. 30 | const MAX_DIST_SQUARED: f64 = 2.0 * 2.0; 31 | 32 | let_expect!(Entity(base, BaseKind::Living(living, living_kind)) = entity); 33 | 34 | living.attack_time = living.attack_time.saturating_sub(1); 35 | if eye_track && living.attack_time == 0 && dist_squared < MAX_DIST_SQUARED { 36 | 37 | let Some(Entity(target_base, BaseKind::Living(_, _))) = world.get_entity_mut(target_id) else { 38 | panic!("target entity should exists"); 39 | }; 40 | 41 | if base.bb.intersects_y(target_base.bb) { 42 | 43 | let attack_damage = match living_kind { 44 | LivingKind::Giant(_) => 50, 45 | LivingKind::PigZombie(_) => 5, 46 | LivingKind::Zombie(_) => 5, 47 | _ => 2, 48 | }; 49 | 50 | living.attack_time = 20; 51 | 52 | target_base.hurt.push(Hurt { 53 | damage: attack_damage, 54 | origin_id: Some(id), 55 | }); 56 | 57 | } 58 | 59 | } 60 | 61 | } 62 | 63 | /// REF: EntitySpider::attackEntity 64 | fn tick_spider_attack(world: &mut World, id: u32, entity: &mut Entity, target_id: u32, dist_squared: f64, eye_track: bool, should_strafe: &mut bool) { 65 | 66 | /// Minimum distance from a player to trigger a climb of the spider. 67 | const MIN_DIST_SQUARED: f64 = 2.0 * 2.0; 68 | /// Maximum distance from a player to trigger a climb of the spider. 69 | const MAX_DIST_SQUARED: f64 = 6.0 * 6.0; 70 | 71 | let_expect!(Entity(base, BaseKind::Living(living, LivingKind::Spider(_))) = entity); 72 | 73 | // If the brightness has changed, there if 1% chance to loose target. 74 | if common::get_entity_light(world, base).brightness() > 0.5 && base.rand.next_int_bounded(100) == 0 { 75 | // Loose target because it's too bright. 76 | living.attack_target = None; 77 | } else if dist_squared > MIN_DIST_SQUARED && dist_squared < MAX_DIST_SQUARED && base.rand.next_int_bounded(10) == 0 { 78 | // If the target is in certain range, there is 10% chance of climbing. 79 | if base.on_ground { 80 | 81 | // Unwrap should be safe because target id should exists at this point. 82 | let Entity(target_base, _) = world.get_entity(target_id).unwrap(); 83 | 84 | let delta = target_base.pos.xz() - base.pos.xz(); 85 | let h_dist = delta.length(); 86 | let h_vel = delta / h_dist * 0.5 * 0.8 + base.vel.xz() * 0.2; 87 | base.vel = DVec3::new(h_vel.x, 0.4, h_vel.y); 88 | 89 | } 90 | } else { 91 | // Fallthrough to direct attack logic... 92 | tick_mob_attack(world, id, entity, target_id, dist_squared, eye_track, should_strafe) 93 | } 94 | 95 | } 96 | 97 | /// REF: EntityCreeper::attackEntity 98 | fn tick_creeper_attack(world: &mut World, id: u32, entity: &mut Entity, _target_id: u32, dist_squared: f64, eye_track: bool, _should_strafe: &mut bool) { 99 | 100 | /// Minimum distance from a player to trigger a climb of the spider. 101 | const IDLE_MAX_DIST_SQUARED: f64 = 3.0 * 3.0; 102 | /// Maximum distance from a player to trigger a climb of the spider. 103 | const IGNITED_MAX_DIST_SQUARED: f64 = 7.0 * 7.0; 104 | 105 | let_expect!(Entity(base, BaseKind::Living(_, LivingKind::Creeper(creeper))) = entity); 106 | 107 | // Check if the creeper should be ignited depending on its current state. 108 | let ignited = 109 | eye_track && 110 | (creeper.ignited_time.is_none() && dist_squared < IDLE_MAX_DIST_SQUARED) || 111 | (creeper.ignited_time.is_some() && dist_squared < IGNITED_MAX_DIST_SQUARED); 112 | 113 | if ignited { 114 | 115 | if creeper.ignited_time.is_none() { 116 | world.push_event(Event::Entity { id, inner: EntityEvent::Metadata }); 117 | } 118 | 119 | let ignited_time = creeper.ignited_time.unwrap_or(0) + 1; 120 | creeper.ignited_time = Some(ignited_time); 121 | 122 | if ignited_time >= 30 { 123 | 124 | // Kill the creeper and return none in order to loose focus on the entity. 125 | world.remove_entity(id, "creeper explode"); 126 | 127 | if creeper.powered { 128 | world.explode(base.pos, 6.0, false, Some(id)); 129 | } else { 130 | world.explode(base.pos, 3.0, false, Some(id)); 131 | } 132 | 133 | } 134 | 135 | } else { 136 | 137 | if creeper.ignited_time.is_some() { 138 | world.push_event(Event::Entity { id, inner: EntityEvent::Metadata }); 139 | creeper.ignited_time = None; 140 | } 141 | 142 | } 143 | 144 | } 145 | 146 | /// REF: EntitySkeleton::attackEntity 147 | fn tick_skeleton_attack(world: &mut World, id: u32, entity: &mut Entity, target_id: u32, dist_squared: f64, eye_track: bool, should_strafe: &mut bool) { 148 | 149 | const MAX_DIST_SQUARED: f64 = 10.0 * 10.0; 150 | 151 | if eye_track && dist_squared < MAX_DIST_SQUARED { 152 | 153 | let_expect!(Entity(base, BaseKind::Living(living, LivingKind::Skeleton(_))) = entity); 154 | let Entity(target_base, _) = world.get_entity(target_id).unwrap(); 155 | 156 | living.attack_time = living.attack_time.saturating_sub(1); 157 | if living.attack_time == 0 { 158 | 159 | living.attack_time = 30; 160 | 161 | let eye_pos = common::calc_eye_pos(base); 162 | let target_eye_pos = common::calc_eye_pos(target_base); 163 | 164 | let arrow = Arrow::new_with(|arrow_base, arrow_projectile, arrow| { 165 | 166 | let mut dir = target_eye_pos - eye_pos; 167 | dir.y += dir.xz().length() * 0.2; 168 | let dir = dir.normalize_or_zero(); 169 | 170 | arrow_base.pos = eye_pos + dir * DVec3::new(1.0, 0.0, 1.0); 171 | arrow_base.look = base.look; 172 | 173 | arrow_base.vel = dir; 174 | arrow_base.vel += arrow_base.rand.next_gaussian_vec() * 0.0075 * 12.0; 175 | arrow_base.vel *= 0.6; 176 | 177 | arrow_projectile.owner_id = Some(id); 178 | arrow.from_player = false; 179 | 180 | }); 181 | 182 | world.spawn_entity(arrow); 183 | 184 | } 185 | 186 | // TODO: Look toward target 187 | *should_strafe = true; 188 | 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /mc173/src/gen/dungeon.rs: -------------------------------------------------------------------------------- 1 | //! Dungeon generator. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block_entity::spawner::SpawnerBlockEntity; 6 | use crate::block_entity::chest::ChestBlockEntity; 7 | use crate::block_entity::BlockEntity; 8 | use crate::item::{ItemStack, self}; 9 | use crate::entity::EntityKind; 10 | use crate::java::JavaRandom; 11 | use crate::world::World; 12 | use crate::geom::Face; 13 | use crate::block; 14 | 15 | use super::FeatureGenerator; 16 | 17 | 18 | /// A generator for mob spawner dungeon. 19 | pub struct DungeonGenerator {} 20 | 21 | impl DungeonGenerator { 22 | pub fn new() -> Self { 23 | Self {} 24 | } 25 | } 26 | 27 | impl DungeonGenerator { 28 | 29 | fn gen_chest_stack(&self, rand: &mut JavaRandom) -> ItemStack { 30 | match rand.next_int_bounded(11) { 31 | 0 => ItemStack::new(item::SADDLE, 0), 32 | 1 => ItemStack::new_sized(item::IRON_INGOT, 0, rand.next_int_bounded(4) as u16 + 1), 33 | 2 => ItemStack::new(item::BREAD, 0), 34 | 3 => ItemStack::new(item::BREAD, 0), 35 | 4 => ItemStack::new_sized(item::GUNPOWDER, 0, rand.next_int_bounded(4) as u16 + 1), 36 | 5 => ItemStack::new_sized(item::STRING, 0, rand.next_int_bounded(4) as u16 + 1), 37 | 6 => ItemStack::new(item::BUCKET, 0), 38 | 7 if rand.next_int_bounded(100) == 0 => 39 | ItemStack::new(item::GOLD_APPLE, 0), 40 | 8 if rand.next_int_bounded(2) == 0 => 41 | ItemStack::new_sized(item::REDSTONE, 0, rand.next_int_bounded(4) as u16 + 1), 42 | 9 if rand.next_int_bounded(10) == 0 => match rand.next_int_bounded(2) { 43 | 0 => ItemStack::new(item::RECORD_13, 0), 44 | 1 => ItemStack::new(item::RECORD_CAT, 0), 45 | _ => unreachable!(), 46 | } 47 | 10 => ItemStack::new(item::DYE, 3), 48 | _ => ItemStack::EMPTY, 49 | } 50 | } 51 | 52 | fn gen_spawner_entity(&self, rand: &mut JavaRandom) -> EntityKind { 53 | match rand.next_int_bounded(4) { 54 | 0 => EntityKind::Skeleton, 55 | 1 | 2 => EntityKind::Zombie, 56 | 3 => EntityKind::Spider, 57 | _ => unreachable!() 58 | } 59 | } 60 | 61 | } 62 | 63 | impl FeatureGenerator for DungeonGenerator { 64 | 65 | fn generate(&mut self, world: &mut World, pos: IVec3, rand: &mut JavaRandom) -> bool { 66 | 67 | let x_radius = rand.next_int_bounded(2) + 2; 68 | let z_radius = rand.next_int_bounded(2) + 2; 69 | let height = 3; 70 | let mut air_count = 0usize; 71 | 72 | let start = pos - IVec3::new(x_radius + 1, 1, x_radius + 1); 73 | let end = pos + IVec3::new(x_radius + 1, height + 1, x_radius + 1); 74 | 75 | for x in start.x..=end.x { 76 | for y in start.y..=end.y { 77 | for z in start.z..=end.z { 78 | 79 | let check_pos = IVec3::new(x, y, z); 80 | let check_material = world.get_block_material(check_pos); 81 | 82 | if y == start.y && !check_material.is_solid() { 83 | return false; 84 | } else if y == end.y && !check_material.is_solid() { 85 | return false; 86 | } else if y == pos.y && (x == start.x || x == end.x || z == start.z || z == end.z) { 87 | if world.is_block_air(check_pos) && world.is_block_air(check_pos + IVec3::Y) { 88 | air_count += 1; 89 | } 90 | } 91 | 92 | } 93 | } 94 | } 95 | 96 | if air_count < 1 || air_count > 5 { 97 | return false; 98 | } 99 | 100 | // Carve the dungeon and fill walls. 101 | for x in start.x..=end.x { 102 | for y in (start.y..end.y).rev() { 103 | for z in start.z..=end.z { 104 | 105 | // PARITY: Notchian impl actually use set_block_notify. 106 | 107 | let carve_pos = IVec3::new(x, y, z); 108 | if x != start.x && y != start.y && z != start.z && x != end.x && z != end.z { 109 | world.set_block(carve_pos, block::AIR, 0); 110 | } else if y >= 0 && !world.get_block_material(carve_pos - IVec3::Y).is_solid() { 111 | world.set_block(carve_pos, block::AIR, 0); 112 | } else if world.get_block_material(carve_pos).is_solid() { 113 | if y == start.y && rand.next_int_bounded(4) != 0 { 114 | world.set_block(carve_pos, block::MOSSY_COBBLESTONE, 0); 115 | } else { 116 | world.set_block(carve_pos, block::COBBLESTONE, 0); 117 | } 118 | } 119 | 120 | } 121 | } 122 | } 123 | 124 | // Place chests. 125 | for _ in 0..2 { 126 | 127 | 'chest_try: for _ in 0..3 { 128 | 129 | let chest_pos = pos + IVec3 { 130 | x: rand.next_int_bounded(x_radius * 2 + 1) - x_radius, 131 | y: 0, 132 | z: rand.next_int_bounded(z_radius * 2 + 1) - z_radius, 133 | }; 134 | 135 | if world.is_block_air(pos) { 136 | 137 | let mut solid_count = 0usize; 138 | for face in Face::HORIZONTAL { 139 | if world.get_block_material(chest_pos + face.delta()).is_solid() { 140 | solid_count += 1; 141 | if solid_count > 1 { 142 | continue 'chest_try; 143 | } 144 | } 145 | } 146 | 147 | if solid_count == 0 { 148 | continue 'chest_try; 149 | } 150 | 151 | let mut chest = ChestBlockEntity::default(); 152 | 153 | // Pick 8 random items. 154 | for _ in 0..8 { 155 | 156 | let stack = self.gen_chest_stack(rand); 157 | if !stack.is_empty() { 158 | *rand.next_choice_mut(&mut chest.inv[..]) = stack; 159 | } 160 | 161 | } 162 | 163 | world.set_block(chest_pos, block::CHEST, 0); 164 | world.set_block_entity(chest_pos, BlockEntity::Chest(chest)); 165 | break; 166 | 167 | } 168 | 169 | } 170 | 171 | } 172 | 173 | let mut spawner = SpawnerBlockEntity::default(); 174 | spawner.entity_kind = self.gen_spawner_entity(rand); 175 | world.set_block(pos, block::SPAWNER, 0); 176 | world.set_block_entity(pos, BlockEntity::Spawner(spawner)); 177 | 178 | true 179 | 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /mc173/src/gen/liquid.rs: -------------------------------------------------------------------------------- 1 | //! Liquids generation. 2 | 3 | use glam::{IVec3, DVec3}; 4 | 5 | use crate::java::JavaRandom; 6 | use crate::world::World; 7 | use crate::geom::Face; 8 | use crate::block; 9 | 10 | use super::FeatureGenerator; 11 | 12 | 13 | /// A generator for lakes. 14 | pub struct LakeGenerator { 15 | fluid_id: u8, 16 | } 17 | 18 | impl LakeGenerator { 19 | 20 | /// Create a new lake generator for the given block id. 21 | #[inline] 22 | pub fn new(fluid_id: u8) -> Self { 23 | Self { fluid_id, } 24 | } 25 | 26 | } 27 | 28 | impl FeatureGenerator for LakeGenerator { 29 | 30 | fn generate(&mut self, world: &mut World, mut pos: IVec3, rand: &mut JavaRandom) -> bool { 31 | 32 | // Lake have a maximum size of 16x8x16, so we subtract half. 33 | pos -= IVec3::new(8, 0, 8); 34 | while pos.y > 0 && world.is_block_air(pos) { 35 | pos.y -= 1; 36 | } 37 | pos.y -= 4; 38 | 39 | // [X][Z][Y] 40 | let mut fill = Box::new([[[false; 8]; 16]; 16]); 41 | 42 | let count = rand.next_int_bounded(4) + 4; 43 | for _ in 0..count { 44 | 45 | let a = rand.next_double_vec() * 46 | DVec3::new(6.0, 4.0, 6.0) + 47 | DVec3::new(3.0, 2.0, 3.0); 48 | 49 | let b = rand.next_double_vec() * 50 | (DVec3::new(16.0, 8.0, 16.0) - a - DVec3::new(2.0, 4.0, 2.0)) + 51 | DVec3::new(1.0, 2.0, 1.0) + a / 2.0; 52 | 53 | let a = a / 2.0; 54 | 55 | for dx in 1..15 { 56 | for dz in 1..15 { 57 | for dy in 1..7 { 58 | let dist = (DVec3::new(dx as f64, dy as f64, dz as f64) - b) / a; 59 | if dist.length_squared() < 1.0 { 60 | fill[dx][dz][dy] = true; 61 | } 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | for dx in 0..16 { 69 | for dz in 0..16 { 70 | for dy in 0..8 { 71 | 72 | let filled = !fill[dx][dz][dy] && ( 73 | dx < 15 && fill[dx + 1][dz][dy] || 74 | dx > 0 && fill[dx - 1][dz][dy] || 75 | dz < 15 && fill[dx][dz + 1][dy] || 76 | dz > 0 && fill[dx][dz - 1][dy] || 77 | dy < 7 && fill[dx][dz][dy + 1] || 78 | dy > 0 && fill[dx][dz][dy - 1] 79 | ); 80 | 81 | if filled { 82 | let check_pos = pos + IVec3::new(dx as i32, dy as i32, dz as i32); 83 | let check_id = world.get_block(check_pos).map(|(id, _)| id).unwrap_or(block::AIR); 84 | let check_material = block::material::get_material(check_id); 85 | if dy >= 4 && check_material.is_fluid() { 86 | return false; 87 | } else if dy < 4 && !check_material.is_solid() && check_id != self.fluid_id { 88 | return false; 89 | } 90 | } 91 | 92 | } 93 | } 94 | } 95 | 96 | for dx in 0..16 { 97 | for dz in 0..16 { 98 | for dy in 0..8 { 99 | if fill[dx][dz][dy] { 100 | let place_pos = pos + IVec3::new(dx as i32, dy as i32, dz as i32); 101 | world.set_block(place_pos, if dy >= 4 { block::AIR } else { self.fluid_id }, 0); 102 | } 103 | } 104 | } 105 | } 106 | 107 | for dx in 0..16 { 108 | for dz in 0..16 { 109 | for dy in 4..8 { 110 | if fill[dx][dz][dy] { 111 | let check_pos = pos + IVec3::new(dx as i32, dy as i32 - 1, dz as i32); 112 | if world.is_block(check_pos, block::DIRT) { 113 | if world.get_light(check_pos).sky > 0 { 114 | world.set_block(check_pos, block::GRASS, 0); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | if let block::LAVA_STILL | block::LAVA_MOVING = self.fluid_id { 123 | for dx in 0..16 { 124 | for dz in 0..16 { 125 | for dy in 0..8 { 126 | 127 | let filled = !fill[dx][dz][dy] && ( 128 | dx < 15 && fill[dx + 1][dz][dy] || 129 | dx > 0 && fill[dx - 1][dz][dy] || 130 | dz < 15 && fill[dx][dz + 1][dy] || 131 | dz > 0 && fill[dx][dz - 1][dy] || 132 | dy < 7 && fill[dx][dz][dy + 1] || 133 | dy > 0 && fill[dx][dz][dy - 1] 134 | ); 135 | 136 | if filled && (dy < 4 || rand.next_int_bounded(2) != 0) { 137 | let place_pos = pos + IVec3::new(dx as i32, dy as i32, dz as i32); 138 | if world.get_block_material(place_pos).is_solid() { 139 | world.set_block(place_pos, block::STONE, 0); 140 | } 141 | } 142 | 143 | } 144 | } 145 | } 146 | } 147 | 148 | true 149 | 150 | } 151 | 152 | } 153 | 154 | 155 | /// A generator for single liquid blocks. 156 | pub struct LiquidGenerator { 157 | fluid_id: u8, 158 | } 159 | 160 | impl LiquidGenerator { 161 | 162 | /// Create a new liquid generator for the given block id. 163 | #[inline] 164 | pub fn new(fluid_id: u8) -> Self { 165 | Self { fluid_id, } 166 | } 167 | 168 | } 169 | 170 | impl FeatureGenerator for LiquidGenerator { 171 | 172 | fn generate(&mut self, world: &mut World, pos: IVec3, _rand: &mut JavaRandom) -> bool { 173 | 174 | if !world.is_block(pos + IVec3::Y, block::STONE) { 175 | return false; 176 | } else if !world.is_block(pos - IVec3::Y, block::STONE) { 177 | return false; 178 | } else if !matches!(world.get_block(pos), Some((block::AIR | block::STONE, _))) { 179 | return false; 180 | } 181 | 182 | let mut stone_count = 0; 183 | let mut air_count = 0; 184 | 185 | for face in Face::HORIZONTAL { 186 | match world.get_block(pos + face.delta()) { 187 | Some((block::STONE, _)) => stone_count += 1, 188 | None | Some((block::AIR, _)) => air_count += 1, 189 | _ => {} 190 | } 191 | } 192 | 193 | if stone_count == 3 && air_count == 1 { 194 | world.set_block(pos, self.fluid_id, 0); 195 | } 196 | 197 | true 198 | 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /mc173/src/gen/math.rs: -------------------------------------------------------------------------------- 1 | //! Math utilities specialized for Minecraft, such as sin/cos precomputed tables, in order 2 | //! to get the best parity with Minecraft generation. 3 | 4 | #[allow(clippy::approx_constant)] 5 | const JAVA_PI: f64 = 3.141592653589793; 6 | 7 | 8 | /// We internally do not use a precomputed table as in Notchian implementation. For now 9 | /// we recompute the table value on each access. 10 | #[inline(always)] 11 | fn mc_sin_table(index: u16) -> f32 { 12 | (index as f64 * JAVA_PI * 2.0 / 65536.0).sin() as f32 13 | } 14 | 15 | #[inline] 16 | fn mc_sin(x: f32) -> f32 { 17 | mc_sin_table((x * 10430.378) as i32 as u16) 18 | } 19 | 20 | #[inline] 21 | fn mc_cos(x: f32) -> f32 { 22 | mc_sin_table((x * 10430.378 + 16384.0) as i32 as u16) 23 | } 24 | 25 | 26 | /// An extension trait to numbers. 27 | pub trait MinecraftMath: Copy { 28 | 29 | const MC_PI: Self; 30 | 31 | /// Computes the sine of a number (in radians) with parity with Notchian impl. 32 | fn mc_sin(self) -> Self; 33 | 34 | /// Computes the cosine of a number (in radians) with parity with Notchian impl. 35 | fn mc_cos(self) -> Self; 36 | 37 | /// Same as [`f32::sin_cos`] but for Notchian impl. 38 | #[inline] 39 | fn mc_sin_cos(self) -> (Self, Self) { 40 | (self.mc_sin(), self.mc_cos()) 41 | } 42 | 43 | } 44 | 45 | impl MinecraftMath for f32 { 46 | 47 | const MC_PI: Self = JAVA_PI as f32; 48 | 49 | #[inline] 50 | fn mc_sin(self) -> Self { 51 | mc_sin(self) 52 | } 53 | 54 | #[inline] 55 | fn mc_cos(self) -> Self { 56 | mc_cos(self) 57 | } 58 | 59 | } 60 | 61 | impl MinecraftMath for f64 { 62 | 63 | const MC_PI: Self = JAVA_PI; 64 | 65 | #[inline] 66 | fn mc_sin(self) -> Self { 67 | mc_sin(self as f32) as f64 68 | } 69 | 70 | #[inline] 71 | fn mc_cos(self) -> Self { 72 | mc_cos(self as f32) as f64 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /mc173/src/gen/mod.rs: -------------------------------------------------------------------------------- 1 | //! World generation module. 2 | //! 3 | //! PARITY: The parity of world generation is really hard to get fully exact, mostly 4 | //! because Minecraft itself is not at parity with itself! The world generation scheduling 5 | //! has a huge impact on chunk populating, so this implementation is on parity but it may 6 | //! not give exact same world on each generation, just like Minecraft. Terrain however, 7 | //! should be exactly the same on same run. 8 | 9 | use glam::IVec3; 10 | 11 | use crate::java::JavaRandom; 12 | use crate::chunk::Chunk; 13 | use crate::world::World; 14 | 15 | // World gen-specific mathematic functions. 16 | pub mod math; 17 | pub mod noise; 18 | 19 | // Feature generators. 20 | pub mod dungeon; 21 | pub mod plant; 22 | pub mod vein; 23 | pub mod liquid; 24 | pub mod tree; 25 | 26 | // Chunks carvers. 27 | pub mod cave; 28 | 29 | // World generators. 30 | mod overworld; 31 | pub use overworld::OverworldGenerator; 32 | 33 | 34 | /// A trait for all chunk generators, a chunk generator is immutable, if any mutable 35 | /// state needs to be stored, the `State` associated type can be used. 36 | pub trait ChunkGenerator { 37 | 38 | /// Type of the cache that is only owned by a single worker. 39 | type State: Default; 40 | 41 | /// Generate only the chunk biomes, this may never be called and this is not called 42 | /// before [`gen_terrain`](Self::gen_terrain). 43 | fn gen_biomes(&self, cx: i32, cz: i32, chunk: &mut Chunk, state: &mut Self::State); 44 | 45 | /// Generate the given chunk's terrain, this should also generate the biomes 46 | /// associated to the terrain generation. The separate method 47 | /// [`gen_biomes`](Self::gen_biomes) is not called before that function. 48 | fn gen_terrain(&self, cx: i32, cz: i32, chunk: &mut Chunk, state: &mut Self::State); 49 | 50 | /// Populate a chunk that is present in a world, note that this world is internal 51 | /// to the generator, this chunk will then be transferred to the real world when 52 | /// done. Populate usually applies with an offset of 8 blocks into the chunk with 53 | /// a 16x16 populate area, this means that neighbor chunks affected are also 54 | /// guaranteed to be loaded. 55 | fn gen_features(&self, cx: i32, cz: i32, world: &mut World, state: &mut Self::State); 56 | 57 | } 58 | 59 | 60 | /// A trait common to all feature generators. 61 | pub trait FeatureGenerator { 62 | 63 | /// Generate the feature at the given position in the world with given RNG. 64 | fn generate(&mut self, world: &mut World, pos: IVec3, rand: &mut JavaRandom) -> bool; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /mc173/src/gen/nether.rs: -------------------------------------------------------------------------------- 1 | //! Nether chunk generator. 2 | 3 | 4 | -------------------------------------------------------------------------------- /mc173/src/gen/plant.rs: -------------------------------------------------------------------------------- 1 | //! Plants feature generation. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block::material::Material; 6 | use crate::java::JavaRandom; 7 | use crate::world::World; 8 | use crate::geom::Face; 9 | use crate::block; 10 | 11 | use super::FeatureGenerator; 12 | 13 | 14 | /// A generator for flower patch. 15 | pub struct PlantGenerator { 16 | plant_id: u8, 17 | plant_metadata: u8, 18 | count: u8, 19 | find_ground: bool, 20 | } 21 | 22 | impl PlantGenerator { 23 | 24 | #[inline] 25 | pub fn new(plant_id: u8, plant_metadata: u8, count: u8, find_ground: bool) -> Self { 26 | Self { 27 | plant_id, 28 | plant_metadata, 29 | count, 30 | find_ground, 31 | } 32 | } 33 | 34 | #[inline] 35 | pub fn new_flower(flower_id: u8) -> Self { 36 | Self::new(flower_id, 0, 64, false) 37 | } 38 | 39 | #[inline] 40 | pub fn new_tall_grass(metadata: u8) -> Self { 41 | Self::new(block::TALL_GRASS, metadata, 128, true) 42 | } 43 | 44 | #[inline] 45 | pub fn new_dead_bush() -> Self { 46 | Self::new(block::DEAD_BUSH, 0, 4, true) 47 | } 48 | 49 | } 50 | 51 | impl FeatureGenerator for PlantGenerator { 52 | 53 | fn generate(&mut self, world: &mut World, mut pos: IVec3, rand: &mut JavaRandom) -> bool { 54 | 55 | if self.find_ground { 56 | while pos.y > 0 { 57 | if !matches!(world.get_block(pos), Some((block::AIR | block::LEAVES, _))) { 58 | break; 59 | } 60 | pos.y -= 1; 61 | } 62 | } 63 | 64 | for _ in 0..self.count { 65 | 66 | let place_pos = pos + IVec3 { 67 | x: rand.next_int_bounded(8) - rand.next_int_bounded(8), 68 | y: rand.next_int_bounded(4) - rand.next_int_bounded(4), 69 | z: rand.next_int_bounded(8) - rand.next_int_bounded(8), 70 | }; 71 | 72 | // PARITY: Check parity of "canBlockStay"... 73 | if world.is_block_air(place_pos) && world.can_place_block(place_pos, Face::NegY, self.plant_id) { 74 | world.set_block(place_pos, self.plant_id, self.plant_metadata); 75 | } 76 | 77 | } 78 | 79 | true 80 | 81 | } 82 | 83 | } 84 | 85 | 86 | /// A generator for sugar canes. 87 | pub struct SugarCanesGenerator(()); 88 | 89 | impl SugarCanesGenerator { 90 | #[inline] 91 | pub fn new() -> Self { 92 | Self(()) 93 | } 94 | } 95 | 96 | impl FeatureGenerator for SugarCanesGenerator { 97 | 98 | fn generate(&mut self, world: &mut World, pos: IVec3, rand: &mut JavaRandom) -> bool { 99 | 100 | for _ in 0..20 { 101 | 102 | let place_pos = pos + IVec3 { 103 | x: rand.next_int_bounded(4) - rand.next_int_bounded(4), 104 | y: 0, 105 | z: rand.next_int_bounded(4) - rand.next_int_bounded(4), 106 | }; 107 | 108 | if world.is_block_air(place_pos) { 109 | 110 | for face in Face::HORIZONTAL { 111 | if world.get_block_material(place_pos - IVec3::Y + face.delta()) == Material::Water { 112 | 113 | let v = rand.next_int_bounded(3) + 1; 114 | let height = rand.next_int_bounded(v) + 2; 115 | 116 | // Check that the bottom cane can be placed. 117 | if world.can_place_block(place_pos, Face::NegY, block::SUGAR_CANES) { 118 | for dy in 0..height { 119 | world.set_block(place_pos + IVec3::new(0, dy, 0), block::SUGAR_CANES, 0); 120 | } 121 | } 122 | 123 | } 124 | } 125 | 126 | } 127 | 128 | } 129 | 130 | true 131 | 132 | } 133 | 134 | } 135 | 136 | 137 | /// A generator for pumpkin. 138 | pub struct PumpkinGenerator(()); 139 | 140 | impl PumpkinGenerator { 141 | #[inline] 142 | pub fn new() -> Self { 143 | Self(()) 144 | } 145 | } 146 | 147 | impl FeatureGenerator for PumpkinGenerator { 148 | 149 | fn generate(&mut self, world: &mut World, pos: IVec3, rand: &mut JavaRandom) -> bool { 150 | 151 | for _ in 0..64 { 152 | 153 | let place_pos = pos + IVec3 { 154 | x: rand.next_int_bounded(8) - rand.next_int_bounded(8), 155 | y: rand.next_int_bounded(4) - rand.next_int_bounded(4), 156 | z: rand.next_int_bounded(8) - rand.next_int_bounded(8), 157 | }; 158 | 159 | // PARITY: Check parity of "canBlockStay"... 160 | if world.is_block_air(place_pos) && world.is_block(place_pos - IVec3::Y, block::GRASS) { 161 | world.set_block(place_pos, block::PUMPKIN, rand.next_int_bounded(4) as u8); 162 | } 163 | 164 | } 165 | 166 | true 167 | 168 | } 169 | 170 | } 171 | 172 | 173 | /// A generator for cactus. 174 | pub struct CactusGenerator(()); 175 | 176 | impl CactusGenerator { 177 | #[inline] 178 | pub fn new() -> Self { 179 | Self(()) 180 | } 181 | } 182 | 183 | impl FeatureGenerator for CactusGenerator { 184 | 185 | fn generate(&mut self, world: &mut World, pos: IVec3, rand: &mut JavaRandom) -> bool { 186 | 187 | for _ in 0..10 { 188 | 189 | let place_pos = pos + IVec3 { 190 | x: rand.next_int_bounded(8) - rand.next_int_bounded(8), 191 | y: rand.next_int_bounded(4) - rand.next_int_bounded(4), 192 | z: rand.next_int_bounded(8) - rand.next_int_bounded(8), 193 | }; 194 | 195 | if world.is_block_air(place_pos) { 196 | 197 | let v = rand.next_int_bounded(3) + 1; 198 | let height = rand.next_int_bounded(v) + 1; 199 | 200 | // Check that the bottom cane can be placed. 201 | for dy in 0..height { 202 | if world.can_place_block(place_pos, Face::NegY, block::CACTUS) { 203 | world.set_block(place_pos + IVec3::new(0, dy, 0), block::CACTUS, 0); 204 | } 205 | } 206 | 207 | } 208 | 209 | } 210 | 211 | true 212 | 213 | } 214 | 215 | } -------------------------------------------------------------------------------- /mc173/src/gen/vein.rs: -------------------------------------------------------------------------------- 1 | //! Clay and ore patch feature. 2 | 3 | use glam::{IVec3, DVec3}; 4 | 5 | use crate::java::JavaRandom; 6 | use crate::world::World; 7 | use crate::block; 8 | 9 | use super::math::MinecraftMath; 10 | use super::FeatureGenerator; 11 | 12 | 13 | /// A generator for mob spawner dungeon. 14 | pub struct VeinGenerator { 15 | replace_id: u8, 16 | place_id: u8, 17 | count: u8, 18 | } 19 | 20 | impl VeinGenerator { 21 | 22 | #[inline] 23 | pub fn new(replace_id: u8, place_id: u8, count: u8) -> Self { 24 | Self { 25 | replace_id, 26 | place_id, 27 | count, 28 | } 29 | } 30 | 31 | #[inline] 32 | pub fn new_clay(count: u8) -> Self { 33 | Self::new(block::SAND, block::CLAY, count) 34 | } 35 | 36 | #[inline] 37 | pub fn new_ore(place_id: u8, count: u8) -> Self { 38 | Self::new(block::STONE, place_id, count) 39 | } 40 | 41 | } 42 | 43 | impl FeatureGenerator for VeinGenerator { 44 | 45 | fn generate(&mut self, world: &mut World, pos: IVec3, rand: &mut JavaRandom) -> bool { 46 | 47 | let angle = rand.next_float() * f32::MC_PI; 48 | let (angle_sin, angle_cos) = angle.mc_sin_cos(); 49 | let angle_sin = angle_sin * self.count as f32 / 8.0; 50 | let angle_cos = angle_cos * self.count as f32 / 8.0; 51 | 52 | let line_start = DVec3 { 53 | x: ((pos.x + 8) as f32 + angle_sin) as f64, 54 | y: (pos.y + rand.next_int_bounded(3) + 2) as f64, 55 | z: ((pos.z + 8) as f32 + angle_cos) as f64, 56 | }; 57 | 58 | let line_stop = DVec3 { 59 | x: ((pos.x + 8) as f32 - angle_sin) as f64, 60 | y: (pos.y + rand.next_int_bounded(3) + 2) as f64, 61 | z: ((pos.z + 8) as f32 - angle_cos) as f64, 62 | }; 63 | 64 | for i in 0..=self.count { 65 | 66 | // Interpolation. 67 | let center_pos = line_start + (line_stop - line_start) * i as f64 / self.count as f64; 68 | 69 | let base_size = rand.next_double() * self.count as f64 / 16.0; 70 | let size = ((i as f32 * f32::MC_PI / self.count as f32).mc_sin() + 1.0) as f64 * base_size + 1.0; 71 | let half_size = size / 2.0; 72 | 73 | let start = (center_pos - half_size).floor().as_ivec3(); 74 | let stop = (center_pos + half_size).floor().as_ivec3(); 75 | 76 | for x in start.x..=stop.x { 77 | for z in start.z..=stop.z { 78 | for y in start.y..=stop.y { 79 | 80 | let place_pos = IVec3::new(x, y, z); 81 | let delta = (place_pos.as_dvec3() + 0.5 - center_pos) / half_size; 82 | 83 | if delta.length_squared() < 1.0 { 84 | if world.is_block(place_pos, self.replace_id) { 85 | world.set_block(place_pos, self.place_id, 0); 86 | } 87 | } 88 | 89 | } 90 | } 91 | } 92 | 93 | } 94 | 95 | true 96 | 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /mc173/src/item/attack.rs: -------------------------------------------------------------------------------- 1 | //! Module to query base attack damage of items. 2 | 3 | use crate::item; 4 | 5 | 6 | /// Get base attack damage of an item. 7 | pub fn get_base_damage(item: u16) -> u16 { 8 | 9 | const DIAMOND_DAMAGE: u16 = 3; 10 | const IRON_DAMAGE: u16 = 2; 11 | const STONE_DAMAGE: u16 = 1; 12 | const WOOD_DAMAGE: u16 = 0; 13 | const GOLD_DAMAGE: u16 = 0; 14 | 15 | // Calculate the damage from the item. 16 | match item { 17 | // Sword 18 | item::DIAMOND_SWORD => 4 + DIAMOND_DAMAGE * 2, 19 | item::IRON_SWORD => 4 + IRON_DAMAGE * 2, 20 | item::STONE_SWORD => 4 + STONE_DAMAGE * 2, 21 | item::WOOD_SWORD => 4 + WOOD_DAMAGE * 2, 22 | item::GOLD_SWORD => 4 + GOLD_DAMAGE * 2, 23 | // Axe 24 | item::DIAMOND_AXE => 3 + DIAMOND_DAMAGE, 25 | item::IRON_AXE => 3 + IRON_DAMAGE, 26 | item::STONE_AXE => 3 + STONE_DAMAGE, 27 | item::WOOD_AXE => 3 + WOOD_DAMAGE, 28 | item::GOLD_AXE => 3 + GOLD_DAMAGE, 29 | // Pickaxe 30 | item::DIAMOND_PICKAXE => 2 + DIAMOND_DAMAGE, 31 | item::IRON_PICKAXE => 2 + IRON_DAMAGE, 32 | item::STONE_PICKAXE => 2 + STONE_DAMAGE, 33 | item::WOOD_PICKAXE => 2 + WOOD_DAMAGE, 34 | item::GOLD_PICKAXE => 2 + GOLD_DAMAGE, 35 | // Shovel 36 | item::DIAMOND_SHOVEL => 1 + DIAMOND_DAMAGE, 37 | item::IRON_SHOVEL => 1 + IRON_DAMAGE, 38 | item::STONE_SHOVEL => 1 + STONE_DAMAGE, 39 | item::WOOD_SHOVEL => 1 + WOOD_DAMAGE, 40 | item::GOLD_SHOVEL => 1 + GOLD_DAMAGE, 41 | // All other items make 1 damage. 42 | _ => 1, 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /mc173/src/item/inv.rs: -------------------------------------------------------------------------------- 1 | //! Inventory data structure storing item stacks. 2 | 3 | use std::iter::FusedIterator; 4 | use std::ops::Range; 5 | 6 | use crate::item::ItemStack; 7 | use crate::item; 8 | 9 | 10 | /// An inventory handle is used to assists item insertion into inventory. It also record 11 | /// stack indices that have changed and therefore allows selective events. 12 | pub struct InventoryHandle<'a> { 13 | inv: &'a mut [ItemStack], 14 | changes: u64, 15 | } 16 | 17 | impl<'a> InventoryHandle<'a> { 18 | 19 | /// Construct a new inventory handle to a slice of item stacks. This functions panics 20 | /// if the given slice is bigger than 64 stacks. 21 | pub fn new(inv: &'a mut [ItemStack]) -> Self { 22 | assert!(inv.len() <= 64); 23 | Self { 24 | inv, 25 | changes: 0, 26 | } 27 | } 28 | 29 | /// Get the item stack at the given index. 30 | #[inline] 31 | pub fn get(&self, index: usize) -> ItemStack { 32 | self.inv[index] 33 | } 34 | 35 | /// Set the item stack at the given index. 36 | #[inline] 37 | pub fn set(&mut self, index: usize, stack: ItemStack) { 38 | if self.inv[index] != stack { 39 | self.inv[index] = stack; 40 | self.changes |= 1 << index; 41 | } 42 | } 43 | 44 | /// Add an item to the inventory, starting by the first slots. 45 | /// 46 | /// The given item stack is modified according to the amount of items actually added 47 | /// to the inventory, its size will be set to zero if fully consumed. 48 | pub fn push_front(&mut self, stack: &mut ItemStack) { 49 | self.push(stack, 0..self.inv.len(), false); 50 | } 51 | 52 | /// Add an item to the inventory, starting from the last slots. 53 | /// 54 | /// The given item stack is modified according to the amount of items actually added 55 | /// to the inventory, its size will be set to zero if fully consumed. 56 | pub fn push_back(&mut self, stack: &mut ItemStack) { 57 | self.push(stack, 0..self.inv.len(), true); 58 | } 59 | 60 | /// Same as [`push_front`](Self::push_front), but this work in a slice of inventory. 61 | pub fn push_front_in(&mut self, stack: &mut ItemStack, range: Range) { 62 | self.push(stack, range, false); 63 | } 64 | 65 | /// Same as [`push_back`](Self::push_back), but this work in a slice of inventory. 66 | pub fn push_back_in(&mut self, stack: &mut ItemStack, range: Range) { 67 | self.push(stack, range, true); 68 | } 69 | 70 | /// Add an item to the inventory. The given item stack is modified according to the 71 | /// amount of items actually added to the inventory, its size will be set to zero if 72 | /// fully consumed. 73 | fn push(&mut self, stack: &mut ItemStack, range: Range, back: bool) { 74 | 75 | // Do nothing if stack size is 0 or the item is air. 76 | if stack.is_empty() { 77 | return; 78 | } 79 | 80 | let item = item::from_id(stack.id); 81 | 82 | // Only accumulate of stack size is greater than 1. 83 | if item.max_stack_size > 1 { 84 | 85 | let mut range = range.clone(); 86 | while let Some(index) = if back { range.next_back() } else { range.next() } { 87 | let slot = &mut self.inv[index]; 88 | // If the slot is of the same item and has space left in the stack size. 89 | if slot.size != 0 && slot.id == stack.id && slot.damage == stack.damage && slot.size < item.max_stack_size { 90 | let available = item.max_stack_size - slot.size; 91 | let to_add = available.min(stack.size); 92 | slot.size += to_add; 93 | stack.size -= to_add; 94 | // NOTE: We requires that size must be less than 64, so the index fit 95 | // in the 64 bits of changes integer. 96 | self.changes |= 1 << index; 97 | if stack.size == 0 { 98 | return; 99 | } 100 | } 101 | } 102 | 103 | } 104 | 105 | // If we land here, some items are remaining to insert in the empty slots. 106 | // We can also land here if the item has damage value. We search empty slots. 107 | let mut range = range.clone(); 108 | while let Some(index) = if back { range.next_back() } else { range.next() } { 109 | let slot = &mut self.inv[index]; 110 | if slot.is_empty() { 111 | // We found an empty slot, insert the whole remaining stack size. 112 | *slot = *stack; 113 | stack.size = 0; 114 | self.changes |= 1 << index; 115 | return; 116 | } 117 | } 118 | 119 | } 120 | 121 | /// Test if the given item can be pushed in this inventory. If true is returned, a 122 | /// call to `push_*` function is guaranteed to fully consume the stack. 123 | pub fn can_push(&self, mut stack: ItemStack) -> bool { 124 | 125 | // Do nothing if stack size is 0 or the item is air. 126 | if stack.is_empty() { 127 | return true; 128 | } 129 | 130 | let item = item::from_id(stack.id); 131 | 132 | for slot in &self.inv[..] { 133 | if slot.is_empty() { 134 | return true; 135 | } else if slot.size != 0 && slot.id == stack.id && slot.damage == stack.damage && slot.size < item.max_stack_size { 136 | let available = item.max_stack_size - slot.size; 137 | let to_add = available.min(stack.size); 138 | stack.size -= to_add; 139 | if stack.size == 0 { 140 | return true; 141 | } 142 | } 143 | } 144 | 145 | false 146 | 147 | } 148 | 149 | /// Consume the equivalent of the given item stack, returning true if successful. 150 | pub fn consume(&mut self, stack: ItemStack) -> bool { 151 | 152 | for (index, slot) in self.inv.iter_mut().enumerate() { 153 | if slot.id == stack.id && slot.damage == stack.damage && slot.size >= stack.size { 154 | slot.size -= stack.size; 155 | self.changes |= 1 << index; 156 | return true; 157 | } 158 | } 159 | 160 | false 161 | 162 | } 163 | 164 | /// Get an iterator for changes that happened in this inventory. 165 | pub fn iter_changes(&self) -> ChangesIter { 166 | ChangesIter { 167 | changes: self.changes, 168 | count: 0, 169 | } 170 | } 171 | 172 | } 173 | 174 | 175 | /// An iterator of changes that happened to an inventory. 176 | pub struct ChangesIter { 177 | changes: u64, 178 | count: u8, 179 | } 180 | 181 | impl FusedIterator for ChangesIter { } 182 | impl Iterator for ChangesIter { 183 | 184 | type Item = usize; 185 | 186 | fn next(&mut self) -> Option { 187 | 188 | while self.count < 64 { 189 | let ret = ((self.changes & 1) != 0).then_some(self.count as usize); 190 | self.changes >>= 1; 191 | self.count += 1; 192 | if let Some(ret) = ret { 193 | return Some(ret); 194 | } 195 | } 196 | 197 | None 198 | 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /mc173/src/item/smelt.rs: -------------------------------------------------------------------------------- 1 | //! Item smelting management. 2 | 3 | use crate::block::material::Material; 4 | use crate::item::ItemStack; 5 | use crate::{block, item}; 6 | 7 | 8 | /// Find a smelting recipe output from given input item/damage. 9 | pub fn find_smelting_output(id: u16, damage: u16) -> Option { 10 | for recipe in RECIPES { 11 | if (recipe.input.id, recipe.input.damage) == (id, damage) { 12 | return Some(recipe.output); 13 | } 14 | } 15 | None 16 | } 17 | 18 | /// Get burn time of the given item id, returning 0 if the given item id is not a fuel. 19 | pub fn get_burn_ticks(id: u16) -> u16 { 20 | if let Ok(id) = u8::try_from(id) { 21 | match id { 22 | block::SAPLING => 100, 23 | _ if block::material::get_material(id) == Material::Wood => 300, 24 | _ => 0, 25 | } 26 | } else { 27 | match id { 28 | item::COAL => 1600, 29 | item::LAVA_BUCKET => 20000, 30 | item::STICK => 100, 31 | _ => 0, 32 | } 33 | } 34 | } 35 | 36 | const RECIPES: &'static [Recipe] = &[ 37 | Recipe::new(ItemStack::new_block(block::IRON_ORE, 0), ItemStack::new(item::IRON_INGOT, 0)), 38 | Recipe::new(ItemStack::new_block(block::GOLD_ORE, 0), ItemStack::new(item::GOLD_INGOT, 0)), 39 | Recipe::new(ItemStack::new_block(block::DIAMOND_ORE, 0), ItemStack::new(item::DIAMOND, 0)), 40 | Recipe::new(ItemStack::new_block(block::SAND, 0), ItemStack::new_block(block::GLASS, 0)), 41 | Recipe::new(ItemStack::new(item::RAW_PORKCHOP, 0), ItemStack::new(item::COOKED_PORKCHOP, 0)), 42 | Recipe::new(ItemStack::new(item::RAW_FISH, 0), ItemStack::new(item::COOKED_FISH, 0)), 43 | Recipe::new(ItemStack::new_block(block::COBBLESTONE, 0), ItemStack::new_block(block::STONE, 0)), 44 | Recipe::new(ItemStack::new(item::CLAY, 0), ItemStack::new(item::BRICK, 0)), 45 | Recipe::new(ItemStack::new_block(block::CACTUS, 0), ItemStack::new(item::DYE, 2)), 46 | Recipe::new(ItemStack::new_block(block::LOG, 0), ItemStack::new(item::COAL, 1)), 47 | ]; 48 | 49 | /// Define a smelting recipe. 50 | struct Recipe { 51 | /// The item stack that is consumed to produce the output one. 52 | input: ItemStack, 53 | /// The output stack that is produced by consuming the input one. 54 | output: ItemStack, 55 | } 56 | 57 | impl Recipe { 58 | 59 | const fn new(input: ItemStack, output: ItemStack) -> Self { 60 | Self { input, output } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /mc173/src/java/io.rs: -------------------------------------------------------------------------------- 1 | //! This module provides read and write extension traits for Java types. 2 | 3 | use byteorder::{ReadBytesExt, WriteBytesExt, BE}; 4 | use std::io::{self, Read, Write}; 5 | 6 | 7 | /// Extension trait with Java read methods. 8 | pub trait ReadJavaExt: Read { 9 | 10 | #[inline] 11 | fn read_java_byte(&mut self) -> io::Result { 12 | ReadBytesExt::read_i8(self) 13 | } 14 | 15 | #[inline] 16 | fn read_java_short(&mut self) -> io::Result { 17 | ReadBytesExt::read_i16::(self) 18 | } 19 | 20 | #[inline] 21 | fn read_java_int(&mut self) -> io::Result { 22 | ReadBytesExt::read_i32::(self) 23 | } 24 | 25 | #[inline] 26 | fn read_java_long(&mut self) -> io::Result { 27 | ReadBytesExt::read_i64::(self) 28 | } 29 | 30 | #[inline] 31 | fn read_java_float(&mut self) -> io::Result { 32 | ReadBytesExt::read_f32::(self) 33 | } 34 | 35 | #[inline] 36 | fn read_java_double(&mut self) -> io::Result { 37 | ReadBytesExt::read_f64::(self) 38 | } 39 | 40 | #[inline] 41 | fn read_java_boolean(&mut self) -> io::Result { 42 | Ok(self.read_java_byte()? != 0) 43 | } 44 | 45 | fn read_java_string16(&mut self, max_len: usize) -> io::Result { 46 | 47 | let len = self.read_java_short()?; 48 | if len < 0 { 49 | return Err(new_invalid_data_err("negative length string")); 50 | } 51 | 52 | if len as usize > max_len { 53 | return Err(new_invalid_data_err("exceeded max string length")); 54 | } 55 | 56 | let mut raw = Vec::with_capacity(len as usize); 57 | for _ in 0..len { 58 | raw.push(ReadBytesExt::read_u16::(self)?); 59 | } 60 | 61 | let ret = char::decode_utf16(raw) 62 | .map(|res| res.unwrap_or(char::REPLACEMENT_CHARACTER)) 63 | .collect::(); 64 | 65 | Ok(ret) 66 | 67 | } 68 | 69 | fn read_java_string8(&mut self) -> io::Result { 70 | 71 | let len = self.read_u16::()?; 72 | let mut buf = vec![0u8; len as usize]; 73 | self.read_exact(&mut buf)?; 74 | 75 | String::from_utf8(buf).map_err(|_| new_invalid_data_err("invalid utf-8 string")) 76 | 77 | } 78 | 79 | } 80 | 81 | /// Extension trait with Java write methods. 82 | pub trait WriteJavaExt: Write { 83 | 84 | #[inline] 85 | fn write_java_byte(&mut self, b: i8) -> io::Result<()> { 86 | WriteBytesExt::write_i8(self, b) 87 | } 88 | 89 | #[inline] 90 | fn write_java_short(&mut self, s: i16) -> io::Result<()> { 91 | WriteBytesExt::write_i16::(self, s) 92 | } 93 | 94 | #[inline] 95 | fn write_java_int(&mut self, i: i32) -> io::Result<()> { 96 | WriteBytesExt::write_i32::(self, i) 97 | } 98 | 99 | #[inline] 100 | fn write_java_long(&mut self, l: i64) -> io::Result<()> { 101 | WriteBytesExt::write_i64::(self, l) 102 | } 103 | 104 | #[inline] 105 | fn write_java_float(&mut self, f: f32) -> io::Result<()> { 106 | WriteBytesExt::write_f32::(self, f) 107 | } 108 | 109 | #[inline] 110 | fn write_java_double(&mut self, d: f64) -> io::Result<()> { 111 | WriteBytesExt::write_f64::(self, d) 112 | } 113 | 114 | #[inline] 115 | fn write_java_boolean(&mut self, b: bool) -> io::Result<()> { 116 | self.write_java_byte(b as i8) 117 | } 118 | 119 | fn write_java_string16(&mut self, s: &str) -> io::Result<()> { 120 | 121 | // Count the number of UTF-16 java character. 122 | let len = s.chars().map(|c| c.len_utf16()).sum::(); 123 | if len > i16::MAX as usize { 124 | return Err(new_invalid_data_err("string too big")); 125 | } 126 | 127 | self.write_java_short(len as i16)?; 128 | for code in s.encode_utf16() { 129 | WriteBytesExt::write_u16::(self, code)?; 130 | } 131 | 132 | Ok(()) 133 | 134 | } 135 | 136 | fn write_java_string8(&mut self, s: &str) -> io::Result<()> { 137 | 138 | if s.len() > u16::MAX as usize { 139 | return Err(new_invalid_data_err("string too big")); 140 | } 141 | 142 | self.write_u16::(s.len() as u16)?; 143 | self.write_all(s.as_bytes()) 144 | 145 | } 146 | 147 | } 148 | 149 | impl ReadJavaExt for R {} 150 | impl WriteJavaExt for W {} 151 | 152 | 153 | /// Return an invalid data io error with specific message. 154 | fn new_invalid_data_err(message: &'static str) -> io::Error { 155 | io::Error::new(io::ErrorKind::InvalidData, message) 156 | } 157 | -------------------------------------------------------------------------------- /mc173/src/java/mod.rs: -------------------------------------------------------------------------------- 1 | //! Modules related to emulation of Java classes. 2 | 3 | mod io; 4 | mod rand; 5 | 6 | pub use io::{ReadJavaExt, WriteJavaExt}; 7 | pub use rand::JavaRandom; 8 | -------------------------------------------------------------------------------- /mc173/src/java/rand.rs: -------------------------------------------------------------------------------- 1 | //! Different kind of pseudo-random number generator. 2 | 3 | use std::sync::atomic::{AtomicI64, Ordering}; 4 | use std::time::{UNIX_EPOCH, SystemTime}; 5 | use std::num::Wrapping; 6 | 7 | use glam::{Vec3, DVec3}; 8 | 9 | 10 | const MULTIPLIER: Wrapping = Wrapping(0x5DEECE66D); 11 | const ADDEND: Wrapping = Wrapping(0xB); 12 | const MASK: Wrapping = Wrapping((1 << 48) - 1); 13 | 14 | const FLOAT_DIV: f32 = (1u32 << 24) as f32; 15 | const DOUBLE_DIV: f64 = (1u64 << 53) as f64; 16 | 17 | 18 | #[inline] 19 | fn initial_scramble(seed: i64) -> Wrapping { 20 | (Wrapping(seed) ^ MULTIPLIER) & MASK 21 | } 22 | 23 | 24 | /// Generate a new seed in the same way as `java.f.Random` (same constants). 25 | fn gen_seed() -> i64 { 26 | static SEED: AtomicI64 = AtomicI64::new(8682522807148012); 27 | let mut current = SEED.load(Ordering::Relaxed); 28 | loop { 29 | let next = current.wrapping_mul(181783497276652981); 30 | match SEED.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) { 31 | Ok(_) => { 32 | // This is a bit different from Java implementation because the nano time 33 | // as an integer value is not available in Rust, even with Instant. 34 | // So we're using duration since unix epoch of the system time, maybe not 35 | // as safe as the Java implementation. 36 | return match SystemTime::now().duration_since(UNIX_EPOCH) { 37 | Ok(d) => next ^ (d.as_nanos() as i64), 38 | Err(_) => next 39 | }; 40 | } 41 | Err(old) => current = old 42 | } 43 | } 44 | } 45 | 46 | 47 | /// A pseudo-random number generator ported from the Java standard *RNG* with additional 48 | /// utility methods better suited for rust. 49 | #[derive(Debug, Clone)] 50 | pub struct JavaRandom { 51 | seed: Wrapping, 52 | next_gaussian: Option, 53 | } 54 | 55 | impl Default for JavaRandom { 56 | fn default() -> Self { 57 | Self::new_seeded() 58 | } 59 | } 60 | 61 | impl JavaRandom { 62 | 63 | #[inline] 64 | pub fn new(seed: i64) -> JavaRandom { 65 | JavaRandom { seed: initial_scramble(seed), next_gaussian: None } 66 | } 67 | 68 | #[inline] 69 | pub fn new_seeded() -> JavaRandom { 70 | Self::new(gen_seed()) 71 | } 72 | 73 | #[inline] 74 | pub fn new_blank() -> JavaRandom { 75 | JavaRandom { seed: Wrapping(0), next_gaussian: None } 76 | } 77 | 78 | #[inline] 79 | pub fn set_seed(&mut self, seed: i64) { 80 | self.seed = initial_scramble(seed); 81 | } 82 | 83 | #[inline] 84 | pub fn get_seed(&self) -> i64 { 85 | self.seed.0 86 | } 87 | 88 | pub fn next_blank(&mut self) { 89 | self.seed = (self.seed * MULTIPLIER + ADDEND) & MASK; 90 | } 91 | 92 | #[inline] 93 | fn next(&mut self, bits: u8) -> i32 { 94 | self.next_blank(); 95 | (self.seed.0 as u64 >> (48 - bits)) as i32 96 | } 97 | 98 | #[inline] 99 | pub fn next_int(&mut self) -> i32 { 100 | self.next(32) 101 | } 102 | 103 | pub fn next_int_bounded(&mut self, bound: i32) -> i32 { 104 | 105 | debug_assert!(bound >= 0, "bound is negative"); 106 | 107 | if (bound & -bound) == bound { 108 | // If the bound is a power of two, this is simpler. 109 | (((bound as i64).wrapping_mul(self.next(31) as i64)) >> 31) as i32 110 | } else { 111 | 112 | let mut bits; 113 | let mut val; 114 | 115 | loop { 116 | bits = self.next(31); 117 | val = bits.rem_euclid(bound); 118 | if bits.wrapping_sub(val).wrapping_add(bound - 1) >= 0 { 119 | break; 120 | } 121 | } 122 | 123 | val 124 | 125 | } 126 | 127 | } 128 | 129 | pub fn next_long(&mut self) -> i64 { 130 | ((self.next(32) as i64) << 32).wrapping_add(self.next(32) as i64) 131 | } 132 | 133 | /// Get the next pseudo-random single-precision float. 134 | pub fn next_float(&mut self) -> f32 { 135 | self.next(24) as f32 / FLOAT_DIV 136 | } 137 | 138 | /// Get the next pseudo-random double-precision float. 139 | pub fn next_double(&mut self) -> f64 { 140 | let high = (self.next(26) as i64) << 27; 141 | let low = self.next(27) as i64; 142 | (high.wrapping_add(low) as f64) / DOUBLE_DIV 143 | } 144 | 145 | /// Get the next pseudo-random double-precision Gaussian random number. 146 | pub fn next_gaussian(&mut self) -> f64 { 147 | if let Some(next_gaussian) = self.next_gaussian.take() { 148 | next_gaussian 149 | } else { 150 | loop { 151 | let v1 = self.next_double() * 2.0 - 1.0; 152 | let v2 = self.next_double() * 2.0 - 1.0; 153 | let s = v1 * v1 + v2 * v2; 154 | if s < 1.0 && s != 0.0 { 155 | let multiplier = (-2.0 * s.ln() / s).sqrt(); 156 | self.next_gaussian = Some(v2 * multiplier); 157 | break v1 * multiplier; 158 | } 159 | } 160 | } 161 | } 162 | 163 | /// Get the next pseudo-random single-precision float vector, x, y and z. 164 | /// **This is not part of the standard Java class.** 165 | pub fn next_float_vec(&mut self) -> Vec3 { 166 | Vec3 { 167 | x: self.next_float(), 168 | y: self.next_float(), 169 | z: self.next_float(), 170 | } 171 | } 172 | 173 | /// Get the next pseudo-random double-precision float vector, x, y and z. 174 | /// **This is not part of the standard Java class.** 175 | pub fn next_double_vec(&mut self) -> DVec3 { 176 | DVec3 { 177 | x: self.next_double(), 178 | y: self.next_double(), 179 | z: self.next_double(), 180 | } 181 | } 182 | 183 | /// Get the next pseudo-random double-precision double vector, x, y and z, 184 | /// with Gaussian distribution. 185 | /// **This is not part of the standard Java class.** 186 | pub fn next_gaussian_vec(&mut self) -> DVec3 { 187 | DVec3 { 188 | x: self.next_gaussian(), 189 | y: self.next_gaussian(), 190 | z: self.next_gaussian(), 191 | } 192 | } 193 | 194 | /// Randomly pick an item in the given slice. 195 | /// **This is not part of the standard Java class.** 196 | #[inline] 197 | pub fn next_choice(&mut self, items: &[T]) -> T { 198 | assert!(!items.is_empty()); 199 | items[self.next_int_bounded(items.len() as i32) as usize] 200 | } 201 | 202 | /// Randomly pick an item in the given slice and return mutable reference to it. 203 | /// **This is not part of the standard Java class.** 204 | #[inline] 205 | pub fn next_choice_ref<'a, T>(&mut self, items: &'a [T]) -> &'a T { 206 | assert!(!items.is_empty()); 207 | &items[self.next_int_bounded(items.len() as i32) as usize] 208 | } 209 | 210 | /// Randomly pick an item in the given slice and return mutable reference to it. 211 | /// **This is not part of the standard Java class.** 212 | #[inline] 213 | pub fn next_choice_mut<'a, T>(&mut self, items: &'a mut [T]) -> &'a mut T { 214 | assert!(!items.is_empty()); 215 | &mut items[self.next_int_bounded(items.len() as i32) as usize] 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /mc173/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Minecraft beta 1.7.3 server backend in Rust. 2 | 3 | pub mod java; 4 | pub mod util; 5 | pub mod geom; 6 | 7 | pub mod block; 8 | pub mod item; 9 | pub mod entity; 10 | pub mod block_entity; 11 | pub mod biome; 12 | 13 | pub mod chunk; 14 | pub mod world; 15 | pub mod storage; 16 | pub mod serde; 17 | pub mod r#gen; 18 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk.rs: -------------------------------------------------------------------------------- 1 | //! Chunk serialization and deserialization from NBT compound. 2 | 3 | use crate::world::ChunkSnapshot; 4 | 5 | use super::nbt::{Nbt, NbtParseError, NbtCompound}; 6 | 7 | pub mod block_entity_nbt; 8 | pub mod entity_kind_nbt; 9 | pub mod item_stack_nbt; 10 | pub mod painting_art_nbt; 11 | pub mod entity_nbt; 12 | pub mod slot_nbt; 13 | pub mod chunk_nbt; 14 | 15 | pub fn from_nbt(root: &Nbt) -> Result { 16 | chunk_nbt::from_nbt(root.parse().as_compound()?) 17 | } 18 | 19 | pub fn to_nbt(snapshot: &ChunkSnapshot) -> Nbt { 20 | let mut comp = NbtCompound::new(); 21 | chunk_nbt::to_nbt(&mut comp, snapshot); 22 | Nbt::Compound(comp) 23 | } 24 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk/block_entity_nbt.rs: -------------------------------------------------------------------------------- 1 | //! NBT serialization and deserialization for [`BlockEntity`] type. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block_entity::note_block::NoteBlockBlockEntity; 6 | use crate::block_entity::dispenser::DispenserBlockEntity; 7 | use crate::block_entity::furnace::FurnaceBlockEntity; 8 | use crate::block_entity::jukebox::JukeboxBlockEntity; 9 | use crate::block_entity::spawner::SpawnerBlockEntity; 10 | use crate::block_entity::piston::PistonBlockEntity; 11 | use crate::block_entity::chest::ChestBlockEntity; 12 | use crate::block_entity::sign::SignBlockEntity; 13 | use crate::block_entity::BlockEntity; 14 | use crate::entity::EntityKind; 15 | use crate::item::ItemStack; 16 | use crate::geom::Face; 17 | 18 | use crate::serde::nbt::{NbtParseError, NbtCompound, NbtCompoundParse}; 19 | 20 | use super::entity_kind_nbt; 21 | use super::slot_nbt; 22 | 23 | pub fn from_nbt(comp: NbtCompoundParse) -> Result<(IVec3, Box), NbtParseError> { 24 | 25 | let x = comp.get_int("x")?; 26 | let y = comp.get_int("y")?; 27 | let z = comp.get_int("z")?; 28 | 29 | let id = comp.get_string("id")?; 30 | let block_entity = Box::new(match id { 31 | "Chest" => { 32 | let mut chest = ChestBlockEntity::default(); 33 | slot_nbt::from_nbt_to_inv(comp.get_list("Items")?, &mut chest.inv[..])?; 34 | BlockEntity::Chest(chest) 35 | } 36 | "Furnace" => { 37 | let mut inv = [ItemStack::EMPTY; 3]; 38 | slot_nbt::from_nbt_to_inv(comp.get_list("Items")?, &mut inv[..])?; 39 | let mut furnace = FurnaceBlockEntity::default(); 40 | furnace.input_stack = inv[0]; 41 | furnace.fuel_stack = inv[1]; 42 | furnace.output_stack = inv[2]; 43 | furnace.burn_remaining_ticks = comp.get_short("BurnTime")?.max(0) as u16; 44 | furnace.smelt_ticks = comp.get_short("CookTime")?.max(0) as u16; 45 | // TODO: burn max ticks 46 | BlockEntity::Furnace(furnace) 47 | } 48 | "Trap" => { 49 | let mut dispenser = DispenserBlockEntity::default(); 50 | slot_nbt::from_nbt_to_inv(comp.get_list("Items")?, &mut dispenser.inv[..])?; 51 | BlockEntity::Dispenser(dispenser) 52 | } 53 | "MobSpawner" => { 54 | let mut spawner = SpawnerBlockEntity::default(); 55 | spawner.entity_kind = entity_kind_nbt::from_nbt(comp.get_string("EntityId")?).unwrap_or(EntityKind::Pig); 56 | spawner.remaining_time = comp.get_short("Delay")? as u16; 57 | BlockEntity::Spawner(spawner) 58 | } 59 | "Music" => { 60 | let mut note_block = NoteBlockBlockEntity::default(); 61 | note_block.note = comp.get_byte("note")? as u8; 62 | BlockEntity::NoteBlock(note_block) 63 | } 64 | "Piston" => { 65 | let mut piston = PistonBlockEntity::default(); 66 | piston.block = comp.get_int("blockId")? as u8; 67 | piston.metadata = comp.get_int("blockData")? as u8; 68 | piston.face = match comp.get_int("facing")? { 69 | 0 => Face::NegY, 70 | 1 => Face::PosY, 71 | 2 => Face::NegZ, 72 | 3 => Face::PosZ, 73 | 4 => Face::NegX, 74 | _ => Face::PosX, 75 | }; 76 | piston.progress = comp.get_float("progress")?; 77 | piston.extending = comp.get_boolean("extending")?; 78 | BlockEntity::Piston(piston) 79 | } 80 | "Sign" => { 81 | let mut sign = SignBlockEntity::default(); 82 | for (i, key) in ["Text1", "Text2", "Text3", "Text4"].into_iter().enumerate() { 83 | sign.lines[i] = comp.get_string(key)?.to_string(); 84 | } 85 | BlockEntity::Sign(sign) 86 | } 87 | "RecordPlayer" => { 88 | BlockEntity::Jukebox(JukeboxBlockEntity { 89 | record: comp.get_int("Record")? as u32 90 | }) 91 | } 92 | _ => return Err(NbtParseError::new(format!("{}/id", comp.path()), "valid block entity id")) 93 | }); 94 | 95 | Ok((IVec3::new(x, y, z), block_entity)) 96 | 97 | } 98 | 99 | pub fn to_nbt<'a>(comp: &'a mut NbtCompound, pos: IVec3, block_entity: &BlockEntity) -> &'a mut NbtCompound { 100 | 101 | comp.insert("x", pos.x); 102 | comp.insert("y", pos.y); 103 | comp.insert("z", pos.z); 104 | 105 | match block_entity { 106 | BlockEntity::Chest(chest) => { 107 | comp.insert("id", "Chest"); 108 | comp.insert("Items", slot_nbt::to_nbt_from_inv(&chest.inv[..])); 109 | } 110 | BlockEntity::Furnace(furnace) => { 111 | comp.insert("id", "Furnace"); 112 | comp.insert("Items", slot_nbt::to_nbt_from_inv(&[furnace.input_stack, furnace.fuel_stack, furnace.output_stack])); 113 | comp.insert("BurnTime", furnace.burn_remaining_ticks); 114 | comp.insert("CookTime", furnace.smelt_ticks); 115 | } 116 | BlockEntity::Dispenser(dispenser) => { 117 | comp.insert("id", "Trap"); 118 | comp.insert("Items", slot_nbt::to_nbt_from_inv(&dispenser.inv[..])); 119 | } 120 | BlockEntity::Spawner(spawner) => { 121 | comp.insert("id", "MobSpawner"); 122 | comp.insert("EntityId", entity_kind_nbt::to_nbt(spawner.entity_kind).unwrap_or("Pig")); 123 | comp.insert("Delay", spawner.remaining_time.min(i16::MAX as _) as i16); 124 | } 125 | BlockEntity::NoteBlock(note_block) => { 126 | comp.insert("id", "Music"); 127 | comp.insert("note", note_block.note); 128 | } 129 | BlockEntity::Piston(piston) => { 130 | comp.insert("id", "Piston"); 131 | comp.insert("blockId", piston.block as u32); 132 | comp.insert("blockData", piston.metadata as u32); 133 | comp.insert("facing", match piston.face { 134 | Face::NegY => 0i32, 135 | Face::PosY => 1, 136 | Face::NegZ => 2, 137 | Face::PosZ => 3, 138 | Face::NegX => 4, 139 | Face::PosX => 5, 140 | }); 141 | comp.insert("progress", piston.progress); 142 | comp.insert("extending", piston.extending); 143 | } 144 | BlockEntity::Sign(sign) => { 145 | comp.insert("id", "Sign"); 146 | for (i, key) in ["Text1", "Text2", "Text3", "Text4"].into_iter().enumerate() { 147 | comp.insert(key, sign.lines[i].as_str()); 148 | } 149 | } 150 | BlockEntity::Jukebox(jukebox) => { 151 | comp.insert("id", "RecordPlayer"); 152 | comp.insert("Record", jukebox.record); 153 | } 154 | } 155 | 156 | comp 157 | 158 | } 159 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk/chunk_nbt.rs: -------------------------------------------------------------------------------- 1 | //! NBT serialization and deserialization for [`ChunkSnapshot`] type. 2 | 3 | use std::sync::Arc; 4 | 5 | use crate::serde::nbt::{NbtCompoundParse, NbtCompound, NbtParseError, Nbt}; 6 | use crate::world::ChunkSnapshot; 7 | 8 | use super::block_entity_nbt; 9 | use super::entity_nbt; 10 | 11 | pub fn from_nbt(comp: NbtCompoundParse) -> Result { 12 | 13 | let level = comp.get_compound("Level")?; 14 | let cx = level.get_int("xPos")?; 15 | let cz = level.get_int("zPos")?; 16 | 17 | let mut snapshot = ChunkSnapshot::new(cx, cz); 18 | let chunk = Arc::get_mut(&mut snapshot.chunk).unwrap(); 19 | 20 | // This is annoying to make so much copies but we have no choice for know because 21 | // this is not yet possible to directly deserialize into an existing buffer. 22 | chunk.block.copy_from_slice(level.get_byte_array("Blocks")?); 23 | chunk.metadata.inner.copy_from_slice(level.get_byte_array("Data")?); 24 | chunk.block_light.inner.copy_from_slice(level.get_byte_array("BlockLight")?); 25 | chunk.sky_light.inner.copy_from_slice(level.get_byte_array("SkyLight")?); 26 | chunk.height.copy_from_slice(level.get_byte_array("HeightMap")?); 27 | 28 | for item in level.get_list("Entities")?.iter() { 29 | let entity = entity_nbt::from_nbt(item.as_compound()?)?; 30 | snapshot.entities.push(entity); 31 | } 32 | 33 | for item in level.get_list("TileEntities")?.iter() { 34 | let (pos, block_entity) = block_entity_nbt::from_nbt(item.as_compound()?)?; 35 | snapshot.block_entities.insert(pos, block_entity); 36 | } 37 | 38 | Ok(snapshot) 39 | 40 | } 41 | 42 | pub fn to_nbt<'a>(comp: &'a mut NbtCompound, snapshot: &ChunkSnapshot) -> &'a mut NbtCompound { 43 | 44 | let mut level = NbtCompound::new(); 45 | 46 | level.insert("xPos", snapshot.cx); 47 | level.insert("zPos", snapshot.cz); 48 | 49 | level.insert("Blocks", snapshot.chunk.block.to_vec()); 50 | level.insert("Data", snapshot.chunk.metadata.inner.to_vec()); 51 | level.insert("BlockLight", snapshot.chunk.block_light.inner.to_vec()); 52 | level.insert("SkyLight", snapshot.chunk.sky_light.inner.to_vec()); 53 | level.insert("HeightMap", snapshot.chunk.height.to_vec()); 54 | 55 | level.insert("Entities", snapshot.entities.iter() 56 | .filter_map(|entity| { 57 | let mut comp = NbtCompound::new(); 58 | if entity_nbt::to_nbt(&mut comp, &entity).is_some() { 59 | Some(Nbt::Compound(comp)) 60 | } else { 61 | None 62 | } 63 | }) 64 | .collect::>()); 65 | 66 | level.insert("TileEntities", snapshot.block_entities.iter() 67 | .map(|(&pos, block_entity)| { 68 | let mut comp = NbtCompound::new(); 69 | block_entity_nbt::to_nbt(&mut comp, pos, &block_entity); 70 | Nbt::Compound(comp) 71 | }) 72 | .collect::>()); 73 | 74 | comp.insert("Level", level); 75 | comp 76 | 77 | } 78 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk/entity_kind_nbt.rs: -------------------------------------------------------------------------------- 1 | //! NBT serialization and deserialization for [`EntityKind`] enumeration. 2 | 3 | use crate::entity::EntityKind; 4 | 5 | pub fn from_nbt(id: &str) -> Option { 6 | Some(match id { 7 | "Arrow" => EntityKind::Arrow, 8 | "Snowball" => EntityKind::Snowball, 9 | "Item" => EntityKind::Item, 10 | "Painting" => EntityKind::Painting, 11 | "Creeper" => EntityKind::Creeper, 12 | "Skeleton" => EntityKind::Skeleton, 13 | "Spider" => EntityKind::Spider, 14 | "Giant" => EntityKind::Giant, 15 | "Zombie" => EntityKind::Zombie, 16 | "Slime" => EntityKind::Slime, 17 | "Ghast" => EntityKind::Ghast, 18 | "PigZombie" => EntityKind::PigZombie, 19 | "Pig" => EntityKind::Pig, 20 | "Sheep" => EntityKind::Sheep, 21 | "Cow" => EntityKind::Cow, 22 | "Chicken" => EntityKind::Chicken, 23 | "Squid" => EntityKind::Squid, 24 | "Wolf" => EntityKind::Wolf, 25 | "PrimedTnt" => EntityKind::Tnt, 26 | "FallingSand" => EntityKind::FallingBlock, 27 | "Minecart" => EntityKind::Minecart, 28 | "Boat" => EntityKind::Boat, 29 | _ => return None 30 | }) 31 | } 32 | 33 | pub fn to_nbt(kind: EntityKind) -> Option<&'static str> { 34 | Some(match kind { 35 | EntityKind::Item => "Item", 36 | EntityKind::Painting => "Painting", 37 | EntityKind::Boat => "Boat", 38 | EntityKind::Minecart => "Minecart", 39 | EntityKind::FallingBlock => "FallingSand", 40 | EntityKind::Tnt => "PrimedTnt", 41 | EntityKind::Arrow => "Arrow", 42 | EntityKind::Snowball => "Snowball", 43 | EntityKind::Ghast => "Ghast", 44 | EntityKind::Slime => "Slime", 45 | EntityKind::Pig => "Pig", 46 | EntityKind::Chicken => "Chicken", 47 | EntityKind::Cow => "Cow", 48 | EntityKind::Sheep => "Sheep", 49 | EntityKind::Squid => "Squid", 50 | EntityKind::Wolf => "Wolf", 51 | EntityKind::Creeper => "Creeper", 52 | EntityKind::Giant => "Giant", 53 | EntityKind::PigZombie => "PigZombie", 54 | EntityKind::Skeleton => "Skeleton", 55 | EntityKind::Spider => "Spider", 56 | EntityKind::Zombie => "Zombie", 57 | _ => return None // Not serializable 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk/item_stack_nbt.rs: -------------------------------------------------------------------------------- 1 | //! NBT serialization and deserialization for [`ItemStack`] type. 2 | 3 | use crate::serde::nbt::{NbtParseError, NbtCompound, NbtCompoundParse}; 4 | use crate::item::ItemStack; 5 | 6 | /// Create an item stack from a NBT compound. 7 | pub fn from_nbt(comp: NbtCompoundParse) -> Result { 8 | let id = comp.get_short("id")? as u16; 9 | let size = comp.get_byte("Count")?.max(0) as u16; 10 | let damage = comp.get_short("Damage")? as u16; 11 | Ok(ItemStack { id, size, damage }) 12 | } 13 | 14 | /// Encode an item stack into a NBT compound. 15 | pub fn to_nbt(comp: &mut NbtCompound, stack: ItemStack) -> &mut NbtCompound { 16 | comp.insert("id", stack.id); 17 | comp.insert("Count", stack.size.min(i8::MAX as _) as i8); 18 | comp.insert("Damage", stack.damage); 19 | comp 20 | } 21 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk/painting_art_nbt.rs: -------------------------------------------------------------------------------- 1 | //! NBT serialization and deserialization for [`PaintingArt`] enumeration. 2 | 3 | use crate::entity::PaintingArt; 4 | 5 | pub fn from_nbt(id: &str) -> Option { 6 | Some(match id { 7 | "Kebab" => PaintingArt::Kebab, 8 | "Aztec" => PaintingArt::Aztec, 9 | "Alban" => PaintingArt::Alban, 10 | "Aztec2" => PaintingArt::Aztec2, 11 | "Bomb" => PaintingArt::Bomb, 12 | "Plant" => PaintingArt::Plant, 13 | "Wasteland" => PaintingArt::Wasteland, 14 | "Pool" => PaintingArt::Pool, 15 | "Courbet" => PaintingArt::Courbet, 16 | "Sea" => PaintingArt::Sea, 17 | "Sunset" => PaintingArt::Sunset, 18 | "Creebet" => PaintingArt::Creebet, 19 | "Wanderer" => PaintingArt::Wanderer, 20 | "Graham" => PaintingArt::Graham, 21 | "Match" => PaintingArt::Match, 22 | "Bust" => PaintingArt::Bust, 23 | "Stage" => PaintingArt::Stage, 24 | "Void" => PaintingArt::Void, 25 | "SkullAndRoses" => PaintingArt::SkullAndRoses, 26 | "Fighters" => PaintingArt::Fighters, 27 | "Pointer" => PaintingArt::Pointer, 28 | "Pigscene" => PaintingArt::Pigscene, 29 | "BurningSkull" => PaintingArt::BurningSkull, 30 | "Skeleton" => PaintingArt::Skeleton, 31 | "DonkeyKong" => PaintingArt::DonkeyKong, 32 | _ => return None 33 | }) 34 | } 35 | 36 | pub fn to_nbt(art: PaintingArt) -> &'static str { 37 | match art { 38 | PaintingArt::Kebab => "Kebab", 39 | PaintingArt::Aztec => "Aztec", 40 | PaintingArt::Alban => "Alban", 41 | PaintingArt::Aztec2 => "Aztec2", 42 | PaintingArt::Bomb => "Bomb", 43 | PaintingArt::Plant => "Plant", 44 | PaintingArt::Wasteland => "Wasteland", 45 | PaintingArt::Pool => "Pool", 46 | PaintingArt::Courbet => "Courbet", 47 | PaintingArt::Sea => "Sea", 48 | PaintingArt::Sunset => "Sunset", 49 | PaintingArt::Creebet => "Creebet", 50 | PaintingArt::Wanderer => "Wanderer", 51 | PaintingArt::Graham => "Graham", 52 | PaintingArt::Match => "Match", 53 | PaintingArt::Bust => "Bust", 54 | PaintingArt::Stage => "Stage", 55 | PaintingArt::Void => "Void", 56 | PaintingArt::SkullAndRoses => "SkullAndRoses", 57 | PaintingArt::Fighters => "Fighters", 58 | PaintingArt::Pointer => "Pointer", 59 | PaintingArt::Pigscene => "Pigscene", 60 | PaintingArt::BurningSkull => "BurningSkull", 61 | PaintingArt::Skeleton => "Skeleton", 62 | PaintingArt::DonkeyKong => "DonkeyKong", 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mc173/src/serde/chunk/slot_nbt.rs: -------------------------------------------------------------------------------- 1 | //! Common NBT serde functions for item slots. 2 | 3 | use crate::serde::nbt::{NbtParseError, NbtCompoundParse, NbtCompound, NbtListParse, Nbt}; 4 | use crate::item::ItemStack; 5 | 6 | use super::item_stack_nbt; 7 | 8 | /// Create an slot and item stack from a NBT compound. 9 | pub fn from_nbt(comp: NbtCompoundParse) -> Result<(u8, ItemStack), NbtParseError> { 10 | let slot = comp.get_byte("Slot")? as u8; 11 | let stack = item_stack_nbt::from_nbt(comp)?; 12 | Ok((slot, stack)) 13 | } 14 | 15 | /// Encode a slot and item stack into a NBT compound. 16 | pub fn to_nbt(comp: &mut NbtCompound, slot: u8, stack: ItemStack) -> &mut NbtCompound { 17 | comp.insert("Slot", slot); 18 | item_stack_nbt::to_nbt(comp, stack) 19 | } 20 | 21 | pub fn from_nbt_to_inv(list: NbtListParse, inv: &mut [ItemStack]) -> Result<(), NbtParseError> { 22 | for item in list.iter() { 23 | let (slot, stack) = from_nbt(item.as_compound()?)?; 24 | if (slot as usize) < inv.len() { 25 | inv[slot as usize] = stack; 26 | } 27 | } 28 | Ok(()) 29 | } 30 | 31 | pub fn to_nbt_from_inv(inv: &[ItemStack]) -> Vec { 32 | let mut list = Vec::new(); 33 | for (index, stack) in inv.iter().copied().enumerate() { 34 | if index < 256 && !stack.is_empty() { 35 | let mut comp = NbtCompound::new(); 36 | to_nbt(&mut comp, index as u8, stack); 37 | list.push(comp.into()); 38 | } 39 | } 40 | list 41 | } 42 | -------------------------------------------------------------------------------- /mc173/src/serde/mod.rs: -------------------------------------------------------------------------------- 1 | //! Serialization and deserialization utilities for worlds, chunks and entities. 2 | 3 | pub mod region; 4 | pub mod chunk; 5 | pub mod nbt; 6 | -------------------------------------------------------------------------------- /mc173/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Various uncategorized utilities. 2 | 3 | 4 | /// A function to better inline the default function call. 5 | #[inline(always)] 6 | pub(crate) fn default() -> T { 7 | T::default() 8 | } 9 | 10 | 11 | /// A fading average 12 | #[derive(Debug, Clone, Default)] 13 | pub struct FadingAverage { 14 | value: f32, 15 | } 16 | 17 | impl FadingAverage { 18 | 19 | #[inline] 20 | pub fn push(&mut self, value: f32, factor: f32) { 21 | self.value = (self.value * (1.0 - factor)) + value * factor; 22 | } 23 | 24 | #[inline] 25 | pub fn get(&self) -> f32 { 26 | self.value 27 | } 28 | 29 | } 30 | 31 | 32 | /// Internal utility function to split a string at a given byte index, but while keeping 33 | /// utf8 boundary and not panicking like [`str::split_at`]. A value greater than `s.len()` 34 | /// will panic. 35 | #[inline] 36 | pub fn split_at_utf8_boundary(s: &str, mut index: usize) -> (&str, &str) { 37 | while !s.is_char_boundary(index) { 38 | // Index 0 is a boundary, so we can decrement without checking overflow. 39 | index -= 1; 40 | } 41 | s.split_at(index) 42 | } 43 | -------------------------------------------------------------------------------- /mc173/src/world/break.rs: -------------------------------------------------------------------------------- 1 | //! Provides methods for breaking blocks with items. Block hardness and break duration 2 | //! depending on the tool. 3 | 4 | use glam::IVec3; 5 | 6 | use crate::block::material::Material; 7 | use crate::{block, item}; 8 | 9 | use super::World; 10 | 11 | 12 | /// Methods related to block breaking. 13 | impl World { 14 | 15 | /// Break a block naturally and loot its items. This returns true if successful, false 16 | /// if the chunk/pos was not valid. It also notifies blocks around, this is basically 17 | /// a wrapper around [`set_block_notify`](Self::set_block_notify) method. 18 | pub fn break_block(&mut self, pos: IVec3) -> Option<(u8, u8)> { 19 | let (prev_id, prev_metadata) = self.set_block_notify(pos, block::AIR, 0)?; 20 | self.spawn_block_loot(pos, prev_id, prev_metadata, 1.0); 21 | Some((prev_id, prev_metadata)) 22 | } 23 | 24 | /// Get the minimum ticks duration required to break the block given its id. 25 | pub fn get_break_duration(&self, item_id: u16, block_id: u8, in_water: bool, on_ground: bool) -> f32 { 26 | 27 | // TODO: Maybe remove hardness from the block definition, because it's only used in 28 | // the game for break duration. 29 | 30 | let hardness = block::material::get_break_hardness(block_id); 31 | if hardness.is_infinite() { 32 | f32::INFINITY 33 | } else { 34 | 35 | // The hardness value in the game is registered as ticks, with a multiplier 36 | // depending on the player's conditions and tools. 37 | 38 | if self.can_break(item_id, block_id) { 39 | 40 | let mut env_modifier = self.get_break_speed(item_id, block_id); 41 | 42 | if in_water { 43 | env_modifier /= 5.0; 44 | } 45 | 46 | if !on_ground { 47 | env_modifier /= 5.0; 48 | } 49 | 50 | hardness * 30.0 / env_modifier 51 | 52 | } else { 53 | hardness * 100.0 54 | } 55 | 56 | } 57 | 58 | } 59 | 60 | /// Check if an item (given its id) can break a block without speed penalties and 61 | /// loose the items. 62 | fn can_break(&self, item_id: u16, block_id: u8) -> bool { 63 | 64 | match block_id { 65 | block::OBSIDIAN => matches!(item_id, 66 | item::DIAMOND_PICKAXE), 67 | block::DIAMOND_ORE | 68 | block::DIAMOND_BLOCK | 69 | block::GOLD_ORE | 70 | block::GOLD_BLOCK | 71 | block::REDSTONE_ORE | 72 | block::REDSTONE_ORE_LIT => matches!(item_id, 73 | item::DIAMOND_PICKAXE | 74 | item::IRON_PICKAXE), 75 | block::IRON_ORE | 76 | block::IRON_BLOCK | 77 | block::LAPIS_ORE | 78 | block::LAPIS_BLOCK => matches!(item_id, 79 | item::DIAMOND_PICKAXE | 80 | item::IRON_PICKAXE | 81 | item::STONE_PICKAXE), 82 | block::COBWEB => matches!(item_id, 83 | item::SHEARS | 84 | item::DIAMOND_SWORD | 85 | item::IRON_SWORD | 86 | item::STONE_SWORD | 87 | item::GOLD_SWORD | 88 | item::WOOD_SWORD), 89 | block::SNOW | 90 | block::SNOW_BLOCK => matches!(item_id, 91 | item::DIAMOND_SHOVEL | 92 | item::IRON_SHOVEL | 93 | item::STONE_SHOVEL | 94 | item::GOLD_SHOVEL | 95 | item::WOOD_SHOVEL), 96 | _ => { 97 | 98 | let material = block::material::get_material(block_id); 99 | if material.is_breakable_by_default() { 100 | return true; 101 | } 102 | 103 | match item_id { 104 | item::DIAMOND_PICKAXE | 105 | item::IRON_PICKAXE | 106 | item::STONE_PICKAXE | 107 | item::GOLD_PICKAXE | 108 | item::WOOD_PICKAXE => matches!(material, Material::Rock | Material::Iron), 109 | _ => false 110 | } 111 | 112 | } 113 | } 114 | 115 | } 116 | 117 | /// Get the speed multiplier for breaking a given block with a given item. 118 | fn get_break_speed(&self, item_id: u16, block_id: u8) -> f32 { 119 | 120 | const DIAMOND_SPEED: f32 = 8.0; 121 | const IRON_SPEED: f32 = 6.0; 122 | const STONE_SPEED: f32 = 4.0; 123 | const WOOD_SPEED: f32 = 2.0; 124 | const GOLD_SPEED: f32 = 12.0; 125 | 126 | match block_id { 127 | block::WOOD | 128 | block::BOOKSHELF | 129 | block::LOG | 130 | block::CHEST => { 131 | // Axe 132 | match item_id { 133 | item::DIAMOND_AXE => DIAMOND_SPEED, 134 | item::IRON_AXE => IRON_SPEED, 135 | item::STONE_AXE => STONE_SPEED, 136 | item::WOOD_AXE => WOOD_SPEED, 137 | item::GOLD_AXE => GOLD_SPEED, 138 | _ => 1.0, 139 | } 140 | } 141 | block::COBBLESTONE | 142 | block::SLAB | 143 | block::DOUBLE_SLAB | 144 | block::STONE | 145 | block::SANDSTONE | 146 | block::MOSSY_COBBLESTONE | 147 | block::IRON_ORE | 148 | block::IRON_BLOCK | 149 | block::GOLD_ORE | 150 | block::GOLD_BLOCK | 151 | block::COAL_ORE | 152 | block::DIAMOND_ORE | 153 | block::DIAMOND_BLOCK | 154 | block::ICE | 155 | block::NETHERRACK | 156 | block::LAPIS_ORE | 157 | block::LAPIS_BLOCK => { 158 | // Pickaxe 159 | match item_id { 160 | item::DIAMOND_PICKAXE => DIAMOND_SPEED, 161 | item::IRON_PICKAXE => IRON_SPEED, 162 | item::STONE_PICKAXE => STONE_SPEED, 163 | item::WOOD_PICKAXE => WOOD_SPEED, 164 | item::GOLD_PICKAXE => GOLD_SPEED, 165 | _ => 1.0, 166 | } 167 | } 168 | block::GRASS | 169 | block::DIRT | 170 | block::SAND | 171 | block::GRAVEL | 172 | block::SNOW | 173 | block::SNOW_BLOCK | 174 | block::CLAY | 175 | block::FARMLAND => { 176 | // Shovel 177 | match item_id { 178 | item::DIAMOND_SHOVEL => DIAMOND_SPEED, 179 | item::IRON_SHOVEL => IRON_SPEED, 180 | item::STONE_SHOVEL => STONE_SPEED, 181 | item::WOOD_SHOVEL => WOOD_SPEED, 182 | item::GOLD_SHOVEL => GOLD_SPEED, 183 | _ => 1.0, 184 | } 185 | } 186 | block::COBWEB => { 187 | match item_id { 188 | item::SHEARS | 189 | item::DIAMOND_SWORD | 190 | item::IRON_SWORD | 191 | item::STONE_SWORD | 192 | item::GOLD_SWORD | 193 | item::WOOD_SWORD => 15.0, 194 | _ => 1.0, 195 | } 196 | } 197 | block::LEAVES => { 198 | match item_id { 199 | item::SHEARS => 15.0, 200 | _ => 1.0, 201 | } 202 | } 203 | _ => match item_id { 204 | item::DIAMOND_SWORD | 205 | item::IRON_SWORD | 206 | item::STONE_SWORD | 207 | item::GOLD_SWORD | 208 | item::WOOD_SWORD => 1.5, 209 | _ => 1.0, 210 | } 211 | } 212 | 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /mc173/src/world/explode.rs: -------------------------------------------------------------------------------- 1 | //! Make explosion in world. 2 | 3 | use glam::{DVec3, IVec3}; 4 | 5 | use tracing::trace; 6 | 7 | use crate::geom::BoundingBox; 8 | use crate::java::JavaRandom; 9 | 10 | use crate::world::bound::RayTraceKind; 11 | use crate::entity::{Entity, Hurt}; 12 | use crate::world::Event; 13 | use crate::block; 14 | 15 | use super::World; 16 | 17 | 18 | /// Methods related to explosions. 19 | impl World { 20 | 21 | /// Make an explosion in the world at the given position and size. The explosion can 22 | /// optionally propagate flames around. 23 | pub fn explode(&mut self, center: DVec3, radius: f32, set_fire: bool, origin_id: Option) { 24 | 25 | /// This is the step to advance each explosion ray. 26 | const STEP: f32 = 0.3; 27 | 28 | trace!("explode, center: {center}, radius: {radius}, set fire: {set_fire}, origin id: {origin_id:?}"); 29 | 30 | let mut rand = JavaRandom::new_seeded(); 31 | let mut affected_pos = Vec::new(); 32 | 33 | // Start by computing each destroyed block. 34 | for dx in 0..16 { 35 | for dy in 0..16 { 36 | for dz in 0..16 { 37 | if dx == 0 || dx == 15 || dy == 0 || dy == 15 || dz == 0 || dz == 15 { 38 | 39 | // Calculate the normalized of the explosion ray. 40 | let dir = (IVec3::new(dx, dy, dz).as_vec3() / 15.0) * 2.0 - 1.0; 41 | let dir = dir.normalize() * STEP; 42 | let dir = dir.as_dvec3(); 43 | 44 | // The initial intensity of this ray of explosion. 45 | let mut intensity = radius * (0.7 + self.rand.next_float() * 0.6); 46 | let mut check_pos = center; 47 | 48 | while intensity > 0.0 { 49 | 50 | let block_pos = check_pos.floor().as_ivec3(); 51 | let Some((block, _)) = self.get_block(block_pos) else { 52 | break // Just abort this ray if we enter unloaded chunk. 53 | }; 54 | 55 | // NOTE: This should properly handle the infinite resistance 56 | // returned by some blocks, this will just set intensity to 57 | // negative infinity and stop the loop. 58 | intensity -= (block::material::get_explosion_resistance(block) + 0.3) * STEP; 59 | if intensity > 0.0 { 60 | 61 | if set_fire 62 | && block == block::AIR 63 | && self.is_block_opaque_cube(block_pos - IVec3::Y) 64 | && rand.next_int_bounded(3) == 0 { 65 | self.set_block_notify(block_pos, block::FIRE, 0); 66 | } 67 | 68 | affected_pos.push((block_pos, block != block::AIR)); 69 | 70 | } 71 | 72 | check_pos += dir; 73 | intensity -= (12.0 / 16.0) * STEP; 74 | 75 | } 76 | 77 | } 78 | } 79 | } 80 | } 81 | 82 | // Calculate the explosion bounding box. 83 | let diameter = (radius * 2.0) as f64; 84 | let bb = BoundingBox { 85 | min: (center - diameter - 1.0).floor(), 86 | max: (center + diameter + 1.0).floor(), 87 | }; 88 | 89 | let mut damaged_entities = Vec::new(); 90 | 91 | // Calculate the amount of damage to apply to each entity in the bounding box. 92 | for (collided_id, Entity(collided_base, _)) in self.iter_entities_colliding(bb) { 93 | 94 | let delta = collided_base.pos - center; 95 | let dist = delta.length(); 96 | let dist_norm = dist as f32 / radius; 97 | 98 | if dist_norm <= 1.0 { 99 | 100 | let dir = delta / dist; 101 | 102 | // The goal here is to compute how many rays starting from every point in 103 | // the entity bounding box we reach the explosion center. The more 104 | let ray = collided_base.bb.min - center; 105 | let step = 1.0 / (collided_base.bb.size() * 2.0 + 1.0); 106 | 107 | // This is the offset to apply to the ray to go to different point into 108 | // the bounding box, step by step. 109 | let mut ray_offset = DVec3::ZERO; 110 | let mut ray_pass = 0usize; 111 | let mut ray_count = 0usize; 112 | 113 | while ray_offset.x <= 1.0 { 114 | ray_offset.y = 0.0; 115 | while ray_offset.y <= 1.0 { 116 | ray_offset.z = 0.0; 117 | while ray_offset.z <= 1.0 { 118 | ray_pass += self.ray_trace_blocks(center, ray + ray_offset, RayTraceKind::Overlay).is_none() as usize; 119 | ray_count += 1; 120 | ray_offset.z += step.z; 121 | } 122 | ray_offset.y += step.y; 123 | } 124 | ray_offset.x += step.x; 125 | } 126 | 127 | // The final damage depends on the distance and the number of rays. 128 | let damage_factor = (1.0 - dist_norm) * (ray_pass as f32 / ray_count as f32); 129 | let damage = (damage_factor * damage_factor + damage_factor) / 2.0 * 8.0 * radius + 1.0; 130 | let damage = damage as u16; 131 | 132 | damaged_entities.push((collided_id, damage, dir * damage_factor as f64)); 133 | 134 | } 135 | 136 | } 137 | 138 | // Finally alter entities. 139 | for (eid, damage, accel) in damaged_entities { 140 | 141 | let Entity(base, _) = self.get_entity_mut(eid).unwrap(); 142 | 143 | base.hurt.push(Hurt { 144 | damage, 145 | origin_id, 146 | }); 147 | 148 | base.vel += accel; 149 | 150 | } 151 | 152 | // Finally drain the destroyed pos and remove blocks. 153 | for (pos, should_destroy) in affected_pos { 154 | if should_destroy { 155 | // We can unwrap because these position were previously checked. 156 | let (prev_block, prev_metadata) = self.set_block_notify(pos, block::AIR, 0).unwrap(); 157 | self.spawn_block_loot(pos, prev_block, prev_metadata, 0.3); 158 | } 159 | } 160 | 161 | self.push_event(Event::Explode { center, radius }); 162 | 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /mc173/src/world/interact.rs: -------------------------------------------------------------------------------- 1 | //! Interaction of players with blocks in the world. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block::material::Material; 6 | use crate::block_entity::BlockEntity; 7 | use crate::geom::Face; 8 | use crate::block; 9 | 10 | use super::{Event, World}; 11 | 12 | 13 | /// Methods related to block interactions when client clicks on a block. 14 | impl World { 15 | 16 | /// Interact with a block at given position. This function returns the interaction 17 | /// result to indicate if the interaction was handled, or if it was 18 | /// 19 | /// The second argument `breaking` indicates if the interaction originate from a 20 | /// player breaking the block. 21 | pub fn interact_block(&mut self, pos: IVec3, breaking: bool) -> Interaction { 22 | if let Some((id, metadata)) = self.get_block(pos) { 23 | self.interact_block_unchecked(pos, id, metadata, breaking) 24 | } else { 25 | Interaction::None 26 | } 27 | } 28 | 29 | /// Internal function to handle block interaction at given position and with known 30 | /// block and metadata. 31 | pub(super) fn interact_block_unchecked(&mut self, pos: IVec3, id: u8, metadata: u8, breaking: bool) -> Interaction { 32 | match id { 33 | block::BUTTON => self.interact_button(pos, metadata), 34 | block::LEVER => self.interact_lever(pos, metadata), 35 | block::TRAPDOOR => self.interact_trapdoor(pos, metadata), 36 | block::IRON_DOOR => true, 37 | block::WOOD_DOOR => self.interact_wood_door(pos, metadata), 38 | block::REPEATER | 39 | block::REPEATER_LIT => self.interact_repeater(pos, id, metadata), 40 | block::REDSTONE_ORE => self.interact_redstone_ore(pos), 41 | block::CRAFTING_TABLE => return Interaction::CraftingTable { pos }, 42 | block::CHEST => return self.interact_chest(pos), 43 | block::FURNACE | 44 | block::FURNACE_LIT => return self.interact_furnace(pos), 45 | block::DISPENSER => return self.interact_dispenser(pos), 46 | block::NOTE_BLOCK => self.interact_note_block(pos, breaking), 47 | _ => return Interaction::None 48 | }.into() 49 | } 50 | 51 | /// Interact with a button block. 52 | fn interact_button(&mut self, pos: IVec3, mut metadata: u8) -> bool { 53 | if !block::button::is_active(metadata) { 54 | block::button::set_active(&mut metadata, true); 55 | self.set_block_notify(pos, block::BUTTON, metadata); 56 | self.schedule_block_tick(pos, block::BUTTON, 20); 57 | } 58 | true 59 | } 60 | 61 | fn interact_lever(&mut self, pos: IVec3, mut metadata: u8) -> bool { 62 | let active = block::lever::is_active(metadata); 63 | block::lever::set_active(&mut metadata, !active); 64 | self.set_block_notify(pos, block::LEVER, metadata); 65 | true 66 | } 67 | 68 | fn interact_trapdoor(&mut self, pos: IVec3, mut metadata: u8) -> bool { 69 | let active = block::trapdoor::is_open(metadata); 70 | block::trapdoor::set_open(&mut metadata, !active); 71 | self.set_block_notify(pos, block::TRAPDOOR, metadata); 72 | true 73 | } 74 | 75 | fn interact_wood_door(&mut self, pos: IVec3, mut metadata: u8) -> bool { 76 | 77 | if block::door::is_upper(metadata) { 78 | if let Some((block::WOOD_DOOR, metadata)) = self.get_block(pos - IVec3::Y) { 79 | self.interact_wood_door(pos - IVec3::Y, metadata); 80 | } 81 | } else { 82 | 83 | let open = block::door::is_open(metadata); 84 | block::door::set_open(&mut metadata, !open); 85 | 86 | self.set_block_notify(pos, block::WOOD_DOOR, metadata); 87 | 88 | if let Some((block::WOOD_DOOR, _)) = self.get_block(pos + IVec3::Y) { 89 | block::door::set_upper(&mut metadata, true); 90 | self.set_block_notify(pos + IVec3::Y, block::WOOD_DOOR, metadata); 91 | } 92 | 93 | } 94 | 95 | true 96 | 97 | } 98 | 99 | fn interact_repeater(&mut self, pos: IVec3, id: u8, mut metadata: u8) -> bool { 100 | let delay = block::repeater::get_delay(metadata); 101 | block::repeater::set_delay(&mut metadata, (delay + 1) % 4); 102 | self.set_block_notify(pos, id, metadata); 103 | true 104 | } 105 | 106 | fn interact_redstone_ore(&mut self, pos: IVec3) -> bool { 107 | self.set_block_notify(pos, block::REDSTONE_ORE_LIT, 0); 108 | false // Notchian client lit the ore but do not mark the interaction. 109 | } 110 | 111 | fn interact_chest(&mut self, pos: IVec3) -> Interaction { 112 | 113 | let Some(BlockEntity::Chest(_)) = self.get_block_entity(pos) else { 114 | return Interaction::Handled 115 | }; 116 | 117 | if self.is_block_opaque_cube(pos + IVec3::Y) { 118 | return Interaction::Handled; 119 | } 120 | 121 | for face in Face::HORIZONTAL { 122 | let face_pos = pos + face.delta(); 123 | if self.is_block(face_pos, block::CHEST) && self.is_block_opaque_cube(face_pos + IVec3::Y) { 124 | return Interaction::Handled; 125 | } 126 | } 127 | 128 | let mut all_pos = vec![pos]; 129 | 130 | // NOTE: Same order as Notchian server for parity, we also insert first or last 131 | // depending on the neighbor chest being on neg or pos face, like Notchian client. 132 | for face in [Face::NegX, Face::PosX, Face::NegZ, Face::PosZ] { 133 | let face_pos = pos + face.delta(); 134 | if let Some(BlockEntity::Chest(_)) = self.get_block_entity(face_pos) { 135 | if face.is_neg() { 136 | all_pos.insert(0, face_pos); 137 | } else { 138 | all_pos.push(face_pos); 139 | } 140 | } 141 | } 142 | 143 | Interaction::Chest { pos: all_pos } 144 | 145 | } 146 | 147 | fn interact_furnace(&mut self, pos: IVec3) -> Interaction { 148 | if let Some(BlockEntity::Furnace(_)) = self.get_block_entity(pos) { 149 | Interaction::Furnace { pos } 150 | } else { 151 | Interaction::None 152 | } 153 | } 154 | 155 | fn interact_dispenser(&mut self, pos: IVec3) -> Interaction { 156 | if let Some(BlockEntity::Dispenser(_)) = self.get_block_entity(pos) { 157 | Interaction::Dispenser { pos } 158 | } else { 159 | Interaction::None 160 | } 161 | } 162 | 163 | fn interact_note_block(&mut self, pos: IVec3, breaking: bool) -> bool { 164 | 165 | let Some(BlockEntity::NoteBlock(note_block)) = self.get_block_entity_mut(pos) else { 166 | return true; 167 | }; 168 | 169 | if !breaking { 170 | note_block.note = (note_block.note + 1) % 25; 171 | } 172 | 173 | let note = note_block.note; 174 | 175 | if !self.is_block_air(pos + IVec3::Y) { 176 | return true; 177 | } 178 | 179 | let instrument = match self.get_block_material(pos - IVec3::Y) { 180 | Material::Rock => 1, 181 | Material::Sand => 2, 182 | Material::Glass => 3, 183 | Material::Wood => 4, 184 | _ => 0, 185 | }; 186 | 187 | self.push_event(Event::Block { 188 | pos, 189 | inner: super::BlockEvent::NoteBlock { 190 | instrument, 191 | note, 192 | }, 193 | }); 194 | 195 | true 196 | 197 | } 198 | 199 | } 200 | 201 | 202 | /// The result of an interaction with a block in the world. 203 | #[derive(Debug, Clone)] 204 | pub enum Interaction { 205 | /// No interaction has been handled. 206 | None, 207 | /// An interaction has been handled by the world. 208 | Handled, 209 | /// A crafting table has been interacted, the front-end should interpret this and 210 | /// open the crafting table window. 211 | CraftingTable { 212 | /// Position of the crafting table being interacted. 213 | pos: IVec3, 214 | }, 215 | /// A chest has been interacted, the front-end should interpret this and open the 216 | /// chest window. 217 | Chest { 218 | /// Positions of the chest block entities to connect to, from top layer in the 219 | /// window to bottom one. They have been checked to exists before. 220 | pos: Vec, 221 | }, 222 | /// A furnace has been interacted, the front-end should interpret this and open the 223 | /// furnace window. 224 | Furnace { 225 | /// Position of the furnace block entity to connect to, it has been checked to 226 | /// exists. 227 | pos: IVec3, 228 | }, 229 | /// A dispenser has been interacted, the front-end should interpret this and open 230 | /// the dispenser window. 231 | Dispenser { 232 | /// Position of the dispenser block entity to connect to, it has been checked to 233 | /// exists. 234 | pos: IVec3, 235 | }, 236 | } 237 | 238 | impl From for Interaction { 239 | #[inline] 240 | fn from(value: bool) -> Self { 241 | if value { Self::Handled } else { Self::None } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /mc173/src/world/loot.rs: -------------------------------------------------------------------------------- 1 | //! Looting functions to spawn items in a world, also contains the loots for each block. 2 | 3 | use std::ops::{Mul, Sub}; 4 | 5 | use glam::{IVec3, DVec3}; 6 | 7 | use crate::entity::Item; 8 | use crate::item::ItemStack; 9 | use crate::{block, item}; 10 | 11 | use super::World; 12 | 13 | 14 | /// Methods related to loot spawning in the world and block loot randomization. 15 | impl World { 16 | 17 | /// Spawn item entity in the world containing the given stack. The velocity of the 18 | /// spawned item stack is random and the initial position depends on the given spread. 19 | /// This item entity will be impossible to pickup for 10 ticks. 20 | pub fn spawn_loot(&mut self, mut pos: DVec3, stack: ItemStack, spread: f32) { 21 | 22 | if spread != 0.0 { 23 | pos += self.rand.next_float_vec() 24 | .mul(spread) 25 | .as_dvec3() 26 | .sub(spread as f64 * 0.5); 27 | } 28 | 29 | let entity = Item::new_with(|base, item| { 30 | base.persistent = true; 31 | base.pos = pos; 32 | base.vel.x = self.rand.next_double() * 0.2 - 0.1; 33 | base.vel.y = 0.2; 34 | base.vel.z = self.rand.next_double() * 0.2 - 0.1; 35 | item.stack = stack; 36 | item.frozen_time = 10; 37 | }); 38 | 39 | self.spawn_entity(entity); 40 | 41 | } 42 | 43 | /// Spawn item entities in the world depending on the loot of the given block id and 44 | /// metadata. Each block has a different random try count and loots, the given chance 45 | /// if looting is checked on each try, typically used for explosions. 46 | pub fn spawn_block_loot(&mut self, pos: IVec3, id: u8, metadata: u8, chance: f32) { 47 | let tries = self.get_block_loot_tries(id, metadata); 48 | for try_num in 0..tries { 49 | if self.rand.next_float() <= self.get_block_loot_chance(id, metadata, try_num, chance) { 50 | let stack = self.get_block_loot_stack(id, metadata, try_num); 51 | if !stack.is_empty() { 52 | self.spawn_loot(pos.as_dvec3() + 0.5, stack, 0.7); 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// Get the tries count from a block and metadata. 59 | fn get_block_loot_tries(&mut self, id: u8, _metadata: u8) -> u8 { 60 | match id { 61 | block::AIR => 0, 62 | block::BOOKSHELF => 0, 63 | block::CAKE => 0, 64 | block::CLAY => 4, 65 | block::WHEAT => 4, // 1 for wheat item + 3 for seeds 66 | block::FIRE => 0, 67 | block::WATER_MOVING | 68 | block::WATER_STILL | 69 | block::LAVA_MOVING | 70 | block::LAVA_STILL => 0, 71 | block::GLASS => 0, 72 | block::GLOWSTONE => 2 + self.rand.next_int_bounded(3) as u8, 73 | block::ICE => 0, 74 | block::LEAVES if self.rand.next_int_bounded(20) != 0 => 0, 75 | block::SPAWNER => 0, 76 | block::LAPIS_ORE => 4 + self.rand.next_int_bounded(5) as u8, 77 | block::PISTON_EXT | 78 | block::PISTON_MOVING => 0, 79 | block::PORTAL => 0, 80 | block::REDSTONE_ORE | 81 | block::REDSTONE_ORE_LIT => 4 + self.rand.next_int_bounded(2) as u8, 82 | block::SNOW => 0, 83 | block::SNOW_BLOCK => 4, 84 | block::DOUBLE_SLAB => 2, 85 | block::TNT => 0, 86 | _ => 1 87 | } 88 | } 89 | 90 | fn get_block_loot_chance(&mut self, id: u8, metadata: u8, try_num: u8, default_chance: f32) -> f32 { 91 | match id { 92 | block::WHEAT if try_num != 0 => metadata as f32 / 14.0, // Fully grown wheat have 0.5 chance. 93 | _ => default_chance, 94 | } 95 | } 96 | 97 | /// Get the drop item stack from a block and metadata. This is called for each try. 98 | fn get_block_loot_stack(&mut self, id: u8, metadata: u8, try_num: u8) -> ItemStack { 99 | match id { 100 | // Bed only drop if not head piece. 101 | block::BED if block::bed::is_head(metadata) => ItemStack::EMPTY, 102 | block::BED => ItemStack::new(item::BED, 0), 103 | // Cake. 104 | block::CAKE => ItemStack::EMPTY, 105 | // Clay. 106 | block::CLAY => ItemStack::new(item::CLAY, 0), 107 | // Wheat, only drop if reached max stage. 108 | block::WHEAT if try_num == 0 && metadata != 7 => return ItemStack::EMPTY, 109 | block::WHEAT if try_num == 0 => ItemStack::new(item::WHEAT, 0), 110 | block::WHEAT => ItemStack::new(item::WHEAT_SEEDS, 0), 111 | // Dead bush. 112 | block::DEAD_BUSH => ItemStack::EMPTY, 113 | // Door only drop if lower part. 114 | block::WOOD_DOOR | 115 | block::IRON_DOOR if block::door::is_upper(metadata) => ItemStack::EMPTY, 116 | block::WOOD_DOOR => ItemStack::new(item::WOOD_DOOR, 0), 117 | block::IRON_DOOR => ItemStack::new(item::IRON_DOOR, 0), 118 | // Farmland and grass. 119 | block::FARMLAND | 120 | block::GRASS => ItemStack::new_block(block::DIRT, 0), 121 | // Fluids. 122 | block::WATER_MOVING | 123 | block::WATER_STILL | 124 | block::LAVA_MOVING | 125 | block::LAVA_STILL => ItemStack::EMPTY, 126 | // Furnace. 127 | block::FURNACE | 128 | block::FURNACE_LIT => ItemStack::new_block(block::FURNACE, 0), 129 | // Glowstone. 130 | block::GLOWSTONE => ItemStack::new(item::GLOWSTONE_DUST, 0), 131 | // Gravel. 132 | block::GRAVEL if self.rand.next_int_bounded(10) == 0 => ItemStack::new(item::FLINT, 0), 133 | // Leaves. 134 | block::LEAVES => ItemStack::new_block(block::SAPLING, metadata & 3), 135 | // Spawner. 136 | block::SPAWNER => ItemStack::EMPTY, 137 | // Ores. 138 | block::COAL_ORE => ItemStack::new(item::COAL, 0), 139 | block::DIAMOND_ORE => ItemStack::new(item::DIAMOND, 0), 140 | block::REDSTONE_ORE | 141 | block::REDSTONE_ORE_LIT => ItemStack::new(item::REDSTONE, 0), 142 | block::LAPIS_ORE => ItemStack::new(item::DYE, 4), 143 | // Piston. 144 | block::PISTON_EXT | 145 | block::PISTON_MOVING => ItemStack::EMPTY, 146 | // Redstone components. 147 | block::REDSTONE => ItemStack::new(item::REDSTONE, 0), 148 | block::REPEATER | 149 | block::REPEATER_LIT => ItemStack::new(item::REPEATER, 0), 150 | block::REDSTONE_TORCH | 151 | block::REDSTONE_TORCH_LIT => ItemStack::new_block(block::REDSTONE_TORCH_LIT, 0), 152 | // Sugar cane. 153 | block::SUGAR_CANES => ItemStack::new(item::SUGAR_CANES, 0), 154 | // Signs. 155 | block::SIGN | 156 | block::WALL_SIGN => ItemStack::new(item::SIGN, 0), 157 | // Snow. 158 | block::SNOW_BLOCK | 159 | block::SNOW => ItemStack::new(item::SNOWBALL, 0), 160 | // Double slab. 161 | block::SLAB | 162 | block::DOUBLE_SLAB => ItemStack::new_block(block::SLAB, metadata), 163 | // Stone. 164 | block::STONE => ItemStack::new_block(block::COBBLESTONE, 0), 165 | // Tall grass. 166 | block::TALL_GRASS if self.rand.next_int_bounded(8) == 0 => ItemStack::new(item::WHEAT_SEEDS, 0), 167 | block::TALL_GRASS => ItemStack::EMPTY, 168 | // Cobweb. 169 | block::COBWEB => ItemStack::new(item::STRING, 0), 170 | // Log type. 171 | block::LOG => ItemStack::new_block(block::LOG, metadata), 172 | // Wool color. 173 | block::WOOL => ItemStack::new_block(block::WOOL, metadata), 174 | // Sapling type. 175 | block::SAPLING => ItemStack::new_block(block::SAPLING, metadata & 3), 176 | // Default, drop the block's item. 177 | _ => ItemStack::new_block(id, 0), 178 | } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /mc173/src/world/material.rs: -------------------------------------------------------------------------------- 1 | //! Various shortcut methods to directly check block materials. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block::material::Material; 6 | use crate::block; 7 | 8 | use super::World; 9 | 10 | 11 | /// Shortcut methods for directly querying and checking a block material and properties. 12 | impl World { 13 | 14 | /// Get the block material at given position, defaults to air if no chunk. 15 | pub fn get_block_material(&self, pos: IVec3) -> Material { 16 | self.get_block(pos).map(|(id, _)| block::material::get_material(id)).unwrap_or_default() 17 | } 18 | 19 | /// Return true if the block at given position can be replaced. 20 | pub fn is_block_replaceable(&self, pos: IVec3) -> bool { 21 | if let Some((id, _)) = self.get_block(pos) { 22 | block::material::get_material(id).is_replaceable() 23 | } else { 24 | false 25 | } 26 | } 27 | 28 | /// Return true if the block at position is an opaque cube. 29 | /// 30 | /// FIXME: A lot of calls to this function should instead be for "normal_cube". This 31 | /// is not exactly the same properties in the Notchian implementation. 32 | pub fn is_block_opaque_cube(&self, pos: IVec3) -> bool { 33 | if let Some((id, _)) = self.get_block(pos) { 34 | block::material::is_opaque_cube(id) 35 | } else { 36 | false 37 | } 38 | } 39 | 40 | /// Return true if the block at position is a normal cube. 41 | pub fn is_block_normal_cube(&self, pos: IVec3) -> bool { 42 | if let Some((id, _)) = self.get_block(pos) { 43 | block::material::is_normal_cube(id) 44 | } else { 45 | false 46 | } 47 | } 48 | 49 | /// Return true if the block at position is material solid. 50 | pub fn is_block_solid(&self, pos: IVec3) -> bool { 51 | if let Some((id, _)) = self.get_block(pos) { 52 | block::material::get_material(id).is_solid() 53 | } else { 54 | false 55 | } 56 | } 57 | 58 | /// Return true if the block at position is air. 59 | #[inline] 60 | pub fn is_block_air(&self, pos: IVec3) -> bool { 61 | if let Some((id, _)) = self.get_block(pos) { 62 | id == block::AIR 63 | } else { 64 | true 65 | } 66 | } 67 | 68 | /// Return true if the block at position is the given one. 69 | #[inline] 70 | pub fn is_block(&self, pos: IVec3, id: u8) -> bool { 71 | if let Some((pos_id, _)) = self.get_block(pos) { 72 | pos_id == id 73 | } else { 74 | false // TODO: id == block::AIR ? because non existing position are air 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /mc173/src/world/place.rs: -------------------------------------------------------------------------------- 1 | //! Advanced block placing methods. 2 | 3 | use glam::IVec3; 4 | 5 | use crate::block_entity::BlockEntity; 6 | use crate::block::material::Material; 7 | use crate::util::default as def; 8 | use crate::geom::Face; 9 | use crate::block; 10 | 11 | use super::World; 12 | 13 | 14 | /// Methods related to block placing. 15 | impl World { 16 | 17 | /// This function checks if the given block id can be placed at a particular position in 18 | /// the world, the given face indicates toward which face this block should be oriented. 19 | pub fn can_place_block(&mut self, pos: IVec3, face: Face, id: u8) -> bool { 20 | 21 | let base = match id { 22 | block::BUTTON if face.is_y() => false, 23 | block::BUTTON => self.is_block_opaque_cube(pos + face.delta()), 24 | block::LEVER if face == Face::PosY => false, 25 | block::LEVER => self.is_block_opaque_cube(pos + face.delta()), 26 | block::LADDER => self.is_block_opaque_around(pos), 27 | block::TRAPDOOR if face.is_y() => false, 28 | block::TRAPDOOR => self.is_block_opaque_cube(pos + face.delta()), 29 | block::PISTON_EXT | 30 | block::PISTON_MOVING => false, 31 | block::DEAD_BUSH => matches!(self.get_block(pos - IVec3::Y), Some((block::SAND, _))), 32 | // PARITY: Notchian impl checks block light >= 8 or see sky 33 | block::DANDELION | 34 | block::POPPY | 35 | block::SAPLING | 36 | block::TALL_GRASS => matches!(self.get_block(pos - IVec3::Y), Some((block::GRASS | block::DIRT | block::FARMLAND, _))), 37 | block::WHEAT => matches!(self.get_block(pos - IVec3::Y), Some((block::FARMLAND, _))), 38 | block::CACTUS => self.can_place_cactus(pos), 39 | block::SUGAR_CANES => self.can_place_sugar_canes(pos), 40 | block::CAKE => self.is_block_solid(pos - IVec3::Y), 41 | block::CHEST => self.can_place_chest(pos), 42 | block::WOOD_DOOR | 43 | block::IRON_DOOR => self.can_place_door(pos), 44 | block::FENCE => matches!(self.get_block(pos - IVec3::Y), Some((block::FENCE, _))) || self.is_block_solid(pos - IVec3::Y), 45 | block::FIRE => self.can_place_fire(pos), 46 | block::TORCH | 47 | block::REDSTONE_TORCH | 48 | block::REDSTONE_TORCH_LIT => self.is_block_normal_cube(pos + face.delta()), 49 | // Common blocks that needs opaque block below. 50 | block::RED_MUSHROOM | // PARITY: Notchian impl checks block light >= 8 or see sky 51 | block::BROWN_MUSHROOM => self.is_block_opaque_cube(pos - IVec3::Y), 52 | block::SNOW => self.is_block_opaque_cube(pos - IVec3::Y), 53 | block::WOOD_PRESSURE_PLATE | 54 | block::STONE_PRESSURE_PLATE | 55 | block::PUMPKIN | 56 | block::PUMPKIN_LIT | 57 | block::RAIL | 58 | block::POWERED_RAIL | 59 | block::DETECTOR_RAIL | 60 | block::REPEATER | 61 | block::REPEATER_LIT | 62 | block::REDSTONE => self.is_block_normal_cube(pos - IVec3::Y), 63 | _ => true, 64 | }; 65 | 66 | // If the block we are placing has an exclusion box and any hard entity is inside, 67 | // we cancel the prevent the placing. 68 | if let Some(bb) = self.get_block_exclusion_box(pos, id) { 69 | if self.has_entity_colliding(bb, true) { 70 | return false; 71 | } 72 | } 73 | 74 | base && self.is_block_replaceable(pos) 75 | 76 | } 77 | 78 | fn can_place_cactus(&mut self, pos: IVec3) -> bool { 79 | for face in Face::HORIZONTAL { 80 | if self.is_block_solid(pos + face.delta()) { 81 | return false; 82 | } 83 | } 84 | matches!(self.get_block(pos - IVec3::Y), Some((block::CACTUS | block::SAND, _))) 85 | } 86 | 87 | fn can_place_sugar_canes(&mut self, pos: IVec3) -> bool { 88 | let below_pos = pos - IVec3::Y; 89 | if let Some((block::SUGAR_CANES | block::GRASS | block::DIRT, _)) = self.get_block(below_pos) { 90 | for face in Face::HORIZONTAL { 91 | if self.get_block_material(below_pos + face.delta()) == Material::Water { 92 | return true; 93 | } 94 | } 95 | } 96 | false 97 | } 98 | 99 | fn can_place_chest(&mut self, pos: IVec3) -> bool { 100 | let mut found_single_chest = false; 101 | for face in Face::HORIZONTAL { 102 | // If block on this face is a chest, check if that block also has a chest. 103 | let neighbor_pos = pos + face.delta(); 104 | if matches!(self.get_block(neighbor_pos), Some((block::CHEST, _))) { 105 | // We can't put chest 106 | if found_single_chest { 107 | return false; 108 | } 109 | // Check if the chest we found isn't a double chest. 110 | for neighbor_face in Face::HORIZONTAL { 111 | // Do not check our potential position. 112 | if face != neighbor_face.opposite() { 113 | if matches!(self.get_block(neighbor_pos + neighbor_face.delta()), Some((block::CHEST, _))) { 114 | return false; // The chest found already is double. 115 | } 116 | } 117 | } 118 | // No other chest found, it's a single chest. 119 | found_single_chest = true; 120 | } 121 | } 122 | true 123 | } 124 | 125 | fn can_place_door(&mut self, pos: IVec3) -> bool { 126 | self.is_block_opaque_cube(pos - IVec3::Y) && self.is_block_replaceable(pos + IVec3::Y) 127 | } 128 | 129 | fn can_place_fire(&mut self, pos: IVec3) -> bool { 130 | if self.is_block_opaque_cube(pos - IVec3::Y) { 131 | true 132 | } else { 133 | for face in Face::ALL { 134 | if let Some((block, _)) = self.get_block(pos + face.delta()) { 135 | if block::material::get_fire_flammability(block) != 0 { 136 | return true; 137 | } 138 | } 139 | } 140 | false 141 | } 142 | } 143 | 144 | /// Place the block at the given position in the world oriented toward given face. Note 145 | /// that this function do not check if this is legal, it will do what's asked. Also, the 146 | /// given metadata may be modified to account for the placement. 147 | pub fn place_block(&mut self, pos: IVec3, face: Face, id: u8, metadata: u8) { 148 | 149 | match id { 150 | block::BUTTON => self.place_faced(pos, face, id, metadata, block::button::set_face), 151 | block::TRAPDOOR => self.place_faced(pos, face, id, metadata, block::trapdoor::set_face), 152 | block::PISTON | 153 | block::STICKY_PISTON => self.place_faced(pos, face, id, metadata, block::piston::set_face), 154 | block::WOOD_STAIR | 155 | block::COBBLESTONE_STAIR => self.place_faced(pos, face, id, metadata, block::stair::set_face), 156 | block::REPEATER | 157 | block::REPEATER_LIT => self.place_faced(pos, face, id, metadata, block::repeater::set_face), 158 | block::PUMPKIN | 159 | block::PUMPKIN_LIT => self.place_faced(pos, face, id, metadata, block::pumpkin::set_face), 160 | block::FURNACE | 161 | block::FURNACE_LIT | 162 | block::DISPENSER => self.place_faced(pos, face, id, metadata, block::dispenser::set_face), 163 | block::TORCH | 164 | block::REDSTONE_TORCH | 165 | block::REDSTONE_TORCH_LIT => self.place_faced(pos, face, id, metadata, block::torch::set_face), 166 | block::LEVER => self.place_lever(pos, face, metadata), 167 | block::LADDER => self.place_ladder(pos, face, metadata), 168 | _ => { 169 | self.set_block_notify(pos, id, metadata); 170 | } 171 | } 172 | 173 | match id { 174 | block::CHEST => self.set_block_entity(pos, BlockEntity::Chest(def())), 175 | block::FURNACE => self.set_block_entity(pos, BlockEntity::Furnace(def())), 176 | block::DISPENSER => self.set_block_entity(pos, BlockEntity::Dispenser(def())), 177 | block::SPAWNER => self.set_block_entity(pos, BlockEntity::Spawner(def())), 178 | block::NOTE_BLOCK => self.set_block_entity(pos, BlockEntity::NoteBlock(def())), 179 | block::JUKEBOX => self.set_block_entity(pos, BlockEntity::Jukebox(def())), 180 | _ => {} 181 | } 182 | 183 | } 184 | 185 | /// Generic function to place a block that has a basic facing function. 186 | fn place_faced(&mut self, pos: IVec3, face: Face, id: u8, mut metadata: u8, func: impl FnOnce(&mut u8, Face)) { 187 | func(&mut metadata, face); 188 | self.set_block_notify(pos, id, metadata); 189 | } 190 | 191 | fn place_lever(&mut self, pos: IVec3, face: Face, mut metadata: u8) { 192 | // When facing down, randomly pick the orientation. 193 | block::lever::set_face(&mut metadata, face, match face { 194 | Face::NegY => self.rand.next_choice(&[Face::PosZ, Face::PosX]), 195 | _ => Face::PosY, 196 | }); 197 | self.set_block_notify(pos, block::LEVER, metadata); 198 | } 199 | 200 | fn place_ladder(&mut self, pos: IVec3, mut face: Face, mut metadata: u8) { 201 | // Privileging desired face, but if desired face cannot support a ladder. 202 | if face.is_y() || !self.is_block_opaque_cube(pos + face.delta()) { 203 | // NOTE: Order is important for parity with client. 204 | for around_face in [Face::PosZ, Face::NegZ, Face::PosX, Face::NegX] { 205 | if self.is_block_opaque_cube(pos + around_face.delta()) { 206 | face = around_face; 207 | break; 208 | } 209 | } 210 | } 211 | block::ladder::set_face(&mut metadata, face); 212 | self.set_block_notify(pos, block::LADDER, metadata); 213 | } 214 | 215 | /// Check is there are at least one opaque block around horizontally. 216 | fn is_block_opaque_around(&mut self, pos: IVec3) -> bool { 217 | for face in Face::HORIZONTAL { 218 | if self.is_block_opaque_cube(pos + face.delta()) { 219 | return true; 220 | } 221 | } 222 | false 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /mc173/src/world/power.rs: -------------------------------------------------------------------------------- 1 | //! Redstone power calculations. The behavior of each power-producing block is described 2 | //! in this module. 3 | 4 | use glam::IVec3; 5 | 6 | use crate::geom::{Face, FaceSet}; 7 | use crate::block; 8 | 9 | use super::World; 10 | 11 | 12 | /// Methods related to redstone power calculation in the world. 13 | impl World { 14 | 15 | /// Check if the given block position get any active power from surrounding faces. 16 | #[inline] 17 | pub fn has_active_power(&mut self, pos: IVec3) -> bool { 18 | Face::ALL.into_iter().any(|face| self.has_active_power_from(pos + face.delta(), face.opposite())) 19 | } 20 | 21 | /// Check if the given block position get any passive power from surrounding faces. 22 | #[inline] 23 | pub fn has_passive_power(&mut self, pos: IVec3) -> bool { 24 | Face::ALL.into_iter().any(|face| self.has_passive_power_from(pos + face.delta(), face.opposite())) 25 | } 26 | 27 | /// Return true if the given block's face produces any active power. 28 | #[inline] 29 | pub fn has_active_power_from(&mut self, pos: IVec3, face: Face) -> bool { 30 | self.get_active_power_from(pos, face) > 0 31 | } 32 | 33 | /// Return true if the given block's face has any passive power. 34 | #[inline] 35 | pub fn has_passive_power_from(&mut self, pos: IVec3, face: Face) -> bool { 36 | self.get_passive_power_from(pos, face) > 0 37 | } 38 | 39 | /// Get the active power produced by a block's face. 40 | pub fn get_active_power_from(&mut self, pos: IVec3, face: Face) -> u8 { 41 | let power = self.get_power_from(pos, face, true); 42 | if power.indirect || !power.passive { 43 | power.level 44 | } else { 45 | 0 46 | } 47 | } 48 | 49 | /// Get the passive power of a block's face. 50 | pub fn get_passive_power_from(&mut self, pos: IVec3, face: Face) -> u8 { 51 | self.get_power_from(pos, face, true).level 52 | } 53 | 54 | /// Get the power produced by a block on a given face. 55 | fn get_power_from(&mut self, pos: IVec3, face: Face, test_block: bool) -> Power { 56 | 57 | let Some((id, metadata)) = self.get_block(pos) else { return Power::OFF }; 58 | 59 | match id { 60 | block::LEVER => self.get_lever_power_from(face, metadata), 61 | block::BUTTON => self.get_button_power_from(face, metadata), 62 | block::REPEATER_LIT => self.get_repeater_power_from(face, metadata), 63 | block::REDSTONE_TORCH_LIT => self.get_redstone_torch_power_from(face, metadata), 64 | block::REDSTONE => self.get_redstone_power_from(pos, face, metadata), 65 | // Opaque block relaying indirect power 66 | _ if test_block && block::material::is_opaque_cube(id) => 67 | self.get_block_power_from(pos, face), 68 | // Non-redstone blocks 69 | _ => Power::OFF 70 | } 71 | 72 | } 73 | 74 | /// Get the power of a block that would be indirectly powered. 75 | fn get_block_power_from(&mut self, pos: IVec3, face: Face) -> Power { 76 | 77 | // By default the block is passive, but if a face has a non-passive power then is 78 | // will no longer be passive. 79 | let mut ret = Power { level: 0, indirect: false, passive: true }; 80 | 81 | // Find the maximum 82 | for test_face in [Face::NegY, Face::PosY, Face::NegZ, Face::PosZ, Face::NegX, Face::PosX] { 83 | if test_face != face { 84 | 85 | // Test the power coming from that face, but disable 'test_block' to avoid 86 | // infinite recursion between those two functions, this assumption is valid 87 | // because a block cannot retransmit other block's power. 88 | let power = self.get_power_from(pos + test_face.delta(), test_face.opposite(), false); 89 | // Only relay the power if the face provides indirect power. 90 | if power.indirect { 91 | 92 | if !power.passive && ret.passive { 93 | ret.level = power.level; 94 | ret.passive = false; 95 | } else if power.passive == ret.passive && power.level > ret.level { 96 | ret.level = power.level; 97 | } 98 | 99 | // If return value is not passive and already maximum level, return. 100 | if !ret.passive && ret.level >= 15 { 101 | break; 102 | } 103 | 104 | } 105 | 106 | } 107 | } 108 | 109 | ret 110 | 111 | } 112 | 113 | fn get_lever_power_from(&mut self, face: Face, metadata: u8) -> Power { 114 | if block::lever::is_active(metadata) { 115 | if block::lever::get_face(metadata).map(|(f, _)| f) == Some(face) { 116 | Power::ON_INDIRECT 117 | } else { 118 | Power::ON_DIRECT 119 | } 120 | } else { 121 | Power::OFF 122 | } 123 | } 124 | 125 | fn get_button_power_from(&mut self, face: Face, metadata: u8) -> Power { 126 | if block::button::is_active(metadata) { 127 | if block::button::get_face(metadata) == Some(face) { 128 | Power::ON_INDIRECT 129 | } else { 130 | Power::ON_DIRECT 131 | } 132 | } else { 133 | Power::OFF 134 | } 135 | } 136 | 137 | fn get_repeater_power_from(&mut self, face: Face, metadata: u8) -> Power { 138 | if block::repeater::get_face(metadata) == face { 139 | Power::ON_INDIRECT 140 | } else { 141 | Power::OFF 142 | } 143 | } 144 | 145 | fn get_redstone_torch_power_from(&mut self, face: Face, metadata: u8) -> Power { 146 | if block::torch::get_face(metadata) == Some(face) { 147 | Power::OFF 148 | } else if face == Face::PosY { 149 | Power::ON_INDIRECT 150 | } else { 151 | Power::ON_DIRECT 152 | } 153 | } 154 | 155 | fn get_redstone_power_from(&mut self, pos: IVec3, face: Face, metadata: u8) -> Power { 156 | if face == Face::PosY || metadata == 0 { 157 | Power::OFF 158 | } else if face == Face::NegY { 159 | Power { level: metadata, indirect: true, passive: true } 160 | } else { 161 | 162 | let mut links = FaceSet::new(); 163 | 164 | let opaque_above = self.get_block(pos + IVec3::Y) 165 | .map(|(above_id, _)| block::material::is_opaque_cube(above_id)) 166 | .unwrap_or(true); 167 | 168 | for face in [Face::NegX, Face::PosX, Face::NegZ, Face::PosZ] { 169 | let face_pos = pos + face.delta(); 170 | if self.is_linkable_from(face_pos, face.opposite()) { 171 | links.insert(face); 172 | } else { 173 | if let Some((id, _)) = self.get_block(face_pos) { 174 | if !block::material::is_opaque_cube(id) { 175 | if self.is_linkable_from(face_pos - IVec3::Y, Face::PosY) { 176 | links.insert(face); 177 | } 178 | } else if !opaque_above { 179 | if self.is_linkable_from(face_pos + IVec3::Y, Face::NegY) { 180 | links.insert(face); 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | // Check if the redstone wire is directly pointing to its horizontal faces, 188 | // if so the current is indirect and can be transmitted through the face block, 189 | // if not it is just a passive signal that can be detected by repeaters. 190 | let indirect = if links.is_empty() { 191 | // The redstone wire has no links, so it has a cross shape and provide power 192 | // to all sides. 193 | true 194 | } else { 195 | match face { 196 | Face::NegZ => links.contains(Face::PosZ) && !links.contains_x(), 197 | Face::PosZ => links.contains(Face::NegZ) && !links.contains_x(), 198 | Face::NegX => links.contains(Face::PosX) && !links.contains_z(), 199 | Face::PosX => links.contains(Face::NegX) && !links.contains_z(), 200 | _ => unreachable!() 201 | } 202 | }; 203 | 204 | Power { level: metadata, indirect, passive: true } 205 | 206 | } 207 | } 208 | 209 | /// Return true if the block at given position can link to a redstone wire from its 210 | /// given face. 211 | fn is_linkable_from(&mut self, pos: IVec3, face: Face) -> bool { 212 | if let Some((id, metadata)) = self.get_block(pos) { 213 | match id { 214 | block::LEVER | 215 | block::BUTTON | 216 | block::DETECTOR_RAIL | 217 | block::WOOD_PRESSURE_PLATE | 218 | block::STONE_PRESSURE_PLATE | 219 | block::REDSTONE_TORCH | 220 | block::REDSTONE_TORCH_LIT | 221 | block::REDSTONE => true, 222 | block::REPEATER | 223 | block::REPEATER_LIT => { 224 | let repeater_face = block::repeater::get_face(metadata); 225 | face == repeater_face.opposite() 226 | } 227 | _ => false 228 | } 229 | } else { 230 | false 231 | } 232 | } 233 | 234 | } 235 | 236 | 237 | /// Internal structure describing the properties of a redstone power signal. 238 | #[derive(Debug)] 239 | struct Power { 240 | /// The redstone power level (0..16). 241 | level: u8, 242 | /// If this power can be relayed indirectly by opaque blocks. 243 | indirect: bool, 244 | /// If this power is passive. 245 | passive: bool, 246 | } 247 | 248 | impl Power { 249 | 250 | const OFF: Self = Self { level: 0, indirect: false, passive: false }; 251 | const ON_INDIRECT: Self = Self { level: 15, indirect: true, passive: false }; 252 | const ON_DIRECT: Self = Self { level: 15, indirect: false, passive: false }; 253 | 254 | } 255 | --------------------------------------------------------------------------------