diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index af0b2c96..294aa4b9 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -46,6 +46,6 @@ jobs: # Artifact name name: ClaimChunk # Upload plugin jar - path: OUT/*.jar + path: OUT/*-plugin.jar # Fail if the output file isn't found if-no-files-found: error diff --git a/.gitignore b/.gitignore index e8f169ba..8656ef44 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ OUT # Gource my_captions.txt source_visual.ppm -source_visual.mp4 \ No newline at end of file +source_visual.mp4 + +# Leftover from tests +*.tmp.sqlite3 diff --git a/README.md b/README.md index c6aa9988..84b05f47 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ implementation("com.cjburkey.claimchunk:claimchunk:0.0.24-RC1") Building -------- [![Automatic Build](https://img.shields.io/github/actions/workflow/status/cjburkey01/ClaimChunk/gradle.yml?branch=main&style=for-the-badge)](https://claimchunk.cjburkey.com/server/Downloads.html#snapshot-downloads) -[![Version Info](https://img.shields.io/static/v1?label=Repository%20Version&message=0.0.24-RC2&color=ff5555&style=for-the-badge)](https://github.com/cjburkey01/ClaimChunk/archive/main.zip) +[![Version Info](https://img.shields.io/static/v1?label=Repository%20Version&message=0.0.25&color=ff5555&style=for-the-badge)](https://github.com/cjburkey01/ClaimChunk/archive/main.zip) If you want to obtain a version of the plugin that isn't available yet (like a snapshot), you can do so by asking on the Discord or building it yourself. Here's how to build it yourself: diff --git a/build.gradle.kts b/build.gradle.kts index 8105720b..276bc604 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,57 +1,56 @@ -// I NEED THESE PLEASE -@file:Suppress("RedundantSemicolon") - +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.vanniktech.maven.publish.SonatypeHost -import org.apache.tools.ant.filters.ReplaceTokens; -import de.undercouch.gradle.tasks.download.Download; +import org.apache.tools.ant.filters.ReplaceTokens +import de.undercouch.gradle.tasks.download.Download plugins { - java; + java - id("de.undercouch.download") version "5.6.0"; - id("io.freefair.lombok") version "8.6"; + id("de.undercouch.download") version "5.6.0" + id("io.freefair.lombok") version "8.6" // Including dependencies in final jar - //id("com.github.johnrengelman.shadow") version "8.1.1"; - id("com.vanniktech.maven.publish") version "0.28.0"; + id("com.github.johnrengelman.shadow") version "8.1.1" + id("com.vanniktech.maven.publish") version "0.28.0" } object DepData { - const val JAVA_VERSION = 17; + const val JAVA_VERSION = 17 - const val LIVE_VERSION = "0.0.24-RC1"; - const val THIS_VERSION = "0.0.24-RC2"; - const val PLUGIN_NAME = "ClaimChunk"; - const val ARCHIVES_BASE_NAME = "claimchunk"; - const val MAIN_CLASS = "com.cjburkey.claimchunk.ClaimChunk"; + const val LIVE_VERSION = "0.0.24-RC1" + const val THIS_VERSION = "0.0.25" + const val PLUGIN_NAME = "ClaimChunk" + const val ARCHIVES_BASE_NAME = "claimchunk" + const val MAIN_CLASS = "com.cjburkey.claimchunk.ClaimChunk" // Only used if you run `gradlew installSpigot` const val SPIGOT_BUILD_TOOLS_URL - = "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar"; - const val SPIGOT_REV = "1.20.4"; + = "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar" + const val SPIGOT_REV = "1.20.4" // Dependency versions - const val BUKKIT_VERSION = "1.20.4-R0.1-SNAPSHOT"; - const val SPIGOT_VERSION = "1.20.4-R0.1-SNAPSHOT"; - const val LATEST_MC_VERSION = "1.20.6"; - const val VAULT_API_VERSION = "1.7"; - const val WORLD_EDIT_CORE_VERSION = "7.2.9"; - const val WORLD_GUARD_BUKKIT_VERSION = "7.0.7"; - const val PLACEHOLDER_API_VERSION = "2.11.1"; - const val JETBRAINS_ANNOTATIONS_VERSION = "23.0.0"; - const val JUNIT_VERSION = "5.10.2"; - const val JUNIT_LAUNCHER_VERSION = "1.10.2"; - // Goldmensch's SmartCommandDispatcher. Thank you!! - // const val SMART_COMMAND_DISPATCHER_VERSION = "2.0.1"; - // And internationalization library! - // const val JALL_I18N_VERSION = "1.0.2" + const val BUKKIT_VERSION = "1.20.4-R0.1-SNAPSHOT" + const val SPIGOT_VERSION = "1.20.4-R0.1-SNAPSHOT" + const val LATEST_MC_VERSION = "1.20.6" + const val VAULT_API_VERSION = "1.7" + const val WORLD_EDIT_CORE_VERSION = "7.2.9" + const val WORLD_GUARD_BUKKIT_VERSION = "7.0.7" + const val PLACEHOLDER_API_VERSION = "2.11.1" + const val JETBRAINS_ANNOTATIONS_VERSION = "23.0.0" + const val JUNIT_VERSION = "5.10.2" + const val JUNIT_LAUNCHER_VERSION = "1.10.2" + const val SQLITE_JDBC_VERSION = "3.42.0.1" + const val JAVAX_PERSISTENCE_VERSION = "2.1.0" + const val JAVAX_TRANSACTION_VERSION = "1.1" + const val SANS_ORM_VERSION = "3.17" + const val SLF4J_VERSION = "1.7.25" // Directories - const val TEST_SERVER_DIR = "run"; - const val OUTPUT_DIR = "OUT"; + const val TEST_SERVER_DIR = "run" + const val OUTPUT_DIR = "OUT" // Readme locations - const val README_IN = "unbuilt_readme.md"; - const val README_OUT = "README.md"; + const val README_IN = "unbuilt_readme.md" + const val README_OUT = "README.md" } // Tokens to replace within files @@ -65,74 +64,77 @@ val replaceTokens = mapOf( "SPIGOT_VERSION" to DepData.SPIGOT_VERSION.substring(0, DepData.SPIGOT_VERSION.indexOf("-")), "LATEST_MC_VERSION" to DepData.LATEST_MC_VERSION ) -); +) // Plugin information -group = "com.cjburkey"; -version = DepData.THIS_VERSION; +group = "com.cjburkey.claimchunk" +version = DepData.THIS_VERSION -val mainDir = layout.projectDirectory; +val mainDir = layout.projectDirectory java { toolchain { - languageVersion = JavaLanguageVersion.of(DepData.JAVA_VERSION); + languageVersion = JavaLanguageVersion.of(DepData.JAVA_VERSION) } } tasks { compileJava { + mustRunAfter("googleFormat") + // Disable incremental compilation (module system bs and spigot no mesh // well) - options.isIncremental = false; + options.isIncremental = false // Enable all compiler warnings for cleaner (hopefully) code - options.isWarnings = true; - options.isDeprecation = true; - options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Xmaxwarns", "200")); - options.encoding = "UTF-8"; + options.isWarnings = true + options.isDeprecation = true + options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Xmaxwarns", "200")) + options.encoding = "UTF-8" } // We don't actually include any other libraries now // (except smartcommanddispatcher, but we do that manually) - /*shadowJar { + shadowJar { + mustRunAfter("updateReadme") + // Set the jar name and version - archiveBaseName.set(DepData.ARCHIVES_BASE_NAME); - archiveClassifier.set("plugin"); - archiveVersion.set(project.version.toString()); + archiveBaseName.set(DepData.ARCHIVES_BASE_NAME) + archiveClassifier.set("plugin") + archiveVersion.set(project.version.toString()) dependencies { - // Exclude annotations from output jar - exclude(dependency("org.jetbrains:annotations:.*")); + exclude(dependency("org.slf4j:slf4j-api")) + exclude(dependency("org.xerial:sqlite-jdbc")) + exclude(dependency("org.jetbrains:annotations")) } - // Move SmartCommandDispatcher to a unique package to avoid clashes with - // any other plugins that might include it in their jar files. - // relocate("de.goldmensch.commanddispatcher", - // "claimchunk.dependency.de.goldmensch.commanddispatcher"); - // Do the same with JALL - // relocate("io.github.goldmensch.jall", - // "claimchunk.dependency.io.github.goldmensch.jall"); - }*/ + relocate("com.zaxxer", "claimchunk.dependency.com.zaxxer") + relocate("javax.persistence", "claimchunk.dependency.javax.persistence") + relocate("javax.transaction", "claimchunk.dependency.javax.transaction") + relocate("org.eclipse", "claimchunk.dependency.org.eclipse") + relocate("org.osgi", "claimchunk.dependency.org.osgi") + } test { - useJUnitPlatform(); + useJUnitPlatform() systemProperties = mapOf( "junit.jupiter.conditions.deactivate" to "*", "junit.jupiter.extensions.autodetection.enabled" to "true", "junit.jupiter.testinstance.lifecycle.default" to "per_class" - ); + ) } clean { // Delete old build(s) - project.delete(files(DepData.OUTPUT_DIR)); + project.delete(files(DepData.OUTPUT_DIR)) // Delete old build(s) from test server plugin dir project.delete( fileTree(mainDir.dir("${DepData.TEST_SERVER_DIR}/plugins")) .include("claimchunk**.jar") - .include("ClaimChunk**.jar")); + .include("ClaimChunk**.jar")) } build { @@ -142,124 +144,124 @@ tasks { // (Also rebuild the README file) finalizedBy("updateReadme", "copyClaimChunkToOutputDir" - ); + ) } // Replace placeholders with values in source and resource files processResources { - filter(replaceTokens); + filter(replaceTokens) } // Fill in readme placeholders register("updateReadme") { - mustRunAfter("build"); - description = "Expands tokens in the unbuilt readme file into the main readme file"; + mustRunAfter("build") + description = "Expands tokens in the unbuilt readme file into the main readme file" - val inf = mainDir.file(DepData.README_IN); - val ouf = mainDir.file(DepData.README_OUT); + val inf = mainDir.file(DepData.README_IN) + val ouf = mainDir.file(DepData.README_OUT) doFirst { closureOf { inputs.file(ouf) - delete(ouf); + delete(ouf) } } // Set the inputs and outputs for the operation - inputs.file(inf); - outputs.file(ouf); + inputs.file(inf) + outputs.file(ouf) // Copy the new readme, rename it, and expand tokens - from(inf); - into(mainDir); - filter(replaceTokens); + from(inf) + into(mainDir) + filter(replaceTokens) rename(DepData.README_IN, DepData.README_OUT) } // Clear out old Spigot versions from test server directory register("deleteOldSpigotInstalls") { - description = "Deletes (any) old Spigot server jars from the test server directory"; + description = "Deletes (any) old Spigot server jars from the test server directory" - delete(fileTree(mainDir.dir(DepData.TEST_SERVER_DIR)).include("spigot-*.jar")); + delete(fileTree(mainDir.dir(DepData.TEST_SERVER_DIR)).include("spigot-*.jar")) } register("downloadSpigotBuildTools") { - description = "Downloads the latest version of the Spigot BuildTools into TEST_SERVER_DIR/TEMP"; + description = "Downloads the latest version of the Spigot BuildTools into TEST_SERVER_DIR/TEMP" - src(DepData.SPIGOT_BUILD_TOOLS_URL); - dest(mainDir.dir(DepData.TEST_SERVER_DIR).dir("TEMP").file("BuildTools.jar")); - overwrite(true); + src(DepData.SPIGOT_BUILD_TOOLS_URL) + dest(mainDir.dir(DepData.TEST_SERVER_DIR).dir("TEMP").file("BuildTools.jar")) + overwrite(true) } // Download and run Spigot BuildTools to generate a Spigot server jar in the spigot `testServerDir` register("installSpigot") { - description = "Downloads and executes the Spigot build tools to generate a server jar in the test server directory."; + description = "Downloads and executes the Spigot build tools to generate a server jar in the test server directory." // Delete old Spigot jar(s) and download BuildTools first - dependsOn("deleteOldSpigotInstalls", "downloadSpigotBuildTools"); + dependsOn("deleteOldSpigotInstalls", "downloadSpigotBuildTools") - val testServerDir = mainDir.dir(DepData.TEST_SERVER_DIR); - val tmpDir = testServerDir.dir("TEMP"); - val tmpServerJar = tmpDir.file("spigot-${DepData.SPIGOT_REV}.jar"); + val testServerDir = mainDir.dir(DepData.TEST_SERVER_DIR) + val tmpDir = testServerDir.dir("TEMP") + val tmpServerJar = tmpDir.file("spigot-${DepData.SPIGOT_REV}.jar") // Run the build tools jar (the manifest main class) - mainClass.set("-jar"); - workingDir(tmpDir); - args("BuildTools.jar", "--nogui", "--rev", DepData.SPIGOT_REV); + mainClass.set("-jar") + workingDir(tmpDir) + args("BuildTools.jar", "--nogui", "--rev", DepData.SPIGOT_REV) doLast { - println("Cleaning up Spigot build"); - tmpServerJar.asFile.copyTo(testServerDir.file("spigot-${DepData.SPIGOT_REV}.jar").asFile, true); - tmpDir.asFile.deleteRecursively(); + println("Cleaning up Spigot build") + tmpServerJar.asFile.copyTo(testServerDir.file("spigot-${DepData.SPIGOT_REV}.jar").asFile, true) + tmpDir.asFile.deleteRecursively() } } // Copy from the libs dir to the plugins directory in the testServerDir register("copyClaimChunkToPluginsDir") { - mustRunAfter("copyClaimChunkToOutputDir"); - description = "Copies ClaimChunk from the build directory to the test server plugin directory."; + mustRunAfter("copyClaimChunkToOutputDir") + description = "Copies ClaimChunk from the build directory to the test server plugin directory." - from(jar); - into(mainDir.dir("${DepData.TEST_SERVER_DIR}/plugins")); + from(shadowJar) + into(mainDir.dir("${DepData.TEST_SERVER_DIR}/plugins")) } register("copyClaimChunkToOutputDir") { - mustRunAfter("updateReadme"); - description = "Copies ClaimChunk from the build directory to the output directory."; + mustRunAfter("updateReadme") + description = "Copies ClaimChunk from the build directory to the output directory." - from(jar); - into(mainDir.dir(DepData.OUTPUT_DIR)); + from(shadowJar) + into(mainDir.dir(DepData.OUTPUT_DIR)) } register("googleFormat") { - description = "Attempts to format source files for ClaimChunk to unify programming style."; + description = "Attempts to format source files for ClaimChunk to unify programming style." // For now, this file is just included with the project for the sake of // ease of use. - val execJarFile = mainDir.file("req/google-java-format-1.22.0-all-deps.jar"); + val execJarFile = mainDir.file("req/google-java-format-1.22.0-all-deps.jar") // Include all source Java files // (I don't think there's a case where I would want to avoid formatting // a file, but be it necessary, this is where it would be implemented) val includedFiles = fileTree("src") { include("**/*.java") - }.files; - inputs.files(includedFiles); - outputs.files(includedFiles); + }.files + inputs.files(includedFiles) + outputs.files(includedFiles) // Run the build tools jar (the manifest main class) // The JVM args are required because of Java's Project Jigsaw - mainClass.set("-jar"); - workingDir(mainDir); - jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"); - jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED"); - jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED"); - jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"); - jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"); - args(execJarFile); - args("--replace"); - args("--aosp"); - args(includedFiles); + mainClass.set("-jar") + workingDir(mainDir) + jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED") + jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED") + jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED") + jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED") + jvmArgs("--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED") + args(execJarFile) + args("--replace") + args("--aosp") + args(includedFiles) } } @@ -269,33 +271,40 @@ tasks { // Extra repos for Bukkit/Spigot stuff repositories { - mavenCentral(); - maven("https://oss.sonatype.org/content/repositories/snapshots/"); - maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/"); - maven("https://maven.enginehub.org/repo/"); - maven("https://repo.mikeprimm.com"); - maven("https://papermc.io/repo/repository/maven-public/"); - maven("https://repo.extendedclip.com/content/repositories/placeholderapi/"); - maven("https://eldonexus.de/repository/maven-public"); + mavenCentral() + maven("https://oss.sonatype.org/content/repositories/snapshots/") + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://maven.enginehub.org/repo/") + maven("https://repo.mikeprimm.com") + maven("https://papermc.io/repo/repository/maven-public/") + maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") + maven("https://eldonexus.de/repository/maven-public") // Why do you have to be special, huh? maven { - url = uri("http://nexus.hc.to/content/repositories/pub_releases/"); - isAllowInsecureProtocol = true; + url = uri("http://nexus.hc.to/content/repositories/pub_releases/") + isAllowInsecureProtocol = true } } dependencies { // Things needed to compile the plugin - compileOnly("org.jetbrains:annotations:${DepData.JETBRAINS_ANNOTATIONS_VERSION}"); - compileOnly("org.spigotmc:spigot-api:${DepData.SPIGOT_VERSION}"); - compileOnly("net.milkbowl.vault:VaultAPI:${DepData.VAULT_API_VERSION}"); - compileOnly("com.sk89q.worldedit:worldedit-core:${DepData.WORLD_EDIT_CORE_VERSION}"); - compileOnly("com.sk89q.worldguard:worldguard-bukkit:${DepData.WORLD_GUARD_BUKKIT_VERSION}"); - compileOnly("me.clip:placeholderapi:${DepData.PLACEHOLDER_API_VERSION}"); - - testImplementation("org.junit.jupiter:junit-jupiter:${DepData.JUNIT_VERSION}"); - testRuntimeOnly("org.junit.platform:junit-platform-launcher:${DepData.JUNIT_LAUNCHER_VERSION}"); + compileOnly("org.jetbrains:annotations:${DepData.JETBRAINS_ANNOTATIONS_VERSION}") + compileOnly("org.spigotmc:spigot-api:${DepData.SPIGOT_VERSION}") + compileOnly("net.milkbowl.vault:VaultAPI:${DepData.VAULT_API_VERSION}") + compileOnly("com.sk89q.worldedit:worldedit-core:${DepData.WORLD_EDIT_CORE_VERSION}") + compileOnly("com.sk89q.worldguard:worldguard-bukkit:${DepData.WORLD_GUARD_BUKKIT_VERSION}") + compileOnly("me.clip:placeholderapi:${DepData.PLACEHOLDER_API_VERSION}") + + // We need these during runtime! + implementation("org.xerial:sqlite-jdbc:${DepData.SQLITE_JDBC_VERSION}") + implementation("org.eclipse.persistence:javax.persistence:${DepData.JAVAX_PERSISTENCE_VERSION}") + implementation("javax.transaction:transaction-api:${DepData.JAVAX_TRANSACTION_VERSION}") + implementation("com.github.h-thurow:q2o:${DepData.SANS_ORM_VERSION}") + + testImplementation("org.slf4j:slf4j-simple:${DepData.SLF4J_VERSION}") + testImplementation("org.junit.jupiter:junit-jupiter:${DepData.JUNIT_VERSION}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:${DepData.JUNIT_LAUNCHER_VERSION}") } diff --git a/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java b/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java index d12dafed..47c8fd75 100644 --- a/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java +++ b/src/main/java/com/cjburkey/claimchunk/ClaimChunk.java @@ -6,7 +6,9 @@ import com.cjburkey.claimchunk.cmd.*; import com.cjburkey.claimchunk.config.ClaimChunkWorldProfileHandler; import com.cjburkey.claimchunk.config.ccconfig.*; +import com.cjburkey.claimchunk.data.DataConvert; import com.cjburkey.claimchunk.data.newdata.*; +import com.cjburkey.claimchunk.data.sqlite.SqLiteDataHandler; import com.cjburkey.claimchunk.event.*; import com.cjburkey.claimchunk.i18n.V2JsonMessages; import com.cjburkey.claimchunk.layer.PlaceholderInitLayer; @@ -31,6 +33,7 @@ import org.jetbrains.annotations.Nullable; import java.io.*; +import java.nio.file.Files; // TODO: Split this plugin up into services that users can use // Services: @@ -64,10 +67,14 @@ public final class ClaimChunk extends JavaPlugin implements IClaimChunkPlugin { - // The global instance of ClaimChunk on this server - // A plugin can only exist in one instance on any given server so it's ok to have a static - // instance - private static ClaimChunk instance; + /** + * External quick access to the main ClaimChunk class. + * + *

