Skip to content

Commit

Permalink
Merge pull request #11 from Ceikry/master
Browse files Browse the repository at this point in the history
Plugin system, plugin API
  • Loading branch information
Pazaz authored Sep 25, 2022
2 parents 2bcf5c5 + 2f750e1 commit 9c6ada0
Show file tree
Hide file tree
Showing 51 changed files with 2,104 additions and 377 deletions.
4 changes: 4 additions & 0 deletions client/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'java'
id 'application'
id 'org.jetbrains.kotlin.jvm' version '1.4.10'
}

mainClassName = 'rt4.client'
Expand Down Expand Up @@ -35,11 +36,14 @@ dependencies {
implementation 'lib:jogl-all-natives-linux-i586'
implementation 'lib:jogl-all-natives-macosx-universal'
implementation 'lib:jogl-all-natives-android-aarch64'

runtime 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.10'
}

jar {
manifest {
attributes 'Main-Class': "$mainClassName"
}
from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
3 changes: 2 additions & 1 deletion client/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"server_port": 43594,
"wl_port": 43595,
"js5_port": 43595,
"mouseWheelZoom": true
"mouseWheelZoom": true,
"pluginsFolder": "plugins"
}
98 changes: 98 additions & 0 deletions client/src/main/java/plugin/Plugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package plugin;

import plugin.api.MiniMenuEntry;
import rt4.Component;
import rt4.Npc;
import rt4.Player;
import rt4.Tile;

/**
* The base plugin class which is meant to be extended by plugins.
* Contains callbacks to many parts of the internal client code.
* @author ceikry
*/
public abstract class Plugin {
long timeOfLastDraw;

void _init() {
Init();
}

void _draw() {
long nowTime = System.currentTimeMillis();
Draw(nowTime - timeOfLastDraw);
timeOfLastDraw = nowTime;
}

/**
* Draw() is called by the client rendering loop so that plugins can draw information onto the screen.
* This will be called once per frame, meaning it is framerate bound.
* @param timeDelta the time (ms) elapsed since the last draw call.
*/
public void Draw(long timeDelta) {}

/**
* Init() is called when the plugin is first loaded
*/
public void Init() {}

/**
* OnXPUpdate() is called when the client receives an XP update packet. This includes at login.
* @param skill - the skill ID being updated
* @param xp - the new total XP for the skill.
*/
public void OnXPUpdate(int skill, int xp) {}

/**
* Update() is called once per tick, aka once every 600ms.
*/
public void Update() {}

/**
* PlayerOverheadDraw() is called once per frame, for every player on the screen. :) Expensive.
* @param screenX the X coordinate on the screen for overhead drawing
* @param screenY the Y coordinate on the screen for overhead drawing
*/
public void PlayerOverheadDraw(Player player, int screenX, int screenY) {}

/**
* NPCOverheadDraw() is called once per frame, for every NPC on the screen. :) Expensive.
* @param screenX the X coordinate on the screen for overhead drawing
* @param screenY the Y coordinate on the screen for overhead drawing
*/
public void NPCOverheadDraw(Npc npc, int screenX, int screenY) {}

/**
* ProcessCommand is called when a user types and sends a message prefixed with ::
* @param commandStr the command the user used - should include :: in comparisons, eg <pre>commandStr.equals("::command")</pre>
* @param args any other tokens included with the initial message. Tokens are determined by spaces.
*/
public void ProcessCommand(String commandStr, String[] args) {}

/**
* ComponentDraw is called when an interface component is being rendered by the client.
* @param componentIndex the index of the component in its parent interface.
* @param component the component itself
* @param screenX the screen X coordinate of this component
* @param screenY the screen Y coordinate of this component
*/
public void ComponentDraw(int componentIndex, Component component, int screenX, int screenY) {}

/**
* OnVarpUpdate is called when varps are updated by the server sending packets.
* @param id the ID of the varp
* @param value the value the varp is being set to.
*/
public void OnVarpUpdate(int id, int value) {}

/**
* OnLogout is called when the client logs out. This should be used to clear player-relevant plugin state.
*/
public void OnLogout() {}

/**
* DrawMiniMenu is called when a MiniMenu entry has been created.
* @param entry the entry
*/
public void DrawMiniMenu(MiniMenuEntry entry) {}
}
73 changes: 73 additions & 0 deletions client/src/main/java/plugin/PluginInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package plugin;

import plugin.annotations.PluginMeta;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Objects;
import java.util.Properties;

/**
* A data class for storing information about plugins.
* @author ceikry
*/
class PluginInfo {
double version;
String author;
String description;

public PluginInfo(String author, String description, double version) {
this.version = version;
this.author = author;
this.description = description;
}

public static PluginInfo loadFromFile(File file) {
Properties prop = new Properties();

try {
prop.load(new FileReader(file));
} catch (FileNotFoundException e) {
System.err.println("File does not exist! - " + file.getAbsolutePath());
return new PluginInfo("", "", 0.0);
} catch (IOException e) {
e.printStackTrace();
return new PluginInfo("", "", 0.0);
}

return new PluginInfo(
prop.get("AUTHOR").toString(),
prop.get("DESCRIPTION").toString(),
Double.parseDouble(prop.get("VERSION").toString())
);
}

public static PluginInfo loadFromClass(Class<?> clazz) {
PluginMeta info = clazz.getAnnotation(PluginMeta.class);

if (info == null) {
return null;
}

return new PluginInfo(
info.author(),
info.description(),
info.version()
);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginInfo that = (PluginInfo) o;
return Double.compare(that.version, version) == 0 && Objects.equals(author, that.author) && Objects.equals(description, that.description);
}

@Override
public int hashCode() {
return Objects.hash(version, author, description);
}
}
137 changes: 137 additions & 0 deletions client/src/main/java/plugin/PluginRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package plugin;

