This commit is contained in:
Robin Appelman 2022-09-03 17:06:57 +02:00
commit 8de1835d29
10 changed files with 282 additions and 73 deletions

View file

@ -37,3 +37,6 @@ wasm-bindgen-test = "0.3.13"
[profile.release]
lto = true
[package.metadata.wasm-pack.profile.release]
wasm-opt = true

View file

@ -1,14 +1,15 @@
mod search;
mod utils;
use crate::utils::set_panic_hook;
use crate::search::{packet_matches, SearchFilter};
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::packet::datatable::{DataTablePacket, SendTableName, ServerClassName};
use tf_demo_parser::demo::packet::Packet;
use tf_demo_parser::demo::parser::DemoHandler;
use tf_demo_parser::demo::parser::RawPacketStream;
use tf_demo_parser::demo::parser::{DemoHandler, NullHandler};
use tf_demo_parser::demo::sendprop::SendPropName;
use wasm_bindgen::prelude::*;
@ -144,6 +145,15 @@ impl Parser {
})
.collect()
}
pub fn search(&self, filter: JsValue) -> Vec<usize> {
let filter: SearchFilter = filter.into_serde().unwrap();
self.packets
.iter()
.enumerate()
.filter_map(|(index, packet)| packet_matches(packet, &filter).then_some(index))
.collect()
}
}
#[derive(Serialize)]

154
src/search.rs Normal file
View file

