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] 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; + } + } +}