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, ); if ($player === null) { $player = $room->players()->create([ 'user_id' => $user?->id, 'name' => $data['player_name'], 'is_moderator' => false, 'last_seen_at' => now(), ]); } else { $player->update([ 'last_seen_at' => now(), 'inactive_at' => null, ]); } $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): Player { [$player, $change, $stateVersion] = DB::transaction(function () use ($room, $user, $playerName): array { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); if ($lockedRoom->isClosed()) { throw ValidationException::withMessages([ 'join_code' => 'Dieser Raum ist geschlossen.', ]); } $nameIsTaken = $lockedRoom->players() ->where('name', $playerName) ->where(function ($query) use ($user): void { $query->whereNull('user_id') ->orWhere('user_id', '!=', $user->id); }) ->exists(); if ($nameIsTaken) { throw ValidationException::withMessages([ 'player_name' => 'Dieser Anzeigename ist in dem Raum bereits vorhanden.', ]); } $player = $lockedRoom->players() ->where('user_id', $user->id) ->first(); $change = null; if ($player === null) { $player = $lockedRoom->players()->create([ 'user_id' => $user->id, 'name' => $playerName, 'is_moderator' => false, 'last_seen_at' => now(), ]); $change = RoomStateChange::PlayerJoined; } else { if ($player->inactive_at !== null) { $change = RoomStateChange::PlayerJoined; } elseif ($player->name !== $playerName) { $change = RoomStateChange::PlayerUpdated; } $player->update([ 'name' => $playerName, 'last_seen_at' => now(), 'inactive_at' => null, ]); } 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; } public function kick(Room $room, Player $player, User $user): void { $stateVersion = DB::transaction(function () use ($room, $player, $user): ?int { $lockedRoom = Room::query() ->lockForUpdate() ->findOrFail($room->id); abort_unless($lockedRoom->owner_id === $user->id, 403); $lockedPlayer = $lockedRoom->players() ->lockForUpdate() ->findOrFail($player->id); abort_if($lockedPlayer->user_id === $lockedRoom->owner_id, 403); if ($lockedPlayer->inactive_at !== null) { return null; } $lockedPlayer->update([ 'inactive_at' => now(), ]); $lockedRoom->update([ 'last_activity_at' => now(), 'state_version' => $lockedRoom->state_version + 1, ]); return $lockedRoom->state_version; }); if ($stateVersion === null) { return; } RoomStateChanged::dispatch( $room->id, $stateVersion, RoomStateChange::PlayerKicked, $this->realtimeState->for($room), $player->id, ); } }