A plugin can only exist in one instance on any given server so it's ok to have a static + * instance I think. We don't actually internally use this + */ + @Getter private static ClaimChunk instance; + // Set once ClaimChunk has registered the `chunk-claim` flag with WorldGuard. private static boolean worldGuardRegisteredFlag = false; @@ -139,7 +146,7 @@ public void onLoad() { // Get the current plugin version version = SemVer.fromString(getDescription().getVersion()); - if (version.marker != null) { + if (version.marker() != null) { Utils.debug("Plugin version is nonstandard release %s", version); } @@ -365,9 +372,10 @@ private void initAnonymousData() { } } + @SuppressWarnings("CommentedOutCode") private boolean initDataHandler() { // Initialize the data handler if another plugin hasn't substituted one already - if (dataHandler == null) { + /*if (dataHandler == null) { // The ternary operator is great // But it's ugly sometimes // Yuck! @@ -383,11 +391,55 @@ private boolean initDataHandler() { this::createJsonDataHandler, JsonDataHandler::deleteFiles)) : createJsonDataHandler(); + }*/ + if (dataHandler == null) { + File dataFolder = new File(getDataFolder(), "/data"); + File sqliteFile = new File(dataFolder, "/claimAndPlayerData.sqlite3"); + File oldClaimedFile = new File(dataFolder, "/claimedChunks.json"); + File oldPlayerFile = new File(dataFolder, "/playerData.json"); + boolean oldUseDb = config.getUseDatabase(); + + IClaimChunkDataHandler oldDataHandler = null; + if (!sqliteFile.exists() + && (oldUseDb || (oldClaimedFile.exists() && oldPlayerFile.exists()))) { + oldDataHandler = + oldUseDb + ? ((config.getGroupRequests()) + ? new BulkMySQLDataHandler<>( + this, this::createJsonDataHandler, ignored -> {}) + : new MySQLDataHandler<>( + this, this::createJsonDataHandler, ignored -> {})) + : createJsonDataHandler(); + } + + dataHandler = new SqLiteDataHandler(sqliteFile); + + if (oldDataHandler != null) { + try { + DataConvert.copyConvert(oldDataHandler, dataHandler); + oldDataHandler.exit(); + + if (oldClaimedFile.exists()) { + Files.move( + oldClaimedFile.toPath(), + new File(dataFolder, "/OLD_claimedChunks.json").toPath()); + } + if (oldPlayerFile.exists()) { + Files.move( + oldPlayerFile.toPath(), + new File(dataFolder, "/OLD_playerData.json").toPath()); + } + } catch (Exception e) { + throw new RuntimeException( + "Failed to initialize previous data handler to convert old data!", e); + } + } } + Utils.debug("Using data handler \"%s\"", dataHandler.getClass().getName()); try { // Initialize the data handler - dataHandler.init(); + if (!dataHandler.getHasInit()) dataHandler.init(); return true; } catch (Exception e) { Utils.err( @@ -400,6 +452,7 @@ private boolean initDataHandler() { "Please double check your config and make sure it's set to the correct data" + " information to ensure ClaimChunk can operate normally"); } + System.exit(-1); return false; } @@ -448,6 +501,7 @@ private void initEcon() { Utils.log("Economy not enabled."); } + @SuppressWarnings("unused") private JsonDataHandler createJsonDataHandler() { // Create the basic JSON data handler return new JsonDataHandler( @@ -483,7 +537,7 @@ private void handleAutoUnclaim() { // Unclaim all of the player's chunks for (ChunkPos chunk : claimedChunks) { chunkHandler.unclaimChunk( - getServer().getWorld(chunk.getWorld()), chunk.getX(), chunk.getZ()); + getServer().getWorld(chunk.world()), chunk.x(), chunk.z()); } Utils.log( @@ -635,6 +689,7 @@ public void onDisable() { dataHandler.exit(); Utils.debug("Cleaned up."); } catch (Exception e) { + Utils.err("Failed to clean up data handler!"); //noinspection CallToPrintStackTrace e.printStackTrace(); } @@ -659,19 +714,6 @@ public void onDisable() { Utils.log("Finished disable."); } - /** - * External quick access to the main ClaimChunk class. - * - * @return The current instance of ClaimChunk - * @see org.bukkit.plugin.PluginManager#getPlugin(String) - * @deprecated It is recommended to use {@code (ClaimChunk) - * Bukkit.getServer().getPluginManager().getPlugin("ClaimChunk")} - */ - @Deprecated - public static ClaimChunk getInstance() { - return instance; - } - public static class DataHandlerAlreadySetException extends Exception { @Serial private static final long serialVersionUID = 49857948732L; diff --git a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkHandler.java b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkHandler.java index 8f8e6567..0ef83699 100644 --- a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkHandler.java @@ -10,7 +10,6 @@ import org.bukkit.entity.Player; import java.util.*; -import java.util.stream.Collectors; // TODO: MOVE FLOOD FILL TO MAIN HANDLER AND REQUIRE RECIPIENT TO BE ONLINE TO // GUARANTEE QUOTA ISN'T EXCEEDED. @@ -29,13 +28,13 @@ public ChunkHandler(IClaimChunkDataHandler dataHandler, ClaimChunk claimChunk) { * Result returned by the {@link #fillClaimInto(String, int, int, int, int, UUID, Collection)} * and {@link #fillClaim(String, int, int, int, UUID)} methods. */ - public static enum FloodClaimResult { + public enum FloodClaimResult { /** The method completed without issues. */ - SUCCESSFULL, + SUCCESSFUL, /** The method recursed too many times and aborted due to that. */ - TOO_MANY_RECUSIONS, + TOO_MANY_RECURSIONS, /** The collection got too big and the method aborted due to that. */ COLLECTION_TOO_BIG, @@ -44,13 +43,13 @@ public static enum FloodClaimResult { * The algorithm hit a claimed chunk that did not belong to the player and aborted due to * this */ - HIT_NONPLAYER_CLAIM; + HIT_NONPLAYER_CLAIM, } /** - * Claims several chunks at once for a player. This method is very unsafe at it's own as it does + * Claims several chunks at once for a player. This method is very unsafe at its own as it does * not test whether the player can actually claim that chunk. This means that ownership can be - * overridden. And the player can go over it's quota as well + * overridden. And the player can go over its quota as well * * @param chunks The chunks to claim * @param player The player that claims these chunks @@ -70,10 +69,9 @@ private void claimAll(Collection chunks, UUID player) { * @param x The chunk x-coord. * @param z The chunk z-coord. * @param player The player for whom to claim the chunk. - * @param floodfill Whether or not flood filling should be attempted * @return The chunk position variable or {@code null} if the chunk is already claimed */ - public ChunkPos claimChunk(String world, int x, int z, UUID player, boolean floodfill) { + public ChunkPos claimChunk(String world, int x, int z, UUID player) { if (isClaimed(world, x, z)) { // If the chunk is already claimed, return null return null; @@ -112,19 +110,19 @@ public ChunkPos claimChunk(String world, int x, int z, UUID player, boolean floo - getClaimed(player)); Map.Entry, FloodClaimResult> result = fillClaim(world, x - 1, z, maxArea, player); - if (result.getValue() == FloodClaimResult.SUCCESSFULL) { + if (result.getValue() == FloodClaimResult.SUCCESSFUL) { claimAll(result.getKey(), player); } else { result = fillClaim(world, x + 1, z, maxArea, player); - if (result.getValue() == FloodClaimResult.SUCCESSFULL) { + if (result.getValue() == FloodClaimResult.SUCCESSFUL) { claimAll(result.getKey(), player); } else { result = fillClaim(world, x, z - 1, maxArea, player); - if (result.getValue() == FloodClaimResult.SUCCESSFULL) { + if (result.getValue() == FloodClaimResult.SUCCESSFUL) { claimAll(result.getKey(), player); } else { result = fillClaim(world, x, z + 1, maxArea, player); - if (result.getValue() == FloodClaimResult.SUCCESSFULL) { + if (result.getValue() == FloodClaimResult.SUCCESSFUL) { claimAll(result.getKey(), player); } } @@ -163,19 +161,19 @@ private FloodClaimResult fillClaimInto( UUID player, Collection collector) { if (recursions == 0) { - return FloodClaimResult.TOO_MANY_RECUSIONS; + return FloodClaimResult.TOO_MANY_RECURSIONS; } if (collector.size() > maxSize) { return FloodClaimResult.COLLECTION_TOO_BIG; } ChunkPos claimingPosition = new ChunkPos(world, x, z); if (collector.contains(claimingPosition)) { - return FloodClaimResult.SUCCESSFULL; + return FloodClaimResult.SUCCESSFUL; } UUID owner = getOwner(claimingPosition); if (owner != null) { if (owner.equals(player)) { - return FloodClaimResult.SUCCESSFULL; // Hit player claim, do not claim it + return FloodClaimResult.SUCCESSFUL; // Hit player claim, do not claim it } else { return FloodClaimResult.HIT_NONPLAYER_CLAIM; // Hit player claim, do not claim it } @@ -183,15 +181,15 @@ private FloodClaimResult fillClaimInto( collector.add(claimingPosition); FloodClaimResult result = fillClaimInto(world, x - 1, z, --recursions, maxSize, player, collector); - if (result != FloodClaimResult.SUCCESSFULL) { + if (result != FloodClaimResult.SUCCESSFUL) { return result; } result = fillClaimInto(world, x + 1, z, recursions, maxSize, player, collector); - if (result != FloodClaimResult.SUCCESSFULL) { + if (result != FloodClaimResult.SUCCESSFUL) { return result; } result = fillClaimInto(world, x, z - 1, recursions, maxSize, player, collector); - if (result != FloodClaimResult.SUCCESSFULL) { + if (result != FloodClaimResult.SUCCESSFUL) { return result; } return fillClaimInto(world, x, z + 1, recursions, maxSize, player, collector); @@ -212,21 +210,6 @@ private Map.Entry, FloodClaimResult> fillClaim( return new AbstractMap.SimpleEntry<>(positions, result); } - /** - * Claims a specific chunk for a player if that chunk is not already owned. This method doesn't - * do any checks other than previous ownership. It is not generally safe to use this method. - * Other public API methods should be used to claim chunks. Does not perform flood filling. - * - * @param world The current world. - * @param x The chunk x-coord. - * @param z The chunk z-coord. - * @param player The player for whom to claim the chunk. - * @return The chunk position variable or {@code null} if the chunk is already claimed - */ - public ChunkPos claimChunk(String world, int x, int z, UUID player) { - return claimChunk(world, x, z, player, false); - } - /** * Claims a specific chunk for a player if that chunk is not already owned. This method doesn't * do any checks other than previous ownership. It is not generally safe to use this method. @@ -236,22 +219,6 @@ public ChunkPos claimChunk(String world, int x, int z, UUID player) { * @param x The chunk x-coord. * @param z The chunk z-coord. * @param player The player for whom to claim the chunk. - * @param floodfill Whether or not flood filling should be attempted - * @return The chunk position variable or {@code null} if the chunk is already claimed - */ - public ChunkPos claimChunk(World world, int x, int z, UUID player, boolean floodfill) { - return claimChunk(world.getName(), x, z, player, floodfill); - } - - /** - * Claims a specific chunk for a player if that chunk is not already owned. This method doesn't - * do any checks other than previous ownership. It is not generally safe to use this method. - * Other public API methods should be used to claim chunks. Does not perform flood filling. - * - * @param world The current world. - * @param x The chunk x-coord. - * @param z The chunk z-coord. - * @param player The player for whom to claim the chunk. * @return The chunk position variable or {@code null} if the chunk is already claimed */ public ChunkPos claimChunk(World world, int x, int z, UUID player) { @@ -332,9 +299,9 @@ public int deleteAllWorldClaims(String worldName) { List chunks = Arrays.stream(dataHandler.getClaimedChunks()) .map(c -> c.chunk) - .filter(pos -> pos.getWorld().equals(worldName)) - .collect(Collectors.toList()); - chunks.forEach(pos -> unclaimChunk(worldName, pos.getX(), pos.getZ())); + .filter(pos -> pos.world().equals(worldName)) + .toList(); + chunks.forEach(pos -> unclaimChunk(worldName, pos.x(), pos.z())); return chunks.size(); } @@ -451,6 +418,7 @@ public boolean isOwner(World world, int x, int z, Player ply) { * @param ply The UUID of the player. * @return Whether this player owns this chunk. */ + @SuppressWarnings("unused") public boolean isOwner(Chunk chunk, UUID ply) { return isOwner(chunk.getWorld(), chunk.getX(), chunk.getZ(), ply); } @@ -503,20 +471,24 @@ public UUID getOwner(ChunkPos pos) { /** * Toggles whether TNT is enabled in the provided chunk. * - * @param chunk The Spigot chunk position. + * @param ignoredChunk The Spigot chunk position. * @return Whether TNT is now (after the toggle) enabled in this chunk. + * @deprecated DOES NOTHING! */ - public boolean toggleTnt(Chunk chunk) { - return dataHandler.toggleTnt(new ChunkPos(chunk)); + @Deprecated + public boolean toggleTnt(Chunk ignoredChunk) { + return false; } /** * Checks whether TNT is enabled in the provided chunk. * - * @param chunk The Spigot chunk position. + * @param ignoredChunk The Spigot chunk position. * @return Whether TNT is currently enabled in this chunk. + * @deprecated DOES NOTHING! */ - public boolean isTntEnabled(Chunk chunk) { - return dataHandler.isTntEnabled(new ChunkPos(chunk)); + @Deprecated + public boolean isTntEnabled(Chunk ignoredChunk) { + return false; } } diff --git a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkOutlineHandler.java b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkOutlineHandler.java index 2a0bd771..9ad8c93e 100644 --- a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkOutlineHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkOutlineHandler.java @@ -100,10 +100,10 @@ private class ChunkOutlineEntry { void onParticle() { // Loop through `yHeight*2+1` y-levels for (var y = plyY - yHeight; y <= plyY + yHeight; y++) { - var zAt = chunkPos.getZ() << 4; + var zAt = chunkPos.z() << 4; // Spawn particles along the x-axis if (sidesShown.north | sidesShown.south) { - for (var x = chunkPos.getX() << 4; x < ((chunkPos.getX() + 1) << 4); x++) { + for (var x = chunkPos.x() << 4; x < ((chunkPos.x() + 1) << 4); x++) { if (sidesShown.north) spawnParticle(player, x, y, zAt); if (sidesShown.south) spawnParticle(player, x, y, zAt + 15); } @@ -113,7 +113,7 @@ void onParticle() { if (sidesShown.east | sidesShown.west) { // TODO: Ignore these offsets for now, overdraw doesn't concern me very much lol for (var z = zAt /* + 1*/; z < zAt + 16 /* - 1*/; z++) { - var xAt = chunkPos.getX() << 4; + var xAt = chunkPos.x() << 4; if (sidesShown.east) spawnParticle(player, xAt, y, z); if (sidesShown.west) spawnParticle(player, xAt + 15, y, z); } diff --git a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPlayerPermissions.java b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPlayerPermissions.java index 218e16a7..506db81c 100644 --- a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPlayerPermissions.java +++ b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPlayerPermissions.java @@ -1,16 +1,30 @@ package com.cjburkey.claimchunk.chunk; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; +import java.util.Objects; public class ChunkPlayerPermissions { + public static final class Masks { + public static int BREAK = 1; + public static int PLACE = 1 << 1; + public static int DOOR = 1 << 2; + public static int REDSTONE = 1 << 3; + public static int VEHICLE = 1 << 4; + public static int INTERACT_ENTITY = 1 << 5; + public static int INTERACT_BLOCK = 1 << 6; + public static int CONTAINERS = 1 << 7; + } + /** * The flags for each permission. These are combined into a single integer to save space in the * file. Format (from Rightmost (i.e. least significant) bit to leftmost): Break, Place, Doors, * Redstone, Ride Boats/Minecarts, interact with entities, interact with blocks, open containers */ - private int permissionFlags; + public int permissionFlags; public ChunkPlayerPermissions() { permissionFlags = 0; @@ -20,104 +34,77 @@ public ChunkPlayerPermissions(final int permissionFlags) { this.permissionFlags = permissionFlags; } + public boolean checkMask(int mask) { + return (permissionFlags & mask) == mask; + } + + private void setAllow(int mask, boolean allow) { + if (allow) permissionFlags |= mask; + else permissionFlags &= ~mask; + } + public boolean canBreak() { - return (permissionFlags & 1) == 1; + return checkMask(Masks.BREAK); } public void allowBreak(final boolean allow) { - if (allow) { - permissionFlags |= 1; - } else { - permissionFlags &= ~1; - } + setAllow(Masks.BREAK, allow); } public boolean canPlace() { - return (permissionFlags & 2) == 2; + return checkMask(Masks.PLACE); } public void allowPlace(final boolean allow) { - if (allow) { - permissionFlags |= 2; - } else { - permissionFlags &= ~2; - } + setAllow(Masks.PLACE, allow); } public boolean canUseDoors() { - return (permissionFlags & 4) == 4; + return checkMask(Masks.DOOR); } public void allowUseDoors(final boolean allow) { - if (allow) { - permissionFlags |= 4; - } else { - permissionFlags &= ~4; - } + setAllow(Masks.DOOR, allow); } public boolean canUseRedstone() { - return (permissionFlags & 8) == 8; + return checkMask(Masks.REDSTONE); } public void allowUseRedstone(final boolean allow) { - if (allow) { - permissionFlags |= 8; - } else { - permissionFlags &= ~8; - } + setAllow(Masks.REDSTONE, allow); } public boolean canUseVehicles() { - return (permissionFlags & 16) == 16; + return checkMask(Masks.VEHICLE); } public void allowUseVehicles(final boolean allow) { - if (allow) { - permissionFlags |= 16; - } else { - permissionFlags &= ~16; - } + setAllow(Masks.VEHICLE, allow); } public boolean canInteractEntities() { - return (permissionFlags & 32) == 32; + return checkMask(Masks.INTERACT_ENTITY); } public void allowInteractEntities(final boolean allow) { - if (allow) { - permissionFlags |= 32; - } else { - permissionFlags &= ~32; - } + setAllow(Masks.INTERACT_ENTITY, allow); } public boolean canInteractBlocks() { - return (permissionFlags & 64) == 64; + return checkMask(Masks.INTERACT_BLOCK); } public void allowInteractBlocks(final boolean allow) { - if (allow) { - permissionFlags |= 64; - } else { - permissionFlags &= ~64; - } + setAllow(Masks.INTERACT_BLOCK, allow); } public boolean canUseContainers() { - return (permissionFlags & 128) == 128; + return checkMask(Masks.CONTAINERS); } public void allowUseContainers(final boolean allow) { - if (allow) { - permissionFlags |= 128; - } else { - permissionFlags &= ~128; - } - } - - public int getPermissionFlags() { - return permissionFlags; + setAllow(Masks.CONTAINERS, allow); } public Map toPermissionsMap() { @@ -135,24 +122,37 @@ public Map toPermissionsMap() { return permissionsMap; } - public static ChunkPlayerPermissions fromPermissionsMap(Map permissions) { + public static @NotNull ChunkPlayerPermissions fromPermissionsMap( + @NotNull Map permissions) { ChunkPlayerPermissions chunkPlayerPermissions = new ChunkPlayerPermissions(); for (Map.Entry perm : permissions.entrySet()) { + boolean permVal = perm.getValue(); switch (perm.getKey()) { - case "break" -> chunkPlayerPermissions.allowBreak(perm.getValue()); - case "place" -> chunkPlayerPermissions.allowPlace(perm.getValue()); - case "doors" -> chunkPlayerPermissions.allowUseDoors(perm.getValue()); - case "redstone" -> chunkPlayerPermissions.allowUseRedstone(perm.getValue()); - case "interactVehicles" -> chunkPlayerPermissions.allowUseVehicles(perm.getValue()); - case "interactEntities" -> - chunkPlayerPermissions.allowInteractEntities(perm.getValue()); - case "interactBlocks" -> - chunkPlayerPermissions.allowInteractBlocks(perm.getValue()); - case "useContainers" -> chunkPlayerPermissions.allowUseContainers(perm.getValue()); + case "break" -> chunkPlayerPermissions.allowBreak(permVal); + case "place" -> chunkPlayerPermissions.allowPlace(permVal); + case "doors" -> chunkPlayerPermissions.allowUseDoors(permVal); + case "redstone" -> chunkPlayerPermissions.allowUseRedstone(permVal); + case "interactVehicles" -> chunkPlayerPermissions.allowUseVehicles(permVal); + case "interactEntities" -> chunkPlayerPermissions.allowInteractEntities(permVal); + case "interactBlocks" -> chunkPlayerPermissions.allowInteractBlocks(permVal); + case "useContainers" -> chunkPlayerPermissions.allowUseContainers(permVal); } } return chunkPlayerPermissions; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChunkPlayerPermissions that = (ChunkPlayerPermissions) o; + return permissionFlags == that.permissionFlags; + } + + @Override + public int hashCode() { + return Objects.hash(permissionFlags); + } } diff --git a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPos.java b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPos.java index b37df07a..3b7f9649 100644 --- a/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPos.java +++ b/src/main/java/com/cjburkey/claimchunk/chunk/ChunkPos.java @@ -4,24 +4,7 @@ import java.util.Objects; -public final class ChunkPos { - - private final String world; - private final int x; - private final int z; - - /** - * Create an instance of a chunk position from raw data. - * - * @param world The name of the world that this chunk is in. - * @param x The x-coordinate of this chunk (in chunk coordinates). - * @param z The y-coordinate of this chunk (in chunk coordinates). - */ - public ChunkPos(String world, int x, int z) { - this.world = world; - this.x = x; - this.z = z; - } +public record ChunkPos(String world, int x, int z) { /** * Create an instance of a chunk position from Spigot's chunk position representation. @@ -32,33 +15,6 @@ public ChunkPos(Chunk chunk) { this(chunk.getWorld().getName(), chunk.getX(), chunk.getZ()); } - /** - * Get the name of the world that this chunk is in. - * - * @return The world name of this chunk. - */ - public String getWorld() { - return world; - } - - /** - * Get the x-coordinate of this chunk. - * - * @return The x-coordinate of this chunk (in chunk coordinates). - */ - public int getX() { - return x; - } - - /** - * Get the y-coordinate of this chunk. - * - * @return The y-coordinate of this chunk (in chunk coordinates). - */ - public int getZ() { - return z; - } - /** * Helper method to get a chunk north, relative to this one. * diff --git a/src/main/java/com/cjburkey/claimchunk/cmd/MainHandler.java b/src/main/java/com/cjburkey/claimchunk/cmd/MainHandler.java index 05f18a43..b7d2ddb1 100644 --- a/src/main/java/com/cjburkey/claimchunk/cmd/MainHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/cmd/MainHandler.java @@ -56,7 +56,7 @@ public void outlineChunk(ChunkPos chunk, Player showTo, int timeToShow) { List particleLocations = new ArrayList<>(); // The current world - World world = claimChunk.getServer().getWorld(chunk.getWorld()); + World world = claimChunk.getServer().getWorld(chunk.world()); // Make sure the world is valid if (world == null) { return; @@ -66,8 +66,8 @@ public void outlineChunk(ChunkPos chunk, Player showTo, int timeToShow) { int showTimeInSeconds = Utils.clamp(timeToShow, 1, 60); // Get the start position in world coordinates - int xStart = chunk.getX() << 4; - int zStart = chunk.getZ() << 4; + int xStart = chunk.x() << 4; + int zStart = chunk.z() << 4; int yStart = (int) showTo.getLocation().getY() - 1; // The particle effects with be three blocks tall @@ -138,8 +138,7 @@ public void claimChunk(Player p, Chunk loc) { loc.getWorld(), loc.getX(), loc.getZ(), - p.getUniqueId(), - true); + p.getUniqueId()); // Error check, though it *shouldn't* occur if (pos == null) { diff --git a/src/main/java/com/cjburkey/claimchunk/data/DataConvert.java b/src/main/java/com/cjburkey/claimchunk/data/DataConvert.java new file mode 100644 index 00000000..0782ee78 --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/data/DataConvert.java @@ -0,0 +1,42 @@ +package com.cjburkey.claimchunk.data; + +import com.cjburkey.claimchunk.data.newdata.IClaimChunkDataHandler; + +/** + * Represents a class that may act as a converter between two different data systems. + * + * @since 0.0.13 + */ +public class DataConvert { + + private DataConvert() {} + + /** + * Copies the data from the provided old data handler into the provided new data handler. This + * does not update the old data handler. + * + * @param oldDataHandler The old handler, may or may not be initialized + * @param newDataHandler The new handler, may or may not be initialized + * @since 0.0.13 + */ + public static void copyConvert( + IClaimChunkDataHandler oldDataHandler, IClaimChunkDataHandler newDataHandler) + throws Exception { + // Initialize the old data handler if it hasn't been initialized yet + if (!oldDataHandler.getHasInit()) oldDataHandler.init(); + + // Load the old data + oldDataHandler.load(); + + // Initialize the new data handler if it hasn't been initialized yet + if (!newDataHandler.getHasInit()) newDataHandler.init(); + + // Copy the player data from the old data handler to the new data handler. + // Make sure we do this before players! The SQLite data handler will make dummy players if + // there aren't proper players in the player data table already. + newDataHandler.addPlayers(oldDataHandler.getFullPlayerData()); + + // Copy the chunks from the old data handler to the new data handler + newDataHandler.addClaimedChunks(oldDataHandler.getClaimedChunks()); + } +} diff --git a/src/main/java/com/cjburkey/claimchunk/data/conversion/ConvertJsonToMySQL.java b/src/main/java/com/cjburkey/claimchunk/data/conversion/ConvertJsonToMySQL.java deleted file mode 100644 index e55a6e7b..00000000 --- a/src/main/java/com/cjburkey/claimchunk/data/conversion/ConvertJsonToMySQL.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.cjburkey.claimchunk.data.conversion; - -import com.cjburkey.claimchunk.ClaimChunk; -import com.cjburkey.claimchunk.data.newdata.JsonDataHandler; -import com.cjburkey.claimchunk.data.newdata.MySQLDataHandler; - -@SuppressWarnings("unused") -public class ConvertJsonToMySQL implements IDataConverter> { - - private final ClaimChunk claimChunk; - - public ConvertJsonToMySQL(ClaimChunk claimChunk) { - this.claimChunk = claimChunk; - } - - @Override - public MySQLDataHandler convert(JsonDataHandler oldDataHandler) throws Exception { - // Create and a new MySQL data handler - MySQLDataHandler newDataHandler = new MySQLDataHandler<>(claimChunk, null, null); - - // Initialize the new data handler - newDataHandler.init(); - - // Convert from the old data handler to the new one - IDataConverter.copyConvert(oldDataHandler, newDataHandler); - - // Return the new data handler - return newDataHandler; - } -} diff --git a/src/main/java/com/cjburkey/claimchunk/data/conversion/ConvertMySQLToJson.java b/src/main/java/com/cjburkey/claimchunk/data/conversion/ConvertMySQLToJson.java deleted file mode 100644 index 2f05d8c0..00000000 --- a/src/main/java/com/cjburkey/claimchunk/data/conversion/ConvertMySQLToJson.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.cjburkey.claimchunk.data.conversion; - -import com.cjburkey.claimchunk.ClaimChunk; -import com.cjburkey.claimchunk.data.newdata.JsonDataHandler; -import com.cjburkey.claimchunk.data.newdata.MySQLDataHandler; - -import java.io.File; - -@SuppressWarnings("unused") -public class ConvertMySQLToJson implements IDataConverter, JsonDataHandler> { - - private final ClaimChunk claimChunk; - private final File claimedChunksFile; - private final File joinedPlayersFile; - - public ConvertMySQLToJson( - ClaimChunk claimChunk, File claimedChunksFile, File joinedPlayersFile) { - this.claimChunk = claimChunk; - this.claimedChunksFile = claimedChunksFile; - this.joinedPlayersFile = joinedPlayersFile; - } - - @Override - public JsonDataHandler convert(MySQLDataHandler oldDataHandler) throws Exception { - // Create and a new MySQL data handler - JsonDataHandler newDataHandler = - new JsonDataHandler(claimChunk, claimedChunksFile, joinedPlayersFile); - - // Initialize the new data handler - newDataHandler.init(); - - // Convert from the old data handler to the new one - IDataConverter.copyConvert(oldDataHandler, newDataHandler); - - // Return the new data handler - return newDataHandler; - } -} diff --git a/src/main/java/com/cjburkey/claimchunk/data/conversion/IDataConverter.java b/src/main/java/com/cjburkey/claimchunk/data/conversion/IDataConverter.java deleted file mode 100644 index 28a6afef..00000000 --- a/src/main/java/com/cjburkey/claimchunk/data/conversion/IDataConverter.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.cjburkey.claimchunk.data.conversion; - -import com.cjburkey.claimchunk.data.newdata.IClaimChunkDataHandler; - -/** - * Represents a class that may act as a converter between two different data systems. - * - * @param The type from which the conversion may occur - * @param The type into which the provided handler will be converted - * @since 0.0.13 - */ -public interface IDataConverter< - From extends IClaimChunkDataHandler, To extends IClaimChunkDataHandler> { - - /** - * Copies the data from the provided old data handler into the provided new data handler. This - * does not update the old data handler. - * - * @param oldDataHandler The old handler - * @param newDataHandler The new handler - * @param The type of the old data handler - * @param The type of the new data handler - * @since 0.0.13 - */ - static void copyConvert( - A oldDataHandler, B newDataHandler) throws Exception { - // Initialize the old data handler if it hasn't been initialized yet - if (!oldDataHandler.getHasInit()) oldDataHandler.init(); - - // Load the old data - oldDataHandler.load(); - - // Initialize the new data handler if it hasn't been initialized yet - if (!newDataHandler.getHasInit()) newDataHandler.init(); - - // Copy the chunks from the old data handler to the new data handler - newDataHandler.addClaimedChunks(oldDataHandler.getClaimedChunks()); - - // Copy the player data from the old data handler to the new data handler - newDataHandler.addPlayers(oldDataHandler.getFullPlayerData()); - } - - /** - * Converts one kind of data handler into the other kind. - * - * @param oldDataHandler The old data handler - * @return A new data handler containing the old data handler's data - * @throws Exception Any error that may occur during any phase of data conversion - * @since 0.0.13 - */ - @SuppressWarnings("unused") - To convert(From oldDataHandler) throws Exception; -} diff --git a/src/main/java/com/cjburkey/claimchunk/data/newdata/BulkMySQLDataHandler.java b/src/main/java/com/cjburkey/claimchunk/data/newdata/BulkMySQLDataHandler.java index b4d1461b..40d62ce0 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/newdata/BulkMySQLDataHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/data/newdata/BulkMySQLDataHandler.java @@ -143,6 +143,7 @@ public void addClaimedChunks(DataChunk[] chunks) { @Override public void removeClaimedChunk(ChunkPos pos) { dataHandler.removeClaimedChunk(pos); + super.removeClaimedChunk(pos); } @Override @@ -161,16 +162,6 @@ public DataChunk[] getClaimedChunks() { return dataHandler.getClaimedChunks(); } - @Override - public boolean toggleTnt(ChunkPos pos) { - return dataHandler.toggleTnt(pos); - } - - @Override - public boolean isTntEnabled(ChunkPos pos) { - return dataHandler.isTntEnabled(pos); - } - @Override public void addPlayer( UUID player, @@ -224,6 +215,7 @@ public void givePlayerAccess( @Override public void takePlayerAccess(ChunkPos chunk, UUID accessor) { dataHandler.takePlayerAccess(chunk, accessor); + super.takePlayerAccess(chunk, accessor); } @Override diff --git a/src/main/java/com/cjburkey/claimchunk/data/newdata/IClaimChunkDataHandler.java b/src/main/java/com/cjburkey/claimchunk/data/newdata/IClaimChunkDataHandler.java index 06790014..015a0b0c 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/newdata/IClaimChunkDataHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/data/newdata/IClaimChunkDataHandler.java @@ -113,7 +113,7 @@ public interface IClaimChunkDataHandler { UUID getChunkOwner(ChunkPos pos); /** - * Retrives all claimed chunks and their owners across all worlds. + * Retrieves all claimed chunks and their owners across all worlds. * * @return An array of all claimed chunks * @since 0.0.13 @@ -123,21 +123,29 @@ public interface IClaimChunkDataHandler { /** * Toggles whether TNT can explode in the given chunk. * - * @param pos The position of the chunk + * @param ignoredPos The position of the chunk * @return Whether TNT is now enabled in the provided chunk * @since 0.0.16 + * @deprecated Unused. */ - boolean toggleTnt(ChunkPos pos); + @Deprecated + default boolean toggleTnt(ChunkPos ignoredPos) { + return false; + } /** * Retrieves whether TNT can explode in the given chunk (regardless of whether TNT is disabled * in the config). * - * @param pos The position of the chunk + * @param ignoredPos The position of the chunk * @return Whether TNT is enabled in the provided chunk * @since 0.0.16 + * @deprecated Unused. */ - boolean isTntEnabled(ChunkPos pos); + @Deprecated + default boolean isTntEnabled(ChunkPos ignoredPos) { + return false; + } // -- PLAYERS -- // @@ -147,7 +155,7 @@ public interface IClaimChunkDataHandler { * @param player The UUID of the player * @param lastIgn The in-game name of the player * @param chunkName The display name for this player's chunks - * @param lastOnlineTime The last time (in ms since January 1, 1970 UTC) that the player was + * @param lastOnlineTime The last time (in ms since January 1, 1970, UTC) that the player was * online * @param alerts Whether to send this player alerts when someone enters their chunks * @since 0.0.24 @@ -185,7 +193,7 @@ default void addPlayer(FullPlayerData playerData) { * @since 0.0.24 */ default void addPlayer(UUID player, String lastIgn, boolean alerts, int defaultMaxClaims) { - this.addPlayer(player, lastIgn, null, 0L, alerts, defaultMaxClaims); + this.addPlayer(player, lastIgn, null, System.currentTimeMillis(), alerts, defaultMaxClaims); } /** @@ -217,7 +225,7 @@ default void addPlayer(UUID player, String lastIgn, boolean alerts, int defaultM UUID getPlayerUUID(String username); /** - * Set the last time (in ms since January 1, 1970 UTC) that the player was online. + * Set the last time (in ms since January 1, 1970, UTC) that the player was online. * * @param player The player whose time should be updated * @param time The new time since the player was last online @@ -229,7 +237,7 @@ default void addPlayer(UUID player, String lastIgn, boolean alerts, int defaultM * Sets the given player's chunk's display name. * * @param player The player whose chunks should have the new display name - * @param name The new display name for this players chunks or {@code null} to clear it + * @param name The new display name for this player's chunks or {@code null} to clear it * @since 0.0.13 */ void setPlayerChunkName(UUID player, @Nullable String name); @@ -238,7 +246,7 @@ default void addPlayer(UUID player, String lastIgn, boolean alerts, int defaultM * Retrieves the given player's chunk's display name. * * @param player The player whose chunks' name to check - * @return The new display name for this players chunks or {@code null} if the player has not + * @return The new display name for this player's chunks or {@code null} if the player has not * joined or the chunks are unnamed * @since 0.0.13 */ @@ -289,7 +297,7 @@ default void addPlayer(UUID player, String lastIgn, boolean alerts, int defaultM * Clamp to 0 * * @param player The player's UUID - * @param numToAdd Number of claims to add + * @param numToTake Number of claims to add * @since 0.0.24 */ void takePlayerExtraMaxClaims(UUID player, int numToTake); diff --git a/src/main/java/com/cjburkey/claimchunk/data/newdata/JsonDataHandler.java b/src/main/java/com/cjburkey/claimchunk/data/newdata/JsonDataHandler.java index 1b12ab54..f6d723f2 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/newdata/JsonDataHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/data/newdata/JsonDataHandler.java @@ -82,21 +82,14 @@ public void load() throws Exception { if (claimedChunks.values().stream().allMatch(c -> c.playerPermissions == null)) { // If all playerPermissions are null, then the JSON files are in the pre 0.0.24 format loadPre0024Data(); + Utils.log("Performing 0.0.23 to 0.0.24+ player data format"); } else if (joinedPlayersFile != null && joinedPlayersFile.exists()) { - joinedPlayers.clear(); for (FullPlayerData player : loadJsonFile(joinedPlayersFile, FullPlayerData[].class)) { joinedPlayers.put(player.player, player); } } } - public void deleteFiles() { - if (claimedChunksFile != null && !claimedChunksFile.delete()) - Utils.err("Failed to delete claimed chunks file"); - if (joinedPlayersFile != null && !joinedPlayersFile.delete()) - Utils.err("Failed to delete joined players file"); - } - void clearData() { claimedChunks.clear(); joinedPlayers.clear(); @@ -150,18 +143,6 @@ public DataChunk[] getClaimedChunks() { .toArray(DataChunk[]::new); } - @Override - public boolean toggleTnt(ChunkPos pos) { - DataChunk chunk = claimedChunks.get(pos); - if (chunk == null) return false; - return (chunk.tnt = !chunk.tnt); - } - - @Override - public boolean isTntEnabled(ChunkPos pos) { - return claimedChunks.containsKey(pos) && claimedChunks.get(pos).tnt; - } - @Override public void addPlayer( UUID player, @@ -446,7 +427,9 @@ private void loadPre0024Data() throws Exception { // Backup existing files doBackup(joinedPlayersFile); - doBackup(claimedChunksFile); + if (claimedChunksFile != null && claimedChunksFile.exists()) { + doBackup(claimedChunksFile); + } } } } diff --git a/src/main/java/com/cjburkey/claimchunk/data/newdata/MySQLDataHandler.java b/src/main/java/com/cjburkey/claimchunk/data/newdata/MySQLDataHandler.java index ee030a98..797e1250 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/newdata/MySQLDataHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/data/newdata/MySQLDataHandler.java @@ -7,7 +7,7 @@ import com.cjburkey.claimchunk.chunk.ChunkPlayerPermissions; import com.cjburkey.claimchunk.chunk.ChunkPos; import com.cjburkey.claimchunk.chunk.DataChunk; -import com.cjburkey.claimchunk.data.conversion.IDataConverter; +import com.cjburkey.claimchunk.data.DataConvert; import com.cjburkey.claimchunk.player.FullPlayerData; import com.cjburkey.claimchunk.player.SimplePlayerData; @@ -116,7 +116,7 @@ public void init() throws Exception { } if (oldDataHandler != null && claimChunk.getConfigHandler().getConvertOldData()) { - IDataConverter.copyConvert(oldDataHandler, this); + DataConvert.copyConvert(oldDataHandler, this); oldDataHandler.exit(); if (onCleanOld != null) { onCleanOld.accept(oldDataHandler); @@ -155,9 +155,9 @@ public void addClaimedChunk(ChunkPos pos, UUID player) { CLAIMED_CHUNKS_Z, CLAIMED_CHUNKS_OWNER); try (PreparedStatement statement = prep(claimChunk, connection, sql)) { - statement.setString(1, pos.getWorld()); - statement.setInt(2, pos.getX()); - statement.setInt(3, pos.getZ()); + statement.setString(1, pos.world()); + statement.setInt(2, pos.x()); + statement.setInt(3, pos.z()); statement.setString(4, player.toString()); statement.execute(); } catch (Exception e) { @@ -187,9 +187,9 @@ public void addClaimedChunks(DataChunk[] chunks) { try (PreparedStatement statement = prep(claimChunk, connection, sql.toString())) { int i = 0; for (DataChunk chunk : chunks) { - statement.setString(4 * i + 1, chunk.chunk.getWorld()); - statement.setInt(4 * i + 2, chunk.chunk.getX()); - statement.setInt(4 * i + 3, chunk.chunk.getZ()); + statement.setString(4 * i + 1, chunk.chunk.world()); + statement.setInt(4 * i + 2, chunk.chunk.x()); + statement.setInt(4 * i + 3, chunk.chunk.z()); statement.setString(4 * i + 4, chunk.player.toString()); i++; } @@ -202,19 +202,61 @@ public void addClaimedChunks(DataChunk[] chunks) { } } + @SuppressWarnings("DuplicatedCode") @Override public void removeClaimedChunk(ChunkPos pos) { - String sql = - String.format( - "DELETE FROM `%s` WHERE `%s`=? AND `%s`=? AND `%s`=?", - CLAIMED_CHUNKS_TABLE_NAME, - CLAIMED_CHUNKS_WORLD, - CLAIMED_CHUNKS_X, - CLAIMED_CHUNKS_Z); - try (PreparedStatement statement = prep(claimChunk, connection, sql)) { - statement.setString(1, pos.getWorld()); - statement.setInt(2, pos.getX()); - statement.setInt(3, pos.getZ()); + // Get the chunk ID + int chunkId; + try (PreparedStatement statement = + prep( + claimChunk, + connection, + String.format( + "SELECT `%s` FROM `%s` WHERE `%s`=? AND `%s`=? AND `%s`=?", + CLAIMED_CHUNKS_ID, + CLAIMED_CHUNKS_TABLE_NAME, + CLAIMED_CHUNKS_WORLD, + CLAIMED_CHUNKS_X, + CLAIMED_CHUNKS_Z))) { + statement.setString(1, pos.world()); + statement.setInt(2, pos.x()); + statement.setInt(3, pos.z()); + + ResultSet resultSet = statement.executeQuery(); + if (!resultSet.next()) return; + chunkId = resultSet.getInt(1); + } catch (Exception e) { + Utils.err("Failed to get chunk id: %s", e.getMessage()); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + return; + } + + // Remove chunk accesses + try (PreparedStatement statement = + prep( + claimChunk, + connection, + String.format( + "DELETE FROM `%s` WHERE `%s`=?", + ACCESS_TABLE_NAME, ACCESS_CHUNK_ID))) { + statement.setInt(1, chunkId); + statement.execute(); + } catch (Exception e) { + Utils.err("Failed to unclaim chunk: %s", e.getMessage()); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + + // Delete the chunk + try (PreparedStatement statement = + prep( + claimChunk, + connection, + String.format( + "DELETE FROM `%s` WHERE `%s`=?", + CLAIMED_CHUNKS_TABLE_NAME, CLAIMED_CHUNKS_ID))) { + statement.setInt(1, chunkId); statement.execute(); } catch (Exception e) { Utils.err("Failed to unclaim chunk: %s", e.getMessage()); @@ -233,9 +275,9 @@ public boolean isChunkClaimed(ChunkPos pos) { CLAIMED_CHUNKS_X, CLAIMED_CHUNKS_Z); try (PreparedStatement statement = prep(claimChunk, connection, sql)) { - statement.setString(1, pos.getWorld()); - statement.setInt(2, pos.getX()); - statement.setInt(3, pos.getZ()); + statement.setString(1, pos.world()); + statement.setInt(2, pos.x()); + statement.setInt(3, pos.z()); try (ResultSet result = statement.executeQuery()) { if (result.next()) return result.getInt(1) > 0; } @@ -259,9 +301,9 @@ public UUID getChunkOwner(ChunkPos pos) { CLAIMED_CHUNKS_X, CLAIMED_CHUNKS_Z); try (PreparedStatement statement = prep(claimChunk, connection, sql)) { - statement.setString(1, pos.getWorld()); - statement.setInt(2, pos.getX()); - statement.setInt(3, pos.getZ()); + statement.setString(1, pos.world()); + statement.setInt(2, pos.x()); + statement.setInt(3, pos.z()); try (ResultSet result = statement.executeQuery()) { if (result.next()) return UUID.fromString(result.getString(1)); } @@ -308,57 +350,6 @@ public DataChunk[] getClaimedChunks() { return chunks.toArray(new DataChunk[0]); } - @Override - public boolean toggleTnt(ChunkPos pos) { - boolean current = isTntEnabled(pos); - String sql = - String.format( - "UPDATE `%s` SET `%s`=? WHERE (`%s`=?) AND (`%s`=?) AND (`%s`=?)", - CLAIMED_CHUNKS_TABLE_NAME, - CLAIMED_CHUNKS_TNT, - CLAIMED_CHUNKS_WORLD, - CLAIMED_CHUNKS_X, - CLAIMED_CHUNKS_Z); - try (PreparedStatement statement = prep(claimChunk, connection, sql)) { - statement.setBoolean(1, !current); - statement.setString(2, pos.getWorld()); - statement.setInt(3, pos.getX()); - statement.setInt(4, pos.getZ()); - statement.execute(); - return !current; - } catch (Exception e) { - Utils.err("Failed to update tnt enabled in chunk: %s", e.getMessage()); - //noinspection CallToPrintStackTrace - e.printStackTrace(); - } - return current; - } - - @Override - public boolean isTntEnabled(ChunkPos pos) { - String sql = - String.format( - "SELECT `%s` FROM `%s` WHERE (`%s`=?) AND (`%s`=?) AND (`%s`=?)", - CLAIMED_CHUNKS_TNT, - CLAIMED_CHUNKS_TABLE_NAME, - CLAIMED_CHUNKS_WORLD, - CLAIMED_CHUNKS_X, - CLAIMED_CHUNKS_Z); - try (PreparedStatement statement = prep(claimChunk, connection, sql)) { - statement.setString(1, pos.getWorld()); - statement.setInt(2, pos.getX()); - statement.setInt(3, pos.getZ()); - try (ResultSet result = statement.executeQuery()) { - if (result.next()) return result.getBoolean(1); - } - } catch (Exception e) { - Utils.err("Failed to retrieve tnt enabled in chunk: %s", e.getMessage()); - //noinspection CallToPrintStackTrace - e.printStackTrace(); - } - return false; - } - @Override public void addPlayer( UUID player, @@ -393,6 +384,7 @@ public void addPlayer( } } + @SuppressWarnings("ExtractMethodRecommender") @Override public void addPlayers(FullPlayerData[] players) { if (players.length == 0) return; @@ -579,6 +571,7 @@ public void setPlayerExtraMaxClaims(UUID player, int extraMaxClaims) { } } + @SuppressWarnings("DuplicatedCode") @Override public void addPlayerExtraMaxClaims(UUID player, int numToAdd) { String sql = @@ -596,6 +589,7 @@ public void addPlayerExtraMaxClaims(UUID player, int numToAdd) { } } + @SuppressWarnings("DuplicatedCode") @Override public void takePlayerExtraMaxClaims(UUID player, int numToTake) { // Ugly but idk how to do this in sql :( @@ -724,9 +718,9 @@ public void givePlayerAccess( CLAIMED_CHUNKS_X, CLAIMED_CHUNKS_Z); try (PreparedStatement chunkIdStatement = prep(claimChunk, connection, getChunkIdSql)) { - chunkIdStatement.setString(1, chunk.getWorld()); - chunkIdStatement.setInt(2, chunk.getX()); - chunkIdStatement.setInt(3, chunk.getZ()); + chunkIdStatement.setString(1, chunk.world()); + chunkIdStatement.setInt(2, chunk.x()); + chunkIdStatement.setInt(3, chunk.z()); try (ResultSet result = chunkIdStatement.executeQuery()) { if (result.next()) { @@ -757,7 +751,7 @@ public void givePlayerAccess( ACCESS_ACCESS_ID); try (PreparedStatement updateStatement = prep(claimChunk, connection, updateStatementSql)) { - updateStatement.setInt(1, permissions.getPermissionFlags()); + updateStatement.setInt(1, permissions.permissionFlags); updateStatement.setInt(2, result.getInt(1)); updateStatement.execute(); @@ -779,7 +773,7 @@ public void givePlayerAccess( insertStatement.setInt(1, chunkId); insertStatement.setString(2, chunkOwner); insertStatement.setString(3, accessor.toString()); - insertStatement.setInt(4, permissions.getPermissionFlags()); + insertStatement.setInt(4, permissions.permissionFlags); insertStatement.execute(); } @@ -835,13 +829,13 @@ public void writeAccessAssociationsBulk(DataChunk[] chunks) { for (DataChunk c : chunks) { for (Map.Entry entry : c.playerPermissions.entrySet()) { - statement.setString(6 * i + 1, c.chunk.getWorld()); - statement.setInt(6 * i + 2, c.chunk.getX()); - statement.setInt(6 * i + 3, c.chunk.getZ()); + statement.setString(6 * i + 1, c.chunk.world()); + statement.setInt(6 * i + 2, c.chunk.x()); + statement.setInt(6 * i + 3, c.chunk.z()); statement.setString(6 * i + 4, c.player.toString()); statement.setString(6 * i + 5, entry.getKey().toString()); - statement.setInt(6 * i + 6, entry.getValue().getPermissionFlags()); - statement.setInt(6 * i + 6, entry.getValue().getPermissionFlags()); + statement.setInt(6 * i + 6, entry.getValue().permissionFlags); + statement.setInt(6 * i + 6, entry.getValue().permissionFlags); i++; } } @@ -867,9 +861,9 @@ public void takePlayerAccess(ChunkPos chunk, UUID accessor) { CLAIMED_CHUNKS_Z); try (PreparedStatement chunkIdStatement = prep(claimChunk, connection, getChunkIdSql)) { - chunkIdStatement.setString(1, chunk.getWorld()); - chunkIdStatement.setInt(2, chunk.getX()); - chunkIdStatement.setInt(3, chunk.getZ()); + chunkIdStatement.setString(1, chunk.world()); + chunkIdStatement.setInt(2, chunk.x()); + chunkIdStatement.setInt(3, chunk.z()); try (ResultSet chunkIdResult = chunkIdStatement.executeQuery()) { if (chunkIdResult.next()) { @@ -914,9 +908,9 @@ public Map getPlayersWithAccess(ChunkPos chunk) { CLAIMED_CHUNKS_Z); try (PreparedStatement statement = prep(claimChunk, connection, getPlayerPermsSql)) { - statement.setString(1, chunk.getWorld()); - statement.setInt(2, chunk.getX()); - statement.setInt(3, chunk.getZ()); + statement.setString(1, chunk.world()); + statement.setInt(2, chunk.x()); + statement.setInt(3, chunk.z()); try (ResultSet result = statement.executeQuery()) { Map playerPermissions = new HashMap<>(); diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java new file mode 100644 index 00000000..1459adf7 --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java @@ -0,0 +1,254 @@ +package com.cjburkey.claimchunk.data.sqlite; + +import com.cjburkey.claimchunk.chunk.ChunkPlayerPermissions; +import com.cjburkey.claimchunk.chunk.ChunkPos; +import com.cjburkey.claimchunk.chunk.DataChunk; +import com.cjburkey.claimchunk.data.newdata.IClaimChunkDataHandler; +import com.cjburkey.claimchunk.player.FullPlayerData; +import com.cjburkey.claimchunk.player.SimplePlayerData; + +import lombok.Getter; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +/* + * I've actually just decided that we're gonna do it this way: + * - SQLite backing database *file* similar to current MySQL integration (which will + * be removed and automatically converted). + * - Have some intermediary layer that can Respond immediately and asynchronously update database. + */ + +/** + * The SHINY, NEW........data handler that tries to fix the data loss issues by which this project + * has been plagued since its conception. + * + *

