A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
369 lines
12 KiB
Go
369 lines
12 KiB
Go
package matrix
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ========================================
|
|
// Breakpilot Drive Game Room Types
|
|
// ========================================
|
|
|
|
// GameMode represents different multiplayer game modes
|
|
type GameMode string
|
|
|
|
const (
|
|
GameModeSolo GameMode = "solo"
|
|
GameModeCoop GameMode = "coop" // 2 players, same track
|
|
GameModeChallenge GameMode = "challenge" // 1v1 competition
|
|
GameModeClassRace GameMode = "class_race" // Whole class competition
|
|
)
|
|
|
|
// GameRoomConfig holds configuration for game rooms
|
|
type GameRoomConfig struct {
|
|
GameMode GameMode `json:"game_mode"`
|
|
SessionID string `json:"session_id"`
|
|
HostUserID string `json:"host_user_id"`
|
|
HostName string `json:"host_name"`
|
|
ClassName string `json:"class_name,omitempty"`
|
|
MaxPlayers int `json:"max_players,omitempty"`
|
|
TeacherIDs []string `json:"teacher_ids,omitempty"`
|
|
EnableVoice bool `json:"enable_voice,omitempty"`
|
|
}
|
|
|
|
// GameRoom represents an active game room
|
|
type GameRoom struct {
|
|
RoomID string `json:"room_id"`
|
|
SessionID string `json:"session_id"`
|
|
GameMode GameMode `json:"game_mode"`
|
|
HostUserID string `json:"host_user_id"`
|
|
Players []string `json:"players"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
IsActive bool `json:"is_active"`
|
|
}
|
|
|
|
// GameEvent represents game events to broadcast
|
|
type GameEvent struct {
|
|
Type string `json:"type"`
|
|
SessionID string `json:"session_id"`
|
|
PlayerID string `json:"player_id"`
|
|
Data interface{} `json:"data"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// GameEventType constants
|
|
const (
|
|
GameEventPlayerJoined = "player_joined"
|
|
GameEventPlayerLeft = "player_left"
|
|
GameEventGameStarted = "game_started"
|
|
GameEventQuizAnswered = "quiz_answered"
|
|
GameEventScoreUpdate = "score_update"
|
|
GameEventAchievement = "achievement"
|
|
GameEventChallengeWon = "challenge_won"
|
|
GameEventRaceFinished = "race_finished"
|
|
)
|
|
|
|
// ========================================
|
|
// Game Room Management
|
|
// ========================================
|
|
|
|
// CreateGameTeamRoom creates a private room for 2-4 players (Co-Op mode)
|
|
func (s *MatrixService) CreateGameTeamRoom(ctx context.Context, config GameRoomConfig) (*CreateRoomResponse, error) {
|
|
roomName := fmt.Sprintf("Breakpilot Drive - Team %s", config.SessionID[:8])
|
|
topic := "Co-Op Spielsession - Arbeitet zusammen!"
|
|
|
|
// All players can write
|
|
users := make(map[string]int)
|
|
users[s.GenerateUserID(config.HostUserID)] = 50
|
|
|
|
req := CreateRoomRequest{
|
|
Name: roomName,
|
|
Topic: topic,
|
|
Visibility: "private",
|
|
Preset: "private_chat",
|
|
InitialState: []StateEvent{
|
|
{
|
|
Type: "m.room.encryption",
|
|
StateKey: "",
|
|
Content: map[string]string{
|
|
"algorithm": "m.megolm.v1.aes-sha2",
|
|
},
|
|
},
|
|
// Custom game state
|
|
{
|
|
Type: "breakpilot.game.session",
|
|
StateKey: "",
|
|
Content: map[string]interface{}{
|
|
"session_id": config.SessionID,
|
|
"game_mode": string(config.GameMode),
|
|
"host_id": config.HostUserID,
|
|
"created_at": time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
},
|
|
},
|
|
PowerLevelContentOverride: &PowerLevels{
|
|
EventsDefault: 0, // All players can send messages
|
|
UsersDefault: 50,
|
|
Users: users,
|
|
Events: map[string]int{
|
|
"breakpilot.game.event": 0, // Anyone can send game events
|
|
},
|
|
},
|
|
}
|
|
|
|
return s.CreateRoom(ctx, req)
|
|
}
|
|
|
|
// CreateGameChallengeRoom creates a 1v1 challenge room
|
|
func (s *MatrixService) CreateGameChallengeRoom(ctx context.Context, config GameRoomConfig, challengerID string, opponentID string) (*CreateRoomResponse, error) {
|
|
roomName := fmt.Sprintf("Challenge: %s", config.SessionID[:8])
|
|
topic := "1v1 Wettbewerb - Möge der Bessere gewinnen!"
|
|
|
|
allPlayers := []string{
|
|
s.GenerateUserID(challengerID),
|
|
s.GenerateUserID(opponentID),
|
|
}
|
|
|
|
users := make(map[string]int)
|
|
for _, id := range allPlayers {
|
|
users[id] = 50
|
|
}
|
|
|
|
req := CreateRoomRequest{
|
|
Name: roomName,
|
|
Topic: topic,
|
|
Visibility: "private",
|
|
Preset: "private_chat",
|
|
Invite: allPlayers,
|
|
InitialState: []StateEvent{
|
|
{
|
|
Type: "breakpilot.game.session",
|
|
StateKey: "",
|
|
Content: map[string]interface{}{
|
|
"session_id": config.SessionID,
|
|
"game_mode": string(GameModeChallenge),
|
|
"challenger_id": challengerID,
|
|
"opponent_id": opponentID,
|
|
"created_at": time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
},
|
|
},
|
|
PowerLevelContentOverride: &PowerLevels{
|
|
EventsDefault: 0,
|
|
UsersDefault: 50,
|
|
Users: users,
|
|
},
|
|
}
|
|
|
|
return s.CreateRoom(ctx, req)
|
|
}
|
|
|
|
// CreateGameClassRaceRoom creates a room for class-wide competition
|
|
func (s *MatrixService) CreateGameClassRaceRoom(ctx context.Context, config GameRoomConfig) (*CreateRoomResponse, error) {
|
|
roomName := fmt.Sprintf("Klassenrennen: %s", config.ClassName)
|
|
topic := fmt.Sprintf("Klassenrennen der %s - Alle gegen alle!", config.ClassName)
|
|
|
|
// Teachers get moderator power level
|
|
users := make(map[string]int)
|
|
for _, teacherID := range config.TeacherIDs {
|
|
users[s.GenerateUserID(teacherID)] = 100
|
|
}
|
|
|
|
req := CreateRoomRequest{
|
|
Name: roomName,
|
|
Topic: topic,
|
|
Visibility: "private",
|
|
Preset: "private_chat",
|
|
InitialState: []StateEvent{
|
|
{
|
|
Type: "breakpilot.game.session",
|
|
StateKey: "",
|
|
Content: map[string]interface{}{
|
|
"session_id": config.SessionID,
|
|
"game_mode": string(GameModeClassRace),
|
|
"class_name": config.ClassName,
|
|
"teacher_ids": config.TeacherIDs,
|
|
"created_at": time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
},
|
|
},
|
|
PowerLevelContentOverride: &PowerLevels{
|
|
EventsDefault: 0, // Students can send messages
|
|
UsersDefault: 10, // Default student level
|
|
Users: users,
|
|
Invite: 100, // Only teachers can invite
|
|
Kick: 100, // Only teachers can kick
|
|
Events: map[string]int{
|
|
"breakpilot.game.event": 0, // Anyone can send game events
|
|
"breakpilot.game.leaderboard": 100, // Only teachers update leaderboard
|
|
},
|
|
},
|
|
}
|
|
|
|
return s.CreateRoom(ctx, req)
|
|
}
|
|
|
|
// ========================================
|
|
// Game Event Broadcasting
|
|
// ========================================
|
|
|
|
// SendGameEvent sends a game event to a room
|
|
func (s *MatrixService) SendGameEvent(ctx context.Context, roomID string, event GameEvent) error {
|
|
event.Timestamp = time.Now().UTC()
|
|
|
|
return s.sendEvent(ctx, roomID, "breakpilot.game.event", event)
|
|
}
|
|
|
|
// SendPlayerJoinedEvent notifies room that a player joined
|
|
func (s *MatrixService) SendPlayerJoinedEvent(ctx context.Context, roomID string, sessionID string, playerID string, playerName string) error {
|
|
event := GameEvent{
|
|
Type: GameEventPlayerJoined,
|
|
SessionID: sessionID,
|
|
PlayerID: playerID,
|
|
Data: map[string]string{
|
|
"player_name": playerName,
|
|
},
|
|
}
|
|
|
|
// Also send a visible message
|
|
msg := fmt.Sprintf("🎮 %s ist dem Spiel beigetreten!", playerName)
|
|
if err := s.SendMessage(ctx, roomID, msg); err != nil {
|
|
// Log but don't fail
|
|
fmt.Printf("Warning: failed to send join message: %v\n", err)
|
|
}
|
|
|
|
return s.SendGameEvent(ctx, roomID, event)
|
|
}
|
|
|
|
// SendScoreUpdateEvent broadcasts score updates
|
|
func (s *MatrixService) SendScoreUpdateEvent(ctx context.Context, roomID string, sessionID string, playerID string, score int, accuracy float64) error {
|
|
event := GameEvent{
|
|
Type: GameEventScoreUpdate,
|
|
SessionID: sessionID,
|
|
PlayerID: playerID,
|
|
Data: map[string]interface{}{
|
|
"score": score,
|
|
"accuracy": accuracy,
|
|
},
|
|
}
|
|
|
|
return s.SendGameEvent(ctx, roomID, event)
|
|
}
|
|
|
|
// SendQuizAnsweredEvent broadcasts when a player answers a quiz
|
|
func (s *MatrixService) SendQuizAnsweredEvent(ctx context.Context, roomID string, sessionID string, playerID string, correct bool, subject string) error {
|
|
event := GameEvent{
|
|
Type: GameEventQuizAnswered,
|
|
SessionID: sessionID,
|
|
PlayerID: playerID,
|
|
Data: map[string]interface{}{
|
|
"correct": correct,
|
|
"subject": subject,
|
|
},
|
|
}
|
|
|
|
return s.SendGameEvent(ctx, roomID, event)
|
|
}
|
|
|
|
// SendAchievementEvent broadcasts when a player earns an achievement
|
|
func (s *MatrixService) SendAchievementEvent(ctx context.Context, roomID string, sessionID string, playerID string, achievementID string, achievementName string) error {
|
|
event := GameEvent{
|
|
Type: GameEventAchievement,
|
|
SessionID: sessionID,
|
|
PlayerID: playerID,
|
|
Data: map[string]interface{}{
|
|
"achievement_id": achievementID,
|
|
"achievement_name": achievementName,
|
|
},
|
|
}
|
|
|
|
// Also send a visible celebration message
|
|
msg := fmt.Sprintf("🏆 Erfolg freigeschaltet: %s!", achievementName)
|
|
if err := s.SendMessage(ctx, roomID, msg); err != nil {
|
|
fmt.Printf("Warning: failed to send achievement message: %v\n", err)
|
|
}
|
|
|
|
return s.SendGameEvent(ctx, roomID, event)
|
|
}
|
|
|
|
// SendChallengeWonEvent broadcasts challenge result
|
|
func (s *MatrixService) SendChallengeWonEvent(ctx context.Context, roomID string, sessionID string, winnerID string, winnerName string, loserName string, winnerScore int, loserScore int) error {
|
|
event := GameEvent{
|
|
Type: GameEventChallengeWon,
|
|
SessionID: sessionID,
|
|
PlayerID: winnerID,
|
|
Data: map[string]interface{}{
|
|
"winner_name": winnerName,
|
|
"loser_name": loserName,
|
|
"winner_score": winnerScore,
|
|
"loser_score": loserScore,
|
|
},
|
|
}
|
|
|
|
// Send celebration message
|
|
msg := fmt.Sprintf("🎉 %s gewinnt gegen %s mit %d zu %d Punkten!", winnerName, loserName, winnerScore, loserScore)
|
|
if err := s.SendHTMLMessage(ctx, roomID, msg, fmt.Sprintf("<h3>🎉 Challenge beendet!</h3><p><strong>%s</strong> gewinnt gegen %s</p><p>Endstand: %d : %d</p>", winnerName, loserName, winnerScore, loserScore)); err != nil {
|
|
fmt.Printf("Warning: failed to send challenge result message: %v\n", err)
|
|
}
|
|
|
|
return s.SendGameEvent(ctx, roomID, event)
|
|
}
|
|
|
|
// SendClassRaceLeaderboard broadcasts current leaderboard in class race
|
|
func (s *MatrixService) SendClassRaceLeaderboard(ctx context.Context, roomID string, sessionID string, leaderboard []map[string]interface{}) error {
|
|
// Build leaderboard message
|
|
msg := "🏁 Aktueller Stand:\n"
|
|
htmlMsg := "<h3>🏁 Aktueller Stand</h3><ol>"
|
|
|
|
for i, entry := range leaderboard {
|
|
if i >= 10 { // Top 10 only
|
|
break
|
|
}
|
|
name := entry["name"].(string)
|
|
score := entry["score"].(int)
|
|
msg += fmt.Sprintf("%d. %s - %d Punkte\n", i+1, name, score)
|
|
htmlMsg += fmt.Sprintf("<li><strong>%s</strong> - %d Punkte</li>", name, score)
|
|
}
|
|
htmlMsg += "</ol>"
|
|
|
|
return s.SendHTMLMessage(ctx, roomID, msg, htmlMsg)
|
|
}
|
|
|
|
// ========================================
|
|
// Game Room Utilities
|
|
// ========================================
|
|
|
|
// AddPlayerToGameRoom invites and sets up a player in a game room
|
|
func (s *MatrixService) AddPlayerToGameRoom(ctx context.Context, roomID string, playerMatrixID string, playerName string) error {
|
|
// Invite the player
|
|
if err := s.InviteUser(ctx, roomID, playerMatrixID); err != nil {
|
|
return fmt.Errorf("failed to invite player: %w", err)
|
|
}
|
|
|
|
// Set display name if not already set
|
|
if err := s.SetDisplayName(ctx, playerMatrixID, playerName); err != nil {
|
|
// Log but don't fail - display name might already be set
|
|
fmt.Printf("Warning: failed to set display name: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CloseGameRoom sends end message and archives the room
|
|
func (s *MatrixService) CloseGameRoom(ctx context.Context, roomID string, sessionID string) error {
|
|
// Send closing message
|
|
msg := "🏁 Spiel beendet! Danke fürs Mitspielen. Dieser Raum wird archiviert."
|
|
if err := s.SendMessage(ctx, roomID, msg); err != nil {
|
|
return fmt.Errorf("failed to send closing message: %w", err)
|
|
}
|
|
|
|
// Update room state to mark as closed
|
|
closeEvent := map[string]interface{}{
|
|
"closed": true,
|
|
"closed_at": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
return s.sendEvent(ctx, roomID, "breakpilot.game.closed", closeEvent)
|
|
}
|