import { get_assister_ids, get_attacker_ids, get_data, get_event_count, get_event, get_kill_ticks, get_map, get_player_entity_id, get_player_name, get_player_steam_id, get_player_user_id, get_victim_ids, get_weapon, parse_demo, XY } from '@demostf/tf-demos-viewer'; import viewer from "@demostf/tf-demos-viewer"; function getCacheBuster(): string { const url = self.location.href; return url.substring(url.indexOf('?')); } export async function parseDemo(bytes: Uint8Array, progressCallback: (progress: number) => void): Promise { await viewer(`/tf-demo-viewer.wasm${getCacheBuster()}`); const state = parse_demo(bytes, progressCallback); let playerCount = state.player_count; let buildingCount = state.building_count; let projectileCount = state.projectile_count; let hasCart = state.has_cart; let boundaries = state.boundaries; let interval_per_tick = state.interval_per_tick; let tickCount = state.tick_count; let kill_ticks = get_kill_ticks(state); let attackers = get_attacker_ids(state); let assisters = get_assister_ids(state); let victims = get_victim_ids(state); let playerInfo = []; for (let i = 0; i < playerCount; i++) { playerInfo.push({ name: get_player_name(state, i), steamId: get_player_steam_id(state, i), entityId: get_player_entity_id(state, i), userId: get_player_user_id(state, i), }) } let kills = []; for (let i = 0; i < kill_ticks.length; i++) { kills.push({ tick: kill_ticks[i], attacker: attackers[i], assister: assisters[i], victim: victims[i], weapon: get_weapon(state, i), }) } let map = get_map(state); let events = kills.map((kill: Kill) => { return { tick: kill.tick, type: "kill", kill } as Event }); let event_count = get_event_count(state); for (let i = 0; i < event_count; i++) { let event = get_event(state, i); event = JSON.parse(event); if (event) { events.push(event); } } events.sort((a, b) => a.tick - b.tick); let data = get_data(state); return new ParsedDemo( playerCount, buildingCount, projectileCount, hasCart, { boundary_min: { x: boundaries.boundary_min.x, y: boundaries.boundary_min.y, }, boundary_max: { x: boundaries.boundary_max.x, y: boundaries.boundary_max.y, } }, { map, interval_per_tick }, data, kills, playerInfo, events, tickCount, ); } export interface PlayerInfo { entityId: number, name: string, steamId: string, userId: number, } export enum Team { Other = 0, Spectator = 1, Red = 2, Blue = 3, } export enum Class { Other = 0, Scout = 1, Sniper = 2, Solder = 3, Demoman = 4, Medic = 5, Heavy = 6, Pyro = 7, Spy = 8, Engineer = 9, } export enum BuildingType { TeleporterEntrance = 0, TeleporterExit = 1, Dispenser = 2, Level1Sentry = 3, Level2Sentry = 4, Level3Sentry = 5, MiniSentry = 6, Unknown = 7, } export enum ProjectileType { Rocket = 0, HealingArrow = 1, Sticky = 2, Pipe = 3, Flare = 4, LooseCannon = 5, Unknown = 7, } export interface WorldBoundaries { boundary_min: { x: number, y: number }, boundary_max: { x: number, y: number } } export interface PlayerState { position: { x: number, y: number }, angle: number, health: number, team: Team, playerClass: Class, info: PlayerInfo, class_data: PlayerClassState, ubered: boolean, cloaked: boolean, } export type PlayerClassState = MedicState | SpyState | null; export enum MedigunType { Uber = 0, Kritzkrieg = 1, Quickfix = 2, Vaccinator = 3, } export interface MedicState { charge: number, medigun: MedigunType target: number, } export interface SpyState { disguise_team: Team, disguise_class: Class, cloak: number, } export interface BuildingState { position: { x: number, y: number }, angle: number, health: number, level: number, team: Team, buildingType: BuildingType, buildProgress: number, shield: boolean, } export interface ProjectileState { position: { x: number, y: number }, angle: number, team: Team, projectileType: ProjectileType, critical: boolean, } export interface Header { interval_per_tick: number, map: string } export interface Kill { tick: number, attacker: number, assister: number, victim: number, weapon: string, } export interface CartState { position: { x: number, y: number }, } function unpack_f32(val: number, min: number, max: number): number { const ratio = val / (Math.pow(2, 16) - 1); return ratio * (max - min) + min; } function unpack_angle(val: number): number { const ratio = val / (Math.pow(2, 8) - 1); return ratio * 360; } export class ParsedDemo { public readonly playerCount: number; public readonly buildingCount: number; public readonly projectileCount: number; public readonly hasCart: boolean; public readonly world: WorldBoundaries; public readonly data: Uint8Array; public readonly header: Header; public readonly tickCount: number; public readonly kills: Kill[]; public readonly playerInfo: PlayerInfo[]; public readonly events: Event[]; constructor( playerCount: number, buildingCount: number, projectileCount: number, hasCart: boolean, world: WorldBoundaries, header: Header, data: Uint8Array, kills: Kill[], playerInfo: PlayerInfo[], events: Event[], tickCount: number ) { this.playerCount = playerCount; this.buildingCount = buildingCount; this.projectileCount = projectileCount; this.hasCart = hasCart; this.world = world; this.header = header; this.data = data; this.kills = kills; this.playerInfo = playerInfo; this.events = events; this.tickCount = tickCount; } getPlayer(tick: number, playerIndex: number): PlayerState { if (playerIndex >= this.playerCount) { throw new Error("Player out of bounds"); } const base = ((playerIndex * this.tickCount) + tick) * PLAYER_PACK_SIZE; return unpackPlayer(this.data, base, this.world, this.playerInfo[playerIndex]); } getBuilding(tick: number, buildingIndex: number): BuildingState { if (buildingIndex >= this.buildingCount) { throw new Error("Building out of bounds"); } const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + ((buildingIndex * this.tickCount) + tick) * BUILDING_PACK_SIZE; return unpackBuilding(this.data, base, this.world); } getProjectile(tick: number, projectileIndex: number): ProjectileState { if (projectileIndex >= this.projectileCount) { throw new Error("Projectile out of bounds"); } const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + (this.buildingCount * this.tickCount * BUILDING_PACK_SIZE) + ((projectileIndex * this.tickCount) + tick) * PROJECTILE_PACK_SIZE; return unpackProjectile(this.data, base, this.world); } getCart(tick: number): CartState | null { if (this.hasCart) { const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + (this.buildingCount * this.tickCount * BUILDING_PACK_SIZE) + (this.projectileCount * this.tickCount * PROJECTILE_PACK_SIZE) + (tick * CART_PACK_SIZE); return unpackCart(this.data, base, this.world); } else { return null; } } } const PLAYER_PACK_SIZE = 9; const BUILDING_PACK_SIZE = 8; const PROJECTILE_PACK_SIZE = 6; const CART_PACK_SIZE = 4; function unpackPlayer(bytes: Uint8Array, base: number, world: WorldBoundaries, info: PlayerInfo): PlayerState { const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x); const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y); const team_class_health = bytes[base + 4] + (bytes[base + 5] << 8); const angle = unpack_angle(bytes[base + 6]); const ubered = (team_class_health & 1) == 1; const health = (team_class_health >> 1) & 511; const team = (team_class_health >> 14) as Team; const playerClass = ((team_class_health >> 10) & 15) as Class; const class_bits = [bytes[base + 7], bytes[base + 8]]; let class_data = null; let cloaked = false; switch (playerClass) { case Class.Medic: class_data = { charge: class_bits[0], medigun: (class_bits[1] >> 6), target: class_bits[1] & 31, } break; case Class.Spy: class_data = { disguise_team: class_bits[0] >> 6, disguise_class: class_bits[0] >> 2 & 15, cloak: class_bits[1] >> 1, } cloaked = (class_bits[1] & 1) === 1; break; } return { position: {x, y}, angle, health, team, playerClass, info, class_data, ubered, cloaked, } } function unpackBuilding(bytes: Uint8Array, base: number, world: WorldBoundaries): BuildingState { const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x); const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y); const team_type_health = bytes[base + 4] + (bytes[base + 5] << 8); const angle = unpack_angle(bytes[base + 6]); const health = (team_type_health >> 1) & 511; const shield = (team_type_health & 1) == 1; const team = (((team_type_health >> 13) & 1) === 0) ? Team.Blue : Team.Red; const level = (team_type_health >> 14); const buildingType = ((team_type_health >> 10) & 7) as BuildingType; const buildProgress = bytes[base + 7]; return { position: {x, y}, angle, health, team, buildingType, level, buildProgress, shield, } } function unpackProjectile(bytes: Uint8Array, base: number, world: WorldBoundaries): ProjectileState { const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x); const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y); const team_type = bytes[base + 4]; const team = (((team_type >> 4) & 1) === 0) ? Team.Blue : Team.Red; const projectileType = ((team_type >> 5) & 7) as ProjectileType; const critical = bytes[base + 5] >> 7 == 1; const angle = unpack_angle(bytes[base + 5] << 1); return { position: {x, y}, angle, team, projectileType, critical, } } function unpackCart(bytes: Uint8Array, base: number, world: WorldBoundaries): CartState { const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x); const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y); return { position: {x, y}, } } export type KillEvent = { type: "kill"; tick: number, kill: Kill, } export type UberEvent = { type: "uber"; tick: number, user_id: number, target_id: number, } export type RawBuildingType = "dispenser" | "teleporter" | "sentrygun"; export type BuildingDestroyedEvent = { type: "building_destroyed"; tick: number, attacker_id: number, assister_id: number, victim_id: number, weapon: string, building_type: RawBuildingType, } export type Event = KillEvent | UberEvent | BuildingDestroyedEvent;