Thursday 2 February 2017

Setting things on Fire! ...in an automatic way

One of the benefits of having access to Haven and Hearth client source code is ability to automate certain routine tasks - to make them less routine or more efficient or both. So why don't we try automating something?

Task Definition

We have Fireplaces and Stockpiles - Fireplaces burn, Stockpiles store fuel (wood blocks) for our Fireplaces. We want to make sure that we have a couple of wood-blocks in inventory (no less than 3), we go to Fireplaces one by one, click-Open them, check what is the fuel level and if it is less than 50 - half, then we drop another wood block into the Fireplace. Relax for half an hour - repeat.

Implementation

We'll base our implementation on code that already exists (SteelRefueler.java) and will modify it for our use-case. So lets create a new class under "haven.automation" extending haven.Window and implementing haven.automation.GobSelectCallback
public class AutoFire extends Window implements GobSelectCallback {
 private static final Text.Foundry infof = new Text.Foundry(Text.sans, 10).aa(true);
 private static final Text.Foundry countf = new Text.Foundry(Text.sans.deriveFont(Font.BOLD), 12).aa(true);
 private List<Gob> fires = new ArrayList<>();
 private List<Gob> stockpiles = new ArrayList<>();
 private final Label lblf, lbls;
 public boolean terminate = false;
 private Button clearbtn, runbtn, stopbtn;
 private static final int TIMEOUT = 2000;
 private static final int HAND_DELAY = 8;
 private static final int SLEEP = 30 * 60 * 1000; // 30 min
 private Thread runner;
Lets have a look at what we do here:

  • infof - foundry for information labels
  • countf - foundry for counter labels
  • fires - array of graphic objects representing fireplaces
  • stockpiles - array of graphic objects representing stockpiles
  • lblf, lbls - labels to show number's of fireplaces and stockpiles configured
  • terminate - a terminate flag
  • clearbton, runbtn, stopbtn - objects for "Clear", "Run" and "Stop" buttons
  • TIMEOUT - two seconds of delay between actions that need to complete before the next action
  • HAND_DELAY - eight milliseconds of delay between attempts to use item in hand
  • SLEEP - delay between loops
  • runner - background thread running our automation
Now lets write a constructor for our class:
 public AutoFire() {
  super(new Coord(270, 180), "Auto Fire");

  final Label lbl = new Label("Alt + Click to select fireplaces and stockpiles.", infof);
  add(lbl, new Coord(30, 20));

  Label lblctxt = new Label("Fires Selected:", infof);
  add(lblctxt, new Coord(15, 60));
  lblf = new Label("0", countf, true);
  add(lblf, new Coord(110, 58));

  Label lblstxt = new Label("Stockpiles Selected:", infof);
  add(lblstxt, new Coord(135, 60));
  lbls = new Label("0", countf, true);
  add(lbls, new Coord(235, 58));

  clearbtn = new Button(140, "Clear Selection") {
   @Override
   public void click() {
    fires.clear();
    stockpiles.clear();
    lblf.settext(fires.size() + "");
    lbls.settext(stockpiles.size() + "");
   }
  };
  add(clearbtn, new Coord(65, 90));

  runbtn = new Button(140, "Run") {
   @Override
   public void click() {
    if (fires.size() == 0) {
     gameui().error("No fires selected.");
     return;
    } else if (stockpiles.size() == 0) {
     gameui().error("No stockpiles selected.");
     return;
    }

    this.hide();
    cbtn.hide();
    clearbtn.hide();
    stopbtn.show();
    terminate = false;

    runner = new Thread(new Runner(), "Auto Fire");
    runner.start();
   }
  };
  add(runbtn, new Coord(65, 140));

  stopbtn = new Button(140, "Stop") {
   @Override
   public void click() {
    terminate = true;
    // TODO: terminate PF
    this.hide();
    runbtn.show();
    clearbtn.show();
    cbtn.show();
   }
  };
  stopbtn.hide();
  add(stopbtn, new Coord(65, 140));
 }

So what is happening here? We create our window, call super() constructor setting it's coordinates and title to "Auto Fire". Then we create labels with respective content and place them around the window. Once that is done - we add our "Clear Selection", "Run" and "Stop" buttons placing them in the window and hiding the "Stop" button.

Each button has click() method overriden with functionality we want it to expose:

  • "Clear Selection" button would empty fires and stockpiles arrays and update text content of labels with appropriate numbers corresponding to new sizes of said array
  • "Run" button will trigger a check for fires and stockpiles arrays sizes and will present an error in case either of two is empty, then it will hide other buttons and show the "Stop" one and create a thread running automation logic
  • "Stop" button sets termnination attribute, hides itself and shows other buttons
As one can see constructor is pretty simple and revolves around setting up the window, defining labels and buttons and associating functionality with button clicks.


Now lets write the actual automation:
 private class Runner implements Runnable {

