Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ This RuneLite plugin tracks mouse clicks during AFK sessions in Old School RuneS
Tracked Stats:
- Consistency: A score from 0 to 100 showing how regular your click timing is. Higher scores mean more consistent intervals.
- Average Click Interval: The average time in milliseconds between clicks.
- Session: Live elapsed time and click count while a session is running.

Purpose: To track afk metrics to compare between activies and methods. Similar to tracking DPS and Kills/hr.

Session history shows your most recent sessions first. Click a session name to rename it. Sessions can be copied to clipboard or deleted (with confirmation).

## Wiki Guide

See the [AFK Activity Tracker Guide](https://oldschool.runescape.wiki/w/Guide:AFK_Activity_Tracker) on the Old School RuneScape Wiki for a detailed breakdown of the tracked metrics, interactive scatter plots comparing activities, and aggregated averages across activity groups like Bankstanding, Fishing, and Salvaging.
Binary file modified panel-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 67 additions & 27 deletions src/main/java/com/afkstatstracker/AfkStatsTrackerPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
Expand All @@ -17,17 +18,22 @@
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collections;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ImageIcon;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.ui.PluginPanel;

public class AfkStatsTrackerPanel extends PluginPanel
Expand All @@ -40,6 +46,8 @@ public class AfkStatsTrackerPanel extends PluginPanel
private JButton stopButton;
private JLabel consistencyValueLabel;
private JLabel avgIntervalValueLabel;
private JLabel sessionElapsedLabel;
private JLabel sessionClicksLabel;

private ConsistencyIndicator consistencyIndicator;
private IntervalIndicator intervalIndicator;
Expand Down Expand Up @@ -104,6 +112,7 @@ public AfkStatsTrackerPanel(AfkStatsTrackerPlugin plugin, SessionHistoryManager
consistencyIndicator = new ConsistencyIndicator();
intervalIndicator = new IntervalIndicator();

statsPanel.add(createSessionCard());
statsPanel.add(consistencyPanel);
statsPanel.add(consistencyIndicator);
statsPanel.add(avgIntervalPanel);
Expand Down Expand Up @@ -142,6 +151,38 @@ private JPanel createStatCard(String title, String tooltip)
return card;
}

private JPanel createSessionCard()
{
JPanel card = new JPanel(new BorderLayout());
card.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, ColorScheme.DARK_GRAY_COLOR),
BorderFactory.createEmptyBorder(8, 5, 8, 5)
));
card.setToolTipText("Elapsed time and click count for the current session");

JLabel titleLabel = new JLabel("Session");
titleLabel.setForeground(Color.GRAY);
card.add(titleLabel, BorderLayout.NORTH);

JPanel valueRow = new JPanel();
valueRow.setLayout(new BoxLayout(valueRow, BoxLayout.X_AXIS));

sessionElapsedLabel = new JLabel("0s");
sessionElapsedLabel.setFont(sessionElapsedLabel.getFont().deriveFont(Font.BOLD, 20f));
sessionElapsedLabel.setAlignmentY(Component.BOTTOM_ALIGNMENT);

sessionClicksLabel = new JLabel(" · 0 clicks");
sessionClicksLabel.setForeground(Color.GRAY);
sessionClicksLabel.setAlignmentY(Component.BOTTOM_ALIGNMENT);

valueRow.add(sessionElapsedLabel);
valueRow.add(sessionClicksLabel);
card.add(valueRow, BorderLayout.CENTER);

card.setMaximumSize(new Dimension(Integer.MAX_VALUE, card.getPreferredSize().height));
return card;
}

