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) {