@ deademx/引擎
@ deademx/引擎 是共享源 2 的核心 @deademx/deadlock (解锁), @deademx/cs2 (评委-评委2)和 @deademx/dota2 (Dota 2).
该引擎为与Source 2演示机合作提供常见课程. 它不自行运行,而依赖于游戏包的配置: .proto 游戏文件和解码器指令.
[注] 所有实例都使用
@deademx/deadlock。其他游戏的API相同.
目录
- 快速启动
玩家的名字和杀人. - 如何运作
滴答 三角洲 包裹和快照. - 数据存在的地方
状态数据、无国籍数据和三角洲. - 截取器
在每个数据包之前或之后召回.InterceptorStage.DEMO_PACKET
每个演示包.InterceptorStage.MESSAGE_PACKET
每个消息包.InterceptorStage.ENTITY_PACKET
实体变化.InterceptorStage.STRING_TABLE
字符串表格更改 .InterceptorStage.USER_COMMAND
用户命令.- 截取流
调用之序.
- 玩家
寻取取取出并回放. - HTTP 广播
解说活相. - 配置图宁( T)
过滤器和事件循环中断 . - 业绩
加速过滤器.- 提示
如何是不断解说快.
- 提示
- 类型
类型脚本和is()。 。 。 . - 词汇
术语和密码. - 有用链接
关于网络和演示的文章. - 许可证
麻省理工学院.
快速启动
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 | 条目更改 | 已更改条目 |
REMOVE | SVC_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。 。 。 .
过滤器
滤镜是解析器性能的主要杠杆——减少解码后的数据会同时切除内存和时间.
| 选项 | 类型 | 默认 | 过滤器 |
|---|---|---|---|
entityClasses | Array<string>|null | null | 哪些实体类的字段解码 |
messagePacketTypes | Array<MessagePacketType>|null | null | 信件类型允许列表 |
messagePacketTypesExclude | Array<MessagePacketType>|null | null | 信件类型列表 |
四个消息包总是通过消息过滤器,因为引擎内部需要它们: 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 是一个遥远的秒。 其它东西加起来都不算什么 表格显示解析速度相对于默认配置.
| 配置 | 死锁 | 反击2 | Dota 二号 |
|---|---|---|---|
所有东西, ParserConfiguration.DEFAULT | 1× (韩语) | 1× (韩语) | 1× (韩语) |
一个实体类别: SVC_PACKET_ENTITIES + 键 entityClasses 使用玩家控制器 | 3.7× (中文(简体) ) | 2.8× (中文(简体) ) | 2.3× (中文(简体) ) |
只有玩家输入 : SVC_USER_COMMANDS | 2.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电话,~3MInterceptorStage.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 |
有用链接
- 源代码多人网络:滴答,快照和三角洲压缩.
- 连接事件和信件:网络消息和游戏事件.
- 联网实体: 实体及其联网字段.
- 用户cmd:用户命令.
- 反击:全球攻击性广播:HTTP广播协议.
- DEM( 文件格式): 旧的演示格式头和框.
- 解密记录工具:录制并播放演示.