Skip to content

tobiasheimboeck/HyLib

Repository files navigation

HyLib

A Hytale utility library providing database functionality and Hytale-specific features.

Features

Database Module (Hytale-independent)

  • Type-safe SQL Queries - Query Builder with explicit column lists
  • SQL Injection Protection - Validated identifiers (Table/Column) instead of string concatenation
  • Flexible Query API - Fluent Builder instead of enum-based templates
  • RowMapper-based Mapping - Clean separation of SQL and domain mapping
  • Connection Pooling - HikariCP for MariaDB
  • In-Memory Cache - Simple cache API

Hytale Module

  • Config API - Type-safe configuration with fluent DSL codec builder
  • Localization API - Multi-language translation system with plugin support

Modules

Database Modules (no Hytale dependencies)

  • database-api: Interfaces, abstractions, Query Builder, RowMapper (usable in Discord Bot, etc.)
  • database-common: Default implementations (Api, ConnectionHandler, Loader)

Hytale Modules (requires Hytale dependencies)

  • hytale-api: Hytale-specific API (CodecBuilder Interface)
  • hytale-common: Hytale-specific implementations (CodecBuilderImpl)
  • database-hytale-plugin: Hytale plugin combining both APIs

Installation

GitHub Packages einrichten

Die Library ist über GitHub Packages verfügbar. Um sie zu verwenden, musst du das GitHub Packages Repository zu deinem Projekt hinzufügen.

1. Repository konfigurieren

Füge das GitHub Packages Repository zu deiner build.gradle.kts hinzu:

repositories {
    mavenCentral()
    
    // GitHub Packages Repository
    maven {
        name = "GitHubPackages"
        url = uri("https://maven.pkg.github.com/OWNER/REPO")
        credentials {
            username = project.findProperty("github.username") as String? 
                ?: System.getenv("GITHUB_ACTOR")
            password = project.findProperty("github.token") as String? 
                ?: System.getenv("GITHUB_TOKEN")
        }
    }
    
    // Hytale Repositories (falls benötigt)
    maven {
        name = "hytale-release"
        url = uri("https://maven.hytale.com/release")
    }
    maven {
        name = "hytale-pre-release"
        url = uri("https://maven.hytale.com/pre-release")
    }
}

Wichtig: Ersetze OWNER und REPO mit deinen GitHub Repository-Informationen:

  • OWNER: GitHub Username oder Organisation (z.B. spacetivity)
  • REPO: Repository Name (z.B. hylib)

2. Authentifizierung einrichten

Du benötigst einen GitHub Personal Access Token (PAT) mit read:packages Berechtigung.

Option A: Über gradle.properties (empfohlen für lokale Entwicklung)

Erstelle eine gradle.properties Datei im Projekt-Root:

github.username=dein-github-username
github.token=dein-github-personal-access-token

Option B: Über Umgebungsvariablen

Setze die Umgebungsvariablen:

export GITHUB_ACTOR=dein-github-username
export GITHUB_TOKEN=dein-github-personal-access-token

GitHub Token erstellen:

  1. Gehe zu: https://github.com/settings/tokens
  2. Klicke auf "Generate new token (classic)"
  3. Wähle die Berechtigung read:packages
  4. Kopiere den generierten Token

3. Dependencies hinzufügen

Füge die Dependencies zu deiner build.gradle.kts hinzu:

dependencies {
    // Database API (no Hytale dependencies - usable in Discord Bot, etc.)
    implementation("dev.spacetivity.tobi.hylib.database:database-api:VERSION")
    implementation("dev.spacetivity.tobi.hylib.database:database-common:VERSION")
    
    // Hytale API (for CodecBuilder - requires Hytale dependencies)
    implementation("dev.spacetivity.tobi.hylib.database:hytale-api:VERSION")
    implementation("dev.spacetivity.tobi.hylib.database:hytale-common:VERSION")
    
    // Hytale Server (für BuilderCodec und Config)
    compileOnly("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4")
    
    // Lombok (optional, aber empfohlen für Getter/Setter)
    compileOnly("org.projectlombok:lombok:1.18.30")
    annotationProcessor("org.projectlombok:lombok:1.18.30")
}

Wichtig: Ersetze VERSION mit der gewünschten Version (z.B. 1.0.0). Verfügbare Versionen findest du unter: https://github.com/OWNER/REPO/packages

