From 215bd664ca9860678701cf621b4e350de1187aff Mon Sep 17 00:00:00 2001 From: Dave Corley Date: Wed, 23 Oct 2024 03:56:47 -0700 Subject: [PATCH] FEAT: Add support for leveled item and creature lists --- resources/Morrowind.fgd | 84 +++++++++++++++++++++++++++++++++++++++++ src/game_object.rs | 79 +++++++++++++++++++++++++++++++++++++- src/main.rs | 64 ++++++++++++++++++++++++------- 3 files changed, 212 insertions(+), 15 deletions(-) diff --git a/resources/Morrowind.fgd b/resources/Morrowind.fgd index 286d654..d641faa 100644 --- a/resources/Morrowind.fgd +++ b/resources/Morrowind.fgd @@ -2193,3 +2193,87 @@ Item8_Id(string) : "Ref Id of contained object" Item8_Count(integer): "Number of contained instances" : 1 ] + + +@PointClass = world_CreatureList : "Leveled Creature List" [ + + RefId(string) : "Ref Id for the object" : "agronian guy" + Chance_None(integer) : "Chance nothing will spawn" : 0 + Spawn_From_All_Levels(choices) : "Use alpha blending for this brush" : 0 = + [ + 0 : "False" + 1 : "True" + ] + + Creature_1_Id(string) : "Ref Id of contained creature" + Creature_1_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_2_Id(string) : "Ref Id of contained creature" + Creature_2_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_3_Id(string) : "Ref Id of contained creature" + Creature_3_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_4_Id(string) : "Ref Id of contained creature" + Creature_4_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_5_Id(string) : "Ref Id of contained creature" + Creature_5_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_6_Id(string) : "Ref Id of contained creature" + Creature_6_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_7_Id(string) : "Ref Id of contained creature" + Creature_7_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_8_Id(string) : "Ref Id of contained creature" + Creature_8_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_9_Id(string) : "Ref Id of contained creature" + Creature_9_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Creature_10_Id(string) : "Ref Id of contained creature" + Creature_10_PlayerLevel(integer): "Player level required to spawn this instance" : 1 +] + + +@PointClass = world_ItemList : "Leveled Item List" [ + + RefId(string) : "Ref Id for the object" : "agronian guy" + Chance_None(integer) : "Chance nothing will spawn" : 0 + Spawn_From_All_Levels(choices) : "Use alpha blending for this brush" : 0 = + [ + 0 : "False" + 1 : "True" + ] + + Item_1_Id(string) : "Ref Id of contained item" + Item_1_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_2_Id(string) : "Ref Id of contained item" + Item_2_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_3_Id(string) : "Ref Id of contained item" + Item_3_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_4_Id(string) : "Ref Id of contained item" + Item_4_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_5_Id(string) : "Ref Id of contained item" + Item_5_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_6_Id(string) : "Ref Id of contained item" + Item_6_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_7_Id(string) : "Ref Id of contained item" + Item_7_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_8_Id(string) : "Ref Id of contained item" + Item_8_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_9_Id(string) : "Ref Id of contained item" + Item_9_PlayerLevel(integer): "Player level required to spawn this instance" : 1 + + Item_10_Id(string) : "Ref Id of contained item" + Item_10_PlayerLevel(integer): "Player level required to spawn this instance" : 1 +] diff --git a/src/game_object.rs b/src/game_object.rs index d3e71ae..afd2f75 100644 --- a/src/game_object.rs +++ b/src/game_object.rs @@ -4,7 +4,8 @@ use tes3::esp::{ Activator, Alchemy, AlchemyData, AlchemyFlags, Apparatus, ApparatusData, Armor, ArmorData, AtmosphereData, AttributeId, AttributeId2, BipedObject, Book, BookData, BookType, Cell, CellFlags, Container, ContainerFlags, Effect, EffectId, EffectId2, EffectRange, Ingredient, - IngredientData, Light, LightData, LightFlags, ObjectFlags, SkillId, SkillId2, TES3Object, + IngredientData, LeveledCreature, LeveledCreatureFlags, LeveledItem, LeveledItemFlags, Light, + LightData, LightFlags, ObjectFlags, SkillId, SkillId2, TES3Object, }; pub fn activator( @@ -200,6 +201,42 @@ pub fn container( }) } +pub fn creature_list(entity_props: &HashMap<&String, &String>, ref_id: &str) -> TES3Object { + TES3Object::LeveledCreature(LeveledCreature { + flags: ObjectFlags::default(), + id: ref_id.to_owned(), + chance_none: entity_props + .get(&"Chance_None".to_string()) + .map_or("0", |v| v) + .parse::() + .unwrap_or_default(), + leveled_creature_flags: match entity_props.get(&"Spawn_From_All_Levels".to_string()) { + Some(value) if *value == "1" => LeveledCreatureFlags::CALCULATE_FROM_ALL_LEVELS, + Some(_) => LeveledCreatureFlags::empty(), + None => LeveledCreatureFlags::empty(), + }, + creatures: collect_list_creatures(&entity_props), + }) +} + +pub fn item_list(entity_props: &HashMap<&String, &String>, ref_id: &str) -> TES3Object { + TES3Object::LeveledItem(LeveledItem { + flags: ObjectFlags::default(), + id: ref_id.to_owned(), + chance_none: entity_props + .get(&"Chance_None".to_string()) + .map_or("0", |v| v) + .parse::() + .unwrap_or_default(), + leveled_item_flags: match entity_props.get(&"Spawn_From_All_Levels".to_string()) { + Some(value) if *value == "1" => LeveledItemFlags::CALCULATE_FROM_ALL_LEVELS, + Some(_) => LeveledItemFlags::empty(), + None => LeveledItemFlags::empty(), + }, + items: collect_list_items(&entity_props), + }) +} + pub fn ingredient( entity_props: &HashMap<&String, &String>, ref_id: &str, @@ -490,6 +527,46 @@ fn collect_contained_objects( contained_objects } +fn collect_list_creatures(prop_map: &HashMap<&String, &String>) -> Vec<(String, u16)> { + let mut contained_objects = Vec::new(); + + for count in 1..25 { + match prop_map.get(&format!("Creature_{count}_Id")) { + Some(contained_id) => { + let level_required: u16 = prop_map + .get(&format!("Creature_{count}_PlayerLevel")) + .map_or("1", |v| v) + .parse() + .expect("Cannot fail parsing a default-initialized integer"); + contained_objects.push((contained_id.to_string(), level_required)) + } + None => continue, + } + } + + contained_objects +} + +fn collect_list_items(prop_map: &HashMap<&String, &String>) -> Vec<(String, u16)> { + let mut contained_objects = Vec::new(); + + for count in 1..25 { + match prop_map.get(&format!("Item_{count}_Id")) { + Some(contained_id) => { + let level_required: u16 = prop_map + .get(&format!("Item_{count}_PlayerLevel")) + .map_or("1", |v| v) + .parse() + .expect("Cannot fail parsing a default-initialized integer"); + contained_objects.push((contained_id.to_string(), level_required)) + } + None => continue, + } + } + + contained_objects +} + fn get_color(color_str: &String) -> [u8; 4] { let mut array = [0; 4]; let colors: Vec<&str> = color_str.split_whitespace().collect(); diff --git a/src/main.rs b/src/main.rs index f55206f..20a0aa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -277,19 +277,6 @@ fn main() { let mut ref_id = format!("{map_dir}-PL-{lowest_available_index}"); ref_id = ref_id[..min(ref_id.len(), 32)].to_string(); - let translation: SV3 = { - let coords: Vec = prop_map - .get(&"origin".to_string()) - .expect("All point entities must have an origin") - .split_whitespace() - .map(|s| s.parse::().expect("Invalid coordinate")) - .collect(); - - assert_eq!(coords.len(), 3, "Origin must have exactly 3 coordinates"); - - SV3::new(coords[0], coords[1], coords[2]) * (*scale_mode) - }; - let radius: u32 = light .chars() .skip_while(|c| !c.is_digit(10)) @@ -311,10 +298,46 @@ fn main() { &mut used_indices, &mut cell, ref_id, - translation, + point_entity_position(scale_mode, &prop_map), + [0.0, 0.0, 0.0], + ); + } + "world_CreatureList" => { + let ref_id = match prop_map.get(&"RefId".to_string()) { + Some(ref_id) => ref_id[..min(ref_id.len(), 32)].to_string(), + None => panic!( + "RefIds are mandatory for all point entities, failed on creature list, entity ID: {}", + entity_id + ), + }; + + if !processed_base_objects.contains(&ref_id) { + created_objects.push(game_object::creature_list(&prop_map, ref_id.as_str())); + processed_base_objects.insert(ref_id.to_string()); + } + + append_cell_reference( + &mut used_indices, + &mut cell, + ref_id, + point_entity_position(scale_mode, &prop_map), [0.0, 0.0, 0.0], ); } + "world_ItemList" => { + let ref_id = match prop_map.get(&"RefId".to_string()) { + Some(ref_id) => ref_id[..min(ref_id.len(), 32)].to_string(), + None => panic!( + "RefIds are mandatory for all point entities, failed on item list, entity ID: {}", + entity_id + ), + }; + + if !processed_base_objects.contains(&ref_id) { + created_objects.push(game_object::item_list(&prop_map, ref_id.as_str())); + processed_base_objects.insert(ref_id.to_string()); + } + } class => { println!("Unidentified point entity class: {class}") } @@ -342,6 +365,19 @@ fn main() { println!("Wrote {plugin_name} to disk successfully."); } +fn point_entity_position(scale_mode: &f32, prop_map: &HashMap<&String, &String>) -> SV3 { + let coords: Vec = prop_map + .get(&"origin".to_string()) + .expect("All point entities must have an origin") + .split_whitespace() + .map(|s| s.parse::().expect("Invalid coordinate")) + .collect(); + + assert_eq!(coords.len(), 3, "Origin must have exactly 3 coordinates"); + + SV3::new(coords[0], coords[1], coords[2]) * (*scale_mode) +} + fn lowest_available_index(used_indices: &HashSet) -> u32 { (1..).find(|&n| !used_indices.contains(&n)).unwrap_or(1) }