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