Compare commits

...

4 commits

Author SHA1 Message Date
af2cef394b Updated Filecfgs 2025-03-19 11:02:00 +01:00
23fcde33f5 Properties calc port for 1.7beta (#7)
Reviewed-on: #7
Co-authored-by: traffic95 <traffic95@xeondev.com>
Co-committed-by: traffic95 <traffic95@xeondev.com>
2025-03-19 08:52:04 +00:00
543727ce4e Added TeleportData support 2025-03-13 19:10:38 +01:00
7412dcec2d CNBeta 1.7.0 support 2025-03-11 18:20:56 +01:00
73 changed files with 49932 additions and 40571 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -41,7 +41,7 @@ Start each service in order from option `a)`.
Most of the configuration (database, encryption keys) is stored in a shared environment configuration file (`environment.toml`). Some of server-specific options are stored in their respective configuration files (which are created upon first startup of each server).
### Logging in
To login to this server, you have to obtain a compatible game client. Currently supported one is `OSPRODWin1.6.0`, you can get it from game launcher. Next, you have to apply the necessary [client patch](https://git.xeondev.com/ObolSquad/trigger-patch). It allows you to connect to the local server and replaces encryption keys with custom ones.
To login to this server, you have to obtain a compatible game client. Currently supported one is `CNBetaWin1.7.0`, you can get it from uploads found on [Our Discord Server](https://discord.gg/reversedrooms). Next, you have to apply the necessary [client patch](https://git.xeondev.com/ObolSquad/trigger-patch). It allows you to connect to the local server and replaces encryption keys with custom ones.
### Management
You can use the [trigger-muip-tool](https://git.xeondev.com/ObolSquad/trigger-muip-tool) to communicate with MUIP server and execute GM commands.

View file

@ -1,7 +1,10 @@
use std::collections::HashMap;
use trigger_protocol::DungeonEquipInfo;
pub struct AvatarUnit {
pub avatar_id: u32,
pub properties: HashMap<u32, i32>,
}
pub struct BuddyUnit {
@ -29,6 +32,7 @@ impl Dungeon {
.iter()
.map(|unit| AvatarUnitInfo {
avatar_id: unit.avatar_id,
properties: unit.properties.clone(),
})
.collect(),
buddy_list: self

View file

@ -1,4 +1,5 @@
mod dungeon;
mod property_util;
pub use dungeon::*;
pub mod scene;
@ -39,7 +40,14 @@ impl GameState {
quest_id: *quest_id,
avatar_list: avatar_id_list
.iter()
.map(|&avatar_id| AvatarUnit { avatar_id })
.map(|&avatar_id| AvatarUnit {
avatar_id,
properties: property_util::calculate_for_avatar(
avatar_id,
filecfg,
dungeon_equip,
),
})
.collect(),
buddy_list: vec![BuddyUnit {
buddy_type: 0,
@ -71,7 +79,14 @@ impl GameState {
quest_id: *quest_id,
avatar_list: avatar_id_list
.iter()
.map(|&avatar_id| AvatarUnit { avatar_id })
.map(|&avatar_id| AvatarUnit {
avatar_id,
properties: property_util::calculate_for_avatar(
avatar_id,
filecfg,
dungeon_equip,
),
})
.collect(),
buddy_list: vec![BuddyUnit {
buddy_type: 0,

View file

@ -0,0 +1,717 @@
use std::collections::HashMap;
use trigger_fileconfig::{
AvatarBattleTemplate, AvatarLevelAdvanceTemplate, AvatarPassiveSkillTemplate, NapFileCfg,
WeaponLevelTemplate, WeaponStarTemplate, WeaponTemplate,
};
use trigger_logic::{battle::EPropertyType, skill::EAvatarSkillType};
use trigger_protocol::{Avatar, DungeonEquipInfo, Equip, Weapon};
struct AvatarFileCfg<'a> {
pub avatar_battle_template: AvatarBattleTemplate<'a>,
pub avatar_passive_skill_templates: Vec<AvatarPassiveSkillTemplate<'a>>,
pub avatar_level_advances: Vec<AvatarLevelAdvanceTemplate<'a>>,
}
struct WeaponFileCfg<'a> {
pub weapon_template: WeaponTemplate<'a>,
pub weapon_level_template: WeaponLevelTemplate<'a>,
pub weapon_star_template: WeaponStarTemplate<'a>,
}
const BEN_AVATAR_ID: u32 = 1121;
const BEN_CORE_PASSIVE_PERCENTAGE: [i32; 7] = [40, 46, 52, 60, 66, 72, 80];
pub fn calculate_for_avatar(
avatar_id: u32,
filecfg: &'static NapFileCfg,
dungeon_equip: &DungeonEquipInfo,
) -> HashMap<u32, i32> {
let mut calculated_properties = HashMap::<_, _>::new();
let Some(player_avatar) = dungeon_equip
.avatar_list
.iter()
.find(|a| (*a).id == avatar_id)
else {
return calculated_properties;
};
let Some(avatar_file_cfg) = get_avatar_filecfg(avatar_id, filecfg) else {
return calculated_properties;
};
insert_avatar_battle_template_properties(&avatar_file_cfg, &mut calculated_properties);
insert_avatar_battle_level_advance_properties(
&avatar_file_cfg,
player_avatar,
&mut calculated_properties,
);
perform_avatar_growth_promotion_calculations(player_avatar, &mut calculated_properties);
add_avatar_passive_skill_properties(
&avatar_file_cfg,
player_avatar,
&mut calculated_properties,
);
let player_weapon = dungeon_equip
.weapon_list
.iter()
.find(|w| w.uid == player_avatar.cur_weapon_uid);
if player_weapon.is_some() {
match get_weapon_filecfg(player_weapon.unwrap(), filecfg) {
Some(weapon_file_cfg) => {
add_weapon_template_properties(&weapon_file_cfg, &mut calculated_properties);
}
None => {}
};
}
let player_dressed_equip_uids: Vec<u32> = player_avatar
.dressed_equip_list
.iter()
.map(|de| de.equip_uid)
.collect();
let player_equips: Vec<&Equip> = dungeon_equip
.equip_list
.iter()
.filter(|e| player_dressed_equip_uids.contains(&e.uid))
.collect();
if player_equips.len() > 0 {
add_player_equip_properties(&player_equips, filecfg, &mut calculated_properties);
add_equipment_suit_properties(&player_equips, filecfg, &mut calculated_properties);
}
calculate_final_properties(&mut calculated_properties);
add_ben_core_passive(avatar_id, player_avatar, &mut calculated_properties);
remove_custom_properties(&mut calculated_properties);
set_battle_properties(&mut calculated_properties);
calculated_properties
}
fn get_avatar_filecfg(
avatar_id: u32,
filecfg: &'static NapFileCfg,
) -> Option<AvatarFileCfg<'static>> {
let Some(avatar_battle_template) = filecfg
.avatar_battle_template_tb()
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == avatar_id as i32)
else {
return None;
};
let avatar_passive_skill_templates: Vec<AvatarPassiveSkillTemplate> = filecfg
.avatar_passive_skill_template_tb()
.data()
.unwrap()
.iter()
.filter(|tmpl| tmpl.avatar_id() == avatar_id as i32)
.collect();
if avatar_passive_skill_templates.len() == 0 {
return None;
}
let avatar_level_advances: Vec<AvatarLevelAdvanceTemplate> = filecfg
.avatar_level_advance_template_tb()
.data()
.unwrap()
.iter()
.filter(|tmpl| tmpl.avatar_id() == avatar_id as i32)
.collect();
if avatar_level_advances.len() == 0 {
return None;
}
Some(AvatarFileCfg {
avatar_battle_template,
avatar_passive_skill_templates,
avatar_level_advances,
})
}
fn insert_avatar_battle_template_properties(
avatar_file_cfg: &AvatarFileCfg,
properties: &mut HashMap<u32, i32>,
) {
properties.insert(
EPropertyType::HpMaxBase.into(),
avatar_file_cfg.avatar_battle_template.hp_max(),
);
properties.insert(
EPropertyType::HpMaxGrowth.into(),
avatar_file_cfg.avatar_battle_template.health_growth(),
);
properties.insert(
EPropertyType::AtkBase.into(),
avatar_file_cfg.avatar_battle_template.attack(),
);
properties.insert(
EPropertyType::AtkGrowth.into(),
avatar_file_cfg.avatar_battle_template.attack_growth(),
);
properties.insert(
EPropertyType::BreakStunBase.into(),
avatar_file_cfg.avatar_battle_template.break_stun(),
);
properties.insert(
EPropertyType::DefBase.into(),
avatar_file_cfg.avatar_battle_template.defence(),
);
properties.insert(
EPropertyType::DefGrowth.into(),
avatar_file_cfg.avatar_battle_template.defence_growth(),
);
properties.insert(
EPropertyType::CritBase.into(),
avatar_file_cfg.avatar_battle_template.crit(),
);
properties.insert(
EPropertyType::CritDmgBase.into(),
avatar_file_cfg.avatar_battle_template.crit_damage(),
);
properties.insert(
EPropertyType::PenBase.into(),
avatar_file_cfg.avatar_battle_template.pen_rate(),
);
properties.insert(
EPropertyType::PenValueBase.into(),
avatar_file_cfg.avatar_battle_template.pen_delta(),
);
properties.insert(
EPropertyType::SpRecoverBase.into(),
avatar_file_cfg.avatar_battle_template.sp_recover(),
);
properties.insert(
EPropertyType::ElementMysteryBase.into(),
avatar_file_cfg.avatar_battle_template.element_mystery(),
);
properties.insert(
EPropertyType::ElementAbnormalPowerBase.into(),
avatar_file_cfg
.avatar_battle_template
.element_abnormal_power(),
);
}
fn insert_avatar_battle_level_advance_properties(
avatar_file_cfg: &AvatarFileCfg,
player_avatar: &Avatar,
properties: &mut HashMap<u32, i32>,
) {
let Some(avatar_level_advance) = avatar_file_cfg
.avatar_level_advances
.iter()
.find(|tmpl| tmpl.id() == player_avatar.rank as i32)
else {
properties.insert(EPropertyType::HpMaxAdvance.into(), 0);
properties.insert(EPropertyType::AtkAdvance.into(), 0);
properties.insert(EPropertyType::DefAdvance.into(), 0);
return;
};
properties.insert(
EPropertyType::HpMaxAdvance.into(),
avatar_level_advance.hp_max(),
);
properties.insert(
EPropertyType::AtkAdvance.into(),
avatar_level_advance.attack(),
);
properties.insert(
EPropertyType::DefAdvance.into(),
avatar_level_advance.defence(),
);
}
fn perform_avatar_growth_promotion_calculations(
player_avatar: &Avatar,
properties: &mut HashMap<u32, i32>,
) {
properties.insert(
EPropertyType::HpMaxBase.into(),
properties.get(&EPropertyType::HpMaxBase.into()).unwrap()
+ (((player_avatar.level - 1) as f32
* *properties.get(&EPropertyType::HpMaxGrowth.into()).unwrap() as f32)
/ 10000f32) as i32
+ properties.get(&EPropertyType::HpMaxAdvance.into()).unwrap(),
);
properties.insert(
EPropertyType::AtkBase.into(),
properties.get(&EPropertyType::AtkBase.into()).unwrap()
+ (((player_avatar.level - 1) as f32
* *properties.get(&EPropertyType::AtkGrowth.into()).unwrap() as f32)
/ 10000f32) as i32
+ properties.get(&EPropertyType::AtkAdvance.into()).unwrap(),
);
properties.insert(
EPropertyType::DefBase.into(),
properties.get(&EPropertyType::DefBase.into()).unwrap()
+ (((player_avatar.level - 1) as f32
* *properties.get(&EPropertyType::DefGrowth.into()).unwrap() as f32)
/ 10000f32) as i32
+ properties.get(&EPropertyType::DefAdvance.into()).unwrap(),
);
}
fn add_avatar_passive_skill_properties(
avatar_file_cfg: &AvatarFileCfg,
player_avatar: &Avatar,
properties: &mut HashMap<u32, i32>,
) {
let passive_skill_level = player_avatar
.skill_type_level
.iter()
.find(|s| s.skill_type == EAvatarSkillType::CoreSkill as u32)
.unwrap()
.level;
avatar_file_cfg
.avatar_passive_skill_templates
.iter()
.find(|a| a.unlock_passive_skill_level() == passive_skill_level as i32)
.map(|tmpl| tmpl.propertys().unwrap())
.unwrap()
.iter()
.for_each(|p| {
properties.insert(
p.property() as u32,
properties.get(&(p.property() as u32)).unwrap_or(&0) + p.value(),
);
});
}
fn get_weapon_filecfg(
player_weapon: &Weapon,
filecfg: &'static NapFileCfg,
) -> Option<WeaponFileCfg<'static>> {
let Some(weapon_template) = filecfg
.weapon_template_tb()
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.item_id() == player_weapon.id as i32)
else {
return None;
};
let Some(weapon_level_template) = filecfg
.weapon_level_template_tb()
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.level() == player_weapon.level as i32)
else {
return None;
};
let Some(weapon_star_template) = filecfg
.weapon_star_template_tb()
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.star() == player_weapon.star as i32)
else {
return None;
};
Some(WeaponFileCfg {
weapon_template,
weapon_level_template,
weapon_star_template,
})
}
fn add_weapon_template_properties(
weapon_file_cfg: &WeaponFileCfg,
properties: &mut HashMap<u32, i32>,
) {
let base_property = weapon_file_cfg.weapon_template.base_property().unwrap();
let base_property_value = base_property.value()
+ (base_property.value() as f32 * weapon_file_cfg.weapon_level_template.rate() as f32
/ 10000f32) as i32
+ (base_property.value() as f32 * weapon_file_cfg.weapon_star_template.star_rate() as f32
/ 10000f32) as i32;
properties.insert(
base_property.property() as u32,
*properties
.get(&(base_property.property() as u32))
.unwrap_or(&0)
+ base_property_value,
);
let rand_property = weapon_file_cfg.weapon_template.rand_property().unwrap();
let rand_property_value = rand_property.value()
+ (rand_property.value() as f32 * weapon_file_cfg.weapon_star_template.rand_rate() as f32
/ 10000f32) as i32;
properties.insert(
rand_property.property() as u32,
*properties
.get(&(rand_property.property() as u32))
.unwrap_or(&0)
+ rand_property_value,
);
}
fn add_player_equip_properties(
player_equips: &Vec<&Equip>,
filecfg: &'static NapFileCfg,
properties: &mut HashMap<u32, i32>,
) {
player_equips.iter().for_each(|pe| {
let equip_rarity = (pe.id / 10) % 10;
let property_rate = filecfg
.equipment_level_template_tb()
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.rarity() == equip_rarity as i32 && tmpl.level() == pe.level as i32)
.map(|tmpl| tmpl.property_rate())
.unwrap_or(1);
pe.propertys.iter().for_each(|p| {
properties.insert(
p.key,
properties.get(&p.key).unwrap_or(&0)
+ p.base_value as i32
+ (p.base_value as f32 * property_rate as f32 / 10000f32) as i32,
);
});
pe.sub_propertys.iter().for_each(|p| {
properties.insert(
p.key,
properties.get(&p.key).unwrap_or(&0)
+ (p.base_value as f32 * p.add_value as f32) as i32,
);
});
});
}
fn add_equipment_suit_properties(
player_equips: &Vec<&Equip>,
filecfg: &'static NapFileCfg,
properties: &mut HashMap<u32, i32>,
) {
let mut suit_counts = HashMap::<i32, i32>::new();
player_equips.iter().for_each(|pe| {
let suit_id = pe.id as i32 / 100 * 100;
suit_counts.insert(suit_id, *suit_counts.get(&suit_id).unwrap_or(&0) + 1);
});
suit_counts.iter().for_each(|sc| {
let Some(equipment_suit_template) = filecfg
.equipment_suit_template_tb()
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == *sc.0)
else {
return;
};
if equipment_suit_template.primary_condition() <= *sc.1 {
equipment_suit_template
.primary_suit_propertys()
.unwrap()
.iter()
.for_each(|p| {
properties.insert(
p.property() as u32,
properties.get(&(p.property() as u32)).unwrap_or(&0) + p.value(),
);
});
}
});
}
fn calculate_final_properties(properties: &mut HashMap<u32, i32>) {
properties.insert(
EPropertyType::HpMax.into(),
properties.get(&EPropertyType::HpMaxBase.into()).unwrap()
+ (*properties.get(&EPropertyType::HpMaxBase.into()).unwrap() as f32
* *properties
.get(&EPropertyType::HpMaxRatio.into())
.unwrap_or(&0) as f32
/ 10000f32)
.ceil() as i32
+ properties
.get(&EPropertyType::HpMaxDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::Atk.into(),
properties.get(&EPropertyType::AtkBase.into()).unwrap()
+ (*properties.get(&EPropertyType::AtkBase.into()).unwrap() as f32
* *properties
.get(&EPropertyType::AtkRatio.into())
.unwrap_or(&0) as f32
/ 10000f32) as i32
+ properties
.get(&EPropertyType::AtkDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::BreakStun.into(),
properties
.get(&EPropertyType::BreakStunBase.into())
.unwrap()
+ (*properties
.get(&EPropertyType::BreakStunBase.into())
.unwrap() as f32
* *properties
.get(&EPropertyType::BreakStunRatio.into())
.unwrap_or(&0) as f32
/ 10000f32) as i32
+ properties
.get(&EPropertyType::BreakStunDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::Def.into(),
properties.get(&EPropertyType::DefBase.into()).unwrap()
+ (*properties.get(&EPropertyType::DefBase.into()).unwrap() as f32
* *properties
.get(&EPropertyType::DefRatio.into())
.unwrap_or(&0) as f32
/ 10000f32) as i32
+ properties
.get(&EPropertyType::DefDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::Crit.into(),
properties.get(&EPropertyType::CritBase.into()).unwrap()
+ properties
.get(&EPropertyType::CritDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::CritDmg.into(),
properties.get(&EPropertyType::CritDmgBase.into()).unwrap()
+ properties
.get(&EPropertyType::CritDmgDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::Pen.into(),
properties.get(&EPropertyType::PenBase.into()).unwrap()
+ properties
.get(&EPropertyType::PenDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::PenValue.into(),
properties.get(&EPropertyType::PenValueBase.into()).unwrap()
+ properties
.get(&EPropertyType::PenValueDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::SpRecover.into(),
properties
.get(&EPropertyType::SpRecoverBase.into())
.unwrap()
+ (*properties
.get(&EPropertyType::SpRecoverBase.into())
.unwrap() as f32
* *properties
.get(&EPropertyType::SpRecoverRatio.into())
.unwrap_or(&0) as f32
/ 10000f32) as i32
+ properties
.get(&EPropertyType::SpRecoverDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::ElementMystery.into(),
properties
.get(&EPropertyType::ElementMysteryBase.into())
.unwrap()
+ properties
.get(&EPropertyType::ElementMysteryDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::ElementAbnormalPower.into(),
properties
.get(&EPropertyType::ElementAbnormalPowerBase.into())
.unwrap()
+ (*properties
.get(&EPropertyType::ElementAbnormalPowerBase.into())
.unwrap() as f32
* *properties
.get(&EPropertyType::ElementAbnormalPowerRatio.into())
.unwrap_or(&0) as f32
/ 10000f32) as i32
+ properties
.get(&EPropertyType::ElementAbnormalPowerDelta.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioPhysics.into(),
properties
.get(&EPropertyType::AddedDamageRatioPhysics1.into())
.unwrap_or(&0)
+ properties
.get(&EPropertyType::AddedDamageRatioPhysics3.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioFire.into(),
properties
.get(&EPropertyType::AddedDamageRatioFire1.into())
.unwrap_or(&0)
+ properties
.get(&EPropertyType::AddedDamageRatioFire3.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioIce.into(),
properties
.get(&EPropertyType::AddedDamageRatioIce1.into())
.unwrap_or(&0)
+ properties
.get(&EPropertyType::AddedDamageRatioIce3.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioElec.into(),
properties
.get(&EPropertyType::AddedDamageRatioElec1.into())
.unwrap_or(&0)
+ properties
.get(&EPropertyType::AddedDamageRatioElec3.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioEther.into(),
properties
.get(&EPropertyType::AddedDamageRatioEther1.into())
.unwrap_or(&0)
+ properties
.get(&EPropertyType::AddedDamageRatioEther3.into())
.unwrap_or(&0),
);
}
fn add_ben_core_passive(
avatar_id: u32,
player_avatar: &Avatar,
properties: &mut HashMap<u32, i32>,
) {
if avatar_id != BEN_AVATAR_ID {
return;
}
let core_level = player_avatar
.skill_type_level
.iter()
.find(|sl| sl.skill_type == EAvatarSkillType::CoreSkill as u32)
.unwrap()
.level as usize;
let def_atk_bonus = match core_level {
1..=7 => {
*properties.get(&EPropertyType::Def.into()).unwrap_or(&0)
* BEN_CORE_PASSIVE_PERCENTAGE[core_level - 1]
/ 100
}
_ => 0,
};
properties.insert(
EPropertyType::Atk.into(),
*properties.get(&EPropertyType::Atk.into()).unwrap_or(&0) + def_atk_bonus,
);
}
fn remove_custom_properties(properties: &mut HashMap<u32, i32>) {
properties.remove(&EPropertyType::HpMaxGrowth.into());
properties.remove(&EPropertyType::AtkGrowth.into());
properties.remove(&EPropertyType::DefGrowth.into());
properties.remove(&EPropertyType::HpMaxAdvance.into());
properties.remove(&EPropertyType::AtkAdvance.into());
properties.remove(&EPropertyType::DefAdvance.into());
}
fn set_battle_properties(properties: &mut HashMap<u32, i32>) {
properties.insert(
EPropertyType::Hp.into(),
*properties.get(&EPropertyType::HpMax.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::HpMaxBattle.into(),
*properties.get(&EPropertyType::HpMax.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::AtkBattle.into(),
*properties.get(&EPropertyType::Atk.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::BreakStunBattle.into(),
*properties
.get(&EPropertyType::BreakStun.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::DefBattle.into(),
*properties.get(&EPropertyType::Def.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::CritBattle.into(),
*properties.get(&EPropertyType::Crit.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::CritDmgBattle.into(),
*properties.get(&EPropertyType::CritDmg.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::PenRatioBattle.into(),
*properties.get(&EPropertyType::Pen.into()).unwrap_or(&0),
);
properties.insert(
EPropertyType::PenDeltaBattle.into(),
*properties
.get(&EPropertyType::PenValue.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::SpRecoverBattle.into(),
*properties
.get(&EPropertyType::SpRecover.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::ElementMysteryBattle.into(),
*properties
.get(&EPropertyType::ElementMystery.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::ElementAbnormalPowerBattle.into(),
*properties
.get(&EPropertyType::ElementAbnormalPower.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioPhysicsBattle.into(),
*properties
.get(&EPropertyType::AddedDamageRatioPhysics.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioFireBattle.into(),
*properties
.get(&EPropertyType::AddedDamageRatioFire.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioIceBattle.into(),
*properties
.get(&EPropertyType::AddedDamageRatioIce.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioElecBattle.into(),
*properties
.get(&EPropertyType::AddedDamageRatioElec.into())
.unwrap_or(&0),
);
properties.insert(
EPropertyType::AddedDamageRatioEtherBattle.into(),
*properties
.get(&EPropertyType::AddedDamageRatioEther.into())
.unwrap_or(&0),
);
}

View file

@ -10,7 +10,7 @@ use trigger_sv::{
net::ServerType,
};
use crate::{logic::GameState, session::BattleSession, AppState};
use crate::{AppState, logic::GameState, session::BattleSession};
pub async fn handle_message(state: &'static AppState, packet: trigger_sv::message::NetworkPacket) {
match packet.opcode {

View file

@ -1,5 +1,5 @@
use trigger_encoding::Decodeable;
use trigger_protocol::{util::ProtocolUnit, ClientCmdID, EndBattleCsReq};
use trigger_protocol::{ClientCmdID, EndBattleCsReq, util::ProtocolUnit};
use trigger_sv::message::GameStateCallback;
use super::BattleSession;

View file

@ -13,7 +13,7 @@ area = 2
[bound_server]
name = "trigger_rs"
title = "Trigger-RS"
seed = "70fed6d1bdf76412"
seed = "789bd844f8c04bc3"
addr = "127.0.0.1"
port = 20501
is_kcp = false

View file

@ -3,7 +3,7 @@ use std::{
sync::{LazyLock, OnceLock},
};
use axum::{routing::get, Router};
use axum::{Router, routing::get};
use config::DispatchConfig;
use tokio::net::TcpListener;
use tracing::error;

View file

@ -1,8 +1,8 @@
use axum::{extract::State, Json};
use axum::{Json, extract::State};
use crate::{
data::{QueryDispatchRsp, ServerListInfo},
AppState,
data::{QueryDispatchRsp, ServerListInfo},
};
pub const ROUTE_ENDPOINT: &str = "/query_dispatch";

View file

@ -1,20 +1,20 @@
use axum::{
Json,
extract::{Query, State},
response::IntoResponse,
Json,
};
use base64::{display::Base64Display, engine::general_purpose::STANDARD, Engine};
use base64::{Engine, display::Base64Display, engine::general_purpose::STANDARD};
use serde::{Deserialize, Serialize, Serializer};
use tracing::debug;
use trigger_cryptography::rsa;
use trigger_sv::config::RsaSetting;
use crate::{
AppState,
data::{
CdnConfExt, CdnDesignData, CdnGameRes, CdnSilenceData, RegionExtension, RegionSwitchFunc,
ServerDispatchData, ServerGateway,
},
AppState,
};
pub const ROUTE_ENDPOINT: &str = "/query_gateway";
@ -127,7 +127,10 @@ fn internal_process(
msg: String::with_capacity(0),
region_name: Borrowed(&server.name),
title: Borrowed(&server.title),
client_secret_key: Owned(base64::engine::general_purpose::STANDARD.encode(&state.environment.security.static_key.seed_buf)),
client_secret_key: Owned(
base64::engine::general_purpose::STANDARD
.encode(&state.environment.security.static_key.seed_buf),
),
cdn_check_url: String::with_capacity(0),
gateway: Some(ServerGateway {
ip: Borrowed(&server.addr),
@ -139,21 +142,33 @@ fn internal_process(
cdn_conf_ext: Some(CdnConfExt {
// TODO: unhardcode this
design_data: CdnDesignData {
base_url: Borrowed("https://autopatchos.zenlesszonezero.com/design_data/1.6_live/output_6898738_6ffe812558/client/"),
data_revision: Borrowed("6898738"),
md5_files: Borrowed(r#"[{"fileName": "data_version", "fileSize": 2065, "fileMD5": "16970197844668905690"}]"#),
base_url: Borrowed(
"https://autopatchcn.juequling.com/design_data/beta_live/output_7054632_323d17319c/client/",
),
data_revision: Borrowed("7054632"),
md5_files: Borrowed(
r#"[{"fileName": "data_version", "fileSize": 4503, "fileMD5": "419987357302147246"}]"#,
),
},
game_res: CdnGameRes {
audio_revision: Borrowed("6898738"),
base_url: Borrowed("https://autopatchos.zenlesszonezero.com/game_res/1.6_live/output_6898738_6ffe812558/client/"),
audio_revision: Borrowed("7025371"),
base_url: Borrowed(
"https://autopatchcn.juequling.com/game_res/beta_live/output_7054632_323d17319c/client/",
),
branch: Borrowed("beta_live"),
md5_files: Borrowed(r#"[{"fileName": "res_version", "fileSize": 1167660, "fileMD5": "8072678507435758384"}, {"fileName": "audio_version", "fileSize": 15447, "fileMD5": "5401804085122358755"}, {"fileName": "base_revision", "fileSize": 4, "fileMD5": "4524394692449115962"}]"#),
res_revision: Borrowed("6898738"),
md5_files: Borrowed(
r#"[{"fileName": "res_version", "fileSize": 2379030, "fileMD5": "15840336186912297231"}, {"fileName": "audio_version", "fileSize": 30435, "fileMD5": "15675397132378459243"}, {"fileName": "base_revision", "fileSize": 18, "fileMD5": "18079377284431001248"}]"#,
),
res_revision: Borrowed("7054632"),
},
silence_data: CdnSilenceData {
base_url: Borrowed("https://autopatchos.zenlesszonezero.com/design_data/1.6_live/output_6898738_6ffe812558/client_silence/"),
md5_files: Borrowed(r#"[{"fileName": "silence_version", "fileSize": 130, "fileMD5": "2077712550601860122"}]"#),
silence_revision: Borrowed("6898738"),
base_url: Borrowed(
"https://autopatchcn.juequling.com/design_data/beta_live/output_7054632_323d17319c/client_silence/",
),
md5_files: Borrowed(
r#"[{"fileName": "silence_version", "fileSize": 647, "fileMD5": "15019531890587528788"}]"#,
),
silence_revision: Borrowed("7042559"),
},
pre_download: None,
}),
@ -174,7 +189,7 @@ fn internal_process(
url_check_nap: String::new(),
url_check_sdk: String::new(),
}),
}
},
})
}

View file

@ -1,11 +1,11 @@
use tracing::{debug, warn};
use trigger_logic::quest::EQuestType;
use trigger_protocol::{util::ProtocolUnit, AvatarSync, CafeSync, ItemSync, PlayerSyncScNotify};
use trigger_protocol::{AvatarSync, CafeSync, ItemSync, PlayerSyncScNotify, util::ProtocolUnit};
use trigger_sv::gm_command::GMCommand;
use crate::AppState;
use super::{player::AvatarPropertyChanges, NapPlayer};
use super::{NapPlayer, player::AvatarPropertyChanges};
pub struct CommandContext<'player> {
pub player: &'player mut NapPlayer,

View file

@ -6,7 +6,7 @@ use quest::QuestModel;
use ramen::RamenModel;
use role::RoleModel;
use scene::SceneModel;
use trigger_database::{entity::*, prelude::*, DatabaseConnection};
use trigger_database::{DatabaseConnection, entity::*, prelude::*};
use trigger_fileconfig::NapFileCfg;
use trigger_logic::scene::ESceneType;
use trigger_protocol::PlayerBasicInfo;

View file

@ -1,6 +1,6 @@
use trigger_database::DatabaseConnection;
use trigger_database::entity::*;
use trigger_database::prelude::*;
use trigger_database::DatabaseConnection;
pub async fn load_player_basic_info(
db: &DatabaseConnection,

View file

@ -23,8 +23,6 @@ pub struct AvatarPropertyChanges {
}
impl RoleModel {
const PIDORS: &[i32] = &[1331, 1291];
pub async fn init(context: NapContext) -> Self {
let avatar_map = Self::load_or_create_avatar_map(&context).await;
@ -332,7 +330,6 @@ impl RoleModel {
pub async fn unlock_avatars(&mut self, avatar_id_list: &[i32]) -> Vec<u32> {
let models = avatar_id_list
.iter()
.filter(|id| !Self::PIDORS.contains(*id))
.filter(|id| !self.is_avatar_unlocked(**id as u32))
.map(|id| {
self.context

View file

@ -12,7 +12,7 @@ use tracing::{error, info};
use trigger_database::DatabaseConnection;
use trigger_fileconfig::NapFileCfg;
use trigger_sv::{
config::{load_json_config, ServerEnvironmentConfiguration, TomlConfig},
config::{ServerEnvironmentConfiguration, TomlConfig, load_json_config},
die, logging,
net::{ServerNetworkManager, ServerType},
print_banner,

View file

@ -1,9 +1,9 @@
use std::sync::Arc;
use crate::{
logic::{gm_util, NapPlayer},
session::GameSession,
AppState,
logic::{NapPlayer, gm_util},
session::GameSession,
};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};

View file

@ -179,6 +179,20 @@ mod client_systems_module {
}
}
pub async fn on_end_newbie(
_context: &mut MessageContext<'_, '_>,
_request: EndNewbieCsReq,
) -> EndNewbieScRsp {
EndNewbieScRsp { retcode: 0 }
}
pub async fn on_sync_global_variables(
_context: &mut MessageContext<'_, '_>,
_request: SyncGlobalVariablesCsReq,
) -> SyncGlobalVariablesScRsp {
SyncGlobalVariablesScRsp { retcode: 0 }
}
pub async fn on_get_trashbin_hermit_data(
_context: &mut MessageContext<'_, '_>,
_request: GetTrashbinHermitDataCsReq,

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod flower_shop_module {
pub async fn on_get_flower_shop_data(
_context: &mut MessageContext<'_, '_>,
_request: GetFlowerShopDataCsReq,
) -> GetFlowerShopDataScRsp {
GetFlowerShopDataScRsp { retcode: 0 }
}
}

View file

@ -1,5 +1,5 @@
use trigger_encoding::Encodeable;
use trigger_protocol::{util::ProtocolUnit, ClientCmdID};
use trigger_protocol::{ClientCmdID, util::ProtocolUnit};
use super::GameSession;
use crate::AppState;
@ -40,7 +40,8 @@ modules! {
miniscape_entrust,
fishing_contest,
ridus_got_boo,
qa_game
qa_game,
flower_shop
}
client_message_forwarding! {

View file

@ -15,7 +15,7 @@ use trigger_sv::{
net::{ServerNetworkManager, ServerType},
};
use crate::logic::{scene_util, NapPlayer};
use crate::logic::{NapPlayer, scene_util};
pub mod message;

View file

@ -12,10 +12,10 @@ use trigger_sv::{
};
use crate::{
AppState,
net::{Connection, NetPacket},
session::SessionState,
util::BinExt,
AppState,
};
pub async fn handle_message(connection: &Connection, state: &'static AppState, packet: NetPacket) {
@ -41,20 +41,33 @@ pub async fn handle_message(connection: &Connection, state: &'static AppState, p
on_keep_alive(
connection,
state,
KeepAliveNotify::decode(&*packet.body).unwrap_or_default()
).await
KeepAliveNotify::decode(&*packet.body).unwrap_or_default(),
)
.await
}
cmd_id if connection.session.is_logged_in() => {
match trigger_protobuf::pb_to_common_protocol_unit(cmd_id, &packet.body) {
Ok(Some(unit)) => state.network_mgr.send_to(ServerType::GameServer, 0, ForwardClientProtocolMessage {
session_id: connection.session.id,
request_id: head.packet_id,
message: unit,
}).await,
Ok(Some(unit)) => {
state
.network_mgr
.send_to(
ServerType::GameServer,
0,
ForwardClientProtocolMessage {
session_id: connection.session.id,
request_id: head.packet_id,
message: unit,
},
)
.await
}
Ok(None) => warn!("ignoring message with unknown cmd_id: {cmd_id}"),
Err(err) => error!(
"failed to decode a message with cmd_id: {} from {} (player_uid: {}), error: {}",
cmd_id, connection.addr(), connection.session.player_uid(), err
cmd_id,
connection.addr(),
connection.session.player_uid(),
err
),
}
}

View file

@ -1,7 +1,7 @@
use tokio::sync::mpsc;
use tracing::warn;
use crate::{net::NetPacket, AppState};
use crate::{AppState, net::NetPacket};
#[derive(Clone)]
pub struct MessageHandler(mpsc::UnboundedSender<(u64, NetPacket)>);

View file

@ -2,8 +2,8 @@ use std::{
io,
net::SocketAddr,
sync::{
atomic::{AtomicU32, Ordering::SeqCst},
Arc, OnceLock,
atomic::{AtomicU32, Ordering::SeqCst},
},
time::Duration,
};

View file

@ -1,6 +1,6 @@
use std::io::{Cursor, Write};
use byteorder::{WriteBytesExt, BE};
use byteorder::{BE, WriteBytesExt};
use trigger_protobuf::PacketHead;
pub struct NetPacket {

View file

@ -3,7 +3,7 @@ use std::{io, net::SocketAddr};
use tokio::net::TcpListener;
use tracing::info;
use crate::{message_handler::MessageHandler, AppState};
use crate::{AppState, message_handler::MessageHandler};
pub async fn serve(
addr: SocketAddr,

View file

@ -1,6 +1,6 @@
use std::sync::{
atomic::{AtomicI64, Ordering::SeqCst},
OnceLock,
atomic::{AtomicI64, Ordering::SeqCst},
};
use atomic_enum::atomic_enum;

View file

@ -107,6 +107,6 @@ use trigger_protocol::{
};
use trigger_sv::message::GameStateCallback;
use crate::logic::{message::RunEventGraphEvent, GameStateListener};
use crate::logic::{GameStateListener, message::RunEventGraphEvent};
use super::scene_unit::{InteractContainer, SceneUnitTag};

View file

@ -1,6 +1,6 @@
use bevy_ecs::{prelude::*, query::QueryData};
use tracing::warn;
use trigger_fileconfig::{main_city_script::MainCityConfig, NapFileCfg};
use trigger_fileconfig::{NapFileCfg, main_city_script::MainCityConfig};
use crate::logic::save::HallSceneSaveData;

View file

@ -3,13 +3,13 @@ use tracing::{debug, warn};
use trigger_protocol::InteractWithUnitScRsp;
use trigger_sv::message::GameStateCallback;
use crate::logic::{message::InteractWithUnitEvent, GameStateListener};
use crate::logic::{GameStateListener, message::InteractWithUnitEvent};
use super::{
NapResources,
event_graph::{ActionChangeInteractCfgEvent, EventGraph, GraphEvent},
hall::MainCitySection,
scene_unit::{InteractContainer, SceneUnitTag},
NapResources,
};
pub fn tick_change_interact(

View file

@ -8,11 +8,11 @@ use scene::PlayerEnterScene;
use trigger_fileconfig::main_city_script::MainCityConfig;
use super::{
GameStateListener,
message::{
EnterSectionEvent, InteractWithUnitEvent, PlayerMoveEvent, RunEventGraphEvent,
SwitchRoleEvent,
},
GameStateListener,
};
pub mod event_graph;

View file

@ -1,8 +1,8 @@
use bevy_ecs::prelude::*;
use crate::logic::{
save::{HallSceneSaveData, MainCityPositionSave},
GameStateListener,
save::{HallSceneSaveData, MainCityPositionSave},
};
use super::{

View file

@ -1,6 +1,6 @@
use bevy_ecs::system::Resource;
use trigger_encoding::Encodeable;
use trigger_protocol::{util::ProtocolUnit, ClientCmdID};
use trigger_protocol::{ClientCmdID, util::ProtocolUnit};
use trigger_sv::{
message::{GameStateCallback, GameStateCallbackMessage},
net::{ServerNetworkManager, ServerType},

View file

@ -2,8 +2,8 @@ use bevy_ecs::event::Event;
use trigger_encoding::Decodeable;
use trigger_logic::scene::Transform;
use trigger_protocol::{
util::ProtocolUnit, ClientCmdID, EnterSectionCsReq, InteractWithUnitCsReq, RunEventGraphCsReq,
SavePosInMainCityCsReq, SwitchRoleCsReq,
ClientCmdID, EnterSectionCsReq, InteractWithUnitCsReq, RunEventGraphCsReq,
SavePosInMainCityCsReq, SwitchRoleCsReq, util::ProtocolUnit,
};
use super::ecs::NapEcs;

View file

@ -1,6 +1,6 @@
use std::{collections::HashMap, sync::mpsc, thread};
use ecs::{scene::PlayerEnterScene, NapEcs};
use ecs::{NapEcs, scene::PlayerEnterScene};
use message::ProtocolEventHandler;
use tracing::debug;
use trigger_protocol::util::ProtocolUnit;
@ -11,8 +11,8 @@ mod listener;
mod message;
pub mod save;
pub use ecs::hall::HallInitData;
pub use ecs::NapResources;
pub use ecs::hall::HallInitData;
pub use listener::GameStateListener;
#[derive(Clone)]

View file

@ -8,9 +8,9 @@ use dashmap::DashMap;
use logic::{GameRunner, NapResources};
use session::HallSession;
use tracing::{error, info};
use trigger_fileconfig::{main_city_script::MainCityConfig, NapFileCfg};
use trigger_fileconfig::{NapFileCfg, main_city_script::MainCityConfig};
use trigger_sv::{
config::{load_json_config, ServerEnvironmentConfiguration, TomlConfig},
config::{ServerEnvironmentConfiguration, TomlConfig, load_json_config},
die, logging,
net::{ServerNetworkManager, ServerType},
print_banner,

View file

@ -5,9 +5,9 @@ use trigger_sv::message::{
};
use crate::{
AppState,
logic::{GameStateListener, HallInitData},
session::HallSession,
AppState,
};
pub async fn handle_message(state: &'static AppState, packet: trigger_sv::message::NetworkPacket) {

View file

@ -1,9 +1,9 @@
use std::borrow::Cow;
use axum::{
Json, Router,
extract::{Query, State},
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
@ -64,7 +64,7 @@ async fn gm_api(
return Json(Response {
retcode: 2,
message: Some(Cow::Owned(format!("invalid command format: {err}"))),
})
});
}
};

View file

@ -1,8 +1,8 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, Data, DeriveInput, Field, Fields, GenericArgument, PathArguments, Type,
TypePath,
Data, DeriveInput, Field, Fields, GenericArgument, PathArguments, Type, TypePath,
parse_macro_input,
};
pub fn impl_gm_input(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -114,7 +114,7 @@ pub fn impl_gm_input(input: proc_macro::TokenStream) -> proc_macro::TokenStream
fn from_str(input: &str) -> Result<Self, GMInputParseError> {
use GMInputParseError::*;
static CMD_TYPE_MAP: ::std::sync::LazyLock<::std::collections::HashMap<&'static str, #internal_enum_name>> =
static CMD_TYPE_MAP: ::std::sync::LazyLock<::std::collections::HashMap<&'static str, #internal_enum_name>> =
::std::sync::LazyLock::new(|| ::std::collections::HashMap::from([#internal_enum_mapping]));
let mut data = input.split(' ');
@ -165,4 +165,3 @@ fn get_field_sub_type(field: &Field) -> String {
_ => panic!("Unsupported field type"),
}
}

View file

@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident};
use syn::{Data, DeriveInput, Fields, Ident, parse_macro_input};
pub fn impl_decodeable(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as DeriveInput);

View file

@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident};
use syn::{Data, DeriveInput, Fields, Ident, parse_macro_input};
pub fn impl_encodeable(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as DeriveInput);

View file

@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, FnArg, Item, ItemMod, ReturnType};
use quote::{ToTokens, quote};
use syn::{FnArg, Item, ItemMod, ReturnType, parse_macro_input};
pub fn imp(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
const INVALID_FUNCTION_SIGNATURE_MSG: &str = "functions in message handler module should have following signature: fn(&mut MessageContext<'_>, CsReq) -> ScRsp";

View file

@ -1,6 +1,6 @@
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, DeriveInput, Meta, MetaList};
use quote::{ToTokens, quote};
use syn::{DeriveInput, Meta, MetaList, parse_macro_input};
mod commands;
mod decodeable;

View file

@ -1,7 +1,7 @@
#![allow(unused)]
use super::tables::{
LOOKUP_G11, LOOKUP_G13, LOOKUP_G14, LOOKUP_G2, LOOKUP_G3, LOOKUP_G9, LOOKUP_RCON, LOOKUP_SBOX,
LOOKUP_G2, LOOKUP_G3, LOOKUP_G9, LOOKUP_G11, LOOKUP_G13, LOOKUP_G14, LOOKUP_RCON, LOOKUP_SBOX,
LOOKUP_SBOX_INV, SHIFT_ROWS_TABLE, SHIFT_ROWS_TABLE_INV,
};

View file

@ -1,6 +1,6 @@
use rsa::{
pkcs1v15::SigningKey, sha2::Sha256, signature::RandomizedSigner, Pkcs1v15Encrypt,
RsaPrivateKey, RsaPublicKey,
Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey, pkcs1v15::SigningKey, sha2::Sha256,
signature::RandomizedSigner,
};
const RSA_CHUNK_SIZE: usize = 117;

View file

@ -10,10 +10,10 @@ pub use sea_orm::DbErr;
use tracing::error;
pub mod prelude {
pub use sea_orm::entity::prelude::*;
pub use sea_orm::entity::ActiveValue::*;
pub use sea_orm::query::Condition;
pub use sea_orm::TransactionTrait;
pub use sea_orm::entity::ActiveValue::*;
pub use sea_orm::entity::prelude::*;
pub use sea_orm::query::Condition;
}
#[derive(Debug, Deserialize)]

View file

@ -1,4 +1,4 @@
use byteorder::{ReadBytesExt, WriteBytesExt, BE};
use byteorder::{BE, ReadBytesExt, WriteBytesExt};
use std::collections::HashMap;
use std::io::{self, Read, Write};

View file

@ -215,17 +215,32 @@ table EquipmentSuitTemplate {
id: int;
name: string;
primary_condition: int;
primary_suit_propertys: [Property];
unk_3: int;
primary_suit_ability: int;
primary_description: string;
secondary_condition: int;
secondary_suit_propertys: [Property];
unk_7: int;
secondary_suit_ability: int;
secondary_description: string;
suit_item_icon: string;
suit_icon: string;
suit_story: string;
suit_filter_option: string;
suit_icon: string;
suit_item_icon: string;
unk_14: int;
order: int;
unk_16: int;
unk_17: int;
unk_18: int;
unk_19: int;
unk_20: int;
unk_21: int;
primary_suit_propertys: [Property];
unk_23: [int];
unk_24: int;
unk_25: [int];
unk_26: [int];
unk_27: [int];
tag: string;
}
table HollowConfigTemplate {
@ -380,6 +395,160 @@ table ConditionConfigTemplate {
type: int;
}
table AvatarBattleTemplate {
id: int;
unk_1: [int];
unk_2: [int];
unk_3: [int];
unk_4: [int];
unk_5: [int];
unk_6: [int];
unk_7: [int];
unk_8: int;
unk_9: int;
unk_10: int;
avatar_piece_id: int;
unk_12: int;
unk_13: int;
hp_max: int;
health_growth: int;
unk_16: int;
unk_17: int;
unk_18: int;
unk_19: int;
unk_20: int;
attack: int;
attack_growth: int;
defence: int;
defence_growth: int;
crit: int;
crit_damage: int;
crit_res: int;
crit_damage_res: int;
pen_rate: int;
pen_delta: int;
luck: int;
stun: int;
break_stun: int;
element_abnormal_power: int;
sp_bar_point: int;
sp_recover: int;
element_mystery: int;
rbl: int;
rbl_correction_factor: int;
rbl_probability: int;
unk_41: int;
unk_42: int;
unk_43: int;
unk_44: int;
unk_45: int;
unk_46: int;
unk_47: int;
unk_48: int;
unk_49: int;
unk_50: int;
unk_51: int;
unk_52: int;
unk_53: int;
unk_54: int;
unk_55: int;
unk_56: int;
unk_57: int;
unk_58: int;
unk_59: int;
unk_60: int;
unk_61: int;
unk_62: int;
unk_63: int;
unk_64: int;
unk_65: int;
unk_66: int;
tags: [string];
unk_68: [int];
unk_69: [int];
unk_70: int;
unk_71: int;
element: [int];
hit_type: [int];
unk_element_camp: [string];
unk_75: int;
unk_76: short;
}
table AvatarLevelAdvanceTemplate {
avatar_id: int;
id: int;
min_level: int;
max_level: int;
hp_max: int;
unk_5: int;
unk_6: int;
attack: int;
defence: int;
unk_9: int;
unk_10: int;
unk_11: int;
unk_12: int;
unk_13: int;
unk_14: int;
unk_15: int;
unk_16: int;
unk_17: int;
unk_18: [int];
promotion_costs: [RefineCost];
}
table WeaponLevelTemplate {
rarity: int;
level: int;
rate: int;
exp: int;
}
table WeaponStarTemplate {
rarity: int;
star: int;
min_level: int;
max_level: int;
star_rate: int;
rand_rate: int;
unk_6: int;
unk_7: int;
unk_8: int;
unk_9: int;
unk_10: int;
unk_11: int;
unk_12: int;
}
table AvatarPassiveSkillTemplate {
skill_id: int;
avatar_id: int;
min_avatar_level: int;
min_passive_skill_level: int;
unlock_passive_skill_level: int;
unk_5: int;
unk_levelup: string;
unk_7: int;
unk_8: int;
unk_9: int;
unk_10: int;
propertys: [Property];
names: [string];
descriptions: [string];
materials_costs: [RefineCost];
}
table EquipmentLevelTemplate {
rarity: int;
level: int;
property_rate: int;
unk_3: int;
unk_4: int;
unk_5: int;
unk_6: int;
}
table AvatarBaseTemplateTb {
data: [AvatarBaseTemplate];
}
@ -451,3 +620,27 @@ table TeleportConfigTemplateTb {
table ConditionConfigTemplateTb {
data: [ConditionConfigTemplate];
}
table AvatarBattleTemplateTb {
data: [AvatarBattleTemplate];
}
table AvatarLevelAdvanceTemplateTb {
data: [AvatarLevelAdvanceTemplate];
}
table WeaponLevelTemplateTb {
data: [WeaponLevelTemplate];
}
table WeaponStarTemplateTb {
data: [WeaponStarTemplate];
}
table AvatarPassiveSkillTemplateTb {
data: [AvatarPassiveSkillTemplate];
}
table EquipmentLevelTemplateTb {
data: [EquipmentLevelTemplate];
}

File diff suppressed because it is too large Load diff

View file

@ -58,4 +58,10 @@ file_cfg! {
BattleGroupConfigTemplateTb;
MusicPlayerConfigTemplateTb;
TeleportConfigTemplateTb;
AvatarBattleTemplateTb;
AvatarLevelAdvanceTemplateTb;
WeaponLevelTemplateTb;
WeaponStarTemplateTb;
AvatarPassiveSkillTemplateTb;
EquipmentLevelTemplateTb;
}

View file

@ -0,0 +1,91 @@
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[repr(u32)]
#[derive(IntoPrimitive, TryFromPrimitive)]
pub enum EPropertyType {
Hp = 1,
HpMax = 111,
Atk = 121,
BreakStun = 122,
Def = 131,
Crit = 201,
CritDmg = 211,
Pen = 231,
PenValue = 232,
SpRecover = 305,
ElementMystery = 312,
ElementAbnormalPower = 314,
AddedDamageRatioPhysics = 315,
AddedDamageRatioFire = 316,
AddedDamageRatioIce = 317,
AddedDamageRatioElec = 318,
AddedDamageRatioEther = 319,
// battle
HpMaxBattle = 1111,
AtkBattle = 1121,
BreakStunBattle = 1122,
DefBattle = 1131,
CritBattle = 1201,
CritDmgBattle = 1211,
PenRatioBattle = 1231,
PenDeltaBattle = 1232,
SpRecoverBattle = 1305,
ElementMysteryBattle = 1312,
ElementAbnormalPowerBattle = 1314,
AddedDamageRatioPhysicsBattle = 1315,
AddedDamageRatioFireBattle = 1316,
AddedDamageRatioIceBattle = 1317,
AddedDamageRatioElecBattle = 1318,
AddedDamageRatioEtherBattle = 1319,
// base
HpMaxBase = 11101,
AtkBase = 12101,
BreakStunBase = 12201,
DefBase = 13101,
CritBase = 20101,
CritDmgBase = 21101,
PenBase = 23101,
PenValueBase = 23201,
SpRecoverBase = 30501,
ElementMysteryBase = 31201,
ElementAbnormalPowerBase = 31401,
// ratio
HpMaxRatio = 11102,
AtkRatio = 12102,
BreakStunRatio = 12202,
DefRatio = 13102,
SpRecoverRatio = 30502,
ElementAbnormalPowerRatio = 31402,
// delta
HpMaxDelta = 11103,
AtkDelta = 12103,
BreakStunDelta = 12203,
DefDelta = 13103,
CritDelta = 20103,
CritDmgDelta = 21103,
PenDelta = 23103,
PenValueDelta = 23203,
SpRecoverDelta = 30503,
ElementMysteryDelta = 31203,
ElementAbnormalPowerDelta = 31403,
// damage ratios 1/3
AddedDamageRatioPhysics1 = 31501,
AddedDamageRatioPhysics3 = 31503,
AddedDamageRatioFire1 = 31601,
AddedDamageRatioFire3 = 31603,
AddedDamageRatioIce1 = 31701,
AddedDamageRatioIce3 = 31703,
AddedDamageRatioElec1 = 31801,
AddedDamageRatioElec3 = 31803,
AddedDamageRatioEther1 = 31901,
AddedDamageRatioEther3 = 31903,
// --- custom
// growth
HpMaxGrowth = 9999_111_0,
AtkGrowth = 9999_121_0,
DefGrowth = 9999_131_0,
// advance
HpMaxAdvance = 9999_111_1,
AtkAdvance = 9999_121_1,
DefAdvance = 9999_131_1,
}

View file

@ -1,4 +1,5 @@
pub mod action_pb;
pub mod battle;
pub mod item;
pub mod quest;
pub mod scene;

View file

@ -5,7 +5,7 @@ use std::{
path::Path,
};
use quote::{quote, ToTokens};
use quote::{ToTokens, quote};
use syn::{Field, GenericArgument, Item, PathArguments, Type, TypePath};
fn main() {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,7 @@ include!("../out/_.rs");
include!("../out/protocol_map.rs");
#[derive(trigger_protobuf_derive::CmdID)]
#[cmdid(8128)]
#[cmdid(5636)]
#[derive(trigger_protobuf_derive::XorFields, Clone, Copy, PartialEq, ::prost::Message)]
pub struct FallbackRsp {}

View file

@ -355,6 +355,7 @@ pub struct DungeonEquipInfo {
#[derive(Default, Debug, Clone, Encodeable, Decodeable)]
pub struct AvatarUnitInfo {
pub avatar_id: u32,
pub properties: HashMap<u32, i32>,
}
#[derive(Default, Debug, Clone, Encodeable, Decodeable)]
@ -1222,6 +1223,30 @@ pub struct GetNewsStandDataScRsp {
pub news_stand_data: Option<NewsStandData>,
}
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(1323)]
pub struct EndNewbieCsReq {
pub group_id: u32,
}
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(1324)]
pub struct EndNewbieScRsp {
pub retcode: i32,
}
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(1325)]
pub struct SyncGlobalVariablesCsReq {
pub global_variables: HashMap<u32, i32>,
}
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(1326)]
pub struct SyncGlobalVariablesScRsp {
pub retcode: i32,
}
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(1340)]
pub struct GetTrashbinHermitDataCsReq {}
@ -1757,3 +1782,15 @@ pub struct GetQuestionsAnswerGameDataCsReq {}
pub struct GetQuestionsAnswerGameDataScRsp {
pub retcode: i32,
}
// FlowerShop
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(7150)]
pub struct GetFlowerShopDataCsReq {}
#[derive(Default, Debug, Clone, Encodeable, Decodeable, ClientCmdID)]
#[id(7151)]
pub struct GetFlowerShopDataScRsp {
pub retcode: i32,
}

View file

@ -1,4 +1,4 @@
use serde::{de::DeserializeOwned, Deserialize, Deserializer};
use serde::{Deserialize, Deserializer, de::DeserializeOwned};
use std::net::SocketAddr;
use tracing::error;
use trigger_database::DatabaseSetting;

View file

@ -1,6 +1,6 @@
use crate::message::opcode;
use trigger_codegen::{Decodeable, Encodeable};
use trigger_protocol::{util::ProtocolUnit, DungeonEquipInfo};
use trigger_protocol::{DungeonEquipInfo, util::ProtocolUnit};
#[derive(Debug, Encodeable, Decodeable)]
pub struct BindClientSessionMessage {

View file

@ -4,7 +4,7 @@ use futures::future::BoxFuture;
use tokio::task::JoinHandle;
use tracing::warn;
use trigger_encoding::Decodeable;
use zeromq::{prelude::*, PullSocket, ZmqError};
use zeromq::{PullSocket, ZmqError, prelude::*};
use crate::message::NetworkPacket;

View file

@ -3,7 +3,7 @@ mod socket;
use std::{collections::HashMap, io::Cursor, net::SocketAddr};
pub use listener::{listen, RecvCallback};
pub use listener::{RecvCallback, listen};
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use socket::ServerSocket;

View file

@ -5,8 +5,8 @@ use std::time::Duration;
use tokio::sync::mpsc;
use tracing::warn;
use trigger_encoding::Encodeable;
use zeromq::prelude::*;
use zeromq::PushSocket;
use zeromq::prelude::*;
use crate::message::NetworkPacket;

View file

@ -30,7 +30,7 @@ user = "root"
password = "root"
[security]
static_key = "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"
static_key = "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"
[[security.rsa_versions]]
version = 3