diff --git a/public/cards/v_antimatter.png b/public/cards/v_antimatter.png new file mode 100644 index 0000000..69a4a7e Binary files /dev/null and b/public/cards/v_antimatter.png differ diff --git a/public/cards/v_blank.png b/public/cards/v_blank.png new file mode 100644 index 0000000..9ec43b6 Binary files /dev/null and b/public/cards/v_blank.png differ diff --git a/public/cards/v_clearance_sale.png b/public/cards/v_clearance_sale.png new file mode 100644 index 0000000..88ed14c Binary files /dev/null and b/public/cards/v_clearance_sale.png differ diff --git a/public/cards/v_crystal_ball.png b/public/cards/v_crystal_ball.png new file mode 100644 index 0000000..2448741 Binary files /dev/null and b/public/cards/v_crystal_ball.png differ diff --git a/public/cards/v_directors_cut.png b/public/cards/v_directors_cut.png new file mode 100644 index 0000000..ce8e0d2 Binary files /dev/null and b/public/cards/v_directors_cut.png differ diff --git a/public/cards/v_glow_up.png b/public/cards/v_glow_up.png new file mode 100644 index 0000000..cbf752a Binary files /dev/null and b/public/cards/v_glow_up.png differ diff --git a/public/cards/v_grabber.png b/public/cards/v_grabber.png new file mode 100644 index 0000000..177b1ef Binary files /dev/null and b/public/cards/v_grabber.png differ diff --git a/public/cards/v_hieroglyph.png b/public/cards/v_hieroglyph.png new file mode 100644 index 0000000..6dac24f Binary files /dev/null and b/public/cards/v_hieroglyph.png differ diff --git a/public/cards/v_hone.png b/public/cards/v_hone.png new file mode 100644 index 0000000..416f584 Binary files /dev/null and b/public/cards/v_hone.png differ diff --git a/public/cards/v_illusion.png b/public/cards/v_illusion.png new file mode 100644 index 0000000..7c6bc01 Binary files /dev/null and b/public/cards/v_illusion.png differ diff --git a/public/cards/v_liquidation.png b/public/cards/v_liquidation.png new file mode 100644 index 0000000..1b5c84a Binary files /dev/null and b/public/cards/v_liquidation.png differ diff --git a/public/cards/v_locked.png b/public/cards/v_locked.png new file mode 100644 index 0000000..89637bd Binary files /dev/null and b/public/cards/v_locked.png differ diff --git a/public/cards/v_magic_trick.png b/public/cards/v_magic_trick.png new file mode 100644 index 0000000..e3c9384 Binary files /dev/null and b/public/cards/v_magic_trick.png differ diff --git a/public/cards/v_money_tree.png b/public/cards/v_money_tree.png new file mode 100644 index 0000000..f9d7288 Binary files /dev/null and b/public/cards/v_money_tree.png differ diff --git a/public/cards/v_nacho_tong.png b/public/cards/v_nacho_tong.png new file mode 100644 index 0000000..c7189cd Binary files /dev/null and b/public/cards/v_nacho_tong.png differ diff --git a/public/cards/v_observatory.png b/public/cards/v_observatory.png new file mode 100644 index 0000000..51a5bd4 Binary files /dev/null and b/public/cards/v_observatory.png differ diff --git a/public/cards/v_omen_globe.png b/public/cards/v_omen_globe.png new file mode 100644 index 0000000..9908de2 Binary files /dev/null and b/public/cards/v_omen_globe.png differ diff --git a/public/cards/v_overstock_norm.png b/public/cards/v_overstock_norm.png new file mode 100644 index 0000000..8918bd3 Binary files /dev/null and b/public/cards/v_overstock_norm.png differ diff --git a/public/cards/v_overstock_plus.png b/public/cards/v_overstock_plus.png new file mode 100644 index 0000000..c801bc0 Binary files /dev/null and b/public/cards/v_overstock_plus.png differ diff --git a/public/cards/v_paint_brush.png b/public/cards/v_paint_brush.png new file mode 100644 index 0000000..4ac2b1e Binary files /dev/null and b/public/cards/v_paint_brush.png differ diff --git a/public/cards/v_palette.png b/public/cards/v_palette.png new file mode 100644 index 0000000..245812a Binary files /dev/null and b/public/cards/v_palette.png differ diff --git a/public/cards/v_petroglyph.png b/public/cards/v_petroglyph.png new file mode 100644 index 0000000..d1856ad Binary files /dev/null and b/public/cards/v_petroglyph.png differ diff --git a/public/cards/v_planet_merchant.png b/public/cards/v_planet_merchant.png new file mode 100644 index 0000000..2820b13 Binary files /dev/null and b/public/cards/v_planet_merchant.png differ diff --git a/public/cards/v_planet_tycoon.png b/public/cards/v_planet_tycoon.png new file mode 100644 index 0000000..084c3ab Binary files /dev/null and b/public/cards/v_planet_tycoon.png differ diff --git a/public/cards/v_recyclomancy.png b/public/cards/v_recyclomancy.png new file mode 100644 index 0000000..830acf3 Binary files /dev/null and b/public/cards/v_recyclomancy.png differ diff --git a/public/cards/v_reroll_glut.png b/public/cards/v_reroll_glut.png new file mode 100644 index 0000000..c73d4fe Binary files /dev/null and b/public/cards/v_reroll_glut.png differ diff --git a/public/cards/v_reroll_surplus.png b/public/cards/v_reroll_surplus.png new file mode 100644 index 0000000..6948ba7 Binary files /dev/null and b/public/cards/v_reroll_surplus.png differ diff --git a/public/cards/v_retcon.png b/public/cards/v_retcon.png new file mode 100644 index 0000000..6d5490f Binary files /dev/null and b/public/cards/v_retcon.png differ diff --git a/public/cards/v_seed_money.png b/public/cards/v_seed_money.png new file mode 100644 index 0000000..8384f3c Binary files /dev/null and b/public/cards/v_seed_money.png differ diff --git a/public/cards/v_tarot_merchant.png b/public/cards/v_tarot_merchant.png new file mode 100644 index 0000000..5299e4b Binary files /dev/null and b/public/cards/v_tarot_merchant.png differ diff --git a/public/cards/v_tarot_tycoon.png b/public/cards/v_tarot_tycoon.png new file mode 100644 index 0000000..3bca031 Binary files /dev/null and b/public/cards/v_tarot_tycoon.png differ diff --git a/public/cards/v_telescope.png b/public/cards/v_telescope.png new file mode 100644 index 0000000..463608a Binary files /dev/null and b/public/cards/v_telescope.png differ diff --git a/public/cards/v_undiscovered.png b/public/cards/v_undiscovered.png new file mode 100644 index 0000000..d5c2c38 Binary files /dev/null and b/public/cards/v_undiscovered.png differ diff --git a/public/cards/v_wasteful.png b/public/cards/v_wasteful.png new file mode 100644 index 0000000..cab2db7 Binary files /dev/null and b/public/cards/v_wasteful.png differ diff --git a/src/app/(home)/log-parser/page.tsx b/src/app/(home)/log-parser/page.tsx index ae87545..d794d7a 100644 --- a/src/app/(home)/log-parser/page.tsx +++ b/src/app/(home)/log-parser/page.tsx @@ -34,6 +34,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { jokers } from '@/shared/jokers' +import { vouchers } from '@/shared/vouchers' import { useFormatter } from 'next-intl' import Image from 'next/image' import { useSearchParams } from 'next/navigation' @@ -107,7 +108,12 @@ type Game = { logOwnerFinalJokers: string[] // Log owner's final jokers opponentFinalJokers: string[] // Opponent's final jokers events: LogEvent[] - rerolls: number + rerolls: number // Log owner's reroll count + rerollCostTotal: number // Log owner's total reroll cost + logOwnerVouchers: string[] // Log owner's vouchers + opponentRerolls: number // Opponent's reroll count + opponentRerollCostTotal: number // Opponent's total reroll cost + opponentVouchers: string[] // Opponent's vouchers winner: 'logOwner' | 'opponent' | null // Who won the game pvpBlinds: PvpBlind[] // PVP blind data currentPvpBlind: number | null // Current PVP blind number @@ -140,6 +146,11 @@ const initGame = (id: number, startDate: Date): Game => ({ opponentFinalJokers: [], events: [], rerolls: 0, + rerollCostTotal: 0, + logOwnerVouchers: [], + opponentRerolls: 0, + opponentRerollCostTotal: 0, + opponentVouchers: [], winner: null, pvpBlinds: [], currentPvpBlind: null, @@ -347,6 +358,34 @@ export default function LogParser() { } continue } + if (line.includes('Client got nemesisEndGameStats message')) { + if (currentGame) { + // Extract Opponent Reroll Count + const rerollCountMatch = line.match(/\(reroll_count: (\d+)\)/) + if (rerollCountMatch?.[1]) { + currentGame.opponentRerolls = Number.parseInt( + rerollCountMatch[1], + 10 + ) + } + + // Extract Opponent Reroll Cost Total + const rerollCostMatch = line.match(/\(reroll_cost_total: (\d+)\)/) + if (rerollCostMatch?.[1]) { + currentGame.opponentRerollCostTotal = Number.parseInt( + rerollCostMatch[1], + 10 + ) + } + + // Extract Opponent Vouchers + const vouchersMatch = line.match(/\(vouchers: ([^)]+)\)/) + if (vouchersMatch?.[1]) { + currentGame.opponentVouchers = vouchersMatch[1].split('-') + } + } + continue + } if (line.includes('Client sent message: action:receiveEndGameJokers')) { if (currentGame) { // Mark end date if not already set (might happen slightly before 'got') @@ -362,6 +401,31 @@ export default function LogParser() { } continue } + if (line.includes('Client sent message: action:nemesisEndGameStats')) { + if (currentGame) { + // Extract Log Owner Reroll Count + const rerollCountMatch = line.match(/reroll_count:(\d+)/) + if (rerollCountMatch?.[1]) { + currentGame.rerolls = Number.parseInt(rerollCountMatch[1], 10) + } + + // Extract Log Owner Reroll Cost Total + const rerollCostMatch = line.match(/reroll_cost_total:(\d+)/) + if (rerollCostMatch?.[1]) { + currentGame.rerollCostTotal = Number.parseInt( + rerollCostMatch[1], + 10 + ) + } + + // Extract Log Owner Vouchers + const vouchersMatch = line.match(/vouchers:(.+)$/) + if (vouchersMatch?.[1]) { + currentGame.logOwnerVouchers = vouchersMatch[1].split('-') + } + } + continue + } if (lineLower.includes('startgame message')) { if (currentGame) { if (!currentGame.endDate) currentGame.endDate = timestamp @@ -855,7 +919,8 @@ export default function LogParser() { ? ((currentGame.endDate instanceof Date ? currentGame.endDate.getTime() : new Date(currentGame.endDate).getTime()) - - currentGame.startDate.getTime()) / 1000 + currentGame.startDate.getTime()) / + 1000 : null games.push(currentGame) } @@ -1049,8 +1114,18 @@ export default function LogParser() { : opponentLabel}

- Rerolls:{' '} + {ownerLabel} Rerolls:{' '} {game.rerolls || 'Unknown'} + {game.rerollCostTotal + ? ` (Cost: ${game.rerollCostTotal})` + : ''} +

+

+ {opponentLabel} Rerolls:{' '} + {game.opponentRerolls || 'Unknown'} + {game.opponentRerollCostTotal + ? ` (Cost: ${game.opponentRerollCostTotal})` + : ''}

Deck: {game.deck || 'Unknown'} @@ -1277,6 +1352,93 @@ export default function LogParser() { + + + Vouchers + + +

+ + {ownerLabel} + {game.winner === 'logOwner' ? ' 🏆' : ''}: + + {game.logOwnerVouchers.length > 0 ? ( + + ) : ( +

+ No data found. +

+ )} +
+
+ + {opponentLabel} + {game.winner === 'opponent' ? ' 🏆' : ''}: + + {game.opponentVouchers.length > 0 ? ( + + ) : ( +

+ No data found. +

+ )} +
+ + Mods @@ -1637,6 +1799,18 @@ function cleanJokerKey(key: string): string { ) } +function cleanVoucherKey(key: string): string { + if (!key) return '' + return key + .trim() + .replace(/^v_/, '') // Remove prefix v_ + .replace(/_/g, ' ') // Replace underscores with spaces + .replace( + /\w\S*/g, // Capitalize each word (Title Case) + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) +} + type JsonValue = | string | number diff --git a/src/shared/vouchers.ts b/src/shared/vouchers.ts new file mode 100644 index 0000000..0ddd428 --- /dev/null +++ b/src/shared/vouchers.ts @@ -0,0 +1,41 @@ +export interface VoucherInfo { + name: string + file: string +} + +export const vouchers: { [key: string]: VoucherInfo } = { + v_antimatter: { name: 'Antimatter', file: 'v_antimatter.png' }, + v_blank: { name: 'Blank', file: 'v_blank.png' }, + v_clearance_sale: { name: 'Clearance Sale', file: 'v_clearance_sale.png' }, + v_crystal_ball: { name: 'Crystal Ball', file: 'v_crystal_ball.png' }, + v_directors_cut: { name: "Director's Cut", file: 'v_directors_cut.png' }, + v_glow_up: { name: 'Glow Up', file: 'v_glow_up.png' }, + v_grabber: { name: 'Grabber', file: 'v_grabber.png' }, + v_hieroglyph: { name: 'Hieroglyph', file: 'v_hieroglyph.png' }, + v_hone: { name: 'Hone', file: 'v_hone.png' }, + v_illusion: { name: 'Illusion', file: 'v_illusion.png' }, + v_liquidation: { name: 'Liquidation', file: 'v_liquidation.png' }, + v_locked: { name: 'Locked', file: 'v_locked.png' }, + v_magic_trick: { name: 'Magic Trick', file: 'v_magic_trick.png' }, + v_money_tree: { name: 'Money Tree', file: 'v_money_tree.png' }, + v_nacho_tong: { name: 'Nacho Tong', file: 'v_nacho_tong.png' }, + v_observatory: { name: 'Observatory', file: 'v_observatory.png' }, + v_omen_globe: { name: 'Omen Globe', file: 'v_omen_globe.png' }, + v_overstock_norm: { name: 'Overstock', file: 'v_overstock_norm.png' }, + v_overstock_plus: { name: 'Overstock Plus', file: 'v_overstock_plus.png' }, + v_paint_brush: { name: 'Paint Brush', file: 'v_paint_brush.png' }, + v_palette: { name: 'Palette', file: 'v_palette.png' }, + v_petroglyph: { name: 'Petroglyph', file: 'v_petroglyph.png' }, + v_planet_merchant: { name: 'Planet Merchant', file: 'v_planet_merchant.png' }, + v_planet_tycoon: { name: 'Planet Tycoon', file: 'v_planet_tycoon.png' }, + v_recyclomancy: { name: 'Recyclomancy', file: 'v_recyclomancy.png' }, + v_reroll_glut: { name: 'Reroll Glut', file: 'v_reroll_glut.png' }, + v_reroll_surplus: { name: 'Reroll Surplus', file: 'v_reroll_surplus.png' }, + v_retcon: { name: 'Retcon', file: 'v_retcon.png' }, + v_seed_money: { name: 'Seed Money', file: 'v_seed_money.png' }, + v_tarot_merchant: { name: 'Tarot Merchant', file: 'v_tarot_merchant.png' }, + v_tarot_tycoon: { name: 'Tarot Tycoon', file: 'v_tarot_tycoon.png' }, + v_telescope: { name: 'Telescope', file: 'v_telescope.png' }, + v_undiscovered: { name: 'Locked', file: 'v_undiscovered.png' }, + v_wasteful: { name: 'Wasteful', file: 'v_wasteful.png' }, +} diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..1d40706 --- /dev/null +++ b/test.ts @@ -0,0 +1,118 @@ +import { db } from '@/server/db' +import { player_games } from '@/server/db/schema' +import { desc, eq } from 'drizzle-orm' +import { Redis } from 'ioredis' +const redisClient = new Redis(process.env.REDIS_URL as string) + +type PlayerState = { + status: string + currentMatch?: { + opponentId: string + startTime: number + } +} + +type StatusCount = { + [key: string]: number +} + +type QueueEntry = { + key: string + value: PlayerState +} + +async function findQueueingPlayers(redis: Redis): Promise { + try { + const queueingPlayers: QueueEntry[] = [] + let cursor = '0' + const pattern = 'player:*:state' + + do { + const [newCursor, keys] = await redis.scan( + cursor, + 'MATCH', + pattern, + 'COUNT', + '1000' + ) + + cursor = newCursor + + if (keys.length > 0) { + const values = await redis.mget(keys) + + keys.forEach((key, index) => { + const value = values[index] + if (!value) return + + try { + const state = JSON.parse(value) as PlayerState + if (state.status === 'queuing') { + queueingPlayers.push({ + key, + value: state, + }) + } + } catch (parseError) { + console.error('Failed to parse player state:', parseError) + console.error('Problematic value:', value) + } + }) + } + } while (cursor !== '0') + + return queueingPlayers + } catch (error) { + console.error('Failed to find queuing players:', error) + throw error + } +} + +redisClient.on('connect', () => { + console.log('Redis client connected') +}) + +redisClient.on('error', (err) => { + console.error('Redis client error:', err) +}) + +async function logQueueingPlayers() { + try { + const queueingPlayers = await findQueueingPlayers(redisClient) + console.log('Found', queueingPlayers.length, 'queuing players:') + console.log(queueingPlayers) + + return await Promise.all( + queueingPlayers.map(async (player) => { + const lastGame = await db + .select() + .from(player_games) + .where( + eq( + player_games.playerId, + player.key.replace('player:', '').replace(':state', '') + ) + ) + .orderBy(desc(player_games.gameTime)) + .limit(1) + return lastGame[0] + }) + ) + } catch (error) { + console.error('Failed to log queuing players:', error) + throw error + } finally { + await redisClient.quit() + } +} + +logQueueingPlayers() + .then((e) => { + for (const p of e) + console.log('--', p?.playerName, Math.round(p?.playerMmr ?? 0), 'MMR') + process.exit(0) + }) + .catch((error) => { + console.error('Unhandled error in logQueueingPlayers:', error) + process.exit(1) + })