@deademx/engine

中文文档
5.0.0发布于 2026/10/27MITTypeScriptES Module近 30 天下载 3413

共享源 2 为 Node.js 和浏览器演示和回放播放引擎

中文使用文档
文档目录29 节
  1. 目录
  2. 快速启动
  3. 如何运作
  4. 数据存在的地方
  5. 状态数据
  6. 无国籍数据
  7. 三角洲
  8. 截取器
  9. InterceptorStage.DEMO_PACKET
  10. InterceptorStage.MESSAGE_PACKET
  11. InterceptorStage.ENTITY_PACKET
  12. InterceptorStage.STRING_TABLE
  13. InterceptorStage.USER_COMMAND
  14. 截取流
  15. 玩家
  16. 寻求
  17. 状态
  18. 导航
  19. 重播
  20. HTTP 广播
  21. 配置图宁( T)
  22. 过滤器
  23. 流调
  24. 业绩
  25. 提示
  26. 类型
  27. 词汇
  28. 有用链接
  29. 许可证

deadem
@ deademx/引擎

@ deademx/引擎 是共享源 2 的核心 @deademx/deadlock (解锁), @deademx/cs2 (评委-评委2)和 @deademx/dota2 (Dota 2).

该引擎为与Source 2演示机合作提供常见课程. 它不自行运行,而依赖于游戏包的配置: .proto 游戏文件和解码器指令.

[注] 所有实例都使用 @deademx/deadlock。其他游戏的API相同.

目录

快速启动

npm install @deademx/deadlock
import { createReadStream } from 'node:fs';

import { Parser } from '@deademx/deadlock';

(async () => {
    const parser = new Parser();
    const stream = createReadStream('./match.dem');

    await parser.parse(stream);

    const demo = parser.getDemo();

    for (const controller of demo.getEntitiesByClassName('CCitadelPlayerController')) {
        console.log(controller.getField('m_iszPlayerName'), controller.getField('m_PlayerDataGlobal.m_iPlayerKills'));
    }

    await parser.dispose(); // cleanup state and resources
})();

如何运作

演示( E).dem)是一对匹配的录音:游戏引擎重放的帧和网络包的序列. 解析器读取了一串字节的流,分解了帧和网络包,解压缩了他们的有效载荷,并提供了一个API来消耗已解码的数据.

演示的结构跟随服务器模拟匹配的方式: 在离散的时间步骤中称为 ticks. 例如"死锁"和"反冲"2通常使用64分的滴答速率,也就是说一秒内有64分的时间步,或者每15.625分一个. 在每个勾选时保存完整的世界状态是低效的,所以格式使用deltas:演示只存储自上个勾选以来的变化. 因此,一滴状态是所有三角洲应用到一滴状态的结果.

溪流有两层包. 外层由帧组成. 这个工程叫它们演示包(DemoPacket) (中文(简体) ). 内层由来自游戏服务器的信息组成,如实体更新,聊天和声音等. 这个项目叫他们信息包(MessagePacket) (中文(简体) ). 演示包共有19个类型,每个游戏包支持大约70-200个类型的信息包. 三种类型的演示包, DEM_SIGNON_PACKET, (中文(简体) ), DEM_PACKET 财务报告和审定财务报表 DEM_FULL_PACKET,并携带这些消息的网络包.

.dem (simplified)
 ├─ tick -1
 │   ├─ DEM_FILE_HEADER
 │   ├─ DEM_SIGNON_PACKET ×N ── MessagePacket, …
 │   ├─ DEM_SEND_TABLES
 │   ├─ DEM_CLASS_INFO
 │   └─ DEM_STRING_TABLES
 ├─ DEM_PACKET       ── MessagePacket, …     first tick
 ├─ DEM_FULL_PACKET  ── MessagePacket, …     snapshot at the first tick, then about once a minute
 ├─ DEM_PACKET       ── MessagePacket, …     next recorded tick
 ├─ …
 ├─ DEM_STOP                                 last tick
 └─ DEM_FILE_INFO                            last tick

