diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..560dcad5af7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# .git-blame-ignore-revs + +# cleanup lang package - mostly renaming variables +98abf5d7773df11c65d66e4e9485d0f1e3ef8821 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 191b0e340d9..5b930003730 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,14 @@ version: 2 updates: - package-ecosystem: "github-actions" + target-branch: "dev/patch" directory: "/" schedule: interval: "weekly" labels: - "dependencies" - package-ecosystem: "gradle" + target-branch: "dev/patch" directory: "/" schedule: interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ce87d77a996..698a65a0554 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,6 +2,6 @@ --- -**Target Minecraft Versions:** -**Requirements:** -**Related Issues:** +**Target Minecraft Versions:** any +**Requirements:** none +**Related Issues:** none diff --git a/.github/workflows/archive-docs.yml b/.github/workflows/archive-docs.yml new file mode 100644 index 00000000000..e183f748d78 --- /dev/null +++ b/.github/workflows/archive-docs.yml @@ -0,0 +1,45 @@ +name: Archive documentation + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + archive-docs: + if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" + needs: release-docs + runs-on: ubuntu-latest + steps: + - name: Configure workflow + id: configuration + run: | + echo "BRANCH_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT + echo "DOCS_OUTPUT_DIR=${GITHUB_WORKSPACE}/skript-docs/docs/archives/${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + echo "DOCS_REPO_DIR=${GITHUB_WORKSPACE}/skript-docs" >> $GITHUB_OUTPUT + echo "SKRIPT_REPO_DIR=${GITHUB_WORKSPACE}/skript" >> $GITHUB_OUTPUT + - name: Checkout Skript + uses: actions/checkout@v4 + with: + submodules: recursive + path: skript + - name: Setup documentation environment + uses: ./skript/.github/workflows/docs/setup-docs + with: + docs_deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + - name: Generate documentation + uses: ./skript/.github/workflows/docs/generate-docs + with: + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + skript_repo_dir: ${{ steps.configuration.outputs.SKRIPT_REPO_DIR }} + is_release: true + generate_javadocs: true + - name: Push archive documentation + uses: ./skript/.github/workflows/docs/push-docs + with: + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + git_name: Archive Docs Bot + git_email: archivedocs@skriptlang.org + git_commit_message: "Update ${{ steps.configuration.outputs.BRANCH_NAME }} archive docs" diff --git a/.github/workflows/cleanup-docs.yml b/.github/workflows/cleanup-docs.yml new file mode 100644 index 00000000000..68d12e84dc1 --- /dev/null +++ b/.github/workflows/cleanup-docs.yml @@ -0,0 +1,39 @@ +name: Cleanup nightly documentation +on: delete +jobs: + cleanup-nightly-docs: + if: github.event.ref_type == 'branch' + runs-on: ubuntu-latest + steps: + - name: Configure workflow + id: configuration + env: + DELETED_BRANCH: ${{ github.event.ref }} + run: | + BRANCH_NAME="${DELETED_BRANCH#refs/*/}" + echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "DOCS_OUTPUT_DIR=${GITHUB_WORKSPACE}/skript-docs/docs/nightly/${BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "DOCS_REPO_DIR=${GITHUB_WORKSPACE}/skript-docs" >> $GITHUB_OUTPUT + - name: Checkout Skript + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + submodules: recursive + path: skript + - name: Setup documentation environment + uses: ./skript/.github/workflows/docs/setup-docs + with: + docs_deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + - name: Cleanup nightly documentation + env: + DOCS_OUTPUT_DIR: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + run: | + rm -rf ${DOCS_OUTPUT_DIR} || true + - name: Push nightly documentation cleanup + uses: ./skript/.github/workflows/docs/push-docs + with: + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + git_name: Nightly Docs Bot + git_email: nightlydocs@skriptlang.org + git_commit_message: "Delete ${{ steps.configuration.outputs.BRANCH_NAME }} branch nightly docs" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index e41c4bf7692..00000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Nightly Docs - -on: - push: - branches: - - master - - 'dev/**' - -jobs: - docs: - if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - path: skript - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'adopt' - cache: gradle - - name: Grant execute permission for gradlew - run: chmod +x ./skript/gradlew - - name: Checkout skript-docs - uses: actions/checkout@v3 - with: - repository: 'SkriptLang/skript-docs' - path: skript-docs - ssh-key: ${{ secrets.DOCS_DEPLOY_KEY }} - - name: Setup documentation environment - env: - DOCS_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }} - run: | - eval `ssh-agent` - DOC_DIR="${GITHUB_WORKSPACE}/skript-docs/docs/nightly/${GITHUB_REF#refs/heads/}" - rm -r ${DOC_DIR}/* || true - echo "DOC_DIR=${DOC_DIR}" >> $GITHUB_ENV - echo "SKRIPT_DOCS_TEMPLATE_DIR=${GITHUB_WORKSPACE}/skript-docs/doc-templates" >> $GITHUB_ENV - echo "SKRIPT_DOCS_OUTPUT_DIR=${DOC_DIR}/" >> $GITHUB_ENV - echo "$DOCS_DEPLOY_KEY" | tr -d '\r' | ssh-add - > /dev/null - mkdir ~/.ssh - ssh-keyscan www.github.com >> ~/.ssh/known_hosts - - name: Build Skript and generate docs - run: | - cd ./skript - ./gradlew clean genDocs javadoc - mv "${GITHUB_WORKSPACE}/skript/build/docs/javadoc" "${DOC_DIR}/javadocs" - cd .. - - name: Push nightly docs - if: success() - run: | - cd ./skript-docs - git config user.email "nightlydocs@skriptlang.org" - git config user.name "Nightly Docs Bot" - git add docs/nightly - git commit -m "Update nightly docs" || (echo "Nothing to push!" && exit 0) - git push origin main diff --git a/.github/workflows/docs/generate-docs/action.yml b/.github/workflows/docs/generate-docs/action.yml new file mode 100644 index 00000000000..2470f852450 --- /dev/null +++ b/.github/workflows/docs/generate-docs/action.yml @@ -0,0 +1,109 @@ +name: Generate documentation + +inputs: + docs_output_dir: + description: "The directory to generate the documentation into" + required: true + type: string + docs_repo_dir: + description: "The skript-docs repository directory" + required: true + type: string + skript_repo_dir: + description: "The skript repository directory" + required: true + type: string + is_release: + description: "Designates whether to generate nightly or release documentation" + required: false + default: false + type: boolean + cleanup_pattern: + description: "A pattern designating which files to delete when cleaning the documentation output directory" + required: false + default: "*" + type: string + generate_javadocs: + description: "Designates whether to generate javadocs for this nightly documentation" + required: false + default: false + type: boolean + +outputs: + DOCS_CHANGED: + description: "Whether or not the documentation has changed since the last push" + value: ${{ steps.generate.outputs.DOCS_CHANGED }} + +runs: + using: 'composite' + steps: + - name: generate-docs + id: generate + shell: bash + env: + DOCS_OUTPUT_DIR: ${{ inputs.docs_output_dir }} + DOCS_REPO_DIR: ${{ inputs.docs_repo_dir }} + SKRIPT_REPO_DIR: ${{ inputs.skript_repo_dir }} + IS_RELEASE: ${{ inputs.is_release }} + CLEANUP_PATTERN: ${{ inputs.cleanup_pattern }} + GENERATE_JAVADOCS: ${{ inputs.generate_javadocs }} + run: | + replace_in_directory() { + find $1 -type f -exec sed -i -e "s/$2/$3/g" {} \; + } + + # this should be replaced with a more reliable jq command, + # but it can't be right now because docs.json is actually not valid json. + get_skript_version_of_directory() { + grep skriptVersion "$1/docs.json" | cut -d\" -f 4 + } + + if [ -d "${DOCS_REPO_DIR}/docs/templates" ] + then + export SKRIPT_DOCS_TEMPLATE_DIR=${DOCS_REPO_DIR}/docs/templates + else # compatibility for older versions + export SKRIPT_DOCS_TEMPLATE_DIR=${DOCS_REPO_DIR}/doc-templates + fi + + export SKRIPT_DOCS_OUTPUT_DIR=/tmp/generated-docs + + cd $SKRIPT_REPO_DIR + if [[ "${IS_RELEASE}" == "true" ]]; then + ./gradlew genReleaseDocs javadoc + elif [[ "${GENERATE_JAVADOCS}" == "true" ]]; then + ./gradlew genNightlyDocs javadoc + else + ./gradlew genNightlyDocs + fi + + if [ -d "${DOCS_OUTPUT_DIR}" ]; then + if [[ "${GENERATE_JAVADOCS}" == "true" ]] || [[ "${IS_RELEASE}" == "true" ]] ; then + mkdir -p "${SKRIPT_DOCS_OUTPUT_DIR}/javadocs" && cp -a "./build/docs/javadoc/." "$_" + fi + + mkdir -p "/tmp/normalized-output-docs" && cp -a "${DOCS_OUTPUT_DIR}/." "$_" + mkdir -p "/tmp/normalized-generated-docs" && cp -a "${SKRIPT_DOCS_OUTPUT_DIR}/." "$_" + + output_skript_version=$(get_skript_version_of_directory "/tmp/normalized-output-docs") + generated_skript_version=$(get_skript_version_of_directory "/tmp/normalized-generated-docs") + + replace_in_directory "/tmp/normalized-output-docs" "${output_skript_version}" "Skript" + replace_in_directory "/tmp/normalized-generated-docs" "${generated_skript_version}" "Skript" + + diff -qbr /tmp/normalized-output-docs /tmp/normalized-generated-docs || diff_exit_code=$? + # If diff exits with exit code 1, that means there were some differences + if [[ ${diff_exit_code} -eq 1 ]]; then + echo "DOCS_CHANGED=true" >> $GITHUB_OUTPUT + echo "Documentation has changed since last push" + else + echo "Documentation hasn't changed since last push" + fi + else + echo "DOCS_CHANGED=true" >> $GITHUB_OUTPUT + echo "No existing documentation found" + fi + + rm -rf ${DOCS_OUTPUT_DIR}/${CLEANUP_PATTERN} || true + mkdir -p "${DOCS_OUTPUT_DIR}/" && cp -a "${SKRIPT_DOCS_OUTPUT_DIR}/." "$_" + + diff --git a/.github/workflows/docs/push-docs/action.yml b/.github/workflows/docs/push-docs/action.yml new file mode 100644 index 00000000000..477a4c46aa4 --- /dev/null +++ b/.github/workflows/docs/push-docs/action.yml @@ -0,0 +1,43 @@ +name: Push documentation + +inputs: + docs_repo_dir: + description: "The skript-docs repository directory" + required: true + type: string + git_email: + description: "The email to use for the Git commit" + required: true + type: string + git_name: + description: "The name to use for the Git commit" + required: true + type: string + git_commit_message: + description: "The message to use for the Git commit" + required: true + type: string + +runs: + using: 'composite' + steps: + - shell: bash + if: success() + env: + DOCS_REPO_DIR: ${{ inputs.docs_repo_dir }} + GIT_EMAIL: ${{ inputs.git_email }} + GIT_NAME: ${{ inputs.git_name }} + GIT_COMMIT_MESSAGE: ${{ inputs.git_commit_message }} + run: | + cd "${DOCS_REPO_DIR}" + git config user.name "${GIT_NAME}" + git config user.email "${GIT_EMAIL}" + git add -A + git commit -m "${GIT_COMMIT_MESSAGE}" || (echo "Nothing to push!" && exit 0) + # Attempt rebasing and pushing 5 times in case another job pushes before us + for i in 1 2 3 4 5 + do + git pull --rebase -X theirs origin main + git push origin main && break + sleep 5 + done diff --git a/.github/workflows/docs/setup-docs/action.yml b/.github/workflows/docs/setup-docs/action.yml new file mode 100644 index 00000000000..3f9fe050f47 --- /dev/null +++ b/.github/workflows/docs/setup-docs/action.yml @@ -0,0 +1,43 @@ +name: Setup documentation environment + +inputs: + docs_deploy_key: + description: "Deploy key for the skript-docs repo" + required: true + type: string + docs_output_dir: + description: "The directory to generate the documentation into" + required: true + type: string + cleanup_pattern: + description: "A pattern designating which files to delete when cleaning the documentation output directory" + required: false + default: "*" + type: string + +runs: + using: 'composite' + steps: + - name: Checkout skript-docs + uses: actions/checkout@v3 + with: + repository: 'SkriptLang/skript-docs' + path: skript-docs + ssh-key: ${{ inputs.docs_deploy_key }} + - uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'adopt' + cache: gradle + - shell: bash + run: chmod +x ./skript/gradlew + - shell: bash + env: + DOCS_DEPLOY_KEY: ${{ inputs.docs_deploy_key }} + DOCS_OUTPUT_DIR: ${{ inputs.docs_output_dir }} + CLEANUP_PATTERN: ${{ inputs.cleanup_pattern }} + run: | + eval `ssh-agent` + echo "$DOCS_DEPLOY_KEY" | tr -d '\r' | ssh-add - > /dev/null + mkdir ~/.ssh + ssh-keyscan www.github.com >> ~/.ssh/known_hosts diff --git a/.github/workflows/java-11-builds.yml b/.github/workflows/java-11-builds.yml new file mode 100644 index 00000000000..443aa925d0c --- /dev/null +++ b/.github/workflows/java-11-builds.yml @@ -0,0 +1,35 @@ +name: Java 11 CI (MC 1.13-1.16) + +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: Build Skript and run test scripts + run: ./gradlew clean skriptTestJava11 + - name: Upload Nightly Build + uses: actions/upload-artifact@v4 + if: success() + with: + name: skript-nightly + path: build/libs/* diff --git a/.github/workflows/java-17-builds.yml b/.github/workflows/java-17-builds.yml index 67007d76d90..2ca6d6bde61 100644 --- a/.github/workflows/java-17-builds.yml +++ b/.github/workflows/java-17-builds.yml @@ -1,4 +1,4 @@ -name: Java 17 CI (MC 1.17+) +name: Java 17 CI (MC 1.17-1.20.4) on: push: @@ -12,13 +12,15 @@ jobs: if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: gradle - name: Grant execute permission for gradlew @@ -26,7 +28,7 @@ jobs: - name: Build Skript and run test scripts run: ./gradlew clean skriptTestJava17 - name: Upload Nightly Build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: skript-nightly diff --git a/.github/workflows/java-8-builds.yml b/.github/workflows/java-21-builds.yml similarity index 64% rename from .github/workflows/java-8-builds.yml rename to .github/workflows/java-21-builds.yml index adcd71fb09b..6c82f22ce78 100644 --- a/.github/workflows/java-8-builds.yml +++ b/.github/workflows/java-21-builds.yml @@ -1,4 +1,4 @@ -name: Java 8 CI (MC 1.13-1.16) +name: Java 21 CI (MC 1.20.6+) on: push: @@ -12,21 +12,23 @@ jobs: if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build Skript and run test scripts - run: ./gradlew clean skriptTestJava8 + run: ./gradlew clean skriptTestJava21 - name: Upload Nightly Build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: skript-nightly diff --git a/.github/workflows/junit-8-builds.yml b/.github/workflows/junit-11-builds.yml similarity index 64% rename from .github/workflows/junit-8-builds.yml rename to .github/workflows/junit-11-builds.yml index 93971c79624..cdf75e4c43a 100644 --- a/.github/workflows/junit-8-builds.yml +++ b/.github/workflows/junit-11-builds.yml @@ -12,16 +12,18 @@ jobs: if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build Skript and run JUnit - run: ./gradlew clean JUnitJava8 + run: ./gradlew clean JUnitJava11 diff --git a/.github/workflows/junit-17-builds.yml b/.github/workflows/junit-17-builds.yml index eca1ebb9706..238bd9b132e 100644 --- a/.github/workflows/junit-17-builds.yml +++ b/.github/workflows/junit-17-builds.yml @@ -1,4 +1,4 @@ -name: JUnit (MC 1.17+) +name: JUnit (MC 1.17-1.20.4) on: push: @@ -12,13 +12,15 @@ jobs: if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: gradle - name: Grant execute permission for gradlew diff --git a/.github/workflows/junit-21-builds.disabled b/.github/workflows/junit-21-builds.disabled new file mode 100644 index 00000000000..07bc5bebbeb --- /dev/null +++ b/.github/workflows/junit-21-builds.disabled @@ -0,0 +1,31 @@ +# Disabled as EasyMock 5.2.0 is required for Java 21 support +# However, we are currently using 5.0.1 (see https://github.com/SkriptLang/Skript/pull/6204#discussion_r1405302009) +name: JUnit (MC 1.20.6+) + +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: Build Skript and run JUnit + run: ./gradlew clean JUnitJava21 diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml new file mode 100644 index 00000000000..0d5cc824ad0 --- /dev/null +++ b/.github/workflows/nightly-docs.yml @@ -0,0 +1,61 @@ +name: Nightly documentation + +on: + push: + branches: + - 'dev/feature' + - 'dev/patch' + - 'enhancement/**' + - 'feature/**' + - 'fix/**' + tags-ignore: + - '**' + +jobs: + nightly-docs: + if: "!contains(toJSON(github.event.commits.*.message), '[ci skip]')" + runs-on: ubuntu-latest + steps: + - name: Configure workflow + id: configuration + env: + DOCS_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }} + run: | + if [ -n "$DOCS_DEPLOY_KEY" ] + then + echo "DOCS_DEPLOY_KEY_PRESENT=true" >> $GITHUB_OUTPUT + else + echo "Secret 'DOCS_DEPLOY_KEY' not present. Exiting job." + fi + BRANCH_NAME="${GITHUB_REF#refs/*/}" + echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "DOCS_OUTPUT_DIR=${GITHUB_WORKSPACE}/skript-docs/docs/nightly/${BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "DOCS_REPO_DIR=${GITHUB_WORKSPACE}/skript-docs" >> $GITHUB_OUTPUT + echo "SKRIPT_REPO_DIR=${GITHUB_WORKSPACE}/skript" >> $GITHUB_OUTPUT + - name: Checkout Skript + uses: actions/checkout@v4 + with: + submodules: recursive + path: skript + - name: Setup documentation environment + if: steps.configuration.outputs.DOCS_DEPLOY_KEY_PRESENT == 'true' + uses: ./skript/.github/workflows/docs/setup-docs + with: + docs_deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + - name: Generate documentation + id: generate + if: steps.configuration.outputs.DOCS_DEPLOY_KEY_PRESENT == 'true' + uses: ./skript/.github/workflows/docs/generate-docs + with: + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + skript_repo_dir: ${{ steps.configuration.outputs.SKRIPT_REPO_DIR }} + - name: Push nightly documentation + if: steps.generate.outputs.DOCS_CHANGED == 'true' + uses: ./skript/.github/workflows/docs/push-docs + with: + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + git_name: Nightly Docs Bot + git_email: nightlydocs@skriptlang.org + git_commit_message: "Update ${{ steps.configuration.outputs.BRANCH_NAME }} branch nightly docs to ${{ github.repository }}@${{ github.sha }}" diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml new file mode 100644 index 00000000000..9cfe198ba93 --- /dev/null +++ b/.github/workflows/release-docs.yml @@ -0,0 +1,45 @@ +name: Release documentation + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + release-docs: + if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" + runs-on: ubuntu-latest + steps: + - name: Configure workflow + id: configuration + run: | + echo "BRANCH_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT + echo "DOCS_OUTPUT_DIR=${GITHUB_WORKSPACE}/skript-docs/docs/" >> $GITHUB_OUTPUT + echo "DOCS_REPO_DIR=${GITHUB_WORKSPACE}/skript-docs" >> $GITHUB_OUTPUT + echo "SKRIPT_REPO_DIR=${GITHUB_WORKSPACE}/skript" >> $GITHUB_OUTPUT + - name: Checkout Skript + uses: actions/checkout@v4 + with: + submodules: recursive + path: skript + - name: Setup documentation environment + uses: ./skript/.github/workflows/docs/setup-docs + with: + docs_deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + - name: Generate documentation + uses: ./skript/.github/workflows/docs/generate-docs + with: + docs_output_dir: ${{ steps.configuration.outputs.DOCS_OUTPUT_DIR }} + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + skript_repo_dir: ${{ steps.configuration.outputs.SKRIPT_REPO_DIR }} + is_release: true + generate_javadocs: true + cleanup_pattern: "!(nightly|archives|templates)" + - name: Push release documentation + uses: ./skript/.github/workflows/docs/push-docs + with: + docs_repo_dir: ${{ steps.configuration.outputs.DOCS_REPO_DIR }} + git_name: Release Docs Bot + git_email: releasedocs@skriptlang.org + git_commit_message: "Update release docs to ${{ steps.configuration.outputs.BRANCH_NAME }}" diff --git a/.github/workflows/repo.yml b/.github/workflows/repo.yml index d7639b98a94..a04327ed64c 100644 --- a/.github/workflows/repo.yml +++ b/.github/workflows/repo.yml @@ -8,13 +8,15 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Set up JDK 17 - uses: actions/setup-java@v3 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: gradle - name: Publish Skript diff --git a/.gitignore b/.gitignore index fbddca8fd46..47df9cbc0de 100755 --- a/.gitignore +++ b/.gitignore @@ -223,3 +223,6 @@ gradle-app.setting # TODO remove this in the future after some time since https://github.com/SkriptLang/Skript/pull/4979 # This ensures developers don't upload their old existing test_runners/ folder. test_runners/ + +## Mac MetaData +**/.DS_Store diff --git a/CLOCKWORK_RELEASE_MODEL.md b/CLOCKWORK_RELEASE_MODEL.md new file mode 100644 index 00000000000..e5e086f62eb --- /dev/null +++ b/CLOCKWORK_RELEASE_MODEL.md @@ -0,0 +1,294 @@ +# Clockwork Release Model + +## Table of Contents +1. [Introduction](#introduction) + - [Preamble](#preamble) + - [Motivations](#motivations) + - [Goals](#goals) + - [Non-Goals](#non-goals) +2. [Release Types](#release-types) + - [Feature Releases](#feature-releases) + - [Patch Releases](#patch-releases) + - [Pre-Releases](#pre-releases) + - [Emergency Patch Releases](#emergency-patch-releases) +3. [Timetable](#timetable) + - [Major Version Schedule](#major-version-schedule) + - [Pre-Release Schedule](#pre-release-schedule) + - [Patch Schedule](#patch-schedule) +4. [Content Curation](#content-curation) + - [Labels](#labels) + - [Branches](#branches) +5. [Conclusion](#conclusion) + - [Paradigm Versions](#addendum-1-paradigm-versions) + - [Failure Standards](#addendum-2-failure-standards) + +## Introduction + +### Preamble + +This document defines the structure of future Skript releases, the kinds of material included in releases and the outline of the dates on which releases will be published. + +A 'release' is the publication of a verified and signed build artefact on the GitHub releases tab, made available for all users to download and install. + +This document does *not* cover the distribution or publication of artifacts built in other ways (e.g. privately, from a nightly action) or those not published from our GitHub (e.g. test builds shared in our public testing group). + +Plans for a new release model began in March 2023 and several models were discussed, with this being the final version agreed upon by the organisation's administrative group and approved by the core contributors. + +### Motivations + +The release cycle for the `2.7.0` version was significant in that it took an unusually long time and included an unusually-large number of additions and changes. + +While it was not the first version to have taken a long time to finalise and produce, it was distinct in that a lot of time had passed since the previous public build of Skript having been marked stable. + +Members of the organisation and the wider community identified several problems that resulted from this, some of which are (not exhaustively) detailed below: +- 291 days had passed since the previous release + - Users were unable to benefit from bug fixes or new features produced during that time + - Although beta versions were released, these were marked unstable and were not fully tested +- When the release arrived it contained a very large number of changes and additions + - Some users were unaware of changes that could not be extensively documented in the changelog or were buried in a large list + - Users who obtained a build elsewhere (e.g. direct download, automatic installer) may have been unaware of the scale of the changes +- Several additions were made at short notice and without sufficient testing + - Some of these introduced problems that required fixes in a following `2.7.1` patch +- Several features could not be completed in time and had to be dropped to a future `2.8.0` version + - One result of this was that any corrections or improvements made as part of these were not present in `2.7.0` + - Aspects of some of these larger-scale changes had to be re-created or cherry-picked for the `2.7.0` version +- The release lacked a clear timetable or vision for what additions to include + - The initial timetable was not adhered to; plans were made for pre-releases to begin at the end of November 2022, which was delayed to early December in order to accommodate a large feature PR (which was eventually dropped to `2.8.0`) + - Delays persisted, and the final release took place 7 months later in September 2023 + - There was no clear cut-off point for new features; feature pull requests were being included even up to 3 days before release + +Of these, the principle complaint is that the `2.7.0` version took a significant amount of time to finish and this had an adverse effect on the community and the wider ecosystem. + +### Goals + +Our release model has been designed to achieve the following goals: +1. To reduce the delay between finishing new features and releasing them to the public. +2. To significantly reduce the time between an issue being fixed and that fix being made public in a stable build. +3. To reduce the risk of untested changes going into a release. +4. To make the release timetable clear and accessible. +5. To prevent a version being indefinitely delayed to accommodate additional changes. + +### Non-Goals + +This release model is not intended to change any of the following: +- The content or feature-theme of a particular version. +- The process for reviewing or approving changes. + +## Release Types + +This section details the different categories of version for release. + +The versioning will follow the form `A.B.C`, where `B` is a [feature version](#Feature-Releases) containing changes and additions, `C` is a [patch version](#Patch-Releases) containing only issue fixes, and `A` is reserved for major paradigmatic changes. + +### Feature Releases +A 'feature' version (labelled `0.X.0`) may contain: +- Additions to the language (syntax, grammar, structures) +- Bug fixes +- Developer API additions and changes +- Breaking changes1 + +All content added to a feature version must pass through the typical review process. +Content must also have been included in a prior [pre-release](#Pre-Releases) for public testing. + +> 1 Breaking changes are to be avoided where possible but may be necessary, such as in a case where a significant improvement could be made to an existing feature but only by changing its structure somehow. +> Such changes should be clearly labelled and well documented, preferably giving users ample notice. + +### Patch Releases +A 'patch' version (labelled `0.0.X`) may contain: +- Bug fixes +- Non-impactful2 improvements to existing features +- Changes to meta content (e.g. documentation) + +There may be **very rare** occasions when a breaking change is necessary in a patch release. These may occur if and only if: either a breaking change is required in order to fix an issue, and the issue is significant enough to need fixing in a patch rather than waiting for a major release, or an issue occurred with an inclusion in the version immediately-prior to this, which must be changed or reverted in some way. + +All content added to a patch version must pass through the typical review process. + +> 2 A non-impactful change is one in which there is no apparent difference to the user in how a feature is employed or what it does but that may have a material difference in areas such as performance, efficiency or machine resource usage. + +### Pre-Releases + +A 'pre-release' version (labelled `0.X.0-preY`) will contain all of the content expected to be in the feature release immediately following this. + +Pre-release versions are a final opportunity for testing and getting public feedback on changes before a major release, allowing time to identify and fix any issues before the proper release, rather than needing an immediate patch. + +The content of a pre-release should be identical to the content of the upcoming release -- barring any bug fixes -- and new content should never be included after a pre-release. + +### Emergency Patch Releases + +An 'emergency patch' version will be released if a critical security vulnerability is reported that the organisation feels prevents an immediate risk to the user base, such that it cannot wait for the subsequent patch. + +An emergency patch will be labelled as another patch version (`0.0.X`). It should be noted that an emergency patch will *not* disrupt the typical timetable detailed below. + +These kinds of releases may be published immediately and do not have to go through the typical reviewing and testing process. \ +They must never include content, additions or unnecessary changes. + +The only content permitted in an emergency patch is the material needed to fix the security risk. + +The exact nature of the security vulnerability (such as the means to reproduce it) should not be included in the notes surrounding the release. + +## Timetable + +The 'clockwork' release model follows a strict monthly cycle, with versions being released on exact dates. + +A table of (expected) dates is displayed below. + +| Date | Release Type | Example Version
Name | +|----------|-----------------|-------------------------| +| 1st Jan | Pre-release | 0.1.0-pre1 | +| 15th Jan | Feature release | 0.1.0 | +| 1st Feb | Patch | 0.1.1 | +| 1st Mar | Patch | 0.1.2 | +| 1st Apr | Patch | 0.1.3 | +| 1st May | Patch | 0.1.4 | +| 1st Jun | Patch | 0.1.5 | +| 1st Jul | Pre-release | 0.2.0-pre1 | +| 15th Jul | Feature release | 0.2.0 | +| 1st Aug | Patch | 0.2.1 | +| 1st Sep | Patch | 0.2.2 | +| 1st Oct | Patch | 0.2.3 | +| 1st Nov | Patch | 0.2.4 | +| 1st Dec | Patch | 0.2.5 | + +An estimated 14 releases are expected per year, with 10 patches, 2 pre-releases and 2 feature-releases that immediately follow them. + +Please note that the actual number may differ from this in cases such as: +- A version requiring multiple pre-releases to correct mistakes (`0.3.0-pre1`, `0.3.0-pre2`) +- An emergency patch having to be released +- No bug fixes being prepared in a month, meaning no patch is needed + +There is no fixed timetable for the circulation of unpublished builds to the public testing group or the addon developers group. + +### Major Version Schedule + +A [feature version](#feature-releases) will be released on the **15th of January** and the **15th of July**. + +This will include all finished content from the previous 6 months that was tested in the pre-release. + +Any features, additions or changes that were *not* ready or approved at the time of the pre-release may **not** be included in the feature release [according to goal 3](#goals). \ +The feature release must **not** be delayed to accomodate content that was not ready by the deadline [according to goal 5](#goals). + +If there is no content ready at the scheduled date of a feature release, the release will be skipped and a notice published explaining this. + +### Pre-Release Schedule + +A [pre-release](#pre-releases) will be released on the **1st of January** and the **1st of July**, leaving two weeks before the following release for public testing to occur. + +This pre-release may include all finished content from the previous 6 months. + +Any features, additions or changes that have *not* passed the review/approval process by the day of the pre-release may **not** be included in the pre-release [according to goal 3](#goals). \ +The pre-release must **not** be delayed to accomodate content that was not ready by the deadline [according to goal 5](#goals). + +If there is no content ready at the scheduled date of a pre-release, the entire feature-release will be skipped and a notice published explaining this. + +If issues are found requiring a new build be produced (e.g. the build fails to load, a core feature is non-functional, a fix was made but needs additional testing) then another version of the pre-release may be published. +There is no limit on the number of pre-releases that can be published if required. + +### Patch Schedule + +A [patch](#patch-releases) will be released on the **1st** of every month (except January and July) containing any fixes prepared during the previous month(s). + +On the 1st of January and July the patch will be replaced by the pre-release. + +A patch should include all bug fixes from the previous month that have passed the review/approval process. + +Ideally, a patch build should be circulated in the public testing group prior to its release, but this is not a strict requirement. + +If there are no applicable bug fixes ready by the scheduled date of the patch then the month will be skipped and the patch will not take place. A public notice is not required to explain this. + +## Content Curation + +To help curate content on our GitHub repository we have designed a new branch model and accompanying labels for categorising contributions. + +### Labels + +We shall provide issue and pull request labels to help categorise changes to prevent contributions missing a release (or slipping into the incorrect kind of release). + +1. `patch-ready` \ + to denote a pull request that has: + - passed the review/approval process + - is of the sufficient kind to be included in a monthly patch version +2. `feature-ready` \ + to denote a pull request that has: + - passed the review/approval process + - should wait for a biannual feature release + - is not suitable to be included in a patch + +### Branches + +We shall maintain three core branches: `dev/patch`, `dev/feature` and `master`, which function vertically3. + +We may also create legacy branches where necessary. \ +As an example, if a previous release, say `2.6.4` requires an emergency security update, a branch can be made from its release tag and the patch may directly target that branch (and be released). + +We may also maintain other assorted branches for individual features, for the purpose of group work or for experimentation. These are not detailed below. + +> 3 Changes are always made to the 'top' (working) branch and then this is merged downwards into the more stable branch below when required. +> +> Branches are never merged upwards. + +#### Patch + +Pull requests that only address issues or are otherwise suitable for a patch release should target the **`dev/patch` branch**. These may be merged whenever appropriate (i.e. all review and testing requirements have passed). + +At the time of the patch release, the **patch branch** will be merged downwards into the **master branch**, and a release will be created from the **master branch**. + +When a feature release occurs and all branches are merged into the master branch, the patch branch will be rebased off the current master commit, effectively bringing it up to speed with the new changes. \ +As an example, when feature version 0.5.0 releases, the patch branch will be at 0.4.5 and missing the new features, so must be rebased off the current release and catch up before changes for version 0.5.1 may be merged. + +#### Feature + +Pull requests that add features, make breaking changes or are otherwise unsuitable for a patch version should target the **`dev/feature` branch**. \ +These should be merged whenever appropriate (i.e. all review and testing requirements have passed), so that testing builds can be created and circulated in the public testing group. + +The **patch branch** may be merged downwards into the **feature branch** whenever appropriate (e.g. after changes have been made to it that may affect the state of contributions targeting the feature branch). + +The feature branch should __**never**__ be merged upwards into the patch branch4. + +The feature branch is only merged downwards into the master branch directly before a full feature release (i.e. after the pre-release and testing is complete.) + +Pre-releases are made directly from the feature branch5. At the end of the pre-release testing period the feature branch can be merged downwards into the master branch in order for the full release to be made. + +> 4 Merges only ever occur downwards. For the patch branch to see changes from the feature branch it must be rebased onto master branch after a feature release occurs. +> +> 5 Merging the branch down for the pre-release would introduce potentially-buggy, untested changes to the master branch. + +#### Master + +The **`master` branch** should reflect the most recent feature release. +Pull requests should **never** directly target the master branch. Changes are made to one of the other branches (as applicable) and then that branch is merged downwards into the **master branch** only when it is time for a release. + +This means that any user building from the master branch is guaranteed to receive a safe, stable build of the quality that we would release. + +The master branch should never be merged upwards into the feature or patch branches. If these branches need to see changes from the master branch they must be rebased onto the latest master branch commit. + +## Conclusion + +It is our aim that this release model will address all of our goals and satisfy our motivations. + +Setting a strict and regular schedule ought to prevent too much time passing without a release, while also helping to prevent a single release from becoming bloated and overbearing. + +By including testing requirements and mandating public pre-releases we hope to solve the persistent issue of untested changes slipping into supposedly-stable versions. + +Finally, by scheduling regular patches we aim to reduce the time between a bug being 'fixed' by a contributor and the userbase being able to benefit from that fix. Keeping these patches as small, controlled releases allows us to mark them as 'stable' therefore letting the version reach a wider audience. + +### Addendum 1: Paradigm Versions + +Paradigmatic `X.0.0` versions were deliberately excluded from this proposal. \ +The reasoning behind this choice was that over 10 years have passed since the inception of major version`2.0.0` in 2012, the previous paradigmatic change. + +As of writing this document there are proposals and roadmaps for a version `3.0.0` but no timetable or predicted date on the horizon. + +This kind of version, were it to be released, would likely take the place of a typical feature release in the model calendar, i.e. occurring on the 15th of January or July. However, due to its potentially-significant nature it may require exceptional changes to the pre-release cycle. + +As details of such a version are neither known nor easy to predict, it has been left to the discretion of the future team to be decided when required. + +### Addendum 2: Failure Standards + +No proposal is complete without failure standards; this model can be deemed to have failed if, in two years' time: +1. The delay between finishing new features and releasing them to the public has not been reduced. +2. The delay between an issue being fixed and that fix being made public in a stable build has not been reduced. +3. Untested features are being released in 'stable' builds. +4. The release timetable is unclear or inaccessible. +5. Versions are being indefinitely delayed to accommodate additional changes. + +Additionally, if this model is considered to have put an undue burden on the core development team, to the extent that it has hampered progress in a significant and measurable way, then it can be considered to have failed. diff --git a/README.md b/README.md index e23e1a288d2..b4ab6c4e48f 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,18 @@ Skript requires **Spigot** to work. You heard it right, **CraftBukkit** does *no **Paper**, which is a fork of Spigot, is recommended; it is required for some parts of Skript to be available. -Skript supports only the **latest** patch versions of Minecraft 1.9+. +Skript supports only the **latest** patch versions of Minecraft 1.13+. For example, this means that 1.16.5 is supported, but 1.16.4 is *not*. Testing with all old patch versions is not feasible for us. -Minecraft 1.8 and earlier are not, and will not be supported. New Minecraft +Minecraft 1.12 and earlier are not, and will not be supported. New Minecraft versions will be supported as soon as possible. ## Download You can find the downloads for each version with their release notes in the [releases page](https://github.com/SkriptLang/Skript/releases). +Two major feature updates are expected each year in January and July, with monthly patches occurring in between. For full details, please review our [release model](CLOCKWORK_RELEASE_MODEL.md). + ## Documentation Documentation is available [here](https://docs.skriptlang.org/) for the latest version of Skript. @@ -75,13 +77,15 @@ Skript has some tests written in Skript. Running them requires a Minecraft server, but our build script will create one for you. Running the tests is easy: ``` -./gradlew (quickTest|skriptTest|skriptTestJava8|skriptTestJava17) +./gradlew (quickTest|skriptTest|skriptTestJava11|skriptTestJava17|skriptTestJava21) ``` quickTest runs the test suite on newest supported server version. -skriptTestJava17 (1.17+) runs the tests on the latest supported Java version. -skriptTestJava8 (1.13-1.16) runs the tests on the oldest supported Java version. -skriptTest runs both skriptTestJava8 and skriptTestJava17 +skriptTestJava21 (1.20.6+) runs the tests on Java 21 supported versions. +skriptTestJava17 (1.17-1.20.4) runs the tests on Java 17 supported versions. +skriptTestJava11 (1.13-1.16) runs the tests on Java 11 supported versions. +skriptTest runs the tests on all versions. +That is, it runs skriptTestJava11, skriptTestJava17, and skriptTestJava21. By running the tests, you agree to Mojang's End User License Agreement. @@ -160,7 +164,7 @@ dependencies { } ``` -An example of the version tag would be ```dev37c```. +An example of the version tag would be ```2.8.5```. > Note: If Gradle isn't able to resolve Skript's dependencies, just [disable the resolution of transitive dependencies](https://docs.gradle.org/current/userguide/resolution_rules.html#sec:disabling_resolution_transitive_dependencies) for Skript in your project. diff --git a/build.gradle b/build.gradle index 4ed4e1603be..486220cf22e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,6 @@ import java.time.LocalTime plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'com.github.hierynomus.license' version '0.16.1' id 'maven-publish' id 'java' } @@ -20,6 +19,7 @@ allprojects { maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url 'https://repo.papermc.io/repository/maven-public/' } + maven { url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } // needed for paper adventure snapshot maven { url 'https://ci.emc.gs/nexus/content/groups/aikar/' } } } @@ -27,20 +27,20 @@ allprojects { dependencies { shadow group: 'io.papermc', name: 'paperlib', version: '1.0.8' shadow group: 'org.bstats', name: 'bstats-bukkit', version: '3.0.2' - shadow group: 'net.kyori', name: 'adventure-text-serializer-bungeecord', version: '4.3.0' + shadow group: 'net.kyori', name: 'adventure-text-serializer-bungeecord', version: '4.3.2' - implementation group: 'io.papermc.paper', name: 'paper-api', version: '1.19.4-R0.1-SNAPSHOT' + implementation group: 'io.papermc.paper', name: 'paper-api', version: '1.21-R0.1-SNAPSHOT' implementation group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.annotation', version: '2.2.700' implementation group: 'com.google.code.findbugs', name: 'findbugs', version: '3.0.1' implementation group: 'com.sk89q.worldguard', name: 'worldguard-legacy', version: '7.0.0-SNAPSHOT' - implementation group: 'net.milkbowl.vault', name: 'Vault', version: '1.7.1', { + implementation group: 'net.milkbowl.vault', name: 'Vault', version: '1.7.3', { exclude group: 'org.bstats', module: 'bstats-bukkit' } implementation fileTree(dir: 'lib', include: '*.jar') testShadow group: 'junit', name: 'junit', version: '4.13.2' - testShadow group: 'org.easymock', name: 'easymock', version: '5.1.0' + testShadow group: 'org.easymock', name: 'easymock', version: '5.0.1' } task checkAliases { @@ -54,23 +54,23 @@ task checkAliases { } task testJar(type: ShadowJar) { - dependsOn(compileTestJava, licenseTest) + dependsOn(compileTestJava) archiveFileName = 'Skript-JUnit.jar' from sourceSets.test.output, sourceSets.main.output, project.configurations.testShadow } task jar(overwrite: true, type: ShadowJar) { dependsOn checkAliases - archiveFileName = jarName ? 'Skript.jar' : jarName + archiveFileName = jarName ? 'Skript-' + project.version + '.jar' : jarName from sourceSets.main.output } task build(overwrite: true, type: ShadowJar) { - archiveFileName = jarName ? 'Skript.jar' : jarName + archiveFileName = jarName ? 'Skript-' + project.version + '.jar' : jarName from sourceSets.main.output } -// Excludes the tests for the build task. Should be using junit, junitJava17, junitJava8, skriptTest, quickTest. +// Excludes the tests for the build task. Should be using junit, junitJava17, junitJava11, skriptTest, quickTest. // We do not want tests to run for building. That's time consuming and annoying. Especially in development. test { exclude '**/*' @@ -140,23 +140,6 @@ publishing { } } -license { - header file('licenseheader.txt') - exclude('**/Metrics.java') // Not under GPLv3 - exclude('**/BurgerHelper.java') // Not exclusively GPLv3 - exclude('**/*.sk') // Sample scripts and maybe aliases - exclude('**/*.lang') // Language files do not have headers (still under GPLv3) - exclude('**/*.json') // JSON files do not have headers -} - -javadoc { - source = sourceSets.main.allJava - classpath = configurations.compileClasspath - options.encoding = 'UTF-8' - // currently our javadoc has a lot of errors, so we need to suppress the linter - options.addStringOption('Xdoclint:none', '-quiet') -} - // Task to check that test scripts are named correctly tasks.register('testNaming') { doLast { @@ -181,17 +164,31 @@ tasks.register('testNaming') { } enum Modifiers { - DEV_MODE, GEN_DOCS, DEBUG, PROFILE, JUNIT + DEV_MODE, GEN_NIGHTLY_DOCS, GEN_RELEASE_DOCS, DEBUG, PROFILE, JUNIT } // Create a test task with given name, environments dir/file, dev mode and java version. -void createTestTask(String name, String desc, String environments, int javaVersion, Modifiers... modifiers) { +// -1 on the timeout means it'll be disabled. +void createTestTask(String name, String desc, String environments, int javaVersion, long timeout, Modifiers... modifiers) { + if (timeout == 0) + timeout = 300000 // 5 minutes boolean junit = modifiers.contains(Modifiers.JUNIT) + boolean releaseDocs = modifiers.contains(Modifiers.GEN_RELEASE_DOCS) + boolean docs = modifiers.contains(Modifiers.GEN_NIGHTLY_DOCS) || releaseDocs + def artifact = 'build' + File.separator + 'libs' + File.separator + if (junit) { + artifact += 'Skript-JUnit.jar' + } else if (releaseDocs) { + artifact += 'Skript-' + version + '.jar' + } else { + artifact += 'Skript-nightly.jar' + } tasks.register(name, JavaExec) { - description = desc; - dependsOn licenseTest + description = desc if (junit) { dependsOn testJar + } else if (releaseDocs) { + dependsOn githubRelease, testNaming } else { dependsOn nightlyRelease, testNaming } @@ -203,21 +200,22 @@ void createTestTask(String name, String desc, String environments, int javaVersi } group = 'execution' classpath = files([ - 'build' + File.separator + 'libs' + File.separator + (junit ? 'Skript-JUnit.jar' : 'Skript-nightly.jar'), + artifact, project.configurations.runtimeClasspath.find { it.name.startsWith('gson') }, sourceSets.main.runtimeClasspath ]) main = 'ch.njol.skript.test.platform.PlatformMain' args = [ 'build/test_runners', - junit ? 'src/test/skript/tests/junit' : 'src/test/skript/tests', + junit ? 'src/test/skript/junit' : 'src/test/skript/tests', 'src/test/resources/runner_data', environments, modifiers.contains(Modifiers.DEV_MODE), - modifiers.contains(Modifiers.GEN_DOCS), + docs, junit, modifiers.contains(Modifiers.DEBUG), - project.findProperty('verbosity') ?: "null" + project.findProperty('verbosity') ?: "null", + timeout ] // Do first is used when throwing exceptions. @@ -226,16 +224,23 @@ void createTestTask(String name, String desc, String environments, int javaVersi if (!gradle.taskGraph.hasTask(":tasks") && !gradle.startParameter.dryRun && modifiers.contains(Modifiers.PROFILE)) { if (!project.hasProperty('profiler')) throw new MissingPropertyException('Add parameter -Pprofiler=', 'profiler', String.class) - + args += '-agentpath:' + project.property('profiler') + '=port=8849,nowait' } } } } -def latestEnv = 'java17/paper-1.19.4.json' -def latestJava = 17 -def oldestJava = 8 +def java21 = 21 +def java17 = 17 +def java11 = 11 + +def latestEnv = 'java21/paper-1.21.0.json' +def latestJava = java21 +def oldestJava = java11 + +def latestJUnitEnv = 'java17/paper-1.20.4.json' +def latestJUnitJava = java17 java { toolchain.languageVersion.set(JavaLanguageVersion.of(latestJava)) @@ -252,23 +257,28 @@ compileTestJava.options.encoding = 'UTF-8' String environments = 'src/test/skript/environments/'; String env = project.property('testEnv') == null ? latestEnv : project.property('testEnv') + '.json' int envJava = project.property('testEnvJavaVersion') == null ? latestJava : Integer.parseInt(project.property('testEnvJavaVersion') as String) -createTestTask('quickTest', 'Runs tests on one environment being the latest supported Java and Minecraft.', environments + latestEnv, latestJava) -createTestTask('skriptTestJava17', 'Runs tests on all Java 17 environments.', environments + 'java17', latestJava) -createTestTask('skriptTestJava8', 'Runs tests on all Java 8 environments.', environments + 'java8', oldestJava) -createTestTask('skriptTestDev', 'Runs testing server and uses \'system.in\' for command input, stop server to finish.', environments + env, envJava, Modifiers.DEV_MODE, Modifiers.DEBUG) -createTestTask('skriptProfile', 'Starts the testing server with JProfiler support.', environments + latestEnv, latestJava, Modifiers.PROFILE) -createTestTask('genDocs', 'Generates the Skript documentation website html files.', environments + env, envJava, Modifiers.GEN_DOCS) +createTestTask('quickTest', 'Runs tests on one environment being the latest supported Java and Minecraft.', environments + latestEnv, latestJava, 0) +createTestTask('skriptTestJava21', 'Runs tests on all Java 21 environments.', environments + 'java21', java21, 0) +createTestTask('skriptTestJava17', 'Runs tests on all Java 17 environments.', environments + 'java17', java17, 0) +createTestTask('skriptTestJava11', 'Runs tests on all Java 11 environments.', environments + 'java11', java11, 0) +createTestTask('skriptTestDev', 'Runs testing server and uses \'system.in\' for command input, stop server to finish.', environments + env, envJava, 0, Modifiers.DEV_MODE, Modifiers.DEBUG) +createTestTask('skriptProfile', 'Starts the testing server with JProfiler support.', environments + latestEnv, latestJava, -1, Modifiers.PROFILE) +createTestTask('genNightlyDocs', 'Generates the Skript documentation website html files.', environments + env, envJava, 0, Modifiers.GEN_NIGHTLY_DOCS) +createTestTask('genReleaseDocs', 'Generates the Skript documentation website html files for a release.', environments + env, envJava, 0, Modifiers.GEN_RELEASE_DOCS) tasks.register('skriptTest') { description = 'Runs tests on all environments.' - dependsOn skriptTestJava8, skriptTestJava17 + dependsOn skriptTestJava11, skriptTestJava17, skriptTestJava21 } -createTestTask('JUnitQuick', 'Runs JUnit tests on one environment being the latest supported Java and Minecraft.', environments + latestEnv, latestJava, Modifiers.JUNIT) -createTestTask('JUnitJava17', 'Runs JUnit tests on all Java 17 environments.', environments + 'java17', latestJava, Modifiers.JUNIT) -createTestTask('JUnitJava8', 'Runs JUnit tests on all Java 8 environments.', environments + 'java8', oldestJava, Modifiers.JUNIT) +createTestTask('JUnitQuick', 'Runs JUnit tests on one environment being the latest supported Java and Minecraft.', environments + latestJUnitEnv, latestJUnitJava, 0, Modifiers.JUNIT) +// Disabled as EasyMock 5.2.0 is required for Java 21 support +// However, we are currently using 5.0.1 (see https://github.com/SkriptLang/Skript/pull/6204#discussion_r1405302009) +//createTestTask('JUnitJava21', 'Runs JUnit tests on all Java 21 environments.', environments + 'java21', java21, 0, Modifiers.JUNIT) +createTestTask('JUnitJava17', 'Runs JUnit tests on all Java 17 environments.', environments + 'java17', java17, 0, Modifiers.JUNIT) +createTestTask('JUnitJava11', 'Runs JUnit tests on all Java 11 environments.', environments + 'java11', java11, 0, Modifiers.JUNIT) tasks.register('JUnit') { description = 'Runs JUnit tests on all environments.' - dependsOn JUnitJava8, JUnitJava17 + dependsOn JUnitJava11, JUnitJava17//, JUnitJava21 } // Build flavor configurations @@ -277,10 +287,8 @@ task githubResources(type: ProcessResources) { include '**' version = project.property('version') def channel = 'stable' - if (version.contains('alpha')) - channel = 'alpha' - else if (version.contains('beta')) - channel = 'beta' + if (version.contains('-')) + channel = 'prerelease' filter ReplaceTokens, tokens: [ 'version' : version, 'today' : '' + LocalTime.now(), @@ -297,7 +305,7 @@ task githubResources(type: ProcessResources) { task githubRelease(type: ShadowJar) { from sourceSets.main.output dependsOn githubResources - archiveFileName = 'Skript-github.jar' + archiveFileName = 'Skript-' + version +'.jar' manifest { attributes( 'Name': 'ch/njol/skript', @@ -312,10 +320,8 @@ task spigotResources(type: ProcessResources) { include '**' version = project.property('version') def channel = 'stable' - if (version.contains('alpha')) - channel = 'alpha' - else if (version.contains('beta')) - channel = 'beta' + if (version.contains('-')) + channel = 'prerelease' filter ReplaceTokens, tokens: [ 'version' : version, 'today' : '' + LocalTime.now(), @@ -351,8 +357,8 @@ task nightlyResources(type: ProcessResources) { 'version' : version, 'today' : '' + LocalTime.now(), 'release-flavor' : 'skriptlang-nightly', // SkriptLang build, automatically done by CI - 'release-channel' : 'alpha', // No update checking, but these are VERY unstable - 'release-updater' : 'ch.njol.skript.update.NoUpdateChecker', // No autoupdates for now + 'release-channel' : 'prerelease', // No update checking, but these are VERY unstable + 'release-updater' : 'ch.njol.skript.update.NoUpdateChecker', // No auto updates for now 'release-source' : '', 'release-download': 'null' ] @@ -362,7 +368,7 @@ task nightlyResources(type: ProcessResources) { task nightlyRelease(type: ShadowJar) { from sourceSets.main.output - dependsOn nightlyResources, licenseMain + dependsOn nightlyResources archiveFileName = 'Skript-nightly.jar' manifest { attributes( @@ -372,3 +378,25 @@ task nightlyRelease(type: ShadowJar) { ) } } + +javadoc { + mustRunAfter(tasks.withType(ProcessResources)) + title = 'Skript ' + project.property('version') + source = sourceSets.main.allJava + + exclude("ch/njol/skript/conditions/**") + exclude("ch/njol/skript/expressions/**") + exclude("ch/njol/skript/effects/**") + exclude("ch/njol/skript/events/**") + exclude("ch/njol/skript/sections/**") + exclude("ch/njol/skript/structures/**") + exclude("ch/njol/skript/lang/function/EffFunctionCall.java") + exclude("ch/njol/skript/lang/function/ExprFunctionCall.java") + exclude("ch/njol/skript/hooks/**") + exclude("ch/njol/skript/test/**") + + classpath = configurations.compileClasspath + sourceSets.main.output + options.encoding = 'UTF-8' + // currently our javadoc has a lot of errors, so we need to suppress the linter + options.addStringOption('Xdoclint:none', '-quiet') +} diff --git a/code-conventions.md b/code-conventions.md index 37ac182cd62..3bcf17c6c9f 100644 --- a/code-conventions.md +++ b/code-conventions.md @@ -66,6 +66,9 @@ With the exception of contacting our own resources (e.g. to check for updates) c Code contributed must be licensed under GPLv3, by **you**. We expect that any code you contribute is either owned by you or you have explicit permission to provide and license it to us. +Licenses do not need to be printed in individual files (or packages) unless the licence applying to the code in +that file (or package) deviates from the licence scope of its containing package. + Third party code (under a compatible licence) _may_ be accepted in the following cases: - It is part of a public, freely-available library or resource. - It is somehow necessary to your contribution, and you have been given permission to include it. @@ -75,34 +78,61 @@ If we receive complaints regarding the licensing of a contribution we will forwa If you have questions or complaints regarding the licensing or reproduction of a contribution you may contact us (the organisation) or the contributor of that code directly. -If, in the future, we need to relicense contributed code, we will contact all contributors involved. +If, in the future, we need to re-license contributed code, we will contact all contributors involved. If we need to remove or alter contributed code due to a licensing issue we will attempt to notify its contributor. ## Code Style ### Formatting +* Imports should be grouped together by type (e.g. all `java.lang...` imports together) + * Following the style of existing imports in a class is encouraged, but not required + * Wildcard `*` imports are permitted (as long as they do not interfere with existing imports), e.g. `java.lang.*`. * Tabs, no spaces (unless in code imported from other projects) -** No tabs/spaces in empty lines + - No tabs/spaces in empty lines * No trailing whitespace * At most 120 characters per line - - In Javadoc/multiline comments, at most 80 characters per line -* When statements consume multiple lines, all lines but first have two tabs of additional indentation + - In Javadoc/multiline comments, at most 80 characters per line +* When statements consume multiple lines, all lines but the first have two tabs of additional indentation + - 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 * 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 - - Otherwise, empty line before and after method is a good rule of thumb + - Empty line after last method in a class is *not* required + - Otherwise, empty line before and after method is a good rule of thumb * If fields have Javadoc, separate them with empty lines * Use empty lines liberally inside methods to improve readability * Use curly brackets to start and end most blocks - - Only when a conditional block (if or else) contains a single statement, they may be omitted - - When omitting brackets, still indent as if the code had brackets - - Avoid omitting brackets if it produces hard-to-read code + - When a block contains a single statement, they may be omitted + - Brackets may not be omitted in a chain of other blocks that require brackets, e.g `if ... else {}` + - When omitting brackets, still indent as if the code had brackets + - Avoid omitting brackets if it produces hard-to-read or ambiguous code +* Ternaries should be avoided where it makes the code complex or difficult to read * Annotations for methods and classes are placed in lines before their declarations, one per line -* When there are multiple annotations, place them in order: - - @Override -> @Nullable -> @SuppressWarnings - - For other annotations, doesn't matter; let your IDE decide + - Annotations for a structure go on the line before that structure + ```java + @Override + @SuppressWarnings("xyz") + public void myMethod() { + // Override goes above method because method is overriding + } + ``` + + - Annotations for the _value_ of a thing go before that value's type declaration + ```java + @Override + public @Nullable Object myMethod() { + // Nullable goes before Object because Object is Nullable + } + ``` +* When there are multiple annotations, it looks nicer to place them in length order (longest last) +but this is not strictly required: + ```java + @Override + @Deprecated + @SuppressWarnings("xyz") + ``` * When splitting Strings into multiple lines the last part of the string must be (space character included) " " + ```java String string = "example string " + @@ -111,6 +141,7 @@ If we need to remove or alter contributed code due to a licensing issue we will * When extending one of following classes: SimpleExpression, SimplePropertyExpression, Effect, Condition... - Put overridden methods in order + - Put static registration before all methods - SimpleExpression: init -> get/getAll -> acceptChange -> change -> setTime -> getTime -> isSingle -> getReturnType -> toString - SimplePropertyExpression: -> init -> convert -> acceptChange -> change -> setTime -> getTime -> getReturnType -> getPropertyName - Effect: init -> execute -> toString @@ -127,9 +158,11 @@ If we need to remove or alter contributed code due to a licensing issue we will - Static constant fields should be named in `UPPER_SNAKE_CASE` * Localised messages should be named in `lower_snake_case` - And that is the only place where snake_case is acceptable -* Use prefixes only where their use has been already estabilished (such as `ExprSomeRandomThing`) +* Use prefixes only where their use has been already established (such as `ExprSomeRandomThing`) - Otherwise, use postfixes where necessary - Common occurrences include: Struct (Structure), Sec (Section), EffSec (EffectSection), Eff (Effect), Cond (Condition), Expr (Expression) +* Ensure variable/field names are descriptive. Avoid using shorthand names like `e`, or `c` + - e.g. Event should be `event`, not `e`. `e` is ambiguous and could mean a number of things ### Comments * Prefer to comment *why* you're doing things instead of how you're doing them @@ -161,33 +194,79 @@ Your comments should look something like these: ## Language Features ### Compatibility -* Contributions should maintain Java 8 source/binary compatibility, even though compiling Skript requires Java 17 - - Users must not need JRE newer than version 8 -* Versions up to and including Java 17 should work too +[//]: # (To be updated for 2.10 for Java 17) +* Contributions should maintain Java 11 source/binary compatibility, even though compiling Skript requires Java 21 + - Users must not need JRE newer than version 11 +* Versions up to and including Java 21 should work too - Please avoid using unsafe reflection * It is recommended to make fields final, if they are effectively final -* Local variables and method parameters should not be declared final +* Local variables and method parameters should not be declared final unless used in anonymous classes, lambdas +or try-with-resources sections where their immutability is necessary * Methods should be declared final only where necessary * Use `@Override` whenever applicable - -### Nullness + - They may be omitted to prevent compilation errors when something overrides only + on a version-dependent basis (e.g. in Library XYZ version 2 we override `getX()` but in version 3 it's + gone, and we call it ourselves) + +### null-ness +* We use **JetBrains** Annotations for specifying null-ness and method contracts. + * If editing a file using a different annotation set (e.g. Javax, Eclipse Sisu, Bukkit) + these should be replaced with their JetBrains equivalent. + * The semantics for JetBrains Annotations are strict _and should be observed!_ + * Many IDEs have built-in compiler-level support for these, and can even be set to produce strict + errors when an annotation is misused; do not misuse them. + * **`@NotNull`** + * > An element annotated with NotNull claims null value is forbidden to return (for methods), + pass to (parameters) and hold (local variables and fields). + * Something is `@NotNull` iff it is never null from its inception (new X) to its garbage collection, + i.e. there is no point in time at which the value could ever be null. + * **`@Nullable`** + * > An element annotated with Nullable claims null value is perfectly valid to return (for methods), + > pass to (parameters) or hold in (local variables and fields). + > + > By convention, this annotation applied only when the value should always be checked + > against null because the developer could do nothing to prevent null from happening. + * Something is `@Nullable` iff there is _absolutely no way of determining_ (other than checking its + value `!= null`) whether it is null. + * In other words, if there is another way of knowing (e.g. you set it yourself, an `isPresent` method, etc.) + then it should not be marked nullable. + * **`@Contract`** + * The contract annotation should be used to express other behaviour (e.g. null depending on parameters). * All fields, method parameters and their return values are non-null by default - - Exceptions: Github API JSON mappings, Metrics -* When something is nullable, mark it as so -* Only ignore nullness errors when a variable is effectively non-null - if in doubt: check - - Most common example is syntax elements, which are not initialised using a constructor + - Exceptions: GitHub API JSON mappings, Metrics +* When ignoring warnings, use the no-inspection comment rather than a blanket suppression annotation * Use assertions liberally: if you're sure something is not null, assert so to the compiler - Makes finding bugs easier for developers -* Assertions must **not** have side-effects - they may be skipped in real environments +* Assertions must **not** have side-effects in non-test packages - they may be skipped in real environments * Avoid checking non-null values for nullability - Unless working around buggy addons, it is rarely necessary - - This is why ignoring nullness errors is particularly dangerous + - This is why ignoring null-ness errors is particularly dangerous +* Annotations on array types **must** be placed properly: + * Annotations on the array itself go before the array brackets + ```java + @Nullable Object @NotNull [] + // a not-null array of nullable objects + ``` + * Annotations on values inside the array go before the value declaration + ```java + @NotNull Object @Nullable [] + // a nullable array of not-null objects + ``` + * If this is not adhered to, an IDE may provide incorrect feedback. ### Assertions Skript must run with assertations enabled; use them in your development environment. \ The JVM flag -ea is used to enable them. +## Code Complexity + +Dense, highly-complex code should be avoided to preserve readability and to help with future maintenance, +especially within a single method body. + +There are many available metrics for measuring code complexity (for different purposes); [we have our own](https://stable.skriptlang.org/Radical_Complexity.pdf). +There are no strict limits for code complexity, but you may be encouraged (or required) to reformat or break up methods +into smaller, more manageable chunks. If in doubt, keep things simple. ## Minecraft Features diff --git a/gradle.properties b/gradle.properties index 220737a816b..469d1e072ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,11 @@ +# Done to increase the memory available to gradle. +# Ensure encoding is consistent across systems. +org.gradle.jvmargs=-Xmx1G -Dfile.encoding=UTF-8 +org.gradle.parallel=true + groupid=ch.njol name=skript -version=2.7.0-beta2 +version=2.8.7 jarName=Skript.jar -testEnv=java17/paper-1.19.4 -testEnvJavaVersion=17 +testEnv=java21/paper-1.21.0 +testEnvJavaVersion=21 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710d..7f93135c49b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc10b601f7c..b82aa23a4f0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421cc..0adc8e1a532 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -197,6 +198,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/licenseheader.txt b/licenseheader.txt deleted file mode 100644 index 15be760be16..00000000000 --- a/licenseheader.txt +++ /dev/null @@ -1,16 +0,0 @@ - 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 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 0a8d7b2d301..78b185aca4c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } rootProject.name = 'Skript' diff --git a/skript-aliases b/skript-aliases index fb9c3044e55..9ea857f6b7d 160000 --- a/skript-aliases +++ b/skript-aliases @@ -1 +1 @@ -Subproject commit fb9c3044e555667b4dc5558467608bd55fa32df0 +Subproject commit 9ea857f6b7dd1e4fc4a35a88149b9e463b537b06 diff --git a/src/main/java/ch/njol/skript/ScriptLoader.java b/src/main/java/ch/njol/skript/ScriptLoader.java index 14cd0b3e628..afafa0b2def 100644 --- a/src/main/java/ch/njol/skript/ScriptLoader.java +++ b/src/main/java/ch/njol/skript/ScriptLoader.java @@ -60,8 +60,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.BlockingQueue; @@ -490,7 +492,7 @@ private static CompletableFuture loadScripts(List configs, O ScriptInfo scriptInfo = new ScriptInfo(); - List>> scripts = new ArrayList<>(); + List scripts = new ArrayList<>(); List> scriptInfoFutures = new ArrayList<>(); for (Config config : configs) { @@ -498,9 +500,9 @@ private static CompletableFuture loadScripts(List configs, O throw new NullPointerException(); CompletableFuture future = makeFuture(() -> { - NonNullPair> pair = loadScript(config); - scripts.add(pair); - scriptInfo.add(new ScriptInfo(1, pair.getSecond().size())); + LoadingScriptInfo info = loadScript(config); + scripts.add(info); + scriptInfo.add(new ScriptInfo(1, info.structures.size())); return null; }, openCloseable); @@ -516,67 +518,92 @@ private static CompletableFuture loadScripts(List configs, O try { openCloseable.open(); - scripts.stream() - .flatMap(pair -> { // Flatten each entry down to a stream of Script-Structure pairs - return pair.getSecond().stream() - .map(structure -> new NonNullPair<>(pair, structure)); - }) - .sorted(Comparator.comparing(pair -> pair.getSecond().getPriority())) - .forEach(pair -> { - Script script = pair.getFirst().getFirst(); - Structure structure = pair.getSecond(); - - parser.setActive(script); - parser.setCurrentStructure(structure); - parser.setNode(structure.getEntryContainer().getSource()); - - try { - if (!structure.preLoad()) - pair.getFirst().getSecond().remove(structure); - } catch (Exception e) { - //noinspection ThrowableNotThrown - Skript.exception(e, "An error occurred while trying to load a Structure."); - pair.getFirst().getSecond().remove(structure); + // build sorted list + // this nest of pairs is terrible, but we need to keep the reference to the modifiable structures list + List> pairs = scripts.stream() + .flatMap(info -> { // Flatten each entry down to a stream of Script-Structure pairs + return info.structures.stream() + .map(structure -> new NonNullPair<>(info, structure)); + }) + .sorted(Comparator.comparing(pair -> pair.getSecond().getPriority())) + .collect(Collectors.toCollection(ArrayList::new)); + + // pre-loading + pairs.removeIf(pair -> { + LoadingScriptInfo loadingInfo = pair.getFirst(); + Structure structure = pair.getSecond(); + + parser.setActive(loadingInfo.script); + parser.setCurrentStructure(structure); + parser.setNode(loadingInfo.nodeMap.get(structure)); + + try { + if (!structure.preLoad()) { + loadingInfo.structures.remove(structure); + return true; } - }); - + } catch (Exception e) { + //noinspection ThrowableNotThrown + Skript.exception(e, "An error occurred while trying to preLoad a Structure."); + loadingInfo.structures.remove(structure); + return true; + } + return false; + }); parser.setInactive(); - // TODO in the future, Structure#load should be split across multiple threads if parallel loading is enabled. + // TODO in the future, Structure#load/Structure#postLoad should be split across multiple threads if parallel loading is enabled. // However, this is not possible right now as reworks in multiple areas will be needed. // For example, the "Commands" class still uses a static list for currentArguments that is cleared between loads. // Until these reworks happen, limiting main loading to asynchronous (not parallel) is the only choice we have. - for (NonNullPair> pair : scripts) { - parser.setActive(pair.getFirst()); - pair.getSecond().removeIf(structure -> { - parser.setCurrentStructure(structure); - parser.setNode(structure.getEntryContainer().getSource()); - try { - return !structure.load(); - } catch (Exception e) { - //noinspection ThrowableNotThrown - Skript.exception(e, "An error occurred while trying to load a Structure."); + + // loading + pairs.removeIf(pair -> { + LoadingScriptInfo loadingInfo = pair.getFirst(); + Structure structure = pair.getSecond(); + + parser.setActive(loadingInfo.script); + parser.setCurrentStructure(structure); + parser.setNode(loadingInfo.nodeMap.get(structure)); + + try { + if (!structure.load()) { + loadingInfo.structures.remove(structure); return true; } - }); - } - + } catch (Exception e) { + //noinspection ThrowableNotThrown + Skript.exception(e, "An error occurred while trying to load a Structure."); + loadingInfo.structures.remove(structure); + return true; + } + return false; + }); parser.setInactive(); - for (NonNullPair> pair : scripts) { - parser.setActive(pair.getFirst()); - pair.getSecond().removeIf(structure -> { - parser.setCurrentStructure(structure); - parser.setNode(structure.getEntryContainer().getSource()); - try { - return !structure.postLoad(); - } catch (Exception e) { - //noinspection ThrowableNotThrown - Skript.exception(e, "An error occurred while trying to load a Structure."); + // post-loading + pairs.removeIf(pair -> { + LoadingScriptInfo loadingInfo = pair.getFirst(); + Structure structure = pair.getSecond(); + + parser.setActive(loadingInfo.script); + parser.setCurrentStructure(structure); + parser.setNode(loadingInfo.nodeMap.get(structure)); + + try { + if (!structure.postLoad()) { + loadingInfo.structures.remove(structure); return true; } - }); - } + } catch (Exception e) { + //noinspection ThrowableNotThrown + Skript.exception(e, "An error occurred while trying to postLoad a Structure."); + loadingInfo.structures.remove(structure); + return true; + } + return false; + }); + parser.setInactive(); return scriptInfo; } catch (Exception e) { @@ -590,17 +617,34 @@ private static CompletableFuture loadScripts(List configs, O }); } + private static class LoadingScriptInfo { + + public final Script script; + + public final List structures; + + public final Map nodeMap; + + public LoadingScriptInfo(Script script, List structures, Map nodeMap) { + this.script = script; + this.structures = structures; + this.nodeMap = nodeMap; + } + + } + /** * Creates a script and loads the provided config into it. * @param config The config to load into a script. - * @return The script that was loaded. + * @return A pair containing the script that was loaded and a modifiable version of the structures list. */ // Whenever you call this method, make sure to also call PreScriptLoadEvent - private static NonNullPair> loadScript(Config config) { + private static LoadingScriptInfo loadScript(Config config) { if (config.getFile() == null) throw new IllegalArgumentException("A config must have a file to be loaded."); ParserInstance parser = getParser(); + Map nodeMap = new HashMap<>(); List structures = new ArrayList<>(); Script script = new Script(config, structures); parser.setActive(script); @@ -610,16 +654,17 @@ private static NonNullPair> loadScript(Config config) { SkriptConfig.configs.add(config); try (CountingLogHandler ignored = new CountingLogHandler(SkriptLogger.SEVERE).start()) { - for (Node cnode : config.getMainNode()) { - if (!(cnode instanceof SectionNode)) { - Skript.error("invalid line - all code has to be put into triggers"); + for (Node node : config.getMainNode()) { + if (!(node instanceof SimpleNode) && !(node instanceof SectionNode)) { + // unlikely to occur, but just in case + Skript.error("could not interpret line as a structure"); continue; } - SectionNode node = ((SectionNode) cnode); String line = node.getKey(); if (line == null) continue; + line = replaceOptions(line); // replace options here before validation if (!SkriptParser.validateLine(line)) continue; @@ -627,14 +672,13 @@ private static NonNullPair> loadScript(Config config) { if (Skript.logVeryHigh() && !Skript.debug()) Skript.info("loading trigger '" + line + "'"); - line = replaceOptions(line); - Structure structure = Structure.parse(line, node, "Can't understand this structure: " + line); if (structure == null) continue; structures.add(structure); + nodeMap.put(structure, node); } if (Skript.logHigh()) { @@ -672,8 +716,8 @@ private static NonNullPair> loadScript(Config config) { Skript.exception(e); } } - - return new NonNullPair<>(script, structures); + + return new LoadingScriptInfo(script, structures, nodeMap); } /* @@ -920,7 +964,7 @@ public static ArrayList loadItems(SectionNode node) { if (subNode instanceof SimpleNode) { long start = System.currentTimeMillis(); - Statement stmt = Statement.parse(expr, "Can't understand this condition/effect: " + expr); + Statement stmt = Statement.parse(expr, items, "Can't understand this condition/effect: " + expr); if (stmt == null) continue; long requiredTime = SkriptConfig.longParseTimeWarningThreshold.value().getMilliSeconds(); @@ -1009,20 +1053,6 @@ public static FileFilter getDisabledScriptsFilter() { * by new methods in this class. */ - /** - * Reloads a single script. - * @param scriptFile The file representing the script to reload. - * @return Future of statistics of the newly loaded script. - * @deprecated Use {@link #reloadScript(Script, OpenCloseable)}. - */ - @Deprecated - public static CompletableFuture reloadScript(File scriptFile, OpenCloseable openCloseable) { - Script script = getScript(scriptFile); - if (script == null) - return CompletableFuture.completedFuture(new ScriptInfo()); - return reloadScript(script, openCloseable); - } - /** * Unloads the provided script. * @param scriptFile The file representing the script to unload. @@ -1049,6 +1079,18 @@ private static ScriptInfo unloadScripts(File folder) { return unloadScripts(getScripts(folder)); } + /** + * Reloads a single script. + * @param scriptFile The file representing the script to reload. + * @return Future of statistics of the newly loaded script. + * @deprecated Use {@link #reloadScript(Script, OpenCloseable)}. + */ + @Deprecated + public static CompletableFuture reloadScript(File scriptFile, OpenCloseable openCloseable) { + unloadScript(scriptFile); + return loadScripts(scriptFile, openCloseable); + } + /** * Reloads all scripts in the given folder and its subfolders. * @param folder A folder. @@ -1058,7 +1100,7 @@ private static ScriptInfo unloadScripts(File folder) { @Deprecated public static CompletableFuture reloadScripts(File folder, OpenCloseable openCloseable) { unloadScripts(folder); - return loadScripts(loadStructures(folder), openCloseable); + return loadScripts(folder, openCloseable); } /** diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index dab73c2b5a6..3755525edb1 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -18,81 +18,15 @@ */ package ch.njol.skript; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.lang.Thread.UncaughtExceptionHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.logging.Filter; -import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; -import java.util.zip.ZipFile; - -import org.bstats.bukkit.Metrics; -import org.bstats.charts.SimplePie; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginCommand; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.server.PluginDisableEvent; -import org.bukkit.event.server.ServerCommandEvent; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.plugin.java.JavaPlugin; -import org.eclipse.jdt.annotation.Nullable; -import org.junit.runner.JUnitCore; -import org.junit.runner.Result; -import org.skriptlang.skript.lang.entry.EntryValidator; -import org.skriptlang.skript.lang.script.Script; -import org.skriptlang.skript.lang.structure.Structure; -import org.skriptlang.skript.lang.structure.StructureInfo; - -import com.google.common.collect.Lists; -import com.google.gson.Gson; - import ch.njol.skript.aliases.Aliases; import ch.njol.skript.bukkitutil.BurgerHelper; import ch.njol.skript.classes.ClassInfo; -import org.skriptlang.skript.lang.comparator.Comparator; -import org.skriptlang.skript.lang.converter.Converter; import ch.njol.skript.classes.data.BukkitClasses; import ch.njol.skript.classes.data.BukkitEventValues; import ch.njol.skript.classes.data.DefaultComparators; import ch.njol.skript.classes.data.DefaultConverters; import ch.njol.skript.classes.data.DefaultFunctions; +import ch.njol.skript.classes.data.DefaultOperations; import ch.njol.skript.classes.data.JavaClasses; import ch.njol.skript.classes.data.SkriptClasses; import ch.njol.skript.command.Commands; @@ -124,8 +58,6 @@ import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.log.Verbosity; import ch.njol.skript.registrations.Classes; -import org.skriptlang.skript.lang.comparator.Comparators; -import org.skriptlang.skript.lang.converter.Converters; import ch.njol.skript.registrations.EventValues; import ch.njol.skript.test.runner.EffObjectives; import ch.njol.skript.test.runner.SkriptJUnitTest; @@ -153,6 +85,76 @@ import ch.njol.util.StringUtils; import ch.njol.util.coll.iterator.CheckedIterator; import ch.njol.util.coll.iterator.EnumerationIterable; +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; +import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.UnknownNullability; +import org.junit.After; +import org.junit.runner.JUnitCore; +import org.junit.runner.Result; +import org.skriptlang.skript.lang.comparator.Comparator; +import org.skriptlang.skript.lang.comparator.Comparators; +import org.skriptlang.skript.lang.converter.Converter; +import org.skriptlang.skript.lang.converter.Converters; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.lang.experiment.ExperimentRegistry; +import ch.njol.skript.registrations.Feature; +import org.skriptlang.skript.lang.script.Script; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.lang.structure.StructureInfo; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Filter; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; // TODO meaningful error if someone uses an %expression with percent signs% outside of text or a variable @@ -169,7 +171,7 @@ *

