Implementar sistema UUID para nombres únicos globales

- Agregado UUID persistente en base de datos local (LokiJS)
- Nombres únicos por UUID en lugar de sessionId
- Nombres persisten entre reconexiones mientras el servidor esté activo
- Migración automática de perfiles existentes
- Registrados handlers para evitar warnings de mensajes no encontrados
This commit is contained in:
2025-08-12 17:35:57 -06:00
parent 75b114d66d
commit b754ec043a
5 changed files with 122 additions and 55 deletions

View File

@@ -49,7 +49,11 @@ class ColyseusService {
async joinLobby(): Promise<Room> { async joinLobby(): Promise<Room> {
try { try {
const room = await this.client.joinOrCreate("lobby"); // Initialize DB first to get UUID
await localDB.init();
const uuid = localDB.getUUID();
const room = await this.client.joinOrCreate("lobby", { uuid });
this.lobbyRoom.value = room; this.lobbyRoom.value = room;
this.currentRoom = room; this.currentRoom = room;
// Require explicit confirmation each time we join the lobby (auto-confirm if saved name exists) // Require explicit confirmation each time we join the lobby (auto-confirm if saved name exists)
@@ -58,22 +62,29 @@ class ColyseusService {
room.onMessage("welcome", async (data) => { room.onMessage("welcome", async (data) => {
this.sessionId.value = data.sessionId; this.sessionId.value = data.sessionId;
if (data.color) this.playerColor.value = data.color; if (data.color) this.playerColor.value = data.color;
// Initialize local DB and optionally auto-apply saved profile
try { // If server already has a name for us, use it
await localDB.init(); if (data.name) {
const profile = localDB.getLocalPlayer(); this.playerName.value = data.name;
// Apply saved color silently this.nameConfirmed.value = true;
if (profile?.color && profile.color !== this.playerColor.value) { } else {
this.setPlayerColor(profile.color); // Initialize local DB and optionally auto-apply saved profile
try {
await localDB.init();
const profile = localDB.getLocalPlayer();
// Apply saved color silently
if (profile?.color && profile.color !== this.playerColor.value) {
this.setPlayerColor(profile.color);
}
if (profile?.name) {
this.playerName.value = profile.name;
try { localDB.setName(profile.name); } catch {}
this.setPlayerName(profile.name);
this.nameConfirmed.value = true;
}
} catch (e) {
console.warn("Local DB init failed", e);
} }
if (profile?.name) {
this.playerName.value = profile.name;
try { localDB.setName(profile.name); } catch {}
this.setPlayerName(profile.name);
this.nameConfirmed.value = true;
}
} catch (e) {
console.warn("Local DB init failed", e);
} }
}); });
@@ -119,7 +130,8 @@ class ColyseusService {
async setPlayerName(name: string): Promise<void> { async setPlayerName(name: string): Promise<void> {
if (this.lobbyRoom.value) { if (this.lobbyRoom.value) {
this.lobbyRoom.value.send("setName", name); const uuid = localDB.getUUID();
this.lobbyRoom.value.send("setName", { name, uuid });
} }
} }

View File

@@ -2,6 +2,7 @@ import Loki from "lokijs";
export interface LocalPlayerDoc { export interface LocalPlayerDoc {
id: string; // fixed id for local profile id: string; // fixed id for local profile
uuid: string; // persistent unique identifier
name: string; name: string;
color: string; color: string;
stats: { stats: {
@@ -29,13 +30,19 @@ class LocalDBService {
this.players = this.db!.addCollection<LocalPlayerDoc>("players", { unique: ["id"] }); this.players = this.db!.addCollection<LocalPlayerDoc>("players", { unique: ["id"] });
} }
// Ensure local profile exists // Ensure local profile exists
if (!this.players.by("id", "local")) { let localPlayer = this.players.by("id", "local");
if (!localPlayer) {
this.players.insert({ this.players.insert({
id: "local", id: "local",
uuid: this.generateUUID(),
name: "", name: "",
color: "#667eea", color: "#667eea",
stats: { totalClicks: 0, gamesPlayed: 0, wins: 0, losses: 0 } stats: { totalClicks: 0, gamesPlayed: 0, wins: 0, losses: 0 }
}); });
} else if (!localPlayer.uuid) {
// Migrate existing profiles to have a UUID
localPlayer.uuid = this.generateUUID();
this.players.update(localPlayer);
} }
this.initialized = true; this.initialized = true;
this.db!.saveDatabase(() => resolve()); this.db!.saveDatabase(() => resolve());
@@ -85,6 +92,25 @@ class LocalDBService {
this.col.update(doc); this.col.update(doc);
this.db?.saveDatabase(); this.db?.saveDatabase();
} }
getUUID(): string {
const doc = this.getLocalPlayer();
if (!doc.uuid) {
doc.uuid = this.generateUUID();
this.col.update(doc);
this.db?.saveDatabase();
}
return doc.uuid;
}
private generateUUID(): string {
// Simple UUID v4 generator
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
} }
export const localDB = new LocalDBService(); export const localDB = new LocalDBService();

View File

@@ -194,6 +194,15 @@ onMounted(() => {
room.onMessage("gameEnd", () => { room.onMessage("gameEnd", () => {
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.rtoken'); } } catch {} try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.rtoken'); } } catch {}
}); });
// Register additional message handlers to avoid warnings
room.onMessage("gamePaused", () => {
// Game paused, could update UI state if needed
});
room.onMessage("variantChanged", (data: { variant: string }) => {
currentVariant.value = data.variant as any;
});
} }
}); });

View File

@@ -4,13 +4,14 @@ import { NameManager } from "../utils/nameManager";
export class LobbyRoom extends Room<LobbyState> { export class LobbyRoom extends Room<LobbyState> {
private updateInterval?: NodeJS.Timeout; private updateInterval?: NodeJS.Timeout;
private sessionToUuid: Map<string, string> = new Map();
onCreate(options: any) { onCreate(options: any) {
this.setState(new LobbyState()); this.setState(new LobbyState());
this.setPrivate(false); this.setPrivate(false);
this.onMessage("setName", (client, playerName: string) => { this.onMessage("setName", (client, data: { name: string; uuid: string }) => {
this.handleSetName(client, playerName); this.handleSetName(client, data);
}); });
this.onMessage("setColor", (client, color: string) => { this.onMessage("setColor", (client, color: string) => {
@@ -31,14 +32,29 @@ export class LobbyRoom extends Room<LobbyState> {
} }
onJoin(client: Client, options: any) { onJoin(client: Client, options: any) {
console.log(`[LobbyRoom] ${client.sessionId} joined lobby`); console.log(`[LobbyRoom] ${client.sessionId} joined lobby with UUID: ${options.uuid}`);
// Do NOT assign a default name on join. Wait until client presses "Set Name".
this.state.addPlayer(client.sessionId, ""); // Store UUID mapping if provided
if (options.uuid) {
client.send("welcome", { this.sessionToUuid.set(client.sessionId, options.uuid);
sessionId: client.sessionId,
color: this.state.players.get(client.sessionId)?.color || "#667eea" // Check if this UUID already has a name
}); const existingName = NameManager.getInstance().getPlayerName(options.uuid);
this.state.addPlayer(client.sessionId, existingName || "");
client.send("welcome", {
sessionId: client.sessionId,
name: existingName || "",
color: this.state.players.get(client.sessionId)?.color || "#667eea"
});
} else {
// Fallback for clients without UUID (shouldn't happen in normal flow)
this.state.addPlayer(client.sessionId, "");
client.send("welcome", {
sessionId: client.sessionId,
color: this.state.players.get(client.sessionId)?.color || "#667eea"
});
}
this.updateAvailableRooms(); this.updateAvailableRooms();
} }
@@ -46,10 +62,8 @@ export class LobbyRoom extends Room<LobbyState> {
onLeave(client: Client, consented: boolean) { onLeave(client: Client, consented: boolean) {
console.log(`[LobbyRoom] ${client.sessionId} left lobby`); console.log(`[LobbyRoom] ${client.sessionId} left lobby`);
const player = this.state.players.get(client.sessionId); // Clean up UUID mapping
if (player) { this.sessionToUuid.delete(client.sessionId);
NameManager.getInstance().releasePlayerName(client.sessionId);
}
this.state.removePlayer(client.sessionId); this.state.removePlayer(client.sessionId);
} }
@@ -61,18 +75,21 @@ export class LobbyRoom extends Room<LobbyState> {
clearInterval(this.updateInterval); clearInterval(this.updateInterval);
} }
this.state.players.forEach(player => { // Clear UUID mappings
NameManager.getInstance().releasePlayerName(player.sessionId); this.sessionToUuid.clear();
});
} }
private handleSetName(client: Client, playerName: string) { private handleSetName(client: Client, data: { name: string; uuid: string }) {
const currentPlayer = this.state.players.get(client.sessionId); const currentPlayer = this.state.players.get(client.sessionId);
if (!currentPlayer) return; if (!currentPlayer) return;
NameManager.getInstance().releasePlayerName(client.sessionId); // Update UUID mapping if provided
if (data.uuid) {
this.sessionToUuid.set(client.sessionId, data.uuid);
}
const uniqueName = NameManager.getInstance().generateUniquePlayerName(playerName, client.sessionId); const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId;
const uniqueName = NameManager.getInstance().generateUniquePlayerName(data.name, uuid);
currentPlayer.name = uniqueName; currentPlayer.name = uniqueName;
@@ -189,4 +206,4 @@ export class LobbyRoom extends Room<LobbyState> {
console.error("[LobbyRoom] Error updating available rooms:", error); console.error("[LobbyRoom] Error updating available rooms:", error);
} }
} }
} }

View File

@@ -1,7 +1,6 @@
export class NameManager { export class NameManager {
private static instance: NameManager; private static instance: NameManager;
private nameCounters: Map<string, number> = new Map(); private uuidToName: Map<string, string> = new Map();
private sessionToName: Map<string, string> = new Map();
private constructor() {} private constructor() {}
@@ -12,16 +11,22 @@ export class NameManager {
return NameManager.instance; return NameManager.instance;
} }
generateUniquePlayerName(baseName: string, sessionId: string): string { generateUniquePlayerName(baseName: string, uuid: string): string {
// If UUID already has a name, return it
const existingName = this.uuidToName.get(uuid);
if (existingName) {
return existingName;
}
const normalizedName = baseName.trim().toLowerCase(); const normalizedName = baseName.trim().toLowerCase();
if (!normalizedName) { if (!normalizedName) {
// Default base name when none is provided // Default base name when none is provided
return this.generateUniquePlayerName('guest', sessionId); return this.generateUniquePlayerName('guest', uuid);
} }
// Try exact name if not in use; otherwise, append incremental suffixes // Try exact name if not in use; otherwise, append incremental suffixes
const isInUse = (name: string) => { const isInUse = (name: string) => {
for (const val of this.sessionToName.values()) { for (const val of this.uuidToName.values()) {
if (val === name) return true; if (val === name) return true;
} }
return false; return false;
@@ -34,22 +39,20 @@ export class NameManager {
uniqueName = `${normalizedName}-${n}`; uniqueName = `${normalizedName}-${n}`;
} }
this.sessionToName.set(sessionId, uniqueName); this.uuidToName.set(uuid, uniqueName);
return uniqueName; return uniqueName;
} }
releasePlayerName(sessionId: string): void { releasePlayerName(uuid: string): void {
const name = this.sessionToName.get(sessionId); // Names are now persistent per UUID, so we don't release them
if (name) { // They only get cleared when the server restarts
this.sessionToName.delete(sessionId);
}
} }
getPlayerName(sessionId: string): string | undefined { getPlayerName(uuid: string): string | undefined {
return this.sessionToName.get(sessionId); return this.uuidToName.get(uuid);
} }
getAllActivePlayers(): string[] { getAllActivePlayers(): string[] {
return Array.from(this.sessionToName.values()); return Array.from(this.uuidToName.values());
} }
} }