diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6d847a5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Build and Publish Release +on: + push: + tags: + - '*' +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: gradle + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + # build + - name: Build with Gradle Wrapper + run: ./gradlew assemble + env: + MVNDI_MVN_USER: ${{ secrets.MVNDI_MVN_USER }} + MVNDI_MVN_KEY: ${{ secrets.MVNDI_MVN_KEY }} + - name: Publish to Remote + run: ./gradlew publish + env: + MVNDI_MVN_USER: ${{ secrets.MVNDI_MVN_USER }} + MVNDI_MVN_KEY: ${{ secrets.MVNDI_MVN_KEY }} + + - name: Get version & release name + id: version + run: | + echo "version=$(./gradlew -q echoVersion)" >> $GITHUB_OUTPUT + echo "releaseName=$(./gradlew -q echoReleaseName)" >> $GITHUB_OUTPUT + + - name: Print version & release name + run: | + echo "Version: ${{ steps.version.outputs.version }}" + echo "Release Name: ${{ steps.version.outputs.releaseName }}" + + + - name: Create an empty CHANGELOG.md if missing + run: | + if [ ! -f CHANGELOG.md ]; then + touch CHANGELOG.md + fi + + - name: Read the first lines of CHANGELOG.md from gradle + id: changelog + run: | + { + echo "changelog<> $GITHUB_OUTPUT + + + - name: Create Release + id: createRelease + uses: ncipollo/release-action@v1.14.0 + with: + allowUpdates: true + updateOnlyUnreleased: true + artifacts: build/libs/${{ github.event.repository.name }}-${{ steps.version.outputs.version }}.jar + token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ steps.version.outputs.version }} + name: ${{ steps.version.outputs.releaseName }} + body: ${{ steps.changelog.outputs.changelog }} + # prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }} #Always release for now + skipIfReleaseExists: true + + # - name: Publish to hangar & modrinth + # env: + # # Make sure you have added the repository secrets in the repository's settings + # HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }} + # MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + # run: ./gradlew assemble publishPluginPublicationToHangar modrinth \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3dadcbe..bf58377 100644 --- a/.gitignore +++ b/.gitignore @@ -486,3 +486,6 @@ FodyWeavers.xsd # Additional files built by Visual Studio # End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,java,gradle,kotlin + + +run*/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a84363d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# 1.6.0 +Improve performances. +Save user settings on the Minecraft world. +Add an option to disable the light lock message. + +# 1.5.0 +Fix translation & add French translation. + +# 1.4.7 +Fix not using the same location for region scheduler & inside the region scheduler as player eye location. + +# 1.4.6 +Fix light removing + +# 1.4.5 +Fix light being removed from the new location instead of the old location. + +# 1.4.4 +Fix player dying exception + +# 1.4.3 +Folia fix: run find best block logic in region thread. + +# 1.4.2 +Fix light location being compare between block & player eye wich was never the same making the light flicker. + +# 1.3.4 +Update to 1.21.11 by using co.aikar:acf-paper instead of the custom NMS commands code from github.xCykrix:spigotdevkit. +Reenable light toggle. +Remove light-culling-distance config option as we now use real light that can be seen from any distance. \ No newline at end of file diff --git a/README.md b/README.md index 4f3ce84..84614fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DynamicLights -Emit Light from Held Items using the 1.17 Light Blocks in Spigot/Paper Servers. +Emit Light from Held Items using the 1.17 Light Blocks in Paper Servers. Discord: https://discord.gg/RHJQFPgbke diff --git a/build.gradle.kts b/build.gradle.kts index ef8c6c2..069df99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,34 +4,38 @@ plugins { id("io.github.goooler.shadow") version "8.1.8" id("java") id("java-library") + id("xyz.jpenilla.run-paper") version "2.3.1" } group = "github.xCykrix" -version = "1.3.0-FOR-MC1.20" +version = "1.6.0" +description="Dynamic Lights for Minecraft Servers without requiring Modding." +val mainMinecraftVersion = "1.21.11" +val supportedMinecraftVersions = "1.21.11 - 1.21.11" repositories { + mavenLocal() mavenCentral() - // Spigot Repository - maven { - url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") - } + // Paper + maven("https://repo.papermc.io/repository/maven-public/") // Upstream GitHub Packages maven { url = uri("https://maven.pkg.github.com/xCykrix/SpigotDevkit") credentials { - username = project.findProperty("GITHUB_ACTOR").toString() ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("GITHUB_TOKEN").toString() ?: System.getenv("GITHUB_TOKEN") + username = "" //project.findProperty("GITHUB_ACTOR").toString() ?: System.getenv("GITHUB_ACTOR") + password = "" //project.findProperty("GITHUB_TOKEN").toString() ?: System.getenv("GITHUB_TOKEN") } } } dependencies { - compileOnly("org.spigotmc:spigot-api:1.21.6-R0.1-SNAPSHOT") - implementation("github.xCykrix:spigotdevkit:1.1.0-CAPI1.20") { - isTransitive = false - } + compileOnly("io.papermc.paper:paper-api:$mainMinecraftVersion-R0.1-SNAPSHOT") + // implementation("github.xCykrix:spigotdevkit:1.1.2") { + // isTransitive = false + // } + implementation("co.aikar:acf-paper:0.5.1-SNAPSHOT") } // Shadow Task @@ -39,8 +43,39 @@ tasks.named("shadowJar") { archiveClassifier = null; } -// Target Java Build (Java 16 - Minecraft 1.17.x) -val targetJavaVersion = 16 +publishing { + publications.create("maven") { + from(components["java"]) + } +} + +tasks { + assemble { + dependsOn(shadowJar) + } + processResources { + val props = mapOf( + "name" to project.name, + "version" to project.version, + "description" to project.description, + "apiVersion" to "1.21.11", + "group" to project.group + ) + inputs.properties(props) + filesMatching("plugin.yml") { + expand(props) + } + } + runServer { + // Configure the Minecraft version for our task. + // This is the only required configuration besides applying the plugin. + // Your plugin's jar (or shadowJar if present) will be used automatically. + minecraftVersion(mainMinecraftVersion) + } + runPaper.folia.registerTask() +} + +val targetJavaVersion = 21 java { val javaVersion = JavaVersion.toVersion(targetJavaVersion) sourceCompatibility = javaVersion @@ -49,3 +84,83 @@ java { toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) } } + +tasks.register("echoVersion") { + group = "documentation" + description = "Displays the version." + doLast { + println("${project.version}") + } +} + +tasks.register("echoReleaseName") { + group = "documentation" + description = "Displays the release name." + doLast { + println("${project.version} [${supportedMinecraftVersions}]") + } +} + +val extractChangelog = tasks.register("extractChangelog") { + group = "documentation" + description = "Extracts the changelog for the current project version from CHANGELOG.md, including the version header." + + val changelog: Property = project.objects.property(String::class) + outputs.upToDateWhen { false } + + doLast { + val version = project.version.toString() + val changelogFile = project.file("CHANGELOG.md") + + if (!changelogFile.exists()) { + println("CHANGELOG.md not found.") + changelog.set("No changelog found.") + return@doLast + } + + val lines = changelogFile.readLines() + val entries = mutableListOf() + var foundVersion = false + + for (line in lines) { + when { + // Include the version line itself + line.trim().equals("# $version", ignoreCase = true) -> { + foundVersion = true + entries.add(line) + } + // Stop collecting at the next version header + foundVersion && line.trim().startsWith("# ") -> break + // Collect lines after the version header + foundVersion -> entries.add(line) + } + } + + val result = if (entries.isEmpty()) { + "Update to $version." + } else { + entries.joinToString("\n").trim() + } + + // println("Changelog for version $version:\n$result") + changelog.set(result) + } + + // Make changelog accessible from other tasks + extensions.add(Property::class.java, "changelog", changelog) +} + +tasks.register("echoLatestVersionChangelog") { + group = "documentation" + description = "Displays the latest version change." + + dependsOn(tasks.named("extractChangelog")) + + doLast { + println((extractChangelog.get().extensions.findByType(Property::class.java) as Property).get()) + } +} + + +val versionString: String = version as String +val isRelease: Boolean = !versionString.contains("SNAPSHOT") \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7705927 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + 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. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + 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 optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/github/xCykrix/dynamicLights/DynamicLights.java b/src/main/java/github/xCykrix/dynamicLights/DynamicLights.java index aaecf8e..42f27f8 100644 --- a/src/main/java/github/xCykrix/dynamicLights/DynamicLights.java +++ b/src/main/java/github/xCykrix/dynamicLights/DynamicLights.java @@ -1,64 +1,90 @@ package github.xCykrix.dynamicLights; -import github.xCykrix.DevkitPlugin; -import github.xCykrix.dynamicLights.command.BaseCommand; +import co.aikar.commands.PaperCommandManager; +import github.xCykrix.dynamicLights.command.DynamicLightsCommand; import github.xCykrix.dynamicLights.event.PlayerHandler; import github.xCykrix.dynamicLights.util.LightManager; import github.xCykrix.dynamicLights.util.LightSource; -import github.xCykrix.helper.LanguageFile; -import github.xCykrix.plugin.AdventurePlugin; -import github.xCykrix.plugin.CommandPlugin; -import github.xCykrix.plugin.ConfigurationPlugin; -import github.xCykrix.plugin.H2MVStorePlugin; -import github.xCykrix.records.Resource; - -public final class DynamicLights extends DevkitPlugin { +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.translation.Argument; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.plugin.java.JavaPlugin; + +public final class DynamicLights extends JavaPlugin { // Core APIs. - public static ConfigurationPlugin configuration; - public static AdventurePlugin adventure; - public static CommandPlugin command; + // public static ConfigurationPlugin configuration; + // public static AdventurePlugin adventure; + // public static CommandPlugin command; // Third Party APIs. - public static H2MVStorePlugin h2; + // public static H2MVStorePlugin h2; // Internal APIs. - public static LanguageFile language; + // public static LanguageFile language; public static LightSource source; public static LightManager manager; + private Translations translations; - @Override + // @Override protected void pre() { - configuration = this.register(new ConfigurationPlugin(this)); - adventure = this.register(new AdventurePlugin(this)); - command = this.register(new CommandPlugin(this)); - h2 = this.register(new H2MVStorePlugin(this)); + // configuration = this.register(new ConfigurationPlugin(this)); + // adventure = this.register(new AdventurePlugin(this)); + // command = this.register(new CommandPlugin(this)); + // h2 = this.register(new H2MVStorePlugin(this)); } - @Override + // @Override public void initialize() { + + // save default config files + saveDefaultConfig(); + saveResource("lights.yml", false); + + this.translations = new Translations(); + translations.reload(); + + // Register Configurations - configuration - .register(new Resource("config.yml", null, this.getResource("config.yml"))) - .register(new Resource("lights.yml", null, this.getResource("lights.yml"))) - .registerLanguageFile(this.getResource("language.yml")); - language = configuration.getLanguageFile(); + // configuration.register(new Resource("config.yml", null, this.getResource("config.yml"))) + // .register(new Resource("lights.yml", null, this.getResource("lights.yml"))).registerLanguageFile(this.getResource("language.yml")); + // language = configuration.getLanguageFile(); // Register Internal APIs. source = new LightSource(this); source.initialize(); manager = new LightManager(this); - manager.initialize(); + // manager.initialize(); // Register Events this.getServer().getPluginManager().registerEvents(new PlayerHandler(this), this); // Register Commands - BaseCommand.getOrCreate(this).generate(command); + PaperCommandManager manager = new PaperCommandManager(this); + manager.registerCommand(new DynamicLightsCommand(this)); } @Override + public void onEnable() { initialize(); } + + @Override + public void reloadConfig() { + super.reloadConfig(); + translations.reload(); + } + + // @Override public void shutdown() { manager.shutdown(); source.shutdown(); } + + public static Component translate(String key) { + return Component.translatable(key, Argument.string("prefix", PlainTextComponentSerializer.plainText().serialize(Component.translatable("prefix")))); + } + + public static DynamicLights getInstance() { return DynamicLights.getPlugin(DynamicLights.class); } + + public Translations getTranslations() { + return translations; + } } diff --git a/src/main/java/github/xCykrix/dynamicLights/Translations.java b/src/main/java/github/xCykrix/dynamicLights/Translations.java new file mode 100644 index 0000000..1f9df31 --- /dev/null +++ b/src/main/java/github/xCykrix/dynamicLights/Translations.java @@ -0,0 +1,142 @@ +package github.xCykrix.dynamicLights; + +import com.google.common.collect.Maps; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.minimessage.translation.MiniMessageTranslationStore; +import net.kyori.adventure.translation.GlobalTranslator; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.codehaus.plexus.util.FileUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class Translations { + private final Path localeFolder = DynamicLights.getInstance().getDataPath().resolve("locale"); + private final Key name = Key.key("minigamelib", "locale"); + private MiniMessageTranslationStore storage = MiniMessageTranslationStore.create(name); + + public void reload() { + GlobalTranslator.translator().removeSource(storage); + storage = MiniMessageTranslationStore.create(name); + + if (!Files.isDirectory(localeFolder)) { + try { + Files.createDirectories(localeFolder); + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + // create default translation if not exists + Locale.availableLocales().forEach(locale -> { + String languageKey = locale.toString(); + Path defaultLocaleFile = localeFolder.resolve(languageKey + ".yml"); + String outLocalePath = "locale/" + languageKey + ".yml"; + if (!Files.exists(defaultLocaleFile) && DynamicLights.getInstance().getResource(outLocalePath) != null) { + DynamicLights.getInstance().saveResource(outLocalePath, false); + } + }); + + List registered = new ArrayList<>(1); + registered.add(Locale.ENGLISH); + + try (var str = Files.list(localeFolder)) { + str.forEach(path -> { + String name = path.getFileName().toString(); + if (!name.endsWith(".yml")) { + return; + } + + String localeName = FileUtils.basename(name, ".yml"); + + Locale locale = Locale.forLanguageTag(localeName); + if (locale != null) load(locale); + registered.add(locale); + DynamicLights.getInstance().getLogger().info("Loaded locale: " + localeName); + }); + } catch (Exception e) { + e.printStackTrace(); + } + + Map english = getEnglishKeys(); + + Locale.availableLocales().forEach(locale -> { + if (locale.getLanguage().isEmpty()) { + return; + } + + if (registered.contains(locale)) { + return; + } + + // Do not save language for each country, just save the country less version. + if(!locale.getLanguage().equals(locale.toString())) { + return; + } + + storage.registerAll(locale, english); + }); + + GlobalTranslator.translator().addSource(storage); + } + + public Component english(TranslatableComponent component) { + Component c = storage.translate(component, Locale.ENGLISH); + return c == null ? component : c; + } + + public Component translate(TranslatableComponent component, Locale locale) { + Component c = storage.translate(component, locale); + DynamicLights.getInstance().getLogger().info("Translated: " + component.toString() + " -> " + c.toString()); + return c == null ? storage.translate(component, Locale.ENGLISH) : c; + } + + private void load(Locale locale) { + Path localeFile = localeFolder.resolve(locale.getLanguage() + ".yml"); + if (!Files.exists(localeFile)) { + try { + Files.createFile(localeFile); + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + Map map = Maps.newHashMap(); + FileConfiguration configuration = YamlConfiguration.loadConfiguration(localeFile.toFile()); + + configuration.getValues(true).forEach((k, o) -> { + if (o instanceof MemorySection) { + return; + } + + map.put(k, (String) o); + }); + + storage.registerAll(locale, map); + } + + private Map getEnglishKeys() { + Map map = Maps.newHashMap(); + FileConfiguration configuration = YamlConfiguration.loadConfiguration(localeFolder.resolve("en.yml").toFile()); + + configuration.getValues(true).forEach((k, o) -> { + if (o instanceof MemorySection) { + return; + } + + map.put(k, (String) o); + }); + + return map; + } +} diff --git a/src/main/java/github/xCykrix/dynamicLights/command/BaseCommand.java b/src/main/java/github/xCykrix/dynamicLights/command/BaseCommand.java deleted file mode 100644 index c75003b..0000000 --- a/src/main/java/github/xCykrix/dynamicLights/command/BaseCommand.java +++ /dev/null @@ -1,80 +0,0 @@ -package github.xCykrix.dynamicLights.command; - -import dist.xCykrix.shade.dev.jorel.commandapi.CommandAPICommand; -import dist.xCykrix.shade.org.apache.commons.lang3.exception.ExceptionUtils; -import github.xCykrix.DevkitPlugin; -import github.xCykrix.dynamicLights.DynamicLights; -import github.xCykrix.extendable.DevkitSimpleState; -import github.xCykrix.plugin.CommandPlugin; -import java.io.IOException; -import java.util.Objects; - -public class BaseCommand extends DevkitSimpleState { - - public static BaseCommand core; - - public static BaseCommand getOrCreate(DevkitPlugin plugin) { - if (core == null) { - core = new BaseCommand(plugin); - } - return core; - } - - public BaseCommand(DevkitPlugin plugin) { - super(plugin); - } - - public void generate(CommandPlugin commandPlugin) { - new CommandAPICommand("dynamiclights") - .withAliases("dynamiclight", "dl") - .withSubcommand( - new CommandAPICommand("toggle") - .withPermission("dynamiclights.toggle") - .executesPlayer(info -> { - String uuid = info.sender().getUniqueId().toString(); - boolean current = DynamicLights.manager.toggles.getOrDefault(uuid, DynamicLights.manager.toggle); - if (!current) { - DynamicLights.adventure.get().player(info.sender()).sendMessage( - DynamicLights.language.getComponentFromID("toggle-on", true)); - DynamicLights.manager.toggles.put(uuid, true); - } else { - DynamicLights.adventure.get().player(info.sender()).sendMessage( - DynamicLights.language.getComponentFromID("toggle-off", true)); - DynamicLights.manager.toggles.put(uuid, false); - } - })) - .withSubcommand( - new CommandAPICommand("lock") - .withPermission("dynamiclights.lock") - .executesPlayer(info -> { - String uuid = info.sender().getUniqueId().toString(); - boolean current = DynamicLights.manager.locks.getOrDefault(uuid, true); - if (!current) { - DynamicLights.adventure.get().player(info.sender()).sendMessage( - DynamicLights.language.getComponentFromID("enable-lock", true)); - DynamicLights.manager.locks.put(uuid, true); - } else { - DynamicLights.adventure.get().player(info.sender()).sendMessage( - DynamicLights.language.getComponentFromID("disable-lock", true)); - DynamicLights.manager.locks.put(uuid, false); - } - })) - .withSubcommand( - new CommandAPICommand("reload") - .withPermission("dynamiclights.reload") - .executes(info -> { - try { - Objects.requireNonNull(DynamicLights.configuration.getYAMLFile("lights.yml")).reload(); - DynamicLights.source.initialize(); - DynamicLights.adventure.get().sender(info.sender()).sendMessage( - DynamicLights.language.getComponentFromID("reload", true)); - } catch (IOException | NullPointerException ex) { - DynamicLights.adventure.get().sender(info.sender()).sendMessage( - DynamicLights.language.getComponentFromID("reload-error", true)); - this.plugin.getLogger().severe("Failed to reload lights.yml."); - this.plugin.getLogger().severe(ExceptionUtils.getStackTrace(ex)); - } - })) - .register(this.plugin); - } -} diff --git a/src/main/java/github/xCykrix/dynamicLights/command/DynamicLightsCommand.java b/src/main/java/github/xCykrix/dynamicLights/command/DynamicLightsCommand.java new file mode 100644 index 0000000..32c001d --- /dev/null +++ b/src/main/java/github/xCykrix/dynamicLights/command/DynamicLightsCommand.java @@ -0,0 +1,95 @@ +package github.xCykrix.dynamicLights.command; + +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Description; +import co.aikar.commands.annotation.Subcommand; +import github.xCykrix.dynamicLights.DynamicLights; +import github.xCykrix.dynamicLights.util.PlayerUtil; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +@CommandAlias("dynamiclights|dynamiclight|dl") +public class DynamicLightsCommand extends co.aikar.commands.BaseCommand {// extends DevkitSimpleState { + + + private static JavaPlugin plugin; + + public DynamicLightsCommand(JavaPlugin plugin) { this.plugin = plugin; } + + @Subcommand("reload") + @Description("Reloads the plugin config and data files") + @CommandPermission("dynamiclights.reload") + public static void onReload(CommandSender commandSender) { + try { + // Objects.requireNonNull(DynamicLights.configuration.getYAMLFile("lights.yml")).reload(); + YamlConfiguration lights = YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder(), "lights.yml")); + DynamicLights.source.initialize(); + DynamicLights.getInstance().getTranslations().reload(); + commandSender.sendMessage(DynamicLights.translate("reload")); + } catch (NullPointerException ex) { + commandSender.sendMessage(DynamicLights.translate("reload-error")); + plugin.getLogger().severe("Failed to reload lights.yml."); + plugin.getLogger().severe(getStackTrace(ex)); + } + } + + public static String getStackTrace(Throwable throwable) { + if (throwable == null) { + return ""; + } else { + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw, true)); + return sw.toString(); + } + } + + @Subcommand("toggle") + @Description("Toggles Dynamic Lights on and off") + @CommandPermission("dynamiclights.toggle") + public static void onToggle(CommandSender commandSender) { + if (commandSender instanceof Player player) { + if (PlayerUtil.getToggleStatus(player)) { + player.sendMessage(DynamicLights.translate("toggle-off")); + } else { + player.sendMessage(DynamicLights.translate("toggle-on")); + } + + PlayerUtil.switchToggleStatus(player); + } + } + + @Subcommand("lock") + @Description("Prevents lights from being placed in the offhand") + @CommandPermission("dynamiclights.lock") + public static void onLock(CommandSender commandSender) { + if (commandSender instanceof Player player) { + if (PlayerUtil.getLockStatus(player)) { + player.sendMessage(DynamicLights.translate("disable-lock")); + } else { + player.sendMessage(DynamicLights.translate("enable-lock")); + } + + PlayerUtil.switchLockStatus(player); + } + } + @Subcommand("lockverbose") + @Description("Make the warning lock message go away") + @CommandPermission("dynamiclights.lock") + public static void onLockVerbose(CommandSender commandSender) { + if (commandSender instanceof Player player) { + if (PlayerUtil.getLockStatus(player)) { + player.sendMessage(DynamicLights.translate("disable-lock-verbose")); + } else { + player.sendMessage(DynamicLights.translate("enable-lock-verbose")); + } + + PlayerUtil.switchLockVerboseStatus(player); + } + } +} diff --git a/src/main/java/github/xCykrix/dynamicLights/event/PlayerHandler.java b/src/main/java/github/xCykrix/dynamicLights/event/PlayerHandler.java index f7fbc8e..9a7000b 100644 --- a/src/main/java/github/xCykrix/dynamicLights/event/PlayerHandler.java +++ b/src/main/java/github/xCykrix/dynamicLights/event/PlayerHandler.java @@ -1,20 +1,19 @@ package github.xCykrix.dynamicLights.event; -import github.xCykrix.DevkitPlugin; import github.xCykrix.dynamicLights.DynamicLights; -import github.xCykrix.extendable.DevkitSimpleState; +import github.xCykrix.dynamicLights.util.PlayerUtil; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerGameModeChangeEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.plugin.java.JavaPlugin; -public class PlayerHandler extends DevkitSimpleState implements Listener { - public PlayerHandler(DevkitPlugin plugin) { - super(plugin); - } +public class PlayerHandler implements Listener { + public PlayerHandler(JavaPlugin plugin) {} @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void playerBlockPlaceEvent(BlockPlaceEvent event) { @@ -26,20 +25,30 @@ public void playerBlockPlaceEvent(BlockPlaceEvent event) { if (!DynamicLights.source.isProtectedLight(event.getItemInHand().getType())) { return; } - if (DynamicLights.manager.locks.getOrDefault(event.getPlayer().getUniqueId().toString(), DynamicLights.manager.toggle)) { - DynamicLights.adventure.get().player(event.getPlayer()).sendMessage(DynamicLights.language.getComponentFromID("prevent-block-place", true)); + if (PlayerUtil.getLockStatus(event.getPlayer())) { + // send a text to the player to explain why the block place event was cancelled + if (PlayerUtil.getLockVerboseStatus(event.getPlayer())) { + event.getPlayer().sendMessage(DynamicLights.translate("prevent-block-place")); + } + // cancel the event event.setCancelled(true); } } } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onPlayerJoinEvent(PlayerJoinEvent event) { - DynamicLights.manager.addPlayer(event.getPlayer()); - } - @EventHandler(priority = EventPriority.MONITOR) public void onPlayerQuitEvent(PlayerQuitEvent event) { - DynamicLights.manager.removePlayer(event.getPlayer().getUniqueId()); + DynamicLights.manager.removePlayerLightEnabled(event.getPlayer().getUniqueId()); + // DynamicLights.manager.removeLightFromLocationRegion(event.getPlayer().getUniqueId()); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerGameModeChangeEvent(PlayerGameModeChangeEvent event) { + DynamicLights.manager.updatePlayerState(event.getPlayer(), event.getNewGameMode()); + } + + @EventHandler + public void onPlayerJoinEvent(PlayerJoinEvent event) { + DynamicLights.manager.updatePlayerState(event.getPlayer(), event.getPlayer().getGameMode()); } } diff --git a/src/main/java/github/xCykrix/dynamicLights/util/LightManager.java b/src/main/java/github/xCykrix/dynamicLights/util/LightManager.java index f71dda7..8e10897 100644 --- a/src/main/java/github/xCykrix/dynamicLights/util/LightManager.java +++ b/src/main/java/github/xCykrix/dynamicLights/util/LightManager.java @@ -1,207 +1,221 @@ -package github.xCykrix.dynamicLights.util; - -import github.xCykrix.DevkitPlugin; -import github.xCykrix.dynamicLights.DynamicLights; -import github.xCykrix.extendable.DevkitFullState; -import dist.xCykrix.shade.dev.dejvokep.boostedyaml.YamlDocument; -import dist.xCykrix.shade.org.h2.mvstore.MVMap; -import java.util.HashMap; -import java.util.UUID; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.block.data.Waterlogged; -import org.bukkit.block.data.type.Light; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitTask; - -public class LightManager extends DevkitFullState { - private final LightSource source; - private final HashMap tasks = new HashMap<>(); - private final HashMap lastLightLocation = new HashMap<>(); - - public final MVMap toggles; - public final MVMap locks; - private final long refresh; - private final int distance; - public final boolean toggle; - - public LightManager(DevkitPlugin plugin) { - super(plugin); - YamlDocument config = DynamicLights.configuration.getYAMLFile("config.yml"); - if (config == null) { - throw new RuntimeException("config.yml is corrupted or contains invalid formatting. Failed to load plugin."); - } - - this.source = DynamicLights.source; - this.toggles = DynamicLights.h2.get().openMap("lightToggleStatus"); - this.locks = DynamicLights.h2.get().openMap("lightLockStatus"); - this.refresh = config.getLong("update-rate"); - this.distance = config.getInt("light-culling-distance"); - this.toggle = config.getBoolean("default-toggle-state"); - } - - @Override - public void initialize() { - } - - @Override - public void shutdown() { - synchronized (this.tasks) { - for (UUID uuid : this.tasks.keySet()) { - this.tasks.get(uuid).cancel(); - } - this.tasks.clear(); - } - } - - public void addPlayer(Player player) { - synchronized (this.tasks) { - if (this.tasks.containsKey(player.getUniqueId())) { - return; - } - this.tasks.put(player.getUniqueId(), - this.plugin.getServer().getScheduler().runTaskTimerAsynchronously(this.plugin, () -> { - Material mainHand = getMaterialOrAir(player.getInventory().getItemInMainHand()); - Material offHand = getMaterialOrAir(player.getInventory().getItemInOffHand()); - Material helmet = getMaterialOrAir(player.getInventory().getHelmet()); - Material chestplate = getMaterialOrAir(player.getInventory().getChestplate()); - Material legging = getMaterialOrAir(player.getInventory().getLeggings()); - Material boot = getMaterialOrAir(player.getInventory().getBoots()); - boolean valid = this.valid(player, mainHand, offHand, helmet, - chestplate, legging, boot); - int lightLevel = 0; - if (valid) { - lightLevel = source.getLightLevel(mainHand, offHand, helmet, - chestplate, legging, boot); - } - for (Player targetPlayer : Bukkit.getOnlinePlayers()) { - String locationId = player.getUniqueId() + "/" + targetPlayer.getUniqueId(); - Location lastLocation = this.getLastLocation(locationId); - if (!valid) { - if (lastLocation != null) { - this.removeLight(targetPlayer, lastLocation); - this.removeLastLocation(locationId); - } - continue; - } - Location nextLocation = player.getEyeLocation(); - if (this.toggles.getOrDefault(targetPlayer.getUniqueId().toString(), this.toggle)) { - if (lightLevel > 0 && differentLocations(lastLocation, nextLocation)) { - if (player.getWorld().getName().equals(targetPlayer.getWorld().getName())) { - if (player.getLocation().distance(targetPlayer.getLocation()) <= this.distance) { - this.addLight(targetPlayer, nextLocation, lightLevel); - this.setLastLocation(locationId, nextLocation); - } - } - } - } - if (lastLocation != null && differentLocations(lastLocation, nextLocation)) { - this.removeLight(targetPlayer, lastLocation); - } - } - }, 50L, refresh)); - } - } - - public void removePlayer(UUID uid) { - synchronized (this.tasks) { - if (this.tasks.containsKey(uid)) { - this.tasks.get(uid).cancel(); - this.tasks.remove(uid); - } - } - } - - public void addLight(Player player, Location location, int lightLevel) { - if (lightLevel == 0) { - return; - } - Light light = (Light) Material.LIGHT.createBlockData(); - if (location.getWorld() == null) { - location.setWorld(player.getWorld()); - } - World world = location.getWorld(); - switch (world.getBlockAt(location).getType()) { - case AIR, CAVE_AIR -> { - light.setWaterlogged(false); - light.setLevel(lightLevel); - } - case WATER -> { - light.setWaterlogged(true); - light.setLevel(lightLevel); - } - default -> { - } - } - player.sendBlockChange(location, light); - } - - public void removeLight(Player player, Location location) { - if (location.getWorld() == null) { - location.setWorld(player.getWorld()); - } - player.sendBlockChange(location, location.getWorld().getBlockAt(location).getBlockData()); - } - - public boolean valid(Player player, Material mainHand, Material offHand, Material helmet, Material chestplate, - Material legging, Material boot) { - boolean hasLightLevel = false; - hasLightLevel = source.hasLightLevel(mainHand) ? true : hasLightLevel; - hasLightLevel = source.hasLightLevel(offHand) ? true : hasLightLevel; - hasLightLevel = source.hasLightLevel(helmet) ? true : hasLightLevel; - hasLightLevel = source.hasLightLevel(chestplate) ? true : hasLightLevel; - hasLightLevel = source.hasLightLevel(legging) ? true : hasLightLevel; - hasLightLevel = source.hasLightLevel(boot) ? true : hasLightLevel; - - if (!hasLightLevel) { - return false; - } - Block currentLocation = player.getEyeLocation().getBlock(); - if (currentLocation.getType() == Material.AIR || currentLocation.getType() == Material.CAVE_AIR) { - return true; - } - if (currentLocation instanceof Waterlogged && ((Waterlogged) currentLocation).isWaterlogged()) { - return false; - } - if (currentLocation.getType() == Material.WATER) { - return source.isSubmersible(mainHand, offHand, helmet, chestplate, legging, boot); - } - return false; - } - - public Location getLastLocation(String uuid) { - return lastLightLocation.getOrDefault(uuid, null); - } - - public void setLastLocation(String uuid, Location location) { - lastLightLocation.put(uuid, location); - } - - public void removeLastLocation(String uuid) { - lastLightLocation.remove(uuid); - } - - private boolean differentLocations(Location l1, Location l2) { - if (l1 == null || l2 == null) { - return true; - } - if (l1.getWorld() == null || l2.getWorld() == null) { - return true; - } - if (!l1.getWorld().getName().equals(l2.getWorld().getName())) { - return true; - } - return l1.getBlockX() != l2.getBlockX() || l1.getBlockY() != l2.getBlockY() || l1.getBlockZ() != l2.getBlockZ(); - } - - private Material getMaterialOrAir(ItemStack item) { - if (item == null) - return Material.AIR; - else - return item.getType(); - } -} +package github.xCykrix.dynamicLights.util; + +import github.xCykrix.dynamicLights.DynamicLights; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.type.Light; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; + +public class LightManager { + private final LightSource source; + private final Map lastLightLocation = new ConcurrentHashMap<>(200); + + private final Set playerLightEnabled; + + private JavaPlugin plugin; + + public LightManager(JavaPlugin plugin) { + this.plugin = plugin; + this.source = DynamicLights.source; + + playerLightEnabled = ConcurrentHashMap.newKeySet(); + + Bukkit.getAsyncScheduler().runAtFixedRate(plugin, st -> tick(), 0L, plugin.getConfig().getLong("update-rate"), TimeUnit.MILLISECONDS); + } + + + // @Override + public void shutdown() { clearAllLights(); } + + public void addPlayerLightEnabled(UUID uuid) { playerLightEnabled.add(uuid); } + + public void removePlayerLightEnabled(UUID uuid) { playerLightEnabled.remove(uuid); } + + public boolean isPlayerLightEnabled(UUID uuid) { return playerLightEnabled.contains(uuid); } + + // Player emit light if (SURVIVAL or ADVENTURE) and they have it enabled. + public void updatePlayerState(Player player, GameMode mode) { + if ((mode == GameMode.SURVIVAL || mode == GameMode.ADVENTURE) && PlayerUtil.getToggleStatus(player)) { + addPlayerLightEnabled(player.getUniqueId()); + } else { + removePlayerLightEnabled(player.getUniqueId()); + // removeLightFromLocationRegion(player.getUniqueId()); + } + } + + public void tick() { + if (!plugin.isEnabled()) { + clearAllLights(); + return; + } + + // For each online player that have the right game mode, check if we should add a light or move the existing one + for (UUID targetPlayerUUID : playerLightEnabled) { + updatePlayerLight(Bukkit.getPlayer(targetPlayerUUID)); + } + + // Remove each light that should not be there + // With a short delay between each tick(), the light is added back when removed from somewhere else, + // so we need to remove it after the main for loop. + // It might call to remove the light many time if the region scheduler is not fast enough, but it's fine I guess. + for (UUID targetPlayerUUID : lastLightLocation.keySet()) { + if (!playerLightEnabled.contains(targetPlayerUUID)) { + this.removeLightFromLocationRegion(targetPlayerUUID); + } + } + } + + public void clearAllLights() { + for (Map.Entry entry : lastLightLocation.entrySet()) { + this.removeLightFromLocationRegion(entry.getKey()); + } + } + + public void updatePlayerLight(Player player) { + Location playerLocation = player.getEyeLocation(); + Bukkit.getRegionScheduler().run(plugin, playerLocation, st -> updateLightToNewLocation(player, playerLocation)); + } + + /** + * Find the best location to place a light and update the light if needed. + */ + private Optional updateLightToNewLocation(Player player, Location eyeLocation) { + Material mainHand = getMaterialOrAir(player.getInventory().getItemInMainHand()); + Material offHand = getMaterialOrAir(player.getInventory().getItemInOffHand()); + Material helmet = getMaterialOrAir(player.getInventory().getHelmet()); + Material chestplate = getMaterialOrAir(player.getInventory().getChestplate()); + Material legging = getMaterialOrAir(player.getInventory().getLeggings()); + Material boot = getMaterialOrAir(player.getInventory().getBoots()); + int lightLevel = source.getLightLevel(mainHand, offHand, helmet, chestplate, legging, boot); + if (lightLevel > 0) { + // plugin.getLogger().info("lightLevel > 0 " + lightLevel); + Block bestBlock = getClosestAcceptableBlock(eyeLocation.getBlock()); + if (bestBlock == null) { + this.removeLightFromLocationRegion(player.getUniqueId()); + return Optional.empty(); + } + Location bestBlockLocation = bestBlock.getLocation(); + if (bestBlockLocation == null) { + this.removeLightFromLocationRegion(player.getUniqueId()); + return Optional.empty(); + } + + // Update the light in Minecraft & int lastLightLocation + Location last = lastLightLocation.getOrDefault(player.getUniqueId(), null); + if (last != null) { + if (last.equals(bestBlockLocation) && bestBlock.getType() == Material.LIGHT + && bestBlock.getBlockData() instanceof Light existingLight && existingLight.getLevel() == lightLevel) { + // The light is already at the right location with the right level, no need to remove or to add + return Optional.empty(); + } + this.removeLightFromLocationRegion(player.getUniqueId()); + } + + if (!player.isOnline()) { // player might have log off in the meantime + return Optional.empty(); + } + lastLightLocation.put(player.getUniqueId(), bestBlockLocation); // replace ligh location + this.addLight(bestBlock, lightLevel, source.isSubmersible(mainHand, offHand, helmet, chestplate, legging, boot)); + + return Optional.of(bestBlockLocation); + } + // plugin.getLogger().info("lightLevel <= 0"); + this.removeLightFromLocationRegion(player.getUniqueId()); + return Optional.empty(); + } + + private boolean acceptableBlock(Block block) { + Material type = block.getType(); + return type == Material.AIR || type == Material.CAVE_AIR || type == Material.LIGHT || type == Material.WATER; + } + + private Block getClosestAcceptableBlock(Block block) { + List possibleLocation = List.of(block, block.getRelative(BlockFace.NORTH), block.getRelative(BlockFace.EAST), + block.getRelative(BlockFace.SOUTH), block.getRelative(BlockFace.WEST), block.getRelative(BlockFace.UP), + block.getRelative(BlockFace.DOWN)); + + for (Block relativeBlock : possibleLocation) { + if (acceptableBlock(relativeBlock)) { + return relativeBlock; + } + } + return null; + } + + public void addLight(Block block, int lightLevel, boolean isSubmersible) { + Light light = (Light) Material.LIGHT.createBlockData(); + switch (block.getType()) { + case AIR, CAVE_AIR -> { + light.setWaterlogged(false); + light.setLevel(lightLevel); + } + case WATER -> { + if (isSubmersible) { + light.setWaterlogged(true); + light.setLevel(lightLevel); + } else { + return; + } + } + default -> { + // do nothing + } + } + + block.getWorld().setBlockData(block.getLocation(), light); + + // DynamicLights.getInstance().getLogger().info("Added light at " + block.getLocation()); + } + + private void removeLight(UUID playerUuid, Location location) { + // Location location = lastLightLocation.get(playerUuid); + if (location != null) { + World world = location.getWorld(); + if (world != null) { + Block b = world.getBlockAt(location); + if (b.getBlockData() instanceof Light light) { + if (light.isWaterlogged()) { + b.setType(Material.WATER); + } else { + b.setType(Material.AIR); + } + } + } + } + // Remove only if its the current last location, else we want to keep that value to remove it later when we will have to remove the + // light. + lastLightLocation.remove(playerUuid, location); + // DynamicLights.getInstance().getLogger().info("Removed light at " + location); + } + + public void removeLightFromLocationRegion(UUID playerUuid) { + if (playerUuid == null || !lastLightLocation.containsKey(playerUuid)) { + return; + } + + Location location = lastLightLocation.get(playerUuid); + // Since it's planned for later, we need to send the current last location before it will be override when placing the new light. + Bukkit.getRegionScheduler().run(plugin, location, st -> this.removeLight(playerUuid, location)); + } + + private Material getMaterialOrAir(ItemStack item) { + if (item == null) + return Material.AIR; + else + return item.getType(); + } +} diff --git a/src/main/java/github/xCykrix/dynamicLights/util/LightSource.java b/src/main/java/github/xCykrix/dynamicLights/util/LightSource.java index fe444f6..2faef80 100644 --- a/src/main/java/github/xCykrix/dynamicLights/util/LightSource.java +++ b/src/main/java/github/xCykrix/dynamicLights/util/LightSource.java @@ -1,41 +1,46 @@ package github.xCykrix.dynamicLights.util; -import github.xCykrix.DevkitPlugin; -import github.xCykrix.dynamicLights.DynamicLights; -import github.xCykrix.extendable.DevkitFullState; -import dist.xCykrix.shade.dev.dejvokep.boostedyaml.YamlDocument; -import dist.xCykrix.shade.dev.dejvokep.boostedyaml.block.Block; +import java.io.File; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import org.bukkit.Material; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; -public class LightSource extends DevkitFullState { +public class LightSource { private final HashMap levelOfLights = new HashMap<>(); private final HashSet submersibleLights = new HashSet<>(); private final HashSet lockedLights = new HashSet<>(); - public LightSource(DevkitPlugin plugin) { - super(plugin); - } + private final JavaPlugin plugin; + + public LightSource(JavaPlugin plugin) { this.plugin = plugin; } - @Override + // @Override public void initialize() { - YamlDocument lights = DynamicLights.configuration.getYAMLFile("lights.yml"); + // YamlDocument lights = DynamicLights.configuration.getYAMLFile("lights.yml"); + YamlConfiguration lights = YamlConfiguration.loadConfiguration(new File(this.plugin.getDataFolder(), "lights.yml")); + + if (lights == null) { throw new RuntimeException("lights.yml is corrupted or contains invalid formatting. Failed to load plugin."); } // Register Light Levels this.levelOfLights.clear(); - Map> levels = lights.getSection("levels").getStoredValue(); - for (Object material : levels.keySet()) { + // Map> levels = lights.getSection("levels").getStoredValue(); + Map levels = lights.getConfigurationSection("levels").getValues(false); + for (Map.Entry entry : levels.entrySet()) { + String materialString = entry.getKey(); + Object levelObject = entry.getValue(); try { - int level = Integer.parseInt(levels.get(material).getStoredValue().toString()); - this.levelOfLights.put(Material.valueOf((String) material), level); + int level = Integer.parseInt(levelObject.toString()); + Material material = Material.valueOf(materialString); + this.levelOfLights.put(material, level); } catch (Exception exception) { - this.plugin.getLogger().warning("Unable to register level for '" + material + "'. " + exception.getMessage()); + this.plugin.getLogger().warning("Unable to register level for '" + materialString + "'. " + exception.getMessage()); } } this.plugin.getLogger().info("Registered " + this.levelOfLights.size() + " items for Dynamic Lights."); @@ -47,12 +52,10 @@ public void initialize() { try { this.submersibleLights.add(Material.valueOf(material)); } catch (Exception exception) { - this.plugin.getLogger() - .warning("Unable to register submersible for '" + material + "'. " + exception.getMessage()); + this.plugin.getLogger().warning("Unable to register submersible for '" + material + "'. " + exception.getMessage()); } } - this.plugin.getLogger() - .info("Registered " + this.submersibleLights.size() + " items for Dynamic Submersible Lights."); + this.plugin.getLogger().info("Registered " + this.submersibleLights.size() + " items for Dynamic Submersible Lights."); // Register Lockable Lights this.lockedLights.clear(); @@ -61,26 +64,22 @@ public void initialize() { try { this.lockedLights.add(Material.valueOf(material)); } catch (Exception exception) { - this.plugin.getLogger() - .warning("Unable to register lockable for '" + material + "'. " + exception.getMessage()); + this.plugin.getLogger().warning("Unable to register lockable for '" + material + "'. " + exception.getMessage()); } } this.plugin.getLogger().info("Registered " + this.lockedLights.size() + " items for Dynamic Locked Lights."); } - @Override + // @Override public void shutdown() { this.lockedLights.clear(); this.submersibleLights.clear(); this.levelOfLights.clear(); } - public boolean hasLightLevel(Material material) { - return levelOfLights.containsKey(material); - } + public boolean hasLightLevel(Material material) { return levelOfLights.containsKey(material); } - public Integer getLightLevel(Material mainHand, Material offHand, Material helmet, Material chestplate, - Material legging, Material boot) { + public Integer getLightLevel(Material mainHand, Material offHand, Material helmet, Material chestplate, Material legging, Material boot) { int level = 0; level = levelOfLights.getOrDefault(boot, level); level = levelOfLights.getOrDefault(legging, level); @@ -91,8 +90,7 @@ public Integer getLightLevel(Material mainHand, Material offHand, Material helme return level; } - public boolean isSubmersible(Material mainHand, Material offHand, Material helmet, Material chestplate, - Material legging, Material boot) { + public boolean isSubmersible(Material mainHand, Material offHand, Material helmet, Material chestplate, Material legging, Material boot) { boolean submersible = false; submersible = submersibleLights.contains(boot) ? true : submersible; submersible = submersibleLights.contains(legging) ? true : submersible; @@ -103,7 +101,5 @@ public boolean isSubmersible(Material mainHand, Material offHand, Material helme return submersible; } - public boolean isProtectedLight(Material offHand) { - return lockedLights.contains(offHand); - } + public boolean isProtectedLight(Material offHand) { return lockedLights.contains(offHand); } } diff --git a/src/main/java/github/xCykrix/dynamicLights/util/PlayerUtil.java b/src/main/java/github/xCykrix/dynamicLights/util/PlayerUtil.java new file mode 100644 index 0000000..e6c756c --- /dev/null +++ b/src/main/java/github/xCykrix/dynamicLights/util/PlayerUtil.java @@ -0,0 +1,37 @@ +package github.xCykrix.dynamicLights.util; + +import github.xCykrix.dynamicLights.DynamicLights; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataType; + +public class PlayerUtil { + private static NamespacedKey lightToggleStatusKey = new NamespacedKey("dynamiclights", "toggle-state"); + private static NamespacedKey lightLockStatusKey = new NamespacedKey("dynamiclights", "lock-state"); + private static NamespacedKey lightLockVerboseStatusKey = new NamespacedKey("dynamiclights", "lock-verbose-state"); + public static boolean getToggleStatus(Player player) { + return player.getPersistentDataContainer().getOrDefault(lightToggleStatusKey, PersistentDataType.BOOLEAN, + DynamicLights.getInstance().getConfig().getBoolean("default-toggle-state", true)); + } + public static void switchToggleStatus(Player player) { + player.getPersistentDataContainer().set(lightToggleStatusKey, PersistentDataType.BOOLEAN, !getToggleStatus(player)); + } + + public static boolean getLockStatus(Player player) { + return player.getPersistentDataContainer().getOrDefault(lightLockStatusKey, PersistentDataType.BOOLEAN, + DynamicLights.getInstance().getConfig().getBoolean("default-lock-state", false)); + } + + public static void switchLockStatus(Player player) { + player.getPersistentDataContainer().set(lightLockStatusKey, PersistentDataType.BOOLEAN, !getLockStatus(player)); + } + + public static boolean getLockVerboseStatus(Player player) { + return player.getPersistentDataContainer().getOrDefault(lightLockVerboseStatusKey, PersistentDataType.BOOLEAN, + DynamicLights.getInstance().getConfig().getBoolean("default-lock-verbose-state", true)); + } + + public static void switchLockVerboseStatus(Player player) { + player.getPersistentDataContainer().set(lightLockVerboseStatusKey, PersistentDataType.BOOLEAN, !getLockVerboseStatus(player)); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 642c748..fe254c7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,15 +1,15 @@ # Update Rate: Controls how often the DynamicLights will tick. Defaults to 5 (4 times per second). Player heavy servers may require less often. update-rate: 5 -# Light Culling Distance: Controls how far away you should see another players DynamicLight. -light-culling-distance: 64 - # Default Toggle State: Controls if Dynamic Lighting is enabled by default. default-toggle-state: true # Default Locking State: Controls if player's offhand are automatically locked by default. -default-lock-state: true +default-lock-state: false + +# Default warn player when locked that they can unlock it with a command. +default-lock-verbose-state: true # DO NOT EDIT THE VERSION. Manual edits may corrupt or reset configuration files. # -version: 3 \ No newline at end of file +version: 3 diff --git a/src/main/resources/language.yml b/src/main/resources/language.yml deleted file mode 100644 index 07b35b4..0000000 --- a/src/main/resources/language.yml +++ /dev/null @@ -1,29 +0,0 @@ -### -# Language File - Adventure API MiniMessage -# -# I recommend backing up this file in the event an update causes translations to be reset. -# -# General: https://docs.advntr.dev/minimessage/index.html -# Formatting: https://docs.advntr.dev/minimessage/format.html#minimessage-format -# -# Realtime Editor: https://webui.advntr.dev/ -### - -### -# Used as the Plugin's Chat and Console Prefix. Available in all "language." components as "". -### -chat-prefix: "[DynamicLights]" - -# Language Index -language: - prevent-block-place: " Light locking mode is currently enabled. You must sneak to place light sources from your Off Hand. This can be toggled with \"/dl lock\"." - enable-lock: " Enabled light lock mode." - disable-lock: " Disabled light lock mode." - toggle-on: " Light rendering enabled for client." - toggle-off: " Light rendering disabled for client." - reload: " Reloaded lights.yml configuration." - reload-error: " Failed to reload lights.yml configuration. Check Console.." - -# DO NOT EDIT THE VERSION. Manual edits may corrupt or reset configuration files. -# -version: 2 \ No newline at end of file diff --git a/src/main/resources/lights.yml b/src/main/resources/lights.yml index 93eb178..f25c9b7 100644 --- a/src/main/resources/lights.yml +++ b/src/main/resources/lights.yml @@ -6,7 +6,7 @@ levels: LANTERN: 13 SOUL_LANTERN: 13 - TORCH: 11 + TORCH: 15 SOUL_TORCH: 11 REDSTONE_TORCH: 7 GLOWSTONE: 15 @@ -15,23 +15,25 @@ levels: OCHRE_FROGLIGHT: 15 PEARLESCENT_FROGLIGHT: 15 VERDANT_FROGLIGHT: 15 + JACK_O_LANTERN: 15 + COPPER_TORCH: 13 # String List of Submersible Materials submersibles: - - "GLOWSTONE" - - "SHROOMLIGHT" - - "SEA_LANTERN" - - "OCHRE_FROGLIGHT" - - "PEARLESCENT_FROGLIGHT" - - "VERDANT_FROGLIGHT" +- GLOWSTONE +- SHROOMLIGHT +- SEA_LANTERN +- OCHRE_FROGLIGHT +- PEARLESCENT_FROGLIGHT +- VERDANT_FROGLIGHT # String List of Lockable Materials lockables: - - "TORCH" - - "SOUL_TORCH" - - "LANTERN" - - "SOUL_LANTERN" +- TORCH +- SOUL_TORCH +- LANTERN +- SOUL_LANTERN # DO NOT EDIT THE VERSION. Manual edits may corrupt or reset configuration files. # -version: 1 \ No newline at end of file +version: 1 diff --git a/src/main/resources/locale/en.yml b/src/main/resources/locale/en.yml new file mode 100644 index 0000000..954ecaa --- /dev/null +++ b/src/main/resources/locale/en.yml @@ -0,0 +1,25 @@ +### +# Language File - Adventure API MiniMessage +# +# I recommend backing up this file in the event an update causes translations to be reset. +# +# General: https://docs.advntr.dev/minimessage/index.html +# Formatting: https://docs.advntr.dev/minimessage/format.html#minimessage-format +# +# Realtime Editor: https://webui.advntr.dev/ +### + +### +# Used as the Plugin's Chat and Console Prefix. Available in all "language." components as "". +### +prefix: "[DynamicLights]" + +prevent-block-place: " Light locking mode is currently enabled. You must sneak to place light sources from your Off Hand. This can be toggled with \"/dl lock\". (Disable this message with \"/dl lockverbose\".)" +enable-lock: " Enabled light lock mode." +disable-lock: " Disabled light lock mode." +enable-lock-verbose: " Enabled light lock mode warning message." +disable-lock-verbose: " Disabled light lock mode warning message." +toggle-on: " Light rendering enabled." +toggle-off: " Light rendering disabled." +reload: " Reloaded lights.yml configuration." +reload-error: " Failed to reload lights.yml configuration. Check Console.." diff --git a/src/main/resources/locale/fr.yml b/src/main/resources/locale/fr.yml new file mode 100644 index 0000000..76d0482 --- /dev/null +++ b/src/main/resources/locale/fr.yml @@ -0,0 +1,25 @@ +### +# Language File - Adventure API MiniMessage +# +# I recommend backing up this file in the event an update causes translations to be reset. +# +# General: https://docs.advntr.dev/minimessage/index.html +# Formatting: https://docs.advntr.dev/minimessage/format.html#minimessage-format +# +# Realtime Editor: https://webui.advntr.dev/ +### + +### +# Used as the Plugin's Chat and Console Prefix. Available in all "language." components as "". +### +prefix: "[DynamicLights]" + +prevent-block-place: " Le mode de verrouillage des sources lumineuses est actuellement activé. Vous devez vous accroupir pour placer des sources lumineuses depuis votre main secondaire. Ce mode peut être désactivé avec « /dl lock ». (Désactivez ce message avec « /dl lockverbose ».)" +enable-lock: " Mode de verrouillage de la lumière activé." +disable-lock: " Mode de verrouillage de la lumière désactivé." +enable-lock-verbose: " Message d'avertissement du mode de verrouillage de la lumière activé." +disable-lock-verbose: " Message d'avertissement du mode de verrouillage de la lumière désactivé." +toggle-on: " Rendu de la lumière activé." +toggle-off: " Rendu de la lumière désactivé." +reload: " Configuration lights.yml rechargée." +reload-error: " Échec du rechargement de la configuration lights.yml. Consultez la console.." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 1543709..6bd899f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,12 +1,13 @@ -name: 'DynamicLights' -description: 'Emit Light from Held Items using the 1.17 Light Blocks.' -website: 'https://www.spigotmc.org/resources/dynamiclights.110707/' -author: 'xCykrix' -version: '1.3.0-MULTI-VERSION' +name: '$name' +description: 'Emit Light from Held Items using the 1.20 Light Blocks.' +website: 'https://github.com/Mvndi/DynamicLights' +authors: [ 'xCykrix', 'Hydrolien' ] +version: $version main: github.xCykrix.dynamicLights.DynamicLights -api-version: '1.17' +api-version: "$apiVersion" load: 'POSTWORLD' +folia-supported: true permissions: dynamiclights.lock: