rework wip

This commit is contained in:
Robin Appelman 2022-09-02 22:11:48 +02:00
commit 93130dd16b
7 changed files with 261 additions and 175 deletions

View file

@ -12,8 +12,11 @@ default = ["console_error_panic_hook"]
[dependencies] [dependencies]
bitbuffer = { version = "0.10" } bitbuffer = { version = "0.10" }
tf-demo-parser = { version = "0.4", path = "../tf-demo-parser" } #tf-demo-parser = { version = "0.4", path = "../tf-demo-parser" }
tf-demo-parser = { version = "0.4", git = "https://github.com/demostf/parser" }
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1" serde_json = "1"
js-sys = "0.3.59"
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by

View file

@ -1,69 +1 @@
<div align="center"> tf2 demo inspector
<h1><code>wasm-pack-template</code></h1>
<strong>A template for kick starting a Rust and WebAssembly project using <a href="https://github.com/rustwasm/wasm-pack">wasm-pack</a>.</strong>
<p>
<a href="https://travis-ci.org/rustwasm/wasm-pack-template"><img src="https://img.shields.io/travis/rustwasm/wasm-pack-template.svg?style=flat-square" alt="Build Status" /></a>
</p>
<h3>
<a href="https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html">Tutorial</a>
<span> | </span>
<a href="https://discordapp.com/channels/442252698964721669/443151097398296587">Chat</a>
</h3>
<sub>Built with 🦀🕸 by <a href="https://rustwasm.github.io/">The Rust and WebAssembly Working Group</a></sub>
</div>
## About
[**📚 Read this template tutorial! 📚**][template-docs]
This template is designed for compiling Rust libraries into WebAssembly and
publishing the resulting package to NPM.
Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other
templates and usages of `wasm-pack`.
[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html
[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html
## 🚴 Usage
### 🐑 Use `cargo generate` to Clone this Template
[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate)
```
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project
```
### 🛠️ Build with `wasm-pack build`
```
wasm-pack build
```
### 🔬 Test in Headless Browsers with `wasm-pack test`
```
wasm-pack test --headless --firefox
```
### 🎁 Publish to NPM with `wasm-pack publish`
```
wasm-pack publish
```
## 🔋 Batteries Included
* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating
between WebAssembly and JavaScript.
* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook)
for logging panic messages to the developer console.
* [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized
for small code size.

View file

@ -1,40 +1,99 @@
mod utils; mod utils;
use wasm_bindgen::prelude::*;
use crate::utils::set_panic_hook; use crate::utils::set_panic_hook;
use tf_demo_parser::demo::parser::{DemoHandler, NullHandler}; use bitbuffer::{BitRead, BitReadBuffer, BitReadStream, LittleEndian};
use js_sys::Function;
use serde::Serialize;
use tf_demo_parser::demo::header::Header; use tf_demo_parser::demo::header::Header;
use tf_demo_parser::demo::packet::datatable::{DataTablePacket, SendTableName, ServerClassName};
use tf_demo_parser::demo::packet::Packet;
use tf_demo_parser::demo::parser::RawPacketStream; use tf_demo_parser::demo::parser::RawPacketStream;
use bitbuffer::{BitRead, LittleEndian, BitReadBuffer, BitReadStream}; use tf_demo_parser::demo::parser::{DemoHandler, NullHandler};
use tf_demo_parser::demo::sendprop::SendPropName;
use wasm_bindgen::prelude::*;
#[cfg(feature = "wee_alloc")] #[cfg(feature = "wee_alloc")]
#[global_allocator] #[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[derive(Debug, Serialize)]
pub struct PacketMeta {
index: usize,
tick: u32,
ty: u8,
}
#[wasm_bindgen] #[wasm_bindgen]
pub struct Parser { pub struct Parser {
handler: DemoHandler<'static, NullHandler>,
header: Header, header: Header,
packets: RawPacketStream<'static>, packets: Vec<Packet<'static>>,
prop_names: Vec<(u64, SendTableName, SendPropName)>,
class_names: Vec<(u16, ServerClassName)>,
} }
#[wasm_bindgen] #[wasm_bindgen]
impl Parser { impl Parser {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(input: Vec<u8>) -> Self { pub fn new(input: Vec<u8>, progress: Function) -> Self {
set_panic_hook();
let buffer = BitReadBuffer::new_owned(input, LittleEndian); let buffer = BitReadBuffer::new_owned(input, LittleEndian);
let mut stream = BitReadStream::new(buffer); let mut stream = BitReadStream::new(buffer);
let header = Header::read(&mut stream).unwrap(); let header = Header::read(&mut stream).unwrap();
let packets = RawPacketStream::new(stream); let mut packet_stream = RawPacketStream::new(stream);
let mut handler = DemoHandler::default(); let mut handler = DemoHandler::default();
handler.handle_header(&header); handler.handle_header(&header);
let mut packets = Vec::new();
let mut last_progress = 0.0;
let mut prop_names = Vec::new();
let mut class_names = Vec::new();
while let Some(packet) = packet_stream.next(handler.get_parser_state()).unwrap() {
let tick = packet.tick();
packets.push(packet.clone());
if let Packet::DataTables(DataTablePacket {
tables,
server_classes,
..
}) = &packet
{
for table in tables {
for prop in &table.props {
prop_names.push((
prop.identifier.into(),
prop.identifier
.table_name()
.unwrap_or_else(|| table.name.clone()),
prop.identifier
.prop_name()
.unwrap_or_else(|| prop.name.clone()),
));
}
}
for class in server_classes {
class_names.push((class.id.into(), class.name.clone()))
}
}
handler.handle_packet(packet).unwrap();
let new_progress = ((tick as f32 / header.ticks as f32) * 100.0).floor();
if new_progress > last_progress {
last_progress = new_progress;
progress
.call1(&JsValue::NULL, &JsValue::from(new_progress))
.unwrap();
}
}
Parser { Parser {
handler,
header, header,
packets packets,
prop_names,
class_names,
} }
} }
@ -42,11 +101,60 @@ impl Parser {
JsValue::from_serde(&self.header).unwrap() JsValue::from_serde(&self.header).unwrap()
} }
pub fn next(&mut self) -> JsValue { pub fn packets(&self) -> Vec<JsValue> {
self.packets.next(&self.handler.state_handler).unwrap().map(|packet| { self.packets
let out = JsValue::from_serde(&packet).unwrap(); .iter()
self.handler.handle_packet(packet).unwrap(); .enumerate()
out .map(|(index, packet)| PacketMeta {
}).unwrap_or(JsValue::NULL) index,
tick: packet.tick(),
ty: packet.packet_type() as u8,
})
.map(|meta| JsValue::from_serde(&meta).unwrap())
.collect()
}
pub fn packet(&self, index: usize) -> JsValue {
JsValue::from_serde(&self.packets[index]).unwrap()
}
pub fn prop_names(&self) -> Vec<JsValue> {
self.prop_names
.iter()
.map(|(identifier, table, prop)| {
JsValue::from_serde(&PropName {
identifier: *identifier,
table: table.to_string(),
prop: prop.to_string(),
})
.unwrap()
})
.collect()
}
pub fn class_names(&self) -> Vec<JsValue> {
self.class_names
.iter()
.map(|(identifier, name)| {
JsValue::from_serde(&ClassName {
identifier: *identifier,
name: name.to_string(),
})
.unwrap()
})
.collect()
} }
} }
#[derive(Serialize)]
pub struct PropName {
pub identifier: u64,
pub table: String,
pub prop: String,
}
#[derive(Serialize)]
pub struct ClassName {
pub identifier: u16,
pub name: String,
}

View file

@ -8,21 +8,46 @@ import {filterPacket, Search, SearchBar} from "./search";
let _style = require('../styles/style.css'); 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 { interface AppState {
loading: boolean, loading: boolean,
header: Header | null, header: Header | null,
packets: Packet[], progress: number,
packets: PacketMeta[],
prop_names: Map<number, { table: string, prop: string }>, prop_names: Map<number, { table: string, prop: string }>,
class_names: Map<number, string>, class_names: Map<number, string>,
active: Packet | null, active: Packet | null,
activeIndex: number | null, activeIndex: number | null,
search: Search, search: Search,
worker: Worker
} }
type MessageData = { type: "progress", progress: number }
| { type: "packet", packet: Packet }
| { type: "done", packets: PacketMeta[], header: Header, prop_names: {identifier: number, table: string, prop: string}[], class_names: {identifier: number, name: string}[] }
| { type: "packet_names", packet: {} };
class App extends Component<{}, AppState> { class App extends Component<{}, AppState> {
state: AppState = { state: AppState = {
loading: false, loading: false,
header: null, header: null,
progress: 0,
packets: [], packets: [],
prop_names: new Map(), prop_names: new Map(),
class_names: new Map(), class_names: new Map(),
@ -33,7 +58,8 @@ class App extends Component<{}, AppState> {
filter: "", filter: "",
classIds: [], classIds: [],
propIds: [], propIds: [],
} },
worker: null
} }
onSearch = debounce((search: Search) => this.setState({search}), 500) onSearch = debounce((search: Search) => this.setState({search}), 500)
@ -41,54 +67,53 @@ class App extends Component<{}, AppState> {
load(data: ArrayBuffer) { load(data: ArrayBuffer) {
this.setState({loading: true}); this.setState({loading: true});
const worker = new Worker('./worker.js'); const worker = new Worker('./worker.js');
worker.addEventListener("message", (event: MessageEvent<{ type: "header", header: Header } | { type: "packet", packet: Packet } | { type: "done" }>) => { this.setState({worker});
switch (event.data.type) { worker.addEventListener("message", (event: MessageEvent<MessageData>) => {
case "header": const data = event.data;
let header = event.data.header; if (data.type !== "progress") {
this.setState({header}); console.log(data);
break;
case "packet":
let packet = event.data.packet;
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});
} }
switch (data.type) {
case "progress":
let progress = data.progress;
this.setState({progress});
break; break;
case "done": case "done":
this.setState({loading: false}); 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; break;
case "packet":
this.setState({active: data.packet})
} }
}); });
worker.postMessage(data, [data]); worker.postMessage({
type: "data",
data
}, [data]);
} }
filteredPackets(): Packet[] { filteredPackets(): PacketMeta[] {
if (this.state.search.filter || this.state.search.entity) {
return this.state.packets.filter(packet => filterPacket(packet, this.state.search));
} else {
return this.state.packets; return this.state.packets;
} // if (this.state.search.filter || this.state.search.entity) {
// return this.state.packets.filter(packet => filterPacket(packet, this.state.search));
// } else {
// return this.state.packets;
// }
} }
render() { render() {
if (this.state.loading && this.state.header && this.state.packets.length) { if (this.state.loading && this.state.progress > 0) {
return ( return (
<> <>
<h1>Loading</h1> <h1>Loading</h1>
<p>{this.state.packets.slice(-1)[0].tick}/{this.state.header.ticks}</p> <progress value={this.state.progress} max={100}/>
</> </>
) )
} else if (this.state.loading) { } else if (this.state.loading) {
@ -107,12 +132,15 @@ class App extends Component<{}, AppState> {
} }
return ( return (
<> <>
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names} prop_names={this.state.prop_names}/> <SearchBar onSearch={this.onSearch} class_names={this.state.class_names}
prop_names={this.state.prop_names}/>
<div className="packets"> <div className="packets">
<PacketTable packets={this.filteredPackets()} class_names={this.state.class_names} <PacketTable packets={this.filteredPackets()} class_names={this.state.class_names}
activeIndex={this.state.activeIndex} activeIndex={this.state.activeIndex}
prop_names={this.state.prop_names} prop_names={this.state.prop_names}
onClick={(activeIndex, active) => this.setState({activeIndex, active})}/> onClick={(index) => {
this.state.worker.postMessage({type: "get", packet: index})
}}/>
{active} {active}
</div> </div>
</> </>
@ -166,10 +194,12 @@ function DemoDropzone(
) )
} }
function debounce(func: Function, timeout = 300){ function debounce(func: Function, timeout = 300) {
let timer: any; let timer: any;
return (...args:any[]) => { return (...args: any[]) => {
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout); timer = setTimeout(() => {
func.apply(this, args);
}, timeout);
}; };
} }