private JPanel createHistorySection()
{
JPanel section = new JPanel(new BorderLayout());
Expand Down Expand Up @@ -183,13 +224,16 @@ public void refreshHistoryPanel()
{
historyContainer.removeAll();

for (Session session : sessionHistoryManager.getSessions())
List<Session> sessions = sessionHistoryManager.getSessions();
Collections.reverse(sessions);

for (Session session : sessions)
{
historyContainer.add(createSessionRow(session));
historyContainer.add(Box.createRigidArea(new Dimension(0, 3)));
}

if (sessionHistoryManager.getSessions().isEmpty())
if (sessions.isEmpty())
{
JLabel emptyLabel = new JLabel("No sessions recorded");
emptyLabel.setForeground(Color.GRAY);
Expand Down Expand Up @@ -235,31 +279,15 @@ public void mouseClicked(MouseEvent e)
statsLabel.setForeground(Color.GRAY);

// Copy icon
JLabel copyIcon = createHoverIcon("\uD83D\uDCCB", "Copy session stats");
JLabel copyIcon = createHoverIcon(
new ImageIcon(ImageUtil.loadImageResource(AfkStatsTrackerPanel.class, "copy.png")),
"Copy session stats");
copyIcon.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
long durationMs = session.getEndTime() - session.getStartTime();
long totalSeconds = durationMs / 1000;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;

String lengthStr;
if (hours > 0)
{
lengthStr = String.format("%dh %dm %ds", hours, minutes, seconds);
}
else if (minutes > 0)
{
lengthStr = String.format("%dm %ds", minutes, seconds);
}
else
{
lengthStr = String.format("%ds", seconds);
}
String lengthStr = DurationFormatter.format(session.getEndTime() - session.getStartTime());

String text = String.format(
"Session: %s\nLength: %s\nConsistency: %d\nAvg Interval: %.0fms\nClicks: %d",
Expand All @@ -274,14 +302,24 @@ else if (minutes > 0)
});

// Delete icon
JLabel deleteIcon = createHoverIcon("\uD83D\uDDD1", "Delete session");
JLabel deleteIcon = createHoverIcon(
new ImageIcon(ImageUtil.loadImageResource(AfkStatsTrackerPanel.class, "delete.png")),
"Delete session");
deleteIcon.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
sessionHistoryManager.deleteSession(session.getId());
refreshHistoryPanel();
int choice = JOptionPane.showConfirmDialog(
AfkStatsTrackerPanel.this,
"Delete \"" + session.getName() + "\"?",
"Delete Session",
JOptionPane.YES_NO_OPTION);
if (choice == JOptionPane.YES_OPTION)
{
sessionHistoryManager.deleteSession(session.getId());
refreshHistoryPanel();
}
}
});

Expand All @@ -303,9 +341,9 @@ public void mouseClicked(MouseEvent e)
return row;
}

private JLabel createHoverIcon(String text, String tooltip)
private JLabel createHoverIcon(ImageIcon imageIcon, String tooltip)
{
JLabel icon = new JLabel(text)
JLabel icon = new JLabel(imageIcon)
{
private boolean hovered = false;

Expand Down Expand Up @@ -394,6 +432,8 @@ public void keyPressed(KeyEvent e)

public void updateStats()
{
sessionElapsedLabel.setText(DurationFormatter.format(plugin.getSessionElapsedMs()));
sessionClicksLabel.setText(" · " + plugin.getClickCount() + " clicks");
int consistency = (int) plugin.getConsistency();
consistencyValueLabel.setText(String.valueOf(consistency));
consistencyIndicator.setValue(consistency);
Expand Down
31 changes: 29 additions & 2 deletions src/main/java/com/afkstatstracker/AfkStatsTrackerPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class AfkStatsTrackerPlugin extends Plugin
private NavigationButton navButton;

private long startTime;
private long endTime;
private boolean isTracking = false;

@Inject
Expand Down Expand Up @@ -81,12 +82,13 @@ public void save(String json)
navButton = NavigationButton.builder()
.tooltip("AFK Stats Tracker")
.icon(ImageUtil.loadImageResource(getClass(), "icon.png")) // Need to add icon
.priority(Integer.MAX_VALUE)
.panel(panel)
.build();

clientToolbar.addNavigation(navButton);

mouseListener = new MouseClickCounterListener(client);
mouseListener = new MouseClickCounterListener();
mouseManager.registerMouseListener(mouseListener);

}
Expand All @@ -106,9 +108,15 @@ protected void shutDown() throws Exception

public void startSession()
{
if (isTracking)
{
return;
}

mouseListener.resetMouseClickCounterListener();
startTime = System.currentTimeMillis();
isTracking = true;
mouseManager.registerMouseListener(mouseListener);
}

public void stopSession()
Expand All @@ -118,7 +126,8 @@ public void stopSession()
return;
}

long endTime = System.currentTimeMillis();
mouseManager.unregisterMouseListener(mouseListener);
endTime = System.currentTimeMillis();
String id = UUID.randomUUID().toString();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String name = "Session " + dateFormat.format(new Date(startTime));
Expand All @@ -137,6 +146,24 @@ public void stopSession()
isTracking = false;
}

