use chrono::{DateTime, Utc}; use psopacket::{pso_packet, PSOPacketData}; use crate::{PSOPacket, PacketParseError, PSOPacketData, utf8_to_utf16_array}; use crate::character::character::SelectScreenCharacter; use std::io::Read; pub const GUILD_CARD_CHUNK_SIZE: usize = 0x6800; pub const PARAM_DATA_CHUNK_SIZE: usize = 0x6800; #[pso_packet(0x03)] pub struct LoginWelcome { #[utf8] copyright: [u8; 0x60], server_key: [u8; 48], client_key: [u8; 48], } impl LoginWelcome { pub fn new(server_key: [u8; 48], client_key: [u8; 48]) -> LoginWelcome { let mut copyright = [0u8; 0x60]; copyright[..0x4B].clone_from_slice(b"Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM."); LoginWelcome { copyright: copyright, server_key: server_key, client_key: client_key, } } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum SessionAction { None, SelectCharacter, NewCharacter, DressingRoom, } impl PSOPacketData for SessionAction { fn from_bytes(cursor: &mut R) -> Result { let mut bytes = [0u8; 1]; let len = cursor.read(&mut bytes).map_err(|_| PacketParseError::ReadError)?; if len != 1 { return Err(PacketParseError::NotEnoughBytes) } match bytes[0] { 0 => Ok(SessionAction::None), 1 => Ok(SessionAction::SelectCharacter), 2 => Ok(SessionAction::NewCharacter), 3 => Ok(SessionAction::DressingRoom), _ => Err(PacketParseError::InvalidValue) } } fn as_bytes(&self) -> Vec { vec![match self { SessionAction::None => 0, SessionAction::SelectCharacter => 1, SessionAction::NewCharacter => 2, SessionAction::DressingRoom => 3, }] } } #[derive(PSOPacketData, Clone, Copy)] pub struct Session { pub version: [u8; 30], pub session_id: u32, pub interserver_checksum: u32, pub action: SessionAction, pub character_slot: u8, // 1..=4 } impl Session { pub fn new() -> Session { Session { version: [0; 30], session_id: 0, interserver_checksum: 0, action: SessionAction::None, character_slot: 0, } } } #[pso_packet(0x93)] pub struct Login { pub tag: u32, pub guildcard: u32, pub version: u16, pub unknown1: [u8; 6], pub team: u32, #[utf8] pub username: [u8; 16], pub unknown2: [u8; 32], #[utf8] pub password: [u8; 16], pub unknown3: [u8; 40], pub hwinfo: [u8; 8], pub session: Session //pub security_data: [u8; 40], } #[derive(Debug, Clone, PartialEq)] pub enum AccountStatus { Ok, Error, InvalidPassword, InvalidPassword2, Maintenance, AlreadyOnline, Banned, Banned2, InvalidUser, PayUp, Locked, BadVersion, } impl PSOPacketData for AccountStatus { fn from_bytes(cursor: &mut R) -> Result { let mut bytes = [0u8; 4]; let len = cursor.read(&mut bytes).map_err(|_| PacketParseError::ReadError)?; if len != 4 { return Err(PacketParseError::NotEnoughBytes) } match bytes[0] { 0 => Ok(AccountStatus::Ok), 1 => Ok(AccountStatus::Error), 2 => Ok(AccountStatus::InvalidPassword), 3 => Ok(AccountStatus::InvalidPassword2), 4 => Ok(AccountStatus::Maintenance), 5 => Ok(AccountStatus::AlreadyOnline), 6 => Ok(AccountStatus::Banned), 7 => Ok(AccountStatus::Banned2), 8 => Ok(AccountStatus::InvalidUser), 9 => Ok(AccountStatus::PayUp), 10 => Ok(AccountStatus::Locked), 11 => Ok(AccountStatus::BadVersion), _ => Err(PacketParseError::InvalidValue), } } fn as_bytes(&self) -> Vec { vec![match self { AccountStatus::Ok => 0, AccountStatus::Error => 1, AccountStatus::InvalidPassword => 2, AccountStatus::InvalidPassword2 => 3, AccountStatus::Maintenance => 4, AccountStatus::AlreadyOnline => 5, AccountStatus::Banned => 6, AccountStatus::Banned2 => 7, AccountStatus::InvalidUser => 8, AccountStatus::PayUp => 9, AccountStatus::Locked => 10, AccountStatus::BadVersion => 11, },0,0,0] } } #[pso_packet(0xE6)] pub struct LoginResponse { pub status: AccountStatus, pub tag: u32, pub guildcard: u32, pub team_id: u32, //pub security_data: [u8; 40], pub session: Session, pub caps: u32, } impl LoginResponse { pub fn by_status(status: AccountStatus, session: Session) -> LoginResponse { LoginResponse { status: status, tag: 0x00010000, //tag: 0x00000100, guildcard: 0, team_id: 0, session: session, caps: 0x00000102, } } pub fn by_char_select(guildcard: u32, team_id: u32, session: Session) -> LoginResponse { LoginResponse { status: AccountStatus::Ok, tag: 0x00010000, //tag: 0x00000100, guildcard: guildcard, team_id: team_id, session: session, caps: 0x00000102, } } } #[pso_packet(0xE0)] pub struct RequestSettings { } #[pso_packet(0xE2)] pub struct SendKeyAndTeamSettings { unknown: [u8; 0x114], keyboard_config: [u8; 0x16C], gamepad_config: [u8; 0x38], guildcard: u32, team_id: u32, //team_info: [u32; 2], team_info: [u8; 8], team_priv: u16, unknown2: u16, //team_name: [u16; 16], team_name: [u8; 32], #[nodebug] team_flag: [u8; 2048], team_rewards: [u8; 8], } impl SendKeyAndTeamSettings { pub fn new(keyboard_config: [u8; 0x16C], gamepad_config: [u8; 0x38], guildcard: u32, team_id: u32) -> SendKeyAndTeamSettings { SendKeyAndTeamSettings { unknown: [0; 0x114], keyboard_config: keyboard_config, gamepad_config: gamepad_config, guildcard: guildcard, team_id: team_id, //team_info: [0; 2], team_info: [0; 8], team_priv: 0, unknown2: 0, //team_name: [0; 16], team_name: [0; 32], team_flag: [0; 2048], team_rewards: [0; 8] } } } #[pso_packet(0x19)] pub struct RedirectClient { pub ip: u32, pub port: u16, pub padding: u16, } impl RedirectClient { pub fn new(ip: u32, port: u16) -> RedirectClient { RedirectClient { ip: ip, port: port, padding: 0, } } } #[pso_packet(0x1E8)] pub struct Checksum { pub checksum: u32, pub padding: u32, } #[pso_packet(0x2E8)] pub struct ChecksumAck { pub ack: u32, } impl ChecksumAck { pub fn new(ack: u32) -> ChecksumAck { ChecksumAck { ack: ack, } } } #[pso_packet(0xE3)] pub struct CharSelect { pub slot: u32, pub reason: u32, // TODO: enum? } #[pso_packet(0xE4)] pub struct CharAck { pub slot: u32, pub code: u32, // TODO: enum? } #[pso_packet(0xE5)] pub struct CharacterPreview { pub slot: u32, pub character: SelectScreenCharacter, } #[pso_packet(0x3E8)] pub struct GuildcardDataRequest { } #[pso_packet(0x1DC)] pub struct GuildcardDataHeader { one: u32, len: u32, checksum: u32, } impl GuildcardDataHeader { pub fn new(len: usize, checksum: u32) -> GuildcardDataHeader { GuildcardDataHeader { one: 1, len: len as u32, checksum: checksum } } } #[pso_packet(0x3DC)] pub struct GuildcardDataChunkRequest { _unknown: u32, pub chunk: u32, pub again: u32, } pub struct GuildcardDataChunk { _unknown: u32, chunk: u32, pub buffer: [u8; GUILD_CARD_CHUNK_SIZE], len: usize, } impl GuildcardDataChunk { pub fn new(chunk: u32, buffer: [u8; GUILD_CARD_CHUNK_SIZE], len: usize) -> GuildcardDataChunk { GuildcardDataChunk { _unknown: 0, chunk: chunk as u32, buffer: buffer, len: len, } } } impl PSOPacket for GuildcardDataChunk { fn from_bytes(_data: &[u8]) -> Result { unimplemented!(); } fn as_bytes(&self) -> Vec { let mut buf: Vec = Vec::new(); buf.extend_from_slice(&u32::to_le_bytes(0)); buf.extend_from_slice(&u32::to_le_bytes(self._unknown)); buf.extend_from_slice(&u32::to_le_bytes(self.chunk)); buf.extend_from_slice(&self.buffer[0..self.len]); while buf.len() % 4 != 0 { buf.push(0); } let pkt_len = (buf.len() + 4) as u16; let mut prebuf: Vec = Vec::new(); prebuf.extend_from_slice(&u16::to_le_bytes(pkt_len)); prebuf.extend_from_slice(&u16::to_le_bytes(0x2DC)); prebuf.append(&mut buf); prebuf } } impl std::fmt::Debug for GuildcardDataChunk { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "packet GuildcardDataChunk {{\n").unwrap(); write!(f, " flag: {:?}\n", 0).unwrap(); write!(f, " _unknown: {:#X?}\n", self._unknown).unwrap(); write!(f, " chunk: {:#X?}\n", self.chunk).unwrap(); write!(f, " buffer: [0..{:#X}]\n", self.len).unwrap(); write!(f, "}}") } } #[pso_packet(0x4EB)] pub struct ParamDataRequest { } #[derive(Clone)] pub struct ParamFile { pub size: u32, pub checksum: u32, pub offset: u32, pub filename: [u8; 0x40], } #[derive(Clone)] pub struct ParamDataHeader { pub files: Vec } impl PSOPacket for ParamDataHeader { fn from_bytes(_data: &[u8]) -> Result { unimplemented!(); } fn as_bytes(&self) -> Vec { let mut buf: Vec = Vec::new(); buf.extend_from_slice(&u32::to_le_bytes(self.files.len() as u32)); for f in &self.files { buf.extend_from_slice(&u32::to_le_bytes(f.size)); buf.extend_from_slice(&u32::to_le_bytes(f.checksum)); buf.extend_from_slice(&u32::to_le_bytes(f.offset)); buf.extend_from_slice(&f.filename[..]); } while buf.len() % 4 != 0 { buf.push(0); } let pkt_len = (buf.len() + 4) as u16; let mut prebuf: Vec = Vec::new(); prebuf.extend_from_slice(&u16::to_le_bytes(pkt_len)); prebuf.extend_from_slice(&u16::to_le_bytes(0x1EB)); prebuf.append(&mut buf); prebuf } } impl std::fmt::Debug for ParamDataHeader { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "packet ParamDataHeader{{\n").unwrap(); write!(f, " files: [..]\n").unwrap(); write!(f, "}}") } } #[pso_packet(0x3EB)] pub struct ParamDataChunkRequest { } #[pso_packet(0x2EB)] pub struct ParamDataChunk { pub chunk: u32, #[nodebug] pub data: [u8; 0x6800], // TODO: why wont the const work here? (blame macros?) } #[pso_packet(0xEC)] pub struct SetFlag { pub flags: u32, } #[pso_packet(0xB1)] pub struct Timestamp { #[utf8] timestamp: [u8; 28], } impl Timestamp { pub fn new(time: DateTime) -> Timestamp { let timestr = time.format("%Y:%m:%d: %H:%M:%S").to_string(); let timebytes = timestr.as_bytes(); let mut timebuf = [0u8; 28]; timebuf[..timebytes.len()].clone_from_slice(timebytes); Timestamp { timestamp: timebuf } } } #[derive(PSOPacketData, Copy, Clone)] pub struct ShipListEntry { pub menu: u32, pub item: u32, pub flags: u16, pub name: [u16; 0x11], } #[pso_packet(0xA0)] pub struct ShipList { baseship: ShipListEntry, pub ships: Vec, } impl ShipList { pub fn new(ships: Vec) -> ShipList { ShipList { baseship: ShipListEntry { menu: ships.get(0).map(|s| s.menu).unwrap_or(0), item: 0, flags: 0, name: utf8_to_utf16_array!("Ship", 0x11), }, ships: ships, } } } #[pso_packet(0x10)] pub struct MenuSelect { pub menu: u32, pub item: u32, } #[cfg(test)] mod tests { #[test] fn test_account_status_enum() { use super::PSOPacket; use super::Session; let pkt = super::LoginResponse { status: super::AccountStatus::InvalidPassword, tag: 0, guildcard: 0, team_id: 0, session: Session::new(), caps: 0, }; let mut bytes = pkt.as_bytes(); assert!(bytes[8] == 2); bytes[8] = 8; let pkt = super::LoginResponse::from_bytes(&bytes).unwrap(); assert!(pkt.status == super::AccountStatus::InvalidUser); } #[test] fn test_key_settings_reply() { use super::PSOPacket; use rand::{Rng}; let mut rng = rand::thread_rng(); let mut keyboard_config = [0u8; 0x16C]; let mut gamepad_config = [0u8; 0x38]; rng.fill(&mut keyboard_config[..]); rng.fill(&mut gamepad_config[..]); let pkt = super::SendKeyAndTeamSettings::new(keyboard_config, gamepad_config, 123, 456); let bytes = pkt.as_bytes(); assert!(bytes[2] == 0xe2); assert!(bytes[8 + 0x114] == keyboard_config[0]); assert!(bytes[8 + 0x114 + 0x16C] == gamepad_config[0]); } #[test] fn test_login_checksum_ack() { use super::PSOPacket; let pkt = super::ChecksumAck::new(1); assert!(pkt.as_bytes() == [0xC, 0, 0xE8, 0x02, 0,0,0,0, 1,0,0,0]); } #[test] fn test_session_size() { use super::PSOPacketData; let session = super::Session::new(); assert!(session.as_bytes().len() == 40); } }