From bfe34183a548467ffc8a7ca036002b9b1c30b0b4 Mon Sep 17 00:00:00 2001
From: Kalle Struik <kalle@kallestruik.nl>
Date: Sat, 8 Mar 2025 15:16:41 +0100
Subject: [PATCH] Load tags

---
 README.md                                     | 16 +--
 potato-data/src/datapack.rs                   | 97 ++++++++++++++++---
 potato-data/src/identifier.rs                 |  8 ++
 potato-data/src/lib.rs                        |  9 ++
 potato-data/src/registry/mod.rs               | 91 ++++++++++++++++-
 potato-data/src/registry/trim_material.rs     |  5 +-
 potato-data/src/registry/trim_pattern.rs      |  2 +-
 potato-data/src/registry/worldgen/biome.rs    |  4 +-
 potato-data/src/tag.rs                        | 20 +++-
 .../src/packet/clientbound/registry_data.rs   | 75 +++++++++++++-
 potato/src/connection.rs                      |  2 +-
 potato/src/server.rs                          | 30 +++---
 12 files changed, 309 insertions(+), 50 deletions(-)

diff --git a/README.md b/README.md
index 5221651..9ccbb2b 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,13 @@ N/A = Not applicable
 | Registry                                         | Loads | Loads tags | Synced to client | Tags synced to client |
 |--------------------------------------------------|-------|------------|------------------|-----------------------|
 | advancement                                      | ❌    | ❌         | ❌               | ❌                    |
-| banner_pattern                                   | ✅    | ❌         | ❌               | ❌                    |
+| banner_pattern                                   | ✅    | ✅         | ❌               | ❌                    |
 <!-- |cat_variant                                       | ❌    | ❌         | ❌               | ❌                    | -->
 | chat_type                                        | ✅    | ❌         | ❌               | ❌                    |
 <!-- |cow_variant                                       | ❌    | ❌         | ❌               | ❌                    | -->
-| damage_type                                      | ✅    | ❌         | ❌               | ❌                    |
+| damage_type                                      | ✅    | ✅        | ❌               | ❌                    |
 | dimension                                        | ❌    | ❌         | ❌               | ❌                    |
-| dimension_type                                   | ✅    | ❌         | ❌               | ❌                    |
+| dimension_type                                   | ✅    | ✅         | ❌               | ❌                    |
 | enchantment                                      | ❌    | ❌         | ❌               | ❌                    |
 | enchantment_provider                             | ❌    | ❌         | ❌               | ❌                    |
 <!-- |frog_variant                                      | ❌    | ❌         | ❌               | ❌                    | -->
@@ -26,17 +26,17 @@ N/A = Not applicable
 | item_modifier                                    | ❌    | ❌         | ❌               | ❌                    |
 | jukebox_song                                     | ❌    | ❌         | ❌               | ❌                    |
 | loot_table                                       | ❌    | ❌         | ❌               | ❌                    |
-| painting_variant                                 | ✅    | ❌         | ❌               | ❌                    |
+| painting_variant                                 | ✅    | ✅         | ❌               | ❌                    |
 <!-- |pig_variant                                       | ❌    | ❌         | ❌               | ❌                    | -->
 | predicate                                        | ❌    | ❌         | ❌               | ❌                    |
 | recipe                                           | ❌    | ❌         | ❌               | ❌                    |
 <!-- |test_environment                                  | ❌    | ❌         | ❌               | ❌                    | -->
 <!-- |test_instance                                     | ❌    | ❌         | ❌               | ❌                    | -->
 | trial_spawner                                    | ❌    | ❌         | ❌               | ❌                    |
-| trim_material                                    | ✅    | ❌         | ❌               | ❌                    |
-| trim_pattern                                     | ✅    | ❌         | ❌               | ❌                    |
-| wolf_variant                                     | ✅    | ❌         | ❌               | ❌                    |
-| worldgen/biome                                   | ✅    | ❌         | ❌               | ❌                    |
+| trim_material                                    | ✅    | ✅         | ❌               | ❌                    |
+| trim_pattern                                     | ✅    | ✅         | ❌               | ❌                    |
+| wolf_variant                                     | ✅    | ✅         | ❌               | ❌                    |
+| worldgen/biome                                   | ✅    | ✅         | ❌               | ❌                    |
 | worldgen/configured_carver                       | ❌    | ❌         | ❌               | ❌                    |
 | worldgen/configured_feature                      | ❌    | ❌         | ❌               | ❌                    |
 | worldgen/density_function                        | ❌    | ❌         | ❌               | ❌                    |
