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:
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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, "");
|
|
||||||
|
|
||||||
client.send("welcome", {
|
// Store UUID mapping if provided
|
||||||
sessionId: client.sessionId,
|
if (options.uuid) {
|
||||||
color: this.state.players.get(client.sessionId)?.color || "#667eea"
|
this.sessionToUuid.set(client.sessionId, options.uuid);
|
||||||
});
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user