614 lines
26 KiB
Rust
614 lines
26 KiB
Rust
use crate::image_processor::ImageProcessor;
|
|
use crate::item_loader::{
|
|
calculate_prices, generate_banknotes, generate_exceptional_items, load_items_from_directory,
|
|
};
|
|
use crate::types::Item;
|
|
use crate::xml_parsers::{parse_items_xml, XmlParseError};
|
|
use diesel::prelude::*;
|
|
use diesel::sqlite::SqliteConnection;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// A database for managing game items loaded from XML files
|
|
#[derive(Debug, Clone)]
|
|
pub struct ItemDatabase {
|
|
items: Vec<Item>,
|
|
items_by_id: HashMap<i32, usize>,
|
|
items_by_name: HashMap<String, Vec<usize>>,
|
|
stackable_item_ids: HashSet<i32>,
|
|
storage_item_ids: HashSet<i32>,
|
|
}
|
|
|
|
impl ItemDatabase {
|
|
/// Create a new empty ItemDatabase
|
|
pub fn new() -> Self {
|
|
Self {
|
|
items: Vec::new(),
|
|
items_by_id: HashMap::new(),
|
|
items_by_name: HashMap::new(),
|
|
stackable_item_ids: HashSet::new(),
|
|
storage_item_ids: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
/// Load items from an XML file (basic loading without advanced features)
|
|
pub fn load_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
|
|
let items = parse_items_xml(path)?;
|
|
let mut db = Self::new();
|
|
db.add_items(items);
|
|
Ok(db)
|
|
}
|
|
|
|
/// Load items from a directory with full support for:
|
|
/// - Multiple XML files
|
|
/// - Stats generation
|
|
/// - Crafting recipe generation
|
|
/// - Exceptional items
|
|
/// - Banknotes
|
|
/// - Price calculation
|
|
pub fn load_from_directory<P: AsRef<Path>>(dir: P) -> Result<Self, XmlParseError> {
|
|
let mut items = load_items_from_directory(dir)?;
|
|
|
|
// Generate exceptional items
|
|
let exceptional = generate_exceptional_items(&items);
|
|
items.extend(exceptional);
|
|
|
|
// Generate banknotes
|
|
let banknotes = generate_banknotes(&items);
|
|
items.extend(banknotes);
|
|
|
|
// Calculate prices
|
|
calculate_prices(&mut items);
|
|
|
|
let mut db = Self::new();
|
|
db.add_items(items);
|
|
Ok(db)
|
|
}
|
|
|
|
/// Add items to the database
|
|
pub fn add_items(&mut self, items: Vec<Item>) {
|
|
for item in items {
|
|
let index = self.items.len();
|
|
self.items_by_id.insert(item.type_id, index);
|
|
|
|
// Add to name index (can have multiple items with same name)
|
|
self.items_by_name
|
|
.entry(item.item_name.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(index);
|
|
|
|
// Track stackable items
|
|
if item.is_stackable() {
|
|
self.stackable_item_ids.insert(item.type_id);
|
|
}
|
|
|
|
// Track storage items
|
|
if item.is_storage_item() {
|
|
self.storage_item_ids.insert(item.type_id);
|
|
}
|
|
|
|
self.items.push(item);
|
|
}
|
|
}
|
|
|
|
/// Check if an item is stackable by ID
|
|
pub fn is_stackable(&self, type_id: i32) -> bool {
|
|
self.stackable_item_ids.contains(&type_id)
|
|
}
|
|
|
|
/// Check if an item is a storage item by ID
|
|
pub fn is_storage_item(&self, type_id: i32) -> bool {
|
|
self.storage_item_ids.contains(&type_id)
|
|
}
|
|
|
|
/// Get an item by ID
|
|
pub fn get_by_id(&self, id: i32) -> Option<&Item> {
|
|
self.items_by_id
|
|
.get(&id)
|
|
.and_then(|&index| self.items.get(index))
|
|
}
|
|
|
|
/// Get items by name (returns all items with matching name)
|
|
pub fn get_by_name(&self, name: &str) -> Vec<&Item> {
|
|
self.items_by_name
|
|
.get(name)
|
|
.map(|indices| {
|
|
indices
|
|
.iter()
|
|
.filter_map(|&index| self.items.get(index))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Get all items
|
|
pub fn all_items(&self) -> &[Item] {
|
|
&self.items
|
|
}
|
|
|
|
/// Get items by category
|
|
pub fn get_by_category(&self, category: &str) -> Vec<&Item> {
|
|
use crate::types::ItemCategory;
|
|
use std::str::FromStr;
|
|
|
|
if let Ok(cat) = ItemCategory::from_str(category) {
|
|
self.items
|
|
.iter()
|
|
.filter(|item| item.has_category(cat))
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get items by slot/item type
|
|
pub fn get_by_slot(&self, slot: &str) -> Vec<&Item> {
|
|
use crate::types::ItemType;
|
|
use std::str::FromStr;
|
|
|
|
if let Ok(item_type) = ItemType::from_str(slot) {
|
|
self.items
|
|
.iter()
|
|
.filter(|item| item.item_type == item_type)
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get items by skill requirement
|
|
pub fn get_by_skill(&self, skill: &str) -> Vec<&Item> {
|
|
use crate::types::SkillType;
|
|
use std::str::FromStr;
|
|
|
|
if let Ok(skill_type) = SkillType::from_str(skill) {
|
|
self.items
|
|
.iter()
|
|
.filter(|item| item.skill == skill_type)
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get items by tool type
|
|
pub fn get_by_tool(&self, tool: &str) -> Vec<&Item> {
|
|
use crate::types::Tool;
|
|
use std::str::FromStr;
|
|
|
|
if let Ok(tool_type) = Tool::from_str(tool) {
|
|
self.items
|
|
.iter()
|
|
.filter(|item| item.tool == tool_type)
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get number of items in database
|
|
pub fn len(&self) -> usize {
|
|
self.items.len()
|
|
}
|
|
|
|
/// Check if database is empty
|
|
pub fn is_empty(&self) -> bool {
|
|
self.items.is_empty()
|
|
}
|
|
|
|
/// Serialize items to JSON for SQL storage
|
|
#[cfg(feature = "diesel")]
|
|
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
|
serde_json::to_string(&self.items)
|
|
}
|
|
|
|
/// Prepare items for SQL insertion (deprecated - use save_to_db instead)
|
|
#[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
|
|
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
|
|
self.items
|
|
.iter()
|
|
.map(|item| {
|
|
let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string());
|
|
(item.type_id, item.item_name.clone(), json)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Save all items to SQLite database
|
|
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
|
|
use crate::schema::{items, crafting_recipes, crafting_recipe_items};
|
|
use diesel::replace_into;
|
|
|
|
conn.transaction::<_, diesel::result::Error, _>(|conn| {
|
|
let mut count = 0;
|
|
|
|
for item in &self.items {
|
|
let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string());
|
|
|
|
// Insert/replace item with all columns
|
|
replace_into(items::table)
|
|
.values((
|
|
items::id.eq(item.type_id),
|
|
items::name.eq(&item.item_name),
|
|
items::data.eq(json),
|
|
items::item_type.eq(item.item_type.to_string()),
|
|
items::level.eq(item.level),
|
|
items::price.eq(item.price),
|
|
items::max_stack.eq(item.max_stack),
|
|
items::storage_size.eq(item.storage_size),
|
|
items::skill.eq(match item.skill {
|
|
crate::types::SkillType::None => "none",
|
|
crate::types::SkillType::Swordsmanship => "swordsmanship",
|
|
crate::types::SkillType::Archery => "archery",
|
|
crate::types::SkillType::Magic => "magic",
|
|
crate::types::SkillType::Defence => "defence",
|
|
crate::types::SkillType::Mining => "mining",
|
|
crate::types::SkillType::Woodcutting => "woodcutting",
|
|
crate::types::SkillType::Fishing => "fishing",
|
|
crate::types::SkillType::Cooking => "cooking",
|
|
crate::types::SkillType::Carpentry => "carpentry",
|
|
crate::types::SkillType::Blacksmithy => "blacksmithy",
|
|
crate::types::SkillType::Tailoring => "tailoring",
|
|
crate::types::SkillType::Alchemy => "alchemy",
|
|
}),
|
|
items::tool.eq(match item.tool {
|
|
crate::types::Tool::None => "none",
|
|
crate::types::Tool::Pickaxe => "pickaxe",
|
|
crate::types::Tool::Hatchet => "hatchet",
|
|
crate::types::Tool::Scythe => "scythe",
|
|
crate::types::Tool::Hammer => "hammer",
|
|
crate::types::Tool::Shears => "shears",
|
|
crate::types::Tool::FishingRod => "fishingrod",
|
|
}),
|
|
items::description.eq(&item.description),
|
|
items::two_handed.eq(item.two_handed as i32),
|
|
items::undroppable.eq(item.undroppable as i32),
|
|
items::undroppable_on_death.eq(item.undroppable_on_death as i32),
|
|
items::unequip_destroy.eq(item.unequip_destroy as i32),
|
|
items::generate_icon.eq(item.generate_icon as i32),
|
|
items::hide_milestone.eq(item.hide_milestone as i32),
|
|
items::cannot_craft_exceptional.eq(item.cannot_craft_exceptional as i32),
|
|
items::storage_all_items.eq(item.storage_all_items as i32),
|
|
items::ability_id.eq(item.ability_id),
|
|
items::special_ability.eq(item.special_ability),
|
|
items::learn_ability_id.eq(item.learn_ability_id),
|
|
items::book_id.eq(item.book_id),
|
|
items::swap_item.eq(item.swap_item),
|
|
))
|
|
.execute(conn)?;
|
|
|
|
// Save crafting recipes for this item
|
|
for recipe in &item.crafting_recipes {
|
|
use diesel::prelude::*;
|
|
|
|
// Insert recipe
|
|
diesel::insert_into(crafting_recipes::table)
|
|
.values((
|
|
crafting_recipes::product_item_id.eq(item.type_id),
|
|
crafting_recipes::skill.eq(match recipe.skill {
|
|
crate::types::SkillType::None => "none",
|
|
crate::types::SkillType::Swordsmanship => "swordsmanship",
|
|
crate::types::SkillType::Archery => "archery",
|
|
crate::types::SkillType::Magic => "magic",
|
|
crate::types::SkillType::Defence => "defence",
|
|
crate::types::SkillType::Mining => "mining",
|
|
crate::types::SkillType::Woodcutting => "woodcutting",
|
|
crate::types::SkillType::Fishing => "fishing",
|
|
crate::types::SkillType::Cooking => "cooking",
|
|
crate::types::SkillType::Carpentry => "carpentry",
|
|
crate::types::SkillType::Blacksmithy => "blacksmithy",
|
|
crate::types::SkillType::Tailoring => "tailoring",
|
|
crate::types::SkillType::Alchemy => "alchemy",
|
|
}),
|
|
crafting_recipes::level.eq(recipe.level),
|
|
crafting_recipes::workbench_id.eq(recipe.workbench_id),
|
|
crafting_recipes::xp.eq(recipe.xp),
|
|
crafting_recipes::unlocked_by_default.eq(recipe.unlocked_by_default as i32),
|
|
crafting_recipes::checks.eq(recipe.checks.as_ref()),
|
|
))
|
|
.execute(conn)?;
|
|
|
|
// Get the recipe_id we just inserted
|
|
let recipe_id: i32 = diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>(
|
|
"last_insert_rowid()"
|
|
))
|
|
.get_result(conn)?;
|
|
|
|
// Insert recipe items (ingredients)
|
|
for ingredient in &recipe.items {
|
|
diesel::insert_into(crafting_recipe_items::table)
|
|
.values((
|
|
crafting_recipe_items::recipe_id.eq(recipe_id),
|
|
crafting_recipe_items::item_id.eq(ingredient.item_id),
|
|
crafting_recipe_items::amount.eq(ingredient.amount),
|
|
))
|
|
.execute(conn)?;
|
|
}
|
|
}
|
|
|
|
count += 1;
|
|
}
|
|
|
|
Ok(count)
|
|
})
|
|
}
|
|
|
|
/// Save all items to SQLite database with icon processing
|
|
///
|
|
/// # Arguments
|
|
/// * `conn` - Database connection
|
|
/// * `icon_path` - Path to the ItemIcons directory (e.g., "CBAssets/Data/Textures/ItemIcons")
|
|
///
|
|
/// # Returns
|
|
/// Tuple of (items_saved, images_processed)
|
|
pub fn save_to_db_with_images<P: AsRef<Path>>(
|
|
&self,
|
|
conn: &mut SqliteConnection,
|
|
icon_path: P,
|
|
) -> Result<(usize, usize), diesel::result::Error> {
|
|
use crate::schema::items;
|
|
use diesel::replace_into;
|
|
|
|
let icon_base_path = icon_path.as_ref();
|
|
let processor = ImageProcessor::new(85.0); // 85% WebP quality
|
|
let mut images_processed = 0;
|
|
|
|
conn.transaction::<_, diesel::result::Error, _>(|conn| {
|
|
let mut count = 0;
|
|
|
|
for item in &self.items {
|
|
let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string());
|
|
|
|
// Process item icon if it exists
|
|
let (icon_large, icon_medium, icon_small) =
|
|
Self::process_item_icon(&processor, icon_base_path, item.type_id);
|
|
|
|
if icon_large.is_some() {
|
|
images_processed += 1;
|
|
}
|
|
|
|
// Insert/replace item with all columns including images
|
|
replace_into(items::table)
|
|
.values((
|
|
items::id.eq(item.type_id),
|
|
items::name.eq(&item.item_name),
|
|
items::data.eq(json),
|
|
items::item_type.eq(item.item_type.to_string()),
|
|
items::level.eq(item.level),
|
|
items::price.eq(item.price),
|
|
items::max_stack.eq(item.max_stack),
|
|
items::storage_size.eq(item.storage_size),
|
|
items::skill.eq(match item.skill {
|
|
crate::types::SkillType::None => "none",
|
|
crate::types::SkillType::Swordsmanship => "swordsmanship",
|
|
crate::types::SkillType::Archery => "archery",
|
|
crate::types::SkillType::Magic => "magic",
|
|
crate::types::SkillType::Defence => "defence",
|
|
crate::types::SkillType::Mining => "mining",
|
|
crate::types::SkillType::Woodcutting => "woodcutting",
|
|
crate::types::SkillType::Fishing => "fishing",
|
|
crate::types::SkillType::Cooking => "cooking",
|
|
crate::types::SkillType::Carpentry => "carpentry",
|
|
crate::types::SkillType::Blacksmithy => "blacksmithy",
|
|
crate::types::SkillType::Tailoring => "tailoring",
|
|
crate::types::SkillType::Alchemy => "alchemy",
|
|
}),
|
|
items::tool.eq(match item.tool {
|
|
crate::types::Tool::None => "none",
|
|
crate::types::Tool::Pickaxe => "pickaxe",
|
|
crate::types::Tool::Hatchet => "hatchet",
|
|
crate::types::Tool::Scythe => "scythe",
|
|
crate::types::Tool::Hammer => "hammer",
|
|
crate::types::Tool::Shears => "shears",
|
|
crate::types::Tool::FishingRod => "fishingrod",
|
|
}),
|
|
items::description.eq(&item.description),
|
|
items::two_handed.eq(item.two_handed as i32),
|
|
items::undroppable.eq(item.undroppable as i32),
|
|
items::undroppable_on_death.eq(item.undroppable_on_death as i32),
|
|
items::unequip_destroy.eq(item.unequip_destroy as i32),
|
|
items::generate_icon.eq(item.generate_icon as i32),
|
|
items::hide_milestone.eq(item.hide_milestone as i32),
|
|
items::cannot_craft_exceptional.eq(item.cannot_craft_exceptional as i32),
|
|
items::storage_all_items.eq(item.storage_all_items as i32),
|
|
items::ability_id.eq(item.ability_id),
|
|
items::special_ability.eq(item.special_ability),
|
|
items::learn_ability_id.eq(item.learn_ability_id),
|
|
items::book_id.eq(item.book_id),
|
|
items::swap_item.eq(item.swap_item),
|
|
items::icon_large.eq(icon_large.as_ref()),
|
|
items::icon_medium.eq(icon_medium.as_ref()),
|
|
items::icon_small.eq(icon_small.as_ref()),
|
|
))
|
|
.execute(conn)?;
|
|
|
|
// Save crafting recipes for this item (same as before)
|
|
for recipe in &item.crafting_recipes {
|
|
use diesel::prelude::*;
|
|
|
|
diesel::insert_into(crate::schema::crafting_recipes::table)
|
|
.values((
|
|
crate::schema::crafting_recipes::product_item_id.eq(item.type_id),
|
|
crate::schema::crafting_recipes::skill.eq(match recipe.skill {
|
|
crate::types::SkillType::None => "none",
|
|
crate::types::SkillType::Swordsmanship => "swordsmanship",
|
|
crate::types::SkillType::Archery => "archery",
|
|
crate::types::SkillType::Magic => "magic",
|
|
crate::types::SkillType::Defence => "defence",
|
|
crate::types::SkillType::Mining => "mining",
|
|
crate::types::SkillType::Woodcutting => "woodcutting",
|
|
crate::types::SkillType::Fishing => "fishing",
|
|
crate::types::SkillType::Cooking => "cooking",
|
|
crate::types::SkillType::Carpentry => "carpentry",
|
|
crate::types::SkillType::Blacksmithy => "blacksmithy",
|
|
crate::types::SkillType::Tailoring => "tailoring",
|
|
crate::types::SkillType::Alchemy => "alchemy",
|
|
}),
|
|
crate::schema::crafting_recipes::level.eq(recipe.level),
|
|
crate::schema::crafting_recipes::workbench_id.eq(recipe.workbench_id),
|
|
crate::schema::crafting_recipes::xp.eq(recipe.xp),
|
|
crate::schema::crafting_recipes::unlocked_by_default.eq(recipe.unlocked_by_default as i32),
|
|
crate::schema::crafting_recipes::checks.eq(recipe.checks.as_ref()),
|
|
))
|
|
.execute(conn)?;
|
|
|
|
let recipe_id: i32 = diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>(
|
|
"last_insert_rowid()"
|
|
))
|
|
.get_result(conn)?;
|
|
|
|
for ingredient in &recipe.items {
|
|
diesel::insert_into(crate::schema::crafting_recipe_items::table)
|
|
.values((
|
|
crate::schema::crafting_recipe_items::recipe_id.eq(recipe_id),
|
|
crate::schema::crafting_recipe_items::item_id.eq(ingredient.item_id),
|
|
crate::schema::crafting_recipe_items::amount.eq(ingredient.amount),
|
|
))
|
|
.execute(conn)?;
|
|
}
|
|
}
|
|
|
|
// Save item stats
|
|
for stat in &item.stats {
|
|
let stat_type_str = match stat.stat_type {
|
|
crate::types::StatType::None => "none",
|
|
crate::types::StatType::Health => "health",
|
|
crate::types::StatType::Mana => "mana",
|
|
crate::types::StatType::HealthRegen => "health_regen",
|
|
crate::types::StatType::ManaRegen => "mana_regen",
|
|
crate::types::StatType::DamagePhysical => "damage_physical",
|
|
crate::types::StatType::DamageMagical => "damage_magical",
|
|
crate::types::StatType::DamageRanged => "damage_ranged",
|
|
crate::types::StatType::AccuracyPhysical => "accuracy_physical",
|
|
crate::types::StatType::AccuracyMagical => "accuracy_magical",
|
|
crate::types::StatType::AccuracyRanged => "accuracy_ranged",
|
|
crate::types::StatType::ResistancePhysical => "resistance_physical",
|
|
crate::types::StatType::ResistanceMagical => "resistance_magical",
|
|
crate::types::StatType::ResistanceRanged => "resistance_ranged",
|
|
crate::types::StatType::Critical => "critical",
|
|
crate::types::StatType::Healing => "healing",
|
|
crate::types::StatType::MovementSpeed => "movement_speed",
|
|
crate::types::StatType::DamageVsBeasts => "damage_vs_beasts",
|
|
crate::types::StatType::DamageVsUndead => "damage_vs_undead",
|
|
crate::types::StatType::CritterSlaying => "critter_slaying",
|
|
};
|
|
|
|
diesel::insert_into(crate::schema::item_stats::table)
|
|
.values((
|
|
crate::schema::item_stats::item_id.eq(item.type_id),
|
|
crate::schema::item_stats::stat_type.eq(stat_type_str),
|
|
crate::schema::item_stats::value.eq(stat.value),
|
|
))
|
|
.execute(conn)?;
|
|
}
|
|
|
|
count += 1;
|
|
}
|
|
|
|
Ok((count, images_processed))
|
|
})
|
|
}
|
|
|
|
/// Helper function to process a single item icon
|
|
/// Returns (large, medium, small) WebP blobs
|
|
fn process_item_icon(
|
|
processor: &ImageProcessor,
|
|
icon_base_path: &Path,
|
|
item_id: i32,
|
|
) -> (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>) {
|
|
// Try both lowercase and uppercase extensions (Linux is case-sensitive)
|
|
let lowercase = icon_base_path.join(format!("{}.png", item_id));
|
|
let uppercase = icon_base_path.join(format!("{}.PNG", item_id));
|
|
|
|
let icon_file = if lowercase.exists() {
|
|
lowercase
|
|
} else if uppercase.exists() {
|
|
uppercase
|
|
} else {
|
|
return (None, None, None);
|
|
};
|
|
|
|
// Process image at 3 sizes: 256, 64, 16
|
|
match processor.process_image(&icon_file, &[256, 64, 16], None, None) {
|
|
Ok(processed) => (
|
|
processed.get(256).cloned(),
|
|
processed.get(64).cloned(),
|
|
processed.get(16).cloned(),
|
|
),
|
|
Err(e) => {
|
|
log::warn!("Failed to process icon for item {}: {}", item_id, e);
|
|
(None, None, None)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load all items from SQLite database
|
|
pub fn load_from_db(conn: &mut SqliteConnection) -> Result<Self, diesel::result::Error> {
|
|
use crate::schema::items::dsl::*;
|
|
|
|
#[derive(Queryable)]
|
|
#[allow(dead_code)]
|
|
struct ItemRecord {
|
|
id: Option<i32>,
|
|
name: String,
|
|
data: String,
|
|
item_type: String,
|
|
level: i32,
|
|
price: i32,
|
|
max_stack: i32,
|
|
storage_size: i32,
|
|
skill: String,
|
|
tool: String,
|
|
description: String,
|
|
two_handed: i32,
|
|
undroppable: i32,
|
|
undroppable_on_death: i32,
|
|
unequip_destroy: i32,
|
|
generate_icon: i32,
|
|
hide_milestone: i32,
|
|
cannot_craft_exceptional: i32,
|
|
storage_all_items: i32,
|
|
ability_id: i32,
|
|
special_ability: i32,
|
|
learn_ability_id: i32,
|
|
book_id: i32,
|
|
swap_item: i32,
|
|
icon_large: Option<Vec<u8>>,
|
|
icon_medium: Option<Vec<u8>>,
|
|
icon_small: Option<Vec<u8>>,
|
|
}
|
|
|
|
let records = items.load::<ItemRecord>(conn)?;
|
|
|
|
let mut loaded_items = Vec::new();
|
|
for record in records {
|
|
// Load from JSON data column (contains complete item info including crafting recipes)
|
|
if let Ok(item) = serde_json::from_str::<Item>(&record.data) {
|
|
loaded_items.push(item);
|
|
}
|
|
}
|
|
|
|
let mut db = Self::new();
|
|
db.add_items(loaded_items);
|
|
Ok(db)
|
|
}
|
|
}
|
|
|
|
impl Default for ItemDatabase {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_item_database_basic() {
|
|
let mut db = ItemDatabase::new();
|
|
assert!(db.is_empty());
|
|
assert_eq!(db.len(), 0);
|
|
}
|
|
}
|