mirror of
https://codeberg.org/demostf/inspector.git
synced 2026-06-03 18:14:08 +02:00
wip
This commit is contained in:
parent
57650faeac
commit
a47bd90a75
6 changed files with 518 additions and 313 deletions
|
|
@ -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
79
www/src/parser.d.ts
vendored
|
|
@ -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
230
www/src/table.tsx
Normal 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(', ')}}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue