diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java index 7c239fd..298bec0 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApi.java @@ -1,59 +1,10 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.user.UserService; -import org.jetbrains.annotations.NotNull; -/** - * Central API contract for interacting with the PlayTime plugin’s core services. - * - *

This interface provides unified access to the main subsystems of the plugin:

- * - * - * - *

External plugins can use this interface to integrate with PlayTime features - * without depending on internal implementation details. The implementation is provided - * automatically by the PlayTime plugin during runtime initialization.

- * - *

Usage Example:

- * - *
{@code
- * PlayTimeApi api = PlayTimeApiProvider.get();
- *
- * UserService userService = api.userService();
- * PlaytimeService playtimeService = api.playtimeService();
- *
- * UUID uuid = player.getUniqueId();
- * UserTime time = playtimeService.getTime(uuid);
- * }
- * - * @see PlaytimeService - * @see com.github.imdmk.playtime.user.UserService - * @see com.github.imdmk.playtime.user.UserTime - */ public interface PlayTimeApi { - /** - * Returns the {@link UserService}, which provides access to user-management operations - * such as creating, saving, and retrieving user data including playtime, - * ranks, and metadata. - * - * @return non-null {@link UserService} instance - */ - @NotNull UserService userService(); + UserService getUserService(); - /** - * Returns the {@link PlaytimeService}, which provides high-level operations for - * retrieving and modifying player playtime data. - * - *

This service acts as the bridge between the plugin’s internal user model - * and the underlying storage or platform-specific systems.

- * - * @return non-null {@link PlaytimeService} instance - */ - @NotNull PlaytimeService playtimeService(); + PlayTimeService getPlayTimeService(); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java index 0a1ff3c..526a273 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeApiProvider.java @@ -2,11 +2,6 @@ import org.jetbrains.annotations.NotNull; -/** - * Static access point for the {@link PlayTimeApi}. - *

- * Thread-safe: publication via synchronized register/unregister and a volatile reference. - */ public final class PlayTimeApiProvider { private static volatile PlayTimeApi API; // visibility across threads @@ -15,35 +10,18 @@ private PlayTimeApiProvider() { throw new UnsupportedOperationException("This class cannot be instantiated."); } - /** - * Returns the registered {@link PlayTimeApi}. - * - * @return the registered API - * @throws IllegalStateException if the API is not registered - */ - public static @NotNull PlayTimeApi get() { - PlayTimeApi api = API; + public static PlayTimeApi get() { + final PlayTimeApi api = API; if (api == null) { throw new IllegalStateException("PlayTimeAPI is not registered."); } return api; } - /** - * Checks if the API is registered - * - * @return {@code true} if the API is registered. - */ public static boolean isRegistered() { return API != null; } - /** - * Registers the {@link PlayTimeApi} instance. - * - * @param api the API instance to register - * @throws IllegalStateException if already registered - */ static synchronized void register(@NotNull PlayTimeApi api) { if (API != null) { throw new IllegalStateException("PlayTimeAPI is already registered."); @@ -51,20 +29,10 @@ static synchronized void register(@NotNull PlayTimeApi api) { API = api; } - /** - * Forces registration of the {@link PlayTimeApi} instance. - *

- * Intended for tests/bootstrap only; overwrites any existing instance. - */ static synchronized void forceRegister(@NotNull PlayTimeApi api) { API = api; } - /** - * Unregisters the {@link PlayTimeApi}. - * - * @throws IllegalStateException if no API was registered - */ static synchronized void unregister() { if (API == null) { throw new IllegalStateException("PlayTimeAPI is not registered."); diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java new file mode 100644 index 0000000..1008372 --- /dev/null +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/PlayTimeService.java @@ -0,0 +1,15 @@ +package com.github.imdmk.playtime; + +import com.github.imdmk.playtime.user.UserTime; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public interface PlayTimeService { + + UserTime getTime(@NotNull UUID uuid); + + void setTime(@NotNull UUID uuid, @NotNull UserTime time); + + void resetTime(@NotNull UUID uuid); +} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java deleted file mode 100644 index cb3895d..0000000 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/PlaytimeService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.UserTime; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -/** - * A high-level abstraction for accessing and modifying player playtime data. - *

- * Implementations of this interface are responsible for bridging between - * the plugin domain model ({@link UserTime}) and the underlying platform’s - * data source (e.g., Bukkit statistics API, database, etc.). - *

- * Playtime is typically expressed in Minecraft ticks (20 ticks = 1 second), - * but the {@link UserTime} abstraction handles conversions to and from human-readable units. - * - * @see com.github.imdmk.playtime.user.UserTime - */ -public interface PlaytimeService { - - /** - * Retrieves the total accumulated playtime for the specified player. - * - * @param uuid - * the unique identifier of the player whose playtime should be fetched; - * must not be {@code null} - * @return - * a non-null {@link UserTime} representing the player’s total playtime. - * If no playtime is recorded or the player has never joined, returns {@link UserTime#ZERO}. - * @throws NullPointerException - * if {@code uuid} is {@code null}. - */ - @NotNull UserTime getTime(@NotNull UUID uuid); - - /** - * Sets the total playtime for the specified player to the given value. - * - * @param uuid - * the unique identifier of the player whose playtime should be updated; - * must not be {@code null} - * @param time - * the new total playtime value to assign; must not be {@code null} - * @throws NullPointerException - * if {@code uuid} or {@code time} is {@code null} - */ - void setTime(@NotNull UUID uuid, @NotNull UserTime time); - - /** - * Resets the total recorded playtime of the specified player to zero. - * - * @param uuid - * the unique identifier of the player whose playtime should be reset; - * must not be {@code null} - * @throws NullPointerException - * if {@code uuid} is {@code null} - */ - void resetTime(@NotNull UUID uuid); -} diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java index ebc559a..bf83867 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/User.java @@ -2,123 +2,50 @@ import org.jetbrains.annotations.NotNull; -import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -/** - * Represents an immutable-identity player aggregate containing all tracked - * playtime-related metadata. - * - *

This class is the main domain model for player statistics and provides: - *

- * - * All numerical fields are stored in atomic structures to allow safe concurrent - * updates from asynchronous tasks (e.g., an async database writes). The name field - * is {@code volatile}, ensuring safe publication across threads. - *

- * Two {@code User} instances are considered equal if and only if their UUIDs match. - */ public final class User { - /** Permanently immutable player UUID. */ private final UUID uuid; - - /** Last known player name. Volatile for safe cross-thread publication. */ private volatile String name; - /** Total accumulated playtime in milliseconds. */ private final AtomicLong playtimeMillis; - /** - * Creates a fully initialized {@code User} instance. - * - * @param uuid unique player identifier (never null) - * @param name last known player name (never null or blank) - * @param playtime initial playtime value (never null) - */ public User(@NotNull UUID uuid, @NotNull String name, @NotNull UserTime playtime) { - Objects.requireNonNull(playtime, "playtime cannot be null"); - - this.uuid = Objects.requireNonNull(uuid, "uuid cannot be null"); - this.name = Objects.requireNonNull(name, "name cannot be null"); + this.uuid = uuid; + this.name = name; this.playtimeMillis = new AtomicLong(playtime.millis()); } - /** - * Convenience constructor for a new player with zero playtime. - * - * @param uuid unique player identifier - * @param name last known player name - */ public User(@NotNull UUID uuid, @NotNull String name) { this(uuid, name, UserTime.ZERO); } - /** - * Returns the unique identifier of this user. - * - * @return player's UUID (never null) - */ @NotNull public UUID getUuid() { return this.uuid; } - /** - * Returns the last known player name. - * - * @return name as a non-null String - */ @NotNull public String getName() { return this.name; } - /** - * Updates the stored player name. - * - * @param name the new name (non-null, non-blank) - * @throws NullPointerException if name is null - * @throws IllegalArgumentException if name is blank - */ public void setName(@NotNull String name) { - Objects.requireNonNull(name, "name cannot be null"); - if (name.trim().isEmpty()) { - throw new IllegalArgumentException("name cannot be blank"); - } this.name = name; } - /** - * Returns the total accumulated playtime as an immutable {@link UserTime} object. - * - * @return playtime value (never null) - */ @NotNull public UserTime getPlaytime() { return UserTime.ofMillis(playtimeMillis.get()); } - /** - * Replaces the stored playtime with a new value. - * - * @param playtime the new playtime (must not be null) - * @throws NullPointerException if playtime is null - */ public void setPlaytime(@NotNull UserTime playtime) { - Objects.requireNonNull(playtime, "playtime cannot be null"); playtimeMillis.set(playtime.millis()); } - /** - * Users are equal if and only if their UUIDs match. - */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -126,17 +53,11 @@ public boolean equals(Object o) { return uuid.equals(other.uuid); } - /** - * Hash code is based solely on UUID. - */ @Override public int hashCode() { return uuid.hashCode(); } - /** - * Returns a concise diagnostic string representation. - */ @Override public String toString() { return "User{" + diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java index 01e7fa5..7f8ee36 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteResult.java @@ -3,32 +3,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** - * Immutable result container representing the outcome of a user deletion attempt. - * - *

This record provides both contextual information: - * the {@link User} instance (if it existed and was deleted) and a - * {@link UserDeleteStatus} value describing the operation result.

- * - *

Usage: Always check {@link #status()} to determine the deletion outcome. - * {@link #user()} may be {@code null} if the user was not found or the operation failed.

- * - * @param user the deleted user instance, or {@code null} if the user did not exist or was not deleted - * @param status non-null result status representing the outcome of the deletion - * - * @see User - * @see UserDeleteStatus - */ public record UserDeleteResult(@Nullable User user, @NotNull UserDeleteStatus status) { - /** - * Indicates whether the deletion succeeded and the user actually existed. - *

- * This method is equivalent to checking: - *

{@code user != null && status == UserDeleteStatus.DELETED}
- * - * @return {@code true} if the user was successfully deleted; {@code false} otherwise - */ public boolean isSuccess() { return this.user != null && this.status == UserDeleteStatus.DELETED; } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java index 8c87147..efb33cc 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserDeleteStatus.java @@ -1,33 +1,7 @@ package com.github.imdmk.playtime.user; -/** - * Enumerates all possible outcomes of a user deletion request. - * - *

This enum is typically returned as part of a {@link UserDeleteResult} - * to describe whether the deletion succeeded, the user was missing, or - * an internal failure occurred during the operation.

- * - *

Usage: Used primarily by {@code UserService} or repository - * implementations to standardize deletion responses.

- * - * @see UserDeleteResult - * @see User - */ public enum UserDeleteStatus { - - /** - * The user existed and was successfully removed from persistent storage. - */ DELETED, - - /** - * The user was not present in the data source at the time of deletion. - */ NOT_FOUND, - - /** - * The deletion operation failed due to an unexpected exception, - * connectivity issue, or database constraint violation. - */ FAILED } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java index 37e44a6..64486e2 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserSaveReason.java @@ -1,47 +1,12 @@ package com.github.imdmk.playtime.user; -/** - * Describes the context in which a {@link User} instance is persisted. - * - *

These reasons help services, repositories, logging and auditing systems - * understand why a save operation took place.

- * - *

Typical usage: passed to {@code UserService#save(User, UserSaveReason)} - * to provide semantic context for persistence logic.

- * - * @see User - * @see UserService - */ public enum UserSaveReason { - /** - * The player joined the server — user data is loaded or created. - */ PLAYER_JOIN, - - /** - * The player left the server — user data should be persisted. - */ PLAYER_LEAVE, - /** - * An administrator explicitly set the user's playtime via command. - */ SET_COMMAND, - - /** - * An administrator reset the user's playtime via command. - */ RESET_COMMAND, - /** - * The user's data was persisted by a scheduled task - * (e.g., automatic save every 5 minutes). - */ - SCHEDULED_SAVE, - - /** - * The user's playtime was reset by a GUI action (e.g., button click). - */ GUI_RESET_CLICK } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java index b3ab8e5..ebc38ec 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserService.java @@ -8,109 +8,19 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -/** - * High-level service for accessing and managing {@link User} data. - *

- * Provides both cache-only (synchronous, safe for main-thread use) - * and asynchronous (database-backed) operations. - *

- * Implementations are expected to handle caching, persistence, and - * consistency automatically. - */ public interface UserService { - /** - * Finds a user by their unique UUID from the in-memory cache only. - *

- * This method is non-blocking and safe to call from the main server thread. - * - * @param uuid the user's UUID - * @return an {@link Optional} containing the user if present in cache, - * or empty if not found - */ - @NotNull Optional findCachedByUuid(@NotNull UUID uuid); + Optional findCachedByUuid(@NotNull UUID uuid); + Optional findCachedByName(@NotNull String name); + Collection getCachedUsers(); - /** - * Finds a user by their name from the in-memory cache only. - *

- * This method is non-blocking and safe to call from the main server thread. - * - * @param name the user's name (case-insensitive, depending on implementation) - * @return an {@link Optional} containing the user if present in cache, - * or empty if not found - */ - @NotNull Optional findCachedByName(@NotNull String name); + CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findByName(@NotNull String name); - /** - * Returns an unmodifiable snapshot of all users currently cached in memory. - *

- * This collection reflects a moment-in-time view and is not updated dynamically. - * Safe to call from the main thread. - * - * @return a collection of cached {@link User} objects - */ - @NotNull Collection getCachedUsers(); + CompletableFuture deleteByUuid(@NotNull UUID uuid); + CompletableFuture deleteByName(@NotNull String name); - /** - * Asynchronously finds a user by their UUID, using cache as the primary source - * and the database as fallback. - * - * @param uuid the user's UUID - * @return a {@link CompletableFuture} containing an {@link Optional} user - * when the lookup completes - */ - @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason); - /** - * Asynchronously finds a user by their name, using cache as the primary source - * and the database as fallback. - * - * @param name the user's name (case-insensitive, depending on implementation) - * @return a {@link CompletableFuture} containing an {@link Optional} user - * when the lookup completes - */ - @NotNull CompletableFuture> findByName(@NotNull String name); - - /** - * Retrieves a list of the top users sorted by spent playtime in descending order. - *

- * The result may come from the in-memory - * cached leaderboard or trigger an asynchronous refresh when the cache has expired - * or does not satisfy the requested limit. - *

- * - * @param limit the maximum number of users to return; values ≤ 0 yield an empty list - * @return a {@link CompletableFuture} that completes with a list of users ordered - * by spent time descending, either from cache or freshly queried from the repository - */ - @NotNull CompletableFuture> findTopByPlayTime(int limit); - - /** - * Asynchronously saves a user to the underlying database and updates the cache. - *

- * If the user already exists, their data is updated. - * - * @param user the user to save - * @param reason the reason of save - * @return a {@link CompletableFuture} containing the saved user - */ - @NotNull CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason); - - /** - * Asynchronously deletes a user by their UUID from both the database and cache. - * - * @param uuid the UUID of the user to delete - * @return a {@link CompletableFuture} completing with {@code true} if the user was deleted, - * or {@code false} if no such user existed - */ - @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid); - - /** - * Asynchronously deletes a user by their name from both the database and cache. - * - * @param name the name of the user to delete - * @return a {@link CompletableFuture} completing with {@code true} if the user was deleted, - * or {@code false} if no such user existed - */ - @NotNull CompletableFuture deleteByName(@NotNull String name); + CompletableFuture> findTopByPlayTime(int limit); } diff --git a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java index 2dc8502..117055a 100644 --- a/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java +++ b/playtime-api/src/main/java/com/github/imdmk/playtime/user/UserTime.java @@ -9,219 +9,91 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; -/** - * Immutable value object representing a duration of time measured in milliseconds. - * - *

This record provides convenient conversions between milliseconds, seconds, - * Bukkit ticks (1 tick = 50 ms), and {@link Duration}, as well as arithmetic - * and comparison utilities for working with user playtime or uptime data.

- * - *

Design notes:

- * - * - * @param millis the milliseconds of time - * - * @see Duration - * @see User - */ public record UserTime(long millis) implements Comparable, Serializable { @Serial private static final long serialVersionUID = 1L; - /** Constant representing zero time. */ public static final UserTime ZERO = new UserTime(0L); - /** Number of milliseconds in a single Bukkit tick (20 ticks = 1 second). */ private static final long MILLIS_PER_TICK = 50L; - - /** Number of milliseconds in one second. */ private static final long MILLIS_PER_SECOND = 1_000L; - /** - * Primary constructor that validates the provided time value. - * - * @param millis total duration in milliseconds (must be ≥ 0) - * @throws IllegalArgumentException if {@code millis} is negative - */ public UserTime { if (millis < 0L) { throw new IllegalArgumentException("UserTime millis cannot be negative"); } } - /** - * Creates a {@code UserTime} from raw milliseconds. - * - * @param millis total milliseconds - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofMillis(long millis) { return new UserTime(millis); } - /** - * Creates a {@code UserTime} from duration. - * - * @param duration a duration - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofDuration(@NotNull Duration duration) { - Objects.requireNonNull(duration, "duration cannot be null"); return ofMillis(duration.toMillis()); } - /** - * Creates a {@code UserTime} from seconds. - * - * @param seconds total seconds - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofSeconds(long seconds) { return new UserTime(seconds * MILLIS_PER_SECOND); } - /** - * Creates a {@code UserTime} from Bukkit ticks (1 tick = 50 ms). - * - * @param ticks total ticks - * @return a new {@code UserTime} instance - */ @Contract("_ -> new") public static @NotNull UserTime ofTicks(long ticks) { return new UserTime(ticks * MILLIS_PER_TICK); } - /** - * Creates a {@code UserTime} from a {@link Duration}. - * - * @param duration non-null duration to convert - * @return a new {@code UserTime} instance - * @throws NullPointerException if {@code duration} is null - */ @Contract("_ -> new") public static @NotNull UserTime from(@NotNull Duration duration) { - Objects.requireNonNull(duration, "duration cannot be null"); return new UserTime(duration.toMillis()); } - /** - * Converts this time to whole seconds (truncated). - * - * @return number of seconds contained in this duration - */ @Contract(pure = true) public long toSeconds() { - return TimeUnit.MILLISECONDS.toSeconds(this.millis); + return TimeUnit.MILLISECONDS.toSeconds(millis); } - /** - * Converts this time to Bukkit ticks (1 tick = 50 ms). - * - * @return total number of ticks represented by this time - */ @Contract(pure = true) public int toTicks() { - return Math.toIntExact(this.millis / MILLIS_PER_TICK); + return Math.toIntExact(millis / MILLIS_PER_TICK); } - /** - * Converts this instance to a {@link Duration}. - * - * @return a duration representing the same amount of time - */ @Contract(pure = true) public @NotNull Duration toDuration() { - return Duration.ofMillis(this.millis); + return Duration.ofMillis(millis); } - /** - * Returns whether this time equals zero. - * - * @return {@code true} if this duration represents zero milliseconds - */ @Contract(pure = true) public boolean isZero() { - return this.millis == 0L; + return millis == 0; } - /** - * Adds another {@code UserTime} to this one. - * - * @param other non-null {@code UserTime} to add - * @return new {@code UserTime} representing the sum - * @throws NullPointerException if {@code other} is null - * @throws ArithmeticException if overflow occurs - */ @Contract(pure = true) - public @NotNull UserTime plus(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return new UserTime(Math.addExact(this.millis, other.millis)); - } - - /** - * Subtracts another {@code UserTime} from this one. - * - * @param other non-null {@code UserTime} to subtract - * @return new {@code UserTime} representing the difference - * @throws NullPointerException if {@code other} is null - * @throws ArithmeticException if overflow occurs - */ + public UserTime plus(@NotNull UserTime other) { + return new UserTime(Math.addExact(millis, other.millis)); + } + @Contract(pure = true) - public @NotNull UserTime minus(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return new UserTime(Math.subtractExact(this.millis, other.millis)); + public UserTime minus(@NotNull UserTime other) { + return new UserTime(Math.subtractExact(millis, other.millis)); } - /** - * Returns the smaller of this and the given time. - * - * @param other non-null time to compare - * @return the smaller {@code UserTime} instance - */ @Contract(pure = true) - public @NotNull UserTime min(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return this.millis <= other.millis ? this : other; + public UserTime min(@NotNull UserTime other) { + return millis <= other.millis ? this : other; } - /** - * Returns the larger of this and the given time. - * - * @param other non-null time to compare - * @return the larger {@code UserTime} instance - */ @Contract(pure = true) - public @NotNull UserTime max(@NotNull UserTime other) { - Objects.requireNonNull(other, "other UserTime is null"); - return this.millis >= other.millis ? this : other; + public UserTime max(@NotNull UserTime other) { + return millis >= other.millis ? this : other; } - /** - * Compares this {@code UserTime} with another by their millisecond values. - * - * @param o other {@code UserTime} to compare against - * @return negative if this is less, zero if equal, positive if greater - */ @Override public int compareTo(@NotNull UserTime o) { - return Long.compare(this.millis, o.millis); + return Long.compare(millis, o.millis); } - /** - * Checks equality based on millisecond value. - * - * @param o object to compare - * @return {@code true} if the given object is a {@code UserTime} with the same millisecond value - */ @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; @@ -229,21 +101,11 @@ public boolean equals(Object o) { return compareTo(userTime) == 0; } - /** - * Returns a hash code consistent with {@link #equals(Object)}. - * - * @return hash based on the millisecond value - */ @Override public int hashCode() { return Objects.hashCode(millis); } - /** - * Returns a concise string representation suitable for logging or debugging. - * - * @return string in the format {@code "UserTime{millis=X}"} - */ @Override @NotNull public String toString() { diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java index 4459b8b..af31de4 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserDeleteEvent.java @@ -7,14 +7,6 @@ import java.util.Objects; -/** - * Fired after a user deletion attempt completes. - *

- * Carries a {@link UserDeleteResult} with the deleted user snapshot (if any) - * and the {@link com.github.imdmk.playtime.user.UserDeleteStatus} outcome. - *

- *

Threading: Dispatched synchronously on the main server thread.

- */ public final class UserDeleteEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); @@ -22,45 +14,20 @@ public final class UserDeleteEvent extends Event { private final UserDeleteResult result; - /** - * Creates a new {@code UserDeleteEvent}. - * - * @param result non-null deletion result - */ public UserDeleteEvent(@NotNull UserDeleteResult result) { super(ASYNC); - this.result = Objects.requireNonNull(result, "result cannot be null"); + this.result = result; } - /** - * Returns the result of the deletion operation, including the status and an - * optional snapshot of the deleted user (if available). - * - * @return non-null {@link UserDeleteResult} representing the outcome of the deletion - */ public @NotNull UserDeleteResult getResult() { return this.result; } - /** - * Returns the handler list used internally by Bukkit to register and manage - * listeners for this event. - * - * @return non-null static {@link HandlerList} for this event type - */ @Override public @NotNull HandlerList getHandlers() { return HANDLERS; } - /** - * Returns the static handler list for this event type. - *

- * This method is required by the Bukkit event framework and allows Bukkit - * to correctly map event handlers to this event class. - * - * @return non-null static {@link HandlerList} - */ public static @NotNull HandlerList getHandlerList() { return HANDLERS; } diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java deleted file mode 100644 index 3b4ebd8..0000000 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserPreSaveEvent.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -/** - * Event fired immediately before a {@link User} instance is persisted - * to the database by the plugin. - * - *

This event serves as a pre-save hook, allowing listeners to - * inspect or modify the {@link User} object before it is written to storage. - * Any changes made to the user during this event will be included in the - * final persisted representation.

- * - *

Note: This event is not cancellable. It is strictly intended - * for mutation or inspection of the user object before saving. If cancellation - * behavior is required in the future, a dedicated cancellable event should be introduced.

- * - *

Thread safety: This event is always fired synchronously on the - * main server thread.

- */ -public final class UserPreSaveEvent extends Event { - - private static final HandlerList HANDLERS = new HandlerList(); - private static final boolean ASYNC = false; - - private final User user; - private final UserSaveReason reason; - - /** - * Creates a new {@code UserPreSaveEvent}. - * - * @param user the user about to be saved (non-null) - * @param reason the context in which the save operation was triggered (non-null) - */ - public UserPreSaveEvent(@NotNull User user, @NotNull UserSaveReason reason) { - super(ASYNC); - this.user = Objects.requireNonNull(user, "user cannot be null"); - this.reason = Objects.requireNonNull(reason, "reason cannot be null"); - } - - /** - * Returns the {@link User} instance that will be persisted. - *

Modifying this object will affect the data written to storage.

- * - * @return the user associated with this event - */ - public @NotNull User getUser() { - return this.user; - } - - /** - * Returns the reason for the save operation. - * - * @return a {@link UserSaveReason} describing why the user is being saved - */ - public @NotNull UserSaveReason getReason() { - return this.reason; - } - - /** - * Returns the static handler list for this event type. - * - * @return the handler list of this event. - */ - @Override - public @NotNull HandlerList getHandlers() { - return HANDLERS; - } - - /** - * Returns the static handler list for this event type. - * - * @return the list of handlers for this event. - */ - public static @NotNull HandlerList getHandlerList() { - return HANDLERS; - } -} diff --git a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java index 35befe5..0c46208 100644 --- a/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java +++ b/playtime-bukkit-api/src/main/java/com/github/imdmk/playtime/UserSaveEvent.java @@ -6,73 +6,33 @@ import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; -import java.util.Objects; - -/** - * Called whenever a {@link User} instance is saved by the plugin. - * - *

This event is fired after user data has been persisted. - * It can be canceled to prevent later operations that depend on the save, - * if applicable within the plugin's logic.

- * - *

Thread safety: This event is always fired synchronously on the main server thread.

- */ public final class UserSaveEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); - private static final boolean ASYNC = false; + private static final boolean ASYNC = true; private final User user; private final UserSaveReason reason; - /** - * Constructs a new {@code UserSaveEvent}. - * - * @param user the user that was saved (non-null) - * @param reason the reason of user save - */ public UserSaveEvent(@NotNull User user, @NotNull UserSaveReason reason) { super(ASYNC); - this.user = Objects.requireNonNull(user, "user cannot be null"); - this.reason = Objects.requireNonNull(reason, "reason cannot be null"); + this.user = user; + this.reason = reason; } - /** - * Returns the {@link User} associated with this event. - * - * @return non-null user involved in this event - */ public @NotNull User getUser() { return this.user; } - /** - * Returns the reason why this save operation was triggered. - * - * @return non-null {@link UserSaveReason} describing the save cause - */ public @NotNull UserSaveReason getReason() { return this.reason; } - /** - * Returns the handler list used internally by Bukkit to manage event listeners. - * - * @return non-null static {@link HandlerList} for this event type - */ @Override public @NotNull HandlerList getHandlers() { return HANDLERS; } - /** - * Returns the static handler list for this event type. - *

- * This method is required by the Bukkit event system and is used - * to register and manage listeners for this event. - * - * @return non-null static {@link HandlerList} - */ public static @NotNull HandlerList getHandlerList() { return HANDLERS; } diff --git a/playtime-core/build.gradle.kts b/playtime-core/build.gradle.kts index 918ecdb..2fdc0bb 100644 --- a/playtime-core/build.gradle.kts +++ b/playtime-core/build.gradle.kts @@ -13,6 +13,9 @@ dependencies { // Dynamic dependency loader implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT") + // Reflections + implementation("org.reflections:reflections:0.10.2") + // Multification implementation("com.eternalcode:multification-bukkit:1.2.3") implementation("com.eternalcode:multification-okaeri:1.2.3") diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java index c66e1be..ece0e34 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeApiAdapter.java @@ -1,21 +1,10 @@ package com.github.imdmk.playtime; import com.github.imdmk.playtime.user.UserService; -import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; @Inject record PlayTimeApiAdapter( - @NotNull UserService userService, - @NotNull PlaytimeService playtimeService) implements PlayTimeApi { - - @Override - public @NotNull UserService userService() { - return userService; - } - - @Override - public @NotNull PlaytimeService playtimeService() { - return playtimeService; - } -} + UserService getUserService, + PlayTimeService getPlayTimeService +) implements PlayTimeApi {} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java deleted file mode 100644 index 1bacd48..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimeBinder.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.infrastructure.injector.Bind; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; - -/** - * Discovers fields in {@link PlayTimePlugin} annotated with {@link Bind} - * and registers their instances into the DI {@link Resources}. - *

- * This approach keeps {@link PlayTimePlugin} focused on lifecycle/bootstrap logic - * while delegating dependency wiring to a dedicated, reflection-based binder. - * Only non-static fields with {@code @Bind} are processed. - */ -final class PlayTimeBinder { - - private final PlayTimePlugin core; - - /** - * Creates a new binder for the given plugin instance. - * - * @param core the plugin root object providing core dependencies - */ - PlayTimeBinder(@NotNull PlayTimePlugin core) { - this.core = Validator.notNull(core, "core"); - } - - /** - * Scans the {@link PlayTimePlugin} class hierarchy, locates fields annotated with - * {@link Bind}, reads their values, and registers them into the provided - * {@link Resources} instance. - * - * @param resources DI container resources to bind into - */ - void bind(@NotNull Resources resources) { - Validator.notNull(resources, "resources"); - - Class type = core.getClass(); - - while (type != null && type != Object.class) { - for (Field field : type.getDeclaredFields()) { - if (!field.isAnnotationPresent(Bind.class)) { - continue; - } - - if (Modifier.isStatic(field.getModifiers())) { - continue; - } - - field.setAccessible(true); - - final Object value; - try { - value = field.get(core); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Failed to access @BindCore field: " + field, e); - } - - if (value == null) { - throw new IllegalStateException("@BindCore field " + field + " is null during binding"); - } - - resources.on(field.getType()).assignInstance(value); - } - - type = type.getSuperclass(); - } - - // Provide Injector via lazy supplier - resources.on(Injector.class).assignInstance(() -> core.injector); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java index a6311bf..2ff80fc 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/PlayTimePlugin.java @@ -1,211 +1,48 @@ package com.github.imdmk.playtime; -import com.eternalcode.multification.notice.Notice; -import com.github.imdmk.playtime.config.ConfigManager; -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.config.InjectorConfigBinder; -import com.github.imdmk.playtime.config.PluginConfig; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; -import com.github.imdmk.playtime.infrastructure.database.DatabaseManager; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; -import com.github.imdmk.playtime.infrastructure.injector.Bind; -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.ModuleContext; -import com.github.imdmk.playtime.infrastructure.module.ModuleInitializer; -import com.github.imdmk.playtime.infrastructure.module.ModuleRegistry; -import com.github.imdmk.playtime.message.MessageConfig; -import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.platform.events.BukkitEventCaller; -import com.github.imdmk.playtime.platform.events.BukkitListenerRegistrar; -import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import com.github.imdmk.playtime.platform.litecommands.InvalidUsageHandlerImpl; -import com.github.imdmk.playtime.platform.litecommands.MissingPermissionsHandlerImpl; -import com.github.imdmk.playtime.platform.litecommands.NoticeResultHandlerImpl; +import com.github.imdmk.playtime.injector.ComponentManager; +import com.github.imdmk.playtime.injector.priority.AnnotationPriorityProvider; +import com.github.imdmk.playtime.injector.processor.ComponentProcessor; +import com.github.imdmk.playtime.injector.processor.ComponentProcessors; +import com.github.imdmk.playtime.injector.subscriber.LocalPublisher; +import com.github.imdmk.playtime.injector.subscriber.Publisher; import com.github.imdmk.playtime.platform.logger.BukkitPluginLogger; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapter; -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapterFactory; -import com.github.imdmk.playtime.platform.scheduler.BukkitTaskScheduler; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.google.common.base.Stopwatch; -import dev.rollczi.litecommands.LiteCommands; -import dev.rollczi.litecommands.LiteCommandsBuilder; -import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; -import org.bstats.bukkit.Metrics; import org.bukkit.Server; -import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.DependencyInjection; import org.panda_lang.utilities.inject.Injector; -import java.sql.SQLException; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.ExecutorService; +import java.io.File; -/** - * Main runtime bootstrap for the PlayTime plugin. - * Threading note: heavy I/O offloaded to {@link ExecutorService}. - */ final class PlayTimePlugin { - private static final String PREFIX = "AdvancedPlayTime"; - private static final int PLUGIN_METRICS_ID = 19362; + //private static final String PREFIX = "AdvancedPlayTime"; + //private static final int PLUGIN_METRICS_ID = 19362; - @Bind private final ModuleRegistry moduleRegistry = new ModuleRegistry(); + private final Injector injector; - @Bind private final Plugin plugin; - @Bind private final PluginLogger logger; - @Bind private final Server server; - @Bind private final ExecutorService executor; - - @Bind private ConfigManager configManager; - - @Bind private DatabaseManager databaseManager; - @Bind private RepositoryContext repositoryContext; - @Bind private RepositoryManager repositoryManager; - - @Bind private MessageService messageService; - @Bind private TaskScheduler taskScheduler; - @Bind private BukkitEventCaller eventCaller; - @Bind private BukkitListenerRegistrar listenerRegistrar; - @Bind private GuiRegistry guiRegistry; - @Bind private PlaceholderAdapter placeholderAdapter; - - @Bind private LiteCommandsBuilder liteCommandsBuilder; - @Bind private LiteCommands liteCommands; - - private Metrics metrics; - - Injector injector; - - PlayTimePlugin( - @NotNull Plugin plugin, - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull ExecutorService executor - ) { - this.plugin = Validator.notNull(plugin, "plugin"); - this.server = Validator.notNull(server, "server"); - this.logger = Validator.notNull(logger, "logger"); - this.executor = Validator.notNull(executor, "executorService"); - } - - PlayTimePlugin(@NotNull Plugin plugin, @NotNull ExecutorService executor) { - this(plugin, plugin.getServer(), new BukkitPluginLogger(plugin), executor); - } - - void enable( - @NotNull List> enabledConfigs, - @NotNull List> enabledModules - ) { - Validator.notNull(enabledConfigs, "enabledConfigs"); - Validator.notNull(enabledModules, "enabled modules"); - - final Stopwatch stopwatch = Stopwatch.createStarted(); - - // Configuration - configManager = new ConfigManager(logger, plugin.getDataFolder()); - configManager.createAll(enabledConfigs); - - // Duration format style - final PluginConfig pluginConfig = configManager.require(PluginConfig.class); - Durations.setDefaultFormatStyle(pluginConfig.durationFormatStyle); - - // Database - final DatabaseConfig databaseConfig = configManager.require(DatabaseConfig.class); - databaseManager = new DatabaseManager(plugin, logger, databaseConfig); - - databaseManager.loadDriver(); - try { - databaseManager.connect(); - } catch (SQLException e) { - logger.error(e, "An error occurred while trying to start all repositories. Disabling plugin..."); - plugin.getPluginLoader().disablePlugin(plugin); - throw new IllegalStateException("Repository startup failed", e); - } - - // Infrastructure services - repositoryContext = new RepositoryContext(executor); - repositoryManager = new RepositoryManager(logger); - - final MessageConfig messageConfig = configManager.require(MessageConfig.class); - messageService = new MessageService(messageConfig, BukkitAudiences.create(plugin)); - - taskScheduler = new BukkitTaskScheduler(plugin, server.getScheduler()); - eventCaller = new BukkitEventCaller(server, taskScheduler); - listenerRegistrar = new BukkitListenerRegistrar(plugin); - guiRegistry = new GuiRegistry(); - placeholderAdapter = PlaceholderAdapterFactory.createFor(plugin, server, logger); - - liteCommandsBuilder = LiteBukkitFactory.builder(PREFIX, plugin, server); - liteCommandsBuilder - .invalidUsage(new InvalidUsageHandlerImpl(messageService)) - .missingPermission(new MissingPermissionsHandlerImpl(messageService)) - .result(Notice.class, new NoticeResultHandlerImpl(messageService)); - - // Dependency Injection + PlayTimePlugin(@NotNull Plugin plugin) { injector = DependencyInjection.createInjector(resources -> { - new PlayTimeBinder(this).bind(resources); - InjectorConfigBinder.bind(resources, configManager.getConfigs()); + resources.on(Plugin.class).assignInstance(plugin); + resources.on(Server.class).assignInstance(plugin.getServer()); + resources.on(File.class).assignInstance(plugin.getDataFolder()); + resources.on(PluginLogger.class).assignInstance(new BukkitPluginLogger(plugin)); }); - // Module initialization - final ModuleContext context = injector.newInstance(ModuleContext.class); - final ModuleInitializer initializer = new ModuleInitializer(context, moduleRegistry, injector); + injector.getResources().on(Publisher.class).assignInstance(new LocalPublisher(injector)); - initializer.loadAndSort(enabledModules); - initializer.bindAll(); - initializer.initAll(); - initializer.registerRepositories(); + ComponentManager componentManager = new ComponentManager(injector, this.getClass().getPackageName()) + .setPriorityProvider(new AnnotationPriorityProvider()); - // Start repositories - Validator.ifNotNull(databaseManager.getConnection(), connection -> { - try { - repositoryManager.startAll(connection); - } catch (SQLException e) { - logger.error(e, "An error occurred while trying to start all repositories. Disabling plugin..."); - plugin.getPluginLoader().disablePlugin(plugin); - throw new IllegalStateException("Repository startup failed", e); - } - }); - - // Activate all feature modules - initializer.activateFeatures(); - - // Build commands - liteCommands = liteCommandsBuilder.build(); - - // Metrics - metrics = new Metrics(plugin, PLUGIN_METRICS_ID); + for (ComponentProcessor processor : injector.invokeMethod(ComponentProcessors.defaults())) - // API - final PlayTimeApiAdapter api = injector.newInstance(PlayTimeApiAdapter.class); - PlayTimeApiProvider.register(api); + componentManager.scanAll(); + componentManager.processAll(); - final Duration elapsed = stopwatch.stop().elapsed(); - logger.info("%s plugin enabled in %s ms", PREFIX, elapsed.toMillis()); } void disable() { - Validator.ifNotNull(configManager, (manager) -> { - manager.saveAll(); - manager.clearAll(); - }); - Validator.ifNotNull(repositoryManager, RepositoryManager::close); - Validator.ifNotNull(databaseManager, DatabaseManager::shutdown); - Validator.ifNotNull(messageService, MessageService::shutdown); - Validator.ifNotNull(taskScheduler, TaskScheduler::shutdown); - Validator.ifNotNull(liteCommands, LiteCommands::unregister); - Validator.ifNotNull(metrics, Metrics::shutdown); - - PlayTimeApiProvider.unregister(); - - logger.info("%s plugin disabled successfully.", PREFIX); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigBinder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigBinder.java deleted file mode 100644 index f0b2f6f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigBinder.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.imdmk.playtime.config; - -import com.github.imdmk.playtime.shared.validate.Validator; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import eu.okaeri.configs.serdes.commons.SerdesCommons; -import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; -import org.jetbrains.annotations.NotNull; - -import java.io.File; - -final class ConfigBinder { - - void bind(@NotNull ConfigSection config, @NotNull File file) { - Validator.notNull(config, "config"); - Validator.notNull(file, "file"); - - final OkaeriSerdesPack serdesPack = config.getSerdesPack(); - final YamlSnakeYamlConfigurer yamlConfigurer = YamlConfigurerFactory.create(); - - config.withConfigurer(yamlConfigurer) - .withSerdesPack(serdesPack) - .withSerdesPack(new SerdesCommons()) - .withBindFile(file) - .withRemoveOrphans(true); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java new file mode 100644 index 0000000..1ef369d --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigConfigurer.java @@ -0,0 +1,24 @@ +package com.github.imdmk.playtime.config; + +import eu.okaeri.configs.serdes.OkaeriSerdesPack; +import eu.okaeri.configs.serdes.commons.SerdesCommons; +import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +final class ConfigConfigurer { + + void configure( + @NotNull ConfigSection config, + @NotNull File file, + OkaeriSerdesPack... serdesPacks + ) { + final YamlSnakeYamlConfigurer configurer = new YamlSnakeYamlConfigurer(YamlFactory.create()); + + config.withConfigurer(configurer, serdesPacks) + .withSerdesPack(new SerdesCommons()) + .withBindFile(file) + .withRemoveOrphans(true); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java index 2327c7a..efaae08 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigFactory.java @@ -1,15 +1,13 @@ package com.github.imdmk.playtime.config; -import com.github.imdmk.playtime.shared.validate.Validator; import eu.okaeri.configs.ConfigManager; +import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; final class ConfigFactory { @NotNull T instantiate(@NotNull Class type) { - Validator.notNull(type, "type"); - try { return ConfigManager.create(type); } catch (OkaeriException e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java index 78097f8..7698030 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigLifecycle.java @@ -1,7 +1,7 @@ package com.github.imdmk.playtime.config; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; +import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.exception.OkaeriException; import org.jetbrains.annotations.NotNull; @@ -10,7 +10,7 @@ final class ConfigLifecycle { private final PluginLogger logger; ConfigLifecycle(@NotNull PluginLogger logger) { - this.logger = Validator.notNull(logger, "logger"); + this.logger = logger; } void initialize(@NotNull ConfigSection config) { @@ -30,7 +30,7 @@ void load(@NotNull ConfigSection config) { void save(@NotNull ConfigSection config) { try { config.save(); - } catch (Exception e) { + } catch (OkaeriException e) { logger.error(e, "Failed to save config %s", config.getClass().getSimpleName()); throw new ConfigAccessException(e); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java index 8aa1d87..2a6cfa8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigSection.java @@ -4,34 +4,10 @@ import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; -/** - * Abstract base class for configuration sections. - * - *

- * Extends {@link OkaeriConfig} to provide a reusable foundation for plugin - * configuration sections. Subclasses are required to specify the - * serialization/deserialization pack and the configuration file name. - *

- * - *

- * Supports automatic recursive loading of nested {@link ConfigSection} - * subclasses declared as fields inside this class. - *

- */ public abstract class ConfigSection extends OkaeriConfig { - /** - * Returns the {@link OkaeriSerdesPack} instance used for serializing and deserializing - * this configuration section. - * - * @return non-null serialization/deserialization pack - */ - public abstract @NotNull OkaeriSerdesPack getSerdesPack(); + public abstract @NotNull OkaeriSerdesPack serdesPack(); + + public abstract @NotNull String fileName(); - /** - * Returns the filename (including extension) used to persist this configuration section. - * - * @return non-null configuration file name - */ - public abstract @NotNull String getFileName(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java similarity index 70% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigManager.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java index 017ac9c..c67c2ee 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigManager.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigService.java @@ -1,18 +1,21 @@ package com.github.imdmk.playtime.config; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; +import eu.okaeri.configs.OkaeriConfig; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; +import org.panda_lang.utilities.inject.annotations.Inject; import java.io.File; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -public final class ConfigManager { +@Service(priority = Priority.LOWEST) +public final class ConfigService { private final Set configs = ConcurrentHashMap.newKeySet(); private final Map, ConfigSection> byType = new ConcurrentHashMap<>(); @@ -20,40 +23,36 @@ public final class ConfigManager { private final File dataFolder; private final ConfigFactory factory; - private final ConfigBinder binder; + private final ConfigConfigurer configurer; private final ConfigLifecycle lifecycle; - public ConfigManager(@NotNull PluginLogger logger, @NotNull File dataFolder) { - this.dataFolder = Validator.notNull(dataFolder, "dataFolder"); + @Inject + public ConfigService(@NotNull PluginLogger logger, @NotNull File dataFolder) { + this.dataFolder = dataFolder; this.factory = new ConfigFactory(); - this.binder = new ConfigBinder(); + this.configurer = new ConfigConfigurer(); this.lifecycle = new ConfigLifecycle(logger); } public @NotNull T create(@NotNull Class type) { final T config = factory.instantiate(type); - final File file = new File(dataFolder, config.getFileName()); + final File file = new File(dataFolder, config.fileName()); - binder.bind(config, file); + configurer.configure(config, file); lifecycle.initialize(config); register(type, config); return config; } - public void createAll(@NotNull List> types) { - types.forEach(this::create); - } - @SuppressWarnings("unchecked") public T get(@NotNull Class type) { return (T) byType.get(type); } public @NotNull T require(@NotNull Class type) { - T config = get(type); - + final T config = get(type); if (config == null) { throw new IllegalStateException("Config not created: " + type.getName()); } @@ -69,11 +68,13 @@ public void saveAll() { configs.forEach(lifecycle::save); } - public @NotNull @Unmodifiable Set getConfigs() { + @NotNull + @Unmodifiable + public Set getConfigs() { return Collections.unmodifiableSet(configs); } - public void clearAll() { + public void shutdown() { configs.clear(); byType.clear(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java deleted file mode 100644 index c1ce395..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/InjectorConfigBinder.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.imdmk.playtime.config; - -import eu.okaeri.configs.OkaeriConfig; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Resources; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.Set; - -public final class InjectorConfigBinder { - - private InjectorConfigBinder() { - throw new UnsupportedOperationException("This is utility class and cannot be instantiated."); - } - - public static void bind(@NotNull Resources resources, @NotNull Set sections) { - final Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); - for (final var section : sections) { - bindRecursive(resources, section, visited); - } - } - - private static void bindRecursive(@NotNull Resources resources, @NotNull Object object, @NotNull Set visited) { - if (!visited.add(object)) { - return; - } - - resources.on(object.getClass()).assignInstance(object); - - for (Class clazz = object.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) { - for (Field field : clazz.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) { - continue; - } - - try { - field.setAccessible(true); - Object value = field.get(object); - if (value instanceof OkaeriConfig nested) { - bindRecursive(resources, nested, visited); - } - } catch (IllegalAccessException e) { - throw new IllegalStateException("Failed to bind config field: " - + clazz.getSimpleName() + "#" + field.getName(), e); - } - } - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java deleted file mode 100644 index b135f37..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/PluginConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.config; - -import com.github.imdmk.playtime.shared.time.DurationFormatStyle; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -public final class PluginConfig extends ConfigSection { - - @Comment({ - "#", - "# Determines how durations (playtime, cooldowns, timers) are formatted", - "#", - "# Available options:", - "# - COMPACT: short form like 3d 4h 10m", - "# - LONG: full names, e.g. 3 days 4 hours", - "# - LONG_WITH_AND: natural flow, e.g. 3 days and 4 hours", - "# - NATURAL: comma-separated, e.g. 3 days, 4 hours", - "#" - }) - public DurationFormatStyle durationFormatStyle = DurationFormatStyle.LONG_WITH_AND; - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> {}; - } - - @Override - public @NotNull String getFileName() { - return "pluginConfig.yml"; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java similarity index 69% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlConfigurerFactory.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java index a00409a..d2e31e2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlConfigurerFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlFactory.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.config; -import eu.okaeri.configs.yaml.snakeyaml.YamlSnakeYamlConfigurer; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -8,13 +7,13 @@ import org.yaml.snakeyaml.representer.Representer; import org.yaml.snakeyaml.resolver.Resolver; -final class YamlConfigurerFactory { +final class YamlFactory { - private YamlConfigurerFactory() { + private YamlFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - static YamlSnakeYamlConfigurer create() { + static Yaml create() { final LoaderOptions loader = new LoaderOptions(); loader.setAllowRecursiveKeys(false); loader.setMaxAliasesForCollections(50); @@ -26,11 +25,10 @@ static YamlSnakeYamlConfigurer create() { options.setIndent(2); options.setSplitLines(false); - final Representer representer = new ConfigRepresenter(options); + final Representer representer = new YamlRepresenter(options); final Resolver resolver = new Resolver(); - final Yaml yaml = new Yaml(constructor, representer, options, loader, resolver); - return new YamlSnakeYamlConfigurer(yaml); + return new Yaml(constructor, representer, options, loader, resolver); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigRepresenter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlRepresenter.java similarity index 95% rename from playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigRepresenter.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlRepresenter.java index 0dd3c67..0e5e41c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/config/ConfigRepresenter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/config/YamlRepresenter.java @@ -11,9 +11,9 @@ import java.util.LinkedHashMap; import java.util.Map; -final class ConfigRepresenter extends Representer { +final class YamlRepresenter extends Representer { - ConfigRepresenter(DumperOptions options) { + YamlRepresenter(DumperOptions options) { super(options); this.representers.put(String.class, new RepresentString()); this.representers.put(Boolean.class, new RepresentBoolean()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java new file mode 100644 index 0000000..6d4dab0 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceConnector.java @@ -0,0 +1,74 @@ +package com.github.imdmk.playtime.database; + +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurer; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.j256.ormlite.jdbc.DataSourceConnectionSource; +import com.j256.ormlite.support.ConnectionSource; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.sql.SQLException; + +final class DataSourceConnector { + + private final PluginLogger logger; + + private final DataSourceFactory dataSourceFactory; + private final DataSourceConfigurer dataSourceConfigurer; + + private volatile HikariDataSource dataSource; + private volatile ConnectionSource connectionSource; + + DataSourceConnector( + @NotNull PluginLogger logger, + @NotNull DataSourceFactory dataSourceFactory, + @NotNull DataSourceConfigurer dataSourceConfigurer + ) { + this.logger = logger; + this.dataSourceFactory = dataSourceFactory; + this.dataSourceConfigurer = dataSourceConfigurer; + } + + synchronized void connect(@NotNull DatabaseConfig config, @NotNull File dataFolder) throws SQLException { + if (dataSource != null || connectionSource != null) { + throw new IllegalStateException("DataSource is already connected"); + } + + final HikariDataSource dataSource = dataSourceFactory.create(config); + + dataSourceConfigurer.configure(dataSource, config, dataFolder); + if (dataSource.getJdbcUrl() == null) { + throw new IllegalStateException("JDBC URL was not set by DataSourceConfigurer"); + } + + final ConnectionSource connectionSource = new DataSourceConnectionSource(dataSource, dataSource.getJdbcUrl()); + + this.dataSource = dataSource; + this.connectionSource = connectionSource; + + logger.info("Connected to %s database.", config.databaseMode); + } + + synchronized void close() { + if (connectionSource != null) { + try { + connectionSource.close(); + } catch (Exception ignored) {} + } + + if (dataSource != null) { + dataSource.close(); + } + + connectionSource = null; + dataSource = null; + } + + @Nullable + ConnectionSource getConnectionSource() { + return connectionSource; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java new file mode 100644 index 0000000..23b3525 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DataSourceFactory.java @@ -0,0 +1,44 @@ +package com.github.imdmk.playtime.database; + +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +final class DataSourceFactory { + + static final String POOL_NAME = "playtime-db-pool"; + + static final int MAX_POOL_SIZE = Math.max(4, Runtime.getRuntime().availableProcessors()); + static final int MIN_IDLE = 0; + + static final int CONNECTION_TIMEOUT = 10_000; + static final int IDLE_TIMEOUT = 60_000; + static final int MAX_LIFETIME = 600_000; + + static final Map SOURCE_PROPERTIES = Map.of( + "cachePrepStmts", true, + "prepStmtCacheSize", 250, + "prepStmtCacheSqlLimit", 2048, + "useServerPrepStmts", true + ); + + HikariDataSource create(@NotNull DatabaseConfig config) { + final HikariDataSource dataSource = new HikariDataSource(); + dataSource.setPoolName(POOL_NAME); + + dataSource.setUsername(config.databaseUserName); + dataSource.setPassword(config.databasePassword); + + dataSource.setMaximumPoolSize(MAX_POOL_SIZE); + dataSource.setMinimumIdle(MIN_IDLE); + + dataSource.setConnectionTimeout(CONNECTION_TIMEOUT); + dataSource.setIdleTimeout(IDLE_TIMEOUT); + dataSource.setMaxLifetime(MAX_LIFETIME); + + SOURCE_PROPERTIES.forEach(dataSource::addDataSourceProperty); + return dataSource; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java new file mode 100644 index 0000000..5b64907 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseBootstrap.java @@ -0,0 +1,67 @@ +package com.github.imdmk.playtime.database; + +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurer; +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; +import com.github.imdmk.playtime.database.library.DriverLibraryLoader; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.j256.ormlite.support.ConnectionSource; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.panda_lang.utilities.inject.annotations.Inject; +import org.panda_lang.utilities.inject.annotations.PostConstruct; + +import java.io.File; +import java.sql.SQLException; + +@Service(priority = Priority.NORMAL) +public final class DatabaseBootstrap { + + private final File dataFolder; + private final DatabaseConfig config; + + private final DriverLibraryLoader libraryLoader; + private final DataSourceConnector dataConnector; + + @Inject + public DatabaseBootstrap( + @NotNull Plugin plugin, + @NotNull File dataFolder, + @NotNull PluginLogger logger, + @NotNull DatabaseConfig config + ) { + this.dataFolder = dataFolder; + this.config = config; + + this.libraryLoader = new DriverLibraryLoader(plugin); + + final DataSourceConfigurer configurer = DataSourceConfigurerFactory.getFor(config.databaseMode); + final DataSourceFactory factory = new DataSourceFactory(); + this.dataConnector = new DataSourceConnector(logger, factory, configurer); + } + + @PostConstruct + public void start() { + libraryLoader.loadFor(config.databaseMode); + + try { + dataConnector.connect(config, dataFolder); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Nullable + public ConnectionSource getConnection() { + return dataConnector.getConnectionSource(); + } + + @Subscribe(event = PlayTimeShutdownEvent.class) + public void shutdown() { + dataConnector.close(); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java similarity index 81% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConfig.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java index 58c6d18..6b22e19 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseConfig.java @@ -1,18 +1,12 @@ -package com.github.imdmk.playtime.infrastructure.database; +package com.github.imdmk.playtime.database; import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; -/** - * Configuration for the database connection layer. - *

- * Supports both embedded (SQLite/H2) and server-based engines (MySQL, MariaDB, PostgreSQL, SQL Server). - * Depending on {@link DatabaseMode}, only a subset of fields is used. - *

- * All unused fields for a given mode are safely ignored by the connector. - */ +@ConfigFile public final class DatabaseConfig extends ConfigSection { @Comment({ @@ -80,12 +74,12 @@ public final class DatabaseConfig extends ConfigSection { public int port = 3306; @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> {}; } @Override - public @NotNull String getFileName() { - return "databaseConfig.yml"; + public @NotNull String fileName() { + return "database.yaml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java new file mode 100644 index 0000000..8a3f01c --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/DatabaseMode.java @@ -0,0 +1,5 @@ +package com.github.imdmk.playtime.database; + +public enum DatabaseMode { + MYSQL, MARIADB, SQLITE, POSTGRESQL, H2, SQL +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurer.java new file mode 100644 index 0000000..3fdb6f8 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurer.java @@ -0,0 +1,15 @@ +package com.github.imdmk.playtime.database.configurer; + +import com.github.imdmk.playtime.database.DatabaseConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +@FunctionalInterface +public interface DataSourceConfigurer { + + void configure(@NotNull HikariDataSource dataSource, + @NotNull DatabaseConfig config, + @NotNull File dataFolder); +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java new file mode 100644 index 0000000..762d737 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/DataSourceConfigurerFactory.java @@ -0,0 +1,31 @@ +package com.github.imdmk.playtime.database.configurer; + +import com.github.imdmk.playtime.database.DatabaseMode; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public final class DataSourceConfigurerFactory { + + private static final Map CONFIGURER_BY_MODE = Map.of( + DatabaseMode.MYSQL, new MySQLConfigurer(), + DatabaseMode.MARIADB, new MariaDBConfigurer(), + DatabaseMode.POSTGRESQL, new PostgreSQLConfigurer(), + DatabaseMode.SQLITE, new SQLiteConfigurer(), + DatabaseMode.H2, new H2Configurer(), + DatabaseMode.SQL, new SQLConfigurer() + ); + + public static @NotNull DataSourceConfigurer getFor(@NotNull DatabaseMode mode) { + final DataSourceConfigurer configurer = CONFIGURER_BY_MODE.get(mode); + if (configurer == null) { + throw new IllegalArgumentException("Unsupported database mode: " + mode); + } + + return configurer; + } + + private DataSourceConfigurerFactory() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/H2Configurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/H2Configurer.java similarity index 81% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/H2Configurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/H2Configurer.java index 05d37a0..9b94715 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/H2Configurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/H2Configurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; @@ -9,7 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; -final class H2Configurer implements DriverConfigurer { +final class H2Configurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:h2:file:%s" + ";MODE=MySQL" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MariaDBConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MariaDBConfigurer.java similarity index 75% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MariaDBConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MariaDBConfigurer.java index 2e0d70f..283af33 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MariaDBConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MariaDBConfigurer.java @@ -1,12 +1,12 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; import java.io.File; -final class MariaDBConfigurer implements DriverConfigurer { +final class MariaDBConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:mariadb://%s:%s/%s" + "?useUnicode=true" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MySQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MySQLConfigurer.java similarity index 79% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MySQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MySQLConfigurer.java index 1a81ce9..aceb877 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/MySQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/MySQLConfigurer.java @@ -1,12 +1,12 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; import java.io.File; -final class MySQLConfigurer implements DriverConfigurer { +final class MySQLConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:mysql://%s:%s/%s" + "?useSSL=false" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/PostgreSQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/PostgreSQLConfigurer.java similarity index 73% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/PostgreSQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/PostgreSQLConfigurer.java index 1dd14f0..68e75c7 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/PostgreSQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/PostgreSQLConfigurer.java @@ -1,12 +1,12 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; import java.io.File; -final class PostgreSQLConfigurer implements DriverConfigurer { +final class PostgreSQLConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:postgresql://%s:%s/%s" + "?sslmode=disable" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLConfigurer.java similarity index 74% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLConfigurer.java index 82d4fe4..c41fb9b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLConfigurer.java @@ -1,12 +1,12 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; import java.io.File; -final class SQLConfigurer implements DriverConfigurer { +final class SQLConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:sqlserver://%s:%s" + ";databaseName=%s" diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLiteConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLiteConfigurer.java similarity index 82% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLiteConfigurer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLiteConfigurer.java index 5f344c1..8fe70cf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/SQLiteConfigurer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/configurer/SQLiteConfigurer.java @@ -1,6 +1,6 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; +package com.github.imdmk.playtime.database.configurer; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; +import com.github.imdmk.playtime.database.DatabaseConfig; import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; @@ -9,7 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; -final class SQLiteConfigurer implements DriverConfigurer { +final class SQLiteConfigurer implements DataSourceConfigurer { private static final String JDBC_URL = "jdbc:sqlite:%s"; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverLibraries.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraries.java similarity index 94% rename from playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverLibraries.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraries.java index b48a4f6..aae238c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverLibraries.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraries.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.dependency; +package com.github.imdmk.playtime.database.library; import com.alessiodp.libby.Library; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraryLoader.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraryLoader.java new file mode 100644 index 0000000..fb8dd0c --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/library/DriverLibraryLoader.java @@ -0,0 +1,37 @@ +package com.github.imdmk.playtime.database.library; + +import com.alessiodp.libby.BukkitLibraryManager; +import com.alessiodp.libby.Library; +import com.github.imdmk.playtime.database.DatabaseMode; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public final class DriverLibraryLoader { + + private static final Map LIBRARIES_BY_MODE = Map.of( + DatabaseMode.MYSQL, DriverLibraries.MYSQL, + DatabaseMode.MARIADB, DriverLibraries.MARIADB, + DatabaseMode.SQLITE, DriverLibraries.SQLITE, + DatabaseMode.POSTGRESQL, DriverLibraries.POSTGRESQL, + DatabaseMode.H2, DriverLibraries.H2, + DatabaseMode.SQL, DriverLibraries.SQL + ); + + private final BukkitLibraryManager libraryManager; + + public DriverLibraryLoader(@NotNull Plugin plugin) { + this.libraryManager = new BukkitLibraryManager(plugin); + this.libraryManager.addMavenCentral(); + } + + public void loadFor(@NotNull DatabaseMode mode) { + final Library library = LIBRARIES_BY_MODE.get(mode); + if (library == null) { + throw new IllegalArgumentException("Unsupported database mode: " + mode); + } + + libraryManager.loadLibrary(library); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java new file mode 100644 index 0000000..1fae687 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/RepositoryBootstrap.java @@ -0,0 +1,11 @@ +package com.github.imdmk.playtime.database.repository; + +import java.sql.SQLException; + +public interface RepositoryBootstrap extends AutoCloseable { + + void start() throws SQLException; + + @Override + void close(); +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java new file mode 100644 index 0000000..b0b42ba --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapper.java @@ -0,0 +1,21 @@ +package com.github.imdmk.playtime.database.repository.ormlite; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.stream.Collectors; + +public interface EntityMapper { + + E toEntity(@NotNull D domain); + + D toDomain(@NotNull E entity); + + default List toDomainList(@NotNull List entities) { + return entities.stream().map(this::toDomain).collect(Collectors.toList()); + } + + default List toEntityList(@NotNull List domains) { + return domains.stream().map(this::toEntity).collect(Collectors.toList()); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java new file mode 100644 index 0000000..a9dce48 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMeta.java @@ -0,0 +1,3 @@ +package com.github.imdmk.playtime.database.repository.ormlite; + +public interface EntityMeta {} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java new file mode 100644 index 0000000..a939133 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepository.java @@ -0,0 +1,94 @@ +package com.github.imdmk.playtime.database.repository.ormlite; + +import com.github.imdmk.playtime.database.DatabaseBootstrap; +import com.github.imdmk.playtime.database.repository.RepositoryBootstrap; +import com.github.imdmk.playtime.platform.logger.PluginLogger; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.logger.Level; +import com.j256.ormlite.logger.Logger; +import com.j256.ormlite.support.ConnectionSource; +import com.j256.ormlite.table.TableUtils; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.sql.SQLException; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public abstract class OrmLiteRepository + implements RepositoryBootstrap { + + private static final Duration EXECUTE_TIMEOUT = Duration.ofSeconds(3); + + protected final PluginLogger logger; + protected final TaskScheduler scheduler; + private final DatabaseBootstrap databaseBootstrap; + + protected volatile Dao dao; + + @Inject + protected OrmLiteRepository( + @NotNull PluginLogger logger, + @NotNull TaskScheduler scheduler, + @NotNull DatabaseBootstrap databaseBootstrap + ) { + this.logger = logger; + this.scheduler = scheduler; + this.databaseBootstrap = databaseBootstrap; + configureOrmLiteLogger(); + } + + protected abstract Class entityClass(); + + protected abstract List> entitySubClasses(); + + @Override + public void start() throws SQLException { + final ConnectionSource connection = databaseBootstrap.getConnection(); + if (connection == null) { + throw new IllegalStateException("DatabaseBootstrap not started before repository initialization"); + } + + for (final Class subClass : this.entitySubClasses()) { + TableUtils.createTableIfNotExists(connection, subClass); + } + + TableUtils.createTableIfNotExists(connection, this.entityClass()); + dao = DaoManager.createDao(connection, this.entityClass()); + } + + @Override + public void close() { + final Dao current = dao; + if (current == null) { + return; + } + + dao = null; + final ConnectionSource connection = current.getConnectionSource(); + if (connection != null) { + DaoManager.unregisterDao(connection, current); + } + } + + protected CompletableFuture execute(@NotNull Supplier supplier) { + final CompletableFuture future = new CompletableFuture<>(); + scheduler.runAsync(() -> { + try { + future.complete(supplier.get()); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future.orTimeout(EXECUTE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + } + + private void configureOrmLiteLogger() { + Logger.setGlobalLogLevel(Level.ERROR); // only errors + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java deleted file mode 100644 index 7386c46..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationConfig.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.github.imdmk.playtime.feature.migration; - -import com.github.imdmk.playtime.config.ConfigSection; -import eu.okaeri.configs.annotation.Comment; -import eu.okaeri.configs.serdes.OkaeriSerdesPack; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; - -public final class MigrationConfig extends ConfigSection { - - @Comment({ - "#", - "# Enables a one-time automatic migration on the first plugin startup.", - "#", - "# How it works:", - "# - When set to true, the plugin will perform a full server migration on startup.", - "# - After the migration completes successfully, the plugin will automatically set this value to false", - "# to prevent running the same migration again on the next startup.", - "# - Set this back to true manually only if you know what you are doing and want to re-run the initial migration.", - "#" - }) - public boolean initialServerMigrationEnabled = true; - - @Comment({ - "#", - "# Maximum allowed execution time for a single migration task.", - "#", - "# If a specific migration step (e.g. processing a batch of players) exceeds this duration,", - "# it will be treated as timed-out and can be cancelled or failed.", - "#" - }) - public Duration migrationTaskTimeout = Duration.ofSeconds(5); - - @Comment({ - "#", - "# Global timeout for the entire migration process.", - "#", - "# This is a hard upper limit for all migration tasks combined. If the full migration does not finish", - "# within this time window, the process will be considered failed or aborted.", - "#" - }) - public Duration migrationGlobalTimeout = Duration.ofMinutes(2); - - @Comment({ - "#", - "# Keep-alive interval for long-running migrations.", - "#", - "# Used to periodically signal that the migration is still active and progressing,", - "# preventing it from being treated as stalled when processing large datasets.", - "#" - }) - public Duration migrationKeepAliveInterval = Duration.ofMinutes(1); - - @Comment({ - "#", - "# Maximum number of migration tasks that can run concurrently.", - "#", - "# This controls how many player/segment migrations can be processed in parallel.", - "#", - "# Recommendations:", - "# - Low values (1–2) are safer for small or heavily loaded servers.", - "# - Higher values (4+) speed up migration but may increase CPU/IO usage.", - "#" - }) - public int migrationMaxConcurrency = 3; - - @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { - return registry -> { - // No custom serializers required for this config. - }; - } - - @Override - public @NotNull String getFileName() { - return "migrationConfig.yml"; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java deleted file mode 100644 index 9e02c03..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationModule.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.imdmk.playtime.feature.migration; - -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.migrator.RepositoryPlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.BukkitPlayerProvider; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.feature.migration.runner.BlockingMigrationRunner; -import com.github.imdmk.playtime.infrastructure.module.Module; -import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class MigrationModule implements Module { - - private PlayerMigrator migrator; - private PlayerProvider provider; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(PlayerMigrator.class).assignInstance(() -> migrator); - resources.on(PlayerProvider.class).assignInstance(() -> provider); - } - - @Override - public void init(@NotNull Injector injector) {} - - @Override - public void afterRegister(@NotNull Plugin plugin, @NotNull Server server, @NotNull Injector injector) { - this.migrator = injector.newInstance(RepositoryPlayerMigrator.class); - this.provider = injector.newInstance(BukkitPlayerProvider.class); - - final BlockingMigrationRunner migrationRunner = injector.newInstance(BlockingMigrationRunner.class); - migrationRunner.execute(); - } - - @Override - public int order() { - return 10; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java deleted file mode 100644 index 9209cda..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/MigrationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.imdmk.playtime.feature.migration; - -import java.time.Duration; - -/** - * Immutable summary of a completed migration run. - * - *

This record captures aggregated statistics such as:

- *
    - *
  • total players processed,
  • - *
  • successful migrations,
  • - *
  • failed migrations,
  • - *
  • total elapsed time.
  • - *
- * - *

Instances of this record are typically created by a - * {@code MigrationRunner} implementation after completing the migration workflow.

- * - * @param total total number of players considered - * @param successful number of successful migrations - * @param failed number of failed migrations - * @param took total duration of the migration process - */ -public record MigrationResult(int total, int successful, int failed, Duration took) { - - /** - * Returns an empty result representing a migration that processed - * no players and took zero time. - * - * @return a zero-valued {@code MigrationResult} - */ - public static MigrationResult empty() { - return new MigrationResult(0, 0, 0, Duration.ZERO); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java deleted file mode 100644 index 2285ee5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/ConfigMigrationListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.listener; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; - -public final class ConfigMigrationListener implements MigrationListener { - - private final MigrationConfig config; - - public ConfigMigrationListener(@NotNull MigrationConfig config) { - this.config = Validator.notNull(config, "config cannot be null"); - } - - @Override - public void onEnd(@NotNull MigrationResult result) { - config.initialServerMigrationEnabled = false; - config.save(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java deleted file mode 100644 index 6a7b97f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/LoggerMigrationListener.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.listener; - -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -public final class LoggerMigrationListener implements MigrationListener { - - private final PluginLogger logger; - - private volatile int completed; - private volatile int total; - - public LoggerMigrationListener(@NotNull PluginLogger logger) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - } - - @Override - public void onStart(int total) { - this.total = total; - this.completed = 0; - logger.info("Starting first-time migration of %d players...", total); - } - - @Override - public void onSuccess(@NotNull OfflinePlayer player) { - incrementAndLogProgress(); - } - - @Override - public void onFailed(@NotNull OfflinePlayer player, @NotNull Throwable throwable) { - logger.warn("Migration failed for %s: %s", player.getUniqueId(), throwable.getMessage()); - incrementAndLogProgress(); - } - - @Override - public void onEnd(@NotNull MigrationResult result) { - logger.info("Migration ended: success=%d, failed=%d, took=%sms", result.successful(), result.failed(), result.took().toMillis()); - } - - private void incrementAndLogProgress() { - int done = completed + 1; - int total = Math.max(1, this.total); - int percent = (int) ((done * 100L) / total); - - if (percent % 5 == 0 || done == total) { - logger.info("Migration progress: %d%% (%d/%d)", percent, done, total); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java deleted file mode 100644 index 6fcd36a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/listener/MigrationListener.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.listener; - -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -/** - * Listener interface for receiving callbacks during a playtime data migration process. - * - *

This listener allows external components to observe and react to the - * lifecycle of a migration operation – from start, through per-player - * results, to the final completion summary.

- * - *

All methods are optional; implementations may override only the events - * they are interested in.

- */ -public interface MigrationListener { - - /** - * Called when the migration process begins. - * - * @param total total number of players scheduled for migration - */ - default void onStart(int total) {} - - /** - * Called when a player's data has been migrated successfully. - * - * @param player the offline player whose migration completed successfully - */ - default void onSuccess(@NotNull OfflinePlayer player) {} - - /** - * Called when a player's migration fails due to an unexpected error. - * - * @param player the offline player whose migration failed - * @param throwable the exception that caused the failure - */ - default void onFailed(@NotNull OfflinePlayer player, @NotNull Throwable throwable) {} - - /** - * Called when the migration process has completed for all players. - * - * @param result summary of the migration, including counts of successes, failures, - * and total processed players - */ - default void onEnd(@NotNull MigrationResult result) {} -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java deleted file mode 100644 index 7dbffd6..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/PlayerMigrator.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.migrator; - -import com.github.imdmk.playtime.user.User; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; - -/** - * Strategy interface responsible for migrating playtime data for a single - * {@link OfflinePlayer} into the plugin’s internal {@link User} domain model. - * - *

Implementations of this interface define how legacy or external data sources - * (e.g., Bukkit statistics API, flat files, third-party plugins, SQL tables) - * are translated into the unified User format used by the PlayTime system.

- * - *

Async contract:
- * The migration operation must be non-blocking and executed asynchronously. - * All heavy computation and I/O must run off the main server thread. - * The returned {@link CompletableFuture} represents the result of the migration.

- * - *

This interface is commonly used by bulk migration processes that iterate - * through all stored players and invoke this migrator per user.

- */ -@FunctionalInterface -public interface PlayerMigrator { - - /** - * Migrates playtime data for the given {@link OfflinePlayer}. - * - * @param player the offline player whose data should be migrated (never null) - * @return a future completing with the migrated {@link User} instance, - * or completing exceptionally if the migration fails - */ - CompletableFuture migrate(@NotNull OfflinePlayer player); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java deleted file mode 100644 index fbaaba5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/migrator/RepositoryPlayerMigrator.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.migrator; - -import com.github.imdmk.playtime.shared.validate.Validator; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserFactory; -import com.github.imdmk.playtime.user.repository.UserRepository; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.concurrent.CompletableFuture; - -public final class RepositoryPlayerMigrator implements PlayerMigrator { - - private final UserRepository userRepository; - private final UserFactory userFactory; - - @Inject - public RepositoryPlayerMigrator( - @NotNull UserRepository userRepository, - @NotNull UserFactory userFactory - ) { - this.userRepository = Validator.notNull(userRepository, "userRepository cannot be null"); - this.userFactory = Validator.notNull(userFactory, "userFactory cannot be null"); - } - - @Override - public CompletableFuture migrate(@NotNull OfflinePlayer player) { - final User user = userFactory.createFrom(player); - return userRepository.save(user); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java deleted file mode 100644 index 2381519..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/BukkitPlayerProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.provider; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -public final class BukkitPlayerProvider implements PlayerProvider { - - private final Server server; - - @Inject - public BukkitPlayerProvider(@NotNull Server server) { - this.server = Validator.notNull(server, "server cannot be null"); - } - - @Override - public @NotNull @Unmodifiable Collection getAllPlayers() { - return List.copyOf(Arrays.asList(server.getOfflinePlayers())); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java deleted file mode 100644 index 73826e8..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/provider/PlayerProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.provider; - -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; - -/** - * Provides access to all players that should participate in a migration run. - * - *

Typical implementations include: - *

    - *
  • fetching all known {@link OfflinePlayer} instances from Bukkit,
  • - *
  • loading players from external storage,
  • - *
  • filtering players based on custom eligibility rules.
  • - *
- * - *

The contract guarantees that the returned collection is non-null, - * but may be empty.

- */ -public interface PlayerProvider { - - /** - * Returns a collection of all players eligible for migration. - * - * @return a non-null collection containing zero or more {@link OfflinePlayer} instances - */ - @NotNull Collection getAllPlayers(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java deleted file mode 100644 index f961edf..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/AsyncMigrationRunner.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.feature.migration.listener.ConfigMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.LoggerMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; -import org.panda_lang.utilities.inject.annotations.PostConstruct; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -public final class AsyncMigrationRunner - implements MigrationRunner>, AutoCloseable { - - private static final String EXECUTOR_THREAD_NAME = "playtime-async-migration-"; - - private final PluginLogger logger; - private final MigrationConfig config; - private final PlayerProvider provider; - private final PlayerMigrator migrator; - - private ExecutorService executor; - - @Inject - public AsyncMigrationRunner( - @NotNull PluginLogger logger, - @NotNull MigrationConfig config, - @NotNull PlayerProvider provider, - @NotNull PlayerMigrator migrator - ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.config = Validator.notNull(config, "config cannot be null"); - this.provider = Validator.notNull(provider, "provider cannot be null"); - this.migrator = Validator.notNull(migrator, "migrator cannot be null"); - } - - @PostConstruct - void postConstruct() { - this.executor = createNewExecutor(config.migrationMaxConcurrency, config.migrationKeepAliveInterval); - } - - @Override - public CompletableFuture execute() { - var runner = new MigrationRunnerImpl(config, provider, migrator, listeners()); - return CompletableFuture.supplyAsync(runner::execute, executor); - } - - @Override - public List listeners() { - return List.of( - new ConfigMigrationListener(config), - new LoggerMigrationListener(logger) - ); - } - - @Override - public void close() { - executor.shutdown(); - try { - if (!executor.awaitTermination(15, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - private ExecutorService createNewExecutor(int maxConcurrency, Duration keepAlive) { - return new ThreadPoolExecutor( - maxConcurrency, maxConcurrency, - keepAlive.toMillis(), TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(), - newThreadFactory(), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - } - - private ThreadFactory newThreadFactory() { - return new ThreadFactory() { - - private final ThreadFactory base = Executors.defaultThreadFactory(); - private final AtomicInteger seq = new AtomicInteger(1); - - @Override - public Thread newThread(@NotNull Runnable r) { - Thread thread = base.newThread(r); - thread.setName(EXECUTOR_THREAD_NAME + seq.getAndIncrement()); - thread.setDaemon(true); - return thread; - } - }; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java deleted file mode 100644 index 5a2ae6c..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/BlockingMigrationRunner.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.feature.migration.listener.ConfigMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.LoggerMigrationListener; -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.List; - -public final class BlockingMigrationRunner implements MigrationRunner { - - private final PluginLogger logger; - private final MigrationConfig config; - private final PlayerProvider provider; - private final PlayerMigrator migrator; - - @Inject - public BlockingMigrationRunner( - @NotNull PluginLogger logger, - @NotNull MigrationConfig config, - @NotNull PlayerProvider provider, - @NotNull PlayerMigrator migrator - ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.config = Validator.notNull(config, "config cannot be null"); - this.provider = Validator.notNull(provider, "provider cannot be null"); - this.migrator = Validator.notNull(migrator, "migrator cannot be null"); - } - - @Override - public MigrationResult execute() { - final var runner = new MigrationRunnerImpl(config, provider, migrator, listeners()); - return runner.execute(); - } - - @Override - public List listeners() { - return List.of( - new ConfigMigrationListener(config), - new LoggerMigrationListener(logger) - ); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java deleted file mode 100644 index 6e2175d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunner.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; - -import java.util.List; - -/** - * Executes a migration process according to a specific strategy and - * dispatches events to registered {@link MigrationListener} instances. - * - *

A {@code MigrationRunner} typically orchestrates:

- *
    - *
  • retrieving players from a {@code PlayerProvider},
  • - *
  • delegating work to a {@code PlayerMigrator},
  • - *
  • collecting results and computing statistics,
  • - *
  • notifying listeners about progress.
  • - *
- * - *

Implementations define whether execution is synchronous or asynchronous, - * and establish the threading model used for callbacks.

- * - * @param the type returned upon completion (e.g. {@code MigrationResult}) - */ -public interface MigrationRunner { - - /** - * Executes the migration process. - * - * @return a result object summarizing the completed migration - */ - T execute(); - - /** - * Returns all listeners that will receive migration callbacks. - * - * @return an immutable or defensive-copied list of listeners - */ - List listeners(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java deleted file mode 100644 index e463420..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/migration/runner/MigrationRunnerImpl.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.github.imdmk.playtime.feature.migration.runner; - -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationResult; -import com.github.imdmk.playtime.feature.migration.listener.MigrationListener; -import com.github.imdmk.playtime.feature.migration.migrator.PlayerMigrator; -import com.github.imdmk.playtime.feature.migration.provider.PlayerProvider; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.google.common.base.Stopwatch; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -final class MigrationRunnerImpl { - - private final MigrationConfig config; - private final PlayerProvider provider; - private final PlayerMigrator migrator; - private final List listeners; - - MigrationRunnerImpl( - @NotNull MigrationConfig config, - @NotNull PlayerProvider provider, - @NotNull PlayerMigrator migrator, - @NotNull List listeners - ) { - this.config = Validator.notNull(config, "config cannot be null"); - this.provider = Validator.notNull(provider, "provider cannot be null"); - this.migrator = Validator.notNull(migrator, "migrator cannot be null"); - this.listeners = Validator.notNull(listeners, "listeners cannot be null"); - } - - MigrationResult execute() { - if (!config.initialServerMigrationEnabled) { - return MigrationResult.empty(); - } - - final Stopwatch stopwatch = Stopwatch.createStarted(); - - final Collection players = provider.getAllPlayers(); - final int total = players.size(); - - listenersForEach(l -> l.onStart(total)); - if (total == 0) { - final MigrationResult empty = MigrationResult.empty(); - listenersForEach(l -> l.onEnd(empty)); - return empty; - } - - final AtomicInteger success = new AtomicInteger(); - final AtomicInteger failed = new AtomicInteger(); - final AtomicInteger inflight = new AtomicInteger(total); - - final Semaphore limiter = new Semaphore(config.migrationMaxConcurrency); - final CompletableFuture allDone = new CompletableFuture<>(); - - for (final OfflinePlayer player : players) { - limiter.acquireUninterruptibly(); - - migrator.migrate(player) - .orTimeout(config.migrationTaskTimeout.toMillis(), TimeUnit.MILLISECONDS) - .whenComplete((u, e) -> { - try { - if (e == null) { - success.incrementAndGet(); - listenersForEach(l -> l.onSuccess(player)); - } else { - failed.incrementAndGet(); - listenersForEach(l -> l.onFailed(player, e)); - } - } finally { - limiter.release(); - if (inflight.decrementAndGet() == 0) { - allDone.complete(null); - } - } - }); - } - - allDone.orTimeout(config.migrationGlobalTimeout.toMillis(), TimeUnit.MILLISECONDS).join(); - - final Duration took = stopwatch.stop().elapsed(); - final MigrationResult result = new MigrationResult(total, success.get(), failed.get(), took); - - listenersForEach(l -> l.onEnd(result)); - - return result; - } - - private void listenersForEach(@NotNull Consumer listenerConsumer) { - for (final var listener : listeners) { - listenerConsumer.accept(listener); - } - } - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java index f9d48c2..450179a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/BukkitPlayTimeService.java @@ -1,7 +1,8 @@ package com.github.imdmk.playtime.feature.playtime; -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.UserTime; import org.bukkit.OfflinePlayer; import org.bukkit.Server; @@ -11,108 +12,48 @@ import java.util.UUID; -/** - * Implementation of {@link PlaytimeService} that retrieves and modifies player playtime data - * directly from the Bukkit {@link Server} statistics API. - *

- * This service operates exclusively on the primary (main) thread of the Bukkit server. - * Any access attempt from a non-primary thread will result in an {@link UnsupportedOperationException}. - *

- * The playtime is based on {@link Statistic#PLAY_ONE_MINUTE}, which internally stores values - * in Minecraft ticks (20 ticks = 1 second). The {@link UserTime} abstraction is used to - * convert between ticks and higher-level time units. - *

- * Thread-safety note: Bukkit statistic access is not thread-safe. - * Always ensure that invocations are done synchronously on the main thread. - */ -final class BukkitPlayTimeService implements PlaytimeService { +@Service(priority = Priority.LOWEST) +final class BukkitPlayTimeService implements PlayTimeService { private static final Statistic PLAYTIME_STATISTIC = Statistic.PLAY_ONE_MINUTE; - private static final UserTime ZERO_TIME = UserTime.ZERO; private final Server server; @Inject BukkitPlayTimeService(@NotNull Server server) { - this.server = Validator.notNull(server, "server"); + this.server = server; } - /** - * Retrieves the total playtime of the specified player. - * - * @param uuid the UUID of the target player (must not be {@code null}) - * @return a {@link UserTime} instance representing the player’s total playtime, - * or {@link UserTime#ZERO} if the player has never joined or has zero ticks recorded - * @throws UnsupportedOperationException if called from a non-primary thread - * @throws NullPointerException if {@code uuid} is {@code null} - */ @Override public @NotNull UserTime getTime(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid cannot be null"); - - if (!isPrimaryThread()) { - throw new UnsupportedOperationException( - "BukkitPlaytimeService#getTime must be called from the primary thread." - ); - } + checkPrimaryThread(); int timeTicks = getOffline(uuid).getStatistic(PLAYTIME_STATISTIC); if (timeTicks <= 0) { - return ZERO_TIME; + return UserTime.ZERO; } return UserTime.ofTicks(timeTicks); } - /** - * Sets the total playtime of the specified player to the given value. - * - * @param uuid the UUID of the target player (must not be {@code null}) - * @param time the desired new total playtime (must not be {@code null}) - * @throws UnsupportedOperationException if called from a non-primary thread - * @throws NullPointerException if any argument is {@code null} - */ @Override public void setTime(@NotNull UUID uuid, @NotNull UserTime time) { - Validator.notNull(uuid, "uuid cannot be null"); - Validator.notNull(time, "time cannot be null"); - - if (!isPrimaryThread()) { - throw new UnsupportedOperationException( - "BukkitPlaytimeService#setTime must be called from the primary thread." - ); - } - + checkPrimaryThread(); getOffline(uuid).setStatistic(PLAYTIME_STATISTIC, time.toTicks()); } - /** - * Resets the total playtime of the specified player to zero. - * - * @param uuid the UUID of the target player - * @throws UnsupportedOperationException if called from a non-primary thread - */ @Override public void resetTime(@NotNull UUID uuid) { - setTime(uuid, ZERO_TIME); + setTime(uuid, UserTime.ZERO); } - /** - * Retrieves the {@link OfflinePlayer} instance associated with the given UUID. - * - * @param uuid the player's UUID (must not be {@code null}) - * @return the corresponding {@link OfflinePlayer} handle - */ - private @NotNull OfflinePlayer getOffline(@NotNull UUID uuid) { + private OfflinePlayer getOffline(UUID uuid) { return server.getOfflinePlayer(uuid); } - /** - * Checks whether the current execution is happening on the primary (main) Bukkit thread. - * - * @return {@code true} if running on the main thread, otherwise {@code false} - */ - private boolean isPrimaryThread() { - return server.isPrimaryThread(); + private void checkPrimaryThread() { + if (!server.isPrimaryThread()) { + throw new UnsupportedOperationException("BukkitPlaytimeService must be called from the primary thread."); + } } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java deleted file mode 100644 index bad056f..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeModule.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime; - -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.feature.playtime.command.TimeCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeResetAllCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeResetCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeSetCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeTopCommand; -import com.github.imdmk.playtime.feature.playtime.command.TimeTopInvalidateCommand; -import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGui; -import com.github.imdmk.playtime.feature.playtime.listener.PlayTimeSaveListener; -import com.github.imdmk.playtime.feature.playtime.placeholder.PlayTimePlaceholder; -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.GuiPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.ListenerPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.PlaceholderPhase; -import com.github.imdmk.playtime.user.UserFactory; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class PlayTimeModule implements Module { - - private PlaytimeService playtimeService; - private UserFactory userFactory; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(PlaytimeService.class).assignInstance(() -> this.playtimeService); - resources.on(UserFactory.class).assignInstance(() -> this.userFactory); - } - - @Override - public void init(@NotNull Injector injector) { - this.playtimeService = injector.newInstance(BukkitPlayTimeService.class); - this.userFactory = injector.newInstance(PlayTimeUserFactory.class); - } - - @Override - public CommandPhase commands(@NotNull Injector injector) { - return builder -> builder.commands( - injector.newInstance(TimeCommand.class), - injector.newInstance(TimeSetCommand.class), - injector.newInstance(TimeTopCommand.class), - injector.newInstance(TimeResetCommand.class), - injector.newInstance(TimeResetAllCommand.class), - injector.newInstance(TimeTopInvalidateCommand.class) - ); - } - - @Override - public ListenerPhase listeners(@NotNull Injector injector) { - return builder -> builder.register( - injector.newInstance(PlayTimeSaveListener.class) - ); - } - - @Override - public GuiPhase guis(@NotNull Injector injector) { - return guiRegistry -> guiRegistry.register(injector.newInstance(PlayTimeTopGui.class)); - } - - @Override - public PlaceholderPhase placeholders(@NotNull Injector injector) { - return adapter -> adapter.register( - injector.newInstance(PlayTimePlaceholder.class) - ); - } - - @Override - public int order() { - return -1; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java index 03b7552..30e00e0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/PlayTimeUserFactory.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.feature.playtime; -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; import com.github.imdmk.playtime.user.UserTime; @@ -15,34 +14,34 @@ /** * Concrete implementation of {@link UserFactory} that constructs {@link User} instances - * using data retrieved from the {@link PlaytimeService}. + * using data retrieved from the {@link PlayTimeService}. * *

This factory supports both online and offline players, resolving their unique identifiers, * last known names, and total recorded playtime from the underlying service.

* - *

Dependency: {@link PlaytimeService} is injected at runtime and must be available + *

Dependency: {@link PlayTimeService} is injected at runtime and must be available * before this factory is used.

* * @see User - * @see PlaytimeService + * @see PlayTimeService * @see UserFactory */ public final class PlayTimeUserFactory implements UserFactory { private static final String UNKNOWN_PLAYER_NAME_FORMAT = "Unknown:%s"; - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; @Inject - public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { - this.playtimeService = Validator.notNull(playtimeService, "playtimeService"); + public PlayTimeUserFactory(@NotNull PlayTimeService playtimeService) { + this.playtimeService = playtimeService; } /** * Creates a {@link User} instance from an online {@link Player}. * *

The user's UUID and current name are taken directly from the live {@link Player} object, - * and their total playtime is resolved via the {@link PlaytimeService}.

+ * and their total playtime is resolved via the {@link PlayTimeService}.

* * @param player non-null online player instance * @return new {@link User} representing the given player and their current playtime @@ -50,12 +49,9 @@ public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { */ @Override public @NotNull User createFrom(@NotNull Player player) { - Validator.notNull(player, "player cannot be null"); - final UUID uuid = player.getUniqueId(); final String name = player.getName(); final UserTime time = playtimeService.getTime(uuid); - return new User(uuid, name, time); } @@ -64,7 +60,7 @@ public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { * *

If the player's name cannot be resolved (e.g. first join or data missing), * a default placeholder name {@code "Unknown"} is used instead. - * The total playtime is fetched from {@link PlaytimeService} based on the player's UUID.

+ * The total playtime is fetched from {@link PlayTimeService} based on the player's UUID.

* * @param player non-null offline player instance * @return new {@link User} representing the offline player and their playtime data @@ -72,12 +68,9 @@ public PlayTimeUserFactory(@NotNull PlaytimeService playtimeService) { */ @Override public @NotNull User createFrom(@NotNull OfflinePlayer player) { - Validator.notNull(player, "player cannot be null"); - final UUID uuid = player.getUniqueId(); final String name = Optional.ofNullable(player.getName()).orElse(UNKNOWN_PLAYER_NAME_FORMAT.formatted(uuid)); final UserTime time = playtimeService.getTime(uuid); - return new User(uuid, name, time); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java index b97e2f0..8638cfa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeCommand.java @@ -1,9 +1,8 @@ package com.github.imdmk.playtime.feature.playtime.command; -import com.github.imdmk.playtime.PlaytimeService; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import dev.rollczi.litecommands.annotations.argument.Arg; @@ -20,15 +19,15 @@ public final class TimeCommand { private final MessageService messageService; - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; @Inject public TimeCommand( @NotNull MessageService messageService, - @NotNull PlaytimeService playtimeService + @NotNull PlayTimeService playtimeService ) { - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); + this.messageService = messageService; + this.playtimeService = playtimeService; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java index 734463e..b41f98e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetAllCommand.java @@ -1,10 +1,9 @@ package com.github.imdmk.playtime.feature.playtime.command; -import com.github.imdmk.playtime.PlaytimeService; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -32,7 +31,7 @@ public final class TimeResetAllCommand { private final Server server; private final PluginLogger logger; private final MessageService messageService; - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; private final UserService userService; private final UserRepository userRepository; private final TaskScheduler taskScheduler; @@ -42,18 +41,18 @@ public TimeResetAllCommand( @NotNull Server server, @NotNull PluginLogger logger, @NotNull MessageService messageService, - @NotNull PlaytimeService playtimeService, + @NotNull PlayTimeService playtimeService, @NotNull UserService userService, @NotNull UserRepository userRepository, @NotNull TaskScheduler taskScheduler ) { - this.server = Validator.notNull(server, "server cannot be null"); - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); - this.userRepository = Validator.notNull(userRepository, "userRepository cannot be null"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); + this.server = server; + this.logger = logger; + this.messageService = messageService; + this.playtimeService = playtimeService; + this.userService = userService; + this.userRepository = userRepository; + this.taskScheduler = taskScheduler; } @Execute @@ -83,7 +82,7 @@ void resetAll(@Context CommandSender sender) { }); } - private CompletableFuture resetUser(@NotNull User user) { + private CompletableFuture resetUser(User user) { user.setPlaytime(UserTime.ZERO); return userService.save(user, SAVE_REASON); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java index a48e233..c230f07 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeResetCommand.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -33,9 +32,9 @@ public TimeResetCommand( @NotNull MessageService messageService, @NotNull UserService userService ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); + this.logger = logger; + this.messageService = messageService; + this.userService = userService; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java index 4b01fa6..0abb4e8 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeSetCommand.java @@ -1,9 +1,9 @@ package com.github.imdmk.playtime.feature.playtime.command; +import com.github.imdmk.playtime.PlayTimeService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -26,17 +26,20 @@ public final class TimeSetCommand { private final PluginLogger logger; private final MessageService messageService; + private final PlayTimeService playTimeService; private final UserService userService; @Inject public TimeSetCommand( @NotNull PluginLogger logger, @NotNull MessageService messageService, + @NotNull PlayTimeService playTimeService, @NotNull UserService userService ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); + this.logger = logger; + this.messageService = messageService; + this.playTimeService = playTimeService; + this.userService = userService; } @Execute @@ -45,6 +48,7 @@ void setPlaytime(@Context CommandSender sender, @Arg @Async User target, @Arg Du final UserTime newTime = UserTime.ofDuration(normalizedTime); target.setPlaytime(newTime); + playTimeService.setTime(target.getUuid(), newTime); userService.save(target, UserSaveReason.SET_COMMAND) .thenAccept(v -> messageService.create() diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java index 6c2ca89..fd81b7c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopCommand.java @@ -4,7 +4,6 @@ import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.gui.view.GuiOpener; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.UserService; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; @@ -32,10 +31,10 @@ public TimeTopCommand( @NotNull UserService userService, @NotNull GuiOpener guiOpener ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); - this.guiOpener = Validator.notNull(guiOpener, "guiOpener cannot be null"); + this.logger = logger; + this.messageService = messageService; + this.userService = userService; + this.guiOpener = guiOpener; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java index 78f5dc9..97dde2e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/command/TimeTopInvalidateCommand.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.command; import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.top.TopUsersCache; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; @@ -23,8 +22,8 @@ public TimeTopInvalidateCommand( @NotNull MessageService messageService, @NotNull TopUsersCache topUsersCache ) { - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.topUsersCache = Validator.notNull(topUsersCache, "topUsersCache cannot be null"); + this.messageService = messageService; + this.topUsersCache = topUsersCache; } @Execute diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java index 004cc9e..ba9cea3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGui.java @@ -18,7 +18,6 @@ import com.github.imdmk.playtime.platform.gui.view.ParameterizedGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; @@ -26,6 +25,7 @@ import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.builder.item.SkullBuilder; import dev.triumphteam.gui.guis.BaseGui; +import dev.triumphteam.gui.guis.GuiItem; import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -61,28 +61,22 @@ public PlayTimeTopGui( @NotNull UserService userService ) { super(navigationBarConfig, taskScheduler, GUI_RENDERER, RENDER_OPTIONS); - this.server = Validator.notNull(server, "server"); - this.topGuiConfig = Validator.notNull(topGuiConfig, "playtimeTopGuiConfig cannot be null"); - this.messageService = Validator.notNull(messageService, "messageService cannot be null"); - this.userService = Validator.notNull(userService, "userService cannot be null"); + this.server = server; + this.topGuiConfig = topGuiConfig; + this.messageService = messageService; + this.userService = userService; } @Override public @NotNull BaseGui createGui(@NotNull Player viewer, @NotNull List users) { - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(users, "users cannot be null"); return GuiFactory.build(topGuiConfig, BaseGui::disableAllInteractions); } @Override public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull List users) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(users, "users cannot be null"); - if (topGuiConfig.fillBorder) { - final var border = ItemGuiTransformer.toGuiItem(topGuiConfig.borderItem); - gui.getFiller().fillBorder(border); + final GuiItem borderItem = ItemGuiTransformer.toGuiItem(topGuiConfig.borderItem); + gui.getFiller().fillBorder(borderItem); } placeExit(gui, viewer, e -> gui.close(viewer)); @@ -92,14 +86,14 @@ public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull placePrevious(gui, viewer); } - final var context = RenderContext.defaultContext(viewer); - final var item = resolveItemFor(viewer, context); + final RenderContext context = RenderContext.defaultContext(viewer); + final ItemGui item = resolveItemFor(viewer, context); for (int i = 0; i < users.size(); i++) { final User user = users.get(i); final int position = i + 1; - final var placeholders = createPlaceholders(user, position); + final AdventurePlaceholders placeholders = createPlaceholders(user, position); final Consumer onClick = (click) -> { if (click.getClick() != topGuiConfig.resetClickType) { @@ -131,9 +125,8 @@ public void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull } private ItemGui resolveItemFor(Player viewer, RenderContext context) { - final var adminItem = topGuiConfig.playerEntryAdminItem; - final var item = topGuiConfig.playerEntryItem; - + final ItemGui adminItem = topGuiConfig.playerEntryAdminItem; + final ItemGui item = topGuiConfig.playerEntryItem; return ITEM_VARIANT_RESOLVER.resolve(viewer, context, List.of(adminItem), item); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java index fd0bab0..76f5605 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/gui/PlayTimeTopGuiConfig.java @@ -1,5 +1,6 @@ package com.github.imdmk.playtime.feature.playtime.gui; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import com.github.imdmk.playtime.platform.adventure.AdventureComponents; import com.github.imdmk.playtime.platform.gui.GuiType; import com.github.imdmk.playtime.platform.gui.config.ConfigurableGui; @@ -14,6 +15,7 @@ import java.util.Collections; +@ConfigFile public final class PlayTimeTopGuiConfig extends OkaeriConfig implements ConfigurableGui { @Comment({ diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java new file mode 100644 index 0000000..eecdad2 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveController.java @@ -0,0 +1,42 @@ +package com.github.imdmk.playtime.feature.playtime.listener; + +import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Controller; +import com.github.imdmk.playtime.user.User; +import com.github.imdmk.playtime.user.UserSaveReason; +import com.github.imdmk.playtime.user.UserService; +import com.github.imdmk.playtime.user.UserTime; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.util.UUID; + +@Controller +public final class PlayTimeSaveController implements Listener { + + private final UserService userService; + private final PlayTimeService playtimeService; + + @Inject + public PlayTimeSaveController( + @NotNull UserService userService, + @NotNull PlayTimeService playtimeService + ) { + this.userService = userService; + this.playtimeService = playtimeService; + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + final UUID uuid = event.getPlayer().getUniqueId(); + + final User user = userService.findCachedByUuid(uuid).orElseThrow(); + final UserTime time = playtimeService.getTime(uuid); + + user.setPlaytime(time); + userService.save(user, UserSaveReason.PLAYER_LEAVE); + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java deleted file mode 100644 index d1d0a78..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/listener/PlayTimeSaveListener.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.imdmk.playtime.feature.playtime.listener; - -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.UserPreSaveEvent; -import com.github.imdmk.playtime.UserSaveEvent; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.github.imdmk.playtime.user.User; -import com.github.imdmk.playtime.user.UserSaveReason; -import com.github.imdmk.playtime.user.UserTime; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.util.UUID; - -public final class PlayTimeSaveListener implements Listener { - - private final PlaytimeService playtimeService; - - @Inject - public PlayTimeSaveListener(@NotNull PlaytimeService playtimeService) { - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onUserPreSave(UserPreSaveEvent event) { - final User user = event.getUser(); - final UUID uuid = user.getUuid(); - final UserSaveReason reason = event.getReason(); - - if (reason == UserSaveReason.PLAYER_LEAVE) { - final UserTime currentPlaytime = playtimeService.getTime(uuid); - user.setPlaytime(currentPlaytime); - } - } - - @EventHandler(priority = EventPriority.HIGHEST) - public void onUserSave(UserSaveEvent event) { - final User user = event.getUser(); - playtimeService.setTime(user.getUuid(), user.getPlaytime()); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java index 1f3a1f3..42560a0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/playtime/placeholder/PlayTimePlaceholder.java @@ -1,9 +1,9 @@ package com.github.imdmk.playtime.feature.playtime.placeholder; -import com.github.imdmk.playtime.PlaytimeService; +import com.github.imdmk.playtime.PlayTimeService; +import com.github.imdmk.playtime.injector.annotations.Placeholder; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import com.github.imdmk.playtime.shared.time.Durations; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -11,13 +11,14 @@ import java.util.UUID; +@Placeholder public final class PlayTimePlaceholder implements PluginPlaceholder { - private final PlaytimeService playtimeService; + private final PlayTimeService playtimeService; @Inject - public PlayTimePlaceholder(@NotNull PlaytimeService playtimeService) { - this.playtimeService = Validator.notNull(playtimeService, "playtimeService cannot be null"); + public PlayTimePlaceholder(@NotNull PlayTimeService playtimeService) { + this.playtimeService = playtimeService; } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java index 8819020..8045c0d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadCommand.java @@ -1,10 +1,9 @@ package com.github.imdmk.playtime.feature.reload; -import com.github.imdmk.playtime.config.ConfigManager; +import com.github.imdmk.playtime.config.ConfigService; import com.github.imdmk.playtime.message.MessageService; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; @@ -19,28 +18,28 @@ public final class ReloadCommand { private final PluginLogger logger; - private final ConfigManager configManager; + private final ConfigService configService; private final TaskScheduler taskScheduler; private final MessageService messageService; @Inject public ReloadCommand( @NotNull PluginLogger logger, - @NotNull ConfigManager configManager, + @NotNull ConfigService configService, @NotNull TaskScheduler taskScheduler, @NotNull MessageService messageService ) { - this.logger = Validator.notNull(logger, "logger"); - this.configManager = Validator.notNull(configManager, "configManager"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler"); - this.messageService = Validator.notNull(messageService, "messageService"); + this.logger = logger; + this.configService = configService; + this.taskScheduler = taskScheduler; + this.messageService = messageService; } @Execute void reload(@Context CommandSender sender) { taskScheduler.runAsync(() -> { try { - configManager.loadAll(); + configService.loadAll(); messageService.send(sender, n -> n.reloadMessages.configReloadedSuccess()); } catch (OkaeriException e) { logger.error(e, "Failed to reload plugin configuration files"); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java deleted file mode 100644 index d3b6dbc..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/feature/reload/ReloadModule.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.feature.reload; - -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class ReloadModule implements Module { - - @Override - public void bind(@NotNull Resources resources) {} - - @Override - public void init(@NotNull Injector injector) {} - - @Override - public CommandPhase commands(@NotNull Injector injector) { - return builder -> builder.commands(injector.newInstance(ReloadCommand.class)); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConnector.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConnector.java deleted file mode 100644 index 5da685e..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseConnector.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database; - -import com.github.imdmk.playtime.infrastructure.database.driver.configurer.DriverConfigurer; -import com.github.imdmk.playtime.infrastructure.database.driver.configurer.DriverConfigurerFactory; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.jdbc.DataSourceConnectionSource; -import com.j256.ormlite.support.ConnectionSource; -import com.zaxxer.hikari.HikariDataSource; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.logging.Level; - -final class DatabaseConnector { - - private static final String POOL_NAME = "playtime-db-pool"; - - private static final int DEFAULT_MAX_POOL_SIZE = 4; - private static final int DEFAULT_MIN_IDLE = 0; - - private static final long DEFAULT_CONNECTION_TIMEOUT_MS = 10_000L; - private static final long DEFAULT_IDLE_TIMEOUT_MS = 60_000L; - private static final long DEFAULT_MAX_LIFETIME_MS = 600_000L; - - private static final boolean CACHE_PREP_STMTS = true; - private static final int PREP_STMT_CACHE_SIZE = 250; - private static final int PREP_STMT_CACHE_SQL_LIMIT = 2048; - private static final boolean USE_SERVER_PREP_STMTS = true; - - private static final Level DATA_SOURCE_LOG_LEVEL = Level.SEVERE; - - private final PluginLogger logger; - private final DatabaseConfig config; - private final DriverConfigurer driverConfigurer; - - private volatile HikariDataSource dataSource; - private volatile ConnectionSource connectionSource; - - /** - * Creates a new connector with an explicit {@link DriverConfigurer}. - * Useful for testing or advanced customization. - * - * @param logger the plugin logger (never null) - * @param config the database configuration (never null) - * @param driverConfigurer strategy used to configure the underlying JDBC driver (never null) - */ - DatabaseConnector( - @NotNull PluginLogger logger, - @NotNull DatabaseConfig config, - @NotNull DriverConfigurer driverConfigurer - ) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.config = Validator.notNull(config, "config cannot be null"); - this.driverConfigurer = Validator.notNull(driverConfigurer, "driverConfigurer cannot be null"); - } - - /** - * Creates a new connector using the default {@link DriverConfigurer} - * resolved from {@link DriverConfigurerFactory} based on {@link DatabaseConfig#databaseMode}. - * - * @param logger the plugin logger (never null) - * @param config the database configuration (never null) - */ - DatabaseConnector( - @NotNull PluginLogger logger, - @NotNull DatabaseConfig config - ) { - this(logger, config, DriverConfigurerFactory.getFor(config.databaseMode)); - } - - /** - * Establishes a new database connection and initializes the internal Hikari connection pool. - *

- * If already connected, this method throws {@link IllegalStateException}. - * Engine-specific configuration (JDBC URL, file paths, flags) is delegated - * to the configured {@link DriverConfigurer}. - * - * @param dataFolder plugin data folder, used especially for file-based databases (e.g. SQLite/H2) - * @throws SQLException if JDBC or ORMLite initialization fails - * @throws IllegalStateException if a connection is already active - */ - synchronized void connect(@NotNull File dataFolder) throws SQLException { - Validator.notNull(dataFolder, "dataFolder cannot be null"); - - if (dataSource != null || connectionSource != null) { - throw new IllegalStateException("DatabaseConnector is already connected."); - } - - final HikariDataSource ds = createHikariDataSource(); - - try { - // Delegated engine-specific configuration (JDBC URL, engine flags, filesystem prep) - driverConfigurer.configure(ds, config, dataFolder); - - final String jdbcUrl = ds.getJdbcUrl(); - if (jdbcUrl == null || jdbcUrl.isBlank()) { - throw new IllegalStateException("DriverConfigurer did not set JDBC URL for mode " + config.databaseMode); - } - - final ConnectionSource source = new DataSourceConnectionSource(ds, jdbcUrl); - - dataSource = ds; - connectionSource = source; - - logger.info("Connected to %s database.", config.databaseMode); - } catch (SQLException e) { - logger.error(e, "Failed to connect to database"); - closeQuietly(ds); - dataSource = null; - connectionSource = null; - throw e; - } catch (Exception e) { - logger.error(e, "Failed to initialize database"); - closeQuietly(ds); - dataSource = null; - connectionSource = null; - throw new IllegalStateException("Database initialization failed", e); - } - } - - /** - * Closes the active database connection and shuts down the underlying HikariCP pool. - *

- * Safe to call multiple times. Exceptions during close are logged but ignored. - */ - synchronized void close() { - if (connectionSource == null && dataSource == null) { - logger.warn("DatabaseConnector#close() called, but not connected."); - return; - } - - try { - if (connectionSource != null) { - connectionSource.close(); - } - } catch (Exception e) { - logger.error(e, "Failed to close ConnectionSource"); - } - - closeQuietly(dataSource); - - connectionSource = null; - dataSource = null; - - logger.info("Database connection closed successfully."); - } - - /** - * Returns whether this connector is currently connected. - * - * @return {@code true} if both {@link ConnectionSource} and {@link HikariDataSource} are active - */ - boolean isConnected() { - final HikariDataSource ds = dataSource; - return connectionSource != null && ds != null && !ds.isClosed(); - } - - /** - * Returns the current active {@link ConnectionSource}, or {@code null} if not connected. - * - * @return active ORMLite connection source, or {@code null} if disconnected - */ - @Nullable ConnectionSource getConnectionSource() { - return connectionSource; - } - - /** - * Creates and configures a new {@link HikariDataSource} with conservative, engine-agnostic defaults. - *

- * Includes: - *

    - *
  • Moderate pool sizing (max 5, min 1 by default).
  • - *
  • Prepared statement caching (effective for MySQL-family drivers, harmless elsewhere).
  • - *
  • Connection, idle and lifetime timeouts with safe values.
  • - *
- * - * @return configured Hikari data source (not yet started) - */ - private @NotNull HikariDataSource createHikariDataSource() { - final HikariDataSource data = new HikariDataSource(); - data.setPoolName(POOL_NAME); - - data.setMaximumPoolSize(Math.max(DEFAULT_MAX_POOL_SIZE, Runtime.getRuntime().availableProcessors())); - data.setMinimumIdle(DEFAULT_MIN_IDLE); - - data.setUsername(config.databaseUserName); - data.setPassword(config.databasePassword); - - // Reduce noisy driver logging if supported - try { - data.getParentLogger().setLevel(DATA_SOURCE_LOG_LEVEL); - } catch (SQLFeatureNotSupportedException ignored) {} - - // Prepared statement cache (useful for MySQL-family; harmless for others) - data.addDataSourceProperty("cachePrepStmts", CACHE_PREP_STMTS); - data.addDataSourceProperty("prepStmtCacheSize", PREP_STMT_CACHE_SIZE); - data.addDataSourceProperty("prepStmtCacheSqlLimit", PREP_STMT_CACHE_SQL_LIMIT); - data.addDataSourceProperty("useServerPrepStmts", USE_SERVER_PREP_STMTS); - - // Timeout configuration (milliseconds) - data.setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT_MS); - data.setIdleTimeout(DEFAULT_IDLE_TIMEOUT_MS); - data.setMaxLifetime(DEFAULT_MAX_LIFETIME_MS); - - return data; - } - - /** - * Closes the given {@link HikariDataSource} without propagating exceptions. - * - * @param ds data source to close (nullable) - */ - private static void closeQuietly(@Nullable HikariDataSource ds) { - try { - if (ds != null) { - ds.close(); - } - } catch (Exception ignored) {} - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseManager.java deleted file mode 100644 index 46d50e8..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseManager.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database; - -import com.github.imdmk.playtime.infrastructure.database.driver.dependency.DriverDependencyLoader; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.support.ConnectionSource; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; - -public final class DatabaseManager { - - private final Plugin plugin; - private final DatabaseConfig config; - - private final DriverDependencyLoader driverLoader; - private final DatabaseConnector connector; - - public DatabaseManager( - @NotNull Plugin plugin, - @NotNull PluginLogger logger, - @NotNull DatabaseConfig config - ) { - this.plugin = Validator.notNull(plugin, "plugin"); - this.config = Validator.notNull(config, "config"); - - this.driverLoader = new DriverDependencyLoader(plugin); - this.connector = new DatabaseConnector(logger, config); - } - - public void loadDriver() { - driverLoader.loadDriverFor(config.databaseMode); - } - - public void connect() throws SQLException { - connector.connect(plugin.getDataFolder()); - } - - @Nullable - public ConnectionSource getConnection() { - return connector.getConnectionSource(); - } - - public void shutdown() { - if (connector.isConnected()) { - connector.close(); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseMode.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseMode.java deleted file mode 100644 index 940faa7..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/DatabaseMode.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database; - -/** - * Enumerates all database engines supported by the plugin. - *

- * Each value represents a distinct JDBC provider and is used to: - *

    - *
  • select and load the correct JDBC driver dynamically,
  • - *
  • apply engine-specific JDBC URL configuration,
  • - *
  • initialize the matching {@code DriverConfigurer} implementation.
  • - *
- *

- * Below each engine is annotated with practical recommendations - * for typical Minecraft server environments. - */ -public enum DatabaseMode { - - /** - * MySQL — recommended for most production servers. - *

- * Stable, well-supported, widely hosted, good performance under sustained load. - * Best choice for: medium–large servers, networks, Bungee/Velocity setups. - */ - MYSQL, - - /** - * MariaDB — drop-in MySQL replacement. - *

- * Often faster for reads, lighter resource usage, very stable on Linux hosts. - * Best choice for: self-hosted servers (VPS/dedicated), users preferring open-source MySQL alternatives. - */ - MARIADB, - - /** - * SQLite — file-based embedded database. - *

- * Zero configuration, no external server needed, safe for smaller datasets. - * Best choice for: small servers, testing environments, local development. - * Avoid it for large playtime tables or heavy concurrent write load. - */ - SQLITE, - - /** - * PostgreSQL — robust, enterprise-grade server engine. - *

- * Very strong consistency guarantees, excellent indexing, powerful features. - * Best choice for: large datasets, advanced analytics, servers on modern hosting (e.g., managed PSQL). - */ - POSTGRESQL, - - /** - * H2 — lightweight embedded or file-based engine. - *

- * Faster than SQLite in many scenarios, supports MySQL compatibility mode. - * Best choice for: plugin developers, embedded deployments, users wanting higher performance without external DB. - * Not recommended for: huge datasets or multi-server networks. - */ - H2, - - /** - * SQL Server (MSSQL) — enterprise Microsoft database engine. - *

- * Works well on Windows hosts, strong enterprise tooling. - * Best choice for: Windows-based servers, corporate networks using MSSQL by default. - * Rarely needed for typical Minecraft environments. - */ - SQL -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java deleted file mode 100644 index 84621cf..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurer.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; - -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.jetbrains.annotations.NotNull; - -import java.io.File; - -/** - * Strategy interface defining how to configure a {@link HikariDataSource} - * for a specific database engine. - *

- * Implementations are responsible for: - *

    - *
  • constructing the correct JDBC URL,
  • - *
  • applying engine-specific HikariCP properties,
  • - *
  • performing any required filesystem preparation (e.g. SQLite/H2 directories).
  • - *
- * This abstraction allows {@link com.github.imdmk.playtime.infrastructure.database.DatabaseConnector} - * to remain engine-agnostic while still supporting multiple database types. - */ -public interface DriverConfigurer { - - /** - * Configures the provided {@link HikariDataSource} instance using the database - * settings supplied in {@link DatabaseConfig} and the plugin data folder. - *

- * Implementations must be deterministic and side-effect-free except for: - *

    - *
  • modifying the {@code dataSource} instance,
  • - *
  • creating required directories for file-based databases.
  • - *
- * - * @param dataSource the HikariCP data source to configure (never null) - * @param config the database configuration containing connection details (never null) - * @param dataFolder the plugin data folder, used especially for file-based engines like SQLite/H2 (never null) - */ - void configure(@NotNull HikariDataSource dataSource, - @NotNull DatabaseConfig config, - @NotNull File dataFolder); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java deleted file mode 100644 index 4085841..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/configurer/DriverConfigurerFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.configurer; - -import com.github.imdmk.playtime.infrastructure.database.DatabaseMode; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; - -/** - * Factory responsible for selecting the correct {@link DriverConfigurer} - * implementation for a given {@link DatabaseMode}. - *

- * All supported drivers are registered statically in an immutable lookup table. - * This ensures fast resolution, avoids reflection, and cleanly separates - * database-specific logic into dedicated strategy classes. - *

- * The factory acts as the single entry point for retrieving driver configuration - * strategies used by {@code DatabaseConnector}. - */ -public final class DriverConfigurerFactory { - - /** Immutable lookup table mapping database modes to their respective configurers. */ - private static final Map CONFIGURER_BY_MODE = Map.of( - DatabaseMode.MYSQL, new MySQLConfigurer(), - DatabaseMode.MARIADB, new MariaDBConfigurer(), - DatabaseMode.POSTGRESQL, new PostgreSQLConfigurer(), - DatabaseMode.SQLITE, new SQLiteConfigurer(), - DatabaseMode.H2, new H2Configurer(), - DatabaseMode.SQL, new SQLConfigurer() - ); - - /** - * Returns the {@link DriverConfigurer} associated with the given {@link DatabaseMode}. - * - * @param mode the selected database engine (never null) - * @return the matching non-null {@link DriverConfigurer} - * @throws IllegalArgumentException if the mode is not supported - */ - public static @NotNull DriverConfigurer getFor(@NotNull DatabaseMode mode) { - Validator.notNull(mode, "mode cannot be null"); - - DriverConfigurer configurer = CONFIGURER_BY_MODE.get(mode); - if (configurer == null) { - throw new IllegalArgumentException("Unsupported database mode: " + mode); - } - - return configurer; - } - - private DriverConfigurerFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java deleted file mode 100644 index f10cf1a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/driver/dependency/DriverDependencyLoader.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.driver.dependency; - -import com.alessiodp.libby.BukkitLibraryManager; -import com.alessiodp.libby.Library; -import com.github.imdmk.playtime.infrastructure.database.DatabaseMode; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; - -/** - * Loads JDBC driver libraries dynamically at runtime using Libby. - *

- * Each {@link DatabaseMode} is mapped to a specific third-party JDBC driver - * defined in {@link DriverLibraries}. This allows the plugin to ship without - * any embedded JDBC drivers and load only the required one on demand. - *

- * This component is deliberately isolated from connection logic to keep the - * database layer modular and compliant with SRP (single responsibility). - */ -public final class DriverDependencyLoader { - - /** Immutable lookup table mapping supported database modes to driver artifacts. */ - private static final Map LIBRARIES_BY_MODE = Map.of( - DatabaseMode.MYSQL, DriverLibraries.MYSQL, - DatabaseMode.MARIADB, DriverLibraries.MARIADB, - DatabaseMode.SQLITE, DriverLibraries.SQLITE, - DatabaseMode.POSTGRESQL, DriverLibraries.POSTGRESQL, - DatabaseMode.H2, DriverLibraries.H2, - DatabaseMode.SQL, DriverLibraries.SQL - ); - - private final BukkitLibraryManager libraryManager; - - /** - * Creates a new dependency loader using a pre-initialized {@link BukkitLibraryManager}. - * Maven Central is automatically added as the default repository source. - * - * @param libraryManager the library manager used to load driver JARs dynamically - */ - public DriverDependencyLoader(@NotNull BukkitLibraryManager libraryManager) { - this.libraryManager = Validator.notNull(libraryManager, "libraryManager cannot be null"); - this.libraryManager.addMavenCentral(); - } - - /** - * Convenience constructor that initializes a {@link BukkitLibraryManager} using the plugin instance. - * - * @param plugin the owning plugin instance - */ - public DriverDependencyLoader(@NotNull Plugin plugin) { - this(new BukkitLibraryManager(plugin)); - } - - /** - * Loads the JDBC driver dependency associated with the given {@link DatabaseMode}. - *

- * If the driver is already loaded, Libby will skip re-loading it automatically. - * - * @param mode the database mode requesting its driver (never null) - * @throws IllegalArgumentException if the mode has no registered driver - */ - public void loadDriverFor(@NotNull DatabaseMode mode) { - Validator.notNull(mode, "mode cannot be null"); - - Library library = LIBRARIES_BY_MODE.get(mode); - if (library == null) { - throw new IllegalArgumentException("Unsupported database mode: " + mode); - } - - libraryManager.loadLibrary(library); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/Repository.java deleted file mode 100644 index 97327d7..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/Repository.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.repository; - -import com.j256.ormlite.support.ConnectionSource; -import org.jetbrains.annotations.NotNull; - -import java.sql.SQLException; - -/** - * Base contract for all repositories. - *

- * Provides lifecycle hooks for database initialization and cleanup. - * Implementations should create their DAO bindings in {@link #start(ConnectionSource)} - * and release resources in {@link #close()}. - */ -public interface Repository extends AutoCloseable { - - /** - * Initializes repository to the given connection source. - * - * @param source the ORMLite connection source - * @throws SQLException if database initialization fails - */ - void start(@NotNull ConnectionSource source) throws SQLException; - - /** - * Closes the repository and releases all resources. - */ - @Override - void close(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryContext.java deleted file mode 100644 index fddcbb3..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryContext.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.repository; - -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ExecutorService; - -/** - * Context container providing shared infrastructure resources - * for all repositories. - * - *

Currently encapsulates the {@link ExecutorService} responsible for - * executing asynchronous database operations. This allows repositories - * to offload blocking I/O work while maintaining a unified execution policy.

- * - *

Usage: Injected into repository instances (see {@link BaseDaoRepository}) - * to provide consistent thread management for database access.

- * - *

Threading: The supplied {@code dbExecutor} should be a dedicated, - * bounded thread pool optimized for database I/O tasks — typically sized according - * to connection pool limits or database concurrency capabilities.

- * - * @param dbExecutor the executor service used for running asynchronous database operations (non-null) - * - * @see BaseDaoRepository - */ -public record RepositoryContext(@NotNull ExecutorService dbExecutor) { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryManager.java deleted file mode 100644 index 9258eb7..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/RepositoryManager.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.repository; - -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.support.ConnectionSource; -import org.jetbrains.annotations.NotNull; - -import java.sql.SQLException; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * Central coordinator that manages the lifecycle of all registered {@link Repository} instances. - * - *

This class provides thread-safe registration, startup, and shutdown of repositories. - * It acts as the single entry point for initializing all repositories - * once the plugin’s database connection has been established.

- * - *

Thread-safety: Repository registration and iteration are backed by - * {@link CopyOnWriteArrayList}, ensuring safe concurrent reads and registrations.

- * - * @see Repository - * @see BaseDaoRepository - * @see ConnectionSource - */ -public final class RepositoryManager implements AutoCloseable { - - private final PluginLogger logger; - - private final List repositories = new CopyOnWriteArrayList<>(); - - public RepositoryManager(@NotNull PluginLogger logger) { - this.logger = Validator.notNull(logger, "logger must not be null"); - } - - /** - * Registers a single repository instance for lifecycle management. - * - *

If the repository has already been registered, the operation is skipped - * and a warning is logged.

- * - * @param repository non-null repository instance - * @throws NullPointerException if {@code repository} is null - */ - public void register(@NotNull Repository repository) { - Validator.notNull(repository, "repository cannot be null"); - if (repositories.contains(repository)) { - logger.warn("Repository %s already registered — skipping", repository.getClass().getSimpleName()); - return; - } - - repositories.add(repository); - } - - /** - * Registers multiple repository instances at once. - * - * @param repositories one or more non-null repositories - * @throws NullPointerException if {@code repositories} array or any element is null - */ - public void register(@NotNull Repository... repositories) { - Validator.notNull(repositories, "repositories cannot be null"); - for (final Repository repository : repositories) { - this.register(repository); - } - } - - /** - * Starts all registered repositories using the provided {@link ConnectionSource}. - * - *

This method creates required tables and initializes all DAO layers. - * If any repository fails to start, the exception is logged and rethrown, - * stopping further startup to prevent inconsistent state.

- * - * @param connectionSource non-null active database connection source - * @throws SQLException if a repository fails to start - * @throws NullPointerException if {@code connectionSource} is null - */ - public void startAll(@NotNull ConnectionSource connectionSource) throws SQLException { - Validator.notNull(connectionSource, "connectionSource cannot be null"); - for (final Repository repository : repositories) { - try { - repository.start(connectionSource); - } catch (SQLException e) { - logger.error(e, "Failed to start repository: %s", repository.getClass().getSimpleName()); - throw e; - } - } - } - - /** - * Gracefully closes all registered repositories. - * - *

Each repository’s {@link Repository#close()} method is invoked individually. - * Exceptions during closing are caught and logged as warnings, allowing - * all repositories to attempt shutdown even if one fails.

- */ - @Override - public void close() { - for (final Repository repository : repositories) { - try { - repository.close(); - } catch (Exception e) { - logger.warn(e, "Error while closing repository: %s", repository.getClass().getSimpleName()); - } - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/BaseDaoRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/BaseDaoRepository.java deleted file mode 100644 index 065bb32..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/BaseDaoRepository.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.repository.ormlite; - -import com.github.imdmk.playtime.infrastructure.database.repository.Repository; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.dao.DaoManager; -import com.j256.ormlite.logger.Level; -import com.j256.ormlite.logger.Logger; -import com.j256.ormlite.support.ConnectionSource; -import com.j256.ormlite.table.TableUtils; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.sql.SQLException; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Supplier; - -/** - * Base class for ORMLite-backed repositories that manages DAO lifecycle, - * schema bootstrapping, and asynchronous query execution. - * - *

Responsibilities:

- *
    - *
  • Create required tables for the main entity and optional subclasses.
  • - *
  • Initialize and expose a typed {@link Dao} instance.
  • - *
  • Provide helper methods to run DAO work asynchronously with a bounded timeout.
  • - *
- * - *

Thread-safety: the {@link #dao} reference is {@code volatile} to ensure - * visibility after initialization. Repository implementations should still avoid compound unsynchronized - * operations on the DAO.

- * - * @param entity type handled by the repository - * @param identifier type of the entity - * - * @see Dao - * @see RepositoryContext - * @see TableUtils - */ -public abstract class BaseDaoRepository implements Repository { - - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(6); - - private final PluginLogger logger; - private final RepositoryContext context; - - protected volatile Dao dao; - - @Inject - protected BaseDaoRepository(@NotNull PluginLogger logger, @NotNull RepositoryContext context) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - this.context = Validator.notNull(context, "context cannot be null"); - Logger.setGlobalLogLevel(Level.ERROR); // Change ORMLITE logging to errors - } - - protected abstract Class entityClass(); - protected abstract List> entitySubClasses(); - - /** - * Initializes the repository: creates missing tables and registers the DAO. - * - *

Tables for {@link #entityClass()} and all {@link #entitySubClasses()} are created if absent. - * Then a new {@link Dao} is obtained via {@link DaoManager#createDao(ConnectionSource, Class)}.

- * - * @param source active ORMLite connection source - * @throws SQLException if schema creation or DAO initialization fails - */ - @Override - public void start(@NotNull ConnectionSource source) throws SQLException { - for (Class subClass : this.entitySubClasses()) { - TableUtils.createTableIfNotExists(source, subClass); - } - - TableUtils.createTableIfNotExists(source, this.entityClass()); - this.dao = DaoManager.createDao(source, this.entityClass()); - } - - /** - * Closes the repository by unregistering the current DAO from its {@link ConnectionSource}. - * - *

This method is idempotent. If no DAO is set, it returns immediately.

- */ - @Override - public void close() { - final Dao current = this.dao; - if (current == null) { - return; - } - - this.dao = null; - ConnectionSource source = current.getConnectionSource(); - if (source != null) { - DaoManager.unregisterDao(source, current); - } - } - - /** - * Executes the supplied task asynchronously on the repository executor with the default timeout. - * - *

Exceptions thrown by the supplier are logged and rethrown as {@link CompletionException}. - * If the task exceeds timeout, the returned future completes exceptionally - * with a {@link TimeoutException} wrapped in a {@link CompletionException}.

- * - * @param supplier unit of work to execute (non-null) - * @param result type - * @return a future completed with the supplier result or exceptionally on failure/timeout - * @throws NullPointerException if {@code supplier} is null - */ - protected CompletableFuture executeAsync(@NotNull Supplier supplier) { - return executeAsync(supplier, DEFAULT_TIMEOUT); - } - - /** - * Executes the supplied task asynchronously on the repository executor with a custom timeout. - * - *

Behavior:

- *
    - *
  • Runs on {@code context.dbExecutor()}.
  • - *
  • Logs and wraps exceptions into {@link CompletionException}.
  • - *
  • Applies {@link CompletableFuture#orTimeout(long, TimeUnit)} with the provided duration.
  • - *
- * - * @param supplier unit of work to execute (non-null) - * @param timeout maximum execution time (non-null) - * @param result type - * @return a future completed with the supplier result or exceptionally on failure/timeout - * @throws NullPointerException if {@code supplier} or {@code timeout} is null - */ - protected CompletableFuture executeAsync(@NotNull Supplier supplier, @NotNull Duration timeout) { - Validator.notNull(supplier, "supplier cannot be null"); - Validator.notNull(timeout, "timeout cannot be null"); - - return CompletableFuture - .supplyAsync(() -> { - try { - return supplier.get(); - } catch (Exception e) { - logger.error(e, "Async DAO operation failed"); - throw new CompletionException(e); - } - }, this.context.dbExecutor()) - .orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS) - .exceptionally(e -> { - if (e instanceof TimeoutException) { - logger.warn("Async DAO operation timed out after %s ms", timeout.toMillis()); - } else { - logger.error(e, "Async DAO operation failed (outer)"); - } - throw (e instanceof CompletionException) - ? (CompletionException) e - : new CompletionException(e); - }); - } - - /** - * Executes the given runnable asynchronously on the repository executor with the default timeout. - * - * @param runnable task to run (non-null) - * @return a future completed normally on success or exceptionally on failure/timeout - * @throws NullPointerException if {@code runnable} is null - */ - protected CompletableFuture executeAsync(@NotNull Runnable runnable) { - return executeAsync(runnable, DEFAULT_TIMEOUT); - } - - /** - * Executes the given runnable asynchronously on the repository executor with a custom timeout. - * - *

Exceptions thrown by the runnable are logged and propagated as {@link CompletionException}. - * On timeout, the future completes exceptionally with a {@link TimeoutException} wrapped in a - * {@link CompletionException}.

- * - * @param runnable task to run (non-null) - * @param timeout maximum execution time (non-null) - * @return a future completed normally on success or exceptionally on failure/timeout - * @throws NullPointerException if {@code runnable} or {@code timeout} is null - */ - protected CompletableFuture executeAsync(@NotNull Runnable runnable, @NotNull Duration timeout) { - return CompletableFuture - .runAsync(() -> { - try { - runnable.run(); - } catch (Exception e) { - logger.error(e, "Async DAO operation failed"); - throw new CompletionException(e); - } - }, this.context.dbExecutor()) - .orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS) - .exceptionally(e -> { - if (e instanceof TimeoutException) { - logger.warn("Async DAO operation (void) timed out after %s ms", timeout.toMillis()); - } else { - logger.error(e, "Async DAO operation failed (void outer)"); - } - throw (e instanceof CompletionException) - ? (CompletionException) e - : new CompletionException(e); - }); - } - - /** - * Executes work requiring an initialized DAO, failing fast if the repository has not been started. - * - *

Use this to guard synchronous code paths that assume the DAO is ready.

- * - * @param work supplier executed with the current repository state (non-null) - * @param result type - * @return the supplier's result - * @throws IllegalStateException if the DAO is not initialized (e.g. {@link #start(ConnectionSource)} not called) - * @throws NullPointerException if {@code work} is null - */ - protected R withDao(@NotNull Supplier work) { - Dao current = this.dao; - if (current == null) { - throw new IllegalStateException(getClass().getSimpleName() + ": DAO not initialized. Call RepositoryManager.startAll()"); - } - - return work.get(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMapper.java deleted file mode 100644 index bac72a6..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMapper.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.repository.ormlite; - -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Defines a bidirectional mapper between persistence-layer entities (ORM objects) - * and domain-layer models (business objects). - *

- * This abstraction keeps the repository layer decoupled from the domain layer, - * allowing storage representations to evolve independently of business logic. - * - * @param entity type used for persistence (ORM/DB representation) - * @param domain model type used in business logic - */ -public interface EntityMapper { - - /** - * Maps a domain model instance into its persistence-layer entity representation. - * - * @param domain the domain object to convert (never null) - * @return the corresponding persistence entity - */ - @NotNull E toEntity(@NotNull D domain); - - /** - * Maps a persistence-layer entity into its domain model representation. - * - * @param entity the persistence entity to convert (never null) - * @return the corresponding domain model - */ - @NotNull D toDomain(@NotNull E entity); - - /** - * Converts a list of persistence entities to domain model objects. - *

- * This is a convenience method for bulk transformations. - * - * @param entities list of entities to convert (never null) - * @return list of mapped domain models - */ - default @NotNull List toDomainList(@NotNull List entities) { - return entities.stream().map(this::toDomain).collect(Collectors.toList()); - } - - /** - * Converts a list of domain model objects to persistence entities. - *

- * This is a convenience method for bulk transformations. - * - * @param domains list of domain objects to convert (never null) - * @return list of mapped persistence entities - */ - default @NotNull List toEntityList(@NotNull List domains) { - return domains.stream().map(this::toEntity).collect(Collectors.toList()); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMeta.java deleted file mode 100644 index 37ce878..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/database/repository/ormlite/EntityMeta.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.database.repository.ormlite; - -/** - * Marker interface for database entity metadata containers. - * - *

All metadata interfaces (e.g. {@code UserEntityMeta}) should extend this - * interface to indicate that they define static constants describing database - * schema elements such as table and column names.

- * - *

This provides a unified contract for schema metadata used by ORMLite - * entities, repositories, and migration utilities.

- */ -public interface EntityMeta { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java deleted file mode 100644 index 917412d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/injector/Bind.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.injector; - -import org.panda_lang.utilities.inject.annotations.Injectable; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a field as a core - * dependency that should be automatically registered into the DI container. - *

- * Fields annotated with {@code @BindCore} are discovered by - * PlayTimeCoreBinder and exposed to - * the Panda DI {@link org.panda_lang.utilities.inject.Resources} as singleton instances. - *

- * Only non-static fields are eligible. A {@code null} value at binding time - * results in a bootstrap failure. - */ -@Injectable -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Bind { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java deleted file mode 100644 index 879c498..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/Module.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.GuiPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.ListenerPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.PlaceholderPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.RepositoryPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.TaskPhase; -import org.bukkit.Server; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -/** - * Base lifecycle contract for all PlayTime modules. - * - *

Lifecycle phases: - *

    - *
  1. bind(Resources) – expose and wire resources into the DI context.
  2. - *
  3. init(Injector) – initialize internal state; safe to use bound resources.
  4. - *
- * The manager guarantees {@code bind()} is called before {@code init()}.

- * - *

Threading: modules are initialized on the server main thread unless documented otherwise. - * Implementations should avoid long blocking operations in {@code bind()} and {@code init()}.

- */ -public interface Module extends ModuleOrdered { - - /** - * Binds resources into the DI container. This phase happens before {@link #init(Injector)}. - * - * @param resources DI resources registry (never {@code null}) - */ - void bind(@NotNull Resources resources); - - /** - * Initializes the module. At this point, all resources are already bound. - * - * @param injector DI injector (never {@code null}) - */ - void init(@NotNull Injector injector); - - /** - * Repositories registration phase (optional). - */ - default RepositoryPhase repositories(@NotNull Injector injector) { return repositoryManager -> {}; } - - /** - * Task scheduling phase (optional). - */ - default TaskPhase tasks(@NotNull Injector injector) { return taskScheduler -> {}; } - - /** - * Listener registration phase (optional). - */ - default ListenerPhase listeners(@NotNull Injector injector) { return listenerRegistrar -> {}; } - - /** - * Command registration phase (optional). - */ - default CommandPhase commands(@NotNull Injector injector) { return liteCommandsConfigurer -> {}; } - - /** - * Gui's registration phase (optional). - */ - default GuiPhase guis(@NotNull Injector injector) { return guiRegistry -> {}; } - - /** - * Placeholder's registration phase (optional). - */ - default PlaceholderPhase placeholders(@NotNull Injector injector) { return placeholderRegistry -> {}; } - - /** - * Final hook invoked after all registrations of this feature are complete. - * Runs on the main server thread. - */ - default void afterRegister(@NotNull Plugin plugin, @NotNull Server server, @NotNull Injector injector) {} - - /** - * Default neutral order. Lower values initialize earlier. - */ - @Override - default int order() { return 0; } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java deleted file mode 100644 index a3202c4..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleContext.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; -import com.github.imdmk.playtime.platform.events.BukkitListenerRegistrar; -import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapter; -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import dev.rollczi.litecommands.LiteCommandsBuilder; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -/** - * Immutable container holding all shared services exposed to {@link Module} implementations. - * - *

Acts as a central context object passed to all module lifecycle phases, providing access to: - *

    - *
  • Bukkit plugin and server environment,
  • - *
  • logging, scheduling and repository infrastructure,
  • - *
  • listener/command/GUI registrars,
  • - *
  • placeholder adapter (PlaceholderAPI-enabled or no-op).
  • - *
- * - *

This record is created once during plugin bootstrap and reused throughout the - * module initialization pipeline.

- */ -@Inject -public record ModuleContext( - @NotNull Plugin plugin, - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull TaskScheduler taskScheduler, - @NotNull RepositoryManager repositoryManager, - @NotNull BukkitListenerRegistrar listenerRegistrar, - @NotNull LiteCommandsBuilder liteCommandsBuilder, - @NotNull GuiRegistry guiRegistry, - @NotNull PlaceholderAdapter placeholderAdapter) { -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java deleted file mode 100644 index b0f7382..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleInitializer.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; - -import java.util.List; -import java.util.function.Consumer; - -/** - * Coordinates the entire lifecycle of all {@link Module} instances. - * - *

The initializer executes modules through a strict, ordered pipeline: - *

    - *
  1. {@link #loadAndSort(List)} – instantiation and deterministic ordering,
  2. - *
  3. {@link #bindAll()} – DI resource binding phase,
  4. - *
  5. {@link #initAll()} – internal module initialization,
  6. - *
  7. {@link #registerRepositories()} – repository descriptor registration,
  8. - *
  9. {@link #activateFeatures()} – tasks, listeners, commands, GUIs, placeholders, hooks.
  10. - *
- * - *

Each step is validated against an internal state machine to enforce correct order and avoid - * partially initialized modules. All operations run exclusively on the Bukkit main thread.

- * - *

Errors thrown by individual modules never abort the lifecycle — they are logged and the - * pipeline continues for remaining modules.

- */ -public final class ModuleInitializer { - - private final ModuleContext context; - private final ModuleRegistry registry; - private final Injector injector; - - private State state = State.NEW; - - /** - * Creates a new module initializer. - * - * @param context shared runtime services accessible to modules - * @param registry module registry used for instantiation and lookup - * @param injector dependency injection container used during load/init - */ - public ModuleInitializer( - @NotNull ModuleContext context, - @NotNull ModuleRegistry registry, - @NotNull Injector injector) { - this.context = Validator.notNull(context, "context cannot be null"); - this.registry = Validator.notNull(registry, "moduleRegistry cannot be null"); - this.injector = Validator.notNull(injector, "injector cannot be null"); - } - - /** - * Instantiates and sorts all module types. - * Must be executed first in the module lifecycle. - */ - public void loadAndSort(@NotNull List> types) { - Validator.notNull(types, "types cannot be null"); - - ensureMainThread(); - ensureState(State.NEW, "loadAndSort"); - - registry.setModuleTypes(types); - registry.instantiateAndSort(injector); - - state = State.LOADED; - } - - /** - * Executes the DI binding phase for all modules. - */ - public void bindAll() { - ensureMainThread(); - ensureState(State.LOADED, "bindAll"); - - forEachModule("bindAll", m -> m.bind(injector.getResources())); - state = State.BOUND; - } - - /** - * Invokes the initialization phase for all modules. - */ - public void initAll() { - ensureMainThread(); - ensureState(State.BOUND, "initAll"); - - forEachModule("initAll", m -> m.init(injector)); - state = State.INITIALIZED; - } - - /** - * Registers repository metadata for all modules. - * Does not perform database I/O. - */ - public void registerRepositories() { - ensureMainThread(); - ensureState(State.INITIALIZED, "registerRepositories"); - - forEachModule("registerRepositories", - m -> m.repositories(injector).register(context.repositoryManager())); - - state = State.REPOS_REGISTERED; - } - - /** - * Activates all runtime features: - * tasks, listeners, commands, GUIs, placeholders, and after-register hooks. - */ - public void activateFeatures() { - ensureMainThread(); - ensureState(State.REPOS_REGISTERED, "activateFeatures"); - - forEachModule("activateFeatures", m -> { - m.tasks(injector).schedule(context.taskScheduler()); - m.listeners(injector).register(context.listenerRegistrar()); - m.commands(injector).configure(context.liteCommandsBuilder()); - m.guis(injector).register(context.guiRegistry()); - m.placeholders(injector).register(context.placeholderAdapter()); - m.afterRegister(context.plugin(), context.server(), injector); - }); - - state = State.FEATURES_ACTIVATED; - } - - /** - * Internal helper executing a phase for each module, - * catching and logging exceptions from individual modules. - */ - private void forEachModule(@NotNull String phase, @NotNull Consumer moduleConsumer) { - for (final Module m : registry.modules()) { - try { - moduleConsumer.accept(m); - } catch (Throwable t) { - context.logger().error(t, "%s phase failed for module %s", phase, m.getClass().getName()); - } - } - } - - /** Validates the current initializer state. */ - private void ensureState(@NotNull State required, @NotNull String op) { - if (state != required) { - throw new IllegalStateException(op + " requires state " + required + ", but was " + state); - } - } - - /** Ensures execution on the Bukkit main thread. */ - private void ensureMainThread() { - if (!context.server().isPrimaryThread()) { - throw new IllegalStateException("PluginModuleInitializer must run on Bukkit main thread"); - } - } - - /** Internal lifecycle states used to validate the correct execution order. */ - private enum State { - NEW, - LOADED, - BOUND, - INITIALIZED, - REPOS_REGISTERED, - FEATURES_ACTIVATED - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java deleted file mode 100644 index 846c1a7..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleOrdered.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -/** - * Defines a simple ordering contract for modules. - * Lower values indicate higher priority (executed earlier). - * - *

Ordering is used by the module manager to produce a deterministic - * initialization sequence. When two modules return the same value, the - * manager should apply a stable tie-breaker (e.g., class name).

- */ -public interface ModuleOrdered { - - /** - * Returns the order value of this component. - * Lower values mean earlier execution. - * - * @return the order value (may be negative) - */ - int order(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java deleted file mode 100644 index dff4f9b..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/ModuleRegistry.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Maintains the registry of all {@link Module} classes and their instantiated, sorted instances. - *

- * This registry is responsible for: - *

    - *
  • holding the declared module types,
  • - *
  • instantiating them via dependency injection,
  • - *
  • sorting them deterministically according to {@link Module#order()}.
  • - *
- *

- * The registry itself is stateless between runs: every call to - * {@link #instantiateAndSort(Injector)} rebuilds the internal module list from the current types. - *

- * Thread-safety: This class is not thread-safe and must be accessed from the main server thread. - */ -public final class ModuleRegistry { - - /** Comparator defining deterministic module ordering: lower {@link Module#order()} first, then by class name. */ - private static final Comparator MODULE_ORDER = Comparator - .comparingInt(Module::order) - .thenComparing(m -> m.getClass().getName()); - - private List> moduleTypes = List.of(); - private List modules = List.of(); - - /** - * Replaces the current set of module types with a new, uninitialized list. - *

- * This method does not instantiate modules; call {@link #instantiateAndSort(Injector)} afterwards - * to build and sort the instances. - * - * @param types the new list of module classes (must not be null) - * @param the module type extending {@link Module} - * @throws NullPointerException if {@code types} is null - */ - public void setModuleTypes(@NotNull List> types) { - Validator.notNull(types, "types cannot be null"); - // defensive copy - moduleTypes = List.copyOf(types); - // reset instances - modules = List.of(); - } - - /** - * Instantiates all declared module classes using the provided {@link Injector} - * and sorts them deterministically by {@link Module#order()} and class name. - *

- * This operation is idempotent for the current module types; previous instances are discarded. - * - * @param injector the dependency injector used to construct module instances (never null) - * @throws NullPointerException if {@code injector} is null - */ - public void instantiateAndSort(@NotNull Injector injector) { - Validator.notNull(injector, "injector cannot be null"); - - final List created = new ArrayList<>(moduleTypes.size()); - for (Class type : moduleTypes) { - created.add(injector.newInstance(type)); - } - - created.sort(MODULE_ORDER); - modules = List.copyOf(created); - } - - /** - * Returns an immutable, deterministically sorted view of all instantiated modules. - *

- * The returned list is guaranteed to be ordered by {@link Module#order()} ascending, - * with a lexicographic tiebreaker on the class name for consistency across JVM runs. - * - * @return an unmodifiable list of module instances (never null, may be empty) - */ - public List modules() { - return Collections.unmodifiableList(modules); - } - - /** - * Clears all registered module types and instances. - *

- * After calling this method, the registry returns to its initial empty state. - */ - public void clear() { - moduleTypes = List.of(); - modules = List.of(); - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java deleted file mode 100644 index 7bb0447..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/CommandPhase.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import dev.rollczi.litecommands.LiteCommandsBuilder; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for command registration. - *

- * Implementations should declare and configure commands using the - * provided {@link LiteCommandsBuilder}. - */ -@FunctionalInterface -public interface CommandPhase { - - /** - * Configures and registers commands for this module. - * - * @param configurer the command configurer used to register LiteCommands commands (never {@code null}) - */ - void configure(@NotNull LiteCommandsBuilder configurer); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java deleted file mode 100644 index 60ce01a..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/GuiPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.gui.GuiRegistry; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for GUI registration. - *

- * Implementations should register all inventory or interface GUIs - * via the provided {@link GuiRegistry}. - */ -@FunctionalInterface -public interface GuiPhase { - - /** - * Registers all GUIs provided by this module. - * - * @param guiRegistry the GUI registry used for GUI definitions and factories (never {@code null}) - */ - void register(@NotNull GuiRegistry guiRegistry); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java deleted file mode 100644 index ba7ed99..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/ListenerPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.events.BukkitListenerRegistrar; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for event listener registration. - *

- * Implementations should register Bukkit {@link org.bukkit.event.Listener}s - * using the provided {@link BukkitListenerRegistrar}. - */ -@FunctionalInterface -public interface ListenerPhase { - - /** - * Registers all Bukkit listeners for this module. - * - * @param registrar the listener registrar used to bind Bukkit event listeners (never {@code null}) - */ - void register(@NotNull BukkitListenerRegistrar registrar); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java deleted file mode 100644 index 15f50e4..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/PlaceholderPhase.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.placeholder.adapter.PlaceholderAdapter; -import org.jetbrains.annotations.NotNull; - -@FunctionalInterface -public interface PlaceholderPhase { - - void register(@NotNull PlaceholderAdapter placeholderAdapter); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java deleted file mode 100644 index ad9ab8d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/RepositoryPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for repository registration. - *

- * Implementations should declare repository descriptors only — no database I/O - * should occur during this phase. - */ -@FunctionalInterface -public interface RepositoryPhase { - - /** - * Registers repository descriptors into the {@link RepositoryManager}. - * - * @param manager the repository manager used for descriptor registration (never {@code null}) - */ - void register(@NotNull RepositoryManager manager); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java b/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java deleted file mode 100644 index b3da145..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/infrastructure/module/phase/TaskPhase.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.imdmk.playtime.infrastructure.module.phase; - -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import org.jetbrains.annotations.NotNull; - -/** - * Functional phase interface responsible for scheduling asynchronous or repeating tasks. - *

- * Implementations register all background or periodic tasks needed by a module - * through the provided {@link TaskScheduler}. - */ -@FunctionalInterface -public interface TaskPhase { - - /** - * Registers all scheduled tasks for this module. - * - * @param scheduler the task scheduler used to register Bukkit or async tasks (never {@code null}) - */ - void schedule(@NotNull TaskScheduler scheduler); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java new file mode 100644 index 0000000..946c780 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/Component.java @@ -0,0 +1,13 @@ +package com.github.imdmk.playtime.injector; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public record Component( + @NotNull Class type, + @NotNull A annotation +) {} + + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java new file mode 100644 index 0000000..11ce942 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentFunctional.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +@FunctionalInterface +public interface ComponentFunctional { + + void accept( + @NotNull T instance, + @NotNull A annotation, + @NotNull ComponentProcessorContext context + ); +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java new file mode 100644 index 0000000..34d3ecf --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentManager.java @@ -0,0 +1,89 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.priority.PriorityProvider; +import com.github.imdmk.playtime.injector.processor.ComponentProcessor; +import com.github.imdmk.playtime.injector.processor.ComponentProcessorContext; +import com.github.imdmk.playtime.injector.processor.FunctionalComponentProcessor; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class ComponentManager { + + private static final PriorityProvider DEFAULT_PRIORITY = priority -> Priority.NORMAL; + + private final Injector injector; + + private final ComponentQueue container; + private final ComponentScanner scanner; + + private final Map, ComponentProcessor> processors = new ConcurrentHashMap<>(); + + public ComponentManager(@NotNull Injector injector, @NotNull String basePackage) { + this.injector = injector; + this.container = new ComponentQueue(DEFAULT_PRIORITY); + this.scanner = new ComponentScanner(basePackage); + } + + public ComponentManager setPriorityProvider(@NotNull PriorityProvider provider) { + container.setPriorityProvider(provider); + return this; + } + + public ComponentManager addProcessor(@NotNull ComponentProcessor processor) { + processors.put(processor.annotation(), processor); + return this; + } + + public ComponentManager addProcessor(@NotNull Class> processorClass) { + return addProcessor(injector.newInstance(processorClass)); + } + + public ComponentManager onProcess( + @NotNull Class annotation, + @NotNull ComponentFunctional consumer + ) { + return onProcess(annotation, Object.class, consumer); + } + + public ComponentManager onProcess( + @NotNull Class annotation, + @NotNull Class targetType, + @NotNull ComponentFunctional consumer + ) { + return addProcessor(new FunctionalComponentProcessor<>(annotation, targetType, consumer)); + } + + public ComponentManager onProcess( + @NotNull ComponentFunctional consumer + ) { + return onProcess(NoneAnnotation.class, Object.class, consumer); + } + + public void scanAll() { + for (final Class annotation : processors.keySet()) { + scanner.scan(annotation).forEach(container::add); + } + } + + public void processAll() { + for (final ComponentProcessor processor : processors.values()) { + container.drain(processor.annotation()) + .forEach(component -> process(processor, component)); + } + } + + @SuppressWarnings("unchecked") + private void process( + ComponentProcessor processor, + Component component + ) { + processor.process((Component) component, new ComponentProcessorContext(injector)); + } + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java new file mode 100644 index 0000000..7706750 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentQueue.java @@ -0,0 +1,70 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.priority.PriorityProvider; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class ComponentQueue { + + private final Object lock = new Object(); + + private final EnumMap, Deque>>> componentsByPriority = new EnumMap<>(Priority.class); + + private PriorityProvider priorityProvider; + + ComponentQueue(@NotNull PriorityProvider priorityProvider) { + setPriorityProvider(priorityProvider); + + for (final Priority priority : Priority.values()) { + this.componentsByPriority.put(priority, new HashMap<>()); + } + } + + void setPriorityProvider(@NotNull PriorityProvider priorityProvider) { + this.priorityProvider = priorityProvider; + } + + void add(@NotNull Component component) { + final Priority priority = this.priorityProvider.apply(component); + + synchronized (this.lock) { + this.componentsByPriority + .get(priority) + .computeIfAbsent( + component.annotation().annotationType(), + a -> new ArrayDeque<>() + ) + .addLast(component); + } + } + + List> drain(@NotNull Class annotation) { + final List> result = new ArrayList<>(); + + synchronized (this.lock) { + for (final Priority priority : Priority.values()) { // Iteration order is defined by Priority enum declaration order + final Deque> queue = this.componentsByPriority.get(priority).get(annotation); + if (queue == null) { + continue; + } + + while (!queue.isEmpty()) { + result.add(queue.pollFirst()); + } + } + } + + return result; + } +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java new file mode 100644 index 0000000..a342719 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/ComponentScanner.java @@ -0,0 +1,40 @@ +package com.github.imdmk.playtime.injector; + +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import org.jetbrains.annotations.NotNull; +import org.reflections.Reflections; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.util.Set; +import java.util.stream.Collectors; + +final class ComponentScanner { + + private final Reflections reflections; + + ComponentScanner(@NotNull String basePackage) { + this.reflections = new Reflections(basePackage); + } + + Set> scan(@NotNull Class annotation) { + if (annotation == NoneAnnotation.class) { + return reflections.getSubTypesOf(Object.class).stream() + .filter(type -> !type.isInterface()) + .filter(type -> !Modifier.isAbstract(type.getModifiers())) + .map(type -> new Component<>( + type, + NoneAnnotation.INSTANCE + )) + .collect(Collectors.toSet()); + } + + return reflections.getTypesAnnotatedWith(annotation).stream() + .map(type -> new Component<>( + type, + type.getAnnotation(annotation) + )) + .collect(Collectors.toSet()); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java new file mode 100644 index 0000000..e481087 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/ConfigFile.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ConfigFile { + + Priority priority() default Priority.LOW; +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java new file mode 100644 index 0000000..235b889 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Controller.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Controller { + + Priority priority() default Priority.HIGHEST; + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java new file mode 100644 index 0000000..42dad5a --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/NoneAnnotation.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.injector.annotations; + +import java.lang.annotation.Annotation; + +public class NoneAnnotation implements Annotation { + + public static final NoneAnnotation INSTANCE = new NoneAnnotation(); + + @Override + public Class annotationType() { + return NoneAnnotation.class; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java new file mode 100644 index 0000000..ca19620 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Placeholder.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Placeholder { + + Priority priority() default Priority.HIGHEST; + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java new file mode 100644 index 0000000..08e1c1d --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Repository.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Repository { + + Priority priority() default Priority.HIGH; + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java new file mode 100644 index 0000000..1cfa54e --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/Service.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector.annotations; + +import com.github.imdmk.playtime.injector.priority.Priority; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Service { + + Priority priority() default Priority.NORMAL; + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java new file mode 100644 index 0000000..9c997af --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteArgument.java @@ -0,0 +1,16 @@ +package com.github.imdmk.playtime.injector.annotations.lite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LiteArgument { + + Class type(); + + String name(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java new file mode 100644 index 0000000..90d1840 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteContextual.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.injector.annotations.lite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LiteContextual { + + Class value(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java new file mode 100644 index 0000000..ccb7809 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/annotations/lite/LiteHandler.java @@ -0,0 +1,14 @@ +package com.github.imdmk.playtime.injector.annotations.lite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LiteHandler { + + Class value(); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java new file mode 100644 index 0000000..e327c94 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/AnnotationPriorityProvider.java @@ -0,0 +1,40 @@ +package com.github.imdmk.playtime.injector.priority; + +import com.github.imdmk.playtime.injector.Component; +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +public final class AnnotationPriorityProvider implements PriorityProvider { + + @Override + public Priority apply(Component component) { + final Class componentClass = component.type(); + + if (component.annotation().annotationType() == NoneAnnotation.class) { + return Priority.HIGHEST; + } + + for (final Annotation annotation : componentClass.getAnnotations()) { + try { + final Method method = annotation.annotationType().getMethod("priority"); + + final Object value = method.invoke(annotation); + if (value instanceof Priority priority) { + return priority; + } + } + catch (NoSuchMethodException ignored) { + // doesn't support priority - skip + } + catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to resolve priority for " + componentClass.getName(), e + ); + } + } + + return Priority.NORMAL; + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java new file mode 100644 index 0000000..2ae1acb --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/Priority.java @@ -0,0 +1,5 @@ +package com.github.imdmk.playtime.injector.priority; + +public enum Priority { + LOWEST, LOW, NORMAL, HIGH, HIGHEST +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java new file mode 100644 index 0000000..8c415b8 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/priority/PriorityProvider.java @@ -0,0 +1,10 @@ +package com.github.imdmk.playtime.injector.priority; + +import com.github.imdmk.playtime.injector.Component; + +import java.util.function.Function; + +@FunctionalInterface +public interface PriorityProvider extends Function, Priority> { +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java new file mode 100644 index 0000000..df64683 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/AbstractComponentProcessor.java @@ -0,0 +1,24 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.injector.Component; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public abstract class AbstractComponentProcessor + implements ComponentProcessor { + + @Override + public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { + final Object instance = context.injector().newInstance(component.type()); + this.handle(instance, component.annotation(), context); + } + + protected abstract void handle( + @NotNull Object instance, + @NotNull A annotation, + @NotNull ComponentProcessorContext context + ); +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java new file mode 100644 index 0000000..8c74dac --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessor.java @@ -0,0 +1,15 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.injector.Component; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public interface ComponentProcessor { + + @NotNull Class annotation(); + + void process(@NotNull Component component, @NotNull ComponentProcessorContext context); + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java new file mode 100644 index 0000000..e29ee84 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessorContext.java @@ -0,0 +1,7 @@ +package com.github.imdmk.playtime.injector.processor; + +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; + +public record ComponentProcessorContext(@NotNull Injector injector) { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java new file mode 100644 index 0000000..b762b06 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/ComponentProcessors.java @@ -0,0 +1,71 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.config.ConfigService; +import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; +import com.github.imdmk.playtime.injector.annotations.Controller; +import com.github.imdmk.playtime.injector.annotations.NoneAnnotation; +import com.github.imdmk.playtime.injector.annotations.Repository; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.subscriber.Publisher; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public final class ComponentProcessors { + + @Inject + public static List> defaults( + @NotNull Plugin plugin, + @NotNull ConfigService configService, + @NotNull Publisher publisher + ) { + List> processors = new ArrayList<>(); + + processors.add(new FunctionalComponentProcessor<>( + ConfigFile.class, + ConfigSection.class, + (config, annotation, context) -> configService.create(config.getClass()) + )); + + processors.add(new FunctionalComponentProcessor<>( + Service.class, + Object.class, + (instance, annotation, context) -> {} + )); + + processors.add(new FunctionalComponentProcessor<>( + Repository.class, + OrmLiteRepository.class, + (repository, annotation, context) -> { + try { + repository.start(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + )); + + processors.add(new FunctionalComponentProcessor<>( + Controller.class, + Listener.class, + (listener, annotation, context) -> plugin.getServer().getPluginManager().registerEvents(listener, plugin) + )); + + processors.add(new FunctionalComponentProcessor<>( + NoneAnnotation.class, + Object.class, + (instance, none, context) -> publisher.subscribe(instance) + )); + + return processors; + } +} + + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java new file mode 100644 index 0000000..059312f --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/processor/FunctionalComponentProcessor.java @@ -0,0 +1,46 @@ +package com.github.imdmk.playtime.injector.processor; + +import com.github.imdmk.playtime.injector.Component; +import com.github.imdmk.playtime.injector.ComponentFunctional; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; + +public final class FunctionalComponentProcessor + implements ComponentProcessor { + + private final Class annotationType; + private final Class targetType; + private final ComponentFunctional consumer; + + public FunctionalComponentProcessor( + @NotNull Class annotationType, + @NotNull Class targetType, + @NotNull ComponentFunctional consumer + ) { + this.annotationType = annotationType; + this.targetType = targetType; + this.consumer = consumer; + } + + @Override + public @NotNull Class annotation() { + return this.annotationType; + } + + @Override + @SuppressWarnings("unchecked") + public void process(@NotNull Component component, @NotNull ComponentProcessorContext context) { + final Object instance = context.injector().newInstance(component.type()); + if (!targetType.isInstance(instance)) { + return; + } + + consumer.accept( + (T) instance, + component.annotation(), + context + ); + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java new file mode 100644 index 0000000..333bff8 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/LocalPublisher.java @@ -0,0 +1,59 @@ +package com.github.imdmk.playtime.injector.subscriber; + +import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.Injector; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class LocalPublisher implements Publisher { + + private final Map, List> subscribers = new HashMap<>(); + + private final Injector injector; + + public LocalPublisher(@NotNull Injector injector) { + this.injector = injector; + } + + @Override + public void subscribe(@NotNull Object instance) { + for (final Method method : instance.getClass().getDeclaredMethods()) { + final Subscribe subscribe = method.getAnnotation(Subscribe.class); + if (subscribe == null) { + continue; + } + + final Class eventType = subscribe.event(); + method.setAccessible(true); + + subscribers + .computeIfAbsent(eventType, k -> new ArrayList<>()) + .add(new SubscriberMethod(instance, method)); + } + } + + @Override + public E publish(@NotNull E event) { + final List list = subscribers.get(event.getClass()); + if (list == null) { + return event; + } + + for (final SubscriberMethod subscriber : list) { + final Object instance = subscriber.instance(); + final Method method = subscriber.method(); + injector.invokeMethod(method, instance, event); + } + + return event; + } + + private record SubscriberMethod(@NotNull Object instance, @NotNull Method method) { + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java new file mode 100644 index 0000000..c89eefc --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Publisher.java @@ -0,0 +1,12 @@ +package com.github.imdmk.playtime.injector.subscriber; + +import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; +import org.jetbrains.annotations.NotNull; + +public interface Publisher { + + void subscribe(@NotNull Object subscriber); + + E publish(@NotNull E event); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java new file mode 100644 index 0000000..ac76356 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/Subscribe.java @@ -0,0 +1,17 @@ +package com.github.imdmk.playtime.injector.subscriber; + +import com.github.imdmk.playtime.injector.subscriber.event.SubscribeEvent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Subscribe { + + Class event(); + +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java new file mode 100644 index 0000000..22389b9 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeInitializeEvent.java @@ -0,0 +1,4 @@ +package com.github.imdmk.playtime.injector.subscriber.event; + +public final class PlayTimeInitializeEvent extends SubscribeEvent { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java new file mode 100644 index 0000000..f05458c --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/PlayTimeShutdownEvent.java @@ -0,0 +1,4 @@ +package com.github.imdmk.playtime.injector.subscriber.event; + +public final class PlayTimeShutdownEvent extends SubscribeEvent { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java new file mode 100644 index 0000000..353ea75 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/injector/subscriber/event/SubscribeEvent.java @@ -0,0 +1,4 @@ +package com.github.imdmk.playtime.injector.subscriber.event; + +public abstract class SubscribeEvent { +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java index 0167806..1b8b7a3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageConfig.java @@ -6,10 +6,12 @@ import com.github.imdmk.playtime.config.ConfigSection; import com.github.imdmk.playtime.feature.playtime.messages.ENPlayTimeMessages; import com.github.imdmk.playtime.feature.reload.messages.ENReloadMessages; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; +@ConfigFile public final class MessageConfig extends ConfigSection { @Comment({ @@ -89,14 +91,14 @@ public final class MessageConfig extends ConfigSection { public ENReloadMessages reloadMessages = new ENReloadMessages(); @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> registry.register( new MultificationSerdesPack(NoticeResolverDefaults.createRegistry()) ); } @Override - public @NotNull String getFileName() { + public @NotNull String fileName() { return "messageConfig.yml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java index 6cf5568..53d0abf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/message/MessageService.java @@ -2,100 +2,44 @@ import com.eternalcode.multification.adventure.AudienceConverter; import com.eternalcode.multification.bukkit.BukkitMultification; -import com.eternalcode.multification.notice.provider.NoticeProvider; import com.eternalcode.multification.translation.TranslationProvider; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; +import com.github.imdmk.playtime.platform.adventure.AdventureComponents; import net.kyori.adventure.platform.AudienceProvider; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.ComponentSerializer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; -/** - * Central message service for the PlayTime plugin, bridging plugin messages with - * the Adventure and MiniMessage APIs through {@link BukkitMultification}. - * - *

This implementation provides a high-level abstraction for sending messages, - * notices, and components to Bukkit {@link CommandSender}s, automatically converting - * them into Adventure audiences via {@link AudienceProvider}.

- * - *

Features:

- *
    - *
  • Uses {@link MessageConfig} as the single translation source (locale-agnostic).
  • - *
  • Serializes and deserializes Adventure {@link Component}s using {@link MiniMessage}.
  • - *
  • Converts Bukkit {@link CommandSender}s into Adventure audiences automatically.
  • - *
  • Supports both {@link Player} and console senders transparently.
  • - *
- * - *

Thread-safety: Message sending is thread-safe and may be performed - * off the main thread. Underlying Adventure components are immutable and safe for reuse.

- * - * @see MessageConfig - * @see BukkitMultification - * @see MiniMessage - * @see AudienceProvider - * @see NoticeProvider - */ +@Service(priority = Priority.LOW) public final class MessageService extends BukkitMultification { - private static final MiniMessage DEFAULT_MINI_MESSAGE = MiniMessage.miniMessage(); - private final MessageConfig messageConfig; private final AudienceProvider audienceProvider; - private final MiniMessage miniMessage; - - public MessageService( - @NotNull MessageConfig messageConfig, - @NotNull AudienceProvider audienceProvider, - @NotNull MiniMessage miniMessage - ) { - this.messageConfig = Validator.notNull(messageConfig, "messageConfig"); - this.audienceProvider = Validator.notNull(audienceProvider, "audienceProvider"); - this.miniMessage = Validator.notNull(miniMessage, "miniMessage"); - } - public MessageService(@NotNull MessageConfig messageConfig, @NotNull BukkitAudiences bukkitAudiences) { - this(messageConfig, bukkitAudiences, DEFAULT_MINI_MESSAGE); + @Inject + public MessageService(@NotNull Plugin plugin, @NotNull MessageConfig messageConfig) { + this.messageConfig = messageConfig; + this.audienceProvider = BukkitAudiences.create(plugin); } - public MessageService(@NotNull MessageConfig messageConfig, @NotNull Plugin plugin) { - this(messageConfig, BukkitAudiences.create(plugin), DEFAULT_MINI_MESSAGE); - } - - /** - * Returns a translation provider that always returns the same {@link MessageConfig} instance, - * ignoring locale differences. - * - * @return locale-agnostic translation provider - */ @Override protected @NotNull TranslationProvider translationProvider() { return provider -> messageConfig; } - /** - * Returns the {@link MiniMessage}-based component serializer. - * - * @return component serializer for text serialization/deserialization - */ @Override protected @NotNull ComponentSerializer serializer() { - return miniMessage; + return AdventureComponents.miniMessage(); } - /** - * Converts Bukkit {@link CommandSender}s into Adventure audiences - * using the configured {@link AudienceProvider}. - * - *

Players are mapped to player audiences, while other senders - * (e.g., console or command blocks) are mapped to {@link AudienceProvider#console()}.

- * - * @return non-null audience converter - */ @Override protected @NotNull AudienceConverter audienceConverter() { return sender -> { @@ -106,30 +50,8 @@ public MessageService(@NotNull MessageConfig messageConfig, @NotNull Plugin plug }; } - /** - * Sends a localized or static notice message to the specified Bukkit {@link CommandSender}. - * - *

The notice is resolved through the active {@link MessageConfig} - * and rendered using {@link MiniMessage} formatting.

- * - * @param sender non-null Bukkit command sender (player, console, etc.) - * @param notice non-null notice provider bound to {@link MessageConfig} - * @throws NullPointerException if {@code sender} or {@code notice} is null - */ - public void send(@NotNull CommandSender sender, @NotNull NoticeProvider notice) { - Validator.notNull(sender, "sender"); - Validator.notNull(notice, "notice"); - create().viewer(sender).notice(notice).send(); - } - - /** - * Shuts down the underlying {@link AudienceProvider} to release Adventure resources. - * - *

This should be called during plugin disable to avoid memory leaks or - * lingering references to the plugin classloader.

- */ + @Subscribe(event = PlayTimeShutdownEvent.class) public void shutdown() { - Validator.notNull(audienceProvider, "audienceProvider cannot be null"); audienceProvider.close(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/ComponentSerializer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponentSerializer.java similarity index 91% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/ComponentSerializer.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponentSerializer.java index f4ce438..6ecaad9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/ComponentSerializer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponentSerializer.java @@ -7,7 +7,7 @@ import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; -public final class ComponentSerializer implements ObjectSerializer { +public final class AdventureComponentSerializer implements ObjectSerializer { @Override public boolean supports(@NotNull Class type) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java index 9de1aea..5639c0a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureComponents.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.adventure; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.format.TextDecoration; @@ -11,21 +10,6 @@ import java.util.Collection; import java.util.List; -/** - * Utilities for working with Adventure {@link Component}s via MiniMessage. - * Platform-agnostic (no Bukkit types). Thread-safe and stateless. - * - *

Notes: - *

    - *
  • All returned collections are unmodifiable.
  • - *
  • Accepts {@link CharSequence} for flexibility.
  • - *
- * - *
- *   Component c = AdventureComponents.text("<red>Hello");
- *   Component plain = AdventureComponents.withoutItalics(c);
- * 
- */ public final class AdventureComponents { private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); @@ -34,26 +18,11 @@ private AdventureComponents() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Deserializes a MiniMessage-formatted text into a {@link Component}. - * - * @param text the MiniMessage-formatted text - * @return the deserialized component - */ - public static @NotNull Component text(@NotNull CharSequence text) { - Validator.notNull(text, "text"); + public static Component text(@NotNull CharSequence text) { return MINI_MESSAGE.deserialize(text.toString()); } - /** - * Deserializes multiple MiniMessage-formatted texts into a list of {@link Component}s. - * - * @param texts array of MiniMessage-formatted texts - * @return an unmodifiable list of deserialized components - */ - public static @NotNull List text(@NotNull CharSequence... texts) { - Validator.notNull(texts, "texts"); - + public static List text(@NotNull CharSequence... texts) { final List out = new ArrayList<>(texts.length); for (CharSequence text : texts) { out.add(MINI_MESSAGE.deserialize(text.toString())); @@ -62,121 +31,59 @@ private AdventureComponents() { return List.copyOf(out); } - /** - * Deserializes a collection of MiniMessage-formatted texts into {@link Component}s. - * - * @param texts iterable of MiniMessage-formatted texts - * @return an unmodifiable list of deserialized components - */ - public static @NotNull List text(@NotNull Iterable texts) { - Validator.notNull(texts, "texts"); - + public static List text(@NotNull Iterable texts) { final List out = new ArrayList<>(); for (CharSequence text : texts) { - Validator.notNull(text, "texts contains null element"); out.add(MINI_MESSAGE.deserialize(text.toString())); } return List.copyOf(out); } - /** - * Returns a copy of the given component with italics disabled. - * - * @param component the source component - * @return a new component without italics - */ - public static @NotNull Component withoutItalics(@NotNull Component component) { - Validator.notNull(component, "component"); + public static Component withoutItalics(@NotNull Component component) { return component.decoration(TextDecoration.ITALIC, false); } - /** - * Deserializes a MiniMessage-formatted text and removes italics. - * - * @param text the MiniMessage-formatted text - * @return a deserialized component without italics - */ - public static @NotNull Component withoutItalics(@NotNull CharSequence text) { + public static Component withoutItalics(@NotNull CharSequence text) { return withoutItalics(text(text)); } - /** - * Converts a {@link ComponentLike} into a {@link Component} and removes italics. - * - * @param like the source component-like object - * @return a new component without italics - */ - public static @NotNull Component withoutItalics(@NotNull ComponentLike like) { - Validator.notNull(like, "component"); + public static Component withoutItalics(@NotNull ComponentLike like) { return like.asComponent().decoration(TextDecoration.ITALIC, false); } - /** - * Disables italics for all given components. - * - * @param strings iterable of strings objects - * @return an unmodifiable list of components with italics disabled - */ - public static @NotNull List withoutItalics(@NotNull String... strings) { - Validator.notNull(strings, "components cannot be null"); - + public static List withoutItalics(@NotNull String... strings) { final List out = new ArrayList<>(); for (final String string : strings) { - Validator.notNull(string, "components contains null element"); out.add(withoutItalics(string)); } return List.copyOf(out); } - /** - * Serializes a {@link Component} into a MiniMessage-formatted string. - * - * @param component the component to serialize - * @return the serialized MiniMessage string - */ - public static @NotNull String serialize(@NotNull Component component) { - Validator.notNull(component, "component cannot be null"); + public static String serialize(@NotNull Component component) { return MINI_MESSAGE.serialize(component); } - /** - * Serializes multiple components into MiniMessage-formatted strings. - * - * @param components collection of component-like objects - * @return an unmodifiable list of serialized strings - */ - public static @NotNull List serialize(@NotNull Collection components) { - Validator.notNull(components, "components cannot be null"); - + public static List serialize(@NotNull Collection components) { final List out = new ArrayList<>(components.size()); for (final ComponentLike component : components) { - Validator.notNull(component, "components contains null element"); out.add(MINI_MESSAGE.serialize(component.asComponent())); } return List.copyOf(out); } - /** - * Serializes multiple components and joins them with the given delimiter. - * - * @param components collection of component-like objects - * @param delimiter string separator between serialized components - * @return a single joined MiniMessage string - */ - public static @NotNull String serializeJoined(@NotNull Collection components, - @NotNull CharSequence delimiter) { - Validator.notNull(components, "components cannot be null"); - Validator.notNull(delimiter, "delimiter cannot be null"); - + public static String serializeJoined(@NotNull Collection components, @NotNull CharSequence delimiter) { final List serialized = new ArrayList<>(components.size()); for (final ComponentLike component : components) { - Validator.notNull(component, "components contains null element"); serialized.add(MINI_MESSAGE.serialize(component.asComponent())); } return String.join(delimiter, serialized); } + + public static MiniMessage miniMessage() { + return MINI_MESSAGE; + } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java index 744887d..2988ba1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventureFormatter.java @@ -1,8 +1,6 @@ package com.github.imdmk.playtime.platform.adventure; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextReplacementConfig; import org.jetbrains.annotations.NotNull; import java.util.Comparator; @@ -10,75 +8,36 @@ import java.util.Map; import java.util.stream.Collectors; -/** - * Utility for applying {@link AdventurePlaceholders} to {@link Component} trees or plain strings. - *

Stateless and thread-safe.

- */ public final class AdventureFormatter { private AdventureFormatter() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** - * Applies placeholders to a plain string and returns a formatted {@link Component}. - * - * @param input plain text input - * @param placeholders placeholders to apply - * @return formatted component - */ - public static @NotNull Component format(@NotNull String input, @NotNull AdventurePlaceholders placeholders) { - Validator.notNull(input, "input"); + public static Component format(@NotNull String input, @NotNull AdventurePlaceholders placeholders) { return format(AdventureComponents.text(input), placeholders); } - /** - * Applies placeholders to each {@link Component} in a list. - * - * @param components list of components - * @param placeholders placeholders to apply - * @return formatted components - */ - public static @NotNull List format(@NotNull List components, @NotNull AdventurePlaceholders placeholders) { - Validator.notNull(components, "components"); + public static List format(@NotNull List components, @NotNull AdventurePlaceholders placeholders) { return components.stream() .map(component -> format(component, placeholders)) .collect(Collectors.toList()); } - /** - * Applies placeholders to a single {@link Component}. - * - * @param input component to format - * @param placeholders placeholders to apply - * @return formatted component - */ - public static @NotNull Component format(@NotNull Component input, @NotNull AdventurePlaceholders placeholders) { - Validator.notNull(input, "input"); - Validator.notNull(placeholders, "placeholders"); - + public static Component format(@NotNull Component input, @NotNull AdventurePlaceholders placeholders) { if (placeholders.isEmpty()) { return input; } // Sort keys by descending length to avoid substring overlap - List> ordered = placeholders.asMap().entrySet().stream() + final List> ordered = placeholders.asMap().entrySet().stream() .sorted(Comparator.>comparingInt(e -> e.getKey().length()).reversed()) - .collect(Collectors.toList()); - - Component out = input; - for (final Map.Entry e : ordered) { - var key = e.getKey(); - var replacement = e.getValue(); - - var config = TextReplacementConfig.builder() - .matchLiteral(key) - .replacement(replacement) - .build(); - - out = out.replaceText(config); - } + .toList(); - return out; + return input.replaceText(builder -> { + for (var entry : ordered) { + builder.matchLiteral(entry.getKey()).replacement(entry.getValue()); + } + }); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java index 5a7926c..7481fec 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/adventure/AdventurePlaceholders.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.adventure; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -10,13 +9,6 @@ import java.util.LinkedHashMap; import java.util.Map; -/** - * Immutable container mapping literal placeholder keys to Adventure {@link Component} values. - *

- * Instances are created via the {@link Builder}. Once built, the mapping is read-only. - *

- * Thread-safety: Fully immutable and safe for concurrent use. - */ public final class AdventurePlaceholders { private static final AdventurePlaceholders EMPTY = new AdventurePlaceholders(Map.of()); @@ -24,130 +16,59 @@ public final class AdventurePlaceholders { private final Map map; private AdventurePlaceholders(@NotNull Map map) { - Validator.notNull(map, "map"); this.map = Collections.unmodifiableMap(map); } - /** - * Returns an unmodifiable view of all placeholder mappings. - * - * @return unmodifiable placeholder map - */ @Unmodifiable - @NotNull public Map asMap() { return map; } - /** - * Returns the number of registered placeholders. - * - * @return placeholder count - */ public int size() { return map.size(); } - /** - * Checks if the placeholder map is empty. - * - * @return {@code true} if no placeholders are defined - */ public boolean isEmpty() { return map.isEmpty(); } - /** - * Returns a shared immutable empty instance. - * - * @return empty placeholder container - */ - public static @NotNull AdventurePlaceholders empty() { + public static AdventurePlaceholders empty() { return EMPTY; } - /** - * Creates a new builder for {@link AdventurePlaceholders}. - * - * @return new builder instance - */ - public static @NotNull Builder builder() { + public static Builder builder() { return new Builder(); } - /** - * Fluent builder for {@link AdventurePlaceholders}. - */ public static final class Builder { private final Map entries = new LinkedHashMap<>(); - /** - * Adds a literal → component mapping. - * - * @param key literal placeholder key - * @param value replacement component - * @return this builder for chaining - */ @Contract("_,_ -> this") - public @NotNull Builder with(@NotNull String key, @NotNull Component value) { - Validator.notNull(key, "key"); - Validator.notNull(value, "value"); + public Builder with(@NotNull String key, @NotNull Component value) { this.entries.put(key, value); return this; } - /** - * Adds a literal → plain text mapping (converted to {@link Component#text(String)}). - * - * @param key literal placeholder key - * @param value replacement text - * @return this builder for chaining - */ @Contract("_,_ -> this") - public @NotNull Builder with(@NotNull String key, @NotNull String value) { - Validator.notNull(key, "key cannot be null"); - Validator.notNull(value, "value cannot be null"); + public Builder with(@NotNull String key, @NotNull String value) { this.entries.put(key, Component.text(value)); return this; } - /** - * Adds all entries from another {@link AdventurePlaceholders}. - * - * @param other another placeholder container - * @return this builder for chaining - */ @Contract("_ -> this") - public @NotNull Builder with(@NotNull AdventurePlaceholders other) { - Validator.notNull(other, "other cannot be null"); + public Builder with(@NotNull AdventurePlaceholders other) { this.entries.putAll(other.asMap()); return this; } - /** - * Adds a placeholder using any object value. - * The value is converted to plain text via {@link String#valueOf(Object)}. - * - * @param key placeholder key - * @param value object to convert and insert - * @return this builder for chaining - * @throws NullPointerException if key or value is null - */ @Contract("_,_ -> this") - public @NotNull Builder with(@NotNull String key, @NotNull Object value) { - Validator.notNull(key, "key cannot be null"); - Validator.notNull(value, "value cannot be null"); + public Builder with(@NotNull String key, @NotNull Object value) { this.entries.put(key, Component.text(String.valueOf(value))); return this; } - /** - * Builds an immutable {@link AdventurePlaceholders} instance. - * - * @return immutable placeholder container - */ - public @NotNull AdventurePlaceholders build() { + public AdventurePlaceholders build() { if (this.entries.isEmpty()) { return EMPTY; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java new file mode 100644 index 0000000..f8a6203 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/BukkitEventCaller.java @@ -0,0 +1,34 @@ +package com.github.imdmk.playtime.platform.event; + +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; +import org.bukkit.Server; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; + +@Service(priority = Priority.LOW) +public final class BukkitEventCaller implements EventCaller { + + private final Server server; + private final TaskScheduler scheduler; + + @Inject + public BukkitEventCaller(@NotNull Server server, @NotNull TaskScheduler scheduler) { + this.server = server; + this.scheduler = scheduler; + } + + @Override + public E callEvent(@NotNull E event) { + if (event.isAsynchronous() || server.isPrimaryThread()) { + server.getPluginManager().callEvent(event); + return event; + } + + scheduler.runSync(() -> server.getPluginManager().callEvent(event)); + return event; + } + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java new file mode 100644 index 0000000..4de83c9 --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/event/EventCaller.java @@ -0,0 +1,10 @@ +package com.github.imdmk.playtime.platform.event; + +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; + +public interface EventCaller { + + E callEvent(@NotNull E event); + +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java deleted file mode 100644 index 5a7a3a5..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitEventCaller.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.imdmk.playtime.platform.events; - -import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.Server; -import org.bukkit.event.Event; -import org.jetbrains.annotations.NotNull; - -/** - * Utility wrapper for safely firing Bukkit {@link Event}s. - * Ensures that synchronous events are always fired on the primary server thread. - */ -public final class BukkitEventCaller { - - private final Server server; - private final TaskScheduler scheduler; - - public BukkitEventCaller(@NotNull Server server, @NotNull TaskScheduler scheduler) { - this.server = Validator.notNull(server, "server cannot be null"); - this.scheduler = Validator.notNull(scheduler, "scheduler cannot be null"); - } - - /** - * Calls the specified Bukkit event ensuring correct thread usage: - *

    - *
  • Asynchronous events are fired on the current thread;
  • - *
  • Synchronous events are fired on the primary server thread.
  • - *
- */ - public T callEvent(@NotNull T event) { - Validator.notNull(event, "event cannot be null"); - - if (event.isAsynchronous()) { - server.getPluginManager().callEvent(event); - return event; - } - - if (server.isPrimaryThread()) { - server.getPluginManager().callEvent(event); - } else { - scheduler.runSync(() -> server.getPluginManager().callEvent(event)); - } - - return event; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java deleted file mode 100644 index 10f1f0b..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/events/BukkitListenerRegistrar.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.imdmk.playtime.platform.events; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.event.Listener; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; - -/** - * Utility component responsible for registering Bukkit {@link Listener}s. - *

- * This registrar provides two registration modes: - *

    - *
  • Direct registration of pre-instantiated listener objects.
  • - *
  • Automatic instantiation and field injection through {@link Injector}.
  • - *
- *

- * All listeners are registered using the plugin's {@link org.bukkit.plugin.PluginManager}. - */ -public final class BukkitListenerRegistrar { - - private final Plugin plugin; - - /** - * Creates a new registrar for the given Bukkit plugin. - * - * @param plugin the plugin instance used for listener registration - * @throws NullPointerException if the plugin is null - */ - public BukkitListenerRegistrar(@NotNull Plugin plugin) { - this.plugin = Validator.notNull(plugin, "plugin cannot be null"); - } - - /** - * Registers the provided listener instances with the Bukkit {@link org.bukkit.plugin.PluginManager}. - * - * @param listeners the listener instances to register - * @throws NullPointerException if the listeners array or any listener is null - */ - public void register(@NotNull Listener... listeners) { - Validator.notNull(listeners, "listeners cannot be null"); - for (final Listener listener : listeners) { - plugin.getServer().getPluginManager().registerEvents(listener, plugin); - } - } - - /** - * Creates and registers listeners using the provided {@link Injector}. - *

- * Each listener class is instantiated and its dependencies are injected automatically. - * - * @param injector the dependency injector to use for listener instantiation - * @param listeners the listener classes to create and register - * @throws NullPointerException if the injector, the listener array, or any class is null - */ - @SafeVarargs - public final void register(@NotNull Injector injector, @NotNull Class... listeners) { - Validator.notNull(injector, "injector cannot be null"); - Validator.notNull(listeners, "listeners cannot be null"); - - for (final Class listenerClass : listeners) { - register(injector.newInstance(listenerClass)); - } - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java deleted file mode 100644 index c2ebe85..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiModule.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime.platform.gui; - -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.platform.gui.render.GuiRenderer; -import com.github.imdmk.playtime.platform.gui.render.TriumphGuiRenderer; -import com.github.imdmk.playtime.platform.gui.view.GuiOpener; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class GuiModule implements Module { - - private GuiOpener guiOpener; - private GuiRenderer guiRenderer; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(GuiOpener.class).assignInstance(() -> this.guiOpener); - resources.on(GuiRenderer.class).assignInstance(() -> this.guiRenderer); - } - - @Override - public void init(@NotNull Injector injector) { - this.guiOpener = injector.newInstance(GuiOpener.class); - this.guiRenderer = injector.newInstance(TriumphGuiRenderer.class); - } - - @Override - public int order() { - return 10; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java index c996139..a6a7df3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiRegistry.java @@ -1,6 +1,7 @@ package com.github.imdmk.playtime.platform.gui; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -10,26 +11,14 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -/** - * Thread-safe registry of {@link IdentifiableGui}, keyed by normalized id. - * Invariant: at most one GUI per concrete class (maintained class index). - */ +@Service(priority = Priority.LOW) public final class GuiRegistry { private final Map byId = new ConcurrentHashMap<>(); private final Map, IdentifiableGui> byClass = new ConcurrentHashMap<>(); - /** - * Registers (or replaces) GUI by its normalized identifier. - * Also updates class index (one instance per class). - * - * @return previously registered GUI under the same id, or {@code null}. - */ - @Nullable - public IdentifiableGui register(@NotNull IdentifiableGui gui) { - Validator.notNull(gui, "gui cannot be null"); - final String id = normalize(Validator.notNull(gui.getId(), "gui identifier cannot be null")); - + public void register(@NotNull IdentifiableGui gui) { + final String id = normalize(gui.getId()); final IdentifiableGui previous = byId.put(id, gui); // maintain class index (assume single instance per class) @@ -40,18 +29,10 @@ public IdentifiableGui register(@NotNull IdentifiableGui gui) { if (previous != null && previous.getClass() != type) { byClass.compute(previous.getClass(), (k, current) -> current == previous ? null : current); } - return previous; } - /** - * Registers GUI only if absent under the same id. - * - * @return {@code true} if registered, {@code false} if id existed. - */ public boolean registerIfAbsent(@NotNull IdentifiableGui gui) { - Validator.notNull(gui, "gui cannot be null"); - final String id = normalize(Validator.notNull(gui.getId(), "gui identifier cannot be null")); - + final String id = normalize(gui.getId()); final IdentifiableGui existing = byId.putIfAbsent(id, gui); if (existing == null) { // we won the race; update class index @@ -61,12 +42,9 @@ public boolean registerIfAbsent(@NotNull IdentifiableGui gui) { return false; } - /** - * Unregisters GUI by id. Updates class index if pointing to same instance. - */ @Nullable public IdentifiableGui unregister(@NotNull String id) { - final String key = normalize(Validator.notNull(id, "id cannot be null")); + final String key = normalize(id); final IdentifiableGui removed = byId.remove(key); if (removed != null) { byClass.compute(removed.getClass(), (k, current) -> current == removed ? null : current); @@ -74,40 +52,28 @@ public IdentifiableGui unregister(@NotNull String id) { return removed; } - /** - * Case-insensitive lookup by id (whitespace-insensitive). - */ @Nullable public IdentifiableGui getById(@NotNull String id) { - final String key = normalize(Validator.notNull(id, "id cannot be null")); - return byId.get(key); + return byId.get(normalize(id)); } - /** - * O(1) exact type lookup. Assumes at most one instance per class. - */ @Nullable + @SuppressWarnings("unchecked") public T getByClass(@NotNull Class type) { - Validator.notNull(type, "type cannot be null"); final IdentifiableGui gui = byClass.get(type); - @SuppressWarnings("unchecked") - final T cast = (T) gui; - return cast; + return (T) gui; } public boolean isRegistered(@NotNull String id) { - final String key = normalize(Validator.notNull(id, "id cannot be null")); - return byId.containsKey(key); + return byId.containsKey(normalize(id)); } - /** Immutable snapshot of normalized ids. */ @Unmodifiable public Set ids() { return Set.copyOf(byId.keySet()); } - /** Current strategy: trim + lowercased (Locale.ROOT). */ - private static String normalize(@NotNull String id) { + private static String normalize(String id) { final String trimmed = id.trim(); return trimmed.toLowerCase(Locale.ROOT); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java index a17c8e5..1dd71ac 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/GuiType.java @@ -1,33 +1,8 @@ package com.github.imdmk.playtime.platform.gui; -/** - * Defines the supported GUI layout types within the plugin. - * - *

Each type represents a different interaction model for displaying items.

- */ public enum GuiType { - - /** - * A fixed-size GUI without pagination or scrolling. - * Suitable for simple static interfaces. - */ STANDARD, - - /** - * A multipage GUI used for displaying large sets of items. - * Provides navigation between pages. - */ PAGINATED, - - /** - * A GUI that supports vertical scrolling. - * Ideal for lists of items exceeding the visible height. - */ SCROLLING_VERTICAL, - - /** - * A GUI that supports horizontal scrolling. - * Useful for side-by-side item navigation. - */ SCROLLING_HORIZONTAL } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java index f9f0a9d..cacc917 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/IdentifiableGui.java @@ -1,18 +1,9 @@ package com.github.imdmk.playtime.platform.gui; import org.jetbrains.annotations.NotNull; -/** - * Represents a GUI component that can be uniquely identified by a string identifier. - *

- * Useful for registering and retrieving GUI instances by their identifier. - */ @FunctionalInterface public interface IdentifiableGui { - /** - * Returns the unique identifier for this GUI. - * - * @return the non-null unique identifier string - */ @NotNull String getId(); + } \ No newline at end of file diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java index a15678f..44a0e4e 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/ConfigurableGui.java @@ -2,27 +2,13 @@ import com.github.imdmk.playtime.platform.gui.GuiType; import net.kyori.adventure.text.Component; -import org.jetbrains.annotations.NotNull; -/** - * Represents a configurable GUI loaded from configuration. - * Implementations should provide all basic GUI metadata and content definitions. - */ public interface ConfigurableGui { - /** - * @return GUI title as Adventure {@link Component} - */ - @NotNull Component title(); + Component title(); - /** - * @return GUI type (e.g. {@link GuiType#STANDARD}, {@link GuiType#PAGINATED}) - */ - @NotNull GuiType type(); + GuiType type(); - /** - * @return GUI rows - */ int rows(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java index 4b08026..f1069b1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/GuiConfig.java @@ -2,7 +2,8 @@ import com.github.imdmk.playtime.config.ConfigSection; import com.github.imdmk.playtime.feature.playtime.gui.PlayTimeTopGuiConfig; -import com.github.imdmk.playtime.platform.adventure.ComponentSerializer; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; +import com.github.imdmk.playtime.platform.adventure.AdventureComponentSerializer; import com.github.imdmk.playtime.platform.gui.item.ItemGuiSerializer; import com.github.imdmk.playtime.platform.serdes.EnchantmentSerializer; import com.github.imdmk.playtime.platform.serdes.SoundSerializer; @@ -10,6 +11,7 @@ import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; +@ConfigFile public final class GuiConfig extends ConfigSection { @Comment({"#", "# Playtime top GUI", "#"}) @@ -19,9 +21,9 @@ public final class GuiConfig extends ConfigSection { public NavigationBarConfig navigationBar = new NavigationBarConfig(); @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> { - registry.register(new ComponentSerializer()); + registry.register(new AdventureComponentSerializer()); registry.register(new ItemGuiSerializer()); registry.register(new EnchantmentSerializer()); registry.register(new SoundSerializer()); @@ -29,7 +31,7 @@ public final class GuiConfig extends ConfigSection { } @Override - public @NotNull String getFileName() { + public @NotNull String fileName() { return "guiConfig.yml"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java index 8e828c1..c3755a5 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/config/NavigationBarConfig.java @@ -6,12 +6,6 @@ import eu.okaeri.configs.annotation.Comment; import org.bukkit.Material; -/** - * Configuration for navigation items used in paginated GUIs. - *

- * Defines visual representation and behavior for navigation controls: - * next, previous, and exit buttons displayed in inventory-based interfaces. - */ public final class NavigationBarConfig extends OkaeriConfig { @Comment({ @@ -100,4 +94,5 @@ public final class NavigationBarConfig extends OkaeriConfig { " " )) .build(); + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java index e5cfff8..d901870 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiBuilderFactory.java @@ -1,39 +1,20 @@ package com.github.imdmk.playtime.platform.gui.factory; import com.github.imdmk.playtime.platform.gui.GuiType; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.builder.gui.BaseGuiBuilder; import dev.triumphteam.gui.components.ScrollType; import dev.triumphteam.gui.guis.Gui; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; -/** - * Factory for creating TriumphGUI {@link BaseGuiBuilder} instances - * based on a provided {@link GuiType}. - *

- * Supports standard, paginated, and scrolling (vertical/horizontal) GUIs. - */ public final class GuiBuilderFactory { private GuiBuilderFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Returns a TriumphGUI builder matching the given {@link GuiType}. - * - * @param type the GUI type - * @param rows the GUI rows - * @return a new {@link BaseGuiBuilder} instance for the given type - * @throws IllegalArgumentException if {@code type} is {@code null} - */ - @Contract(pure = true) - public static @NotNull BaseGuiBuilder forType(@NotNull GuiType type, int rows) { - Validator.notNull(type, "type cannot be null"); - + public static BaseGuiBuilder forType(@NotNull GuiType type, int rows) { return switch (type) { case STANDARD -> Gui.gui().rows(rows); case PAGINATED -> Gui.paginated().rows(rows); @@ -42,20 +23,8 @@ private GuiBuilderFactory() { }; } - /** - * Creates and immediately customizes a TriumphGUI builder. - * - * @param type the GUI type - * @param rows the GUI rows - * @param editConsumer consumer for post-creation customization (e.g., size, disableAllInteractions) - * @return a modified {@link BaseGuiBuilder} instance - * @throws IllegalArgumentException if {@code type} or {@code editConsumer} is {@code null} - */ - public static @NotNull BaseGuiBuilder forType(@NotNull GuiType type, int rows, @NotNull Consumer> editConsumer) { - Validator.notNull(type, "type cannot be null"); - Validator.notNull(editConsumer, "editConsumer cannot be null"); - - BaseGuiBuilder builder = forType(type, rows); + public static BaseGuiBuilder forType(@NotNull GuiType type, int rows, @NotNull Consumer> editConsumer) { + final BaseGuiBuilder builder = forType(type, rows); editConsumer.accept(builder); return builder; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java index cee734d..190904a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/factory/GuiFactory.java @@ -1,54 +1,25 @@ package com.github.imdmk.playtime.platform.gui.factory; import com.github.imdmk.playtime.platform.gui.config.ConfigurableGui; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; -/** - * Factory for creating {@link BaseGui} instances from {@link ConfigurableGui}. - *

- * Responsibilities: - *

    - *
  • Delegate to {@link GuiBuilderFactory} based on configured type,
  • - *
  • Apply base attributes (e.g. title),
  • - *
  • Optionally allow post-creation customization via a {@link Consumer}.
  • - *
- */ public final class GuiFactory { private GuiFactory() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Builds a GUI instance from the provided configuration. - * - * @param config the GUI configuration - * @return a new {@link BaseGui} instance - * @throws IllegalArgumentException if the GUI type is unsupported - */ - public static @NotNull BaseGui build(@NotNull ConfigurableGui config) { - Validator.notNull(config, "config cannot be null"); + public static BaseGui build(@NotNull ConfigurableGui config) { return GuiBuilderFactory.forType(config.type(), config.rows()) .title(config.title()) .create(); } - /** - * Builds and immediately customizes a GUI using the provided consumer. - * - * @param config the GUI configuration - * @param editConsumer consumer to modify the GUI instance before returning - * @return the configured {@link BaseGui} - */ - public static @NotNull BaseGui build(@NotNull ConfigurableGui config, @NotNull Consumer editConsumer) { - Validator.notNull(config, "config cannot be null"); - Validator.notNull(editConsumer, "editConsumer cannot be null"); - - BaseGui gui = build(config); + public static BaseGui build(@NotNull ConfigurableGui config, @NotNull Consumer editConsumer) { + final BaseGui gui = build(config); editConsumer.accept(gui); return gui; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java index f9f967b..c0db2da 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGui.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.gui.item; -import com.github.imdmk.playtime.shared.validate.Validator; import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; @@ -16,12 +15,6 @@ import java.util.List; import java.util.Map; -/** - * Immutable data model representing a GUI item definition. - *

- * Pure data – no logic, no rendering. - * Used to describe items in configuration and GUI assembly layers. - */ public record ItemGui( @NotNull Material material, @NotNull Component name, @@ -34,10 +27,6 @@ public record ItemGui( ) { public ItemGui { - Validator.notNull(material, "material cannot be null"); - Validator.notNull(name, "name cannot be null"); - Validator.notNull(lore, "lore cannot be null"); - lore = List.copyOf(lore); if (enchantments != null) { @@ -67,21 +56,20 @@ public static final class Builder { @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder material(@NotNull Material material) { - this.material = Validator.notNull(material, "material cannot be null"); + this.material = material; return this; } @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder name(@NotNull Component name) { - this.name = Validator.notNull(name, "name cannot be null"); + this.name = name; return this; } @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder lore(@NotNull List lore) { - Validator.notNull(lore, "lore cannot be null"); this.lore = List.copyOf(lore); return this; } @@ -117,9 +105,6 @@ public Builder requiredPermission(@Nullable String permission) { @CheckReturnValue @Contract(value = "_, _ -> this", mutates = "this") public Builder addEnchantment(@NotNull Enchantment enchantment, @NotNull Integer level) { - Validator.notNull(enchantment, "enchantment cannot be null"); - Validator.notNull(level, "level cannot be null"); - Map newEnchantments = new HashMap<>(this.enchantments); newEnchantments.put(enchantment, level); this.enchantments = Map.copyOf(newEnchantments); @@ -129,8 +114,6 @@ public Builder addEnchantment(@NotNull Enchantment enchantment, @NotNull Integer @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder addFlags(@NotNull ItemFlag... toAdd) { - Validator.notNull(toAdd, "flags cannot be null"); - List newFlags = new ArrayList<>(this.flags); Collections.addAll(newFlags, toAdd); this.flags = List.copyOf(newFlags); @@ -140,15 +123,12 @@ public Builder addFlags(@NotNull ItemFlag... toAdd) { @CheckReturnValue @Contract(value = "_ -> this", mutates = "this") public Builder appendLore(@NotNull Component... lines) { - Validator.notNull(lines, "lines cannot be null"); - List newLore = new ArrayList<>(this.lore); Collections.addAll(newLore, lines); this.lore = List.copyOf(newLore); return this; } - @NotNull public ItemGui build() { return new ItemGui( this.material, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java index 5eacf92..d48aac2 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiSerializer.java @@ -24,22 +24,22 @@ public void serialize(@NotNull ItemGui item, @NotNull SerializationData data, @N data.add("name", item.name(), Component.class); data.addCollection("lore", item.lore(), Component.class); - var slot = item.slot(); + final var slot = item.slot(); if (slot != null) { data.add("slot", slot, Integer.class); } - var enchantments = item.enchantments(); + final var enchantments = item.enchantments(); if (enchantments != null && !enchantments.isEmpty()) { data.addAsMap("enchantments", item.enchantments(), Enchantment.class, Integer.class); } - var flags = item.flags(); + final var flags = item.flags(); if (flags != null && !flags.isEmpty()) { data.addCollection("flags", flags, ItemFlag.class); } - var permission = item.requiredPermission(); + final var permission = item.requiredPermission(); if (permission != null && !permission.isBlank()) { data.add("permission", permission, String.class); } @@ -47,14 +47,14 @@ public void serialize(@NotNull ItemGui item, @NotNull SerializationData data, @N @Override public ItemGui deserialize(@NotNull DeserializationData data, @NotNull GenericsDeclaration generics) { - var material = data.get("material", Material.class); - var name = data.get("name", Component.class); - var lore = data.getAsList("lore", Component.class); + final var material = data.get("material", Material.class); + final var name = data.get("name", Component.class); + final var lore = data.getAsList("lore", Component.class); - var slot = data.get("slot", Integer.class); - var enchantments = data.getAsMap("enchantments", Enchantment.class, Integer.class); - var flags = data.getAsList("flags", ItemFlag.class); - var permission = data.get("permission", String.class); + final var slot = data.get("slot", Integer.class); + final var enchantments = data.getAsMap("enchantments", Enchantment.class, Integer.class); + final var flags = data.getAsList("flags", ItemFlag.class); + final var permission = data.get("permission", String.class); return new ItemGui( material, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java index 50b9954..35b309d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemGuiTransformer.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.platform.gui.item; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.builder.item.ItemBuilder; import dev.triumphteam.gui.components.GuiAction; @@ -12,74 +11,31 @@ import java.util.function.Consumer; -/** - * Stateless utility that converts {@link ItemGui} definitions into Triumph {@link GuiItem}s. - * - *

Thread-safety: Pure transformation; prefer main thread for Bukkit objects.

- */ public final class ItemGuiTransformer { private ItemGuiTransformer() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); } - /** - * Creates a {@link GuiItem} with a no-op click handler. - * - * @param item item definition (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if {@code item} is {@code null} - */ - public static @NotNull GuiItem toGuiItem(@NotNull ItemGui item) { + public static GuiItem toGuiItem(@NotNull ItemGui item) { return toGuiItem(item, (e) -> {}, (b) -> {}); } - /** - * Creates a {@link GuiItem} wiring a {@link GuiAction} click handler. - * - * @param item item (non-null) - * @param onClick click handler (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if any argument is {@code null} - */ - public static @NotNull GuiItem toGuiItem(@NotNull ItemGui item, @NotNull GuiAction onClick) { + public static GuiItem toGuiItem(@NotNull ItemGui item, @NotNull GuiAction onClick) { return toGuiItem(item, onClick, (b) -> {}); } - /** - * Creates a {@link GuiItem} wiring a standard {@link Consumer} click handler. - * Convenience overload that adapts to Triumph's {@link GuiAction}. - * - * @param item item (non-null) - * @param onClick click handler (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if any argument is {@code null} - */ - public static @NotNull GuiItem toGuiItem(@NotNull ItemGui item, @NotNull Consumer onClick) { + public static GuiItem toGuiItem(@NotNull ItemGui item, @NotNull Consumer onClick) { return toGuiItem(item, onClick::accept, (b) -> {}); } - /** - * Creates a {@link GuiItem} with handler and optional builder editor. - * - * @param item item (non-null) - * @param onClick click handler (non-null) - * @param builderEditor item builder editor (non-null) - * @return a new {@link GuiItem} instance - * @throws IllegalArgumentException if any argument is {@code null} - */ - public static @NotNull GuiItem toGuiItem( + public static GuiItem toGuiItem( @NotNull ItemGui item, @NotNull GuiAction onClick, @NotNull Consumer> builderEditor ) { - Validator.notNull(item, "item cannot be null"); - Validator.notNull(onClick, "onClick cannot be null"); - Validator.notNull(builderEditor, "builderEditor cannot be null"); - - final Material material = item.material(); - final BaseItemBuilder builder = - material == Material.PLAYER_HEAD ? ItemBuilder.skull() : ItemBuilder.from(material); + final var material = item.material(); + final var builder = material == Material.PLAYER_HEAD ? ItemBuilder.skull() : ItemBuilder.from(material); builder.name(item.name()); builder.lore(item.lore()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java index ce50b59..a6c8051 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantPermissionResolver.java @@ -1,44 +1,11 @@ package com.github.imdmk.playtime.platform.gui.item; import com.github.imdmk.playtime.platform.gui.render.RenderContext; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.entity.HumanEntity; import org.jetbrains.annotations.NotNull; -/** - * Resolves which {@link ItemGui} variant should be displayed to a viewer - * based on their permission state. - * - *

This implementation iterates through candidate items in order and returns - * the first one that either: - *

    - *
  • Has no required permission ({@code requiredPermission() == null}), or
  • - *
  • Has a permission that the viewer possesses, as determined by - * {@link RenderContext#permissionEvaluator()}.
  • - *
- * If no candidate matches, a predefined fallback item is returned.

- * - *

Usage: Typically used by GUI renderers to determine which - * item variant to display for users with different roles or permission levels.

- * - *

Thread-safety: This resolver is stateless and thread-safe.

- * - * @see ItemGui - * @see ItemVariantResolver - * @see RenderContext - */ public final class ItemVariantPermissionResolver implements ItemVariantResolver { - /** - * Resolves the first matching {@link ItemGui} variant visible to the given viewer. - * - * @param viewer the player or entity viewing the GUI (non-null) - * @param context current rendering context, providing permission evaluation (non-null) - * @param candidates ordered list of possible item variants (non-null) - * @param fallback default item to return if no candidate matches (non-null) - * @return the first permitted item variant, or {@code fallback} if none are allowed - * @throws NullPointerException if any argument is null - */ @Override public ItemGui resolve( @NotNull HumanEntity viewer, @@ -46,11 +13,6 @@ public ItemGui resolve( @NotNull Iterable candidates, @NotNull ItemGui fallback ) { - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(context, "context cannot be null"); - Validator.notNull(candidates, "candidates cannot be null"); - Validator.notNull(fallback, "fallback cannot be null"); - for (final ItemGui item : candidates) { if (item == null) { continue; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java index 163011d..328a13c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/item/ItemVariantResolver.java @@ -4,35 +4,8 @@ import org.bukkit.entity.HumanEntity; import org.jetbrains.annotations.NotNull; -/** - * Defines a strategy for selecting which {@link ItemGui} variant - * should be displayed to a specific viewer during GUI rendering. - * - *

Implementations of this interface encapsulate different - * resolution logics — e.g., by permission, by user state, - * by contextual conditions, or by custom business rules.

- * - *

The resolver is typically used within GUI frameworks to decide - * which visual representation of an item (variant) to render for a given player.

- * - *

Thread-safety: Implementations should be stateless and thread-safe.

- * - * @see ItemGui - * @see RenderContext - * @see ItemVariantPermissionResolver - */ public interface ItemVariantResolver { - /** - * Resolves the most appropriate {@link ItemGui} variant to display. - * - * @param viewer the player or entity viewing the GUI (non-null) - * @param context the current rendering context providing permission checks, locale, etc. (non-null) - * @param candidates iterable collection of possible item variants, evaluated in order (non-null) - * @param fallback default item variant to use if none match (non-null) - * @return the resolved item variant, never {@code null} (at least {@code fallback}) - * @throws NullPointerException if any parameter is null - */ ItemGui resolve(@NotNull HumanEntity viewer, @NotNull RenderContext context, @NotNull Iterable candidates, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java index 2f6fcb8..6015bfa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/GuiRenderer.java @@ -9,10 +9,6 @@ import java.util.function.Consumer; -/** - * Renders and places {@link ItemGui} into {@link BaseGui} instances. - * Invoke only on the Bukkit main thread. - */ public interface GuiRenderer { @Contract(mutates = "param1") @@ -26,10 +22,6 @@ default void setItem(@NotNull BaseGui gui, setItem(gui, slot, item, context, options, onClick, b -> {}); } - /** - * Sets the item in a specific slot (overwrites existing content). - * Supports per-slot customization via {@code builderEditor}. - */ @Contract(mutates = "param1") void setItem(@NotNull BaseGui gui, int slot, @@ -81,10 +73,6 @@ default void addItem(@NotNull BaseGui gui, addItem(gui, item, context, options, onClick, b -> {}); } - /** - * Adds the item to the next free slot. - * Supports per-slot customization via {@code builderEditor}. - */ @Contract(mutates = "param1") void addItem(@NotNull BaseGui gui, @NotNull ItemGui item, diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java index ee7cb96..4ce4519 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/NoPermissionPolicy.java @@ -1,19 +1,6 @@ package com.github.imdmk.playtime.platform.gui.render; -/** - * Defines how a GUI element should behave when the viewer lacks - * the required permission to interact with or view the item. - */ public enum NoPermissionPolicy { - - /** - * The item is completely hidden and not placed in the GUI. - */ HIDE, - - /** - * The item is still visible but interaction is disabled. - * Clicking it will trigger the "onDenied" consumer if provided. - */ DISABLE } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java index 2c27ab3..f7e5a3c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/PermissionEvaluator.java @@ -3,23 +3,8 @@ import org.bukkit.entity.HumanEntity; import org.jetbrains.annotations.NotNull; -/** - * Strategy interface for checking player permissions. - *

- * This abstraction allows GUIs and renderers to remain independent - * from the underlying permission system (e.g. Bukkit, Vault, LuckPerms). - *

- * Implementations should be thread-safe if evaluated asynchronously. - */ @FunctionalInterface public interface PermissionEvaluator { - /** - * Checks whether the given human entity possesses the specified permission. - * - * @param entity the entity being checked (non-null) - * @param permission the permission node (non-null) - * @return {@code true} if the player has the permission; {@code false} otherwise - */ boolean has(@NotNull HumanEntity entity, @NotNull String permission); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java index 1369aa4..f34a6ef 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderContext.java @@ -4,29 +4,12 @@ import org.bukkit.permissions.Permissible; import org.jetbrains.annotations.NotNull; -/** - * Immutable context used during GUI item rendering. - *

- * Encapsulates the viewer and the permission evaluation strategy, - * ensuring renderers remain stateless and easily testable. - *

- * Thread-safety: This record is immutable and thread-safe - * as long as the underlying {@link PermissionEvaluator} implementation is thread-safe. - * - * @param viewer the player for whom the GUI is being rendered - * @param permissionEvaluator the strategy used to check permissions - */ public record RenderContext( @NotNull Player viewer, @NotNull PermissionEvaluator permissionEvaluator ) { - /** - * Creates a default context that checks if player has permission. - * - * @return the default {@link RenderContext} instance - */ - public static @NotNull RenderContext defaultContext(@NotNull Player viewer) { + public static RenderContext defaultContext(@NotNull Player viewer) { return new RenderContext(viewer, Permissible::hasPermission); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java index 03a8d30..ccfaffa 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/RenderOptions.java @@ -5,36 +5,16 @@ import java.util.function.Consumer; -/** - * Rendering options that define how permission handling - * and denied interactions are processed during GUI rendering. - * - * @param policy how to handle items when the viewer lacks permission - * @param onDenied consumer called when a denied item is clicked - *

- * Thread-safety: This record is immutable and thread-safe, - * provided that the supplied {@link Consumer} implementation is thread-safe. - */ public record RenderOptions( @NotNull NoPermissionPolicy policy, @NotNull Consumer onDenied ) { - /** - * Creates a default option that disables unauthorized items silently. - * - * @return the default {@link RenderOptions} instance - */ - public static @NotNull RenderOptions defaultDenySilently() { + public static RenderOptions defaultDenySilently() { return new RenderOptions(NoPermissionPolicy.DISABLE, e -> {}); } - /** - * Creates a default option that hides unauthorized items completely. - * - * @return the default {@link RenderOptions} instance - */ - public static @NotNull RenderOptions defaultHide() { + public static RenderOptions defaultHide() { return new RenderOptions(NoPermissionPolicy.HIDE, e -> {}); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java index 5fbbc70..3ce9477 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/render/TriumphGuiRenderer.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.platform.gui.item.ItemGui; import com.github.imdmk.playtime.platform.gui.item.ItemGuiTransformer; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.builder.item.BaseItemBuilder; import dev.triumphteam.gui.components.GuiAction; import dev.triumphteam.gui.guis.BaseGui; @@ -12,117 +11,64 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; -/** - * Default {@link GuiRenderer} implementation using the Triumph GUI API. - *

- * Responsible for rendering {@link ItemGui} objects into a {@link BaseGui}, - * applying permission policies and wiring click handlers. - * - *

Behavior: - *

    - *
  • If {@link NoPermissionPolicy#HIDE} → item is not rendered (returns {@code null}).
  • - *
  • If {@link NoPermissionPolicy#DISABLE} → click is blocked (cancelled) silently.
  • - *
  • Otherwise → executes provided click handler.
  • - *
- */ public final class TriumphGuiRenderer implements GuiRenderer { - /** - * Creates a new renderer instance. - *

Renderer is stateless and may be safely reused.

- * - * @return new {@link TriumphGuiRenderer} instance - */ public static TriumphGuiRenderer newRenderer() { return new TriumphGuiRenderer(); } - /** - * Places a rendered {@link ItemGui} into the specified GUI slot. - *

If the item should be hidden (policy {@code HIDE}), it will not be placed.

- * - * @param gui target GUI - * @param slot target slot index - * @param item GUI item definition - * @param context render context - * @param options render options - * @param onClick click action to execute if allowed - * @param builderEditor optional builder customization - */ @Override @Contract(mutates = "param1") public void setItem( - @NotNull final BaseGui gui, - final int slot, - @NotNull final ItemGui item, - @NotNull final RenderContext context, - @NotNull final RenderOptions options, - @NotNull final Consumer onClick, - @NotNull final Consumer> builderEditor + @NotNull BaseGui gui, + int slot, + @NotNull ItemGui item, + @NotNull RenderContext context, + @NotNull RenderOptions options, + @NotNull Consumer onClick, + @NotNull Consumer> builderEditor ) { - validateArgs(gui, item, context, options, onClick, builderEditor); - - final GuiItem guiItem = buildGuiItem(item, context, options, onClick, builderEditor); - if (guiItem != null) { - gui.setItem(slot, guiItem); + final GuiItem builtItem = buildGuiItem(item, context, options, onClick, builderEditor); + if (builtItem != null) { + gui.setItem(slot, builtItem); } } - /** - * Adds a rendered {@link ItemGui} to the GUI at the next available position. - *

If the item should be hidden (policy {@code HIDE}), it will not be added.

- * - * @param gui target GUI - * @param item GUI item definition - * @param context render context - * @param options render options - * @param onClick click action to execute if allowed - * @param builderEditor optional builder customization - */ @Override @Contract(mutates = "param1") public void addItem( - @NotNull final BaseGui gui, - @NotNull final ItemGui item, - @NotNull final RenderContext context, - @NotNull final RenderOptions options, - @NotNull final Consumer onClick, - @NotNull final Consumer> builderEditor + @NotNull BaseGui gui, + @NotNull ItemGui item, + @NotNull RenderContext context, + @NotNull RenderOptions options, + @NotNull Consumer onClick, + @NotNull Consumer> builderEditor ) { - validateArgs(gui, item, context, options, onClick, builderEditor); - - final GuiItem guiItem = buildGuiItem(item, context, options, onClick, builderEditor); - if (guiItem != null) { - gui.addItem(guiItem); + final GuiItem builtItem = buildGuiItem(item, context, options, onClick, builderEditor); + if (builtItem != null) { + gui.addItem(builtItem); } } - /** - * Builds a {@link GuiItem} based on the given item definition and context. - *

- * Permission logic: - *

    - *
  • If the viewer lacks permission and policy is {@code HIDE}, returns {@code null}.
  • - *
  • If the viewer lacks permission and policy is {@code DISABLE}, click is blocked silently.
  • - *
- * - * @return a built {@link GuiItem}, or {@code null} if hidden - */ - private @Nullable GuiItem buildGuiItem( - @NotNull final ItemGui item, - @NotNull final RenderContext context, - @NotNull final RenderOptions options, - @NotNull final Consumer onClick, - @NotNull final Consumer> builderEditor + private static GuiItem buildGuiItem( + ItemGui item, + RenderContext context, + RenderOptions options, + Consumer onClick, + Consumer> builderEditor ) { final String requiredPerm = item.requiredPermission(); + final boolean allowedForViewerNow = hasPermission(requiredPerm, context, context.viewer()); + if (!allowedForViewerNow && options.policy() == NoPermissionPolicy.HIDE) { + return null; + } + final GuiAction clickHandler = event -> { - if (!has(requiredPerm, context, event.getWhoClicked())) { + if (!hasPermission(requiredPerm, context, event.getWhoClicked())) { event.setCancelled(true); event.setResult(Event.Result.DENY); options.onDenied().accept(event); @@ -132,46 +78,10 @@ public void addItem( onClick.accept(event); }; - final boolean allowedForViewerNow = has(requiredPerm, context, context.viewer()); - if (!allowedForViewerNow && options.policy() == NoPermissionPolicy.HIDE) { - return null; - } - return ItemGuiTransformer.toGuiItem(item, clickHandler, builderEditor); } - /** - * Checks if the given entity has the required permission. - * - * @param permission permission string or {@code null} - * @param context render context - * @param entity entity to check - * @return {@code true} if allowed, otherwise {@code false} - */ - private static boolean has(@Nullable final String permission, - @NotNull final RenderContext context, - @NotNull final HumanEntity entity) { + private static boolean hasPermission(String permission, RenderContext context, HumanEntity entity) { return permission == null || context.permissionEvaluator().has(entity, permission); } - - /** - * Ensures all arguments are non-null. - * - * @throws NullPointerException if any argument is {@code null} - */ - private static void validateArgs( - final BaseGui gui, - final ItemGui item, - final RenderContext context, - final RenderOptions options, - final Consumer onClick, - final Consumer> builderEditor - ) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(item, "item cannot be null"); - Validator.notNull(context, "context cannot be null"); - Validator.notNull(options, "options cannot be null"); - Validator.notNull(onClick, "onClick cannot be null"); - Validator.notNull(builderEditor, "builderEditor cannot be null"); - } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java index 17d6264..2a6599b 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/AbstractGui.java @@ -1,12 +1,9 @@ package com.github.imdmk.playtime.platform.gui.view; -import com.github.imdmk.playtime.platform.gui.config.GuiConfig; import com.github.imdmk.playtime.platform.gui.config.NavigationBarConfig; import com.github.imdmk.playtime.platform.gui.render.GuiRenderer; -import com.github.imdmk.playtime.platform.gui.render.RenderContext; import com.github.imdmk.playtime.platform.gui.render.RenderOptions; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -14,18 +11,6 @@ import java.util.function.Consumer; -/** - * Thin base for GUI implementations. - *

- * Responsibilities: - *

    - *
  • Provide navigation helpers (Next/Previous/Exit),
  • - *
  • Hold shared collaborators: {@link GuiConfig}, {@link TaskScheduler}, - * {@link GuiRenderer}, {@link RenderContext}, {@link RenderOptions}.
  • - *
- * - * Threading: All methods are expected to be called on the Bukkit main thread. - */ public abstract class AbstractGui { protected final NavigationBarConfig config; @@ -35,22 +20,16 @@ public abstract class AbstractGui { private final NavigationBar navigationBar; - /** - * @param config GUI config (visual defaults, nav items, etc.) - * @param taskScheduler scheduler for short, sync GUI updates - * @param renderer renderer that places items and enforces permission policy - * @param renderOptions render options (no-permission policy, onDenied) - */ protected AbstractGui( @NotNull NavigationBarConfig config, @NotNull TaskScheduler taskScheduler, @NotNull GuiRenderer renderer, @NotNull RenderOptions renderOptions ) { - this.config = Validator.notNull(config, "config cannot be null"); - this.scheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); - this.renderer = Validator.notNull(renderer, "renderer cannot be null"); - this.renderOptions = Validator.notNull(renderOptions, "renderOptions cannot be null"); + this.config = config; + this.scheduler = taskScheduler; + this.renderer = renderer; + this.renderOptions = renderOptions; this.navigationBar = new NavigationBar( this.config, @@ -60,44 +39,15 @@ protected AbstractGui( ); } - /** - * Places the "Next" control if the GUI is paginated. - * - * @param gui target GUI - * @param viewer target viewer - */ protected void placeNext(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); navigationBar.setNext(gui, viewer); } - /** - * Places the "Previous" control if the GUI is paginated. - * - * @param gui target GUI - * @param viewer target viewer - */ protected void placePrevious(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); navigationBar.setPrevious(gui, viewer); } - /** - * Places the "Exit" control. - * - * @param gui target GUI - * @param viewer target viewer - * @param exit action to run on click - */ - protected void placeExit( - @NotNull BaseGui gui, - @NotNull Player viewer, - @NotNull Consumer exit) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(exit, "exit cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); + protected void placeExit(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull Consumer exit) { navigationBar.setExit(gui, viewer, exit); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java index 5558fb9..5b604fb 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GridSlots.java @@ -1,12 +1,5 @@ package com.github.imdmk.playtime.platform.gui.view; -/** - * Provides predefined slot positions for common GUI navigation controls - * (Next, Previous, Exit) depending on the GUI height (3–6 rows). - *

- * Each constant represents the index of an inventory slot where - * navigation buttons should be placed. - */ final class GridSlots { private static final int ROW_3_NEXT = 25; @@ -29,12 +22,6 @@ private GridSlots() { throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); } - /** - * Returns the inventory slot index for the "Next Page" button. - * - * @param rows number of GUI rows (3–6) - * @return slot index for the next-page control - */ static int next(int rows) { return switch (rows) { case 3 -> ROW_3_NEXT; @@ -45,12 +32,6 @@ static int next(int rows) { }; } - /** - * Returns the inventory slot index for the "Previous Page" button. - * - * @param rows number of GUI rows (3–6) - * @return slot index for the previous-page control - */ static int previous(int rows) { return switch (rows) { case 3 -> ROW_3_PREVIOUS; @@ -61,12 +42,6 @@ static int previous(int rows) { }; } - /** - * Returns the inventory slot index for the "Exit" button. - * - * @param rows number of GUI rows (3–6) - * @return slot index for the exit control - */ static int exit(int rows) { return switch (rows) { case 3 -> ROW_3_EXIT; diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java index 4670266..040e6f3 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/GuiOpener.java @@ -1,25 +1,15 @@ package com.github.imdmk.playtime.platform.gui.view; +import com.github.imdmk.playtime.injector.annotations.Service; import com.github.imdmk.playtime.platform.gui.GuiRegistry; import com.github.imdmk.playtime.platform.gui.IdentifiableGui; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -/** - * Opens GUIs by id or class on the Bukkit main thread. - * - *

Responsibilities:

- *
    - *
  • Lookup GUIs via {@link GuiRegistry},
  • - *
  • Invoke {@code BaseGui#open(Player)} on the main thread using {@link TaskScheduler}.
  • - *
- * - * Thread-safety: Safe to call from any thread. Actual GUI operations are marshalled to the main thread. - */ +@Service public final class GuiOpener { private final GuiRegistry registry; @@ -27,110 +17,85 @@ public final class GuiOpener { @Inject public GuiOpener(@NotNull GuiRegistry registry, @NotNull TaskScheduler taskScheduler) { - this.registry = Validator.notNull(registry, "registry cannot be null"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); + this.registry = registry; + this.taskScheduler = taskScheduler; } - /** - * Opens a non-parameterized GUI by its concrete class. - * - * @throws IllegalArgumentException if GUI is not registered or not a {@link SimpleGui} - */ public void open( @NotNull Class type, - @NotNull Player viewer) { - Validator.notNull(type, "type cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - - IdentifiableGui gui = require(type); + @NotNull Player viewer + ) { + final IdentifiableGui gui = require(type); if (!(gui instanceof SimpleGui simpleGui)) { throw wrongType(type.getName(), gui, "SimpleGui"); } - BaseGui baseGui = simpleGui.createGui(); + final BaseGui baseGui = simpleGui.createGui(); simpleGui.prepareItems(baseGui, viewer); taskScheduler.runSync(() -> baseGui.open(viewer)); } - /** - * Opens a parameterized GUI by its concrete class. - * - * @throws IllegalArgumentException if GUI is not registered or not a {@link ParameterizedGui} - */ @SuppressWarnings("unchecked") - public void open(@NotNull Class> type, - @NotNull Player viewer, - @NotNull T parameter) { - Validator.notNull(type, "type cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(parameter, "parameter cannot be null"); - - IdentifiableGui gui = require(type); + public void open( + @NotNull Class> type, + @NotNull Player viewer, + @NotNull T parameter + ) { + final IdentifiableGui gui = require(type); if (!(gui instanceof ParameterizedGui paramGui)) { throw wrongType(type.getName(), gui, "ParameterizedGui"); } - ParameterizedGui typed = (ParameterizedGui) paramGui; - BaseGui baseGui = typed.createGui(viewer, parameter); + final ParameterizedGui typed = (ParameterizedGui) paramGui; + final BaseGui baseGui = typed.createGui(viewer, parameter); + typed.prepareItems(baseGui, viewer, parameter); taskScheduler.runSync(() -> baseGui.open(viewer)); } - /** - * Opens a non-parameterized GUI by id for the given player. - * - * @throws IllegalArgumentException if id is unknown or GUI is not a {@link SimpleGui} - */ + public void open( @NotNull String id, - @NotNull Player viewer) { - Validator.notNull(id, "id cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - - IdentifiableGui gui = require(id); + @NotNull Player viewer + ) { + final IdentifiableGui gui = require(id); if (!(gui instanceof SimpleGui simpleGui)) { throw wrongType(id, gui, "SimpleGui"); } - BaseGui baseGui = simpleGui.createGui(); + final BaseGui baseGui = simpleGui.createGui(); + simpleGui.prepareItems(baseGui, viewer); taskScheduler.runSync(() -> baseGui.open(viewer)); } - /** - * Opens a parameterized GUI by id for the given player. - * - * @throws IllegalArgumentException if id is unknown or GUI is not a {@link ParameterizedGui} - */ @SuppressWarnings("unchecked") public void open( @NotNull String id, @NotNull Player viewer, - @NotNull T parameter) { - Validator.notNull(id, "id cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(parameter, "parameter cannot be null"); - - IdentifiableGui gui = require(id); + @NotNull T parameter + ) { + final IdentifiableGui gui = require(id); if (!(gui instanceof ParameterizedGui paramGui)) { throw wrongType(id, gui, "ParameterizedGui"); } - ParameterizedGui typed = (ParameterizedGui) paramGui; - BaseGui baseGui = typed.createGui(viewer, parameter); + final ParameterizedGui typed = (ParameterizedGui) paramGui; + final BaseGui baseGui = typed.createGui(viewer, parameter); + typed.prepareItems(baseGui, viewer, parameter); taskScheduler.runSync(() -> baseGui.open(viewer)); } - private @NotNull IdentifiableGui require(@NotNull String id) { - IdentifiableGui gui = registry.getById(id); + private IdentifiableGui require(String id) { + final IdentifiableGui gui = registry.getById(id); if (gui == null) { throw new IllegalArgumentException("No GUI registered under id '" + id + "'"); } return gui; } - private @NotNull IdentifiableGui require(@NotNull Class type) { - IdentifiableGui gui = registry.getByClass(type); + private IdentifiableGui require(Class type) { + final IdentifiableGui gui = registry.getByClass(type); if (gui == null) { throw new IllegalArgumentException("No GUI registered for class '" + type.getName() + "'"); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java index 843cd10..9d330b4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/NavigationBar.java @@ -5,7 +5,6 @@ import com.github.imdmk.playtime.platform.gui.render.RenderContext; import com.github.imdmk.playtime.platform.gui.render.RenderOptions; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.triumphteam.gui.guis.BaseGui; import dev.triumphteam.gui.guis.PaginatedGui; import org.bukkit.entity.Player; @@ -15,116 +14,75 @@ import java.time.Duration; import java.util.function.Consumer; -/** - * Places navigation controls (Next, Previous, Exit) into Triumph GUIs. - *

- * Responsibilities: - *

    - *
  • Compute target slots via {@link GridSlots},
  • - *
  • Delegate permission/policy enforcement to {@link GuiRenderer},
  • - *
  • Provide short-lived feedback (e.g., "no next/previous") and restore original items.
  • - *
- * - * Threading: All methods are expected to be called on the Bukkit main thread. - * The class is stateless w.r.t. rendering; it holds only injected collaborators. - */ final class NavigationBar { - private static final Duration RESTORE_DELAY = Duration.ofSeconds(1); + private static final Duration DELAY = Duration.ofSeconds(1); private final NavigationBarConfig config; private final TaskScheduler scheduler; private final GuiRenderer renderer; private final RenderOptions renderOptions; - /** - * @param config navigation bar config (items, etc.) - * @param scheduler scheduler for short delayed updates - * @param renderer GUI renderer enforcing permission policy - * @param renderOptions render options (no-permission policy, onDenied) - */ NavigationBar( @NotNull NavigationBarConfig config, @NotNull TaskScheduler scheduler, @NotNull GuiRenderer renderer, @NotNull RenderOptions renderOptions ) { - this.config = Validator.notNull(config, "config cannot be null"); - this.renderer = Validator.notNull(renderer, "renderer cannot be null"); - this.scheduler = Validator.notNull(scheduler, "scheduler cannot be null"); - this.renderOptions = Validator.notNull(renderOptions, "renderOptions cannot be null"); + this.config = config; + this.renderer = renderer; + this.scheduler = scheduler; + this.renderOptions = renderOptions; } - /** - * Places the "Next page" button if {@code gui} is paginated. - */ void setNext(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - if (!(gui instanceof PaginatedGui paginated)) { return; } - final var context = RenderContext.defaultContext(viewer); - final var slot = GridSlots.next(gui.getRows()); + final RenderContext context = RenderContext.defaultContext(viewer); + final int slot = GridSlots.next(gui.getRows()); final Consumer onClick = event -> { if (!paginated.next()) { renderer.setItem(gui, event.getSlot(), config.noNextItem, context, renderOptions, this::noop); - restoreLater(() -> setNext(gui, viewer)); + runLater(() -> setNext(gui, viewer)); } }; renderer.setItem(gui, slot, config.nextItem, context, renderOptions, onClick); } - /** - * Places the "Previous page" button if {@code gui} is paginated. - */ void setPrevious(@NotNull BaseGui gui, @NotNull Player viewer) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - if (!(gui instanceof PaginatedGui paginated)) { return; } - final var context = RenderContext.defaultContext(viewer); - final var slot = GridSlots.previous(gui.getRows()); + final RenderContext context = RenderContext.defaultContext(viewer); + final int slot = GridSlots.previous(gui.getRows()); final Consumer onClick = event -> { if (!paginated.previous()) { renderer.setItem(gui, event.getSlot(), config.noPreviousItem, context, renderOptions, this::noop); - restoreLater(() -> setPrevious(gui, viewer)); + runLater(() -> setPrevious(gui, viewer)); } }; renderer.setItem(gui, slot, config.previousItem, context, renderOptions, onClick); } - /** - * Places the "Exit" button which triggers the provided action. - */ void setExit( @NotNull BaseGui gui, @NotNull Player viewer, - @NotNull Consumer exit) { - Validator.notNull(gui, "gui cannot be null"); - Validator.notNull(viewer, "viewer cannot be null"); - Validator.notNull(exit, "exit cannot be null"); - - final var context = RenderContext.defaultContext(viewer); - final var slot = GridSlots.exit(gui.getRows()); + @NotNull Consumer exit + ) { + final RenderContext context = RenderContext.defaultContext(viewer); + final int slot = GridSlots.exit(gui.getRows()); renderer.setItem(gui, slot, config.exitItem, context, renderOptions, exit); } - /** - * Schedules a short delayed restore action (e.g., after showing "no next/previous"). - */ - private void restoreLater(@NotNull Runnable restoreAction) { - Validator.notNull(restoreAction, "restoreAction cannot be null"); - scheduler.runLaterSync(restoreAction, RESTORE_DELAY); + private void runLater(Runnable runnable) { + scheduler.runLaterSync(runnable, DELAY); } private void noop(@NotNull InventoryClickEvent e) { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java index 8a8c924..bda1ecd 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/ParameterizedGui.java @@ -5,29 +5,10 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -/** - * Represents a generic GUI that requires a parameter to be initialized and populated. - * Acts as a template for all GUIs that are parameterized, defining a default open lifecycle. - * - * @param the type of parameter used for populating the GUI - */ public interface ParameterizedGui extends IdentifiableGui { - /** - * Creates a new instance of the GUI. - * - * @param viewer the player viewing the GUI - * @param parameter the parameter used to customize the GUI - * @return the initialized {@link BaseGui} instance - */ BaseGui createGui(@NotNull Player viewer, @NotNull T parameter); - /** - * Prepares and populates the GUI with core content based on the parameter. - * - * @param gui the GUI to populate - * @param viewer the player viewing the GUI - * @param parameter the context parameter - */ void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer, @NotNull T parameter); + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java index fa235d1..0144eaf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/gui/view/SimpleGui.java @@ -5,24 +5,9 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -/** - * Represents a simple GUI that does not require a parameter to be created or populated. - * Defines a standard lifecycle for opening such GUIs. - */ public interface SimpleGui extends IdentifiableGui { - /** - * Creates a new instance of the GUI. - * - * @return the initialized {@link BaseGui} instance - */ BaseGui createGui(); - /** - * Prepares and populates the GUI with its core content. - * - * @param gui the GUI to populate - * @param viewer the player viewing the GUI - */ void prepareItems(@NotNull BaseGui gui, @NotNull Player viewer); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java index f454964..4c13fb1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/litecommands/InvalidUsageHandlerImpl.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.platform.litecommands; import com.github.imdmk.playtime.message.MessageService; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.rollczi.litecommands.handler.result.ResultHandlerChain; import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invalidusage.InvalidUsageHandler; @@ -15,7 +14,7 @@ public final class InvalidUsageHandlerImpl implements InvalidUsageHandler invocation, Notice notice, ResultHandlerChain chain) { - messageService.send(invocation.sender(), n -> notice); + messageService.create() + .viewer(invocation.sender()) + .notice(n -> notice) + .send(); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java index 5623ca7..f15ea04 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/logger/BukkitPluginLogger.java @@ -1,56 +1,21 @@ package com.github.imdmk.playtime.platform.logger; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; -/** - * Bukkit-specific implementation of {@link PluginLogger} delegating to a standard - * {@link java.util.logging.Logger} obtained from a Bukkit {@link Plugin}. - * - *

This class provides formatted and structured logging methods for common log levels - * (INFO, WARNING, DEBUG, SEVERE) with support for formatted messages and throwable logging.

- * - *

Design notes:

- *
    - *
  • Acts as a lightweight adapter to bridge the internal plugin logging interface with Bukkit’s logger.
  • - *
  • Formatting uses {@link String#format(Locale, String, Object...)} with {@link Locale#ROOT} to ensure locale safety.
  • - *
  • Supports overloaded methods for flexible log message creation, including formatted and exception-based variants.
  • - *
- * - *

Thread-safety: Delegates to the underlying {@link Logger}, which is thread-safe for concurrent use.

- * - * @see PluginLogger - * @see Plugin#getLogger() - * @see Logger - */ public final class BukkitPluginLogger implements PluginLogger { /** Backing {@link java.util.logging.Logger} provided by Bukkit. */ private final Logger logger; - /** - * Creates a new {@code BukkitPluginLogger} wrapping an existing {@link Logger}. - * - * @param logger non-null backing logger instance - * @throws NullPointerException if {@code logger} is null - */ - public BukkitPluginLogger(@NotNull Logger logger) { - this.logger = Validator.notNull(logger, "logger cannot be null"); - } - - /** - * Creates a new {@code BukkitPluginLogger} bound to the given Bukkit {@link Plugin}. - * - * @param plugin non-null Bukkit plugin instance - * @throws NullPointerException if {@code plugin} is null - */ + @Inject public BukkitPluginLogger(@NotNull Plugin plugin) { - this(plugin.getLogger()); + this.logger = plugin.getLogger(); } @Override @@ -108,17 +73,7 @@ public void error(@NotNull Throwable throwable, @NotNull String message, @NotNul logger.log(Level.SEVERE, format(message, args), throwable); } - /** - * Formats a message using {@link String#format(Locale, String, Object...)} with {@link Locale#ROOT}. - * - * @param message format string (non-null) - * @param args format arguments (non-null) - * @return formatted message - * @throws NullPointerException if {@code message} or {@code args} is null - */ - private String format(@NotNull String message, @NotNull Object... args) { - Validator.notNull(message, "message cannot be null"); - Validator.notNull(args, "args cannot be null"); + private String format(String message, Object... args) { return String.format(Locale.ROOT, message, args); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java index b86e551..3f4afda 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/PluginPlaceholder.java @@ -5,30 +5,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** - * Plugin-level abstraction for a single placeholder. - *

- * This interface is framework-agnostic and does not depend on PlaceholderAPI. - * Implementations can be adapted to any placeholder platform. - */ public interface PluginPlaceholder { - /** - * Unique identifier of the placeholder set. - * Example: "playtime" - */ @NotNull String identifier(); - /** - * Called for online players, if supported by the underlying platform. - */ default @Nullable String onRequest(@NotNull Player player, @NotNull String params) { return null; } - /** - * Called for offline players, if supported by the underlying platform. - */ default @Nullable String onRequest(@NotNull OfflinePlayer player, @NotNull String params) { return null; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java index e1fb145..42e8421 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapter.java @@ -3,31 +3,14 @@ import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; import org.jetbrains.annotations.NotNull; -/** - * Strategy for registering {@link PluginPlaceholder} instances - * against a concrete placeholder platform (e.g. PlaceholderAPI), - * or acting as a no-op implementation when the platform is not present. - */ public interface PlaceholderAdapter { - /** - * @return {@code true} if the underlying placeholder platform is available. - */ boolean isAvailable(); - /** - * Registers the given placeholder if the platform is available. - */ void register(@NotNull PluginPlaceholder placeholder); - /** - * Unregisters the given placeholder, if it was registered. - */ void unregister(@NotNull PluginPlaceholder placeholder); - /** - * Unregisters all placeholders managed by this registrar. - */ void unregisterAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java index baa0e53..35c230f 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterFactory.java @@ -1,59 +1,23 @@ package com.github.imdmk.playtime.platform.placeholder.adapter; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.Server; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; -/** - * Factory responsible for creating the appropriate {@link PlaceholderAdapter} - * implementation based on runtime plugin availability. - * - *

This class detects whether PlaceholderAPI is installed and enabled on the server. - * Depending on its presence, it returns either:

- * - *
    - *
  • {@link PlaceholderAPIAdapter} – full integration with PlaceholderAPI;
  • - *
  • {@link NoopPlaceholderAdapter} – a no-operation fallback that safely disables - * placeholder support without causing errors.
  • - *
- * - *

This allows the plugin to offer optional PlaceholderAPI integration without requiring it - * as a hard dependency, while keeping all placeholder logic abstracted behind - * the {@link PlaceholderAdapter} interface.

- * - *

Thread-safety: The factory contains no mutable state and is fully thread-safe.

- */ public final class PlaceholderAdapterFactory { private static final String PLACEHOLDER_API_NAME = "PlaceholderAPI"; - /** - * Creates a {@link PlaceholderAdapter} appropriate for the current server environment. - * - *

If PlaceholderAPI is detected and enabled, a {@link PlaceholderAPIAdapter} is returned. - * Otherwise, a {@link NoopPlaceholderAdapter} is provided, which safely performs no operations.

- * - * @param plugin the owning plugin instance; must not be null - * @param server the Bukkit server instance; must not be null - * @param logger the plugin logger for diagnostic output; must not be null - * @return a fully initialized placeholder adapter suitable for the environment - * @throws NullPointerException if any argument is null - */ public static PlaceholderAdapter createFor( @NotNull Plugin plugin, @NotNull Server server, @NotNull PluginLogger logger ) { - Validator.notNull(plugin, "plugin cannot be null"); - Validator.notNull(server, "server cannot be null"); - Validator.notNull(logger, "logger cannot be null"); - - boolean isEnabled = server.getPluginManager().isPluginEnabled(PLACEHOLDER_API_NAME); + final boolean isEnabled = server.getPluginManager().isPluginEnabled(PLACEHOLDER_API_NAME); if (isEnabled) { - logger.info("PlaceholderAPI detected — using PlaceholderApiAdapter."); - return new PlaceholderAPIAdapter(plugin, logger); + logger.info("PlaceholderAPI detected — using PlaceholderAdapterImpl."); + return new PlaceholderAdapterImpl(plugin, logger); } logger.info("PlaceholderAPI not found — using NoopPlaceholderAdapter."); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAPIAdapter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java similarity index 85% rename from playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAPIAdapter.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java index 5be739c..75c8bfe 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAPIAdapter.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/placeholder/adapter/PlaceholderAdapterImpl.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.placeholder.PluginPlaceholder; -import com.github.imdmk.playtime.shared.validate.Validator; import me.clip.placeholderapi.expansion.PlaceholderExpansion; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -13,16 +12,16 @@ import java.util.HashMap; import java.util.Map; -final class PlaceholderAPIAdapter implements PlaceholderAdapter { +final class PlaceholderAdapterImpl implements PlaceholderAdapter { private final Plugin plugin; private final PluginLogger logger; private final Map expansions = new HashMap<>(); - PlaceholderAPIAdapter(@NotNull Plugin plugin, @NotNull PluginLogger logger) { - this.plugin = Validator.notNull(plugin, "plugin cannot be null"); - this.logger = Validator.notNull(logger, "logger cannot be null"); + PlaceholderAdapterImpl(@NotNull Plugin plugin, @NotNull PluginLogger logger) { + this.plugin = plugin; + this.logger = logger; } @Override @@ -32,8 +31,6 @@ public boolean isAvailable() { @Override public void register(@NotNull PluginPlaceholder placeholder) { - Validator.notNull(placeholder, "placeholder cannot be null"); - if (expansions.containsKey(placeholder)) { logger.warn("Placeholder with name %s is already registered!", placeholder.identifier()); return; @@ -47,8 +44,6 @@ public void register(@NotNull PluginPlaceholder placeholder) { @Override public void unregister(@NotNull PluginPlaceholder placeholder) { - Validator.notNull(placeholder, "placeholder cannot be null"); - final PlaceholderExpansion expansion = expansions.remove(placeholder); if (expansion != null) { expansion.unregister(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java index 2c07b97..f0a2c1d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/BukkitTaskScheduler.java @@ -1,147 +1,84 @@ package com.github.imdmk.playtime.platform.scheduler; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; +import org.panda_lang.utilities.inject.annotations.Inject; import java.time.Duration; -/** - * {@link TaskScheduler} implementation backed by the Bukkit {@link BukkitScheduler}. - * - *

Provides a clean, Duration-based API for scheduling synchronous and asynchronous - * tasks, including delayed and repeating executions.

- * - *

All time values are expressed using {@link Duration} and internally converted - * to Minecraft ticks (1 tick = 50 ms).

- * - *

Thread-safety: This class is thread-safe. It holds only immutable - * references to {@link Plugin} and {@link BukkitScheduler}.

- */ +@Service(priority = Priority.LOWEST) public final class BukkitTaskScheduler implements TaskScheduler { - /** Number of milliseconds per Minecraft tick. */ private static final long MILLIS_PER_TICK = 50L; private final Plugin plugin; private final BukkitScheduler scheduler; + @Inject public BukkitTaskScheduler(@NotNull Plugin plugin, @NotNull BukkitScheduler scheduler) { - this.plugin = Validator.notNull(plugin, "plugin cannot be null"); - this.scheduler = Validator.notNull(scheduler, "scheduler cannot be null"); + this.plugin = plugin; + this.scheduler = scheduler; } @Override public @NotNull BukkitTask runSync(@NotNull Runnable runnable) { - Validator.notNull(runnable, "runnable cannot be null"); return scheduler.runTask(plugin, runnable); } - @Override - public @NotNull BukkitTask runSync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return scheduler.runTask(plugin, task); - } - @Override public @NotNull BukkitTask runAsync(@NotNull Runnable runnable) { - Validator.notNull(runnable, "runnable cannot be null"); return scheduler.runTaskAsynchronously(plugin, runnable); } @Override - public @NotNull BukkitTask runAsync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return scheduler.runTaskAsynchronously(plugin, task); - } - - @Override - public @NotNull BukkitTask runLaterAsync(@NotNull Runnable runnable, @NotNull Duration delay) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); + public @NotNull BukkitTask runLaterAsync( + @NotNull Runnable runnable, + @NotNull Duration delay + ) { return scheduler.runTaskLaterAsynchronously(plugin, runnable, toTicks(delay)); } @Override - public @NotNull BukkitTask runLaterAsync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runLaterAsync(task, task.delay()); - } - - @Override - public @NotNull BukkitTask runLaterSync(@NotNull Runnable runnable, @NotNull Duration delay) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); + public @NotNull BukkitTask runLaterSync( + @NotNull Runnable runnable, + @NotNull Duration delay + ) { return scheduler.runTaskLater(plugin, runnable, toTicks(delay)); } - @Override - public @NotNull BukkitTask runLaterSync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runLaterSync(task, task.delay()); - } - @Override public @NotNull BukkitTask runTimerSync( @NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period ) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); - Validator.notNull(period, "period cannot be null"); - return scheduler.runTaskTimer(plugin, runnable, toTicks(delay), toTicks(period)); } - @Override - public @NotNull BukkitTask runTimerSync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runTimerSync(task, task.delay(), task.period()); - } - @Override public @NotNull BukkitTask runTimerAsync( @NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period ) { - Validator.notNull(runnable, "runnable cannot be null"); - Validator.notNull(delay, "delay cannot be null"); - Validator.notNull(period, "period cannot be null"); - return scheduler.runTaskTimerAsynchronously(plugin, runnable, toTicks(delay), toTicks(period)); } - @Override - public @NotNull BukkitTask runTimerAsync(@NotNull PluginTask task) { - Validator.notNull(task, "task cannot be null"); - return runTimerAsync(task, task.delay(), task.period()); - } - @Override public void cancelTask(int taskId) { scheduler.cancelTask(taskId); } @Override - public void shutdown() { + public void cancelAllTasks() { scheduler.cancelTasks(plugin); } - /** - * Converts the given duration to Minecraft ticks. - *

- * Fractions are truncated. Negative durations return {@code 0}. - * - * @param duration duration to convert; must not be null - * @return number of ticks (≥ 0) - */ - private static int toTicks(@NotNull Duration duration) { - Validator.notNull(duration, "duration cannot be null"); - + private static int toTicks(Duration duration) { long ticks = duration.toMillis() / MILLIS_PER_TICK; return ticks <= 0 ? 0 : (int) ticks; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java deleted file mode 100644 index 21ea995..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/PluginTask.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.imdmk.playtime.platform.scheduler; - -import java.time.Duration; - -/** - * Represents a declarative task definition used by the {@link TaskScheduler}. - * - *

A {@code PluginTask} bundles together:

- *
    - *
  • the executable logic ({@link #run()}),
  • - *
  • a delay before the first execution ({@link #delay()}),
  • - *
  • an optional repeating period ({@link #period()}).
  • - *
- * - *

Instances are consumed by scheduler methods that accept {@link PluginTask}, - * allowing tasks to be declared as self-contained objects instead of passing - * raw parameters into every scheduling call.

- * - *

Repeating vs. non-repeating:

- *
    - *
  • If {@link #period()} returns {@code Duration.ZERO}, the task is executed once after the delay.
  • - *
  • If {@link #period()} is greater than zero, the task is executed repeatedly.
  • - *
- * - *

Threading: Whether the task runs synchronously or asynchronously - * depends solely on the {@link TaskScheduler} method used (e.g., {@code runTimerSync}, {@code runTimerAsync}).

- */ -public interface PluginTask extends Runnable { - - /** - * The task logic to be executed by the scheduler. - *

- * Called either once (if {@link #period()} is zero) or repeatedly - * (if {@link #period()} is greater than zero), depending on how - * the task is scheduled. - *

- */ - @Override - void run(); - - /** - * Returns the delay before the first execution. - * - *

A zero delay means the task should run immediately.

- * - * @return the initial delay, never {@code null} - */ - Duration delay(); - - /** - * Returns the repeat period for this task. - * - *

If this returns {@code Duration.ZERO}, the task is treated as - * a one-shot task and will not repeat after the first execution.

- * - *

If the value is greater than zero, the scheduler executes the - * task repeatedly with this interval.

- * - * @return the repeat interval, never {@code null} - */ - Duration period(); -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java index dde36b9..42ca5f1 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/platform/scheduler/TaskScheduler.java @@ -5,148 +5,21 @@ import java.time.Duration; -/** - * Abstraction layer over the Bukkit {@link org.bukkit.scheduler.BukkitScheduler}, - * providing a clean, consistent API for scheduling synchronous and asynchronous tasks - * using either raw {@link Runnable} instances or declarative {@link PluginTask} objects. - * - *

Threading rules:

- *
    - *
  • Sync methods execute on the main server thread.
  • - *
  • Async methods execute off the main thread and must not access Bukkit API objects that require sync.
  • - *
- * - *

Delay & period units: All {@code Duration} values are converted to - * Minecraft ticks (1 tick = 50ms).

- * - *

PluginTask usage: All overloads accepting {@link PluginTask} - * automatically use the task's declared delay and period.

- */ public interface TaskScheduler { - /** - * Executes the given runnable immediately on the main server thread. - * - * @param runnable non-null logic to execute - * @return the task handle - */ BukkitTask runSync(@NotNull Runnable runnable); - /** - * Executes the given {@link PluginTask} immediately on the main server thread. - * - *

{@link PluginTask#delay()} and {@link PluginTask#period()} are ignored; - * this method always runs instantly.

- * - * @param task non-null task instance - * @return the task handle - */ - BukkitTask runSync(@NotNull PluginTask task); - - /** - * Executes the given runnable immediately on a separate thread. - * - * @param runnable non-null logic to execute asynchronously - * @return the task handle - */ BukkitTask runAsync(@NotNull Runnable runnable); - /** - * Executes the given {@link PluginTask} immediately on a separate thread. - * - *

{@link PluginTask#delay()} and {@link PluginTask#period()} are ignored; - * this method always runs instantly.

- * - * @param task non-null task instance - * @return the task handle - */ - BukkitTask runAsync(@NotNull PluginTask task); - - /** - * Executes the runnable asynchronously after the given delay. - * - * @param runnable task logic - * @param delay delay before execution (converted to ticks) - * @return the task handle - */ BukkitTask runLaterAsync(@NotNull Runnable runnable, @NotNull Duration delay); - /** - * Executes the {@link PluginTask} asynchronously after {@link PluginTask#delay()}. - * - *

Runs once unless {@link PluginTask#period()} is non-zero.

- * - * @param task task definition - * @return the task handle - */ - BukkitTask runLaterAsync(@NotNull PluginTask task); - - /** - * Executes the runnable synchronously after the given delay. - * - * @param runnable task logic - * @param delay delay before execution (converted to ticks) - * @return the task handle - */ BukkitTask runLaterSync(@NotNull Runnable runnable, @NotNull Duration delay); - /** - * Executes the {@link PluginTask} synchronously after {@link PluginTask#delay()}. - * - *

Runs once unless {@link PluginTask#period()} is non-zero.

- * - * @param task task definition - * @return the task handle - */ - BukkitTask runLaterSync(@NotNull PluginTask task); - - /** - * Schedules a synchronous repeating task. - * - * @param runnable logic to execute - * @param delay initial delay before the first run - * @param period time between runs - * @return the created repeating task - */ BukkitTask runTimerSync(@NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period); - /** - * Schedules a synchronous repeating {@link PluginTask} using its delay/period. - * - * @param task task definition - * @return the created repeating task - */ - BukkitTask runTimerSync(@NotNull PluginTask task); - - /** - * Schedules an asynchronous repeating task. - * - * @param runnable logic to execute - * @param delay initial delay before the first execution - * @param period time between consecutive executions - * @return the created repeating task - */ BukkitTask runTimerAsync(@NotNull Runnable runnable, @NotNull Duration delay, @NotNull Duration period); - /** - * Schedules an asynchronous repeating {@link PluginTask} using its delay/period. - * - * @param task task definition - * @return the created repeating task - */ - BukkitTask runTimerAsync(@NotNull PluginTask task); - - /** - * Cancels a scheduled task via its Bukkit ID. - * - * @param taskId scheduler task ID - */ void cancelTask(int taskId); - /** - * Cancels all tasks created for the associated plugin. - * - *

Called during plugin shutdown.

- */ - void shutdown(); + void cancelAllTasks(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java deleted file mode 100644 index 58f9476..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationSplitter.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.imdmk.playtime.shared.time; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; -import java.util.EnumMap; -import java.util.Map; - -/** - * Utility class responsible for splitting a {@link Duration} - * into its component units (days, hours, minutes, seconds). - *

- * This keeps the extraction logic in a single place, shared across - * different formatters. - */ -public final class DurationSplitter { - - private DurationSplitter() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } - - /** - * Splits the given duration into ordered units: days, hours, minutes, seconds. - * - * @param duration the duration to split (non-null) - * @return map of {@link DurationUnit} to its value in the given duration - */ - public static @NotNull Map split(@NotNull Duration duration) { - Validator.notNull(duration, "duration"); - - EnumMap parts = new EnumMap<>(DurationUnit.class); - for (DurationUnit unit : DurationUnit.ORDERED) { - parts.put(unit, unit.extract(duration)); - } - - return parts; - } -} - diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java deleted file mode 100644 index f8ef205..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/Durations.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.github.imdmk.playtime.shared.time; - -import com.github.imdmk.playtime.shared.validate.Validator; -import org.jetbrains.annotations.NotNull; - -import java.time.Duration; - -/** - * Utility class providing human-readable formatting helpers for {@link Duration}. - *

- * Supports multiple predefined {@link DurationFormatStyle} strategies. - * Zero or negative durations are normalized to {@code "<1s"}. - *

- * This class is stateless apart from the configurable default style. - */ -public final class Durations { - - /** Upper bound for any clamped duration (10 years). */ - private static final Duration MAX_NORMALIZED_DURATION = Duration.ofDays(3650); - - /** Returned when the duration is zero or negative. */ - private static final String LESS_THAN_SECOND = "<1s"; - - /** Default style used when no explicit format style is provided. */ - private static DurationFormatStyle DEFAULT_FORMAT_STYLE = DurationFormatStyle.NATURAL; - - private Durations() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); - } - - /** - * Formats the given duration using {@link #DEFAULT_FORMAT_STYLE}. - *

- * Zero or negative durations return {@code "<1s"}. - * - * @param duration the duration to format (non-null) - * @return formatted duration string (never {@code null}) - */ - public static @NotNull String format(@NotNull Duration duration) { - return format(duration, DEFAULT_FORMAT_STYLE); - } - - /** - * Formats the given duration using the specified {@link DurationFormatStyle}. - *

- * Zero or negative durations return {@code "<1s"}. - * - * @param duration the duration to format (non-null) - * @param style formatting strategy (non-null) - * @return human-readable duration string (never {@code null}) - * @throws IllegalArgumentException if duration or style are {@code null} - */ - public static @NotNull String format(@NotNull Duration duration, @NotNull DurationFormatStyle style) { - Validator.notNull(duration, "duration"); - Validator.notNull(style, "style"); - - if (duration.isZero() || duration.isNegative()) { - return LESS_THAN_SECOND; - } - - return style.format(duration); - } - - /** - * Sets the global default {@link DurationFormatStyle} used by - * {@link #format(Duration)}. - *

- * This modifies process-wide behavior and should be configured during - * plugin initialization. - * - * @param style the new default style (non-null) - * @throws IllegalArgumentException if the provided style is {@code null} - */ - public static void setDefaultFormatStyle(@NotNull DurationFormatStyle style) { - Validator.notNull(style, "durationFormatStyle"); - DEFAULT_FORMAT_STYLE = style; - } - - /** - * Normalizes (clamps) the given duration so it’s always non-negative - * and does not exceed {@link #MAX_NORMALIZED_DURATION}. - * - * @param input duration to normalize (must not be null) - * @return clamped, non-negative duration - */ - public static @NotNull Duration clamp(@NotNull Duration input) { - Validator.notNull(input, "duration"); - - if (input.isNegative()) { - return Duration.ZERO; - } - - return input.compareTo(MAX_NORMALIZED_DURATION) > 0 ? MAX_NORMALIZED_DURATION : input; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java b/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java deleted file mode 100644 index f356272..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/validate/Validator.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.imdmk.playtime.shared.validate; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; - -/** - * Utility class for common validation checks. - *

- * Provides null-safety guards used throughout the codebase. - */ -public final class Validator { - - private Validator() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); - } - - /** - * Ensures the given object is not {@code null}. - *

- * This method is typically used to validate constructor arguments and - * configuration values. If the supplied object is non-null, it is returned - * unchanged; otherwise a {@link NullPointerException} is thrown with the - * provided message. - * - * @param obj the value to validate; may be null - * @param context context of exception used when {@code obj} is null - * @param type of the validated object - * @return the non-null value of {@code obj} - * @throws NullPointerException if {@code obj} is null - */ - public static T notNull(@Nullable T obj, @NotNull String context) { - if (obj == null) { - throw new NullPointerException(context + " cannot be null"); - } - return obj; - } - - /** - * Executes the given {@link Consumer} only if the supplied object is not {@code null}. - *

- * This helper is especially useful during shutdown or cleanup phases where - * optional components may or may not be initialized. The consumer itself - * must be non-null; however, it will only be invoked when {@code obj} is non-null. - * - *

Example usage: - *

-     * Validator.ifNotNull(taskScheduler, TaskScheduler::shutdown);
-     * Validator.ifNotNull(messageService, MessageService::shutdown);
-     * 
- * - * @param obj the object to check before executing the consumer; may be null - * @param consumer operation to execute when {@code obj} is non-null (never null) - * @param type of the object passed to the consumer - * @throws NullPointerException if {@code consumer} is null - */ - public static void ifNotNull(@Nullable T obj, @NotNull Consumer consumer) { - Validator.notNull(consumer, "consumer is null"); - if (obj != null) { - consumer.accept(obj); - } - } - -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java similarity index 51% rename from playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java index dd3f53c..e90c150 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationFormatStyle.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationFormatStyle.java @@ -1,4 +1,4 @@ -package com.github.imdmk.playtime.shared.time; +package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; @@ -7,21 +7,8 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; -/** - * Defines formatting strategies for converting a {@link Duration} into - * a human-readable string. - *

- * Each style provides its own implementation of {@link #format(Duration)}. - * The underlying logic splits the duration into days, hours, minutes and seconds - * and then renders only non-zero units in a style-specific way. - */ public enum DurationFormatStyle { - /** - * Compact representation using short unit abbreviations. - *

- * Example: {@code 30d 30m 3s} - */ COMPACT { @Override public String format(@NotNull Duration duration) { @@ -30,12 +17,6 @@ public String format(@NotNull Duration duration) { Separator.SPACE); } }, - - /** - * Long form with full unit names, separated by spaces. - *

- * Example: {@code 30 days 30 minutes 3 seconds} - */ LONG { @Override public String format(@NotNull Duration duration) { @@ -44,12 +25,6 @@ public String format(@NotNull Duration duration) { Separator.SPACE); } }, - - /** - * Long form with {@code " and "} between units. - *

- * Example: {@code 30 days and 30 minutes and 3 seconds} - */ LONG_WITH_AND { @Override public String format(@NotNull Duration duration) { @@ -58,12 +33,6 @@ public String format(@NotNull Duration duration) { Separator.AND); } }, - - /** - * Natural language-like form using commas between units. - *

- * Example: {@code 30 days, 30 minutes, 3 seconds} - */ NATURAL { @Override public String format(@NotNull Duration duration) { @@ -73,42 +42,20 @@ public String format(@NotNull Duration duration) { } }; - /** - * Formats the given {@link Duration} using this style. - *

- * The duration is first decomposed into days, hours, minutes and seconds, - * and only non-zero units are included in the output. - * - * @param duration the duration to format; must not be {@code null} - * @return formatted duration according to this style (never {@code null}) - */ public abstract String format(@NotNull Duration duration); - /** - * Joins non-zero units of the given duration using the provided formatter - * and separator. - * - * @param duration duration to format - * @param valueFormatter function converting (unit, value) → string - * @param separator separator strategy - * @return formatted string, or empty string if all units are zero - */ protected static String formatWith( @NotNull Duration duration, @NotNull BiFunction valueFormatter, @NotNull Separator separator ) { final Map parts = DurationSplitter.split(duration); - return parts.entrySet().stream() .filter(e -> e.getValue() > 0) .map(e -> valueFormatter.apply(e.getKey(), e.getValue())) .collect(Collectors.joining(separator.value())); } - /** - * Separator strategies used between formatted units. - */ protected enum Separator { SPACE(" "), @@ -121,12 +68,6 @@ protected enum Separator { this.value = value; } - /** - * Returns the underlying separator string. - * - * @return separator value - */ - @NotNull public String value() { return value; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java new file mode 100644 index 0000000..b25ffde --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationSplitter.java @@ -0,0 +1,24 @@ +package com.github.imdmk.playtime.time; + +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.EnumMap; +import java.util.Map; + +public final class DurationSplitter { + + private DurationSplitter() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); + } + + public static Map split(@NotNull Duration duration) { + final EnumMap parts = new EnumMap<>(DurationUnit.class); + for (final DurationUnit unit : DurationUnit.ORDERED) { + parts.put(unit, unit.extract(duration)); + } + + return parts; + } +} + diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java similarity index 75% rename from playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java index 590694e..ef6638a 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/shared/time/DurationUnit.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/DurationUnit.java @@ -1,14 +1,9 @@ -package com.github.imdmk.playtime.shared.time; +package com.github.imdmk.playtime.time; import org.jetbrains.annotations.NotNull; import java.time.Duration; -/** - * Supported duration units and their metadata. - *

- * This enum centralizes singular/plural names, abbreviations, and extraction logic. - */ public enum DurationUnit { DAY("day", "days", "d") { @@ -36,8 +31,7 @@ public int extract(@NotNull Duration duration) { } }; - /** Ordered for consistent output. */ - public static final DurationUnit[] ORDERED = { + protected static final DurationUnit[] ORDERED = { DAY, HOUR, MINUTE, SECOND }; @@ -55,12 +49,12 @@ public int extract(@NotNull Duration duration) { public abstract int extract(@NotNull Duration duration); - public @NotNull String getAbbreviation() { + public String getAbbreviation() { return abbreviation; } - public @NotNull String toDisplayName(int value) { - String word = (value == 1 ? singular : plural); + protected String toDisplayName(int value) { + final String word = (value == 1 ? singular : plural); return DISPLAY_NAME_FORMAT.formatted(value, word); } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java new file mode 100644 index 0000000..d349fcb --- /dev/null +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/time/Durations.java @@ -0,0 +1,40 @@ +package com.github.imdmk.playtime.time; + +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +public final class Durations { + + private static final Duration MAX_NORMALIZED_DURATION = Duration.ofDays(3650); + private static final String LESS_THAN_SECOND = "<1s"; + private static DurationFormatStyle DEFAULT_FORMAT_STYLE = DurationFormatStyle.NATURAL; + + private Durations() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static String format(@NotNull Duration duration) { + return format(duration, DEFAULT_FORMAT_STYLE); + } + + public static String format(@NotNull Duration duration, @NotNull DurationFormatStyle style) { + if (duration.isZero() || duration.isNegative()) { + return LESS_THAN_SECOND; + } + + return style.format(duration); + } + + public static void setDefaultFormatStyle(@NotNull DurationFormatStyle style) { + DEFAULT_FORMAT_STYLE = style; + } + + public static Duration clamp(@NotNull Duration input) { + if (input.isNegative()) { + return Duration.ZERO; + } + + return input.compareTo(MAX_NORMALIZED_DURATION) > 0 ? MAX_NORMALIZED_DURATION : input; + } +} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java index 549603d..f01bb83 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserArgument.java @@ -2,7 +2,6 @@ import com.github.imdmk.playtime.message.MessageConfig; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; @@ -17,12 +16,6 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; -/** - * Argument resolver for {@link User} objects. - *

- * Performs a cache-only lookup on the primary server thread to avoid blocking, - * and a full asynchronous lookup (cache → database) off the main thread. - */ final class UserArgument extends ArgumentResolver { private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(2); @@ -39,10 +32,10 @@ final class UserArgument extends ArgumentResolver { @NotNull MessageConfig messageConfig, @NotNull UserService userService ) { - this.logger = Validator.notNull(logger, "logger"); - this.server = Validator.notNull(server, "server"); - this.messageConfig = Validator.notNull(messageConfig, "config"); - this.userService = Validator.notNull(userService, "userService"); + this.logger = logger; + this.server = server; + this.messageConfig = messageConfig; + this.userService = userService; } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java index f2fda4a..9ce7704 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserFactory.java @@ -4,21 +4,9 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -/** - * Platform-to-domain adapter. Creates a new domain {@link User} from - * a Bukkit {@link Player} on first join. No update logic here. - */ public interface UserFactory { - /** - * Creates a fully initialized {@link User} from the given {@link Player}. - * Implementations should set initial playtime using platform stats. - */ - @NotNull User createFrom(@NotNull Player player); + User createFrom(@NotNull Player player); - /** - * Creates a fully initialized {@link User} from the given {@link OfflinePlayer}. - * Implementations should set initial playtime using platform stats. - */ - @NotNull User createFrom(@NotNull OfflinePlayer offlinePlayer); + User createFrom(@NotNull OfflinePlayer offlinePlayer); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java deleted file mode 100644 index 5919f4d..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserModule.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.infrastructure.module.phase.CommandPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.ListenerPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.RepositoryPhase; -import com.github.imdmk.playtime.infrastructure.module.phase.TaskPhase; -import com.github.imdmk.playtime.user.cache.CaffeineUserCache; -import com.github.imdmk.playtime.user.cache.UserCache; -import com.github.imdmk.playtime.user.listener.UserJoinListener; -import com.github.imdmk.playtime.user.listener.UserQuitListener; -import com.github.imdmk.playtime.user.repository.UserEntityMapper; -import com.github.imdmk.playtime.user.repository.UserRepository; -import com.github.imdmk.playtime.user.repository.UserRepositoryOrmLite; -import com.github.imdmk.playtime.user.top.MemoryTopUsersCache; -import com.github.imdmk.playtime.user.top.TopUsersCache; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.Injector; -import org.panda_lang.utilities.inject.Resources; - -public final class UserModule implements Module { - - private UserCache userCache; - private UserEntityMapper userEntityMapper; - private UserRepository userRepository; - private TopUsersCache topUsersCache; - private UserService userService; - - @Override - public void bind(@NotNull Resources resources) { - resources.on(UserCache.class).assignInstance(() -> this.userCache); - resources.on(UserEntityMapper.class).assignInstance(() -> this.userEntityMapper); - resources.on(UserRepository.class).assignInstance(() -> this.userRepository); - resources.on(TopUsersCache.class).assignInstance(() -> this.topUsersCache); - resources.on(UserService.class).assignInstance(() -> this.userService); - } - - @Override - public void init(@NotNull Injector injector) { - this.userCache = new CaffeineUserCache(); - this.userEntityMapper = new UserEntityMapper(); - this.userRepository = injector.newInstance(UserRepositoryOrmLite.class); - this.topUsersCache = injector.newInstance(MemoryTopUsersCache.class); - this.userService = injector.newInstance(UserServiceImpl.class); - } - - @Override - public ListenerPhase listeners(@NotNull Injector injector) { - return registrar -> registrar.register( - injector.newInstance(UserJoinListener.class), - injector.newInstance(UserQuitListener.class) - ); - } - - @Override - public RepositoryPhase repositories(@NotNull Injector injector) { - return manager -> manager.register(this.userRepository); - } - - @Override - public CommandPhase commands(@NotNull Injector injector) { - return builder -> builder.argument(User.class, injector.newInstance(UserArgument.class)); - } - - @Override - public TaskPhase tasks(@NotNull Injector injector) { - return scheduler -> { - scheduler.runTimerAsync(injector.newInstance(UserSaveTask.class)); - }; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java deleted file mode 100644 index b6a2ce2..0000000 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserSaveTask.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.imdmk.playtime.user; - -import com.github.imdmk.playtime.PlaytimeService; -import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.platform.scheduler.PluginTask; -import com.github.imdmk.playtime.shared.validate.Validator; -import org.bukkit.Server; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.panda_lang.utilities.inject.annotations.Inject; - -import java.time.Duration; - -final class UserSaveTask implements PluginTask { - - private static final Duration INITIAL_DELAY = Duration.ofMinutes(30); - private static final Duration INTERVAL = Duration.ofMinutes(30); - - private final Server server; - private final PluginLogger logger; - private final PlaytimeService playtimeService; - private final UserService userService; - - @Inject - UserSaveTask( - @NotNull Server server, - @NotNull PluginLogger logger, - @NotNull PlaytimeService playtimeService, - @NotNull UserService userService - ) { - this.server = Validator.notNull(server, "server"); - this.logger = Validator.notNull(logger, "logger"); - this.playtimeService = Validator.notNull(playtimeService, "playtime"); - this.userService = Validator.notNull(userService, "userService"); - } - - @Override - public void run() { - for (final Player player : server.getOnlinePlayers()) { - userService.findCachedByUuid(player.getUniqueId()) - .ifPresent(this::updateAndSaveUser); - } - } - - private void updateAndSaveUser(@NotNull User user) { - UserTime time = playtimeService.getTime(user.getUuid()); - user.setPlaytime(time); - - userService.save(user, UserSaveReason.SCHEDULED_SAVE) - .exceptionally(e -> { - logger.error(e, "Failed to perform scheduled save for user %s (%s)", user.getName(), user.getUuid()); - return null; - }); - } - - @Override - public Duration delay() { - return INITIAL_DELAY; - } - - @Override - public Duration period() { - return INTERVAL; - } -} diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java index 93286a4..945ef71 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/UserServiceImpl.java @@ -1,11 +1,11 @@ package com.github.imdmk.playtime.user; import com.github.imdmk.playtime.UserDeleteEvent; -import com.github.imdmk.playtime.UserPreSaveEvent; import com.github.imdmk.playtime.UserSaveEvent; -import com.github.imdmk.playtime.platform.events.BukkitEventCaller; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.platform.event.BukkitEventCaller; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.cache.UserCache; import com.github.imdmk.playtime.user.repository.UserRepository; import com.github.imdmk.playtime.user.top.TopUsersCache; @@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +@Service(priority = Priority.HIGH) final class UserServiceImpl implements UserService { private static final Duration TIMEOUT = Duration.ofSeconds(2L); @@ -39,36 +40,32 @@ final class UserServiceImpl implements UserService { @NotNull UserRepository repository, @NotNull BukkitEventCaller eventCaller ) { - this.logger = Validator.notNull(logger, "logger"); - this.cache = Validator.notNull(cache, "cache"); - this.topUsersCache = Validator.notNull(topUsersCache, "topUsersCache"); - this.repository = Validator.notNull(repository, "repository"); - this.eventCaller = Validator.notNull(eventCaller, "eventCaller"); + this.logger = logger; + this.cache = cache; + this.topUsersCache = topUsersCache; + this.repository = repository; + this.eventCaller = eventCaller; } @Override - public @NotNull Optional findCachedByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); + public Optional findCachedByUuid(@NotNull UUID uuid) { return cache.getUserByUuid(uuid); } @Override - public @NotNull Optional findCachedByName(@NotNull String name) { - Validator.notNull(name, "name"); + public Optional findCachedByName(@NotNull String name) { return cache.getUserByName(name); } @Override @Unmodifiable - public @NotNull Collection getCachedUsers() { + public Collection getCachedUsers() { return cache.getCache(); // returns unmodifiable } @Override - public @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - - Optional cached = cache.getUserByUuid(uuid); + public CompletableFuture> findByUuid(@NotNull UUID uuid) { + final Optional cached = cache.getUserByUuid(uuid); if (cached.isPresent()) { return CompletableFuture.completedFuture(cached); } @@ -86,10 +83,8 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture> findByName(@NotNull String name) { - Validator.notNull(name, "name"); - - Optional cached = cache.getUserByName(name); + public CompletableFuture> findByName(@NotNull String name) { + final Optional cached = cache.getUserByName(name); if (cached.isPresent()) { return CompletableFuture.completedFuture(cached); } @@ -107,17 +102,12 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture> findTopByPlayTime(int limit) { + public CompletableFuture> findTopByPlayTime(int limit) { return topUsersCache.getTopByPlayTime(limit); } @Override - public @NotNull CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason) { - Validator.notNull(user, "user"); - Validator.notNull(reason, "reason"); - - eventCaller.callEvent(new UserPreSaveEvent(user, reason)); - + public CompletableFuture save(@NotNull User user, @NotNull UserSaveReason reason) { return repository.save(user) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(saved -> { @@ -132,8 +122,7 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); + public CompletableFuture deleteByUuid(@NotNull UUID uuid) { return repository.deleteByUuid(uuid) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(result -> { @@ -150,8 +139,7 @@ final class UserServiceImpl implements UserService { } @Override - public @NotNull CompletableFuture deleteByName(@NotNull String name) { - Validator.notNull(name, "name"); + public CompletableFuture deleteByName(@NotNull String name) { return repository.deleteByName(name) .orTimeout(TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .thenApply(deleteResult -> { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java index cc45992..349ec7c 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/CaffeineUserCache.java @@ -3,10 +3,14 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; +import com.github.imdmk.playtime.injector.subscriber.Subscribe; +import com.github.imdmk.playtime.injector.subscriber.event.PlayTimeShutdownEvent; import com.github.imdmk.playtime.user.User; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; +import org.panda_lang.utilities.inject.annotations.Inject; import java.time.Duration; import java.util.ArrayList; @@ -16,10 +20,7 @@ import java.util.UUID; import java.util.function.Consumer; -/** - * Caffeine-based implementation of {@link UserCache} with dual indexing by UUID and name. - * Entries expire after a period of inactivity and after a maximum lifetime. - */ +@Service(priority = Priority.LOWEST) public final class CaffeineUserCache implements UserCache { private static final Duration DEFAULT_EXPIRE_AFTER_ACCESS = Duration.ofHours(2); @@ -29,9 +30,6 @@ public final class CaffeineUserCache implements UserCache { private final Cache cacheByName; public CaffeineUserCache(@NotNull Duration expireAfterAccess, @NotNull Duration expireAfterWrite) { - Validator.notNull(expireAfterAccess, "expireAfterAccess"); - Validator.notNull(expireAfterWrite, "expireAfterWrite"); - this.cacheByName = Caffeine.newBuilder() .expireAfterWrite(expireAfterWrite) .expireAfterAccess(expireAfterAccess) @@ -48,14 +46,13 @@ public CaffeineUserCache(@NotNull Duration expireAfterAccess, @NotNull Duration .build(); } + @Inject public CaffeineUserCache() { this(DEFAULT_EXPIRE_AFTER_ACCESS, DEFAULT_EXPIRE_AFTER_WRITE); } @Override public void cacheUser(@NotNull User user) { - Validator.notNull(user, "user"); - final UUID uuid = user.getUuid(); final String name = user.getName(); @@ -73,16 +70,12 @@ public void cacheUser(@NotNull User user) { @Override public void invalidateUser(@NotNull User user) { - Validator.notNull(user, "user"); - cacheByUuid.invalidate(user.getUuid()); cacheByName.invalidate(user.getName()); } @Override public void invalidateByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - final User cached = cacheByUuid.getIfPresent(uuid); cacheByUuid.invalidate(uuid); if (cached != null) { @@ -92,8 +85,6 @@ public void invalidateByUuid(@NotNull UUID uuid) { @Override public void invalidateByName(@NotNull String name) { - Validator.notNull(name, "name"); - final UUID uuid = cacheByName.getIfPresent(name); if (uuid != null) { invalidateByUuid(uuid); @@ -103,24 +94,18 @@ public void invalidateByName(@NotNull String name) { } @Override - public @NotNull Optional getUserByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); + public Optional getUserByUuid(@NotNull UUID uuid) { return Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); } @Override - public @NotNull Optional getUserByName(@NotNull String name) { - Validator.notNull(name, "name"); - + public Optional getUserByName(@NotNull String name) { final UUID uuid = cacheByName.getIfPresent(name); return uuid == null ? Optional.empty() : Optional.ofNullable(cacheByUuid.getIfPresent(uuid)); } @Override public void updateUserNameMapping(@NotNull User user, @NotNull String oldName) { - Validator.notNull(user, "user cannot be null"); - Validator.notNull(oldName, "oldName cannot be null"); - final String newName = user.getName(); if (!oldName.equals(newName)) { cacheByName.invalidate(oldName); @@ -132,8 +117,6 @@ public void updateUserNameMapping(@NotNull User user, @NotNull String oldName) { @Override public void forEachUser(@NotNull Consumer action) { - Validator.notNull(action, "action cannot be null"); - // Snapshot to avoid iterating over a live view while mutating the cache for (final User user : new ArrayList<>(cacheByUuid.asMap().values())) { action.accept(user); @@ -141,13 +124,13 @@ public void forEachUser(@NotNull Consumer action) { } @Override - @NotNull @Unmodifiable public Collection getCache() { return List.copyOf(cacheByUuid.asMap().values()); } @Override + @Subscribe(event = PlayTimeShutdownEvent.class) public void invalidateAll() { cacheByUuid.invalidateAll(); cacheByName.invalidateAll(); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java index 109c1e0..98fcdcf 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/cache/UserCache.java @@ -2,91 +2,31 @@ import com.github.imdmk.playtime.user.User; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; import java.util.Collection; import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; -/** - * Cache for {@link User} aggregates. - *

- * Implementations are expected to maintain a fast lookup by both UUID and name, - * keep mappings consistent during updates, and guarantee thread-safety - * if used in a concurrent environment. - */ public interface UserCache { - /** - * Adds or replaces the given user in the cache. - * - * @param user the user instance to store - */ void cacheUser(@NotNull User user); - /** - * Removes the given user from the cache, if present. - * - * @param user the user instance to remove - */ void invalidateUser(@NotNull User user); - /** - * Removes a cached user by UUID. - * - * @param uuid the UUID to remove - */ void invalidateByUuid(@NotNull UUID uuid); - /** - * Removes a cached user by name (case-insensitive matching recommended). - * - * @param name the username to remove - */ void invalidateByName(@NotNull String name); - /** - * Retrieves a user by UUID. - * - * @param uuid the UUID to search - * @return an {@link Optional} containing the cached user, if present - */ - @NotNull Optional getUserByUuid(@NotNull UUID uuid); + Optional getUserByUuid(@NotNull UUID uuid); - /** - * Retrieves a user by name. - * - * @param name the username to search - * @return an {@link Optional} containing the cached user, if present - */ - @NotNull Optional getUserByName(@NotNull String name); + Optional getUserByName(@NotNull String name); - /** - * Updates internal name mappings for users whose username has changed. - * - * @param user the user instance with the new name - * @param oldName the previous username - */ void updateUserNameMapping(@NotNull User user, @NotNull String oldName); - /** - * Iterates over all cached users and executes the given action. - * - * @param action the callback executed for each cached user - */ void forEachUser(@NotNull Consumer action); - /** - * Returns a view of all cached users. - * - * @return an unmodifiable collection of all cached users - */ - @NotNull @Unmodifiable Collection getCache(); - /** - * Clears the entire cache. - */ void invalidateAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java similarity index 78% rename from playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java index 1671fa0..bab36c9 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserJoinListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserJoinController.java @@ -1,8 +1,8 @@ -package com.github.imdmk.playtime.user.listener; +package com.github.imdmk.playtime.user.controller; +import com.github.imdmk.playtime.injector.annotations.Controller; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserFactory; import com.github.imdmk.playtime.user.UserSaveReason; @@ -17,15 +17,12 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -import java.time.Duration; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.TimeUnit; -public final class UserJoinListener implements Listener { +@Controller +public final class UserJoinController implements Listener { - private static final Duration FIND_TIMEOUT = Duration.ofSeconds(2); - private static final Duration SAVE_TIMEOUT = Duration.ofSeconds(2); private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_JOIN; private final Server server; @@ -35,18 +32,18 @@ public final class UserJoinListener implements Listener { private final TaskScheduler taskScheduler; @Inject - public UserJoinListener( + public UserJoinController( @NotNull Server server, @NotNull PluginLogger logger, @NotNull UserService userService, @NotNull UserFactory userFactory, @NotNull TaskScheduler taskScheduler ) { - this.server = Validator.notNull(server, "server"); - this.logger = Validator.notNull(logger, "logger"); - this.userService = Validator.notNull(userService, "userService"); - this.userFactory = Validator.notNull(userFactory, "userFactory cannot be null"); - this.taskScheduler = Validator.notNull(taskScheduler, "taskScheduler cannot be null"); + this.server = server; + this.logger = logger; + this.userService = userService; + this.userFactory = userFactory; + this.taskScheduler = taskScheduler; } @EventHandler(priority = EventPriority.LOWEST) @@ -63,7 +60,6 @@ private void handlePlayerJoin(Player player) { final UUID uuid = player.getUniqueId(); userService.findByUuid(uuid) - .orTimeout(FIND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((optional, e) -> { if (e != null) { logger.error(e, "Failed to load user on join uuid=%s", uuid); @@ -88,7 +84,6 @@ private void handleNewUser(Player player) { private void handleExistingUser(Player player, User user) { final String name = player.getName(); - if (!updateNameIfChanged(user, name)) { return; } @@ -98,7 +93,6 @@ private void handleExistingUser(Player player, User user) { private void saveUser(User user, String context) { userService.save(user, SAVE_REASON) - .orTimeout(SAVE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((r, e) -> { if (e != null) { logger.error(e, "Failed to save user %s uuid=%s", context, user.getUuid()); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java similarity index 67% rename from playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java rename to playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java index b2ba6b4..5f5b410 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/listener/UserQuitListener.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/controller/UserQuitController.java @@ -1,7 +1,7 @@ -package com.github.imdmk.playtime.user.listener; +package com.github.imdmk.playtime.user.controller; +import com.github.imdmk.playtime.injector.annotations.Controller; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.UserSaveReason; import com.github.imdmk.playtime.user.UserService; import org.bukkit.entity.Player; @@ -12,22 +12,20 @@ import org.jetbrains.annotations.NotNull; import org.panda_lang.utilities.inject.annotations.Inject; -import java.time.Duration; import java.util.UUID; -import java.util.concurrent.TimeUnit; -public final class UserQuitListener implements Listener { +@Controller +public final class UserQuitController implements Listener { - private static final Duration SAVE_TIMEOUT = Duration.ofSeconds(2); private static final UserSaveReason SAVE_REASON = UserSaveReason.PLAYER_LEAVE; private final PluginLogger logger; private final UserService userService; @Inject - public UserQuitListener(@NotNull PluginLogger logger, @NotNull UserService userService) { - this.logger = Validator.notNull(logger, "logger"); - this.userService = Validator.notNull(userService, "userService"); + public UserQuitController(@NotNull PluginLogger logger, @NotNull UserService userService) { + this.logger = logger; + this.userService = userService; } @EventHandler(priority = EventPriority.HIGHEST) @@ -38,7 +36,6 @@ public void onPlayerQuit(PlayerQuitEvent event) { final String name = player.getName(); userService.findCachedByUuid(uuid).ifPresent(user -> userService.save(user, SAVE_REASON) - .orTimeout(SAVE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((u, e) -> { if (e != null) { logger.error(e, "Failed to save user on quit %s (%s)", name, uuid); diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java index f9884ab..99d8f94 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntity.java @@ -7,28 +7,18 @@ import java.util.Objects; import java.util.UUID; -/** - * Persistent representation of a user stored in the database. - *

- * This entity is managed by ORMLite and maps directly to the "spent_time_users" table. - * It mirrors the in-memory {@code User} object used in runtime logic. - */ @DatabaseTable(tableName = UserEntityMeta.TABLE) public final class UserEntity { - /** Primary key — unique player UUID. */ @DatabaseField(id = true, canBeNull = false, columnName = UserEntityMeta.Col.UUID) private UUID uuid; - /** Last known player name. */ @DatabaseField(canBeNull = false, index = true, columnName = UserEntityMeta.Col.NAME) private String name; - /** Total spent time in milliseconds. */ @DatabaseField(canBeNull = false, columnName = UserEntityMeta.Col.PLAYTIME_MILLIS) private long playtimeMillis; - /** No-arg constructor required by ORMLite. */ public UserEntity() {} public UserEntity(@NotNull UUID uuid, @NotNull String name, long playtimeMillis) { @@ -37,7 +27,7 @@ public UserEntity(@NotNull UUID uuid, @NotNull String name, long playtimeMillis) this.playtimeMillis = playtimeMillis; } - public @NotNull UUID getUuid() { + public UUID getUuid() { return this.uuid; } @@ -45,7 +35,7 @@ public void setUuid(@NotNull UUID uuid) { this.uuid = uuid; } - public @NotNull String getName() { + public String getName() { return this.name; } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java index 2cb2865..fafaa28 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMapper.java @@ -1,19 +1,18 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.EntityMapper; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.database.repository.ormlite.EntityMapper; +import com.github.imdmk.playtime.injector.annotations.Service; +import com.github.imdmk.playtime.injector.priority.Priority; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserTime; import org.jetbrains.annotations.NotNull; -/** - * Maps between the persistent {@link UserEntity} and the in-memory {@link User}. - */ -public final class UserEntityMapper implements EntityMapper { +@Service(priority = Priority.LOW) +public final class UserEntityMapper + implements EntityMapper { @Override - public @NotNull UserEntity toEntity(@NotNull User user) { - Validator.notNull(user, "user"); + public UserEntity toEntity(@NotNull User user) { return new UserEntity( user.getUuid(), user.getName(), @@ -22,8 +21,7 @@ public final class UserEntityMapper implements EntityMapper { } @Override - public @NotNull User toDomain(@NotNull UserEntity entity) { - Validator.notNull(entity, "entity"); + public User toDomain(@NotNull UserEntity entity) { return new User( entity.getUuid(), entity.getName(), diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java index 9bdb70b..622d112 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserEntityMeta.java @@ -1,35 +1,17 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.EntityMeta; +import com.github.imdmk.playtime.database.repository.ormlite.EntityMeta; -/** - * Database metadata for the {@code advanced_playtime_users} table. - * - *

This interface defines the table name and all column identifiers used by - * {@link UserEntity} and the corresponding repository implementation.

- * - *

Centralizing these names ensures consistency across entity mappings, - * DAO queries, migrations, and schema creation routines.

- */ interface UserEntityMeta extends EntityMeta { - /** Name of the table storing persistent user records. */ String TABLE = "advanced_playtime_users"; - /** - * Column name definitions for {@link UserEntity}. - * - *

All constants represent physical column names in the database schema.

- */ interface Col { - /** Unique player identifier (primary key, NOT NULL). */ String UUID = "uuid"; - /** Last known player name (NOT NULL, indexed). */ String NAME = "name"; - /** Total accumulated playtime in milliseconds (NOT NULL). */ String PLAYTIME_MILLIS = "playtimeMillis"; } } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java index a7cc1bc..fca03c0 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepository.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.Repository; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserDeleteResult; import org.jetbrains.annotations.NotNull; @@ -10,71 +9,16 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -/** - * Asynchronous repository for managing and querying {@link User} records. - *

All methods return non-null {@link CompletableFuture}s and complete exceptionally on failure.

- *

Name matching policy should be documented by the implementation (recommended: case-insensitive, normalized).

- */ -public interface UserRepository extends Repository { +public interface UserRepository { - /** - * Finds a user by UUID. - * - * @param uuid non-null UUID - * @return non-null future with an Optional user (empty if not found) - */ - @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findByUuid(@NotNull UUID uuid); + CompletableFuture> findByName(@NotNull String name); + CompletableFuture> findAll(); - /** - * Finds a user by exact name (implementation should document case handling). - * - * @param name non-null username - * @return non-null future with an Optional user (empty if not found) - */ - @NotNull CompletableFuture> findByName(@NotNull String name); + CompletableFuture> findTopByPlayTime(long limit); - /** - * Retrieves all users from the data source. - *

- * The returned list order is implementation-defined unless otherwise documented. - * Implementations are expected to return an immutable, non-null list, and may apply - * internal caching or batching strategies for performance. - *

- * - * @return non-null future with all persisted users (possibly empty) - */ - @NotNull CompletableFuture> findAll(); + CompletableFuture deleteByUuid(@NotNull UUID uuid); + CompletableFuture deleteByName(@NotNull String name); - /** - * Returns top users by spent time, sorted descending. - * Ties are resolved deterministically (e.g., by UUID ascending). - * - * @param limit number of users to return; must be > 0 - * @return non-null future with an immutable list (possibly empty) - */ - @NotNull CompletableFuture> findTopByPlayTime(long limit); - - /** - * Creates or updates the user (upsert). - * - * @param user non-null user - * @return non-null future with the persisted user - */ - @NotNull CompletableFuture save(@NotNull User user); - - /** - * Deletes a user by UUID. - * - * @param uuid non-null UUID - * @return non-null future that completes with {@code true} if a row was deleted, otherwise {@code false} - */ - @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid); - - /** - * Deletes a user by name. - * - * @param name non-null username - * @return non-null future that completes with {@code true} if a row was deleted, otherwise {@code false} - */ - @NotNull CompletableFuture deleteByName(@NotNull String name); + CompletableFuture save(@NotNull User user); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java index 0b891e5..7baa30d 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/repository/UserRepositoryOrmLite.java @@ -1,9 +1,10 @@ package com.github.imdmk.playtime.user.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.DatabaseBootstrap; +import com.github.imdmk.playtime.database.repository.ormlite.OrmLiteRepository; +import com.github.imdmk.playtime.injector.annotations.Repository; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; +import com.github.imdmk.playtime.platform.scheduler.TaskScheduler; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.UserDeleteResult; import com.github.imdmk.playtime.user.UserDeleteStatus; @@ -16,22 +17,22 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +@Repository public final class UserRepositoryOrmLite - extends BaseDaoRepository + extends OrmLiteRepository implements UserRepository { - private final PluginLogger logger; private final UserEntityMapper mapper; @Inject public UserRepositoryOrmLite( @NotNull PluginLogger logger, - @NotNull RepositoryContext context, + @NotNull TaskScheduler taskScheduler, + @NotNull DatabaseBootstrap databaseBootstrap, @NotNull UserEntityMapper mapper ) { - super(logger, context); - this.logger = Validator.notNull(logger, "logger"); - this.mapper = Validator.notNull(mapper, "mapper"); + super(logger, taskScheduler, databaseBootstrap); + this.mapper = mapper; } @Override @@ -45,9 +46,8 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - return executeAsync(() -> { + public CompletableFuture> findByUuid(@NotNull UUID uuid) { + return execute(() -> { try { return Optional.ofNullable(dao.queryForId(uuid)) .map(mapper::toDomain); @@ -59,11 +59,10 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findByName(@NotNull String name) { - Validator.notNull(name, "name"); - return executeAsync(() -> { + public CompletableFuture> findByName(@NotNull String name) { + return execute(() -> { try { - UserEntity entity = dao.queryBuilder() + final UserEntity entity = dao.queryBuilder() .where().eq(UserEntityMeta.Col.NAME, name) .queryForFirst(); return Optional.ofNullable(entity).map(mapper::toDomain); @@ -75,8 +74,8 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findAll() { - return executeAsync(() -> { + public CompletableFuture> findAll() { + return execute(() -> { try { return mapper.toDomainList(dao.queryForAll()); } catch (SQLException e) { @@ -87,12 +86,12 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture> findTopByPlayTime(long limit) { + public CompletableFuture> findTopByPlayTime(long limit) { if (limit <= 0) { return CompletableFuture.completedFuture(List.of()); } - return executeAsync(() -> { + return execute(() -> { try { return mapper.toDomainList( dao.queryBuilder() @@ -109,9 +108,8 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture save(@NotNull User user) { - Validator.notNull(user, "user"); - return executeAsync(() -> { + public CompletableFuture save(@NotNull User user) { + return execute(() -> { try { dao.createOrUpdate(mapper.toEntity(user)); return user; @@ -123,18 +121,16 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture deleteByUuid(@NotNull UUID uuid) { - Validator.notNull(uuid, "uuid"); - return executeAsync(() -> { + public CompletableFuture deleteByUuid(@NotNull UUID uuid) { + return execute(() -> { try { - UserEntity userEntity = dao.queryForId(uuid); + final UserEntity userEntity = dao.queryForId(uuid); if (userEntity == null) { return new UserDeleteResult(null, UserDeleteStatus.NOT_FOUND); } - User user = mapper.toDomain(userEntity); - - int rows = dao.deleteById(uuid); + final User user = mapper.toDomain(userEntity); + final int rows = dao.deleteById(uuid); return rows > 0 ? new UserDeleteResult(user, UserDeleteStatus.DELETED) : new UserDeleteResult(user, UserDeleteStatus.FAILED); @@ -146,20 +142,18 @@ protected List> entitySubClasses() { } @Override - public @NotNull CompletableFuture deleteByName(@NotNull String name) { - Validator.notNull(name, "name"); - return executeAsync(() -> { + public CompletableFuture deleteByName(@NotNull String name) { + return execute(() -> { try { - UserEntity userEntity = dao.queryBuilder() + final UserEntity userEntity = dao.queryBuilder() .where().eq(UserEntityMeta.Col.NAME, name) .queryForFirst(); if (userEntity == null) { return new UserDeleteResult(null, UserDeleteStatus.NOT_FOUND); } - User user = mapper.toDomain(userEntity); - - int rows = dao.delete(userEntity); + final User user = mapper.toDomain(userEntity); + final int rows = dao.delete(userEntity); return rows > 0 ? new UserDeleteResult(user, UserDeleteStatus.DELETED) : new UserDeleteResult(user, UserDeleteStatus.FAILED); @@ -169,4 +163,6 @@ protected List> entitySubClasses() { } }); } + + } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java index 5f1ce6e..8bdaaa4 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/CachedLeaderboard.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.user.top; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; @@ -9,40 +8,17 @@ import java.time.Instant; import java.util.List; -/** - * Immutable representation of a cached leaderboard snapshot. - *

- * Holds the ordered user list, the limit used during loading, and the timestamp - * when the data was retrieved. Provides logic for determining whether the snapshot - * is still valid for a given request. - */ record CachedLeaderboard( - @NotNull @Unmodifiable List users, + @NotNull List users, int limit, @NotNull Instant loadedAt ) { - /** - * Constructs a new leaderboard snapshot. - * A defensive copy of the user list is created to ensure immutability. - */ CachedLeaderboard { - Validator.notNull(users, "users"); - Validator.notNull(loadedAt, "loadedAt"); users = List.copyOf(users); } - /** - * Determines whether this leaderboard is valid for the requested limit and expiration policy. - * - * @param requestedLimit limit requested by the caller - * @param expireAfter duration after which the snapshot becomes stale - * @param now current time reference - * @return {@code true} if the leaderboard is fresh and large enough, otherwise {@code false} - */ boolean isUsable(int requestedLimit, @NotNull Duration expireAfter, @NotNull Instant now) { - Validator.notNull(now, "now"); - if (this.limit < requestedLimit) { return false; } @@ -51,16 +27,10 @@ boolean isUsable(int requestedLimit, @NotNull Duration expireAfter, @NotNull Ins return true; } - Instant expiresAt = this.loadedAt.plus(expireAfter); + final Instant expiresAt = this.loadedAt.plus(expireAfter); return expiresAt.isAfter(now); } - /** - * Returns the ordered user list. - * This list is unmodifiable and safe to expose. - * - * @return immutable list of users - */ @Override @Unmodifiable public @NotNull List users() { diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java index 1f1d5df..33d5102 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/MemoryTopUsersCache.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.user.top; import com.github.imdmk.playtime.platform.logger.PluginLogger; -import com.github.imdmk.playtime.shared.validate.Validator; import com.github.imdmk.playtime.user.User; import com.github.imdmk.playtime.user.repository.UserRepository; import org.jetbrains.annotations.NotNull; @@ -26,9 +25,9 @@ public MemoryTopUsersCache( @NotNull TopUsersCacheConfig config, @NotNull UserRepository userRepository ) { - this.logger = Validator.notNull(logger, "logger"); - this.config = Validator.notNull(config, "config"); - this.userRepository = Validator.notNull(userRepository, "userRepository"); + this.logger = logger; + this.config = config; + this.userRepository = userRepository; } @Override diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java index e840a2c..6270415 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCache.java @@ -5,35 +5,11 @@ import java.util.List; import java.util.concurrent.CompletableFuture; -/** - * Cache abstraction for retrieving the top users by spent time. - *

- * Implementations are responsible for storing and serving leaderboard data, - * including cache invalidation and optional limit-based slicing. - */ public interface TopUsersCache { - /** - * Returns the cached or freshly loaded leaderboard using the default limit - * defined in the cache configuration. - * - * @return future containing an ordered list of top users - */ CompletableFuture> getTopByPlayTime(); - /** - * Returns the cached or freshly loaded leaderboard limited to the given size. - * Implementations may slice an existing cached leaderboard or trigger a reload - * if the cache is stale or insufficient for the requested limit. - * - * @param limit maximum number of users to return - * @return future containing an ordered list of top users - */ CompletableFuture> getTopByPlayTime(int limit); - /** - * Invalidates all cached leaderboard data. - * Next invocation of {@link #getTopByPlayTime()} will trigger a reload. - */ void invalidateAll(); } diff --git a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java index 2762395..c034e01 100644 --- a/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java +++ b/playtime-core/src/main/java/com/github/imdmk/playtime/user/top/TopUsersCacheConfig.java @@ -1,12 +1,14 @@ package com.github.imdmk.playtime.user.top; import com.github.imdmk.playtime.config.ConfigSection; +import com.github.imdmk.playtime.injector.annotations.ConfigFile; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.serdes.OkaeriSerdesPack; import org.jetbrains.annotations.NotNull; import java.time.Duration; +@ConfigFile public final class TopUsersCacheConfig extends ConfigSection { @Comment({ @@ -53,12 +55,12 @@ public final class TopUsersCacheConfig extends ConfigSection { public Duration topUsersQueryTimeout = Duration.ofSeconds(3); @Override - public @NotNull OkaeriSerdesPack getSerdesPack() { + public @NotNull OkaeriSerdesPack serdesPack() { return registry -> {}; } @Override - public @NotNull String getFileName() { - return "leaderboardConfig.yml"; + public @NotNull String fileName() { + return "leaderboard.yml"; } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java similarity index 63% rename from playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java index 8aedc8c..d12ac1d 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DriverConfigurerFactoryTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/driver/DataSourceConfigurerFactoryTest.java @@ -1,19 +1,19 @@ package com.github.imdmk.playtime.database.driver; -import com.github.imdmk.playtime.infrastructure.database.DatabaseMode; -import com.github.imdmk.playtime.infrastructure.database.driver.configurer.DriverConfigurerFactory; +import com.github.imdmk.playtime.database.DatabaseMode; +import com.github.imdmk.playtime.database.configurer.DataSourceConfigurerFactory; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNullPointerException; -class DriverConfigurerFactoryTest { +class DataSourceConfigurerFactoryTest { @Test void shouldReturnConfigurerForEachSupportedMode() { for (DatabaseMode mode : DatabaseMode.values()) { - assertThatCode(() -> DriverConfigurerFactory.getFor(mode)) + assertThatCode(() -> DataSourceConfigurerFactory.getFor(mode)) .doesNotThrowAnyException(); } } @@ -21,7 +21,7 @@ void shouldReturnConfigurerForEachSupportedMode() { @Test void shouldRejectNullMode() { assertThatNullPointerException() - .isThrownBy(() -> DriverConfigurerFactory.getFor(null)); + .isThrownBy(() -> DataSourceConfigurerFactory.getFor(null)); } @Test @@ -29,7 +29,7 @@ void shouldThrowForUnsupportedMode() { // Just to be sure — if enum expands in future // create invalid fake enum value assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> DriverConfigurerFactory.getFor(DatabaseMode.valueOf("NON_EXISTENT"))); // if ever added + .isThrownBy(() -> DataSourceConfigurerFactory.getFor(DatabaseMode.valueOf("NON_EXISTENT"))); // if ever added } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java index 498b585..c821959 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/RepositoryManagerTest.java @@ -1,7 +1,5 @@ package com.github.imdmk.playtime.database.repository; -import com.github.imdmk.playtime.infrastructure.database.repository.Repository; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryManager; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.j256.ormlite.support.ConnectionSource; import org.junit.jupiter.api.Test; @@ -20,7 +18,7 @@ class RepositoryManagerTest { @Test void registerShouldStoreRepository() { PluginLogger logger = mock(PluginLogger.class); - Repository repo = mock(Repository.class); + RepositoryBootstrap repo = mock(RepositoryBootstrap.class); RepositoryManager manager = new RepositoryManager(logger); @@ -38,8 +36,8 @@ void registerShouldStoreRepository() { @Test void startAllShouldInvokeStartOnEachRepository() throws Exception { PluginLogger logger = mock(PluginLogger.class); - Repository repo1 = mock(Repository.class); - Repository repo2 = mock(Repository.class); + RepositoryBootstrap repo1 = mock(RepositoryBootstrap.class); + RepositoryBootstrap repo2 = mock(RepositoryBootstrap.class); ConnectionSource source = mock(ConnectionSource.class); RepositoryManager manager = new RepositoryManager(logger); @@ -54,7 +52,7 @@ void startAllShouldInvokeStartOnEachRepository() throws Exception { @Test void startAllShouldLogAndRethrowSQLException() throws Exception { PluginLogger logger = mock(PluginLogger.class); - Repository faulty = mock(Repository.class); + RepositoryBootstrap faulty = mock(RepositoryBootstrap.class); ConnectionSource source = mock(ConnectionSource.class); doThrow(new SQLException("boom")).when(faulty).start(source); @@ -73,8 +71,8 @@ void startAllShouldLogAndRethrowSQLException() throws Exception { @Test void closeShouldInvokeCloseOnEachRepository() { PluginLogger logger = mock(PluginLogger.class); - Repository repo1 = mock(Repository.class); - Repository repo2 = mock(Repository.class); + RepositoryBootstrap repo1 = mock(RepositoryBootstrap.class); + RepositoryBootstrap repo2 = mock(RepositoryBootstrap.class); RepositoryManager manager = new RepositoryManager(logger); manager.register(repo1, repo2); @@ -88,7 +86,7 @@ void closeShouldInvokeCloseOnEachRepository() { @Test void closeShouldLogWarningWhenRepositoryThrows() { PluginLogger logger = mock(PluginLogger.class); - Repository repo = mock(Repository.class); + RepositoryBootstrap repo = mock(RepositoryBootstrap.class); doThrow(new RuntimeException("err")).when(repo).close(); diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java index 8e37c4d..9e387bb 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/EntityMapperTest.java @@ -1,6 +1,5 @@ package com.github.imdmk.playtime.database.repository.ormlite; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.EntityMapper; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -18,7 +17,7 @@ private static class FakeMapper implements EntityMapper { } @Override - public @NotNull Integer toDomain(String entity) { + public @NotNull Integer toDomain(@NotNull String entity) { return Integer.parseInt(entity.substring(1)); } } diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java similarity index 95% rename from playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java index 3e34ea8..134d3c1 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/BaseDaoRepositoryTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/database/repository/ormlite/OrmLiteRepositoryTest.java @@ -1,7 +1,6 @@ package com.github.imdmk.playtime.database.repository.ormlite; -import com.github.imdmk.playtime.infrastructure.database.repository.RepositoryContext; -import com.github.imdmk.playtime.infrastructure.database.repository.ormlite.BaseDaoRepository; +import com.github.imdmk.playtime.database.repository.RepositoryContext; import com.github.imdmk.playtime.platform.logger.PluginLogger; import com.j256.ormlite.dao.Dao; import org.junit.jupiter.api.Test; @@ -22,9 +21,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -class BaseDaoRepositoryTest { +class OrmLiteRepositoryTest { - public static class TestDaoRepository extends BaseDaoRepository { + public static class TestDaoRepository extends OrmLiteRepository { public TestDaoRepository(PluginLogger logger, RepositoryContext context) { super(logger, context); diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationFormatStyleTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java similarity index 100% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationFormatStyleTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationFormatStyleTest.java diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationSplitterTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java similarity index 100% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationSplitterTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationSplitterTest.java diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationUnitTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java similarity index 94% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationUnitTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java index 853bee5..4fb45ce 100644 --- a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationUnitTest.java +++ b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationUnitTest.java @@ -4,8 +4,6 @@ import java.time.Duration; -import static org.assertj.core.api.Assertions.assertThat; - class DurationUnitTest { @Test diff --git a/playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationsTest.java b/playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java similarity index 100% rename from playtime-core/src/test/java/com/github/imdmk/playtime/shared/time/DurationsTest.java rename to playtime-core/src/test/java/com/github/imdmk/playtime/time/DurationsTest.java diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java deleted file mode 100644 index a1d98de..0000000 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderDefaultSettings.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.config.PluginConfig; -import com.github.imdmk.playtime.feature.migration.MigrationConfig; -import com.github.imdmk.playtime.feature.migration.MigrationModule; -import com.github.imdmk.playtime.feature.playtime.PlayTimeModule; -import com.github.imdmk.playtime.feature.reload.ReloadModule; -import com.github.imdmk.playtime.infrastructure.database.DatabaseConfig; -import com.github.imdmk.playtime.infrastructure.module.Module; -import com.github.imdmk.playtime.message.MessageConfig; -import com.github.imdmk.playtime.platform.gui.GuiModule; -import com.github.imdmk.playtime.platform.gui.config.GuiConfig; -import com.github.imdmk.playtime.user.UserModule; -import com.github.imdmk.playtime.user.top.TopUsersCacheConfig; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Default bootstrap settings for PlayTime: config sections and plugin modules. - */ -class LoaderDefaultSettings implements LoaderSettings { - - @Override - public @NotNull List> configSections() { - return List.of( - PluginConfig.class, - MessageConfig.class, - DatabaseConfig.class, - GuiConfig.class, - MigrationConfig.class, - TopUsersCacheConfig.class - ); - } - - @Override - public @NotNull List> pluginModules() { - return List.of( - UserModule.class, - PlayTimeModule.class, - GuiModule.class, - MigrationModule.class, - ReloadModule.class - ); - } -} diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java deleted file mode 100644 index f649c45..0000000 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/LoaderSettings.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.imdmk.playtime; - -import com.github.imdmk.playtime.config.ConfigSection; -import com.github.imdmk.playtime.infrastructure.module.Module; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Defines the bootstrap configuration used by {@link PlayTimePluginLoader}. - * - *

This interface decouples the loader from concrete configuration sources, - * enabling custom setups (testing, profiling, modular distributions, etc.).

- */ -public interface LoaderSettings { - - /** - * Returns a list of all {@link ConfigSection} types that should be registered - * and loaded during plugin bootstrap. - * - * @return non-null list of configuration section classes - */ - @NotNull List> configSections(); - - /** - * Returns the ordered list of {@link Module} classes that define - * the plugin's functional modules (features, services, listeners, commands). - * - * @return non-null list of plugin module classes - */ - @NotNull List> pluginModules(); -} diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java deleted file mode 100644 index 5be123c..0000000 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimeExecutorFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.imdmk.playtime; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.time.Duration; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - -/** - * Factory and utilities for worker executor used by PlayTime. - */ -final class PlayTimeExecutorFactory { - - private static final String WORKER_THREAD_NAME = "AdvancedPlayTime-Worker"; - private static final Duration SHUTDOWN_TIMEOUT = Duration.ofSeconds(3); - - private PlayTimeExecutorFactory() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated."); - } - - /** - * Creates a dedicated single-threaded worker executor for asynchronous plugin operations. - * The executor uses a named daemon thread ({@code PlayTime-Worker}). - * - * @return configured single-threaded executor service - */ - static @NotNull ExecutorService newWorkerExecutor() { - ThreadFactory factory = runnable -> { - Thread thread = new Thread(runnable, WORKER_THREAD_NAME); - thread.setDaemon(true); - return thread; - }; - - return Executors.newSingleThreadExecutor(factory); - } - - /** - * Shuts down the given executor quietly, awaiting termination for a short period. - * If it fails to terminate gracefully, all running tasks are forcibly cancelled. - * - * @param executor the executor to shut down, may be {@code null} - */ - static void shutdownQuietly(@Nullable ExecutorService executor) { - if (executor == null) { - return; - } - - executor.shutdown(); - try { - if (!executor.awaitTermination(SHUTDOWN_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException ie) { - executor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } -} - diff --git a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java b/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java index a935cb8..c3a18c9 100644 --- a/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java +++ b/playtime-plugin/src/main/java/com/github/imdmk/playtime/PlayTimePluginLoader.java @@ -1,51 +1,23 @@ package com.github.imdmk.playtime; -import com.github.imdmk.playtime.shared.validate.Validator; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.ExecutorService; - public final class PlayTimePluginLoader extends JavaPlugin { - private final ExecutorService executor; - private final LoaderSettings settings; - - private volatile PlayTimePlugin pluginCore; - - public PlayTimePluginLoader(@NotNull ExecutorService executor, @NotNull LoaderSettings settings) { - this.executor = Validator.notNull(executor, "executor"); - this.settings = Validator.notNull(settings, "settings"); - } + private PlayTimePlugin pluginCore; - public PlayTimePluginLoader() { - this(PlayTimeExecutorFactory.newWorkerExecutor(), new LoaderDefaultSettings()); - } - - /** - * Called by Bukkit when the plugin is being enabled - */ @Override public void onEnable() { final Plugin plugin = this; - - this.pluginCore = new PlayTimePlugin(plugin, executor); - this.pluginCore.enable(settings.configSections(), settings.pluginModules()); + this.pluginCore = new PlayTimePlugin(plugin); } - /** - * Called by Bukkit when the plugin is being disabled, either on server shutdown - * or via manual reload. - */ @Override public void onDisable() { if (this.pluginCore != null) { this.pluginCore.disable(); this.pluginCore = null; } - - PlayTimeExecutorFactory.shutdownQuietly(executor); } }