Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zenless Zone Zero #23

Merged
merged 14 commits into from
Jul 6, 2024
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

## 功能

- [x] 支持 **`原神`****`崩坏:星穹铁道`** 游戏抽卡记录。
- [x] 支持 **`原神`****`崩坏:星穹铁道`** 和 **`绝区零`** 游戏抽卡记录。
- [x] 管理游戏的多个账号。
- [x] 获取游戏的抽卡链接。
- [x] 获取抽卡记录并保存到本地数据库文件。
Expand Down Expand Up @@ -67,16 +67,14 @@
## 特别感谢

* [UIGF organization](https://uigf.org)
* [DGP-Studio/Snap.Hutao](https://github.com/DGP-Studio/Snap.Hutao)
* [YuehaiTeam/cocogoat](https://github.com/YuehaiTeam/cocogoat)
* [vikiboss/gs-helper](https://github.com/vikiboss/gs-helper)

## 协议

> [!NOTE]
> MIT OR Apache-2.0 **仅供个人学习交流使用。请勿用于任何商业或违法违规用途。**
>
> 本软件不会收集任何用户数据。所产生的数据(包括但不限于使用数据、抽卡数据、账号信息等)均保存在用户本地。
> 本软件不会向您索要任何关于 ©miHoYo 账户的账号密码信息,也不会收集任何用户数据。所产生的数据(包括但不限于使用数据、抽卡数据、UID 信息等)均保存在用户本地。

### 部分资源文件

Expand All @@ -89,4 +87,5 @@
* [src/assets/images/Logo.png](src/assets/images/Logo.png)
* [src/assets/images/genshin/*](src/assets/images/genshin)
* [src/assets/images/starrail/*](src/assets/images/starrail)
* [src/assets/images/zzz/*](src/assets/images/zzz)
* [src-tauri/icons/*](src-tauri/icons/)
8 changes: 7 additions & 1 deletion src-tauri/src/gacha/impl_genshin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use super::{
GameDataDirectoryFinder,
};
use crate::error::Result;
use crate::storage::entity_account::AccountFacet;
use async_trait::async_trait;
use reqwest::Client as Reqwest;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -114,7 +115,12 @@ impl GachaRecordFetcher for GenshinGacha {
end_id: Option<&str>,
) -> Result<Option<Vec<Self::Target>>> {
let response = fetch_gacha_records::<GenshinGachaRecordPagination>(
reqwest, ENDPOINT, gacha_url, gacha_type, end_id,
reqwest,
&AccountFacet::Genshin,
ENDPOINT,
gacha_url,
gacha_type,
end_id,
)
.await?;

Expand Down
8 changes: 7 additions & 1 deletion src-tauri/src/gacha/impl_starrail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use super::{
GameDataDirectoryFinder,
};
use crate::error::Result;
use crate::storage::entity_account::AccountFacet;
use async_trait::async_trait;
use reqwest::Client as Reqwest;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -116,7 +117,12 @@ impl GachaRecordFetcher for StarRailGacha {
end_id: Option<&str>,
) -> Result<Option<Vec<Self::Target>>> {
let response = fetch_gacha_records::<StarRailGachaRecordPagination>(
reqwest, ENDPOINT, gacha_url, gacha_type, end_id,
reqwest,
&AccountFacet::StarRail,
ENDPOINT,
gacha_url,
gacha_type,
end_id,
)
.await?;

Expand Down
147 changes: 147 additions & 0 deletions src-tauri/src/gacha/impl_zzz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use super::utilities::{
fetch_gacha_records, lookup_cognosphere_dir, lookup_gacha_urls_from_endpoint, lookup_mihoyo_dir,
lookup_path_line_from_keyword, lookup_valid_cache_data_dir,
};
use super::{
GachaRecord, GachaRecordFetcher, GachaRecordFetcherChannel, GachaUrl, GachaUrlFinder,
GameDataDirectoryFinder,
};
use crate::error::Result;
use crate::storage::entity_account::AccountFacet;
use async_trait::async_trait;
use reqwest::Client as Reqwest;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::cmp::Ordering;
use std::path::{Path, PathBuf};

#[derive(Default, Deserialize)]
pub struct ZenlessZoneZeroGacha;

/// Game Directory

impl GameDataDirectoryFinder for ZenlessZoneZeroGacha {
fn find_game_data_directories(&self) -> Result<Vec<PathBuf>> {
let cognosphere_dir = lookup_cognosphere_dir();
let mihoyo_dir = lookup_mihoyo_dir();
let mut directories = Vec::new();

// TODO: Untested
const INTERNATIONAL_PLAYER_LOG: &str = "Zenless Zone Zero/Player.log";
const INTERNATIONAL_DIR_KEYWORD: &str = "/ZenlessZoneZero_Data/";

let mut player_log = cognosphere_dir.join(INTERNATIONAL_PLAYER_LOG);
if let Some(directory) = lookup_path_line_from_keyword(player_log, INTERNATIONAL_DIR_KEYWORD)? {
directories.push(directory);
}

const CHINESE_PLAYER_LOG: &str = "绝区零/Player.log";
const CHINESE_DIR_KEYWORD: &str = "/ZenlessZoneZero_Data/";

player_log = mihoyo_dir.join(CHINESE_PLAYER_LOG);
if let Some(directory) = lookup_path_line_from_keyword(player_log, CHINESE_DIR_KEYWORD)? {
directories.push(directory);
}

Ok(directories)
}
}

/// Gacha Url

const ENDPOINT: &str = "/api/getGachaLog?";

impl GachaUrlFinder for ZenlessZoneZeroGacha {
fn find_gacha_urls<P: AsRef<Path>>(&self, game_data_dir: P) -> Result<Vec<GachaUrl>> {
// See: https://github.com/lgou2w/HoYo.Gacha/issues/10
let cache_data_dir = lookup_valid_cache_data_dir(game_data_dir)?;
lookup_gacha_urls_from_endpoint(cache_data_dir, ENDPOINT, true)
}
}

/// Gacha Record

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ZenlessZoneZeroGachaRecord {
pub id: String,
pub uid: String,
pub gacha_id: String,
pub gacha_type: String,
pub item_id: String,
pub count: String,
pub time: String,
pub name: String,
pub lang: String,
pub item_type: String,
pub rank_type: String,
}

impl GachaRecord for ZenlessZoneZeroGachaRecord {
fn id(&self) -> &str {
&self.id
}

fn as_any(&self) -> &dyn Any {
self
}
}

impl PartialOrd for ZenlessZoneZeroGachaRecord {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.id.partial_cmp(&other.id)
}
}

/// Gacha Record Fetcher

#[allow(unused)]
#[derive(Deserialize)]
pub(crate) struct ZenlessZoneZeroGachaRecordPagination {
page: String,
size: String,
// total: String,
list: Vec<ZenlessZoneZeroGachaRecord>,
region: String,
region_time_zone: i8,
}

#[async_trait]
impl GachaRecordFetcher for ZenlessZoneZeroGacha {
type Target = ZenlessZoneZeroGachaRecord;

async fn fetch_gacha_records(
&self,
reqwest: &Reqwest,
gacha_url: &str,
gacha_type: Option<&str>,
end_id: Option<&str>,
) -> Result<Option<Vec<Self::Target>>> {
let response = fetch_gacha_records::<ZenlessZoneZeroGachaRecordPagination>(
reqwest,
&AccountFacet::ZenlessZoneZero,
ENDPOINT,
gacha_url,
gacha_type,
end_id,
)
.await?;

Ok(response.data.map(|pagination| pagination.list))
}

async fn fetch_gacha_records_any_uid(
&self,
reqwest: &Reqwest,
gacha_url: &str,
) -> Result<Option<String>> {
let result = self
.fetch_gacha_records(reqwest, gacha_url, None, None)
.await?;
Ok(result.and_then(|gacha_records| gacha_records.first().map(|record| record.uid.clone())))
}
}

#[async_trait]
impl GachaRecordFetcherChannel<ZenlessZoneZeroGachaRecord> for ZenlessZoneZeroGacha {
type Fetcher = Self;
}
2 changes: 2 additions & 0 deletions src-tauri/src/gacha/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod declare;
mod impl_genshin;
mod impl_starrail;
mod impl_zzz;
mod plugin;
mod utilities;

Expand All @@ -10,4 +11,5 @@ pub mod uigf;
pub use declare::*;
pub use impl_genshin::*;
pub use impl_starrail::*;
pub use impl_zzz::*;
pub use plugin::*;
38 changes: 38 additions & 0 deletions src-tauri/src/gacha/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::srgf;
use super::uigf;
use super::utilities::{create_default_reqwest, find_gacha_url_and_validate_consistency};
use super::ZenlessZoneZeroGacha;
use super::{
create_fetcher_channel, GachaRecordFetcherChannelFragment, GachaUrlFinder,
GameDataDirectoryFinder, GenshinGacha, StarRailGacha,
Expand All @@ -23,6 +24,7 @@ async fn find_game_data_directories(facet: AccountFacet) -> Result<Vec<PathBuf>>
match facet {
AccountFacet::Genshin => GenshinGacha.find_game_data_directories(),
AccountFacet::StarRail => StarRailGacha.find_game_data_directories(),
AccountFacet::ZenlessZoneZero => ZenlessZoneZeroGacha.find_game_data_directories(),
}
}

Expand All @@ -41,6 +43,11 @@ async fn find_gacha_url(
let gacha_urls = StarRailGacha.find_gacha_urls(game_data_dir)?;
find_gacha_url_and_validate_consistency(&StarRailGacha, &facet, &uid, &gacha_urls).await?
}
AccountFacet::ZenlessZoneZero => {
let gacha_urls = ZenlessZoneZeroGacha.find_gacha_urls(game_data_dir)?;
find_gacha_url_and_validate_consistency(&ZenlessZoneZeroGacha, &facet, &uid, &gacha_urls)
.await?
}
};

Ok(gacha_url.to_string())
Expand Down Expand Up @@ -102,6 +109,25 @@ async fn pull_all_gacha_records(
)
.await?
}
AccountFacet::ZenlessZoneZero => {
create_fetcher_channel(
ZenlessZoneZeroGacha,
reqwest,
ZenlessZoneZeroGacha,
gacha_url,
gacha_type_and_last_end_id_mappings,
|fragment| async {
window.emit(&event_channel, &fragment)?;
if save_to_storage {
if let GachaRecordFetcherChannelFragment::Data(data) = fragment {
storage.save_zzz_gacha_records(&data).await?;
}
}
Ok(())
},
)
.await?
}
}

Ok(())
Expand Down Expand Up @@ -141,6 +167,10 @@ async fn import_gacha_records(
let gacha_records = srgf::convert_srgf_to_offical(&mut srgf)?;
storage.save_starrail_gacha_records(&gacha_records).await
}
AccountFacet::ZenlessZoneZero => {
// TODO: Import ZZZ Gacha Records
todo!("Import ZZZ Gacha Records")
}
}
}

Expand All @@ -167,6 +197,10 @@ async fn export_gacha_records(
let (primary, format) = match facet {
AccountFacet::Genshin => ("原神祈愿记录", "UIGF"),
AccountFacet::StarRail => ("星穹铁道跃迁记录", "SRGF"),
AccountFacet::ZenlessZoneZero => {
// TODO: Export ZZZ Gacha Records
todo!("Export ZZZ Gacha Records")
}
};
let filename = format!(
"{}_{}_{}_{uid}_{time}.json",
Expand Down Expand Up @@ -205,6 +239,10 @@ async fn export_gacha_records(
let srgf = srgf::SRGF::new(uid, lang, time_zone, &now, srgf_list)?;
srgf.to_writer(writer, false)?;
}
AccountFacet::ZenlessZoneZero => {
// TODO: Export ZZZ Gacha Records
todo!("Export ZZZ Gacha Records")
}
}

Ok(filename)
Expand Down
14 changes: 11 additions & 3 deletions src-tauri/src/gacha/utilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ pub(super) struct GachaResponse<T> {

pub(super) async fn fetch_gacha_records<T: Sized + DeserializeOwned + Send>(
reqwest: &Reqwest,
facet: &AccountFacet,
endpoint: &str,
gacha_url: &str,
gacha_type: Option<&str>,
Expand All @@ -266,14 +267,21 @@ pub(super) async fn fetch_gacha_records<T: Sized + DeserializeOwned + Send>(
.into_owned()
.collect();

let gacha_type_field: &'static str = if facet == &AccountFacet::ZenlessZoneZero {
"real_gacha_type"
} else {
"gacha_type"
};

let origin_gacha_type = queries
.get("gacha_type")
.get(gacha_type_field)
.cloned()
.ok_or(Error::IllegalGachaUrl)?;

let origin_end_id = queries.get("end_id").cloned();
let gacha_type = gacha_type.unwrap_or(&origin_gacha_type);

queries.remove("gacha_type");
queries.remove(gacha_type_field);
queries.remove("page");
queries.remove("size");
queries.remove("begin_id");
Expand All @@ -285,7 +293,7 @@ pub(super) async fn fetch_gacha_records<T: Sized + DeserializeOwned + Send>(
.query_pairs_mut()
.append_pair("page", "1")
.append_pair("size", "20")
.append_pair("gacha_type", gacha_type);
.append_pair(gacha_type_field, gacha_type);

if let Some(end_id) = end_id.or(origin_end_id.as_deref()) {
url.query_pairs_mut().append_pair("end_id", end_id);
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/storage/entity_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub enum AccountFacet {
#[sea_orm(string_value = "starrail")]
#[serde(rename = "starrail")]
StarRail,
#[sea_orm(string_value = "zzz")]
#[serde(rename = "zzz")]
ZenlessZoneZero,
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
Expand Down
Loading
Loading