Merge branch 'sam/inventory-overflow' into 'master'

Inventory Overflow Slots for Persistence

See merge request veloren/veloren!4021
This commit is contained in:
Samuel Keiffer 2024-02-03 02:15:11 +00:00
commit aef9d27f64
17 changed files with 477 additions and 108 deletions

View File

@ -1119,6 +1119,13 @@ impl Client {
pub fn swap_slots(&mut self, a: Slot, b: Slot) {
match (a, b) {
(Slot::Overflow(o), Slot::Inventory(inv))
| (Slot::Inventory(inv), Slot::Overflow(o)) => {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::OverflowMove(o, inv),
)));
},
(Slot::Overflow(_), _) | (_, Slot::Overflow(_)) => {},
(Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
),
@ -1138,6 +1145,9 @@ impl Client {
Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::InventoryEvent(InventoryEvent::Drop(inv)),
)),
Slot::Overflow(o) => self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::InventoryEvent(InventoryEvent::OverflowDrop(o)),
)),
}
}
@ -1165,6 +1175,7 @@ impl Client {
pub fn split_swap_slots(&mut self, a: Slot, b: Slot) {
match (a, b) {
(Slot::Overflow(_), _) | (_, Slot::Overflow(_)) => {},
(Slot::Equip(equip), slot) | (slot, Slot::Equip(equip)) => self.control_action(
ControlAction::InventoryAction(InventoryAction::Swap(equip, slot)),
),
@ -1184,6 +1195,9 @@ impl Client {
Slot::Inventory(inv) => self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::InventoryEvent(InventoryEvent::SplitDrop(inv)),
)),
Slot::Overflow(o) => self.send_msg(ClientGeneral::ControlEvent(
ControlEvent::InventoryEvent(InventoryEvent::OverflowSplitDrop(o)),
)),
}
}
@ -1393,6 +1407,7 @@ impl Client {
if let Some(item) = match item {
Slot::Equip(equip_slot) => inv.equipped(equip_slot),
Slot::Inventory(invslot) => inv.get(invslot),
Slot::Overflow(_) => None,
} {
item.has_durability()
} else {

View File

@ -31,6 +31,9 @@ pub enum InventoryEvent {
craft_event: CraftEvent,
craft_sprite: Option<VolumePos>,
},
OverflowMove(usize, InvSlotId),
OverflowDrop(usize),
OverflowSplitDrop(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -86,6 +89,11 @@ impl From<InventoryEvent> for InventoryManip {
craft_event,
craft_sprite,
},
InventoryEvent::OverflowMove(o, inv) => {
Self::Swap(Slot::Overflow(o), Slot::Inventory(inv))
},
InventoryEvent::OverflowDrop(o) => Self::Drop(Slot::Overflow(o)),
InventoryEvent::OverflowSplitDrop(o) => Self::SplitDrop(Slot::Overflow(o)),
}
}
}

View File

