Educating OSBot, one rant at a time
EDIT: This tutorial is meant for people who already have some grasp on how to write a script. If you are completely new to scripting, this tutorial is not for you.
The node method of making scripts is definitely by far one of the best methods out there, but it has so many flaws that make for bad programming practices. Imagine you're making a pickpocket script, you might have the following nodes:
EatNode
WalkToBankNode
WalkFromBankNode
BankingNode
PickpocketNode
Now, given how most node-based scripts work, it simply does a for loop on each node and runs the first node in the list that can be executed. So if we enter our nodes in like this
PickpocketNode, BankingNode, WalkToBankNode, WalkFromBankNode, EatNode (EatNode being last, PickpocketNode being first)
we have actually just have a big issue: eating is the lowest priority. The idea of nodes is to keep the logic for one thing self-contained within another, but if we enter in our EatNode last, we will need to check to make sure our health is high enough in PickpocketNode (to ensure we don't, you know, die). This means you either ship along some global statics (bad!!!!), a script settings object (good!!!) or just forget about it.
Now, what if I could tell you that we can prioritise our nodes based on what happened last? Now we can say have our eat node a very high priority after we've walked to the bank (maybe we're in DMM and have 1 food left but low hp) and very high priority after we've pickpocketed something (because we may have just taken damage), but fairly low priority otherwise. We can constantly give our pickpocket node a high priority, and run our walk from bank node immediately after we've banked.
This does a few things for us:
We don't have static priority - this is great because we as humans don't have static priority for things either!
We no longer rely on the order we put nodes in to our list, we only care about when they should be ran
Now, the node system I propose isn't perfect, but it's a damn sight better and provides us a lot more legroom for upgrading in the future. Also, this will get you a lot more comfortable with some of Java's more advanced features, namely annotations which make every high level programmer cum immediately.
The Goal: Make a flexible node system that has dynamic priority
The Result: By the end of this, you will have a working node system with two example nodes (ImmediateNode and DefaultNode) which will show you how flexible the system is.
Lazy Kids: Leave now. All code here has been screenshotted so you can't copy/paste it. Learn something or GTFO
Step 1: How the Fuck Will We Do This?
We need to decide how we're going to store things, and how things will be written in code.
For this project, we need a manager of some sort that handles the sorting of nodes, we need something to handle priority, and we need to determine how we will denote prioritisation.
For this, we will create a NodeManager class, a Priority enum, and we will cover prioritisation in step 2.
What the fuck is this? This enumerated type is less of an enumerated type and more of a... well.. class. However, this "class" only has a set number of values! This means that we can only specify LOWEST, LOW, DEFAULT, HIGH, HIGHEST, IMMEDIATELY. If you want to be an absolute madman, you can add additional priorities in this file.
Well, you lost me. I understand NONE of this. This is actually really simple! This class stores references to a list of NodeObjects (come to that later), our last executed node, as well as a "default" or empty node (which we will return when we can't execute anything else to avoid nasty nulls), and finally a Comparator. A comparator simply compares two objects. In this case, we want to compare the priority of two different nodes.
We will also create an interface called Node.
This looks nothing like my Node class! That's because this is an interface. In programming, an interface is meant to represent the barebones object (in simpler terms, an interface is a blueprint for an object). Read the big documentation comment at the top of it if you want to sneak in a Script instance.
Step 2: Decide How to Manage Prioritisation
Now, we need to create a way to determine the priority of our nodes. There are two real ways to do this:
A second parameter in our NodeManager#addNode(Node node) method, which would have every dynamic priority attached. This could actually get very messy, so I'm not even going to explain it better.
We can attach annotations to each Node class we write, which keeps the logic contained and doesn't clog up our methods with useless garbage.
Step 2.1: What's an Annotation?
In Java, we have these nice little things called "annotations", and they make programming in Java a whole lot nicer. Annotations are effectively little nuggets of code that annotate our methods, variables and classes. They're great because they bridge the gap between human-readable code & complicated data structures. We've actually come across annotations before, at least if you know what a ScriptManifest is.
We're going to use annotations here and we're going to love it.
Step 3: Write Annotations
We're going to need two annotations here: a bog standard PrioritisedNode annotation, and a Condition annotation (I'm calling mine "After" in this implementation)
Hey, I kinda get this! It's very simple, but is also very, very powerful.
Holy fuck, this is similar! That's because it is.
Step 4: Write a Node!
We're going to write an "ImmediateNode". This node will have an IMMEDIATE priority, but after it is executed it will have a LOW priority (so that something else can execute!).
Oh fuck, you lost me. Now, this is a normal node (written just like you'd normally write a node), except we've plugged in our annotations that we just made. The first annotation (PrioritisedNode), says that by default we have an IMMEDIATE priority. The second annotation (After) says that after we execute ImmediateNode (this node), its priority is set to LOW.
Step 5: Write another Node!
Oh, fuck! We can use this to write other nodes, too! We're now going to write a "DefaultNode". This node will always have the DEFAULT priority.
Bitch, did you just gender your code? Damn straight I did. Notice how we omit the (priority = <something>) part in our annotation here? This is because we're using the default value we set earlier! We also set no After annotation, which means it will never have a different priority.
Step 6: What do we do now???
Well now that we've complicated things, we're going to need to update our NodeManager class. We need to add a few more things:
A NodeObject class, this will allow us to track these annotations and make them in to something a little more computable
A getNextNode method, which will get the next possible node to execute.
We'll start with the getNextNode method (within NodeManager).
Why do we sort? Why do we loop? WHY? We call Collections#sort(List, Comparator) to sort the list based on our previous condition - the comparator we made earlier! This doesn't return anything, but instead modifies the list we pass through. We iterate because we also need to find nodes that we CAN execute, otherwise we may just be executing garbage we can't do. We also set our lastNode variable provided we find a node, so that we can properly calculate our priorities next run.
Step 7: Objectify the Nodes
We're going to create something called an inner class - this is a class that is special to another class, kinda like you are to your parents. We're going to insert an extra class statement at the very bottom of NodeManager (but not outside the last bracket!) - this keeps things cleaner, especially because we don't want to access this class outside the manager.
?????????????????? This is a bit more complicated to explain, so we're going to ignore the constructor. Instead, we'll focus on getPriority(NodeManager) - if the last node doesn't exist (it's null), we return our default priority. Otherwise, we return the priority given by our After annotation, but if that doesn't exist we return our default priority.
Step 8: ???
Step 9: Profit!
We have now created our node system! We can use it like so:
And, hopefully, after all this hard work, we'll get this output in our logger box:
Notice how, although we added DefaultNode first, it ran ImmediateNode first? And furthermore, even though ImmediateNode has a priority of IMMEDIATE, it doesn't get ran that second time? That's because of the After condition we put in that!
Conclusion
Nodes are great, but they aren't perfect. So I made them perfect. Use this in all kinds of scripts, and claim you wrote the code yourself. Be proud of yourself, you just actually read a tutorial in its entirety.
Exercises (ie things I was too lazy to type up)
Java does not allow two annotations of the same type to exist on a single object. This means one node can only have one priority change - how can we make one node have many priority changes? (Hint: make a third annotation whose only value is an array of After annotations, and iterate through them in NodeManager)
Can you expand the code to do more things than just changing priority after one node executes?