Skip to content

Commit

Permalink
Improve analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
GreenAppers committed Oct 31, 2024
1 parent 1d283ab commit 9fe8d96
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 124 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"electron-is-dev": "^3.0.1",
"electron-log": "^5.2.0",
"electron-squirrel-startup": "^1.0.1",
"electron-store": "^8.2.0",
"framer-motion": "^11.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^2.13.0",
"tail": "^2.2.6"
"split2": "^4.2.0",
"tail": "^2.1.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand All @@ -50,6 +52,7 @@
"@electron/fuses": "^1.8.0",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
"@types/split2": "^4.2.3",
"@types/tail": "^2.2.3",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
Expand All @@ -66,5 +69,8 @@
"ts-loader": "^9.2.2",
"ts-node": "^10.0.0",
"typescript": "~4.5.4"
},
"engines": {
"node": "v20.16.0"
}
}
203 changes: 159 additions & 44 deletions src/components/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { Heading, IconButton, Link, Tooltip } from '@chakra-ui/react'
import { EditIcon } from '@chakra-ui/icons'
import {
Flex,
Heading,
IconButton,
Link,
List,
ListItem,
Select,
Tooltip,
} from '@chakra-ui/react'
import {
ChevronDownIcon,
ChevronUpIcon,
EditIcon,
SettingsIcon,
} from '@chakra-ui/icons'
import React, { useEffect, useState } from 'react'

import type { TimeSeries } from '../types'
import { addSampleToTimeseries } from '../utils/timeseries'
import TimeseriesChart from './TimeseriesChart'
import { STORE_KEYS } from '../constants'

