mirror of
https://github.com/motajs/template.git
synced 2026-11-03 15:34:31 +08:00
feat: 录像系统
This commit is contained in:
1 parent
f63da7dc3d
commit
bf6702be54
9 files changed
+529
-254
No files matched your search
@@ -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<IReplayArraySave>
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './array';
|
||||
export * from './sandbox';
|
||||
export * from './types';
|
||||
@@ -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<IReplaySandboxHook>
|
||||
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<IReplayReadStream>;
|
||||
|
||||
constructor(
|
||||
readonly route: IReplayArray,
|
||||
readonly system: IReplaySystem,
|
||||
startIndex: number
|
||||
) {
|
||||
super();
|
||||
this.reader = route.createReadStream(startIndex);
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IReplaySandboxHook>
|
||||
): IHookController<IReplaySandboxHook> {
|
||||
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<void> {
|
||||
if (this.pausing || this.ended || !this.playing) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
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<void> {
|
||||
if (this.pausing || this.ended || !this.playing) return;
|
||||
await this.pause();
|
||||
this.forEachHook(hook => hook.onStopReplay?.());
|
||||
}
|
||||
|
||||
async step(): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<IReplaySystemHook>
|
||||
implements IReplaySystem
|
||||
{
|
||||
replaying: boolean = false;
|
||||
sandbox: IReplaySandbox | null = null;
|
||||
route: IReplayArray;
|
||||
|
||||
/** 所有注册的指令 */
|
||||
private readonly commands: Map<number, IReplayCommand> = 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<IReplaySystemHook>
|
||||
): IHookController<IReplaySystemHook> {
|
||||
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<IReplaySandboxConfig>
|
||||
): 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;
|
||||
}
|
||||
}
|
||||
@@ -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<void>;
|
||||
execute(step: IReplayStepHandler): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IReplaySandboxHook extends IHookBase {
|
||||
@@ -57,8 +57,12 @@ export interface IReplaySandboxHook extends IHookBase {
|
||||
export interface IReplaySandbox extends IHookable<IReplaySandboxHook> {
|
||||
/** 是否处于暂停状态 */
|
||||
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<IReplaySandboxHook> {
|
||||
*/
|
||||
setSpeed(speed: number): void;
|
||||
|
||||
/**
|
||||
* 获取已经播放过的录像步数量,不包括当前正在播放的录像步
|
||||
*/
|
||||
getReplayed(): number;
|
||||
|
||||
/**
|
||||
* 开始连续播放
|
||||
*/
|
||||
@@ -88,8 +97,9 @@ export interface IReplaySandbox extends IHookable<IReplaySandboxHook> {
|
||||
|
||||
/**
|
||||
* 单步播放一步,返回的 Promise 在该步渲染完成后兑现
|
||||
* @returns 当前步是否正常播放完成
|
||||
*/
|
||||
step(): Promise<void>;
|
||||
step(): Promise<boolean>;
|
||||
}
|
||||
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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<IReplaySystemSave>, IHookable<IReplaySystemHook> {
|
||||
export interface IReplaySystem extends IHookable<IReplaySystemHook> {
|
||||
/** 当前是否处在录像播放状态 */
|
||||
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<string, unknown>
|
||||
): IReplaySandbox;
|
||||
createReplaySandbox(config: Readonly<IReplaySandboxConfig>): IReplaySandbox;
|
||||
|
||||
/**
|
||||
* 释放当前活跃的沙箱
|
||||
|
||||
Reference in new issue
Block a user