public boolean isTracking()
{
return isTracking;
}

public long getSessionElapsedMs()
{
if (startTime == 0)
{
return 0;
}
if (isTracking)
{
return System.currentTimeMillis() - startTime;
}
return endTime - startTime;
}

public long getConsistency()
{
return computeConsistency(mouseListener.getClickCounter());
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/afkstatstracker/DurationFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.afkstatstracker;

public final class DurationFormatter
{
private DurationFormatter()
{
}

public static String format(long durationMs)
{
long totalSeconds = Math.max(0, durationMs) / 1000;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;

if (hours > 0)
{
return String.format("%dh %dm %ds", hours, minutes, seconds);
}
if (minutes > 0)
{
return String.format("%dm %ds", minutes, seconds);
}
return String.format("%ds", seconds);
}
}
19 changes: 8 additions & 11 deletions src/main/java/com/afkstatstracker/MouseClickCounterListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import net.runelite.api.Client;
import net.runelite.client.input.MouseAdapter;

public class MouseClickCounterListener extends MouseAdapter
{
private final List<Long> clickTimestamps = new ArrayList<>();
private final Client client;
MouseClickCounterListener(Client client)
{
this.client = client;
}

@Override
public MouseEvent mousePressed(MouseEvent mouseEvent)
Expand All @@ -22,15 +16,18 @@ public MouseEvent mousePressed(MouseEvent mouseEvent)
return mouseEvent;
}

public List<Long> getClickCounter() { return this.clickTimestamps; }
public synchronized List<Long> getClickCounter()
{
return new ArrayList<>(clickTimestamps);
}

public void addClick()
public synchronized void addClick()
{
this.clickTimestamps.add(System.currentTimeMillis());
clickTimestamps.add(System.currentTimeMillis());
}

public void resetMouseClickCounterListener()
public synchronized void resetMouseClickCounterListener()
{
this.clickTimestamps.clear();
}
}
}
Binary file added src/main/resources/com/afkstatstracker/copy.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/resources/com/afkstatstracker/delete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/test/java/com/afkstatstracker/DurationFormatterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.afkstatstracker;

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class DurationFormatterTest
{
@Test
public void testZero()
{
assertEquals("0s", DurationFormatter.format(0));
}

@Test
public void testSubSecondRoundsDown()
{
assertEquals("0s", DurationFormatter.format(999));
}

@Test
public void testSecondsOnly()
{
assertEquals("45s", DurationFormatter.format(45_000));
}

@Test
public void testMinutesAndSeconds()
{
assertEquals("12m 34s", DurationFormatter.format(754_000));
}

@Test
public void testHoursMinutesSeconds()
{
assertEquals("2h 5m 9s", DurationFormatter.format(7_509_000));
}

@Test
public void testNegativeClampsToZero()
{
assertEquals("0s", DurationFormatter.format(-5_000));
}
}
Loading
Loading