update lua table parser

This commit is contained in:
2025-06-09 21:58:31 +02:00
parent 482852c134
commit d5aca92bf1
2 changed files with 258 additions and 33 deletions

View File

@@ -0,0 +1,241 @@
// @ts-nocheck
'use client'
type LuaValue = string | number | boolean | null | LuaTable | LuaValue[]
type LuaTable = { [key: string]: LuaValue }
export class LuaParser {
private pos = 0
private input = ''
private currentChar: string | null | undefined = null
constructor(input: string) {
this.input = input.trim()
this.currentChar = this.input[0] ?? null
}
private advance(): void {
this.pos++
this.currentChar =
this.pos < this.input.length ? this.input[this.pos] : null
}
private skipWhitespace(): void {
while (
this.currentChar?.match(/\s/) !== null &&
this.currentChar !== null
) {
this.advance()
}
}
private parseString(): string {
let result = ''
const quote = this.currentChar
this.advance() // Skip opening quote
while (this.currentChar !== null && this.currentChar !== quote) {
if (this.currentChar === '\\') {
this.advance()
switch (this.currentChar) {
case 'n':
result += '\n'
break
case 't':
result += '\t'
break
case 'r':
result += '\r'
break
case 'b':
result += '\b'
break
case 'f':
result += '\f'
break
case '"':
case "'":
case '\\':
result += this.currentChar
break
default:
throw new Error(`Invalid escape sequence: \\${this.currentChar}`)
}
} else {
result += this.currentChar
}
this.advance()
}
if (this.currentChar === null) {
throw new Error('Unterminated string')
}
this.advance() // Skip closing quote
return result
}
private parseNumber(): number {
let result = ''
if (this.currentChar === '-') {
result += this.currentChar
this.advance()
}
while (
this.currentChar?.match(/[\d.]/) !== null &&
this.currentChar !== null
) {
result += this.currentChar
this.advance()
}
const num = Number.parseFloat(result)
if (isNaN(num)) {
throw new Error(`Invalid number: ${result}`)
}
return num
}
private parseIdentifier(): string {
let result = ''
while (
this.currentChar?.match(/[a-zA-Z0-9_]/) !== null &&
this.currentChar !== null
) {
result += this.currentChar
this.advance()
}
return result
}
private parseValue(): LuaValue {
this.skipWhitespace()
if (this.currentChar === null) {
throw new Error('Unexpected end of input')
}
switch (this.currentChar) {
case '{':
return this.parseTable()
case '"':
case "'":
return this.parseString()
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return this.parseNumber()
default: {
const identifier = this.parseIdentifier()
switch (identifier.toLowerCase()) {
case 'true':
return true
case 'false':
return false
case 'nil':
return null
default:
throw new Error(`Unexpected identifier: ${identifier}`)
}
}
}
}
private parseTable(): LuaTable | LuaValue[] {
this.advance() // Skip '{'
this.skipWhitespace()
const result: LuaTable = {}
const array: LuaValue[] = []
let isArray = true
let index = 0
while (this.currentChar !== null && this.currentChar !== '}') {
this.skipWhitespace()
if (this.currentChar === '[') {
isArray = false
this.advance() // Skip '['
const key = this.parseValue()
if (typeof key !== 'string' && typeof key !== 'number') {
throw new Error('Table key must be string or number')
}
this.skipWhitespace()
if (this.currentChar !== ']') {
throw new Error("Expected ']'")
}
this.advance() // Skip ']'
this.skipWhitespace()
if (this.currentChar !== '=') {
throw new Error("Expected '='")
}
this.advance() // Skip '='
const value = this.parseValue()
result[String(key)] = value
} else {
const value = this.parseValue()
if (isArray) {
array.push(value)
index++
} else {
result[String(index)] = value
index++
}
}
this.skipWhitespace()
if (this.currentChar === ',') {
this.advance()
} else if (this.currentChar !== '}') {
throw new Error("Expected ',' or '}'")
}
}
if (this.currentChar === null) {
throw new Error('Unterminated table')
}
this.advance() // Skip '}'
return isArray ? array : result
}
public parse(): LuaValue {
const result = this.parseValue()
this.skipWhitespace()
if (this.currentChar !== null) {
throw new Error('Unexpected characters after end of input')
}
return result
}
}
export class LuaToJsonConverter {
private static readonly logger = console
public static async convert(luaString: string): Promise<string> {
try {
const parser = new LuaParser(luaString)
const parsed = parser.parse()
return JSON.stringify(parsed, null, 2)
} catch (error) {
this.logger.error('Error converting Lua to JSON:', error)
throw error
}
}
}

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import { LuaToJsonConverter } from '@/app/(home)/log-parser/lua-parser'
import { OptimizedImage } from '@/components/optimized-image' import { OptimizedImage } from '@/components/optimized-image'
import { import {
Card, Card,
@@ -37,7 +38,6 @@ import { jokers } from '@/shared/jokers'
import { useFormatter } from 'next-intl' import { useFormatter } from 'next-intl'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { type PvpBlind, PvpBlindsCard } from './_components/pvp-blinds' import { type PvpBlind, PvpBlindsCard } from './_components/pvp-blinds'
// Define the structure for individual log events within a game // Define the structure for individual log events within a game
type LogEvent = { type LogEvent = {
timestamp: Date timestamp: Date
@@ -1418,31 +1418,9 @@ type JsonValue =
| JsonValue[] | JsonValue[]
| { [key: string]: JsonValue } | { [key: string]: JsonValue }
function luaTableToJson(luaString: string): string { async function luaTableToJson(luaString: string): string {
let str = luaString.replace(/^return\s*/, '') const str = luaString.replace(/^return\s*/, '')
return LuaToJsonConverter.convert(str)
// Handle nested tables closing properly
str = str.replace(/,(\s*})/g, '$1')
// Handle array-style table entries [1] = value
str = str.replace(/\[(\d+)\]\s*=/g, '"$1":')
// Handle string keys ["key"] = value
str = str.replace(/\[(["'][^"']+["'])\]\s*=/g, '$1:')
// Handle regular key = value
str = str.replace(/(\w+)\s*=/g, '"$1":')
// Replace single quotes with double quotes
str = str.replace(/'/g, '"')
// Clean up empty tables
str = str.replace(/{}/g, '{}')
// Remove trailing commas inside objects
str = str.replace(/,(\s*[}\]])/g, '$1')
return str
} }
async function decodePackedString(encodedString: string): Promise<JsonValue> { async function decodePackedString(encodedString: string): Promise<JsonValue> {
@@ -1466,7 +1444,8 @@ async function decodePackedString(encodedString: string): Promise<JsonValue> {
} }
// Convert Lua table to JSON // Convert Lua table to JSON
const jsonString = luaTableToJson(decompressedString) const jsonString = await luaTableToJson(decompressedString)
console.log(jsonString)
const result = JSON.parse(jsonString) as JsonValue const result = JSON.parse(jsonString) as JsonValue
return result return result
} catch (error) { } catch (error) {
@@ -1479,13 +1458,18 @@ async function decodePackedString(encodedString: string): Promise<JsonValue> {
async function parseJokersFromString(str: string) { async function parseJokersFromString(str: string) {
// Check if the string starts with 'H4' indicating a packed string // Check if the string starts with 'H4' indicating a packed string
// This is a common prefix for base64 encoded gzip strings // This is a common prefix for base64 encoded gzip strings
if (str.startsWith('H4')) { try {
const decoded = await decodePackedString(str) if (str.startsWith('H4')) {
if (decoded && typeof decoded === 'object' && 'cards' in decoded) { const decoded = await decodePackedString(str)
return Object.values(decoded.cards as any).map( if (decoded && typeof decoded === 'object' && 'cards' in decoded) {
(c: any) => c.save_fields.center return Object.values(decoded.cards as any).map(
) (c: any) => c.save_fields.center
)
}
} }
} catch (e) {
console.error('Failed to parse jokers from string:', str, e)
return []
} }
return str.split(';').filter(Boolean) // Remove empty strings if any return str.split(';').filter(Boolean) // Remove empty strings if any
} }