Merge pull request #634 from f0rmatme/dpstracker

Added performance stats aka dps counter
This commit is contained in:
Tyler Bochard
2019-06-17 18:26:40 -04:00
committed by GitHub
5 changed files with 728 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019, TheStonedTurtle <https://github.com/TheStonedTurtle>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.performancestats;
import lombok.Getter;
import lombok.Setter;
import net.runelite.http.api.ws.messages.party.PartyMemberMessage;
@Getter
class Performance extends PartyMemberMessage
{
private static final double TICK_LENGTH = 0.6;
@Setter
String username;
double damageDealt = 0;
double highestHitDealt = 0;
double damageTaken = 0;
double highestHitTaken = 0;
int lastActivityTick = -1;
@Setter
double ticksSpent = 0;
void addDamageDealt(double a, int currentTick)
{
damageDealt += a;
if (a > highestHitDealt)
{
highestHitDealt = a;
}
this.lastActivityTick = currentTick;
}
void addDamageTaken(double a, int currentTick)
{
damageTaken += a;
if (a > highestHitTaken)
{
highestHitTaken = a;
}
this.lastActivityTick = currentTick;
}
void incrementTicksSpent()
{
ticksSpent++;
}
void reset()
{
damageDealt = 0;
highestHitDealt = 0;
damageTaken = 0;
highestHitTaken = 0;
lastActivityTick = -1;
ticksSpent = 0;
}
double getSecondsSpent()
{
return Math.round(this.ticksSpent * TICK_LENGTH);
}
double getDPS()
{
return Math.round( (this.damageDealt / this.getSecondsSpent()) * 100) / 100.00;
}
String getHumanReadableSecondsSpent()
{
final double secondsSpent = getSecondsSpent();
if (secondsSpent <= 60)
{
return String.format("%2.0f", secondsSpent) + "s";
}
final double s = secondsSpent % 3600 % 60;
final double m = Math.floor(secondsSpent % 3600 / 60);
final double h = Math.floor(secondsSpent / 3600);
return h < 1 ? String.format("%2.0f:%02.0f", m, s) : String.format("%2.0f:%02.0f:%02.0f", h, m, s);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2019, TheStonedTurtle <https://github.com/TheStonedTurtle>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.performancestats;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
@ConfigGroup("performancestats")
public interface PerformanceStatsConfig extends Config
{
@ConfigItem(
position = 0,
keyName = "submitTimeout",
name = "Submit Timeout (seconds)",
description = "Submits after this many seconds of inactivity"
)
default int submitTimeout()
{
return 30;
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (c) 2018, TheStonedTurtle <https://github.com/TheStonedTurtle>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.performancestats;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import javax.inject.Inject;
import net.runelite.api.MenuAction;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayMenuEntry;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.OverlayPriority;
import net.runelite.client.ui.overlay.components.ComponentConstants;
import net.runelite.client.ui.overlay.components.PanelComponent;
import net.runelite.client.ui.overlay.components.table.TableAlignment;
import net.runelite.client.ui.overlay.components.table.TableComponent;
public class PerformanceStatsOverlay extends Overlay
{
private static final String TARGET = "Performance Stats";
private static final String[] COLUMNS = {
"Player", "Dealt", "Taken", "DPS", "Elapsed"
};
private final PerformanceStatsPlugin tracker;
private final PanelComponent panelComponent = new PanelComponent();
private final TableComponent tableComponent = new TableComponent();
@Inject
PerformanceStatsOverlay(PerformanceStatsPlugin tracker)
{
super(tracker);
setPosition(OverlayPosition.TOP_RIGHT);
setPriority(OverlayPriority.LOW);
this.tracker = tracker;
getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Pause", TARGET));
getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Reset", TARGET));
getMenuEntries().add(new OverlayMenuEntry(MenuAction.RUNELITE_OVERLAY, "Submit", TARGET));
panelComponent.setPreferredSize(new Dimension(350, 0));
panelComponent.setBackgroundColor(ComponentConstants.STANDARD_BACKGROUND_COLOR);
tableComponent.setDefaultAlignment(TableAlignment.CENTER);
tableComponent.setColumns(COLUMNS);
panelComponent.getChildren().add(tableComponent);
}
@Override
public String getName()
{
return TARGET;
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!tracker.isEnabled())
{
return null;
}
final Performance performance = tracker.getPerformance();
graphics.setColor(Color.WHITE);
tableComponent.getRows().clear();
final String[] rowElements = createRowElements(performance);
tableComponent.addRow(rowElements);
for (Performance p : tracker.getPartyDataMap().values())
{
if (p.getMemberId().equals(performance.getMemberId()))
{
continue;
}
final String[] eles = createRowElements(p);
tableComponent.addRow(eles);
}
return panelComponent.render(graphics);
}
private String[] createRowElements(Performance performance)
{
return new String[]
{
performance.getUsername(),
String.valueOf((int) Math.round(performance.getDamageDealt())) + " | " + String.valueOf((int) Math.round(performance.getHighestHitDealt())),
String.valueOf((int) Math.round(performance.getDamageTaken())) + " | " + String.valueOf((int) Math.round(performance.getHighestHitTaken())),
String.valueOf(performance.getDPS()),
performance.getHumanReadableSecondsSpent()
};
}
}

View File

@@ -0,0 +1,452 @@
/*
* Copyright (c) 2018, TheStonedTurtle <https://github.com/TheStonedTurtle>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.client.plugins.performancestats;
import com.google.inject.Provides;
import java.text.DecimalFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Actor;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.NPC;
import net.runelite.api.Skill;
import net.runelite.api.WorldType;
import net.runelite.api.events.ExperienceChanged;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.HitsplatApplied;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.client.chat.ChatColorType;
import net.runelite.client.chat.ChatMessageBuilder;
import net.runelite.client.chat.ChatMessageManager;
import net.runelite.client.chat.QueuedMessage;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.OverlayMenuClicked;
import net.runelite.client.events.PartyChanged;
import net.runelite.client.game.NPCManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.PluginType;
import net.runelite.client.ui.overlay.OverlayManager;
import net.runelite.client.util.Text;
import net.runelite.client.ws.PartyMember;
import net.runelite.client.ws.PartyService;
import net.runelite.client.ws.WSClient;
import net.runelite.http.api.ws.messages.party.UserPart;
import net.runelite.http.api.ws.messages.party.UserSync;
@PluginDescriptor(
name = "Performance Stats",
description = "Displays your current performance stats",
tags = {"performance", "stats", "dps", "damage", "combat"},
type = PluginType.UTILITY,
enabledByDefault = false
)
@Slf4j
public class PerformanceStatsPlugin extends Plugin
{
// For every damage point dealt 1.33 experience is given to the player's hitpoints (base rate)
private static final double HITPOINT_RATIO = 1.33;
private static final double DMM_MULTIPLIER_RATIO = 10;
private static final double GAME_TICK_SECONDS = 0.6;
private static final DecimalFormat numberFormat = new DecimalFormat("#,###");
@Inject
private Client client;
@Inject
private ChatMessageManager chatMessageManager;
@Inject
private PerformanceStatsConfig config;
@Inject
private PerformanceStatsOverlay performanceTrackerOverlay;
@Inject
private OverlayManager overlayManager;
@Inject
private NPCManager npcManager;
@Inject
private PartyService partyService;
@Inject
private WSClient wsClient;
@Getter
private boolean enabled = false;
@Getter
private boolean paused = false;
@Getter
private final Performance performance = new Performance();
// Keep track of actor last tick as sometimes getInteracting can return null when hp xp event is triggered
// as the player clicked away at the perfect time
private Actor oldTarget;
private double hpExp;
private boolean hopping;
private int pausedTicks = 0;
// Party System
@Getter
private final Map<UUID, Performance> partyDataMap = Collections.synchronizedMap(new HashMap<>());
@Provides
PerformanceStatsConfig getConfig(ConfigManager configManager)
{
return configManager.getConfig(PerformanceStatsConfig.class);
}
@Override
protected void startUp()
{
overlayManager.add(performanceTrackerOverlay);
wsClient.registerMessage(Performance.class);
}
@Override
protected void shutDown()
{
overlayManager.remove(performanceTrackerOverlay);
wsClient.unregisterMessage(Performance.class);
disable();
reset();
}
@Subscribe
public void onGameStateChanged(GameStateChanged event)
{
switch (event.getGameState())
{
case LOGIN_SCREEN:
disable();
break;
case HOPPING:
hopping = true;
break;
}
}
@Subscribe
public void onHitsplatApplied(HitsplatApplied e)
{
if (isPaused())
{
return;
}
if (e.getActor().equals(client.getLocalPlayer()))
{
// Auto enables when hitsplat is applied to player
if (!isEnabled())
{
enable();
}
performance.addDamageTaken(e.getHitsplat().getAmount(), client.getTickCount());
}
}
@Subscribe
public void onExperienceChanged(ExperienceChanged c)
{
if (isPaused() || hopping)
{
return;
}
if (c.getSkill().equals(Skill.HITPOINTS))
{
final double oldExp = hpExp;
hpExp = client.getSkillExperience(Skill.HITPOINTS);
// Ignore initial login
if (client.getTickCount() < 2)
{
return;
}
final double diff = hpExp - oldExp;
if (diff < 1)
{
return;
}
// Auto enables when player receives hp exp
if (!isEnabled())
{
enable();
}
final double damageDealt = calculateDamageDealt(diff);
performance.addDamageDealt(damageDealt, client.getTickCount());
}
}
@Subscribe
public void onScriptCallbackEvent(ScriptCallbackEvent e)
{
// Handles Fake XP drops (Ironman in PvP, DMM Cap, 200m xp, etc)
if (isPaused())
{
return;
}
if (!"fakeXpDrop".equals(e.getEventName()))
{
return;
}
final int[] intStack = client.getIntStack();
final int intStackSize = client.getIntStackSize();
final int skillId = intStack[intStackSize - 2];
final Skill skill = Skill.values()[skillId];
if (skill.equals(Skill.HITPOINTS))
{
// Auto enables when player would have received hp exp
if (!isEnabled())
{
enable();
}
final int exp = intStack[intStackSize - 1];
performance.addDamageDealt(calculateDamageDealt(exp), client.getTickCount());
}
}
@Subscribe
public void onGameTick(GameTick t)
{
oldTarget = client.getLocalPlayer().getInteracting();
if (!isEnabled())
{
return;
}
if (isPaused())
{
pausedTicks++;
return;
}
performance.incrementTicksSpent();
hopping = false;
final int timeout = config.submitTimeout();
if (timeout > 0)
{
final double tickTimeout = timeout / GAME_TICK_SECONDS;
final int activityDiff = (client.getTickCount() - pausedTicks) - performance.getLastActivityTick();
if (activityDiff > tickTimeout)
{
// offset the tracker time to account for idle timeout
// Leave an additional tick to pad elapsed time
final double offset = tickTimeout - GAME_TICK_SECONDS;
performance.setTicksSpent(performance.getTicksSpent() - offset);
submit();
}
}
final String name = client.getLocalPlayer().getName();
performance.setUsername(Text.removeTags(name));
sendPerformance();
}
@Subscribe
public void onOverlayMenuClicked(OverlayMenuClicked c)
{
if (!c.getOverlay().equals(performanceTrackerOverlay))
{
return;
}
switch (c.getEntry().getOption())
{
case "Pause":
togglePaused();
break;
case "Reset":
reset();
break;
case "Submit":
submit();
break;
}
}
private void enable()
{
this.enabled = true;
hpExp = client.getSkillExperience(Skill.HITPOINTS);
}
private void disable()
{
this.enabled = false;
}
private void togglePaused()
{
this.paused = !this.paused;
}
private void reset()
{
this.enabled = false;
this.paused = false;
this.performance.reset();
pausedTicks = 0;
}
private void submit()
{
final String message = createPerformanceMessage(performance);
chatMessageManager.queue(QueuedMessage.builder()
.type(ChatMessageType.GAMEMESSAGE)
.runeLiteFormattedMessage(message)
.build());
reset();
}
/**
* Calculates damage dealt based on HP xp gained accounting for multipliers such as DMM mode
* @param diff HP xp gained
* @return damage dealt
*/
private double calculateDamageDealt(double diff)
{
double damageDealt = diff / HITPOINT_RATIO;
// DeadMan mode has an XP modifier
if (client.getWorldType().contains(WorldType.DEADMAN))
{
damageDealt = damageDealt / DMM_MULTIPLIER_RATIO;
}
// Some NPCs have an XP modifier, account for it here.
Actor a = client.getLocalPlayer().getInteracting();
if (!(a instanceof NPC))
{
// If we are interacting with nothing we may have clicked away at the perfect time fall back to last tick
if (!(oldTarget instanceof NPC))
{
log.warn("Couldn't find current or past target for experienced gain...");
return damageDealt;
}
a = oldTarget;
}
final int npcId = ((NPC) a).getId();
return damageDealt / npcManager.getXpModifier(npcId);
}
private String createPerformanceMessage(final Performance p)
{
// Expected result: Damage Dealt: ## (Max: ##), Damage Taken: ## (Max: ##), Time Spent: ##:## (DPS: ##.##)
return new ChatMessageBuilder()
.append(ChatColorType.NORMAL)
.append("Damage dealt: ")
.append(ChatColorType.HIGHLIGHT)
.append(numberFormat.format(p.getDamageDealt()))
.append(ChatColorType.NORMAL)
.append(" (Max: ")
.append(ChatColorType.HIGHLIGHT)
.append(numberFormat.format(p.getHighestHitDealt()))
.append(ChatColorType.NORMAL)
.append("), Damage Taken: ")
.append(ChatColorType.HIGHLIGHT)
.append(numberFormat.format(p.getDamageTaken()))
.append(ChatColorType.NORMAL)
.append(" (Max: ")
.append(ChatColorType.HIGHLIGHT)
.append(numberFormat.format(p.getHighestHitTaken()))
.append(ChatColorType.NORMAL)
.append("), Time Spent: ")
.append(ChatColorType.HIGHLIGHT)
.append(p.getHumanReadableSecondsSpent())
.append(ChatColorType.NORMAL)
.append(" (DPS: ")
.append(ChatColorType.HIGHLIGHT)
.append(String.valueOf(p.getDPS()))
.append(ChatColorType.NORMAL)
.append(")")
.build();
}
private void sendPerformance()
{
final PartyMember me = partyService.getLocalMember();
if (me != null && me.getMemberId() != null)
{
performance.setMemberId(me.getMemberId());
wsClient.send(performance);
}
}
@Subscribe
public void onPerformance(final Performance performance)
{
partyDataMap.put(performance.getMemberId(), performance);
}
@Subscribe
public void onUserSync(final UserSync event)
{
if (isEnabled())
{
sendPerformance();
}
}
@Subscribe
public void onUserPart(final UserPart event)
{
partyDataMap.remove(event.getMemberId());
}
@Subscribe
public void onPartyChanged(final PartyChanged event)
{
// Reset party
partyDataMap.clear();
}
}

View File

@@ -7,6 +7,8 @@
iload 1
sconst "fakeXpDrop"
runelite_callback ;
pop_int
pop_int
iconst 105
iconst 83
iconst 681