Gacha System implementation

## Abstract

This PR implements

- The Gacha System for client. Player can pull in defined pools with a similar experience to zenless & mihoyo gacha, and these status can be saved through player save and recovered.
- `player kick` command in extra. Command `player kick [player_uid] (optional: [reason_id])` can be used to kick a player to offline.

## Support list

- Similar probability to mihoyo gacha
- QingYi & Jane Doe's Agent & W-Engine banner
- Standard Banner:
  - Get a S Agent in the first 50 pulls
  - Get a 20% discount for the first 5 10-pulls
  - Choose a S Agent once you pulled for 300
- ~~Bangboo Banner~~ (not working due to other reasons)
  - Choosing your demanded S bangboo for 100% UP

## Principle

- A complex configuration file `gacha.jsonc` is introduced to define all behaviours.
- Gacha status is saved via `GachaModelBin`.

For more informations about the config and save, an article is available [here](https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/).

## Known issues

- You can not see any bangboos in the collection when choosing Bangboo.
- **Specifically for 1.1 Beta**, performing gacha may lead to random client issues, including:
  - The TVs showing rarity ending up in blank after its flash.
  - Game become black screen.
  - If clicking 'Skip' but not fast enough, game'll stuck and not able to do anything. You may try to click 'Skip' scarcely when 'REC' shows, or after all animations has stopped.

Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com>
Reviewed-on: #1
Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com>
Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
This commit is contained in:
YYHEggEgg 2024-08-04 11:41:23 +00:00 committed by xeon
parent 220fbc42f0
commit 55b7ed3beb
19 changed files with 3164 additions and 112 deletions

File diff suppressed because it is too large Load diff

View file

@ -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"] }

View file

@ -0,0 +1,255 @@
use std::collections::{HashMap, HashSet};
use chrono::{prelude::Local, DateTime};
use proto::GachaAddedItemType;
use serde::{Deserialize, Deserializer};
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<ProbabilityPoint>,
// 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<f64>,
}
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<f64> = 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<String>,
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 FreeSelectItem {
pub milestones: Vec<u32>,
pub rarity: u32,
pub category_tags: Vec<String>,
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<String, TenPullDiscount>,
pub must_gain_item_map: HashMap<String, MustGainItem>,
pub advanced_guarantee_map: HashMap<String, AdvancedGuarantee>,
pub free_select_map: HashMap<String, FreeSelectItem>,
}
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<u32>,
pub category_weight: u32,
#[serde(default, deserialize_with = "from_str")]
pub item_type: GachaAddedItemType,
}
pub fn from_str<'de, D>(deserializer: D) -> Result<GachaAddedItemType, D::Error>
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)]
pub struct GachaAvailableItemsInfo {
pub rarity: u32,
#[serde(default)]
pub extra_items_policy_tags: Vec<String>,
pub categories: HashMap<String, GachaCategoryInfo>,
pub probability_model_tag: String,
#[serde(default)]
pub category_guarantee_policy_tags: Vec<String>,
}
#[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<Local>,
pub end_time: DateTime<Local>,
#[serde(default)]
pub discount_policy_tags: Vec<String>,
pub sharing_guarantee_info_category: String,
pub gacha_items: Vec<GachaAvailableItemsInfo>,
}
impl CharacterGachaPool {
pub fn is_still_open(&self, now: &DateTime<Local>) -> bool {
self.start_time <= *now && *now <= self.end_time
}
pub fn post_configure(&mut self, probability_model_map: &HashMap<String, ProbabilityModel>) {
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<CharacterGachaPool>,
pub probability_model_map: HashMap<String, ProbabilityModel>,
pub category_guarantee_policy_map: HashMap<String, CategoryGuaranteePolicy>,
pub extra_items_policy_map: HashMap<String, ExtraItemsPolicy>,
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);
}
}
}

45
nap_data/src/gacha/mod.rs Normal file
View file

@ -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<GachaConfiguration> = OnceLock::new();
#[derive(Deserialize, Debug)]
pub struct EventGraphConfig {
pub event_id: u32,
pub actions: Vec<ActionConfig>,
}
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::<GachaConfiguration>(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()
}

View file

@ -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<HashMap<u32, u64>> = 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(())
}

View file

@ -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"] }

View file

@ -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)";
}
}

View file

