Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Please explain in reasonable detail what your series of changes does, and how it does it.
Make sure to link any relevant github issues which instigated your changes.

you can safely leave delete this comment as you fill out this text box.
you can safely leave or delete this comment as you fill out this text box.
Please leave the Summary and Testing sub titles.
-->

Expand All @@ -14,6 +14,6 @@ Please leave the Summary and Testing sub titles.
DO NOT leave this section empty, please add any relevant testing evidence (in game, unit tests, etc)
that show that your changes achieve the desired effect.

you can safely leave delete this comment as you fill out this text box.
you can safely leave or delete this comment as you fill out this text box.
Please leave the Summary and Testing sub titles.
-->
2 changes: 1 addition & 1 deletion src/main/java/com/attacktimer/AnimationData.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public enum AnimationData
MAGIC_ANCIENT_MULTI_TARGET_PVP(1979, AttackStyle.MAGIC, Spellbook.ANCIENT), // Burst & Barrage animations (tested all 8, different weapons)
MAGIC_ANCIENT_SINGLE_TARGET(10091, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations (tested all 8, different weapons)
MAGIC_ANCIENT_SINGLE_TARGET_PVP(1978, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations

MAGIC_ARCEUUS_DEMONBANE(8977, AttackStyle.MAGIC, Spellbook.ARCEUUS), // Also greater corruption, so that may accidentally trigger a manual-cast, but that's probably fine only affects Muspah
MAGIC_ARCEUUS_GRASP(8972, AttackStyle.MAGIC, Spellbook.ARCEUUS),

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/attacktimer/AttackStyle.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ public enum AttackStyle
this.name = name;
this.skills = skills;
}

@Override
public String toString()
{
return this.name;
}
}
112 changes: 106 additions & 6 deletions src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import com.attacktimer.ClientUtils.Utils;
import com.attacktimer.VariableSpeed.VariableSpeed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteArrayDataOutput;
import com.google.inject.Provides;
import java.awt.Color;
import java.awt.Dimension;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
Expand All @@ -45,18 +48,23 @@
import net.runelite.api.Actor;
import net.runelite.api.Client;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.NPC;
import net.runelite.api.Player;
import net.runelite.api.Skill;
import net.runelite.api.Varbits;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.FakeXpDrop;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.InteractingChanged;
import net.runelite.api.events.SoundEffectPlayed;
import net.runelite.api.events.StatChanged;
import net.runelite.api.events.VarClientIntChanged;
import net.runelite.api.events.VarbitChanged;
import net.runelite.api.gameval.InventoryID;
import net.runelite.api.gameval.VarPlayerID;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.events.ConfigChanged;
Expand Down Expand Up @@ -106,6 +114,9 @@ public enum AttackState
@Inject
private NPCManager npcManager;

@Inject
private ClientThread clientThread;

public int tickPeriod = 0;

private int uiHideDebounceTickCount = 0;
Expand All @@ -126,6 +137,14 @@ public enum AttackState

public int pendingEatDelayTicks = 0;

private ArrayDeque<Integer> specialPercentageEvents = new ArrayDeque<Integer>();
private Map<Skill, ArrayDeque<Integer>> combatExpEarned = Map.of(
Skill.MAGIC, new ArrayDeque<Integer>(),
Skill.RANGED, new ArrayDeque<Integer>(),
Skill.DEFENCE, new ArrayDeque<Integer>(),
Skill.STRENGTH, new ArrayDeque<Integer>(),
Skill.ATTACK, new ArrayDeque<Integer>()
);

private static final int UI_HIDE_DEBOUNCE_TICKS_MAX = 1;
private static final int ATTACK_DELAY_NONE = 0;
Expand Down Expand Up @@ -188,6 +207,10 @@ public void onVarbitChanged(VarbitChanged varbitChanged)
{
currentSpellBook = Spellbook.fromVarbit(varbitChanged.getValue());
}
if (varbitChanged.getVarpId() == VarPlayerID.SA_ENERGY)
{
specialPercentageEvents.addLast(varbitChanged.getValue());
}
}

// onVarbitChanged happens when the user causes some interaction therefore we can't rely on some fixed
Expand Down Expand Up @@ -227,6 +250,34 @@ public void onSoundEffectPlayed(SoundEffectPlayed event)
soundEffectId = event.getSoundId();
}

