mirror of
https://github.com/redstrate/Kawari.git
synced 2025-07-17 10:47:44 +00:00
Implement better support for buying from gil shops
-You can now purchase multiple things in a row -It shows the proper log messages -For the moment all items go to the very first inventory slot, and overwrite each other
This commit is contained in:
parent
efa07bb87f
commit
eb9d08866e
6 changed files with 239 additions and 19 deletions
|
@ -294,6 +294,16 @@
|
|||
"name": "LevequestCompleteList",
|
||||
"opcode": 371,
|
||||
"size": 232
|
||||
},
|
||||
{
|
||||
"name": "GilShopTransactionAck",
|
||||
"opcode": 974,
|
||||
"size": 32
|
||||
},
|
||||
{
|
||||
"name": "GilShopRelatedUnk",
|
||||
"opcode": 435,
|
||||
"size": 64
|
||||
}
|
||||
],
|
||||
"ClientZoneIpcType": [
|
||||
|
|
|
@ -4,24 +4,34 @@
|
|||
-- Scene 00010: Displays shop interface
|
||||
-- Scene 00255: Unknown, but this was also observed when capturing gil shop transaction packets. When used standalone it softlocks.
|
||||
|
||||
SCENE_GREETING = 00000
|
||||
SCENE_SHOW_SHOP = 00010
|
||||
SCENE_SHOP_END = 00255
|
||||
function onTalk(target, player)
|
||||
--[[ Params observed:
|
||||
--[[ Params observed for SCENE_GREETING:
|
||||
Gil shops: [0, 1]
|
||||
Non- shops: [1, 0]
|
||||
MGP shops: [1, 100]
|
||||
It's unclear what these mean since shops seem to open fine without these.
|
||||
]]
|
||||
player:play_scene(target, EVENT_ID, 00000, 8192, {0})
|
||||
player:play_scene(target, EVENT_ID, SCENE_GREETING, 8192, {0})
|
||||
end
|
||||
|
||||
function onReturn(scene, results, player)
|
||||
if scene == 0 then
|
||||
--[[ Retail sends 221 zeroes as u32s as the params to the shop cutscene, but it opens fine with a single zero u32.
|
||||
Perhaps they are leftovers from earlier expansions? According to Sapphire, the params used to be significantly more complex.
|
||||
Historically, it also seems cutscene 00040 was used instead of 00010 as it is now.
|
||||
When the shop scene finishes and returns control to the server, the server will then have the client play scene 255 with no params.
|
||||
]]
|
||||
player:play_scene(player.id, EVENT_ID, 00010, 1 | 0x2000, {0})
|
||||
elseif scene == 10 then
|
||||
if scene == SCENE_GREETING then
|
||||
params = {}
|
||||
for i=1,221 do
|
||||
params[i] = 0
|
||||
end
|
||||
player:play_scene(player.id, EVENT_ID, SCENE_SHOW_SHOP, 1 | 0x2000, params)
|
||||
elseif scene == SCENE_SHOW_SHOP then
|
||||
player:play_scene(player.id, EVENT_ID, SCENE_SHOP_END, 1 | 0x2000, {})
|
||||
else
|
||||
player:finish_event(EVENT_ID)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,7 +36,7 @@ use tokio::sync::mpsc::{Receiver, UnboundedReceiver, UnboundedSender, channel, u
|
|||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use kawari::INVENTORY_ACTION_ACK_GENERAL;
|
||||
use kawari::{INVENTORY_ACTION_ACK_GENERAL, INVENTORY_ACTION_ACK_SHOP};
|
||||
|
||||
fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) {
|
||||
let (send, recv) = channel(64);
|
||||
|
@ -910,29 +910,42 @@ async fn client_loop(
|
|||
}
|
||||
|
||||
if let Some(item_info) = result {
|
||||
if connection.player_data.inventory.currency.gil.quantity >= item_info.price_mid {
|
||||
// TODO: send the proper response packets!
|
||||
connection.player_data.inventory.currency.gil.quantity -= item_info.price_mid;
|
||||
connection.player_data.inventory.add_in_next_free_slot(Item::new(1, item_info.id));
|
||||
connection.send_inventory(false).await;
|
||||
// TODO: send an actual system notice, this is just a placeholder to provide feedback that the player actually bought something.
|
||||
connection.send_message(&format!("You obtained one or more items: {} (id: {})!", item_info.name, item_info.id)).await;
|
||||
if connection.player_data.inventory.currency.gil.quantity >= *item_quantity * item_info.price_mid {
|
||||
connection.player_data.inventory.currency.gil.quantity -= *item_quantity * item_info.price_mid;
|
||||
// TODO: We need to obtain information on where the item was added, as the client needs to be told.
|
||||
// For now we hardcode it to the very first inventory slot.
|
||||
//connection.player_data.inventory.add_in_next_free_slot(Item::new(*item_quantity, item_info.id));
|
||||
connection.player_data.inventory.add_in_slot(Item::new(*item_quantity, item_info.id), &ContainerType::Inventory0, 0);
|
||||
|
||||
connection.send_gilshop_unk(0x07D0, 0, connection.player_data.inventory.currency.gil.quantity, 1).await;
|
||||
|
||||
connection.send_inventory_ack(u32::MAX, INVENTORY_ACTION_ACK_SHOP as u16).await;
|
||||
|
||||
// TODO: This is hardcoded to the first item slot in the inventory, fix this
|
||||
connection.send_gilshop_unk(0, 0, *item_quantity, item_info.id).await;
|
||||
connection.send_gilshop_ack(*event_id, item_info.id, *item_quantity, item_info.price_mid).await;
|
||||
|
||||
let target_id = connection.player_data.target_actorid;
|
||||
connection.event_scene(&target_id, *event_id, 10, 8193, vec![1, 100]).await;
|
||||
} else {
|
||||
connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await;
|
||||
connection.event_finish(*event_id).await;
|
||||
}
|
||||
} else {
|
||||
connection.send_message("Unable to find shop item, this is a bug in Kawari!").await;
|
||||
connection.event_finish(*event_id).await;
|
||||
}
|
||||
} else if *buy_sell_mode == SELL {
|
||||
// TODO: Implement selling items back to shops
|
||||
connection.send_message("Selling items to shops is not yet implemented. Cancelling event...").await;
|
||||
connection.event_finish(*event_id).await;
|
||||
} else {
|
||||
tracing::error!("Received unknown transaction mode {buy_sell_mode}!");
|
||||
connection.event_finish(*event_id).await;
|
||||
}
|
||||
// Cancel the event for now so the client doesn't get stuck
|
||||
connection.event_finish(*event_id).await;
|
||||
}
|
||||
ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => {
|
||||
connection.player_data.target_actorid = *actor_id;
|
||||
// load event
|
||||
{
|
||||
let ipc = ServerZoneIpcSegment {
|
||||
|
|
|
@ -284,6 +284,12 @@ impl Inventory {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_in_slot(&mut self, item: Item, container_type: &ContainerType, index: u16) {
|
||||
let container = self.get_container_mut(container_type);
|
||||
let slot = container.get_slot_mut(index);
|
||||
slot.clone_from(&item);
|
||||
}
|
||||
|
||||
fn get_container_mut(&mut self, container_type: &ContainerType) -> &mut dyn Storage {
|
||||
match container_type {
|
||||
ContainerType::Inventory0 => &mut self.pages[0],
|
||||
|
|
|
@ -483,6 +483,37 @@ pub enum ServerZoneIpcData {
|
|||
#[bw(pad_size_to = 6)]
|
||||
unk2: Vec<u8>,
|
||||
},
|
||||
#[brw(little)]
|
||||
#[br(pre_assert(*magic == ServerZoneIpcType::GilShopTransactionAck))]
|
||||
GilShopTransactionAck {
|
||||
event_id: u32,
|
||||
/// Always 0x697 when buying?
|
||||
unk1: u32,
|
||||
/// Always 3 when buying?
|
||||
unk2: u32,
|
||||
item_id: u32,
|
||||
item_quantity: u32,
|
||||
#[brw(pad_after = 8)]
|
||||
total_sale_cost: u32,
|
||||
},
|
||||
#[brw(little)]
|
||||
#[br(pre_assert(*magic == ServerZoneIpcType::GilShopRelatedUnk))]
|
||||
GilShopRelatedUnk {
|
||||
/// Starts from zero and increases by one for each of these packets during this gameplay session
|
||||
sequence: u32,
|
||||
#[brw(pad_before = 4)]
|
||||
/// On the first pass, it's 0x07D0, and on the second pass it's the destination storage id
|
||||
unk1_and_dst_storage_id: u16,
|
||||
/// On the first pass it's 0x0000, and on the second pass it's the slot number within the destination storage
|
||||
unk2_and_dst_slot: u16,
|
||||
/// On the first pass, it's the player's remaining gil after the purchase, and on the second pass it's the number of items in that slot after purchase
|
||||
gil_and_item_quantity: u32,
|
||||
/// On the first pass it's 0x0000_0001 and on the second pass it's the item id
|
||||
unk3_and_item_id: u32,
|
||||
#[brw(pad_before = 12, pad_after = 28)]
|
||||
/// Always 0x7530_0000
|
||||
unk4: u32,
|
||||
},
|
||||
Unknown {
|
||||
#[br(count = size - 32)]
|
||||
unk: Vec<u8>,
|
||||
|
@ -970,6 +1001,28 @@ mod tests {
|
|||
unk2: Vec::default(),
|
||||
},
|
||||
),
|
||||
(
|
||||
ServerZoneIpcType::GilShopTransactionAck,
|
||||
ServerZoneIpcData::GilShopTransactionAck {
|
||||
event_id: 0,
|
||||
unk1: 0,
|
||||
unk2: 0,
|
||||
item_id: 0,
|
||||
item_quantity: 0,
|
||||
total_sale_cost: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
ServerZoneIpcType::GilShopRelatedUnk,
|
||||
ServerZoneIpcData::GilShopRelatedUnk {
|
||||
sequence: 0,
|
||||
unk1_and_dst_storage_id: 0,
|
||||
unk2_and_dst_slot: 0,
|
||||
gil_and_item_quantity: 0,
|
||||
unk3_and_item_id: 0,
|
||||
unk4: 0,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
for (opcode, data) in &ipc_types {
|
||||
|
|
|
@ -20,10 +20,10 @@ use crate::{
|
|||
zone::{
|
||||
ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory,
|
||||
ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, Config,
|
||||
ContainerInfo, CurrencyInfo, DisplayFlag, EffectKind, Equip, GameMasterRank, InitZone,
|
||||
ItemInfo, Move, NpcSpawn, ObjectKind, PlayerStats, PlayerSubKind, QuestActiveList,
|
||||
ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect, StatusEffectList,
|
||||
UpdateClassInfo, Warp, WeatherChange,
|
||||
ContainerInfo, CurrencyInfo, DisplayFlag, EffectKind, Equip, EventScene,
|
||||
GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind, PlayerStats,
|
||||
PlayerSubKind, QuestActiveList, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect,
|
||||
StatusEffectList, UpdateClassInfo, Warp, WeatherChange,
|
||||
},
|
||||
},
|
||||
opcodes::ServerZoneIpcType,
|
||||
|
@ -84,6 +84,9 @@ pub struct PlayerData {
|
|||
pub aetherytes: Vec<u8>,
|
||||
pub completed_quests: Vec<u8>,
|
||||
pub item_sequence: u32,
|
||||
pub shop_sequence: u32,
|
||||
/// Store the target actor id for the purpose of chaining cutscenes.
|
||||
pub target_actorid: ObjectTypeId,
|
||||
}
|
||||
|
||||
/// Various obsfucation-related bits like the seeds and keys for this connection.
|
||||
|
@ -888,7 +891,49 @@ impl ZoneConnection {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn event_scene(
|
||||
&mut self,
|
||||
target: &ObjectTypeId,
|
||||
event_id: u32,
|
||||
scene: u16,
|
||||
scene_flags: u32,
|
||||
params: Vec<u32>,
|
||||
) {
|
||||
let scene = EventScene {
|
||||
actor_id: *target,
|
||||
event_id,
|
||||
scene,
|
||||
scene_flags,
|
||||
params_count: params.len() as u8,
|
||||
params,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some((op_code, data)) = scene.package_scene() {
|
||||
let ipc = ServerZoneIpcSegment {
|
||||
op_code,
|
||||
timestamp: timestamp_secs(),
|
||||
data,
|
||||
..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;
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Unable to play event {event_id}, scene {:?}, scene_flags {scene_flags}!",
|
||||
scene
|
||||
);
|
||||
self.event_finish(event_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn event_finish(&mut self, handler_id: u32) {
|
||||
self.player_data.target_actorid = ObjectTypeId::default();
|
||||
// sent event finish
|
||||
{
|
||||
let ipc = ServerZoneIpcSegment {
|
||||
|
@ -931,6 +976,89 @@ impl ZoneConnection {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn send_inventory_ack(&mut self, sequence: u32, action_type: u16) {
|
||||
let ipc = ServerZoneIpcSegment {
|
||||
op_code: ServerZoneIpcType::InventoryActionAck,
|
||||
timestamp: timestamp_secs(),
|
||||
data: ServerZoneIpcData::InventoryActionAck {
|
||||
sequence,
|
||||
action_type,
|
||||
},
|
||||
..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;
|
||||
|
||||
self.player_data.item_sequence += 1;
|
||||
}
|
||||
|
||||
pub async fn send_gilshop_ack(
|
||||
&mut self,
|
||||
event_id: u32,
|
||||
item_id: u32,
|
||||
item_quantity: u32,
|
||||
price_per_item: u32,
|
||||
) {
|
||||
let ipc = ServerZoneIpcSegment {
|
||||
op_code: ServerZoneIpcType::GilShopTransactionAck,
|
||||
timestamp: timestamp_secs(),
|
||||
data: ServerZoneIpcData::GilShopTransactionAck {
|
||||
event_id,
|
||||
unk1: 0x697,
|
||||
unk2: 3,
|
||||
item_id,
|
||||
item_quantity,
|
||||
total_sale_cost: item_quantity * price_per_item,
|
||||
},
|
||||
..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;
|
||||
}
|
||||
|
||||
pub async fn send_gilshop_unk(
|
||||
&mut self,
|
||||
unk1_and_dst_storage_id: u16,
|
||||
unk2_and_dst_slot: u16,
|
||||
gil_and_item_quantity: u32,
|
||||
unk3_and_item_id: u32,
|
||||
) {
|
||||
let ipc = ServerZoneIpcSegment {
|
||||
op_code: ServerZoneIpcType::GilShopRelatedUnk,
|
||||
timestamp: timestamp_secs(),
|
||||
data: ServerZoneIpcData::GilShopRelatedUnk {
|
||||
sequence: self.player_data.shop_sequence,
|
||||
unk1_and_dst_storage_id,
|
||||
unk2_and_dst_slot,
|
||||
gil_and_item_quantity,
|
||||
unk3_and_item_id,
|
||||
unk4: 0x7530_0000,
|
||||
},
|
||||
..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;
|
||||
|
||||
self.player_data.shop_sequence += 1;
|
||||
}
|
||||
|
||||
pub async fn begin_log_out(&mut self) {
|
||||
self.gracefully_logged_out = true;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue