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