1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use crate::persistence::{character_updater::CharacterUpdater, PersistedComponents};
use common::{
    character::CharacterId,
    comp::{
        inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
        Waypoint, BASE_ABILITY_LIMIT,
    },
};
use specs::{Entity, WriteExpect};

const VALID_STARTER_ITEMS: &[[Option<&str>; 2]] = &[
    [None, None], // Not used with an unmodified client but should still be allowed (zesterer)
    [Some("common.items.weapons.hammer.starter_hammer"), None],
    [Some("common.items.weapons.bow.starter"), None],
    [Some("common.items.weapons.axe.starter_axe"), None],
    [Some("common.items.weapons.staff.starter_staff"), None],
    [Some("common.items.weapons.sword.starter"), None],
    [
        Some("common.items.weapons.sword_1h.starter"),
        Some("common.items.weapons.sword_1h.starter"),
    ],
];

#[derive(Debug)]
pub enum CreationError {
    InvalidWeapon,
    InvalidBody,
}

pub fn create_character(
    entity: Entity,
    player_uuid: String,
    character_alias: String,
    character_mainhand: Option<String>,
    character_offhand: Option<String>,
    body: Body,
    hardcore: bool,
    character_updater: &mut WriteExpect<'_, CharacterUpdater>,
    waypoint: Option<Waypoint>,
) -> Result<(), CreationError> {
    // quick fix whitelist validation for now; eventually replace the
    // `Option<String>` with an index into a server-provided list of starter
    // items, and replace `comp::body::Body` with `comp::body::humanoid::Body`
    // throughout the messages involved
    if !matches!(body, Body::Humanoid(_)) {
        return Err(CreationError::InvalidBody);
    }
    if !VALID_STARTER_ITEMS.contains(&[character_mainhand.as_deref(), character_offhand.as_deref()])
    {
        return Err(CreationError::InvalidWeapon);
    };
    // The client sends None if a weapon hand is empty
    let loadout = LoadoutBuilder::empty()
        .defaults()
        .active_mainhand(character_mainhand.map(|x| Item::new_from_asset_expect(&x)))
        .active_offhand(character_offhand.map(|x| Item::new_from_asset_expect(&x)))
        .build();
    let mut inventory = Inventory::with_loadout_humanoid(loadout);

    let stats = Stats::new(character_alias.to_string(), body);
    let skill_set = SkillSet::default();
    // Default items for new characters
    inventory
        .push(Item::new_from_asset_expect(
            "common.items.consumable.potion_minor",
        ))
        .expect("Inventory has at least 2 slots left!");
    inventory
        .push(Item::new_from_asset_expect("common.items.food.cheese"))
        .expect("Inventory has at least 1 slot left!");
    inventory
        .push_recipe_group(Item::new_from_asset_expect("common.items.recipes.default"))
        .expect("New inventory should not already have default recipe group.");

    let map_marker = None;

    character_updater.create_character(entity, player_uuid, character_alias, PersistedComponents {
        body,
        hardcore: hardcore.then_some(common::comp::Hardcore),
        stats,
        skill_set,
        inventory,
        waypoint,
        pets: Vec::new(),
        active_abilities: common::comp::ActiveAbilities::default_limited(BASE_ABILITY_LIMIT),
        map_marker,
    });
    Ok(())
}

pub fn edit_character(
    entity: Entity,
    player_uuid: String,
    id: CharacterId,
    character_alias: String,
    body: Body,
    character_updater: &mut WriteExpect<'_, CharacterUpdater>,
) -> Result<(), CreationError> {
    if !matches!(body, Body::Humanoid(_)) {
        return Err(CreationError::InvalidBody);
    }

    character_updater.edit_character(entity, player_uuid, id, character_alias, (body,));
    Ok(())
}

// Error handling
impl core::fmt::Display for CreationError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            CreationError::InvalidWeapon => write!(
                f,
                "Invalid weapon.\nServer and client might be partially incompatible."
            ),
            CreationError::InvalidBody => write!(
                f,
                "Invalid Body.\nServer and client might be partially incompatible"
            ),
        }
    }
}