OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / distribute / hypertps-win64 / scripts / discover-gamestate.cjs

80 lines · javascript ref:
#!/usr/bin/env node
// discover-gamestate.cjs — connect to a keet-daemon over UDS and DISCOVER a room's
// gamestate schema purely by probing (state.list), with ZERO hardcoded game
// knowledge. Demonstrates that an agent can build/confirm context from the data
// itself rather than reimplementing the Godot-side contract.
//   KEET_SOCK=<sock> node discover-gamestate.cjs <roomId>
'use strict'
const net = require('net')
const SOCK = process.env.KEET_SOCK || '/tmp/keet-daemon.sock'
const roomId = process.argv[2]
if (!roomId) { console.error('usage: discover-gamestate.cjs <roomId>'); process.exit(1) }

function rpc (method, params) {
  return new Promise((resolve, reject) => {
    const sock = net.connect(SOCK); let buf = Buffer.alloc(0); const id = (Math.random() * 1e9) | 0
    const t = setTimeout(() => { sock.destroy(); reject(new Error('timeout')) }, 20000)
    sock.on('data', c => { buf = Buffer.concat([buf, c]); while (buf.length >= 4) { const n = buf.readUInt32LE(0); if (buf.length < 4 + n) break; const m = JSON.parse(buf.slice(4, 4 + n).toString()); buf = buf.slice(4 + n); if (m.id !== id) continue; clearTimeout(t); sock.destroy(); if (m.ok === false || m.error) reject(new Error(m.error)); else resolve(m.data !== undefined ? m.data : m) } })
    sock.once('connect', () => { const b = Buffer.from(JSON.stringify({ id, method, params })); const l = Buffer.alloc(4); l.writeUInt32LE(b.length, 0); sock.write(Buffer.concat([l, b])) })
    sock.once('error', e => { clearTimeout(t); reject(e) })
  })
}

// derive a key "family" by replacing the variable trailing segments with <*>
function family (key) {
  // collapse map:<id>: prefix, and trailing id/seq segments
  return key
    .replace(/^map:[^:]+:/, 'map:<mapId>:')
    .replace(/:[0-9a-f]{16,}$/i, ':<hex>')
    .replace(/:[^:]*[0-9][^:]*$/, m => /registry/.test(key) ? ':<entityId>' : (/players/.test(key) ? ':<memberId>' : ':<id>'))
}
function shapeOf (v) {
  const val = (v && typeof v === 'object' && 'value' in v) ? v.value : v
  if (val == null) return 'null'
  if (typeof val === 'number') return 'number'
  if (typeof val === 'object') return 'json:' + JSON.stringify(Object.keys(val)).slice(0, 60)
  const s = String(val)
  if (/^\{[\s\S]*"pos"\s*:\s*\[/.test(s)) return 'gdliteral:{pos:[x,y,z],...}'
  if (/^\{/.test(s)) return 'gdliteral:dict'
  if (/^Vector3\(/.test(s)) return 'gdliteral:Vector3'
  if (/^-?\d+\.?\d*$/.test(s)) return 'gdliteral:number'
  if (/^".*"$/.test(s)) return 'gdliteral:string'
  return 'string:' + s.slice(0, 30)
}

async function main () {
  const info = await rpc('state.info', { roomId }).catch(() => ({}))
  console.log('room', roomId.slice(0, 16), '| writable', info.writable, '| stateBaseKey', (info.stateBaseKey || '').slice(0, 12))
  const r = await rpc('state.list', { roomId, prefix: '' })
  const entries = r.entries || r || {}
  const keys = Object.keys(entries)
  console.log('discovered', keys.length, 'state entries\n')

  // group by derived family
  const fams = new Map()
  for (const k of keys) {
    const f = family(k)
    if (!fams.has(f)) fams.set(f, { count: 0, sampleKey: k, sampleVal: entries[k] })
    fams.get(f).count++
  }
  console.log('=== DERIVED SCHEMA (from data, no hardcoded game knowledge) ===')
  for (const [f, info] of [...fams.entries()].sort((a, b) => b[1].count - a[1].count)) {
    const sv = info.sampleVal
    const svs = (sv && typeof sv === 'object' && 'value' in sv) ? sv.value : sv
    console.log(`  ${String(info.count).padStart(4)}x  ${f}`)
    console.log(`         shape: ${shapeOf(sv)}`)
    console.log(`         e.g.   ${info.sampleKey.slice(0, 70)}`)
    console.log(`              = ${(typeof svs === 'string' ? svs : JSON.stringify(svs)).replace(/\n/g, ' ').slice(0, 90)}`)
  }
  // special: __members__ (membership projection)
  if (entries.__members__ !== undefined) {
    const m = entries.__members__
    const mm = (m && typeof m === 'object' && 'value' in m) ? m.value : m
    const parsed = typeof mm === 'string' ? JSON.parse(mm) : mm
    console.log('\n=== __members__ projection ===')
    console.log('  members:', Object.keys(parsed || {}).length, '| sample:', JSON.stringify(parsed).slice(0, 100))
  }
  process.exit(0)
}
main().catch(e => { console.error('PROBE ERROR:', e.message); process.exit(2) })