diff --git a/src/app/(home)/log-parser/page.tsx b/src/app/(home)/log-parser/page.tsx new file mode 100644 index 0000000..8ca05ac --- /dev/null +++ b/src/app/(home)/log-parser/page.tsx @@ -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([]) + + 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 ( +
+ { + const file = files[0] + if (!(file instanceof File)) { + return + } + return parseLogFile(file) + }} + > + + + + + + Drop files here or click to upload + + Upload your corrupted profile.jkr and get a + fixed version. + + + + + + +
+ {logLines.map((line, i) => ( +
+ {line.text} +
+ ))} +
+
+ ) +}