diff --git a/src/assets/build/downloading.rs b/src/assets/build/downloading.rs index ceb560b1..bf814308 100644 --- a/src/assets/build/downloading.rs +++ b/src/assets/build/downloading.rs @@ -11,7 +11,21 @@ pub fn generate() { // Gotta use file locking cos nexttest running multiple builds at doesn't play nice let _lock = lock_generation(assets_dir.join(".generate.lock")); - if !generated_assets_exist(&assets_dir) { + let version_changed = if assets_dir.join("version").exists() + && std::fs::read_to_string(assets_dir.join("version")) + .expect("Failed to read version file") + == crate::SERVER_VERSION + { + info!("Server version unchanged, skipping asset generation"); + false + } else { + info!("Server version changed, regenerating assets"); + std::fs::remove_dir_all(&assets_dir).expect("Failed to remove old generated assets"); + std::fs::create_dir(&assets_dir).expect("Failed to create generated assets directory"); + true + }; + + if !generated_assets_exist(&assets_dir) || version_changed { let server_jar_path = assets_dir.join("server.jar"); if !server_jar_path.exists() { download_jar(server_jar_path); @@ -57,6 +71,8 @@ pub fn generate() { include_str!("notice.txt"), ) .expect("Could not write notice file"); + std::fs::write(assets_dir.join("version"), crate::SERVER_VERSION) + .expect("Could not write version file"); } else { println!("cargo:error=Setup failed"); } diff --git a/src/assets/build/generate_source.rs b/src/assets/build/generate_source.rs index 2c4c4f70..ed575ab8 100644 --- a/src/assets/build/generate_source.rs +++ b/src/assets/build/generate_source.rs @@ -1,5 +1,6 @@ use crate::item_to_block_mapping::write_item_to_block_mapping; use crate::registry_packets::write_registry_packets; +use crate::tag_packets::write_tag_packets; use std::collections::BTreeMap; use std::env; use std::fs; @@ -12,6 +13,7 @@ pub fn generate_source(assets_path: PathBuf) { let blockstates_path = write_blockstates(&out_dir, &reports_dir); let item_to_block_mapping_path = write_item_to_block_mapping(&out_dir, &reports_dir); let registry_packets_path = write_registry_packets(&out_dir, &data_dir); + let tag_packets_path = write_tag_packets(&out_dir, &assets_path, &data_dir); let mut content = String::new(); content.push_str("pub const BLOCKSTATES: &str = include_str!("); @@ -26,6 +28,9 @@ pub fn generate_source(assets_path: PathBuf) { content.push_str("pub const REGISTRY_PACKETS: &str = include_str!("); content.push_str(&format!("{:?}", registry_packets_path.to_string_lossy())); content.push_str(");\n\n"); + content.push_str("pub const TAG_PACKETS: &str = include_str!("); + content.push_str(&format!("{:?}", tag_packets_path.to_string_lossy())); + content.push_str(");\n\n"); content.push_str("pub mod reports {\n"); write_dir(&mut content, &reports_dir, 1); content.push_str("}\n"); diff --git a/src/assets/build/main.rs b/src/assets/build/main.rs index 7b0ba1aa..ee8e7ae6 100644 --- a/src/assets/build/main.rs +++ b/src/assets/build/main.rs @@ -2,12 +2,15 @@ mod downloading; mod generate_source; mod item_to_block_mapping; mod registry_packets; +mod tag_packets; use semver::Version; use std::path::PathBuf; const MIN_JAVA_VERSION: Version = Version::new(21, 0, 0); +const SERVER_VERSION: &str = "1.21.8"; + const NO_JAVA_MESSAGE: &str = "No java install detected. If you have installed one, please add to your path. If you haven't,\ the Adoptium JDK is recommended and you can get it here: https://adoptium.net/temurin/releases?version=25. If you go with another\ JDK you'll need to make sure it supports at least version 21, but higher is better."; diff --git a/src/assets/build/registry_packets.rs b/src/assets/build/registry_packets.rs index f650dfef..57a66d4b 100644 --- a/src/assets/build/registry_packets.rs +++ b/src/assets/build/registry_packets.rs @@ -18,6 +18,12 @@ const REGISTRY_PACKET_IDS: &[&str] = &[ "minecraft:dimension_type", "minecraft:damage_type", "minecraft:banner_pattern", + "minecraft:enchantment", + "minecraft:jukebox_song", + "minecraft:instrument", + "minecraft:test_environment", + "minecraft:test_instance", + "minecraft:dialog", ]; pub fn write_registry_packets(out_dir: &Path, data_dir: &Path) -> PathBuf { diff --git a/src/assets/build/tag_packets.rs b/src/assets/build/tag_packets.rs new file mode 100644 index 00000000..06eefaa0 --- /dev/null +++ b/src/assets/build/tag_packets.rs @@ -0,0 +1,187 @@ +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +type RegistryIds = BTreeMap; +type TagPackets = BTreeMap>>; + +pub fn write_tag_packets(out_dir: &Path, assets_dir: &Path, data_dir: &Path) -> PathBuf { + let extracted_dir = assets_dir + .parent() + .expect("Assets directory should have a parent") + .join("extracted"); + let tags_path = extracted_dir.join("tags.json"); + + println!("cargo:rerun-if-changed={}", tags_path.display()); + + let tags: BTreeMap>> = + serde_json::from_str(&fs::read_to_string(&tags_path).expect("Failed to read tags.json")) + .expect("Failed to parse tags.json"); + + let report_registry_ids = read_report_registry_ids(assets_dir); + let extracted_registry_ids = read_extracted_registry_ids(&extracted_dir); + + let mut tag_packets = TagPackets::new(); + for (tag_registry, tag_set) in tags { + let registry_id = format!("minecraft:{tag_registry}"); + let registry_ids = report_registry_ids + .get(®istry_id) + .cloned() + .or_else(|| extracted_registry_ids.get(®istry_id).cloned()) + .unwrap_or_else(|| read_data_registry_ids(data_dir, ®istry_id)); + + let mut packet_tags = BTreeMap::new(); + for (tag_name, values) in tag_set { + let values = values + .iter() + .map(|value| { + let value = namespaced(value); + *registry_ids.get(&value).unwrap_or_else(|| { + panic!("No protocol id for tag value {value} in {registry_id}") + }) + }) + .collect(); + + packet_tags.insert(tag_name, values); + } + + tag_packets.insert(registry_id, packet_tags); + } + + let path = out_dir.join("tag_packets.json"); + let content = serde_json::to_string(&tag_packets).expect("Failed to serialize tag packets"); + fs::write(&path, content).expect("Failed to write tag packets"); + path +} + +fn read_report_registry_ids(assets_dir: &Path) -> BTreeMap { + let path = assets_dir + .join("generated") + .join("reports") + .join("registries.json"); + let registries: Value = + serde_json::from_str(&fs::read_to_string(path).expect("Failed to read registries report")) + .expect("Failed to parse registries report"); + + let mut ids = BTreeMap::new(); + let Some(registries) = registries.as_object() else { + return ids; + }; + + for (registry_id, registry) in registries { + let Some(entries) = registry.get("entries").and_then(Value::as_object) else { + continue; + }; + + let mut registry_ids = RegistryIds::new(); + for (entry_id, entry) in entries { + let protocol_id = entry + .get("protocol_id") + .and_then(Value::as_i64) + .expect("Registry report entry missing protocol_id"); + registry_ids.insert(namespaced(entry_id), protocol_id as i32); + } + + ids.insert(registry_id.clone(), registry_ids); + } + + ids +} + +fn read_extracted_registry_ids(extracted_dir: &Path) -> BTreeMap { + let mut ids = BTreeMap::new(); + + read_extracted_ids( + &mut ids, + "minecraft:enchantment", + &extracted_dir.join("enchantments.json"), + ); + read_extracted_ids( + &mut ids, + "minecraft:entity_type", + &extracted_dir.join("entities.json"), + ); + + ids +} + +fn read_extracted_ids(ids: &mut BTreeMap, registry_id: &str, path: &Path) { + if !path.exists() { + return; + } + + let entries: Value = + serde_json::from_str(&fs::read_to_string(path).expect("Failed to read extracted registry")) + .expect("Failed to parse extracted registry"); + let Some(entries) = entries.as_object() else { + return; + }; + + let registry_ids = ids.entry(registry_id.to_string()).or_default(); + for (entry_id, entry) in entries { + let Some(id) = entry.get("id").and_then(Value::as_i64) else { + continue; + }; + registry_ids + .entry(namespaced(entry_id)) + .or_insert(id as i32); + } +} + +fn read_data_registry_ids(data_dir: &Path, registry_id: &str) -> RegistryIds { + let registry_dir = data_dir.join(registry_id.replacen(':', "/", 1)); + assert!( + registry_dir.exists(), + "Generated registry data missing for {registry_id}" + ); + + read_registry_entries(®istry_dir, ®istry_dir) + .into_keys() + .enumerate() + .map(|(id, entry)| (entry, id as i32)) + .collect() +} + +fn read_registry_entries(dir: &Path, root: &Path) -> BTreeMap { + let mut entries = fs::read_dir(dir) + .expect("Failed to read registry directory") + .collect::, _>>() + .expect("Failed to read registry directory entry"); + + entries.sort_by_key(|entry| entry.path()); + + let mut registry_entries = BTreeMap::new(); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + registry_entries.extend(read_registry_entries(&path, root)); + continue; + } + + if path.extension().and_then(|extension| extension.to_str()) != Some("json") { + continue; + } + + let id = path + .strip_prefix(root) + .expect("Registry entry should be inside registry root") + .with_extension("") + .components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect::>() + .join("/"); + + registry_entries.insert(namespaced(&id), ()); + } + + registry_entries +} + +fn namespaced(value: &str) -> String { + if value.contains(':') { + value.to_string() + } else { + format!("minecraft:{value}") + } +} diff --git a/src/base/macros/src/lib.rs b/src/base/macros/src/lib.rs index da70538c..a93ec254 100644 --- a/src/base/macros/src/lib.rs +++ b/src/base/macros/src/lib.rs @@ -13,6 +13,7 @@ mod net; mod profiling; mod registries_packets; mod static_loading; +mod update_tags; #[proc_macro_attribute] pub fn profile(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -76,6 +77,11 @@ pub fn build_registry_packets(input: TokenStream) -> TokenStream { registries_packets::build_mapping(input) } +#[proc_macro] +pub fn build_update_tags(input: TokenStream) -> TokenStream { + update_tags::build_mapping(input) +} + /// A macro to lookup block state IDs at compile time. /// /// Feed in the block name as a string literal, and an optional set of properties as a map. diff --git a/src/base/macros/src/update_tags.rs b/src/base/macros/src/update_tags.rs new file mode 100644 index 00000000..1170b6a2 --- /dev/null +++ b/src/base/macros/src/update_tags.rs @@ -0,0 +1,18 @@ +use quote::quote; +use std::collections::BTreeMap; + +pub(crate) fn build_mapping(_: proc_macro::TokenStream) -> proc_macro::TokenStream { + let tag_packets: BTreeMap>> = + serde_json::from_str(temper_assets::generated::TAG_PACKETS).unwrap(); + + let tag_packets = tag_packets + .into_iter() + .map(|(registry_id, tags)| (registry_id, tags.into_iter().collect::>())) + .collect::>(); + let raw_packets_data = bitcode::encode(&tag_packets); + + quote! { + vec![#(#raw_packets_data),*] + } + .into() +} diff --git a/src/net/protocol/src/outgoing/mod.rs b/src/net/protocol/src/outgoing/mod.rs index 76a5a17f..9bfc571f 100644 --- a/src/net/protocol/src/outgoing/mod.rs +++ b/src/net/protocol/src/outgoing/mod.rs @@ -59,6 +59,7 @@ pub mod unload_chunk; pub mod hurt_animation; pub mod respawn; pub mod set_health; +pub mod update_tags; pub mod update_time; pub mod synchronise_vehicle_position; diff --git a/src/net/protocol/src/outgoing/registry_data.rs b/src/net/protocol/src/outgoing/registry_data.rs index f39387a8..de9f665a 100644 --- a/src/net/protocol/src/outgoing/registry_data.rs +++ b/src/net/protocol/src/outgoing/registry_data.rs @@ -59,12 +59,24 @@ pub struct RegistryEntry { #[cfg(test)] mod tests { - use crate::outgoing::registry_data::RegistryEntry; + use crate::outgoing::registry_data::{REGISTRY_PACKETS, RegistryEntry}; use indexmap::IndexMap; use serde_json::Value; use std::io::Write; use temper_codec::net_types::prefixed_optional::PrefixedOptional; + #[test] + fn includes_tag_dependent_synced_registries() { + let registry_ids = REGISTRY_PACKETS + .iter() + .map(|packet| packet.registry_id.as_str()) + .collect::>(); + + assert!(registry_ids.contains(&"minecraft:enchantment")); + assert!(registry_ids.contains(&"minecraft:instrument")); + assert!(registry_ids.contains(&"minecraft:dialog")); + } + #[test] #[ignore] fn generate_nbt() { diff --git a/src/net/protocol/src/outgoing/update_tags.rs b/src/net/protocol/src/outgoing/update_tags.rs new file mode 100644 index 00000000..9997b3fa --- /dev/null +++ b/src/net/protocol/src/outgoing/update_tags.rs @@ -0,0 +1,113 @@ +use lazy_static::lazy_static; +use temper_codec::net_types::{length_prefixed_vec::LengthPrefixedVec, var_int::VarInt}; +use temper_macros::{NetEncode, build_update_tags, packet}; + +#[derive(NetEncode)] +#[packet(packet_id = "update_tags", state = "configuration")] +pub struct UpdateTagsPacket { + pub registries: LengthPrefixedVec, +} + +impl Default for UpdateTagsPacket { + fn default() -> Self { + Self::new() + } +} + +impl UpdateTagsPacket { + pub fn new() -> Self { + Self { + registries: LengthPrefixedVec::new(process_tag_packets()), + } + } +} + +lazy_static! { + pub static ref UPDATE_TAGS_PACKET: UpdateTagsPacket = UpdateTagsPacket::new(); +} + +fn process_tag_packets() -> Vec { + let raw_packets = build_update_tags!(); + let decoded: Vec<(String, Vec<(String, Vec)>)> = + bitcode::decode(&raw_packets).expect("Generated update tags payload should decode"); + + decoded + .into_iter() + .map(|(registry_id, tags)| TagRegistry { + registry_id, + tags: LengthPrefixedVec::new( + tags.into_iter() + .map(|(name, entries)| Tag { + name, + entries: LengthPrefixedVec::new( + entries.into_iter().map(VarInt::new).collect(), + ), + }) + .collect(), + ), + }) + .collect() +} + +#[derive(Clone, Debug, NetEncode)] +pub struct TagRegistry { + pub registry_id: String, + pub tags: LengthPrefixedVec, +} + +#[derive(Clone, Debug, NetEncode)] +pub struct Tag { + pub name: String, + pub entries: LengthPrefixedVec, +} + +#[cfg(test)] +mod tests { + use crate::outgoing::update_tags::UPDATE_TAGS_PACKET; + + #[test] + fn includes_fluid_water_tags() { + let fluids = UPDATE_TAGS_PACKET + .registries + .data + .iter() + .find(|registry| registry.registry_id == "minecraft:fluid") + .expect("fluid tags should be present"); + + let water = fluids + .tags + .data + .iter() + .find(|tag| tag.name == "minecraft:water") + .expect("minecraft:water fluid tag should be present"); + + let entries = water + .entries + .data + .iter() + .map(|entry| entry.0) + .collect::>(); + + assert_eq!(entries, vec![2, 1]); + } + + #[test] + fn includes_enchantment_tags() { + let enchantments = UPDATE_TAGS_PACKET + .registries + .data + .iter() + .find(|registry| registry.registry_id == "minecraft:enchantment") + .expect("enchantment tags should be present"); + + let tradeable = enchantments + .tags + .data + .iter() + .find(|tag| tag.name == "minecraft:tradeable") + .expect("minecraft:tradeable enchantment tag should be present"); + + assert!(!tradeable.entries.data.is_empty()); + assert_eq!(tradeable.entries.data[0].0, 27); + } +} diff --git a/src/net/runtime/src/conn_init/login.rs b/src/net/runtime/src/conn_init/login.rs index f5f8d2c3..41a6ffc4 100644 --- a/src/net/runtime/src/conn_init/login.rs +++ b/src/net/runtime/src/conn_init/login.rs @@ -15,6 +15,7 @@ use temper_protocol::incoming::packet_skeleton::PacketSkeleton; use temper_protocol::outgoing::login_success::{LoginSuccessPacket, LoginSuccessProperties}; use temper_protocol::outgoing::registry_data::REGISTRY_PACKETS; use temper_protocol::outgoing::set_default_spawn_position::DEFAULT_SPAWN_POSITION; +use temper_protocol::outgoing::update_tags::UPDATE_TAGS_PACKET; use temper_state::GlobalState; use temper_components::entity_identity::Identity; @@ -325,6 +326,8 @@ async fn finish_configuration( conn_write.send_packet_ref(packet)?; } + conn_write.send_packet_ref(&*UPDATE_TAGS_PACKET)?; + // Send brand conn_write.send_packet(ClientBoundPluginMessagePacket::brand())?;