1649 lines
55 KiB
TypeScript
1649 lines
55 KiB
TypeScript
import type { Client } from './client'
|
|
import { type Api, type UserItem, type UserMusicDetail } from './typings/api'
|
|
import { MusicDifficultyID } from './typings/api/base/MusicDifficultyID'
|
|
import { PlayComboFlagID } from './typings/api/base/PlayComboFlagID'
|
|
import { PlaySyncFlagID } from './typings/api/base/PlaySyncFlagID'
|
|
import { CardItem } from './typings/api/GetUserCardApi'
|
|
import { UserItemKind } from './typings/api/base/UserItem'
|
|
import { Playlog } from './typings/api/UploadUserPlaylogListApi'
|
|
import { LogoutType } from './typings/api/UserLogoutApi'
|
|
import { toLocalDateString, toLocalDateTimeString } from './utils/date'
|
|
import {
|
|
CharacterSet,
|
|
convertAchievementToScoreRank,
|
|
ScoreEntry,
|
|
ScoreSet,
|
|
ItemSet,
|
|
unpackPresent,
|
|
Mission
|
|
} from './containers'
|
|
import { AwaitableEventEmitter, AwaitableEventMap } from './utils/event'
|
|
|
|
type RequestAllApi =
|
|
| 'GetUserItemApi'
|
|
| 'GetUserCardApi'
|
|
| 'GetUserCourseApi'
|
|
| 'GetUserMusicApi'
|
|
| 'GetUserFavoriteItemApi'
|
|
| 'GetUserMapApi'
|
|
| 'GetUserLoginBonusApi'
|
|
|
|
export interface Present {
|
|
[UserItemKind.Plate]: ItemSet
|
|
[UserItemKind.Title]: ItemSet
|
|
[UserItemKind.Icon]: ItemSet
|
|
[UserItemKind.Music]: ItemSet
|
|
[UserItemKind.Character]: ItemSet
|
|
[UserItemKind.Partner]: ItemSet
|
|
[UserItemKind.Frame]: ItemSet
|
|
[UserItemKind.Ticket]: ItemSet
|
|
// NOTE: 若 Present 的 itemId 经解析后指向 UserItemKind.Present, UserItemKind.MusicMas, UserItemKind.MusicRem, UserItemKind.MusicSrg 则会直接忽略此礼物。
|
|
}
|
|
|
|
export interface SaveEventMap extends AwaitableEventMap<void> {
|
|
progress: [ProgressEvent]
|
|
}
|
|
|
|
export interface SaveProgress {
|
|
loaded: number
|
|
total: number
|
|
}
|
|
|
|
interface CacheMap {
|
|
root?: {
|
|
get data(): NonNullable<CacheMap['root.data']>
|
|
get card(): NonNullable<CacheMap['root.card']>
|
|
get character(): NonNullable<CacheMap['root.character']>
|
|
get item(): NonNullable<CacheMap['root.item']>
|
|
get course(): NonNullable<CacheMap['root.course']>
|
|
get charge(): NonNullable<CacheMap['root.charge']>
|
|
get favorite(): NonNullable<CacheMap['root.favorite']>
|
|
get ghost(): NonNullable<CacheMap['root.ghost']>
|
|
get map(): NonNullable<CacheMap['root.map']>
|
|
get loginBonus(): NonNullable<CacheMap['root.loginBonus']>
|
|
get region(): NonNullable<CacheMap['root.region']>
|
|
get recommendRateMusic(): NonNullable<CacheMap['root.recommendRateMusic']>
|
|
get recommendSelectMusic(): NonNullable<
|
|
CacheMap['root.recommendSelectMusic']
|
|
>
|
|
get option(): NonNullable<CacheMap['root.option']>
|
|
get extend(): NonNullable<CacheMap['root.extend']>
|
|
get rating(): NonNullable<CacheMap['root.rating']>
|
|
get score(): NonNullable<CacheMap['root.score']>
|
|
get activity(): NonNullable<CacheMap['root.activity']>
|
|
get mission(): NonNullable<CacheMap['root.mission']>
|
|
}
|
|
'root.item'?: {
|
|
get [UserItemKind.Plate](): NonNullable<CacheMap['root.item.plate']>
|
|
get [UserItemKind.Title](): NonNullable<CacheMap['root.item.title']>
|
|
get [UserItemKind.Partner](): NonNullable<CacheMap['root.item.partner']>
|
|
get [UserItemKind.Icon](): NonNullable<CacheMap['root.item.icon']>
|
|
get [UserItemKind.Present](): NonNullable<CacheMap['root.item.present']>
|
|
get [UserItemKind.Frame](): NonNullable<CacheMap['root.item.frame']>
|
|
get [UserItemKind.Ticket](): NonNullable<CacheMap['root.item.ticket']>
|
|
get [UserItemKind.Music](): NonNullable<CacheMap['root.item.music']>
|
|
get [UserItemKind.MusicMas](): NonNullable<CacheMap['root.item.musicMas']>
|
|
get [UserItemKind.MusicRem](): NonNullable<CacheMap['root.item.musicRem']>
|
|
// get musicSrg(): NonNullable<CacheMap['root.item.musicSrg']>
|
|
}
|
|
'root.item.present'?: Promise<Present>
|
|
'root.favorite'?: {
|
|
get icon(): NonNullable<CacheMap['root.favorite.icon']>
|
|
get plate(): NonNullable<CacheMap['root.favorite.plate']>
|
|
get title(): NonNullable<CacheMap['root.favorite.title']>
|
|
get character(): NonNullable<CacheMap['root.favorite.character']>
|
|
get frame(): NonNullable<CacheMap['root.favorite.frame']>
|
|
get music(): NonNullable<CacheMap['root.favorite.music']>
|
|
get rival(): NonNullable<CacheMap['root.favorite.rival']>
|
|
}
|
|
'root.data'?: Promise<
|
|
Api['GetUserDataApi'][1]['userData'] & { banState: number }
|
|
>
|
|
'root.card'?: Promise<{
|
|
card: NonNullable<Api['GetUserCardApi'][1]['userCardList']>
|
|
serialId: NonNullable<Api['GetUserCardApi'][1]['serialIdList']>
|
|
}>
|
|
'root.map'?: Promise<NonNullable<Api['GetUserMapApi'][1]['userMapList']>>
|
|
// TODO: Watch Modification
|
|
'root.character'?: Promise<CharacterSet>
|
|
'root.item.plate'?: Promise<ItemSet>
|
|
'root.item.title'?: Promise<ItemSet>
|
|
'root.item.partner'?: Promise<ItemSet>
|
|
'root.item.icon'?: Promise<ItemSet>
|
|
'root.item.frame'?: Promise<ItemSet>
|
|
'root.item.ticket'?: Promise<ItemSet>
|
|
'root.item.music'?: Promise<ItemSet>
|
|
'root.item.musicMas'?: Promise<ItemSet>
|
|
'root.item.musicRem'?: Promise<ItemSet>
|
|
// 'root.item.musicSrg'?: Promise<
|
|
// NonNullable<Api['GetUserItemApi'][1]['userItemList']>
|
|
// >
|
|
'root.ghost'?: Promise<Api['GetUserGhostApi'][1]['userGhostList']>
|
|
'root.favorite.icon'?: Promise<
|
|
Api['GetUserFavoriteApi'][1]['userFavorite']['itemIdList']
|
|
>
|
|
'root.favorite.plate'?: Promise<
|
|
Api['GetUserFavoriteApi'][1]['userFavorite']['itemIdList']
|
|
>
|
|
'root.favorite.title'?: Promise<
|
|
Api['GetUserFavoriteApi'][1]['userFavorite']['itemIdList']
|
|
>
|
|
'root.favorite.character'?: Promise<
|
|
Api['GetUserFavoriteApi'][1]['userFavorite']['itemIdList']
|
|
>
|
|
'root.favorite.frame'?: Promise<
|
|
Api['GetUserFavoriteApi'][1]['userFavorite']['itemIdList']
|
|
>
|
|
'root.favorite.music'?: Promise<
|
|
NonNullable<Api['GetUserFavoriteItemApi'][1]['userFavoriteItemList']>
|
|
>
|
|
'root.favorite.rival'?: Promise<
|
|
NonNullable<Api['GetUserFavoriteItemApi'][1]['userFavoriteItemList']>
|
|
>
|
|
'root.loginBonus'?: Promise<
|
|
NonNullable<Api['GetUserLoginBonusApi'][1]['userLoginBonusList']>
|
|
>
|
|
'root.course'?: Promise<
|
|
NonNullable<Api['GetUserCourseApi'][1]['userCourseList']>
|
|
>
|
|
'root.charge'?: Promise<
|
|
NonNullable<Api['GetUserChargeApi'][1]['userChargeList']>
|
|
>
|
|
'root.score'?: Promise<ScoreSet>
|
|
'root.score.reset'?: () => void
|
|
'root.rating'?: Promise<Api['GetUserRatingApi'][1]['userRating']>
|
|
'root.region'?: Promise<
|
|
NonNullable<Api['GetUserRegionApi'][1]['userRegionList']>
|
|
>
|
|
'root.recommendRateMusic'?: Promise<
|
|
Api['GetUserRecommendRateMusicApi'][1]['userRecommendRateMusicIdList']
|
|
>
|
|
'root.recommendSelectMusic'?: Promise<
|
|
Api['GetUserRecommendSelectMusicApi'][1]['userRecommendSelectionMusicIdList']
|
|
>
|
|
'root.option'?: Promise<Api['GetUserOptionApi'][1]['userOption']>
|
|
'root.extend'?: Promise<Api['GetUserExtendApi'][1]['userExtend']>
|
|
'root.activity'?: Promise<Api['GetUserActivityApi'][1]['userActivity']>
|
|
'root.mission'?: Promise<Mission>
|
|
}
|
|
|
|
export type PlaylogInit = Partial<Playlog> & {
|
|
musicId: number
|
|
level: MusicDifficultyID
|
|
achievement: number
|
|
tapCriticalPerfect: number
|
|
tapPerfect: number
|
|
tapGreat: number
|
|
tapGood: number
|
|
tapMiss: number
|
|
holdCriticalPerfect: number
|
|
holdPerfect: number
|
|
holdGreat: number
|
|
holdGood: number
|
|
holdMiss: number
|
|
slideCriticalPerfect: number
|
|
slidePerfect: number
|
|
slideGreat: number
|
|
slideGood: number
|
|
slideMiss: number
|
|
touchCriticalPerfect: number
|
|
touchPerfect: number
|
|
touchGreat: number
|
|
touchGood: number
|
|
touchMiss: number
|
|
breakCriticalPerfect: number
|
|
breakPerfect: number
|
|
breakGreat: number
|
|
breakGood: number
|
|
breakMiss: number
|
|
fastCount: number
|
|
lateCount: number
|
|
isCriticalDisp: boolean
|
|
comboStatus: PlayComboFlagID
|
|
syncStatus: PlaySyncFlagID
|
|
maxCombo: number
|
|
}
|
|
|
|
export function calculateDeluxscore(playlog: PlaylogInit) {
|
|
const tapDx =
|
|
playlog.tapCriticalPerfect * 3 + playlog.tapPerfect * 2 + playlog.tapGreat
|
|
const holdDx =
|
|
playlog.holdCriticalPerfect * 3 +
|
|
playlog.holdPerfect * 2 +
|
|
playlog.holdGreat
|
|
const slideDx =
|
|
playlog.slideCriticalPerfect * 3 +
|
|
playlog.slidePerfect * 2 +
|
|
playlog.slideGreat
|
|
const touchDx =
|
|
playlog.touchCriticalPerfect * 3 +
|
|
playlog.touchPerfect * 2 +
|
|
playlog.touchGreat
|
|
const breakDx =
|
|
playlog.breakCriticalPerfect * 3 +
|
|
playlog.breakPerfect * 2 +
|
|
playlog.breakGreat
|
|
return tapDx + holdDx + slideDx + touchDx + breakDx
|
|
}
|
|
|
|
export function splitPresent(data: UserItem[]): Present {
|
|
const present: Map<UserItemKind, UserItem[]> = new Map([
|
|
[UserItemKind.Plate, []],
|
|
[UserItemKind.Title, []],
|
|
[UserItemKind.Icon, []],
|
|
[UserItemKind.Music, []],
|
|
[UserItemKind.Character, []],
|
|
[UserItemKind.Partner, []],
|
|
[UserItemKind.Frame, []],
|
|
[UserItemKind.Ticket, []]
|
|
])
|
|
for (const item of data) {
|
|
if (item.itemKind === UserItemKind.Present && item.isValid) {
|
|
const [kind, id] = unpackPresent(item.itemId)
|
|
if (present.has(kind)) {
|
|
present.get(kind)?.push({
|
|
itemKind: kind,
|
|
itemId: id,
|
|
stock: item.stock,
|
|
isValid: item.isValid
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
[UserItemKind.Plate]: new ItemSet(
|
|
present.get(UserItemKind.Plate) ?? [],
|
|
UserItemKind.Plate,
|
|
true
|
|
),
|
|
[UserItemKind.Title]: new ItemSet(
|
|
present.get(UserItemKind.Title) ?? [],
|
|
UserItemKind.Title,
|
|
true
|
|
),
|
|
[UserItemKind.Icon]: new ItemSet(
|
|
present.get(UserItemKind.Icon) ?? [],
|
|
UserItemKind.Icon,
|
|
true
|
|
),
|
|
[UserItemKind.Music]: new ItemSet(
|
|
present.get(UserItemKind.Music) ?? [],
|
|
UserItemKind.Music,
|
|
true
|
|
),
|
|
[UserItemKind.Character]: new ItemSet(
|
|
present.get(UserItemKind.Character) ?? [],
|
|
UserItemKind.Character,
|
|
true
|
|
),
|
|
[UserItemKind.Partner]: new ItemSet(
|
|
present.get(UserItemKind.Partner) ?? [],
|
|
UserItemKind.Partner,
|
|
true
|
|
),
|
|
[UserItemKind.Frame]: new ItemSet(
|
|
present.get(UserItemKind.Frame) ?? [],
|
|
UserItemKind.Frame,
|
|
true
|
|
),
|
|
[UserItemKind.Ticket]: new ItemSet(
|
|
present.get(UserItemKind.Ticket) ?? [],
|
|
UserItemKind.Ticket,
|
|
true
|
|
)
|
|
}
|
|
}
|
|
|
|
export class User {
|
|
private _cache: CacheMap = {}
|
|
constructor(
|
|
public id: number,
|
|
public loginId: bigint | null,
|
|
public dateTime: Date | null,
|
|
public client: Client,
|
|
public isLogin: boolean,
|
|
public loginDateTime: Date,
|
|
public token: string = ''
|
|
) { }
|
|
|
|
private _useCache<T extends keyof CacheMap>(
|
|
key: T,
|
|
fn: () => NonNullable<CacheMap[T]>
|
|
) {
|
|
if (this._cache[key]) {
|
|
return this._cache[key]
|
|
}
|
|
const result = fn()
|
|
this._cache[key] = result
|
|
return result
|
|
}
|
|
|
|
private _queryCache<T extends keyof CacheMap>(
|
|
key: T
|
|
): CacheMap[T] | undefined {
|
|
return this._cache[key]
|
|
}
|
|
|
|
get data() {
|
|
// 被 sbga 大粪代码气笑了
|
|
const requestAll = async <T extends keyof Api & RequestAllApi>(
|
|
apiName: T,
|
|
data: Api[T][0]
|
|
): Promise<Api[T][1][]> => {
|
|
const result: Api[T][1][] = []
|
|
let index = 0n
|
|
while (true) {
|
|
const resp = (await this.client.request(
|
|
apiName,
|
|
Object.assign({}, data, {
|
|
nextIndex:
|
|
index === 0n
|
|
? BigInt(
|
|
(data as { nextIndex?: number | bigint }).nextIndex ?? 0n
|
|
)
|
|
: index
|
|
}),
|
|
{
|
|
userId: this.id
|
|
}
|
|
)) as Api[T][1] & {
|
|
nextIndex: number | bigint
|
|
}
|
|
result.push(resp)
|
|
index = BigInt(resp.nextIndex)
|
|
if (index === 0n) break
|
|
}
|
|
return result
|
|
}
|
|
const getItem = async (kind: UserItemKind) => {
|
|
return (
|
|
await requestAll('GetUserItemApi', {
|
|
userId: this.id,
|
|
nextIndex: BigInt(kind) * 10000000000n,
|
|
maxCount: 100
|
|
})
|
|
)
|
|
.map(v => v.userItemList ?? [])
|
|
.flat()
|
|
}
|
|
const getFavorite = async (
|
|
kind: number
|
|
): Promise<Api['GetUserFavoriteApi'][1]['userFavorite']['itemIdList']> => {
|
|
return (
|
|
await this.client.request(
|
|
'GetUserFavoriteApi',
|
|
{
|
|
userId: this.id,
|
|
itemKind: kind
|
|
},
|
|
{
|
|
userId: this.id
|
|
}
|
|
)
|
|
).userFavorite.itemIdList
|
|
}
|
|
const getFavoriteItem = async (kind: UserItemKind) => {
|
|
return (
|
|
await requestAll('GetUserFavoriteItemApi', {
|
|
userId: this.id,
|
|
kind,
|
|
isAllFavoriteItem: false,
|
|
nextIndex: 0n,
|
|
maxCount: 100
|
|
})
|
|
)
|
|
.map(v => v.userFavoriteItemList ?? [])
|
|
.flat()
|
|
}
|
|
/* eslint-disable-next-line @typescript-eslint/no-this-alias */
|
|
const self = this
|
|
return this._useCache('root', () => ({
|
|
get data() {
|
|
return self._useCache('root.data', async () => {
|
|
const res = await self.client.request(
|
|
'GetUserDataApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
if (res === null) throw new Error('User data is null')
|
|
const result = {
|
|
...res.userData,
|
|
banState: res.banState
|
|
}
|
|
const obj = result as unknown as Record<string, unknown>
|
|
delete obj['friendCode']
|
|
delete obj['nameplateId']
|
|
delete obj['cmLastEmoneyBrand']
|
|
delete obj['trophyId']
|
|
delete obj['cmLastEmoneyCredit']
|
|
return result
|
|
})
|
|
},
|
|
get card() {
|
|
return self._useCache(
|
|
'root.card',
|
|
async () =>
|
|
(
|
|
await requestAll('GetUserCardApi', {
|
|
userId: self.id,
|
|
nextIndex: 0,
|
|
maxCount: 10000
|
|
})
|
|
)
|
|
.map(v => [v.userCardList ?? [], v.serialIdList ?? []] as const)
|
|
.reduce((previous, current) => {
|
|
if (Array.isArray(previous)) {
|
|
return {
|
|
card: [...previous[0], ...current[0]],
|
|
serialId: [...previous[1], ...current[1]]
|
|
} as unknown as readonly [CardItem[], unknown[]]
|
|
} else {
|
|
const p = previous as unknown as {
|
|
card: CardItem[]
|
|
serialId: unknown[]
|
|
}
|
|
p.card.push(...current[0])
|
|
p.serialId.push(...current[1])
|
|
return previous
|
|
}
|
|
}) as unknown as {
|
|
card: CardItem[]
|
|
serialId: unknown[]
|
|
}
|
|
)
|
|
},
|
|
get character() {
|
|
return self._useCache(
|
|
'root.character',
|
|
async () =>
|
|
new CharacterSet(
|
|
(
|
|
await self.client.request(
|
|
'GetUserCharacterApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userCharacterList ?? []
|
|
) // TODO: Character Instance
|
|
)
|
|
},
|
|
get item() {
|
|
return self._useCache('root.item', () => ({
|
|
get [UserItemKind.Plate]() {
|
|
return self._useCache(
|
|
'root.item.plate',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Plate),
|
|
UserItemKind.Plate,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.Title]() {
|
|
return self._useCache(
|
|
'root.item.title',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Title),
|
|
UserItemKind.Title,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.Partner]() {
|
|
return self._useCache(
|
|
'root.item.partner',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Partner),
|
|
UserItemKind.Partner,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.Icon]() {
|
|
return self._useCache(
|
|
'root.item.icon',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Icon),
|
|
UserItemKind.Icon,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.Present]() {
|
|
return self._useCache('root.item.present', async () =>
|
|
splitPresent(await getItem(UserItemKind.Present))
|
|
)
|
|
},
|
|
get [UserItemKind.Frame]() {
|
|
return self._useCache(
|
|
'root.item.frame',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Frame),
|
|
UserItemKind.Frame,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.Ticket]() {
|
|
return self._useCache(
|
|
'root.item.ticket',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Ticket),
|
|
UserItemKind.Ticket,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.Music]() {
|
|
return self._useCache(
|
|
'root.item.music',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.Music),
|
|
UserItemKind.Music,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.MusicMas]() {
|
|
return self._useCache(
|
|
'root.item.musicMas',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.MusicMas),
|
|
UserItemKind.MusicMas,
|
|
false
|
|
)
|
|
)
|
|
},
|
|
get [UserItemKind.MusicRem]() {
|
|
return self._useCache(
|
|
'root.item.musicRem',
|
|
async () =>
|
|
new ItemSet(
|
|
await getItem(UserItemKind.MusicRem),
|
|
UserItemKind.MusicRem,
|
|
false
|
|
)
|
|
)
|
|
}
|
|
// get musicSrg() {
|
|
// return self._useCache('root.item.musicSrg', () =>
|
|
// getItem(UserItemKind.MusicSrg)
|
|
// )
|
|
// },
|
|
}))
|
|
},
|
|
get course() {
|
|
return self._useCache('root.course', async () =>
|
|
(
|
|
await requestAll('GetUserCourseApi', {
|
|
userId: self.id,
|
|
nextIndex: 0n
|
|
})
|
|
)
|
|
.map(v => v.userCourseList ?? [])
|
|
.flat()
|
|
)
|
|
},
|
|
get charge() {
|
|
return self._useCache(
|
|
'root.charge',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserChargeApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userChargeList ?? []
|
|
)
|
|
},
|
|
get favorite() {
|
|
return self._useCache('root.favorite', () => ({
|
|
get icon() {
|
|
return self._useCache('root.favorite.icon', () => getFavorite(1))
|
|
},
|
|
get plate() {
|
|
return self._useCache('root.favorite.plate', () => getFavorite(2))
|
|
},
|
|
get title() {
|
|
return self._useCache('root.favorite.title', () => getFavorite(3))
|
|
},
|
|
get character() {
|
|
return self._useCache('root.favorite.character', () =>
|
|
getFavorite(4)
|
|
)
|
|
},
|
|
get frame() {
|
|
return self._useCache('root.favorite.frame', () => getFavorite(5))
|
|
},
|
|
get music() {
|
|
return self._useCache('root.favorite.music', () =>
|
|
getFavoriteItem(1)
|
|
)
|
|
},
|
|
get rival() {
|
|
return self._useCache('root.favorite.rival', () =>
|
|
getFavoriteItem(2)
|
|
)
|
|
}
|
|
}))
|
|
},
|
|
get ghost() {
|
|
return self._useCache(
|
|
'root.ghost',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserGhostApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userGhostList // TODO: Ghost Instance
|
|
)
|
|
},
|
|
get map() {
|
|
return self._useCache('root.map', async () =>
|
|
(
|
|
await requestAll('GetUserMapApi', {
|
|
userId: self.id,
|
|
nextIndex: 0n,
|
|
maxCount: 10000
|
|
})
|
|
)
|
|
.map(v => v.userMapList ?? [])
|
|
.flat()
|
|
)
|
|
},
|
|
get loginBonus() {
|
|
return self._useCache('root.loginBonus', async () =>
|
|
(
|
|
await requestAll('GetUserLoginBonusApi', {
|
|
userId: self.id,
|
|
nextIndex: 0n,
|
|
maxCount: 10000
|
|
})
|
|
)
|
|
.map(v => v.userLoginBonusList ?? [])
|
|
.flat()
|
|
)
|
|
},
|
|
get region() {
|
|
return self._useCache(
|
|
'root.region',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserRegionApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userRegionList ?? []
|
|
)
|
|
},
|
|
get recommendRateMusic() {
|
|
return self._useCache(
|
|
'root.recommendRateMusic',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserRecommendRateMusicApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userRecommendRateMusicIdList
|
|
)
|
|
},
|
|
get recommendSelectMusic() {
|
|
return self._useCache(
|
|
'root.recommendSelectMusic',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserRecommendSelectMusicApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userRecommendSelectionMusicIdList
|
|
)
|
|
},
|
|
get option() {
|
|
return self._useCache('root.option', async () => {
|
|
const result = (
|
|
await self.client.request(
|
|
'GetUserOptionApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userOption
|
|
const obj = result as unknown as Record<string, unknown>
|
|
delete obj['tempoVolume']
|
|
return result
|
|
})
|
|
},
|
|
get extend() {
|
|
return self._useCache(
|
|
'root.extend',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserExtendApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userExtend
|
|
)
|
|
},
|
|
get rating() {
|
|
return self._useCache('root.rating', async () => {
|
|
const result = (
|
|
await self.client.request(
|
|
'GetUserRatingApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userRating
|
|
const obj = result.udemae as unknown as Record<string, unknown>
|
|
delete obj['MaxLoseNum']
|
|
delete obj['NpcLoseNum']
|
|
delete obj['NpcMaxLoseNum']
|
|
delete obj['NpcMaxWinNum']
|
|
delete obj['NpcTotalLoseNum']
|
|
delete obj['NpcTotalWinNum']
|
|
delete obj['NpcWinNum']
|
|
return result
|
|
})
|
|
},
|
|
get score() {
|
|
return self._useCache('root.score', async () => {
|
|
const res = ScoreSet.resettable(
|
|
(
|
|
await requestAll('GetUserMusicApi', {
|
|
userId: self.id,
|
|
nextIndex: 0n,
|
|
maxCount: 10000
|
|
})
|
|
)
|
|
.map(v => v.userMusicList ?? [])
|
|
.flat()
|
|
.map(v => v.userMusicDetailList ?? [])
|
|
.flat()
|
|
.map(v => {
|
|
const obj = v as unknown as Record<string, unknown>
|
|
delete obj['extNum2']
|
|
return v
|
|
})
|
|
)
|
|
self._cache['root.score.reset'] = res[1]
|
|
return res[0]
|
|
})
|
|
},
|
|
get activity() {
|
|
return self._useCache(
|
|
'root.activity',
|
|
async () =>
|
|
(
|
|
await self.client.request(
|
|
'GetUserActivityApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
).userActivity ?? {}
|
|
)
|
|
},
|
|
get mission() {
|
|
return self._useCache('root.mission', async () => {
|
|
const result = await self.client.request(
|
|
'GetUserMissionDataApi',
|
|
{
|
|
userId: self.id
|
|
},
|
|
{ userId: self.id }
|
|
)
|
|
return new Mission(result.userWeeklyData, result.userMissionDataList)
|
|
})
|
|
}
|
|
}))
|
|
}
|
|
|
|
async [Symbol.asyncDispose]() {
|
|
if (this.isLogin) {
|
|
await this.logout()
|
|
}
|
|
}
|
|
|
|
async refresh() {
|
|
if (this.isLogin) {
|
|
await this.logout()
|
|
}
|
|
|
|
const newInstance = await this.client.login(this.id, this.token, {
|
|
isContinue: true
|
|
})
|
|
this.isLogin = newInstance.isLogin
|
|
this.loginId = newInstance.loginId
|
|
this.token = newInstance.token
|
|
this.loginDateTime = newInstance.loginDateTime
|
|
}
|
|
|
|
async charge(options: {
|
|
kind: number
|
|
stock: number
|
|
price: number
|
|
purchaseDate?: Date
|
|
validDate?: Date
|
|
}) {
|
|
options.purchaseDate = options.purchaseDate ?? new Date()
|
|
// validDate 默认为 purchaseDate + 90 天的凌晨4点
|
|
options.validDate =
|
|
options.validDate ??
|
|
new Date(
|
|
options.purchaseDate.getFullYear(),
|
|
options.purchaseDate.getMonth(),
|
|
options.purchaseDate.getDate() + 90,
|
|
4,
|
|
0,
|
|
0,
|
|
0
|
|
)
|
|
await this.client.request(
|
|
'UpsertUserChargelogApi',
|
|
{
|
|
userId: this.id,
|
|
userCharge: {
|
|
chargeId: options.kind,
|
|
stock: options.stock,
|
|
purchaseDate: toLocalDateTimeString(options.purchaseDate),
|
|
validDate: toLocalDateTimeString(options.validDate)
|
|
},
|
|
userChargelog: {
|
|
chargeId: options.kind,
|
|
clientId: this.client.shortChipId,
|
|
regionId: this.client.place.region.id,
|
|
placeId: this.client.place.id,
|
|
price: options.price,
|
|
purchaseDate: toLocalDateTimeString(options.purchaseDate)
|
|
}
|
|
},
|
|
{
|
|
userId: this.id
|
|
}
|
|
)
|
|
}
|
|
|
|
async logout(type = LogoutType.Logout) {
|
|
if (!this.isLogin) {
|
|
throw new Error('Unable to logout: User already logged out')
|
|
}
|
|
if (!this.loginDateTime) {
|
|
throw new Error('Unable to logout: loginDateTime is null')
|
|
}
|
|
this.isLogin = false
|
|
const timestamp = Math.floor(this.loginDateTime.getTime() / 1000)
|
|
await this.client.request(
|
|
'UserLogoutApi',
|
|
{
|
|
userId: this.id,
|
|
accessCode: '',
|
|
regionId: this.client.place.region.id,
|
|
placeId: this.client.place.id,
|
|
clientId: this.client.shortChipId,
|
|
loginDateTime: timestamp,
|
|
type
|
|
},
|
|
{
|
|
userId: this.id
|
|
}
|
|
)
|
|
}
|
|
|
|
async update(options: {
|
|
romVersion: string
|
|
dataVersion: string
|
|
}): Promise<void> {
|
|
const data = await this.data.data
|
|
data.lastRomVersion = options.romVersion
|
|
data.lastDataVersion = options.dataVersion
|
|
}
|
|
|
|
/**
|
|
* Saves the current user's state including play log, user data, character information,
|
|
* and various game details to the backend.
|
|
*
|
|
* This asynchronous function aggregates new and existing play log data, generates additional
|
|
* play logs if necessary, and prepares comprehensive user data by merging character details,
|
|
* item lists, mission data, and various other game state information.
|
|
*
|
|
* The function performs the following main steps:
|
|
* - Verifies that the user is logged in; throws an error if not.
|
|
* - Generates random numbers and special values used in play logs.
|
|
* - Constructs play log entries by merging pre-existing log entries with newly generated ones.
|
|
* - Fetches and validates the user's selected characters, ensuring that necessary character
|
|
* information is retrieved or defaulted.
|
|
* - Aggregates and organizes several parts of the user data, including scores, achievements,
|
|
* and other related information.
|
|
* - Sequentially uploads each play log entry and then sends the complete user state update
|
|
* to the backend via client API requests.
|
|
*
|
|
* @param options - An optional object containing:
|
|
* - playlog: An array of play log entries (of types Playlog or PlaylogInit) to be used in the save.
|
|
* - eventMode: A boolean indicating if the event mode should be enabled.
|
|
* - freePlay: A boolean indicating if the free play mode should be enabled.
|
|
*
|
|
* @throws {Error} Throws if the user is not logged in correctly or if character information is
|
|
* missing or invalid.
|
|
*
|
|
* @returns A Promise that resolves when the save process (including all API requests) has completed.
|
|
*/
|
|
save(options?: {
|
|
playlog?: (Playlog | PlaylogInit)[]
|
|
eventMode?: boolean
|
|
freePlay?: boolean
|
|
}): AwaitableEventEmitter<void, SaveEventMap> {
|
|
const emitter = AwaitableEventEmitter.fromAsync(async () => {
|
|
const [
|
|
data,
|
|
character,
|
|
score,
|
|
option,
|
|
extend,
|
|
charge,
|
|
mission,
|
|
activity,
|
|
rating
|
|
] = await Promise.all([
|
|
this.data.data,
|
|
this.data.character,
|
|
this.data.score,
|
|
this.data.option,
|
|
this.data.extend,
|
|
this.data.charge,
|
|
this.data.mission,
|
|
this.data.activity,
|
|
this.data.rating
|
|
])
|
|
const generatePlaySpecial = () => {
|
|
// 生成一个 1 至 1037933 的随机整数
|
|
let num = (Math.floor(Math.random() * 1037933) + 1) * 2069
|
|
num += 1024 // += GameManager.CalcSpecialNum();
|
|
let num2 = 0
|
|
for (let i = 0; i < 32; i++) {
|
|
num2 <<= 1
|
|
num2 += num % 2
|
|
num >>= 1
|
|
}
|
|
return num2
|
|
}
|
|
const generatePlaylog = (detail: UserMusicDetail) => {
|
|
return {
|
|
level: detail.level,
|
|
achievement: 0,
|
|
musicId: detail.musicId,
|
|
tapCriticalPerfect: 0,
|
|
tapPerfect: 0,
|
|
tapGreat: 0,
|
|
tapGood: 0,
|
|
tapMiss: 1,
|
|
holdCriticalPerfect: 0,
|
|
holdPerfect: 0,
|
|
holdGreat: 0,
|
|
holdGood: 0,
|
|
holdMiss: 0,
|
|
slideCriticalPerfect: 0,
|
|
slidePerfect: 0,
|
|
slideGreat: 0,
|
|
slideGood: 0,
|
|
slideMiss: 0,
|
|
touchCriticalPerfect: 0,
|
|
touchPerfect: 0,
|
|
touchGreat: 0,
|
|
touchGood: 0,
|
|
touchMiss: 0,
|
|
breakCriticalPerfect: 0,
|
|
breakPerfect: 0,
|
|
breakGreat: 0,
|
|
breakGood: 0,
|
|
breakMiss: 0,
|
|
fastCount: 0,
|
|
lateCount: 0,
|
|
isCriticalDisp: false,
|
|
comboStatus: PlayComboFlagID.None,
|
|
syncStatus: PlaySyncFlagID.None,
|
|
maxCombo: 0,
|
|
totalCombo: 0,
|
|
maxSync: 0,
|
|
totalSync: 0
|
|
} satisfies PlaylogInit
|
|
}
|
|
interface CharacterData {
|
|
characterId1: number
|
|
characterAwakening1: number
|
|
characterLevel1: number
|
|
characterId2: number
|
|
characterAwakening2: number
|
|
characterLevel2: number
|
|
characterId3: number
|
|
characterAwakening3: number
|
|
characterLevel3: number
|
|
characterId4: number
|
|
characterAwakening4: number
|
|
characterLevel4: number
|
|
characterId5: number
|
|
characterAwakening5: number
|
|
characterLevel5: number
|
|
}
|
|
const convertVersionNumber = (version: string): number => {
|
|
// 1.41.00 -> 1041000
|
|
const parts = version.split('.')
|
|
if (parts.length !== 3) {
|
|
throw new Error(`Invalid version format: ${version}`)
|
|
}
|
|
const major = parseInt(parts[0], 10)
|
|
const minor = parseInt(parts[1], 10)
|
|
const patch = parseInt(parts[2], 10)
|
|
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
|
|
throw new Error(`Invalid version number: ${version}`)
|
|
}
|
|
return major * 1000000 + minor * 1000 + patch
|
|
}
|
|
const getUserSelectedCharacter = (
|
|
option: Partial<CharacterData>
|
|
): CharacterData => {
|
|
const raiseException = (message: string) => {
|
|
throw new Error(message)
|
|
}
|
|
const final: CharacterData = {
|
|
characterId1: 0,
|
|
characterAwakening1: 0,
|
|
characterLevel1: 0,
|
|
characterId2: 0,
|
|
characterAwakening2: 0,
|
|
characterLevel2: 0,
|
|
characterId3: 0,
|
|
characterAwakening3: 0,
|
|
characterLevel3: 0,
|
|
characterId4: 0,
|
|
characterAwakening4: 0,
|
|
characterLevel4: 0,
|
|
characterId5: 0,
|
|
characterAwakening5: 0,
|
|
characterLevel5: 0
|
|
}
|
|
for (let i = 0; i < 5; i++) {
|
|
const originalId = option[
|
|
`characterId${i + 1}` as keyof CharacterData
|
|
] as number | undefined
|
|
const originalAwakening = option[
|
|
`characterAwakening${i + 1}` as keyof CharacterData
|
|
] as number | undefined
|
|
const originalLevel = option[
|
|
`characterLevel${i + 1}` as keyof CharacterData
|
|
] as number | undefined
|
|
if (originalId !== undefined) {
|
|
Object.assign(final, {
|
|
[`characterId${i + 1}` as keyof CharacterData]: originalId,
|
|
[`characterAwakening${i + 1}` as keyof CharacterData]:
|
|
originalAwakening ??
|
|
character.get(originalId)?.awakening ??
|
|
raiseException(`Character ${i + 1} not found`),
|
|
[`characterLevel${i + 1}` as keyof CharacterData]:
|
|
originalLevel ??
|
|
character.get(originalId)?.level ??
|
|
raiseException(`Character ${i + 1} not found`)
|
|
})
|
|
} else if (
|
|
originalLevel !== undefined ||
|
|
originalAwakening !== undefined
|
|
) {
|
|
throw new Error(
|
|
`Character ${i + 1} ID is required if level or awakening is set`
|
|
)
|
|
} else {
|
|
const fill = character.get(data.charaSlot[i])
|
|
if (!fill) throw new Error(`Character ${i + 1} not found`)
|
|
Object.assign(final, {
|
|
[`characterId${i + 1}` as keyof CharacterData]: data.charaSlot[i],
|
|
[`characterAwakening${i + 1}` as keyof CharacterData]:
|
|
fill.awakening,
|
|
[`characterLevel${i + 1}` as keyof CharacterData]: fill.level
|
|
})
|
|
}
|
|
}
|
|
return final
|
|
}
|
|
if (!this.loginId || !this.dateTime)
|
|
throw new Error('Not logged in correctly')
|
|
// TODO: type hint
|
|
|
|
let playlog: (Playlog | PlaylogInit)[] = options?.playlog ?? []
|
|
const musicDetail = score.export()
|
|
const generatedPlaylog = musicDetail
|
|
.map(v => generatePlaylog(v.value))
|
|
.filter(v => {
|
|
return !playlog.some(
|
|
v2 => v2.level === v.level && v2.musicId === v.musicId
|
|
)
|
|
}) satisfies PlaylogInit[]
|
|
playlog = playlog.concat(generatedPlaylog)
|
|
if (playlog.length === 0) {
|
|
const ids = Array.from(score.keys())
|
|
const randomId = ids[Math.floor(Math.random() * ids.length)]
|
|
const entry = Object.entries(score.get(randomId)).find(([, v]) => {
|
|
return !!v
|
|
})
|
|
if (!entry)
|
|
throw new Error('At least one playlog is required for new user')
|
|
const level = Number(entry[0]) as MusicDifficultyID
|
|
const detail = entry[1] as ScoreEntry
|
|
const generated: UserMusicDetail = {
|
|
musicId: randomId,
|
|
achievement: detail.achievement,
|
|
comboStatus: detail.comboStatus,
|
|
syncStatus: detail.syncStatus,
|
|
deluxscoreMax: detail.deluxscoreMax,
|
|
level,
|
|
playCount: detail.playCount,
|
|
scoreRank: detail.scoreRank,
|
|
extNum1: detail.extNum1
|
|
}
|
|
musicDetail.push({ value: generated, isNew: false })
|
|
playlog.push(generatePlaylog(generated))
|
|
}
|
|
const playlogList: Playlog[] = await Promise.all(
|
|
Array.from(playlog.entries()).map(
|
|
async ([index, log]) =>
|
|
Object.assign(
|
|
{},
|
|
{
|
|
userId: 0,
|
|
orderId: 0,
|
|
playlogId: this.loginId,
|
|
version: convertVersionNumber(data.lastRomVersion),
|
|
placeId: this.client.place.id,
|
|
placeName: this.client.place.name,
|
|
loginDate: Math.floor(this.dateTime!.getTime() / 1000),
|
|
playDate: '',
|
|
userPlayDate: '',
|
|
type: 0,
|
|
trackNo: (index % 4) + 1,
|
|
vsMode: 0,
|
|
vsStatus: 0,
|
|
vsUserName: '',
|
|
vsUserRating: 0,
|
|
vsUserAchievement: 0,
|
|
vsUserGradeRank: 0,
|
|
vsRank: 0,
|
|
playerNum: 1,
|
|
playedUserId1: 0,
|
|
playedUserName1: '',
|
|
playedMusicLevel1: MusicDifficultyID.Basic,
|
|
playedUserId2: 0,
|
|
playedUserName2: '',
|
|
playedMusicLevel2: MusicDifficultyID.Basic,
|
|
playedUserId3: 0,
|
|
playedUserName3: '',
|
|
playedMusicLevel3: MusicDifficultyID.Basic,
|
|
...getUserSelectedCharacter(log),
|
|
scoreRank: convertAchievementToScoreRank(
|
|
log.achievement,
|
|
log.achievement > 1010000 &&
|
|
log.level === MusicDifficultyID.Utage
|
|
), // TODO: utage
|
|
isTap:
|
|
log.tapCriticalPerfect +
|
|
log.tapPerfect +
|
|
log.tapGreat +
|
|
log.tapGood +
|
|
log.tapMiss >
|
|
0,
|
|
isHold:
|
|
log.holdCriticalPerfect +
|
|
log.holdPerfect +
|
|
log.holdGreat +
|
|
log.holdGood +
|
|
log.holdMiss >
|
|
0,
|
|
isSlide:
|
|
log.slideCriticalPerfect +
|
|
log.slidePerfect +
|
|
log.slideGreat +
|
|
log.slideGood +
|
|
log.slideMiss >
|
|
0,
|
|
isTouch:
|
|
log.touchCriticalPerfect +
|
|
log.touchPerfect +
|
|
log.touchGreat +
|
|
log.touchGood +
|
|
log.touchMiss >
|
|
0,
|
|
isBreak:
|
|
log.breakCriticalPerfect +
|
|
log.breakPerfect +
|
|
log.breakGreat +
|
|
log.breakGood +
|
|
log.breakMiss >
|
|
0,
|
|
isFastLateDisp: true,
|
|
deluxscore: calculateDeluxscore(log as PlaylogInit),
|
|
isAchieveNewRecord: false,
|
|
isDeluxscoreNewRecord: false,
|
|
isClear: log.achievement >= 500000,
|
|
totalCombo:
|
|
log.tapCriticalPerfect +
|
|
log.tapPerfect +
|
|
log.tapGreat +
|
|
log.tapGood +
|
|
log.tapMiss +
|
|
(log.holdCriticalPerfect +
|
|
log.holdPerfect +
|
|
log.holdGreat +
|
|
log.holdGood +
|
|
log.holdMiss) +
|
|
(log.slideCriticalPerfect +
|
|
log.slidePerfect +
|
|
log.slideGreat +
|
|
log.slideGood +
|
|
log.slideMiss) +
|
|
(log.touchCriticalPerfect +
|
|
log.touchPerfect +
|
|
log.touchGreat +
|
|
log.touchGood +
|
|
log.touchMiss) +
|
|
(log.breakCriticalPerfect +
|
|
log.breakPerfect +
|
|
log.breakGreat +
|
|
log.breakGood +
|
|
log.breakMiss),
|
|
maxSync:
|
|
log.tapCriticalPerfect +
|
|
log.tapPerfect +
|
|
log.tapGreat +
|
|
log.tapGood +
|
|
log.tapMiss +
|
|
(log.holdCriticalPerfect +
|
|
log.holdPerfect +
|
|
log.holdGreat +
|
|
log.holdGood +
|
|
log.holdMiss) +
|
|
(log.slideCriticalPerfect +
|
|
log.slidePerfect +
|
|
log.slideGreat +
|
|
log.slideGood +
|
|
log.slideMiss) +
|
|
(log.touchCriticalPerfect +
|
|
log.touchPerfect +
|
|
log.touchGreat +
|
|
log.touchGood +
|
|
log.touchMiss) +
|
|
(log.breakCriticalPerfect +
|
|
log.breakPerfect +
|
|
log.breakGreat +
|
|
log.breakGood +
|
|
log.breakMiss),
|
|
totalSync: 0,
|
|
beforeRating: log.beforeRating ?? data.playerRating,
|
|
afterRating: log.afterRating ?? data.playerRating,
|
|
beforeGrade: log.beforeGrade ?? data.gradeRating,
|
|
afterGrade: log.afterGrade ?? data.gradeRating,
|
|
afterGradeRank: log.afterGradeRank ?? data.gradeRank,
|
|
beforeDeluxRating: log.beforeDeluxRating ?? data.playerRating,
|
|
afterDeluxRating: log.afterDeluxRating ?? data.playerRating,
|
|
isPlayTutorial: false,
|
|
isEventMode: false,
|
|
isFreedomMode: false,
|
|
playMode: 0,
|
|
isNewFree: false,
|
|
trialPlayAchievement: -1,
|
|
extNum1: 0,
|
|
extNum2: 0,
|
|
extNum4: 0,
|
|
extBool1:
|
|
log.achievement > 1010000 &&
|
|
log.level === MusicDifficultyID.Utage, // NOTE: This is a workaround for Buddy charts (バディ譜面)
|
|
extBool2: false
|
|
},
|
|
log
|
|
) satisfies Playlog
|
|
)
|
|
)
|
|
const groupedPlaylogList = []
|
|
const groupedMusicDetailList = []
|
|
for (let i = 0; i < playlogList.length; i += 4) {
|
|
groupedPlaylogList.push(playlogList.slice(i, i + 4))
|
|
groupedMusicDetailList.push(musicDetail.slice(i, i + 4))
|
|
}
|
|
let lastPlayDate = new Date()
|
|
// let lastPlayDate = new Date(
|
|
// fromLocalDateTimeString(data.lastPlayDate).getTime() + 1000
|
|
// )
|
|
for (let [index, group] of groupedPlaylogList.entries()) {
|
|
if (index > 0) {
|
|
const currentDate = new Date()
|
|
if (currentDate.getTime() - lastPlayDate.getTime() < 1000) {
|
|
await new Promise(resolve =>
|
|
setTimeout(
|
|
resolve,
|
|
1100 - (currentDate.getTime() - lastPlayDate.getTime())
|
|
)
|
|
)
|
|
}
|
|
await this.refresh()
|
|
lastPlayDate = new Date()
|
|
}
|
|
group = group.map(v =>
|
|
Object.assign({}, v, {
|
|
playlogId: this.loginId,
|
|
playDate: toLocalDateString(lastPlayDate),
|
|
userPlayDate: toLocalDateTimeString(lastPlayDate, true)
|
|
})
|
|
)
|
|
if (index === 0) {
|
|
await new Promise(resolve => setTimeout(resolve, 70000))
|
|
await this.client.request('UpsertUserAllApi', {
|
|
userId: this.id,
|
|
playlogId: this.loginId,
|
|
isEventMode: !!(options?.eventMode ?? false),
|
|
isFreePlay: !!(options?.freePlay ?? false),
|
|
loginDateTime: Math.floor(this.loginDateTime.getTime() / 1000),
|
|
userPlaylogList: group,
|
|
upsertUserAll: {
|
|
userData: [
|
|
Object.assign({}, data, {
|
|
dateTime: Math.floor(this.dateTime.getTime() / 1000),
|
|
lastGameId: this.client.codename,
|
|
lastPlaceId: this.client.place.id,
|
|
lastPlaceName: this.client.place.name,
|
|
lastRegionId: this.client.place.region.id,
|
|
lastRegionName: this.client.place.region.name[0],
|
|
lastClientId: this.client.shortChipId,
|
|
accessCode: '',
|
|
lastLoginDate: toLocalDateTimeString(this.dateTime, true), // TODO: compare
|
|
lastPlayDate: toLocalDateTimeString(lastPlayDate, true)
|
|
} satisfies Partial<
|
|
Api['UpsertUserAllApi'][0]['upsertUserAll']['userData'][0]
|
|
>) satisfies Api['UpsertUserAllApi'][0]['upsertUserAll']['userData'][0]
|
|
// data
|
|
],
|
|
userExtend: [extend],
|
|
userOption: [option],
|
|
...(await (async () => {
|
|
const character =
|
|
(await this._queryCache('root.character'))?.export() ?? []
|
|
return {
|
|
userCharacterList: character.map(v => v.value),
|
|
isNewCharacterList: character
|
|
.map(v => (v.isNew ? '1' : '0'))
|
|
.join('')
|
|
}
|
|
})()),
|
|
...(await (async () => {
|
|
const itemList = [
|
|
...((await this._queryCache('root.item.plate'))?.export() ??
|
|
[]),
|
|
...((await this._queryCache('root.item.title'))?.export() ??
|
|
[]),
|
|
...((await this._queryCache('root.item.partner'))?.export() ??
|
|
[]),
|
|
...((await this._queryCache('root.item.icon'))?.export() ??
|
|
[]),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Plate
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Title
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Icon
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Music
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Character
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Partner
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Frame
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.present'))?.[
|
|
UserItemKind.Ticket
|
|
]?.export() ?? []),
|
|
...((await this._queryCache('root.item.frame'))?.export() ??
|
|
[]),
|
|
...((await this._queryCache('root.item.ticket'))?.export() ??
|
|
[]),
|
|
...((await this._queryCache('root.item.music'))?.export() ??
|
|
[]),
|
|
...((
|
|
await this._queryCache('root.item.musicMas')
|
|
)?.export() ?? []),
|
|
...((
|
|
await this._queryCache('root.item.musicRem')
|
|
)?.export() ?? [])
|
|
]
|
|
return {
|
|
userItemList: itemList.map(v => v.value),
|
|
isNewItemList: itemList
|
|
.map(v => (v.isNew ? '1' : '0'))
|
|
.join('')
|
|
}
|
|
})()),
|
|
userGhost: [], // TODO: lazy update
|
|
userMapList: [], // TODO: lazy update
|
|
userLoginBonusList: [], // TODO: lazy update
|
|
userRatingList: [rating],
|
|
...(() => {
|
|
return {
|
|
userMusicDetailList: groupedMusicDetailList[index].map(
|
|
v => v.value
|
|
),
|
|
isNewMusicDetailList: groupedMusicDetailList[index]
|
|
.map(v => (v.isNew ? '1' : '0'))
|
|
.join('')
|
|
}
|
|
})(),
|
|
userCourseList: [], // await this.data.course, // TODO: lazy update
|
|
userFavoriteList: [], // TODO: lazy update
|
|
userFriendSeasonRankingList: [], // TODO: lazy update
|
|
userWeeklyData: mission.weekly,
|
|
userMissionDataList: mission.mission,
|
|
userIntimateList: [], // TODO: lazy update
|
|
isNewUserIntimateList: '',
|
|
userKaleidxScopeList: [], // TODO: lazy update
|
|
isNewKaleidxScopeList: '',
|
|
userShopItemStockList: [], // sega 程序员没有设置,故意的还是不小心的?
|
|
userGetPointList: [], // TODO: lazy update
|
|
userTradeItemList: [], // TODO: lazy update
|
|
userFavoritemusicList: [], // TODO: lazy update
|
|
isNewFavoritemusicList: '',
|
|
userChargeList: charge.map(v => ({
|
|
chargeId: v.chargeId,
|
|
stock: v.stock,
|
|
purchaseDate: v.purchaseDate,
|
|
validDate: v.validDate
|
|
})),
|
|
userActivityList: [activity], // TODO: slice activity data & activity container
|
|
userGamePlaylogList: [
|
|
{
|
|
playlogId: this.loginId,
|
|
version: data.lastRomVersion,
|
|
playDate: toLocalDateTimeString(lastPlayDate, true),
|
|
playMode: 0,
|
|
useTicketId: -1,
|
|
playCredit: options?.freePlay ? 0 : 1,
|
|
playTrack: group.length,
|
|
clientId: this.client.shortChipId,
|
|
isPlayTutorial: false,
|
|
isEventMode: !!(options?.eventMode ?? false),
|
|
isNewFree: false,
|
|
playCount: 0,
|
|
playSpecial: generatePlaySpecial(),
|
|
playOtherUserId: 0
|
|
}
|
|
],
|
|
user2pPlaylog: {
|
|
userId1: 0,
|
|
userId2: 0,
|
|
userName1: '',
|
|
userName2: '',
|
|
regionId: 0,
|
|
placeId: 0,
|
|
user2pPlaylogDetailList: []
|
|
},
|
|
// 对应的槽位变更: 0, 对应的槽位追加: 1
|
|
// TODO: 根据变更计算字段
|
|
isNewMapList: '',
|
|
isNewLoginBonusList: '',
|
|
isNewCourseList: '',
|
|
isNewFavoriteList: '',
|
|
isNewFriendSeasonRankingList: ''
|
|
}
|
|
} satisfies Api['UpsertUserAllApi'][0], {
|
|
userId: this.id
|
|
})
|
|
} else {
|
|
await new Promise(resolve => setTimeout(resolve, 70000))
|
|
await this.client.request('UpsertUserAllApi', {
|
|
userId: this.id,
|
|
playlogId: this.loginId,
|
|
isEventMode: !!(options?.eventMode ?? false),
|
|
isFreePlay: !!(options?.freePlay ?? false),
|
|
loginDateTime: Math.floor(this.loginDateTime.getTime() / 1000),
|
|
userPlaylogList: group,
|
|
upsertUserAll: {
|
|
userData: [
|
|
Object.assign({}, data, {
|
|
dateTime: Math.floor(this.dateTime.getTime() / 1000),
|
|
lastGameId: this.client.codename,
|
|
lastPlaceId: this.client.place.id,
|
|
lastPlaceName: this.client.place.name,
|
|
lastRegionId: this.client.place.region.id,
|
|
lastRegionName: this.client.place.region.name[0],
|
|
lastClientId: this.client.shortChipId,
|
|
accessCode: '',
|
|
lastLoginDate: toLocalDateTimeString(this.dateTime, true),
|
|
lastPlayDate: toLocalDateTimeString(lastPlayDate, true)
|
|
} satisfies Partial<
|
|
Api['UpsertUserAllApi'][0]['upsertUserAll']['userData'][0]
|
|
>) satisfies Api['UpsertUserAllApi'][0]['upsertUserAll']['userData'][0]
|
|
// data
|
|
],
|
|
userExtend: [extend],
|
|
userOption: [option],
|
|
userCharacterList: [],
|
|
isNewCharacterList: '',
|
|
userItemList: [],
|
|
isNewItemList: '',
|
|
userGhost: [],
|
|
userMapList: [],
|
|
userLoginBonusList: [],
|
|
userRatingList: [rating],
|
|
...(() => {
|
|
return {
|
|
userMusicDetailList: groupedMusicDetailList[index].map(
|
|
v => v.value
|
|
),
|
|
isNewMusicDetailList: groupedMusicDetailList[index]
|
|
.map(v => (v.isNew ? '1' : '0'))
|
|
.join('')
|
|
}
|
|
})(),
|
|
userCourseList: [],
|
|
userFavoriteList: [],
|
|
userFriendSeasonRankingList: [],
|
|
userWeeklyData: mission.weekly,
|
|
userMissionDataList: mission.mission,
|
|
userIntimateList: [],
|
|
isNewUserIntimateList: '',
|
|
userKaleidxScopeList: [],
|
|
isNewKaleidxScopeList: '',
|
|
userShopItemStockList: [],
|
|
userGetPointList: [],
|
|
userTradeItemList: [],
|
|
userFavoritemusicList: [],
|
|
isNewFavoritemusicList: '',
|
|
userChargeList: charge.map(v => ({
|
|
chargeId: v.chargeId,
|
|
stock: v.stock,
|
|
purchaseDate: v.purchaseDate,
|
|
validDate: v.validDate
|
|
})),
|
|
userActivityList: [activity],
|
|
userGamePlaylogList: [
|
|
{
|
|
playlogId: this.loginId,
|
|
version: data.lastRomVersion,
|
|
playDate: toLocalDateTimeString(lastPlayDate, true),
|
|
playMode: 0,
|
|
useTicketId: -1,
|
|
playCredit: options?.freePlay ? 0 : 1,
|
|
playTrack: group.length,
|
|
clientId: this.client.shortChipId,
|
|
isPlayTutorial: false,
|
|
isEventMode: !!(options?.eventMode ?? false),
|
|
isNewFree: false,
|
|
playCount: 0,
|
|
playSpecial: generatePlaySpecial(),
|
|
playOtherUserId: 0
|
|
}
|
|
],
|
|
user2pPlaylog: {
|
|
userId1: 0,
|
|
userId2: 0,
|
|
userName1: '',
|
|
userName2: '',
|
|
regionId: 0,
|
|
placeId: 0,
|
|
user2pPlaylogDetailList: []
|
|
},
|
|
isNewMapList: '',
|
|
isNewLoginBonusList: '',
|
|
isNewCourseList: '',
|
|
isNewFavoriteList: '',
|
|
isNewFriendSeasonRankingList: ''
|
|
}
|
|
} satisfies Api['UpsertUserAllApi'][0], { userId: this.id })
|
|
}
|
|
emitter.emit('progress', {
|
|
loaded: index + 1,
|
|
total: groupedPlaylogList.length
|
|
})
|
|
}
|
|
this._cache['root.score.reset']?.()
|
|
})
|
|
return emitter
|
|
}
|
|
}
|