diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 2b8d8c142d1..7c437804bf7 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -7,6 +7,9 @@ import ch.njol.skript.command.Commands; import ch.njol.skript.doc.Documentation; import ch.njol.skript.events.EvtSkript; +import ch.njol.skript.examples.BukkitExampleScripts; +import ch.njol.skript.examples.CoreExampleScripts; +import ch.njol.skript.examples.ExampleScriptManager; import ch.njol.skript.expressions.arithmetic.ExprArithmetic; import ch.njol.skript.hooks.Hook; import ch.njol.skript.lang.*; @@ -163,6 +166,12 @@ public final class Skript extends JavaPlugin implements Listener { private static boolean disabled = false; private static boolean partDisabled = false; + static @Nullable ExampleScriptManager exampleManager; + + public static @Nullable ExampleScriptManager getExampleManager() { + return exampleManager; + } + public static Skript getInstance() { if (instance == null) throw new IllegalStateException(); @@ -398,11 +407,9 @@ public void onEnable() { if (!scriptsFolder.isDirectory() || !config.exists() || !features.exists() || !lang.exists() || !aliasesFolder.exists()) { ZipFile f = null; try { - boolean populateExamples = false; if (!scriptsFolder.isDirectory()) { if (!scriptsFolder.mkdirs()) throw new IOException("Could not create the directory " + scriptsFolder); - populateExamples = true; } boolean populateLanguageFiles = false; @@ -422,15 +429,9 @@ public void onEnable() { if (e.isDirectory()) continue; File saveTo = null; - if (populateExamples && e.getName().startsWith(SCRIPTSFOLDER + "/")) { - String fileName = e.getName().substring(e.getName().indexOf("/") + 1); - // All example scripts must be disabled for jar security. - if (!fileName.startsWith(ScriptLoader.DISABLED_SCRIPT_PREFIX)) - fileName = ScriptLoader.DISABLED_SCRIPT_PREFIX + fileName; - saveTo = new File(scriptsFolder, fileName); - } else if (populateLanguageFiles - && e.getName().startsWith("lang/") - && !e.getName().endsWith("default.lang")) { + if (populateLanguageFiles + && e.getName().startsWith("lang/") + && !e.getName().endsWith("default.lang")) { String fileName = e.getName().substring(e.getName().lastIndexOf("/") + 1); saveTo = new File(lang, fileName); } else if (e.getName().equals("config.sk")) { @@ -454,7 +455,7 @@ public void onEnable() { } } } - info("Successfully generated the config and the example scripts."); + info("Successfully generated the config."); } catch (ZipException ignored) {} catch (IOException e) { error("Error generating the default files: " + ExceptionUtils.toString(e)); } finally { @@ -466,6 +467,10 @@ public void onEnable() { } } + exampleManager = new ExampleScriptManager(); + exampleManager.installExamples("Skript (core)", CoreExampleScripts.provider(), scriptsFolder); + exampleManager.installExamples("Skript (Bukkit)", BukkitExampleScripts.provider(), scriptsFolder); + // initialize the modern Skript instance skript = org.skriptlang.skript.Skript.of(getClass(), getName()); unmodifiableSkript = new ModernSkriptBridge.SpecialUnmodifiableSkript(skript); diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index af381ff3049..26a4af93a48 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -1,23 +1,24 @@ package ch.njol.skript; -import java.io.File; -import java.io.IOException; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.Nullable; - +import ch.njol.skript.examples.ExampleScript; +import ch.njol.skript.examples.ExampleScriptManager; +import ch.njol.skript.examples.ExampleScriptProvider; import ch.njol.skript.util.Utils; import ch.njol.skript.util.Version; +import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.localization.Localizer; import org.skriptlang.skript.registration.SyntaxRegistry; import org.skriptlang.skript.util.Registry; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. */ @@ -93,6 +94,43 @@ public String getLanguageFileDirectory() { return localizer().languageFileDirectory(); } + /** + * Registers example scripts with Skript for this addon. + * + * @param scripts Example scripts to install + * @throws IOException If an I/O error occurs while installing the scripts + */ + public void registerExampleScripts(ExampleScript... scripts) throws IOException { + ExampleScriptManager manager = Skript.getExampleManager(); + if (manager == null) + throw new IllegalStateException("Example script manager is not initialized"); + + manager.installExamples( + plugin.getName(), + Arrays.asList(scripts), + Skript.getInstance().getScriptsFolder() + ); + } + + /** + * Registers example scripts with Skript for this addon using a provider. + * + * @param provider Provider that supplies example scripts to install + * @throws IOException If an I/O error occurs while installing the scripts + */ + public void registerExampleScripts(ExampleScriptProvider provider) throws IOException { + ExampleScriptManager manager = Skript.getExampleManager(); + if (manager == null) + throw new IllegalStateException("Example script manager is not initialized"); + + manager.installExamples( + plugin.getName(), + provider, + Skript.getInstance().getScriptsFolder() + ); + } + + @Nullable private File file; diff --git a/src/main/java/ch/njol/skript/examples/BukkitExampleScripts.java b/src/main/java/ch/njol/skript/examples/BukkitExampleScripts.java new file mode 100644 index 00000000000..a62857ff40c --- /dev/null +++ b/src/main/java/ch/njol/skript/examples/BukkitExampleScripts.java @@ -0,0 +1,45 @@ +package ch.njol.skript.examples; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; + +/** + * Example scripts that rely on Bukkit-specific behaviours or APIs. + */ +public final class BukkitExampleScripts { + + public static final List EXAMPLES = List.of( + load("chest menus.sk"), + load("commands.sk"), + load("events.sk"), + load("timings.sk") + ); + + private static final ExampleScriptProvider PROVIDER = () -> EXAMPLES; + + private BukkitExampleScripts() {} + + public static Collection all() { + return EXAMPLES; + } + + public static ExampleScriptProvider provider() { + return PROVIDER; + } + + private static ExampleScript load(String name) { + String path = "scripts/-examples/" + name; + try (InputStream in = BukkitExampleScripts.class.getClassLoader().getResourceAsStream(path)) { + if (in == null) + throw new IllegalStateException("Missing example script " + path); + String content = new String(in.readAllBytes(), StandardCharsets.UTF_8); + return new ExampleScript(name, content); + } catch (IOException e) { + throw new RuntimeException("Failed to load example script " + path, e); + } + } + +} diff --git a/src/main/java/ch/njol/skript/examples/CoreExampleScripts.java b/src/main/java/ch/njol/skript/examples/CoreExampleScripts.java new file mode 100644 index 00000000000..39736e1e8ca --- /dev/null +++ b/src/main/java/ch/njol/skript/examples/CoreExampleScripts.java @@ -0,0 +1,46 @@ +package ch.njol.skript.examples; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; + +public final class CoreExampleScripts { + + public static final List EXAMPLES = List.of( + load("experimental features/for loops.sk"), + load("experimental features/queues.sk"), + load("experimental features/script reflection.sk"), + load("functions.sk"), + load("loops.sk"), + load("options and meta.sk"), + load("text formatting.sk"), + load("variables.sk") + ); + + private static final ExampleScriptProvider PROVIDER = () -> EXAMPLES; + + private CoreExampleScripts() {} + + public static Collection all() { + return EXAMPLES; + } + + public static ExampleScriptProvider provider() { + return PROVIDER; + } + + private static ExampleScript load(String name) { + String path = "scripts/-examples/" + name; + try (InputStream in = CoreExampleScripts.class.getClassLoader().getResourceAsStream(path)) { + if (in == null) + throw new IllegalStateException("Missing example script " + path); + String content = new String(in.readAllBytes(), StandardCharsets.UTF_8); + return new ExampleScript(name, content); + } catch (IOException e) { + throw new RuntimeException("Failed to load example script " + path, e); + } + } + +} diff --git a/src/main/java/ch/njol/skript/examples/ExampleScript.java b/src/main/java/ch/njol/skript/examples/ExampleScript.java new file mode 100644 index 00000000000..2a9ed7e6463 --- /dev/null +++ b/src/main/java/ch/njol/skript/examples/ExampleScript.java @@ -0,0 +1,31 @@ +package ch.njol.skript.examples; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Represents an example script bundled with Skript or an addon. + */ +public record ExampleScript(String name, String content) { + + /** + * Loads an example script from a resource contained within an addon JAR. + * + * @param plugin The plugin providing the resource + * @param resourcePath The path to the resource inside the plugin + * @param outputName The name of the file to install the example as + * @return A new {@link ExampleScript} containing the resource's content + * @throws IOException If the resource cannot be found or read + */ + public static ExampleScript fromResource(JavaPlugin plugin, String resourcePath, String outputName) throws IOException { + try (InputStream in = plugin.getResource(resourcePath)) { + if (in == null) + throw new IOException("Resource not found: " + resourcePath); + String content = new String(in.readAllBytes(), StandardCharsets.UTF_8); + return new ExampleScript(outputName, content); + } + } +} diff --git a/src/main/java/ch/njol/skript/examples/ExampleScriptManager.java b/src/main/java/ch/njol/skript/examples/ExampleScriptManager.java new file mode 100644 index 00000000000..18dabf307f6 --- /dev/null +++ b/src/main/java/ch/njol/skript/examples/ExampleScriptManager.java @@ -0,0 +1,128 @@ +package ch.njol.skript.examples; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.DosFileAttributeView; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Handles the installation of bundled example scripts for Skript and its addons. + * + * The manager keeps track of which examples were already installed using a file located next + * to the user's scripts directory. + */ +public final class ExampleScriptManager { + + private Set installed; + private File installedFile; + + public ExampleScriptManager() {} + + /** + * Loads the list of already-installed example scripts for the provided scripts directory. + * + * @param scriptsDir the root of the scripts directory where examples are stored + */ + private void loadInstalled(File scriptsDir) { + File parent = scriptsDir.getParentFile(); + installedFile = new File(parent == null ? scriptsDir : parent, ".loaded_examples"); + installed = new LinkedHashSet<>(); + if (!installedFile.exists()) + return; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(installedFile), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty()) + installed.add(line); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load installed examples", e); + } + } + + /** + * Persists the list of installed example scripts. Hides the metadata file on Windows systems. + */ + private void flushInstalled() { + if (installedFile == null || installed == null) + return; + File parent = installedFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) // failed to create directory for installed examples + return; + boolean isWindows = System.getProperty("os.name").startsWith("Windows"); + Path installedPath = installedFile.toPath(); + DosFileAttributeView dosView = null; + if (isWindows && Files.exists(installedPath)) { + dosView = Files.getFileAttributeView(installedPath, DosFileAttributeView.class); + if (dosView != null) { + try { + if (dosView.readAttributes().isHidden()) + dosView.setHidden(false); + } catch (IOException ignored) {} + } + } + + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(installedFile), StandardCharsets.UTF_8))) { + for (String entry : installed) { + writer.write(entry); + writer.newLine(); + } + } catch (IOException e) { // failed to save installed examples + return; + } + + if (isWindows) { + try { + if (dosView != null) { + dosView.setHidden(true); + } else { + Files.setAttribute(installedPath, "dos:hidden", true); + } + } catch (Exception ignored) {} + } + } + + /** + * Installs the provided collection of example scripts into the user's scripts directory. + * + * @param plugin the name of the plugin providing the examples; used for namespacing on disk + * @param scripts the examples to install + * @param scriptsDir the root of the scripts directory where the examples should be written + */ + public void installExamples(String plugin, Collection scripts, File scriptsDir) { + loadInstalled(scriptsDir); + boolean dirty = false; + File baseDir = new File(scriptsDir, "-examples/" + plugin); + for (ExampleScript script : scripts) { + String key = plugin + "/" + script.name(); + if (installed.add(key)) { + dirty = true; + File file = new File(baseDir, script.name()); + file.getParentFile().mkdirs(); + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { + writer.write(script.content()); + } catch (IOException e) { + throw new RuntimeException("Failed to write example script " + file, e); + } + } + } + if (dirty) + flushInstalled(); + } + + /** + * Installs example scripts provided by an {@link ExampleScriptProvider}. + * + * @param plugin the name of the plugin providing the examples + * @param provider a supplier of example scripts to install + * @param scriptsDir the root of the scripts directory where the examples should be written + */ + public void installExamples(String plugin, ExampleScriptProvider provider, File scriptsDir) { + installExamples(plugin, provider.scripts(), scriptsDir); + } + +} diff --git a/src/main/java/ch/njol/skript/examples/ExampleScriptProvider.java b/src/main/java/ch/njol/skript/examples/ExampleScriptProvider.java new file mode 100644 index 00000000000..8190d4fd8dd --- /dev/null +++ b/src/main/java/ch/njol/skript/examples/ExampleScriptProvider.java @@ -0,0 +1,18 @@ +package ch.njol.skript.examples; + +import java.util.Collection; + +/** + * Provides example scripts for installation. + */ +@FunctionalInterface +public interface ExampleScriptProvider { + + /** + * Returns the example scripts supplied by this provider. + * + * @return a collection of example scripts + */ + Collection scripts(); + +}