filterbar

This commit is contained in:
Robin Appelman 2021-08-01 19:38:12 +02:00
commit aa20564ccd
6 changed files with 291 additions and 26 deletions

View file

@ -4,6 +4,7 @@ import {useDropzone} from 'react-dropzone'
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import {Header} from "./header"; import {Header} from "./header";
import {PacketDetails, PacketTable} from "./table"; import {PacketDetails, PacketTable} from "./table";
import {filterPacket, Search, SearchBar} from "./search";
let _style = require('../styles/style.css'); let _style = require('../styles/style.css');
@ -11,10 +12,11 @@ interface AppState {
loading: boolean, loading: boolean,
header: Header | null, header: Header | null,
packets: Packet[], packets: Packet[],
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,
} }
class App extends Component<{}, AppState> { class App extends Component<{}, AppState> {
@ -26,7 +28,15 @@ class App extends Component<{}, AppState> {
class_names: new Map(), class_names: new Map(),
active: null, active: null,
activeIndex: null, activeIndex: null,
search: {
entity: 0,
filter: "",
classIds: [],
propIds: [],
} }
}
onSearch = debounce((search: Search) => this.setState({search}), 500)
load(data: ArrayBuffer) { load(data: ArrayBuffer) {
this.setState({loading: true}); this.setState({loading: true});
@ -65,6 +75,14 @@ class App extends Component<{}, AppState> {
worker.postMessage(data, [data]); worker.postMessage(data, [data]);
} }
filteredPackets(): Packet[] {
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.header && this.state.packets.length) {
return ( return (
@ -83,16 +101,20 @@ class App extends Component<{}, AppState> {
let active = <></>; let active = <></>;
if (this.state.active) { if (this.state.active) {
active = <div className="details"><PacketDetails packet={this.state.active} active = <div className="details"><PacketDetails packet={this.state.active}
search={this.state.search}
prop_names={this.state.prop_names} prop_names={this.state.prop_names}
class_names={this.state.class_names}/></div> class_names={this.state.class_names}/></div>
} }
return ( return (
<> <>
<PacketTable packets={this.state.packets} class_names={this.state.class_names} <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}
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={(activeIndex, active) => this.setState({activeIndex, active})}/>
{active} {active}
</div>
</> </>
) )
} else { } else {
@ -142,3 +164,11 @@ function DemoDropzone(
</div> </div>
) )
} }
function debounce(func: Function, timeout = 300){
let timer: any;
return (...args:any[]) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}

View file

@ -1,13 +1,15 @@
import {GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser"; import {EventInfo, GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser";
import React from "react"; import React from "react";
import {filterEntity, filterMessage, Search} from "../search";
export interface MessageInfoProps { export interface MessageInfoProps {
msg: Message, msg: Message,
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>,
search: Search
} }
export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) { export function MessageInfo({msg, prop_names, class_names, search}: MessageInfoProps) {
switch (msg.type) { switch (msg.type) {
case "Print": case "Print":
return <>{msg.value}</> return <>{msg.value}</>
@ -29,7 +31,8 @@ export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
case "GameEventList": case "GameEventList":
return <>{msg.event_list.map(formatEventDefinition).map((str, i) => (<p key={i}>{str}</p>))}</> return <>{msg.event_list.map(formatEventDefinition).map((str, i) => (<p key={i}>{str}</p>))}</>
case "PacketEntities": case "PacketEntities":
let entities = msg.entities.map(entity => formatEntity(entity, prop_names, class_names)).map((str, i) => <p let entities = filteredEntities(msg.entities, search).map(entity => formatEntity(entity, prop_names, class_names)).map((str, i) =>
<p
key={i}>{str}</p>); key={i}>{str}</p>);
let deleted = <></> let deleted = <></>
if (msg.removed_entities.length > 0) { if (msg.removed_entities.length > 0) {
@ -44,7 +47,7 @@ export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
{deleted} {deleted}
</> </>
case "TempEntities": case "TempEntities":
let events = msg.events.map(event => { let events = filteredTempEntities(msg.events, search).map(event => {
let class_name = class_names.get(event.class_id); let class_name = class_names.get(event.class_id);
let props = event.props.map(prop => { let props = event.props.map(prop => {
let names = prop_names.get(prop.identifier); let names = prop_names.get(prop.identifier);
@ -92,3 +95,25 @@ function formatEventDefinition(event: GameEventDefinition): string {
let values = event.entries.map(entry => `${entry.name}: ${entry.kind}`); let values = event.entries.map(entry => `${entry.name}: ${entry.kind}`);
return `${event.event_type}{${values.join(', ')}}`; return `${event.event_type}{${values.join(', ')}}`;
} }
function filteredEntities(entities: PacketEntity[], search: Search) {
if (search.filter || search.entity) {
return entities.filter(entities => {
return (search.entity == 0 || search.entity == entities.entity_index) &&
(search.filter.length < 3 || filterEntity(entities.server_class, entities.props, search))
});
} else {
return entities;
}
}
function filteredTempEntities(entities: EventInfo[], search: Search) {
if (search.entity) {
return [];
}
if (search.filter) {
return entities.filter(entities => filterEntity(entities.class_id, entities.props, search));
} else {
return entities;
}
}

25
www/src/search.css Normal file
View file

@ -0,0 +1,25 @@
.search > form {
display: flex;
flex-direction: row;
.filter {
flex-grow: 1;
}
label {
margin: 0 5px;
display: flex;
flex-direction: row;
align-items: center;
}
input {
flex-grow: 1;
width: 150px;
margin: 0 5px;
height: 24px;
border-color: #ddd;
}
border-bottom: 1px solid #888;
}

165
www/src/search.tsx Normal file
View file

@ -0,0 +1,165 @@
import React, {ChangeEvent, Component} from "react";
import './search.css'
import {Message, Packet, SendProp, StringTable} from "./parser";
export interface Search {
filter: string,
entity: number,
propIds: number[],
classIds: number[],
}
export interface SearchBarProps {
onSearch: (search: Search) => void,
prop_names: Map<number, { table: string, prop: string }>,
class_names: Map<number, string>,
}
export interface SearchBarState {
filter: string,
entity: number
}
export class SearchBar extends Component<SearchBarProps, SearchBarState> {
state: SearchBarState = {
filter: "",
entity: 0
}
getSearch(): Search {
return {
filter: 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),
}
}
onFilter(event: ChangeEvent<HTMLInputElement>) {
let filter = event.target.value;
this.setState({filter});
setTimeout(() => this.props.onSearch(this.getSearch()), 1);
}
onEntity(event: ChangeEvent<HTMLInputElement>) {
let entity = parseInt(event.target.value, 10);
this.setState({entity});
setTimeout(() => this.props.onSearch(this.getSearch()), 1);
}
render() {
return (
<div className="search">
<form>
<label className="filter">
Filter
<input onInput={this.onFilter.bind(this)}/>
</label>
<label className="entity">
Entity
<input onInput={this.onEntity.bind(this)} type="number"/>
</label>
</form>
</div>
)
}
}
export function filterPacket(
packet: Packet,
search: Search,
): boolean {
switch (packet.type) {
case "Sigon":
case "Message":
return packet.messages.some(msg => filterMessage(msg, search))
case "SyncTick":
return false;
case "ConsoleCmd":
return search.entity == 0 && packet.command.includes(search.filter);
case "UserCmd":
return false;
case "DataTables":
return false;
case "Stop":
return false;
case "StringTables":
return search.entity == 0 && packet.tables.some(table => filterStringTable(table, search));
}
}
function filterPropNames(prop_names: Map<number, { table: string, prop: string }>, filter: string): number[] {
let ids = [];
for (let [id, {table, prop}] of prop_names.entries()) {
if (table.toLowerCase().includes(filter) || prop.toLowerCase().includes(filter)) {
ids.push(id)
}
}
return ids;
}
function filterClassNames(class_names: Map<number, string>, filter: string): number[] {
let ids = [];
for (let [id, name] of class_names.entries()) {
if (name.toLowerCase().includes(filter)) {
ids.push(id)
}
}
return ids;
}
export function filterMessage(
message: Message,
search: Search,
): boolean {
switch (message.type) {
case "File":
return search.entity == 0 && message.file_name.includes(search.filter);
case "StringCmd":
return search.entity == 0 && message.command.includes(search.filter);
case "SetConVar":
return search.entity == 0 && message.vars.some(cvar => cvar.value.includes(search.filter) || cvar.key.includes(search.filter));
case "Print":
return search.entity == 0 && message.value.includes(search.filter);
case "ClassInfo":
return search.entity == 0 && message.entries.some(entry => entry.class_name.includes(search.filter) || entry.table_name.includes(search.filter));
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)));
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)));
case "Text":
return search.entity == 0 && message.text.includes(search.filter);
case "EntityMessage":
return search.entity == 0 && search.classIds.includes(message.class_id)
case "GameEvent":
return search.entity == 0 && message.event.type.includes(search.filter)
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.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);
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));
}
function filterStringTable(table: StringTable, search: Search): boolean {
if (table.name.includes(search.filter)) {
return true;
} else if (table.entries.some(([_index, entry]) => entry.text.includes(search.filter))) {
return true;
}
return false;
}

View file

@ -3,11 +3,12 @@ import {GameEventDefinition, Message, Packet, PacketEntity, SendPropValue, UserC
import {FixedSizeList as List} from 'react-window'; 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";
interface TableProps { interface TableProps {
packets: Packet[], packets: Packet[],
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, packet: Packet) => void,
activeIndex: number | null, activeIndex: number | null,
} }
@ -23,7 +24,7 @@ export function PacketTable({packets, prop_names, class_names, onClick, activeIn
return ( return (
<> <>
<List className="list" height={window.innerHeight} itemCount={packets.length} itemSize={30} width={210}> <List className="list" height={window.innerHeight - 31} itemCount={packets.length} itemSize={30} width={210}>
{Row} {Row}
</List> </List>
</> </>
@ -77,17 +78,26 @@ export function PacketRow({packet}: RowProps) {
interface DetailProps { interface DetailProps {
packet: Packet, packet: Packet,
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>,
search: Search,
} }
export function PacketDetails({packet, prop_names, class_names}: DetailProps) { function filteredMessages(messages: Message[], search: Search) {
if (search.filter || search.entity) {
return messages.filter(message => filterMessage(message, search));
} else {
return messages;
}
}
export function PacketDetails({packet, prop_names, class_names, search}: DetailProps) {
switch (packet.type) { switch (packet.type) {
case "Sigon": case "Sigon":
case "Message": case "Message":
let rows = packet.messages.map((message, y) => <tr key={y}> let rows = filteredMessages(packet.messages, search).map((message, y) => <tr key={y}>
<td className="type">{message.type}</td> <td className="type">{message.type}</td>
<td><MessageInfo msg={message} prop_names={prop_names} class_names={class_names}/></td> <td><MessageInfo msg={message} prop_names={prop_names} class_names={class_names} search={search}/></td>
</tr>) </tr>)
return ( return (
<table> <table>

View file

@ -10,19 +10,27 @@ span.type {
} }
div.details { div.details {
display: inline-block; flex-grow: 1;
width: calc(100vw - 210px);
height: 100vh;
overflow: auto; overflow: auto;
width: calc(100vw - 210px);
height: 100%;
} }
div.list { div.list {
display: inline-block;
} }
#root { #root {
height: 100%;
display: flex;
flex-direction: column;
.packets {
min-height: 0;
flex: 1;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
position: relative;
}
} }
table { table {
@ -51,9 +59,11 @@ html, body, #root {
.prop_row { .prop_row {
margin: 0; margin: 0;
&.active { &.active {
background: #ccc; background: #ccc;
} }
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;