Skip to content
This repository has been archived by the owner on Nov 17, 2024. It is now read-only.

EnkaAPI // Add MiHoMoAPI mirror server support. #172

Merged
merged 9 commits into from
Apr 22, 2024
1 change: 1 addition & 0 deletions Features/System/View/ThanksView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct ThanksView: View {
Text(verbatim: "SwifterSwift\nhttps://github.com/SwifterSwift/SwifterSwift")
Text(verbatim: "Defaults - Sindre Sorhus\nhttps://github.com/sindresorhus/Defaults")
Text(verbatim: "Enka API - Enka Network\nhttps://enka.network/?hsr")
Text(verbatim: "MiHoMo Origin API Mirror - MiHoMo\nhttps://github.com/Mar-7th/March7th-Docs")
}
.font(.caption)
Divider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ public struct ResIcon: View {
public let imageHandler: (Image) -> Image

public var body: some View {
#if os(OSX)
if let image = NSImage(contentsOfFile: path) {
imageHandler(Image(nsImage: image))
} else {
AsyncImage(url: .init(fileURLWithPath: path)) { image in
imageHandler(image)
} placeholder: {
placeholder()
}
}
#else
if let image = UIImage(contentsOfFile: path) {
imageHandler(Image(uiImage: image))
} else {
Expand All @@ -36,6 +47,7 @@ public struct ResIcon: View {
placeholder()
}
}
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,13 @@
self.localizedName = theDB.locTable[nameHash] ?? "EnkaId: \(fetched.tid)"
self.trainedLevel = fetched.level
self.refinement = fetched.rank
self.basicProps = fetched.flat.props.compactMap { currentRecord in
self.basicProps = fetched.getFlat(theDB: theDB).props.compactMap { currentRecord in
if let theType = EnkaHSR.PropertyType(rawValue: currentRecord.type) {
return PropertyPair(theDB: theDB, type: theType, value: currentRecord.value)
}
return nil
}
// TODO: 目前先忽略那些没有图标的词条,回头单独再订做一套图标。

Check warning on line 249 in Packages/HBEnkaAPI/Sources/HBEnkaAPI/DataSummarySupport/AvatarSummarized.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Todo Violation: TODOs should be resolved (目前先忽略那些没有图标的词条,回头单独再订做一套图标。) (todo)
self.specialProps = theDB.meta.equipmentSkill.query(
id: enkaId, stage: fetched.rank
).filter(\.key.hasPropIcon).map { key, value in
Expand Down Expand Up @@ -295,7 +295,8 @@
self.enkaId = fetched.tid
self.commonInfo = theCommonInfo
self.paramDataFetched = fetched
let props: [PropertyPair] = fetched.flat.props.compactMap { currentRecord in
guard let flat = fetched.getFlat(theDB: theDB) else { return nil }
let props: [PropertyPair] = flat.props.compactMap { currentRecord in
if let theType = EnkaHSR.PropertyType(rawValue: currentRecord.type) {
return PropertyPair(theDB: theDB, type: theType, value: currentRecord.value, isArtifact: true)
}
Expand All @@ -304,12 +305,15 @@
guard let theMainProp = props.first else { return nil }
self.mainProp = theMainProp
self.subProps = Array(props.dropFirst())
self.setID = flat.setID
}

// MARK: Public

/// Unique Artifact ID, defining its Rarity, Set Suite, and Body Part.
public let enkaId: Int
/// Artifact Set ID.
public let setID: Int
/// Common information fetched from EnkaDB.
public let commonInfo: EnkaHSR.DBModels.Artifact
/// Data from Enka query result profile.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,18 @@ extension EnkaHSR.QueryRelated.DetailInfo.Avatar {

// Panel: Base Props from the Weapon.

let baseMetaWeapon = theDB.meta.equipment[equipment.tid.description]?[equipment.promotionRank.description]
guard let baseMetaWeapon = baseMetaWeapon else { return nil }
panel.maxHP += baseMetaWeapon.baseHP
panel.attack += baseMetaWeapon.baseAttack
panel.defence += baseMetaWeapon.baseDefence
panel.maxHP += baseMetaWeapon.hpAdd * Double(equipInfo.trainedLevel - 1)
panel.attack += baseMetaWeapon.attackAdd * Double(equipInfo.trainedLevel - 1)
panel.defence += baseMetaWeapon.defenceAdd * Double(equipInfo.trainedLevel - 1)
let equipFlat = equipment.getFlat(theDB: theDB)
panel.maxHP += equipFlat.props.first { EnkaHSR.PropertyType(rawValue: $0.type) == .baseHP }?.value ?? 0
panel.attack += equipFlat.props.first { EnkaHSR.PropertyType(rawValue: $0.type) == .baseAttack }?.value ?? 0
panel.defence += equipFlat.props.first { EnkaHSR.PropertyType(rawValue: $0.type) == .baseDefence }?.value ?? 0

// Panel: Handle all additional props

// Panel: - Additional Props from the Weapon.

let weaponSpecialProps: [EnkaHSR.AvatarSummarized.PropertyPair] = equipInfo.specialProps

// Panel: 来自天赋树的面板加成。
// English: Base and Additional Props from the Skill Tree.
// Panel: Base and Additional Props from the Skill Tree.

let skillTreeProps: [EnkaHSR.AvatarSummarized.PropertyPair] = skillTreeList.compactMap { currentNode in
if currentNode.level == 1 {
Expand All @@ -82,7 +77,7 @@ extension EnkaHSR.QueryRelated.DetailInfo.Avatar {
let artifactSetProps: [EnkaHSR.AvatarSummarized.PropertyPair] = {
var resultPairs = [EnkaHSR.AvatarSummarized.PropertyPair]()
var setIDCounters: [Int: Int] = [:]
artifactsInfo.map(\.paramDataFetched.flat.setID).forEach { setIDCounters[$0, default: 0] += 1 }
artifactsInfo.map(\.setID).forEach { setIDCounters[$0, default: 0] += 1 }
setIDCounters.forEach { setId, count in
guard count >= 2 else { return }
let x = theDB.meta.relic.setSkill.query(id: setId, stage: 2).map {
Expand All @@ -103,7 +98,7 @@ extension EnkaHSR.QueryRelated.DetailInfo.Avatar {
let allProps = skillTreeProps + weaponSpecialProps + artifactProps + artifactSetProps
panel.triageAndHandle(theDB: theDB, allProps, element: mainInfo.element)

// Panel: 将最终面板转成输出物件要用到的格式。
// Panel: Final Output.

let propPair = panel.converted(theDB: theDB, element: mainInfo.element)

Expand Down Expand Up @@ -239,3 +234,52 @@ extension EnkaHSR.AvatarSummarized.PropertyPair {
}
}
}

extension EnkaHSR.QueryRelated.DetailInfo.ArtifactItem {
public func getFlat(theDB: EnkaHSR.EnkaDB) -> EnkaHSR.QueryRelated.DetailInfo.ArtifactItem.Flat? {
var result = [EnkaHSR.QueryRelated.DetailInfo.Prop]()
guard let matchedArtifact = theDB.artifacts[tid.description] else { return nil }
let mainAffix = theDB.meta.relic.mainAffix[
matchedArtifact.mainAffixGroup.description
]?[mainAffixId.description]
if let mainAffix = mainAffix {
result.append(
.init(
type: mainAffix.property.rawValue,
value: mainAffix.baseValue + mainAffix.levelAdd * Double(level ?? 0)
)
)
}
subAffixList.forEach { sub in
guard let subAffix = theDB.meta.relic.subAffix[
matchedArtifact.subAffixGroup.description
]?[sub.affixId.description] else { return }
result.append(
.init(
type: subAffix.property.rawValue,
value: subAffix.baseValue * Double(sub.cnt) + subAffix.stepValue * Double(sub.step ?? 0)
)
)
}
return .init(
props: result,
setName: matchedArtifact.setID,
setID: matchedArtifact.setID
)
}
}

extension EnkaHSR.QueryRelated.DetailInfo.Equipment {
public func getFlat(theDB: EnkaHSR.EnkaDB) -> EnkaHSR.QueryRelated.DetailInfo.EquipmentFlat {
var result = [EnkaHSR.QueryRelated.DetailInfo.Prop]()
if let table = theDB.meta.equipment[tid.description]?[(promotion ?? 0).description] {
let summedHP = table.baseHP + table.hpAdd * (Double(level) - 1)
let summedATK = table.baseAttack + table.attackAdd * (Double(level) - 1)
let summedDEF = table.baseDefence + table.defenceAdd * (Double(level) - 1)
result.append(.init(type: EnkaHSR.PropertyType.baseHP.rawValue, value: summedHP))
result.append(.init(type: EnkaHSR.PropertyType.baseAttack.rawValue, value: summedATK))
result.append(.init(type: EnkaHSR.PropertyType.baseDefence.rawValue, value: summedDEF))
}
return .init(props: result, name: theDB.weapons[tid.description]?.equipmentName.hash ?? 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension EnkaHSR.QueryRelated {

public struct QueriedProfile: Codable, Hashable {
public let detailInfo: DetailInfo?
public let uid: String
public let uid: String?
public let message: String?
}

Expand Down Expand Up @@ -40,24 +40,31 @@ extension EnkaHSR.QueryRelated {
self.isDisplayAvatar = isDisplayAvatar
self.platform = platform
self.avatarDetailList = avatarDetailList
self.assistAvatarList = []
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uid = try container.decode(Int.self, forKey: .uid)
self.nickname = try container.decode(String?.self, forKey: .nickname) ?? "@Nanashibito"
self.level = try container.decode(Int?.self, forKey: .level) ?? 0
self.friendCount = try container.decode(Int?.self, forKey: .friendCount) ?? 0
self.signature = try container.decode(String?.self, forKey: .signature) ?? ""
self.recordInfo = try container.decode(RecordInfo?.self, forKey: .recordInfo)
self.headIcon = try container.decode(Int?.self, forKey: .headIcon) ?? 1310
self.worldLevel = try container.decode(Int?.self, forKey: .worldLevel) ?? 0
self.isDisplayAvatar = try container.decode(Bool?.self, forKey: .isDisplayAvatar) ?? false
self.avatarDetailList = try container.decode([Avatar]?.self, forKey: .avatarDetailList) ?? []
self.nickname = (try? container.decode(String.self, forKey: .nickname)) ?? "@Nanashibito"
self.level = (try? container.decode(Int.self, forKey: .level)) ?? 0
self.friendCount = (try? container.decode(Int.self, forKey: .friendCount)) ?? 0
self.signature = (try? container.decode(String.self, forKey: .signature)) ?? ""
self.recordInfo = try? container.decode(RecordInfo.self, forKey: .recordInfo)
self.headIcon = (try? container.decode(Int.self, forKey: .headIcon)) ?? 1310
self.worldLevel = (try? container.decode(Int.self, forKey: .worldLevel)) ?? 0
self.isDisplayAvatar = (try? container.decode(Bool.self, forKey: .isDisplayAvatar)) ?? false
// 在这个阶段就将 assistAvatarList 的内容并入到 avatarDetailList 内。
let avatarListPrimary = (try? container.decode([Avatar].self, forKey: .assistAvatarList)) ?? []
var avatarListSecondary = (try? container.decode([Avatar].self, forKey: .avatarDetailList)) ?? []
let filteredCharIDs = avatarListPrimary.map(\.avatarId)
avatarListSecondary.removeAll { filteredCharIDs.contains($0.avatarId) }
self.assistAvatarList = []
self.avatarDetailList = avatarListPrimary + avatarListSecondary
do {
self.platform = .init(rawValue: try container.decode(Int?.self, forKey: .platform) ?? 0) ?? .editor
self.platform = .init(rawValue: (try container.decode(Int?.self, forKey: .platform)) ?? 0) ?? .editor
} catch {
self.platform = .init(string: try container.decode(String?.self, forKey: .platform) ?? "EDITOR")
self.platform = .init(string: (try? container.decode(String?.self, forKey: .platform)) ?? "EDITOR")
}
}

Expand All @@ -72,7 +79,7 @@ extension EnkaHSR.QueryRelated {
public let isDisplayAvatar: Bool
public let platform: PlatformType
public let avatarDetailList: [Avatar]
// public let assistAvatarList: [Avatar]
public let assistAvatarList: [Avatar]
}
}

Expand Down Expand Up @@ -107,8 +114,8 @@ extension EnkaHSR.QueryRelated.DetailInfo {
// MARK: Public

public let rank, level, tid: Int
// public let flat: EquipmentFlat? // UNAVAILABLE_IN_MIHOMO_ORIGIN_RESULTS.
public let promotion: Int?
public let flat: EquipmentFlat

// Promotion, guarded
public var promotionRank: Int {
Expand All @@ -122,7 +129,7 @@ extension EnkaHSR.QueryRelated.DetailInfo {
case level
case tid
case promotion
case flat = "_flat"
// case flat = "_flat" // UNAVAILABLE_IN_MIHOMO_ORIGIN_RESULTS.
}
}

Expand Down Expand Up @@ -163,7 +170,7 @@ extension EnkaHSR.QueryRelated.DetailInfo {
public let level: Int?
public let subAffixList: [SubAffixItem]
public let mainAffixId, tid: Int
public let flat: ArtifactItem.Flat
// public let flat: ArtifactItem.Flat? // UNAVAILABLE_IN_MIHOMO_ORIGIN_RESULTS.
public let exp: Int?

// MARK: Internal
Expand All @@ -174,7 +181,7 @@ extension EnkaHSR.QueryRelated.DetailInfo {
case subAffixList
case mainAffixId
case tid
case flat = "_flat"
// case flat = "_flat" // UNAVAILABLE_IN_MIHOMO_ORIGIN_RESULTS.
case exp
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ extension EnkaHSR.Sputnik {
)
throw EnkaHSR.QueryRelated.Exception.refreshTooFast(dateWhenRefreshable: date)
} else {
let enkaOfficial = EnkaHSR.HostType.profileQueryURLHeader + uid
let server = EnkaHSR.HostType(uid: uid)
let enkaOfficial = server.profileQueryURLHeader + uid
// swiftlint:disable force_unwrapping
let url = URL(string: enkaOfficial)!
// swiftlint:enable force_unwrapping
Expand Down
25 changes: 20 additions & 5 deletions Packages/HBEnkaAPI/Sources/HBEnkaAPI/HBEnkaAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,25 @@ extension EnkaHSR {
case mainlandChina = 0
case enkaGlobal = 1

// MARK: Public
// MARK: Lifecycle

public static var profileQueryURLHeader: String {
// MicroGG 目前不支持星穹铁道的资料查询。
"https://enka.network/api/hsr/uid/"
public init(uid: String) {
var theUID = uid
while theUID.count > 9 {
theUID = theUID.dropFirst().description
}
guard let initial = theUID.first, let initialInt = Int(initial.description) else {
self = .enkaGlobal
return
}
switch initialInt {
case 1 ... 5: self = .mainlandChina
default: self = .enkaGlobal
}
}

// MARK: Public

public var enkaDBSourceURLHeader: String {
switch self {
case .mainlandChina: return "https://gitcode.net/SHIKISUEN/Enka-API-docs/-/raw/master/"
Expand All @@ -134,7 +146,10 @@ extension EnkaHSR {
}

public var profileQueryURLHeader: String {
Self.profileQueryURLHeader
switch self {
case .mainlandChina: return "https://api.mihomo.me/sr_info/"
case .enkaGlobal: return "https://enka.network/api/hsr/uid/"
}
}

public func viceVersa() -> Self {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// (c) 2023 and onwards Pizza Studio (GPL v3.0 License).
// ====================
// This code is released under the GPL v3.0 License (SPDX-License-Identifier: GPL-3.0)

import Foundation

extension MiHoMo.QueriedProfile {
public static func fetch(lang: String = "en", uid: String) async throws -> Self {
let langTag = lang.isEmpty ? "" : "&lang=\(lang)"
let urlLink = "https://api.mihomo.me/sr_info_parsed/\(uid)?version=v2\(langTag)"
// swiftlint:disable force_unwrapping
let url = URL(string: urlLink)!
// swiftlint:enable force_unwrapping
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let requestResult = try decoder.decode(
Self.self,
from: data
)
return requestResult
}

public static func fetchEnka(uid: String) async throws -> EnkaHSR.QueryRelated.QueriedProfile {
let urlLink = "https://api.mihomo.me/sr_info/\(uid)"
// swiftlint:disable force_unwrapping
let url = URL(string: urlLink)!
// swiftlint:enable force_unwrapping
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
let decoder = JSONDecoder()
let requestResult = try decoder.decode(
EnkaHSR.QueryRelated.QueriedProfile.self,
from: data
)
return requestResult
}
}
Loading
Loading