Feat: accept optional client username and forward to ClientApp #271

Merged
jona.walpert merged 15 commits from feat/change-username-at-start-or-in-game into main 2026-04-12 11:17:39 +02:00
3 changed files with 90 additions and 23 deletions
Showing only changes of commit c094b224c3 - Show all commits
@@ -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<Message> 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<Instant> 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();
@@ -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();
@@ -12,10 +12,11 @@ public class UserRegistry {
private final ConcurrentHashMap<SessionId, User> 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<User> 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<User> 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<User> getByUsername(String username) {
return Optional.ofNullable(byName.get(username));
@@ -185,4 +193,31 @@ public class UserRegistry {
public Collection<User> 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;
}
}