I hope this is better :) + * + *

Mutations are written to disk immediately, but data is kept in memory + * + * @since 0.0.25 + */ +public class SqLiteDataHandler implements IClaimChunkDataHandler { + + @Getter private final File claimChunkDb; + private boolean init = false; + private HashMap claimedChunks; + private HashMap joinedPlayers; + private SqLiteWrapper sqLiteWrapper; + + public SqLiteDataHandler(@NotNull File claimChunkDb) { + this.claimChunkDb = claimChunkDb; + } + + @Override + public void init() { + joinedPlayers = new HashMap<>(); + claimedChunks = new HashMap<>(); + sqLiteWrapper = new SqLiteWrapper(claimChunkDb, false); + + init = true; + } + + @Override + public boolean getHasInit() { + return init; + } + + @Override + public void exit() { + sqLiteWrapper.close(); + } + + @Override + public void save() { + // Don't do anything, we save as we go + } + + @Override + public void load() throws Exception { + for (FullPlayerData player : sqLiteWrapper.getAllPlayers()) { + joinedPlayers.putIfAbsent(player.player, player); + } + for (DataChunk chunk : sqLiteWrapper.getAllChunks()) { + claimedChunks.putIfAbsent(chunk.chunk, chunk); + } + } + + @Override + public void addClaimedChunk(ChunkPos pos, UUID player) { + DataChunk chunk = new DataChunk(pos, player, new HashMap<>(), false); + claimedChunks.put(pos, chunk); + sqLiteWrapper.addClaimedChunk(chunk); + } + + @Override + public void addClaimedChunks(DataChunk[] chunks) { + Arrays.stream(chunks).forEach(chunk -> addClaimedChunk(chunk.chunk, chunk.player)); + } + + @Override + public void removeClaimedChunk(ChunkPos pos) { + claimedChunks.remove(pos); + sqLiteWrapper.removeClaimedChunk(pos); + } + + @Override + public boolean isChunkClaimed(ChunkPos pos) { + return claimedChunks.containsKey(pos); + } + + @Override + public @Nullable UUID getChunkOwner(ChunkPos pos) { + DataChunk chunk = claimedChunks.get(pos); + return chunk == null ? null : chunk.player; + } + + @Override + public DataChunk[] getClaimedChunks() { + return claimedChunks.values().toArray(new DataChunk[0]); + } + + @Override + public void addPlayer(FullPlayerData playerData) { + joinedPlayers.put(playerData.player, playerData); + sqLiteWrapper.addPlayer(playerData); + } + + @Override + public void addPlayer( + UUID player, + String lastIgn, + @Nullable String chunkName, + long lastOnlineTime, + boolean alerts, + int extraMaxClaims) { + addPlayer( + new FullPlayerData( + player, lastIgn, chunkName, lastOnlineTime, alerts, extraMaxClaims)); + } + + @Override + public void addPlayers(FullPlayerData[] players) { + // this::addPlayer calls SQLite mutation + Arrays.stream(players).forEach(this::addPlayer); + } + + @Override + public @Nullable String getPlayerUsername(UUID player) { + FullPlayerData ply = joinedPlayers.get(player); + return ply == null ? null : ply.lastIgn; + } + + @Override + public @Nullable UUID getPlayerUUID(String username) { + for (FullPlayerData ply : joinedPlayers.values()) { + if (username.equals(ply.lastIgn)) return ply.player; + } + return null; + } + + @Override + public void setPlayerLastOnline(UUID player, long time) { + FullPlayerData ply = joinedPlayers.get(player); + if (ply != null) ply.lastOnlineTime = time; + sqLiteWrapper.setPlayerLastOnline(player, time); + } + + @Override + public void setPlayerChunkName(UUID player, @Nullable String name) { + FullPlayerData ply = joinedPlayers.get(player); + if (ply != null) ply.chunkName = name; + sqLiteWrapper.setPlayerChunkName(player, name); + } + + @Override + public @Nullable String getPlayerChunkName(UUID player) { + FullPlayerData ply = joinedPlayers.get(player); + return ply == null ? null : ply.chunkName; + } + + @Override + public void setPlayerReceiveAlerts(UUID player, boolean receiveAlerts) { + FullPlayerData ply = joinedPlayers.get(player); + if (ply != null) ply.alert = receiveAlerts; + sqLiteWrapper.setPlayerReceiveAlerts(player, receiveAlerts); + } + + @Override + public boolean getPlayerReceiveAlerts(UUID player) { + FullPlayerData ply = joinedPlayers.get(player); + return ply != null && ply.alert; + } + + @Override + public void setPlayerExtraMaxClaims(UUID player, int maxClaims) { + FullPlayerData ply = joinedPlayers.get(player); + if (ply != null) ply.extraMaxClaims = maxClaims; + sqLiteWrapper.setPlayerExtraMaxClaims(player, maxClaims); + } + + @Override + public void addPlayerExtraMaxClaims(UUID player, int numToAdd) { + // This method executes database modification + setPlayerExtraMaxClaims(player, getPlayerExtraMaxClaims(player) + Math.abs(numToAdd)); + } + + @Override + public void takePlayerExtraMaxClaims(UUID player, int numToTake) { + // This method executes database modification + setPlayerExtraMaxClaims(player, Math.max(0, getPlayerExtraMaxClaims(player) - numToTake)); + } + + @Override + public int getPlayerExtraMaxClaims(UUID player) { + FullPlayerData ply = joinedPlayers.get(player); + if (ply != null) return ply.extraMaxClaims; + return 0; + } + + @Override + public boolean hasPlayer(UUID player) { + return joinedPlayers.containsKey(player); + } + + @Override + public Collection getPlayers() { + return joinedPlayers.values().stream() + .map(FullPlayerData::toSimplePlayer) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public FullPlayerData[] getFullPlayerData() { + return joinedPlayers.values().toArray(new FullPlayerData[0]); + } + + @Override + public void givePlayerAccess( + ChunkPos chunk, UUID accessor, ChunkPlayerPermissions permissions) { + DataChunk chunkData = claimedChunks.get(chunk); + if (chunkData != null) { + chunkData.playerPermissions.put(accessor, permissions); + sqLiteWrapper.setPlayerAccess(chunk, accessor, permissions.permissionFlags); + } + } + + @Override + public void takePlayerAccess(ChunkPos chunk, UUID accessor) { + DataChunk chunkData = claimedChunks.get(chunk); + if (chunkData != null) chunkData.playerPermissions.remove(accessor); + sqLiteWrapper.removePlayerAccess(chunk, accessor); + } + + @Override + public Map getPlayersWithAccess(ChunkPos chunk) { + DataChunk chunkData = claimedChunks.get(chunk); + if (chunkData != null) return chunkData.playerPermissions; + return null; + } +} diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java new file mode 100644 index 00000000..50b7e895 --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java @@ -0,0 +1,84 @@ +package com.cjburkey.claimchunk.data.sqlite; + +import com.zaxxer.q2o.Q2Sql; +import com.zaxxer.q2o.SqlClosure; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +/** This class is responsible for creating, loading, and upgrading the database file. */ +public class SqLiteTableMigrationManager { + + public static void go() { + // Make tables if they don't exist + tryCreateTables(); + + // Call migration check methods here. + } + + private static void tryCreateTables() { + // Create player data table + Q2Sql.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS player_data ( + player_uuid TEXT PRIMARY KEY NOT NULL, + last_ign TEXT NOT NULL, + chunk_name TEXT, + last_online_time INTEGER NOT NULL, + alerts_enabled INTEGER NOT NULL, + extra_max_claims INTEGER NOT NULL + ) STRICT + """); + + // Chunk data table + Q2Sql.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS chunk_data ( + chunk_id INTEGER PRIMARY KEY, + chunk_world TEXT NOT NULL, + chunk_x INTEGER NOT NULL, + chunk_z INTEGER NOT NULL, + owner_uuid TEXT NOT NULL, + + FOREIGN KEY(owner_uuid) REFERENCES player_data(player_uuid) + ) STRICT + """); + + // Granular chunk player permission table + Q2Sql.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS chunk_permissions ( + chunk_id INTEGER NOT NULL, + other_player_uuid TEXT NOT NULL, + permission_bits INTEGER NOT NULL, + + PRIMARY KEY(chunk_id, other_player_uuid) + FOREIGN KEY(chunk_id) REFERENCES chunk_data(chunk_id), + FOREIGN KEY(other_player_uuid) REFERENCES player_data(player_uuid) + ) STRICT + """); + } + + // Use this method to determine if a column exists in a table to perform migrations + @SuppressWarnings("unused") + public static boolean columnExists(String tableName, String columnName) { + return SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=? + """)) { + statement.setString(1, tableName); + statement.setString(2, columnName); + ResultSet resultSet = statement.executeQuery(); + int count = resultSet.next() ? resultSet.getInt(1) : 0; + return count > 0; + } + }); + } + + // Whenever a column is added or moved or transformed or whatever, add a + // method here to perform that transformation and call it in initialize_tables. + +} diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java new file mode 100644 index 00000000..4b0283d6 --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java @@ -0,0 +1,402 @@ +package com.cjburkey.claimchunk.data.sqlite; + +import com.cjburkey.claimchunk.chunk.ChunkPlayerPermissions; +import com.cjburkey.claimchunk.chunk.ChunkPos; +import com.cjburkey.claimchunk.chunk.DataChunk; +import com.cjburkey.claimchunk.player.FullPlayerData; +import com.zaxxer.q2o.*; + +import org.jetbrains.annotations.NotNull; +import org.sqlite.SQLiteDataSource; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public record SqLiteWrapper(File dbFile, boolean usesTransactionManager) implements Closeable { + + private static final String SELECT_CHUNK_ID_SQL = + """ + ( + SELECT chunk_id + FROM chunk_data + WHERE chunk_world=? AND chunk_x=? AND chunk_z=? + ) + """; + private static final Pattern SELECT_CHUNK_ID_SQL_PATTERN = + Pattern.compile(Pattern.quote("%%SELECT_CHUNK_ID_SQL%%")); + + public SqLiteWrapper(@NotNull File dbFile, boolean usesTransactionManager) { + this.dbFile = dbFile; + this.usesTransactionManager = usesTransactionManager; + + try { + //noinspection ResultOfMethodCallIgnored + dbFile.createNewFile(); + } catch (IOException e) { + throw new RuntimeException( + "Failed to create new database file even though it didn't exist!", e); + } + + // Make sure the SQLite driver exists and get it in the classpath + // for the DriverManager to search. + SQLiteDataSource dataSource = new SQLiteDataSource(); + dataSource.setUrl("jdbc:sqlite:" + dbFile); + if (usesTransactionManager) q2o.initializeTxSimple(dataSource); + else q2o.initializeTxNone(dataSource); + + // Initialize the tables and perform any changes to them + SqLiteTableMigrationManager.go(); + } + + @Override + public void close() { + q2o.deinitialize(); + } + + // -- DATABASE INTEGRATIONS! -- // + + public void addClaimedChunk(DataChunk chunk) { + SqlClosure.sqlExecute( + connection -> { + // Make sure the player already exists! + // If there isn't a player with their UUID as a primary key, I'm pretty sure + // inserting into the chunk data would fail. This only really matters during + // loading, as there could be a chance the player is missing, somehow? + try (PreparedStatement statement = + connection.prepareStatement( + """ + INSERT OR IGNORE INTO player_data ( + player_uuid, + last_ign, + chunk_name, + last_online_time, + alerts_enabled, + extra_max_claims + ) VALUES ( + ?, "", NULL, 0, TRUE, 0 + ) + """)) { + statement.setString(1, chunk.player.toString()); + statement.execute(); + } + + // Add the chunk + try (PreparedStatement statement = + connection.prepareStatement( + """ + INSERT INTO chunk_data ( + chunk_world, + chunk_x, + chunk_z, + owner_uuid + ) VALUES ( + ?, ?, ?, ? + ) + """)) { + int next = setChunkPosParams(statement, 1, chunk.chunk); + statement.setString(next, chunk.player.toString()); + statement.execute(); + } + + // Add the player permissions + if (!chunk.playerPermissions.isEmpty()) { + String permsInsertPrefixSql = + """ + INSERT INTO chunk_permissions ( + chunk_id, + other_player_uuid, + permission_bits + ) VALUES + """; + + // Better way to do this? + ArrayList params = new ArrayList<>(); + for (int i = 0; i < chunk.playerPermissions.size(); i++) { + params.add("(%%SELECT_CHUNK_ID_SQL%%, ?, ?)"); + } + String finalSql = + chunkIdQuery(permsInsertPrefixSql + String.join(",", params)); + + try (PreparedStatement statement = connection.prepareStatement(finalSql)) { + int currentParam = 1; + for (Map.Entry entry : + chunk.playerPermissions.entrySet()) { + currentParam = + setChunkPosParams(statement, currentParam, chunk.chunk); + statement.setString(currentParam++, entry.getKey().toString()); + statement.setInt(currentParam++, entry.getValue().permissionFlags); + } + statement.execute(); + } + } + + return null; + }); + } + + public void removeClaimedChunk(ChunkPos chunk) { + SqlClosure.sqlExecute( + connection -> { + // Remove all granted permissions for the chunk + try (PreparedStatement statement = + connection.prepareStatement( + chunkIdQuery( + """ + DELETE FROM chunk_permissions + WHERE chunk_id=%%SELECT_CHUNK_ID_SQL%% + """))) { + setChunkPosParams(statement, 1, chunk); + statement.execute(); + } + + // Remove chunk + try (PreparedStatement statement = + connection.prepareStatement( + """ + DELETE FROM chunk_data + WHERE chunk_world=? AND chunk_x=? AND chunk_z=? + """)) { + setChunkPosParams(statement, 1, chunk); + statement.execute(); + } + + return null; + }); + } + + // The provided player data will replace an existing row + public void addPlayer(FullPlayerData playerData) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + INSERT OR REPLACE INTO player_data ( + player_uuid, + last_ign, + chunk_name, + last_online_time, + alerts_enabled, + extra_max_claims + ) VALUES ( + ?, ?, ?, ?, ?, ? + ) + """)) { + statement.setString(1, playerData.player.toString()); + statement.setString(2, playerData.lastIgn); + statement.setString(3, playerData.chunkName); + statement.setLong(4, playerData.lastOnlineTime); + statement.setBoolean(5, playerData.alert); + statement.setInt(6, playerData.extraMaxClaims); + statement.execute(); + return null; + } + }); + } + + public void setPlayerLastOnline(UUID player, long time) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET last_online_time=? + WHERE player_uuid=? + """)) { + statement.setLong(1, time); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); + } + + public void setPlayerChunkName(UUID player, String chunkName) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET chunk_name=? + WHERE player_uuid=? + """)) { + statement.setString(1, chunkName); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); + } + + public void setPlayerReceiveAlerts(UUID player, boolean receiveAlerts) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET receiveAlerts=? + WHERE player_uuid=? + """)) { + statement.setBoolean(1, receiveAlerts); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); + } + + public void setPlayerExtraMaxClaims(UUID player, int extraMaxClaims) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET extra_max_claims=? + WHERE player_uuid=? + """)) { + statement.setInt(1, extraMaxClaims); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); + } + + public void setPlayerAccess(ChunkPos chunk, UUID accessor, int permissionFlags) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + chunkIdQuery( + """ + INSERT INTO chunk_permissions ( + chunk_id, + other_player_uuid, + permission_bits + ) VALUES ( + %%SELECT_CHUNK_ID_SQL%%, ?, ? + ) + ON CONFLICT(chunk_id, other_player_uuid) DO + UPDATE SET permission_bits=excluded.permission_bits + """))) { + int next = setChunkPosParams(statement, 1, chunk); + statement.setString(next, accessor.toString()); + statement.setInt(next + 1, permissionFlags); + statement.execute(); + } + return null; + }); + } + + public void removePlayerAccess(ChunkPos chunk, UUID accessor) { + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + chunkIdQuery( + """ + DELETE FROM chunk_permissions + WHERE chunk_id=%%SELECT_CHUNK_ID_SQL%% + AND other_player_uuid=? + """))) { + int next = setChunkPosParams(statement, 1, chunk); + statement.setString(next, accessor.toString()); + statement.execute(); + return null; + } + }); + } + + // -- Loading stuff -- // + + public List getAllPlayers() { + return Q2ObjList.fromClause(SqlDataPlayer.class, null).stream() + .map(FullPlayerData::new) + .toList(); + } + + public Collection getAllChunks() { + HashMap> permissions = new HashMap<>(); + HashMap owners = new HashMap<>(); + + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + SELECT chunk_world, chunk_x, chunk_z, owner_uuid, + other_player_uuid, permission_bits + FROM chunk_permissions + RIGHT JOIN chunk_data + ON chunk_permissions.chunk_id=chunk_data.chunk_id + """)) { + ResultSet resultSet = statement.executeQuery(); + while (resultSet.next()) { + String world = resultSet.getString("chunk_world"); + int chunk_x = resultSet.getInt("chunk_x"); + int chunk_z = resultSet.getInt("chunk_z"); + ChunkPos pos = new ChunkPos(world, chunk_x, chunk_z); + + String otherUuid = resultSet.getString("other_player_uuid"); + if (otherUuid != null) { + UUID otherPlayer = UUID.fromString(otherUuid); + ChunkPlayerPermissions chunkPerms = + new ChunkPlayerPermissions( + resultSet.getInt("permission_bits")); + + permissions + .computeIfAbsent(pos, ignoredPos -> new HashMap<>()) + .put(otherPlayer, chunkPerms); + } + + UUID owner = UUID.fromString(resultSet.getString("owner_uuid")); + owners.putIfAbsent(pos, owner); + } + } + + return null; + }); + + for (SqlDataChunk chunk : Q2ObjList.fromClause(SqlDataChunk.class, null)) { + owners.putIfAbsent( + new ChunkPos(chunk.world, chunk.x, chunk.z), UUID.fromString(chunk.uuid)); + } + + return owners.entrySet().stream() + .map( + entry -> + new DataChunk( + entry.getKey(), + entry.getValue(), + permissions.getOrDefault(entry.getKey(), new HashMap<>()), + false)) + .collect(Collectors.toList()); + } + + // -- Queries -- // + + // Returns the index of the next parameter! + private int setChunkPosParams( + PreparedStatement statement, int worldParameterNum, ChunkPos chunkPos) + throws SQLException { + statement.setString(worldParameterNum, chunkPos.world()); + statement.setInt(worldParameterNum + 1, chunkPos.x()); + statement.setInt(worldParameterNum + 2, chunkPos.z()); + return worldParameterNum + 3; + } + + private String chunkIdQuery(String sql) { + return SELECT_CHUNK_ID_SQL_PATTERN.matcher(sql).replaceAll(SELECT_CHUNK_ID_SQL); + } +} diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqlDataChunk.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqlDataChunk.java new file mode 100644 index 00000000..dfb4af3f --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqlDataChunk.java @@ -0,0 +1,19 @@ +package com.cjburkey.claimchunk.data.sqlite; + +import javax.persistence.*; + +@Table(name = "chunk_data") +public class SqlDataChunk { + + @Column(name = "chunk_world") + String world; + + @Column(name = "chunk_x") + int x; + + @Column(name = "chunk_z") + int z; + + @Column(name = "owner_uuid") + String uuid; +} diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqlDataPlayer.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqlDataPlayer.java new file mode 100644 index 00000000..2a3892bf --- /dev/null +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqlDataPlayer.java @@ -0,0 +1,28 @@ +package com.cjburkey.claimchunk.data.sqlite; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.Table; + +@Table(name = "player_data") +public class SqlDataPlayer { + + @Id + @Column(name = "player_uuid") + public String uuid; + + @Column(name = "last_ign") + public String lastIgn; + + @Column(name = "chunk_name") + public String chunkName; + + @Column(name = "last_online_time") + public long lastOnlineTime; + + @Column(name = "alerts_enabled") + public boolean alert; + + @Column(name = "extra_max_claims") + public int extraMaxClaims; +} diff --git a/src/main/java/com/cjburkey/claimchunk/event/PlayerConnectionHandler.java b/src/main/java/com/cjburkey/claimchunk/event/PlayerConnectionHandler.java index 4908ffc5..e740c5e9 100644 --- a/src/main/java/com/cjburkey/claimchunk/event/PlayerConnectionHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/event/PlayerConnectionHandler.java @@ -59,5 +59,8 @@ public void onPlayerJoin(PlayerJoinEvent e) { public void onPlayerLeave(PlayerQuitEvent e) { claimChunk.getAdminOverrideHandler().remove(e.getPlayer().getUniqueId()); AutoClaimHandler.disable(e.getPlayer()); + claimChunk + .getPlayerHandler() + .setLastJoinedTime(e.getPlayer().getUniqueId(), System.currentTimeMillis()); } } diff --git a/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java b/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java index db0ef5ec..4bf8d864 100644 --- a/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/event/WorldProfileEventHandler.java @@ -148,7 +148,7 @@ public void onBlockPlace(BlockPlaceEvent event) { BlockAccess.BlockAccessType.PLACE); } - /** Event handler for when a player right clicks on a block. */ + /** Event handler for when a player right-clicks on a block. */ @EventHandler public void onBlockInteraction(PlayerInteractEvent event) { if (event != null diff --git a/src/main/java/com/cjburkey/claimchunk/layer/PlaceholderInitLayer.java b/src/main/java/com/cjburkey/claimchunk/layer/PlaceholderInitLayer.java index fab9977e..bace4d39 100644 --- a/src/main/java/com/cjburkey/claimchunk/layer/PlaceholderInitLayer.java +++ b/src/main/java/com/cjburkey/claimchunk/layer/PlaceholderInitLayer.java @@ -7,6 +7,7 @@ import lombok.Getter; +@SuppressWarnings("LombokGetterMayBeUsed") public class PlaceholderInitLayer implements IClaimChunkLayer { @Getter private ClaimChunkPlaceholders placeholders; @@ -33,6 +34,7 @@ public boolean onEnable(IClaimChunkPlugin claimChunk) { "An error occurred while trying to enable the PlaceholderAPI expansion for" + " claimchunk placeholders!"); Utils.err("Here is the error for reference:"); + //noinspection CallToPrintStackTrace e.printStackTrace(); } diff --git a/src/main/java/com/cjburkey/claimchunk/player/FullPlayerData.java b/src/main/java/com/cjburkey/claimchunk/player/FullPlayerData.java index 578d2868..27ef0381 100644 --- a/src/main/java/com/cjburkey/claimchunk/player/FullPlayerData.java +++ b/src/main/java/com/cjburkey/claimchunk/player/FullPlayerData.java @@ -1,5 +1,7 @@ package com.cjburkey.claimchunk.player; +import com.cjburkey.claimchunk.data.sqlite.SqlDataPlayer; + import java.util.UUID; public class FullPlayerData implements Cloneable { @@ -26,6 +28,16 @@ public FullPlayerData( this.extraMaxClaims = extraMaxClaims; } + public FullPlayerData(SqlDataPlayer player) { + this( + UUID.fromString(player.uuid), + player.lastIgn, + player.chunkName, + player.lastOnlineTime, + player.alert, + player.extraMaxClaims); + } + private FullPlayerData(FullPlayerData clone) { this( clone.player, diff --git a/src/main/java/com/cjburkey/claimchunk/player/PlayerHandler.java b/src/main/java/com/cjburkey/claimchunk/player/PlayerHandler.java index 64ed899f..5bd77b25 100644 --- a/src/main/java/com/cjburkey/claimchunk/player/PlayerHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/player/PlayerHandler.java @@ -73,7 +73,7 @@ public Map> getAllPlayerPermissions(ChunkPos chunk) { } public void changePermissions(ChunkPos chunk, UUID accessor, Map permissions) { - if (permissions.values().stream().allMatch(v -> v == false)) { + if (permissions.values().stream().noneMatch(v -> v)) { // All permissions are false, so remove the accessor's access entirely dataHandler.takePlayerAccess(chunk, accessor); } else { @@ -127,9 +127,12 @@ public int getMaxClaims(UUID player) { } public void onJoin(Player ply) { - if (!dataHandler.hasPlayer(ply.getUniqueId())) { + UUID uuid = ply.getUniqueId(); + if (dataHandler.hasPlayer(uuid)) { + dataHandler.setPlayerLastOnline(uuid, System.currentTimeMillis()); + } else { dataHandler.addPlayer( - ply.getUniqueId(), + uuid, ply.getName(), claimChunk.getConfigHandler().getDefaultSendAlertsToOwner(), 0); diff --git a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/admin/AdminUnclaimAllCmd.java b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/admin/AdminUnclaimAllCmd.java index 8f9335bc..ffaf2b79 100644 --- a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/admin/AdminUnclaimAllCmd.java +++ b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/admin/AdminUnclaimAllCmd.java @@ -52,8 +52,8 @@ public boolean onCall(@NotNull String cmdUsed, @NotNull CommandSender executor, var claimedChunks = chunkHandler.getClaimedChunks(ply); int unclaimed = 0; for (var chunk : claimedChunks) { - if (allWorlds || player.getWorld().getName().equals(chunk.getWorld())) { - chunkHandler.unclaimChunk(chunk.getWorld(), chunk.getX(), chunk.getZ()); + if (allWorlds || player.getWorld().getName().equals(chunk.world())) { + chunkHandler.unclaimChunk(chunk.world(), chunk.x(), chunk.z()); unclaimed++; } } diff --git a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ListCmd.java b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ListCmd.java index 88aca86e..e945c790 100644 --- a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ListCmd.java +++ b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ListCmd.java @@ -91,8 +91,8 @@ public boolean onCall(@NotNull String cmdUsed, @NotNull CommandSender executor, + claimChunk .getMessages() .claimsChunk - .replace("%%X%%", "" + (chunks[i].getX() << 4)) - .replace("%%Z%%", "" + (chunks[i].getZ() << 4))); + .replace("%%X%%", "" + (chunks[i].x() << 4)) + .replace("%%Z%%", "" + (chunks[i].z() << 4))); } return true; } diff --git a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ShowClaimedCmd.java b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ShowClaimedCmd.java index 9415064d..038612d0 100644 --- a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ShowClaimedCmd.java +++ b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/ShowClaimedCmd.java @@ -69,8 +69,8 @@ public boolean onCall(@NotNull String cmdUsed, @NotNull CommandSender executor, // Create a set of this player's claimed chunks within the given radius HashSet claimedChunks = new HashSet<>(); - for (var x = chunkPos.getX() - radius; x <= chunkPos.getX() + radius; x++) { - for (var z = chunkPos.getZ() - radius; z <= chunkPos.getZ() + radius; z++) { + for (var x = chunkPos.x() - radius; x <= chunkPos.x() + radius; x++) { + for (var z = chunkPos.z() - radius; z <= chunkPos.z() + radius; z++) { if (claimChunk .getChunkHandler() .isOwner(player.getWorld(), x, z, player.getUniqueId())) { diff --git a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/UnclaimAllCmd.java b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/UnclaimAllCmd.java index 7a643ed9..2e7edbda 100644 --- a/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/UnclaimAllCmd.java +++ b/src/main/java/com/cjburkey/claimchunk/smartcommand/sub/ply/UnclaimAllCmd.java @@ -42,16 +42,11 @@ public boolean onCall(@NotNull String cmdUsed, @NotNull CommandSender executor, var claimedChunks = chunkHandler.getClaimedChunks(player.getUniqueId()); int unclaimed = 0; for (var chunk : claimedChunks) { - if ((allWorlds || player.getWorld().getName().equals(chunk.getWorld())) + if ((allWorlds || player.getWorld().getName().equals(chunk.world())) && claimChunk .getMainHandler() .unclaimChunk( - false, - true, - player, - chunk.getWorld(), - chunk.getX(), - chunk.getZ())) { + false, true, player, chunk.world(), chunk.x(), chunk.z())) { unclaimed++; } } diff --git a/src/main/java/com/cjburkey/claimchunk/update/SemVer.java b/src/main/java/com/cjburkey/claimchunk/update/SemVer.java index ac5196d9..050427b8 100644 --- a/src/main/java/com/cjburkey/claimchunk/update/SemVer.java +++ b/src/main/java/com/cjburkey/claimchunk/update/SemVer.java @@ -1,6 +1,6 @@ package com.cjburkey.claimchunk.update; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -8,28 +8,10 @@ * A nice little semantic versioning class. Feel free to use this in your own projects if you want * :) */ -public class SemVer implements Comparable { +public record SemVer(int major, int minor, int patch, String marker) implements Comparable { - @SuppressWarnings("WeakerAccess") - public final int major; - - @SuppressWarnings("WeakerAccess") - public final int minor; - - @SuppressWarnings("WeakerAccess") - public final int patch; - - @SuppressWarnings("WeakerAccess") - public final String marker; - - private SemVer(int major, int minor, int patch, @Nullable String marker) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.marker = ((marker == null) ? null : marker.toUpperCase()); - } - - public static SemVer fromString(final String version) { + public static @NotNull SemVer fromString(@NotNull String version) + throws IllegalArgumentException { try { final String[] split = version.trim().split("\\."); if (split.length != 3) { @@ -43,10 +25,11 @@ public static SemVer fromString(final String version) { final int major = Integer.parseInt(split[0].trim()); final int minor = Integer.parseInt(split[1].trim()); final int patch = Integer.parseInt(patchMarker[0].trim()); - final String marker = ((patchMarker.length == 2) ? patchMarker[1].trim() : null); + final String marker = + ((patchMarker.length == 2) ? patchMarker[1].trim().toUpperCase() : null); return new SemVer(major, minor, patch, marker); } catch (Exception e) { - throw INVALID_SEMVER(version); + throw INVALID_SEMVER(version, e); } } @@ -54,6 +37,10 @@ private static IllegalArgumentException INVALID_SEMVER(String input) { return new IllegalArgumentException("Invalid SemVer format: " + input); } + private static IllegalArgumentException INVALID_SEMVER(String input, Throwable cause) { + return new IllegalArgumentException("Invalid SemVer format: " + input, cause); + } + @Override public int compareTo(SemVer o) { if (o.major > major) return -1; diff --git a/src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java b/src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java new file mode 100644 index 00000000..6162204b --- /dev/null +++ b/src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java @@ -0,0 +1,187 @@ +package com.cjburkey.claimchunk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.cjburkey.claimchunk.chunk.ChunkPlayerPermissions; +import com.cjburkey.claimchunk.chunk.ChunkPos; +import com.cjburkey.claimchunk.chunk.DataChunk; +import com.cjburkey.claimchunk.data.sqlite.SqLiteTableMigrationManager; +import com.cjburkey.claimchunk.data.sqlite.SqLiteWrapper; +import com.cjburkey.claimchunk.player.FullPlayerData; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +class TestSQLPlease { + + @Test + void ensureColumnExistsMethodWorks() { + // Must create the wrapper to initialize (and deinitialize) connection + try (TestQlWrap ignoredWrapper = new TestQlWrap()) { + // Make sure that instantiating SqLiteWrapper created the tables + assert SqLiteTableMigrationManager.columnExists("player_data", "player_uuid"); + assert SqLiteTableMigrationManager.columnExists("chunk_data", "owner_uuid"); + assert SqLiteTableMigrationManager.columnExists("chunk_permissions", "permission_bits"); + assert !SqLiteTableMigrationManager.columnExists("chunk_hell", "permission_bits"); + assert !SqLiteTableMigrationManager.columnExists("player_data", "fake_col"); + } + } + + @Test + void ensureNoDataLoss() { + try (TestQlWrap wrapper = new TestQlWrap()) { + // Add a random player + UUID ply1Uuid = UUID.randomUUID(); + UUID ply2Uuid = UUID.randomUUID(); + wrapper.sql.addPlayer( + new FullPlayerData( + ply1Uuid, "SomeGuysName", null, System.currentTimeMillis(), true, 0)); + wrapper.sql.addPlayer( + new FullPlayerData( + ply2Uuid, + "OtherPersonsName", + "queenshit", + System.currentTimeMillis(), + false, + 0)); + + // Make fake accessors and permissions + UUID accessorUuid1 = UUID.randomUUID(); + UUID accessorUuid2 = UUID.randomUUID(); + ChunkPlayerPermissions permissions1 = new ChunkPlayerPermissions(0b11111111); + ChunkPlayerPermissions permissions2 = new ChunkPlayerPermissions(0b10101101); + + // Add a chunk to the player and give the permissions to the other players + ChunkPos chunkPos = new ChunkPos("world", 10, -3); + DataChunk chunkData = new DataChunk(chunkPos, ply1Uuid, new HashMap<>(), false); + chunkData.playerPermissions.put(accessorUuid1, permissions1); + chunkData.playerPermissions.put(accessorUuid2, permissions2); + wrapper.sql.addClaimedChunk(chunkData); + + // Make sure both players get loaded + Collection players = wrapper.sql.getAllPlayers(); + assertEquals(2, players.size()); + assert players.stream() + .allMatch(ply -> ply.player.equals(ply1Uuid) || ply.player.equals(ply2Uuid)); + assert players.stream().anyMatch(ply -> "queenshit".equals(ply.chunkName)); + + // Load the chunk after adding it + Collection loadedChunks = wrapper.sql.getAllChunks(); + DataChunk loadedChunk = loadedChunks.iterator().next(); + assertNotNull(loadedChunk); + + // Make sure the chunk exists when we load from the database + assert loadedChunk.player.equals(ply1Uuid) && loadedChunk.chunk.equals(chunkPos); + // Make sure the chunk permission got loaded correctly + assertEquals(permissions1, loadedChunk.playerPermissions.get(accessorUuid1)); + assertEquals(permissions2, loadedChunk.playerPermissions.get(accessorUuid2)); + } + } + + @Test + void multiplePermissions() { + try (TestQlWrap wrapper = new TestQlWrap()) { + UUID owner = UUID.randomUUID(); + UUID accessor1 = UUID.randomUUID(); + UUID accessor2 = UUID.randomUUID(); + ChunkPos chunk = new ChunkPos("world", 824, -29); + DataChunk chunkData = new DataChunk(chunk, owner, new HashMap<>(), false); + chunkData.playerPermissions.put(accessor1, new ChunkPlayerPermissions(0b01)); + chunkData.playerPermissions.put(accessor2, new ChunkPlayerPermissions(0b10)); + + // Add the players + wrapper.sql.addPlayer( + new FullPlayerData( + owner, "PersonHere", null, System.currentTimeMillis(), true, 0)); + wrapper.sql.addPlayer( + new FullPlayerData( + accessor1, "PersonThere", null, System.currentTimeMillis(), true, 0)); + wrapper.sql.addPlayer( + new FullPlayerData( + accessor2, "AnotherOne", null, System.currentTimeMillis(), true, 0)); + + // Add the chunk + wrapper.sql.addClaimedChunk(chunkData); + + // Load the chunk and make sure it contains both accessors + Map loadedPerms = + wrapper.sql.getAllChunks().iterator().next().playerPermissions; + assert loadedPerms.containsKey(accessor1); + assert loadedPerms.containsKey(accessor2); + } + } + + @Test + void insertOrUpdatePermission() { + try (TestQlWrap wrapper = new TestQlWrap()) { + UUID owner = UUID.randomUUID(); + UUID accessor = UUID.randomUUID(); + ChunkPos chunk = new ChunkPos("world", 824, -29); + int flags1 = 0b10101001; + int flags2 = 0b01010100; + + // Add the players and the chunk + wrapper.sql.addPlayer( + new FullPlayerData( + owner, "PersonHere", null, System.currentTimeMillis(), true, 0)); + wrapper.sql.addPlayer( + new FullPlayerData( + accessor, "PersonThere", null, System.currentTimeMillis(), true, 0)); + wrapper.sql.addClaimedChunk(new DataChunk(chunk, owner, new HashMap<>(), false)); + + // Insert the permission and check it + wrapper.sql.setPlayerAccess(chunk, accessor, flags1); + assertEquals( + flags1, + wrapper.sql + .getAllChunks() + .iterator() + .next() + .playerPermissions + .get(accessor) + .permissionFlags); + + // Update the permission and check it + wrapper.sql.setPlayerAccess(chunk, accessor, flags2); + assertEquals( + flags2, + wrapper.sql + .getAllChunks() + .iterator() + .next() + .playerPermissions + .get(accessor) + .permissionFlags); + + // Remove the permission and make sure there aren't any permissions now + wrapper.sql.removePlayerAccess(chunk, accessor); + assert wrapper.sql.getAllChunks().iterator().next().playerPermissions.isEmpty(); + } + } + + protected static File randomDbFile() { + return new File(UUID.randomUUID() + ".tmp.sqlite3"); + } + + static class TestQlWrap implements AutoCloseable { + SqLiteWrapper sql; + File dbFile; + + TestQlWrap() { + dbFile = randomDbFile(); + sql = new SqLiteWrapper(dbFile, false); + dbFile.deleteOnExit(); + } + + @Override + public void close() { + sql.close(); + } + } +}