teeworlds
Version:
Library for (ingame) teeworlds bots.
632 lines (589 loc) • 18 kB
text/typescript
import { UUIDManager, createTwMD5Hash } from "./UUIDManager";
import { Client } from "./client";
import { MsgUnpacker } from "./MsgUnpacker";
import { SnapshotItemTypes, DeltaItem, DDNetItem, Item } from "./enums_types/types";
var decoder = new TextDecoder('utf-8');
const ___itemAppendix: {"type_id": number, "size": number, "name": string}[] = [ // only used for the events underneath. the actual itemAppendix below this is only used for size
{"type_id": 0, "size": 0, "name": "obj_ex"},
{"type_id": 1, "size": 10, "name": "obj_player_input"},
{"type_id": 2, "size": 6, "name": "obj_projectile"},
{"type_id": 3, "size": 5, "name": "obj_laser"},
{"type_id": 4, "size": 4, "name": "obj_pickup"},
{"type_id": 5, "size": 3, "name": "obj_flag"},
{"type_id": 6, "size": 8, "name": "obj_game_info"},
{"type_id": 7, "size": 4, "name": "obj_game_data"},
{"type_id": 8, "size": 15, "name": "obj_character_core"},
{"type_id": 9, "size": 22, "name": "obj_character"},
{"type_id": 10, "size": 5, "name": "obj_player_info"},
{"type_id": 11, "size": 17, "name": "obj_client_info"},
{"type_id": 12, "size": 3, "name": "obj_spectator_info"},
{"type_id": 13, "size": 2, "name": "common"}, // event_common
{"type_id": 14, "size": 2, "name": "explosion"}, // event_explosion
{"type_id": 15, "size": 2, "name": "spawn"}, // event_spawn
{"type_id": 16, "size": 2, "name": "hammerhit"}, // event_hammerhit
{"type_id": 17, "size": 3, "name": "death"}, // event_death
{"type_id": 18, "size": 3, "name": "sound_global"}, // event_sound_global
{"type_id": 19, "size": 3, "name": "sound_world"}, // event_sound_world
{"type_id": 20, "size": 3, "name": "damage_indicator"} // event_damage_indicator
]
const itemAppendix: number[] = [
0,
10,
6,
5,
4,
3,
8,
4,
15,
22,
5,
17,
3,
2,
2,
2,
2,
3,
3,
3,
3,
]
export enum items {
OBJ_EX,
OBJ_PLAYER_INPUT,
OBJ_PROJECTILE,
OBJ_LASER,
OBJ_PICKUP,
OBJ_FLAG,
OBJ_GAME_INFO,
OBJ_GAME_DATA,
OBJ_CHARACTER_CORE,
OBJ_CHARACTER,
OBJ_PLAYER_INFO,
OBJ_CLIENT_INFO,
OBJ_SPECTATOR_INFO,
EVENT_COMMON,
EVENT_EXPLOSION,
EVENT_SPAWN,
EVENT_HAMMERHIT,
EVENT_DEATH,
EVENT_SOUND_GLOBAL,
EVENT_SOUND_WORLD,
EVENT_DAMAGE_INDICATOR
}
interface eSnap {
Snapshot: {Key: number, Data: number[]},
ack: number,
}
// https://github.com/ddnet/ddnet/blob/571b0b36de83d18f2524ee371fc3223d04b94135/datasrc/network.py#L236
let supported_uuids = [
"my-own-object@heinrich5991.de",
"character@netobj.ddnet.tw", // validate_size=False
"player@netobj.ddnet.tw",
"gameinfo@netobj.ddnet.tw", // validate_size=False
"projectile@netobj.ddnet.tw",
"laser@netobj.ddnet.tw",
]
export class Snapshot {
deltas: DeltaItem[] = [];
eSnapHolder: eSnap[] = [];
crc_errors: number = 0;
client: Client;
uuid_manager: UUIDManager = new UUIDManager(32767, true); // snapshot max_type
constructor(_client: Client) {
this.client = _client;
}
private IntsToStr(pInts: number[]): string {
var pIntz: number[] = [];
// var pStr = ''
for (let x of pInts) {
// pStr += String.fromCharCode((((x) >> 24) & 0xff) - 128);
pIntz.push((((x) >> 24) & 0xff) - 128);
// pStr += String.fromCharCode((((x) >> 16) & 0xff) - 128);
pIntz.push((((x) >> 16) & 0xff) - 128);
// pStr += String.fromCharCode((((x) >> 8) & 0xff) - 128);
pIntz.push((((x) >> 8) & 0xff) - 128);
// pStr += String.fromCharCode(((x) & 0xff) - 128);
pIntz.push(((x) & 0xff) - 128);
}
pIntz.splice(-1, 1)
let pStr = decoder.decode(new Uint8Array(pIntz));
pStr = pStr.replace(/\0.*/g, ''); // Remove content from first null char to end.
return pStr;
}
private parseItem(data: number[], Type: number, id: number): Item | DDNetItem {
var _item = {} as Item | DDNetItem;
if (Type >= 0x4000) { // offset uuid type
if (this.uuid_manager.LookupType(Type)?.name == "my-own-object@heinrich5991.de") {
_item = {
m_Test: data[0]
} as SnapshotItemTypes.MyOwnObject;
} else if (this.uuid_manager.LookupType(Type)?.name == "character@netobj.ddnet.tw") {
_item = {
m_Flags: data[0],
m_FreezeEnd: data[1],
m_Jumps: data[2],
m_TeleCheckpoint: data[3],
m_StrongWeakID: data[4],
// # New data fields for jump display, freeze bar and ninja bar
// # Default values indicate that these values should not be used
m_JumpedTotal: data[5] ?? null,
m_NinjaActivationTick: data[6] ?? null,
m_FreezeStart: data[7] ?? null,
// # New data fields for improved target accuracy
m_TargetX: data[8] ?? null,
m_TargetY: data[9] ?? null,
id: id
} as SnapshotItemTypes.DDNetCharacter;
}
else if (this.uuid_manager.LookupType(Type)?.name == "player@netobj.ddnet.tw") {
_item = {
m_Flags: data[0],
m_AuthLevel: data[1],
id: id
} as SnapshotItemTypes.DDNetPlayer
}
else if (this.uuid_manager.LookupType(Type)?.name == "gameinfo@netobj.ddnet.tw") {
_item = {
m_Flags: data[0],
m_Version: data[1],
m_Flags2: data[2]
} as SnapshotItemTypes.GameInfoEx
}
else if (this.uuid_manager.LookupType(Type)?.name == "projectile@netobj.ddnet.tw") {
_item = {
m_X: data[0],
m_Y: data[1],
m_Angle: data[2],
m_Data: data[3],
m_Type: data[3],
m_StartTick: data[3]
} as SnapshotItemTypes.DDNetProjectile
}
else if (this.uuid_manager.LookupType(Type)?.name == "laser@netobj.ddnet.tw") {
_item = {
m_ToX: data[0],
m_ToY: data[1],
m_FromX: data[2],
m_FromY: data[3],
m_Owner: data[3],
m_Type: data[3]
} as SnapshotItemTypes.DDNetLaser
}
return _item;
}
switch (Type) {
case items.OBJ_EX:
break;
case items.OBJ_PLAYER_INPUT:
_item = {
direction: data[0],
target_x: data[1],
target_y: data[2],
jump: data[3],
fire: data[4],
hook: data[5],
player_flags: data[6],
wanted_weapon: data[7],
next_weapon: data[8],
prev_weapon: data[9],
} as SnapshotItemTypes.PlayerInput
break;
case items.OBJ_PROJECTILE:
_item = {
x: data[0],
y: data[1],
vel_x: data[2],
vel_y: data[3],
type_: data[4],
start_tick: data[5],
} as SnapshotItemTypes.Projectile
break;
case items.OBJ_LASER:
_item = {
x: data[0],
y: data[1],
from_x: data[2],
from_y: data[3],
start_tick: data[4],
} as SnapshotItemTypes.Laser
break;
case items.OBJ_PICKUP:
_item = {
x: data[0],
y: data[1],
type_: data[2],
subtype: data[3],
} as SnapshotItemTypes.Pickup
break;
case items.OBJ_FLAG:
_item = {
x: data[0],
y: data[1],
team: data[2],
} as SnapshotItemTypes.Flag
break;
case items.OBJ_GAME_INFO:
_item = {
game_flags: data[0],
game_state_flags: data[1],
round_start_tick: data[2],
warmup_timer: data[3],
score_limit: data[4],
time_limit: data[5],
round_num: data[6],
round_current: data[7],
} as SnapshotItemTypes.GameInfo
break;
case items.OBJ_GAME_DATA:
_item = {
teamscore_red: data[0],
teamscore_blue: data[1],
flag_carrier_red: data[2],
flag_carrier_blue: data[3],
} as SnapshotItemTypes.GameData
break;
case items.OBJ_CHARACTER_CORE:
_item = {
tick: data[0],
x: data[1],
y: data[2],
vel_x: data[3],
vel_y: data[4],
angle: data[5],
direction: data[6],
jumped: data[7],
hooked_player: data[8],
hook_state: data[9],
hook_tick: data[10],
hook_x: data[11],
hook_y: data[12],
hook_dx: data[13],
hook_dy: data[14],
} as SnapshotItemTypes.CharacterCore
break;
case items.OBJ_CHARACTER:
_item = {
character_core: {
tick: data[0],
x: data[1],
y: data[2],
vel_x: data[3],
vel_y: data[4],
angle: data[5],
direction: data[6],
jumped: data[7],
hooked_player: data[8],
hook_state: data[9],
hook_tick: data[10],
hook_x: data[11],
hook_y: data[12],
hook_dx: data[13],
hook_dy: data[14],
} as SnapshotItemTypes.CharacterCore,
player_flags: data[15],
health: data[16],
armor: data[17],
ammo_count: data[18],
weapon: data[19],
emote: data[20],
attack_tick: data[21],
client_id: id
} as SnapshotItemTypes.Character
break;
case items.OBJ_PLAYER_INFO:
_item = {
local: data[0],
client_id: data[1],
team: data[2],
score: data[3],
latency: data[4],
} as SnapshotItemTypes.PlayerInfo
break;
case items.OBJ_CLIENT_INFO:
_item = {
name: this.IntsToStr([data[0], data[1], data[2], data[3]]),
clan: this.IntsToStr([data[4], data[5], data[6]]),
country: data[7],
skin: this.IntsToStr([data[8], data[9], data[10], data[11], data[12], data[13]]),
use_custom_color: Number(data.slice(14, 15)),
color_body: Number(data.slice(15, 16)),
color_feet: Number(data.slice(16, 17)),
id: id
} as SnapshotItemTypes.ClientInfo
break;
case items.OBJ_SPECTATOR_INFO:
_item = {
spectator_id: data[0],
x: data[1],
y: data[2],
} as SnapshotItemTypes.SpectatorInfo
break;
case items.EVENT_COMMON:
_item = {
x: data[0],
y: data[1],
} as SnapshotItemTypes.Common
break;
case items.EVENT_EXPLOSION:
_item = {
common: {
x: data[0],
y: data[1]
} as SnapshotItemTypes.Common
} as SnapshotItemTypes.Explosion
break;
case items.EVENT_SPAWN:
_item = {
common: {
x: data[0],
y: data[1]
} as SnapshotItemTypes.Common
} as SnapshotItemTypes.Spawn
break;
case items.EVENT_HAMMERHIT:
_item = {
common: {
x: data[0],
y: data[1]
} as SnapshotItemTypes.Common
} as SnapshotItemTypes.HammerHit
break;
case items.EVENT_DEATH:
_item = {
client_id: data[0],
common: {
x: data[1],
y: data[2]
} as SnapshotItemTypes.Common
} as SnapshotItemTypes.Death
break;
case items.EVENT_SOUND_GLOBAL:
_item = {
common: {
x: data[0],
y: data[1]
} as SnapshotItemTypes.Common,
sound_id: data[2]
} as SnapshotItemTypes.SoundGlobal
break;
case items.EVENT_SOUND_WORLD:
_item = {
common: {
x: data[0],
y: data[1]
} as SnapshotItemTypes.Common,
sound_id: data[2]
} as SnapshotItemTypes.SoundWorld
break;
case items.EVENT_DAMAGE_INDICATOR:
_item = {
angle: data[0],
common: {
x: data[0],
y: data[1]
} as SnapshotItemTypes.Common,
} as SnapshotItemTypes.DamageInd
break;
}
return _item;
}
crc() {
var checksum = 0;
// this.eSnapHolder.forEach(snap => {
// if (snap.ack == tick)
// snap.Snapshot.Data.forEach(el => checksum += el);
// })
this.deltas.forEach(snap => {
// if (snap.ack == tick)
snap.data.forEach(el => checksum += el);
})
return checksum & 0xffffffff;
}
unpackSnapshot(snap: Buffer, deltatick: number, recvTick: number, WantedCrc: number) {
let unpacker = new MsgUnpacker(snap);
let deltaSnaps: eSnap[] = [];
if (deltatick == -1) {
this.eSnapHolder = [];
this.deltas = [];
} else {
this.eSnapHolder = this.eSnapHolder.filter(a => {
if (a.ack == deltatick)
deltaSnaps.push(a);
return a.ack >= deltatick
})
}
if (snap.length == 0) {
// empty snap, copy old one into new ack
this.eSnapHolder.forEach(snap => {
if (snap.ack == deltatick)
this.eSnapHolder.push({Snapshot: snap.Snapshot, ack: recvTick});
})
return {items: [], recvTick: recvTick};
}
let oldDeltas = this.deltas;
this.deltas = [];
/* key = (((type_id) << 16) | (id))
* key_to_id = ((key) & 0xffff)
* key_to_type_id = ((key >> 16) & 0xffff)
* https://github.com/heinrich5991/libtw2/blob/master/snapshot/src/
* https://github.com/heinrich5991/libtw2/blob/master/doc/snapshot.md
*/
var _events: {type_id: number, parsed: Item | DDNetItem}[] = [];
let num_removed_items = unpacker.unpackInt();
let num_item_deltas = unpacker.unpackInt();
unpacker.unpackInt(); // _zero padding
/*snapshot_delta:
[ 4] num_removed_items
[ 4] num_item_deltas
[ 4] _zero
[*4] removed_item_keys
[ ] item_deltas
*/
var deleted: number[] = [];
for (let i = 0; i < num_removed_items; i++) {
let deleted_key = unpacker.unpackInt(); // removed_item_keys
deleted.push(deleted_key)
}
/*item_delta:
[ 4] type_id
[ 4] id
[ 4] _size
[*4] data_delta*/
// let items: {'items': {'data': number[], 'type_id': number, 'id': number, 'key': number}[]/*, 'client_infos': client_info[], 'player_infos': player_info[]*/, lost: number} = {items: [],/* client_infos: client_infos, player_infos: player_infos,*/ lost: 0};
// let deltaSnaps = this.eSnapHolder.filter(a => a.ack === deltatick);
if (deltaSnaps.length == 0 && deltatick >= 0) {
return {items: [], recvTick: -1};
}
for (let i = 0; i < num_item_deltas; i++) {
let type_id = unpacker.unpackInt();
let id = unpacker.unpackInt();
const key = (((type_id) << 16) | (id))
let _size;
if (type_id > 0 && type_id < itemAppendix.length) {
_size = itemAppendix[type_id];
} else
_size = unpacker.unpackInt();
let data: number[] = [];
for (let j = 0; j < _size; j++) {
// if (unpacker.remaining.length > 0) {
data[j] = (unpacker.unpackInt());
// } else console.log(_size, "???")
}
let changed = false;
if (deltatick >= 0) {
// let index = deltaSnaps.map(delta => delta.Snapshot.Key).indexOf(key)
let delta = deltaSnaps.find(delta => delta.Snapshot.Key === key);
if (delta !== undefined) {
let out = UndiffItem(delta.Snapshot.Data, data)
data = out;
changed = true;
} // else no previous, use new data
}
let parsed: Item | DDNetItem;
if (type_id !== 0) {
if (!changed) {
let oldDelta = oldDeltas.find(delta => delta.key == key);
if (oldDelta !== undefined && compareArrays(data, oldDelta.data)) {
parsed = oldDelta.parsed;
} else
parsed = this.parseItem(data, type_id, id)
} else
parsed = this.parseItem(data, type_id, id)
this.eSnapHolder.push({Snapshot: {Data: data, Key: key}, ack: recvTick});
this.deltas.push({
data,
key,
id,
type_id,
parsed
});
if (type_id >= items.EVENT_COMMON && type_id <= items.EVENT_DAMAGE_INDICATOR) {
// this.client.SnapshotUnpacker.
_events.push({type_id, parsed});
// this.client.SnapshotUnpacker.emit(___itemAppendix[type_id].name, parsed);
}
} else {
this.eSnapHolder.push({Snapshot: {Data: data, Key: key}, ack: recvTick});
this.deltas.push({
data,
key,
id,
type_id,
parsed: {} as Item
});
let test = (int: number) => [(int >> 24) & 0xff, (int >> 16) & 0xff, (int >> 8) & 0xff, (int) & 0xff ];
let test2 = (ints: number[]) => ints.map(a => test(a)).flat();
let targetUUID = Buffer.from(test2(data));
if (!this.uuid_manager.LookupType(id)) {
supported_uuids.forEach((a, i) => {
let uuid = createTwMD5Hash(a);
if (targetUUID.compare(uuid) == 0) {
this.uuid_manager.RegisterName(a, id);
supported_uuids.splice(i, 1);
}
})
}
}
}
for (let newSnap of deltaSnaps) {
if (deleted.includes(newSnap.Snapshot.Key)) {
continue;
}
if (this.eSnapHolder.findIndex(a => a.ack == recvTick && a.Snapshot.Key == newSnap.Snapshot.Key) === -1) { // ugly copy new snap to eSnapHolder (if it isnt pushed already)
this.eSnapHolder.push({Snapshot: {Data: newSnap.Snapshot.Data, Key: newSnap.Snapshot.Key}, ack: recvTick});
let oldDelta = oldDeltas.find(delta => delta.key == newSnap.Snapshot.Key);
if (oldDelta !== undefined && compareArrays(newSnap.Snapshot.Data, oldDelta.data)) {
this.deltas.push(oldDelta);
} else {
this.deltas.push({
data: newSnap.Snapshot.Data,
key: newSnap.Snapshot.Key,
id: newSnap.Snapshot.Key & 0xffff,
type_id: ((newSnap.Snapshot.Key >> 16) & 0xffff),
parsed: this.parseItem(newSnap.Snapshot.Data, ((newSnap.Snapshot.Key >> 16) & 0xffff), ((newSnap.Snapshot.Key) & 0xffff))
});
}
}
}
let _crc = this.crc();
if (_crc !== WantedCrc) {
this.deltas = oldDeltas;
this.crc_errors++;
if (this.crc_errors > 5) {
recvTick = -1;
this.crc_errors = 0;
this.eSnapHolder = [];
this.deltas = [];
} else {
recvTick = deltatick;
}
} else if (this.crc_errors > 0)
this.crc_errors--;
_events.forEach(a => this.client.SnapshotUnpacker.emit(___itemAppendix[a.type_id].name, a.parsed))
return {items: this.deltas, recvTick};
}
}
function compareArrays(first: number[], second: number[]) {
if (first.length !== second.length)
return false;
for (var i = 0; i < first.length; i++) {
if (first[i] !== second[i])
return false;
}
return true;
}
function UndiffItem(oldItem: number[], newItem: number[]): number[] {
let out: number[] = newItem;
// if (JSON.stringify(newItem) === JSON.stringify(oldItem))
// return newItem;
oldItem.forEach((a, i) => {
if (a !== undefined && out[i] !== undefined) {
out[i] += a;
} else {
out[i] = 0;
}
})
return out;
}