mirror of
https://github.com/Jikoo/OpenInv.git
synced 2026-11-04 08:42:05 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fccdcf91 | ||
|
|
960acbfe9d | ||
|
|
3c904499c5 | ||
|
|
baa6b3d734 | ||
|
|
7f6c339e06 | ||
|
|
00736c8d30 | ||
|
|
cb1ec77c24 | ||
|
|
c8c8a72819 | ||
|
|
16f91ddc9f | ||
|
|
c83511e475 | ||
|
|
c095a78013 |
No files matched your search
@@ -8,3 +8,12 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
ignore:
|
||||
# The SQLite JDBC is provided by Spigot and, for now, Paper.
|
||||
# To ensure it is still present even if Paper stops providing it,
|
||||
# we need to declare it as a library.
|
||||
# To prevent needlessly downloading (and loading) a second copy,
|
||||
# we want to ensure that we are using the provided version.
|
||||
- dependency-name: "org.xerial:sqlite-jdbc"
|
||||
# Spigot and Spigot API updates are manual.
|
||||
- dependency-name: "org.spigotmc:*"
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
# We can't use 'maven' prebuilt cache setup because it requires that the project have a pom file.
|
||||
# BuildTools installs to Maven local if available, so it's easier to just rely on that.
|
||||
- name: Cache Spigot dependency
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.m2/repository/org/spigotmc/
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
# Upload artifacts
|
||||
- name: Upload Distributable Jar
|
||||
id: upload-final
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist/*
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release to CurseForge
|
||||
name: Release to external sites
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -8,30 +8,90 @@ jobs:
|
||||
curseforge_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- &parse-paper-versions
|
||||
name: Parse Paper Versions
|
||||
id: parse_versions
|
||||
run: |
|
||||
# Find line declaring Paper versions.
|
||||
raw=$(grep "\\*\\*Paper:\\*\\*" <<< "${{ github.event.release.body }}")
|
||||
|
||||
- name: Fetch Github Release Asset
|
||||
# Enable extended glob pattern to match 0 or more whitespace characters.
|
||||
shopt -s extglob
|
||||
# Trim Paper versions identifier prefix.
|
||||
raw=${raw/*([[:space:]])'**'Paper:'**'*([[:space:]])/}
|
||||
# Replace commas and optional spaces with a newline.
|
||||
raw=${raw//,*([[:space:]])/$'\n'}
|
||||
# Turn extglob back off.
|
||||
shopt -u extglob
|
||||
|
||||
echo "$raw"
|
||||
printf "PAPER_VERSIONS<<EOF\n%s\nEOF\n" "$raw" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- &fetch-release-asset
|
||||
name: Fetch Github Release Asset
|
||||
uses: dsaltares/fetch-gh-release-asset@1.1.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: ${{ github.event.release.id }}
|
||||
file: OpenInv.jar
|
||||
|
||||
- name: Set CurseForge Variables
|
||||
run: . scripts/set_curseforge_env.sh "${{ github.event.release.body }}"
|
||||
|
||||
- name: Create CurseForge Release
|
||||
uses: itsmeow/curseforge-upload@v3
|
||||
uses: Kira-NT/mc-publish@v3.3
|
||||
with:
|
||||
token: "${{ secrets.CURSEFORGE_TOKEN }}"
|
||||
project_id: 31432
|
||||
game_endpoint: minecraft
|
||||
file_path: ./OpenInv.jar
|
||||
display_name: "${{ github.event.release.name }}"
|
||||
game_versions: "${{ env.CURSEFORGE_MINECRAFT_VERSIONS }}"
|
||||
release_type: release
|
||||
changelog_type: markdown
|
||||
changelog: "${{ github.event.release.body }}"
|
||||
curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }}
|
||||
curseforge-id: 31432
|
||||
files: ./OpenInv.jar
|
||||
game-versions: "${{ steps.parse_versions.outputs.PAPER_VERSIONS }}"
|
||||
game-version-filter: releases
|
||||
|
||||
modrinth_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- *parse-paper-versions
|
||||
- *fetch-release-asset
|
||||
|
||||
- name: Create Modrinth Release
|
||||
uses: Kira-NT/mc-publish@v3.3
|
||||
with:
|
||||
modrinth-id: 1UlvXbzL
|
||||
modrinth-featured: false
|
||||
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
|
||||
files: ./OpenInv.jar
|
||||
game-versions: "${{ steps.parse_versions.outputs.PAPER_VERSIONS }}"
|
||||
game-version-filter: releases
|
||||
|
||||
hangar_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Parse Hangar Paper Versions
|
||||
id: parse_versions
|
||||
run: |
|
||||
# Find line declaring Paper versions.
|
||||
raw=$(grep "\\*\\*Paper:\\*\\*" <<< "${{ github.event.release.body }}")
|
||||
# Trim Paper versions identifier prefix.
|
||||
raw=${raw/'**'Paper:'**'/}
|
||||
|
||||
# Generate JSON for Hangar. GitHub Actions provides jq 1.7 in the 24.04 image.
|
||||
# Trim, then split on comma and optional spaces.
|
||||
hangar_versions=$(jq -nc --arg versions "$raw" '{"PAPER": $versions | trim | split(", *";null)}')
|
||||
|
||||
echo "$hangar_versions"
|
||||
echo "HANGAR_PAPER_VERSIONS=$hangar_versions" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- *fetch-release-asset
|
||||
|
||||
- name: Create Hangar Release
|
||||
uses: benwoo1110/hangar-upload-action@v1
|
||||
with:
|
||||
api_token: ${{ secrets.HANGAR_TOKEN }}
|
||||
slug: 'OpenInv'
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
channel: release
|
||||
files: |-
|
||||
[
|
||||
{
|
||||
"path": "./OpenInv.jar",
|
||||
"platforms": ["PAPER"]
|
||||
}
|
||||
]
|
||||
platform_dependencies: "${{ steps.parse_versions.outputs.HANGAR_PAPER_VERSIONS }}"
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo ${{ github.event.number }} > ./pr/pr_number
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: pr_number
|
||||
path: pr/
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
- name: Build resource pack
|
||||
id: upload-resource-pack
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: openinv-legibility-pack
|
||||
path: ./resource-pack/openinv-legibility-pack/
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2011-2022 lishid. All rights reserved.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.lishid.openinv.util;
|
||||
|
||||
import org.bukkit.permissions.Permissible;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An enum containing all permissions directly checked by OpenInv.
|
||||
*
|
||||
* <p>Note that this is not an exhaustive list! This does not contain
|
||||
* all permissions managed by Bukkit, largely parent nodes.</p>
|
||||
*/
|
||||
public enum Permissions {
|
||||
|
||||
/// Permission to open one's own inventory.
|
||||
INVENTORY_OPEN_SELF("inventory.open.self"),
|
||||
/// Permission to open someone else's inventory.
|
||||
INVENTORY_OPEN_OTHER("inventory.open.other"),
|
||||
/**
|
||||
* Permission to edit one's own inventory.
|
||||
*
|
||||
* <p>Note that this does not guarantee that the user has access to open the inventory!
|
||||
* Be sure to check {@link #INVENTORY_OPEN_SELF} first.</p>
|
||||
*/
|
||||
INVENTORY_EDIT_SELF("inventory.edit.self"),
|
||||
/**
|
||||
* Permission to edit someone else's inventory.
|
||||
*
|
||||
* <p>Note that this does not guarantee that the user has access to open the inventory!
|
||||
* Be sure to check {@link #INVENTORY_OPEN_OTHER} first.</p>
|
||||
*/
|
||||
INVENTORY_EDIT_OTHER("inventory.edit.other"),
|
||||
/// Permission to insert any item into the head slot.
|
||||
INVENTORY_SLOT_HEAD_ANY("inventory.slot.head.any"),
|
||||
/// Permission to insert any item into the chest slot.
|
||||
INVENTORY_SLOT_CHEST_ANY("inventory.slot.chest.any"),
|
||||
/// Permission to insert any item into the legs slot.
|
||||
INVENTORY_SLOT_LEGS_ANY("inventory.slot.legs.any"),
|
||||
/// Permission to insert any item into the feet slot.
|
||||
INVENTORY_SLOT_FEET_ANY("inventory.slot.feet.any"),
|
||||
/// Permission to drop items as the player via the drop slot.
|
||||
INVENTORY_SLOT_DROP("inventory.slot.drop"),
|
||||
|
||||
/// Permission to open one's own ender chest.
|
||||
ENDERCHEST_OPEN_SELF("enderchest.open.self"),
|
||||
/// Permission to open someone else's ender chest.
|
||||
ENDERCHEST_OPEN_OTHER("enderchest.open.other"),
|
||||
/**
|
||||
* Permission to edit one's own ender chest.
|
||||
*
|
||||
* <p>Note that this does not guarantee that the user has access to open the inventory!
|
||||
* Be sure to check {@link #ENDERCHEST_OPEN_SELF} first.</p>
|
||||
*/
|
||||
ENDERCHEST_EDIT_SELF("enderchest.edit.self"),
|
||||
/**
|
||||
* Permission to edit someone else's ender chest.
|
||||
*
|
||||
* <p>Note that this does not guarantee that the user has access to open the inventory!
|
||||
* Be sure to check {@link #ENDERCHEST_OPEN_OTHER} first.</p>
|
||||
*/
|
||||
ENDERCHEST_EDIT_OTHER("enderchest.edit.other"),
|
||||
|
||||
/// Permission to clear one's own inventory.
|
||||
CLEAR_SELF("clear.self"),
|
||||
/// Permission to clear someone else's inventory.
|
||||
CLEAR_OTHER("clear.other"),
|
||||
|
||||
/// Permission to view inventories and ender chests from other worlds.
|
||||
ACCESS_CROSSWORLD("access.crossworld"),
|
||||
/// Permission to access offline players' inventories and ender chests.
|
||||
ACCESS_OFFLINE("access.offline"),
|
||||
/// Permission to access online players' inventories and ender chests.
|
||||
ACCESS_ONLINE("access.online"),
|
||||
/// Permission granting the ability to edit players with the same access level.
|
||||
ACCESS_EQUAL_EDIT("access.equal.edit"),
|
||||
/// Permission to view, but not edit, players with the same access level.
|
||||
ACCESS_EQUAL_VIEW("access.equal.view"),
|
||||
/// Permission to deny access to players with the same access level.
|
||||
ACCESS_EQUAL_DENY("access.equal.deny"),
|
||||
|
||||
/// Permission to perform inventory interaction while in spectator mode.
|
||||
SPECTATE_CLICK("spectate.click"),
|
||||
|
||||
/// Permission to use the AnyContainer feature.
|
||||
CONTAINER_ANY("container.any"),
|
||||
/// Permission to use the AnyContainer feature but not the command to toggle it.
|
||||
CONTAINER_ANY_USE("container.any.use"),
|
||||
/// Permission to use the SilentContainer feature.
|
||||
CONTAINER_SILENT("container.silent"),
|
||||
/// Permission to use the SilentContainer feature but not the command to toggle it.
|
||||
CONTAINER_SILENT_USE("container.silent.use"),
|
||||
/// Permission to search inventories and ender chests.
|
||||
SEARCH_INVENTORY("search.inventory"),
|
||||
/// Permission to search containers.
|
||||
SEARCH_CONTAINER("search.container");
|
||||
|
||||
private final String permission;
|
||||
|
||||
Permissions(String permission) {
|
||||
this.permission = "openinv." + permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a {@link Permissible} has this permission.
|
||||
*
|
||||
* @param permissible the Permissible
|
||||
* @return true if the permission is granted
|
||||
*/
|
||||
public boolean hasPermission(@NotNull Permissible permissible) {
|
||||
return permissible.hasPermission(permission);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.lishid.openinv.util.setting;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
@@ -24,6 +25,15 @@ public interface PlayerToggle {
|
||||
*/
|
||||
boolean is(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Get the state of the toggle for a particular {@link Player},
|
||||
* accounting for permissions required to use the feature.
|
||||
*
|
||||
* @param player the player
|
||||
* @return true if the setting is enabled and the player has the required permissions
|
||||
*/
|
||||
boolean is(@NotNull Player player);
|
||||
|
||||
/**
|
||||
* Set the state of the toggle for a particular player ID.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.lishid.openinv.util.setting;
|
||||
|
||||
import com.lishid.openinv.util.Permissions;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
@@ -19,8 +21,20 @@ import java.util.UUID;
|
||||
public final class PlayerToggles {
|
||||
|
||||
private static final Map<String, PlayerToggle> TOGGLES = new HashMap<>();
|
||||
private static final PlayerToggle ANY = add(new MemoryToggle("AnyContainer"));
|
||||
private static final PlayerToggle SILENT = add(new MemoryToggle("SilentContainer"));
|
||||
private static final PlayerToggle ANY = add(new MemoryToggle("AnyContainer") {
|
||||
@Override
|
||||
public boolean is(@NotNull Player player) {
|
||||
return is(player.getUniqueId())
|
||||
&& (Permissions.CONTAINER_ANY.hasPermission(player) || Permissions.CONTAINER_ANY_USE.hasPermission(player));
|
||||
}
|
||||
});
|
||||
private static final PlayerToggle SILENT = add(new MemoryToggle("SilentContainer") {
|
||||
@Override
|
||||
public boolean is(@NotNull Player player) {
|
||||
return is(player.getUniqueId())
|
||||
&& (Permissions.CONTAINER_SILENT.hasPermission(player) || Permissions.CONTAINER_SILENT_USE.hasPermission(player));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the AnyContainer toggle.
|
||||
@@ -72,7 +86,7 @@ public final class PlayerToggles {
|
||||
throw new IllegalStateException("Cannot create instance of utility class.");
|
||||
}
|
||||
|
||||
private static class MemoryToggle implements PlayerToggle {
|
||||
private static abstract class MemoryToggle implements PlayerToggle {
|
||||
|
||||
private final @NotNull Set<UUID> enabled;
|
||||
private final @NotNull String name;
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011-2022 lishid. All rights reserved.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.lishid.openinv.util;
|
||||
|
||||
import org.bukkit.permissions.Permissible;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public enum Permissions {
|
||||
|
||||
INVENTORY_OPEN_SELF("inventory.open.self"),
|
||||
INVENTORY_OPEN_OTHER("inventory.open.other"),
|
||||
INVENTORY_EDIT_SELF("inventory.edit.self"),
|
||||
INVENTORY_EDIT_OTHER("inventory.edit.other"),
|
||||
INVENTORY_SLOT_HEAD_ANY("inventory.slot.head.any"),
|
||||
INVENTORY_SLOT_CHEST_ANY("inventory.slot.chest.any"),
|
||||
INVENTORY_SLOT_LEGS_ANY("inventory.slot.legs.any"),
|
||||
INVENTORY_SLOT_FEET_ANY("inventory.slot.feet.any"),
|
||||
INVENTORY_SLOT_DROP("inventory.slot.drop"),
|
||||
|
||||
ENDERCHEST_OPEN_SELF("enderchest.open.self"),
|
||||
ENDERCHEST_OPEN_OTHER("enderchest.open.other"),
|
||||
ENDERCHEST_EDIT_SELF("enderchest.edit.self"),
|
||||
ENDERCHEST_EDIT_OTHER("enderchest.edit.other"),
|
||||
|
||||
CLEAR_SELF("clear.self"),
|
||||
CLEAR_OTHER("clear.other"),
|
||||
|
||||
ACCESS_CROSSWORLD("access.crossworld"),
|
||||
ACCESS_OFFLINE("access.offline"),
|
||||
ACCESS_ONLINE("access.online"),
|
||||
ACCESS_EQUAL_EDIT("access.equal.edit"),
|
||||
ACCESS_EQUAL_VIEW("access.equal.view"),
|
||||
ACCESS_EQUAL_DENY("access.equal.deny"),
|
||||
|
||||
SPECTATE_CLICK("spectate.click"),
|
||||
|
||||
CONTAINER_ANY("container.any"),
|
||||
CONTAINER_SILENT("container.silent"),
|
||||
SEARCH_INVENTORY("search.inventory"),
|
||||
SEARCH_CONTAINER("search.container");
|
||||
|
||||
private final String permission;
|
||||
|
||||
Permissions(String permission) {
|
||||
this.permission = "openinv." + permission;
|
||||
}
|
||||
|
||||
public boolean hasPermission(@NotNull Permissible permissible) {
|
||||
return permissible.hasPermission(permission);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Project meta
|
||||
group=com.lishid.openinv
|
||||
version=5.2.0
|
||||
version=5.3.0
|
||||
description=A Bukkit plugin for opening normally-inaccessible inventories.
|
||||
|
||||
# Gradle configuration
|
||||
|
||||
@@ -4,11 +4,12 @@ specialsource = "1.11.5"
|
||||
planarwrappers = "3.3.0"
|
||||
annotations = "26.0.2-1"
|
||||
paperweight = "2.0.0-beta.17"
|
||||
shadow = "9.2.2"
|
||||
shadow = "9.3.1"
|
||||
folia-scheduler-wrapper = "v0.0.3"
|
||||
errorprone-core = "2.45.0"
|
||||
errorprone-gradle = "4.2.0"
|
||||
errorprone-core = "2.46.0"
|
||||
errorprone-gradle = "4.4.0"
|
||||
slf4j = "2.0.17"
|
||||
sqlite-jdbc = "3.49.1.0"
|
||||
|
||||
[libraries]
|
||||
spigotapi = { module = "org.spigotmc:spigot-api", version.ref = "spigotapi" }
|
||||
@@ -19,6 +20,7 @@ folia-scheduler-wrapper = { module = "com.github.NahuLD.folia-scheduler-wrapper:
|
||||
errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone-core" }
|
||||
errorprone-gradle = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorprone-gradle" }
|
||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" }
|
||||
|
||||
[plugins]
|
||||
paperweight = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" }
|
||||
|
||||
@@ -22,10 +22,14 @@ dependencies {
|
||||
implementation(project(":openinvadapterspigot", configuration = SpigotReobf.ARTIFACT_CONFIG))
|
||||
implementation(libs.planarwrappers)
|
||||
implementation(libs.folia.scheduler.wrapper)
|
||||
compileOnly(libs.sqlite.jdbc)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
expand("version" to version)
|
||||
expand(
|
||||
"version" to version,
|
||||
"sqlite" to libs.sqlite.jdbc.get().version
|
||||
)
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
|
||||
@@ -56,6 +56,7 @@ import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* The main class for OpenInv.
|
||||
@@ -69,6 +70,10 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
|
||||
private PlayerLoader playerLoader;
|
||||
private boolean isSpigot = false;
|
||||
|
||||
public PlayerLoader getPlayerLoader() {
|
||||
return playerLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
super.reloadConfig();
|
||||
@@ -96,6 +101,11 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
|
||||
@Override
|
||||
public void onDisable() {
|
||||
inventoryManager.evictAll();
|
||||
try {
|
||||
playerLoader.getProfileStore().shutdown();
|
||||
} catch (Exception e) {
|
||||
getLogger().log(Level.WARNING, "Failed to shut down profile store correctly", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -39,8 +39,6 @@ import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A listener managing AnyContainer, SilentContainer, and more.
|
||||
*/
|
||||
@@ -71,15 +69,14 @@ public class ContainerListener implements Listener {
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
UUID playerId = player.getUniqueId();
|
||||
boolean any = Permissions.CONTAINER_ANY.hasPermission(player) && PlayerToggles.any().is(playerId);
|
||||
boolean any = PlayerToggles.any().is(player);
|
||||
boolean needsAny = accessor.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock());
|
||||
|
||||
if (!any && needsAny) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean silent = Permissions.CONTAINER_SILENT.hasPermission(player) && PlayerToggles.silent().is(playerId);
|
||||
boolean silent = PlayerToggles.silent().is(player);
|
||||
|
||||
// If anycontainer or silentcontainer is active
|
||||
if (any || silent) {
|
||||
@@ -104,7 +101,7 @@ public class ContainerListener implements Listener {
|
||||
}
|
||||
|
||||
InventoryHolder holder = event.getInventory().getHolder();
|
||||
if (PlayerToggles.silent().is(player.getUniqueId())
|
||||
if (PlayerToggles.silent().is(player)
|
||||
&& holder != null
|
||||
&& this.accessor.getAnySilentContainer().isAnySilentContainer(holder)) {
|
||||
this.accessor.getAnySilentContainer().deactivateContainer(player);
|
||||
|
||||
@@ -5,13 +5,16 @@ import com.google.common.cache.CacheBuilder;
|
||||
import com.google.errorprone.annotations.Keep;
|
||||
import com.lishid.openinv.OpenInv;
|
||||
import com.lishid.openinv.util.config.Config;
|
||||
import com.lishid.openinv.util.profile.OfflinePlayerProfileStore;
|
||||
import com.lishid.openinv.util.profile.Profile;
|
||||
import com.lishid.openinv.util.profile.ProfileStore;
|
||||
import com.lishid.openinv.util.profile.sqlite.SqliteProfileStore;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.profile.PlayerProfile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -33,7 +36,8 @@ public class PlayerLoader implements Listener {
|
||||
private final @NotNull InventoryManager inventoryManager;
|
||||
private final @NotNull InternalAccessor internalAccessor;
|
||||
private final @NotNull Logger logger;
|
||||
private final @NotNull Cache<String, PlayerProfile> lookupCache;
|
||||
private final @NotNull Cache<String, Profile> lookupCache;
|
||||
private @NotNull ProfileStore profileStore;
|
||||
|
||||
public PlayerLoader(
|
||||
@NotNull OpenInv plugin,
|
||||
@@ -46,10 +50,30 @@ public class PlayerLoader implements Listener {
|
||||
this.config = config;
|
||||
this.inventoryManager = inventoryManager;
|
||||
this.internalAccessor = internalAccessor;
|
||||
try {
|
||||
SqliteProfileStore sqliteStore = new SqliteProfileStore(plugin);
|
||||
sqliteStore.setup();
|
||||
sqliteStore.tryImport();
|
||||
this.profileStore = sqliteStore;
|
||||
} catch (Exception e) {
|
||||
this.profileStore = new OfflinePlayerProfileStore(logger);
|
||||
}
|
||||
this.logger = logger;
|
||||
this.lookupCache = CacheBuilder.newBuilder().maximumSize(20).build();
|
||||
}
|
||||
|
||||
public @NotNull ProfileStore getProfileStore() {
|
||||
return profileStore;
|
||||
}
|
||||
|
||||
public void setProfileStore(@NotNull ProfileStore profileStore) {
|
||||
plugin.getLogger().log(
|
||||
Level.INFO,
|
||||
() -> "Setting profile store implementation to " + profileStore.getClass().getName()
|
||||
);
|
||||
this.profileStore = profileStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a {@link Player} from an {@link OfflinePlayer}. If the user has not played before or the default world for
|
||||
* the server is not loaded, this will return {@code null}.
|
||||
@@ -59,22 +83,20 @@ public class PlayerLoader implements Listener {
|
||||
* @throws IllegalStateException if the server version is unsupported
|
||||
*/
|
||||
public @Nullable Player load(@NotNull OfflinePlayer offline) {
|
||||
UUID key = offline.getUniqueId();
|
||||
|
||||
Player player = offline.getPlayer();
|
||||
if (player != null) {
|
||||
return player;
|
||||
}
|
||||
|
||||
player = inventoryManager.getLoadedPlayer(key);
|
||||
if (player != null) {
|
||||
return player;
|
||||
}
|
||||
|
||||
if (config.isOfflineDisabled() || !internalAccessor.isSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
player = inventoryManager.getLoadedPlayer(offline.getUniqueId());
|
||||
if (player != null) {
|
||||
return player;
|
||||
}
|
||||
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
return internalAccessor.getPlayerDataManager().loadPlayer(offline);
|
||||
}
|
||||
@@ -93,13 +115,6 @@ public class PlayerLoader implements Listener {
|
||||
}
|
||||
|
||||
public @Nullable OfflinePlayer matchExact(@NotNull String name) {
|
||||
// Warn if called on the main thread - if we resort to searching offline players, this may take several seconds.
|
||||
if (Bukkit.getServer().isPrimaryThread()) {
|
||||
logger.warning("Call to PlayerSearchCache#matchPlayer made on the main thread!");
|
||||
logger.warning("This can cause the server to hang, potentially severely.");
|
||||
logger.log(Level.WARNING, "Current stack trace", new Throwable("Current stack trace"));
|
||||
}
|
||||
|
||||
OfflinePlayer player;
|
||||
|
||||
try {
|
||||
@@ -123,9 +138,9 @@ public class PlayerLoader implements Listener {
|
||||
}
|
||||
|
||||
// Cached offline match.
|
||||
PlayerProfile cachedResult = lookupCache.getIfPresent(name);
|
||||
if (cachedResult != null && cachedResult.getUniqueId() != null) {
|
||||
player = Bukkit.getOfflinePlayer(cachedResult.getUniqueId());
|
||||
Profile cachedResult = lookupCache.getIfPresent(name);
|
||||
if (cachedResult != null) {
|
||||
player = Bukkit.getOfflinePlayer(cachedResult.id());
|
||||
// Ensure player is an existing player.
|
||||
if (player.hasPlayedBefore() || player.isOnline()) {
|
||||
return player;
|
||||
@@ -135,10 +150,15 @@ public class PlayerLoader implements Listener {
|
||||
}
|
||||
|
||||
// Exact offline match second - ensure offline access works when matchable users are online.
|
||||
player = Bukkit.getServer().getOfflinePlayer(name);
|
||||
Profile profile = profileStore.getProfileExact(name);
|
||||
if (profile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
player = Bukkit.getOfflinePlayer(profile.id());
|
||||
|
||||
if (player.hasPlayedBefore()) {
|
||||
lookupCache.put(name, player.getPlayerProfile());
|
||||
lookupCache.put(name, profile);
|
||||
return player;
|
||||
}
|
||||
|
||||
@@ -160,40 +180,35 @@ public class PlayerLoader implements Listener {
|
||||
}
|
||||
|
||||
// Finally, inexact offline match.
|
||||
float bestMatch = 0;
|
||||
for (OfflinePlayer offline : Bukkit.getServer().getOfflinePlayers()) {
|
||||
if (offline.getName() == null) {
|
||||
// Loaded by UUID only, name has never been looked up.
|
||||
continue;
|
||||
}
|
||||
Profile profile = getProfileStore().getProfileInexact(name);
|
||||
|
||||
float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName());
|
||||
|
||||
if (currentMatch == 1.0F) {
|
||||
return offline;
|
||||
}
|
||||
|
||||
if (currentMatch > bestMatch) {
|
||||
bestMatch = currentMatch;
|
||||
player = offline;
|
||||
}
|
||||
if (profile == null) {
|
||||
// No match found.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
// If a match was found, store it.
|
||||
lookupCache.put(name, player.getPlayerProfile());
|
||||
// Get associated player and store match.
|
||||
player = Bukkit.getOfflinePlayer(profile.id());
|
||||
if (player.hasPlayedBefore()) {
|
||||
lookupCache.put(name, profile);
|
||||
return player;
|
||||
}
|
||||
|
||||
// No players have ever joined the server.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Keep
|
||||
@EventHandler
|
||||
private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
|
||||
plugin.getScheduler().runTaskLaterAsynchronously(() -> updateMatches(event), 7L);
|
||||
}
|
||||
|
||||
private void updateMatches(@NotNull PlayerJoinEvent event) {
|
||||
// Update profile store.
|
||||
profileStore.addProfile(new Profile(event.getPlayer()));
|
||||
|
||||
// If player is not new, any cached values are valid.
|
||||
if (event.getPlayer().hasPlayedBefore()) {
|
||||
if (event.getPlayer().hasPlayedBefore() || lookupCache.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,36 +216,19 @@ public class PlayerLoader implements Listener {
|
||||
String name = event.getPlayer().getName();
|
||||
lookupCache.invalidate(name);
|
||||
|
||||
// If the cache is empty, nothing to do. Don't hit scheduler.
|
||||
if (lookupCache.size() == 0) {
|
||||
return;
|
||||
Iterator<Map.Entry<String, Profile>> iterator = lookupCache.asMap().entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Profile> entry = iterator.next();
|
||||
String oldMatch = entry.getValue().name();
|
||||
String lookup = entry.getKey();
|
||||
float oldMatchScore = StringMetric.compareJaroWinkler(lookup, oldMatch);
|
||||
float newMatchScore = StringMetric.compareJaroWinkler(lookup, name);
|
||||
|
||||
// If new match exceeds old match, delete old match.
|
||||
if (newMatchScore > oldMatchScore) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getScheduler().runTaskLaterAsynchronously(
|
||||
() -> {
|
||||
Iterator<Map.Entry<String, PlayerProfile>> iterator = lookupCache.asMap().entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, PlayerProfile> entry = iterator.next();
|
||||
String oldMatch = entry.getValue().getName();
|
||||
|
||||
// Shouldn't be possible - all profiles should be complete.
|
||||
if (oldMatch == null) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
String lookup = entry.getKey();
|
||||
float oldMatchScore = StringMetric.compareJaroWinkler(lookup, oldMatch);
|
||||
float newMatchScore = StringMetric.compareJaroWinkler(lookup, name);
|
||||
|
||||
// If new match exceeds old match, delete old match.
|
||||
if (newMatchScore > oldMatchScore) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
7L
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.lishid.openinv.util.profile;
|
||||
|
||||
import com.github.jikoo.planarwrappers.scheduler.TickTimeUnit;
|
||||
import me.nahu.scheduler.wrapper.WrappedJavaPlugin;
|
||||
import me.nahu.scheduler.wrapper.runnable.WrappedRunnable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public abstract class BatchProfileStore implements ProfileStore {
|
||||
|
||||
private final Set<Profile> pending = Collections.synchronizedSet(new HashSet<>());
|
||||
private final AtomicReference<WrappedRunnable> insertTask = new AtomicReference<>();
|
||||
protected final @NotNull WrappedJavaPlugin plugin;
|
||||
|
||||
protected BatchProfileStore(@NotNull WrappedJavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addProfile(@NotNull Profile profile) {
|
||||
pending.add(profile);
|
||||
buildBatch();
|
||||
}
|
||||
|
||||
private void buildBatch() {
|
||||
if (insertTask.compareAndSet(null, new WrappedRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
pushBatch();
|
||||
}
|
||||
})) {
|
||||
try {
|
||||
// Wait 5 seconds to accumulate other player data to reduce scheduler load on larger servers.
|
||||
insertTask.get().runTaskLaterAsynchronously(plugin, TickTimeUnit.toTicks(5, TimeUnit.SECONDS));
|
||||
} catch (IllegalStateException e) {
|
||||
// If scheduling task fails, server is most likely shutting down.
|
||||
insertTask.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pushBatch() {
|
||||
Set<Profile> batch = new HashSet<>(pending);
|
||||
// This is a bit roundabout but removes the risk of data loss.
|
||||
pending.removeAll(batch);
|
||||
|
||||
// Push current batch.
|
||||
pushBatch(batch);
|
||||
|
||||
WrappedRunnable running = insertTask.getAndSet(null);
|
||||
if (running != null) {
|
||||
running.cancel();
|
||||
}
|
||||
|
||||
// If more profiles have been added, build another batch.
|
||||
if (!pending.isEmpty()) {
|
||||
buildBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
WrappedRunnable wrappedRunnable = insertTask.get();
|
||||
if (wrappedRunnable != null) {
|
||||
wrappedRunnable.cancel();
|
||||
}
|
||||
pushBatch();
|
||||
insertTask.set(null);
|
||||
}
|
||||
|
||||
protected abstract void pushBatch(@NotNull Set<Profile> batch);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.lishid.openinv.util.profile;
|
||||
|
||||
import me.nahu.scheduler.wrapper.runnable.WrappedRunnable;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class OfflinePlayerImporter extends WrappedRunnable {
|
||||
|
||||
private final @NotNull BatchProfileStore profileStore;
|
||||
private final int batchSize;
|
||||
|
||||
public OfflinePlayerImporter(@NotNull BatchProfileStore profileStore, int batchSize) {
|
||||
this.profileStore = profileStore;
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Set<Profile> batch = new HashSet<>();
|
||||
for (OfflinePlayer offline : Bukkit.getOfflinePlayers()) {
|
||||
String name = offline.getName();
|
||||
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
batch.add(new Profile(name, offline.getUniqueId()));
|
||||
|
||||
if (batch.size() >= batchSize) {
|
||||
profileStore.pushBatch(batch);
|
||||
batch.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (!batch.isEmpty()) {
|
||||
profileStore.pushBatch(batch);
|
||||
}
|
||||
|
||||
onComplete();
|
||||
}
|
||||
|
||||
public abstract void onComplete();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.lishid.openinv.util.profile;
|
||||
|
||||
import com.lishid.openinv.util.StringMetric;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class OfflinePlayerProfileStore implements ProfileStore {
|
||||
|
||||
private final @NotNull Logger logger;
|
||||
|
||||
public OfflinePlayerProfileStore(@NotNull Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addProfile(@NotNull Profile profile) {
|
||||
// No-op. Server handles profile creation and storage.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup() {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// No-op. Nothing to push.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tryImport() {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile getProfileExact(@NotNull String name) {
|
||||
ProfileStore.warnMainThread(logger);
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer offline = Bukkit.getOfflinePlayer(name);
|
||||
|
||||
if (!offline.hasPlayedBefore()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String realName = offline.getName();
|
||||
if (realName == null) {
|
||||
realName = name;
|
||||
}
|
||||
|
||||
return new Profile(realName, offline.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile getProfileInexact(@NotNull String search) {
|
||||
ProfileStore.warnMainThread(logger);
|
||||
|
||||
float bestMatch = 0.0F;
|
||||
Profile bestProfile = null;
|
||||
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
|
||||
String name = player.getName();
|
||||
if (name == null) {
|
||||
// Discount UUID-only profiles; Direct UUID lookup should already be done.
|
||||
return null;
|
||||
}
|
||||
|
||||
float currentMatch = StringMetric.compareJaroWinkler(name, name);
|
||||
|
||||
if (currentMatch > bestMatch) {
|
||||
bestMatch = currentMatch;
|
||||
bestProfile = new Profile(name, player.getUniqueId());
|
||||
}
|
||||
|
||||
// A score of 1 is an exact match. Don't bother checking the rest.
|
||||
if (currentMatch == 1.0F) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bestProfile;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.lishid.openinv.util.profile;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record Profile(@NotNull String name, @NotNull UUID id) {
|
||||
|
||||
public Profile(@NotNull Player player) {
|
||||
this(player.getName(), player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// As names are the unique key, profiles with the same name should collide.
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.lishid.openinv.util.profile;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public interface ProfileStore {
|
||||
|
||||
void addProfile(@NotNull Profile profile);
|
||||
|
||||
void setup() throws Exception;
|
||||
|
||||
void shutdown() throws Exception;
|
||||
|
||||
void tryImport() throws Exception;
|
||||
|
||||
@Nullable Profile getProfileExact(@NotNull String name);
|
||||
|
||||
@Nullable Profile getProfileInexact(@NotNull String search);
|
||||
|
||||
static void warnMainThread(@NotNull Logger logger) {
|
||||
if (!Bukkit.getServer().isPrimaryThread()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Throwable throwable = new Throwable("Current stack trace");
|
||||
StackTraceElement[] stackTrace = throwable.getStackTrace();
|
||||
|
||||
if (stackTrace.length < 2) {
|
||||
// Not possible.
|
||||
return;
|
||||
}
|
||||
|
||||
StackTraceElement caller = stackTrace[1];
|
||||
|
||||
logger.warning(() ->
|
||||
String.format(
|
||||
"Call to %s#%s made on the main thread!",
|
||||
caller.getClassName(),
|
||||
caller.getMethodName()
|
||||
)
|
||||
);
|
||||
logger.warning("This can cause the server to hang, potentially severely.");
|
||||
logger.log(Level.WARNING, "Current stack trace", throwable);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lishid.openinv.util.profile.sqlite;
|
||||
|
||||
import com.lishid.openinv.util.StringMetric;
|
||||
import org.sqlite.Function;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class JaroWinklerFunction extends Function {
|
||||
|
||||
@Override
|
||||
protected void xFunc() throws SQLException {
|
||||
if (args() != 2) {
|
||||
throw new SQLException("JaroWinkler(str, str) requires 2 arguments but got " + args());
|
||||
}
|
||||
String val1 = value_text(0);
|
||||
String val2 = value_text(1);
|
||||
|
||||
result(StringMetric.compareJaroWinkler(val1 == null ? "" : val1, val2 == null ? "" : val2));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package com.lishid.openinv.util.profile.sqlite;
|
||||
|
||||
import com.github.jikoo.planarwrappers.function.ThrowingFunction;
|
||||
import com.lishid.openinv.util.profile.BatchProfileStore;
|
||||
import com.lishid.openinv.util.profile.OfflinePlayerImporter;
|
||||
import com.lishid.openinv.util.profile.Profile;
|
||||
import me.nahu.scheduler.wrapper.WrappedJavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.sqlite.Function;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class SqliteProfileStore extends BatchProfileStore {
|
||||
|
||||
private static final int BATCH_SIZE = 1_000;
|
||||
private static final int MAX_BATCHES = 20;
|
||||
|
||||
private Connection connection;
|
||||
|
||||
public SqliteProfileStore(@NotNull WrappedJavaPlugin plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup() throws Exception {
|
||||
// Touch implementation to ensure it is available, then create connection.
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + "/profiles.db");
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
// Create main profile table.
|
||||
statement.executeUpdate(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS profiles(
|
||||
name TEXT PRIMARY KEY,
|
||||
uuid_least INTEGER NOT NULL,
|
||||
uuid_most INTEGER NOT NULL,
|
||||
last_update TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) WITHOUT ROWID
|
||||
"""
|
||||
);
|
||||
// Create meta table.
|
||||
statement.executeUpdate(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS openinv_meta(
|
||||
name TEXT PRIMARY KEY,
|
||||
value INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
// Add JaroWinkler function to the connection.
|
||||
Function.create(connection, "JaroWinkler", new JaroWinklerFunction());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
try {
|
||||
if (!connection.getAutoCommit()) {
|
||||
connection.commit();
|
||||
}
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Exception closing database: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tryImport() throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT value FROM openinv_meta WHERE name = 'imported'"
|
||||
)) {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next() && resultSet.getInt("value") == 1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Indexing player names.");
|
||||
new OfflinePlayerImporter(this, BATCH_SIZE) {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO openinv_meta(name, value) VALUES ('imported', 1)"
|
||||
)) {
|
||||
statement.executeUpdate();
|
||||
plugin.getLogger().info("Indexed player names successfully.");
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Exception marking import complete: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}.runTaskAsynchronously(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void pushBatch(@NotNull Set<Profile> batch) {
|
||||
if (batch.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int batchSize = Math.min(batch.size(), BATCH_SIZE);
|
||||
int rem = batch.size() % batchSize;
|
||||
|
||||
Iterator<Profile> iterator = batch.iterator();
|
||||
|
||||
try {
|
||||
boolean autoCommit = connection.getAutoCommit();
|
||||
if (autoCommit) {
|
||||
connection.setAutoCommit(false);
|
||||
}
|
||||
|
||||
pushBatch(iterator, batch.size(), batchSize);
|
||||
|
||||
if (rem > 0) {
|
||||
pushBatch(iterator, rem, rem);
|
||||
}
|
||||
|
||||
connection.commit();
|
||||
|
||||
if (autoCommit) {
|
||||
connection.setAutoCommit(true);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Encountered an exception updating profiles", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void pushBatch(Iterator<Profile> iterator, int iteratorSize, int batchSize) throws SQLException {
|
||||
try (PreparedStatement upsert = createBulkUpsertProfile(batchSize)) {
|
||||
for (int batchIndex = 0; batchIndex < iteratorSize / batchSize; ++batchIndex) {
|
||||
for (int entryIndex = 0; entryIndex < batchSize; ++entryIndex) {
|
||||
int startIndex = entryIndex * 3;
|
||||
Profile profile = iterator.next();
|
||||
upsert.setString(startIndex + 1, profile.name());
|
||||
upsert.setLong(startIndex + 2, profile.id().getLeastSignificantBits());
|
||||
upsert.setLong(startIndex + 3, profile.id().getMostSignificantBits());
|
||||
}
|
||||
upsert.addBatch();
|
||||
|
||||
// If we're at the maximum number of batches allowed, commit.
|
||||
if ((batchIndex + 1) % MAX_BATCHES == 0) {
|
||||
upsert.executeBatch();
|
||||
connection.commit();
|
||||
}
|
||||
}
|
||||
upsert.executeBatch();
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull PreparedStatement createBulkUpsertProfile(int count) throws SQLException {
|
||||
return connection.prepareStatement(
|
||||
"""
|
||||
INSERT INTO profiles(name, uuid_least, uuid_most)
|
||||
VALUES (?, ?, ?)
|
||||
""" + ", (?, ?, ?)".repeat(count - 1) +
|
||||
"""
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
uuid_least=excluded.uuid_least,
|
||||
uuid_most=excluded.uuid_most,
|
||||
last_update=CURRENT_TIMESTAMP
|
||||
"""
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile getProfileExact(@NotNull String name) {
|
||||
return getProfile(
|
||||
name,
|
||||
text -> {
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT name, uuid_least, uuid_most FROM `profiles` WHERE name = ?"
|
||||
);
|
||||
statement.setString(1, text);
|
||||
return statement;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile getProfileInexact(@NotNull String search) {
|
||||
return getProfile(
|
||||
search,
|
||||
text -> {
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"""
|
||||
SELECT name, uuid_least, uuid_most FROM `profiles`
|
||||
WHERE name LIKE ?
|
||||
ORDER BY JaroWinkler(?, name) DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
);
|
||||
statement.setString(1, getLikePrefix(text));
|
||||
statement.setString(2, text);
|
||||
return statement;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private @NotNull String getLikePrefix(@NotNull String search) {
|
||||
StringBuilder prefix = new StringBuilder();
|
||||
int searchLen = search.length();
|
||||
if (searchLen > 0) {
|
||||
char first = search.charAt(0);
|
||||
|
||||
// Add first character of the search to LIKE prefix.
|
||||
prefix.append(first);
|
||||
|
||||
// If the first character appears to be Geyser-like, append another.
|
||||
if (searchLen > 1 && (first == '.' || first == '*')) {
|
||||
prefix.append(search.charAt(1));
|
||||
}
|
||||
}
|
||||
|
||||
// Add wildcard.
|
||||
prefix.append('%');
|
||||
return prefix.toString();
|
||||
}
|
||||
|
||||
private @Nullable Profile getProfile(
|
||||
@NotNull String text,
|
||||
@NotNull ThrowingFunction<String, PreparedStatement, SQLException> create
|
||||
) {
|
||||
try (PreparedStatement statement = create.apply(text)) {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
// Fetch profile data.
|
||||
String name = resultSet.getString(1);
|
||||
|
||||
// Ignore illegal data.
|
||||
if (name == null || name.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UUID uuid = new UUID(resultSet.getLong(3), resultSet.getLong(2));
|
||||
return new Profile(name, uuid);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Encountered an exception retrieving profile", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,9 @@ description: Open a player's inventory as a chest and interact with it in real t
|
||||
api-version: "1.13"
|
||||
folia-supported: true
|
||||
|
||||
libraries:
|
||||
- "org.xerial:sqlite-jdbc:${sqlite}"
|
||||
|
||||
permissions:
|
||||
|
||||
openinv:
|
||||
@@ -76,8 +79,16 @@ permissions:
|
||||
# Container features
|
||||
openinv.container:
|
||||
children:
|
||||
openinv.container.any: true
|
||||
openinv.container.silent: true
|
||||
openinv.container.any:
|
||||
children:
|
||||
openinv.container.any.use: true
|
||||
openinv.container.any.use:
|
||||
default: false
|
||||
openinv.container.silent:
|
||||
children:
|
||||
openinv.container.silent.use: true
|
||||
openinv.container.silent.use:
|
||||
default: false
|
||||
# Search functionality
|
||||
openinv.search:
|
||||
children:
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright (C) 2011-2021 lishid. All rights reserved.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, version 3.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Note that this script is designed for use in GitHub Actions, and is not
|
||||
# particularly robust nor configurable. Run from project parent directory.
|
||||
|
||||
if [[ ! $1 ]]; then
|
||||
echo "No changelog, no Minecraft versions."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Find line declaring Paper versions.
|
||||
raw=$(grep "**Paper:**" <<< "$1")
|
||||
|
||||
# Enable extended glob pattern to match 0 or more whitespace characters.
|
||||
shopt -s extglob
|
||||
# Trim Paper versions identifier prefix.
|
||||
raw=${raw##*([[:space:]])'**'Paper:'**'*([[:space:]])}
|
||||
# Replace commas and optional spaces with a newline.
|
||||
raw=${raw//,*([[:space:]])/$'\n'}
|
||||
# Turn extglob back off.
|
||||
shopt -u extglob
|
||||
|
||||
# Split into an array on newlines.
|
||||
readarray -td $'\n' versions <<< "${raw}"
|
||||
|
||||
for version in "${versions[@]}"; do
|
||||
# Parse Minecraft minor version by dropping everything from the second period onward.
|
||||
# CurseForge doesn't usually add patch versions for Bukkit, so we're more likely to
|
||||
# hit a supported identifier this way.
|
||||
version="${version%[.-]"${version#*.*[.-]}"}"
|
||||
|
||||
# Skip already listed versions
|
||||
if [[ "$minecraft_versions" =~ "$version"($|,) ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Append comma if variable is set, then append version.
|
||||
# Note that Minecraft versions on CurseForge are declared "Minecraft x.y.z"
|
||||
minecraft_versions="${minecraft_versions:+${minecraft_versions},}Minecraft ${version}"
|
||||
done
|
||||
|
||||
printf "$minecraft_versions\n"
|
||||
#echo "CURSEFORGE_MINECRAFT_VERSIONS=$minecraft_versions" >> "$GITHUB_ENV"
|
||||
Reference in new issue
Block a user