From a7a1f4da376ba58215f0335110f507aebcd14f70 Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:41:18 +0800 Subject: [PATCH 1/7] Implement static config gacha needed --- assets/GachaConfig/gacha.jsonc | 0 nap_data/Cargo.toml | 2 + nap_data/src/gacha/gacha_config.rs | 229 ++++++++++++++++++ nap_data/src/gacha/mod.rs | 45 ++++ nap_data/src/lib.rs | 6 + nap_gameserver/src/config.rs | 1 + nap_gameserver/src/logic/gacha/gacha_model.rs | 18 ++ nap_gameserver/src/logic/gacha/mod.rs | 4 + nap_gameserver/src/logic/gacha/record.rs | 0 nap_gameserver/src/logic/mod.rs | 1 + nap_gameserver/src/logic/player/player.rs | 4 + nap_proto/out/bin.rs | 73 ++++++ 12 files changed, 383 insertions(+) create mode 100644 assets/GachaConfig/gacha.jsonc create mode 100644 nap_data/src/gacha/gacha_config.rs create mode 100644 nap_data/src/gacha/mod.rs create mode 100644 nap_gameserver/src/logic/gacha/gacha_model.rs create mode 100644 nap_gameserver/src/logic/gacha/mod.rs create mode 100644 nap_gameserver/src/logic/gacha/record.rs diff --git a/assets/GachaConfig/gacha.jsonc b/assets/GachaConfig/gacha.jsonc new file mode 100644 index 0000000..e69de29 diff --git a/nap_data/Cargo.toml b/nap_data/Cargo.toml index 9c6161f..cd7aced 100644 --- a/nap_data/Cargo.toml +++ b/nap_data/Cargo.toml @@ -18,3 +18,5 @@ tracing.workspace = true # Internal proto.workspace = true +jsonc-parser = { version = "0.23.0", features = ["serde"] } +chrono = { version = "0.4.38", features = ["serde"] } diff --git a/nap_data/src/gacha/gacha_config.rs b/nap_data/src/gacha/gacha_config.rs new file mode 100644 index 0000000..fa777ed --- /dev/null +++ b/nap_data/src/gacha/gacha_config.rs @@ -0,0 +1,229 @@ +use std::collections::{HashMap, HashSet}; + +use chrono::{prelude::Local, DateTime}; +use serde::Deserialize; +use tracing; + +#[derive(Debug, Default, Deserialize)] +pub struct ExtraItemsPolicy { + pub id: u32, + pub count: u32, + #[serde(default)] + pub apply_on_owned_count: u32, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ProbabilityPoint { + pub start_pity: u32, + pub start_chance_percent: f64, + #[serde(default)] + pub increment_percent: f64, +} + +#[derive(Debug, Default, Deserialize)] +pub struct ProbabilityModel { + #[serde(default)] + pub clear_status_on_higher_rarity_pulled: bool, + pub points: Vec, + // This value is for display only, so it's set when + // the maximum guarantee is not equal to the + // automatically calculated value (commonly, less than). + #[serde(default)] + pub maximum_guarantee_pity: u32, + + #[serde(skip_deserializing)] + probability_percents: Vec, +} + +impl ProbabilityModel { + fn get_maximum_guarantee(&self) -> u32 { + self.probability_percents.len() as u32 - 1 + } + + pub fn post_configure(&mut self, tag: &String) { + self.points.sort_by_key(|point| point.start_pity); + + let mut probability_percents: Vec = vec![0.0]; + for (i, point) in self.points.iter().enumerate() { + if i > 0 { + let last_point = &self.points[i - 1]; + let last_stop_percent = last_point.start_chance_percent + + last_point.increment_percent + * (point.start_pity - last_point.start_pity) as f64; + if last_stop_percent > point.start_chance_percent { + tracing::warn!("Gacha - ProbabilityModel '{tag}': The start chance of '{point:?}' is less than the value inherited from the previous point."); + } + } + + let mut max_pity = 2000; + if i < self.points.len() - 1 { + let next_point = &self.points[i + 1]; + max_pity = next_point.start_pity - 1; + let max_probability = point.start_chance_percent + + point.increment_percent + * (next_point.start_pity - 1 - point.start_pity) as f64; + assert!(max_probability < 100.0, "Gacha - ProbabilityModel '{tag}': Probability already reached 100% in '{point:?}' (though points with higher pity left)"); + } + + let mut pity = point.start_pity; + let mut percent = point.start_chance_percent; + while pity <= max_pity { + if max_pity >= 2000 && percent >= 100.0 { + probability_percents.push(100.0); + break; + } + probability_percents.push(percent); + percent += point.increment_percent; + pity += 1; + } + assert!(pity <= 2000, "Gacha - ProbabilityModel '{tag}' (point {i}): Haven't reached 100% guarantee probability at Pity 2001. The current probability is {percent}%. Crazy."); + } + + self.probability_percents = probability_percents; + if self.maximum_guarantee_pity <= 0 { + self.maximum_guarantee_pity = self.get_maximum_guarantee(); + } + } + + pub fn get_chance_percent(&self, pity: &u32) -> f64 { + // The vec length is 1 bigger than the maximum pity (1-based) + let guarantee_pity = self.probability_percents.len() - 1; + let idx = *pity as usize; + if idx > guarantee_pity { + return self.probability_percents[guarantee_pity]; + } + self.probability_percents[idx] + } +} + +#[allow(dead_code)] +#[derive(Debug, Default, Deserialize)] +pub struct CategoryGuaranteePolicy { + pub included_category_tags: HashSet, + pub trigger_on_failure_times: u32, + pub clear_status_on_target_changed: bool, + pub chooseable: bool, +} + +#[derive(Debug, Default, Deserialize)] +pub struct TenPullDiscount { + pub use_limit: u32, + pub discounted_prize: u32, +} + +#[derive(Debug, Default, Deserialize)] +pub struct AdvancedGuarantee { + pub use_limit: u32, + pub rarity: u32, + pub guarantee_pity: u32, +} + +#[derive(Debug, Default, Deserialize)] +pub struct MustGainItem { + pub use_limit: u32, + pub rarity: u32, + pub category_tag: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct DiscountPolicyCollection { + pub ten_pull_discount_map: HashMap, + pub must_gain_item_map: HashMap, + pub advanced_guarantee_map: HashMap, +} + +impl DiscountPolicyCollection { + pub fn post_configure(&mut self) { + for (tag, ten_pull_discount) in self.ten_pull_discount_map.iter() { + let discounted_prize = ten_pull_discount.discounted_prize; + assert!(discounted_prize < 10, "Gacha - DiscountPolicy '{tag}': ten_pull_discount's value should be smaller than 10 (read {discounted_prize})."); + } + } +} + +#[derive(Debug, Default, Deserialize)] +pub struct GachaCategoryInfo { + #[serde(default)] + pub is_promotional_items: bool, + pub item_ids: Vec, + pub category_weight: u32, +} + +#[derive(Debug, Default, Deserialize)] +pub struct GachaAvailableItemsInfo { + pub rarity: u32, + #[serde(default)] + pub extra_items_policy_tags: Vec, + pub categories: HashMap, + pub probability_model_tag: String, + #[serde(default)] + pub category_guarantee_policy_tags: Vec, +} + +#[allow(dead_code)] +#[derive(Debug, Default, Deserialize)] +pub struct CharacterGachaPool { + pub gacha_schedule_id: u32, + pub gacha_parent_schedule_id: u32, + pub comment: String, + pub gacha_type: u32, + pub cost_item_id: u32, + pub start_time: DateTime, + pub end_time: DateTime, + #[serde(default)] + pub discount_policy_tags: Vec, + pub sharing_guarantee_info_category: String, + pub gacha_items: Vec, +} + +impl CharacterGachaPool { + pub fn is_still_open(&self, now: &DateTime) -> bool { + self.start_time <= *now && *now <= self.end_time + } + + pub fn post_configure(&mut self, probability_model_map: &HashMap) { + self.gacha_items + .sort_by_key(|item_list| u32::MAX - item_list.rarity); + for items_info in self.gacha_items.iter_mut() { + assert!(probability_model_map.contains_key(&items_info.probability_model_tag), "Gacha - CharacterGachaPool '{}': Specified ProbabilityModel tag '{}' that does not exist.", self.gacha_schedule_id, items_info.probability_model_tag); + } + } +} + +#[derive(Debug, Default, Deserialize)] +pub struct GachaCommonProperties { + pub up_item_category_tag: String, + pub s_item_rarity: u32, + pub a_item_rarity: u32, + // TODO: PostConfigure check + pub ten_pull_discount_tag: String, + pub newcomer_advanced_s_tag: String, +} + +#[derive(Debug, Default, Deserialize)] +pub struct GachaConfiguration { + pub character_gacha_pool_list: Vec, + pub probability_model_map: HashMap, + pub category_guarantee_policy_map: HashMap, + pub extra_items_policy_map: HashMap, + pub discount_policies: DiscountPolicyCollection, + pub common_properties: GachaCommonProperties, +} + +impl GachaConfiguration { + pub fn post_configure(&mut self) { + assert!( + self.category_guarantee_policy_map + .contains_key(&self.common_properties.up_item_category_tag), + "The UP category should be valid in policy map." + ); + + for (tag, policy) in self.probability_model_map.iter_mut() { + policy.post_configure(&tag); + } + self.discount_policies.post_configure(); + for character_pool in self.character_gacha_pool_list.iter_mut() { + character_pool.post_configure(&self.probability_model_map); + } + } +} diff --git a/nap_data/src/gacha/mod.rs b/nap_data/src/gacha/mod.rs new file mode 100644 index 0000000..4b64edc --- /dev/null +++ b/nap_data/src/gacha/mod.rs @@ -0,0 +1,45 @@ +use std::sync::OnceLock; + +use gacha_config::GachaConfiguration; +use jsonc_parser::{parse_to_serde_value, ParseOptions}; +use serde::Deserialize; + +use crate::{action::ActionConfig, DataLoadError}; + +pub mod gacha_config; + +const GACHA_CONFIG_NAME: &str = "gacha.jsonc"; +static GACHACONF: OnceLock = OnceLock::new(); + +#[derive(Deserialize, Debug)] +pub struct EventGraphConfig { + pub event_id: u32, + pub actions: Vec, +} + +pub(crate) fn load_gacha_config(path: &str) -> Result<(), DataLoadError> { + let jsonc_data = std::fs::read_to_string(format!("{path}/{GACHA_CONFIG_NAME}")) + .map_err(|err| DataLoadError::IoError(err))?; + + let json_value = parse_to_serde_value( + &jsonc_data, + &ParseOptions { + allow_comments: true, + allow_loose_object_property_names: false, + allow_trailing_commas: true, + }, + ) + .map_err(|err| DataLoadError::JsoncParseError(err))? + .unwrap(); + + let mut result = serde_json::from_value::(json_value) + .map_err(|err| DataLoadError::FromJsonError(String::from("GachaConfiguration"), err))?; + result.post_configure(); + + GACHACONF.set(result).unwrap(); + Ok(()) +} + +pub fn global_gacha_config() -> &'static GachaConfiguration { + GACHACONF.get().unwrap() +} diff --git a/nap_data/src/lib.rs b/nap_data/src/lib.rs index 5e30e4d..7aaf0d4 100644 --- a/nap_data/src/lib.rs +++ b/nap_data/src/lib.rs @@ -1,9 +1,11 @@ pub mod action; pub mod event; +pub mod gacha; pub mod tables; use std::{collections::HashMap, sync::OnceLock}; +use jsonc_parser::errors::ParseError; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -12,6 +14,7 @@ pub struct AssetsConfig { pub filecfg_path: String, pub event_config_path: String, pub usm_keys_path: String, + pub gacha_config_path: String, } #[derive(Error, Debug)] @@ -20,6 +23,8 @@ pub enum DataLoadError { IoError(#[from] std::io::Error), #[error("from_json failed for type {0}, error: {1}")] FromJsonError(String, serde_json::Error), + #[error("jsonc_parser parse as json error: {0}")] + JsoncParseError(#[from] ParseError), } static USM_KEY_MAP: OnceLock> = OnceLock::new(); @@ -31,6 +36,7 @@ pub fn init_data(config: &AssetsConfig) -> Result<(), DataLoadError> { tracing::warn!("failed to load USM keys, in-game cutscenes will not work! Reason: {err}"); USM_KEY_MAP.set(HashMap::new()).unwrap(); } + gacha::load_gacha_config(&config.gacha_config_path)?; Ok(()) } diff --git a/nap_gameserver/src/config.rs b/nap_gameserver/src/config.rs index b2555f8..7a5104a 100644 --- a/nap_gameserver/src/config.rs +++ b/nap_gameserver/src/config.rs @@ -21,6 +21,7 @@ impl Default for NapGSConfig { filecfg_path: String::from("assets/FileCfg"), event_config_path: String::from("assets/EventConfig"), usm_keys_path: String::from("assets/VideoUSMEncKeys.json"), + gacha_config_path: String::from("assets/GachaConfig"), }, } } diff --git a/nap_gameserver/src/logic/gacha/gacha_model.rs b/nap_gameserver/src/logic/gacha/gacha_model.rs new file mode 100644 index 0000000..0798875 --- /dev/null +++ b/nap_gameserver/src/logic/gacha/gacha_model.rs @@ -0,0 +1,18 @@ +use proto::GachaModelBin; + +#[derive(Default)] +pub struct GachaModel { + pub gacha_bin: GachaModelBin, +} + +impl GachaModel { + pub fn from_bin(gacha_bin: GachaModelBin) -> Self { + Self { + gacha_bin + } + } + + pub fn to_bin(&self) -> GachaModelBin { + self.gacha_bin.clone() + } +} \ No newline at end of file diff --git a/nap_gameserver/src/logic/gacha/mod.rs b/nap_gameserver/src/logic/gacha/mod.rs new file mode 100644 index 0000000..a90cce2 --- /dev/null +++ b/nap_gameserver/src/logic/gacha/mod.rs @@ -0,0 +1,4 @@ +mod gacha_model; +mod record; + +pub use gacha_model::GachaModel; \ No newline at end of file diff --git a/nap_gameserver/src/logic/gacha/record.rs b/nap_gameserver/src/logic/gacha/record.rs new file mode 100644 index 0000000..e69de29 diff --git a/nap_gameserver/src/logic/mod.rs b/nap_gameserver/src/logic/mod.rs index fdb810e..79390cb 100644 --- a/nap_gameserver/src/logic/mod.rs +++ b/nap_gameserver/src/logic/mod.rs @@ -1,5 +1,6 @@ pub mod battle; mod enums; +pub mod gacha; pub mod game; pub mod item; pub mod math; diff --git a/nap_gameserver/src/logic/player/player.rs b/nap_gameserver/src/logic/player/player.rs index 583d822..294444e 100644 --- a/nap_gameserver/src/logic/player/player.rs +++ b/nap_gameserver/src/logic/player/player.rs @@ -1,6 +1,7 @@ use data::tables::{self, AvatarBaseID}; use proto::{ItemStatic, PlayerDataBin, Retcode}; +use super::gacha::GachaModel; use super::game::{FrontendGame, FrontendGameError, GameInstance, LogicError}; use super::item::{ItemModel, ItemUID}; use super::main_city_model::MainCityModel; @@ -16,6 +17,7 @@ pub struct Player { pub role_model: RoleModel, pub item_model: ItemModel, pub main_city_model: MainCityModel, + pub gacha_model: GachaModel, } impl Player { @@ -28,6 +30,7 @@ impl Player { role_model: Some(self.role_model.to_bin()), item_model: Some(self.item_model.to_bin()), main_city_model: Some(self.main_city_model.to_bin()), + gacha_model: Some(self.gacha_model.to_bin()), } } @@ -44,6 +47,7 @@ impl Player { .main_city_model .map(MainCityModel::from_bin) .unwrap_or_default(), + gacha_model: bin.gacha_model.map(GachaModel::from_bin).unwrap_or_default(), ..Default::default() } } diff --git a/nap_proto/out/bin.rs b/nap_proto/out/bin.rs index 29587ca..7f93d83 100644 --- a/nap_proto/out/bin.rs +++ b/nap_proto/out/bin.rs @@ -119,6 +119,77 @@ pub struct MainCityModelBin { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct GachaProgressBin { + /// The pity (counting how many pulls) of this pull (in previous record) or the next pull (in status). + #[prost(uint32, tag = "1")] + pub pity: u32, + /// The failure times of this category. + #[prost(map = "string, uint32", tag = "2")] + pub categories_progress_map: ::std::collections::HashMap< + ::prost::alloc::string::String, + u32, + >, + /// The selected priority for a Chooseable category. + #[prost(map = "string, string", tag = "3")] + pub category_chosen_guarantee_map: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GachaExtraItemBin { + #[prost(uint32, tag = "1")] + pub extra_item_id: u32, + #[prost(uint32, tag = "2")] + pub extra_item_count: u32, + /// How many objects of the main item obtained in gacha is present in the player's bag (before gacha). + /// This is used for something like converting when there're extra characters. + #[prost(uint32, tag = "3")] + pub currently_gained: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GachaRecordBin { + #[prost(int64, tag = "1")] + pub pull_timestamp: i64, + #[prost(uint32, tag = "2")] + pub obtained_item_id: u32, + #[prost(uint32, tag = "3")] + pub gacha_id: u32, + /// The progress BEFORE this gacha is performed. uint32 is rarity. + #[prost(map = "uint32, message", tag = "4")] + pub progress_map: ::std::collections::HashMap, + #[prost(message, optional, tag = "5")] + pub extra_item_bin: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GachaStatusBin { + #[prost(map = "uint32, message", tag = "1")] + pub rarity_status_map: ::std::collections::HashMap, + #[prost(map = "string, uint32", tag = "2")] + pub discount_usage_map: ::std::collections::HashMap< + ::prost::alloc::string::String, + u32, + >, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GachaModelBin { + /// Gacha Status query. string is sharing_guarantee_info_category. + #[prost(map = "string, message", tag = "1")] + pub gacha_status_map: ::std::collections::HashMap< + ::prost::alloc::string::String, + GachaStatusBin, + >, + #[prost(message, repeated, tag = "2")] + pub gacha_records: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "3")] + pub random_number: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PlayerDataBin { #[prost(message, optional, tag = "1")] pub basic_data_model: ::core::option::Option, @@ -130,4 +201,6 @@ pub struct PlayerDataBin { pub item_model: ::core::option::Option, #[prost(message, optional, tag = "5")] pub main_city_model: ::core::option::Option, + #[prost(message, optional, tag = "6")] + pub gacha_model: ::core::option::Option, } -- 2.34.1 From db2677be503358022812764ad4056023a45fbc1b Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:52:35 +0800 Subject: [PATCH 2/7] Proto (Gacha related) --- nap_proto/out/_.rs | 130 ++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/nap_proto/out/_.rs b/nap_proto/out/_.rs index c7779be..eb6ccfb 100644 --- a/nap_proto/out/_.rs +++ b/nap_proto/out/_.rs @@ -227,7 +227,7 @@ pub struct Nkhnlakggmj { pub iddehlcpjjn: ::prost::alloc::string::String, #[xor(10163)] #[prost(int64, tag = "14")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(8852)] #[prost(uint32, tag = "8")] pub ppnbkmndpjc: u32, @@ -829,7 +829,7 @@ pub struct Fkkojjgepnb { pub lchdjcdjiik: u32, #[xor(4851)] #[prost(int64, tag = "3")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(8589)] #[prost(int32, tag = "505")] pub lecpejadije: i32, @@ -4512,7 +4512,7 @@ pub struct Kancdiinhgh { #[prost(uint64, tag = "2")] pub dbkpbkpcoog: u64, #[prost(uint64, tag = "3")] - pub phkcdmjheen: u64, + pub end_timestamp: u64, } #[derive(proto_gen::CmdID)] #[cmdid(2438)] @@ -8223,7 +8223,7 @@ pub struct Ihhaahinlik { #[prost(int64, tag = "3")] pub dbkpbkpcoog: i64, #[prost(int64, tag = "4")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -9108,7 +9108,7 @@ pub struct Gkfhklbbcpo { #[derive(Clone, PartialEq, ::prost::Message)] pub struct Enkkecgonaf { #[prost(uint32, tag = "1")] - pub abalhhfapla: u32, + pub cost_item_count: u32, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -9603,7 +9603,7 @@ pub struct Gfpehbhpgfb { pub avatars: ::prost::alloc::vec::Vec, #[xor(8960)] #[prost(uint32, tag = "14")] - pub abalhhfapla: u32, + pub cost_item_count: u32, #[xor(8889)] #[prost(uint32, tag = "6")] pub damhjcgieco: u32, @@ -9668,7 +9668,7 @@ pub struct Cbccakknimc { pub struct Bfkkjofnjjo { #[xor(4795)] #[prost(uint32, tag = "10")] - pub abalhhfapla: u32, + pub cost_item_count: u32, #[xor(5066)] #[prost(uint32, tag = "4")] pub cmacbfkaoma: u32, @@ -10605,7 +10605,7 @@ pub struct Npdgpemipij { #[prost(int32, tag = "5")] pub peamchapinf: i32, #[prost(string, tag = "2")] - pub phkcdmjheen: ::prost::alloc::string::String, + pub end_timestamp: ::prost::alloc::string::String, } #[derive(proto_gen::CmdID)] #[cmdid(5101)] @@ -12389,7 +12389,7 @@ pub struct InteractWithUnitCsReq { #[derive(Clone, PartialEq, ::prost::Message)] pub struct Ofcdfiihahe { #[prost(uint32, tag = "1")] - pub abalhhfapla: u32, + pub cost_item_count: u32, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -12926,7 +12926,7 @@ pub struct Hdjijldgmab { pub struct Cnmgfcllhpl { #[xor(11487)] #[prost(uint32, tag = "3")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, } #[derive(proto_gen::CmdID)] #[cmdid(665)] @@ -13423,7 +13423,7 @@ pub struct Jlldijenmoc { pub dphghojclod: u32, #[xor(8783)] #[prost(int64, tag = "2")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(525)] #[prost(uint32, tag = "8")] pub leagnodilli: u32, @@ -13731,19 +13731,19 @@ pub struct Omdfkjoopce { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Effclengcpo { +pub struct DoGachaCsReq { #[xor(13734)] #[prost(uint32, tag = "15")] pub eglpecifagd: u32, #[xor(4101)] #[prost(uint32, tag = "1")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, #[xor(8588)] #[prost(uint32, tag = "13")] - pub madciamhahg: u32, + pub random_number: u32, #[xor(10835)] #[prost(uint32, tag = "3")] - pub abalhhfapla: u32, + pub cost_item_count: u32, #[xor(3853)] #[prost(uint32, tag = "5")] pub gacha_type: u32, @@ -14035,10 +14035,10 @@ pub struct Nffblkigaan { pub weapon_uid: u32, #[xor(2492)] #[prost(uint32, tag = "9")] - pub eijhjbplhih: u32, + pub need_item_id: u32, #[xor(13312)] #[prost(uint32, tag = "8")] - pub bplmpghdklb: u32, + pub need_item_count: u32, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -15705,7 +15705,7 @@ pub struct Gcffmmdgobe { #[prost(int32, tag = "1")] pub opmgaofadph: i32, #[prost(int32, tag = "2")] - pub abalhhfapla: i32, + pub cost_item_count: i32, } #[derive(proto_gen::CmdID)] #[cmdid(1224)] @@ -16201,13 +16201,13 @@ pub struct ClientSystemsInfo { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Feanepokfam { +pub struct NeedItemInfo { #[xor(156)] #[prost(uint32, tag = "15")] - pub bplmpghdklb: u32, + pub need_item_count: u32, #[xor(11997)] #[prost(uint32, tag = "5")] - pub eijhjbplhih: u32, + pub need_item_id: u32, } #[derive(proto_gen::CmdID)] #[cmdid(1140)] @@ -16647,14 +16647,14 @@ pub struct Ejjimjcohgg { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Akjiehealco { +pub struct DoGachaScRsp { #[prost(message, optional, tag = "6")] pub gacha_data: ::core::option::Option, #[prost(message, repeated, tag = "13")] - pub mjajbddaemm: ::prost::alloc::vec::Vec, + pub gain_item_list: ::prost::alloc::vec::Vec, #[xor(12395)] #[prost(uint32, tag = "11")] - pub abalhhfapla: u32, + pub cost_item_count: u32, #[xor(14106)] #[prost(int32, tag = "5")] pub retcode: i32, @@ -17252,7 +17252,7 @@ pub struct Ipfpofcbnjp { pub struct Ilehibpgief { #[xor(14424)] #[prost(int64, tag = "4")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[prost(uint32, repeated, tag = "6")] pub pjilpbkknan: ::prost::alloc::vec::Vec, #[xor(4341)] @@ -17842,7 +17842,7 @@ pub struct Odijcgldmia { #[prost(uint32, tag = "11")] pub bpegheknole: u32, #[prost(message, repeated, tag = "14")] - pub bapbocgilep: ::prost::alloc::vec::Vec, + pub bapbocgilep: ::prost::alloc::vec::Vec, #[prost(map = "uint32, uint32", tag = "10")] pub maeegjdknkg: ::std::collections::HashMap, #[prost(uint32, repeated, tag = "9")] @@ -17850,20 +17850,20 @@ pub struct Odijcgldmia { #[prost(string, tag = "3")] pub fnanndecaan: ::prost::alloc::string::String, #[prost(message, repeated, tag = "13")] - pub pbalmllekpp: ::prost::alloc::vec::Vec, + pub need_item_info_list: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag = "6")] - pub mplcofohjnl: ::prost::alloc::vec::Vec, + pub mplcofohjnl: ::prost::alloc::vec::Vec, #[xor(5947)] #[prost(uint32, tag = "5")] pub r#type: u32, #[prost(message, repeated, tag = "8")] - pub dnadnehoogk: ::prost::alloc::vec::Vec, + pub dnadnehoogk: ::prost::alloc::vec::Vec, #[xor(5194)] #[prost(uint32, tag = "1")] pub dgaafhidocl: u32, #[xor(11358)] #[prost(uint32, tag = "12")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -18737,7 +18737,7 @@ pub struct Ilghjldjhcl { pub hbmnikpdgon: ::prost::alloc::string::String, #[xor(9518)] #[prost(int64, tag = "9")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(1810)] #[prost(uint32, tag = "1")] pub oeaieooemng: u32, @@ -18990,7 +18990,7 @@ pub struct Cobgikcepkp { #[prost(int32, tag = "1")] pub imhfejennof: i32, #[prost(string, tag = "4")] - pub phkcdmjheen: ::prost::alloc::string::String, + pub end_timestamp: ::prost::alloc::string::String, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -22212,9 +22212,9 @@ pub struct Mbokdhgpobc { #[derive(Clone, PartialEq, ::prost::Message)] pub struct Gacha { #[prost(uint32, repeated, tag = "1425")] - pub jkbfeediiho: ::prost::alloc::vec::Vec, + pub chooseable_up_list: ::prost::alloc::vec::Vec, #[prost(uint32, repeated, tag = "15")] - pub nfngmliibdf: ::prost::alloc::vec::Vec, + pub up_s_item_list: ::prost::alloc::vec::Vec, #[xor(1098)] #[prost(uint32, tag = "11")] pub obabhacfokn: u32, @@ -22222,7 +22222,7 @@ pub struct Gacha { pub ioiebkbcnoi: ::prost::alloc::string::String, #[xor(13960)] #[prost(uint32, tag = "698")] - pub gbmmbcbefmp: u32, + pub advanced_s_guarantee: u32, #[xor(1462)] #[prost(uint32, tag = "10")] pub kagiddohcmk: u32, @@ -22230,30 +22230,30 @@ pub struct Gacha { #[prost(uint32, tag = "1921")] pub nammdglepbk: u32, #[prost(message, repeated, tag = "3")] - pub pbalmllekpp: ::prost::alloc::vec::Vec, + pub need_item_info_list: ::prost::alloc::vec::Vec, #[xor(9792)] #[prost(int64, tag = "14")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(6233)] #[prost(uint32, tag = "563")] - pub dimlpkbggfc: u32, + pub discount_ten_roll_prize: u32, #[prost(bool, tag = "1756")] pub nkeigieepgn: bool, #[prost(uint32, repeated, tag = "755")] - pub hbhckgpjian: ::prost::alloc::vec::Vec, + pub free_select_item_list: ::prost::alloc::vec::Vec, #[prost(string, tag = "2")] - pub mpmnlphpdlk: ::prost::alloc::string::String, + pub drop_history_webview: ::prost::alloc::string::String, #[xor(4816)] #[prost(uint32, tag = "13")] - pub pladpclalgn: u32, + pub gacha_schedule_id: u32, #[xor(11392)] #[prost(uint32, tag = "574")] - pub fjhglcclbgm: u32, + pub free_select_required_pull: u32, #[prost(uint32, repeated, tag = "6")] - pub goainlmbhnn: ::prost::alloc::vec::Vec, + pub up_a_item_list: ::prost::alloc::vec::Vec, #[xor(8288)] #[prost(uint32, tag = "12")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, #[xor(8537)] #[prost(uint32, tag = "1662")] pub pcjdafaaimg: u32, @@ -22273,26 +22273,26 @@ pub struct Gacha { pub gacha_type: u32, #[xor(5057)] #[prost(uint32, tag = "244")] - pub ahidoimfiof: u32, + pub s_guarantee: u32, #[xor(1870)] #[prost(int64, tag = "8")] pub start_timestamp: i64, #[xor(562)] #[prost(uint32, tag = "726")] - pub mkiplhjemoi: u32, + pub free_select_progress: u32, #[xor(8338)] #[prost(uint32, tag = "96")] pub ihjnkoijdgh: u32, #[xor(4210)] #[prost(uint32, tag = "789")] - pub gokmdbojehm: u32, + pub a_guarantee: u32, #[prost(uint32, repeated, tag = "4")] pub akggbhgkifd: ::prost::alloc::vec::Vec, #[xor(1056)] #[prost(uint32, tag = "2002")] - pub kikannccmmo: u32, + pub discount_avaliable_num: u32, #[prost(string, tag = "7")] - pub jahbjmphipl: ::prost::alloc::string::String, + pub gacha_info_list_webview: ::prost::alloc::string::String, #[xor(9009)] #[prost(uint32, tag = "419")] pub ekjlhhdekka: u32, @@ -22699,7 +22699,7 @@ pub struct Mcjgjlpjfjc { pub gebpolkeieh: u32, #[xor(4412)] #[prost(int64, tag = "1")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -23294,7 +23294,7 @@ pub struct GachaData { pub cmamhfldihg: ::core::option::Option, #[xor(9369)] #[prost(uint32, tag = "13")] - pub madciamhahg: u32, + pub random_number: u32, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -23399,7 +23399,7 @@ pub struct Nalkdbjimgk { pub aohakmnfinf: u32, #[xor(2280)] #[prost(uint32, tag = "4")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, } #[derive(proto_gen::CmdID)] #[cmdid(4620)] @@ -24021,7 +24021,7 @@ pub struct Himappelgdm { pub gppmclpnjoe: u32, #[xor(4244)] #[prost(uint64, tag = "11")] - pub phkcdmjheen: u64, + pub end_timestamp: u64, #[xor(6824)] #[prost(uint32, tag = "7")] pub enhandfaalc: u32, @@ -24564,7 +24564,7 @@ pub struct Pahjnbjogon { pub item_id: u32, #[xor(1511)] #[prost(uint32, tag = "1")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, } #[derive(proto_gen::CmdID)] #[derive(proto_gen::XorFields)] @@ -24653,7 +24653,7 @@ pub struct Eindafcpkce { #[prost(int32, tag = "1")] pub nkiegkopoeg: i32, #[prost(int32, tag = "2")] - pub abalhhfapla: i32, + pub cost_item_count: i32, } #[derive(proto_gen::CmdID)] #[cmdid(3276)] @@ -26395,7 +26395,7 @@ pub struct Pfcmihnfmme { pub cfnblioopmp: u32, #[xor(13350)] #[prost(int64, tag = "15")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(13289)] #[prost(int64, tag = "5")] pub kehdpankopd: i64, @@ -27534,7 +27534,7 @@ pub struct TipsInfo { pub struct Ieimfkpmegp { #[xor(378)] #[prost(uint32, tag = "12")] - pub dcealmadfgi: u32, + pub gacha_parent_schedule_id: u32, #[xor(1946)] #[prost(uint32, tag = "7")] pub avatar_id: u32, @@ -31149,7 +31149,7 @@ pub struct Bgheihedbcb { pub apociobpoho: u32, #[xor(9137)] #[prost(int64, tag = "2")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[prost(bool, tag = "4")] pub mgfcmlpkjkg: bool, #[xor(438)] @@ -31470,7 +31470,7 @@ pub struct Nnbooaekcml { pub ncjcmkgfpej: ::prost::alloc::vec::Vec, #[xor(15963)] #[prost(int32, tag = "13")] - pub eijhjbplhih: i32, + pub need_item_id: i32, } #[derive(proto_gen::CmdID)] #[cmdid(816)] @@ -32445,21 +32445,21 @@ pub struct Mkaecbehadi { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Depahhdodeb { +pub struct GainItemInfo { #[xor(9764)] #[prost(uint32, tag = "5")] pub num: u32, #[xor(12647)] #[prost(uint32, tag = "2")] - pub ollapieolpm: u32, + pub extra_item_count: u32, #[prost(map = "uint32, uint32", tag = "4")] - pub nkdheebkjjl: ::std::collections::HashMap, + pub extra_items: ::std::collections::HashMap, #[xor(11858)] #[prost(uint32, tag = "11")] pub item_id: u32, #[xor(6146)] #[prost(uint32, tag = "8")] - pub dghfjhiikkn: u32, + pub extra_item_id: u32, #[xor(10021)] #[prost(uint32, tag = "9")] pub uid: u32, @@ -32766,7 +32766,7 @@ pub struct Lipadknfagg { pub abdfdamklia: bool, #[xor(1841)] #[prost(int64, tag = "1696")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(15522)] #[prost(int64, tag = "14")] pub jkjhhbpcaon: i64, @@ -32871,7 +32871,7 @@ pub struct Gojokjnppnp { pub mlfannobjdp: bool, #[xor(1038)] #[prost(int64, tag = "4")] - pub phkcdmjheen: i64, + pub end_timestamp: i64, #[xor(5317)] #[prost(uint32, tag = "14")] pub ialhcipedom: u32, -- 2.34.1 From 55780314d1ce11166b4d80a44fc1398933748845 Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:53:54 +0800 Subject: [PATCH 3/7] Gacha impl --- nap_gameserver/Cargo.toml | 1 + nap_gameserver/src/handlers/gacha.rs | 227 ++++++++++- nap_gameserver/src/handlers/mod.rs | 1 + nap_gameserver/src/logic/gacha/gacha_model.rs | 382 +++++++++++++++++- 4 files changed, 603 insertions(+), 8 deletions(-) diff --git a/nap_gameserver/Cargo.toml b/nap_gameserver/Cargo.toml index fafe57c..f949cbe 100644 --- a/nap_gameserver/Cargo.toml +++ b/nap_gameserver/Cargo.toml @@ -41,3 +41,4 @@ tracing-bunyan-formatter.workspace = true common.workspace = true data.workspace = true proto.workspace = true +chrono = { version = "0.4.38", features = ["serde"] } diff --git a/nap_gameserver/src/handlers/gacha.rs b/nap_gameserver/src/handlers/gacha.rs index 3b1637e..dcc8de4 100644 --- a/nap_gameserver/src/handlers/gacha.rs +++ b/nap_gameserver/src/handlers/gacha.rs @@ -1,13 +1,234 @@ +use data::gacha::{gacha_config::*, global_gacha_config}; + +use proto::*; use super::*; +use chrono::{DateTime, Local}; pub async fn on_get_gacha_data( _session: &NetSession, _player: &mut Player, req: GetGachaDataCsReq, ) -> NetResult { - Ok(GetGachaDataScRsp { + if req.gacha_type != 3 { + // tracing::info!("non-supported gacha type {}", body.gacha_type); + Ok(GetGachaDataScRsp { + retcode: Retcode::RetSucc.into(), + gacha_type: req.gacha_type, + gacha_data: Some(GachaData::default()), + }) + } else { + // tracing::info!("construct gacha info"); + Ok(GetGachaDataScRsp { + retcode: Retcode::RetSucc.into(), + gacha_type: req.gacha_type, + gacha_data: Some(generate_all_gacha_info(_player)), + }) + } +} + +pub async fn on_do_gacha( + _session: &NetSession, + _player: &mut Player, + req: DoGachaCsReq, +) -> NetResult { + let gachaconf = global_gacha_config(); + let gacha_model = &mut _player.gacha_model; + let pull_time = Local::now(); + let target_pool = get_gacha_pool( + &gachaconf.character_gacha_pool_list, + &req.gacha_parent_schedule_id, + &pull_time, + ); + if let None = target_pool { + return Ok(DoGachaScRsp { + retcode: Retcode::RetSucc.into(), + ..Default::default() + }); + }; + let target_pool = target_pool.unwrap(); + + // TODO: Validate cost_item_count + // tracing::info!("cost_item_count: {}", body.cost_item_count); + let mut pull_count = if req.cost_item_count > 1 { 10 } else { 1 }; + + if pull_count == 10 { + let discount_tag = &gachaconf.common_properties.ten_pull_discount_tag; + if target_pool.discount_policy_tags.contains(&discount_tag) { + let gacha_bin = &mut gacha_model.gacha_bin; + let status_bin = gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap(); + let discount_policy = gachaconf + .discount_policies + .ten_pull_discount_map + .get(discount_tag) + .unwrap(); + let usage = status_bin.discount_usage_map.get_mut(discount_tag).unwrap(); + if *usage < discount_policy.use_limit { + *usage += 1; + } + } + } + + let mut gain_item_list: Vec = vec![]; + while pull_count > 0 { + let pull_result = gacha_model.perform_pull_pool(&pull_time, target_pool); + let extra_item_bin = pull_result.extra_item_bin.unwrap(); + gain_item_list.push(GainItemInfo { + item_id: pull_result.obtained_item_id, + extra_item_id: extra_item_bin.extra_item_id, + extra_item_count: extra_item_bin.extra_item_count, + num: 1, + ..GainItemInfo::default() + }); + pull_count -= 1; + } + + Ok(DoGachaScRsp { retcode: Retcode::RetSucc.into(), - gacha_type: req.gacha_type, - gacha_data: Some(GachaData::default()), + gain_item_list, + gacha_data: Some(generate_all_gacha_info(_player)), + cost_item_count: req.cost_item_count, }) } + +fn generate_gacha_info_from_pool( + gacha_bin: &GachaModelBin, + target_pool: &CharacterGachaPool, + common_properties: &GachaCommonProperties, +) -> Gacha { + let gachaconf = data::gacha::global_gacha_config(); + let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category; + let status_bin = gacha_bin + .gacha_status_map + .get(sharing_guarantee_category_tag) + .unwrap(); + let pity_s = status_bin + .rarity_status_map + .get(&common_properties.s_item_rarity) + .unwrap() + .pity; + let pity_a = status_bin + .rarity_status_map + .get(&common_properties.a_item_rarity) + .unwrap() + .pity; + let mut up_s_item_list: Vec = vec![]; + let mut up_a_item_list: Vec = vec![]; + let mut s_guarantee: u32 = 0; + let mut a_guarantee: u32 = 0; + for rarity_items in target_pool.gacha_items.iter() { + for category in rarity_items.categories.values() { + let probability_model = gachaconf + .probability_model_map + .get(&rarity_items.probability_model_tag) + .unwrap(); + if rarity_items.rarity == common_properties.s_item_rarity { + if category.is_promotional_items { + up_s_item_list = category.item_ids.clone(); + } + s_guarantee = probability_model.maximum_guarantee_pity - pity_s + 1; + } + if rarity_items.rarity == common_properties.a_item_rarity { + if category.is_promotional_items { + up_a_item_list = category.item_ids.clone(); + } + a_guarantee = probability_model.maximum_guarantee_pity - pity_a + 1; + } + } + } + + let mut discount_ten_roll_prize: u32 = 0; + let mut discount_avaliable_num: u32 = 0; + let mut advanced_s_guarantee: u32 = 0; + for discount_policy_tag in target_pool.discount_policy_tags.iter() { + if common_properties.newcomer_advanced_s_tag == *discount_policy_tag { + let policy = gachaconf + .discount_policies + .advanced_guarantee_map + .get(discount_policy_tag) + .unwrap(); + if status_bin + .discount_usage_map + .get(discount_policy_tag) + .unwrap() + < &policy.use_limit + { + advanced_s_guarantee = policy.guarantee_pity - pity_s + 1; + } + } else if common_properties.ten_pull_discount_tag == *discount_policy_tag { + let policy = gachaconf + .discount_policies + .ten_pull_discount_map + .get(discount_policy_tag) + .unwrap(); + let discount_usage = status_bin + .discount_usage_map + .get(discount_policy_tag) + .unwrap(); + if discount_usage < &policy.use_limit { + discount_ten_roll_prize = policy.discounted_prize; + discount_avaliable_num = policy.use_limit - discount_usage; + } + } + } + + let need_item_info_list: Vec = vec![NeedItemInfo { + need_item_id: target_pool.cost_item_id, + need_item_count: 1, + }]; + + Gacha { + gacha_schedule_id: target_pool.gacha_schedule_id, + gacha_parent_schedule_id: target_pool.gacha_parent_schedule_id, + gacha_type: target_pool.gacha_type, + start_timestamp: target_pool.start_time.timestamp(), + end_timestamp: target_pool.end_time.timestamp(), + discount_avaliable_num, + discount_ten_roll_prize, + up_s_item_list, + up_a_item_list, + advanced_s_guarantee, + s_guarantee, + a_guarantee, + need_item_info_list, + // iehkehofjop: target_pool.gacha_parent_schedule_id, + // eggcehlgkii: 223, + // ijoahiepmfo: 101, + ..Gacha::default() + } +} + +fn generate_all_gacha_info(_player: &Player) -> GachaData { + let gachaconf = global_gacha_config(); + let gacha_bin = &_player.gacha_model.gacha_bin; + let mut gacha_list: Vec = vec![]; + for target_pool in gachaconf.character_gacha_pool_list.iter() { + gacha_list.push(generate_gacha_info_from_pool( + &gacha_bin, + target_pool, + &gachaconf.common_properties, + )); + } + GachaData { + random_number: 0, + gacha_pool: Some(GachaPool { gacha_list }), + ..GachaData::default() + } +} + +fn get_gacha_pool<'conf>( + character_gacha_pool_list: &'conf Vec, + gacha_parent_schedule_id: &u32, + pull_time: &DateTime, +) -> Option<&'conf CharacterGachaPool> { + for target_pool in character_gacha_pool_list.iter() { + if &target_pool.gacha_parent_schedule_id == gacha_parent_schedule_id + && target_pool.is_still_open(pull_time) + { + return Some(target_pool); + } + } + None +} diff --git a/nap_gameserver/src/handlers/mod.rs b/nap_gameserver/src/handlers/mod.rs index e791399..4e83d49 100644 --- a/nap_gameserver/src/handlers/mod.rs +++ b/nap_gameserver/src/handlers/mod.rs @@ -119,6 +119,7 @@ req_handlers! { event_graph::RunEventGraph; quest::BeginArchiveBattleQuest; quest::FinishArchiveQuest; + gacha::DoGacha; } notify_handlers! { diff --git a/nap_gameserver/src/logic/gacha/gacha_model.rs b/nap_gameserver/src/logic/gacha/gacha_model.rs index 0798875..510c5db 100644 --- a/nap_gameserver/src/logic/gacha/gacha_model.rs +++ b/nap_gameserver/src/logic/gacha/gacha_model.rs @@ -1,4 +1,11 @@ -use proto::GachaModelBin; +use data::gacha; +use data::gacha::gacha_config::*; +use proto::*; + +use chrono::{DateTime, Local}; +use rand::{thread_rng, Rng}; +use std::collections::{HashMap, HashSet}; +use std::hash::{BuildHasher, Hash}; #[derive(Default)] pub struct GachaModel { @@ -7,12 +14,377 @@ pub struct GachaModel { impl GachaModel { pub fn from_bin(gacha_bin: GachaModelBin) -> Self { - Self { - gacha_bin - } + let result = Self { gacha_bin }; + result.post_deserialize() } pub fn to_bin(&self) -> GachaModelBin { self.gacha_bin.clone() } -} \ No newline at end of file + + pub fn post_deserialize(mut self) -> GachaModel { + let gachaconf = gacha::global_gacha_config(); + for gacha_pool in gachaconf.character_gacha_pool_list.iter() { + let gacha_comp_bin = &mut self.gacha_bin; + let mut gacha_status_map = &mut gacha_comp_bin.gacha_status_map; + let status_bin = get_or_add( + &mut gacha_status_map, + &gacha_pool.sharing_guarantee_info_category, + ); + for rarity_items in gacha_pool.gacha_items.iter() { + let progress_bin = + get_or_add(&mut status_bin.rarity_status_map, &rarity_items.rarity); + if progress_bin.pity <= 0 { + progress_bin.pity = 1; + } + for category_guarantee_policy_tag in + rarity_items.category_guarantee_policy_tags.iter() + { + get_or_add( + &mut progress_bin.categories_progress_map, + &category_guarantee_policy_tag, + ); + } + } + for discount_policy_tag in gacha_pool.discount_policy_tags.iter() { + get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag); + } + } + self + } + + pub fn perform_pull_pool<'bin, 'conf>( + &'bin mut self, + pull_time: &DateTime, + target_pool: &'conf CharacterGachaPool, + ) -> GachaRecordBin { + let mut gacha_bin = &mut self.gacha_bin; + let (rarity_items, progress_bin, status_bin, probability_model) = + determine_rarity(&gacha_bin, target_pool); + let (category_tag, category) = determine_category(rarity_items, progress_bin, target_pool); + let result = + determine_gacha_result(pull_time, category, target_pool, status_bin, rarity_items); + update_pity(&mut gacha_bin, rarity_items, probability_model, target_pool); + update_category_guarantee_info(&mut gacha_bin, rarity_items, &category_tag, target_pool); + update_discount(&mut gacha_bin, target_pool, &category_tag, rarity_items); + result + } +} + +fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>( + map: &'a mut HashMap, + key: &K, +) -> &'a mut V { + if !map.contains_key(key) { + map.insert(key.clone(), V::default()); + } + map.get_mut(key).unwrap() +} + +fn rand_rarity<'bin, 'conf>( + target_pool: &'conf CharacterGachaPool, + status_bin: &'bin GachaStatusBin, +) -> ( + &'conf GachaAvailableItemsInfo, + &'bin GachaProgressBin, + &'conf ProbabilityModel, +) { + let gachaconf = gacha::global_gacha_config(); + let mut rng = thread_rng(); + let rarity_status_map = &status_bin.rarity_status_map; + // gacha_items is already sorted by rarity descendingly in its post_configure. + for rarity_items in target_pool.gacha_items.iter() { + // Surely any judgement should be made on the current pity. + let progress_bin = rarity_status_map.get(&rarity_items.rarity).unwrap(); + let pity = progress_bin.pity; + let probability_model = gachaconf + .probability_model_map + .get(&rarity_items.probability_model_tag) + .unwrap(); + if rng.gen_range(0.0..100.0) <= probability_model.get_chance_percent(&pity) { + return (rarity_items, progress_bin, probability_model); + } + } + panic!("The user failed to get any items."); +} + +fn determine_rarity<'bin, 'conf>( + gacha_bin: &'bin GachaModelBin, + target_pool: &'conf CharacterGachaPool, +) -> ( + &'conf GachaAvailableItemsInfo, + &'bin GachaProgressBin, + &'bin GachaStatusBin, + &'conf ProbabilityModel, +) { + let gachaconf = gacha::global_gacha_config(); + let status_bin = gacha_bin + .gacha_status_map + .get(&target_pool.sharing_guarantee_info_category) + .expect(&format!( + "post_deserialize forgot StatusBin/sharing_guarantee_info_category: {}", + target_pool.sharing_guarantee_info_category + )); + let (mut rarity_items, mut progress_bin, mut probability_model) = + rand_rarity(target_pool, &status_bin); + + // We should take AdvancedGuarantee discount into consideration. + for discount_tag in target_pool.discount_policy_tags.iter() { + if let Some(discount) = gachaconf + .discount_policies + .advanced_guarantee_map + .get(discount_tag) + { + if discount.rarity <= rarity_items.rarity { + continue; + } + if status_bin + .discount_usage_map + .get(discount_tag) + .expect(&format!( + "post_deserialize forgot StatusBin/discount_usage_map: {}", + discount_tag + )) + >= &discount.use_limit + { + continue; + } + let higher_progress_bin = + status_bin + .rarity_status_map + .get(&discount.rarity) + .expect(&format!( + "post_deserialize forgot StatusBin/rarity_status_map: {}", + &discount.rarity + )); + if higher_progress_bin.pity >= discount.guarantee_pity { + let mut found_rarity_items = false; + for gacha_items in target_pool.gacha_items.iter() { + if gacha_items.rarity == discount.rarity { + rarity_items = gacha_items; + probability_model = gachaconf + .probability_model_map + .get(&gacha_items.probability_model_tag) + .unwrap(); + found_rarity_items = true; + break; + } + } + assert!(found_rarity_items, "Handle AdvancedGuarantee Discount ({discount_tag}) error: The target rarity does not exist in this pool."); + progress_bin = higher_progress_bin; + } + } + } + + (rarity_items, progress_bin, status_bin, probability_model) +} + +fn determine_category<'bin, 'conf>( + rarity_items: &'conf GachaAvailableItemsInfo, + progress_bin: &'bin GachaProgressBin, + target_pool: &'conf CharacterGachaPool, +) -> (String, &'conf GachaCategoryInfo) { + let gachaconf = gacha::global_gacha_config(); + let mut category_tag_inited = false; + let mut category_tag_result: HashSet = HashSet::new(); + // First of all, if there's a chooseable category and + // it's can be triggered, then we MUST give that + // category's item. + // TODO: Only Genshin can do + // Then we should take a look at MustGainItem. + if !category_tag_inited { + for discount_policy_tag in target_pool.discount_policy_tags.iter() { + if let Some(discount) = gachaconf + .discount_policies + .must_gain_item_map + .get(discount_policy_tag) + { + if discount.rarity != rarity_items.rarity { + continue; + } + category_tag_result.insert(discount.category_tag.clone()); + category_tag_inited = true; + } + } + } + // Otherwise, just select as normal. + if !category_tag_inited { + for tag in rarity_items.categories.keys() { + category_tag_result.insert(tag.clone()); + } + for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { + let category_guarantee_policy = gachaconf + .category_guarantee_policy_map + .get(guarantee_policy_tag) + .unwrap(); + let failure_times = progress_bin.categories_progress_map + .get(guarantee_policy_tag) + .expect(&format!("post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", &rarity_items.rarity, guarantee_policy_tag)); + if failure_times >= &category_guarantee_policy.trigger_on_failure_times { + category_tag_result = category_tag_result + .intersection(&category_guarantee_policy.included_category_tags) + .cloned() + .collect(); + } + } + // category_tag_inited = true; + } + + let mut categories: Vec<(String, &GachaCategoryInfo)> = vec![]; + let mut weight_sum = 0; + for result_tag in category_tag_result { + let category = rarity_items.categories.get(&result_tag).unwrap(); + categories.push((result_tag, category)); + weight_sum += category.category_weight; + } + + let randomnum = rand::thread_rng().gen_range(0..weight_sum); + let mut enumerated_ranges_end = 0; + for category in categories.into_iter() { + if randomnum <= enumerated_ranges_end + category.1.category_weight { + return (category.0, category.1); + } + enumerated_ranges_end += category.1.category_weight; + } + panic!("No category is chosen."); +} + +fn determine_gacha_result<'bin, 'conf>( + pull_time: &DateTime, + category: &'conf GachaCategoryInfo, + target_pool: &'conf CharacterGachaPool, + status_bin: &'bin GachaStatusBin, + rarity_items: &'conf GachaAvailableItemsInfo, +) -> GachaRecordBin { + let gachaconf = gacha::global_gacha_config(); + let item_pool_len = category.item_ids.len() as u32; + let item_id = category + .item_ids + .get(rand::thread_rng().gen_range(0..item_pool_len) as usize) + .unwrap(); + let mut extra_item_id: u32 = 0; + let mut extra_item_count: u32 = 0; + + for extra_items_policy_tag in rarity_items.extra_items_policy_tags.iter() { + let extra_items_policy = gachaconf + .extra_items_policy_map + .get(extra_items_policy_tag) + .unwrap(); + // TODO: apply_on_owned_count in a context with bag + if extra_items_policy.apply_on_owned_count == 0 { + extra_item_id = extra_items_policy.id; + extra_item_count = extra_items_policy.count; + } + } + let extra_item_bin = GachaExtraItemBin { + extra_item_id, + extra_item_count, + currently_gained: 0, + }; + GachaRecordBin { + pull_timestamp: pull_time.timestamp(), + obtained_item_id: item_id.clone(), + gacha_id: target_pool.gacha_schedule_id.clone(), + progress_map: status_bin.rarity_status_map.clone(), + extra_item_bin: Some(extra_item_bin), + } +} + +fn update_pity<'bin, 'conf>( + gacha_bin: &'bin mut GachaModelBin, + rarity_items: &'conf GachaAvailableItemsInfo, + probability_model: &'conf ProbabilityModel, + target_pool: &'conf CharacterGachaPool, +) { + let status_bin = gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap(); + for (rarity, rarity_status) in status_bin.rarity_status_map.iter_mut() { + if (rarity == &rarity_items.rarity) + || (probability_model.clear_status_on_higher_rarity_pulled + && rarity < &rarity_items.rarity) + { + rarity_status.pity = 1; + } else { + rarity_status.pity += 1; + } + } +} + +fn update_category_guarantee_info<'bin, 'conf>( + gacha_bin: &'bin mut GachaModelBin, + rarity_items: &'conf GachaAvailableItemsInfo, + category_tag: &String, + target_pool: &'conf CharacterGachaPool, +) { + let gachaconf = gacha::global_gacha_config(); + let status_bin = gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap(); + let progress_bin = status_bin + .rarity_status_map + .get_mut(&rarity_items.rarity) + .unwrap(); + for policy_tag in rarity_items.category_guarantee_policy_tags.iter() { + let policy = gachaconf + .category_guarantee_policy_map + .get(policy_tag) + .unwrap(); + // TODO: Chooseable guarantee not implemented + let prev_failure = progress_bin + .categories_progress_map + .get_mut(policy_tag) + .expect(&format!( + "post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", + rarity_items.rarity, policy_tag + )); + if policy.included_category_tags.contains(category_tag) { + *prev_failure = 0; + } else { + *prev_failure += 1; + } + } +} + +fn update_discount<'bin, 'conf>( + gacha_bin: &'bin mut GachaModelBin, + target_pool: &'conf CharacterGachaPool, + category_tag: &String, + rarity_items: &GachaAvailableItemsInfo, +) { + let gachaconf = gacha::global_gacha_config(); + for (policy_tag, policy) in gachaconf.discount_policies.must_gain_item_map.iter() { + if *category_tag != policy.category_tag { + continue; + } + if !target_pool.discount_policy_tags.contains(policy_tag) { + continue; + } + let status_bin = gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap(); + let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap(); + if *usage < policy.use_limit { + *usage += 1; + } + } + for (policy_tag, policy) in gachaconf.discount_policies.advanced_guarantee_map.iter() { + if rarity_items.rarity != policy.rarity { + continue; + } + if !target_pool.discount_policy_tags.contains(policy_tag) { + continue; + } + let status_bin = gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap(); + let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap(); + if *usage < policy.use_limit { + *usage += 1; + } + } +} -- 2.34.1 From 157832198c3ea8af537cd49d48ffba7033b961b6 Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:37:13 +0800 Subject: [PATCH 4/7] Gacha Model bugfix: post_configure on creating --- nap_gameserver/src/logic/gacha/gacha_model.rs | 10 +++++++++- nap_gameserver/src/logic/player/player.rs | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/nap_gameserver/src/logic/gacha/gacha_model.rs b/nap_gameserver/src/logic/gacha/gacha_model.rs index 510c5db..75a457a 100644 --- a/nap_gameserver/src/logic/gacha/gacha_model.rs +++ b/nap_gameserver/src/logic/gacha/gacha_model.rs @@ -7,11 +7,19 @@ use rand::{thread_rng, Rng}; use std::collections::{HashMap, HashSet}; use std::hash::{BuildHasher, Hash}; -#[derive(Default)] pub struct GachaModel { pub gacha_bin: GachaModelBin, } +impl Default for GachaModel { + fn default() -> GachaModel { + let result = GachaModel { + gacha_bin: GachaModelBin::default(), + }; + result.post_deserialize() + } +} + impl GachaModel { pub fn from_bin(gacha_bin: GachaModelBin) -> Self { let result = Self { gacha_bin }; diff --git a/nap_gameserver/src/logic/player/player.rs b/nap_gameserver/src/logic/player/player.rs index 294444e..ff85a0f 100644 --- a/nap_gameserver/src/logic/player/player.rs +++ b/nap_gameserver/src/logic/player/player.rs @@ -62,6 +62,12 @@ impl Player { .add_resource(ItemStatic::FrontendGold as u32, 1_000_000); self.item_model .add_resource(ItemStatic::GameDiamond as u32, 1_000_000); + self.item_model + .add_resource(ItemStatic::GachaTicketEvent as u32, 30_000); + self.item_model + .add_resource(ItemStatic::GachaTicketStandard as u32, 30_000); + self.item_model + .add_resource(ItemStatic::GachaTicketBangboo as u32, 30_000); self.item_model.add_resource(ItemStatic::Energy as u32, 240); } -- 2.34.1 From 950a3725ea076dae0940fc02c21d8cdf2b14e9b5 Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:41:20 +0800 Subject: [PATCH 5/7] command: player kick [uid] [reason] --- nap_gameserver/src/commands/mod.rs | 1 + nap_gameserver/src/commands/player.rs | 43 +++++++++++++- nap_gameserver/src/net/session.rs | 4 ++ nap_proto/out/_.rs | 86 +++++++++++++++------------ 4 files changed, 96 insertions(+), 38 deletions(-) diff --git a/nap_gameserver/src/commands/mod.rs b/nap_gameserver/src/commands/mod.rs index 8480297..d976fe3 100644 --- a/nap_gameserver/src/commands/mod.rs +++ b/nap_gameserver/src/commands/mod.rs @@ -87,5 +87,6 @@ impl CommandManager { player::procedure "[player_uid] [procedure_id]" "changes current beginner procedure id, parameter -1 can be used for skipping it"; avatar::add "[player_uid] [avatar_id]" "gives avatar with specified id to player"; item::add_weapon "[player_uid] [weapon_id]" "gives weapon with specified id to player"; + player::kick "[player_uid] [reason]" "kick the specified player (reason is optional)"; } } diff --git a/nap_gameserver/src/commands/player.rs b/nap_gameserver/src/commands/player.rs index 0aeaea2..e0e00fe 100644 --- a/nap_gameserver/src/commands/player.rs +++ b/nap_gameserver/src/commands/player.rs @@ -1,5 +1,5 @@ use data::tables::{AvatarBaseID, ProcedureConfigID}; -use proto::PlayerSyncScNotify; +use proto::{DisconnectReason, DisconnectScNotify, PlayerSyncScNotify}; use crate::ServerState; @@ -126,3 +126,44 @@ pub async fn procedure( "successfully changed procedure_id to {procedure_id:?}" )) } + +pub async fn kick( + args: ArgSlice<'_>, + state: &ServerState, +) -> Result> { + const USAGE: &str = "Usage: player kick [player_uid]"; + + if args.len() > 2 { + return Ok(USAGE.to_string()); + } + + let uid = args[0].parse::()?; + let reason = match args.get(1) { + Some(arg) => match arg.parse::() { + Ok(val) => val, + Err(_err) => 1, + }, + None => 1, + }; + let reason_str = match DisconnectReason::try_from(reason) { + Ok(converted_enum) => converted_enum.as_str_name().to_owned(), + Err(_err) => reason.to_string(), + }; + + let Some(player_lock) = state.player_mgr.get_player(uid).await else { + return Ok(String::from("player not found")); + }; + + let session_id = player_lock.lock().await.current_session_id(); + + if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() { + session + .notify(DisconnectScNotify { reason: reason }) + .await?; + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + session.shutdown().await?; + Ok(format!("kicked player, uid: {uid}, reason: {reason_str}")) + } else { + Ok(format!("player uid: {uid} is not online yet.")) + } +} diff --git a/nap_gameserver/src/net/session.rs b/nap_gameserver/src/net/session.rs index f3457f6..b749992 100644 --- a/nap_gameserver/src/net/session.rs +++ b/nap_gameserver/src/net/session.rs @@ -247,4 +247,8 @@ impl NetSession { pub fn set_state(&self, state: NetSessionState) { self.state.store(state, std::sync::atomic::Ordering::SeqCst); } + + pub async fn shutdown(&self) -> Result<(), std::io::Error> { + self.writer.lock().await.shutdown().await + } } diff --git a/nap_proto/out/_.rs b/nap_proto/out/_.rs index eb6ccfb..447fe85 100644 --- a/nap_proto/out/_.rs +++ b/nap_proto/out/_.rs @@ -28249,8 +28249,8 @@ pub struct Flpgnabkedc { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Igapopdfcef { - #[prost(enumeration = "Obpfhejepck", tag = "15")] +pub struct DisconnectScNotify { + #[prost(enumeration = "DisconnectReason", tag = "15")] pub reason: i32, } #[derive(proto_gen::CmdID)] @@ -54259,9 +54259,9 @@ pub enum ItemStatic { FrontendGold = 10, GameDiamond = 100, RechargeDiamond = 101, - Lcdlfoheaim = 110, - Mndhfchkhcl = 111, - Fcffdnjnemf = 112, + GachaTicketStandard = 110, + GachaTicketEvent = 111, + GachaTicketBangboo = 112, Exp = 201, Energy = 501, Gdglcfmgoji = 3000001, @@ -54280,9 +54280,9 @@ impl ItemStatic { ItemStatic::FrontendGold => "ITEM_STATIC_FRONTEND_GOLD", ItemStatic::GameDiamond => "ITEM_STATIC_GAME_DIAMOND", ItemStatic::RechargeDiamond => "ITEM_STATIC_RECHARGE_DIAMOND", - ItemStatic::Lcdlfoheaim => "ItemStatic_LCDLFOHEAIM", - ItemStatic::Mndhfchkhcl => "ItemStatic_MNDHFCHKHCL", - ItemStatic::Fcffdnjnemf => "ItemStatic_FCFFDNJNEMF", + ItemStatic::GachaTicketStandard => "ItemStatic_GACHA_TICKET_STANDARD", + ItemStatic::GachaTicketEvent => "ItemStatic_GACHA_TICKET_EVENT", + ItemStatic::GachaTicketBangboo => "ItemStatic_GACHA_TICKET_BANGBOO", ItemStatic::Exp => "ITEM_STATIC_EXP", ItemStatic::Energy => "ITEM_STATIC_ENERGY", ItemStatic::Gdglcfmgoji => "ItemStatic_GDGLCFMGOJI", @@ -54298,9 +54298,9 @@ impl ItemStatic { "ITEM_STATIC_FRONTEND_GOLD" => Some(Self::FrontendGold), "ITEM_STATIC_GAME_DIAMOND" => Some(Self::GameDiamond), "ITEM_STATIC_RECHARGE_DIAMOND" => Some(Self::RechargeDiamond), - "ItemStatic_LCDLFOHEAIM" => Some(Self::Lcdlfoheaim), - "ItemStatic_MNDHFCHKHCL" => Some(Self::Mndhfchkhcl), - "ItemStatic_FCFFDNJNEMF" => Some(Self::Fcffdnjnemf), + "ItemStatic_GACHA_TICKET_STANDARD" => Some(Self::GachaTicketStandard), + "ItemStatic_GACHA_TICKET_EVENT" => Some(Self::GachaTicketEvent), + "ItemStatic_GACHA_TICKET_BANGBOO" => Some(Self::GachaTicketBangboo), "ITEM_STATIC_EXP" => Some(Self::Exp), "ITEM_STATIC_ENERGY" => Some(Self::Energy), "ItemStatic_GDGLCFMGOJI" => Some(Self::Gdglcfmgoji), @@ -55320,50 +55320,62 @@ impl Jiedoddkipa { #[derive(proto_gen::XorFields)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] -pub enum Obpfhejepck { +pub enum DisconnectReason { Domjllafeac = 0, - Gdbjccehcdb = 1, + /// 您的账号在其他地方登录,请检查是否存在账号泄露风险,是否要重新登录游戏? + ServerRelogin = 1, Clcbejonokn = 2, - Jlimlghggke = 3, - Omjjggajcbh = 4, - Kdpiceebneb = 5, + /// 游戏正在维护中,请等维护完成后再尝试登录。 + ServerShutdown = 3, + /// UID:"3" + /// 您已掉线,请返回登录界面重试 + ServerKick = 4, + /// 登录状态已失效,请重新登录 + AccountPasswordChange = 5, + /// 您已断开链接,请重新登录 Nmcpihldicf = 6, + /// 您的卡死问题已处理完成,请重新登录游戏 Ihcmpplkgkn = 7, + /// 您的PSN™的好友关系已解绑,现在可自由处理游戏内好友关系 Lgjemnlahcp = 8, Mfgomdhiejh = 9, } -impl Obpfhejepck { +impl DisconnectReason { /// String value of the enum field names used in the ProtoBuf definition. /// /// The values are not transformed in any way and thus are considered stable /// (if the ProtoBuf definition does not change) and safe for programmatic use. pub fn as_str_name(&self) -> &'static str { match self { - Obpfhejepck::Domjllafeac => "OBPFHEJEPCK_DOMJLLAFEAC", - Obpfhejepck::Gdbjccehcdb => "OBPFHEJEPCK_GDBJCCEHCDB", - Obpfhejepck::Clcbejonokn => "OBPFHEJEPCK_CLCBEJONOKN", - Obpfhejepck::Jlimlghggke => "OBPFHEJEPCK_JLIMLGHGGKE", - Obpfhejepck::Omjjggajcbh => "OBPFHEJEPCK_OMJJGGAJCBH", - Obpfhejepck::Kdpiceebneb => "OBPFHEJEPCK_KDPICEEBNEB", - Obpfhejepck::Nmcpihldicf => "OBPFHEJEPCK_NMCPIHLDICF", - Obpfhejepck::Ihcmpplkgkn => "OBPFHEJEPCK_IHCMPPLKGKN", - Obpfhejepck::Lgjemnlahcp => "OBPFHEJEPCK_LGJEMNLAHCP", - Obpfhejepck::Mfgomdhiejh => "OBPFHEJEPCK_MFGOMDHIEJH", + DisconnectReason::Domjllafeac => "DISCONNECT_REASON_DOMJLLAFEAC", + DisconnectReason::ServerRelogin => "DISCONNECT_REASON_SERVER_RELOGIN", + DisconnectReason::Clcbejonokn => "DISCONNECT_REASON_CLCBEJONOKN", + DisconnectReason::ServerShutdown => "DISCONNECT_REASON_SERVER_SHUTDOWN", + DisconnectReason::ServerKick => "DISCONNECT_REASON_SERVER_KICK", + DisconnectReason::AccountPasswordChange => { + "DISCONNECT_REASON_ACCOUNT_PASSWORD_CHANGE" + } + DisconnectReason::Nmcpihldicf => "DISCONNECT_REASON_NMCPIHLDICF", + DisconnectReason::Ihcmpplkgkn => "DISCONNECT_REASON_IHCMPPLKGKN", + DisconnectReason::Lgjemnlahcp => "DISCONNECT_REASON_LGJEMNLAHCP", + DisconnectReason::Mfgomdhiejh => "DISCONNECT_REASON_MFGOMDHIEJH", } } /// Creates an enum from field names used in the ProtoBuf definition. pub fn from_str_name(value: &str) -> ::core::option::Option { match value { - "OBPFHEJEPCK_DOMJLLAFEAC" => Some(Self::Domjllafeac), - "OBPFHEJEPCK_GDBJCCEHCDB" => Some(Self::Gdbjccehcdb), - "OBPFHEJEPCK_CLCBEJONOKN" => Some(Self::Clcbejonokn), - "OBPFHEJEPCK_JLIMLGHGGKE" => Some(Self::Jlimlghggke), - "OBPFHEJEPCK_OMJJGGAJCBH" => Some(Self::Omjjggajcbh), - "OBPFHEJEPCK_KDPICEEBNEB" => Some(Self::Kdpiceebneb), - "OBPFHEJEPCK_NMCPIHLDICF" => Some(Self::Nmcpihldicf), - "OBPFHEJEPCK_IHCMPPLKGKN" => Some(Self::Ihcmpplkgkn), - "OBPFHEJEPCK_LGJEMNLAHCP" => Some(Self::Lgjemnlahcp), - "OBPFHEJEPCK_MFGOMDHIEJH" => Some(Self::Mfgomdhiejh), + "DISCONNECT_REASON_DOMJLLAFEAC" => Some(Self::Domjllafeac), + "DISCONNECT_REASON_SERVER_RELOGIN" => Some(Self::ServerRelogin), + "DISCONNECT_REASON_CLCBEJONOKN" => Some(Self::Clcbejonokn), + "DISCONNECT_REASON_SERVER_SHUTDOWN" => Some(Self::ServerShutdown), + "DISCONNECT_REASON_SERVER_KICK" => Some(Self::ServerKick), + "DISCONNECT_REASON_ACCOUNT_PASSWORD_CHANGE" => { + Some(Self::AccountPasswordChange) + } + "DISCONNECT_REASON_NMCPIHLDICF" => Some(Self::Nmcpihldicf), + "DISCONNECT_REASON_IHCMPPLKGKN" => Some(Self::Ihcmpplkgkn), + "DISCONNECT_REASON_LGJEMNLAHCP" => Some(Self::Lgjemnlahcp), + "DISCONNECT_REASON_MFGOMDHIEJH" => Some(Self::Mfgomdhiejh), _ => None, } } -- 2.34.1 From 05e8ef740d7e24356788e6b9afe1d9dd7d045e24 Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:33:11 +0800 Subject: [PATCH 6/7] Make Gacha interact with Item & Role Model --- nap_data/src/gacha/gacha_config.rs | 18 +++++- nap_gameserver/src/commands/player.rs | 5 +- nap_gameserver/src/handlers/gacha.rs | 56 ++++++++++++++++--- nap_gameserver/src/logic/gacha/gacha_model.rs | 1 + nap_proto/out/bin.rs | 34 +++++++++++ 5 files changed, 104 insertions(+), 10 deletions(-) diff --git a/nap_data/src/gacha/gacha_config.rs b/nap_data/src/gacha/gacha_config.rs index fa777ed..c539ac3 100644 --- a/nap_data/src/gacha/gacha_config.rs +++ b/nap_data/src/gacha/gacha_config.rs @@ -1,7 +1,8 @@ use std::collections::{HashMap, HashSet}; use chrono::{prelude::Local, DateTime}; -use serde::Deserialize; +use proto::GachaAddedItemType; +use serde::{Deserialize, Deserializer}; use tracing; #[derive(Debug, Default, Deserialize)] @@ -147,6 +148,21 @@ pub struct GachaCategoryInfo { pub is_promotional_items: bool, pub item_ids: Vec, pub category_weight: u32, + #[serde(default, deserialize_with = "from_str")] + pub item_type: GachaAddedItemType, +} + +pub fn from_str<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + + let result = GachaAddedItemType::from_str_name(&s); + match result { + Some(val) => Ok(val), + None => Ok(GachaAddedItemType::None) + } } #[derive(Debug, Default, Deserialize)] diff --git a/nap_gameserver/src/commands/player.rs b/nap_gameserver/src/commands/player.rs index e0e00fe..1a5aa98 100644 --- a/nap_gameserver/src/commands/player.rs +++ b/nap_gameserver/src/commands/player.rs @@ -138,12 +138,13 @@ pub async fn kick( } let uid = args[0].parse::()?; + let default_reason = DisconnectReason::ServerKick.into(); let reason = match args.get(1) { Some(arg) => match arg.parse::() { Ok(val) => val, - Err(_err) => 1, + Err(_err) => default_reason, }, - None => 1, + None => default_reason, }; let reason_str = match DisconnectReason::try_from(reason) { Ok(converted_enum) => converted_enum.as_str_name().to_owned(), diff --git a/nap_gameserver/src/handlers/gacha.rs b/nap_gameserver/src/handlers/gacha.rs index dcc8de4..31fd93a 100644 --- a/nap_gameserver/src/handlers/gacha.rs +++ b/nap_gameserver/src/handlers/gacha.rs @@ -1,8 +1,11 @@ -use data::gacha::{gacha_config::*, global_gacha_config}; +use data::{ + gacha::{gacha_config::*, global_gacha_config}, + tables::{AvatarBaseID, WeaponID}, +}; -use proto::*; use super::*; use chrono::{DateTime, Local}; +use proto::*; pub async fn on_get_gacha_data( _session: &NetSession, @@ -33,6 +36,8 @@ pub async fn on_do_gacha( ) -> NetResult { let gachaconf = global_gacha_config(); let gacha_model = &mut _player.gacha_model; + let item_model = &mut _player.item_model; + let role_model = &mut _player.role_model; let pull_time = Local::now(); let target_pool = get_gacha_pool( &gachaconf.character_gacha_pool_list, @@ -47,9 +52,9 @@ pub async fn on_do_gacha( }; let target_pool = target_pool.unwrap(); - // TODO: Validate cost_item_count - // tracing::info!("cost_item_count: {}", body.cost_item_count); + // tracing::info!("cost_item_count: {}", req.cost_item_count); let mut pull_count = if req.cost_item_count > 1 { 10 } else { 1 }; + let mut cost_count = pull_count; if pull_count == 10 { let discount_tag = &gachaconf.common_properties.ten_pull_discount_tag; @@ -67,18 +72,53 @@ pub async fn on_do_gacha( let usage = status_bin.discount_usage_map.get_mut(discount_tag).unwrap(); if *usage < discount_policy.use_limit { *usage += 1; + cost_count = discount_policy.discounted_prize; } } } + if cost_count != req.cost_item_count { + return Ok(DoGachaScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + } else { + // TODO: cost resource + } let mut gain_item_list: Vec = vec![]; while pull_count > 0 { let pull_result = gacha_model.perform_pull_pool(&pull_time, target_pool); let extra_item_bin = pull_result.extra_item_bin.unwrap(); + let uid = match GachaAddedItemType::try_from(pull_result.item_type) { + Ok(enum_val) => match enum_val { + GachaAddedItemType::Weapon => match WeaponID::new(pull_result.obtained_item_id) { + Some(id) => item_model.add_weapon(id).value(), + None => 0, + }, + GachaAddedItemType::Character => { + match AvatarBaseID::new(pull_result.obtained_item_id) { + Some(id) => { + role_model.add_avatar(id); + 0 + } + None => 0, + } + } + _ => 0, + }, + Err(_err) => 0, + }; + if extra_item_bin.extra_item_id != 0 { + item_model.add_resource( + extra_item_bin.extra_item_id, + extra_item_bin.extra_item_count, + ); + } gain_item_list.push(GainItemInfo { item_id: pull_result.obtained_item_id, extra_item_id: extra_item_bin.extra_item_id, extra_item_count: extra_item_bin.extra_item_count, + uid, num: 1, ..GainItemInfo::default() }); @@ -193,9 +233,9 @@ fn generate_gacha_info_from_pool( s_guarantee, a_guarantee, need_item_info_list, - // iehkehofjop: target_pool.gacha_parent_schedule_id, - // eggcehlgkii: 223, - // ijoahiepmfo: 101, + lpklhoobkbh: target_pool.gacha_parent_schedule_id, + nammdglepbk: 593, + hgmcofcjmbg: 101, ..Gacha::default() } } @@ -211,6 +251,8 @@ fn generate_all_gacha_info(_player: &Player) -> GachaData { &gachaconf.common_properties, )); } + + // tracing::info!("gacha_list: {:?}", gacha_list); GachaData { random_number: 0, gacha_pool: Some(GachaPool { gacha_list }), diff --git a/nap_gameserver/src/logic/gacha/gacha_model.rs b/nap_gameserver/src/logic/gacha/gacha_model.rs index 75a457a..5f70037 100644 --- a/nap_gameserver/src/logic/gacha/gacha_model.rs +++ b/nap_gameserver/src/logic/gacha/gacha_model.rs @@ -295,6 +295,7 @@ fn determine_gacha_result<'bin, 'conf>( gacha_id: target_pool.gacha_schedule_id.clone(), progress_map: status_bin.rarity_status_map.clone(), extra_item_bin: Some(extra_item_bin), + item_type: category.item_type.into(), } } diff --git a/nap_proto/out/bin.rs b/nap_proto/out/bin.rs index 7f93d83..01bb4aa 100644 --- a/nap_proto/out/bin.rs +++ b/nap_proto/out/bin.rs @@ -162,6 +162,8 @@ pub struct GachaRecordBin { pub progress_map: ::std::collections::HashMap, #[prost(message, optional, tag = "5")] pub extra_item_bin: ::core::option::Option, + #[prost(enumeration = "GachaAddedItemType", tag = "6")] + pub item_type: i32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -204,3 +206,35 @@ pub struct PlayerDataBin { #[prost(message, optional, tag = "6")] pub gacha_model: ::core::option::Option, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum GachaAddedItemType { + None = 0, + Weapon = 1, + Character = 2, + Bangboo = 3, +} +impl GachaAddedItemType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + GachaAddedItemType::None => "GACHA_ADDED_ITEM_TYPE_NONE", + GachaAddedItemType::Weapon => "GACHA_ADDED_ITEM_TYPE_WEAPON", + GachaAddedItemType::Character => "GACHA_ADDED_ITEM_TYPE_CHARACTER", + GachaAddedItemType::Bangboo => "GACHA_ADDED_ITEM_TYPE_BANGBOO", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "GACHA_ADDED_ITEM_TYPE_NONE" => Some(Self::None), + "GACHA_ADDED_ITEM_TYPE_WEAPON" => Some(Self::Weapon), + "GACHA_ADDED_ITEM_TYPE_CHARACTER" => Some(Self::Character), + "GACHA_ADDED_ITEM_TYPE_BANGBOO" => Some(Self::Bangboo), + _ => None, + } + } +} -- 2.34.1 From 3043d45ac9aac8a4ab3a5feb605869b3cceb9473 Mon Sep 17 00:00:00 2001 From: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Date: Sun, 4 Aug 2024 19:00:34 +0800 Subject: [PATCH 7/7] implement free agent & choosing UP --- assets/GachaConfig/gacha.jsonc | 1433 +++++++++++++++++ nap_data/src/gacha/gacha_config.rs | 10 + nap_gameserver/src/handlers/gacha.rs | 428 ++++- nap_gameserver/src/handlers/mod.rs | 2 + nap_gameserver/src/logic/gacha/gacha_model.rs | 126 +- nap_proto/out/_.rs | 12 +- nap_proto/out/bin.rs | 17 +- 7 files changed, 1968 insertions(+), 60 deletions(-) diff --git a/assets/GachaConfig/gacha.jsonc b/assets/GachaConfig/gacha.jsonc index e69de29..d79abd7 100644 --- a/assets/GachaConfig/gacha.jsonc +++ b/assets/GachaConfig/gacha.jsonc @@ -0,0 +1,1433 @@ +{ + // See configuration principle at: + // ZZZ Server Emu Wish system Design Brief - https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/ + // + // 本文件主要根据 CBT3 测试时可用的抽卡详情页编制,部分规则依据 1.0 实际情况进行了重制。 + // Zhu Yuan (Agent) -「直如朱丝绳」独家频段: https://webstatic.mihoyo.com/nap/event/e20230424gacha/index.html?gacha_id=8de6b658666b8862dc70e14526198f3ae02e5d&lang=zh-cn®ion=prod_cb01_cn&game_biz=nap_cn + // Zhu Yuan (W-Engine) - 「喧哗奏鸣」音擎频段: https://webstatic.mihoyo.com/nap/event/e20230424gacha/index.html?gacha_id=9ee2f7078ca9c045a1c30b9fa524f7990057e1&lang=zh-cn®ion=prod_cb01_cn&game_biz=nap_cn + // Standard Wish (Agent + W-Engine) - 「热门卡司」常驻频段: https://webstatic.mihoyo.com/nap/event/e20230424gacha/index.html?gacha_id=a41a5a30e2f7b64126e5ca80ac06af304273bc&lang=zh-cn®ion=prod_cb01_cn&game_biz=nap_cn + // Victoria Housekeeping (Bangboo) -「卓越搭档」邦布频段: https://webstatic.mihoyo.com/nap/event/e20230424gacha/index.html?gacha_id=5d921fbd92214d478c9f8d6e2e2887902734b8&lang=zh-cn®ion=prod_cb01_cn&game_biz=nap_cn + "character_gacha_pool_list": [ + { + "gacha_schedule_id": 1001001, + "gacha_parent_schedule_id": 1001, + "comment": "Persist, Standard Wish", + "gacha_type": 1, + "cost_item_id": 110, // 原装母带 + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2099-12-31T23:59:59+08:00", + "sharing_guarantee_info_category": "Standard Wish", + "order": 1, + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-90-AgentPool", + // S级信号的基础概率为 0.600%, + // S级代理人的基础概率为 0.300%, + // S级音擎的基础概率为 0.300%。 + "categories": { + "Standard:Agent": { + "item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER", + // 此处的顺序同时由代理人自选 Policy standard-banner-300-S + // 使用,更改会导致客户端行为与服务端不一致。 + "item_ids": [ + 1181, // 格莉丝 + 1211, // 丽娜 + 1101, // 珂蕾妲 + 1021, // 猫又 + 1041, // 「11号」 + 1141, // 莱卡恩 + ], + "category_weight": 50 + }, + "Standard:W-Engine": { + "item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON", + "item_ids": [ + 14102, // 钢铁肉垫 + 14110, // 燃狱齿轮 + 14114, // 拘缚者 + 14104, // 硫磺石 + 14118, // 嵌合编译器 + 14121, // 啜泣摇篮 + ], + "category_weight": 50 + } + } + }, + { + // A级信号的基础概率为 9.400%, + // A级代理人的基础概率为 4.700%, + // A级音擎的基础概率为 4.700%。 + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-AgentPool", + "categories": { + "Standard:Agent": { + "item_type": "GACHA_ADDED_ITEM_TYPE_CHARACTER", + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1131, // 苍角 + 1151, // 露西 + 1281, // 派派 + 1271, // 赛斯 + ], + "category_weight": 50 + }, + "Standard:W-Engine": { + "item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON", + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13113, // 含羞恶面 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + 13127, // 维序者-特化型 + ], + "category_weight": 50 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_type": "GACHA_ADDED_ITEM_TYPE_WEAPON", + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 50 + } + } + } + ], + "discount_policy_tags": [ + "5x-10-poll-discount-8", + "50-poll-S", + "first-S-Agent", + "standard-banner-300-S" + ] + }, + { + "gacha_schedule_id": 5001001, + "gacha_parent_schedule_id": 5001, + "comment": "1.0, Full, Persistent, Bangboo", + "gacha_type": 5, + "cost_item_id": 112, // 邦布券 + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2077-12-31T23:59:59+08:00", + "sharing_guarantee_info_category": "Bangboo Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-Bangboo", + ], + "probability_model_tag": "get-S-80-WEngine", + "category_guarantee_policy_tags": [ + "chooseable-up-bangboo" + ], + "categories": { + "Standard:Bangboo": { + "item_ids": [ + 54002, // 阿全 + 54005, // 艾米莉安 + 54006, // 飚速布 + 54008, // 插头布 + 54009, // 共鸣布 + 54001, // 鲨牙布 + 54013, // 左轮布 + ], + "category_weight": 1 + }, + "Event-Exclusive:Bangboo": { + "item_ids": [ + 54004, // 巴特勒 + ], + "is_promotional_items": true, + "category_weight": 1 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-Bangboo", + ], + "probability_model_tag": "get-A-10-WEngine", + "category_guarantee_policy_tags": [ + "common-up-bangboo" + ], + "categories": { + "Standard:Bangboo": { + "item_ids": [ + 53001, // 企鹅布 + 53003, // 寻宝布 + 53005, // 纸壳布 + 53004, // 扑击布 + 53006, // 纸袋布 + 53007, // 泪眼布 + 53008, // 果核布 + 53009, // 飞靶布 + 53010, // 电击布 + 53011, // 磁力布 + 54003, // 恶魔布 + ], + "category_weight": 1 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-Bangboo", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 2001001, + "gacha_parent_schedule_id": 2001, + "comment": "1.0, First Half, Allen / 艾莲, Agent", + "gacha_type": 2, + "cost_item_id": 111, + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2024-07-24T11:59:59+08:00", + "sharing_guarantee_info_category": "Character Event Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-90-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1021, // 猫又 + 1101, // 珂蕾妲 + 1041, // 「11号」 + 1141, // 莱卡恩 + 1181, // 格莉丝 + 1211, // 丽娜 + ], + "category_weight": 50 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1191, // 艾莲 + ], + "category_weight": 50 + } + } + }, + { + // 在本期独家频段中,调频获取A级信号的基础概率为 9.400%, + // 其中A级代理人的基础概率为 7.050%, + // A级音擎的基础概率为 2.350%。 + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1121, // 本 + 1151, // 露西 + 1281, // 派派 + ], + "category_weight": 3525 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1111, // 安东 + 1131, // 苍角 + ], + "category_weight": 3525 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13113, // 含羞恶面 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + ], + "category_weight": 2350 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 3001001, + "gacha_parent_schedule_id": 3001, + "comment": "1.0, First Half, Allen / 艾莲, W-Engine", + "gacha_type": 3, + "cost_item_id": 111, + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2024-07-24T11:59:59+08:00", + "sharing_guarantee_info_category": "W-Engine Wish", + "gacha_items": [ + { + // 在本期音擎频段中,调频获取S级音擎的基础概率为 1.000%, + // 最多 80 次调频必能通过保底获取S级音擎。 + // S级音擎的综合概率(含保底)为 2.000%。 + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-80-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 14102, // 钢铁肉垫 + 14110, // 燃狱齿轮 + 14114, // 拘缚者 + 14104, // 硫磺石 + 14118, // 嵌合编译器 + 14121, // 啜泣摇篮 + ], + "category_weight": 25 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 14119, // 深海访客 + ], + "is_promotional_items": true, + "category_weight": 75 + } + } + }, + { + // 在本期音擎频段中,调频获取A级信号的基础概率为 15.000%, + // 其中A级音擎的基础概率为 13.125%, + // A级代理人的基础概率为 1.875%。 + // A级信号的综合概率(含保底)为 18.000%。 + // 当调频获取到A级信号时,有 75.000% 的概率为本期A级概率提升音擎中的一个。 + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1131, // 苍角 + 1151, // 露西 + 1281, // 派派 + ], + "category_weight": 3750 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13112, // 比格气缸 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + ], + "category_weight": 13125 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 13113, // 含羞恶面 + 13111, // 旋钻机-赤轴 + ], + "is_promotional_items": true, + "category_weight": 13125 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 2001002, + "gacha_parent_schedule_id": 2002, + "comment": "1.0, Second Half, Zhu Yuan / 朱鸢, Agent", + "gacha_type": 2, + "cost_item_id": 111, + "start_time": "2024-07-24T12:00:00+08:00", + "end_time": "2024-08-01T12:00:00+08:00", + "sharing_guarantee_info_category": "Character Event Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-90-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1021, // 猫又 + 1101, // 珂蕾妲 + 1041, // 「11号」 + 1141, // 莱卡恩 + 1181, // 格莉丝 + 1211, // 丽娜 + ], + "category_weight": 50 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1241 // 朱鸢 + ], + "category_weight": 50 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1131, // 苍角 + 1151, // 露西 + 1281, // 派派 + ], + "category_weight": 3525 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1031, // 妮可 + 1121, // 本 + ], + "category_weight": 3525 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13113, // 含羞恶面 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + ], + "category_weight": 2350 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 3001002, + "gacha_parent_schedule_id": 3002, + "comment": "1.0, Second Half, Zhu Yuan / 朱鸢, W-Engine", + "gacha_type": 3, + "cost_item_id": 111, + "start_time": "2024-07-24T12:00:00+08:00", + "end_time": "2024-08-01T12:00:00+08:00", + "sharing_guarantee_info_category": "W-Engine Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-80-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 14102, // 钢铁肉垫 + 14110, // 燃狱齿轮 + 14114, // 拘缚者 + 14104, // 硫磺石 + 14118, // 嵌合编译器 + 14121, // 啜泣摇篮 + ], + "category_weight": 25 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 14124 // 防暴者VI型 + ], + "is_promotional_items": true, + "category_weight": 75 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1131, // 苍角 + 1151, // 露西 + 1281, // 派派 + ], + "category_weight": 3750 + }, + "Standard:W-Engine": { + "item_ids": [ + 13007, // 正版变身器 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13001, // 街头巨星 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13113, // 含羞恶面 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + ], + "category_weight": 13125 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 13103, // 聚宝箱 + 13112, // 比格气缸 + ], + "is_promotional_items": true, + "category_weight": 13125 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 2001003, + "gacha_parent_schedule_id": 2003, + "comment": "1.1, Unknown Half, QingYi / 青衣, Agent", + "gacha_type": 2, + "cost_item_id": 111, + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2077-12-31T23:59:59+08:00", + "sharing_guarantee_info_category": "Character Event Wish", + "gacha_items": [ + { + // 在本期独家频段中,调频获取S级代理人的基础概率为 0.600%, + // 当调频获取到S级代理人时,有 50.000% 的概率为本期限定S级代理人。 + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-90-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1021, // 猫又 + 1101, // 珂蕾妲 + 1041, // 「11号」 + 1141, // 莱卡恩 + 1181, // 格莉丝 + 1211, // 丽娜 + ], + "category_weight": 50 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1251, // 青衣 + ], + "category_weight": 50 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1151, // 露西 + 1281, // 派派 + ], + "category_weight": 3525 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1131, // 苍角 + 1271, // 赛斯 + ], + "category_weight": 3525 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13113, // 含羞恶面 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + 13127, // 维序者-特化型 + ], + "category_weight": 2350 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 3001003, + "gacha_parent_schedule_id": 3003, + "comment": "1.1, Unknown Half, QingYi / 青衣, W-Engine", + "gacha_type": 3, + "cost_item_id": 111, + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2077-12-31T23:59:59+08:00", + "sharing_guarantee_info_category": "W-Engine Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-80-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 14102, // 钢铁肉垫 + 14110, // 燃狱齿轮 + 14114, // 拘缚者 + 14104, // 硫磺石 + 14118, // 嵌合编译器 + 14121, // 啜泣摇篮 + ], + "category_weight": 25 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 14125, // 玉壶青冰 + ], + "is_promotional_items": true, + "category_weight": 75 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1131, // 苍角 + ], + "category_weight": 3750 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + ], + "category_weight": 13125 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 13113, // 含羞恶面 + 13127, // 维序者-特化型 + ], + "is_promotional_items": true, + "category_weight": 13125 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 2001004, + "gacha_parent_schedule_id": 2004, + "comment": "1.1, Unknown Half, Jane Doe / 简·杜, Agent", + "gacha_type": 2, + "cost_item_id": 111, + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2077-12-31T23:59:59+08:00", + "sharing_guarantee_info_category": "Character Event Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-90-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1021, // 猫又 + 1101, // 珂蕾妲 + 1041, // 「11号」 + 1141, // 莱卡恩 + 1181, // 格莉丝 + 1211, // 丽娜 + ], + "category_weight": 50 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1261, // 简 + ], + "category_weight": 50 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-AgentPool", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1151, // 露西 + 1281, // 派派 + ], + "category_weight": 3525 + }, + "Event-Exclusive:Agent": { + "is_promotional_items": true, + "item_ids": [ + 1271, // 赛斯 + 1131, // 苍角 + ], + "category_weight": 3525 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13113, // 含羞恶面 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + 13127, // 维序者-特化型 + ], + "category_weight": 2350 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + { + "gacha_schedule_id": 3001004, + "gacha_parent_schedule_id": 3004, + "comment": "1.1, Unknown Half, Jane Doe / 简·杜, W-Engine", + "gacha_type": 3, + "cost_item_id": 111, + "start_time": "2024-07-04T06:00:00+08:00", + "end_time": "2077-12-31T23:59:59+08:00", + "sharing_guarantee_info_category": "W-Engine Wish", + "gacha_items": [ + { + "rarity": 4, + "extra_items_policy_tags": [ + "S-item", + ], + "probability_model_tag": "get-S-80-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 14102, // 钢铁肉垫 + 14110, // 燃狱齿轮 + 14114, // 拘缚者 + 14104, // 硫磺石 + 14118, // 嵌合编译器 + 14121, // 啜泣摇篮 + ], + "category_weight": 25 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 14126, // 淬锋钳刺 + ], + "is_promotional_items": true, + "category_weight": 75 + } + } + }, + { + "rarity": 3, + "extra_items_policy_tags": [ + "A-item", + ], + "probability_model_tag": "get-A-10-WEngine", + "category_guarantee_policy_tags": [ + "promotional-items" + ], + "categories": { + "Standard:Agent": { + "item_ids": [ + 1011, // 安比 + 1031, // 妮可 + 1061, // 可琳 + 1081, // 比利 + 1111, // 安东 + 1121, // 本 + 1131, // 苍角 + ], + "category_weight": 3750 + }, + "Standard:W-Engine": { + "item_ids": [ + 13001, // 街头巨星 + 13002, // 时光切片 + 13003, // 雨林饕客 + 13004, // 星徽引擎 + 13005, // 人为刀俎 + 13006, // 贵重骨核 + 13007, // 正版变身器 + 13008, // 双生泣星 + 13010, // 兔能环 + 13101, // 德玛拉电池Ⅱ型 + 13103, // 聚宝箱 + 13106, // 家政员 + 13108, // 仿制星徽引擎 + 13111, // 旋钻机-赤轴 + 13112, // 比格气缸 + 13115, // 好斗的阿炮 + 13128, // 轰鸣座驾 + ], + "category_weight": 13125 + }, + "Event-Exclusive:W-Engine": { + "item_ids": [ + 13127, // 维序者-特化型 + 13113, // 含羞恶面 + ], + "is_promotional_items": true, + "category_weight": 13125 + } + } + }, + { + "rarity": 2, + "extra_items_policy_tags": [ + "B-item", + ], + "probability_model_tag": "get-B", + "categories": { + "Standard:W-Engine": { + "item_ids": [ + 12001, // 「月相」-望 + 12002, // 「月相」-晦 + 12003, // 「月相」-朔 + 12004, // 「残响」-Ⅰ型 + 12005, // 「残响」-Ⅱ型 + 12006, // 「残响」-Ⅲ型 + 12007, // 「湍流」-铳型 + 12008, // 「湍流」-矢型 + 12009, // 「湍流」-斧型 + 12010, // 「电磁暴」-壹式 + 12011, // 「电磁暴」-贰式 + 12012, // 「电磁暴」-叁式 + 12013, // 「恒等式」-本格 + 12014, // 「恒等式」-变格 + ], + "category_weight": 100 + } + } + } + ] + }, + ], + "probability_model_map": { + // 以下内容根据 @一棵平衡树 公测后解析进行了更新。 + // https://www.bilibili.com/video/BV1oW42197RR + "get-S-90-AgentPool": { + "points": [ + { + "start_pity": 1, + "start_chance_percent": 0.6 + }, + { + "start_pity": 73, + "start_chance_percent": 0.6, + "increment_percent": 6 + } + ] + }, + "get-A-10-AgentPool": { + "clear_status_on_higher_rarity_pulled": true, + "points": [ + { + "start_pity": 1, + "start_chance_percent": 9.4 + }, + { + "start_pity": 10, + "start_chance_percent": 100 + } + ] + }, + "get-B": { + "points": [ + { + "start_pity": 1, + "start_chance_percent": 100 + } + ] + }, + "get-S-80-WEngine": { + // "maximum_guarantee_pity": 80, + "points": [ + { + "start_pity": 1, + "start_chance_percent": 1 + }, + { + "start_pity": 64, + "start_chance_percent": 1, + "increment_percent": 6 + }, + { + "start_pity": 80, + "start_chance_percent": 100 + } + ] + }, + "get-A-10-WEngine": { + "clear_status_on_higher_rarity_pulled": true, + "points": [ + { + "start_pity": 1, + "start_chance_percent": 15 + }, + { + "start_pity": 10, + "start_chance_percent": 100 + } + ] + } + }, + "extra_items_policy_map": { + "S-item": { + "id": 115, // 信号余波 + "count": 40 + }, + "A-item": { + "id": 115, // 信号余波 + "count": 8 + }, + "B-item": { + "id": 117, // 信号残响 + "count": 20 + }, + "S-Bangboo": { + "id": 116, // 邦布币 + "count": 50 + }, + "A-Bangboo": { + "id": 116, // 邦布币 + "count": 10 + }, + "B-Bangboo": { + "id": 116, // 邦布币 + "count": 2 + } + }, + "discount_policies": { + "ten_pull_discount_map": { + "5x-10-poll-discount-8": { + "use_limit": 5, + "discounted_prize": 8 + }, + }, + "must_gain_item_map": { + "first-S-Agent": { + "use_limit": 1, + "rarity": 4, + "category_tag": "Standard:Agent" + }, + }, + "advanced_guarantee_map": { + "50-poll-S": { + "use_limit": 1, + "rarity": 4, + "guarantee_pity": 50 + }, + }, + "free_select_map": { + "standard-banner-300-S": { + "free_select_progress_record_tag": "standard-banner-300-S__free_select_total_progress", + "free_select_usage_record_tag": "standard-banner-300-S__free_select_milestone_idx", + "rarity": 4, + "category_tags": [ + "Standard:Agent", + ], + "milestones": [ + 300, + ] + }, + } + }, + "category_guarantee_policy_map": { + "promotional-items": { + "included_category_tags": [ + // 当调频获取到S级代理人时,有 50.000% 的概率为本期限定S级代理人。 + "Event-Exclusive:Agent", + "Event-Exclusive:W-Engine" + ], + "trigger_on_failure_times": 1, + "clear_status_on_target_changed": false, + "chooseable": false + }, + "chooseable-up-bangboo": { + "included_category_tags": [ + "Standard:Bangboo", + "Event-Exclusive:Bangboo" + ], + "trigger_on_failure_times": 0, + "clear_status_on_target_changed": false, + "chooseable": true + }, + "common-up-bangboo": { + "included_category_tags": [ + "Standard:Bangboo", + "Event-Exclusive:Bangboo" + ], + "trigger_on_failure_times": 0, + "clear_status_on_target_changed": false, + "chooseable": false + } + }, + // 定义供祈愿显示信息使用的额外属性。 + "common_properties": { + "up_item_category_tag": "promotional-items", + "s_item_rarity": 4, + "a_item_rarity": 3, + "ten_pull_discount_tag": "5x-10-poll-discount-8", + "newcomer_advanced_s_tag": "50-poll-S" + } +} \ No newline at end of file diff --git a/nap_data/src/gacha/gacha_config.rs b/nap_data/src/gacha/gacha_config.rs index c539ac3..486319f 100644 --- a/nap_data/src/gacha/gacha_config.rs +++ b/nap_data/src/gacha/gacha_config.rs @@ -126,11 +126,21 @@ pub struct MustGainItem { pub category_tag: String, } +#[derive(Debug, Default, Deserialize)] +pub struct FreeSelectItem { + pub milestones: Vec, + pub rarity: u32, + pub category_tags: Vec, + pub free_select_progress_record_tag: String, + pub free_select_usage_record_tag: String, +} + #[derive(Debug, Default, Deserialize)] pub struct DiscountPolicyCollection { pub ten_pull_discount_map: HashMap, pub must_gain_item_map: HashMap, pub advanced_guarantee_map: HashMap, + pub free_select_map: HashMap, } impl DiscountPolicyCollection { diff --git a/nap_gameserver/src/handlers/gacha.rs b/nap_gameserver/src/handlers/gacha.rs index 31fd93a..20c0163 100644 --- a/nap_gameserver/src/handlers/gacha.rs +++ b/nap_gameserver/src/handlers/gacha.rs @@ -1,3 +1,11 @@ +use std::{ + cmp::min, + collections::{ + hash_map::Entry::{Occupied, Vacant}, + HashSet, + }, +}; + use data::{ gacha::{gacha_config::*, global_gacha_config}, tables::{AvatarBaseID, WeaponID}, @@ -24,7 +32,7 @@ pub async fn on_get_gacha_data( Ok(GetGachaDataScRsp { retcode: Retcode::RetSucc.into(), gacha_type: req.gacha_type, - gacha_data: Some(generate_all_gacha_info(_player)), + gacha_data: Some(generate_all_gacha_info(_player, &Local::now())), }) } } @@ -45,8 +53,12 @@ pub async fn on_do_gacha( &pull_time, ); if let None = target_pool { + tracing::info!( + "refuse gacha because: pool of parent_schedule_id {} not found", + req.gacha_parent_schedule_id + ); return Ok(DoGachaScRsp { - retcode: Retcode::RetSucc.into(), + retcode: Retcode::RetFail.into(), ..Default::default() }); }; @@ -72,11 +84,15 @@ pub async fn on_do_gacha( let usage = status_bin.discount_usage_map.get_mut(discount_tag).unwrap(); if *usage < discount_policy.use_limit { *usage += 1; - cost_count = discount_policy.discounted_prize; + // cost_count = discount_policy.discounted_prize; } } } if cost_count != req.cost_item_count { + tracing::info!( + "refuse gacha because: expected cost item {cost_count}, found {}", + req.cost_item_count + ); return Ok(DoGachaScRsp { retcode: Retcode::RetFail.into(), ..Default::default() @@ -88,7 +104,7 @@ pub async fn on_do_gacha( let mut gain_item_list: Vec = vec![]; while pull_count > 0 { let pull_result = gacha_model.perform_pull_pool(&pull_time, target_pool); - let extra_item_bin = pull_result.extra_item_bin.unwrap(); + let extra_item_bin = pull_result.extra_item_bin.clone().unwrap(); let uid = match GachaAddedItemType::try_from(pull_result.item_type) { Ok(enum_val) => match enum_val { GachaAddedItemType::Weapon => match WeaponID::new(pull_result.obtained_item_id) { @@ -123,16 +139,236 @@ pub async fn on_do_gacha( ..GainItemInfo::default() }); pull_count -= 1; + gacha_model.gacha_bin.gacha_records.push(pull_result); } Ok(DoGachaScRsp { retcode: Retcode::RetSucc.into(), gain_item_list, - gacha_data: Some(generate_all_gacha_info(_player)), + gacha_data: Some(generate_all_gacha_info(_player, &pull_time)), cost_item_count: req.cost_item_count, }) } +pub async fn on_gacha_free_agent( + _session: &NetSession, + _player: &mut Player, + req: GachaFreeAgentCsReq, +) -> NetResult { + let gachaconf = global_gacha_config(); + let gacha_model = &mut _player.gacha_model; + let role_model = &mut _player.role_model; + let pull_time = Local::now(); + let target_pool = get_gacha_pool( + &gachaconf.character_gacha_pool_list, + &req.gacha_parent_schedule_id, + &pull_time, + ); + if let None = target_pool { + tracing::info!( + "refuse free agent because: pool of parent_schedule_id {} not found", + req.gacha_parent_schedule_id + ); + return Ok(GachaFreeAgentScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + }; + let target_pool = target_pool.unwrap(); + let gacha_bin = &mut _player.gacha_model.gacha_bin; + let sharing_guarantee_category_tag = &target_pool.sharing_guarantee_info_category; + let status_bin = gacha_bin + .gacha_status_map + .get_mut(sharing_guarantee_category_tag) + .unwrap(); + + let mut free_select_policy: Option<&FreeSelectItem> = None; + let mut free_select_progress: u32 = 0; + let mut free_select_required_pull: u32 = 0; + for discount_policy_tag in target_pool.discount_policy_tags.iter() { + if gachaconf + .discount_policies + .free_select_map + .contains_key(discount_policy_tag) + { + let policy = gachaconf + .discount_policies + .free_select_map + .get(discount_policy_tag) + .unwrap(); + let free_select_demand_idx = usize::try_from( + *(status_bin + .discount_usage_map + .get(&policy.free_select_usage_record_tag) + .unwrap()), + ) + .unwrap(); + if policy.milestones.len() <= free_select_demand_idx { + continue; + } + + let free_select_actual_progress = status_bin + .discount_usage_map + .get(&policy.free_select_progress_record_tag) + .unwrap(); + free_select_policy = Some(policy); + free_select_required_pull = policy + .milestones + .get(free_select_demand_idx) + .unwrap() + .to_owned(); + free_select_progress = min(free_select_required_pull, *free_select_actual_progress); + } + } + + if let None = free_select_policy { + tracing::info!( + "refuse free agent because: pool of parent_schedule_id {} hasn't defined free agent discount yet (or used up chance)", + req.gacha_parent_schedule_id + ); + return Ok(GachaFreeAgentScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + } else if free_select_progress < free_select_required_pull { + tracing::info!( + "refuse free agent because: use pulled {free_select_progress} (after last free agent) in parent_schedule_id {}, required {free_select_required_pull}", + req.gacha_parent_schedule_id + ); + return Ok(GachaFreeAgentScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + } + let free_select_policy = free_select_policy.unwrap(); + let mut has_demanded_item = false; + for rarity_items in target_pool.gacha_items.iter() { + if rarity_items.rarity != free_select_policy.rarity { + continue; + } + for (category_tag, category) in rarity_items.categories.iter() { + if !free_select_policy.category_tags.contains(category_tag) { + continue; + } + has_demanded_item |= category.item_ids.contains(&req.avatar_id); + } + } + if !has_demanded_item { + tracing::info!( + "refuse free agent because: pool of parent_schedule_id {} doesn't have demanded item {}", + req.gacha_parent_schedule_id, req.avatar_id + ); + return Ok(GachaFreeAgentScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + } + + role_model.add_avatar(AvatarBaseID::new(req.avatar_id).unwrap()); + (*status_bin + .discount_usage_map + .get_mut(&free_select_policy.free_select_usage_record_tag) + .unwrap()) += 1; + (*status_bin + .discount_usage_map + .get_mut(&free_select_policy.free_select_progress_record_tag) + .unwrap()) -= free_select_required_pull; + + Ok(GachaFreeAgentScRsp { + retcode: Retcode::RetSucc.into(), + }) +} + +pub async fn on_choose_gacha_up( + _session: &NetSession, + _player: &mut Player, + req: ChooseGachaUpCsReq, +) -> NetResult { + let gachaconf = global_gacha_config(); + let gacha_model = &mut _player.gacha_model; + let pull_time = Local::now(); + let target_pool = get_gacha_pool( + &gachaconf.character_gacha_pool_list, + &req.gacha_parent_schedule_id, + &pull_time, + ); + if let None = target_pool { + return Ok(ChooseGachaUpScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + }; + let target_pool = target_pool.unwrap(); + + for rarity_items in target_pool.gacha_items.iter() { + if rarity_items.rarity != gachaconf.common_properties.s_item_rarity { + continue; + } + for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { + let category_guarantee_policy = gachaconf + .category_guarantee_policy_map + .get(guarantee_policy_tag) + .unwrap(); + if !category_guarantee_policy.chooseable { + continue; + } + let mut up_category: Option<&String> = None; + for (category_tag, category) in rarity_items.categories.iter() { + if category.item_ids.contains(&req.item_id) { + up_category = Some(category_tag); + break; + } + } + if let None = up_category { + return Ok(ChooseGachaUpScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }); + }; + let up_category = up_category.unwrap(); + + let progress_bin = gacha_model + .gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap() + .rarity_status_map + .get_mut(&rarity_items.rarity) + .unwrap(); + match progress_bin + .categories_chosen_guarantee_item_map + .entry(guarantee_policy_tag.clone()) + { + Occupied(mut occupied_entry) => { + occupied_entry.insert(req.item_id); + } + Vacant(vacant_entry) => { + vacant_entry.insert(req.item_id); + } + }; + match progress_bin + .categories_chosen_guarantee_category_map + .entry(up_category.clone()) + { + Occupied(mut occupied_entry) => { + occupied_entry.insert(up_category.clone()); + } + Vacant(vacant_entry) => { + vacant_entry.insert(up_category.clone()); + } + }; + return Ok(ChooseGachaUpScRsp { + retcode: Retcode::RetSucc.into(), + ..Default::default() + }); + } + } + + Ok(ChooseGachaUpScRsp { + retcode: Retcode::RetFail.into(), + ..Default::default() + }) +} fn generate_gacha_info_from_pool( gacha_bin: &GachaModelBin, target_pool: &CharacterGachaPool, @@ -154,34 +390,13 @@ fn generate_gacha_info_from_pool( .get(&common_properties.a_item_rarity) .unwrap() .pity; - let mut up_s_item_list: Vec = vec![]; - let mut up_a_item_list: Vec = vec![]; - let mut s_guarantee: u32 = 0; - let mut a_guarantee: u32 = 0; - for rarity_items in target_pool.gacha_items.iter() { - for category in rarity_items.categories.values() { - let probability_model = gachaconf - .probability_model_map - .get(&rarity_items.probability_model_tag) - .unwrap(); - if rarity_items.rarity == common_properties.s_item_rarity { - if category.is_promotional_items { - up_s_item_list = category.item_ids.clone(); - } - s_guarantee = probability_model.maximum_guarantee_pity - pity_s + 1; - } - if rarity_items.rarity == common_properties.a_item_rarity { - if category.is_promotional_items { - up_a_item_list = category.item_ids.clone(); - } - a_guarantee = probability_model.maximum_guarantee_pity - pity_a + 1; - } - } - } let mut discount_ten_roll_prize: u32 = 0; let mut discount_avaliable_num: u32 = 0; let mut advanced_s_guarantee: u32 = 0; + let mut free_select_progress: u32 = 0; + let mut free_select_required_pull: u32 = 0; + let mut free_select_policy: Option<&FreeSelectItem> = None; for discount_policy_tag in target_pool.discount_policy_tags.iter() { if common_properties.newcomer_advanced_s_tag == *discount_policy_tag { let policy = gachaconf @@ -211,6 +426,123 @@ fn generate_gacha_info_from_pool( discount_ten_roll_prize = policy.discounted_prize; discount_avaliable_num = policy.use_limit - discount_usage; } + } else if gachaconf + .discount_policies + .free_select_map + .contains_key(discount_policy_tag) + { + let policy = gachaconf + .discount_policies + .free_select_map + .get(discount_policy_tag) + .unwrap(); + let free_select_demand_idx = usize::try_from( + *(status_bin + .discount_usage_map + .get(&policy.free_select_usage_record_tag) + .unwrap()), + ) + .unwrap(); + if policy.milestones.len() <= free_select_demand_idx { + continue; + } + + let free_select_actual_progress = status_bin + .discount_usage_map + .get(&policy.free_select_progress_record_tag) + .unwrap(); + free_select_policy = Some(policy); + free_select_required_pull = policy + .milestones + .get(free_select_demand_idx) + .unwrap() + .to_owned(); + free_select_progress = min(free_select_required_pull, *free_select_actual_progress); + } + } + + let mut up_s_item_list: Vec = vec![]; + let mut up_a_item_list: Vec = vec![]; + let mut free_select_item_list: Vec = vec![]; + let mut chooseable_up_list: Vec = vec![]; + let mut chosen_up_item: u32 = 0; + let mut s_guarantee: u32 = 0; + let mut a_guarantee: u32 = 0; + for rarity_items in target_pool.gacha_items.iter() { + let mut chooseable_up_included_category_tags: Option<&HashSet> = None; + let mut chooseable_policy_tag: Option<&String> = None; + for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { + let category_guarantee_policy = gachaconf + .category_guarantee_policy_map + .get(guarantee_policy_tag) + .unwrap(); + if !category_guarantee_policy.chooseable { + continue; + } + chooseable_policy_tag = Some(guarantee_policy_tag); + chooseable_up_included_category_tags = + Some(&category_guarantee_policy.included_category_tags); + if let Some(item) = status_bin + .rarity_status_map + .get(&rarity_items.rarity) + .unwrap() + .categories_chosen_guarantee_item_map + .get(guarantee_policy_tag) + { + chosen_up_item = item.clone(); + } + } + + for (category_tag, category) in rarity_items.categories.iter() { + let probability_model = gachaconf + .probability_model_map + .get(&rarity_items.probability_model_tag) + .unwrap(); + let maximum_pity = &probability_model.maximum_guarantee_pity; + if rarity_items.rarity == common_properties.s_item_rarity { + if category.is_promotional_items { + up_s_item_list = category.item_ids.clone(); + } + // tracing::info!("pity_s: {pity_s}"); + // thread 'tokio-runtime-worker' panicked at nap_gameserver\src\handlers\gacha.rs:369:31: + // attempt to subtract with overflow + s_guarantee = maximum_pity - min(pity_s, maximum_pity.clone()) + 1; + } + if rarity_items.rarity == common_properties.a_item_rarity { + if category.is_promotional_items { + up_a_item_list = category.item_ids.clone(); + } + // tracing::info!("pity_a: {pity_a}"); + a_guarantee = maximum_pity - min(pity_a, maximum_pity.clone()) + 1; + } + + if let Some(val) = free_select_policy { + if val.rarity == rarity_items.rarity && val.category_tags.contains(category_tag) { + free_select_item_list.append(&mut category.item_ids.clone()); + } + } + + if let Some(tags) = chooseable_up_included_category_tags { + if tags.contains(category_tag) { + chooseable_up_list.append(&mut category.item_ids.clone()); + } + } + } + + if let Some(priority_policy_tag) = chooseable_policy_tag { + // if let Some(item) = status_bin + // .rarity_status_map + // .get(&rarity_items.rarity) + // .unwrap() + // .categories_chosen_guarantee_item_map + // .get(priority_policy_tag) + // { + if rarity_items.rarity == gachaconf.common_properties.s_item_rarity { + up_s_item_list = vec![]; + } else if rarity_items.rarity == gachaconf.common_properties.a_item_rarity { + up_a_item_list = vec![]; + } + // } } } @@ -219,7 +551,7 @@ fn generate_gacha_info_from_pool( need_item_count: 1, }]; - Gacha { + let mut result = Gacha { gacha_schedule_id: target_pool.gacha_schedule_id, gacha_parent_schedule_id: target_pool.gacha_parent_schedule_id, gacha_type: target_pool.gacha_type, @@ -227,34 +559,46 @@ fn generate_gacha_info_from_pool( end_timestamp: target_pool.end_time.timestamp(), discount_avaliable_num, discount_ten_roll_prize, - up_s_item_list, - up_a_item_list, advanced_s_guarantee, s_guarantee, a_guarantee, need_item_info_list, - lpklhoobkbh: target_pool.gacha_parent_schedule_id, - nammdglepbk: 593, - hgmcofcjmbg: 101, + free_select_progress, + free_select_required_pull, + free_select_item_list, + chosen_up_item, + // nammdglepbk: 563, + // hgmcofcjmbg: 101, + // akggbhgkifd: chooseable_up_list.clone(), + chooseable_up_list, ..Gacha::default() + }; + if up_s_item_list.len() > 0 { + result.up_s_item_list = up_s_item_list; } + if up_a_item_list.len() > 0 { + result.up_a_item_list = up_a_item_list; + } + result } -fn generate_all_gacha_info(_player: &Player) -> GachaData { +fn generate_all_gacha_info(_player: &Player, now: &DateTime) -> GachaData { let gachaconf = global_gacha_config(); let gacha_bin = &_player.gacha_model.gacha_bin; let mut gacha_list: Vec = vec![]; for target_pool in gachaconf.character_gacha_pool_list.iter() { - gacha_list.push(generate_gacha_info_from_pool( - &gacha_bin, - target_pool, - &gachaconf.common_properties, - )); + if target_pool.is_still_open(now) { + gacha_list.push(generate_gacha_info_from_pool( + &gacha_bin, + target_pool, + &gachaconf.common_properties, + )); + } } // tracing::info!("gacha_list: {:?}", gacha_list); GachaData { - random_number: 0, + random_number: 6167, gacha_pool: Some(GachaPool { gacha_list }), ..GachaData::default() } diff --git a/nap_gameserver/src/handlers/mod.rs b/nap_gameserver/src/handlers/mod.rs index 4e83d49..c869ef7 100644 --- a/nap_gameserver/src/handlers/mod.rs +++ b/nap_gameserver/src/handlers/mod.rs @@ -120,6 +120,8 @@ req_handlers! { quest::BeginArchiveBattleQuest; quest::FinishArchiveQuest; gacha::DoGacha; + gacha::ChooseGachaUp; + gacha::GachaFreeAgent; } notify_handlers! { diff --git a/nap_gameserver/src/logic/gacha/gacha_model.rs b/nap_gameserver/src/logic/gacha/gacha_model.rs index 5f70037..ba201db 100644 --- a/nap_gameserver/src/logic/gacha/gacha_model.rs +++ b/nap_gameserver/src/logic/gacha/gacha_model.rs @@ -52,10 +52,42 @@ impl GachaModel { &mut progress_bin.categories_progress_map, &category_guarantee_policy_tag, ); + + let guarantee_policy = gachaconf + .category_guarantee_policy_map + .get(category_guarantee_policy_tag) + .unwrap(); + if !guarantee_policy.chooseable { + continue; + } + get_or_add( + &mut progress_bin.categories_chosen_guarantee_progress_map, + &category_guarantee_policy_tag, + ); } } for discount_policy_tag in gacha_pool.discount_policy_tags.iter() { - get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag); + if gachaconf + .discount_policies + .free_select_map + .contains_key(discount_policy_tag) + { + let policy = gachaconf + .discount_policies + .free_select_map + .get(discount_policy_tag) + .unwrap(); + get_or_add( + &mut status_bin.discount_usage_map, + &policy.free_select_progress_record_tag, + ); + get_or_add( + &mut status_bin.discount_usage_map, + &policy.free_select_usage_record_tag, + ); + } else { + get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag); + } } } self @@ -70,8 +102,14 @@ impl GachaModel { let (rarity_items, progress_bin, status_bin, probability_model) = determine_rarity(&gacha_bin, target_pool); let (category_tag, category) = determine_category(rarity_items, progress_bin, target_pool); - let result = - determine_gacha_result(pull_time, category, target_pool, status_bin, rarity_items); + let result = determine_gacha_result( + pull_time, + category, + target_pool, + status_bin, + progress_bin, + rarity_items, + ); update_pity(&mut gacha_bin, rarity_items, probability_model, target_pool); update_category_guarantee_info(&mut gacha_bin, rarity_items, &category_tag, target_pool); update_discount(&mut gacha_bin, target_pool, &category_tag, rarity_items); @@ -196,9 +234,27 @@ fn determine_category<'bin, 'conf>( let mut category_tag_inited = false; let mut category_tag_result: HashSet = HashSet::new(); // First of all, if there's a chooseable category and - // it's can be triggered, then we MUST give that - // category's item. - // TODO: Only Genshin can do + // it is SELECTED then we MUST give that category's item. + for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { + let category_guarantee_policy = gachaconf + .category_guarantee_policy_map + .get(guarantee_policy_tag) + .unwrap(); + if !category_guarantee_policy.chooseable { + continue; + } + // As we found a policy defined chooseable, we + // should head to look whether the user chose + // the category he want. + if let Some(category_tag) = progress_bin + .categories_chosen_guarantee_category_map + .get(guarantee_policy_tag) + { + // User chose a category; our work are done here. + category_tag_result.insert(category_tag.clone()); + category_tag_inited = true; + } + } // Then we should take a look at MustGainItem. if !category_tag_inited { for discount_policy_tag in target_pool.discount_policy_tags.iter() { @@ -262,14 +318,49 @@ fn determine_gacha_result<'bin, 'conf>( category: &'conf GachaCategoryInfo, target_pool: &'conf CharacterGachaPool, status_bin: &'bin GachaStatusBin, + progress_bin: &'bin GachaProgressBin, rarity_items: &'conf GachaAvailableItemsInfo, ) -> GachaRecordBin { let gachaconf = gacha::global_gacha_config(); let item_pool_len = category.item_ids.len() as u32; - let item_id = category - .item_ids - .get(rand::thread_rng().gen_range(0..item_pool_len) as usize) - .unwrap(); + let mut item_id: Option<&u32> = None; + // We should see whether user's search priority exists. + for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { + let category_guarantee_policy = gachaconf + .category_guarantee_policy_map + .get(guarantee_policy_tag) + .unwrap(); + if !category_guarantee_policy.chooseable { + continue; + } + // Firstly, judge whether the user failed enough times. + // The user is limited to get only this category's item, + // so we should record the user's failure to get his + // selected item elsewhere. + if progress_bin + .categories_chosen_guarantee_progress_map + .get(guarantee_policy_tag) + .unwrap() + < &category_guarantee_policy.trigger_on_failure_times + { + continue; + } + // We directly look whether user chose an UP item. + if let Some(item) = progress_bin + .categories_chosen_guarantee_item_map + .get(guarantee_policy_tag) + { + item_id = Some(item); + } + } + + let item_id = match item_id { + Some(val) => val, + None => category + .item_ids + .get(rand::thread_rng().gen_range(0..item_pool_len) as usize) + .unwrap(), + }; let mut extra_item_id: u32 = 0; let mut extra_item_count: u32 = 0; @@ -279,6 +370,7 @@ fn determine_gacha_result<'bin, 'conf>( .get(extra_items_policy_tag) .unwrap(); // TODO: apply_on_owned_count in a context with bag + // TODO: That's what RoleModel should do, not me. if extra_items_policy.apply_on_owned_count == 0 { extra_item_id = extra_items_policy.id; extra_item_count = extra_items_policy.count; @@ -396,4 +488,18 @@ fn update_discount<'bin, 'conf>( *usage += 1; } } + for (policy_tag, policy) in gachaconf.discount_policies.free_select_map.iter() { + if !target_pool.discount_policy_tags.contains(policy_tag) { + continue; + } + let status_bin = gacha_bin + .gacha_status_map + .get_mut(&target_pool.sharing_guarantee_info_category) + .unwrap(); + let progress = status_bin + .discount_usage_map + .get_mut(&policy.free_select_progress_record_tag) + .unwrap(); + *progress += 1; + } } diff --git a/nap_proto/out/_.rs b/nap_proto/out/_.rs index 447fe85..7bc0341 100644 --- a/nap_proto/out/_.rs +++ b/nap_proto/out/_.rs @@ -6844,7 +6844,7 @@ pub struct Pofhbffcjap { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Nhffnnompfh { +pub struct ChooseGachaUpScRsp { #[xor(4109)] #[prost(int32, tag = "2")] pub retcode: i32, @@ -17992,7 +17992,7 @@ pub mod gbcnocdacgf { #[prost(message, tag = "12")] Summonee(super::Endadpkgkid), #[prost(message, tag = "13")] - BuddyId(super::Ecjcmfjjgdp), + Buddy(super::Ecjcmfjjgdp), #[prost(message, tag = "14")] DropItem(super::Mfbjkggafmo), #[prost(message, tag = "15")] @@ -22295,7 +22295,7 @@ pub struct Gacha { pub gacha_info_list_webview: ::prost::alloc::string::String, #[xor(9009)] #[prost(uint32, tag = "419")] - pub ekjlhhdekka: u32, + pub chosen_up_item: u32, #[prost(string, tag = "923")] pub fjohnbicmce: ::prost::alloc::string::String, #[xor(1379)] @@ -24558,7 +24558,7 @@ pub struct Obpccjhnbpe { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Pahjnbjogon { +pub struct ChooseGachaUpCsReq { #[xor(3180)] #[prost(uint32, tag = "6")] pub item_id: u32, @@ -27531,7 +27531,7 @@ pub struct TipsInfo { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Ieimfkpmegp { +pub struct GachaFreeAgentCsReq { #[xor(378)] #[prost(uint32, tag = "12")] pub gacha_parent_schedule_id: u32, @@ -31687,7 +31687,7 @@ pub struct Amhlhmjgcpk { #[derive(proto_gen::XorFields)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct Idonpiailid { +pub struct GachaFreeAgentScRsp { #[xor(3883)] #[prost(int32, tag = "3")] pub retcode: i32, diff --git a/nap_proto/out/bin.rs b/nap_proto/out/bin.rs index 01bb4aa..f4ea087 100644 --- a/nap_proto/out/bin.rs +++ b/nap_proto/out/bin.rs @@ -117,6 +117,7 @@ pub struct MainCityModelBin { #[prost(uint32, tag = "3")] pub section_id: u32, } +/// The progress record of a specified rarity. All maps' keys are category_guarantee_policy_tag. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GachaProgressBin { @@ -129,12 +130,24 @@ pub struct GachaProgressBin { ::prost::alloc::string::String, u32, >, - /// The selected priority for a Chooseable category. + /// The selected priority (category) for a Chooseable category. #[prost(map = "string, string", tag = "3")] - pub category_chosen_guarantee_map: ::std::collections::HashMap< + pub categories_chosen_guarantee_category_map: ::std::collections::HashMap< ::prost::alloc::string::String, ::prost::alloc::string::String, >, + /// The selectedpriority (a specified item) for a Chooseable category. + #[prost(map = "string, uint32", tag = "4")] + pub categories_chosen_guarantee_item_map: ::std::collections::HashMap< + ::prost::alloc::string::String, + u32, + >, + /// The failure times for selected priority (a specified item). + #[prost(map = "string, uint32", tag = "5")] + pub categories_chosen_guarantee_progress_map: ::std::collections::HashMap< + ::prost::alloc::string::String, + u32, + >, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -- 2.34.1