veloren_server/persistence/character/
mod.rs

1//! Database operations related to character data
2//!
3//! Methods in this module should remain private to the persistence module -
4//! database updates and loading are communicated via requests to the
5//! [`CharacterLoader`] and [`CharacterUpdater`] while results/responses are
6//! polled and handled each server tick.
7extern crate rusqlite;
8
9use super::{error::PersistenceError, models::*};
10use crate::{
11    comp::{self, Inventory},
12    persistence::{
13        EditableComponents, PersistedComponents,
14        character::conversions::{
15            convert_active_abilities_from_database, convert_active_abilities_to_database,
16            convert_body_from_database, convert_body_to_database_json,
17            convert_character_from_database, convert_hardcore_from_database,
18            convert_hardcore_to_database, convert_inventory_from_database_items,
19            convert_items_to_database_items, convert_loadout_from_database_items,
20            convert_recipe_book_from_database_items, convert_skill_groups_to_database,
21            convert_skill_set_from_database, convert_stats_from_database,
22            convert_waypoint_from_database_json, convert_waypoint_to_database_json,
23        },
24        character_loader::{CharacterCreationResult, CharacterDataResult, CharacterListResult},
25        character_updater::PetPersistenceData,
26        error::PersistenceError::DatabaseError,
27    },
28};
29use common::{
30    character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER},
31    event::UpdateCharacterMetadata,
32};
33use core::ops::Range;
34use rusqlite::{Connection, ToSql, Transaction, types::Value};
35use std::{num::NonZeroU64, rc::Rc};
36use tracing::{debug, error, trace, warn};
37
38/// Private module for very tightly coupled database conversion methods.  In
39/// general, these have many invariants that need to be maintained when they're
40/// called--do not assume it's safe to make these public!
41mod conversions;
42
43pub(crate) type EntityId = i64;
44
45pub(crate) use conversions::convert_waypoint_from_database_json as parse_waypoint;
46
47const CHARACTER_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.character";
48const INVENTORY_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.inventory";
49const LOADOUT_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.loadout";
50const OVERFLOW_ITEMS_PSEUDO_CONTAINER_DEF_ID: &str =
51    "veloren.core.pseudo_containers.overflow_items";
52const RECIPE_BOOK_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.recipe_book";
53const INVENTORY_PSEUDO_CONTAINER_POSITION: &str = "inventory";
54const LOADOUT_PSEUDO_CONTAINER_POSITION: &str = "loadout";
55const OVERFLOW_ITEMS_PSEUDO_CONTAINER_POSITION: &str = "overflow_items";
56const RECIPE_BOOK_PSEUDO_CONTAINER_POSITION: &str = "recipe_book";
57const WORLD_PSEUDO_CONTAINER_ID: EntityId = 1;
58
59#[derive(Clone, Copy)]
60struct CharacterContainers {
61    inventory_container_id: EntityId,
62    loadout_container_id: EntityId,
63    overflow_items_container_id: EntityId,
64    recipe_book_container_id: EntityId,
65}
66
67/// Load the inventory/loadout
68///
69/// Loading is done recursively to ensure that each is topologically sorted in
70/// the sense required by convert_inventory_from_database_items.
71///
72/// For items with components, the parent item must sorted so that its
73/// components are after the parent item.
74pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, PersistenceError> {
75    let mut stmt = connection.prepare_cached(
76        "
77        WITH RECURSIVE
78        items_tree (
79            item_id,
80            parent_container_item_id,
81            item_definition_id,
82            stack_size,
83            position,
84            properties
85        ) AS (
86            SELECT  item_id,
87                    parent_container_item_id,
88                    item_definition_id,
89                    stack_size,
90                    position,
91                    properties
92            FROM item
93            WHERE parent_container_item_id = ?1
94            UNION ALL
95            SELECT  item.item_id,
96                    item.parent_container_item_id,
97                    item.item_definition_id,
98                    item.stack_size,
99                    item.position,
100                    item.properties
101            FROM item, items_tree
102            WHERE item.parent_container_item_id = items_tree.item_id
103        )
104        SELECT  *
105        FROM    items_tree",
106    )?;
107
108    let items = stmt
109        .query_map([root], |row| {
110            Ok(Item {
111                item_id: row.get(0)?,
112                parent_container_item_id: row.get(1)?,
113                item_definition_id: row.get(2)?,
114                stack_size: row.get(3)?,
115                position: row.get(4)?,
116                properties: row.get(5)?,
117            })
118        })?
119        .filter_map(Result::ok)
120        .collect::<Vec<Item>>();
121
122    Ok(items)
123}
124
125/// Load stored data for a character.
126///
127/// After first logging in, and after a character is selected, we fetch this
128/// data for the purpose of inserting their persisted data for the entity.
129pub fn load_character_data(
130    requesting_player_uuid: String,
131    char_id: CharacterId,
132    connection: &Connection,
133) -> CharacterDataResult {
134    let character_containers = get_pseudo_containers(connection, char_id)?;
135    let inventory_items = load_items(connection, character_containers.inventory_container_id)?;
136    let loadout_items = load_items(connection, character_containers.loadout_container_id)?;
137    let overflow_items_items =
138        load_items(connection, character_containers.overflow_items_container_id)?;
139    let recipe_book_items = load_items(connection, character_containers.recipe_book_container_id)?;
140
141    let mut stmt = connection.prepare_cached(
142        "
143        SELECT  c.character_id,
144                c.alias,
145                c.waypoint,
146                c.hardcore,
147                b.variant,
148                b.body_data
149        FROM    character c
150        JOIN    body b ON (c.character_id = b.body_id)
151        WHERE   c.player_uuid = ?1
152        AND     c.character_id = ?2",
153    )?;
154
155    let (body_data, character_data) = stmt.query_row(
156        [requesting_player_uuid.clone(), char_id.0.to_string()],
157        |row| {
158            let character_data = Character {
159                character_id: row.get(0)?,
160                player_uuid: requesting_player_uuid,
161                alias: row.get(1)?,
162                waypoint: row.get(2)?,
163                hardcore: row.get(3)?,
164            };
165
166            let body_data = Body {
167                body_id: row.get(0)?,
168                variant: row.get(4)?,
169                body_data: row.get(5)?,
170            };
171
172            Ok((body_data, character_data))
173        },
174    )?;
175
176    let (char_waypoint, char_map_marker) = match character_data
177        .waypoint
178        .as_ref()
179        .map(|x| convert_waypoint_from_database_json(x))
180    {
181        Some(Ok(w)) => w,
182        Some(Err(e)) => {
183            warn!(
184                "Error reading waypoint from database for character ID
185    {}, error: {}",
186                char_id.0, e
187            );
188            (None, None)
189        },
190        None => (None, None),
191    };
192
193    let mut stmt = connection.prepare_cached(
194        "
195        SELECT  skill_group_kind,
196                earned_exp,
197                spent_exp,
198                skills,
199                hash_val
200        FROM    skill_group
201        WHERE   entity_id = ?1",
202    )?;
203
204    let skill_group_data = stmt
205        .query_map([char_id.0], |row| {
206            Ok(SkillGroup {
207                entity_id: char_id.0,
208                skill_group_kind: row.get(0)?,
209                earned_exp: row.get(1)?,
210                spent_exp: row.get(2)?,
211                skills: row.get(3)?,
212                hash_val: row.get(4)?,
213            })
214        })?
215        .filter_map(Result::ok)
216        .collect::<Vec<SkillGroup>>();
217
218    #[rustfmt::skip]
219    let mut stmt = connection.prepare_cached("
220        SELECT  p.pet_id,
221                p.name,
222                b.variant,
223                b.body_data
224        FROM    pet p
225        JOIN    body b ON (p.pet_id = b.body_id)
226        WHERE   p.character_id = ?1",
227    )?;
228
229    let db_pets = stmt
230        .query_map([char_id.0], |row| {
231            Ok(Pet {
232                database_id: row.get(0)?,
233                name: row.get(1)?,
234                body_variant: row.get(2)?,
235                body_data: row.get(3)?,
236            })
237        })?
238        .filter_map(Result::ok)
239        .collect::<Vec<Pet>>();
240
241    // Re-construct the pet components for the player's pets, including
242    // de-serializing the pets' bodies and creating their Pet and Stats
243    // components
244    let pets = db_pets
245        .iter()
246        .filter_map(|db_pet| {
247            if let Ok(pet_body) =
248                convert_body_from_database(&db_pet.body_variant, &db_pet.body_data)
249            {
250                let pet = comp::Pet::new_from_database(
251                    NonZeroU64::new(db_pet.database_id as u64).unwrap(),
252                );
253                let pet_stats = comp::Stats::new(db_pet.name.to_owned(), pet_body);
254                Some((pet, pet_body, pet_stats))
255            } else {
256                warn!(
257                    "Failed to deserialize pet_id: {} for character_id {}",
258                    db_pet.database_id, char_id.0
259                );
260                None
261            }
262        })
263        .collect::<Vec<(comp::Pet, comp::Body, comp::Stats)>>();
264
265    let mut stmt = connection.prepare_cached(
266        "
267            SELECT  ability_sets
268            FROM    ability_set
269            WHERE   entity_id = ?1",
270    )?;
271
272    let ability_set_data = stmt.query_row([char_id.0], |row| {
273        Ok(AbilitySets {
274            entity_id: char_id.0,
275            ability_sets: row.get(0)?,
276        })
277    })?;
278
279    let (skill_set, skill_set_persistence_load_error) =
280        convert_skill_set_from_database(&skill_group_data);
281    let body = convert_body_from_database(&body_data.variant, &body_data.body_data)?;
282    let hardcore = convert_hardcore_from_database(character_data.hardcore)?;
283    Ok((
284        PersistedComponents {
285            body,
286            hardcore,
287            stats: convert_stats_from_database(character_data.alias, body),
288            skill_set,
289            inventory: convert_inventory_from_database_items(
290                character_containers.inventory_container_id,
291                &inventory_items,
292                character_containers.loadout_container_id,
293                &loadout_items,
294                character_containers.overflow_items_container_id,
295                &overflow_items_items,
296                &recipe_book_items,
297            )?,
298            waypoint: char_waypoint,
299            pets,
300            active_abilities: convert_active_abilities_from_database(&ability_set_data),
301            map_marker: char_map_marker,
302        },
303        UpdateCharacterMetadata {
304            skill_set_persistence_load_error,
305        },
306    ))
307}
308
309/// Loads a list of characters belonging to the player. This data is a small
310/// subset of the character's data, and is used to render the character and
311/// their level in the character list.
312///
313/// In the event that a join fails, for a character (i.e. they lack an entry for
314/// stats, body, etc...) the character is skipped, and no entry will be
315/// returned.
316pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> CharacterListResult {
317    let mut stmt = connection.prepare_cached(
318        "
319            SELECT  character_id,
320                    alias,
321                    waypoint,
322                    hardcore
323            FROM    character
324            WHERE   player_uuid = ?1
325            ORDER BY character_id",
326    )?;
327
328    let characters = stmt
329        .query_map([player_uuid_], |row| {
330            Ok(Character {
331                character_id: row.get(0)?,
332                alias: row.get(1)?,
333                player_uuid: player_uuid_.to_owned(),
334                waypoint: row.get(2)?,
335                hardcore: row.get(3)?,
336            })
337        })?
338        .map(|x| x.unwrap())
339        .collect::<Vec<Character>>();
340    drop(stmt);
341
342    characters
343        .iter()
344        .map(|character_data| {
345            let char = convert_character_from_database(character_data);
346
347            let mut stmt = connection.prepare_cached(
348                "
349                SELECT  body_id,
350                        variant,
351                        body_data
352                FROM    body
353                WHERE   body_id = ?1",
354            )?;
355            let db_body = stmt.query_row([char.id.map(|c| c.0)], |row| {
356                Ok(Body {
357                    body_id: row.get(0)?,
358                    variant: row.get(1)?,
359                    body_data: row.get(2)?,
360                })
361            })?;
362            drop(stmt);
363
364            let char_body = convert_body_from_database(&db_body.variant, &db_body.body_data)?;
365
366            let hardcore = convert_hardcore_from_database(character_data.hardcore)?;
367
368            let loadout_container_id = get_pseudo_container_id(
369                connection,
370                CharacterId(character_data.character_id),
371                LOADOUT_PSEUDO_CONTAINER_POSITION,
372            )?;
373
374            let loadout_items = load_items(connection, loadout_container_id)?;
375
376            let loadout =
377                convert_loadout_from_database_items(loadout_container_id, &loadout_items)?;
378
379            let recipe_book_container_id = get_pseudo_container_id(
380                connection,
381                CharacterId(character_data.character_id),
382                RECIPE_BOOK_PSEUDO_CONTAINER_POSITION,
383            )?;
384
385            let recipe_book_items = load_items(connection, recipe_book_container_id)?;
386
387            let recipe_book = convert_recipe_book_from_database_items(&recipe_book_items)?;
388
389            Ok(CharacterItem {
390                character: char,
391                body: char_body,
392                hardcore: hardcore.is_some(),
393                inventory: Inventory::with_loadout(loadout, char_body)
394                    .with_recipe_book(recipe_book),
395                location: character_data.waypoint.as_ref().cloned(),
396            })
397        })
398        .collect()
399}
400
401pub fn create_character(
402    uuid: &str,
403    character_alias: &str,
404    persisted_components: PersistedComponents,
405    transaction: &mut Transaction,
406) -> CharacterCreationResult {
407    check_character_limit(uuid, transaction)?;
408
409    let PersistedComponents {
410        body,
411        hardcore,
412        stats: _,
413        skill_set,
414        inventory,
415        waypoint,
416        pets: _,
417        active_abilities,
418        map_marker,
419    } = persisted_components;
420
421    // Fetch new entity IDs for character, inventory, loadout, overflow items, and
422    // recipe book
423    let mut new_entity_ids = get_new_entity_ids(transaction, |next_id| next_id + 5)?;
424
425    // Create pseudo-container items for character
426    let character_id = new_entity_ids.next().unwrap();
427    let inventory_container_id = new_entity_ids.next().unwrap();
428    let loadout_container_id = new_entity_ids.next().unwrap();
429    let overflow_items_container_id = new_entity_ids.next().unwrap();
430    let recipe_book_container_id = new_entity_ids.next().unwrap();
431
432    let pseudo_containers = vec![
433        Item {
434            stack_size: 1,
435            item_id: character_id,
436            parent_container_item_id: WORLD_PSEUDO_CONTAINER_ID,
437            item_definition_id: CHARACTER_PSEUDO_CONTAINER_DEF_ID.to_owned(),
438            position: character_id.to_string(),
439            properties: String::new(),
440        },
441        Item {
442            stack_size: 1,
443            item_id: inventory_container_id,
444            parent_container_item_id: character_id,
445            item_definition_id: INVENTORY_PSEUDO_CONTAINER_DEF_ID.to_owned(),
446            position: INVENTORY_PSEUDO_CONTAINER_POSITION.to_owned(),
447            properties: String::new(),
448        },
449        Item {
450            stack_size: 1,
451            item_id: loadout_container_id,
452            parent_container_item_id: character_id,
453            item_definition_id: LOADOUT_PSEUDO_CONTAINER_DEF_ID.to_owned(),
454            position: LOADOUT_PSEUDO_CONTAINER_POSITION.to_owned(),
455            properties: String::new(),
456        },
457        Item {
458            stack_size: 1,
459            item_id: overflow_items_container_id,
460            parent_container_item_id: character_id,
461            item_definition_id: OVERFLOW_ITEMS_PSEUDO_CONTAINER_DEF_ID.to_owned(),
462            position: OVERFLOW_ITEMS_PSEUDO_CONTAINER_POSITION.to_owned(),
463            properties: String::new(),
464        },
465        Item {
466            stack_size: 1,
467            item_id: recipe_book_container_id,
468            parent_container_item_id: character_id,
469            item_definition_id: RECIPE_BOOK_PSEUDO_CONTAINER_DEF_ID.to_owned(),
470            position: RECIPE_BOOK_PSEUDO_CONTAINER_POSITION.to_owned(),
471            properties: String::new(),
472        },
473    ];
474
475    let mut stmt = transaction.prepare_cached(
476        "
477        INSERT INTO item (item_id,
478                          parent_container_item_id,
479                          item_definition_id,
480                          stack_size,
481                          position,
482                          properties)
483        VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
484    )?;
485
486    for pseudo_container in pseudo_containers {
487        stmt.execute([
488            &pseudo_container.item_id as &dyn ToSql,
489            &pseudo_container.parent_container_item_id,
490            &pseudo_container.item_definition_id,
491            &pseudo_container.stack_size,
492            &pseudo_container.position,
493            &pseudo_container.properties,
494        ])?;
495    }
496    drop(stmt);
497
498    let mut stmt = transaction.prepare_cached(
499        "
500        INSERT INTO body (body_id,
501                          variant,
502                          body_data)
503        VALUES (?1, ?2, ?3)",
504    )?;
505
506    let (body_variant, body_json) = convert_body_to_database_json(&body)?;
507    stmt.execute([
508        &character_id as &dyn ToSql,
509        &body_variant.to_string(),
510        &body_json,
511    ])?;
512    drop(stmt);
513
514    let mut stmt = transaction.prepare_cached(
515        "
516        INSERT INTO character (character_id,
517                               player_uuid,
518                               alias,
519                               waypoint,
520                               hardcore)
521        VALUES (?1, ?2, ?3, ?4, ?5)",
522    )?;
523
524    stmt.execute([
525        &character_id as &dyn ToSql,
526        &uuid,
527        &character_alias,
528        &convert_waypoint_to_database_json(waypoint, map_marker),
529        &convert_hardcore_to_database(hardcore),
530    ])?;
531    drop(stmt);
532
533    let db_skill_groups =
534        convert_skill_groups_to_database(CharacterId(character_id), skill_set.skill_groups());
535
536    let mut stmt = transaction.prepare_cached(
537        "
538        INSERT INTO skill_group (entity_id,
539                                 skill_group_kind,
540                                 earned_exp,
541                                 spent_exp,
542                                 skills,
543                                 hash_val)
544        VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
545    )?;
546
547    for skill_group in db_skill_groups {
548        stmt.execute([
549            &character_id as &dyn ToSql,
550            &skill_group.skill_group_kind,
551            &skill_group.earned_exp,
552            &skill_group.spent_exp,
553            &skill_group.skills,
554            &skill_group.hash_val,
555        ])?;
556    }
557    drop(stmt);
558
559    let ability_sets =
560        convert_active_abilities_to_database(CharacterId(character_id), &active_abilities);
561
562    let mut stmt = transaction.prepare_cached(
563        "
564        INSERT INTO ability_set (entity_id,
565                                 ability_sets)
566        VALUES (?1, ?2)",
567    )?;
568
569    stmt.execute([
570        &character_id as &dyn ToSql,
571        &ability_sets.ability_sets as &dyn ToSql,
572    ])?;
573    drop(stmt);
574
575    // Insert default inventory and loadout item records
576    let mut inserts = Vec::new();
577
578    get_new_entity_ids(transaction, |mut next_id| {
579        let inserts_ = convert_items_to_database_items(
580            loadout_container_id,
581            &inventory,
582            inventory_container_id,
583            overflow_items_container_id,
584            recipe_book_container_id,
585            &mut next_id,
586        );
587        inserts = inserts_;
588        next_id
589    })?;
590
591    let mut stmt = transaction.prepare_cached(
592        "
593        INSERT INTO item (item_id,
594                          parent_container_item_id,
595                          item_definition_id,
596                          stack_size,
597                          position,
598                          properties)
599        VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
600    )?;
601
602    for item in inserts {
603        stmt.execute([
604            &item.model.item_id as &dyn ToSql,
605            &item.model.parent_container_item_id,
606            &item.model.item_definition_id,
607            &item.model.stack_size,
608            &item.model.position,
609            &item.model.properties,
610        ])?;
611    }
612    drop(stmt);
613
614    load_character_list(uuid, transaction).map(|list| (CharacterId(character_id), list))
615}
616
617pub fn edit_character(
618    editable_components: EditableComponents,
619    transaction: &mut Transaction,
620    character_id: CharacterId,
621    uuid: &str,
622    character_alias: &str,
623) -> CharacterCreationResult {
624    let (body,) = editable_components;
625    let mut char_list = load_character_list(uuid, transaction);
626
627    if let Ok(char_list) = &mut char_list {
628        if let Some(char) = char_list
629            .iter_mut()
630            .find(|c| c.character.id == Some(character_id))
631        {
632            if let (comp::Body::Humanoid(new), comp::Body::Humanoid(old)) = (body, char.body) {
633                if new.species != old.species || new.body_type != old.body_type {
634                    warn!(
635                        "Character edit rejected due to failed validation - Character ID: {} \
636                         Alias: {}",
637                        character_id.0, character_alias
638                    );
639                    return Err(PersistenceError::CharacterDataError);
640                } else {
641                    char.body = body;
642                }
643            }
644        }
645    }
646
647    let mut stmt = transaction
648        .prepare_cached("UPDATE body SET variant = ?1, body_data = ?2 WHERE body_id = ?3")?;
649
650    let (body_variant, body_data) = convert_body_to_database_json(&body)?;
651    stmt.execute([
652        &body_variant.to_string(),
653        &body_data,
654        &character_id.0 as &dyn ToSql,
655    ])?;
656    drop(stmt);
657
658    let mut stmt =
659        transaction.prepare_cached("UPDATE character SET alias = ?1 WHERE character_id = ?2")?;
660
661    stmt.execute([&character_alias, &character_id.0 as &dyn ToSql])?;
662    drop(stmt);
663
664    char_list.map(|list| (character_id, list))
665}
666
667/// Permanently deletes a character
668pub fn delete_character(
669    requesting_player_uuid: &str,
670    char_id: CharacterId,
671    transaction: &mut Transaction,
672) -> Result<(), PersistenceError> {
673    debug!(?requesting_player_uuid, ?char_id, "Deleting character");
674
675    let mut stmt = transaction.prepare_cached(
676        "
677        SELECT  COUNT(1)
678        FROM    character
679        WHERE   character_id = ?1
680        AND     player_uuid = ?2",
681    )?;
682
683    let result = stmt.query_row([&char_id.0 as &dyn ToSql, &requesting_player_uuid], |row| {
684        let y: i64 = row.get(0)?;
685        Ok(y)
686    })?;
687    drop(stmt);
688
689    if result != 1 {
690        // The character does not exist, or does not belong to the requesting player so
691        // silently drop the request.
692        return Ok(());
693    }
694
695    // Delete skill groups
696    let mut stmt = transaction.prepare_cached(
697        "
698        DELETE
699        FROM    skill_group
700        WHERE   entity_id = ?1",
701    )?;
702
703    stmt.execute([&char_id.0])?;
704    drop(stmt);
705
706    let pet_ids = get_pet_ids(char_id, transaction)?
707        .iter()
708        .map(|x| Value::from(*x))
709        .collect::<Vec<Value>>();
710    if !pet_ids.is_empty() {
711        delete_pets(transaction, char_id, Rc::new(pet_ids))?;
712    }
713
714    // Delete ability sets
715    let mut stmt = transaction.prepare_cached(
716        "
717        DELETE
718        FROM    ability_set
719        WHERE   entity_id = ?1",
720    )?;
721
722    stmt.execute([&char_id.0])?;
723    drop(stmt);
724
725    // Delete character
726    let mut stmt = transaction.prepare_cached(
727        "
728        DELETE
729        FROM    character
730        WHERE   character_id = ?1",
731    )?;
732
733    stmt.execute([&char_id.0])?;
734    drop(stmt);
735
736    // Delete body
737    let mut stmt = transaction.prepare_cached(
738        "
739        DELETE
740        FROM    body
741        WHERE   body_id = ?1",
742    )?;
743
744    stmt.execute([&char_id.0])?;
745    drop(stmt);
746
747    // Delete all items, recursively walking all containers starting from the
748    // "character" pseudo-container that is the root for all items owned by
749    // a character.
750    let mut stmt = transaction.prepare_cached(
751        "
752        WITH RECURSIVE
753        parents AS (
754            SELECT  item_id
755            FROM    item
756            WHERE   item.item_id = ?1 -- Item with character id is the character pseudo-container
757            UNION ALL
758            SELECT  item.item_id
759            FROM    item,
760                    parents
761            WHERE   item.parent_container_item_id = parents.item_id
762        )
763        DELETE
764        FROM    item
765        WHERE   EXISTS (SELECT 1 FROM parents WHERE parents.item_id = item.item_id)",
766    )?;
767
768    let deleted_item_count = stmt.execute([&char_id.0])?;
769    drop(stmt);
770
771    if deleted_item_count < 3 {
772        return Err(PersistenceError::OtherError(format!(
773            "Error deleting from item table for char_id {} (expected at least 3 deletions, found \
774             {})",
775            char_id.0, deleted_item_count
776        )));
777    }
778
779    Ok(())
780}
781
782/// Before creating a character, we ensure that the limit on the number of
783/// characters has not been exceeded
784pub fn check_character_limit(
785    uuid: &str,
786    transaction: &mut Transaction,
787) -> Result<(), PersistenceError> {
788    let mut stmt = transaction.prepare_cached(
789        "
790        SELECT  COUNT(1)
791        FROM    character
792        WHERE   player_uuid = ?1",
793    )?;
794
795    #[expect(clippy::needless_question_mark)]
796    let character_count: i64 = stmt.query_row([&uuid], |row| Ok(row.get(0)?))?;
797    drop(stmt);
798
799    if character_count < MAX_CHARACTERS_PER_PLAYER as i64 {
800        Ok(())
801    } else {
802        Err(PersistenceError::CharacterLimitReached)
803    }
804}
805
806/// NOTE: This relies heavily on serializability to work correctly.
807///
808/// The count function takes the starting entity id, and returns the desired
809/// count of new entity IDs.
810///
811/// These are then inserted into the entities table.
812fn get_new_entity_ids(
813    transaction: &mut Transaction,
814    mut max: impl FnMut(i64) -> i64,
815) -> Result<Range<EntityId>, PersistenceError> {
816    // The sqlite_sequence table is used here to avoid reusing entity IDs for
817    // deleted entities. This table always contains the highest used ID for
818    // each AUTOINCREMENT column in a SQLite database.
819    let mut stmt = transaction.prepare_cached(
820        "
821        SELECT  seq + 1 AS entity_id
822        FROM    sqlite_sequence
823        WHERE   name = 'entity'",
824    )?;
825
826    #[expect(clippy::needless_question_mark)]
827    let next_entity_id = stmt.query_row([], |row| Ok(row.get(0)?))?;
828    let max_entity_id = max(next_entity_id);
829
830    // Create a new range of IDs and insert them into the entity table
831    let new_ids: Range<EntityId> = next_entity_id..max_entity_id;
832
833    let mut stmt = transaction.prepare_cached("INSERT INTO entity (entity_id) VALUES (?1)")?;
834
835    // SQLite has no bulk insert
836    for i in new_ids.clone() {
837        stmt.execute([i])?;
838    }
839
840    trace!(
841        "Created {} new persistence entity_ids: {}",
842        new_ids.end - new_ids.start,
843        new_ids
844            .clone()
845            .map(|x| x.to_string())
846            .collect::<Vec<String>>()
847            .join(", ")
848    );
849    Ok(new_ids)
850}
851
852/// Fetches the pseudo_container IDs for a character
853fn get_pseudo_containers(
854    connection: &Connection,
855    character_id: CharacterId,
856) -> Result<CharacterContainers, PersistenceError> {
857    let character_containers = CharacterContainers {
858        loadout_container_id: get_pseudo_container_id(
859            connection,
860            character_id,
861            LOADOUT_PSEUDO_CONTAINER_POSITION,
862        )?,
863        inventory_container_id: get_pseudo_container_id(
864            connection,
865            character_id,
866            INVENTORY_PSEUDO_CONTAINER_POSITION,
867        )?,
868        overflow_items_container_id: get_pseudo_container_id(
869            connection,
870            character_id,
871            OVERFLOW_ITEMS_PSEUDO_CONTAINER_POSITION,
872        )?,
873        recipe_book_container_id: get_pseudo_container_id(
874            connection,
875            character_id,
876            RECIPE_BOOK_PSEUDO_CONTAINER_POSITION,
877        )?,
878    };
879
880    Ok(character_containers)
881}
882
883fn get_pseudo_container_id(
884    connection: &Connection,
885    character_id: CharacterId,
886    pseudo_container_position: &str,
887) -> Result<EntityId, PersistenceError> {
888    let mut stmt = connection.prepare_cached(
889        "
890        SELECT  item_id
891        FROM    item
892        WHERE   parent_container_item_id = ?1
893        AND     position = ?2",
894    )?;
895
896    #[expect(clippy::needless_question_mark)]
897    let res = stmt.query_row(
898        [
899            character_id.0.to_string(),
900            pseudo_container_position.to_string(),
901        ],
902        |row| Ok(row.get(0)?),
903    );
904
905    match res {
906        Ok(id) => Ok(id),
907        Err(e) => {
908            error!(
909                ?e,
910                ?character_id,
911                ?pseudo_container_position,
912                "Failed to retrieve pseudo container ID"
913            );
914            Err(DatabaseError(e))
915        },
916    }
917}
918
919/// Stores new pets in the database, and removes pets from the database that the
920/// player no longer has. Currently there are no actual updates to pet data
921/// since we don't store any updatable data about pets in the database.
922fn update_pets(
923    char_id: CharacterId,
924    pets: Vec<PetPersistenceData>,
925    transaction: &mut Transaction,
926) -> Result<(), PersistenceError> {
927    debug!("Updating {} pets for character {}", pets.len(), char_id.0);
928
929    let db_pets = get_pet_ids(char_id, transaction)?;
930    if !db_pets.is_empty() {
931        let dead_pet_ids = Rc::new(
932            db_pets
933                .iter()
934                .filter(|pet_id| {
935                    !pets.iter().any(|(pet, _, _)| {
936                        pet.get_database_id()
937                            .load()
938                            .is_some_and(|x| x.get() == **pet_id as u64)
939                    })
940                })
941                .map(|x| Value::from(*x))
942                .collect::<Vec<Value>>(),
943        );
944
945        if !dead_pet_ids.is_empty() {
946            delete_pets(transaction, char_id, dead_pet_ids)?;
947        }
948    }
949
950    for (pet, body, stats) in pets
951        .iter()
952        .filter(|(pet, _, _)| pet.get_database_id().load().is_none())
953    {
954        let pet_entity_id = get_new_entity_ids(transaction, |next_id| next_id + 1)?.start;
955
956        let (body_variant, body_json) = convert_body_to_database_json(body)?;
957
958        #[rustfmt::skip]
959        let mut stmt = transaction.prepare_cached("
960            INSERT
961            INTO    body (
962                    body_id,
963                    variant,
964                    body_data)
965            VALUES  (?1, ?2, ?3)"
966        )?;
967
968        stmt.execute([
969            &pet_entity_id as &dyn ToSql,
970            &body_variant.to_string(),
971            &body_json,
972        ])?;
973
974        #[rustfmt::skip]
975        let mut stmt = transaction.prepare_cached("
976            INSERT
977            INTO    pet (
978                    pet_id,
979                    character_id,
980                    name)
981            VALUES  (?1, ?2, ?3)",
982        )?;
983
984        stmt.execute([&pet_entity_id as &dyn ToSql, &char_id.0, &stats.name])?;
985        drop(stmt);
986
987        pet.get_database_id()
988            .store(NonZeroU64::new(pet_entity_id as u64));
989    }
990
991    Ok(())
992}
993
994fn get_pet_ids(
995    char_id: CharacterId,
996    transaction: &mut Transaction,
997) -> Result<Vec<i64>, PersistenceError> {
998    #[rustfmt::skip]
999        let mut stmt = transaction.prepare_cached("
1000        SELECT  pet_id
1001        FROM    pet
1002        WHERE   character_id = ?1
1003    ")?;
1004
1005    #[expect(clippy::needless_question_mark)]
1006    let db_pets = stmt
1007        .query_map([&char_id.0], |row| Ok(row.get(0)?))?
1008        .map(|x| x.unwrap())
1009        .collect::<Vec<i64>>();
1010    drop(stmt);
1011    Ok(db_pets)
1012}
1013
1014fn delete_pets(
1015    transaction: &mut Transaction,
1016    char_id: CharacterId,
1017    pet_ids: Rc<Vec<Value>>,
1018) -> Result<(), PersistenceError> {
1019    #[rustfmt::skip]
1020    let mut stmt = transaction.prepare_cached("
1021            DELETE
1022            FROM    pet
1023            WHERE   pet_id IN rarray(?1)"
1024    )?;
1025
1026    let delete_count = stmt.execute([&pet_ids])?;
1027    drop(stmt);
1028    debug!(
1029        "Deleted {} pets for character id {}",
1030        delete_count, char_id.0
1031    );
1032
1033    #[rustfmt::skip]
1034    let mut stmt = transaction.prepare_cached("
1035            DELETE
1036            FROM    body
1037            WHERE   body_id IN rarray(?1)"
1038    )?;
1039
1040    let delete_count = stmt.execute([&pet_ids])?;
1041    debug!(
1042        "Deleted {} pet bodies for character id {}",
1043        delete_count, char_id.0
1044    );
1045
1046    Ok(())
1047}
1048
1049pub fn update(
1050    char_id: CharacterId,
1051    char_skill_set: comp::SkillSet,
1052    inventory: Inventory,
1053    pets: Vec<PetPersistenceData>,
1054    char_waypoint: Option<comp::Waypoint>,
1055    active_abilities: comp::ability::ActiveAbilities,
1056    map_marker: Option<comp::MapMarker>,
1057    transaction: &mut Transaction,
1058) -> Result<(), PersistenceError> {
1059    // Run pet persistence
1060    update_pets(char_id, pets, transaction)?;
1061
1062    let pseudo_containers = get_pseudo_containers(transaction, char_id)?;
1063    let mut upserts = Vec::new();
1064    // First, get all the entity IDs for any new items, and identify which
1065    // slots to upsert and which ones to delete.
1066    get_new_entity_ids(transaction, |mut next_id| {
1067        let upserts_ = convert_items_to_database_items(
1068            pseudo_containers.loadout_container_id,
1069            &inventory,
1070            pseudo_containers.inventory_container_id,
1071            pseudo_containers.overflow_items_container_id,
1072            pseudo_containers.recipe_book_container_id,
1073            &mut next_id,
1074        );
1075        upserts = upserts_;
1076        next_id
1077    })?;
1078
1079    // Next, delete any slots we aren't upserting.
1080    trace!("Deleting items for character_id {}", char_id.0);
1081    let mut existing_item_ids: Vec<_> = vec![
1082        Value::from(pseudo_containers.inventory_container_id),
1083        Value::from(pseudo_containers.loadout_container_id),
1084        Value::from(pseudo_containers.overflow_items_container_id),
1085        Value::from(pseudo_containers.recipe_book_container_id),
1086    ];
1087    for it in load_items(transaction, pseudo_containers.inventory_container_id)? {
1088        existing_item_ids.push(Value::from(it.item_id));
1089    }
1090    for it in load_items(transaction, pseudo_containers.loadout_container_id)? {
1091        existing_item_ids.push(Value::from(it.item_id));
1092    }
1093    for it in load_items(transaction, pseudo_containers.overflow_items_container_id)? {
1094        existing_item_ids.push(Value::from(it.item_id));
1095    }
1096    for it in load_items(transaction, pseudo_containers.recipe_book_container_id)? {
1097        existing_item_ids.push(Value::from(it.item_id));
1098    }
1099
1100    let non_upserted_items = upserts
1101        .iter()
1102        .map(|item_pair| Value::from(item_pair.model.item_id))
1103        .collect::<Vec<Value>>();
1104
1105    let mut stmt = transaction.prepare_cached(
1106        "
1107        DELETE
1108        FROM    item
1109        WHERE   parent_container_item_id
1110        IN      rarray(?1)
1111        AND     item_id NOT IN rarray(?2)",
1112    )?;
1113    let delete_count = stmt.execute([Rc::new(existing_item_ids), Rc::new(non_upserted_items)])?;
1114    trace!("Deleted {} items", delete_count);
1115
1116    // Upsert items
1117    let expected_upsert_count = upserts.len();
1118    if expected_upsert_count > 0 {
1119        let (upserted_items, _): (Vec<_>, Vec<_>) = upserts
1120            .into_iter()
1121            .map(|model_pair| {
1122                debug_assert_eq!(
1123                    model_pair.model.item_id,
1124                    model_pair.comp.load().unwrap().get() as i64
1125                );
1126                (model_pair.model, model_pair.comp)
1127            })
1128            .unzip();
1129        trace!(
1130            "Upserting items {:?} for character_id {}",
1131            upserted_items, char_id.0
1132        );
1133
1134        // When moving inventory items around, foreign key constraints on
1135        // `parent_container_item_id` can be temporarily violated by one
1136        // upsert, but restored by another upsert. Deferred constraints
1137        // allow SQLite to check this when committing the transaction.
1138        // The `defer_foreign_keys` pragma treats the foreign key
1139        // constraints as deferred for the next transaction (it turns itself
1140        // off at the commit boundary). https://sqlite.org/foreignkeys.html#fk_deferred
1141        transaction.pragma_update(None, "defer_foreign_keys", "ON")?;
1142
1143        let mut stmt = transaction.prepare_cached(
1144            "
1145            REPLACE
1146            INTO    item (item_id,
1147                          parent_container_item_id,
1148                          item_definition_id,
1149                          stack_size,
1150                          position,
1151                          properties)
1152            VALUES  (?1, ?2, ?3, ?4, ?5, ?6)",
1153        )?;
1154
1155        for item in upserted_items.iter() {
1156            stmt.execute([
1157                &item.item_id as &dyn ToSql,
1158                &item.parent_container_item_id,
1159                &item.item_definition_id,
1160                &item.stack_size,
1161                &item.position,
1162                &item.properties,
1163            ])?;
1164        }
1165    }
1166
1167    let db_skill_groups = convert_skill_groups_to_database(char_id, char_skill_set.skill_groups());
1168
1169    let mut stmt = transaction.prepare_cached(
1170        "
1171        REPLACE
1172        INTO    skill_group (entity_id,
1173                             skill_group_kind,
1174                             earned_exp,
1175                             spent_exp,
1176                             skills,
1177                             hash_val)
1178        VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1179    )?;
1180
1181    for skill_group in db_skill_groups {
1182        stmt.execute([
1183            &skill_group.entity_id as &dyn ToSql,
1184            &skill_group.skill_group_kind,
1185            &skill_group.earned_exp,
1186            &skill_group.spent_exp,
1187            &skill_group.skills,
1188            &skill_group.hash_val,
1189        ])?;
1190    }
1191
1192    let db_waypoint = convert_waypoint_to_database_json(char_waypoint, map_marker);
1193
1194    let mut stmt = transaction.prepare_cached(
1195        "
1196        UPDATE  character
1197        SET     waypoint = ?1
1198        WHERE   character_id = ?2
1199    ",
1200    )?;
1201
1202    let waypoint_count = stmt.execute([&db_waypoint as &dyn ToSql, &char_id.0])?;
1203
1204    if waypoint_count != 1 {
1205        return Err(PersistenceError::OtherError(format!(
1206            "Error updating character table for char_id {}",
1207            char_id.0
1208        )));
1209    }
1210
1211    let ability_sets = convert_active_abilities_to_database(char_id, &active_abilities);
1212
1213    let mut stmt = transaction.prepare_cached(
1214        "
1215        UPDATE  ability_set
1216        SET     ability_sets = ?1
1217        WHERE   entity_id = ?2
1218    ",
1219    )?;
1220
1221    let ability_sets_count = stmt.execute([
1222        &ability_sets.ability_sets as &dyn ToSql,
1223        &char_id.0 as &dyn ToSql,
1224    ])?;
1225
1226    if ability_sets_count != 1 {
1227        return Err(PersistenceError::OtherError(format!(
1228            "Error updating ability_set table for char_id {}",
1229            char_id.0,
1230        )));
1231    }
1232
1233    Ok(())
1234}