Write RuneScape 3 plugins in Java or Kotlin, similar to the way you would write a RuneLite plugin, and get readable Lua for the RS3 Client Plugin API.
Named after the Oneiromancer.
Status: early development. Plugins written in Java or Kotlin compile to Lua and build into the folder and zip that go to Mod.io; what each language supports is in java-subset.md and kotlin-subset.md. Versions will remain unstable until the RS3 API reaches stability at 1.0.0. We will not update this to 1.0.0 until that happens at a minimum.
RS3 client plugins are Lua 5.4 scripts, which Jagex's moderators review before they are published on Mod.io. Oneiro lets you write them in Java or Kotlin, with the tools you know: Gradle, your IDE, type checking and completion for the whole RS3 API. It compiles your code into Lua that reads as if written by hand, one module per file, with comments carried over, Lua Language Server annotations, and direct calls to the RS3 API. There is no runtime library. Only the Lua is submitted and moderated.
The source keeps the shape of a RuneLite plugin, a Plugin class with @Subscribe functions and a settings interface, and calls the RS3 API by the names it documents. In Java:
package com.example.coins;
import static org.adelheid.oneiro.rs3.Globals.log;
import org.adelheid.oneiro.api.Plugin;
import org.adelheid.oneiro.api.Subscribe;
import org.adelheid.oneiro.rs3.Inventory;
import org.adelheid.oneiro.rs3.InventoryUpdatedEventPayload;
import org.adelheid.oneiro.rs3.config;
import org.adelheid.oneiro.rs3.id;
/** Logs the coins in your backpack whenever the amount changes. */
public final class CoinWatch extends Plugin {
private CoinSettings settings;
private int shown = -1;
@Subscribe
public void onInventoryUpdated(InventoryUpdatedEventPayload event) {
// The bank and other inventories send updates too
if (event.inv.id != id.Inv.RS3_INV || !settings.enabled()) {
return;
}
int coins = Inventory.Find(config.Inv.RS3_INV).GetTotal(config.Obj.RS3_COINS);
if (coins != shown) {
shown = coins;
log("Coins in the backpack: " + coins);
}
}
}In Kotlin:
package com.example.coins
import org.adelheid.oneiro.api.Plugin
import org.adelheid.oneiro.api.Subscribe
import org.adelheid.oneiro.rs3.Globals.log
import org.adelheid.oneiro.rs3.Inventory
import org.adelheid.oneiro.rs3.InventoryUpdatedEventPayload
import org.adelheid.oneiro.rs3.config
import org.adelheid.oneiro.rs3.id
/** Logs the coins in your backpack whenever the amount changes. */
class CoinWatch : Plugin() {
private lateinit var settings: CoinSettings
private var shown = -1
@Subscribe
fun onInventoryUpdated(event: InventoryUpdatedEventPayload) {
// The bank and other inventories send updates too
if (event.inv.id != id.Inv.RS3_INV || !settings.enabled()) {
return
}
val coins = Inventory.Find(config.Inv.RS3_INV).GetTotal(config.Obj.RS3_COINS)
if (coins != shown) {
shown = coins
log("Coins in the backpack: $coins")
}
}
}Either becomes this main.lua:
-- main.lua
-- Logs the coins in your backpack whenever the amount changes.
local CoinSettings = require("src/coin_settings")
local shown = -1
local function onInventoryUpdated(event)
-- The bank and other inventories send updates too
if event.inv.id ~= id.Inv.INV or not CoinSettings.enabled() then
return
end
local coins = Inventory.Find(config.Inv.INV):GetTotal(config.Obj.COINS)
if coins ~= shown then
shown = coins
log(formatStr("Coins in the backpack: {}", coins))
end
end
Event.InventoryUpdated.Subscribe("coin-watch.onInventoryUpdated", onInventoryUpdated)
-- The client calls PluginShutdown when the plugin is disabled, and requires it to be global.
function PluginShutdown()
Event.InventoryUpdated.Unsubscribe("coin-watch.onInventoryUpdated")
endThe Gradle plugin also writes plugin.json from the build, checks the folder that ships, runs the plugin's Lua tests,
zips it for upload, and links it into the beta client's dev_plugins folder. A construct Oneiro cannot
translate, or a call that needs a permission the build does not declare, is a compile error that says what to do
instead. A call that Jagex's moderators review closely, such as a change to the right-click menu, compiles with a
warning that names what they look at.
You need:
- Gradle 9; Oneiro is tested with 9.8.2.
- JDK 25, which Oneiro compiles with. Gradle finds it when it is installed, whichever Java Gradle itself runs on.
- For a plugin written in Kotlin: the Kotlin Gradle plugin 2.3.0 or later.
- To try the plugin in game: the RS3 beta client.
In a new folder named coin-watch, create the Gradle wrapper with gradle wrapper --gradle-version 9.8.2, then
build.gradle.kts:
plugins {
// For a plugin written in Kotlin, add kotlin("jvm") version "2.4.21"
id("org.adelheid.oneiro") version "0.4.0"
}
version = "1.0.0"
repositories {
mavenCentral()
}
oneiro {
stubs {
// The RS3 API's type information, from which Oneiro generates the RS3 bindings on your machine
remote("https://github.com/adelheidsoftware/rs3-api-types", "9f151d069fec71348ec19d7f87722d8ff7178e9d")
}
plugin {
name = "Coin Watch"
contactEmail = "you@example.org"
}
}The folder's name is the project's name, and so the plugin's identifier: the name of its zip and of its link in the
client. The build needs no settings.gradle.kts, except on a machine without the JDK that Oneiro compiles with,
where this one lets Gradle download it:
plugins {
// Lets Gradle download the JDK that Oneiro compiles with
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}Put CoinWatch.java from above in src/main/java/com/example/coins/, or CoinWatch.kt in
src/main/kotlin/com/example/coins/, and its settings interface next to it. As CoinSettings.java:
package com.example.coins;
import org.adelheid.oneiro.api.ConfigGroup;
import org.adelheid.oneiro.api.ConfigItem;
/** Coin Watch's settings. */
@ConfigGroup("coinwatch")
public interface CoinSettings {
@ConfigItem(keyName = "enabled", name = "Log the coins")
default boolean enabled() {
return true;
}
}As CoinSettings.kt:
package com.example.coins
import org.adelheid.oneiro.api.ConfigGroup
import org.adelheid.oneiro.api.ConfigItem
/** Coin Watch's settings. */
@ConfigGroup("coinwatch")
interface CoinSettings {
@ConfigItem(keyName = "enabled", name = "Log the coins")
fun enabled(): Boolean = true
}A plugin is written in one language; the build fails when it has both Java and Kotlin sources. A plugin written in
Kotlin also needs the Kotlin Analysis API, which JetBrains publishes only to its own repositories. In a build like this
one, which declares its repositories in build.gradle.kts, Oneiro's Gradle plugin adds those repositories itself.
Only a build that declares its repositories in settings.gradle.kts instead, under dependencyResolutionManagement,
adds the JetBrains ones there, as
the plugin model shows.
Then build the plugin and link it into the client:
./gradlew build # compiles, checks, tests and zips the plugin
./gradlew linkDevPlugin # links it into the client's dev_plugins folder, onceThe zip for Mod.io is build/distributions/coin-watch-1.0.0.zip. On Windows, linkDevPlugin finds the beta client's
dev_plugins folder on its own; elsewhere, name it in the build:
oneiro { devPlugins = file("/path/to/dev_plugins") }.
In the client's console, prf finds the newly linked plugin and pon coin-watch enables it. After each build,
prl coin-watch reloads it. When the console shows an error from the plugin, copy it into a file and run
./gradlew explainError --input=console.txt: it shows the lines of your source that the error came from, and what the
client's own messages mean.
- From RuneLite to RS3: what a RuneLite plugin's events, getters, settings, overlays, notifiers and timers become, and how the rules differ between the games.
- The plugin model and the build: the project layout, the
oneiro { }block, the tasks and the checks. - Using Lua: library plugins, vendored libraries and Lua of your own, where their types come from, and how to write and share types for a library that has none.
- Supported Java and supported Kotlin: every construct Oneiro accepts, the Lua it becomes, and what it rejects.
- Architecture: how Oneiro is put together, for contributors.
- The RS3 Client Plugin API documentation and Jagex's plugin guidelines.
Jagex reviews every plugin before it is published and expects its developer to understand all of its code. Oneiro's Lua is written to be read: read it before you submit.
./gradlew buildTo build a plugin against a checkout of Oneiro instead of a release, for example to try a change, include the checkout
in the plugin's settings.gradle.kts and apply the plugin without a version, as
CONTRIBUTING.md describes.
Contributions are welcome; CONTRIBUTING.md explains how to build, test and add to the supported languages. Changes are listed in CHANGELOG.md.
MIT, see LICENSE.
Oneiro is not affiliated with or endorsed by Jagex or RuneLite. RuneScape is a trademark of Jagex Ltd.