Creating a Diablo key presser with Java

Share

Anyone still playing Diablo 3 and eagerly awaiting Diablo 4 will be aware how simple the gameplay of some classes is in the current state. In order to survive the highest greater rifts a lot of classes rely on a defensive ability that they have to press every so often to stay alive, and quite often characters will focus on collecting cooldown reduction stats to ensure the ability is re-castable at a shorter interval than its duration to maintain a 100% uptime. Skills like these begin to feel taxing just pressing the same button at a fixed duration, and that’s just the kind of problem that you normally try to automate!

Disclaimer: Blizzard and most game companies do not allow you to run 3rd party applications that aid or facilitate game play in an online setting so if you wish to try this, do so at your own risk. A lot of this can be automated already by taking advantage of numlock on the pc which has been approved by Blizzard. (Yes that button has a function other than turning on a light on your keyboard.) Also no, I’m not here to do this all for you so there will not be a link to my complete code, but you should be able to piece together enough to make your own from the provided code snippets.

Ok, so where to begin? I think the most basic functionality would be for our code to be able to simulate keys being pressed. To achieve this we can make use of Java’s Robot class.

private static Robot getRobot() {
  try {
    return new Robot();
  } catch (AWTException e) {
    e.printStackTrace();
  }
  return null;
}

Ok, so now we have a robot – how do we make it push my buttons? The robot class has a keyPress method which will simulate you pushing down on a key, and similarly a keyRelease method which will simulate you lifting up your finger again. Now, obviously if you were to use both methods one after the other it’s possible that the key press might be too fast for an online game to register and no human can press a key quite that fast. Therefore we can utilise the robot.delay method to specify the number of milliseconds you want in between. (note: the maximum delay you can specify is 60,000 aka 1 minute.)

The delay is created by using a Thread.sleep so we don’t want it to be run as part of our main application thread otherwise we won’t be able to do anything while holding the key. To get around this, we will wrap our key-pressing robot in a Runnable and use a ThreadPoolExecutor to execute the code.

    public static long minDuration = 35;
    public static long maxDuration = 500;
    private static int threads = 10;
    private static int threads = 10;
    private static ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threads);

    public static void press(int key) {
        press(key,randomLong(minDuration, maxDuration));
    }

    public static void press(int key, long delay) {
        Runnable runnable = () -> {
            Robot robot = getRobot();
            robot.keyPress(key);
            robot.delay(Math.toIntExact(delay));
            robot.keyRelease(key);
        };

        executor.execute(runnable);
    }

    static long randomLong(long min, long max){
        return min + (long) (Math.random() * (max - min));
    }

Here we can see I’ve also overridden the press method so what we have the choice of stipulating the key-press delay, or just randomising between a min and max duration. The randomisation is mainly useful for better simulating human activity should that be something that concerns you.

The next step is calling our key press code. To do this I created a method for each ability following a similar pattern;

    private ScheduledExecutorService executorService;

    private void vengeance(double cdr, int key) {
        long cd = 90000;
        long actual = (long) (cd * (1 - cdr) * (1 - 0.65));
        executorService.scheduleAtFixedRate(() -> KeyPress.press(key), 0, actual + KeyPress.maxDuration, TimeUnit.MILLISECONDS);
    }

    private void fanOfKnives(double cdr, int key) {
        long cd = 10000;
        long actual = (long) (cd * (1 - cdr));
        executorService.scheduleAtFixedRate(() -> KeyPress.press(key), 0, actual + KeyPress.maxDuration, TimeUnit.MILLISECONDS);
    }

    private void preparation(double cdr, int key) {
        long cd = 45000;
        long actual = (long) (cd * (1 - cdr));
        executorService.scheduleAtFixedRate(() -> KeyPress.press(key), 0, actual + KeyPress.maxDuration, TimeUnit.MILLISECONDS);
    }

These methods take in the key you want to press and your current cooldown reduction as a value between 0 and 1. (e.g. if your character sheet says you have 48.13% cooldown reduction you would use 0.4813). I then use the base cooldown of the ability to work out the actual cooldown after applying the cooldown reduction. We then use a ScheduledExecutorService to fire our abilities at a fixed rate, with a delay of zero and using the actual cooldown plus the length of time that we might have held down the key last time as the rate.

To tie it all together we can just initialise our ScheduledExecutorService and call the abilities we want to use.

    public void runDemonHunter() {
            executorService = Executors.newScheduledThreadPool(10);
            System.out.println("DH Started");
            double cdr = 0.4813;

            //button 2
            vengeance(cdr, KeyEvent.VK_2);

            //button 3
            fanOfKnives(cdr, KeyEvent.VK_3);

            //button 4
            preparation(cdr, KeyEvent.VK_4);
    }

That’s pretty much the basics of it, but there’s plenty of room for improvement! For example, tabbing out and running your application is a pain, but your skill activation interrupting your teleport to town is infuriating; so why not add the ability to start and stop your application. This is going to get a little more complicated.

The easiest way I found to implement a key listener was to use a library called jnativehook.

<dependency>
   <groupId>com.1stleg</groupId>
   <artifactId>jnativehook</artifactId>
   <version>2.1.0</version>
</dependency>

I personally like using maven to manage my dependencies but you can also just download the jar and add it as a dependent jar to your project.

With that done you will want to create a class to implement the NativeKeyListener class. I will show you what I did and talk it through a bit below.

import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class KeyListener implements NativeKeyListener {
    private Launcher launcher;
    private Set<Set<Integer>> triggers;
    private Set<Integer> pressed;
    private Instant triggered;

    public KeyListener(Launcher launcher){
        this.launcher = launcher;
        pressed = Collections.synchronizedSet(new HashSet<>());
        triggers = new HashSet<>();
        triggers.add(setOf(NativeKeyEvent.VC_F2));
        triggers.add(setOf(NativeKeyEvent.VC_F3));
        triggers.add(setOf(NativeKeyEvent.VC_F4));
        triggered = Instant.now();
    }
    public void nativeKeyTyped(NativeKeyEvent e) {

    }

    public void nativeKeyPressed(NativeKeyEvent e) {
        int key = e.getKeyCode();
        pressed.add(key);
        System.out.println(NativeKeyEvent.getKeyText(key) + ", " + pressed.toString());
        if (isTrigger() && isTriggerable()){
            trigger();
        }
    }

    public void nativeKeyReleased(NativeKeyEvent e) {
        pressed.remove(e.getKeyCode());
    }

    private boolean isTrigger(){
        for(Set<Integer> trigger : triggers){
            if(pressed.containsAll(trigger)){
                return true;
            }
        }
        return false;
    }

    private void trigger(){
        triggered = Instant.now();
        launcher.executeCommand(Set.copyOf(pressed));
    }

    private boolean isTriggerable(){
        return Instant.now().isAfter(triggered.plus(1, ChronoUnit.SECONDS));
    }

    private Set<Integer> setOf(int ... keys){
        Set<Integer> s = new HashSet<>();
        for(int key : keys){
            s.add(key);
        }
        return s;
    }
}

As part of my constructor you can see that I am passing in my Launcher class. This is so that the listener can trigger events in my application when the appropriate keys are pushed.

Triggers are the keys that I am specifically looking for. It might seem strange that I decided to represent them as a Set of Sets of keys, but this was to cater for the option of having key combinations work, like ctrl + 1 to execute one option and ctrl + 2 to do another but as you can see I ended up just using F2 – F4 in the end.

When implementing the NativeKeyListener there are 3 methods that are implemented; nativeKeyTyped, nativeKeyPressed and nativeKeyReleased. For my purposes I was only interested in the latter 2. Every time a key is pressed I add it to my pressed Set and remove it once released, so I should at all times know the state of what is pressed.

When a key is pressed I check if the pressed keys matches any of my triggers, and then I also have a isTriggerable time based gate to prevent firing the same command multiple times within a second. This is of course entirely optional but if you want a trigger to toggle an action repeat firing can be frustrating.

I think I’ve rambled on for long enough, so any questions feel free to post below!

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *