diff --git a/eternalcore-api/src/main/java/com/eternalcode/core/EternalCoreApi.java b/eternalcore-api/src/main/java/com/eternalcode/core/EternalCoreApi.java index 9c56892ce..c53609a6f 100644 --- a/eternalcore-api/src/main/java/com/eternalcode/core/EternalCoreApi.java +++ b/eternalcore-api/src/main/java/com/eternalcode/core/EternalCoreApi.java @@ -10,6 +10,7 @@ import com.eternalcode.core.feature.spawn.SpawnService; import com.eternalcode.core.feature.teleport.TeleportService; import com.eternalcode.core.feature.warp.WarpService; +import com.eternalcode.core.user.UserManager; public interface EternalCoreApi { @@ -32,4 +33,6 @@ public interface EternalCoreApi { TeleportService getTeleportService(); WarpService getWarpService(); + + UserManager getUserManager(); } diff --git a/eternalcore-api/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-api/src/main/java/com/eternalcode/core/user/User.java new file mode 100644 index 000000000..1e4513e93 --- /dev/null +++ b/eternalcore-api/src/main/java/com/eternalcode/core/user/User.java @@ -0,0 +1,18 @@ +package com.eternalcode.core.user; + +import java.time.Instant; +import java.util.UUID; + +/** + * Immutable snapshot of a player's EternalCore account data. + */ +public interface User { + + UUID getUniqueId(); + + String getName(); + + Instant getLastSeen(); + + Instant getAccountCreated(); +} diff --git a/eternalcore-api/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-api/src/main/java/com/eternalcode/core/user/UserManager.java new file mode 100644 index 000000000..79d2ac28e --- /dev/null +++ b/eternalcore-api/src/main/java/com/eternalcode/core/user/UserManager.java @@ -0,0 +1,30 @@ +package com.eternalcode.core.user; + +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface UserManager { + + /** + * Returns the cached user with the given unique id. + */ + Optional getUser(UUID uniqueId); + + /** + * Returns the cached user with the given name. + */ + Optional getUser(String name); + + /** + * Loads the user from cache or database, creating a new one if it does not exist. + * Refreshes the user's name and last seen time. + */ + CompletableFuture findOrCreate(UUID uniqueId, String name); + + /** + * Returns an immutable snapshot of all cached users. + */ + Collection getUsers(); +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/EternalCoreApiImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/EternalCoreApiImpl.java index 4680437ba..7efcc312d 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/EternalCoreApiImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/EternalCoreApiImpl.java @@ -11,6 +11,7 @@ import com.eternalcode.core.feature.teleport.TeleportService; import com.eternalcode.core.feature.warp.WarpService; import com.eternalcode.core.injector.DependencyProvider; +import com.eternalcode.core.user.UserManager; class EternalCoreApiImpl implements EternalCoreApi { @@ -69,4 +70,9 @@ public TeleportService getTeleportService() { public WarpService getWarpService() { return this.dependencyProvider.getDependency(WarpService.class); } + + @Override + public UserManager getUserManager() { + return this.dependencyProvider.getDependency(UserManager.class); + } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/IgnoreCommand.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/IgnoreCommand.java index 270df1129..f78a159f0 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/IgnoreCommand.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/IgnoreCommand.java @@ -33,7 +33,10 @@ void ignore(@Context User sender, @Arg User target) { UUID targetUuid = target.getUniqueId(); if (sender.equals(target)) { - this.noticeService.viewer(sender, translation -> translation.ignore().cannotIgnoreSelf()); + this.noticeService.create() + .user(sender) + .notice(translation -> translation.ignore().cannotIgnoreSelf()) + .send(); return; } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/UnIgnoreCommand.java b/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/UnIgnoreCommand.java index cd514aa44..fb7577061 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/UnIgnoreCommand.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/feature/ignore/UnIgnoreCommand.java @@ -33,7 +33,10 @@ void unignore(@Context User sender, @Arg("ignored_player") User target) { UUID targetUuid = target.getUniqueId(); if (sender.equals(target)) { - this.noticeService.viewer(sender, translation -> translation.ignore().cannotUnignoreSelf()); + this.noticeService.create() + .user(sender) + .notice(translation -> translation.ignore().cannotUnignoreSelf()) + .send(); return; } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java deleted file mode 100644 index 73e5dfe49..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/User.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.viewer.Viewer; - -import java.time.Instant; -import java.util.Objects; -import java.util.UUID; - -public class User implements Viewer { - - private final String name; - private final UUID uuid; - private final Instant lastSeen; - private final Instant accountCreated; - - public User(UUID uuid, String name, Instant lastSeen, Instant accountCreated) { - this.name = name; - this.uuid = uuid; - this.lastSeen = lastSeen; - this.accountCreated = accountCreated; - } - - @Override - public String getName() { - return this.name; - } - - @Override - public UUID getUniqueId() { - return this.uuid; - } - - public Instant getLastSeen() { - return this.lastSeen; - } - - public Instant getAccountCreated() { - return this.accountCreated; - } - - public User updateLastSeen(Instant lastSeen) { - return new User(this.uuid, this.name, lastSeen, this.accountCreated); - } - - @Override - public boolean isConsole() { - return false; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof User user)) { - return false; - } - return this.name.equals(user.name) && - this.uuid.equals(user.uuid) && - Objects.equals(this.lastSeen, user.lastSeen) && - Objects.equals(this.accountCreated, user.accountCreated); - } - - @Override - public int hashCode() { - return Objects.hash(this.name, this.uuid, this.lastSeen, this.accountCreated); - } -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserImpl.java new file mode 100644 index 000000000..6cd0d1138 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserImpl.java @@ -0,0 +1,70 @@ +package com.eternalcode.core.user; + +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +public final class UserImpl implements User { + + private final UUID uniqueId; + private final String name; + private final Instant lastSeen; + private final Instant accountCreated; + + public UserImpl(UUID uniqueId, String name, Instant lastSeen, Instant accountCreated) { + this.uniqueId = uniqueId; + this.name = name; + this.lastSeen = lastSeen; + this.accountCreated = accountCreated; + } + + static UserImpl create(UUID uniqueId, String name, Instant lastSeen, Instant accountCreated) { + return new UserImpl(uniqueId, name, lastSeen, accountCreated); + } + + @Override + public UUID getUniqueId() { + return this.uniqueId; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Instant getLastSeen() { + return this.lastSeen; + } + + @Override + public Instant getAccountCreated() { + return this.accountCreated; + } + + UserImpl withName(String name) { + return new UserImpl(this.uniqueId, name, this.lastSeen, this.accountCreated); + } + + UserImpl withLastSeen(Instant lastSeen) { + return new UserImpl(this.uniqueId, this.name, lastSeen, this.accountCreated); + } + + @Override + public boolean equals(Object o) { + if (o == null || this.getClass() != o.getClass()) { + return false; + } + UserImpl user = (UserImpl) o; + + return Objects.equals(this.uniqueId, user.uniqueId) + && Objects.equals(this.name, user.name) + && Objects.equals(this.lastSeen, user.lastSeen) + && Objects.equals(this.accountCreated, user.accountCreated); + } + + @Override + public int hashCode() { + return Objects.hash(this.uniqueId, this.name, this.lastSeen, this.accountCreated); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java deleted file mode 100644 index aa1222578..000000000 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManager.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.eternalcode.core.user; - -import com.eternalcode.core.injector.annotations.Inject; -import com.eternalcode.core.injector.annotations.component.Service; -import com.eternalcode.core.user.database.UserRepository; -import java.time.Instant; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -@Service -public class UserManager { - - private final Map usersByUniqueId = new ConcurrentHashMap<>(); - private final Map usersByName = new ConcurrentHashMap<>(); - private final UserRepository userRepository; - - @Inject - public UserManager(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public Optional getUser(UUID uniqueId) { - return Optional.ofNullable(this.usersByUniqueId.get(uniqueId)); - } - - public Optional getUser(String name) { - return Optional.ofNullable(this.usersByName.get(name)); - } - - public CompletableFuture findOrCreate(UUID uniqueId, String name) { - User cached = this.usersByUniqueId.get(uniqueId); - - if (cached != null) { - User updatedUser = this.updateNameIfChanged(cached, name); - return CompletableFuture.completedFuture(this.updateLastSeen(updatedUser)); - } - - return this.userRepository.getUser(uniqueId) - .thenApply(optionalUser -> { - User user = optionalUser - .map(existing -> { - User updatedUser = this.updateNameIfChanged(existing, name); - return this.updateLastSeen(updatedUser); - }) - .orElseGet(() -> this.createNewUser(uniqueId, name)); - - this.add(user); - return user; - }); - } - - private User createNewUser(UUID uniqueId, String name) { - Instant now = Instant.now(); - User user = new User(uniqueId, name, now, now); - this.userRepository.saveUser(user); - return user; - } - - private User updateNameIfChanged(User user, String name) { - if (user.getName().equals(name)) { - return user; - } - - User updated = new User(user.getUniqueId(), name, user.getLastSeen(), user.getAccountCreated()); - this.removeNameMapping(user); - this.add(updated); - this.userRepository.saveUser(updated); - return updated; - } - - private User updateLastSeen(User user) { - User updated = user.updateLastSeen(Instant.now()); - this.add(updated); - this.userRepository.saveUser(updated); - return updated; - } - - private void removeNameMapping(User user) { - this.usersByName.remove(user.getName(), user); - } - - private void add(User user) { - this.usersByUniqueId.put(user.getUniqueId(), user); - this.usersByName.put(user.getName(), user); - } -} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManagerImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManagerImpl.java new file mode 100644 index 000000000..e551d0c25 --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/UserManagerImpl.java @@ -0,0 +1,116 @@ +package com.eternalcode.core.user; + +import com.eternalcode.core.injector.annotations.Inject; +import com.eternalcode.core.injector.annotations.component.Service; +import com.eternalcode.core.user.database.UserRepository; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +@Service +class UserManagerImpl implements UserManager { + + private final Map users = new ConcurrentHashMap<>(); + private final Map names = new ConcurrentHashMap<>(); + + private final UserRepository userRepository; + + @Inject + UserManagerImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public Optional getUser(UUID uniqueId) { + return Optional.ofNullable(this.users.get(uniqueId)); + } + + @Override + public Optional getUser(String name) { + UUID uniqueId = this.names.get(normalize(name)); + + return uniqueId == null + ? Optional.empty() + : this.getUser(uniqueId); + } + + @Override + public Collection getUsers() { + return List.copyOf(this.users.values()); + } + + @Override + public CompletableFuture findOrCreate(UUID uniqueId, String name) { + User cached = this.users.get(uniqueId); + + if (cached != null) { + return this.refreshName(cached, name); + } + + return this.userRepository.getUser(uniqueId) + .thenCompose(optional -> optional + .map(user -> this.refreshName(user, name)) + .orElseGet(() -> this.create(uniqueId, name))); + } + + private CompletableFuture create(UUID uniqueId, String name) { + Instant now = Instant.now(); + + User user = UserImpl.create( + uniqueId, + name, + now, + now + ); + + return this.saveAndCache(user); + } + + private CompletableFuture refreshName(User user, String name) { + if (user.getName().equals(name)) { + this.cache(user); + return CompletableFuture.completedFuture(user); + } + + User updated = UserImpl.create( + user.getUniqueId(), + name, + user.getLastSeen(), + user.getAccountCreated() + ); + + return this.saveAndCache(updated); + } + + private CompletableFuture saveAndCache(User user) { + return this.userRepository.saveUser(user) + .thenApply(ignored -> { + this.cache(user); + return user; + }); + } + + private synchronized void cache(User user) { + User previous = this.users.put(user.getUniqueId(), user); + + if (previous != null) { + this.names.remove( + normalize(previous.getName()), + previous.getUniqueId() + ); + } + + this.names.put(normalize(user.getName()), user.getUniqueId()); + } + + private static String normalize(String name) { + return name.toLowerCase(Locale.ROOT); + } +} diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java index 847c85631..95cacef9a 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepository.java @@ -1,6 +1,7 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import com.eternalcode.core.user.UserImpl; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java index 00964c3c4..50076ee55 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserRepositoryImpl.java @@ -6,6 +6,7 @@ import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.component.Repository; import com.eternalcode.core.user.User; +import com.eternalcode.core.user.UserImpl; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; import java.util.Optional; diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java index 2610a605e..1996203c8 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/user/database/UserTable.java @@ -1,11 +1,11 @@ package com.eternalcode.core.user.database; import com.eternalcode.core.user.User; +import com.eternalcode.core.user.UserImpl; import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import java.util.Date; -import java.time.Instant; import java.util.UUID; @DatabaseTable(tableName = "eternal_core_users") @@ -33,12 +33,16 @@ class UserTable { this.accountCreated = accountCreated; } - User toUser() { - return new User(this.uniqueId, this.name, this.lastSeen.toInstant(), this.accountCreated.toInstant()); + UserImpl toUser() { + return new UserImpl(this.uniqueId, this.name, this.lastSeen.toInstant(), this.accountCreated.toInstant()); } static UserTable from(User user) { - return new UserTable(user.getUniqueId(), user.getName(), Date.from(user.getLastSeen()), - Date.from(user.getAccountCreated())); + return new UserTable( + user.getUniqueId(), + user.getName(), + Date.from(user.getLastSeen()), + Date.from(user.getAccountCreated()) + ); } } diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/viewer/BukkitViewerProvider.java b/eternalcore-core/src/main/java/com/eternalcode/core/viewer/BukkitViewerProvider.java index ac5198022..d5d38f72f 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/viewer/BukkitViewerProvider.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/viewer/BukkitViewerProvider.java @@ -7,7 +7,6 @@ import com.eternalcode.multification.viewer.ViewerProvider; import java.util.Collection; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.UUID; import org.bukkit.Server; @@ -72,13 +71,13 @@ public Viewer console() { @Override public Viewer player(UUID uuid) { return this.userManager.getUser(uuid) - .map(Viewer.class::cast) + .map(this::user) .orElseGet(() -> BukkitViewerImpl.player(uuid)); } @Override public Viewer user(User user) { - return BukkitViewerImpl.player(user.getUniqueId()); + return new UserViewer(user); } public Viewer sender(CommandSender commandSender) { @@ -88,13 +87,7 @@ public Viewer sender(CommandSender commandSender) { @Override public Viewer any(Object any) { if (any instanceof Player player) { - Optional userOption = this.userManager.getUser(player.getUniqueId()); - - if (userOption.isPresent()) { - return userOption.get(); - } - - return BukkitViewerImpl.player(player.getUniqueId()); + return this.player(player.getUniqueId()); } if (any instanceof ConsoleCommandSender || any instanceof RemoteConsoleCommandSender diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/viewer/UserViewer.java b/eternalcore-core/src/main/java/com/eternalcode/core/viewer/UserViewer.java new file mode 100644 index 000000000..eb7f39def --- /dev/null +++ b/eternalcore-core/src/main/java/com/eternalcode/core/viewer/UserViewer.java @@ -0,0 +1,22 @@ +package com.eternalcode.core.viewer; + +import com.eternalcode.core.user.User; +import java.util.UUID; + +record UserViewer(User user) implements Viewer { + + @Override + public UUID getUniqueId() { + return this.user.getUniqueId(); + } + + @Override + public boolean isConsole() { + return false; + } + + @Override + public String getName() { + return this.user.getName(); + } +}