veloren_server/persistence/character/
conversions.rs

1use crate::persistence::{
2    character::EntityId,
3    error::PersistenceError,
4    json_models::{
5        self, CharacterPosition, DatabaseAbilitySet, DatabaseItemProperties, GenericBody,
6        HumanoidBody,
7    },
8    models::{AbilitySets, Character, Item, SkillGroup},
9};
10use common::{
11    character::CharacterId,
12    comp::{
13        ActiveAbilities, Body as CompBody, Hardcore, Inventory, MapMarker, Stats, Waypoint, body,
14        inventory::{
15            item::{Item as VelorenItem, MaterialStatManifest, tool::AbilityMap},
16            loadout::{Loadout, LoadoutError},
17            loadout_builder::LoadoutBuilder,
18            recipe_book::RecipeBook,
19            slot::InvSlotId,
20        },
21        item,
22        skillset::{self, SkillGroupKind, SkillSet, skills::Skill},
23    },
24    resources::Time,
25};
26use core::{convert::TryFrom, num::NonZeroU64};
27use hashbrown::HashMap;
28use lazy_static::lazy_static;
29use std::{collections::VecDeque, str::FromStr, sync::Arc};
30use tracing::{trace, warn};
31
32#[derive(Debug)]
33pub struct ItemModelPair {
34    pub comp: Arc<item::ItemId>,
35    pub model: Item,
36}
37
38// Decoupled from the ECS resource because the plumbing is getting complicated;
39// shouldn't matter unless someone's hot-reloading material stats on the live
40// server
41lazy_static! {
42    pub static ref MATERIAL_STATS_MANIFEST: MaterialStatManifest =
43        MaterialStatManifest::load().cloned();
44    pub static ref ABILITY_MAP: AbilityMap = AbilityMap::load().cloned();
45}
46
47/// Returns a vector that contains all item rows to upsert; parent is
48/// responsible for deleting items from the same owner that aren't affirmatively
49/// kept by this.
50///
51/// NOTE: This method does not yet handle persisting nested items within
52/// inventories. Although loadout items do store items inside them this does
53/// not currently utilise `parent_container_id` - all loadout items have the
54/// loadout pseudo-container as their parent.
55pub fn convert_items_to_database_items(
56    loadout_container_id: EntityId,
57    inventory: &Inventory,
58    inventory_container_id: EntityId,
59    overflow_items_container_id: EntityId,
60    recipe_book_container_id: EntityId,
61    next_id: &mut i64,
62) -> Vec<ItemModelPair> {
63    let loadout = inventory
64        .loadout_items_with_persistence_key()
65        .map(|(slot, item)| (slot.to_string(), item, loadout_container_id));
66
67    let overflow_items = inventory.overflow_items().enumerate().map(|(i, item)| {
68        (
69            serde_json::to_string(&i).expect("failed to serialize index of overflow item"),
70            Some(item),
71            overflow_items_container_id,
72        )
73    });
74
75    let recipe_book = inventory
76        .persistence_recipes_iter_with_index()
77        .map(|(i, item)| {
78            (
79                serde_json::to_string(&i)
80                    .expect("failed to serialize index of recipe from recipe book"),
81                Some(item),
82                recipe_book_container_id,
83            )
84        });
85    // Inventory slots.
86    let inventory = inventory.slots_with_id().map(|(pos, item)| {
87        (
88            serde_json::to_string(&pos).expect("failed to serialize InvSlotId"),
89            item.as_ref(),
90            inventory_container_id,
91        )
92    });
93
94    // Use Breadth-first search to recurse into containers/modular weapons to store
95    // their parts
96    let mut bfs_queue: VecDeque<_> = inventory
97        .chain(loadout)
98        .chain(overflow_items)
99        .chain(recipe_book)
100        .collect();
101    let mut upserts = Vec::new();
102    let mut depth = HashMap::new();
103    depth.insert(inventory_container_id, 0);
104    depth.insert(loadout_container_id, 0);
105    depth.insert(overflow_items_container_id, 0);
106    depth.insert(recipe_book_container_id, 0);
107    while let Some((position, item, parent_container_item_id)) = bfs_queue.pop_front() {
108        // Construct new items.
109        if let Some(item) = item {
110            // Try using the next available id in the sequence as the default for new items.
111            let new_item_id = NonZeroU64::new(u64::try_from(*next_id).expect(
112                "We are willing to crash if the next entity id overflows (or is otherwise \
113                 negative).",
114            ))
115            .expect("next_id should not be zero, either");
116
117            // Fast (kinda) path: acquire read for the common case where an id has
118            // already been assigned.
119            let comp = item.get_item_id_for_database();
120            let item_id = comp.load()
121                // First, we filter out "impossible" entity IDs--IDs that are larger
122                // than the maximum sequence value (next_id).  This is important
123                // because we update the item ID atomically, *before* we know whether
124                // this transaction has completed successfully, and we don't abort the
125                // process on a failed transaction.  In such cases, new IDs from
126                // aborted transactions will show up as having a higher value than the
127                // current max sequence number.  Because the only place that modifies
128                // the item_id through a shared reference is (supposed to be) this
129                // function, which is part of the batch update transaction, we can
130                // assume that any rollback during the update would fail to insert
131                // *any* new items for the current character; this means that any items
132                // inserted between the failure and now (i.e. values less than next_id)
133                // would either not be items at all, or items belonging to other
134                // characters, leading to an easily detectable SQLite failure that we
135                // can use to atomically set the id back to None (if it was still the
136                // same bad value).
137                //
138                // Note that this logic only requires that all the character's items be
139                // updated within the same serializable transaction; the argument does
140                // not depend on SQLite-specific details (like locking) or on the fact
141                // that a user's transactions are always serialized on their own
142                // session.  Also note that since these IDs are in-memory, we don't
143                // have to worry about their values during, e.g., a process crash;
144                // serializability will take care of us in those cases.  Finally, note
145                // that while we have not yet implemented the "liveness" part of the
146                // algorithm (resetting ids back to None if we detect errors), this is
147                // not needed for soundness, and this part can be deferred until we
148                // switch to an execution model where such races are actually possible
149                // during normal gameplay.
150                .and_then(|item_id| Some(if item_id >= new_item_id {
151                    // Try to atomically exchange with our own, "correct" next id.
152                    match comp.compare_exchange(Some(item_id), Some(new_item_id)) {
153                        Ok(_) => {
154                            let item_id = *next_id;
155                            // We won the race, use next_id and increment it.
156                            *next_id += 1;
157                            item_id
158                        },
159                        Err(item_id) => {
160                            // We raced with someone, and they won the race, so we know
161                            // this transaction must abort unless they finish first.  So,
162                            // just assume they will finish first, and use their assigned
163                            // item_id.
164                            EntityId::try_from(item_id?.get())
165                                .expect("We always choose legal EntityIds as item ids")
166                        },
167                    }
168                } else { EntityId::try_from(item_id.get()).expect("We always choose legal EntityIds as item ids") }))
169                // Finally, we're in the case where no entity was assigned yet (either
170                // ever, or due to corrections after a rollback).  This proceeds
171                // identically to the "impossible ID" case.
172                .unwrap_or_else(|| {
173                    // Try to atomically compare with the empty id.
174                    match comp.compare_exchange(None, Some(new_item_id)) {
175                        Ok(_) => {
176                            let item_id = *next_id;
177                            *next_id += 1;
178                            item_id
179                        },
180                        Err(item_id) => {
181                            EntityId::try_from(item_id.expect("TODO: Fix handling of reset to None when we have concurrent writers.").get())
182                                .expect("We always choose legal EntityIds as item ids")
183                        },
184                    }
185                });
186
187            depth.insert(item_id, depth[&parent_container_item_id] + 1);
188
189            for (i, component) in item.components().iter().enumerate() {
190                // recursive items' children have the same position as their parents, and since
191                // they occur afterwards in the topological sort of the parent graph (which
192                // should still always be a tree, even with recursive items), we
193                // have enough information to put them back into their parents on load
194                bfs_queue.push_back((format!("component_{}", i), Some(component), item_id));
195            }
196
197            let item_properties = json_models::item_properties_to_db_model(item);
198
199            let upsert = ItemModelPair {
200                model: Item {
201                    item_definition_id: item.persistence_item_id(),
202                    position,
203                    parent_container_item_id,
204                    item_id,
205                    stack_size: if item.is_stackable() {
206                        item.amount().into()
207                    } else {
208                        1
209                    },
210                    properties: serde_json::to_string(&item_properties)
211                        .expect("Failed to convert item properties to a json string."),
212                },
213                // Continue to remember the atomic, in case we detect an error later and want
214                // to roll back to preserve liveness.
215                comp,
216            };
217            upserts.push(upsert);
218        }
219    }
220    upserts.sort_by_key(|pair| (depth[&pair.model.item_id], pair.model.item_id));
221    trace!("upserts: {:#?}", upserts);
222    upserts
223}
224
225pub fn convert_body_to_database_json(
226    comp_body: &CompBody,
227) -> Result<(&str, String), PersistenceError> {
228    Ok(match comp_body {
229        CompBody::Humanoid(body) => (
230            "humanoid",
231            serde_json::to_string(&HumanoidBody::from(body))?,
232        ),
233        CompBody::QuadrupedLow(body) => (
234            "quadruped_low",
235            serde_json::to_string(&GenericBody::from(body))?,
236        ),
237        CompBody::QuadrupedMedium(body) => (
238            "quadruped_medium",
239            serde_json::to_string(&GenericBody::from(body))?,
240        ),
241        CompBody::QuadrupedSmall(body) => (
242            "quadruped_small",
243            serde_json::to_string(&GenericBody::from(body))?,
244        ),
245        CompBody::BirdMedium(body) => (
246            "bird_medium",
247            serde_json::to_string(&GenericBody::from(body))?,
248        ),
249        CompBody::Crustacean(body) => (
250            "crustacean",
251            serde_json::to_string(&GenericBody::from(body))?,
252        ),
253        _ => {
254            return Err(PersistenceError::ConversionError(format!(
255                "Unsupported body type for persistence: {:?}",
256                comp_body
257            )));
258        },
259    })
260}
261
262pub fn convert_waypoint_to_database_json(
263    waypoint: Option<Waypoint>,
264    map_marker: Option<MapMarker>,
265) -> Option<String> {
266    if waypoint.is_some() || map_marker.is_some() {
267        let charpos = CharacterPosition {
268            waypoint: waypoint.map(|w| w.get_pos()),
269            map_marker: map_marker.map(|m| m.0),
270        };
271        Some(
272            serde_json::to_string(&charpos)
273                .map_err(|err| {
274                    PersistenceError::ConversionError(format!("Error encoding waypoint: {:?}", err))
275                })
276                .ok()?,
277        )
278    } else {
279        None
280    }
281}
282
283pub fn convert_waypoint_from_database_json(
284    position: &str,
285) -> Result<(Option<Waypoint>, Option<MapMarker>), PersistenceError> {
286    let character_position =
287        serde_json::de::from_str::<CharacterPosition>(position).map_err(|err| {
288            PersistenceError::ConversionError(format!(
289                "Error de-serializing waypoint: {} err: {}",
290                position, err
291            ))
292        })?;
293    Ok((
294        character_position
295            .waypoint
296            .map(|pos| Waypoint::new(pos, Time(0.0))),
297        character_position.map_marker.map(MapMarker),
298    ))
299}
300
301// Used to handle cases of modular items that are composed of components.
302// When called with the index of a component's parent item, it can get a mutable
303// reference to that parent item so that the component can be added to the
304// parent item. If the item corresponding to the index this is called on is
305// itself a component, recursively goes through inventory until it grabs
306// component.
307fn get_mutable_item<'a, 'b, T>(
308    index: usize,
309    inventory_items: &'a [Item],
310    item_indices: &'a HashMap<i64, usize>,
311    inventory: &'b mut T,
312    get_mut_item: &'a impl Fn(&'b mut T, &str) -> Option<&'b mut VelorenItem>,
313) -> Result<&'a mut VelorenItem, PersistenceError>
314where
315    'b: 'a,
316{
317    // First checks if item is a component, if it is, tries to get a mutable
318    // reference to itself by getting a mutable reference to the item that is its
319    // parent
320    //
321    // It is safe to directly index into `inventory_items` with `index` as the
322    // parent item of a component is loaded before its components, therefore the
323    // index of a parent item should exist when loading the component.
324    let parent_id = inventory_items[index].parent_container_item_id;
325    if inventory_items[index].position.contains("component_") {
326        if let Some(parent) = item_indices.get(&parent_id).map(move |i| {
327            get_mutable_item(
328                *i,
329                inventory_items,
330                item_indices,
331                inventory,
332                // slot,
333                get_mut_item,
334            )
335        }) {
336            // Parses component index
337            let position = &inventory_items[index].position;
338            let component_index = position
339                .split('_')
340                .nth(1)
341                .and_then(|s| s.parse::<usize>().ok())
342                .ok_or_else(|| {
343                    PersistenceError::ConversionError(format!(
344                        "Failed to parse position stored in database: {position}."
345                    ))
346                })?;
347            // Returns mutable reference to component item by accessing the component
348            // through its parent item item
349            parent?
350                .persistence_access_mutable_component(component_index)
351                .ok_or_else(|| {
352                    PersistenceError::ConversionError(format!(
353                        "Component in position {component_index} doesn't exist on parent item \
354                         {parent_id}."
355                    ))
356                })
357        } else {
358            Err(PersistenceError::ConversionError(format!(
359                "Parent item with id {parent_id} does not exist in database."
360            )))
361        }
362    } else {
363        get_mut_item(inventory, &inventory_items[index].position).ok_or_else(|| {
364            PersistenceError::ConversionError(format!(
365                "Unable to retrieve parent veloren item {parent_id} of component from inventory."
366            ))
367        })
368    }
369}
370
371/// Properly-recursive items (currently modular weapons) occupy the same
372/// inventory slot as their parent. The caller is responsible for ensuring that
373/// inventory_items and loadout_items are topologically sorted (i.e. forall i,
374/// `items[i].parent_container_item_id == x` implies exists j < i satisfying
375/// `items[j].item_id == x`)
376pub fn convert_inventory_from_database_items(
377    inventory_container_id: i64,
378    inventory_items: &[Item],
379    loadout_container_id: i64,
380    loadout_items: &[Item],
381    overflow_items_container_id: i64,
382    overflow_items: &[Item],
383    recipe_book_items: &[Item],
384) -> Result<Inventory, PersistenceError> {
385    // Loadout items must be loaded before inventory items since loadout items
386    // provide inventory slots. Since items stored inside loadout items actually
387    // have their parent_container_item_id as the loadout pseudo-container we rely
388    // on populating the loadout items first, and then inserting the items into the
389    // inventory at the correct position.
390    //
391    let loadout = convert_loadout_from_database_items(loadout_container_id, loadout_items)?;
392    let overflow_items =
393        convert_overflow_items_from_database_items(overflow_items_container_id, overflow_items)?;
394    let recipe_book = convert_recipe_book_from_database_items(recipe_book_items)?;
395    let mut inventory = Inventory::with_loadout_humanoid(loadout).with_recipe_book(recipe_book);
396    let mut item_indices = HashMap::new();
397
398    let mut failed_inserts = HashMap::new();
399
400    // In order to items with components to properly load, it is important that this
401    // item iteration occurs in order so that any modular items are loaded before
402    // its components.
403    for (i, db_item) in inventory_items.iter().enumerate() {
404        item_indices.insert(db_item.item_id, i);
405
406        let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
407        let item_properties =
408            serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
409        json_models::apply_db_item_properties(&mut item, &item_properties);
410
411        // NOTE: Since this is freshly loaded, the atomic is *unique.*
412        let comp = item.get_item_id_for_database();
413
414        // Item ID
415        comp.store(Some(NonZeroU64::try_from(db_item.item_id as u64).map_err(
416            |_| PersistenceError::ConversionError("Item with zero item_id".to_owned()),
417        )?));
418
419        // Stack Size
420        if db_item.stack_size == 1 || item.is_stackable() {
421            // FIXME: On failure, collect the set of items that don't fit and return them
422            // (to be dropped next to the player) as this could be the result of
423            // a change in the max amount for that item.
424            item.set_amount(u32::try_from(db_item.stack_size).map_err(|_| {
425                PersistenceError::ConversionError(format!(
426                    "Invalid item stack size for stackable={}: {}",
427                    item.is_stackable(),
428                    &db_item.stack_size
429                ))
430            })?)
431            .map_err(|_| {
432                PersistenceError::ConversionError("Error setting amount for item".to_owned())
433            })?;
434        }
435
436        // Insert item into inventory
437
438        // Slot position
439        let slot = |s: &str| {
440            serde_json::from_str::<InvSlotId>(s).map_err(|_| {
441                PersistenceError::ConversionError(format!(
442                    "Failed to parse item position: {:?}",
443                    &db_item.position
444                ))
445            })
446        };
447
448        if db_item.parent_container_item_id == inventory_container_id {
449            match slot(&db_item.position) {
450                Ok(slot) => {
451                    let insert_res = inventory.insert_at(slot, item);
452
453                    match insert_res {
454                        Ok(None) => {
455                            // Insert successful
456                        },
457                        Ok(Some(_item)) => {
458                            // If inventory.insert returns an item, it means it was swapped for
459                            // an item that already occupied the
460                            // slot. Multiple items being stored
461                            // in the database for the same slot is
462                            // an error.
463                            return Err(PersistenceError::ConversionError(
464                                "Inserted an item into the same slot twice".to_string(),
465                            ));
466                        },
467                        Err(item) => {
468                            // If this happens there were too many items in the database for the
469                            // current inventory size
470                            failed_inserts.insert(db_item.position.clone(), item);
471                        },
472                    }
473                },
474                Err(err) => {
475                    return Err(err);
476                },
477            }
478        } else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
479            get_mutable_item(
480                j,
481                inventory_items,
482                &item_indices,
483                &mut (&mut inventory, &mut failed_inserts),
484                &|(inv, f_i): &mut (&mut Inventory, &mut HashMap<String, VelorenItem>), s| {
485                    // Attempts first to access inventory if that slot exists there. If it does not
486                    // it instead attempts to access failed inserts list.
487                    slot(s)
488                        .ok()
489                        .and_then(|slot| inv.slot_mut(slot))
490                        .and_then(|a| a.as_mut())
491                        .or_else(|| f_i.get_mut(s))
492                },
493            )?
494            .persistence_access_add_component(item);
495        } else {
496            return Err(PersistenceError::ConversionError(format!(
497                "Couldn't find parent item {} before item {} in inventory",
498                db_item.parent_container_item_id, db_item.item_id
499            )));
500        }
501    }
502
503    // For overflow items and failed inserts, attempt to push to inventory. If push
504    // fails, move to overflow slots.
505    if let Err(inv_error) = inventory.push_all(
506        overflow_items
507            .into_iter()
508            .chain(failed_inserts.into_values()),
509    ) {
510        inventory.persistence_push_overflow_items(inv_error.returned_items());
511    }
512
513    // Some items may have had components added, so update the item config of each
514    // item to ensure that it correctly accounts for components that were added
515    inventory.persistence_update_all_item_states(&ABILITY_MAP, &MATERIAL_STATS_MANIFEST);
516
517    Ok(inventory)
518}
519
520pub fn convert_loadout_from_database_items(
521    loadout_container_id: i64,
522    database_items: &[Item],
523) -> Result<Loadout, PersistenceError> {
524    let loadout_builder = LoadoutBuilder::empty();
525    let mut loadout = loadout_builder.build();
526    let mut item_indices = HashMap::new();
527
528    // In order to items with components to properly load, it is important that this
529    // item iteration occurs in order so that any modular items are loaded before
530    // its components.
531    for (i, db_item) in database_items.iter().enumerate() {
532        item_indices.insert(db_item.item_id, i);
533
534        let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
535        let item_properties =
536            serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
537        json_models::apply_db_item_properties(&mut item, &item_properties);
538
539        // NOTE: item id is currently *unique*, so we can store the ID safely.
540        let comp = item.get_item_id_for_database();
541        comp.store(Some(NonZeroU64::try_from(db_item.item_id as u64).map_err(
542            |_| PersistenceError::ConversionError("Item with zero item_id".to_owned()),
543        )?));
544
545        let convert_error = |err| match err {
546            LoadoutError::InvalidPersistenceKey => PersistenceError::ConversionError(format!(
547                "Invalid persistence key: {}",
548                &db_item.position
549            )),
550            LoadoutError::NoParentAtSlot => PersistenceError::ConversionError(format!(
551                "No parent item at slot: {}",
552                &db_item.position
553            )),
554        };
555
556        if db_item.parent_container_item_id == loadout_container_id {
557            loadout
558                .set_item_at_slot_using_persistence_key(&db_item.position, item)
559                .map_err(convert_error)?;
560        } else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
561            get_mutable_item(j, database_items, &item_indices, &mut loadout, &|l, s| {
562                l.get_mut_item_at_slot_using_persistence_key(s).ok()
563            })?
564            .persistence_access_add_component(item);
565        } else {
566            return Err(PersistenceError::ConversionError(format!(
567                "Couldn't find parent item {} before item {} in loadout",
568                db_item.parent_container_item_id, db_item.item_id
569            )));
570        }
571    }
572
573    // Some items may have had components added, so update the item config of each
574    // item to ensure that it correctly accounts for components that were added
575    loadout.persistence_update_all_item_states(&ABILITY_MAP, &MATERIAL_STATS_MANIFEST);
576
577    Ok(loadout)
578}
579
580pub fn convert_overflow_items_from_database_items(
581    overflow_items_container_id: i64,
582    database_items: &[Item],
583) -> Result<Vec<VelorenItem>, PersistenceError> {
584    let mut overflow_items_with_database_position = HashMap::new();
585    let mut item_indices = HashMap::new();
586
587    // In order to items with components to properly load, it is important that this
588    // item iteration occurs in order so that any modular items are loaded before
589    // its components.
590    for (i, db_item) in database_items.iter().enumerate() {
591        item_indices.insert(db_item.item_id, i);
592
593        let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
594        let item_properties =
595            serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
596        json_models::apply_db_item_properties(&mut item, &item_properties);
597
598        // NOTE: item id is currently *unique*, so we can store the ID safely.
599        let comp = item.get_item_id_for_database();
600
601        // Item ID
602        comp.store(Some(NonZeroU64::try_from(db_item.item_id as u64).map_err(
603            |_| PersistenceError::ConversionError("Item with zero item_id".to_owned()),
604        )?));
605
606        // Stack Size
607        if db_item.stack_size == 1 || item.is_stackable() {
608            // FIXME: On failure, collect the set of items that don't fit and return them
609            // (to be dropped next to the player) as this could be the result of
610            // a change in the max amount for that item.
611            item.set_amount(u32::try_from(db_item.stack_size).map_err(|_| {
612                PersistenceError::ConversionError(format!(
613                    "Invalid item stack size for stackable={}: {}",
614                    item.is_stackable(),
615                    &db_item.stack_size
616                ))
617            })?)
618            .map_err(|_| {
619                PersistenceError::ConversionError("Error setting amount for item".to_owned())
620            })?;
621        }
622
623        if db_item.parent_container_item_id == overflow_items_container_id {
624            match overflow_items_with_database_position.insert(db_item.position.clone(), item) {
625                None => {
626                    // Insert successful
627                },
628                Some(_item) => {
629                    // If insert returns a value, database had two items stored with the same
630                    // position which is an error.
631                    return Err(PersistenceError::ConversionError(
632                        "Inserted an item into the same overflow slot twice".to_string(),
633                    ));
634                },
635            }
636        } else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
637            get_mutable_item(
638                j,
639                database_items,
640                &item_indices,
641                &mut overflow_items_with_database_position,
642                &|o_i, s| o_i.get_mut(s),
643            )?
644            .persistence_access_add_component(item);
645        } else {
646            return Err(PersistenceError::ConversionError(format!(
647                "Couldn't find parent item {} before item {} in overflow items",
648                db_item.parent_container_item_id, db_item.item_id
649            )));
650        }
651    }
652
653    let overflow_items = overflow_items_with_database_position
654        .into_values()
655        .collect::<Vec<_>>();
656
657    Ok(overflow_items)
658}
659
660fn get_item_from_asset(item_definition_id: &str) -> Result<common::comp::Item, PersistenceError> {
661    common::comp::Item::new_from_asset(item_definition_id).map_err(|err| {
662        PersistenceError::AssetError(format!(
663            "Error loading item asset: {} - {}",
664            item_definition_id, err
665        ))
666    })
667}
668
669/// Generates the code to deserialize a specific body variant from JSON
670macro_rules! deserialize_body {
671    ($body_data:expr, $body_variant:tt, $body_type:tt) => {{
672        let json_model = serde_json::de::from_str::<GenericBody>($body_data)?;
673        CompBody::$body_variant(common::comp::$body_type::Body {
674            species: common::comp::$body_type::Species::from_str(&json_model.species)
675                .map_err(|_| {
676                    PersistenceError::ConversionError(format!(
677                        "Missing species: {}",
678                        json_model.species
679                    ))
680                })?
681                .to_owned(),
682            body_type: common::comp::$body_type::BodyType::from_str(&json_model.body_type)
683                .map_err(|_| {
684                    PersistenceError::ConversionError(format!(
685                        "Missing body type: {}",
686                        json_model.species
687                    ))
688                })?
689                .to_owned(),
690        })
691    }};
692}
693pub fn convert_body_from_database(
694    variant: &str,
695    body_data: &str,
696) -> Result<CompBody, PersistenceError> {
697    Ok(match variant {
698        // The humanoid variant doesn't use the body_variant! macro as it is unique in having
699        // extra fields on its body struct
700        "humanoid" => {
701            let json_model = serde_json::de::from_str::<HumanoidBody>(body_data)?;
702            CompBody::Humanoid(body::humanoid::Body {
703                species: body::humanoid::ALL_SPECIES
704                    .get(json_model.species as usize)
705                    .ok_or_else(|| {
706                        PersistenceError::ConversionError(format!(
707                            "Missing species: {}",
708                            json_model.species
709                        ))
710                    })?
711                    .to_owned(),
712                body_type: body::humanoid::ALL_BODY_TYPES
713                    .get(json_model.body_type as usize)
714                    .ok_or_else(|| {
715                        PersistenceError::ConversionError(format!(
716                            "Missing body_type: {}",
717                            json_model.body_type
718                        ))
719                    })?
720                    .to_owned(),
721                hair_style: json_model.hair_style,
722                beard: json_model.beard,
723                eyes: json_model.eyes,
724                accessory: json_model.accessory,
725                hair_color: json_model.hair_color,
726                skin: json_model.skin,
727                eye_color: json_model.eye_color,
728            })
729        },
730        "quadruped_low" => {
731            deserialize_body!(body_data, QuadrupedLow, quadruped_low)
732        },
733        "quadruped_medium" => {
734            deserialize_body!(body_data, QuadrupedMedium, quadruped_medium)
735        },
736        "quadruped_small" => {
737            deserialize_body!(body_data, QuadrupedSmall, quadruped_small)
738        },
739        "bird_medium" => {
740            deserialize_body!(body_data, BirdMedium, bird_medium)
741        },
742        "crustacean" => {
743            deserialize_body!(body_data, Crustacean, crustacean)
744        },
745        _ => {
746            return Err(PersistenceError::ConversionError(format!(
747                "{} is not a supported body type for deserialization",
748                variant
749            )));
750        },
751    })
752}
753
754pub fn convert_character_from_database(character: &Character) -> common::character::Character {
755    common::character::Character {
756        id: Some(CharacterId(character.character_id)),
757        alias: String::from(&character.alias),
758    }
759}
760
761pub fn convert_stats_from_database(alias: String, body: CompBody) -> Stats {
762    let mut new_stats = Stats::empty(body);
763    new_stats.name = alias;
764    new_stats
765}
766
767pub fn convert_hardcore_from_database(hardcore: i64) -> Result<Option<Hardcore>, PersistenceError> {
768    match hardcore {
769        0 => Ok(None),
770        1 => Ok(Some(common::comp::Hardcore)),
771        _ => Err(PersistenceError::ConversionError(format!(
772            "Invalid hardcore field: {hardcore}"
773        ))),
774    }
775}
776
777pub fn convert_hardcore_to_database(hardcore: Option<Hardcore>) -> i64 {
778    if hardcore.is_some() { 1 } else { 0 }
779}
780
781/// NOTE: This does *not* return an error on failure, since we can partially
782/// recover from some failures.  Instead, it returns the error in the second
783/// return value; make sure to handle it if present!
784pub fn convert_skill_set_from_database(
785    skill_groups: &[SkillGroup],
786) -> (SkillSet, Option<skillset::SkillsPersistenceError>) {
787    let (skillless_skill_groups, deserialized_skills) =
788        convert_skill_groups_from_database(skill_groups);
789    SkillSet::load_from_database(skillless_skill_groups, deserialized_skills)
790}
791
792fn convert_skill_groups_from_database(
793    skill_groups: &[SkillGroup],
794) -> (
795    // Skill groups in the vec do not contain skills, those are added later. The skill group only
796    // contains fields related to experience and skill points
797    HashMap<SkillGroupKind, skillset::SkillGroup>,
798    //
799    HashMap<SkillGroupKind, Result<Vec<Skill>, skillset::SkillsPersistenceError>>,
800) {
801    let mut new_skill_groups = HashMap::new();
802    let mut deserialized_skills = HashMap::new();
803    for skill_group in skill_groups.iter() {
804        let skill_group_kind = json_models::db_string_to_skill_group(&skill_group.skill_group_kind);
805        let mut new_skill_group = skillset::SkillGroup {
806            skill_group_kind,
807            // Available and earned exp and sp are reconstructed below
808            earned_exp: 0,
809            available_exp: 0,
810            available_sp: 0,
811            earned_sp: 0,
812            // Ordered skills empty here as skills get inserted later as they are unlocked, so long
813            // as there is not a respec.
814            ordered_skills: Vec::new(),
815        };
816
817        // Add experience to skill group through method to ensure invariant of
818        // (earned_exp >= available_exp) are maintained
819        // Adding experience will automatically earn all possible skill points
820        let skill_group_exp = skill_group.earned_exp.clamp(0, i64::from(u32::MAX)) as u32;
821        new_skill_group.add_experience(skill_group_exp);
822
823        use skillset::SkillsPersistenceError;
824
825        let skills_result = if skill_group.spent_exp != i64::from(new_skill_group.spent_exp()) {
826            // If persisted spent exp does not equal the spent exp after reacquiring skill
827            // points, force a respec
828            Err(SkillsPersistenceError::SpentExpMismatch)
829        } else if Some(&skill_group.hash_val) != skillset::SKILL_GROUP_HASHES.get(&skill_group_kind)
830        {
831            // Else if persisted hash for skill group does not match current hash for skill
832            // group, force a respec
833            Err(SkillsPersistenceError::HashMismatch)
834        } else {
835            // Else attempt to deserialize skills from a json string
836            match serde_json::from_str::<Vec<Skill>>(&skill_group.skills) {
837                // If it correctly deserializes, return the persisted skills
838                Ok(skills) => Ok(skills),
839                // Else if doesn't deserialize correctly, force a respec
840                Err(err) => {
841                    warn!(
842                        "Skills failed to correctly deserialized\nError: {:#?}\nRaw JSON: {:#?}",
843                        err, &skill_group.skills
844                    );
845                    Err(SkillsPersistenceError::DeserializationFailure)
846                },
847            }
848        };
849
850        deserialized_skills.insert(skill_group_kind, skills_result);
851
852        new_skill_groups.insert(skill_group_kind, new_skill_group);
853    }
854    (new_skill_groups, deserialized_skills)
855}
856
857pub fn convert_skill_groups_to_database<'a, I: Iterator<Item = &'a skillset::SkillGroup>>(
858    entity_id: CharacterId,
859    skill_groups: I,
860) -> Vec<SkillGroup> {
861    let skill_group_hashes = &skillset::SKILL_GROUP_HASHES;
862    skill_groups
863        .into_iter()
864        .map(|sg| SkillGroup {
865            entity_id: entity_id.0,
866            skill_group_kind: json_models::skill_group_to_db_string(sg.skill_group_kind),
867            earned_exp: i64::from(sg.earned_exp),
868            spent_exp: i64::from(sg.spent_exp()),
869            // If fails to convert, just forces a respec on next login
870            skills: serde_json::to_string(&sg.ordered_skills).unwrap_or_else(|_| "".to_string()),
871            hash_val: skill_group_hashes
872                .get(&sg.skill_group_kind)
873                .cloned()
874                .unwrap_or_default(),
875        })
876        .collect()
877}
878
879pub fn convert_active_abilities_to_database(
880    entity_id: CharacterId,
881    active_abilities: &ActiveAbilities,
882) -> AbilitySets {
883    let ability_sets = json_models::active_abilities_to_db_model(active_abilities);
884    AbilitySets {
885        entity_id: entity_id.0,
886        ability_sets: serde_json::to_string(&ability_sets).unwrap_or_default(),
887    }
888}
889
890pub fn convert_active_abilities_from_database(ability_sets: &AbilitySets) -> ActiveAbilities {
891    let ability_sets = serde_json::from_str::<Vec<DatabaseAbilitySet>>(&ability_sets.ability_sets)
892        .unwrap_or_else(|err| {
893            common_base::dev_panic!(format!(
894                "Failed to parse ability sets. Error: {:#?}\nAbility sets:\n{:#?}",
895                err, ability_sets.ability_sets
896            ));
897            Vec::new()
898        });
899    json_models::active_abilities_from_db_model(ability_sets)
900}
901
902pub fn convert_recipe_book_from_database_items(
903    database_items: &[Item],
904) -> Result<RecipeBook, PersistenceError> {
905    let mut recipes_groups = Vec::new();
906
907    for db_item in database_items.iter() {
908        let item = get_item_from_asset(db_item.item_definition_id.as_str())?;
909
910        // NOTE: item id is currently *unique*, so we can store the ID safely.
911        let comp = item.get_item_id_for_database();
912        comp.store(Some(NonZeroU64::try_from(db_item.item_id as u64).map_err(
913            |_| PersistenceError::ConversionError("Item with zero item_id".to_owned()),
914        )?));
915
916        recipes_groups.push(item);
917    }
918
919    let recipe_book = RecipeBook::recipe_book_from_persistence(recipes_groups);
920
921    Ok(recipe_book)
922}