567 lines
14 KiB
Rust
567 lines
14 KiB
Rust
use chrono::{DateTime, Utc};
|
|
|
|
use psopacket::{pso_packet, PSOPacketData};
|
|
use crate::{PSOPacket, PacketParseError, PSOPacketData};
|
|
use crate::util::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,
|
|
server_key,
|
|
client_key,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum SessionAction {
|
|
None,
|
|
SelectCharacter,
|
|
NewCharacter,
|
|
DressingRoom,
|
|
}
|
|
|
|
impl PSOPacketData for SessionAction {
|
|
fn from_bytes<R: Read>(cursor: &mut R) -> Result<Self, PacketParseError> {
|
|
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<u8> {
|
|
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 Default for Session {
|
|
fn default() -> 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<R: Read>(cursor: &mut R) -> Result<Self, PacketParseError> {
|
|
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<u8> {
|
|
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,
|
|
tag: 0x00010000,
|
|
//tag: 0x00000100,
|
|
guildcard: 0,
|
|
team_id: 0,
|
|
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,
|
|
team_id,
|
|
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,
|
|
gamepad_config,
|
|
guildcard,
|
|
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,
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
buffer,
|
|
len,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PSOPacket for GuildcardDataChunk {
|
|
fn from_bytes(_data: &[u8]) -> Result<GuildcardDataChunk, PacketParseError> {
|
|
unimplemented!();
|
|
}
|
|
|
|
fn as_bytes(&self) -> Vec<u8> {
|
|
let mut buf: Vec<u8> = 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<u8> = 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 {
|
|
writeln!(f, "packet GuildcardDataChunk {{").unwrap();
|
|
writeln!(f, " flag: {:?}", 0).unwrap();
|
|
writeln!(f, " _unknown: {:#X?}", self._unknown).unwrap();
|
|
writeln!(f, " chunk: {:#X?}", self.chunk).unwrap();
|
|
writeln!(f, " buffer: [0..{:#X}]", self.len).unwrap();
|
|
writeln!(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<ParamFile>
|
|
}
|
|
|
|
impl PSOPacket for ParamDataHeader {
|
|
fn from_bytes(_data: &[u8]) -> Result<ParamDataHeader, PacketParseError> {
|
|
unimplemented!();
|
|
}
|
|
|
|
fn as_bytes(&self) -> Vec<u8> {
|
|
let mut buf: Vec<u8> = 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<u8> = 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 {
|
|
writeln!(f, "packet ParamDataHeader{{").unwrap();
|
|
writeln!(f, " files: [..]").unwrap();
|
|
writeln!(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<Utc>) -> 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<ShipListEntry>,
|
|
}
|
|
|
|
impl ShipList {
|
|
pub fn new(ships: Vec<ShipListEntry>) -> ShipList {
|
|
ShipList {
|
|
baseship: ShipListEntry {
|
|
menu: ships.first().map(|s| s.menu).unwrap_or(0),
|
|
item: 0,
|
|
flags: 0,
|
|
name: utf8_to_utf16_array("Ship"),
|
|
},
|
|
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::default(),
|
|
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::default();
|
|
assert!(session.as_bytes().len() == 40);
|
|
}
|
|
}
|