引擎在状态下累积了三角洲,用于三种数据: Entities, (中文(简体) ), String Tables 财务报告和审定财务报表 User Commands页:1.

当需要一次通行证时,按顺序阅读非常适合. 然而,对于随机访问来说,效率低下:要在比赛中间求取需要应用整个上半场的三角洲. 为了提高随机访问的效率,演示还包含一个快照(DEM_FULL_PACKET大约每60秒。 一个快照包含实体和字符串表格的全部状态. 一个搜索从最近的快照中应用三角洲,而不是从溪流的起始处应用.

引擎输出两种阅读方式:

  • Parser 尽可能快地读取一次和顺序的演示.
  • Player 缓冲演示,索引其快照,以实时速度(或另一种速率)提供回放,并寻求任意勾选.

两个班都一样 拦截器 API (英语).

数据存在的地方

引擎在 Demo,而无国籍数据,如声音事件和杀戮,则只存在于数据包中,可以通过截取器访问.

状态数据

状态数据由实体,字符串表格和用户命令等组成. parser.getDemo() 财务报告和审定财务报表 player.getDemo() 返回 Demo,它在当前勾选时保存此数据。 引擎变了 Demo 对象,因为它应用数据包,而从未替换.

何时 parse() 返回时, Demo 比赛结束 下面的例子以特定的勾选方式捕捉状态,并将每个玩家的10分钟净值打印入演示:

import { createReadStream } from 'node:fs';

import { InterceptorStage, Parser } from '@deademx/deadlock';

const TICK = 10 * 60 * 64; // 10 minutes into the demo at 64 ticks per second

const parser = new Parser();

const interceptor = (demoPacket) => {
    if (demoPacket.tick < TICK) return;

    for (const controller of parser.getDemo().getEntitiesByClassName('CCitadelPlayerController')) {
        if (controller.getField('m_iTeamNum') < 2) continue; // spectators and SourceTV

        console.log(controller.getField('m_iszPlayerName'), controller.getField('m_PlayerDataGlobal.m_iGoldNetWorth'));
    }

    parser.unregisterPostInterceptor(InterceptorStage.DEMO_PACKET, interceptor);
};

parser.registerPostInterceptor(InterceptorStage.DEMO_PACKET, interceptor);

await parser.parse(createReadStream('./match.dem'));

实体

实体是游戏对象,如武器,蠕动或能力,具有类名和场. 这个 SVC_PACKET_ENTITIES 消息包载有其更新.

在"死锁"中,玩家是两个实体. 那个卒子 CCitadelPlayerPawn 保持英雄的地位和健康 控制器 CCitadelPlayerController 名称和净值。 处理字段,通常 m_h*,链接实体. demo.getEntityByHandle() 决定:

for (const pawn of demo.getEntitiesByClassName('CCitadelPlayerPawn')) {
    const controller = demo.getEntityByHandle(pawn.getField('m_hController'));

    if (controller === null) continue;

    console.log(controller.getField('m_iszPlayerName'), pawn.getField('m_iHealth'));
}

演示 – 字符串表

字符串表是一个有索引条目的数据容器,每个有文本字符串和可选的二进制数据. DEM_STRING_TABLES, (中文(简体) ), SVC_CREATE_STRING_TABLE, (中文(简体) ), SVC_UPDATE_STRING_TABLE 财务报告和审定财务报表 SVC_CLEAR_ALL_STRING_TABLES 带着它的变化。 譬如说, userinfo 存储播放器名称,以及 ActiveModifiers 储存活的起伏和起伏.

demo.stringTableContainer.getByType() 返回其表格 StringTableType数字 :

for (const table of demo.stringTableContainer.getTables()) {
    console.log(table.type.name, table.getEntriesCount());
}

const userInfo = demo.stringTableContainer.getByType(StringTableType.USER_INFO);

for (const entry of userInfo.getEntries()) {
    console.log(entry.id, entry.value?.name);
}

[注] USER_INFO 仅在玩家连接时保留一个条目, 因此在 parse() 大多数条目是空的.

Demo – 用户命令

用户命令是玩家输入的网络代表,如按下按钮,取景角度和移动等. 这个 SVC_USER_COMMANDS 信息包随身携带。 Dota 2 演示中没有用户命令 .

demo.getUserCommands() 返回每个玩家槽的最新命令,槽 N 的控制器是实体 N + 1.

for (const command of demo.getUserCommands()) {
    const controller = demo.getEntity(command.slot + 1);

    if (controller === null) continue;

    console.log(controller.getField('m_iszPlayerName'), command.state.base.viewangles);
}

无国籍数据

无国籍数据是其它数据包携带的 引擎没有保存它,只有 拦截器 接受这个 在死锁中, 消息包 CITADEL_USER_MESSAGE_DAMAGE 拥有攻击者、受害者和损失数额。 演示包 DEM_FILE_HEADER 持有地图和服务器名称.

三角洲

三角洲是状态数据的变化. 三个拦截阶段报告三角洲: ENTITY_PACKET 对于实体, STRING_TABLE 用于字符串表格和 USER_COMMAND 用于用户命令。 每个事件只持有已经改变的东西,例如一个实体已经改变的字段. 这个 截取器 报告部分描述了这些内容.

截取器

截取器就是回叫器 InterceptorStage. 发动机以前称为(PRE或之后(POST将一个包应用到 Demo这样 PRE 看过以前的状态 POST 新的一个. Parser 财务报告和审定财务报表 Player 提供四种方法:

  • registerPreInterceptor(stage, callback) 财务报告和审定财务报表 registerPostInterceptor(stage, callback) 添加拦截器.
  • unregisterPreInterceptor(stage, callback) 财务报告和审定财务报表 unregisterPostInterceptor(stage, callback) 拿开.

拦截器是同步的,引擎不会等待返回的承诺.

阶段钩子签名
DEMO_PACKET(演示包 : DemoPacket(虚空)
MESSAGE_PACKET(演示包 : DemoPacket,留言包: MessagePacket(虚空)
ENTITY_PACKET(演示包 : DemoPacket,留言包: MessagePacket,事件:矩阵 <EntityMutationEvent>) 无效
STRING_TABLE(演示包 : DemoPacket,留言包: MessagePacket QQ 无效, 事件: 阵列 <StringTableMutationEvent>) 无效
USER_COMMAND(演示包 : DemoPacket,留言包: MessagePacket,事件:矩阵 <UserCommandEvent>) 无效

InterceptorStage.DEMO_PACKET

每个演示包一个呼叫.

import { InterceptorStage, Parser } from '@deademx/deadlock';

const parser = new Parser();

parser.registerPostInterceptor(InterceptorStage.DEMO_PACKET, (demoPacket) => {
    console.log(`Tick [ ${demoPacket.tick} ] | Type [ ${demoPacket.type.code} ]`);
});

InterceptorStage.MESSAGE_PACKET

每个消息包一个电话.

import { MessagePacketType } from '@deademx/deadlock';

parser.registerPostInterceptor(InterceptorStage.MESSAGE_PACKET, (demoPacket, messagePacket) => {
    if (!messagePacket.is(MessagePacketType.CITADEL_USER_MESSAGE_CHAT_MESSAGE)) return;

    console.log(`Tick [ ${demoPacket.tick} ] | Slot [ ${messagePacket.data.playerSlot} ] | ${messagePacket.data.text}`);
});

InterceptorStage.ENTITY_PACKET

一通电话 SVC_PACKET_ENTITIES 信息,带有 EntityMutationEvent 对于每个已改变的实体.

import { EntityOperation } from '@deademx/deadlock';

parser.registerPostInterceptor(InterceptorStage.ENTITY_PACKET, (demoPacket, messagePacket, events) => {
    for (const event of events) {
        if (event.operation !== EntityOperation.UPDATE) continue;
        if (event.entity.class.name !== 'CCitadelPlayerPawn') continue;

        const [health] = event.getChanges(['m_iHealth']);

        if (health !== undefined) {
            console.log(`Tick [ ${demoPacket.tick} ] | Entity [ ${event.entity.class.name}|${event.entity.index} ] | Health [ ${health} ]`);
        }
    }
});
event.operation何时event.getChanges()
CREATE实体出现所有初始字段
UPDATE字段变化已更改字段
LEAVE服务器停止发送实体 。 留在这 Demo 与 entity.active 设置为 false空
DELETE该实体从 Demo空

InterceptorStage.STRING_TABLE

一通电话 DEM_STRING_TABLES (光盘)和 SVC_CREATE_STRING_TABLE, (中文(简体) ), SVC_UPDATE_STRING_TABLE 或者说 SVC_CLEAR_ALL_STRING_TABLES (信息包),带 StringTableMutationEvent 每个更改的表格.

import { StringTableOperation, StringTableType } from '@deademx/deadlock';

parser.registerPreInterceptor(InterceptorStage.STRING_TABLE, (demoPacket, messagePacket, events) => {
    for (const event of events) {
        if (!event.table.is(StringTableType.USER_INFO) || event.operation !== StringTableOperation.UPDATE) continue;

        for (const entry of event.entries) {
            const before = event.table.getEntryById(entry.id)?.value ?? null; // PRE: the table still holds the previous entry
            const after = entry.value;

            if (before === null && after !== null) {
                console.log(`Tick [ ${demoPacket.tick} ] | Connected [ ${after.name} ]`);
            }

            if (before !== null && after === null) {
                console.log(`Tick [ ${demoPacket.tick} ] | Disconnected [ ${before.name} ]`);
            }
        }
    }
});
event.operation何时event.entries
CREATE创建或替换表格所有条目
UPDATE条目更改已更改条目
REMOVESVC_CLEAR_ALL_STRING_TABLES 删除表格空

InterceptorStage.USER_COMMAND

一通电话 SVC_USER_COMMANDS 或者说 SVC_USER_COMMAND_KEYFRAME 信息,带有 UserCommandEvent 每个命令. event.userCommand 是更新的命令,并且 event.getChanges() 返回命令更改的内容,形状为 state。 。 。 .

parser.registerPostInterceptor(InterceptorStage.USER_COMMAND, (demoPacket, messagePacket, events) => {
    for (const event of events) {
        const { inShop } = event.getChanges(); // Deadlock: true while the shop is open

        if (inShop === true) {
            console.log(`Tick [ ${demoPacket.tick} ] | Slot [ ${event.userCommand.slot} ] | Shop opened`);
        }
    }
});

截取流

PRE DEMO_PACKET
 └─ DEM_FILE_HEADER
POST DEMO_PACKET
...
PRE DEMO_PACKET
 └─ DEM_PACKET
     ├─ PRE MESSAGE_PACKET
     │   └─ NET_TICK
     ├─ POST MESSAGE_PACKET
     ├─ ...
     ├─ PRE MESSAGE_PACKET
     │   └─ SVC_PACKET_ENTITIES
     │       ├─ PRE ENTITY_PACKET
     │       │   └─ events
     │       └─ POST ENTITY_PACKET
     ├─ POST MESSAGE_PACKET
     ├─ PRE MESSAGE_PACKET
     │   └─ SVC_UPDATE_STRING_TABLE
     │       ├─ PRE STRING_TABLE
     │       │   └─ events
     │       └─ POST STRING_TABLE
     ├─ POST MESSAGE_PACKET
     ├─ PRE MESSAGE_PACKET
     │   └─ SVC_USER_COMMANDS
     │       └─ POST USER_COMMAND
     ├─ POST MESSAGE_PACKET
     └─ ...
POST DEMO_PACKET

玩家

Parser 从头到尾都读过一次演示. Player 向后搜索, 步数按勾选, 并按实时速度或任何其他速度播放演示 . Player 将整个演示读取为 load() 并索引其快照. Player 尚不支持 HTTP 广播 .

寻求

Player 提供 seekToTick(tick),它从最近的早期快照开始,并应用三角洲直至 tick寻找时间随着时间的距离而增加.

import { createReadStream } from 'node:fs';

import { Player } from '@deademx/deadlock';

const player = new Player();

await player.load(createReadStream('./match.dem'));
await player.seekToTick(player.getLastTick());

const controllers = player.getDemo().getEntitiesByClassName('CCitadelPlayerController');

for (const controller of controllers) {
    const name = controller.getField('m_iszPlayerName');
    const netWorth = controller.getField('m_PlayerDataGlobal.m_iGoldNetWorth');

    console.log(`[ ${name} ] | m_iGoldNetWorth [ ${netWorth} ]`);
}

await player.dispose();

状态

Player 随状态机:

IDLE
 ├── load()        → LOADED
 └── dispose()     → DISPOSED

LOADED
 ├── play()        → PLAYING
 ├── seekToTick()  → SEEKING → LOADED
 └── dispose()     → DISPOSED

PLAYING
 ├── pause() / end → LOADED
 └── dispose()     → DISPOSED

SEEKING
 ├── (completes)   → LOADED
 └── dispose()     → DISPOSED

一个失败的搜索仍然返回到 LOADED。 。 。 .

导航

Player 提供了两种每次移动一个勾选的方法:

  • player.nextTick() 是同步和便宜的,因为它应用了一个三角洲。 在最后一个勾选时,它返回 false。 。 。 .
  • player.prevTick() 是同步的,成本与搜索一样高,因为它从最近的快照开始.
await player.seekToTick(10 * 60 * 64); // 10 minutes into the demo at 64 ticks per second

for (let i = 0; i < 10 && player.nextTick(); i++) { // 10 ticks forward
    console.log(`Tick [ ${player.getCurrentTick()} ]`);
}

重播

Player 提供 play(rate),实时播放演示。 这个 rate 参数乘以速度。 回来的承诺:

  • 返回到最后一个勾选时决定 .
  • 以 PlaybackInterruptedError 与 reason 'paused' 之后 player.pause() 或者说 player.seekToTick()。 。 。 .
  • 以 PlaybackInterruptedError 与 reason 'stopped' 之后 player.stop()。 。 。 .
  • 以 PlaybackInterruptedError 与 reason 'disposed' 之后 player.dispose()。 。 。 .
  • 如果包处理失败, 则以原出错拒绝 .
const playback = player.play(0.5); // half speed

setTimeout(() => player.pause(), 1000);

try {
    await playback;
} catch (error) {
    console.log(error.reason); // 'paused'
}

HTTP 广播

HTTP广播在片断中通过HTTP进行直播匹配. 发动机为它提供两个课:

  • BroadcastGateway 请求从广播服务器取出碎片.
  • BroadcastAgent 跟着一个匹配,下载其碎片,并将其变成一串流. 30秒后,没有新的碎片,特工停止了.

广播的结构不同于 .dem 文档,所以 Parser 需要明确来源: DemoSource.HTTP_BROADCAST。 。 。 .

import { BroadcastAgent, BroadcastGateway, DemoSource, Parser } from '@deademx/deadlock';

const MATCH_ID = 'MATCH_IDENTIFIER_AND_SALT';

const gateway = new BroadcastGateway('dist1-ord1.steamcontent.com/tv');
const agent = new BroadcastAgent(gateway, MATCH_ID);

const parser = new Parser();

await parser.parse(agent.stream(), DemoSource.HTTP_BROADCAST);

默认, agent.stream() 在服务器报告到的碎片前启动四个碎片 /sync接近现场. agent.stream(true) 从广播的第一个片段开始.

配置图宁( T)

Parser 财务报告和审定财务报表 Player 带一个 ParserConfiguration 在建筑师。 没有一个,两个都用 ParserConfiguration.DEFAULT。 。 。 .

过滤器

滤镜是解析器性能的主要杠杆——减少解码后的数据会同时切除内存和时间.

选项类型默认过滤器
entityClassesArray<string>|nullnull哪些实体类的字段解码
messagePacketTypesArray<MessagePacketType>|nullnull信件类型允许列表
messagePacketTypesExcludeArray<MessagePacketType>|nullnull信件类型列表

四个消息包总是通过消息过滤器,因为引擎内部需要它们: SVC_SERVER_INFO, (中文(简体) ), SVC_CREATE_STRING_TABLE, (中文(简体) ), SVC_UPDATE_STRING_TABLE 财务报告和审定财务报表 SVC_CLEAR_ALL_STRING_TABLES。 。 。 .

import { MessagePacketType, Parser, Player, ParserConfiguration } from '@deademx/deadlock';

// Skips all entities: everything else is decoded.
const configA = new ParserConfiguration({
    messagePacketTypesExclude: [MessagePacketType.SVC_PACKET_ENTITIES]
});

// Decodes only entities, and only the fields of player pawns and controllers.
const configB = new ParserConfiguration({
    messagePacketTypes: [MessagePacketType.SVC_PACKET_ENTITIES],
    entityClasses: ['CCitadelPlayerPawn', 'CCitadelPlayerController']
});

new Parser(configA);
new Player(configB);

流调

没有休息,长时间的剖析 或寻求冻结UI。 每个 N 演示包, N 所在位置 breakInterval,引擎会向事件循环输出, 所以计时器,输入和渲染在两者之间运行。 一个更低的值使得一个UI更能响应,并花费了一点速度. 默认为1000.

new Parser(new ParserConfiguration({ breakInterval: 100 }));

业绩

实体解码( E)SVC_PACKET_ENTITIES) 占了分析者的大部分作品. SVC_USER_COMMANDS 是一个遥远的秒。 其它东西加起来都不算什么 表格显示解析速度相对于默认配置.

配置死锁反击2Dota 二号
所有东西, ParserConfiguration.DEFAULT1× (韩语)1× (韩语)1× (韩语)
一个实体类别: SVC_PACKET_ENTITIES + 键 entityClasses 使用玩家控制器3.7× (中文(简体) )2.8× (中文(简体) )2.3× (中文(简体) )
只有玩家输入 : SVC_USER_COMMANDS2.7× (中文(简体) )1.8× (中文(简体) )—— 说
一个聊天消息类型, 没有实体和用户命令5.0× (中文(简体) )3.7× (中文(简体) )4.2× (中文(简体) )

绝对数和内存使用量在 @deademx/deadlock, (中文(简体) ), @deademx/cs2 财务报告和审定财务报表 @deademx/dota2。 。 。 .

提示

  • 过滤到底需要什么。 最大的业绩收益.

  • 下调 breakInterval 在浏览器中。 默认 1000 (原始内容存档于2018-09-21). suits Node.js. Deadem 探测器 用途 100 保持UI的反应.

  • 注意呼叫数。 500 MB 死锁演示可能包含 ~ 150k InterceptorStage.DEMO_PACKET 电话,~3M InterceptorStage.MESSAGE_PACKET 调用,和带有~100M字段更新的~10M实体事件. 工作繁重 InterceptorStage.MESSAGE_PACKET 截取器的化合物速度快,并且可以罐装性能.

  • 使用 InterceptorStage.ENTITY_PACKET 只有在需要比对时, 简单的读取当前状态——玩家位置,健康等——使用 Demo 从一个 InterceptorStage.DEMO_PACKET 代之以拦截器.

  • 样本,而不是读取每一个勾。 用户界面很少在每个勾点上需要一个值 。 每秒一次意味着"死锁"和"反冲"2中每64个虱子,"多塔"2中每30个虱子.

  • 随所需要多读. 首选点读取通过 entity.getField('name') 结束 entity.unpackFlattened() 和田地行走者。 实体状态被存储在打入的数组中,数值只在读取时实现. 联合来文: Entity.getField 说明成本.

类型

@deademx/engine 和游戏包船 TypeScript 声明. 包装 data 实值 unknown 直至 is() 将其缩小为游戏中的类型 .proto 文档.

parser.registerPostInterceptor(InterceptorStage.MESSAGE_PACKET, (demoPacket, messagePacket) => {
    if (messagePacket.is(MessagePacketType.CITADEL_USER_MESSAGE_CHAT_MESSAGE)) {
        console.log(messagePacket.data.text); // string
    }
});

实体字段没有静态类型. Deadem 探测器 显示每个实体的类型和价值,以及 getField<T>() 将预期类型取为 T数字 :

const health = pawn.getField<number>('m_iHealth'); // number, an unset field is undefined

词汇

任期含义代码
基线类的默认字段值 。 一个新的实体从它们开始.ClassBaseline
类例如,实体的类型 CCitadelPlayerPawn定义了字段.Class
演示当前勾选时的状态:实体,字符串表格,用户命令和服务器信息.Demo
演示包外包的演示流,19种类型之一.DemoPacket, (中文(简体) ), DemoPacketType
实体游戏世界的一个具有类和被命名字段的对象,例如英雄或能力.Entity
外地例如,一个实体的命名值 m_iHealth。点加入巢名.Field
处理以一个实体的索引和序列编号.Demo.getEntityByHandle
截取器在引擎应用之前或之后接收每包的回调 .InterceptorStage
信件包演示包的内包, 例如实体更新或聊天消息 。 每个游戏包都添加自己的 MessagePacketType 数值.MessagePacket, (中文(简体) ), MessagePacketType
解析器阅读一次演示,从头到尾.Parser
玩家拖出一个演示,然后寻找,步骤和播放.Player
抓图A 中的实体状态和字符串表 DEM_FULL_PACKET大约一分钟一次 引擎只在搜索开始时应用.—— 说
字符串表一个有索引条目的数据容器,每个有文本字符串和可选的二进制数据.StringTable, (中文(简体) ), StringTableType
选中服务器模拟的一个步骤,通常在"死锁"和"反冲"2中为64分每秒,而"多塔"2中为30分.Server (单位:千美元)tickRate(中文(简体) )
用户命令一个玩家在勾选时的输入:按钮,取景角度和运动.UserCommand

许可证

麻省理工学院

版本与发布记录

@deademx/engine 共发布 21 个版本,首次发布于 2026/04/22;过去 12 个月发布了 21 个版本。

版本发布日期距今
5.0.0latest2026/10/2716 天前
5.0.0-rc.1rc2026/10/2716 天前
5.0.0-rc.02026/10/2319 天前
4.0.22026/10/101 个月前
4.0.12026/09/102 个月前
4.0.02026/09/012 个月前

其他发布通道:alpha → 2.1.0-alpha.1,rc → 5.0.0-rc.1。

关于 @deademx/engine 的常见问题

@deademx/engine 是什么?

共享源 2 为 Node.js 和浏览器演示和回放播放引擎

如何安装 @deademx/engine?

在项目目录运行 npm install @deademx/engine,或使用 pnpm add / yarn add / bun add。

@deademx/engine 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@deademx/engine 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@deademx/engine 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@deademx/engine 可以商用吗?

@deademx/engine 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@deademx/engine 的下载量有多少?

根据 npm 官方下载统计,@deademx/engine 在 2026/10/12 – 2026/11/10 这 30 天内被下载 3,413 次,日均约 114 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@deademx/engine 还在维护吗?

最新版本 5.0.0 发布于 2026/10/27(16 天前);至今共发布 21 个版本,其中过去 12 个月发布了 21 个;首次发布于 2026/04/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。