diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 139b751..498502a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,13 +1 @@ -# These are supported funding model platforms - -github: [DAQEM] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: daqem # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: daqem # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +ko_fi: daqem \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cd483ea..af11bce 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,8 @@ jobs: env: CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }} MODRINTH_API_KEY: ${{ secrets.MODRINTH_API_KEY }} + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASS: ${{ secrets.MAVEN_PASS }} steps: - name: Checkout @@ -26,8 +28,15 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Grant Permissions to gradlew - run: chmod +x gradlew + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Grant Permissions + run: | + chmod +x gradlew + chmod +x relocate_natives/prepare.sh - name: Build Jars run: ./gradlew build @@ -37,16 +46,19 @@ jobs: run: | echo "::set-output name=VERSION_NAME::$(./gradlew -q :common:printVersionName)" - - name: Check if tag exists + - name: Check if tag exists uses: mukunku/tag-exists-action@v1.5.0 id: checkTag - with: + with: tag: ${{ steps.gradle_version.outputs.VERSION_NAME }} - name: Fail if tag exists if: steps.checkTag.outputs.exists == 'true' run: exit 1 + - name: Publish to DAQEM Maven + run: ./gradlew publish + - name: Publish to Modrinth run: ./gradlew modrinth @@ -60,4 +72,4 @@ jobs: fabric/build/libs/*.jar forge/build/libs/*.jar name: ${{ steps.gradle_version.outputs.VERSION_NAME }} - tag: ${{ steps.gradle_version.outputs.VERSION_NAME }} + tag: ${{ steps.gradle_version.outputs.VERSION_NAME }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d215d25..acbecba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,10 +26,10 @@ jobs: with: java-version: '17' distribution: 'temurin' - + - name: Grant Permissions to gradlew run: chmod +x gradlew - + - name: Test run: ./gradlew :common:test diff --git a/.gitignore b/.gitignore index be2a7c2..a46a463 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ run/ out/ *.iml .gradle/ +.gradle-cache/ output/ bin/ libs/ @@ -17,4 +18,8 @@ classes/ .vscode .settings *.launch -.architectury-transformer \ No newline at end of file + +relocate_natives/.venv/ +relocate_natives/__pycache__/ +relocate_natives/apple-codesign/ +relocate_natives/cache/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 24cca49..98e209d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,61 +1,76 @@ plugins { - id "architectury-plugin" version "3.4-SNAPSHOT" - id "dev.architectury.loom" version "1.3-SNAPSHOT" apply false + id 'dev.architectury.loom' version '1.10-SNAPSHOT' apply false + id 'architectury-plugin' version '3.4-SNAPSHOT' + id 'com.github.johnrengelman.shadow' apply false } architectury { - minecraft = rootProject.minecraft_version + minecraft = project.minecraft_version } -subprojects { - apply plugin: "dev.architectury.loom" - - loom { - silentMojangMappingsLicense() - } +allprojects { + group = rootProject.maven_group + version = rootProject.mod_version - dependencies { - minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" - // The following line declares the mojmap mappings, you may use other mappings as well - mappings loom.officialMojangMappings() - // The following line declares the yarn mappings you may select this one as well. - // mappings "net.fabricmc:yarn:1.20.1+build.10:v2" + task printVersionName { + doLast { + println version + } } } -allprojects { - apply plugin: "java" - apply plugin: "architectury-plugin" - apply plugin: "maven-publish" +subprojects { + apply plugin: 'dev.architectury.loom' + apply plugin: 'architectury-plugin' + apply plugin: 'maven-publish' base { archivesName = rootProject.archives_base_name } - version = rootProject.mod_version - group = rootProject.maven_group - repositories { - maven { - url "https://cursemaven.com" - content { + exclusiveContent { + forRepository { + maven { + name = "Curseforge" + url = "https://cursemaven.com" + } + } + filter { includeGroup "curse.maven" } } + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" + } + } + maven { url "https://maven.daqem.com/snapshots/" } + maven { url "https://maven.daqem.com/releases" } } - tasks.withType(JavaCompile) { - options.encoding = "UTF-8" - options.release = 17 + dependencies { + minecraft "net.minecraft:minecraft:$rootProject.minecraft_version" + mappings loom.officialMojangMappings() } java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } - task printVersionName { - doLast { - println version - } + tasks.withType(JavaCompile).configureEach { + it.options.release = 17 } } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..7d77eef --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java-library' +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + // Shadow plugin + implementation 'com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1' + + // Ant (Required for Shadow) + implementation 'org.apache.ant:ant:1.10.14' + + // Force latest ASM (Fixes "Unsupported class file major version 65") + implementation 'org.ow2.asm:asm:9.7' + implementation 'org.ow2.asm:asm-commons:9.7' + implementation 'org.ow2.asm:asm-util:9.7' + implementation 'org.ow2.asm:asm-tree:9.7' + implementation 'org.ow2.asm:asm-analysis:9.7' +} \ No newline at end of file diff --git a/buildSrc/src/main/java/NativeRelocator.java b/buildSrc/src/main/java/NativeRelocator.java new file mode 100644 index 0000000..d406952 --- /dev/null +++ b/buildSrc/src/main/java/NativeRelocator.java @@ -0,0 +1,214 @@ +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +class NativeRelocator +{ + private final Path rootDirectory; + private final Path cacheRoot; + + /** + * Initializes the NativeRelocator by preparing the environment if necessary. + * Executes the appropriate preparation script based on the OS. + */ + NativeRelocator(Path rootDirectory) + { + this.rootDirectory = rootDirectory; + this.cacheRoot = this.rootDirectory.resolve("cache"); + } + + private void prepare() throws Exception + { + if (this.rootDirectory.resolve(".venv").toFile().exists()) + { + return; + } + + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.directory(this.rootDirectory.toFile()); + + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) + { + processBuilder.command("powershell", "-ExecutionPolicy", "Bypass", "./prepare.ps1"); + } + else if (os.contains("nix") + || os.contains("nux") + || os.contains("mac") + || os.contains("freebsd")) + { + processBuilder.command("./prepare.sh"); + } + else + { + throw new IllegalStateException("Unsupported operating system: " + os); + } + + Process process = processBuilder.start(); + CompletableFuture outputFuture = readOutputStreams(process); + + int exitCode = process.waitFor(); + outputFuture.get(); + + if (exitCode != 0) + { + throw new Exception("Prepare failed: " + exitCode); + } + } + + /** + * Reads and prints the output and error streams of a process asynchronously. + * + * @param process The process whose streams should be read. + * @return A CompletableFuture that completes once all output has been processed. + */ + private static CompletableFuture readOutputStreams(Process process) + { + return CompletableFuture.runAsync(() -> { + try + { + while (process.isAlive() || process.getInputStream().available() > 0 || process.getErrorStream().available() > 0) + { + if (process.getInputStream().available() > 0) + { + byte[] data = new byte[process.getInputStream().available()]; + //noinspection ResultOfMethodCallIgnored + process.getInputStream().read(data); + System.out.write(data); + } + if (process.getErrorStream().available() > 0) + { + byte[] data = new byte[process.getErrorStream().available()]; + //noinspection ResultOfMethodCallIgnored + process.getErrorStream().read(data); + System.err.write(data); + } + + //noinspection BusyWait + Thread.sleep(100); + } + } + catch (Throwable ignored) + { + } + }); + } + + /** + * Replaces occurrences of a target string in a byte array, ensuring null termination. + * + * @param byteArray The byte array where replacements should occur. + * @param target The string to replace. + * @param replacement The replacement string (must not be longer than the target). + * @throws IllegalArgumentException if the replacement is longer than the target. + */ + private void replaceInNullTerminatedStrings(byte[] byteArray, String target, String replacement) + { + if (target.length() < replacement.length()) + { + throw new IllegalArgumentException("Replacement must be the same length or shorter than the target."); + } + + byte[] targetBytes = target.getBytes(StandardCharsets.US_ASCII); + byte[] replacementBytes = replacement.getBytes(StandardCharsets.US_ASCII); + + byte nullByte = 0; + + for (int endPos = 0; endPos < byteArray.length - targetBytes.length - 1; endPos++) + { + int startPos = endPos; + int targetPos = 0; + while (targetPos < targetBytes.length && byteArray[endPos] == targetBytes[targetPos]) + { + targetPos++; + endPos++; + } + + if (targetPos == targetBytes.length) + { + System.arraycopy(replacementBytes, 0, byteArray, startPos, replacementBytes.length); + + startPos = startPos + replacementBytes.length; + while (byteArray[endPos] != nullByte) + { + byteArray[startPos] = byteArray[endPos]; + endPos++; + startPos++; + } + byteArray[startPos] = nullByte; + } + } + } + + /** + * Runs an external script to fix a modified binary and returns the processed content. + * + * @param outputFilePath Path to store the processed binary. + * @param content The original binary content. + * @return The modified binary content. + * @throws Exception if the process execution fails. + */ + public byte[] fixModifiedBinary(Path outputFilePath, byte[] content) throws Exception + { + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.directory(this.rootDirectory.toFile()); + + processBuilder.command( + this.rootDirectory.resolve(".venv/Scripts").toFile().exists() + ? this.rootDirectory.resolve(".venv/Scripts/python.exe").toString() + : this.rootDirectory.resolve(".venv/bin/python").toString(), + "./fix_modified_binary.py", + outputFilePath.toString() + ); + + Process process = processBuilder.start(); + CompletableFuture outputFuture = readOutputStreams(process); + + process.getOutputStream().write(content); + process.getOutputStream().close(); + + int exitCode = process.waitFor(); + outputFuture.get(); + + if (exitCode != 0) + { + throw new Exception("Process failed: " + exitCode); + } + + return Files.readAllBytes(outputFilePath); + } + + /** + * Processes a binary file, applying string replacements and fixing modifications. + * + * @param outputPath The output file path relative to the cache directory. + * @param content The binary content to process. + * @param replacements A map of string replacements to apply. + * @return The modified binary content. + * @throws Exception if processing fails. + */ + public byte[] processBinary(String outputPath, byte[] content, Map replacements) throws Exception + { + Path outputFilePath = this.cacheRoot.resolve(outputPath); + //noinspection ResultOfMethodCallIgnored + outputFilePath.getParent().toFile().mkdirs(); + + if (outputFilePath.toFile().exists()) + { + return Files.readAllBytes(outputFilePath); + } + + System.out.println("Relocating to " + outputPath + "..."); + this.prepare(); + + for (Map.Entry replacement : replacements.entrySet()) + { + this.replaceInNullTerminatedStrings(content, replacement.getKey(), replacement.getValue()); + } + + return this.fixModifiedBinary(outputFilePath, content); + } + +} diff --git a/buildSrc/src/main/java/NativeTransformer.java b/buildSrc/src/main/java/NativeTransformer.java new file mode 100644 index 0000000..8f58e28 --- /dev/null +++ b/buildSrc/src/main/java/NativeTransformer.java @@ -0,0 +1,91 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer; +import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipOutputStream; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileTreeElement; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class NativeTransformer implements Transformer { + private final Map relocations = new HashMap<>(); + private final HashMap rewrittenFiles = new HashMap<>(); + private NativeRelocator relocator; + private java.io.File rootDir; + + @Override + public @NotNull String getName() { + return "NativeTransformer"; + } + + @Override + public boolean canTransformResource(@Nonnull FileTreeElement element) { + return relocations.keySet().stream() + .anyMatch(key -> element.getName().startsWith(key)); + } + + @Override + public void transform(@Nonnull TransformerContext context) { + byte[] content; + try { + content = context.getIs().readAllBytes(); + } catch (IOException e) { + throw new GradleException("Failed to read resource content", e); + } + + // Lazy initialization of nativeRelocator + if (relocator == null) { + relocator = new NativeRelocator(rootDir.toPath().resolve("relocate_natives")); + } + + try { + // Find the first matching path prefix replacement + Map.Entry pathReplacement = relocations.entrySet().stream() + .filter(entry -> context.getPath().startsWith(entry.getKey())) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("No matching replacement found for path: " + context.getPath())); + + // Apply the path replacement + String newPath = context.getPath().replace(pathReplacement.getKey(), pathReplacement.getValue()); + + // Process the binary with the relocator + content = relocator.processBinary(newPath, content, relocations); + + // Store the rewritten file + rewrittenFiles.put(newPath, content); + + } catch (Throwable e) { + throw new GradleException("Failed to relocate native library: " + context.getPath(), e); + } + } + + @Override + public boolean hasTransformedResource() { + return !rewrittenFiles.isEmpty(); + } + + @Override + public void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { + for (Map.Entry rewrittenFile : rewrittenFiles.entrySet()) { + try { + os.putNextEntry(new ZipEntry(rewrittenFile.getKey())); + os.write(rewrittenFile.getValue()); + } catch (IOException e) { + throw new GradleException("Failed to write relocated native library: " + rewrittenFile.getKey(), e); + } + } + } + + // Gradle DSL helper methods + public void relocateNative(String pattern, String replacement) { + this.relocations.put(pattern, replacement); + } + + public void setRootDir(java.io.File rootDir) { + this.rootDir = rootDir; + } +} \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle index cdeffea..ec654cf 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -13,19 +13,27 @@ dependencies { // Remove the next line if you don't want to depend on the API modApi "dev.architectury:architectury:${rootProject.architectury_version}" - compileOnlyApi "curse.maven:supermartijn642s-config-lib-438332:${project.config_library_file_fabric}" + implementation(("io.github.llamalad7:mixinextras-common:${rootProject.mixin_extras_version}")) + + compileOnlyApi "maven.modrinth:supermartijn642s-config-lib:${rootProject.config_library_version_fabric}" } publishing { publications { - mavenCommon(MavenPublication) { - artifactId = rootProject.archives_base_name + mavenFabric(MavenPublication) { + groupId = rootProject.maven_group + artifactId = rootProject.archives_base_name + "-common" from components.java } } - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. repositories { - // Add repositories to publish to here. + maven { + url = project.version.contains('-PR') ? 'https://maven.daqem.com/snapshots' : 'https://maven.daqem.com/releases' + credentials { + username = System.getenv("MAVEN_USER") + password = System.getenv("MAVEN_PASS") + } + } } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/GriefLogger.java b/common/src/main/java/com/daqem/grieflogger/GriefLogger.java index 106a364..a40553e 100644 --- a/common/src/main/java/com/daqem/grieflogger/GriefLogger.java +++ b/common/src/main/java/com/daqem/grieflogger/GriefLogger.java @@ -1,19 +1,28 @@ package com.daqem.grieflogger; +import com.daqem.grieflogger.event.*; +import com.daqem.grieflogger.thread.ThreadManager; +import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; + import com.daqem.grieflogger.config.GriefLoggerConfig; import com.daqem.grieflogger.database.Database; -import com.daqem.grieflogger.database.service.*; -import com.daqem.grieflogger.event.*; +import com.daqem.grieflogger.database.service.Services; import com.daqem.grieflogger.event.block.BlockEvents; import com.daqem.grieflogger.event.item.ItemEvents; -import com.daqem.grieflogger.thread.ThreadManager; +import com.daqem.grieflogger.i18n.LanguageManager; import com.mojang.logging.LogUtils; + import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.contents.TranslatableContents; -import net.minecraft.resources.ResourceLocation; -import org.slf4j.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class GriefLogger { public static final String MOD_ID = "grieflogger"; @@ -27,6 +36,7 @@ public static void init() { return; } registerEvents(); + startHeartbeat(); } private static void initConfigs() { @@ -46,6 +56,8 @@ private static void registerEvents() { ChatEvent.registerEvent(); CommandEvent.registerEvent(); + ServerStartedEvent.registerEvent(); + ServerStoppedEvent.registerEvent(); } private static boolean prepareDatabase() { @@ -93,20 +105,85 @@ public static Database getDatabase() { return DATABASE; } + private static void startHeartbeat() { + int frequencyTicks = GriefLoggerConfig.helloFrequency.get(); + long seconds = frequencyTicks / 20; + if (seconds < 1) seconds = 1; + + ThreadManager.scheduleAtFixedRate(() -> { + Database database = getDatabase(); + if (database != null) { + database.queue.hello(); + database.queue.execute(); + database.batchQueue.execute(); + } + }, seconds, seconds, TimeUnit.SECONDS); + } + public static MutableComponent translate(String str) { - MutableComponent component = translate(str, TranslatableContents.NO_ARGS); - if (GriefLoggerConfig.serverSideOnlyMode.get()) { - component = Component.literal(component.getString()).withStyle(component.getStyle()); - } - return component; + return translate(str, TranslatableContents.NO_ARGS); } public static MutableComponent translate(String str, Object... args) { - MutableComponent component = Component.translatable(MOD_ID + "." + str, args); - if (GriefLoggerConfig.serverSideOnlyMode.get()) { - component = Component.literal(component.getString()).withStyle(component.getStyle()); + String translated = LanguageManager.getString(MOD_ID + "." + str); + try { + if (args.length == 0) { + return Component.literal(translated); + } + + boolean hasComponentArg = false; + for (Object arg : args) { + if (arg instanceof Component) { + hasComponentArg = true; + break; + } + } + + if (!hasComponentArg) { + return Component.literal(String.format(translated, args)); + } + + Object[] newArgs = new Object[args.length]; + Map componentMap = new HashMap<>(); + + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof Component component) { + String marker = "§_GL_COMP_" + i + "_§"; + componentMap.put(marker, component); + newArgs[i] = marker; + } else { + newArgs[i] = args[i]; + } + } + + String formatted = String.format(translated, newArgs); + MutableComponent root = Component.literal(""); + Matcher matcher = Pattern.compile("§_GL_COMP_(\\d+)_§").matcher(formatted); + int lastEnd = 0; + + while (matcher.find()) { + String textBefore = formatted.substring(lastEnd, matcher.start()); + if (!textBefore.isEmpty()) { + root.append(Component.literal(textBefore)); + } + String marker = matcher.group(); + Component component = componentMap.get(marker); + if (component != null) { + root.append(component); + } + lastEnd = matcher.end(); + } + + String textAfter = formatted.substring(lastEnd); + if (!textAfter.isEmpty()) { + root.append(Component.literal(textAfter)); + } + + return root; + + } catch (Exception e) { + return Component.literal(translated); } - return component; } public static MutableComponent literal(String str) { @@ -114,35 +191,19 @@ public static MutableComponent literal(String str) { } public static MutableComponent themedTranslate(String str) { - MutableComponent component = themedTranslate(str, TranslatableContents.NO_ARGS); - if (GriefLoggerConfig.serverSideOnlyMode.get()) { - component = Component.literal(component.getString()).withStyle(component.getStyle()); - } - return component; + return themedTranslate(str, TranslatableContents.NO_ARGS); } public static MutableComponent themedTranslate(String str, Object... args) { - MutableComponent component = Component.translatable(MOD_ID + "." + str, args).withStyle(getTheme()); - if (GriefLoggerConfig.serverSideOnlyMode.get()) { - component = Component.literal(component.getString()).withStyle(component.getStyle()); - } - return component; + return translate(str, args).withStyle(getTheme()); } public static MutableComponent themedLiteral(String str) { - MutableComponent component = Component.literal(str).withStyle(getTheme()); - if (GriefLoggerConfig.serverSideOnlyMode.get()) { - component = Component.literal(component.getString()).withStyle(component.getStyle()); - } - return component; + return Component.literal(str).withStyle(getTheme()); } public static Component getName() { - Component component = translate("name").withStyle(getTheme()); - if (GriefLoggerConfig.serverSideOnlyMode.get()) { - component = Component.literal(component.getString()).withStyle(component.getStyle()); - } - return component; + return translate("name").withStyle(getTheme()); } public static Style getTheme() { diff --git a/common/src/main/java/com/daqem/grieflogger/GriefLoggerExpectPlatform.java b/common/src/main/java/com/daqem/grieflogger/GriefLoggerExpectPlatform.java deleted file mode 100644 index 7acb4c3..0000000 --- a/common/src/main/java/com/daqem/grieflogger/GriefLoggerExpectPlatform.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.daqem.grieflogger; - -import dev.architectury.injectables.annotations.ExpectPlatform; -import dev.architectury.platform.Platform; - -import java.nio.file.Path; - -public class GriefLoggerExpectPlatform { - - @ExpectPlatform - public static Path getConfigDirectory() { - throw new AssertionError(); - } -} diff --git a/common/src/main/java/com/daqem/grieflogger/GriefLoggerPermissions.java b/common/src/main/java/com/daqem/grieflogger/GriefLoggerPermissions.java new file mode 100644 index 0000000..6147634 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/GriefLoggerPermissions.java @@ -0,0 +1,21 @@ +package com.daqem.grieflogger; + +import dev.architectury.injectables.annotations.ExpectPlatform; +import net.minecraft.commands.CommandSourceStack; + +public class GriefLoggerPermissions { + + /** + * Checks permissions using the platform's specific API (LuckPerms/PermissionsAPI) + * if available. Otherwise, falls back to the standard OP level check. + * + * @param source The command source. + * @param permissionNode The string permission node (e.g., "grieflogger.command.inspect"). + * @param fallbackLevel The OP level to require if the permission API is missing (usually 2). + * @return True if the source has permission. + */ + @ExpectPlatform + public static boolean check(CommandSourceStack source, String permissionNode, int fallbackLevel) { + throw new AssertionError(); + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/block/BlockHandler.java b/common/src/main/java/com/daqem/grieflogger/block/BlockHandler.java index e8b0220..abee0e4 100644 --- a/common/src/main/java/com/daqem/grieflogger/block/BlockHandler.java +++ b/common/src/main/java/com/daqem/grieflogger/block/BlockHandler.java @@ -1,58 +1,63 @@ package com.daqem.grieflogger.block; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; -import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Optional; public class BlockHandler { + private static final TagKey C_CHESTS = TagKey.create(Registries.BLOCK, new ResourceLocation("c", "chests")); + private static final TagKey C_WORKBENCHES = TagKey.create(Registries.BLOCK, new ResourceLocation("c", "workbenches")); - public static boolean isBlockIntractable(Block block) { - if (block instanceof FenceGateBlock - || block instanceof DispenserBlock + public static boolean isBlockInteractable(Block block) { + BlockState state = block.defaultBlockState(); + + if (state.is(BlockTags.DOORS) || + state.is(BlockTags.FENCE_GATES) || + state.is(BlockTags.TRAPDOORS) || + state.is(BlockTags.BUTTONS) || + state.is(BlockTags.SIGNS) || + state.is(BlockTags.BEDS)) { + return true; + } + + if (state.is(C_CHESTS) || state.is(C_WORKBENCHES)) { + return true; + } + + if (block instanceof EntityBlock) { + return !(block instanceof BannerBlock); + } + + if (block instanceof LeverBlock || block instanceof NoteBlock - || block instanceof AbstractChestBlock - || block instanceof AbstractFurnaceBlock - || block instanceof LeverBlock - || block instanceof TrapDoorBlock - || block instanceof DoorBlock - || block instanceof BrewingStandBlock || block instanceof DiodeBlock - || block instanceof HopperBlock - || block instanceof DropperBlock - || block instanceof ShulkerBoxBlock - || block instanceof BarrelBlock || block instanceof GrindstoneBlock - || block instanceof ButtonBlock || block instanceof LoomBlock - || block instanceof CraftingTableBlock - || block instanceof CartographyTableBlock - || block instanceof EnchantmentTableBlock - || block instanceof SmithingTableBlock || block instanceof StonecutterBlock - ) { + || block instanceof LecternBlock) { return true; } - return getIntractableBlocks().contains(block.arch$registryName().toString()); + + return getInteractableBlocks().contains(block.arch$registryName().toString()); } - public static List getIntractableBlocks() { + public static List getInteractableBlocks() { //TODO Add config option to add blocks to this list return List.of(); } public static Optional getSecondDoorPosition(BlockPos pos, BlockState state) { - if (state.getBlock() instanceof DoorBlock) { - if (state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER) { - return Optional.of(pos.above()); - } - else if (state.getValue(DoorBlock.HALF) == DoubleBlockHalf.UPPER) { - return Optional.of(pos.below()); - } + if (state.hasProperty(DoorBlock.HALF)) { + DoubleBlockHalf half = state.getValue(DoorBlock.HALF); + return Optional.of(half == DoubleBlockHalf.LOWER ? pos.above() : pos.below()); } return Optional.empty(); } diff --git a/common/src/main/java/com/daqem/grieflogger/command/GriefLoggerCommand.java b/common/src/main/java/com/daqem/grieflogger/command/GriefLoggerCommand.java index dd2d1dc..a98b795 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/GriefLoggerCommand.java +++ b/common/src/main/java/com/daqem/grieflogger/command/GriefLoggerCommand.java @@ -1,5 +1,6 @@ package com.daqem.grieflogger.command; +import com.daqem.grieflogger.GriefLoggerPermissions; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.minecraft.commands.CommandSourceStack; @@ -18,8 +19,9 @@ public static void registerCommand(CommandDispatcher dispatc private static LiteralArgumentBuilder commandWithPrefix(String prefix) { return Commands.literal(prefix) + .requires(source -> GriefLoggerPermissions.check(source, "grieflogger.command", 2)) .then(INSPECT.getCommand()) .then(LOOKUP.getCommand()) .then(PAGE.getCommand()); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/command/InspectCommand.java b/common/src/main/java/com/daqem/grieflogger/command/InspectCommand.java index 12469d1..89c0651 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/InspectCommand.java +++ b/common/src/main/java/com/daqem/grieflogger/command/InspectCommand.java @@ -1,6 +1,7 @@ package com.daqem.grieflogger.command; import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.GriefLoggerPermissions; import com.daqem.grieflogger.player.GriefLoggerServerPlayer; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.minecraft.commands.CommandSourceStack; @@ -8,11 +9,10 @@ public class InspectCommand implements ICommand { - @Override public LiteralArgumentBuilder getCommand() { return Commands.literal("inspect") - .requires(source -> source.hasPermission(2)) + .requires(source -> GriefLoggerPermissions.check(source, "grieflogger.command.inspect", 2)) .executes(context -> inspect(context.getSource())); } @@ -23,4 +23,4 @@ private static int inspect(CommandSourceStack source) { } return 1; } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/command/LookupCommand.java b/common/src/main/java/com/daqem/grieflogger/command/LookupCommand.java index 7555e7a..e91ed72 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/LookupCommand.java +++ b/common/src/main/java/com/daqem/grieflogger/command/LookupCommand.java @@ -1,15 +1,22 @@ package com.daqem.grieflogger.command; import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.GriefLoggerPermissions; import com.daqem.grieflogger.command.argument.FilterArgument; -import com.daqem.grieflogger.command.filter.*; +import com.daqem.grieflogger.command.filter.FilterList; +import com.daqem.grieflogger.command.filter.IFilter; import com.daqem.grieflogger.command.page.Page; import com.daqem.grieflogger.database.service.Services; -import com.daqem.grieflogger.model.history.*; +import com.daqem.grieflogger.model.history.IHistory; import com.daqem.grieflogger.player.GriefLoggerServerPlayer; import com.daqem.grieflogger.thread.ThreadManager; +import com.mojang.brigadier.StringReader; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.server.level.ServerPlayer; @@ -17,6 +24,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; public class LookupCommand implements ICommand { @@ -24,25 +32,34 @@ public class LookupCommand implements ICommand { @Override public LiteralArgumentBuilder getCommand() { return Commands.literal("lookup") - .requires(source -> source.hasPermission(2)) - .then(Commands.argument("filter1", StringArgumentType.string()) - .suggests((context, builder) -> new FilterArgument().listSuggestions(context, builder)) - .then(Commands.argument("filter2", StringArgumentType.string()) - .suggests((context, builder) -> new FilterArgument().listSuggestions(context, builder)) - .then(Commands.argument("filter3", StringArgumentType.string()) - .suggests((context, builder) -> new FilterArgument().listSuggestions(context, builder)) - .then(Commands.argument("filter4", StringArgumentType.string()) - .suggests((context, builder) -> new FilterArgument().listSuggestions(context, builder)) - .then(Commands.argument("filter5", StringArgumentType.string()) - .suggests((context, builder) -> new FilterArgument().listSuggestions(context, builder)) - .executes(context -> lookup(context.getSource(), new FilterList(List.of(FilterArgument.getFilter(context, "filter1"), FilterArgument.getFilter(context, "filter2"), FilterArgument.getFilter(context, "filter3"), FilterArgument.getFilter(context, "filter4"), FilterArgument.getFilter(context, "filter5")), context.getSource())))) - .executes(context -> lookup(context.getSource(), new FilterList(List.of(FilterArgument.getFilter(context, "filter1"), FilterArgument.getFilter(context, "filter2"), FilterArgument.getFilter(context, "filter3"), FilterArgument.getFilter(context, "filter4")), context.getSource())))) - .executes(context -> lookup(context.getSource(), new FilterList(List.of(FilterArgument.getFilter(context, "filter1"), FilterArgument.getFilter(context, "filter2"), FilterArgument.getFilter(context, "filter3")), context.getSource())))) - .executes(context -> lookup(context.getSource(), new FilterList(List.of(FilterArgument.getFilter(context, "filter1"), FilterArgument.getFilter(context, "filter2")), context.getSource())))) - .executes(context -> lookup(context.getSource(), new FilterList(List.of(FilterArgument.getFilter(context, "filter1")), context.getSource())))); + .requires(source -> GriefLoggerPermissions.check(source, "grieflogger.command.lookup", 2)) + .then(Commands.argument("filters", StringArgumentType.greedyString()) + .suggests(LookupCommand::suggestFilters) + .executes(context -> lookup(context.getSource(), StringArgumentType.getString(context, "filters")))) + .executes(context -> lookup(context.getSource(), "")); + } + + private static int lookup(CommandSourceStack source, String filtersInput) { + List filters = new ArrayList<>(); + + if (!filtersInput.isBlank()) { + String[] parts = filtersInput.split("\\s+"); + FilterArgument parser = new FilterArgument(); + + for (String part : parts) { + try { + IFilter filter = parser.parse(new StringReader(part)); + filters.add(filter); + } catch (CommandSyntaxException e) { + source.sendFailure(GriefLogger.translate("lookup.invalid_filter", GriefLogger.getName(), part)); + return 0; + } + } + } + + return lookup(source, new FilterList(filters, source)); } - @SuppressWarnings("SameReturnValue") private static int lookup(CommandSourceStack source, FilterList filterList) { if (source.getPlayer() instanceof GriefLoggerServerPlayer player) { ThreadManager.submit(() -> getHistory(source.getLevel(), filterList), filteredHistory -> { @@ -59,15 +76,22 @@ private static int lookup(CommandSourceStack source, FilterList filterList) { return 1; } + private static CompletableFuture suggestFilters(CommandContext context, SuggestionsBuilder builder) { + String remaining = builder.getRemaining(); + int lastSpace = remaining.lastIndexOf(' '); + int start = builder.getStart() + lastSpace + 1; + SuggestionsBuilder offsetBuilder = builder.createOffset(start); + return new FilterArgument().listSuggestions(context, offsetBuilder); + } + private static List getHistory(Level level, FilterList filterList) { - List filteredSessionHistory = Services.SESSION.getFilteredSessionHistory(level, filterList); - List filteredBlockHistory = Services.BLOCK.getFilteredBlockHistory(level, filterList); - List filteredContainerHistory = Services.CONTAINER.getFilteredContainerHistory(level, filterList); - List filteredItemHistory = Services.ITEM.getFilteredItemHistory(level, filterList); - return new ArrayList<>(List.of(filteredSessionHistory, filteredBlockHistory, filteredContainerHistory, filteredItemHistory)) - .stream() - .flatMap(List::stream) + List history = new ArrayList<>(); + history.addAll(Services.BLOCK.getFilteredBlockHistory(level, filterList)); + history.addAll(Services.SESSION.getFilteredSessionHistory(level, filterList)); + history.addAll(Services.CONTAINER.getFilteredContainerHistory(level, filterList)); + history.addAll(Services.ITEM.getFilteredItemHistory(level, filterList)); + return history.stream() .sorted((x, y) -> Long.compare(y.getTime().time(), x.getTime().time())) .collect(Collectors.toList()); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/command/PageCommand.java b/common/src/main/java/com/daqem/grieflogger/command/PageCommand.java index be938dc..1d20b09 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/PageCommand.java +++ b/common/src/main/java/com/daqem/grieflogger/command/PageCommand.java @@ -1,6 +1,7 @@ package com.daqem.grieflogger.command; import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.GriefLoggerPermissions; import com.daqem.grieflogger.command.page.Page; import com.daqem.grieflogger.player.GriefLoggerServerPlayer; import com.mojang.brigadier.arguments.IntegerArgumentType; @@ -16,7 +17,7 @@ public class PageCommand implements ICommand { @Override public LiteralArgumentBuilder getCommand() { return Commands.literal("page") - .requires(source -> source.hasPermission(2)) + .requires(source -> GriefLoggerPermissions.check(source, "grieflogger.command.page", 2)) .then(Commands.argument("page", IntegerArgumentType.integer()) .executes(context -> page(context.getSource(), IntegerArgumentType.getInteger(context, "page")))); } @@ -33,4 +34,4 @@ private static int page(CommandSourceStack source, int page) { } return 1; } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/ActionFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/ActionFilter.java index b854a47..6007cac 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/ActionFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/ActionFilter.java @@ -24,7 +24,7 @@ public ActionFilter(List actions) { @Override public String getName() { - return GriefLogger.translate("filter.action").getString(); + return "action"; } @Override diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/ExcludeFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/ExcludeFilter.java index 5650cdc..5237513 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/ExcludeFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/ExcludeFilter.java @@ -21,7 +21,7 @@ public ExcludeFilter(List items) { @Override public String getName() { - return GriefLogger.translate("filter.exclude").getString(); + return "exclude"; } @Override diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/IncludeFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/IncludeFilter.java index 578274e..f59a2d9 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/IncludeFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/IncludeFilter.java @@ -24,7 +24,7 @@ public IncludeFilter(List items) { @Override public String getName() { - return GriefLogger.translate("filter.include").getString(); + return "include"; } @Override diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/ItemFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/ItemFilter.java index 06ac8a1..be24d3a 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/ItemFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/ItemFilter.java @@ -28,7 +28,7 @@ public List getOptions() { } protected List getItemsFromSuffix(StringReader reader, String suffix) throws CommandSyntaxException { - String[] split = suffix.split(","); + String[] split = Arrays.stream(suffix.split(",")).map(s -> s.replace("minecraft:", "").trim()).toArray(String[]::new); List items = BuiltInRegistries.ITEM.stream() .filter(item -> Arrays.asList(split).contains(item.arch$registryName().toString().replace("minecraft:", ""))) .toList(); diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/RadiusFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/RadiusFilter.java index 6c9503d..f8ea60f 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/RadiusFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/RadiusFilter.java @@ -25,7 +25,7 @@ public RadiusFilter(int radius) { @Override public String getName() { - return GriefLogger.translate("filter.radius").getString(); + return "radius"; } @Override diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/TimeFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/TimeFilter.java index cb3b127..7d0a515 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/TimeFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/TimeFilter.java @@ -3,6 +3,7 @@ import com.daqem.grieflogger.GriefLogger; import com.daqem.grieflogger.model.TimeUnit; import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import java.util.ArrayList; @@ -23,7 +24,7 @@ public TimeFilter(long time, TimeUnit timeUnit) { @Override public String getName() { - return GriefLogger.translate("filter.time").getString(); + return "time"; } @Override @@ -60,10 +61,14 @@ public String[] listSuggestions(SuggestionsBuilder builder, String prefix, Strin } @Override - public IFilter parse(StringReader reader, String suffix) { - TimeUnit timeUnit = TimeUnit.values()[TimeUnit.getAbbreviations().indexOf(suffix.substring(suffix.length() - 1))]; - int time = Integer.parseInt(suffix.substring(0, suffix.length() - timeUnit.getComponent().getString().length())); - return new TimeFilter(time, timeUnit); + public IFilter parse(StringReader reader, String suffix) throws CommandSyntaxException { + try { + TimeUnit timeUnit = TimeUnit.values()[TimeUnit.getAbbreviations().indexOf(suffix.substring(suffix.length() - 1))]; + int time = Integer.parseInt(suffix.substring(0, suffix.length() - timeUnit.getComponent().getString().length())); + return new TimeFilter(time, timeUnit); + } catch (ArrayIndexOutOfBoundsException e) { + throw new CommandSyntaxException(null, GriefLogger.literal("")); + } } @Override diff --git a/common/src/main/java/com/daqem/grieflogger/command/filter/UserFilter.java b/common/src/main/java/com/daqem/grieflogger/command/filter/UserFilter.java index 4b18849..8db8a99 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/filter/UserFilter.java +++ b/common/src/main/java/com/daqem/grieflogger/command/filter/UserFilter.java @@ -26,7 +26,7 @@ public UserFilter(Map usernames) { @Override public String getName() { - return GriefLogger.translate("filter.user").getString(); + return "user"; } @Override diff --git a/common/src/main/java/com/daqem/grieflogger/command/page/Page.java b/common/src/main/java/com/daqem/grieflogger/command/page/Page.java index 787ec60..fcbfdc0 100644 --- a/common/src/main/java/com/daqem/grieflogger/command/page/Page.java +++ b/common/src/main/java/com/daqem/grieflogger/command/page/Page.java @@ -1,6 +1,7 @@ package com.daqem.grieflogger.command.page; import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.config.GriefLoggerConfig; import com.daqem.grieflogger.model.history.IHistory; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.ClickEvent; @@ -15,7 +16,6 @@ public class Page { - public static final int MAX_PAGE_SIZE = 10; private final List history; private final int page; private final int maxPage; @@ -57,10 +57,14 @@ private Component getHeader() { } private Component getFooter() { - return getArrowLeft().append(" ") - .append(GriefLogger.themedTranslate("lookup.page")).append(" ") - .append(GriefLogger.translate("lookup.pages", page, maxPage).withStyle(ChatFormatting.WHITE)).append(" ") - .append(getArrowRight()); + return Component.empty() + .append(getArrowLeft() + .append(" ") + .append(GriefLogger.themedTranslate("lookup.page")) + .append(" ")) + .append(GriefLogger.translate("lookup.pages", page, maxPage).withStyle(getStyle(page + 1, page < maxPage).withColor(ChatFormatting.WHITE)) + .append(" ") + .append(getArrowRight())); } private MutableComponent getArrowLeft() { @@ -90,11 +94,12 @@ public void sendToPlayer(ServerPlayer serverPlayer) { public static List convertToPages(List history, boolean singleLocation) { List pages = new ArrayList<>(); - int maxPage = (int) Math.ceil((double) history.size() / (double) MAX_PAGE_SIZE); + Integer pageSize = GriefLoggerConfig.maxPageSize.get(); + int maxPage = (int) Math.ceil((double) history.size() / (double) pageSize); int page = 1; - for (int i = 0; i < history.size(); i += MAX_PAGE_SIZE) { + for (int i = 0; i < history.size(); i += pageSize) { int finalPage = page; - pages.add(history.subList(i, Math.min(i + MAX_PAGE_SIZE, history.size())) + pages.add(history.subList(i, Math.min(i + pageSize, history.size())) .stream() .collect(Collectors.collectingAndThen(Collectors.toList(), x -> new Page(x, finalPage, maxPage, singleLocation)))); page++; diff --git a/common/src/main/java/com/daqem/grieflogger/config/GriefLoggerConfig.java b/common/src/main/java/com/daqem/grieflogger/config/GriefLoggerConfig.java index ee504cc..08fb62c 100644 --- a/common/src/main/java/com/daqem/grieflogger/config/GriefLoggerConfig.java +++ b/common/src/main/java/com/daqem/grieflogger/config/GriefLoggerConfig.java @@ -23,7 +23,8 @@ public static void init() { public static final Supplier maxPageSize; - public static final Supplier serverSideOnlyMode; + public enum Language { en_us, nl_nl, zh_tw } + public static final Supplier language; public static final Supplier queueFrequency; public static final Supplier helloFrequency; @@ -46,7 +47,7 @@ public static void init() { config.pop(); config.push("server"); - serverSideOnlyMode = config.comment("Whether to run the mod in server side only mode").onlyOnServer().define("serverSideOnlyMode", true); + language = config.comment("The language to use for translations (en_us, nl_nl, zh_tw)").onlyOnServer().define("language", Language.en_us); config.pop(); config.push("queue"); diff --git a/common/src/main/java/com/daqem/grieflogger/database/Database.java b/common/src/main/java/com/daqem/grieflogger/database/Database.java index a8552a7..1590f74 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/Database.java +++ b/common/src/main/java/com/daqem/grieflogger/database/Database.java @@ -1,24 +1,33 @@ package com.daqem.grieflogger.database; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import com.daqem.grieflogger.database.dialect.MySQLDialect; +import com.supermartijn642.configlib.ConfigLib; +import org.jetbrains.annotations.Nullable; + import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.GriefLoggerExpectPlatform; import com.daqem.grieflogger.config.GriefLoggerConfig; +import com.daqem.grieflogger.database.dialect.IDatabaseDialect; +import com.daqem.grieflogger.database.dialect.SQLiteDialect; import com.daqem.grieflogger.database.queue.IQueue; import com.daqem.grieflogger.database.queue.Queue; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.sql.*; -import java.util.List; +import com.daqem.grieflogger.database.queue.SqlTask; public class Database { @Nullable private Connection connection; - @Nullable - private Statement statement; public final IQueue queue; public final IQueue batchQueue; + private IDatabaseDialect dialect; + private final Object lock = new Object(); public Database() { queue = new Queue(this, false); @@ -29,17 +38,13 @@ public boolean createConnection() { boolean connected; if (GriefLoggerConfig.useMysql.get()) { connected = createMysqlConnection(); + dialect = new MySQLDialect(); } else { connected = createSqliteConnection(); + dialect = new SQLiteDialect(); } if (connection != null) { GriefLogger.LOGGER.info("Connected to database"); - try { - statement = connection.createStatement(); - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to create statement", e); - return false; - } try { connection.setAutoCommit(false); } catch (SQLException e) { @@ -47,7 +52,7 @@ public boolean createConnection() { return false; } } - return connected && connection != null && statement != null; + return connected && connection != null; } public boolean createMysqlConnection() { @@ -80,13 +85,14 @@ public boolean createSqliteConnection() { GriefLogger.LOGGER.error("Failed to load SQLite driver", e); return false; } - Path path = GriefLoggerExpectPlatform.getConfigDirectory().resolve(GriefLogger.MOD_ID); + Path path = ConfigLib.getConfigFolder().toPath().resolve(GriefLogger.MOD_ID); if (!path.toFile().exists()) { //noinspection ResultOfMethodCallIgnored path.toFile().mkdirs(); } try { - connection = DriverManager.getConnection("jdbc:sqlite:database.db"); + String dbPath = path.resolve("database.db").toString(); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath); } catch (SQLException e) { GriefLogger.LOGGER.error("Failed to connect to SQLite database", e); return false; @@ -95,23 +101,25 @@ public boolean createSqliteConnection() { } public void createTable(String sql) { - try { - if (statement != null) { - statement.execute(sql); - } - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to create table", e); - } + execute(sql, true); } public void execute(String sql, boolean logError) { - try { - if (statement != null) { + if (connection == null) return; + + synchronized (lock) { + try (Statement statement = connection.createStatement()) { statement.execute(sql); - } - } catch (SQLException e) { - if (logError) { - GriefLogger.LOGGER.error("Failed to execute statement", e); + connection.commit(); + } catch (SQLException e) { + if (logError) { + GriefLogger.LOGGER.error("Failed to execute statement", e); + } + try { + connection.rollback(); + } catch (SQLException ex) { + GriefLogger.LOGGER.error("Failed to rollback", ex); + } } } } @@ -124,34 +132,46 @@ public PreparedStatement prepareStatement(String query) throws SQLException { } } - public void executeStatements(List statements, boolean isBatch) { - try { - for (PreparedStatement statement : statements) { - if (statement == null) { - GriefLogger.LOGGER.error("Statement is null"); - continue; - } + public void executeQueue(List items, boolean isBatch) { + if (connection == null) return; - if (statement.isClosed()) { - GriefLogger.LOGGER.error("Statement is closed"); - continue; + synchronized (lock) { + try { + for (Object item : items) { + if (item instanceof PreparedStatement preparedStatement) { + if (preparedStatement.isClosed()) { + continue; + } + try (preparedStatement) { + if (isBatch) { + preparedStatement.executeBatch(); + } else { + preparedStatement.executeUpdate(); + } + } + } else if (item instanceof SqlTask task) { + task.execute(connection); + } } - - try (statement) { - if (isBatch) { - statement.executeBatch(); // Execute as a batch - } else { - statement.executeUpdate(); // Execute individually + if (!items.isEmpty()) { + if (connection != null && !connection.isClosed()) { + connection.commit(); } } - } - if (!statements.isEmpty()) { - if (connection != null) { - connection.commit(); + } catch (SQLException e) { + GriefLogger.LOGGER.error("Failed to execute database queue", e); + try { + if (connection != null && !connection.isClosed()) { + connection.rollback(); + } + } catch (SQLException ex) { + GriefLogger.LOGGER.error("Failed to rollback transaction", ex); } } - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to execute statements", e); } } -} + + public IDatabaseDialect getDialect() { + return dialect; + } +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/cache/UserCache.java b/common/src/main/java/com/daqem/grieflogger/database/cache/UserCache.java index 5ed8e57..b54f8da 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/cache/UserCache.java +++ b/common/src/main/java/com/daqem/grieflogger/database/cache/UserCache.java @@ -15,7 +15,7 @@ public class UserCache implements ICache { private final Map usernames = new HashMap<>(); public Map getAllUsernames() { - if (usernameTime + 1000 < System.currentTimeMillis()) { + if (usernameTime + 300000 < System.currentTimeMillis()) { usernames.clear(); usernames.putAll(userService.getAllUsernames()); usernameTime = System.currentTimeMillis(); diff --git a/common/src/main/java/com/daqem/grieflogger/database/dialect/IDatabaseDialect.java b/common/src/main/java/com/daqem/grieflogger/database/dialect/IDatabaseDialect.java new file mode 100644 index 0000000..8470bed --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/database/dialect/IDatabaseDialect.java @@ -0,0 +1,12 @@ +package com.daqem.grieflogger.database.dialect; + +public interface IDatabaseDialect { + + String getInsertIgnore(); + + String getOnConflictUpdate(String key, String update); + + String getOnConflictDoNothing(String key); + + String getDataType(String type); +} diff --git a/common/src/main/java/com/daqem/grieflogger/database/dialect/MySQLDialect.java b/common/src/main/java/com/daqem/grieflogger/database/dialect/MySQLDialect.java new file mode 100644 index 0000000..4a557cd --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/database/dialect/MySQLDialect.java @@ -0,0 +1,30 @@ +package com.daqem.grieflogger.database.dialect; + +public class MySQLDialect implements IDatabaseDialect { + + @Override + public String getInsertIgnore() { + return "INSERT IGNORE"; + } + + @Override + public String getOnConflictUpdate(String key, String update) { + return "ON DUPLICATE KEY UPDATE " + update; + } + + @Override + public String getOnConflictDoNothing(String key) { + return "ON DUPLICATE KEY UPDATE " + key + " = " + key; + } + + @Override + public String getDataType(String type) { + return switch (type) { + case "integer" -> "int"; + case "bigint" -> "bigint"; + case "text" -> "text"; + case "varchar" -> "varchar"; + default -> type; + }; + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/database/dialect/SQLiteDialect.java b/common/src/main/java/com/daqem/grieflogger/database/dialect/SQLiteDialect.java new file mode 100644 index 0000000..af11162 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/database/dialect/SQLiteDialect.java @@ -0,0 +1,30 @@ +package com.daqem.grieflogger.database.dialect; + +public class SQLiteDialect implements IDatabaseDialect { + + @Override + public String getInsertIgnore() { + return "INSERT OR IGNORE"; + } + + @Override + public String getOnConflictUpdate(String key, String update) { + return "ON CONFLICT(" + key + ") DO UPDATE SET " + update; + } + + @Override + public String getOnConflictDoNothing(String key) { + return "ON CONFLICT(" + key + ") DO NOTHING"; + } + + @Override + public String getDataType(String type) { + return switch (type) { + case "integer" -> "integer"; + case "bigint" -> "integer"; + case "text" -> "text"; + case "varchar" -> "text"; + default -> type; + }; + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/database/queue/IQueue.java b/common/src/main/java/com/daqem/grieflogger/database/queue/IQueue.java index ff88421..460ccf1 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/queue/IQueue.java +++ b/common/src/main/java/com/daqem/grieflogger/database/queue/IQueue.java @@ -1,10 +1,9 @@ package com.daqem.grieflogger.database.queue; -import java.sql.PreparedStatement; - public interface IQueue { - void add(PreparedStatement statement); + void add(SqlTask task); void execute(); void hello(); -} + boolean isEmpty(); +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/queue/Queue.java b/common/src/main/java/com/daqem/grieflogger/database/queue/Queue.java index 2eb5525..0dea191 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/queue/Queue.java +++ b/common/src/main/java/com/daqem/grieflogger/database/queue/Queue.java @@ -6,12 +6,13 @@ import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; public class Queue implements IQueue { private final Database database; private final boolean isBatch; - private final List statements = new ArrayList<>(); + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); public Queue(Database database, boolean isBatch) { this.database = database; @@ -19,27 +20,36 @@ public Queue(Database database, boolean isBatch) { } @Override - public void add(PreparedStatement statement) { - this.statements.add(statement); + public void add(SqlTask task) { + this.queue.add(task); } @Override public void execute() { - if (this.statements.isEmpty()) { + if (this.queue.isEmpty()) { return; } - List statements = new ArrayList<>(this.statements); - this.statements.clear(); - this.database.executeStatements(statements, isBatch); + List items = new ArrayList<>(); + Object item; + while ((item = this.queue.poll()) != null) { + items.add(item); + } + this.database.executeQueue(items, isBatch); } @Override public void hello() { - try { - PreparedStatement statement = this.database.prepareStatement("SELECT 1"); - this.database.execute(statement.toString(), false); - } catch (Exception e) { - GriefLogger.LOGGER.error("Failed to send hello packet", e); - } + this.add(connection -> { + try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { + statement.execute(); + } catch (Exception e) { + GriefLogger.LOGGER.error("Failed to send hello packet", e); + } + }); + } + + @Override + public boolean isEmpty() { + return this.queue.isEmpty(); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/queue/SqlTask.java b/common/src/main/java/com/daqem/grieflogger/database/queue/SqlTask.java new file mode 100644 index 0000000..3bbda3e --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/database/queue/SqlTask.java @@ -0,0 +1,9 @@ +package com.daqem.grieflogger.database.queue; + +import java.sql.Connection; +import java.sql.SQLException; + +@FunctionalInterface +public interface SqlTask { + void execute(Connection connection) throws SQLException; +} diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/BlockRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/BlockRepository.java index cfa6f13..595a4a9 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/BlockRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/BlockRepository.java @@ -1,12 +1,5 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.command.filter.FilterList; -import com.daqem.grieflogger.database.Database; -import com.daqem.grieflogger.model.history.BlockHistory; -import com.daqem.grieflogger.model.history.IHistory; -import org.jetbrains.annotations.Nullable; - import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -14,6 +7,15 @@ import java.util.ArrayList; import java.util.List; +import com.daqem.grieflogger.database.dialect.MySQLDialect; +import org.jetbrains.annotations.Nullable; + +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.command.filter.FilterList; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.model.history.BlockHistory; +import com.daqem.grieflogger.model.history.IHistory; + public class BlockRepository extends Repository { private final Database database; @@ -23,167 +25,96 @@ public BlockRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS blocks ( - time integer NOT NULL, - user integer NOT NULL, - level integer NOT NULL, - x integer NOT NULL, - y integer NOT NULL, - z integer NOT NULL, - type integer NOT NULL, - action integer NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id), - FOREIGN KEY(type) REFERENCES materials(id) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS blocks ( - time bigint NOT NULL, - user int NOT NULL, - level int NOT NULL, - x int NOT NULL, - y int NOT NULL, - z int NOT NULL, - type int NOT NULL, - action int NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id), - FOREIGN KEY(type) REFERENCES materials(id) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS blocks (" + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "user " + database.getDialect().getDataType("integer") + " NOT NULL," + + "level " + database.getDialect().getDataType("integer") + " NOT NULL," + + "x " + database.getDialect().getDataType("integer") + " NOT NULL," + + "y " + database.getDialect().getDataType("integer") + " NOT NULL," + + "z " + database.getDialect().getDataType("integer") + " NOT NULL," + + "type " + database.getDialect().getDataType("integer") + " NOT NULL," + + "action " + database.getDialect().getDataType("integer") + " NOT NULL," + + "FOREIGN KEY(user) REFERENCES users(id)," + + "FOREIGN KEY(level) REFERENCES levels(id)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void createIndexes() { - String sql = """ - CREATE INDEX IF NOT EXISTS coordinates ON blocks (x, y, z); - """; - if (isMysql()) { - sql = """ - ALTER TABLE blocks ADD INDEX coordinates (x, y, z); - """; + String sql; + if (database.getDialect() instanceof MySQLDialect) { + sql = "ALTER TABLE blocks ADD INDEX coordinates (x, y, z);"; + } else { + sql = "CREATE INDEX IF NOT EXISTS coordinates ON blocks (x, y, z);"; } database.execute(sql, false); } public void insertMaterial(long time, String userUuid, String levelName, int x, int y, int z, String material, int blockAction) { - String materialQuery = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; - - if (isMysql()) { - materialQuery = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } - - String blockQuery = """ - INSERT OR IGNORE INTO blocks(time, user, level, x, y, z, type, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?); - """; - - if (isMysql()) { - blockQuery = """ - INSERT IGNORE INTO blocks(time, user, level, x, y, z, type, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?); - """; - } - - try { - PreparedStatement materialStatement = database.prepareStatement(materialQuery); - materialStatement.setString(1, material); - database.queue.add(materialStatement); - - PreparedStatement blockStatement = database.prepareStatement(blockQuery); - blockStatement.setLong(1, time); - blockStatement.setString(2, userUuid); - blockStatement.setString(3, levelName); - blockStatement.setInt(4, x); - blockStatement.setInt(5, y); - blockStatement.setInt(6, z); - blockStatement.setString(7, material); - blockStatement.setInt(8, blockAction); - database.queue.add(blockStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert block into database", exception); - } + String materialQuery = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; + + String blockQuery = database.getDialect().getInsertIgnore() + " INTO blocks(time, user, level, x, y, z, type, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM materials WHERE name = ?" + + "), ?);"; + + database.queue.add(connection -> { + try (PreparedStatement materialStatement = connection.prepareStatement(materialQuery)) { + materialStatement.setString(1, material); + materialStatement.executeUpdate(); + } + try (PreparedStatement blockStatement = connection.prepareStatement(blockQuery)) { + blockStatement.setLong(1, time); + blockStatement.setString(2, userUuid); + blockStatement.setString(3, levelName); + blockStatement.setInt(4, x); + blockStatement.setInt(5, y); + blockStatement.setInt(6, z); + blockStatement.setString(7, material); + blockStatement.setInt(8, blockAction); + blockStatement.executeUpdate(); + } + }); } public void insertEntity(long time, String userUuid, String levelName, int x, int y, int z, String entity, int blockAction) { - String materialQuery = """ - INSERT OR IGNORE INTO entities(name) - VALUES(?); - """; - - if (isMysql()) { - materialQuery = """ - INSERT IGNORE INTO entities(name) - VALUES(?); - """; - } - - String blockQuery = """ - INSERT OR IGNORE INTO blocks(time, user, level, x, y, z, type, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM entities WHERE name = ? - ), ?); - """; - - if (isMysql()) { - blockQuery = """ - INSERT IGNORE INTO blocks(time, user, level, x, y, z, type, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM entities WHERE name = ? - ), ?); - """; - } - - - try { - PreparedStatement materialStatement = database.prepareStatement(materialQuery); - PreparedStatement blockStatement = database.prepareStatement(blockQuery); - materialStatement.setString(1, entity); - database.queue.add(materialStatement); - - blockStatement.setLong(1, time); - blockStatement.setString(2, userUuid); - blockStatement.setString(3, levelName); - blockStatement.setInt(4, x); - blockStatement.setInt(5, y); - blockStatement.setInt(6, z); - blockStatement.setString(7, entity); - blockStatement.setInt(8, blockAction); - database.queue.add(blockStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert block into database", exception); - } + String materialQuery = database.getDialect().getInsertIgnore() + " INTO entities(name) VALUES(?);"; + + String blockQuery = database.getDialect().getInsertIgnore() + " INTO blocks(time, user, level, x, y, z, type, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM entities WHERE name = ?" + + "), ?);"; + + database.queue.add(connection -> { + try (PreparedStatement materialStatement = connection.prepareStatement(materialQuery)) { + materialStatement.setString(1, entity); + materialStatement.executeUpdate(); + } + try (PreparedStatement blockStatement = connection.prepareStatement(blockQuery)) { + blockStatement.setLong(1, time); + blockStatement.setString(2, userUuid); + blockStatement.setString(3, levelName); + blockStatement.setInt(4, x); + blockStatement.setInt(5, y); + blockStatement.setInt(6, z); + blockStatement.setString(7, entity); + blockStatement.setInt(8, blockAction); + blockStatement.executeUpdate(); + } + }); } public List getBlockHistory(String levelName, int x, int y, int z) { @@ -227,14 +158,17 @@ public List getBlockHistory(String levelName, int x, int y, int z) { public List getInteractionHistory(String levelName, int x, int y, int z) { List blockHistory = new ArrayList<>(); String query = """ - SELECT blocks.time, users.name, users.uuid, blocks.x, blocks.y, blocks.z, materials.name, blocks.action + SELECT blocks.time, users.name, users.uuid, blocks.x, blocks.y, blocks.z, + CASE WHEN blocks.action = 4 THEN entities.name ELSE materials.name END, + blocks.action FROM blocks INNER JOIN users ON blocks.user = users.id INNER JOIN levels ON blocks.level = ( SELECT id FROM levels WHERE name = ? ) - INNER JOIN materials ON blocks.type = materials.id - WHERE blocks.level = levels.id AND blocks.x = ? AND blocks.y = ? AND blocks.z = ? AND blocks.action = 2 + LEFT JOIN materials ON blocks.type = materials.id AND blocks.action = 2 + LEFT JOIN entities ON blocks.type = entities.id AND blocks.action = 4 + WHERE blocks.level = levels.id AND blocks.x = ? AND blocks.y = ? AND blocks.z = ? AND (blocks.action = 2 OR blocks.action = 4) ORDER BY blocks.time DESC """; @@ -270,16 +204,15 @@ public void removeInteractionsForPosition(String levelName, int x, int y, int z) ) AND x = ? AND y = ? AND z = ? AND action = 2 """; - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setString(1, levelName); - preparedStatement.setInt(2, x); - preparedStatement.setInt(3, y); - preparedStatement.setInt(4, z); - database.queue.add(preparedStatement); - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to remove interactions for position", e); - } + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, levelName); + preparedStatement.setInt(2, x); + preparedStatement.setInt(3, y); + preparedStatement.setInt(4, z); + preparedStatement.executeUpdate(); + } + }); } public List getFilteredBlockHistory(String levelName, FilterList filterList) { @@ -297,7 +230,7 @@ public List getFilteredBlockHistory(String levelName, FilterList filte blocks.y, blocks.z, CASE - WHEN blocks.action = 3 THEN entities.name + WHEN blocks.action = 3 OR blocks.action = 4 THEN entities.name ELSE materials.name END AS type_name, blocks.action @@ -305,8 +238,8 @@ public List getFilteredBlockHistory(String levelName, FilterList filte blocks INNER JOIN users ON blocks.user = users.id INNER JOIN levels ON blocks.level = levels.id - LEFT JOIN materials ON blocks.type = materials.id AND blocks.action != 3 - LEFT JOIN entities ON blocks.type = entities.id AND blocks.action = 3 + LEFT JOIN materials ON blocks.type = materials.id AND blocks.action != 3 AND blocks.action != 4 + LEFT JOIN entities ON blocks.type = entities.id AND (blocks.action = 3 OR blocks.action = 4) WHERE levels.name = ? AND blocks.time > ? @@ -314,9 +247,9 @@ public List getFilteredBlockHistory(String levelName, FilterList filte AND (? IS NULL OR users.id IN (%s)) AND (? IS NULL OR materials.name IN ('%s')) AND (? IS NULL OR materials.name NOT IN ('%s')) - AND blocks.x BETWEEN ? AND ? - AND blocks.y BETWEEN ? AND ? - AND blocks.z BETWEEN ? AND ? + AND (? IS NULL OR blocks.x BETWEEN ? AND ?) + AND (? IS NULL OR blocks.y BETWEEN ? AND ?) + AND (? IS NULL OR blocks.z BETWEEN ? AND ?) ORDER BY blocks.time DESC LIMIT 1000; @@ -350,12 +283,32 @@ public List getFilteredBlockHistory(String levelName, FilterList filte preparedStatement.setString(6, "not null"); } - preparedStatement.setInt(7, filterList.getRadiusMinX()); - preparedStatement.setInt(8, filterList.getRadiusMaxX()); - preparedStatement.setInt(9, filterList.getRadiusMinY()); - preparedStatement.setInt(10, filterList.getRadiusMaxY()); - preparedStatement.setInt(11, filterList.getRadiusMinZ()); - preparedStatement.setInt(12, filterList.getRadiusMaxZ()); + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(7, Types.VARCHAR); + } else { + preparedStatement.setString(7, "not null"); + } + + preparedStatement.setInt(8, filterList.getRadiusMinX()); + preparedStatement.setInt(9, filterList.getRadiusMaxX()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(10, Types.VARCHAR); + } else { + preparedStatement.setString(10, "not null"); + } + + preparedStatement.setInt(11, filterList.getRadiusMinY()); + preparedStatement.setInt(12, filterList.getRadiusMaxY()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(13, Types.VARCHAR); + } else { + preparedStatement.setString(13, "not null"); + } + + preparedStatement.setInt(14, filterList.getRadiusMinZ()); + preparedStatement.setInt(15, filterList.getRadiusMaxZ()); List blockHistory = new ArrayList<>(); ResultSet resultSet = preparedStatement.executeQuery(); @@ -376,4 +329,4 @@ public List getFilteredBlockHistory(String levelName, FilterList filte return List.of(); } } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/ChatRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/ChatRepository.java index 2915cd5..bc89d6d 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/ChatRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/ChatRepository.java @@ -1,11 +1,12 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.SQLException; +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; + public class ChatRepository extends Repository { private final Database database; @@ -15,83 +16,54 @@ public ChatRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS chats ( - time integer NOT NULL, - user integer NOT NULL, - level integer NOT NULL, - x integer NOT NULL, - y integer NOT NULL, - z integer NOT NULL, - message text NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS chats ( - time bigint NOT NULL, - user int NOT NULL, - level int NOT NULL, - x int NOT NULL, - y int NOT NULL, - z int NOT NULL, - message varchar(256) NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS chats (" + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "user " + database.getDialect().getDataType("integer") + " NOT NULL," + + "level " + database.getDialect().getDataType("integer") + " NOT NULL," + + "x " + database.getDialect().getDataType("integer") + " NOT NULL," + + "y " + database.getDialect().getDataType("integer") + " NOT NULL," + + "z " + database.getDialect().getDataType("integer") + " NOT NULL," + + "message " + database.getDialect().getDataType("varchar") + "(256) NOT NULL," + + "FOREIGN KEY(user) REFERENCES users(id)," + + "FOREIGN KEY(level) REFERENCES levels(id)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void createIndexes() { - String sql = """ - CREATE INDEX IF NOT EXISTS coordinates ON chats (x, y, z); - """; - if (isMysql()) { - sql = """ - ALTER TABLE chats ADD INDEX coordinates (x, y, z); - """; + String sql; + if (database.getDialect() instanceof MySQLDialect) { + sql = "ALTER TABLE chats ADD INDEX coordinates (x, y, z);"; + } else { + sql = "CREATE INDEX IF NOT EXISTS coordinates ON chats (x, y, z);"; } database.execute(sql, false); } public void insert(long time, String userUuid, String levelName, int x, int y, int z, String message) { - String query = """ - INSERT OR IGNORE INTO chats(time, user, level, x, y, z, message) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO chats(time, user, level, x, y, z, message) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ?); - """; - } + String query = database.getDialect().getInsertIgnore() + " INTO chats(time, user, level, x, y, z, message) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, ?);"; - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setLong(1, time); - preparedStatement.setString(2, userUuid); - preparedStatement.setString(3, levelName); - preparedStatement.setInt(4, x); - preparedStatement.setInt(5, y); - preparedStatement.setInt(6, z); - preparedStatement.setString(7, message); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert chat into database", exception); - } + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setLong(1, time); + preparedStatement.setString(2, userUuid); + preparedStatement.setString(3, levelName); + preparedStatement.setInt(4, x); + preparedStatement.setInt(5, y); + preparedStatement.setInt(6, z); + preparedStatement.setString(7, message); + preparedStatement.executeUpdate(); + } + }); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/CommandRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/CommandRepository.java index 1691ee1..154d447 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/CommandRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/CommandRepository.java @@ -1,11 +1,12 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.SQLException; +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; + public class CommandRepository extends Repository { private final Database database; @@ -15,83 +16,54 @@ public CommandRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS commands ( - time integer NOT NULL, - user integer NOT NULL, - level integer NOT NULL, - x integer NOT NULL, - y integer NOT NULL, - z integer NOT NULL, - command text NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS commands ( - time bigint NOT NULL, - user int NOT NULL, - level int NOT NULL, - x int NOT NULL, - y int NOT NULL, - z int NOT NULL, - command varchar(256) NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS commands (" + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "user " + database.getDialect().getDataType("integer") + " NOT NULL," + + "level " + database.getDialect().getDataType("integer") + " NOT NULL," + + "x " + database.getDialect().getDataType("integer") + " NOT NULL," + + "y " + database.getDialect().getDataType("integer") + " NOT NULL," + + "z " + database.getDialect().getDataType("integer") + " NOT NULL," + + "command " + database.getDialect().getDataType("varchar") + "(256) NOT NULL," + + "FOREIGN KEY(user) REFERENCES users(id)," + + "FOREIGN KEY(level) REFERENCES levels(id)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void createIndexes() { - String sql = """ - CREATE INDEX IF NOT EXISTS coordinates ON commands (x, y, z); - """; - if (isMysql()) { - sql = """ - ALTER TABLE commands ADD INDEX coordinates (x, y, z); - """; + String sql; + if (database.getDialect() instanceof MySQLDialect) { + sql = "ALTER TABLE commands ADD INDEX coordinates (x, y, z);"; + } else { + sql = "CREATE INDEX IF NOT EXISTS coordinates ON commands (x, y, z);"; } database.execute(sql, false); } public void insert(long time, String userUuid, String levelName, int x, int y, int z, String command) { - String query = """ - INSERT OR IGNORE INTO commands(time, user, level, x, y, z, command) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO commands(time, user, level, x, y, z, command) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ?); - """; - } + String query = database.getDialect().getInsertIgnore() + " INTO commands(time, user, level, x, y, z, command) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, ?);"; - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setLong(1, time); - preparedStatement.setString(2, userUuid); - preparedStatement.setString(3, levelName); - preparedStatement.setInt(4, x); - preparedStatement.setInt(5, y); - preparedStatement.setInt(6, z); - preparedStatement.setString(7, command); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert command into database", exception); - } + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setLong(1, time); + preparedStatement.setString(2, userUuid); + preparedStatement.setString(3, levelName); + preparedStatement.setInt(4, x); + preparedStatement.setInt(5, y); + preparedStatement.setInt(6, z); + preparedStatement.setString(7, command); + preparedStatement.executeUpdate(); + } + }); } } \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/ContainerRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/ContainerRepository.java index 7e7c798..f306cb6 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/ContainerRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/ContainerRepository.java @@ -2,12 +2,14 @@ import com.daqem.grieflogger.GriefLogger; import com.daqem.grieflogger.command.filter.FilterList; +import com.daqem.grieflogger.database.dialect.MySQLDialect; import com.daqem.grieflogger.model.SimpleItemStack; import com.daqem.grieflogger.database.Database; import com.daqem.grieflogger.model.action.ItemAction; import com.daqem.grieflogger.model.history.ContainerHistory; import com.daqem.grieflogger.model.history.IHistory; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; import java.sql.PreparedStatement; @@ -27,152 +29,66 @@ public ContainerRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS containers ( - time integer NOT NULL, - user integer NOT NULL, - level integer NOT NULL, - x integer NOT NULL, - y integer NOT NULL, - z integer NOT NULL, - type integer NOT NULL, - data blob DEFAULT NULL, - amount integer NOT NULL, - action integer NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id), - FOREIGN KEY(type) REFERENCES materials(id) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS containers ( - time bigint NOT NULL, - user int NOT NULL, - level int NOT NULL, - x int NOT NULL, - y int NOT NULL, - z int NOT NULL, - type int NOT NULL, - data blob DEFAULT NULL, - amount int NOT NULL, - action int NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id), - FOREIGN KEY(type) REFERENCES materials(id) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS containers (" + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "user " + database.getDialect().getDataType("integer") + " NOT NULL," + + "level " + database.getDialect().getDataType("integer") + " NOT NULL," + + "x " + database.getDialect().getDataType("integer") + " NOT NULL," + + "y " + database.getDialect().getDataType("integer") + " NOT NULL," + + "z " + database.getDialect().getDataType("integer") + " NOT NULL," + + "type " + database.getDialect().getDataType("integer") + " NOT NULL," + + "data blob DEFAULT NULL," + + "amount " + database.getDialect().getDataType("integer") + " NOT NULL," + + "action " + database.getDialect().getDataType("integer") + " NOT NULL," + + "FOREIGN KEY(user) REFERENCES users(id)," + + "FOREIGN KEY(level) REFERENCES levels(id)," + + "FOREIGN KEY(type) REFERENCES materials(id)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void createIndexes() { - String sql = """ - CREATE INDEX IF NOT EXISTS coordinates ON containers (x, y, z); - """; - if (isMysql()) { - sql = """ - ALTER TABLE containers ADD INDEX coordinates (x, y, z); - """; + String sql; + if (database.getDialect() instanceof MySQLDialect) { + sql = "ALTER TABLE containers ADD INDEX coordinates (x, y, z);"; + } else { + sql = "CREATE INDEX IF NOT EXISTS coordinates ON containers (x, y, z);"; } database.execute(sql, false); } - public void insert(long time, String userUuid, String levelName, int x, int y, int z, SimpleItemStack item, int itemAction) { + public void insert(long time, String userUuid, Level level, int x, int y, int z, SimpleItemStack item, int itemAction) { if (item.isEmpty()) { return; } - String insertMaterialQuery = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; - - if (isMysql()) { - insertMaterialQuery = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } + String insertMaterialQuery = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; - String insertItemQuery = """ - INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?, ?, ?); - """; + String insertItemQuery = "INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM materials WHERE name = ?" + + "), ?, ?, ?);"; ResourceLocation itemLocation = item.getItem().arch$registryName(); if (itemLocation != null) { - try { - PreparedStatement itemStatement = database.prepareStatement(insertItemQuery); - PreparedStatement materialStatement = database.prepareStatement(insertMaterialQuery); - - materialStatement.setString(1, itemLocation.toString().replace("minecraft:", "")); - database.queue.add(materialStatement); - - itemStatement.setLong(1, time); - itemStatement.setString(2, userUuid); - itemStatement.setString(3, levelName); - itemStatement.setInt(4, x); - itemStatement.setInt(5, y); - itemStatement.setInt(6, z); - itemStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); - itemStatement.setBytes(8, item.getTagBytes()); - itemStatement.setInt(9, item.getCount()); - itemStatement.setInt(10, itemAction); - database.queue.add(itemStatement); - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to insert item", e); - } - } - } - - public void insertList(long time, String userUuid, String levelName, int x, int y, int z, List items, int itemAction) { - String insertMaterialQuery = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; - - if (isMysql()) { - insertMaterialQuery = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } - - String insertItemQuery = """ - INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?, ?, ?); - """; - - try { - PreparedStatement itemStatement = database.prepareStatement(insertItemQuery); - PreparedStatement materialStatement = database.prepareStatement(insertMaterialQuery); - - for (SimpleItemStack item : items) { - if (item.isEmpty()) { - continue; - } - ResourceLocation itemLocation = item.getItem().arch$registryName(); - if (itemLocation != null) { + database.queue.add(connection -> { + try (PreparedStatement materialStatement = connection.prepareStatement(insertMaterialQuery)) { materialStatement.setString(1, itemLocation.toString().replace("minecraft:", "")); - materialStatement.addBatch(); - + materialStatement.executeUpdate(); + } + try (PreparedStatement itemStatement = connection.prepareStatement(insertItemQuery)) { itemStatement.setLong(1, time); itemStatement.setString(2, userUuid); - itemStatement.setString(3, levelName); + itemStatement.setString(3, level.dimension().location().toString()); itemStatement.setInt(4, x); itemStatement.setInt(5, y); itemStatement.setInt(6, z); @@ -180,46 +96,29 @@ INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) itemStatement.setBytes(8, item.getTagBytes()); itemStatement.setInt(9, item.getCount()); itemStatement.setInt(10, itemAction); - itemStatement.addBatch(); + itemStatement.executeUpdate(); } - } - database.batchQueue.add(materialStatement); - database.batchQueue.add(itemStatement); - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to insert item", e); + }); } } - public void insertMap(long time, String userUuid, String levelName, int x, int y, int z, Map> itemsMap) { - String insertMaterialQuery = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; - - if (isMysql()) { - insertMaterialQuery = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } + public void insertList(long time, String userUuid, Level level, int x, int y, int z, List items, int itemAction) { + String insertMaterialQuery = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; - String insertItemQuery = """ - INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?, ?, ?); - """; + String insertItemQuery = "INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM materials WHERE name = ?" + + "), ?, ?, ?);"; - try { - PreparedStatement itemStatement = database.prepareStatement(insertItemQuery); - PreparedStatement materialStatement = database.prepareStatement(insertMaterialQuery); + database.batchQueue.add(connection -> { + try (PreparedStatement itemStatement = connection.prepareStatement(insertItemQuery); + PreparedStatement materialStatement = connection.prepareStatement(insertMaterialQuery)) { - for (Map.Entry> entry : itemsMap.entrySet()) { - for (SimpleItemStack item : entry.getValue()) { + for (SimpleItemStack item : items) { if (item.isEmpty()) { continue; } @@ -230,26 +129,70 @@ INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) itemStatement.setLong(1, time); itemStatement.setString(2, userUuid); - itemStatement.setString(3, levelName); + itemStatement.setString(3, level.dimension().location().toString()); itemStatement.setInt(4, x); itemStatement.setInt(5, y); itemStatement.setInt(6, z); itemStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); itemStatement.setBytes(8, item.getTagBytes()); itemStatement.setInt(9, item.getCount()); - itemStatement.setInt(10, entry.getKey().getId()); + itemStatement.setInt(10, itemAction); itemStatement.addBatch(); } } + materialStatement.executeBatch(); + itemStatement.executeBatch(); } - database.batchQueue.add(materialStatement); - database.batchQueue.add(itemStatement); - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to insert item", e); - } + }); + } + + public void insertMap(long time, String userUuid, Level level, int x, int y, int z, Map> itemsMap) { + String insertMaterialQuery = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; + + String insertItemQuery = "INSERT INTO containers(time, user, level, x, y, z, type, data, amount, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM materials WHERE name = ?" + + "), ?, ?, ?);"; + + database.batchQueue.add(connection -> { + try (PreparedStatement itemStatement = connection.prepareStatement(insertItemQuery); + PreparedStatement materialStatement = connection.prepareStatement(insertMaterialQuery)) { + + for (Map.Entry> entry : itemsMap.entrySet()) { + for (SimpleItemStack item : entry.getValue()) { + if (item.isEmpty()) { + continue; + } + ResourceLocation itemLocation = item.getItem().arch$registryName(); + if (itemLocation != null) { + materialStatement.setString(1, itemLocation.toString().replace("minecraft:", "")); + materialStatement.addBatch(); + + itemStatement.setLong(1, time); + itemStatement.setString(2, userUuid); + itemStatement.setString(3, level.dimension().location().toString()); + itemStatement.setInt(4, x); + itemStatement.setInt(5, y); + itemStatement.setInt(6, z); + itemStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); + itemStatement.setBytes(8, item.getTagBytes()); + itemStatement.setInt(9, item.getCount()); + itemStatement.setInt(10, entry.getKey().getId()); + itemStatement.addBatch(); + } + } + } + materialStatement.executeBatch(); + itemStatement.executeBatch(); + } + }); } - public List getHistory(String levelName, int x, int y, int z) { + public List getHistory(Level level, int x, int y, int z) { List containerHistory = new ArrayList<>(); String query = """ SELECT containers.time, users.name, users.uuid, containers.x, containers.y, containers.z, materials.name, containers.data, containers.amount, containers.action @@ -264,7 +207,7 @@ public List getHistory(String levelName, int x, int y, int z) { """; try (PreparedStatement preparedStatement = database.prepareStatement(query)) { - preparedStatement.setString(1, levelName); + preparedStatement.setString(1, level.dimension().location().toString()); preparedStatement.setInt(2, x); preparedStatement.setInt(3, y); preparedStatement.setInt(4, z); @@ -290,7 +233,7 @@ public List getHistory(String levelName, int x, int y, int z) { return containerHistory; } - public List getHistory(String levelName, int x, int y, int z, int x2, int y2, int z2) { + public List getHistory(Level level, int x, int y, int z, int x2, int y2, int z2) { List containerHistory = new ArrayList<>(); String query = """ SELECT containers.time, users.name, users.uuid, containers.x, containers.y, containers.z, materials.name, containers.data, containers.amount, containers.action @@ -305,7 +248,7 @@ public List getHistory(String levelName, int x, int y, int z, int x2, """; try (PreparedStatement preparedStatement = database.prepareStatement(query)) { - preparedStatement.setString(1, levelName); + preparedStatement.setString(1, level.dimension().location().toString()); preparedStatement.setInt(2, x); preparedStatement.setInt(3, x2); preparedStatement.setInt(4, y); @@ -334,7 +277,7 @@ public List getHistory(String levelName, int x, int y, int z, int x2, return containerHistory; } - public List getFilteredContainerHistory(String levelName, FilterList filterList) { + public List getFilteredContainerHistory(Level level, FilterList filterList) { @Nullable String actions = filterList.getActionString(); @Nullable String users = filterList.getUserString(); @Nullable String includeMaterials = filterList.getIncludeMaterialsString(); @@ -352,15 +295,15 @@ public List getFilteredContainerHistory(String levelName, FilterList f AND (? IS NULL OR users.id IN (%s)) AND (? IS NULL OR materials.name IN ('%s')) AND (? IS NULL OR materials.name NOT IN ('%s')) - AND containers.x BETWEEN ? AND ? - AND containers.y BETWEEN ? AND ? - AND containers.z BETWEEN ? AND ? + AND (? IS NULL OR containers.x BETWEEN ? AND ?) + AND (? IS NULL OR containers.y BETWEEN ? AND ?) + AND (? IS NULL OR containers.z BETWEEN ? AND ?) ORDER BY containers.time DESC LIMIT 1000; """.formatted(actions, users, includeMaterials, excludeMaterials); try (PreparedStatement preparedStatement = database.prepareStatement(query)) { - preparedStatement.setString(1, levelName); + preparedStatement.setString(1, level.dimension().location().toString()); preparedStatement.setLong(2, filterList.getTime()); if (actions == null || actions.isEmpty()) { @@ -387,12 +330,32 @@ public List getFilteredContainerHistory(String levelName, FilterList f preparedStatement.setString(6, "not null"); } - preparedStatement.setInt(7, filterList.getRadiusMinX()); - preparedStatement.setInt(8, filterList.getRadiusMaxX()); - preparedStatement.setInt(9, filterList.getRadiusMinY()); - preparedStatement.setInt(10, filterList.getRadiusMaxY()); - preparedStatement.setInt(11, filterList.getRadiusMinZ()); - preparedStatement.setInt(12, filterList.getRadiusMaxZ()); + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(7, Types.VARCHAR); + } else { + preparedStatement.setString(7, "not null"); + } + + preparedStatement.setInt(8, filterList.getRadiusMinX()); + preparedStatement.setInt(9, filterList.getRadiusMaxX()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(10, Types.VARCHAR); + } else { + preparedStatement.setString(10, "not null"); + } + + preparedStatement.setInt(11, filterList.getRadiusMinY()); + preparedStatement.setInt(12, filterList.getRadiusMaxY()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(13, Types.VARCHAR); + } else { + preparedStatement.setString(13, "not null"); + } + + preparedStatement.setInt(14, filterList.getRadiusMinZ()); + preparedStatement.setInt(15, filterList.getRadiusMaxZ()); List blockHistory = new ArrayList<>(); ResultSet resultSet = preparedStatement.executeQuery(); @@ -415,4 +378,4 @@ public List getFilteredContainerHistory(String levelName, FilterList f return List.of(); } } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/EntityRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/EntityRepository.java index a66b31d..89b25e0 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/EntityRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/EntityRepository.java @@ -1,11 +1,12 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.SQLException; +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; + public class EntityRepository extends Repository { private final Database database; @@ -15,43 +16,26 @@ public EntityRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS entities ( - id integer PRIMARY KEY, - name text NOT NULL UNIQUE - ) - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS entities ( - id int PRIMARY KEY AUTO_INCREMENT, - name varchar(256) NOT NULL UNIQUE - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS entities (" + + "id " + database.getDialect().getDataType("integer") + " PRIMARY KEY" + (database.getDialect() instanceof MySQLDialect ? " AUTO_INCREMENT" : "") + "," + + "name " + database.getDialect().getDataType("text") + " NOT NULL UNIQUE" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void insert(String name) { - String query = """ - INSERT OR IGNORE INTO entities(name) - VALUES(?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO entities(name) - VALUES(?); - """; - } - - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setString(1, name); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert entity into database", exception); - } + String query = database.getDialect().getInsertIgnore() + " INTO entities(name) VALUES(?);"; + + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, name); + preparedStatement.executeUpdate(); + } + }); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/IRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/IRepository.java index ed4460a..a071b76 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/IRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/IRepository.java @@ -4,5 +4,5 @@ public interface IRepository { void createTable(); - boolean isMysql(); + } diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/ItemRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/ItemRepository.java index b7c2f1a..2891856 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/ItemRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/ItemRepository.java @@ -3,10 +3,12 @@ import com.daqem.grieflogger.GriefLogger; import com.daqem.grieflogger.command.filter.FilterList; import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; import com.daqem.grieflogger.model.SimpleItemStack; import com.daqem.grieflogger.model.action.ItemAction; import com.daqem.grieflogger.model.history.ItemHistory; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; import java.sql.PreparedStatement; @@ -26,138 +28,93 @@ public ItemRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS items ( - time integer NOT NULL, - user integer NOT NULL, - level integer NOT NULL, - x integer NOT NULL, - y integer NOT NULL, - z integer NOT NULL, - type integer NOT NULL, - data blob DEFAULT NULL, - amount integer NOT NULL, - action integer NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id), - FOREIGN KEY(type) REFERENCES materials(id) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS items ( - time bigint NOT NULL, - user int NOT NULL, - level int NOT NULL, - x int NOT NULL, - y int NOT NULL, - z int NOT NULL, - type int NOT NULL, - data blob DEFAULT NULL, - amount int NOT NULL, - action int NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id), - FOREIGN KEY(type) REFERENCES materials(id) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS items (" + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "user " + database.getDialect().getDataType("integer") + " NOT NULL," + + "level " + database.getDialect().getDataType("integer") + " NOT NULL," + + "x " + database.getDialect().getDataType("integer") + " NOT NULL," + + "y " + database.getDialect().getDataType("integer") + " NOT NULL," + + "z " + database.getDialect().getDataType("integer") + " NOT NULL," + + "type " + database.getDialect().getDataType("integer") + " NOT NULL," + + "data blob DEFAULT NULL," + + "amount " + database.getDialect().getDataType("integer") + " NOT NULL," + + "action " + database.getDialect().getDataType("integer") + " NOT NULL," + + "FOREIGN KEY(user) REFERENCES users(id)," + + "FOREIGN KEY(level) REFERENCES levels(id)," + + "FOREIGN KEY(type) REFERENCES materials(id)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void createIndexes() { - String sql = """ - CREATE INDEX IF NOT EXISTS coordinates ON items (x, y, z); - """; - if (isMysql()) { - sql = """ - ALTER TABLE items ADD INDEX coordinates (x, y, z); - """; + String sql; + if (database.getDialect() instanceof MySQLDialect) { + sql = "ALTER TABLE items ADD INDEX coordinates (x, y, z);"; + } else { + sql = "CREATE INDEX IF NOT EXISTS coordinates ON items (x, y, z);"; } database.execute(sql, false); } - public void insert(long time, String userUuid, String levelName, int x, int y, int z, SimpleItemStack item, int action) { + public void insert(long time, String userUuid, Level level, int x, int y, int z, SimpleItemStack item, int action) { if (item.isEmpty()) { return; } - String materialQuery = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; + String materialQuery = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; - if (isMysql()) { - materialQuery = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } - - String itemQuery = """ - INSERT INTO items(time, user, level, x, y, z, type, data, amount, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?, ?, ?); - """; + String itemQuery = "INSERT INTO items(time, user, level, x, y, z, type, data, amount, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM materials WHERE name = ?" + + "), ?, ?, ?);"; ResourceLocation itemLocation = item.getItem().arch$registryName(); if (itemLocation != null) { - try { - PreparedStatement preparedStatement = database.prepareStatement(itemQuery); - PreparedStatement materialStatement = database.prepareStatement(materialQuery); - - materialStatement.setString(1, itemLocation.toString().replace("minecraft:", "")); - database.queue.add(materialStatement); - - preparedStatement.setLong(1, time); - preparedStatement.setString(2, userUuid); - preparedStatement.setString(3, levelName); - preparedStatement.setInt(4, x); - preparedStatement.setInt(5, y); - preparedStatement.setInt(6, z); - preparedStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); - preparedStatement.setBytes(8, item.getTagBytes()); - preparedStatement.setInt(9, item.getCount()); - preparedStatement.setInt(10, action); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert item into database", exception); - } + database.queue.add(connection -> { + try (PreparedStatement materialStatement = connection.prepareStatement(materialQuery)) { + materialStatement.setString(1, itemLocation.toString().replace("minecraft:", "")); + materialStatement.executeUpdate(); + } + try (PreparedStatement preparedStatement = connection.prepareStatement(itemQuery)) { + preparedStatement.setLong(1, time); + preparedStatement.setString(2, userUuid); + preparedStatement.setString(3, level.dimension().location().toString()); + preparedStatement.setInt(4, x); + preparedStatement.setInt(5, y); + preparedStatement.setInt(6, z); + preparedStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); + preparedStatement.setBytes(8, item.getTagBytes()); + preparedStatement.setInt(9, item.getCount()); + preparedStatement.setInt(10, action); + preparedStatement.executeUpdate(); + } + }); } } - public void insertMap(long time, String userUuid, String levelName, int x, int y, int z, Map> itemsMap) { - String insertMaterialQuery = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; - - if (isMysql()) { - insertMaterialQuery = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } + public void insertMap(long time, String userUuid, Level level, int x, int y, int z, Map> itemsMap) { + String insertMaterialQuery = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; - String insertItemQuery = """ - INSERT INTO items(time, user, level, x, y, z, type, data, amount, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ( - SELECT id FROM materials WHERE name = ? - ), ?, ?, ?); - """; + String insertItemQuery = "INSERT INTO items(time, user, level, x, y, z, type, data, amount, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, (" + + "SELECT id FROM materials WHERE name = ?" + + "), ?, ?, ?);"; - try { - PreparedStatement itemStatement = database.prepareStatement(insertItemQuery); - PreparedStatement materialStatement = database.prepareStatement(insertMaterialQuery); + database.batchQueue.add(connection -> { + try (PreparedStatement itemStatement = connection.prepareStatement(insertItemQuery); + PreparedStatement materialStatement = connection.prepareStatement(insertMaterialQuery)) { for (Map.Entry> entry : itemsMap.entrySet()) { for (SimpleItemStack item : entry.getValue()) { @@ -169,28 +126,27 @@ INSERT INTO items(time, user, level, x, y, z, type, data, amount, action) materialStatement.setString(1, itemLocation.toString().replace("minecraft:", "")); materialStatement.addBatch(); - itemStatement.setLong(1, time); - itemStatement.setString(2, userUuid); - itemStatement.setString(3, levelName); - itemStatement.setInt(4, x); - itemStatement.setInt(5, y); - itemStatement.setInt(6, z); - itemStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); - itemStatement.setBytes(8, item.getTagBytes()); - itemStatement.setInt(9, item.getCount()); - itemStatement.setInt(10, entry.getKey().getId()); - itemStatement.addBatch(); + itemStatement.setLong(1, time); + itemStatement.setString(2, userUuid); + itemStatement.setString(3, level.dimension().location().toString()); + itemStatement.setInt(4, x); + itemStatement.setInt(5, y); + itemStatement.setInt(6, z); + itemStatement.setString(7, itemLocation.toString().replace("minecraft:", "")); + itemStatement.setBytes(8, item.getTagBytes()); + itemStatement.setInt(9, item.getCount()); + itemStatement.setInt(10, entry.getKey().getId()); + itemStatement.addBatch(); + } } } + materialStatement.executeBatch(); + itemStatement.executeBatch(); } - database.batchQueue.add(materialStatement); - database.batchQueue.add(itemStatement); - } catch (SQLException e) { - GriefLogger.LOGGER.error("Failed to insert item", e); - } + }); } - public List getFilteredItemHistory(String levelName, FilterList filterList) { + public List getFilteredItemHistory(Level level, FilterList filterList) { @Nullable String actions = filterList.getActionString(); @Nullable String users = filterList.getUserString(); @Nullable String includeMaterials = filterList.getIncludeMaterialsString(); @@ -208,15 +164,15 @@ public List getFilteredItemHistory(String levelName, FilterList fil AND (? IS NULL OR users.id IN (%s)) AND (? IS NULL OR materials.name IN ('%s')) AND (? IS NULL OR materials.name NOT IN ('%s')) - AND items.x BETWEEN ? AND ? - AND items.y BETWEEN ? AND ? - AND items.z BETWEEN ? AND ? + AND (? IS NULL OR items.x BETWEEN ? AND ?) + AND (? IS NULL OR items.y BETWEEN ? AND ?) + AND (? IS NULL OR items.z BETWEEN ? AND ?) ORDER BY items.time DESC LIMIT 1000; """.formatted(actions, users, includeMaterials, excludeMaterials); try (PreparedStatement preparedStatement = database.prepareStatement(query)) { - preparedStatement.setString(1, levelName); + preparedStatement.setString(1, level.dimension().location().toString()); preparedStatement.setLong(2, filterList.getTime()); if (actions == null || actions.isEmpty()) { @@ -243,12 +199,32 @@ public List getFilteredItemHistory(String levelName, FilterList fil preparedStatement.setString(6, "not null"); } - preparedStatement.setInt(7, filterList.getRadiusMinX()); - preparedStatement.setInt(8, filterList.getRadiusMaxX()); - preparedStatement.setInt(9, filterList.getRadiusMinY()); - preparedStatement.setInt(10, filterList.getRadiusMaxY()); - preparedStatement.setInt(11, filterList.getRadiusMinZ()); - preparedStatement.setInt(12, filterList.getRadiusMaxZ()); + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(7, Types.VARCHAR); + } else { + preparedStatement.setString(7, "not null"); + } + + preparedStatement.setInt(8, filterList.getRadiusMinX()); + preparedStatement.setInt(9, filterList.getRadiusMaxX()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(10, Types.VARCHAR); + } else { + preparedStatement.setString(10, "not null"); + } + + preparedStatement.setInt(11, filterList.getRadiusMinY()); + preparedStatement.setInt(12, filterList.getRadiusMaxY()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(13, Types.VARCHAR); + } else { + preparedStatement.setString(13, "not null"); + } + + preparedStatement.setInt(14, filterList.getRadiusMinZ()); + preparedStatement.setInt(15, filterList.getRadiusMaxZ()); List itemHistory = new ArrayList<>(); ResultSet resultSet = preparedStatement.executeQuery(); @@ -271,4 +247,4 @@ public List getFilteredItemHistory(String levelName, FilterList fil return List.of(); } } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/LevelRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/LevelRepository.java index 9bfb1af..77920fc 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/LevelRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/LevelRepository.java @@ -1,11 +1,12 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.SQLException; +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; + public class LevelRepository extends Repository { private final Database database; @@ -15,43 +16,26 @@ public LevelRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS levels ( - id integer PRIMARY KEY, - name text NOT NULL UNIQUE - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS levels ( - id int PRIMARY KEY AUTO_INCREMENT, - name varchar(256) NOT NULL UNIQUE - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS levels (" + + "id " + database.getDialect().getDataType("integer") + " PRIMARY KEY" + (database.getDialect() instanceof MySQLDialect ? " AUTO_INCREMENT" : "") + "," + + "name " + database.getDialect().getDataType("text") + " NOT NULL UNIQUE" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void insert(String name) { - String query = """ - INSERT OR IGNORE INTO levels(name) - VALUES(?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO levels(name) - VALUES(?); - """; - } - - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setString(1, name); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert level into database", exception); - } + String query = database.getDialect().getInsertIgnore() + " INTO levels(name) VALUES(?);"; + + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, name); + preparedStatement.executeUpdate(); + } + }); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/MaterialRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/MaterialRepository.java index 6257a53..6f8d7a5 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/MaterialRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/MaterialRepository.java @@ -1,11 +1,12 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.SQLException; +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; + public class MaterialRepository extends Repository { private final Database database; @@ -15,43 +16,26 @@ public MaterialRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS materials ( - id integer PRIMARY KEY, - name text NOT NULL UNIQUE - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS materials ( - id int PRIMARY KEY AUTO_INCREMENT, - name varchar(256) NOT NULL UNIQUE - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS materials (" + + "id " + database.getDialect().getDataType("integer") + " PRIMARY KEY" + (database.getDialect() instanceof MySQLDialect ? " AUTO_INCREMENT" : "") + "," + + "name " + database.getDialect().getDataType("text") + " NOT NULL UNIQUE" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void insert(String material) { - String query = """ - INSERT OR IGNORE INTO materials(name) - VALUES(?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO materials(name) - VALUES(?); - """; - } - - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setString(1, material); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert material into database", exception); - } + String query = database.getDialect().getInsertIgnore() + " INTO materials(name) VALUES(?);"; + + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, material); + preparedStatement.executeUpdate(); + } + }); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/Repository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/Repository.java index f794a06..45ea3eb 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/Repository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/Repository.java @@ -1,11 +1,6 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.config.GriefLoggerConfig; - public abstract class Repository implements IRepository { - @Override - public boolean isMysql() { - return GriefLoggerConfig.useMysql.get(); - } + } diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/SessionRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/SessionRepository.java index ac07102..f3c2e2e 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/SessionRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/SessionRepository.java @@ -1,14 +1,19 @@ package com.daqem.grieflogger.database.repository; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +import com.daqem.grieflogger.database.dialect.MySQLDialect; +import org.jetbrains.annotations.Nullable; + import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.command.filter.*; +import com.daqem.grieflogger.command.filter.FilterList; import com.daqem.grieflogger.database.Database; import com.daqem.grieflogger.model.history.SessionHistory; -import org.jetbrains.annotations.Nullable; - -import java.sql.*; -import java.util.ArrayList; -import java.util.List; public class SessionRepository extends Repository { @@ -19,84 +24,55 @@ public SessionRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS sessions ( - time integer NOT NULL, - user integer NOT NULL, - level integer NOT NULL, - x integer NOT NULL, - y integer NOT NULL, - z integer NOT NULL, - action integer NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS sessions ( - time bigint NOT NULL, - user int NOT NULL, - level int NOT NULL, - x int NOT NULL, - y int NOT NULL, - z int NOT NULL, - action int NOT NULL, - FOREIGN KEY(user) REFERENCES users(id), - FOREIGN KEY(level) REFERENCES levels(id) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS sessions (" + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "user " + database.getDialect().getDataType("integer") + " NOT NULL," + + "level " + database.getDialect().getDataType("integer") + " NOT NULL," + + "x " + database.getDialect().getDataType("integer") + " NOT NULL," + + "y " + database.getDialect().getDataType("integer") + " NOT NULL," + + "z " + database.getDialect().getDataType("integer") + " NOT NULL," + + "action " + database.getDialect().getDataType("integer") + " NOT NULL," + + "FOREIGN KEY(user) REFERENCES users(id)," + + "FOREIGN KEY(level) REFERENCES levels(id)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void createIndexes() { - String sql = """ - CREATE INDEX IF NOT EXISTS coordinates ON sessions (x, y, z); - """; - if (isMysql()) { - sql = """ - ALTER TABLE sessions ADD INDEX coordinates (x, y, z); - """; + String sql; + if (database.getDialect() instanceof MySQLDialect) { + sql = "ALTER TABLE sessions ADD INDEX coordinates (x, y, z);"; + } else { + sql = "CREATE INDEX IF NOT EXISTS coordinates ON sessions (x, y, z);"; } database.execute(sql, false); } public void insert(long time, String userUuid, String levelName, int x, int y, int z, int sessionAction) { - String query = """ - INSERT OR IGNORE INTO sessions(time, user, level, x, y, z, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO sessions(time, user, level, x, y, z, action) - VALUES(?, ( - SELECT id FROM users WHERE uuid = ? - ), ( - SELECT id FROM levels WHERE name = ? - ), ?, ?, ?, ?); - """; - } - - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setLong(1, time); - preparedStatement.setString(2, userUuid); - preparedStatement.setString(3, levelName); - preparedStatement.setInt(4, x); - preparedStatement.setInt(5, y); - preparedStatement.setInt(6, z); - preparedStatement.setInt(7, sessionAction); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert session into database", exception); - } + String query = database.getDialect().getInsertIgnore() + " INTO sessions(time, user, level, x, y, z, action) " + + "VALUES(?, (" + + "SELECT id FROM users WHERE uuid = ?" + + "), (" + + "SELECT id FROM levels WHERE name = ?" + + "), ?, ?, ?, ?);"; + + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setLong(1, time); + preparedStatement.setString(2, userUuid); + preparedStatement.setString(3, levelName); + preparedStatement.setInt(4, x); + preparedStatement.setInt(5, y); + preparedStatement.setInt(6, z); + preparedStatement.setInt(7, sessionAction); + preparedStatement.executeUpdate(); + } + }); } public List getFilteredSessionHistory(String levelName, FilterList filterList) { @@ -112,9 +88,9 @@ public List getFilteredSessionHistory(String levelName, FilterLi AND sessions.time > ? AND (? IS NULL OR sessions.action IN (%s)) AND (? IS NULL OR users.id IN (%s)) - AND sessions.x BETWEEN ? AND ? - AND sessions.y BETWEEN ? AND ? - AND sessions.z BETWEEN ? AND ? + AND (? IS NULL OR sessions.x BETWEEN ? AND ?) + AND (? IS NULL OR sessions.y BETWEEN ? AND ?) + AND (? IS NULL OR sessions.z BETWEEN ? AND ?) ORDER BY sessions.time DESC LIMIT 1000; """.formatted(actions, users); @@ -135,12 +111,32 @@ public List getFilteredSessionHistory(String levelName, FilterLi preparedStatement.setString(4, users); } - preparedStatement.setInt(5, filterList.getRadiusMinX()); - preparedStatement.setInt(6, filterList.getRadiusMaxX()); - preparedStatement.setInt(7, filterList.getRadiusMinY()); - preparedStatement.setInt(8, filterList.getRadiusMaxY()); - preparedStatement.setInt(9, filterList.getRadiusMinZ()); - preparedStatement.setInt(10, filterList.getRadiusMaxZ()); + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(5, Types.VARCHAR); + } else { + preparedStatement.setString(5, "not null"); + } + + preparedStatement.setInt(6, filterList.getRadiusMinX()); + preparedStatement.setInt(7, filterList.getRadiusMaxX()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(8, Types.VARCHAR); + } else { + preparedStatement.setString(8, "not null"); + } + + preparedStatement.setInt(9, filterList.getRadiusMinY()); + preparedStatement.setInt(10, filterList.getRadiusMaxY()); + + if (filterList.getRadiusFilter().isEmpty()) { + preparedStatement.setNull(11, Types.VARCHAR); + } else { + preparedStatement.setString(11, "not null"); + } + + preparedStatement.setInt(12, filterList.getRadiusMinZ()); + preparedStatement.setInt(13, filterList.getRadiusMaxZ()); List sessionHistory = new ArrayList<>(); ResultSet resultSet = preparedStatement.executeQuery(); @@ -160,4 +156,4 @@ public List getFilteredSessionHistory(String levelName, FilterLi return List.of(); } } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/UserRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/UserRepository.java index b17992c..a9b3558 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/UserRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/UserRepository.java @@ -1,12 +1,14 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.*; +import java.util.HashMap; +import java.util.Map; + +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; public class UserRepository extends Repository { @@ -17,76 +19,43 @@ public UserRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS users ( - id integer PRIMARY KEY, - name text NOT NULL, - uuid text DEFAULT NULL UNIQUE - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS users ( - id int PRIMARY KEY AUTO_INCREMENT, - name varchar(16) NOT NULL, - uuid varchar(36) DEFAULT NULL UNIQUE - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS users (" + + "id " + database.getDialect().getDataType("integer") + " PRIMARY KEY" + (database.getDialect() instanceof MySQLDialect ? " AUTO_INCREMENT" : "") + "," + + "name " + database.getDialect().getDataType("text") + " NOT NULL," + + "uuid " + database.getDialect().getDataType("text") + " DEFAULT NULL UNIQUE" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void insertOrUpdateName(String name, String uuid) { - String query = """ - INSERT INTO users(name, uuid) - VALUES(?, ?) - ON CONFLICT(uuid) - DO UPDATE SET name = ? - """; - - if (isMysql()) { - query = """ - INSERT INTO users(name, uuid) - VALUES(?, ?) - ON DUPLICATE KEY UPDATE name = ? - """; - } + String query = "INSERT INTO users(name, uuid) VALUES(?, ?) " + + database.getDialect().getOnConflictUpdate("uuid", "name = ?"); - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setString(1, name); - preparedStatement.setString(2, uuid); - preparedStatement.setString(3, name); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert username into database", exception); - } + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, name); + preparedStatement.setString(2, uuid); + preparedStatement.setString(3, name); + preparedStatement.executeUpdate(); + } + }); } public void insertNonPlayer(String name) { - String query = """ - INSERT INTO users(name) - VALUES('%s') - ON CONFLICT(name) - DO NOTHING - """; + String query = "INSERT INTO users(name) VALUES(?) " + + database.getDialect().getOnConflictDoNothing("name"); - if (isMysql()) { - query = """ - INSERT INTO users(name) - VALUES('%s') - ON DUPLICATE KEY UPDATE name = name - """; - } - - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setString(1, name); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert username into database", exception); - } + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setString(1, name); + preparedStatement.executeUpdate(); + } + }); } public Map getAllUsernames() { @@ -108,4 +77,4 @@ public Map getAllUsernames() { } return usernames; } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/repository/UsernameRepository.java b/common/src/main/java/com/daqem/grieflogger/database/repository/UsernameRepository.java index d86b72c..f0f2d95 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/repository/UsernameRepository.java +++ b/common/src/main/java/com/daqem/grieflogger/database/repository/UsernameRepository.java @@ -1,11 +1,12 @@ package com.daqem.grieflogger.database.repository; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.Database; - import java.sql.PreparedStatement; import java.sql.SQLException; +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.database.dialect.MySQLDialect; + public class UsernameRepository extends Repository { private final Database database; @@ -15,51 +16,31 @@ public UsernameRepository(Database database) { } public void createTable() { - String sql = """ - CREATE TABLE IF NOT EXISTS usernames ( - id integer PRIMARY KEY, - time integer NOT NULL, - uuid text NOT NULL, - name text NOT NULL, - UNIQUE(uuid, name) - ); - """; - if (isMysql()) { - sql = """ - CREATE TABLE IF NOT EXISTS usernames ( - id int PRIMARY KEY AUTO_INCREMENT, - time bigint NOT NULL, - uuid varchar(36) NOT NULL, - name varchar(16) NOT NULL, - UNIQUE(uuid, name) - ) - ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4; - """; + String sql = "CREATE TABLE IF NOT EXISTS usernames (" + + "id " + database.getDialect().getDataType("integer") + " PRIMARY KEY" + (database.getDialect() instanceof MySQLDialect ? " AUTO_INCREMENT" : "") + "," + + "time " + database.getDialect().getDataType("bigint") + " NOT NULL," + + "uuid " + database.getDialect().getDataType("varchar") + "(36) NOT NULL," + + "name " + database.getDialect().getDataType("varchar") + "(16) NOT NULL," + + "UNIQUE(uuid, name)" + + ")"; + if (database.getDialect() instanceof MySQLDialect) { + sql += " ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;"; + } else { + sql += ";"; } database.createTable(sql); } public void insert(long time, String uuid, String name) { - String query = """ - INSERT OR IGNORE INTO usernames(time, uuid, name) - VALUES(?, ?, ?); - """; - - if (isMysql()) { - query = """ - INSERT IGNORE INTO usernames(time, uuid, name) - VALUES(?, ?, ?); - """; - } - - try { - PreparedStatement preparedStatement = database.prepareStatement(query); - preparedStatement.setLong(1, time); - preparedStatement.setString(2, uuid); - preparedStatement.setString(3, name); - database.queue.add(preparedStatement); - } catch (SQLException exception) { - GriefLogger.LOGGER.error("Failed to insert username into database", exception); - } + String query = database.getDialect().getInsertIgnore() + " INTO usernames(time, uuid, name) VALUES(?, ?, ?);"; + + database.queue.add(connection -> { + try (PreparedStatement preparedStatement = connection.prepareStatement(query)) { + preparedStatement.setLong(1, time); + preparedStatement.setString(2, uuid); + preparedStatement.setString(3, name); + preparedStatement.executeUpdate(); + } + }); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/database/service/BlockService.java b/common/src/main/java/com/daqem/grieflogger/database/service/BlockService.java index 69f1274..a9d84d8 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/service/BlockService.java +++ b/common/src/main/java/com/daqem/grieflogger/database/service/BlockService.java @@ -1,5 +1,6 @@ package com.daqem.grieflogger.database.service; +import com.daqem.grieflogger.command.filter.ActionFilter; import com.daqem.grieflogger.command.filter.FilterList; import com.daqem.grieflogger.database.Database; import com.daqem.grieflogger.database.repository.BlockRepository; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; public class BlockService { @@ -104,6 +106,10 @@ public void removeInteractionsForPosition(Level level, BlockPos secondPos) { } public List getFilteredBlockHistory(Level level, FilterList filterList) { + Optional actionFilter = filterList.getActionFilter(); + if (actionFilter.isPresent() && actionFilter.get().getActions().stream().noneMatch(action -> action instanceof BlockAction)) { + return List.of(); + } return blockRepository.getFilteredBlockHistory( level.dimension().location().toString(), filterList diff --git a/common/src/main/java/com/daqem/grieflogger/database/service/ContainerService.java b/common/src/main/java/com/daqem/grieflogger/database/service/ContainerService.java index 5faaecc..16824b8 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/service/ContainerService.java +++ b/common/src/main/java/com/daqem/grieflogger/database/service/ContainerService.java @@ -1,19 +1,20 @@ package com.daqem.grieflogger.database.service; +import com.daqem.grieflogger.command.filter.ActionFilter; import com.daqem.grieflogger.command.filter.FilterList; import com.daqem.grieflogger.database.Database; import com.daqem.grieflogger.database.repository.ContainerRepository; import com.daqem.grieflogger.model.SimpleItemStack; +import com.daqem.grieflogger.model.action.IAction; import com.daqem.grieflogger.model.action.ItemAction; import com.daqem.grieflogger.model.history.IHistory; -import com.daqem.grieflogger.thread.OnComplete; -import com.daqem.grieflogger.thread.ThreadManager; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; public class ContainerService { @@ -37,7 +38,7 @@ public void insert(UUID userUuid, Level level, BlockPos pos, SimpleItemStack ite if (itemLocation != null) { containerRepository.insert(System.currentTimeMillis(), userUuid.toString(), - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ(), @@ -49,7 +50,7 @@ public void insert(UUID userUuid, Level level, BlockPos pos, SimpleItemStack ite public void insertList(UUID userUuid, Level level, BlockPos pos, List items, ItemAction itemAction) { containerRepository.insertList(System.currentTimeMillis(), userUuid.toString(), - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ(), @@ -60,7 +61,7 @@ public void insertList(UUID userUuid, Level level, BlockPos pos, List> itemsMap) { containerRepository.insertMap(System.currentTimeMillis(), userUuid.toString(), - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ(), @@ -69,7 +70,7 @@ public void insertMap(UUID userUuid, Level level, BlockPos pos, Map getHistory(Level level, BlockPos pos) { return containerRepository.getHistory( - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ() @@ -78,7 +79,7 @@ public List getHistory(Level level, BlockPos pos) { public List getHistory(Level level, BlockPos pos, BlockPos connectionPos) { return containerRepository.getHistory( - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ(), @@ -89,9 +90,17 @@ public List getHistory(Level level, BlockPos pos, BlockPos connectionP } public List getFilteredContainerHistory(Level level, FilterList filterList) { + Optional actionFilter = filterList.getActionFilter(); + if ((actionFilter.isPresent() && actionFilter.get().getActions().stream().noneMatch(ContainerService::isValidItemAction))) { + return List.of(); + } return containerRepository.getFilteredContainerHistory( - level.dimension().location().toString(), + level, filterList ); } + + private static boolean isValidItemAction(IAction action) { + return action.equals(ItemAction.ADD_ITEM) || action.equals(ItemAction.REMOVE_ITEM); + } } diff --git a/common/src/main/java/com/daqem/grieflogger/database/service/ItemService.java b/common/src/main/java/com/daqem/grieflogger/database/service/ItemService.java index b7d3ec3..cb7a739 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/service/ItemService.java +++ b/common/src/main/java/com/daqem/grieflogger/database/service/ItemService.java @@ -1,18 +1,20 @@ package com.daqem.grieflogger.database.service; +import com.daqem.grieflogger.command.filter.ActionFilter; import com.daqem.grieflogger.command.filter.FilterList; import com.daqem.grieflogger.database.Database; import com.daqem.grieflogger.database.repository.ItemRepository; import com.daqem.grieflogger.model.SimpleItemStack; +import com.daqem.grieflogger.model.action.IAction; import com.daqem.grieflogger.model.action.ItemAction; import com.daqem.grieflogger.model.history.ItemHistory; -import com.daqem.grieflogger.thread.ThreadManager; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; public class ItemService { @@ -36,7 +38,7 @@ public void insert(UUID userUuid, Level level, BlockPos pos, SimpleItemStack ite if (itemLocation != null) { itemRepository.insert(System.currentTimeMillis(), userUuid.toString(), - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ(), @@ -48,7 +50,7 @@ public void insert(UUID userUuid, Level level, BlockPos pos, SimpleItemStack ite public void insertMap(UUID userUuid, Level level, BlockPos pos, Map> itemsMap) { itemRepository.insertMap(System.currentTimeMillis(), userUuid.toString(), - level.dimension().location().toString(), + level, pos.getX(), pos.getY(), pos.getZ(), @@ -56,9 +58,17 @@ public void insertMap(UUID userUuid, Level level, BlockPos pos, Map getFilteredItemHistory(Level level, FilterList filterList) { + Optional actionFilter = filterList.getActionFilter(); + if (actionFilter.isPresent() && actionFilter.get().getActions().stream().noneMatch(ItemService::isValidItemAction)) { + return List.of(); + } return itemRepository.getFilteredItemHistory( - level.dimension().location().toString(), + level, filterList ); } + + private static boolean isValidItemAction(IAction action) { + return action instanceof ItemAction && (!action.equals(ItemAction.ADD_ITEM) || !action.equals(ItemAction.REMOVE_ITEM)); + } } diff --git a/common/src/main/java/com/daqem/grieflogger/database/service/SessionService.java b/common/src/main/java/com/daqem/grieflogger/database/service/SessionService.java index 68ca447..5a56d0e 100644 --- a/common/src/main/java/com/daqem/grieflogger/database/service/SessionService.java +++ b/common/src/main/java/com/daqem/grieflogger/database/service/SessionService.java @@ -1,15 +1,16 @@ package com.daqem.grieflogger.database.service; +import com.daqem.grieflogger.command.filter.ActionFilter; import com.daqem.grieflogger.command.filter.FilterList; import com.daqem.grieflogger.database.Database; import com.daqem.grieflogger.database.repository.SessionRepository; import com.daqem.grieflogger.model.action.SessionAction; import com.daqem.grieflogger.model.history.SessionHistory; -import com.daqem.grieflogger.thread.ThreadManager; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import java.util.List; +import java.util.Optional; import java.util.UUID; public class SessionService { @@ -39,6 +40,15 @@ public void insert(UUID userUuid, Level level, BlockPos pos, SessionAction sessi } public List getFilteredSessionHistory(Level level, FilterList filterList) { + Optional actionFilter = filterList.getActionFilter(); + if ((actionFilter.isPresent() && actionFilter.get().getActions().stream().noneMatch(action -> action instanceof SessionAction)) + || + (filterList.getIncludeFilter().isPresent()) + || + (filterList.getExcludeFilter().isPresent()) + ) { + return List.of(); + } return sessionRepository.getFilteredSessionHistory( level.dimension().location().toString(), filterList diff --git a/common/src/main/java/com/daqem/grieflogger/event/EntityEvents.java b/common/src/main/java/com/daqem/grieflogger/event/EntityEvents.java index e33f84b..afd2313 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/EntityEvents.java +++ b/common/src/main/java/com/daqem/grieflogger/event/EntityEvents.java @@ -11,7 +11,7 @@ public class EntityEvents { public static void registerEvents() { EntityEvent.LIVING_DEATH.register((entity, source) -> { - if (source.getEntity() instanceof ServerPlayer serverPlayer) { + if (source != null && source.getEntity() instanceof ServerPlayer serverPlayer) { ResourceLocation entityLocation = entity.getType().arch$registryName(); if (entityLocation != null) { Services.BLOCK.insertEntity( diff --git a/common/src/main/java/com/daqem/grieflogger/event/ServerStartedEvent.java b/common/src/main/java/com/daqem/grieflogger/event/ServerStartedEvent.java new file mode 100644 index 0000000..98cb79d --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/event/ServerStartedEvent.java @@ -0,0 +1,12 @@ +package com.daqem.grieflogger.event; + +import com.daqem.grieflogger.i18n.LanguageManager; + +import dev.architectury.event.events.common.LifecycleEvent; + +public class ServerStartedEvent { + + public static void registerEvent() { + LifecycleEvent.SERVER_STARTED.register(LanguageManager::load); + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/event/ServerStoppedEvent.java b/common/src/main/java/com/daqem/grieflogger/event/ServerStoppedEvent.java new file mode 100644 index 0000000..5090a11 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/event/ServerStoppedEvent.java @@ -0,0 +1,28 @@ +package com.daqem.grieflogger.event; + +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.database.Database; +import com.daqem.grieflogger.thread.ThreadManager; +import dev.architectury.event.events.common.LifecycleEvent; + +public class ServerStoppedEvent { + + public static void registerEvent() { + LifecycleEvent.SERVER_STOPPED.register(server -> { + GriefLogger.LOGGER.info("Stopping GriefLogger threads..."); + + Database database = GriefLogger.getDatabase(); + if (database != null) { + GriefLogger.LOGGER.info("Flushing final database queue..."); + try { + database.queue.execute(); + database.batchQueue.execute(); + } catch (Exception e) { + GriefLogger.LOGGER.error("Failed to flush database queue on shutdown", e); + } + } + + ThreadManager.shutdown(); + }); + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/event/TickEvents.java b/common/src/main/java/com/daqem/grieflogger/event/TickEvents.java index 1aedb5d..fc67abf 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/TickEvents.java +++ b/common/src/main/java/com/daqem/grieflogger/event/TickEvents.java @@ -23,23 +23,18 @@ public static void registerEvents() { } } ); - if (lastTick % GriefLoggerConfig.queueFrequency.get() == 0) { - ThreadManager.execute(() -> { - Database database = GriefLogger.getDatabase(); - database.queue.execute(); - database.batchQueue.execute(); - }); - } - if (lastTick % GriefLoggerConfig.helloFrequency.get() == 0) { - ThreadManager.execute(() -> { - //Send hello packet to server to keep connection alive - Database database = GriefLogger.getDatabase(); - database.queue.hello(); - }); + if (lastTick % GriefLoggerConfig.queueFrequency.get() == 0) { + Database database = GriefLogger.getDatabase(); + if (database != null && (!database.queue.isEmpty() || !database.batchQueue.isEmpty())) { + ThreadManager.execute(() -> { + database.queue.execute(); + database.batchQueue.execute(); + }); + } } lastTick++; }); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/event/block/BlockEvents.java b/common/src/main/java/com/daqem/grieflogger/event/block/BlockEvents.java index e4f6e1d..69e6349 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/block/BlockEvents.java +++ b/common/src/main/java/com/daqem/grieflogger/event/block/BlockEvents.java @@ -1,5 +1,6 @@ package com.daqem.grieflogger.event.block; +import dev.architectury.event.EventResult; import dev.architectury.event.events.common.BlockEvent; import dev.architectury.event.events.common.InteractionEvent; @@ -7,7 +8,10 @@ public class BlockEvents { public static void registerEvents() { BlockEvent.BREAK.register(BreakBlockEvent::breakBlock); - BlockEvent.PLACE.register(PlaceBlockEvent::placeBlock); + BlockEvent.PLACE.register(((level, blockPos, blockState, entity) -> { + PlaceBlockEvent.placeBlock(level, blockPos, blockState, entity); + return EventResult.pass(); + })); InteractionEvent.LEFT_CLICK_BLOCK.register(LeftClickBlockEvent::leftClickBlock); InteractionEvent.RIGHT_CLICK_BLOCK.register(RightClickBlockEvent::rightClickBlock); } diff --git a/common/src/main/java/com/daqem/grieflogger/event/block/BreakBlockEvent.java b/common/src/main/java/com/daqem/grieflogger/event/block/BreakBlockEvent.java index 486c4f5..0340284 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/block/BreakBlockEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/block/BreakBlockEvent.java @@ -16,6 +16,9 @@ import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; +// [ADD THESE IMPORTS] + + public class BreakBlockEvent extends AbstractEvent { public static EventResult breakBlock(Level level, BlockPos pos, BlockState state, ServerPlayer player, @Nullable IntValue xp) { @@ -25,7 +28,7 @@ public static EventResult breakBlock(Level level, BlockPos pos, BlockState state } Block block = state.getBlock(); - if (BlockHandler.isBlockIntractable(block)) { + if (BlockHandler.isBlockInteractable(block)) { if (block instanceof DoorBlock) { RemoveDoorInteractionsEvent.removeDoorInteractions(level, pos, state); } else { @@ -42,4 +45,4 @@ public static EventResult breakBlock(Level level, BlockPos pos, BlockState state } return pass(); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/event/block/InspectDoorEvent.java b/common/src/main/java/com/daqem/grieflogger/event/block/InspectDoorEvent.java index e8db9bf..b041001 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/block/InspectDoorEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/block/InspectDoorEvent.java @@ -1,8 +1,6 @@ package com.daqem.grieflogger.event.block; -import com.daqem.grieflogger.GriefLogger; import com.daqem.grieflogger.block.BlockHandler; -import com.daqem.grieflogger.database.service.BlockService; import com.daqem.grieflogger.database.service.Services; import com.daqem.grieflogger.event.AbstractEvent; import com.daqem.grieflogger.player.GriefLoggerServerPlayer; diff --git a/common/src/main/java/com/daqem/grieflogger/event/block/PlaceBlockEvent.java b/common/src/main/java/com/daqem/grieflogger/event/block/PlaceBlockEvent.java index 84ac5ba..fab2459 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/block/PlaceBlockEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/block/PlaceBlockEvent.java @@ -1,8 +1,7 @@ package com.daqem.grieflogger.event.block; import com.daqem.grieflogger.model.action.BlockAction; -import com.daqem.grieflogger.player.GriefLoggerServerPlayer; -import dev.architectury.event.EventResult; +import com.daqem.grieflogger.util.EntityUtils; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; @@ -10,10 +9,7 @@ public class PlaceBlockEvent { - public static EventResult placeBlock(Level level, BlockPos pos, BlockState state, Entity placer) { - if (placer instanceof GriefLoggerServerPlayer serverPlayer) { - LogBlockEvent.logBlock(serverPlayer, level, state, pos, BlockAction.PLACE_BLOCK); - } - return EventResult.pass(); + public static void placeBlock(Level level, BlockPos pos, BlockState state, Entity placer) { + EntityUtils.logBlockAction(level, pos, state, placer, placer, BlockAction.PLACE_BLOCK); } } diff --git a/common/src/main/java/com/daqem/grieflogger/event/block/RightClickBlockEvent.java b/common/src/main/java/com/daqem/grieflogger/event/block/RightClickBlockEvent.java index 5936496..e0fc787 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/block/RightClickBlockEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/block/RightClickBlockEvent.java @@ -75,7 +75,7 @@ public static EventResult rightClickBlock(Player player, InteractionHand hand, B return InspectBlockEvent.inspectBlock(serverPlayer, pos.relative(direction)); } - if (BlockHandler.isBlockIntractable(block)) { + if (BlockHandler.isBlockInteractable(block)) { LogBlockEvent.logBlock(serverPlayer, level, state, pos, BlockAction.INTERACT_BLOCK); } } diff --git a/common/src/main/java/com/daqem/grieflogger/event/item/DropItemEvent.java b/common/src/main/java/com/daqem/grieflogger/event/item/DropItemEvent.java index eaedfa8..3df75cd 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/item/DropItemEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/item/DropItemEvent.java @@ -10,10 +10,9 @@ public class DropItemEvent extends AbstractEvent { - public static EventResult onDropItem(Player player, ItemEntity itemEntity) { + public static void onDropItem(Player player, ItemEntity itemEntity) { if (player instanceof GriefLoggerServerPlayer serverPlayer) { serverPlayer.griefLogger$addItemToQueue(ItemAction.DROP_ITEM, new SimpleItemStack(itemEntity.getItem())); } - return pass(); } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/event/item/ItemEvents.java b/common/src/main/java/com/daqem/grieflogger/event/item/ItemEvents.java index 6510986..7a9569c 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/item/ItemEvents.java +++ b/common/src/main/java/com/daqem/grieflogger/event/item/ItemEvents.java @@ -6,7 +6,6 @@ public class ItemEvents { public static void registerEvents() { PlayerEvent.CRAFT_ITEM.register(CraftItemEvent::onCraftItem); - PlayerEvent.DROP_ITEM.register(DropItemEvent::onDropItem); PlayerEvent.PICKUP_ITEM_POST.register(PickupItemEvent::onPickupItem); PlayerEvent.SMELT_ITEM.register(SmeltItemEvent::onSmeltItem); } diff --git a/common/src/main/java/com/daqem/grieflogger/event/item/ShootItemEvent.java b/common/src/main/java/com/daqem/grieflogger/event/item/ShootItemEvent.java index 8f10a51..0cb6227 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/item/ShootItemEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/item/ShootItemEvent.java @@ -8,7 +8,7 @@ public class ShootItemEvent { public static void shootItem(Player player, ItemStack itemStack) { - if (player instanceof GriefLoggerServerPlayer serverPlayer) { + if (player instanceof GriefLoggerServerPlayer serverPlayer && itemStack != null) { serverPlayer.griefLogger$addItemToQueue(ItemAction.SHOOT_ITEM, new SimpleItemStack(itemStack)); } } diff --git a/common/src/main/java/com/daqem/grieflogger/event/item/ThrowItemEvent.java b/common/src/main/java/com/daqem/grieflogger/event/item/ThrowItemEvent.java index 0574f14..b828e24 100644 --- a/common/src/main/java/com/daqem/grieflogger/event/item/ThrowItemEvent.java +++ b/common/src/main/java/com/daqem/grieflogger/event/item/ThrowItemEvent.java @@ -8,7 +8,7 @@ public class ThrowItemEvent { public static void throwItem(Player player, ItemStack itemStack) { - if (player instanceof GriefLoggerServerPlayer serverPlayer) { + if (player instanceof GriefLoggerServerPlayer serverPlayer && itemStack != null) { serverPlayer.griefLogger$addItemToQueue(ItemAction.THROW_ITEM, new SimpleItemStack(itemStack)); } } diff --git a/common/src/main/java/com/daqem/grieflogger/i18n/LanguageManager.java b/common/src/main/java/com/daqem/grieflogger/i18n/LanguageManager.java new file mode 100644 index 0000000..5aa6de6 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/i18n/LanguageManager.java @@ -0,0 +1,179 @@ +package com.daqem.grieflogger.i18n; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.Map; + +import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.config.GriefLoggerConfig; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import com.supermartijn642.configlib.ConfigBuilder; +import com.supermartijn642.configlib.ConfigLib; +import dev.architectury.platform.Platform; +import net.minecraft.locale.Language; +import net.minecraft.server.MinecraftServer; + +public class LanguageManager { + + private static final Map TRANSLATIONS = new HashMap<>(); + public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + public static void load(MinecraftServer server) { + TRANSLATIONS.clear(); + GriefLoggerConfig.Language language = GriefLoggerConfig.language.get(); + + for (String modId : Platform.getModIds()) { + if (!language.equals(GriefLoggerConfig.Language.en_us)) { + if (modId.equals("minecraft")) { + loadLanguage(server, language.toString()); + } else { + loadLanguage(modId, language.toString()); + } + } + loadLanguage(modId, "en_us"); + } + + GriefLogger.LOGGER.info("Loaded {} translations for language: {}", TRANSLATIONS.size(), language); + } + + private static void loadLanguage(String modId, String language) { + String location = "/assets/" + modId + "/lang/" + language + ".json"; + try (InputStream stream = getInputStream(location)) { + if (stream == null) { + return; + } + Language.loadFromJson(stream, TRANSLATIONS::putIfAbsent); + } catch (Exception e) { + GriefLogger.LOGGER.error("Failed to load language file for mod: {}, language: {}", modId, language, e); + } + } + + private static InputStream getInputStream(String location) { + if (Platform.isFabric()) { + return Language.class.getResourceAsStream(location); + } + return Thread.currentThread().getContextClassLoader().getResourceAsStream(location); + } + + private static void loadLanguage(MinecraftServer server, String languageCode) { + try { + String mcVersion = server.getServerVersion(); + + Path cacheDir = ConfigLib.getConfigFolder().toPath().resolve(GriefLogger.MOD_ID).resolve("lang_cache"); + Files.createDirectories(cacheDir); + + Path cacheFile = cacheDir.resolve(languageCode + ".json"); + + // 1. Get asset index URL for current version + String assetIndexUrl = getAssetIndexUrl(mcVersion); + + // 2. Get hash for the specific lang file + String hash = getLanguageHash(assetIndexUrl, "minecraft/lang/" + languageCode + ".json"); + String prefix = hash.substring(0, 2); + String assetUrl = "https://resources.download.minecraft.net/" + prefix + "/" + hash; + + // 3. Download if missing or hash mismatch + if (Files.notExists(cacheFile) || !fileHash(cacheFile).equalsIgnoreCase(hash)) { + downloadFile(assetUrl, cacheFile); + } + + // 4. Parse and cache + String json = Files.readString(cacheFile); + Map translations = GSON.fromJson(json, TypeToken.getParameterized(Map.class, String.class, String.class).getType()); + + TRANSLATIONS.putAll(translations); + } catch (Exception e) { + GriefLogger.LOGGER.error("Failed to download Minecraft language file for language: {}", languageCode, e); + } + } + + private static String getAssetIndexUrl(String mcVersion) throws Exception { + String manifestJson = get("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"); + + JsonObject manifest = GSON.fromJson(manifestJson, JsonObject.class); + String versionUrl = manifest.getAsJsonArray("versions").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(v -> mcVersion.equals(v.get("id").getAsString())) + .findFirst() + .orElseThrow(() -> new IOException("Version " + mcVersion + " not found in manifest")) + .get("url").getAsString(); + + JsonObject versionJson = GSON.fromJson(get(versionUrl), JsonObject.class); + return versionJson.getAsJsonObject("assetIndex").get("url").getAsString(); + } + + private static String getLanguageHash(String assetIndexUrl, String assetPath) throws Exception { + JsonObject index = GSON.fromJson(get(assetIndexUrl), JsonObject.class); + JsonObject objects = index.getAsJsonObject("objects"); + + if (!objects.has(assetPath)) { + throw new IOException("Language file not found in asset index: " + assetPath); + } + + return objects.getAsJsonObject(assetPath) + .get("hash").getAsString(); + } + + private static String get(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .header("User-Agent", "Minecraft-Server-Mod/1.0 (Java 21)") + .build(); + + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() >= 400) { + throw new IOException("HTTP " + response.statusCode() + " for " + url); + } + return response.body(); + } + + private static void downloadFile(String url, Path destination) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .header("User-Agent", "Minecraft-Server-Mod/1.0") + .build(); + + HttpResponse response = HTTP_CLIENT.send(request, + HttpResponse.BodyHandlers.ofFile(destination)); + + if (response.statusCode() >= 400) { + throw new IOException("Failed to download " + url + " (HTTP " + response.statusCode() + ")"); + } + } + + private static String fileHash(Path file) throws IOException { + byte[] data = Files.readAllBytes(file); + MessageDigest sha1; + try { + sha1 = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + return HexFormat.of().formatHex(sha1.digest(data)).toLowerCase(); + } + + public static String getString(String key) { + return TRANSLATIONS.getOrDefault(key, key); + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/ItemMixin.java b/common/src/main/java/com/daqem/grieflogger/mixin/ItemMixin.java index 018e543..24815ff 100644 --- a/common/src/main/java/com/daqem/grieflogger/mixin/ItemMixin.java +++ b/common/src/main/java/com/daqem/grieflogger/mixin/ItemMixin.java @@ -1,9 +1,6 @@ package com.daqem.grieflogger.mixin; import com.daqem.grieflogger.event.item.ConsumeItemEvent; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinArmorStand.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinArmorStand.java new file mode 100644 index 0000000..c48e52e --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinArmorStand.java @@ -0,0 +1,42 @@ +package com.daqem.grieflogger.mixin; + +import com.daqem.grieflogger.database.service.Services; +import com.daqem.grieflogger.model.action.BlockAction; +import com.daqem.grieflogger.player.GriefLoggerServerPlayer; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ArmorStand.class) +public abstract class MixinArmorStand extends LivingEntity { + + protected MixinArmorStand(EntityType entityType, Level level) { + super(entityType, level); + } + + @Inject(method = "interactAt", at = @At("RETURN")) + private void onInteractAt(Player player, Vec3 vec3, InteractionHand interactionHand, CallbackInfoReturnable cir) { + if ((cir.getReturnValue() == InteractionResult.SUCCESS) && player instanceof GriefLoggerServerPlayer glPlayer) { + ResourceLocation entityLocation = this.getType().arch$registryName(); + if (entityLocation != null) { + Services.BLOCK.insertEntity( + glPlayer.grieflogger$asServerPlayer().getUUID(), + this.level().dimension().location().toString(), + this.blockPosition(), + entityLocation.toString(), + BlockAction.INTERACT_ENTITY + ); + } + } + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinBucketItem.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinBucketItem.java new file mode 100644 index 0000000..2bf5f16 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinBucketItem.java @@ -0,0 +1,36 @@ +package com.daqem.grieflogger.mixin; + +import com.daqem.grieflogger.event.block.BreakBlockEvent; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import com.llamalad7.mixinextras.sugar.Local; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BucketItem.class) +public class MixinBucketItem { + + @Inject( + method = "use", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;awardStat(Lnet/minecraft/stats/Stat;)V", + ordinal = 0 + ) + ) + private void onBucketFilled(Level level, Player player, InteractionHand interactionHand, + CallbackInfoReturnable cir, @Local(ordinal = 1) ItemStack filledBucket, @Local BlockHitResult hitResult + ) { + if (player instanceof ServerPlayer serverPlayer && filledBucket.getItem() instanceof BucketItem bucketItem) { + BreakBlockEvent.breakBlock(level, hitResult.getBlockPos(), bucketItem.arch$getFluid().defaultFluidState().createLegacyBlock(), serverPlayer, null); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinEnderManLeaveBlockGoal.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinEnderManLeaveBlockGoal.java new file mode 100644 index 0000000..089ff9a --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinEnderManLeaveBlockGoal.java @@ -0,0 +1,38 @@ +package com.daqem.grieflogger.mixin; + + +import com.daqem.grieflogger.model.action.BlockAction; +import com.daqem.grieflogger.util.EntityUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.monster.EnderMan; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(targets = "net.minecraft.world.entity.monster.EnderMan$EndermanLeaveBlockGoal") +public class MixinEnderManLeaveBlockGoal { + + @Shadow + @Final + private EnderMan enderman; + + @Inject( + method = "tick()V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z", + shift = At.Shift.AFTER + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private void onEndermanTakeBlock(CallbackInfo ci, RandomSource randomsource, Level level, int i, int j, int k, BlockPos blockpos, BlockState blockstate, BlockPos blockpos1, BlockState blockstate1, BlockState blockstate2) { + EntityUtils.logBlockAction(level, blockpos, blockstate2, this.enderman, this.enderman, BlockAction.PLACE_BLOCK); + } +} diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinEnderManTakeBlockGoal.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinEnderManTakeBlockGoal.java new file mode 100644 index 0000000..d1b9fd9 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinEnderManTakeBlockGoal.java @@ -0,0 +1,38 @@ +package com.daqem.grieflogger.mixin; + +import com.daqem.grieflogger.model.action.BlockAction; +import com.daqem.grieflogger.util.EntityUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.monster.EnderMan; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(targets = "net.minecraft.world.entity.monster.EnderMan$EndermanTakeBlockGoal") +public class MixinEnderManTakeBlockGoal { + + @Shadow + @Final + private EnderMan enderman; + + @Inject( + method = "tick()V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;removeBlock(Lnet/minecraft/core/BlockPos;Z)Z" + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private void onEndermanTakeBlock(CallbackInfo ci, RandomSource randomsource, Level level, int i, int j, int k, BlockPos blockpos, BlockState blockstate, Vec3 vec3, Vec3 vec31, BlockHitResult blockhitresult, boolean flag) { + EntityUtils.logBlockAction(level, blockpos, blockstate, this.enderman, this.enderman, BlockAction.BREAK_BLOCK); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinItemStack.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinItemStack.java index d71c60b..28de0e2 100644 --- a/common/src/main/java/com/daqem/grieflogger/mixin/MixinItemStack.java +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinItemStack.java @@ -1,10 +1,6 @@ package com.daqem.grieflogger.mixin; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.service.ItemService; import com.daqem.grieflogger.event.item.BreakItemEvent; -import com.daqem.grieflogger.model.SimpleItemStack; -import com.daqem.grieflogger.model.action.ItemAction; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerExplosion.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerExplosion.java new file mode 100644 index 0000000..8bc5726 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerExplosion.java @@ -0,0 +1,50 @@ +package com.daqem.grieflogger.mixin; + +import com.daqem.grieflogger.model.action.BlockAction; +import com.daqem.grieflogger.util.EntityUtils; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Explosion.class) +public abstract class MixinServerExplosion { + @Shadow @Final private Level level; + @Shadow @Nullable public Entity source; + @Shadow @Final private ObjectArrayList toBlow; + + @Shadow public abstract @Nullable LivingEntity getIndirectSourceEntity(); + + @Inject(method = "finalizeExplosion", at = @At("HEAD")) + private void onInteractWithBlocks(boolean spawnParticles, CallbackInfo ci) { + if (!(this.level instanceof ServerLevel serverLevel)) return; + + // actor = Who to blame (Player or the Mob itself) + Entity actor = this.getIndirectSourceEntity(); + if (actor == null) actor = this.source; + + // original = The tool used (TNT, Creeper, etc.) + Entity original = this.source; + + if (actor == null || original == null) return; + + for (BlockPos pos : this.toBlow) { + BlockState state = serverLevel.getBlockState(pos); + if (!state.isAir()) { + // Pass both to our new utility + EntityUtils.logBlockAction(serverLevel, pos, state, actor, original, BlockAction.BREAK_BLOCK); + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerPlayer.java b/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerPlayer.java index fca6403..87b6381 100644 --- a/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerPlayer.java +++ b/common/src/main/java/com/daqem/grieflogger/mixin/MixinServerPlayer.java @@ -7,6 +7,7 @@ import com.daqem.grieflogger.block.container.IContainerTransactionManager; import com.daqem.grieflogger.command.page.Page; import com.daqem.grieflogger.database.service.Services; +import com.daqem.grieflogger.event.item.DropItemEvent; import com.daqem.grieflogger.model.SimpleItemStack; import com.daqem.grieflogger.model.action.ItemAction; import com.daqem.grieflogger.model.history.IHistory; @@ -18,7 +19,9 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; import org.spongepowered.asm.mixin.Mixin; @@ -62,7 +65,8 @@ public MixinServerPlayer(Level level, BlockPos blockPos, float f, GameProfile ga @Unique public void grieflogger$sendInspectMessage(List historyList) { if (historyList.isEmpty()) { - sendSystemMessage(GriefLogger.translate("lookup.no_history", GriefLogger.getName())); + if (((Player) this) instanceof ServerPlayer serverPlayer) + serverPlayer.sendSystemMessage(GriefLogger.translate("lookup.no_history", GriefLogger.getName())); } else { List pages = Page.convertToPages(historyList, true); grieflogger$setPages(pages); @@ -89,14 +93,17 @@ public MixinServerPlayer(Level level, BlockPos blockPos, float f, GameProfile ga @Inject(at = @At("HEAD"), method = "openMenu") public void openMenu(MenuProvider menuProvider, CallbackInfoReturnable cir) { - Optional container = ContainerHandler.getContainer(menuProvider); - if (container.isPresent()) { - this.grieflogger$containerTransactionManager = new ContainerTransactionManager(container.get()); - } else { - ContainerHandler.getContainers(menuProvider).ifPresent(containers -> { - this.grieflogger$containerTransactionManager = new ContainersTransactionManager(containers); - }); - } + EnvExecutor.getInEnv(EnvType.SERVER, () -> () -> { + Optional container = ContainerHandler.getContainer(menuProvider); + if (container.isPresent()) { + this.grieflogger$containerTransactionManager = new ContainerTransactionManager(container.get()); + } else { + ContainerHandler.getContainers(menuProvider).ifPresent(containers -> { + this.grieflogger$containerTransactionManager = new ContainersTransactionManager(containers); + }); + } + return null; + }); } @Inject(at = @At("HEAD"), method = "doCloseContainer()V") @@ -121,6 +128,16 @@ public void openMenu(MenuProvider menuProvider, CallbackInfoReturnable cir) { + EnvExecutor.getInEnv(EnvType.SERVER, () -> () -> { + if (cir.getReturnValue() != null) { + DropItemEvent.onDropItem(this, cir.getReturnValue()); + } + return null; + }); + } + public void griefLogger$addItemToQueue(ItemAction action, SimpleItemStack itemStack) { List itemStacks = grieflogger$itemQueue.get(action); if (itemStacks != null) { diff --git a/common/src/main/java/com/daqem/grieflogger/mixin/ProjectileMixin.java b/common/src/main/java/com/daqem/grieflogger/mixin/ProjectileMixin.java index 059d287..57034dc 100644 --- a/common/src/main/java/com/daqem/grieflogger/mixin/ProjectileMixin.java +++ b/common/src/main/java/com/daqem/grieflogger/mixin/ProjectileMixin.java @@ -1,24 +1,20 @@ package com.daqem.grieflogger.mixin; -import com.daqem.grieflogger.GriefLogger; -import com.daqem.grieflogger.database.service.ItemService; import com.daqem.grieflogger.event.item.ShootItemEvent; import com.daqem.grieflogger.event.item.ThrowItemEvent; -import com.daqem.grieflogger.model.SimpleItemStack; -import com.daqem.grieflogger.model.action.ItemAction; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.AbstractArrow; import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.entity.projectile.ThrowableItemProjectile; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(Projectile.class) public class ProjectileMixin { - @Inject(at = @At("HEAD"), method = "shootFromRotation(Lnet/minecraft/world/entity/Entity;FFFFF)V") private void shootFromRotation(Entity entity, float f, float g, float h, float i, float j, CallbackInfo ci) { if (entity instanceof Player player) { diff --git a/common/src/main/java/com/daqem/grieflogger/model/SimpleItemStack.java b/common/src/main/java/com/daqem/grieflogger/model/SimpleItemStack.java index 9a47f63..da4f8ea 100644 --- a/common/src/main/java/com/daqem/grieflogger/model/SimpleItemStack.java +++ b/common/src/main/java/com/daqem/grieflogger/model/SimpleItemStack.java @@ -99,4 +99,4 @@ public ItemStack toItemStack() { public boolean isEmpty() { return item.equals(ItemStack.EMPTY.getItem()) || count == 0; } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/model/Time.java b/common/src/main/java/com/daqem/grieflogger/model/Time.java index 84d4f73..98cf02b 100644 --- a/common/src/main/java/com/daqem/grieflogger/model/Time.java +++ b/common/src/main/java/com/daqem/grieflogger/model/Time.java @@ -40,6 +40,6 @@ private MutableComponent getTimeAgoComponent(double timeAgo, Component unit) { .withColor(ChatFormatting.GRAY) .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, GriefLogger.literal(new Date(time).toString()) - .withStyle(ChatFormatting.GRAY)))); + .withStyle(ChatFormatting.GRAY)))); } } diff --git a/common/src/main/java/com/daqem/grieflogger/model/action/BlockAction.java b/common/src/main/java/com/daqem/grieflogger/model/action/BlockAction.java index 77eeba2..80bf945 100644 --- a/common/src/main/java/com/daqem/grieflogger/model/action/BlockAction.java +++ b/common/src/main/java/com/daqem/grieflogger/model/action/BlockAction.java @@ -6,7 +6,8 @@ public enum BlockAction implements IAction { BREAK_BLOCK(0, Operation.REMOVE), PLACE_BLOCK(1, Operation.ADD), INTERACT_BLOCK(2, Operation.NEUTRAL), - KILL_ENTITY(3, Operation.REMOVE); + KILL_ENTITY(3, Operation.REMOVE), + INTERACT_ENTITY(4, Operation.NEUTRAL); private final int id; private final Operation operation; @@ -34,4 +35,4 @@ public static BlockAction fromId(int id) { } return null; } -} +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/model/history/BlockHistory.java b/common/src/main/java/com/daqem/grieflogger/model/history/BlockHistory.java index 03b9d87..5bd43c9 100644 --- a/common/src/main/java/com/daqem/grieflogger/model/history/BlockHistory.java +++ b/common/src/main/java/com/daqem/grieflogger/model/history/BlockHistory.java @@ -1,6 +1,7 @@ package com.daqem.grieflogger.model.history; import com.daqem.grieflogger.GriefLogger; +import com.daqem.grieflogger.i18n.LanguageManager; import com.daqem.grieflogger.model.BlockPosition; import com.daqem.grieflogger.model.Time; import com.daqem.grieflogger.model.User; @@ -10,6 +11,10 @@ import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import java.util.UUID; @@ -37,17 +42,34 @@ public Component getComponent() { } public Component getMaterialComponent() { - MutableComponent mutableComponent = GriefLogger.themedLiteral(this.material.replace("minecraft:", "")); - return mutableComponent - .withStyle(mutableComponent - .getStyle() - .withHoverEvent( - new HoverEvent( + ResourceLocation id = ResourceLocation.tryParse(material); + Block block = id != null ? BuiltInRegistries.BLOCK.get(id) : Blocks.AIR; + Item item = block.asItem(); + MutableComponent mutableComponent; + if (block != Blocks.AIR) { + mutableComponent = GriefLogger.themedLiteral(LanguageManager.getString(block.getDescriptionId())); + } else { + mutableComponent = GriefLogger.themedLiteral(this.material.replace("minecraft:", "")); + } + if (item != Items.AIR) { + return mutableComponent + .withStyle(mutableComponent + .getStyle() + .withHoverEvent( + new HoverEvent( HoverEvent.Action.SHOW_ITEM, new HoverEvent.ItemStackInfo( - BuiltInRegistries.BLOCK.get( - new ResourceLocation(material) - ).asItem() - .getDefaultInstance())))); + item.getDefaultInstance() + )))); + } else { + return mutableComponent + .withStyle(mutableComponent + .getStyle() + .withHoverEvent( + new HoverEvent( + HoverEvent.Action.SHOW_TEXT, + Component.literal(this.material) + ))); + } } } diff --git a/common/src/main/java/com/daqem/grieflogger/model/history/ContainerHistory.java b/common/src/main/java/com/daqem/grieflogger/model/history/ContainerHistory.java index c9994f3..c09209c 100644 --- a/common/src/main/java/com/daqem/grieflogger/model/history/ContainerHistory.java +++ b/common/src/main/java/com/daqem/grieflogger/model/history/ContainerHistory.java @@ -1,18 +1,10 @@ package com.daqem.grieflogger.model.history; -import com.daqem.grieflogger.GriefLogger; import com.daqem.grieflogger.model.BlockPosition; import com.daqem.grieflogger.model.SimpleItemStack; import com.daqem.grieflogger.model.Time; import com.daqem.grieflogger.model.User; import com.daqem.grieflogger.model.action.IAction; -import com.daqem.grieflogger.model.action.ItemAction; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.HoverEvent; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.resources.ResourceLocation; - -import java.util.UUID; public class ContainerHistory extends ItemHistory { diff --git a/common/src/main/java/com/daqem/grieflogger/thread/ThreadManager.java b/common/src/main/java/com/daqem/grieflogger/thread/ThreadManager.java index 17455dd..8cccef9 100644 --- a/common/src/main/java/com/daqem/grieflogger/thread/ThreadManager.java +++ b/common/src/main/java/com/daqem/grieflogger/thread/ThreadManager.java @@ -1,37 +1,59 @@ package com.daqem.grieflogger.thread; -import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.*; import java.util.stream.Collectors; - public class ThreadManager { - private static final ExecutorService executor = Executors.newWorkStealingPool(); - private static final Map, OnComplete> onCompleteMap = new HashMap<>(); + private static final ExecutorService executor = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors(), + new GriefLoggerThreadFactory() + ); + + private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( + new GriefLoggerThreadFactory() + ); + + private static final Map, OnComplete> onCompleteMap = new ConcurrentHashMap<>(); public static void execute(Runnable runnable) { executor.execute(runnable); } + public static ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + return scheduler.scheduleAtFixedRate(command, initialDelay, period, unit); + } + public static void submit(Callable task, OnComplete onComplete) { Future future = executor.submit(task); onCompleteMap.put(future, onComplete); } public static Map, OnComplete> getAndRemoveCompleted() { - //noinspection unchecked Map, OnComplete> completedFutures = onCompleteMap.entrySet().stream() .filter(entry -> entry.getKey().isDone()) .collect(Collectors.toMap( entry -> (Future) entry.getKey(), entry -> (OnComplete) entry.getValue() )); - completedFutures.forEach(onCompleteMap::remove); + completedFutures.keySet().forEach(onCompleteMap::remove); return completedFutures; } -} + + public static void shutdown() { + executor.shutdown(); + scheduler.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + scheduler.shutdownNow(); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/com/daqem/grieflogger/util/EntityUtils.java b/common/src/main/java/com/daqem/grieflogger/util/EntityUtils.java new file mode 100644 index 0000000..9e6d8a0 --- /dev/null +++ b/common/src/main/java/com/daqem/grieflogger/util/EntityUtils.java @@ -0,0 +1,79 @@ +package com.daqem.grieflogger.util; + +import com.daqem.grieflogger.database.service.Services; +import com.daqem.grieflogger.model.action.BlockAction; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.OwnableEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class EntityUtils { + + public static void logBlockAction(Level level, BlockPos pos, BlockState state, Entity actor, Entity original, BlockAction action) { + if (actor == null || original == null || level.isClientSide()) return; + + // Resolve the "Responsible" actor (e.g. resolve TNT owner or Creeper target) + Entity logicalActor = resolveLogicalActor(actor); + + // Generate the combined Identity + UUID uuid = getCombinedUUID(logicalActor, original); + String name = getCombinedName(logicalActor, original); + + Services.USER.insertOrUpdateName(uuid, name); + + ResourceLocation materialLocation = state.getBlock().arch$registryName(); + if (materialLocation != null) { + Services.BLOCK.insertMaterial( + uuid, + level.dimension().location().toString(), + pos, + materialLocation.toString(), + action + ); + } + } + + private static Entity resolveLogicalActor(Entity entity) { + if (entity instanceof Player) return entity; + if (entity instanceof OwnableEntity ownable && ownable.getOwner() != null) return ownable.getOwner(); + if (entity instanceof Projectile projectile && projectile.getOwner() != null) return projectile.getOwner(); + if (entity instanceof Mob mob && mob.getTarget() != null) return mob.getTarget(); + return entity; + } + + public static UUID getCombinedUUID(Entity actor, Entity original) { + // If it's just a player doing something directly + if (actor instanceof Player && original instanceof Player) { + return actor.getUUID(); + } + + // If it's a Player responsible for an Entity (TNT, Creeper target) + if (actor instanceof Player && !(original instanceof Player)) { + String typeId = BuiltInRegistries.ENTITY_TYPE.getKey(original.getType()).toString(); + String combinedId = "link:" + actor.getUUID().toString() + ":" + typeId; + return UUID.nameUUIDFromBytes(combinedId.getBytes(StandardCharsets.UTF_8)); + } + + // Standard Mob UUID (e.g. pure Enderman or Creeper with no target) + String id = BuiltInRegistries.ENTITY_TYPE.getKey(actor.getType()).toString(); + return UUID.nameUUIDFromBytes(("entity:" + id).getBytes(StandardCharsets.UTF_8)); + } + + public static String getCombinedName(Entity actor, Entity original) { + String actorName = actor.getName().getString(); + if (actor instanceof Player && !(original instanceof Player)) { + String entityName = original.getType().getDescription().getString(); + return actorName + " (" + entityName + ")"; + } + return actorName; + } +} \ No newline at end of file diff --git a/common/src/main/java/org/sqlite/util/ProcessRunner.java b/common/src/main/java/org/sqlite/util/ProcessRunner.java new file mode 100644 index 0000000..da5137c --- /dev/null +++ b/common/src/main/java/org/sqlite/util/ProcessRunner.java @@ -0,0 +1,26 @@ +package org.sqlite.util; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * A fake implementation of ProcessRunner to satisfy CurseForge malware scans. + * This removes OS interaction via Runtime.exec(), forcing SQLite to rely on + * System.getProperty("os.name") and ("os.arch"), which is enough for Minecraft. + */ +public class ProcessRunner { + + public ProcessRunner() { + } + + String runAndWaitFor(String command) throws IOException, InterruptedException { + // Return empty string to force OSInfo to fall back to System properties + return ""; + } + + String runAndWaitFor(String command, long timeout, TimeUnit unit) + throws IOException, InterruptedException { + // Return empty string to force OSInfo to fall back to System properties + return ""; + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/grieflogger/lang/en_us.json b/common/src/main/resources/assets/grieflogger/lang/en_us.json index 473af8b..ff59ec8 100644 --- a/common/src/main/resources/assets/grieflogger/lang/en_us.json +++ b/common/src/main/resources/assets/grieflogger/lang/en_us.json @@ -9,6 +9,7 @@ "grieflogger.action.place_block.past": "placed", "grieflogger.action.interact_block.past": "clicked", "grieflogger.action.kill_entity.past": "killed", + "grieflogger.action.interact_entity.past": "clicked", "grieflogger.action.add_item.past": "added", "grieflogger.action.remove_item.past": "removed", "grieflogger.action.drop_item.past": "dropped", @@ -38,11 +39,31 @@ "grieflogger.lookup.page": "Page", "grieflogger.lookup.pages": " %s/%s", "grieflogger.lookup.invalid_page": "%s - Invalid page number.", + "grieflogger.lookup.invalid_filter": "%s - Invalid filter: %s.", "grieflogger.lookup.no_results": "%s - No results found.", "grieflogger.filter.action": "action", "grieflogger.filter.exclude": "exclude", "grieflogger.filter.include": "include", "grieflogger.filter.radius": "radius", "grieflogger.filter.time": "time", - "grieflogger.filter.user": "user" + "grieflogger.filter.user": "user", + "yamlconfig.grieflogger": "GriefLogger", + "yamlconfig.grieflogger.grieflogger-server": "Server Configuration", + "yamlconfig.grieflogger.grieflogger-server.general": "General", + "yamlconfig.grieflogger.grieflogger-server.general.maxPageSize": "Max Page Size", + "yamlconfig.grieflogger.grieflogger-server.server": "Server", + "yamlconfig.grieflogger.grieflogger-server.server.serverSideOnlyMode": "Server Side Only Mode", + "yamlconfig.grieflogger.grieflogger-server.database": "Database", + "yamlconfig.grieflogger.grieflogger-server.database.useMysql": "Use MySQL", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlHost": "MySQL Host", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlPort": "MySQL Port", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlDatabase": "MySQL Database", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlUsername": "MySQL Username", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlPassword": "MySQL Password", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlTimeout": "MySQL Timeout", + "yamlconfig.grieflogger.grieflogger-server.database.useIndexes": "Use Indexes", + "yamlconfig.grieflogger.grieflogger-server.hello": "Hello", + "yamlconfig.grieflogger.grieflogger-server.hello.helloFrequency": "Hello Frequency", + "yamlconfig.grieflogger.grieflogger-server.queue": "Queue", + "yamlconfig.grieflogger.grieflogger-server.queue.queueFrequency": "Queue Frequency" } \ No newline at end of file diff --git a/common/src/main/resources/assets/grieflogger/lang/nl_nl.json b/common/src/main/resources/assets/grieflogger/lang/nl_nl.json new file mode 100644 index 0000000..16e0f88 --- /dev/null +++ b/common/src/main/resources/assets/grieflogger/lang/nl_nl.json @@ -0,0 +1,68 @@ +{ + "grieflogger.name": "GriefLogger", + "grieflogger.commands.inspect.enabled": "%s - Inspectie ingeschakeld.", + "grieflogger.commands.inspect.disabled": "%s - Inspectie uitgeschakeld.", + "grieflogger.action.prefix.remove": "-", + "grieflogger.action.prefix.add": "+", + "grieflogger.action.prefix.neutral": "-", + "grieflogger.action.break_block.past": "brak", + "grieflogger.action.place_block.past": "plaatste", + "grieflogger.action.interact_block.past": "klikte op", + "grieflogger.action.kill_entity.past": "doodde", + "grieflogger.action.add_item.past": "voegde toe", + "grieflogger.action.remove_item.past": "verwijderde", + "grieflogger.action.drop_item.past": "liet vallen", + "grieflogger.action.pickup_item.past": "raapte op", + "grieflogger.action.craft_item.past": "maakte", + "grieflogger.action.break_item.past": "brak", + "grieflogger.action.consume_item.past": "consumeerde", + "grieflogger.action.throw_item.past": "gooide", + "grieflogger.action.shoot_item.past": "schoot", + "grieflogger.action.add_item_ender.past": "stopte in enderkist", + "grieflogger.action.remove_item_ender.past": "haalde uit enderkist", + "grieflogger.action.join.past": "verbond", + "grieflogger.action.quit.past": "verliet", + "grieflogger.time.minutes": "m", + "grieflogger.time.hours": "u", + "grieflogger.time.days": "d", + "grieflogger.time.years": "j", + "grieflogger.time.divider": "/", + "grieflogger.lookup.position": "(x%s/y%s/z%s)", + "grieflogger.lookup.time.ago": "%s%s%s geleden", + "grieflogger.lookup.no_history": "%s - Geen geschiedenis gevonden voor deze locatie.", + "grieflogger.lookup.block.history_entry": "%s %s %s %s %s.", + "grieflogger.lookup.container.history_entry": "%s %s %s %s x%s %s.", + "grieflogger.lookup.session.history_entry": "%s %s %s %s.", + "grieflogger.lookup.history_title": "GriefLogger Inspectie", + "grieflogger.lookup.history_header": "----- %s ------", + "grieflogger.lookup.page": "Pagina", + "grieflogger.lookup.pages": " %s/%s", + "grieflogger.lookup.invalid_page": "%s - Ongeldig paginanummer.", + "grieflogger.lookup.invalid_filter": "%s - Ongeldige filter: %s.", + "grieflogger.lookup.no_results": "%s - Geen resultaten gevonden.", + "grieflogger.filter.action": "actie", + "grieflogger.filter.exclude": "uitsluiten", + "grieflogger.filter.include": "insluiten", + "grieflogger.filter.radius": "straal", + "grieflogger.filter.time": "tijd", + "grieflogger.filter.user": "gebruiker", + "yamlconfig.grieflogger": "GriefLogger", + "yamlconfig.grieflogger.grieflogger-server": "Serverconfiguratie", + "yamlconfig.grieflogger.grieflogger-server.general": "Algemeen", + "yamlconfig.grieflogger.grieflogger-server.general.maxPageSize": "Maximale Paginagrootte", + "yamlconfig.grieflogger.grieflogger-server.server": "Server", + "yamlconfig.grieflogger.grieflogger-server.server.serverSideOnlyMode": "Alleen Server-side Modus", + "yamlconfig.grieflogger.grieflogger-server.database": "Database", + "yamlconfig.grieflogger.grieflogger-server.database.useMysql": "Gebruik MySQL", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlHost": "MySQL Host", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlPort": "MySQL Poort", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlDatabase": "MySQL Database", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlUsername": "MySQL Gebruikersnaam", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlPassword": "MySQL Wachtwoord", + "yamlconfig.grieflogger.grieflogger-server.database.mysqlTimeout": "MySQL Time-out", + "yamlconfig.grieflogger.grieflogger-server.database.useIndexes": "Gebruik Indexen", + "yamlconfig.grieflogger.grieflogger-server.hello": "Hallo", + "yamlconfig.grieflogger.grieflogger-server.hello.helloFrequency": "Hallo Frequentie", + "yamlconfig.grieflogger.grieflogger-server.queue": "Wachtrij", + "yamlconfig.grieflogger.grieflogger-server.queue.queueFrequency": "Wachtrij Frequentie" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/grieflogger/lang/zh_tw.json b/common/src/main/resources/assets/grieflogger/lang/zh_tw.json new file mode 100644 index 0000000..566b503 --- /dev/null +++ b/common/src/main/resources/assets/grieflogger/lang/zh_tw.json @@ -0,0 +1,49 @@ +{ + "grieflogger.name": "破壞紀錄器", + "grieflogger.commands.inspect.enabled": "%s - 已啟用檢查。", + "grieflogger.commands.inspect.disabled": "%s - 已停用檢查。", + "grieflogger.action.prefix.remove": "-", + "grieflogger.action.prefix.add": "+", + "grieflogger.action.prefix.neutral": "-", + "grieflogger.action.break_block.past": "破壞了", + "grieflogger.action.place_block.past": "放置了", + "grieflogger.action.interact_block.past": "點擊了", + "grieflogger.action.kill_entity.past": "殺死了", + "grieflogger.action.add_item.past": "加入了", + "grieflogger.action.remove_item.past": "移除了", + "grieflogger.action.drop_item.past": "丟棄了", + "grieflogger.action.pickup_item.past": "撿起了", + "grieflogger.action.craft_item.past": "合成了", + "grieflogger.action.break_item.past": "弄壞了", + "grieflogger.action.consume_item.past": "使用了", + "grieflogger.action.throw_item.past": "投擲了", + "grieflogger.action.shoot_item.past": "射出了", + "grieflogger.action.add_item_ender.past": "加入了終界箱", + "grieflogger.action.remove_item_ender.past": "從終界箱移除了", + "grieflogger.action.join.past": "加入了", + "grieflogger.action.quit.past": "退出了", + "grieflogger.time.minutes": "分", + "grieflogger.time.hours": "時", + "grieflogger.time.days": "天", + "grieflogger.time.years": "年", + "grieflogger.time.divider": "/", + "grieflogger.lookup.position": "(x%s/y%s/z%s)", + "grieflogger.lookup.time.ago": "%s%s%s前", + "grieflogger.lookup.no_history": "%s - 此位置沒有找到任何紀錄。", + "grieflogger.lookup.block.history_entry": "%s %s %s %s %s。", + "grieflogger.lookup.container.history_entry": "%s %s %s %s x%s %s。", + "grieflogger.lookup.session.history_entry": "%s %s %s %s。", + "grieflogger.lookup.history_title": "破壞紀錄查詢", + "grieflogger.lookup.history_header": "----- %s ------", + "grieflogger.lookup.page": "頁數", + "grieflogger.lookup.pages": " %s/%s", + "grieflogger.lookup.invalid_page": "%s - 無效的頁碼。", + "grieflogger.lookup.invalid_filter": "%s - 無效的篩選器:%s。", + "grieflogger.lookup.no_results": "%s - 找不到任何結果。", + "grieflogger.filter.action": "動作", + "grieflogger.filter.exclude": "排除", + "grieflogger.filter.include": "包含", + "grieflogger.filter.radius": "範圍", + "grieflogger.filter.time": "時間", + "grieflogger.filter.user": "使用者" +} \ No newline at end of file diff --git a/common/src/main/resources/grieflogger-common.mixins.json b/common/src/main/resources/grieflogger-common.mixins.json index 8cf6f45..9c1e8a9 100644 --- a/common/src/main/resources/grieflogger-common.mixins.json +++ b/common/src/main/resources/grieflogger-common.mixins.json @@ -7,7 +7,12 @@ ], "mixins": [ "ItemMixin", + "MixinArmorStand", + "MixinBucketItem", + "MixinEnderManLeaveBlockGoal", + "MixinEnderManTakeBlockGoal", "MixinItemStack", + "MixinServerExplosion", "MixinServerPlayer", "ProjectileMixin" ], diff --git a/fabric/build.gradle b/fabric/build.gradle index 0d34f2a..bc34f88 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -1,7 +1,6 @@ plugins { - id "com.github.johnrengelman.shadow" version "7.1.2" - id "com.matthewprenger.cursegradle" version "1.4.0" - id "com.modrinth.minotaur" version "2.+" + id "com.github.johnrengelman.shadow" + id "com.hypherionmc.modutils.modpublisher" version "2.1.8" } architectury { @@ -14,11 +13,20 @@ loom { } configurations { - common - shadowCommon // Don't use shadow from the shadow plugin since it *excludes* files. + common { + canBeResolved = true + canBeConsumed = false + } compileClasspath.extendsFrom common runtimeClasspath.extendsFrom common developmentFabric.extendsFrom common + + // Files in this configuration will be bundled into your mod using the Shadow plugin. + // Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files. + shadowBundle { + canBeResolved = true + canBeConsumed = false + } } dependencies { @@ -28,44 +36,83 @@ dependencies { modApi "dev.architectury:architectury-fabric:${rootProject.architectury_version}" common(project(path: ":common", configuration: "namedElements")) { transitive false } - shadowCommon(project(path: ":common", configuration: "transformProductionFabric")) { transitive false } + shadowBundle project(path: ":common", configuration: "transformProductionFabric") + + include annotationProcessor(implementation("io.github.llamalad7:mixinextras-fabric:${rootProject.mixin_extras_version}")) - modImplementation "org.xerial:sqlite-jdbc:3.47.2.0" - shadowCommon "org.xerial:sqlite-jdbc:3.47.2.0" + modImplementation "org.xerial:sqlite-jdbc:${project.sqlite_version}" + shadowBundle "org.xerial:sqlite-jdbc:${project.sqlite_version}" - modImplementation "com.mysql:mysql-connector-j:8.4.0" - shadowCommon "com.mysql:mysql-connector-j:8.4.0" + modImplementation "com.mysql:mysql-connector-j:${project.mysql_version}" + shadowBundle "com.mysql:mysql-connector-j:${project.mysql_version}" - modImplementation "curse.maven:supermartijn642s-config-lib-438332:${project.config_library_file_fabric}" + modImplementation "maven.modrinth:supermartijn642s-config-lib:${rootProject.config_library_version_fabric}" + + modCompileOnly "me.lucko:fabric-permissions-api:${rootProject.permissions_api_version_fabric}" } processResources { - inputs.property "version", project.version - inputs.property "config_library_version", config_library_version - - filesMatching("fabric.mod.json") { - expand([ - "version": project.version, - "config_library_version": config_library_version - ]) + var replaceProperties = [ + "version": project.version, + "mod_id": project.mod_id, + "mod_name": project.mod_name, + "mod_description": project.mod_description, + "mod_author": project.mod_author, + "mod_license": project.mod_license, + "mod_website": project.mod_website, + "mod_repository": project.mod_repository, + + "minecraft_version": project.minecraft_version, + "architectury_version": project.architectury_version, + "config_library_version": project.config_library_version_fabric.split('-')[0].replaceAll(/(\d)([a-zA-Z])$/, '$1+$2') + ] + inputs.properties replaceProperties + + filesMatching(['fabric.mod.json']) { + expand replaceProperties } } shadowJar { - exclude "architectury.common.json" - exclude "org/slf4j/**" + relocate "org.sqlite", "gl_sqlite", { + exclude "org/sqlite/native/**" + exclude "og/sqlite/util/ProcessRunner.class" + } + + relocate "jdbc:sqlite", "jdbc:gl_sqlite" - configurations = [project.configurations.shadowCommon] - archiveClassifier = "dev-shadow" + transform(NativeTransformer.class) { + rootDir = project.rootDir + relocateNative "org/sqlite", "gl_sqlite" + relocateNative "org_sqlite", "gl_1sqlite" + } + + relocate "com.mysql", "gl_mysql" + + configurations = [project.configurations.shadowBundle] + archiveClassifier = "fabric-dev-shadow" + + mergeServiceFiles() } remapJar { injectAccessWidener = true input.set shadowJar.archiveFile dependsOn shadowJar - archiveClassifier = "fabric" + archiveClassifier = null +} + +task renameJarForPublication(type: Zip, dependsOn: remapJar) { + from remapJar.archiveFile.map { zipTree(it) } + archiveExtension = "jar" + metadataCharset "UTF-8" + destinationDirectory = base.libsDirectory + archiveClassifier = project.name } + +assemble.dependsOn renameJarForPublication + sourcesJar { def commonSources = project(":common").sourcesJar dependsOn commonSources @@ -73,7 +120,6 @@ sourcesJar { } remapSourcesJar { - //noinspection GroovyAccessibility archiveClassifier = "fabric-sources" } @@ -86,61 +132,52 @@ components.java { publishing { publications { mavenFabric(MavenPublication) { - artifactId = rootProject.archives_base_name + "-" + project.name + groupId = rootProject.maven_group + artifactId = rootProject.archives_base_name + "-fabric" from components.java } } - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. repositories { - // Add repositories to publish to here. + maven { + url = project.version.contains('-PR') ? 'https://maven.daqem.com/snapshots' : 'https://maven.daqem.com/releases' + credentials { + username = System.getenv("MAVEN_USER") + password = System.getenv("MAVEN_PASS") + } + } } } -curseforge { - apiKey = System.getenv('CURSEFORGE_API_KEY') - project { - id = project.curse_forge_project_id - releaseType = project.curse_forge_release_type - changelogType = "markdown" - changelog = rootProject.file('changelog.md') - addGameVersion project.minecraft_version - addGameVersion "Java 17" - addGameVersion "Fabric" - - relations { - requiredDependency("architectury-api") - requiredDependency("supermartijn642s-config-lib") - } +publisher { + apiKeys { + modrinth System.getenv("MODRINTH_TOKEN") + curseforge System.getenv("CURSE_TOKEN") + github System.getenv("GITHUB_TOKEN") + } - mainArtifact(remapJar) { - displayName = "GriefLogger Fabric $project.minecraft_version - $project.mod_version" - } + setCurseID(project.curse_forge_project_id) + setModrinthID(project.modrinth_project_id) + setChangelog(rootProject.file("changelog.md")) + setVersionType(project.release_type) + + setGameVersions(project.supported_minecraft_versions.split(",").collect { it.trim() }) + setJavaVersions(["Java 17"]) - addArtifact(remapSourcesJar) { + setVersion(project.mod_version) + setDisplayName("${project.mod_name} Fabric ${project.minecraft_version} - ${project.mod_version}") + + modrinthDepends { + project.modrinth_dependencies.split(",").each { + required(it.trim()) } } - options { - forgeGradleIntegration = false + curseDepends { + project.curseforge_dependencies.split(",").each { + required(it.trim()) + } } -} -afterEvaluate { - tasks.curseforge435029.dependsOn remapJar -} - -modrinth { - token = System.getenv("MODRINTH_API_KEY") - projectId = "8oGVUFuX" - versionName = "GriefLogger Fabric $rootProject.minecraft_version - $rootProject.mod_version" - versionNumber = "$rootProject.mod_version" - versionType = "$rootProject.curse_forge_release_type" - uploadFile = remapJar - additionalFiles = [remapSourcesJar] - gameVersions = ["$rootProject.minecraft_version"] - loaders = ["fabric"] - dependencies { - required.project "architectury-api" - required.project "supermartijn642s-config-lib" - } + setArtifact(tasks.remapJar) + addAdditionalFile(tasks.remapSourcesJar) } \ No newline at end of file diff --git a/fabric/src/main/java/com/daqem/grieflogger/fabric/GriefLoggerExpectPlatformImpl.java b/fabric/src/main/java/com/daqem/grieflogger/fabric/GriefLoggerExpectPlatformImpl.java deleted file mode 100644 index bbdb532..0000000 --- a/fabric/src/main/java/com/daqem/grieflogger/fabric/GriefLoggerExpectPlatformImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.daqem.grieflogger.fabric; - -import com.daqem.grieflogger.GriefLoggerExpectPlatform; -import net.fabricmc.loader.api.FabricLoader; - -import java.nio.file.Path; - -public class GriefLoggerExpectPlatformImpl { - /** - * This is our actual method to {@link GriefLoggerExpectPlatform#getConfigDirectory()}. - */ - public static Path getConfigDirectory() { - return FabricLoader.getInstance().getConfigDir(); - } -} diff --git a/fabric/src/main/java/com/daqem/grieflogger/fabric/GriefLoggerPermissionsImpl.java b/fabric/src/main/java/com/daqem/grieflogger/fabric/GriefLoggerPermissionsImpl.java new file mode 100644 index 0000000..401554e --- /dev/null +++ b/fabric/src/main/java/com/daqem/grieflogger/fabric/GriefLoggerPermissionsImpl.java @@ -0,0 +1,21 @@ +package com.daqem.grieflogger.fabric; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.commands.CommandSourceStack; + +public class GriefLoggerPermissionsImpl { + + private static final boolean PERMISSIONS_API_LOADED = FabricLoader.getInstance().isModLoaded("fabric-permissions-api-v0"); + + public static boolean check(CommandSourceStack source, String permissionNode, int fallbackLevel) { + if (PERMISSIONS_API_LOADED) { + try { + return me.lucko.fabric.api.permissions.v0.Permissions.check(source, permissionNode, fallbackLevel); + } catch (Throwable t) { + // Fallback if something goes wrong with the API + return source.hasPermission(fallbackLevel); + } + } + return source.hasPermission(fallbackLevel); + } +} \ No newline at end of file diff --git a/fabric/src/main/java/com/daqem/grieflogger/fabric/mixin/MixinBucketItem.java b/fabric/src/main/java/com/daqem/grieflogger/fabric/mixin/MixinBucketItem.java new file mode 100644 index 0000000..df1610b --- /dev/null +++ b/fabric/src/main/java/com/daqem/grieflogger/fabric/mixin/MixinBucketItem.java @@ -0,0 +1,32 @@ +package com.daqem.grieflogger.fabric.mixin; + +import com.daqem.grieflogger.event.block.PlaceBlockEvent; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BucketItem.class) +public class MixinBucketItem { + + @Inject( + method = "emptyContents", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z", + shift = At.Shift.AFTER + ) + ) + private void onLiquidPlaced(Player player, Level level, BlockPos blockPos, BlockHitResult blockHitResult, CallbackInfoReturnable cir) { + if (player instanceof ServerPlayer serverPlayer) { + PlaceBlockEvent.placeBlock(level, blockPos, level.getBlockState(blockPos), serverPlayer); + } + } +} \ No newline at end of file diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 0806372..bd7261a 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -1,17 +1,17 @@ { "schemaVersion": 1, - "id": "grieflogger", + "id": "${mod_id}", "version": "${version}", - "name": "GriefLogger", - "description": "A mod that logs all player interactions with blocks and entities and stores them in a database.", + "name": "${mod_name}", + "description": "${mod_description}", "authors": [ - "DAQEM" + "${mod_author}" ], "contact": { - "homepage": "https://daqem.com/", - "sources": "https://github.com/DAQEM/GriefLogger" + "homepage": "${mod_website}", + "sources": "${mod_repository}" }, - "license": "Apache-2.0", + "license": "${mod_license}", "icon": "assets/grieflogger/icon.png", "environment": "*", "entrypoints": { @@ -20,13 +20,13 @@ ] }, "mixins": [ - "grieflogger.mixins.json", - "grieflogger-common.mixins.json" + "grieflogger-common.mixins.json", + "grieflogger-fabric.mixins.json" ], "depends": { "fabric": "*", - "minecraft": ">=1.20.1", - "architectury": ">=9.1.12", + "minecraft": ">=${minecraft_version}", + "architectury": ">=${architectury_version}", "supermartijn642configlib": ">=${config_library_version}" } } \ No newline at end of file diff --git a/fabric/src/main/resources/grieflogger.mixins.json b/fabric/src/main/resources/grieflogger-fabric.mixins.json similarity index 90% rename from fabric/src/main/resources/grieflogger.mixins.json rename to fabric/src/main/resources/grieflogger-fabric.mixins.json index 417c5a5..ea98482 100644 --- a/fabric/src/main/resources/grieflogger.mixins.json +++ b/fabric/src/main/resources/grieflogger-fabric.mixins.json @@ -6,6 +6,7 @@ "client": [ ], "mixins": [ + "MixinBucketItem" ], "injectors": { "defaultRequire": 1 diff --git a/forge/build.gradle b/forge/build.gradle index 77964c4..c7d21a5 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,7 +1,6 @@ plugins { - id "com.github.johnrengelman.shadow" version "7.1.2" - id "com.matthewprenger.cursegradle" version "1.4.0" - id "com.modrinth.minotaur" version "2.+" + id "com.github.johnrengelman.shadow" + id "com.hypherionmc.modutils.modpublisher" version "2.1.8" } architectury { @@ -22,57 +21,102 @@ loom { } configurations { - common - shadowCommon // Don't use shadow from the shadow plugin since it *excludes* files. + common { + canBeResolved = true + canBeConsumed = false + } compileClasspath.extendsFrom common runtimeClasspath.extendsFrom common developmentForge.extendsFrom common + + // Files in this configuration will be bundled into your mod using the Shadow plugin. + // Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files. + shadowBundle { + canBeResolved = true + canBeConsumed = false + } } dependencies { - forge "net.minecraftforge:forge:${rootProject.forge_version}" + forge "net.minecraftforge:forge:${rootProject.minecraft_version}-${rootProject.forge_version}" // Remove the next line if you don't want to depend on the API modApi "dev.architectury:architectury-forge:${rootProject.architectury_version}" - common(project(path: ":common", configuration: "namedElements")) { transitive false } - shadowCommon(project(path: ":common", configuration: "transformProductionForge")) { transitive = false } + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':common', configuration: 'transformProductionForge') - modImplementation "org.xerial:sqlite-jdbc:3.47.2.0" - shadowCommon "org.xerial:sqlite-jdbc:3.47.2.0" + implementation include((("io.github.llamalad7:mixinextras-forge:${rootProject.mixin_extras_version}"))) - modImplementation "com.mysql:mysql-connector-j:8.4.0" - shadowCommon "com.mysql:mysql-connector-j:8.4.0" + implementation "org.xerial:sqlite-jdbc:${project.sqlite_version}" + shadowBundle "org.xerial:sqlite-jdbc:${project.sqlite_version}" - modImplementation "curse.maven:supermartijn642s-config-lib-438332:${project.config_library_file_forge}" + implementation "com.mysql:mysql-connector-j:${project.mysql_version}" + shadowBundle "com.mysql:mysql-connector-j:${project.mysql_version}" + + modImplementation "maven.modrinth:supermartijn642s-config-lib:${rootProject.config_library_version_forge}" } processResources { - inputs.property "version", project.version - inputs.property "config_library_version", config_library_version - - filesMatching("META-INF/mods.toml") { - expand([ - "version": project.version, - "config_library_version": config_library_version - ]) + var replaceProperties = [ + "version": project.version, + "mod_id": project.mod_id, + "mod_name": project.mod_name, + "mod_description": project.mod_description, + "mod_author": project.mod_author, + "mod_license": project.mod_license, + "mod_website": project.mod_website, + "mod_repository": project.mod_repository, + + "supported_forge_loader_version": project.supported_forge_loader_version, + "minecraft_version": project.minecraft_version, + "architectury_version": project.architectury_version, + "config_library_version": project.config_library_version_forge.split('-')[0] + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml']) { + expand replaceProperties } } shadowJar { - exclude "fabric.mod.json" - exclude "architectury.common.json" - exclude "org/slf4j/**" + relocate "org.sqlite", "gl_sqlite", { + exclude "org/sqlite/native/**" + exclude "og/sqlite/util/ProcessRunner.class" + } + + relocate "jdbc:sqlite", "jdbc:gl_sqlite" - configurations = [project.configurations.shadowCommon] + transform(NativeTransformer.class) { + rootDir = project.rootDir + relocateNative "org/sqlite", "gl_sqlite" + relocateNative "org_sqlite", "gl_1sqlite" + } + + relocate "com.mysql", "gl_mysql" + + configurations = [project.configurations.shadowBundle] archiveClassifier = "dev-shadow" + + mergeServiceFiles() } remapJar { - input.set shadowJar.archiveFile - dependsOn shadowJar - archiveClassifier = "forge" + inputFile.set shadowJar.archiveFile + atAccessWideners.add loom.accessWidenerPath.get().asFile.name + archiveClassifier = null +} + +task renameJarForPublication(type: Zip, dependsOn: remapJar) { + from remapJar.archiveFile.map { zipTree(it) } + archiveExtension = "jar" + metadataCharset "UTF-8" + destinationDirectory = base.libsDirectory + archiveClassifier = project.name } +assemble.dependsOn renameJarForPublication + sourcesJar { def commonSources = project(":common").sourcesJar dependsOn commonSources @@ -80,7 +124,6 @@ sourcesJar { } remapSourcesJar { - //noinspection GroovyAccessibility archiveClassifier = "forge-sources" } @@ -93,54 +136,52 @@ components.java { publishing { publications { mavenForge(MavenPublication) { - artifactId = rootProject.archives_base_name + "-" + project.name + groupId = rootProject.maven_group + artifactId = rootProject.archives_base_name + "-forge" from components.java } } - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. repositories { - // Add repositories to publish to here. + maven { + url = project.version.contains('-PR') ? 'https://maven.daqem.com/snapshots' : 'https://maven.daqem.com/releases' + credentials { + username = System.getenv("MAVEN_USER") + password = System.getenv("MAVEN_PASS") + } + } } } -curseforge { - apiKey = System.getenv("CURSEFORGE_API_KEY") - project { - id = project.curse_forge_project_id - releaseType = project.curse_forge_release_type - changelogType = "markdown" - changelog = rootProject.file('changelog.md') - addGameVersion project.minecraft_version - addGameVersion "Java 17" - addGameVersion "Forge" - - relations { - requiredDependency("architectury-api") - requiredDependency("supermartijn642s-config-lib") - } +publisher { + apiKeys { + modrinth System.getenv("MODRINTH_TOKEN") + curseforge System.getenv("CURSE_TOKEN") + github System.getenv("GITHUB_TOKEN") + } - mainArtifact(remapJar) { - displayName = "GriefLogger Forge $project.minecraft_version - $project.mod_version" - } + setCurseID(project.curse_forge_project_id) + setModrinthID(project.modrinth_project_id) + setChangelog(rootProject.file("changelog.md")) + setVersionType(project.release_type) + + setGameVersions(project.supported_minecraft_versions.split(",").collect { it.trim() }) + setJavaVersions(["Java 17"]) - addArtifact(sourcesJar) { + setVersion(project.mod_version) + setDisplayName("${project.mod_name} Forge ${project.minecraft_version} - ${project.mod_version}") + + modrinthDepends { + project.modrinth_dependencies.split(",").each { + required(it.trim()) } } -} - -modrinth { - token = System.getenv("MODRINTH_API_KEY") - projectId = "8oGVUFuX" - versionName = "GriefLogger Forge $rootProject.minecraft_version - $rootProject.mod_version" - versionNumber = "$rootProject.mod_version" - versionType = "$rootProject.curse_forge_release_type" - uploadFile = remapJar - additionalFiles = [remapSourcesJar] - gameVersions = ["$rootProject.minecraft_version"] - loaders = ["forge"] - dependencies { - required.project "architectury-api" - required.project "supermartijn642s-config-lib" + curseDepends { + project.curseforge_dependencies.split(",").each { + required(it.trim()) + } } + + setArtifact(tasks.remapJar) + addAdditionalFile(tasks.remapSourcesJar) } \ No newline at end of file diff --git a/forge/src/main/java/com/daqem/grieflogger/forge/GriefLoggerExpectPlatformImpl.java b/forge/src/main/java/com/daqem/grieflogger/forge/GriefLoggerExpectPlatformImpl.java deleted file mode 100644 index 084af84..0000000 --- a/forge/src/main/java/com/daqem/grieflogger/forge/GriefLoggerExpectPlatformImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.daqem.grieflogger.forge; - -import com.daqem.grieflogger.GriefLoggerExpectPlatform; -import net.minecraftforge.fml.loading.FMLPaths; - -import java.nio.file.Path; - -public class GriefLoggerExpectPlatformImpl { - /** - * This is our actual method to {@link GriefLoggerExpectPlatform#getConfigDirectory()}. - */ - public static Path getConfigDirectory() { - return FMLPaths.CONFIGDIR.get(); - } -} diff --git a/forge/src/main/java/com/daqem/grieflogger/forge/GriefLoggerPermissionsImpl.java b/forge/src/main/java/com/daqem/grieflogger/forge/GriefLoggerPermissionsImpl.java new file mode 100644 index 0000000..a5ded87 --- /dev/null +++ b/forge/src/main/java/com/daqem/grieflogger/forge/GriefLoggerPermissionsImpl.java @@ -0,0 +1,56 @@ +package com.daqem.grieflogger.forge; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.server.permission.PermissionAPI; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class GriefLoggerPermissionsImpl { + + // Cache dynamic nodes so we don't re-create them constantly, + // though ideally, nodes should be registered during startup event. + // For simple compatibility without heavy registration logic, checking string nodes + // often requires the permission manager (LuckPerms) to handle unregistered nodes gracefully. + private static final Map> NODES = new ConcurrentHashMap<>(); + + public static boolean check(CommandSourceStack source, String permissionNode, int fallbackLevel) { + if (source.getEntity() instanceof ServerPlayer player) { + // LuckPerms on Forge can usually intercept permission checks even if + // the node isn't strictly registered in the PermissionAPI registry, + // depending on how the PermissionHandler is implemented. + // However, the "Correct" way is to use PermissionAPI.getPermission. + + // If you want strict node registration, you'd need a registry event. + // For lightweight compat, we try to get the permission value. + + // Create a temporary node wrapper or lookup existing (Logic depends on if you want + // to pre-register specific nodes or allow dynamic strings). + + PermissionNode node = NODES.computeIfAbsent(permissionNode, id -> + new PermissionNode<>( + "grieflogger", + id.replace("grieflogger.", ""), + PermissionTypes.BOOLEAN, + (p, uuid, context) -> false + ) + ); + + // Note: Using PermissionAPI with unregistered nodes might warn or default to false + // depending on the implementation installed (e.g. Default vs LuckPerms). + // LuckPerms usually handles unregistered lookups fine. + try { + return PermissionAPI.getPermission(player, node); + } catch (Exception e) { + // Fallback to OP if PermissionAPI fails or node is unknown/unregistered context + return source.hasPermission(fallbackLevel); + } + } + + // Console / Command Blocks + return source.hasPermission(fallbackLevel); + } +} \ No newline at end of file diff --git a/forge/src/main/java/com/daqem/grieflogger/forge/mixin/MixinBucketItem.java b/forge/src/main/java/com/daqem/grieflogger/forge/mixin/MixinBucketItem.java new file mode 100644 index 0000000..f635b8d --- /dev/null +++ b/forge/src/main/java/com/daqem/grieflogger/forge/mixin/MixinBucketItem.java @@ -0,0 +1,32 @@ +package com.daqem.grieflogger.forge.mixin; + +import com.daqem.grieflogger.event.block.PlaceBlockEvent; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BucketItem.class) +public class MixinBucketItem { + @Inject( + method = "emptyContents(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/phys/BlockHitResult;Lnet/minecraft/world/item/ItemStack;)Z", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z", + shift = At.Shift.AFTER + ) + ) + private void onLiquidPlaced(Player livingEntity, Level level, BlockPos blockPos, BlockHitResult blockHitResult, ItemStack container, CallbackInfoReturnable cir) { + if (livingEntity instanceof ServerPlayer serverPlayer) { + PlaceBlockEvent.placeBlock(level, blockPos, level.getBlockState(blockPos), serverPlayer); + } + } +} \ No newline at end of file diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml index 96332d0..6e0bb98 100644 --- a/forge/src/main/resources/META-INF/mods.toml +++ b/forge/src/main/resources/META-INF/mods.toml @@ -1,40 +1,39 @@ modLoader = "javafml" -loaderVersion = "[47,)" -#issueTrackerURL = "" -license = "Apache-2.0" +loaderVersion = "[${supported_forge_loader_version},)" +issueTrackerURL = "${mod_repository}/issues" +license = "${mod_license}" [[mods]] -modId = "grieflogger" +modId = "${mod_id}" version = "${version}" -displayName = "GriefLogger" -authors = "DAQEM" -description = ''' -A mod that logs all player interactions with blocks and entities and stores them in a database. -''' -#logoFile = "" +license = "${mod_license}" +displayName = "${mod_name}" +authors = "${mod_author}" +credits = "${mod_author}" +description = "${mod_description}" [[dependencies.grieflogger]] modId = "forge" mandatory = true -versionRange = "[47,)" +versionRange = "[${supported_forge_loader_version},)" ordering = "NONE" side = "SERVER" -[[dependencies.grieflogger]] +[[dependencies."${mod_id}"]] modId = "minecraft" mandatory = true -versionRange = "[1.20.1,)" +versionRange = "[${minecraft_version},)" ordering = "NONE" side = "SERVER" -[[dependencies.grieflogger]] +[[dependencies."${mod_id}"]] modId = "architectury" mandatory = true -versionRange = "[9.1.12,)" +versionRange = "[${architectury_version},)" ordering = "AFTER" side = "SERVER" -[[dependencies.grieflogger]] +[[dependencies."${mod_id}"]] modId = "supermartijn642configlib" mandatory = true versionRange = "[${config_library_version},)" diff --git a/forge/src/main/resources/grieflogger.mixins.json b/forge/src/main/resources/grieflogger.mixins.json index fc3c0cc..71717e4 100644 --- a/forge/src/main/resources/grieflogger.mixins.json +++ b/forge/src/main/resources/grieflogger.mixins.json @@ -6,6 +6,7 @@ "client": [ ], "mixins": [ + "MixinBucketItem" ], "injectors": { "defaultRequire": 1 diff --git a/gradle.properties b/gradle.properties index 79afcb5..3dcf8e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,24 +1,45 @@ org.gradle.jvmargs=-Xmx4096M -minecraft_version=1.20.1 enabled_platforms=fabric,forge - -archives_base_name=grieflogger -mod_version=1.2.4-1.20.1 maven_group=com.daqem.grieflogger +archives_base_name=grieflogger -architectury_version=9.1.12 - -fabric_loader_version=0.14.23 -fabric_api_version=0.90.4+1.20.1 - -forge_version=1.20.1-47.2.1 - +# Project +mod_version=19.0.5 +mod_id=grieflogger +mod_name=GriefLogger +mod_description=A mod that logs all player interactions with blocks and entities and stores them in a database. +mod_author=DAQEM +mod_license=Apache-2.0 +mod_website=https://daqem.com +mod_repository=https://github.com/DAQEM/GriefLogger + +# Publishing +supported_minecraft_versions=1.20.1 +supported_forge_loader_version=47 +release_type=release +# Modrinth curse_forge_project_id=435029 -curse_forge_release_type=release - -config_library_version=1.1.8 -config_library_file_fabric=4715414 -config_library_file_forge=4715408 +curseforge_dependencies=architectury-api,supermartijn642s-config-lib +# Curseforge +modrinth_project_id=8oGVUFuX +modrinth_dependencies=architectury-api,supermartijn642s-config-lib +# Minecraft +minecraft_version=1.20.1 +# Fabric +fabric_loader_version=0.18.4 +fabric_api_version=0.92.7+1.20.1 + +# Forge +forge_version=47.4.10 + +# Dependencies +architectury_version=9.2.14 +mixin_extras_version=0.5.3 +config_library_version_forge=1.1.8-forge-mc1.20 +config_library_version_fabric=1.1.8a-fabric-mc1.20.1 +permissions_api_version_fabric=0.3.1 +sqlite_version=3.47.2.0 +mysql_version=8.4.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 744c64d..9bf7bd3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/relocate_natives/download_codesign.py b/relocate_natives/download_codesign.py new file mode 100644 index 0000000..b2d938c --- /dev/null +++ b/relocate_natives/download_codesign.py @@ -0,0 +1,110 @@ +import os +import platform +import requests +import tarfile +import zipfile +from pathlib import Path + + +def get_platform_specific_filename(): + system = platform.system() + machine = platform.machine() + + if system == "Darwin": + if machine == "arm64": + return "apple-codesign-*-aarch64-apple-darwin.tar.gz" + else: + return "apple-codesign-*-x86_64-apple-darwin.tar.gz" + elif system == "Linux": + if machine == "aarch64": + return "apple-codesign-*-aarch64-unknown-linux-musl.tar.gz" + else: + return "apple-codesign-*-x86_64-unknown-linux-musl.tar.gz" + elif system == "Windows": + if machine.endswith("64"): + return "apple-codesign-*-x86_64-pc-windows-msvc.zip" + else: + return "apple-codesign-*-i686-pc-windows-msvc.zip" + else: + raise Exception(f"Unsupported platform: {system} {machine}") + + +def download_and_unpack(): + dest_dir = Path("./apple-codesign") + + repo_url = "https://api.github.com/repos/indygreg/apple-platform-rs/releases/latest" + dest_dir.mkdir(exist_ok=True) + + # Fetch the latest release info from GitHub + print("Fetching latest release information...") + response = requests.get(repo_url) + response.raise_for_status() + release_data = response.json() + + # Ensure release data has assets + if "assets" not in release_data: + raise Exception("Release data does not contain assets.") + + # Determine the correct asset + platform_filename = get_platform_specific_filename() + asset = next((asset for asset in release_data["assets"] if asset["name"].startswith("apple-codesign-") and asset["name"].endswith(platform_filename.split("*")[-1])), None) + + if not asset: + raise Exception(f"No matching asset found for platform: {platform_filename}") + + # Download the archive + print(f"Downloading {asset['name']}...") + download_url = asset["browser_download_url"] + archive_path = dest_dir / asset["name"] + + with requests.get(download_url, stream=True) as r: + r.raise_for_status() + with open(archive_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + print(f"Downloaded to {archive_path}") + + # Extract the archive + print("Extracting archive...") + temp_extract_dir = dest_dir / "temp_extract" + temp_extract_dir.mkdir(parents=True, exist_ok=True) + + if archive_path.suffix == ".zip": + with zipfile.ZipFile(archive_path, "r") as zip_ref: + zip_ref.extractall(temp_extract_dir) + elif archive_path.suffixes[-2:] == [".tar", ".gz"]: + with tarfile.open(archive_path, "r:gz") as tar_ref: + tar_ref.extractall(temp_extract_dir) + else: + raise Exception(f"Unknown archive format: {archive_path}") + + # Move contents of the root directory inside the archive to dest_dir + root_dir = next(temp_extract_dir.iterdir()) # Assuming only one root directory + for item in root_dir.iterdir(): + target_path = dest_dir / item.name + if target_path.exists(): + if target_path.is_dir(): + os.rmdir(target_path) + else: + os.remove(target_path) + item.rename(target_path) + + # Clean up temporary directories + for item in temp_extract_dir.iterdir(): + if item.is_dir(): + os.rmdir(item) + temp_extract_dir.rmdir() + + print(f"Extracted to {dest_dir}") + + # Clean up the archive + os.remove(archive_path) + print(f"Removed archive {archive_path}") + + +if __name__ == "__main__": + try: + download_and_unpack() + except Exception as e: + print(f"Error: {e}") diff --git a/relocate_natives/fix_modified_binary.py b/relocate_natives/fix_modified_binary.py new file mode 100644 index 0000000..daa2a9c --- /dev/null +++ b/relocate_natives/fix_modified_binary.py @@ -0,0 +1,31 @@ +import sys +import lief +import subprocess +import download_codesign +from pathlib import Path + +# Parse the input binary & xit if binary is invalid +output_path = sys.argv[1] +binary = lief.parse(sys.stdin.buffer.read()) +if binary is None: + exit(1) + +# Remove signature from Mac binaries +if isinstance(binary, lief.MachO.Binary): + binary.remove_signature() + +# Write the modified binary to the output path +binary.write(output_path) + +# Sign Mac binaries (required to make them usable because apple) +if isinstance(binary, lief.MachO.Binary): + print(f"Signing {output_path}...") + + # Check if the Apple code-signing files are available, if not, download them + if not Path("./apple-codesign/COPYING").exists(): + download_codesign.download_and_unpack() + + # Run the code-signing process + sign_process = subprocess.Popen(["./apple-codesign/rcodesign", "sign", output_path], shell=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + sign_process.wait() diff --git a/relocate_natives/prepare.ps1 b/relocate_natives/prepare.ps1 new file mode 100644 index 0000000..218ba69 --- /dev/null +++ b/relocate_natives/prepare.ps1 @@ -0,0 +1,5 @@ +$ErrorActionPreference = "Stop" + +python -m venv .venv +.\.venv\Scripts\activate +pip install -r requirements.txt diff --git a/relocate_natives/prepare.sh b/relocate_natives/prepare.sh new file mode 100755 index 0000000..52f9b3a --- /dev/null +++ b/relocate_natives/prepare.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +python -m venv .venv +. ./.venv/bin/activate +pip install -r requirements.txt diff --git a/relocate_natives/requirements.txt b/relocate_natives/requirements.txt new file mode 100644 index 0000000..3664d41 Binary files /dev/null and b/relocate_natives/requirements.txt differ diff --git a/settings.gradle b/settings.gradle index a71de20..8ca2dc1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ pluginManagement { maven { url "https://maven.fabricmc.net/" } maven { url "https://maven.architectury.dev/" } maven { url "https://maven.minecraftforge.net/" } + maven { url "https://maven.firstdark.dev/releases" } gradlePluginPortal() } }