9
0
mirror of https://github.com/Xiao-MoMi/Custom-Nameplates.git synced 2025-12-31 04:36:29 +00:00
This commit is contained in:
XiaoMoMi
2024-10-05 22:42:28 +08:00
parent d8324da1d9
commit 3aa7e5c012
859 changed files with 28657 additions and 22513 deletions

View File

@@ -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));
}
}

View File

@@ -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.
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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(?, ?)";
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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 ] }
]
}

View File

@@ -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%
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

View File

@@ -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);
}

View File

@@ -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 ] }
]
}

View File

@@ -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%
}

View 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"
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,10 @@
{
"pack":{
"pack_format": 32,
"description":"CustomNameplates",
"supported_formats": {
"min_inclusive": 32,
"max_inclusive": 34
}
}
}

View 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

View 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: ...

View 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

View 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 doesnt 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"

View 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>"

View 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'

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -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

View File

@@ -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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1,6 @@
image: bell
height: 10
ascent: 4
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

View File

@@ -0,0 +1,6 @@
image: bubble
height: 10
ascent: 4
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

View File

@@ -0,0 +1,6 @@
image: clock
height: 10
ascent: 4
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

View File

@@ -0,0 +1,6 @@
image: coin
height: 10
ascent: -14
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

View File

@@ -0,0 +1,6 @@
image: compass
height: 10
ascent: 4
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

View File

@@ -0,0 +1,6 @@
image: stamina_0
height: 9
ascent: -16
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

View File

@@ -0,0 +1,6 @@
image: stamina_1
height: 9
ascent: -16
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -0,0 +1,6 @@
image: stamina_2
height: 9
ascent: -16
shadow:
remove: true
opacity: 254

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View File

@@ -0,0 +1,6 @@
image: weather
height: 10
ascent: 4
shadow:
remove: true
opacity: 254

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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