diff --git a/potato-data/src/datapack.rs b/potato-data/src/datapack.rs
index d19f85e..83ffc58 100644
--- a/potato-data/src/datapack.rs
+++ b/potato-data/src/datapack.rs
@@ -1,10 +1,13 @@
 use std::{ffi::OsStr, fs::read_to_string, path::PathBuf};
 
+use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
 use crate::{
+    TagOrIdentifier,
     identifier::Identifier,
-    registry::{Registries, Registry},
+    registry::{Registries, Registry, RegistryError},
+    tag::Tag,
 };
 
 pub struct Datapack {
@@ -35,7 +38,7 @@ impl Datapack {
         Ok(())
     }
 
-    fn load_registry<T: serde::de::DeserializeOwned>(
+    fn load_registry<T: serde::de::DeserializeOwned + Clone>(
         &self,
         registry: &mut Registry<T>,
         path: &str,
@@ -55,26 +58,85 @@ impl Datapack {
                 .and_then(OsStr::to_str)
                 .ok_or(DatapackError::Utf8)?;
             let path = namespace.join(path);
-            if let Ok(files) = std::fs::read_dir(path) {
-                for file in files {
-                    let file = file?.path();
-                    let name = file
-                        .file_stem()
-                        .and_then(OsStr::to_str)
-                        .ok_or(DatapackError::Utf8)?;
-                    let identfiier = Identifier::new_str(namespace_str, name);
+            let files = std::fs::read_dir(path)?;
+            for file in files {
+                println!("Loading file: {:?}", file);
+                let file = file?.path();
+                let name = file
+                    .file_stem()
+                    .and_then(OsStr::to_str)
+                    .ok_or(DatapackError::Utf8)?;
+                let identfiier = Identifier::new_str(namespace_str, name);
 
-                    let data: T = serde_json::from_str(&read_to_string(file)?)?;
+                let data: T = serde_json::from_str(&read_to_string(file)?)?;
 
-                    registry.insert(identfiier, data);
-                }
+                registry.register(identfiier, data)?;
             }
         }
 
+        self.load_registry_tags(registry, path)?;
+
+        Ok(())
+    }
+
+    fn load_registry_tags<T: Clone>(
+        &self,
+        registry: &mut Registry<T>,
+        path: &str,
+    ) -> Result<(), DatapackError> {
+        let namespaces: Vec<_> = self
+            .path
+            .join("data")
+            .read_dir()
+            .unwrap()
+            .map(|f| f.unwrap().path())
+            .filter(|p| p.is_dir())
+            .collect();
+
+        for namespace in namespaces {
+            let namespace_str = namespace
+                .file_name()
+                .and_then(OsStr::to_str)
+                .ok_or(DatapackError::Utf8)?;
+            let path = namespace.join("tags").join(path);
+            load_registry_tags_in_directory(registry, namespace_str, path)?;
+        }
+
         Ok(())
     }
 }
 
+fn load_registry_tags_in_directory<T: Clone>(
+    registry: &mut Registry<T>,
+    namespace: &str,
+    dir: PathBuf,
+) -> Result<(), DatapackError> {
+    if let Ok(files) = std::fs::read_dir(dir) {
+        for file in files {
+            let file = file?.path();
+            if file.is_dir() {
+                load_registry_tags_in_directory(registry, namespace, file)?;
+            } else {
+                let name = file
+                    .file_stem()
+                    .and_then(OsStr::to_str)
+                    .ok_or(DatapackError::Utf8)?;
+                let tag = Tag::new_str(namespace, name);
+
+                let data: RegistryTagData = serde_json::from_str(&read_to_string(file)?)?;
+
+                if data.replace {
+                    registry.replace_tag(tag, data.values);
+                } else {
+                    registry.register_tag(tag, data.values);
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
 #[derive(Debug, Error)]
 pub enum DatapackError {
     #[error("IO error while reading datapack: {0}")]
@@ -83,4 +145,13 @@ pub enum DatapackError {
     Json(#[from] serde_json::Error),
     #[error("Invalid UTF-8 in datapack")]
     Utf8,
+    #[error("Error while registering item in datapack: {0}")]
+    Registry(#[from] RegistryError),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct RegistryTagData {
+    #[serde(default)]
+    replace: bool,
+    values: Vec<TagOrIdentifier>,
 }
diff --git a/potato-data/src/identifier.rs b/potato-data/src/identifier.rs
index 30db5b8..1542c41 100644
--- a/potato-data/src/identifier.rs
+++ b/potato-data/src/identifier.rs
@@ -1,3 +1,5 @@
+use std::fmt;
+
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
@@ -42,6 +44,12 @@ impl Identifier {
     }
 }
 
+impl fmt::Display for Identifier {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}:{}", self.namespace, self.path)
+    }
+}
+
 impl From<Identifier> for String {
     fn from(value: Identifier) -> Self {
         (&value).into()
diff --git a/potato-data/src/lib.rs b/potato-data/src/lib.rs
index 11df7f5..efc66bb 100644
--- a/potato-data/src/lib.rs
+++ b/potato-data/src/lib.rs
@@ -1,4 +1,6 @@
+use identifier::Identifier;
 use serde::{Deserialize, Serialize};
+use tag::Tag;
 
 pub mod block_state;
 pub mod color;
@@ -23,3 +25,10 @@ pub enum OneOrMany<T> {
     One(T),
     Many(Vec<T>),
 }
+
+#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum TagOrIdentifier {
+    Tag(Tag),
+    Identifier(Identifier),
+}
diff --git a/potato-data/src/registry/mod.rs b/potato-data/src/registry/mod.rs
index a04225a..04333a3 100644
--- a/potato-data/src/registry/mod.rs
+++ b/potato-data/src/registry/mod.rs
@@ -5,12 +5,13 @@ use chat_type::ChatType;
 use damage_type::DamageType;
 use dimension_type::DimensionType;
 use painting_variant::PaintingVariant;
+use thiserror::Error;
 use trim_material::TrimMaterial;
 use trim_pattern::TrimPattern;
 use wolf_variant::WolfVariant;
 use worldgen::biome::Biome;
 
-use crate::identifier::Identifier;
+use crate::{TagOrIdentifier, identifier::Identifier, tag::Tag};
 
 pub mod banner_pattern;
 pub mod chat_type;
@@ -22,8 +23,6 @@ pub mod trim_pattern;
 pub mod wolf_variant;
 pub mod worldgen;
 
-pub type Registry<T> = HashMap<Identifier, T>;
-
 #[derive(Default)]
 pub struct Registries {
     // advancement
@@ -71,3 +70,89 @@ pub struct WorldgenRegistries {
     // flat_level_generator_preset
     // multi_noise_biome_source_parameter_list
 }
+
+#[derive(Debug, Error)]
+pub enum RegistryError {
+    #[error("Duplicate key: {0}")]
+    DuplicateKey(Identifier),
+}
+
+pub struct Registry<T>
+where
+    T: Clone,
+{
+    // Int id -> T
+    by_id: Vec<T>,
+    // Int id -> Identifier
+    id_to_key: Vec<Identifier>,
+    // Identifier -> Int Id
+    to_id: HashMap<Identifier, usize>,
+    // Identifier -> T
+    by_key: HashMap<Identifier, T>,
+
+    tags: HashMap<Tag, Vec<TagOrIdentifier>>,
+}
+
+impl<T> Default for Registry<T>
+where
+    T: Clone,
+{
+    fn default() -> Self {
+        Self {
+            by_id: Default::default(),
+            id_to_key: Default::default(),
+            to_id: Default::default(),
+            by_key: Default::default(),
+            tags: Default::default(),
+        }
+    }
+}
+
+impl<T> Registry<T>
+where
+    T: Clone,
+{
+    pub fn register(&mut self, key: Identifier, value: T) -> Result<(), RegistryError> {
+        if self.to_id.contains_key(&key) {
+            return Err(RegistryError::DuplicateKey(key));
+        }
+        let id = self.by_id.len();
+        self.by_id.push(value.clone());
+        self.id_to_key.push(key.clone());
+        self.to_id.insert(key.clone(), id);
+        self.by_key.insert(key, value);
+
+        Ok(())
+    }
+
+    pub fn register_tag(&mut self, tag: Tag, entries: Vec<TagOrIdentifier>) {
+        match self.tags.get_mut(&tag) {
+            Some(values) => {
+                values.extend(entries);
+            }
+            None => {
+                self.tags.insert(tag, entries);
+            }
+        }
+    }
+
+    pub fn replace_tag(&mut self, tag: Tag, entries: Vec<TagOrIdentifier>) {
+        self.tags.insert(tag, entries);
+    }
+
+    pub fn len(&self) -> usize {
+        self.by_id.len()
+    }
+
+    pub fn tags_len(&self) -> usize {
+        self.tags.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.by_id.is_empty()
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &T)> {
+        self.id_to_key.iter().zip(self.by_id.iter())
+    }
+}
diff --git a/potato-data/src/registry/trim_material.rs b/potato-data/src/registry/trim_material.rs
index 633d7e1..a401277 100644
--- a/potato-data/src/registry/trim_material.rs
+++ b/potato-data/src/registry/trim_material.rs
@@ -7,8 +7,9 @@ use crate::{identifier::Identifier, text_component::TextComponent};
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct TrimMaterial {
     pub asset_name: String,
-    pub description: TextComponent,
     pub ingredient: Identifier,
+    #[serde(default)]
+    pub item_model_index: f32,
     pub override_armor_assets: Option<HashMap<Identifier, String>>,
-    // NOTE: Wiki says item_model_index exists as option, but can't find it in the vanilla pack
+    pub description: TextComponent,
 }
diff --git a/potato-data/src/registry/trim_pattern.rs b/potato-data/src/registry/trim_pattern.rs
index d2ab20c..3b454fe 100644
--- a/potato-data/src/registry/trim_pattern.rs
+++ b/potato-data/src/registry/trim_pattern.rs
@@ -5,8 +5,8 @@ use crate::{identifier::Identifier, text_component::TextComponent};
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct TrimPattern {
     pub asset_id: Identifier,
-    pub description: TextComponent,
     pub template_item: Identifier,
+    pub description: TextComponent,
     #[serde(default)]
     pub decal: bool,
 }
diff --git a/potato-data/src/registry/worldgen/biome.rs b/potato-data/src/registry/worldgen/biome.rs
index 6cd5924..29f3eb8 100644
--- a/potato-data/src/registry/worldgen/biome.rs
+++ b/potato-data/src/registry/worldgen/biome.rs
@@ -20,7 +20,7 @@ pub struct Biome {
     pub spawn_costs: HashMap<Identifier, BiomeSpawnCost>,
 }
 
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum BiomeTemperatureModifier {
     #[default]
@@ -51,7 +51,7 @@ pub struct SoundEvent {
     pub range: Option<f32>,
 }
 
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum BiomeGrassColorModifier {
     #[default]
diff --git a/potato-data/src/tag.rs b/potato-data/src/tag.rs
index c575804..eada051 100644
--- a/potato-data/src/tag.rs
+++ b/potato-data/src/tag.rs
@@ -4,10 +4,28 @@ use thiserror::Error;
 use crate::identifier::{Identifier, IdentifierError};
 
 // TODO: Needs to serialize/deserialize as the identifier with a hash in front.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
 #[serde(try_from = "&str", into = "String")]
 pub struct Tag(Identifier);
 
+impl Tag {
+    pub fn new(namespace: String, path: String) -> Self {
+        Self(Identifier::new(namespace, path))
+    }
+
+    pub fn minecraft(path: String) -> Self {
+        Self::new("minecraft".to_string(), path)
+    }
+
+    pub fn new_str(namespace: &str, path: &str) -> Self {
+        Self::new(namespace.to_string(), path.to_string())
+    }
+
+    pub fn minecraft_str(path: &str) -> Self {
+        Self::minecraft(path.to_string())
+    }
+}
+
 impl TryFrom<&str> for Tag {
     type Error = TagError;
     fn try_from(value: &str) -> Result<Self, Self::Error> {
diff --git a/potato-protocol/src/packet/clientbound/registry_data.rs b/potato-protocol/src/packet/clientbound/registry_data.rs
index a300f92..af71476 100644
--- a/potato-protocol/src/packet/clientbound/registry_data.rs
+++ b/potato-protocol/src/packet/clientbound/registry_data.rs
@@ -1,10 +1,20 @@
 use potato_data::{
+    Either,
     identifier::Identifier,
     registry::{
-        banner_pattern::BannerPattern, chat_type::ChatType, damage_type::DamageType,
-        dimension_type::DimensionType, painting_variant::PaintingVariant,
-        trim_material::TrimMaterial, trim_pattern::TrimPattern, wolf_variant::WolfVariant,
-        worldgen::biome::Biome,
+        self,
+        banner_pattern::BannerPattern,
+        chat_type::ChatType,
+        damage_type::DamageType,
+        dimension_type::DimensionType,
+        painting_variant::PaintingVariant,
+        trim_material::TrimMaterial,
+        trim_pattern::TrimPattern,
+        wolf_variant::WolfVariant,
+        worldgen::biome::{
+            BiomeAdditionsSound, BiomeGrassColorModifier, BiomeMoodSound, BiomeMusicEntry,
+            BiomeParticle, BiomeTemperatureModifier, SoundEvent,
+        },
     },
 };
 use potato_protocol_derive::{Packet, PacketEncodable};
@@ -55,10 +65,65 @@ impl_from_registry!(
     TrimMaterial,
     TrimPattern,
     BannerPattern,
-    Biome,
     ChatType,
     DamageType,
     DimensionType,
     WolfVariant,
     PaintingVariant
 );
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Biome {
+    has_precipitation: bool,
+    temperature: f32,
+    temperature_modifier: BiomeTemperatureModifier,
+    downfall: f32,
+    effects: BiomeEffects,
+}
+
+impl From<&registry::worldgen::biome::Biome> for RegistryData {
+    fn from(value: &registry::worldgen::biome::Biome) -> Self {
+        RegistryData::Biome(Box::new(Biome {
+            has_precipitation: value.has_precipitation,
+            temperature: value.temperature,
+            temperature_modifier: value.temperature_modifier,
+            downfall: value.downfall,
+            effects: (&value.effects).into(),
+        }))
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct BiomeEffects {
+    fog_color: i32,
+    water_color: i32,
+    water_fog_color: i32,
+    sky_color: i32,
+    foliage_color: Option<i32>,
+    grass_color: Option<i32>,
+    grass_color_modifier: BiomeGrassColorModifier,
+    particle: Option<BiomeParticle>,
+    ambient_sound: Option<Either<Identifier, SoundEvent>>,
+    mood_sound: Option<BiomeMoodSound>,
+    additions_sound: Option<BiomeAdditionsSound>,
+    music: Option<Vec<BiomeMusicEntry>>,
+}
+
+impl From<&registry::worldgen::biome::BiomeEffects> for BiomeEffects {
+    fn from(value: &registry::worldgen::biome::BiomeEffects) -> Self {
+        BiomeEffects {
+            fog_color: value.fog_color,
+            water_color: value.water_color,
+            water_fog_color: value.water_fog_color,
+            sky_color: value.sky_color,
+            foliage_color: value.foliage_color,
+            grass_color: value.grass_color,
+            grass_color_modifier: value.grass_color_modifier,
+            particle: value.particle.clone(),
+            ambient_sound: value.ambient_sound.clone(),
+            mood_sound: value.mood_sound.clone(),
+            additions_sound: value.additions_sound.clone(),
+            music: value.music.clone(),
+        }
+    }
+}
diff --git a/potato/src/connection.rs b/potato/src/connection.rs
index f248e64..98d4224 100644
--- a/potato/src/connection.rs
+++ b/potato/src/connection.rs
@@ -437,7 +437,7 @@ impl Client {
         Ok(())
     }
 
-    async fn send_registry<T>(
+    async fn send_registry<T: Clone>(
         &self,
         identifier: Identifier,
         registry: &Registry<T>,
diff --git a/potato/src/server.rs b/potato/src/server.rs
index a104740..37bed33 100644
--- a/potato/src/server.rs
+++ b/potato/src/server.rs
@@ -6,6 +6,13 @@ pub struct Server {
     pub registries: Arc<Registries>,
 }
 
+macro_rules! print_registry_loaded {
+    ($registry:expr, $name:literal) => {
+        println!("Loaded {} {}s", $registry.len(), $name);
+        println!("Loaded {} {} tags", $registry.tags_len(), $name);
+    };
+}
+
 impl Server {
     #[allow(clippy::new_without_default)]
     pub fn new() -> Server {
@@ -24,20 +31,15 @@ impl Server {
             .for_each(|pack| pack.load(&mut registries).expect("Failed to load datapack"));
 
         println!("Finished loading datapacks");
-        println!("Loaded {} trim materials", registries.trim_materials.len());
-        println!("Loaded {} trim patterns", registries.trim_patterns.len());
-        println!("Loaded {} chat types", registries.chat_types.len());
-        println!(
-            "Loaded {} dimension types",
-            registries.dimension_types.len()
-        );
-        println!(
-            "Loaded {} painting variants",
-            registries.painting_variants.len()
-        );
-        println!("Loaded {} wolf variants", registries.wolf_variants.len());
-        println!("Loaded {} damage types", registries.damage_types.len());
-        println!("Loaded {} biomes", registries.worldgen.biomes.len());
+        print_registry_loaded!(registries.trim_materials, "trim material");
+        print_registry_loaded!(registries.trim_patterns, "trim pattern");
+        print_registry_loaded!(registries.banner_patterns, "banner pattern");
+        print_registry_loaded!(registries.chat_types, "chat type");
+        print_registry_loaded!(registries.dimension_types, "dimension type");
+        print_registry_loaded!(registries.painting_variants, "painting variant");
+        print_registry_loaded!(registries.wolf_variants, "wolf variant");
+        print_registry_loaded!(registries.damage_types, "damage_type");
+        print_registry_loaded!(registries.worldgen.biomes, "biome");
 
         Server {
             registries: Arc::new(registries),