Ink Actions
Ink actions are classes bound to an Ink tag keyword (# keyword:arg1:arg2). When that keyword appears in a script, the action is instantiated and executed.
Creating an Ink action
Extend InkAction and annotate it with @InkCommand:
@InkCommand(
keyword = "teleport",
description = "Teleports the player",
syntax = "teleport <x:float> <y:float> <z:float>",
side = Side.SERVER
)
public class TeleportAction extends InkAction {
private float x, y, z;
@Override
protected InkActionResult doValidate(ParsedCommand command, IScene scene) {
x = command.getFloat("x");
y = command.getFloat("y");
z = command.getFloat("z");
return InkActionResult.ok();
}
@Override
protected InkActionResult doExecute(IPlayerSession session) {
session.getPlayer().teleportTo(x, y, z);
return InkActionResult.ok();
}
}doValidate runs first and receives the parsed arguments. Read and store everything you need there, because doExecute only receives the player session.
Reading arguments
Use the typed accessors on ParsedCommand:
command.getString("name") // String
command.getInt("count") // int
command.getFloat("duration") // float
command.getBoolean("loop") // boolean
command.flag("block") // boolean, true if the flag was present in the tagSee Syntax for how to declare argument types in @InkCommand.
Client-side Ink actions
Server actions run on the server, client actions run on the client. The split is done through inheritance rather than duplication.
Start with a base class where doExecute returns InkActionResult.ignored(), this is what the server sees:
@InkCommand(
keyword = "shake",
syntax = "shake <amplitude:float>",
side = Side.CLIENT
)
public class ShakeAction extends InkAction {
protected float amplitude;
@Override
protected InkActionResult doValidate(ParsedCommand command, IScene scene) {
amplitude = command.getFloat("amplitude");
return InkActionResult.ok();
}
@Override
protected InkActionResult doExecute(IPlayerSession session) {
return InkActionResult.ignored();
}
}Then create a Client-prefixed subclass that overrides doExecute with the actual client logic:
public class ClientShakeAction extends ShakeAction {
@Override
protected InkActionResult doExecute(IPlayerSession session) {
// client-side screen shake logic
return InkActionResult.ok();
}
}Registering
Register both classes through your AddonContext:
ctx.registerInkAction(ShakeAction.class, ShakeAction::new);
ctx.registerInkAction(ClientShakeAction.class, ClientShakeAction::new);Multi-tick actions
If your action runs over multiple ticks, override the relevant lifecycle methods:
@Override
public void tick() {
// called every server tick while isRunning
}
@Override
public void partialTick(float partialTick) {
// called every frame on the client
}
@Override
public void render(GuiGraphicsExtractor graphics, float partialTick) {
// client-side rendering
}
@Override
public void stop() {
// cleanup when the action ends
}Set blocking = true in doValidate if the action should pause the Ink script until it finishes.
InkActionResult
Returned by both doValidate and doExecute:
InkActionResult.ok() // success
InkActionResult.ignored() // this side doesn't handle it
InkActionResult.block() // pauses the action queue
InkActionResult.error("msg") // validation or execution failure
InkActionResult.warn("msg") // non-fatal warningInkActionUtil
// converts a time value (e.g. 2.5, "minute") to seconds
double seconds = InkActionUtil.getSecondsFromTimeValue(2.5, "minute");
// replaces %variable% with their Ink story value
String resolved = InkActionUtil.parseVariables(story, "Hello %player_name%!");