Skip to main content

veloren_server/
character_creator.rs

1use crate::persistence::{PersistedComponents, character_updater::CharacterUpdater};
2use common::{
3    character::CharacterId,
4    comp::{
5        BASE_ABILITY_LIMIT, Body, Content, 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)]
25#[expect(clippy::enum_variant_names)]
26pub enum CreationError {
27    InvalidWeapon,
28    InvalidBody,
29    InvalidAlias,
30}
31
32pub fn create_character(
33    entity: Entity,
34    player_uuid: String,
35    character_alias: String,
36    character_mainhand: Option<String>,
37    character_offhand: Option<String>,
38    body: Body,
39    hardcore: bool,
40    character_updater: &mut WriteExpect<'_, CharacterUpdater>,
41    waypoint: Option<Waypoint>,
42) -> Result<(), CreationError> {
43    if !common::character::verify_character_name(&character_alias) {
44        return Err(CreationError::InvalidAlias);
45    }
46    // quick fix whitelist validation for now; eventually replace the
47    // `Option<String>` with an index into a server-provided list of starter
48    // items, and replace `comp::body::Body` with `comp::body::humanoid::Body`
49    // throughout the messages involved
50    if !matches!(body, Body::Humanoid(_)) {
51        return Err(CreationError::InvalidBody);
52    }
53    if !VALID_STARTER_ITEMS.contains(&[character_mainhand.as_deref(), character_offhand.as_deref()])
54    {
55        return Err(CreationError::InvalidWeapon);
56    };
57    // The client sends None if a weapon hand is empty
58    let loadout = LoadoutBuilder::empty()
59        .defaults()
60        .active_mainhand(character_mainhand.map(|x| Item::new_from_asset_expect(&x)))
61        .active_offhand(character_offhand.map(|x| Item::new_from_asset_expect(&x)))
62        .build();
63    let mut inventory = Inventory::with_loadout_humanoid(loadout);
64
65    let stats = Stats::new(Content::Plain(character_alias.to_string()), body);
66    let skill_set = SkillSet::default();
67    // Default items for new characters
68    inventory
69        .push(Item::new_from_asset_expect(
70            "common.items.consumable.potion_minor",
71        ))
72        .expect("Inventory has at least 2 slots left!");
73    inventory
74        .push(Item::new_from_asset_expect("common.items.food.cheese"))
75        .expect("Inventory has at least 1 slot left!");
76    inventory
77        .push_recipe_group(Item::new_from_asset_expect("common.items.recipes.default"))
78        .expect("New inventory should not already have default recipe group.");
79
80    let map_marker = None;
81
82    character_updater.create_character(entity, player_uuid, character_alias, PersistedComponents {
83        body,
84        hardcore: hardcore.then_some(common::comp::Hardcore),
85        stats,
86        skill_set,
87        inventory,
88        waypoint,
89        pets: Vec::new(),
90        active_abilities: common::comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT),
91        map_marker,
92    });
93    Ok(())
94}
95
96pub fn edit_character(
97    entity: Entity,
98    player_uuid: String,
99    id: CharacterId,
100    character_alias: String,
101    body: Body,
102    character_updater: &mut WriteExpect<'_, CharacterUpdater>,
103) -> Result<(), CreationError> {
104    if !common::character::verify_character_name(&character_alias) {
105        return Err(CreationError::InvalidAlias);
106    }
107
108    if !matches!(body, Body::Humanoid(_)) {
109        return Err(CreationError::InvalidBody);
110    }
111
112    character_updater.edit_character(
113        entity,
114        player_uuid,
115        id,
116        Some(character_alias),
117        (body,),
118        None,
119    );
120    Ok(())
121}
122
123// Error handling
124impl core::fmt::Display for CreationError {
125    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126        match self {
127            CreationError::InvalidWeapon => write!(
128                f,
129                "Invalid weapon.\nServer and client might be partially incompatible."
130            ),
131            CreationError::InvalidBody => write!(
132                f,
133                "Invalid Body.\nServer and client might be partially incompatible"
134            ),
135            CreationError::InvalidAlias => write!(
136                f,
137                "Invalid Alias.\nServer and client might be partially incompatible"
138            ),
139        }
140    }
141}