mirror of
https://codeberg.org/demostf/inspector.git
synced 2026-06-03 18:14:08 +02:00
split out worker stuff
This commit is contained in:
parent
d360ffec68
commit
a1838c5164
5 changed files with 294 additions and 218 deletions
184
www/src/App.tsx
Normal file
184
www/src/App.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import {Packet} from "./parser";
|
||||
import React, {useCallback, Component} from 'react'
|
||||
import {useDropzone} from 'react-dropzone'
|
||||
import {Header} from "./header";
|
||||
import {PacketDetails, PacketTable} from "./table";
|
||||
import {isSearchEmpty, SearchBar, SearchFilter} from "./search";
|
||||
import {DemoWorker, ResponseMessageData} from "./rpc";
|
||||
|
||||
let _style = require('../styles/style.css');
|
||||
|
||||
export interface PacketMeta {
|
||||
index: number,
|
||||
tick: number,
|
||||
ty: PacketType,
|
||||
}
|
||||
|
||||
export enum PacketType {
|
||||
Signon = 1,
|
||||
Message = 2,
|
||||
SyncTick = 3,
|
||||
ConsoleCmd = 4,
|
||||
UserCmd = 5,
|
||||
DataTables = 6,
|
||||
Stop = 7,
|
||||
StringTables = 8,
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
loading: boolean,
|
||||
header: Header | null,
|
||||
progress: number,
|
||||
packets: PacketMeta[],
|
||||
prop_names: Map<string, { table: string, prop: string }>,
|
||||
class_names: Map<number, string>,
|
||||
active: Packet | null,
|
||||
activeIndex: number | null,
|
||||
search: SearchFilter,
|
||||
matches: number[],
|
||||
worker: DemoWorker | null,
|
||||
}
|
||||
|
||||
export class App extends Component<{}, AppState> {
|
||||
state: AppState = {
|
||||
loading: false,
|
||||
header: null,
|
||||
progress: 0,
|
||||
packets: [],
|
||||
prop_names: new Map(),
|
||||
class_names: new Map(),
|
||||
active: null,
|
||||
activeIndex: null,
|
||||
search: {
|
||||
entity: 0,
|
||||
search: "",
|
||||
class_ids: [],
|
||||
prop_ids: [],
|
||||
},
|
||||
matches: [],
|
||||
worker: null
|
||||
}
|
||||
|
||||
onSearch = debounce((search: SearchFilter) => {
|
||||
if (!isSearchEmpty(search) && this.state.worker) {
|
||||
console.log(search);
|
||||
this.state.worker.search(search).then(matches => this.setState({matches}));
|
||||
}
|
||||
this.setState({search});
|
||||
}, 500)
|
||||
|
||||
selectPacket = (packet: number) => {
|
||||
this.state.worker.get(packet).then(packet => this.setState({active: packet}));
|
||||
}
|
||||
|
||||
async load(data: ArrayBuffer) {
|
||||
this.setState({loading: true});
|
||||
const worker = new DemoWorker();
|
||||
this.setState({worker});
|
||||
const parsed = await worker.load(data, (progress) => {
|
||||
this.setState({progress});
|
||||
});
|
||||
|
||||
const prop_names = new Map();
|
||||
for (let prop of parsed.prop_names) {
|
||||
prop_names.set(prop.identifier, prop);
|
||||
}
|
||||
const class_names = new Map();
|
||||
for (let c of parsed.class_names) {
|
||||
class_names.set(c.identifier, c.name);
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
packets: parsed.packets,
|
||||
header: parsed.header,
|
||||
prop_names,
|
||||
class_names
|
||||
});
|
||||
}
|
||||
|
||||
filteredPackets(): PacketMeta[] {
|
||||
if (isSearchEmpty(this.state.search)) {
|
||||
return this.state.packets;
|
||||
} else {
|
||||
return this.state.matches.map(index => this.state.packets[index]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading && this.state.progress > 0) {
|
||||
return (
|
||||
<>
|
||||
<h1>Loading</h1>
|
||||
<progress value={this.state.progress} max={100}/>
|
||||
</>
|
||||
)
|
||||
} else if (this.state.loading) {
|
||||
return (
|
||||
<>
|
||||
<h1>Loading</h1>
|
||||
</>
|
||||
)
|
||||
} else if (this.state.packets.length) {
|
||||
let active = <></>;
|
||||
const packets = this.filteredPackets();
|
||||
if (this.state.active) {
|
||||
active = <div className="details"><PacketDetails packet={this.state.active}
|
||||
search={this.state.search}
|
||||
prop_names={this.state.prop_names}
|
||||
class_names={this.state.class_names}/></div>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names}
|
||||
prop_names={this.state.prop_names}/>
|
||||
<div className="packets">
|
||||
<PacketTable packets={packets} class_names={this.state.class_names}
|
||||
activeIndex={this.state.activeIndex}
|
||||
prop_names={this.state.prop_names}
|
||||
onClick={this.selectPacket}/>
|
||||
{active}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<DemoDropzone onDrop={(data) => this.load(data)}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DemoDropzone({onDrop}: { onDrop: (data: ArrayBuffer) => void }) {
|
||||
const onDropCb = useCallback((acceptedFiles: File[]) => {
|
||||
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 className="dropzone" {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
{
|
||||
isDragActive ?
|
||||
<p>Drop the demo file here ...</p> :
|
||||
<p>Drag 'n' drop a demo here, or click to select a file</p>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function debounce(func: Function, timeout = 300) {
|
||||
let timer: any;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, timeout);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,208 +1,7 @@
|
|||
import {Packet} from "./parser";
|
||||
import React, {useCallback, Component} from 'react'
|
||||
import {useDropzone} from 'react-dropzone'
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {Header} from "./header";
|
||||
import {PacketDetails, PacketTable} from "./table";
|
||||
import {filterPacket, isSearchEmpty, SearchBar, SearchFilter} from "./search";
|
||||
import {ResponseMessageData} from "./rpc";
|
||||
|
||||
let _style = require('../styles/style.css');
|
||||
|
||||
export interface PacketMeta {
|
||||
index: number,
|
||||
tick: number,
|
||||
ty: PacketType,
|
||||
}
|
||||
|
||||
export enum PacketType {
|
||||
Signon = 1,
|
||||
Message = 2,
|
||||
SyncTick = 3,
|
||||
ConsoleCmd = 4,
|
||||
UserCmd = 5,
|
||||
DataTables = 6,
|
||||
Stop = 7,
|
||||
StringTables = 8,
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
loading: boolean,
|
||||
header: Header | null,
|
||||
progress: number,
|
||||
packets: PacketMeta[],
|
||||
prop_names: Map<string, { table: string, prop: string }>,
|
||||
class_names: Map<number, string>,
|
||||
active: Packet | null,
|
||||
activeIndex: number | null,
|
||||
search: SearchFilter,
|
||||
matches: number[],
|
||||
worker: Worker
|
||||
}
|
||||
|
||||
class App extends Component<{}, AppState> {
|
||||
state: AppState = {
|
||||
loading: false,
|
||||
header: null,
|
||||
progress: 0,
|
||||
packets: [],
|
||||
prop_names: new Map(),
|
||||
class_names: new Map(),
|
||||
active: null,
|
||||
activeIndex: null,
|
||||
search: {
|
||||
entity: 0,
|
||||
search: "",
|
||||
class_ids: [],
|
||||
prop_ids: [],
|
||||
},
|
||||
matches: [],
|
||||
worker: null
|
||||
}
|
||||
|
||||
onSearch = debounce((search: SearchFilter) => {
|
||||
if (!isSearchEmpty(search)) {
|
||||
console.log(search);
|
||||
this.state.worker.postMessage({type: "search", filter: search});
|
||||
}
|
||||
this.setState({search});
|
||||
}, 500)
|
||||
|
||||
load(data: ArrayBuffer) {
|
||||
this.setState({loading: true});
|
||||
const worker = new Worker('./worker.js');
|
||||
this.setState({worker});
|
||||
worker.addEventListener("message", (event: MessageEvent<ResponseMessageData>) => {
|
||||
const data = event.data;
|
||||
if (data.type !== "progress") {
|
||||
console.log(data);
|
||||
}
|
||||
switch (data.type) {
|
||||
case "progress":
|
||||
let progress = data.progress;
|
||||
this.setState({progress});
|
||||
break;
|
||||
case "done":
|
||||
const prop_names = new Map();
|
||||
for (let prop of data.prop_names) {
|
||||
prop_names.set(prop.identifier, prop);
|
||||
}
|
||||
const class_names = new Map();
|
||||
for (let c of data.class_names) {
|
||||
class_names.set(c.identifier, c.name);
|
||||
}
|
||||
this.setState({
|
||||
loading: false,
|
||||
packets: data.packets,
|
||||
header: data.header,
|
||||
prop_names,
|
||||
class_names
|
||||
});
|
||||
break;
|
||||
case "packet":
|
||||
this.setState({active: data.packet});
|
||||
break;
|
||||
case "search_result":
|
||||
this.setState({matches: data.matches});
|
||||
break;
|
||||
}
|
||||
});
|
||||
worker.postMessage({
|
||||
type: "data",
|
||||
data
|
||||
}, [data]);
|
||||
}
|
||||
|
||||
filteredPackets(): PacketMeta[] {
|
||||
if (isSearchEmpty(this.state.search)) {
|
||||
return this.state.packets;
|
||||
} else {
|
||||
return this.state.matches.map(index => this.state.packets[index]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading && this.state.progress > 0) {
|
||||
return (
|
||||
<>
|
||||
<h1>Loading</h1>
|
||||
<progress value={this.state.progress} max={100}/>
|
||||
</>
|
||||
)
|
||||
} else if (this.state.loading) {
|
||||
return (
|
||||
<>
|
||||
<h1>Loading</h1>
|
||||
</>
|
||||
)
|
||||
} else if (this.state.packets.length) {
|
||||
let active = <></>;
|
||||
const packets = this.filteredPackets();
|
||||
if (this.state.active) {
|
||||
active = <div className="details"><PacketDetails packet={this.state.active}
|
||||
search={this.state.search}
|
||||
prop_names={this.state.prop_names}
|
||||
class_names={this.state.class_names}/></div>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names}
|
||||
prop_names={this.state.prop_names}/>
|
||||
<div className="packets">
|
||||
<PacketTable packets={packets} class_names={this.state.class_names}
|
||||
activeIndex={this.state.activeIndex}
|
||||
prop_names={this.state.prop_names}
|
||||
onClick={(index) => {
|
||||
this.state.worker.postMessage({type: "get", packet: index})
|
||||
}}/>
|
||||
{active}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<DemoDropzone onDrop={(data) => this.load(data)}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
import React from 'react'
|
||||
import {createRoot} from "react-dom/client";
|
||||
import {App} from './App'
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render(<App/>);
|
||||
|
||||
function DemoDropzone({onDrop}: { onDrop: (data: ArrayBuffer) => void }) {
|
||||
const onDropCb = useCallback((acceptedFiles: File[]) => {
|
||||
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 className="dropzone" {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
{
|
||||
isDragActive ?
|
||||
<p>Drop the demo file here ...</p> :
|
||||
<p>Drag 'n' drop a demo here, or click to a file</p>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function debounce(func: Function, timeout = 300) {
|
||||
let timer: any;
|
||||
return (...args: any[]) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, timeout);
|
||||
};
|
||||
}
|
||||
109
www/src/rpc.ts
109
www/src/rpc.ts
|
|
@ -1,14 +1,105 @@
|
|||
import {SearchFilter} from "./search";
|
||||
import {Packet} from "./parser";
|
||||
import {Header} from "./header";
|
||||
import {PacketMeta} from "./index";
|
||||
import {PacketMeta} from "./App";
|
||||
import {Simulate} from "react-dom/test-utils";
|
||||
import progress = Simulate.progress;
|
||||
|
||||
export type RequestMessageData = {type: "data", data: ArrayBuffer}
|
||||
| {type: "get", packet: number}
|
||||
| {type: "search", filter: SearchFilter}
|
||||
export type RequestMessageData = { type: "data", sequence?: number, data: ArrayBuffer }
|
||||
| { type: "get", sequence?: number, packet: number }
|
||||
| { type: "search", sequence?: number, filter: SearchFilter }
|
||||
|
||||
export type ResponseMessageData = { type: "progress", progress: number }
|
||||
| { type: "packet", packet: Packet }
|
||||
| { type: "done", packets: PacketMeta[], header: Header, prop_names: { identifier: string, table: string, prop: string }[], class_names: { identifier: number, name: string }[] }
|
||||
| { type: "packet_names", packet: {} }
|
||||
| { type: "search_result", matches: number[] };
|
||||
export type ResponseMessageData = { type: "error", sequence: number, e: Error }
|
||||
|{ type: "progress", sequence: number, progress: number }
|
||||
| { type: "packet", sequence: number, packet: Packet }
|
||||
| { type: "done", sequence: number, packets: PacketMeta[], header: Header, prop_names: { identifier: string, table: string, prop: string }[], class_names: { identifier: number, name: string }[] }
|
||||
| { type: "search_result", sequence: number, matches: number[] };
|
||||
|
||||
const ResponseTypeMap = {
|
||||
"search": "search_result",
|
||||
"get": "packet",
|
||||
"data": "done",
|
||||
}
|
||||
|
||||
export interface ParsedDemo {
|
||||
packets: PacketMeta[],
|
||||
header: Header,
|
||||
prop_names: { identifier: string, table: string, prop: string }[],
|
||||
class_names: { identifier: number, name: string }[]
|
||||
}
|
||||
|
||||
export class DemoWorker {
|
||||
worker: Worker
|
||||
packets: PacketMeta[] = [];
|
||||
lastSequence = 0;
|
||||
callbacks: Map<number, [(_: ResponseMessageData) => void, (_: Error) => void]>;
|
||||
onProgress: null | ((progress: number) => void) = null;
|
||||
|
||||
constructor() {
|
||||
this.callbacks = new Map();
|
||||
this.worker = new Worker('./worker.js');
|
||||
this.worker.addEventListener("message", (event: MessageEvent<ResponseMessageData>) => {
|
||||
const data = event.data;
|
||||
const sequence = data.sequence;
|
||||
const [resolve, reject] = this.callbacks.get(sequence);
|
||||
if (data.type == "error") {
|
||||
this.callbacks.delete(sequence);
|
||||
reject(data.e);
|
||||
} else if (data.type === "progress") {
|
||||
if (this.onProgress) {
|
||||
this.onProgress(data.progress);
|
||||
}
|
||||
} else {
|
||||
this.callbacks.delete(sequence);
|
||||
resolve(event.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postMessage(message: RequestMessageData, transfer: Transferable[] = []): Promise<ResponseMessageData> {
|
||||
const sequence = this.lastSequence++;
|
||||
message.sequence = sequence;
|
||||
this.worker.postMessage(message, transfer);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.callbacks.set(sequence, [resolve, reject]);
|
||||
});
|
||||
}
|
||||
|
||||
public async load(data: ArrayBuffer, onProgress: (progress: number) => void): Promise<ParsedDemo> {
|
||||
this.onProgress = onProgress;
|
||||
const response = await this.postMessage({
|
||||
type: "data",
|
||||
data,
|
||||
}, [data]);
|
||||
this.onProgress = null;
|
||||
if (response.type === "done") {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error("Invalid response type");
|
||||
}
|
||||
}
|
||||
|
||||
public async get(packet: number): Promise<Packet> {
|
||||
const response = await this.postMessage({
|
||||
type: "get",
|
||||
packet,
|
||||
});
|
||||
if (response.type === "packet") {
|
||||
return response.packet;
|
||||
} else {
|
||||
throw new Error("Invalid response type");
|
||||
}
|
||||
}
|
||||
|
||||
public async search(filter: SearchFilter): Promise<number[]> {
|
||||
const response = await this.postMessage({
|
||||
type: "search",
|
||||
filter,
|
||||
});
|
||||
if (response.type === "search_result") {
|
||||
return response.matches;
|
||||
} else {
|
||||
throw new Error("Invalid response type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import {FixedSizeList as List} from 'react-window';
|
|||
import {MessageInfo} from "./packets/message";
|
||||
import {UserCmdDetails} from "./packets/usercmd";
|
||||
import {filterMessage, filterPacket, SearchFilter} from "./search";
|
||||
import {PacketMeta, PacketType} from "./index"
|
||||
import {PacketMeta, PacketType} from "./App"
|
||||
|
||||
interface TableProps {
|
||||
packets: PacketMeta[],
|
||||
|
|
|
|||
|
|
@ -7,32 +7,34 @@ let parser: Parser | null = null;
|
|||
|
||||
onmessage = function (event: MessageEvent<RequestMessageData>) {
|
||||
const data = event.data;
|
||||
const sequence = data.sequence;
|
||||
switch (data.type) {
|
||||
case "data":
|
||||
import("demo-inspector")
|
||||
.then(({Parser}) => {
|
||||
parser = new Parser(new Uint8Array(data.data), (progress: number) => {
|
||||
postMessage({type: "progress", progress})
|
||||
postMessage({type: "progress", progress, sequence})
|
||||
});
|
||||
postMessage({
|
||||
type: "done",
|
||||
packets: parser.packets(),
|
||||
header: parser.header(),
|
||||
prop_names: parser.prop_names(),
|
||||
class_names: parser.class_names()
|
||||
class_names: parser.class_names(),
|
||||
sequence
|
||||
})
|
||||
})
|
||||
break;
|
||||
case "search":
|
||||
if (parser) {
|
||||
const matches = parser.search(data.filter);
|
||||
postMessage({type: "search_result", matches: Array.prototype.slice.call(matches)})
|
||||
postMessage({type: "search_result", matches: Array.prototype.slice.call(matches), sequence})
|
||||
}
|
||||
break;
|
||||
case "get":
|
||||
if (parser) {
|
||||
const packet = parser.packet(data.packet);
|
||||
postMessage({type: "packet", packet})
|
||||
postMessage({type: "packet", packet, sequence})
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue