From aecad81134663dfc96b3c7f4e8732c8f6289d3f6 Mon Sep 17 00:00:00 2001 From: CakeLancelot Date: Wed, 8 Jan 2025 03:21:02 -0600 Subject: [PATCH 1/3] Implement rockets and grenades: Validation/cleanup not finished yet. --- src/ai.rs | 2 + src/bin/shard/combat.rs | 269 +++++++++++++++++++++++++++++++++++++--- src/bin/shard/main.rs | 4 + src/chunk.rs | 23 ++++ src/entity/player.rs | 50 ++++++++ src/item.rs | 7 +- src/lib.rs | 4 + src/net/ffclient.rs | 4 + src/skills.rs | 48 ++++--- src/tabledata.rs | 2 + 10 files changed, 376 insertions(+), 37 deletions(-) diff --git a/src/ai.rs b/src/ai.rs index daab0c59..37e83101 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -1085,6 +1085,8 @@ impl AINode for CheckAttack { npc.get_id(), target_ids, false, + (None, None), + (None, None), state, clients, )); diff --git a/src/bin/shard/combat.rs b/src/bin/shard/combat.rs index b9086dca..81a70278 100644 --- a/src/bin/shard/combat.rs +++ b/src/bin/shard/combat.rs @@ -1,12 +1,16 @@ +use std::time::SystemTime; + use rusty_fusion::{ - entity::{Combatant, Entity}, + defines::EQUIP_SLOT_HAND, + entity::{Combatant, Entity, EntityID, Projectile, ProjectileKind}, error::*, net::{ packet::{PacketID::*, *}, - ClientMap, + ClientMap, FFClient, }, skills, state::ShardServerState, + unused, Position, }; #[allow(non_camel_case_types)] @@ -19,33 +23,34 @@ struct sTargetNpcId { } impl FFPacket for sTargetNpcId {} -pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { - const MAX_TARGETS: usize = 3; +fn get_targets( + client: &mut FFClient, + state: &mut ShardServerState, + target_count: usize, + max_targets: Option, +) -> FFResult<(Vec, u32)> { const BATTERY_BASE_COST: u32 = 6; - let client = clients.get_self(); - let pc_id = client.get_player_id()?; - let pkt: sP_CL2FE_REQ_PC_ATTACK_NPCs = *client.get_packet(P_CL2FE_REQ_PC_ATTACK_NPCs)?; - let target_count = pkt.iNPCCnt as usize; - if target_count == 0 { - return Ok(()); - } - - let mut target_ids = Vec::with_capacity(MAX_TARGETS); + let mut target_ids = Vec::with_capacity(max_targets.unwrap_or(3)); let mut weapon_boosts_needed = 0; + for i in 0..target_count { // TODO stricter anti-cheat. // validate target count, range, attack cooldown, etc against weapon stats - if i >= MAX_TARGETS { + if max_targets.is_some_and(|max| i >= max) { return Err(FFError::build( Severity::Warning, format!( - "Player {} tried to attack {} NPCs (max {})", - pc_id, pkt.iNPCCnt, MAX_TARGETS + "Tried to attack {} entities (max {})", + target_count, + max_targets.unwrap() ), )); } - let npc_id = client.get_struct::()?.iNPC_ID; + let Ok(trailer) = client.get_struct::() else { + break; + }; + let npc_id = trailer.iNPC_ID; let npc = match state.get_npc(npc_id) { Ok(npc) => npc, Err(e) => { @@ -57,6 +62,23 @@ pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> target_ids.push(npc.get_id()); } + Ok((target_ids, weapon_boosts_needed)) +} + +pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { + const MAX_TARGETS: usize = 3; + + let client = clients.get_self(); + let pc_id = client.get_player_id()?; + let pkt: sP_CL2FE_REQ_PC_ATTACK_NPCs = *client.get_packet(P_CL2FE_REQ_PC_ATTACK_NPCs)?; + let target_count = pkt.iNPCCnt as usize; + if target_count == 0 { + return Ok(()); + } + + let (target_ids, weapon_boosts_needed) = + get_targets(client, state, target_count, Some(MAX_TARGETS))?; + // consume weapon boosts let player = state.get_player_mut(pc_id)?; let weapon_boosts = player.get_weapon_boosts(); @@ -69,7 +91,218 @@ pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> }; // attack handler - skills::do_basic_attack(player.get_id(), &target_ids, charged, state, clients)?; + skills::do_basic_attack( + player.get_id(), + &target_ids, + charged, + (None, None), + (None, None), + state, + clients, + )?; + + Ok(()) +} + +pub fn pc_grenade_fire(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { + let client = clients.get_self(); + let pc_id = client.get_player_id()?; + let player = state.get_player_mut(pc_id)?; + + let pkt: sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE = + *client.get_packet(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE)?; + + let Some(weapon) = player.get_equipped()[EQUIP_SLOT_HAND as usize] else { + return Err(FFError::build( + Severity::Warning, + "Tried to throw grenade but no weapon in hand".to_string(), + )); + }; + + let weapon_data = weapon.get_stats()?; + + if weapon_data.target_mode != Some(6) { + return Err(FFError::build( + Severity::Warning, + "Tried to throw grenade but holding wrong weapon type".to_string(), + )); + } + + let projectile = Projectile { + projectile_kind: ProjectileKind::Grenade, + single_power: player.get_single_power(), + multi_power: player.get_multi_power(), + charged: false, + end_time: SystemTime::now() + weapon_data.projectile_time.unwrap(), + start_pos: Position { + z: player.get_position().z + 100, + ..player.get_position() + }, + // todo: validate + end_pos: Position { + x: pkt.iToX, + y: pkt.iToY, + z: pkt.iToZ + 100, + }, + }; + + let Some(bullet_id) = player.add_projectile(projectile.clone()) else { + return Err(FFError::build( + Severity::Warning, + "Attempted to throw grenade but player has too many projectiles".to_string(), + )); + }; + + let resp = sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC { + iSkillID: unused!(), + iToX: projectile.end_pos.x, + iToY: projectile.end_pos.y, + iToZ: projectile.end_pos.z, + iBulletID: bullet_id, + Bullet: projectile.clone().into(), + iBatteryW: player.get_weapon_boosts() as i32, + bNanoDeactive: unused!(), + iNanoID: unused!(), + iNanoStamina: unused!(), + }; + + let bcast = sP_FE2CL_PC_GRENADE_STYLE_FIRE { + iPC_ID: pc_id, + iToX: pkt.iToX, + iToY: pkt.iToY, + iToZ: pkt.iToZ, + iBulletID: bullet_id, + Bullet: projectile.into(), + bNanoDeactive: unused!(), + }; + + state + .entity_map + .for_each_around(EntityID::Player(pc_id), clients, |c| { + c.send_packet(P_FE2CL_PC_GRENADE_STYLE_FIRE, &bcast) + }); + clients + .get_self() + .send_packet(P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, &resp) +} + +pub fn pc_rocket_fire(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { + let client = clients.get_self(); + let pc_id = client.get_player_id()?; + let player = state.get_player_mut(pc_id)?; + + let pkt: sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE = + *client.get_packet(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE)?; + + let Some(weapon) = player.get_equipped()[EQUIP_SLOT_HAND as usize] else { + return Err(FFError::build( + Severity::Warning, + "Tried to fire rocket but no weapon in hand".to_string(), + )); + }; + + let weapon_data = weapon.get_stats()?; + + if weapon_data.target_mode != Some(5) { + return Err(FFError::build( + Severity::Warning, + "Tried to fire rocket but holding wrong weapon type".to_string(), + )); + } + + let projectile = Projectile { + projectile_kind: ProjectileKind::Rocket(weapon.id), + single_power: player.get_single_power(), + multi_power: player.get_multi_power(), + charged: false, + end_time: SystemTime::now() + weapon_data.projectile_time.unwrap(), + start_pos: Position { + z: player.get_position().z + 100, + ..player.get_position() + }, + // todo: validate + end_pos: Position { + x: pkt.iToX, + y: pkt.iToY, + z: pkt.iToZ + 100, + }, + }; + + let Some(bullet_id) = player.add_projectile(projectile.clone()) else { + return Err(FFError::build( + Severity::Warning, + "Attempted to fire rocket but player has too many projectiles".to_string(), + )); + }; + + let resp = sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC { + iSkillID: unused!(), + iToX: projectile.end_pos.x, + iToY: projectile.end_pos.y, + iToZ: projectile.end_pos.z, + iBulletID: bullet_id, + Bullet: projectile.clone().into(), + iBatteryW: player.get_weapon_boosts() as i32, + bNanoDeactive: unused!(), + iNanoID: unused!(), + iNanoStamina: unused!(), + }; + + let bcast = sP_FE2CL_PC_GRENADE_STYLE_FIRE { + iPC_ID: pc_id, + iToX: pkt.iToX, + iToY: pkt.iToY, + iToZ: pkt.iToZ, + iBulletID: bullet_id, + Bullet: projectile.into(), + bNanoDeactive: unused!(), + }; + + state + .entity_map + .for_each_around(EntityID::Player(pc_id), clients, |c| { + c.send_packet(P_FE2CL_PC_GRENADE_STYLE_FIRE, &bcast) + }); + clients + .get_self() + .send_packet(P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, &resp) +} + +pub fn pc_projectile_hit(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { + const EXPLOSION_RADIUS: u32 = 250; // todo pull from tdata + let client = clients.get_self(); + let pc_id = client.get_player_id()?; + let player = state.get_player_mut(pc_id)?; + + // Rocket and grenade hit are indentical + let pkt: sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT = *client.get_packet_unchecked()?; + + let Some(projectile) = player.remove_projectile(pkt.iBulletID) else { + return Ok(()); + }; + + let (target_ids, _weapon_boosts_needed) = + get_targets(client, state, pkt.iTargetCnt as usize, None)?; + let player = state.get_player(pc_id)?; + + // todo: validate + let hit_position = Position::new(pkt.iX, pkt.iY, pkt.iZ); + let target_ids = state.entity_map.filter_ids_in_proximity( + hit_position, + player.instance_id, + &target_ids, + EXPLOSION_RADIUS, + ); + + skills::do_basic_attack( + EntityID::Player(pc_id), + &target_ids, + projectile.charged, + (Some(projectile.single_power), Some(projectile.multi_power)), + (Some(pkt.iBulletID), Some(projectile.clone())), + state, + clients, + )?; Ok(()) } diff --git a/src/bin/shard/main.rs b/src/bin/shard/main.rs index c9beda20..6952f5fb 100644 --- a/src/bin/shard/main.rs +++ b/src/bin/shard/main.rs @@ -307,6 +307,10 @@ fn handle_packet( P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT => chat::pc_avatar_emotes_chat(&mut clients, state), // P_CL2FE_REQ_PC_ATTACK_NPCs => combat::pc_attack_npcs(&mut clients, state), + P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE => combat::pc_grenade_fire(&mut clients, state), + P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE => combat::pc_rocket_fire(&mut clients, state), + P_CL2FE_REQ_PC_ROCKET_STYLE_HIT => combat::pc_projectile_hit(&mut clients, state), + P_CL2FE_REQ_PC_GRENADE_STYLE_HIT => combat::pc_projectile_hit(&mut clients, state), // P_CL2FE_REQ_ITEM_MOVE => item::item_move(&mut clients, state), P_CL2FE_REQ_PC_ITEM_DELETE => item::item_delete(clients.get_self(), state), diff --git a/src/chunk.rs b/src/chunk.rs index 006d80a5..31182aef 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -287,6 +287,29 @@ impl EntityMap { Ok(()) } + pub fn filter_ids_in_proximity( + &self, + point: Position, + instance_id: InstanceID, + ids: &[EntityID], + range: u32, + ) -> Vec { + let mut within_range = Vec::with_capacity(ids.len()); + for id in ids { + if let Some(entry) = self.registry.get(id) { + if entry.entity.get_chunk_coords().i != instance_id { + continue; + } + if entry.entity.get_position().distance_to(&point) > range { + continue; + } + + within_range.push(*id); + } + } + within_range + } + pub fn gen_next_pc_id(&mut self) -> i32 { let id = self.next_pc_id; if id == u32::MAX { diff --git a/src/entity/player.rs b/src/entity/player.rs index 1de3fe1a..e3b3a8c8 100644 --- a/src/entity/player.rs +++ b/src/entity/player.rs @@ -284,6 +284,36 @@ pub struct PreWarpData { pub position: Position, } +#[derive(Debug, Clone)] +pub enum ProjectileKind { + Grenade, + Rocket(i16), +} + +#[derive(Debug, Clone)] +pub struct Projectile { + pub projectile_kind: ProjectileKind, + pub single_power: i32, + pub multi_power: i32, + pub charged: bool, + pub start_pos: Position, + pub end_pos: Position, + pub end_time: SystemTime, +} + +impl From for sPCBullet { + fn from(value: Projectile) -> Self { + Self { + eAT: 0, + iID: match value.projectile_kind { + ProjectileKind::Grenade => 1, + ProjectileKind::Rocket(item_id) => item_id as i32, + }, + bCharged: value.charged as i32, + } + } +} + #[derive(Debug, Clone)] pub struct BuddyListEntry { pub pc_uid: i64, @@ -428,6 +458,8 @@ pub struct Player { fusion_matter: u32, nano_potions: u32, weapon_boosts: u32, + owned_projectiles: HashMap, + last_projectile_id: i8, pub buddy_list_synced: bool, buddy_list: BuddyList, pub buddy_offered_to: Option, @@ -1211,6 +1243,24 @@ impl Player { self.nano_potions } + pub fn add_projectile(&mut self, projectile: Projectile) -> Option { + if self.owned_projectiles.len() == SIZEOF_PC_BULLET_SLOT as usize { + return None; + } + let bullet_id = self.last_projectile_id; + self.last_projectile_id += 1; + if self.last_projectile_id == SIZEOF_PC_BULLET_SLOT as i8 { + self.last_projectile_id = 0 + }; + + self.owned_projectiles.insert(bullet_id, projectile); + Some(bullet_id) + } + + pub fn remove_projectile(&mut self, projectile_id: i8) -> Option { + self.owned_projectiles.remove(&projectile_id) + } + pub fn set_pre_warp(&mut self) { // we only save pre-warp when we're not in an instance if self.instance_id.instance_num.is_none() { diff --git a/src/item.rs b/src/item.rs index 310369ab..6ee79c91 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,4 +1,7 @@ -use std::{cmp::min, time::SystemTime}; +use std::{ + cmp::min, + time::{Duration, SystemTime}, +}; use crate::{ defines::*, @@ -160,6 +163,8 @@ pub struct ItemStats { pub gender: Option, pub single_power: Option, pub multi_power: Option, + pub target_mode: Option, + pub projectile_time: Option, pub defense: Option, pub speed: Option, } diff --git a/src/lib.rs b/src/lib.rs index e2b6e7e1..f51cc8cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,10 @@ pub struct Position { pub z: i32, } impl Position { + pub fn new(x: i32, y: i32, z: i32) -> Self { + Self { x, y, z } + } + pub fn distance_to_weighted(&self, other: &Position, weights: (f32, f32, f32)) -> u32 { // scaling down for the multiplication helps to avoid overflow here const DIST_MATH_SCALE: f32 = 100.0; diff --git a/src/net/ffclient.rs b/src/net/ffclient.rs index 3d4c0f61..6e9676e0 100644 --- a/src/net/ffclient.rs +++ b/src/net/ffclient.rs @@ -309,6 +309,10 @@ impl FFClient { self.in_buf.get_packet(pkt_id) } + pub fn get_packet_unchecked(&mut self) -> FFResult<&T> { + self.in_buf.get_packet(self.peek_packet_id()?) + } + pub fn get_struct(&mut self) -> FFResult<&T> { self.in_buf.get_struct() } diff --git a/src/skills.rs b/src/skills.rs index c5c55e50..c3da43d5 100644 --- a/src/skills.rs +++ b/src/skills.rs @@ -2,7 +2,7 @@ use rand::Rng; use crate::{ defines::*, - entity::{Combatant, EntityID}, + entity::{Combatant, EntityID, Projectile}, enums::CombatStyle, error::*, net::{ @@ -23,6 +23,8 @@ pub fn do_basic_attack( attacker_id: EntityID, target_ids: &[EntityID], charged: bool, + powers: (Option, Option), + projectile: (Option, Option), state: &mut ShardServerState, clients: &mut ClientMap, ) -> FFResult<()> { @@ -32,9 +34,9 @@ pub fn do_basic_attack( let mut attacker_client = attacker.get_client(clients); let power = if target_ids.len() == 1 { - attacker.get_single_power() + powers.0.unwrap_or(attacker.get_single_power()) } else { - attacker.get_multi_power() + powers.1.unwrap_or(attacker.get_multi_power()) }; let basic_attack = BasicAttack { power, @@ -129,22 +131,32 @@ pub fn do_basic_attack( let mut payload = PacketBuffer::default(); match attacker_id { EntityID::Player(pc_id) => { - // response packet - let player = state.get_player(pc_id).unwrap(); - let resp = sP_FE2CL_PC_ATTACK_NPCs_SUCC { - iBatteryW: player.get_weapon_boosts() as i32, - iNPCCnt: npc_attack_count as i32, - }; - if let Some(client) = &mut attacker_client { - client.queue_packet(P_FE2CL_PC_ATTACK_NPCs_SUCC, &resp); - } + if projectile.0.is_some() && projectile.1.is_some() { + let resp = sP_FE2CL_PC_GRENADE_STYLE_HIT { + iPC_ID: pc_id, + iBulletID: projectile.0.unwrap(), + Bullet: projectile.1.unwrap().into(), + iTargetCnt: npc_attack_count as i32, + }; + payload.queue_packet(P_FE2CL_PC_GRENADE_STYLE_HIT, &resp); + } else { + // response packet + let player = state.get_player(pc_id).unwrap(); + let resp = sP_FE2CL_PC_ATTACK_NPCs_SUCC { + iBatteryW: player.get_weapon_boosts() as i32, + iNPCCnt: npc_attack_count as i32, + }; + if let Some(client) = &mut attacker_client { + client.queue_packet(P_FE2CL_PC_ATTACK_NPCs_SUCC, &resp); + } - // broadcast packet - let pkt = sP_FE2CL_PC_ATTACK_NPCs { - iPC_ID: pc_id, - iNPCCnt: npc_attack_count as i32, - }; - payload.queue_packet(P_FE2CL_PC_ATTACK_NPCs, &pkt); + // broadcast packet + let pkt = sP_FE2CL_PC_ATTACK_NPCs { + iPC_ID: pc_id, + iNPCCnt: npc_attack_count as i32, + }; + payload.queue_packet(P_FE2CL_PC_ATTACK_NPCs, &pkt); + } } EntityID::NPC(npc_id) => { let pkt = sP_FE2CL_NPC_ATTACK_CHARs { diff --git a/src/tabledata.rs b/src/tabledata.rs index 6735fc5a..f4b66002 100644 --- a/src/tabledata.rs +++ b/src/tabledata.rs @@ -994,6 +994,8 @@ fn load_item_data( gender: data.m_iReqSex.map(|v| v as i8), single_power: data.m_iPointRat, multi_power: data.m_iGroupRat, + target_mode: data.m_iTargetMode, + projectile_time: data.m_iDeliverTime.map(|v| Duration::from_millis(v as u64)), defense: data.m_iDefenseRat, speed: data.m_iUp_runSpeed, }; From fb1fcb04b6ab630b0389504d205ae50561f096fb Mon Sep 17 00:00:00 2001 From: CakeLancelot Date: Sat, 11 Jan 2025 03:13:43 -0600 Subject: [PATCH 2/3] Use singular packet handler for both projectile types, implement boosts --- src/bin/shard/combat.rs | 147 +++++++++++++++------------------------- src/bin/shard/main.rs | 4 +- src/entity/player.rs | 11 +++ src/enums.rs | 11 +++ src/item.rs | 4 +- src/tabledata.rs | 6 +- 6 files changed, 84 insertions(+), 99 deletions(-) diff --git a/src/bin/shard/combat.rs b/src/bin/shard/combat.rs index 81a70278..6cd25fbb 100644 --- a/src/bin/shard/combat.rs +++ b/src/bin/shard/combat.rs @@ -3,6 +3,7 @@ use std::time::SystemTime; use rusty_fusion::{ defines::EQUIP_SLOT_HAND, entity::{Combatant, Entity, EntityID, Projectile, ProjectileKind}, + enums::WeaponTargetMode, error::*, net::{ packet::{PacketID::*, *}, @@ -13,6 +14,8 @@ use rusty_fusion::{ unused, Position, }; +const BATTERY_BASE_COST: u32 = 6; + #[allow(non_camel_case_types)] #[allow(non_snake_case)] #[repr(packed(4))] @@ -29,8 +32,6 @@ fn get_targets( target_count: usize, max_targets: Option, ) -> FFResult<(Vec, u32)> { - const BATTERY_BASE_COST: u32 = 6; - let mut target_ids = Vec::with_capacity(max_targets.unwrap_or(3)); let mut weapon_boosts_needed = 0; @@ -81,14 +82,8 @@ pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> // consume weapon boosts let player = state.get_player_mut(pc_id)?; - let weapon_boosts = player.get_weapon_boosts(); - let charged = if weapon_boosts >= weapon_boosts_needed { - player.set_weapon_boosts(weapon_boosts - weapon_boosts_needed); - true - } else { - player.set_weapon_boosts(0); - false - }; + + let charged = player.consume_weapon_boosts(weapon_boosts_needed); // attack handler skills::do_basic_attack( @@ -104,117 +99,78 @@ pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> Ok(()) } -pub fn pc_grenade_fire(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { - let client = clients.get_self(); - let pc_id = client.get_player_id()?; - let player = state.get_player_mut(pc_id)?; - - let pkt: sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE = - *client.get_packet(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE)?; - - let Some(weapon) = player.get_equipped()[EQUIP_SLOT_HAND as usize] else { - return Err(FFError::build( - Severity::Warning, - "Tried to throw grenade but no weapon in hand".to_string(), - )); - }; - - let weapon_data = weapon.get_stats()?; - - if weapon_data.target_mode != Some(6) { - return Err(FFError::build( - Severity::Warning, - "Tried to throw grenade but holding wrong weapon type".to_string(), - )); - } - - let projectile = Projectile { - projectile_kind: ProjectileKind::Grenade, - single_power: player.get_single_power(), - multi_power: player.get_multi_power(), - charged: false, - end_time: SystemTime::now() + weapon_data.projectile_time.unwrap(), - start_pos: Position { - z: player.get_position().z + 100, - ..player.get_position() - }, - // todo: validate - end_pos: Position { - x: pkt.iToX, - y: pkt.iToY, - z: pkt.iToZ + 100, - }, - }; - - let Some(bullet_id) = player.add_projectile(projectile.clone()) else { - return Err(FFError::build( - Severity::Warning, - "Attempted to throw grenade but player has too many projectiles".to_string(), - )); - }; - - let resp = sP_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC { - iSkillID: unused!(), - iToX: projectile.end_pos.x, - iToY: projectile.end_pos.y, - iToZ: projectile.end_pos.z, - iBulletID: bullet_id, - Bullet: projectile.clone().into(), - iBatteryW: player.get_weapon_boosts() as i32, - bNanoDeactive: unused!(), - iNanoID: unused!(), - iNanoStamina: unused!(), - }; +pub fn pc_fire_rocket(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { + let pkt: sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE = *clients + .get_self() + .get_packet(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE)?; - let bcast = sP_FE2CL_PC_GRENADE_STYLE_FIRE { - iPC_ID: pc_id, + let new_pkt = sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE { + iSkillID: pkt.iSkillID, iToX: pkt.iToX, iToY: pkt.iToY, iToZ: pkt.iToZ, - iBulletID: bullet_id, - Bullet: projectile.into(), - bNanoDeactive: unused!(), }; - state - .entity_map - .for_each_around(EntityID::Player(pc_id), clients, |c| { - c.send_packet(P_FE2CL_PC_GRENADE_STYLE_FIRE, &bcast) - }); - clients + pc_fire_projectile(clients, state, new_pkt, false) +} + +pub fn pc_fire_grenade(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { + let pkt: sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE = *clients .get_self() - .send_packet(P_FE2CL_REP_PC_GRENADE_STYLE_FIRE_SUCC, &resp) + .get_packet(P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE)?; + + pc_fire_projectile(clients, state, pkt, true) } -pub fn pc_rocket_fire(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { +fn pc_fire_projectile( + clients: &mut ClientMap, + state: &mut ShardServerState, + pkt: sP_CL2FE_REQ_PC_GRENADE_STYLE_FIRE, + is_grenade: bool, +) -> FFResult<()> { let client = clients.get_self(); let pc_id = client.get_player_id()?; let player = state.get_player_mut(pc_id)?; - let pkt: sP_CL2FE_REQ_PC_ROCKET_STYLE_FIRE = - *client.get_packet(P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE)?; + let action_message = if is_grenade { + "throw grenade" + } else { + "fire rocket" + }; + let expected_target_mode = if is_grenade { + WeaponTargetMode::Grenade + } else { + WeaponTargetMode::Rocket + }; let Some(weapon) = player.get_equipped()[EQUIP_SLOT_HAND as usize] else { return Err(FFError::build( Severity::Warning, - "Tried to fire rocket but no weapon in hand".to_string(), + format!("Tried to {} but no weapon in hand", action_message), )); }; let weapon_data = weapon.get_stats()?; - if weapon_data.target_mode != Some(5) { + if weapon_data.target_mode != Some(expected_target_mode) { return Err(FFError::build( Severity::Warning, - "Tried to fire rocket but holding wrong weapon type".to_string(), + format!("Tried to {} but holding wrong weapon type", action_message), )); } + let weapon_boosts_needed = BATTERY_BASE_COST + weapon_data.required_level as u32; + let charged = player.consume_weapon_boosts(weapon_boosts_needed); + let projectile = Projectile { - projectile_kind: ProjectileKind::Rocket(weapon.id), + projectile_kind: if is_grenade { + ProjectileKind::Grenade + } else { + ProjectileKind::Rocket(weapon.id) + }, single_power: player.get_single_power(), multi_power: player.get_multi_power(), - charged: false, + charged, end_time: SystemTime::now() + weapon_data.projectile_time.unwrap(), start_pos: Position { z: player.get_position().z + 100, @@ -231,7 +187,10 @@ pub fn pc_rocket_fire(clients: &mut ClientMap, state: &mut ShardServerState) -> let Some(bullet_id) = player.add_projectile(projectile.clone()) else { return Err(FFError::build( Severity::Warning, - "Attempted to fire rocket but player has too many projectiles".to_string(), + format!( + "Attempted to {} but player has too many projectiles", + action_message + ), )); }; @@ -269,7 +228,7 @@ pub fn pc_rocket_fire(clients: &mut ClientMap, state: &mut ShardServerState) -> } pub fn pc_projectile_hit(clients: &mut ClientMap, state: &mut ShardServerState) -> FFResult<()> { - const EXPLOSION_RADIUS: u32 = 250; // todo pull from tdata + const EXPLOSION_RADIUS: u32 = 300; // TODO pull from tdata let client = clients.get_self(); let pc_id = client.get_player_id()?; let player = state.get_player_mut(pc_id)?; @@ -285,7 +244,7 @@ pub fn pc_projectile_hit(clients: &mut ClientMap, state: &mut ShardServerState) get_targets(client, state, pkt.iTargetCnt as usize, None)?; let player = state.get_player(pc_id)?; - // todo: validate + // TODO: validate let hit_position = Position::new(pkt.iX, pkt.iY, pkt.iZ); let target_ids = state.entity_map.filter_ids_in_proximity( hit_position, diff --git a/src/bin/shard/main.rs b/src/bin/shard/main.rs index 6952f5fb..7239f59d 100644 --- a/src/bin/shard/main.rs +++ b/src/bin/shard/main.rs @@ -307,8 +307,8 @@ fn handle_packet( P_CL2FE_REQ_PC_AVATAR_EMOTES_CHAT => chat::pc_avatar_emotes_chat(&mut clients, state), // P_CL2FE_REQ_PC_ATTACK_NPCs => combat::pc_attack_npcs(&mut clients, state), - P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE => combat::pc_grenade_fire(&mut clients, state), - P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE => combat::pc_rocket_fire(&mut clients, state), + P_CL2FE_REQ_PC_GRENADE_STYLE_FIRE => combat::pc_fire_grenade(&mut clients, state), + P_CL2FE_REQ_PC_ROCKET_STYLE_FIRE => combat::pc_fire_rocket(&mut clients, state), P_CL2FE_REQ_PC_ROCKET_STYLE_HIT => combat::pc_projectile_hit(&mut clients, state), P_CL2FE_REQ_PC_GRENADE_STYLE_HIT => combat::pc_projectile_hit(&mut clients, state), // diff --git a/src/entity/player.rs b/src/entity/player.rs index e3b3a8c8..44e976f5 100644 --- a/src/entity/player.rs +++ b/src/entity/player.rs @@ -1243,6 +1243,17 @@ impl Player { self.nano_potions } + pub fn consume_weapon_boosts(&mut self, amount: u32) -> bool { + let weapon_boosts = self.get_weapon_boosts(); + if weapon_boosts >= amount { + self.set_weapon_boosts(weapon_boosts - amount); + true + } else { + self.set_weapon_boosts(0); + false + } + } + pub fn add_projectile(&mut self, projectile: Projectile) -> Option { if self.owned_projectiles.len() == SIZEOF_PC_BULLET_SLOT as usize { return None; diff --git a/src/enums.rs b/src/enums.rs index 388faca7..0afcf3ab 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -148,6 +148,17 @@ ffenum!(ItemType, i16, { SkillBuffEffect = 138, /*eItemType_SkillBuffEffect*/ }); +// eWeaponTargetMode.cs +ffenum!(WeaponTargetMode, i32, { + Hand = 0, + Melee = 1, + Pistol = 2, + Shotgun = 3, + Sniper = 4, + Rocket = 5, + Grenade = 6, +}); + // eTransportationType.cs ffenum!(TransportationType, i32, { /*eTT_None*/ diff --git a/src/item.rs b/src/item.rs index 6ee79c91..1e01d945 100644 --- a/src/item.rs +++ b/src/item.rs @@ -6,7 +6,7 @@ use std::{ use crate::{ defines::*, entity::RewardData, - enums::{ItemType, RewardCategory, RewardType}, + enums::{ItemType, RewardCategory, RewardType, WeaponTargetMode}, error::{panic_log, FFError, FFResult}, net::packet::*, tabledata::tdata_get, @@ -163,7 +163,7 @@ pub struct ItemStats { pub gender: Option, pub single_power: Option, pub multi_power: Option, - pub target_mode: Option, + pub target_mode: Option, pub projectile_time: Option, pub defense: Option, pub speed: Option, diff --git a/src/tabledata.rs b/src/tabledata.rs index f4b66002..86c1972e 100644 --- a/src/tabledata.rs +++ b/src/tabledata.rs @@ -994,7 +994,11 @@ fn load_item_data( gender: data.m_iReqSex.map(|v| v as i8), single_power: data.m_iPointRat, multi_power: data.m_iGroupRat, - target_mode: data.m_iTargetMode, + target_mode: if let Some(v) = data.m_iTargetMode { + Some(v.try_into().map_err(|e: FFError| e.get_msg().to_string())?) + } else { + None + }, projectile_time: data.m_iDeliverTime.map(|v| Duration::from_millis(v as u64)), defense: data.m_iDefenseRat, speed: data.m_iUp_runSpeed, From 3e3c2cc63dc6ef155b10f98afc29a52c9b200518 Mon Sep 17 00:00:00 2001 From: CakeLancelot Date: Sat, 11 Jan 2025 19:56:48 -0600 Subject: [PATCH 3/3] Fixes for multiple targets (capped at 100 currently) Removed --- src/bin/shard/combat.rs | 86 +++++++++++++++++++++++++++++++---------- src/enums.rs | 3 +- src/net/ffclient.rs | 10 ++--- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/bin/shard/combat.rs b/src/bin/shard/combat.rs index 6cd25fbb..c7e73b46 100644 --- a/src/bin/shard/combat.rs +++ b/src/bin/shard/combat.rs @@ -3,7 +3,7 @@ use std::time::SystemTime; use rusty_fusion::{ defines::EQUIP_SLOT_HAND, entity::{Combatant, Entity, EntityID, Projectile, ProjectileKind}, - enums::WeaponTargetMode, + enums::{CharType, WeaponTargetMode}, error::*, net::{ packet::{PacketID::*, *}, @@ -24,12 +24,24 @@ const BATTERY_BASE_COST: u32 = 6; struct sTargetNpcId { pub iNPC_ID: i32, } +#[allow(non_camel_case_types)] +#[allow(non_snake_case)] +#[repr(packed(4))] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct sTargetAnyId { + pub iEntity_ID: i32, + pub eCT: i32, +} + impl FFPacket for sTargetNpcId {} +impl FFPacket for sTargetAnyId {} fn get_targets( client: &mut FFClient, state: &mut ShardServerState, target_count: usize, + target_type: Option, max_targets: Option, ) -> FFResult<(Vec, u32)> { let mut target_ids = Vec::with_capacity(max_targets.unwrap_or(3)); @@ -39,28 +51,57 @@ fn get_targets( // TODO stricter anti-cheat. // validate target count, range, attack cooldown, etc against weapon stats if max_targets.is_some_and(|max| i >= max) { - return Err(FFError::build( + log( Severity::Warning, - format!( + &format!( "Tried to attack {} entities (max {})", target_count, max_targets.unwrap() ), - )); + ); + return Ok((target_ids, weapon_boosts_needed)); } - let Ok(trailer) = client.get_struct::() else { - break; - }; - let npc_id = trailer.iNPC_ID; - let npc = match state.get_npc(npc_id) { - Ok(npc) => npc, - Err(e) => { - log_error(&e); - continue; + + match target_type { + Some(CharType::Player) => todo!(), + None | Some(CharType::All) => { + let Ok(trailer) = client.get_struct::() else { + break; + }; + let entity_id = trailer.iEntity_ID; + let Ok(char_type) = CharType::try_from(trailer.eCT) else { + continue; + }; + + let entity_id = match char_type { + CharType::Unknown => continue, + CharType::Player => EntityID::Player(entity_id), + CharType::NPC | CharType::Mob => EntityID::NPC(entity_id), + CharType::All => continue, + }; + + let Ok(combatant) = state.get_combatant(entity_id) else { + continue; + }; + weapon_boosts_needed += BATTERY_BASE_COST + combatant.get_level() as u32; + target_ids.push(combatant.get_id()); + } + Some(_) => { + let Ok(trailer) = client.get_struct::() else { + break; + }; + let npc_id = trailer.iNPC_ID; + let npc = match state.get_npc(npc_id) { + Ok(npc) => npc, + Err(e) => { + log_error(&e); + continue; + } + }; + weapon_boosts_needed += BATTERY_BASE_COST + npc.get_level() as u32; + target_ids.push(npc.get_id()); } - }; - weapon_boosts_needed += BATTERY_BASE_COST + npc.get_level() as u32; - target_ids.push(npc.get_id()); + } } Ok((target_ids, weapon_boosts_needed)) @@ -77,12 +118,15 @@ pub fn pc_attack_npcs(clients: &mut ClientMap, state: &mut ShardServerState) -> return Ok(()); } - let (target_ids, weapon_boosts_needed) = - get_targets(client, state, target_count, Some(MAX_TARGETS))?; + let (target_ids, weapon_boosts_needed) = get_targets( + client, + state, + target_count, + Some(CharType::Mob), + Some(MAX_TARGETS), + )?; - // consume weapon boosts let player = state.get_player_mut(pc_id)?; - let charged = player.consume_weapon_boosts(weapon_boosts_needed); // attack handler @@ -241,7 +285,7 @@ pub fn pc_projectile_hit(clients: &mut ClientMap, state: &mut ShardServerState) }; let (target_ids, _weapon_boosts_needed) = - get_targets(client, state, pkt.iTargetCnt as usize, None)?; + get_targets(client, state, pkt.iTargetCnt as usize, None, Some(100))?; let player = state.get_player(pc_id)?; // TODO: validate diff --git a/src/enums.rs b/src/enums.rs index 0afcf3ab..516e20a9 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -80,7 +80,8 @@ ffenum!(CharType, i32, 5, { Unknown = 0, Player = 1, NPC = 2, - Mob = 3, + Mob = 4, + All = 255, }); // eTaskTypeProperty.cs diff --git a/src/net/ffclient.rs b/src/net/ffclient.rs index 6e9676e0..d8082c41 100644 --- a/src/net/ffclient.rs +++ b/src/net/ffclient.rs @@ -154,11 +154,11 @@ impl PacketBuffer { let from = self.ptr; let to = from + sz; if to > self.buf.len() { - panic_log(&format!( - "Payload too big for output ({} > {})", - sz, - self.buf.len() - )); + log( + Severity::Warning, + &format!("Payload too big for output ({} > {})", to, self.buf.len()), + ); + return; } self.buf[from..to].copy_from_slice(dat);