Browse Source

Merge pull request 'misc shit' (#246) from cleaning_up into master

pbs
jake 4 years ago
parent
commit
898cb9064d
  1. 1
      Cargo.toml
  2. 4
      src/bin/login.rs
  3. 3
      src/bin/main.rs
  4. 3
      src/bin/ship.rs
  5. 2
      src/common/interserver.rs
  6. 2
      src/entity/gateway/entitygateway.rs
  7. 7
      src/entity/gateway/inmemory.rs
  8. 8
      src/entity/gateway/postgres/postgres.rs
  9. 56
      src/login/character.rs
  10. 20
      src/login/login.rs
  11. 112
      src/login_main.rs
  12. 44
      src/patch_main.rs
  13. 24
      src/ship/items/manager.rs
  14. 66
      src/ship/monster.rs
  15. 2
      src/ship/packet/handler/direct_message.rs
  16. 2
      src/ship/packet/handler/message.rs
  17. 5
      src/ship/packet/handler/room.rs
  18. 3
      src/ship/room.rs
  19. 95
      src/ship/ship.rs
  20. 8
      tests/common.rs
  21. 104
      tests/test_rooms.rs

1
Cargo.toml

@ -34,4 +34,5 @@ refinery = { version = "0.3.0", features = ["postgres"] }
sqlx = { version = "0.4.0-beta.1", features = ["postgres", "json", "chrono"] } sqlx = { version = "0.4.0-beta.1", features = ["postgres", "json", "chrono"] }
strum = "0.19.5" strum = "0.19.5"
strum_macros = "0.19" strum_macros = "0.19"
anyhow = "1.0.33"

4
src/bin/login.rs

@ -3,6 +3,7 @@ use elseware::entity::gateway::postgres::PostgresGateway;
use elseware::login::login::LoginServerState; use elseware::login::login::LoginServerState;
use elseware::login::character::CharacterServerState; use elseware::login::character::CharacterServerState;
use elseware::common::mainloop::{login_mainloop, character_mainloop}; use elseware::common::mainloop::{login_mainloop, character_mainloop};
use elseware::common::interserver::AuthToken;
fn main() { fn main() {
let colors = fern::colors::ColoredLevelConfig::new() let colors = fern::colors::ColoredLevelConfig::new()
@ -33,13 +34,14 @@ fn main() {
let db_password = std::env::var("DB_PASSWORD").unwrap(); let db_password = std::env::var("DB_PASSWORD").unwrap();
let db_dbname = std::env::var("DB_DBNAME").unwrap(); let db_dbname = std::env::var("DB_DBNAME").unwrap();
let charserv_ip = std::env::var("CHARSERV_IP").unwrap().parse().unwrap(); let charserv_ip = std::env::var("CHARSERV_IP").unwrap().parse().unwrap();
let shipgate_token = std::env::var("SHIPGATE_TOKEN").unwrap();
let entity_gateway = PostgresGateway::new(&db_host, &db_dbname, &db_username, &db_password); let entity_gateway = PostgresGateway::new(&db_host, &db_dbname, &db_username, &db_password);
let thread_entity_gateway = entity_gateway.clone(); let thread_entity_gateway = entity_gateway.clone();
let login_state = LoginServerState::new(thread_entity_gateway, charserv_ip); let login_state = LoginServerState::new(thread_entity_gateway, charserv_ip);
let login_loop = login_mainloop(login_state, elseware::login::login::LOGIN_PORT); let login_loop = login_mainloop(login_state, elseware::login::login::LOGIN_PORT);
let char_state = CharacterServerState::new(entity_gateway);
let char_state = CharacterServerState::new(entity_gateway, AuthToken(shipgate_token.into()));
let character_loop = character_mainloop(char_state, elseware::login::character::CHARACTER_PORT, elseware::login::login::COMMUNICATION_PORT); let character_loop = character_mainloop(char_state, elseware::login::character::CHARACTER_PORT, elseware::login::login::COMMUNICATION_PORT);
info!("[auth/character] starting server"); info!("[auth/character] starting server");

3
src/bin/main.rs

@ -9,6 +9,7 @@ use elseware::entity::account::{NewUserAccountEntity, NewUserSettingsEntity};
use elseware::entity::gateway::{EntityGateway, InMemoryGateway, PostgresGateway}; use elseware::entity::gateway::{EntityGateway, InMemoryGateway, PostgresGateway};
use elseware::entity::character::NewCharacterEntity; use elseware::entity::character::NewCharacterEntity;
use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation}; use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation};
use elseware::common::interserver::AuthToken;
use elseware::entity::item; use elseware::entity::item;
use elseware::common::mainloop::*; use elseware::common::mainloop::*;
@ -405,7 +406,7 @@ fn main() {
let thread_entity_gateway = entity_gateway.clone(); let thread_entity_gateway = entity_gateway.clone();
info!("[character] starting server"); info!("[character] starting server");
let char_state = CharacterServerState::new(thread_entity_gateway);
let char_state = CharacterServerState::new(thread_entity_gateway, AuthToken("".into()));
let character_loop = character_mainloop(char_state, elseware::login::character::CHARACTER_PORT, elseware::login::login::COMMUNICATION_PORT); let character_loop = character_mainloop(char_state, elseware::login::character::CHARACTER_PORT, elseware::login::login::COMMUNICATION_PORT);
let thread_entity_gateway = entity_gateway.clone(); let thread_entity_gateway = entity_gateway.clone();

3
src/bin/ship.rs

@ -2,6 +2,7 @@ use log::{info};
use elseware::entity::gateway::postgres::PostgresGateway; use elseware::entity::gateway::postgres::PostgresGateway;
use elseware::ship::ship::ShipServerStateBuilder; use elseware::ship::ship::ShipServerStateBuilder;
use elseware::common::mainloop::ship_mainloop; use elseware::common::mainloop::ship_mainloop;
use elseware::common::interserver::AuthToken;
fn main() { fn main() {
let colors = fern::colors::ColoredLevelConfig::new() let colors = fern::colors::ColoredLevelConfig::new()
@ -33,6 +34,7 @@ fn main() {
let db_dbname = std::env::var("DB_DBNAME").unwrap(); let db_dbname = std::env::var("DB_DBNAME").unwrap();
let entity_gateway = PostgresGateway::new(&db_host, &db_dbname, &db_username, &db_password); let entity_gateway = PostgresGateway::new(&db_host, &db_dbname, &db_username, &db_password);
let shipgate_token = std::env::var("SHIPGATE_TOKEN").unwrap();
let ship_name = std::env::var("SHIP_NAME").unwrap().parse().unwrap(); let ship_name = std::env::var("SHIP_NAME").unwrap().parse().unwrap();
let ip = std::env::var("SELF_IP").unwrap().parse().unwrap(); let ip = std::env::var("SELF_IP").unwrap().parse().unwrap();
let ship_state = ShipServerStateBuilder::new() let ship_state = ShipServerStateBuilder::new()
@ -40,6 +42,7 @@ fn main() {
.ip(ip) .ip(ip)
.port(elseware::ship::ship::SHIP_PORT) .port(elseware::ship::ship::SHIP_PORT)
.gateway(entity_gateway) .gateway(entity_gateway)
.auth_token(AuthToken(shipgate_token.into()))
.build(); .build();
let shipgate_ip = std::env::var("SHIPGATE_IP").unwrap().parse().unwrap(); let shipgate_ip = std::env::var("SHIPGATE_IP").unwrap().parse().unwrap();

2
src/common/interserver.rs

@ -5,7 +5,7 @@ use crate::entity::character::CharacterEntityId;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ServerId(pub usize); pub struct ServerId(pub usize);
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthToken(pub String); pub struct AuthToken(pub String);
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

2
src/entity/gateway/entitygateway.rs

@ -65,7 +65,7 @@ pub trait EntityGateway: Send + Sync + Clone {
unimplemented!(); unimplemented!();
} }
async fn save_item(&mut self, _item: &ItemEntity) -> Result<(), GatewayError> {
async fn change_item(&mut self, _id: &ItemEntityId, _item: &ItemDetail) -> Result<(), GatewayError> {
unimplemented!(); unimplemented!();
} }

7
src/entity/gateway/inmemory.rs

@ -165,9 +165,12 @@ impl EntityGateway for InMemoryGateway {
Ok(new_item) Ok(new_item)
} }
async fn save_item(&mut self, item: &ItemEntity) -> Result<(), GatewayError> {
async fn change_item(&mut self, id: &ItemEntityId, item: &ItemDetail) -> Result<(), GatewayError> {
let mut items = self.items.lock().unwrap(); let mut items = self.items.lock().unwrap();
items.insert(item.id, item.clone());
if let Some((_, ref mut old_item)) = items.iter_mut().find(|(existing_id, _)| **existing_id == *id) {
old_item.item = item.clone();
}
Ok(()) Ok(())
} }

8
src/entity/gateway/postgres/postgres.rs

@ -312,6 +312,14 @@ impl EntityGateway for PostgresGateway {
}) })
} }
async fn change_item(&mut self, id: &ItemEntityId, item: &ItemDetail) -> Result<(), GatewayError> {
sqlx::query("update item set item = $1 where id = $2")
.bind(sqlx::types::Json(PgItemDetail::from(item.clone())))
.bind(id.0)
.execute(&self.pool).await?;
Ok(())
}
async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) -> Result<(), GatewayError> { async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) -> Result<(), GatewayError> {
let mut tx = self.pool.begin().await?; let mut tx = self.pool.begin().await?;
if let ItemLocation::Inventory{slot, ..} = &item_location { if let ItemLocation::Inventory{slot, ..} = &item_location {

56
src/login/character.rs

@ -1,6 +1,6 @@
#![allow(dead_code, unused_assignments)] #![allow(dead_code, unused_assignments)]
use std::io::Read; use std::io::Read;
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use rand::Rng; use rand::Rng;
use crc::{crc32, Hasher32}; use crc::{crc32, Hasher32};
@ -28,11 +28,13 @@ use crate::entity::item::mag::Mag;
use crate::entity::character::{CharacterEntity, NewCharacterEntity, CharacterClass, TechLevel}; use crate::entity::character::{CharacterEntity, NewCharacterEntity, CharacterClass, TechLevel};
use crate::login::login::{get_login_status, check_if_already_online}; use crate::login::login::{get_login_status, check_if_already_online};
use crate::common::interserver::AuthToken;
pub const CHARACTER_PORT: u16 = 12001; pub const CHARACTER_PORT: u16 = 12001;
const SHIP_MENU_ID: u32 = 1; const SHIP_MENU_ID: u32 = 1;
#[derive(Debug)]
#[derive(thiserror::Error, Debug)]
#[error("")]
pub enum CharacterError { pub enum CharacterError {
InvalidMenuSelection(u32, u32), InvalidMenuSelection(u32, u32),
ClientNotFound(ClientId), ClientNotFound(ClientId),
@ -174,6 +176,8 @@ pub struct CharacterServerState<EG: EntityGateway> {
clients: HashMap<ClientId, ClientState>, clients: HashMap<ClientId, ClientState>,
ships: BTreeMap<ServerId, Ship>, ships: BTreeMap<ServerId, Ship>,
level_table: CharacterLevelTable, level_table: CharacterLevelTable,
auth_token: AuthToken,
authenticated_ships: BTreeSet<ServerId>,
} }
@ -266,7 +270,7 @@ async fn new_character<EG: EntityGateway>(entity_gateway: &mut EG, user: &UserAc
impl<EG: EntityGateway> CharacterServerState<EG> { impl<EG: EntityGateway> CharacterServerState<EG> {
pub fn new(entity_gateway: EG) -> CharacterServerState<EG> {
pub fn new(entity_gateway: EG, auth_token: AuthToken) -> CharacterServerState<EG> {
let (param_header, param_data) = generate_param_data("data/param/"); let (param_header, param_data) = generate_param_data("data/param/");
CharacterServerState { CharacterServerState {
@ -276,10 +280,12 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
clients: HashMap::new(), clients: HashMap::new(),
ships: BTreeMap::new(), ships: BTreeMap::new(),
level_table: CharacterLevelTable::new(), level_table: CharacterLevelTable::new(),
auth_token: auth_token,
authenticated_ships: BTreeSet::new(),
} }
} }
async fn validate_login(&mut self, id: ClientId, pkt: &Login) -> Result<Vec<SendCharacterPacket>, CharacterError> {
async fn validate_login(&mut self, id: ClientId, pkt: &Login) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
Ok(match get_login_status(&self.entity_gateway, pkt).await.and_then(check_if_already_online) { Ok(match get_login_status(&self.entity_gateway, pkt).await.and_then(check_if_already_online) {
Ok(mut user) => { Ok(mut user) => {
@ -299,7 +305,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
}) })
} }
fn send_ship_list(&mut self, _id: ClientId, _pkt: &Login) -> Result<Vec<SendCharacterPacket>, CharacterError> {
fn send_ship_list(&mut self, _id: ClientId, _pkt: &Login) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
Ok(vec![SendCharacterPacket::Timestamp(Timestamp::new(chrono::Utc::now())), Ok(vec![SendCharacterPacket::Timestamp(Timestamp::new(chrono::Utc::now())),
SendCharacterPacket::ShipList(ShipList::new(self.ships.iter().map(|(i, s)| { SendCharacterPacket::ShipList(ShipList::new(self.ships.iter().map(|(i, s)| {
ShipListEntry { ShipListEntry {
@ -312,7 +318,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
]) ])
} }
async fn get_settings(&mut self, id: ClientId) -> Result<Vec<SendCharacterPacket>, CharacterError> {
async fn get_settings(&mut self, id: ClientId) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
let user = client.user.as_ref().unwrap(); let user = client.user.as_ref().unwrap();
@ -331,7 +337,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
Ok(vec![pkt]) Ok(vec![pkt])
} }
async fn char_select(&mut self, id: ClientId, select: &CharSelect) -> Result<Vec<SendCharacterPacket>, CharacterError> {
async fn char_select(&mut self, id: ClientId, select: &CharSelect) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
if client.characters.is_none() { if client.characters.is_none() {
client.characters = Some(self.entity_gateway.get_characters_by_user(client.user.as_ref().unwrap()).await.map_err(|_| CharacterError::CouldNotLoadCharacters)?); client.characters = Some(self.entity_gateway.get_characters_by_user(client.user.as_ref().unwrap()).await.map_err(|_| CharacterError::CouldNotLoadCharacters)?);
@ -377,7 +383,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
})] })]
} }
async fn guildcard_data_header(&mut self, id: ClientId) -> Result<Vec<SendCharacterPacket>, CharacterError> {
async fn guildcard_data_header(&mut self, id: ClientId) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
let guildcard_data = self.entity_gateway.get_guild_card_data_by_user(client.user.as_ref().unwrap()).await.map_err(|_| CharacterError::CouldNotLoadGuildcard)?; let guildcard_data = self.entity_gateway.get_guild_card_data_by_user(client.user.as_ref().unwrap()).await.map_err(|_| CharacterError::CouldNotLoadGuildcard)?;
@ -389,7 +395,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
Ok(vec![SendCharacterPacket::GuildcardDataHeader(GuildcardDataHeader::new(bytes.len(), crc.sum32()))]) Ok(vec![SendCharacterPacket::GuildcardDataHeader(GuildcardDataHeader::new(bytes.len(), crc.sum32()))])
} }
fn guildcard_data_chunk(&mut self, id: ClientId, chunk: u32, again: u32) -> Result<Vec<SendCharacterPacket>, CharacterError> {
fn guildcard_data_chunk(&mut self, id: ClientId, chunk: u32, again: u32) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
Ok(if again != 0 { Ok(if again != 0 {
let start = chunk as usize * GUILD_CARD_CHUNK_SIZE; let start = chunk as usize * GUILD_CARD_CHUNK_SIZE;
@ -405,7 +411,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
}) })
} }
async fn set_flag(&mut self, id: ClientId, setflag: &SetFlag) -> Result<std::option::IntoIter<SendCharacterPacket>, CharacterError> {
async fn set_flag(&mut self, id: ClientId, setflag: &SetFlag) -> Result<std::option::IntoIter<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
let mut user = client.user.as_mut().unwrap(); let mut user = client.user.as_mut().unwrap();
user.flags = setflag.flags; user.flags = setflag.flags;
@ -413,7 +419,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
Ok(None.into_iter()) Ok(None.into_iter())
} }
fn param_data_chunk_request(&mut self, id: ClientId, _request: &ParamDataChunkRequest) -> Result<Vec<SendCharacterPacket>, CharacterError> {
fn param_data_chunk_request(&mut self, id: ClientId, _request: &ParamDataChunkRequest) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
let chunk = client.param_index; let chunk = client.param_index;
client.param_index += 1; client.param_index += 1;
@ -434,7 +440,7 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
// TODO: move USERFLAGS over to SessionAction // TODO: move USERFLAGS over to SessionAction
async fn character_preview(&mut self, id: ClientId, preview: &CharacterPreview) -> Result<Vec<SendCharacterPacket>, CharacterError> {
async fn character_preview(&mut self, id: ClientId, preview: &CharacterPreview) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?; let client = self.clients.get_mut(&id).ok_or(CharacterError::ClientNotFound(id))?;
let mut user = client.user.as_mut().unwrap(); let mut user = client.user.as_mut().unwrap();
if user.flags == USERFLAG_NEWCHAR { if user.flags == USERFLAG_NEWCHAR {
@ -458,9 +464,9 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
]) ])
} }
fn select_ship(&mut self, menuselect: &MenuSelect) -> Result<Vec<SendCharacterPacket>, CharacterError> {
fn select_ship(&mut self, menuselect: &MenuSelect) -> Result<Vec<SendCharacterPacket>, anyhow::Error> {
if menuselect.menu != SHIP_MENU_ID { if menuselect.menu != SHIP_MENU_ID {
return Err(CharacterError::InvalidMenuSelection(menuselect.menu, menuselect.item));
Err(CharacterError::InvalidMenuSelection(menuselect.menu, menuselect.item))?;
} }
let ship = self.ships.get(&ServerId(menuselect.item as usize)) let ship = self.ships.get(&ServerId(menuselect.item as usize))
@ -473,9 +479,9 @@ impl<EG: EntityGateway> CharacterServerState<EG> {
impl<EG: EntityGateway> ServerState for CharacterServerState<EG> { impl<EG: EntityGateway> ServerState for CharacterServerState<EG> {
type SendPacket = SendCharacterPacket; type SendPacket = SendCharacterPacket;
type RecvPacket = RecvCharacterPacket; type RecvPacket = RecvCharacterPacket;
type PacketError = CharacterError;
type PacketError = anyhow::Error;
async fn on_connect(&mut self, id: ClientId) -> Result<Vec<OnConnect<Self::SendPacket>>, CharacterError> {
async fn on_connect(&mut self, id: ClientId) -> Result<Vec<OnConnect<Self::SendPacket>>, anyhow::Error> {
self.clients.insert(id, ClientState::new()); self.clients.insert(id, ClientState::new());
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -492,7 +498,7 @@ impl<EG: EntityGateway> ServerState for CharacterServerState<EG> {
} }
async fn handle(&mut self, id: ClientId, pkt: &RecvCharacterPacket) async fn handle(&mut self, id: ClientId, pkt: &RecvCharacterPacket)
-> Result<Box<dyn Iterator<Item = (ClientId, SendCharacterPacket)> + Send>, CharacterError> {
-> Result<Box<dyn Iterator<Item = (ClientId, SendCharacterPacket)> + Send>, anyhow::Error> {
Ok(match pkt { Ok(match pkt {
RecvCharacterPacket::Login(login) => { RecvCharacterPacket::Login(login) => {
if login.session.action == SessionAction::SelectCharacter { if login.session.action == SessionAction::SelectCharacter {
@ -535,7 +541,7 @@ impl<EG: EntityGateway> ServerState for CharacterServerState<EG> {
}) })
} }
async fn on_disconnect(&mut self, id: ClientId) -> Result<Vec<(ClientId, SendCharacterPacket)>, CharacterError> {
async fn on_disconnect(&mut self, id: ClientId) -> Result<Vec<(ClientId, SendCharacterPacket)>, anyhow::Error> {
if let Some(client) = self.clients.remove(&id) { if let Some(client) = self.clients.remove(&id) {
if let Some(mut user) = client.user { if let Some(mut user) = client.user {
user.at_character= false; user.at_character= false;
@ -558,9 +564,15 @@ impl<EG: EntityGateway> InterserverActor for CharacterServerState<EG> {
async fn action(&mut self, id: ServerId, msg: Self::RecvMessage) -> Result<Vec<(ServerId, Self::SendMessage)>, Self::Error> { async fn action(&mut self, id: ServerId, msg: Self::RecvMessage) -> Result<Vec<(ServerId, Self::SendMessage)>, Self::Error> {
match msg { match msg {
ShipMessage::Authenticate(_auth_token) => {},
ShipMessage::Authenticate(auth_token) => {
if self.auth_token == auth_token {
self.authenticated_ships.insert(id);
}
},
ShipMessage::NewShip(new_ship) => { ShipMessage::NewShip(new_ship) => {
if self.authenticated_ships.contains(&id) {
self.ships.insert(id, new_ship); self.ships.insert(id, new_ship);
}
}, },
_ => {} _ => {}
} }
@ -681,7 +693,7 @@ mod test {
} }
} }
let mut server = CharacterServerState::new(TestData {});
let mut server = CharacterServerState::new(TestData {}, AuthToken("".into()));
let mut clientstate = ClientState::new(); let mut clientstate = ClientState::new();
clientstate.user = Some(UserAccountEntity { clientstate.user = Some(UserAccountEntity {
id: UserAccountId(1), id: UserAccountId(1),
@ -716,7 +728,7 @@ mod test {
#[derive(Clone)] #[derive(Clone)]
struct TestData; struct TestData;
impl EntityGateway for TestData {} impl EntityGateway for TestData {}
let mut server = CharacterServerState::new(TestData {});
let mut server = CharacterServerState::new(TestData {}, AuthToken("".into()));
let send = server.handle(ClientId(1), &RecvCharacterPacket::Checksum(Checksum {checksum: 1234, let send = server.handle(ClientId(1), &RecvCharacterPacket::Checksum(Checksum {checksum: 1234,
padding: 0, padding: 0,
})).await.unwrap().collect::<Vec<_>>(); })).await.unwrap().collect::<Vec<_>>();
@ -746,7 +758,7 @@ mod test {
at_ship: false, at_ship: false,
}); });
let mut server = CharacterServerState::new(test_data.clone());
let mut server = CharacterServerState::new(test_data.clone(), AuthToken("".into()));
server.clients.insert(ClientId(1), fake_user.clone()); server.clients.insert(ClientId(1), fake_user.clone());
let mut send = server.handle(ClientId(1), &RecvCharacterPacket::SetFlag(SetFlag {flags: 1})).await.unwrap().collect::<Vec<_>>(); let mut send = server.handle(ClientId(1), &RecvCharacterPacket::SetFlag(SetFlag {flags: 1})).await.unwrap().collect::<Vec<_>>();
assert!(test_data.get_user_by_id(UserAccountId(3)).await.unwrap().flags == 1); assert!(test_data.get_user_by_id(UserAccountId(3)).await.unwrap().flags == 1);

20
src/login/login.rs

@ -20,7 +20,8 @@ use crate::entity::account::{UserAccountEntity};
pub const LOGIN_PORT: u16 = 12000; pub const LOGIN_PORT: u16 = 12000;
pub const COMMUNICATION_PORT: u16 = 12123; pub const COMMUNICATION_PORT: u16 = 12123;
#[derive(Debug)]
#[derive(thiserror::Error, Debug)]
#[error("")]
pub enum LoginError { pub enum LoginError {
DbError DbError
} }
@ -62,11 +63,7 @@ pub async fn get_login_status(entity_gateway: &impl EntityGateway, pkt: &Login)
let username = array_to_utf8(pkt.username).map_err(|_err| AccountStatus::Error)?; let username = array_to_utf8(pkt.username).map_err(|_err| AccountStatus::Error)?;
let password = array_to_utf8(pkt.password).map_err(|_err| AccountStatus::Error)?; let password = array_to_utf8(pkt.password).map_err(|_err| AccountStatus::Error)?;
let user = entity_gateway.get_user_by_name(username).await.map_err(|_| AccountStatus::InvalidUser)?; let user = entity_gateway.get_user_by_name(username).await.map_err(|_| AccountStatus::InvalidUser)?;
/*if user.is_currently_online() {
return Err(AccountStatus::AlreadyOnline)
}*/
log::info!("user: {}, activated {}", user.username, user.activated);
if !user.activated { if !user.activated {
return Err(AccountStatus::PayUp) return Err(AccountStatus::PayUp)
} }
@ -110,7 +107,7 @@ impl<EG: EntityGateway> LoginServerState<EG> {
} }
} }
async fn validate_login(&mut self, id: ClientId, pkt: &Login) -> Result<Vec<SendLoginPacket>, LoginError> {
async fn validate_login(&mut self, id: ClientId, pkt: &Login) -> Result<Vec<SendLoginPacket>, anyhow::Error> {
match get_login_status(&self.entity_gateway, pkt).await.and_then(check_if_already_online) { match get_login_status(&self.entity_gateway, pkt).await.and_then(check_if_already_online) {
Ok(mut user) => { Ok(mut user) => {
user.at_login = true; user.at_login = true;
@ -133,9 +130,10 @@ impl<EG: EntityGateway> LoginServerState<EG> {
impl<EG: EntityGateway> ServerState for LoginServerState<EG> { impl<EG: EntityGateway> ServerState for LoginServerState<EG> {
type SendPacket = SendLoginPacket; type SendPacket = SendLoginPacket;
type RecvPacket = RecvLoginPacket; type RecvPacket = RecvLoginPacket;
type PacketError = LoginError;
//type PacketError = LoginError;
type PacketError = anyhow::Error;
async fn on_connect(&mut self, _id: ClientId) -> Result<Vec<OnConnect<Self::SendPacket>>, LoginError> {
async fn on_connect(&mut self, _id: ClientId) -> Result<Vec<OnConnect<Self::SendPacket>>, anyhow::Error> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut server_key = [0u8; 48]; let mut server_key = [0u8; 48];
@ -150,7 +148,7 @@ impl<EG: EntityGateway> ServerState for LoginServerState<EG> {
} }
async fn handle(&mut self, id: ClientId, pkt: &Self::RecvPacket) async fn handle(&mut self, id: ClientId, pkt: &Self::RecvPacket)
-> Result<Box<dyn Iterator<Item = (ClientId, Self::SendPacket)> + Send>, LoginError> {
-> Result<Box<dyn Iterator<Item = (ClientId, Self::SendPacket)> + Send>, anyhow::Error> {
Ok(match pkt { Ok(match pkt {
RecvLoginPacket::Login(login) => { RecvLoginPacket::Login(login) => {
Box::new(self.validate_login(id, login).await? Box::new(self.validate_login(id, login).await?
@ -162,7 +160,7 @@ impl<EG: EntityGateway> ServerState for LoginServerState<EG> {
}) })
} }
async fn on_disconnect(&mut self, id: ClientId) -> Result<Vec<(ClientId, SendLoginPacket)>, LoginError> {
async fn on_disconnect(&mut self, id: ClientId) -> Result<Vec<(ClientId, SendLoginPacket)>, anyhow::Error> {
if let Some(username) = self.clients.remove(&id) { if let Some(username) = self.clients.remove(&id) {
if let Ok(mut user) = self.entity_gateway.get_user_by_name(username).await { if let Ok(mut user) = self.entity_gateway.get_user_by_name(username).await {
user.at_login = false; user.at_login = false;
@ -231,7 +229,7 @@ mod test {
}) })
} }
async fn save_user(&mut self, user: &UserAccountEntity) -> Result<(), GatewayError> {
async fn save_user(&mut self, _user: &UserAccountEntity) -> Result<(), GatewayError> {
Ok(()) Ok(())
} }
}; };

112
src/login_main.rs

@ -1,112 +0,0 @@
#![feature(const_generics)]
mod common;
mod login;
mod entity;
use std::thread;
use std::collections::HashMap;
use bcrypt;
use libpso::character::settings;
use libpso::character::character as pso_character;
use libpso::character::guildcard;
use libpso::{utf8_to_array, utf8_to_utf16_array};
use entity::gateway::EntityGateway;
use entity::account::{UserAccount, UserSettings, GuildCardData};
use entity::character::Character;
use login::login::LoginServerState;
use login::character::CharacterServerState;
use std::time::SystemTime;
#[derive(Clone)]
struct LoginStubData {
users: HashMap<String, UserAccount>,
characters: [Option<Character> ;4],
}
impl LoginStubData {
fn new() -> LoginStubData {
let mut c = pso_character::Character::default();
c.name = utf8_to_utf16_array!("Test Char", 16);
let mut users = HashMap::new();
users.insert("hi".to_string(), UserAccount {
id: 1,
username: "hi".to_owned(),
password: bcrypt::hash("qwer", 5).unwrap(),
guildcard: None,
team_id: None,
banned: false,
muted_until: SystemTime::now(),
created_at: SystemTime::now(),
flags: 0,
});
LoginStubData {
users: users,
characters: [Some(Character {
id: 1,
slot: 0,
user_id: 1,
character: c,
}),
None, None, None]
}
}
}
impl EntityGateway for LoginStubData {
fn get_user_by_name(&self, username: String) -> Option<UserAccount> {
self.users.get(&username).map(|user| user.clone())
}
fn get_user_settings_by_user(&self, user: &UserAccount) -> Option<UserSettings> {
Some(UserSettings {
id: 0,
user_id: user.id,
settings: settings::UserSettings::default()
})
}
fn set_user(&mut self, user: &UserAccount) {
self.users.insert(user.username.clone(), user.clone());
}
fn get_characters_by_user(&self, _user: &UserAccount) -> [Option<Character>; 4] {
self.characters
}
fn set_character(&mut self, char: &Character) {
self.characters[char.slot as usize] = Some(char.clone());
}
fn get_guild_card_data_by_user(&self, user: &UserAccount) -> GuildCardData {
GuildCardData {
id: 1,
user_id: user.id,
guildcard: guildcard::GuildCardData::default(),
}
}
}
fn main() {
println!("[login+character] starting server");
let auth_thread = thread::spawn(|| {
let auth_state = LoginServerState::new(LoginStubData::new());
common::mainloop::mainloop(auth_state, login::login::LOGIN_PORT);
});
let char_thread = thread::spawn(|| {
let char_state = CharacterServerState::new(LoginStubData::new());
common::mainloop::mainloop(char_state, login::character::CHARACTER_PORT);
});
auth_thread.join().unwrap();
char_thread.join().unwrap();
}

44
src/patch_main.rs

@ -1,44 +0,0 @@
#![feature(const_generics)]
mod common;
mod patch;
use crate::patch::patch::{PatchServerState, PatchTreeIterItem, generate_patch_tree, load_config, load_motd};
fn main() {
println!("[patch] starting server");
let patch_config = load_config();
let patch_motd: String = load_motd();
if let Err(_) = std::fs::read_dir(patch_config.path.as_str()) {
println!("Patch directory {} does not exist. Attempting to create it...", patch_config.path.as_str());
if let Err(err) = std::fs::create_dir(patch_config.path.as_str()) {
panic!("Failed to create patch directory! \n{}", err);
}
}
let (patch_file_tree, patch_file_lookup) = generate_patch_tree(patch_config.path.as_str());
println!("[patch] files to patch:");
let mut indent = 0;
for item in patch_file_tree.flatten() {
match item {
PatchTreeIterItem::Directory(path) => {
let s = path.to_str().unwrap();
println!("{: >2$}\u{2517}\u{2500}\u{2500} {}", "", s, indent * 4);
indent += 1;
},
PatchTreeIterItem::File(path, id) => {
let s = path.to_str().unwrap();
println!("{: >3$}\u{2520}\u{2500}\u{2500} {} ({})", "", s, id, indent * 4);
},
PatchTreeIterItem::UpDirectory => {
indent -= 1;
}
}
}
let patch_state = PatchServerState::new(patch_file_tree, patch_file_lookup, patch_motd);
common::mainloop::mainloop(patch_state, patch_config.port);
println!("[patch] exiting...");
}

24
src/ship/items/manager.rs

@ -215,7 +215,7 @@ impl ItemManager {
let inventory = self.character_inventory.get_mut(&character.id).unwrap(); let inventory = self.character_inventory.get_mut(&character.id).unwrap();
inventory.initialize_item_ids(base_inventory_id); inventory.initialize_item_ids(base_inventory_id);
let base_bank_id = ((area_client.local_client.id() as u32) << 21) | 0x20000; let base_bank_id = ((area_client.local_client.id() as u32) << 21) | 0x20000;
let default_bank = self.character_bank.get_mut(&character.id);//.unwrap().get_mut(&BankName("".to_string()));
let default_bank = self.character_bank.get_mut(&character.id);
match default_bank { match default_bank {
Some(default_bank) => { Some(default_bank) => {
default_bank.initialize_item_ids(base_bank_id); default_bank.initialize_item_ids(base_bank_id);
@ -933,16 +933,8 @@ impl ItemManager {
character_id: character.id, character_id: character.id,
slot: slot, slot: slot,
equipped: true, equipped: true,
}).await;
entity_gateway.save_item(&ItemEntity{
id: inventory_item.entity_id,
location: ItemLocation::Inventory{
character_id: character.id,
slot: slot,
equipped: true,
},
item: inventory_item.item.clone(),
}).await;
}).await?;
entity_gateway.change_item(&inventory_item.entity_id, &inventory_item.item).await?;
Ok(()) Ok(())
} }
@ -968,15 +960,7 @@ impl ItemManager {
slot: slot, slot: slot,
equipped: false, equipped: false,
}).await; }).await;
entity_gateway.save_item(&ItemEntity{
id: inventory_item.entity_id,
location: ItemLocation::Inventory{
character_id: character.id,
slot: slot,
equipped: false,
},
item: inventory_item.item.clone(),
}).await;
entity_gateway.change_item(&inventory_item.entity_id, &inventory_item.item).await?;
Ok(()) Ok(())
} }

66
src/ship/monster.rs

@ -12,6 +12,7 @@ pub enum MonsterParseError {
UnknownMonster(String), UnknownMonster(String),
} }
pub struct MonsterStatError;
#[derive(Debug, Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq, enum_utils::FromStr, derive_more::Display)] #[derive(Debug, Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq, enum_utils::FromStr, derive_more::Display)]
pub enum MonsterType { pub enum MonsterType {
@ -175,40 +176,37 @@ fn load_battle_param(filename: &str) -> HashMap<MonsterType, MonsterStats> {
}).collect() }).collect()
} }
pub fn load_monster_stats_table(mode: &RoomMode) -> HashMap<MonsterType, MonsterStats> {
pub fn load_monster_stats_table(mode: &RoomMode) -> Result<HashMap<MonsterType, MonsterStats>, MonsterStatError> {
match mode { match mode {
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::Normal} => load_battle_param("ep1_multi_normal.toml"),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::Hard} => load_battle_param("ep1_multi_hard.toml"),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::VeryHard} => load_battle_param("ep1_multi_veryhard.toml"),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::Ultimate} => load_battle_param("ep1_multi_ultimate.toml"),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::Normal} => load_battle_param("ep2_multi_normal.toml"),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::Hard} => load_battle_param("ep2_multi_hard.toml"),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::VeryHard} => load_battle_param("ep2_multi_veryhard.toml"),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::Ultimate} => load_battle_param("ep2_multi_ultimate.toml"),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::Normal} => load_battle_param("ep4_multi_normal.toml"),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::Hard} => load_battle_param("ep4_multi_hard.toml"),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::VeryHard} => load_battle_param("ep4_multi_veryhard.toml"),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::Ultimate} => load_battle_param("ep4_multi_ultimate.toml"),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::Normal} => load_battle_param("ep1_solo_normal.toml"),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::Hard} => load_battle_param("ep1_solo_hard.toml"),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::VeryHard} => load_battle_param("ep1_solo_veryhard.toml"),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::Ultimate} => load_battle_param("ep1_solo_ultimate.toml"),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::Normal} => load_battle_param("ep2_solo_normal.toml"),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::Hard} => load_battle_param("ep2_solo_hard.toml"),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::VeryHard} => load_battle_param("ep2_solo_veryhard.toml"),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::Ultimate} => load_battle_param("ep2_solo_ultimate.toml"),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::Normal} => load_battle_param("ep4_solo_normal.toml"),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::Hard} => load_battle_param("ep4_solo_hard.toml"),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::VeryHard} => load_battle_param("ep4_solo_veryhard.toml"),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::Ultimate} => load_battle_param("ep4_solo_ultimate.toml"),
_ => panic!(),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::Normal} => Ok(load_battle_param("ep1_multi_normal.toml")),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::Hard} => Ok(load_battle_param("ep1_multi_hard.toml")),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::VeryHard} => Ok(load_battle_param("ep1_multi_veryhard.toml")),
RoomMode::Multi {episode: Episode::One, difficulty: Difficulty::Ultimate} => Ok(load_battle_param("ep1_multi_ultimate.toml")),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::Normal} => Ok(load_battle_param("ep2_multi_normal.toml")),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::Hard} => Ok(load_battle_param("ep2_multi_hard.toml")),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::VeryHard} => Ok(load_battle_param("ep2_multi_veryhard.toml")),
RoomMode::Multi {episode: Episode::Two, difficulty: Difficulty::Ultimate} => Ok(load_battle_param("ep2_multi_ultimate.toml")),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::Normal} => Ok(load_battle_param("ep4_multi_normal.toml")),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::Hard} => Ok(load_battle_param("ep4_multi_hard.toml")),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::VeryHard} => Ok(load_battle_param("ep4_multi_veryhard.toml")),
RoomMode::Multi {episode: Episode::Four, difficulty: Difficulty::Ultimate} => Ok(load_battle_param("ep4_multi_ultimate.toml")),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::Normal} => Ok(load_battle_param("ep1_solo_normal.toml")),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::Hard} => Ok(load_battle_param("ep1_solo_hard.toml")),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::VeryHard} => Ok(load_battle_param("ep1_solo_veryhard.toml")),
RoomMode::Single {episode: Episode::One, difficulty: Difficulty::Ultimate} => Ok(load_battle_param("ep1_solo_ultimate.toml")),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::Normal} => Ok(load_battle_param("ep2_solo_normal.toml")),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::Hard} => Ok(load_battle_param("ep2_solo_hard.toml")),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::VeryHard} => Ok(load_battle_param("ep2_solo_veryhard.toml")),
RoomMode::Single {episode: Episode::Two, difficulty: Difficulty::Ultimate} => Ok(load_battle_param("ep2_solo_ultimate.toml")),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::Normal} => Ok(load_battle_param("ep4_solo_normal.toml")),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::Hard} => Ok(load_battle_param("ep4_solo_hard.toml")),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::VeryHard} => Ok(load_battle_param("ep4_solo_veryhard.toml")),
RoomMode::Single {episode: Episode::Four, difficulty: Difficulty::Ultimate} => Ok(load_battle_param("ep4_solo_ultimate.toml")),
_ => Err(MonsterStatError),
} }
} }

