Compare commits

..

1 Commits

Author SHA1 Message Date
Auxilor
4a8935f06d Added custom blocks API with initial oraxen integration 2024-07-30 15:57:44 +01:00
118 changed files with 1664 additions and 3398 deletions

View File

@@ -30,7 +30,7 @@ jobs:
- run: ./gradlew build --full-stacktrace
# - uses: actions/upload-artifact@v2
# with:
# name: eco-dev-${{ steps.vars.outputs.sha_short }}
# path: build/libs
- uses: actions/upload-artifact@v2
with:
name: eco-dev-${{ steps.vars.outputs.sha_short }}
path: build/libs

3
.gitignore vendored
View File

@@ -22,6 +22,3 @@ gradle-app.setting
# Mac OSX
.DS_Store
# Kotlin
.kotlin

View File

@@ -1,21 +1,20 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21")
}
}
plugins {
id("java-library")
id("com.gradleup.shadow") version "8.3.5"
id("io.github.goooler.shadow") version "8.1.7"
id("maven-publish")
id("java")
kotlin("jvm") version "2.1.0"
kotlin("jvm") version "1.9.21"
kotlin("plugin.serialization") version "1.9.21"
}
dependencies {
@@ -34,29 +33,23 @@ dependencies {
implementation(project(path = ":eco-core:core-nms:v1_20_R2", configuration = "reobf"))
implementation(project(path = ":eco-core:core-nms:v1_20_R3", configuration = "reobf"))
implementation(project(path = ":eco-core:core-nms:v1_21", configuration = "reobf"))
implementation(project(path = ":eco-core:core-nms:v1_21_3", configuration = "reobf"))
implementation(project(path = ":eco-core:core-nms:v1_21_4", configuration = "reobf"))
}
allprojects {
apply(plugin = "java")
apply(plugin = "java-library")
apply(plugin = "maven-publish")
apply(plugin = "com.gradleup.shadow")
apply(plugin = "io.github.goooler.shadow")
apply(plugin = "kotlin")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
repositories {
mavenCentral()
maven("https://repo.auxilor.io/repository/maven-public/")
maven("https://jitpack.io") {
content { includeGroupByRegex("com\\.github\\..*") }
}
// Paper
maven("https://repo.papermc.io/repository/maven-public/")
// SuperiorSkyblock2
maven("https://repo.bg-software.com/repository/api/")
@@ -70,7 +63,7 @@ allprojects {
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
// ProtocolLib
maven("https://repo.dmulloy2.net/nexus/repository/public/")
//maven("https://repo.dmulloy2.net/nexus/repository/public/")
// WorldGuard
maven("https://maven.enginehub.org/repo/")
@@ -111,12 +104,12 @@ allprojects {
dependencies {
// Kotlin
implementation(kotlin("stdlib", version = "2.1.0"))
implementation(kotlin("stdlib", version = "1.9.21"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// Included in spigot jar, no need to move to implementation
compileOnly("org.jetbrains:annotations:23.0.0")
compileOnly("com.google.guava:guava:32.0.0-jre")
compileOnly("com.google.guava:guava:31.1-jre")
// Test
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2")
@@ -160,8 +153,8 @@ allprojects {
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
kotlinOptions {
jvmTarget = "17"
}
}
@@ -184,14 +177,14 @@ allprojects {
}
withType<JavaCompile>().configureEach {
options.release.set(17)
options.release = 17
}
}
java {
withSourcesJar()
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
languageVersion = JavaLanguageVersion.of(21)
}
}
}
@@ -219,6 +212,7 @@ tasks {
//relocate("com.mysql", "com.willfp.eco.libs.mysql")
relocate("com.mongodb", "com.willfp.eco.libs.mongodb")
relocate("org.bson", "com.willfp.eco.libs.bson")
relocate("org.litote", "com.willfp.eco.libs.litote")
relocate("org.reactivestreams", "com.willfp.eco.libs.reactivestreams")
relocate("reactor.", "com.willfp.eco.libs.reactor.") // Dot in name to be safe
relocate("com.moandjiezana.toml", "com.willfp.eco.libs.toml")

View File

@@ -38,23 +38,15 @@ public class Prerequisite {
);
/**
* Requires the server to be running at least 1.21.3.
*/
public static final Prerequisite HAS_1_21_3 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("1_21_3"),
"Requires server to be running 1.21.3+"
);
/**
* Requires the server to be running at least 1.21.
* Requires the server to be running 1.21.
*/
public static final Prerequisite HAS_1_21 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("1_21") || HAS_1_21_3.isMet(),
() -> ProxyConstants.NMS_VERSION.contains("1_21"),
"Requires server to be running 1.21+"
);
/**
* Requires the server to be running at least 1.20.5.
* Requires the server to be running 1.20.5.
*/
public static final Prerequisite HAS_1_20_5 = new Prerequisite(
() -> (ProxyConstants.NMS_VERSION.contains("1_20_") && !ProxyConstants.NMS_VERSION.contains("R"))
@@ -63,7 +55,7 @@ public class Prerequisite {
);
/**
* Requires the server to be running at least 1.20.3.
* Requires the server to be running 1.20.3.
*/
public static final Prerequisite HAS_1_20_3 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("20_R3") || HAS_1_20_5.isMet(),
@@ -71,7 +63,7 @@ public class Prerequisite {
);
/**
* Requires the server to be running at least 1.20.
* Requires the server to be running 1.20.
*/
public static final Prerequisite HAS_1_20 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("20") || HAS_1_20_3.isMet(),
@@ -79,7 +71,7 @@ public class Prerequisite {
);
/**
* Requires the server to be running at least 1.19.4.
* Requires the server to be running 1.19.4.
*/
public static final Prerequisite HAS_1_19_4 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("19_R3") || HAS_1_20.isMet(),
@@ -87,7 +79,7 @@ public class Prerequisite {
);
/**
* Requires the server to be running at least 1.19.
* Requires the server to be running 1.19.
*/
public static final Prerequisite HAS_1_19 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("19") || HAS_1_20.isMet(),
@@ -95,7 +87,7 @@ public class Prerequisite {
);
/**
* Requires the server to be running at least 1.18.
* Requires the server to be running 1.18.
*/
public static final Prerequisite HAS_1_18 = new Prerequisite(
() -> ProxyConstants.NMS_VERSION.contains("18") || HAS_1_19.isMet(),

View File

@@ -0,0 +1,149 @@
package com.willfp.eco.core.blocks;
import com.willfp.eco.core.blocks.impl.EmptyTestableBlock;
import com.willfp.eco.core.blocks.impl.MaterialTestableBlock;
import com.willfp.eco.core.blocks.impl.UnrestrictedMaterialTestableBlock;
import com.willfp.eco.core.blocks.provider.BlockProvider;
import com.willfp.eco.util.NamespacedKeyUtils;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Class to manage all custom and vanilla blocks.
*/
public final class Blocks {
/**
* All entities.
*/
private static final Map<NamespacedKey, TestableBlock> REGISTRY = new ConcurrentHashMap<>();
/**
* All block providers.
*/
private static final Map<String, BlockProvider> PROVIDERS = new ConcurrentHashMap<>();
/**
* The lookup handler.
*/
private static final BlocksLookupHandler BLOCKS_LOOKUP_HANDLER = new BlocksLookupHandler(Blocks::doParse);
/**
* Register a new custom block.
*
* @param key The key of the block.
* @param block The block.
*/
public static void registerCustomBlock(@NotNull final NamespacedKey key,
@NotNull final TestableBlock block) {
REGISTRY.put(key, block);
}
/**
* Register a new block provider.
*
* @param provider The provider.
*/
public static void registerBlockProvider(@NotNull final BlockProvider provider) {
PROVIDERS.put(provider.getNamespace(), provider);
}
/**
* Remove a block.
*
* @param key The key of the block.
*/
public static void removeCustomBlock(@NotNull final NamespacedKey key) {
REGISTRY.remove(key);
}
/**
* This is the backbone of the eco block system.
* <p>
* You can look up a TestableBlock for any material or custom block,
* and it will return it.
* <p>
* If you want to get a Block instance from this, then just call
* {@link TestableBlock#place(Location)}.
*
* @param key The lookup string.
* @return The testable block, or an empty testable block if not found.
*/
@NotNull
public static TestableBlock lookup(@NotNull final String key) {
return BLOCKS_LOOKUP_HANDLER.parseKey(key);
}
@NotNull
private static TestableBlock doParse(@NotNull final String[] args) {
if (args.length == 0) {
return new EmptyTestableBlock();
}
String[] split = args[0].toLowerCase().split(":");
if (split.length == 1) {
if (args[0].startsWith("*")) {
Material type = Material.getMaterial(args[0].substring(1));
return (type == null) ? new EmptyTestableBlock() : new UnrestrictedMaterialTestableBlock(type);
} else {
Material type = Material.getMaterial(args[0].toUpperCase());
return (type == null) ? new EmptyTestableBlock() : new MaterialTestableBlock(type);
}
}
NamespacedKey namespacedKey = NamespacedKeyUtils.create(split[0], split[1]);
TestableBlock block = REGISTRY.get(namespacedKey);
if (block != null) {
return block;
}
BlockProvider provider = PROVIDERS.get(split[0]);
if (provider == null) {
return new EmptyTestableBlock();
}
block = provider.provideForKey(split[1]);
if (block == null) {
return new EmptyTestableBlock();
}
registerCustomBlock(namespacedKey, block);
return block;
}
/**
* Get if block is a custom block.
*
* @param block The block to check.
* @return If is custom.
*/
public static boolean isCustomBlock(@NotNull final Block block) {
for (TestableBlock testable : REGISTRY.values()) {
if (testable.matches(block)) {
return true;
}
}
return false;
}
/**
* Get all registered custom blocks.
*
* @return A set of all blocks.
*/
public static Set<TestableBlock> getCustomBlocks() {
return new HashSet<>(REGISTRY.values());
}
private Blocks() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}

View File

@@ -0,0 +1,48 @@
package com.willfp.eco.core.blocks;
import com.willfp.eco.core.blocks.impl.EmptyTestableBlock;
import com.willfp.eco.core.blocks.impl.GroupedTestableBlocks;
import com.willfp.eco.core.lookup.LookupHandler;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.function.Function;
/**
* Handle block lookup strings.
*/
public class BlocksLookupHandler implements LookupHandler<TestableBlock> {
/**
* The parser.
*/
private final Function<String[], @NotNull TestableBlock> parser;
/**
* Create new lookup handler.
*
* @param parser The parser.
*/
public BlocksLookupHandler(@NotNull final Function<String[], @NotNull TestableBlock> parser) {
this.parser = parser;
}
@Override
public @NotNull TestableBlock parse(@NotNull final String[] args) {
return parser.apply(args);
}
@Override
public boolean validate(@NotNull final TestableBlock object) {
return !(object instanceof EmptyTestableBlock);
}
@Override
public @NotNull TestableBlock getFailsafe() {
return new EmptyTestableBlock();
}
@Override
public @NotNull TestableBlock join(@NotNull final Collection<TestableBlock> options) {
return new GroupedTestableBlocks(options);
}
}

View File

@@ -0,0 +1,84 @@
package com.willfp.eco.core.blocks;
import org.apache.commons.lang.Validate;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* A custom block has 3 components.
*
* <ul>
* <li>The key to identify it</li>
* <li>The test to check if any block is this custom block</li>
* <li>The supplier to spawn the custom {@link Block}</li>
* </ul>
*/
public class CustomBlock implements TestableBlock {
/**
* The key.
*/
private final NamespacedKey key;
/**
* The test for block to pass.
*/
private final Predicate<@NotNull Block> test;
/**
* The provider to spawn the block.
*/
private final Function<Location, Block> provider;
/**
* Create a new custom block.
*
* @param key The block key.
* @param test The test.
* @param provider The provider to spawn the block.
*/
public CustomBlock(@NotNull final NamespacedKey key,
@NotNull final Predicate<@NotNull Block> test,
@NotNull final Function<Location, Block> provider) {
this.key = key;
this.test = test;
this.provider = provider;
}
@Override
public boolean matches(@Nullable final Block other) {
if (other == null) {
return false;
}
return test.test(other);
}
@Override
public @NotNull Block place(@NotNull final Location location) {
Validate.notNull(location.getWorld());
return provider.apply(location);
}
/**
* Register the block.
*/
public void register() {
Blocks.registerCustomBlock(this.getKey(), this);
}
/**
* Get the key.
*
* @return The key.
*/
public NamespacedKey getKey() {
return this.key;
}
}

View File

@@ -0,0 +1,31 @@
package com.willfp.eco.core.blocks;
import com.willfp.eco.core.lookup.Testable;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A block with a test.
*/
public interface TestableBlock extends Testable<Block> {
/**
* If a Block matches the test.
*
* @param other The other block.
* @return If the block matches.
*/
@Override
boolean matches(@Nullable Block other);
/**
* Place the block.
*
* @param location The location.
* @return The block.
*/
@NotNull
Block place(@NotNull Location location);
}

View File

@@ -0,0 +1,29 @@
package com.willfp.eco.core.blocks.impl;
import com.willfp.eco.core.blocks.TestableBlock;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Empty block.
*/
public class EmptyTestableBlock implements TestableBlock {
/**
* Create a new empty testable block.
*/
public EmptyTestableBlock() {
}
@Override
public boolean matches(@Nullable final Block other) {
return false;
}
@Override
public @NotNull Block place(@NotNull final Location location) {
return location.getBlock();
}
}

View File

@@ -0,0 +1,60 @@
package com.willfp.eco.core.blocks.impl;
import com.willfp.eco.core.blocks.TestableBlock;
import com.willfp.eco.util.NumberUtils;
import org.apache.commons.lang.Validate;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
/**
* A group of testable blocks.
*/
public class GroupedTestableBlocks implements TestableBlock {
/**
* The children.
*/
private final Collection<TestableBlock> children;
/**
* Create a new group of testable blocks.
*
* @param children The children.
*/
public GroupedTestableBlocks(@NotNull final Collection<TestableBlock> children) {
Validate.isTrue(!children.isEmpty(), "Group must have at least one child!");
this.children = children;
}
@Override
public boolean matches(@Nullable final Block other) {
for (TestableBlock child : children) {
if (child.matches(other)) {
return true;
}
}
return false;
}
@Override
public @NotNull Block place(@NotNull final Location location) {
return new ArrayList<>(children)
.get(NumberUtils.randInt(0, children.size() - 1))
.place(location);
}
/**
* Get the children.
*
* @return The children.
*/
public Collection<TestableBlock> getChildren() {
return new ArrayList<>(children);
}
}

View File

@@ -0,0 +1,59 @@
package com.willfp.eco.core.blocks.impl;
import com.willfp.eco.core.blocks.Blocks;
import com.willfp.eco.core.blocks.TestableBlock;
import org.apache.commons.lang.Validate;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A testable block for vanilla materials.
*/
public class MaterialTestableBlock implements TestableBlock {
/**
* The block type.
*/
private final Material material;
/**
* Create a new unrestricted material testable block.
*
* @param material The material.
*/
public MaterialTestableBlock(@NotNull final Material material) {
this.material = material;
}
@Override
public boolean matches(@Nullable final Block block) {
boolean simpleMatches = block != null && block.getType() == material;
if (!simpleMatches) {
return false;
}
return !Blocks.isCustomBlock(block);
}
@Override
public @NotNull Block place(@NotNull Location location) {
Validate.notNull(location.getWorld());
Block block = location.getWorld().getBlockAt(location);
block.setType(material);
return block;
}
/**
* Get the material.
*
* @return The material.
*/
public Material getMaterial() {
return this.material;
}
}

View File

@@ -0,0 +1,52 @@
package com.willfp.eco.core.blocks.impl;
import com.willfp.eco.core.blocks.TestableBlock;
import org.apache.commons.lang.Validate;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A testable block for materials regardless of data.
*/
public class UnrestrictedMaterialTestableBlock implements TestableBlock {
/**
* The block type.
*/
private final Material material;
/**
* Create a new unrestricted material testable block.
*
* @param material The material.
*/
public UnrestrictedMaterialTestableBlock(@NotNull final Material material) {
this.material = material;
}
@Override
public boolean matches(@Nullable final Block other) {
return other != null && other.getType() == material;
}
@Override
public @NotNull Block place(@NotNull Location location) {
Validate.notNull(location.getWorld());
Block block = location.getWorld().getBlockAt(location);
block.setType(material);
return block;
}
/**
* Get the material.
*
* @return The material.
*/
public Material getMaterial() {
return this.material;
}
}

View File

@@ -0,0 +1,49 @@
package com.willfp.eco.core.blocks.provider;
import com.willfp.eco.core.blocks.TestableBlock;
import com.willfp.eco.core.registry.Registry;
import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Block providers are call-site registrations for blocks. In other words,
* they only register their blocks when a request is made. This is marginally
* slower, however it is required for certain plugins, and fixes bugs related to
* loading orders.
*
* @see TestableBlock
*/
public abstract class BlockProvider {
/**
* The namespace.
*/
private final String namespace;
/**
* Create a new BlockProvider for a specific namespace.
*
* @param namespace The namespace.
*/
protected BlockProvider(@NotNull final String namespace) {
this.namespace = namespace;
}
/**
* Provide a TestableBlock for a given key.
*
* @param key The block ID.
* @return The TestableBlock, or null if not found.
*/
@Nullable
public abstract TestableBlock provideForKey(@NotNull String key);
/**
* Get the namespace.
*
* @return The namespace.
*/
public String getNamespace() {
return this.namespace;
}
}

View File

@@ -1,44 +0,0 @@
package com.willfp.eco.core.data.handlers;
import com.willfp.eco.core.data.keys.PersistentDataKey;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
/**
* Handles data read/write for a {@link com.willfp.eco.core.data.keys.PersistentDataKeyType} for a specific
* data handler.
*
* @param <T> The type of data.
*/
public abstract class DataTypeSerializer<T> {
/**
* Create a new data type serializer.
*/
protected DataTypeSerializer() {
}
/**
* Read a value.
*
* @param uuid The uuid.
* @param key The key.
* @return The value.
*/
@Nullable
public abstract T readAsync(@NotNull final UUID uuid,
@NotNull final PersistentDataKey<T> key);
/**
* Write a value.
*
* @param uuid The uuid.
* @param key The key.
* @param value The value.
*/
public abstract void writeAsync(@NotNull final UUID uuid,
@NotNull final PersistentDataKey<T> key,
@NotNull final T value);
}

View File

@@ -1,180 +0,0 @@
package com.willfp.eco.core.data.handlers;
import com.willfp.eco.core.data.keys.PersistentDataKey;
import com.willfp.eco.core.registry.Registrable;
import com.willfp.eco.core.tuples.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Handles persistent data.
*/
public abstract class PersistentDataHandler implements Registrable {
/**
* The id.
*/
private final String id;
/**
* The executor.
*/
private final ExecutorService executor = Executors.newCachedThreadPool();
/**
* Create a new persistent data handler.
*
* @param id The id.
*/
protected PersistentDataHandler(@NotNull final String id) {
this.id = id;
}
/**
* Get all UUIDs with saved data.
* <p>
* This is a blocking operation.
*
* @return All saved UUIDs.
*/
public abstract Set<UUID> getSavedUUIDs();
/**
* Save to disk.
* <p>
* If write commits to disk, this method does not need to be overridden.
* <p>
* This method is called asynchronously.
*/
protected void doSave() {
// Save to disk
}
/**
* If the handler should autosave.
*
* @return If the handler should autosave.
*/
public boolean shouldAutosave() {
return true;
}
/**
* Save the data.
*/
public final void save() {
executor.submit(this::doSave);
}
/**
* Read a key from persistent data.
*
* @param uuid The uuid.
* @param key The key.
* @param <T> The type of the key.
* @return The value, or null if not found.
*/
@Nullable
public final <T> T read(@NotNull final UUID uuid,
@NotNull final PersistentDataKey<T> key) {
DataTypeSerializer<T> serializer = key.getType().getSerializer(this);
Future<T> future = executor.submit(() -> serializer.readAsync(uuid, key));
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
return null;
}
}
/**
* Write a key to persistent data.
*
* @param uuid The uuid.
* @param key The key.
* @param value The value.
* @param <T> The type of the key.
*/
public final <T> void write(@NotNull final UUID uuid,
@NotNull final PersistentDataKey<T> key,
@NotNull final T value) {
DataTypeSerializer<T> serializer = key.getType().getSerializer(this);
executor.submit(() -> serializer.writeAsync(uuid, key, value));
}
/**
* Serialize profile.
*
* @param uuid The uuid to serialize.
* @param keys The keys to serialize.
* @return The serialized data.
*/
@NotNull
public final SerializedProfile serializeProfile(@NotNull final UUID uuid,
@NotNull final Set<PersistentDataKey<?>> keys) {
Map<PersistentDataKey<?>, CompletableFuture<Object>> futures = keys.stream()
.collect(Collectors.toMap(
key -> key,
key -> CompletableFuture.supplyAsync(() -> read(uuid, key), executor)
));
Map<PersistentDataKey<?>, Object> data = futures.entrySet().stream()
.map(entry -> new Pair<PersistentDataKey<?>, Object>(entry.getKey(), entry.getValue().join()))
.filter(entry -> entry.getSecond() != null)
.collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
return new SerializedProfile(uuid, data);
}
/**`
* Load profile data.
*
* @param profile The profile.
*/
@SuppressWarnings("unchecked")
public final void loadSerializedProfile(@NotNull final SerializedProfile profile) {
for (Map.Entry<PersistentDataKey<?>, Object> entry : profile.data().entrySet()) {
PersistentDataKey<?> key = entry.getKey();
Object value = entry.getValue();
// This cast is safe because the data is serialized
write(profile.uuid(), (PersistentDataKey<? super Object>) key, value);
}
}
/**
* Save and shutdown the handler.
*
* @throws InterruptedException If the writes could not be awaited.
*/
public final void shutdown() throws InterruptedException {
doSave();
if (executor.isShutdown()) {
return;
}
executor.shutdown();
while (!executor.awaitTermination(2, TimeUnit.MINUTES)) {
// Wait
}
}
@Override
@NotNull
public final String getID() {
return id;
}
}

View File

@@ -1,20 +0,0 @@
package com.willfp.eco.core.data.handlers;
import com.willfp.eco.core.data.keys.PersistentDataKey;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
/**
* Serialized profile.
*
* @param uuid The uuid.
* @param data The data.
*/
public record SerializedProfile(
@NotNull UUID uuid,
@NotNull Map<PersistentDataKey<?>, Object> data
) {
}

View File

@@ -34,19 +34,6 @@ public final class PersistentDataKey<T> {
*/
private final boolean isLocal;
/**
* Create a new Persistent Data Key.
*
* @param key The key.
* @param type The data type.
* @param defaultValue The default value.
*/
public PersistentDataKey(@NotNull final NamespacedKey key,
@NotNull final PersistentDataKeyType<T> type,
@NotNull final T defaultValue) {
this(key, type, defaultValue, false);
}
/**
* Create a new Persistent Data Key.
*
@@ -67,6 +54,24 @@ public final class PersistentDataKey<T> {
Eco.get().registerPersistentKey(this);
}
/**
* Create a new Persistent Data Key.
*
* @param key The key.
* @param type The data type.
* @param defaultValue The default value.
*/
public PersistentDataKey(@NotNull final NamespacedKey key,
@NotNull final PersistentDataKeyType<T> type,
@NotNull final T defaultValue) {
this.key = key;
this.defaultValue = defaultValue;
this.type = type;
this.isLocal = false;
Eco.get().registerPersistentKey(this);
}
@Override
public String toString() {
return "PersistentDataKey{"

View File

@@ -1,17 +1,12 @@
package com.willfp.eco.core.data.keys;
import com.willfp.eco.core.config.interfaces.Config;
import com.willfp.eco.core.data.handlers.DataTypeSerializer;
import com.willfp.eco.core.data.handlers.PersistentDataHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
@@ -65,58 +60,24 @@ public final class PersistentDataKeyType<T> {
*/
private final String name;
/**
* The serializers for this key type.
*/
private final Map<PersistentDataHandler, DataTypeSerializer<T>> serializers = new HashMap<>();
/**
* Create new PersistentDataKeyType.
*
* @param name The name.
*/
private PersistentDataKeyType(@NotNull final String name) {
VALUES.add(this);
this.name = name;
}
/**
* Get the name of the key type.
*
* @return The name.
*/
@NotNull
public String name() {
return name;
}
/**
* Register a serializer for this key type.
* Create new PersistentDataKeyType.
*
* @param handler The handler.
* @param serializer The serializer.
* @param name The name.
*/
public void registerSerializer(@NotNull final PersistentDataHandler handler,
@NotNull final DataTypeSerializer<T> serializer) {
this.serializers.put(handler, serializer);
}
private PersistentDataKeyType(@NotNull final String name) {
VALUES.add(this);
/**
* Get the serializer for a handler.
*
* @param handler The handler.
* @return The serializer.
*/
@NotNull
public DataTypeSerializer<T> getSerializer(@NotNull final PersistentDataHandler handler) {
DataTypeSerializer<T> serializer = this.serializers.get(handler);
if (serializer == null) {
throw new NoSuchElementException("No serializer for handler: " + handler);
}
return serializer;
this.name = name;
}
@Override

View File

@@ -6,7 +6,6 @@ import com.willfp.eco.core.entities.TestableEntity;
import com.willfp.eco.core.entities.ai.EntityGoal;
import com.willfp.eco.core.items.Items;
import com.willfp.eco.core.serialization.KeyedDeserializer;
import com.willfp.eco.util.SoundUtils;
import org.bukkit.NamespacedKey;
import org.bukkit.Sound;
import org.bukkit.entity.LivingEntity;
@@ -51,15 +50,9 @@ public record EntityGoalUseItem(
TestableEntity filter = Entities.lookup(config.getString("condition"));
Sound sound = SoundUtils.getSound(config.getString("sound"));
if (sound == null) {
return null;
}
return new EntityGoalUseItem(
Items.lookup(config.getString("item")).getItem(),
sound,
Sound.valueOf(config.getString("sound").toUpperCase()),
filter::matches
);
}

View File

@@ -11,22 +11,19 @@ import org.bukkit.entity.Raider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Allows an entity to attack the closest target within a given subset of specific target types.
*
* @param targets The type of entities to attack.
* @param target The type of entities to attack.
* @param checkVisibility If visibility should be checked.
* @param checkCanNavigate If navigation should be checked.
* @param reciprocalChance 1 in reciprocalChance chance of not activating on any tick.
* @param targetFilter The filter for targets to match.
*/
public record TargetGoalNearestAttackable(
@NotNull Set<TestableEntity> targets,
@NotNull TestableEntity target,
boolean checkVisibility,
boolean checkCanNavigate,
int reciprocalChance,
@@ -35,16 +32,16 @@ public record TargetGoalNearestAttackable(
/**
* Create a new target goal.
*
* @param targets The type of entities to attack.
* @param target The type of entities to attack.
* @param checkVisibility If visibility should be checked.
* @param checkCanNavigate If navigation should be checked.
* @param reciprocalChance 1 in reciprocalChance chance of not activating on any tick.
*/
public TargetGoalNearestAttackable(@NotNull final Set<TestableEntity> targets,
public TargetGoalNearestAttackable(@NotNull final TestableEntity target,
final boolean checkVisibility,
final boolean checkCanNavigate,
final int reciprocalChance) {
this(targets, checkVisibility, checkCanNavigate, reciprocalChance, it -> true);
this(target, checkVisibility, checkCanNavigate, reciprocalChance, it -> true);
}
/**
@@ -68,15 +65,11 @@ public record TargetGoalNearestAttackable(
return null;
}
Set<TestableEntity> targets = config.getStrings("target").stream()
.map(Entities::lookup)
.collect(Collectors.toSet());
if (config.has("targetFilter")) {
TestableEntity filter = Entities.lookup(config.getString("targetFilter"));
return new TargetGoalNearestAttackable(
targets,
Entities.lookup(config.getString("target")),
config.getBool("checkVisibility"),
config.getBool("checkCanNavigate"),
config.getInt("reciprocalChance"),
@@ -84,7 +77,7 @@ public record TargetGoalNearestAttackable(
);
} else {
return new TargetGoalNearestAttackable(
targets,
Entities.lookup(config.getString("target")),
config.getBool("checkVisibility"),
config.getBool("checkCanNavigate"),
config.getInt("reciprocalChance")

View File

@@ -0,0 +1,24 @@
package com.willfp.eco.core.integrations.customblocks;
import com.willfp.eco.core.integrations.Integration;
/**
* Wrapper class for custom block integrations.
*/
public interface CustomBlocksIntegration extends Integration {
/**
* Register all the custom block for a specific plugin into eco.
*
* @see com.willfp.eco.core.blocks.Blocks
*/
default void registerAllBlocks() {
// Override when needed.
}
/**
* Register {@link com.willfp.eco.core.blocks.provider.BlockProvider}s.
*/
default void registerProvider() {
// Override when needed.
}
}

View File

@@ -0,0 +1,45 @@
package com.willfp.eco.core.integrations.customblocks;
import com.willfp.eco.core.integrations.IntegrationRegistry;
import org.jetbrains.annotations.NotNull;
/**
* Class to handle custom block integrations.
*/
public final class CustomBlocksManager {
/**
* A set of all registered integrations.
*/
private static final IntegrationRegistry<CustomBlocksIntegration> REGISTRY = new IntegrationRegistry<>();
/**
* Register a new integration.
*
* @param integration The integration to register.
*/
public static void register(@NotNull final CustomBlocksIntegration integration) {
REGISTRY.register(integration);
}
/**
* Register all the custom block for a specific plugin into eco.
*
* @see com.willfp.eco.core.blocks.Blocks
*/
public static void registerAllBlocks() {
REGISTRY.forEachSafely(CustomBlocksIntegration::registerAllBlocks);
}
/**
* Register all the custom blocks for a specific plugin into eco.
*
* @see com.willfp.eco.core.blocks.Blocks
*/
public static void registerProviders() {
REGISTRY.forEachSafely(CustomBlocksIntegration::registerProvider);
}
private CustomBlocksManager() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}

View File

@@ -2,7 +2,6 @@ package com.willfp.eco.core.proxy;
import com.willfp.eco.core.version.Version;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
@@ -29,23 +28,13 @@ public final class ProxyConstants {
"v1_20_R1",
"v1_20_R2",
"v1_20_R3",
"v1_21",
"v1_21_3",
"v1_21_4"
"v1_21"
);
private ProxyConstants() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
private static String convertVersion(@NotNull final String version) {
return switch (version) {
case "v1_21_1" -> "v1_21";
case "v1_21_2" -> "v1_21_3";
default -> version;
};
}
static {
String currentMinecraftVersion = Bukkit.getServer().getBukkitVersion().split("-")[0];
String nmsVersion;
@@ -56,6 +45,6 @@ public final class ProxyConstants {
nmsVersion = "v" + currentMinecraftVersion.replace(".", "_");
}
NMS_VERSION = convertVersion(nmsVersion);
NMS_VERSION = nmsVersion;
}
}

View File

@@ -2,7 +2,6 @@ package com.willfp.eco.core.sound;
import com.willfp.eco.core.config.interfaces.Config;
import com.willfp.eco.core.serialization.ConfigDeserializer;
import com.willfp.eco.util.SoundUtils;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.World;
@@ -83,20 +82,20 @@ public record PlayableSound(@NotNull Sound sound,
return null;
}
Sound sound = SoundUtils.getSound(config.getString("sound"));
try {
Sound sound = Sound.valueOf(config.getString("sound").toUpperCase());
if (sound == null) {
double pitch = Objects.requireNonNullElse(config.getDoubleOrNull("pitch"), 1.0);
double volume = Objects.requireNonNullElse(config.getDoubleOrNull("volume"), 1.0);
return new PlayableSound(
sound,
pitch,
volume
);
} catch (IllegalArgumentException e) {
return null;
}
double pitch = Objects.requireNonNullElse(config.getDoubleOrNull("pitch"), 1.0);
double volume = Objects.requireNonNullElse(config.getDoubleOrNull("volume"), 1.0);
return new PlayableSound(
sound,
pitch,
volume
);
}
}
}

View File

@@ -1,50 +0,0 @@
package com.willfp.eco.util;
import com.willfp.eco.core.Prerequisite;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.Sound;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
/**
* Utilities / API methods for sounds.
*/
public final class SoundUtils {
/**
* Get a sound in a version-compatible way.
*
* @param name The name of the sound, case-insensitive.
* @return The sound, or null if not found.
*/
@Nullable
public static Sound getSound(@NotNull final String name) {
if (!Prerequisite.HAS_1_21_3.isMet()) {
try {
return Sound.valueOf(name.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
// First try from registry (preferred)
Sound fromRegistry = Registry.SOUNDS.get(NamespacedKey.minecraft(name.toLowerCase()));
if (fromRegistry != null) {
return fromRegistry;
}
// Next try using reflection (for legacy enum names)
try {
Field field = Sound.class.getDeclaredField(name.toUpperCase());
return (Sound) field.get(null);
} catch (ReflectiveOperationException e) {
return null;
}
}
private SoundUtils() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}

View File

@@ -3,10 +3,7 @@
package com.willfp.eco.core.entities
import com.willfp.eco.core.entities.ai.EntityController
import com.willfp.eco.core.items.Items
import com.willfp.eco.core.items.TestableItem
import org.bukkit.entity.Mob
import org.bukkit.inventory.ItemStack
/** @see EntityController.getFor */
val <T : Mob> T.controller: EntityController<T>

View File

@@ -1,21 +1,19 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "com.willfp"
version = rootProject.version
dependencies {
compileOnly(project(":eco-core:core-backend"))
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
compileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT")
}
tasks {
compileJava {
options.release.set(21)
options.release = 21
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
kotlinOptions {
jvmTarget = "21"
}
}
}

View File

@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "com.willfp"
version = rootProject.version
@@ -9,7 +7,7 @@ dependencies {
implementation("org.objenesis:objenesis:3.2")
compileOnly("io.papermc.paper:paper-api:1.20.2-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("me.clip:placeholderapi:2.11.4")
compileOnly("net.kyori:adventure-text-minimessage:4.10.0")
compileOnly("net.kyori:adventure-platform-bukkit:4.1.0")
compileOnly("org.yaml:snakeyaml:1.33")
@@ -18,12 +16,12 @@ dependencies {
tasks {
compileJava {
options.release.set(17)
options.release = 17
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
kotlinOptions {
jvmTarget = "17"
}
}
}

View File

@@ -80,11 +80,9 @@ class EcoEventManager(private val plugin: EcoPlugin) : EventManager {
}
override fun registerPacketListener(listener: PacketListener) {
listeners[listener.priority].add(
RegisteredPacketListener(
plugin,
listener
)
listeners[listener.priority] += RegisteredPacketListener(
plugin,
listener
)
}
}

View File

@@ -23,7 +23,7 @@ class EcoSlotBuilder(private val provider: SlotProvider) : SlotBuilder {
private var notCaptiveFor: (Player) -> Boolean = { _ -> false}
override fun onClick(type: ClickType, action: SlotHandler): SlotBuilder {
handlers[type].add(action)
handlers[type] += action
return this
}

View File

@@ -14,16 +14,12 @@ object ArgParserEnchantment : LookupArgParser {
val enchants = mutableMapOf<Enchantment, Int>()
for (arg in args) {
try {
val argSplit = arg.split(":")
val argSplit = arg.split(":")
val enchant = Enchantment.getByKey(NamespacedKey.minecraft(argSplit[0].lowercase())) ?: continue
val level = argSplit.getOrNull(1)?.toIntOrNull() ?: enchant.maxLevel
val enchant = Enchantment.getByKey(NamespacedKey.minecraft(argSplit[0].lowercase())) ?: continue
val level = argSplit.getOrNull(1)?.toIntOrNull() ?: enchant.maxLevel
enchants[enchant] = level
} catch (e: IllegalArgumentException) {
continue
}
enchants[enchant] = level
}
if (enchants.isEmpty()) {

View File

@@ -12,7 +12,7 @@ object ParticleFactoryRGB : ParticleFactory {
if (Prerequisite.HAS_1_20_5.isMet) {
Particle.valueOf("DUST")
} else {
Particle.valueOf("REDSTONE")
Particle.valueOf("REDSTONE_DUST")
}
}.getOrNull()

View File

@@ -50,7 +50,8 @@ class EcoProxyFactory(
)
} else {
ProxyError(
"Could not initialize proxy. Are you running a supported server version?",
"Could not initialize proxy. If you're seeing this error message"
+ ", something has gone badly wrong. This almost definitely isn't user error, blame the developer.",
e
)
}

View File

@@ -1,5 +1,5 @@
plugins {
id("io.papermc.paperweight.userdev") version "2.0.0-beta.14" apply false
id("io.papermc.paperweight.userdev") version "1.7.1" apply false
}

View File

@@ -48,7 +48,7 @@ var SkullMeta.texture: String?
* at java.lang.String.checkBoundsBeginEnd(String.java:4604) ~[?:?]
* at java.lang.String.substring(String.java:2707) ~[?:?]
* at java.lang.String.substring(String.java:2680) ~[?:?]
* at com.willfp.eco.internal.spigot.proxy.v1_19_R1.common.SkullKt.setTexture(ModernSkull.kt:36)
* at com.willfp.eco.internal.spigot.proxy.v1_19_R1.common.SkullKt.setTexture(Skull.kt:36)
*
if (base64.length < 20) {
return

View File

@@ -54,6 +54,8 @@ class EcoEntityController<T : Mob>(
priority, goal.getGoalFactory()?.create(goal, nms) ?: return this
)
nms.targetSelector
return this
}

View File

@@ -10,7 +10,7 @@ import net.minecraft.world.entity.monster.RangedAttackMob
object RangedBowAttackGoalFactory : EntityGoalFactory<EntityGoalRangedBowAttack> {
override fun create(apiGoal: EntityGoalRangedBowAttack, entity: PathfinderMob): Goal? {
if (entity !is Monster) return null
(if (entity !is Monster) return null)
if (entity !is RangedAttackMob) return null
return RangedBowAttackGoal(

View File

@@ -11,7 +11,7 @@ import net.minecraft.world.entity.monster.RangedAttackMob
object RangedCrossbowAttackGoalFactory : EntityGoalFactory<EntityGoalRangedCrossbowAttack> {
override fun create(apiGoal: EntityGoalRangedCrossbowAttack, entity: PathfinderMob): Goal? {
if (entity !is Monster) return null
(if (entity !is Monster) return null)
if (entity !is RangedAttackMob) return null
if (entity !is CrossbowAttackMob) return null

View File

@@ -1,7 +1,6 @@
package com.willfp.eco.internal.spigot.proxy.common.ai.target
import com.willfp.eco.core.entities.ai.target.TargetGoalNearestAttackable
import com.willfp.eco.core.lookup.matches
import com.willfp.eco.internal.spigot.proxy.common.ai.TargetGoalFactory
import com.willfp.eco.internal.spigot.proxy.common.toBukkitEntity
import net.minecraft.world.entity.LivingEntity
@@ -18,9 +17,7 @@ object NearestAttackableGoalFactory : TargetGoalFactory<TargetGoalNearestAttacka
apiGoal.checkVisibility,
apiGoal.checkCanNavigate,
) {
val bukkit = it.toBukkitEntity()
apiGoal.targetFilter.test(bukkit) && apiGoal.targets.any { t -> t.matches(bukkit) }
apiGoal.targetFilter.test(it.toBukkitEntity()) && apiGoal.target.matches(it.toBukkitEntity())
}
}

View File

@@ -3,19 +3,12 @@ package com.willfp.eco.internal.spigot.proxy.common.packet.display
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.core.packet.PacketEvent
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.common.toResourceLocation
import com.willfp.eco.util.namespacedKeyOf
import net.minecraft.network.protocol.game.ClientboundPlaceGhostRecipePacket
import net.minecraft.resources.ResourceLocation
class PacketAutoRecipe(
private val plugin: EcoPlugin
) : PacketListener {
private val fKey = ClientboundPlaceGhostRecipePacket::class.java
.declaredFields
.first { it.type == ResourceLocation::class.java }
.apply { isAccessible = true }
override fun onSend(event: PacketEvent) {
val packet = event.packet.handle as? ClientboundPlaceGhostRecipePacket ?: return
@@ -31,7 +24,9 @@ class PacketAutoRecipe(
return
}
val fKey = packet.javaClass.getDeclaredField("b")
fKey.isAccessible = true
val key = fKey[packet] as ResourceLocation
fKey[packet] = namespacedKeyOf(key.namespace, key.path + "_displayed").toResourceLocation()
fKey[packet] = ResourceLocation(key.namespace, key.path + "_displayed")
}
}

View File

@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("io.papermc.paperweight.userdev")
}
@@ -9,17 +7,17 @@ version = rootProject.version
dependencies {
compileOnly(project(":eco-core:core-nms:common"))
paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT")
paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT")
}
tasks {
compileJava {
options.release.set(21)
options.release = 21
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
kotlinOptions {
jvmTarget = "21"
}
}
}

View File

@@ -1,55 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.common.modern
import com.mojang.authlib.GameProfile
import com.mojang.authlib.properties.Property
import net.minecraft.world.item.component.ResolvableProfile
import org.bukkit.inventory.meta.SkullMeta
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.UUID
private lateinit var setProfile: Method
private lateinit var profile: Field
private lateinit var value: Field
var SkullMeta.texture: String?
get() {
if (!::value.isInitialized) {
// Doing it this way because Property was changed to be a record and this is
// a quick hack to get around that
value = Property::class.java.getDeclaredField("value")
value.isAccessible = true
}
if (!::profile.isInitialized) {
// Assumes instance of CraftMetaSkull; package-private class so can't do manual type check
profile = this.javaClass.getDeclaredField("profile")
profile.isAccessible = true
}
val profile = profile[this] as ResolvableProfile? ?: return null
val properties = profile.properties ?: return null
val props = properties["textures"] ?: return null
val prop = props.toMutableList().firstOrNull() ?: return null
return value[prop] as String?
}
set(base64) {
if (!::setProfile.isInitialized) {
// Same here; that's why I can't delegate to a lazy initializer
setProfile = this.javaClass.getDeclaredMethod("setProfile", ResolvableProfile::class.java)
setProfile.isAccessible = true
}
if (base64 == null || base64.length < 20) {
setProfile.invoke(this, null)
} else {
val uuid = UUID(
base64.substring(base64.length - 20).hashCode().toLong(),
base64.substring(base64.length - 10).hashCode().toLong()
)
val profile = GameProfile(uuid, "eco")
profile.properties.put("textures", Property("textures", base64))
val resolvable = ResolvableProfile(profile)
setProfile.invoke(this, resolvable)
}
}

View File

@@ -17,7 +17,6 @@ import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.Style
import net.kyori.adventure.text.format.TextColor
import net.kyori.adventure.text.format.TextDecoration
import net.minecraft.core.Holder
import net.minecraft.core.component.DataComponentType
import net.minecraft.core.component.DataComponents
import net.minecraft.core.registries.Registries
@@ -26,7 +25,6 @@ import net.minecraft.util.Unit
import net.minecraft.world.item.component.CustomData
import net.minecraft.world.item.component.CustomModelData
import net.minecraft.world.item.component.ItemLore
import net.minecraft.world.item.enchantment.ItemEnchantments
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftRegistry
import org.bukkit.craftbukkit.CraftServer
@@ -36,7 +34,6 @@ import org.bukkit.inventory.ItemFlag
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataContainer
import kotlin.math.max
import kotlin.math.min
private val unstyledComponent = Component.empty().style {
it.color(null).decoration(TextDecoration.ITALIC, false)
@@ -46,17 +43,17 @@ private fun Component.unstyled(): Component {
return unstyledComponent.append(this)
}
open class NewEcoFastItemStack(
private val bukkit: ItemStack,
class NewEcoFastItemStack(
private val bukkit: ItemStack
) : ImplementedFIS {
// Cast is there because, try as I might, I can't get IntellIJ to recognise half the classes in the dev bundle
@Suppress("USELESS_CAST")
protected val handle = bukkit.asNMSStack() as net.minecraft.world.item.ItemStack
private val handle = bukkit.asNMSStack() as net.minecraft.world.item.ItemStack
private val pdc = (handle.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: CompoundTag()).makePdc()
override fun getEnchants(checkStored: Boolean): Map<Enchantment, Int> {
val enchantments = handle.get(DataComponents.ENCHANTMENTS) ?: ItemEnchantments.EMPTY
val enchantments = handle.get(DataComponents.ENCHANTMENTS) ?: return emptyMap()
val map = mutableMapOf<Enchantment, Int>()
@@ -83,17 +80,22 @@ open class NewEcoFastItemStack(
enchantment: Enchantment,
checkStored: Boolean
): Int {
val minecraft = CraftRegistry.bukkitToMinecraftHolder(
enchantment,
Registries.ENCHANTMENT
)
val minecraft =
CraftRegistry.bukkitToMinecraft<Enchantment, net.minecraft.world.item.enchantment.Enchantment>(
enchantment
)
val server = Bukkit.getServer() as CraftServer
val access = server.server.registryAccess()
val holder = access.registryOrThrow(Registries.ENCHANTMENT).wrapAsHolder(minecraft)
val enchantments = handle.get(DataComponents.ENCHANTMENTS) ?: return 0
var level = enchantments.getLevel(minecraft)
var level = enchantments.getLevel(holder)
if (checkStored) {
val storedEnchantments = handle.get(DataComponents.STORED_ENCHANTMENTS) ?: return 0
level = max(level, storedEnchantments.getLevel(minecraft))
level = max(level, storedEnchantments.getLevel(holder))
}
return level
@@ -366,23 +368,19 @@ open class NewEcoFastItemStack(
override fun getType(): org.bukkit.Material = handle.getItem().toMaterial()
/*
Custom model data doesn't work based on an integer since 1.21.3, so these methods do nothing
*/
override fun getCustomModelData(): Int? =
null
handle.get(DataComponents.CUSTOM_MODEL_DATA)?.value
override fun setCustomModelData(data: Int?) {
if (data == null) {
handle.remove(DataComponents.CUSTOM_MODEL_DATA)
} else {
handle.set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(data))
}
apply()
}
// END
override fun equals(other: Any?): Boolean {
if (other !is NewEcoFastItemStack) {
return false

View File

@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("io.papermc.paperweight.userdev")
}
@@ -10,7 +8,7 @@ version = rootProject.version
dependencies {
implementation(project(":eco-core:core-nms:modern"))
implementation(project(":eco-core:core-nms:common"))
paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT")
paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT")
implementation("net.kyori:adventure-text-minimessage:4.11.0") {
version {
@@ -41,12 +39,12 @@ tasks {
}
compileJava {
options.release.set(21)
options.release = 21
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
kotlinOptions {
jvmTarget = "21"
}
}
}

View File

@@ -3,34 +3,10 @@ package com.willfp.eco.internal.spigot.proxy.v1_21
import com.willfp.eco.core.fast.FastItemStack
import com.willfp.eco.internal.spigot.proxy.FastItemStackFactoryProxy
import com.willfp.eco.internal.spigot.proxy.common.modern.NewEcoFastItemStack
import net.minecraft.core.Registry
import net.minecraft.core.component.DataComponents
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceKey
import net.minecraft.world.item.component.CustomModelData
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.inventory.ItemStack
class FastItemStackFactory : FastItemStackFactoryProxy {
override fun create(itemStack: ItemStack): FastItemStack {
return LegacyNewEcoFastItemStack(itemStack)
}
private class LegacyNewEcoFastItemStack(itemStack: ItemStack) :
NewEcoFastItemStack(itemStack) {
override fun getCustomModelData(): Int? =
handle.get(DataComponents.CUSTOM_MODEL_DATA)?.value
override fun setCustomModelData(data: Int?) {
if (data == null) {
handle.remove(DataComponents.CUSTOM_MODEL_DATA)
} else {
handle.set(DataComponents.CUSTOM_MODEL_DATA, CustomModelData(data))
}
apply()
}
return NewEcoFastItemStack(itemStack)
}
}

View File

@@ -1,7 +1,7 @@
package com.willfp.eco.internal.spigot.proxy.v1_21
import com.willfp.eco.internal.spigot.proxy.SkullProxy
import com.willfp.eco.internal.spigot.proxy.common.modern.texture
import com.willfp.eco.internal.spigot.proxy.common.texture
import org.bukkit.inventory.meta.SkullMeta
class Skull : SkullProxy {

View File

@@ -1,52 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("io.papermc.paperweight.userdev")
}
group = "com.willfp"
version = rootProject.version
dependencies {
implementation(project(":eco-core:core-nms:modern"))
implementation(project(":eco-core:core-nms:common"))
paperweight.paperDevBundle("1.21.3-R0.1-SNAPSHOT")
implementation("net.kyori:adventure-text-minimessage:4.11.0") {
version {
strictly("4.11.0")
}
exclude(group = "net.kyori", module = "adventure-api")
}
}
tasks {
build {
dependsOn(reobfJar)
}
reobfJar {
mustRunAfter(shadowJar)
}
shadowJar {
relocate(
"com.willfp.eco.internal.spigot.proxy.common",
"com.willfp.eco.internal.spigot.proxy.v1_21_3.common"
)
relocate(
"net.kyori.adventure.text.minimessage",
"com.willfp.eco.internal.spigot.proxy.v1_21_3.minimessage"
)
}
compileJava {
options.release.set(21)
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
}

View File

@@ -1,35 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.command.PluginCommandBase
import com.willfp.eco.internal.spigot.proxy.BukkitCommandsProxy
import org.bukkit.Bukkit
import org.bukkit.command.Command
import org.bukkit.command.SimpleCommandMap
import org.bukkit.craftbukkit.CraftServer
import java.lang.reflect.Field
class BukkitCommands : BukkitCommandsProxy {
private val knownCommandsField: Field by lazy {
SimpleCommandMap::class.java.getDeclaredField("knownCommands")
.apply {
isAccessible = true
}
}
@Suppress("UNCHECKED_CAST")
private val knownCommands: MutableMap<String, Command>
get() = knownCommandsField.get(getCommandMap()) as MutableMap<String, Command>
override fun getCommandMap(): SimpleCommandMap {
return (Bukkit.getServer() as CraftServer).commandMap
}
override fun syncCommands() {
(Bukkit.getServer() as CraftServer).syncCommands()
}
override fun unregisterCommand(command: PluginCommandBase) {
knownCommands.remove(command.name)
knownCommands.remove("${command.plugin.name.lowercase()}:${command.name}")
}
}

View File

@@ -1,171 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.internal.spigot.proxy.CommonsInitializerProxy
import com.willfp.eco.internal.spigot.proxy.common.CommonsProvider
import com.willfp.eco.internal.spigot.proxy.common.packet.PacketInjectorListener
import com.willfp.eco.internal.spigot.proxy.common.toResourceLocation
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer
import net.minecraft.core.component.DataComponents
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.Tag
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.item.Item
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.craftbukkit.entity.CraftEntity
import org.bukkit.craftbukkit.entity.CraftMob
import org.bukkit.craftbukkit.entity.CraftPlayer
import org.bukkit.craftbukkit.inventory.CraftItemStack
import org.bukkit.craftbukkit.inventory.CraftMetaArmor
import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry
import org.bukkit.craftbukkit.util.CraftMagicNumbers
import org.bukkit.craftbukkit.util.CraftNamespacedKey
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Mob
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataContainer
import java.lang.reflect.Field
class CommonsInitializer : CommonsInitializerProxy {
override fun init(plugin: EcoPlugin) {
CommonsProvider.setIfNeeded(CommonsProviderImpl)
plugin.onEnable {
plugin.eventManager.registerListener(PacketInjectorListener)
}
}
object CommonsProviderImpl : CommonsProvider {
private val cisHandle: Field = CraftItemStack::class.java.getDeclaredField("handle").apply {
isAccessible = true
}
private val pdcRegsitry = CraftMetaArmor::class.java
.superclass // Access CraftMetaItem
.getDeclaredField("DATA_TYPE_REGISTRY")
.apply { isAccessible = true }
.get(null) as CraftPersistentDataTypeRegistry
override val nbtTagString = CraftMagicNumbers.NBT.TAG_STRING
override fun toPathfinderMob(mob: Mob): PathfinderMob? {
val craft = mob as? CraftMob ?: return null
return craft.handle as? PathfinderMob
}
override fun toResourceLocation(namespacedKey: NamespacedKey): ResourceLocation =
CraftNamespacedKey.toMinecraft(namespacedKey)
override fun asNMSStack(itemStack: ItemStack): net.minecraft.world.item.ItemStack {
return if (itemStack !is CraftItemStack) {
CraftItemStack.asNMSCopy(itemStack)
} else {
cisHandle[itemStack] as net.minecraft.world.item.ItemStack? ?: CraftItemStack.asNMSCopy(itemStack)
}
}
override fun asBukkitStack(itemStack: net.minecraft.world.item.ItemStack): ItemStack {
return CraftItemStack.asCraftMirror(itemStack)
}
override fun mergeIfNeeded(itemStack: ItemStack, nmsStack: net.minecraft.world.item.ItemStack) {
if (itemStack !is CraftItemStack) {
itemStack.itemMeta = CraftItemStack.asCraftMirror(nmsStack).itemMeta
}
}
override fun toBukkitEntity(entity: net.minecraft.world.entity.LivingEntity): LivingEntity? =
CraftEntity.getEntity(Bukkit.getServer() as CraftServer, entity) as? LivingEntity
override fun makePdc(tag: CompoundTag, base: Boolean): PersistentDataContainer {
fun emptyPdc(): CraftPersistentDataContainer = CraftPersistentDataContainer(pdcRegsitry)
fun CompoundTag?.toPdc(): PersistentDataContainer {
val pdc = emptyPdc()
this ?: return pdc
val keys = this.allKeys
for (key in keys) {
pdc.put(key, this[key])
}
return pdc
}
return if (base) {
tag.toPdc()
} else {
if (tag.contains("PublicBukkitValues")) {
tag.getCompound("PublicBukkitValues").toPdc()
} else {
emptyPdc()
}
}
}
override fun setPdc(
tag: CompoundTag,
pdc: PersistentDataContainer?,
item: net.minecraft.world.item.ItemStack?
) {
fun CraftPersistentDataContainer.toTag(): CompoundTag {
val compound = CompoundTag()
val rawPublicMap: Map<String, Tag> = this.raw
for ((key, value) in rawPublicMap) {
compound.put(key, value)
}
return compound
}
val container = when (pdc) {
is CraftPersistentDataContainer? -> pdc
else -> null
}
if (item != null) {
if (container != null && !container.isEmpty) {
for (key in tag.allKeys.toSet()) {
tag.remove(key)
}
tag.merge(container.toTag())
} else {
item.remove(DataComponents.CUSTOM_DATA)
}
} else {
if (container != null && !container.isEmpty) {
tag.put("PublicBukkitValues", container.toTag())
} else {
tag.remove("PublicBukkitValues")
}
}
}
override fun materialToItem(material: Material): Item =
BuiltInRegistries.ITEM.getOptional(material.key.toResourceLocation())
.orElseThrow { IllegalArgumentException("Material is not item!") }
override fun itemToMaterial(item: Item) =
Material.getMaterial(BuiltInRegistries.ITEM.getKey(item).path.uppercase())
?: throw IllegalArgumentException("Invalid material!")
override fun toNMS(player: Player): ServerPlayer {
return (player as CraftPlayer).handle
}
override fun toNMS(component: Component): net.minecraft.network.chat.Component {
val json = JSONComponentSerializer.json().serialize(component)
val holderLookupProvider = (Bukkit.getServer() as CraftServer).server.registryAccess()
return net.minecraft.network.chat.Component.Serializer.fromJson(json, holderLookupProvider)!!
}
}
}

View File

@@ -1,57 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.packet.Packet
import com.willfp.eco.core.packet.sendPacket
import com.willfp.eco.internal.spigot.proxy.DisplayNameProxy
import com.willfp.eco.internal.spigot.proxy.common.toNMS
import net.kyori.adventure.text.Component
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
import net.minecraft.network.syncher.EntityDataAccessor
import net.minecraft.network.syncher.SynchedEntityData
import net.minecraft.world.entity.Entity
import org.bukkit.craftbukkit.entity.CraftLivingEntity
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Player
import java.util.Optional
@Suppress("UNCHECKED_CAST")
class DisplayName : DisplayNameProxy {
private val displayNameAccessor = Entity::class.java
.declaredFields
.filter { it.type == EntityDataAccessor::class.java }
.toList()[2]
.apply { isAccessible = true }
.get(null) as EntityDataAccessor<Optional<net.minecraft.network.chat.Component>>
private val customNameVisibleAccessor = Entity::class.java
.declaredFields
.filter { it.type == EntityDataAccessor::class.java }
.toList()[3]
.apply { isAccessible = true }
.get(null) as EntityDataAccessor<Boolean>
override fun setClientsideDisplayName(
entity: LivingEntity,
player: Player,
displayName: Component,
visible: Boolean
) {
if (entity !is CraftLivingEntity) {
return
}
val nmsComponent = displayName.toNMS()
val nmsEntity = entity.handle
val packet = ClientboundSetEntityDataPacket(
nmsEntity.id,
listOf(
SynchedEntityData.DataValue.create(displayNameAccessor, Optional.of(nmsComponent)),
SynchedEntityData.DataValue.create(customNameVisibleAccessor, visible)
)
)
player.sendPacket(Packet(packet))
}
}

View File

@@ -1,15 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.internal.entities.EcoDummyEntity
import com.willfp.eco.internal.spigot.proxy.DummyEntityFactoryProxy
import org.bukkit.Location
import org.bukkit.craftbukkit.CraftWorld
import org.bukkit.entity.Entity
import org.bukkit.entity.Zombie
class DummyEntityFactory : DummyEntityFactoryProxy {
override fun createDummyEntity(location: Location): Entity {
val world = location.world as CraftWorld
return EcoDummyEntity(world.createEntity(location, Zombie::class.java))
}
}

View File

@@ -1,12 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.entities.ai.EntityController
import com.willfp.eco.internal.spigot.proxy.EntityControllerFactoryProxy
import com.willfp.eco.internal.spigot.proxy.v1_21_3.entity.EcoEntityController
import org.bukkit.entity.Mob
class EntityControllerFactory : EntityControllerFactoryProxy {
override fun <T : Mob> createEntityController(entity: T): EntityController<T> {
return EcoEntityController(entity)
}
}

View File

@@ -1,91 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.data.ExtendedPersistentDataContainer
import com.willfp.eco.internal.spigot.proxy.ExtendedPersistentDataContainerFactoryProxy
import net.minecraft.nbt.Tag
import org.bukkit.Material
import org.bukkit.craftbukkit.inventory.CraftItemStack
import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataContainer
import org.bukkit.persistence.PersistentDataType
import java.lang.reflect.Field
class ExtendedPersistentDataContainerFactory : ExtendedPersistentDataContainerFactoryProxy {
private val registry: CraftPersistentDataTypeRegistry
init {
/*
Can't grab actual instance since it's in CraftMetaItem (which is package-private)
And getting it would mean more janky reflection
*/
val item = CraftItemStack.asCraftCopy(ItemStack(Material.STONE))
val pdc = item.itemMeta!!.persistentDataContainer
// Cross-version compatibility:
val registryField: Field = try {
CraftPersistentDataContainer::class.java.getDeclaredField("registry")
} catch (e: NoSuchFieldException) {
CraftPersistentDataContainer::class.java.superclass.getDeclaredField("registry")
}
this.registry = registryField
.apply { isAccessible = true }.get(pdc) as CraftPersistentDataTypeRegistry
}
override fun adapt(pdc: PersistentDataContainer): ExtendedPersistentDataContainer {
return when (pdc) {
is CraftPersistentDataContainer -> EcoPersistentDataContainer(pdc)
else -> throw IllegalArgumentException("Custom PDC instance ${pdc::class.java.name} is not supported!")
}
}
override fun newPdc(): PersistentDataContainer {
return CraftPersistentDataContainer(registry)
}
inner class EcoPersistentDataContainer(
private val handle: CraftPersistentDataContainer
) : ExtendedPersistentDataContainer {
@Suppress("UNCHECKED_CAST")
private val customDataTags: MutableMap<String, Tag> =
CraftPersistentDataContainer::class.java.getDeclaredField("customDataTags")
.apply { isAccessible = true }.get(handle) as MutableMap<String, Tag>
override fun <T : Any, Z : Any> set(key: String, dataType: PersistentDataType<T, Z>, value: Z) {
customDataTags[key] =
registry.wrap(dataType, dataType.toPrimitive(value, handle.adapterContext))
}
override fun <T : Any, Z : Any> has(key: String, dataType: PersistentDataType<T, Z>): Boolean {
val value = customDataTags[key] ?: return false
return registry.isInstanceOf(dataType, value)
}
override fun <T : Any, Z : Any> get(key: String, dataType: PersistentDataType<T, Z>): Z? {
val value = customDataTags[key] ?: return null
return dataType.fromPrimitive(registry.extract<T, Tag>(dataType, value), handle.adapterContext)
}
override fun <T : Any, Z : Any> getOrDefault(
key: String,
dataType: PersistentDataType<T, Z>,
defaultValue: Z
): Z {
return get(key, dataType) ?: defaultValue
}
override fun remove(key: String) {
customDataTags.remove(key)
}
override fun getAllKeys(): MutableSet<String> {
return customDataTags.keys
}
override fun getBase(): PersistentDataContainer {
return handle
}
}
}

View File

@@ -1,16 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.fast.FastItemStack
import com.willfp.eco.internal.spigot.proxy.FastItemStackFactoryProxy
import com.willfp.eco.internal.spigot.proxy.common.modern.NewEcoFastItemStack
import net.minecraft.core.Registry
import net.minecraft.resources.ResourceKey
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.inventory.ItemStack
class FastItemStackFactory : FastItemStackFactoryProxy {
override fun create(itemStack: ItemStack): FastItemStack {
return NewEcoFastItemStack(itemStack)
}
}

View File

@@ -1,33 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.display.Display
import com.willfp.eco.internal.spigot.proxy.MiniMessageTranslatorProxy
import com.willfp.eco.util.toLegacy
import net.kyori.adventure.text.minimessage.MiniMessage
class MiniMessageTranslator : MiniMessageTranslatorProxy {
override fun format(message: String): String {
var mut = message
val startsWithPrefix = mut.startsWith(Display.PREFIX)
if (startsWithPrefix) {
mut = mut.substring(2)
}
mut = mut.replace('§', '&')
val miniMessage = runCatching {
MiniMessage.miniMessage().deserialize(
mut
).toLegacy()
}.getOrNull() ?: mut
mut = if (startsWithPrefix) {
Display.PREFIX + miniMessage
} else {
miniMessage
}
return mut
}
}

View File

@@ -1,47 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.PacketHandlerProxy
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketAutoRecipe
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketHeldItemSlot
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketSetSlot
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketWindowItems
import com.willfp.eco.internal.spigot.proxy.common.packet.display.frame.clearFrames
import com.willfp.eco.internal.spigot.proxy.v1_21_3.packet.NewItemsPacketOpenWindowMerchant
import com.willfp.eco.internal.spigot.proxy.v1_21_3.packet.NewItemsPacketSetCreativeSlot
import net.minecraft.network.protocol.Packet
import org.bukkit.craftbukkit.entity.CraftPlayer
import org.bukkit.entity.Player
class PacketHandler : PacketHandlerProxy {
override fun sendPacket(player: Player, packet: com.willfp.eco.core.packet.Packet) {
if (player !is CraftPlayer) {
return
}
val handle = packet.handle
if (handle !is Packet<*>) {
return
}
player.handle.connection.send(handle)
}
override fun clearDisplayFrames() {
clearFrames()
}
override fun getPacketListeners(plugin: EcoPlugin): List<PacketListener> {
// No PacketAutoRecipe for 1.21.3+ because recipes have been changed internally
return listOf(
PacketHeldItemSlot,
NewItemsPacketOpenWindowMerchant,
NewItemsPacketSetCreativeSlot,
PacketSetSlot,
PacketWindowItems(plugin)
)
}
}

View File

@@ -1,80 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.mojang.serialization.Dynamic
import com.willfp.eco.core.items.TestableItem
import com.willfp.eco.core.recipe.parts.EmptyTestableItem
import com.willfp.eco.internal.spigot.proxy.SNBTConverterProxy
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.SnbtPrinterTagVisitor
import net.minecraft.nbt.TagParser
import net.minecraft.server.MinecraftServer
import net.minecraft.util.datafix.fixes.References
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.craftbukkit.inventory.CraftItemStack
import org.bukkit.craftbukkit.util.CraftMagicNumbers
import org.bukkit.inventory.ItemStack
private val registryAccess = (Bukkit.getServer() as CraftServer).server.registryAccess()
class SNBTConverter : SNBTConverterProxy {
private fun parseItemSNBT(snbt: String): CompoundTag? {
val nbt = runCatching { TagParser.parseTag(snbt) }.getOrNull() ?: return null
val dataVersion = if (nbt.contains("DataVersion")) {
nbt.getInt("DataVersion")
} else null
// If the data version is the same as the server's data version, we don't need to fix it
if (dataVersion == CraftMagicNumbers.INSTANCE.dataVersion) {
return nbt
}
return MinecraftServer.getServer().fixerUpper.update(
References.ITEM_STACK,
Dynamic(NbtOps.INSTANCE, nbt),
dataVersion ?: 3700, // 3700 is the 1.20.4 data version
CraftMagicNumbers.INSTANCE.dataVersion
).value as CompoundTag
}
override fun fromSNBT(snbt: String): ItemStack? {
val tag = parseItemSNBT(snbt) ?: return null
val nms = net.minecraft.world.item.ItemStack.parse(registryAccess, tag).orElse(null) ?: return null
return CraftItemStack.asBukkitCopy(nms)
}
override fun toSNBT(itemStack: ItemStack): String {
val nms = CraftItemStack.asNMSCopy(itemStack)
val tag = nms.save(registryAccess) as CompoundTag
tag.putInt("DataVersion", CraftMagicNumbers.INSTANCE.dataVersion)
return SnbtPrinterTagVisitor().visit(tag)
}
override fun makeSNBTTestable(snbt: String): TestableItem {
val tag = parseItemSNBT(snbt) ?: return EmptyTestableItem()
val nms = net.minecraft.world.item.ItemStack.parse(registryAccess, tag).orElse(null)
?: return EmptyTestableItem()
tag.remove("Count")
return SNBTTestableItem(CraftItemStack.asBukkitCopy(nms), tag)
}
class SNBTTestableItem(
private val item: ItemStack,
private val tag: CompoundTag
) : TestableItem {
override fun matches(itemStack: ItemStack?): Boolean {
if (itemStack == null) {
return false
}
val nms = CraftItemStack.asNMSCopy(itemStack)
val nmsTag = nms.save(registryAccess) as CompoundTag
nmsTag.remove("Count")
return tag.copy().merge(nmsTag) == nmsTag && itemStack.type == item.type
}
override fun getItem(): ItemStack = item
}
}

View File

@@ -1,18 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.internal.spigot.proxy.SkullProxy
import com.willfp.eco.internal.spigot.proxy.common.modern.texture
import org.bukkit.inventory.meta.SkullMeta
class Skull : SkullProxy {
override fun setSkullTexture(
meta: SkullMeta,
base64: String
) {
meta.texture = base64
}
override fun getSkullTexture(
meta: SkullMeta
): String? = meta.texture
}

View File

@@ -1,11 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3
import com.willfp.eco.internal.spigot.proxy.TPSProxy
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
class TPS : TPSProxy {
override fun getTPS(): Double {
return (Bukkit.getServer() as CraftServer).handle.server.tps1.average
}
}

View File

@@ -1,95 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3.entity
import com.willfp.eco.core.entities.ai.CustomGoal
import com.willfp.eco.core.entities.ai.EntityController
import com.willfp.eco.core.entities.ai.EntityGoal
import com.willfp.eco.core.entities.ai.TargetGoal
import com.willfp.eco.internal.spigot.proxy.common.ai.CustomGoalFactory
import com.willfp.eco.internal.spigot.proxy.common.ai.getGoalFactory
import com.willfp.eco.internal.spigot.proxy.common.toPathfinderMob
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.goal.Goal
import org.bukkit.entity.Mob
class EcoEntityController<T : Mob>(
private val handle: T
) : EntityController<T> {
override fun addEntityGoal(priority: Int, goal: EntityGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
nms.goalSelector.addGoal(
priority,
goal.getGoalFactory()?.create(goal, nms) ?: return this
)
return this
}
override fun removeEntityGoal(goal: EntityGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
val predicate: (Goal) -> Boolean = if (goal is CustomGoal<*>) {
{ CustomGoalFactory.isGoalOfType(it, goal) }
} else {
{ goal.getGoalFactory()?.isGoalOfType(it) == true }
}
for (wrapped in nms.goalSelector.availableGoals.toSet()) {
if (predicate(wrapped.goal)) {
nms.goalSelector.removeGoal(wrapped.goal)
}
}
return this
}
override fun clearEntityGoals(): EntityController<T> {
val nms = getNms() ?: return this
nms.goalSelector.availableGoals.clear()
return this
}
override fun addTargetGoal(priority: Int, goal: TargetGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
nms.targetSelector.addGoal(
priority, goal.getGoalFactory()?.create(goal, nms) ?: return this
)
nms.targetSelector
return this
}
override fun removeTargetGoal(goal: TargetGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
val predicate: (Goal) -> Boolean = if (goal is CustomGoal<*>) {
{ CustomGoalFactory.isGoalOfType(it, goal) }
} else {
{ goal.getGoalFactory()?.isGoalOfType(it) == true }
}
for (wrapped in nms.targetSelector.availableGoals.toSet()) {
if (predicate(wrapped.goal)) {
nms.targetSelector.removeGoal(wrapped.goal)
}
}
return this
}
override fun clearTargetGoals(): EntityController<T> {
val nms = getNms() ?: return this
nms.targetSelector.availableGoals.clear()
return this
}
private fun getNms(): PathfinderMob? {
return handle.toPathfinderMob()
}
override fun getEntity(): T {
return handle
}
}

View File

@@ -1,35 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3.packet
import com.willfp.eco.core.display.Display
import com.willfp.eco.core.packet.PacketEvent
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.common.asBukkitStack
import net.minecraft.network.protocol.game.ClientboundMerchantOffersPacket
import net.minecraft.world.item.trading.MerchantOffers
object NewItemsPacketOpenWindowMerchant : PacketListener {
private val field = ClientboundMerchantOffersPacket::class.java
.declaredFields
.first { it.type == MerchantOffers::class.java }
.apply { isAccessible = true }
override fun onSend(event: PacketEvent) {
val packet = event.packet.handle as? ClientboundMerchantOffersPacket ?: return
val offers = MerchantOffers()
for (offer in packet.offers) {
val new = offer.copy()
Display.display(new.baseCostA.itemStack.asBukkitStack(), event.player)
if (new.costB.isPresent) {
Display.display(new.costB.get().itemStack.asBukkitStack(), event.player)
}
Display.display(new.result.asBukkitStack(), event.player)
offers += new
}
field.set(packet, offers)
}
}

View File

@@ -1,19 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_3.packet
import com.willfp.eco.core.display.Display
import com.willfp.eco.core.packet.PacketEvent
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.common.asBukkitStack
import com.willfp.eco.internal.spigot.proxy.common.packet.display.frame.DisplayFrame
import com.willfp.eco.internal.spigot.proxy.common.packet.display.frame.lastDisplayFrame
import net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket
object NewItemsPacketSetCreativeSlot : PacketListener {
override fun onReceive(event: PacketEvent) {
val packet = event.packet.handle as? ServerboundSetCreativeModeSlotPacket ?: return
Display.revert(packet.itemStack.asBukkitStack())
event.player.lastDisplayFrame = DisplayFrame.EMPTY
}
}

View File

@@ -1,52 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("io.papermc.paperweight.userdev")
}
group = "com.willfp"
version = rootProject.version
dependencies {
implementation(project(":eco-core:core-nms:modern"))
implementation(project(":eco-core:core-nms:common"))
paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT")
implementation("net.kyori:adventure-text-minimessage:4.11.0") {
version {
strictly("4.11.0")
}
exclude(group = "net.kyori", module = "adventure-api")
}
}
tasks {
build {
dependsOn(reobfJar)
}
reobfJar {
mustRunAfter(shadowJar)
}
shadowJar {
relocate(
"com.willfp.eco.internal.spigot.proxy.common",
"com.willfp.eco.internal.spigot.proxy.v1_21_4.common"
)
relocate(
"net.kyori.adventure.text.minimessage",
"com.willfp.eco.internal.spigot.proxy.v1_21_4.minimessage"
)
}
compileJava {
options.release.set(21)
}
compileKotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
}

View File

@@ -1,35 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.command.PluginCommandBase
import com.willfp.eco.internal.spigot.proxy.BukkitCommandsProxy
import org.bukkit.Bukkit
import org.bukkit.command.Command
import org.bukkit.command.SimpleCommandMap
import org.bukkit.craftbukkit.CraftServer
import java.lang.reflect.Field
class BukkitCommands : BukkitCommandsProxy {
private val knownCommandsField: Field by lazy {
SimpleCommandMap::class.java.getDeclaredField("knownCommands")
.apply {
isAccessible = true
}
}
@Suppress("UNCHECKED_CAST")
private val knownCommands: MutableMap<String, Command>
get() = knownCommandsField.get(getCommandMap()) as MutableMap<String, Command>
override fun getCommandMap(): SimpleCommandMap {
return (Bukkit.getServer() as CraftServer).commandMap
}
override fun syncCommands() {
(Bukkit.getServer() as CraftServer).syncCommands()
}
override fun unregisterCommand(command: PluginCommandBase) {
knownCommands.remove(command.name)
knownCommands.remove("${command.plugin.name.lowercase()}:${command.name}")
}
}

View File

@@ -1,171 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.internal.spigot.proxy.CommonsInitializerProxy
import com.willfp.eco.internal.spigot.proxy.common.CommonsProvider
import com.willfp.eco.internal.spigot.proxy.common.packet.PacketInjectorListener
import com.willfp.eco.internal.spigot.proxy.common.toResourceLocation
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer
import net.minecraft.core.component.DataComponents
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.Tag
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.item.Item
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.craftbukkit.entity.CraftEntity
import org.bukkit.craftbukkit.entity.CraftMob
import org.bukkit.craftbukkit.entity.CraftPlayer
import org.bukkit.craftbukkit.inventory.CraftItemStack
import org.bukkit.craftbukkit.inventory.CraftMetaArmor
import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry
import org.bukkit.craftbukkit.util.CraftMagicNumbers
import org.bukkit.craftbukkit.util.CraftNamespacedKey
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Mob
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataContainer
import java.lang.reflect.Field
class CommonsInitializer : CommonsInitializerProxy {
override fun init(plugin: EcoPlugin) {
CommonsProvider.setIfNeeded(CommonsProviderImpl)
plugin.onEnable {
plugin.eventManager.registerListener(PacketInjectorListener)
}
}
object CommonsProviderImpl : CommonsProvider {
private val cisHandle: Field = CraftItemStack::class.java.getDeclaredField("handle").apply {
isAccessible = true
}
private val pdcRegsitry = CraftMetaArmor::class.java
.superclass // Access CraftMetaItem
.getDeclaredField("DATA_TYPE_REGISTRY")
.apply { isAccessible = true }
.get(null) as CraftPersistentDataTypeRegistry
override val nbtTagString = CraftMagicNumbers.NBT.TAG_STRING
override fun toPathfinderMob(mob: Mob): PathfinderMob? {
val craft = mob as? CraftMob ?: return null
return craft.handle as? PathfinderMob
}
override fun toResourceLocation(namespacedKey: NamespacedKey): ResourceLocation =
CraftNamespacedKey.toMinecraft(namespacedKey)
override fun asNMSStack(itemStack: ItemStack): net.minecraft.world.item.ItemStack {
return if (itemStack !is CraftItemStack) {
CraftItemStack.asNMSCopy(itemStack)
} else {
cisHandle[itemStack] as net.minecraft.world.item.ItemStack? ?: CraftItemStack.asNMSCopy(itemStack)
}
}
override fun asBukkitStack(itemStack: net.minecraft.world.item.ItemStack): ItemStack {
return CraftItemStack.asCraftMirror(itemStack)
}
override fun mergeIfNeeded(itemStack: ItemStack, nmsStack: net.minecraft.world.item.ItemStack) {
if (itemStack !is CraftItemStack) {
itemStack.itemMeta = CraftItemStack.asCraftMirror(nmsStack).itemMeta
}
}
override fun toBukkitEntity(entity: net.minecraft.world.entity.LivingEntity): LivingEntity? =
CraftEntity.getEntity(Bukkit.getServer() as CraftServer, entity) as? LivingEntity
override fun makePdc(tag: CompoundTag, base: Boolean): PersistentDataContainer {
fun emptyPdc(): CraftPersistentDataContainer = CraftPersistentDataContainer(pdcRegsitry)
fun CompoundTag?.toPdc(): PersistentDataContainer {
val pdc = emptyPdc()
this ?: return pdc
val keys = this.allKeys
for (key in keys) {
pdc.put(key, this[key])
}
return pdc
}
return if (base) {
tag.toPdc()
} else {
if (tag.contains("PublicBukkitValues")) {
tag.getCompound("PublicBukkitValues").toPdc()
} else {
emptyPdc()
}
}
}
override fun setPdc(
tag: CompoundTag,
pdc: PersistentDataContainer?,
item: net.minecraft.world.item.ItemStack?
) {
fun CraftPersistentDataContainer.toTag(): CompoundTag {
val compound = CompoundTag()
val rawPublicMap: Map<String, Tag> = this.raw
for ((key, value) in rawPublicMap) {
compound.put(key, value)
}
return compound
}
val container = when (pdc) {
is CraftPersistentDataContainer? -> pdc
else -> null
}
if (item != null) {
if (container != null && !container.isEmpty) {
for (key in tag.allKeys.toSet()) {
tag.remove(key)
}
tag.merge(container.toTag())
} else {
item.remove(DataComponents.CUSTOM_DATA)
}
} else {
if (container != null && !container.isEmpty) {
tag.put("PublicBukkitValues", container.toTag())
} else {
tag.remove("PublicBukkitValues")
}
}
}
override fun materialToItem(material: Material): Item =
BuiltInRegistries.ITEM.getOptional(material.key.toResourceLocation())
.orElseThrow { IllegalArgumentException("Material is not item!") }
override fun itemToMaterial(item: Item) =
Material.getMaterial(BuiltInRegistries.ITEM.getKey(item).path.uppercase())
?: throw IllegalArgumentException("Invalid material!")
override fun toNMS(player: Player): ServerPlayer {
return (player as CraftPlayer).handle
}
override fun toNMS(component: Component): net.minecraft.network.chat.Component {
val json = JSONComponentSerializer.json().serialize(component)
val holderLookupProvider = (Bukkit.getServer() as CraftServer).server.registryAccess()
return net.minecraft.network.chat.Component.Serializer.fromJson(json, holderLookupProvider)!!
}
}
}

View File

@@ -1,57 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.packet.Packet
import com.willfp.eco.core.packet.sendPacket
import com.willfp.eco.internal.spigot.proxy.DisplayNameProxy
import com.willfp.eco.internal.spigot.proxy.common.toNMS
import net.kyori.adventure.text.Component
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
import net.minecraft.network.syncher.EntityDataAccessor
import net.minecraft.network.syncher.SynchedEntityData
import net.minecraft.world.entity.Entity
import org.bukkit.craftbukkit.entity.CraftLivingEntity
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Player
import java.util.Optional
@Suppress("UNCHECKED_CAST")
class DisplayName : DisplayNameProxy {
private val displayNameAccessor = Entity::class.java
.declaredFields
.filter { it.type == EntityDataAccessor::class.java }
.toList()[2]
.apply { isAccessible = true }
.get(null) as EntityDataAccessor<Optional<net.minecraft.network.chat.Component>>
private val customNameVisibleAccessor = Entity::class.java
.declaredFields
.filter { it.type == EntityDataAccessor::class.java }
.toList()[3]
.apply { isAccessible = true }
.get(null) as EntityDataAccessor<Boolean>
override fun setClientsideDisplayName(
entity: LivingEntity,
player: Player,
displayName: Component,
visible: Boolean
) {
if (entity !is CraftLivingEntity) {
return
}
val nmsComponent = displayName.toNMS()
val nmsEntity = entity.handle
val packet = ClientboundSetEntityDataPacket(
nmsEntity.id,
listOf(
SynchedEntityData.DataValue.create(displayNameAccessor, Optional.of(nmsComponent)),
SynchedEntityData.DataValue.create(customNameVisibleAccessor, visible)
)
)
player.sendPacket(Packet(packet))
}
}

View File

@@ -1,15 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.internal.entities.EcoDummyEntity
import com.willfp.eco.internal.spigot.proxy.DummyEntityFactoryProxy
import org.bukkit.Location
import org.bukkit.craftbukkit.CraftWorld
import org.bukkit.entity.Entity
import org.bukkit.entity.Zombie
class DummyEntityFactory : DummyEntityFactoryProxy {
override fun createDummyEntity(location: Location): Entity {
val world = location.world as CraftWorld
return EcoDummyEntity(world.createEntity(location, Zombie::class.java))
}
}

View File

@@ -1,12 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.entities.ai.EntityController
import com.willfp.eco.internal.spigot.proxy.EntityControllerFactoryProxy
import com.willfp.eco.internal.spigot.proxy.v1_21_4.entity.EcoEntityController
import org.bukkit.entity.Mob
class EntityControllerFactory : EntityControllerFactoryProxy {
override fun <T : Mob> createEntityController(entity: T): EntityController<T> {
return EcoEntityController(entity)
}
}

View File

@@ -1,91 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.data.ExtendedPersistentDataContainer
import com.willfp.eco.internal.spigot.proxy.ExtendedPersistentDataContainerFactoryProxy
import net.minecraft.nbt.Tag
import org.bukkit.Material
import org.bukkit.craftbukkit.inventory.CraftItemStack
import org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataContainer
import org.bukkit.persistence.PersistentDataType
import java.lang.reflect.Field
class ExtendedPersistentDataContainerFactory : ExtendedPersistentDataContainerFactoryProxy {
private val registry: CraftPersistentDataTypeRegistry
init {
/*
Can't grab actual instance since it's in CraftMetaItem (which is package-private)
And getting it would mean more janky reflection
*/
val item = CraftItemStack.asCraftCopy(ItemStack(Material.STONE))
val pdc = item.itemMeta!!.persistentDataContainer
// Cross-version compatibility:
val registryField: Field = try {
CraftPersistentDataContainer::class.java.getDeclaredField("registry")
} catch (e: NoSuchFieldException) {
CraftPersistentDataContainer::class.java.superclass.getDeclaredField("registry")
}
this.registry = registryField
.apply { isAccessible = true }.get(pdc) as CraftPersistentDataTypeRegistry
}
override fun adapt(pdc: PersistentDataContainer): ExtendedPersistentDataContainer {
return when (pdc) {
is CraftPersistentDataContainer -> EcoPersistentDataContainer(pdc)
else -> throw IllegalArgumentException("Custom PDC instance ${pdc::class.java.name} is not supported!")
}
}
override fun newPdc(): PersistentDataContainer {
return CraftPersistentDataContainer(registry)
}
inner class EcoPersistentDataContainer(
private val handle: CraftPersistentDataContainer
) : ExtendedPersistentDataContainer {
@Suppress("UNCHECKED_CAST")
private val customDataTags: MutableMap<String, Tag> =
CraftPersistentDataContainer::class.java.getDeclaredField("customDataTags")
.apply { isAccessible = true }.get(handle) as MutableMap<String, Tag>
override fun <T : Any, Z : Any> set(key: String, dataType: PersistentDataType<T, Z>, value: Z) {
customDataTags[key] =
registry.wrap(dataType, dataType.toPrimitive(value, handle.adapterContext))
}
override fun <T : Any, Z : Any> has(key: String, dataType: PersistentDataType<T, Z>): Boolean {
val value = customDataTags[key] ?: return false
return registry.isInstanceOf(dataType, value)
}
override fun <T : Any, Z : Any> get(key: String, dataType: PersistentDataType<T, Z>): Z? {
val value = customDataTags[key] ?: return null
return dataType.fromPrimitive(registry.extract<T, Tag>(dataType, value), handle.adapterContext)
}
override fun <T : Any, Z : Any> getOrDefault(
key: String,
dataType: PersistentDataType<T, Z>,
defaultValue: Z
): Z {
return get(key, dataType) ?: defaultValue
}
override fun remove(key: String) {
customDataTags.remove(key)
}
override fun getAllKeys(): MutableSet<String> {
return customDataTags.keys
}
override fun getBase(): PersistentDataContainer {
return handle
}
}
}

View File

@@ -1,17 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.fast.FastItemStack
import com.willfp.eco.internal.spigot.proxy.FastItemStackFactoryProxy
import com.willfp.eco.internal.spigot.proxy.common.modern.NewEcoFastItemStack
import net.minecraft.core.Registry
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceKey
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.inventory.ItemStack
class FastItemStackFactory : FastItemStackFactoryProxy {
override fun create(itemStack: ItemStack): FastItemStack {
return NewEcoFastItemStack(itemStack)
}
}

View File

@@ -1,33 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.display.Display
import com.willfp.eco.internal.spigot.proxy.MiniMessageTranslatorProxy
import com.willfp.eco.util.toLegacy
import net.kyori.adventure.text.minimessage.MiniMessage
class MiniMessageTranslator : MiniMessageTranslatorProxy {
override fun format(message: String): String {
var mut = message
val startsWithPrefix = mut.startsWith(Display.PREFIX)
if (startsWithPrefix) {
mut = mut.substring(2)
}
mut = mut.replace('§', '&')
val miniMessage = runCatching {
MiniMessage.miniMessage().deserialize(
mut
).toLegacy()
}.getOrNull() ?: mut
mut = if (startsWithPrefix) {
Display.PREFIX + miniMessage
} else {
miniMessage
}
return mut
}
}

View File

@@ -1,47 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.PacketHandlerProxy
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketAutoRecipe
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketHeldItemSlot
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketSetSlot
import com.willfp.eco.internal.spigot.proxy.common.packet.display.PacketWindowItems
import com.willfp.eco.internal.spigot.proxy.common.packet.display.frame.clearFrames
import com.willfp.eco.internal.spigot.proxy.v1_21_4.packet.NewItemsPacketOpenWindowMerchant
import com.willfp.eco.internal.spigot.proxy.v1_21_4.packet.NewItemsPacketSetCreativeSlot
import net.minecraft.network.protocol.Packet
import org.bukkit.craftbukkit.entity.CraftPlayer
import org.bukkit.entity.Player
class PacketHandler : PacketHandlerProxy {
override fun sendPacket(player: Player, packet: com.willfp.eco.core.packet.Packet) {
if (player !is CraftPlayer) {
return
}
val handle = packet.handle
if (handle !is Packet<*>) {
return
}
player.handle.connection.send(handle)
}
override fun clearDisplayFrames() {
clearFrames()
}
override fun getPacketListeners(plugin: EcoPlugin): List<PacketListener> {
// No PacketAutoRecipe for 1.21.3+ because recipes have been changed internally
return listOf(
PacketHeldItemSlot,
NewItemsPacketOpenWindowMerchant,
NewItemsPacketSetCreativeSlot,
PacketSetSlot,
PacketWindowItems(plugin)
)
}
}

View File

@@ -1,80 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.mojang.serialization.Dynamic
import com.willfp.eco.core.items.TestableItem
import com.willfp.eco.core.recipe.parts.EmptyTestableItem
import com.willfp.eco.internal.spigot.proxy.SNBTConverterProxy
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.SnbtPrinterTagVisitor
import net.minecraft.nbt.TagParser
import net.minecraft.server.MinecraftServer
import net.minecraft.util.datafix.fixes.References
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
import org.bukkit.craftbukkit.inventory.CraftItemStack
import org.bukkit.craftbukkit.util.CraftMagicNumbers
import org.bukkit.inventory.ItemStack
private val registryAccess = (Bukkit.getServer() as CraftServer).server.registryAccess()
class SNBTConverter : SNBTConverterProxy {
private fun parseItemSNBT(snbt: String): CompoundTag? {
val nbt = runCatching { TagParser.parseTag(snbt) }.getOrNull() ?: return null
val dataVersion = if (nbt.contains("DataVersion")) {
nbt.getInt("DataVersion")
} else null
// If the data version is the same as the server's data version, we don't need to fix it
if (dataVersion == CraftMagicNumbers.INSTANCE.dataVersion) {
return nbt
}
return MinecraftServer.getServer().fixerUpper.update(
References.ITEM_STACK,
Dynamic(NbtOps.INSTANCE, nbt),
dataVersion ?: 3700, // 3700 is the 1.20.4 data version
CraftMagicNumbers.INSTANCE.dataVersion
).value as CompoundTag
}
override fun fromSNBT(snbt: String): ItemStack? {
val tag = parseItemSNBT(snbt) ?: return null
val nms = net.minecraft.world.item.ItemStack.parse(registryAccess, tag).orElse(null) ?: return null
return CraftItemStack.asBukkitCopy(nms)
}
override fun toSNBT(itemStack: ItemStack): String {
val nms = CraftItemStack.asNMSCopy(itemStack)
val tag = nms.save(registryAccess) as CompoundTag
tag.putInt("DataVersion", CraftMagicNumbers.INSTANCE.dataVersion)
return SnbtPrinterTagVisitor().visit(tag)
}
override fun makeSNBTTestable(snbt: String): TestableItem {
val tag = parseItemSNBT(snbt) ?: return EmptyTestableItem()
val nms = net.minecraft.world.item.ItemStack.parse(registryAccess, tag).orElse(null)
?: return EmptyTestableItem()
tag.remove("Count")
return SNBTTestableItem(CraftItemStack.asBukkitCopy(nms), tag)
}
class SNBTTestableItem(
private val item: ItemStack,
private val tag: CompoundTag
) : TestableItem {
override fun matches(itemStack: ItemStack?): Boolean {
if (itemStack == null) {
return false
}
val nms = CraftItemStack.asNMSCopy(itemStack)
val nmsTag = nms.save(registryAccess) as CompoundTag
nmsTag.remove("Count")
return tag.copy().merge(nmsTag) == nmsTag && itemStack.type == item.type
}
override fun getItem(): ItemStack = item
}
}

View File

@@ -1,18 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.internal.spigot.proxy.SkullProxy
import com.willfp.eco.internal.spigot.proxy.common.modern.texture
import org.bukkit.inventory.meta.SkullMeta
class Skull : SkullProxy {
override fun setSkullTexture(
meta: SkullMeta,
base64: String
) {
meta.texture = base64
}
override fun getSkullTexture(
meta: SkullMeta
): String? = meta.texture
}

View File

@@ -1,11 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4
import com.willfp.eco.internal.spigot.proxy.TPSProxy
import org.bukkit.Bukkit
import org.bukkit.craftbukkit.CraftServer
class TPS : TPSProxy {
override fun getTPS(): Double {
return (Bukkit.getServer() as CraftServer).handle.server.tps1.average
}
}

View File

@@ -1,95 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4.entity
import com.willfp.eco.core.entities.ai.CustomGoal
import com.willfp.eco.core.entities.ai.EntityController
import com.willfp.eco.core.entities.ai.EntityGoal
import com.willfp.eco.core.entities.ai.TargetGoal
import com.willfp.eco.internal.spigot.proxy.common.ai.CustomGoalFactory
import com.willfp.eco.internal.spigot.proxy.common.ai.getGoalFactory
import com.willfp.eco.internal.spigot.proxy.common.toPathfinderMob
import net.minecraft.world.entity.PathfinderMob
import net.minecraft.world.entity.ai.goal.Goal
import org.bukkit.entity.Mob
class EcoEntityController<T : Mob>(
private val handle: T
) : EntityController<T> {
override fun addEntityGoal(priority: Int, goal: EntityGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
nms.goalSelector.addGoal(
priority,
goal.getGoalFactory()?.create(goal, nms) ?: return this
)
return this
}
override fun removeEntityGoal(goal: EntityGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
val predicate: (Goal) -> Boolean = if (goal is CustomGoal<*>) {
{ CustomGoalFactory.isGoalOfType(it, goal) }
} else {
{ goal.getGoalFactory()?.isGoalOfType(it) == true }
}
for (wrapped in nms.goalSelector.availableGoals.toSet()) {
if (predicate(wrapped.goal)) {
nms.goalSelector.removeGoal(wrapped.goal)
}
}
return this
}
override fun clearEntityGoals(): EntityController<T> {
val nms = getNms() ?: return this
nms.goalSelector.availableGoals.clear()
return this
}
override fun addTargetGoal(priority: Int, goal: TargetGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
nms.targetSelector.addGoal(
priority, goal.getGoalFactory()?.create(goal, nms) ?: return this
)
nms.targetSelector
return this
}
override fun removeTargetGoal(goal: TargetGoal<in T>): EntityController<T> {
val nms = getNms() ?: return this
val predicate: (Goal) -> Boolean = if (goal is CustomGoal<*>) {
{ CustomGoalFactory.isGoalOfType(it, goal) }
} else {
{ goal.getGoalFactory()?.isGoalOfType(it) == true }
}
for (wrapped in nms.targetSelector.availableGoals.toSet()) {
if (predicate(wrapped.goal)) {
nms.targetSelector.removeGoal(wrapped.goal)
}
}
return this
}
override fun clearTargetGoals(): EntityController<T> {
val nms = getNms() ?: return this
nms.targetSelector.availableGoals.clear()
return this
}
private fun getNms(): PathfinderMob? {
return handle.toPathfinderMob()
}
override fun getEntity(): T {
return handle
}
}

View File

@@ -1,35 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4.packet
import com.willfp.eco.core.display.Display
import com.willfp.eco.core.packet.PacketEvent
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.common.asBukkitStack
import net.minecraft.network.protocol.game.ClientboundMerchantOffersPacket
import net.minecraft.world.item.trading.MerchantOffers
object NewItemsPacketOpenWindowMerchant : PacketListener {
private val field = ClientboundMerchantOffersPacket::class.java
.declaredFields
.first { it.type == MerchantOffers::class.java }
.apply { isAccessible = true }
override fun onSend(event: PacketEvent) {
val packet = event.packet.handle as? ClientboundMerchantOffersPacket ?: return
val offers = MerchantOffers()
for (offer in packet.offers) {
val new = offer.copy()
Display.display(new.baseCostA.itemStack.asBukkitStack(), event.player)
if (new.costB.isPresent) {
Display.display(new.costB.get().itemStack.asBukkitStack(), event.player)
}
Display.display(new.result.asBukkitStack(), event.player)
offers += new
}
field.set(packet, offers)
}
}

View File

@@ -1,19 +0,0 @@
package com.willfp.eco.internal.spigot.proxy.v1_21_4.packet
import com.willfp.eco.core.display.Display
import com.willfp.eco.core.packet.PacketEvent
import com.willfp.eco.core.packet.PacketListener
import com.willfp.eco.internal.spigot.proxy.common.asBukkitStack
import com.willfp.eco.internal.spigot.proxy.common.packet.display.frame.DisplayFrame
import com.willfp.eco.internal.spigot.proxy.common.packet.display.frame.lastDisplayFrame
import net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket
object NewItemsPacketSetCreativeSlot : PacketListener {
override fun onReceive(event: PacketEvent) {
val packet = event.packet.handle as? ServerboundSetCreativeModeSlotPacket ?: return
Display.revert(packet.itemStack.asBukkitStack())
event.player.lastDisplayFrame = DisplayFrame.EMPTY
}
}

View File

@@ -1,3 +1,5 @@
import org.gradle.internal.impldep.org.junit.experimental.categories.Categories.CategoryFilter.exclude
group = "com.willfp"
version = rootProject.version
@@ -7,13 +9,16 @@ dependencies {
// Libraries
implementation("com.github.WillFP:Crunch:1.1.3")
implementation("com.mysql:mysql-connector-j:8.4.0")
implementation("org.jetbrains.exposed:exposed-core:0.53.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.53.0")
implementation("com.zaxxer:HikariCP:5.1.0")
implementation("mysql:mysql-connector-java:8.0.25")
implementation("org.jetbrains.exposed:exposed-core:0.37.3")
implementation("org.jetbrains.exposed:exposed-dao:0.37.3")
implementation("org.jetbrains.exposed:exposed-jdbc:0.37.3")
implementation("com.zaxxer:HikariCP:5.0.0")
implementation("net.kyori:adventure-platform-bukkit:4.1.0")
implementation("org.javassist:javassist:3.29.2-GA")
implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.1.2")
implementation("org.mongodb:mongodb-driver-kotlin-coroutine:5.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1")
implementation("org.mongodb:bson-kotlinx:5.0.0")
implementation("com.moandjiezana.toml:toml4j:0.7.2") {
exclude(group = "com.google.code.gson", module = "gson")
}
@@ -24,7 +29,7 @@ dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.2-R0.1-SNAPSHOT")
// Plugin dependencies
compileOnly("com.comphenix.protocol:ProtocolLib:5.1.0")
compileOnly("com.comphenix.protocol:ProtocolLib:5.0.0-SNAPSHOT")
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.7-SNAPSHOT")
compileOnly("com.github.TechFortress:GriefPrevention:16.17.1")
compileOnly("com.github.TownyAdvanced:Towny:0.99.5.21") {
@@ -35,7 +40,7 @@ dependencies {
compileOnly("fr.neatmonster:nocheatplus:3.16.1-SNAPSHOT")
compileOnly("com.github.jiangdashao:matrix-api-repo:317d4635fd")
compileOnly("com.gmail.nossr50.mcMMO:mcMMO:2.1.202")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("me.clip:placeholderapi:2.11.4")
compileOnly("com.github.brcdev-minecraft:shopgui-api:3.0.0")
compileOnly("com.github.LoneDev6:API-ItemsAdder:2.4.7")
compileOnly("com.arcaniax:HeadDatabase-API:1.3.1")
@@ -43,11 +48,12 @@ dependencies {
compileOnly("com.github.EssentialsX:Essentials:2.18.2")
compileOnly("com.bgsoftware:SuperiorSkyblockAPI:1.8.3")
compileOnly("com.github.MilkBowl:VaultAPI:1.7")
compileOnly("com.github.WhipDevelopment:CrashClaim:c697d3e9ef")
compileOnly("com.github.WhipDevelopment:CrashClaim:f9cd7d92eb")
compileOnly("com.github.decentsoftware-eu:decentholograms:2.8.5")
compileOnly("com.github.Gypopo:EconomyShopGUI-API:1.4.6")
compileOnly("com.github.N0RSKA:ScytherAPI:55a")
compileOnly("org.black_ixx:playerpoints:3.2.5")
compileOnly("com.github.Ssomar-Developement:SCore:3.4.7")
compileOnly("io.lumine:Mythic:5.3.5")
compileOnly("io.lumine:LumineUtils:1.19-SNAPSHOT")
compileOnly("com.SirBlobman.combatlogx:CombatLogX-API:10.0.0.0-SNAPSHOT")
@@ -60,7 +66,7 @@ dependencies {
compileOnly("net.william278.huskclaims:huskclaims-bukkit:1.0.1")
compileOnly("net.william278:husktowns:2.6.1")
compileOnly("com.github.jojodmo:ItemBridge:b0054538c1")
compileOnly("de.oliver:FancyHolograms:2.4.0")
compileOnly("de.oliver:FancyHolograms:2.3.0")
compileOnly(fileTree("../../lib") {
include("*.jar")
@@ -70,6 +76,7 @@ dependencies {
tasks {
shadowJar {
minimize {
exclude(dependency("org.litote.kmongo:kmongo-coroutine:.*"))
exclude(dependency("org.jetbrains.exposed:.*:.*"))
exclude(dependency("com.willfp:ModelEngineBridge:.*"))
}

View File

@@ -4,6 +4,7 @@ import com.willfp.eco.core.Eco
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.core.PluginLike
import com.willfp.eco.core.PluginProps
import com.willfp.eco.core.Prerequisite
import com.willfp.eco.core.command.CommandBase
import com.willfp.eco.core.command.PluginCommandBase
import com.willfp.eco.core.config.ConfigType
@@ -43,7 +44,8 @@ import com.willfp.eco.internal.proxy.EcoProxyFactory
import com.willfp.eco.internal.scheduling.EcoScheduler
import com.willfp.eco.internal.spigot.data.DataYml
import com.willfp.eco.internal.spigot.data.KeyRegistry
import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler
import com.willfp.eco.internal.spigot.data.ProfileHandler
import com.willfp.eco.internal.spigot.data.storage.HandlerType
import com.willfp.eco.internal.spigot.integrations.bstats.MetricHandler
import com.willfp.eco.internal.spigot.math.DelegatedExpressionHandler
import com.willfp.eco.internal.spigot.math.ImmediatePlaceholderTranslationExpressionHandler
@@ -72,7 +74,7 @@ import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.SkullMeta
import org.bukkit.persistence.PersistentDataContainer
import java.net.URLClassLoader
import java.util.UUID
import java.util.*
private val loadedEcoPlugins = mutableMapOf<String, EcoPlugin>()
@@ -80,7 +82,10 @@ private val loadedEcoPlugins = mutableMapOf<String, EcoPlugin>()
class EcoImpl : EcoSpigotPlugin(), Eco {
override val dataYml = DataYml(this)
override val profileHandler = ProfileHandler(this)
override val profileHandler = ProfileHandler(
HandlerType.valueOf(this.configYml.getString("data-handler").uppercase()),
this
)
init {
getProxy(CommonsInitializerProxy::class.java).init(this)
@@ -285,10 +290,10 @@ class EcoImpl : EcoSpigotPlugin(), Eco {
bukkitAudiences
override fun getServerProfile() =
profileHandler.getServerProfile()
profileHandler.loadServerProfile()
override fun loadPlayerProfile(uuid: UUID) =
profileHandler.getPlayerProfile(uuid)
profileHandler.load(uuid)
override fun createDummyEntity(location: Location): Entity =
getProxy(DummyEntityFactoryProxy::class.java).createDummyEntity(location)

View File

@@ -9,6 +9,7 @@ import com.willfp.eco.core.integrations.IntegrationLoader
import com.willfp.eco.core.integrations.afk.AFKManager
import com.willfp.eco.core.integrations.anticheat.AnticheatManager
import com.willfp.eco.core.integrations.antigrief.AntigriefManager
import com.willfp.eco.core.integrations.customblocks.CustomBlocksManager
import com.willfp.eco.core.integrations.customentities.CustomEntitiesManager
import com.willfp.eco.core.integrations.customitems.CustomItemsManager
import com.willfp.eco.core.integrations.economy.EconomyManager
@@ -61,10 +62,11 @@ import com.willfp.eco.internal.price.PriceFactoryXP
import com.willfp.eco.internal.price.PriceFactoryXPLevels
import com.willfp.eco.internal.recipes.AutocrafterPatch
import com.willfp.eco.internal.spigot.arrows.ArrowDataListener
import com.willfp.eco.internal.spigot.data.DataListener
import com.willfp.eco.internal.spigot.data.DataYml
import com.willfp.eco.internal.spigot.data.PlayerBlockListener
import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler
import com.willfp.eco.internal.spigot.data.profiles.ProfileLoadListener
import com.willfp.eco.internal.spigot.data.ProfileHandler
import com.willfp.eco.internal.spigot.data.storage.ProfileSaver
import com.willfp.eco.internal.spigot.drops.CollatedRunnable
import com.willfp.eco.internal.spigot.eventlisteners.EntityDeathByEntityListeners
import com.willfp.eco.internal.spigot.eventlisteners.NaturalExpGainListenersPaper
@@ -100,6 +102,7 @@ import com.willfp.eco.internal.spigot.integrations.antigrief.AntigriefRPGHorses
import com.willfp.eco.internal.spigot.integrations.antigrief.AntigriefSuperiorSkyblock2
import com.willfp.eco.internal.spigot.integrations.antigrief.AntigriefTowny
import com.willfp.eco.internal.spigot.integrations.antigrief.AntigriefWorldGuard
import com.willfp.eco.internal.spigot.integrations.customblocks.CustomBlocksOraxen
import com.willfp.eco.internal.spigot.integrations.customentities.CustomEntitiesMythicMobs
import com.willfp.eco.internal.spigot.integrations.customitems.CustomItemsCustomCrafting
import com.willfp.eco.internal.spigot.integrations.customitems.CustomItemsDenizen
@@ -148,7 +151,7 @@ import org.bukkit.inventory.ItemStack
abstract class EcoSpigotPlugin : EcoPlugin() {
abstract val dataYml: DataYml
abstract val profileHandler: ProfileHandler
protected abstract val profileHandler: ProfileHandler
protected var bukkitAudiences: BukkitAudiences? = null
init {
@@ -257,6 +260,9 @@ abstract class EcoSpigotPlugin : EcoPlugin() {
// Init FIS
this.getProxy(FastItemStackFactoryProxy::class.java).create(ItemStack(Material.AIR)).unwrap()
// Preload categorized persistent data keys
profileHandler.initialize()
// Init adventure
if (!Prerequisite.HAS_PAPER.isMet) {
bukkitAudiences = BukkitAudiences.create(this)
@@ -277,11 +283,14 @@ abstract class EcoSpigotPlugin : EcoPlugin() {
override fun createTasks() {
CollatedRunnable(this)
if (!profileHandler.migrateIfNecessary()) {
profileHandler.profileWriter.startTickingAutosave()
profileHandler.profileWriter.startTickingSaves()
this.scheduler.runLater(3) {
profileHandler.migrateIfNeeded()
}
profileHandler.startAutosaving()
ProfileSaver(this, profileHandler).startTicking()
this.scheduler.runTimer(
this.configYml.getInt("display-frame-ttl").toLong(),
this.configYml.getInt("display-frame-ttl").toLong(),
@@ -400,6 +409,9 @@ abstract class EcoSpigotPlugin : EcoPlugin() {
// Placeholder
IntegrationLoader("PlaceholderAPI") { PlaceholderManager.addIntegration(PlaceholderIntegrationPAPI()) },
// Custom Blocks
IntegrationLoader("Oraxen") { CustomBlocksManager.register(CustomBlocksOraxen(this)) },
// Misc
IntegrationLoader("mcMMO") { McmmoManager.register(McmmoIntegrationImpl()) },
IntegrationLoader("Multiverse-Inventories") {
@@ -420,7 +432,7 @@ abstract class EcoSpigotPlugin : EcoPlugin() {
GUIListener(this),
ArrowDataListener(this),
ArmorChangeEventListeners(this),
ProfileLoadListener(this, profileHandler),
DataListener(this, profileHandler),
PlayerBlockListener(this),
ServerLocking
)

View File

@@ -1,4 +1,4 @@
package com.willfp.eco.internal.spigot.data.profiles
package com.willfp.eco.internal.spigot.data
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.util.PlayerUtils
@@ -9,18 +9,15 @@ import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerLoginEvent
import org.bukkit.event.player.PlayerQuitEvent
class ProfileLoadListener(
class DataListener(
private val plugin: EcoPlugin,
private val handler: ProfileHandler
) : Listener {
@EventHandler(priority = EventPriority.LOWEST)
fun onLogin(event: PlayerLoginEvent) {
handler.unloadProfile(event.player.uniqueId)
}
@EventHandler(priority = EventPriority.HIGHEST)
fun onLeave(event: PlayerQuitEvent) {
handler.unloadProfile(event.player.uniqueId)
val profile = handler.accessLoadedProfile(event.player.uniqueId) ?: return
handler.saveKeysFor(event.player.uniqueId, profile.data.keys)
handler.unloadPlayer(event.player.uniqueId)
}
@EventHandler
@@ -29,4 +26,9 @@ class ProfileLoadListener(
PlayerUtils.updateSavedDisplayName(event.player)
}
}
@EventHandler(priority = EventPriority.LOWEST)
fun onLogin(event: PlayerLoginEvent) {
handler.unloadPlayer(event.player.uniqueId)
}
}

View File

@@ -0,0 +1,110 @@
package com.willfp.eco.internal.spigot.data
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.core.data.PlayerProfile
import com.willfp.eco.core.data.Profile
import com.willfp.eco.core.data.ServerProfile
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.willfp.eco.internal.spigot.data.storage.DataHandler
import com.willfp.eco.util.namespacedKeyOf
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
abstract class EcoProfile(
val data: MutableMap<PersistentDataKey<*>, Any>,
val uuid: UUID,
private val handler: DataHandler,
private val localHandler: DataHandler
) : Profile {
override fun <T : Any> write(key: PersistentDataKey<T>, value: T) {
this.data[key] = value
CHANGE_MAP.add(uuid)
}
override fun <T : Any> read(key: PersistentDataKey<T>): T {
@Suppress("UNCHECKED_CAST")
if (this.data.containsKey(key)) {
return this.data[key] as T
}
this.data[key] = if (key.isSavedLocally) {
localHandler.read(uuid, key)
} else {
handler.read(uuid, key)
} ?: key.defaultValue
return read(key)
}
override fun equals(other: Any?): Boolean {
if (other !is EcoProfile) {
return false
}
return this.uuid == other.uuid
}
override fun hashCode(): Int {
return this.uuid.hashCode()
}
companion object {
val CHANGE_MAP: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
}
}
class EcoPlayerProfile(
data: MutableMap<PersistentDataKey<*>, Any>,
uuid: UUID,
handler: DataHandler,
localHandler: DataHandler
) : EcoProfile(data, uuid, handler, localHandler), PlayerProfile {
override fun toString(): String {
return "EcoPlayerProfile{uuid=$uuid}"
}
}
private val serverIDKey = PersistentDataKey(
namespacedKeyOf("eco", "server_id"),
PersistentDataKeyType.STRING,
""
)
private val localServerIDKey = PersistentDataKey(
namespacedKeyOf("eco", "local_server_id"),
PersistentDataKeyType.STRING,
""
)
class EcoServerProfile(
data: MutableMap<PersistentDataKey<*>, Any>,
handler: DataHandler,
localHandler: DataHandler
) : EcoProfile(data, serverProfileUUID, handler, localHandler), ServerProfile {
override fun getServerID(): String {
if (this.read(serverIDKey).isBlank()) {
this.write(serverIDKey, UUID.randomUUID().toString())
}
return this.read(serverIDKey)
}
override fun getLocalServerID(): String {
if (this.read(localServerIDKey).isBlank()) {
this.write(localServerIDKey, UUID.randomUUID().toString())
}
return this.read(localServerIDKey)
}
override fun toString(): String {
return "EcoServerProfile"
}
}
private val PersistentDataKey<*>.isSavedLocally: Boolean
get() = this == localServerIDKey
|| EcoPlugin.getPlugin(this.key.namespace)?.isUsingLocalStorage == true
|| this.isLocal

View File

@@ -1,20 +1,55 @@
package com.willfp.eco.internal.spigot.data
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import org.bukkit.NamespacedKey
import java.math.BigDecimal
object KeyRegistry {
private val registry = mutableMapOf<NamespacedKey, PersistentDataKey<*>>()
fun registerKey(key: PersistentDataKey<*>) {
if (key.defaultValue == null) {
throw IllegalArgumentException("Default value cannot be null!")
if (this.registry.containsKey(key.key)) {
this.registry.remove(key.key)
}
validateKey(key)
this.registry[key.key] = key
}
fun getRegisteredKeys(): Set<PersistentDataKey<*>> {
return registry.values.toSet()
fun getRegisteredKeys(): MutableSet<PersistentDataKey<*>> {
return registry.values.toMutableSet()
}
private fun <T> validateKey(key: PersistentDataKey<T>) {
val default = key.defaultValue
when (key.type) {
PersistentDataKeyType.INT -> if (default !is Int) {
throw IllegalArgumentException("Invalid Data Type! Should be Int")
}
PersistentDataKeyType.DOUBLE -> if (default !is Double) {
throw IllegalArgumentException("Invalid Data Type! Should be Double")
}
PersistentDataKeyType.BOOLEAN -> if (default !is Boolean) {
throw IllegalArgumentException("Invalid Data Type! Should be Boolean")
}
PersistentDataKeyType.STRING -> if (default !is String) {
throw IllegalArgumentException("Invalid Data Type! Should be String")
}
PersistentDataKeyType.STRING_LIST -> if (default !is List<*> || default.firstOrNull() !is String?) {
throw IllegalArgumentException("Invalid Data Type! Should be String List")
}
PersistentDataKeyType.CONFIG -> if (default !is Config) {
throw IllegalArgumentException("Invalid Data Type! Should be Config")
}
PersistentDataKeyType.BIG_DECIMAL -> if (default !is BigDecimal) {
throw IllegalArgumentException("Invalid Data Type! Should be BigDecimal")
}
else -> throw NullPointerException("Null value found!")
}
}
}

View File

@@ -0,0 +1,185 @@
package com.willfp.eco.internal.spigot.data
import com.willfp.eco.core.data.PlayerProfile
import com.willfp.eco.core.data.Profile
import com.willfp.eco.core.data.ServerProfile
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.profile
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.ServerLocking
import com.willfp.eco.internal.spigot.data.storage.DataHandler
import com.willfp.eco.internal.spigot.data.storage.HandlerType
import com.willfp.eco.internal.spigot.data.storage.MongoDataHandler
import com.willfp.eco.internal.spigot.data.storage.MySQLDataHandler
import com.willfp.eco.internal.spigot.data.storage.YamlDataHandler
import org.bukkit.Bukkit
import java.util.UUID
val serverProfileUUID = UUID(0, 0)
class ProfileHandler(
private val type: HandlerType,
private val plugin: EcoSpigotPlugin
) {
private val loaded = mutableMapOf<UUID, EcoProfile>()
private val localHandler = YamlDataHandler(plugin, this)
val handler: DataHandler = when (type) {
HandlerType.YAML -> localHandler
HandlerType.MYSQL -> MySQLDataHandler(plugin, this)
HandlerType.MONGO -> MongoDataHandler(plugin, this)
}
fun accessLoadedProfile(uuid: UUID): EcoProfile? =
loaded[uuid]
fun loadGenericProfile(uuid: UUID): Profile {
val found = loaded[uuid]
if (found != null) {
return found
}
val data = mutableMapOf<PersistentDataKey<*>, Any>()
val profile = if (uuid == serverProfileUUID)
EcoServerProfile(data, handler, localHandler) else EcoPlayerProfile(data, uuid, handler, localHandler)
loaded[uuid] = profile
return profile
}
fun load(uuid: UUID): PlayerProfile {
return loadGenericProfile(uuid) as PlayerProfile
}
fun loadServerProfile(): ServerProfile {
return loadGenericProfile(serverProfileUUID) as ServerProfile
}
fun saveKeysFor(uuid: UUID, keys: Set<PersistentDataKey<*>>) {
val profile = accessLoadedProfile(uuid) ?: return
val map = mutableMapOf<PersistentDataKey<*>, Any>()
for (key in keys) {
map[key] = profile.data[key] ?: continue
}
handler.saveKeysFor(uuid, map)
// Don't save to local handler if it's the same handler.
if (localHandler != handler) {
localHandler.saveKeysFor(uuid, map)
}
}
fun unloadPlayer(uuid: UUID) {
loaded.remove(uuid)
}
fun save() {
handler.save()
if (localHandler != handler) {
localHandler.save()
}
}
fun migrateIfNeeded() {
if (!plugin.configYml.getBool("perform-data-migration")) {
return
}
if (!plugin.dataYml.has("previous-handler")) {
plugin.dataYml.set("previous-handler", type.name)
plugin.dataYml.save()
}
val previousHandlerType = HandlerType.valueOf(plugin.dataYml.getString("previous-handler"))
if (previousHandlerType == type) {
return
}
val previousHandler = when (previousHandlerType) {
HandlerType.YAML -> YamlDataHandler(plugin, this)
HandlerType.MYSQL -> MySQLDataHandler(plugin, this)
HandlerType.MONGO -> MongoDataHandler(plugin, this)
}
ServerLocking.lock("Migrating player data! Check console for more information.")
plugin.logger.info("eco has detected a change in data handler!")
plugin.logger.info("Migrating server data from ${previousHandlerType.name} to ${type.name}")
plugin.logger.info("This will take a while!")
plugin.logger.info("Initializing previous handler...")
previousHandler.initialize()
val players = Bukkit.getOfflinePlayers().map { it.uniqueId }
plugin.logger.info("Found data for ${players.size} players!")
/*
Declared here as its own function to be able to use T.
*/
fun <T : Any> migrateKey(uuid: UUID, key: PersistentDataKey<T>, from: DataHandler, to: DataHandler) {
val previous: T? = from.read(uuid, key)
if (previous != null) {
Bukkit.getOfflinePlayer(uuid).profile.write(key, previous) // Nope, no idea.
to.write(uuid, key, previous)
}
}
var i = 1
for (uuid in players) {
plugin.logger.info("Migrating data for $uuid... ($i / ${players.size})")
for (key in PersistentDataKey.values()) {
// Why this? Because known points *really* likes to break things with the legacy MySQL handler.
if (key.key.key == "known_points") {
continue
}
try {
migrateKey(uuid, key, previousHandler, handler)
} catch (e: Exception) {
plugin.logger.info("Could not migrate ${key.key} for $uuid! This is probably because they do not have any data.")
}
}
i++
}
plugin.logger.info("Saving new data...")
handler.save()
plugin.logger.info("Updating previous handler...")
plugin.dataYml.set("previous-handler", type.name)
plugin.dataYml.save()
plugin.logger.info("The server will now automatically be restarted...")
ServerLocking.unlock()
Bukkit.getServer().shutdown()
}
fun initialize() {
handler.initialize()
if (localHandler != handler) {
localHandler.initialize()
}
}
fun startAutosaving() {
if (!plugin.configYml.getBool("yaml.autosave")) {
return
}
val interval = plugin.configYml.getInt("yaml.autosave-interval") * 20L
plugin.scheduler.runTimer(20, interval) {
handler.saveAsync()
localHandler.saveAsync()
}
}
}

View File

@@ -1,40 +0,0 @@
package com.willfp.eco.internal.spigot.data.handlers
import com.willfp.eco.core.data.handlers.PersistentDataHandler
import com.willfp.eco.core.registry.KRegistrable
import com.willfp.eco.core.registry.Registry
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.data.handlers.impl.MongoDBPersistentDataHandler
import com.willfp.eco.internal.spigot.data.handlers.impl.MySQLPersistentDataHandler
import com.willfp.eco.internal.spigot.data.handlers.impl.YamlPersistentDataHandler
abstract class PersistentDataHandlerFactory(
override val id: String
): KRegistrable {
abstract fun create(plugin: EcoSpigotPlugin): PersistentDataHandler
}
object PersistentDataHandlers: Registry<PersistentDataHandlerFactory>() {
init {
register(object : PersistentDataHandlerFactory("yaml") {
override fun create(plugin: EcoSpigotPlugin) =
YamlPersistentDataHandler(plugin)
})
register(object : PersistentDataHandlerFactory("mysql") {
override fun create(plugin: EcoSpigotPlugin) =
MySQLPersistentDataHandler(plugin.configYml.getSubsection("mysql"))
})
register(object : PersistentDataHandlerFactory("mongodb") {
override fun create(plugin: EcoSpigotPlugin) =
MongoDBPersistentDataHandler(plugin.configYml.getSubsection("mongodb"))
})
// Configs should also accept "mongo"
register(object : PersistentDataHandlerFactory("mongo") {
override fun create(plugin: EcoSpigotPlugin) =
MongoDBPersistentDataHandler(plugin.configYml.getSubsection("mongodb"))
})
}
}

View File

@@ -1,142 +0,0 @@
package com.willfp.eco.internal.spigot.data.handlers.impl
import com.mongodb.MongoClientSettings
import com.mongodb.client.model.Filters
import com.mongodb.kotlin.client.coroutine.MongoClient
import com.willfp.eco.core.config.Configs
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.data.handlers.DataTypeSerializer
import com.willfp.eco.core.data.handlers.PersistentDataHandler
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlerFactory
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.bson.BsonArray
import org.bson.BsonBoolean
import org.bson.BsonDecimal128
import org.bson.BsonDocument
import org.bson.BsonDouble
import org.bson.BsonInt32
import org.bson.BsonString
import org.bson.BsonValue
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.pojo.PojoCodecProvider
import java.math.BigDecimal
import java.util.UUID
class LegacyMongoDBPersistentDataHandler(
config: Config
) : PersistentDataHandler("legacy_mongodb") {
private val codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build())
)
private val client = MongoClient.create(config.getString("url"))
private val database = client.getDatabase(config.getString("database"))
private val collection = database.getCollection<BsonDocument>("uuidprofile")
.withCodecRegistry(codecRegistry)
init {
PersistentDataKeyType.STRING.registerSerializer(this, object : LegacyMongoSerializer<String>() {
override fun deserialize(value: BsonValue): String {
return value.asString().value
}
})
PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : LegacyMongoSerializer<Boolean>() {
override fun deserialize(value: BsonValue): Boolean {
return value.asBoolean().value
}
})
PersistentDataKeyType.INT.registerSerializer(this, object : LegacyMongoSerializer<Int>() {
override fun deserialize(value: BsonValue): Int {
return value.asInt32().value
}
})
PersistentDataKeyType.DOUBLE.registerSerializer(this, object : LegacyMongoSerializer<Double>() {
override fun deserialize(value: BsonValue): Double {
return value.asDouble().value
}
})
PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : LegacyMongoSerializer<List<String>>() {
override fun deserialize(value: BsonValue): List<String> {
return value.asArray().values.map { it.asString().value }
}
})
PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : LegacyMongoSerializer<BigDecimal>() {
override fun deserialize(value: BsonValue): BigDecimal {
return value.asDecimal128().value.bigDecimalValue()
}
})
PersistentDataKeyType.CONFIG.registerSerializer(this, object : LegacyMongoSerializer<Config>() {
private fun deserializeConfigValue(value: BsonValue): Any {
return when (value) {
is BsonString -> value.value
is BsonInt32 -> value.value
is BsonDouble -> value.value
is BsonBoolean -> value.value
is BsonDecimal128 -> value.value.bigDecimalValue()
is BsonArray -> value.values.map { deserializeConfigValue(it) }
is BsonDocument -> value.mapValues { (_, v) -> deserializeConfigValue(v) }
else -> throw IllegalArgumentException("Could not deserialize config value type ${value::class.simpleName}")
}
}
override fun deserialize(value: BsonValue): Config {
@Suppress("UNCHECKED_CAST")
return Configs.fromMap(deserializeConfigValue(value.asDocument()) as Map<String, Any>)
}
})
}
override fun getSavedUUIDs(): Set<UUID> {
return runBlocking {
collection.find().toList().map {
UUID.fromString(it.getString("_id").value)
}.toSet()
}
}
private abstract inner class LegacyMongoSerializer<T : Any> : DataTypeSerializer<T>() {
override fun readAsync(uuid: UUID, key: PersistentDataKey<T>): T? {
return runBlocking {
val filter = Filters.eq("_id", uuid.toString())
val profile = collection.find(filter)
.firstOrNull() ?: return@runBlocking null
val dataMap = profile.getDocument("data")
val value = dataMap[key.key.toString()] ?: return@runBlocking null
try {
return@runBlocking deserialize(value)
} catch (e: Exception) {
null
}
}
}
override fun writeAsync(uuid: UUID, key: PersistentDataKey<T>, value: T) {
throw UnsupportedOperationException("Legacy Mongo does not support writing")
}
protected abstract fun deserialize(value: BsonValue): T
}
object Factory: PersistentDataHandlerFactory("legacy_mongo") {
override fun create(plugin: EcoSpigotPlugin): PersistentDataHandler {
return LegacyMongoDBPersistentDataHandler(plugin.configYml.getSubsection("mongodb"))
}
}
}

View File

@@ -1,106 +0,0 @@
package com.willfp.eco.internal.spigot.data.handlers.impl
import com.willfp.eco.core.config.ConfigType
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.config.readConfig
import com.willfp.eco.core.data.handlers.DataTypeSerializer
import com.willfp.eco.core.data.handlers.PersistentDataHandler
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlerFactory
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.math.BigDecimal
import java.util.UUID
class LegacyMySQLPersistentDataHandler(
config: Config
) : PersistentDataHandler("legacy_mysql") {
private val dataSource = HikariDataSource(HikariConfig().apply {
driverClassName = "com.mysql.cj.jdbc.Driver"
username = config.getString("user")
password = config.getString("password")
jdbcUrl = "jdbc:mysql://" +
"${config.getString("host")}:" +
"${config.getString("port")}/" +
config.getString("database")
maximumPoolSize = config.getInt("connections")
})
private val database = Database.connect(dataSource)
private val table = object : UUIDTable("eco_data") {
val data = text("json_data", eagerLoading = true)
}
init {
transaction(database) {
SchemaUtils.create(table)
}
PersistentDataKeyType.STRING.registerSerializer(this, LegacyMySQLSerializer<String>())
PersistentDataKeyType.BOOLEAN.registerSerializer(this, LegacyMySQLSerializer<Boolean>())
PersistentDataKeyType.INT.registerSerializer(this, LegacyMySQLSerializer<Int>())
PersistentDataKeyType.DOUBLE.registerSerializer(this, LegacyMySQLSerializer<Double>())
PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, LegacyMySQLSerializer<BigDecimal>())
PersistentDataKeyType.CONFIG.registerSerializer(this, LegacyMySQLSerializer<Config>())
PersistentDataKeyType.STRING_LIST.registerSerializer(this, LegacyMySQLSerializer<List<String>>())
}
override fun getSavedUUIDs(): Set<UUID> {
return transaction(database) {
table.selectAll()
.map { it[table.id] }
.toSet()
}.map { it.value }.toSet()
}
private inner class LegacyMySQLSerializer<T : Any> : DataTypeSerializer<T>() {
override fun readAsync(uuid: UUID, key: PersistentDataKey<T>): T? {
val json = transaction(database) {
table.selectAll()
.where { table.id eq uuid }
.limit(1)
.singleOrNull()
?.get(table.data)
}
if (json == null) {
return null
}
val data = readConfig(json, ConfigType.JSON)
val value: Any? = when (key.type) {
PersistentDataKeyType.INT -> data.getIntOrNull(key.key.toString())
PersistentDataKeyType.DOUBLE -> data.getDoubleOrNull(key.key.toString())
PersistentDataKeyType.STRING -> data.getStringOrNull(key.key.toString())
PersistentDataKeyType.BOOLEAN -> data.getBoolOrNull(key.key.toString())
PersistentDataKeyType.STRING_LIST -> data.getStringsOrNull(key.key.toString())
PersistentDataKeyType.CONFIG -> data.getSubsectionOrNull(key.key.toString())
PersistentDataKeyType.BIG_DECIMAL -> data.getBigDecimalOrNull(key.key.toString())
else -> null
}
@Suppress("UNCHECKED_CAST")
return value as? T?
}
override fun writeAsync(uuid: UUID, key: PersistentDataKey<T>, value: T) {
throw UnsupportedOperationException("Legacy MySQL does not support writing")
}
}
object Factory: PersistentDataHandlerFactory("legacy_mysql") {
override fun create(plugin: EcoSpigotPlugin): PersistentDataHandler {
return LegacyMySQLPersistentDataHandler(plugin.configYml.getSubsection("mysql"))
}
}
}

View File

@@ -1,192 +0,0 @@
package com.willfp.eco.internal.spigot.data.handlers.impl
import com.mongodb.MongoClientSettings
import com.mongodb.client.model.Filters
import com.mongodb.client.model.ReplaceOptions
import com.mongodb.kotlin.client.coroutine.MongoClient
import com.willfp.eco.core.config.Configs
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.data.handlers.DataTypeSerializer
import com.willfp.eco.core.data.handlers.PersistentDataHandler
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.bson.BsonArray
import org.bson.BsonBoolean
import org.bson.BsonDecimal128
import org.bson.BsonDocument
import org.bson.BsonDouble
import org.bson.BsonInt32
import org.bson.BsonObjectId
import org.bson.BsonString
import org.bson.BsonValue
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.pojo.PojoCodecProvider
import org.bson.types.Decimal128
import java.math.BigDecimal
import java.util.UUID
class MongoDBPersistentDataHandler(
config: Config
) : PersistentDataHandler("mongo") {
private val codecRegistry = CodecRegistries.fromRegistries(
MongoClientSettings.getDefaultCodecRegistry(),
CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build())
)
private val client = MongoClient.create(config.getString("url"))
private val database = client.getDatabase(config.getString("database"))
private val collection = database.getCollection<BsonDocument>(config.getString("collection"))
.withCodecRegistry(codecRegistry)
init {
PersistentDataKeyType.STRING.registerSerializer(this, object : MongoSerializer<String>() {
override fun serialize(value: String): BsonValue {
return BsonString(value)
}
override fun deserialize(value: BsonValue): String {
return value.asString().value
}
})
PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : MongoSerializer<Boolean>() {
override fun serialize(value: Boolean): BsonValue {
return BsonBoolean(value)
}
override fun deserialize(value: BsonValue): Boolean {
return value.asBoolean().value
}
})
PersistentDataKeyType.INT.registerSerializer(this, object : MongoSerializer<Int>() {
override fun serialize(value: Int): BsonValue {
return BsonInt32(value)
}
override fun deserialize(value: BsonValue): Int {
return value.asInt32().value
}
})
PersistentDataKeyType.DOUBLE.registerSerializer(this, object : MongoSerializer<Double>() {
override fun serialize(value: Double): BsonValue {
return BsonDouble(value)
}
override fun deserialize(value: BsonValue): Double {
return value.asDouble().value
}
})
PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MongoSerializer<List<String>>() {
override fun serialize(value: List<String>): BsonValue {
return BsonArray(value.map { BsonString(it) })
}
override fun deserialize(value: BsonValue): List<String> {
return value.asArray().values.map { it.asString().value }
}
})
PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : MongoSerializer<BigDecimal>() {
override fun serialize(value: BigDecimal): BsonValue {
return BsonDecimal128(Decimal128(value))
}
override fun deserialize(value: BsonValue): BigDecimal {
return value.asDecimal128().value.bigDecimalValue()
}
})
PersistentDataKeyType.CONFIG.registerSerializer(this, object : MongoSerializer<Config>() {
private fun deserializeConfigValue(value: BsonValue): Any {
return when (value) {
is BsonString -> value.value
is BsonInt32 -> value.value
is BsonDouble -> value.value
is BsonBoolean -> value.value
is BsonDecimal128 -> value.value.bigDecimalValue()
is BsonArray -> value.values.map { deserializeConfigValue(it) }
is BsonDocument -> value.mapValues { (_, v) -> deserializeConfigValue(v) }
else -> throw IllegalArgumentException("Could not deserialize config value type ${value::class.simpleName}")
}
}
private fun serializeConfigValue(value: Any): BsonValue {
return when (value) {
is String -> BsonString(value)
is Int -> BsonInt32(value)
is Double -> BsonDouble(value)
is Boolean -> BsonBoolean(value)
is BigDecimal -> BsonDecimal128(Decimal128(value))
is List<*> -> BsonArray(value.map { serializeConfigValue(it!!) })
is Map<*, *> -> BsonDocument().apply {
value.forEach { (k, v) -> append(k.toString(), serializeConfigValue(v!!)) }
}
else -> throw IllegalArgumentException("Could not serialize config value type ${value::class.simpleName}")
}
}
override fun serialize(value: Config): BsonValue {
return serializeConfigValue(value.toMap())
}
override fun deserialize(value: BsonValue): Config {
@Suppress("UNCHECKED_CAST")
return Configs.fromMap(deserializeConfigValue(value.asDocument()) as Map<String, Any>)
}
})
}
override fun getSavedUUIDs(): Set<UUID> {
return runBlocking {
collection.find().toList().map {
UUID.fromString(it.getString("uuid").value)
}.toSet()
}
}
private abstract inner class MongoSerializer<T : Any> : DataTypeSerializer<T>() {
override fun readAsync(uuid: UUID, key: PersistentDataKey<T>): T? {
return runBlocking {
val filter = Filters.eq("uuid", uuid.toString())
val profile = collection.find(filter)
.firstOrNull() ?: return@runBlocking null
val value = profile[key.key.toString()] ?: return@runBlocking null
deserialize(value)
}
}
override fun writeAsync(uuid: UUID, key: PersistentDataKey<T>, value: T) {
runBlocking {
val filter = Filters.eq("uuid", uuid.toString())
val profile = collection.find(filter).firstOrNull()
?: BsonDocument()
.append("_id", BsonObjectId())
.append("uuid", BsonString(uuid.toString()))
profile.append(key.key.toString(), serialize(value))
collection.replaceOne(
filter,
profile,
ReplaceOptions().upsert(true)
)
}
}
protected abstract fun serialize(value: T): BsonValue
protected abstract fun deserialize(value: BsonValue): T
}
}

View File

@@ -1,267 +0,0 @@
package com.willfp.eco.internal.spigot.data.handlers.impl
import com.willfp.eco.core.config.ConfigType
import com.willfp.eco.core.config.Configs
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.config.readConfig
import com.willfp.eco.core.data.handlers.DataTypeSerializer
import com.willfp.eco.core.data.handlers.PersistentDataHandler
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.replace
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.upsert
import java.math.BigDecimal
import java.util.UUID
import kotlin.math.pow
private const val VALUE_COLUMN_NAME = "dataValue"
private const val UUID_COLUMN_NAME = "profileUUID"
private const val KEY_COLUMN_NAME = "dataKey"
private const val INDEX_COLUMN_NAME = "listIndex"
class MySQLPersistentDataHandler(
config: Config
) : PersistentDataHandler("mysql") {
private val dataSource = HikariDataSource(HikariConfig().apply {
driverClassName = "com.mysql.cj.jdbc.Driver"
username = config.getString("user")
password = config.getString("password")
jdbcUrl = "jdbc:mysql://" +
"${config.getString("host")}:" +
"${config.getString("port")}/" +
config.getString("database")
maximumPoolSize = config.getInt("connections")
})
private val prefix = config.getString("prefix")
private val database = Database.connect(dataSource)
init {
PersistentDataKeyType.STRING.registerSerializer(this, object : DirectStoreSerializer<String>() {
override val table = object : KeyTable<String>("string") {
override val value = varchar(VALUE_COLUMN_NAME, 256)
}
}.createTable())
PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : DirectStoreSerializer<Boolean>() {
override val table = object : KeyTable<Boolean>("boolean") {
override val value = bool(VALUE_COLUMN_NAME)
}
}.createTable())
PersistentDataKeyType.INT.registerSerializer(this, object : DirectStoreSerializer<Int>() {
override val table = object : KeyTable<Int>("int") {
override val value = integer(VALUE_COLUMN_NAME)
}
}.createTable())
PersistentDataKeyType.DOUBLE.registerSerializer(this, object : DirectStoreSerializer<Double>() {
override val table = object : KeyTable<Double>("double") {
override val value = double(VALUE_COLUMN_NAME)
}
}.createTable())
PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : DirectStoreSerializer<BigDecimal>() {
override val table = object : KeyTable<BigDecimal>("big_decimal") {
// 34 digits of precision, 4 digits of scale
override val value = decimal(VALUE_COLUMN_NAME, 34, 4)
}
}.createTable())
PersistentDataKeyType.CONFIG.registerSerializer(this, object : SingleValueSerializer<Config, String>() {
override val table = object : KeyTable<String>("config") {
override val value = text(VALUE_COLUMN_NAME)
}
override fun convertFromStored(value: String): Config {
return readConfig(value, ConfigType.JSON)
}
override fun convertToStored(value: Config): String {
// Store config as JSON
return if (value.type == ConfigType.JSON) {
value.toPlaintext()
} else {
Configs.fromMap(value.toMap(), ConfigType.JSON).toPlaintext()
}
}
}.createTable())
PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : MultiValueSerializer<String>() {
override val table = object : ListKeyTable<String>("string_list") {
override val value = varchar(VALUE_COLUMN_NAME, 256)
}
}.createTable())
}
override fun getSavedUUIDs(): Set<UUID> {
val savedUUIDs = mutableSetOf<UUID>()
for (keyType in PersistentDataKeyType.values()) {
val serializer = keyType.getSerializer(this) as MySQLSerializer<*>
savedUUIDs.addAll(serializer.getSavedUUIDs())
}
return savedUUIDs
}
private abstract inner class MySQLSerializer<T : Any> : DataTypeSerializer<T>() {
protected abstract val table: ProfileTable
fun getSavedUUIDs(): Set<UUID> {
return transaction(database) {
table.selectAll().map { it[table.uuid] }.toSet()
}
}
fun createTable(): MySQLSerializer<T> {
transaction(database) {
SchemaUtils.create(table)
}
return this
}
}
// T is the key type
// S is the stored value type
private abstract inner class SingleValueSerializer<T : Any, S : Any> : MySQLSerializer<T>() {
abstract override val table: KeyTable<S>
abstract fun convertToStored(value: T): S
abstract fun convertFromStored(value: S): T
override fun readAsync(uuid: UUID, key: PersistentDataKey<T>): T? {
val stored = transaction(database) {
table.selectAll()
.where { (table.uuid eq uuid) and (table.key eq key.key.toString()) }
.limit(1)
.singleOrNull()
?.get(table.value)
}
return stored?.let { convertFromStored(it) }
}
override fun writeAsync(uuid: UUID, key: PersistentDataKey<T>, value: T) {
withRetries {
transaction(database) {
table.upsert {
it[table.uuid] = uuid
it[table.key] = key.key.toString()
it[table.value] = convertToStored(value)
}
}
}
}
}
private abstract inner class DirectStoreSerializer<T : Any> : SingleValueSerializer<T, T>() {
override fun convertToStored(value: T): T {
return value
}
override fun convertFromStored(value: T): T {
return value
}
}
private abstract inner class MultiValueSerializer<T : Any> : MySQLSerializer<List<T>>() {
abstract override val table: ListKeyTable<T>
override fun readAsync(uuid: UUID, key: PersistentDataKey<List<T>>): List<T>? {
val stored = transaction(database) {
table.selectAll()
.where { (table.uuid eq uuid) and (table.key eq key.key.toString()) }
.orderBy(table.index)
.map { it[table.value] }
}
return stored
}
override fun writeAsync(uuid: UUID, key: PersistentDataKey<List<T>>, value: List<T>) {
withRetries {
transaction(database) {
// Remove existing values greater than the new list size
table.deleteWhere {
(table.uuid eq uuid) and
(table.key eq key.key.toString()) and
(table.index greaterEq value.size)
}
// Replace existing values in bounds
value.forEachIndexed { index, t ->
table.replace {
it[table.uuid] = uuid
it[table.key] = key.key.toString()
it[table.index] = index
it[table.value] = t
}
}
}
}
}
}
private abstract inner class ProfileTable(name: String) : Table(prefix + name) {
val uuid = uuid(UUID_COLUMN_NAME)
}
private abstract inner class KeyTable<T>(name: String) : ProfileTable(name) {
val key = varchar(KEY_COLUMN_NAME, 128)
abstract val value: Column<T>
override val primaryKey = PrimaryKey(uuid, key)
init {
uniqueIndex(uuid, key)
}
}
private abstract inner class ListKeyTable<T>(name: String) : ProfileTable(name) {
val key = varchar(KEY_COLUMN_NAME, 128)
val index = integer(INDEX_COLUMN_NAME)
abstract val value: Column<T>
override val primaryKey = PrimaryKey(uuid, key, index)
init {
uniqueIndex(uuid, key, index)
}
}
private inline fun <T> withRetries(action: () -> T): T? {
var retries = 1
while (true) {
try {
return action()
} catch (e: Exception) {
if (retries > 5) {
return null
}
retries++
// Exponential backoff
runBlocking {
delay(2.0.pow(retries.toDouble()).toLong())
}
}
}
}
}

View File

@@ -1,72 +0,0 @@
package com.willfp.eco.internal.spigot.data.handlers.impl
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.data.handlers.DataTypeSerializer
import com.willfp.eco.core.data.handlers.PersistentDataHandler
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import java.math.BigDecimal
import java.util.UUID
class YamlPersistentDataHandler(
plugin: EcoSpigotPlugin
) : PersistentDataHandler("yaml") {
private val dataYml = plugin.dataYml
init {
PersistentDataKeyType.STRING.registerSerializer(this, object : YamlSerializer<String>() {
override fun read(config: Config, key: String) = config.getStringOrNull(key)
})
PersistentDataKeyType.BOOLEAN.registerSerializer(this, object : YamlSerializer<Boolean>() {
override fun read(config: Config, key: String) = config.getBoolOrNull(key)
})
PersistentDataKeyType.INT.registerSerializer(this, object : YamlSerializer<Int>() {
override fun read(config: Config, key: String) = config.getIntOrNull(key)
})
PersistentDataKeyType.DOUBLE.registerSerializer(this, object : YamlSerializer<Double>() {
override fun read(config: Config, key: String) = config.getDoubleOrNull(key)
})
PersistentDataKeyType.STRING_LIST.registerSerializer(this, object : YamlSerializer<List<String>>() {
override fun read(config: Config, key: String) = config.getStringsOrNull(key)
})
PersistentDataKeyType.CONFIG.registerSerializer(this, object : YamlSerializer<Config>() {
override fun read(config: Config, key: String) = config.getSubsectionOrNull(key)
})
PersistentDataKeyType.BIG_DECIMAL.registerSerializer(this, object : YamlSerializer<BigDecimal>() {
override fun read(config: Config, key: String) = config.getBigDecimalOrNull(key)
})
}
override fun getSavedUUIDs(): Set<UUID> {
return dataYml.getSubsection("player").getKeys(false)
.map { UUID.fromString(it) }
.toSet()
}
override fun shouldAutosave(): Boolean {
return true
}
override fun doSave() {
dataYml.save()
}
private abstract inner class YamlSerializer<T: Any>: DataTypeSerializer<T>() {
protected abstract fun read(config: Config, key: String): T?
final override fun readAsync(uuid: UUID, key: PersistentDataKey<T>): T? {
return read(dataYml, "player.$uuid.${key.key}")
}
final override fun writeAsync(uuid: UUID, key: PersistentDataKey<T>, value: T) {
dataYml.set("player.$uuid.${key.key}", value)
}
}
}

View File

@@ -1,141 +0,0 @@
package com.willfp.eco.internal.spigot.data.profiles
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.ServerLocking
import com.willfp.eco.internal.spigot.data.KeyRegistry
import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlerFactory
import com.willfp.eco.internal.spigot.data.handlers.PersistentDataHandlers
import com.willfp.eco.internal.spigot.data.handlers.impl.LegacyMongoDBPersistentDataHandler
import com.willfp.eco.internal.spigot.data.handlers.impl.LegacyMySQLPersistentDataHandler
import com.willfp.eco.internal.spigot.data.handlers.impl.MongoDBPersistentDataHandler
import com.willfp.eco.internal.spigot.data.handlers.impl.MySQLPersistentDataHandler
import com.willfp.eco.internal.spigot.data.handlers.impl.YamlPersistentDataHandler
import com.willfp.eco.internal.spigot.data.profiles.impl.EcoPlayerProfile
import com.willfp.eco.internal.spigot.data.profiles.impl.EcoProfile
import com.willfp.eco.internal.spigot.data.profiles.impl.EcoServerProfile
import com.willfp.eco.internal.spigot.data.profiles.impl.serverProfileUUID
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
const val LEGACY_MIGRATED_KEY = "legacy-data-migrated"
class ProfileHandler(
private val plugin: EcoSpigotPlugin
) {
private val handlerId = plugin.configYml.getString("data-handler")
val localHandler = YamlPersistentDataHandler(plugin)
val defaultHandler = PersistentDataHandlers[handlerId]?.create(plugin)
?: throw IllegalArgumentException("Invalid data handler ($handlerId)")
val profileWriter = ProfileWriter(plugin, this)
private val loaded = ConcurrentHashMap<UUID, EcoProfile>()
fun getPlayerProfile(uuid: UUID): EcoPlayerProfile {
return loaded.computeIfAbsent(uuid) {
EcoPlayerProfile(it, this)
} as EcoPlayerProfile
}
fun getServerProfile(): EcoServerProfile {
return loaded.computeIfAbsent(serverProfileUUID) {
EcoServerProfile(this)
} as EcoServerProfile
}
fun unloadProfile(uuid: UUID) {
loaded.remove(uuid)
}
fun save() {
localHandler.shutdown()
defaultHandler.shutdown()
}
fun migrateIfNecessary(): Boolean {
if (!plugin.configYml.getBool("perform-data-migration")) {
return false
}
// First install
if (!plugin.dataYml.has("previous-handler")) {
plugin.dataYml.set("previous-handler", defaultHandler.id)
plugin.dataYml.set(LEGACY_MIGRATED_KEY, true)
plugin.dataYml.save()
return false
}
val previousHandlerId = plugin.dataYml.getString("previous-handler").lowercase()
if (previousHandlerId != defaultHandler.id) {
val fromFactory = PersistentDataHandlers[previousHandlerId] ?: return false
scheduleMigration(fromFactory)
return true
}
if (defaultHandler is MySQLPersistentDataHandler && !plugin.dataYml.getBool(LEGACY_MIGRATED_KEY)) {
plugin.logger.info("eco has detected a legacy MySQL database. Migrating to new MySQL database...")
scheduleMigration(LegacyMySQLPersistentDataHandler.Factory)
return true
}
if (defaultHandler is MongoDBPersistentDataHandler && !plugin.dataYml.getBool(LEGACY_MIGRATED_KEY)) {
plugin.logger.info("eco has detected a legacy MongoDB database. Migrating to new MongoDB database...")
scheduleMigration(LegacyMongoDBPersistentDataHandler.Factory)
return true
}
return false
}
private fun scheduleMigration(fromFactory: PersistentDataHandlerFactory) {
ServerLocking.lock("Migrating player data! Check console for more information.")
// Run after 5 ticks to allow plugins to load their data keys
plugin.scheduler.runLater(5) {
doMigrate(fromFactory)
plugin.dataYml.set(LEGACY_MIGRATED_KEY, true)
plugin.dataYml.save()
}
}
private fun doMigrate(fromFactory: PersistentDataHandlerFactory) {
plugin.logger.info("eco has detected a change in data handler")
plugin.logger.info("${fromFactory.id} --> ${defaultHandler.id}")
plugin.logger.info("This will take a while! Players will not be able to join during this time.")
val fromHandler = fromFactory.create(plugin)
val toHandler = defaultHandler
val keys = KeyRegistry.getRegisteredKeys()
plugin.logger.info("Keys to migrate: ${keys.map { it.key }.joinToString(", ") }}")
plugin.logger.info("Loading profile UUIDs from ${fromFactory.id}...")
plugin.logger.info("This step may take a while depending on the size of your database.")
val uuids = fromHandler.getSavedUUIDs()
plugin.logger.info("Found ${uuids.size} profiles to migrate")
for ((index, uuid) in uuids.withIndex()) {
plugin.logger.info("(${index + 1}/${uuids.size}) Migrating $uuid")
val profile = fromHandler.serializeProfile(uuid, keys)
toHandler.loadSerializedProfile(profile)
}
plugin.logger.info("Profile writes submitted! Waiting for completion...")
toHandler.shutdown()
plugin.logger.info("Updating previous handler...")
plugin.dataYml.set("previous-handler", handlerId)
plugin.dataYml.save()
plugin.logger.info("The server will now automatically be restarted...")
plugin.server.shutdown()
}
}

View File

@@ -1,59 +0,0 @@
package com.willfp.eco.internal.spigot.data.profiles
import com.willfp.eco.core.EcoPlugin
import com.willfp.eco.core.data.keys.PersistentDataKey
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/*
The profile writer exists as an optimization to batch writes to the database.
This is necessary because values frequently change multiple times per tick,
and we don't want to write to the database every time a value changes.
Instead, we only commit the last value that was set every interval (default 1 tick).
*/
class ProfileWriter(
private val plugin: EcoPlugin,
private val handler: ProfileHandler
) {
private val saveInterval = plugin.configYml.getInt("save-interval").toLong()
private val autosaveInterval = plugin.configYml.getInt("autosave-interval").toLong()
private val valuesToWrite = ConcurrentHashMap<WriteRequest<*>, Any>()
fun <T : Any> write(uuid: UUID, key: PersistentDataKey<T>, value: T) {
valuesToWrite[WriteRequest(uuid, key)] = value
}
fun startTickingSaves() {
plugin.scheduler.runTimer(20, saveInterval) {
val iterator = valuesToWrite.iterator()
while (iterator.hasNext()) {
val (request, value) = iterator.next()
iterator.remove()
val dataHandler = if (request.key.isSavedLocally) handler.localHandler else handler.defaultHandler
// Pass the value to the data handler
@Suppress("UNCHECKED_CAST")
dataHandler.write(request.uuid, request.key as PersistentDataKey<Any>, value)
}
}
}
fun startTickingAutosave() {
plugin.scheduler.runTimer(autosaveInterval, autosaveInterval) {
if (handler.localHandler.shouldAutosave()) {
handler.localHandler.save()
}
}
}
private data class WriteRequest<T>(val uuid: UUID, val key: PersistentDataKey<T>)
}
val PersistentDataKey<*>.isSavedLocally: Boolean
get() = this.isLocal || EcoPlugin.getPlugin(this.key.namespace)?.isUsingLocalStorage == true

View File

@@ -1,14 +0,0 @@
package com.willfp.eco.internal.spigot.data.profiles.impl
import com.willfp.eco.core.data.PlayerProfile
import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler
import java.util.UUID
class EcoPlayerProfile(
uuid: UUID,
handler: ProfileHandler
) : EcoProfile(uuid, handler), PlayerProfile {
override fun toString(): String {
return "EcoPlayerProfile{uuid=$uuid}"
}
}

View File

@@ -1,48 +0,0 @@
package com.willfp.eco.internal.spigot.data.profiles.impl
import com.willfp.eco.core.data.Profile
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler
import com.willfp.eco.internal.spigot.data.profiles.isSavedLocally
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
abstract class EcoProfile(
val uuid: UUID,
private val handler: ProfileHandler
) : Profile {
private val data = ConcurrentHashMap<PersistentDataKey<*>, Any>()
override fun <T : Any> write(key: PersistentDataKey<T>, value: T) {
this.data[key] = value
handler.profileWriter.write(uuid, key, value)
}
override fun <T : Any> read(key: PersistentDataKey<T>): T {
@Suppress("UNCHECKED_CAST")
if (this.data.containsKey(key)) {
return this.data[key] as T
}
this.data[key] = if (key.isSavedLocally) {
handler.localHandler.read(uuid, key)
} else {
handler.defaultHandler.read(uuid, key)
} ?: key.defaultValue
return read(key)
}
override fun equals(other: Any?): Boolean {
if (other !is EcoProfile) {
return false
}
return this.uuid == other.uuid
}
override fun hashCode(): Int {
return this.uuid.hashCode()
}
}

View File

@@ -1,47 +0,0 @@
package com.willfp.eco.internal.spigot.data.profiles.impl
import com.willfp.eco.core.data.ServerProfile
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.willfp.eco.internal.spigot.data.profiles.ProfileHandler
import com.willfp.eco.util.namespacedKeyOf
import java.util.UUID
val serverIDKey = PersistentDataKey(
namespacedKeyOf("eco", "server_id"),
PersistentDataKeyType.STRING,
""
)
val localServerIDKey = PersistentDataKey(
namespacedKeyOf("eco", "local_server_id"),
PersistentDataKeyType.STRING,
"",
true
)
val serverProfileUUID = UUID(0, 0)
class EcoServerProfile(
handler: ProfileHandler
) : EcoProfile(serverProfileUUID, handler), ServerProfile {
override fun getServerID(): String {
if (this.read(serverIDKey).isBlank()) {
this.write(serverIDKey, UUID.randomUUID().toString())
}
return this.read(serverIDKey)
}
override fun getLocalServerID(): String {
if (this.read(localServerIDKey).isBlank()) {
this.write(localServerIDKey, UUID.randomUUID().toString())
}
return this.read(localServerIDKey)
}
override fun toString(): String {
return "EcoServerProfile"
}
}

View File

@@ -0,0 +1,37 @@
package com.willfp.eco.internal.spigot.data.storage
import com.willfp.eco.core.data.keys.PersistentDataKey
import java.util.UUID
abstract class DataHandler(
val type: HandlerType
) {
/**
* Read value from a key.
*/
abstract fun <T : Any> read(uuid: UUID, key: PersistentDataKey<T>): T?
/**
* Write value to a key.
*/
abstract fun <T : Any> write(uuid: UUID, key: PersistentDataKey<T>, value: T)
/**
* Save a set of keys for a given UUID.
*/
abstract fun saveKeysFor(uuid: UUID, keys: Map<PersistentDataKey<*>, Any>)
// Everything below this are methods that are only needed for certain implementations.
open fun save() {
}
open fun saveAsync() {
}
open fun initialize() {
}
}

View File

@@ -0,0 +1,7 @@
package com.willfp.eco.internal.spigot.data.storage
enum class HandlerType {
YAML,
MYSQL,
MONGO
}

View File

@@ -0,0 +1,134 @@
package com.willfp.eco.internal.spigot.data.storage
import com.mongodb.client.model.Filters
import com.mongodb.client.model.ReplaceOptions
import com.mongodb.client.model.UpdateOptions
import com.mongodb.client.model.Updates
import com.mongodb.kotlin.client.coroutine.MongoClient
import com.mongodb.kotlin.client.coroutine.MongoCollection
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.data.ProfileHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.bson.codecs.pojo.annotations.BsonId
import java.util.UUID
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.bukkit.Bukkit
@Suppress("UNCHECKED_CAST")
class MongoDataHandler(
plugin: EcoSpigotPlugin,
private val handler: ProfileHandler
) : DataHandler(HandlerType.MONGO) {
private val client: MongoClient
private val collection: MongoCollection<UUIDProfile>
private val scope = CoroutineScope(Dispatchers.IO)
init {
System.setProperty(
"org.litote.mongo.mapping.service",
"org.litote.kmongo.jackson.JacksonClassMappingTypeService"
)
val url = plugin.configYml.getString("mongodb.url")
client = MongoClient.create(url)
collection = client.getDatabase(plugin.configYml.getString("mongodb.database"))
.getCollection<UUIDProfile>("uuidprofile") // Compat with jackson mapping
}
override fun <T : Any> read(uuid: UUID, key: PersistentDataKey<T>): T? {
return runBlocking {
doRead(uuid, key)
}
}
override fun <T : Any> write(uuid: UUID, key: PersistentDataKey<T>, value: T) {
scope.launch {
doWrite(uuid, key, value)
}
}
override fun saveKeysFor(uuid: UUID, keys: Map<PersistentDataKey<*>, Any>) {
scope.launch {
for ((key, value) in keys) {
saveKey(uuid, key, value)
}
}
}
private suspend fun <T : Any> saveKey(uuid: UUID, key: PersistentDataKey<T>, value: Any) {
val data = value as T
doWrite(uuid, key, data)
}
private suspend fun <T> doWrite(uuid: UUID, key: PersistentDataKey<T>, value: T) {
val profile = getOrCreateDocument(uuid)
profile.data.run {
if (value == null) {
this.remove(key.key.toString())
} else {
this[key.key.toString()] = value
}
}
collection.updateOne(
Filters.eq(UUIDProfile::uuid.name, uuid.toString()),
Updates.set(UUIDProfile::data.name, profile.data)
)
}
private suspend fun <T> doRead(uuid: UUID, key: PersistentDataKey<T>): T? {
val profile = collection.find<UUIDProfile>(Filters.eq(UUIDProfile::uuid.name, uuid.toString()))
.firstOrNull() ?: return key.defaultValue
return profile.data[key.key.toString()] as? T?
}
private suspend fun getOrCreateDocument(uuid: UUID): UUIDProfile {
val profile = collection.find<UUIDProfile>(Filters.eq(UUIDProfile::uuid.name, uuid.toString()))
.firstOrNull()
return if (profile == null) {
val toInsert = UUIDProfile(
uuid.toString(),
mutableMapOf()
)
collection.replaceOne(
Filters.eq(UUIDProfile::uuid.name, uuid.toString()),
toInsert,
ReplaceOptions().upsert(true)
)
toInsert
} else {
profile
}
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
return other is MongoDataHandler
}
override fun hashCode(): Int {
return type.hashCode()
}
}
@Serializable
internal data class UUIDProfile(
// Storing UUID as strings for serialization
@SerialName("_id") val uuid: String,
// Storing NamespacedKeys as strings for serialization
val data: MutableMap<String, @Contextual Any>
)

View File

@@ -0,0 +1,169 @@
package com.willfp.eco.internal.spigot.data.storage
import com.github.benmanes.caffeine.cache.Caffeine
import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.willfp.eco.core.config.ConfigType
import com.willfp.eco.core.config.interfaces.Config
import com.willfp.eco.core.config.readConfig
import com.willfp.eco.core.data.keys.PersistentDataKey
import com.willfp.eco.core.data.keys.PersistentDataKeyType
import com.willfp.eco.internal.spigot.EcoSpigotPlugin
import com.willfp.eco.internal.spigot.data.ProfileHandler
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.TextColumnType
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.util.UUID
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/*
Better than old MySQL data handler, but that's only because it's literally just dumping all the
data into a single text column, containing the contents of the players profile as a Config.
Whatever. At least it works.
*/
@Suppress("UNCHECKED_CAST")
class MySQLDataHandler(
plugin: EcoSpigotPlugin,
private val handler: ProfileHandler
) : DataHandler(HandlerType.MYSQL) {
private val database: Database
private val table = UUIDTable("eco_data")
private val rows = Caffeine.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.build<UUID, ResultRow>()
private val threadFactory = ThreadFactoryBuilder().setNameFormat("eco-mysql-thread-%d").build()
private val executor = Executors.newFixedThreadPool(plugin.configYml.getInt("mysql.threads"), threadFactory)
private val dataColumn: Column<String>
get() = table.columns.first { it.name == "json_data" } as Column<String>
init {
val config = HikariConfig()
config.driverClassName = "com.mysql.cj.jdbc.Driver"
config.username = plugin.configYml.getString("mysql.user")
config.password = plugin.configYml.getString("mysql.password")
config.jdbcUrl = "jdbc:mysql://" +
"${plugin.configYml.getString("mysql.host")}:" +
"${plugin.configYml.getString("mysql.port")}/" +
plugin.configYml.getString("mysql.database")
config.maximumPoolSize = plugin.configYml.getInt("mysql.connections")
database = Database.connect(HikariDataSource(config))
transaction(database) {
SchemaUtils.create(table)
table.apply {
registerColumn<String>("json_data", TextColumnType())
}
SchemaUtils.createMissingTablesAndColumns(table, withLogs = false)
}
}
override fun <T : Any> read(uuid: UUID, key: PersistentDataKey<T>): T? {
val data = getData(uuid)
val value: Any? = when (key.type) {
PersistentDataKeyType.INT -> data.getIntOrNull(key.key.toString())
PersistentDataKeyType.DOUBLE -> data.getDoubleOrNull(key.key.toString())
PersistentDataKeyType.STRING -> data.getStringOrNull(key.key.toString())
PersistentDataKeyType.BOOLEAN -> data.getBoolOrNull(key.key.toString())
PersistentDataKeyType.STRING_LIST -> data.getStringsOrNull(key.key.toString())
PersistentDataKeyType.CONFIG -> data.getSubsectionOrNull(key.key.toString())
PersistentDataKeyType.BIG_DECIMAL -> data.getBigDecimalOrNull(key.key.toString())
else -> null
}
return value as? T?
}
override fun <T : Any> write(uuid: UUID, key: PersistentDataKey<T>, value: T) {
val data = getData(uuid)
data.set(key.key.toString(), value)
setData(uuid, data)
}
override fun saveKeysFor(uuid: UUID, keys: Map<PersistentDataKey<*>, Any>) {
executor.submit {
val data = getData(uuid)
for ((key, value) in keys) {
data.set(key.key.toString(), value)
}
doSetData(uuid, data)
}
}
private fun getData(uuid: UUID): Config {
val plaintext = transaction(database) {
val row = rows.get(uuid) {
val row = table.select { table.id eq uuid }.limit(1).singleOrNull()
if (row != null) {
row
} else {
transaction(database) {
table.insert {
it[id] = uuid
it[dataColumn] = "{}"
}
}
table.select { table.id eq uuid }.limit(1).singleOrNull()
}
}
row.getOrNull(dataColumn) ?: "{}"
}
return readConfig(plaintext, ConfigType.JSON)
}
private fun setData(uuid: UUID, config: Config) {
executor.submit {
doSetData(uuid, config)
}
}
private fun doSetData(uuid: UUID, config: Config) {
transaction(database) {
table.update({ table.id eq uuid }) {
it[dataColumn] = config.toPlaintext()
}
}
}
override fun initialize() {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(table, withLogs = false)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
return other is MySQLDataHandler
}
override fun hashCode(): Int {
return type.hashCode()
}
}

Some files were not shown because too many files have changed in this diff Show More