mirror of
https://codeberg.org/demostf/inspector.git
synced 2026-06-03 10:04:09 +02:00
filterbar
This commit is contained in:
parent
69d1318a43
commit
aa20564ccd
6 changed files with 291 additions and 26 deletions
|
|
@ -4,6 +4,7 @@ import {useDropzone} from 'react-dropzone'
|
|||
import ReactDOM from "react-dom";
|
||||
import {Header} from "./header";
|
||||
import {PacketDetails, PacketTable} from "./table";
|
||||
import {filterPacket, Search, SearchBar} from "./search";
|
||||
|
||||
let _style = require('../styles/style.css');
|
||||
|
||||
|
|
@ -11,10 +12,11 @@ interface AppState {
|
|||
loading: boolean,
|
||||
header: Header | null,
|
||||
packets: Packet[],
|
||||
prop_names: Map<number, { table: String, prop: String }>,
|
||||
class_names: Map<number, String>,
|
||||
prop_names: Map<number, { table: string, prop: string }>,
|
||||
class_names: Map<number, string>,
|
||||
active: Packet | null,
|
||||
activeIndex: number | null,
|
||||
search: Search,
|
||||
}
|
||||
|
||||
class App extends Component<{}, AppState> {
|
||||
|
|
@ -26,7 +28,15 @@ class App extends Component<{}, AppState> {
|
|||
class_names: new Map(),
|
||||
active: null,
|
||||
activeIndex: null,
|
||||
search: {
|
||||
entity: 0,
|
||||
filter: "",
|
||||
classIds: [],
|
||||
propIds: [],
|
||||
}
|
||||
}
|
||||
|
||||
onSearch = debounce((search: Search) => this.setState({search}), 500)
|
||||
|
||||
load(data: ArrayBuffer) {
|
||||
this.setState({loading: true});
|
||||
|
|
@ -65,6 +75,14 @@ class App extends Component<{}, AppState> {
|
|||
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() {
|
||||
if (this.state.loading && this.state.header && this.state.packets.length) {
|
||||
return (
|
||||
|
|
@ -83,16 +101,20 @@ class App extends Component<{}, AppState> {
|
|||
let active = <></>;
|
||||
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 (
|
||||
<>
|
||||
<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}
|
||||
prop_names={this.state.prop_names}
|
||||
onClick={(activeIndex, active) => this.setState({activeIndex, active})}/>
|
||||
{active}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
|
|
@ -142,3 +164,11 @@ function DemoDropzone(
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function debounce(func: Function, timeout = 300){
|
||||
let timer: any;
|
||||
return (...args:any[]) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import {GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser";
|
||||
import {EventInfo, GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser";
|
||||
import React from "react";
|
||||
import {filterEntity, filterMessage, Search} from "../search";
|
||||
|
||||
export interface MessageInfoProps {
|
||||
msg: Message,
|
||||
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) {
|
||||
case "Print":
|
||||
return <>{msg.value}</>
|
||||
|
|
@ -29,7 +31,8 @@ export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
|
|||
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
|
||||
let entities = filteredEntities(msg.entities, search).map(entity => formatEntity(entity, prop_names, class_names)).map((str, i) =>
|
||||
<p
|
||||
key={i}>{str}</p>);
|
||||
let deleted = <></>
|
||||
if (msg.removed_entities.length > 0) {
|
||||
|
|
@ -44,7 +47,7 @@ export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
|
|||
{deleted}
|
||||
</>
|
||||
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 props = event.props.map(prop => {
|
||||
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}`);
|
||||
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
25
www/src/search.css
Normal 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
165
www/src/search.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -3,11 +3,12 @@ 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";
|
||||
|
||||
interface TableProps {
|
||||
packets: Packet[],
|
||||
prop_names: Map<number, { table: String, prop: String }>,
|
||||
class_names: Map<number, String>,
|
||||
prop_names: Map<number, { table: string, prop: string }>,
|
||||
class_names: Map<number, string>,
|
||||
onClick: (i: number, packet: Packet) => void,
|
||||
activeIndex: number | null,
|
||||
}
|
||||
|
|
@ -23,7 +24,7 @@ export function PacketTable({packets, prop_names, class_names, onClick, activeIn
|
|||
|
||||
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}
|
||||
</List>
|
||||
</>
|
||||
|
|
@ -77,17 +78,26 @@ export function PacketRow({packet}: RowProps) {
|
|||
|
||||
interface DetailProps {
|
||||
packet: Packet,
|
||||
prop_names: Map<number, { table: String, prop: String }>,
|
||||
class_names: Map<number, String>,
|
||||
prop_names: Map<number, { table: string, prop: 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) {
|
||||
case "Sigon":
|
||||
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><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>)
|
||||
return (
|
||||
<table>
|
||||
|
|
|
|||
|
|
@ -10,19 +10,27 @@ span.type {
|
|||
}
|
||||
|
||||
div.details {
|
||||
display: inline-block;
|
||||
width: calc(100vw - 210px);
|
||||
height: 100vh;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
width: calc(100vw - 210px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.list {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.packets {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
|
|
@ -51,9 +59,11 @@ html, body, #root {
|
|||
|
||||
.prop_row {
|
||||
margin: 0;
|
||||
|
||||
&.active {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue