From b5b4553015d2081c037989e3a1154a57262952b3 Mon Sep 17 00:00:00 2001 From: median-dxz Date: Mon, 8 Jul 2024 09:41:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=E5=85=B3=E5=8D=A1?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=88=B0=E6=96=B0=E7=89=88api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.md | 50 ++-- eslint.config.mjs | 27 +- packages/core/battle/internal.ts | 8 +- packages/core/battle/level/index.ts | 52 ++-- packages/core/battle/provider.ts | 4 +- packages/core/common/utils.ts | 2 +- packages/core/event-source/EventSource.ts | 9 +- .../source-builder/fromLevelManager.ts | 20 ++ packages/core/internal/core.ts | 5 +- .../core/internal/features/registerHooks.ts | 52 +--- packages/core/package.json | 2 +- packages/core/pet-helper/PetLocation.ts | 8 +- packages/core/pet-helper/PetStore.ts | 28 +- packages/launcher/package.json | 4 +- .../petFragment/{petFragment.ts => index.ts} | 104 +++---- .../launcher/src/builtin/petFragment/types.ts | 23 +- .../src/builtin/realm/LevelCourageTower.ts | 2 +- .../src/builtin/realm/LevelElfKingsTrial.ts | 2 +- .../src/builtin/realm/LevelExpTraining.ts | 2 +- .../src/builtin/realm/LevelStudyTraining.ts | 2 +- .../src/builtin/realm/LevelTitanHole.ts | 4 +- .../src/builtin/realm/LevelXTeamRoom.ts | 2 +- packages/launcher/src/builtin/realm/index.ts | 11 +- packages/launcher/src/components/ListMenu.tsx | 6 +- .../src/components/PanelTable/PanelTable.tsx | 6 +- packages/launcher/src/constants.ts | 18 +- .../src/context/TaskSchedulerProvider.tsx | 266 ++++++++++-------- .../launcher/src/context/useTaskScheduler.ts | 18 +- packages/launcher/src/features/engine.ts | 15 +- packages/launcher/src/features/registerLog.ts | 2 +- .../src/services/config/usePetGroups.ts | 4 +- packages/launcher/src/services/endpoints.ts | 20 +- packages/launcher/src/services/mod/handler.ts | 6 +- packages/launcher/src/services/mod/install.ts | 9 +- .../launcher/src/services/mod/metadata.ts | 14 +- .../services/mod/{schema.ts => schemas.ts} | 13 +- packages/launcher/src/services/mod/utils.ts | 46 +-- packages/launcher/src/services/store/mod.ts | 37 ++- packages/launcher/src/services/store/task.ts | 4 +- packages/launcher/src/services/useBagPets.ts | 12 +- .../usePersistentConfig.ts | 16 +- .../launcher/src/services/useTaskConfig.ts | 13 + .../useCachedReturn.ts => shared/hooks.ts} | 0 packages/launcher/src/shared/index.ts | 57 ++++ .../launcher/src/{utils => shared}/logger.ts | 0 packages/launcher/src/shared/types.ts | 3 + .../views/AutomationView/CommonLevelView.tsx | 203 ++++++------- .../views/AutomationView/DailySignView.tsx | 49 +++- .../AutomationView/PetFragmentLevelView.tsx | 131 ++++----- .../TaskStateView/TaskStateList.tsx | 216 +++++++------- .../GameControllerView/AutoCureState.tsx | 8 +- .../views/GameControllerView/BattleFire.tsx | 37 ++- .../GameControllerView/Inventory/index.tsx | 8 +- .../InstallView/InstallFromLocalForm.tsx | 5 + .../views/ModView/ManagementView/ModList.tsx | 18 +- .../launcher/src/views/PackageCapture.tsx | 26 +- packages/launcher/tsconfig.json | 2 +- packages/mod-type/index.ts | 11 +- packages/server/package.json | 4 +- packages/server/src/client-types.ts | 1 + packages/server/src/data/LauncherConfig.ts | 4 +- packages/server/src/data/ModIndexes.ts | 8 +- packages/server/src/data/PetCacheManager.ts | 4 +- packages/server/src/data/PetFragmentLevel.ts | 17 -- packages/server/src/data/TaskConfig.ts | 60 ++++ packages/server/src/data/init.ts | 6 +- packages/server/src/paths.ts | 1 - .../server/src/router/config/catchTime.ts | 22 ++ packages/server/src/router/config/index.ts | 49 +--- packages/server/src/router/config/launcher.ts | 27 ++ packages/server/src/router/config/task.ts | 29 ++ packages/server/src/router/mod/index.ts | 25 +- packages/server/src/router/mod/manager.ts | 8 +- packages/server/src/router/mod/schemas.ts | 9 +- .../server/src/router/mod/uploadAndInstall.ts | 2 +- packages/server/src/server.ts | 11 +- .../src/{utils => shared}/SEASConfigData.ts | 0 .../server/src/{utils => shared}/index.ts | 0 packages/server/src/shared/schemas.ts | 12 + packages/server/tsconfig.json | 4 +- pnpm-lock.yaml | 24 ++ sdk/mods/median/LocalPetSkin.ts | 27 +- sdk/mods/median/module/PetBag.ts | 4 +- sdk/mods/median/sign/daily.ts | 6 +- sdk/mods/median/sign/team-dispatch.ts | 2 +- sdk/mods/median/sign/team.ts | 4 +- sdk/mods/median/sign/vip.ts | 6 +- sdk/package-lock.json | 10 +- sdk/package.json | 2 +- 89 files changed, 1133 insertions(+), 977 deletions(-) create mode 100644 packages/core/event-source/source-builder/fromLevelManager.ts rename packages/launcher/src/builtin/petFragment/{petFragment.ts => index.ts} (55%) rename packages/launcher/src/services/mod/{schema.ts => schemas.ts} (67%) rename packages/launcher/src/{utils => services}/usePersistentConfig.ts (62%) create mode 100644 packages/launcher/src/services/useTaskConfig.ts rename packages/launcher/src/{utils/hooks/useCachedReturn.ts => shared/hooks.ts} (100%) create mode 100644 packages/launcher/src/shared/index.ts rename packages/launcher/src/{utils => shared}/logger.ts (100%) create mode 100644 packages/launcher/src/shared/types.ts delete mode 100644 packages/server/src/data/PetFragmentLevel.ts create mode 100644 packages/server/src/data/TaskConfig.ts create mode 100644 packages/server/src/router/config/catchTime.ts create mode 100644 packages/server/src/router/config/launcher.ts create mode 100644 packages/server/src/router/config/task.ts rename packages/server/src/{utils => shared}/SEASConfigData.ts (100%) rename packages/server/src/{utils => shared}/index.ts (100%) create mode 100644 packages/server/src/shared/schemas.ts diff --git a/changelog.md b/changelog.md index f31da69f..30b60e5e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,17 @@ // 以下信息仅在 >= 1.0 版本内显示,后续 1.0 版本发布后(应为第一个可用的稳定版)将删除并重新开始记录 changelog // 为什么要删掉?一个是因为乱;一个是后面肯定会用上更好的 changelog 管理工具(changeset 预定)而不是手动编辑;最重要的一点:changelog 一般是供开发者了解功能变更,从而帮助版本的迁移。但是前面这会根本没有人用,自然不用关心这些问题... -当前更新内容 模组安装与管理(2024.6) +当前更新内容 模组安装与管理 & 关卡功能重构(2024.7) + +- [ ] 关卡配置与mod配置UI +- [ ] 调度界面, 删除全部已完成 +- [ ] 收星星上限出错出错处理 +- [ ] 更新vip签到(flash商城?) +- [ ] 全屏与刷新按钮 +- [ ] 关卡添加与结束的snackbar +- [ ] rotating算子基于内部计数器而不是round +- [ ] 命名空间的脆弱性 +- [ ] core v1.0.0 - [ ] https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#the-configdir-template-variable-for-configuration-files - [ ] 升级sdk的ts版本 @@ -10,30 +20,25 @@ - [ ] 需要一次性提供 Launcher Api 版本,Core 版本和自身版本 - [ ] launcher api 版本和 launcher 版本之间的关系? - [ ] 同步server launcher mod-type的版本,保持统一 -- [x] 统一签到模组(`sign`)到关卡, 将无战斗纯发包的也视为关卡的一种 - - [x] 可能需要重新设计关卡的整体接口 - - [ ] 顺便解决关卡进度指示器的问题 - [ ] 战斗日志保存 - [ ] script 通过语法树进行高级反混淆, 暂定主要目标是升级 async/await - [ ] 后端日志管理 - [ ] rotating -- [ ] 配置管理 - [ ] 预置功能管理 -- [x] 一键收星星 +- [ ] 内置模组在更新时的处理 +- [ ] 日任掉线 - [ ] 自定义背景(各种意义上?) -- [x] 控制中心:自动治疗开关 - [ ] QuickAccess 重做,移入主界面底部 - [ ] ctOverride -- [ ] 日常新增:功勋任务 - [ ] spt 扫荡 +- [ ] 作战实验室 六界扫荡 星际迷航 神兽 - [ ] 常用配置查询 - [ ] 精灵养成模组,每天自动养成指定精灵 - [ ] 自动消耗积分 - [ ] 指定一个列表,自动选择没有达到指标的精灵进行养成 -- [ ] launcher 语法高亮模块 +- [ ] launcher 语法高亮模块 hljs - [ ] launcher 轻量配置编辑器模块,日志显示模块 - [ ] launcher 和 server 的 logger 模块 -- [ ] 调度界面, 删除全部已完成 - [ ] 模组加载时的出错处理 - [ ] 教程的 cc 共享协议 - [ ] 删除博客功能 @@ -42,30 +47,26 @@ - [ ] 由后端支持的更新 dist 文件夹进行软件更新 - [ ] dc 群组 - [ ] yo 工作区脚手架构建 -- [ ] 引入 changeset +- [ ] 引入 changeset + 发版 - [ ] 优化自动构建脚本 - [ ] 搜索功能 - [ ] sdk/预设/例子/模组发布转移到单独的仓库 - [ ] store逻辑复用优化 - [ ] 拆分优化调度器list -- [ ] pick函数 - [ ] 优化内置日志输出,信息输出,封装组件 - [ ] tarui/electron 打包项目 - [ ] 控件拖动 - [ ] 当前面板路径 - [ ] 进入对战超时 -- [ ] 更新vip签到 -- [ ] 日任掉线 - [ ] 更换名片精灵页面的重复 -- [ ] 全屏与刷新按钮 -- [ ] 收星星上限出错出错处理 -- [ ] 收星星上限逻辑 - [ ] coverage文件夹换位置 - [ ] 对等依赖<->dts定义(例如注册查询表)(可参考:next模块定义,mui对样式引擎的定义依赖) - [ ] 模组定义新增标志位,表明不支持热重载 - [ ] 取消对http-proxy-middleware的依赖 +- [ ] PanelTable是不是直接useMemo就行了,奇怪的cacheReturn hook +- [ ] 错误边界 -# Core 当前版本 v1.0.0-rc.5 +# Core 当前版本 v1.0.0-rc.6 - [x] 精简 api 界面, 删除不必要的导出 - [x] 删除不常用的子包导出 @@ -88,20 +89,20 @@ - [x] 日志模块重写,输出 object 而不是消息,可以子 logger 化 - [ ] 查询关卡获取的因子数量 - [x] 启用单元测试 -- [ ] 对于 CoreLoad 的注册 hook, 提供标志位来进行功能的打开,关闭 - - [ ] 同时公开 hook 数组, 以便登录器层可以开关特定功能 - [ ] 解耦登录器/后端特定逻辑, 分离非核心功能, 部分移动到登录器下的`features`包下, 由登录器提供扩展定义, 部分合并到`engine`中 - [ ] script 解密 - [ ] 对战显血 - [ ] 自动关弹窗 - [ ] logFilter - -# Core 1.1.0 - - [ ] 查询魂印激活放到 SEAPet 中 +- [x] LevelRunner使用事件流 - [ ] 完善 pet 缓存逻辑 - [ ] 同时优化第五和魂印的检测逻辑 -- [ ] 集成测试作为单独的包,移出 core +- [x] 对于 CoreLoad 的注册 hook, 提供标志位来进行功能的打开,关闭 + - [x] 同时公开 hook 数组, 以便登录器层可以开关特定功能 +- [x] 集成测试作为单独的包,移出 core + +# Core v1.1.0 # 低优先级 @@ -111,7 +112,6 @@ - [x] 根组件渲染提前到 Core.init - [ ] jsx 外部化, import-map 共享 react - [ ] cookie issue -- [ ] 更换 store 库(不用 context) - [ ] 在三个加载资源处都显示 总加载文件数 当前文件名 当前已下载大小/当前文件大小 - [ ] 考虑如何使`Strategy`添加更简单的 fallback 支持(可能添加一个字段?) - [ ] 例如`true`代表 fallback 到自动, 否则链式依次 fallback diff --git a/eslint.config.mjs b/eslint.config.mjs index fd66974e..77981772 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -68,7 +68,18 @@ export default tsEslint.config( '@typescript-eslint/dot-notation': ['error', { allowIndexSignaturePropertyAccess: true }], '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], '@typescript-eslint/no-unnecessary-condition': ['error', { allowConstantLoopConditions: true }], - '@typescript-eslint/no-invalid-void-type': ['error', { allowAsThisParameter: true }] + '@typescript-eslint/no-invalid-void-type': ['error', { allowAsThisParameter: true }], + '@typescript-eslint/no-confusing-void-expression': [ + 'error', + { ignoreArrowShorthand: true, ignoreVoidOperator: true } + ], + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: false + } + ], + '@typescript-eslint/no-extraneous-class': 'off' } }, { @@ -107,7 +118,8 @@ export default tsEslint.config( // @ts-expect-error rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': 'warn' + 'react-refresh/only-export-components': 'warn', + '@typescript-eslint/no-unsafe-assignment': 'warn' // swr 当前版本解构出的 error 只能推导出 any } }, // @sea/server @@ -117,17 +129,6 @@ export default tsEslint.config( globals: { ...globals.node } - }, - rules: { - '@typescript-eslint/no-misused-promises': [ - 'error', - { - checksVoidReturn: { - arguments: false, - attributes: false - } - } - ] } }, { diff --git a/packages/core/battle/internal.ts b/packages/core/battle/internal.ts index 8f2ddf77..443fac21 100644 --- a/packages/core/battle/internal.ts +++ b/packages/core/battle/internal.ts @@ -60,9 +60,7 @@ export default () => { context.triggerLock?.(isWin); manager.clear(); }) - .catch((err: unknown) => { - console.error(err); - }); + .catch((err: unknown) => console.error(err)); } }); @@ -73,7 +71,5 @@ export default () => { return [fi, si] as const; }); - SEAEventSource.socket(CommandID.NOTE_USE_SKILL, 'receive').on((data) => { - cachedRoundInfo.update([...data]); - }); + SEAEventSource.socket(CommandID.NOTE_USE_SKILL, 'receive').on((data) => cachedRoundInfo.update([...data])); }; diff --git a/packages/core/battle/level/index.ts b/packages/core/battle/level/index.ts index f6fe8b6d..1fd16afb 100644 --- a/packages/core/battle/level/index.ts +++ b/packages/core/battle/level/index.ts @@ -1,3 +1,4 @@ +import { Subject, tap } from 'rxjs'; import { delay } from '../../common/utils.js'; import { engine } from '../../internal/index.js'; import { spet } from '../../pet-helper/index.js'; @@ -14,6 +15,10 @@ class LevelManager { private runner: LevelRunner | null = null; lock: Promise | null = null; + update$ = new Subject(); + nextAction$ = new Subject(); + log$ = new Subject(); + get running() { return this.runner != null; } @@ -29,10 +34,7 @@ class LevelManager { try { await this.lock; } catch (e) { - throw new Error(`关卡运行失败: ${e as string}`); - } finally { - this.lock = null; - manager.clear(); + // pass } } @@ -40,7 +42,8 @@ class LevelManager { if (this.running) throw new Error('你必须先停止当前Runner的运行!'); this.runner = runner; - const { logger } = runner; + const log$ = new Subject(); + const subscription = log$.pipe(tap(runner.logger)).subscribe(this.log$); const lockFn = async () => { const autoCureState = await engine.autoCureState(); @@ -53,7 +56,7 @@ class LevelManager { const { strategy, pets, beforeBattle } = levelBattle; - logger('准备对战'); + log$.next('准备对战'); await engine.switchBag(pets); void engine.toggleAutoCure(false); @@ -61,11 +64,11 @@ class LevelManager { await delay(100); - logger('执行beforeBattle'); + log$.next('执行beforeBattle'); await beforeBattle?.(); await spet(pets[0]).default(); - logger('进入对战'); + log$.next('进入对战'); try { if (!this.runner) throw new Error('关卡已停止运行'); @@ -74,28 +77,30 @@ class LevelManager { void runner.actions['battle'].call(runner); }, strategy); - logger('对战完成'); + log$.next('对战完成'); } catch (error) { this.runner = null; - logger(`接管对战失败: ${error as string}`); + log$.next(`接管对战失败: ${error as string}`); } manager.clear(); }; while (this.runner) { - logger('更新关卡信息'); await runner.update(); + this.update$.next(); + log$.next('更新关卡信息'); + const nextAction = runner.next(); - logger(`next action: ${nextAction}`); + this.nextAction$.next(nextAction); + log$.next(`next action: ${nextAction}`); + switch (nextAction) { case LevelAction.BATTLE: await battle(); break; case LevelAction.STOP: - if (runner.actions['stop']) { - await runner.actions['stop'].call(runner); - } + await runner.actions['stop']?.call(runner); this.runner = null; break; default: @@ -104,17 +109,24 @@ class LevelManager { } await delay(100); } - logger('正在停止关卡'); + log$.next('正在停止关卡'); // 恢复自动治疗状态 await engine.toggleAutoCure(autoCureState); engine.cureAllPet(); await delay(200); - - logger('关卡完成'); - this.lock = this.runner = null; }; - this.lock = lockFn(); + this.lock = lockFn() + .catch((err: unknown) => { + log$.next(`关卡运行失败: ${err as string}`); + throw err; + }) + .finally(() => { + log$.next('关卡运行结束'); + subscription.unsubscribe(); + this.lock = this.runner = null; + manager.clear(); + }); } } diff --git a/packages/core/battle/provider.ts b/packages/core/battle/provider.ts index dbcefde4..012a4b33 100644 --- a/packages/core/battle/provider.ts +++ b/packages/core/battle/provider.ts @@ -39,7 +39,7 @@ function getCurRoundInfo() { id: FighterModelFactory.playerMode.info.petID, name: FighterModelFactory.playerMode.info.petName, hp: { - gain: roundInfo[0].hp.gain, + gain: 0, remain: FighterModelFactory.playerMode.info.hp, max: FighterModelFactory.playerMode.info.maxHP }, @@ -51,7 +51,7 @@ function getCurRoundInfo() { id: FighterModelFactory.enemyMode.info.petID, name: FighterModelFactory.enemyMode.info.petName, hp: { - gain: roundInfo[1].hp.gain, + gain: 0, remain: FighterModelFactory.enemyMode.info.hp, max: FighterModelFactory.enemyMode.info.maxHP }, diff --git a/packages/core/common/utils.ts b/packages/core/common/utils.ts index 1516e634..6d2da7d7 100644 --- a/packages/core/common/utils.ts +++ b/packages/core/common/utils.ts @@ -17,7 +17,7 @@ export class HookedSymbol { export function delay(time: number): Promise { return new Promise((resolver) => setTimeout(resolver, time)); } -/** 去抖 所有小于指定间隔的调用只会响应最后一个 */ +/** 去抖 所有小于指定间隔的调用只会在最后一个超时后响应 */ export function debounce(func: F, time: number) { let timer: number | undefined; return function (this: unknown, ...args: Parameters) { diff --git a/packages/core/event-source/EventSource.ts b/packages/core/event-source/EventSource.ts index c14feb33..ef302f7c 100644 --- a/packages/core/event-source/EventSource.ts +++ b/packages/core/event-source/EventSource.ts @@ -1,8 +1,10 @@ -import { take, type Observable, type Subscription } from 'rxjs'; +import type { Observable, Subscription } from 'rxjs'; +import { take } from 'rxjs'; import { fromEvent } from './source-builder/fromEvent.js'; import { fromEventPattern } from './source-builder/fromEventPattern.js'; import { fromGameModule } from './source-builder/fromGameModule.js'; import { fromHook } from './source-builder/fromHook.js'; +import { fromLevelManager } from './source-builder/fromLevelManager.js'; import { fromEgret } from './source-builder/fromNative.js'; import { fromSocket } from './source-builder/fromSocket.js'; @@ -16,8 +18,8 @@ export class SEAEventSource { private subscriptions = new Map(); private subscriptionId = 0; - constructor(EventSource$: Observable) { - this._source$ = EventSource$; + constructor(eventSource$: Observable) { + this._source$ = eventSource$; } on(handler: (data: T) => void) { @@ -45,4 +47,5 @@ export class SEAEventSource { static readonly hook = fromHook; static readonly egret = fromEgret; static readonly socket = fromSocket; + static readonly levelManger = fromLevelManager; } diff --git a/packages/core/event-source/source-builder/fromLevelManager.ts b/packages/core/event-source/source-builder/fromLevelManager.ts new file mode 100644 index 00000000..dcb9a87f --- /dev/null +++ b/packages/core/event-source/source-builder/fromLevelManager.ts @@ -0,0 +1,20 @@ +import { levelManager } from '../../battle/index.js'; +import { SEAEventSource } from '../EventSource.js'; + +type LevelManagerEvents = 'update' | 'nextAction' | 'log'; + +export function fromLevelManager(event: 'update'): SEAEventSource; +export function fromLevelManager(event: 'nextAction' | 'log'): SEAEventSource; + +export function fromLevelManager(event: LevelManagerEvents) { + switch (event) { + case 'update': + return new SEAEventSource(levelManager.update$); + case 'nextAction': + return new SEAEventSource(levelManager.nextAction$); + case 'log': + return new SEAEventSource(levelManager.log$); + default: + throw new Error(`Invalid type ${event as string}, type could only be 'update'`); + } +} diff --git a/packages/core/internal/core.ts b/packages/core/internal/core.ts index 571e4367..61e5cf80 100644 --- a/packages/core/internal/core.ts +++ b/packages/core/internal/core.ts @@ -4,7 +4,7 @@ import { SEAEventSource } from '../event-source/index.js'; import { fixSoundLoad } from './features/fixSoundLoad.js'; import { coreSetup } from './features/index.js'; -const VERSION = '1.0.0-rc.6'; +const VERSION = '1.0.0-rc.7'; const SEER_READY_EVENT = 'seerh5_ready'; export interface SetupFn { @@ -18,7 +18,6 @@ export class SEAC { readonly version = VERSION; private loadCalled = false; - private setupFns: SetupFn[] = []; private setup(type: SetupFn['type']) { this.setupFns .filter(({ type: _type }) => _type === type) @@ -50,6 +49,8 @@ export class SEAC { } } + public setupFns: SetupFn[] = []; + addSetupFn(type: SetupFn['type'], fn: SetupFn['setup']) { this.setupFns.push({ type, setup: fn }); } diff --git a/packages/core/internal/features/registerHooks.ts b/packages/core/internal/features/registerHooks.ts index 48dd6d66..7315c884 100644 --- a/packages/core/internal/features/registerHooks.ts +++ b/packages/core/internal/features/registerHooks.ts @@ -9,9 +9,7 @@ export default () => { ModuleManager.loadScript = wrapper(ModuleManager.loadScript).after((_, script) => { resolve(script); }); - return () => { - restoreHookedFn(ModuleManager, 'loadScript'); - }; + return () => restoreHookedFn(ModuleManager, 'loadScript'); }); HookPointRegistry.register('module:openMainPanel', (resolve) => { @@ -19,9 +17,7 @@ export default () => { await this.openPanel(this._mainPanelName); resolve({ module: this.moduleName, panel: this._mainPanelName }); }); - return () => { - restoreHookedFn(BasicMultPanelModule.prototype, 'onShowMainPanel'); - }; + return () => restoreHookedFn(BasicMultPanelModule.prototype, 'onShowMainPanel'); }); HookPointRegistry.register('module:show', (resolve) => { @@ -34,9 +30,7 @@ export default () => { resolve({ module, moduleInstance: currModule }); } }); - return () => { - restoreHookedFn(ModuleManager, 'beginShow'); - }; + return () => restoreHookedFn(ModuleManager, 'beginShow'); }); HookPointRegistry.register('battle:showEndProp', (resolve) => { @@ -51,9 +45,7 @@ export default () => { map(() => void null) ) .subscribe(resolve); - return () => { - subscription.unsubscribe(); - }; + return () => subscription.unsubscribe(); }); HookPointRegistry.register('module:destroy', (resolve) => { @@ -67,18 +59,14 @@ export default () => { delete this._modules[key]; } }); - return () => { - restoreHookedFn(ModuleManager, 'removeModuleInstance'); - }; + return () => restoreHookedFn(ModuleManager, 'removeModuleInstance'); }); HookPointRegistry.register('pop_view:open', (resolve) => { PopViewManager.prototype.openView = wrapper(PopViewManager.prototype.openView).after((r, view) => { resolve((Object.getPrototypeOf(view) as WithClass).__class__); }); - return () => { - restoreHookedFn(PopViewManager.prototype, 'openView'); - }; + return () => restoreHookedFn(PopViewManager.prototype, 'openView'); }); HookPointRegistry.register('pop_view:close', (resolve) => { @@ -96,9 +84,7 @@ export default () => { resolve((Object.getPrototypeOf(popView) as WithClass).__class__); } }); - return () => { - restoreHookedFn(PopViewManager.prototype, 'hideView'); - }; + return () => restoreHookedFn(PopViewManager.prototype, 'hideView'); }); HookPointRegistry.register('award:show', (resolve) => { @@ -110,18 +96,14 @@ export default () => { await delay(500); this.destroy(); }); - return () => { - restoreHookedFn(AwardItemDialog.prototype, 'startEvent'); - }; + return () => restoreHookedFn(AwardItemDialog.prototype, 'startEvent'); }); HookPointRegistry.register('award:receive', (resolve) => { AwardManager.showDialog = wrapper(AwardManager.showDialog).after((_, _dialog, items) => { resolve({ items }); }); - return () => { - restoreHookedFn(AwardManager, 'showDialog'); - }; + return () => restoreHookedFn(AwardManager, 'showDialog'); }); HookPointRegistry.register('battle:roundEnd', (resolve) => { @@ -136,9 +118,7 @@ export default () => { }; playerMode.nextRound = wrapper(playerMode.nextRound.bind(playerMode)).after(resolve); }); - return () => { - restoreHookedFn(PetFightController, 'onStartFight'); - }; + return () => restoreHookedFn(PetFightController, 'onStartFight'); }); HookPointRegistry.register('battle:start', (resolve) => { @@ -172,18 +152,14 @@ export default () => { HookPointRegistry.register('battle:end', (resolve) => { EventManager.addEventListener(PetFightEvent.ALARM_CLICK, resolve, null); - return () => { - EventManager.removeEventListener(PetFightEvent.ALARM_CLICK, resolve, null); - }; + return () => EventManager.removeEventListener(PetFightEvent.ALARM_CLICK, resolve, null); }); HookPointRegistry.register('socket:send', (resolve) => { SocketConnection.mainSocket.send = wrapper(SocketConnection.mainSocket.send).before((cmd, data) => { resolve({ cmd, data }); }); - return () => { - restoreHookedFn(SocketConnection.mainSocket, 'send'); - }; + return () => restoreHookedFn(SocketConnection.mainSocket, 'send'); }); HookPointRegistry.register('socket:receive', (resolve) => { @@ -192,8 +168,6 @@ export default () => { resolve({ cmd, buffer }); } ); - return () => { - restoreHookedFn(SocketConnection.mainSocket, 'dispatchCmd'); - }; + return () => restoreHookedFn(SocketConnection.mainSocket, 'dispatchCmd'); }); }; diff --git a/packages/core/package.json b/packages/core/package.json index d08915de..b182f9f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,7 @@ "name": "@sea/core", "description": "SEA 核心", "type": "module", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "license": "MPL-2.0", "private": true, "exports": { diff --git a/packages/core/pet-helper/PetLocation.ts b/packages/core/pet-helper/PetLocation.ts index 437bfd72..1672c49f 100644 --- a/packages/core/pet-helper/PetLocation.ts +++ b/packages/core/pet-helper/PetLocation.ts @@ -90,17 +90,13 @@ export const LocationTransformTable: { Storage: { async Default(ct) { if (PetManager.isBagFull) return false; - return new Promise((res) => { - PetManager.storageToBag(ct, res); - }) + return new Promise((res) => PetManager.storageToBag(ct, res)) .then(() => PetManager.setDefault(ct)) .then(() => true); }, async Bag(ct) { if (PetManager.isBagFull) return false; - return new Promise((res) => { - PetManager.storageToBag(ct, res); - }).then(() => true); + return new Promise((res) => PetManager.storageToBag(ct, res)).then(() => true); }, async SecondBag(ct) { if (PetManager.isSecondBagFull) return false; diff --git a/packages/core/pet-helper/PetStore.ts b/packages/core/pet-helper/PetStore.ts index df39a3c8..52261a72 100644 --- a/packages/core/pet-helper/PetStore.ts +++ b/packages/core/pet-helper/PetStore.ts @@ -39,25 +39,11 @@ class SEAPetStore { this.bag.deactivate(); }); - SEAEventSource.socket(CommandID.ADD_LOVE_PET, 'receive').on(() => { - this.miniInfo.deactivate(); - }); - - SEAEventSource.socket(CommandID.DEL_LOVE_PET, 'receive').on(() => { - this.miniInfo.deactivate(); - }); - - SEAEventSource.socket(CommandID.PET_CURE, 'receive').on(() => { - this.bag.deactivate(); - }); - - SEAEventSource.socket(CommandID.PET_ONE_CURE, 'receive').on(() => { - this.bag.deactivate(); - }); - - SEAEventSource.socket(CommandID.USE_PET_ITEM_OUT_OF_FIGHT, 'send').on(() => { - this.bag.deactivate(); - }); + SEAEventSource.socket(CommandID.ADD_LOVE_PET, 'receive').on(() => this.miniInfo.deactivate()); + SEAEventSource.socket(CommandID.DEL_LOVE_PET, 'receive').on(() => this.miniInfo.deactivate()); + SEAEventSource.socket(CommandID.PET_CURE, 'receive').on(() => this.bag.deactivate()); + SEAEventSource.socket(CommandID.PET_ONE_CURE, 'receive').on(() => this.bag.deactivate()); + SEAEventSource.socket(CommandID.USE_PET_ITEM_OUT_OF_FIGHT, 'send').on(() => this.bag.deactivate()); SEAEventSource.socket(42019, 'send').on((data) => { if (Array.isArray(data) && data.length === 2 && data[0] === 22439) { @@ -80,9 +66,7 @@ class SEAPetStore { this.bag = new CacheData( [PetManager.infos.map((p) => new CaughtPet(p)), PetManager.secondInfos.map((p) => new CaughtPet(p))], - () => { - PetManager.updateBagInfo(); - } + () => PetManager.updateBagInfo() ); const updateMiniInfo = () => { diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 83ac69c4..b3720528 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -2,7 +2,7 @@ "name": "@sea/launcher", "private": true, "type": "module", - "version": "0.7.0", + "version": "0.8.0", "main": "./src/index.tsx", "scripts": { "dev": "vite --open", @@ -22,7 +22,9 @@ "@sea/core": "workspace:*", "@trpc/client": "11.0.0-rc.417", "@trpc/server": "11.0.0-rc.417", + "@vue/reactivity": "^3.4.31", "chalk": "^5.3.0", + "dequal": "^2.0.3", "immer": "^10.1.1", "nanoclamp": "^2.0.10", "notistack": "^3.0.1", diff --git a/packages/launcher/src/builtin/petFragment/petFragment.ts b/packages/launcher/src/builtin/petFragment/index.ts similarity index 55% rename from packages/launcher/src/builtin/petFragment/petFragment.ts rename to packages/launcher/src/builtin/petFragment/index.ts index dbb34443..6bd3067a 100644 --- a/packages/launcher/src/builtin/petFragment/petFragment.ts +++ b/packages/launcher/src/builtin/petFragment/index.ts @@ -5,47 +5,30 @@ import { PetFragmentLevel, delay, engine, - socket, - type IPFLevelBoss + socket } from '@sea/core'; -import type { - LevelMeta, - LevelData as SEALevelData, - SEAModContext, - SEAModExport, - SEAModMetadata, - TaskRunner -} from '@sea/mod-type'; -import type { IPetFragmentRunner, PetFragmentOption } from './types'; - -interface LevelData extends SEALevelData { - pieces: number; - failedTimes: number; - curDifficulty: Difficulty; - curPosition: number; - isChallenge: boolean; - bosses: IPFLevelBoss[]; -} +import type { LevelMeta, SEAModContext, SEAModExport, SEAModMetadata } from '@sea/mod-type'; +import { task } from '@sea/mod-type'; +import type { IPetFragmentRunner, PetFragmentLevelData, PetFragmentOption } from './types'; + +export const PET_FRAGMENT_LEVEL_ID = 'PetFragmentLevel'; export const metadata = { - id: 'PetFragmentLevel', + id: PET_FRAGMENT_LEVEL_ID, scope: MOD_SCOPE_BUILTIN, version: VERSION, - description: '精灵因子' + description: '精灵因子', + data: [] as PetFragmentOption[] } satisfies SEAModMetadata; -export default function ({ logger, meta }: SEAModContext) { - class PetFragmentRunner implements TaskRunner, IPetFragmentRunner { - static readonly meta: LevelMeta = { - maxTimes: 3, - name: '精灵因子', - id: meta.id - }; - - get meta() { - return PetFragmentRunner.meta; - } +export default function ({ logger, battle }: SEAModContext) { + const taskMetadata: LevelMeta = { + maxTimes: 3, + name: '精灵因子', + id: PET_FRAGMENT_LEVEL_ID + }; + class PetFragmentRunner implements IPetFragmentRunner { get name() { return `精灵因子-${this.frag.name}`; } @@ -53,32 +36,28 @@ export default function ({ logger, meta }: SEAModContext) { readonly designId: number; readonly frag: PetFragmentLevel; - data: LevelData; + data: PetFragmentLevelData; logger = logger; - constructor(public option: PetFragmentOption) { - this.option = option; - this.option.battle = this.option.battle.map((strategy) => { - const beforeBattle = async () => { - await delay(Math.round(Math.random() * 1000) + 5000); - return strategy.beforeBattle?.(); - }; - return { ...strategy, beforeBattle }; - }); + constructor(public options: PetFragmentOption) { + const LevelObj = engine.getPetFragmentLevelObj(options.id); - const LevelObj: seerh5.PetFragmentLevelObj = config.xml - .getAnyRes('new_super_design') - .Root.Design.find((r: seerh5.PetFragmentLevelObj) => r.ID === option.id); + if (!LevelObj) throw new Error(`未找到精灵因子关卡: ${options.id}`); this.frag = new PetFragmentLevel(LevelObj); this.designId = this.frag.id; - this.data = {} as LevelData; + this.data = {} as PetFragmentLevelData; this.logger = logger.bind(logger, this.name); } selectLevelBattle() { - return this.option.battle.at(this.data.curPosition)!; + const battleStrategy = battle(this.options.battle.at(this.data.curPosition)!); + const beforeBattle = async () => { + await delay(Math.round(Math.random() * 1000) + 5000); + return battleStrategy.beforeBattle?.(); + }; + return { ...battleStrategy, beforeBattle }; } async update() { @@ -87,14 +66,14 @@ export default function ({ logger, meta }: SEAModContext) { data.pieces = await engine.itemNum(frag.petFragmentItem); - data.remainingTimes = this.meta.maxTimes - values[0]; + data.remainingTimes = taskMetadata.maxTimes - values[0]; data.failedTimes = values[1]; data.curDifficulty = (values[2] >> 8) & 255; - if (data.curDifficulty === Difficulty.NotSelected && this.option.difficulty) { - data.curDifficulty = this.option.difficulty; + if (data.curDifficulty === Difficulty.NotSelected && this.options.difficulty) { + data.curDifficulty = this.options.difficulty; } data.curPosition = values[2] >> 16; - data.isChallenge = data.curDifficulty !== 0 && data.curPosition !== 0; + data.isChallenge = data.curDifficulty !== Difficulty.NotSelected && data.curPosition !== 0; data.progress = (data.curPosition / 5) * 100; switch (data.curDifficulty) { @@ -112,10 +91,10 @@ export default function ({ logger, meta }: SEAModContext) { } } - next(): string { + next() { const data = this.data; if (data.isChallenge || data.remainingTimes > 0) { - if (this.option.sweep) { + if (this.options.sweep) { return 'sweep'; } else { return LevelAction.BATTLE; @@ -124,7 +103,7 @@ export default function ({ logger, meta }: SEAModContext) { return LevelAction.STOP; } - readonly actions: Record Promise> = { + readonly actions = { sweep: async () => { await socket.sendByQueue(41283, [this.designId, 4 + this.data.curDifficulty]); this.logger('执行一次扫荡'); @@ -133,7 +112,7 @@ export default function ({ logger, meta }: SEAModContext) { const checkData = await socket.sendByQueue(41284, [this.designId, this.data.curDifficulty]); const check = new DataView(checkData!).getUint32(0); if (check === 0) { - socket.sendByQueue(41282, [this.designId, this.data.curDifficulty]); + await socket.sendByQueue(41282, [this.designId, this.data.curDifficulty]); } else { const err = `出战情况不合法: ${check}`; BubblerManager.getInstance().showText(err); @@ -143,7 +122,16 @@ export default function ({ logger, meta }: SEAModContext) { }; } + const tasks = [ + task({ + metadata: taskMetadata, + runner(_, options) { + return new PetFragmentRunner(options as unknown as PetFragmentOption); + } + }) + ]; + return { - tasks: [PetFragmentRunner] - } satisfies SEAModExport; + tasks + } as SEAModExport; } diff --git a/packages/launcher/src/builtin/petFragment/types.ts b/packages/launcher/src/builtin/petFragment/types.ts index 326dd33d..5858cce8 100644 --- a/packages/launcher/src/builtin/petFragment/types.ts +++ b/packages/launcher/src/builtin/petFragment/types.ts @@ -1,16 +1,25 @@ -import type { PetFragmentLevelDifficulty as Difficulty, LevelBattle, PetFragmentLevel } from '@sea/core'; +import type { PetFragmentLevelDifficulty as Difficulty, IPFLevelBoss, PetFragmentLevel } from '@sea/core'; +import type { LevelData, LevelMeta, Task } from '@sea/mod-type'; export interface PetFragmentOption { id: number; - difficulty: Difficulty; + difficulty: number; sweep: boolean; - battle: LevelBattle[]; + battle: string[]; } -export type PetFragmentOptionRaw = Omit & { battle: string[] }; - -export interface IPetFragmentRunner { +export interface IPetFragmentRunner + extends ReturnType['runner']> { readonly designId: number; readonly frag: PetFragmentLevel; - option: PetFragmentOption; + options: PetFragmentOption; +} + +export interface PetFragmentLevelData extends LevelData { + pieces: number; + failedTimes: number; + curDifficulty: Difficulty; + curPosition: number; + isChallenge: boolean; + bosses: IPFLevelBoss[]; } diff --git a/packages/launcher/src/builtin/realm/LevelCourageTower.ts b/packages/launcher/src/builtin/realm/LevelCourageTower.ts index d3e9ef9c..ea6c5fec 100644 --- a/packages/launcher/src/builtin/realm/LevelCourageTower.ts +++ b/packages/launcher/src/builtin/realm/LevelCourageTower.ts @@ -11,7 +11,7 @@ export interface Data extends LevelData { export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => task({ - meta: { + metadata: { name: '勇者之塔', maxTimes: 5, id: 'LevelCourageTower' diff --git a/packages/launcher/src/builtin/realm/LevelElfKingsTrial.ts b/packages/launcher/src/builtin/realm/LevelElfKingsTrial.ts index 0c53dddc..47dd16cf 100644 --- a/packages/launcher/src/builtin/realm/LevelElfKingsTrial.ts +++ b/packages/launcher/src/builtin/realm/LevelElfKingsTrial.ts @@ -52,7 +52,7 @@ export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => { } satisfies SEAConfigSchema; return task({ - meta, + metadata: meta, configSchema, runner: (meta, options) => { const elfKingId = Number(options.elfKingId); diff --git a/packages/launcher/src/builtin/realm/LevelExpTraining.ts b/packages/launcher/src/builtin/realm/LevelExpTraining.ts index 6cb60a07..ebdb301f 100644 --- a/packages/launcher/src/builtin/realm/LevelExpTraining.ts +++ b/packages/launcher/src/builtin/realm/LevelExpTraining.ts @@ -11,7 +11,7 @@ export interface Data extends LevelData { export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => task({ - meta: { + metadata: { name: '经验训练场', maxTimes: 6, id: 'LevelExpTraining' diff --git a/packages/launcher/src/builtin/realm/LevelStudyTraining.ts b/packages/launcher/src/builtin/realm/LevelStudyTraining.ts index 99a19e80..4aa903c0 100644 --- a/packages/launcher/src/builtin/realm/LevelStudyTraining.ts +++ b/packages/launcher/src/builtin/realm/LevelStudyTraining.ts @@ -11,7 +11,7 @@ export interface Data extends LevelData { export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => task({ - meta: { + metadata: { name: '学习力训练场', maxTimes: 6, id: 'LevelStudyTraining' diff --git a/packages/launcher/src/builtin/realm/LevelTitanHole.ts b/packages/launcher/src/builtin/realm/LevelTitanHole.ts index 8a9e56d0..2e050b5b 100644 --- a/packages/launcher/src/builtin/realm/LevelTitanHole.ts +++ b/packages/launcher/src/builtin/realm/LevelTitanHole.ts @@ -19,7 +19,7 @@ export interface Meta extends LevelMeta { export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => task({ - meta: { + metadata: { id: 'LevelTitanHole', name: '泰坦矿洞', maxTimes: 2, @@ -109,7 +109,7 @@ export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => await socket.sendByQueue(42395, [104, 1, 3, 0]); }, async sweep() { - // TODO + await socket.sendByQueue(42395, [104, 6, 3, 0]); } } }) diff --git a/packages/launcher/src/builtin/realm/LevelXTeamRoom.ts b/packages/launcher/src/builtin/realm/LevelXTeamRoom.ts index c596139b..a57b5c32 100644 --- a/packages/launcher/src/builtin/realm/LevelXTeamRoom.ts +++ b/packages/launcher/src/builtin/realm/LevelXTeamRoom.ts @@ -14,7 +14,7 @@ export interface Data extends LevelData { export default (logger: AnyFunction, battle: (name: string) => LevelBattle) => task({ - meta: { + metadata: { id: 'LevelXTeamRoom', name: 'X战队密室', maxTimes: 3 diff --git a/packages/launcher/src/builtin/realm/index.ts b/packages/launcher/src/builtin/realm/index.ts index e932c015..acc327b7 100644 --- a/packages/launcher/src/builtin/realm/index.ts +++ b/packages/launcher/src/builtin/realm/index.ts @@ -13,8 +13,7 @@ export const metadata = { id: 'realm', scope: MOD_SCOPE_BUILTIN, version: VERSION, - description: '日常关卡', - configSchema: {} + description: '日常关卡' } satisfies SEAModMetadata; export default function realm({ logger, battle }: SEAModContext) { @@ -27,7 +26,7 @@ export default function realm({ logger, battle }: SEAModContext LevelTitanHole(logger, battle), LevelXTeamRoom(logger, battle), task({ - meta: { id: 'Test', name: '测试', maxTimes: 1 }, + metadata: { id: 'Test', name: '测试', maxTimes: 1 }, runner() { return { data: { @@ -39,18 +38,18 @@ export default function realm({ logger, battle }: SEAModContext return battle(''); }, async update() { - await Promise.resolve(this.data.customField + '1'); + await Promise.resolve(); + this.data.progress += 33; }, logger: NOOP, next() { - if (this.data.progress === 3) { + if (this.data.progress >= 99) { return 'stop'; } return 'run'; }, actions: { async run() { - this.data.customField = 'custom'; await delay(3000); } } diff --git a/packages/launcher/src/components/ListMenu.tsx b/packages/launcher/src/components/ListMenu.tsx index 641374a6..eaf4a12d 100644 --- a/packages/launcher/src/components/ListMenu.tsx +++ b/packages/launcher/src/components/ListMenu.tsx @@ -1,4 +1,4 @@ -import { useCachedReturn } from '@/utils/hooks/useCachedReturn'; +import { useCachedReturn } from '@/shared/hooks'; import { Menu, MenuItem, alpha, type MenuItemProps, type MenuProps } from '@mui/material'; import React from 'react'; @@ -59,8 +59,8 @@ export function ListMenu(props: ListMenuProps) { maxHeight: '50vh', overflowY: 'auto', bgcolor: (theme) => alpha(theme.palette.secondary.main, 0.88), - ...MenuListSx, - }, + ...MenuListSx + } }} open={open} onClose={handleCloseMenu} diff --git a/packages/launcher/src/components/PanelTable/PanelTable.tsx b/packages/launcher/src/components/PanelTable/PanelTable.tsx index 27c68575..2243ead0 100644 --- a/packages/launcher/src/components/PanelTable/PanelTable.tsx +++ b/packages/launcher/src/components/PanelTable/PanelTable.tsx @@ -2,7 +2,7 @@ import type { TableCellProps, TableProps } from '@mui/material'; import { Table, TableBody, TableCell, TableHead, TableRow, alpha } from '@mui/material'; import React from 'react'; -import { useCachedReturn } from '@/utils/hooks/useCachedReturn'; +import { useCachedReturn } from '@/shared/hooks'; import { RowDataContext, RowIndexContext } from './usePanelTableData'; export type PanelColumns = Array<{ field: string; columnName: string } & TableCellProps>; @@ -20,9 +20,7 @@ export function PanelTable(props: PanelTableProps) { const { toRowKey, data, columns, rowElement, ...tableProps } = props; const keyRef = useCachedReturn(data, toRowKey, (r, data) => r ?? JSON.stringify(data)); - const dataRef = useCachedReturn(data, undefined, (r, data, index) => { - return { data, index }; - }); + const dataRef = useCachedReturn(data, undefined, (r, data, index) => ({ data, index })); return ( diff --git a/packages/launcher/src/constants.ts b/packages/launcher/src/constants.ts index 9662a980..5e7824eb 100644 --- a/packages/launcher/src/constants.ts +++ b/packages/launcher/src/constants.ts @@ -1,6 +1,6 @@ import type { seac } from '@sea/core'; -export const VERSION = '0.7.0'; +export const VERSION = '0.8.0'; export const CORE_VERSION: typeof seac.version = '1.0.0-rc.6'; export const IS_DEV = import.meta.env.DEV; export const CMD_MASK = [ @@ -17,15 +17,17 @@ export const CMD_MASK = [ export const MOD_SCOPE_DEFAULT = 'external'; export const MOD_SCOPE_BUILTIN = 'builtin'; -export const DS = { +export const QueryKey = { multiValue: { - autoCure: 'swr://multiValue/autoCure', - battleFire: 'swr://multiValue/battleFire', - eyeEquipment: 'swr://multiValue/eyeEquipment', - title: 'swr://multiValue/title', - suit: 'swr://multiValue/suit' + autoCure: 'game://multiValue/autoCure', + battleFire: 'game://multiValue/battleFire', + eyeEquipment: 'game://multiValue/eyeEquipment', + title: 'game://multiValue/title', + suit: 'game://multiValue/suit' }, - petBag: 'ds://PetBag' + petBag: 'db://PetBag', + taskConfig: 'db://taskConfig', + taskIsCompleted: 'task://isCompleted' }; export const MOD_BUILTIN_UPDATE_STRATEGY: 'always' | 'never' | 'version' = 'always'; diff --git a/packages/launcher/src/context/TaskSchedulerProvider.tsx b/packages/launcher/src/context/TaskSchedulerProvider.tsx index f598d1b8..0d22bca3 100644 --- a/packages/launcher/src/context/TaskSchedulerProvider.tsx +++ b/packages/launcher/src/context/TaskSchedulerProvider.tsx @@ -1,33 +1,41 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { levelManager, SEAEventSource, Subscription } from '@sea/core'; -import type { TaskRunner } from '@sea/mod-type'; +import { dateTime2hhmmss } from '@/shared'; +import { LevelAction, levelManager, SEAEventSource, Subscription } from '@sea/core'; +import type { LevelData, Task } from '@sea/mod-type'; import { produce } from 'immer'; -import React, { useCallback, useEffect, useReducer, type PropsWithChildren, type Reducer } from 'react'; +import React, { useCallback, useEffect, useReducer, useState, type PropsWithChildren, type Reducer } from 'react'; import { TaskScheduler, type TaskState } from './useTaskScheduler'; -type ActionType = { - enqueue: { runner: TaskRunner }; - dequeue: { runner: TaskRunner }; +interface ActionType { + enqueue: { task: Task; runnerId: number; options?: Record }; + dequeue: { runnerId: number }; moveNext: undefined; changeRunnerStatus: { status: TaskState['status']; error?: Error }; changeSchedulerStatus: { status: LevelSchedulerState['status'] }; addRunnerBattleCount: undefined; - logWithRunner: { message: string }; + addRunnerLog: { message: string }; + updateRunnerData: { data: LevelData }; setPause: boolean; -}; +} -type Action = { +interface Action { type: keyof ActionType; payload?: unknown; -}; +} type LevelSchedulerState = Pick; -const reducer: Reducer = (state, { type, payload }) => { - function enqueue(prev: LevelSchedulerState, payload: ActionType['enqueue']) { - return produce(prev, (state) => { +const reducer: Reducer = (prev, { type, payload }) => + produce(prev, (state) => { + function enqueue({ task, options, runnerId }: ActionType['enqueue']) { const { queue } = state; - queue.push({ runner: payload.runner, status: 'pending', logs: [], battleCount: 0 }); + queue.push({ + status: 'pending', + logs: [], + battleCount: 0, + options, + task, + runnerId + }); if (state.currentIndex == undefined) { for (let i = 0; i < queue.length; i++) { if (queue[i].status === 'pending') { @@ -36,13 +44,11 @@ const reducer: Reducer = (state, { type, payload }) } } } - }); - } + } - function dequeue(prev: LevelSchedulerState, payload: ActionType['dequeue']) { - return produce(prev, (state) => { + function dequeue(payload: ActionType['dequeue']) { const { queue } = state; - const index = queue.findIndex((item) => item.runner === payload.runner); + const index = queue.findIndex((item) => item.runnerId === payload.runnerId); if (index !== -1) { if (state.currentIndex && index < state.currentIndex) { state.currentIndex = state.currentIndex - 1; @@ -52,11 +58,9 @@ const reducer: Reducer = (state, { type, payload }) state.currentIndex = undefined; } } - }); - } + } - function moveNext(prev: LevelSchedulerState) { - return produce(prev, (state) => { + function moveNext() { if (state.currentIndex == undefined) { return; } @@ -64,11 +68,9 @@ const reducer: Reducer = (state, { type, payload }) if (state.currentIndex >= state.queue.length) { state.currentIndex = undefined; } - }); - } + } - function changeRunnerStatus(prev: LevelSchedulerState, payload: ActionType['changeRunnerStatus']) { - return produce(prev, (state) => { + function changeRunnerState(payload: ActionType['changeRunnerStatus']) { const { queue, currentIndex } = state; if (currentIndex != undefined) { const item = queue[currentIndex]; @@ -83,127 +85,151 @@ const reducer: Reducer = (state, { type, payload }) item.endTime = Date.now(); } } - }); - } + } - function addRunnerBattleCount(prev: LevelSchedulerState) { - return produce(prev, (state) => { + function addRunnerBattleCount() { const { queue, currentIndex } = state; if (currentIndex != undefined) { const item = queue[currentIndex]; item.battleCount++; } - }); - } + } - function logWithRunner(prev: LevelSchedulerState, payload: ActionType['logWithRunner']) { - return produce(prev, (state) => { + function addRunnerLog(payload: ActionType['addRunnerLog']) { const { queue, currentIndex } = state; if (currentIndex != undefined) { const item = queue[currentIndex]; - item.logs.push(payload.message); + item.logs.push(`[${dateTime2hhmmss.format(new Date())}] ${payload.message}`); } - }); - } + } - function changeSchedulerStatus(prev: LevelSchedulerState, payload: ActionType['changeSchedulerStatus']) { - return produce(prev, (state) => { + function changeSchedulerState(payload: ActionType['changeSchedulerStatus']) { state.status = payload.status; - }); - } + } + + function updateRunnerData(payload: ActionType['updateRunnerData']) { + const { queue, currentIndex } = state; + if (currentIndex != undefined) { + const item = queue[currentIndex]; + item.runnerData = JSON.parse(JSON.stringify(payload.data)) as LevelData; + } + } - function setPaused(prev: LevelSchedulerState, payload: ActionType['setPause']) { - return produce(prev, (state) => { + function setPaused(payload: ActionType['setPause']) { state.isPaused = payload; - }); - } + } - switch (type) { - case 'enqueue': - return enqueue(state, payload as ActionType[typeof type]); - case 'dequeue': - return dequeue(state, payload as ActionType[typeof type]); - case 'moveNext': - return moveNext(state); - case 'changeRunnerStatus': - return changeRunnerStatus(state, payload as ActionType[typeof type]); - case 'changeSchedulerStatus': - return changeSchedulerStatus(state, payload as ActionType[typeof type]); - case 'setPause': - return setPaused(state, payload as ActionType[typeof type]); - case 'addRunnerBattleCount': - return addRunnerBattleCount(state); - case 'logWithRunner': - return logWithRunner(state, payload as ActionType[typeof type]); - default: - return state; - } -}; + switch (type) { + case 'enqueue': + enqueue(payload as ActionType[typeof type]); + break; + case 'dequeue': + dequeue(payload as ActionType[typeof type]); + break; + case 'moveNext': + moveNext(); + break; + case 'changeRunnerStatus': + changeRunnerState(payload as ActionType[typeof type]); + break; + case 'changeSchedulerStatus': + changeSchedulerState(payload as ActionType[typeof type]); + break; + case 'setPause': + setPaused(payload as ActionType[typeof type]); + break; + case 'addRunnerBattleCount': + addRunnerBattleCount(); + break; + case 'addRunnerLog': + addRunnerLog(payload as ActionType[typeof type]); + break; + case 'updateRunnerData': + updateRunnerData(payload as ActionType[typeof type]); + break; + default: + break; + } + }); export const TaskSchedulerProvider = ({ children }: PropsWithChildren) => { + const [counter, setCounter] = useState(0); const [state, dispatch] = useReducer(reducer, { queue: [], status: 'ready', isPaused: false }); - // const updateRequest = useRef(false); const changeSchedulerState = useCallback((status: LevelSchedulerState['status']) => { dispatch({ type: 'changeSchedulerStatus', - payload: { status } as ActionType['changeSchedulerStatus'] + payload: { status } satisfies ActionType['changeSchedulerStatus'] }); }, []); const changeRunnerState = useCallback((status: TaskState['status'], error?: Error) => { dispatch({ type: 'changeRunnerStatus', - payload: { status, error } as ActionType['changeRunnerStatus'] + payload: { status, error } satisfies ActionType['changeRunnerStatus'] }); }, []); const tryStartNextRunner = useCallback(() => { const { isPaused, queue, status, currentIndex } = state; - console.log(`tryStartNextRunner`, levelManager.running, state); + console.log(`关卡调度器: [tryStartNextRunner]: `, levelManager.running, state); if (isPaused || currentIndex == undefined || status !== 'ready') { return; } if (levelManager.running) { - console.error(`关卡调度器: 不合理的State: ${state}`); + console.error(`关卡调度器: [tryStartNextRunner]: 不合理的State: ${JSON.stringify(state)}`); console.error(`LevelManger的上一次运行未释放`); } - changeRunnerState('running'); - changeSchedulerState('running'); - - const { runner } = queue[currentIndex]; + const { task, options } = queue[currentIndex]; + const metadata = task.metadata; + const runner = task.runner(metadata, options as undefined); + const sub = new Subscription(); - // TODO! 在后端日志功能完善后 重新设计该部分 - runner.logger = new Proxy(runner.logger, { - apply(target, thisArg, argArray) { - dispatch({ type: 'logWithRunner', payload: { message: argArray[0] } }); - return Reflect.apply(target, thisArg, argArray); + sub.on(SEAEventSource.levelManger('update'), () => { + dispatch({ + type: 'updateRunnerData', + payload: { data: runner.data } satisfies ActionType['updateRunnerData'] + }); + }); + sub.on(SEAEventSource.levelManger('nextAction'), (action) => { + if (action === 'battle') { + dispatch({ type: 'addRunnerBattleCount' }); } }); + sub.on(SEAEventSource.levelManger('log'), (message) => { + dispatch({ type: 'addRunnerLog', payload: { message } satisfies ActionType['addRunnerLog'] }); + }); levelManager.run(runner); - const { lock } = levelManager; + dispatch({ + type: 'updateRunnerData', + payload: { data: runner.data } + }); + changeRunnerState('running'); + changeSchedulerState('running'); + const { lock } = levelManager; lock! .then(() => { changeRunnerState('completed'); }) - .catch((e) => { - changeRunnerState('error', e); + .catch((e: unknown) => { + changeRunnerState('error', e as Error); }) - .finally(async () => { - try { - await levelManager.stop(); - } catch (e) { - console.error(e); + .finally(() => { + if (runner.next() === LevelAction.STOP) { + changeRunnerState('completed'); + } else { + changeRunnerState('stopped'); } + sub.dispose(); changeSchedulerState('ready'); dispatch({ type: 'moveNext' }); }); - }, [changeSchedulerState, changeRunnerState, state]); + }, [changeRunnerState, changeSchedulerState, state]); useEffect(() => { const { queue, isPaused, status, currentIndex } = state; @@ -215,59 +241,49 @@ export const TaskSchedulerProvider = ({ children }: PropsWithChildren) = } }); - useEffect(() => { - const sub = new Subscription(); - sub.on(SEAEventSource.hook('battle:start'), () => { - dispatch({ type: 'addRunnerBattleCount' }); - }); - return () => sub.dispose(); - }, []); - - const tryStopCurrentRunner = useCallback(() => { + const tryStopCurrentRunner = useCallback(async () => { if (state.status !== 'running') { return; } changeSchedulerState('waitingForStop'); - return levelManager - .stop() - .catch((err) => { - console.error(`停止关卡失败: ${err}`); - }) - .finally(() => { - changeSchedulerState('ready'); - changeRunnerState('stopped'); - }); - }, [changeRunnerState, changeSchedulerState, state.status]); - const handleEnqueue = useCallback((runner: TaskRunner) => { - dispatch({ type: 'enqueue', payload: { runner } }); - }, []); + return levelManager.stop().catch((e: unknown) => { + if (e instanceof Error) { + console.error(`停止关卡失败: ${e.message}`); + } else { + console.error(`停止关卡失败: ${JSON.stringify(e)}`); + } + }); + }, [changeSchedulerState, state.status]); + + const handleEnqueue = useCallback( + (task: Task, options?: Record) => { + const runnerId = counter; + dispatch({ type: 'enqueue', payload: { task, options, runnerId } satisfies ActionType['enqueue'] }); + setCounter((counter) => counter + 1); + }, + [counter] + ); const handleDequeue = useCallback( - async (runner: TaskRunner) => { - if (state.queue.findIndex((r) => r.runner === runner) === state.currentIndex) { + async (runnerId: number) => { + if (state.queue.findIndex((r) => r.runnerId === runnerId) === state.currentIndex) { await tryStopCurrentRunner(); - dispatch({ type: 'dequeue', payload: { runner } }); - } else { - dispatch({ type: 'dequeue', payload: { runner } }); } + dispatch({ type: 'dequeue', payload: { runnerId } satisfies ActionType['dequeue'] }); }, [state, tryStopCurrentRunner] ); - const handlePause = useCallback(async () => { + const handlePause = useCallback(() => { dispatch({ type: 'setPause', payload: true }); - await tryStopCurrentRunner(); + void tryStopCurrentRunner(); }, [tryStopCurrentRunner]); const handleResume = useCallback(() => { dispatch({ type: 'setPause', payload: false }); }, []); - const handleStopCurrentRunner = useCallback(async () => { - await tryStopCurrentRunner(); - }, [tryStopCurrentRunner]); - return ( ) = dequeue: handleDequeue, pause: handlePause, resume: handleResume, - stopCurrentRunner: handleStopCurrentRunner + stopCurrentRunner: tryStopCurrentRunner }} > {children} diff --git a/packages/launcher/src/context/useTaskScheduler.ts b/packages/launcher/src/context/useTaskScheduler.ts index d8642640..38e7496e 100644 --- a/packages/launcher/src/context/useTaskScheduler.ts +++ b/packages/launcher/src/context/useTaskScheduler.ts @@ -1,28 +1,32 @@ -import type { TaskRunner } from '@sea/mod-type'; +import type { Task } from '@sea/mod-type'; import { createContext, useContext } from 'react'; +export type TaskRunner = ReturnType; + export interface TaskState { status: 'pending' | 'running' | 'completed' | 'error' | 'stopped'; error?: Error; - runner: TaskRunner; + options?: Record; startTime?: number; endTime?: number; - // TODO logs: string[]; battleCount: number; + task: Task; + runnerId: number; + runnerData?: ReturnType['data']; } export interface TaskScheduler { - queue: Array; + queue: TaskState[]; currentIndex?: number; /** 注意不是LevelManager的状态, 而是调度器自身的, ready表示队列为空或者被暂停 */ status: 'ready' | 'running' | 'waitingForStop'; isPaused: boolean; - enqueue: (runner: TaskRunner) => void; - dequeue: (runner: TaskRunner) => void; + enqueue: (task: Task, options?: Record) => void; + dequeue: (runnerId: number) => void; pause: () => void; resume: () => void; - stopCurrentRunner: () => void; + stopCurrentRunner: () => Promise; } export const TaskScheduler = createContext({} as TaskScheduler); diff --git a/packages/launcher/src/features/engine.ts b/packages/launcher/src/features/engine.ts index 3e5e5902..1c3df747 100644 --- a/packages/launcher/src/features/engine.ts +++ b/packages/launcher/src/features/engine.ts @@ -1,5 +1,15 @@ import { engine, socket } from '@sea/core'; +declare let config: { + xml: { + getAnyRes: (name: 'new_super_design') => { + Root: { + Design: seerh5.PetFragmentLevelObj[]; + }; + }; + }; +}; + export function extendCoreEngine() { engine.extend({ async updateBattleFireInfo() { @@ -9,7 +19,7 @@ export function extendCoreEngine() { return { type: r[0], valid: r[1] > 0 && SystemTimerManager.time < r[1], - timeLeft: r[1] - SystemTimerManager.time, + timeLeft: r[1] - SystemTimerManager.time }; }, changeEquipment(type: Parameters[0], itemId: number) { @@ -17,5 +27,8 @@ export function extendCoreEngine() { MainManager.actorInfo.requestChangeClothes(type, itemId, resolve); }); }, + getPetFragmentLevelObj(id: number) { + return config.xml.getAnyRes('new_super_design').Root.Design.find((r) => r.ID === id); + } }); } diff --git a/packages/launcher/src/features/registerLog.ts b/packages/launcher/src/features/registerLog.ts index 9c18b78a..c08885f3 100644 --- a/packages/launcher/src/features/registerLog.ts +++ b/packages/launcher/src/features/registerLog.ts @@ -1,4 +1,4 @@ -import * as Logger from '@/utils/logger'; +import * as Logger from '@/shared/logger'; import { SEAEventSource, Subscription, wrapper } from '@sea/core'; export function registerLog() { diff --git a/packages/launcher/src/services/config/usePetGroups.ts b/packages/launcher/src/services/config/usePetGroups.ts index 513f5fce..e87ab6d2 100644 --- a/packages/launcher/src/services/config/usePetGroups.ts +++ b/packages/launcher/src/services/config/usePetGroups.ts @@ -1,6 +1,6 @@ -import { usePersistentConfig } from '@/utils/usePersistentConfig'; +import { usePersistentConfig } from '@/services/usePersistentConfig'; -const defaultData: number[][] = Array(7).fill((() => [])()); +const defaultData: number[][] = Array(7).fill((() => [] as number[])()); export function usePetGroups() { const { data: petGroups, isLoading, mutate } = usePersistentConfig('PetGroups', defaultData); diff --git a/packages/launcher/src/services/endpoints.ts b/packages/launcher/src/services/endpoints.ts index 12ecaa97..a5f1c94e 100644 --- a/packages/launcher/src/services/endpoints.ts +++ b/packages/launcher/src/services/endpoints.ts @@ -1,4 +1,3 @@ -import type { PetFragmentOptionRaw } from '@/builtin/petFragment/types'; import type { DataObject } from '@sea/mod-type'; import type { ApiRouter, ModInstallOptions } from '@sea/server'; import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; @@ -14,6 +13,8 @@ const trpcClient = createTRPCClient({ const { modRouter, configRouter } = trpcClient; +export const { task } = configRouter; + export const mod = { fetchList: async () => modRouter.modList.query(), data: async (scope: string, id: string) => modRouter.data.query({ scope, id }), @@ -53,22 +54,17 @@ export const mod = { }; export type ConfigKey = 'PetGroups'; -export const getConfig = async (key: ConfigKey) => configRouter.launcherConfig.query(key) as Promise; +export const getConfig = async (key: ConfigKey) => configRouter.launcher.item.query(key) as Promise; -export const setConfig = async (key: ConfigKey, value: unknown) => - configRouter.setLauncherConfig.mutate({ key, value }); +export const setConfig = async (key: ConfigKey, value: unknown) => configRouter.launcher.setItem.mutate({ key, value }); export async function queryCatchTime(name: string): Promise<[string, number]>; export async function queryCatchTime(): Promise>; export async function queryCatchTime(name?: string) { - return configRouter.allCatchTime.query(name); + return configRouter.catchTime.all.query(name); } +export const updateAllCatchTime = async (data: Map) => configRouter.catchTime.mutate.mutate(data); -export const updateAllCatchTime = async (data: Map) => configRouter.updateAllCatchTime.mutate(data); - -export const deleteCatchTime = async (name: string) => configRouter.deleteCatchTime.mutate(name); - -export const addCatchTime = async (name: string, time: number) => configRouter.setCatchTime.mutate([name, time]); +export const deleteCatchTime = async (name: string) => configRouter.catchTime.delete.mutate(name); -export const getPetFragmentConfig = async () => - configRouter.petFragmentLevel.query() as Promise; +export const addCatchTime = async (name: string, time: number) => configRouter.catchTime.set.mutate([name, time]); diff --git a/packages/launcher/src/services/mod/handler.ts b/packages/launcher/src/services/mod/handler.ts index 14678e00..0932a0bc 100644 --- a/packages/launcher/src/services/mod/handler.ts +++ b/packages/launcher/src/services/mod/handler.ts @@ -31,7 +31,7 @@ export async function createModContext(metadata: SEAModMetadata) { const data = await getModData(meta); const config = await getModConfig(meta); - return { meta, logger, ct, battle, ...data, ...config } as SEAModContext; + return { meta, logger, ct, battle, ...data, ...config } as SEAModContext; } class ModDeploymentHandler { @@ -73,7 +73,7 @@ class ModDeploymentHandler { }); break; case 'PetFragmentLevel': - await import('@/builtin/petFragment/petFragment').then(({ default: factory, metadata }) => { + await import('@/builtin/petFragment').then(({ default: factory, metadata }) => { this.factory = factory as ModFactory; this.metadata = buildMetadata(metadata); }); @@ -106,7 +106,7 @@ class ModDeploymentHandler { try { const context = await createModContext(this.metadata); const exports = await this.factory(context); - const ins = new ModInstance(this.metadata, exports); + const ins = new ModInstance(context, exports); store.set(namespace, ins); console.log(`模组: ${namespace} 部署成功`); } catch (err) { diff --git a/packages/launcher/src/services/mod/install.ts b/packages/launcher/src/services/mod/install.ts index aa34a2ee..e09fef61 100644 --- a/packages/launcher/src/services/mod/install.ts +++ b/packages/launcher/src/services/mod/install.ts @@ -1,10 +1,11 @@ import { MOD_BUILTIN_UPDATE_STRATEGY, MOD_SCOPE_BUILTIN } from '@/constants'; import * as endpoints from '@/services/endpoints'; -import { SEAModLogger } from '@/utils/logger'; +import { buildDefaultConfig } from '@/shared/index'; +import { SEAModLogger } from '@/shared/logger'; import type { SEAModMetadata } from '@sea/mod-type'; import type { ModInstallOptions } from '@sea/server'; -import { buildDefaultConfig, buildMetadata } from './metadata'; -import { ModMetadataSchema } from './schema'; +import { buildMetadata } from './metadata'; +import { ModMetadataSchema } from './schemas'; export async function prefetchModMetadata(url: string) { const modImport = (await import(/* @vite-ignore */ url)) as { metadata: SEAModMetadata }; @@ -38,7 +39,7 @@ export async function installBuiltinMods() { import('@/builtin/strategy'), import('@/builtin/battle'), import('@/builtin/realm'), - import('@/builtin/petFragment/petFragment'), + import('@/builtin/petFragment'), import('@/builtin/command') ]); diff --git a/packages/launcher/src/services/mod/metadata.ts b/packages/launcher/src/services/mod/metadata.ts index 1ad48534..893150b3 100644 --- a/packages/launcher/src/services/mod/metadata.ts +++ b/packages/launcher/src/services/mod/metadata.ts @@ -1,12 +1,12 @@ import { MOD_SCOPE_DEFAULT } from '@/constants'; -import type { SEAFormItemSchema, SEAModMetadata } from '@sea/mod-type'; +import type { SEAModMetadata } from '@sea/mod-type'; export type DefinedModMetadata = Required> & Pick; export function getNamespace(meta: SEAModMetadata) { const { scope, id } = meta; - return `${scope}::${id}`; + return `${scope ?? MOD_SCOPE_DEFAULT}::${id}`; } export function buildMetadata(metadata: SEAModMetadata) { @@ -19,13 +19,3 @@ export function buildMetadata(metadata: SEAModMetadata) { return { ...metadata, scope, version, preload, description } as DefinedModMetadata; } - -export function buildDefaultConfig(configSchema: Record) { - const keys = Object.keys(configSchema); - const defaultConfig: Record = {}; - keys.forEach((key) => { - const item = configSchema[key]; - defaultConfig[key] = item.default; - }); - return defaultConfig; -} diff --git a/packages/launcher/src/services/mod/schema.ts b/packages/launcher/src/services/mod/schemas.ts similarity index 67% rename from packages/launcher/src/services/mod/schema.ts rename to packages/launcher/src/services/mod/schemas.ts index c7600d36..4546ce3b 100644 --- a/packages/launcher/src/services/mod/schema.ts +++ b/packages/launcher/src/services/mod/schemas.ts @@ -1,3 +1,4 @@ +import type { DataObject } from '@sea/mod-type'; import { z } from 'zod'; const ConfigItemSchema = z @@ -11,12 +12,22 @@ const ConfigItemSchema = z ]) ); +const DateObjectSchema = z.custom( + (data) => + data !== null && + typeof data === 'object' && + (Object.getPrototypeOf(data) === Object.prototype || + Array.isArray(data) || + data instanceof Set || + data instanceof Map) +); + export const ModMetadataSchema = z.object({ id: z.string(), scope: z.string().optional(), version: z.string().optional(), description: z.string().optional(), preload: z.boolean().optional(), - data: z.object({}).optional(), + data: DateObjectSchema.optional(), configSchema: z.record(ConfigItemSchema).optional() }); diff --git a/packages/launcher/src/services/mod/utils.ts b/packages/launcher/src/services/mod/utils.ts index 99f91ac5..d39a482b 100644 --- a/packages/launcher/src/services/mod/utils.ts +++ b/packages/launcher/src/services/mod/utils.ts @@ -1,36 +1,12 @@ import * as endpoints from '@/services/endpoints'; -import { CommonLogger, LogStyle } from '@/utils/logger'; -import type { SEAModMetadata } from '@sea/mod-type'; -import { buildDefaultConfig } from './metadata'; +import { buildDefaultConfig } from '@/shared/index'; +import { CommonLogger, LogStyle } from '@/shared/logger'; +import { reactive } from '@vue/reactivity'; +import type { DefinedModMetadata } from './metadata'; -type ProxyObjectRef> = T & { ref: T }; -function createProxyObjectRef>(initValue: T) { - const observable = { - ref: initValue - } as ProxyObjectRef; - - const proxy = new Proxy(observable, { - get(target, prop, receiver) { - if (Object.hasOwn(target, prop)) { - return Reflect.get(target, prop, receiver); - } else { - return Reflect.get(target.ref, prop, receiver); - } - }, - set(target, prop, value, receiver) { - if (prop === 'ref') { - return Reflect.set(target, prop, value, receiver); - } else { - return Reflect.set(target.ref, prop, value, receiver); - } - } - }); - - return proxy; -} export const getLogger = (id: string) => CommonLogger(id, 'info', LogStyle.mod); -export async function getModData({ scope, id, data: defaultData }: SEAModMetadata & { scope: string }) { +export async function getModData({ scope, id, data: defaultData }: DefinedModMetadata) { if (defaultData) { let data = await endpoints.mod.data(scope, id); @@ -39,19 +15,13 @@ export async function getModData({ scope, id, data: defaultData }: SEAModMetadat data = defaultData; } - const proxyData = createProxyObjectRef(data); - - const mutate = (recipe: (draft: unknown) => void) => { - recipe(proxyData.ref); - void endpoints.mod.setData(scope, id, proxyData.ref); - }; - - return { data: proxyData, mutate }; + const proxyData = reactive(data); + return { data: proxyData }; } return {}; } -export async function getModConfig({ scope, id, configSchema }: SEAModMetadata & { scope: string }) { +export async function getModConfig({ scope, id, configSchema }: DefinedModMetadata) { if (configSchema) { let config; config = await endpoints.mod.config(scope, id); diff --git a/packages/launcher/src/services/store/mod.ts b/packages/launcher/src/services/store/mod.ts index ba82a19d..1711a48a 100644 --- a/packages/launcher/src/services/store/mod.ts +++ b/packages/launcher/src/services/store/mod.ts @@ -1,6 +1,9 @@ -import type { Battle, Command, SEAModExport, Strategy, Task } from '@sea/mod-type'; +import { debounce, type AnyFunction } from '@sea/core'; +import type { Battle, Command, SEAModContext, SEAModExport, Strategy, Task } from '@sea/mod-type'; +import { effect, stop, toRaw } from '@vue/reactivity'; -import { type AnyFunction } from '@sea/core'; +import { dequal } from 'dequal'; +import * as endpoints from '../endpoints'; import { getNamespace, type DefinedModMetadata } from '../mod/metadata'; import * as battleStore from './battle'; import * as commandStore from './command'; @@ -16,7 +19,7 @@ export class ModInstance { commands: string[] = []; constructor( - public meta: DefinedModMetadata, + public ctx: SEAModContext, { battles, commands, install, strategies, tasks, uninstall }: SEAModExport ) { // 加载导出的内容 @@ -28,11 +31,27 @@ export class ModInstance { // 执行副作用 install && install(); + // 声明data的模组需要注册响应式更新以及清理函数 + if (ctx.data) { + const mutate = debounce( + () => void endpoints.mod.setData(ctx.meta.scope, ctx.meta.id, toRaw(ctx.data)!), + 10000 + ); + const effectRunner = effect(() => { + dequal(ctx.data, toRaw(ctx.data)); + mutate(); + }); + + this.addFinalizer(() => { + stop(effectRunner); + }); + } + uninstall && this.addFinalizer(uninstall); } get namespace() { - return getNamespace(this.meta); + return getNamespace(this.ctx.meta); } addFinalizer(finalizer: AnyFunction) { @@ -74,7 +93,7 @@ export class ModInstance { tasks.forEach((task) => { taskStore.add(this.namespace, task); - this.tasks.push(task.meta.name); + this.tasks.push(task.metadata.name); }); this.finalizers.push(() => { @@ -110,16 +129,12 @@ export const store = new Map(); export function teardown() { store.forEach((mod) => { - if (mod.finalizers.length === 0) { - // need reload app - return; - } try { store.delete(mod.namespace); mod.dispose(); - console.log(`卸载模组: ${mod.namespace}`); + console.log(`撤销模组部署: ${mod.namespace}`); } catch (error) { - console.error(`模组卸载失败: ${mod.namespace}`, error); + console.error(`撤销模组部署失败: ${mod.namespace}`, error); } }); } diff --git a/packages/launcher/src/services/store/task.ts b/packages/launcher/src/services/store/task.ts index aca3dbfd..3b8c19ac 100644 --- a/packages/launcher/src/services/store/task.ts +++ b/packages/launcher/src/services/store/task.ts @@ -10,8 +10,8 @@ export interface TaskInstance { export const store = new Map(); export function add(mod: string, task: Task) { - const name = task.meta.name; - const id = task.meta.id; + const name = task.metadata.name; + const id = task.metadata.id; const instance: TaskInstance = { task: task, diff --git a/packages/launcher/src/services/useBagPets.ts b/packages/launcher/src/services/useBagPets.ts index 6641daa6..47bfada3 100644 --- a/packages/launcher/src/services/useBagPets.ts +++ b/packages/launcher/src/services/useBagPets.ts @@ -1,4 +1,4 @@ -import { DS } from '@/constants'; +import { QueryKey } from '@/constants'; import type { Pet } from '@sea/core'; import { SEAEventSource, SEAPetStore, Subscription, debounce } from '@sea/core'; import type { SWRSubscriptionOptions } from 'swr/subscription'; @@ -6,17 +6,21 @@ import useSWRSubscription from 'swr/subscription'; export function useBagPets() { const { data: pets } = useSWRSubscription( - DS.petBag, + QueryKey.petBag, (_, { next }: SWRSubscriptionOptions) => { const bagCache = SEAPetStore.bag; const updateBag = bagCache.get.bind(bagCache); - updateBag().then((pets) => next(null, pets[0])); + void updateBag().then((pets) => { + next(null, pets[0]); + }); const sub = new Subscription(); sub.on(SEAEventSource.hook('pet_bag:deactivate'), debounce(updateBag, 500)); sub.on(SEAEventSource.hook('pet_bag:update'), (pets) => { next(null, pets[0]); }); - return () => sub.dispose(); + return () => { + sub.dispose(); + }; }, { fallbackData: null diff --git a/packages/launcher/src/utils/usePersistentConfig.ts b/packages/launcher/src/services/usePersistentConfig.ts similarity index 62% rename from packages/launcher/src/utils/usePersistentConfig.ts rename to packages/launcher/src/services/usePersistentConfig.ts index f9aed467..68223ea8 100644 --- a/packages/launcher/src/utils/usePersistentConfig.ts +++ b/packages/launcher/src/services/usePersistentConfig.ts @@ -7,24 +7,16 @@ export function usePersistentConfig(key: endpoints.ConfigKey, initValue: T) { data, isLoading, mutate: persistenceMutate - } = useSWR( - key, - (key: endpoints.ConfigKey) => - endpoints.getConfig(key).then((v) => { - return (v as T) ?? initValue; - }), - { - fallbackData: initValue - } - ); + } = useSWR(key, (key: endpoints.ConfigKey) => endpoints.getConfig(key).then((v) => (v as T) ?? initValue), { + fallbackData: initValue + }); - const mutate = (recipe: (draft: Draft) => void) => { + const mutate = async (recipe: (draft: Draft) => void) => persistenceMutate(async () => { const nextData = produce(data, recipe); await endpoints.setConfig(key, nextData); return nextData; }); - }; return { data, isLoading, mutate }; } diff --git a/packages/launcher/src/services/useTaskConfig.ts b/packages/launcher/src/services/useTaskConfig.ts new file mode 100644 index 00000000..48576c4e --- /dev/null +++ b/packages/launcher/src/services/useTaskConfig.ts @@ -0,0 +1,13 @@ +import { QueryKey } from '@/constants'; +import useSWR from 'swr'; +import { task } from './endpoints'; + +export function useTaskConfig() { + const { data, error, isLoading } = useSWR([QueryKey.taskConfig, 'all'], () => task.all.query()); + + return { data, error, isLoading } as { + data: Awaited> | undefined; + error: unknown; + isLoading: boolean; + }; +} diff --git a/packages/launcher/src/utils/hooks/useCachedReturn.ts b/packages/launcher/src/shared/hooks.ts similarity index 100% rename from packages/launcher/src/utils/hooks/useCachedReturn.ts rename to packages/launcher/src/shared/hooks.ts diff --git a/packages/launcher/src/shared/index.ts b/packages/launcher/src/shared/index.ts new file mode 100644 index 00000000..13f35fab --- /dev/null +++ b/packages/launcher/src/shared/index.ts @@ -0,0 +1,57 @@ +import type { GetConfigObjectTypeFromSchema, LevelMeta, SEAConfigSchema, SEAFormItemSchema, Task } from '@sea/mod-type'; +import type { TaskConfigData } from '@sea/server'; + +export function buildDefaultConfig(configSchema: Record) { + const keys = Object.keys(configSchema); + const defaultConfig: Record = {}; + keys.forEach((key) => { + const item = configSchema[key]; + defaultConfig[key] = item.default; + }); + return defaultConfig; +} + +export function getTaskCurrentOptions(task: Task, taskConfig?: TaskConfigData | undefined): undefined; + +export function getTaskCurrentOptions( + task: Task, + taskConfig?: TaskConfigData | undefined +): GetConfigObjectTypeFromSchema; + +export function getTaskCurrentOptions( + task: Task, + taskConfig?: TaskConfigData | undefined +) { + if (task.configSchema == undefined) { + return undefined; + } + + // 声明配置项, 声明现有配置但没找到对应的配置 + if (taskConfig?.currentOptions !== undefined && !taskConfig.options.has(taskConfig.currentOptions)) { + throw new Error('未找到任务配置'); + } + + // 声明配置项, 未声明现有配置 + if (!taskConfig?.currentOptions) { + return buildDefaultConfig(task.configSchema); + } + + return taskConfig.options.get(taskConfig.currentOptions)!; +} + +export const time2mmss = (n: number) => { + n = Math.round(n / 1000); + if (Object.is(n, -0)) { + n = 0; + } + const format = Intl.NumberFormat(undefined, { + minimumIntegerDigits: 2 + }); + return `${format.format(Math.trunc(n / 60))}:${format.format(n % 60)}`; +}; + +export const dateTime2hhmmss = Intl.DateTimeFormat('zh-cn', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' +}); diff --git a/packages/launcher/src/utils/logger.ts b/packages/launcher/src/shared/logger.ts similarity index 100% rename from packages/launcher/src/utils/logger.ts rename to packages/launcher/src/shared/logger.ts diff --git a/packages/launcher/src/shared/types.ts b/packages/launcher/src/shared/types.ts new file mode 100644 index 00000000..304761e0 --- /dev/null +++ b/packages/launcher/src/shared/types.ts @@ -0,0 +1,3 @@ +import type { LevelMeta, SEAConfigSchema, Task } from '@sea/mod-type'; + +export type AnyTask = Task; diff --git a/packages/launcher/src/views/AutomationView/CommonLevelView.tsx b/packages/launcher/src/views/AutomationView/CommonLevelView.tsx index ece9e7f7..31bfddea 100644 --- a/packages/launcher/src/views/AutomationView/CommonLevelView.tsx +++ b/packages/launcher/src/views/AutomationView/CommonLevelView.tsx @@ -1,86 +1,76 @@ import { Box, Button, CircularProgress, Typography } from '@mui/material'; -import React, { useCallback, useEffect } from 'react'; -import useSWR from 'swr'; +import React, { useMemo } from 'react'; -import { PanelField, useIndex, useRowData } from '@/components/PanelTable'; +import { PanelField, useRowData } from '@/components/PanelTable'; import { PanelTable, type PanelColumns } from '@/components/PanelTable/PanelTable'; import { SeaTableRow } from '@/components/styled/TableRow'; -import { MOD_SCOPE_BUILTIN } from '@/constants'; +import { MOD_SCOPE_BUILTIN, QueryKey } from '@/constants'; import { useModStore } from '@/context/useModStore'; import { useTaskScheduler } from '@/context/useTaskScheduler'; +import { getNamespace as ns } from '@/services/mod/metadata'; +import type { TaskInstance } from '@/services/store/task'; +import { useTaskConfig } from '@/services/useTaskConfig'; +import { getTaskCurrentOptions } from '@/shared'; +import type { AnyTask } from '@/shared/types'; import { LevelAction } from '@sea/core'; -import type { Task } from '@sea/mod-type'; -import { produce } from 'immer'; - -// import * as Endpoints from '@/service/endpoints'; - -// const rows: Array = React.useMemo( -// () => [ -// { -// name: '泰坦矿洞', -// module: , -// async sweep() { -// await socket.sendByQueue(42395, [104, 6, 3, 0]); -// }, -// async getState() { -// const [count, step] = await socket.multiValue(18724, 18725); -// return count === 2 && step === 0; -// }, -// }, -// // { name: '作战实验室' -// { -// name: '六界神王殿', -// async sweep() { -// await socket.sendByQueue(45767, [38, 3]); -// return; -// }, -// async getState() { -// let state = true; -// const values = await socket.multiValue(11411, 11412, 11413, 11414); -// for (let i = 1; i <= 7; i++) { -// const group = Math.trunc((i - 1) / 2); -// const v = [values[group] & ((1 << 16) - 1), values[group] >> 16]; -// // console.log(v[(i - 1) % 2] & 15); -// if ((v[(i - 1) % 2] & 15) < 3) { -// state = false; -// break; -// } -// } -// return state; -// }, -// }, -// ], -// [running] -// ); +import type { TaskConfigData } from '@sea/server'; +import useSWR from 'swr'; + +// name: '作战实验室' +// name: '六界神王殿', +// async sweep() { +// await socket.sendByQueue(45767, [38, 3]); +// return; +// }, +// async getState() { +// let state = true; +// const values = await socket.multiValue(11411, 11412, 11413, 11414); +// for (let i = 1; i <= 7; i++) { +// const group = Math.trunc((i - 1) / 2); +// const v = [values[group] & ((1 << 16) - 1), values[group] >> 16]; +// // console.log(v[(i - 1) % 2] & 15); +// if ((v[(i - 1) % 2] & 15) < 3) { +// state = false; +// break; +// } +// } +// return state; +// }, +// }, interface Row { - taskClass: Task; - config?: Record; + taskInstance: TaskInstance; + config?: TaskConfigData; } +const toRowKey = (row: Row) => row.taskInstance.id; + export function CommonLevelView() { - const { taskStore: levelStore } = useModStore(); - const [taskCompleted, setTaskCompleted] = React.useState([]); + const { taskStore } = useModStore(); + const { data: taskConfig, isLoading, error } = useTaskConfig(); - const { - data: rows = [], - isLoading, - error - } = useSWR('ds://configs/level', async () => { - const r: Row[] = []; - levelStore.forEach((levelInstance) => { - if (levelInstance.ownerMod === `${MOD_SCOPE_BUILTIN}::PetFragmentLevel`) { - return; - } - if (Object.hasOwn(levelInstance.task.prototype, 'selectLevelBattle')) { - r.push({ taskClass: levelInstance.task }); - } - }); + const rows = useMemo( + () => + Array.from(taskStore.values()) + .filter(({ ownerMod, task: _task, id }) => { + if (!taskConfig) { + return false; + } + + if (ownerMod === ns({ scope: MOD_SCOPE_BUILTIN, id: 'PetFragmentLevel' })) { + return false; + } + + const task = _task as AnyTask; - return r; - }); + const runner = task.runner(task.metadata, getTaskCurrentOptions(task, taskConfig.get(id))); + return Boolean(runner.selectLevelBattle); + }) + .map((task) => ({ taskInstance: task, config: taskConfig?.get(task.id) }) as Row), + [taskConfig, taskStore] + ); - const col: PanelColumns = React.useMemo( + const col: PanelColumns = useMemo( () => [ { field: 'name', @@ -102,11 +92,9 @@ export function CommonLevelView() { [] ); - const toRowKey = useCallback((row: Row) => row.taskClass.meta.name, []); - - if (isLoading) + if (!taskConfig || isLoading) return ( - + 加载数据中 @@ -114,73 +102,56 @@ export function CommonLevelView() { if (error) { console.error(error); - return {String(error)}; + return ( + + {String(error)}; + + ); } return ( <> - } - /> + } /> ); } -interface PanelRowProps { - taskCompleted: boolean[]; - setTaskCompleted: React.Dispatch>; -} - -const PanelRow = React.memo(({ taskCompleted, setTaskCompleted }: PanelRowProps) => { +const PanelRow = React.memo(() => { const { enqueue } = useTaskScheduler(); - const { config, taskClass: levelClass } = useRowData(); - const runner = React.useMemo(() => new levelClass(config), [levelClass, config]); - const index = useIndex(); - const completed = taskCompleted[index]; - - runner.next = new Proxy(runner.next.bind(runner), { - apply(target, thisArg, argArray) { - const r = Reflect.apply(target, thisArg, argArray); - setTaskCompleted( - produce((draft) => { - draft[index] = r === LevelAction.STOP; - }) - ); - return r; + const { + taskInstance: { task, name: taskName }, + config + } = useRowData(); + + const currentOptions = useMemo(() => getTaskCurrentOptions(task as AnyTask, config), [task, config]); + + const { data: completed } = useSWR( + [QueryKey.taskIsCompleted, task], + async () => { + const runner = (task as AnyTask).runner(task.metadata, currentOptions); + await runner.update(); + return runner.next() === LevelAction.STOP; + }, + { + fallbackData: false } - }); - - useEffect(() => { - runner.update().then(() => runner.next()); - }, [runner]); + ); return ( - {levelClass.meta.name} + {taskName} {completed ? '已完成' : '未完成'} - - - {typeof config === 'object' && 'sweep' in config && ( - 扫荡: {config.sweep ? '开启' : '关闭'} - )} - + ); }); diff --git a/packages/launcher/src/views/AutomationView/DailySignView.tsx b/packages/launcher/src/views/AutomationView/DailySignView.tsx index bb126ed8..b2b352c2 100644 --- a/packages/launcher/src/views/AutomationView/DailySignView.tsx +++ b/packages/launcher/src/views/AutomationView/DailySignView.tsx @@ -1,18 +1,41 @@ import { PanelField, PanelTable, useRowData, type PanelColumns } from '@/components/PanelTable'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Button, ButtonGroup, CircularProgress } from '@mui/material'; import { SeaTableRow } from '@/components/styled/TableRow'; +import { MOD_SCOPE_BUILTIN } from '@/constants'; import { useModStore } from '@/context/useModStore'; +import { getNamespace as ns } from '@/services/mod/metadata'; import type { TaskInstance } from '@/services/store/task'; +import { useTaskConfig } from '@/services/useTaskConfig'; +import { getTaskCurrentOptions } from '@/shared'; +import type { AnyTask } from '@/shared/types'; import { LevelAction, delay } from '@sea/core'; -import type { TaskRunner } from '@sea/mod-type'; import useSWR from 'swr'; export function DailySignView() { const { taskStore } = useModStore(); - const signs = Array.from(taskStore.values()).filter((i) => !(i.task.prototype as TaskRunner).selectLevelBattle); + const { data: taskConfig } = useTaskConfig(); + + const signs = useMemo( + () => + Array.from(taskStore.values()).filter(({ ownerMod, task: _task, id }) => { + if (!taskConfig) { + return false; + } + + if (ownerMod === ns({ scope: MOD_SCOPE_BUILTIN, id: 'PetFragmentLevel' })) { + return false; + } + + const task = _task as AnyTask; + + const runner = task.runner(task.metadata, getTaskCurrentOptions(task, taskConfig.get(id))); + return !runner.selectLevelBattle; + }), + [taskConfig, taskStore] + ); const columns: PanelColumns = React.useMemo( () => [ @@ -50,14 +73,18 @@ export function DailySignView() { const PanelRow = () => { const ins = useRowData(); - const { ownerMod, name, task: taskClass } = ins; - const task = new taskClass(); + const { ownerMod, name, task: _task } = ins; + const task = _task as AnyTask; + const runner = task.runner(task.metadata, getTaskCurrentOptions(task)); const { data: state, mutate } = useSWR( `ds://mod/sign/${ownerMod}/${name}`, async () => { - await task.update(); - return { timesHaveRun: task.meta.maxTimes - task.data.remainingTimes, maxTimes: task.meta.maxTimes }; + await runner.update(); + return { + timesHaveRun: task.metadata.maxTimes - runner.data.remainingTimes, + maxTimes: task.metadata.maxTimes + }; }, { revalidateOnFocus: false, revalidateOnMount: true } ); @@ -75,10 +102,10 @@ const PanelRow = () => { onClick={() => { void (async () => { console.log(`正在执行${name}`); - await task.update(); - while (task.data.remainingTimes > 0) { - await task.actions[LevelAction.AWARD]?.call(task); - await delay(50).then(() => task.update()); + await runner.update(); + while (runner.data.remainingTimes > 0) { + await runner.actions[LevelAction.AWARD]?.call(task); + await delay(50).then(() => runner.update()); } await mutate(); })(); diff --git a/packages/launcher/src/views/AutomationView/PetFragmentLevelView.tsx b/packages/launcher/src/views/AutomationView/PetFragmentLevelView.tsx index 0333d8be..508a896f 100644 --- a/packages/launcher/src/views/AutomationView/PetFragmentLevelView.tsx +++ b/packages/launcher/src/views/AutomationView/PetFragmentLevelView.tsx @@ -1,67 +1,24 @@ -import { PanelField, useIndex, useRowData } from '@/components/PanelTable'; +import { PanelField, useRowData } from '@/components/PanelTable'; import { PanelTable, type PanelColumns } from '@/components/PanelTable/PanelTable'; import { SeaTableRow } from '@/components/styled/TableRow'; -import * as endpoints from '@/services/endpoints'; -import { Box, Button, CircularProgress, Typography } from '@mui/material'; -import { produce } from 'immer'; -import React, { useCallback, useEffect } from 'react'; -import useSWR from 'swr'; - -import { LevelAction } from '@sea/core'; +import { Box, Button, Typography } from '@mui/material'; +import React, { useMemo } from 'react'; -import type { IPetFragmentRunner, PetFragmentOption, PetFragmentOptionRaw } from '@/builtin/petFragment/types'; +import { PET_FRAGMENT_LEVEL_ID } from '@/builtin/petFragment'; +import type { IPetFragmentRunner, PetFragmentOption } from '@/builtin/petFragment/types'; +import { MOD_SCOPE_BUILTIN, QueryKey } from '@/constants'; import { useMainState } from '@/context/useMainState'; import { useModStore } from '@/context/useModStore'; import { useTaskScheduler } from '@/context/useTaskScheduler'; -import { store } from '@/services/store/battle'; -import type { TaskRunner } from '@sea/mod-type'; - -type RunnerInstance = IPetFragmentRunner & TaskRunner; +import { getNamespace as ns } from '@/services/mod/metadata'; +import { engine, LevelAction } from '@sea/core'; +import useSWR from 'swr'; -const loadOption = async (option: PetFragmentOptionRaw) => { - return { - ...option, - battle: option.battle.map((n) => { - const b = store.get(n); - if (b) { - return b.battle(); - } - throw new Error(`Battle ${n} not found`); - }) - } as PetFragmentOption; -}; +const toRowKey = (data: PetFragmentOption) => engine.getPetFragmentLevelObj(data.id)!.Desc; export function PetFragmentLevelView() { - const { taskStore: levelStore } = useModStore(); - - const PetFragmentRunner = levelStore.get('PetFragmentLevel')!; - const [taskCompleted, setTaskCompleted] = React.useState>([]); - - const { - data: rows = [], - isLoading, - error - } = useSWR('ds://configs/level/petFragment', async () => { - const allConfig = await endpoints.getPetFragmentConfig(); - const options = await Promise.all(allConfig.map(loadOption)); - return options.map((option) => new PetFragmentRunner.task(option) as RunnerInstance); - }); - - rows.forEach((runner, index) => { - runner.next = new Proxy(runner.next.bind(runner), { - apply(target, thisArg, argArray) { - const r = Reflect.apply(target, thisArg, argArray); - setTaskCompleted( - produce((draft) => { - draft[index] = r === LevelAction.STOP; - }) - ); - return r; - } - }); - }); - + const { modStore, taskStore } = useModStore(); const col: PanelColumns = React.useMemo( () => [ { @@ -84,60 +41,66 @@ export function PetFragmentLevelView() { [] ); - const toRowKey = useCallback((row: RunnerInstance) => row.frag.name, []); + const modIns = modStore.get(ns({ scope: MOD_SCOPE_BUILTIN, id: PET_FRAGMENT_LEVEL_ID })); + const taskIns = taskStore.get(PET_FRAGMENT_LEVEL_ID); + const optionsList = useMemo(() => (modIns?.ctx.data as undefined | PetFragmentOption[]) ?? [], [modIns?.ctx.data]); - if (isLoading) + if (!modIns || !taskIns) { return ( - - 加载数据中 - + + 未部署精灵因子模组 ); + } - if (error) { - console.error(error); - return {String(error)}; + const missingIds = optionsList.map(({ id }) => id).filter((id) => !engine.getPetFragmentLevelObj(id)); + if (missingIds.length > 0) { + return 错误的精灵因子ID: {missingIds.join(', ')}; } return ( <> - - } - /> + + + } /> ); } -interface PanelRowProps { - taskCompleted: boolean[]; -} - -const PanelRow = React.memo(({ taskCompleted }: PanelRowProps) => { +const PanelRow = React.memo(() => { const { setOpen } = useMainState(); const { enqueue } = useTaskScheduler(); - const runner = useRowData(); - const index = useIndex(); - const completed = taskCompleted[index]; + const { taskStore } = useModStore(); + const options = useRowData(); + const task = taskStore.get(PET_FRAGMENT_LEVEL_ID)!.task; + + const runner = useMemo( + () => task.runner(task.metadata, options as unknown as undefined) as IPetFragmentRunner, + [options, task] + ); - useEffect(() => { - runner.update().then(() => runner.next()); - }, [runner]); + const { data: completed } = useSWR( + [QueryKey.taskIsCompleted, task, options], + async () => { + await runner.update(); + return runner.next() === LevelAction.STOP; + }, + { + fallbackData: false + } + ); return ( - {runner.frag.name} + {runner.name} {completed ? '已完成' : '未完成'} - 扫荡: {runner.option.sweep ? '开启' : '关闭'} + 扫荡: {options.sweep ? '开启' : '关闭'}