Skip to content
Draft
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
7 changes: 7 additions & 0 deletions protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ pub struct OnContinueSBAChainEvent {
pub actor_index: u32,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ItemGiveEvent {
pub item_id: u32,
pub count: i32,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Message {
OnAreaEnter(AreaEnterEvent),
Expand All @@ -210,4 +216,5 @@ pub enum Message {
OnPerformSBA(OnPerformSBAEvent),
OnContinueSBAChain(OnContinueSBAChainEvent),
PlayerLoadEvent(PlayerLoadEvent),
ItemGiveEvent(ItemGiveEvent),
}
61 changes: 61 additions & 0 deletions src-hook/src/hooks/item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use anyhow::{anyhow, Result};

use protocol::{ItemGiveEvent, Message};
use retour::static_detour;

use crate::{event, process::Process};

type OnGiveItemFunc = unsafe extern "system" fn(*const usize, u32, i32, u8) -> usize;

static_detour! {
static OnGiveItem: unsafe extern "system" fn(*const usize, u32, i32, u8) -> usize;
}

const ON_GIVE_ITEM_SIG: &str = "E8 $ { ' } 8B 17 4C 89 E1 E8";

#[derive(Clone)]
pub struct OnItemGiveHook {
tx: event::Tx,
}

impl OnItemGiveHook {
pub fn new(tx: event::Tx) -> Self {
OnItemGiveHook { tx }
}

pub fn setup(&self, process: &Process) -> Result<()> {
let cloned_self = self.clone();

if let Ok(on_item_give) = process.search_address(ON_GIVE_ITEM_SIG) {
#[cfg(feature = "console")]
println!("Found on item give");

unsafe {
let func: OnGiveItemFunc = std::mem::transmute(on_item_give);
OnGiveItem
.initialize(func, move |a1, a2, a3, a4| cloned_self.run(a1, a2, a3, a4))?;
OnGiveItem.enable()?;
}
} else {
return Err(anyhow!("Could not find on_item_give"));
}

Ok(())
}

fn run(&self, a1: *const usize, item_id: u32, count: i32, flag: u8) -> usize {
#[cfg(feature = "console")]
println!(
"on item give, item_id={:x}, count={}, flag={}",
item_id, count, flag
);

let ret = unsafe { OnGiveItem.call(a1, item_id, count, flag) };

let _ = self
.tx
.send(Message::ItemGiveEvent(ItemGiveEvent { item_id, count }));

ret
}
}
5 changes: 5 additions & 0 deletions src-hook/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{event, process::Process};
use self::{
area::OnAreaEnterHook,
damage::{OnProcessDamageHook, OnProcessDotHook},
item::OnItemGiveHook,
player::OnLoadPlayerHook,
quest::{OnLoadQuestHook, OnQuestCompleteHook},
sba::{
Expand All @@ -17,6 +18,7 @@ mod area;
mod damage;
mod ffi;
mod globals;
mod item;
mod player;
mod quest;
mod sba;
Expand Down Expand Up @@ -47,6 +49,9 @@ pub fn setup_hooks(tx: event::Tx) -> Result<()> {
OnCheckSBACollisionHook::new(tx.clone()).setup(&process)?;
OnContinueSBAChainHook::new(tx.clone()).setup(&process)?;

/* Item Drops */
OnItemGiveHook::new(tx.clone()).setup(&process)?;

Ok(())
}

Expand Down
3 changes: 2 additions & 1 deletion src-tauri/lang/en/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
"sba-chart": "Skybound Arts Gauge",
"quest-status": "Status",
"overview": "Overview",
"equipment": "Equipment"
"equipment": "Equipment",
"item-drops": "Item Drops"
}
},
"characters": {
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/db/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ pub fn get_logs(
})
.collect::<rusqlite::Result<Vec<LogEntry>>>();

return Ok(rows.unwrap_or(vec![]));
Ok(rows.unwrap_or(vec![]))
}

pub fn get_logs_count(
Expand Down Expand Up @@ -214,5 +214,5 @@ pub fn get_logs_count(

let row: i32 = stmt.query_row(&*params, |r| r.get(0))?;

return Ok(row);
Ok(row)
}
9 changes: 9 additions & 0 deletions src-tauri/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ pub fn setup_db() -> Result<()> {
M::up("ALTER TABLE logs ADD COLUMN quest_id INTEGER"),
M::up("ALTER TABLE logs ADD COLUMN quest_elapsed_time INTEGER"),
M::up("ALTER TABLE logs ADD COLUMN quest_completed BOOLEAN"),
M::up(
r#"CREATE TABLE IF NOT EXISTS item_drops (
id INTEGER PRIMARY KEY,
log_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
item_count INTEGER NOT NULL,
time INTEGER NOT NULL
)"#,
),
]);

info!("Database found, running migrations..");
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ fn connect_and_run_parser(app: AppHandle) {

while let Some(Ok(msg)) = reader.next().await {
// Handle EOF when the game closes.
if msg.len() == 0 {
if msg.is_empty() {
break;
}

Expand Down Expand Up @@ -439,6 +439,9 @@ fn connect_and_run_parser(app: AppHandle) {
protocol::Message::OnContinueSBAChain(event) => {
state.on_continue_sba_chain(event)
}
protocol::Message::ItemGiveEvent(event) => {
state.on_item_give_event(event);
}
}
}
}
Expand Down
87 changes: 61 additions & 26 deletions src-tauri/src/parser/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::{collections::HashMap, io::BufReader};
use anyhow::Result;
use chrono::Utc;
use protocol::{
AreaEnterEvent, DamageEvent, Message, OnAttemptSBAEvent, OnContinueSBAChainEvent,
OnPerformSBAEvent, OnUpdateSBAEvent, PlayerLoadEvent, QuestCompleteEvent,
AreaEnterEvent, DamageEvent, ItemGiveEvent, Message, OnAttemptSBAEvent,
OnContinueSBAChainEvent, OnPerformSBAEvent, OnUpdateSBAEvent, PlayerLoadEvent,
QuestCompleteEvent,
};
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -203,13 +204,21 @@ impl EnemyState {
}
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemDrop {
item_id: u32,
count: i32,
}

/// The necessary details of an encounter that can be used to recreate the state at any point in time.
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Encounter {
pub player_data: [Option<PlayerData>; 4],
pub quest_id: Option<u32>,
pub quest_timer: Option<u32>,

#[serde(default)]
pub quest_completed: bool,

Expand Down Expand Up @@ -293,6 +302,8 @@ pub struct DerivedEncounterState {
pub party: HashMap<u32, PlayerState>,
/// Derived target stats, damage done to each target.
targets: HashMap<u32, EnemyState>,
/// Derived item drops
pub item_drops: Vec<ItemDrop>,
}

impl Default for DerivedEncounterState {
Expand All @@ -305,6 +316,7 @@ impl Default for DerivedEncounterState {
status: ParserStatus::Waiting,
party: HashMap::new(),
targets: HashMap::new(),
item_drops: Vec::new(),
}
}
}
Expand Down Expand Up @@ -371,6 +383,26 @@ impl DerivedEncounterState {
player.update_dps(now, self.start_time);
}
}

fn process_item_event(&mut self, event: &ItemGiveEvent) {
// @TODO(false): Negative item counts are counted as item usage, can track potion usage.
if event.count > 0 {
// Find and update the item drop count if it exists, otherwise add a new item drop.
let existing_item = self
.item_drops
.iter_mut()
.find(|item| item.item_id == event.item_id);

if let Some(existing_item) = existing_item {
existing_item.count += event.count;
} else {
self.item_drops.push(ItemDrop {
item_id: event.item_id,
count: event.count,
});
}
}
}
}

