diff --git a/.github/workflows/checkstyle.yml b/.github/workflows/checkstyle.yml new file mode 100644 index 00000000000..409ca6b4fd7 --- /dev/null +++ b/.github/workflows/checkstyle.yml @@ -0,0 +1,37 @@ +name: checkstyle + +on: + push: + branches: + - master + - 'dev/**' + pull_request: + +jobs: + build: + if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'adopt' + cache: gradle + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Run checkstyle + run: ./gradlew clean checkstyleMain + - name: Upload checkstyle report + uses: actions/upload-artifact@v4 + if: success() + with: + name: checkstyle-report + path: | + build/reports/checkstyle/*.xml + build/reports/checkstyle/*.html diff --git a/.github/workflows/github-issues/issues-labeled.yml b/.github/workflows/github-issues/issues-labeled.yml new file mode 100644 index 00000000000..6f65129b48c --- /dev/null +++ b/.github/workflows/github-issues/issues-labeled.yml @@ -0,0 +1,22 @@ +name: When labels are modified, run actions. + +on: + issues: + types: [labeled] + +jobs: + remove-good-first-issue-label: + if: ${{ github.event.label.name == 'completed' || github.event.label.name == 'PR available'}} + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/github-script@v6 + with: + script: | + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ["good first issue"] + }) diff --git a/build.gradle b/build.gradle index 88fe6a634a8..6052dc530da 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' id 'maven-publish' id 'java' + id 'checkstyle' } configurations { @@ -42,6 +43,11 @@ dependencies { testShadow group: 'org.easymock', name: 'easymock', version: '5.4.0' } +checkstyle { + configFile = new File("checkstyle.xml") + sourceSets = [] // disables checkstyle after build task +} + task checkAliases { description 'Checks for the existence of the aliases.' doLast { diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000000..42794bbeccb --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/code-conventions.md b/code-conventions.md index c9b70f8d5a4..f47329495a0 100644 --- a/code-conventions.md +++ b/code-conventions.md @@ -97,6 +97,7 @@ If we need to remove or alter contributed code due to a licensing issue we will - The exception to this is breaking up conditional statements (e.g. `if (x || y)`) where the condition starts may be aligned * Each class begins with an empty line +* Each Java file ends with an empty line * No squeezing of multiple lines of code on a single line * Separate method declarations with empty lines - Empty line after last method in a class is *not* required diff --git a/gradle.properties b/gradle.properties index a620b360bd5..8fa041e0878 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true groupid=ch.njol name=skript -version=2.9.2 +version=2.9.3 jarName=Skript.jar testEnv=java21/paper-1.21.0 testEnvJavaVersion=21 diff --git a/src/main/java/ch/njol/skript/SkriptCommand.java b/src/main/java/ch/njol/skript/SkriptCommand.java index c735541cae3..9dc50018dd5 100644 --- a/src/main/java/ch/njol/skript/SkriptCommand.java +++ b/src/main/java/ch/njol/skript/SkriptCommand.java @@ -21,7 +21,9 @@ import ch.njol.skript.aliases.Aliases; import ch.njol.skript.command.CommandHelp; import ch.njol.skript.doc.Documentation; +import ch.njol.skript.doc.DocumentationIdProvider; import ch.njol.skript.doc.HTMLGenerator; +import ch.njol.skript.doc.JSONGenerator; import ch.njol.skript.localization.ArgsMessage; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.PluralizingArgsMessage; @@ -213,7 +215,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St if (scriptInfo.files == 0) { info(sender, "reload.empty folder", fileName); } else { - reloaded(sender, logHandler, timingLogHandler, "x scripts in folder", fileName, scriptInfo.files); + if (logHandler.numErrors() == 0) { + reloaded(sender, logHandler, timingLogHandler, "x scripts in folder success", fileName, scriptInfo.files); + } else { + reloaded(sender, logHandler, timingLogHandler, "x scripts in folder error", fileName, scriptInfo.files); + } } }); } @@ -403,9 +409,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } File outputDir = Documentation.getDocsOutputDirectory(); outputDir.mkdirs(); - HTMLGenerator generator = new HTMLGenerator(templateDir, outputDir); + HTMLGenerator htmlGenerator = new HTMLGenerator(templateDir, outputDir); + JSONGenerator jsonGenerator = new JSONGenerator(templateDir, outputDir); Skript.info(sender, "Generating docs..."); - generator.generate(); // Try to generate docs... hopefully + htmlGenerator.generate(); // Try to generate docs... hopefully + jsonGenerator.generate(); Skript.info(sender, "Documentation generated!"); } else if (args[0].equalsIgnoreCase("test") && TestMode.DEV_MODE) { File scriptFile; diff --git a/src/main/java/ch/njol/skript/aliases/Aliases.java b/src/main/java/ch/njol/skript/aliases/Aliases.java index 19daed69359..3e67971698a 100644 --- a/src/main/java/ch/njol/skript/aliases/Aliases.java +++ b/src/main/java/ch/njol/skript/aliases/Aliases.java @@ -395,17 +395,17 @@ public static void load() { } /** - * Temporarily create an alias for a material which may not have an alias yet. + * Temporarily create an alias for materials which do not have aliases yet. */ private static void loadMissingAliases() { if (!Skript.methodExists(Material.class, "getKey")) return; for (Material material : Material.values()) { - if (!provider.hasAliasForMaterial(material)) { + if (!material.isLegacy() && !provider.hasAliasForMaterial(material)) { NamespacedKey key = material.getKey(); String name = key.getKey().replace("_", " "); parser.loadAlias(name + "¦s", key.toString()); - Skript.debug(ChatColor.YELLOW + "Creating temporary alias for: " + key.toString()); + Skript.debug(ChatColor.YELLOW + "Creating temporary alias for: " + key); } } } diff --git a/src/main/java/ch/njol/skript/aliases/ItemType.java b/src/main/java/ch/njol/skript/aliases/ItemType.java index 8b78d39229b..aa772d2b646 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemType.java +++ b/src/main/java/ch/njol/skript/aliases/ItemType.java @@ -1,21 +1,3 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.aliases; import ch.njol.skript.aliases.ItemData.OldItemData; @@ -41,6 +23,7 @@ import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.Tag; +import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.Skull; @@ -391,22 +374,35 @@ public boolean hasType() { */ public boolean setBlock(Block block, boolean applyPhysics) { for (int i = random.nextInt(types.size()); i < types.size(); i++) { - ItemData d = types.get(i); - Material blockType = ItemUtils.asBlock(d.type); + ItemData data = types.get(i); + Material blockType = ItemUtils.asBlock(data.type); + if (blockType == null) // Ignore items which cannot be placed continue; - if (BlockUtils.set(block, blockType, d.getBlockValues(), applyPhysics)) { - ItemMeta itemMeta = getItemMeta(); - if (itemMeta instanceof SkullMeta) { - OfflinePlayer offlinePlayer = ((SkullMeta) itemMeta).getOwningPlayer(); - if (offlinePlayer == null) - continue; - Skull skull = (Skull) block.getState(); + + if (!BlockUtils.set(block, blockType, data.getBlockValues(), applyPhysics)) + continue; + + ItemMeta itemMeta = getItemMeta(); + + if (itemMeta instanceof SkullMeta) { + OfflinePlayer offlinePlayer = ((SkullMeta) itemMeta).getOwningPlayer(); + if (offlinePlayer == null) + continue; + Skull skull = (Skull) block.getState(); + if (offlinePlayer.getName() != null) { skull.setOwningPlayer(offlinePlayer); - skull.update(false, applyPhysics); + } else if (ItemUtils.CAN_CREATE_PLAYER_PROFILE) { + //noinspection deprecation + skull.setOwnerProfile(Bukkit.createPlayerProfile(offlinePlayer.getUniqueId(), "")); + } else { + //noinspection deprecation + skull.setOwner(""); } - return true; + skull.update(false, applyPhysics); } + + return true; } return false; } diff --git a/src/main/java/ch/njol/skript/bukkitutil/BukkitUnsafe.java b/src/main/java/ch/njol/skript/bukkitutil/BukkitUnsafe.java index 5a2912039fb..f47a3f5d8d7 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/BukkitUnsafe.java +++ b/src/main/java/ch/njol/skript/bukkitutil/BukkitUnsafe.java @@ -70,11 +70,7 @@ public class BukkitUnsafe { @Nullable public static Material getMaterialFromMinecraftId(String id) { - // On 1.13, Vanilla and Spigot names are same - if (id.length() > 9) - return Material.matchMaterial(id.substring(10)); // Strip 'minecraft:' out - else // Malformed material name - return null; + return Material.matchMaterial(id); } public static void modifyItemStack(ItemStack stack, String arguments) { diff --git a/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java b/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java index ced9f5a8367..a74a604664d 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java +++ b/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java @@ -21,7 +21,9 @@ import ch.njol.skript.Skript; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.util.slot.Slot; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.OfflinePlayer; import org.bukkit.Tag; import org.bukkit.TreeType; import org.bukkit.block.Block; @@ -32,9 +34,11 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; import org.jetbrains.annotations.Nullable; import java.util.HashMap; +import java.util.UUID; /** * Miscellaneous static utility methods related to items. @@ -44,6 +48,7 @@ public class ItemUtils { public static final boolean HAS_MAX_DAMAGE = Skript.methodExists(Damageable.class, "getMaxDamage"); // Introduced in Paper 1.21 public static final boolean HAS_RESET = Skript.methodExists(Damageable.class, "resetDamage"); + public static final boolean CAN_CREATE_PLAYER_PROFILE = Skript.methodExists(Bukkit.class, "createPlayerProfile", UUID.class, String.class); /** * Gets damage/durability of an item, or 0 if it does not have damage. @@ -148,6 +153,30 @@ public static void setDamage(ItemType itemType, int damage) { } } + /** + * Sets the owner of a player head. + * @param skull player head item to modify + * @param player owner of the head + */ + public static void setHeadOwner(ItemType skull, OfflinePlayer player) { + ItemMeta meta = skull.getItemMeta(); + if (!(meta instanceof SkullMeta)) + return; + + SkullMeta skullMeta = (SkullMeta) meta; + + if (player.getName() != null) { + skullMeta.setOwningPlayer(player); + } else if (CAN_CREATE_PLAYER_PROFILE) { + //noinspection deprecation + skullMeta.setOwnerProfile(Bukkit.createPlayerProfile(player.getUniqueId(), "")); + } else { + skullMeta.setOwningPlayer(null); + } + + skull.setItemMeta(skullMeta); + } + /** * Gets a block material corresponding to given item material, which might * be the given material. If no block material is found, null is returned. diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java index ba0a66a1f41..94b5fe789c4 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java @@ -61,6 +61,7 @@ import org.bukkit.entity.Projectile; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.bukkit.event.entity.EntityDamageEvent.DamageCause; +import org.bukkit.event.entity.EntityPotionEffectEvent; import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; import org.bukkit.event.entity.EntityTransformEvent.TransformReason; import org.bukkit.event.inventory.ClickType; @@ -1526,6 +1527,11 @@ public String toVariableNameString(EnchantmentOffer eo) { .name("Transform Reason") .description("Represents a transform reason of an entity transform event.") .since("2.8.0")); + Classes.registerClass(new EnumClassInfo<>(EntityPotionEffectEvent.Cause.class, "entitypotioncause", "entity potion causes") + .user("(entity )?potion ?effect ?cause") + .name("Entity Potion Cause") + .description("Represents the cause of the action of a potion effect on an entity, e.g. arrow, command") + .since("INSERT VERSION")); } } diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java index 82fca450068..c53810d5d69 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java @@ -109,6 +109,7 @@ import org.bukkit.event.entity.EntityTeleportEvent; import org.bukkit.event.entity.EntityTransformEvent; import org.bukkit.event.entity.EntityTransformEvent.TransformReason; +import org.bukkit.event.entity.EntityPotionEffectEvent; import org.bukkit.event.entity.FireworkExplodeEvent; import org.bukkit.event.entity.HorseJumpEvent; import org.bukkit.event.entity.ItemDespawnEvent; @@ -178,6 +179,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.Recipe; +import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionData; import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionType; @@ -548,6 +550,33 @@ public DamageCause get(final EntityDeathEvent e) { return ldc == null ? null : ldc.getCause(); } }, 0); + + // Entity Potion Effect + EventValues.registerEventValue(EntityPotionEffectEvent.class, PotionEffect.class, new Getter() { + @Override + public PotionEffect get(EntityPotionEffectEvent event) { + return event.getOldEffect(); + } + }, EventValues.TIME_PAST); + EventValues.registerEventValue(EntityPotionEffectEvent.class, PotionEffect.class, new Getter() { + @Override + public PotionEffect get(EntityPotionEffectEvent event) { + return event.getNewEffect(); + } + }, EventValues.TIME_NOW); + EventValues.registerEventValue(EntityPotionEffectEvent.class, PotionEffectType.class, new Getter() { + @Override + public PotionEffectType get(EntityPotionEffectEvent event) { + return event.getModifiedType(); + } + }, EventValues.TIME_NOW); + EventValues.registerEventValue(EntityPotionEffectEvent.class, EntityPotionEffectEvent.Cause.class, new Getter() { + @Override + public EntityPotionEffectEvent.Cause get(EntityPotionEffectEvent event) { + return event.getCause(); + } + }, EventValues.TIME_NOW); + // ProjectileHitEvent // ProjectileHitEvent#getHitBlock was added in 1.11 if (Skript.methodExists(ProjectileHitEvent.class, "getHitBlock")) diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultOperations.java b/src/main/java/ch/njol/skript/classes/data/DefaultOperations.java index 860c7ee1030..1588d31ece8 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultOperations.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultOperations.java @@ -47,11 +47,7 @@ public class DefaultOperations { return left.doubleValue() * right.doubleValue(); }); Arithmetics.registerOperation(Operator.DIVISION, Number.class, (left, right) -> left.doubleValue() / right.doubleValue()); - Arithmetics.registerOperation(Operator.EXPONENTIATION, Number.class, (left, right) -> { - if (Utils.isInteger(left, right) && right.longValue() >= 0) - return (long) Math.pow(left.longValue(), right.longValue()); - return Math.pow(left.doubleValue(), right.doubleValue()); - }); + Arithmetics.registerOperation(Operator.EXPONENTIATION, Number.class, (left, right) -> Math.pow(left.doubleValue(), right.doubleValue())); Arithmetics.registerDifference(Number.class, (left, right) -> { if (Utils.isInteger(left, right)) return Math.abs(left.longValue() - right.longValue()); diff --git a/src/main/java/ch/njol/skript/conditions/CondIsWithin.java b/src/main/java/ch/njol/skript/conditions/CondIsWithin.java index e6b88ed710a..7ec25401ae8 100644 --- a/src/main/java/ch/njol/skript/conditions/CondIsWithin.java +++ b/src/main/java/ch/njol/skript/conditions/CondIsWithin.java @@ -103,7 +103,7 @@ public boolean check(Event event) { Location one = loc1.getSingle(event); Location two = loc2.getSingle(event); if (one == null || two == null || one.getWorld() != two.getWorld()) - return false; + return isNegated(); AABB box = new AABB(one, two); return locsToCheck.check(event, box::contains, isNegated()); } @@ -111,7 +111,7 @@ public boolean check(Event event) { // else, within an entity/block/chunk/world Object area = this.area.getSingle(event); if (area == null) - return false; + return isNegated(); // Entities if (area instanceof Entity) { diff --git a/src/main/java/ch/njol/skript/doc/DocumentationGenerator.java b/src/main/java/ch/njol/skript/doc/DocumentationGenerator.java new file mode 100644 index 00000000000..949e59f3ca5 --- /dev/null +++ b/src/main/java/ch/njol/skript/doc/DocumentationGenerator.java @@ -0,0 +1,23 @@ +package ch.njol.skript.doc; + +import java.io.File; + +/** + * Represents a class which generates a documentation format (like HTML or JSON) + */ +public abstract class DocumentationGenerator { + + protected File templateDir; + protected File outputDir; + + public DocumentationGenerator(File templateDir, File outputDir) { + this.templateDir = templateDir; + this.outputDir = outputDir; + } + + /** + * Generates the documentation file + */ + public abstract void generate(); + +} diff --git a/src/main/java/ch/njol/skript/doc/DocumentationIdProvider.java b/src/main/java/ch/njol/skript/doc/DocumentationIdProvider.java new file mode 100644 index 00000000000..4725e4bfaf6 --- /dev/null +++ b/src/main/java/ch/njol/skript/doc/DocumentationIdProvider.java @@ -0,0 +1,145 @@ +package ch.njol.skript.doc; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.SkriptEventInfo; +import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.function.Function; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.registrations.Classes; +import org.skriptlang.skript.lang.structure.Structure; + +import java.util.Iterator; +import java.util.Objects; +import java.util.function.Predicate; + +public class DocumentationIdProvider { + + /** + * Some syntax classes are registered more than once. This method applies a suffix + * to the id in order to differentiate them + * @param id the potentially conflicting ID + * @param collisionCount the number of conflicts this element has + * @return the unique ID for the element + */ + private static String addCollisionSuffix(String id, int collisionCount) { + if (collisionCount == 0) { + return id; + } + return id + "-" + (collisionCount + 1); + } + + /** + * Calculates the number of collisions in an iterator + * @param potentialCollisions the iterator of potential collisions + * @param collisionCriteria a predicate which checks whether a potential collision is really a collision + * @param equalsCriteria a predicate which checks whether a potential collision equals the current element we are generating + * @return the number of collisions in potentialCollisions up until equalsCriteria was true + */ + private static int calculateCollisionCount(Iterator potentialCollisions, Predicate collisionCriteria, + Predicate equalsCriteria) { + int collisionCount = 0; + while (potentialCollisions.hasNext()) { + T potentialCollision = potentialCollisions.next(); + if (collisionCriteria.test(potentialCollision)) { + if (equalsCriteria.test(potentialCollision)) { + break; + } + collisionCount += 1; + } + } + return collisionCount; + } + + /** + * Gets the documentation ID of a syntax element + * @param syntaxInfo the SyntaxElementInfo to get the ID of + * @return the ID of the syntax element + */ + public static String getId(SyntaxElementInfo syntaxInfo) { + Class syntaxClass = syntaxInfo.getElementClass(); + Iterator> syntaxElementIterator; + if (Effect.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getEffects().iterator(); + } else if (Condition.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getConditions().iterator(); + } else if (Expression.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getExpressions(); + } else if (Section.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getSections().iterator(); + } else if (Structure.class.isAssignableFrom(syntaxClass)) { + syntaxElementIterator = Skript.getStructures().iterator(); + } else { + throw new IllegalStateException("Unsupported syntax type provided"); + } + int collisionCount = calculateCollisionCount(syntaxElementIterator, + elementInfo -> elementInfo.getElementClass() == syntaxClass, + elementInfo -> elementInfo == syntaxInfo); + DocumentationId documentationIdAnnotation = syntaxClass.getAnnotation(DocumentationId.class); + if (documentationIdAnnotation == null) { + return addCollisionSuffix(syntaxClass.getSimpleName(), collisionCount); + } + return addCollisionSuffix(documentationIdAnnotation.value(), collisionCount); + } + + /** + * Gets the documentation ID of a function + * @param function the function to get the ID of + * @return the documentation ID of the function + */ + public static String getId(Function function) { + int collisionCount = calculateCollisionCount(Functions.getJavaFunctions().iterator(), + javaFunction -> function.getName().equals(javaFunction.getName()), + javaFunction -> javaFunction == function); + return addCollisionSuffix(function.getName(), collisionCount); + } + + /** + * Gets either the explicitly declared documentation ID or code name of a classinfo + * @param classInfo the ClassInfo to get the ID of + * @return the ID of the ClassInfo + */ + private static String getClassInfoId(ClassInfo classInfo) { + return Objects.requireNonNullElse(classInfo.getDocumentationID(), classInfo.getCodeName()); + } + + /** + * Gets the documentation ID of a ClassInfo + * @param classInfo the ClassInfo to get the ID of + * @return the ID of the ClassInfo + */ + public static String getId(ClassInfo classInfo) { + String classInfoId = getClassInfoId(classInfo); + int collisionCount = calculateCollisionCount(Classes.getClassInfos().iterator(), + otherClassInfo -> classInfoId.equals(getClassInfoId(otherClassInfo)), + otherClassInfo -> classInfo == otherClassInfo); + return addCollisionSuffix(classInfoId, collisionCount); + } + + /** + * Gets either the explicitly declared documentation ID or default ID of an event + * @param eventInfo the event to get the ID of + * @return the ID of the event + */ + private static String getEventId(SkriptEventInfo eventInfo) { + return Objects.requireNonNullElse(eventInfo.getDocumentationID(), eventInfo.getId()); + } + + /** + * Gets the documentation ID of an event + * @param eventInfo the event to get the ID of + * @return the ID of the event + */ + public static String getId(SkriptEventInfo eventInfo) { + String eventId = getEventId(eventInfo); + int collisionCount = calculateCollisionCount(Skript.getEvents().iterator(), + otherEventInfo -> eventId.equals(getEventId(otherEventInfo)), + otherEventInfo -> otherEventInfo == eventInfo); + return addCollisionSuffix(eventId, collisionCount); + } + +} diff --git a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java index 23e6018eed7..c159959c4d8 100644 --- a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java +++ b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java @@ -1,21 +1,3 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.doc; import ch.njol.skript.Skript; @@ -30,12 +12,11 @@ import ch.njol.skript.lang.SyntaxElementInfo; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.JavaFunction; -import ch.njol.skript.lang.function.Parameter; import ch.njol.skript.registrations.Classes; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.io.Files; -import org.apache.commons.lang.StringUtils; + import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.block.BlockCanBuildEvent; @@ -60,23 +41,19 @@ /** * Template engine, primarily used for generating Skript documentation * pages by combining data from annotations and templates. - * + * */ -public class HTMLGenerator { +public class HTMLGenerator extends DocumentationGenerator { private static final String SKRIPT_VERSION = Skript.getVersion().toString().replaceAll("-(dev|alpha|beta)\\d*", ""); // Filter branches private static final Pattern NEW_TAG_PATTERN = Pattern.compile(SKRIPT_VERSION + "(?!\\.)"); // (?!\\.) to avoid matching 2.6 in 2.6.1 etc. private static final Pattern RETURN_TYPE_LINK_PATTERN = Pattern.compile("( ?href=\"(classes\\.html|)#|)\\$\\{element\\.return-type-linkcheck}"); - private final File template; - private final File output; private final String skeleton; public HTMLGenerator(File templateDir, File outputDir) { - this.template = templateDir; - this.output = outputDir; - - this.skeleton = readFile(new File(template + "/template.html")); // Skeleton which contains every other page + super(templateDir, outputDir); + this.skeleton = readFile(new File(this.templateDir + "/template.html")); // Skeleton which contains every other page } /** @@ -128,7 +105,7 @@ private static Iterator> sortedAnnotatedItera list.sort(annotatedComparator); return list.iterator(); } - + /** * Sorts events alphabetically. */ @@ -143,19 +120,19 @@ public int compare(@Nullable SkriptEventInfo o1, @Nullable SkriptEventInfo assert false; throw new NullPointerException(); } - + if (o1.getElementClass().getAnnotation(NoDoc.class) != null) return 1; else if (o2.getElementClass().getAnnotation(NoDoc.class) != null) return -1; - + return o1.name.compareTo(o2.name); } } - + private static final EventComparator eventComparator = new EventComparator(); - + /** * Sorts class infos alphabetically. */ @@ -170,21 +147,21 @@ public int compare(@Nullable ClassInfo o1, @Nullable ClassInfo o2) { assert false; throw new NullPointerException(); } - + String name1 = o1.getDocName(); if (name1 == null) name1 = o1.getCodeName(); String name2 = o2.getDocName(); if (name2 == null) name2 = o2.getCodeName(); - + return name1.compareTo(name2); } - + } - + private static final ClassInfoComparator classInfoComparator = new ClassInfoComparator(); - + /** * Sorts functions by their names, alphabetically. */ @@ -199,40 +176,41 @@ public int compare(@Nullable JavaFunction o1, @Nullable JavaFunction o2) { assert false; throw new NullPointerException(); } - + return o1.getName().compareTo(o2.getName()); } - + } - + private static final FunctionComparator functionComparator = new FunctionComparator(); /** * Generates documentation using template and output directories * given in the constructor. */ + @Override @SuppressWarnings("unchecked") public void generate() { - for (File f : template.listFiles()) { + for (File f : templateDir.listFiles()) { if (f.getName().matches("css|js|assets")) { // Copy CSS/JS/Assets folders String slashName = "/" + f.getName(); - File fileTo = new File(output + slashName); + File fileTo = new File(outputDir + slashName); fileTo.mkdirs(); - for (File filesInside : new File(template + slashName).listFiles()) { - if (filesInside.isDirectory()) + for (File filesInside : new File(templateDir + slashName).listFiles()) { + if (filesInside.isDirectory()) continue; - + if (!filesInside.getName().toLowerCase(Locale.ENGLISH).endsWith(".png")) { // Copy images writeFile(new File(fileTo + "/" + filesInside.getName()), readFile(filesInside)); } - + else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { try { Files.copy(filesInside, new File(fileTo + "/" + filesInside.getName())); } catch (IOException e) { e.printStackTrace(); } - + } } continue; @@ -265,7 +243,7 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { } for (String name : replace) { - String temp = readFile(new File(template + "/templates/" + name)); + String temp = readFile(new File(templateDir + "/templates/" + name)); temp = temp.replace("${skript.version}", Skript.getVersion().toString()); page = page.replace("${include " + name + "}", temp); } @@ -276,15 +254,15 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { String[] genParams = page.substring(generate + 11, nextBracket).split(" "); StringBuilder generated = new StringBuilder(); - String descTemp = readFile(new File(template + "/templates/" + genParams[1])); + String descTemp = readFile(new File(templateDir + "/templates/" + genParams[1])); String genType = genParams[0]; boolean isDocsPage = genType.equals("docs"); if (genType.equals("structures") || isDocsPage) { for (Iterator> it = sortedAnnotatedIterator( - (Iterator) Skript.getStructures().stream().filter(structure -> structure.getClass() == StructureInfo.class).iterator()); - it.hasNext(); ) { + (Iterator) Skript.getStructures().stream().filter(structure -> structure.getClass() == StructureInfo.class).iterator()); + it.hasNext(); ) { StructureInfo info = it.next(); assert info != null; @@ -372,12 +350,12 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { generated.append(generateFunction(descTemp, info)); } } - + page = page.replace(page.substring(generate, nextBracket + 1), generated.toString()); - + generate = page.indexOf("${generate", nextBracket); } - + String name = f.getName(); if (name.endsWith(".html")) { // Fix some stuff specially for HTML page = page.replace("\t", "    "); // Tab to 4 non-collapsible spaces @@ -385,10 +363,10 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { page = minifyHtml(page); } assert page != null; - writeFile(new File(output + File.separator + name), page); + writeFile(new File(outputDir + File.separator + name), page); } } - + private static String minifyHtml(String page) { StringBuilder sb = new StringBuilder(page.length()); boolean space = false; @@ -403,7 +381,7 @@ private static String minifyHtml(String page) { space = false; sb.appendCodePoint(c); } - + i += Character.charCount(c); } return replaceBr(sb.toString()); @@ -416,27 +394,27 @@ private static String minifyHtml(String page) { private static String replaceBr(String page) { return page.replaceAll("
", "\n"); } - + private static String handleIf(String desc, String start, boolean value) { assert desc != null; int ifStart = desc.indexOf(start); while (ifStart != -1) { int ifEnd = desc.indexOf("${end}", ifStart); String data = desc.substring(ifStart + start.length() + 1, ifEnd); - + String before = desc.substring(0, ifStart); String after = desc.substring(ifEnd + 6); if (value) desc = before + data + after; else desc = before + after; - + ifStart = desc.indexOf(start, ifEnd); } - + return desc; } - + /** * Generates documentation entry for a type which is documented using * annotations. This means expressions, effects and conditions. @@ -465,24 +443,16 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu Description description = c.getAnnotation(Description.class); desc = desc.replace("${element.desc}", Joiner.on("\n").join(getDefaultIfNullOrEmpty((description != null ? description.value() : null), "Unknown description.")).replace("\n\n", "

")); desc = desc.replace("${element.desc-safe}", Joiner.on("\n").join(getDefaultIfNullOrEmpty((description != null ? description.value() : null), "Unknown description.")) - .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); + .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // Examples Examples examples = c.getAnnotation(Examples.class); desc = desc.replace("${element.examples}", Joiner.on("
").join(getDefaultIfNullOrEmpty((examples != null ? Documentation.escapeHTML(examples.value()) : null), "Missing examples."))); desc = desc.replace("${element.examples-safe}", Joiner.on("\\n").join(getDefaultIfNullOrEmpty((examples != null ? Documentation.escapeHTML(examples.value()) : null), "Missing examples.")) - .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); + .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // Documentation ID - DocumentationId docId = c.getAnnotation(DocumentationId.class); - String ID = docId != null ? (docId != null ? docId.value() : null) : c.getSimpleName(); - // Fix duplicated IDs - if (page != null) { - if (page.contains("href=\"#" + ID + "\"")) { - ID = ID + "-" + (StringUtils.countMatches(page, "href=\"#" + ID + "\"") + 1); - } - } - desc = desc.replace("${element.id}", ID); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Cancellable desc = handleIf(desc, "${if cancellable}", false); @@ -552,11 +522,11 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); StringBuilder patterns = new StringBuilder(); for (String line : getDefaultIfNullOrEmpty(info.patterns, "Missing patterns.")) { assert line != null; @@ -564,7 +534,7 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu String parsed = pattern.replace("${element.pattern}", line); patterns.append(parsed); } - + String toReplace = "${generate element.patterns " + split[1] + "}"; desc = desc.replace(toReplace, patterns.toString()); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.toString().replace("\\", "\\\\")); @@ -573,7 +543,7 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu assert desc != null; return desc; } - + private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String page) { Class c = info.getElementClass(); String desc; @@ -590,7 +560,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("\n").join(description).replace("\n\n", "

")); desc = desc - .replace("${element.desc-safe}", Joiner.on("\\n").join(description) + .replace("${element.desc-safe}", Joiner.on("\\n").join(description) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // By Addon @@ -603,7 +573,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc - .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) + .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); String[] keywords = info.getKeywords(); @@ -621,14 +591,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable desc = desc.replace("${element.cancellable}", cancellable ? "Yes" : ""); // if not cancellable the section is hidden // Documentation ID - String ID = info.getDocumentationID() != null ? info.getDocumentationID() : info.getId(); - // Fix duplicated IDs - if (page != null) { - if (page.contains("href=\"#" + ID + "\"")) { - ID = ID + "-" + (StringUtils.countMatches(page, "href=\"#" + ID + "\"") + 1); - } - } - desc = desc.replace("${element.id}", ID); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Events Events events = c.getAnnotation(Events.class); @@ -668,14 +631,14 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable int nextBracket = desc.indexOf("}", generate); String data = desc.substring(generate + 11, nextBracket); toGen.add(data); - + generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); StringBuilder patterns = new StringBuilder(); for (String line : getDefaultIfNullOrEmpty(info.patterns, "Missing patterns.")) { assert line != null; @@ -683,7 +646,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String parsed = pattern.replace("${element.pattern}", line); patterns.append(parsed); } - + desc = desc.replace("${generate element.patterns " + split[1] + "}", patterns.toString()); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.toString().replace("\\", "\\\\")); } @@ -691,7 +654,7 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable assert desc != null; return desc; } - + private String generateClass(String descTemp, ClassInfo info, @Nullable String page) { Class c = info.getC(); String desc; @@ -708,7 +671,7 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("\n").join(description).replace("\n\n", "

")); desc = desc - .replace("${element.desc-safe}", Joiner.on("\\n").join(description) + .replace("${element.desc-safe}", Joiner.on("\\n").join(description) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // By Addon @@ -721,20 +684,13 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc.replace("${element.examples-safe}", Joiner.on("\\n").join(Documentation.escapeHTML(examples)) - .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); + .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); Keywords keywords = c.getAnnotation(Keywords.class); desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords.value())); // Documentation ID - String ID = info.getDocumentationID() != null ? info.getDocumentationID() : info.getCodeName(); - // Fix duplicated IDs - if (page != null) { - if (page.contains("href=\"#" + ID + "\"")) { - ID = ID + "-" + (StringUtils.countMatches(page, "href=\"#" + ID + "\"") + 1); - } - } - desc = desc.replace("${element.id}", ID); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Cancellable desc = handleIf(desc, "${if cancellable}", false); @@ -777,14 +733,14 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin int nextBracket = desc.indexOf("}", generate); String data = desc.substring(generate + 11, nextBracket); toGen.add(data); - + generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); StringBuilder patterns = new StringBuilder(); String[] lines = getDefaultIfNullOrEmpty(info.getUsage(), "Missing patterns."); if (lines == null) @@ -795,15 +751,15 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin String parsed = pattern.replace("${element.pattern}", line); patterns.append(parsed); } - + desc = desc.replace("${generate element.patterns " + split[1] + "}", patterns.toString()); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.toString().replace("\\", "\\\\")); } - + assert desc != null; return desc; } - + private String generateFunction(String descTemp, JavaFunction info) { String desc = ""; @@ -819,7 +775,7 @@ private String generateFunction(String descTemp, JavaFunction info) { String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("\n").join(description)); desc = desc - .replace("${element.desc-safe}", Joiner.on("\\n").join(description) + .replace("${element.desc-safe}", Joiner.on("\\n").join(description) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); // By Addon @@ -832,14 +788,14 @@ private String generateFunction(String descTemp, JavaFunction info) { String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc - .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) + .replace("${element.examples-safe}", Joiner.on("\\n").join(examples) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); String[] keywords = info.getKeywords(); desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords)); // Documentation ID - desc = desc.replace("${element.id}", info.getName()); + desc = desc.replace("${element.id}", DocumentationIdProvider.getId(info)); // Cancellable desc = handleIf(desc, "${if cancellable}", false); @@ -870,31 +826,26 @@ private String generateFunction(String descTemp, JavaFunction info) { int nextBracket = desc.indexOf("}", generate); String data = desc.substring(generate + 11, nextBracket); toGen.add(data); - + generate = desc.indexOf("${generate", nextBracket); } - + // Assume element.pattern generate for (String data : toGen) { String[] split = data.split(" "); - String pattern = readFile(new File(template + "/templates/" + split[1])); + String pattern = readFile(new File(templateDir + "/templates/" + split[1])); String patterns = ""; - Parameter[] params = info.getParameters(); - String[] types = new String[params.length]; - for (int i = 0; i < types.length; i++) { - types[i] = params[i].toString(); - } - String line = docName + "(" + Joiner.on(", ").join(types) + ")"; // Better not have nulls + String line = info.getSignature().toString(false, false); // Better not have nulls patterns += pattern.replace("${element.pattern}", line); - + desc = desc.replace("${generate element.patterns " + split[1] + "}", patterns); desc = desc.replace("${generate element.patterns-safe " + split[1] + "}", patterns.replace("\\", "\\\\")); } - + assert desc != null; return desc; } - + @SuppressWarnings("null") private static String readFile(File f) { try { @@ -904,7 +855,7 @@ private static String readFile(File f) { return ""; } } - + private static void writeFile(File f, String data) { try { Files.write(data, f, StandardCharsets.UTF_8); @@ -912,7 +863,7 @@ private static void writeFile(File f, String data) { e.printStackTrace(); } } - + private static String cleanPatterns(final String patterns) { return Documentation.cleanPatterns(patterns); } @@ -926,14 +877,14 @@ private static String cleanPatterns(final String patterns, boolean escapeHTML) { /** * Checks if a string is empty or null then it will return the message provided - * + * * @param string the String to check * @param message the String to return if either condition is true */ public String getDefaultIfNullOrEmpty(@Nullable String string, String message) { return (string == null || string.isEmpty()) ? message : string; // Null check first otherwise NullPointerException is thrown } - + public String[] getDefaultIfNullOrEmpty(@Nullable String[] string, String message) { return (string == null || string.length == 0 || string[0].equals("")) ? new String[]{ message } : string; // Null check first otherwise NullPointerException is thrown } diff --git a/src/main/java/ch/njol/skript/doc/JSONGenerator.java b/src/main/java/ch/njol/skript/doc/JSONGenerator.java new file mode 100644 index 00000000000..9e33a90f66c --- /dev/null +++ b/src/main/java/ch/njol/skript/doc/JSONGenerator.java @@ -0,0 +1,251 @@ +package ch.njol.skript.doc; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.SkriptEventInfo; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.lang.function.JavaFunction; +import ch.njol.skript.registrations.Classes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.lang.structure.StructureInfo; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Generates JSON docs + */ +public class JSONGenerator extends DocumentationGenerator { + + public JSONGenerator(File templateDir, File outputDir) { + super(templateDir, outputDir); + } + + /** + * Coverts a String array to a JsonArray + * @param strings the String array to convert + * @return the JsonArray containing the Strings + */ + private static @Nullable JsonArray convertToJsonArray(String @Nullable [] strings) { + if (strings == null) + return null; + JsonArray jsonArray = new JsonArray(); + for (String string : strings) + jsonArray.add(new JsonPrimitive(string)); + return jsonArray; + } + + /** + * Generates the documentation JsonObject for an element that is annotated with documentation + * annotations (e.g. effects, conditions, etc.) + * @param syntaxInfo the syntax info element to generate the documentation object of + * @return the JsonObject representing the documentation of the provided syntax element + */ + private @Nullable JsonObject generatedAnnotatedElement(SyntaxElementInfo syntaxInfo) { + Class syntaxClass = syntaxInfo.getElementClass(); + Name nameAnnotation = syntaxClass.getAnnotation(Name.class); + if (nameAnnotation == null || syntaxClass.getAnnotation(NoDoc.class) != null) + return null; + JsonObject syntaxJsonObject = new JsonObject(); + syntaxJsonObject.addProperty("id", DocumentationIdProvider.getId(syntaxInfo)); + syntaxJsonObject.addProperty("name", nameAnnotation.value()); + + Since sinceAnnotation = syntaxClass.getAnnotation(Since.class); + syntaxJsonObject.addProperty("since", sinceAnnotation == null ? null : sinceAnnotation.value()); + + Description descriptionAnnotation = syntaxClass.getAnnotation(Description.class); + if (descriptionAnnotation != null) { + syntaxJsonObject.add("description", convertToJsonArray(descriptionAnnotation.value())); + } else { + syntaxJsonObject.add("description", new JsonArray()); + } + + Examples examplesAnnotation = syntaxClass.getAnnotation(Examples.class); + if (examplesAnnotation != null) { + syntaxJsonObject.add("examples", convertToJsonArray(examplesAnnotation.value())); + } else { + syntaxJsonObject.add("examples", new JsonArray()); + } + + + syntaxJsonObject.add("patterns", convertToJsonArray(syntaxInfo.getPatterns())); + return syntaxJsonObject; + } + + /** + * Generates the documentation JsonObject for an event + * @param eventInfo the event to generate the documentation object for + * @return a documentation JsonObject for the event + */ + private JsonObject generateEventElement(SkriptEventInfo eventInfo) { + JsonObject syntaxJsonObject = new JsonObject(); + syntaxJsonObject.addProperty("id", DocumentationIdProvider.getId(eventInfo)); + syntaxJsonObject.addProperty("name", eventInfo.name); + syntaxJsonObject.addProperty("since", eventInfo.getSince()); + syntaxJsonObject.add("description", convertToJsonArray(eventInfo.getDescription())); + syntaxJsonObject.add("examples", convertToJsonArray(eventInfo.getExamples())); + syntaxJsonObject.add("patterns", convertToJsonArray(eventInfo.patterns)); + return syntaxJsonObject; + } + + + /** + * Generates a JsonArray containing the documentation JsonObjects for each structure in the iterator + * @param infos the structures to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each structure + */ + private > JsonArray generateStructureElementArray(Iterator infos) { + JsonArray syntaxArray = new JsonArray(); + infos.forEachRemaining(info -> { + if (info instanceof SkriptEventInfo eventInfo) { + syntaxArray.add(generateEventElement(eventInfo)); + } else { + JsonObject structureElementJsonObject = generatedAnnotatedElement(info); + if (structureElementJsonObject != null) + syntaxArray.add(structureElementJsonObject); + } + }); + return syntaxArray; + } + + /** + * Generates a JsonArray containing the documentation JsonObjects for each syntax element in the iterator + * @param infos the syntax elements to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each syntax element + */ + private > JsonArray generateSyntaxElementArray(Iterator infos) { + JsonArray syntaxArray = new JsonArray(); + infos.forEachRemaining(info -> { + JsonObject syntaxJsonObject = generatedAnnotatedElement(info); + if (syntaxJsonObject != null) + syntaxArray.add(syntaxJsonObject); + }); + return syntaxArray; + } + + /** + * Generates the documentation JsonObject for a classinfo + * @param classInfo the ClassInfo to generate the documentation of + * @return the documentation Jsonobject of the ClassInfo + */ + private @Nullable JsonObject generateClassInfoElement(ClassInfo classInfo) { + if (!classInfo.hasDocs()) + return null; + JsonObject syntaxJsonObject = new JsonObject(); + syntaxJsonObject.addProperty("id", DocumentationIdProvider.getId(classInfo)); + syntaxJsonObject.addProperty("name", getClassInfoName(classInfo)); + syntaxJsonObject.addProperty("since", classInfo.getSince()); + syntaxJsonObject.add("description", convertToJsonArray(classInfo.getDescription())); + syntaxJsonObject.add("examples", convertToJsonArray(classInfo.getExamples())); + syntaxJsonObject.add("patterns", convertToJsonArray(classInfo.getUsage())); + return syntaxJsonObject; + } + + /** + * Generates a JsonArray containing the documentation JsonObjects for each classinfo in the iterator + * @param classInfos the classinfos to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each classinfo + */ + private JsonArray generateClassInfoArray(Iterator> classInfos) { + JsonArray syntaxArray = new JsonArray(); + classInfos.forEachRemaining(classInfo -> { + JsonObject classInfoElement = generateClassInfoElement(classInfo); + if (classInfoElement != null) + syntaxArray.add(classInfoElement); + }); + return syntaxArray; + } + + /** + * Gets either the explicitly declared documentation name or code name of a ClassInfo + * @param classInfo the ClassInfo to get the effective name of + * @return the effective name of the ClassInfo + */ + private String getClassInfoName(ClassInfo classInfo) { + return Objects.requireNonNullElse(classInfo.getDocName(), classInfo.getCodeName()); + } + + /** + * Generates the documentation JsonObject for a JavaFunction + * @param function the JavaFunction to generate the JsonObject of + * @return the JsonObject of the JavaFunction + */ + private JsonObject generateFunctionElement(JavaFunction function) { + JsonObject functionJsonObject = new JsonObject(); + functionJsonObject.addProperty("id", DocumentationIdProvider.getId(function)); + functionJsonObject.addProperty("name", function.getName()); + functionJsonObject.addProperty("since", function.getSince()); + functionJsonObject.add("description", convertToJsonArray(function.getDescription())); + functionJsonObject.add("examples", convertToJsonArray(function.getExamples())); + + ClassInfo returnType = function.getReturnType(); + if (returnType != null) { + functionJsonObject.addProperty("return-type", getClassInfoName(returnType)); + } + + String functionSignature = function.getSignature().toString(false, false); + functionJsonObject.add("patterns", convertToJsonArray(new String[] { functionSignature })); + return functionJsonObject; + } + + /** + * Generates a JsonArray containing the documentation JsonObjects for each function in the iterator + * @param functions the functions to generate documentation for + * @return a JsonArray containing the documentation JsonObjects for each function + */ + private JsonArray generateFunctionArray(Iterator> functions) { + JsonArray syntaxArray = new JsonArray(); + functions.forEachRemaining(function -> syntaxArray.add(generateFunctionElement(function))); + return syntaxArray; + } + + /** + * Writes the documentation JsonObject to an output path + * @param outputPath the path to write the documentation to + * @param jsonDocs the documentation JsonObject + */ + private void saveDocs(Path outputPath, JsonObject jsonDocs) { + try { + Gson jsonGenerator = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + Files.writeString(outputPath, jsonGenerator.toJson(jsonDocs)); + } catch (IOException exception) { + //noinspection ThrowableNotThrown + Skript.exception(exception, "An error occurred while trying to generate JSON documentation"); + } + } + + @Override + public void generate() { + JsonObject jsonDocs = new JsonObject(); + + jsonDocs.add("skriptVersion", new JsonPrimitive(Skript.getVersion().toString())); + jsonDocs.add("conditions", generateSyntaxElementArray(Skript.getConditions().iterator())); + jsonDocs.add("effects", generateSyntaxElementArray(Skript.getEffects().iterator())); + jsonDocs.add("expressions", generateSyntaxElementArray(Skript.getExpressions())); + jsonDocs.add("events", generateStructureElementArray(Skript.getEvents().iterator())); + jsonDocs.add("classes", generateClassInfoArray(Classes.getClassInfos().iterator())); + + Stream> structuresExcludingEvents = Skript.getStructures().stream() + .filter(structureInfo -> !(structureInfo instanceof SkriptEventInfo)); + jsonDocs.add("structures", generateStructureElementArray(structuresExcludingEvents.iterator())); + jsonDocs.add("sections", generateSyntaxElementArray(Skript.getSections().iterator())); + + jsonDocs.add("functions", generateFunctionArray(Functions.getJavaFunctions().iterator())); + + saveDocs(outputDir.toPath().resolve("docs.json"), jsonDocs); + } + +} diff --git a/src/main/java/ch/njol/skript/effects/EffCancelEvent.java b/src/main/java/ch/njol/skript/effects/EffCancelEvent.java index 317cdd3a9ee..3343f032cdf 100644 --- a/src/main/java/ch/njol/skript/effects/EffCancelEvent.java +++ b/src/main/java/ch/njol/skript/effects/EffCancelEvent.java @@ -1,34 +1,5 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.effects; -import org.bukkit.entity.Player; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.Event.Result; -import org.bukkit.event.block.BlockCanBuildEvent; -import org.bukkit.event.inventory.InventoryInteractEvent; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerLoginEvent; -import org.jetbrains.annotations.Nullable; - import ch.njol.skript.Skript; import ch.njol.skript.bukkitutil.PlayerUtils; import ch.njol.skript.doc.Description; @@ -42,39 +13,58 @@ import ch.njol.skript.log.ErrorQuality; import ch.njol.skript.util.Utils; import ch.njol.util.Kleenean; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.block.BlockCanBuildEvent; +import org.bukkit.event.entity.EntityToggleSwimEvent; +import org.bukkit.event.inventory.InventoryInteractEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.jetbrains.annotations.Nullable; -/** - * @author Peter Güttinger - */ @Name("Cancel Event") @Description("Cancels the event (e.g. prevent blocks from being placed, or damage being taken).") -@Examples({"on damage:", - " victim is a player", - " victim has the permission \"skript.god\"", - " cancel the event"}) +@Examples({ + "on damage:", + "\tvictim is a player", + "\tvictim has the permission \"skript.god\"", + "\tcancel the event" +}) @Since("1.0") public class EffCancelEvent extends Effect { + static { Skript.registerEffect(EffCancelEvent.class, "cancel [the] event", "uncancel [the] event"); } private boolean cancel; - - @SuppressWarnings("null") + @Override - public boolean init(final Expression[] vars, final int matchedPattern, final Kleenean isDelayed, final ParseResult parser) { + public boolean init(Expression[] expressions, int matchedPattern, + Kleenean isDelayed, ParseResult parseResult) { if (isDelayed == Kleenean.TRUE) { - Skript.error("Can't cancel an event anymore after it has already passed", ErrorQuality.SEMANTIC_ERROR); + Skript.error("An event cannot be cancelled after it has already passed", ErrorQuality.SEMANTIC_ERROR); return false; } + cancel = matchedPattern == 0; - final Class[] es = getParser().getCurrentEvents(); - if (es == null) + Class[] currentEvents = getParser().getCurrentEvents(); + + if (currentEvents == null) + return false; + + if (cancel && getParser().isCurrentEvent(EntityToggleSwimEvent.class)) { + Skript.error("Cancelling a toggle swim event has no effect"); return false; - for (final Class e : es) { - if (Cancellable.class.isAssignableFrom(e) || BlockCanBuildEvent.class.isAssignableFrom(e)) + } + + for (Class event : currentEvents) { + if (Cancellable.class.isAssignableFrom(event) || BlockCanBuildEvent.class.isAssignableFrom(event)) return true; // TODO warning if some event(s) cannot be cancelled even though some can (needs a way to be suppressed) } + if (getParser().isCurrentEvent(PlayerLoginEvent.class)) Skript.error("A connect event cannot be cancelled, but the player may be kicked ('kick player by reason of \"...\"')", ErrorQuality.SEMANTIC_ERROR); else @@ -83,24 +73,24 @@ public boolean init(final Expression[] vars, final int matchedPattern, final } @Override - public void execute(final Event e) { - if (e instanceof Cancellable) - ((Cancellable) e).setCancelled(cancel); - if (e instanceof PlayerInteractEvent) { - EvtClick.interactTracker.eventModified((Cancellable) e); - ((PlayerInteractEvent) e).setUseItemInHand(cancel ? Result.DENY : Result.DEFAULT); - ((PlayerInteractEvent) e).setUseInteractedBlock(cancel ? Result.DENY : Result.DEFAULT); - } else if (e instanceof BlockCanBuildEvent) { - ((BlockCanBuildEvent) e).setBuildable(!cancel); - } else if (e instanceof PlayerDropItemEvent) { - PlayerUtils.updateInventory(((PlayerDropItemEvent) e).getPlayer()); - } else if (e instanceof InventoryInteractEvent) { - PlayerUtils.updateInventory(((Player) ((InventoryInteractEvent) e).getWhoClicked())); + public void execute(Event event) { + if (event instanceof Cancellable) + ((Cancellable) event).setCancelled(cancel); + if (event instanceof PlayerInteractEvent) { + EvtClick.interactTracker.eventModified((Cancellable) event); + ((PlayerInteractEvent) event).setUseItemInHand(cancel ? Event.Result.DENY : Event.Result.DEFAULT); + ((PlayerInteractEvent) event).setUseInteractedBlock(cancel ? Event.Result.DENY : Event.Result.DEFAULT); + } else if (event instanceof BlockCanBuildEvent) { + ((BlockCanBuildEvent) event).setBuildable(!cancel); + } else if (event instanceof PlayerDropItemEvent) { + PlayerUtils.updateInventory(((PlayerDropItemEvent) event).getPlayer()); + } else if (event instanceof InventoryInteractEvent) { + PlayerUtils.updateInventory(((Player) ((InventoryInteractEvent) event).getWhoClicked())); } } @Override - public String toString(final @Nullable Event e, final boolean debug) { + public String toString(@Nullable Event event, boolean debug) { return (cancel ? "" : "un") + "cancel event"; } diff --git a/src/main/java/ch/njol/skript/effects/EffSort.java b/src/main/java/ch/njol/skript/effects/EffSort.java index d387fe7bae0..25f7fc840e0 100644 --- a/src/main/java/ch/njol/skript/effects/EffSort.java +++ b/src/main/java/ch/njol/skript/effects/EffSort.java @@ -1,21 +1,3 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.effects; import ch.njol.skript.Skript; @@ -48,10 +30,13 @@ import java.util.Set; @Name("Sort") -@Description({ - "Sorts a list variable using either the natural ordering of the contents or the results of the given expression.", - "Be warned, this will overwrite the indices of the list variable." -}) +@Description(""" + Sorts a list variable using either the natural ordering of the contents or the results of the given expression. + Be warned, this will overwrite the indices of the list variable. + + When using the full sort %~objects% (by|based on) <expression> pattern, + the input expression can be used to refer to the current item being sorted. + (See input expression for more information.)""") @Examples({ "set {_words::*} to \"pineapple\", \"banana\", \"yoghurt\", and \"apple\"", "sort {_words::*} # alphabetical sort", diff --git a/src/main/java/ch/njol/skript/events/EvtEntityPotion.java b/src/main/java/ch/njol/skript/events/EvtEntityPotion.java new file mode 100644 index 00000000000..ab56287ef2b --- /dev/null +++ b/src/main/java/ch/njol/skript/events/EvtEntityPotion.java @@ -0,0 +1,74 @@ +/** + * 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 . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.events; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import org.bukkit.event.Event; +import org.bukkit.event.entity.EntityPotionEffectEvent; +import org.bukkit.potion.PotionEffectType; + +import javax.annotation.Nullable; + +public class EvtEntityPotion extends SkriptEvent { + + static { + Skript.registerEvent("Entity Potion Effect", EvtEntityPotion.class, EntityPotionEffectEvent.class, + "entity potion effect [modif[y|ication]] [[of] %-potioneffecttypes%] [due to %-entitypotioncause%]") + .description("Called when an entity's potion effect is modified.", "This modification can include adding, removing or changing their potion effect.") + .examples( + "on entity potion effect modification:", + "\t\tbroadcast \"A potion effect was added to %event-entity%!\" ", + "", + "on entity potion effect modification of night vision:") + .since("INSERT VERSION"); + } + + @SuppressWarnings("unchecked") + private Expression potionEffects; + private Expression cause; + + @Override + public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult) { + potionEffects = (Expression) args[0]; + cause = (Expression) args[1]; + return true; + } + + @Override + public boolean check(Event event) { + EntityPotionEffectEvent potionEvent = (EntityPotionEffectEvent) event; + boolean effectMatches = potionEffects == null || + (potionEvent.getOldEffect() != null && potionEffects.check(event, effectType -> effectType.equals(potionEvent.getOldEffect().getType()))) || + (potionEvent.getNewEffect() != null && potionEffects.check(event, effectType -> effectType.equals(potionEvent.getNewEffect().getType()))); + + boolean causeMatches = cause == null || cause.check(event, cause -> cause.equals(potionEvent.getCause())); + + return effectMatches && causeMatches; + } + + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "on entity potion effect modification"; + } +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprHoverList.java b/src/main/java/ch/njol/skript/expressions/ExprHoverList.java index a3b58b6b9d3..835319f3449 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprHoverList.java +++ b/src/main/java/ch/njol/skript/expressions/ExprHoverList.java @@ -104,7 +104,7 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { if (HAS_NEW_LISTED_PLAYER_INFO) { List values = new ArrayList<>(); - if (mode != ChangeMode.DELETE && mode != ChangeMode.RESET) { + if (mode != ChangeMode.DELETE && mode != ChangeMode.RESET && mode != ChangeMode.REMOVE) { for (Object object : delta) { if (object instanceof Player) { Player player = (Player) object; @@ -124,7 +124,9 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { sample.addAll(values); break; case REMOVE: - sample.removeAll(values); + for (Object value : delta) { + sample.removeIf(profile -> profile.name().equals(value)); + } break; case DELETE: case RESET: @@ -135,7 +137,7 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { } List values = new ArrayList<>(); - if (mode != ChangeMode.DELETE && mode != ChangeMode.RESET) { + if (mode != ChangeMode.DELETE && mode != ChangeMode.RESET && mode != ChangeMode.REMOVE) { for (Object object : delta) { if (object instanceof Player) { Player player = (Player) object; @@ -150,13 +152,14 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { switch (mode) { case SET: sample.clear(); - sample.addAll(values); - break; + // $FALL-THROUGH$ case ADD: sample.addAll(values); break; case REMOVE: - sample.removeAll(values); + for (Object value : delta) { + sample.removeIf(profile -> profile.getName() != null && profile.getName().equals(value)); + } break; case DELETE: case RESET: diff --git a/src/main/java/ch/njol/skript/expressions/ExprItems.java b/src/main/java/ch/njol/skript/expressions/ExprItems.java index 652cb3f6a72..4fb0c11d66a 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprItems.java +++ b/src/main/java/ch/njol/skript/expressions/ExprItems.java @@ -1,21 +1,3 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.expressions; import ch.njol.skript.Skript; @@ -54,7 +36,7 @@ public class ExprItems extends SimpleExpression { private static final ItemType[] ALL_BLOCKS = Arrays.stream(Material.values()) - .filter(Material::isBlock) + .filter(material -> !material.isLegacy() && material.isBlock()) .map(ItemType::new) .toArray(ItemType[]::new); diff --git a/src/main/java/ch/njol/skript/expressions/ExprPlayerChatCompletions.java b/src/main/java/ch/njol/skript/expressions/ExprPlayerChatCompletions.java new file mode 100644 index 00000000000..7bf7d335214 --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprPlayerChatCompletions.java @@ -0,0 +1,93 @@ +package ch.njol.skript.expressions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.RequiredPlugins; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.util.coll.CollectionUtils; + +@Name("Player Chat Completions") +@Description({ + "The custom chat completion suggestions. You can add, set, remove, and clear them. Removing the names of online players with this expression is ineffective.", + "This expression will not return anything due to Bukkit limitations." +}) +@Examples({ + "add \"Skript\" and \"Njol\" to chat completions of all players", + "remove \"text\" from {_p}'s chat completions", + "clear player's chat completions" +}) +@RequiredPlugins("Spigot 1.19+") +@Since("INSERT VERSION") +public class ExprPlayerChatCompletions extends SimplePropertyExpression { + + static { + if (Skript.methodExists(Player.class, "addCustomChatCompletions", Collection.class)) + register(ExprPlayerChatCompletions.class, String.class, "[custom] chat completion[s]", "players"); + } + + @Override + public @Nullable String convert(Player player) { + return null; // Due to Bukkit limitations + } + + @Override + public @Nullable Class[] acceptChange(ChangeMode mode) { + return switch (mode) { + case ADD, SET, REMOVE, DELETE, RESET -> CollectionUtils.array(String[].class); + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + Player[] players = getExpr().getArray(event); + if (players.length == 0) + return; + List completions = new ArrayList<>(); + if (delta != null && (mode == ChangeMode.ADD || mode == ChangeMode.REMOVE || mode == ChangeMode.SET)) { + completions = Arrays.stream(delta) + .filter(String.class::isInstance) + .map(String.class::cast) + .collect(Collectors.toList()); + } + switch (mode) { + case DELETE, RESET, SET -> { + for (Player player : players) + player.setCustomChatCompletions(completions); + } + case ADD -> { + for (Player player : players) + player.addCustomChatCompletions(completions); + } + case REMOVE -> { + for (Player player : players) + player.removeCustomChatCompletions(completions); + } + } + } + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + protected String getPropertyName() { + return "custom chat completions"; + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java b/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java index 9cf189d5643..f9ae82447fd 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java +++ b/src/main/java/ch/njol/skript/expressions/ExprRandomNumber.java @@ -18,9 +18,11 @@ */ package ch.njol.skript.expressions; +import java.util.Arrays; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; +import ch.njol.skript.lang.Literal; import ch.njol.util.Math2; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -36,62 +38,90 @@ import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; -@Name("Random Number") +@Name("Random Numbers") @Description({ - "A random number or integer between two given numbers. Use 'number' if you want any number with decimal parts, or use use 'integer' if you only want whole numbers.", - "Please note that the order of the numbers doesn't matter, i.e. random number between 2 and 1 will work as well as random number between 1 and 2." + "A given amount of random numbers or integers between two given numbers. Use 'number' if you want any number with decimal parts, or use use 'integer' if you only want whole numbers.", + "Please note that the order of the numbers doesn't matter, i.e. random number between 2 and 1 will work as well as random number between 1 and 2." }) @Examples({ - "set the player's health to a random number between 5 and 10", - "send \"You rolled a %random integer from 1 to 6%!\" to the player" + "set the player's health to a random number between 5 and 10", + "send \"You rolled a %random integer from 1 to 6%!\" to the player", + "set {_chances::*} to 5 random integers between 5 and 96", + "set {_decimals::*} to 3 random numbers between 2.7 and -1.5" }) -@Since("1.4") +@Since("1.4, INSERT VERSION (Multiple random numbers)") public class ExprRandomNumber extends SimpleExpression { static { Skript.registerExpression(ExprRandomNumber.class, Number.class, ExpressionType.COMBINED, - "[a] random (:integer|number) (from|between) %number% (to|and) %number%"); + "[a|%-integer%] random (:integer|number)[s] (from|between) %number% (to|and) %number%"); } - private Expression from, to; + @Nullable + private Expression amount; + private Expression lower, upper; private boolean isInteger; @Override @SuppressWarnings("unchecked") - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parser) { - from = (Expression) exprs[0]; - to = (Expression) exprs[1]; - isInteger = parser.hasTag("integer"); + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + amount = (Expression) exprs[0]; + lower = (Expression) exprs[1]; + upper = (Expression) exprs[2]; + isInteger = parseResult.hasTag("integer"); return true; } @Override @Nullable protected Number[] get(Event event) { - Number from = this.from.getSingle(event); - Number to = this.to.getSingle(event); + Number lowerNumber = lower.getSingle(event); + Number upperNumber = upper.getSingle(event); + if (upperNumber == null || lowerNumber == null || !Double.isFinite(lowerNumber.doubleValue()) || !Double.isFinite(upperNumber.doubleValue())) + return new Number[0]; - if (to == null || from == null || !Double.isFinite(from.doubleValue()) || !Double.isFinite(to.doubleValue())) + Integer amount = this.amount == null ? Integer.valueOf(1) : this.amount.getSingle(event); + if (amount == null || amount <= 0) return new Number[0]; + double lower = Math.min(lowerNumber.doubleValue(), upperNumber.doubleValue()); + double upper = Math.max(lowerNumber.doubleValue(), upperNumber.doubleValue()); Random random = ThreadLocalRandom.current(); - double min = Math.min(from.doubleValue(), to.doubleValue()); - double max = Math.max(from.doubleValue(), to.doubleValue()); - if (isInteger) { - long inf = Math2.ceil(min); - long sup = Math2.floor(max); - if (max - min < 1 && inf - sup <= 1) { - if (sup == inf || min == inf) - return new Long[] {inf}; - if (max == sup) - return new Long[] {sup}; + Long[] longs = new Long[amount]; + long floored_upper = Math2.floor(upper); + long ceiled_lower = Math2.ceil(lower); + + // catch issues like `integer between 0.5 and 0.6` + if (upper - lower < 1 && ceiled_lower - floored_upper <= 1) { + if (floored_upper == ceiled_lower || lower == ceiled_lower) { + Arrays.fill(longs, ceiled_lower); + return longs; + } + if (upper == floored_upper) { + Arrays.fill(longs, floored_upper); + return longs; + } return new Long[0]; } - return new Long[] {inf + Math2.mod(random.nextLong(), sup - inf + 1)}; + + for (int i = 0; i < amount; i++) + longs[i] = Math2.ceil(lower) + Math2.mod(random.nextLong(), floored_upper - ceiled_lower + 1); + return longs; + // non-integers + } else { + Double[] doubles = new Double[amount]; + for (int i = 0; i < amount; i++) + doubles[i] = Math.min(lower + random.nextDouble() * (upper - lower), upper); + return doubles; } + } - return new Double[] {min + random.nextDouble() * (max - min)}; + @Override + public boolean isSingle() { + if (amount instanceof Literal) + return ((Literal) amount).getSingle() == 1; + return amount == null; } @Override @@ -101,12 +131,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - return "a random " + (isInteger ? "integer" : "number") + " between " + from.toString(event, debug) + " and " + to.toString(event, debug); - } - - @Override - public boolean isSingle() { - return true; + return (amount == null ? "a" : amount.toString(event, debug)) + " random " + (isInteger ? "integer" : "number") + + (amount == null ? "" : "s") + " between " + lower.toString(event, debug) + " and " + upper.toString(event, debug); } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprSkull.java b/src/main/java/ch/njol/skript/expressions/ExprSkull.java index bce8422a76d..8b8c0e0e19b 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprSkull.java +++ b/src/main/java/ch/njol/skript/expressions/ExprSkull.java @@ -1,86 +1,45 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.expressions; +import ch.njol.skript.bukkitutil.ItemUtils; import org.bukkit.Material; import org.bukkit.OfflinePlayer; -import org.bukkit.inventory.meta.SkullMeta; import org.jetbrains.annotations.Nullable; -import ch.njol.skript.Skript; -import ch.njol.skript.aliases.Aliases; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.util.Kleenean; -/** - * @author Peter Güttinger - */ @Name("Player Skull") @Description("Gets a skull item representing a player. Skulls for other entities are provided by the aliases.") -@Examples({"give the victim's skull to the attacker", - "set the block at the entity to the entity's skull"}) +@Examples({ + "give the victim's skull to the attacker", + "set the block at the entity to the entity's skull" +}) @Since("2.0") -public class ExprSkull extends SimplePropertyExpression { - +public class ExprSkull extends SimplePropertyExpression { + static { register(ExprSkull.class, ItemType.class, "(head|skull)", "offlineplayers"); } - - /** - * In 2017, SkullMeta finally got a method that takes OfflinePlayer. - */ - private static final boolean newSkullOwner = Skript.methodExists(SkullMeta.class, "setOwningPlayer", OfflinePlayer.class); - - @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { - return super.init(exprs, matchedPattern, isDelayed, parseResult); - } - - @SuppressWarnings("deprecation") + @Override - @Nullable - public ItemType convert(final Object o) { + public @Nullable ItemType convert(OfflinePlayer player) { ItemType skull = new ItemType(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) skull.getItemMeta(); - if (newSkullOwner) - meta.setOwningPlayer((OfflinePlayer) o); - else - meta.setOwner(((OfflinePlayer) o).getName()); - skull.setItemMeta(meta); + ItemUtils.setHeadOwner(skull, player); return skull; } - + @Override public Class getReturnType() { return ItemType.class; } - + @Override protected String getPropertyName() { return "skull"; } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprSkullOwner.java b/src/main/java/ch/njol/skript/expressions/ExprSkullOwner.java index ec80ad70793..bc102126022 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprSkullOwner.java +++ b/src/main/java/ch/njol/skript/expressions/ExprSkullOwner.java @@ -1,23 +1,6 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.expressions; +import ch.njol.skript.bukkitutil.ItemUtils; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -25,6 +8,7 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import ch.njol.util.coll.CollectionUtils; +import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.block.Block; import org.bukkit.block.BlockState; @@ -66,11 +50,20 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { OfflinePlayer offlinePlayer = (OfflinePlayer) delta[0]; for (Block block : getExpr().getArray(event)) { BlockState state = block.getState(); - if (state instanceof Skull) { - Skull skull = (Skull) state; + if (!(state instanceof Skull)) + continue; + + Skull skull = (Skull) state; + if (offlinePlayer.getName() != null) { skull.setOwningPlayer(offlinePlayer); - skull.update(true, false); + } else if (ItemUtils.CAN_CREATE_PLAYER_PROFILE) { + //noinspection deprecation + skull.setOwnerProfile(Bukkit.createPlayerProfile(offlinePlayer.getUniqueId(), "")); + } else { + //noinspection deprecation + skull.setOwner(""); } + skull.update(true, false); } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprTime.java b/src/main/java/ch/njol/skript/expressions/ExprTime.java index 4e769f6656d..e2199e18a80 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprTime.java +++ b/src/main/java/ch/njol/skript/expressions/ExprTime.java @@ -1,27 +1,5 @@ -/** - * 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 . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ package ch.njol.skript.expressions; -import org.bukkit.World; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - import ch.njol.skript.Skript; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; @@ -32,42 +10,51 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.Getter; import ch.njol.skript.util.Time; import ch.njol.skript.util.Timeperiod; import ch.njol.skript.util.Timespan; import ch.njol.util.Kleenean; import ch.njol.util.coll.CollectionUtils; +import org.bukkit.World; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; -/** - * @author Peter Güttinger - */ @Name("Time") -@Description("The time of a world.") -@Examples({"time in world is between 18:00 and 6:00:", - " broadcast \"It's night-time, watch out for monsters!\""}) +@Description({ + "The time of a world.", + "Use the \"minecraft timespan\" syntax to change the time according " + + "to Minecraft's time intervals.", + "Since Minecraft uses discrete intervals for time (ticks), " + + "changing the time by real-world minutes or real-world seconds only changes it approximately.", + "Removing an amount of time from a world's time will move the clock forward a day." +}) +@Examples({ + "set time of world \"world\" to 2:00", + "add 2 minecraft hours to time of world \"world\"", + "add 54 real seconds to time of world \"world\" # approximately 1 minecraft hour" +}) @Since("1.0") public class ExprTime extends PropertyExpression { + + // 18000 is the offset to allow for using "add 2:00" without going to a new day + // and causing unexpected behaviour + private static final int TIME_TO_TIMESPAN_OFFSET = 18000; + static { - Skript.registerExpression(ExprTime.class, Time.class, ExpressionType.PROPERTY, "[the] time[s] [([with]in|of) %worlds%]", "%worlds%'[s] time[s]"); + Skript.registerExpression(ExprTime.class, Time.class, ExpressionType.PROPERTY, + "[the] time[s] [([with]in|of) %worlds%]", "%worlds%'[s] time[s]"); } - - @SuppressWarnings({"unchecked", "null"}) + + @SuppressWarnings("unchecked") @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parser) { - setExpr((Expression) exprs[0]); + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parser) { + setExpr((Expression) expressions[0]); return true; } - + @Override - protected Time[] get(final Event e, final World[] source) { - return get(source, new Getter() { - @Override - public Time get(final World w) { - return new Time((int) w.getTime()); - } - }); + protected Time[] get(Event event, World[] worlds) { + return get(worlds, world -> new Time((int) world.getTime())); } @Override @@ -76,56 +63,62 @@ public Class[] acceptChange(final ChangeMode mode) { switch (mode) { case ADD: case REMOVE: - return CollectionUtils.array(Timespan.class); + // allow time to avoid conversion to timespan, which causes all sorts of headaches + return CollectionUtils.array(Time.class, Timespan.class); case SET: return CollectionUtils.array(Time.class, Timeperiod.class); - case DELETE: - case REMOVE_ALL: - case RESET: default: return null; } } - + @Override - public void change(final Event e, final @Nullable Object[] delta, final ChangeMode mode) { - final World[] worlds = getExpr().getArray(e); - int mod = 1; - switch (mode) { - case SET: - assert delta != null; - final int time = delta[0] instanceof Time ? ((Time) delta[0]).getTicks() : ((Timeperiod) delta[0]).start; - for (final World w : worlds) { - w.setTime(time); - } - break; - case REMOVE: - mod = -1; - //$FALL-THROUGH$ - case ADD: - assert delta != null; - final Timespan ts = (Timespan) delta[0]; - for (final World w : worlds) { - w.setTime(w.getTime() + mod * ts.getTicks()); - } - break; - case DELETE: - case REMOVE_ALL: - case RESET: - assert false; + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + if (getExpr() == null || delta == null) + return; + + Object time = delta[0]; + if (time == null) + return; + + World[] worlds = getExpr().getArray(event); + + long ticks = 0; + if (time instanceof Time) { + if (mode != ChangeMode.SET) { + ticks = ((Time) time).getTicks() - TIME_TO_TIMESPAN_OFFSET; + } else { + ticks = ((Time) time).getTicks(); + } + } else if (time instanceof Timespan) { + ticks = ((Timespan) time).getAs(Timespan.TimePeriod.TICK); + } else if (time instanceof Timeperiod) { + ticks = ((Timeperiod) time).start; + } + + for (World world : worlds) { + switch (mode) { + case ADD: + world.setTime(world.getTime() + ticks); + break; + case REMOVE: + world.setTime(world.getTime() - ticks); + break; + case SET: + world.setTime(ticks); + break; + } } } - + @Override public Class