@Subscribe
protected void onFakeXpDrop(FakeXpDrop event)
{
if (!combatExpEarned.containsKey(event.getSkill()))
{
return;
}
combatExpEarned.get(event.getSkill()).addLast(event.getXp());
if (attackState == AttackState.DELAYED_FIRST_TICK)
{
performAttack();
}
}

@Subscribe
protected void onStatChanged(StatChanged event)
{
if (!combatExpEarned.containsKey(event.getSkill()))
{
return;
}
combatExpEarned.get(event.getSkill()).addLast(event.getXp());
if (attackState == AttackState.DELAYED_FIRST_TICK)
{
performAttack();
}
}

// endregion

@Provides
Expand All @@ -235,6 +286,37 @@ AttackTimerMetronomeConfig provideConfig(ConfigManager configManager)
return configManager.getConfig(AttackTimerMetronomeConfig.class);
}

private int computeDamage(AttackStyle attackStyle, AttackProcedure atkType, AnimationData curAnimation)
{
switch (atkType)
{
case POWERED_STAVE:
// TODO not needed for any variable speed
return -1;
case MANUAL_AUTO_CAST:
if (attackStyle == AttackStyle.DEFENSIVE_CASTING || attackStyle == AttackStyle.DEFENSIVE)
{
// just use the defense exp to compute the damage
return Utils.getLastDelta(combatExpEarned.get(Skill.DEFENCE));
}
else
{
// deduct the fixed exp based on the spell
// (for now this only works for dark demon bane which awkwardly gives fractional exp)
var mageExp = Utils.getLastDelta(combatExpEarned.get(Skill.MAGIC));
if (curAnimation != AnimationData.MAGIC_ARCEUUS_DEMONBANE)
{
return -1;
}
return (int) Math.ceil(((double) mageExp - 43.5D) / 2.0D);
}
case MELEE_OR_RANGE:
// TODO not needed for any variable speed
return -1;
}
return -1;
}

private int getItemIdFromContainer(ItemContainer container, int slotID)
{
if (container == null)
Expand All @@ -248,7 +330,7 @@ private int getItemIdFromContainer(ItemContainer container, int slotID)
private int getWeaponId()
{
int weaponId = getItemIdFromContainer(
client.getItemContainer(InventoryID.EQUIPMENT),
client.getItemContainer(InventoryID.WORN),
EquipmentInventorySlot.WEAPON.getSlotIdx()
);

Expand Down Expand Up @@ -302,27 +384,32 @@ private int getMagicBaseSpeed(int weaponId)

private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curAnimation, boolean matchesSpellbook)
{
var specDelta = Utils.getLastDelta(specialPercentageEvents);
int damageDealt = -1;
if (stave != null && stave.getAnimations().contains(curAnimation))
{
damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.POWERED_STAVE, curAnimation);
// We are currently dealing with a staves in which case we can make decisions based on the
// spellbook flag. We can only improve this by using a deprecated API to check the projectile
// matches the stave rather than a manual spell, but this is good enough for now.
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.POWERED_STAVE, 4);
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.POWERED_STAVE, damageDealt, specDelta, 4);
}

if (matchesSpellbook && isManualCasting(curAnimation))
{
damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.MANUAL_AUTO_CAST, curAnimation);
// You can cast with anything equipped in which case we shouldn't look to invent for speed.
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, getMagicBaseSpeed(weaponId));
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, damageDealt, specDelta,getMagicBaseSpeed(weaponId));
}

damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.MELEE_OR_RANGE, curAnimation);
ItemStats weaponStats = getWeaponStats(weaponId);
if (weaponStats == null)
{
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, 4); // Assume barehanded == 4t
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, damageDealt, specDelta, 4); // Assume barehanded == 4t
}
// Deadline for next available attack.
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, weaponStats.getEquipment().getAspeed());
return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, damageDealt, specDelta, weaponStats.getEquipment().getAspeed());
}

private static final List<Integer> SPECIAL_NPCS = Arrays.asList(10507, 9435, 9438, 9441, 9444); // Combat Dummy + Nightmare Pillars
Expand Down Expand Up @@ -438,6 +525,7 @@ public void onChatMessage(ChatMessage event)
// We should always add eat delay
pendingEatDelayTicks += attackDelay;
}
VariableSpeed.onChatMessage(event);
}

