diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0a4d56cd..6009e621 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -3,6 +3,7 @@ on: branches: - main - dev + - refactor workflow_dispatch: jobs: diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index adffd02d..0aa800cc 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/CHANGELOG.md b/CHANGELOG.md index f1bd4347..d324ef20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ - [TODO] 对局页面添加一个截图到剪贴板的按钮。 -# v1.3.0 DRAFT +- [CHANGED] 移除对局中自动路由功能。 + +# v1.3.0 Sakurako DRAFT ## 修复 diff --git a/README.md b/README.md index eec2548a..64534b20 100644 --- a/README.md +++ b/README.md @@ -123,27 +123,18 @@ node-gyp build League Akari 的实现参考了许多现有的优秀开源项目,这些项目为软件的部分模块开发提供了清晰的思路指导,特此表示感谢。❤️ -- [LCU API 文档 1](https://www.mingweisamuel.com/lcu-schema/tool/#/) - -- [LCU 文档 - League of Legends LCU and Riot Client API Docs](https://github.com/KebsCS/lcu-and-riotclient-api) - -- [Community Dragon](https://www.communitydragon.org/documentation/assets) - -- [Pengu Loader - ✨ The ultimate JavaScript plugin loader, build your unmatched LoL Client.](https://github.com/PenguLoader/PenguLoader) - -- [Seraphine - 英雄联盟战绩查询工具](https://github.com/Zzaphkiel/Seraphine) - -- [lol-helper - 英雄联盟工具,LCU API,一键喊话,战绩查询,一键发送战绩,更改段位显示,更改背景页,牛马/上等马/下等马,彩虹屁,禁用英雄 ,秒选英雄,解锁炫彩皮肤等](https://github.com/4379711/lol-helper) - -- [Joi - 一个英雄联盟助手工具](https://github.com/watchingfun/Joi) - -- [fix-lcu-window - 解决《英雄联盟》客户端异常窗口大小的问题。](https://github.com/LeagueTavern/fix-lcu-window) - -- [vscode-league-respawn-timer - An extension to display League of Legends player respawn time in Visual Studio Code.](https://github.com/Coooookies/vscode-league-respawn-timer) - -- [LeaguePrank](https://github.com/LeagueTavern/LeaguePrank) - -- [frank - A bran-new League of Legends assistant software, a replacement for WeGame.](https://github.com/Java-S12138/frank) +| 项目名称 | 描述 | +| --- | --- | +| ⭐⭐⭐ [Pengu Loader](https://github.com/PenguLoader/PenguLoader) | 用于 UX 客户端调试和逆向工程工具 | +| ⭐⭐⭐ [League of Legends LCU and Riot Client API Docs](https://github.com/KebsCS/lcu-and-riotclient-api) | LCU API 文档参考 | +| ⭐⭐ [Community Dragon](https://www.communitydragon.org/documentation/assets) | 资源管理和参考文档 | +| ⭐⭐ [Seraphine](https://github.com/Zzaphkiel/Seraphine) | 缝合重灾区,提供了集成思路 | +| ⭐ [fix-lcu-window](https://github.com/LeagueTavern/fix-lcu-window) | 修复客户端窗口大小问题的思路借鉴 | +| ⭐ [Joi](https://github.com/watchingfun/Joi) | OP.GG 相关实现的参考 | +| ⭐ [lol-helper](https://github.com/4379711/lol-helper) | (曾经的) 卡炫彩功能和工具设计的参考 | +| ⭐ [vscode-league-respawn-timer](https://github.com/Coooookies/vscode-league-respawn-timer) | 重生倒计时功能的参考 | +| ⭐ [LeaguePrank](https://github.com/LeagueTavern/LeaguePrank) | 趣味功能的实现参考 | +| ⭐ [LCU API](https://www.mingweisamuel.com/lcu-schema/tool/#/) | LCU API 早期参考文档 | # 5. FAQ - 常见问题及回答 diff --git a/package.json b/package.json index 9402ae2b..c227637c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.7.7", "axios-retry": "^4.5.0", "dayjs": "^1.11.13", + "fast-csv": "^5.0.2", "lodash": "^4.17.21", "luaparse": "^0.3.1", "mobx": "^6.13.5", @@ -42,7 +43,7 @@ "@electron-toolkit/tsconfig": "^1.0.1", "@electron/notarize": "^2.5.0", "@rushstack/eslint-patch": "^1.10.4", - "@swc/core": "^1.7.36", + "@swc/core": "^1.7.40", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/deep-eql": "^4.0.2", "@types/lodash": "^4.17.12", @@ -73,12 +74,12 @@ "semver": "^7.6.3", "typescript": "^5.6.3", "vfonts": "^0.0.3", - "vite": "^5.4.9", + "vite": "^5.4.10", "vue": "^3.5.12", "vue-chartjs": "^5.3.1", "vue-i18n": "10.0.4", "vue-router": "^4.4.5", - "vue-tsc": "^2.1.6" + "vue-tsc": "^2.1.8" }, "packageManager": "yarn@4.5.1" } diff --git a/src/main/bootstrap/index.ts b/src/main/bootstrap/index.ts index 51aca43a..803dbc35 100644 --- a/src/main/bootstrap/index.ts +++ b/src/main/bootstrap/index.ts @@ -7,7 +7,7 @@ import { AutoReplyMain } from '@main/shards/auto-reply' import { AutoSelectMain } from '@main/shards/auto-select' import { GameClientMain } from '@main/shards/game-client' import { AkariIpcMain } from '@main/shards/ipc' -import { KeyboardShortcutsMain } from '@main/shards/keyboard-shorcuts' +import { KeyboardShortcutsMain } from '@main/shards/keyboard-shortcuts' import { LeagueClientMain } from '@main/shards/league-client' import { LeagueClientUxMain } from '@main/shards/league-client-ux' import { LoggerFactoryMain } from '@main/shards/logger-factory' @@ -66,6 +66,8 @@ declare module '@shared/akari-shard/manager' { write: (config: BaseConfig) => void } + version: string + /** * 退出应用 */ @@ -134,7 +136,7 @@ export function bootstrap() { logger.info({ message: `League Akari ${app.getVersion()}`, - namespace: 'bootstrap' + namespace: 'app' }) try { @@ -160,6 +162,7 @@ export function bootstrap() { write: (config: any) => writeBaseConfig(config) } manager.global.isAdministrator = isAdministrator + manager.global.version = app.getVersion() manager.global.quit = () => app.quit() manager.global.restart = () => { app.relaunch() @@ -250,17 +253,21 @@ export function bootstrap() { shardDisposed = true events.removeAllListeners() logger.info({ - message: `应用退出`, - namespace: 'bootstrap' + message: `应用即将退出`, + namespace: 'app' }) - logger.on('finish', () => app.quit()) + logger.on('finish', () => app.exit()) // 不知为何, 使用 app.quit() 在这里不会生效. 除非包裹 setImmediate 或 setTimeout logger.end() }) }) + + app.on('quit', () => { + console.log(`[${dayjs().format('YYYY-MM-DD HH:mm:ss:SSS')}] [finale] 应用退出`) + }) } catch (error) { logger.error({ message: `[10001] 应用启动时出现错误 ${formatError(error)}`, - namespace: 'bootstrap' + namespace: 'app' }) dialog.showErrorBox('应用启动时出现错误', formatError(error)) logger.on('finish', () => app.exit(10001)) diff --git a/src/main/shards/app-common/index.ts b/src/main/shards/app-common/index.ts index fbf1c80c..57c43c5c 100644 --- a/src/main/shards/app-common/index.ts +++ b/src/main/shards/app-common/index.ts @@ -1,10 +1,11 @@ import { IAkariShardInitDispose } from '@shared/akari-shard/interface' import { AkariSharedGlobalShard, SHARED_GLOBAL_ID } from '@shared/akari-shard/manager' +import { app, shell } from 'electron' import { AkariIpcMain } from '../ipc' import { MobxUtilsMain } from '../mobx-utils' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { AppCommonSettings, AppCommonState } from './state' /** @@ -25,7 +26,7 @@ export class AppCommonMain implements IAkariShardInitDispose { private _shared: AkariSharedGlobalShard private _ipc: AkariIpcMain private _mobx: MobxUtilsMain - private _setting: MobxSettingService + private _setting: SetterSettingService constructor(deps: any) { this._shared = deps[SHARED_GLOBAL_ID] @@ -46,6 +47,8 @@ export class AppCommonMain implements IAkariShardInitDispose { this._shared.global.events.on('second-instance', (commandLine, workingDirectory) => { this._ipc.sendEvent(AppCommonMain.id, 'second-instance', commandLine, workingDirectory) }) + + this.state.setBaseConfig(this._shared.global.baseConfig.value) } private _setDisableHardwareAccelerationAndRelaunch(s: boolean) { @@ -70,6 +73,10 @@ export class AppCommonMain implements IAkariShardInitDispose { this._shared.global.restart() } + openUserDataDir() { + return shell.openPath(app.getPath('userData')) + } + private async _handleState() { await this._setting.applyToState() this._mobx.propSync(AppCommonMain.id, 'settings', this.settings, [ @@ -78,7 +85,8 @@ export class AppCommonMain implements IAkariShardInitDispose { ]) this._mobx.propSync(AppCommonMain.id, 'state', this.state, [ 'isAdministrator', - 'disableHardwareAcceleration' + 'disableHardwareAcceleration', + 'baseConfig' ]) // 状态指示, 是否禁用硬件加速 @@ -93,5 +101,13 @@ export class AppCommonMain implements IAkariShardInitDispose { this._ipc.onCall(AppCommonMain.id, 'setDisableHardwareAcceleration', (s: boolean) => { this._setDisableHardwareAccelerationAndRelaunch(s) }) + + this._ipc.onCall(AppCommonMain.id, 'getVersion', () => { + return this._shared.global.version + }) + + this._ipc.onCall(AppCommonMain.id, 'openUserDataDir', () => { + return this.openUserDataDir() + }) } } diff --git a/src/main/shards/app-common/state.ts b/src/main/shards/app-common/state.ts index 66cc48e0..c7b8189c 100644 --- a/src/main/shards/app-common/state.ts +++ b/src/main/shards/app-common/state.ts @@ -1,4 +1,5 @@ -import { makeAutoObservable } from 'mobx' +import { BaseConfig } from '@main/bootstrap/base-config' +import { makeAutoObservable, observable } from 'mobx' export class AppCommonState { isAdministrator: boolean = false @@ -8,6 +9,8 @@ export class AppCommonState { */ disableHardwareAcceleration: boolean = false + baseConfig: BaseConfig | null + setAdministrator(s: boolean) { this.isAdministrator = s } @@ -16,8 +19,12 @@ export class AppCommonState { this.disableHardwareAcceleration = s } + setBaseConfig(s: BaseConfig | null) { + this.baseConfig = s + } + constructor() { - makeAutoObservable(this) + makeAutoObservable(this, { baseConfig: observable.ref }) } } diff --git a/src/main/shards/auto-gameflow/index.ts b/src/main/shards/auto-gameflow/index.ts index 7a5dd023..f5037c71 100644 --- a/src/main/shards/auto-gameflow/index.ts +++ b/src/main/shards/auto-gameflow/index.ts @@ -1,727 +1,727 @@ -import { TimeoutTask } from '@main/utils/timer' -import { IAkariShardInitDispose } from '@shared/akari-shard/interface' -import { ChoiceMaker } from '@shared/utils/choice-maker' -import { formatError } from '@shared/utils/errors' -import { randomInt } from '@shared/utils/random' -import { comparer, computed } from 'mobx' - -import { AkariIpcMain } from '../ipc' -import { LeagueClientMain } from '../league-client' -import { AkariLogger, LoggerFactoryMain } from '../logger-factory' -import { MobxUtilsMain } from '../mobx-utils' -import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' -import { AutoGameflowSettings, AutoGameflowState } from './state' - -/** - * 自动游戏流程相关功能 - */ -export class AutoGameflowMain implements IAkariShardInitDispose { - static id = 'auto-gameflow-main' - static dependencies = [ - 'logger-factory-main', - 'setting-factory-main', - 'league-client-main', - 'akari-ipc-main', - 'mobx-utils-main' - ] - - public readonly settings = new AutoGameflowSettings() - public readonly state: AutoGameflowState - - private readonly _loggerFactory: LoggerFactoryMain - private readonly _settingFactory: SettingFactoryMain - private readonly _log: AkariLogger - private readonly _lc: LeagueClientMain - private readonly _setting: MobxSettingService - private readonly _mobx: MobxUtilsMain - private readonly _ipc: AkariIpcMain - - private _autoAcceptTimerId: NodeJS.Timeout | null = null - private _autoSearchMatchTimerId: NodeJS.Timeout | null = null - private _autoSearchMatchCountdownTimerId: NodeJS.Timeout | null = null - - private _playAgainTask = new TimeoutTask(() => this._playAgainFn()) - private _dodgeTask = new TimeoutTask(() => this._dodgeFn()) - private _reconnectTask = new TimeoutTask(() => this._reconnectFn()) - - static HONOR_CATEGORY = ['HEART'] as const - - static PLAY_AGAIN_WAIT_FOR_BALLOT_TIMEOUT = 3250 - static PLAY_AGAIN_WAIT_FOR_STATS_TIMEOUT = 10000 - static PLAY_AGAIN_BUFFER_TIMEOUT = 1575 - - constructor(deps: any) { - this._loggerFactory = deps['logger-factory-main'] - this._settingFactory = deps['setting-factory-main'] - this._log = this._loggerFactory.create(AutoGameflowMain.id) - this._lc = deps['league-client-main'] - this._mobx = deps['mobx-utils-main'] - this._ipc = deps['akari-ipc-main'] - this.state = new AutoGameflowState(this._lc.data, this.settings) - this._setting = this._settingFactory.create( - AutoGameflowMain.id, - { - autoAcceptDelaySeconds: { default: this.settings.autoAcceptDelaySeconds }, - autoAcceptEnabled: { default: this.settings.autoAcceptEnabled }, - autoHonorEnabled: { default: this.settings.autoHonorEnabled }, - autoHonorStrategy: { default: this.settings.autoHonorStrategy }, - autoMatchmakingDelaySeconds: { default: this.settings.autoMatchmakingDelaySeconds }, - autoMatchmakingEnabled: { default: this.settings.autoMatchmakingEnabled }, - autoMatchmakingMaximumMatchDuration: { - default: this.settings.autoMatchmakingMaximumMatchDuration - }, - autoMatchmakingMinimumMembers: { default: this.settings.autoMatchmakingMinimumMembers }, - playAgainEnabled: { default: this.settings.playAgainEnabled }, - autoReconnectEnabled: { default: this.settings.autoReconnectEnabled }, - autoMatchmakingRematchFixedDuration: { - default: this.settings.autoMatchmakingRematchFixedDuration - }, - autoMatchmakingRematchStrategy: { default: this.settings.autoMatchmakingRematchStrategy }, - autoMatchmakingWaitForInvitees: { default: this.settings.autoMatchmakingWaitForInvitees }, - autoHandleInvitationsEnabled: { default: this.settings.autoHandleInvitationsEnabled }, - dodgeAtLastSecondThreshold: { default: this.settings.dodgeAtLastSecondThreshold }, - invitationHandlingStrategies: { default: this.settings.invitationHandlingStrategies } - }, - this.settings - ) - } - - private _handleIpcCall() { - this._ipc.onCall(AutoGameflowMain.id, 'cancelAutoAccept', () => { - this.cancelAutoAccept('normal') - }) - this._ipc.onCall(AutoGameflowMain.id, 'cancelAutoSearchMatch', () => { - this.cancelAutoSearchMatch('normal') - }) - this._ipc.onCall(AutoGameflowMain.id, 'setWillDodgeAtLastSecond', (enabled: boolean) => { - this.state.setWillDodgeAtLastSecond(enabled) - }) - } - - private _handleLogging() { - // 监听 gameflow - this._mobx.reaction( - () => this._lc.data.gameflow.phase, - (phase) => { - this._log.info(`游戏流阶段变化: ${phase}`) - } - ) - } - - private _handleAutoAccept() { - this._mobx.reaction( - () => this._lc.data.gameflow.phase, - (phase) => { - if (!this.settings.autoAcceptEnabled) { - return - } - - if (phase === 'ReadyCheck') { - this.state.setAcceptAt(Date.now() + this.settings.autoAcceptDelaySeconds * 1e3) - this._autoAcceptTimerId = setTimeout( - () => this._acceptMatch(), - this.settings.autoAcceptDelaySeconds * 1e3 - ) - - this._log.info(`ReadyCheck! 即将在 ${this.settings.autoAcceptDelaySeconds} 秒后接受对局`) - } else { - if (this._autoAcceptTimerId) { - if (this.state.willAccept) { - this._log.info(`取消了即将进行的接受操作 - 不在游戏 ReadyCheck 过程中`) - } - - clearTimeout(this._autoAcceptTimerId) - this._autoAcceptTimerId = null - } - this.state.clearAutoAccept() - } - }, - { fireImmediately: true } - ) - - this._mobx.reaction( - () => this.settings.autoAcceptEnabled, - (enabled) => { - if (!enabled) { - this.cancelAutoAccept('normal') - } - }, - { fireImmediately: true } - ) - - this._lc.events.on('/lol-matchmaking/v1/ready-check', (event) => { - if ( - event.data && - (event.data.playerResponse === 'Declined' || event.data.playerResponse === 'Accepted') - ) { - this.cancelAutoAccept('declined') - } - }) - } - - private _handleAutoPlayAgain() { - this._mobx.reaction( - () => [this._lc.data.gameflow.phase, this.settings.playAgainEnabled] as const, - async ([phase, enabled]) => { - if ( - !enabled || - (phase !== 'WaitingForStats' && phase !== 'PreEndOfGame' && phase !== 'EndOfGame') - ) { - this._playAgainTask.cancel() - return - } - - // 如果停留在结算页面时间过长,将考虑返回 - if (phase === 'WaitingForStats' && enabled) { - this._log.info( - `位于 WaitingForStats,等待 ${AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_STATS_TIMEOUT} ms` - ) - this._playAgainTask.start(AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_STATS_TIMEOUT) - return - } - - // 在某些模式中,可能会出现仅有 PreEndOfGame 的情况,需要做一个计时器 - if (phase === 'PreEndOfGame' && enabled) { - this._log.info(`等待点赞事件 ${AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_BALLOT_TIMEOUT} ms`) - this._playAgainTask.start(AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_BALLOT_TIMEOUT) - return - } - - if (phase === 'EndOfGame' && enabled) { - this._log.info(`将在 ${AutoGameflowMain.PLAY_AGAIN_BUFFER_TIMEOUT} ms 后回到房间`) - this._playAgainTask.start(AutoGameflowMain.PLAY_AGAIN_BUFFER_TIMEOUT) - return - } - }, - { equals: comparer.shallow, fireImmediately: true } - ) - } - - private _handleAutoReconnect() { - this._mobx.reaction( - () => [this._lc.data.gameflow.phase, this.settings.autoReconnectEnabled] as const, - ([phase, enabled]) => { - if (phase === 'Reconnect' && enabled) { - this._log.info('将在短暂延迟后尝试重新连接') - this._reconnectTask.start(1000) - } else { - this._reconnectTask.cancel() - } - } - ) - } - - private _handleAutoHandleInvitation() { - this._mobx.reaction( - () => - [ - this._lc.data.lobby.receivedInvitations, - this.settings.autoHandleInvitationsEnabled, - this.settings.invitationHandlingStrategies - ] as const, - async ([invitations, enabled, strategies]) => { - if (!enabled || invitations.length === 0) { - return - } - - this._log.info(`处理邀请: ${JSON.stringify(invitations)}, ${JSON.stringify(strategies)}`) - - const availableInvitations = invitations.filter( - (i) => i.state === 'Pending' && i.canAcceptInvitation - ) - - if (availableInvitations.length === 0) { - return - } - - // 先找到任意一个符合要求的, decline 或 accept 或 ignore - const availableStrategies = availableInvitations - .map((i) => { - const strategy = strategies[i.gameConfig.inviteGameType] - - if (strategy) { - return { - id: i.invitationId, - inviteGameType: i.gameConfig.inviteGameType, - strategy: strategies[i.gameConfig.inviteGameType] - } - } - - return { - id: i.invitationId, - inviteGameType: i.gameConfig.inviteGameType, - strategy: strategies[''] || 'ignore' - } - }) - .toSorted((a, b) => { - if (a.strategy === 'accept' && b.strategy !== 'accept') { - return -1 - } else if (a.strategy !== 'accept' && b.strategy === 'accept') { - return 1 - } else if (a.strategy === 'decline' && b.strategy !== 'decline') { - return -1 - } else if (a.strategy !== 'decline' && b.strategy === 'decline') { - return 1 - } else { - return 0 - } - }) - - if (availableStrategies.length === 0) { - return - } - - const candidate = availableStrategies[0] - - try { - if (candidate.strategy === 'accept') { - await this._lc.api.lobby.acceptReceivedInvitation(candidate.id) - this._log.info(`自动处理邀请: ${candidate.id}, ${candidate.strategy}`) - } else if (candidate.strategy === 'decline') { - await this._lc.api.lobby.declineReceivedInvitation(candidate.id) - this._log.info(`自动处理邀请: ${candidate.id}, ${candidate.strategy}`) - } else { - this._log.info(`忽略这个邀请: ${candidate.id}, ${candidate.strategy}`) - } - } catch (error) { - this._log.warn(`自动处理失败: ${formatError(error)}`) - } - } - ) - } - - private _adjustDodgeTimer(msLeft: number, threshold: number) { - const dodgeIn = Math.max(msLeft - threshold * 1e3, 0) - this._log.info(`时间校正:将在 ${dodgeIn} ms 后秒退`) - this._dodgeTask.start(dodgeIn) - this.state.setDodgeAt(Date.now() + dodgeIn) - } - - private _handleLastSecondDodge() { - this._mobx.reaction( - () => [Boolean(this._lc.data.champSelect.session), this.state.willDodgeAtLastSecond] as const, - ([hasSession, enabled]) => { - if (!hasSession || !enabled) { - if (this._dodgeTask.cancel()) { - this._log.info('预定秒退已取消') - } - this.state.setDodgeAt(-1) - this.state.setWillDodgeAtLastSecond(false) - return - } - }, - { equals: comparer.shallow } - ) - - this._mobx.reaction( - () => - [ - this._lc.data.champSelect.session?.timer, - this.state.willDodgeAtLastSecond, - this.settings.dodgeAtLastSecondThreshold - ] as const, - ([timer, enabled, threshold]) => { - if (timer && enabled) { - if (timer.phase === 'FINALIZATION') { - this._adjustDodgeTimer(timer.adjustedTimeLeftInPhase, threshold) - } - } - }, - { equals: comparer.shallow } - ) - } - - private _handleAutoBallot() { - const honorables = computed(() => { - if (!this._lc.data.honor.ballot) { - return null - } - - const { - eligibleAllies, - eligibleOpponents, - gameId, - votePool: { votes } - } = this._lc.data.honor.ballot - - return { - allies: eligibleAllies.filter((p) => !p.botPlayer).map((p) => p.puuid), - opponents: eligibleOpponents.filter((p) => !p.botPlayer).map((p) => p.puuid), - votes, - gameId - } - }) - - this._mobx.reaction( - () => [honorables.get(), this.settings.autoHonorEnabled] as const, - async ([h, enabled]) => { - if (h && h.gameId) { - this._playAgainTask.cancel() - } - - if (h && h.gameId && enabled) { - try { - const eogStatus = (await this._lc.api.lobby.getEogStatus()).data - const lobbyMembers = [ - ...eogStatus.eogPlayers, - ...eogStatus.leftPlayers, - ...eogStatus.readyPlayers - ] - const candidates: string[] = [] - - const lobbyAllies = h.allies.filter((p) => lobbyMembers.includes(p)) - - if (lobbyAllies.length > 0) { - const actualLobbyVotes = Math.min(h.votes, lobbyAllies.length) - const weights = Array(lobbyAllies.length).fill(1) - const maker = new ChoiceMaker(weights, lobbyAllies) - const lobbyCandidates = maker.choose(actualLobbyVotes) - candidates.push(...lobbyCandidates) - } - - const leftPlayers = [...h.allies, ...h.opponents].filter( - (p) => !lobbyMembers.includes(p) - ) - const actualLeftVotes = Math.min(h.votes - candidates.length, leftPlayers.length) - - if (actualLeftVotes > 0) { - const leftWeights = Array(leftPlayers.length).fill(1) - const leftMaker = new ChoiceMaker(leftWeights, leftPlayers) - const leftCandidates = leftMaker.choose(actualLeftVotes) - candidates.push(...leftCandidates) - } - - for (const puuid of candidates) { - await this._lc.api.honor.honor( - AutoGameflowMain.HONOR_CATEGORY[ - randomInt(0, AutoGameflowMain.HONOR_CATEGORY.length) - ], - puuid - ) - } - - await this._lc.api.honor.ballot() - this._log.info(`自动点赞:给玩家 ${candidates.join(', ')} 点赞, 对局 ID: ${h.gameId}`) - } catch (error) { - this._ipc.sendEvent(AutoGameflowMain.id, 'error-auto-honor', formatError(error)) - this._log.warn(`自动点赞出现错误 ${formatError(error)}`) - } - } - }, - { - equals: comparer.structural, - fireImmediately: true - } - ) - } - - private _handleAutoSearchMatch() { - this._mobx.reaction( - () => this.settings.autoMatchmakingEnabled, - (enabled) => { - if (!enabled) { - this.cancelAutoSearchMatch('normal') - } - }, - { fireImmediately: true } - ) - - this._mobx.reaction( - () => [this.state.activityStartStatus, this.settings.autoMatchmakingEnabled] as const, - ([s, enabled]) => { - if (!enabled) { - this.cancelAutoSearchMatch('normal') - return - } - - if (s === 'can-start-activity') { - this._log.info(`现在将在 ${this.settings.autoMatchmakingDelaySeconds} 秒后开始匹配`) - this.state.setSearchMatchAt(Date.now() + this.settings.autoMatchmakingDelaySeconds * 1e3) - this._autoSearchMatchTimerId = setTimeout( - () => this._startMatchmaking(), - this.settings.autoMatchmakingDelaySeconds * 1e3 - ) - - this._sendAutoSearchMatchInfoInChat() - this._autoSearchMatchCountdownTimerId = setInterval( - () => this._sendAutoSearchMatchInfoInChat(), - 1000 - ) - } else if (s === 'unavailable' || s === 'cannot-start-activity') { - this.cancelAutoSearchMatch('normal') - } else { - this.cancelAutoSearchMatch(s) - } - }, - { equals: comparer.shallow, fireImmediately: true } - ) - - const simplifiedSearchState = computed(() => { - if (!this._lc.data.matchmaking.search) { - return null - } - - return { - timeInQueue: this._lc.data.matchmaking.search.timeInQueue, - estimatedQueueTime: this._lc.data.matchmaking.search.estimatedQueueTime, - searchState: this._lc.data.matchmaking.search.searchState, - lowPriorityData: this._lc.data.matchmaking.search.lowPriorityData, - isCurrentlyInQueue: this._lc.data.matchmaking.search.isCurrentlyInQueue - } - }) - - let penaltyTime = 0 - this._mobx.reaction( - () => Boolean(simplifiedSearchState.get()), - (hasSearchState) => { - if (hasSearchState) { - penaltyTime = simplifiedSearchState.get()?.lowPriorityData.penaltyTime || 0 - } else { - penaltyTime = 0 - } - }, - { fireImmediately: true } - ) - - this._mobx.reaction( - () => - [ - simplifiedSearchState.get(), - this.settings.autoMatchmakingRematchStrategy, - this.settings.autoMatchmakingRematchFixedDuration - ] as const, - ([s, st, d]) => { - if (st === 'never' || !s || s.searchState !== 'Searching') { - return - } - - if (!s.isCurrentlyInQueue) { - return - } - - if (st === 'fixed-duration') { - if (s.timeInQueue - penaltyTime >= d) { - this._lc.api.lobby.deleteSearchMatch().catch((e) => { - this._log.warn(`尝试取消匹配时失败 ${formatError(e)}`) - }) - return - } - } else if (st === 'estimated-duration') { - if (s.timeInQueue - penaltyTime >= s.estimatedQueueTime) { - this._lc.api.lobby.deleteSearchMatch().catch((e) => { - this._log.warn(`尝试取消匹配时失败 ${formatError(e)}`) - }) - } - } - }, - { equals: comparer.structural, fireImmediately: true } - ) - } - - private async _acceptMatch() { - try { - await this._lc.api.matchmaking.accept() - } catch (error) { - this._ipc.sendEvent(AutoGameflowMain.id, 'error-accept-match', formatError(error)) - this._log.warn(`无法接受对局 ${formatError(error)}`) - } - this.state.clearAutoAccept() - this._autoSearchMatchTimerId = null - } - - private async _startMatchmaking() { - try { - if (this._autoSearchMatchCountdownTimerId) { - clearInterval(this._autoSearchMatchCountdownTimerId) - this._autoSearchMatchCountdownTimerId = null - } - this.state.clearAutoSearchMatch() - this._autoSearchMatchTimerId = null - await this._lc.api.lobby.searchMatch() - } catch (error) { - this._ipc.sendEvent(AutoGameflowMain.id, 'error-matchmaking', formatError(error)) - this._log.warn(`无法开始匹配 ${formatError(error)}`) - } - } - - private async _playAgainFn() { - try { - this._log.info('Play again, 返回房间') - await this._lc.api.lobby.playAgain() - } catch (error) { - this._log.warn(`尝试 Play again 时失败`, error) - } - } - - private async _dodgeFn() { - try { - this._log.info('Dodge, 秒退') - await this._lc.api.login.dodge() - } catch (error) { - this._log.warn(`尝试秒退时失败`, error) - } finally { - this.state.setDodgeAt(-1) - } - } - - private async _reconnectFn() { - try { - this._log.info('Reconnect! 尝试重新连接') - await this._lc.api.gameflow.reconnect() - } catch (error) { - this._log.warn(`尝试重新连接失败`, error) - } - } - - private async _handleState() { - await this._setting.applyToState() - - this._setting.onChange('dodgeAtLastSecondThreshold', async (v, { setter }) => { - if (v < 0) { - v = 0 - } - - this.settings.setDodgeAtLastSecondThreshold(v) - await setter() - }) - - this._setting.onChange('autoAcceptEnabled', async (v, { setter }) => { - if (!v) { - this.cancelAutoAccept('normal') - } - - this.settings.setAutoAcceptEnabled(v) - await setter() - }) - - this._mobx.propSync(AutoGameflowMain.id, 'settings', this.settings, [ - 'autoAcceptDelaySeconds', - 'autoAcceptEnabled', - 'autoHonorEnabled', - 'autoHonorStrategy', - 'autoMatchmakingDelaySeconds', - 'autoMatchmakingEnabled', - 'autoMatchmakingMinimumMembers', - 'autoMatchmakingRematchFixedDuration', - 'autoMatchmakingRematchStrategy', - 'autoMatchmakingWaitForInvitees', - 'playAgainEnabled', - 'dodgeAtLastSecondThreshold', - 'autoHandleInvitationsEnabled', - 'autoReconnectEnabled', - 'autoMatchmakingMaximumMatchDuration', - 'invitationHandlingStrategies' - ]) - - this._mobx.propSync(AutoGameflowMain.id, 'state', this.state, [ - 'willAccept', - 'willAcceptAt', - 'willSearchMatch', - 'willSearchMatchAt', - 'activityStartStatus', - 'willDodgeAt', - 'willDodgeAtLastSecond' - ]) - } - - cancelAutoAccept(reason?: string) { - if (this.state.willAccept) { - if (this._autoAcceptTimerId) { - clearTimeout(this._autoAcceptTimerId) - this._autoAcceptTimerId = null - } - this.state.clearAutoAccept() - if (reason === 'accepted') { - this._log.info(`取消了即将进行的接受 - 已经被接受`) - } else if (reason === 'declined') { - this._log.info(`取消了即将进行的接受 - 已经被拒绝`) - } else { - this._log.info(`取消了即将进行的接受`) - } - } - } - - cancelAutoSearchMatch(reason?: string) { - if (this.state.willSearchMatch) { - if (this._autoSearchMatchTimerId) { - clearTimeout(this._autoSearchMatchTimerId) - this._autoSearchMatchTimerId = null - } - if (this._autoSearchMatchCountdownTimerId) { - this._sendAutoSearchMatchInfoInChat(reason) - clearInterval(this._autoSearchMatchCountdownTimerId) - this._autoSearchMatchCountdownTimerId = null - } - - this.state.clearAutoSearchMatch() - this._log.info(`即将进行的自动匹配对局已取消,${reason || '未知'}`) - } - } - - private _sendAutoSearchMatchInfoInChat = async (cancel?: string) => { - if (this._lc.data.chat.conversations.customGame && this.state.willSearchMatch) { - if (cancel === 'normal') { - this._lc.api.chat - .chatSend( - this._lc.data.chat.conversations.customGame.id, - `[League Akari] 自动匹配已取消`, - 'celebration' - ) - .catch(() => {}) - return - } else if (cancel === 'waiting-for-invitee') { - this._lc.api.chat - .chatSend( - this._lc.data.chat.conversations.customGame.id, - `[League Akari] 自动匹配已取消,等待被邀请者`, - 'celebration' - ) - .catch(() => {}) - return - } else if (cancel === 'not-the-leader') { - this._lc.api.chat - .chatSend( - this._lc.data.chat.conversations.customGame.id, - `[League Akari] 自动匹配已取消,当前不是房间房主`, - 'celebration' - ) - .catch(() => {}) - return - } else if (cancel === 'waiting-for-penalty-time') { - this._lc.api.chat - .chatSend( - this._lc.data.chat.conversations.customGame.id, - `[League Akari] 自动匹配已取消,等待秒退计时器`, - 'celebration' - ) - .catch(() => {}) - return - } - - const time = (this.state.willSearchMatchAt - Date.now()) / 1e3 - this._lc.api.chat - .chatSend( - this._lc.data.chat.conversations.customGame.id, - `[League Akari] 将在 ${Math.abs(time).toFixed()} 秒后自动匹配`, - 'celebration' - ) - .catch(() => {}) - } - } - - async onInit() { - await this._handleState() - this._handleIpcCall() - this._handleAutoBallot() - this._handleAutoAccept() - this._handleAutoPlayAgain() - this._handleAutoReconnect() - this._handleAutoHandleInvitation() - this._handleLogging() - this._handleLastSecondDodge() - this._handleAutoSearchMatch() - } - - async onDispose() {} -} +import { TimeoutTask } from '@main/utils/timer' +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { ChoiceMaker } from '@shared/utils/choice-maker' +import { formatError } from '@shared/utils/errors' +import { randomInt } from '@shared/utils/random' +import { comparer, computed } from 'mobx' + +import { AkariIpcMain } from '../ipc' +import { LeagueClientMain } from '../league-client' +import { AkariLogger, LoggerFactoryMain } from '../logger-factory' +import { MobxUtilsMain } from '../mobx-utils' +import { SettingFactoryMain } from '../setting-factory' +import { SetterSettingService } from '../setting-factory/setter-setting-service' +import { AutoGameflowSettings, AutoGameflowState } from './state' + +/** + * 自动游戏流程相关功能 + */ +export class AutoGameflowMain implements IAkariShardInitDispose { + static id = 'auto-gameflow-main' + static dependencies = [ + 'logger-factory-main', + 'setting-factory-main', + 'league-client-main', + 'akari-ipc-main', + 'mobx-utils-main' + ] + + public readonly settings = new AutoGameflowSettings() + public readonly state: AutoGameflowState + + private readonly _loggerFactory: LoggerFactoryMain + private readonly _settingFactory: SettingFactoryMain + private readonly _log: AkariLogger + private readonly _lc: LeagueClientMain + private readonly _setting: SetterSettingService + private readonly _mobx: MobxUtilsMain + private readonly _ipc: AkariIpcMain + + private _autoAcceptTimerId: NodeJS.Timeout | null = null + private _autoSearchMatchTimerId: NodeJS.Timeout | null = null + private _autoSearchMatchCountdownTimerId: NodeJS.Timeout | null = null + + private _playAgainTask = new TimeoutTask(() => this._playAgainFn()) + private _dodgeTask = new TimeoutTask(() => this._dodgeFn()) + private _reconnectTask = new TimeoutTask(() => this._reconnectFn()) + + static HONOR_CATEGORY = ['HEART'] as const + + static PLAY_AGAIN_WAIT_FOR_BALLOT_TIMEOUT = 3250 + static PLAY_AGAIN_WAIT_FOR_STATS_TIMEOUT = 10000 + static PLAY_AGAIN_BUFFER_TIMEOUT = 1575 + + constructor(deps: any) { + this._loggerFactory = deps['logger-factory-main'] + this._settingFactory = deps['setting-factory-main'] + this._log = this._loggerFactory.create(AutoGameflowMain.id) + this._lc = deps['league-client-main'] + this._mobx = deps['mobx-utils-main'] + this._ipc = deps['akari-ipc-main'] + this.state = new AutoGameflowState(this._lc.data, this.settings) + this._setting = this._settingFactory.create( + AutoGameflowMain.id, + { + autoAcceptDelaySeconds: { default: this.settings.autoAcceptDelaySeconds }, + autoAcceptEnabled: { default: this.settings.autoAcceptEnabled }, + autoHonorEnabled: { default: this.settings.autoHonorEnabled }, + autoHonorStrategy: { default: this.settings.autoHonorStrategy }, + autoMatchmakingDelaySeconds: { default: this.settings.autoMatchmakingDelaySeconds }, + autoMatchmakingEnabled: { default: this.settings.autoMatchmakingEnabled }, + autoMatchmakingMaximumMatchDuration: { + default: this.settings.autoMatchmakingMaximumMatchDuration + }, + autoMatchmakingMinimumMembers: { default: this.settings.autoMatchmakingMinimumMembers }, + playAgainEnabled: { default: this.settings.playAgainEnabled }, + autoReconnectEnabled: { default: this.settings.autoReconnectEnabled }, + autoMatchmakingRematchFixedDuration: { + default: this.settings.autoMatchmakingRematchFixedDuration + }, + autoMatchmakingRematchStrategy: { default: this.settings.autoMatchmakingRematchStrategy }, + autoMatchmakingWaitForInvitees: { default: this.settings.autoMatchmakingWaitForInvitees }, + autoHandleInvitationsEnabled: { default: this.settings.autoHandleInvitationsEnabled }, + dodgeAtLastSecondThreshold: { default: this.settings.dodgeAtLastSecondThreshold }, + invitationHandlingStrategies: { default: this.settings.invitationHandlingStrategies } + }, + this.settings + ) + } + + private _handleIpcCall() { + this._ipc.onCall(AutoGameflowMain.id, 'cancelAutoAccept', () => { + this.cancelAutoAccept('normal') + }) + this._ipc.onCall(AutoGameflowMain.id, 'cancelAutoMatchmaking', () => { + this.cancelAutoMatchmaking('normal') + }) + this._ipc.onCall(AutoGameflowMain.id, 'setWillDodgeAtLastSecond', (enabled: boolean) => { + this.state.setWillDodgeAtLastSecond(enabled) + }) + } + + private _handleLogging() { + // 监听 gameflow + this._mobx.reaction( + () => this._lc.data.gameflow.phase, + (phase) => { + this._log.info(`游戏流阶段变化: ${phase}`) + } + ) + } + + private _handleAutoAccept() { + this._mobx.reaction( + () => this._lc.data.gameflow.phase, + (phase) => { + if (!this.settings.autoAcceptEnabled) { + return + } + + if (phase === 'ReadyCheck') { + this.state.setAcceptAt(Date.now() + this.settings.autoAcceptDelaySeconds * 1e3) + this._autoAcceptTimerId = setTimeout( + () => this._acceptMatch(), + this.settings.autoAcceptDelaySeconds * 1e3 + ) + + this._log.info(`ReadyCheck! 即将在 ${this.settings.autoAcceptDelaySeconds} 秒后接受对局`) + } else { + if (this._autoAcceptTimerId) { + if (this.state.willAccept) { + this._log.info(`取消了即将进行的接受操作 - 不在游戏 ReadyCheck 过程中`) + } + + clearTimeout(this._autoAcceptTimerId) + this._autoAcceptTimerId = null + } + this.state.clearAutoAccept() + } + }, + { fireImmediately: true } + ) + + this._mobx.reaction( + () => this.settings.autoAcceptEnabled, + (enabled) => { + if (!enabled) { + this.cancelAutoAccept('normal') + } + }, + { fireImmediately: true } + ) + + this._lc.events.on('/lol-matchmaking/v1/ready-check', (event) => { + if ( + event.data && + (event.data.playerResponse === 'Declined' || event.data.playerResponse === 'Accepted') + ) { + this.cancelAutoAccept('declined') + } + }) + } + + private _handleAutoPlayAgain() { + this._mobx.reaction( + () => [this._lc.data.gameflow.phase, this.settings.playAgainEnabled] as const, + async ([phase, enabled]) => { + if ( + !enabled || + (phase !== 'WaitingForStats' && phase !== 'PreEndOfGame' && phase !== 'EndOfGame') + ) { + this._playAgainTask.cancel() + return + } + + // 如果停留在结算页面时间过长,将考虑返回 + if (phase === 'WaitingForStats' && enabled) { + this._log.info( + `位于 WaitingForStats,等待 ${AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_STATS_TIMEOUT} ms` + ) + this._playAgainTask.start(AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_STATS_TIMEOUT) + return + } + + // 在某些模式中,可能会出现仅有 PreEndOfGame 的情况,需要做一个计时器 + if (phase === 'PreEndOfGame' && enabled) { + this._log.info(`等待点赞事件 ${AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_BALLOT_TIMEOUT} ms`) + this._playAgainTask.start(AutoGameflowMain.PLAY_AGAIN_WAIT_FOR_BALLOT_TIMEOUT) + return + } + + if (phase === 'EndOfGame' && enabled) { + this._log.info(`将在 ${AutoGameflowMain.PLAY_AGAIN_BUFFER_TIMEOUT} ms 后回到房间`) + this._playAgainTask.start(AutoGameflowMain.PLAY_AGAIN_BUFFER_TIMEOUT) + return + } + }, + { equals: comparer.shallow, fireImmediately: true } + ) + } + + private _handleAutoReconnect() { + this._mobx.reaction( + () => [this._lc.data.gameflow.phase, this.settings.autoReconnectEnabled] as const, + ([phase, enabled]) => { + if (phase === 'Reconnect' && enabled) { + this._log.info('将在短暂延迟后尝试重新连接') + this._reconnectTask.start(1000) + } else { + this._reconnectTask.cancel() + } + } + ) + } + + private _handleAutoHandleInvitation() { + this._mobx.reaction( + () => + [ + this._lc.data.lobby.receivedInvitations, + this.settings.autoHandleInvitationsEnabled, + this.settings.invitationHandlingStrategies + ] as const, + async ([invitations, enabled, strategies]) => { + if (!enabled || invitations.length === 0) { + return + } + + this._log.info(`处理邀请: ${JSON.stringify(invitations)}, ${JSON.stringify(strategies)}`) + + const availableInvitations = invitations.filter( + (i) => i.state === 'Pending' && i.canAcceptInvitation + ) + + if (availableInvitations.length === 0) { + return + } + + // 先找到任意一个符合要求的, decline 或 accept 或 ignore + const availableStrategies = availableInvitations + .map((i) => { + const strategy = strategies[i.gameConfig.inviteGameType] + + if (strategy) { + return { + id: i.invitationId, + inviteGameType: i.gameConfig.inviteGameType, + strategy: strategies[i.gameConfig.inviteGameType] + } + } + + return { + id: i.invitationId, + inviteGameType: i.gameConfig.inviteGameType, + strategy: strategies[''] || 'ignore' + } + }) + .toSorted((a, b) => { + if (a.strategy === 'accept' && b.strategy !== 'accept') { + return -1 + } else if (a.strategy !== 'accept' && b.strategy === 'accept') { + return 1 + } else if (a.strategy === 'decline' && b.strategy !== 'decline') { + return -1 + } else if (a.strategy !== 'decline' && b.strategy === 'decline') { + return 1 + } else { + return 0 + } + }) + + if (availableStrategies.length === 0) { + return + } + + const candidate = availableStrategies[0] + + try { + if (candidate.strategy === 'accept') { + await this._lc.api.lobby.acceptReceivedInvitation(candidate.id) + this._log.info(`自动处理邀请: ${candidate.id}, ${candidate.strategy}`) + } else if (candidate.strategy === 'decline') { + await this._lc.api.lobby.declineReceivedInvitation(candidate.id) + this._log.info(`自动处理邀请: ${candidate.id}, ${candidate.strategy}`) + } else { + this._log.info(`忽略这个邀请: ${candidate.id}, ${candidate.strategy}`) + } + } catch (error) { + this._log.warn(`自动处理失败: ${formatError(error)}`) + } + } + ) + } + + private _adjustDodgeTimer(msLeft: number, threshold: number) { + const dodgeIn = Math.max(msLeft - threshold * 1e3, 0) + this._log.info(`时间校正:将在 ${dodgeIn} ms 后秒退`) + this._dodgeTask.start(dodgeIn) + this.state.setDodgeAt(Date.now() + dodgeIn) + } + + private _handleLastSecondDodge() { + this._mobx.reaction( + () => [Boolean(this._lc.data.champSelect.session), this.state.willDodgeAtLastSecond] as const, + ([hasSession, enabled]) => { + if (!hasSession || !enabled) { + if (this._dodgeTask.cancel()) { + this._log.info('预定秒退已取消') + } + this.state.setDodgeAt(-1) + this.state.setWillDodgeAtLastSecond(false) + return + } + }, + { equals: comparer.shallow } + ) + + this._mobx.reaction( + () => + [ + this._lc.data.champSelect.session?.timer, + this.state.willDodgeAtLastSecond, + this.settings.dodgeAtLastSecondThreshold + ] as const, + ([timer, enabled, threshold]) => { + if (timer && enabled) { + if (timer.phase === 'FINALIZATION') { + this._adjustDodgeTimer(timer.adjustedTimeLeftInPhase, threshold) + } + } + }, + { equals: comparer.shallow } + ) + } + + private _handleAutoBallot() { + const honorables = computed(() => { + if (!this._lc.data.honor.ballot) { + return null + } + + const { + eligibleAllies, + eligibleOpponents, + gameId, + votePool: { votes } + } = this._lc.data.honor.ballot + + return { + allies: eligibleAllies.filter((p) => !p.botPlayer).map((p) => p.puuid), + opponents: eligibleOpponents.filter((p) => !p.botPlayer).map((p) => p.puuid), + votes, + gameId + } + }) + + this._mobx.reaction( + () => [honorables.get(), this.settings.autoHonorEnabled] as const, + async ([h, enabled]) => { + if (h && h.gameId) { + this._playAgainTask.cancel() + } + + if (h && h.gameId && enabled) { + try { + const eogStatus = (await this._lc.api.lobby.getEogStatus()).data + const lobbyMembers = [ + ...eogStatus.eogPlayers, + ...eogStatus.leftPlayers, + ...eogStatus.readyPlayers + ] + const candidates: string[] = [] + + const lobbyAllies = h.allies.filter((p) => lobbyMembers.includes(p)) + + if (lobbyAllies.length > 0) { + const actualLobbyVotes = Math.min(h.votes, lobbyAllies.length) + const weights = Array(lobbyAllies.length).fill(1) + const maker = new ChoiceMaker(weights, lobbyAllies) + const lobbyCandidates = maker.choose(actualLobbyVotes) + candidates.push(...lobbyCandidates) + } + + const leftPlayers = [...h.allies, ...h.opponents].filter( + (p) => !lobbyMembers.includes(p) + ) + const actualLeftVotes = Math.min(h.votes - candidates.length, leftPlayers.length) + + if (actualLeftVotes > 0) { + const leftWeights = Array(leftPlayers.length).fill(1) + const leftMaker = new ChoiceMaker(leftWeights, leftPlayers) + const leftCandidates = leftMaker.choose(actualLeftVotes) + candidates.push(...leftCandidates) + } + + for (const puuid of candidates) { + await this._lc.api.honor.honor( + AutoGameflowMain.HONOR_CATEGORY[ + randomInt(0, AutoGameflowMain.HONOR_CATEGORY.length) + ], + puuid + ) + } + + await this._lc.api.honor.ballot() + this._log.info(`自动点赞:给玩家 ${candidates.join(', ')} 点赞, 对局 ID: ${h.gameId}`) + } catch (error) { + this._ipc.sendEvent(AutoGameflowMain.id, 'error-auto-honor', formatError(error)) + this._log.warn(`自动点赞出现错误 ${formatError(error)}`) + } + } + }, + { + equals: comparer.structural, + fireImmediately: true + } + ) + } + + private _handleAutoSearchMatch() { + this._mobx.reaction( + () => this.settings.autoMatchmakingEnabled, + (enabled) => { + if (!enabled) { + this.cancelAutoMatchmaking('normal') + } + }, + { fireImmediately: true } + ) + + this._mobx.reaction( + () => [this.state.activityStartStatus, this.settings.autoMatchmakingEnabled] as const, + ([s, enabled]) => { + if (!enabled) { + this.cancelAutoMatchmaking('normal') + return + } + + if (s === 'can-start-activity') { + this._log.info(`现在将在 ${this.settings.autoMatchmakingDelaySeconds} 秒后开始匹配`) + this.state.setSearchMatchAt(Date.now() + this.settings.autoMatchmakingDelaySeconds * 1e3) + this._autoSearchMatchTimerId = setTimeout( + () => this._startMatchmaking(), + this.settings.autoMatchmakingDelaySeconds * 1e3 + ) + + this._sendAutoSearchMatchInfoInChat() + this._autoSearchMatchCountdownTimerId = setInterval( + () => this._sendAutoSearchMatchInfoInChat(), + 1000 + ) + } else if (s === 'unavailable' || s === 'cannot-start-activity') { + this.cancelAutoMatchmaking('normal') + } else { + this.cancelAutoMatchmaking(s) + } + }, + { equals: comparer.shallow, fireImmediately: true } + ) + + const simplifiedSearchState = computed(() => { + if (!this._lc.data.matchmaking.search) { + return null + } + + return { + timeInQueue: this._lc.data.matchmaking.search.timeInQueue, + estimatedQueueTime: this._lc.data.matchmaking.search.estimatedQueueTime, + searchState: this._lc.data.matchmaking.search.searchState, + lowPriorityData: this._lc.data.matchmaking.search.lowPriorityData, + isCurrentlyInQueue: this._lc.data.matchmaking.search.isCurrentlyInQueue + } + }) + + let penaltyTime = 0 + this._mobx.reaction( + () => Boolean(simplifiedSearchState.get()), + (hasSearchState) => { + if (hasSearchState) { + penaltyTime = simplifiedSearchState.get()?.lowPriorityData.penaltyTime || 0 + } else { + penaltyTime = 0 + } + }, + { fireImmediately: true } + ) + + this._mobx.reaction( + () => + [ + simplifiedSearchState.get(), + this.settings.autoMatchmakingRematchStrategy, + this.settings.autoMatchmakingRematchFixedDuration + ] as const, + ([s, st, d]) => { + if (st === 'never' || !s || s.searchState !== 'Searching') { + return + } + + if (!s.isCurrentlyInQueue) { + return + } + + if (st === 'fixed-duration') { + if (s.timeInQueue - penaltyTime >= d) { + this._lc.api.lobby.deleteSearchMatch().catch((e) => { + this._log.warn(`尝试取消匹配时失败 ${formatError(e)}`) + }) + return + } + } else if (st === 'estimated-duration') { + if (s.timeInQueue - penaltyTime >= s.estimatedQueueTime) { + this._lc.api.lobby.deleteSearchMatch().catch((e) => { + this._log.warn(`尝试取消匹配时失败 ${formatError(e)}`) + }) + } + } + }, + { equals: comparer.structural, fireImmediately: true } + ) + } + + private async _acceptMatch() { + try { + await this._lc.api.matchmaking.accept() + } catch (error) { + this._ipc.sendEvent(AutoGameflowMain.id, 'error-accept-match', formatError(error)) + this._log.warn(`无法接受对局 ${formatError(error)}`) + } + this.state.clearAutoAccept() + this._autoSearchMatchTimerId = null + } + + private async _startMatchmaking() { + try { + if (this._autoSearchMatchCountdownTimerId) { + clearInterval(this._autoSearchMatchCountdownTimerId) + this._autoSearchMatchCountdownTimerId = null + } + this.state.clearAutoSearchMatch() + this._autoSearchMatchTimerId = null + await this._lc.api.lobby.searchMatch() + } catch (error) { + this._ipc.sendEvent(AutoGameflowMain.id, 'error-matchmaking', formatError(error)) + this._log.warn(`无法开始匹配 ${formatError(error)}`) + } + } + + private async _playAgainFn() { + try { + this._log.info('Play again, 返回房间') + await this._lc.api.lobby.playAgain() + } catch (error) { + this._log.warn(`尝试 Play again 时失败`, error) + } + } + + private async _dodgeFn() { + try { + this._log.info('Dodge, 秒退') + await this._lc.api.login.dodge() + } catch (error) { + this._log.warn(`尝试秒退时失败`, error) + } finally { + this.state.setDodgeAt(-1) + } + } + + private async _reconnectFn() { + try { + this._log.info('Reconnect! 尝试重新连接') + await this._lc.api.gameflow.reconnect() + } catch (error) { + this._log.warn(`尝试重新连接失败`, error) + } + } + + private async _handleState() { + await this._setting.applyToState() + + this._setting.onChange('dodgeAtLastSecondThreshold', async (v, { setter }) => { + if (v < 0) { + v = 0 + } + + this.settings.setDodgeAtLastSecondThreshold(v) + await setter() + }) + + this._setting.onChange('autoAcceptEnabled', async (v, { setter }) => { + if (!v) { + this.cancelAutoAccept('normal') + } + + this.settings.setAutoAcceptEnabled(v) + await setter() + }) + + this._mobx.propSync(AutoGameflowMain.id, 'settings', this.settings, [ + 'autoAcceptDelaySeconds', + 'autoAcceptEnabled', + 'autoHonorEnabled', + 'autoHonorStrategy', + 'autoMatchmakingDelaySeconds', + 'autoMatchmakingEnabled', + 'autoMatchmakingMinimumMembers', + 'autoMatchmakingRematchFixedDuration', + 'autoMatchmakingRematchStrategy', + 'autoMatchmakingWaitForInvitees', + 'playAgainEnabled', + 'dodgeAtLastSecondThreshold', + 'autoHandleInvitationsEnabled', + 'autoReconnectEnabled', + 'autoMatchmakingMaximumMatchDuration', + 'invitationHandlingStrategies' + ]) + + this._mobx.propSync(AutoGameflowMain.id, 'state', this.state, [ + 'willAccept', + 'willAcceptAt', + 'willSearchMatch', + 'willSearchMatchAt', + 'activityStartStatus', + 'willDodgeAt', + 'willDodgeAtLastSecond' + ]) + } + + cancelAutoAccept(reason?: string) { + if (this.state.willAccept) { + if (this._autoAcceptTimerId) { + clearTimeout(this._autoAcceptTimerId) + this._autoAcceptTimerId = null + } + this.state.clearAutoAccept() + if (reason === 'accepted') { + this._log.info(`取消了即将进行的接受 - 已经被接受`) + } else if (reason === 'declined') { + this._log.info(`取消了即将进行的接受 - 已经被拒绝`) + } else { + this._log.info(`取消了即将进行的接受`) + } + } + } + + cancelAutoMatchmaking(reason?: string) { + if (this.state.willSearchMatch) { + if (this._autoSearchMatchTimerId) { + clearTimeout(this._autoSearchMatchTimerId) + this._autoSearchMatchTimerId = null + } + if (this._autoSearchMatchCountdownTimerId) { + this._sendAutoSearchMatchInfoInChat(reason) + clearInterval(this._autoSearchMatchCountdownTimerId) + this._autoSearchMatchCountdownTimerId = null + } + + this.state.clearAutoSearchMatch() + this._log.info(`即将进行的自动匹配对局已取消,${reason || '未知'}`) + } + } + + private _sendAutoSearchMatchInfoInChat = async (cancel?: string) => { + if (this._lc.data.chat.conversations.customGame && this.state.willSearchMatch) { + if (cancel === 'normal') { + this._lc.api.chat + .chatSend( + this._lc.data.chat.conversations.customGame.id, + `[League Akari] 自动匹配已取消`, + 'celebration' + ) + .catch(() => {}) + return + } else if (cancel === 'waiting-for-invitee') { + this._lc.api.chat + .chatSend( + this._lc.data.chat.conversations.customGame.id, + `[League Akari] 自动匹配已取消,等待被邀请者`, + 'celebration' + ) + .catch(() => {}) + return + } else if (cancel === 'not-the-leader') { + this._lc.api.chat + .chatSend( + this._lc.data.chat.conversations.customGame.id, + `[League Akari] 自动匹配已取消,当前不是房间房主`, + 'celebration' + ) + .catch(() => {}) + return + } else if (cancel === 'waiting-for-penalty-time') { + this._lc.api.chat + .chatSend( + this._lc.data.chat.conversations.customGame.id, + `[League Akari] 自动匹配已取消,等待秒退计时器`, + 'celebration' + ) + .catch(() => {}) + return + } + + const time = (this.state.willSearchMatchAt - Date.now()) / 1e3 + this._lc.api.chat + .chatSend( + this._lc.data.chat.conversations.customGame.id, + `[League Akari] 将在 ${Math.abs(time).toFixed()} 秒后自动匹配`, + 'celebration' + ) + .catch(() => {}) + } + } + + async onInit() { + await this._handleState() + this._handleIpcCall() + this._handleAutoBallot() + this._handleAutoAccept() + this._handleAutoPlayAgain() + this._handleAutoReconnect() + this._handleAutoHandleInvitation() + this._handleLogging() + this._handleLastSecondDodge() + this._handleAutoSearchMatch() + } + + async onDispose() {} +} diff --git a/src/main/shards/auto-gameflow/state.ts b/src/main/shards/auto-gameflow/state.ts index 53ddc088..64740a0a 100644 --- a/src/main/shards/auto-gameflow/state.ts +++ b/src/main/shards/auto-gameflow/state.ts @@ -1,220 +1,220 @@ -import { makeAutoObservable, observable } from 'mobx' - -import { LeagueClientSyncedData } from '../league-client/data' - -export type AutoHonorStrategy = - | 'prefer-lobby-member' // 随机优先组队时房间内成员 - | 'only-lobby-member' // 随机仅限组队时房间内成员 - | 'all-member' // 随机所有可点赞玩家 - | 'opt-out' // 直接跳过 - | 'all-member-including-opponent' // 随机所有可点赞玩家,包括对手 - -export type AutoMatchmakingStrategy = 'never' | 'fixed-duration' | 'estimated-duration' - -export class AutoGameflowSettings { - autoHonorEnabled: boolean = false - autoHonorStrategy: AutoHonorStrategy = 'prefer-lobby-member' - - playAgainEnabled: boolean = false - - autoAcceptEnabled: boolean = false - autoAcceptDelaySeconds: number = 0 - - autoReconnectEnabled: boolean = false - - autoMatchmakingEnabled: boolean = false - autoMatchmakingMaximumMatchDuration: number = 0 - autoMatchmakingRematchStrategy: AutoMatchmakingStrategy = 'never' - autoMatchmakingRematchFixedDuration: number = 2 - autoMatchmakingDelaySeconds: number = 5 - autoMatchmakingMinimumMembers = 1 // 最低满足人数 - autoMatchmakingWaitForInvitees: boolean = true // 等待邀请中的用户 - - autoHandleInvitationsEnabled: boolean = false - invitationHandlingStrategies: Record = {} - - dodgeAtLastSecondThreshold: number = 2 - - setAutoHonorEnabled(enabled: boolean) { - this.autoHonorEnabled = enabled - } - - setAutoHonorStrategy(strategy: AutoHonorStrategy) { - this.autoHonorStrategy = strategy - } - - setPlayAgainEnabled(enabled: boolean) { - this.playAgainEnabled = enabled - } - - setAutoAcceptEnabled(enabled: boolean) { - this.autoAcceptEnabled = enabled - } - - setAutoAcceptDelaySeconds(seconds: number) { - this.autoAcceptDelaySeconds = seconds - } - - setAutoReconnectEnabled(enabled: boolean) { - this.autoReconnectEnabled = enabled - } - - setAutoMatchmakingEnabled(enabled: boolean) { - this.autoMatchmakingEnabled = enabled - } - - setAutoMatchmakingDelaySeconds(seconds: number) { - this.autoMatchmakingDelaySeconds = seconds - } - - setAutoMatchmakingMinimumMembers(count: number) { - this.autoMatchmakingMinimumMembers = count - } - - setAutoMatchmakingWaitForInvitees(yes: boolean) { - this.autoMatchmakingWaitForInvitees = yes - } - - setAutoMatchmakingRematchStrategy(s: AutoMatchmakingStrategy) { - this.autoMatchmakingRematchStrategy = s - } - - setAutoMatchmakingRematchFixedDuration(seconds: number) { - this.autoMatchmakingRematchFixedDuration = seconds - } - - setautoHandleInvitationsEnabled(enabled: boolean) { - this.autoHandleInvitationsEnabled = enabled - } - - setDodgeAtLastSecondThreshold(threshold: number) { - this.dodgeAtLastSecondThreshold = threshold - } - - setInvitationHandlingStrategies(strategies: Record) { - this.invitationHandlingStrategies = strategies - } - - constructor() { - makeAutoObservable(this, { - invitationHandlingStrategies: observable.struct - }) - } -} - -export class AutoGameflowState { - /** - * 即将进行自动接受操作 - */ - willAccept: boolean = false - - /** - * 即将进行的自动接受操作将在指定时间戳完成 - */ - willAcceptAt: number = -1 - - willSearchMatch: boolean = false - - /** - * 即将进行的匹配开始的时间 - */ - willSearchMatchAt: number = -1 - - /** - * 即将进行的秒退操作将在指定时间执行 - */ - willDodgeAt: number = -1 - - /** - * 是否在最后一秒秒退 - */ - willDodgeAtLastSecond: boolean = false - - get activityStartStatus() { - if (!this._lcData.lobby.lobby) { - return 'unavailable' - } - - if (this._lcData.gameflow.session?.gameData.isCustomGame) { - return 'unavailable' - } - - const self = this._lcData.lobby.lobby.members.find( - (m) => m.puuid === this._lcData.summoner.me?.puuid - ) - - if (self) { - if (!self.isLeader) { - return 'not-the-leader' - } - } else { - return 'unavailable' - } - - if (this._lcData.matchmaking.search) { - const errors = this._lcData.matchmaking.search.errors - const maxPenaltyTime = errors.reduce( - (prev, cur) => Math.max(cur.penaltyTimeRemaining, prev), - -Infinity - ) - - if (maxPenaltyTime > 0) { - return 'waiting-for-penalty-time' - } - } - - if (this.settings.autoMatchmakingWaitForInvitees) { - const hasPendingInvitation = this._lcData.lobby.lobby.invitations.some( - (i) => i.state === 'Pending' - ) - if (hasPendingInvitation) { - return 'waiting-for-invitees' - } - } - - if (this._lcData.lobby.lobby.members.length < this.settings.autoMatchmakingMinimumMembers) { - return 'insufficient-members' - } - - if (this._lcData.lobby.lobby.canStartActivity) { - return 'can-start-activity' - } else { - return 'cannot-start-activity' - } - } - - setAcceptAt(at: number) { - this.willAccept = true - this.willAcceptAt = at - } - - setSearchMatchAt(at: number) { - this.willSearchMatch = true - this.willSearchMatchAt = at - } - - setWillDodgeAtLastSecond(yes: boolean) { - this.willDodgeAtLastSecond = yes - } - - setDodgeAt(at: number) { - this.willDodgeAt = at - } - - clearAutoAccept() { - this.willAccept = false - this.willAcceptAt = -1 - } - - clearAutoSearchMatch() { - this.willSearchMatch = false - this.willSearchMatchAt = -1 - } - - constructor( - private readonly _lcData: LeagueClientSyncedData, - private readonly settings: AutoGameflowSettings - ) { - makeAutoObservable(this) - } -} +import { makeAutoObservable, observable } from 'mobx' + +import { LeagueClientSyncedData } from '../league-client/data' + +export type AutoHonorStrategy = + | 'prefer-lobby-member' // 随机优先组队时房间内成员 + | 'only-lobby-member' // 随机仅限组队时房间内成员 + | 'all-member' // 随机所有可点赞玩家 + | 'opt-out' // 直接跳过 + | 'all-member-including-opponent' // 随机所有可点赞玩家,包括对手 + +export type AutoMatchmakingStrategy = 'never' | 'fixed-duration' | 'estimated-duration' + +export class AutoGameflowSettings { + autoHonorEnabled: boolean = false + autoHonorStrategy: AutoHonorStrategy = 'prefer-lobby-member' + + playAgainEnabled: boolean = false + + autoAcceptEnabled: boolean = false + autoAcceptDelaySeconds: number = 0 + + autoReconnectEnabled: boolean = false + + autoMatchmakingEnabled: boolean = false + autoMatchmakingMaximumMatchDuration: number = 0 + autoMatchmakingRematchStrategy: AutoMatchmakingStrategy = 'never' + autoMatchmakingRematchFixedDuration: number = 2 + autoMatchmakingDelaySeconds: number = 5 + autoMatchmakingMinimumMembers = 1 // 最低满足人数 + autoMatchmakingWaitForInvitees: boolean = true // 等待邀请中的用户 + + autoHandleInvitationsEnabled: boolean = false + invitationHandlingStrategies: Record = {} + + dodgeAtLastSecondThreshold: number = 2 + + setAutoHonorEnabled(enabled: boolean) { + this.autoHonorEnabled = enabled + } + + setAutoHonorStrategy(strategy: AutoHonorStrategy) { + this.autoHonorStrategy = strategy + } + + setPlayAgainEnabled(enabled: boolean) { + this.playAgainEnabled = enabled + } + + setAutoAcceptEnabled(enabled: boolean) { + this.autoAcceptEnabled = enabled + } + + setAutoAcceptDelaySeconds(seconds: number) { + this.autoAcceptDelaySeconds = seconds + } + + setAutoReconnectEnabled(enabled: boolean) { + this.autoReconnectEnabled = enabled + } + + setAutoMatchmakingEnabled(enabled: boolean) { + this.autoMatchmakingEnabled = enabled + } + + setAutoMatchmakingDelaySeconds(seconds: number) { + this.autoMatchmakingDelaySeconds = seconds + } + + setAutoMatchmakingMinimumMembers(count: number) { + this.autoMatchmakingMinimumMembers = count + } + + setAutoMatchmakingWaitForInvitees(yes: boolean) { + this.autoMatchmakingWaitForInvitees = yes + } + + setAutoMatchmakingRematchStrategy(s: AutoMatchmakingStrategy) { + this.autoMatchmakingRematchStrategy = s + } + + setAutoMatchmakingRematchFixedDuration(seconds: number) { + this.autoMatchmakingRematchFixedDuration = seconds + } + + setAutoHandleInvitationsEnabled(enabled: boolean) { + this.autoHandleInvitationsEnabled = enabled + } + + setDodgeAtLastSecondThreshold(threshold: number) { + this.dodgeAtLastSecondThreshold = threshold + } + + setInvitationHandlingStrategies(strategies: Record) { + this.invitationHandlingStrategies = strategies + } + + constructor() { + makeAutoObservable(this, { + invitationHandlingStrategies: observable.struct + }) + } +} + +export class AutoGameflowState { + /** + * 即将进行自动接受操作 + */ + willAccept: boolean = false + + /** + * 即将进行的自动接受操作将在指定时间戳完成 + */ + willAcceptAt: number = -1 + + willSearchMatch: boolean = false + + /** + * 即将进行的匹配开始的时间 + */ + willSearchMatchAt: number = -1 + + /** + * 即将进行的秒退操作将在指定时间执行 + */ + willDodgeAt: number = -1 + + /** + * 是否在最后一秒秒退 + */ + willDodgeAtLastSecond: boolean = false + + get activityStartStatus() { + if (!this._lcData.lobby.lobby) { + return 'unavailable' + } + + if (this._lcData.gameflow.session?.gameData.isCustomGame) { + return 'unavailable' + } + + const self = this._lcData.lobby.lobby.members.find( + (m) => m.puuid === this._lcData.summoner.me?.puuid + ) + + if (self) { + if (!self.isLeader) { + return 'not-the-leader' + } + } else { + return 'unavailable' + } + + if (this._lcData.matchmaking.search) { + const errors = this._lcData.matchmaking.search.errors + const maxPenaltyTime = errors.reduce( + (prev, cur) => Math.max(cur.penaltyTimeRemaining, prev), + -Infinity + ) + + if (maxPenaltyTime > 0) { + return 'waiting-for-penalty-time' + } + } + + if (this.settings.autoMatchmakingWaitForInvitees) { + const hasPendingInvitation = this._lcData.lobby.lobby.invitations.some( + (i) => i.state === 'Pending' + ) + if (hasPendingInvitation) { + return 'waiting-for-invitees' + } + } + + if (this._lcData.lobby.lobby.members.length < this.settings.autoMatchmakingMinimumMembers) { + return 'insufficient-members' + } + + if (this._lcData.lobby.lobby.canStartActivity) { + return 'can-start-activity' + } else { + return 'cannot-start-activity' + } + } + + setAcceptAt(at: number) { + this.willAccept = true + this.willAcceptAt = at + } + + setSearchMatchAt(at: number) { + this.willSearchMatch = true + this.willSearchMatchAt = at + } + + setWillDodgeAtLastSecond(yes: boolean) { + this.willDodgeAtLastSecond = yes + } + + setDodgeAt(at: number) { + this.willDodgeAt = at + } + + clearAutoAccept() { + this.willAccept = false + this.willAcceptAt = -1 + } + + clearAutoSearchMatch() { + this.willSearchMatch = false + this.willSearchMatchAt = -1 + } + + constructor( + private readonly _lcData: LeagueClientSyncedData, + private readonly settings: AutoGameflowSettings + ) { + makeAutoObservable(this) + } +} diff --git a/src/main/shards/auto-reply/index.ts b/src/main/shards/auto-reply/index.ts index 51564110..fadedd65 100644 --- a/src/main/shards/auto-reply/index.ts +++ b/src/main/shards/auto-reply/index.ts @@ -8,7 +8,7 @@ import { LeagueClientMain } from '../league-client' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' import { MobxUtilsMain } from '../mobx-utils' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { AutoReplySettings } from './state' /** @@ -30,7 +30,7 @@ export class AutoReplyMain implements IAkariShardInitDispose { private readonly _settingFactory: SettingFactoryMain private readonly _log: AkariLogger private readonly _lc: LeagueClientMain - private readonly _setting: MobxSettingService + private readonly _setting: SetterSettingService private readonly _mobx: MobxUtilsMain private readonly _ipc: AkariIpcMain diff --git a/src/main/shards/auto-select/index.ts b/src/main/shards/auto-select/index.ts index f05fd094..d882aaa4 100644 --- a/src/main/shards/auto-select/index.ts +++ b/src/main/shards/auto-select/index.ts @@ -1,499 +1,501 @@ -import { IAkariShardInitDispose } from '@shared/akari-shard/interface' -import { formatError } from '@shared/utils/errors' -import { comparer, computed } from 'mobx' - -import { AkariIpcMain } from '../ipc' -import { LeagueClientMain } from '../league-client' -import { AkariLogger, LoggerFactoryMain } from '../logger-factory' -import { MobxUtilsMain } from '../mobx-utils' -import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' -import { AutoSelectSettings, AutoSelectState } from './state' - -export class AutoSelectMain implements IAkariShardInitDispose { - static id = 'auto-select-main' - static dependencies = [ - 'logger-factory-main', - 'setting-factory-main', - 'league-client-main', - 'akari-ipc-main', - 'mobx-utils-main' - ] - - private readonly _loggerFactory: LoggerFactoryMain - private readonly _settingFactory: SettingFactoryMain - private readonly _log: AkariLogger - private readonly _lc: LeagueClientMain - private readonly _setting: MobxSettingService - private readonly _mobx: MobxUtilsMain - private readonly _ipc: AkariIpcMain - - public readonly settings = new AutoSelectSettings() - public readonly state: AutoSelectState - - private _grabTimerId: NodeJS.Timeout | null = null - - constructor(deps: any) { - this._loggerFactory = deps['logger-factory-main'] - this._log = this._loggerFactory.create(AutoSelectMain.id) - this._lc = deps['league-client-main'] - this._mobx = deps['mobx-utils-main'] - this._ipc = deps['akari-ipc-main'] - this._settingFactory = deps['setting-factory-main'] - this.state = new AutoSelectState(this._lc.data, this.settings) - this._setting = this._settingFactory.create( - AutoSelectMain.id, - { - benchExpectedChampions: { default: this.settings.benchExpectedChampions }, - expectedChampions: { default: this.settings.expectedChampions }, - bannedChampions: { default: this.settings.bannedChampions }, - normalModeEnabled: { default: this.settings.normalModeEnabled }, - selectTeammateIntendedChampion: { default: this.settings.selectTeammateIntendedChampion }, - showIntent: { default: this.settings.showIntent }, - completed: { default: this.settings.completed }, - benchModeEnabled: { default: this.settings.benchModeEnabled }, - benchSelectFirstAvailableChampion: { - default: this.settings.benchSelectFirstAvailableChampion - }, - grabDelaySeconds: { default: this.settings.grabDelaySeconds }, - banEnabled: { default: this.settings.banEnabled }, - banTeammateIntendedChampion: { default: this.settings.banTeammateIntendedChampion } - }, - this.settings - ) - } - - private async _handleState() { - await this._setting.applyToState() - - this._mobx.propSync(AutoSelectMain.id, 'settings', this.settings, [ - 'normalModeEnabled', - 'selectTeammateIntendedChampion', - 'showIntent', - 'completed', - 'benchModeEnabled', - 'benchSelectFirstAvailableChampion', - 'grabDelaySeconds', - 'banEnabled', - 'banTeammateIntendedChampion', - 'benchExpectedChampions', - 'expectedChampions', - 'bannedChampions' - ]) - - this._mobx.propSync(AutoSelectMain.id, 'state', this.state, [ - 'upcomingBan', - 'upcomingPick', - 'upcomingGrab', - 'memberMe' - ]) - } - - async onInit() { - await this._handleState() - } - - private _handleAutoPickBan() { - this._mobx.reaction( - () => this.state.upcomingPick, - async (pick) => { - if (!pick) { - return - } - - if (pick.isActingNow && pick.action.isInProgress) { - if ( - !this.settings.completed && - this.state.champSelectActionInfo?.memberMe.championId === pick.championId - ) { - return - } - - try { - this._log.info( - `现在选择:${pick.championId}, ${this.settings.completed}, actionId=${pick.action.id}` - ) - - await this._lc.api.champSelect.pickOrBan( - pick.championId, - this.settings.completed, - 'pick', - pick.action.id - ) - } catch (error) { - this._ipc.sendEvent(AutoSelectMain.id, 'error-pick', pick.championId) - this._log.warn(`尝试自动执行 pick 时失败, 目标英雄: ${pick.championId}`, error) - } - - return - } - - if (!pick.isActingNow) { - if (!this.settings.showIntent) { - return - } - - if (this.state.champSelectActionInfo?.session.isCustomGame) { - return - } - - if (this.state.champSelectActionInfo?.memberMe.championId) { - return - } - - const thatAction = this.state.champSelectActionInfo?.pick.find( - (a) => a.id === pick.action.id - ) - if (thatAction && thatAction.championId === pick.championId) { - return - } - - try { - this._log.info(`现在预选:${pick.championId}, actionId=${pick.action.id}`) - - await this._lc.api.champSelect.action(pick.action.id, { championId: pick.championId }) - } catch (error) { - this._ipc.sendEvent(AutoSelectMain.id, 'error-pre-pick', pick.championId) - this._log.warn(`尝试自动执行预选时失败, 目标英雄: ${pick.championId}`, error) - } - return - } - } - ) - - this._mobx.reaction( - () => this.state.upcomingBan, - async (ban) => { - if (!ban) { - return - } - - if (ban.action.isInProgress && ban.isActingNow) { - try { - await this._lc.api.champSelect.pickOrBan(ban.championId, true, 'ban', ban.action.id) - } catch (error) { - this._ipc.sendEvent(AutoSelectMain.id, 'error-ban', ban.championId) - this._log.warn(`尝试自动执行 pick 时失败, 目标英雄: ${ban.championId}`, error) - } - } - } - ) - - this._mobx.reaction( - () => this.state.upcomingPick, - (pick) => { - this._log.info(`Upcoming Pick - 即将进行的选择: ${JSON.stringify(pick)}`) - } - ) - - this._mobx.reaction( - () => this.state.upcomingBan, - (ban) => { - this._log.info(`Upcoming Ban - 即将进行的禁用: ${JSON.stringify(ban)}`) - } - ) - - this._mobx.reaction( - () => this.state.upcomingGrab, - (grab) => { - this._log.info(`Upcoming Grab - 即将进行的交换: ${JSON.stringify(grab)}`) - } - ) - - // for logging only - const positionInfo = computed( - () => { - if (!this.state.champSelectActionInfo) { - return null - } - - if (!this.settings.normalModeEnabled || !this.settings.banEnabled) { - return null - } - - const position = this.state.champSelectActionInfo.memberMe.assignedPosition - - const championsBan = this.settings.bannedChampions - const championsPick = this.settings.expectedChampions - - return { - position, - ban: championsBan, - pick: championsPick - } - }, - { equals: comparer.structural } - ) - - this._mobx.reaction( - () => positionInfo.get(), - (info) => { - if (info) { - this._log.info( - `当前分配到位置: ${info.position || '<空>'}, 预设选用英雄: ${JSON.stringify(info.pick)}, 预设禁用英雄: ${JSON.stringify(info.ban)}` - ) - } - } - ) - - this._mobx.reaction( - () => this._lc.data.chat.conversations.championSelect?.id, - (id) => { - if (id && this._lc.data.gameflow.phase === 'ChampSelect') { - if (!this._lc.data.champSelect.session) { - return - } - - const texts: string[] = [] - if (!this._lc.data.champSelect.session.benchEnabled && this.settings.normalModeEnabled) { - texts.push('普通模式自动选择已开启') - } - - if (this._lc.data.champSelect.session.benchEnabled && this.settings.benchModeEnabled) { - texts.push('随机模式自动选择已开启') - } - - if (!this._lc.data.champSelect.session.benchEnabled && this.settings.banEnabled) { - let hasBanAction = false - for (const arr of this._lc.data.champSelect.session.actions) { - if (arr.findIndex((a) => a.type === 'ban') !== -1) { - hasBanAction = true - break - } - } - if (hasBanAction) { - texts.push('自动禁用已开启') - } - } - - if (texts.length) { - this._lc.api.chat - .chatSend(id, `[League Akari] ${texts.join(', ')}`, 'celebration') - .catch(() => {}) - } - } - } - ) - } - - private _handleBenchMode() { - interface BenchChampionInfo { - // 最近一次在英雄选择台上的时间 - lastTimeOnBench: number - } - - // 追踪了英雄选择信息的细节 k = 英雄 ID,v = 英雄信息 - const benchChampions = new Map() - - const diffBenchAndUpdate = (prevBench: number[], newBench: number[], time: number) => { - // 多出来的英雄,新的有但上一次没有 - newBench.forEach((c) => { - if (!prevBench.includes(c)) { - benchChampions.set(c, { lastTimeOnBench: time }) - } - }) - - // 消失的英雄,旧的有但新的没有 - prevBench.forEach((c) => { - if (!newBench.includes(c)) { - benchChampions.delete(c) - } - }) - } - - const simplifiedCsSession = computed(() => { - if (!this._lc.data.champSelect.session) { - return null - } - - const { benchEnabled, localPlayerCellId, benchChampions, myTeam } = - this._lc.data.champSelect.session - - return { benchEnabled, localPlayerCellId, benchChampions, myTeam } - }) - - this._mobx.reaction( - () => - [ - simplifiedCsSession.get(), - this.settings.benchExpectedChampions, - this.settings.benchModeEnabled, - this.settings.benchSelectFirstAvailableChampion - ] as const, - ([session, expected, enabled, onlyFirst], [prevSession]) => { - if (!session) { - // session 被清空的情况, 区分一开始就没有的情况 - if (prevSession) { - benchChampions.clear() - } - return - } - - if (!session.benchEnabled) { - return - } - - // Diff - const now = Date.now() - diffBenchAndUpdate( - prevSession?.benchChampions.map((c) => c.championId) || [], - session.benchChampions.map((c) => c.championId), - now - ) - - if (!enabled) { - if (this.state.upcomingGrab) { - this._log.info( - `关闭了该功能, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` - ) - this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) - clearTimeout(this._grabTimerId!) - this._grabTimerId = null - this.state.setUpcomingGrab(null) - } - return - } - - // 当前会话中可选的英雄 - const availableExpectedChampions = expected.filter( - (c) => - this._lc.data.champSelect.currentPickableChampionIds.has(c) && - !this._lc.data.champSelect.disabledChampionIds.has(c) - ) - const pickableChampionsOnBench = availableExpectedChampions.filter((c) => - benchChampions.has(c) - ) - - // 本次变更, 如果有即将进行的交换, 则根据情况判断是否应该取消 - if (this.state.upcomingGrab) { - if (pickableChampionsOnBench.length === 0) { - this._log.info( - `已无可选英雄, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` - ) - this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) - clearTimeout(this._grabTimerId!) - this._grabTimerId = null - this.state.setUpcomingGrab(null) - return - } - - if (onlyFirst) { - // 对于 onlyFirst 的情况, 如果预计的英雄仍位于可选的第一位, 那么就返回 - if (pickableChampionsOnBench[0] === this.state.upcomingGrab.championId) { - return - } else { - this._log.info( - `已非首选英雄, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` - ) - this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) - clearTimeout(this._grabTimerId!) - this._grabTimerId = null - this.state.setUpcomingGrab(null) - } - } else { - // 对于非 onlyFirst 的情况, 只要目标还在期望列表中,且仍在选择台中, 那么直接返回 - if (pickableChampionsOnBench.includes(this.state.upcomingGrab.championId)) { - return - } else { - this._log.info( - `已不在期望列表中, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` - ) - this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) - clearTimeout(this._grabTimerId!) - this._grabTimerId = null - this.state.setUpcomingGrab(null) - } - } - } - - if (pickableChampionsOnBench.length === 0) { - return - } - - const selfChampionId = session.myTeam.find( - (v) => v.cellId === session.localPlayerCellId - )?.championId - - if (!selfChampionId) { - return - } - - if (onlyFirst) { - // 对于 onlyFirst, 如果手上的英雄优先级比较高, 那么没有必要再次选择 - const indexInHand = availableExpectedChampions.indexOf(selfChampionId) - const indexInFirstPickable = availableExpectedChampions.indexOf( - pickableChampionsOnBench[0] - ) - - if (indexInHand !== -1 && indexInHand < indexInFirstPickable) { - return - } - } else { - // 对于非 onlyFirst, 如果自己的英雄在期望列表中, 那么没有必要再次选择 - if (availableExpectedChampions.includes(selfChampionId)) { - return - } - } - - const newTarget = pickableChampionsOnBench[0] - const waitTime = Math.max( - this.settings.grabDelaySeconds * 1e3 - - (now - benchChampions.get(newTarget)!.lastTimeOnBench), - 0 - ) - - this._log.info(`目标交换英雄: ${newTarget}`) - this.state.setUpcomingGrab(newTarget, Date.now() + waitTime) - this._notifyInChat('select', this.state.upcomingGrab!.championId, waitTime).catch(() => {}) - this._grabTimerId = setTimeout(() => this._trySwap(), waitTime) - }, - { equals: comparer.structural } - ) - - this._mobx.reaction( - () => this._lc.data.gameflow.phase, - (phase) => { - if (phase !== 'ChampSelect' && this.state.upcomingGrab) { - this.state.setUpcomingGrab(null) - this._grabTimerId = null - } - } - ) - } - - private async _notifyInChat(type: 'cancel' | 'select', championId: number, time = 0) { - if (!this._lc.data.chat.conversations.championSelect) { - return - } - - try { - await this._lc.api.chat.chatSend( - this._lc.data.chat.conversations.championSelect.id, - type === 'select' - ? `[League Akari] - [自动选择]: 即将在 ${(time / 1000).toFixed(1)} 秒后选择 ${this._lc.data.gameData.champions[championId]?.name || championId}` - : `[League Akari] - [自动选择]: 已取消选择 ${this._lc.data.gameData.champions[championId]?.name || championId}`, - 'celebration' - ) - } catch (error) { - this._ipc.sendEvent(AutoSelectMain.id, 'error-chat-send', formatError(error)) - this._log.warn(`无法发送信息`, error) - } - } - - private async _trySwap() { - if (!this.state.upcomingGrab) { - return - } - - try { - await this._lc.api.champSelect.benchSwap(this.state.upcomingGrab.championId) - this._log.info(`已交换英雄: ${this.state.upcomingGrab.championId}`) - } catch (error) { - this._ipc.sendEvent(AutoSelectMain.id, 'error-bench-swap', this.state.upcomingGrab.championId) - this._log.warn(`在尝试交换英雄时发生错误`, error) - } finally { - this._grabTimerId = null - this.state.setUpcomingGrab(null) - } - } -} +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { formatError } from '@shared/utils/errors' +import { comparer, computed } from 'mobx' + +import { AkariIpcMain } from '../ipc' +import { LeagueClientMain } from '../league-client' +import { AkariLogger, LoggerFactoryMain } from '../logger-factory' +import { MobxUtilsMain } from '../mobx-utils' +import { SettingFactoryMain } from '../setting-factory' +import { SetterSettingService } from '../setting-factory/setter-setting-service' +import { AutoSelectSettings, AutoSelectState } from './state' + +export class AutoSelectMain implements IAkariShardInitDispose { + static id = 'auto-select-main' + static dependencies = [ + 'logger-factory-main', + 'setting-factory-main', + 'league-client-main', + 'akari-ipc-main', + 'mobx-utils-main' + ] + + private readonly _loggerFactory: LoggerFactoryMain + private readonly _settingFactory: SettingFactoryMain + private readonly _log: AkariLogger + private readonly _lc: LeagueClientMain + private readonly _setting: SetterSettingService + private readonly _mobx: MobxUtilsMain + private readonly _ipc: AkariIpcMain + + public readonly settings = new AutoSelectSettings() + public readonly state: AutoSelectState + + private _grabTimerId: NodeJS.Timeout | null = null + + constructor(deps: any) { + this._loggerFactory = deps['logger-factory-main'] + this._log = this._loggerFactory.create(AutoSelectMain.id) + this._lc = deps['league-client-main'] + this._mobx = deps['mobx-utils-main'] + this._ipc = deps['akari-ipc-main'] + this._settingFactory = deps['setting-factory-main'] + this.state = new AutoSelectState(this._lc.data, this.settings) + this._setting = this._settingFactory.create( + AutoSelectMain.id, + { + benchExpectedChampions: { default: this.settings.benchExpectedChampions }, + expectedChampions: { default: this.settings.expectedChampions }, + bannedChampions: { default: this.settings.bannedChampions }, + normalModeEnabled: { default: this.settings.normalModeEnabled }, + selectTeammateIntendedChampion: { default: this.settings.selectTeammateIntendedChampion }, + showIntent: { default: this.settings.showIntent }, + completed: { default: this.settings.completed }, + benchModeEnabled: { default: this.settings.benchModeEnabled }, + benchSelectFirstAvailableChampion: { + default: this.settings.benchSelectFirstAvailableChampion + }, + grabDelaySeconds: { default: this.settings.grabDelaySeconds }, + banEnabled: { default: this.settings.banEnabled }, + banTeammateIntendedChampion: { default: this.settings.banTeammateIntendedChampion } + }, + this.settings + ) + } + + private async _handleState() { + await this._setting.applyToState() + + this._mobx.propSync(AutoSelectMain.id, 'settings', this.settings, [ + 'normalModeEnabled', + 'selectTeammateIntendedChampion', + 'showIntent', + 'completed', + 'benchModeEnabled', + 'benchSelectFirstAvailableChampion', + 'grabDelaySeconds', + 'banEnabled', + 'banTeammateIntendedChampion', + 'benchExpectedChampions', + 'expectedChampions', + 'bannedChampions' + ]) + + this._mobx.propSync(AutoSelectMain.id, 'state', this.state, [ + 'upcomingBan', + 'upcomingPick', + 'upcomingGrab', + 'memberMe' + ]) + } + + async onInit() { + await this._handleState() + this._handleAutoPickBan() + this._handleBenchMode() + } + + private _handleAutoPickBan() { + this._mobx.reaction( + () => this.state.upcomingPick, + async (pick) => { + if (!pick) { + return + } + + if (pick.isActingNow && pick.action.isInProgress) { + if ( + !this.settings.completed && + this.state.champSelectActionInfo?.memberMe.championId === pick.championId + ) { + return + } + + try { + this._log.info( + `现在选择:${pick.championId}, ${this.settings.completed}, actionId=${pick.action.id}` + ) + + await this._lc.api.champSelect.pickOrBan( + pick.championId, + this.settings.completed, + 'pick', + pick.action.id + ) + } catch (error) { + this._ipc.sendEvent(AutoSelectMain.id, 'error-pick', pick.championId) + this._log.warn(`尝试自动执行 pick 时失败, 目标英雄: ${pick.championId}`, error) + } + + return + } + + if (!pick.isActingNow) { + if (!this.settings.showIntent) { + return + } + + if (this.state.champSelectActionInfo?.session.isCustomGame) { + return + } + + if (this.state.champSelectActionInfo?.memberMe.championId) { + return + } + + const thatAction = this.state.champSelectActionInfo?.pick.find( + (a) => a.id === pick.action.id + ) + if (thatAction && thatAction.championId === pick.championId) { + return + } + + try { + this._log.info(`现在预选:${pick.championId}, actionId=${pick.action.id}`) + + await this._lc.api.champSelect.action(pick.action.id, { championId: pick.championId }) + } catch (error) { + this._ipc.sendEvent(AutoSelectMain.id, 'error-pre-pick', pick.championId) + this._log.warn(`尝试自动执行预选时失败, 目标英雄: ${pick.championId}`, error) + } + return + } + } + ) + + this._mobx.reaction( + () => this.state.upcomingBan, + async (ban) => { + if (!ban) { + return + } + + if (ban.action.isInProgress && ban.isActingNow) { + try { + await this._lc.api.champSelect.pickOrBan(ban.championId, true, 'ban', ban.action.id) + } catch (error) { + this._ipc.sendEvent(AutoSelectMain.id, 'error-ban', ban.championId) + this._log.warn(`尝试自动执行 pick 时失败, 目标英雄: ${ban.championId}`, error) + } + } + } + ) + + this._mobx.reaction( + () => this.state.upcomingPick, + (pick) => { + this._log.info(`Upcoming Pick - 即将进行的选择: ${JSON.stringify(pick)}`) + } + ) + + this._mobx.reaction( + () => this.state.upcomingBan, + (ban) => { + this._log.info(`Upcoming Ban - 即将进行的禁用: ${JSON.stringify(ban)}`) + } + ) + + this._mobx.reaction( + () => this.state.upcomingGrab, + (grab) => { + this._log.info(`Upcoming Grab - 即将进行的交换: ${JSON.stringify(grab)}`) + } + ) + + // for logging only + const positionInfo = computed( + () => { + if (!this.state.champSelectActionInfo) { + return null + } + + if (!this.settings.normalModeEnabled || !this.settings.banEnabled) { + return null + } + + const position = this.state.champSelectActionInfo.memberMe.assignedPosition + + const championsBan = this.settings.bannedChampions + const championsPick = this.settings.expectedChampions + + return { + position, + ban: championsBan, + pick: championsPick + } + }, + { equals: comparer.structural } + ) + + this._mobx.reaction( + () => positionInfo.get(), + (info) => { + if (info) { + this._log.info( + `当前分配到位置: ${info.position || '<空>'}, 预设选用英雄: ${JSON.stringify(info.pick)}, 预设禁用英雄: ${JSON.stringify(info.ban)}` + ) + } + } + ) + + this._mobx.reaction( + () => this._lc.data.chat.conversations.championSelect?.id, + (id) => { + if (id && this._lc.data.gameflow.phase === 'ChampSelect') { + if (!this._lc.data.champSelect.session) { + return + } + + const texts: string[] = [] + if (!this._lc.data.champSelect.session.benchEnabled && this.settings.normalModeEnabled) { + texts.push('普通模式自动选择已开启') + } + + if (this._lc.data.champSelect.session.benchEnabled && this.settings.benchModeEnabled) { + texts.push('随机模式自动选择已开启') + } + + if (!this._lc.data.champSelect.session.benchEnabled && this.settings.banEnabled) { + let hasBanAction = false + for (const arr of this._lc.data.champSelect.session.actions) { + if (arr.findIndex((a) => a.type === 'ban') !== -1) { + hasBanAction = true + break + } + } + if (hasBanAction) { + texts.push('自动禁用已开启') + } + } + + if (texts.length) { + this._lc.api.chat + .chatSend(id, `[League Akari] ${texts.join(', ')}`, 'celebration') + .catch(() => {}) + } + } + } + ) + } + + private _handleBenchMode() { + interface BenchChampionInfo { + // 最近一次在英雄选择台上的时间 + lastTimeOnBench: number + } + + // 追踪了英雄选择信息的细节 k = 英雄 ID,v = 英雄信息 + const benchChampions = new Map() + + const diffBenchAndUpdate = (prevBench: number[], newBench: number[], time: number) => { + // 多出来的英雄,新的有但上一次没有 + newBench.forEach((c) => { + if (!prevBench.includes(c)) { + benchChampions.set(c, { lastTimeOnBench: time }) + } + }) + + // 消失的英雄,旧的有但新的没有 + prevBench.forEach((c) => { + if (!newBench.includes(c)) { + benchChampions.delete(c) + } + }) + } + + const simplifiedCsSession = computed(() => { + if (!this._lc.data.champSelect.session) { + return null + } + + const { benchEnabled, localPlayerCellId, benchChampions, myTeam } = + this._lc.data.champSelect.session + + return { benchEnabled, localPlayerCellId, benchChampions, myTeam } + }) + + this._mobx.reaction( + () => + [ + simplifiedCsSession.get(), + this.settings.benchExpectedChampions, + this.settings.benchModeEnabled, + this.settings.benchSelectFirstAvailableChampion + ] as const, + ([session, expected, enabled, onlyFirst], [prevSession]) => { + if (!session) { + // session 被清空的情况, 区分一开始就没有的情况 + if (prevSession) { + benchChampions.clear() + } + return + } + + if (!session.benchEnabled) { + return + } + + // Diff + const now = Date.now() + diffBenchAndUpdate( + prevSession?.benchChampions.map((c) => c.championId) || [], + session.benchChampions.map((c) => c.championId), + now + ) + + if (!enabled) { + if (this.state.upcomingGrab) { + this._log.info( + `关闭了该功能, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` + ) + this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) + clearTimeout(this._grabTimerId!) + this._grabTimerId = null + this.state.setUpcomingGrab(null) + } + return + } + + // 当前会话中可选的英雄 + const availableExpectedChampions = expected.filter( + (c) => + this._lc.data.champSelect.currentPickableChampionIds.has(c) && + !this._lc.data.champSelect.disabledChampionIds.has(c) + ) + const pickableChampionsOnBench = availableExpectedChampions.filter((c) => + benchChampions.has(c) + ) + + // 本次变更, 如果有即将进行的交换, 则根据情况判断是否应该取消 + if (this.state.upcomingGrab) { + if (pickableChampionsOnBench.length === 0) { + this._log.info( + `已无可选英雄, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` + ) + this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) + clearTimeout(this._grabTimerId!) + this._grabTimerId = null + this.state.setUpcomingGrab(null) + return + } + + if (onlyFirst) { + // 对于 onlyFirst 的情况, 如果预计的英雄仍位于可选的第一位, 那么就返回 + if (pickableChampionsOnBench[0] === this.state.upcomingGrab.championId) { + return + } else { + this._log.info( + `已非首选英雄, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` + ) + this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) + clearTimeout(this._grabTimerId!) + this._grabTimerId = null + this.state.setUpcomingGrab(null) + } + } else { + // 对于非 onlyFirst 的情况, 只要目标还在期望列表中,且仍在选择台中, 那么直接返回 + if (pickableChampionsOnBench.includes(this.state.upcomingGrab.championId)) { + return + } else { + this._log.info( + `已不在期望列表中, 取消即将进行的交换:ID:${this.state.upcomingGrab.championId}` + ) + this._notifyInChat('cancel', this.state.upcomingGrab.championId).catch(() => {}) + clearTimeout(this._grabTimerId!) + this._grabTimerId = null + this.state.setUpcomingGrab(null) + } + } + } + + if (pickableChampionsOnBench.length === 0) { + return + } + + const selfChampionId = session.myTeam.find( + (v) => v.cellId === session.localPlayerCellId + )?.championId + + if (!selfChampionId) { + return + } + + if (onlyFirst) { + // 对于 onlyFirst, 如果手上的英雄优先级比较高, 那么没有必要再次选择 + const indexInHand = availableExpectedChampions.indexOf(selfChampionId) + const indexInFirstPickable = availableExpectedChampions.indexOf( + pickableChampionsOnBench[0] + ) + + if (indexInHand !== -1 && indexInHand < indexInFirstPickable) { + return + } + } else { + // 对于非 onlyFirst, 如果自己的英雄在期望列表中, 那么没有必要再次选择 + if (availableExpectedChampions.includes(selfChampionId)) { + return + } + } + + const newTarget = pickableChampionsOnBench[0] + const waitTime = Math.max( + this.settings.grabDelaySeconds * 1e3 - + (now - benchChampions.get(newTarget)!.lastTimeOnBench), + 0 + ) + + this._log.info(`目标交换英雄: ${newTarget}`) + this.state.setUpcomingGrab(newTarget, Date.now() + waitTime) + this._notifyInChat('select', this.state.upcomingGrab!.championId, waitTime).catch(() => {}) + this._grabTimerId = setTimeout(() => this._trySwap(), waitTime) + }, + { equals: comparer.structural } + ) + + this._mobx.reaction( + () => this._lc.data.gameflow.phase, + (phase) => { + if (phase !== 'ChampSelect' && this.state.upcomingGrab) { + this.state.setUpcomingGrab(null) + this._grabTimerId = null + } + } + ) + } + + private async _notifyInChat(type: 'cancel' | 'select', championId: number, time = 0) { + if (!this._lc.data.chat.conversations.championSelect) { + return + } + + try { + await this._lc.api.chat.chatSend( + this._lc.data.chat.conversations.championSelect.id, + type === 'select' + ? `[League Akari] - [自动选择]: 即将在 ${(time / 1000).toFixed(1)} 秒后选择 ${this._lc.data.gameData.champions[championId]?.name || championId}` + : `[League Akari] - [自动选择]: 已取消选择 ${this._lc.data.gameData.champions[championId]?.name || championId}`, + 'celebration' + ) + } catch (error) { + this._ipc.sendEvent(AutoSelectMain.id, 'error-chat-send', formatError(error)) + this._log.warn(`无法发送信息`, error) + } + } + + private async _trySwap() { + if (!this.state.upcomingGrab) { + return + } + + try { + await this._lc.api.champSelect.benchSwap(this.state.upcomingGrab.championId) + this._log.info(`已交换英雄: ${this.state.upcomingGrab.championId}`) + } catch (error) { + this._ipc.sendEvent(AutoSelectMain.id, 'error-bench-swap', this.state.upcomingGrab.championId) + this._log.warn(`在尝试交换英雄时发生错误`, error) + } finally { + this._grabTimerId = null + this.state.setUpcomingGrab(null) + } + } +} diff --git a/src/main/shards/game-client/index.ts b/src/main/shards/game-client/index.ts index 3ba9d22e..49de0fb8 100644 --- a/src/main/shards/game-client/index.ts +++ b/src/main/shards/game-client/index.ts @@ -6,10 +6,11 @@ import https from 'https' import toolkit from '../../native/laToolkitWin32x64.node' import { AkariIpcMain } from '../ipc' +import { KeyboardShortcutsMain } from '../keyboard-shortcuts' import { LeagueClientMain } from '../league-client' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { SgpMain } from '../sgp' import { GameClientSettings } from './state' @@ -29,7 +30,8 @@ export class GameClientMain implements IAkariShardInitDispose { 'logger-factory-main', 'setting-factory-main', 'sgp-main', - 'league-client-main' + 'league-client-main', + 'keyboard-shortcuts-main' ] static GAME_CLIENT_PROCESS_NAME = 'League of Legends.exe' @@ -40,9 +42,10 @@ export class GameClientMain implements IAkariShardInitDispose { private readonly _loggerFactory: LoggerFactoryMain private readonly _settingFactory: SettingFactoryMain private readonly _log: AkariLogger - private readonly _setting: MobxSettingService + private readonly _setting: SetterSettingService private readonly _sgp: SgpMain private readonly _lc: LeagueClientMain + private readonly _kbd: KeyboardShortcutsMain private readonly _http = axios.create({ baseURL: GameClientMain.GAME_CLIENT_BASE_URL, @@ -65,6 +68,7 @@ export class GameClientMain implements IAkariShardInitDispose { this._api = new GameClientHttpApiAxiosHelper(this._http) this._sgp = deps['sgp-main'] this._lc = deps['league-client-main'] + this._kbd = deps['keyboard-shortcuts-main'] this._setting = this._settingFactory.create( GameClientMain.id, @@ -85,10 +89,20 @@ export class GameClientMain implements IAkariShardInitDispose { async onInit() { await this._setting.applyToState() - this._handleCall() + this._handleIpcCall() + this._handleTerminateGameClientOnAltF4() } - private _handleCall() { + private _handleTerminateGameClientOnAltF4() { + // 松手时触发, 而非按下时触发 + this._kbd.events.on('last-active-shortcut', ({ id }) => { + if (id === 'LeftAlt+F4' || id === 'RightAlt+F4') { + this._terminateGameClient() + } + }) + } + + private _handleIpcCall() { this._ipc.onCall(GameClientMain.id, 'terminateGameClient', () => { this._terminateGameClient() }) diff --git a/src/main/shards/in-game-send/index.ts b/src/main/shards/in-game-send/index.ts new file mode 100644 index 00000000..70def619 --- /dev/null +++ b/src/main/shards/in-game-send/index.ts @@ -0,0 +1,58 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcMain } from '../ipc' +import { KeyboardShortcutsMain } from '../keyboard-shortcuts' +import { AkariLogger, LoggerFactoryMain } from '../logger-factory' +import { MobxUtilsMain } from '../mobx-utils' +import { OngoingGameMain } from '../ongoing-game' +import { SettingFactoryMain } from '../setting-factory' +import { SetterSettingService } from '../setting-factory/setter-setting-service' +import { InGameSendSettings, InGameSendState } from './state' + +/** + * 用于在游戏中模拟发送的相关功能 + * - 游戏内发送消息 + * - 英雄选择阶段发送消息 + */ +export class InGameSendMain implements IAkariShardInitDispose { + static id = 'in-game-send-main' + static dependencies = [ + 'akari-ipc-main', + 'mobx-utils-main', + 'logger-factory-main', + 'setting-factory-main', + 'keyboard-shortcuts-main', + 'ongoing-game-main' + ] + + private _loggerFactory: LoggerFactoryMain + private _settingFactory: SettingFactoryMain + private _log: AkariLogger + private _mobx: MobxUtilsMain + private _ipc: AkariIpcMain + private _setting: SetterSettingService + private _kbd: KeyboardShortcutsMain + private _ongoingGame: OngoingGameMain + + public readonly settings = new InGameSendSettings() + public readonly state = new InGameSendState() + + constructor(deps: any) { + this._loggerFactory = deps['logger-factory-main'] + this._settingFactory = deps['setting-factory-main'] + this._mobx = deps['mobx-utils-main'] + this._ipc = deps['akari-ipc-main'] + this._kbd = deps['keyboard-shortcuts-main'] + this._ongoingGame = deps['ongoing-game-main'] + this._log = this._loggerFactory.create(InGameSendMain.id) + this._setting = this._settingFactory.create(InGameSendMain.id, {}, this.settings) + } + + private async _handleState() { + await this._setting.applyToState() + } + + async onInit() { + await this._handleState() + } +} diff --git a/src/main/shards/in-game-send/state.ts b/src/main/shards/in-game-send/state.ts new file mode 100644 index 00000000..377e3be1 --- /dev/null +++ b/src/main/shards/in-game-send/state.ts @@ -0,0 +1,13 @@ +import { makeAutoObservable } from 'mobx' + +export class InGameSendSettings { + constructor() { + makeAutoObservable(this) + } +} + +export class InGameSendState { + constructor() { + makeAutoObservable(this) + } +} diff --git a/src/main/shards/index.ts b/src/main/shards/index.ts deleted file mode 100644 index 1d7fdd9b..00000000 --- a/src/main/shards/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { AkariManager } from '@shared/akari-shard/manager' - -// import { AkariIpcMain } from './ipc' -// import { LoggerMain } from './logger' -// import { StateSyncMain } from './state-sync' - -// const manager = new AkariManager() - -// manager.use(AkariIpcMain, LoggerMain, StateSyncMain) - -// manager.setup() diff --git a/src/main/shards/ipc/index.ts b/src/main/shards/ipc/index.ts index 655db4eb..81176cac 100644 --- a/src/main/shards/ipc/index.ts +++ b/src/main/shards/ipc/index.ts @@ -7,12 +7,12 @@ export interface IpcMainSuccessDataType { data: T } -export interface IpcMainErrorDataType { +export interface IpcMainErrorDataType { success: false - error: T + error: any } -export type IpcMainDataType = IpcMainSuccessDataType | IpcMainErrorDataType +export type IpcMainDataType = IpcMainSuccessDataType | IpcMainErrorDataType /** * League Akari 的 IPC 主进程实现 diff --git a/src/main/shards/keyboard-shorcuts/definitions.ts b/src/main/shards/keyboard-shorcuts/definitions.ts deleted file mode 100644 index 53728f9f..00000000 --- a/src/main/shards/keyboard-shorcuts/definitions.ts +++ /dev/null @@ -1,186 +0,0 @@ -export interface KeyDefinition { - _nameRaw: string - name: string - standardName: string -} - -// copied from global-key-listener -export const VKEY_MAP: Record = { - 0x30: { _nameRaw: 'VK_0', name: '0', standardName: '0' }, - 0x31: { _nameRaw: 'VK_1', name: '1', standardName: '1' }, - 0x32: { _nameRaw: 'VK_2', name: '2', standardName: '2' }, - 0x33: { _nameRaw: 'VK_3', name: '3', standardName: '3' }, - 0x34: { _nameRaw: 'VK_4', name: '4', standardName: '4' }, - 0x35: { _nameRaw: 'VK_5', name: '5', standardName: '5' }, - 0x36: { _nameRaw: 'VK_6', name: '6', standardName: '6' }, - 0x37: { _nameRaw: 'VK_7', name: '7', standardName: '7' }, - 0x38: { _nameRaw: 'VK_8', name: '8', standardName: '8' }, - 0x39: { _nameRaw: 'VK_9', name: '9', standardName: '9' }, - 0x41: { _nameRaw: 'VK_A', name: 'A', standardName: 'A' }, - 0x42: { _nameRaw: 'VK_B', name: 'B', standardName: 'B' }, - 0x43: { _nameRaw: 'VK_C', name: 'C', standardName: 'C' }, - 0x44: { _nameRaw: 'VK_D', name: 'D', standardName: 'D' }, - 0x45: { _nameRaw: 'VK_E', name: 'E', standardName: 'E' }, - 0x46: { _nameRaw: 'VK_F', name: 'F', standardName: 'F' }, - 0x47: { _nameRaw: 'VK_G', name: 'G', standardName: 'G' }, - 0x48: { _nameRaw: 'VK_H', name: 'H', standardName: 'H' }, - 0x49: { _nameRaw: 'VK_I', name: 'I', standardName: 'I' }, - 0x4a: { _nameRaw: 'VK_J', name: 'J', standardName: 'J' }, - 0x4b: { _nameRaw: 'VK_K', name: 'K', standardName: 'K' }, - 0x4c: { _nameRaw: 'VK_L', name: 'L', standardName: 'L' }, - 0x4d: { _nameRaw: 'VK_M', name: 'M', standardName: 'M' }, - 0x4e: { _nameRaw: 'VK_N', name: 'N', standardName: 'N' }, - 0x4f: { _nameRaw: 'VK_O', name: 'O', standardName: 'O' }, - 0x50: { _nameRaw: 'VK_P', name: 'P', standardName: 'P' }, - 0x51: { _nameRaw: 'VK_Q', name: 'Q', standardName: 'Q' }, - 0x52: { _nameRaw: 'VK_R', name: 'R', standardName: 'R' }, - 0x53: { _nameRaw: 'VK_S', name: 'S', standardName: 'S' }, - 0x54: { _nameRaw: 'VK_T', name: 'T', standardName: 'T' }, - 0x55: { _nameRaw: 'VK_U', name: 'U', standardName: 'U' }, - 0x56: { _nameRaw: 'VK_V', name: 'V', standardName: 'V' }, - 0x57: { _nameRaw: 'VK_W', name: 'W', standardName: 'W' }, - 0x58: { _nameRaw: 'VK_X', name: 'X', standardName: 'X' }, - 0x59: { _nameRaw: 'VK_Y', name: 'Y', standardName: 'Y' }, - 0x5a: { _nameRaw: 'VK_Z', name: 'Z', standardName: 'Z' }, - 0x01: { _nameRaw: 'VK_LBUTTON', name: 'LBUTTON', standardName: 'MOUSE LEFT' }, - 0x02: { _nameRaw: 'VK_RBUTTON', name: 'RBUTTON', standardName: 'MOUSE RIGHT' }, - 0x03: { _nameRaw: 'VK_CANCEL', name: 'CANCEL', standardName: '' }, - 0x04: { _nameRaw: 'VK_MBUTTON', name: 'MBUTTON', standardName: 'MOUSE MIDDLE' }, - 0x05: { _nameRaw: 'VK_XBUTTON1', name: 'XBUTTON1', standardName: 'MOUSE X1' }, - 0x06: { _nameRaw: 'VK_XBUTTON2', name: 'XBUTTON2', standardName: 'MOUSE X2' }, - 0x08: { _nameRaw: 'VK_BACK', name: 'BACK', standardName: 'BACKSPACE' }, - 0x09: { _nameRaw: 'VK_TAB', name: 'TAB', standardName: 'TAB' }, - 0x0d: { _nameRaw: 'VK_RETURN', name: 'RETURN', standardName: 'RETURN' }, - 0x10: { _nameRaw: 'VK_SHIFT', name: 'SHIFT', standardName: '' }, - 0x11: { _nameRaw: 'VK_CONTROL', name: 'CONTROL', standardName: '' }, - 0x12: { _nameRaw: 'VK_MENU', name: 'MENU', standardName: '' }, - 0x13: { _nameRaw: 'VK_PAUSE', name: 'PAUSE', standardName: '' }, - 0x14: { _nameRaw: 'VK_CAPITAL', name: 'CAPSLOCK', standardName: 'CAPS LOCK' }, - 0x15: { _nameRaw: 'VK_KANA', name: 'KANA', standardName: '' }, - 0x16: { _nameRaw: 'VK_IME_ON', name: 'IME_ON', standardName: '' }, - 0x17: { _nameRaw: 'VK_JUNJA', name: 'JUNJA', standardName: '' }, - 0x18: { _nameRaw: 'VK_FINAL', name: 'FINAL', standardName: '' }, - 0x19: { _nameRaw: 'VK_HANJA', name: 'HANJA', standardName: '' }, - 0x1a: { _nameRaw: 'VK_IME_OFF', name: 'IME_OFF', standardName: '' }, - 0x1b: { _nameRaw: 'VK_ESCAPE', name: 'ESCAPE', standardName: 'ESCAPE' }, - 0x1c: { _nameRaw: 'VK_CONVERT', name: 'CONVERT', standardName: '' }, - 0x1d: { _nameRaw: 'VK_NONCONVERT', name: 'NONCONVERT', standardName: '' }, - 0x1e: { _nameRaw: 'VK_ACCEPT', name: 'ACCEPT', standardName: '' }, - 0x1f: { _nameRaw: 'VK_MODECHANGE', name: 'MODECHANGE', standardName: '' }, - 0x20: { _nameRaw: 'VK_SPACE', name: 'SPACE', standardName: 'SPACE' }, - 0x21: { _nameRaw: 'VK_PRIOR', name: 'PRIOR', standardName: 'PAGE UP' }, - 0x22: { _nameRaw: 'VK_NEXT', name: 'NEXT', standardName: 'PAGE DOWN' }, - 0x23: { _nameRaw: 'VK_END', name: 'END', standardName: 'END' }, - 0x24: { _nameRaw: 'VK_HOME', name: 'HOME', standardName: 'HOME' }, - 0x25: { _nameRaw: 'VK_LEFT', name: 'LEFT', standardName: 'LEFT ARROW' }, - 0x26: { _nameRaw: 'VK_UP', name: 'UP', standardName: 'UP ARROW' }, - 0x27: { _nameRaw: 'VK_RIGHT', name: 'RIGHT', standardName: 'RIGHT ARROW' }, - 0x28: { _nameRaw: 'VK_DOWN', name: 'DOWN', standardName: 'DOWN ARROW' }, - 0x29: { _nameRaw: 'VK_SELECT', name: 'SELECT', standardName: '' }, - 0x2a: { _nameRaw: 'VK_PRINT', name: 'PRINT', standardName: '' }, - 0x2b: { _nameRaw: 'VK_EXECUTE', name: 'EXECUTE', standardName: '' }, - 0x2c: { _nameRaw: 'VK_SNAPSHOT', name: 'SNAPSHOT', standardName: 'PRINT SCREEN' }, - 0x2d: { _nameRaw: 'VK_INSERT', name: 'INSERT', standardName: 'INS' }, - 0x2e: { _nameRaw: 'VK_DELETE', name: 'DELETE', standardName: 'DELETE' }, - 0x2f: { _nameRaw: 'VK_HELP', name: 'HELP', standardName: '' }, - 0x5b: { _nameRaw: 'VK_LWIN', name: 'LWIN', standardName: 'LEFT META' }, - 0x5c: { _nameRaw: 'VK_RWIN', name: 'RWIN', standardName: 'RIGHT META' }, - 0x5d: { _nameRaw: 'VK_APPS', name: 'APPS', standardName: '' }, - 0x5f: { _nameRaw: 'VK_SLEEP', name: 'SLEEP', standardName: '' }, - 0x60: { _nameRaw: 'VK_NUMPAD0', name: 'NUMPAD0', standardName: 'NUMPAD 0' }, - 0x61: { _nameRaw: 'VK_NUMPAD1', name: 'NUMPAD1', standardName: 'NUMPAD 1' }, - 0x62: { _nameRaw: 'VK_NUMPAD2', name: 'NUMPAD2', standardName: 'NUMPAD 2' }, - 0x63: { _nameRaw: 'VK_NUMPAD3', name: 'NUMPAD3', standardName: 'NUMPAD 3' }, - 0x64: { _nameRaw: 'VK_NUMPAD4', name: 'NUMPAD4', standardName: 'NUMPAD 4' }, - 0x65: { _nameRaw: 'VK_NUMPAD5', name: 'NUMPAD5', standardName: 'NUMPAD 5' }, - 0x66: { _nameRaw: 'VK_NUMPAD6', name: 'NUMPAD6', standardName: 'NUMPAD 6' }, - 0x67: { _nameRaw: 'VK_NUMPAD7', name: 'NUMPAD7', standardName: 'NUMPAD 7' }, - 0x68: { _nameRaw: 'VK_NUMPAD8', name: 'NUMPAD8', standardName: 'NUMPAD 8' }, - 0x69: { _nameRaw: 'VK_NUMPAD9', name: 'NUMPAD9', standardName: 'NUMPAD 9' }, - 0x6a: { _nameRaw: 'VK_MULTIPLY', name: 'MULTIPLY', standardName: 'NUMPAD MULTIPLY' }, - 0x6b: { _nameRaw: 'VK_ADD', name: 'ADD', standardName: 'NUMPAD PLUS' }, - 0x0c: { _nameRaw: 'VK_CLEAR', name: 'CLEAR', standardName: 'NUMPAD CLEAR' }, - 0x6d: { _nameRaw: 'VK_SUBTRACT', name: 'SUBTRACT', standardName: 'NUMPAD MINUS' }, - 0x6e: { _nameRaw: 'VK_DECIMAL', name: 'DECIMAL', standardName: 'NUMPAD DOT' }, - 0x6f: { _nameRaw: 'VK_DIVIDE', name: 'DIVIDE', standardName: 'NUMPAD DIVIDE' }, - 0x6c: { _nameRaw: 'VK_SEPARATOR', name: 'SEPARATOR', standardName: '' }, - 0x70: { _nameRaw: 'VK_F1', name: 'F1', standardName: 'F1' }, - 0x71: { _nameRaw: 'VK_F2', name: 'F2', standardName: 'F2' }, - 0x72: { _nameRaw: 'VK_F3', name: 'F3', standardName: 'F3' }, - 0x73: { _nameRaw: 'VK_F4', name: 'F4', standardName: 'F4' }, - 0x74: { _nameRaw: 'VK_F5', name: 'F5', standardName: 'F5' }, - 0x75: { _nameRaw: 'VK_F6', name: 'F6', standardName: 'F6' }, - 0x76: { _nameRaw: 'VK_F7', name: 'F7', standardName: 'F7' }, - 0x77: { _nameRaw: 'VK_F8', name: 'F8', standardName: 'F8' }, - 0x78: { _nameRaw: 'VK_F9', name: 'F9', standardName: 'F9' }, - 0x79: { _nameRaw: 'VK_F10', name: 'F10', standardName: 'F10' }, - 0x7a: { _nameRaw: 'VK_F11', name: 'F11', standardName: 'F11' }, - 0x7b: { _nameRaw: 'VK_F12', name: 'F12', standardName: 'F12' }, - 0x7c: { _nameRaw: 'VK_F13', name: 'F13', standardName: 'F13' }, - 0x7d: { _nameRaw: 'VK_F14', name: 'F14', standardName: 'F14' }, - 0x7e: { _nameRaw: 'VK_F15', name: 'F15', standardName: 'F15' }, - 0x7f: { _nameRaw: 'VK_F16', name: 'F16', standardName: 'F16' }, - 0x80: { _nameRaw: 'VK_F17', name: 'F17', standardName: 'F17' }, - 0x81: { _nameRaw: 'VK_F18', name: 'F18', standardName: 'F18' }, - 0x82: { _nameRaw: 'VK_F19', name: 'F19', standardName: 'F19' }, - 0x83: { _nameRaw: 'VK_F20', name: 'F20', standardName: 'F20' }, - 0x84: { _nameRaw: 'VK_F21', name: 'F21', standardName: 'F21' }, - 0x85: { _nameRaw: 'VK_F22', name: 'F22', standardName: 'F22' }, - 0x86: { _nameRaw: 'VK_F23', name: 'F23', standardName: 'F23' }, - 0x87: { _nameRaw: 'VK_F24', name: 'F24', standardName: 'F24' }, - 0x90: { _nameRaw: 'VK_NUMLOCK', name: 'NUMLOCK', standardName: 'NUM LOCK' }, - 0x91: { _nameRaw: 'VK_SCROLL', name: 'SCROLL', standardName: 'SCROLL LOCK' }, - 0xa0: { _nameRaw: 'VK_LSHIFT', name: 'LSHIFT', standardName: 'LEFT SHIFT' }, - 0xa1: { _nameRaw: 'VK_RSHIFT', name: 'RSHIFT', standardName: 'RIGHT SHIFT' }, - 0xa2: { _nameRaw: 'VK_LCONTROL', name: 'LCONTROL', standardName: 'LEFT CTRL' }, - 0xa3: { _nameRaw: 'VK_RCONTROL', name: 'RCONTROL', standardName: 'RIGHT CTRL' }, - 0xa4: { _nameRaw: 'VK_LMENU', name: 'LALT', standardName: 'LEFT ALT' }, - 0xa5: { _nameRaw: 'VK_RMENU', name: 'RALT', standardName: 'RIGHT ALT' }, - 0xa6: { _nameRaw: 'VK_BROWSER_BACK', name: 'BROWSER_BACK', standardName: '' }, - 0xa7: { _nameRaw: 'VK_BROWSER_FORWARD', name: 'BROWSER_FORWARD', standardName: '' }, - 0xa8: { _nameRaw: 'VK_BROWSER_REFRESH', name: 'BROWSER_REFRESH', standardName: '' }, - 0xa9: { _nameRaw: 'VK_BROWSER_STOP', name: 'BROWSER_STOP', standardName: '' }, - 0xaa: { _nameRaw: 'VK_BROWSER_SEARCH', name: 'BROWSER_SEARCH', standardName: '' }, - 0xab: { _nameRaw: 'VK_BROWSER_FAVORITES', name: 'BROWSER_FAVORITES', standardName: '' }, - 0xac: { _nameRaw: 'VK_BROWSER_HOME', name: 'BROWSER_HOME', standardName: '' }, - 0xad: { _nameRaw: 'VK_VOLUME_MUTE', name: 'VOLUME_MUTE', standardName: '' }, - 0xae: { _nameRaw: 'VK_VOLUME_DOWN', name: 'VOLUME_DOWN', standardName: '' }, - 0xaf: { _nameRaw: 'VK_VOLUME_UP', name: 'VOLUME_UP', standardName: '' }, - 0xb0: { _nameRaw: 'VK_MEDIA_NEXT_TRACK', name: 'MEDIA_NEXT_TRACK', standardName: '' }, - 0xb1: { _nameRaw: 'VK_MEDIA_PREV_TRACK', name: 'MEDIA_PREV_TRACK', standardName: '' }, - 0xb2: { _nameRaw: 'VK_MEDIA_STOP', name: 'MEDIA_STOP', standardName: '' }, - 0xb3: { _nameRaw: 'VK_MEDIA_PLAY_PAUSE', name: 'MEDIA_PLAY_PAUSE', standardName: '' }, - 0xb4: { _nameRaw: 'VK_LAUNCH_MAIL', name: 'LAUNCH_MAIL', standardName: '' }, - 0xb5: { _nameRaw: 'VK_LAUNCH_MEDIA_SELECT', name: 'LAUNCH_MEDIA_SELECT', standardName: '' }, - 0xb6: { _nameRaw: 'VK_LAUNCH_APP1', name: 'LAUNCH_APP1', standardName: '' }, - 0xb7: { _nameRaw: 'VK_LAUNCH_APP2', name: 'LAUNCH_APP2', standardName: '' }, - 0xba: { _nameRaw: 'VK_OEM_1', name: 'OEM_1', standardName: 'SEMICOLON' }, - 0xbb: { _nameRaw: 'VK_OEM_PLUS', name: 'OEM_PLUS', standardName: 'EQUALS' }, - 0xbc: { _nameRaw: 'VK_OEM_COMMA', name: 'OEM_COMMA', standardName: 'COMMA' }, - 0xbd: { _nameRaw: 'VK_OEM_MINUS', name: 'OEM_MINUS', standardName: 'MINUS' }, - 0xbe: { _nameRaw: 'VK_OEM_PERIOD', name: 'OEM_PERIOD', standardName: 'DOT' }, - 0xbf: { _nameRaw: 'VK_OEM_2', name: 'OEM_2', standardName: 'FORWARD SLASH' }, - 0xc0: { _nameRaw: 'VK_OEM_3', name: 'OEM_3', standardName: 'SECTION' }, - 0xdb: { _nameRaw: 'VK_OEM_4', name: 'OEM_4', standardName: 'SQUARE BRACKET OPEN' }, - 0xdc: { _nameRaw: 'VK_OEM_5', name: 'OEM_5', standardName: 'BACKSLASH' }, - 0xdd: { _nameRaw: 'VK_OEM_6', name: 'OEM_6', standardName: 'SQUARE BRACKET CLOSE' }, - 0xde: { _nameRaw: 'VK_OEM_7', name: 'OEM_7', standardName: 'QUOTE' }, - 0xdf: { _nameRaw: 'VK_OEM_8', name: 'OEM_8', standardName: '' }, - 0xe2: { _nameRaw: 'VK_OEM_102', name: 'OEM_102', standardName: 'BACKTICK' }, - 0xe5: { _nameRaw: 'VK_PROCESSKEY', name: 'PROCESSKEY', standardName: '' }, - 0xe7: { _nameRaw: 'VK_PACKET', name: 'PACKET', standardName: '' }, - 0xf6: { _nameRaw: 'VK_ATTN', name: 'ATTN', standardName: '' }, - 0xf7: { _nameRaw: 'VK_CRSEL', name: 'CRSEL', standardName: '' }, - 0xf8: { _nameRaw: 'VK_EXSEL', name: 'EXSEL', standardName: '' }, - 0xf9: { _nameRaw: 'VK_EREOF', name: 'EREOF', standardName: '' }, - 0xfa: { _nameRaw: 'VK_PLAY', name: 'PLAY', standardName: '' }, - 0xfb: { _nameRaw: 'VK_ZOOM', name: 'ZOOM', standardName: '' }, - 0xfc: { _nameRaw: 'VK_NONAME', name: 'NONAME', standardName: '' }, - 0xfd: { _nameRaw: 'VK_PA1', name: 'PA1', standardName: '' }, - 0xfe: { _nameRaw: 'VK_OEM_CLEAR', name: 'OEM_CLEAR', standardName: '' } -} - -export function isModifierKey(keyCode: number) { - // VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN, VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL, VK_LMENU, VK_RMENU - return [0x10, 0x11, 0x12, 0xa0, 0xa1, 0xa2, 0xa3].includes(keyCode) -} diff --git a/src/main/shards/keyboard-shorcuts/index.ts b/src/main/shards/keyboard-shorcuts/index.ts deleted file mode 100644 index cdbf5719..00000000 --- a/src/main/shards/keyboard-shorcuts/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import input from '@main/native/la-input-win64.node' -import { IAkariShardInitDispose } from '@shared/akari-shard/interface' -import EventEmitter from 'node:events' - -import { AppCommonMain } from '../app-common' -import { KeyDefinition, VKEY_MAP } from './definitions' - -/** - * 处理键盘快捷键的模块 - * 通过较为 Native 的方式, 使其可以在程序外任何地方使用, 前提是程序有管理员权限 - */ -export class KeyboardShortcutsMain implements IAkariShardInitDispose { - static id = 'keyboard-shortcuts-main' - static dependencies = ['app-common-main'] - - private readonly _common: AppCommonMain - - private _tracked: number[] = new Array(384).fill(0) - - /** - * 原生监听到的键盘事件 - */ - public readonly events = new EventEmitter<{ - key: [event: KeyDefinition & { isDown: boolean }] - }>() - - constructor(deps: any) { - this._common = deps['app-common-main'] - } - - sendKey(code: number, press: boolean) { - input.sendKey(code, press) - } - - sendKeys(str: string) { - input.sendKeys(str) - } - - async onInit() { - if (this._common.state.isAdministrator) { - input.startHook() - - const DEFAULT_KEY_DEF = { - _nameRaw: 'UNKNOWN', - name: 'UNKNOWN', - standardName: '' - } - - input.onKeyEvent((key) => { - const [keyCodeRaw, state] = key.split(',') - - const keyCode = parseInt(keyCodeRaw) - const isDown = state === 'DOWN' - - this.events.emit('key', { - ...(VKEY_MAP[keyCode] || DEFAULT_KEY_DEF), - isDown - }) - - this._tracked[keyCode] = state === 'DOWN' ? 1 : 0 - }) - } - } - - async onDispose() { - if (this._common.state.isAdministrator) { - input.stopHook() - } - this.events.removeAllListeners() - } -} diff --git a/src/main/shards/keyboard-shortcuts/definitions.ts b/src/main/shards/keyboard-shortcuts/definitions.ts new file mode 100644 index 00000000..17b732d0 --- /dev/null +++ b/src/main/shards/keyboard-shortcuts/definitions.ts @@ -0,0 +1,892 @@ +export interface KeyDefinition { + _nameRaw: string + name: string + standardName: string + keyId: string +} + +// 键位信息来自 global-key-listener +export const VKEY_MAP: Record = { + 1: { + _nameRaw: 'VK_LBUTTON', + name: 'LBUTTON', + standardName: 'MOUSE LEFT', + keyId: 'LeftMouseButton' + }, + 2: { + _nameRaw: 'VK_RBUTTON', + name: 'RBUTTON', + standardName: 'MOUSE RIGHT', + keyId: 'RightMouseButton' + }, + 3: { + _nameRaw: 'VK_CANCEL', + name: 'CANCEL', + standardName: '', + keyId: 'Cancel' + }, + 4: { + _nameRaw: 'VK_MBUTTON', + name: 'MBUTTON', + standardName: 'MOUSE MkeyIdDLE', + keyId: 'MkeyIddleMouseButton' + }, + 5: { + _nameRaw: 'VK_XBUTTON1', + name: 'XBUTTON1', + standardName: 'MOUSE X1', + keyId: 'MouseXButton1' + }, + 6: { + _nameRaw: 'VK_XBUTTON2', + name: 'XBUTTON2', + standardName: 'MOUSE X2', + keyId: 'MouseXButton2' + }, + 8: { + _nameRaw: 'VK_BACK', + name: 'BACK', + standardName: 'BACKSPACE', + keyId: 'Backspace' + }, + 9: { + _nameRaw: 'VK_TAB', + name: 'TAB', + standardName: 'TAB', + keyId: 'Tab' + }, + 12: { + _nameRaw: 'VK_CLEAR', + name: 'CLEAR', + standardName: 'NUMPAD CLEAR', + keyId: 'NumpadClear' + }, + 13: { + _nameRaw: 'VK_RETURN', + name: 'RETURN', + standardName: 'RETURN', + keyId: 'Enter' + }, + 16: { + _nameRaw: 'VK_SHIFT', + name: 'SHIFT', + standardName: '', + keyId: 'Shift' + }, + 17: { + _nameRaw: 'VK_CONTROL', + name: 'CONTROL', + standardName: '', + keyId: 'Control' + }, + 18: { + _nameRaw: 'VK_MENU', + name: 'MENU', + standardName: '', + keyId: 'Alt' + }, + 19: { + _nameRaw: 'VK_PAUSE', + name: 'PAUSE', + standardName: '', + keyId: 'Pause' + }, + 20: { + _nameRaw: 'VK_CAPITAL', + name: 'CAPSLOCK', + standardName: 'CAPS LOCK', + keyId: 'CapsLock' + }, + 21: { + _nameRaw: 'VK_KANA', + name: 'KANA', + standardName: '', + keyId: 'Kana' + }, + 22: { + _nameRaw: 'VK_IME_ON', + name: 'IME_ON', + standardName: '', + keyId: 'IMEOn' + }, + 23: { + _nameRaw: 'VK_JUNJA', + name: 'JUNJA', + standardName: '', + keyId: 'Junja' + }, + 24: { + _nameRaw: 'VK_FINAL', + name: 'FINAL', + standardName: '', + keyId: 'Final' + }, + 25: { + _nameRaw: 'VK_HANJA', + name: 'HANJA', + standardName: '', + keyId: 'Hanja' + }, + 26: { + _nameRaw: 'VK_IME_OFF', + name: 'IME_OFF', + standardName: '', + keyId: 'IMEOff' + }, + 27: { + _nameRaw: 'VK_ESCAPE', + name: 'ESCAPE', + standardName: 'ESCAPE', + keyId: 'Escape' + }, + 28: { + _nameRaw: 'VK_CONVERT', + name: 'CONVERT', + standardName: '', + keyId: 'Convert' + }, + 29: { + _nameRaw: 'VK_NONCONVERT', + name: 'NONCONVERT', + standardName: '', + keyId: 'NonConvert' + }, + 30: { + _nameRaw: 'VK_ACCEPT', + name: 'ACCEPT', + standardName: '', + keyId: 'Accept' + }, + 31: { + _nameRaw: 'VK_MODECHANGE', + name: 'MODECHANGE', + standardName: '', + keyId: 'ModeChange' + }, + 32: { + _nameRaw: 'VK_SPACE', + name: 'SPACE', + standardName: 'SPACE', + keyId: 'Space' + }, + 33: { + _nameRaw: 'VK_PRIOR', + name: 'PRIOR', + standardName: 'PAGE UP', + keyId: 'PageUp' + }, + 34: { + _nameRaw: 'VK_NEXT', + name: 'NEXT', + standardName: 'PAGE DOWN', + keyId: 'PageDown' + }, + 35: { + _nameRaw: 'VK_END', + name: 'END', + standardName: 'END', + keyId: 'End' + }, + 36: { + _nameRaw: 'VK_HOME', + name: 'HOME', + standardName: 'HOME', + keyId: 'Home' + }, + 37: { + _nameRaw: 'VK_LEFT', + name: 'LEFT', + standardName: 'LEFT ARROW', + keyId: 'LeftArrow' + }, + 38: { + _nameRaw: 'VK_UP', + name: 'UP', + standardName: 'UP ARROW', + keyId: 'UpArrow' + }, + 39: { + _nameRaw: 'VK_RIGHT', + name: 'RIGHT', + standardName: 'RIGHT ARROW', + keyId: 'RightArrow' + }, + 40: { + _nameRaw: 'VK_DOWN', + name: 'DOWN', + standardName: 'DOWN ARROW', + keyId: 'DownArrow' + }, + 41: { + _nameRaw: 'VK_SELECT', + name: 'SELECT', + standardName: '', + keyId: 'Select' + }, + 42: { + _nameRaw: 'VK_PRINT', + name: 'PRINT', + standardName: '', + keyId: 'Print' + }, + 43: { + _nameRaw: 'VK_EXECUTE', + name: 'EXECUTE', + standardName: '', + keyId: 'Execute' + }, + 44: { + _nameRaw: 'VK_SNAPSHOT', + name: 'SNAPSHOT', + standardName: 'PRINT SCREEN', + keyId: 'PrintScreen' + }, + 45: { + _nameRaw: 'VK_INSERT', + name: 'INSERT', + standardName: 'INS', + keyId: 'Insert' + }, + 46: { + _nameRaw: 'VK_DELETE', + name: 'DELETE', + standardName: 'DELETE', + keyId: 'Delete' + }, + 47: { + _nameRaw: 'VK_HELP', + name: 'HELP', + standardName: '', + keyId: 'Help' + }, + 48: { _nameRaw: 'VK_0', name: '0', standardName: '0', keyId: '0' }, + 49: { _nameRaw: 'VK_1', name: '1', standardName: '1', keyId: '1' }, + 50: { _nameRaw: 'VK_2', name: '2', standardName: '2', keyId: '2' }, + 51: { _nameRaw: 'VK_3', name: '3', standardName: '3', keyId: '3' }, + 52: { _nameRaw: 'VK_4', name: '4', standardName: '4', keyId: '4' }, + 53: { _nameRaw: 'VK_5', name: '5', standardName: '5', keyId: '5' }, + 54: { _nameRaw: 'VK_6', name: '6', standardName: '6', keyId: '6' }, + 55: { _nameRaw: 'VK_7', name: '7', standardName: '7', keyId: '7' }, + 56: { _nameRaw: 'VK_8', name: '8', standardName: '8', keyId: '8' }, + 57: { _nameRaw: 'VK_9', name: '9', standardName: '9', keyId: '9' }, + 65: { _nameRaw: 'VK_A', name: 'A', standardName: 'A', keyId: 'A' }, + 66: { _nameRaw: 'VK_B', name: 'B', standardName: 'B', keyId: 'B' }, + 67: { _nameRaw: 'VK_C', name: 'C', standardName: 'C', keyId: 'C' }, + 68: { _nameRaw: 'VK_D', name: 'D', standardName: 'D', keyId: 'D' }, + 69: { _nameRaw: 'VK_E', name: 'E', standardName: 'E', keyId: 'E' }, + 70: { _nameRaw: 'VK_F', name: 'F', standardName: 'F', keyId: 'F' }, + 71: { _nameRaw: 'VK_G', name: 'G', standardName: 'G', keyId: 'G' }, + 72: { _nameRaw: 'VK_H', name: 'H', standardName: 'H', keyId: 'H' }, + 73: { _nameRaw: 'VK_I', name: 'I', standardName: 'I', keyId: 'I' }, + 74: { _nameRaw: 'VK_J', name: 'J', standardName: 'J', keyId: 'J' }, + 75: { _nameRaw: 'VK_K', name: 'K', standardName: 'K', keyId: 'K' }, + 76: { _nameRaw: 'VK_L', name: 'L', standardName: 'L', keyId: 'L' }, + 77: { _nameRaw: 'VK_M', name: 'M', standardName: 'M', keyId: 'M' }, + 78: { _nameRaw: 'VK_N', name: 'N', standardName: 'N', keyId: 'N' }, + 79: { _nameRaw: 'VK_O', name: 'O', standardName: 'O', keyId: 'O' }, + 80: { _nameRaw: 'VK_P', name: 'P', standardName: 'P', keyId: 'P' }, + 81: { _nameRaw: 'VK_Q', name: 'Q', standardName: 'Q', keyId: 'Q' }, + 82: { _nameRaw: 'VK_R', name: 'R', standardName: 'R', keyId: 'R' }, + 83: { _nameRaw: 'VK_S', name: 'S', standardName: 'S', keyId: 'S' }, + 84: { _nameRaw: 'VK_T', name: 'T', standardName: 'T', keyId: 'T' }, + 85: { _nameRaw: 'VK_U', name: 'U', standardName: 'U', keyId: 'U' }, + 86: { _nameRaw: 'VK_V', name: 'V', standardName: 'V', keyId: 'V' }, + 87: { _nameRaw: 'VK_W', name: 'W', standardName: 'W', keyId: 'W' }, + 88: { _nameRaw: 'VK_X', name: 'X', standardName: 'X', keyId: 'X' }, + 89: { _nameRaw: 'VK_Y', name: 'Y', standardName: 'Y', keyId: 'Y' }, + 90: { _nameRaw: 'VK_Z', name: 'Z', standardName: 'Z', keyId: 'Z' }, + 91: { + _nameRaw: 'VK_LWIN', + name: 'LWIN', + standardName: 'LEFT META', + keyId: 'LeftMeta' + }, + 92: { + _nameRaw: 'VK_RWIN', + name: 'RWIN', + standardName: 'RIGHT META', + keyId: 'RightMeta' + }, + 93: { + _nameRaw: 'VK_APPS', + name: 'APPS', + standardName: '', + keyId: 'Apps' + }, + 95: { + _nameRaw: 'VK_SLEEP', + name: 'SLEEP', + standardName: '', + keyId: 'Sleep' + }, + 96: { + _nameRaw: 'VK_NUMPAD0', + name: 'NUMPAD0', + standardName: 'NUMPAD 0', + keyId: 'Numpad0' + }, + 97: { + _nameRaw: 'VK_NUMPAD1', + name: 'NUMPAD1', + standardName: 'NUMPAD 1', + keyId: 'Numpad1' + }, + 98: { + _nameRaw: 'VK_NUMPAD2', + name: 'NUMPAD2', + standardName: 'NUMPAD 2', + keyId: 'Numpad2' + }, + 99: { + _nameRaw: 'VK_NUMPAD3', + name: 'NUMPAD3', + standardName: 'NUMPAD 3', + keyId: 'Numpad3' + }, + 100: { + _nameRaw: 'VK_NUMPAD4', + name: 'NUMPAD4', + standardName: 'NUMPAD 4', + keyId: 'Numpad4' + }, + 101: { + _nameRaw: 'VK_NUMPAD5', + name: 'NUMPAD5', + standardName: 'NUMPAD 5', + keyId: 'Numpad5' + }, + 102: { + _nameRaw: 'VK_NUMPAD6', + name: 'NUMPAD6', + standardName: 'NUMPAD 6', + keyId: 'Numpad6' + }, + 103: { + _nameRaw: 'VK_NUMPAD7', + name: 'NUMPAD7', + standardName: 'NUMPAD 7', + keyId: 'Numpad7' + }, + 104: { + _nameRaw: 'VK_NUMPAD8', + name: 'NUMPAD8', + standardName: 'NUMPAD 8', + keyId: 'Numpad8' + }, + 105: { + _nameRaw: 'VK_NUMPAD9', + name: 'NUMPAD9', + standardName: 'NUMPAD 9', + keyId: 'Numpad9' + }, + 106: { + _nameRaw: 'VK_MULTIPLY', + name: 'MULTIPLY', + standardName: 'NUMPAD MULTIPLY', + keyId: 'NumpadMultiply' + }, + 107: { + _nameRaw: 'VK_ADD', + name: 'ADD', + standardName: 'NUMPAD PLUS', + keyId: 'NumpadPlus' + }, + 108: { + _nameRaw: 'VK_SEPARATOR', + name: 'SEPARATOR', + standardName: '', + keyId: 'Separator' + }, + 109: { + _nameRaw: 'VK_SUBTRACT', + name: 'SUBTRACT', + standardName: 'NUMPAD MINUS', + keyId: 'NumpadMinus' + }, + 110: { + _nameRaw: 'VK_DECIMAL', + name: 'DECIMAL', + standardName: 'NUMPAD DOT', + keyId: 'NumpadDot' + }, + 111: { + _nameRaw: 'VK_DIVkeyIdE', + name: 'DIVkeyIdE', + standardName: 'NUMPAD DIVkeyIdE', + keyId: 'NumpadDivkeyIde' + }, + 112: { + _nameRaw: 'VK_F1', + name: 'F1', + standardName: 'F1', + keyId: 'F1' + }, + 113: { + _nameRaw: 'VK_F2', + name: 'F2', + standardName: 'F2', + keyId: 'F2' + }, + 114: { + _nameRaw: 'VK_F3', + name: 'F3', + standardName: 'F3', + keyId: 'F3' + }, + 115: { + _nameRaw: 'VK_F4', + name: 'F4', + standardName: 'F4', + keyId: 'F4' + }, + 116: { + _nameRaw: 'VK_F5', + name: 'F5', + standardName: 'F5', + keyId: 'F5' + }, + 117: { + _nameRaw: 'VK_F6', + name: 'F6', + standardName: 'F6', + keyId: 'F6' + }, + 118: { + _nameRaw: 'VK_F7', + name: 'F7', + standardName: 'F7', + keyId: 'F7' + }, + 119: { + _nameRaw: 'VK_F8', + name: 'F8', + standardName: 'F8', + keyId: 'F8' + }, + 120: { + _nameRaw: 'VK_F9', + name: 'F9', + standardName: 'F9', + keyId: 'F9' + }, + 121: { + _nameRaw: 'VK_F10', + name: 'F10', + standardName: 'F10', + keyId: 'F10' + }, + 122: { + _nameRaw: 'VK_F11', + name: 'F11', + standardName: 'F11', + keyId: 'F11' + }, + 123: { + _nameRaw: 'VK_F12', + name: 'F12', + standardName: 'F12', + keyId: 'F12' + }, + 124: { + _nameRaw: 'VK_F13', + name: 'F13', + standardName: 'F13', + keyId: 'F13' + }, + 125: { + _nameRaw: 'VK_F14', + name: 'F14', + standardName: 'F14', + keyId: 'F14' + }, + 126: { + _nameRaw: 'VK_F15', + name: 'F15', + standardName: 'F15', + keyId: 'F15' + }, + 127: { + _nameRaw: 'VK_F16', + name: 'F16', + standardName: 'F16', + keyId: 'F16' + }, + 128: { + _nameRaw: 'VK_F17', + name: 'F17', + standardName: 'F17', + keyId: 'F17' + }, + 129: { + _nameRaw: 'VK_F18', + name: 'F18', + standardName: 'F18', + keyId: 'F18' + }, + 130: { + _nameRaw: 'VK_F19', + name: 'F19', + standardName: 'F19', + keyId: 'F19' + }, + 131: { + _nameRaw: 'VK_F20', + name: 'F20', + standardName: 'F20', + keyId: 'F20' + }, + 132: { + _nameRaw: 'VK_F21', + name: 'F21', + standardName: 'F21', + keyId: 'F21' + }, + 133: { + _nameRaw: 'VK_F22', + name: 'F22', + standardName: 'F22', + keyId: 'F22' + }, + 134: { + _nameRaw: 'VK_F23', + name: 'F23', + standardName: 'F23', + keyId: 'F23' + }, + 135: { + _nameRaw: 'VK_F24', + name: 'F24', + standardName: 'F24', + keyId: 'F24' + }, + 144: { + _nameRaw: 'VK_NUMLOCK', + name: 'NUMLOCK', + standardName: 'NUM LOCK', + keyId: 'NumLock' + }, + 145: { + _nameRaw: 'VK_SCROLL', + name: 'SCROLL', + standardName: 'SCROLL LOCK', + keyId: 'ScrollLock' + }, + 160: { + _nameRaw: 'VK_LSHIFT', + name: 'LSHIFT', + standardName: 'LEFT SHIFT', + keyId: 'LeftShift' + }, + 161: { + _nameRaw: 'VK_RSHIFT', + name: 'RSHIFT', + standardName: 'RIGHT SHIFT', + keyId: 'RightShift' + }, + 162: { + _nameRaw: 'VK_LCONTROL', + name: 'LCONTROL', + standardName: 'LEFT CTRL', + keyId: 'LeftControl' + }, + 163: { + _nameRaw: 'VK_RCONTROL', + name: 'RCONTROL', + standardName: 'RIGHT CTRL', + keyId: 'RightControl' + }, + 164: { + _nameRaw: 'VK_LMENU', + name: 'LALT', + standardName: 'LEFT ALT', + keyId: 'LeftAlt' + }, + 165: { + _nameRaw: 'VK_RMENU', + name: 'RALT', + standardName: 'RIGHT ALT', + keyId: 'RightAlt' + }, + 166: { + _nameRaw: 'VK_BROWSER_BACK', + name: 'BROWSER_BACK', + standardName: '', + keyId: 'BrowserBack' + }, + 167: { + _nameRaw: 'VK_BROWSER_FORWARD', + name: 'BROWSER_FORWARD', + standardName: '', + keyId: 'BrowserForward' + }, + 168: { + _nameRaw: 'VK_BROWSER_REFRESH', + name: 'BROWSER_REFRESH', + standardName: '', + keyId: 'BrowserRefresh' + }, + 169: { + _nameRaw: 'VK_BROWSER_STOP', + name: 'BROWSER_STOP', + standardName: '', + keyId: 'BrowserStop' + }, + 170: { + _nameRaw: 'VK_BROWSER_SEARCH', + name: 'BROWSER_SEARCH', + standardName: '', + keyId: 'BrowserSearch' + }, + 171: { + _nameRaw: 'VK_BROWSER_FAVORITES', + name: 'BROWSER_FAVORITES', + standardName: '', + keyId: 'BrowserFavorites' + }, + 172: { + _nameRaw: 'VK_BROWSER_HOME', + name: 'BROWSER_HOME', + standardName: '', + keyId: 'BrowserHome' + }, + 173: { + _nameRaw: 'VK_VOLUME_MUTE', + name: 'VOLUME_MUTE', + standardName: '', + keyId: 'VolumeMute' + }, + 174: { + _nameRaw: 'VK_VOLUME_DOWN', + name: 'VOLUME_DOWN', + standardName: '', + keyId: 'VolumeDown' + }, + 175: { + _nameRaw: 'VK_VOLUME_UP', + name: 'VOLUME_UP', + standardName: '', + keyId: 'VolumeUp' + }, + 176: { + _nameRaw: 'VK_MEDIA_NEXT_TRACK', + name: 'MEDIA_NEXT_TRACK', + standardName: '', + keyId: 'NextTrack' + }, + 177: { + _nameRaw: 'VK_MEDIA_PREV_TRACK', + name: 'MEDIA_PREV_TRACK', + standardName: '', + keyId: 'PreviousTrack' + }, + 178: { + _nameRaw: 'VK_MEDIA_STOP', + name: 'MEDIA_STOP', + standardName: '', + keyId: 'StopMedia' + }, + 179: { + _nameRaw: 'VK_MEDIA_PLAY_PAUSE', + name: 'MEDIA_PLAY_PAUSE', + standardName: '', + keyId: 'PlayPauseMedia' + }, + 180: { + _nameRaw: 'VK_LAUNCH_MAIL', + name: 'LAUNCH_MAIL', + standardName: '', + keyId: 'LaunchMail' + }, + 181: { + _nameRaw: 'VK_LAUNCH_MEDIA_SELECT', + name: 'LAUNCH_MEDIA_SELECT', + standardName: '', + keyId: 'LaunchMediaSelect' + }, + 182: { + _nameRaw: 'VK_LAUNCH_APP1', + name: 'LAUNCH_APP1', + standardName: '', + keyId: 'LaunchApp1' + }, + 183: { + _nameRaw: 'VK_LAUNCH_APP2', + name: 'LAUNCH_APP2', + standardName: '', + keyId: 'LaunchApp2' + }, + 186: { + _nameRaw: 'VK_OEM_1', + name: 'OEM_1', + standardName: 'SEMICOLON', + keyId: 'Semicolon' + }, + 187: { + _nameRaw: 'VK_OEM_PLUS', + name: 'OEM_PLUS', + standardName: 'EQUALS', + keyId: 'Equals' + }, + 188: { + _nameRaw: 'VK_OEM_COMMA', + name: 'OEM_COMMA', + standardName: 'COMMA', + keyId: 'Comma' + }, + 189: { + _nameRaw: 'VK_OEM_MINUS', + name: 'OEM_MINUS', + standardName: 'MINUS', + keyId: 'Minus' + }, + 190: { + _nameRaw: 'VK_OEM_PERIOD', + name: 'OEM_PERIOD', + standardName: 'DOT', + keyId: 'Dot' + }, + 191: { + _nameRaw: 'VK_OEM_2', + name: 'OEM_2', + standardName: 'FORWARD SLASH', + keyId: 'ForwardSlash' + }, + 192: { + _nameRaw: 'VK_OEM_3', + name: 'OEM_3', + standardName: 'SECTION', + keyId: 'Section' + }, + 219: { + _nameRaw: 'VK_OEM_4', + name: 'OEM_4', + standardName: 'SQUARE BRACKET OPEN', + keyId: 'OpenBracket' + }, + 220: { + _nameRaw: 'VK_OEM_5', + name: 'OEM_5', + standardName: 'BACKSLASH', + keyId: 'Backslash' + }, + 221: { + _nameRaw: 'VK_OEM_6', + name: 'OEM_6', + standardName: 'SQUARE BRACKET CLOSE', + keyId: 'CloseBracket' + }, + 222: { + _nameRaw: 'VK_OEM_7', + name: 'OEM_7', + standardName: 'QUOTE', + keyId: 'Quote' + }, + 223: { + _nameRaw: 'VK_OEM_8', + name: 'OEM_8', + standardName: '', + keyId: 'OEM8' + }, + 226: { + _nameRaw: 'VK_OEM_102', + name: 'OEM_102', + standardName: 'BACKTICK', + keyId: 'Backtick' + }, + 229: { + _nameRaw: 'VK_PROCESSKEY', + name: 'PROCESSKEY', + standardName: '', + keyId: 'ProcessKey' + }, + 231: { + _nameRaw: 'VK_PACKET', + name: 'PACKET', + standardName: '', + keyId: 'Packet' + }, + 246: { + _nameRaw: 'VK_ATTN', + name: 'ATTN', + standardName: '', + keyId: 'Attention' + }, + 247: { + _nameRaw: 'VK_CRSEL', + name: 'CRSEL', + standardName: '', + keyId: 'CrSel' + }, + 248: { + _nameRaw: 'VK_EXSEL', + name: 'EXSEL', + standardName: '', + keyId: 'ExSel' + }, + 249: { + _nameRaw: 'VK_EREOF', + name: 'EREOF', + standardName: '', + keyId: 'EraseEOF' + }, + 250: { + _nameRaw: 'VK_PLAY', + name: 'PLAY', + standardName: '', + keyId: 'Play' + }, + 251: { + _nameRaw: 'VK_ZOOM', + name: 'ZOOM', + standardName: '', + keyId: 'Zoom' + }, + 252: { + _nameRaw: 'VK_NONAME', + name: 'NONAME', + standardName: '', + keyId: 'NoName' + }, + 253: { + _nameRaw: 'VK_PA1', + name: 'PA1', + standardName: '', + keyId: 'PA1' + }, + 254: { + _nameRaw: 'VK_OEM_CLEAR', + name: 'OEM_CLEAR', + standardName: '', + keyId: 'OEMClear' + } +} + +export const MODIFIER_KEYS = new Set([17, 16, 18, 160, 162, 164, 91, 161, 163, 165, 92]) + +export const UNIFIED_KEY_ID = { + 16: 'Shift', + 160: 'Shift', + 161: 'Shift', + 162: 'Control', + 163: 'Control', + 164: 'Alt', + 165: 'Alt', + 91: 'Meta', + 92: 'Meta', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 109: 'Minus', + 110: 'Dot' +} as const + +export function isModifierKey(keyCode: number) { + return MODIFIER_KEYS.has(keyCode) +} diff --git a/src/main/shards/keyboard-shortcuts/index.ts b/src/main/shards/keyboard-shortcuts/index.ts new file mode 100644 index 00000000..958403f3 --- /dev/null +++ b/src/main/shards/keyboard-shortcuts/index.ts @@ -0,0 +1,177 @@ +import input from '@main/native/la-input-win64.node' +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import EventEmitter from 'node:events' + +import { AppCommonMain } from '../app-common' +import { AkariIpcMain } from '../ipc' +import { KeyDefinition, UNIFIED_KEY_ID, VKEY_MAP, isModifierKey } from './definitions' + +interface ShortcutDetails { + keyCodes: number[] + keys: KeyDefinition[] + id: string + unifiedId: string +} + +/** + * 处理键盘快捷键的模块 + * 通过较为 Native 的方式, 使其可以在程序外任何地方使用, 前提是程序有管理员权限 + */ +export class KeyboardShortcutsMain implements IAkariShardInitDispose { + static id = 'keyboard-shortcuts-main' + static dependencies = ['app-common-main', 'akari-ipc-main'] + + private readonly _common: AppCommonMain + private readonly _ipc: AkariIpcMain + + /** 除了修饰键之外的其他按键 */ + private readonly _pressedOtherKeys = new Set() + + /** 修饰键 */ + private readonly _pressedModifierKeys = new Set() + + /** 最后一次激活的快捷键组合, 用于追踪在所有按键结束后的快捷键情况 */ + private _lastActiveShortcut: number[] = [] + + /** + * 修饰键的惯例可读顺序, 用于组合成好看的字符串 + */ + static readonly MODIFIER_READING_ORDER = { + 162: 0, + 163: 1, + 16: 2, + 160: 3, + 161: 4, + 18: 5, + 164: 6, + 165: 7, + 91: 8, + 92: 9 + } + + /** + * 原生监听到的键盘事件 + */ + public readonly events = new EventEmitter<{ + /** + * 在任意一个有意义的快捷键被按下时触发 + */ + shortcut: [details: ShortcutDetails] + + /** + * 在所有按键结束后, 且最后一次激活的快捷键组合被触发时触发 + * 这个事件用于规避 SendInput 在模拟过程中, 和现有正在进行的按键冲突的问题 + */ + 'last-active-shortcut': [details: ShortcutDetails] + }>() + + constructor(deps: any) { + this._common = deps['app-common-main'] + this._ipc = deps['akari-ipc-main'] + } + + sendKey(code: number, press: boolean) { + input.sendKey(code, press) + } + + sendKeys(str: string) { + input.sendKeys(str) + } + + async onInit() { + if (this._common.state.isAdministrator) { + input.startHook() + + input.onKeyEvent((key) => { + const [keyCodeRaw, state] = key.split(',') + + // ignore VK_PACKET (231) + if (keyCodeRaw === '231') { + return + } + + if (!VKEY_MAP[keyCodeRaw]) { + return + } + + const keyCode = parseInt(keyCodeRaw, 10) + const isDown = state === 'DOWN' + + if (isModifierKey(keyCode)) { + // skip if unchanged + if (this._pressedModifierKeys.has(keyCode) === isDown) { + return + } + + if (isDown) { + this._pressedModifierKeys.add(keyCode) + } else { + this._pressedModifierKeys.delete(keyCode) + } + } else { + if (isDown) { + this._pressedOtherKeys.add(keyCode) + const modifiers = Array.from(this._pressedModifierKeys.values()) + const sorted = modifiers.toSorted((a, b) => { + return ( + KeyboardShortcutsMain.MODIFIER_READING_ORDER[a] - + KeyboardShortcutsMain.MODIFIER_READING_ORDER[b] + ) + }) + + const keyCodes = [...sorted, keyCode] + const keys = keyCodes.map((k) => VKEY_MAP[k]) + const combined = keys.map((k) => k.keyId).join('+') + const unified = [ + ...new Set(keyCodes.map((k) => UNIFIED_KEY_ID[k] || VKEY_MAP[k].keyId)) + ].join('+') + + this._lastActiveShortcut = keyCodes + this.events.emit('shortcut', { keyCodes, keys, id: combined, unifiedId: unified }) + this._ipc.sendEvent(KeyboardShortcutsMain.id, 'shortcut', { + keyCodes, + keys, + id: combined, + unifiedId: unified + }) + } else { + this._pressedOtherKeys.delete(keyCode) + } + } + + if ( + this._pressedModifierKeys.size === 0 && + this._pressedOtherKeys.size === 0 && + this._lastActiveShortcut.length > 0 + ) { + const keys = this._lastActiveShortcut.map((k) => VKEY_MAP[k]) + const combined = keys.map((k) => k.keyId).join('+') + const unified = [ + ...new Set(this._lastActiveShortcut.map((k) => UNIFIED_KEY_ID[k] || VKEY_MAP[k].keyId)) + ].join('+') + + this.events.emit('last-active-shortcut', { + keyCodes: this._lastActiveShortcut, + keys, + id: combined, + unifiedId: unified + }) + this._ipc.sendEvent(KeyboardShortcutsMain.id, 'last-active-shortcut', { + keyCodes: this._lastActiveShortcut, + keys, + id: combined, + unifiedId: unified + }) + this._lastActiveShortcut = [] + } + }) + } + } + + async onDispose() { + if (this._common.state.isAdministrator) { + input.stopHook() + } + this.events.removeAllListeners() + } +} diff --git a/src/main/shards/league-client-ux/index.ts b/src/main/shards/league-client-ux/index.ts index 3616eca6..de942a6f 100644 --- a/src/main/shards/league-client-ux/index.ts +++ b/src/main/shards/league-client-ux/index.ts @@ -4,6 +4,9 @@ import { IAkariShardInitDispose } from '@shared/akari-shard/interface' import { AppCommonMain } from '../app-common' import { AkariIpcMain } from '../ipc' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' +import { MobxUtilsMain } from '../mobx-utils' +import { SettingFactoryMain } from '../setting-factory' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { LeagueClientUxSettings, LeagueClientUxState } from './state' /** @@ -15,7 +18,8 @@ export class LeagueClientUxMain implements IAkariShardInitDispose { 'akari-ipc-main', 'mobx-utils-main', 'app-common-main', - 'logger-factory-main' + 'logger-factory-main', + 'setting-factory-main' ] static UX_PROCESS_NAME = 'LeagueClientUx.exe' @@ -27,7 +31,10 @@ export class LeagueClientUxMain implements IAkariShardInitDispose { private readonly _ipc: AkariIpcMain private readonly _common: AppCommonMain private readonly _loggerFactory: LoggerFactoryMain + private readonly _settingFactory: SettingFactoryMain private readonly _log: AkariLogger + private readonly _mobx: MobxUtilsMain + private readonly _setting: SetterSettingService private _pollTimerId: NodeJS.Timeout | null = null @@ -35,11 +42,24 @@ export class LeagueClientUxMain implements IAkariShardInitDispose { this._ipc = deps['akari-ipc-main'] this._common = deps['app-common-main'] this._loggerFactory = deps['logger-factory-main'] + this._mobx = deps['mobx-utils-main'] + this._settingFactory = deps['setting-factory-main'] this._log = this._loggerFactory.create(LeagueClientUxMain.id) + this._setting = this._settingFactory.create( + LeagueClientUxMain.id, + { + useWmic: { default: this.settings.useWmic } + }, + this.settings + ) } async onInit() { this._handlePollExistingUx() + + await this._setting.applyToState() + this._mobx.propSync(LeagueClientUxMain.id, 'settings', this.settings, ['useWmic']) + this._mobx.propSync(LeagueClientUxMain.id, 'state', this.state, ['launchedClients']) } async onDispose() { diff --git a/src/main/shards/league-client/index.ts b/src/main/shards/league-client/index.ts index 949006d1..3da7cb0f 100644 --- a/src/main/shards/league-client/index.ts +++ b/src/main/shards/league-client/index.ts @@ -16,7 +16,7 @@ import { LeagueClientUxMain } from '../league-client-ux' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' import { MobxUtilsMain } from '../mobx-utils' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { LeagueClientSyncedData } from './data' import { LeagueClientSettings, LeagueClientState } from './state' @@ -60,7 +60,7 @@ export class LeagueClientMain implements IAkariShardInitDispose { private readonly _mobx: MobxUtilsMain private readonly _ux: LeagueClientUxMain private readonly _settingFactory: SettingFactoryMain - private readonly _setting: MobxSettingService + private readonly _setting: SetterSettingService private _http: AxiosInstance | null = null private _ws: WebSocket | null = null diff --git a/src/main/shards/logger-factory/index.ts b/src/main/shards/logger-factory/index.ts index 7cbd76e4..07930b85 100644 --- a/src/main/shards/logger-factory/index.ts +++ b/src/main/shards/logger-factory/index.ts @@ -129,17 +129,25 @@ export class LoggerFactoryMain implements IAkariShardInitDispose { (namespace: string, level: string, ...args: any[]) => { switch (level) { case 'info': - return this.info(namespace, ...args) + this.info(namespace, ...args) + return case 'warn': - return this.warn(namespace, ...args) + this.warn(namespace, ...args) + return case 'error': - return this.error(namespace, ...args) + this.error(namespace, ...args) + return case 'debug': - return this.debug(namespace, ...args) + this.debug(namespace, ...args) + return default: - return this.info(namespace, ...args) + this.info(namespace, ...args) } } ) + + this._ipc.onCall(LoggerFactoryMain.id, 'openLogsDir', () => { + this.openLogsDir() + }) } } diff --git a/src/main/shards/ongoing-game/index.ts b/src/main/shards/ongoing-game/index.ts index d5c87e6c..7c3bbe59 100644 --- a/src/main/shards/ongoing-game/index.ts +++ b/src/main/shards/ongoing-game/index.ts @@ -1,772 +1,790 @@ -import { IAkariShardInitDispose } from '@shared/akari-shard/interface' -import { EMPTY_PUUID } from '@shared/constants/common' -import { - MatchHistoryGamesAnalysisAll, - MatchHistoryGamesAnalysisTeamSide, - analyzeMatchHistory, - analyzeTeamMatchHistory -} from '@shared/utils/analysis' -import { calculateTogetherTimes, removeOverlappingSubsets } from '@shared/utils/team-up-calc' -import { comparer, computed, toJS } from 'mobx' -import PQueue from 'p-queue' - -import { AkariIpcMain } from '../ipc' -import { LeagueClientMain } from '../league-client' -import { AkariLogger, LoggerFactoryMain } from '../logger-factory' -import { MobxUtilsMain } from '../mobx-utils' -import { SavedPlayerMain } from '../saved-player' -import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' -import { SgpMain } from '../sgp' -import { OngoingGameSettings, OngoingGameState } from './state' - -/** - * 用于游戏过程中的对局分析, 包括在此期间的战绩查询, 计算等 - */ -export class OngoingGameMain implements IAkariShardInitDispose { - static id = 'ongoing-game-main' - static dependencies = [ - 'logger-factory-main', - 'setting-factory-main', - 'league-client-main', - 'akari-ipc-main', - 'mobx-utils-main', - 'sgp-main', - 'saved-player-main' - ] - - static LOADING_PRIORITY = { - SUMMONER: 1, - MATCH_HISTORY: 2, - SAVED_INFO: 3, - RANKED_STATS: 4, - CHAMPION_MASTERY: 5 - } - - /** - * 目前已知的可用队列, 这是为了避免查询不支持队列时返回为空的情况 - */ - static SAFE_QUEUES = new Set([ - `q_420`, - `q_430`, - `q_440`, - `q_450`, // ARAM - `q_490`, - `q_900`, // URF - `q_1400`, // ULTBOOK - `q_1700`, - `q_1900` - ]) - - private readonly _loggerFactory: LoggerFactoryMain - private readonly _settingFactory: SettingFactoryMain - private readonly _log: AkariLogger - private readonly _lc: LeagueClientMain - private readonly _setting: MobxSettingService - private readonly _mobx: MobxUtilsMain - private readonly _ipc: AkariIpcMain - private readonly _sgp: SgpMain - private readonly _saved: SavedPlayerMain - - public readonly settings = new OngoingGameSettings() - public readonly state: OngoingGameState - - /** 为**加载战绩**设置的特例 */ - private readonly _mhQueue = new PQueue() - /** 为**加载战绩**设置的特例 */ - private _mhController: AbortController | null = null - - /** - * 其他 API 的并发控制 - */ - private readonly _queue = new PQueue() - private _controller: AbortController | null = null - - constructor(deps: any) { - this._loggerFactory = deps['logger-factory-main'] - this._log = this._loggerFactory.create(OngoingGameMain.id) - this._lc = deps['league-client-main'] - this._mobx = deps['mobx-utils-main'] - this._ipc = deps['akari-ipc-main'] - this._settingFactory = deps['setting-factory-main'] - this._sgp = deps['sgp-main'] - this._saved = deps['saved-player-main'] - this._setting = this._settingFactory.create( - OngoingGameMain.id, - { - concurrency: { default: this.settings.concurrency }, - enabled: { default: this.settings.enabled }, - matchHistoryLoadCount: { default: this.settings.matchHistoryLoadCount }, - orderPlayerBy: { default: this.settings.orderPlayerBy }, - preMadeTeamThreshold: { default: this.settings.preMadeTeamThreshold }, - matchHistoryUseSgpApi: { default: this.settings.matchHistoryUseSgpApi } - }, - this.settings - ) - this.state = new OngoingGameState(this._lc.data) - } - - private async _handleState() { - await this._setting.applyToState() - this._mobx.propSync(OngoingGameMain.id, 'settings', this.settings, [ - 'concurrency', - 'enabled', - 'matchHistoryLoadCount', - 'orderPlayerBy', - 'preMadeTeamThreshold', - 'matchHistoryUseSgpApi' - ]) - this._mobx.propSync(OngoingGameMain.id, 'state', this.state, [ - 'championSelections', - 'gameInfo', - 'positionAssignments', - 'premadeTeams', - 'queryStage', - 'teams', - 'matchHistoryTag' - ]) - } - - async onInit() { - await this._handleState() - this._handlePQueue() - this._handleLoad() - this._handleIpcCall() - this._handleCalculation() - } - - private _handlePQueue() { - this._mhQueue.on('active', () => { - this._log.debug( - `更新队列: 并发=${this._mhQueue.concurrency}, 当前数量=${this._mhQueue.size}, 等待中=${this._mhQueue.pending}` - ) - }) - - this._queue.on('active', () => { - this._log.debug( - `更新队列: 并发=${this._mhQueue.concurrency}, 当前数量=${this._mhQueue.size}, 等待中=${this._mhQueue.pending}` - ) - }) - - this._mobx.reaction( - () => this.settings.concurrency, - (concurrency) => { - this._mhQueue.concurrency = concurrency - this._queue.concurrency = concurrency - }, - { fireImmediately: true } - ) - } - - private _handleLoad() { - this._mobx.reaction( - () => this.state.queryStage, - (stage) => { - // 设计时, 必须保证加载流程是完全可控的 - // 阶段切换会立即取消之前的请求, 虽然在大部分情况下无需这么做 - if (this._controller) { - this._controller.abort() - this._controller = null - } - - if (this._mhController) { - this._mhController.abort() - this._mhController = null - } - - if (stage.phase === 'unavailable') { - this.state.clear() - this.state.setMatchHistoryTag(null) - this._ipc.sendEvent(OngoingGameMain.id, 'clear') - return - } - - this._controller = new AbortController() - this._mhController = new AbortController() - - if (this.state.queryStage.phase === 'champ-select') { - this._champSelect({ - mhSignal: this._mhController.signal, - signal: this._controller.signal, - force: false - }) - } else if (this.state.queryStage.phase === 'in-game') { - this._inGame({ - mhSignal: this._mhController.signal, - signal: this._controller.signal, - force: false - }) - } - } - ) - - // 这些条件发生变化, 那么就会重新计算 - const unionQueryCondition = computed( - () => { - return { - count: this.settings.matchHistoryLoadCount, - tag: this.state.matchHistoryTag || undefined - } - }, - { equals: comparer.structural } - ) - - // 战绩重新加载条件 - this._mobx.reaction( - () => unionQueryCondition.get(), - (condition) => { - if (this.state.queryStage.phase === 'unavailable') { - return - } - - if (this._mhController) { - this._mhController.abort() - this._mhController = null - } - - const controller = new AbortController() - this._mhController = controller - - const puuids = this.getPuuidsToLoadForPlayers() - puuids.forEach((puuid) => { - this._loadPlayerMatchHistory(puuid, { - signal: controller.signal, - count: condition.count, - tag: condition.tag, - force: false - }) - }) - }, - { delay: 1000 } - ) - } - - /** - * - * @param options 其中的 force, 用于标识是否强制刷新. 若为 false, 在查询条件未发生变动时不会重新加载 - */ - private _champSelect(options: { mhSignal: AbortSignal; signal: AbortSignal; force: boolean }) { - const { mhSignal, signal, force } = options - - const puuids = this.getPuuidsToLoadForPlayers() - puuids.forEach((puuid) => { - this._loadPlayerMatchHistory(puuid, { - signal: mhSignal, - force, - count: this.settings.matchHistoryLoadCount - }) - this._loadPlayerSummoner(puuid, { signal, force }) - this._loadPlayerRankedStats(puuid, { signal, force }) - this._loadPlayerSavedInfo(puuid, { signal, force }) - this._loadPlayerChampionMasteries(puuid, { signal, force }) - }) - } - - /** 目前实现同 #._champSelect */ - private _inGame(options: { mhSignal: AbortSignal; signal: AbortSignal; force: boolean }) { - const { mhSignal, signal, force } = options - - const puuids = this.getPuuidsToLoadForPlayers() - puuids.forEach((puuid) => { - this._loadPlayerMatchHistory(puuid, { - signal: mhSignal, - force, - count: this.settings.matchHistoryLoadCount - }) - this._loadPlayerSummoner(puuid, { signal, force }) - this._loadPlayerRankedStats(puuid, { signal, force }) - this._loadPlayerSavedInfo(puuid, { signal, force }) - this._loadPlayerChampionMasteries(puuid, { signal, force }) - }) - } - - private _clearAndReload() { - if (this._controller) { - this._controller.abort() - this._controller = null - } - - if (this._mhController) { - this._mhController.abort() - this._mhController = null - } - - this.state.clear() - this._ipc.sendEvent(OngoingGameMain.id, 'clear') - - this._controller = new AbortController() - this._mhController = new AbortController() - - if (this.state.queryStage.phase === 'champ-select') { - this._champSelect({ - mhSignal: this._mhController.signal, - signal: this._controller.signal, - force: true - }) - } else if (this.state.queryStage.phase === 'in-game') { - this._inGame({ - mhSignal: this._mhController.signal, - signal: this._controller.signal, - force: true - }) - } - } - - private getPuuidsToLoadForPlayers() { - if (this.state.queryStage.phase === 'unavailable') { - return [] - } - - if (this.state.queryStage.phase === 'champ-select') { - const session = this._lc.data.champSelect.session - if (!session) { - return [] - } - - const m = session.myTeam.filter((p) => p.puuid && p.puuid !== EMPTY_PUUID).map((t) => t.puuid) - - const t = session.theirTeam - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .map((t) => t.puuid) - - return [...m, ...t] - } else if (this.state.queryStage.phase === 'in-game') { - const session = this._lc.data.gameflow.session - - if (!session) { - return [] - } - - const m = session.gameData.teamOne - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .map((t) => t.puuid) - - const t = session.gameData.teamTwo - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .map((t) => t.puuid) - - return [...m, ...t] - } - - return [] - } - - private async _loadPlayerMatchHistory( - puuid: string, - options: { - signal?: AbortSignal - tag?: string - count?: number - force?: boolean - } = {} - ) { - const isAbleToUseSgpApi = - this.settings.matchHistoryUseSgpApi && - this._sgp.state.availability.serversSupported.matchHistory - - let { count = 20, signal, tag, force } = options - - const current = this.state.matchHistory[puuid] - if ( - !force && // 在不强制更新的情况下 - current && // 在存在值的情况下 - current.targetCount === count && // 必要条件之一: 加载数量没有变化 - current.source === (isAbleToUseSgpApi ? 'sgp' : 'lcu') && // 必要条件之一: 数据来源没有变化 - (!isAbleToUseSgpApi || current.tag === tag) // 必要条件之一: SGP API 时, tag 也必须一致 (LCU API 将忽略 tag, 本来也没用) - ) { - // 以上不需要重新加载的前提, 是假设在一个对局期间, 这些数据都不会发生变化 - // 事实上在一个对局期间, 大部分情况是不会发生变化的 - return - } - - if (isAbleToUseSgpApi) { - // SGP API 可以筛选战绩 - // 在未设置筛选条件的情况下, 会根据设置的偏好来决定是否筛选 - if (tag === undefined) { - if (this.settings.matchHistoryQueuePreference === 'all') { - tag = undefined - } else if ( - this.settings.matchHistoryQueuePreference === 'current' && - this.state.queryStage.gameInfo && - OngoingGameMain.SAFE_QUEUES.has(`q_${this.state.queryStage.gameInfo.queueId}`) - ) { - tag = `q_${this.state.queryStage.gameInfo.queueId}` - } - } else { - // 对于已经设置 tag 偏好的情况, 会检测是否是安全队列, 否则重置 - if (!OngoingGameMain.SAFE_QUEUES.has(tag)) { - tag = undefined - } - } - - const data = await this._mhQueue - .add(() => this._sgp.getMatchHistoryLcuFormat(puuid, 0, count, tag), { - signal, - priority: OngoingGameMain.LOADING_PRIORITY.MATCH_HISTORY - }) - .catch((error) => this._handleAbortError(error)) - - if (!data) { - return - } - - const toBeLoaded = { - data: data.games.games, - targetCount: count, - source: 'sgp' as 'sgp' | 'lcu', - tag - } - - this.state.matchHistory[puuid] = toBeLoaded - this._ipc.sendEvent(OngoingGameMain.id, 'match-history-loaded', puuid, toBeLoaded) - } else { - const res = await this._queue - .add(() => this._lc.api.matchHistory.getMatchHistory(puuid, 0, count - 1), { - signal, - priority: OngoingGameMain.LOADING_PRIORITY.MATCH_HISTORY - }) - .catch((error) => this._handleAbortError(error)) - - if (!res) { - return - } - - const data = res.data - - const toBeLoaded = { - data: data.games.games, - targetCount: count, - source: 'lcu' as 'sgp' | 'lcu' - } - - this.state.matchHistory[puuid] = toBeLoaded - this._ipc.sendEvent(OngoingGameMain.id, 'match-history-loaded', puuid, toBeLoaded) - } - } - - private async _loadPlayerSummoner( - puuid: string, - options: { - signal?: AbortSignal - force?: boolean - } = {} - ) { - const { signal, force } = options - - // 如果不是强制更新, 并且已经有数据, 那么就不再加载 - if (!force && this.state.summoner[puuid]) { - return - } - - const res = await this._queue - .add(() => this._lc.api.summoner.getSummonerByPuuid(puuid), { - signal, - priority: OngoingGameMain.LOADING_PRIORITY.SUMMONER - }) - .catch((error) => this._handleAbortError(error)) - - if (!res) { - return - } - - const data = res.data - - const toBeLoaded = { data, source: 'lcu' as 'sgp' | 'lcu' } - this.state.summoner[puuid] = toBeLoaded - this._ipc.sendEvent(OngoingGameMain.id, 'summoner-loaded', puuid, toBeLoaded) - } - - private async _loadPlayerSavedInfo( - puuid: string, - options: { - signal?: AbortSignal - force?: boolean - } = {} - ) { - // just used to suppress ts error - if (!this._lc.state.auth || !this._lc.data.summoner.me) { - return - } - - const query = { - puuid, - selfPuuid: this._lc.data.summoner.me.puuid, - region: this._lc.state.auth.region, - rsoPlatformId: this._lc.state.auth.rsoPlatformId - } - - const { signal, force } = options - - if (!force && this.state.savedInfo[puuid]) { - return - } - - const res = await this._queue - .add(() => this._saved.querySavedPlayerWithGames(query), { - signal, - priority: OngoingGameMain.LOADING_PRIORITY.SAVED_INFO - }) - .catch((error) => this._handleAbortError(error)) - - if (!res) { - return - } - - this.state.savedInfo[puuid] = res - this._ipc.sendEvent(OngoingGameMain.id, 'saved-info-loaded', puuid, res) - } - - private async _loadPlayerRankedStats( - puuid: string, - options: { - signal?: AbortSignal - force?: boolean - } = {} - ) { - const { signal, force } = options - - if (!force && this.state.rankedStats[puuid]) { - return - } - - const res = await this._mhQueue - .add(() => this._lc.api.ranked.getRankedStats(puuid), { - signal, - priority: OngoingGameMain.LOADING_PRIORITY.RANKED_STATS - }) - .catch((error) => this._handleAbortError(error)) - - if (!res) { - return - } - - const data = res.data - - const toBeLoaded = { data, source: 'lcu' as 'sgp' | 'lcu' } - this.state.rankedStats[puuid] = toBeLoaded - this._ipc.sendEvent(OngoingGameMain.id, 'ranked-stats-loaded', puuid, toBeLoaded) - } - - private async _loadPlayerChampionMasteries( - puuid: string, - options: { - signal?: AbortSignal - force?: boolean - } = {} - ) { - const { signal, force } = options - - if (!force && this.state.championMastery[puuid]) { - return - } - - const res = await this._mhQueue - .add(() => this._lc.api.championMastery.getPlayerChampionMastery(puuid), { - signal, - priority: OngoingGameMain.LOADING_PRIORITY.CHAMPION_MASTERY - }) - .catch((error) => this._handleAbortError(error)) - - if (!res) { - return - } - - const data = res.data - - const simplifiedMastery = data - .map((m) => ({ - championId: m.championId, - championLevel: m.championLevel, - championPoints: m.championPoints, - milestoneGrades: m.milestoneGrades - })) - .reduce((obj, cur) => { - obj[cur.championId] = cur - return obj - }, {} as any) - - const toBeLoaded = { data: simplifiedMastery, source: 'lcu' as 'sgp' | 'lcu' } - this.state.championMastery[puuid] = toBeLoaded - this._ipc.sendEvent(OngoingGameMain.id, 'champion-mastery-loaded', puuid, toBeLoaded) - } - - private _handleIpcCall() { - this._ipc.onCall(OngoingGameMain.id, 'getAll', () => { - const matchHistory = toJS(this.state.matchHistory) - const summoner = toJS(this.state.summoner) - const rankedStats = toJS(this.state.rankedStats) - const savedInfo = toJS(this.state.savedInfo) - - return { matchHistory, summoner, rankedStats, savedInfo } - }) - - this._ipc.onCall(OngoingGameMain.id, 'setMatchHistoryTag', (tag: string) => { - if (OngoingGameMain.SAFE_QUEUES.has(tag)) { - this.state.setMatchHistoryTag(tag) - } - }) - - this._ipc.onCall(OngoingGameMain.id, 'reload', () => { - this._clearAndReload() - }) - } - - private _calcTeamUp() { - if (!this.state.teams) { - return null - } - - const games = Object.values(this.state.matchHistory) - .map((m) => m.data) - .flat() - - if (!games.length) { - return null - } - - // 统计所有目前游戏中的每个队伍,并且将这些队伍分别视为一个独立的个体,使用 `${游戏ID}|${队伍ID}` 进行唯一区分 - const teamSides = new Map() - for (const game of games) { - const mode = game.gameMode - - // participantId -> puuid - const participantsMap = game.participantIdentities.reduce( - (obj, current) => { - obj[current.participantId] = current.player.puuid - return obj - }, - {} as Record - ) - - let grouped: { teamId: number; puuid: string }[] - - // 对于竞技场模式,在战绩接口中只有一个队伍。如果要区分小队,需要使用 subteamPlacement 或 subteamId 字段 - if (mode === 'CHERRY') { - grouped = game.participants.map((p) => ({ - teamId: p.stats.subteamPlacement, // 取值范围是 1, 2, 3, 4, 这个实际上也是最终队伍排名 - puuid: participantsMap[p.participantId] - })) - } else { - // 对于其他模式,按照两队式计算 - grouped = game.participants.map((p) => ({ - teamId: p.teamId, - puuid: participantsMap[p.participantId] - })) - } - - // teamId -> puuid[],这个记录的是这条战绩中的 - const teamPlayersMap = grouped.reduce( - (obj, current) => { - if (obj[current.teamId]) { - obj[current.teamId].push(current.puuid) - } else { - obj[current.teamId] = [current.puuid] - } - return obj - }, - {} as Record - ) - - // sideId -> puuid[],按照队伍区分。 - Object.entries(teamPlayersMap).forEach(([teamId, players]) => { - const sideId = `${game.gameId}|${teamId}` - if (teamSides.has(sideId)) { - return - } - teamSides.set(sideId, players) - }) - } - - const matches = Array.from(teamSides).map(([id /* sideId */, players]) => ({ id, players })) - - // key: teamSide, values: { players: string[], times: number }[] - const result = Object.entries(this.state.teams).reduce( - (obj, [team, teamPlayers]) => { - obj[team] = calculateTogetherTimes(matches, teamPlayers, this.settings.preMadeTeamThreshold) - - return obj - }, - {} as Record< - string, - { - players: string[] - times: number - }[] - > - ) - - // teamSide -> players[][] - const combinedGroups: Record = {} - - for (const [team, playerGroups] of Object.entries(result)) { - const groups = playerGroups.map((pg) => pg.players) - combinedGroups[team] = removeOverlappingSubsets(groups) as string[][] - } - - return combinedGroups - } - - private _calcAnalysis() { - if (!this.state.teams) { - return null - } - - const playerAnalyses: Record = {} - - for (const [puuid, matchHistory] of Object.entries(this.state.matchHistory)) { - if (!matchHistory) { - continue - } - - const analysis = analyzeMatchHistory( - matchHistory.data.map((mh) => ({ game: mh, isDetailed: true })), // for compatibility - puuid - ) - if (analysis) { - playerAnalyses[puuid] = analysis - } - } - - const teamAnalyses: Record = {} - - for (const [sideId, puuids] of Object.entries(this.state.teams)) { - const teamPlayerAnalyses = puuids.map((p) => playerAnalyses[p]).filter(Boolean) - const teamAnalysis = analyzeTeamMatchHistory(teamPlayerAnalyses) - if (teamAnalysis) { - teamAnalyses[sideId] = teamAnalysis - } - } - - return { - players: playerAnalyses, - teams: teamAnalyses - } - } - - private _handleCalculation() { - // 重新计算战绩信息 - this._mobx.reaction( - () => Object.values(this.state.matchHistory), - (_changedV) => { - this.state.setPlayerStats(this._calcAnalysis()) - }, - { delay: 200, equals: comparer.shallow } - ) - - // 重新计算预组队 - this._mobx.reaction( - () => [Object.values(this.state.matchHistory), this.settings.preMadeTeamThreshold] as const, - ([_changedV, _threshold]) => { - this.state.setPremadeTeams(this._calcTeamUp()) - }, - { delay: 200, equals: comparer.shallow } - ) - } - - private _handleAbortError(e: any) { - if (e instanceof Error && e.name === 'AbortError') { - return - } - return Promise.reject(e) - } -} +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { EMPTY_PUUID } from '@shared/constants/common' +import { + MatchHistoryGamesAnalysisAll, + MatchHistoryGamesAnalysisTeamSide, + analyzeMatchHistory, + analyzeTeamMatchHistory +} from '@shared/utils/analysis' +import { calculateTogetherTimes, removeOverlappingSubsets } from '@shared/utils/team-up-calc' +import _ from 'lodash' +import { comparer, computed, toJS } from 'mobx' +import PQueue from 'p-queue' + +import { AkariIpcMain } from '../ipc' +import { LeagueClientMain } from '../league-client' +import { AkariLogger, LoggerFactoryMain } from '../logger-factory' +import { MobxUtilsMain } from '../mobx-utils' +import { SavedPlayerMain } from '../saved-player' +import { SettingFactoryMain } from '../setting-factory' +import { SetterSettingService } from '../setting-factory/setter-setting-service' +import { SgpMain } from '../sgp' +import { OngoingGameSettings, OngoingGameState } from './state' + +/** + * 用于游戏过程中的对局分析, 包括在此期间的战绩查询, 计算等 + */ +export class OngoingGameMain implements IAkariShardInitDispose { + static id = 'ongoing-game-main' + static dependencies = [ + 'logger-factory-main', + 'setting-factory-main', + 'league-client-main', + 'akari-ipc-main', + 'mobx-utils-main', + 'sgp-main', + 'saved-player-main' + ] + + static LOADING_PRIORITY = { + SUMMONER: 1, + MATCH_HISTORY: 2, + SAVED_INFO: 3, + RANKED_STATS: 4, + CHAMPION_MASTERY: 5 + } + + /** + * 目前已知的可用队列, 这是为了避免查询不支持队列时返回为空的情况 + */ + static SAFE_TAGS = new Set([ + `q_420`, + `q_430`, + `q_440`, + `q_450`, // ARAM + `q_490`, + `q_900`, // URF + `q_1400`, // ULTBOOK + `q_1700`, + `q_1900` + ]) + + private readonly _loggerFactory: LoggerFactoryMain + private readonly _settingFactory: SettingFactoryMain + private readonly _log: AkariLogger + private readonly _lc: LeagueClientMain + private readonly _setting: SetterSettingService + private readonly _mobx: MobxUtilsMain + private readonly _ipc: AkariIpcMain + private readonly _sgp: SgpMain + private readonly _saved: SavedPlayerMain + + public readonly settings = new OngoingGameSettings() + public readonly state: OngoingGameState + + /** 为**加载战绩**设置的特例 */ + private readonly _mhQueue = new PQueue() + /** 为**加载战绩**设置的特例 */ + private _mhController: AbortController | null = null + + /** + * 其他 API 的并发控制 + */ + private readonly _queue = new PQueue() + private _controller: AbortController | null = null + + private _debouncedUpdateMatchHistoryFn = _.debounce(() => this._updateMatchHistory(), 500) + + constructor(deps: any) { + this._loggerFactory = deps['logger-factory-main'] + this._log = this._loggerFactory.create(OngoingGameMain.id) + this._lc = deps['league-client-main'] + this._mobx = deps['mobx-utils-main'] + this._ipc = deps['akari-ipc-main'] + this._settingFactory = deps['setting-factory-main'] + this._sgp = deps['sgp-main'] + this._saved = deps['saved-player-main'] + this._setting = this._settingFactory.create( + OngoingGameMain.id, + { + concurrency: { default: this.settings.concurrency }, + enabled: { default: this.settings.enabled }, + matchHistoryLoadCount: { default: this.settings.matchHistoryLoadCount }, + premadeTeamThreshold: { default: this.settings.premadeTeamThreshold }, + matchHistoryUseSgpApi: { default: this.settings.matchHistoryUseSgpApi }, + matchHistoryTagPreference: { default: this.settings.matchHistoryTagPreference } + }, + this.settings + ) + this.state = new OngoingGameState(this._lc.data) + } + + private async _handleState() { + await this._setting.applyToState() + this._mobx.propSync(OngoingGameMain.id, 'settings', this.settings, [ + 'concurrency', + 'enabled', + 'matchHistoryLoadCount', + 'premadeTeamThreshold', + 'matchHistoryUseSgpApi', + 'matchHistoryTagPreference' + ]) + this._mobx.propSync(OngoingGameMain.id, 'state', this.state, [ + 'championSelections', + 'gameInfo', + 'positionAssignments', + 'premadeTeams', + 'queryStage', + 'teams', + 'matchHistoryTag' + ]) + } + + async onInit() { + await this._handleState() + this._handlePQueue() + this._handleLoad() + this._handleIpcCall() + this._handleCalculation() + + // for better control + this._setting.onChange('matchHistoryLoadCount', async (value, { setter }) => { + if (value >= 1 && value <= 200) { + await setter(value) + this._debouncedUpdateMatchHistoryFn() + } + }) + + this._setting.onChange('matchHistoryUseSgpApi', async (value, { setter }) => { + await setter(value) + this._debouncedUpdateMatchHistoryFn() + }) + + this._setting.onChange('premadeTeamThreshold', async (value, { setter }) => { + if (value >= 3) { + await setter(value) + } + }) + } + + private _handlePQueue() { + this._mhQueue.on('active', () => { + this._log.debug( + `更新队列: 并发=${this._mhQueue.concurrency}, 当前数量=${this._mhQueue.size}, 等待中=${this._mhQueue.pending}` + ) + }) + + this._queue.on('active', () => { + this._log.debug( + `更新队列: 并发=${this._mhQueue.concurrency}, 当前数量=${this._mhQueue.size}, 等待中=${this._mhQueue.pending}` + ) + }) + + this._mobx.reaction( + () => this.settings.concurrency, + (concurrency) => { + this._mhQueue.concurrency = concurrency + this._queue.concurrency = concurrency + }, + { fireImmediately: true } + ) + } + + private _handleLoad() { + this._mobx.reaction( + () => [this.state.queryStage, this.settings.enabled] as const, + ([stage, enabled]) => { + if (this._controller) { + this._controller.abort() + this._controller = null + } + + if (this._mhController) { + this._mhController.abort() + this._mhController = null + } + + this._debouncedUpdateMatchHistoryFn.cancel() + + if (stage.phase === 'unavailable' || !enabled) { + this.state.clear() + this.state.setMatchHistoryTag('all') + this._ipc.sendEvent(OngoingGameMain.id, 'clear') + return + } + + this._controller = new AbortController() + this._mhController = new AbortController() + + if (this.state.queryStage.phase === 'champ-select') { + this._champSelect({ + mhSignal: this._mhController.signal, + signal: this._controller.signal, + force: false + }) + } else if (this.state.queryStage.phase === 'in-game') { + this._inGame({ + mhSignal: this._mhController.signal, + signal: this._controller.signal, + force: false + }) + } + }, + { equals: comparer.shallow } + ) + } + + private _updateMatchHistory() { + if (!this.settings.enabled) { + return + } + + if (this.state.queryStage.phase === 'unavailable') { + return + } + + if (this._mhController) { + this._mhController.abort() + this._mhController = null + } + + const controller = new AbortController() + this._mhController = controller + + const puuids = this.getPuuidsToLoadForPlayers() + puuids.forEach((puuid) => { + this._loadPlayerMatchHistory(puuid, { + signal: controller.signal, + count: this.settings.matchHistoryLoadCount, + tag: this.state.matchHistoryTag, + force: false, + useSgpApi: this.settings.matchHistoryUseSgpApi + }) + }) + } + + /** + * + * @param options 其中的 force, 用于标识是否强制刷新. 若为 false, 在查询条件未发生变动时不会重新加载 + */ + private _champSelect(options: { mhSignal: AbortSignal; signal: AbortSignal; force: boolean }) { + const { mhSignal, signal, force } = options + + const puuids = this.getPuuidsToLoadForPlayers() + puuids.forEach((puuid) => { + this._loadPlayerMatchHistory(puuid, { + signal: mhSignal, + force, + count: this.settings.matchHistoryLoadCount, + useSgpApi: this.settings.matchHistoryUseSgpApi + }) + this._loadPlayerSummoner(puuid, { signal, force }) + this._loadPlayerRankedStats(puuid, { signal, force }) + this._loadPlayerSavedInfo(puuid, { signal, force }) + this._loadPlayerChampionMasteries(puuid, { signal, force }) + }) + } + + /** 目前实现同 #._champSelect */ + private _inGame(options: { mhSignal: AbortSignal; signal: AbortSignal; force: boolean }) { + const { mhSignal, signal, force } = options + + const puuids = this.getPuuidsToLoadForPlayers() + puuids.forEach((puuid) => { + this._loadPlayerMatchHistory(puuid, { + signal: mhSignal, + force, + count: this.settings.matchHistoryLoadCount, + useSgpApi: this.settings.matchHistoryUseSgpApi + }) + this._loadPlayerSummoner(puuid, { signal, force }) + this._loadPlayerRankedStats(puuid, { signal, force }) + this._loadPlayerSavedInfo(puuid, { signal, force }) + this._loadPlayerChampionMasteries(puuid, { signal, force }) + }) + } + + private _clearAndReload() { + if (this._controller) { + this._controller.abort() + this._controller = null + } + + if (this._mhController) { + this._mhController.abort() + this._mhController = null + } + + this.state.clear() + this._ipc.sendEvent(OngoingGameMain.id, 'clear') + + this._controller = new AbortController() + this._mhController = new AbortController() + + if (this.state.queryStage.phase === 'champ-select') { + this._champSelect({ + mhSignal: this._mhController.signal, + signal: this._controller.signal, + force: true + }) + } else if (this.state.queryStage.phase === 'in-game') { + this._inGame({ + mhSignal: this._mhController.signal, + signal: this._controller.signal, + force: true + }) + } + } + + private getPuuidsToLoadForPlayers() { + if (this.state.queryStage.phase === 'unavailable') { + return [] + } + + if (this.state.queryStage.phase === 'champ-select') { + const session = this._lc.data.champSelect.session + if (!session) { + return [] + } + + const m = session.myTeam.filter((p) => p.puuid && p.puuid !== EMPTY_PUUID).map((t) => t.puuid) + + const t = session.theirTeam + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .map((t) => t.puuid) + + return [...m, ...t] + } else if (this.state.queryStage.phase === 'in-game') { + const session = this._lc.data.gameflow.session + + if (!session) { + return [] + } + + const m = session.gameData.teamOne + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .map((t) => t.puuid) + + const t = session.gameData.teamTwo + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .map((t) => t.puuid) + + return [...m, ...t] + } + + return [] + } + + private async _loadPlayerMatchHistory( + puuid: string, + options: { + signal?: AbortSignal + tag?: string + count?: number + force?: boolean + useSgpApi?: boolean + } = {} + ) { + let { count = 20, signal, tag, force, useSgpApi } = options + + const isAbleToUseSgpApi = + useSgpApi && this._sgp.state.availability.serversSupported.matchHistory + + const current = this.state.matchHistory[puuid] + if ( + !force && // 在不强制更新的情况下 + current && // 在存在值的情况下 + current.targetCount === count && // 必要条件之一: 加载数量没有变化 + current.source === (isAbleToUseSgpApi ? 'sgp' : 'lcu') && // 必要条件之一: 数据来源没有变化 + (!isAbleToUseSgpApi || current.tag === tag) // 必要条件之一: SGP API 时, tag 也必须一致 (LCU API 将忽略 tag, 本来也没用) + ) { + // 以上不需要重新加载的前提, 是假设在一个对局期间, 这些数据都不会发生变化 + // ) 事实上在一个对局期间, 大部分情况是不会发生变化的 + return + } + + if (isAbleToUseSgpApi) { + // SGP API 可以筛选战绩 + // 在未设置筛选条件的情况下, 会根据设置的偏好来决定是否筛选 + if (tag === undefined || tag === 'all') { + if (this.settings.matchHistoryTagPreference === 'all') { + this.state.setMatchHistoryTag('all') + } else if ( + this.settings.matchHistoryTagPreference === 'current' && + this.state.queryStage.gameInfo && + OngoingGameMain.SAFE_TAGS.has(`q_${this.state.queryStage.gameInfo.queueId}`) + ) { + tag = `q_${this.state.queryStage.gameInfo.queueId}` + this.state.setMatchHistoryTag(`q_${this.state.queryStage.gameInfo.queueId}`) + } + } else { + // 对于已经设置 tag 偏好的情况, 会检测是否是安全队列, 否则重置 + if (!OngoingGameMain.SAFE_TAGS.has(tag)) { + tag = undefined + this.state.setMatchHistoryTag('all') + } + } + + const data = await this._mhQueue + .add(() => this._sgp.getMatchHistoryLcuFormat(puuid, 0, count, tag), { + signal, + priority: OngoingGameMain.LOADING_PRIORITY.MATCH_HISTORY + }) + .catch((error) => this._handleAbortError(error)) + + if (!data) { + return + } + + const toBeLoaded = { + data: data.games.games, + targetCount: count, + source: 'sgp' as 'sgp' | 'lcu', + tag + } + + this.state.matchHistory[puuid] = toBeLoaded + this._ipc.sendEvent(OngoingGameMain.id, 'match-history-loaded', puuid, toBeLoaded) + } else { + const res = await this._queue + .add(() => this._lc.api.matchHistory.getMatchHistory(puuid, 0, count - 1), { + signal, + priority: OngoingGameMain.LOADING_PRIORITY.MATCH_HISTORY + }) + .catch((error) => this._handleAbortError(error)) + + if (!res) { + return + } + + const data = res.data + + const toBeLoaded = { + data: data.games.games, + targetCount: count, + source: 'lcu' as 'sgp' | 'lcu' + } + + this.state.matchHistory[puuid] = toBeLoaded + this._ipc.sendEvent(OngoingGameMain.id, 'match-history-loaded', puuid, toBeLoaded) + } + } + + private async _loadPlayerSummoner( + puuid: string, + options: { + signal?: AbortSignal + force?: boolean + } = {} + ) { + const { signal, force } = options + + // 如果不是强制更新, 并且已经有数据, 那么就不再加载 + if (!force && this.state.summoner[puuid]) { + return + } + + const res = await this._queue + .add(() => this._lc.api.summoner.getSummonerByPuuid(puuid), { + signal, + priority: OngoingGameMain.LOADING_PRIORITY.SUMMONER + }) + .catch((error) => this._handleAbortError(error)) + + if (!res) { + return + } + + const data = res.data + + const toBeLoaded = { data, source: 'lcu' as 'sgp' | 'lcu' } + this.state.summoner[puuid] = toBeLoaded + this._ipc.sendEvent(OngoingGameMain.id, 'summoner-loaded', puuid, toBeLoaded) + } + + private async _loadPlayerSavedInfo( + puuid: string, + options: { + signal?: AbortSignal + force?: boolean + } = {} + ) { + // just used to suppress ts error + if (!this._lc.state.auth || !this._lc.data.summoner.me) { + return + } + + const query = { + puuid, + selfPuuid: this._lc.data.summoner.me.puuid, + region: this._lc.state.auth.region, + rsoPlatformId: this._lc.state.auth.rsoPlatformId + } + + const { signal, force } = options + + if (!force && this.state.savedInfo[puuid]) { + return + } + + const res = await this._queue + .add(() => this._saved.querySavedPlayerWithGames(query), { + signal, + priority: OngoingGameMain.LOADING_PRIORITY.SAVED_INFO + }) + .catch((error) => this._handleAbortError(error)) + + if (!res) { + return + } + + this.state.savedInfo[puuid] = res + this._ipc.sendEvent(OngoingGameMain.id, 'saved-info-loaded', puuid, res) + } + + private async _loadPlayerRankedStats( + puuid: string, + options: { + signal?: AbortSignal + force?: boolean + } = {} + ) { + const { signal, force } = options + + if (!force && this.state.rankedStats[puuid]) { + return + } + + const res = await this._mhQueue + .add(() => this._lc.api.ranked.getRankedStats(puuid), { + signal, + priority: OngoingGameMain.LOADING_PRIORITY.RANKED_STATS + }) + .catch((error) => this._handleAbortError(error)) + + if (!res) { + return + } + + const data = res.data + + const toBeLoaded = { data, source: 'lcu' as 'sgp' | 'lcu' } + this.state.rankedStats[puuid] = toBeLoaded + this._ipc.sendEvent(OngoingGameMain.id, 'ranked-stats-loaded', puuid, toBeLoaded) + } + + private async _loadPlayerChampionMasteries( + puuid: string, + options: { + signal?: AbortSignal + force?: boolean + } = {} + ) { + const { signal, force } = options + + if (!force && this.state.championMastery[puuid]) { + return + } + + const res = await this._mhQueue + .add(() => this._lc.api.championMastery.getPlayerChampionMastery(puuid), { + signal, + priority: OngoingGameMain.LOADING_PRIORITY.CHAMPION_MASTERY + }) + .catch((error) => this._handleAbortError(error)) + + if (!res) { + return + } + + const data = res.data + + const simplifiedMastery = data + .map((m) => ({ + championId: m.championId, + championLevel: m.championLevel, + championPoints: m.championPoints, + milestoneGrades: m.milestoneGrades + })) + .reduce((obj, cur) => { + obj[cur.championId] = cur + return obj + }, {} as any) + + const toBeLoaded = { data: simplifiedMastery, source: 'lcu' as 'sgp' | 'lcu' } + this.state.championMastery[puuid] = toBeLoaded + this._ipc.sendEvent(OngoingGameMain.id, 'champion-mastery-loaded', puuid, toBeLoaded) + } + + private _handleIpcCall() { + this._ipc.onCall(OngoingGameMain.id, 'getAll', () => { + const matchHistory = toJS(this.state.matchHistory) + const summoner = toJS(this.state.summoner) + const rankedStats = toJS(this.state.rankedStats) + const savedInfo = toJS(this.state.savedInfo) + const championMastery = toJS(this.state.championMastery) + + return { matchHistory, summoner, rankedStats, savedInfo, championMastery } + }) + + this._ipc.onCall(OngoingGameMain.id, 'setMatchHistoryTag', (tag: string) => { + if (OngoingGameMain.SAFE_TAGS.has(tag) || tag === 'all') { + this.state.setMatchHistoryTag(tag) + this._debouncedUpdateMatchHistoryFn() + } + }) + + this._ipc.onCall(OngoingGameMain.id, 'reload', () => { + this._clearAndReload() + }) + } + + private _calcTeamUp() { + if (!this.state.teams) { + return null + } + + const games = Object.values(this.state.matchHistory) + .map((m) => m.data) + .flat() + + if (!games.length) { + return null + } + + // 统计所有目前游戏中的每个队伍,并且将这些队伍分别视为一个独立的个体,使用 `${游戏ID}|${队伍ID}` 进行唯一区分 + const teamSides = new Map() + for (const game of games) { + const mode = game.gameMode + + // participantId -> puuid + const participantsMap = game.participantIdentities.reduce( + (obj, current) => { + obj[current.participantId] = current.player.puuid + return obj + }, + {} as Record + ) + + let grouped: { teamId: number; puuid: string }[] + + // 对于竞技场模式,在战绩接口中只有一个队伍。如果要区分小队,需要使用 subteamPlacement 或 subteamId 字段 + if (mode === 'CHERRY') { + grouped = game.participants.map((p) => ({ + teamId: p.stats.subteamPlacement, // 取值范围是 1, 2, 3, 4, 这个实际上也是最终队伍排名 + puuid: participantsMap[p.participantId] + })) + } else { + // 对于其他模式,按照两队式计算 + grouped = game.participants.map((p) => ({ + teamId: p.teamId, + puuid: participantsMap[p.participantId] + })) + } + + // teamId -> puuid[],这个记录的是这条战绩中的 + const teamPlayersMap = grouped.reduce( + (obj, current) => { + if (obj[current.teamId]) { + obj[current.teamId].push(current.puuid) + } else { + obj[current.teamId] = [current.puuid] + } + return obj + }, + {} as Record + ) + + // sideId -> puuid[],按照队伍区分。 + Object.entries(teamPlayersMap).forEach(([teamId, players]) => { + const sideId = `${game.gameId}|${teamId}` + if (teamSides.has(sideId)) { + return + } + teamSides.set(sideId, players) + }) + } + + const matches = Array.from(teamSides).map(([id /* sideId */, players]) => ({ id, players })) + + // key: teamSide, values: { players: string[], times: number }[] + const result = Object.entries(this.state.teams).reduce( + (obj, [team, teamPlayers]) => { + obj[team] = calculateTogetherTimes(matches, teamPlayers, this.settings.premadeTeamThreshold) + + return obj + }, + {} as Record< + string, + { + players: string[] + times: number + }[] + > + ) + + // teamSide -> players[][] + const combinedGroups: Record = {} + + for (const [team, playerGroups] of Object.entries(result)) { + const groups = playerGroups.map((pg) => pg.players) + combinedGroups[team] = removeOverlappingSubsets(groups) as string[][] + } + + return combinedGroups + } + + private _calcAnalysis() { + if (!this.state.teams) { + return null + } + + const playerAnalyses: Record = {} + + for (const [puuid, matchHistory] of Object.entries(this.state.matchHistory)) { + if (!matchHistory) { + continue + } + + const analysis = analyzeMatchHistory( + matchHistory.data.map((mh) => ({ game: mh, isDetailed: true })), // for compatibility + puuid + ) + if (analysis) { + playerAnalyses[puuid] = analysis + } + } + + const teamAnalyses: Record = {} + + for (const [sideId, puuids] of Object.entries(this.state.teams)) { + const teamPlayerAnalyses = puuids.map((p) => playerAnalyses[p]).filter(Boolean) + const teamAnalysis = analyzeTeamMatchHistory(teamPlayerAnalyses) + if (teamAnalysis) { + teamAnalyses[sideId] = teamAnalysis + } + } + + return { + players: playerAnalyses, + teams: teamAnalyses + } + } + + private _handleCalculation() { + // 重新计算战绩信息 + this._mobx.reaction( + () => Object.values(this.state.matchHistory), + (_changedV) => { + this.state.setPlayerStats(this._calcAnalysis()) + }, + { delay: 200, equals: comparer.shallow } + ) + + // 重新计算预组队 + this._mobx.reaction( + () => [Object.values(this.state.matchHistory), this.settings.premadeTeamThreshold] as const, + ([_changedV, _threshold]) => { + this.state.setPremadeTeams(this._calcTeamUp()) + }, + { delay: 200, equals: comparer.shallow } + ) + } + + private _handleAbortError(e: any) { + if (e instanceof Error && e.name === 'AbortError') { + return + } + return Promise.reject(e) + } +} diff --git a/src/main/shards/ongoing-game/state.ts b/src/main/shards/ongoing-game/state.ts index c56f9bc3..bcfbdb80 100644 --- a/src/main/shards/ongoing-game/state.ts +++ b/src/main/shards/ongoing-game/state.ts @@ -1,438 +1,444 @@ -import { EMPTY_PUUID } from '@shared/constants/common' -import { PlayerChampionMastery } from '@shared/types/lcu/champion-mastery' -import { Game, MatchHistory } from '@shared/types/lcu/match-history' -import { RankedStats } from '@shared/types/lcu/ranked' -import { SummonerInfo } from '@shared/types/lcu/summoner' -import { - MatchHistoryGamesAnalysisAll, - MatchHistoryGamesAnalysisTeamSide -} from '@shared/utils/analysis' -import { parseSelectedRole } from '@shared/utils/ranked' -import { computed, makeAutoObservable, observable } from 'mobx' - -import { LeagueClientSyncedData } from '../league-client/data' -import { SavedPlayer } from '../storage/entities/SavedPlayers' - -export class OngoingGameSettings { - enabled: boolean = true - preMadeTeamThreshold: number = 3 - matchHistoryLoadCount: number = 20 - concurrency: number = 3 - orderPlayerBy: 'win-rate' | 'kda' | 'default' | 'akari-score' = 'default' - - /** - * 查询战绩时是否优先使用 SGP API - */ - matchHistoryUseSgpApi: boolean = true - - /** - * 战绩查询时, 优先查询当前模式还是全部模式 - */ - matchHistoryQueuePreference: 'current' | 'all' = 'current' - - setEnabled(value: boolean) { - this.enabled = value - } - - setPreMadeTeamThreshold(value: number) { - this.preMadeTeamThreshold = value - } - - setMatchHistoryLoadCount(value: number) { - this.matchHistoryLoadCount = value - } - - setConcurrency(limit: number) { - this.concurrency = limit - } - - setMatchHistoryUseSgpApi(value: boolean) { - this.matchHistoryUseSgpApi = value - } - - setOrderPlayerBy(value: 'win-rate' | 'kda' | 'default' | 'akari-score') { - this.orderPlayerBy = value - } - - constructor() { - makeAutoObservable(this) - } -} - -export class OngoingGameState { - get gameInfo() { - if (!this._lcData.gameflow.session) { - return null - } - - return { - queueId: !this._lcData.gameflow.session.gameData.queue.id, - queueType: !this._lcData.gameflow.session.gameData.queue.type, - gameId: !this._lcData.gameflow.session.gameData.gameId, - gameMode: !this._lcData.gameflow.session.gameData.queue.gameMode - } - } - - /** - * 当前进行的英雄选择 - */ - get championSelections() { - if (this.queryStage.phase === 'champ-select') { - if (!this._lcData.champSelect.session) { - return null - } - - const selections: Record = {} - this._lcData.champSelect.session.myTeam.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - selections[p.puuid] = p.championId || p.championPickIntent - } - }) - - this._lcData.champSelect.session.theirTeam.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - selections[p.puuid] = p.championId || p.championPickIntent - } - }) - - return selections - } else if (this.queryStage.phase === 'in-game') { - if (!this._lcData.gameflow.session) { - return null - } - - const selections: Record = {} - this._lcData.gameflow.session.gameData.playerChampionSelections.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - selections[p.puuid] = p.championId - } - }) - - this._lcData.gameflow.session.gameData.teamOne.forEach((p) => { - if (p.championId) { - selections[p.puuid] = p.championId - } - }) - - this._lcData.gameflow.session.gameData.teamTwo.forEach((p) => { - if (p.championId) { - selections[p.puuid] = p.championId - } - }) - - return selections - } - - return null - } - - get positionAssignments() { - if (this.queryStage.phase === 'champ-select') { - if (!this._lcData.champSelect.session) { - return null - } - - const assignments: Record = {} - - this._lcData.champSelect.session.myTeam.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - assignments[p.puuid] = { - position: p.assignedPosition.toUpperCase(), - role: null - } - } - }) - - this._lcData.champSelect.session.theirTeam.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - assignments[p.puuid] = { - position: p.assignedPosition.toUpperCase(), - role: null - } - } - }) - - return assignments - } else if (this.queryStage.phase === 'in-game') { - if (!this._lcData.gameflow.session) { - return null - } - - const assignments: Record = {} - - this._lcData.gameflow.session.gameData.teamOne.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - assignments[p.puuid] = { - position: p.selectedPosition, - role: parseSelectedRole(p.selectedRole) - } - } - }) - - this._lcData.gameflow.session.gameData.teamTwo.forEach((p) => { - if (p.puuid && p.puuid !== EMPTY_PUUID) { - assignments[p.puuid] = { - position: p.selectedPosition, - role: parseSelectedRole(p.selectedRole) - } - } - }) - - return assignments - } - - return null - } - - /** - * 当前对局的队伍分配 - */ - get teams() { - if (this.queryStage.phase === 'champ-select') { - if (!this._lcData.champSelect.session) { - return null - } - - const teams: Record = {} - - this._lcData.champSelect.session.myTeam - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .forEach((p) => { - const key = p.team ? `our-${p.team}` : 'our' - if (!teams[key]) { - teams[key] = [] - } - teams[key].push(p.puuid) - }) - - this._lcData.champSelect.session.theirTeam - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .forEach((p) => { - const key = p.team ? `their-${p.team}` : 'their' - if (!teams[key]) { - teams[key] = [] - } - teams[key].push(p.puuid) - }) - - return teams - } else if (this.queryStage.phase === 'in-game') { - if (!this._lcData.gameflow.session) { - return null - } - - const teams: Record = { - 100: [], - 200: [] - } - - this._lcData.gameflow.session.gameData.teamOne - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .forEach((p) => { - teams['100'].push(p.puuid) - }) - - this._lcData.gameflow.session.gameData.teamTwo - .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) - .forEach((p) => { - teams['200'].push(p.puuid) - }) - - return teams - } - - return null - } - - /** - * 当前游戏的进行状态简化,用于区分 League Akari 的几个主要阶段 - * - * unavailable - 不需要介入的状态 - * - * champ-select - 正在英雄选择阶段 - * - * in-game - 在游戏中或游戏结算中 - */ - get queryStage() { - if ( - this._lcData.gameflow.session && - this._lcData.gameflow.session.phase === 'ChampSelect' && - this._lcData.champSelect.session - ) { - return { - phase: 'champ-select', - gameInfo: { - queueId: this._lcData.gameflow.session.gameData.queue.id, - queueType: this._lcData.gameflow.session.gameData.queue.type, - gameId: this._lcData.gameflow.session.gameData.gameId, - gameMode: this._lcData.gameflow.session.gameData.queue.gameMode - } - } - } - - if ( - this._lcData.gameflow.session && - (this._lcData.gameflow.session.phase === 'GameStart' || - this._lcData.gameflow.session.phase === 'InProgress' || - this._lcData.gameflow.session.phase === 'WaitingForStats' || - this._lcData.gameflow.session.phase === 'PreEndOfGame' || - this._lcData.gameflow.session.phase === 'EndOfGame' || - this._lcData.gameflow.session.phase === 'Reconnect') - ) { - return { - phase: 'in-game', - gameInfo: { - queueId: this._lcData.gameflow.session.gameData.queue.id, - queueType: this._lcData.gameflow.session.gameData.queue.type, - gameId: this._lcData.gameflow.session.gameData.gameId, - gameMode: this._lcData.gameflow.session.gameData.queue.gameMode - } - } - } - - return { - phase: 'unavailable', - gameInfo: null - } - } - - /** - * 在游戏结算时,League Akari 会额外进行一些操作 - */ - get isInEog() { - return ( - this._lcData.gameflow.phase === 'WaitingForStats' || - this._lcData.gameflow.phase === 'PreEndOfGame' || - this._lcData.gameflow.phase === 'EndOfGame' - ) - } - - /** - * 计算出来的预设队伍 - */ - premadeTeams: Record | null = null - - setPremadeTeams(value: Record | null) { - this.premadeTeams = value - } - - /** - * 根据目前所有战绩计算出来的玩家分析数据 - */ - playerStats: { - players: Record - teams: Record - } | null = null - - setPlayerStats( - value: { - players: Record - teams: Record - } | null - ) { - this.playerStats = value - } - - /** - * 战绩列表的 tag, 用于 SGP API - */ - matchHistoryTag: string | null - - setMatchHistoryTag(value: string | null) { - this.matchHistoryTag = value - } - - /** - * 每名玩家的战绩 - * 手动同步 - */ - matchHistory: Record< - string, - { - /** 战绩源, lc 为通过 LC 代理查询服务器, SGP 为直接查询服务器. 前者高可用 */ - source: 'lcu' | 'sgp' - - /** 适用于 SGP 的 tag string, 当设置为 lcu 时, 该选项会被忽略 */ - tag?: string - - /** 目标加载数量, 非实际数量 */ - targetCount: number - - /** 大概不用说明 */ - data: Game[] - } - > = {} - - /** - * 每名玩家的召唤师信息 - * 手动同步 - */ - summoner: Record< - string, - { - source: 'lcu' | 'sgp' - data: SummonerInfo - } - > = {} - - /** - * 每名玩家的段位 - * 手动同步 - */ - rankedStats: Record< - string, - { - source: 'lcu' | 'sgp' - data: RankedStats - } - > = {} - - /** - * 每名玩家的段位 - * 手动同步 - */ - championMastery: Record< - string, - { - source: 'lcu' | 'sgp' - data: Record< - number, - { - championId: number - championLevel: number - championPoints: number - } - > - } - > = {} - - savedInfo: Record = {} - - clear() { - this.playerStats = null - this.premadeTeams = {} - this.matchHistory = {} - this.summoner = {} - this.savedInfo = {} - this.matchHistoryTag = null - } - - constructor(private readonly _lcData: LeagueClientSyncedData) { - makeAutoObservable(this, { - // shallow object - matchHistory: observable.shallow, - summoner: observable.shallow, - rankedStats: observable.shallow, - savedInfo: observable.shallow, - - // structured data - championSelections: computed.struct, - gameInfo: computed.struct, - positionAssignments: computed.struct, - teams: computed.struct, - premadeTeams: observable.struct, - playerStats: observable.struct, - queryStage: computed.struct - }) - } -} +import { EMPTY_PUUID } from '@shared/constants/common' +import { Game } from '@shared/types/lcu/match-history' +import { RankedStats } from '@shared/types/lcu/ranked' +import { SummonerInfo } from '@shared/types/lcu/summoner' +import { + MatchHistoryGamesAnalysisAll, + MatchHistoryGamesAnalysisTeamSide +} from '@shared/utils/analysis' +import { ParsedRole, parseSelectedRole } from '@shared/utils/ranked' +import { computed, makeAutoObservable, observable } from 'mobx' + +import { LeagueClientSyncedData } from '../league-client/data' +import { SavedPlayer } from '../storage/entities/SavedPlayers' + +export class OngoingGameSettings { + enabled: boolean = true + premadeTeamThreshold: number = 3 + matchHistoryLoadCount: number = 20 + concurrency: number = 3 + + /** + * 查询战绩时是否优先使用 SGP API + */ + matchHistoryUseSgpApi: boolean = true + + /** + * 战绩查询时, 优先查询当前模式还是全部模式, 仅当 SGP API 启用时有效 + */ + matchHistoryTagPreference: 'current' | 'all' = 'current' + + setEnabled(value: boolean) { + this.enabled = value + } + + setPreMadeTeamThreshold(value: number) { + this.premadeTeamThreshold = value + } + + setMatchHistoryLoadCount(value: number) { + this.matchHistoryLoadCount = value + } + + setConcurrency(limit: number) { + this.concurrency = limit + } + + setMatchHistoryUseSgpApi(value: boolean) { + this.matchHistoryUseSgpApi = value + } + + constructor() { + makeAutoObservable(this) + } +} + +export class OngoingGameState { + get gameInfo() { + if (!this._lcData.gameflow.session) { + return null + } + + return { + queueId: !this._lcData.gameflow.session.gameData.queue.id, + queueType: !this._lcData.gameflow.session.gameData.queue.type, + gameId: !this._lcData.gameflow.session.gameData.gameId, + gameMode: !this._lcData.gameflow.session.gameData.queue.gameMode + } + } + + /** + * 当前进行的英雄选择 + */ + get championSelections() { + if (this.queryStage.phase === 'champ-select') { + if (!this._lcData.champSelect.session) { + return null + } + + const selections: Record = {} + this._lcData.champSelect.session.myTeam.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + selections[p.puuid] = p.championId || p.championPickIntent + } + }) + + this._lcData.champSelect.session.theirTeam.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + selections[p.puuid] = p.championId || p.championPickIntent + } + }) + + return selections + } else if (this.queryStage.phase === 'in-game') { + if (!this._lcData.gameflow.session) { + return null + } + + const selections: Record = {} + this._lcData.gameflow.session.gameData.playerChampionSelections.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + selections[p.puuid] = p.championId + } + }) + + this._lcData.gameflow.session.gameData.teamOne.forEach((p) => { + if (p.championId) { + selections[p.puuid] = p.championId + } + }) + + this._lcData.gameflow.session.gameData.teamTwo.forEach((p) => { + if (p.championId) { + selections[p.puuid] = p.championId + } + }) + + return selections + } + + return null + } + + get positionAssignments() { + if (this.queryStage.phase === 'champ-select') { + if (!this._lcData.champSelect.session) { + return null + } + + const assignments: Record< + string, + { + position: string + role: ParsedRole | null + } + > = {} + + this._lcData.champSelect.session.myTeam.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + assignments[p.puuid] = { + position: p.assignedPosition.toUpperCase(), + role: null + } + } + }) + + this._lcData.champSelect.session.theirTeam.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + assignments[p.puuid] = { + position: p.assignedPosition.toUpperCase(), + role: null + } + } + }) + + return assignments + } else if (this.queryStage.phase === 'in-game') { + if (!this._lcData.gameflow.session) { + return null + } + + const assignments: Record< + string, + { + position: string + role: ParsedRole | null + } + > = {} + + this._lcData.gameflow.session.gameData.teamOne.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + assignments[p.puuid] = { + position: p.selectedPosition, + role: parseSelectedRole(p.selectedRole) + } + } + }) + + this._lcData.gameflow.session.gameData.teamTwo.forEach((p) => { + if (p.puuid && p.puuid !== EMPTY_PUUID) { + assignments[p.puuid] = { + position: p.selectedPosition, + role: parseSelectedRole(p.selectedRole) + } + } + }) + + return assignments + } + + return null + } + + /** + * 当前对局的队伍分配 + */ + get teams() { + if (this.queryStage.phase === 'champ-select') { + if (!this._lcData.champSelect.session) { + return null + } + + const teams: Record = {} + + this._lcData.champSelect.session.myTeam + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .forEach((p) => { + const key = p.team ? `our-${p.team}` : 'our' + if (!teams[key]) { + teams[key] = [] + } + teams[key].push(p.puuid) + }) + + this._lcData.champSelect.session.theirTeam + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .forEach((p) => { + const key = p.team ? `their-${p.team}` : 'their' + if (!teams[key]) { + teams[key] = [] + } + teams[key].push(p.puuid) + }) + + return teams + } else if (this.queryStage.phase === 'in-game') { + if (!this._lcData.gameflow.session) { + return null + } + + const teams: Record = { + 100: [], + 200: [] + } + + this._lcData.gameflow.session.gameData.teamOne + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .forEach((p) => { + teams['100'].push(p.puuid) + }) + + this._lcData.gameflow.session.gameData.teamTwo + .filter((p) => p.puuid && p.puuid !== EMPTY_PUUID) + .forEach((p) => { + teams['200'].push(p.puuid) + }) + + return teams + } + + return null + } + + /** + * 当前游戏的进行状态简化,用于区分 League Akari 的几个主要阶段 + * + * unavailable - 不需要介入的状态 + * + * champ-select - 正在英雄选择阶段 + * + * in-game - 在游戏中或游戏结算中 + */ + get queryStage() { + if ( + this._lcData.gameflow.session && + this._lcData.gameflow.session.phase === 'ChampSelect' && + this._lcData.champSelect.session + ) { + return { + phase: 'champ-select', + gameInfo: { + queueId: this._lcData.gameflow.session.gameData.queue.id, + queueType: this._lcData.gameflow.session.gameData.queue.type, + gameId: this._lcData.gameflow.session.gameData.gameId, + gameMode: this._lcData.gameflow.session.gameData.queue.gameMode + } + } + } + + if ( + this._lcData.gameflow.session && + (this._lcData.gameflow.session.phase === 'GameStart' || + this._lcData.gameflow.session.phase === 'InProgress' || + this._lcData.gameflow.session.phase === 'WaitingForStats' || + this._lcData.gameflow.session.phase === 'PreEndOfGame' || + this._lcData.gameflow.session.phase === 'EndOfGame' || + this._lcData.gameflow.session.phase === 'Reconnect') + ) { + return { + phase: 'in-game', + gameInfo: { + queueId: this._lcData.gameflow.session.gameData.queue.id, + queueType: this._lcData.gameflow.session.gameData.queue.type, + gameId: this._lcData.gameflow.session.gameData.gameId, + gameMode: this._lcData.gameflow.session.gameData.queue.gameMode + } + } + } + + return { + phase: 'unavailable', + gameInfo: null + } + } + + /** + * 在游戏结算时,League Akari 会额外进行一些操作 + */ + get isInEog() { + return ( + this._lcData.gameflow.phase === 'WaitingForStats' || + this._lcData.gameflow.phase === 'PreEndOfGame' || + this._lcData.gameflow.phase === 'EndOfGame' + ) + } + + /** + * 计算出来的预设队伍 + */ + premadeTeams: Record | null = null + + setPremadeTeams(value: Record | null) { + this.premadeTeams = value + } + + /** + * 根据目前所有战绩计算出来的玩家分析数据 + */ + playerStats: { + players: Record + teams: Record + } | null = null + + setPlayerStats( + value: { + players: Record + teams: Record + } | null + ) { + this.playerStats = value + } + + /** + * 战绩列表的 tag, 用于 SGP API + */ + matchHistoryTag: string + + setMatchHistoryTag(value: string) { + this.matchHistoryTag = value + } + + /** + * 每名玩家的战绩 + * 手动同步 + */ + matchHistory: Record< + string, + { + /** 战绩源, lc 为通过 LC 代理查询服务器, SGP 为直接查询服务器. 前者高可用 */ + source: 'lcu' | 'sgp' + + /** 适用于 SGP 的 tag string, 当设置为 lcu 时, 该选项会被忽略 */ + tag?: string + + /** 目标加载数量, 非实际数量 */ + targetCount: number + + /** 大概不用说明 */ + data: Game[] + } + > = {} + + /** + * 每名玩家的召唤师信息 + * 手动同步 + */ + summoner: Record< + string, + { + source: 'lcu' | 'sgp' + data: SummonerInfo + } + > = {} + + /** + * 每名玩家的段位 + * 手动同步 + */ + rankedStats: Record< + string, + { + source: 'lcu' | 'sgp' + data: RankedStats + } + > = {} + + /** + * 每名玩家的段位 + * 手动同步 + */ + championMastery: Record< + string, + { + source: 'lcu' | 'sgp' + data: Record< + number, + { + championId: number + championLevel: number + championPoints: number + } + > + } + > = {} + + savedInfo: Record = {} + + clear() { + this.playerStats = null + this.premadeTeams = {} + this.matchHistory = {} + this.summoner = {} + this.savedInfo = {} + this.matchHistoryTag = 'all' + } + + constructor(private readonly _lcData: LeagueClientSyncedData) { + makeAutoObservable(this, { + // shallow object + matchHistory: observable.shallow, + summoner: observable.shallow, + rankedStats: observable.shallow, + savedInfo: observable.shallow, + + // structured data + championSelections: computed.struct, + gameInfo: computed.struct, + positionAssignments: computed.struct, + teams: computed.struct, + premadeTeams: observable.struct, + playerStats: observable.struct, + queryStage: computed.struct + }) + } +} diff --git a/src/main/shards/respawn-timer/index.ts b/src/main/shards/respawn-timer/index.ts index 7b0823a0..222fa41a 100644 --- a/src/main/shards/respawn-timer/index.ts +++ b/src/main/shards/respawn-timer/index.ts @@ -6,7 +6,7 @@ import { LeagueClientMain } from '../league-client' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' import { MobxUtilsMain } from '../mobx-utils' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { RespawnTimerSettings, RespawnTimerState } from './state' export class RespawnTimerMain implements IAkariShardInitDispose { @@ -30,7 +30,7 @@ export class RespawnTimerMain implements IAkariShardInitDispose { private readonly _leagueClient: LeagueClientMain private readonly _mobx: MobxUtilsMain private readonly _settingFactory: SettingFactoryMain - private readonly _setting: MobxSettingService + private readonly _setting: SetterSettingService private _timer: NodeJS.Timeout private _isStarted = false diff --git a/src/main/shards/saved-player/index.ts b/src/main/shards/saved-player/index.ts index d150fed8..e93e5d8b 100644 --- a/src/main/shards/saved-player/index.ts +++ b/src/main/shards/saved-player/index.ts @@ -1,242 +1,242 @@ -import { IAkariShardInitDispose } from '@shared/akari-shard/interface' -import { Equal } from 'typeorm' - -import { AkariIpcMain } from '../ipc' -import { StorageMain } from '../storage' -import { EncounteredGame } from '../storage/entities/EncounteredGame' -import { SavedPlayer } from '../storage/entities/SavedPlayers' -import { - EncounteredGameQueryDto, - EncounteredGameSaveDto, - SavedPlayerQueryDto, - SavedPlayerSaveDto, - UpdateTagDto, - WithEncounteredGamesQueryDto -} from './types' - -/** - * 记录的玩家信息查询 - */ -export class SavedPlayerMain implements IAkariShardInitDispose { - static id = 'saved-player-main' - static dependencies = ['akari-ipc-main', 'storage-main'] - - static ENCOUNTERED_GAME_QUERY_DEFAULT_PAGE_SIZE = 40 - - private readonly _ipc: AkariIpcMain - private readonly _storage: StorageMain - - constructor(deps: any) { - this._ipc = deps['akari-ipc-main'] - this._storage = deps['storage-main'] - } - - async onInit() { - this._handleIpcCall() - } - - /** - * - * @param query - * @returns - */ - async queryEncounteredGames(query: EncounteredGameQueryDto) { - const pageSize = query.pageSize || SavedPlayerMain.ENCOUNTERED_GAME_QUERY_DEFAULT_PAGE_SIZE - const page = query.page || 1 - - const take = pageSize - const skip = (page - 1) * pageSize - - const encounteredGames = await this._storage.dataSource.manager.find(EncounteredGame, { - where: { - selfPuuid: Equal(query.selfPuuid), - puuid: Equal(query.puuid), - region: Equal(query.region), - rsoPlatformId: Equal(query.rsoPlatformId), - queueType: query.queueType ? Equal(query.queueType) : undefined - }, - order: { updateAt: query.timeOrder || 'desc' }, - take, - skip - }) - - return encounteredGames - } - - async saveEncounteredGame(dto: EncounteredGameSaveDto) { - const g = new EncounteredGame() - g.gameId = dto.gameId - g.region = dto.region - g.rsoPlatformId = dto.rsoPlatformId - g.selfPuuid = dto.selfPuuid - g.puuid = dto.puuid - g.queueType = dto.queueType || '' - g.updateAt = new Date() - return this._storage.dataSource.manager.save(g) - } - - async querySavedPlayer(query: SavedPlayerQueryDto) { - if (!query.puuid || !query.selfPuuid || !query.region) { - throw new Error('puuid, selfPuuid or region cannot be empty') - } - - return this._storage.dataSource.manager.findOneBy(SavedPlayer, { - puuid: Equal(query.puuid), - selfPuuid: Equal(query.selfPuuid), - region: Equal(query.region), - rsoPlatformId: Equal(query.rsoPlatformId) - }) - } - - async querySavedPlayerWithGames(query: SavedPlayerQueryDto & WithEncounteredGamesQueryDto) { - if (!query.puuid || !query.selfPuuid || !query.region) { - throw new Error('puuid, selfPuuid or region cannot be empty') - } - - const savedPlayer = await this._storage.dataSource.manager.findOneBy(SavedPlayer, { - puuid: Equal(query.puuid), - selfPuuid: Equal(query.selfPuuid), - region: Equal(query.region), - rsoPlatformId: Equal(query.rsoPlatformId) - }) - - if (savedPlayer) { - const encounteredGames = await this.queryEncounteredGames({ - puuid: query.puuid, - selfPuuid: query.selfPuuid, - region: query.region, - rsoPlatformId: query.rsoPlatformId, - queueType: query.queueType - }) - - return { ...savedPlayer, encounteredGames } - } - - return null - } - - async deleteSavedPlayer(query: SavedPlayerQueryDto) { - if (!query.puuid || !query.selfPuuid || !query.region) { - throw new Error('puuid, selfPuuid or region cannot be empty') - } - - return this._storage.dataSource.manager.delete(SavedPlayer, query) - } - - async saveSavedPlayer(player: SavedPlayerSaveDto) { - if (!player.puuid || !player.selfPuuid || !player.region) { - throw new Error('puuid, selfPuuid or region cannot be empty') - } - - const savedPlayer = new SavedPlayer() - const date = new Date() - savedPlayer.puuid = player.puuid - - if (player.tag !== undefined) { - savedPlayer.tag = player.tag - } - - savedPlayer.selfPuuid = player.selfPuuid - savedPlayer.rsoPlatformId = player.rsoPlatformId - savedPlayer.region = player.region - savedPlayer.updateAt = date - - if (player.encountered) { - savedPlayer.lastMetAt = date - } - - return this._storage.dataSource.manager.save(savedPlayer) - } - - /** - * 查询玩家的所有标记, 包括非此账号标记的 - * 不可跨区服查询 - * @param query - */ - async getPlayerTags(query: SavedPlayerQueryDto) { - if (!query.puuid || !query.selfPuuid || !query.region) { - throw new Error('puuid, selfPuuid or region cannot be empty') - } - - const players = await this._storage.dataSource.manager.findBy(SavedPlayer, { - puuid: Equal(query.puuid), - region: Equal(query.region), - rsoPlatformId: Equal(query.rsoPlatformId) - }) - - return players - .filter((p) => p.tag) - .map((p) => { - return { - ...p, - markedBySelf: p.selfPuuid === query.selfPuuid - } - }) - } - - /** - * 更改某个玩家的 Tag, 提供标记者和被标记者的 puuid - * 提供为空则为删除标记 - */ - async updatePlayerTag(dto: UpdateTagDto) { - // 这里的 selfPuuid 是标记者的 puuid - if (!dto.puuid || !dto.selfPuuid) { - throw new Error('puuid, selfPuuid cannot be empty') - } - - const player = await this._storage.dataSource.manager.findOneBy(SavedPlayer, { - puuid: Equal(dto.puuid), - selfPuuid: Equal(dto.selfPuuid) - }) - - if (!player) { - throw new Error('player not found') - } - - player.tag = dto.tag - player.updateAt = new Date() - return this._storage.dataSource.manager.save(player) - } - - private _handleIpcCall() { - this._ipc.onCall(SavedPlayerMain.id, 'querySavedPlayer', (query: SavedPlayerQueryDto) => { - return this.querySavedPlayer(query) - }) - - this._ipc.onCall( - SavedPlayerMain.id, - 'querySavedPlayerWithGames', - (query: SavedPlayerQueryDto & WithEncounteredGamesQueryDto) => { - return this.querySavedPlayerWithGames(query) - } - ) - - this._ipc.onCall(SavedPlayerMain.id, 'saveSavedPlayer', (player: SavedPlayerSaveDto) => { - return this.saveSavedPlayer(player) - }) - - this._ipc.onCall(SavedPlayerMain.id, 'deleteSavedPlayer', (query: SavedPlayerQueryDto) => { - return this.deleteSavedPlayer(query) - }) - - this._ipc.onCall( - SavedPlayerMain.id, - 'queryEncounteredGames', - (query: EncounteredGameQueryDto) => { - return this.queryEncounteredGames(query) - } - ) - - this._ipc.onCall(SavedPlayerMain.id, 'saveEncounteredGame', (dto: EncounteredGameSaveDto) => { - return this.saveEncounteredGame(dto) - }) - - this._ipc.onCall(SavedPlayerMain.id, 'getPlayerTags', (query: SavedPlayerQueryDto) => { - return this.getPlayerTags(query) - }) - - this._ipc.onCall(SavedPlayerMain.id, 'updatePlayerTag', (dto: UpdateTagDto) => { - return this.updatePlayerTag(dto) - }) - } -} +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { Equal } from 'typeorm' + +import { AkariIpcMain } from '../ipc' +import { StorageMain } from '../storage' +import { EncounteredGame } from '../storage/entities/EncounteredGame' +import { SavedPlayer } from '../storage/entities/SavedPlayers' +import { + EncounteredGameQueryDto, + EncounteredGameSaveDto, + SavedPlayerQueryDto, + SavedPlayerSaveDto, + UpdateTagDto, + WithEncounteredGamesQueryDto +} from './types' + +/** + * 记录的玩家信息查询 + */ +export class SavedPlayerMain implements IAkariShardInitDispose { + static id = 'saved-player-main' + static dependencies = ['akari-ipc-main', 'storage-main'] + + static ENCOUNTERED_GAME_QUERY_DEFAULT_PAGE_SIZE = 40 + + private readonly _ipc: AkariIpcMain + private readonly _storage: StorageMain + + constructor(deps: any) { + this._ipc = deps['akari-ipc-main'] + this._storage = deps['storage-main'] + } + + async onInit() { + this._handleIpcCall() + } + + /** + * + * @param query + * @returns + */ + async queryEncounteredGames(query: EncounteredGameQueryDto) { + const pageSize = query.pageSize || SavedPlayerMain.ENCOUNTERED_GAME_QUERY_DEFAULT_PAGE_SIZE + const page = query.page || 1 + + const take = pageSize + const skip = (page - 1) * pageSize + + const encounteredGames = await this._storage.dataSource.manager.find(EncounteredGame, { + where: { + selfPuuid: Equal(query.selfPuuid), + puuid: Equal(query.puuid), + region: Equal(query.region), + rsoPlatformId: Equal(query.rsoPlatformId), + queueType: query.queueType ? Equal(query.queueType) : undefined + }, + order: { updateAt: query.timeOrder || 'desc' }, + take, + skip + }) + + return encounteredGames + } + + async saveEncounteredGame(dto: EncounteredGameSaveDto) { + const g = new EncounteredGame() + g.gameId = dto.gameId + g.region = dto.region + g.rsoPlatformId = dto.rsoPlatformId + g.selfPuuid = dto.selfPuuid + g.puuid = dto.puuid + g.queueType = dto.queueType || '' + g.updateAt = new Date() + return this._storage.dataSource.manager.save(g) + } + + async querySavedPlayer(query: SavedPlayerQueryDto) { + if (!query.puuid || !query.selfPuuid || !query.region) { + throw new Error('puuid, selfPuuid or region cannot be empty') + } + + return this._storage.dataSource.manager.findOneBy(SavedPlayer, { + puuid: Equal(query.puuid), + selfPuuid: Equal(query.selfPuuid), + region: Equal(query.region), + rsoPlatformId: Equal(query.rsoPlatformId) + }) + } + + async querySavedPlayerWithGames(query: SavedPlayerQueryDto & WithEncounteredGamesQueryDto) { + if (!query.puuid || !query.selfPuuid || !query.region) { + throw new Error('puuid, selfPuuid or region cannot be empty') + } + + const savedPlayer = await this._storage.dataSource.manager.findOneBy(SavedPlayer, { + puuid: Equal(query.puuid), + selfPuuid: Equal(query.selfPuuid), + region: Equal(query.region), + rsoPlatformId: Equal(query.rsoPlatformId) + }) + + if (savedPlayer) { + const encounteredGames = await this.queryEncounteredGames({ + puuid: query.puuid, + selfPuuid: query.selfPuuid, + region: query.region, + rsoPlatformId: query.rsoPlatformId, + queueType: query.queueType + }) + + return { ...savedPlayer, encounteredGames } + } + + return null + } + + async deleteSavedPlayer(query: SavedPlayerQueryDto) { + if (!query.puuid || !query.selfPuuid || !query.region) { + throw new Error('puuid, selfPuuid or region cannot be empty') + } + + return this._storage.dataSource.manager.delete(SavedPlayer, query) + } + + async saveSavedPlayer(player: SavedPlayerSaveDto) { + if (!player.puuid || !player.selfPuuid || !player.region) { + throw new Error('puuid, selfPuuid or region cannot be empty') + } + + const savedPlayer = new SavedPlayer() + const date = new Date() + savedPlayer.puuid = player.puuid + + if (player.tag !== undefined) { + savedPlayer.tag = player.tag + } + + savedPlayer.selfPuuid = player.selfPuuid + savedPlayer.rsoPlatformId = player.rsoPlatformId + savedPlayer.region = player.region + savedPlayer.updateAt = date + + if (player.encountered) { + savedPlayer.lastMetAt = date + } + + return this._storage.dataSource.manager.save(savedPlayer) + } + + /** + * 查询玩家的所有标记, 包括非此账号标记的 + * 不可跨区服查询 + * @param query + */ + async getPlayerTags(query: SavedPlayerQueryDto) { + if (!query.puuid || !query.selfPuuid || !query.region) { + throw new Error('puuid, selfPuuid or region cannot be empty') + } + + const players = await this._storage.dataSource.manager.findBy(SavedPlayer, { + puuid: Equal(query.puuid), + region: Equal(query.region), + rsoPlatformId: Equal(query.rsoPlatformId) + }) + + return players + .filter((p) => p.tag) + .map((p) => { + return { + ...p, + markedBySelf: p.selfPuuid === query.selfPuuid + } + }) + } + + /** + * 更改某个玩家的 Tag, 提供标记者和被标记者的 puuid + * 提供为空则为删除标记 + */ + async updatePlayerTag(dto: UpdateTagDto) { + // 这里的 selfPuuid 是标记者的 puuid + if (!dto.puuid || !dto.selfPuuid) { + throw new Error('puuid, selfPuuid cannot be empty') + } + + const player = await this._storage.dataSource.manager.findOneBy(SavedPlayer, { + puuid: Equal(dto.puuid), + selfPuuid: Equal(dto.selfPuuid) + }) + + if (!player) { + throw new Error('player not found') + } + + player.tag = dto.tag + player.updateAt = new Date() + return this._storage.dataSource.manager.save(player) + } + + private _handleIpcCall() { + this._ipc.onCall(SavedPlayerMain.id, 'querySavedPlayer', (query: SavedPlayerQueryDto) => { + return this.querySavedPlayer(query) + }) + + this._ipc.onCall( + SavedPlayerMain.id, + 'querySavedPlayerWithGames', + (query: SavedPlayerQueryDto & WithEncounteredGamesQueryDto) => { + return this.querySavedPlayerWithGames(query) + } + ) + + this._ipc.onCall(SavedPlayerMain.id, 'saveSavedPlayer', (player: SavedPlayerSaveDto) => { + return this.saveSavedPlayer(player) + }) + + this._ipc.onCall(SavedPlayerMain.id, 'deleteSavedPlayer', (query: SavedPlayerQueryDto) => { + return this.deleteSavedPlayer(query) + }) + + this._ipc.onCall( + SavedPlayerMain.id, + 'queryEncounteredGames', + (query: EncounteredGameQueryDto) => { + return this.queryEncounteredGames(query) + } + ) + + this._ipc.onCall(SavedPlayerMain.id, 'saveEncounteredGame', (dto: EncounteredGameSaveDto) => { + return this.saveEncounteredGame(dto) + }) + + this._ipc.onCall(SavedPlayerMain.id, 'getPlayerTags', (query: SavedPlayerQueryDto) => { + return this.getPlayerTags(query) + }) + + this._ipc.onCall(SavedPlayerMain.id, 'updatePlayerTag', (dto: UpdateTagDto) => { + return this.updatePlayerTag(dto) + }) + } +} diff --git a/src/main/shards/self-update/index.ts b/src/main/shards/self-update/index.ts index 04ea25de..59b3a906 100644 --- a/src/main/shards/self-update/index.ts +++ b/src/main/shards/self-update/index.ts @@ -6,7 +6,7 @@ import { } from '@shared/constants/common' import { FileInfo, GithubApiLatestRelease } from '@shared/types/github' import { formatError } from '@shared/utils/errors' -import axios, { AxiosResponse } from 'axios' +import axios, { AxiosResponse, isAxiosError } from 'axios' import { app, shell } from 'electron' import { comparer } from 'mobx' import { extractFull } from 'node-7z' @@ -22,7 +22,7 @@ import { AkariIpcMain } from '../ipc' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' import { MobxUtilsMain } from '../mobx-utils' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { SelfUpdateSettings, SelfUpdateState } from './state' /** @@ -59,7 +59,7 @@ export class SelfUpdateMain implements IAkariShardInitDispose { private readonly _loggerFactory: LoggerFactoryMain private readonly _settingFactory: SettingFactoryMain private readonly _log: AkariLogger - private readonly _setting: MobxSettingService + private readonly _setting: SetterSettingService private _http = axios.create({ headers: { 'User-Agent': SelfUpdateMain.USER_AGENT } diff --git a/src/main/shards/setting-factory/index.ts b/src/main/shards/setting-factory/index.ts index f1819549..9e24de3f 100644 --- a/src/main/shards/setting-factory/index.ts +++ b/src/main/shards/setting-factory/index.ts @@ -1,10 +1,13 @@ import { IAkariShardInitDispose } from '@shared/akari-shard/interface' import { Paths } from '@shared/utils/types' +import { app } from 'electron' +import fs from 'node:fs' +import path from 'node:path' import { AkariIpcMain } from '../ipc' -import { MobxUtilsMain } from '../mobx-utils' import { StorageMain } from '../storage' -import { MobxSettingService } from './mobx-setting-service' +import { Setting } from '../storage/entities/Settings' +import { SetterSettingService } from './setter-setting-service' export type OnChangeCallback = ( newValue: T, @@ -33,18 +36,16 @@ export interface SettingSchema { */ export class SettingFactoryMain implements IAkariShardInitDispose { static id = 'setting-factory-main' - static dependencies = ['storage-main', 'akari-ipc-main', 'mobx-utils-main'] + static dependencies = ['storage-main', 'akari-ipc-main'] private readonly _ipc: AkariIpcMain private readonly _storage: StorageMain - private readonly _mobx: MobxUtilsMain - private readonly _settings: Map = new Map() + private readonly _settings: Map = new Map() constructor(deps: any) { this._ipc = deps['akari-ipc-main'] this._storage = deps['storage-main'] - this._mobx = deps['mobx-utils-main'] } create( @@ -56,7 +57,7 @@ export class SettingFactoryMain implements IAkariShardInitDispose { throw new Error(`namespace ${namespace} already created`) } - const service = new MobxSettingService(this, SettingFactoryMain, namespace, schema, obj, { + const service = new SetterSettingService(this, SettingFactoryMain, namespace, schema, obj, { storage: this._storage }) @@ -64,6 +65,124 @@ export class SettingFactoryMain implements IAkariShardInitDispose { return service } + /** + * 拥有指定设置项吗? + */ + _hasKeyInStorage(namespace: string, key: string) { + const key2 = `${namespace}/${key}` + return this._storage.dataSource.manager.existsBy(Setting, { key: key2 }) + } + + /** + * 获取指定设置项的值 + * @param key + * @param defaultValue + * @returns + */ + async _getFromStorage(namespace: string, key: string): Promise + async _getFromStorage(namespace: string, key: string, defaultValue: T): Promise + async _getFromStorage(namespace: string, key: string, defaultValue?: any) { + const key2 = `${namespace}/${key}` + const v = await this._storage.dataSource.manager.findOneBy(Setting, { key: key2 }) + if (!v) { + if (defaultValue !== undefined) { + return defaultValue + } + throw new Error(`cannot find setting of key ${key}`) + } + + return v.value + } + + /** + * 设置指定设置项的值 + * @param key + * @param value + */ + async _saveToStorage(namespace: string, key: string, value: any) { + const key2 = `${namespace}/${key}` + + if (!key2 || value === undefined) { + throw new Error('key or value cannot be empty') + } + + await this._storage.dataSource.manager.save(Setting.create(key2, value)) + } + + /** + * 删除设置项, 但通常没有用过 + * @param key + */ + async _removeFromStorage(namespace: string, key: string) { + const key2 = `${namespace}/${key}` + if (!key2) { + throw new Error('key is required') + } + + await this._storage.dataSource.manager.delete(Setting, { key: key2 }) + } + + /** + * 从应用目录读取某个 JSON 文件,提供一个文件名 + */ + async readFromJsonConfigFile(namespace: string, filename: string): Promise { + if (!namespace) { + throw new Error('domain is required') + } + + const jsonPath = path.join( + app.getPath('userData'), + SetterSettingService.CONFIG_DIR_NAME, + namespace, + filename + ) + + if (!fs.existsSync(jsonPath)) { + throw new Error(`config file ${filename} does not exist`) + } + + // 读取 UTF-8 格式的 JSON 文件 + const content = await fs.promises.readFile(jsonPath, 'utf-8') + return JSON.parse(content) + } + + /** + * 将某个东西写入到 JSON 文件中,提供一个文件名 + */ + async writeToJsonConfigFile(namespace: string, filename: string, data: any) { + if (!namespace) { + throw new Error('domain is required') + } + + const jsonPath = path.join( + app.getPath('userData'), + SetterSettingService.CONFIG_DIR_NAME, + namespace, + filename + ) + + await fs.promises.mkdir(path.dirname(jsonPath), { recursive: true }) + await fs.promises.writeFile(jsonPath, JSON.stringify(data, null, 2), 'utf-8') + } + + /** + * 检查某个 json 配置文件是否存在 + */ + async jsonConfigFileExists(namespace: string, filename: string) { + if (!namespace) { + throw new Error('domain is required') + } + + const jsonPath = path.join( + app.getPath('userData'), + SetterSettingService.CONFIG_DIR_NAME, + namespace, + filename + ) + + return fs.existsSync(jsonPath) + } + async onInit() { /** * 渲染进程请求获取设置项 @@ -73,13 +192,23 @@ export class SettingFactoryMain implements IAkariShardInitDispose { 'set', async (namespace: string, key: string, newValue: any) => { const service = this._settings.get(namespace) - if (!service) { - throw new Error(`namespace ${namespace} not found`) - } - await service.set(key, newValue) + if (service) { + await service.set(key, newValue) + } else { + this._saveToStorage(namespace, key, newValue) + } } ) + + this._ipc.onCall(SettingFactoryMain.id, 'get', async (namespace: string, key: string) => { + const service = this._settings.get(namespace) + if (service) { + return service.get(key) + } + + return this._getFromStorage(namespace, key) + }) } async onDispose() { diff --git a/src/main/shards/setting-factory/mobx-setting-service.ts b/src/main/shards/setting-factory/mobx-setting-service.ts deleted file mode 100644 index 6fc05fd1..00000000 --- a/src/main/shards/setting-factory/mobx-setting-service.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { app } from 'electron' -import _ from 'lodash' -import { runInAction } from 'mobx' -import { existsSync, promises } from 'node:fs' -import { dirname, join } from 'path' - -import { OnChangeCallback, SettingFactoryMain } from '.' -import { StorageMain } from '../storage' -import { Setting } from '../storage/entities/Settings' - -/** - * 结合 mobx 状态同步的设置项服务 - * 耦合了状态和设置项读写的功能, 顺便还能读写 JSON 文件 - */ -export class MobxSettingService { - static CONFIG_DIR_NAME = 'AkariConfig' - - private readonly _storage: StorageMain - - constructor( - private readonly _storageFactory: SettingFactoryMain, - private readonly _C: typeof SettingFactoryMain, - private readonly _namespace: string, - // for accessibility - public readonly _schema: Record, - public readonly _obj: object, - _deps: any - ) { - this._storage = _deps.storage - } - - /** - * 拥有指定设置项吗? - */ - _hasKeyInStorage(key: string) { - const key2 = `${this._namespace}/${key}` - return this._storage.dataSource.manager.existsBy(Setting, { key: key2 }) - } - - /** - * 获取指定设置项的值 - * @param key - * @param defaultValue - * @returns - */ - async _getFromStorage(key: string): Promise - async _getFromStorage(key: string, defaultValue: T): Promise - async _getFromStorage(key: string, defaultValue?: any) { - const key2 = `${this._namespace}/${key}` - const v = await this._storage.dataSource.manager.findOneBy(Setting, { key: key2 }) - if (!v) { - if (defaultValue !== undefined) { - return defaultValue - } - throw new Error(`cannot find setting of key ${key}`) - } - - return v.value - } - - /** - * 设置指定设置项的值 - * @param key - * @param value - */ - async _saveToStorage(key: string, value: any) { - const key2 = `${this._namespace}/${key}` - - if (!key2 || value === undefined) { - throw new Error('key or value cannot be empty') - } - - await this._storage.dataSource.manager.save(Setting.create(key2, value)) - } - - /** - * 删除设置项, 但通常没有用过 - * @param key - */ - async _removeFromStorage(key: string) { - const key2 = `${this._namespace}/${key}` - if (!key2) { - throw new Error('key is required') - } - - await this._storage.dataSource.manager.delete(Setting, { key: key2 }) - } - - /** - * 获取所有设置项 - */ - async _getAllFromStorage() { - const items: Record = {} - const jobs = Object.entries(this._schema).map(async ([key, schema]) => { - const value = await this._getFromStorage(key as any, schema.default) - items[key] = value - }) - await Promise.all(jobs) - return items - } - - /** - * 获取设置项, 并存储到这个 mobx 对象中 - * @param obj Mobx Observable - * @returns 所有设置项 - */ - async applyToState() { - const items = await this._getAllFromStorage() - Object.entries(items).forEach(([key, value]) => { - _.set(this._obj, key, value) - }) - return items - } - - /** - * 从应用目录读取某个 JSON 文件,提供一个文件名 - */ - async readFromJsonConfigFile(filename: string): Promise { - if (!this._namespace) { - throw new Error('domain is required') - } - - const jsonPath = join( - app.getPath('userData'), - MobxSettingService.CONFIG_DIR_NAME, - this._namespace, - filename - ) - - if (!existsSync(jsonPath)) { - throw new Error(`config file ${filename} does not exist`) - } - - // 读取 UTF-8 格式的 JSON 文件 - const content = await promises.readFile(jsonPath, 'utf-8') - return JSON.parse(content) - } - - /** - * 将某个东西写入到 JSON 文件中,提供一个文件名 - */ - async writeToJsonConfigFile(filename: string, data: any) { - if (!this._namespace) { - throw new Error('domain is required') - } - - const jsonPath = join( - app.getPath('userData'), - MobxSettingService.CONFIG_DIR_NAME, - this._namespace, - filename - ) - - await promises.mkdir(dirname(jsonPath), { recursive: true }) - await promises.writeFile(jsonPath, JSON.stringify(data, null, 2), 'utf-8') - } - - /** - * 检查某个 json 配置文件是否存在 - */ - async jsonConfigFileExists(filename: string) { - if (!this._namespace) { - throw new Error('domain is required') - } - - const jsonPath = join( - app.getPath('userData'), - MobxSettingService.CONFIG_DIR_NAME, - this._namespace, - filename - ) - - return existsSync(jsonPath) - } - - /** - * 当某个设置项发生变化时, 拦截此行为 - * @param newValue - * @param extra - */ - onChange(key: string, fn: OnChangeCallback) { - const _fn = this._schema[key].onChange - // 重复设置, 会报错 - if (_fn) { - throw new Error(`onChange for key ${key} already set`) - } - - this._schema[key].onChange = fn - } - - /** - * 设置设置项的新值, 并更新状态 - * @param key - * @param newValue - */ - async set(key: string, newValue: any) { - const fn = this._schema[key]?.onChange - - if (fn) { - const oldValue = this._obj[key as any] - await fn(newValue, { - oldValue, - key, - setter: async (v?: any) => { - if (v === undefined) { - runInAction(() => _.set(this._obj, key, newValue)) - await this._saveToStorage(key as any, v) - } else { - runInAction(() => _.set(this._obj, key, v)) - await this._saveToStorage(key as any, newValue) - } - } - }) - } else { - runInAction(() => _.set(this._obj, key, newValue)) - await this._saveToStorage(key, newValue) - } - } - - /** - * placeholder - * @param key - */ - remove(key: string): never { - console.error(`Deemo will finally find his ${key}, not Celia but Alice`) - throw new Error('not implemented') - } -} diff --git a/src/main/shards/setting-factory/setter-setting-service.ts b/src/main/shards/setting-factory/setter-setting-service.ts new file mode 100644 index 00000000..e69b52e1 --- /dev/null +++ b/src/main/shards/setting-factory/setter-setting-service.ts @@ -0,0 +1,125 @@ +import _ from 'lodash' +import { runInAction } from 'mobx' + +import { OnChangeCallback, SettingFactoryMain } from '.' + +/** + * 在更新设置时同时更改状态, 状态同步的设置项服务 + * 耦合了状态和设置项读写的功能, 顺便还能读写 JSON 文件 + */ +export class SetterSettingService { + static CONFIG_DIR_NAME = 'AkariConfig' + + constructor( + private readonly _ins: SettingFactoryMain, + private readonly _C: typeof SettingFactoryMain, + private readonly _namespace: string, + // for accessibility + public readonly _schema: Record, + public readonly _obj: object, + _deps: any + ) {} + + _getFromStorage(key: string, defaultValue: any) { + return this._ins._getFromStorage(this._namespace, key, defaultValue) + } + + _saveToStorage(key: string, value: any) { + return this._ins._saveToStorage(this._namespace, key, value) + } + + /** + * 获取所有设置项 + */ + async _getAllFromStorage() { + const items: Record = {} + const jobs = Object.entries(this._schema).map(async ([key, schema]) => { + const value = await this._ins._getFromStorage(this._namespace, key as any, schema.default) + items[key] = value + }) + await Promise.all(jobs) + return items + } + + /** + * 获取设置项, 并存储到这个 mobx 对象中 + * @param obj Mobx Observable + * @returns 所有设置项 + */ + async applyToState() { + const items = await this._getAllFromStorage() + Object.entries(items).forEach(([key, value]) => { + _.set(this._obj, key, value) + }) + return items + } + + async readFromJsonConfigFile(filename: string): Promise { + return this._ins.readFromJsonConfigFile(this._namespace, filename) + } + + async writeToJsonConfigFile(filename: string, data: any) { + return this._ins.writeToJsonConfigFile(this._namespace, filename, data) + } + + async jsonConfigFileExists(filename: string) { + return this._ins.jsonConfigFileExists(this._namespace, filename) + } + + /** + * 当某个设置项发生变化时, 拦截此行为 + * @param newValue + * @param extra + */ + onChange(key: string, fn: OnChangeCallback) { + const _fn = this._schema[key].onChange + // 重复设置, 会报错 + if (_fn) { + throw new Error(`onChange for key ${key} already set`) + } + + this._schema[key].onChange = fn + } + + /** + * 设置设置项的新值, 并更新状态 + * @param key + * @param newValue + */ + async set(key: string, newValue: any) { + const fn = this._schema[key]?.onChange + + if (fn) { + const oldValue = this._obj[key as any] + await fn(newValue, { + oldValue, + key, + setter: async (v?: any) => { + if (v === undefined) { + runInAction(() => _.set(this._obj, key, newValue)) + await this._ins._saveToStorage(this._namespace, key as any, v) + } else { + runInAction(() => _.set(this._obj, key, v)) + await this._ins._saveToStorage(this._namespace, key as any, newValue) + } + } + }) + } else { + runInAction(() => _.set(this._obj, key, newValue)) + await this._ins._saveToStorage(this._namespace, key, newValue) + } + } + + async get(key: string) { + return _.get(this._obj, key) + } + + /** + * placeholder + * @param key + */ + remove(key: string): never { + console.error(`Deemo will finally find his ${key}, not Celia but Alice`) + throw new Error('not implemented') + } +} diff --git a/src/main/shards/sgp/index.ts b/src/main/shards/sgp/index.ts index 14236aa9..b979497a 100644 --- a/src/main/shards/sgp/index.ts +++ b/src/main/shards/sgp/index.ts @@ -13,7 +13,7 @@ import { LeagueClientMain } from '../league-client' import { AkariLogger, LoggerFactoryMain } from '../logger-factory' import { MobxUtilsMain } from '../mobx-utils' import { SettingFactoryMain } from '../setting-factory' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { SgpState } from './state' /** @@ -90,7 +90,7 @@ export class SgpMain implements IAkariShardInitDispose { private _loggerFactory: LoggerFactoryMain private _settingFactory: SettingFactoryMain private _log: AkariLogger - private _setting: MobxSettingService + private _setting: SetterSettingService private _mobx: MobxUtilsMain private _lc: LeagueClientMain private _ipc: AkariIpcMain diff --git a/src/main/shards/window-manager/index.ts b/src/main/shards/window-manager/index.ts index f054cdb3..3f25e54b 100644 --- a/src/main/shards/window-manager/index.ts +++ b/src/main/shards/window-manager/index.ts @@ -10,7 +10,7 @@ import { AkariIpcMain } from '../ipc' import { LeagueClientMain } from '../league-client' import { AkariLogger } from '../logger-factory' import { MobxUtilsMain } from '../mobx-utils' -import { MobxSettingService } from '../setting-factory/mobx-setting-service' +import { SetterSettingService } from '../setting-factory/setter-setting-service' import { WindowManagerSettings, WindowManagerState } from './state' export class WindowManagerMain implements IAkariShardInitDispose { @@ -42,10 +42,14 @@ export class WindowManagerMain implements IAkariShardInitDispose { private readonly _ipc: AkariIpcMain private readonly _mobx: MobxUtilsMain private readonly _log: AkariLogger - private readonly _setting: MobxSettingService + private readonly _setting: SetterSettingService private readonly _lc: LeagueClientMain - private _willClose = false + /** + * 标记位, 用于判断是否是即将退出应用程序 (需要全部窗口关闭) + */ + private _willQuit = false + private _nextCloseAction: string | null = null private _mw: BrowserWindow | null = null @@ -78,20 +82,21 @@ export class WindowManagerMain implements IAkariShardInitDispose { this._mobx.propSync(WindowManagerMain.id, 'state', this.state, [ 'mainWindowFocus', - 'mainWindowReady', + // 'mainWindowReady', 'mainWindowShow', - 'mainWindowSize', + // 'mainWindowSize', 'mainWindowStatus', 'auxWindowFocus', 'auxWindowReady', - 'auxWindowBounds', + // 'auxWindowBounds', 'auxWindowFunctionality', - 'auxWindowStatus', - 'auxWindowFunctionalityBounds' + 'auxWindowStatus' + // 'auxWindowFunctionalityBounds' ]) this._mobx.propSync(WindowManagerMain.id, 'settings', this.settings, [ 'mainWindowCloseAction', + 'auxWindowEnabled', 'auxWindowAutoShow', 'auxWindowOpacity', 'auxWindowPinned', @@ -103,6 +108,7 @@ export class WindowManagerMain implements IAkariShardInitDispose { this._handleAuxWindowIpcCall() this._handleAuxWindowObservations() + // TODO DEBUG REMOVE THIS this.createMainWindow() } @@ -233,7 +239,7 @@ export class WindowManagerMain implements IAkariShardInitDispose { } showOrRestoreAuxWindow(inactive = false) { - if (this._aw) { + if (this._aw && this.state.auxWindowReady) { if (!this.state.auxWindowShow) { if (inactive) { this._aw.showInactive() @@ -319,7 +325,7 @@ export class WindowManagerMain implements IAkariShardInitDispose { * @returns */ private _handleCloseMainWindow(event: Event) { - if (this._willClose) { + if (this._willQuit) { this._aw?.close() return } @@ -339,7 +345,7 @@ export class WindowManagerMain implements IAkariShardInitDispose { this._ipc.sendEvent(WindowManagerMain.id, 'main-window-close-asking') this.showOrRestoreMainWindow() } else { - this._willClose = true + this._willQuit = true this._mw?.close() this._log.info('主窗口将关闭') } @@ -586,13 +592,12 @@ export class WindowManagerMain implements IAkariShardInitDispose { this._aw.on('page-title-updated', (e) => e.preventDefault()) this._aw.on('close', (e) => { - // TODO - // if (this._isForceClose) { - // this._isForceClose = false - // return - // } - // e.preventDefault() - // this.hideWindow() + if (this._willQuit) { + return + } + + e.preventDefault() + this.hideAuxWindow() }) if (is.dev && process.env['ELECTRON_RENDERER_URL']) { @@ -791,7 +796,7 @@ export class WindowManagerMain implements IAkariShardInitDispose { } showOrRestoreMainWindow(inactive = false) { - if (this._mw) { + if (this._mw && this.state.mainWindowReady) { if (!this.state.mainWindowShow) { if (inactive) { this._mw.showInactive() diff --git a/src/renderer-shared/shards/app-common/index.ts b/src/renderer-shared/shards/app-common/index.ts new file mode 100644 index 00000000..abf41223 --- /dev/null +++ b/src/renderer-shared/shards/app-common/index.ts @@ -0,0 +1,59 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useAppCommonStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'app-common-main' + +export class AppCommonRenderer implements IAkariShardInitDispose { + static id = 'app-common-renderer' + static dependencies = [ + 'akari-ipc-renderer', + 'setting-utils-renderer', + 'pinia-mobx-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + onSecondInstance(fn: (commandLine: string[], workingDirectory: string) => void) { + return this._ipc.onEventVue(MAIN_SHARD_NAMESPACE, 'second-instance', fn) + } + + getVersion() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'getVersion') as Promise + } + + openUserDataDir() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'openUserDataDir') + } + + setInKyokoMode(s: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'isInKyokoMode', s) + } + + setShowFreeSoftwareDeclaration(s: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'showFreeSoftwareDeclaration', s) + } + + setDisableHardwareAcceleration(s: boolean) { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'setDisableHardwareAcceleration', s) + } + + async onInit() { + const store = useAppCommonStore() + store.version = await this.getVersion() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } +} diff --git a/src/renderer-shared/shards/app-common/store.ts b/src/renderer-shared/shards/app-common/store.ts new file mode 100644 index 00000000..427a6ef8 --- /dev/null +++ b/src/renderer-shared/shards/app-common/store.ts @@ -0,0 +1,26 @@ +import { defineStore } from 'pinia' +import { ref, shallowReactive, shallowRef } from 'vue' + +interface BaseConfig { + disableHardwareAcceleration?: boolean +} + +export const useAppCommonStore = defineStore('shard:app-common-renderer', () => { + const settings = shallowReactive({ + showFreeSoftwareDeclaration: false, + isInKyokoMode: false + }) + + const version = ref('0.0.0') + const isAdministrator = ref(false) + const disableHardwareAcceleration = ref(false) + const baseConfig = shallowRef(null) + + return { + settings, + isAdministrator, + disableHardwareAcceleration, + version, + baseConfig + } +}) diff --git a/src/renderer-shared/shards/auto-gameflow/index.ts b/src/renderer-shared/shards/auto-gameflow/index.ts new file mode 100644 index 00000000..7bd9e728 --- /dev/null +++ b/src/renderer-shared/shards/auto-gameflow/index.ts @@ -0,0 +1,110 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useAutoGameflowStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'auto-gameflow-main' + +export class AutoGameflowRenderer implements IAkariShardInitDispose { + static id = 'auto-gameflow-renderer' + static dependencies = [ + 'akari-ipc-renderer', + 'setting-utils-renderer', + 'pinia-mobx-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + cancelAutoAccept() { + this._ipc.call(MAIN_SHARD_NAMESPACE, 'cancelAutoAccept') + } + + cancelAutoMatchmaking() { + this._ipc.call(MAIN_SHARD_NAMESPACE, 'cancelAutoMatchmaking') + } + + setWillDodgeAtLastSecond(enabled: number) { + this._ipc.call(MAIN_SHARD_NAMESPACE, 'setWillDodgeAtLastSecond', enabled) + } + + setAutoHonorEnabled(enabled: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoHonorEnabled', enabled) + } + + setAutoHonorStrategy(strategy: string) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoHonorStrategy', strategy) + } + + setPlayAgainEnabled(enabled: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'playAgainEnabled', enabled) + } + + setAutoAcceptEnabled(enabled: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoAcceptEnabled', enabled) + } + + setAutoAcceptDelaySeconds(seconds: number) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoAcceptDelaySeconds', seconds) + } + + setAutoReconnectEnabled(enabled: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoReconnectEnabled', enabled) + } + + setAutoMatchmakingEnabled(enabled: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingEnabled', enabled) + } + + setAutoMatchmakingMaximumMatchDuration(seconds: number) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingMaximumMatchDuration', seconds) + } + + setAutoMatchmakingDelaySeconds(seconds: number) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingDelaySeconds', seconds) + } + + setAutoMatchmakingMinimumMembers(count: number) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingMinimumMembers', count) + } + + setAutoMatchmakingWaitForInvitees(yes: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingWaitForInvitees', yes) + } + + setAutoMatchmakingRematchStrategy(s: string) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingRematchStrategy', s) + } + + setAutoMatchmakingRematchFixedDuration(seconds: number) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoMatchmakingRematchFixedDuration', seconds) + } + + setAutoHandleInvitationsEnabled(enabled: boolean) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'autoHandleInvitationsEnabled', enabled) + } + + setDodgeAtLastSecondThreshold(threshold: number) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'dodgeAtLastSecondThreshold', threshold) + } + + setInvitationHandlingStrategies(strategies: Record) { + this._setting.set(MAIN_SHARD_NAMESPACE, 'invitationHandlingStrategies', strategies) + } + + async onInit() { + const store = useAutoGameflowStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } +} diff --git a/src/renderer-shared/shards/auto-gameflow/store.ts b/src/renderer-shared/shards/auto-gameflow/store.ts new file mode 100644 index 00000000..cb0ec1bc --- /dev/null +++ b/src/renderer-shared/shards/auto-gameflow/store.ts @@ -0,0 +1,38 @@ +import { defineStore } from 'pinia' +import { shallowReactive, shallowRef } from 'vue' + +// copied from main shard +export type AutoHonorStrategy = + | 'prefer-lobby-member' + | 'only-lobby-member' + | 'all-member' + | 'opt-out' + | 'all-member-including-opponent' + +// copied from main shard +export type AutoMatchmakingStrategy = 'never' | 'fixed-duration' | 'estimated-duration' + +export const useAutoGameflowStore = defineStore('shard:auto-gameflow-renderer', () => { + const settings = shallowReactive({ + autoHonorEnabled: false, + autoHonorStrategy: 'prefer-lobby-member' as AutoHonorStrategy, + playAgainEnabled: false, + autoAcceptEnabled: false, + autoAcceptDelaySeconds: 0, + autoReconnectEnabled: false, + autoMatchmakingEnabled: false, + autoMatchmakingMaximumMatchDuration: 0, + autoMatchmakingRematchStrategy: 'never' as AutoMatchmakingStrategy, + autoMatchmakingRematchFixedDuration: 2, + autoMatchmakingDelaySeconds: 5, + autoMatchmakingMinimumMembers: 1, + autoMatchmakingWaitForInvitees: true, + autoHandleInvitationsEnabled: false, + invitationHandlingStrategies: {} as Record, + dodgeAtLastSecondThreshold: 2 + }) + + return { + settings + } +}) diff --git a/src/renderer-shared/shards/auto-reply/index.ts b/src/renderer-shared/shards/auto-reply/index.ts new file mode 100644 index 00000000..2290e973 --- /dev/null +++ b/src/renderer-shared/shards/auto-reply/index.ts @@ -0,0 +1,45 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useAutoReplyStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'auto-reply-main' + +export class AutoReplyRenderer implements IAkariShardInitDispose { + static id = 'auto-reply-renderer' + static dependencies = [ + 'akari-ipc-renderer', + 'setting-utils-renderer', + 'pinia-mobx-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + setEnabled(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'enabled', enabled) + } + + setText(text: string) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'text', text) + } + + setEnableOnAway(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'enableOnAway', enabled) + } + + async onInit() { + const store = useAutoReplyStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } +} diff --git a/src/renderer-shared/shards/auto-reply/store.ts b/src/renderer-shared/shards/auto-reply/store.ts new file mode 100644 index 00000000..86e62839 --- /dev/null +++ b/src/renderer-shared/shards/auto-reply/store.ts @@ -0,0 +1,14 @@ +import { defineStore } from 'pinia' +import { ref, shallowReactive, shallowRef } from 'vue' + +export const useAutoReplyStore = defineStore('shard:auto-reply-renderer', () => { + const settings = shallowReactive({ + enabled: false, + text: '', + enableOnAway: false + }) + + return { + settings + } +}) diff --git a/src/renderer-shared/shards/auto-select/index.ts b/src/renderer-shared/shards/auto-select/index.ts new file mode 100644 index 00000000..ed4dfc7d --- /dev/null +++ b/src/renderer-shared/shards/auto-select/index.ts @@ -0,0 +1,75 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useAutoSelectStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'auto-select-main' + +export class AutoSelectRenderer implements IAkariShardInitDispose { + static id = 'auto-select-renderer' + static dependencies = ['setting-utils-renderer', 'pinia-mobx-utils-renderer'] + + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + setNormalModeEnabled(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'normalModeEnabled', enabled) + } + + setExpectedChampions(expectedChampions: Record) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'expectedChampions', expectedChampions) + } + + setSelectTeammateIntendedChampion(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'selectTeammateIntendedChampion', enabled) + } + + setShowIntent(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'showIntent', enabled) + } + + setCompleted(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'completed', enabled) + } + + setBenchModeEnabled(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'benchModeEnabled', enabled) + } + + setBenchExpectedChampions(expectedChampions: number[]) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'benchExpectedChampions', expectedChampions) + } + + setGrabDelaySeconds(seconds: number) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'grabDelaySeconds', seconds) + } + + setBenchSelectFirstAvailableChampion(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'benchSelectFirstAvailableChampion', enabled) + } + + setBanEnabled(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'banEnabled', enabled) + } + + setBannedChampions(bannedChampions: Record) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'bannedChampions', bannedChampions) + } + + setBanTeammateIntendedChampion(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'banTeammateIntendedChampion', enabled) + } + + async onInit() { + const store = useAutoSelectStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } +} diff --git a/src/renderer-shared/shards/auto-select/store.ts b/src/renderer-shared/shards/auto-select/store.ts new file mode 100644 index 00000000..2be97702 --- /dev/null +++ b/src/renderer-shared/shards/auto-select/store.ts @@ -0,0 +1,59 @@ +import { ChampSelectTeam } from '@shared/types/lcu/champ-select' +import { defineStore } from 'pinia' +import { shallowReactive, shallowRef } from 'vue' + +// copied from main shard +interface UpcomingBanPick { + championId: number + isActingNow: boolean + action: { + id: number + isInProgress: boolean + completed: boolean + } +} + +export const useAutoSelectStore = defineStore('shard:auto-select-renderer', () => { + const settings = shallowReactive({ + normalModeEnabled: false, + expectedChampions: { + top: [], + jungle: [], + middle: [], + bottom: [], + utility: [], + default: [] + }, + selectTeammateIntendedChampion: false, + showIntent: false, + completed: false, + benchModeEnabled: false, + benchSelectFirstAvailableChampion: false, + benchExpectedChampions: [], + grabDelaySeconds: 1, + banEnabled: false, + bannedChampions: { + top: [], + jungle: [], + middle: [], + bottom: [], + utility: [], + default: [] + }, + banTeammateIntendedChampion: false + }) + + const upcomingPick = shallowRef(null) + const upcomingBan = shallowRef(null) + const upcomingGrab = shallowRef<{ championId: number; willGrabAt: number } | null>(null) + const memberMe = shallowRef(null) + + return { + settings, + + upcomingPick, + upcomingBan, + upcomingGrab, + memberMe + } +}) diff --git a/src/renderer-shared/shards/ipc/index.ts b/src/renderer-shared/shards/ipc/index.ts index 050b0378..d64a00a3 100644 --- a/src/renderer-shared/shards/ipc/index.ts +++ b/src/renderer-shared/shards/ipc/index.ts @@ -1,6 +1,10 @@ import { ElectronAPI } from '@electron-toolkit/preload' import { IAkariShardInitDispose } from '@shared/akari-shard/interface' -import { IpcRenderer, IpcRendererEvent } from 'electron' +import { AkariSharedGlobalShard, SHARED_GLOBAL_ID } from '@shared/akari-shard/manager' +import { IpcRendererEvent } from 'electron' +import { getCurrentScope, onScopeDispose } from 'vue' + +import type { LoggerRenderer } from '../logger' declare global { interface Window { @@ -8,8 +12,25 @@ declare global { } } +export interface IpcMainSuccessDataType { + success: true + data: T +} + +export interface IpcMainErrorDataType { + success: false + error: any +} + +const LOGGER_SHARD_NAMESPACE = 'logger-renderer' + +export type IpcMainDataType = IpcMainSuccessDataType | IpcMainErrorDataType + export class AkariIpcRenderer implements IAkariShardInitDispose { static id = 'akari-ipc-renderer' + static dependencies = [SHARED_GLOBAL_ID] + + private readonly _shared: AkariSharedGlobalShard private _eventMap = new Map>() private _cancelFn: (() => void) | null = null @@ -49,8 +70,20 @@ export class AkariIpcRenderer implements IAkariShardInitDispose { * @param args * @returns */ - call(namespace: string, fnName: string, ...args: any[]) { - return window.electron.ipcRenderer.invoke('akariCall', namespace, fnName, ...args) as Promise + async call(namespace: string, fnName: string, ...args: any[]) { + const result: IpcMainDataType = await window.electron.ipcRenderer.invoke( + 'akariCall', + namespace, + fnName, + ...args + ) + if (result.success) { + return result.data as T + } else { + const logger = this._shared.manager.getInstance(LOGGER_SHARD_NAMESPACE) + logger?.error(namespace, result.error) + throw new Error('IPC Error', { cause: result.error }) + } } /** @@ -74,6 +107,15 @@ export class AkariIpcRenderer implements IAkariShardInitDispose { } } + /** + * Vue 可自行解除订阅的事件 + */ + onEventVue(namespace: string, eventName: string, fn: (...args: any[]) => void) { + const disposeFn = this.onEvent(namespace, eventName, fn) + getCurrentScope() && onScopeDispose(() => disposeFn()) + return disposeFn + } + /** * 取消订阅一个事件 * @param namespace @@ -89,7 +131,8 @@ export class AkariIpcRenderer implements IAkariShardInitDispose { } } - constructor() { + constructor(deps: any) { + this._shared = deps[SHARED_GLOBAL_ID] this._dispatchEvent = this._dispatchEvent.bind(this) } } diff --git a/src/renderer-shared/shards/keyboard-shortcut/index.ts b/src/renderer-shared/shards/keyboard-shortcut/index.ts new file mode 100644 index 00000000..d3660f7d --- /dev/null +++ b/src/renderer-shared/shards/keyboard-shortcut/index.ts @@ -0,0 +1,42 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcRenderer } from '../ipc' + +const MAIN_SHARD_NAMESPACE = 'keyboard-shortcuts-main' + +// copied from main shard +interface ShortcutDetails { + keyCodes: number[] + keys: { + _nameRaw: string + name: string + standardName: string + readableName: string + }[] + id: string + unifiedId: string +} + +/** + * 连接到主进程的快捷键服务 + */ +export class KeyboardShortcutsRenderer implements IAkariShardInitDispose { + static id = 'keyboard-shortcuts-renderer' + static dependencies = ['akari-ipc-renderer'] + + private readonly _ipc: AkariIpcRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + } + + onShortcut(fn: (event: ShortcutDetails) => void) { + return this._ipc.onEventVue(MAIN_SHARD_NAMESPACE, 'shortcut', fn) + } + + onLastActiveShortcut(fn: (event: ShortcutDetails) => void) { + return this._ipc.onEventVue(MAIN_SHARD_NAMESPACE, 'last-active-shortcut', fn) + } + + async onInit() {} +} diff --git a/src/renderer-shared/shards/league-client-ux/index.ts b/src/renderer-shared/shards/league-client-ux/index.ts new file mode 100644 index 00000000..0dd7787b --- /dev/null +++ b/src/renderer-shared/shards/league-client-ux/index.ts @@ -0,0 +1,37 @@ +import { AkariIpcRenderer } from '../ipc' +import { LoggerRenderer } from '../logger' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useLeagueClientUxStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'league-client-ux-main' + +export class LeagueClientUxRenderer { + static id = 'league-client-ux-renderer' + static dependencies = [ + 'akari-ipc-renderer', + 'pinia-mobx-utils-renderer', + 'setting-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + setUseWmic(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'useWmic', enabled) + } + + async onInit() { + const store = useLeagueClientUxStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } +} diff --git a/src/renderer-shared/shards/league-client-ux/store.ts b/src/renderer-shared/shards/league-client-ux/store.ts new file mode 100644 index 00000000..820cb433 --- /dev/null +++ b/src/renderer-shared/shards/league-client-ux/store.ts @@ -0,0 +1,16 @@ +import { defineStore } from 'pinia' +import { shallowReactive, shallowRef } from 'vue' +import { UxCommandLine } from '../league-client/store' + +export const useLeagueClientUxStore = defineStore('shard:league-client-ux', () => { + const settings = shallowReactive({ + useWmic: false + }) + + const launchedClients = shallowRef([]) + + return { + settings, + launchedClients + } +}) diff --git a/src/renderer-shared/shards/league-client/index.ts b/src/renderer-shared/shards/league-client/index.ts index fc01ba1c..1ef26051 100644 --- a/src/renderer-shared/shards/league-client/index.ts +++ b/src/renderer-shared/shards/league-client/index.ts @@ -1,52 +1,52 @@ -import { LeagueClientHttpApiAxiosHelper } from '@shared/http-api-axios-helper/league-client' -import axios from 'axios' - -import { AkariIpcRenderer } from '../ipc' -import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' -import { SettingUtilsRenderer } from '../setting' -import { useLeagueClientStore } from './store' - -export const MAIN_SHARD_NAMESPACE = 'league-client-main' - -export class LeagueClientRenderer { - static id = 'league-client-renderer' - static dependencies = [ - 'pinia-mobx-utils-renderer', - 'akari-ipc-renderer', - 'setting-utils-renderer' - ] - - private readonly _ipc: AkariIpcRenderer - private readonly _pm: PiniaMobxUtilsRenderer - private readonly _setting: SettingUtilsRenderer - - public readonly api = new LeagueClientHttpApiAxiosHelper( - axios.create({ baseURL: 'akari://league-client' }) - ) - - async onInit() { - const store = useLeagueClientStore() - - this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'gameData', store.gameData) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'honor', store.honor) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'champSelect', store.champSelect) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'chat', store.chat) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'matchmaking', store.matchmaking) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'gameflow', store.gameflow) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'lobby', store.lobby) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'login', store.login) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'summoner', store.summoner) - this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) - } - - constructor(deps: any) { - this._pm = deps['pinia-mobx-utils-renderer'] - this._ipc = deps['akari-ipc-renderer'] - this._setting = deps['setting-utils-renderer'] - } - - setAutoConnect(enabled: boolean) { - return this._setting.set(MAIN_SHARD_NAMESPACE, 'autoConnect', enabled) - } -} +import { LeagueClientHttpApiAxiosHelper } from '@shared/http-api-axios-helper/league-client' +import axios from 'axios' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useLeagueClientStore } from './store' + +export const MAIN_SHARD_NAMESPACE = 'league-client-main' + +export class LeagueClientRenderer { + static id = 'league-client-renderer' + static dependencies = [ + 'pinia-mobx-utils-renderer', + 'akari-ipc-renderer', + 'setting-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + public readonly api = new LeagueClientHttpApiAxiosHelper( + axios.create({ baseURL: 'akari://league-client' }) + ) + + async onInit() { + const store = useLeagueClientStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'gameData', store.gameData) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'honor', store.honor) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'champSelect', store.champSelect) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'chat', store.chat) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'matchmaking', store.matchmaking) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'gameflow', store.gameflow) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'lobby', store.lobby) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'login', store.login) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'summoner', store.summoner) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } + + constructor(deps: any) { + this._pm = deps['pinia-mobx-utils-renderer'] + this._ipc = deps['akari-ipc-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + setAutoConnect(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'autoConnect', enabled) + } +} diff --git a/src/renderer-shared/shards/league-client/store.ts b/src/renderer-shared/shards/league-client/store.ts index 1ca1d9d9..1e39f596 100644 --- a/src/renderer-shared/shards/league-client/store.ts +++ b/src/renderer-shared/shards/league-client/store.ts @@ -1,121 +1,121 @@ -import { ChampSelectSession } from '@shared/types/lcu/champ-select' -import { ChatPerson, Conversation } from '@shared/types/lcu/chat' -import { - Augment, - ChampionSimple, - Item, - Perk, - Perkstyles, - Queue, - SummonerSpell -} from '@shared/types/lcu/game-data' -import { GameflowPhase, GameflowSession } from '@shared/types/lcu/gameflow' -import { Ballot } from '@shared/types/lcu/honorV2' -import { Lobby, ReceivedInvitation } from '@shared/types/lcu/lobby' -import { LoginQueueState } from '@shared/types/lcu/login' -import { GetSearch, ReadyCheck } from '@shared/types/lcu/matchmaking' -import { SummonerInfo } from '@shared/types/lcu/summoner' -import { defineStore } from 'pinia' -import { shallowReactive, shallowRef } from 'vue' - -// copied -export type LcConnectionStateType = 'connecting' | 'connected' | 'disconnected' - -// copied -export interface UxCommandLine { - port: number - pid: number - authToken: string - certificate: string - region: string - rsoPlatformId: string - riotClientPort: number - riotClientAuthToken: string -} - -export const useLeagueClientStore = defineStore('store:league-client-renderer', () => { - const connectionState = shallowRef('disconnected') - const auth = shallowRef(null) - const connectingClient = shallowRef(null) - - const settings = shallowReactive({ - autoConnect: false - }) - - const gameData = { - champions: shallowRef>({}), - augments: shallowRef>({}), - perks: shallowRef>({}), - perkstyles: shallowRef>({}), - queues: shallowRef>({}), - items: shallowRef>({}), - summonerSpells: shallowRef>({}) - } as const - - const champSelect = { - session: shallowRef(null), - currentChampion: shallowRef(null), - currentPickableChampionIds: shallowRef([]), - currentBannableChampionIds: shallowRef([]), - disabledChampionIds: shallowRef([]) - } as const - - const chat = { - me: shallowRef(null), - conversations: { - championSelect: shallowRef(null), - postGame: shallowRef(null), - customGame: shallowRef(null) - } as const, - participants: { - championSelect: shallowRef(null), - postGame: shallowRef(null), - customGame: shallowRef(null) - } as const - } as const - - const gameflow = { - phase: shallowRef(null), - session: shallowRef(null) - } as const - - const honor = { - ballot: shallowRef(null) - } as const - - const lobby = { - lobby: shallowRef(null), - receivedInvitations: shallowRef([]) - } as const - - const summoner = { - me: shallowRef(null) - } as const - - const login = { - loginQueueState: shallowRef(null) - } as const - - const matchmaking = { - readyCheck: shallowRef(null), - search: shallowRef(null) - } as const - - return { - gameData, - champSelect, - chat, - gameflow, - honor, - lobby, - summoner, - login, - matchmaking, - - settings, - - connectionState, - auth, - connectingClient - } -}) +import { ChampSelectSession } from '@shared/types/lcu/champ-select' +import { ChatPerson, Conversation } from '@shared/types/lcu/chat' +import { + Augment, + ChampionSimple, + Item, + Perk, + Perkstyles, + Queue, + SummonerSpell +} from '@shared/types/lcu/game-data' +import { GameflowPhase, GameflowSession } from '@shared/types/lcu/gameflow' +import { Ballot } from '@shared/types/lcu/honorV2' +import { Lobby, ReceivedInvitation } from '@shared/types/lcu/lobby' +import { LoginQueueState } from '@shared/types/lcu/login' +import { GetSearch, ReadyCheck } from '@shared/types/lcu/matchmaking' +import { SummonerInfo } from '@shared/types/lcu/summoner' +import { defineStore } from 'pinia' +import { shallowReactive, shallowRef } from 'vue' + +// copied +export type LcConnectionStateType = 'connecting' | 'connected' | 'disconnected' + +// copied +export interface UxCommandLine { + port: number + pid: number + authToken: string + certificate: string + region: string + rsoPlatformId: string + riotClientPort: number + riotClientAuthToken: string +} + +export const useLeagueClientStore = defineStore('shard:league-client-renderer', () => { + const connectionState = shallowRef('disconnected') + const auth = shallowRef(null) + const connectingClient = shallowRef(null) + + const settings = shallowReactive({ + autoConnect: false + }) + + const gameData = { + champions: shallowRef>({}), + augments: shallowRef>({}), + perks: shallowRef>({}), + perkstyles: shallowRef>({}), + queues: shallowRef>({}), + items: shallowRef>({}), + summonerSpells: shallowRef>({}) + } as const + + const champSelect = { + session: shallowRef(null), + currentChampion: shallowRef(null), + currentPickableChampionIds: shallowRef>(new Set()), + currentBannableChampionIds: shallowRef>(new Set()), + disabledChampionIds: shallowRef>(new Set()) + } as const + + const chat = { + me: shallowRef(null), + conversations: { + championSelect: shallowRef(null), + postGame: shallowRef(null), + customGame: shallowRef(null) + } as const, + participants: { + championSelect: shallowRef(null), + postGame: shallowRef(null), + customGame: shallowRef(null) + } as const + } as const + + const gameflow = { + phase: shallowRef(null), + session: shallowRef(null) + } as const + + const honor = { + ballot: shallowRef(null) + } as const + + const lobby = { + lobby: shallowRef(null), + receivedInvitations: shallowRef([]) + } as const + + const summoner = { + me: shallowRef(null) + } as const + + const login = { + loginQueueState: shallowRef(null) + } as const + + const matchmaking = { + readyCheck: shallowRef(null), + search: shallowRef(null) + } as const + + return { + gameData, + champSelect, + chat, + gameflow, + honor, + lobby, + summoner, + login, + matchmaking, + + settings, + + connectionState, + auth, + connectingClient + } +}) diff --git a/src/renderer-shared/shards/logger/index.ts b/src/renderer-shared/shards/logger/index.ts index 993f98f7..aada5aa7 100644 --- a/src/renderer-shared/shards/logger/index.ts +++ b/src/renderer-shared/shards/logger/index.ts @@ -46,45 +46,81 @@ export class LoggerRenderer { info(namespace: string, ...args: any[]) { console.info( - `%c[${dayjs().format('HH:mm:ss')}] [%c${namespace}%c]`, - 'color: blue; font-weight: bold;', // 时间部分样式 - 'color: green; font-style: italic;', // namespace部分样式 + `%c[${dayjs().format('HH:mm:ss')}] %c[%c${namespace}%c] %c[info]`, + 'color: #3498db; font-weight: bold;', 'color: inherit;', + 'color: #2e2571; font-weight: bold;', + 'color: inherit;', + 'color: #004c3c; font-weight: bold;', ...args ) - return this._ipc.call(MAIN_SHARD_NAMESPACE, namespace, 'info', this._objectsToString(...args)) + return this._ipc.call( + MAIN_SHARD_NAMESPACE, + 'log', + namespace, + 'info', + this._objectsToString(...args) + ) } warn(namespace: string, ...args: any[]) { console.warn( - `%c[${dayjs().format('HH:mm:ss')}] [%c${namespace}%c]`, - 'color: blue; font-weight: bold;', // 时间部分样式 - 'color: green; font-style: italic;', // namespace部分样式 + `%c[${dayjs().format('HH:mm:ss')}] %c[%c${namespace}%c] %c[warn]`, + 'color: #3498db; font-weight: bold;', + 'color: inherit;', + 'color: #2e2571; font-weight: bold;', 'color: inherit;', + 'color: #004c3c; font-weight: bold;', ...args ) - return this._ipc.call(MAIN_SHARD_NAMESPACE, namespace, 'warn', this._objectsToString(...args)) + return this._ipc.call( + MAIN_SHARD_NAMESPACE, + 'log', + namespace, + 'warn', + this._objectsToString(...args) + ) } error(namespace: string, ...args: any[]) { console.error( - `%c[${dayjs().format('HH:mm:ss')}] [%c${namespace}%c]`, - 'color: blue; font-weight: bold;', - 'color: green; font-style: italic;', + `%c[${dayjs().format('HH:mm:ss')}] %c[%c${namespace}%c] %c[error]`, + 'color: #3498db; font-weight: bold;', + 'color: inherit;', + 'color: #2e2571; font-weight: bold;', 'color: inherit;', + 'color: #004c3c; font-weight: bold;', ...args ) - return this._ipc.call(MAIN_SHARD_NAMESPACE, namespace, 'error', this._objectsToString(...args)) + return this._ipc.call( + MAIN_SHARD_NAMESPACE, + 'log', + namespace, + 'error', + this._objectsToString(...args) + ) } debug(namespace: string, ...args: any[]) { console.debug( - `%c[${dayjs().format('HH:mm:ss')}] [%c${namespace}%c]`, - 'color: blue; font-weight: bold;', - 'color: green; font-style: italic;', + `%c[${dayjs().format('HH:mm:ss')}] %c[%c${namespace}%c] %c[debug]`, + 'color: #3498db; font-weight: bold;', + 'color: inherit;', + 'color: #2e2571; font-weight: bold;', 'color: inherit;', + 'color: #004c3c; font-weight: bold;', ...args ) - return this._ipc.call(MAIN_SHARD_NAMESPACE, namespace, 'debug', this._objectsToString(...args)) + return this._ipc.call( + MAIN_SHARD_NAMESPACE, + 'log', + namespace, + 'debug', + this._objectsToString(...args) + ) + } + + openLogsDir() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'openLogsDir') } } diff --git a/src/renderer-shared/shards/notification/index.ts b/src/renderer-shared/shards/notification/index.ts new file mode 100644 index 00000000..9807e03f --- /dev/null +++ b/src/renderer-shared/shards/notification/index.ts @@ -0,0 +1,11 @@ +/** + * 管理渲染进程中的各类通知 + * 通知包括两种 + * - 进行中的通知, 如当前正在进行的东西 + * - 瞬时的通知, 例如错误提示等 + */ +export class NotificationRenderer { + static id = 'notification-renderer' + + constructor() {} +} diff --git a/src/renderer-shared/shards/ongoing-game/index.ts b/src/renderer-shared/shards/ongoing-game/index.ts new file mode 100644 index 00000000..a3601fb7 --- /dev/null +++ b/src/renderer-shared/shards/ongoing-game/index.ts @@ -0,0 +1,132 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { effectScope, watch } from 'vue' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useOngoingGameStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'ongoing-game-main' +export class OngoingGameRenderer implements IAkariShardInitDispose { + static id = 'ongoing-game-renderer' + static dependencies = [ + 'akari-ipc-renderer', + 'pinia-mobx-utils-renderer', + 'setting-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + private _scope = effectScope() + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + setConcurrency(value: number) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'concurrency', value) + } + + setEnabled(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'enabled', value) + } + + setMatchHistoryLoadCount(value: number) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'matchHistoryLoadCount', value) + } + + setPremadeTeamThreshold(value: number) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'premadeTeamThreshold', value) + } + + setMatchHistoryUseSgpApi(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'matchHistoryUseSgpApi', value) + } + + setMatchHistoryTag(value: string) { + this._ipc.call(MAIN_SHARD_NAMESPACE, 'setMatchHistoryTag', value) + } + + setMatchHistoryTagPreference(value: 'current' | 'all') { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'matchHistoryQueuePreference', value) + } + + reload() { + this._ipc.call(MAIN_SHARD_NAMESPACE, 'reload') + } + + getAll() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'getAll') as Promise<{ + matchHistory: any + summoner: any + rankedStats: any + championMastery: any + savedInfo: any + }> + } + + async onInit() { + const store = useOngoingGameStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + + this._ipc.onEvent(MAIN_SHARD_NAMESPACE, 'clear', () => { + store.summoner = {} + store.matchHistory = {} + store.rankedStats = {} + store.championMastery = {} + store.savedInfo = {} + }) + + this._ipc.onEvent(MAIN_SHARD_NAMESPACE, 'match-history-loaded', (puuid: string, data) => { + store.matchHistory[puuid] = data + }) + + this._ipc.onEvent(MAIN_SHARD_NAMESPACE, 'summoner-loaded', (puuid: string, data) => { + store.summoner[puuid] = data + }) + + this._ipc.onEvent(MAIN_SHARD_NAMESPACE, 'ranked-stats-loaded', (puuid: string, data) => { + store.rankedStats[puuid] = data + }) + + this._ipc.onEvent(MAIN_SHARD_NAMESPACE, 'champion-mastery-loaded', (puuid: string, data) => { + store.championMastery[puuid] = data + }) + + this._ipc.onEvent(MAIN_SHARD_NAMESPACE, 'saved-info-loaded', (puuid: string, data) => { + store.savedInfo[puuid] = data + }) + + const { championMastery, matchHistory, rankedStats, savedInfo, summoner } = await this.getAll() + store.championMastery = championMastery + store.matchHistory = matchHistory + store.rankedStats = rankedStats + store.savedInfo = savedInfo + store.summoner = summoner + + store.settings.orderPlayerBy = await this._setting.get( + OngoingGameRenderer.id, + 'orderPlayerBy', + store.settings.orderPlayerBy + ) + + this._scope.run(() => { + watch( + () => store.settings.orderPlayerBy, + (newValue) => { + this._setting.set(OngoingGameRenderer.id, 'orderPlayerBy', newValue) + } + ) + }) + } + + async onDispose() { + this._scope.stop() + } +} diff --git a/src/renderer-shared/shards/ongoing-game/store.ts b/src/renderer-shared/shards/ongoing-game/store.ts new file mode 100644 index 00000000..fa8a20b7 --- /dev/null +++ b/src/renderer-shared/shards/ongoing-game/store.ts @@ -0,0 +1,146 @@ +import { Mastery } from '@shared/types/lcu/champion-mastery' +import { Game } from '@shared/types/lcu/match-history' +import { RankedStats } from '@shared/types/lcu/ranked' +import { SummonerInfo } from '@shared/types/lcu/summoner' +import { + MatchHistoryGamesAnalysisAll, + MatchHistoryGamesAnalysisTeamSide +} from '@shared/utils/analysis' +import { ParsedRole } from '@shared/utils/ranked' +import { defineStore } from 'pinia' +import { shallowReactive, shallowRef } from 'vue' + +// copied from main shard +interface OngoingGameInfo { + queueId: number + queueType: string + gameId: number + gameMode: string +} + +// copied from main shard +interface MatchHistoryPlayer { + source: 'lcu' | 'sgp' + tag?: string + targetCount: number + data: Game[] +} + +// copied from main shard +interface SummonerPlayer { + source: 'lcu' | 'sgp' + data: SummonerInfo +} + +// copied from main shard +interface RankedStatsPlayer { + source: 'lcu' | 'sgp' + data: RankedStats +} + +// copied from main shard +interface ChampionMasteryPlayer { + source: 'lcu' | 'sgp' + data: Record +} + +// copied from main shard +interface EncounteredGame { + id: number + + gameId: number + + puuid: string + + selfPuuid: string + + region: string + + rsoPlatformId: string + + updateAt: Date + + queueType: string +} + +// copied from main shard +export interface SavedInfo { + puuid: string + + selfPuuid: string + + region: string + + rsoPlatformId: string + + tag: string | null + + updateAt: Date + + lastMetAt: Date | null + + encounteredGames: EncounteredGame[] +} + +export const useOngoingGameStore = defineStore('shard:ongoing-game-renderer', () => { + const settings = shallowReactive({ + enabled: false, + premadeTeamThreshold: 3, + matchHistoryLoadCount: 20, + concurrency: 3, + matchHistoryUseSgpApi: true, + matchHistoryTagPreference: 'current' as 'current' | 'all', + + // renderer only + orderPlayerBy: 'default' as 'win-rate' | 'kda' | 'default' | 'akari-score' + }) + + const gameInfo = shallowRef(null) + const championSelections = shallowRef | null>(null) + const positionAssignments = shallowRef | null>(null) + const teams = shallowRef | null>(null) + + // untyped + const queryStage = shallowRef() + const isInEog = shallowRef(false) + const premadeTeams = shallowRef | null>(null) + + const playerStats = shallowRef<{ + players: Record + teams: Record + } | null>(null) + + const matchHistoryTag = shallowRef(null) + + const matchHistory = shallowRef>({}) + const summoner = shallowRef>({}) + const rankedStats = shallowRef>({}) + const championMastery = shallowRef>({}) + const savedInfo = shallowRef>({}) + + return { + settings, + + gameInfo, + championSelections, + positionAssignments, + teams, + queryStage, + isInEog, + premadeTeams, + playerStats, + matchHistoryTag, + + matchHistory, + summoner, + rankedStats, + championMastery, + savedInfo + } +}) diff --git a/src/renderer-shared/shards/self-update/index.ts b/src/renderer-shared/shards/self-update/index.ts new file mode 100644 index 00000000..be629bb4 --- /dev/null +++ b/src/renderer-shared/shards/self-update/index.ts @@ -0,0 +1,67 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useSelfUpdateStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'self-update-main' + +export class SelfUpdateRenderer implements IAkariShardInitDispose { + static id = 'self-update-renderer' + + static dependencies = [ + 'akari-ipc-renderer', + 'pinia-mobx-utils-renderer', + 'setting-utils-renderer' + ] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._setting = deps['setting-utils-renderer'] + } + + checkUpdates() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'checkUpdates') + } + + startUpdate() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'startUpdate') + } + + cancelUpdate() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'cancelUpdate') + } + + setAnnouncementRead(sha: string) { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'setAnnouncementRead', sha) + } + + openNewUpdatesDir() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'openNewUpdatesDir') + } + + setAutoCheckUpdates(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'autoCheckUpdates', enabled) + } + + setAutoDownloadUpdates(enabled: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'autoDownloadUpdates', enabled) + } + + setDownloadSource(source: 'gitee' | 'github') { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'downloadSource', source) + } + + async onInit() { + const store = useSelfUpdateStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + } +} diff --git a/src/renderer-shared/shards/self-update/store.ts b/src/renderer-shared/shards/self-update/store.ts new file mode 100644 index 00000000..0af964c3 --- /dev/null +++ b/src/renderer-shared/shards/self-update/store.ts @@ -0,0 +1,60 @@ +import { defineStore } from 'pinia' +import { ref, shallowReactive, shallowRef } from 'vue' + +// copied from main shard +interface UpdateProgressInfo { + phase: 'downloading' | 'unpacking' | 'waiting-for-restart' | 'download-failed' | 'unpack-failed' + + downloadingProgress: number + + averageDownloadSpeed: number + + downloadTimeLeft: number + + fileSize: number + + unpackingProgress: number +} + +// copied from main shard +interface CurrentAnnouncement { + content: string + updateAt: Date + isRead: boolean + sha: string +} + +// copied from main shard +interface NewUpdates { + source: 'gitee' | 'github' + currentVersion: string + releaseVersion: string + releaseNotesUrl: string + downloadUrl: string + filename: string + releaseNotes: string +} + +export const useSelfUpdateStore = defineStore('shard:self-update-renderer', () => { + const settings = shallowReactive({ + autoCheckUpdates: true, + autoDownloadUpdates: true, + downloadSource: 'gitee' as 'gitee' | 'github' + }) + + const isCheckingUpdates = ref(false) + const lastCheckAt = ref(null) + const updateProgressInfo = shallowRef(null) + const currentAnnouncement = shallowRef(null) + const newUpdates = shallowRef(null) + + return { + settings, + + isCheckingUpdates, + lastCheckAt, + updateProgressInfo, + currentAnnouncement, + newUpdates + } +}) diff --git a/src/renderer-shared/shards/setting/index.ts b/src/renderer-shared/shards/setting-utils/index.ts similarity index 71% rename from src/renderer-shared/shards/setting/index.ts rename to src/renderer-shared/shards/setting-utils/index.ts index 587d6e00..98703b7b 100644 --- a/src/renderer-shared/shards/setting/index.ts +++ b/src/renderer-shared/shards/setting-utils/index.ts @@ -15,4 +15,8 @@ export class SettingUtilsRenderer { set(namespace: string, key: string, value: any) { return this._ipc.call(MAIN_SHARD_NAMESPACE, 'set', namespace, key, value) } + + async get(namespace: string, key: string, defaultValue?: any) { + return (await this._ipc.call(MAIN_SHARD_NAMESPACE, 'get', namespace, key)) ?? defaultValue + } } diff --git a/src/renderer-shared/shards/sgp/index.ts b/src/renderer-shared/shards/sgp/index.ts new file mode 100644 index 00000000..a2bfa85a --- /dev/null +++ b/src/renderer-shared/shards/sgp/index.ts @@ -0,0 +1,63 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { AvailableServersMap } from '@shared/data-sources/sgp' +import { Game, MatchHistory } from '@shared/types/lcu/match-history' + +import { AkariIpcRenderer } from '../ipc' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { useSgpStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'sgp-main' + +export class SgpRenderer implements IAkariShardInitDispose { + static id = 'sgp-renderer' + static dependencies = ['akari-ipc-renderer', 'pinia-mobx-utils-renderer'] + + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + + constructor(deps: any) { + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + } + + getSupportedSgpServers() { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'getSupportedSgpServers') + } + + getMatchHistoryLcuFormat( + playerPuuid: string, + start: number, + count: number, + tag?: string, + sgpServerId?: string + ) { + return this._ipc.call( + MAIN_SHARD_NAMESPACE, + 'getMatchHistoryLcuFormat', + playerPuuid, + start, + count, + tag, + sgpServerId + ) + } + + getGameSummaryLcuFormat(gameId: number, sgpServerId?: string) { + return this._ipc.call( + MAIN_SHARD_NAMESPACE, + 'getGameSummaryLcuFormat', + gameId, + sgpServerId + ) + } + + getSpectatorGameflow(puuid: string, sgpServerId?: string) { + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'getSpectatorGameflow', puuid, sgpServerId) + } + + async onInit() { + const store = useSgpStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + } +} diff --git a/src/renderer-shared/shards/sgp/store.ts b/src/renderer-shared/shards/sgp/store.ts new file mode 100644 index 00000000..793cb05b --- /dev/null +++ b/src/renderer-shared/shards/sgp/store.ts @@ -0,0 +1,34 @@ +import { AvailableServersMap } from '@shared/data-sources/sgp' +import { defineStore } from 'pinia' +import { shallowRef } from 'vue' + +export const useSgpStore = defineStore('shard:sgp-renderer', () => { + const availability = shallowRef<{ + region: string + rsoPlatform: string + sgpServerId: string + serversSupported: { + matchHistory: boolean + common: boolean + } + sgpServers: AvailableServersMap + }>({ + region: '', + rsoPlatform: '', + sgpServerId: '', + serversSupported: { + matchHistory: false, + common: false + }, + sgpServers: { + servers: {}, + tencentServerMatchHistoryInteroperability: [], + tencentServerSpectatorInteroperability: [], + tencentServerSummonerInteroperability: [] + } + }) + + return { + availability + } +}) diff --git a/src/renderer-shared/shards/window-manager/index.ts b/src/renderer-shared/shards/window-manager/index.ts new file mode 100644 index 00000000..11b5dda3 --- /dev/null +++ b/src/renderer-shared/shards/window-manager/index.ts @@ -0,0 +1,140 @@ +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' + +import { AkariIpcRenderer } from '../ipc' +import { LoggerRenderer } from '../logger' +import { PiniaMobxUtilsRenderer } from '../pinia-mobx-utils' +import { SettingUtilsRenderer } from '../setting-utils' +import { useWindowManagerStore } from './store' + +const MAIN_SHARD_NAMESPACE = 'window-manager-main' + +export class WindowManagerRenderer implements IAkariShardInitDispose { + static id = 'window-manager-renderer' + static dependencies = [ + 'setting-utils-renderer', + 'akari-ipc-renderer', + 'pinia-mobx-utils-renderer', + 'logger-renderer' + ] + + private readonly _setting: SettingUtilsRenderer + private readonly _ipc: AkariIpcRenderer + private readonly _pm: PiniaMobxUtilsRenderer + private readonly _log: LoggerRenderer + + constructor(deps: any) { + this._setting = deps['setting-utils-renderer'] + this._ipc = deps['akari-ipc-renderer'] + this._pm = deps['pinia-mobx-utils-renderer'] + this._log = deps['logger-renderer'] + } + + async onInit() { + const store = useWindowManagerStore() + + this._pm.sync(MAIN_SHARD_NAMESPACE, 'state', store) + this._pm.sync(MAIN_SHARD_NAMESPACE, 'settings', store.settings) + } + + onAskClose(fn: (...args: any[]) => void) { + return this._ipc.onEventVue(MAIN_SHARD_NAMESPACE, 'main-window-close-asking', fn) + } + + maximizeMainWindow() { + this._log.info(WindowManagerRenderer.id, '最大化主窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/maximize') + } + + minimizeMainWindow() { + this._log.info(WindowManagerRenderer.id, '最小化主窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/minimize') + } + + restoreMainWindow() { + this._log.info(WindowManagerRenderer.id, '恢复主窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/restore') + } + + closeMainWindow(strategy?: string) { + this._log.info(WindowManagerRenderer.id, '关闭主窗口', `策略: ${strategy || '[NONE]'}`) + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/close', strategy) + } + + toggleMainWindowDevtools() { + this._log.info(WindowManagerRenderer.id, '切换主窗口开发者工具') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/toggleDevtools') + } + + hideMainWindow() { + this._log.info(WindowManagerRenderer.id, '隐藏主窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/hide') + } + + showMainWindow() { + this._log.info(WindowManagerRenderer.id, '显示主窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/show') + } + + minimizeAuxWindow() { + this._log.info(WindowManagerRenderer.id, '最小化辅助窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'aux-window/minimize') + } + + restoreAuxWindow() { + this._log.info(WindowManagerRenderer.id, '恢复辅助窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'aux-window/restore') + } + + hideAuxWindow() { + this._log.info(WindowManagerRenderer.id, '隐藏辅助窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'aux-window/hide') + } + + showAuxWindow() { + this._log.info(WindowManagerRenderer.id, '显示辅助窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'aux-window/show') + } + + resetAuxWindowPosition() { + this._log.info(WindowManagerRenderer.id, '重置辅助窗口位置') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'aux-window/resetWindowPosition') + } + + setFunctionality(functionality: string) { + this._log.info(WindowManagerRenderer.id, '设置辅助窗口功能', functionality) + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'aux-window/setFunctionality', functionality) + } + + unmaximizeMainWindow() { + this._log.info(WindowManagerRenderer.id, '取消最大化主窗口') + return this._ipc.call(MAIN_SHARD_NAMESPACE, 'main-window/unmaximize') + } + + setMainWindowCloseAction(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'mainWindowCloseAction', value) + } + + setAuxWindowAutoShow(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'auxWindowAutoShow', value) + } + + setAuxWindowEnabled(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'auxWindowEnabled', value) + } + + setAuxWindowOpacity(value: number) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'auxWindowOpacity', value) + } + + setAuxWindowPinned(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'auxWindowPinned', value) + } + + setAuxWindowShowSkinSelector(value: boolean) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'auxWindowShowSkinSelector', value) + } + + setAuxWindowZoomFactor(value: number) { + return this._setting.set(MAIN_SHARD_NAMESPACE, 'auxWindowZoomFactor', value) + } +} diff --git a/src/renderer-shared/shards/window-manager/store.ts b/src/renderer-shared/shards/window-manager/store.ts new file mode 100644 index 00000000..6665b857 --- /dev/null +++ b/src/renderer-shared/shards/window-manager/store.ts @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia' +import { ref, shallowReactive } from 'vue' + +// copied +export type MainWindowCloseAction = 'minimize-to-tray' | 'quit' | 'ask' + +export const useWindowManagerStore = defineStore('shard:window-manager-renderer', () => { + const settings = shallowReactive({ + mainWindowCloseAction: 'ask' as MainWindowCloseAction, + auxWindowEnabled: true, + auxWindowAutoShow: true, + auxWindowOpacity: 0.9, + auxWindowPinned: true, + auxWindowShowSkinSelector: false, + auxWindowZoomFactor: 1.0 + }) + + const mainWindowStatus = ref('normal') + const mainWindowFocus = ref('focused') + const mainWindowShow = ref(true) + + const auxWindowStatus = ref('normal') + const auxWindowFocus = ref('focused') + const auxWindowShow = ref(true) + const auxWindowFunctionality = ref('indicator') + + return { + settings, + mainWindowStatus, + mainWindowFocus, + mainWindowShow, + auxWindowStatus, + auxWindowFocus, + auxWindowShow, + auxWindowFunctionality + } +}) diff --git a/src/renderer/src-auxiliary-window/main.ts b/src/renderer/src-auxiliary-window/main.ts index a4ef9074..716deb7f 100644 --- a/src/renderer/src-auxiliary-window/main.ts +++ b/src/renderer/src-auxiliary-window/main.ts @@ -1,4 +1,11 @@ -import { appRendererModule as am } from '@renderer-shared/modules/app' +import { AkariIpcRenderer } from '@renderer-shared/shards/ipc' +import { LeagueClientRenderer } from '@renderer-shared/shards/league-client' +import { LoggerRenderer } from '@renderer-shared/shards/logger' +import { PiniaMobxUtilsRenderer } from '@renderer-shared/shards/pinia-mobx-utils' +import { RiotClientRenderer } from '@renderer-shared/shards/riot-client' +import { SettingUtilsRenderer } from '@renderer-shared/shards/setting-utils' +import { WindowManagerRenderer } from '@renderer-shared/shards/window-manager' +import { AkariManager } from '@shared/akari-shard/manager' import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' import duration from 'dayjs/plugin/duration' @@ -9,7 +16,6 @@ import { createApp } from 'vue' import NaiveUIProviderApp from './NaiveUIProviderApp.vue' import './assets/css/styles.less' -import { setupLeagueAkariRendererModules } from './modules' import { router } from './routes' dayjs.extend(relativeTime) @@ -19,14 +25,30 @@ const app = createApp(NaiveUIProviderApp) app.use(router) app.use(createPinia()) +const manager = new AkariManager() + +app.provide('shard-manager', manager) + +manager.use( + LeagueClientRenderer, + RiotClientRenderer, + SettingUtilsRenderer, + AkariIpcRenderer, + PiniaMobxUtilsRenderer, + LoggerRenderer, + WindowManagerRenderer +) + try { - await setupLeagueAkariRendererModules() -} catch (error) { - console.error('League Akari 无法正确加载:', error) -} finally { + await manager.setup() + + const logger = manager.getInstance('logger-renderer') as LoggerRenderer + app.config.errorHandler = (err, instance, info) => { - am.logger.error(info, err) + logger.error('Vue', err, instance, info) console.error('Vue Error:', err, instance, info) } app.mount('#app') +} catch (error) { + console.error('League Akari 无法正确加载:', error) } diff --git a/src/renderer/src-auxiliary-window/views/opgg/OpggTier.vue b/src/renderer/src-auxiliary-window/views/opgg/OpggTier.vue index 818216f3..5b18b86f 100644 --- a/src/renderer/src-auxiliary-window/views/opgg/OpggTier.vue +++ b/src/renderer/src-auxiliary-window/views/opgg/OpggTier.vue @@ -1,694 +1,695 @@ - - - - - - - + + + + + + + diff --git a/src/renderer/src-main-window/App.vue b/src/renderer/src-main-window/App.vue index c0663aaa..54bfabb9 100644 --- a/src/renderer/src-main-window/App.vue +++ b/src/renderer/src-main-window/App.vue @@ -14,16 +14,15 @@ - - - - + + + + + + + diff --git a/src/renderer/src-main-window/components/PlayerTagEditModal.vue b/src/renderer/src-main-window/components/PlayerTagEditModal.vue index 382f5856..9b0b39d9 100644 --- a/src/renderer/src-main-window/components/PlayerTagEditModal.vue +++ b/src/renderer/src-main-window/components/PlayerTagEditModal.vue @@ -35,10 +35,9 @@ import LcuImage from '@renderer-shared/components/LcuImage.vue' import { getSummonerByPuuid } from '@renderer-shared/http-api/summoner' import { coreFunctionalityRendererModule as cfm } from '@renderer-shared/modules/core-functionality' import { SavedPlayerInfo } from '@renderer-shared/modules/core-functionality/store' -import { useLcuConnectionStore } from '@renderer-shared/modules/lcu-connection/store' -import { useSummonerStore } from '@renderer-shared/modules/lcu-state-sync/summoner' import { storageRendererModule as sm } from '@renderer-shared/modules/storage' import { laNotification } from '@renderer-shared/notification' +import { useLeagueClientStore } from '@renderer-shared/shards/league-client/store' import { SummonerInfo } from '@shared/types/lcu/summoner' import { summonerName } from '@shared/utils/name' import { NButton, NInput, NModal } from 'naive-ui' @@ -49,7 +48,7 @@ const show = defineModel('show', { default: false }) const summonerInfo = shallowRef(null) const savedInfo = shallowRef(null) -const lc = useLcuConnectionStore() +const lc = useLeagueClientStore() const inputEl = useTemplateRef('input') @@ -62,7 +61,7 @@ const props = defineProps<{ }>() watch([() => show.value, () => props.puuid], async ([sh, puuid]) => { - if (!puuid || !lc.auth || !summoner.me) { + if (!puuid || !lc.auth || !lc.summoner.me) { summonerInfo.value = null return } @@ -73,7 +72,7 @@ watch([() => show.value, () => props.puuid], async ([sh, puuid]) => { summonerInfo.value = s const p = await sm.querySavedPlayerWithGames({ - selfPuuid: summoner.me.puuid, + selfPuuid: lc.summoner.me.puuid, puuid: props.puuid, region: lc.auth.region, rsoPlatformId: lc.auth.rsoPlatformId @@ -97,17 +96,16 @@ watch([() => show.value, () => summonerInfo.value], ([s, u]) => { } }) -const summoner = useSummonerStore() const text = ref('') const handleSaveTag = async () => { - if (!lc.auth || !summoner.me || !props.puuid) { + if (!lc.auth || !lc.summoner.me || !props.puuid) { return } try { await cfm.saveSavedPlayer({ - selfPuuid: summoner.me.puuid, + selfPuuid: lc.summoner.me.puuid, puuid: props.puuid, region: lc.auth.region, rsoPlatformId: lc.auth.rsoPlatformId, diff --git a/src/renderer/src-main-window/components/UpdateModal.vue b/src/renderer/src-main-window/components/UpdateModal.vue index cfff89c1..fb533a9a 100644 --- a/src/renderer/src-main-window/components/UpdateModal.vue +++ b/src/renderer/src-main-window/components/UpdateModal.vue @@ -4,36 +4,36 @@ size="small" preset="card" v-model:show="show" - :class="styles['settings-modal']" + :class="$style['settings-modal']" > -
+
- 新版本可用:{{ au.newUpdates.releaseVersion }} (当前版本:{{ - au.newUpdates.currentVersion + 新版本可用:{{ sus.newUpdates.releaseVersion }} (当前版本:{{ + sus.newUpdates.currentVersion }})
-
当前版本:{{ au.newUpdates.currentVersion }}
+
当前版本:{{ sus.newUpdates.currentVersion }}
- {{ au.newUpdates.source === 'github' ? 'Github' : 'Gitee' }} 发布页面{{ sus.newUpdates.source === 'github' ? 'Github' : 'Gitee' }} 发布页面 {{ au.newUpdates.source === 'github' ? 'Github' : 'Gitee' }} 下载{{ sus.newUpdates.source === 'github' ? 'Github' : 'Gitee' }} 下载
@@ -43,23 +43,22 @@ + + + + + diff --git a/src/renderer/src-main-window/components/settings-modal/GeneralSettings.vue b/src/renderer/src-main-window/components/settings-modal/GeneralSettings.vue index a7ab142e..da1c839a 100644 --- a/src/renderer/src-main-window/components/settings-modal/GeneralSettings.vue +++ b/src/renderer/src-main-window/components/settings-modal/GeneralSettings.vue @@ -199,6 +199,7 @@ const matchHistorySourceOptions = [ } .card-header-title.disabled { - color: rgb(97, 97, 97); + color: rgba(255, 255, 255, 0.35); } + diff --git a/src/renderer/src-main-window/components/settings-modal/OngoingGameSettings.vue b/src/renderer/src-main-window/components/settings-modal/OngoingGameSettings.vue index 8dcdb4d7..fafff851 100644 --- a/src/renderer/src-main-window/components/settings-modal/OngoingGameSettings.vue +++ b/src/renderer/src-main-window/components/settings-modal/OngoingGameSettings.vue @@ -10,20 +10,8 @@ > - - - + + + + + + + 所有模式 + 当前模式 + + - + + + +
diff --git a/src/renderer/src-main-window/components/settings-modal/SettingsModal.vue b/src/renderer/src-main-window/components/settings-modal/SettingsModal.vue index e7130d8d..08472511 100644 --- a/src/renderer/src-main-window/components/settings-modal/SettingsModal.vue +++ b/src/renderer/src-main-window/components/settings-modal/SettingsModal.vue @@ -27,9 +27,9 @@ import DebugSettings from './DebugSettings.vue' import GeneralSettings from './GeneralSettings.vue' import OngoingGameSettings from './OngoingGameSettings.vue' -const styles = useCssModule() -const show = defineModel('show', { default: false }) +const styles = (useCssModule as any)() +const show = defineModel('show', { default: false }) const tabName = defineModel('tabName', { default: 'basic' }) diff --git a/src/renderer/src-main-window/main.ts b/src/renderer/src-main-window/main.ts index 5a587a78..63b751ce 100644 --- a/src/renderer/src-main-window/main.ts +++ b/src/renderer/src-main-window/main.ts @@ -1,9 +1,14 @@ +import { AppCommonRenderer } from '@renderer-shared/shards/app-common' import { AkariIpcRenderer } from '@renderer-shared/shards/ipc' import { LeagueClientRenderer } from '@renderer-shared/shards/league-client' +import { LeagueClientUxRenderer } from '@renderer-shared/shards/league-client-ux' import { LoggerRenderer } from '@renderer-shared/shards/logger' import { PiniaMobxUtilsRenderer } from '@renderer-shared/shards/pinia-mobx-utils' import { RiotClientRenderer } from '@renderer-shared/shards/riot-client' -import { SettingUtilsRenderer } from '@renderer-shared/shards/setting' +import { SelfUpdateRenderer } from '@renderer-shared/shards/self-update' +import { SettingUtilsRenderer } from '@renderer-shared/shards/setting-utils' +import { SgpRenderer } from '@renderer-shared/shards/sgp' +import { WindowManagerRenderer } from '@renderer-shared/shards/window-manager' import { AkariManager } from '@shared/akari-shard/manager' import dayjs from 'dayjs' import 'dayjs/locale/zh-cn' @@ -29,12 +34,17 @@ const manager = new AkariManager() app.provide('shard-manager', manager) manager.use( + AkariIpcRenderer, + AppCommonRenderer, LeagueClientRenderer, + LeagueClientUxRenderer, + LoggerRenderer, + PiniaMobxUtilsRenderer, RiotClientRenderer, + SelfUpdateRenderer, SettingUtilsRenderer, - AkariIpcRenderer, - PiniaMobxUtilsRenderer, - LoggerRenderer + SgpRenderer, + WindowManagerRenderer ) try { diff --git a/src/renderer/src-main-window/shards/match-history-tabs/index.ts b/src/renderer/src-main-window/shards/match-history-tabs/index.ts new file mode 100644 index 00000000..0c433cd5 --- /dev/null +++ b/src/renderer/src-main-window/shards/match-history-tabs/index.ts @@ -0,0 +1,62 @@ +import { SettingUtilsRenderer } from '@renderer-shared/shards/setting-utils' +import { IAkariShardInitDispose } from '@shared/akari-shard/interface' +import { effectScope, watch } from 'vue' + +import { useMatchHistoryTabsStore } from './store' + +export class MatchHistoryTabsRenderer implements IAkariShardInitDispose { + static id = 'match-history-tabs-renderer' + static dependencies = ['setting-utils-renderer'] + + private readonly _setting: SettingUtilsRenderer + + constructor(deps: any) { + this._setting = deps['setting-utils-renderer'] + } + + private _scope = effectScope() + + async onInit() { + await this._handleSettings() + } + + async onDispose() { + this._scope.stop() + } + + /** + * 一些设置项仅在渲染进程中使用, 主进程并不需要 + * 由于使用到的地方比较少, 目前先手动同步 + */ + private async _handleSettings() { + const store = useMatchHistoryTabsStore() + + store.settings.refreshTabsAfterGameEnds = await this._setting.get( + MatchHistoryTabsRenderer.id, + 'refreshTabsAfterGameEnds', + store.settings.refreshTabsAfterGameEnds + ) + + store.settings.matchHistoryUseSgpApi = await this._setting.get( + MatchHistoryTabsRenderer.id, + 'matchHistoryUseSgpApi', + store.settings.matchHistoryUseSgpApi + ) + + this._scope.run(() => { + watch( + () => store.settings.refreshTabsAfterGameEnds, + async (newValue) => { + await this._setting.set(MatchHistoryTabsRenderer.id, 'refreshTabsAfterGameEnds', newValue) + } + ) + + watch( + () => store.settings.matchHistoryUseSgpApi, + async (newValue) => { + await this._setting.set(MatchHistoryTabsRenderer.id, 'matchHistoryUseSgpApi', newValue) + } + ) + }) + } +} diff --git a/src/renderer/src-main-window/shards/match-history-tabs/store.ts b/src/renderer/src-main-window/shards/match-history-tabs/store.ts new file mode 100644 index 00000000..4433ac36 --- /dev/null +++ b/src/renderer/src-main-window/shards/match-history-tabs/store.ts @@ -0,0 +1,20 @@ +import { defineStore } from 'pinia' +import { shallowReactive } from 'vue' + +export const useMatchHistoryTabsStore = defineStore('shard:match-history-tabs-renderer', () => { + const settings = shallowReactive({ + /** + * 游戏结束后刷新涉及到的页面卡 + */ + refreshTabsAfterGameEnds: true, + + /** + * 优先使用 SGP API 查询战绩 + */ + matchHistoryUseSgpApi: true + }) + + return { + settings + } +}) diff --git a/src/renderer/src-main-window/views/automation/AutoGameflow.vue b/src/renderer/src-main-window/views/automation/AutoGameflow.vue index cb82c553..b71aa8d0 100644 --- a/src/renderer/src-main-window/views/automation/AutoGameflow.vue +++ b/src/renderer/src-main-window/views/automation/AutoGameflow.vue @@ -10,8 +10,8 @@ :label-width="200" > @@ -23,8 +23,8 @@ > @@ -54,8 +54,8 @@ 自动点赞 以完成点赞投票阶段 @@ -97,20 +97,20 @@ :label-width="200" > @@ -151,8 +151,8 @@ :label-width="200" > @@ -168,18 +168,18 @@ class="control-item-margin" label="退出匹配时间 (s)" :label-description=" - agf.settings.autoMatchmakingRematchStrategy !== 'fixed-duration' + store.settings.autoMatchmakingRematchStrategy !== 'fixed-duration' ? `该选项仅当停止匹配策略为固定时间时生效` : `在超过该时间后,将停止匹配,单位为秒` " - :disabled="agf.settings.autoMatchmakingRematchStrategy !== 'fixed-duration'" + :disabled="store.settings.autoMatchmakingRematchStrategy !== 'fixed-duration'" :label-width="200" > @@ -206,8 +206,8 @@ :label-width="200" > @@ -263,8 +263,9 @@ diff --git a/src/renderer/src-main-window/views/automation/AutoMisc.vue b/src/renderer/src-main-window/views/automation/AutoMisc.vue index a3a74f79..4812c938 100644 --- a/src/renderer/src-main-window/views/automation/AutoMisc.vue +++ b/src/renderer/src-main-window/views/automation/AutoMisc.vue @@ -43,11 +43,13 @@ + + + + + diff --git a/src/renderer/src-main-window/views/ongoing-game/PlayerInfoCard.vue b/src/renderer/src-main-window/views/ongoing-game/PlayerInfoCard.vue index d00c07b9..a16dbd99 100644 --- a/src/renderer/src-main-window/views/ongoing-game/PlayerInfoCard.vue +++ b/src/renderer/src-main-window/views/ongoing-game/PlayerInfoCard.vue @@ -1,1123 +1,1124 @@ - - - - - + + + + + diff --git a/src/renderer/src-main-window/views/ongoing-game/ongoing-game-utils.ts b/src/renderer/src-main-window/views/ongoing-game/ongoing-game-utils.ts index 26411954..0e98a513 100644 --- a/src/renderer/src-main-window/views/ongoing-game/ongoing-game-utils.ts +++ b/src/renderer/src-main-window/views/ongoing-game/ongoing-game-utils.ts @@ -1,258 +1,247 @@ -import { useLcuConnectionStore } from '@renderer-shared/modules/lcu-connection/store' -import { useChampSelectStore } from '@renderer-shared/modules/lcu-state-sync/champ-select' -import { useGameDataStore } from '@renderer-shared/modules/lcu-state-sync/game-data' -import { useGameflowStore } from '@renderer-shared/modules/lcu-state-sync/gameflow' -import { InjectionKey, computed } from 'vue' - -import BronzeMedal from '@main-window/assets/ranked-icons/bronze.png' -import ChallengerMedal from '@main-window/assets/ranked-icons/challenger.png' -import DiamondMedal from '@main-window/assets/ranked-icons/diamond.png' -import EmeraldMedal from '@main-window/assets/ranked-icons/emerald.png' -import GoldMedal from '@main-window/assets/ranked-icons/gold.png' -import GrandmasterMedal from '@main-window/assets/ranked-icons/grandmaster.png' -import IronMedal from '@main-window/assets/ranked-icons/iron.png' -import MasterMedal from '@main-window/assets/ranked-icons/master.png' -import PlatinumMedal from '@main-window/assets/ranked-icons/platinum.png' -import SilverMedal from '@main-window/assets/ranked-icons/silver.png' - -export const RANKED_MEDAL_MAP: Record = { - IRON: IronMedal, - BRONZE: BronzeMedal, - SILVER: SilverMedal, - GOLD: GoldMedal, - PLATINUM: PlatinumMedal, - EMERALD: EmeraldMedal, - DIAMOND: DiamondMedal, - MASTER: MasterMedal, - GRANDMASTER: GrandmasterMedal, - CHALLENGER: ChallengerMedal -} - -export const POSITION_ASSIGNMENT_REASON = { - FILL_SECONDARY: { - name: '副选补位', - color: '#82613b', - foregroundColor: '#ffffff' - }, - FILL_PRIMARY: { - name: '主选补位', - color: '#5b4694', - foregroundColor: '#ffffff' - }, - PRIMARY: { - name: '主选', - color: '#5b4694', - foregroundColor: '#ffffff' - }, - SECONDARY: { - name: '副选', - color: '#5b4694', - foregroundColor: '#ffffff' - }, - AUTOFILL: { - name: '系统补位', - color: '#944646', - foregroundColor: '#ffffff' - } -} - -export function useQueueOptions() { - const gameData = useGameDataStore() - - return computed(() => { - return [ - { - label: '当前队列', - value: -10 - }, - { - label: '全部队列', - value: -20 - }, - { - label: gameData.queues[420]?.name || 'Ranked Solo/Duo', - value: 420 - }, - { - label: gameData.queues[430]?.name || 'Normal', - value: 430 - }, - { - label: gameData.queues[440]?.name || 'Ranked Flex', - value: 440 - }, - { - label: gameData.queues[450]?.name || 'ARAM', - value: 450 - }, - - { - label: gameData.queues[1700]?.name || 'ARENA', - value: 1700 - }, - { - label: gameData.queues[490]?.name || 'Quickplay', - value: 490 - }, - { - label: gameData.queues[1900]?.name || 'URF', - value: 1900 - }, - { - label: gameData.queues[900]?.name || 'ARURF', - value: 900 - } - ] - }) -} - -export function useOrderOptions() { - return [ - { - label: '楼层顺序', - value: 'default' - }, - { - label: '胜率降序', - value: 'win-rate' - }, - { - label: 'KDA 降序', - value: 'kda' - }, - { - label: '评分降序', - value: 'akari-score' - } - ] -} - -export function useIdleState() { - const gameflow = useGameflowStore() - const champSelect = useChampSelectStore() - const lc = useLcuConnectionStore() - - return computed(() => { - return ( - gameflow.phase === 'Lobby' || - gameflow.phase === 'None' || - gameflow.phase === 'Matchmaking' || - gameflow.phase === 'ReadyCheck' || - gameflow.phase === 'WatchInProgress' || - (gameflow.phase !== 'InProgress' && - champSelect.session && - champSelect.session.isSpectating) || - lc.state !== 'connected' - ) - }) -} - -export interface TeamMeta { - name: string - side: number // 100 和 200 代表红色方和蓝色方, -1 代表未知 -} - -export const TEAM_NAMES: Record = { - '100': { - name: '蓝队', - side: 100 - }, - '200': { - name: '红队', - side: 200 - }, - our: { - name: '我方', - side: -1 - }, - their: { - name: '敌方', - side: -1 - }, - 'our-1': { - name: '我方 (蓝队)', - side: 100 - }, - 'our-2': { - name: '我方 (红队)', - side: 200 - }, - 'their-1': { - name: '敌方 (蓝队)', - side: 100 - }, - 'their-2': { - name: '敌方 (红队)', - side: 200 - } -} - -export const CHINESE_NUMBERS = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'] -export const ENGLISH_NUMBERS = [ - '1st', - '2nd', - '3rd', - '4th', - '5th', - '6th', - '7th', - '8th', - '9th', - '10th' -] - -export const POSITION_NAMES = { - AUTOFILL: '无', - FILL_PRIMARY: '主选补位', - FILL_SECONDARY: '副选补位', - FILL: '补位', - UNSELECTED: '未选择', - BOTTOM: '下路', - JUNGLE: '打野', - MIDDLE: '中路', - UTILITY: '辅助' -} - -export const PRE_MADE_TEAMS = [ - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - 'G', - 'H', - 'I', - 'J', - 'K', - 'L', - 'M', - 'N', - 'O', - 'P', - 'Q', - 'R', - 'S', - 'T', - 'U', - 'V', - 'W', - 'X', - 'Y', - 'Z' -] - -export const PRE_MADE_TEAM_COLORS = { - A: { foregroundColor: '#da2e80', color: '#fff' }, - B: { foregroundColor: '#17d628', color: '#000' }, - C: { foregroundColor: '#628aff', color: '#000' }, - D: { foregroundColor: '#17c1d6', color: '#000' }, - E: { foregroundColor: '#d4de17', color: '#000' }, - F: { foregroundColor: '#b517b5', color: '#ff' }, - G: { foregroundColor: '#48e5db', color: '#000' }, - H: { foregroundColor: '#d63a17', color: '#fff' } -} - -export const FIXED_CARD_WIDTH_PX_LITERAL = '240px' - -export const ONGOING_GAME_COMP_K = Symbol('AKARI_OGC_IJK') as InjectionKey +import { useLeagueClientStore } from '@renderer-shared/shards/league-client/store' +import { computed } from 'vue' + +import BronzeMedal from '@main-window/assets/ranked-icons/bronze.png' +import ChallengerMedal from '@main-window/assets/ranked-icons/challenger.png' +import DiamondMedal from '@main-window/assets/ranked-icons/diamond.png' +import EmeraldMedal from '@main-window/assets/ranked-icons/emerald.png' +import GoldMedal from '@main-window/assets/ranked-icons/gold.png' +import GrandmasterMedal from '@main-window/assets/ranked-icons/grandmaster.png' +import IronMedal from '@main-window/assets/ranked-icons/iron.png' +import MasterMedal from '@main-window/assets/ranked-icons/master.png' +import PlatinumMedal from '@main-window/assets/ranked-icons/platinum.png' +import SilverMedal from '@main-window/assets/ranked-icons/silver.png' + +export const RANKED_MEDAL_MAP: Record = { + IRON: IronMedal, + BRONZE: BronzeMedal, + SILVER: SilverMedal, + GOLD: GoldMedal, + PLATINUM: PlatinumMedal, + EMERALD: EmeraldMedal, + DIAMOND: DiamondMedal, + MASTER: MasterMedal, + GRANDMASTER: GrandmasterMedal, + CHALLENGER: ChallengerMedal +} + +export const POSITION_ASSIGNMENT_REASON = { + FILL_SECONDARY: { + name: '副选补位', + color: '#82613b', + foregroundColor: '#ffffff' + }, + FILL_PRIMARY: { + name: '主选补位', + color: '#5b4694', + foregroundColor: '#ffffff' + }, + PRIMARY: { + name: '主选', + color: '#5b4694', + foregroundColor: '#ffffff' + }, + SECONDARY: { + name: '副选', + color: '#5b4694', + foregroundColor: '#ffffff' + }, + AUTOFILL: { + name: '系统补位', + color: '#944646', + foregroundColor: '#ffffff' + } +} + +export function useSgpTagOptions() { + const lc = useLeagueClientStore() + + return computed(() => { + return [ + { + label: '全部队列', + value: 'all' + }, + { + label: lc.gameData.queues[420]?.name || 'Ranked Solo/Duo', + value: `q_420` + }, + { + label: lc.gameData.queues[430]?.name || 'Normal', + value: `q_430` + }, + { + label: lc.gameData.queues[440]?.name || 'Ranked Flex', + value: `q_440` + }, + { + label: lc.gameData.queues[450]?.name || 'ARAM', + value: `q_450` + }, + + { + label: lc.gameData.queues[1700]?.name || 'ARENA', + value: 'q_1700' + }, + { + label: lc.gameData.queues[490]?.name || 'Quickplay', + value: `q_490` + }, + { + label: lc.gameData.queues[1900]?.name || 'URF', + value: `q_1900` + }, + { + label: lc.gameData.queues[900]?.name || 'ARURF', + value: `q_900` + } + ] + }) +} + +export function useOrderOptions() { + return [ + { + label: '楼层顺序', + value: 'default' + }, + { + label: '胜率降序', + value: 'win-rate' + }, + { + label: 'KDA 降序', + value: 'kda' + }, + { + label: '评分降序', + value: 'akari-score' + } + ] +} + +export function useIdleState() { + const lc = useLeagueClientStore() + + return computed(() => { + return ( + lc.gameflow.phase === 'Lobby' || + lc.gameflow.phase === 'None' || + lc.gameflow.phase === 'Matchmaking' || + lc.gameflow.phase === 'ReadyCheck' || + lc.gameflow.phase === 'WatchInProgress' || + (lc.gameflow.phase !== 'InProgress' && + lc.champSelect.session && + lc.champSelect.session.isSpectating) || + lc.connectionState !== 'connected' + ) + }) +} + +export interface TeamMeta { + name: string + side: number // 100 和 200 代表红色方和蓝色方, -1 代表未知 +} + +export const TEAM_NAMES: Record = { + '100': { + name: '蓝队', + side: 100 + }, + '200': { + name: '红队', + side: 200 + }, + our: { + name: '我方', + side: -1 + }, + their: { + name: '敌方', + side: -1 + }, + 'our-1': { + name: '我方 (蓝队)', + side: 100 + }, + 'our-2': { + name: '我方 (红队)', + side: 200 + }, + 'their-1': { + name: '敌方 (蓝队)', + side: 100 + }, + 'their-2': { + name: '敌方 (红队)', + side: 200 + } +} + +export const CHINESE_NUMBERS = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'] +export const ENGLISH_NUMBERS = [ + '1st', + '2nd', + '3rd', + '4th', + '5th', + '6th', + '7th', + '8th', + '9th', + '10th' +] + +export const POSITION_NAMES = { + AUTOFILL: '无', + FILL_PRIMARY: '主选补位', + FILL_SECONDARY: '副选补位', + FILL: '补位', + UNSELECTED: '未选择', + BOTTOM: '下路', + JUNGLE: '打野', + MIDDLE: '中路', + UTILITY: '辅助' +} + +export const PRE_MADE_TEAMS = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z' +] + +export const PREMADE_TEAM_COLORS = { + A: { foregroundColor: '#da2e80', color: '#fff' }, + B: { foregroundColor: '#17d628', color: '#000' }, + C: { foregroundColor: '#628aff', color: '#000' }, + D: { foregroundColor: '#17c1d6', color: '#000' }, + E: { foregroundColor: '#d4de17', color: '#000' }, + F: { foregroundColor: '#b517b5', color: '#ff' }, + G: { foregroundColor: '#48e5db', color: '#000' }, + H: { foregroundColor: '#d63a17', color: '#fff' } +} + +export const FIXED_CARD_WIDTH_PX_LITERAL = '240px' diff --git a/src/renderer/src-main-window/views/toolkit/in-process/ChampionBench.vue b/src/renderer/src-main-window/views/toolkit/in-process/ChampionBench.vue index 9e2dd425..530925a7 100644 --- a/src/renderer/src-main-window/views/toolkit/in-process/ChampionBench.vue +++ b/src/renderer/src-main-window/views/toolkit/in-process/ChampionBench.vue @@ -30,7 +30,7 @@ :key="c.championId" class="champion-image" :class="{ - 'champion-image-invalid': !cs.currentPickableChampionIds.has(c.championId) + 'champion-image-invalid': !lcs.champSelect.currentPickableChampionIds.has(c.championId) }" :src="championIconUrl(c.championId)" @click="() => handleBenchSwap(c.championId)" @@ -42,7 +42,7 @@
- {{ gameflow.phase === 'ChampSelect' ? '当前模式不可用' : '未处于英雄选择过程中' }} + {{ lcs.gameflow.phase === 'ChampSelect' ? '当前模式不可用' : '未处于英雄选择过程中' }}
@@ -50,40 +50,40 @@ - - diff --git a/src/renderer/src-main-window/views/toolkit/misc/ChatAvailability.vue b/src/renderer/src-main-window/views/toolkit/misc/ChatAvailability.vue index 37ff9d9c..69a3ba93 100644 --- a/src/renderer/src-main-window/views/toolkit/misc/ChatAvailability.vue +++ b/src/renderer/src-main-window/views/toolkit/misc/ChatAvailability.vue @@ -9,9 +9,9 @@ > @@ -30,16 +30,19 @@ - - + + + + + diff --git a/src/renderer/src-main-window/views/toolkit/misc/Spectate.vue b/src/renderer/src-main-window/views/toolkit/misc/Spectate.vue index 750ea1d1..62b7a4cd 100644 --- a/src/renderer/src-main-window/views/toolkit/misc/Spectate.vue +++ b/src/renderer/src-main-window/views/toolkit/misc/Spectate.vue @@ -2,9 +2,11 @@ @@ -18,7 +20,7 @@ 调起观战 import ControlItem from '@renderer-shared/components/ControlItem.vue' -import { getFriends } from '@renderer-shared/http-api/chat' -import { deleteLobby } from '@renderer-shared/http-api/lobby' -import { launchSpectator } from '@renderer-shared/http-api/spectator' -import { getSummonerAlias, getSummonerByName } from '@renderer-shared/http-api/summoner' -import { useGameflowStore } from '@renderer-shared/modules/lcu-state-sync/gameflow' -import { useSummonerStore } from '@renderer-shared/modules/lcu-state-sync/summoner' import { laNotification } from '@renderer-shared/notification' +import { useShardInstance } from '@renderer-shared/shards' +import { LeagueClientRenderer } from '@renderer-shared/shards/league-client' +import { useLeagueClientStore } from '@renderer-shared/shards/league-client/store' import { Friend } from '@shared/types/lcu/chat' import { resolveSummonerName } from '@shared/utils/identity' import { summonerName } from '@shared/utils/name' @@ -60,8 +59,8 @@ import { AxiosError } from 'axios' import { NButton, NCard, NDropdown, NInput } from 'naive-ui' import { computed, reactive, ref } from 'vue' -const gameflow = useGameflowStore() -const summoner = useSummonerStore() +const lcs = useLeagueClientStore() +const lc = useShardInstance('league-client-renderer') const spectator = reactive({ summonerIdentity: '', @@ -69,7 +68,7 @@ const spectator = reactive({ }) const handleSpectate = async () => { - if (spectator.isProcessing || gameflow.phase !== 'None') { + if (spectator.isProcessing || lcs.gameflow.phase !== 'None') { return } @@ -81,8 +80,10 @@ const handleSpectate = async () => { targetPuuid = spectator.summonerIdentity } else { try { - if (summoner.newIdSystemEnabled) { - const s = await getSummonerAlias(...resolveSummonerName(spectator.summonerIdentity)) + if (lcs.summoner.me?.tagLine) { + const s = await lc.api.summoner.getSummonerAlias( + ...resolveSummonerName(spectator.summonerIdentity) + ) if (s) { targetPuuid = s.puuid } else { @@ -91,7 +92,7 @@ const handleSpectate = async () => { } else { const { data: { puuid } - } = await getSummonerByName(spectator.summonerIdentity) + } = await lc.api.summoner.getSummonerByName(spectator.summonerIdentity) targetPuuid = puuid } } catch (error) { @@ -103,7 +104,7 @@ const handleSpectate = async () => { } try { - await launchSpectator(targetPuuid) + await lc.api.spectator.launchSpectator(targetPuuid) laNotification.success('观战', '已拉起观战') } catch (error) { @@ -119,14 +120,14 @@ const handleSpectate = async () => { // 一个 PUUID 的版本 const handleSpectatePuuid = async (puuid: string) => { - if (spectator.isProcessing || gameflow.phase !== 'None') { + if (spectator.isProcessing || lcs.gameflow.phase !== 'None') { return } spectator.isProcessing = true try { - await launchSpectator(puuid) + await lc.api.spectator.launchSpectator(puuid) laNotification.success('观战', '已拉起观战') } catch (error) { @@ -148,7 +149,7 @@ const watchableFriendOptions = computed(() => { const handleLoadFriends = async () => { try { - friends.value = (await getFriends()).data + friends.value = (await lc.api.chat.getFriends()).data } catch (error) { console.error('好友列表加载失败', error) } diff --git a/src/renderer/src-main-window/views/toolkit/misc/SummonerProfile.vue b/src/renderer/src-main-window/views/toolkit/misc/SummonerProfile.vue index ef0196a1..4ed11b04 100644 --- a/src/renderer/src-main-window/views/toolkit/misc/SummonerProfile.vue +++ b/src/renderer/src-main-window/views/toolkit/misc/SummonerProfile.vue @@ -53,7 +53,7 @@ size="small" type="primary" @click="isModalShow = true" - :disabled="lc.state !== 'connected'" + :disabled="lcs.connectionState !== 'connected'" >选择 @@ -64,7 +64,7 @@ :label-width="200" > ('league-client-renderer') const currentChampionId = ref() const currentSkinId = ref() const currentAugmentId = ref() const championOptions = computed(() => { - const list = Object.values(gameData.champions).reduce((arr, current) => { + const list = Object.values(lcs.gameData.champions).reduce((arr, current) => { if (current.id === -1) { return arr } @@ -266,7 +260,7 @@ watch( return } - const details = (await getChampDetails(id)).data + const details = (await lc.api.gameData.getChampDetails(id)).data if (details.id !== currentChampionId.value) { return @@ -291,9 +285,9 @@ const handleApplyToProfile = async () => { isProceeding.value = true try { - await setSummonerBackgroundSkin(currentSkinId.value) + await lc.api.summoner.setSummonerBackgroundSkin(currentSkinId.value) if (currentAugmentId.value !== undefined) { - await setSummonerBackgroundAugments(currentAugmentId.value) + await lc.api.summoner.setSummonerBackgroundAugments(currentAugmentId.value) } message.success('成功', { duration: 1000 }) } catch (error) { @@ -335,8 +329,8 @@ const handleRemovePrestigeCrest = async () => { try { isRemovingPrestigeCrest.value = true - const current = await getRegalia() - await updateRegalia({ + const current = await lc.api.regalia.getRegalia() + await lc.api.regalia.updateRegalia({ preferredCrestType: 'prestige', preferredBannerType: current.data.bannerType, selectedPrestigeCrest: FIXED_PRESTIGE_CREST @@ -350,8 +344,6 @@ const handleRemovePrestigeCrest = async () => { } } -const summoner = useSummonerStore() - const isRemovingTokens = ref(false) // Copied from Seraphine: https://github.com/Zzaphkiel/Seraphine const handleRemoveTokens = async () => { @@ -363,7 +355,7 @@ const handleRemoveTokens = async () => { isRemovingTokens.value = true await updatePlayerPreferences({ challengeIds: [], - bannerAccent: (await getMe()).data.lol?.bannerIdSelected + bannerAccent: (await lc.api.chat.getMe()).data.lol?.bannerIdSelected }) message.success('请求成功') } catch (error) { diff --git a/src/shared/akari-shard/manager.ts b/src/shared/akari-shard/manager.ts index 2e97b4df..0a3324cb 100644 --- a/src/shared/akari-shard/manager.ts +++ b/src/shared/akari-shard/manager.ts @@ -116,12 +116,8 @@ export class AkariManager { * @param id 模块 ID * @returns 模块 ID 实例 */ - getInstance(id: string) { - if (!this._instances.has(id)) { - throw new Error(`Shard with id "${id}" does not exist`) - } - - return this._instances.get(id)! + getInstance(id: string) { + return this._instances.get(id) as T | undefined } private _resolve(id: string, visited: Set, stack: string[]) { diff --git a/src/shared/http-api-axios-helper/league-client/chat.ts b/src/shared/http-api-axios-helper/league-client/chat.ts index 8534a0ad..04dbc91a 100644 --- a/src/shared/http-api-axios-helper/league-client/chat.ts +++ b/src/shared/http-api-axios-helper/league-client/chat.ts @@ -1,76 +1,82 @@ -import { ChatMessage, ChatPerson, Conversation, Friend } from '@shared/types/lcu/chat' -import { AxiosInstance } from 'axios' - -export type AvailabilityType = 'chat' | 'mobile' | 'dnd' | 'away' | 'offline' - -export class ChatHttpApi { - constructor(private _http: AxiosInstance) {} - - getFriends() { - return this._http.get('/lol-chat/v1/friends') - } - - getMe() { - return this._http.get('/lol-chat/v1/me') - } - - getConversations() { - return this._http.get('/lol-chat/v1/conversations') - } - - getParticipants(id: string) { - return this._http.get(`/lol-chat/v1/conversations/${id}/participants`) - } - - changeAvailability(availability: AvailabilityType) { - return this._http.put('/lol-chat/v1/me', { - availability, - ...((availability === 'offline' || availability === 'away') && { - lol: { gameStatus: 'outOfGame' } - }), - ...(availability === 'dnd' && { lol: { gameStatus: 'inGame' } }) - }) - } - - // 暂时不了解summonerId有什么用处,经过测试是加不加都行,尽量保持原样 - chatSend( - targetId: number | string, - message: string, - type: string = 'chat', - isHistorical: boolean = false, - summonerId?: number - ) { - return this._http.post(`/lol-chat/v1/conversations/${targetId}/messages`, { - body: message, - fromId: summonerId, - fromPid: '', - fromSummonerId: summonerId ?? 0, - id: targetId, - isHistorical, - timestamp: '', - type - }) - } - - getChatParticipants(chatRoomId: string) { - return this._http.get(`/lol-chat/v1/conversations/${chatRoomId}/participants`) - } - - changeRanked(rankedLeagueQueue: string, rankedLeagueTier: string, rankedLeagueDivision?: string) { - return this._http.put('/lol-chat/v1/me', { - lol: { - rankedLeagueQueue, - rankedLeagueTier, - rankedLeagueDivision - } - }) - } - - friendRequests(gameName: string, tagLine: string) { - return this._http.post('/lol-chat/v2/friend-requests', { - gameName, - tagLine, - gameTag: tagLine - }) - } -} +import { ChatMessage, ChatPerson, Conversation, Friend } from '@shared/types/lcu/chat' +import { AxiosInstance } from 'axios' + +export type AvailabilityType = 'chat' | 'mobile' | 'dnd' | 'away' | 'offline' + +export class ChatHttpApi { + constructor(private _http: AxiosInstance) {} + + getFriends() { + return this._http.get('/lol-chat/v1/friends') + } + + getMe() { + return this._http.get('/lol-chat/v1/me') + } + + getConversations() { + return this._http.get('/lol-chat/v1/conversations') + } + + getParticipants(id: string) { + return this._http.get(`/lol-chat/v1/conversations/${id}/participants`) + } + + changeAvailability(availability: AvailabilityType) { + return this._http.put('/lol-chat/v1/me', { + availability, + ...((availability === 'offline' || availability === 'away') && { + lol: { gameStatus: 'outOfGame' } + }), + ...(availability === 'dnd' && { lol: { gameStatus: 'inGame' } }) + }) + } + + // 暂时不了解summonerId有什么用处,经过测试是加不加都行,尽量保持原样 + chatSend( + targetId: number | string, + message: string, + type: string = 'chat', + isHistorical: boolean = false, + summonerId?: number + ) { + return this._http.post(`/lol-chat/v1/conversations/${targetId}/messages`, { + body: message, + fromId: summonerId, + fromPid: '', + fromSummonerId: summonerId ?? 0, + id: targetId, + isHistorical, + timestamp: '', + type + }) + } + + getChatParticipants(chatRoomId: string) { + return this._http.get(`/lol-chat/v1/conversations/${chatRoomId}/participants`) + } + + changeRanked(rankedLeagueQueue: string, rankedLeagueTier: string, rankedLeagueDivision?: string) { + return this._http.put('/lol-chat/v1/me', { + lol: { + rankedLeagueQueue, + rankedLeagueTier, + rankedLeagueDivision + } + }) + } + + friendRequests(gameName: string, tagLine: string) { + return this._http.post('/lol-chat/v2/friend-requests', { + gameName, + tagLine, + gameTag: tagLine + }) + } + + setChatStatusMessage(message: string) { + return this._http.put('/lol-chat/v1/me', { + statusMessage: message + }) + } +} diff --git a/src/shared/http-api-axios-helper/league-client/game-data.ts b/src/shared/http-api-axios-helper/league-client/game-data.ts index b0de7b50..6d4cc552 100644 --- a/src/shared/http-api-axios-helper/league-client/game-data.ts +++ b/src/shared/http-api-axios-helper/league-client/game-data.ts @@ -1,57 +1,62 @@ -import { - Augment, - ChampDetails, - ChampionSimple, - GameMap, - GameMapAsset, - Item, - Perk, - Perkstyles, - Queue, - SummonerSpell -} from '@shared/types/lcu/game-data' -import { AxiosInstance } from 'axios' - -export class GameDataHttpApi { - constructor(private _http: AxiosInstance) {} - - getSummonerSpells() { - return this._http.get('/lol-game-data/assets/v1/summoner-spells.json') - } - - getPerkstyles() { - return this._http.get('/lol-game-data/assets/v1/perkstyles.json') - } - - getItems() { - return this._http.get('/lol-game-data/assets/v1/items.json') - } - - getChampionSummary() { - return this._http.get('/lol-game-data/assets/v1/champion-summary.json') - } - - getMaps() { - return this._http.get('/lol-game-data/assets/v1/maps.json') - } - - getPerks() { - return this._http.get('/lol-game-data/assets/v1/perks.json') - } - - getQueues() { - return this._http.get('/lol-game-data/assets/v1/queues.json') - } - - getMapAssets() { - return this._http.get('/lol-game-data/assets/v1/map-assets/map-assets.json') - } - - getChampDetails(champId: number) { - return this._http.get(`/lol-game-data/assets/v1/champions/${champId}.json`) - } - - getAugments() { - return this._http.get('/lol-game-data/assets/v1/cherry-augments.json') - } -} +import { + Augment, + ChampDetails, + ChampionSimple, + GameMap, + GameMapAsset, + Item, + Perk, + Perkstyles, + Queue, + StrawberryHub, + SummonerSpell +} from '@shared/types/lcu/game-data' +import { AxiosInstance } from 'axios' + +export class GameDataHttpApi { + constructor(private _http: AxiosInstance) {} + + getSummonerSpells() { + return this._http.get('/lol-game-data/assets/v1/summoner-spells.json') + } + + getPerkstyles() { + return this._http.get('/lol-game-data/assets/v1/perkstyles.json') + } + + getItems() { + return this._http.get('/lol-game-data/assets/v1/items.json') + } + + getChampionSummary() { + return this._http.get('/lol-game-data/assets/v1/champion-summary.json') + } + + getMaps() { + return this._http.get('/lol-game-data/assets/v1/maps.json') + } + + getPerks() { + return this._http.get('/lol-game-data/assets/v1/perks.json') + } + + getQueues() { + return this._http.get('/lol-game-data/assets/v1/queues.json') + } + + getMapAssets() { + return this._http.get('/lol-game-data/assets/v1/map-assets/map-assets.json') + } + + getChampDetails(champId: number) { + return this._http.get(`/lol-game-data/assets/v1/champions/${champId}.json`) + } + + getAugments() { + return this._http.get('/lol-game-data/assets/v1/cherry-augments.json') + } + + getStrawberryHub() { + return this._http.get('/lol-game-data/assets/v1/strawberry-hub.json') + } +} diff --git a/src/shared/http-api-axios-helper/league-client/index.ts b/src/shared/http-api-axios-helper/league-client/index.ts index dd0099db..5074b4de 100644 --- a/src/shared/http-api-axios-helper/league-client/index.ts +++ b/src/shared/http-api-axios-helper/league-client/index.ts @@ -1,68 +1,74 @@ -import { AxiosInstance } from 'axios' - -import { ChampSelectHttpApi } from './champ-select' -import { ChampionMasteryHttpApi } from './champion-mastery' -import { ChatHttpApi } from './chat' -import { EntitlementsHttpApi } from './entitlements' -import { GameDataHttpApi } from './game-data' -import { GameflowHttpApi } from './gameflow' -import { HonorHttpApi } from './honor' -import { LobbyHttpApi } from './lobby' -import { LoginHttpApi } from './login' -import { LolLeagueSessionHttpApi } from './lol-league-session' -import { LootHttpApi } from './loot' -import { MatchHistoryHttpApi } from './match-history' -import { MatchmakingHttpApi } from './matchmaking' -import { PlayerNotificationsHttpApi } from './player-notifications' -import { ProcessControlHttpApi } from './process-control' -import { RankedHttpApi } from './ranked' -import { RiotClientHttpApi } from './riotclient' -import { SpectatorHttpApi } from './spectator' -import { SummonerHttpApi } from './summoner' - -/** - * 基于 Axios 封装的调用 - */ -export class LeagueClientHttpApiAxiosHelper { - public readonly champSelect: ChampSelectHttpApi - public readonly championMastery: ChampionMasteryHttpApi - public readonly chat: ChatHttpApi - public readonly entitlements: EntitlementsHttpApi - public readonly gameData: GameDataHttpApi - public readonly gameflow: GameflowHttpApi - public readonly honor: HonorHttpApi - public readonly lobby: LobbyHttpApi - public readonly login: LoginHttpApi - public readonly lolLeagueSession: LolLeagueSessionHttpApi - public readonly loot: LootHttpApi - public readonly matchHistory: MatchHistoryHttpApi - public readonly matchmaking: MatchmakingHttpApi - public readonly playerNotifications: PlayerNotificationsHttpApi - public readonly processControl: ProcessControlHttpApi - public readonly ranked: RankedHttpApi - public readonly riotclient: RiotClientHttpApi - public readonly spectator: SpectatorHttpApi - public readonly summoner: SummonerHttpApi - - constructor(private _http: AxiosInstance) { - this.champSelect = new ChampSelectHttpApi(this._http) - this.championMastery = new ChampionMasteryHttpApi(this._http) - this.chat = new ChatHttpApi(this._http) - this.entitlements = new EntitlementsHttpApi(this._http) - this.gameData = new GameDataHttpApi(this._http) - this.gameflow = new GameflowHttpApi(this._http) - this.honor = new HonorHttpApi(this._http) - this.lobby = new LobbyHttpApi(this._http) - this.login = new LoginHttpApi(this._http) - this.lolLeagueSession = new LolLeagueSessionHttpApi(this._http) - this.loot = new LootHttpApi(this._http) - this.matchHistory = new MatchHistoryHttpApi(this._http) - this.matchmaking = new MatchmakingHttpApi(this._http) - this.playerNotifications = new PlayerNotificationsHttpApi(this._http) - this.processControl = new ProcessControlHttpApi(this._http) - this.ranked = new RankedHttpApi(this._http) - this.riotclient = new RiotClientHttpApi(this._http) - this.spectator = new SpectatorHttpApi(this._http) - this.summoner = new SummonerHttpApi(this._http) - } -} +import { AxiosInstance } from 'axios' + +import { ChampSelectHttpApi } from './champ-select' +import { ChampionMasteryHttpApi } from './champion-mastery' +import { ChatHttpApi } from './chat' +import { EntitlementsHttpApi } from './entitlements' +import { GameDataHttpApi } from './game-data' +import { GameflowHttpApi } from './gameflow' +import { HonorHttpApi } from './honor' +import { LobbyHttpApi } from './lobby' +import { LoginHttpApi } from './login' +import { LolLeagueSessionHttpApi } from './lol-league-session' +import { LootHttpApi } from './loot' +import { MatchHistoryHttpApi } from './match-history' +import { MatchmakingHttpApi } from './matchmaking' +import { PlayerNotificationsHttpApi } from './player-notifications' +import { ProcessControlHttpApi } from './process-control' +import { RankedHttpApi } from './ranked' +import { RegaliaHttpApi } from './regalia' +import { RiotClientHttpApi } from './riotclient' +import { SpectatorHttpApi } from './spectator' +import { SummonerHttpApi } from './summoner' +import { LoadoutsHttpApi } from './loadouts' + +/** + * 基于 Axios 封装的调用 + */ +export class LeagueClientHttpApiAxiosHelper { + public readonly champSelect: ChampSelectHttpApi + public readonly championMastery: ChampionMasteryHttpApi + public readonly chat: ChatHttpApi + public readonly entitlements: EntitlementsHttpApi + public readonly gameData: GameDataHttpApi + public readonly gameflow: GameflowHttpApi + public readonly honor: HonorHttpApi + public readonly lobby: LobbyHttpApi + public readonly login: LoginHttpApi + public readonly lolLeagueSession: LolLeagueSessionHttpApi + public readonly loot: LootHttpApi + public readonly matchHistory: MatchHistoryHttpApi + public readonly matchmaking: MatchmakingHttpApi + public readonly playerNotifications: PlayerNotificationsHttpApi + public readonly processControl: ProcessControlHttpApi + public readonly ranked: RankedHttpApi + public readonly riotclient: RiotClientHttpApi + public readonly spectator: SpectatorHttpApi + public readonly summoner: SummonerHttpApi + public readonly regalia: RegaliaHttpApi + public readonly loadouts: LoadoutsHttpApi + + constructor(private _http: AxiosInstance) { + this.champSelect = new ChampSelectHttpApi(this._http) + this.championMastery = new ChampionMasteryHttpApi(this._http) + this.chat = new ChatHttpApi(this._http) + this.entitlements = new EntitlementsHttpApi(this._http) + this.gameData = new GameDataHttpApi(this._http) + this.gameflow = new GameflowHttpApi(this._http) + this.honor = new HonorHttpApi(this._http) + this.lobby = new LobbyHttpApi(this._http) + this.login = new LoginHttpApi(this._http) + this.lolLeagueSession = new LolLeagueSessionHttpApi(this._http) + this.loot = new LootHttpApi(this._http) + this.matchHistory = new MatchHistoryHttpApi(this._http) + this.matchmaking = new MatchmakingHttpApi(this._http) + this.playerNotifications = new PlayerNotificationsHttpApi(this._http) + this.processControl = new ProcessControlHttpApi(this._http) + this.ranked = new RankedHttpApi(this._http) + this.riotclient = new RiotClientHttpApi(this._http) + this.spectator = new SpectatorHttpApi(this._http) + this.summoner = new SummonerHttpApi(this._http) + this.regalia = new RegaliaHttpApi(this._http) + this.loadouts = new LoadoutsHttpApi(this._http) + } +} diff --git a/src/shared/http-api-axios-helper/league-client/loadouts.ts b/src/shared/http-api-axios-helper/league-client/loadouts.ts new file mode 100644 index 00000000..46e1ffea --- /dev/null +++ b/src/shared/http-api-axios-helper/league-client/loadouts.ts @@ -0,0 +1,21 @@ +import { AccountScopeLoadouts } from '@shared/types/lcu/game-data' +import { AxiosInstance } from 'axios' + +/** + * 下一次的模式开放, 涉及到 STRAWBERRY 估计 API 会有很大变动 + */ +export class LoadoutsHttpApi { + constructor(private _http: AxiosInstance) {} + + setStrawberryDifficulty(contentId: string, difficulty: number) { + return this._http.patch(`/lol-loadouts/v4/loadouts/${contentId}`, { + loadout: { + STRAWBERRY_DIFFICULTY: { inventoryType: 'STRAWBERRY_LOADOUT_ITEM', itemId: difficulty } + } + }) + } + + getAccountScopeLoadouts() { + return this._http.get('/lol-loadouts/v4/loadouts/scope/account') + } +} diff --git a/src/shared/http-api-axios-helper/league-client/lobby.ts b/src/shared/http-api-axios-helper/league-client/lobby.ts index d36fb763..f689f9fb 100644 --- a/src/shared/http-api-axios-helper/league-client/lobby.ts +++ b/src/shared/http-api-axios-helper/league-client/lobby.ts @@ -1,117 +1,140 @@ -import { - AvailableBot, - EogStatus, - Lobby, - LobbyMember, - ReceivedInvitation -} from '@shared/types/lcu/lobby' -import { AxiosInstance } from 'axios' - -export class LobbyHttpApi { - constructor(private _http: AxiosInstance) {} - - createCustomLobby( - mode: string, - mapId: number, - spectatorPolicy: string, - lobbyName: string, - lobbyPassword: string | null, - isCustom: boolean - ) { - return this._http.post('/lol-lobby/v2/lobby', { - customGameLobby: { - configuration: { - gameMode: mode, - gameMutator: '', - gameServerRegion: '', - mapId, - mutators: { id: 1 }, // 1 自选 2 征召 3 禁用 4 全随机 - spectatorPolicy, - teamSize: 5 - }, - lobbyName, - lobbyPassword - }, - isCustom - }) - } - - createQueueLobby(queueId: number) { - return this._http.post('/lol-lobby/v2/lobby', { queueId }) - } - - createPractice5x5(name = 'League Stalker Room', password = '') { - return this.createCustomLobby('PRACTICETOOL', 11, 'AllAllowed', name, password, true) - } - - /** - * 提升为房主 - * @param summonerId 目标召唤师 ID - */ - - promote(summonerId: string | number) { - return this._http.post(`/lol-lobby/v2/lobby/members/${summonerId}/promote`) - } - - kick(summonerId: string | number) { - return this._http.post(`/lol-lobby/v2/lobby/members/${summonerId}/kick`) - } - - getMembers() { - return this._http.get('/lol-lobby/v2/lobby/members') - } - - getLobby() { - return this._http.get('/lol-lobby/v2/lobby') - } - - /** - * 可以选择的人机种类 - */ - getAvailableBots() { - return this._http.get('/lol-lobby/v2/lobby/custom/available-bots') - } - - /** - * 是否可以添加人机 - */ - isBotEnabled() { - return this._http.get('/lol-lobby/v2/lobby/custom/bots-enabled') - } - - addBot(botDifficulty: string, champId: number, teamId: '100' | '200') { - return this._http.post('/lol-lobby/v1/lobby/custom/bots', { - botDifficulty, - championId: champId, - teamId - }) - } - - searchMatch() { - return this._http.post('/lol-lobby/v2/lobby/matchmaking/search') - } - - deleteSearchMatch() { - return this._http.delete('/lol-lobby/v2/lobby/matchmaking/search') - } - - playAgain() { - return this._http.post('/lol-lobby/v2/play-again') - } - - getEogStatus() { - return this._http.get('/lol-lobby/v2/party/eog-status') - } - - acceptReceivedInvitation(invitationId: string) { - return this._http.post(`/lol-lobby/v2/received-invitations/${invitationId}/accept`) - } - - declineReceivedInvitation(invitationId: string) { - return this._http.post(`/lol-lobby/v2/received-invitations/${invitationId}/decline`) - } - - getReceivedInvitations() { - return this._http.get('/lol-lobby/v2/received-invitations') - } -} +import { + AvailableBot, + EogStatus, + Lobby, + LobbyMember, + QueueEligibility, + ReceivedInvitation +} from '@shared/types/lcu/lobby' +import { AxiosInstance } from 'axios' + +export class LobbyHttpApi { + constructor(private _http: AxiosInstance) {} + + createCustomLobby( + mode: string, + mapId: number, + spectatorPolicy: string, + lobbyName: string, + lobbyPassword: string | null, + isCustom: boolean + ) { + return this._http.post('/lol-lobby/v2/lobby', { + customGameLobby: { + configuration: { + gameMode: mode, + gameMutator: '', + gameServerRegion: '', + mapId, + mutators: { id: 1 }, // 1 自选 2 征召 3 禁用 4 全随机 + spectatorPolicy, + teamSize: 5 + }, + lobbyName, + lobbyPassword + }, + isCustom + }) + } + + createQueueLobby(queueId: number) { + return this._http.post('/lol-lobby/v2/lobby', { queueId }) + } + + createPractice5x5(name = 'League Stalker Room', password = '') { + return this.createCustomLobby('PRACTICETOOL', 11, 'AllAllowed', name, password, true) + } + + /** + * 提升为房主 + * @param summonerId 目标召唤师 ID + */ + + promote(summonerId: string | number) { + return this._http.post(`/lol-lobby/v2/lobby/members/${summonerId}/promote`) + } + + kick(summonerId: string | number) { + return this._http.post(`/lol-lobby/v2/lobby/members/${summonerId}/kick`) + } + + getMembers() { + return this._http.get('/lol-lobby/v2/lobby/members') + } + + getLobby() { + return this._http.get('/lol-lobby/v2/lobby') + } + + deleteLobby() { + return this._http.delete('/lol-lobby/v2/lobby') + } + + /** + * 可以选择的人机种类 + */ + getAvailableBots() { + return this._http.get('/lol-lobby/v2/lobby/custom/available-bots') + } + + /** + * 是否可以添加人机 + */ + isBotEnabled() { + return this._http.get('/lol-lobby/v2/lobby/custom/bots-enabled') + } + + addBot(botDifficulty: string, champId: number, teamId: '100' | '200') { + return this._http.post('/lol-lobby/v1/lobby/custom/bots', { + botDifficulty, + championId: champId, + teamId + }) + } + + searchMatch() { + return this._http.post('/lol-lobby/v2/lobby/matchmaking/search') + } + + deleteSearchMatch() { + return this._http.delete('/lol-lobby/v2/lobby/matchmaking/search') + } + + playAgain() { + return this._http.post('/lol-lobby/v2/play-again') + } + + getEogStatus() { + return this._http.get('/lol-lobby/v2/party/eog-status') + } + + acceptReceivedInvitation(invitationId: string) { + return this._http.post(`/lol-lobby/v2/received-invitations/${invitationId}/accept`) + } + + declineReceivedInvitation(invitationId: string) { + return this._http.post(`/lol-lobby/v2/received-invitations/${invitationId}/decline`) + } + + getReceivedInvitations() { + return this._http.get('/lol-lobby/v2/received-invitations') + } + + getEligiblePartyQueues() { + return this._http.post('/lol-lobby/v2/eligibility/party') + } + + getEligibleSelfQueues() { + return this._http.post('/lol-lobby/v2/eligibility/self') + } + + setPlayerSlotsStrawberry1(championId: number, mapId = 1, difficultyId = 1) { + return this._http.put('/lol-lobby/v1/lobby/members/localMember/player-slots', [ + { championId, positionPreference: 'UNSELECTED', spell1: mapId, spell2: difficultyId } + ]) + } + + setStrawberryMapId(data: { contentId: string; itemId: number }) { + return this._http.put('/lol-lobby/v2/lobby/strawberryMapId', data) + } +} diff --git a/src/shared/http-api-axios-helper/league-client/regalia.ts b/src/shared/http-api-axios-helper/league-client/regalia.ts new file mode 100644 index 00000000..bb070f44 --- /dev/null +++ b/src/shared/http-api-axios-helper/league-client/regalia.ts @@ -0,0 +1,13 @@ +import { AxiosInstance } from 'axios' + +export class RegaliaHttpApi { + constructor(private _http: AxiosInstance) {} + + updateRegalia(dto: object) { + return this._http.put('/lol-regalia/v2/current-summoner/regalia', dto) + } + + getRegalia() { + return this._http.get('/lol-regalia/v2/current-summoner/regalia') + } +} diff --git a/src/shared/http-api-axios-helper/league-client/summoner.ts b/src/shared/http-api-axios-helper/league-client/summoner.ts index 22d5a86c..0f335cbc 100644 --- a/src/shared/http-api-axios-helper/league-client/summoner.ts +++ b/src/shared/http-api-axios-helper/league-client/summoner.ts @@ -1,55 +1,62 @@ -import { SummonerInfo } from '@shared/types/lcu/summoner' -import { AxiosInstance } from 'axios' - -export class SummonerHttpApi { - constructor(private _http: AxiosInstance) {} - - getCurrentSummoner() { - return this._http.get('/lol-summoner/v1/current-summoner') - } - - getSummoner(id: number) { - return this._http.get(`/lol-summoner/v1/summoners/${id}`) - } - - getSummonerByPuuid(puuid: string) { - return this._http.get(`/lol-summoner/v2/summoners/puuid/${puuid}`) - } - - getSummonerByName(name: string) { - return this._http.get(`/lol-summoner/v1/summoners?name=${name}`) - } - - checkAvailability(name: string) { - return this._http.get(`/lol-summoner/v1/check-name-availability-new-summoners/${name}`) - } - - updateSummonerProfile(data: { inventory?: string; key: string; value: any }) { - return this._http.post('/lol-summoner/v1/current-summoner/summoner-profile', data) - } - - updateSummonerName(name: string) { - return this._http.post('/lol-summoner/v1/current-summoner/name', name) - } - - newSummonerName(name: string) { - return this._http.post('/lol-summoner/v1/summoners', { name }) - } - - setSummonerBackgroundSkin(skinId: number) { - return this.updateSummonerProfile({ - key: 'backgroundSkinId', - value: skinId - }) - } - - getSummonerAliases(nameTagList: { gameName: string; tagLine: string }[]) { - return this._http.post('/lol-summoner/v1/summoners/aliases', nameTagList) - } - - async getSummonerAlias(name: string, tag: string) { - const response = await this.getSummonerAliases([{ gameName: name, tagLine: tag }]) - const result = response.data[0] - return result || null - } -} +import { SummonerInfo } from '@shared/types/lcu/summoner' +import { AxiosInstance } from 'axios' + +export class SummonerHttpApi { + constructor(private _http: AxiosInstance) {} + + getCurrentSummoner() { + return this._http.get('/lol-summoner/v1/current-summoner') + } + + getSummoner(id: number) { + return this._http.get(`/lol-summoner/v1/summoners/${id}`) + } + + getSummonerByPuuid(puuid: string) { + return this._http.get(`/lol-summoner/v2/summoners/puuid/${puuid}`) + } + + getSummonerByName(name: string) { + return this._http.get(`/lol-summoner/v1/summoners?name=${name}`) + } + + checkAvailability(name: string) { + return this._http.get(`/lol-summoner/v1/check-name-availability-new-summoners/${name}`) + } + + updateSummonerProfile(data: { inventory?: string; key: string; value: any }) { + return this._http.post('/lol-summoner/v1/current-summoner/summoner-profile', data) + } + + updateSummonerName(name: string) { + return this._http.post('/lol-summoner/v1/current-summoner/name', name) + } + + newSummonerName(name: string) { + return this._http.post('/lol-summoner/v1/summoners', { name }) + } + + setSummonerBackgroundSkin(skinId: number) { + return this.updateSummonerProfile({ + key: 'backgroundSkinId', + value: skinId + }) + } + + setSummonerBackgroundAugments(augmentId: string) { + return this.updateSummonerProfile({ + key: 'backgroundSkinAugments', + value: augmentId + }) + } + + getSummonerAliases(nameTagList: { gameName: string; tagLine: string }[]) { + return this._http.post('/lol-summoner/v1/summoners/aliases', nameTagList) + } + + async getSummonerAlias(name: string, tag: string) { + const response = await this.getSummonerAliases([{ gameName: name, tagLine: tag }]) + const result = response.data[0] + return result || null + } +} diff --git a/src/shared/i18n/en-US.json b/src/shared/i18n/en-US.json deleted file mode 100644 index 90da972f..00000000 --- a/src/shared/i18n/en-US.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "auto-select-renderer": { - "settings": { - "enabled": { - "title": "Enabled", - "description": "Enabled in normal modes. Such as match mode, rank mode, etc. Any mode that is not a random hero mode" - }, - "showIntent": { - "title": "Show Intent", - "description": "Pre-select the champion that will be automatically selected" - }, - "selectTeammateIntendedChampion": { - "title": "Ignore Teammate's Intent", - "description": "When enabled, it will not consider the teammate's pre-selected champion, otherwise it will avoid conflicts with the teammate's selection" - }, - "completed": { - "title": "Selection Strategy", - "description": "LOCK immediately or JUST SHOW", - "options": { - "true": "Lock", - "false": "Show" - } - }, - "expectedChampions": { - "title": "Expected Champions", - "description": "
Select champions according to the preset list
\n
If the current mode does not have lane information or the current lane has not set a champion, it will select according to the default list
\n
The priority of selection is the order defined in the list, and the champion closer to the front will be selected first
" - } - } - }, - "match-history-tabs-renderer": { - "lose": "Lose", - "win": "Win", - "remake": "Remake", - "team100": "Blue Team", - "team200": "Red Team" - }, - "common": { - "lanes": { - "top": "TOP", - "jungle": "JUNGLE", - "mid": "MIDDLE", - "middle": "MIDDLE", - "bot": "BOTTOM", - "support": "SUPPORT", - "utility": "SUPPORT", - "TOP": "TOP", - "JUNGLE": "JUNGLE", - "MID": "MIDDLE", - "MIDDLE": "MIDDLE", - "BOT": "BOTTOM", - "SUPPORT": "SUPPORT", - "UTILITY": "SUPPORT" - } - }, - "sgpServer": { - "TENCENT_1": "Ionia", - "TENCENT_10": "Demacia" - } -} \ No newline at end of file diff --git a/src/shared/i18n/en-US.yaml b/src/shared/i18n/en-US.yaml new file mode 100644 index 00000000..6ce5d752 --- /dev/null +++ b/src/shared/i18n/en-US.yaml @@ -0,0 +1,45 @@ +auto-select-renderer: + settings: + enabled: + label: Enabled + description: Enable in normal modes. Such as match mode, rank mode, etc. + showIntent: + label: Show Intent + description: Show the champion that will be automatically selected + selectTeammateIntendedChampion: + label: Ignore Teammate Intent + description: When enabled, it will not consider the teammate's intended champion, otherwise it will avoid conflicts with the teammate's selection + completed: + label: Selection Strategy + description: Lock immediately or just show + options: + true: Lock immediately + false: Just show + expectedChampions: + label: Intended Champions + description: | +
Select champions based on the preset list
+
If the current mode does not have lane information or the current lane has not set a champion, it will select according to the default list
+
The selection priority is defined in the order of the list, and the champion closer to the front will be selected first
+ +match-history-tabs-renderer: + lose: Lose + win: Win + remake: Remake + team100: Blue Team + team200: Red Team + +common: + lanes: + top: Top + jungle: Jungle + mid: Mid + bot: Bot + support: Support + utility: Support + TOP: Top + JUNGLE: Jungle + MID: Mid + BOT: Bot + SUPPORT: Support + UTILITY: Support diff --git a/src/shared/i18n/zh-CN.json b/src/shared/i18n/zh-CN.json deleted file mode 100644 index dcac2ad1..00000000 --- a/src/shared/i18n/zh-CN.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "auto-select-renderer": { - "settings": { - "enabled": { - "title": "启用", - "description": "在常规的模式中启用。如匹配模式,排位模式等任何非随机英雄的模式" - }, - "showIntent": { - "title": "提前预选", - "description": "预选即将自动选用的英雄" - }, - "selectTeammateIntendedChampion": { - "title": "无视队友预选", - "description": "开启后将不会考虑队友的预选英雄,反之会避免与队友的选择冲突" - }, - "completed": { - "title": "选择策略", - "description": "立即锁定或只是亮出", - "options": { - "true": "立即锁定", - "false": "只是亮出" - } - }, - "expectedChampions": { - "title": "意向英雄", - "description": "
将根据预设列表选择英雄
\n
若当前模式不存在分路信息当前分路未设置英雄,则按照默认列表进行选择
\n
选择优先级为列表定义顺序,优先选择位置靠前的英雄
" - } - } - }, - "match-history-tabs-renderer": { - "lose": "失败", - "win": "胜利", - "remake": "重开", - "team100": "蓝队", - "team200": "红队" - }, - "common": { - "lanes": { - "top": "上单", - "jungle": "打野", - "mid": "中单", - "bot": "下路", - "support": "辅助", - "utility": "辅助", - "TOP": "上单", - "JUNGLE": "打野", - "MID": "中单", - "BOT": "下路", - "SUPPORT": "辅助", - "UTILITY": "辅助" - } - } -} \ No newline at end of file diff --git a/src/shared/i18n/zh-CN.yaml b/src/shared/i18n/zh-CN.yaml new file mode 100644 index 00000000..6265b161 --- /dev/null +++ b/src/shared/i18n/zh-CN.yaml @@ -0,0 +1,82 @@ +auto-select-renderer: + settings: + enabled: + label: 启用 + description: 在常规的模式中启用。如匹配模式,排位模式等任何非随机英雄的模式 + showIntent: + label: 提前预选 + description: 预选即将自动选用的英雄 + selectTeammateIntendedChampion: + label: 无视队友预选 + description: 开启后将不会考虑队友的预选英雄,反之会避免与队友的选择冲突 + completed: + label: 选择策略 + description: 立即锁定或只是亮出 + options: + true: 立即锁定 + false: 只是亮出 + expectedChampions: + label: 意向英雄 + description: | +
将根据预设列表选择英雄
+
若当前模式不存在分路信息当前分路未设置英雄,则按照默认列表进行选择
+
选择优先级为列表定义顺序,优先选择位置靠前的英雄
+ +match-history-tabs-renderer: + lose: 失败 + win: 胜利 + remake: 重开 + team100: 蓝队 + team200: 红队 + +settings-about-panel-component: + line1: 是开源软件,专注于提供一些额外的功能,以辅助英雄联盟的游戏体验,其几乎所有实现都依赖 + line2: 项目参考 + line3: Github: + line4: © 2024 Hanxven. 本软件是开源软件,遵循 MIT 许可证。 + downloadSource: + label: 检查更新 + description: 从下载源 ({source}) 中检查更新 + buttonCheck: 检查更新 + buttonShowNewUpdate: 新版本内容 + buttonRecentlyCheck: 最近检查 + updateProgressInfo: + label: 更新流程 + description: 正在进行的更新流程 + downloadingProgress: + label: 下载更新包 + progress: 已完成 {percent} % + remainingTime: 剩余:{time} + downloadFailed: 下载失败 + unpackingProgress: + label: 解压更新包 + progress: 已完成 {percent} % + unpackFailed: 解压出错 + restart: + label: 等待重新启动 + description: 关闭应用后将进行自动更新流程 + updatesDir: + label: 更新目录 + description: 当前更新已下载的位置,若无法执行完整的自动更新流程,则需要手动更新 + +announcement-modal-component: + title: 公告 + updatedAt: 更新 + close: 关闭 + read: 已读 + currentNoAnnouncement: 暂无公告 + +common: + lanes: + top: 上单 + jungle: 打野 + mid: 中单 + bot: 下路 + support: 辅助 + utility: 辅助 + TOP: 上单 + JUNGLE: 打野 + MID: 中单 + BOT: 下路 + SUPPORT: 辅助 + UTILITY: 辅助 diff --git a/tsconfig.node.json b/tsconfig.node.json index fa194c1e..0d5107eb 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,6 +1,6 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", - "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/*", "src/shared/**/*"], + "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/*", "src/shared/**/*", "src/main/shards/game-client"], "compilerOptions": { "target": "ESNext", "lib": ["ESNext"], diff --git a/yarn.lock b/yarn.lock index 50be3b00..5628be6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -310,7 +310,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.0": +"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.24.0": version: 7.24.4 resolution: "@babel/parser@npm:7.24.4" bin: @@ -801,6 +801,33 @@ __metadata: languageName: node linkType: hard +"@fast-csv/format@npm:5.0.2": + version: 5.0.2 + resolution: "@fast-csv/format@npm:5.0.2" + dependencies: + lodash.escaperegexp: "npm:^4.1.2" + lodash.isboolean: "npm:^3.0.3" + lodash.isequal: "npm:^4.5.0" + lodash.isfunction: "npm:^3.0.9" + lodash.isnil: "npm:^4.0.0" + checksum: 10c0/dd31e0d6809a4e8e77e24927da35357aa49018965cbee9040a49af66d3dd0adbcdb4b516f1b2092523ea8d410e17ae55ed3cedeb51743d6869d0476def73f5b9 + languageName: node + linkType: hard + +"@fast-csv/parse@npm:5.0.2": + version: 5.0.2 + resolution: "@fast-csv/parse@npm:5.0.2" + dependencies: + lodash.escaperegexp: "npm:^4.1.2" + lodash.groupby: "npm:^4.6.0" + lodash.isfunction: "npm:^3.0.9" + lodash.isnil: "npm:^4.0.0" + lodash.isundefined: "npm:^3.0.1" + lodash.uniq: "npm:^4.5.0" + checksum: 10c0/191d5e8f60468700d1b14a799c08c6aeae56da0a4e215c979e08079f767b1487028bc95a02e1deb01d9812ecbba0b628ebc4697c20142e73b7951924c568c06e + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -1154,90 +1181,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-darwin-arm64@npm:1.7.36" +"@swc/core-darwin-arm64@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-darwin-arm64@npm:1.7.40" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-darwin-x64@npm:1.7.36" +"@swc/core-darwin-x64@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-darwin-x64@npm:1.7.40" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.36" +"@swc/core-linux-arm-gnueabihf@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.40" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-linux-arm64-gnu@npm:1.7.36" +"@swc/core-linux-arm64-gnu@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-linux-arm64-gnu@npm:1.7.40" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-linux-arm64-musl@npm:1.7.36" +"@swc/core-linux-arm64-musl@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-linux-arm64-musl@npm:1.7.40" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-linux-x64-gnu@npm:1.7.36" +"@swc/core-linux-x64-gnu@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-linux-x64-gnu@npm:1.7.40" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-linux-x64-musl@npm:1.7.36" +"@swc/core-linux-x64-musl@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-linux-x64-musl@npm:1.7.40" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-win32-arm64-msvc@npm:1.7.36" +"@swc/core-win32-arm64-msvc@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-win32-arm64-msvc@npm:1.7.40" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-win32-ia32-msvc@npm:1.7.36" +"@swc/core-win32-ia32-msvc@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-win32-ia32-msvc@npm:1.7.40" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.7.36": - version: 1.7.36 - resolution: "@swc/core-win32-x64-msvc@npm:1.7.36" +"@swc/core-win32-x64-msvc@npm:1.7.40": + version: 1.7.40 + resolution: "@swc/core-win32-x64-msvc@npm:1.7.40" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:^1.7.36": - version: 1.7.36 - resolution: "@swc/core@npm:1.7.36" +"@swc/core@npm:^1.7.40": + version: 1.7.40 + resolution: "@swc/core@npm:1.7.40" dependencies: - "@swc/core-darwin-arm64": "npm:1.7.36" - "@swc/core-darwin-x64": "npm:1.7.36" - "@swc/core-linux-arm-gnueabihf": "npm:1.7.36" - "@swc/core-linux-arm64-gnu": "npm:1.7.36" - "@swc/core-linux-arm64-musl": "npm:1.7.36" - "@swc/core-linux-x64-gnu": "npm:1.7.36" - "@swc/core-linux-x64-musl": "npm:1.7.36" - "@swc/core-win32-arm64-msvc": "npm:1.7.36" - "@swc/core-win32-ia32-msvc": "npm:1.7.36" - "@swc/core-win32-x64-msvc": "npm:1.7.36" + "@swc/core-darwin-arm64": "npm:1.7.40" + "@swc/core-darwin-x64": "npm:1.7.40" + "@swc/core-linux-arm-gnueabihf": "npm:1.7.40" + "@swc/core-linux-arm64-gnu": "npm:1.7.40" + "@swc/core-linux-arm64-musl": "npm:1.7.40" + "@swc/core-linux-x64-gnu": "npm:1.7.40" + "@swc/core-linux-x64-musl": "npm:1.7.40" + "@swc/core-win32-arm64-msvc": "npm:1.7.40" + "@swc/core-win32-ia32-msvc": "npm:1.7.40" + "@swc/core-win32-x64-msvc": "npm:1.7.40" "@swc/counter": "npm:^0.1.3" "@swc/types": "npm:^0.1.13" peerDependencies: @@ -1266,7 +1293,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10c0/8ab5382900a20dd2c37236fb09961a9407aa761a8fd5b4cd1c06bca564145d49f1375d819ead96b6cc0c6787bc8f8050eeb32e620ffbfed8734a2bc950368009 + checksum: 10c0/d763413eb649e365282de70994ff6582ecc32b282721d04bd72b8c4403bc3a43327cd4d05fdac2bdb21eb2a6c6de77d9268ace1d358848ee9ffa7d81463b7ee8 languageName: node linkType: hard @@ -1613,43 +1640,30 @@ __metadata: languageName: node linkType: hard -"@volar/language-core@npm:2.4.1, @volar/language-core@npm:~2.4.1": - version: 2.4.1 - resolution: "@volar/language-core@npm:2.4.1" +"@volar/language-core@npm:2.4.8, @volar/language-core@npm:~2.4.8": + version: 2.4.8 + resolution: "@volar/language-core@npm:2.4.8" dependencies: - "@volar/source-map": "npm:2.4.1" - checksum: 10c0/3784422c6a6ac37043203574b79df4345b4edbf1404c2ee8d36da05d55bd356453582f30f612b11ea70846fcd45daf80daf84d1cef81ba156668a53f182983fb + "@volar/source-map": "npm:2.4.8" + checksum: 10c0/f2d2e29f09dfd5f44db4a94c2a64755ed9f7a7855e0f7e118ab59bff21a7e036079e83968a7c49495a11e0be0dcfed02a5547691849ee9414445c121d97aa221 languageName: node linkType: hard -"@volar/source-map@npm:2.4.1": - version: 2.4.1 - resolution: "@volar/source-map@npm:2.4.1" - checksum: 10c0/d40a9c2f209a329b9bd7ae51b39ca70ad73d62df69a250b04ff3e0e93626abdb6e436966028557c27fddd32fab19eba765062b2d91b44f128778f5292fd89406 +"@volar/source-map@npm:2.4.8": + version: 2.4.8 + resolution: "@volar/source-map@npm:2.4.8" + checksum: 10c0/aadab874105e53628d4480a5b6e8e01d3febca326fd822130186a42ebec8397ae6d396c7c3a6993aefe14b45598fe5db55f81b4bd2ebaaa75aad9b9435db68a6 languageName: node linkType: hard -"@volar/typescript@npm:~2.4.1": - version: 2.4.1 - resolution: "@volar/typescript@npm:2.4.1" +"@volar/typescript@npm:~2.4.8": + version: 2.4.8 + resolution: "@volar/typescript@npm:2.4.8" dependencies: - "@volar/language-core": "npm:2.4.1" + "@volar/language-core": "npm:2.4.8" path-browserify: "npm:^1.0.1" vscode-uri: "npm:^3.0.8" - checksum: 10c0/33d423b2081948ed7c2ddd1d9e9fb6d195f9c4f4ab7e96989e8e777234241bd0ba0ec316c256a43392ea781bcd5d5de024f20bbf2bdecef24b0599990247750a - languageName: node - linkType: hard - -"@vue/compiler-core@npm:3.4.21": - version: 3.4.21 - resolution: "@vue/compiler-core@npm:3.4.21" - dependencies: - "@babel/parser": "npm:^7.23.9" - "@vue/shared": "npm:3.4.21" - entities: "npm:^4.5.0" - estree-walker: "npm:^2.0.2" - source-map-js: "npm:^1.0.2" - checksum: 10c0/3ee871b95e17948d10375093c8dd3265923f844528a24ac67512c201ddb9b628021c010565f3e50f2e551b217c502e80a7901384f616a977a04f81e68c64a37c + checksum: 10c0/c1a21b21c53f3cdc7d59dfbe9f84bfbb479af01f0ea117b5b35466a41b1633c2058ce4c73645f7e98d7bb6c649dc6ba1d13e12be1c518e01b22352750892560a languageName: node linkType: hard @@ -1666,7 +1680,7 @@ __metadata: languageName: node linkType: hard -"@vue/compiler-dom@npm:3.5.12": +"@vue/compiler-dom@npm:3.5.12, @vue/compiler-dom@npm:^3.5.0": version: 3.5.12 resolution: "@vue/compiler-dom@npm:3.5.12" dependencies: @@ -1676,16 +1690,6 @@ __metadata: languageName: node linkType: hard -"@vue/compiler-dom@npm:^3.4.0": - version: 3.4.21 - resolution: "@vue/compiler-dom@npm:3.4.21" - dependencies: - "@vue/compiler-core": "npm:3.4.21" - "@vue/shared": "npm:3.4.21" - checksum: 10c0/b4a1099eddacded2663d12388b48088ca0be0d8969a070476f49e4e65da9b22851fc897cc693662b178e7e7fdee98fcf9ea3617a1f626c3a1b2089815cb1264e - languageName: node - linkType: hard - "@vue/compiler-sfc@npm:3.5.12": version: 3.5.12 resolution: "@vue/compiler-sfc@npm:3.5.12" @@ -1737,15 +1741,15 @@ __metadata: languageName: node linkType: hard -"@vue/language-core@npm:2.1.6": - version: 2.1.6 - resolution: "@vue/language-core@npm:2.1.6" +"@vue/language-core@npm:2.1.8": + version: 2.1.8 + resolution: "@vue/language-core@npm:2.1.8" dependencies: - "@volar/language-core": "npm:~2.4.1" - "@vue/compiler-dom": "npm:^3.4.0" + "@volar/language-core": "npm:~2.4.8" + "@vue/compiler-dom": "npm:^3.5.0" "@vue/compiler-vue2": "npm:^2.7.16" - "@vue/shared": "npm:^3.4.0" - computeds: "npm:^0.0.1" + "@vue/shared": "npm:^3.5.0" + alien-signals: "npm:^0.2.0" minimatch: "npm:^9.0.3" muggle-string: "npm:^0.4.1" path-browserify: "npm:^1.0.1" @@ -1754,7 +1758,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/bad09d54929f09d0d809f13ac1a3ccf0ab0d848c11c420e83a951f7fecfe15537caf95fc55756770a4d79f1fa6b4488bf2846afaba6854746fbb349cbb294bed + checksum: 10c0/5106e5961438c33db7e11c2f2369320989d31550a616d636b680ae05c1a91f9fe332fee4aa78c6f9b7f416e8fda893ffa0f3ebab282fe8837159b7ff7eb182de languageName: node linkType: hard @@ -1801,14 +1805,7 @@ __metadata: languageName: node linkType: hard -"@vue/shared@npm:3.4.21, @vue/shared@npm:^3.4.0": - version: 3.4.21 - resolution: "@vue/shared@npm:3.4.21" - checksum: 10c0/79cba4228c3c1769ba8024302d7dbebf6ed1b77fb2e7a69e635cdebaa1c18b409e9c27ce27ccbe3a98e702a7e2dae1b87754d87f0b29adfe2a8f9e1e7c7899d5 - languageName: node - linkType: hard - -"@vue/shared@npm:3.5.12": +"@vue/shared@npm:3.5.12, @vue/shared@npm:^3.5.0": version: 3.5.12 resolution: "@vue/shared@npm:3.5.12" checksum: 10c0/48f94406c42921901b21a57a7ebb401bbceb497152baf0554e5d5a11cbaa79958f966042e9d95614c0b02e8681b7e1b6c010fcb8b28c6bda1b090f2ddd7540d8 @@ -1961,6 +1958,13 @@ __metadata: languageName: node linkType: hard +"alien-signals@npm:^0.2.0": + version: 0.2.0 + resolution: "alien-signals@npm:0.2.0" + checksum: 10c0/5548ae59c929a44048800661cc45d5fb000f9cb40d1d29a11a8e4210c17fa042ac321f8a9d87d849fdd7be4311e2593b59faeab9416067aea44091729af009f1 + languageName: node + linkType: hard + "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -2658,13 +2662,6 @@ __metadata: languageName: node linkType: hard -"computeds@npm:^0.0.1": - version: 0.0.1 - resolution: "computeds@npm:0.0.1" - checksum: 10c0/8a8736f1f43e4a99286519785d71a10ece8f444a2fa1fc2fe1f03dedf63f3477b45094002c85a2826f7631759c9f5a00b4ace47456997f253073fc525e8983de - languageName: node - linkType: hard - "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -3314,6 +3311,16 @@ __metadata: languageName: node linkType: hard +"fast-csv@npm:^5.0.2": + version: 5.0.2 + resolution: "fast-csv@npm:5.0.2" + dependencies: + "@fast-csv/format": "npm:5.0.2" + "@fast-csv/parse": "npm:5.0.2" + checksum: 10c0/73a1c4f82df10179ca08030f4a03de0283df43c1f6ac6d5ca5508b087eeda0d87d5a47a9599df4c9627a6d611ca608b61cfb37fba93ba75cb6debf14398e34be + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -4213,7 +4220,7 @@ __metadata: "@electron-toolkit/utils": "npm:^3.0.0" "@electron/notarize": "npm:^2.5.0" "@rushstack/eslint-patch": "npm:^1.10.4" - "@swc/core": "npm:^1.7.36" + "@swc/core": "npm:^1.7.40" "@trivago/prettier-plugin-sort-imports": "npm:^4.3.0" "@types/deep-eql": "npm:^4.0.2" "@types/lodash": "npm:^4.17.12" @@ -4239,6 +4246,7 @@ __metadata: electron: "npm:^32.1.2" electron-builder: "npm:25.1.8" electron-vite: "npm:^2.3.0" + fast-csv: "npm:^5.0.2" less: "npm:^4.2.0" lodash: "npm:^4.17.21" luaparse: "npm:^0.3.1" @@ -4257,13 +4265,13 @@ __metadata: typeorm: "npm:^0.3.20" typescript: "npm:^5.6.3" vfonts: "npm:^0.0.3" - vite: "npm:^5.4.9" + vite: "npm:^5.4.10" vm2: "npm:^3.9.19" vue: "npm:^3.5.12" vue-chartjs: "npm:^5.3.1" vue-i18n: "npm:10.0.4" vue-router: "npm:^4.4.5" - vue-tsc: "npm:^2.1.6" + vue-tsc: "npm:^2.1.8" winston: "npm:^3.15.0" ws: "npm:^8.18.0" languageName: unknown @@ -4334,6 +4342,13 @@ __metadata: languageName: node linkType: hard +"lodash.escaperegexp@npm:^4.1.2": + version: 4.1.2 + resolution: "lodash.escaperegexp@npm:4.1.2" + checksum: 10c0/484ad4067fa9119bb0f7c19a36ab143d0173a081314993fe977bd00cf2a3c6a487ce417a10f6bac598d968364f992153315f0dbe25c9e38e3eb7581dd333e087 + languageName: node + linkType: hard + "lodash.flattendeep@npm:^4.4.0": version: 4.4.0 resolution: "lodash.flattendeep@npm:4.4.0" @@ -4341,6 +4356,20 @@ __metadata: languageName: node linkType: hard +"lodash.groupby@npm:^4.6.0": + version: 4.6.0 + resolution: "lodash.groupby@npm:4.6.0" + checksum: 10c0/3d136cad438ad6c3a078984ef60e057a3498b1312aa3621b00246ecb99e8f2c4d447e2815460db7a0b661a4fe4e2eeee96c84cb661a824bad04b6cf1f7bc6e9b + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7 + languageName: node + linkType: hard + "lodash.isempty@npm:^4.4.0": version: 4.4.0 resolution: "lodash.isempty@npm:4.4.0" @@ -4348,6 +4377,34 @@ __metadata: languageName: node linkType: hard +"lodash.isequal@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.isequal@npm:4.5.0" + checksum: 10c0/dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f + languageName: node + linkType: hard + +"lodash.isfunction@npm:^3.0.9": + version: 3.0.9 + resolution: "lodash.isfunction@npm:3.0.9" + checksum: 10c0/e88620922f5f104819496884779ca85bfc542efb2946df661ab3e2cd38da5c8375434c6adbedfc76dd3c2b04075d2ba8ec215cfdedf08ddd2e3c3467e8a26ccd + languageName: node + linkType: hard + +"lodash.isnil@npm:^4.0.0": + version: 4.0.0 + resolution: "lodash.isnil@npm:4.0.0" + checksum: 10c0/1a410a62eb2e797f077d038c11cbf1ea18ab36f713982849f086f86e050234d69988c76fa18d00278c0947daec67e9ecbc666326b8a06b43e36d3ece813a8120 + languageName: node + linkType: hard + +"lodash.isundefined@npm:^3.0.1": + version: 3.0.1 + resolution: "lodash.isundefined@npm:3.0.1" + checksum: 10c0/00ca2ae6fc83e10f806769130ee62b5bf419a4aaa52d1a084164b4cf2b2ab1dbf7246e05c72cf0df2ebf4ea38ab565a688c1a7362b54331bb336ea8b492f327f + languageName: node + linkType: hard + "lodash.negate@npm:^3.0.2": version: 3.0.2 resolution: "lodash.negate@npm:3.0.2" @@ -4355,6 +4412,13 @@ __metadata: languageName: node linkType: hard +"lodash.uniq@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.uniq@npm:4.5.0" + checksum: 10c0/262d400bb0952f112162a320cc4a75dea4f66078b9e7e3075ffbc9c6aa30b3e9df3cf20e7da7d566105e1ccf7804e4fbd7d804eee0b53de05d83f16ffbf41c5e + languageName: node + linkType: hard + "lodash@npm:^4.17.15, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -6498,9 +6562,9 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.4.9": - version: 5.4.9 - resolution: "vite@npm:5.4.9" +"vite@npm:^5.4.10": + version: 5.4.10 + resolution: "vite@npm:5.4.10" dependencies: esbuild: "npm:^0.21.3" fsevents: "npm:~2.3.3" @@ -6537,7 +6601,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/e9c59f2c639047e37c79bbbb151c7a55a3dc27932957cf4cf0447ee0bdcc1ddfd9b1fb3ba0465371c01ba3616d62561327855794c2d652213c3a10a32e6d369d + checksum: 10c0/4ef4807d2fd166a920de244dbcec791ba8a903b017a7d8e9f9b4ac40d23f8152c1100610583d08f542b47ca617a0505cfc5f8407377d610599d58296996691ed languageName: node linkType: hard @@ -6621,18 +6685,18 @@ __metadata: languageName: node linkType: hard -"vue-tsc@npm:^2.1.6": - version: 2.1.6 - resolution: "vue-tsc@npm:2.1.6" +"vue-tsc@npm:^2.1.8": + version: 2.1.8 + resolution: "vue-tsc@npm:2.1.8" dependencies: - "@volar/typescript": "npm:~2.4.1" - "@vue/language-core": "npm:2.1.6" + "@volar/typescript": "npm:~2.4.8" + "@vue/language-core": "npm:2.1.8" semver: "npm:^7.5.4" peerDependencies: typescript: ">=5.0.0" bin: vue-tsc: ./bin/vue-tsc.js - checksum: 10c0/6a0676f5ef53cabd142a43ee593d34332ad6f807159861c096c94851a2bd07901294f9e6a0422de8d215d558b54d5a219d0d6626fea626b03f951e72a74026bd + checksum: 10c0/71a6c5fa647c688e838db95570d4b56e4ad8651783b2ad8ff2a9d9c10a7730e5052db73576173ef1e57039ac0b03769c37628b667dff0cb01bb9f3e939725b20 languageName: node linkType: hard