* Once you made sure that Skript is loaded you can use Skript.getInstance() whenever you need a reference to the plugin, but you likely won't need it since all API * methods are static. - * + * * @author Peter Güttinger * @see #registerAddon(JavaPlugin) * @see #registerCondition(Class, String...) @@ -182,34 +184,34 @@ * @see Converters#registerConverter(Class, Class, Converter) */ public final class Skript extends JavaPlugin implements Listener { - + // ================ PLUGIN ================ - + @Nullable private static Skript instance = null; - + private static boolean disabled = false; private static boolean partDisabled = false; - + public static Skript getInstance() { final Skript i = instance; if (i == null) throw new IllegalStateException(); return i; } - + /** * Current updater instance used by Skript. */ @Nullable private SkriptUpdater updater; - + public Skript() throws IllegalStateException { if (instance != null) throw new IllegalStateException("Cannot create multiple instances of Skript!"); instance = this; } - + private static Version minecraftVersion = new Version(666), UNKNOWN_VERSION = new Version(666); private static ServerPlatform serverPlatform = ServerPlatform.BUKKIT_UNKNOWN; // Start with unknown... onLoad changes this @@ -227,24 +229,26 @@ public static void updateMinecraftVersion() { minecraftVersion = new Version("" + m.group()); } } - + @Nullable private static Version version = null; - + @Deprecated(forRemoval = true) // TODO this field will be replaced by a proper registry later + private static @UnknownNullability ExperimentRegistry experimentRegistry; + public static Version getVersion() { final Version v = version; if (v == null) throw new IllegalStateException(); return v; } - + public static final Message m_invalid_reload = new Message("skript.invalid reload"), m_finished_loading = new Message("skript.finished loading"), m_no_errors = new Message("skript.no errors"), m_no_scripts = new Message("skript.no scripts"); private static final PluralizingArgsMessage m_scripts_loaded = new PluralizingArgsMessage("skript.scripts loaded"); - + public static ServerPlatform getServerPlatform() { if (classExists("net.glowstone.GlowServer")) { return ServerPlatform.BUKKIT_GLOWSTONE; // Glowstone has timings too, so must check for it first @@ -270,7 +274,7 @@ private static boolean using32BitJava() { // Property returned should either be "Java HotSpot(TM) 32-Bit Server VM" or "OpenJDK 32-Bit Server VM" if 32-bit and using OracleJDK/OpenJDK return System.getProperty("java.vm.name").contains("32"); } - + /** * Checks if server software and Minecraft version are supported. * Prints errors or warnings to console if something is wrong. @@ -287,7 +291,7 @@ private static boolean checkServerPlatform() { minecraftVersion = new Version("" + m.group()); } Skript.debug("Loading for Minecraft " + minecraftVersion); - + // Check that MC version is supported if (!isRunningMinecraft(1, 9)) { // Prevent loading when not running at least Minecraft 1.9 @@ -296,7 +300,7 @@ private static boolean checkServerPlatform() { Skript.error("Note that those versions are, of course, completely unsupported!"); return false; } - + // Check that current server platform is somewhat supported serverPlatform = getServerPlatform(); Skript.debug("Server platform: " + serverPlatform); @@ -316,7 +320,7 @@ private static boolean checkServerPlatform() { Skript.warning("It will still probably work, but if it does not, you are on your own."); Skript.warning("Skript officially supports Paper and Spigot."); } - + // If nothing got triggered, everything is probably ok return true; } @@ -328,7 +332,7 @@ private static boolean checkServerPlatform() { * Checks whether a hook has been enabled. * @param hook The hook to check. * @return Whether the hook is enabled. - * @see #disableHookRegistration(Class[]) + * @see #disableHookRegistration(Class[]) */ public static boolean isHookEnabled(Class> hook) { return !disabledHookRegistrations.contains(hook); @@ -346,7 +350,7 @@ public static boolean isFinishedLoadingHooks() { * Disables the registration for the given hook classes. If Skript has been enabled, this method * will throw an API exception. It should be used in something like {@link JavaPlugin#onLoad()}. * @param hooks The hooks to disable the registration of. - * @see #isHookEnabled(Class) + * @see #isHookEnabled(Class) */ @SafeVarargs public static void disableHookRegistration(Class>... hooks) { @@ -362,6 +366,13 @@ public static void disableHookRegistration(Class>... hooks) { */ private File scriptsFolder; + /** + * @return The manager for experimental, optional features. + */ + public static ExperimentRegistry experiments() { + return experimentRegistry; + } + /** * @return The folder containing all Scripts. */ @@ -371,7 +382,7 @@ public File getScriptsFolder() { scriptsFolder.mkdirs(); return scriptsFolder; } - + @Override public void onEnable() { Bukkit.getPluginManager().registerEvents(this, this); @@ -380,11 +391,11 @@ public void onEnable() { setEnabled(false); return; } - + handleJvmArguments(); // JVM arguments - + version = new Version("" + getDescription().getVersion()); // Skript version - + // Start the updater // Note: if config prohibits update checks, it will NOT do network connections try { @@ -392,7 +403,9 @@ public void onEnable() { } catch (Exception e) { Skript.exception(e, "Update checker could not be initialized."); } - + experimentRegistry = new ExperimentRegistry(this); + Feature.registerAll(getAddonInstance(), experimentRegistry); + if (!getDataFolder().isDirectory()) getDataFolder().mkdirs(); @@ -468,7 +481,7 @@ public void onEnable() { // initialize the Skript addon instance getAddonInstance(); - + // Load classes which are always safe to use new JavaClasses(); // These may be needed in configuration @@ -487,15 +500,15 @@ public void onEnable() { } catch (Throwable e) { classLoadError = e; } - + // Config must be loaded after Java and Skript classes are parseable // ... but also before platform check, because there is a config option to ignore some errors SkriptConfig.load(); - + // Now override the verbosity if test mode is enabled if (TestMode.VERBOSITY != null) SkriptLogger.setVerbosity(Verbosity.valueOf(TestMode.VERBOSITY)); - + // Use the updater, now that it has been configured to (not) do stuff if (updater != null) { CommandSender console = Bukkit.getConsoleSender(); @@ -517,28 +530,29 @@ public void onEnable() { throw e; // Uh oh, this shouldn't happen. Re-throw the error. } } - + // If loading can continue (platform ok), check for potentially thrown error if (classLoadError != null) { exception(classLoadError); setEnabled(false); return; } - + PluginCommand skriptCommand = getCommand("skript"); assert skriptCommand != null; // It is defined, unless build is corrupted or something like that skriptCommand.setExecutor(new SkriptCommand()); skriptCommand.setTabCompleter(new SkriptCommandTabCompleter()); - + // Load Bukkit stuff. It is done after platform check, because something might be missing! new BukkitEventValues(); - + new DefaultComparators(); new DefaultConverters(); new DefaultFunctions(); - + new DefaultOperations(); + ChatMessages.registerListeners(); - + try { getAddonInstance().loadClasses("ch.njol.skript", "conditions", "effects", "events", "expressions", "entity", "sections", "structures"); @@ -549,17 +563,17 @@ public void onEnable() { } Commands.registerListeners(); - + if (logNormal()) info(" " + Language.get("skript.copyright")); - + final long tick = testing() ? Bukkit.getWorlds().get(0).getFullTime() : 0; Bukkit.getScheduler().scheduleSyncDelayedTask(this, new Runnable() { @SuppressWarnings("synthetic-access") @Override public void run() { assert Bukkit.getWorlds().get(0).getFullTime() == tick; - + // Load hooks from Skript jar try { try (JarFile jar = new JarFile(getFile())) { @@ -587,68 +601,69 @@ public void run() { Skript.exception(e); } finishedLoadingHooks = true; - + if (TestMode.ENABLED) { info("Preparing Skript for testing..."); tainted = true; try { getAddonInstance().loadClasses("ch.njol.skript.test.runner"); + if (TestMode.JUNIT) + getAddonInstance().loadClasses("org.skriptlang.skript.test.junit.registration"); } catch (IOException e) { Skript.exception("Failed to load testing environment."); Bukkit.getServer().shutdown(); } } - + stopAcceptingRegistrations(); - - + + Documentation.generate(); // TODO move to test classes? - - Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.this, () -> { - if (logNormal()) - info("Loading variables..."); - long vls = System.currentTimeMillis(); - - LogHandler h = SkriptLogger.startLogHandler(new ErrorDescLogHandler() { - @Override - public LogResult log(final LogEntry entry) { - super.log(entry); - if (entry.level.intValue() >= Level.SEVERE.intValue()) { - logEx(entry.message); // no [Skript] prefix - return LogResult.DO_NOT_LOG; - } else { - return LogResult.LOG; - } - } - - @Override - protected void beforeErrors() { - logEx(); - logEx("===!!!=== Skript variable load error ===!!!==="); - logEx("Unable to load (all) variables:"); - } - - @Override - protected void afterErrors() { - logEx(); - logEx("Skript will work properly, but old variables might not be available at all and new ones may or may not be saved until Skript is able to create a backup of the old file and/or is able to connect to the database (which requires a restart of Skript)!"); - logEx(); + + // Variable loading + if (logNormal()) + info("Loading variables..."); + long vls = System.currentTimeMillis(); + + LogHandler h = SkriptLogger.startLogHandler(new ErrorDescLogHandler() { + @Override + public LogResult log(final LogEntry entry) { + super.log(entry); + if (entry.level.intValue() >= Level.SEVERE.intValue()) { + logEx(entry.message); // no [Skript] prefix + return LogResult.DO_NOT_LOG; + } else { + return LogResult.LOG; } - }); - - try (CountingLogHandler c = new CountingLogHandler(SkriptLogger.SEVERE).start()) { - if (!Variables.load()) - if (c.getCount() == 0) - error("(no information available)"); - } finally { - h.stop(); } - - long vld = System.currentTimeMillis() - vls; - if (logNormal()) - info("Loaded " + Variables.numVariables() + " variables in " + ((vld / 100) / 10.) + " seconds"); + + @Override + protected void beforeErrors() { + logEx(); + logEx("===!!!=== Skript variable load error ===!!!==="); + logEx("Unable to load (all) variables:"); + } + + @Override + protected void afterErrors() { + logEx(); + logEx("Skript will work properly, but old variables might not be available at all and new ones may or may not be saved until Skript is able to create a backup of the old file and/or is able to connect to the database (which requires a restart of Skript)!"); + logEx(); + } }); - + + try (CountingLogHandler c = new CountingLogHandler(SkriptLogger.SEVERE).start()) { + if (!Variables.load()) + if (c.getCount() == 0) + error("(no information available)"); + } finally { + h.stop(); + } + + long vld = System.currentTimeMillis() - vls; + if (logNormal()) + info("Loaded " + Variables.numVariables() + " variables in " + ((vld / 100) / 10.) + " seconds"); + // Skript initialization done debug("Early init done"); @@ -687,11 +702,13 @@ protected void afterErrors() { TestTracker.testFailed("exception was thrown during execution"); } if (TestMode.JUNIT) { - SkriptLogger.setVerbosity(Verbosity.DEBUG); info("Running all JUnit tests..."); long milliseconds = 0, tests = 0, fails = 0, ignored = 0, size = 0; try { List> classes = Lists.newArrayList(Utils.getClasses(Skript.getInstance(), "org.skriptlang.skript.test", "tests")); + // Don't attempt to run inner/anonymous classes as tests + classes.removeIf(Class::isAnonymousClass); + classes.removeIf(Class::isLocalClass); // Test that requires package access. This is only present when compiling with src/test. classes.add(Class.forName("ch.njol.skript.variables.FlatFileStorageTest")); size = classes.size(); @@ -705,6 +722,23 @@ protected void afterErrors() { Result junit = JUnitCore.runClasses(clazz); TestTracker.testStarted("JUnit: '" + test + "'"); + /** + * Usage of @After is pointless if the JUnit class requires delay. As the @After will happen instantly. + * The JUnit must override the 'cleanup' method to avoid Skript automatically cleaning up the test data. + */ + boolean overrides = false; + for (Method method : clazz.getDeclaredMethods()) { + if (!method.isAnnotationPresent(After.class)) + continue; + if (SkriptJUnitTest.getShutdownDelay() > 1) + warning("Using @After in JUnit classes, happens instantaneously, and JUnit class '" + test + "' requires a delay. Do your test cleanup in the script junit file or 'cleanup' method."); + if (method.getName().equals("cleanup")) + overrides = true; + } + if (SkriptJUnitTest.getShutdownDelay() > 1 && !overrides) + error("The JUnit class '" + test + "' does not override the method 'cleanup' thus the test data will instantly be cleaned up. " + + "This JUnit test requires longer shutdown time: " + SkriptJUnitTest.getShutdownDelay()); + // Collect all data from the current JUnit test. shutdownDelay = Math.max(shutdownDelay, SkriptJUnitTest.getShutdownDelay()); tests += junit.getRunCount(); @@ -715,9 +749,11 @@ protected void afterErrors() { // If JUnit failures are present, add them to the TestTracker. junit.getFailures().forEach(failure -> { String message = failure.getMessage() == null ? "" : " " + failure.getMessage(); - TestTracker.testFailed("'" + test + "': " + message); + TestTracker.JUnitTestFailed(test, message); Skript.exception(failure.getException(), "JUnit test '" + failure.getTestHeader() + " failed."); }); + if (SkriptJUnitTest.class.isAssignableFrom(clazz)) + ((SkriptJUnitTest) clazz.getConstructor().newInstance()).cleanup(); SkriptJUnitTest.clearJUnitTest(); } } catch (IOException e) { @@ -725,10 +761,12 @@ protected void afterErrors() { } catch (ClassNotFoundException e) { // Should be the Skript test jar gradle task. assert false : "Class 'ch.njol.skript.variables.FlatFileStorageTest' was not found."; + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { + Skript.exception(e, "Failed to initalize test JUnit classes."); } if (ignored > 0) Skript.warning("There were " + ignored + " ignored test cases! This can mean they are not properly setup in order in that class!"); - + info("Completed " + tests + " JUnit tests in " + size + " classes with " + fails + " failures in " + milliseconds + " milliseconds."); } } @@ -737,7 +775,7 @@ protected void afterErrors() { // Delay server shutdown to stop the server from crashing because the current tick takes a long time due to all the tests Bukkit.getScheduler().runTaskLater(Skript.this, () -> { if (TestMode.JUNIT && !EffObjectives.isJUnitComplete()) - TestTracker.testFailed(EffObjectives.getFailedObjectivesString()); + EffObjectives.fail(); info("Collecting results to " + TestMode.RESULTS_FILE); String results = new Gson().toJson(TestTracker.collectResults()); @@ -771,7 +809,7 @@ protected void afterErrors() { SkriptConfig.defaultEventPriority.value().name().toLowerCase(Locale.ENGLISH).replace('_', ' ') )); metrics.addCustomChart(new SimplePie("logPlayerCommands", () -> - SkriptConfig.logPlayerCommands.value().toString() + String.valueOf((SkriptConfig.logEffectCommands.value() || SkriptConfig.logPlayerCommands.value())) )); metrics.addCustomChart(new SimplePie("maxTargetDistance", () -> SkriptConfig.maxTargetBlockDistance.value().toString() @@ -803,7 +841,7 @@ protected void afterErrors() { )); metrics.addCustomChart(new SimplePie("releaseChannel", SkriptConfig.releaseChannel::value)); Skript.metrics = metrics; - + /* * Start loading scripts */ @@ -854,10 +892,10 @@ protected void afterErrors() { throw Skript.exception(e); } }); - + } }); - + Bukkit.getPluginManager().registerEvents(new Listener() { @EventHandler public void onJoin(final PlayerJoinEvent e) { @@ -869,7 +907,7 @@ public void run() { SkriptUpdater updater = getUpdater(); if (updater == null) return; - + // Don't actually check for updates to avoid breaking Github rate limit if (updater.getReleaseStatus() == ReleaseStatus.OUTDATED) { // Last check indicated that an update is available @@ -884,17 +922,17 @@ public void run() { } } }, this); - + // Tell Timings that we are here! SkriptTimings.setSkript(this); } - + /** * Handles -Dskript.stuff command line arguments. */ private void handleJvmArguments() { Path folder = getDataFolder().toPath(); - + /* * Burger is a Python application that extracts data from Minecraft. * Datasets for most common versions are available for download. @@ -933,13 +971,13 @@ private void handleJvmArguments() { return; } } - + // Use BurgerHelper to create some mappings, then dump them as JSON try { BurgerHelper burger = new BurgerHelper(burgerInput); Map materials = burger.mapMaterials(); Map ids = BurgerHelper.mapIds(); - + Gson gson = new Gson(); Files.write(folder.resolve("materials_mappings.json"), gson.toJson(materials) .getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); @@ -950,18 +988,18 @@ private void handleJvmArguments() { } } } - + public static Version getMinecraftVersion() { return minecraftVersion; } - + /** * @return Whether this server is running CraftBukkit */ public static boolean isRunningCraftBukkit() { return serverPlatform == ServerPlatform.BUKKIT_CRAFTBUKKIT; } - + /** * @return Whether this server is running Minecraft major.minor or higher */ @@ -971,24 +1009,24 @@ public static boolean isRunningMinecraft(final int major, final int minor) { } return minecraftVersion.compareTo(major, minor) >= 0; } - + public static boolean isRunningMinecraft(final int major, final int minor, final int revision) { if (minecraftVersion.compareTo(UNKNOWN_VERSION) == 0) { updateMinecraftVersion(); } return minecraftVersion.compareTo(major, minor, revision) >= 0; } - + public static boolean isRunningMinecraft(final Version v) { if (minecraftVersion.compareTo(UNKNOWN_VERSION) == 0) { updateMinecraftVersion(); } return minecraftVersion.compareTo(v) >= 0; } - + /** * Used to test whether certain Bukkit features are supported. - * + * * @param className * @return Whether the given class exists. * @deprecated use {@link #classExists(String)} @@ -997,10 +1035,10 @@ public static boolean isRunningMinecraft(final Version v) { public static boolean supports(final String className) { return classExists(className); } - + /** * Tests whether a given class exists in the classpath. - * + * * @param className The {@link Class#getCanonicalName() canonical name} of the class * @return Whether the given class exists. */ @@ -1012,10 +1050,10 @@ public static boolean classExists(final String className) { return false; } } - + /** * Tests whether a method exists in the given class. - * + * * @param c The class * @param methodName The name of the method * @param parameterTypes The parameter types of the method @@ -1031,12 +1069,12 @@ public static boolean methodExists(final Class c, final String methodName, fi return false; } } - + /** * Tests whether a method exists in the given class, and whether the return type matches the expected one. *

