1extern 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
40mod 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
69pub 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
127pub 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 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 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
318pub 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 let mut new_entity_ids = get_new_entity_ids(transaction, |next_id| next_id + 5)?;
433
434 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 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
683pub 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 return Ok(());
709 }
710
711 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 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 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 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 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
798pub 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
822fn get_new_entity_ids(
829 transaction: &mut Transaction,
830 mut max: impl FnMut(i64) -> i64,
831) -> Result<Range<EntityId>, PersistenceError> {
832 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 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 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
868fn 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
935fn 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 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 update_pets(char_id, pets, transaction)?;
1079
1080 let pseudo_containers = get_pseudo_containers(transaction, char_id)?;
1081 let mut upserts = Vec::new();
1082 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 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 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 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}