Skip to main content

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