Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>This interface provides unified access to the main subsystems of the plugin:</p>
*
* <ul>
* <li>{@link UserService} – manages player data persistence, synchronization,
* and user-specific statistics.</li>
* <li>{@link PlaytimeService} – handles retrieval and manipulation of player
* playtime data.</li>
* </ul>
*
* <p>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.</p>
*
* <p><b>Usage Example:</b></p>
*
* <pre>{@code
* PlayTimeApi api = PlayTimeApiProvider.get();
*
* UserService userService = api.userService();
* PlaytimeService playtimeService = api.playtimeService();
*
* UUID uuid = player.getUniqueId();
* UserTime time = playtimeService.getTime(uuid);
* }</pre>
*
* @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.
*
* <p>This service acts as the bridge between the plugin’s internal user model
* and the underlying storage or platform-specific systems.</p>
*
* @return non-null {@link PlaytimeService} instance
*/
@NotNull PlaytimeService playtimeService();
PlayTimeService getPlayTimeService();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

import org.jetbrains.annotations.NotNull;

/**
* Static access point for the {@link PlayTimeApi}.
* <p>
* Thread-safe: publication via synchronized register/unregister and a volatile reference.
*/
public final class PlayTimeApiProvider {

private static volatile PlayTimeApi API; // visibility across threads
Expand All @@ -15,56 +10,29 @@ 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.");
}
API = api;
}

/**
* Forces registration of the {@link PlayTimeApi} instance.
* <p>
* 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.");
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,141 +2,62 @@

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.
*
* <p>This class is the main domain model for player statistics and provides:
* <ul>
* <li>stable identity via {@link UUID},</li>
* <li>thread-safe counters using {@link AtomicLong} and {@link AtomicInteger},</li>
* <li>mutable fields for name, join tracking, and playtime accumulation.</li>
* </ul>
*
* 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.
* <p>
* 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;
if (!(o instanceof User other)) return false;
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{" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.</p>
*
* <p><strong>Usage:</strong> 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.</p>
*
* @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.
* <p>
* This method is equivalent to checking:
* <pre>{@code user != null && status == UserDeleteStatus.DELETED}</pre>
*
* @return {@code true} if the user was successfully deleted; {@code false} otherwise
*/
public boolean isSuccess() {
return this.user != null && this.status == UserDeleteStatus.DELETED;
}
Expand Down
Loading
Loading