/// The parser for the encounter.
Expand Down Expand Up @@ -445,6 +477,9 @@ impl Parser {
Message::DamageEvent(event) => {
self.derived_state.process_damage_event(*timestamp, event);
}
Message::ItemGiveEvent(event) => {
self.derived_state.process_item_event(event);
}
_ => {
self.derived_state.end_time = *timestamp;
}
Expand All @@ -468,6 +503,9 @@ impl Parser {
self.derived_state.process_damage_event(*timestamp, event);
}
}
Message::ItemGiveEvent(event) => {
self.derived_state.process_item_event(event);
}
_ => {
self.derived_state.end_time = *timestamp;
}
Expand All @@ -487,7 +525,11 @@ impl Parser {

let mut last_event_timestamp = start_time;

for (timestamp, event) in self.encounter.event_log() {
for (timestamp, event) in self
.encounter
.event_log()
.filter(|(_, event)| !matches!(event, Message::ItemGiveEvent(_)))
{
let last_index = ((last_event_timestamp - start_time) / interval) as usize;
let index = ((timestamp - start_time) / interval) as usize;

Expand Down Expand Up @@ -570,29 +612,6 @@ impl Parser {
self.encounter.quest_id = Some(event.quest_id);
self.encounter.quest_timer = Some(event.elapsed_time_in_secs);
self.encounter.quest_completed = true;

if self.status == ParserStatus::InProgress {
self.update_status(ParserStatus::Stopped);

if self.has_damage() {
match self.save_encounter_to_db() {
Ok(id) => {
if let Some(window) = &self.window_handle {
let _ = window.emit("encounter-saved", id);
}
}
Err(e) => {
if let Some(window) = &self.window_handle {
let _ = window.emit("encounter-saved-error", e.to_string());
}
}
}
}

if let Some(window) = &self.window_handle {
let _ = window.emit("encounter-update", &self.derived_state);
}
}
}

// Called when a damage event is received from the game.
Expand Down Expand Up @@ -855,11 +874,27 @@ impl Parser {

let id = conn.last_insert_rowid();

for item_drop in self.derived_state.item_drops.iter() {
conn.execute(
r#"INSERT INTO item_drops (log_id, item_id, item_count, time) VALUES (?, ?, ?, ?)"#,
params![id, item_drop.item_id, item_drop.count, start_datetime.timestamp_millis()],
)?;
}

return Ok(Some(id));
}

Ok(None)
}

pub fn on_item_give_event(&mut self, event: ItemGiveEvent) {
self.encounter.push_event(
Utc::now().timestamp_millis(),
Message::ItemGiveEvent(event.clone()),
);

self.derived_state.process_item_event(&event);
}
}

/// Converts a v0 parser into a v1 parser, but does not reparse the encounter.
Expand Down
7 changes: 4 additions & 3 deletions src-tauri/src/parser/v1/player_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ impl PlayerState {
{
self.last_known_pet_skill = Some(event.action_id);
}

const PET_NORMAL: ActionType = ActionType::Normal(FerrySkillId::PetNormal as u32);
let action = if is_ferry_pet_normal {

if is_ferry_pet_normal {
// Note technically the pet portion of Onslaught will count as a Pet normal, but I think that's fine since
// it does exactly as much as a pet normal. Could consider adding Onslaught (pet) as a separate category
PET_NORMAL
Expand All @@ -61,8 +63,7 @@ impl PlayerState {
}
} else {
event.action_id
};
return action;
}
}

pub fn update_from_damage_event(&mut self, event: &DamageEvent) {
Expand Down
Loading