2
src/ship/packet/handler/direct_message.rs

@ -85,7 +85,6 @@ where
let client_and_drop = clients_in_area.into_iter() let client_and_drop = clients_in_area.into_iter()
.filter_map(|area_client| { .filter_map(|area_client| {
room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| { room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| {
warn!("drop is? {:?}", item_drop_type);
(area_client, item_drop_type) (area_client, item_drop_type)
}) })
}); });
@ -183,7 +182,6 @@ EG: EntityGateway
let client_and_drop = clients_in_area.into_iter() let client_and_drop = clients_in_area.into_iter()
.filter_map(|area_client| { .filter_map(|area_client| {
room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| { room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| {
warn!("drop is? {:?}", item_drop_type);
(area_client, item_drop_type) (area_client, item_drop_type)
}) })
}); });

2
src/ship/packet/handler/message.rs

@ -26,7 +26,7 @@ pub async fn request_exp<EG: EntityGateway>(id: ClientId,
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;
let monster = room.maps.enemy_by_id(request_exp.enemy_id as usize)?; let monster = room.maps.enemy_by_id(request_exp.enemy_id as usize)?;
let monster_stats = room.monster_stats.get(&monster.monster).unwrap();
let monster_stats = room.monster_stats.get(&monster.monster).ok_or(ShipError::UnknownMonster(monster.monster.clone()))?;
let exp_gain = if request_exp.last_hitter == 1 { let exp_gain = if request_exp.last_hitter == 1 {
monster_stats.exp monster_stats.exp

5
src/ship/packet/handler/room.rs

@ -77,6 +77,9 @@ pub fn join_room(id: ClientId,
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?; let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?;
item_manager.add_character_to_room(room_id, &client.character, area_client);
let leader = client_location.get_room_leader(room_id).map_err(|err| -> ClientLocationError { err.into() })?; let leader = client_location.get_room_leader(room_id).map_err(|err| -> ClientLocationError { err.into() })?;
let join_room = builder::room::join_room(id, clients, client_location, room_id, &room)?; let join_room = builder::room::join_room(id, clients, client_location, room_id, &room)?;
let add_to = builder::room::add_to_room(id, &client, &area_client, &leader, item_manager, level_table, room_id)?; let add_to = builder::room::add_to_room(id, &client, &area_client, &leader, item_manager, level_table, room_id)?;
@ -84,8 +87,6 @@ pub fn join_room(id: ClientId,
let room = rooms.get_mut(room_id.0).unwrap().as_mut().unwrap(); let room = rooms.get_mut(room_id.0).unwrap().as_mut().unwrap();
room.bursting = true; room.bursting = true;
item_manager.add_character_to_room(room_id, &client.character, area_client);
let mut result: Box<dyn Iterator<Item=(ClientId, SendShipPacket)> + Send> = Box::new( let mut result: Box<dyn Iterator<Item=(ClientId, SendShipPacket)> + Send> = Box::new(
vec![(id, SendShipPacket::JoinRoom(join_room))] vec![(id, SendShipPacket::JoinRoom(join_room))]
.into_iter() .into_iter()

3
src/ship/room.rs

@ -13,6 +13,7 @@ pub enum RoomCreationError {
InvalidMode, InvalidMode,
InvalidEpisode(u8), InvalidEpisode(u8),
InvalidDifficulty(u8), InvalidDifficulty(u8),
CouldNotLoadMonsterStats(RoomMode),
} }
@ -231,7 +232,7 @@ impl RoomState {
}; };
Ok(RoomState { Ok(RoomState {
monster_stats: Box::new(load_monster_stats_table(&room_mode)),
monster_stats: Box::new(load_monster_stats_table(&room_mode).map_err(|_| RoomCreationError::CouldNotLoadMonsterStats(room_mode.clone()))?),
mode: room_mode, mode: room_mode,
random_seed: rand::thread_rng().gen(), random_seed: rand::thread_rng().gen(),
name: String::from_utf16_lossy(&create_room.name).trim_matches(char::from(0)).into(), name: String::from_utf16_lossy(&create_room.name).trim_matches(char::from(0)).into(),

95
src/ship/ship.rs

@ -22,7 +22,7 @@ use crate::entity::gateway::{EntityGateway, GatewayError};
use crate::entity::account::{UserAccountEntity, UserSettingsEntity}; use crate::entity::account::{UserAccountEntity, UserSettingsEntity};
use crate::entity::character::{CharacterEntity, SectionID}; use crate::entity::character::{CharacterEntity, SectionID};
use crate::ship::location::{ClientLocation, RoomLobby, MAX_ROOMS, ClientLocationError};
use crate::ship::location::{ClientLocation, RoomLobby, MAX_ROOMS, ClientLocationError, GetNeighborError, GetClientsError, GetAreaError};
use crate::ship::items; use crate::ship::items;
use crate::ship::room; use crate::ship::room;
@ -45,6 +45,9 @@ pub enum ShipError {
InvalidSlot(ClientId, u32), InvalidSlot(ClientId, u32),
TooManyClients, TooManyClients,
ClientLocationError(#[from] ClientLocationError), ClientLocationError(#[from] ClientLocationError),
GetNeighborError(#[from] GetNeighborError),
GetClientsError(#[from] GetClientsError),
GetAreaError(#[from] GetAreaError),
MapsError(#[from] MapsError), MapsError(#[from] MapsError),
MapAreaError(#[from] MapAreaError), MapAreaError(#[from] MapAreaError),
InvalidRoom(u32), InvalidRoom(u32),
@ -63,6 +66,7 @@ pub enum ShipError {
NotEnoughMeseta(ClientId, u32), NotEnoughMeseta(ClientId, u32),
ShopError, ShopError,
GatewayError(#[from] GatewayError), GatewayError(#[from] GatewayError),
UnknownMonster(crate::ship::monster::MonsterType),
} }
#[derive(Debug)] #[derive(Debug)]
@ -294,6 +298,7 @@ pub struct ShipServerStateBuilder<EG: EntityGateway> {
name: Option<String>, name: Option<String>,
ip: Option<Ipv4Addr>, ip: Option<Ipv4Addr>,
port: Option<u16>, port: Option<u16>,
auth_token: Option<AuthToken>,
} }
impl<EG: EntityGateway> ShipServerStateBuilder<EG> { impl<EG: EntityGateway> ShipServerStateBuilder<EG> {
@ -303,6 +308,7 @@ impl<EG: EntityGateway> ShipServerStateBuilder<EG> {
name: None, name: None,
ip: None, ip: None,
port: None, port: None,
auth_token: None,
} }
} }
@ -326,6 +332,11 @@ impl<EG: EntityGateway> ShipServerStateBuilder<EG> {
self self
} }
pub fn auth_token(mut self, auth_token: AuthToken) -> ShipServerStateBuilder<EG> {
self.auth_token = Some(auth_token);
self
}
pub fn build(self) -> ShipServerState<EG> { pub fn build(self) -> ShipServerState<EG> {
ShipServerState { ShipServerState {
entity_gateway: self.entity_gateway.unwrap(), entity_gateway: self.entity_gateway.unwrap(),
@ -339,6 +350,7 @@ impl<EG: EntityGateway> ShipServerStateBuilder<EG> {
ip: self.ip.unwrap_or(Ipv4Addr::new(127,0,0,1)), ip: self.ip.unwrap_or(Ipv4Addr::new(127,0,0,1)),
port: self.port.unwrap_or(SHIP_PORT), port: self.port.unwrap_or(SHIP_PORT),
shops: Box::new(ItemShops::new()), shops: Box::new(ItemShops::new()),
auth_token: self.auth_token.unwrap_or(AuthToken("".into())),
} }
} }
} }
@ -355,6 +367,7 @@ pub struct ShipServerState<EG: EntityGateway> {
ip: Ipv4Addr, ip: Ipv4Addr,
port: u16, port: u16,
shops: Box<ItemShops>, shops: Box<ItemShops>,
auth_token: AuthToken,
} }
impl<EG: EntityGateway> ShipServerState<EG> { impl<EG: EntityGateway> ShipServerState<EG> {
@ -362,93 +375,93 @@ impl<EG: EntityGateway> ShipServerState<EG> {
ShipServerStateBuilder::new() ShipServerStateBuilder::new()
} }
async fn message(&mut self, id: ClientId, msg: &Message) -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
match &msg.msg {
async fn message(&mut self, id: ClientId, msg: &Message) -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error> {
Ok(match &msg.msg {
GameMessage::RequestExp(request_exp) => { GameMessage::RequestExp(request_exp) => {
handler::message::request_exp(id, request_exp, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.rooms, &self.level_table).await
handler::message::request_exp(id, request_exp, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.rooms, &self.level_table).await?
}, },
GameMessage::PlayerDropItem(player_drop_item) => { GameMessage::PlayerDropItem(player_drop_item) => {
handler::message::player_drop_item(id, player_drop_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await
handler::message::player_drop_item(id, player_drop_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await?
}, },
GameMessage::DropCoordinates(drop_coordinates) => { GameMessage::DropCoordinates(drop_coordinates) => {
handler::message::drop_coordinates(id, drop_coordinates, &self.client_location, &mut self.clients, &self.rooms)
handler::message::drop_coordinates(id, drop_coordinates, &self.client_location, &mut self.clients, &self.rooms)?
}, },
GameMessage::PlayerNoLongerHasItem(no_longer_has_item) => { GameMessage::PlayerNoLongerHasItem(no_longer_has_item) => {
handler::message::split_item_stack(id, no_longer_has_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await
handler::message::split_item_stack(id, no_longer_has_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::PlayerChangedMap(_) | GameMessage::PlayerChangedMap2(_) | GameMessage::TellOtherPlayerMyLocation(_) | GameMessage::PlayerChangedMap(_) | GameMessage::PlayerChangedMap2(_) | GameMessage::TellOtherPlayerMyLocation(_) |
GameMessage::PlayerWarpingToFloor(_) | GameMessage::PlayerTeleported(_) | GameMessage::PlayerStopped(_) | GameMessage::PlayerWarpingToFloor(_) | GameMessage::PlayerTeleported(_) | GameMessage::PlayerStopped(_) |
GameMessage::PlayerLoadedIn(_) | GameMessage::PlayerWalking(_) | GameMessage::PlayerRunning(_) | GameMessage::PlayerLoadedIn(_) | GameMessage::PlayerWalking(_) | GameMessage::PlayerRunning(_) |
GameMessage::PlayerWarped(_) | GameMessage::PlayerChangedFloor(_) | GameMessage::InitializeSpeechNpc(_) => { GameMessage::PlayerWarped(_) | GameMessage::PlayerChangedFloor(_) | GameMessage::InitializeSpeechNpc(_) => {
handler::message::update_player_position(id, &msg, &mut self.clients, &mut self.client_location, &self.rooms)
handler::message::update_player_position(id, &msg, &mut self.clients, &mut self.client_location, &self.rooms)?
}, },
GameMessage::ChargeAttack(charge_attack) => { GameMessage::ChargeAttack(charge_attack) => {
handler::message::charge_attack(id, charge_attack, &mut self.clients, &mut self.entity_gateway).await
handler::message::charge_attack(id, charge_attack, &mut self.clients, &mut self.entity_gateway).await?
}, },
GameMessage::PlayerUseItem(player_use_item) => { GameMessage::PlayerUseItem(player_use_item) => {
handler::message::use_item(id, player_use_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await
handler::message::use_item(id, player_use_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::PlayerUsedMedicalCenter(player_used_medical_center) => { GameMessage::PlayerUsedMedicalCenter(player_used_medical_center) => {
handler::message::player_used_medical_center(id, &player_used_medical_center, &mut self.entity_gateway, &mut self.clients).await
handler::message::player_used_medical_center(id, &player_used_medical_center, &mut self.entity_gateway, &mut self.clients).await?
}, },
GameMessage::PlayerFeedMag(player_feed_mag) => { GameMessage::PlayerFeedMag(player_feed_mag) => {
handler::message::player_feed_mag(id, &player_feed_mag, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await
handler::message::player_feed_mag(id, &player_feed_mag, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::PlayerEquipItem(player_equip_item) => { GameMessage::PlayerEquipItem(player_equip_item) => {
handler::message::player_equips_item(id, &player_equip_item, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await
handler::message::player_equips_item(id, &player_equip_item, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::PlayerUnequipItem(player_unequip_item) => { GameMessage::PlayerUnequipItem(player_unequip_item) => {
handler::message::player_unequips_item(id, &player_unequip_item, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await
handler::message::player_unequips_item(id, &player_unequip_item, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::SortItems(sort_items) => { GameMessage::SortItems(sort_items) => {
handler::message::player_sorts_items(id, sort_items, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await
handler::message::player_sorts_items(id, sort_items, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await?
}, },
_ => { _ => {
let cmsg = msg.clone(); let cmsg = msg.clone();
Ok(Box::new(self.client_location.get_client_neighbors(id).unwrap().into_iter()
Box::new(self.client_location.get_client_neighbors(id).unwrap().into_iter()
.map(move |client| { .map(move |client| {
(client.client, SendShipPacket::Message(cmsg.clone())) (client.client, SendShipPacket::Message(cmsg.clone()))
})))
}))
}, },
}
})
} }
async fn direct_message(&mut self, id: ClientId, msg: &DirectMessage) -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
async fn direct_message(&mut self, id: ClientId, msg: &DirectMessage) -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error> {
let target = msg.flag; let target = msg.flag;
match &msg.msg {
Ok(match &msg.msg {
GameMessage::GuildcardSend(guildcard_send) => { GameMessage::GuildcardSend(guildcard_send) => {
Ok(handler::direct_message::guildcard_send(id, guildcard_send, target, &self.client_location, &self.clients))
handler::direct_message::guildcard_send(id, guildcard_send, target, &self.client_location, &self.clients)
}, },
GameMessage::RequestItem(request_item) => { GameMessage::RequestItem(request_item) => {
handler::direct_message::request_item(id, request_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await
handler::direct_message::request_item(id, request_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await?
}, },
GameMessage::PickupItem(pickup_item) => { GameMessage::PickupItem(pickup_item) => {
handler::direct_message::pickup_item(id, pickup_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await
handler::direct_message::pickup_item(id, pickup_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::BoxDropRequest(box_drop_request) => { GameMessage::BoxDropRequest(box_drop_request) => {
handler::direct_message::request_box_item(id, box_drop_request, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await
handler::direct_message::request_box_item(id, box_drop_request, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await?
}, },
GameMessage::BankRequest(_bank_request) => { GameMessage::BankRequest(_bank_request) => {
handler::direct_message::send_bank_list(id, &self.clients, &mut self.item_manager).await
handler::direct_message::send_bank_list(id, &self.clients, &mut self.item_manager).await?
}, },
GameMessage::BankInteraction(bank_interaction) => { GameMessage::BankInteraction(bank_interaction) => {
handler::direct_message::bank_interaction(id, bank_interaction, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await
handler::direct_message::bank_interaction(id, bank_interaction, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await?
}, },
GameMessage::ShopRequest(shop_request) => { GameMessage::ShopRequest(shop_request) => {
handler::direct_message::shop_request(id, shop_request, &self.client_location, &mut self.clients, &self.rooms, &self.level_table, &mut self.shops).await
handler::direct_message::shop_request(id, shop_request, &self.client_location, &mut self.clients, &self.rooms, &self.level_table, &mut self.shops).await?
}, },
GameMessage::BuyItem(buy_item) => { GameMessage::BuyItem(buy_item) => {
handler::direct_message::buy_item(id, buy_item, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await
handler::direct_message::buy_item(id, buy_item, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await?
}, },
_ => { _ => {
let cmsg = msg.clone(); let cmsg = msg.clone();
Ok(Box::new(self.client_location.get_all_clients_by_client(id).unwrap().into_iter()
Box::new(self.client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8) .filter(move |client| client.local_client.id() == target as u8)
.map(move |client| { .map(move |client| {
(client.client, SendShipPacket::DirectMessage(cmsg.clone())) (client.client, SendShipPacket::DirectMessage(cmsg.clone()))
})))
}))
}, },
}
})
} }
} }
@ -456,9 +469,9 @@ impl<EG: EntityGateway> ShipServerState<EG> {
impl<EG: EntityGateway> ServerState for ShipServerState<EG> { impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
type SendPacket = SendShipPacket; type SendPacket = SendShipPacket;
type RecvPacket = RecvShipPacket; type RecvPacket = RecvShipPacket;
type PacketError = ShipError;
type PacketError = anyhow::Error;
async fn on_connect(&mut self, _id: ClientId) -> Result<Vec<OnConnect<Self::SendPacket>>, ShipError> {
async fn on_connect(&mut self, _id: ClientId) -> Result<Vec<OnConnect<Self::SendPacket>>, anyhow::Error> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut server_key = [0u8; 48]; let mut server_key = [0u8; 48];
@ -473,7 +486,7 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
} }
async fn handle(&mut self, id: ClientId, pkt: &RecvShipPacket) async fn handle(&mut self, id: ClientId, pkt: &RecvShipPacket)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error> {
Ok(match pkt { Ok(match pkt {
RecvShipPacket::Login(login) => { RecvShipPacket::Login(login) => {
Box::new(handler::auth::validate_login(id, login, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager, &self.name).await?.into_iter().map(move |pkt| (id, pkt))) Box::new(handler::auth::validate_login(id, login, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager, &self.name).await?.into_iter().map(move |pkt| (id, pkt)))
@ -580,13 +593,13 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
}) })
} }
async fn on_disconnect(&mut self, id: ClientId) -> Result<Vec<(ClientId, SendShipPacket)>, ShipError> {
async fn on_disconnect(&mut self, id: ClientId) -> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error> {
// TODO: don't unwrap! // TODO: don't unwrap!
let client = self.clients.get(&id).unwrap();
let area_client = self.client_location.get_local_client(id).unwrap();
let neighbors = self.client_location.get_client_neighbors(id).unwrap();
let client = self.clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
let area_client = self.client_location.get_local_client(id)?;
let neighbors = self.client_location.get_client_neighbors(id)?;
let pkt = match self.client_location.get_area(id).unwrap() {
let pkt = match self.client_location.get_area(id)? {
RoomLobby::Room(room) => { RoomLobby::Room(room) => {
if neighbors.len() == 0 { if neighbors.len() == 0 {
self.rooms[room.0] = None; self.rooms[room.0] = None;
@ -622,7 +635,9 @@ impl<EG: EntityGateway> InterserverActor for ShipServerState<EG> {
type Error = (); type Error = ();
async fn on_connect(&mut self, id: ServerId) -> Vec<(ServerId, Self::SendMessage)> { async fn on_connect(&mut self, id: ServerId) -> Vec<(ServerId, Self::SendMessage)> {
vec![ /* ShipMessage::Authenticate(AuthToken("hi".into())), */ (id, ShipMessage::NewShip(Ship {
vec![
(id, ShipMessage::Authenticate(self.auth_token.clone())),
(id, ShipMessage::NewShip(Ship {
name: self.name.clone(), name: self.name.clone(),
ip: self.ip.clone(), ip: self.ip.clone(),
port: self.port, port: self.port,

8
tests/common.rs

@ -59,6 +59,13 @@ pub async fn create_room<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id:
create_room_with_difficulty(ship, id, name, password, Difficulty::Normal).await; create_room_with_difficulty(ship, id, name, password, Difficulty::Normal).await;
} }
pub async fn leave_room<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId) {
ship.handle(id, &RecvShipPacket::LobbySelect(LobbySelect {
menu: 3,
lobby: 0,
})).await.unwrap().for_each(drop);
}
pub async fn create_room_with_difficulty<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, name: &str, password: &str, difficulty: Difficulty) { pub async fn create_room_with_difficulty<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, name: &str, password: &str, difficulty: Difficulty) {
ship.handle(id, &RecvShipPacket::CreateRoom(CreateRoom { ship.handle(id, &RecvShipPacket::CreateRoom(CreateRoom {
unknown: [0; 2], unknown: [0; 2],
@ -79,4 +86,5 @@ pub async fn join_room<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: Cl
menu: ROOM_MENU_ID, menu: ROOM_MENU_ID,
item: room_id, item: room_id,
})).await.unwrap().for_each(drop); })).await.unwrap().for_each(drop);
ship.handle(id, &RecvShipPacket::DoneBursting(DoneBursting {})).await.unwrap().for_each(drop);
} }

104
tests/test_rooms.rs

@ -0,0 +1,104 @@
use elseware::common::serverstate::{ClientId, ServerState};
use elseware::entity::gateway::{EntityGateway, InMemoryGateway};
use elseware::entity::item;
use elseware::ship::ship::{ShipServerState, RecvShipPacket, SendShipPacket};
use libpso::packet::ship::*;
use libpso::packet::messages::*;
#[path = "common.rs"]
mod common;
use common::*;
#[async_std::test]
async fn test_item_ids_reset_when_rejoining_rooms() {
let mut entity_gateway = InMemoryGateway::new();
let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
for slot in 0..3 {
entity_gateway.create_item(
item::NewItemEntity {
item: item::ItemDetail::Weapon(
item::weapon::Weapon {
weapon: item::weapon::WeaponType::Saber,
grind: 0,
special: None,
attrs: [None, None, None],
tekked: true,
modifiers: Vec::new(),
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
slot: slot,
equipped: false,
}
}).await.unwrap();
}
for slot in 0..10 {
entity_gateway.create_item(
item::NewItemEntity {
item: item::ItemDetail::Weapon(
item::weapon::Weapon {
weapon: item::weapon::WeaponType::Saber,
grind: 0,
special: None,
attrs: [None, None, None],
tekked: true,
modifiers: Vec::new(),
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
slot: slot,
equipped: false,
}
}).await.unwrap();
}
let mut ship = ShipServerState::builder()
.gateway(entity_gateway.clone())
.build();
log_in_char(&mut ship, ClientId(1), "a1", "a").await;
log_in_char(&mut ship, ClientId(2), "a2", "a").await;
join_lobby(&mut ship, ClientId(1)).await;
join_lobby(&mut ship, ClientId(2)).await;
create_room(&mut ship, ClientId(1), "room", "").await;
let p = ship.handle(ClientId(2), &RecvShipPacket::MenuSelect(MenuSelect {
menu: ROOM_MENU_ID,
item: 0,
})).await.unwrap().collect::<Vec<_>>();
ship.handle(ClientId(2), &RecvShipPacket::DoneBursting(DoneBursting {})).await.unwrap().for_each(drop);
match &p[1].1 {
SendShipPacket::AddToRoom(add_to) => {
println!("addto {:?}", add_to);
assert_eq!(add_to.playerinfo.inventory.items.iter().map(|k| k.item_id).collect::<Vec<_>>(),
vec![0x210000,0x210001,0x210002,0x210003,0x210004,0x210005,0x210006,0x210007,0x210008,0x210009,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
},
_ => panic!(),
}
leave_room(&mut ship, ClientId(2)).await;
let p = ship.handle(ClientId(2), &RecvShipPacket::MenuSelect(MenuSelect {
menu: ROOM_MENU_ID,
item: 0,
})).await.unwrap().collect::<Vec<_>>();
match &p[1].1 {
SendShipPacket::AddToRoom(add_to) => {
assert_eq!(add_to.playerinfo.inventory.items.iter().map(|k| k.item_id).collect::<Vec<_>>(),
vec![0x210000,0x210001,0x210002,0x210003,0x210004,0x210005,0x210006,0x210007,0x210008,0x210009,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
},
_ => panic!(),
}
}
Loading…
Cancel
Save