View file

@ -80,14 +80,15 @@ function formatPropValue(value: SendPropValue): string {
function formatEntity(entity: PacketEntity, prop_names: Map<number, { table: String, prop: String }>, class_names: Map<number, String>,): string { 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 class_name = class_names.get(entity.server_class);
let baseline = entity.baseline_props.map(prop => { // let baseline = entity.baseline_props.map(prop => {
let names = prop_names.get(prop.identifier); // let names = prop_names.get(prop.identifier);
if (names) { // if (names) {
return `(${names.table}.${names.prop})=${formatPropValue(prop.value)}`; // return `(${names.table}.${names.prop})=${formatPropValue(prop.value)}`;
} else { // } else {
return `([unknown prop])=${prop.value}`; // return `([unknown prop])=${prop.value}`;
} // }
}) // })
let props = entity.props.map(prop => { let props = entity.props.map(prop => {
let names = prop_names.get(prop.identifier); let names = prop_names.get(prop.identifier);
if (names) { if (names) {
@ -96,7 +97,7 @@ function formatEntity(entity: PacketEntity, prop_names: Map<number, { table: Str
return `[unknown prop]=${prop.value}`; return `[unknown prop]=${prop.value}`;
} }
}) })
return `entity ${entity.entity_index}(${class_name}) ${entity.update_type}: ` + baseline.concat(props).join(', '); return `entity ${entity.entity_index}(${class_name}) ${entity.update_type}: ` + props.join(', ');
} }
function formatEventDefinition(event: GameEventDefinition): string { function formatEventDefinition(event: GameEventDefinition): string {

View file

@ -4,20 +4,21 @@ import {FixedSizeList as List} from 'react-window';
import {MessageInfo} from "./packets/message"; import {MessageInfo} from "./packets/message";
import {UserCmdDetails} from "./packets/usercmd"; import {UserCmdDetails} from "./packets/usercmd";
import {filterMessage, filterPacket, Search} from "./search"; import {filterMessage, filterPacket, Search} from "./search";
import {PacketMeta, PacketType} from "./index"
interface TableProps { interface TableProps {
packets: Packet[], packets: PacketMeta[],
prop_names: Map<number, { table: string, prop: string }>, prop_names: Map<number, { table: string, prop: string }>,
class_names: Map<number, string>, class_names: Map<number, string>,
onClick: (i: number, packet: Packet) => void, onClick: (i: number) => void,
activeIndex: number | null, activeIndex: number | null,
} }
export function PacketTable({packets, prop_names, class_names, onClick, activeIndex}: TableProps) { export function PacketTable({packets, prop_names, class_names, onClick, activeIndex}: TableProps) {
const Row: (props: { index: number, style: CSSProperties }) => any = ({index, style}) => ( const Row: (props: { index: number, style: CSSProperties }) => any = ({index, style}) => (
<p key={index} onClick={() => { <p key={packets[index].index} onClick={() => {
onClick(index, packets[index]) onClick(packets[index].index)
}} style={style} className={(activeIndex == index ? 'active ' : '') + 'prop_row'}> }} style={style} className={(activeIndex == packets[index].index ? 'active ' : '') + 'prop_row'}>
<PacketRow packet={packets[index]}/> <PacketRow packet={packets[index]}/>
</p> </p>
); );
@ -32,46 +33,46 @@ export function PacketTable({packets, prop_names, class_names, onClick, activeIn
} }
interface RowProps { interface RowProps {
packet: Packet, packet: PacketMeta,
} }
export function PacketRow({packet}: RowProps) { export function PacketRow({packet}: RowProps) {
switch (packet.type) { switch (packet.ty) {
case "Signon": case PacketType.Signon:
case "Message": case PacketType.Message:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</> </>
case "SyncTick": case PacketType.SyncTick:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</>; </>;
case "ConsoleCmd": case PacketType.ConsoleCmd:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</>; </>;
case "UserCmd": case PacketType.UserCmd:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</>; </>;
case "DataTables": case PacketType.DataTables:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</>; </>;
case "Stop": case PacketType.Stop:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</>; </>;
case "StringTables": case PacketType.StringTables:
return <> return <>
<span className="tick">{packet.tick}</span> <span className="tick">{packet.tick}</span>
<span className="type">{packet.type}</span> <span className="type">{PacketType[packet.ty]}</span>
</>; </>;
} }
} }

View file

@ -2,22 +2,33 @@ import {Parser} from "demo-inspector";
declare function postMessage(message: any): void; declare function postMessage(message: any): void;
onmessage = function (event) { let parser: Parser | null = null;
let data = event.data as ArrayBuffer;
type MessageData = {type: "data", data: ArrayBuffer} | {type: "get", packet: number}
onmessage = function (event: MessageEvent<MessageData>) {
const data = event.data;
switch (data.type) {
case "data":
import("demo-inspector") import("demo-inspector")
.then(({Parser}) => { .then(({Parser}) => {
console.log(data); parser = new Parser(new Uint8Array(data.data), (progress: number) => {
let parser = new Parser(new Uint8Array(data)); postMessage({type: "progress", progress})
});
postMessage({type: "header", header: parser.header()}) postMessage({
type: "done",
let packet; packets: parser.packets(),
do { header: parser.header(),
packet = parser.next(); prop_names: parser.prop_names(),
if (packet) { class_names: parser.class_names()
})
})
break;
case "get":
if (parser) {
const packet = parser.packet(data.packet);
postMessage({type: "packet", packet}) postMessage({type: "packet", packet})
} }
} while (packet); break;
postMessage({type: "done"}) }
})
}; };