@ -1,5 +1,5 @@
use data::tables::{AvatarBaseID, ProcedureConfigID};
use proto::PlayerSyncScNotify;
use proto::{DisconnectReason, DisconnectScNotify, PlayerSyncScNotify};
use crate::ServerState;
@ -126,3 +126,45 @@ pub async fn procedure(
"successfully changed procedure_id to {procedure_id:?}"
))
}
pub async fn kick(
args: ArgSlice<'_>,
state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
const USAGE: &str = "Usage: player kick [player_uid]";
if args.len() > 2 {
return Ok(USAGE.to_string());
}
let uid = args[0].parse::<u32>()?;
let default_reason = DisconnectReason::ServerKick.into();
let reason = match args.get(1) {
Some(arg) => match arg.parse::<i32>() {
Ok(val) => val,
Err(_err) => default_reason,
},
None => default_reason,
};
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."))
}
}

View file

@ -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"),
},
}
}

View file

@ -1,13 +1,620 @@
use std::{
cmp::min,
collections::{
hash_map::Entry::{Occupied, Vacant},
HashSet,
},
};
use data::{
gacha::{gacha_config::*, global_gacha_config},
tables::{AvatarBaseID, WeaponID},
};
use super::*;
use chrono::{DateTime, Local};
use proto::*;
pub async fn on_get_gacha_data(
_session: &NetSession,
_player: &mut Player,
req: GetGachaDataCsReq,
) -> NetResult<GetGachaDataScRsp> {
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, &Local::now())),
})
}
}
pub async fn on_do_gacha(
_session: &NetSession,
_player: &mut Player,
req: DoGachaCsReq,
) -> NetResult<DoGachaScRsp> {
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,
&req.gacha_parent_schedule_id,
&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::RetFail.into(),
..Default::default()
});
};
let target_pool = target_pool.unwrap();
// 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;
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;
// 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()
});
} else {
// TODO: cost resource
}
let mut gain_item_list: Vec<GainItemInfo> = 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.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) {
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()
});
pull_count -= 1;
gacha_model.gacha_bin.gacha_records.push(pull_result);
}
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, &pull_time)),
cost_item_count: req.cost_item_count,
})
}
pub async fn on_gacha_free_agent(
_session: &NetSession,
_player: &mut Player,
req: GachaFreeAgentCsReq,
) -> NetResult<GachaFreeAgentScRsp> {
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<ChooseGachaUpScRsp> {
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,
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 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
.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;
}
} 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<u32> = vec![];
let mut up_a_item_list: Vec<u32> = vec![];
let mut free_select_item_list: Vec<u32> = vec![];
let mut chooseable_up_list: Vec<u32> = 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<String>> = 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![];
}
// }
}
}
let need_item_info_list: Vec<NeedItemInfo> = vec![NeedItemInfo {
need_item_id: target_pool.cost_item_id,
need_item_count: 1,
}];
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,
start_timestamp: target_pool.start_time.timestamp(),
end_timestamp: target_pool.end_time.timestamp(),
discount_avaliable_num,
discount_ten_roll_prize,
advanced_s_guarantee,
s_guarantee,
a_guarantee,
need_item_info_list,
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, now: &DateTime<Local>) -> GachaData {
let gachaconf = global_gacha_config();
let gacha_bin = &_player.gacha_model.gacha_bin;
let mut gacha_list: Vec<Gacha> = vec![];
for target_pool in gachaconf.character_gacha_pool_list.iter() {
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: 6167,
gacha_pool: Some(GachaPool { gacha_list }),
..GachaData::default()
}
}
fn get_gacha_pool<'conf>(
character_gacha_pool_list: &'conf Vec<CharacterGachaPool>,
gacha_parent_schedule_id: &u32,
pull_time: &DateTime<Local>,
) -> 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
}

View file

@ -119,6 +119,9 @@ req_handlers! {
event_graph::RunEventGraph;
quest::BeginArchiveBattleQuest;
quest::FinishArchiveQuest;
gacha::DoGacha;
gacha::ChooseGachaUp;
gacha::GachaFreeAgent;
}
notify_handlers! {

View file

@ -0,0 +1,505 @@
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};
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 };
result.post_deserialize()
}
pub fn to_bin(&self) -> GachaModelBin {
self.gacha_bin.clone()
}
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,
);
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() {
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
}
pub fn perform_pull_pool<'bin, 'conf>(
&'bin mut self,
pull_time: &DateTime<Local>,
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,
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);
result
}
}
fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>(
map: &'a mut HashMap<K, V, S>,
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<String> = HashSet::new();
// First of all, if there's a chooseable category and
// 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() {
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<Local>,
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 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;
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
// 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;
}
}
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),
item_type: category.item_type.into(),
}
}
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;
}
}
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;
}
}

