diff --git a/packages-user/data-common/src/replay/array.ts b/packages-user/data-common/src/replay/array.ts index cd4fb3c..989e85c 100644 --- a/packages-user/data-common/src/replay/array.ts +++ b/packages-user/data-common/src/replay/array.ts @@ -2,11 +2,13 @@ import { logger } from '@motajs/common'; import { IReplayArray, IReplayArrayConfig, + IReplayArraySave, IReplayReadStream, IReplayStepHandler, ReplayCommandWidth, ReplayParamValue } from './types'; +import { ISaveableContent } from '../save'; interface INormalizedParam { /** @@ -44,7 +46,9 @@ interface IDecodedCommand { readonly paramCount: number; } -export class ReplayArray implements IReplayArray { +export class ReplayArray + implements IReplayArray, ISaveableContent +{ length: number = 0; commandWidth: ReplayCommandWidth = ReplayCommandWidth.Uint8; @@ -705,6 +709,9 @@ export class ReplayArray implements IReplayArray { expired: false, read: () => { + if (stream.expired) { + logger.warn(155); + } if (stream.index >= this.length) return null; const { command, paramCount } = this.decodeCommand(currCommand); const params = this.decodeParamList(currParam, paramCount); @@ -787,4 +794,30 @@ export class ReplayArray implements IReplayArray { getParamArray(): ArrayBuffer { return this.paramBuffer; } + + //#region 存读档 + + saveState(): IReplayArraySave { + return { + commands: this.commandBuffer, + params: this.paramBuffer, + metadata: { + commandWidth: this.commandWidth + } + }; + } + + loadState(state: IReplayArraySave): void { + this.commandBuffer = state.commands; + this.paramBuffer = state.params; + this.commandWidth = state.metadata.commandWidth; + + this.commandView = new DataView(this.commandBuffer); + this.paramView = new DataView(this.paramBuffer); + this.commandArray = new Uint8Array(this.commandBuffer); + this.paramArray = new Uint8Array(this.paramBuffer); + this.indexArray = new Uint32Array(this.indexBuffer); + + this.expireStreams(); + } } diff --git a/packages-user/data-common/src/replay/func.ts b/packages-user/data-common/src/replay/func.ts new file mode 100644 index 0000000..ad36036 --- /dev/null +++ b/packages-user/data-common/src/replay/func.ts @@ -0,0 +1,183 @@ +import { logger } from '@motajs/common'; +import { IReplaySystem } from './types'; + +interface IReplaySafetyCollection { + /** 这个安全对象(由 `@shouldReplay()` 触发)的名称,即传入给 `@shouldReplay` 的参数 */ + readonly name: string; + /** 这个安全对象所包含的所有字对象 */ + readonly messages: IReplaySafetyCollection[]; +} + +interface IReplaySafetyDetailQueue { + /** 详细信息代码 */ + readonly code: number; + /** 此详细信息对应的安全对象 */ + readonly collection: IReplaySafetyCollection; +} + +type ReplayDecorator = < + Return, + This, + Func extends (this: This, ...args: any[]) => Return +>( + method: Func, + context: ClassMethodDecoratorContext +) => (this: This, ...args: any[]) => Return; + +/** 本次收集使用的录像系统 */ +let replaySystem: IReplaySystem | null = null; +/** 当前是否正在进行录像收集 */ +let collecting: boolean = false; +/** 收集之前录像系统的录像长度 */ +let beforeLength: number = 0; +/** 收集时是否有 `@ignoreReplay` 调用 */ +let shouldIgnore: boolean = false; + +/** 录像安全收集根对象 */ +const collection: IReplaySafetyCollection = { + name: 'root', + messages: [] +}; +/** 当前录像安全收集对象 */ +let currentCollection: IReplaySafetyCollection | null = null; + +/** 详细信息 code */ +let detailCode = 0; +/** 录像收集详细信息队列,保留 50 个以确保可以在控制台重复输出 */ +const detailQueue: IReplaySafetyDetailQueue[] = []; + +/** + * 开始录像安全性检查收集,一般情况下不需要手动调用此接口 + * @param system 录像系统对象 + */ +export function beginReplaySafetyCollection(system: IReplaySystem): void { + if (collecting || replaySystem) { + logger.warn(159); + return; + } + replaySystem = system; + collecting = true; + beforeLength = system.route.length; + collection.messages.length = 0; + currentCollection = collection; +} + +/** + * 结束录像安全性检查收集,并输出收集结果 + */ +export function endReplaySafetyCollection(): void { + if (!collecting || !replaySystem) { + logger.warn(160); + return; + } + if (shouldIgnore) return; + if (replaySystem.route.length > beforeLength) return; + if (collection.messages.length === 0) return; + + // 需要把收集内容输出,这里只输出一层,完整输出需要在控制台手动调用 + const code = detailCode++; + const col: IReplaySafetyCollection = { + name: `detail#${code}`, + messages: collection.messages.slice() + }; + const command = `Mota.require('@user/data-common').logReplaySafetyDetail(${code});`; + const simplified = [...collection.messages] + .sort((a, b) => b.messages.length - a.messages.length) + .map(v => v.name) + .join('\n'); + logger.warn(161, command, simplified); + + while (detailQueue.length > 50) { + detailQueue.shift(); + } + detailQueue.push({ + code, + collection: col + }); +} + +/** + * 输出指定的录像安全对象的所有详细信息 + * @param collection 录像安全对象 + */ +function logCollection(collection: IReplaySafetyCollection): void { + console.group(collection.name); + const sorted = collection.messages.toSorted( + (a, b) => b.messages.length - a.messages.length + ); + for (const msg of sorted) { + if (msg.messages.length === 0) { + console.log(msg.name); + } else { + logCollection(msg); + } + } + console.groupEnd(); +} + +/** + * 输出某次录像安全检查的详细信息 + * @param code 录像安全检查对象的代码 + */ +export function logReplaySafetyDetail(code: number): void { + const root = detailQueue.find(v => v.code === code); + if (!root) { + logger.warn(162, code.toString()); + return; + } + logCollection(root.collection); +} + +/** + * 将一个类的方法标记为需要计入录像 + * @param message 如果录像安全检查出现问题,那么会使用此作为信息输出 + */ +export function shouldReplay(message: string): ReplayDecorator { + return function < + Return, + This, + Args extends any[], + Func extends (this: This, ...args: Args) => Return + >( + this: This, + method: Func, + context: ClassMethodDecoratorContext + ): (this: This, ...args: Args) => Return { + return function (this: This, ...args: Args): Return { + const before = currentCollection; + if (!before) return method.apply(this, args); + + const exist = before.messages.find(v => v.name === message); + const newCollection: IReplaySafetyCollection = exist ?? { + name: `${String(context.name)}: ${message}`, + messages: [] + }; + if (!exist) { + before.messages.push(newCollection); + } + + currentCollection = newCollection; + const result = method.apply(this, args); + currentCollection = before; + return result; + }; + }; +} + +/** + * 将一个类的方法标记为忽略录像安全检查,一般用于异步内容等不是立刻会触发状态修改的方法, + * 不要使用此方法来规避控制台的录像错误警告,否则很可能导致录像出错。 + */ +export function ignoreReplay(): ReplayDecorator { + return function < + Return, + This, + Args extends any[], + Func extends (this: This, ...args: Args) => Return + >(this: This, method: Func): (this: This, ...args: Args) => Return { + return function (this: This, ...args: Args): Return { + shouldIgnore = true; + return method.apply(this, args); + }; + }; +} diff --git a/packages-user/data-common/src/replay/index.ts b/packages-user/data-common/src/replay/index.ts index faa2756..7e40945 100644 --- a/packages-user/data-common/src/replay/index.ts +++ b/packages-user/data-common/src/replay/index.ts @@ -1,2 +1,3 @@ export * from './array'; +export * from './sandbox'; export * from './types'; diff --git a/packages-user/data-common/src/replay/sandbox.ts b/packages-user/data-common/src/replay/sandbox.ts new file mode 100644 index 0000000..92973dd --- /dev/null +++ b/packages-user/data-common/src/replay/sandbox.ts @@ -0,0 +1,146 @@ +import { + Hookable, + HookController, + IHookController, + logger +} from '@motajs/common'; +import { + IReplayArray, + IReplayReadStream, + IReplaySandbox, + IReplaySandboxHook, + IReplaySystem +} from './types'; + +export class ReplaySandbox + extends Hookable + implements IReplaySandbox +{ + pausing: boolean = true; + speed: number = 1; + ended: boolean = false; + playing: boolean = false; + + /** 下一步是否需要暂停播放 */ + private needPause: boolean = false; + /** 当前录像是否播放完毕 */ + private ending: boolean = false; + /** 暂停 `Promise` 的 `resolve` 函数 */ + private pauseResolve: () => void = () => {}; + + /** 录像的流式读取器 */ + private reader: Readonly; + + constructor( + readonly route: IReplayArray, + readonly system: IReplaySystem, + startIndex: number + ) { + super(); + this.reader = route.createReadStream(startIndex); + } + + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } + + setSpeed(speed: number): void { + this.speed = speed; + this.forEachHook(hook => hook.onSpeedSet?.(speed)); + } + + getReplayed(): number { + return this.reader.index; + } + + /** + * 开始录像重播循环 + */ + private async startReplayLoop() { + if (this.ended) return; + if (this.reader.expired) { + logger.warn(156); + return; + } + while (true) { + if (this.needPause && !this.pausing) { + this.pausing = true; + this.needPause = false; + this.pauseResolve(); + this.forEachHook(hook => hook.onPauseReplay?.()); + break; + } + if (this.reader.expired) { + logger.warn(156); + break; + } + const success = await this.step(); + if (!success) break; + } + if (this.ending) { + this.ended = true; + } + } + + play(): void { + if (this.playing || this.ended) return; + this.pausing = false; + this.playing = true; + this.forEachHook(hook => hook.onStartReplay?.()); + this.startReplayLoop(); + } + + pause(): Promise { + if (this.pausing || this.ended || !this.playing) { + return Promise.resolve(); + } + const { promise, resolve } = Promise.withResolvers(); + this.pauseResolve = resolve; + this.needPause = true; + return promise; + } + + resume(): void { + if (!this.pausing || !this.playing || this.ended) return; + this.pausing = false; + this.forEachHook(hook => hook.onResumeReplay?.()); + this.startReplayLoop(); + } + + async stop(): Promise { + if (this.pausing || this.ended || !this.playing) return; + await this.pause(); + this.forEachHook(hook => hook.onStopReplay?.()); + } + + async step(): Promise { + if (!this.pausing || !this.playing || this.ended) return false; + if (this.reader.expired) { + logger.warn(156); + return false; + } + const next = this.reader.read(); + if (!next) { + this.ending = true; + return false; + } + const command = this.system.getCommand(next.command); + if (!command) { + logger.warn(157, next.command.toString()); + return false; + } + const success = await command.execute(next); + if (!success) { + logger.warn( + 158, + next.command.toString(), + JSON.stringify(next.params) + ); + return false; + } + await Promise.all(this.forEachHook(hook => hook.onStep?.(next))); + return true; + } +} diff --git a/packages-user/data-common/src/replay/system.ts b/packages-user/data-common/src/replay/system.ts new file mode 100644 index 0000000..f12793a --- /dev/null +++ b/packages-user/data-common/src/replay/system.ts @@ -0,0 +1,87 @@ +import { + Hookable, + HookController, + IHookController, + logger +} from '@motajs/common'; +import { + IReplayArray, + IReplayCommand, + IReplaySandbox, + IReplaySandboxConfig, + IReplaySystem, + IReplaySystemHook, + ReplayCommandWidth, + ReplayParamValue +} from './types'; +import { ReplayArray } from './array'; +import { ReplaySandbox } from './sandbox'; + +export class ReplaySystem + extends Hookable + implements IReplaySystem +{ + replaying: boolean = false; + sandbox: IReplaySandbox | null = null; + route: IReplayArray; + + /** 所有注册的指令 */ + private readonly commands: Map = new Map(); + + constructor() { + super(); + this.route = new ReplayArray({ + initCommandLength: 1000, + initParamLength: 10000, + commandExpandMultiplier: 1.2, + paramExpandMultiplier: 1.2, + commandWidth: ReplayCommandWidth.Uint8, + commandMaxLength: 1_000_000, + paramMaxLength: 10_000_000 + }); + } + + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } + + registerCommand(code: number, command: IReplayCommand): void { + if (this.commands.has(code)) { + logger.warn(163); + return; + } + this.commands.set(code, command); + } + + getCommand(code: number): IReplayCommand | null { + return this.commands.get(code) ?? null; + } + + record(code: number, ...params: ReplayParamValue[]): void { + this.route.add(code, params); + this.forEachHook(hook => + hook.onRecordCommand?.(code, this.route.length, params) + ); + } + + createReplaySandbox( + config: Readonly + ): IReplaySandbox { + config.reseter.reset(config.save); + const sandbox = new ReplaySandbox( + config.route, + this, + config.startIndex ?? 0 + ); + this.sandbox = sandbox; + this.forEachHook(hook => hook.onCreateSandbox?.(sandbox)); + return sandbox; + } + + releaseSandbox(): void { + this.sandbox?.stop(); + this.sandbox = null; + } +} diff --git a/packages-user/data-common/src/replay/types.ts b/packages-user/data-common/src/replay/types.ts index 51525dc..8b4e0a2 100644 --- a/packages-user/data-common/src/replay/types.ts +++ b/packages-user/data-common/src/replay/types.ts @@ -1,5 +1,4 @@ import { IHookable, IHookBase } from '@motajs/common'; -import { ISaveableContent } from '../save'; export type ReplayParamValue = number | string | boolean | bigint; @@ -16,8 +15,9 @@ export interface IReplayCommand { /** * 执行当前录像步对应的操作逻辑 * @param step 当前录像步信息 + * @returns 当此录像步执行完毕时兑现的 `Promise`,兑现值表示此录像步是否播放成功 */ - execute(step: IReplayStepHandler): Promise; + execute(step: IReplayStepHandler): Promise; } export interface IReplaySandboxHook extends IHookBase { @@ -57,8 +57,12 @@ export interface IReplaySandboxHook extends IHookBase { export interface IReplaySandbox extends IHookable { /** 是否处于暂停状态 */ readonly pausing: boolean; + /** 当前录像是否已经播放完毕 */ + readonly ended: boolean; /** 播放倍率,1 为正常速度 */ readonly speed: number; + /** 当前是否正在播放,当调用 `play` 后,调用 `stop` 前,此值会是 `true` */ + readonly playing: boolean; /** * 设置播放倍率 @@ -66,6 +70,11 @@ export interface IReplaySandbox extends IHookable { */ setSpeed(speed: number): void; + /** + * 获取已经播放过的录像步数量,不包括当前正在播放的录像步 + */ + getReplayed(): number; + /** * 开始连续播放 */ @@ -88,8 +97,9 @@ export interface IReplaySandbox extends IHookable { /** * 单步播放一步,返回的 Promise 在该步渲染完成后兑现 + * @returns 当前步是否正常播放完成 */ - step(): Promise; + step(): Promise; } export interface IStateReseter { @@ -108,7 +118,7 @@ export const enum ReplayCommandWidth { } export interface IReplayReadStream { - /** 当前读取位置的步索引 */ + /** 已经读取过的录像步数量,即上一次调用 `read` 时读取的录像步索引 */ index: number; /** 录像总步数 */ length: number; @@ -144,6 +154,38 @@ export interface IReplayArrayConfig { paramMaxLength: number; } +export interface IReplayMetadataSave { + /** 当前录像使用的指令码宽度 */ + readonly commandWidth: ReplayCommandWidth; +} + +export interface IReplayArraySave { + /** + * 指令数组,在 Uint8 位宽下,两个字节为一组,第一个字节为参数数量,第二个字节为指令标识。 + * 在 Uint16 位宽下,三个字节为一组,第一个字节为参数数量,后两个字节组成的 uint16 为指令标识。 + */ + readonly commands: ArrayBuffer; + + /** + * 参数数组,由参数类型和参数值组成。参数类型占据一个字节,参数值根据类型不同占据不同的字节。 + * 参数类型列表(包含参数类型字节): + * + * - 0: boolean --- 2 Byte + * - 1: int8 --- 2 Byte + * - 2: int16 --- 3 Byte + * - 3: int32 --- 5 Byte + * - 4: int64 --- 9 Byte + * - 5: float --- 9 Byte + * - 6: bigint --- n + 1 Byte, 其中 n 是 bigint 的字节数 + * - 7: string --- n + 1 Byte, 其中 n 是字符串编码后的字节数 + * - 8 ~ 255: n - 7 长度的字符串 --- n + 1 Byte, 其中 n 是字符串编码后的字节数 + */ + readonly params: ArrayBuffer; + + /** 录像元数据 */ + readonly metadata: IReplayMetadataSave; +} + export interface IReplayArray { /** 录像中的总步数 */ readonly length: number; @@ -260,40 +302,18 @@ export interface IReplaySystemHook extends IHookBase { ): void; } -export interface IReplayMetadataSave { - /** 当前录像使用的指令码宽度 */ - readonly commandWidth: ReplayCommandWidth; +export interface IReplaySandboxConfig { + /** 录像播放沙箱使用的录像数组 */ + route: IReplayArray; + /** 录像播放前进行的状态重置使用的状态重置对象 */ + reseter: IStateReseter; + /** 录像播放的起始索引,默认为 0 */ + startIndex?: number; + /** 初始化使用的存档对象 */ + save?: Map; } -export interface IReplaySystemSave { - /** - * 指令数组,在 Uint8 位宽下,两个字节为一组,第一个字节为参数数量,第二个字节为指令标识。 - * 在 Uint16 位宽下,三个字节为一组,第一个字节为参数数量,后两个字节组成的 uint16 为指令标识。 - */ - readonly commands: ArrayBuffer; - - /** - * 参数数组,由参数类型和参数值组成。参数类型占据一个字节,参数值根据类型不同占据不同的字节。 - * 参数类型列表: - * - * - 0: boolean --- 2 Byte - * - 1: int8 --- 2 Byte - * - 2: int16 --- 3 Byte - * - 3: int32 --- 5 Byte - * - 4: int64 --- 9 Byte - * - 5: float --- 9 Byte - * - 6: bigint --- n + 1 Byte, 其中 n 是 bigint 的字节数 - * - 7: string --- n + 1 Byte, 其中 n 是字符串编码后的字节数 - * - 8 ~ 255: n - 7 长度的字符串 --- n + 1 Byte, 其中 n 是字符串编码后的字节数 - */ - readonly params: ArrayBuffer; - - /** 录像元数据 */ - readonly metadata: IReplayMetadataSave; -} - -export interface IReplaySystem - extends ISaveableContent, IHookable { +export interface IReplaySystem extends IHookable { /** 当前是否处在录像播放状态 */ readonly replaying: boolean; /** 当前正在播放的录像沙箱实例 */ @@ -303,27 +323,29 @@ export interface IReplaySystem /** * 注册一个录像命令 - * @param id 命令的唯一标识 + * @param code 命令的唯一标识 * @param command 命令对应的执行对象 */ - registerCommand(id: number, command: IReplayCommand): void; + registerCommand(code: number, command: IReplayCommand): void; + + /** + * 根据录像命令标识符获取命令对象 + * @param code 命令标识符 + */ + getCommand(code: number): IReplayCommand | null; /** * 向录像末尾追加一条录像步 - * @param id 命令标识 + * @param code 命令标识 * @param params 可变数量的录像参数 */ - record(id: number, ...params: ReplayParamValue[]): void; + record(code: number, ...params: ReplayParamValue[]): void; /** * 创建一个录像播放沙箱 - * @param reseter 状态重置器,用于在播放前重置游戏状态 - * @param save 可选,已加载的存档数据,用于从存档状态继续播放 + * @param config 录像播放沙箱的配置对象 */ - createReplaySandbox( - reseter: IStateReseter, - save?: Map - ): IReplaySandbox; + createReplaySandbox(config: Readonly): IReplaySandbox; /** * 释放当前活跃的沙箱 diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 226b52a..a1026d3 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -214,6 +214,15 @@ "151": "0 or 1 is expected for boolean replay array, but got $1.", "152": "Bigint replay param reached its limit, min for -(2n ** 2047n), and max for 2n ** 2047n - 1n.", "153": "Up to 255 parameters for a single replay command, but got $1. Overflowed parameters will be ignored.", - "154": "Cannot transfer replay command $1 from uint16 to uint8, since it's greater than 255." + "154": "Cannot transfer replay command $1 from uint16 to uint8, since it's greater than 255.", + "155": "Replay read stream expired, ignore this warning if you do need read expired replay data, or check your replay reading logic.", + "156": "Replay needs a not expired read stream, please recreate a new replay sandbox with a latest read stream.", + "157": "Replay error: Unknown replay command $1.", + "158": "Replay error: Replay step execute failed. Command code: $1, params: $2.", + "159": "Replay safety collection cannot begin while another collection is running.", + "160": "Replay safety collection ending function is called outside replay safety collection unexpectedly.", + "161": "Replay safety detected. Followings are simplified detect info. To get detailed info, run: '$1'.\n$2", + "162": "Cannot find replay safety detail info of code $1. The detail info may expired, or it just has not been existed.", + "163": "Replay command code $1 has already been registered. This will not replace the old registry." } } diff --git a/prompt.md b/prompt.md deleted file mode 100644 index 6c62a50..0000000 --- a/prompt.md +++ /dev/null @@ -1,193 +0,0 @@ -# 核心定位 - -你的身份不是架构设计者,而是辅助我开发项目与设计系统的起草者。实现功能永远排在最后一名——保证代码结构清晰、便于阅读、尽量减少代码跳转才是最核心的任务。哪怕实现不了功能,也要保证代码结构清晰。你的任务是尽量写出大致正确的代码以减少我的思考负担,而非追求完美。尝试一次性写出让我满意的完美代码往往会适得其反。 - -该项目的架构已经过大量设计与验证,当前架构是深思熟虑的结果。除非我明确要求,否则不要重新设计架构,也不要主动优化已有设计。你的职责是在保持整体风格与架构稳定的前提下,完成需求并自然扩展已有系统。 - -# 规则优先级 - -本文为 AI Agent 专用提示词,我有时可以不遵守,你**必须**遵守。 - -当多种规则来源产生冲突时,按以下优先级裁定: - -1. **已有代码 > 本文**:当前文件中的已有实现是最高优先级规范。当 prompt、已有代码、自身知识冲突时,优先遵循已有代码。 -2. **不确定时保守**:如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。 -3. **本文 > dev.md**:`dev.md` 中的规则在本文中强度升级: - -| dev.md 原文级别 | 本文对应级别 | -| ------------------------- | ------------ | -| 「一般不建议 / 尽量避免」 | **绝对禁止** | -| 「建议 / 最好」 | **必须遵守** | - -典型升级项——**绝对禁止**:`as` 关键字、`@ts-expect-error`、`getter` 和 `setter`、三元运算符不允许换行。 - -# 代码规则 - -## 修改原则 - -1. **最小修改**:默认不重新设计已有代码,不重构已有实现,不为「更优雅」而改变已有模式。新代码应是原有代码的自然延续。若认为已有代码存在错误,在对话中提出,不要直接修改。 -2. **文件边界**:修改文件前务必重读以确保内容最新。修改后必须在对话中说明所有改动的文件及具体内容。不得修改文档「涉及文件」节未提及的文件。所有公共方法必须在文档中提及,不允许在代码中新增未在文档中写明的公共方法。 -3. **歧义提问**:遇到任何模糊、不清晰、歧义的地方,立即向我提问,不要自行假设。若完成需求所需的接口尚不存在,立即停止实现并提出疑问,不要擅自新增接口。 - -## 代码组织 - -4. **功能聚合**:以「连续阅读一个功能所需的认知切换最少」为目标组织代码。相似功能放在一起,不以修饰符类型分隔。长文件或功能较多的类使用 `#region` 分区,按方法功能划分。 -5. **维护成本优先**:允许局部重复、允许多几个变量与 if 分支。绝不鼓励:为减少行数增加抽象、为减少重复增加函数跳转、为追求「现代 TS」使用复杂语法。 -6. **文件分配**:一个文件只能包含一个类。若存在多个同类型简单实现类(均 < 50 行且 implements 同一接口),必须先向我确认,同意后方可放在同一文件。 -7. **条件语句**:对于同一个等级的条件,如果满足与不满足都需要执行某些代码,那么必须写完成的 `if - else`,不得在 `if` 里面通过 `return` 提前返回来达成 `else` 效果。 - -## 注释 - -8. **私有成员必须注释**:所有私有方法与私有成员必须添加 jsDoc 注释,私有方法的参数必须添加注释说明。构造函数的属性声明参数除外。 -9. **公共注释唯一**:受保护与公共成员、方法必须在**源头处**添加注释(多数情况为 `interface`)。继承而来的成员,除非功能或描述有变化,否则不得重复添加注释。 -10. **方法注释换行**:方法 jsDoc 必须使用换行风格;成员 jsDoc 简短时可用不换行风格。 - -## 类型与错误处理 - -11. **允许类型错误**:编写代码时允许出现 TypeScript 类型错误。不要为了修复类型错误而违反上述规则或增加代码复杂度。尤其不得出现 `as` 关键字。 -12. **错误必须抛出**:任何理应报错的场景必须使用 `logger` 接口抛出错误或警告,不得使用 `return` 等方法静默处理。`logger` 必须分配合理的 `code`,不得复用无关 code 或使用 `0`。所有场景下,`logger` 都不会影响系统和游戏的正常运行,不应抛出任何异常中断来终止运行。 -13. **非空判断**:对象直接用 `if (!object)`;字面量使用 `lodash` 的 `isNil`(`if (isNil(value))`),不得使用 `if (value === undefined)` 等方式。 -14. **公共方法**:不得出现仅在类中定义的公共方法,所有公共方法必须先在接口中定义,然后在类中使用 `implements` 实现。 -15. **成员只读**:所有需要被实现为类的接口,其成员必须只读,如果有赋值需求,那么应使用相应的方法来完成赋值操作。 - -## 架构约束 - -16. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。 - -# 开发流程 - -当我提出需求时,若未明确说明直接实现或另有指示,遵循以下流程: - -1. 阅读当前代码,分析需求,将需求整理为 markdown 文档放在 `docs/dev` 目录下(含子目录,视情况自行判断)。文档需标注需求细节与代码实现的大体思路。本阶段遇到任何问题应向我提问确认,不得自行假设。 -2. 我会针对文档内容提问,你根据设计思路回答。我会指出设计问题,你根据要求更新文档。 -3. 我可能对文档做细微调整,实现前请重新仔细阅读最终版本。实现过程中如有问题应向我提问,而非自行决定。 -4. 我会粗略阅读你写出的代码并指出明显问题,你需要修改。 -5. 我会认真阅读并调整你的代码,形成最终方案。 -6. 你需要对比我的最终代码与你自己的代码,总结本次实现中的核心问题,以便后续改进。 - -# 文档要求 - -文档不是设计说明书,而是**需求理解报告**。文档的第一目标不是提出最佳方案,而是暴露 Agent 对需求的理解模型,供我人工确认。 - -在完成接口设计前,必须先明确: - -- 需求中明确要求了什么 -- 需求中隐含要求了什么 -- 当前设计依赖哪些假设 -- 如果假设错误会导致什么设计错误 - -**禁止直接从需求跳跃到接口设计。** 任何接口设计必须能够追溯到前面的需求事实和逻辑推导。 - -换言之,文档是一份基于若干假设推导出的一种合理设计方案,重点在于从假设推导出设计的过程,而非设计本身。也就是说,文档本身是一道数学推理题,从公理(设计前提)和定义(核心概念定义)出发,通过逻辑推理(设计思路)得出结论(接口分析),每个环节都依赖于前面的环节,最终得出的接口设计需要从逻辑上使人信服。所有的章节最核心的关键词就是**为什么**,每个章节必须写明**为什么会这样**,如果阅读文档后,我仍然不知道"为什么会设计出这个接口",那么这份文档就是失败的。 - -同时,任何时候都不能写一个接口能干什么事,这个信息对我来说没有任何价值,有价值的是从需求推导出接口设计的逻辑推理过程,也就是**文档写的是为什么**,**写是什么没有任何意义**。 - -**绝对注意**:我发给你的文字并不一定全是需求,可能会有很多内容是我经过思考之后得出的结论,这些内容不应当处理为需求,你应该通过需求建立一个合理的逻辑链来得出我给出的结论。典型的结论性语句就是,需要设计什么什么接口、因为什么什么所以怎么怎么样等等。真正的需求应该可以用几个非常简洁的点总结出来。 - -# 文档结构 - -按以下格式编写,其余需求自行组织。 - -- 我会使用 `>` 引用块在文档中批注,因此**不要在文档中使用引用块**。 -- 文档控制在 100-250 行,简洁但包含所有必要信息;不擅自修改示例文档格式。 -- 一般不需要流程图;若必须使用 `mermaid`。 -- 示例文档参考 `docs/dev/template.md`,务必认真阅读后再编写文档。一切行文思路严格按照示例文档走,不要按照自己的想法修改。已编写完成的开发文档可能不符合示例文档要求或内容已过时,不要参考。 -- 不得出现大片的 `inline-code`,不是不能写,而是不能大片地出现。像接口名等情况正常使用即可。这一条的目的不是为了不让你用 `inline-code`,而是为了让你不去在文档中简单地罗列一堆接口,接口名、方法名等正常情况完全鼓励使用。 -- 关于行数和百分比的约束只是帮助你理解文档中哪些内容是重点,哪些不是,并不需要严格遵循,只要重点正确即可。 - -```md -# 需求综述 - -描述清楚需求的内容、动机与目的。 - -# 需求理解 - -分析需求,指出你对本次需求的理解。每条使用总分结构,先用一个短语总结,然后解释,总结短语不要加粗。 - -## 明确需求 - -从任务描述中可以明确得知的直接需求。 - -## 隐含需求 - -任务描述中不包含但对任务本身有重大影响的需求。 - -## 未确定需求 - -任务描述中不包含且不属于隐含需求的内容,往往是一些语义模糊的内容。没有则不写此节。 - -# 设计前提 - -指出本次设计的前提(如同数学公理),本设计的所有内容都基于此前提进行。每条使用总分结构,先用一个短语总结,然后解释,不要加粗。这一章节应该详细解释每个概念是什么,而不是每个概念会导致什么结果,或者它本身会包含什么行为。 - -# 核心概念定义 - -详细描述涉及本次任务的核心概念。我会提供若干个需要解释的概念,如有需要可自行添加。这部分必须使用严谨的逻辑阐述,不能是简单一两句话。使用总分的结构描述,先用一句话给出定义“这个概念是什么”,然后再详细解释。 - -# 接口设计分析 - -我可能不会给出完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的。这一章最核心的就是为什么,必须让我知道你为什么要这么设计接口,接口频率为什么是这么多,预期体量为什么是这么大。 - -## IExample - -### 设计思路 - -分析需求后编写接口的设计思路。重点是分析需求并说明接口是如何设计出来的,而非简单阐述接口内容。这部分是从设计前提和概念定制推导出接口分析的过程,必须详细分析。写的时候,必须有因有果,由于什么需求,所以要设计哪些接口,不得直接说某个接口可以干什么,不得直接说某个接口可以用于哪些场景。只说某个接口的作用没有任何意义,只有从定义和设计前提推导出设计的这个推导过程有意义,所以不要分析接口可以干什么。对于每个接口,必须写明为什么需要这个接口,“由于什么什么需求,因此需要设计什么什么接口”。当我看完后,必须得让我知道为什么要这么设计接口,如果我看完之后还是不知道为什么,那么这个章节写的就是失败的。 - -### 接口分析 - -按接口逐一分析成员与方法的预期使用频率。频率分为高 / 中 / 低,指**用户编写此调用的频率**,而非运行时频率或引擎调用频率。使用频率越高,名称长度宜越短。对于成员,必须写明其类型,对于方法,必须写明其参数及类型。接口分析不要包含继承而来的接口,只分析接口本身包含的内容。必须写明为什么预期频率是这么多,如果我看完之后依然没办法知道为什么,说明这个章节写的是失败的。 - -- 成员 `property: type`:预期频率**高频**。进行分析。 -- 方法 `method(param1: type1, ...)`:预期频率**中频**。进行分析。 - -### 预期体量 - -写出预期的代码体量并分析原因。不得只写一句预期多少行,必须详细分析,分条阐述。这里不是要实现思路,而是要写为什么某个功能需要这么多行。预期是对代码量的预估,目的是让我了解到你对某个功能复杂度的理解是否正确,不需要非常精确。预期体量是对实现复杂度的预期,而不是对接口定义的预期,因此不要写接口定义预期多少行。当我读完这个章节后,我必须得理解你为什么会认为某个功能需要这么多行,如果不行,说明这个章节写的是失败的。 - -- 功能一预期 100 行:进行分析。 -- 功能二预期 50 行:进行分析。 -- 方法 `method` 预期 20 行:进行分析。 - ---- - -以上内容应占据文档的 60% 以上。以上内容中不要写任何关于私有方法或私有成员的内容,以上是设计层面的,是暴露给使用者的接口,私有方法和成员不属于设计层面,所以不应该出现。 - ---- - -# 实现思路 - -对于复杂逻辑,分条描述实现思路。简单实现不要写入本节。 - -## 复杂需求 - -简述某个复杂需求的实现思路,分条写,写成有序列表。 - -# 涉及文件 - -## `@user/package/[folder/]file.ts` - -除非必要或我明确提出,一般不建议擅自新增公共方法或成员,必要时可向我提问。不需要写得过细,涵盖重要信息即可。不要出现大段的 `inline-code`(不是不能写,是不要成片地写,该用的时候就应该用,比如接口分析时分条,某些接口名称等等)。描述不要具体到变量或成员名称等,用简短的一两句话来描述即可(针对冒号后面的内容)。写的时候不要把多个接口或成员写到一个条目里面,分开写。 - -- [ ] 新增 `IInterface` 接口:描述新增动机与目的、用途 -- [ ] 新增 `Type` 类型别名:描述新增动机与目的、用途 -- [ ] 编写 `Class.method` 方法:描述实现的大体内容 -- [ ] 修改 `Class.method` 方法中的部分内容:描述修改内容与目的 - -### `@motajs/package/[folder/]file.ts` - -... - -# 待确认问题 - -如果描述中有歧义或模糊之处,在此列出,此处只允许列出设计相关的问题,不允许列出实现相关的问题。提问必须是以下类型之一: - -- **未定义概念**:我没有明确说明某一个名词具体指什么 -- **规则冲突**:我的描述中前后产生矛盾 -- **语义模糊**:某一个概念或规则可以有多种不同解释方法 -- **设计偏好**:对于某个需求可以推导出多种不同设计方案,需要我决定 - -不允许出现以下低价值问题: - -- **风格偏好**:如命名是否合理、`logger` 的文字描述是否合理等 -``` diff --git a/review.md b/review.md deleted file mode 100644 index 6b9e1a7..0000000 --- a/review.md +++ /dev/null @@ -1,13 +0,0 @@ -# 核心职责 - -你的职责不是辅助我进行架构设计,也不是进行系统代码编写,而是对我写出的代码进行 review,以及测试用例的编写工作。 - -# Code Review - -在 code review 期间,你需要阅读我写出的代码,以及我给出的需求说明,提出我的代码中可能不合理的部分,我会针对这些问题给出回答,或修改相应的代码,执行几轮循环。期间不得修改我写出的代码。 - -在 review 期间,务必留意 @dev.md 中的代码规范要求,如果出现了某些不合规范的地方,需要在对话中提出,尤其注意 `as` 关键字、方法排序等要求,重点关注写有“不建议”、“建议”、“最好”、“尽量”等字样的规则。 - -# 测试用例 - -由于系统构建的不完全,目前还没有推进到需要进行单元测试的环节。