Added selective dump features
This commit is contained in:
@@ -12,17 +12,13 @@
|
||||
* next line, leaving one line with no trailing groups and
|
||||
* one with no `[ADDR]` prefix.
|
||||
*
|
||||
* Two things make shape 1 harder than it looks:
|
||||
* Notes:
|
||||
*
|
||||
* - ObjectPath CAN contain spaces (`... (Director BP)_C:UberGraphFrame`,
|
||||
* `Default__SkyCreator:Sun Light Component`) - 5.6k lines' worth. So the
|
||||
* path is not "up to the first space"; the trailing `[k: v]` groups have
|
||||
* to be peeled off the RIGHT and the path is whatever is left.
|
||||
* - Enum constants have no path at all, so a left-to-right scan reads the
|
||||
* first group as the path and files 15k objects under a bogus `[n` root.
|
||||
* - ObjectPath CAN contain spaces (`... (Director BP)_C:UberGraphFrameDefault__SkyCreator:Sun Light Component`) - 5.6k lines' worth. So the
|
||||
* path is not "up to the first space"; the trailing `[k: v]` groups have to be peeled off the RIGHT and the path is whatever is left.
|
||||
* - Enum constants have no path at all, so a left-to-right scan reads the first group as the path and files 15k objects under a bogus `[n` root.
|
||||
*
|
||||
* Peeling from the right is only safe because no object path in the dump
|
||||
* contains `[` or `]`; that is asserted by treating a leftover bracket in the
|
||||
* Peeling from the right is only safe because no object path in the dump contains `[` or `]`; that is asserted by treating a leftover bracket in the
|
||||
* path region as damage rather than as text.
|
||||
*/
|
||||
|
||||
@@ -56,10 +52,10 @@ export interface DumpColumns {
|
||||
*
|
||||
* rootOff:
|
||||
*
|
||||
* Byte offset at which the real `/...` path begins. Array inner properties
|
||||
* are dumped as `ComponentTags./Script/Engine.ActorComponent:ComponentTags`;
|
||||
* that leading qualifier is kept for display but skipped when indexing, so
|
||||
* the inner property files under its array rather than at the tree root.
|
||||
* Byte offset at which the real `/...` path begins.
|
||||
*
|
||||
* Array inner properties are dumped as `ComponentTags./Script/Engine.ActorComponent:ComponentTags`;
|
||||
* that leading qualifier is kept for display but skipped when indexing, so the inner property files under its array rather than at the tree root.
|
||||
*/
|
||||
rootOff: Uint16Array
|
||||
outer: Float64Array // Address from `or:` (outer) or `owr:` (owner), whichever the line carried. 0 = none.
|
||||
|
||||
@@ -35,7 +35,7 @@ function encodeLower(s: string): Uint8Array {
|
||||
export class DumpStore {
|
||||
readonly cols: DumpColumns
|
||||
private decoder = new TextDecoder()
|
||||
/** Row indices ordered by path bytes. Built lazily; powers the tree. */
|
||||
/** Row indices ordered by path bytes. */
|
||||
private byPath: Uint32Array | null = null
|
||||
|
||||
constructor(cols: DumpColumns) {
|
||||
@@ -51,11 +51,7 @@ export class DumpStore {
|
||||
return this.decoder.decode(pathBlob.subarray(pathStart[row]!, pathStart[row + 1]!))
|
||||
}
|
||||
|
||||
/**
|
||||
* A zero here is a real value, not a missing one: row 0 starts at
|
||||
* pathStart 0, and nameOff is 0 for any path with no separator at all
|
||||
* (`CIM_Linear`). Guarding with `!x` dropped both.
|
||||
*/
|
||||
|
||||
name(row: number): string {
|
||||
const { pathStart, pathBlob, nameOff } = this.cols
|
||||
return this.decoder.decode(
|
||||
@@ -63,7 +59,6 @@ export class DumpStore {
|
||||
)
|
||||
}
|
||||
|
||||
/** Path minus the leaf name, minus the trailing separator. */
|
||||
container(row: number): string {
|
||||
const { pathStart, pathBlob, nameOff } = this.cols
|
||||
if (!nameOff[row]) return '' // the whole path is the name
|
||||
@@ -95,7 +90,7 @@ export class DumpStore {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- matching
|
||||
// --- matching
|
||||
|
||||
// rootOff is 0 for every path that already starts with '/', which is nearly
|
||||
// all of them, so the old `!rootOff[row]` guard rejected everything except
|
||||
@@ -128,14 +123,9 @@ export class DumpStore {
|
||||
return false
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ query
|
||||
// --- query
|
||||
|
||||
|
||||
/**
|
||||
* Single linear pass over every row. At 800k rows this is single-digit
|
||||
* milliseconds for type filters and ~40 ms for a substring scan, which is
|
||||
* why there is no inverted index here — it would cost more to maintain
|
||||
* than it saves.
|
||||
*/
|
||||
query(q: Query): { rows: Uint32Array; typeCounts: Int32Array } {
|
||||
const { count, typeId, types } = this.cols
|
||||
const typeCounts = new Int32Array(types.length)
|
||||
@@ -290,6 +280,30 @@ export class DumpStore {
|
||||
return [start, lo]
|
||||
}
|
||||
|
||||
/**
|
||||
* Every row at exactly `prefix` or beneath it, in path order.
|
||||
*/
|
||||
subtree(prefix: string): Uint32Array {
|
||||
const order = this.ensurePathOrder()
|
||||
if (!prefix) return order.slice()
|
||||
|
||||
const pat = new TextEncoder().encode(prefix)
|
||||
const [lo, hi] = this.range(pat)
|
||||
const { pathStart, pathBlob, rootOff } = this.cols
|
||||
|
||||
const out = new Uint32Array(hi - lo)
|
||||
let n = 0
|
||||
for (let i = lo; i < hi; i++) {
|
||||
const row = order[i]!
|
||||
const after = pathStart[row]! + rootOff[row]! + pat.length
|
||||
const end = pathStart[row + 1]!
|
||||
if (after > end) continue
|
||||
const c = pathBlob[after]
|
||||
if (after === end || c === SLASH || c === DOT || c === COLON) out[n++] = row
|
||||
}
|
||||
return out.slice(0, n)
|
||||
}
|
||||
|
||||
private childCache = new Map<string, TreeChild[]>()
|
||||
|
||||
/**
|
||||
|
||||
160
app/lib/report.ts
Normal file
160
app/lib/report.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { DumpStore } from './dump-store'
|
||||
import { KIND_ENUM } from './dump-parse'
|
||||
|
||||
/**
|
||||
* A subtree rendered as a reference sheet for writing reflection code.
|
||||
*
|
||||
* The audience is someone about to hook a UFunction: they need the object
|
||||
* path to look it up, and the parameter layout — names, types, and byte
|
||||
* offsets in declaration order — to lay out the params struct the hook
|
||||
* receives. So the report is two passes over the same rows. The outline
|
||||
* answers "what is in here", and the detail section answers "what do I have
|
||||
* to write", with functions given the offset table that is the whole point.
|
||||
*/
|
||||
|
||||
const FUNCTION_TYPES = new Set(['Function', 'DelegateFunction', 'SparseDelegateFunction'])
|
||||
|
||||
/** Beyond this the report stops being a reference and starts being the dump. */
|
||||
export const REPORT_CAP = 25_000
|
||||
|
||||
export interface ReportResult {
|
||||
text: string
|
||||
objects: number
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
const hex16 = (n: number) => '0x' + n.toString(16).toUpperCase().padStart(16, '0')
|
||||
const hexOff = (n: number) => '+0x' + n.toString(16).toUpperCase().padStart(4, '0')
|
||||
|
||||
function stamp(t: number): string {
|
||||
const p = (n: number) => String(n).padStart(2, '0')
|
||||
const d = new Date(t)
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* The part of a display path that hangs below `root`.
|
||||
*
|
||||
* Not simply `path.slice(root.length)`. Array and map inner properties are
|
||||
* dumped with a leading qualifier — `K2Node_MakeArray_Array./Game/…:UpdateHP`
|
||||
* — which the store skips when indexing, so such a row is legitimately inside
|
||||
* the subtree while its *displayed* path does not begin with the root at all.
|
||||
* Slicing blind turns those lines into garbage at a random offset.
|
||||
*/
|
||||
function relativeTo(root: string, path: string): string {
|
||||
if (!root) return path
|
||||
const at = path.indexOf(root)
|
||||
return at < 0 ? path : path.slice(at + root.length)
|
||||
}
|
||||
|
||||
/** Separator depth, for indenting the outline. */
|
||||
function depthOf(rel: string): number {
|
||||
let d = 0
|
||||
for (let i = 0; i < rel.length; i++) {
|
||||
const c = rel[i]
|
||||
if (c === '/' || c === '.' || c === ':') d++
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
export function buildReport(
|
||||
store: DumpStore,
|
||||
rows: Uint32Array,
|
||||
meta: { root: string; label: string },
|
||||
): ReportResult {
|
||||
const truncated = rows.length > REPORT_CAP
|
||||
const used = truncated ? rows.subarray(0, REPORT_CAP) : rows
|
||||
|
||||
// One pass to materialise, because both sections read the same rows and
|
||||
// `store.row` decodes strings out of the path blob each time it is called.
|
||||
const objs = Array.from(used, (r) => store.row(r))
|
||||
|
||||
// Children are grouped by their container so that a function can find its
|
||||
// parameters without a second scan per function.
|
||||
const byContainer = new Map<string, typeof objs>()
|
||||
for (const o of objs) {
|
||||
const list = byContainer.get(o.container)
|
||||
if (list) list.push(o)
|
||||
else byContainer.set(o.container, [o])
|
||||
}
|
||||
|
||||
const out: string[] = []
|
||||
const { root, label } = meta
|
||||
|
||||
out.push('UE4SS OBJECT EXPLORER — SUBTREE REPORT')
|
||||
out.push('='.repeat(72))
|
||||
out.push(`root ${root || '(everything)'}`)
|
||||
out.push(`dump ${label}`)
|
||||
out.push(`objects ${objs.length.toLocaleString()}${truncated ? ` (capped from ${rows.length.toLocaleString()})` : ''}`)
|
||||
out.push(`generated ${stamp(Date.now())}`)
|
||||
if (truncated) {
|
||||
out.push('')
|
||||
out.push(`NOTE This subtree holds ${rows.length.toLocaleString()} objects; only the first`)
|
||||
out.push(` ${REPORT_CAP.toLocaleString()} are listed. Scope to a narrower node for a complete report.`)
|
||||
}
|
||||
out.push('')
|
||||
|
||||
// ------------------------------------------------------------- outline
|
||||
out.push('OUTLINE')
|
||||
out.push('-'.repeat(72))
|
||||
for (const o of objs) {
|
||||
const rel = relativeTo(root, o.path)
|
||||
const bits = [`${' '.repeat(depthOf(rel))}${rel || o.path}`]
|
||||
// Flag the qualifier rather than dropping it: an inner property listed
|
||||
// only by its tail reads as a duplicate of the array it belongs to.
|
||||
if (root && !o.path.startsWith(root)) bits.push(`(inner of ${o.path.slice(0, o.path.indexOf(root))})`)
|
||||
bits.push(`· ${o.type}`)
|
||||
if (o.address) bits.push(`@ ${hex16(o.address)}`)
|
||||
if (o.offset >= 0) bits.push(hexOff(o.offset))
|
||||
if (o.kind === KIND_ENUM && o.value !== null) bits.push(`= ${o.value}`)
|
||||
out.push(bits.join(' '))
|
||||
}
|
||||
out.push('')
|
||||
|
||||
// ------------------------------------------------------------ functions
|
||||
const fns = objs.filter((o) => FUNCTION_TYPES.has(o.type))
|
||||
if (fns.length) {
|
||||
out.push('FUNCTIONS')
|
||||
out.push('-'.repeat(72))
|
||||
out.push(`${fns.length} callable${fns.length === 1 ? '' : 's'} in this subtree.`)
|
||||
out.push('')
|
||||
|
||||
for (const f of fns) {
|
||||
// Declaration order is offset order: UE lays parameters out in the
|
||||
// params struct in the order they are declared, return value last.
|
||||
const params = (byContainer.get(f.path) ?? []).slice().sort((a, b) => a.offset - b.offset)
|
||||
|
||||
out.push(f.path)
|
||||
out.push(` name ${f.name}`)
|
||||
out.push(` type ${f.type}`)
|
||||
out.push(` address ${hex16(f.address)}`)
|
||||
if (f.outer) out.push(` outer ${hex16(f.outer)}`)
|
||||
if (!params.length) {
|
||||
out.push(' params none')
|
||||
} else {
|
||||
out.push(` params ${params.length}`)
|
||||
const w = Math.max(...params.map((p) => p.name.length), 4)
|
||||
out.push(` ${'OFFSET'.padEnd(9)}${'NAME'.padEnd(w + 2)}TYPE`)
|
||||
for (const p of params) {
|
||||
out.push(` ${hexOff(p.offset).padEnd(9)}${p.name.padEnd(w + 2)}${p.type}`)
|
||||
}
|
||||
}
|
||||
out.push('')
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- objects
|
||||
out.push('OBJECTS')
|
||||
out.push('-'.repeat(72))
|
||||
for (const o of objs) {
|
||||
out.push(o.path)
|
||||
out.push(` type ${o.type}`)
|
||||
out.push(` address ${hex16(o.address)}`)
|
||||
if (o.outer) out.push(` outer ${hex16(o.outer)}`)
|
||||
if (o.offset >= 0) out.push(` offset ${hexOff(o.offset)} (${o.offset})`)
|
||||
if (o.kind === KIND_ENUM && o.value !== null) out.push(` value ${o.value}`)
|
||||
out.push('')
|
||||
}
|
||||
|
||||
return { text: out.join('\n'), objects: objs.length, truncated }
|
||||
}
|
||||
Reference in New Issue
Block a user