* Note that this method doesn't work properly if multiple methods with the same name and parameters exist but have different return types. - * + * * @param c The class * @param methodName The name of the method * @param parameterTypes The parameter types of the method @@ -1053,10 +1091,10 @@ public static boolean methodExists(final Class c, final String methodName, fi return false; } } - + /** * Tests whether a field exists in the given class. - * + * * @param c The class * @param fieldName The name of the field * @return Whether the given field exists. @@ -1071,23 +1109,23 @@ public static boolean fieldExists(final Class c, final String fieldName) { return false; } } - + @Nullable static Metrics metrics; - + @Nullable public static Metrics getMetrics() { return metrics; } - + @SuppressWarnings("null") private final static Collection closeOnDisable = Collections.synchronizedCollection(new ArrayList()); - + /** * Registers a Closeable that should be closed when this plugin is disabled. *

* All registered Closeables will be closed after all scripts have been stopped. - * + * * @param closeable */ public static void closeOnDisable(final Closeable closeable) { @@ -1136,7 +1174,17 @@ public void onPluginDisable(PluginDisableEvent event) { try { // Spigot removed the mapping for this method in 1.18, so its back to obfuscated method // 1.19 mapping is u and 1.18 is v - String isRunningMethod = Skript.isRunningMinecraft(1, 19) ? "u" : Skript.isRunningMinecraft(1, 18) ? "v" :"isRunning"; + String isRunningMethod = "isRunning"; + + if (Skript.isRunningMinecraft(1, 20, 5)) { + isRunningMethod = "x"; + } else if (Skript.isRunningMinecraft(1, 20)) { + isRunningMethod = "v"; + } else if (Skript.isRunningMinecraft(1, 19)) { + isRunningMethod = "u"; + } else if (Skript.isRunningMinecraft(1, 18)) { + isRunningMethod = "v"; + } IS_RUNNING = MC_SERVER.getClass().getMethod(isRunningMethod); } catch (NoSuchMethodException e) { throw new RuntimeException(e); @@ -1168,13 +1216,14 @@ public void onDisable() { if (disabled) return; disabled = true; + this.experimentRegistry = null; if (!partDisabled) { beforeDisable(); } - + Bukkit.getScheduler().cancelTasks(this); - + for (Closeable c : closeOnDisable) { try { c.close(); @@ -1183,46 +1232,46 @@ public void onDisable() { } } } - + // ================ CONSTANTS, OPTIONS & OTHER ================ - + public final static String SCRIPTSFOLDER = "scripts"; - + public static void outdatedError() { error("Skript v" + getInstance().getDescription().getVersion() + " is not fully compatible with Bukkit " + Bukkit.getVersion() + ". Some feature(s) will be broken until you update Skript."); } - + public static void outdatedError(final Exception e) { outdatedError(); if (testing()) e.printStackTrace(); } - + /** * A small value, useful for comparing doubles or floats. *

* E.g. to test whether two floating-point numbers are equal: - * + * *

 	 * Math.abs(a - b) < Skript.EPSILON
 	 * 
- * + * * or whether a location is within a specific radius of another location: - * + * *
 	 * location.distanceSquared(center) - radius * radius < Skript.EPSILON
 	 * 
- * + * * @see #EPSILON_MULT */ public final static double EPSILON = 1e-10; /** * A value a bit larger than 1 - * + * * @see #EPSILON */ public final static double EPSILON_MULT = 1.00001; - + /** * The maximum ID a block can have in Minecraft. */ @@ -1231,19 +1280,19 @@ public static void outdatedError(final Exception e) { * The maximum data value of Minecraft, i.e. Short.MAX_VALUE - Short.MIN_VALUE. */ public final static int MAXDATAVALUE = Short.MAX_VALUE - Short.MIN_VALUE; - + // TODO localise Infinity, -Infinity, NaN (and decimal point?) public static String toString(final double n) { return StringUtils.toString(n, SkriptConfig.numberAccuracy.value()); } - + public final static UncaughtExceptionHandler UEH = new UncaughtExceptionHandler() { @Override public void uncaughtException(final @Nullable Thread t, final @Nullable Throwable e) { Skript.exception(e, "Exception in thread " + (t == null ? null : t.getName())); } }; - + /** * Creates a new Thread and sets its UncaughtExceptionHandler. The Thread is not started automatically. */ @@ -1252,38 +1301,38 @@ public static Thread newThread(final Runnable r, final String name) { t.setUncaughtExceptionHandler(UEH); return t; } - + // ================ REGISTRATIONS ================ - + private static boolean acceptRegistrations = true; - + public static boolean isAcceptRegistrations() { if (instance == null) throw new IllegalStateException("Skript was never loaded"); return acceptRegistrations && instance.isEnabled(); } - + public static void checkAcceptRegistrations() { - if (!isAcceptRegistrations()) + if (!isAcceptRegistrations() && !Skript.testing()) throw new SkriptAPIException("Registration can only be done during plugin initialization"); } - + private static void stopAcceptingRegistrations() { Converters.createChainedConverters(); acceptRegistrations = false; - + Classes.onRegistrationsStop(); } - + // ================ ADDONS ================ - + private final static HashMap addons = new HashMap<>(); - + /** * Registers an addon to Skript. This is currently not required for addons to work, but the returned {@link SkriptAddon} provides useful methods for registering syntax elements * and adding new strings to Skript's localization system (e.g. the required "types.[type]" strings for registered classes). - * + * * @param p The plugin */ public static SkriptAddon registerAddon(final JavaPlugin p) { @@ -1294,25 +1343,25 @@ public static SkriptAddon registerAddon(final JavaPlugin p) { addons.put(p.getName(), addon); return addon; } - + @Nullable public static SkriptAddon getAddon(final JavaPlugin p) { return addons.get(p.getName()); } - + @Nullable public static SkriptAddon getAddon(final String name) { return addons.get(name); } - + @SuppressWarnings("null") public static Collection getAddons() { return Collections.unmodifiableCollection(addons.values()); } - + @Nullable private static SkriptAddon addon; - + /** * @return A {@link SkriptAddon} representing Skript. */ @@ -1333,7 +1382,7 @@ public static SkriptAddon getAddonInstance() { /** * registers a {@link Condition}. - * + * * @param condition The condition's class * @param patterns Skript patterns to match this condition */ @@ -1344,10 +1393,10 @@ public static void registerCondition(final Class condit conditions.add(info); statements.add(info); } - + /** * Registers an {@link Effect}. - * + * * @param effect The effect's class * @param patterns Skript patterns to match this effect */ @@ -1376,11 +1425,11 @@ public static void registerSection(Class section, String. public static Collection> getStatements() { return statements; } - + public static Collection> getConditions() { return conditions; } - + public static Collection> getEffects() { return effects; } @@ -1390,14 +1439,14 @@ public static Collection> getSections() { } // ================ EXPRESSIONS ================ - + private final static List> expressions = new ArrayList<>(100); - + private final static int[] expressionTypesStartIndices = new int[ExpressionType.values().length]; - + /** * Registers an expression. - * + * * @param c The expression's class * @param returnType The superclass of all values returned by the expression * @param type The expression's {@link ExpressionType type}. This is used to determine in which order to try to parse expressions. @@ -1415,12 +1464,12 @@ public static , T> void registerExpression(final Class> getExpressions() { return expressions.iterator(); } - + public static Iterator> getExpressions(final Class... returnTypes) { return new CheckedIterator<>(getExpressions(), new NullableChecker>() { @Override @@ -1436,14 +1485,15 @@ public boolean check(final @Nullable ExpressionInfo i) { } }); } - + // ================ EVENTS ================ + private static final List> events = new ArrayList<>(50); private static final List> structures = new ArrayList<>(10); /** * Registers an event. - * + * * @param name Capitalised name of the event without leading "On" which is added automatically (Start the name with an asterisk to prevent this). Used for error messages and * the documentation. * @param c The event's class @@ -1455,10 +1505,10 @@ public boolean check(final @Nullable ExpressionInfo i) { public static SkriptEventInfo registerEvent(String name, Class c, Class event, String... patterns) { return registerEvent(name, c, new Class[] {event}, patterns); } - + /** * Registers an event. - * + * * @param name The name of the event, used for error messages * @param c The event's class * @param events The Bukkit events this event applies to @@ -1471,10 +1521,10 @@ public static SkriptEventInfo registerEvent(String na String[] transformedPatterns = new String[patterns.length]; for (int i = 0; i < patterns.length; i++) - transformedPatterns[i] = "[on] " + SkriptEvent.fixPattern(patterns[i]) + SkriptEventInfo.EVENT_PRIORITY_SYNTAX; + transformedPatterns[i] = SkriptEvent.fixPattern(patterns[i]); SkriptEventInfo r = new SkriptEventInfo<>(name, transformedPatterns, c, originClassPath, events); - structures.add(r); + Skript.events.add(r); return r; } @@ -1485,6 +1535,13 @@ public static void registerStructure(Class c, String... structures.add(structureInfo); } + public static void registerSimpleStructure(Class c, String... patterns) { + checkAcceptRegistrations(); + String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); + StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath, true); + structures.add(structureInfo); + } + public static void registerStructure(Class c, EntryValidator entryValidator, String... patterns) { checkAcceptRegistrations(); String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); @@ -1492,14 +1549,8 @@ public static void registerStructure(Class c, EntryVali structures.add(structureInfo); } - /** - * Modifications made to the returned Collection will not be reflected in the events available for parsing. - */ public static Collection> getEvents() { - // Only used in documentation generation, so generating a new list each time is fine - return (Collection>) (Collection) structures.stream() - .filter(info -> info instanceof SkriptEventInfo) - .collect(Collectors.toList()); + return events; } public static List> getStructures() { @@ -1507,10 +1558,10 @@ public static List> getStructures() { } // ================ COMMANDS ================ - + /** * Dispatches a command with calling command events - * + * * @param sender * @param command * @return Whether the command was run @@ -1535,39 +1586,39 @@ public static boolean dispatchCommand(final CommandSender sender, final String c return false; } } - + // ================ LOGGING ================ - + public static boolean logNormal() { return SkriptLogger.log(Verbosity.NORMAL); } - + public static boolean logHigh() { return SkriptLogger.log(Verbosity.HIGH); } - + public static boolean logVeryHigh() { return SkriptLogger.log(Verbosity.VERY_HIGH); } - + public static boolean debug() { return SkriptLogger.debug(); } - + public static boolean testing() { return debug() || Skript.class.desiredAssertionStatus(); } - + public static boolean log(final Verbosity minVerb) { return SkriptLogger.log(minVerb); } - + public static void debug(final String info) { if (!debug()) return; SkriptLogger.log(SkriptLogger.DEBUG, info); } - + /** * @see SkriptLogger#log(Level, String) */ @@ -1575,7 +1626,7 @@ public static void debug(final String info) { public static void info(final String info) { SkriptLogger.log(Level.INFO, info); } - + /** * @see SkriptLogger#log(Level, String) */ @@ -1583,7 +1634,7 @@ public static void info(final String info) { public static void warning(final String warning) { SkriptLogger.log(Level.WARNING, warning); } - + /** * @see SkriptLogger#log(Level, String) */ @@ -1592,54 +1643,54 @@ public static void error(final @Nullable String error) { if (error != null) SkriptLogger.log(Level.SEVERE, error); } - + /** * Use this in {@link Expression#init(Expression[], int, Kleenean, ch.njol.skript.lang.SkriptParser.ParseResult)} (and other methods that are called during the parsing) to log * errors with a specific {@link ErrorQuality}. - * + * * @param error * @param quality */ public static void error(final String error, final ErrorQuality quality) { SkriptLogger.log(new LogEntry(SkriptLogger.SEVERE, quality, error)); } - + private final static String EXCEPTION_PREFIX = "#!#! "; - + /** * Used if something happens that shouldn't happen - * + * * @param info Description of the error and additional information * @return an EmptyStacktraceException to throw if code execution should terminate. */ public static EmptyStacktraceException exception(final String... info) { return exception(null, info); } - + public static EmptyStacktraceException exception(final @Nullable Throwable cause, final String... info) { return exception(cause, null, null, info); } - + public static EmptyStacktraceException exception(final @Nullable Throwable cause, final @Nullable Thread thread, final String... info) { return exception(cause, thread, null, info); } - + public static EmptyStacktraceException exception(final @Nullable Throwable cause, final @Nullable TriggerItem item, final String... info) { return exception(cause, null, item, info); } - + /** * Maps Java packages of plugins to descriptions of said plugins. * This is only done for plugins that depend or soft-depend on Skript. */ private static Map pluginPackages = new HashMap<>(); private static boolean checkedPlugins = false; - + /** * Set by Skript when doing something that users shouldn't do. */ private static boolean tainted = false; - + /** * Set to true when an exception is thrown. */ @@ -1655,7 +1706,7 @@ public static void markErrored() { /** * Used if something happens that shouldn't happen - * + * * @param cause exception that shouldn't occur * @param info Description of the error and additional information * @return an EmptyStacktraceException to throw if code execution should terminate. @@ -1669,11 +1720,11 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina } // First error: gather plugin package information - if (!checkedPlugins) { + if (!checkedPlugins) { for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { if (plugin.getName().equals("Skript")) // Don't track myself! continue; - + PluginDescriptionFile desc = plugin.getDescription(); if (desc.getDepend().contains("Skript") || desc.getSoftDepend().contains("Skript")) { // Take actual main class out from the qualified name @@ -1682,24 +1733,24 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina for (int i = 0; i < parts.length - 1; i++) { name.append(parts[i]).append('.'); } - + // Put this to map pluginPackages.put(name.toString(), desc); if (Skript.debug()) Skript.info("Identified potential addon: " + desc.getFullName() + " (" + name.toString() + ")"); } } - + checkedPlugins = true; // No need to do this next time } - + String issuesUrl = "https://github.com/SkriptLang/Skript/issues"; - + logEx(); logEx("[Skript] Severe Error:"); logEx(info); logEx(); - + // Parse something useful out of the stack trace StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); Set stackPlugins = new HashSet<>(); @@ -1709,9 +1760,9 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina stackPlugins.add(e.getValue()); // Yes? Add it to list } } - + SkriptUpdater updater = Skript.getInstance().getUpdater(); - + // Check if server platform is supported if (tainted) { logEx("Skript is running with developer command-line options."); @@ -1753,7 +1804,7 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina String website = desc.getWebsite(); if (website != null && !website.isEmpty()) // Add website if found pluginsMessage.append(" (").append(desc.getWebsite()).append(")"); - + pluginsMessage.append(" "); } logEx(pluginsMessage.toString()); @@ -1766,12 +1817,12 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina String website = desc.getWebsite(); if (website != null && !website.isEmpty()) // Add website if found pluginsMessage.append(" (").append(desc.getWebsite()).append(")"); - + pluginsMessage.append(" "); } logEx(pluginsMessage.toString()); } - + logEx("You should try disabling those plugins one by one, trying to find which one causes it."); logEx("If the error doesn't disappear even after disabling all listed plugins, it is probably Skript issue."); logEx("In that case, you will be given instruction on how should you report it."); @@ -1779,7 +1830,7 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina logEx("Only if the author tells you to do so, report it to Skript's issue tracker."); } } - + logEx(); logEx("Stack trace:"); if (cause == null || cause.getStackTrace().length == 0) { @@ -1794,7 +1845,7 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina cause = cause.getCause(); first = false; } - + logEx(); logEx("Version Information:"); if (updater != null) { @@ -1830,14 +1881,14 @@ public static EmptyStacktraceException exception(@Nullable Throwable cause, fina logEx(); logEx("End of Error."); logEx(); - + return new EmptyStacktraceException(); } - + static void logEx() { SkriptLogger.LOGGER.severe(EXCEPTION_PREFIX); } - + static void logEx(final String... lines) { for (final String line : lines) SkriptLogger.LOGGER.severe(EXCEPTION_PREFIX + line); @@ -1852,7 +1903,7 @@ public static String getSkriptPrefix() { public static void info(final CommandSender sender, final String info) { sender.sendMessage(Utils.replaceEnglishChatStyles(getSkriptPrefix() + info)); } - + /** * @param message * @param permission @@ -1861,25 +1912,25 @@ public static void info(final CommandSender sender, final String info) { public static void broadcast(final String message, final String permission) { Bukkit.broadcast(Utils.replaceEnglishChatStyles(getSkriptPrefix() + message), permission); } - + public static void adminBroadcast(final String message) { broadcast(message, "skript.admin"); } - + /** * Similar to {@link #info(CommandSender, String)} but no [Skript] prefix is added. - * + * * @param sender * @param info */ public static void message(final CommandSender sender, final String info) { sender.sendMessage(Utils.replaceEnglishChatStyles(info)); } - + public static void error(final CommandSender sender, final String error) { sender.sendMessage(Utils.replaceEnglishChatStyles(getSkriptPrefix() + ChatColor.DARK_RED + error)); } - + /** * Gets the updater instance currently used by Skript. * @return SkriptUpdater instance. @@ -1888,5 +1939,5 @@ public static void error(final CommandSender sender, final String error) { public SkriptUpdater getUpdater() { return updater; } - + } diff --git a/src/main/java/ch/njol/skript/SkriptCommand.java b/src/main/java/ch/njol/skript/SkriptCommand.java index 7f01e3ff331..0bc25391267 100644 --- a/src/main/java/ch/njol/skript/SkriptCommand.java +++ b/src/main/java/ch/njol/skript/SkriptCommand.java @@ -62,19 +62,19 @@ public class SkriptCommand implements CommandExecutor { // TODO /skript scripts show/list - lists all enabled and/or disabled scripts in the scripts folder and/or subfolders (maybe add a pattern [using * and **]) // TODO document this command on the website private static final CommandHelp SKRIPT_COMMAND_HELP = new CommandHelp("/skript", SkriptColor.LIGHT_CYAN, CONFIG_NODE + ".help") - .add(new CommandHelp("reload", SkriptColor.DARK_RED) + .add(new CommandHelp("reload", SkriptColor.DARK_CYAN) .add("all") .add("config") .add("aliases") .add("scripts") .add("