From 927c093915550b27548471d42b791f7ce8fcb209 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 1 Jul 2025 19:49:25 -0400 Subject: [PATCH] Send your completed quests list We aren't going to be adding quests ever or anytime soon, so all you can do right now is force every quest to unlock with the aptly named !completeallquests. --- USAGE.md | 1 + resources/opcodes.json | 5 ++++ resources/scripts/commands/Commands.lua | 17 ++++++----- .../commands/debug/CompleteAllQuests.lua | 6 ++++ src/bin/kawari-world.rs | 2 ++ src/ipc/zone/mod.rs | 7 +++++ src/lib.rs | 3 ++ src/world/connection.rs | 29 ++++++++++++++++++- src/world/database.rs | 19 ++++++++---- src/world/lua.rs | 9 ++++++ 10 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 resources/scripts/commands/debug/CompleteAllQuests.lua diff --git a/USAGE.md b/USAGE.md index 6a15c57..c6f0543 100644 --- a/USAGE.md +++ b/USAGE.md @@ -116,6 +116,7 @@ These special debug commands start with `!` and are custom to Kawari. * `!finishevent`: Forcefully finishes the current event, useful if the script has an error and you're stuck talking to something. * `!item `: Gives you an item matching by name. * `!inspect`: Prints info about the player. +* `!completeallquests`: Completes every quest in the game, useful for accessing stuff gated behind quest completion. ### GM commands diff --git a/resources/opcodes.json b/resources/opcodes.json index 127c684..eb0d3ef 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -229,6 +229,11 @@ "name": "InventoryActionAck", "opcode": 483, "size": 16 + }, + { + "name": "QuestCompleteList", + "opcode": 240, + "size": 760 } ], "ClientZoneIpcType": [ diff --git a/resources/scripts/commands/Commands.lua b/resources/scripts/commands/Commands.lua index 6480494..29cb64a 100644 --- a/resources/scripts/commands/Commands.lua +++ b/resources/scripts/commands/Commands.lua @@ -37,11 +37,12 @@ registerGMCommand(GM_TERRITORY_INFO, GM_DIR.."TerritoryInfo.lua") -- Debug commands -- Please keep these in alphabetical order! -registerCommand("classjob", DBG_DIR.."ClassJob.lua") -registerCommand("festival", DBG_DIR.."Festival.lua") -registerCommand("inspect", GM_DIR.."InspectPlayer.lua") -- TODO: remove this once we figure out the GMInspect IPC opcode -registerCommand("nudge", DBG_DIR.."Nudge.lua") -registerCommand("ost", DBG_DIR.."OnScreenTest.lua") -registerCommand("permtest", DBG_DIR.."PermissionTest.lua") -registerCommand("setpos", DBG_DIR.."SetPos.lua") -registerCommand("unlock", DBG_DIR.."Unlock.lua") +registerCommand("classjob", DBG_DIR.."ClassJob.lua") +registerCommand("festival", DBG_DIR.."Festival.lua") +registerCommand("inspect", GM_DIR.."InspectPlayer.lua") -- TODO: remove this once we figure out the GMInspect IPC opcode +registerCommand("nudge", DBG_DIR.."Nudge.lua") +registerCommand("ost", DBG_DIR.."OnScreenTest.lua") +registerCommand("permtest", DBG_DIR.."PermissionTest.lua") +registerCommand("setpos", DBG_DIR.."SetPos.lua") +registerCommand("unlock", DBG_DIR.."Unlock.lua") +registerCommand("completeallquests", DBG_DIR.."CompleteAllQuests.lua") diff --git a/resources/scripts/commands/debug/CompleteAllQuests.lua b/resources/scripts/commands/debug/CompleteAllQuests.lua new file mode 100644 index 0000000..081de0f --- /dev/null +++ b/resources/scripts/commands/debug/CompleteAllQuests.lua @@ -0,0 +1,6 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[completeallquests] " + +function onCommand(args, player) + player:complete_all_quests() +end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 6b19373..59cd7e3 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -317,6 +317,8 @@ async fn client_loop( .await; } + connection.send_quest_information().await; + let zone_id = connection.player_data.zone_id; connection.change_zone(zone_id).await; diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 63cc8a8..b4857d4 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -327,6 +327,13 @@ pub enum ServerZoneIpcData { #[brw(pad_after = 26)] unk2: u16, }, + #[br(pre_assert(*magic == ServerZoneIpcType::QuestCompleteList))] + QuestCompleteList { + // TODO: what is this? a bitmask probably? + #[br(count = 760)] + #[bw(pad_size_to = 760)] + unk1: Vec, + }, Unknown { #[br(count = size - 32)] unk: Vec, diff --git a/src/lib.rs b/src/lib.rs index aff662b..fa556e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,3 +75,6 @@ pub const UNLOCK_BITMASK_SIZE: usize = 92; /// The size of the aetheryte unlock bitmask. // TODO: this can be automatically derived from game data pub const AETHERYTE_UNLOCK_BITMASK_SIZE: usize = 30; + +/// The size of the completed quest bitmask. +pub const COMPLETED_QUEST_BITMASK_SIZE: usize = 691; diff --git a/src/world/connection.rs b/src/world/connection.rs index fce42d0..dde95f1 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -9,7 +9,7 @@ use mlua::Function; use tokio::net::TcpStream; use crate::{ - OBFUSCATION_ENABLED_MODE, + COMPLETED_QUEST_BITMASK_SIZE, OBFUSCATION_ENABLED_MODE, common::{ GameData, ObjectId, ObjectTypeId, Position, timestamp_secs, value_to_flag_byte_index_value, }, @@ -82,6 +82,7 @@ pub struct PlayerData { pub unlocks: Vec, pub aetherytes: Vec, + pub completed_quests: Vec, } /// Represents a single connection between an instance of the client and the world server @@ -777,6 +778,10 @@ impl ZoneConnection { .add_in_next_free_slot(Item::new(1, *id)); self.send_inventory(false).await; } + Task::CompleteAllQuests {} => { + self.player_data.completed_quests = vec![0xFF; COMPLETED_QUEST_BITMASK_SIZE]; + self.send_quest_information().await; + } } } player.queued_tasks.clear(); @@ -1195,4 +1200,26 @@ impl ZoneConnection { .unwrap(); self.player_data.classjob_exp[index as usize] = exp; } + + pub async fn send_quest_information(&mut self) { + // quest complete list + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::QuestCompleteList, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::QuestCompleteList { + unk1: self.player_data.completed_quests.clone(), + }, + ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + } } diff --git a/src/world/database.rs b/src/world/database.rs index bb5f26c..88d8e98 100644 --- a/src/world/database.rs +++ b/src/world/database.rs @@ -4,7 +4,7 @@ use rusqlite::Connection; use serde::Deserialize; use crate::{ - AETHERYTE_UNLOCK_BITMASK_SIZE, UNLOCK_BITMASK_SIZE, + AETHERYTE_UNLOCK_BITMASK_SIZE, COMPLETED_QUEST_BITMASK_SIZE, UNLOCK_BITMASK_SIZE, common::{ CustomizeData, GameData, Position, workdefinitions::{CharaMake, ClientSelectData, RemakeMode}, @@ -48,7 +48,7 @@ impl WorldDatabase { // Create characters data table { - let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING, city_state INTEGER, zone_id INTEGER, pos_x REAL, pos_y REAL, pos_z REAL, rotation REAL, inventory STRING, remake_mode INTEGER, gm_rank INTEGER, classjob_id INTEGER, classjob_levels STRING, classjob_exp STRING, unlocks STRING, aetherytes STRING);"; + let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING, city_state INTEGER, zone_id INTEGER, pos_x REAL, pos_y REAL, pos_z REAL, rotation REAL, inventory STRING, remake_mode INTEGER, gm_rank INTEGER, classjob_id INTEGER, classjob_levels STRING, classjob_exp STRING, unlocks STRING, aetherytes STRING, completed_quests STRING);"; connection.execute(query, ()).unwrap(); } @@ -278,7 +278,7 @@ impl WorldDatabase { .unwrap(); stmt = connection - .prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank, classjob_id, classjob_levels, classjob_exp, unlocks, aetherytes FROM character_data WHERE content_id = ?1") + .prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank, classjob_id, classjob_levels, classjob_exp, unlocks, aetherytes, completed_quests FROM character_data WHERE content_id = ?1") .unwrap(); let ( pos_x, @@ -293,6 +293,7 @@ impl WorldDatabase { classjob_exp, unlocks, aetherytes, + completed_quests, ): ( f32, f32, @@ -306,6 +307,7 @@ impl WorldDatabase { String, String, String, + String, ) = stmt .query_row((content_id,), |row| { Ok(( @@ -321,6 +323,7 @@ impl WorldDatabase { row.get(9)?, row.get(10)?, row.get(11)?, + row.get(12)?, )) }) .unwrap(); @@ -345,6 +348,7 @@ impl WorldDatabase { classjob_exp: serde_json::from_str(&classjob_exp).unwrap(), unlocks: serde_json::from_str(&unlocks).unwrap(), aetherytes: serde_json::from_str(&aetherytes).unwrap(), + completed_quests: serde_json::from_str(&completed_quests).unwrap(), ..Default::default() } } @@ -354,7 +358,7 @@ impl WorldDatabase { let connection = self.connection.lock().unwrap(); let mut stmt = connection - .prepare("UPDATE character_data SET zone_id=?1, pos_x=?2, pos_y=?3, pos_z=?4, rotation=?5, inventory=?6, classjob_id=?7, classjob_levels=?8, classjob_exp=?9, unlocks=?10, aetherytes=?11 WHERE content_id = ?12") + .prepare("UPDATE character_data SET zone_id=?1, pos_x=?2, pos_y=?3, pos_z=?4, rotation=?5, inventory=?6, classjob_id=?7, classjob_levels=?8, classjob_exp=?9, unlocks=?10, aetherytes=?11, completed_quests=?12 WHERE content_id = ?13") .unwrap(); stmt.execute(( data.zone_id, @@ -368,6 +372,7 @@ impl WorldDatabase { serde_json::to_string(&data.classjob_exp).unwrap(), serde_json::to_string(&data.unlocks).unwrap(), serde_json::to_string(&data.aetherytes).unwrap(), + serde_json::to_string(&data.completed_quests).unwrap(), data.content_id, )) .unwrap(); @@ -534,6 +539,9 @@ impl WorldDatabase { // fill out initial aetherytes let aetherytes = vec![0u8; AETHERYTE_UNLOCK_BITMASK_SIZE]; + // fill out initial completed quests` + let completed_quests = vec![0u8; COMPLETED_QUEST_BITMASK_SIZE]; + // insert ids connection .execute( @@ -545,7 +553,7 @@ impl WorldDatabase { // insert char data connection .execute( - "INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90, ?7, ?8, ?9, ?10, ?11);", + "INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90, ?7, ?8, ?9, ?10, ?11, ?12);", ( content_id, name, @@ -558,6 +566,7 @@ impl WorldDatabase { serde_json::to_string(&classjob_exp).unwrap(), serde_json::to_string(&unlocks).unwrap(), serde_json::to_string(&aetherytes).unwrap(), + serde_json::to_string(&completed_quests).unwrap(), ), ) .unwrap(); diff --git a/src/world/lua.rs b/src/world/lua.rs index b9c4be9..3f1fc31 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -36,6 +36,7 @@ pub enum Task { RemoveGil { amount: u32 }, UnlockOrchestrion { id: u16, on: bool }, AddItem { id: u32 }, + CompleteAllQuests {}, } #[derive(Default, Clone)] @@ -369,6 +370,10 @@ impl LuaPlayer { fn add_item(&mut self, id: u32) { self.queued_tasks.push(Task::AddItem { id }); } + + fn complete_all_quests(&mut self) { + self.queued_tasks.push(Task::CompleteAllQuests {}); + } } impl UserData for LuaPlayer { @@ -496,6 +501,10 @@ impl UserData for LuaPlayer { this.add_item(id); Ok(()) }); + methods.add_method_mut("complete_all_quests", |_, this, _: ()| { + this.complete_all_quests(); + Ok(()) + }); } fn add_fields>(fields: &mut F) {