// onInteractingChanged is the driver for detecting if the player attacked out side the usual tick window
Expand Down Expand Up @@ -523,6 +611,17 @@ public void onGameTick(GameTick tick)
// clamp the attackDelayHoldoffTicks at -20, this is so we correctly account for eats even when not
// attacking, but don't count down forever.
attackDelayHoldoffTicks = Math.max(-20, attackDelayHoldoffTicks - 1);
if (specialPercentageEvents.size() > 5)
{
specialPercentageEvents.removeFirst();
}
for (var q : combatExpEarned.values())
{
if (q.size() > 5)
{
q.removeFirst();
}
}
}


Expand Down Expand Up @@ -551,6 +650,7 @@ protected void shutDown() throws Exception
attackDelayHoldoffTicks = 0;
}

@VisibleForTesting
public void writeState(ByteArrayDataOutput outChannel)
{
StringBuilder sb = new StringBuilder();
Expand Down
61 changes: 54 additions & 7 deletions src/main/java/com/attacktimer/ClientUtils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,20 @@
import com.attacktimer.AttackStyle;
import com.attacktimer.AttackType;
import com.attacktimer.WeaponType;
import java.util.ArrayDeque;
import net.runelite.api.Actor;
import net.runelite.api.Client;
import net.runelite.api.EquipmentInventorySlot;
import net.runelite.api.InventoryID;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.NPC;
import net.runelite.api.VarPlayer;
import net.runelite.api.Varbits;
import net.runelite.api.WorldView;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.gameval.InventoryID;
import net.runelite.api.gameval.VarPlayerID;
import net.runelite.api.gameval.VarbitID;
import org.apache.commons.lang3.ArrayUtils;

public class Utils
{
Expand All @@ -55,7 +58,7 @@ public static int getItemIdFromContainer(ItemContainer container, int slotID)

public static int getWeaponId(Client client)
{
return getItemIdFromContainer(client.getItemContainer(InventoryID.EQUIPMENT),
return getItemIdFromContainer(client.getItemContainer(InventoryID.WORN),
EquipmentInventorySlot.WEAPON.getSlotIdx());
}

Expand All @@ -72,9 +75,17 @@ public static WorldPoint getLocation(Client client)
public static AttackStyle getAttackStyle(Client client)
{
final AttackStyle[] attackStyles = getWeaponType(client).getAttackStyles(client);
final int currentAttackStyleVarbit = client.getVarpValue(VarPlayer.ATTACK_STYLE);
int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE);
final int castingMode = client.getVarbitValue(VarbitID.AUTOCAST_DEFMODE);
if (currentAttackStyleVarbit < attackStyles.length)
{
// from script4525
// Even though the client has 5 attack styles for Staffs, only attack styles 0-4 are used, with an additional
// casting mode set for defensive casting
if (currentAttackStyleVarbit == 4)
{
currentAttackStyleVarbit += castingMode;
}
return attackStyles[currentAttackStyleVarbit];
}

Expand All @@ -84,15 +95,15 @@ public static AttackStyle getAttackStyle(Client client)
// returns null for unknown weapons
public static WeaponType getWeaponType(Client client)
{
final int currentEquippedWeaponTypeVarbit = client.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE);
final int currentEquippedWeaponTypeVarbit = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY);
return WeaponType.getWeaponType(currentEquippedWeaponTypeVarbit);
}

// returns null for unknown weapons
public static AttackType getAttackType(Client client)
{
final WeaponType weaponType = getWeaponType(client);
final int currentAttackStyleVarbit = client.getVarpValue(VarPlayer.ATTACK_STYLE);
final int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE);
if (currentAttackStyleVarbit < weaponType.getAttackTypes().length)
{
return weaponType.getAttackTypes()[currentAttackStyleVarbit];
Expand Down Expand Up @@ -124,4 +135,40 @@ public static NPC getTargetNPC(Client client)
return null;
}

// TODO comment
public static boolean isInRegionId(Client client, int id)
{
WorldView wv = client.getTopLevelWorldView();
if (wv == null)
{
return false;
}

int[] regions = wv.getMapRegions();
if (regions == null || regions.length == 0)
{
return false;
}

return ArrayUtils.contains(regions, id);
}

// TODO comment
public static int getLastDelta(ArrayDeque<Integer> events)
{
int i = 0, last = -1, secondLast = -1;
var it = events.descendingIterator();
while (it.hasNext())
{
if (i == 0)
last = it.next();
else if (i == 1)
secondLast = it.next();
else
break;
i++;
}
var delta = last - secondLast;
return delta;
}
}
Loading
Loading