Skip to content

Commit

Permalink
Merge pull request #23 from lgou2w/zenless
Browse files Browse the repository at this point in the history
Zenless Zone Zero
  • Loading branch information
lgou2w authored Jul 6, 2024
2 parents 2bcd802 + c0e6cad commit 3e7aa97
Show file tree
Hide file tree
Showing 65 changed files with 708 additions and 153 deletions.
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

0 comments on commit 3e7aa97

Please sign in to comment.