-
-
Notifications
You must be signed in to change notification settings - Fork 368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Module System #4573
Add Module System #4573
Changes from 2 commits
adaa688
0328f2b
0e44435
78b61ca
d2f75b7
72e5797
9a51f9f
3767632
7305653
d4e01a4
2a3f6d0
803eeb9
c08c4a6
3038c51
10973c4
d6206e2
4ac2d16
2712285
66de7cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -498,14 +498,9 @@ public void onEnable() { | |
new DefaultFunctions(); | ||
|
||
ChatMessages.registerListeners(); | ||
|
||
try { | ||
getAddonInstance().loadClasses("ch.njol.skript", "conditions", "effects", "events", "expressions", "entity", "sections"); | ||
} catch (final Exception e) { | ||
exception(e, "Could not load required .class files: " + e.getLocalizedMessage()); | ||
setEnabled(false); | ||
return; | ||
} | ||
|
||
getAddonInstance().loadClasses("ch.njol.skript", "conditions", "effects", "events", "expressions", "entity", "sections") | ||
.loadModules("org.skriptlang.skript"); | ||
|
||
Commands.registerListeners(); | ||
|
||
|
@@ -518,42 +513,23 @@ public void onEnable() { | |
@Override | ||
public void run() { | ||
assert Bukkit.getWorlds().get(0).getFullTime() == tick; | ||
|
||
// Load hooks from Skript jar | ||
try { | ||
try (JarFile jar = new JarFile(getFile())) { | ||
for (final JarEntry e : new EnumerationIterable<>(jar.entries())) { | ||
if (e.getName().startsWith("ch/njol/skript/hooks/") && e.getName().endsWith("Hook.class") && StringUtils.count("" + e.getName(), '/') <= 5) { | ||
final String c = e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length()); | ||
try { | ||
final Class<?> hook = Class.forName(c, true, getClassLoader()); | ||
if (hook != null && Hook.class.isAssignableFrom(hook) && !hook.isInterface() && Hook.class != hook && isHookEnabled((Class<? extends Hook<?>>) hook)) { | ||
hook.getDeclaredConstructor().setAccessible(true); | ||
hook.getDeclaredConstructor().newInstance(); | ||
} | ||
} catch (final ClassNotFoundException ex) { | ||
Skript.exception(ex, "Cannot load class " + c); | ||
} catch (final ExceptionInInitializerError err) { | ||
Skript.exception(err.getCause(), "Class " + c + " generated an exception while loading"); | ||
} | ||
} | ||
getAddonInstance().loadClasses(c -> { | ||
if (Hook.class.isAssignableFrom(c) && !c.isInterface() && !Modifier.isAbstract(c.getModifiers())) { | ||
try { | ||
c.getDeclaredConstructor().newInstance(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to move away from using |
||
} catch (Exception ex) { | ||
Skript.exception(ex, "Failed to load hook class " + c); | ||
} | ||
} | ||
} catch (final Exception e) { | ||
error("Error while loading plugin hooks" + (e.getLocalizedMessage() == null ? "" : ": " + e.getLocalizedMessage())); | ||
Skript.exception(e); | ||
} | ||
}, false, "ch.njol.skript.hooks", false); | ||
finishedLoadingHooks = true; | ||
|
||
if (TestMode.ENABLED) { | ||
info("Preparing Skript for testing..."); | ||
tainted = true; | ||
try { | ||
getAddonInstance().loadClasses("ch.njol.skript", "tests"); | ||
} catch (IOException e) { | ||
Skript.exception("Failed to load testing environment."); | ||
Bukkit.getServer().shutdown(); | ||
} | ||
getAddonInstance().loadClasses("ch.njol.skript", "tests"); | ||
} | ||
|
||
stopAcceptingRegistrations(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,116 +18,162 @@ | |
*/ | ||
package ch.njol.skript; | ||
|
||
import ch.njol.skript.localization.Language; | ||
import ch.njol.skript.util.Utils; | ||
import ch.njol.skript.util.Version; | ||
import ch.njol.util.StringUtils; | ||
import ch.njol.util.coll.iterator.EnumerationIterable; | ||
import org.bukkit.plugin.java.JavaPlugin; | ||
import org.eclipse.jdt.annotation.Nullable; | ||
import org.skriptlang.skript.registration.Module; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.lang.reflect.InvocationTargetException; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
import java.util.function.Consumer; | ||
import java.util.jar.JarEntry; | ||
import java.util.jar.JarFile; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
import org.bukkit.plugin.java.JavaPlugin; | ||
import org.eclipse.jdt.annotation.Nullable; | ||
|
||
import ch.njol.skript.localization.Language; | ||
import ch.njol.skript.util.Utils; | ||
import ch.njol.skript.util.Version; | ||
import ch.njol.util.coll.iterator.EnumerationIterable; | ||
|
||
/** | ||
* Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. | ||
* | ||
* @author Peter Güttinger | ||
*/ | ||
public final class SkriptAddon { | ||
|
||
public final JavaPlugin plugin; | ||
public final Version version; | ||
private final String name; | ||
|
||
/** | ||
* Package-private constructor. Use {@link Skript#registerAddon(JavaPlugin)} to get a SkriptAddon for your plugin. | ||
* | ||
* @param p | ||
* @param plugin The plugin representing the SkriptAddon to be registered. | ||
*/ | ||
SkriptAddon(final JavaPlugin p) { | ||
plugin = p; | ||
name = "" + p.getName(); | ||
Version v; | ||
SkriptAddon(JavaPlugin plugin) { | ||
this.plugin = plugin; | ||
|
||
Version version; | ||
String descriptionVersion = plugin.getDescription().getVersion(); | ||
try { | ||
v = new Version("" + p.getDescription().getVersion()); | ||
version = new Version(descriptionVersion); | ||
} catch (final IllegalArgumentException e) { | ||
final Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(p.getDescription().getVersion()); | ||
Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(descriptionVersion); | ||
if (!m.find()) | ||
throw new IllegalArgumentException("The version of the plugin " + p.getName() + " does not contain any numbers: " + p.getDescription().getVersion()); | ||
v = new Version(Utils.parseInt("" + m.group(1)), m.group(2) == null ? 0 : Utils.parseInt("" + m.group(2)), m.group(3) == null ? 0 : Utils.parseInt("" + m.group(3))); | ||
Skript.warning("The plugin " + p.getName() + " uses a non-standard version syntax: '" + p.getDescription().getVersion() + "'. Skript will use " + v + " instead."); | ||
throw new IllegalArgumentException("The version of the plugin " + plugin.getName() + " does not contain any numbers: " + descriptionVersion); | ||
version = new Version(Utils.parseInt("" + m.group(1)), m.group(2) == null ? 0 : Utils.parseInt("" + m.group(2)), m.group(3) == null ? 0 : Utils.parseInt("" + m.group(3))); | ||
Skript.warning("The plugin " + plugin.getName() + " uses a non-standard version syntax: '" + descriptionVersion + "'. Skript will use " + version + " instead."); | ||
} | ||
version = v; | ||
this.version = version; | ||
} | ||
|
||
@Override | ||
public final String toString() { | ||
return name; | ||
public String toString() { | ||
return plugin.getName(); | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
return plugin.getName(); | ||
} | ||
|
||
/** | ||
* Loads classes of the plugin by package. Useful for registering many syntax elements like Skript does it. | ||
* | ||
* @param basePackage The base package to add to all sub packages, e.g. <tt>"ch.njol.skript"</tt>. | ||
* @param subPackages Which subpackages of the base package should be loaded, e.g. <tt>"expressions", "conditions", "effects"</tt>. Subpackages of these packages will be loaded | ||
* as well. Use an empty array to load all subpackages of the base package. | ||
* @throws IOException If some error occurred attempting to read the plugin's jar file. | ||
* Loads classes of the plugin by package. Useful for registering many syntax elements like Skript. | ||
* | ||
* @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). | ||
* @param subPackages Specific subpackages to search in (e.g. 'conditions') | ||
* If no subpackages are provided, all subpackages of the base package will be searched. | ||
* @return This SkriptAddon | ||
*/ | ||
public SkriptAddon loadClasses(String basePackage, String... subPackages) { | ||
return loadClasses(null, true, basePackage, true, subPackages); | ||
} | ||
|
||
/** | ||
* Loads classes of the plugin by package. Useful for registering many syntax elements like Skript. | ||
* | ||
* @param withClass A consumer that will run with each found class. | ||
* @param initialize Whether classes found in the package search should be initialized. | ||
* @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). | ||
* @param recursive Whether to recursively search through the subpackages provided. | ||
* @param subPackages Specific subpackages to search in (e.g. 'conditions') | ||
* If no subpackages are provided, all subpackages of the base package will be searched. | ||
* @return This SkriptAddon | ||
*/ | ||
public SkriptAddon loadClasses(String basePackage, final String... subPackages) throws IOException { | ||
assert subPackages != null; | ||
final JarFile jar = new JarFile(getFile()); | ||
@SuppressWarnings("ThrowableNotThrown") | ||
public SkriptAddon loadClasses(@Nullable Consumer<Class<?>> withClass, boolean initialize, String basePackage, boolean recursive, String... subPackages) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for (int i = 0; i < subPackages.length; i++) | ||
subPackages[i] = subPackages[i].replace('.', '/') + "/"; | ||
basePackage = basePackage.replace('.', '/') + "/"; | ||
try { | ||
for (final JarEntry e : new EnumerationIterable<>(jar.entries())) { | ||
if (e.getName().startsWith(basePackage) && e.getName().endsWith(".class")) { | ||
|
||
int depth = !recursive ? StringUtils.count(basePackage, '/') + 1 : 0; | ||
|
||
File file = getFile(); | ||
if (file == null) { | ||
Skript.error("Unable to retrieve file from addon '" + getName() + "'. Classes will not be loaded."); | ||
return this; | ||
} | ||
|
||
try (final JarFile jar = new JarFile(file)) { | ||
boolean hasWithClass = withClass != null; | ||
for (JarEntry e : new EnumerationIterable<>(jar.entries())) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been exploring a more efficient way of getting all classes within a package, will let you know on discord if it's a viable alternative. |
||
String name = e.getName(); | ||
if (name.startsWith(basePackage) && name.endsWith(".class") && (recursive || StringUtils.count(name, '/') <= depth)) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
boolean load = subPackages.length == 0; | ||
for (final String sub : subPackages) { | ||
if (e.getName().startsWith(sub, basePackage.length())) { | ||
for (String subPackage : subPackages) { | ||
if (e.getName().startsWith(subPackage, basePackage.length())) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
load = true; | ||
break; | ||
} | ||
} | ||
|
||
if (load) { | ||
final String c = e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length()); | ||
String c = e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length()); | ||
try { | ||
Class.forName(c, true, plugin.getClass().getClassLoader()); | ||
} catch (final ClassNotFoundException ex) { | ||
Skript.exception(ex, "Cannot load class " + c + " from " + this); | ||
} catch (final ExceptionInInitializerError err) { | ||
Class<?> clazz = Class.forName(c, initialize, plugin.getClass().getClassLoader()); | ||
if (hasWithClass) | ||
withClass.accept(clazz); | ||
} catch (ClassNotFoundException ex) { | ||
Skript.exception(ex, "Cannot load class " + c); | ||
} catch (ExceptionInInitializerError err) { | ||
Skript.exception(err.getCause(), this + "'s class " + c + " generated an exception while loading"); | ||
} | ||
continue; | ||
} | ||
} | ||
} | ||
} finally { | ||
try { | ||
jar.close(); | ||
} catch (final IOException e) {} | ||
} catch (IOException e) { | ||
Skript.exception(e, "Failed to load classes for addon: " + plugin.getName()); | ||
} | ||
return this; | ||
} | ||
|
||
/** | ||
* Loads all module classes found in the package search. | ||
* @param basePackage The base package to start searching in (e.g. 'ch.njol.skript'). | ||
* @param subPackages Specific subpackages to search in (e.g. 'conditions'). | ||
* If no subpackages are provided, all subpackages will be searched. | ||
* Note that the search will go no further than the first layer of subpackages. | ||
*/ | ||
@SuppressWarnings("ThrowableNotThrown") | ||
public SkriptAddon loadModules(String basePackage, String... subPackages) { | ||
return loadClasses(c -> { | ||
if (Module.class.isAssignableFrom(c) && !c.isInterface() && !Modifier.isAbstract(c.getModifiers())) { | ||
try { | ||
((Module) c.getConstructor().newInstance()).register(this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, we may be able to find a better option for this. |
||
} catch (Exception e) { | ||
Skript.exception(e, "Failed to load registration " + c); | ||
} | ||
} | ||
}, false, basePackage, false, subPackages); | ||
} | ||
|
||
@Nullable | ||
private String languageFileDirectory = null; | ||
|
||
/** | ||
* Makes Skript load language files from the specified directory, e.g. "lang" or "skript lang" if you have a lang folder yourself. Localised files will be read from the | ||
* plugin's jar and the plugin's data folder, but the default English file is only taken from the jar and <b>must</b> exist! | ||
* Loads language files from the specified directory (e.g. "lang") into Skript. | ||
* Localized files will be read from the plugin's jar and the plugin's data file, | ||
* but the <b>default.lang</b> file is only taken from the jar and <b>must</b> exist! | ||
* | ||
* @param directory Directory name | ||
* @return This SkriptAddon | ||
|
@@ -142,7 +188,11 @@ public SkriptAddon setLanguageFileDirectory(String directory) { | |
Language.loadDefault(this); | ||
return this; | ||
} | ||
|
||
|
||
/** | ||
* @return The language file directory set for this addon. | ||
* It must first be set using {@link #setLanguageFileDirectory(String)}. | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
@Nullable | ||
public String getLanguageFileDirectory() { | ||
return languageFileDirectory; | ||
|
@@ -152,27 +202,25 @@ public String getLanguageFileDirectory() { | |
private File file = null; | ||
|
||
/** | ||
* @return The jar file of the plugin. The first invocation of this method uses reflection to invoke the protected method {@link JavaPlugin#getFile()} to get the plugin's jar | ||
* file. The file is then cached and returned upon subsequent calls to this method to reduce usage of reflection. | ||
* @return The jar file of the plugin. | ||
* After this method is first called, the file will be cached for future use. | ||
*/ | ||
@Nullable | ||
public File getFile() { | ||
if (file != null) | ||
return file; | ||
try { | ||
final Method getFile = JavaPlugin.class.getDeclaredMethod("getFile"); | ||
Method getFile = JavaPlugin.class.getDeclaredMethod("getFile"); | ||
getFile.setAccessible(true); | ||
file = (File) getFile.invoke(plugin); | ||
return file; | ||
} catch (final NoSuchMethodException e) { | ||
Skript.outdatedError(e); | ||
} catch (final IllegalArgumentException e) { | ||
} catch (NoSuchMethodException | IllegalArgumentException e) { | ||
Skript.outdatedError(e); | ||
} catch (final IllegalAccessException e) { | ||
} catch (IllegalAccessException e) { | ||
assert false; | ||
} catch (final SecurityException e) { | ||
} catch (SecurityException e) { | ||
throw new RuntimeException(e); | ||
} catch (final InvocationTargetException e) { | ||
} catch (InvocationTargetException e) { | ||
throw new RuntimeException(e.getCause()); | ||
} | ||
return null; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/** | ||
* This file is part of Skript. | ||
* | ||
* Skript is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* Skript is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with Skript. If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
* Copyright Peter Güttinger, SkriptLang team and contributors | ||
*/ | ||
package org.skriptlang.skript.registration; | ||
|
||
import ch.njol.skript.SkriptAddon; | ||
|
||
import java.io.IOException; | ||
|
||
/** | ||
* A module is a part of a {@link SkriptAddon} containing related syntax, classinfos, converters, etc. | ||
* Modules can be loaded using {@link SkriptAddon#loadModules(String, String...)}. | ||
* Note that when loading 'org.skriptlang.skript.X', the module class should be placed at 'org.skriptlang.skript.X.ModuleClassHere' | ||
* as the mentioned method will not search deeper than the provided subpackages. | ||
*/ | ||
public abstract class Module { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @param addon The addon responsible for registering this module. | ||
* To be used for registering syntax, classinfos, etc. | ||
*/ | ||
public abstract void register(SkriptAddon addon) throws IOException; | ||
|
||
/** | ||
* Loads syntax elements for this module assuming "elements" to be the location of syntax elements. | ||
* @param loader The SkriptAddon to load syntax with. | ||
*/ | ||
public final void loadSyntax(SkriptAddon loader) { | ||
loadSyntax(loader, "elements"); | ||
} | ||
|
||
/** | ||
* Loads syntax elements for this module. | ||
* @param loader The SkriptAddon to load syntax with. | ||
* @param packageName The location of syntax elements (ex: "elements") | ||
*/ | ||
public final void loadSyntax(SkriptAddon loader, String packageName) { | ||
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
APickledWalrus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
loader.loadClasses(getClass().getPackage().getName() + "." + packageName); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this actually for? It's a test-only action but I can't see a comment explaining it.