Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions core/src/ipc/zone/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,12 +433,12 @@ pub enum ClientZoneIpcData {
unk2: u16,
#[brw(pad_after = 1)] // Seems to be empty/zeroes
unk3: u16,
#[brw(pad_size_to = 20)]
#[br(count = 20)]
#[brw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
name: String,
unk4: [u8; 13], // Unknown data, likely garbage since several other client opcodes have been discovered to leave garbage behind around name strings
unk4: [u8; 1],
},
CrossworldLinkshellMemberListRequest {
linkshell_id: u64,
Expand All @@ -464,6 +464,23 @@ pub enum ClientZoneIpcData {
#[brw(pad_after = 8)] // empty
listing_id: u64,
},
CheckCWLinkshellNameAvailability {
unk1: u8, // TODO: What is this? Seems to be always 1?
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
name: String,
unk2: [u8; 7], // Unknown data, likely garbage since several other client opcodes have been discovered to leave garbage behind around name strings*/
},
CreateNewCrossworldLinkshell {
/// The name of the cross-world linkshell.
#[brw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
name: String,
},
}

#[cfg(test)]
Expand Down
20 changes: 15 additions & 5 deletions core/src/ipc/zone/server/linkshell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl LinkshellEntry {

/// Represents one member entry in the CWLSMemberList.
#[binrw]
#[derive(Debug, Default, Clone)]
#[derive(Clone, Debug, Default)]
pub struct CWLSMemberListEntry {
pub content_id: u64,
pub unk_timestamp: u32, // Possibly when this member joined, or last had their rank changed?
Expand Down Expand Up @@ -70,15 +70,15 @@ pub enum CWLSPermissionRank {

/// Represents the CWLS's id number and ChatChannel. This was added to help reduce copy paste in CrossworldLinkshell & CrossworldLinkshellEx.
#[binrw]
#[derive(Debug, Default, Clone)]
#[derive(Clone, Debug, Default)]
pub struct CWLSCommonIdentifiers {
pub linkshell_id: u64,
pub linkshell_chat_id: ChatChannel,
}

/// Represents the CWLS's name & permission rank info. This was added to help reduce copy paste in CrossworldLinkshell & CrossworldLinkshellEx.
#[binrw]
#[derive(Debug, Default, Clone)]
#[derive(Clone, Debug, Default)]
pub struct CWLSCommon {
/// The client's rank in the CWLS.
pub rank: CWLSPermissionRank,
Expand All @@ -93,7 +93,7 @@ pub struct CWLSCommon {

/// Represents data of a single CWLS. This version is used on login.
#[binrw]
#[derive(Debug, Default, Clone)]
#[derive(Clone, Debug, Default)]
pub struct CrossworldLinkshell {
pub ids: CWLSCommonIdentifiers,
/// The client's name and rank in the CWLS.
Expand All @@ -107,7 +107,7 @@ impl CrossworldLinkshell {

/// Represents data of a single CWLS. This extended version is used when the CWLS menu is opened.
#[binrw]
#[derive(Debug, Default, Clone)]
#[derive(Clone, Debug, Default)]
pub struct CrossworldLinkshellEx {
pub ids: CWLSCommonIdentifiers,
/// A 32-bit Unix timestmap indicating when this CWLS was created.
Expand All @@ -121,3 +121,13 @@ impl CrossworldLinkshellEx {
pub const SIZE: usize = 64;
pub const COUNT: usize = 8;
}

/// The result sent back to the client when they ask if a CWLS's name is available for use.
#[binrw]
#[brw(repr = u8)]
#[derive(Clone, Debug, Default, PartialEq)]
pub enum CWLSNameAvailability {
#[default]
Available = 0,
NotAvailable = 1,
}
24 changes: 22 additions & 2 deletions core/src/ipc/zone/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ pub use marketboard::MarketBoardItem;

mod linkshell;
pub use linkshell::{
CWLSMemberListEntry, CWLSPermissionRank, CrossworldLinkshell, CrossworldLinkshellEx,
LinkshellEntry,
CWLSCommon, CWLSCommonIdentifiers, CWLSMemberListEntry, CWLSNameAvailability,
CWLSPermissionRank, CrossworldLinkshell, CrossworldLinkshellEx, LinkshellEntry,
};

mod spawn_treasure;
Expand Down Expand Up @@ -1056,6 +1056,26 @@ pub enum ServerZoneIpcData {
listing_id: u64,
unk: [u8; 456],
},
CWLinkshellNameAvailability {
unk1: u8, // TODO: What is this? Seems to be always 1.
/// If the desired name was available or not.
result: CWLSNameAvailability,
/// The desired name.
#[brw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
#[brw(pad_after = 6)]
name: String,
},
NewCrossworldLinkshell {
/// The CWLS's id number and ChatChannel information.
ids: CWLSCommonIdentifiers,
unk_timestamp1: u32, // Unknown 32-bit Unix timestamp, likely the cwls's creation time.
unk_timestamp2: u32, // Seems to be the same timestamp repeated? Might be the member's join time?
/// The member's rank in the cross-world linkshell, and the linkshell's name.
common: CWLSCommon,
},
}

#[cfg(test)]
Expand Down
16 changes: 16 additions & 0 deletions resources/data/opcodes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,14 @@ ServerZoneIpcType:
comment: Sent in response to ViewCrossRealmListing
opcode: 735
size: 464
- name: CWLinkshellNameAvailability
comment: The server responds to CheckCWLinkshellNameAvailability, informing the client if the desired name was available or not.
opcode: 906
size: 40
- name: NewCrossworldLinkshell
comment: The server responds to NewCrossworldLinkshellInfoRequest, providing some information on the newly created/joined cwls.
opcode: 855
size: 64
ClientZoneIpcType:
- name: InitRequest
comment: Sent by the client when they successfully initialize with the server, and they need several bits of information (e.g. what zone to load.)
Expand Down Expand Up @@ -912,6 +920,14 @@ ClientZoneIpcType:
comment: Seen while viewing information for a specific Party Finder listing.
opcode: 810
size: 16
- name: CheckCWLinkshellNameAvailability
comment: The client requests if this name is available for use with a new cross-world linkshell.
opcode: 591
size: 40
- name: CreateNewCrossworldLinkshell
comment: The client requests for the server to commence with creating the new cross-world linkshell after confirming its name's availability.
opcode: 587
size: 32
ServerLobbyIpcType:
- name: NackReply
comment: Sent by the server to indicate an lobby error occured.
Expand Down
3 changes: 2 additions & 1 deletion servers/world/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ pub enum FromServer {
/// A chat message from one of the client's cwlses has been received.
CWLSMessageSent(CWLinkshellMessage),
/// Inform the zone and chat connections about their linkshell channels.
SetLinkshellChatChannels(Vec<u32>, Vec<u32>),
SetLinkshellChatChannels(Vec<u32>, Vec<u32>, bool),
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -389,6 +389,7 @@ pub enum ToServer {
ObjectId,
Option<Vec<(u64, CWLSPermissionRank)>>,
Option<Vec<u64>>,
bool,
),
/// The client sent a message to a cross-world linkshell.
CWLSMessageSent(ObjectId, SendCWLinkshellMessage),
Expand Down
153 changes: 149 additions & 4 deletions servers/world/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use kawari::ipc::zone::{
GameMasterRank, OnlineStatus, ServerZoneIpcData, SocialListUIFlags, SocialListUILanguages,
};
pub use models::{
AetherCurrent, Aetheryte, Character, ClassJob, Companion, Content, Friends, Mentor, Quest,
SearchInfo, Unlock, Volatile,
AetherCurrent, Aetheryte, Character, ClassJob, Companion, Content, Friends, LinkshellMembers,
Mentor, Quest, SearchInfo, Unlock, Volatile,
};

mod schema;
Expand All @@ -27,8 +27,8 @@ use kawari::{
constants::AVAILABLE_CLASSJOBS,
ipc::lobby::{CharacterDetails, CharacterFlag},
ipc::zone::{
CWLSMemberListEntry, CWLSPermissionRank, CrossworldLinkshellEx, OnlineStatusMask,
PlayerEntry,
CWLSCommon, CWLSCommonIdentifiers, CWLSMemberListEntry, CWLSNameAvailability,
CWLSPermissionRank, CrossworldLinkshellEx, OnlineStatusMask, PlayerEntry,
},
};

Expand Down Expand Up @@ -1251,6 +1251,151 @@ impl WorldDatabase {
Some(members)
}

pub fn add_member_to_linkshell(
&mut self,
for_linkshell_id: i64,
for_content_id: i64,
their_rank: CWLSPermissionRank,
their_invite_time: i64,
) -> bool {
use schema::linkshell_members::dsl::*;

let already_member = linkshell_members
.select(content_id)
.filter(content_id.eq(for_content_id))
.filter(linkshell_id.eq(for_linkshell_id))
.first::<i64>(&mut self.connection);

// If they're not in this linkshell, add them.
if already_member.is_err() {
let next_id = if let Ok(highest) = linkshell_members
.select(id)
.order(id.desc())
.first::<i64>(&mut self.connection)
{
highest + 1
} else {
1 // Start from a safe default if there are no members.
};
let new_member = LinkshellMembers {
id: next_id,
content_id: for_content_id,
linkshell_id: for_linkshell_id,
invite_time: their_invite_time,
rank: their_rank as i32,
};

let result = diesel::insert_into(linkshell_members)
.values(new_member)
.execute(&mut self.connection);

match result {
Ok(_) => {
return true;
}

Err(err) => {
tracing::warn!(
"Failed to add member to linkshell due to the following error: {err:#?}"
);
return false;
}
}
} else {
tracing::warn!(
"This character {for_content_id} is already in this linkshell {for_content_id}!"
);
}

false
}

pub fn linkshell_name_available(&mut self, desired_name: String) -> CWLSNameAvailability {
// Linkshell names must be: between 3 and 20 characters in length, may contain punctuation, not contain double spaces/underscores, not contain a space at the start or end of the name, and the name may not consist solely of punctuation.
// TODO: Should we bother enforcing the other rules if a player somehow bypassed the client-side limitations?
use schema::linkshells::dsl::*;

if desired_name.len() >= 3 && desired_name.len() <= 20 {
let already_exists = linkshells
.select(name)
.filter(name.eq(desired_name.clone()))
.first::<String>(&mut self.connection);

if already_exists.is_err() {
return CWLSNameAvailability::Available;
}
}
CWLSNameAvailability::NotAvailable
}

pub fn create_linkshell(
&mut self,
from_content_id: i64,
ls_name: String,
is_crossworld_ls: bool,
) -> Option<CrossworldLinkshellEx> {
use schema::linkshells::dsl::*;

// Only allow creation if this LS doesn't exist already. Probably a bit redundant with how the order of events goes, but never hurts.
if self.linkshell_name_available(ls_name.clone()) == CWLSNameAvailability::Available {
let ls_creation_time = diesel::select(unixepoch())
.get_result::<i64>(&mut self.connection)
.unwrap_or_default();

let next_id = if let Ok(highest) = linkshells
.select(id)
.order(id.desc())
.first::<i64>(&mut self.connection)
{
highest + 1
} else {
1 // Start from a safe default if there are no linkshells at all on the server.
};

let linkshell = models::Linkshells {
id: next_id,
name: ls_name.clone(),
creation_time: ls_creation_time,
is_crossworld: is_crossworld_ls,
};

let result = diesel::insert_into(linkshells)
.values(linkshell)
.execute(&mut self.connection);

match result {
Ok(_) => {
let rank = CWLSPermissionRank::Master;
if self.add_member_to_linkshell(
next_id,
from_content_id,
rank,
ls_creation_time,
) {
return Some(CrossworldLinkshellEx {
ids: CWLSCommonIdentifiers {
linkshell_id: next_id as u64,
..Default::default()
},
creation_time: ls_creation_time as u32,
common: CWLSCommon {
rank,
name: ls_name.clone(),
},
});
}
}
Err(err) => tracing::error!(
"Failed to create the linkshell because of the following error: {err:#?}"
),
}
} else {
tracing::warn!("The linkshell name {ls_name} already exists!");
}

None
}

pub fn do_cleanup_tasks(&mut self) {
use schema::volatile::dsl::*;

Expand Down
16 changes: 13 additions & 3 deletions servers/world/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ async fn client_chat_loop(
}
FromServer::SetPartyChatChannel(channel_id) => connection.set_party_chatchannel(channel_id).await,
FromServer::PartyMessageSent(message_info) => connection.party_message_received(message_info).await,
FromServer::SetLinkshellChatChannels(cwls, local) => connection.set_linkshell_chatchannels(cwls, local).await,
FromServer::SetLinkshellChatChannels(cwls, local, _) => connection.set_linkshell_chatchannels(cwls, local).await,
FromServer::CWLSMessageSent(message_info) => connection.cwls_message_received(message_info).await,
_ => tracing::error!("ChatConnection {:#?} received a FromServer message we don't care about: {:#?}, ensure you're using the right client network or that you've implemented a handler for it if we actually care about it!", client_handle.id, msg),
},
Expand Down Expand Up @@ -2751,6 +2751,14 @@ async fn process_packet(
);
connection.send_ipc_self(ipc).await;
}
ClientZoneIpcData::CheckCWLinkshellNameAvailability { name, .. } => {
connection
.check_cwlinkshell_name_availability(name.clone())
.await;
}
ClientZoneIpcData::CreateNewCrossworldLinkshell { name } => {
connection.create_crossworld_linkshell(name.clone()).await;
}
ClientZoneIpcData::Unknown { unk } => {
tracing::warn!(
"Unknown Zone packet {:?} recieved ({} bytes), this should be handled!",
Expand Down Expand Up @@ -3098,10 +3106,12 @@ async fn process_server_msg(
database.commit_parties(parties);
}
FromServer::TreasureSpawn(treasure) => connection.spawn_treasure(treasure).await,
FromServer::SetLinkshellChatChannels(cwlses, _locals) => {
FromServer::SetLinkshellChatChannels(cwlses, _locals, need_to_send_linkshells) => {
// TODO: There might be a better way to do this. We need the chatchannels to be set *before* sending the "overview" or chat will break.
connection.set_linkshell_chatchannels(cwlses).await;
connection.send_crossworld_linkshells(false).await;
if need_to_send_linkshells {
connection.send_crossworld_linkshells(false).await;
}
}
_ => {
tracing::error!(
Expand Down
Loading
Loading