import plugin.api.MiniMenuEntry;
import plugin.api.MiniMenuType;
import rt4.*;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* Responsible for loading and broadcasting methods to all plugins.
* @author ceikry
*/
public class PluginRepository {
static HashMap<PluginInfo, Plugin> loadedPlugins = new HashMap<>();

public static void registerPlugin(PluginInfo info, Plugin plugin) {
loadedPlugins.put(info, plugin);
}

public static void reloadPlugins() {
loadedPlugins.clear();
Init();
}

public static void Init() {
File pluginsDirectory = new File(GlobalJsonConfig.instance.pluginsFolder);

if (!pluginsDirectory.exists()) {
System.out.println("Skipping plugin initialization - " + pluginsDirectory.getAbsolutePath() + " does not exist.");
return;
}

try {
URL[] classPath = {pluginsDirectory.toURI().toURL()};
URLClassLoader loader = new URLClassLoader(classPath);

for(File file : Objects.requireNonNull(pluginsDirectory.listFiles())) {
if(!file.isDirectory()) continue;
if(file.getName().equals("META-INF")) continue;

File infoFile = new File(file.getAbsoluteFile() + File.separator + "plugin.properties");
File pluginRoot = new File(file.getAbsoluteFile() + File.separator + "plugin.class");

if (!pluginRoot.exists()) {
System.err.println("Unable to load plugin " + file.getName() + " because plugin.class is absent!");
continue;
}

Class<?> clazz = loader.loadClass(file.getName() + ".plugin");

PluginInfo info;
if (infoFile.exists())
info = PluginInfo.loadFromFile(infoFile);
else
info = PluginInfo.loadFromClass(clazz);

if (info == null) {
System.err.println("Unable to load plugin " + file.getName() + " because it contains no information about author, version, etc!");
continue;
}

try {
Plugin thisPlugin = (Plugin) clazz.newInstance();
thisPlugin._init();
registerPlugin(info, thisPlugin);
} catch (Exception e) {
System.err.println("Error loading plugin " + file.getName() + ":");
e.printStackTrace();
return;
}

List<File> otherClasses = Arrays.stream(Objects.requireNonNull(file.listFiles()))
.filter((f) ->
!f.getName().equals("plugin.class") && f.getName().contains(".class"))
.collect(Collectors.toList());

for (File f : otherClasses) {
loader.loadClass(file.getName() + "." + f.getName().replace(".class",""));
}

System.out.println("Successfully loaded plugin " + file.getName() + ", version " + info.version);
}
} catch (Exception e) {
System.err.println("Unexpected exception during plugin initialization:");
e.printStackTrace();
}
}

public static void Update() {
loadedPlugins.values().forEach(Plugin::Update);
}

public static void Draw() {
loadedPlugins.values().forEach(Plugin::_draw);
}

public static void NPCOverheadDraw(Npc npc, int screenX, int screenY) {
loadedPlugins.values().forEach((plugin) -> plugin.NPCOverheadDraw(npc, screenX, screenY));
}

public static void PlayerOverheadDraw(Player player, int screenX, int screenY) {
loadedPlugins.values().forEach((plugin) -> plugin.PlayerOverheadDraw(player, screenX, screenY));
}

public static void ProcessCommand(JagString commandStr) {
String[] tokens = commandStr.toString().split(" ");
String[] args = Arrays.copyOfRange(tokens, 1, tokens.length);
loadedPlugins.values().forEach((plugin) -> plugin.ProcessCommand(tokens[0], args));
}

public static void ComponentDraw(int componentIndex, Component component, int screenX, int screenY) {
loadedPlugins.values().forEach((plugin) -> plugin.ComponentDraw(componentIndex, component, screenX, screenY));
}

public static void OnVarpUpdate(int id, int value) {
loadedPlugins.values().forEach((plugin) -> plugin.OnVarpUpdate(id, value));
}

public static void OnXPUpdate(int skill, int xp) {
loadedPlugins.values().forEach((plugin) -> plugin.OnXPUpdate(skill, xp));
}

public static void OnLogout() {
loadedPlugins.values().forEach(Plugin::OnLogout);
}

public static void DrawMiniMenu(MiniMenuEntry entry) {
loadedPlugins.values().forEach((plugin) -> plugin.DrawMiniMenu(entry));
}
}
11 changes: 11 additions & 0 deletions client/src/main/java/plugin/annotations/PluginMeta.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package plugin.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface PluginMeta {
String author();
String description();
double version();
}
Loading

0 comments on commit 9c6ada0

Please sign in to comment.