-
Notifications
You must be signed in to change notification settings - Fork 6
Getting Started
The first thing you need to do before initializing any scripts is to set the ScriptAPI Messengers. Messengers is what the API uses to deliver messages to the client. This shouldn't be too complicated, but can be time consuming. By default, all messengers will dump their parameters into the logger, unless set.
ScriptAPI.INSTANCE.setMessengerSay((SayMessenger<User>) (userObject, speakerType, speakerTemplateId, replaceTemplateId, param, message, previous, next) ->
userObject.get().sendPacket(new ScriptSayBuilder(speakerTemplateId).setMessage(message).setParam((byte)param).setPrev(previous).setNext(next))
);
ScriptAPI.INSTANCE.setMessengerAskAccept((AskAcceptMessenger<User>) (userObject, speakerType, speakerTemplateId, param, message) -> {
userObject.get().sendPacket(new ScriptAskAcceptBuilder(speakerTemplateId).setParam((byte)param).setMessage(message));
});
...
ScriptAPI.INSTANCE.setMessengerPlayPortalSE((PlayPortalSEMessenger<User>)userObject -> {
userObject.get().sendPacket(new UserEffectLocalBuilder(UserEffect.PlayPortalSE));
});
Additionally, unless you are using version 92 of the client, you need to set the MessageType conversion. This will convert all ScriptMessageType values to their correct value. This is important for doing cross version scripts. By default, the API treats your version like version 92.
// ScriptMessageType.SAY will now be treated as a 1 by the API
// This is used to check return values, etc.
ScriptAPI.INSTANCE.setScriptMessageType(ScriptMessageType.SAY, 1);
There are many different ways to load the scripts themselves. In production, you will more than likely just use a .jar file and load them normally. In development, you might want the ability to reload as necessary without having to repackage a .jar. We'll be using the later to showcase the potential. The following snippets will load .classs files from a directory(C:\Scripts\).
// Our folder structure looks like this:
// Path: C:\Scripts\
// NpcScripts: C:\Scripts\moe\maple\scripts\npc\
// Notice how I'm using the full package name for the path? This is important.
var urls = path.toUri().toURL();
var clsLoader = new URLClassLoader(new URL[]{urls});
var npcScripts = new HashMap<String, Class<MoeScript>>();
// With.continueException :: Will continue on exception, useful for
// if we reach a bad script or something weird occurs.
With.continueException(getAllClasses("moe.maple.scripts.npc"), name -> {
processPkg(npcScripts, name);
});
and for our processing method:
private void processPkg(Map<String, Class<MoeScript>> map, String name) throws ClassNotFoundException {
var cls = clsLoader.loadClass(name);
var annotation = cls.getAnnotation(Script.class);
if (annotation == null) return;
if (annotation.name().length == 0) throw new IllegalArgumentException("Name isn't set for this script: "+name);
for (String s : annotation.name()) {
map.put(s, (Class<MoeScript>)cls);
}
log.debug("Found new script: {} -- wzName: {} / desc: {}", name, annotation.name(), annotation.description());
}
Great, now our scripts are loaded from C:\Scripts\ and shoved into a HashMap<String, Class<?>>. Lets continue this with a way to get an actual Script we can use.
private Optional<MoeScript> getAndConstruct(Map<String, Class<MoeScript>> map, String key) {
try {
// Do we have the script by that name? If not, default to the NOT_FOUND script.
var cls = map.containsKey(key) ? map.get(key) : map.get(Constants.DEFAULT_NOTFOUND);
if (cls == null)
return Optional.empty();
// This assumes your constructor has no args. This will be the case for most scripts.
// If your script DOES have arguments in the constructor, you will need to add a check here.
var constructor = cls.getConstructor();
var script = (MoeScript)constructor.newInstance();
// If this isn't the script we were looking for(because we defaulted to a 404 script)
// Then set the expected name value, this will allow the script to display a messages
// about which script was missing.
if (script.name().equalsIgnoreCase(Constants.DEFAULT_NOTFOUND))
script.setExpected(key);
return Optional.of(script);
} catch (Exception e) {
log.error("Oh no, I couldn't load the script class: {}", key, e);
}
return Optional.empty();
}
@Override
public Optional<MoeScript> getScript(Npc npc) {
var name = npc.getScript(); // NPCs are loaded directly from .WZ with their script value
var ret = getAndConstruct(npcScripts, name); // Lets construct the script from the WZName
ret.ifPresent(script -> { // Great, we got a script! :D
// Great, we have our script. Now we need to set all the proxy objects this script will need.
// This isn't shown, but later on the UserProxy will be set. If you forget to set
// the user proxy, bad things will happen.
script.setNpcObject(ScriptTool.getNpcObject(script, npc));
script.setFieldObject(ScriptTool.getFieldObject(script, npc.getField()));
});
return ret;
}
If you noticed above, I use a utility class called ScriptTool to create the proxy objects for the classes. You don't necessarily have to do this, as you can just re-use the proxy object of a class.