veloren_server/
character_creator.rs

1use crate::persistence::{PersistedComponents, character_updater::CharacterUpdater};
2use common::{
3    character::CharacterId,
4    comp::{
5        BASE_ABILITY_LIMIT, Body, Inventory, Item, SkillSet, Stats, Waypoint,
6        inventory::loadout_builder::LoadoutBuilder,
7    },
8};
9use specs::{Entity, WriteExpect};
10
11const VALID_STARTER_ITEMS: &[[Option<&str>; 2]] = &[
12    [None, None], // Not used with an unmodified client but should still be allowed (zesterer)
13    [Some("common.items.weapons.hammer.starter_hammer"), None],
14    [Some("common.items.weapons.bow.starter"), None],
15    [Some("common.items.weapons.axe.starter_axe"), None],
16    [Some("common.items.weapons.staff.starter_staff"), None],
17    [Some("common.items.weapons.sword.starter"), None],
18    [
19        Some("common.items.weapons.sword_1h.starter"),
20        Some("common.items.weapons.sword_1h.starter"),
21    ],
22];
23
24#[derive(Debug)]
25pub enum CreationError {
26    InvalidWeapon,
27    InvalidBody,
28}
29
30pub fn create_character(
31    entity: Entity,
32    player_uuid: String,
33    character_alias: String,
34    character_mainhand: Option<String>,
35    character_offhand: Option<String>,
36    body: Body,
37    hardcore: bool,
38    character_updater: &mut WriteExpect<'_, CharacterUpdater>,
39    waypoint: Option<Waypoint>,
40) -> Result<(), CreationError> {
41    // quick fix whitelist validation for now; eventually replace the
42    // `Option<String>` with an index into a server-provided list of starter
43    // items, and replace `comp::body::Body` with `comp::body::humanoid::Body`
44    // throughout the messages involved
45    if !matches!(body, Body::Humanoid(_)) {
46        return Err(CreationError::InvalidBody);
47    }
48    if !VALID_STARTER_ITEMS.contains(&[character_mainhand.as_deref(), character_offhand.as_deref()])
49    {
50        return Err(CreationError::InvalidWeapon);
51    };
52    // The client sends None if a weapon hand is empty
53    let loadout = LoadoutBuilder::empty()
54        .defaults()
55        .active_mainhand(character_mainhand.map(|x| Item::new_from_asset_expect(&x)))
56        .active_offhand(character_offhand.map(|x| Item::new_from_asset_expect(&x)))
57        .build();
58    let mut inventory = Inventory::with_loadout_humanoid(loadout);
59
60    let stats = Stats::new(character_alias.to_string(), body);
61    let skill_set = SkillSet::default();
62    // Default items for new characters
63    inventory
64        .push(Item::new_from_asset_expect(
65            "common.items.consumable.potion_minor",
66        ))
67        .expect("Inventory has at least 2 slots left!");
68    inventory
69        .push(Item::new_from_asset_expect("common.items.food.cheese"))
70        .expect("Inventory has at least 1 slot left!");
71    inventory
72        .push_recipe_group(Item::new_from_asset_expect("common.items.recipes.default"))
73        .expect("New inventory should not already have default recipe group.");
74
75    let map_marker = None;
76
77    character_updater.create_character(entity, player_uuid, character_alias, PersistedComponents {
78        body,
79        hardcore: hardcore.then_some(common::comp::Hardcore),
80        stats,
81        skill_set,
82        inventory,
83        waypoint,
84        pets: Vec::new(),
85        active_abilities: common::comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT),
86        map_marker,
87    });
88    Ok(())
89}
90
91pub fn edit_character(
92    entity: Entity,
93    player_uuid: String,
94    id: CharacterId,
95    character_alias: String,
96    body: Body,
97    character_updater: &mut WriteExpect<'_, CharacterUpdater>,
98) -> Result<(), CreationError> {
99    if !matches!(body, Body::Humanoid(_)) {
100        return Err(CreationError::InvalidBody);
101    }
102
103    character_updater.edit_character(entity, player_uuid, id, character_alias, (body,));
104    Ok(())
105}
106
107// Error handling
108impl core::fmt::Display for CreationError {
109    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110        match self {
111            CreationError::InvalidWeapon => write!(
112                f,
113                "Invalid weapon.\nServer and client might be partially incompatible."
114            ),
115            CreationError::InvalidBody => write!(
116                f,
117                "Invalid Body.\nServer and client might be partially incompatible"
118            ),
119        }
120    }
121}