This commit is contained in:
Robin Appelman 2021-07-26 23:39:13 +02:00
commit a47bd90a75
6 changed files with 518 additions and 313 deletions

View file

@ -3,20 +3,25 @@ import React, {useCallback, Component} from 'react'
import {useDropzone} from 'react-dropzone'
import ReactDOM from "react-dom";
import {Header} from "./header";
import {PacketDetails, PacketTable} from "./table";
class App extends Component<{}, {
interface AppState {
loading: boolean,
header: Header | null,
packets: Packet[]
}> {
state: {
loading: boolean,
header: Header | null,
packets: Packet[]
} = {
packets: Packet[],
prop_names: Map<number, { table: String, prop: String }>,
class_names: Map<number, String>,
active: Packet | null,
}
class App extends Component<{}, AppState> {
state: AppState = {
loading: false,
header: null,
packets: []
packets: [],
prop_names: new Map(),
class_names: new Map(),
active: null,
}
load(data: ArrayBuffer) {
@ -33,6 +38,19 @@ class App extends Component<{}, {
let packets = this.state.packets;
packets.push(packet);
this.setState({packets});
if (packet.type == "DataTables") {
let prop_names = this.state.prop_names;
let class_names = this.state.class_names;
for (let table of packet.tables) {
for (let prop of table.props) {
prop_names.set(prop.identifier, {table: table.name, prop: prop.name});
}
}
for (let server_class of packet.server_classes) {
class_names.set(server_class.id, server_class.name);
}
this.setState({class_names, prop_names});
}
break;
case "done":
this.setState({loading: false});
@ -46,58 +64,78 @@ class App extends Component<{}, {
render() {
if (this.state.loading && this.state.header && this.state.packets.length) {
return (
<div>
<>
<h1>Loading</h1>
<p>{this.state.packets.slice(-1)[0].tick}/{this.state.header.ticks}</p>
</div>
</>
)
} else if (this.state.loading) {
return (
<div>
<>
<h1>Loading</h1>
</div>
</>
)
} else if (this.state.packets.length) {
let active = <></>;
if (this.state.active) {
active = <div className="details"><PacketDetails packet={this.state.active}
prop_names={this.state.prop_names}
class_names={this.state.class_names}/></div>
}
return (
<div>
<h1>{this.state.packets.length}</h1>
</div>
<>
<PacketTable packets={this.state.packets} class_names={this.state.class_names}
prop_names={this.state.prop_names} onClick={active => this.setState({active})}/>
{active}
</>
)
} else {
}
else
{
return (
<div>
<>
<DemoDropzone onDrop={(data) => this.load(data)}/>
</div>
</>
)
}
}
}
}
ReactDOM.render(
<App/>,
ReactDOM.render(
<App/>
,
document.getElementById("root")
);
);
function DemoDropzone({onDrop}: { onDrop: (data: ArrayBuffer) => void }) {
const onDropCb = useCallback(acceptedFiles => {
let reader = new FileReader();
reader.readAsArrayBuffer(acceptedFiles[0]);
reader.addEventListener('load', () => {
let result = reader.result as ArrayBuffer;
onDrop(result)
});
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop: onDropCb})
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{
isDragActive ?
<p>Drop the files here ...</p> :
<p>Drag 'n' drop some files here, or click to select files</p>
}
</div>
function DemoDropzone(
{
onDrop
}
:
{
onDrop: (data: ArrayBuffer) => void
}
)
}
{
const onDropCb = useCallback(acceptedFiles => {
let reader = new FileReader();
reader.readAsArrayBuffer(acceptedFiles[0]);
reader.addEventListener('load', () => {
let result = reader.result as ArrayBuffer;
onDrop(result)
});
}, [])
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop: onDropCb})
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{
isDragActive ?
<p>Drop the files here ...</p> :
<p>Drag 'n' drop some files here, or click to select files</p>
}
</div>
)
}

79
www/src/parser.d.ts vendored
View file

@ -204,25 +204,51 @@ export type Message =
}
| (
| {
SayText2: SayText2Message;
client: number;
from?: string | null;
kind: ChatMessageKind;
raw: number;
text: string;
type: "SayText2";
[k: string]: unknown;
}
| {
Text: TextMessage;
location: HudTextLocation;
substitute: [string, string, string, string];
text: string;
type: "Text";
[k: string]: unknown;
}
| {
ResetHUD: ResetHudMessage;
data: number;
type: "ResetHUD";
[k: string]: unknown;
}
| {
Train: TrainMessage;
data: number;
type: "Train";
[k: string]: unknown;
}
| {
VoiceSubtitle: VoiceSubtitleMessage;
client: number;
item: number;
menu: number;
type: "VoiceSubtitle";
[k: string]: unknown;
}
| {
Shake: ShakeMessage;
amplitude: number;
command: number;
duration: number;
frequency: number;
type: "Shake";
[k: string]: unknown;
}
| {
Unknown: [number, UnknownUserMessage];
data: BitReadStream;
raw_type: number;
type: "Unknown";
[k: string]: unknown;
}
)
| {
@ -3251,45 +3277,6 @@ export interface Vector {
z: number;
[k: string]: unknown;
}
export interface SayText2Message {
client: number;
from?: string | null;
kind: ChatMessageKind;
raw: number;
text: string;
[k: string]: unknown;
}
export interface TextMessage {
location: HudTextLocation;
substitute: [string, string, string, string];
text: string;
[k: string]: unknown;
}
export interface ResetHudMessage {
data: number;
[k: string]: unknown;
}
export interface TrainMessage {
data: number;
[k: string]: unknown;
}
export interface VoiceSubtitleMessage {
client: number;
item: number;
menu: number;
[k: string]: unknown;
}
export interface ShakeMessage {
amplitude: number;
command: number;
duration: number;
frequency: number;
[k: string]: unknown;
}
export interface UnknownUserMessage {
data: BitReadStream;
[k: string]: unknown;
}
export interface PacketEntity {
delay?: number | null;
entity_index: EntityId;

230
www/src/table.tsx Normal file
View file

@ -0,0 +1,230 @@
import React, {CSSProperties, JSXElementConstructor, ReactElement} from 'react';
import {GameEventDefinition, Message, Packet, PacketEntity, SendPropValue, UserCmd} from "./parser";
import {FixedSizeList as List} from 'react-window';
interface TableProps {
packets: Packet[],
prop_names: Map<number, { table: String, prop: String }>,
class_names: Map<number, String>,
onClick: (packet: Packet) => void,
}
export function PacketTable({packets, prop_names, class_names, onClick}: TableProps) {
const Row: (props: { index: number, style: CSSProperties }) => any = ({index, style}) => (
<PacketRowMemo style={style} key={index} i={index} packet={packets[index]} class_names={class_names}
prop_names={prop_names}
onClick={onClick}
expanded={false}/>
);
return (
<>
<List className="list" height={window.innerHeight} itemCount={packets.length} itemSize={30} width={210}>
{Row}
</List>
</>
)
}
interface RowProps {
style: CSSProperties,
i: number,
packet: Packet,
prop_names: Map<number, { table: String, prop: String }>,
class_names: Map<number, String>,
expanded: boolean,
onClick: (packet: Packet) => void,
}
export function PacketRow({style, i, packet, onClick}: RowProps) {
switch (packet.type) {
case "Sigon":
case "Message":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>
case "SyncTick":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>;
case "ConsoleCmd":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>;
case "UserCmd":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>;
case "DataTables":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>;
case "Stop":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>;
case "StringTables":
return <div onClick={() => {
onClick(packet)
}} style={style} key={`packet-${i}`}>
<span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span>
</div>;
}
}
interface DetailProps {
packet: Packet,
prop_names: Map<number, { table: String, prop: String }>,
class_names: Map<number, String>,
}
export function PacketDetails({packet, prop_names, class_names}: DetailProps) {
switch (packet.type) {
case "Sigon":
case "Message":
let rows = packet.messages.map((message, y) => <tr key={y}>
<td className="type">{message.type}</td>
<td>{messageInfoText(message, prop_names, class_names)}</td>
</tr>)
return (
<table>
<tbody>
{rows}
</tbody>
</table>
)
case "SyncTick":
return <></>
case "ConsoleCmd":
return <>{packet.command}</>
case "UserCmd":
return <>{formatUserCmd(packet.cmd)}</>
case "DataTables":
return <>{packet.tables.length}</>
case "Stop":
return <></>
case "StringTables":
return <>{packet.tables.length}</>
}
}
const PacketRowMemo = React.memo(PacketRow, (a, b) => a.i == b.i);
function messageInfoText(msg: Message, prop_names: Map<number, { table: String, prop: String }>, class_names: Map<number, String>) {
switch (msg.type) {
case "Print":
return <>msg.value</>
case "ServerInfo":
return <>stv: {msg.stv ? 'true' : 'false'}, map: {msg.map}, player count: {msg.player_count},
map: {msg.map}</>
case "NetTick":
return <>Tick {msg.tick}, frame time: {msg.frame_time}, std_dev: {msg.std_dev}</>
case "ParseSounds":
return <>{msg.reliable ? 'reliable' : 'unreliable'} {msg.num} sounds: {msg.length} bits</>
case "VoiceInit":
return <>{msg.codec} at quality {msg.quality} and sampling rage {msg.sampling_rate}</>
case "SigOnState":
return <>state: {msg.state}, count: {msg.count}</>
case "SetConVar":
return <>{msg.vars.map(cvar => `${cvar.key}=${cvar.value}`).join(', ')}</>
case "SetView":
return <>set view to entity {msg.index}</>
case "GameEventList":
return <>{msg.event_list.map(formatEventDefinition).map((str, i) => (<p key={i}>{str}</p>))}</>
case "PacketEntities":
let entities = msg.entities.map(entity => formatEntity(entity, prop_names, class_names)).map((str, i) => <p
key={i}>{str}</p>);
let deleted = <></>
if (msg.removed_entities.length > 0) {
deleted = <>deleted: {msg.removed_entities.join(', ')}</>
}
return <>
{entities}
{deleted}
</>
default:
let json = msg;
delete json.type;
return <>{JSON.stringify(json)}</>
}
}
function formatUserCmd(cmd: UserCmd): string {
let out = `${cmd.command_number} - tick ${cmd.tick_count}: `;
const formatOptionNum = (x: number | null) => x === null ? 0 : x;
const anyNonNull = (xs: (number | null)[]) => xs.findIndex(x => x !== null) != -1;
let parts = [];
if (anyNonNull(cmd.view_angles)) {
parts.push(`view angles: ${cmd.view_angles.map(formatOptionNum).join(',')}`);
}
if (anyNonNull(cmd.movement)) {
parts.push(`movement: ${cmd.movement.map(formatOptionNum).join(',')}`);
}
if (cmd.mouse_dx !== null || cmd.mouse_dy !== null) {
parts.push(`mouse: ${[cmd.mouse_dx, cmd.mouse_dy].map(formatOptionNum).join(',')}`);
}
if (cmd.impulse) {
parts.push(`impulse ${cmd.impulse}`)
}
if (cmd.buttons) {
parts.push(`buttons ${cmd.buttons}`)
}
if (cmd.weapon_select) {
parts.push(`weapon ${cmd.weapon_select.select}(${cmd.weapon_select.subtype})`)
}
if (parts.length == 0) {
parts.push(`no data`)
}
return out + parts.join(', ');
}
function formatPropValue(value: SendPropValue): string {
if (Array.isArray(value)) {
return '[' + value.map(formatPropValue).join(',') + ']'
} else if (typeof value === "number" || typeof value === "string") {
return `${value}`
} else {
return JSON.stringify(value)
}
}
function formatEntity(entity: PacketEntity, prop_names: Map<number, { table: String, prop: String }>, class_names: Map<number, String>,): string {
let class_name = class_names.get(entity.server_class);
let props = entity.props.map(prop => {
let names = prop_names.get(prop.identifier);
if (names) {
return `${names.table}.${names.prop}=${formatPropValue(prop.value)}`;
} else {
return `[unknown prop]=${prop.value}`;
}
})
return `entity ${entity.entity_index}(${class_name}) ${entity.pvs}: ` + props.join(', ');
}
function formatEventDefinition(event: GameEventDefinition): string {
let values = event.entries.map(entry => `${entry.name}: ${entry.kind}`);
return `${event.event_type}{${values.join(', ')}}`;
}