@ -0,0 +1,154 @@
use serde::{Deserialize, Serialize};
use tf_demo_parser::demo::message::gameevent::GameEventMessage;
use tf_demo_parser::demo::message::packetentities::{PacketEntitiesMessage, PacketEntity};
use tf_demo_parser::demo::message::setconvar::SetConVarMessage;
use tf_demo_parser::demo::message::stringtable::{
CreateStringTableMessage, UpdateStringTableMessage,
};
use tf_demo_parser::demo::message::tempentities::TempEntitiesMessage;
use tf_demo_parser::demo::message::{
EntityMessage, FileMessage, GetCvarValueMessage, Message, PrintMessage, SetViewMessage,
StringCmdMessage,
};
use tf_demo_parser::demo::packet::consolecmd::ConsoleCmdPacket;
use tf_demo_parser::demo::packet::message::MessagePacket;
use tf_demo_parser::demo::packet::stringtable::{StringTableEntry, StringTablePacket};
use tf_demo_parser::demo::packet::Packet;
#[derive(Serialize, Deserialize)]
pub struct SearchFilter {
pub entity: u32,
pub search: String,
#[serde(default)]
pub prop_ids: Vec<u64>,
#[serde(default)]
pub class_ids: Vec<u32>,
}
impl SearchFilter {
pub fn has_entity_filter(&self) -> bool {
!self.search.is_empty() || !self.prop_ids.is_empty() || !self.class_ids.is_empty()
}
}
pub fn packet_matches(packet: &Packet, filter: &SearchFilter) -> bool {
// return false;
// if packet
// .packet_type()
// .as_lowercase_str()
// .contains(&filter.search)
// {
// return true;
// }
match packet {
Packet::Signon(MessagePacket { messages, .. })
| Packet::Message(MessagePacket { messages, .. }) => messages
.iter()
.any(|message| message_matches(message, filter)),
Packet::SyncTick(_) => false,
Packet::ConsoleCmd(ConsoleCmdPacket { command, .. }) => command.contains(&filter.search),
Packet::UserCmd(_) => false,
Packet::DataTables(_) => false,
Packet::Stop(_) => false,
Packet::StringTables(StringTablePacket { tables, .. }) => tables.iter().any(|table| {
table.name.contains(&filter.search)
|| table.entries.iter().any(|(_, entry)| {
entry
.text
.as_deref()
.map(|text| text.contains(&filter.search))
.unwrap_or_default()
})
}),
}
}
fn message_matches(message: &Message, filter: &SearchFilter) -> bool {
let has_search = !filter.search.is_empty();
match message {
Message::File(FileMessage { file_name, .. }) => {
has_search && file_name.contains(&filter.search)
}
Message::StringCmd(StringCmdMessage { command, .. }) => {
has_search && command.contains(&filter.search)
}
Message::SetConVar(SetConVarMessage { vars, .. }) => {
has_search
&& vars.iter().any(|var| {
var.key.contains(&filter.search) || var.value.contains(&filter.search)
})
}
Message::Print(PrintMessage { value }) => {
has_search && value.as_ref().contains(&filter.search)
}
Message::CreateStringTable(CreateStringTableMessage { table, .. }) => {
has_search && table.name.contains(&filter.search)
|| table
.entries
.iter()
.any(|(_, entry)| string_entry_matches(entry, filter))
}
Message::UpdateStringTable(UpdateStringTableMessage { entries, .. }) => {
has_search
&& entries
.iter()
.any(|(_, entry)| string_entry_matches(entry, filter))
}
Message::SetView(SetViewMessage { index }) => (*index as u32) == filter.entity,
Message::UserMessage(_) => false,
Message::EntityMessage(EntityMessage { class_id, .. }) => {
filter.class_ids.contains(&(*class_id as u32))
}
Message::GameEvent(GameEventMessage { event, .. }) => {
has_search && event.event_type().as_str().contains(&filter.search)
}
Message::PacketEntities(PacketEntitiesMessage {
entities,
removed_entities,
..
}) => {
(removed_entities.contains(&filter.entity.into()) && !filter.has_entity_filter())
|| entities.iter().any(|entity| entity_matches(entity, filter))
}
Message::TempEntities(TempEntitiesMessage { events }) => events.iter().any(|event| {
filter
.class_ids
.contains(&(u16::from(event.class_id).into()))
}),
Message::GetCvarValue(GetCvarValueMessage { value, .. }) => {
has_search && value.contains(&filter.search)
}
_ => false,
}
}
fn string_entry_matches(entry: &StringTableEntry, filter: &SearchFilter) -> bool {
(!filter.search.is_empty())
&& entry
.text
.as_deref()
.map(|text| text.contains(&filter.search))
.unwrap_or_default()
}
fn entity_matches(entity: &PacketEntity, filter: &SearchFilter) -> bool {
if entity.entity_index != filter.entity && filter.entity != 0 {
return false;
}
if !filter.has_entity_filter() {
return true;
}
if filter
.class_ids
.contains(&u16::from(entity.server_class).into())
{
return true;
}
entity
.props
.iter()
.any(|prop| filter.prop_ids.contains(&prop.identifier.into()))
}

View file

@ -1,3 +1,4 @@
#[allow(dead_code)]
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then

View file

