Skip to content

Commit

Permalink
Make regexes configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
GreenAppers committed Nov 17, 2024
1 parent f3802c3 commit 4e6ee19
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 92 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"electron-log": "^5.2.0",
"electron-squirrel-startup": "^1.0.1",
"framer-motion": "^11.5.6",
"glob": "^11.0.0",
"p-settle": "^5.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
130 changes: 55 additions & 75 deletions src/components/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import React, { useEffect, useState } from 'react'
import type { TimeSeries, TimeValue } from '../types'
import { addSamplesToTimeseries } from '../utils/timeseries'
import TimeseriesChart from './TimeseriesChart'
import { QUERY_KEYS, STORE_KEYS } from '../constants'
import { GameAnalyticsPattern, QUERY_KEYS, STORE_KEYS } from '../constants'
import { useQuery } from '@tanstack/react-query'

interface AnalyticsWindow {
Expand All @@ -43,12 +43,6 @@ interface TimeseriesUpdates {

type PlayersTimeseriesUpdates = Record<string, TimeseriesUpdates>

const soldContainer = /Successfully sold a container worth: \$([,\d]+.\d+)!/
const soldContainer2 = /Sold \d+ item\(s\) for \$([,\d]+.\d+)!/

const playerKilledPVPLegacy =
/(\S+) has been killed by (\S+) with ([.\d]+) health left./

const formatPlayerKey = (userName: string, serverName: string) =>
`${userName}@${serverName}`

Expand Down Expand Up @@ -145,6 +139,15 @@ function topUpAnalyticsTimeSeries(analytics: Record<string, GameAnalytics>) {
return result
}

export const useGameAnalyticsPatternsQuery = () =>
useQuery<GameAnalyticsPattern[]>({
queryKey: [QUERY_KEYS.useGameAnalyticsPatterns],
queryFn: () => window.api.store.get(STORE_KEYS.gameAnalyticsPatterns),
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})

export const useGameLogDirectoriesQuery = () =>
useQuery<string[]>({
queryKey: [QUERY_KEYS.useGameLogDirectories],
Expand All @@ -162,101 +165,75 @@ export function Analytics() {
duration: 60 * 1000,
samples: 24 * 60,
})
const gameAnalyticsPatterns = useGameAnalyticsPatternsQuery()
const gameLogDirectories = useGameLogDirectoriesQuery()
const [showGameLogDirectories, setShowGameLogDirectories] = useState(false)
const [showGameLogFiles, setShowGameLogFiles] = useState(false)

useEffect(() => {
let total = 0
let earliestTimestamp: Date | undefined
if (!gameLogDirectories.isSuccess || !gameLogDirectories.data?.length)
return

const stats: Record<
string,
{
earliestTimestamp: Date
series: Record<string, { total: number }>
}
> = {}

const handle = window.api.readGameLogs(
gameLogDirectories.data,
(lines) => {
const updates: PlayersTimeseriesUpdates = {}
for (const line of lines) {
if (!earliestTimestamp) earliestTimestamp = line.timestamp
const key = formatPlayerKey(line.userName, line.serverName)
const stat =
stats[key] ||
(stats[key] = { earliestTimestamp: line.timestamp, series: {} })

ensureAnalyticsTimeSeriesUpdate(
updates,
line.userName,
line.serverName
).sources.add(line.source)

const playerKilledPVPLegacyMatch = line.content.match(
playerKilledPVPLegacy
)
if (playerKilledPVPLegacyMatch) {
let userNameMatch = false
if (playerKilledPVPLegacyMatch[1] === line.userName) {
addAnalyticsTimeSeriesUpdate(
updates,
line.userName,
line.serverName,
'deaths',
line.timestamp,
1
)
userNameMatch = true
if (!gameAnalyticsPatterns.isSuccess) continue
for (const pattern of gameAnalyticsPatterns.data) {
const match = line.content.match(pattern.pattern)
if (
!match ||
(pattern.usernameIndex !== undefined &&
match[pattern.usernameIndex] !== line.userName)
) {
continue
}
if (playerKilledPVPLegacyMatch[2] === line.userName) {
addAnalyticsTimeSeriesUpdate(
updates,
line.userName,
line.serverName,
'kills',
line.timestamp,
1
)
userNameMatch = true
}
if (userNameMatch)
console.log(
'PVP kill',
playerKilledPVPLegacyMatch[1],
'was killed by',
playerKilledPVPLegacyMatch[2],
'with',
playerKilledPVPLegacyMatch[3],
'health left',
line.source
)
continue
}

let soldContainerValue = 0
const soldContainerMatch = line.content.match(soldContainer)
if (soldContainerMatch) {
soldContainerValue = parseFloat(
soldContainerMatch[1].replace(/,/g, '')
)
}
const soldContainerMatch2 = line.content.match(soldContainer2)
if (soldContainerMatch2) {
soldContainerValue = parseFloat(
soldContainerMatch2[1].replace(/,/g, '')
)
}

if (soldContainerValue) {
total += soldContainerValue
const value =
(pattern.valueIndex &&
parseFloat(match[pattern.valueIndex]?.replace(/,/g, ''))) ||
1
const seriesStat =
stat.series[pattern.name] ||
(stat.series[pattern.name] = { total: 0 })
seriesStat.total += value
addAnalyticsTimeSeriesUpdate(
updates,
line.userName,
line.serverName,
'sold',
pattern.name,
line.timestamp,
soldContainerValue
value
)

const totalSeconds =
(line.timestamp.getTime() - earliestTimestamp.getTime()) / 1000
const ratePerMinute = (total * 60) / totalSeconds
(line.timestamp.getTime() - stat.earliestTimestamp.getTime()) /
1000
const ratePerMinute = (seriesStat.total * 60) / totalSeconds
console.log(
`${line.userName}@${line.serverName} Sold container value`,
soldContainerValue,
`${line.userName}@${line.serverName} ${pattern.name}`,
value,
'Total',
total,
seriesStat.total,
'Rate',
ratePerMinute.toFixed(2),
'per minute',
Expand All @@ -265,7 +242,6 @@ export function Analytics() {
line.timestamp,
line.source
)
continue
}
}
setAnalytics((analytics) =>
Expand All @@ -278,6 +254,8 @@ export function Analytics() {
return () => window.api.removeListener(handle)
}, [
analyticsWindow,
gameAnalyticsPatterns.isSuccess,
gameAnalyticsPatterns.data,
gameLogDirectories.isSuccess,
gameLogDirectories.data,
setAnalytics,
Expand Down Expand Up @@ -307,7 +285,9 @@ export function Analytics() {
onChange={(event) => setAnalyticsProfile(event.target.value)}
>
{Object.keys(analytics).map((profile) => (
<option key={profile} value={profile}>{profile}</option>
<option key={profile} value={profile}>
{profile}
</option>
))}
</Select>
</Flex>
Expand Down
12 changes: 12 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ export const gameAccount = z.object({
yggdrasilToken: minecraftLoginResponse,
})

export const gameAnalyticsPattern = z.object({
name: z.string(),
pattern: z
.union([z.string(), z.instanceof(RegExp)])
.transform((x) => new RegExp(x)),
usernameIndex: z.number().optional(),
valueIndex: z.number().optional(),
})

export const gameInstall = z.object({
name: z.string(),
path: z.string(),
Expand Down Expand Up @@ -213,17 +222,20 @@ export type MojangStringsTemplate = z.infer<typeof mojangStringsTemplate>
export type XSTSAuthorizeResponse = z.infer<typeof xstsAuthorizeResponse>

export type GameAccount = z.infer<typeof gameAccount>
export type GameAnalyticsPattern = z.infer<typeof gameAnalyticsPattern>
export type GameInstall = z.infer<typeof gameInstall>
export type Waypoint = z.infer<typeof waypoint>

export const STORE_KEYS = {
gameAnalyticsPatterns: 'gameAnalyticsPatterns',
gameAccounts: 'gameAccounts',
gameInstalls: 'gameInstalls',
gameLogDirectories: 'gameLogDirectories',
waypoints: 'waypoints',
}

export const QUERY_KEYS = {
useGameAnalyticsPatterns: 'useGameAnalyticsPatterns',
useGameInstalls: 'useGameInstalls',
useGameLogDirectories: 'useGameLogDirectories',
useMojangVersionManifests: 'useMojangVersionManifests',
Expand Down
24 changes: 21 additions & 3 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
import { z } from 'zod'
import Store from 'zod-electron-store'

import { getDefaultGameLogDirectories } from './utils/gamelog'
import {
gameAccount,
GameAccount,
gameAnalyticsPattern,
gameInstall,
GameInstall,
STORE_KEYS,
waypoint,
} from './constants'
import {
getDefaultAnalyticsPatterns,
getDefaultGameLogDirectories,
} from './utils/gamelog'

export { Store }

export const storeSchema = z.object({
gameAccounts: z.array(gameAccount).default([]),
gameAnalyticsPatterns: z
.array(gameAnalyticsPattern)
.default(getDefaultAnalyticsPatterns()),
gameInstalls: z.array(gameInstall).default([]),
gameLogDirectories: z.array(z.string()).default(getDefaultGameLogDirectories()),
gameLogDirectories: z
.array(z.string())
.default(getDefaultGameLogDirectories()),
waypoints: z.array(waypoint).default([]),
})

export type StoreSchema = z.infer<typeof storeSchema>

export const newStore = () => new Store<StoreSchema>({ schema: storeSchema })
export const newStore = () =>
new Store<StoreSchema>({
schema: storeSchema,
serialize: (value) =>
JSON.stringify(
value,
(_, x) => (x instanceof RegExp ? x.toString().slice(1,-1) : x),
'\t'
),
})

export const updateGameAccount = (
store: Store<StoreSchema>,
Expand Down
45 changes: 32 additions & 13 deletions src/utils/gamelog.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import fs from 'fs'
import { glob } from 'glob'
import os from 'os'
import path from 'path'
import split2 from 'split2'
import stream from 'stream'
import { Tail } from 'tail'
import zlib from 'zlib'
import { GameAnalyticsPattern } from '../constants'
import type { GameLog, GameLogLine } from '../types'

export interface GameLogContext {
Expand Down Expand Up @@ -55,16 +57,36 @@ export function getDefaultGameLogDirectories() {
`${process.env.HOME}/Library/Application Support/minecraft/logs`
)
paths.push(`${process.env.HOME}/.lunarclient/offline/multiver/logs`)
paths.push('/Applications/MultiMC.app/Data/instances/*/.minecraft/logs')
break
}

return paths.filter((path) => {
try {
return fs.statSync(path).isDirectory()
} catch {
return false
}
})
return paths
}

export function getDefaultAnalyticsPatterns(): GameAnalyticsPattern[] {
return [
{
name: 'sold',
pattern: /Successfully sold a container worth: \$([,\d]+.\d+)!/,
valueIndex: 1,
},
{
name: 'sold',
pattern: /Sold \d+ item\(s\) for \$([,\d]+.\d+)!/,
valueIndex: 1,
},
{
name: 'deaths',
pattern: /(\S+) has been killed by (\S+) with ([.\d]+) health left./,
usernameIndex: 1,
},
{
name: 'kills',
pattern: /(\S+) has been killed by (\S+) with ([.\d]+) health left./,
usernameIndex: 2,
},
]
}

export async function findGameLogFiles(
Expand All @@ -75,12 +97,9 @@ export async function findGameLogFiles(
const logFiles: string[] = []
for (const dir of gameLogDirectories) {
try {
const files = await fs.promises.readdir(dir)
logFiles.push(
...files
.filter((file) => file.endsWith('.log') || file.endsWith('.log.gz'))
.map((file) => path.join(dir, file))
)
const pattern = path.join(dir, '*.{log,log.gz}')
const files = await glob(pattern)
logFiles.push(...files)
} catch {
continue
}
Expand Down
Loading

0 comments on commit 4e6ee19

Please sign in to comment.