View file

@ -0,0 +1,4 @@
mod gacha_model;
mod record;
pub use gacha_model::GachaModel;

View file

View file

@ -1,5 +1,6 @@
pub mod battle;
mod enums;
pub mod gacha;
pub mod game;
pub mod item;
pub mod math;

View file

@ -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()
}
}
@ -58,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);
}

View file

@ -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
}
}

View file

@ -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)]
@ -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,
@ -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<u32>,
#[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<GachaData>,
#[prost(message, repeated, tag = "13")]
pub mjajbddaemm: ::prost::alloc::vec::Vec<Depahhdodeb>,
pub gain_item_list: ::prost::alloc::vec::Vec<GainItemInfo>,
#[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<u32>,
#[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<Feanepokfam>,
pub bapbocgilep: ::prost::alloc::vec::Vec<NeedItemInfo>,
#[prost(map = "uint32, uint32", tag = "10")]
pub maeegjdknkg: ::std::collections::HashMap<u32, u32>,
#[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<Feanepokfam>,
pub need_item_info_list: ::prost::alloc::vec::Vec<NeedItemInfo>,
#[prost(message, repeated, tag = "6")]
pub mplcofohjnl: ::prost::alloc::vec::Vec<Feanepokfam>,
pub mplcofohjnl: ::prost::alloc::vec::Vec<NeedItemInfo>,
#[xor(5947)]
#[prost(uint32, tag = "5")]
pub r#type: u32,
#[prost(message, repeated, tag = "8")]
pub dnadnehoogk: ::prost::alloc::vec::Vec<Feanepokfam>,
pub dnadnehoogk: ::prost::alloc::vec::Vec<NeedItemInfo>,
#[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)]
@ -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")]
@ -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<u32>,
pub chooseable_up_list: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "15")]
pub nfngmliibdf: ::prost::alloc::vec::Vec<u32>,
pub up_s_item_list: ::prost::alloc::vec::Vec<u32>,
#[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<Feanepokfam>,
pub need_item_info_list: ::prost::alloc::vec::Vec<NeedItemInfo>,
#[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<u32>,
pub free_select_item_list: ::prost::alloc::vec::Vec<u32>,
#[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<u32>,
pub up_a_item_list: ::prost::alloc::vec::Vec<u32>,
#[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,29 +22273,29 @@ 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<u32>,
#[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,
pub chosen_up_item: u32,
#[prost(string, tag = "923")]
pub fjohnbicmce: ::prost::alloc::string::String,
#[xor(1379)]
@ -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<Dpgipnmocnj>,
#[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,
@ -24558,13 +24558,13 @@ 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,
#[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,
@ -27531,10 +27531,10 @@ 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 dcealmadfgi: u32,
pub gacha_parent_schedule_id: u32,
#[xor(1946)]
#[prost(uint32, tag = "7")]
pub avatar_id: u32,
@ -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)]
@ -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<Bnpkpfadbdd>,
#[xor(15963)]
#[prost(int32, tag = "13")]
pub eijhjbplhih: i32,
pub need_item_id: i32,
}
#[derive(proto_gen::CmdID)]
#[cmdid(816)]
@ -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,
@ -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<u32, u32>,
pub extra_items: ::std::collections::HashMap<u32, u32>,
#[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,
@ -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<Self> {
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,
}
}

View file

@ -117,6 +117,92 @@ 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 {
/// 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 (category) for a Chooseable category.
#[prost(map = "string, string", tag = "3")]
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)]
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<u32, GachaProgressBin>,
#[prost(message, optional, tag = "5")]
pub extra_item_bin: ::core::option::Option<GachaExtraItemBin>,
#[prost(enumeration = "GachaAddedItemType", tag = "6")]
pub item_type: i32,
}
#[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<u32, GachaProgressBin>,
#[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<GachaRecordBin>,
#[prost(uint32, tag = "3")]
pub random_number: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PlayerDataBin {
@ -130,4 +216,38 @@ pub struct PlayerDataBin {
pub item_model: ::core::option::Option<ItemModelBin>,
#[prost(message, optional, tag = "5")]
pub main_city_model: ::core::option::Option<MainCityModelBin>,
#[prost(message, optional, tag = "6")]
pub gacha_model: ::core::option::Option<GachaModelBin>,
}
#[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<Self> {
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,
}
}
}