2025-07-09 22:59:39 -04:00
|
|
|
use crate::common::GameData;
|
2025-07-12 17:40:22 -04:00
|
|
|
use binrw::binrw;
|
2025-06-28 10:44:25 -04:00
|
|
|
use icarus::{ClassJob::ClassJobSheet, Race::RaceSheet};
|
2025-05-09 18:35:44 -04:00
|
|
|
use physis::common::Language;
|
2025-05-02 16:15:54 -04:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
2025-05-02 23:24:31 -04:00
|
|
|
use crate::ipc::zone::ItemOperation;
|
2025-05-02 16:15:54 -04:00
|
|
|
|
|
|
|
mod equipped;
|
|
|
|
pub use equipped::EquippedStorage;
|
|
|
|
|
|
|
|
mod generic;
|
|
|
|
pub use generic::GenericStorage;
|
|
|
|
|
|
|
|
mod item;
|
|
|
|
pub use item::Item;
|
|
|
|
|
|
|
|
mod storage;
|
|
|
|
pub use storage::{ContainerType, Storage};
|
|
|
|
|
2025-06-24 19:15:25 -04:00
|
|
|
mod currency;
|
|
|
|
pub use currency::CurrencyStorage;
|
|
|
|
|
2025-07-12 17:40:22 -04:00
|
|
|
use crate::{
|
|
|
|
INVENTORY_ACTION_COMBINE_STACK, INVENTORY_ACTION_DISCARD, INVENTORY_ACTION_EXCHANGE,
|
|
|
|
INVENTORY_ACTION_MOVE, INVENTORY_ACTION_SPLIT_STACK,
|
|
|
|
};
|
2025-07-04 12:34:03 -04:00
|
|
|
|
2025-05-02 16:15:54 -04:00
|
|
|
const MAX_NORMAL_STORAGE: usize = 35;
|
|
|
|
const MAX_LARGE_STORAGE: usize = 50;
|
|
|
|
|
2025-07-12 17:40:22 -04:00
|
|
|
#[binrw]
|
|
|
|
#[derive(Debug, Clone, Default, Copy, PartialEq)]
|
|
|
|
#[brw(repr = u8)]
|
|
|
|
#[repr(u8)]
|
|
|
|
pub enum ItemOperationKind {
|
|
|
|
/// The operation opcode/type when discarding an item from the inventory.
|
|
|
|
Discard = INVENTORY_ACTION_DISCARD,
|
|
|
|
#[default]
|
|
|
|
/// The operation opcode/type when moving an item to an emtpy slot in the inventory.
|
|
|
|
Move = INVENTORY_ACTION_MOVE,
|
|
|
|
/// The operation opcode/type when moving an item to a slot occupied by another in the inventory.
|
|
|
|
Exchange = INVENTORY_ACTION_EXCHANGE,
|
|
|
|
/// The operation opcode/type when splitting stacks of identical items.
|
|
|
|
SplitStack = INVENTORY_ACTION_SPLIT_STACK,
|
|
|
|
/// The operation opcode/type when combining stacks of identical items.
|
|
|
|
CombineStack = INVENTORY_ACTION_COMBINE_STACK,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<u8> for ItemOperationKind {
|
|
|
|
type Error = ();
|
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
|
|
match value {
|
|
|
|
x if x == ItemOperationKind::Discard as u8 => Ok(ItemOperationKind::Discard),
|
|
|
|
x if x == ItemOperationKind::Move as u8 => Ok(ItemOperationKind::Move),
|
|
|
|
x if x == ItemOperationKind::Exchange as u8 => Ok(ItemOperationKind::Exchange),
|
|
|
|
x if x == ItemOperationKind::SplitStack as u8 => Ok(ItemOperationKind::SplitStack),
|
|
|
|
x if x == ItemOperationKind::CombineStack as u8 => Ok(ItemOperationKind::CombineStack),
|
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-02 16:15:54 -04:00
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
|
|
pub struct Inventory {
|
|
|
|
pub equipped: EquippedStorage,
|
|
|
|
pub pages: [GenericStorage<MAX_NORMAL_STORAGE>; 4],
|
|
|
|
pub armoury_main_hand: GenericStorage<MAX_LARGE_STORAGE>,
|
|
|
|
pub armoury_head: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_body: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_hands: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_legs: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_feet: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_off_hand: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_earring: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_necklace: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_bracelet: GenericStorage<MAX_NORMAL_STORAGE>,
|
|
|
|
pub armoury_rings: GenericStorage<MAX_LARGE_STORAGE>,
|
|
|
|
pub armoury_soul_crystal: GenericStorage<MAX_NORMAL_STORAGE>,
|
2025-06-24 19:15:25 -04:00
|
|
|
pub currency: CurrencyStorage,
|
2025-05-02 16:15:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Inventory {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
equipped: EquippedStorage::default(),
|
|
|
|
pages: std::array::from_fn(|_| GenericStorage::default()),
|
|
|
|
armoury_main_hand: GenericStorage::default(),
|
|
|
|
armoury_head: GenericStorage::default(),
|
|
|
|
armoury_body: GenericStorage::default(),
|
|
|
|
armoury_hands: GenericStorage::default(),
|
|
|
|
armoury_legs: GenericStorage::default(),
|
|
|
|
armoury_feet: GenericStorage::default(),
|
|
|
|
armoury_off_hand: GenericStorage::default(),
|
|
|
|
armoury_earring: GenericStorage::default(),
|
|
|
|
armoury_necklace: GenericStorage::default(),
|
|
|
|
armoury_bracelet: GenericStorage::default(),
|
|
|
|
armoury_rings: GenericStorage::default(),
|
|
|
|
armoury_soul_crystal: GenericStorage::default(),
|
2025-06-24 19:15:25 -04:00
|
|
|
currency: CurrencyStorage::default(),
|
2025-05-02 16:15:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> IntoIterator for &'a Inventory {
|
|
|
|
type Item = (ContainerType, &'a dyn Storage);
|
|
|
|
type IntoIter = InventoryIterator<'a>;
|
|
|
|
fn into_iter(self) -> InventoryIterator<'a> {
|
|
|
|
InventoryIterator {
|
|
|
|
inventory: self,
|
|
|
|
curr: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-12 07:43:01 -04:00
|
|
|
#[cfg(not(target_family = "wasm"))]
|
|
|
|
impl rusqlite::types::FromSql for Inventory {
|
|
|
|
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
2025-07-09 22:59:39 -04:00
|
|
|
Ok(serde_json::from_str(&String::column_result(value)?).unwrap())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-02 16:15:54 -04:00
|
|
|
pub struct InventoryIterator<'a> {
|
|
|
|
inventory: &'a Inventory,
|
|
|
|
curr: u16,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Iterator for InventoryIterator<'a> {
|
|
|
|
type Item = (ContainerType, &'a dyn Storage);
|
|
|
|
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
|
|
let curr = self.curr;
|
|
|
|
self.curr += 1;
|
|
|
|
|
2025-06-25 17:02:19 -04:00
|
|
|
if curr >= 18 {
|
2025-05-02 16:15:54 -04:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let container_type = match curr {
|
|
|
|
// inventory
|
|
|
|
0 => ContainerType::Inventory0,
|
|
|
|
1 => ContainerType::Inventory1,
|
|
|
|
2 => ContainerType::Inventory2,
|
|
|
|
3 => ContainerType::Inventory3,
|
|
|
|
|
|
|
|
// armory
|
|
|
|
4 => ContainerType::ArmoryOffWeapon,
|
|
|
|
5 => ContainerType::ArmoryHead,
|
|
|
|
6 => ContainerType::ArmoryBody,
|
|
|
|
7 => ContainerType::ArmoryHand,
|
|
|
|
8 => ContainerType::ArmoryLeg,
|
|
|
|
9 => ContainerType::ArmoryFoot,
|
|
|
|
10 => ContainerType::ArmoryEarring,
|
|
|
|
11 => ContainerType::ArmoryNeck,
|
|
|
|
12 => ContainerType::ArmoryWrist,
|
|
|
|
13 => ContainerType::ArmoryRing,
|
|
|
|
14 => ContainerType::ArmorySoulCrystal,
|
|
|
|
15 => ContainerType::ArmoryWeapon,
|
|
|
|
|
|
|
|
// equipped
|
|
|
|
16 => ContainerType::Equipped,
|
2025-06-25 17:02:19 -04:00
|
|
|
|
|
|
|
// currency
|
|
|
|
17 => ContainerType::Currency,
|
2025-05-02 16:15:54 -04:00
|
|
|
_ => panic!("Inventory iterator invalid!"),
|
|
|
|
};
|
|
|
|
|
|
|
|
Some((
|
|
|
|
container_type,
|
|
|
|
self.inventory.get_container(&container_type),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Inventory {
|
2025-06-28 10:44:25 -04:00
|
|
|
/// Equip the starting items for a given classjob
|
|
|
|
pub fn equip_classjob_items(&mut self, classjob_id: u16, game_data: &mut GameData) {
|
2025-07-08 21:53:24 -04:00
|
|
|
let sheet = ClassJobSheet::read_from(&mut game_data.resource, Language::English).unwrap();
|
2025-06-28 10:44:25 -04:00
|
|
|
let row = sheet.get_row(classjob_id as u32).unwrap();
|
|
|
|
|
|
|
|
self.equipped.main_hand =
|
|
|
|
Item::new(1, *row.ItemStartingWeapon().into_i32().unwrap() as u32);
|
|
|
|
|
|
|
|
// TODO: don't hardcode
|
|
|
|
self.equipped.ears = Item::new(1, 0x00003b1b);
|
|
|
|
self.equipped.neck = Item::new(1, 0x00003b1a);
|
|
|
|
self.equipped.wrists = Item::new(1, 0x00003b1c);
|
|
|
|
self.equipped.right_ring = Item::new(1, 0x0000114a);
|
|
|
|
self.equipped.left_ring = Item::new(1, 0x00003b1d);
|
|
|
|
}
|
|
|
|
|
2025-05-02 16:15:54 -04:00
|
|
|
/// Equip the starting items for a given race
|
|
|
|
pub fn equip_racial_items(&mut self, race_id: u8, gender: u8, game_data: &mut GameData) {
|
2025-07-08 21:53:24 -04:00
|
|
|
let sheet = RaceSheet::read_from(&mut game_data.resource, Language::English).unwrap();
|
2025-05-09 18:35:44 -04:00
|
|
|
let row = sheet.get_row(race_id as u32).unwrap();
|
2025-05-02 16:15:54 -04:00
|
|
|
|
|
|
|
if gender == 0 {
|
2025-05-09 18:35:44 -04:00
|
|
|
self.equipped.body = Item::new(1, *row.RSEMBody().into_i32().unwrap() as u32);
|
|
|
|
self.equipped.hands = Item::new(1, *row.RSEMHands().into_i32().unwrap() as u32);
|
|
|
|
self.equipped.legs = Item::new(1, *row.RSEMLegs().into_i32().unwrap() as u32);
|
|
|
|
self.equipped.feet = Item::new(1, *row.RSEMFeet().into_i32().unwrap() as u32);
|
2025-05-02 16:15:54 -04:00
|
|
|
} else {
|
2025-05-09 18:35:44 -04:00
|
|
|
self.equipped.body = Item::new(1, *row.RSEFBody().into_i32().unwrap() as u32);
|
|
|
|
self.equipped.hands = Item::new(1, *row.RSEFHands().into_i32().unwrap() as u32);
|
|
|
|
self.equipped.legs = Item::new(1, *row.RSEFLegs().into_i32().unwrap() as u32);
|
|
|
|
self.equipped.feet = Item::new(1, *row.RSEFFeet().into_i32().unwrap() as u32);
|
2025-05-02 16:15:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-12 17:40:22 -04:00
|
|
|
/// Helper functions to reduce boilerplate
|
|
|
|
fn get_item_mut(&mut self, storage_id: ContainerType, storage_index: u16) -> &mut Item {
|
|
|
|
let container = self.get_container_mut(&storage_id);
|
|
|
|
container.get_slot_mut(storage_index)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_item(&self, storage_id: ContainerType, storage_index: u16) -> Item {
|
|
|
|
let container = self.get_container(&storage_id);
|
|
|
|
*container.get_slot(storage_index)
|
|
|
|
}
|
|
|
|
|
2025-05-02 23:24:31 -04:00
|
|
|
pub fn process_action(&mut self, action: &ItemOperation) {
|
2025-07-12 17:40:22 -04:00
|
|
|
match action.operation_type {
|
|
|
|
ItemOperationKind::Discard => {
|
|
|
|
let src_item = self.get_item_mut(action.src_storage_id, action.src_container_index);
|
|
|
|
*src_item = Item::default();
|
|
|
|
}
|
|
|
|
ItemOperationKind::CombineStack => {
|
|
|
|
let src_item;
|
|
|
|
{
|
|
|
|
let original_item =
|
|
|
|
self.get_item_mut(action.src_storage_id, action.src_container_index);
|
|
|
|
src_item = *original_item;
|
|
|
|
*original_item = Item::default();
|
|
|
|
}
|
|
|
|
|
|
|
|
let dst_item = self.get_item_mut(action.dst_storage_id, action.dst_container_index);
|
|
|
|
// TODO: We ought to check the max stack size for a given item id and disallow overflow
|
|
|
|
dst_item.quantity += src_item.quantity;
|
2025-05-02 16:15:54 -04:00
|
|
|
}
|
2025-07-12 17:40:22 -04:00
|
|
|
ItemOperationKind::SplitStack => {
|
|
|
|
let mut src_item;
|
|
|
|
{
|
|
|
|
let original_item =
|
|
|
|
self.get_item_mut(action.src_storage_id, action.src_container_index);
|
|
|
|
if original_item.quantity >= action.dst_stack {
|
|
|
|
original_item.quantity -= action.dst_stack;
|
|
|
|
src_item = *original_item;
|
|
|
|
src_item.quantity = action.dst_stack
|
|
|
|
} else {
|
|
|
|
tracing::warn!(
|
|
|
|
"Client sent a bogus split amount: {}! Rejecting item operation!",
|
|
|
|
action.dst_stack
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2025-05-02 16:15:54 -04:00
|
|
|
|
2025-07-12 17:40:22 -04:00
|
|
|
let dst_item = self.get_item_mut(action.dst_storage_id, action.dst_container_index);
|
|
|
|
dst_item.clone_from(&src_item);
|
|
|
|
}
|
|
|
|
ItemOperationKind::Exchange | ItemOperationKind::Move => {
|
|
|
|
let src_item = self.get_item(action.src_storage_id, action.src_container_index);
|
2025-05-02 16:15:54 -04:00
|
|
|
|
2025-07-12 17:40:22 -04:00
|
|
|
// move src item into dst slot
|
|
|
|
let dst_slot = self.get_item_mut(action.dst_storage_id, action.dst_container_index);
|
|
|
|
let dst_item = *dst_slot;
|
2025-05-02 16:15:54 -04:00
|
|
|
dst_slot.clone_from(&src_item);
|
|
|
|
|
2025-07-12 17:40:22 -04:00
|
|
|
// move dst item into src slot
|
|
|
|
let src_slot = self.get_item_mut(action.src_storage_id, action.src_container_index);
|
2025-05-02 16:15:54 -04:00
|
|
|
src_slot.clone_from(&dst_item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_in_next_free_slot(&mut self, item: Item) {
|
|
|
|
for page in &mut self.pages {
|
|
|
|
for slot in &mut page.slots {
|
|
|
|
if slot.quantity == 0 {
|
|
|
|
slot.clone_from(&item);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_container_mut(&mut self, container_type: &ContainerType) -> &mut dyn Storage {
|
|
|
|
match container_type {
|
|
|
|
ContainerType::Inventory0 => &mut self.pages[0],
|
|
|
|
ContainerType::Inventory1 => &mut self.pages[1],
|
|
|
|
ContainerType::Inventory2 => &mut self.pages[2],
|
|
|
|
ContainerType::Inventory3 => &mut self.pages[3],
|
|
|
|
ContainerType::Equipped => &mut self.equipped,
|
2025-06-24 19:15:25 -04:00
|
|
|
ContainerType::Currency => &mut self.currency,
|
2025-05-02 16:15:54 -04:00
|
|
|
ContainerType::ArmoryOffWeapon => &mut self.armoury_off_hand,
|
|
|
|
ContainerType::ArmoryHead => &mut self.armoury_head,
|
|
|
|
ContainerType::ArmoryBody => &mut self.armoury_body,
|
|
|
|
ContainerType::ArmoryHand => &mut self.armoury_hands,
|
|
|
|
ContainerType::ArmoryLeg => &mut self.armoury_legs,
|
|
|
|
ContainerType::ArmoryFoot => &mut self.armoury_feet,
|
|
|
|
ContainerType::ArmoryEarring => &mut self.armoury_earring,
|
|
|
|
ContainerType::ArmoryNeck => &mut self.armoury_necklace,
|
|
|
|
ContainerType::ArmoryWrist => &mut self.armoury_bracelet,
|
|
|
|
ContainerType::ArmoryRing => &mut self.armoury_rings,
|
|
|
|
ContainerType::ArmorySoulCrystal => &mut self.armoury_soul_crystal,
|
|
|
|
ContainerType::ArmoryWeapon => &mut self.armoury_main_hand,
|
2025-07-13 08:14:35 -04:00
|
|
|
_ => todo!(),
|
2025-05-02 16:15:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_container(&self, container_type: &ContainerType) -> &dyn Storage {
|
|
|
|
match container_type {
|
|
|
|
ContainerType::Inventory0 => &self.pages[0],
|
|
|
|
ContainerType::Inventory1 => &self.pages[1],
|
|
|
|
ContainerType::Inventory2 => &self.pages[2],
|
|
|
|
ContainerType::Inventory3 => &self.pages[3],
|
|
|
|
ContainerType::Equipped => &self.equipped,
|
2025-06-24 19:15:25 -04:00
|
|
|
ContainerType::Currency => &self.currency,
|
2025-05-02 16:15:54 -04:00
|
|
|
ContainerType::ArmoryOffWeapon => &self.armoury_off_hand,
|
|
|
|
ContainerType::ArmoryHead => &self.armoury_head,
|
|
|
|
ContainerType::ArmoryBody => &self.armoury_body,
|
|
|
|
ContainerType::ArmoryHand => &self.armoury_hands,
|
|
|
|
ContainerType::ArmoryLeg => &self.armoury_legs,
|
|
|
|
ContainerType::ArmoryFoot => &self.armoury_feet,
|
|
|
|
ContainerType::ArmoryEarring => &self.armoury_earring,
|
|
|
|
ContainerType::ArmoryNeck => &self.armoury_necklace,
|
|
|
|
ContainerType::ArmoryWrist => &self.armoury_bracelet,
|
|
|
|
ContainerType::ArmoryRing => &self.armoury_rings,
|
|
|
|
ContainerType::ArmorySoulCrystal => &self.armoury_soul_crystal,
|
|
|
|
ContainerType::ArmoryWeapon => &self.armoury_main_hand,
|
2025-07-13 08:14:35 -04:00
|
|
|
_ => todo!(),
|
2025-05-02 16:15:54 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_main_weapon_id(&self, game_data: &mut GameData) -> u64 {
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.main_hand.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_model_ids(&self, game_data: &mut GameData) -> [u32; 10] {
|
|
|
|
[
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.head.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.body.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.hands.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.legs.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.feet.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.ears.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.neck.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.wrists.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.left_ring.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
game_data
|
2025-06-28 14:29:12 -04:00
|
|
|
.get_primary_model_id(self.equipped.right_ring.apparent_id())
|
2025-05-02 16:15:54 -04:00
|
|
|
.unwrap_or(0) as u32,
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|