@ -2,9 +2,11 @@ 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, Search, SearchBar} from "./search";
import {filterPacket, isSearchEmpty, SearchBar, SearchFilter} from "./search";
import {ResponseMessageData} from "./rpc";
let _style = require('../styles/style.css');
@ -34,15 +36,11 @@ interface AppState {
class_names: Map<number, string>,
active: Packet | null,
activeIndex: number | null,
search: Search,
search: SearchFilter,
matches: number[],
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> {
state: AppState = {
loading: false,
@ -55,20 +53,27 @@ class App extends Component<{}, AppState> {
activeIndex: null,
search: {
entity: 0,
filter: "",
classIds: [],
propIds: [],
search: "",
class_ids: [],
prop_ids: [],
},
matches: [],
worker: null
}
onSearch = debounce((search: Search) => this.setState({search}), 500)
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<MessageData>) => {
worker.addEventListener("message", (event: MessageEvent<ResponseMessageData>) => {
const data = event.data;
if (data.type !== "progress") {
console.log(data);
@ -96,7 +101,11 @@ class App extends Component<{}, AppState> {
});
break;
case "packet":
this.setState({active: data.packet})
this.setState({active: data.packet});
break;
case "search_result":
this.setState({matches: data.matches});
break;
}
});
worker.postMessage({
@ -106,12 +115,11 @@ class App extends Component<{}, AppState> {
}
filteredPackets(): PacketMeta[] {
if (isSearchEmpty(this.state.search)) {
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;
// }
} else {
return this.state.matches.map(index => this.state.packets[index]);
}
}
render() {
@ -130,6 +138,7 @@ class App extends Component<{}, AppState> {
)
} 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}
@ -141,7 +150,7 @@ class App extends Component<{}, AppState> {
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names}
prop_names={this.state.prop_names}/>
<div className="packets">
<PacketTable packets={this.filteredPackets()} class_names={this.state.class_names}
<PacketTable packets={packets} class_names={this.state.class_names}
activeIndex={this.state.activeIndex}
prop_names={this.state.prop_names}
onClick={(index) => {
@ -161,12 +170,9 @@ class App extends Component<{}, AppState> {
}
}
ReactDOM.render(
<App/>
,
document.getElementById("root")
);
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App/>);
function DemoDropzone({onDrop}: { onDrop: (data: ArrayBuffer) => void }) {
const onDropCb = useCallback((acceptedFiles: File[]) => {

View file

@ -1,12 +1,12 @@
import {EventInfo, GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser";
import React from "react";
import {filterEntity, filterMessage, Search} from "../search";
import {filterEntity, filterMessage, isSearchEmpty, SearchFilter} from "../search";
export interface MessageInfoProps {
msg: Message,
prop_names: Map<number, { table: String, prop: String }>,
class_names: Map<number, String>,
search: Search
search: SearchFilter
}
export function MessageInfo({msg, prop_names, class_names, search}: MessageInfoProps) {
@ -105,22 +105,22 @@ function formatEventDefinition(event: GameEventDefinition): string {
return `${event.event_type}{${values.join(', ')}}`;
}
function filteredEntities(entities: PacketEntity[], search: Search) {
if (search.filter || search.entity) {
function filteredEntities(entities: PacketEntity[], search: SearchFilter) {
if (!isSearchEmpty(search)) {
return entities.filter(entities => {
return (search.entity == 0 || search.entity == entities.entity_index) &&
(search.filter.length < 3 || filterEntity(entities.server_class, entities.baseline_props.concat(entities.props), search))
(search.search.length < 3 || filterEntity(entities.server_class, entities.props, search))
});
} else {
return entities;
}
}
function filteredTempEntities(entities: EventInfo[], search: Search) {
function filteredTempEntities(entities: EventInfo[], search: SearchFilter) {
if (search.entity) {
return [];
}
if (search.filter) {
if (!isSearchEmpty(search)) {
return entities.filter(entities => filterEntity(entities.class_id, entities.props, search));
} else {
return entities;

14
www/src/rpc.ts Normal file
View file

@ -0,0 +1,14 @@
import {SearchFilter} from "./search";
import {Packet} from "./parser";
import {Header} from "./header";
import {PacketMeta} from "./index";
export type RequestMessageData = {type: "data", data: ArrayBuffer}
| {type: "get", packet: number}
| {type: "search", filter: SearchFilter}
export type ResponseMessageData = { 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: {} }
| { type: "search_result", matches: number[] };

View file

@ -3,15 +3,15 @@ import React, {ChangeEvent, Component} from "react";
import './search.css'
import {Message, Packet, SendProp, StringTable} from "./parser";
export interface Search {
filter: string,
export interface SearchFilter {
entity: number,
propIds: number[],
classIds: number[],
search: string,
prop_ids: number[],
class_ids: number[],
}
export interface SearchBarProps {
onSearch: (search: Search) => void,
onSearch: (search: SearchFilter) => void,
prop_names: Map<number, { table: string, prop: string }>,
class_names: Map<number, string>,
}
@ -27,12 +27,12 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
entity: 0
}
getSearch(): Search {
getSearch(): SearchFilter {
return {
filter: this.state.filter,
search: this.state.filter,
entity: this.state.entity,
propIds: filterPropNames(this.props.prop_names, this.state.filter),
classIds: filterClassNames(this.props.class_names, this.state.filter),
prop_ids: filterPropNames(this.props.prop_names, this.state.filter),
class_ids: filterClassNames(this.props.class_names, this.state.filter),
}
}
@ -70,7 +70,7 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
export function filterPacket(
packet: Packet,
search: Search,
search: SearchFilter,
): boolean {
switch (packet.type) {
case "Signon":
@ -79,7 +79,7 @@ export function filterPacket(
case "SyncTick":
return false;
case "ConsoleCmd":
return search.entity == 0 && packet.command.includes(search.filter);
return search.entity == 0 && packet.command.includes(search.search);
case "UserCmd":
return false;
case "DataTables":
@ -92,6 +92,9 @@ export function filterPacket(
}
function filterPropNames(prop_names: Map<number, { table: string, prop: string }>, filter: string): number[] {
if (filter.length === 0) {
return [];
}
filter = filter.toLowerCase();
let ids = [];
for (let [id, {table, prop}] of prop_names.entries()) {
@ -103,6 +106,9 @@ function filterPropNames(prop_names: Map<number, { table: string, prop: string }
}
function filterClassNames(class_names: Map<number, string>, filter: string): number[] {
if (filter.length === 0) {
return [];
}
filter = filter.toLowerCase();
let ids = [];
for (let [id, name] of class_names.entries()) {
@ -115,54 +121,64 @@ function filterClassNames(class_names: Map<number, string>, filter: string): num
export function filterMessage(
message: Message,
search: Search,
search: SearchFilter,
): boolean {
switch (message.type) {
case "File":
return search.entity == 0 && message.file_name.includes(search.filter);
return search.entity == 0 && message.file_name.includes(search.search);
case "StringCmd":
return search.entity == 0 && message.command.includes(search.filter);
return search.entity == 0 && message.command.includes(search.search);
case "SetConVar":
return search.entity == 0 && message.vars.some(cvar => cvar.value.includes(search.filter) || cvar.key.includes(search.filter));
return search.entity == 0 && message.vars.some(cvar => cvar.value.includes(search.search) || cvar.key.includes(search.search));
case "Print":
return search.entity == 0 && message.value.includes(search.filter);
return search.entity == 0 && message.value.includes(search.search);
case "ClassInfo":
return search.entity == 0 && message.entries.some(entry => entry.class_name.includes(search.filter) || entry.table_name.includes(search.filter));
return search.entity == 0 && message.entries.some(entry => entry.class_name.includes(search.search) || entry.table_name.includes(search.search));
case "CreateStringTable":
return search.entity == 0 && filterStringTable(message.table, search);
case "UpdateStringTable":
return search.entity == 0 && message.entries.some(([_index, entry]) => (entry.text && entry.text.includes(search.filter)));
return search.entity == 0 && message.entries.some(([_index, entry]) => (entry.text && entry.text.includes(search.search)));
case "SetView":
return search.entity == 0 && message.index === search.entity;
case "SayText2":
return search.entity == 0 && ((message.text && message.text.includes(search.filter)) || (message.from && message.from.includes(search.filter)));
return search.entity == 0 && ((message.text && message.text.includes(search.search)) || (message.from && message.from.includes(search.search)));
case "Text":
return search.entity == 0 && message.text.includes(search.filter);
return search.entity == 0 && message.text.includes(search.search);
case "EntityMessage":
return search.entity == 0 && search.classIds.includes(message.class_id)
return search.entity == 0 && search.class_ids.includes(message.class_id)
case "GameEvent":
return search.entity == 0 && message.event.type.includes(search.filter)
return search.entity == 0 && message.event.type.includes(search.search)
case "PacketEntities":
return message.removed_entities.includes(search.entity) || message.entities.some(entity => (search.entity == 0 || entity.entity_index == search.entity) && filterEntity(entity.server_class, entity.baseline_props.concat(entity.props), search))
return message.removed_entities.includes(search.entity)
|| message.entities.some(entity => (search.entity == 0 || entity.entity_index == search.entity)
&& filterEntity(entity.server_class, entity.props, search))
case "TempEntities":
return search.entity == 0 && message.events.some(event => filterEntity(event.class_id, event.props, search))
case "GetCvarValue":
return search.entity == 0 && message.value.includes(search.filter);
return search.entity == 0 && message.value.includes(search.search);
default:
return false;
}
}
export function filterEntity(class_id: number, props: SendProp[], search: Search): boolean {
return search.classIds.includes(class_id) || props.some(prop => search.propIds.includes(prop.identifier))
|| props.some(prop => prop.value == search.filter);
export function filterEntity(class_id: number, props: SendProp[], search: SearchFilter): boolean {
if (search.search.length === 0 && search.class_ids.length === 0 && search.prop_ids.length === 0) {
return true;
}
function filterStringTable(table: StringTable, search: Search): boolean {
if (table.name.includes(search.filter)) {
return search.class_ids.includes(class_id) || props.some(prop => search.prop_ids.includes(prop.identifier))
|| props.some(prop => prop.value == search.search);
}
function filterStringTable(table: StringTable, search: SearchFilter): boolean {
if (table.name.includes(search.search)) {
return true;
} else if (table.entries.some(([_index, entry]) => entry.text.includes(search.filter))) {
} else if (table.entries.some(([_index, entry]) => entry.text.includes(search.search))) {
return true;
}
return false;
}
export function isSearchEmpty(filter: SearchFilter) {
return filter.search.length === 0 && filter.entity === 0 && filter.class_ids.length === 0 && filter.prop_ids.length === 0
}

View file

@ -3,7 +3,7 @@ import {GameEventDefinition, Message, Packet, PacketEntity, SendPropValue, UserC
import {FixedSizeList as List} from 'react-window';
import {MessageInfo} from "./packets/message";
import {UserCmdDetails} from "./packets/usercmd";
import {filterMessage, filterPacket, Search} from "./search";
import {filterMessage, filterPacket, SearchFilter} from "./search";
import {PacketMeta, PacketType} from "./index"
interface TableProps {
@ -81,11 +81,11 @@ interface DetailProps {
packet: Packet,
prop_names: Map<number, { table: string, prop: string }>,
class_names: Map<number, string>,
search: Search,
search: SearchFilter,
}
function filteredMessages(messages: Message[], search: Search) {
if (search.filter || search.entity) {
function filteredMessages(messages: Message[], search: SearchFilter) {
if (search.search || search.entity) {
return messages.filter(message => filterMessage(message, search));
} else {
return messages;

View file

@ -1,12 +1,11 @@
import {Parser} from "demo-inspector";
import {RequestMessageData, ResponseMessageData} from "./rpc";
declare function postMessage(message: any): void;
declare function postMessage(message: ResponseMessageData): void;
let parser: Parser | null = null;
type MessageData = {type: "data", data: ArrayBuffer} | {type: "get", packet: number}
onmessage = function (event: MessageEvent<MessageData>) {
onmessage = function (event: MessageEvent<RequestMessageData>) {
const data = event.data;
switch (data.type) {
case "data":
@ -24,6 +23,12 @@ onmessage = function (event: MessageEvent<MessageData>) {
})
})
break;
case "search":
if (parser) {
const matches = parser.search(data.filter);
postMessage({type: "search_result", matches: Array.prototype.slice.call(matches)})
}
break;
case "get":
if (parser) {
const packet = parser.packet(data.packet);