interface AnalyticsWindow {
beginDate?: Date
Expand All @@ -21,10 +36,29 @@ interface GameAnalytics {
}

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./

function topUpAnalyticsTimeSeries(analytics: Record<string, GameAnalytics>) {
const now = new Date()
const result = { ...analytics }
for (const key in analytics) {
const userAnalytics = analytics[key]
for (const seriesName in userAnalytics.timeSeries) {
const timeseries = userAnalytics.timeSeries[seriesName]
result[key].timeSeries[seriesName] = addSampleToTimeseries(
0,
now,
timeseries,
now
)
}
}
return result
}

function updateAnalyticsTimeSeries(
analytics: Record<string, GameAnalytics>,
window: AnalyticsWindow,
Expand All @@ -33,7 +67,7 @@ function updateAnalyticsTimeSeries(
seriesName: string,
value: number,
timestamp: Date,
source: string
source?: string
) {
const key = `${userName}@${serverName}`
let userAnalytics = analytics[key]
Expand All @@ -54,36 +88,50 @@ function updateAnalyticsTimeSeries(
}
}
timeseries = addSampleToTimeseries(value, timestamp, timeseries, timestamp)
return {
const result = {
...analytics,
[key]: {
...userAnalytics,
gamelogs: [
...userAnalytics.gamelogs,
...(userAnalytics.gamelogs.find((x) => x === source) ? [] : [source]),
...(!source || userAnalytics.gamelogs.find((x) => x === source)
? []
: [source]),
],
timeSeries: {
...userAnalytics.timeSeries,
[seriesName]: timeseries,
},
},
}
// console.log('updateAnalyticsTimeSeries result', result)
return result
}

export function Analytics() {
const [analytics, setAnalytics] = useState<Record<string, GameAnalytics>>({})
const [analyticsProfile, setAnalyticsProfile] = useState('')
const [analyticsWindow, setAnalyticsWindow] = useState<AnalyticsWindow>({
beginDate: new Date(new Date().setHours(0, 0, 0, 0)),
duration: 24 * 60 * 1000,
samples: 24 * 10,
beginDate: new Date(new Date().setHours(0, 0, 0, 0) - 24 * 60 * 60 * 1000),
duration: 60 * 1000,
samples: 24 * 60,
})
const [gameLogDirectories, setGameLogDirectories] = useState<string[]>([])
const [showGameLogDirectories, setShowGameLogDirectories] = useState(false)
const [showGameLogFiles, setShowGameLogFiles] = useState(false)

// load store
useEffect(() => {
setGameLogDirectories(window.api.store.get(STORE_KEYS.gameLogDirectories))
}, [])

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

const handle = window.api.readGameLogs(
gameLogDirectories,
(
userName: string,
serverName: string,
Expand Down Expand Up @@ -140,11 +188,21 @@ export function Analytics() {
return
}

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

if (soldContainerValue) {
total += soldContainerValue
setAnalytics((analytics) =>
updateAnalyticsTimeSeries(
Expand All @@ -162,15 +220,17 @@ export function Analytics() {
(timestamp.getTime() - earliestTimestamp.getTime()) / 1000
const ratePerMinute = (total * 60) / totalSeconds
console.log(
'Sold container value',
`${userName}@${serverName} Sold container value`,
soldContainerValue,
'Total',
total,
'Rate',
ratePerMinute.toFixed(2),
'per minute',
(ratePerMinute * 60).toFixed(2),
'per hour'
'per hour',
timestamp,
source
)
return
}
Expand All @@ -179,57 +239,112 @@ export function Analytics() {
analyticsWindow.endDate
)
return () => window.api.removeListener(handle)
}, [setAnalytics])
}, [analyticsWindow, gameLogDirectories, setAnalytics])

/*
useEffect(() => {
setAnalytics(topUpAnalyticsTimeSeries)
const handle = setInterval(
() =>
setSoldTimeseries((timeseries) =>
addSampleToTimeseries(0, new Date(), timeseries)
),
() => setAnalytics(topUpAnalyticsTimeSeries),
30 * 1000
)
return () => clearInterval(handle)
}, [setSoldTimeseries])
*/
}, [setAnalytics])

const session: GameAnalytics | undefined =
analytics[analyticsProfile] || analytics[Object.keys(analytics)[0]]
const sessionName = analytics[analyticsProfile]
? analyticsProfile
: Object.keys(analytics)[0] ?? ''
const session: GameAnalytics | undefined = analytics[sessionName]

return (
<>
<Heading>
📊{' '}
{(session?.userName || '') +
(session?.userName && session?.serverName ? ' @ ' : '') +
(session?.serverName || '')}
<Flex>
📊&nbsp;
<Select
value={sessionName}
onChange={(event) => setAnalyticsProfile(event.target.value)}
>
{Object.keys(analytics).map((profile) => (
<option value={profile}>{profile}</option>
))}
</Select>
</Flex>
</Heading>
<Heading as="h6" size="xs">
Game log&nbsp;
<Link
onClick={() =>
window.api.openBrowserWindow(`file://${session?.gamelogs?.[0]}`)
}
>
{session?.gamelogs?.[0]}
</Link>

<Heading as="h6" size="xs" marginTop="1rem">
Game logs&nbsp;
<Tooltip label="Show game log files">
<IconButton
aria-label="Game log files"
icon={showGameLogFiles ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => setShowGameLogFiles((x) => !x)}
/>
</Tooltip>
&nbsp;
<Tooltip label="Choose game log">
<Tooltip label="Setup game log directories">
<IconButton
aria-label="Game log"
icon={<EditIcon />}
onClick={() =>
window.api
.openFileDialog(session?.gamelogs?.[0])
.then((gameLogPath) => {
console.log('setGameLogPath', gameLogPath)
})
}
aria-label="Game log directories"
icon={<SettingsIcon />}
onClick={() => setShowGameLogDirectories((x) => !x)}
/>
</Tooltip>
</Heading>

{showGameLogFiles && (
<List>
{(session?.gamelogs ?? []).map((gamelog) => (
<ListItem>
<Link
onClick={() =>
window.api.openBrowserWindow(`file://${gamelog}`)
}
>
{gamelog}
</Link>
</ListItem>
))}
</List>
)}

{showGameLogDirectories && (
<>
<Heading as="h6" size="xs" marginTop="1rem">
Game log directories
</Heading>
<List>
{(gameLogDirectories ?? []).map((directory) => (
<ListItem>
<Link
onClick={() =>
window.api.openBrowserWindow(`file://${directory}`)
}
>
{directory}
</Link>
</ListItem>
))}
<ListItem>
<Tooltip label="Add game log directory">
<>
<IconButton
aria-label="Add game log directory"
icon={<EditIcon />}
onClick={() =>
window.api
.openFileDialog(session?.gamelogs?.[0])
.then((gameLogPath) => {
console.log('setGameLogPath', gameLogPath)
})
}
/>
&nbsp;Add directory
</>
</Tooltip>
</ListItem>
</List>
</>
)}

<Heading as="h5" size="sm" marginTop="2rem">
Kills
</Heading>
Expand Down
2 changes: 1 addition & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function App() {
<TabList>
<Tab>Analytics</Tab>
<Tab>Anti/PieRay</Tab>
<Tab>Nether Portals</Tab>
<Tab>Launcher</Tab>
<Tab>Strongholds</Tab>
<Tab>Waypoints</Tab>
</TabList>
Expand Down
9 changes: 4 additions & 5 deletions src/components/PieRayHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ListIcon,
ListItem,
Link,
Spacer,
Table,
TableContainer,
Tbody,
Expand Down Expand Up @@ -324,14 +323,14 @@ export function PieRayHelper() {
dataKey="x"
type="number"
name="x"
domain={[minX - 1, maxX + 1]}
domain={[(minX ?? 0) - 1, (maxX ?? 0) + 1]}
allowDecimals={false}
/>
<YAxis
dataKey="z"
type="number"
name="z"
domain={[minZ - 1, maxZ + 1]}
domain={[(minZ ?? 0) - 1, (maxZ ?? 0) + 1]}
allowDecimals={false}
/>
<RechartsTooltip cursor={{ strokeDasharray: '3 3' }} />
Expand Down Expand Up @@ -373,8 +372,8 @@ export function PieRayHelper() {
<Td>
<Icon as={StarIcon} color="yellow.300" marginX="5px" />
</Td>
<Td>{foundX * 16 + 8 * (1 + (distX % 2))}</Td>
<Td>{foundZ * 16 + 8 * (1 + (distZ % 2))}</Td>
<Td>{foundX * 16 + 8 * (1 + ((distX ?? 0) % 2))}</Td>
<Td>{foundZ * 16 + 8 * (1 + ((distZ ?? 0) % 2))}</Td>
<Td>{foundX}</Td>
<Td>{foundZ}</Td>
<Td></Td>
Expand Down
Loading

0 comments on commit 9fe8d96

Please sign in to comment.