@ -1439,6 +1439,30 @@ impl Item {
self.update_item_state(ability_map, msm);
}
/// If an item is stackable and has an amount greater than 1, creates a new
/// item with half the amount (rounded down), and decreases the amount of
/// the original item by the same quantity.
#[must_use = "Returned items will be lost if not used"]
pub fn take_half(
&mut self,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Option<Item> {
if self.is_stackable() && self.amount() > 1 {
let mut return_item = self.duplicate(ability_map, msm);
let returning_amount = self.amount() / 2;
self.decrease_amount(returning_amount).ok()?;
return_item.set_amount(returning_amount).expect(
"return_item.amount() = self.amount() / 2 < self.amount() (since self.amount() ≥ \
1) self.max_amount() = return_item.max_amount(), since return_item is a \
duplicate of item",
);
Some(return_item)
} else {
None
}
}
#[cfg(test)]
pub fn create_test_item_from_kind(kind: ItemKind) -> Self {
let ability_map = &AbilityMap::load().read();

View File

@ -45,6 +45,11 @@ pub struct Inventory {
/// The "built-in" slots belonging to the inventory itself, all other slots
/// are provided by equipped items
slots: Vec<InvSlot>,
/// For when slot amounts are rebalanced or the inventory otherwise does not
/// have enough space to hold all the items after loading from database.
/// These slots are "remove-only" meaning that during normal gameplay items
/// can only be removed from these slots and never entered.
overflow_items: Vec<Item>,
}
/// Errors which the methods on `Inventory` produce
@ -55,6 +60,14 @@ pub enum Error {
Full(Vec<Item>),
}
impl Error {
pub fn returned_items(self) -> impl Iterator<Item = Item> {
match self {
Error::Full(items) => items.into_iter(),
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum InventorySortOrder {
Name,
@ -115,6 +128,7 @@ impl Inventory {
next_sort_order: InventorySortOrder::Name,
loadout,
slots: vec![None; DEFAULT_INVENTORY_SLOTS],
overflow_items: Vec::new(),
}
}
@ -123,10 +137,11 @@ impl Inventory {
next_sort_order: InventorySortOrder::Name,
loadout,
slots: vec![None; 1],
overflow_items: Vec::new(),
}
}
/// Total number of slots in in the inventory.
/// Total number of slots in the inventory.
pub fn capacity(&self) -> usize { self.slots().count() }
/// An iterator of all inventory slots
@ -136,6 +151,9 @@ impl Inventory {
.chain(self.loadout.inv_slots_with_id().map(|(_, slot)| slot))
}
/// An iterator of all overflow slots in the inventory
pub fn overflow_items(&self) -> impl Iterator<Item = &Item> { self.overflow_items.iter() }
/// A mutable iterator of all inventory slots
fn slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
self.slots.iter_mut().chain(self.loadout.inv_slots_mut())
@ -451,6 +469,20 @@ impl Inventory {
self.slot(inv_slot_id).and_then(Option::as_ref)
}
/// Get content of an overflow slot
pub fn get_overflow(&self, overflow: usize) -> Option<&Item> {
self.overflow_items.get(overflow)
}
/// Get content of any kind of slot
pub fn get_slot(&self, slot: Slot) -> Option<&Item> {
match slot {
Slot::Inventory(inv_slot) => self.get(inv_slot),
Slot::Equip(equip) => self.equipped(equip),
Slot::Overflow(overflow) => self.get_overflow(overflow),
}
}
/// Get item from inventory
pub fn get_by_hash(&self, item_hash: u64) -> Option<&Item> {
self.slots().flatten().find(|i| i.item_hash() == item_hash)
@ -506,11 +538,39 @@ impl Inventory {
*self.slot_mut(b).unwrap() = slot_a;
}
/// Moves an item from an overflow slot to an inventory slot
pub fn move_overflow_item(&mut self, overflow: usize, inv_slot: InvSlotId) {
match self.slot(inv_slot) {
Some(Some(_)) => {
warn!("Attempted to move from overflow slot to a filled inventory slot");
return;
},
None => {
warn!("Attempted to move from overflow slot to a non-existent inventory slot");
return;
},
Some(None) => {},
};
let item = self.overflow_items.remove(overflow);
*self.slot_mut(inv_slot).unwrap() = Some(item);
}
/// Remove an item from the slot
pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
self.slot_mut(inv_slot_id).and_then(|item| item.take())
}
/// Remove an item from an overflow slot
#[must_use = "Returned items will be lost if not used"]
pub fn overflow_remove(&mut self, overflow_slot: usize) -> Option<Item> {
if overflow_slot < self.overflow_items.len() {
Some(self.overflow_items.remove(overflow_slot))
} else {
None
}
}
/// Remove just one item from the slot
pub fn take(
&mut self,
@ -536,6 +596,7 @@ impl Inventory {
}
/// Takes half of the items from a slot in the inventory
#[must_use = "Returned items will be lost if not used"]
pub fn take_half(
&mut self,
inv_slot_id: InvSlotId,
@ -543,19 +604,24 @@ impl Inventory {
msm: &MaterialStatManifest,
) -> Option<Item> {
if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
if item.is_stackable() && item.amount() > 1 {
let mut return_item = item.duplicate(ability_map, msm);
let returning_amount = item.amount() / 2;
item.decrease_amount(returning_amount).ok()?;
return_item.set_amount(returning_amount).expect(
"return_item.amount() = item.amount() / 2 < item.amount() (since \
item.amount() 1) item.max_amount() = return_item.max_amount(), since \
return_item is a duplicate of item",
);
Some(return_item)
} else {
self.remove(inv_slot_id)
}
item.take_half(ability_map, msm)
.or_else(|| self.remove(inv_slot_id))
} else {
None
}
}
/// Takes half of the items from an overflow slot
#[must_use = "Returned items will be lost if not used"]
pub fn overflow_take_half(
&mut self,
overflow_slot: usize,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Option<Item> {
if let Some(item) = self.overflow_items.get_mut(overflow_slot) {
item.take_half(ability_map, msm)
.or_else(|| self.overflow_remove(overflow_slot))
} else {
None
}
@ -763,6 +829,15 @@ impl Inventory {
self.loadout.swap_slots(slot_a, slot_b, time);
Vec::new()
},
(Slot::Overflow(overflow_slot), Slot::Inventory(inv_slot))
| (Slot::Inventory(inv_slot), Slot::Overflow(overflow_slot)) => {
self.move_overflow_item(overflow_slot, inv_slot);
Vec::new()
},
// Items from overflow slots cannot be equipped until moved into a real inventory slot
(Slot::Overflow(_), Slot::Equip(_)) | (Slot::Equip(_), Slot::Overflow(_)) => Vec::new(),
// Items cannot be moved between overflow slots
(Slot::Overflow(_), Slot::Overflow(_)) => Vec::new(),
}
}
@ -910,6 +985,9 @@ impl Inventory {
item.update_item_state(ability_map, msm);
}
});
self.overflow_items
.iter_mut()
.for_each(|item| item.update_item_state(ability_map, msm));
}
/// Increments durability lost for all valid items equipped in loadout and
@ -955,8 +1033,17 @@ impl Inventory {
self.loadout
.repair_item_at_slot(equip_slot, ability_map, msm);
},
// Items in overflow slots cannot be repaired until they are moved to a real slot
Slot::Overflow(_) => {},
}
}
/// When loading a character from the persistence system, pushes any items
/// to overflow_items that were not able to be loaded into or pushed to the
/// inventory
pub fn persistence_push_overflow_items<I: Iterator<Item = Item>>(&mut self, overflow_items: I) {
self.overflow_items.extend(overflow_items);
}
}
impl Component for Inventory {

View File

@ -15,6 +15,7 @@ pub enum SlotError {
pub enum Slot {
Inventory(InvSlotId),
Equip(EquipSlot),
Overflow(usize),
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
@ -106,15 +107,6 @@ pub enum ArmorSlot {
Bag4,
}
impl Slot {
pub fn can_hold(self, item_kind: &ItemKind) -> bool {
match (self, item_kind) {
(Self::Inventory(_), _) => true,
(Self::Equip(slot), item_kind) => slot.can_hold(item_kind),
}
}
}
impl EquipSlot {
pub fn can_hold(self, item_kind: &ItemKind) -> bool {
match (self, item_kind) {

View File

@ -23,6 +23,7 @@ fn push_full() {
.map(|a| Some(a.duplicate(ability_map, msm)))
.collect(),
loadout: LoadoutBuilder::empty().build(),
overflow_items: vec![],
};
assert_eq!(
inv.push(TEST_ITEMS[0].duplicate(ability_map, msm))
@ -43,6 +44,7 @@ fn push_all_full() {
.map(|a| Some(a.duplicate(ability_map, msm)))
.collect(),
loadout: LoadoutBuilder::empty().build(),
overflow_items: vec![],
};
let Error::Full(leftovers) = inv
.push_all(
@ -73,6 +75,7 @@ fn push_unique_all_full() {
.map(|a| Some(a.duplicate(ability_map, msm)))
.collect(),
loadout: LoadoutBuilder::empty().build(),
overflow_items: vec![],
};
inv.push_all_unique(
TEST_ITEMS
@ -92,6 +95,7 @@ fn push_all_empty() {
next_sort_order: InventorySortOrder::Name,
slots: vec![None, None],
loadout: LoadoutBuilder::empty().build(),
overflow_items: vec![],
};
inv.push_all(
TEST_ITEMS
@ -111,6 +115,7 @@ fn push_all_unique_empty() {
next_sort_order: InventorySortOrder::Name,
slots: vec![None, None],
loadout: LoadoutBuilder::empty().build(),
overflow_items: vec![],
};
inv.push_all_unique(
TEST_ITEMS

View File

@ -1030,6 +1030,8 @@ impl RepairRecipeBook {
if let Some(item) = match item {
Slot::Equip(slot) => inv.equipped(slot),
Slot::Inventory(slot) => inv.get(slot),
// Items in overflow slots cannot be repaired until item is moved to a real slot
Slot::Overflow(_) => None,
} {
if let Some(repair_recipe) = self.repair_recipe(item) {
repair_recipe

View File

@ -1141,6 +1141,9 @@ pub fn handle_manipulate_loadout(
let inv_manip = InventoryManip::Use(slot);
output_events.emit_server(ServerEvent::InventoryManip(data.entity, inv_manip));
},
InventoryAction::Use(Slot::Overflow(_)) => {
// Items in overflow slots cannot be used until moved to a real slot
},
InventoryAction::ToggleSpriteLight(pos, enable) => {
if matches!(pos.kind, Volume::Terrain) {
let sprite_interact = sprite_interact::SpriteInteractKind::ToggleLight(enable);

View File

@ -559,6 +559,8 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
}
Some(InventoryUpdateEvent::Used)
},
// Items in overflow slots cannot be used
Slot::Overflow(_) => None,
};
drop(inventories);
@ -663,6 +665,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let item = match slot {
Slot::Inventory(slot) => inventory.take_half(slot, &ability_map, &msm),
Slot::Equip(_) => None,
Slot::Overflow(_) => None,
};
if let Some(item) = item {
@ -686,6 +689,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let item = match slot {
Slot::Inventory(slot) => inventory.remove(slot),
Slot::Equip(slot) => inventory.replace_loadout_item(slot, None, time),
Slot::Overflow(slot) => inventory.overflow_remove(slot),
};
// FIXME: We should really require the drop and write to be atomic!
@ -717,6 +721,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let item = match slot {
Slot::Inventory(slot) => inventory.take_half(slot, ability_map, &msm),
Slot::Equip(_) => None,
Slot::Overflow(o) => inventory.overflow_take_half(o, ability_map, &msm),
};
// FIXME: We should really require the drop and write to be atomic!

View File

@ -0,0 +1,32 @@
CREATE TEMP TABLE _temp_character_overflow_items_pairings
(
temp_overflow_items_container_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
character_id INT NOT NULL,
overflow_items_container_id INT
);
INSERT
INTO _temp_character_overflow_items_pairings
SELECT NULL,
i.item_id,
NULL
FROM item i
WHERE i.item_definition_id = 'veloren.core.pseudo_containers.character';
UPDATE _temp_character_overflow_items_pairings
SET overflow_items_container_id = ((SELECT MAX(entity_id) FROM entity) + temp_overflow_items_container_id);
INSERT
INTO entity
SELECT t.overflow_items_container_id
FROM _temp_character_overflow_items_pairings t;
INSERT
INTO item
SELECT t.overflow_items_container_id,
t.character_id,
'veloren.core.pseudo_containers.overflow_items',
1,
'overflow_items',
''
FROM _temp_character_overflow_items_pairings t;

View File

@ -46,14 +46,18 @@ pub(crate) use conversions::convert_waypoint_from_database_json as parse_waypoin
const CHARACTER_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.character";
const INVENTORY_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.inventory";
const LOADOUT_PSEUDO_CONTAINER_DEF_ID: &str = "veloren.core.pseudo_containers.loadout";
const OVERFLOW_ITEMS_PSEUDO_CONTAINER_DEF_ID: &str =
"veloren.core.pseudo_containers.overflow_items";
const INVENTORY_PSEUDO_CONTAINER_POSITION: &str = "inventory";
const LOADOUT_PSEUDO_CONTAINER_POSITION: &str = "loadout";
const OVERFLOW_ITEMS_PSEUDO_CONTAINER_POSITION: &str = "overflow_items";
const WORLD_PSEUDO_CONTAINER_ID: EntityId = 1;
#[derive(Clone, Copy)]
struct CharacterContainers {
inventory_container_id: EntityId,
loadout_container_id: EntityId,
overflow_items_container_id: EntityId,
}
/// Load the inventory/loadout
@ -126,6 +130,8 @@ pub fn load_character_data(
let character_containers = get_pseudo_containers(connection, char_id)?;
let inventory_items = load_items(connection, character_containers.inventory_container_id)?;
let loadout_items = load_items(connection, character_containers.loadout_container_id)?;
let overflow_items_items =
load_items(connection, character_containers.overflow_items_container_id)?;
let mut stmt = connection.prepare_cached(
"
@ -276,6 +282,8 @@ pub fn load_character_data(
&inventory_items,
character_containers.loadout_container_id,
&loadout_items,
character_containers.overflow_items_container_id,
&overflow_items_items,
)?,
waypoint: char_waypoint,
pets,
@ -383,13 +391,14 @@ pub fn create_character(
map_marker,
} = persisted_components;
// Fetch new entity IDs for character, inventory and loadout
let mut new_entity_ids = get_new_entity_ids(transaction, |next_id| next_id + 3)?;
// Fetch new entity IDs for character, inventory, loadout, and overflow items
let mut new_entity_ids = get_new_entity_ids(transaction, |next_id| next_id + 4)?;
// Create pseudo-container items for character
let character_id = new_entity_ids.next().unwrap();
let inventory_container_id = new_entity_ids.next().unwrap();
let loadout_container_id = new_entity_ids.next().unwrap();
let overflow_items_container_id = new_entity_ids.next().unwrap();
let pseudo_containers = vec![
Item {
@ -416,6 +425,14 @@ pub fn create_character(
position: LOADOUT_PSEUDO_CONTAINER_POSITION.to_owned(),
properties: String::new(),
},
Item {
stack_size: 1,
item_id: overflow_items_container_id,
parent_container_item_id: character_id,
item_definition_id: OVERFLOW_ITEMS_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: OVERFLOW_ITEMS_PSEUDO_CONTAINER_POSITION.to_owned(),
properties: String::new(),
},
];
let mut stmt = transaction.prepare_cached(
@ -524,6 +541,7 @@ pub fn create_character(
loadout_container_id,
&inventory,
inventory_container_id,
overflow_items_container_id,
&mut next_id,
);
inserts = inserts_;
@ -807,6 +825,11 @@ fn get_pseudo_containers(
character_id,
INVENTORY_PSEUDO_CONTAINER_POSITION,
)?,
overflow_items_container_id: get_pseudo_container_id(
connection,
character_id,
OVERFLOW_ITEMS_PSEUDO_CONTAINER_POSITION,
)?,
};
Ok(character_containers)
@ -1001,6 +1024,7 @@ pub fn update(
pseudo_containers.loadout_container_id,
&inventory,
pseudo_containers.inventory_container_id,
pseudo_containers.overflow_items_container_id,
&mut next_id,
);
upserts = upserts_;
@ -1012,6 +1036,7 @@ pub fn update(
let mut existing_item_ids: Vec<_> = vec![
Value::from(pseudo_containers.inventory_container_id),
Value::from(pseudo_containers.loadout_container_id),
Value::from(pseudo_containers.overflow_items_container_id),
];
for it in load_items(transaction, pseudo_containers.inventory_container_id)? {
existing_item_ids.push(Value::from(it.item_id));
@ -1019,6 +1044,9 @@ pub fn update(
for it in load_items(transaction, pseudo_containers.loadout_container_id)? {
existing_item_ids.push(Value::from(it.item_id));
}
for it in load_items(transaction, pseudo_containers.overflow_items_container_id)? {
existing_item_ids.push(Value::from(it.item_id));
}
let non_upserted_items = upserts
.iter()

View File

@ -56,16 +56,25 @@ pub fn convert_items_to_database_items(
loadout_container_id: EntityId,
inventory: &Inventory,
inventory_container_id: EntityId,
overflow_items_container_id: EntityId,
next_id: &mut i64,
) -> Vec<ItemModelPair> {
let loadout = inventory
.loadout_items_with_persistence_key()
.map(|(slot, item)| (slot.to_string(), item, loadout_container_id));
let overflow_items = inventory.overflow_items().enumerate().map(|(i, item)| {
(
serde_json::to_string(&i).expect("failed to serialize index of overflow item"),
Some(item),
overflow_items_container_id,
)
});
// Inventory slots.
let inventory = inventory.slots_with_id().map(|(pos, item)| {
(
serde_json::to_string(&pos).expect("failed to serialize InventorySlotPos"),
serde_json::to_string(&pos).expect("failed to serialize InvSlotId"),
item.as_ref(),
inventory_container_id,
)
@ -73,11 +82,12 @@ pub fn convert_items_to_database_items(
// Use Breadth-first search to recurse into containers/modular weapons to store
// their parts
let mut bfs_queue: VecDeque<_> = inventory.chain(loadout).collect();
let mut bfs_queue: VecDeque<_> = inventory.chain(loadout).chain(overflow_items).collect();
let mut upserts = Vec::new();
let mut depth = HashMap::new();
depth.insert(inventory_container_id, 0);
depth.insert(loadout_container_id, 0);
depth.insert(overflow_items_container_id, 0);
while let Some((position, item, parent_container_item_id)) = bfs_queue.pop_front() {
// Construct new items.
if let Some(item) = item {
@ -352,6 +362,8 @@ pub fn convert_inventory_from_database_items(
inventory_items: &[Item],
loadout_container_id: i64,
loadout_items: &[Item],
overflow_items_container_id: i64,
overflow_items: &[Item],
) -> Result<Inventory, PersistenceError> {
// Loadout items must be loaded before inventory items since loadout items
// provide inventory slots. Since items stored inside loadout items actually
@ -360,9 +372,13 @@ pub fn convert_inventory_from_database_items(
// inventory at the correct position.
//
let loadout = convert_loadout_from_database_items(loadout_container_id, loadout_items)?;
let overflow_items =
convert_overflow_items_from_database_items(overflow_items_container_id, overflow_items)?;
let mut inventory = Inventory::with_loadout_humanoid(loadout);
let mut item_indices = HashMap::new();
let mut failed_inserts = HashMap::new();
// In order to items with components to properly load, it is important that this
// item iteration occurs in order so that any modular items are loaded before
// its components.
@ -412,36 +428,50 @@ pub fn convert_inventory_from_database_items(
};
if db_item.parent_container_item_id == inventory_container_id {
let slot = slot(&db_item.position)?;
let insert_res = inventory.insert_at(slot, item).map_err(|_| {
// If this happens there were too many items in the database for the current
// inventory size
//
// FIXME: On failure, collect the set of items that don't fit and return them
// (to be dropped next to the player) as this could be the
// result of a change in the slot capacity for an equipped bag
// (or a change in the inventory size).
PersistenceError::ConversionError(format!(
"Error inserting item into inventory, position: {:?}",
slot
))
})?;
match slot(&db_item.position) {
Ok(slot) => {
let insert_res = inventory.insert_at(slot, item);
if insert_res.is_some() {
// If inventory.insert returns an item, it means it was swapped for an item that
// already occupied the slot. Multiple items being stored in the database for
// the same slot is an error.
return Err(PersistenceError::ConversionError(
"Inserted an item into the same slot twice".to_string(),
));
match insert_res {
Ok(None) => {
// Insert successful
},
Ok(Some(_item)) => {
// If inventory.insert returns an item, it means it was swapped for
// an item that already occupied the
// slot. Multiple items being stored
// in the database for the same slot is
// an error.
return Err(PersistenceError::ConversionError(
"Inserted an item into the same slot twice".to_string(),
));
},
Err(item) => {
// If this happens there were too many items in the database for the
// current inventory size
failed_inserts.insert(db_item.position.clone(), item);
},
}
},
Err(err) => {
return Err(err);
},
}
} else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
get_mutable_item(
j,
inventory_items,
&item_indices,
&mut inventory,
&|inv, s| inv.slot_mut(slot(s).ok()?).and_then(|a| a.as_mut()),
&mut (&mut inventory, &mut failed_inserts),
&|(inv, f_i): &mut (&mut Inventory, &mut HashMap<String, VelorenItem>), s| {
// Attempts first to access inventory if that slot exists there. If it does not
// it instead attempts to access failed inserts list.
slot(s)
.ok()
.and_then(|slot| inv.slot_mut(slot))
.and_then(|a| a.as_mut())
.or_else(|| f_i.get_mut(s))
},
)?
.persistence_access_add_component(item);
} else {
@ -452,6 +482,16 @@ pub fn convert_inventory_from_database_items(
}
}
// For overflow items and failed inserts, attempt to push to inventory. If push
// fails, move to overflow slots.
if let Err(inv_error) = inventory.push_all(
overflow_items
.into_iter()
.chain(failed_inserts.into_values()),
) {
inventory.persistence_push_overflow_items(inv_error.returned_items());
}
// Some items may have had components added, so update the item config of each
// item to ensure that it correctly accounts for components that were added
inventory.persistence_update_all_item_states(&ABILITY_MAP, &MATERIAL_STATS_MANIFEST);
@ -519,6 +559,86 @@ pub fn convert_loadout_from_database_items(
Ok(loadout)
}
pub fn convert_overflow_items_from_database_items(
overflow_items_container_id: i64,
database_items: &[Item],
) -> Result<Vec<VelorenItem>, PersistenceError> {
let mut overflow_items_with_database_position = HashMap::new();
let mut item_indices = HashMap::new();
// In order to items with components to properly load, it is important that this
// item iteration occurs in order so that any modular items are loaded before
// its components.
for (i, db_item) in database_items.iter().enumerate() {
item_indices.insert(db_item.item_id, i);
let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
let item_properties =
serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
json_models::apply_db_item_properties(&mut item, &item_properties);
// NOTE: item id is currently *unique*, so we can store the ID safely.
let comp = item.get_item_id_for_database();
// Item ID
comp.store(Some(NonZeroU64::try_from(db_item.item_id as u64).map_err(
|_| PersistenceError::ConversionError("Item with zero item_id".to_owned()),
)?));
// Stack Size
if db_item.stack_size == 1 || item.is_stackable() {
// FIXME: On failure, collect the set of items that don't fit and return them
// (to be dropped next to the player) as this could be the result of
// a change in the max amount for that item.
item.set_amount(u32::try_from(db_item.stack_size).map_err(|_| {
PersistenceError::ConversionError(format!(
"Invalid item stack size for stackable={}: {}",
item.is_stackable(),
&db_item.stack_size
))
})?)
.map_err(|_| {
PersistenceError::ConversionError("Error setting amount for item".to_owned())
})?;
}
if db_item.parent_container_item_id == overflow_items_container_id {
match overflow_items_with_database_position.insert(db_item.position.clone(), item) {
None => {
// Insert successful
},
Some(_item) => {
// If insert returns a value, database had two items stored with the same
// position which is an error.
return Err(PersistenceError::ConversionError(
"Inserted an item into the same overflow slot twice".to_string(),
));
},
}
} else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
get_mutable_item(
j,
database_items,
&item_indices,
&mut overflow_items_with_database_position,
&|o_i, s| o_i.get_mut(s),
)?
.persistence_access_add_component(item);
} else {
return Err(PersistenceError::ConversionError(format!(
"Couldn't find parent item {} before item {} in overflow items",
db_item.parent_container_item_id, db_item.item_id
)));
}
}
let overflow_items = overflow_items_with_database_position
.into_values()
.collect::<Vec<_>>();
Ok(overflow_items)
}
fn get_item_from_asset(item_definition_id: &str) -> Result<common::comp::Item, PersistenceError> {
common::comp::Item::new_from_asset(item_definition_id).map_err(|err| {
PersistenceError::AssetError(format!(

View File

@ -20,7 +20,7 @@ use common::{
assets::AssetExt,
combat::{combat_rating, perception_dist_multiplier_from_stealth, Damage},
comp::{
inventory::InventorySortOrder,
inventory::{slot::Slot, InventorySortOrder},
item::{ItemDef, ItemDesc, ItemI18n, MaterialStatManifest, Quality},
Body, Energy, Health, Inventory, Poise, SkillSet, Stats,
},
@ -308,9 +308,10 @@ impl<'a> InventoryScroller<'a> {
// Create available inventory slot widgets
if state.ids.inv_slots.len() < self.inventory.capacity() {
state.update(|s| {
s.ids
.inv_slots
.resize(self.inventory.capacity(), &mut ui.widget_id_generator());
s.ids.inv_slots.resize(
self.inventory.capacity() + self.inventory.overflow_items().count(),
&mut ui.widget_id_generator(),
);
});
}
if state.ids.inv_slot_names.len() < self.inventory.capacity() {
@ -363,7 +364,17 @@ impl<'a> InventoryScroller<'a> {
};
let mut i = 0;
let mut items = self.inventory.slots_with_id().collect::<Vec<_>>();
let mut items = self
.inventory
.slots_with_id()
.map(|(slot, item)| (Slot::Inventory(slot), item.as_ref()))
.chain(
self.inventory
.overflow_items()
.enumerate()
.map(|(i, item)| (Slot::Overflow(i), Some(item))),
)
.collect::<Vec<_>>();
if self.details_mode && !self.is_us {
items.sort_by_cached_key(|(_, item)| {
(
@ -419,6 +430,11 @@ impl<'a> InventoryScroller<'a> {
slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 1.0, 1.0, 1.0));
}
// Highlight in red slots that are overflow
if matches!(pos, Slot::Overflow(_)) {
slot_widget = slot_widget.with_background_color(Color::Rgba(1.0, 0.0, 0.0, 1.0));
}
if let Some(item) = item {
let quality_col_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey,

View File

@ -1576,6 +1576,7 @@ impl<'a> Widget for Crafting<'a> {
modifier: craft_slot_2.and_then(|slot| match slot {
Slot::Inventory(slot) => Some(slot),
Slot::Equip(_) => None,
Slot::Overflow(_) => None,
}),
});
}
@ -1736,6 +1737,7 @@ impl<'a> Widget for Crafting<'a> {
.and_then(|slot| match slot {
Slot::Inventory(slot) => self.inventory.get(slot),
Slot::Equip(_) => None,
Slot::Overflow(_) => None,
})
.and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
{
@ -1746,6 +1748,7 @@ impl<'a> Widget for Crafting<'a> {
.and_then(|slot| match slot {
Slot::Inventory(slot) => self.inventory.get(slot),
Slot::Equip(_) => None,
Slot::Overflow(_) => None,
})
.and_then(|item| {
item.item_definition_id().itemdef_id().map(String::from)
@ -1769,6 +1772,7 @@ impl<'a> Widget for Crafting<'a> {
if let Some(item) = match craft_slot_1 {
Some(Slot::Inventory(slot)) => self.inventory.get(slot),
Some(Slot::Equip(slot)) => self.inventory.equipped(slot),
Some(Slot::Overflow(_)) => None,
None => None,
} {
if let Some(recipe) = self.client.repair_recipe_book().repair_recipe(item) {

View File

@ -3887,9 +3887,18 @@ impl Hud {
'slot_events: for event in self.slot_manager.maintain(ui_widgets) {
use slots::{AbilitySlot, InventorySlot, SlotKind::*};
let to_slot = |slot_kind| match slot_kind {
Inventory(
i @ InventorySlot {
slot: Slot::Inventory(_) | Slot::Overflow(_),
ours: true,
..
},
) => Some(i.slot),
Inventory(InventorySlot {
slot, ours: true, ..
}) => Some(Slot::Inventory(slot)),
slot: Slot::Equip(_),
ours: true,
..
}) => None,
Inventory(InventorySlot { ours: false, .. }) => None,
Equip(e) => Some(Slot::Equip(e)),
Hotbar(_) => None,
@ -3913,21 +3922,27 @@ impl Hud {
Hotbar(h),
) = (a, b)
{
if let Some(item) = inventories
.get(info.viewpoint_entity)
.and_then(|inv| inv.get(slot))
{
self.hotbar.add_inventory_link(h, item);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
if let Slot::Inventory(slot) = slot {
if let Some(item) = inventories
.get(info.viewpoint_entity)
.and_then(|inv| inv.get(slot))
{
self.hotbar.add_inventory_link(h, item);
events.push(Event::ChangeHotbarState(Box::new(
self.hotbar.to_owned(),
)));
}
}
} else if let (Hotbar(a), Hotbar(b)) = (a, b) {
self.hotbar.swap(a, b);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
} else if let (Inventory(i), Trade(t)) = (a, b) {
if i.ours == t.ours {
if let Some(inventory) = inventories.get(t.entity) {
if let (Some(inventory), Slot::Inventory(slot)) =
(inventories.get(t.entity), i.slot)
{
events.push(Event::TradeAction(TradeAction::AddItem {
item: i.slot,
item: slot,
quantity: i.amount(inventory).unwrap_or(1),
ours: i.ours,
}));
@ -3973,18 +3988,20 @@ impl Hud {
(AbilitySlot::Ability(_), AbilitySlot::Ability(_)) => {},
}
} else if let (Inventory(i), Crafting(c)) = (a, b) {
// Add item to crafting input
if inventories
.get(info.viewpoint_entity)
.and_then(|inv| inv.get(i.slot))
.map_or(false, |item| {
(c.requirement)(item, client.component_recipe_book(), c.info)
})
{
self.show
.crafting_fields
.recipe_inputs
.insert(c.index, Slot::Inventory(i.slot));
if let Slot::Inventory(slot) = i.slot {
// Add item to crafting input
if inventories
.get(info.viewpoint_entity)
.and_then(|inv| inv.get(slot))
.map_or(false, |item| {
(c.requirement)(item, client.component_recipe_book(), c.info)
})
{
self.show
.crafting_fields
.recipe_inputs
.insert(c.index, i.slot);
}
}
} else if let (Equip(e), Crafting(c)) = (a, b) {
// Add item to crafting input
@ -4055,21 +4072,27 @@ impl Hud {
bypass_dialog: false,
});
} else if let (Inventory(i), Hotbar(h)) = (a, b) {
if let Some(item) = inventories
.get(info.viewpoint_entity)
.and_then(|inv| inv.get(i.slot))
{
self.hotbar.add_inventory_link(h, item);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
if let Slot::Inventory(slot) = i.slot {
if let Some(item) = inventories
.get(info.viewpoint_entity)
.and_then(|inv| inv.get(slot))
{
self.hotbar.add_inventory_link(h, item);
events.push(Event::ChangeHotbarState(Box::new(
self.hotbar.to_owned(),
)));
}
}
} else if let (Hotbar(a), Hotbar(b)) = (a, b) {
self.hotbar.swap(a, b);
events.push(Event::ChangeHotbarState(Box::new(self.hotbar.to_owned())));
} else if let (Inventory(i), Trade(t)) = (a, b) {
if i.ours == t.ours {
if let Some(inventory) = inventories.get(t.entity) {
if let (Some(inventory), Slot::Inventory(slot)) =
(inventories.get(t.entity), i.slot)
{
events.push(Event::TradeAction(TradeAction::AddItem {
item: i.slot,
item: slot,
quantity: i.amount(inventory).unwrap_or(1) / 2,
ours: i.ours,
}));
@ -4253,24 +4276,26 @@ impl Hud {
match slot {
Inventory(i) => {
if let Some(inventory) = inventories.get(i.entity) {
let mut quantity = 1;
if auto_quantity {
do_auto_quantity(
inventory,
i.slot,
i.ours,
false,
&mut quantity,
);
let inv_quantity = i.amount(inventory).unwrap_or(1);
quantity = quantity.min(inv_quantity);
}
if let Slot::Inventory(slot) = i.slot {
let mut quantity = 1;
if auto_quantity {
do_auto_quantity(
inventory,
slot,
i.ours,
false,
&mut quantity,
);
let inv_quantity = i.amount(inventory).unwrap_or(1);
quantity = quantity.min(inv_quantity);
}
events.push(Event::TradeAction(TradeAction::AddItem {
item: i.slot,
quantity,
ours: i.ours,
}));
events.push(Event::TradeAction(TradeAction::AddItem {
item: slot,
quantity,
ours: i.ours,
}));
}
}
},
Trade(t) => {
@ -4530,7 +4555,7 @@ impl Hud {
) {
use slots::InventorySlot;
if let Some(slots::SlotKind::Inventory(InventorySlot {
slot: i,
slot: Slot::Inventory(i),
ours: true,
..
})) = slot_manager.selected()

View File

@ -36,7 +36,7 @@ pub type SlotManager = slot::SlotManager<SlotKind>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InventorySlot {
pub slot: InvSlotId,
pub slot: Slot,
pub entity: EcsEntity,
pub ours: bool,
}
@ -45,12 +45,12 @@ impl SlotKey<Inventory, ItemImgs> for InventorySlot {
type ImageKey = ItemKey;
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
source.get(self.slot).map(|i| (i.into(), None))
source.get_slot(self.slot).map(|i| (i.into(), None))
}
fn amount(&self, source: &Inventory) -> Option<u32> {
source
.get(self.slot)
.get_slot(self.slot)
.map(|item| item.amount())
.filter(|amount| *amount > 1)
}
@ -90,7 +90,7 @@ impl SlotKey<Inventory, ItemImgs> for TradeSlot {
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
self.invslot.and_then(|inv_id| {
InventorySlot {
slot: inv_id,
slot: Slot::Inventory(inv_id),
ours: self.ours,
entity: self.entity,
}
@ -102,7 +102,7 @@ impl SlotKey<Inventory, ItemImgs> for TradeSlot {
self.invslot
.and_then(|inv_id| {
InventorySlot {
slot: inv_id,
slot: Slot::Inventory(inv_id),
ours: self.ours,
entity: self.entity,
}
@ -286,6 +286,7 @@ impl CraftSlot {
match self.slot {
Some(Slot::Inventory(slot)) => inv.get(slot),
Some(Slot::Equip(slot)) => inv.equipped(slot),
Some(Slot::Overflow(_)) => None,
None => None,
}
}

View File

@ -1753,6 +1753,7 @@ impl PlayState for SessionState {
move_allowed = false;
}
},
Slot::Overflow(_) => {},
}
};
}
@ -2021,6 +2022,7 @@ impl PlayState for SessionState {
let item = match item {
Slot::Equip(slot) => inventory.equipped(slot),
Slot::Inventory(slot) => inventory.get(slot),
Slot::Overflow(_) => None,
}?;
let repair_recipe =
client.repair_recipe_book().repair_recipe(item)?;