From c094b224c3c2a8d6cf9be0a384dfbfa67ed8bafb Mon Sep 17 00:00:00 2001 From: Jona Walpert Date: Fri, 10 Apr 2026 14:20:21 +0200 Subject: [PATCH] Feat: Auto-assign playerN and add server-side rename support Make username mutable, assign playerN for anonymous logins, and add atomic changeUsername in UserRegistry. Refs #68 --- .../cs108/casono/server/domain/user/User.java | 27 ++++++-- .../server/domain/user/UserFactory.java | 25 ++++++-- .../server/domain/user/UserRegistry.java | 61 +++++++++++++++---- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java index dc42ac3..a40e16f 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/User.java @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; /** Represents an authenticated user on the server. */ public class User { private final UserId id; - private final String name; + private String name; private SessionId sessionId; private Instant disconnectedAt; private final Queue messages; @@ -20,8 +20,8 @@ public class User { /** * Creates a new User with the given ID, name and session. * - * @param id the unique identifier for this user - * @param name the display name of this user + * @param id the unique identifier for this user + * @param name the display name of this user * @param sessionId the session currently associated with this user */ public User(UserId id, String name, SessionId sessionId) { @@ -46,10 +46,21 @@ public class User { * * @return the user name */ - public String getName() { + public synchronized String getName() { return name; } + /** + * Sets a new display name for this user. Thread-safe; callers must ensure the + * registry is + * updated to maintain uniqueness when needed. + * + * @param newName the new display name + */ + public synchronized void setName(String newName) { + this.name = newName; + } + /** * Returns the session currently associated with this user, if any. * @@ -62,7 +73,8 @@ public class User { /** * Returns the time at which this user disconnected, if applicable. * - * @return an Optional containing the disconnect timestamp, or empty if connected + * @return an Optional containing the disconnect timestamp, or empty if + * connected */ public Optional getDisconnectedAt() { return Optional.ofNullable(disconnectedAt); @@ -78,7 +90,10 @@ public class User { this.disconnectedAt = null; } - /** Marks this user as disconnected by clearing the session and recording the timestamp. */ + /** + * Marks this user as disconnected by clearing the session and recording the + * timestamp. + */ public void markDisconnected() { this.sessionId = null; this.disconnectedAt = Instant.now(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java index 84ab1ce..559c833 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserFactory.java @@ -1,10 +1,12 @@ package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; +import java.util.concurrent.atomic.AtomicInteger; /** Creates new users, resolving name conflicts automatically. */ public class UserFactory { private final UserRegistry registry; + private final AtomicInteger anonymousCounter = new AtomicInteger(1); /** * Creates a new UserFactory backed by the given registry. @@ -16,15 +18,30 @@ public class UserFactory { } /** - * Creates and registers a new user with the given name and session. If the name is already - * taken, a numeric suffix is appended and incremented until a free name is found (e.g. - * Lars_001, Lars_002, ...). + * Creates and registers a new user with the given name and session. If the name + * is already + * taken, a numeric suffix is appended and incremented until a free name is + * found (e.g. + * Lars_001, Lars_002, ...). If the desiredName is null or empty, an automatic + * name of the + * form "playerN" is assigned, incrementing N until a free name is found. * * @param desiredName the preferred display name - * @param sessionId the session to associate with the new user + * @param sessionId the session to associate with the new user * @return the newly created and registered user */ public User create(String desiredName, SessionId sessionId) { + if (desiredName == null || desiredName.isBlank()) { + // assign playerN names for anonymous logins + while (true) { + String candidate = "player" + anonymousCounter.getAndIncrement(); + var result = registry.registerIfAvailable(candidate, sessionId); + if (result.isPresent()) { + return result.get(); + } + } + } + var result = registry.registerIfAvailable(desiredName, sessionId); if (result.isPresent()) { return result.get(); diff --git a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java index b3cd551..3a91742 100644 --- a/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java +++ b/src/main/java/ch/unibas/dmi/dbis/cs108/casono/server/domain/user/UserRegistry.java @@ -12,10 +12,11 @@ public class UserRegistry { private final ConcurrentHashMap bySessionId = new ConcurrentHashMap<>(); /** - * Attempts to register a user under the given name atomically. Returns the registered user, or + * Attempts to register a user under the given name atomically. Returns the + * registered user, or * empty if the name is already taken. * - * @param name the desired display name + * @param name the desired display name * @param sessionId the session to associate with the new user * @return an Optional containing the new user, or empty if the name was taken */ @@ -92,8 +93,10 @@ public class UserRegistry { } /** - * Removes the user with the given ID, but only if they are still disconnected. This prevents - * removing a user who has reconnected between the cleanup job's check and its removal call. + * Removes the user with the given ID, but only if they are still disconnected. + * This prevents + * removing a user who has reconnected between the cleanup job's check and its + * removal call. * * @param userId the ID of the user to remove */ @@ -112,7 +115,8 @@ public class UserRegistry { } /** - * Marks the user associated with the given session as disconnected, clearing the session + * Marks the user associated with the given session as disconnected, clearing + * the session * association and recording the disconnect timestamp. * * @param sessionId the session ID of the disconnected client @@ -127,9 +131,10 @@ public class UserRegistry { } /** - * Reassociates a user with a new session, effectively restoring them after a reconnect. + * Reassociates a user with a new session, effectively restoring them after a + * reconnect. * - * @param userId the ID of the user to reconnect + * @param userId the ID of the user to reconnect * @param sessionId the new session ID * @return an Optional containing the user, or empty if the user was not found */ @@ -148,8 +153,9 @@ public class UserRegistry { * Looks up a user by their {@link SessionId}. * * @param sessionId the SessionId to look up - * @return an Optional containing the {@link User}, or empty if no user is associated with this - * SessionId + * @return an Optional containing the {@link User}, or empty if no user is + * associated with this + * SessionId */ public Optional getBySessionId(SessionId sessionId) { return Optional.ofNullable(bySessionId.get(sessionId)); @@ -159,8 +165,9 @@ public class UserRegistry { * Looks up a user by their {@link UserId}. * * @param userId the UserId to look up - * @return an Optional containing the {@link User}, or empty if no user is associated with this - * UserId + * @return an Optional containing the {@link User}, or empty if no user is + * associated with this + * UserId */ public Optional getByUserId(UserId userId) { return Optional.ofNullable(byId.get(userId)); @@ -170,8 +177,9 @@ public class UserRegistry { * Looks up a user by their username. * * @param username the username to look up - * @return an Optional containing the {@link User}, or empty if no user is associated with this - * username + * @return an Optional containing the {@link User}, or empty if no user is + * associated with this + * username */ public Optional getByUsername(String username) { return Optional.ofNullable(byName.get(username)); @@ -185,4 +193,31 @@ public class UserRegistry { public Collection getAllUsers() { return byId.values(); } + + /** + * Attempts to change the username of an existing user. Ensures the new name is + * not already + * taken and updates internal indices atomically. + * + * @param userId the id of the user to rename + * @param newName the desired new name + * @return true if the rename succeeded, false if the name was already taken or + * user not found + */ + public synchronized boolean changeUsername(UserId userId, String newName) { + User user = byId.get(userId); + if (user == null) { + return false; + } + + if (byName.containsKey(newName)) { + return false; + } + + // remove old mapping and put new mapping + byName.remove(user.getName()); + user.setName(newName); + byName.put(newName, user); + return true; + } }