Vollständiges Beispiel

// build.gradle.kts
plugins {
    id("java")
    id("java-library")
}

repositories {
    mavenCentral()
    
    maven {
        name = "GitHubPackages"
        url = uri("https://maven.pkg.github.com/spacetivity/hylib")
        credentials {
            username = project.findProperty("github.username") as String? 
                ?: System.getenv("GITHUB_ACTOR")
            password = project.findProperty("github.token") as String? 
                ?: System.getenv("GITHUB_TOKEN")
        }
    }
    
    maven {
        name = "hytale-release"
        url = uri("https://maven.hytale.com/release")
    }
}

dependencies {
    // Database API
    implementation("dev.spacetivity.tobi.hylib.database:database-api:1.0.0")
    
    // Hytale Server
    compileOnly("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4")
    
    // Lombok
    compileOnly("org.projectlombok:lombok:1.18.30")
    annotationProcessor("org.projectlombok:lombok:1.18.30")
}

Quick Start

1. API Initialization

import dev.spacetivity.tobi.hylib.database.api.DatabaseProvider;
import dev.spacetivity.tobi.hylib.database.api.connection.credentials.impl.MariaDbCredentials;
import dev.spacetivity.tobi.hylib.database.common.DatabaseApiImpl;

// Create and register the API
DatabaseApi api = new DatabaseApiImpl();
DatabaseProvider.register(api);

// Establish connection (optional, can be done later)
MariaDbCredentials credentials = new MariaDbCredentials(
    "localhost", 3306, "user", "database", "secret"
);
api.establishConnection(credentials);

2. Connection Handling

DatabaseConnectionHandler db = DatabaseProvider.getApi().getDatabaseConnectionHandler();

// MariaDB Connector (HikariCP)
DatabaseConnector<HikariDataSource, DatabaseCredentials> sqlConnector = 
    db.getConnectorNullsafe(DatabaseType.MARIADB);
Connection connection = sqlConnector.getSafeConnection().getConnection();

Safe Identifiers

Table & Column

Instead of free strings, validated identifiers are used that prevent SQL injection:

// Table Identifier
Table usersTable = Table.of("users");  // Validated: only [A-Za-z0-9_]

// Column Identifier
Column idColumn = Column.of("id");
Column nameColumn = Column.of("user_name");

// SQL-safe output
String sql = "SELECT * FROM " + usersTable.toSql();  // `users`
String col = idColumn.toSql();  // `id`

Validation: Only alphanumeric characters and underscores allowed. Invalid names throw IllegalArgumentException.


Query Builder API

SELECT Queries

import dev.spacetivity.tobi.hylib.database.api.connection.impl.sql.builder.SqlBuilder;

Table usersTable = Table.of("users");
Column idCol = Column.of("id");
Column nameCol = Column.of("name");
Column emailCol = Column.of("email");

// Simple SELECT with WHERE
BuiltQuery query = SqlBuilder
    .select(idCol, nameCol, emailCol)
    .from(usersTable)
    .where(idCol, 123)
    .build();

// With ORDER BY and LIMIT
BuiltQuery sortedQuery = SqlBuilder
    .select(idCol, nameCol)
    .from(usersTable)
    .where(emailCol, "test@example.com")
    .orderBy(nameCol, true)  // ASC
    .limit(10)
    .build();

// Multiple WHERE conditions (AND)
BuiltQuery multiWhere = SqlBuilder
    .select(idCol, nameCol)
    .from(usersTable)
    .where(idCol, 123)
    .where(nameCol, "John")
    .build();

INSERT Queries

BuiltQuery insert = SqlBuilder
    .insertInto(usersTable)
    .value(idCol, 123)
    .value(nameCol, "John Doe")
    .value(emailCol, "john@example.com")
    .build();

// Or multiple values at once
BuiltQuery batchInsert = SqlBuilder
    .insertInto(usersTable)
    .values(
        new Column[]{idCol, nameCol, emailCol},
        new Object[]{123, "John", "john@example.com"}
    )
    .build();

UPDATE Queries

BuiltQuery update = SqlBuilder
    .update(usersTable)
    .set(nameCol, "Jane Doe")
    .set(emailCol, "jane@example.com")
    .where(idCol, 123)
    .build();

// Multiple WHERE conditions
BuiltQuery updateMulti = SqlBuilder
    .update(usersTable)
    .set(emailCol, "new@example.com")
    .where(idCol, 123)
    .where(nameCol, "John")
    .build();

DELETE Queries

BuiltQuery delete = SqlBuilder
    .deleteFrom(usersTable)
    .where(idCol, 123)
    .build();

// Multiple WHERE conditions
BuiltQuery deleteMulti = SqlBuilder
    .deleteFrom(usersTable)
    .where(idCol, 123)
    .where(nameCol, "John")
    .build();

RowMapper

The RowMapper<T> interface separates SQL logic from domain mapping:

import dev.spacetivity.tobi.hylib.database.api.connection.impl.sql.RowMapper;

public class User {
    private final int id;
    private final String name;
    private final String email;
    
    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

// RowMapper implementation
RowMapper<User> userMapper = (ResultSet rs) -> {
    return new User(
        rs.getInt("id"),
        rs.getString("name"),
        rs.getString("email")
    );
};

// Usage in repository
BuiltQuery query = SqlBuilder.select(idCol, nameCol, emailCol)
    .from(usersTable)
    .where(idCol, 123)
    .build();

Optional<User> user = queryOne(query, userMapper);
List<User> users = query(query, userMapper);

Repository Pattern

Creating a Base Repository

import dev.spacetivity.tobi.hylib.database.api.connection.impl.sql.*;
import dev.spacetivity.tobi.hylib.database.api.repository.impl.AbstractMariaDbRepository;

public class UserRepository extends AbstractMariaDbRepository<User> {
    
    // Column definitions as constants
    private static final Table USERS_TABLE = Table.of("users");
    private static final Column ID_COL = Column.of("id");
    private static final Column NAME_COL = Column.of("name");
    private static final Column EMAIL_COL = Column.of("email");
    
    public UserRepository(DatabaseConnectionHandler db, Connection connection) {
        super(db, TableDefinition.create(
            connection,
            USERS_TABLE,
            SQLColumn.fromPrimary(ID_COL, SQLDataType.INTEGER),
            SQLColumn.from(NAME_COL, SQLDataType.VARCHAR),
            SQLColumn.fromNullable(EMAIL_COL, SQLDataType.VARCHAR)
        ));
    }
    
    @Override
    public User deserializeResultSet(ResultSet rs) throws SQLException {
        return new User(
            rs.getInt(ID_COL.name()),
            rs.getString(NAME_COL.name()),
            rs.getString(EMAIL_COL.name())
        );
    }
    
    @Override
    public void insert(User user) {
        BuiltQuery insert = SqlBuilder
            .insertInto(USERS_TABLE)
            .value(ID_COL, user.getId())
            .value(NAME_COL, user.getName())
            .value(EMAIL_COL, user.getEmail())
            .build();
        executeUpdate(insert);
    }
    
    // Custom query methods
    public Optional<User> findById(int id) {
        BuiltQuery query = SqlBuilder
            .select(ID_COL, NAME_COL, EMAIL_COL)
            .from(USERS_TABLE)
            .where(ID_COL, id)
            .build();
        return queryOne(query, this::deserializeResultSet);
    }
    
    public List<User> findByName(String name) {
        BuiltQuery query = SqlBuilder
            .select(ID_COL, NAME_COL, EMAIL_COL)
            .from(USERS_TABLE)
            .where(NAME_COL, name)
            .orderBy(ID_COL, true)
            .build();
        return query(query, this::deserializeResultSet);
    }
    
    public void updateEmail(int id, String email) {
        BuiltQuery update = SqlBuilder
            .update(USERS_TABLE)
            .set(EMAIL_COL, email)
            .where(ID_COL, id)
            .build();
        executeUpdate(update);
    }
    
    public void deleteById(int id) {
        BuiltQuery delete = SqlBuilder
            .deleteFrom(USERS_TABLE)
            .where(ID_COL, id)
            .build();
        executeUpdate(delete);
    }
    
    public boolean exists(int id) {
        return exists(ID_COL, id);
    }
}

Registering a Repository

DatabaseConnectionHandler db = DatabaseProvider.getApi().getDatabaseConnectionHandler();
Connection connection = db.getConnectorNullsafe(DatabaseType.MARIADB)
    .getSafeConnection()
    .getConnection();

UserRepository userRepo = new UserRepository(db, connection);

RepositoryLoader loader = DatabaseProvider.getApi().getRepositoryLoader();
loader.register(userRepo);

Asynchronous Queries

// Async GET
CompletableFuture<User> futureUser = userRepo.getAsync(ID_COL, 123);
futureUser.thenAccept(user -> {
    System.out.println("User: " + user.getName());
});

// Async GET ALL
CompletableFuture<List<User>> futureUsers = userRepo.getAllAsync();
futureUsers.thenAccept(users -> {
    users.forEach(u -> System.out.println(u.getName()));
});

Available Repository Methods

Query Methods

// Base query methods (protected)
List<T> query(BuiltQuery query, RowMapper<T> mapper)
Optional<T> queryOne(BuiltQuery query, RowMapper<T> mapper)
int executeUpdate(BuiltQuery query)
boolean existsQuery(BuiltQuery query)

// Public convenience methods
T getSync(Column keyColumn, Object key)
Optional<T> findById(Column keyColumn, Object key)  // via queryOne
boolean exists(Column keyColumn, Object key)
List<T> getAllSync()

Async Methods

CompletableFuture<T> getAsync(Column keyColumn, Object key)
CompletableFuture<List<T>> getAllAsync()

SQL Data Types

import dev.spacetivity.tobi.hylib.database.api.connection.impl.sql.SQLDataType;

// Text Types
SQLDataType.VARCHAR      // VARCHAR(255)
SQLDataType.CHAR         // CHAR(1)
SQLDataType.TEXT         // TEXT

// Numeric Types
SQLDataType.INTEGER      // INT
SQLDataType.BIGINT       // BIGINT
SQLDataType.DECIMAL     // DECIMAL
SQLDataType.DOUBLE      // DOUBLE

// Date/Time Types
SQLDataType.DATE         // DATE
SQLDataType.TIMESTAMP   // TIMESTAMP

// Boolean
SQLDataType.BOOLEAN     // TINYINT

SQLColumn Factory Methods

// Primary Key (NOT NULL)
SQLColumn.fromPrimary(Column.of("id"), SQLDataType.INTEGER)
SQLColumn.fromPrimary("id", SQLDataType.INTEGER)  // String variant

// NOT NULL Column
SQLColumn.from(Column.of("name"), SQLDataType.VARCHAR)
SQLColumn.from("name", SQLDataType.VARCHAR)

// Nullable Column
SQLColumn.fromNullable(Column.of("email"), SQLDataType.VARCHAR)
SQLColumn.fromNullable("email", SQLDataType.VARCHAR)

// Custom Value
SQLColumn.from(Column.of("status"), "VARCHAR(50) DEFAULT 'active'")

Config API

The Config API provides type-safe configuration management using a fluent DSL for building codecs. Codecs are created programmatically at runtime using method references.

Features

  • Fluent DSL - Simple, readable API for building codecs
  • Method References - Type-safe getter/setter references
  • Default Values - Support for optional fields with default values
  • Runtime Creation - No compile-time code generation required
  • Hytale integration - Works seamlessly with Hytale's Config<T> system

Quick Start

1. Create a Config Class

Create a config class with getters and setters:

package com.example.plugin.config;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MyPluginConfig {
    
    private String serverName = "MyServer";
    private int maxPlayers = 100;
    private boolean enabled = true;
    private String apiKey;
    private String databaseUrl = "jdbc:mariadb://localhost:3306/mydb";
}

2. Create a Codec

Add a static codec() method to your config class. Note: HytaleApi must be initialized before calling newCodec():

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import dev.spacetivity.tobi.hylib.hytale.api.HytaleProvider;

public class MyPluginConfig {
    // ... fields ...
    
