1extern 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
40mod 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
67pub 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
143pub 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 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 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
320pub 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 let mut new_entity_ids = get_new_entity_ids(transaction, |next_id| next_id + 5)?;
441
442 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 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
688pub 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 return Ok(());
714 }
715
716 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 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 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 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 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
803pub 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
827fn get_new_entity_ids(
834 transaction: &mut Transaction,
835 mut max: impl FnMut(i64) -> i64,
836) -> Result<Range<EntityId>, PersistenceError> {
837 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 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 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
873fn 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
940fn 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 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 update_pets(char_id, pets, transaction)?;
1084
1085 let pseudo_containers = get_pseudo_containers(transaction, char_id)?;
1086 let mut upserts = Vec::new();
1087 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 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 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 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}