3.0
@@ -0,0 +1,196 @@
|
||||
package net.momirealms.customnameplates.backend.storage;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import net.momirealms.customnameplates.api.AbstractCNPlayer;
|
||||
import net.momirealms.customnameplates.api.CNPlayer;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.feature.JoinQuitListener;
|
||||
import net.momirealms.customnameplates.api.helper.GsonHelper;
|
||||
import net.momirealms.customnameplates.api.storage.DataStorageProvider;
|
||||
import net.momirealms.customnameplates.api.storage.StorageManager;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.JsonData;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.backend.storage.method.DummyStorage;
|
||||
import net.momirealms.customnameplates.backend.storage.method.database.nosql.MongoDBProvider;
|
||||
import net.momirealms.customnameplates.backend.storage.method.database.nosql.RedisManager;
|
||||
import net.momirealms.customnameplates.backend.storage.method.database.sql.H2Provider;
|
||||
import net.momirealms.customnameplates.backend.storage.method.database.sql.MariaDBProvider;
|
||||
import net.momirealms.customnameplates.backend.storage.method.database.sql.MySQLProvider;
|
||||
import net.momirealms.customnameplates.backend.storage.method.database.sql.SQLiteProvider;
|
||||
import net.momirealms.customnameplates.backend.storage.method.file.JsonProvider;
|
||||
import net.momirealms.customnameplates.backend.storage.method.file.YAMLProvider;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class StorageManagerImpl implements StorageManager, JoinQuitListener {
|
||||
|
||||
private final CustomNameplates plugin;
|
||||
private DataStorageProvider dataSource;
|
||||
private StorageType previousType;
|
||||
private boolean hasRedis;
|
||||
private RedisManager redisManager;
|
||||
private String serverID;
|
||||
|
||||
public StorageManagerImpl(final CustomNameplates plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@Override
|
||||
public void onPlayerJoin(CNPlayer player) {
|
||||
UUID uuid = player.uuid();
|
||||
Executor async = plugin.getScheduler().async();
|
||||
if (hasRedis) {
|
||||
this.redisManager.getPlayerData(uuid, async).thenAccept(playerData1 -> {
|
||||
if (playerData1.isPresent()) {
|
||||
PlayerData data = playerData1.get();
|
||||
handleDataLoad(player, data);
|
||||
((AbstractCNPlayer) player).setLoaded(true);
|
||||
this.redisManager.updatePlayerData(data, async).thenAccept(result -> {
|
||||
if (!result) {
|
||||
plugin.getPluginLogger().warn("Failed to refresh redis player data for " + player.name());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.getDataSource().getPlayerData(uuid, async).thenAccept(playerData2 -> {
|
||||
if (playerData2.isPresent()) {
|
||||
PlayerData data = playerData2.get();
|
||||
handleDataLoad(player, data);
|
||||
((AbstractCNPlayer) player).setLoaded(true);
|
||||
this.redisManager.updatePlayerData(data, async).thenAccept(result -> {
|
||||
if (!result) {
|
||||
plugin.getPluginLogger().warn("Failed to refresh redis player data for " + player.name());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
plugin.getPluginLogger().warn("Failed to load player data for " + player.name());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.getDataSource().getPlayerData(uuid, async).thenAccept(playerData -> {
|
||||
if (playerData.isPresent()) {
|
||||
PlayerData data = playerData.get();
|
||||
handleDataLoad(player, data);
|
||||
((AbstractCNPlayer) player).setLoaded(true);
|
||||
} else {
|
||||
plugin.getPluginLogger().warn("Failed to load player data for " + player.name());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDataLoad(CNPlayer player, PlayerData data) {
|
||||
player.equippedBubble(data.bubble());
|
||||
player.equippedNameplate(data.nameplate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerQuit(CNPlayer player) {
|
||||
((AbstractCNPlayer) player).setLoaded(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
YamlDocument config = plugin.getConfigManager().loadConfig("database.yml");
|
||||
this.serverID = config.getString("unique-server-id", "default");
|
||||
try {
|
||||
config.save(new File(plugin.getDataFolder(), "database.yml"));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Check if storage type has changed and reinitialize if necessary
|
||||
StorageType storageType = StorageType.valueOf(config.getString("data-storage-method", "H2"));
|
||||
if (storageType != previousType) {
|
||||
if (this.dataSource != null) this.dataSource.disable();
|
||||
this.previousType = storageType;
|
||||
switch (storageType) {
|
||||
case H2 -> this.dataSource = new H2Provider(plugin);
|
||||
case JSON -> this.dataSource = new JsonProvider(plugin);
|
||||
case YAML -> this.dataSource = new YAMLProvider(plugin);
|
||||
case SQLite -> this.dataSource = new SQLiteProvider(plugin);
|
||||
case MySQL -> this.dataSource = new MySQLProvider(plugin);
|
||||
case MariaDB -> this.dataSource = new MariaDBProvider(plugin);
|
||||
case MongoDB -> this.dataSource = new MongoDBProvider(plugin);
|
||||
case NONE -> this.dataSource = new DummyStorage(plugin);
|
||||
}
|
||||
if (this.dataSource != null) this.dataSource.initialize(config);
|
||||
else plugin.getPluginLogger().severe("No storage type is set.");
|
||||
}
|
||||
|
||||
// Handle Redis configuration
|
||||
if (!this.hasRedis && config.getBoolean("Redis.enable", false)) {
|
||||
this.redisManager = new RedisManager(plugin);
|
||||
this.redisManager.initialize(config);
|
||||
this.hasRedis = true;
|
||||
}
|
||||
|
||||
// Disable Redis if it was enabled but is now disabled
|
||||
if (this.hasRedis && !config.getBoolean("Redis.enable", false) && this.redisManager != null) {
|
||||
this.hasRedis = false;
|
||||
this.redisManager.disable();
|
||||
this.redisManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (this.dataSource != null)
|
||||
this.dataSource.disable();
|
||||
if (this.redisManager != null)
|
||||
this.redisManager.disable();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataStorageProvider getDataSource() {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRedisEnabled() {
|
||||
return hasRedis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull PlayerData data) {
|
||||
return toJson(data).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String toJson(@NotNull PlayerData data) {
|
||||
return GsonHelper.get().toJson(data.toGsonData());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PlayerData fromJson(UUID uuid, String json) {
|
||||
try {
|
||||
return GsonHelper.get().fromJson(json, JsonData.class).toPlayerData(uuid);
|
||||
} catch (JsonSyntaxException e) {
|
||||
plugin.getPluginLogger().severe("Failed to get PlayerData from json. Json: " + json);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PlayerData fromBytes(UUID uuid, byte[] data) {
|
||||
return fromJson(uuid, new String(data, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.DataStorageProvider;
|
||||
|
||||
public abstract class AbstractStorage implements DataStorageProvider {
|
||||
|
||||
protected CustomNameplates plugin;
|
||||
|
||||
public AbstractStorage(CustomNameplates plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(YamlDocument config) {
|
||||
// This method can be overridden in subclasses to perform initialization tasks specific to the storage type.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// This method can be overridden in subclasses to perform cleanup or shutdown tasks specific to the storage type.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method;
|
||||
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class DummyStorage extends AbstractStorage {
|
||||
|
||||
public DummyStorage(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
return CompletableFuture.completedFuture(Optional.of(PlayerData.empty(uuid)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
return CompletableFuture.completedFuture(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.nosql;
|
||||
|
||||
import com.mongodb.*;
|
||||
import com.mongodb.client.*;
|
||||
import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.Projections;
|
||||
import com.mongodb.client.model.UpdateOptions;
|
||||
import com.mongodb.client.model.Updates;
|
||||
import com.mongodb.client.result.UpdateResult;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import dev.dejvokep.boostedyaml.block.implementation.Section;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.backend.storage.method.AbstractStorage;
|
||||
import org.bson.Document;
|
||||
import org.bson.UuidRepresentation;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.types.Binary;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class MongoDBProvider extends AbstractStorage {
|
||||
|
||||
private MongoClient mongoClient;
|
||||
private MongoDatabase database;
|
||||
private String collectionPrefix;
|
||||
|
||||
public MongoDBProvider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(YamlDocument config) {
|
||||
Section section = config.getSection("MongoDB");
|
||||
if (section == null) {
|
||||
plugin.getPluginLogger().warn("Failed to load database config. It seems that your config is broken. Please regenerate a new one.");
|
||||
return;
|
||||
}
|
||||
|
||||
collectionPrefix = section.getString("collection-prefix", "nameplates");
|
||||
var settings = MongoClientSettings.builder().uuidRepresentation(UuidRepresentation.STANDARD);
|
||||
if (!section.getString("connection-uri", "").equals("")) {
|
||||
settings.applyConnectionString(new ConnectionString(section.getString("connection-uri", "")));
|
||||
this.mongoClient = MongoClients.create(settings.build());
|
||||
this.database = mongoClient.getDatabase(section.getString("database", "minecraft"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.contains("user")) {
|
||||
MongoCredential credential = MongoCredential.createCredential(
|
||||
section.getString("user", "root"),
|
||||
section.getString("database", "minecraft"),
|
||||
section.getString("password", "password").toCharArray()
|
||||
);
|
||||
settings.credential(credential);
|
||||
}
|
||||
|
||||
settings.applyToClusterSettings(builder -> builder.hosts(Collections.singletonList(new ServerAddress(
|
||||
section.getString("host", "localhost"),
|
||||
section.getInt("port", 27017)
|
||||
))));
|
||||
this.mongoClient = MongoClients.create(settings.build());
|
||||
this.database = mongoClient.getDatabase(section.getString("database", "minecraft"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (this.mongoClient != null) {
|
||||
this.mongoClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection name for a specific subcategory of data.
|
||||
*
|
||||
* @param value The subcategory identifier.
|
||||
* @return The full collection name including the prefix.
|
||||
*/
|
||||
public String getCollectionName(String value) {
|
||||
return getCollectionPrefix() + "_" + value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection prefix used for MongoDB collections.
|
||||
*
|
||||
* @return The collection prefix.
|
||||
*/
|
||||
public String getCollectionPrefix() {
|
||||
return collectionPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.MongoDB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
CompletableFuture<Optional<PlayerData>> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
|
||||
Document doc = collection.find(Filters.eq("uuid", uuid)).first();
|
||||
if (doc == null) {
|
||||
future.complete(Optional.empty());
|
||||
} else if (plugin.getPlayer(uuid) != null) {
|
||||
Binary binary = (Binary) doc.get("data");
|
||||
future.complete(Optional.of(plugin.getStorageManager().fromBytes(uuid, binary.getData())));
|
||||
} else {
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
|
||||
try {
|
||||
Document query = new Document("uuid", playerData.uuid());
|
||||
Bson updates = Updates.combine(Updates.set("data", new Binary(plugin.getStorageManager().toBytes(playerData))));
|
||||
UpdateOptions options = new UpdateOptions().upsert(true);
|
||||
UpdateResult result = collection.updateOne(query, updates, options);
|
||||
future.complete(result.wasAcknowledged());
|
||||
} catch (MongoException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() {
|
||||
Set<UUID> uuids = new HashSet<>();
|
||||
MongoCollection<Document> collection = database.getCollection(getCollectionName("data"));
|
||||
try {
|
||||
Bson projectionFields = Projections.fields(Projections.include("uuid"));
|
||||
try (MongoCursor<Document> cursor = collection.find().projection(projectionFields).iterator()) {
|
||||
while (cursor.hasNext()) {
|
||||
uuids.add(cursor.next().get("uuid", UUID.class));
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.getPluginLogger().warn("Failed to get unique data.", e);
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.nosql;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import dev.dejvokep.boostedyaml.block.implementation.Section;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.backend.storage.method.AbstractStorage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class RedisManager extends AbstractStorage {
|
||||
|
||||
private static RedisManager instance;
|
||||
private final static String STREAM = "customnameplate";
|
||||
private JedisPool jedisPool;
|
||||
private String password;
|
||||
private int port;
|
||||
private String host;
|
||||
private boolean useSSL;
|
||||
|
||||
public RedisManager(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance of the RedisManager.
|
||||
*
|
||||
* @return The RedisManager instance.
|
||||
*/
|
||||
public static RedisManager getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Jedis resource for interacting with the Redis server.
|
||||
*
|
||||
* @return A Jedis resource.
|
||||
*/
|
||||
public Jedis getJedis() {
|
||||
return jedisPool.getResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Redis connection and configuration based on the plugin's YAML configuration.
|
||||
*/
|
||||
@Override
|
||||
public void initialize(YamlDocument config) {
|
||||
Section section = config.getSection("Redis");
|
||||
if (section == null) {
|
||||
plugin.getPluginLogger().warn("Failed to load database config. It seems that your config is broken. Please regenerate a new one.");
|
||||
return;
|
||||
}
|
||||
|
||||
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
|
||||
jedisPoolConfig.setTestWhileIdle(true);
|
||||
jedisPoolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(30000));
|
||||
jedisPoolConfig.setNumTestsPerEvictionRun(-1);
|
||||
jedisPoolConfig.setMinEvictableIdleDuration(Duration.ofMillis(section.getInt("MinEvictableIdleTimeMillis", 1800000)));
|
||||
jedisPoolConfig.setMaxTotal(section.getInt("MaxTotal",8));
|
||||
jedisPoolConfig.setMaxIdle(section.getInt("MaxIdle",8));
|
||||
jedisPoolConfig.setMinIdle(section.getInt("MinIdle",1));
|
||||
jedisPoolConfig.setMaxWait(Duration.ofMillis(section.getInt("MaxWaitMillis")));
|
||||
|
||||
password = section.getString("password", "");
|
||||
port = section.getInt("port", 6379);
|
||||
host = section.getString("host", "localhost");
|
||||
useSSL = section.getBoolean("use-ssl", false);
|
||||
|
||||
if (password.isBlank()) {
|
||||
jedisPool = new JedisPool(jedisPoolConfig, host, port, 0, useSSL);
|
||||
} else {
|
||||
jedisPool = new JedisPool(jedisPoolConfig, host, port, 0, password, useSSL);
|
||||
}
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.info();
|
||||
plugin.getPluginLogger().info("Redis server connected.");
|
||||
} catch (JedisException e) {
|
||||
plugin.getPluginLogger().warn("Failed to connect redis.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the Redis connection by closing the JedisPool.
|
||||
*/
|
||||
@Override
|
||||
public void disable() {
|
||||
if (jedisPool != null && !jedisPool.isClosed())
|
||||
jedisPool.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.Redis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
CompletableFuture<Optional<PlayerData>> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try (Jedis jedis = getJedis()) {
|
||||
byte[] key = getRedisKey("cn_data", uuid);
|
||||
byte[] data = jedis.get(key);
|
||||
if (data != null) {
|
||||
future.complete(Optional.of(plugin.getStorageManager().fromBytes(uuid, data)));
|
||||
plugin.debug(() -> "Redis data retrieved for " + uuid + "; normal data");
|
||||
} else {
|
||||
future.complete(Optional.empty());
|
||||
plugin.debug(() -> "Redis data retrieved for " + uuid + "; empty data");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getPluginLogger().warn("Failed to get redis data for " + uuid, e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try (Jedis jedis = getJedis()) {
|
||||
jedis.setex(
|
||||
getRedisKey("cn_data", playerData.uuid()),
|
||||
1200,
|
||||
plugin.getStorageManager().toBytes(playerData)
|
||||
);
|
||||
plugin.debug(() -> "Redis data set for " + playerData.uuid());
|
||||
future.complete(true);
|
||||
} catch (Exception e) {
|
||||
plugin.getPluginLogger().warn("Failed to set redis data for player " + playerData.uuid(), e);
|
||||
future.complete(false);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() {
|
||||
return new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Redis key for a specified key and UUID.
|
||||
*
|
||||
* @param key The key identifier.
|
||||
* @param uuid The UUID to include in the key.
|
||||
* @return A byte array representing the Redis key.
|
||||
*/
|
||||
private byte[] getRedisKey(String key, @NotNull UUID uuid) {
|
||||
return (key + ":" + uuid).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.sql;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import dev.dejvokep.boostedyaml.block.implementation.Section;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
public abstract class AbstractHikariDatabase extends AbstractSQLDatabase {
|
||||
|
||||
private HikariDataSource dataSource;
|
||||
private final String driverClass;
|
||||
private final String sqlBrand;
|
||||
|
||||
public AbstractHikariDatabase(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
this.driverClass = getStorageType() == StorageType.MariaDB ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
|
||||
this.sqlBrand = getStorageType() == StorageType.MariaDB ? "MariaDB" : "MySQL";
|
||||
try {
|
||||
Class.forName(this.driverClass);
|
||||
} catch (ClassNotFoundException e1) {
|
||||
if (getStorageType() == StorageType.MariaDB) {
|
||||
plugin.getPluginLogger().warn("No MariaDB driver is found");
|
||||
} else if (getStorageType() == StorageType.MySQL) {
|
||||
try {
|
||||
Class.forName("com.mysql.jdbc.Driver");
|
||||
} catch (ClassNotFoundException e2) {
|
||||
plugin.getPluginLogger().warn("No MySQL driver is found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(YamlDocument config) {
|
||||
Section section = config.getSection(sqlBrand);
|
||||
|
||||
if (section == null) {
|
||||
plugin.getPluginLogger().warn("Failed to load database config. It seems that your config is broken. Please regenerate a new one.");
|
||||
return;
|
||||
}
|
||||
|
||||
super.tablePrefix = section.getString("table-prefix", "nameplates");
|
||||
HikariConfig hikariConfig = new HikariConfig();
|
||||
hikariConfig.setUsername(section.getString("user", "root"));
|
||||
hikariConfig.setPassword(section.getString("password", "pa55w0rd"));
|
||||
hikariConfig.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
|
||||
sqlBrand.toLowerCase(Locale.ENGLISH),
|
||||
section.getString("host", "localhost"),
|
||||
section.getString("port", "3306"),
|
||||
section.getString("database", "minecraft"),
|
||||
section.getString("connection-parameters")
|
||||
));
|
||||
hikariConfig.setDriverClassName(driverClass);
|
||||
hikariConfig.setMaximumPoolSize(section.getInt("Pool-Settings.max-pool-size", 10));
|
||||
hikariConfig.setMinimumIdle(section.getInt("Pool-Settings.min-idle", 10));
|
||||
hikariConfig.setMaxLifetime(section.getLong("Pool-Settings.max-lifetime", 180000L));
|
||||
hikariConfig.setConnectionTimeout(section.getLong("Pool-Settings.time-out", 20000L));
|
||||
hikariConfig.setPoolName("CustomFishingHikariPool");
|
||||
try {
|
||||
hikariConfig.setKeepaliveTime(section.getLong("Pool-Settings.keep-alive-time", 60000L));
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
}
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.putAll(
|
||||
Map.of("cachePrepStmts", "true",
|
||||
"prepStmtCacheSize", "250",
|
||||
"prepStmtCacheSqlLimit", "2048",
|
||||
"useServerPrepStmts", "true",
|
||||
"useLocalSessionState", "true",
|
||||
"useLocalTransactionState", "true"
|
||||
));
|
||||
properties.putAll(
|
||||
Map.of(
|
||||
"rewriteBatchedStatements", "true",
|
||||
"cacheResultSetMetadata", "true",
|
||||
"cacheServerConfiguration", "true",
|
||||
"elideSetAutoCommits", "true",
|
||||
"maintainTimeStats", "false")
|
||||
);
|
||||
hikariConfig.setDataSourceProperties(properties);
|
||||
dataSource = new HikariDataSource(hikariConfig);
|
||||
super.createTableIfNotExist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (dataSource != null && !dataSource.isClosed())
|
||||
dataSource.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.sql;
|
||||
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.backend.storage.method.AbstractStorage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* An abstract base class for SQL database implementations that handle player data storage.
|
||||
*/
|
||||
public abstract class AbstractSQLDatabase extends AbstractStorage {
|
||||
|
||||
protected String tablePrefix;
|
||||
|
||||
public AbstractSQLDatabase(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connection to the SQL database.
|
||||
*
|
||||
* @return A database connection.
|
||||
* @throws SQLException If there is an error establishing a connection.
|
||||
*/
|
||||
public abstract Connection getConnection() throws SQLException;
|
||||
|
||||
/**
|
||||
* Create tables for storing data if they don't exist in the database.
|
||||
*/
|
||||
public void createTableIfNotExist() {
|
||||
try (Connection connection = getConnection()) {
|
||||
final String[] databaseSchema = getSchema(getStorageType().name().toLowerCase(Locale.ENGLISH));
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to create tables", e);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to get sql connection", e);
|
||||
} catch (IOException e) {
|
||||
plugin.getPluginLogger().warn("Failed to get schema resource", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL schema from a resource file.
|
||||
*
|
||||
* @param fileName The name of the schema file.
|
||||
* @return An array of SQL statements to create tables.
|
||||
* @throws IOException If there is an error reading the schema resource.
|
||||
*/
|
||||
private String[] getSchema(@NotNull String fileName) throws IOException {
|
||||
return replaceSchemaPlaceholder(new String(Objects.requireNonNull(plugin.getResourceStream("schema/" + fileName + ".sql"))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholder values in SQL schema with the table prefix.
|
||||
*
|
||||
* @param sql The SQL schema string.
|
||||
* @return The SQL schema string with placeholders replaced.
|
||||
*/
|
||||
private String replaceSchemaPlaceholder(@NotNull String sql) {
|
||||
return sql.replace("{prefix}", tablePrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of a database table based on a sub-table name and the table prefix.
|
||||
*
|
||||
* @param sub The sub-table name.
|
||||
* @return The full table name.
|
||||
*/
|
||||
public String getTableName(String sub) {
|
||||
return getTablePrefix() + "_" + sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current table prefix.
|
||||
*
|
||||
* @return The table prefix.
|
||||
*/
|
||||
public String getTablePrefix() {
|
||||
return tablePrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
CompletableFuture<Optional<PlayerData>> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try (
|
||||
Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("data")))
|
||||
) {
|
||||
statement.setString(1, uuid.toString());
|
||||
ResultSet rs = statement.executeQuery();
|
||||
if (rs.next()) {
|
||||
Blob blob = rs.getBlob("data");
|
||||
byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
future.complete(Optional.of(plugin.getStorageManager().fromBytes(uuid, dataByteArray)));
|
||||
} else if (plugin.getPlayer(uuid) != null) {
|
||||
var data = PlayerData.empty(uuid);
|
||||
this.insertPlayerData(uuid, data);
|
||||
future.complete(Optional.of(data));
|
||||
} else {
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to get " + uuid + "'s data.", e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
public void insertPlayerData(UUID uuid, PlayerData playerData) {
|
||||
try (
|
||||
Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_INSERT_DATA_BY_UUID, getTableName("data")))
|
||||
) {
|
||||
statement.setString(1, uuid.toString());
|
||||
statement.setBlob(2, new ByteArrayInputStream(plugin.getStorageManager().toBytes(playerData)));
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to insert " + uuid + "'s data.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try (
|
||||
Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data")))
|
||||
) {
|
||||
statement.setBlob(1, new ByteArrayInputStream(plugin.getStorageManager().toBytes(playerData)));
|
||||
statement.setString(2, playerData.uuid().toString());
|
||||
statement.executeUpdate();
|
||||
future.complete(true);
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to update " + playerData.uuid() + "'s data.", e);
|
||||
future.complete(false);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() {
|
||||
Set<UUID> uuids = new HashSet<>();
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_ALL_UUID, getTableName("data")))) {
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
UUID uuid = UUID.fromString(rs.getString("uuid"));
|
||||
uuids.add(uuid);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to get unique data.", e);
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
public static class SqlConstants {
|
||||
public static final String SQL_SELECT_BY_UUID = "SELECT * FROM `%s` WHERE `uuid` = ?";
|
||||
public static final String SQL_SELECT_ALL_UUID = "SELECT uuid FROM `%s`";
|
||||
public static final String SQL_UPDATE_BY_UUID = "UPDATE `%s` SET `data` = ? WHERE `uuid` = ?";
|
||||
public static final String SQL_INSERT_DATA_BY_UUID = "INSERT INTO `%s`(`uuid`, `data`) VALUES(?, ?)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.sql;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.common.dependency.Dependency;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.Connection;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* An implementation of AbstractSQLDatabase that uses the H2 embedded database for player data storage.
|
||||
*/
|
||||
public class H2Provider extends AbstractSQLDatabase {
|
||||
|
||||
private Object connectionPool;
|
||||
private Method disposeMethod;
|
||||
private Method getConnectionMethod;
|
||||
|
||||
public H2Provider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the H2 database and connection pool based on the configuration.
|
||||
*/
|
||||
@Override
|
||||
public void initialize(YamlDocument config) {
|
||||
File databaseFile = new File(plugin.getDataFolder(), config.getString("H2.file", "data.db"));
|
||||
super.tablePrefix = config.getString("H2.table-prefix", "nameplates");
|
||||
|
||||
final String url = String.format("jdbc:h2:%s", databaseFile.getAbsolutePath());
|
||||
ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER));
|
||||
try {
|
||||
Class<?> connectionClass = classLoader.loadClass("org.h2.jdbcx.JdbcConnectionPool");
|
||||
Method createPoolMethod = connectionClass.getMethod("create", String.class, String.class, String.class);
|
||||
this.connectionPool = createPoolMethod.invoke(null, url, "sa", "");
|
||||
this.disposeMethod = connectionClass.getMethod("dispose");
|
||||
this.getConnectionMethod = connectionClass.getMethod("getConnection");
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
super.createTableIfNotExist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (connectionPool != null) {
|
||||
try {
|
||||
disposeMethod.invoke(connectionPool);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.H2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() {
|
||||
try {
|
||||
return (Connection) getConnectionMethod.invoke(connectionPool);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.sql;
|
||||
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
|
||||
public class MariaDBProvider extends AbstractHikariDatabase {
|
||||
|
||||
public MariaDBProvider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.MariaDB;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.sql;
|
||||
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
|
||||
public class MySQLProvider extends AbstractHikariDatabase {
|
||||
|
||||
public MySQLProvider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.MySQL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.database.sql;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.common.dependency.Dependency;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class SQLiteProvider extends AbstractSQLDatabase {
|
||||
|
||||
private Connection connection;
|
||||
private File databaseFile;
|
||||
private Constructor<?> connectionConstructor;
|
||||
private ExecutorService executor;
|
||||
|
||||
public SQLiteProvider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(YamlDocument config) {
|
||||
ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER, Dependency.SLF4J_SIMPLE, Dependency.SLF4J_API));
|
||||
try {
|
||||
Class<?> connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection");
|
||||
connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
this.executor = Executors.newFixedThreadPool(1);
|
||||
this.databaseFile = new File(plugin.getDataFolder(), config.getString("SQLite.file", "data") + ".db");
|
||||
super.tablePrefix = config.getString("SQLite.table-prefix", "customfishing");
|
||||
super.createTableIfNotExist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
}
|
||||
try {
|
||||
if (connection != null && !connection.isClosed())
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.SQLite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connection to the SQLite database.
|
||||
*
|
||||
* @return A database connection.
|
||||
* @throws SQLException If there is an error establishing a connection.
|
||||
*/
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
return connection;
|
||||
}
|
||||
try {
|
||||
var properties = new Properties();
|
||||
properties.setProperty("foreign_keys", Boolean.toString(true));
|
||||
properties.setProperty("encoding", "'UTF-8'");
|
||||
properties.setProperty("synchronous", "FULL");
|
||||
connection = (Connection) this.connectionConstructor.newInstance("jdbc:sqlite:" + databaseFile.toString(), databaseFile.toString(), properties);
|
||||
return connection;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
if (e.getCause() instanceof SQLException) {
|
||||
throw (SQLException) e.getCause();
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
CompletableFuture<Optional<PlayerData>> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try (
|
||||
Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_SELECT_BY_UUID, getTableName("data")))
|
||||
) {
|
||||
statement.setString(1, uuid.toString());
|
||||
ResultSet rs = statement.executeQuery();
|
||||
if (rs.next()) {
|
||||
byte[] dataByteArray = rs.getBytes("data");
|
||||
future.complete(Optional.of(plugin.getStorageManager().fromBytes(uuid, dataByteArray)));
|
||||
} else if (plugin.getPlayer(uuid) != null) {
|
||||
var data = PlayerData.empty(uuid);
|
||||
this.insertPlayerData(uuid, data);
|
||||
future.complete(Optional.of(data));
|
||||
} else {
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to get " + uuid + "'s data.", e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
public void insertPlayerData(UUID uuid, PlayerData playerData) {
|
||||
try (
|
||||
Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_INSERT_DATA_BY_UUID, getTableName("data")))
|
||||
) {
|
||||
statement.setString(1, uuid.toString());
|
||||
statement.setBytes(2, plugin.getStorageManager().toBytes(playerData));
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to insert " + uuid + "'s data.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try (
|
||||
Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(String.format(SqlConstants.SQL_UPDATE_BY_UUID, getTableName("data")))
|
||||
) {
|
||||
statement.setBytes(1, plugin.getStorageManager().toBytes(playerData));
|
||||
statement.setString(2, playerData.uuid().toString());
|
||||
statement.executeUpdate();
|
||||
future.complete(true);
|
||||
} catch (SQLException e) {
|
||||
plugin.getPluginLogger().warn("Failed to update " + playerData.uuid() + "'s data.", e);
|
||||
future.complete(false);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.file;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.JsonData;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.backend.storage.method.AbstractStorage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A data storage implementation that uses JSON files to store player data.
|
||||
*/
|
||||
public class JsonProvider extends AbstractStorage {
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public JsonProvider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
File folder = new File(plugin.getDataFolder(), "data");
|
||||
if (!folder.exists()) folder.mkdirs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.JSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
CompletableFuture<Optional<PlayerData>> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
File file = getPlayerDataFile(uuid);
|
||||
PlayerData playerData;
|
||||
if (file.exists()) {
|
||||
playerData = readFromJsonFile(file, JsonData.class).toPlayerData(uuid);
|
||||
} else if (plugin.getPlayer(uuid) != null) {
|
||||
playerData = PlayerData.empty(uuid);
|
||||
} else {
|
||||
playerData = null;
|
||||
}
|
||||
future.complete(Optional.ofNullable(playerData));
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
this.saveToJsonFile(playerData.toGsonData(), getPlayerDataFile(playerData.uuid()));
|
||||
} catch (Exception e) {
|
||||
future.complete(false);
|
||||
}
|
||||
future.complete(true);
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file associated with a player's UUID for storing JSON data.
|
||||
*
|
||||
* @param uuid The UUID of the player.
|
||||
* @return The file for the player's data.
|
||||
*/
|
||||
public File getPlayerDataFile(UUID uuid) {
|
||||
return new File(plugin.getDataFolder(), "data" + File.separator + uuid + ".json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an object to a JSON file.
|
||||
*
|
||||
* @param obj The object to be saved as JSON.
|
||||
* @param filepath The file path where the JSON file should be saved.
|
||||
*/
|
||||
public void saveToJsonFile(Object obj, File filepath) {
|
||||
Gson gson = new Gson();
|
||||
try (FileWriter file = new FileWriter(filepath)) {
|
||||
gson.toJson(obj, file);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON content from a file and parse it into an object of the specified class.
|
||||
*
|
||||
* @param file The JSON file to read.
|
||||
* @param classOfT The class of the object to parse the JSON into.
|
||||
* @param <T> The type of the object.
|
||||
* @return The parsed object.
|
||||
*/
|
||||
public <T> T readFromJsonFile(File file, Class<T> classOfT) {
|
||||
Gson gson = new Gson();
|
||||
String jsonContent = new String(readFileToByteArray(file), StandardCharsets.UTF_8);
|
||||
return gson.fromJson(jsonContent, classOfT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a file and return them as a byte array.
|
||||
*
|
||||
* @param file The file to read.
|
||||
* @return The byte array representing the file's content.
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public byte[] readFileToByteArray(File file) {
|
||||
byte[] fileBytes = new byte[(int) file.length()];
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
fis.read(fileBytes);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return fileBytes;
|
||||
}
|
||||
|
||||
// Retrieve a set of unique user UUIDs based on JSON data files in the 'data' folder.
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() {
|
||||
// No legacy files
|
||||
File folder = new File(plugin.getDataFolder(), "data");
|
||||
Set<UUID> uuids = new HashSet<>();
|
||||
if (folder.exists()) {
|
||||
File[] files = folder.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
uuids.add(UUID.fromString(file.getName().substring(file.getName().length() - 5)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.momirealms.customnameplates.backend.storage.method.file;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import net.momirealms.customnameplates.api.CustomNameplates;
|
||||
import net.momirealms.customnameplates.api.storage.StorageType;
|
||||
import net.momirealms.customnameplates.api.storage.data.PlayerData;
|
||||
import net.momirealms.customnameplates.backend.storage.method.AbstractStorage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class YAMLProvider extends AbstractStorage {
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public YAMLProvider(CustomNameplates plugin) {
|
||||
super(plugin);
|
||||
File folder = new File(plugin.getDataFolder(), "data");
|
||||
if (!folder.exists()) folder.mkdirs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageType getStorageType() {
|
||||
return StorageType.YAML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file associated with a player's UUID for storing YAML data.
|
||||
*
|
||||
* @param uuid The UUID of the player.
|
||||
* @return The file for the player's data.
|
||||
*/
|
||||
public File getPlayerDataFile(UUID uuid) {
|
||||
return new File(plugin.getDataFolder(), "data" + File.separator + uuid + ".yml");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<PlayerData>> getPlayerData(UUID uuid, Executor executor) {
|
||||
CompletableFuture<Optional<PlayerData>> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
File dataFile = getPlayerDataFile(uuid);
|
||||
if (!dataFile.exists()) {
|
||||
if (plugin.getPlayer(uuid) != null) {
|
||||
future.complete(Optional.of(PlayerData.empty(uuid)));
|
||||
} else {
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
return;
|
||||
}
|
||||
YamlDocument data = plugin.getConfigManager().loadData(dataFile);
|
||||
PlayerData playerData = PlayerData.builder()
|
||||
.uuid(uuid)
|
||||
.nameplate(data.getString("nameplate", "none"))
|
||||
.bubble(data.getString("bubble", "none"))
|
||||
.build();
|
||||
future.complete(Optional.of(playerData));
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@Override
|
||||
public CompletableFuture<Boolean> updatePlayerData(PlayerData playerData, Executor executor) {
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
if (executor == null) executor = plugin.getScheduler().async();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
File dataFile = getPlayerDataFile(playerData.uuid());
|
||||
if (!dataFile.exists()) {
|
||||
dataFile.getParentFile().mkdirs();
|
||||
dataFile.createNewFile();
|
||||
}
|
||||
YamlDocument data = plugin.getConfigManager().loadData(dataFile);
|
||||
data.set("bubble", playerData.bubble());
|
||||
data.set("nameplate", playerData.nameplate());
|
||||
data.save(dataFile);
|
||||
future.complete(true);
|
||||
} catch (IOException e) {
|
||||
future.complete(false);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> getUniqueUsers() {
|
||||
File folder = new File(plugin.getDataFolder(), "data");
|
||||
Set<UUID> uuids = new HashSet<>();
|
||||
if (folder.exists()) {
|
||||
File[] files = folder.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
uuids.add(UUID.fromString(file.getName().substring(0, file.getName().length() - 4)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#version 150
|
||||
#moj_import<fog.glsl>
|
||||
|
||||
uniform sampler2D Sampler0;
|
||||
uniform vec4 ColorModulator;
|
||||
uniform float FogStart,FogEnd;
|
||||
uniform vec4 FogColor;
|
||||
uniform float GameTime;
|
||||
|
||||
in float vertexDistance;
|
||||
in vec4 vertexColor;
|
||||
in vec2 texCoord0;
|
||||
in float depthLevel;
|
||||
%SHADER_0%
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec4 texColor = texture(Sampler0, texCoord0);
|
||||
vec4 color = texColor * vertexColor * ColorModulator;%SHADER_1%
|
||||
if (color.a < 0.1) {
|
||||
discard;
|
||||
}
|
||||
if (texColor.a == 254.0/255.0) {
|
||||
if (depthLevel == 0.00) {
|
||||
discard;
|
||||
} else {
|
||||
color = vec4(texColor.rgb, 1.0) * vertexColor * ColorModulator;
|
||||
}
|
||||
}
|
||||
fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"blend": {
|
||||
"func": "add",
|
||||
"srcrgb": "srcalpha",
|
||||
"dstrgb": "1-srcalpha"
|
||||
},
|
||||
"vertex": "rendertype_text",
|
||||
"fragment": "rendertype_text",
|
||||
"attributes": [
|
||||
"Position",
|
||||
"Color",
|
||||
"UV0",
|
||||
"UV2"
|
||||
],
|
||||
"samplers": [
|
||||
{ "name": "Sampler0" },
|
||||
{ "name": "Sampler2" }
|
||||
],
|
||||
"uniforms": [
|
||||
{ "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
|
||||
{ "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
|
||||
{ "name": "IViewRotMat", "type": "matrix3x3", "count": 9, "values": [ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ] },
|
||||
{ "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
|
||||
{ "name": "FogStart", "type": "float", "count": 1, "values": [ 0.0 ] },
|
||||
{ "name": "FogEnd", "type": "float", "count": 1, "values": [ 1.0 ] },
|
||||
{ "name": "GameTime", "type": "float", "count": 1, "values": [ 1.0 ] },
|
||||
{ "name": "FogColor", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#version 150
|
||||
#moj_import <fog.glsl>
|
||||
|
||||
in vec3 Position;
|
||||
in vec4 Color;
|
||||
in vec2 UV0;
|
||||
in ivec2 UV2;
|
||||
|
||||
uniform sampler2D Sampler2;
|
||||
uniform mat4 ModelViewMat;
|
||||
uniform mat4 ProjMat;
|
||||
uniform mat3 IViewRotMat;
|
||||
uniform float GameTime;
|
||||
uniform int FogShape;
|
||||
uniform vec2 ScreenSize;
|
||||
|
||||
out float vertexDistance;
|
||||
out vec4 vertexColor;
|
||||
out vec2 texCoord0;
|
||||
out float depthLevel;
|
||||
%SHADER_0%
|
||||
void main() {
|
||||
vec4 vertex = vec4(Position, 1.0);
|
||||
vertexDistance = fog_distance(ModelViewMat, IViewRotMat * Position, FogShape);
|
||||
depthLevel = Position.z;
|
||||
texCoord0 = UV0;
|
||||
%SHADER_1%%SHADER_2%%SHADER_3%
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 144 B |
@@ -0,0 +1,31 @@
|
||||
#version 150
|
||||
#moj_import<fog.glsl>
|
||||
|
||||
uniform sampler2D Sampler0;
|
||||
uniform vec4 ColorModulator;
|
||||
uniform float FogStart,FogEnd;
|
||||
uniform vec4 FogColor;
|
||||
uniform float GameTime;
|
||||
|
||||
in float vertexDistance;
|
||||
in vec4 vertexColor;
|
||||
in vec2 texCoord0;
|
||||
in float depthLevel;
|
||||
%SHADER_0%
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec4 texColor = texture(Sampler0, texCoord0);
|
||||
vec4 color = texColor * vertexColor * ColorModulator;%SHADER_1%
|
||||
if (color.a < 0.1) {
|
||||
discard;
|
||||
}
|
||||
if (texColor.a == 254.0/255.0) {
|
||||
if (depthLevel == 1000.00) {
|
||||
discard;
|
||||
} else {
|
||||
color = vec4(texColor.rgb, 1.0) * vertexColor * ColorModulator;
|
||||
}
|
||||
}
|
||||
fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"blend": {
|
||||
"func": "add",
|
||||
"srcrgb": "srcalpha",
|
||||
"dstrgb": "1-srcalpha"
|
||||
},
|
||||
"vertex": "rendertype_text",
|
||||
"fragment": "rendertype_text",
|
||||
"attributes": [
|
||||
"Position",
|
||||
"Color",
|
||||
"UV0",
|
||||
"UV2"
|
||||
],
|
||||
"samplers": [
|
||||
{ "name": "Sampler0" },
|
||||
{ "name": "Sampler2" }
|
||||
],
|
||||
"uniforms": [
|
||||
{ "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
|
||||
{ "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
|
||||
{ "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
|
||||
{ "name": "FogStart", "type": "float", "count": 1, "values": [ 0.0 ] },
|
||||
{ "name": "FogEnd", "type": "float", "count": 1, "values": [ 1.0 ] },
|
||||
{ "name": "GameTime", "type": "float", "count": 1, "values": [ 1.0 ] },
|
||||
{ "name": "FogColor", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#version 150
|
||||
#moj_import <fog.glsl>
|
||||
|
||||
in vec3 Position;
|
||||
in vec4 Color;
|
||||
in vec2 UV0;
|
||||
in ivec2 UV2;
|
||||
|
||||
uniform sampler2D Sampler2;
|
||||
uniform mat4 ModelViewMat;
|
||||
uniform mat4 ProjMat;
|
||||
uniform float GameTime;
|
||||
uniform int FogShape;
|
||||
uniform vec2 ScreenSize;
|
||||
|
||||
out float vertexDistance;
|
||||
out vec4 vertexColor;
|
||||
out vec2 texCoord0;
|
||||
out float depthLevel;
|
||||
%SHADER_0%
|
||||
void main() {
|
||||
vec4 vertex = vec4(Position, 1.0);
|
||||
vertexDistance = fog_distance(Position, FogShape);
|
||||
depthLevel = Position.z;
|
||||
texCoord0 = UV0;
|
||||
%SHADER_1%%SHADER_2%%SHADER_3%
|
||||
}
|
||||
21
backend/src/main/resources/ResourcePack/pack.mcmeta
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"pack":{
|
||||
"pack_format": 8,
|
||||
"description":"CustomNameplates",
|
||||
"supported_formats": {
|
||||
"min_inclusive": 8,
|
||||
"max_inclusive": 34
|
||||
}
|
||||
},
|
||||
"overlays": {
|
||||
"entries": [
|
||||
{
|
||||
"formats": {
|
||||
"min_inclusive": 32,
|
||||
"max_inclusive": 34
|
||||
},
|
||||
"directory": "overlay_1_20_5"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
backend/src/main/resources/ResourcePack/pack.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
10
backend/src/main/resources/ResourcePack/pack_1_20_5.mcmeta
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"pack":{
|
||||
"pack_format": 32,
|
||||
"description":"CustomNameplates",
|
||||
"supported_formats": {
|
||||
"min_inclusive": 32,
|
||||
"max_inclusive": 34
|
||||
}
|
||||
}
|
||||
}
|
||||
213
backend/src/main/resources/config.yml
Normal file
@@ -0,0 +1,213 @@
|
||||
# Do not modify this value
|
||||
config-version: '${config_version}'
|
||||
# Enables or disables debug mode
|
||||
debug: false
|
||||
# Enables or disables metrics collection via BStats
|
||||
metrics: true
|
||||
# Enables automatic update checks
|
||||
update-checker: true
|
||||
# Forces a specific locale (e.g., zh_cn)
|
||||
force-locale: ''
|
||||
# Module Settings
|
||||
# Modifying these modules may result in distorted images. If this occurs, reinstall the newly generated resource pack.
|
||||
modules:
|
||||
nameplates: true
|
||||
backgrounds: true
|
||||
bubbles: true
|
||||
bossbars: true
|
||||
actionbars: true
|
||||
images: true
|
||||
# Plugin Integrations
|
||||
integrations:
|
||||
# Should Nameplates merge resource packs with these plugins during reload?
|
||||
resource-pack:
|
||||
ItemsAdder: false
|
||||
ItemsAdder-old-method: false
|
||||
Oraxen: false
|
||||
# Some chats, such as staff chats or menu inputs, may need to be excluded from capture.
|
||||
# Supported chat plugins are listed below, which allow blacklist channels and /ignore functionality to work correctly.
|
||||
chat:
|
||||
TrChat: false
|
||||
VentureChat: false
|
||||
HuskChat: false
|
||||
CarbonChat: false
|
||||
AdvancedChat: false
|
||||
Essentials: false
|
||||
# Resource Pack Generation Settings
|
||||
resource-pack:
|
||||
# Disables resource pack generation at server startup
|
||||
disable-generation-on-start: false
|
||||
# Namespace identifier
|
||||
namespace: "nameplates"
|
||||
# Font selection
|
||||
font: "default"
|
||||
# The starting character for custom fonts
|
||||
# This character is used, but it won't affect normal Korean characters during chat, as they belong to the "minecraft:default" font.
|
||||
initial-char: '뀁'
|
||||
# Specify directories for PNG file generation.
|
||||
# This helps maintain an organized resource pack structure.
|
||||
image-path:
|
||||
nameplates: 'font/nameplates/'
|
||||
backgrounds: 'font/backgrounds/'
|
||||
images: 'font/images/'
|
||||
bubbles: 'font/bubbles/'
|
||||
space-split: 'font/base/'
|
||||
# Shader Settings
|
||||
shader:
|
||||
# Enables shader generation
|
||||
enable: true
|
||||
# Hides scoreboard numbers
|
||||
hide-scoreboard-number: false
|
||||
# Enables support for animated text shaders
|
||||
animated-text: false
|
||||
# Enables ItemsAdder text effect support
|
||||
ItemsAdder-text-effects: false
|
||||
# Bossbar transparency settings
|
||||
transparent-bossbar:
|
||||
# Specify the color of the bossbar to hide
|
||||
color: YELLOW
|
||||
# Generate transparent bossbars for Minecraft 1.20.2+
|
||||
"1_20_2+": true
|
||||
# Generate transparent bossbars for Minecraft 1.17-1.20.1
|
||||
"1_17-1_20_1": true
|
||||
# Legacy Unicode support for Minecraft 1.20+ clients, as these images were removed in 1.20, affecting the creation of decent texts.
|
||||
# Enabling this increases your resource pack size by approximately 900KB.
|
||||
legacy-unicodes: true
|
||||
# Additional Settings
|
||||
other-settings:
|
||||
# It is recommended to use the MiniMessage format. If you prefer legacy color codes using "&", enable this support.
|
||||
# Disabling it will improve color formatting performance.
|
||||
legacy-color-code-support: true
|
||||
# Set a delay for actionbar/bossbar to prevent compatibility issues
|
||||
send-delay: 0
|
||||
# Determines whether CustomNameplates should capture actionbars from other plugins
|
||||
catch-other-plugin-actionbar: true
|
||||
# Should the plugin listen for chat canceled events by unknown chat plugins?
|
||||
unsafe-chat-event: false
|
||||
# Set the default placeholder refresh interval
|
||||
default-placeholder-refresh-interval: 1
|
||||
# Set the refresh interval for better performance in ticks (Especially for those heavy placeholders)
|
||||
placeholder-refresh-interval:
|
||||
"%player_name%": 100
|
||||
"%vault_prefix%": 1
|
||||
# CustomNameplates provides some templates for reading the default Minecraft fonts
|
||||
# You can replace the files under /font or add new configuration to let the plugin get these templates.
|
||||
# Templates can be used in advance-data.yml
|
||||
font-templates:
|
||||
space:
|
||||
space:
|
||||
" ": 4
|
||||
"\\u200c": 0
|
||||
unihex:
|
||||
unifont:
|
||||
file: unifont.zip
|
||||
generate: false
|
||||
size_overrides:
|
||||
- from: "\\u3001"
|
||||
to: "\\u30FF"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\u3200"
|
||||
to: "\\u9FFF"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\u1100"
|
||||
to: "\\u11FF"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\u3130"
|
||||
to: "\\u318F"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\uA960"
|
||||
to: "\\uA97F"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\uD7B0"
|
||||
to: "\\uD7FF"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\uAC00"
|
||||
to: "\\uD7AF"
|
||||
left: 1
|
||||
right: 15
|
||||
- from: "\\uF900"
|
||||
to: "\\uFAFF"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\uFF01"
|
||||
to: "\\uFF5E"
|
||||
left: 0
|
||||
right: 15
|
||||
unifont_jp:
|
||||
file: unifont_jp.zip
|
||||
generate: false
|
||||
filter:
|
||||
"jp": true
|
||||
size_overrides:
|
||||
- from: "\\u3200"
|
||||
to: "\\u9FFF"
|
||||
left: 0
|
||||
right: 15
|
||||
- from: "\\uF900"
|
||||
to: "\\uFAFF"
|
||||
left: 0
|
||||
right: 15
|
||||
bitmap:
|
||||
ascii:
|
||||
codepoints: ascii
|
||||
file: ascii.png
|
||||
height: 8
|
||||
custom: false # Is this a non-default Minecraft font? If enabled, plugin will create a new font image in the generated resource pack
|
||||
ascii_sga:
|
||||
codepoints: ascii_sga
|
||||
file: ascii_sga.png
|
||||
height: 8
|
||||
custom: false
|
||||
asciillager:
|
||||
codepoints: asciillager
|
||||
file: asciillager.png
|
||||
height: 8
|
||||
custom: false
|
||||
nonlatin_european:
|
||||
codepoints: nonlatin_european
|
||||
file: nonlatin_european.png
|
||||
height: 8
|
||||
custom: false
|
||||
accented:
|
||||
codepoints: accented
|
||||
file: accented.png
|
||||
height: 12
|
||||
custom: false
|
||||
legacy_unicode:
|
||||
legacy_unicode:
|
||||
file: unicode_page_%02x.png
|
||||
height: 8
|
||||
custom: false
|
||||
unicode:
|
||||
unicode:
|
||||
file: unicode_page_%02x.png
|
||||
sizes: glyph_sizes.bin
|
||||
ttf:
|
||||
example:
|
||||
generate: false
|
||||
file: example.ttf
|
||||
size: 10.0
|
||||
oversample: 8.0
|
||||
skip: []
|
||||
# If the font is a bitmap font, the number represents the ascent
|
||||
# If the font is a ttf font, the number represents the Y shift
|
||||
shift-fonts:
|
||||
shift_0:
|
||||
- space
|
||||
- nonlatin_european:7
|
||||
- accented:9
|
||||
- ascii:7
|
||||
- unifont_jp
|
||||
- unifont
|
||||
shift_1:
|
||||
- space
|
||||
- nonlatin_european:3
|
||||
- accented:6
|
||||
- ascii:3
|
||||
- legacy_unicode:3
|
||||
24
backend/src/main/resources/configs/actionbar.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
# Important: Players can only see one actionbar at any given moment, no overlap allowed!
|
||||
actionbar:
|
||||
# Players need this permission to display the actionbar, the conditions can be customized
|
||||
conditions:
|
||||
condition_1:
|
||||
type: permission
|
||||
refresh-interval: 20 # Refresh rate, in ticks, to recheck the permission status
|
||||
value:
|
||||
- actionbar.show
|
||||
text-display-order:
|
||||
1:
|
||||
# Duration of the text display, measured in ticks (20 ticks = 1 second)
|
||||
# -1 means the text will stay on the screen forever!
|
||||
duration: -1
|
||||
# The message to be shown in the actionbar
|
||||
text: '%np_conditional_actionbar%'
|
||||
# Optional settings for finer control:
|
||||
# When enabling conditions, make sure at least one valid actionbar is displayed to the player,
|
||||
# or it may lead to an endless loop with nothing visible.
|
||||
# If the player doesn't meet the condition, this actionbar will simply be skipped.
|
||||
# The condition is only checked once per cycle, until the next time it comes around.
|
||||
conditions: {}
|
||||
#2: ...
|
||||
#3: ...
|
||||
52
backend/src/main/resources/configs/advance-data.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
# namespace:font
|
||||
minecraft:default:
|
||||
# default advance
|
||||
default: 9.0
|
||||
# Sets the loading sequence of the templates
|
||||
template-loading-sequence:
|
||||
- unifont
|
||||
- nonlatin_european
|
||||
- accented
|
||||
- ascii
|
||||
- space
|
||||
# The values here would override the same character in the template
|
||||
values:
|
||||
默: 9.0
|
||||
|
||||
minecraft:uniform:
|
||||
default: 9.0
|
||||
template-loading-sequence:
|
||||
- unifont
|
||||
- space
|
||||
|
||||
minecraft:alt:
|
||||
default: 9.0
|
||||
template-loading-sequence:
|
||||
- ascii_sga
|
||||
- space
|
||||
|
||||
minecraft:asciillager:
|
||||
default: 9.0
|
||||
template-loading-sequence:
|
||||
- asciillager
|
||||
- space
|
||||
|
||||
customcrops:default:
|
||||
default: 9.0
|
||||
template-loading-sequence: []
|
||||
values:
|
||||
뀁: -1.0
|
||||
뀂: 5.0
|
||||
뀃: 7.0
|
||||
뀄: 7.0
|
||||
뀅: 5.0
|
||||
|
||||
minecraft:customcrops:
|
||||
default: 9.0
|
||||
template-loading-sequence: []
|
||||
values:
|
||||
뀁: -1.0
|
||||
뀂: 5.0
|
||||
뀃: 7.0
|
||||
뀄: 7.0
|
||||
뀅: 5.0
|
||||
39
backend/src/main/resources/configs/bossbar.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
# You can create as many bossbar sections as you'd like
|
||||
bossbar_1:
|
||||
# Choose the color of the bossbar (Options: BLUE, GREEN, PINK, PURPLE, RED, WHITE, YELLOW)
|
||||
color: YELLOW
|
||||
# Decide how the progress bar looks (Options: "progress", "notched_6", "notched_10", "notched_12", "notched_20")
|
||||
overlay: PROGRESS
|
||||
conditions:
|
||||
condition_1:
|
||||
type: permission
|
||||
refresh-interval: 20 # How often, in ticks, to check if the player still has the required permission
|
||||
value:
|
||||
- bossbar.show
|
||||
# The bossbar will cycle through the text in this exact order:
|
||||
text-display-order:
|
||||
1:
|
||||
# Time (in ticks) this text stays on the screen (100 ticks = 5 seconds)
|
||||
duration: 100
|
||||
# What message to display in the bossbar
|
||||
text: '%np_background_hello%'
|
||||
2:
|
||||
# Display for 10 seconds
|
||||
duration: 200
|
||||
text: '%np_background_time% %np_background_location% %np_background_weather%'
|
||||
# How often to refresh this text, in ticks (1 tick = 1/20 of a second)
|
||||
refresh-frequency: 1
|
||||
3:
|
||||
# Show for 5 seconds
|
||||
duration: 100
|
||||
text: '%np_background_update%'
|
||||
# Optional extra control:
|
||||
# Make sure at least one bossbar message is shown, or else it could result in an endless cycle with nothing visible!
|
||||
# If the player doesn’t meet the condition, this message will be skipped.
|
||||
# Conditions are only checked once per cycle until it comes back around.
|
||||
conditions:
|
||||
permission: nameplates.admin # Only players with this permission will see this specific message
|
||||
equals:
|
||||
# Compare two values and act based on the result
|
||||
value1: '%np_is-latest%'
|
||||
value2: 'false' # Only display if the value is "false"
|
||||
44
backend/src/main/resources/configs/bubble.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
# Requirements for sending the bubble
|
||||
sender-requirements:
|
||||
permission: bubbles.send
|
||||
'!gamemode': spectator
|
||||
potion-effect: "INVISIBILITY<0"
|
||||
|
||||
viewer-requirements:
|
||||
permission: bubbles.see
|
||||
|
||||
# blacklist channels
|
||||
blacklist-channels:
|
||||
- Private
|
||||
- Staff
|
||||
|
||||
# ALL: all the players can see the bubble
|
||||
# JOINED: players in the same channel can see each other's bubble
|
||||
# CAN_JOIN: players that have permission to certain channels would see the bubble in that channel
|
||||
channel-mode: ALL
|
||||
|
||||
# Default bubble to display if player's bubble is "none"
|
||||
default-bubble: 'chat'
|
||||
|
||||
# Y offset from the highest nameplate
|
||||
y-offset: 0.1
|
||||
|
||||
view-range: 0.5
|
||||
|
||||
# Bubble display time (in ticks)
|
||||
stay-duration: 150
|
||||
appear-duration: 4
|
||||
disappear-duration: 2
|
||||
|
||||
bubble-settings:
|
||||
chat:
|
||||
display-name: "White Bubbles"
|
||||
max-lines: 3
|
||||
lines:
|
||||
1: chat_1
|
||||
2: chat_2
|
||||
3: chat_3
|
||||
line-width: 150
|
||||
background-color: 0,0,0,0
|
||||
text-prefix: "<black><font:nameplates:shift_0>"
|
||||
text-suffix: "</font></black>"
|
||||
125
backend/src/main/resources/configs/custom-placeholders.yml
Normal file
@@ -0,0 +1,125 @@
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/conditional-text
|
||||
conditional-text:
|
||||
actionbar:
|
||||
priority_1:
|
||||
text: '%np_background_other_actionbar%'
|
||||
conditions:
|
||||
'||':
|
||||
'!=':
|
||||
value1: '%player_remaining_air%'
|
||||
value2: "300"
|
||||
'!gamemode': survival
|
||||
priority_2:
|
||||
text: '%np_static_money_hud%%np_offset_-180%%np_static_other_actionbar%'
|
||||
conditions:
|
||||
'!equals':
|
||||
value1: '%np_actionbar%'
|
||||
value2: ""
|
||||
priority_3:
|
||||
text: '%np_static_money_hud%'
|
||||
weather:
|
||||
priority_1:
|
||||
text: 'Sunny'
|
||||
conditions:
|
||||
weather:
|
||||
- clear
|
||||
priority_2:
|
||||
text: 'Rainy'
|
||||
conditions:
|
||||
weather:
|
||||
- rain
|
||||
priority_3:
|
||||
text: 'Thunder'
|
||||
conditions:
|
||||
weather:
|
||||
- thunder
|
||||
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/nameplate-text
|
||||
nameplate-text:
|
||||
halloween:
|
||||
nameplate: halloween
|
||||
text: '<gradient:#FFD700:#FFA500:#FFD700>Today is Halloween! Trick or treat!</gradient>'
|
||||
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/background-text
|
||||
background-text:
|
||||
location:
|
||||
background: bedrock_1
|
||||
text: '%np_image_compass% %np_shift_location%'
|
||||
remove-shadow: true
|
||||
time:
|
||||
background: bedrock_1
|
||||
text: '%np_image_clock% %np_shift_time%'
|
||||
remove-shadow: true
|
||||
weather:
|
||||
background: bedrock_1
|
||||
text: '%np_image_weather% %np_shift_weather%'
|
||||
remove-shadow: true
|
||||
hello:
|
||||
background: bedrock_1
|
||||
text: '%np_image_bubble% %np_shift_hello%'
|
||||
remove-shadow: true
|
||||
update:
|
||||
background: bedrock_1
|
||||
text: '%np_image_bell% %np_shift_update%'
|
||||
remove-shadow: true
|
||||
other_actionbar:
|
||||
background: bedrock_2
|
||||
text: '%np_actionbar%'
|
||||
remove-shadow: true
|
||||
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/static-text
|
||||
static-text:
|
||||
money_hud:
|
||||
position: right
|
||||
text: '%np_image_coin% %np_shift_money%'
|
||||
value: 180
|
||||
other_actionbar:
|
||||
position: middle
|
||||
text: "%np_background_other_actionbar%"
|
||||
value: 180
|
||||
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/descent-text
|
||||
shift-text:
|
||||
player_name:
|
||||
text: "%player_name%"
|
||||
font: shift_0
|
||||
location:
|
||||
text: "Your Location: %np_switch_world% (%player_x%,%player_y%,%player_z%)"
|
||||
font: shift_1
|
||||
time:
|
||||
text: "Time: %np_time%"
|
||||
font: shift_1
|
||||
weather:
|
||||
text: "Weather: %np_conditional_weather%"
|
||||
font: shift_1
|
||||
update:
|
||||
text: "A newer version of CustomNameplates is available!"
|
||||
font: shift_1
|
||||
money:
|
||||
text: "%vault_eco_balance%"
|
||||
font: shift_1
|
||||
hello:
|
||||
text: "Hello 여보세요 你好 こんにちは, Thanks for using CustomNameplates"
|
||||
font: shift_1
|
||||
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/switch-text
|
||||
switch-text:
|
||||
world:
|
||||
switch: '%player_world%'
|
||||
case:
|
||||
'world': "<green>Overworld</green>"
|
||||
'world_nether': "<red>The Nether</red>"
|
||||
'world_the_end': "<red>The End</red>"
|
||||
default: "<gray>Unknown world</gray>"
|
||||
|
||||
# https://mo-mi.gitbook.io/xiaomomi-plugins/plugin-wiki/customnameplates/custom-placeholders/vanilla-hud
|
||||
vanilla-hud:
|
||||
stamina_hud:
|
||||
reverse: true
|
||||
images:
|
||||
empty: stamina_0
|
||||
half: stamina_1
|
||||
full: stamina_2
|
||||
placeholder:
|
||||
value: '1.1'
|
||||
max-value: '2'
|
||||
67
backend/src/main/resources/configs/nameplate.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# Duration (in seconds) for which the nameplate preview will be displayed.
|
||||
preview-duration: 5
|
||||
|
||||
# Default nameplate shown when a player's nameplate is set to "none."
|
||||
default-nameplate: 'none'
|
||||
|
||||
# Whether to make the nameplate always visible to the player
|
||||
always-show: false
|
||||
|
||||
# Configuration for nameplate behavior and appearance.
|
||||
nameplate:
|
||||
# Prefix to be displayed before the player's name.
|
||||
# The prefix here will become part of the nameplate
|
||||
prefix: ''
|
||||
# Placeholder for the player's name
|
||||
# The default configuration uses shift to ensure that the player name not affected by the clientside `force-unicode-font` setting.
|
||||
player-name: '%np_shift_player_name%'
|
||||
# Suffix to be displayed after the player's name.
|
||||
# The suffix here will become part of the nameplate
|
||||
suffix: ''
|
||||
|
||||
# Configuration for Unlimited tags.
|
||||
unlimited:
|
||||
tag_1:
|
||||
text: '%np_tag-image%'
|
||||
translation: 0,0.2,0
|
||||
viewer-conditions: { }
|
||||
owner-conditions:
|
||||
condition_potion:
|
||||
type: potion-effect
|
||||
value: "INVISIBILITY<0"
|
||||
refresh-interval: 1
|
||||
self-disguised: false
|
||||
affected-by-crouching: true
|
||||
affected-by-scale-attribute: true
|
||||
line-width: 1024
|
||||
background-color: 0,0,0,0
|
||||
tag_2:
|
||||
text: '%np_tag-text%'
|
||||
translation: 0.001,0.2,0.001
|
||||
viewer-conditions: { }
|
||||
owner-conditions:
|
||||
has-nameplate: true
|
||||
condition_potion:
|
||||
type: potion-effect
|
||||
value: "INVISIBILITY<0"
|
||||
refresh-interval: 1
|
||||
self-disguised: false
|
||||
affected-by-crouching: true
|
||||
affected-by-scale-attribute: true
|
||||
line-width: 1024
|
||||
background-color: 0,0,0,0
|
||||
tag_3:
|
||||
text: '%np_tag-text%'
|
||||
translation: 0,0.2,0
|
||||
viewer-conditions: { }
|
||||
owner-conditions:
|
||||
has-nameplate: false
|
||||
condition_potion:
|
||||
type: potion-effect
|
||||
value: "INVISIBILITY<0"
|
||||
refresh-interval: 1
|
||||
self-disguised: false
|
||||
affected-by-crouching: true
|
||||
affected-by-scale-attribute: true
|
||||
line-width: 1024
|
||||
background-color: 64,0,0,0
|
||||
BIN
backend/src/main/resources/contents/backgrounds/b0.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b1.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b128.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b16.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b2.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b4.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b64.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
backend/src/main/resources/contents/backgrounds/b8.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,21 @@
|
||||
left:
|
||||
image: b0
|
||||
height: 14
|
||||
ascent: 7
|
||||
|
||||
right:
|
||||
image: b0
|
||||
height: 14
|
||||
ascent: 7
|
||||
|
||||
middle:
|
||||
height: 14
|
||||
ascent: 7
|
||||
1: b1
|
||||
2: b2
|
||||
4: b4
|
||||
8: b8
|
||||
16: b16
|
||||
32: b32
|
||||
64: b64
|
||||
128: b128
|
||||
@@ -0,0 +1,21 @@
|
||||
left:
|
||||
image: b0
|
||||
height: 14
|
||||
ascent: 12
|
||||
|
||||
right:
|
||||
image: b0
|
||||
height: 14
|
||||
ascent: 12
|
||||
|
||||
middle:
|
||||
height: 14
|
||||
ascent: 12
|
||||
1: b1
|
||||
2: b2
|
||||
4: b4
|
||||
8: b8
|
||||
16: b16
|
||||
32: b32
|
||||
64: b64
|
||||
128: b128
|
||||
16
backend/src/main/resources/contents/bubbles/chat_1.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
left:
|
||||
image: chat_1_left
|
||||
height: 13
|
||||
ascent: 9
|
||||
middle:
|
||||
image: chat_1_middle
|
||||
height: 13
|
||||
ascent: 9
|
||||
right:
|
||||
image: chat_1_right
|
||||
height: 13
|
||||
ascent: 9
|
||||
tail:
|
||||
image: chat_1_tail
|
||||
height: 13
|
||||
ascent: 9
|
||||
BIN
backend/src/main/resources/contents/bubbles/chat_1_left.png
Normal file
|
After Width: | Height: | Size: 143 B |
BIN
backend/src/main/resources/contents/bubbles/chat_1_middle.png
Normal file
|
After Width: | Height: | Size: 132 B |
BIN
backend/src/main/resources/contents/bubbles/chat_1_right.png
Normal file
|
After Width: | Height: | Size: 144 B |
BIN
backend/src/main/resources/contents/bubbles/chat_1_tail.png
Normal file
|
After Width: | Height: | Size: 150 B |
16
backend/src/main/resources/contents/bubbles/chat_2.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
left:
|
||||
image: chat_2_left
|
||||
height: 23
|
||||
ascent: 19
|
||||
middle:
|
||||
image: chat_2_middle
|
||||
height: 23
|
||||
ascent: 19
|
||||
right:
|
||||
image: chat_2_right
|
||||
height: 23
|
||||
ascent: 19
|
||||
tail:
|
||||
image: chat_2_tail
|
||||
height: 23
|
||||
ascent: 19
|
||||
BIN
backend/src/main/resources/contents/bubbles/chat_2_left.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
backend/src/main/resources/contents/bubbles/chat_2_middle.png
Normal file
|
After Width: | Height: | Size: 160 B |
BIN
backend/src/main/resources/contents/bubbles/chat_2_right.png
Normal file
|
After Width: | Height: | Size: 190 B |
BIN
backend/src/main/resources/contents/bubbles/chat_2_tail.png
Normal file
|
After Width: | Height: | Size: 174 B |
16
backend/src/main/resources/contents/bubbles/chat_3.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
left:
|
||||
image: chat_3_left
|
||||
height: 33
|
||||
ascent: 29
|
||||
middle:
|
||||
image: chat_3_middle
|
||||
height: 33
|
||||
ascent: 29
|
||||
right:
|
||||
image: chat_3_right
|
||||
height: 33
|
||||
ascent: 29
|
||||
tail:
|
||||
image: chat_3_tail
|
||||
height: 33
|
||||
ascent: 29
|
||||
BIN
backend/src/main/resources/contents/bubbles/chat_3_left.png
Normal file
|
After Width: | Height: | Size: 194 B |
BIN
backend/src/main/resources/contents/bubbles/chat_3_middle.png
Normal file
|
After Width: | Height: | Size: 160 B |
BIN
backend/src/main/resources/contents/bubbles/chat_3_right.png
Normal file
|
After Width: | Height: | Size: 191 B |
BIN
backend/src/main/resources/contents/bubbles/chat_3_tail.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
backend/src/main/resources/contents/images/bell.png
Normal file
|
After Width: | Height: | Size: 156 B |
6
backend/src/main/resources/contents/images/bell.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: bell
|
||||
height: 10
|
||||
ascent: 4
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/bubble.png
Normal file
|
After Width: | Height: | Size: 118 B |
6
backend/src/main/resources/contents/images/bubble.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: bubble
|
||||
height: 10
|
||||
ascent: 4
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/clock.png
Normal file
|
After Width: | Height: | Size: 143 B |
6
backend/src/main/resources/contents/images/clock.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: clock
|
||||
height: 10
|
||||
ascent: 4
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/coin.png
Normal file
|
After Width: | Height: | Size: 163 B |
6
backend/src/main/resources/contents/images/coin.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: coin
|
||||
height: 10
|
||||
ascent: -14
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/compass.png
Normal file
|
After Width: | Height: | Size: 175 B |
6
backend/src/main/resources/contents/images/compass.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: compass
|
||||
height: 10
|
||||
ascent: 4
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/stamina_0.png
Normal file
|
After Width: | Height: | Size: 121 B |
6
backend/src/main/resources/contents/images/stamina_0.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: stamina_0
|
||||
height: 9
|
||||
ascent: -16
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/stamina_1.png
Normal file
|
After Width: | Height: | Size: 157 B |
6
backend/src/main/resources/contents/images/stamina_1.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: stamina_1
|
||||
height: 9
|
||||
ascent: -16
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/stamina_2.png
Normal file
|
After Width: | Height: | Size: 207 B |
6
backend/src/main/resources/contents/images/stamina_2.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: stamina_2
|
||||
height: 9
|
||||
ascent: -16
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
BIN
backend/src/main/resources/contents/images/weather.png
Normal file
|
After Width: | Height: | Size: 126 B |
6
backend/src/main/resources/contents/images/weather.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
image: weather
|
||||
height: 10
|
||||
ascent: 4
|
||||
shadow:
|
||||
remove: true
|
||||
opacity: 254
|
||||
13
backend/src/main/resources/contents/nameplates/cat.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
display-name: Sad Cat
|
||||
left:
|
||||
image: cat_left
|
||||
height: 16
|
||||
ascent: 12
|
||||
middle:
|
||||
image: cat_middle
|
||||
height: 16
|
||||
ascent: 12
|
||||
right:
|
||||
image: cat_right
|
||||
height: 16
|
||||
ascent: 12
|
||||
BIN
backend/src/main/resources/contents/nameplates/cat_left.png
Normal file
|
After Width: | Height: | Size: 396 B |
BIN
backend/src/main/resources/contents/nameplates/cat_middle.png
Normal file
|
After Width: | Height: | Size: 194 B |
BIN
backend/src/main/resources/contents/nameplates/cat_right.png
Normal file
|
After Width: | Height: | Size: 391 B |
14
backend/src/main/resources/contents/nameplates/cheems.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
name-color: WHITE
|
||||
display-name: Thank you, Cheems
|
||||
left:
|
||||
image: cheems_left
|
||||
height: 16
|
||||
ascent: 12
|
||||
middle:
|
||||
image: cheems_middle
|
||||
height: 16
|
||||
ascent: 12
|
||||
right:
|
||||
image: cheems_right
|
||||
height: 16
|
||||
ascent: 12
|
||||
BIN
backend/src/main/resources/contents/nameplates/cheems_left.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
backend/src/main/resources/contents/nameplates/cheems_middle.png
Normal file
|
After Width: | Height: | Size: 957 B |
BIN
backend/src/main/resources/contents/nameplates/cheems_right.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
14
backend/src/main/resources/contents/nameplates/egg.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
name-color: WHITE
|
||||
display-name: Happy Egg
|
||||
left:
|
||||
image: egg_left
|
||||
height: 16
|
||||
ascent: 12
|
||||
middle:
|
||||
image: egg_middle
|
||||
height: 16
|
||||
ascent: 12
|
||||
right:
|
||||
image: egg_right
|
||||
height: 16
|
||||
ascent: 12
|
||||
BIN
backend/src/main/resources/contents/nameplates/egg_left.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
backend/src/main/resources/contents/nameplates/egg_middle.png
Normal file
|
After Width: | Height: | Size: 998 B |
BIN
backend/src/main/resources/contents/nameplates/egg_right.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |