mirror of
https://github.com/ershisan99/www.git
synced 2025-12-18 12:34:17 +00:00
add logs parser
This commit is contained in:
371
src/app/(home)/log-parser/page.tsx
Normal file
371
src/app/(home)/log-parser/page.tsx
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dropzone,
|
||||||
|
DropzoneDescription,
|
||||||
|
DropzoneGroup,
|
||||||
|
DropzoneInput,
|
||||||
|
DropzoneTitle,
|
||||||
|
DropzoneUploadIcon,
|
||||||
|
DropzoneZone,
|
||||||
|
} from '@/components/ui/dropzone'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type LogLine = {
|
||||||
|
text: string
|
||||||
|
type: 'event' | 'status' | 'system'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game = {
|
||||||
|
host: string | null
|
||||||
|
guest: string | null
|
||||||
|
hostMods: string | null
|
||||||
|
guestMods: string | null
|
||||||
|
deck: string | null
|
||||||
|
seed: string | null
|
||||||
|
seedType: string | null
|
||||||
|
isHost: boolean | null
|
||||||
|
moneyGained: number
|
||||||
|
moneySpent: number
|
||||||
|
startDate: Date
|
||||||
|
endDate: Date | null
|
||||||
|
lastLives: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState = {
|
||||||
|
currentGame: Game | null
|
||||||
|
games: Game[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initGame = (): Game => ({
|
||||||
|
host: null,
|
||||||
|
guest: null,
|
||||||
|
hostMods: null,
|
||||||
|
guestMods: null,
|
||||||
|
deck: null,
|
||||||
|
seed: null,
|
||||||
|
seedType: null,
|
||||||
|
isHost: null,
|
||||||
|
moneyGained: 0,
|
||||||
|
moneySpent: 0,
|
||||||
|
startDate: new Date(),
|
||||||
|
endDate: null,
|
||||||
|
lastLives: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatDuration = (seconds: number): string => {
|
||||||
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
const secs = Math.floor(seconds % 60)
|
||||||
|
|
||||||
|
const parts = []
|
||||||
|
if (hours > 0) parts.push(`${hours}h`)
|
||||||
|
if (minutes > 0) parts.push(`${minutes}m`)
|
||||||
|
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`)
|
||||||
|
|
||||||
|
return parts.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogParser() {
|
||||||
|
const [logLines, setLogLines] = useState<LogLine[]>([])
|
||||||
|
|
||||||
|
const parseLogFile = async (file: File) => {
|
||||||
|
const state: GameState = {
|
||||||
|
currentGame: null,
|
||||||
|
games: [],
|
||||||
|
}
|
||||||
|
let lastSeenDeck = null
|
||||||
|
|
||||||
|
const lines: LogLine[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await file.text()
|
||||||
|
const logLines = content.split('\n')
|
||||||
|
|
||||||
|
for (const line of logLines) {
|
||||||
|
const timeMatch = line.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/)
|
||||||
|
const timestamp = timeMatch?.[1] ? new Date(timeMatch[1]) : new Date()
|
||||||
|
const lineLower = line.toLowerCase()
|
||||||
|
|
||||||
|
if (lineLower.includes('enemyinfo')) {
|
||||||
|
if (!state.currentGame) continue
|
||||||
|
|
||||||
|
const match = line.match(/lives:(\d+)/)
|
||||||
|
if (match) {
|
||||||
|
const newLives = match[1] ? Number.parseInt(match[1]) : 0
|
||||||
|
if (newLives < state.currentGame.lastLives) {
|
||||||
|
lines.push({ text: 'Lost a life', type: 'event' })
|
||||||
|
}
|
||||||
|
state.currentGame.lastLives = newLives
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineLower.includes('lobbyinfo message')) {
|
||||||
|
if (line.includes('host:')) {
|
||||||
|
const hostMatch = line.match(/host: ([^ )]+)/)
|
||||||
|
const guestMatch = line.match(/guest: ([^ )]+)/)
|
||||||
|
const hostModsMatch = line.match(/hostHash: ([^ )]+)/)
|
||||||
|
const guestModsMatch = line.match(/guestHash: ([^ )]+)/)
|
||||||
|
|
||||||
|
if (state.currentGame) {
|
||||||
|
state.currentGame.host = hostMatch?.[1] || null
|
||||||
|
state.currentGame.guest = guestMatch?.[1] || null
|
||||||
|
state.currentGame.hostMods = hostModsMatch?.[1] || ''
|
||||||
|
state.currentGame.guestMods = guestModsMatch?.[1] || ''
|
||||||
|
state.currentGame.isHost = line.includes('isHost: true')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineLower.includes('lobbyoptions')) {
|
||||||
|
const deckMatch = line.match(/back: ([^)]+)\)/)
|
||||||
|
const seedTypeMatch = line.match(/custom_seed: ([^)]+)/)
|
||||||
|
|
||||||
|
console.log(deckMatch, seedTypeMatch)
|
||||||
|
|
||||||
|
lastSeenDeck = deckMatch?.[1] || null
|
||||||
|
console.log({ lastSeenDeck })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineLower.includes('startgame message')) {
|
||||||
|
if (state.currentGame) {
|
||||||
|
state.currentGame.endDate = new Date()
|
||||||
|
state.games.push(state.currentGame)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentGame = initGame()
|
||||||
|
const seedMatch = line.match(/seed:\s*([^) ]+)/)
|
||||||
|
state.currentGame.startDate = timestamp
|
||||||
|
state.currentGame.seed = seedMatch?.[1] || null
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
{ text: '=== New Game Started ===', type: 'system' },
|
||||||
|
{
|
||||||
|
text: `Start Time: ${state.currentGame.startDate.toISOString()}`,
|
||||||
|
type: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Deck: ${state.currentGame.deck || lastSeenDeck || 'None'}`,
|
||||||
|
type: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Seed: ${state.currentGame.seed || 'Unknown'}`,
|
||||||
|
type: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `Custom Seed: ${state.currentGame.seedType || 'unknown'}`,
|
||||||
|
type: 'system',
|
||||||
|
},
|
||||||
|
{ text: '===================', type: 'system' },
|
||||||
|
{ text: '', type: 'system' }
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.includes('Client got receiveEndGameJokers')) {
|
||||||
|
if (!state.currentGame) continue
|
||||||
|
state.currentGame.endDate = timestamp
|
||||||
|
|
||||||
|
const seedMatch = line.match(/seed: ([A-Z0-9]+)/)
|
||||||
|
state.currentGame.seed = seedMatch?.[1] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lineLower.includes('client sent')) continue
|
||||||
|
|
||||||
|
if (lineLower.includes('moneymoved')) {
|
||||||
|
if (!state.currentGame) continue
|
||||||
|
|
||||||
|
const match = line.match(/amount: *(-?\d+)/)
|
||||||
|
if (match) {
|
||||||
|
const amount = match[1] ? Number.parseInt(match[1]) : -1
|
||||||
|
if (amount >= 0) {
|
||||||
|
state.currentGame.moneyGained += amount
|
||||||
|
lines.push({
|
||||||
|
text: `Gained $${amount} (Total gained: $${state.currentGame.moneyGained})`,
|
||||||
|
type: 'event',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const spent = Math.abs(amount)
|
||||||
|
lines.push({
|
||||||
|
text: `Spent $${spent}`,
|
||||||
|
type: 'event',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (lineLower.includes('spentlastshop')) {
|
||||||
|
if (!state.currentGame) continue
|
||||||
|
|
||||||
|
const match = line.match(/amount: *(\d+)/)
|
||||||
|
if (match) {
|
||||||
|
const amount = match[1] ? Number.parseInt(match[1]) : -1
|
||||||
|
state.currentGame.moneySpent += amount
|
||||||
|
lines.push({
|
||||||
|
text: `Spent $${amount} last shop (Total spent: $${state.currentGame.moneySpent})`,
|
||||||
|
type: 'event',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (lineLower.includes('usedcard')) {
|
||||||
|
const match = line.match(/card:([^,\n]+)/i)
|
||||||
|
if (match) {
|
||||||
|
const raw = match[1] ? match[1].trim() : ''
|
||||||
|
const clean = raw.replace(/^(c_mp_|j_mp_)/, '')
|
||||||
|
const pretty = clean
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(
|
||||||
|
/\w\S*/g,
|
||||||
|
(txt) =>
|
||||||
|
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||||
|
)
|
||||||
|
lines.push({ text: `Used ${pretty}`, type: 'event' })
|
||||||
|
}
|
||||||
|
} else if (lineLower.includes('setlocation')) {
|
||||||
|
const locMatch = line.match(/location:([a-zA-Z0-9_-]+)/)
|
||||||
|
if (locMatch) {
|
||||||
|
const locCode = locMatch[1]
|
||||||
|
if (locCode === 'loc_selecting' || !locCode) continue
|
||||||
|
|
||||||
|
let locationText: string
|
||||||
|
if (locCode === 'loc_shop') {
|
||||||
|
locationText = 'Shop'
|
||||||
|
} else if (locCode.startsWith('loc_playing-')) {
|
||||||
|
const subcode = locCode.slice('loc_playing-'.length)
|
||||||
|
if (subcode.startsWith('bl_')) {
|
||||||
|
const blindName = subcode
|
||||||
|
.slice(3)
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(
|
||||||
|
/\w\S*/g,
|
||||||
|
(txt) =>
|
||||||
|
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||||
|
)
|
||||||
|
locationText = `${blindName} Blind`
|
||||||
|
} else {
|
||||||
|
const readable = subcode
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(
|
||||||
|
/\w\S*/g,
|
||||||
|
(txt) =>
|
||||||
|
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||||
|
)
|
||||||
|
locationText = `Playing ${readable}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
locationText = locCode
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(
|
||||||
|
/\w\S*/g,
|
||||||
|
(txt) =>
|
||||||
|
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
lines.push({ text: locationText, type: 'status' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.currentGame) {
|
||||||
|
// state.currentGame.endDate = new Date()
|
||||||
|
state.games.push(state.currentGame)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.games.forEach((game, i) => {
|
||||||
|
const duration = game.endDate
|
||||||
|
? (game.endDate.getTime() - game.startDate.getTime()) / 1000
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const startTimeStr = game.startDate.toLocaleTimeString()
|
||||||
|
const endTimeStr = game.endDate?.toLocaleTimeString() || 'Unknown'
|
||||||
|
|
||||||
|
const summaryLines = [
|
||||||
|
{ text: `=== Game ${i + 1} Summary ===`, type: 'system' },
|
||||||
|
{ text: `Start Time: ${startTimeStr}`, type: 'system' },
|
||||||
|
{ text: `End Time: ${endTimeStr}`, type: 'system' },
|
||||||
|
{ text: `Duration: ${formatDuration(duration)}`, type: 'system' },
|
||||||
|
{ text: `Total Money Gained: $${game.moneyGained}`, type: 'system' },
|
||||||
|
{ text: `Total Money Spent: $${game.moneySpent}`, type: 'system' },
|
||||||
|
{
|
||||||
|
text: `Net Money: $${game.moneyGained - game.moneySpent}`,
|
||||||
|
type: 'system',
|
||||||
|
},
|
||||||
|
{ text: '==================', type: 'system' },
|
||||||
|
{ text: '', type: 'system' },
|
||||||
|
] as LogLine[]
|
||||||
|
|
||||||
|
lines.unshift(...summaryLines)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalGained = state.games.reduce((sum, g) => sum + g.moneyGained, 0)
|
||||||
|
const totalSpent = state.games.reduce((sum, g) => sum + g.moneySpent, 0)
|
||||||
|
|
||||||
|
lines.unshift(
|
||||||
|
{ text: `=== Overall Summary ===`, type: 'system' },
|
||||||
|
{ text: `Total Games: ${state.games.length}`, type: 'system' },
|
||||||
|
{ text: `Total Money Gained: $${totalGained}`, type: 'system' },
|
||||||
|
{ text: `Total Money Spent: $${totalSpent}`, type: 'system' },
|
||||||
|
{
|
||||||
|
text: `Overall Net Money: $${totalGained - totalSpent}`,
|
||||||
|
type: 'system',
|
||||||
|
},
|
||||||
|
{ text: '==================', type: 'system' },
|
||||||
|
{ text: '', type: 'system' }
|
||||||
|
)
|
||||||
|
|
||||||
|
setLogLines(lines)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing log:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mx-auto flex w-[calc(100%-1rem)] max-w-fd-container flex-col items-start gap-4 pt-16'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Dropzone
|
||||||
|
onDropAccepted={(files) => {
|
||||||
|
const file = files[0]
|
||||||
|
if (!(file instanceof File)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return parseLogFile(file)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropzoneZone className={'w-full'}>
|
||||||
|
<DropzoneInput />
|
||||||
|
<DropzoneGroup className='gap-4'>
|
||||||
|
<DropzoneUploadIcon />
|
||||||
|
<DropzoneGroup>
|
||||||
|
<DropzoneTitle>Drop files here or click to upload</DropzoneTitle>
|
||||||
|
<DropzoneDescription>
|
||||||
|
Upload your corrupted <strong>profile.jkr</strong> and get a
|
||||||
|
fixed version.
|
||||||
|
</DropzoneDescription>
|
||||||
|
</DropzoneGroup>
|
||||||
|
</DropzoneGroup>
|
||||||
|
</DropzoneZone>
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
<div className='mt-8'>
|
||||||
|
{logLines.map((line, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`py-2 ${
|
||||||
|
line.type === 'event'
|
||||||
|
? 'text-blue-400'
|
||||||
|
: line.type === 'status'
|
||||||
|
? 'text-green-400'
|
||||||
|
: 'font-mono text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user