    public static BuilderCodec<MyPluginConfig> codec() {
        return HytaleProvider.getApi().newCodec(MyPluginConfig.class)
                .field("server-name", Codec.STRING, 
                    MyPluginConfig::setServerName, 
                    MyPluginConfig::getServerName)
                    .withDefault("MyServer")
                .and()
                .field("max-players", Codec.INTEGER,
                    MyPluginConfig::setMaxPlayers,
                    MyPluginConfig::getMaxPlayers)
                    .withDefault(100)
                .and()
                .field("enabled", Codec.BOOLEAN,
                    MyPluginConfig::setEnabled,
                    MyPluginConfig::isEnabled)
                    .withDefault(true)
                .and()
                .field("api-key", Codec.STRING,
                    MyPluginConfig::setApiKey,
                    MyPluginConfig::getApiKey)
                .and()
                .field("database-url", Codec.STRING,
                    MyPluginConfig::setDatabaseUrl,
                    MyPluginConfig::getDatabaseUrl)
                    .withDefault("jdbc:mariadb://localhost:3306/mydb")
                .build();
    }
}

3. Use the Config

Important: HytaleApi must be initialized before calling newCodec(). Initialize it in your plugin constructor:

import com.hypixel.hytale.server.core.util.Config;
import dev.spacetivity.tobi.hylib.database.api.DatabaseProvider;
import dev.spacetivity.tobi.hylib.database.common.DatabaseApiImpl;
import dev.spacetivity.tobi.hylib.hytale.api.HytaleProvider;
import dev.spacetivity.tobi.hylib.hytale.common.HytaleApiImpl;

public class MyPlugin extends JavaPlugin {
    
    private Config<MyPluginConfig> config;
    
    public MyPlugin(JavaPluginInit init) {
        super(init);
        
        // Initialize DatabaseApi first
        DatabaseApi api = new DatabaseApiImpl();
        DatabaseProvider.register(api);
        
        // Now you can create configs with codecs
        config = withConfig("MyConfig", MyPluginConfig.codec());
    }
    
    @Override
    protected void setup() {
        super.setup();
        
        // Access config values
        MyPluginConfig cfg = config.get();
        String serverName = cfg.getServerName();
        int maxPlayers = cfg.getMaxPlayers();
        boolean enabled = cfg.isEnabled();
        
        if (cfg.getApiKey() == null) {
            getLogger().error("API key is required!");
            return;
        }
    }
    
    public MyPluginConfig getConfig() {
        return config.get();
    }
}

CodecBuilder API

The CodecBuilder provides a fluent DSL for building codecs. Codecs are created via HytaleProvider.getApi().newCodec(Class<T>):

Basic Usage

BuilderCodec<MyConfig> codec = HytaleProvider.getApi().newCodec(MyConfig.class)
    .field("key", Codec.STRING, MyConfig::setValue, MyConfig::getValue)
    .build();

With Default Value

BuilderCodec<MyConfig> codec = HytaleProvider.getApi().newCodec(MyConfig.class)
    .field("hostname", Codec.STRING, MyConfig::setHostname, MyConfig::getHostname)
        .withDefault("localhost")
    .build();

Multiple Fields

BuilderCodec<MyConfig> codec = HytaleProvider.getApi().newCodec(MyConfig.class)
    .field("hostname", Codec.STRING, MyConfig::setHostname, MyConfig::getHostname)
        .withDefault("localhost")
    .and()
    .field("port", Codec.INTEGER, MyConfig::setPort, MyConfig::getPort)
        .withDefault(3306)
    .build();

Supported Codec Types

The following Hytale Codec types are available:

Primitive Types

  • Codec.STRING
  • Codec.INTEGER
  • Codec.LONG
  • Codec.DOUBLE
  • Codec.FLOAT
  • Codec.BOOLEAN
  • Codec.BYTE
  • Codec.SHORT

Object Types

