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. */ /** Represents an authenticated user on the server. */
public class User { public class User {
private final UserId id; private final UserId id;
private final String name; private String name;
private SessionId sessionId; private SessionId sessionId;
private Instant disconnectedAt; private Instant disconnectedAt;
private final Queue<Message> messages; private final Queue<Message> messages;
@@ -46,10 +46,21 @@ public class User {
* *
* @return the user name * @return the user name
*/ */
public String getName() { public synchronized String getName() {
return name; 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. * 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. * 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() { public Optional<Instant> getDisconnectedAt() {
return Optional.ofNullable(disconnectedAt); return Optional.ofNullable(disconnectedAt);
@@ -78,7 +90,10 @@ public class User {
this.disconnectedAt = null; 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() { public void markDisconnected() {
this.sessionId = null; this.sessionId = null;
this.disconnectedAt = Instant.now(); this.disconnectedAt = Instant.now();
@@ -1,10 +1,12 @@
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user; package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId; import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
import java.util.concurrent.atomic.AtomicInteger;
/** Creates new users, resolving name conflicts automatically. */ /** Creates new users, resolving name conflicts automatically. */
public class UserFactory { public class UserFactory {
private final UserRegistry registry; private final UserRegistry registry;
private final AtomicInteger anonymousCounter = new AtomicInteger(1);
/** /**
* Creates a new UserFactory backed by the given registry. * 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 * Creates and registers a new user with the given name and session. If the name
* taken, a numeric suffix is appended and incremented until a free name is found (e.g. * is already
* Lars_001, Lars_002, ...). * 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 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 * @return the newly created and registered user
*/ */
public User create(String desiredName, SessionId sessionId) { 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); var result = registry.registerIfAvailable(desiredName, sessionId);
if (result.isPresent()) { if (result.isPresent()) {
return result.get(); return result.get();
@@ -12,7 +12,8 @@ public class UserRegistry {
private final ConcurrentHashMap<SessionId, User> bySessionId = new ConcurrentHashMap<>(); 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. * empty if the name is already taken.
* *
* @param name the desired display name * @param name the desired display name
@@ -92,8 +93,10 @@ public class UserRegistry {
} }
/** /**
* Removes the user with the given ID, but only if they are still disconnected. This prevents * Removes the user with the given ID, but only if they are still disconnected.
* removing a user who has reconnected between the cleanup job's check and its removal call. * 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 * @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. * association and recording the disconnect timestamp.
* *
* @param sessionId the session ID of the disconnected client * @param sessionId the session ID of the disconnected client
@@ -127,7 +131,8 @@ 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 * @param sessionId the new session ID
@@ -148,7 +153,8 @@ public class UserRegistry {
* Looks up a user by their {@link SessionId}. * Looks up a user by their {@link SessionId}.
* *
* @param sessionId the SessionId to look up * @param sessionId the SessionId to look up
* @return an Optional containing the {@link User}, or empty if no user is associated with this * @return an Optional containing the {@link User}, or empty if no user is
* associated with this
* SessionId * SessionId
*/ */
public Optional<User> getBySessionId(SessionId sessionId) { public Optional<User> getBySessionId(SessionId sessionId) {
@@ -159,7 +165,8 @@ public class UserRegistry {
* Looks up a user by their {@link UserId}. * Looks up a user by their {@link UserId}.
* *
* @param userId the UserId to look up * @param userId the UserId to look up
* @return an Optional containing the {@link User}, or empty if no user is associated with this * @return an Optional containing the {@link User}, or empty if no user is
* associated with this
* UserId * UserId
*/ */
public Optional<User> getByUserId(UserId userId) { public Optional<User> getByUserId(UserId userId) {
@@ -170,7 +177,8 @@ public class UserRegistry {
* Looks up a user by their username. * Looks up a user by their username.
* *
* @param username the username to look up * @param username the username to look up
* @return an Optional containing the {@link User}, or empty if no user is associated with this * @return an Optional containing the {@link User}, or empty if no user is
* associated with this
* username * username
*/ */
public Optional<User> getByUsername(String username) { public Optional<User> getByUsername(String username) {
@@ -185,4 +193,31 @@ public class UserRegistry {
public Collection<User> getAllUsers() { public Collection<User> getAllUsers() {
return byId.values(); 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;
}
} }