where('join_code', $joinCode) ->first(); } public function findExistingPlayer( Room $room, ?User $user, mixed $sessionPlayerId, ): ?Player { if ($user !== null) { return $room->players() ->where('user_id', $user->id) ->first(); } if (! is_numeric($sessionPlayerId)) { return null; } return $room->players()->find((int) $sessionPlayerId); } /** * @param array{join_code: string, player_name: string} $data * @param array $sessionPlayerIds * @return array{Room, Player} */ public function join(array $data, ?User $user, array $sessionPlayerIds): array { [$room, $player] = DB::transaction(function () use ($data, $user, $sessionPlayerIds): array { $room = Room::query() ->where('join_code', $data['join_code']) ->lockForUpdate() ->firstOrFail(); if ($room->isClosed()) { throw ValidationException::withMessages([ 'join_code' => 'Dieser Raum ist geschlossen.', ]); } $player = $this->findExistingPlayer( $room, $user, $sessionPlayerIds[$room->id] ?? null, ); $shouldModerate = $this->shouldModerate($room, $user); if ($player === null) { $player = $room->players()->create([ 'user_id' => $user?->id, 'name' => $data['player_name'], 'is_moderator' => $shouldModerate, 'last_seen_at' => now(), ]); } else { $player->update([ 'last_seen_at' => now(), 'is_moderator' => $player->is_moderator || $shouldModerate, ]); } $room->update([ 'last_activity_at' => now(), 'state_version' => $room->state_version + 1, ]); return [$room, $player]; }); RoomStateChanged::dispatch( $room->id, $room->state_version, RoomStateChange::PlayerJoined, $this->realtimeState->for($room), ); return [$room, $player]; } public function joinEmbedded( Room $room, ?User $user, string $playerName, mixed $sessionPlayerId, ): Player { [$player, $change, $stateVersion] = DB::transaction(function () use ($room, $user, $playerName, $sessionPlayerId): array { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); if ($lockedRoom->isClosed()) { throw ValidationException::withMessages([ 'join_code' => 'Dieser Raum ist geschlossen.', ]); } $player = $this->findExistingEmbeddedPlayer( $lockedRoom, $user, $sessionPlayerId, ); $shouldModerate = $this->shouldModerate($lockedRoom, $user); $nameIsTaken = $lockedRoom->players() ->where('name', $playerName) ->when( $player !== null, fn ($query) => $query->whereKeyNot($player->id), ) ->exists(); if ($nameIsTaken) { throw ValidationException::withMessages([ 'player_name' => 'Dieser Anzeigename ist in dem Raum bereits vorhanden.', ]); } $change = null; if ($player === null) { $player = $lockedRoom->players()->create([ 'user_id' => $user?->id, 'name' => $playerName, 'is_moderator' => $shouldModerate, 'last_seen_at' => now(), ]); $change = RoomStateChange::PlayerJoined; } else { if ($player->name !== $playerName || ($shouldModerate && ! $player->is_moderator)) { $change = RoomStateChange::PlayerUpdated; } $updates = [ 'name' => $playerName, 'last_seen_at' => now(), 'is_moderator' => $player->is_moderator || $shouldModerate, ]; if ( $user !== null && $player->user_id === null && ! $lockedRoom->players() ->where('user_id', $user->id) ->whereKeyNot($player->id) ->exists() ) { $updates['user_id'] = $user->id; } $player->update($updates); } if ($change === null) { return [$player, null, null]; } $lockedRoom->update([ 'last_activity_at' => now(), 'state_version' => $lockedRoom->state_version + 1, ]); return [$player, $change, $lockedRoom->state_version]; }); if ($change instanceof RoomStateChange && is_int($stateVersion)) { RoomStateChanged::dispatch( $room->id, $stateVersion, $change, $this->realtimeState->for($room), ); } return $player; } private function shouldModerate(Room $room, ?User $user): bool { if ($user !== null && $room->owner_id === $user->id) { return true; } return $room->owner_id === null && ! $room->players()->where('is_moderator', true)->exists(); } private function findExistingEmbeddedPlayer( Room $room, ?User $user, mixed $sessionPlayerId, ): ?Player { if ($user !== null) { $userPlayer = $room->players() ->where('user_id', $user->id) ->lockForUpdate() ->first(); if ($userPlayer !== null) { return $userPlayer; } } if (is_numeric($sessionPlayerId)) { $sessionPlayer = $room->players() ->whereKey((int) $sessionPlayerId) ->lockForUpdate() ->first(); if ( $sessionPlayer !== null && ($user === null || $sessionPlayer->user_id === null || $sessionPlayer->user_id === $user->id) ) { return $sessionPlayer; } } return null; } public function activateInstance(Room $room, Player $player, string $instanceId): Player { [$activePlayer, $replacedInstanceId, $stateVersion] = DB::transaction(function () use ($room, $player, $instanceId): array { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); $lockedPlayer = $lockedRoom->players() ->lockForUpdate() ->findOrFail($player->id); if ($lockedPlayer->active_instance_id === $instanceId) { return [$lockedPlayer, null, null]; } $replacedInstanceId = $lockedPlayer->active_instance_id; $lockedPlayer->update([ 'active_instance_id' => $instanceId, 'last_seen_at' => now(), ]); if ($replacedInstanceId === null) { return [$lockedPlayer, null, null]; } $lockedRoom->update([ 'last_activity_at' => now(), 'state_version' => $lockedRoom->state_version + 1, ]); return [$lockedPlayer, $replacedInstanceId, $lockedRoom->state_version]; }); if (is_string($replacedInstanceId) && is_int($stateVersion)) { RoomStateChanged::dispatch( $room->id, $stateVersion, RoomStateChange::PlayerInstanceReplaced, $this->realtimeState->for($room), $activePlayer->id, $replacedInstanceId, ); } return $activePlayer; } public function kick( Room $room, Player $player, ?User $user, ?Player $currentPlayer, ): void { $stateVersion = DB::transaction(function () use ($room, $player, $user, $currentPlayer): int { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); $lockedCurrentPlayer = $currentPlayer === null ? null : $lockedRoom->players() ->lockForUpdate() ->find($currentPlayer->id); $isOwner = $user !== null && $lockedRoom->owner_id === $user->id; $participantCanManage = ! $lockedRoom->isClosed() && $lockedCurrentPlayer !== null && ($lockedCurrentPlayer->is_moderator || $lockedRoom->allow_participants_to_manage_rounds); abort_unless($isOwner || $participantCanManage, 403); $lockedPlayer = $lockedRoom->players() ->lockForUpdate() ->findOrFail($player->id); abort_if( $lockedPlayer->is_moderator || ($lockedRoom->owner_id !== null && $lockedPlayer->user_id === $lockedRoom->owner_id) || $lockedCurrentPlayer?->id === $lockedPlayer->id, 403, ); $lockedPlayer->delete(); $lockedRoom->update([ 'last_activity_at' => now(), 'state_version' => $lockedRoom->state_version + 1, ]); return $lockedRoom->state_version; }); RoomStateChanged::dispatch( $room->id, $stateVersion, RoomStateChange::PlayerKicked, $this->realtimeState->for($room), $player->id, ); } }