  • Codec.UUID_STRING
  • Codec.DURATION
  • Codec.INSTANT
  • Codec.PATH
  • Codec.LOG_LEVEL

Custom Codecs

You can also use custom codecs from Hytale's codec system:

import com.hypixel.hytale.codec.FunctionCodec;

// For enums
FunctionCodec<GameMode> gameModeCodec = new FunctionCodec<>(
    Codec.STRING, 
    GameMode::valueOf, 
    Enum::name
);

BuilderCodec<MyConfig> codec = HytaleProvider.getApi().newCodec(MyConfig.class)
    .field("mode", gameModeCodec, MyConfig::setMode, MyConfig::getMode)
        .withDefault(GameMode.NORMAL)
    .build();

Complete Example

Here's a complete example of a database configuration:

package com.example.plugin.config;

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import dev.spacetivity.tobi.hylib.database.api.DatabaseProvider;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class DatabaseConfig {
    
    private String hostname = "localhost";
    private int port = 3306;
    private String database;
    private String username;
    private String password;
    private int poolSize = 10;
    private java.time.Duration connectionTimeout = java.time.Duration.ofSeconds(30);
    private boolean sslEnabled = false;
    
    public static BuilderCodec<DatabaseConfig> codec() {
        return HytaleProvider.getApi().newCodec(DatabaseConfig.class)
                .field("hostname", Codec.STRING, 
                    DatabaseConfig::setHostname, 
                    DatabaseConfig::getHostname)
                    .withDefault("localhost")
                .and()
                .field("port", Codec.INTEGER,
                    DatabaseConfig::setPort,
                    DatabaseConfig::getPort)
                    .withDefault(3306)
                .and()
                .field("database", Codec.STRING,
                    DatabaseConfig::setDatabase,
                    DatabaseConfig::getDatabase)
                .and()
                .field("username", Codec.STRING,
                    DatabaseConfig::setUsername,
                    DatabaseConfig::getUsername)
                .and()
                .field("password", Codec.STRING,
                    DatabaseConfig::setPassword,
                    DatabaseConfig::getPassword)
                .and()
                .field("pool-size", Codec.INTEGER,
                    DatabaseConfig::setPoolSize,
                    DatabaseConfig::getPoolSize)
                    .withDefault(10)
                .and()
                .field("connection-timeout", Codec.DURATION,
                    DatabaseConfig::setConnectionTimeout,
                    DatabaseConfig::getConnectionTimeout)
                    .withDefault(java.time.Duration.ofSeconds(30))
                .and()
                .field("ssl-enabled", Codec.BOOLEAN,
                    DatabaseConfig::setSslEnabled,
                    DatabaseConfig::isSslEnabled)
                    .withDefault(false)
                .build();
    }
}

Usage:

public class DatabasePlugin extends JavaPlugin {
    
    private Config<DatabaseConfig> dbConfig;
    
    public DatabasePlugin(JavaPluginInit init) {
        super(init);
        
        dbConfig = withConfig("DatabaseConfig", DatabaseConfig.codec());
    }
    
    @Override
    protected void setup() {
        super.setup();
        
        DatabaseConfig config = dbConfig.get();
        
        // Use config values
        MariaDbCredentials credentials = new MariaDbCredentials(
            config.getHostname(),
            config.getPort(),
            config.getDatabase(),
            config.getUsername(),
            config.getPassword()
        );
        
        // Initialize database with config
        DatabaseApi api = new DatabaseApiImpl();
        api.establishConnection(credentials);
        DatabaseProvider.register(api);
    }
}

Best Practices

  1. Use Lombok: Use @Getter and @Setter to avoid boilerplate:
@Getter
@Setter
public class MyConfig {
    // ...
}
  1. Static Codec Method: Create a static codec() method in your config class:
public class MyConfig {
    // ... fields ...
    
    public static BuilderCodec<MyConfig> codec() {
        return HytaleProvider.getApi().newCodec(MyConfig.class)
            // ... fields ...
            .build();
    }
}
  1. Meaningful Field Names: Use descriptive field names and config keys:
// Good
.field("max-concurrent-connections", Codec.INTEGER, ...)
    .withDefault(10)

// Bad
.field("mcc", Codec.INTEGER, ...)
    .withDefault(10)
  1. Required vs Optional: Use .withDefault() for optional fields with defaults, or omit it for required fields:
// Required field (no default)
.field("api-key", Codec.STRING, MyConfig::setApiKey, MyConfig::getApiKey)

// Optional field (with default)
.field("timeout", Codec.DURATION, MyConfig::setTimeout, MyConfig::getTimeout)
    .withDefault(Duration.ofSeconds(30))

// Optional field (nullable - BuilderCodec handles null automatically)
.field("password", Codec.STRING, MyConfig::setPassword, MyConfig::getPassword)
  1. Group Related Configs: Create separate config classes for different concerns:
public class DatabaseConfig { /* ... */ }
public class PluginConfig { /* ... */ }
public class FeatureConfig { /* ... */ }
  1. Validate After Loading: Check required fields after loading:
DatabaseConfig config = dbConfig.get();
if (config.getApiKey() == null || config.getApiKey().isEmpty()) {
    throw new IllegalStateException("API key is required!");
}

Localization API

The Localization API provides a multi-language translation system that supports multiple plugins. Each plugin can register its own language files, and translations are automatically merged.

Features

