package service import ( "fmt" "math" "sort" "strings" "time" "browser.local/platform/domain" "browser.local/platform/repo" ) func (svc *CoreService) GetGamePlayerStateForSession(sessionID, playerID string) (domain.GamePlayerStateSnapshot, error) { player, err := svc.store.GamePlayers().Get(playerID) if err != nil { return domain.GamePlayerStateSnapshot{}, err } if err = svc.authorizeServerLifecycle(sessionID, player.ServerInstanceID); err != nil { return domain.GamePlayerStateSnapshot{}, err } return svc.currentGamePlayerState(player) } func (svc *CoreService) RequestGamePlayerStatePatchForSession(sessionID, playerID string, request domain.GamePlayerStatePatchRequest) (domain.GamePlayerStatePatch, error) { player, err := svc.store.GamePlayers().Get(playerID) if err != nil { return domain.GamePlayerStatePatch{}, err } user, err := svc.GetCurrentUser(sessionID) if err != nil { return domain.GamePlayerStatePatch{}, err } if err = svc.authorizeServerLifecycle(sessionID, player.ServerInstanceID); err != nil { return domain.GamePlayerStatePatch{}, err } state, err := svc.currentGamePlayerState(player) if err != nil { return domain.GamePlayerStatePatch{}, err } if err = validateGamePlayerStatePatch(state, request); err != nil { return domain.GamePlayerStatePatch{}, err } stamp := svc.now() patch := domain.GamePlayerStatePatch{ID: fmt.Sprintf("game-player-state-patch-%d", stamp.UnixNano()), ServerInstanceID: player.ServerInstanceID, GamePlayerRecordID: player.ID, GamePlayerID: player.GamePlayerID, GameVersion: state.GameVersion, ExpectedStateVersion: state.StateVersion, SafetyWindow: state.SafetyWindow, Changes: append([]domain.GamePlayerStatePatchChange(nil), request.Changes...), Reason: strings.TrimSpace(request.Reason), RequesterID: user.ID, Status: domain.GamePlayerStatePatchPendingApproval, CreatedAt: stamp, UpdatedAt: stamp} if err = svc.store.GamePlayerStatePatches().Create(patch); err != nil { return domain.GamePlayerStatePatch{}, err } if _, err = svc.recordAuditEventWithID(user.ID, "game-player-state.patch.request", "game-player-state-patch", patch.ID, domain.AuditResultQueued, "player state patch awaiting platform administrator approval"); err != nil { return domain.GamePlayerStatePatch{}, err } return domain.CopyGamePlayerStatePatch(patch), nil } func (svc *CoreService) ApproveGamePlayerStatePatchForSession(sessionID, patchID string) (domain.GamePlayerStatePatch, error) { patch, err := svc.store.GamePlayerStatePatches().Get(patchID) if err != nil { return domain.GamePlayerStatePatch{}, err } user, err := svc.GetCurrentUser(sessionID) if err != nil { return domain.GamePlayerStatePatch{}, err } if !isPlatformAdmin(user) { return domain.GamePlayerStatePatch{}, ErrForbidden } if err = svc.authorizeServerLifecycle(sessionID, patch.ServerInstanceID); err != nil { return domain.GamePlayerStatePatch{}, err } if patch.Status != domain.GamePlayerStatePatchPendingApproval { return domain.GamePlayerStatePatch{}, validationError("player state patch is not awaiting approval") } player, err := svc.store.GamePlayers().Get(patch.GamePlayerRecordID) if err != nil { return domain.GamePlayerStatePatch{}, err } if player.ServerInstanceID != patch.ServerInstanceID { return domain.GamePlayerStatePatch{}, repo.ErrNotFound } state, err := svc.currentGamePlayerState(player) if err != nil { return domain.GamePlayerStatePatch{}, err } request := domain.GamePlayerStatePatchRequest{GameVersion: patch.GameVersion, ExpectedStateVersion: patch.ExpectedStateVersion, SafetyWindow: patch.SafetyWindow, Changes: patch.Changes, Reason: patch.Reason} if err = validateGamePlayerStatePatch(state, request); err != nil { return domain.GamePlayerStatePatch{}, err } instance, err := svc.store.ServerInstances().Get(patch.ServerInstanceID) if err != nil { return domain.GamePlayerStatePatch{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.GamePlayerStatePatch{}, err } profileKey, ok := gameClientBridgeProfileKey(plugin) if !ok { return domain.GamePlayerStatePatch{}, validationError("SCUM controlled player state companion profile is unavailable") } command, err := svc.queueGameClientBridgeCommand(user.ID, domain.GameClientBridgeQueueRequest{ServerInstanceID: patch.ServerInstanceID, PluginID: instance.PluginID, ProfileKey: profileKey, CommandType: domain.SCUMPlayerStatePatchCommandType, Payload: gamePlayerStatePatchPayload(patch), IdempotencyKey: patch.ID, Priority: 10, ExpiresAt: svc.now().Add(2 * time.Minute)}) if err != nil { return domain.GamePlayerStatePatch{}, err } stamp := svc.now() patch.ApproverID = user.ID patch.ApprovedAt = stamp patch.UpdatedAt = stamp patch.Status = domain.GamePlayerStatePatchQueued patch.BridgeCommandID = command.ID if err = svc.store.GamePlayerStatePatches().Update(patch); err != nil { return domain.GamePlayerStatePatch{}, err } if _, err = svc.recordAuditEventWithID(user.ID, "game-player-state.patch.approve", "game-player-state-patch", patch.ID, domain.AuditResultQueued, "platform administrator approved typed player state patch"); err != nil { return domain.GamePlayerStatePatch{}, err } return domain.CopyGamePlayerStatePatch(patch), nil } func (svc *CoreService) ListGamePlayerStatePatchesForSession(sessionID, playerID string) ([]domain.GamePlayerStatePatch, error) { player, err := svc.store.GamePlayers().Get(playerID) if err != nil { return nil, err } if err = svc.authorizeServerLifecycle(sessionID, player.ServerInstanceID); err != nil { return nil, err } patches, err := svc.store.GamePlayerStatePatches().List(domain.GamePlayerStatePatchFilter{GamePlayerRecordID: player.ID}) if err != nil { return nil, err } for i := range patches { if err := svc.reconcileGamePlayerStatePatch(&patches[i]); err != nil { return nil, err } } sort.Slice(patches, func(i, j int) bool { return patches[i].CreatedAt.After(patches[j].CreatedAt) }) return patches, nil } func (svc *CoreService) currentGamePlayerState(player domain.GamePlayer) (domain.GamePlayerStateSnapshot, error) { snapshots, err := svc.store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ServerInstanceID: player.ServerInstanceID, Type: domain.SCUMPlayerStateSnapshotType, Limit: 100}) if err != nil { return domain.GamePlayerStateSnapshot{}, err } var latest domain.GameClientBridgeSnapshot found := false for _, snapshot := range snapshots { if strings.TrimSpace(stringValue(snapshot.Payload["playerId"])) != player.GamePlayerID { continue } if !found || snapshot.ObservedAt.After(latest.ObservedAt) { latest, found = snapshot, true } } if !found { return domain.GamePlayerStateSnapshot{}, validationError("current player state snapshot is unavailable") } fields, ok := numberMap(latest.Payload["fields"]) if !ok { return domain.GamePlayerStateSnapshot{}, validationError("player state snapshot fields are invalid") } state := domain.GamePlayerStateSnapshot{ServerInstanceID: player.ServerInstanceID, GamePlayerRecordID: player.ID, GamePlayerID: player.GamePlayerID, GameVersion: strings.TrimSpace(stringValue(latest.Payload["gameVersion"])), StateVersion: strings.TrimSpace(stringValue(latest.Payload["stateVersion"])), SafetyWindow: strings.TrimSpace(stringValue(latest.Payload["safetyWindow"])), MaintenanceVerified: boolValue(latest.Payload["maintenanceVerified"]), PlayerOnline: boolValue(latest.Payload["playerOnline"]), Fields: fields, ObservedAt: latest.ObservedAt} if state.GameVersion == "" || state.StateVersion == "" { return domain.GamePlayerStateSnapshot{}, validationError("player state snapshot is incomplete") } return state, nil } func validateGamePlayerStatePatch(state domain.GamePlayerStateSnapshot, request domain.GamePlayerStatePatchRequest) error { if _, ok := domain.SCUMPlayerStateCatalogForVersion(state.GameVersion); !ok { return validationError("SCUM server game version does not support controlled player state patches") } if request.GameVersion != state.GameVersion { return validationError("player state patch game version conflicts with current server state") } if request.ExpectedStateVersion != state.StateVersion { return validationError("player state patch conflicts with current state version") } if !state.MaintenanceVerified || state.PlayerOnline || state.SafetyWindow == "" || request.SafetyWindow != state.SafetyWindow { return validationError("player state patch requires a verified maintenance/offline safety window") } if reason := strings.TrimSpace(request.Reason); len(reason) < 4 || len(reason) > 240 { return validationError("player state patch reason must be 4 to 240 characters") } if len(request.Changes) == 0 || len(request.Changes) > 8 { return validationError("player state patch must contain 1 to 8 changes") } seen := map[string]bool{} for _, change := range request.Changes { field, ok := domain.GamePlayerStateFieldForVersion(state.GameVersion, change.FieldKey) if !ok || seen[change.FieldKey] { return validationError("player state patch field is not supported by the server version") } seen[change.FieldKey] = true before, exists := state.Fields[change.FieldKey] if !exists || before != change.Before || math.IsNaN(change.After) || math.IsInf(change.After, 0) || change.After < field.Minimum || change.After > field.Maximum { return validationError("player state patch has an invalid field value or stale before value") } } return nil } func gamePlayerStatePatchPayload(patch domain.GamePlayerStatePatch) map[string]any { changes := make([]any, len(patch.Changes)) for i, change := range patch.Changes { changes[i] = map[string]any{"fieldKey": change.FieldKey, "before": change.Before, "after": change.After} } return map[string]any{"playerId": patch.GamePlayerID, "gameVersion": patch.GameVersion, "expectedStateVersion": patch.ExpectedStateVersion, "safetyWindow": patch.SafetyWindow, "reason": patch.Reason, "changes": changes} } func gameClientBridgeProfileKey(plugin domain.GamePlugin) (string, bool) { for _, profile := range plugin.RuntimeProfiles.ClientManagers { if containsString(profile.Health.RequiredCapabilities, gameClientBridgeCapability) { return profile.Key, true } } return "", false } func (svc *CoreService) reconcileGamePlayerStatePatch(patch *domain.GamePlayerStatePatch) error { if patch.Status != domain.GamePlayerStatePatchQueued || patch.BridgeCommandID == "" { return nil } command, err := svc.store.GameClientBridgeCommands().Get(patch.BridgeCommandID) if err != nil { return err } stamp := svc.now() changed := false if command.State == domain.GameClientBridgeCommandFailed { patch.Status = domain.GamePlayerStatePatchExecutionFailed patch.ExecutionSummary = command.Result.Summary changed = true } else if command.State == domain.GameClientBridgeCommandExpired || command.State == domain.GameClientBridgeCommandCancelled { patch.Status = domain.GamePlayerStatePatchExecutionUnknown patch.ExecutionSummary = command.Result.Summary changed = true } else if command.State == domain.GameClientBridgeCommandSucceeded { version := strings.TrimSpace(stringValue(command.Result.Payload["confirmedStateVersion"])) fields, ok := numberMap(command.Result.Payload["confirmedFields"]) if version == "" || version == patch.ExpectedStateVersion || !ok || !confirmedPatchFields(patch.Changes, fields) { patch.Status = domain.GamePlayerStatePatchConfirmationFailed patch.ExecutionSummary = command.Result.Summary changed = true } else { patch.Status = domain.GamePlayerStatePatchConfirmed patch.ConfirmedStateVersion = version patch.ExecutionSummary = command.Result.Summary changed = true } } if !changed { return nil } patch.CompletedAt = stamp patch.UpdatedAt = stamp if err = svc.store.GamePlayerStatePatches().Update(*patch); err != nil { return err } _, err = svc.recordAuditEventWithID("component:scum-client-manager", "game-player-state.patch.result", "game-player-state-patch", patch.ID, domain.AuditResultSuccess, "typed player state patch terminal result recorded") return err } func confirmedPatchFields(changes []domain.GamePlayerStatePatchChange, fields map[string]float64) bool { for _, change := range changes { if value, ok := fields[change.FieldKey]; !ok || value != change.After { return false } } return true } func stringValue(value any) string { text, _ := value.(string); return text } func boolValue(value any) bool { flag, _ := value.(bool); return flag } func numberMap(value any) (map[string]float64, bool) { raw, ok := value.(map[string]any) if !ok { return nil, false } out := map[string]float64{} for key, value := range raw { number, ok := value.(float64) if !ok { return nil, false } out[key] = number } return out, true }