UNPKG

teeworlds

Version:

Library for (ingame) teeworlds bots.

632 lines (589 loc) 18 kB
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; }