  • Type-safe Keys - Use LangKey instead of raw strings for compile-time safety
  • Multi-language Support - Load translations from multiple language files
  • Plugin Support - Each plugin can register its own language files
  • Placeholder Support - Use {0}, {1}, etc. for dynamic values
  • Fallback Handling - Falls back to default language if translation not found
  • Dynamic Reloading - Reload language files at runtime

Quick Start for Plugins

1. Create Language Files

Create language files in your plugin's src/main/resources/lang/ directory using the structure lang/{language}/*.json:

lang/en/player.json:

{
  "myplugin.welcome": "Welcome {0}!",
  "myplugin.goodbye": "Goodbye {0}!"
}

lang/en/commands.json:

{
  "myplugin.command.help": "Help command",
  "myplugin.command.info": "Info command"
}

lang/de/player.json:

{
  "myplugin.welcome": "Willkommen {0}!",
  "myplugin.goodbye": "Auf Wiedersehen {0}!"
}

lang/de/commands.json:

{
  "myplugin.command.help": "Hilfe-Befehl",
  "myplugin.command.info": "Info-Befehl"
}

Note: Multiple JSON files per language are supported. All keys from all files are merged together.

2. Register Your Plugin's Language Source

In your plugin's setup() method, register your ClassLoader:

import dev.spacetivity.tobi.hylib.hytale.api.HytaleProvider;

public class MyPlugin extends JavaPlugin {
    
    @Override
    protected void setup() {
        super.setup();
        
        // Register your plugin's language files
        // Make sure HytaleApi is already initialized (usually by HyLib plugin)
        HytaleProvider.getApi().getLocalization()
            .registerLanguageSource(getClass().getClassLoader());
    }
}

3. Create Type-safe Keys

Create a key class in your plugin:

import dev.spacetivity.tobi.hylib.hytale.api.localization.LangKey;

public final class MyPluginKeys {
    private MyPluginKeys() {}
    
    public static final LangKey WELCOME = LangKey.of("myplugin.welcome");
    public static final LangKey GOODBYE = LangKey.of("myplugin.goodbye");
    public static final LangKey ERROR_NOT_FOUND = LangKey.of("myplugin.error.not_found");
}

4. Use Translations

import dev.spacetivity.tobi.hylib.hytale.api.HytaleProvider;
import dev.spacetivity.tobi.hylib.hytale.api.localization.Locale;
import dev.spacetivity.tobi.hylib.hytale.api.localization.Localization;

public class MyCommand {
    
    public void execute(Player player) {
        Localization loc = HytaleProvider.getApi().getLocalization();
        
        // Get player's locale (from LanguageComponent or HyPlayer)
        Locale playerLocale = player.getLanguage(); // or get from HyPlayer/LanguageComponent
        
        // Translate with locale using type-safe keys
        String welcomeMsg = loc.translate(MyPluginKeys.WELCOME, playerLocale, player.getName());
        player.sendMessage(welcomeMsg);
        
        // Or use default locale
        String goodbyeMsg = loc.translate(MyPluginKeys.GOODBYE, player.getName());
        player.sendMessage(goodbyeMsg);
    }
}

Type-safe Translation Keys

Use LangKey for type-safe translation keys. This prevents typos and makes refactoring easier.

Creating Keys

// Simple key creation
LangKey welcomeKey = LangKey.of("myplugin.welcome");

// Recommended: Create a key class
public final class MyPluginKeys {
    private MyPluginKeys() {}
    
    public static final LangKey WELCOME = LangKey.of("myplugin.welcome");
    public static final LangKey GOODBYE = LangKey.of("myplugin.goodbye");
    public static final LangKey ERROR_NOT_FOUND = LangKey.of("myplugin.error.not_found");
}

Translation Key Naming Convention

To avoid conflicts between plugins, use a prefix for your translation keys:

