From 1d5c3dfc9f4e5e6e8da1407a67adf137a069deef Mon Sep 17 00:00:00 2001 From: The Dax Date: Wed, 16 Sep 2026 07:11:52 -0400 Subject: [PATCH 1/4] Initial documentation of the Fellowship Finder & related opcodes -Fellowships are still unimplemented, this is mostly for PacketAnalzyer, and general learning -CreateFellowship (client) & Fellowships (server) will be documented at a later date --- core/src/ipc/zone/client/mod.rs | 3 ++ core/src/ipc/zone/common.rs | 31 ++++++++++++ core/src/ipc/zone/server/fellowships.rs | 64 +++++++++++++++++++++++++ core/src/ipc/zone/server/mod.rs | 18 +++++++ docs/src/update_ingame_checklist.md | 2 +- resources/data/opcodes.yml | 14 +++++- servers/world/src/main.rs | 21 +++++++- 7 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 core/src/ipc/zone/server/fellowships.rs diff --git a/core/src/ipc/zone/client/mod.rs b/core/src/ipc/zone/client/mod.rs index 6d7e1042..8a14ebb7 100644 --- a/core/src/ipc/zone/client/mod.rs +++ b/core/src/ipc/zone/client/mod.rs @@ -669,6 +669,9 @@ pub enum ClientZoneIpcData { PerformanceNote { unk1: [u8; 24], }, + CreateFellowship { + unk1: [u8; 64], + }, } #[cfg(test)] diff --git a/core/src/ipc/zone/common.rs b/core/src/ipc/zone/common.rs index 14d9a4a0..213fa1e4 100644 --- a/core/src/ipc/zone/common.rs +++ b/core/src/ipc/zone/common.rs @@ -20,6 +20,37 @@ pub struct StrategyBoardUpdate { data: Vec, } +/// Represents circle/Fellowship activity tags that players set for Fellowships they've created, or tags that can be used to search for Fellowships to join. +#[binrw] +#[brw(repr = u16)] +#[derive(Clone, Debug, Default)] +pub enum FellowshipActivityTag { + #[default] + None = 0, + RolePlaying = 1, + PlayerEvents = 2, + MakingFriends = 3, + NoviceSupport = 4, + Casual = 5, + Hardcore = 6, + Leveling = 7, + Battle = 8, + Crafting = 9, + Gathering = 10, + Housing = 11, + Hunts = 12, + TreasureHunt = 13, + PvP = 14, + Fishing = 15, + DomanMahjong = 16, + Performance = 17, + Glamours = 18, + GroupPose = 19, + TripleTriad = 20, + Collectables = 21, + Chatting = 22, +} + /// Represents housing appeal tags that players can set for their owned housing. #[binrw] #[brw(repr = u8)] diff --git a/core/src/ipc/zone/server/fellowships.rs b/core/src/ipc/zone/server/fellowships.rs new file mode 100644 index 00000000..d94e488b --- /dev/null +++ b/core/src/ipc/zone/server/fellowships.rs @@ -0,0 +1,64 @@ +use binrw::binrw; + +use crate::{ + common::{CHAR_NAME_MAX_LENGTH, ClientLanguage, read_string, write_string}, + ipc::zone::{FellowshipActivityTag, SocialListUILanguages}, +}; + +#[binrw] +#[derive(Clone, Default, Debug)] +pub struct FellowshipSearchInfo { + /// The Fellowship's community id (assumed). + pub community_id: u64, + /// The master or recruiter's content id (assumed). + pub master_content_id: u64, // TODO: Figure out which content id is which + /// The recruiter or master's content id (assumed). + pub recruiter_content_id: u64, + /// A 32-bit UNIX timestamp indicating when this recruitment listing will expire. + pub recruitment_deadline: u32, + /// The languages this Fellowship is seeking. + pub languages: SocialListUILanguages, // Fellowship language(s), this might be SocialListUILanguages + #[brw(pad_before = 3, pad_after = 3)] // Probably just padding + /// The primary/underlined language of this Fellowship. This seems to be set to the recruiter's client language at the time of Fellowship's creation, or during the time of starting a recruitment listing. + pub primary_language: ClientLanguage, // 0 = Japanese, 1 = English, 2 = German, 3 = French + /// The home world of the Fellowship's recruiter. + pub recruiter_world_id: u16, + /// The home world of the Fellowship's master. + pub master_world_id: u16, + /// The current number of members in this Fellowship, out of 1000 (but the client will display numbers higher than 1000). + pub current_member_count: u16, + /// The number of members the recruiter is attempting to reach (up to 1000, but the client can display up to 65,535 properly). + pub target_member_count: u16, + /// The Fellowship's first activity icon. References the CircleActivity excel sheet. + pub activity1: FellowshipActivityTag, + pub unk1: [u8; 4], // Has values in it, but it's unclear what they control yet. Changing them doesn't seem to affect what the client shows in the Finder window. + /// The Fellowship's second activity icon. References the CircleActivity excel sheet. + pub activity2: FellowshipActivityTag, + /// The fellowship's third activity icon. References the CircleActivity excel sheet. + pub activity3: FellowshipActivityTag, + /// The name of the Fellowship. It can be up to 60 characters in length (fewer if using Unicode glyphs). + #[brw(pad_size_to = 61)] + #[br(count = 60)] + #[br(map = read_string)] + #[bw(map = write_string)] + pub fellowship_name: String, + /// The Fellowship recruiter's name. The recruiter can be the same person as the master. + #[brw(pad_size_to = CHAR_NAME_MAX_LENGTH)] + #[br(count = CHAR_NAME_MAX_LENGTH)] + #[br(map = read_string)] + #[bw(map = write_string)] + pub recruiter_name: String, + /// The Fellowship master's name. + #[brw(pad_size_to = CHAR_NAME_MAX_LENGTH)] + #[br(count = CHAR_NAME_MAX_LENGTH)] + #[br(map = read_string)] + #[bw(map = write_string)] + pub master_name: String, + /// The Fellowship's text description. This can be up to 192 characters in length (fewer if using Unicode glyphs). + #[brw(pad_after = 5)] + #[brw(pad_size_to = 192)] + #[br(count = 192)] + #[br(map = read_string)] + #[bw(map = write_string)] + pub fellowship_description: String, +} diff --git a/core/src/ipc/zone/server/mod.rs b/core/src/ipc/zone/server/mod.rs index ebcb2d3d..6743da19 100644 --- a/core/src/ipc/zone/server/mod.rs +++ b/core/src/ipc/zone/server/mod.rs @@ -159,6 +159,9 @@ pub use marketboard::{MarketBoardHistory, MarketBoardHistoryEntry, MarketBoardIt mod linkshell; pub use linkshell::*; +mod fellowships; +pub use fellowships::*; + mod spawn_treasure; pub use spawn_treasure::SpawnTreasure; @@ -1506,6 +1509,21 @@ pub enum ServerZoneIpcData { hits: u32, }, ZoneDiceRollResult(ZoneDiceRollResult), + SearchFellowshipsNoResults { + #[brw(pad_before = 4, pad_after = 16)] + err_code: u32, // Name is assumed, observed as 0x01240C89, changing this seems to make the client incorrectly think there are results incoming + }, + SearchFellowshipsResults { + #[brw(pad_before = 4)] // Seems to always be zeroes/padding + unk1: u32, // Seems to always be 0xFFFF_FFFF + sequence_end: u32, // Is typically 0xFFFF_FFFF, but seems to change to 0x28 as the final value when a number of FellowshipFinderResults have been sent. If only one is sent, it stays at 0xFFFF_FFFF and sequence_current will be 0. More research is needed for this. + /// The current sequence value, so the client knows which packet this is so far. If it's set to 0, the client won't expect more to be coming (the appropriate page arrows in the Finder window will be disabled). + sequence_current: u32, + /// The actual Fellowship result items. + #[br(count = 4)] + #[bw(pad_size_to = 4 * 376)] + fellowships: Vec, + }, } #[cfg(test)] diff --git a/docs/src/update_ingame_checklist.md b/docs/src/update_ingame_checklist.md index 797b77e8..8e56ef61 100644 --- a/docs/src/update_ingame_checklist.md +++ b/docs/src/update_ingame_checklist.md @@ -122,5 +122,5 @@ The following tests should also be visible to others in the zone upon completion - [ ] Trading: attempt to trade with another player, and ensure the client tells you that the other player is unable trade. - [ ] Run the `/playtime` command and ensure it displays your character's total play time. For the following, ensure the following items display their correct unimplemented messages when trying to do them. -- [ ] Fellowships & Fellowship Finder +- [ ] Fellowships & Fellowship Finder: ensure the Finder shows no results (and doesn't display an error message), and that creating a Fellowship sends a server notice indicating it's unimplemented. - [ ] Run the `/dice` and `/random` commands and ensure they display both for you and another player diff --git a/resources/data/opcodes.yml b/resources/data/opcodes.yml index 2e3f8c3a..9f73a082 100644 --- a/resources/data/opcodes.yml +++ b/resources/data/opcodes.yml @@ -269,7 +269,7 @@ ServerZoneIpcType: size: 104 - name: Fellowships comment: The fellowships you have joined. - opcode: 108 + opcode: 439 size: 808 - name: DailyQuests comment: Sent shortly after the server sends InitZone, this with UnkZoneLoad2 it makes everything load faster. @@ -791,6 +791,14 @@ ServerZoneIpcType: comment: Sent to the client and nearby players after the client sends a dice roll. opcode: 801 size: 64 +- name: SearchFellowshipsResults + comment: Sent to the client when their Fellowship Finder search yielded some results. + opcode: 682 + size: 1520 +- name: SearchFellowshipsNoResults + comment: Sent to the client when their Fellowship Finder search yielded no results. + opcode: 218 + size: 24 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.) @@ -1192,6 +1200,10 @@ ClientZoneIpcType: comment: Sent when playing an instrument. opcode: 266 size: 24 +- name: CreateFellowship + comment: Sent when creating a new Fellowship. + opcode: 243 + size: 64 ServerLobbyIpcType: - name: NackReply comment: Sent by the server to indicate an lobby error occured. diff --git a/servers/world/src/main.rs b/servers/world/src/main.rs index 8feea294..fb8c8807 100644 --- a/servers/world/src/main.rs +++ b/servers/world/src/main.rs @@ -2644,13 +2644,25 @@ async fn process_packet( connection.send_ipc_self(ipc).await; } ClientZoneIpcData::RequestFellowships { .. } => { - tracing::info!("Fellowships is unimplemented"); + // Report back that they're not in a fellowship + // TODO: Implement this for real! + let ipc = ServerZoneIpcSegment::new(ServerZoneIpcData::Fellowships { + unk1: [0; 808], + }); + connection.send_ipc_self(ipc).await; } ClientZoneIpcData::RequestCrossworldLinkshells { .. } => { connection.send_crossworld_linkshells(true).await; } ClientZoneIpcData::SearchFellowships { .. } => { - tracing::info!("Fellowship Finder is unimplemented"); + // Report back that no results were found. + // TODO: Implement this for real! + let ipc = ServerZoneIpcSegment::new( + ServerZoneIpcData::SearchFellowshipsNoResults { + err_code: 0x01240C89, + }, + ); + connection.send_ipc_self(ipc).await; } ClientZoneIpcData::StartCountdown { starter_actor_id, @@ -3752,6 +3764,11 @@ async fn process_packet( ClientZoneIpcData::PerformanceNote { .. } => { // TODO: what to do with these? } + ClientZoneIpcData::CreateFellowship { .. } => { + connection + .send_notice("Creating Fellowships is currently unimplemented.") + .await; + } ClientZoneIpcData::Unknown { unk } => { tracing::warn!( "Unknown Zone packet {:?} recieved ({} bytes)", From 034d8cc32ad12e7ee7a49e1419fb982022161d8d Mon Sep 17 00:00:00 2001 From: The Dax Date: Wed, 16 Sep 2026 17:29:46 -0400 Subject: [PATCH 2/4] Document more about Fellowships -Document the client request to create and join Fellowships -Document and add the server rejection responses since we don't implement Fellowships yet --- core/src/common/game.rs | 2 + core/src/ipc/zone/client/mod.rs | 10 +++- core/src/ipc/zone/server/fellowships.rs | 12 ++-- core/src/ipc/zone/server/mod.rs | 2 +- resources/data/opcodes.yml | 4 ++ servers/world/src/main.rs | 73 ++++++++++++++++++++----- 6 files changed, 84 insertions(+), 19 deletions(-) diff --git a/core/src/common/game.rs b/core/src/common/game.rs index 9532b99c..39e52f6a 100644 --- a/core/src/common/game.rs +++ b/core/src/common/game.rs @@ -717,6 +717,8 @@ pub enum LogMessageType { UnableToAcceptAttachmentInventoryFull = 0x17B, // Unable to accept attachment. Inventory is full. FurnitureMovedToStoreroom = 0xD9F, // The was moved to your storeroom. FurnitureMovedToInventory = 0xDA0, // The was moved to your inventory. + UnableToJoinMoreFellowships = 0x15FE, // You cannot join any more Fellowships. + UnableToCreateMoreFellowships = 0x15E3, // You cannot create any more Fellowshiops. } /// Names for rows in the Excel sheet of the same name. diff --git a/core/src/ipc/zone/client/mod.rs b/core/src/ipc/zone/client/mod.rs index 8a14ebb7..2ffa732f 100644 --- a/core/src/ipc/zone/client/mod.rs +++ b/core/src/ipc/zone/client/mod.rs @@ -670,7 +670,15 @@ pub enum ClientZoneIpcData { unk1: [u8; 24], }, CreateFellowship { - unk1: [u8; 64], + #[brw(pad_size_to = 60)] + #[br(count = 60)] + #[br(map = read_string)] + #[bw(map = write_string)] + #[brw(pad_after = 4)] // Seems to just be padding/garbage + name: String, + }, + SearchFellowshipsJoin { + community_id: u64, }, } diff --git a/core/src/ipc/zone/server/fellowships.rs b/core/src/ipc/zone/server/fellowships.rs index d94e488b..31a1ca49 100644 --- a/core/src/ipc/zone/server/fellowships.rs +++ b/core/src/ipc/zone/server/fellowships.rs @@ -1,7 +1,11 @@ use binrw::binrw; +use bstr::BString; use crate::{ - common::{CHAR_NAME_MAX_LENGTH, ClientLanguage, read_string, write_string}, + common::{ + CHAR_NAME_MAX_LENGTH, ClientLanguage, read_sestring, read_string, write_sestring, + write_string, + }, ipc::zone::{FellowshipActivityTag, SocialListUILanguages}, }; @@ -58,7 +62,7 @@ pub struct FellowshipSearchInfo { #[brw(pad_after = 5)] #[brw(pad_size_to = 192)] #[br(count = 192)] - #[br(map = read_string)] - #[bw(map = write_string)] - pub fellowship_description: String, + #[br(map = read_sestring)] + #[bw(map = write_sestring)] + pub fellowship_description: BString, // NOTE: This is a BString due to the fact that SEString macros can appear in its contents. } diff --git a/core/src/ipc/zone/server/mod.rs b/core/src/ipc/zone/server/mod.rs index 6743da19..cf8efd70 100644 --- a/core/src/ipc/zone/server/mod.rs +++ b/core/src/ipc/zone/server/mod.rs @@ -1511,7 +1511,7 @@ pub enum ServerZoneIpcData { ZoneDiceRollResult(ZoneDiceRollResult), SearchFellowshipsNoResults { #[brw(pad_before = 4, pad_after = 16)] - err_code: u32, // Name is assumed, observed as 0x01240C89, changing this seems to make the client incorrectly think there are results incoming + err_code: u32, // Name is assumed, observed as 0x01240C89. Changing this to an incorrect value seems to make the client wait for more results (the UI window acts as if it's waiting with a spinner)? }, SearchFellowshipsResults { #[brw(pad_before = 4)] // Seems to always be zeroes/padding diff --git a/resources/data/opcodes.yml b/resources/data/opcodes.yml index 9f73a082..4aa5c221 100644 --- a/resources/data/opcodes.yml +++ b/resources/data/opcodes.yml @@ -1204,6 +1204,10 @@ ClientZoneIpcType: comment: Sent when creating a new Fellowship. opcode: 243 size: 64 +- name: SearchFellowshipsJoin + comment: Sent when joining a Fellowship from the Fellowship Finder. + opcode: 237 + size: 8 ServerLobbyIpcType: - name: NackReply comment: Sent by the server to indicate an lobby error occured. diff --git a/servers/world/src/main.rs b/servers/world/src/main.rs index fb8c8807..05d069c2 100644 --- a/servers/world/src/main.rs +++ b/servers/world/src/main.rs @@ -5,10 +5,10 @@ use std::time::{Instant, SystemTime}; use axum::Router; use axum::routing::get; use kawari::common::{ - BasicServiceAccountData, ContainerType, DEBUG_COMMAND_TRIGGER, FestivalId, HandlerId, - HandlerType, InstanceContentType, ItemOperationKind, LogMessageType, ObjectId, ObjectTypeId, - ObjectTypeKind, PlayerStateFlags1, PlayerStateFlags2, PlayerStateFlags3, Position, - QuestSpecialFlags, TAB_SHARED_FATE_COUNT, WarpType, calculate_max_level, + BasicServiceAccountData, ClientLanguage, ContainerType, DEBUG_COMMAND_TRIGGER, FestivalId, + HandlerId, HandlerType, InstanceContentType, ItemOperationKind, LogMessageType, ObjectId, + ObjectTypeId, ObjectTypeKind, PlayerStateFlags1, PlayerStateFlags2, PlayerStateFlags3, + Position, QuestSpecialFlags, TAB_SHARED_FATE_COUNT, WarpType, calculate_max_level, }; use kawari::config::get_config; use kawari_world::inventory::{Item, MAX_LARGE_STORAGE, Storage, get_next_free_slot}; @@ -26,8 +26,9 @@ use kawari::ipc::zone::{ }; use kawari::ipc::zone::{ - Blacklist, BlacklistedCharacter, ClientTriggerCommand, ClientZoneIpcData, ReadyCheckReply, - ServerZoneIpcData, ServerZoneIpcSegment, + Blacklist, BlacklistedCharacter, ClientTriggerCommand, ClientZoneIpcData, + FellowshipActivityTag, FellowshipSearchInfo, ReadyCheckReply, ServerZoneIpcData, + ServerZoneIpcSegment, SocialListUILanguages, }; use kawari::common::{CharacterMode, NETWORK_TIMEOUT, RECEIVE_BUFFER_SIZE}; @@ -2655,13 +2656,45 @@ async fn process_packet( connection.send_crossworld_linkshells(true).await; } ClientZoneIpcData::SearchFellowships { .. } => { - // Report back that no results were found. - // TODO: Implement this for real! + // TODO: Actually implement Fellowship searches, for now none of the player's search criteria are used. + let fellowships = vec![ + FellowshipSearchInfo { + community_id: 1, + master_content_id: 1, + recruiter_content_id: 1, + recruitment_deadline: 0xFFFF_FFFF, + languages: SocialListUILanguages::JAPANESE + | SocialListUILanguages::ENGLISH + | SocialListUILanguages::GERMAN + | SocialListUILanguages::FRENCH, + primary_language: ClientLanguage::English, + recruiter_world_id: connection.config.world_id, + master_world_id: connection.config.world_id, + current_member_count: 999, + target_member_count: 1000, + activity1: FellowshipActivityTag::Chatting, + unk1: [0; 4], + activity2: FellowshipActivityTag::DomanMahjong, + activity3: FellowshipActivityTag::Glamours, + fellowship_name: "Kawari Fellowship Test".to_string(), + recruiter_name: "Kawari Player".to_string(), + master_name: "Kawari Player".to_string(), + fellowship_description: b"The Fellowship's description.".into(), + }, + FellowshipSearchInfo::default(), + FellowshipSearchInfo::default(), + FellowshipSearchInfo::default(), + ]; + let ipc = ServerZoneIpcSegment::new( - ServerZoneIpcData::SearchFellowshipsNoResults { - err_code: 0x01240C89, + ServerZoneIpcData::SearchFellowshipsResults { + unk1: 0xFFFF_FFFF, + sequence_end: 0xFFFF_FFFF, + sequence_current: 0, + fellowships, }, ); + connection.send_ipc_self(ipc).await; } ClientZoneIpcData::StartCountdown { @@ -3765,9 +3798,23 @@ async fn process_packet( // TODO: what to do with these? } ClientZoneIpcData::CreateFellowship { .. } => { - connection - .send_notice("Creating Fellowships is currently unimplemented.") - .await; + let ipc = + ServerZoneIpcSegment::new(ServerZoneIpcData::ShowLinkshellError { + log_message: LogMessageType::UnableToCreateMoreFellowships + as u16, + unk: 0, // I don't have a capture of this yet, but zero works fine. + }); + + connection.send_ipc_self(ipc).await; + } + ClientZoneIpcData::SearchFellowshipsJoin { .. } => { + let ipc = + ServerZoneIpcSegment::new(ServerZoneIpcData::ShowLinkshellError { + log_message: LogMessageType::UnableToJoinMoreFellowships as u16, + unk: 0x0124_0E82, // Observed in a capture, but it doesn't seem to matter what we set it to, zero works too. + }); + + connection.send_ipc_self(ipc).await; } ClientZoneIpcData::Unknown { unk } => { tracing::warn!( From d68f39c7af94e13539f8dba58752d4ca1727553e Mon Sep 17 00:00:00 2001 From: The Dax Date: Wed, 16 Sep 2026 17:37:57 -0400 Subject: [PATCH 3/4] Update checklist: correct outdated tasks --- docs/src/update_ingame_checklist.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/update_ingame_checklist.md b/docs/src/update_ingame_checklist.md index 8e56ef61..16dd629d 100644 --- a/docs/src/update_ingame_checklist.md +++ b/docs/src/update_ingame_checklist.md @@ -122,5 +122,6 @@ The following tests should also be visible to others in the zone upon completion - [ ] Trading: attempt to trade with another player, and ensure the client tells you that the other player is unable trade. - [ ] Run the `/playtime` command and ensure it displays your character's total play time. For the following, ensure the following items display their correct unimplemented messages when trying to do them. -- [ ] Fellowships & Fellowship Finder: ensure the Finder shows no results (and doesn't display an error message), and that creating a Fellowship sends a server notice indicating it's unimplemented. +- [ ] Fellowships: ensure that creating a Fellowship displays "You cannot create any more Fellowships." +- [ ] Fellowship Finder: ensure that the window displays the sample Fellowship, and that attempting to join it displays "You cannot join any more Fellowships." - [ ] Run the `/dice` and `/random` commands and ensure they display both for you and another player From 60ab9b679191aad9e376f481808f33b7bcd624b3 Mon Sep 17 00:00:00 2001 From: The Dax Date: Wed, 16 Sep 2026 21:30:13 -0400 Subject: [PATCH 4/4] Remove outdated comments --- core/src/ipc/zone/server/fellowships.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/ipc/zone/server/fellowships.rs b/core/src/ipc/zone/server/fellowships.rs index 31a1ca49..a20774ce 100644 --- a/core/src/ipc/zone/server/fellowships.rs +++ b/core/src/ipc/zone/server/fellowships.rs @@ -21,10 +21,10 @@ pub struct FellowshipSearchInfo { /// A 32-bit UNIX timestamp indicating when this recruitment listing will expire. pub recruitment_deadline: u32, /// The languages this Fellowship is seeking. - pub languages: SocialListUILanguages, // Fellowship language(s), this might be SocialListUILanguages + pub languages: SocialListUILanguages, #[brw(pad_before = 3, pad_after = 3)] // Probably just padding /// The primary/underlined language of this Fellowship. This seems to be set to the recruiter's client language at the time of Fellowship's creation, or during the time of starting a recruitment listing. - pub primary_language: ClientLanguage, // 0 = Japanese, 1 = English, 2 = German, 3 = French + pub primary_language: ClientLanguage, /// The home world of the Fellowship's recruiter. pub recruiter_world_id: u16, /// The home world of the Fellowship's master.