  @Override
  public void run() {
   GameUI gui = gameui();
   while (!terminate) {
    gui.syslog.append("Starting Loop!",Color.CYAN);
    floop: for (Gob f : fires) {
     // take fuel from stockpiles if we don't have enough in the
     // inventory
     int availableFuelBlock = gui.maininv.getItemPartialCount("Block");
     if (availableFuelBlock < 3)
      getfuel();

     // find one piece of fuel in the inventory
     WItem fuel = gui.maininv.getItemPartial("Block");
     if (fuel == null)
      continue;

     int fuelticks = 50; // it takes two blocks to fill the fire to 100

     // navigate to fire
     gui.map.pfRightClick(f, -1, 3, 0, null);
     try {
      gui.map.pfthread.join();
     } catch (InterruptedException e) {
      return;
     }

     if (terminate)
      return;
     
     try {
          Thread.sleep(TIMEOUT);
     } catch (InterruptedException e) {
          return;
     }
     
     Window cwnd = gui.getwnd("Fireplace");
     if (cwnd == null)
      continue;
     VMeter vm = cwnd.getchild(VMeter.class);
     if (vm == null)
      continue;

     gui.syslog.append("VMeter: "+vm.amount+" > "+(100-fuelticks),Color.CYAN);
     if (vm.amount > (100 - fuelticks))
      continue;

     int fueltoload = (100 - vm.amount) / fuelticks;
     
     // take fuel
     fuel.item.wdgmsg("take", new Coord(fuel.item.sz.x / 2, fuel.item.sz.y / 2));
     int timeout = 0;
     while (gui.hand.isEmpty()) {
      timeout += HAND_DELAY;
      if (timeout >= TIMEOUT)
       continue floop;
      try {
       Thread.sleep(HAND_DELAY);
      } catch (InterruptedException e) {
       return;
      }
     }
     gui.syslog.append("Hand is not empty!", Color.CYAN);
     for (; fueltoload > 0; fueltoload--) {
      if (terminate)
       return;
      while(!gui.hand.isEmpty()){
      gui.map.wdgmsg("itemact", Coord.z, f.rc.floor(posres), fueltoload == 1 ? 0 : 1, 0, (int) f.id,
        f.rc.floor(posres), 0, -1);
      try {
       Thread.sleep(1000);
      } catch (InterruptedException e) {
       return;
      }
      }
      timeout = 0;
      while (true) {
       WItem newfuel = gui.vhand;
       if (newfuel != fuel) {
        fuel = newfuel;
        break;
       }

       timeout += HAND_DELAY;
       if (timeout >= TIMEOUT)
        break;
       try {
        Thread.sleep(HAND_DELAY);
       } catch (InterruptedException e) {
        return;
       }
      }
     }

     WItem hand = gui.vhand;
     // if the fireplace is full already we'll end up with a
     // stockpile on the cursor
     if (hand != null) {
      gui.map.wdgmsg("place", Coord.z, 0, 3, 0);
      gui.map.wdgmsg("drop", hand.c.add(Inventory.sqsz.div(2)).div(Inventory.invsq.sz()));
     }
    }

   try {
    Thread.sleep(SLEEP);
   } catch (InterruptedException e) {
    return;
   }

   }

  }
 }
As one can see class implements Runnable, the same one we used to implement AutoKin in one of the previous posts (https://winter-in-hnh.blogspot.ru/2017/01/extending-haven-and-hearth-client.html) and as previously we create a run() method that actually implements what we want - lets have a look at what code does:

  1. We have a while loop that runs as long as terminate attribute is not set
  2. Every loop we write "Starting Loop!" into System chat so that we can identify loop running
  3. Then we have another loop going through all configured fireplaces, so next steps are executed for each fireplace
  4. Check availability of "Blocks" in the inventory and if we have less than three execute getfuel() method - we will add it later
  5. We define fuelticks to be 50 - since two blocks make it up for 100 in fuel meter
  6. Then we execute pfRightClick which invokes path finding to firm move to the object and then right-click it to open flower menu
  7. We wait for path finding thread to complete
  8. We check if we are to terminate
  9. We wait for TIMEOUT - while we wait another thread is going to react to opened flower menu selecting 'Open' - we will handle that later separately
  10. Search for window titled "Fireplace" and find a child element of that window of VMeter.class
  11. It's now good to drop a diagnostic message showing how much fuel we have in the Fireplace vs when we have to feed some fuel to it
  12. If we have enough fuel - switch to next Fireplace
  13. Take fuel - wood-block from inventory and wait till you get fuel in hand
  14. Once fuel is in hand - place it into Fireplace
  15. If fuel is left in hand - create a stockpile
  16. Sleep for SLEEP amount of seconds
  17. Repeat
So this implements our automation, what is left are various support functions, lets have a look at what they do
 private void getfuel() {
  GameUI gui = gameui();
  Glob glob = gui.map.glob;
  for (Gob s : stockpiles) {
   if (terminate)
    return;

   // make sure stockpile still exists
   synchronized (glob.oc) {
    if (glob.oc.getgob(s.id) == null)
     continue;
   }

   // navigate to the stockpile and load up on fuel
   gameui().map.pfRightClick(s, -1, 3, 1, null);
   try {
    gui.map.pfthread.join();
   } catch (InterruptedException e) {
    return;
   }

   // return if got enough fuel
   int availableFuelBlock = gui.maininv.getItemPartialCount("Block");
   if (availableFuelBlock >= 3)
    return;
  }
 }
Here we loop through all configured stockpiles right-clicking them to get whatever is stored in them, and once we have more fuel than expected (more than three) we complete the loop.
Next method is pretty important since that is what implements GobSelectCallback and allows to configure our fireplaces and stockpiles.
 public void gobselect(Gob gob) {
  Resource res = gob.getres();
  if (res != null) {
   if (res.name.equals("gfx/terobjs/pow")) {
    if (!fires.contains(gob)) {
     fires.add(gob);
     lblf.settext(fires.size() + "");
    }
   } else if (res.name.equals("gfx/terobjs/stockpile-wblock")) {
    if (!stockpiles.contains(gob)) {
     stockpiles.add(gob);
     lbls.settext(stockpiles.size() + "");
    }
   }
  }
 }
This method is called when you Alt+Click an object, what it does it checks object type by it's name and if it is a Fireplace or wood-block Stockpile object gets remembered in one of two arrays. Next two functions handle window closure or typeing while window is active:
 @Override
 public void wdgmsg(Widget sender, String msg, Object... args) {
  if (sender == cbtn)
   reqdestroy();
  else
   super.wdgmsg(sender, msg, args);
 }

 @Override
 public boolean type(char key, KeyEvent ev) {
  if (key == 27) {
   if (cbtn.visible)
    reqdestroy();
   return true;
  }
  return super.type(key, ev);
 }
Finally we have terminate method handling termination of automation activity
 public void terminate() {
  terminate = true;
  if (runner != null)
   runner.interrupt();
  this.destroy();
 }

Support and integration

Now we have all the functionality implemented and it's time to integrate it into the client, we also need a couple support things to make things happen. Since we are going to add a menu item button for our automation we will need to create a resource like described in one of the previous posts - https://winter-in-hnh.blogspot.ru/2017/01/lets-add-buttons.html
  1. Create a resource icon "res\paginae\amber\autofire" that would trigger "autofeedfire" action and register it in MenuGrid::attach() method
    p.add(paginafor(Resource.local().load("paginae/amber/autofire")));
    
  2. Register "autofeedfire" action in MenuGrid::use() method
            } else if (ad[1].equals("autofeedfire")){
             if (gui.getwnd("Auto Fire") == null) {
                    AutoFire sw = new AutoFire();
                    gui.map.autofire = sw;
                    gui.add(sw, new Coord(gui.sz.x / 2 - sw.sz.x / 2, gui.sz.y / 2 - sw.sz.y / 2 - 200));
                    synchronized (GobSelectCallback.class) {
                        gui.map.registerGobSelect(sw);
                    }
                }
            }
    
  3. Modify haven.MapView.canceltasks() method to  accommodate for autofire functionality
    if (autofire != null)
                autofire.terminate();
    
  4. Last thing to take care of would be to teach the client to automatically select 'Open' from flower menu when right-clicking the fireplace, this would require several changes:
    In Config.java we need to add autoopen paramater:
    public static boolean autoshear = Utils.getprefb("autoshear", false);
    public static boolean autoopen = Utils.getprefb("autoopen", false);
    

    In FloweMenu.java we need to ensure that if autoopen parameter is set it should be automatically picked:
    Config.autoshear && p.name.equals("Shear wool") ||
    Config.autoopen && p.name.equals("Open") ||
    p.name.equals(nextAutoSel) && System.currentTimeMillis() - nextAutoSelTimeout < 2000)) {
This should get everything in place - make sure to have RES file properly placed, rebuild the project and you should be all set.
This is how it should look like in game:

Done for today

So today we automated a task of feeding fireplaces, we used SteelRefueler as a template, added another button to the menu to trigger our functionality. Enjoy!

No comments:

Post a Comment