  • Good: myplugin.welcome, myplugin.command.help
  • Bad: welcome, command.help (might conflict with other plugins)

Placeholder Usage

Use placeholders {0}, {1}, etc. in your translation strings:

{
  "myplugin.welcome": "Welcome {0}!",
  "myplugin.message": "Hello {0}, you have {1} items"
}
// Using type-safe keys
LangKey welcomeKey = LangKey.of("myplugin.welcome");
LangKey messageKey = LangKey.of("myplugin.message");

String msg1 = loc.translate(welcomeKey, lang, "PlayerName");
// Result: "Welcome PlayerName!"

String msg2 = loc.translate(messageKey, lang, "PlayerName", 5);
// Result: "Hello PlayerName, you have 5 items"

Getting Player Language

To get a player's preferred language, you can use the LanguageComponent or HyPlayer:

import dev.spacetivity.tobi.hylib.hytale.api.HytaleProvider;
import dev.spacetivity.tobi.hylib.hytale.api.localization.Locale;
import dev.spacetivity.tobi.hylib.hytale.api.localization.Localization;
import dev.spacetivity.tobi.hylib.hytale.api.localization.LanguageComponent;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;

// From LanguageComponent (ECS)
LanguageComponent languageComponent = store.getComponent(playerRef, LanguageComponent.getComponentType());
Localization loc = HytaleProvider.getApi().getLocalization();
Locale locale = languageComponent != null ? languageComponent.getLanguage() : loc.getDefaultLanguage();

// Or from HyPlayer (database)
HyPlayer hyPlayer = HytaleProvider.getApi().getHyPlayerService().getOnlineHyPlayer(uuid);
Locale locale = hyPlayer != null ? hyPlayer.getLanguage() : loc.getDefaultLanguage();

Reloading Language Files

To reload all language files (useful during development):

HytaleProvider.getApi().getLocalization().reload();

Available Languages

Check which languages are available (only languages with translation files):

Set<Lang> langs = HytaleProvider.getApi().getLocalization().getAvailableLanguages();
// Returns: Set of Lang objects for langs that have translation files

// Create a Lang instance for a specific code
HytaleApi api = HytaleProvider.getApi();
Lang english = api.newLang("en");
Lang german = api.newLang("de");

Cache API

In-Memory Cache

import dev.spacetivity.tobi.hylib.database.api.cache.AbstractInMemoryCache;

public class LocalUserCache extends AbstractInMemoryCache<String, User> {
    // Optional: Custom Logic
}

// Register
CacheLoader cacheLoader = DatabaseProvider.getApi().getCacheLoader();
LocalUserCache cache = new LocalUserCache();
cacheLoader.register(cache);

// Usage
cache.insert("key", user);
User user = cache.getValue("key");
cache.remove("key");

Best Practices

1. Column Definitions as Constants

public class UserRepository extends AbstractMariaDbRepository<User> {
    private static final Column ID_COL = Column.of("id");
    private static final Column NAME_COL = Column.of("name");
    // ...
}

2. Explicit Column Lists Instead of SELECT *

// Good
SqlBuilder.select(ID_COL, NAME_COL, EMAIL_COL).from(table)

// Bad
SqlBuilder.select(Column.of("*")).from(table)  // Not supported

3. Reuse RowMapper

// Good: Method Reference
queryOne(query, this::deserializeResultSet)

// Also good: Lambda
queryOne(query, rs -> new User(rs.getInt("id"), rs.getString("name")))

4. TableDefinition Only for Schema Generation

// TableDefinition only in constructor for generate()
super(db, TableDefinition.create(connection, table, columns...));

// For queries: Use Table identifier
Table usersTable = Table.of("users");

Important Notes

  • DatabaseProvider.getApi() throws IllegalStateException if no instance has been registered
  • HytaleProvider.getApi().newCodec() requires HytaleApi to be initialized first
  • DatabaseConnector#getSafeConnection() throws NullPointerException if no connection exists
  • Dependencies for MariaDB are compileOnly - must be provided at runtime
  • Table/Column identifiers are validated at compile time
  • Query Builder always creates PreparedStatements (parameter binding)
  • ResultSet is automatically closed correctly (try-with-resources)

Dependencies

Runtime Dependencies

runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.0.7'
runtimeOnly 'com.zaxxer:HikariCP:5.0.1'
runtimeOnly 'com.google.code.gson:gson:2.10.1'

Config API Dependencies

For using the Config API in your project:

dependencies {
    // Database API
    implementation("dev.spacetivity.tobi.hylib.database:database-api:1.0-SNAPSHOT")
    
    // Hytale Server (für BuilderCodec und Config)
    compileOnly("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4")
    
    // Lombok (optional, aber empfohlen für Getter/Setter)
    compileOnly("org.projectlombok:lombok:...")
    annotationProcessor("org.projectlombok:lombok:...")
}

License

See LICENSE file in the project root.

About

A type-safe, SQL-injection-resistant database API

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages