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
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:
Now lets write the actual automation:
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:
- We have a while loop that runs as long as terminate attribute is not set
- Every loop we write "Starting Loop!" into System chat so that we can identify loop running
- Then we have another loop going through all configured fireplaces, so next steps are executed for each fireplace
- Check availability of "Blocks" in the inventory and if we have less than three execute getfuel() method - we will add it later
- We define fuelticks to be 50 - since two blocks make it up for 100 in fuel meter
- Then we execute pfRightClick which invokes path finding to firm move to the object and then right-click it to open flower menu
- We wait for path finding thread to complete
- We check if we are to terminate
- 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
- Search for window titled "Fireplace" and find a child element of that window of VMeter.class
- 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
- If we have enough fuel - switch to next Fireplace
- Take fuel - wood-block from inventory and wait till you get fuel in hand
- Once fuel is in hand - place it into Fireplace
- If fuel is left in hand - create a stockpile
- Sleep for SLEEP amount of seconds
- 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.
This is how it should look like in game:
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
- 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")));
- 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); } } }
- Modify haven.MapView.canceltasks() method to accommodate for autofire functionality
if (autofire != null) autofire.terminate();
- 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 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