From 83b7e1f9ce1e7523377682e01ef181a979f3681f Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sat, 28 Feb 2026 20:13:39 -0500 Subject: [PATCH] Include self in transfer targets and exclude item owner --- client/public/version.js | 2 +- client/src/main.ts | 35 +++++++++++++++++++-------- client/src/network/messageHandlers.ts | 12 +++++---- client/src/network/protocol.ts | 2 ++ client/src/state/gameState.ts | 1 + server/app/models.py | 1 + server/app/server.py | 4 +-- 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/client/public/version.js b/client/public/version.js index 6c68ddc..d3113d1 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.03.01 R320"; +window.CHGRID_WEB_VERSION = "2026.03.01 R321"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 4a2a50b..de2b274 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1060,12 +1060,18 @@ function itemManagementOptionsFor(item: WorldItem): ItemManagementOption[] { if (canManageDeleteItem(item)) { options.push({ action: 'delete', label: 'Delete item' }); } - if (canManageTransferItem(item) && state.peers.size > 0) { + if (canManageTransferItem(item) && (state.player.id !== null || state.peers.size > 0)) { options.push({ action: 'transfer', label: 'Transfer item' }); } return options; } +/** Resolves transfer-target label for either local player id or a remote peer id. */ +function transferTargetLabel(userPeerId: string): string { + if (userPeerId === state.player.id) return state.player.nickname || 'Unknown user'; + return state.peers.get(userPeerId)?.nickname ?? 'Unknown user'; +} + /** Opens item-management options for one selected item. */ function beginItemManagement(item: WorldItem): void { const options = itemManagementOptionsFor(item); @@ -2894,12 +2900,21 @@ function handleItemManageOptionsModeInput(code: string, key: string): void { }); return; } - const targetIds = Array.from(state.peers.values()) - .map((peer) => peer.id) - .filter((peerId) => peerId !== state.player.id && state.peers.has(peerId)) + const ownerUserId = item.createdBy.trim(); + const targetIds = [ + ...(state.player.id ? [state.player.id] : []), + ...Array.from(state.peers.values()).map((peer) => peer.id), + ] + .filter((peerId, index, arr) => arr.indexOf(peerId) === index) + .filter((peerId) => { + if (!ownerUserId) return true; + if (peerId === state.player.id) return authUserId !== ownerUserId; + const peer = state.peers.get(peerId); + return (peer?.userId ?? '') !== ownerUserId; + }) .sort((a, b) => { - const left = state.peers.get(a)?.nickname ?? ''; - const right = state.peers.get(b)?.nickname ?? ''; + const left = transferTargetLabel(a); + const right = transferTargetLabel(b); return left.localeCompare(right, undefined, { sensitivity: 'base' }); }); if (targetIds.length === 0) { @@ -2910,7 +2925,7 @@ function handleItemManageOptionsModeInput(code: string, key: string): void { itemManagementTargetUserIds = targetIds; itemManagementTargetUserIndex = 0; state.mode = 'itemManageTransferUser'; - const firstLabel = state.peers.get(itemManagementTargetUserIds[0])?.nickname ?? 'Unknown user'; + const firstLabel = transferTargetLabel(itemManagementTargetUserIds[0]); updateStatus(firstLabel); audio.sfxUiBlip(); return; @@ -2930,11 +2945,11 @@ function handleItemManageTransferUserModeInput(code: string, key: string): void return; } const control = handleListControlKey(code, key, itemManagementTargetUserIds, itemManagementTargetUserIndex, (userId) => { - return state.peers.get(userId)?.nickname ?? 'Unknown user'; + return transferTargetLabel(userId); }); if (control.type === 'move') { itemManagementTargetUserIndex = control.index; - const label = state.peers.get(itemManagementTargetUserIds[itemManagementTargetUserIndex])?.nickname ?? 'Unknown user'; + const label = transferTargetLabel(itemManagementTargetUserIds[itemManagementTargetUserIndex]); updateStatus(label); audio.sfxUiBlip(); return; @@ -2947,7 +2962,7 @@ function handleItemManageTransferUserModeInput(code: string, key: string): void audio.sfxUiCancel(); return; } - const targetLabel = state.peers.get(targetId)?.nickname ?? 'Unknown user'; + const targetLabel = transferTargetLabel(targetId); openItemManagementConfirm({ itemId: item.id, action: 'transfer', diff --git a/client/src/network/messageHandlers.ts b/client/src/network/messageHandlers.ts index 062f779..ac96247 100644 --- a/client/src/network/messageHandlers.ts +++ b/client/src/network/messageHandlers.ts @@ -154,11 +154,12 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco if (!deps.isPeerNegotiationReady()) { deps.enqueuePendingSignal(message); if (!deps.state.peers.has(message.senderId)) { - deps.state.peers.set(message.senderId, { - id: message.senderId, - nickname: deps.sanitizeName(message.senderNickname || 'user...') || 'user...', - x: Number.isFinite(message.x) ? message.x : 20, - y: Number.isFinite(message.y) ? message.y : 20, + deps.state.peers.set(message.senderId, { + id: message.senderId, + userId: null, + nickname: deps.sanitizeName(message.senderNickname || 'user...') || 'user...', + x: Number.isFinite(message.x) ? message.x : 20, + y: Number.isFinite(message.y) ? message.y : 20, }); } break; @@ -167,6 +168,7 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco if (!deps.state.peers.has(peer.id)) { deps.state.peers.set(peer.id, { id: peer.id, + userId: null, nickname: deps.sanitizeName(peer.nickname) || 'user...', x: peer.x, y: peer.y, diff --git a/client/src/network/protocol.ts b/client/src/network/protocol.ts index 52573c1..6108e0d 100644 --- a/client/src/network/protocol.ts +++ b/client/src/network/protocol.ts @@ -24,6 +24,7 @@ export const welcomeMessageSchema = z.object({ id: z.string(), player: z.object({ id: z.string(), + userId: z.string().nullable().optional(), nickname: z.string(), x: z.number().int(), y: z.number().int(), @@ -31,6 +32,7 @@ export const welcomeMessageSchema = z.object({ users: z.array( z.object({ id: z.string(), + userId: z.string().nullable().optional(), nickname: z.string(), x: z.number().int(), y: z.number().int(), diff --git a/client/src/state/gameState.ts b/client/src/state/gameState.ts index 9d8474b..c7cd895 100644 --- a/client/src/state/gameState.ts +++ b/client/src/state/gameState.ts @@ -62,6 +62,7 @@ export type Player = { export type PeerState = { id: string; + userId?: string | null; nickname: string; x: number; y: number; diff --git a/server/app/models.py b/server/app/models.py index 86e6c1b..ed38e65 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -209,6 +209,7 @@ ClientPacket = ( class RemoteUser(BaseModel): id: str + userId: str | None = None nickname: str x: int y: int diff --git a/server/app/server.py b/server/app/server.py index ab1adf7..4da06a3 100644 --- a/server/app/server.py +++ b/server/app/server.py @@ -1389,14 +1389,14 @@ class SignalingServer: """Send initial world snapshot to a newly connected client.""" users = [ - RemoteUser(id=other.id, nickname=other.nickname, x=other.x, y=other.y) + RemoteUser(id=other.id, userId=other.user_id, nickname=other.nickname, x=other.x, y=other.y) for ws, other in self.clients.items() if ws is not client.websocket ] packet = WelcomePacket( type="welcome", id=client.id, - player=RemoteUser(id=client.id, nickname=client.nickname, x=client.x, y=client.y), + player=RemoteUser(id=client.id, userId=client.user_id, nickname=client.nickname, x=client.x, y=client.y), users=users, items=[self._outbound_item(item).model_dump(exclude_none=True) for item in self.items.values()], worldConfig={