diff --git a/api/main.proto b/api/main.proto index 6a67f12..dbf6d9d 100644 --- a/api/main.proto +++ b/api/main.proto @@ -912,6 +912,14 @@ service EveningDetectiveServer { }; } + rpc DownloadGamePrintMaterials(GetGameReq) returns (google.api.HttpBody) { + option (google.api.http) = {get: "/api/games/{ID}/print-materials"}; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags : "Игры"; + summary: "Скачать материалы игры для печати ZIP-архивом"; + }; + } + rpc DownloadTeamQR(GetGameReq) returns (google.api.HttpBody) { option (google.api.http) = { get: "/api/games/{ID}/teams/qr.pdf" @@ -2401,13 +2409,13 @@ message GameResultsPackageRsp { } message DeviceHeartbeatReq { - string deviceID = 1; - string connectionID = 2; - optional int32 organizerGameID = 3; + string deviceID = 1; + string connectionID = 2; + optional int32 organizerGameID = 3; optional string teamID = 4; - string password = 5; - string screenID = 6; - string accessToken = 7; + string password = 5; + string screenID = 6; + string accessToken = 7; } message DeviceHeartbeatRsp { diff --git a/bin/evening_detective_server b/bin/evening_detective_server index 56606f1..8e165d1 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index 4490d37..0ff85f8 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -40,6 +40,7 @@ import ( "evening_detective_server/internal/services/game_export_service" "evening_detective_server/internal/services/game_import_service" "evening_detective_server/internal/services/game_package_service" + "evening_detective_server/internal/services/game_print_service" "evening_detective_server/internal/services/game_results_package_service" "evening_detective_server/internal/services/game_review_service" "evening_detective_server/internal/services/game_service" @@ -221,6 +222,7 @@ func main() { reviewService := game_review_service.NewGameReviewService(reviewsRepo, gameRepo, teamRepo, os.Getenv("APP_BASE_URL")) teamService := team_service.NewTeamService(gameRepo, teamRepo, passwordGenerator, os.Getenv("APP_BASE_URL"), emailSender) + gamePrintService := game_print_service.NewGamePrintService(gameRepo, scenarioService, teamRepo, teamService, fileStorage, os.Getenv("FILE_PREFIX_DOMAIN")) participationService := participation_service.NewParticipationService(teamRepo, os.Getenv("APP_BASE_URL")) ticketService := ticket_service.NewTicketService(ticketRepo, os.Getenv("APP_BASE_URL")) @@ -298,6 +300,7 @@ func main() { gameResultsApplyService, devicePresenceService, wifi_service.New(os.Getenv("CUDY_SNAPSHOT_FILE")), + gamePrintService, ), ) diff --git a/cmd/evening_detective_server/main.swagger.json b/cmd/evening_detective_server/main.swagger.json index 7196ee0..5ed24cf 100644 --- a/cmd/evening_detective_server/main.swagger.json +++ b/cmd/evening_detective_server/main.swagger.json @@ -841,6 +841,38 @@ ] } }, + "/api/games/{ID}/print-materials": { + "get": { + "summary": "Скачать материалы игры для печати ZIP-архивом", + "operationId": "EveningDetectiveServer_DownloadGamePrintMaterials", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/apiHttpBody" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "ID", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "Игры" + ] + } + }, "/api/games/{ID}/reset": { "put": { "summary": "Сбросить игру", diff --git a/docs/game_rules.go b/docs/game_rules.go new file mode 100644 index 0000000..5fbe487 --- /dev/null +++ b/docs/game_rules.go @@ -0,0 +1,18 @@ +// Package docs embeds the canonical game rules used in printable materials. +package docs + +import _ "embed" + +//go:embed GAME_RULES_ONLINE.md +var onlineRules string + +//go:embed GAME_RULES_OFFLINE.md +var offlineRules string + +// GameRules returns the rules for a game's format. +func GameRules(format string) string { + if format == "offline" { + return offlineRules + } + return onlineRules +} diff --git a/internal/app/game_print.go b/internal/app/game_print.go new file mode 100644 index 0000000..e1b5e78 --- /dev/null +++ b/internal/app/game_print.go @@ -0,0 +1,43 @@ +package app + +import ( + "context" + "mime" + + "evening_detective_server/internal/modules/roles" + "evening_detective_server/proto" + + "google.golang.org/genproto/googleapis/api/httpbody" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +func (s *server) DownloadGamePrintMaterials(ctx context.Context, req *proto.GetGameReq) (*httpbody.HttpBody, error) { + claims, err := claimsFromContext(ctx) + if err != nil { + return nil, err + } + if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { + return nil, status.Error(codes.PermissionDenied, "нет доступа к материалам игры") + } + if req == nil || req.ID <= 0 { + return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры") + } + data, filename, err := s.gamePrintService.Download(ctx, int(req.ID)) + if err != nil { + return nil, gamePrintError(err) + } + if err := grpc.SetHeader(ctx, metadata.Pairs( + "Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}), + "X-Content-Type-Options", "nosniff", + "Cache-Control", "no-store", + )); err != nil { + return nil, status.Error(codes.Internal, "не удалось установить заголовки архива") + } + return &httpbody.HttpBody{ + ContentType: "application/zip", + Data: data, + }, nil +} diff --git a/internal/app/game_print_errors.go b/internal/app/game_print_errors.go new file mode 100644 index 0000000..17dac60 --- /dev/null +++ b/internal/app/game_print_errors.go @@ -0,0 +1,35 @@ +package app + +import ( + "context" + "errors" + "log" + + "evening_detective_server/internal/modules/file_storage" + "evening_detective_server/internal/services/game_print_service" + "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/team_service" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func gamePrintError(err error) error { + switch { + case errors.Is(err, team_service.ErrGameNotFound), errors.Is(err, scenarios_service.ErrScenarioNotFound): + return status.Error(codes.NotFound, "игра или сценарий не найдены") + case errors.Is(err, game_print_service.ErrPackageTooLarge): + return status.Error(codes.ResourceExhausted, game_print_service.ErrPackageTooLarge.Error()) + case errors.Is(err, game_print_service.ErrInvalidPrintText): + return status.Error(codes.FailedPrecondition, "текст не помещается на лист или содержит неподдерживаемые символы") + case errors.Is(err, file_storage.ErrFileNotFound): + return status.Error(codes.FailedPrecondition, "не найден файл улики; проверьте материалы сценария") + case errors.Is(err, context.Canceled): + return status.Error(codes.Canceled, "скачивание отменено") + case errors.Is(err, context.DeadlineExceeded): + return status.Error(codes.DeadlineExceeded, "превышено время подготовки материалов") + default: + log.Printf("game print materials: %v", err) + return status.Error(codes.Internal, "не удалось подготовить материалы для печати") + } +} diff --git a/internal/app/game_print_test.go b/internal/app/game_print_test.go new file mode 100644 index 0000000..1e5d54d --- /dev/null +++ b/internal/app/game_print_test.go @@ -0,0 +1,111 @@ +package app + +import ( + "bytes" + "context" + "errors" + "fmt" + "testing" + + "evening_detective_server/internal/modules/file_storage" + "evening_detective_server/internal/repos" + "evening_detective_server/internal/services/game_print_service" + "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/team_service" + "evening_detective_server/proto" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type printGameStub struct{} + +func (printGameStub) GetGameByID(context.Context, int) (*repos.Game, error) { + return &repos.Game{ID: 1, Name: "Дело №1", Format: "offline"}, nil +} + +func (printGameStub) GetFullScenarioByID(context.Context, int) (*scenarios_service.Scenario, error) { + return &scenarios_service.Scenario{}, nil +} + +func (printGameStub) GetActiveTeamsByGameID(context.Context, int) ([]*repos.Team, error) { + return nil, nil +} + +func TestDownloadGamePrintMaterialsHeaders(t *testing.T) { + for _, role := range []string{"organizer", "author", "admin"} { + t.Run(role, func(t *testing.T) { + stub := printGameStub{} + s := &server{gamePrintService: game_print_service.NewGamePrintService(stub, stub, stub, nil, nil, "")} + stream := &mockServerTransportStream{} + ctx := grpc.NewContextWithServerTransportStream(ctxWithClaims(role), stream) + body, err := s.DownloadGamePrintMaterials(ctx, &proto.GetGameReq{ID: 1}) + if err != nil { + t.Fatal(err) + } + if body.ContentType != "application/zip" || !bytes.HasPrefix(body.Data, []byte("PK")) { + t.Fatal("invalid ZIP response") + } + for key, want := range map[string]string{ + "content-disposition": "attachment; filename=game-1-print-materials.zip", + "cache-control": "no-store", + "x-content-type-options": "nosniff", + } { + values := stream.header.Get(key) + if len(values) != 1 || values[0] != want { + t.Errorf("%s = %v, want %s", key, values, want) + } + } + }) + } +} + +func TestDownloadGamePrintMaterialsAccess(t *testing.T) { + for _, tc := range []struct { + name string + ctx context.Context + request *proto.GetGameReq + want codes.Code + }{ + {"anonymous", context.Background(), &proto.GetGameReq{ID: 1}, codes.Unauthenticated}, + {"player", ctxWithClaims("user"), &proto.GetGameReq{ID: 1}, codes.PermissionDenied}, + {"developer", ctxWithClaims("developer"), &proto.GetGameReq{ID: 1}, codes.PermissionDenied}, + {"organizer invalid ID", ctxWithClaims("organizer"), &proto.GetGameReq{}, codes.InvalidArgument}, + {"author invalid ID", ctxWithClaims("author"), &proto.GetGameReq{ID: -1}, codes.InvalidArgument}, + {"admin nil request", ctxWithClaims("admin"), nil, codes.InvalidArgument}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := (&server{}).DownloadGamePrintMaterials(tc.ctx, tc.request) + if status.Code(err) != tc.want { + t.Fatalf("code = %v, want %v", status.Code(err), tc.want) + } + }) + } +} + +func TestGamePrintErrorMapping(t *testing.T) { + for _, tc := range []struct { + err error + want codes.Code + }{ + {team_service.ErrGameNotFound, codes.NotFound}, + {scenarios_service.ErrScenarioNotFound, codes.NotFound}, + {game_print_service.ErrPackageTooLarge, codes.ResourceExhausted}, + {game_print_service.ErrInvalidPrintText, codes.FailedPrecondition}, + {file_storage.ErrFileNotFound, codes.FailedPrecondition}, + {context.Canceled, codes.Canceled}, + {context.DeadlineExceeded, codes.DeadlineExceeded}, + {errors.New("private storage detail"), codes.Internal}, + } { + t.Run(tc.err.Error(), func(t *testing.T) { + err := gamePrintError(fmt.Errorf("wrapped: %w", tc.err)) + if status.Code(err) != tc.want { + t.Fatalf("code = %v, want %v", status.Code(err), tc.want) + } + if tc.want == codes.Internal && status.Convert(err).Message() != "не удалось подготовить материалы для печати" { + t.Fatal("private error exposed") + } + }) + } +} diff --git a/internal/app/server.go b/internal/app/server.go index 35b09cd..79ef7b9 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -6,6 +6,7 @@ import ( "evening_detective_server/internal/services/file_service" "evening_detective_server/internal/services/game_export_service" "evening_detective_server/internal/services/game_import_service" + "evening_detective_server/internal/services/game_print_service" "evening_detective_server/internal/services/game_results_package_service" "evening_detective_server/internal/services/game_review_service" "evening_detective_server/internal/services/game_service" @@ -40,6 +41,7 @@ type server struct { gameResultsService *game_results_package_service.GameResultsPackageService gameResultsApplyService *game_results_package_service.GameResultsApplyService gameService *game_service.GameService + gamePrintService *game_print_service.GamePrintService gameplayService *gameplay_service.GameplayService answerService *answer_service.AnswerService reviewService *game_review_service.GameReviewService @@ -76,8 +78,10 @@ func NewServer( gameResultsApplyService *game_results_package_service.GameResultsApplyService, devicePresenceService *device_presence_service.DevicePresenceService, wifiService *wifi_service.Service, + gamePrintService *game_print_service.GamePrintService, ) proto.EveningDetectiveServerServer { return &server{ + gamePrintService: gamePrintService, devicePresenceService: devicePresenceService, wifiService: wifiService, gameImportService: gameImportService, diff --git a/internal/modules/team_qr_pdf/generator.go b/internal/modules/team_qr_pdf/generator.go index 9efcfcd..d9972ec 100644 --- a/internal/modules/team_qr_pdf/generator.go +++ b/internal/modules/team_qr_pdf/generator.go @@ -25,6 +25,11 @@ type Card struct { //go:embed fonts/DejaVuSansCondensed.ttf var font []byte +// RegisterFont adds the existing Cyrillic print font to a PDF document. +func RegisterFont(pdf *fpdf.Fpdf) { + pdf.AddUTF8FontFromBytes("teams", "", font) +} + // Generate returns a PDF containing team QR-code cards. func Generate(cards []Card) ([]byte, error) { if len(cards) == 0 { @@ -33,7 +38,7 @@ func Generate(cards []Card) ([]byte, error) { pdf := fpdf.New("P", "mm", "A4", "") pdf.SetAutoPageBreak(false, 0) pdf.SetCellMargin(0) - pdf.AddUTF8FontFromBytes("teams", "", font) + RegisterFont(pdf) pdf.SetFillColor(0, 0, 0) const margin, width, height, qrSize = 10.0, 190.0 / 3, 277.0 / 4, 36.0 for i, card := range cards { diff --git a/internal/services/game_export_service/export_test.go b/internal/services/game_export_service/export_test.go index f878237..db53947 100644 --- a/internal/services/game_export_service/export_test.go +++ b/internal/services/game_export_service/export_test.go @@ -378,7 +378,7 @@ func verifyExportedGameReads(t *testing.T, f serviceFixture, worker *game_export games := game_service.NewGameService(f.games, f.teams, f.scenarios, nil, "", actions_repo.NewActionsRepo(f.pool), applications_repo.NewApplicationsRepo(f.pool)) mapRepo := game_map_repo.NewGameMapRepo(f.pool) server := app.NewServer(nil, nil, nil, nil, games, nil, nil, nil, nil, nil, nil, - screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil, nil, nil) + screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil, nil, nil, nil) ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{UserID: f.userID, Roles: []string{"organizer"}}) game, err := server.GetGame(ctx, &proto.GetGameReq{ID: int32(f.gameID)}) check(t, err) diff --git a/internal/services/game_print_service/pdf.go b/internal/services/game_print_service/pdf.go new file mode 100644 index 0000000..d3e1e1c --- /dev/null +++ b/internal/services/game_print_service/pdf.go @@ -0,0 +1,209 @@ +package game_print_service + +import ( + "bytes" + "fmt" + "strings" + "unicode/utf8" + + "evening_detective_server/internal/modules/cleaner" + "evening_detective_server/internal/modules/storytelling" + "evening_detective_server/internal/modules/team_qr_pdf" + "evening_detective_server/internal/repos" + + "github.com/go-pdf/fpdf" +) + +func newPDF(orientation string) *fpdf.Fpdf { + pdf := fpdf.New(orientation, "mm", "A4", "") + pdf.SetMargins(15, 15, 15) + pdf.SetAutoPageBreak(true, 15) + team_qr_pdf.RegisterFont(pdf) + pdf.SetFont("teams", "", 11) + pdf.SetFooterFunc(func() { + pdf.SetY(-12) + pdf.SetFont("teams", "", 8) + pdf.CellFormat(0, 5, fmt.Sprint(pdf.PageNo()), "", 0, "C", false, 0, "") + }) + pdf.AddPage() + return pdf +} + +func printText(pdf *fpdf.Fpdf, text string) string { + if !utf8.ValidString(text) { + pdf.SetError(ErrInvalidPrintText) + return "" + } + for _, r := range text { + // shortcut: fpdf supports only BMP text; replace the renderer when emoji printing is required. + if r > 0xffff { + pdf.SetError(ErrInvalidPrintText) + return "" + } + } + return strings.ReplaceAll(strings.ReplaceAll(text, "\r\n", "\n"), "\t", " ") +} + +func heading(pdf *fpdf.Fpdf, text string) { + pdf.SetFont("teams", "", 15) + pdf.MultiCell(0, 7, printText(pdf, text), "", "L", false) + pdf.Ln(3) + pdf.SetFont("teams", "", 11) +} + +func body(pdf *fpdf.Fpdf, text string) { + pdf.MultiCell(0, 6, printText(pdf, text), "", "L", false) + pdf.Ln(3) +} + +func pdfBytes(pdf *fpdf.Fpdf) ([]byte, error) { + var output bytes.Buffer + if err := pdf.Output(&output); err != nil { + return nil, err + } + return output.Bytes(), nil +} + +func textPDF(game, title, text string) ([]byte, error) { + pdf := newPDF("P") + heading(pdf, title) + body(pdf, game) + body(pdf, text) + return pdfBytes(pdf) +} + +func rulesPDF(rules string) ([]byte, error) { + pdf := newPDF("P") + for _, paragraph := range strings.Split(rules, "\n\n") { + if strings.HasPrefix(paragraph, "#") { + if pdf.GetY() > 245 { + pdf.AddPage() + } + heading(pdf, strings.TrimSpace(strings.TrimLeft(paragraph, "#"))) + } else { + body(pdf, paragraph) + } + } + return pdfBytes(pdf) +} + +func questionsPDF(game string, questions []*storytelling.Question) ([]byte, error) { + pdf := newPDF("P") + heading(pdf, "Вопросы дела") + body(pdf, game) + body(pdf, "Команда: __________________________________________________") + textCleaner := cleaner.NewCleaner() + for i, question := range questions { + if pdf.GetY() > 235 { + pdf.AddPage() + } + body(pdf, fmt.Sprintf("%d. %s", i+1, textCleaner.ClearText(question.Text))) + if pdf.GetY() > 247 { + pdf.AddPage() + } + for range 4 { + y := pdf.GetY() + 7 + pdf.Line(15, y, 195, y) + pdf.SetY(y) + } + pdf.Ln(6) + } + return pdfBytes(pdf) +} + +func draftPDF(game, title string) ([]byte, error) { + pdf := newPDF("P") + heading(pdf, title) + body(pdf, game) + body(pdf, "Команда: __________________________________________________") + pdf.SetDrawColor(180, 180, 180) + for y := pdf.GetY() + 8; y < 277; y += 8 { + pdf.Line(15, y, 195, y) + } + return pdfBytes(pdf) +} + +func answersPDF(game string, story *storytelling.Story) ([]byte, error) { + pdf := newPDF("P") + heading(pdf, "Для ведущего: ответы и развязка") + body(pdf, game) + textCleaner := cleaner.NewCleaner() + for i, question := range story.Questions { + body(pdf, fmt.Sprintf("%d. %s", i+1, textCleaner.ClearText(question.Text))) + body(pdf, "Ответ: "+question.Answer) + } + if story.Conclusion != nil { + heading(pdf, "Развязка") + body(pdf, story.Conclusion.Text) + } + return pdfBytes(pdf) +} + +func issuancePDF(game string, teams []*repos.Team, clues []*storytelling.Application) ([]byte, error) { + pdf := newPDF("L") + pdf.SetAutoPageBreak(false, 0) + const columnsPerPage = 8 + const teamWidth, clueWidth, lineHeight = 51.0, 27.0, 4.0 + if len(clues) == 0 { + clues = []*storytelling.Application{{Name: "Улика"}} + } + if len(teams) == 0 { + for range 12 { + teams = append(teams, &repos.Team{}) + } + } + for start := 0; start < len(clues); start += columnsPerPage { + end := min(start+columnsPerPage, len(clues)) + labels := []string{"Команда / улика"} + for _, clue := range clues[start:end] { + labels = append(labels, clue.Name) + } + for row := 0; row < len(teams); { + if start > 0 || row > 0 { + pdf.AddPage() + } + heading(pdf, "Лист выдачи улик") + body(pdf, game) + pdf.SetFont("teams", "", 9) + widths := []float64{teamWidth} + for range labels[1:] { + widths = append(widths, clueWidth) + } + headerHeight := 10.0 + for i, label := range labels { + headerHeight = max(headerHeight, float64(len(pdf.SplitText(printText(pdf, label), widths[i]-4)))*lineHeight+4) + } + if pdf.GetY()+headerHeight+10 > 195 { + return nil, fmt.Errorf("issuance header does not fit: %w", ErrInvalidPrintText) + } + tableRow(pdf, labels, widths, headerHeight) + pageStartRow := row + for row < len(teams) { + name := printText(pdf, teams[row].Name) + height := max(10.0, float64(len(pdf.SplitText(name, teamWidth-4)))*lineHeight+4) + if pdf.GetY()+height > 195 { + if row == pageStartRow { + return nil, fmt.Errorf("team name does not fit: %w", ErrInvalidPrintText) + } + break + } + cells := make([]string, len(labels)) + cells[0] = name + tableRow(pdf, cells, widths, height) + row++ + } + } + } + return pdfBytes(pdf) +} + +func tableRow(pdf *fpdf.Fpdf, cells []string, widths []float64, height float64) { + x, y := 15.0, pdf.GetY() + for i, cell := range cells { + pdf.Rect(x, y, widths[i], height, "D") + pdf.SetXY(x+2, y+2) + pdf.MultiCell(widths[i]-4, 4, printText(pdf, cell), "", "L", false) + x += widths[i] + } + pdf.SetXY(15, y+height) +} diff --git a/internal/services/game_print_service/service.go b/internal/services/game_print_service/service.go new file mode 100644 index 0000000..d5dc9fc --- /dev/null +++ b/internal/services/game_print_service/service.go @@ -0,0 +1,234 @@ +// Package game_print_service builds printable game materials. +package game_print_service + +import ( + "archive/zip" + "bytes" + "context" + "errors" + "fmt" + "net/url" + "path" + "strings" + "unicode" + + "evening_detective_server/docs" + "evening_detective_server/internal/modules/cleaner" + "evening_detective_server/internal/modules/file_storage" + "evening_detective_server/internal/modules/storytelling" + "evening_detective_server/internal/repos" + "evening_detective_server/internal/repos/games_repo" + "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/team_service" +) + +const ( + playerFolder = "Для игрока/" + hostFolder = "Для ведущего/" + maxPackageSize = 256 << 20 +) + +var ( + ErrPackageTooLarge = errors.New("материалы для печати превышают 256 МБ") + ErrInvalidPrintText = errors.New("текст материалов содержит неподдерживаемые символы") +) + +// IGamesReader supplies the game used in a print package. +type IGamesReader interface { + GetGameByID(context.Context, int) (*repos.Game, error) +} + +// IScenariosReader supplies the complete story, including organizer answers. +type IScenariosReader interface { + GetFullScenarioByID(context.Context, int) (*scenarios_service.Scenario, error) +} + +// ITeamsReader supplies participating teams, excluding cancelled registrations. +type ITeamsReader interface { + GetActiveTeamsByGameID(context.Context, int) ([]*repos.Team, error) +} + +// ITeamQRGenerator uses the same team list as the issuance sheet. +type ITeamQRGenerator interface { + GenerateTeamQR(context.Context, *repos.Game, []*repos.Team) ([]byte, string, error) +} + +// GamePrintService builds an archive without persisting generated documents. +type GamePrintService struct { + games IGamesReader + scenarios IScenariosReader + teams ITeamsReader + qr ITeamQRGenerator + files file_storage.IFileStorage + domain string +} + +// NewGamePrintService creates the print package service. +func NewGamePrintService(games IGamesReader, scenarios IScenariosReader, teams ITeamsReader, qr ITeamQRGenerator, files file_storage.IFileStorage, domain string) *GamePrintService { + return &GamePrintService{ + games: games, + scenarios: scenarios, + teams: teams, + qr: qr, + files: files, + domain: domain, + } +} + +// Download builds current player and organizer materials for a game. +func (s *GamePrintService) Download(ctx context.Context, gameID int) ([]byte, string, error) { + if err := ctx.Err(); err != nil { + return nil, "", err + } + game, err := s.games.GetGameByID(ctx, gameID) + if err != nil { + if errors.Is(err, games_repo.ErrGameNotFound) { + return nil, "", team_service.ErrGameNotFound + } + return nil, "", err + } + scenario, err := s.scenarios.GetFullScenarioByID(ctx, game.ScenarioID) + if err != nil { + return nil, "", err + } + teams, err := s.teams.GetActiveTeamsByGameID(ctx, gameID) + if err != nil { + return nil, "", err + } + story := scenario.Story + if story == nil { + story = &storytelling.Story{} + } + clues := collectClues(story) + var output bytes.Buffer + archive := zip.NewWriter(&output) + defer archive.Close() + size := 0 + add := func(name string, data []byte) error { + if err := ctx.Err(); err != nil { + return err + } + size += len(data) + if size > maxPackageSize { + return ErrPackageTooLarge + } + entry, err := archive.Create(name) + if err != nil { + return err + } + _, err = entry.Write(data) + return err + } + intro := "" + if story.Introduction != nil { + intro = cleaner.NewCleaner().ClearText(story.Introduction.Text) + } + documents := []struct { + name string + generate func() ([]byte, error) + }{ + {playerFolder + "Правила.pdf", func() ([]byte, error) { return rulesPDF(docs.GameRules(game.Format)) }}, + {playerFolder + "Введение.pdf", func() ([]byte, error) { return textPDF(game.Name, "Введение", intro) }}, + {playerFolder + "Вопросы.pdf", func() ([]byte, error) { return questionsPDF(game.Name, story.Questions) }}, + {playerFolder + "Список пидарасов.pdf", func() ([]byte, error) { return draftPDF(game.Name, "Список пидарасов") }}, + {playerFolder + "Черновик расследования.pdf", func() ([]byte, error) { return draftPDF(game.Name, "Черновик расследования") }}, + {hostFolder + "Ответы и развязка.pdf", func() ([]byte, error) { return answersPDF(game.Name, story) }}, + {hostFolder + "Лист выдачи улик.pdf", func() ([]byte, error) { return issuancePDF(game.Name, teams, clues) }}, + } + for _, document := range documents { + if err := ctx.Err(); err != nil { + return nil, "", err + } + data, err := document.generate() + if err != nil { + return nil, "", fmt.Errorf("generate %s: %w", document.name, err) + } + if err := add(document.name, data); err != nil { + return nil, "", err + } + } + if len(teams) > 0 { + data, _, err := s.qr.GenerateTeamQR(ctx, game, teams) + if err != nil { + return nil, "", fmt.Errorf("generate team QR: %w", err) + } + if err := add(hostFolder+"QR команд.pdf", data); err != nil { + return nil, "", err + } + } + if err := add(playerFolder+"Улики/", nil); err != nil { + return nil, "", err + } + var external []string + for i, clue := range clues { + ref := strings.TrimPrefix(clue.Image, s.domain) + if ref == "" { + continue + } + parsed, err := url.Parse(ref) + if err != nil { + return nil, "", fmt.Errorf("invalid clue reference: %w", err) + } + if parsed.IsAbs() || parsed.Host != "" { + external = append(external, clue.Name+": "+clue.Image) + continue + } + file, err := s.files.Get(ctx, ref) + if err != nil { + return nil, "", fmt.Errorf("load clue %d: %w", i+1, err) + } + filename := fmt.Sprintf("%03d - %s%s", i+1, safeName(clue.Name), path.Ext(path.Base(ref))) + if err := add(playerFolder+"Улики/"+filename, file.Data); err != nil { + return nil, "", err + } + } + if len(external) > 0 { + data, err := textPDF(game.Name, "Материалы для ручного скачивания", strings.Join(external, "\n\n")) + if err != nil { + return nil, "", err + } + if err := add(hostFolder+"Внешние материалы.pdf", data); err != nil { + return nil, "", err + } + } + if err := archive.Close(); err != nil { + return nil, "", err + } + return output.Bytes(), fmt.Sprintf("game-%d-print-materials.zip", gameID), nil +} + +func collectClues(story *storytelling.Story) []*storytelling.Application { + var clues []*storytelling.Application + seen := make(map[string]bool) + for _, place := range story.Places { + for _, clue := range place.Applications { + key := clue.Image + if key == "" { + key = "name:" + clue.Name + } + if !seen[key] { + seen[key] = true + clues = append(clues, clue) + } + } + } + return clues +} + +func safeName(name string) string { + name = strings.Map(func(r rune) rune { + if unicode.IsControl(r) || strings.ContainsRune(`\/:*?"<>|`, r) { + return '_' + } + return r + }, name) + name = strings.Trim(name, " .") + if name == "" { + return "Улика" + } + runes := []rune(name) + if len(runes) > 80 { + name = string(runes[:80]) + } + return name +} diff --git a/internal/services/game_print_service/service_test.go b/internal/services/game_print_service/service_test.go new file mode 100644 index 0000000..705aeee --- /dev/null +++ b/internal/services/game_print_service/service_test.go @@ -0,0 +1,335 @@ +package game_print_service + +import ( + "archive/zip" + "bytes" + "context" + "errors" + "fmt" + "io" + "os/exec" + "path" + "strings" + "testing" + + "evening_detective_server/internal/modules/file_storage" + "evening_detective_server/internal/modules/storytelling" + "evening_detective_server/internal/repos" + "evening_detective_server/internal/repos/games_repo" + "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/team_service" +) + +type printFixture struct { + file_storage.IFileStorage + game *repos.Game + story *storytelling.Story + teams []*repos.Team + files map[string][]byte + gameErr error + scenarioErr error + teamsErr error + fileErr error + qrCalls int +} + +func (f *printFixture) GetGameByID(context.Context, int) (*repos.Game, error) { + return f.game, f.gameErr +} + +func (f *printFixture) GetFullScenarioByID(context.Context, int) (*scenarios_service.Scenario, error) { + return &scenarios_service.Scenario{Story: f.story}, f.scenarioErr +} + +func (f *printFixture) GetActiveTeamsByGameID(context.Context, int) ([]*repos.Team, error) { + var teams []*repos.Team + for _, team := range f.teams { + if team.ParticipationStatus != "cancelled" { + teams = append(teams, team) + } + } + return teams, f.teamsErr +} + +func (f *printFixture) GenerateTeamQR(ctx context.Context, game *repos.Game, teams []*repos.Team) ([]byte, string, error) { + f.qrCalls++ + return team_service.NewTeamService(nil, nil, nil, "https://game.test", nil).GenerateTeamQR(ctx, game, teams) +} + +func (f *printFixture) Get(ctx context.Context, name string) (*file_storage.File, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if f.fileErr != nil { + return nil, f.fileErr + } + data, ok := f.files[name] + if !ok { + return nil, file_storage.ErrFileNotFound + } + return &file_storage.File{Data: data}, nil +} + +func newPrintFixture() *printFixture { + clue := &storytelling.Application{Name: "Письмо / улика", Image: "https://files.test/clue.pdf"} + return &printFixture{ + game: &repos.Game{ID: 1, Name: "Дело №1", Format: "offline", ScenarioID: 2}, + story: &storytelling.Story{ + Introduction: &storytelling.Introduction{Text: "Начало расследования"}, + Questions: []*storytelling.Question{{Text: "Кто виновен?", Answer: "СЕКРЕТНЫЙ ОТВЕТ"}}, + Conclusion: &storytelling.Conclusion{Text: "СЕКРЕТНАЯ РАЗВЯЗКА"}, + Places: []*storytelling.Place{{Applications: []*storytelling.Application{clue, clue}}}, + }, + teams: []*repos.Team{ + {ID: "team-1", Name: "Детективы", Password: "secret", ParticipationStatus: "confirmed"}, + {ID: "team-2", Name: "Ожидающая команда", Password: "secret", ParticipationStatus: "pending"}, + {ID: "team-3", Name: "Отменённая команда", Password: "secret", ParticipationStatus: "cancelled"}, + }, + files: map[string][]byte{"clue.pdf": []byte("%PDF-original-clue")}, + } +} + +func (f *printFixture) service() *GamePrintService { + return NewGamePrintService(f, f, f, f, f, "https://files.test/") +} + +func readPrintArchive(t *testing.T, raw []byte) map[string][]byte { + t.Helper() + archive, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw))) + if err != nil { + t.Fatal(err) + } + entries := make(map[string][]byte) + for _, entry := range archive.File { + if !strings.HasPrefix(entry.Name, playerFolder) && !strings.HasPrefix(entry.Name, hostFolder) { + t.Fatalf("unexpected root entry %s", entry.Name) + } + if _, exists := entries[entry.Name]; exists { + t.Fatalf("duplicate entry %s", entry.Name) + } + reader, err := entry.Open() + if err != nil { + t.Fatal(err) + } + data, err := io.ReadAll(reader) + reader.Close() + if err != nil { + t.Fatal(err) + } + entries[entry.Name] = data + } + return entries +} + +func TestDownloadPrintMaterials(t *testing.T) { + for _, tc := range []struct { + name string + change func(*printFixture) + wantQR bool + wantExternal bool + }{ + {"complete", func(*printFixture) {}, true, false}, + {"without teams", func(f *printFixture) { f.teams = nil }, false, false}, + {"only cancelled teams", func(f *printFixture) { f.teams = f.teams[2:] }, false, false}, + {"external clue", func(f *printFixture) { f.story.Places[0].Applications[0].Image = "https://external.test/clue.pdf" }, true, true}, + {"empty story", func(f *printFixture) { f.story = nil }, true, false}, + } { + t.Run(tc.name, func(t *testing.T) { + f := newPrintFixture() + tc.change(f) + data, name, err := f.service().Download(context.Background(), 1) + if err != nil { + t.Fatal(err) + } + if name != "game-1-print-materials.zip" { + t.Fatalf("filename = %s", name) + } + entries := readPrintArchive(t, data) + for _, name := range []string{ + playerFolder + "Правила.pdf", playerFolder + "Введение.pdf", playerFolder + "Вопросы.pdf", + playerFolder + "Список пидарасов.pdf", playerFolder + "Черновик расследования.pdf", + hostFolder + "Ответы и развязка.pdf", hostFolder + "Лист выдачи улик.pdf", + } { + if !bytes.HasPrefix(entries[name], []byte("%PDF-")) { + t.Errorf("missing PDF %s", name) + } + } + _, hasQR := entries[hostFolder+"QR команд.pdf"] + _, hasExternal := entries[hostFolder+"Внешние материалы.pdf"] + if hasQR != tc.wantQR || hasExternal != tc.wantExternal { + t.Fatalf("QR = %v, external = %v", hasQR, hasExternal) + } + if !tc.wantQR && f.qrCalls != 0 { + t.Fatal("generated QR for an empty team list") + } + if f.story != nil && !tc.wantExternal { + if !bytes.Equal(entries[playerFolder+"Улики/001 - Письмо _ улика.pdf"], f.files["clue.pdf"]) { + t.Fatal("original clue was changed or missing") + } + wantEntries := 9 + if tc.wantQR { + wantEntries++ + } + if len(entries) != wantEntries { + t.Fatalf("unexpected entry count, duplicated clues: %d", len(entries)) + } + } + }) + } +} + +func TestDownloadPrintErrors(t *testing.T) { + broken := errors.New("private storage detail") + for _, tc := range []struct { + name string + change func(*printFixture) + want error + }{ + {"game not found", func(f *printFixture) { f.gameErr = games_repo.ErrGameNotFound }, team_service.ErrGameNotFound}, + {"scenario not found", func(f *printFixture) { f.scenarioErr = scenarios_service.ErrScenarioNotFound }, scenarios_service.ErrScenarioNotFound}, + {"team read error", func(f *printFixture) { f.teamsErr = broken }, broken}, + {"missing clue", func(f *printFixture) { delete(f.files, "clue.pdf") }, file_storage.ErrFileNotFound}, + {"file read error", func(f *printFixture) { f.fileErr = broken }, broken}, + {"unsupported text", func(f *printFixture) { f.game.Name = "Дело 🕵️" }, ErrInvalidPrintText}, + } { + t.Run(tc.name, func(t *testing.T) { + f := newPrintFixture() + tc.change(f) + data, _, err := f.service().Download(context.Background(), 1) + if !errors.Is(err, tc.want) || len(data) != 0 { + t.Fatalf("data = %d bytes, error = %v; want %v", len(data), err, tc.want) + } + }) + } +} + +func TestIssuanceSheetPagination(t *testing.T) { + for _, tc := range []struct{ teams, clues int }{{0, 0}, {1, 1}, {40, 17}} { + t.Run(fmt.Sprintf("%d teams %d clues", tc.teams, tc.clues), func(t *testing.T) { + var teams []*repos.Team + var clues []*storytelling.Application + for i := range tc.teams { + teams = append(teams, &repos.Team{Name: fmt.Sprintf("Команда %d с длинным названием", i+1)}) + } + for i := range tc.clues { + clues = append(clues, &storytelling.Application{Name: fmt.Sprintf("Улика %d с длинной подписью", i+1)}) + } + data, err := issuancePDF("Дело №1", teams, clues) + if err != nil { + t.Fatal(err) + } + pages := bytes.Count(data, []byte("/Type /Page\n")) + if pages < 1 || (tc.clues > 8 && pages < 6) { + t.Fatalf("page count = %d", pages) + } + }) + } +} + +func extractedPDFText(t *testing.T, data []byte) string { + t.Helper() + command := exec.Command("pdftotext", "-", "-") + command.Stdin = bytes.NewReader(data) + text, err := command.CombinedOutput() + if err != nil { + t.Fatalf("extract PDF text: %v: %s", err, text) + } + return string(text) +} + +func TestPrintedContents(t *testing.T) { + if _, err := exec.LookPath("pdftotext"); err != nil { + t.Skip("pdftotext is needed only for optional rendered-content checks") + } + for _, format := range []string{"offline", "online"} { + t.Run(format, func(t *testing.T) { + f := newPrintFixture() + f.game.Format = format + data, _, err := f.service().Download(context.Background(), 1) + if err != nil { + t.Fatal(err) + } + entries := readPrintArchive(t, data) + for name, data := range entries { + if !strings.HasSuffix(name, ".pdf") || strings.Contains(name, "Улики/") { + continue + } + text := extractedPDFText(t, data) + if strings.HasPrefix(name, playerFolder) && !strings.Contains(strings.ToLower(text), strings.ToLower(strings.TrimSuffix(path.Base(name), ".pdf"))) { + t.Fatalf("missing heading in %s", name) + } + if strings.HasPrefix(name, playerFolder) && strings.Contains(text, "СЕКРЕТ") { + t.Fatalf("organizer secrets in %s", name) + } + if strings.Contains(name, "Ответы и развязка") && (!strings.Contains(text, "СЕКРЕТНЫЙ ОТВЕТ") || !strings.Contains(text, "СЕКРЕТНАЯ РАЗВЯЗКА")) { + t.Fatal("missing organizer secrets") + } + if strings.Contains(name, "QR команд") || strings.Contains(name, "Лист выдачи") { + if strings.Contains(text, "Отменённая") || !strings.Contains(text, "Детективы") || !strings.Contains(text, "Ожидающая") { + t.Fatalf("wrong participating teams in %s: %s", name, text) + } + } + if strings.Contains(name, "Правила") { + want := "правила онлайн-игры" + if format == "offline" { + want = "правила очной игры" + } + if !strings.Contains(text, want) || strings.Contains(text, "##") { + t.Fatalf("wrong printed rules: %s", text) + } + } + } + }) + } +} + +func TestPrintCluesPreserveOriginalFiles(t *testing.T) { + for _, extension := range []string{"pdf", "png", "webp", "mp3"} { + t.Run(extension, func(t *testing.T) { + f := newPrintFixture() + original := []byte("original bytes without conversion") + f.story.Places[0].Applications[0].Image = "https://files.test/file." + extension + f.files["file."+extension] = original + data, _, err := f.service().Download(context.Background(), 1) + if err != nil { + t.Fatal(err) + } + entries := readPrintArchive(t, data) + if !bytes.Equal(entries[playerFolder+"Улики/001 - Письмо _ улика."+extension], original) { + t.Fatal("original bytes were changed") + } + }) + } +} + +func TestPrintHonorsCancellation(t *testing.T) { + for _, tc := range []struct { + name string + want error + }{{"cancelled", context.Canceled}} { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, _, err := newPrintFixture().service().Download(ctx, 1) + if !errors.Is(err, tc.want) { + t.Fatalf("error = %v, want %v", err, tc.want) + } + }) + } +} + +func TestPrintRejectsOversizedPackage(t *testing.T) { + for _, tc := range []struct { + name string + size int + }{{"original exceeds package budget", maxPackageSize}} { + t.Run(tc.name, func(t *testing.T) { + f := newPrintFixture() + f.files["clue.pdf"] = make([]byte, tc.size) + data, _, err := f.service().Download(context.Background(), 1) + if !errors.Is(err, ErrPackageTooLarge) || len(data) != 0 { + t.Fatalf("data = %d bytes, error = %v", len(data), err) + } + }) + } +} diff --git a/internal/services/team_service/service.go b/internal/services/team_service/service.go index 2b822f9..9218b3d 100644 --- a/internal/services/team_service/service.go +++ b/internal/services/team_service/service.go @@ -159,6 +159,14 @@ func (s *TeamService) DownloadTeamQR(ctx context.Context, id int) ([]byte, strin if len(teams) == 0 { return nil, "", ErrNoCards } + return s.GenerateTeamQR(ctx, game, teams) +} + +// GenerateTeamQR uses the provided teams so a print package shares one team list. +func (s *TeamService) GenerateTeamQR(ctx context.Context, game *repos.Game, teams []*repos.Team) ([]byte, string, error) { + if err := ctx.Err(); err != nil { + return nil, "", err + } cards, filename, err := teamQRData(s.appBaseURL, game, teams) if err != nil { return nil, "", err diff --git a/proto/main.pb.go b/proto/main.pb.go index b7dfd97..67746c7 100644 --- a/proto/main.pb.go +++ b/proto/main.pb.go @@ -13665,7 +13665,7 @@ const file_main_proto_rawDesc = "" + "\x1aMUSIC_PLAYBACK_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16MUSIC_PLAYBACK_STOPPED\x10\x01\x12\x1a\n" + "\x16MUSIC_PLAYBACK_PLAYING\x10\x02\x12\x19\n" + - "\x15MUSIC_PLAYBACK_PAUSED\x10\x032\xbf\xdc\x01\n" + + "\x15MUSIC_PLAYBACK_PAUSED\x10\x032\xaa\xde\x01\n" + "\x16EveningDetectiveServer\x12\xa9\x04\n" + "\x0fDeviceHeartbeat\x122.crabs.evening_detective_server.DeviceHeartbeatReq\x1a2.crabs.evening_detective_server.DeviceHeartbeatRsp\"\xad\x03\x92A\x88\x03\n" + "\x14Устройства\x12OОбновить присутствие подключения браузера\x1a\x9c\x02Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.b\x00\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/devices/heartbeat\x12\xa1\x04\n" + @@ -13871,7 +13871,9 @@ const file_main_proto_rawDesc = "" + "\x11SetScreenPlanStep\x124.crabs.evening_detective_server.SetScreenPlanStepReq\x1a4.crabs.evening_detective_server.SetScreenPlanStepRsp\"v\x92AD\n" + "\fЭкраны\x124Перейти на шаг плана экранов\x82\xd3\xe4\x93\x02):\x01*\"$/api/games/{gameID}/screen-plan/step\x12\xe9\x01\n" + "\x10GetScreenResults\x123.crabs.evening_detective_server.GetScreenResultsReq\x1a3.crabs.evening_detective_server.GetScreenResultsRsp\"k\x92AI\n" + - "\fЭкраны\x129Получить результаты для экрана\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/screens/results\x12\xcb\x01\n" + + "\fЭкраны\x129Получить результаты для экрана\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/screens/results\x12\xe8\x01\n" + + "\x1aDownloadGamePrintMaterials\x12*.crabs.evening_detective_server.GetGameReq\x1a\x14.google.api.HttpBody\"\x87\x01\x92A]\n" + + "\bИгры\x12QСкачать материалы игры для печати ZIP-архивом\x82\xd3\xe4\x93\x02!\x12\x1f/api/games/{ID}/print-materials\x12\xcb\x01\n" + "\x0eDownloadTeamQR\x12*.crabs.evening_detective_server.GetGameReq\x1a\x14.google.api.HttpBody\"w\x92AP\n" + "\bИгры\x12DСкачать PDF с QR-кодами всех команд игры\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/games/{ID}/teams/qr.pdf\x12\xa4\x02\n" + "\x0eGetGameAnswers\x121.crabs.evening_detective_server.GetGameAnswersReq\x1a1.crabs.evening_detective_server.GetGameAnswersRsp\"\xab\x01\x92A\x84\x01\n" + @@ -14408,145 +14410,147 @@ var file_main_proto_depIdxs = []int32{ 149, // 190: crabs.evening_detective_server.EveningDetectiveServer.SaveScreenPlan:input_type -> crabs.evening_detective_server.SaveScreenPlanReq 151, // 191: crabs.evening_detective_server.EveningDetectiveServer.SetScreenPlanStep:input_type -> crabs.evening_detective_server.SetScreenPlanStepReq 153, // 192: crabs.evening_detective_server.EveningDetectiveServer.GetScreenResults:input_type -> crabs.evening_detective_server.GetScreenResultsReq - 159, // 193: crabs.evening_detective_server.EveningDetectiveServer.DownloadTeamQR:input_type -> crabs.evening_detective_server.GetGameReq - 161, // 194: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:input_type -> crabs.evening_detective_server.GetGameAnswersReq - 163, // 195: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:input_type -> crabs.evening_detective_server.GetGameResultsReq - 168, // 196: crabs.evening_detective_server.EveningDetectiveServer.CreateGameReview:input_type -> crabs.evening_detective_server.CreateGameReviewReq - 170, // 197: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviews:input_type -> crabs.evening_detective_server.GetGameReviewsReq - 171, // 198: crabs.evening_detective_server.EveningDetectiveServer.GetScenarioReviews:input_type -> crabs.evening_detective_server.GetScenarioReviewsReq - 170, // 199: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviewsForModeration:input_type -> crabs.evening_detective_server.GetGameReviewsReq - 172, // 200: crabs.evening_detective_server.EveningDetectiveServer.GetReviewsForModeration:input_type -> crabs.evening_detective_server.GetReviewsForModerationReq - 174, // 201: crabs.evening_detective_server.EveningDetectiveServer.ModerateGameReview:input_type -> crabs.evening_detective_server.ModerateGameReviewReq - 176, // 202: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:input_type -> crabs.evening_detective_server.UpdateGameReq - 178, // 203: crabs.evening_detective_server.EveningDetectiveServer.StartGame:input_type -> crabs.evening_detective_server.StartGameReq - 180, // 204: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:input_type -> crabs.evening_detective_server.PauseGameReq - 182, // 205: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:input_type -> crabs.evening_detective_server.PlayGameReq - 184, // 206: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:input_type -> crabs.evening_detective_server.FinishGameReq - 186, // 207: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:input_type -> crabs.evening_detective_server.ResetGameReq - 188, // 208: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:input_type -> crabs.evening_detective_server.DeleteGameReq - 190, // 209: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:input_type -> crabs.evening_detective_server.AddTeamReq - 192, // 210: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:input_type -> crabs.evening_detective_server.UpdateTeamReq - 194, // 211: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:input_type -> crabs.evening_detective_server.DeleteTeamReq - 196, // 212: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:input_type -> crabs.evening_detective_server.GiveTeamApplicationsReq - 198, // 213: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:input_type -> crabs.evening_detective_server.GetTeamStoryReq - 201, // 214: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:input_type -> crabs.evening_detective_server.AddTeamActionReq - 203, // 215: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:input_type -> crabs.evening_detective_server.DeleteLastTeamActionReq - 205, // 216: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:input_type -> crabs.evening_detective_server.GetTeamAnswersReq - 207, // 217: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:input_type -> crabs.evening_detective_server.SubmitTeamAnswersReq - 209, // 218: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:input_type -> crabs.evening_detective_server.SetTeamAnswerScoreReq - 237, // 219: crabs.evening_detective_server.EveningDetectiveServer.DeviceHeartbeat:output_type -> crabs.evening_detective_server.DeviceHeartbeatRsp - 239, // 220: crabs.evening_detective_server.EveningDetectiveServer.GetDevices:output_type -> crabs.evening_detective_server.GetDevicesRsp - 224, // 221: crabs.evening_detective_server.EveningDetectiveServer.GetTasks:output_type -> crabs.evening_detective_server.GetTasksRsp - 226, // 222: crabs.evening_detective_server.EveningDetectiveServer.GetTaskAssignees:output_type -> crabs.evening_detective_server.GetTaskAssigneesRsp - 231, // 223: crabs.evening_detective_server.EveningDetectiveServer.CreateTask:output_type -> crabs.evening_detective_server.TaskRsp - 231, // 224: crabs.evening_detective_server.EveningDetectiveServer.UpdateTask:output_type -> crabs.evening_detective_server.TaskRsp - 231, // 225: crabs.evening_detective_server.EveningDetectiveServer.SetTaskStatus:output_type -> crabs.evening_detective_server.TaskRsp - 232, // 226: crabs.evening_detective_server.EveningDetectiveServer.DeleteTask:output_type -> crabs.evening_detective_server.DeleteTaskRsp - 157, // 227: crabs.evening_detective_server.EveningDetectiveServer.GetParticipation:output_type -> crabs.evening_detective_server.ParticipationRsp - 157, // 228: crabs.evening_detective_server.EveningDetectiveServer.AnswerParticipation:output_type -> crabs.evening_detective_server.ParticipationRsp - 5, // 229: crabs.evening_detective_server.EveningDetectiveServer.Ping:output_type -> crabs.evening_detective_server.PingRsp - 7, // 230: crabs.evening_detective_server.EveningDetectiveServer.Echo:output_type -> crabs.evening_detective_server.EchoRsp - 9, // 231: crabs.evening_detective_server.EveningDetectiveServer.Signup:output_type -> crabs.evening_detective_server.SignupRsp - 11, // 232: crabs.evening_detective_server.EveningDetectiveServer.RefreshPassword:output_type -> crabs.evening_detective_server.RefreshPasswordRsp - 13, // 233: crabs.evening_detective_server.EveningDetectiveServer.Login:output_type -> crabs.evening_detective_server.LoginRsp - 15, // 234: crabs.evening_detective_server.EveningDetectiveServer.Refresh:output_type -> crabs.evening_detective_server.RefreshRsp - 17, // 235: crabs.evening_detective_server.EveningDetectiveServer.DeleteAccount:output_type -> crabs.evening_detective_server.DeleteAccountRsp - 19, // 236: crabs.evening_detective_server.EveningDetectiveServer.GetTerms:output_type -> crabs.evening_detective_server.GetTermsRsp - 21, // 237: crabs.evening_detective_server.EveningDetectiveServer.GetPrivacy:output_type -> crabs.evening_detective_server.GetPrivacyRsp - 23, // 238: crabs.evening_detective_server.EveningDetectiveServer.GetConsent:output_type -> crabs.evening_detective_server.GetConsentRsp - 25, // 239: crabs.evening_detective_server.EveningDetectiveServer.GetUsers:output_type -> crabs.evening_detective_server.GetUsersRsp - 28, // 240: crabs.evening_detective_server.EveningDetectiveServer.GetUserByID:output_type -> crabs.evening_detective_server.GetUserByIDRsp - 30, // 241: crabs.evening_detective_server.EveningDetectiveServer.GetMe:output_type -> crabs.evening_detective_server.GetMeRsp - 32, // 242: crabs.evening_detective_server.EveningDetectiveServer.ChangeUserPassword:output_type -> crabs.evening_detective_server.ChangeUserPasswordRsp - 34, // 243: crabs.evening_detective_server.EveningDetectiveServer.AddUserRole:output_type -> crabs.evening_detective_server.AddUserRoleRsp - 36, // 244: crabs.evening_detective_server.EveningDetectiveServer.DeleteUserRole:output_type -> crabs.evening_detective_server.DeleteUserRoleRsp - 38, // 245: crabs.evening_detective_server.EveningDetectiveServer.GetPermissions:output_type -> crabs.evening_detective_server.GetPermissionsRsp - 40, // 246: crabs.evening_detective_server.EveningDetectiveServer.UploadFile:output_type -> crabs.evening_detective_server.UploadFileRsp - 245, // 247: crabs.evening_detective_server.EveningDetectiveServer.DownloadFile:output_type -> google.api.HttpBody - 45, // 248: crabs.evening_detective_server.EveningDetectiveServer.GetPlaylists:output_type -> crabs.evening_detective_server.GetPlaylistsRsp - 47, // 249: crabs.evening_detective_server.EveningDetectiveServer.CreatePlaylist:output_type -> crabs.evening_detective_server.CreatePlaylistRsp - 49, // 250: crabs.evening_detective_server.EveningDetectiveServer.UpdatePlaylist:output_type -> crabs.evening_detective_server.UpdatePlaylistRsp - 51, // 251: crabs.evening_detective_server.EveningDetectiveServer.DeletePlaylist:output_type -> crabs.evening_detective_server.DeletePlaylistRsp - 53, // 252: crabs.evening_detective_server.EveningDetectiveServer.UploadPlaylistTrack:output_type -> crabs.evening_detective_server.UploadPlaylistTrackRsp - 55, // 253: crabs.evening_detective_server.EveningDetectiveServer.DeletePlaylistTrack:output_type -> crabs.evening_detective_server.DeletePlaylistTrackRsp - 57, // 254: crabs.evening_detective_server.EveningDetectiveServer.SetScenarioPlaylist:output_type -> crabs.evening_detective_server.SetScenarioPlaylistRsp - 59, // 255: crabs.evening_detective_server.EveningDetectiveServer.AddScenario:output_type -> crabs.evening_detective_server.AddScenarioRsp - 61, // 256: crabs.evening_detective_server.EveningDetectiveServer.GetMyScenarios:output_type -> crabs.evening_detective_server.GetMyScenariosRsp - 63, // 257: crabs.evening_detective_server.EveningDetectiveServer.GetScenariosCatalog:output_type -> crabs.evening_detective_server.GetScenariosCatalogRsp - 65, // 258: crabs.evening_detective_server.EveningDetectiveServer.GetFullScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp - 65, // 259: crabs.evening_detective_server.EveningDetectiveServer.GetScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp - 79, // 260: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenario:output_type -> crabs.evening_detective_server.UpdateScenarioRsp - 81, // 261: crabs.evening_detective_server.EveningDetectiveServer.PublicScenario:output_type -> crabs.evening_detective_server.PublicScenarioRsp - 83, // 262: crabs.evening_detective_server.EveningDetectiveServer.DraftScenario:output_type -> crabs.evening_detective_server.DraftScenarioRsp - 85, // 263: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenario:output_type -> crabs.evening_detective_server.DeleteScenarioRsp - 87, // 264: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioPlace:output_type -> crabs.evening_detective_server.AddScenarioPlaceRsp - 89, // 265: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioPlace:output_type -> crabs.evening_detective_server.UpdateScenarioPlaceRsp - 91, // 266: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioPlace:output_type -> crabs.evening_detective_server.DeleteScenarioPlaceRsp - 97, // 267: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioQuestion:output_type -> crabs.evening_detective_server.AddScenarioQuestionRsp - 99, // 268: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioQuestion:output_type -> crabs.evening_detective_server.UpdateScenarioQuestionRsp - 101, // 269: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioQuestion:output_type -> crabs.evening_detective_server.DeleteScenarioQuestionRsp - 212, // 270: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioBackground:output_type -> crabs.evening_detective_server.UpdateScenarioBackgroundRsp - 95, // 271: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioIntro:output_type -> crabs.evening_detective_server.UpdateScenarioIntroRsp - 93, // 272: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioConclusion:output_type -> crabs.evening_detective_server.UpdateScenarioConclusionRsp - 245, // 273: crabs.evening_detective_server.EveningDetectiveServer.DownloadScenarioArchive:output_type -> google.api.HttpBody - 103, // 274: crabs.evening_detective_server.EveningDetectiveServer.UploadScenarioArchive:output_type -> crabs.evening_detective_server.UploadScenarioArchiveRsp - 105, // 275: crabs.evening_detective_server.EveningDetectiveServer.AddGame:output_type -> crabs.evening_detective_server.AddGameRsp - 107, // 276: crabs.evening_detective_server.EveningDetectiveServer.GetGames:output_type -> crabs.evening_detective_server.GetGamesRsp - 160, // 277: crabs.evening_detective_server.EveningDetectiveServer.GetGame:output_type -> crabs.evening_detective_server.GetGameRsp - 110, // 278: crabs.evening_detective_server.EveningDetectiveServer.ImportGame:output_type -> crabs.evening_detective_server.GameImportRsp - 245, // 279: crabs.evening_detective_server.EveningDetectiveServer.DownloadGameResultsPackage:output_type -> google.api.HttpBody - 235, // 280: crabs.evening_detective_server.EveningDetectiveServer.ApplyGameResultsPackage:output_type -> crabs.evening_detective_server.GameResultsPackageRsp - 113, // 281: crabs.evening_detective_server.EveningDetectiveServer.StartGameExport:output_type -> crabs.evening_detective_server.GameExportRsp - 113, // 282: crabs.evening_detective_server.EveningDetectiveServer.GetGameExport:output_type -> crabs.evening_detective_server.GameExportRsp - 214, // 283: crabs.evening_detective_server.EveningDetectiveServer.IssueGameTickets:output_type -> crabs.evening_detective_server.IssueGameTicketsRsp - 217, // 284: crabs.evening_detective_server.EveningDetectiveServer.GetTicket:output_type -> crabs.evening_detective_server.GetTicketRsp - 219, // 285: crabs.evening_detective_server.EveningDetectiveServer.AcceptTicket:output_type -> crabs.evening_detective_server.AcceptTicketRsp - 116, // 286: crabs.evening_detective_server.EveningDetectiveServer.CreateScreenSession:output_type -> crabs.evening_detective_server.CreateScreenSessionRsp - 118, // 287: crabs.evening_detective_server.EveningDetectiveServer.GetScreenState:output_type -> crabs.evening_detective_server.GetScreenStateRsp - 120, // 288: crabs.evening_detective_server.EveningDetectiveServer.GetGameScreens:output_type -> crabs.evening_detective_server.GetGameScreensRsp - 123, // 289: crabs.evening_detective_server.EveningDetectiveServer.GetGameMusic:output_type -> crabs.evening_detective_server.GetGameMusicRsp - 125, // 290: crabs.evening_detective_server.EveningDetectiveServer.SetGameMusic:output_type -> crabs.evening_detective_server.SetGameMusicRsp - 127, // 291: crabs.evening_detective_server.EveningDetectiveServer.AdvanceScreenMusic:output_type -> crabs.evening_detective_server.AdvanceScreenMusicRsp - 129, // 292: crabs.evening_detective_server.EveningDetectiveServer.BindScreen:output_type -> crabs.evening_detective_server.BindScreenRsp - 131, // 293: crabs.evening_detective_server.EveningDetectiveServer.UnbindScreen:output_type -> crabs.evening_detective_server.UnbindScreenRsp - 133, // 294: crabs.evening_detective_server.EveningDetectiveServer.SetScreenSound:output_type -> crabs.evening_detective_server.SetScreenSoundRsp - 136, // 295: crabs.evening_detective_server.EveningDetectiveServer.GetGameMap:output_type -> crabs.evening_detective_server.GetGameMapRsp - 138, // 296: crabs.evening_detective_server.EveningDetectiveServer.UpdateGameMapItem:output_type -> crabs.evening_detective_server.UpdateGameMapItemRsp - 140, // 297: crabs.evening_detective_server.EveningDetectiveServer.SetScreenMode:output_type -> crabs.evening_detective_server.SetScreenModeRsp - 142, // 298: crabs.evening_detective_server.EveningDetectiveServer.SetScreenTimer:output_type -> crabs.evening_detective_server.SetScreenTimerRsp - 144, // 299: crabs.evening_detective_server.EveningDetectiveServer.SendScreenCommand:output_type -> crabs.evening_detective_server.SendScreenCommandRsp - 148, // 300: crabs.evening_detective_server.EveningDetectiveServer.GetScreenPlan:output_type -> crabs.evening_detective_server.GetScreenPlanRsp - 150, // 301: crabs.evening_detective_server.EveningDetectiveServer.SaveScreenPlan:output_type -> crabs.evening_detective_server.SaveScreenPlanRsp - 152, // 302: crabs.evening_detective_server.EveningDetectiveServer.SetScreenPlanStep:output_type -> crabs.evening_detective_server.SetScreenPlanStepRsp - 154, // 303: crabs.evening_detective_server.EveningDetectiveServer.GetScreenResults:output_type -> crabs.evening_detective_server.GetScreenResultsRsp - 245, // 304: crabs.evening_detective_server.EveningDetectiveServer.DownloadTeamQR:output_type -> google.api.HttpBody - 162, // 305: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:output_type -> crabs.evening_detective_server.GetGameAnswersRsp - 166, // 306: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:output_type -> crabs.evening_detective_server.GetGameResultsRsp - 169, // 307: crabs.evening_detective_server.EveningDetectiveServer.CreateGameReview:output_type -> crabs.evening_detective_server.CreateGameReviewRsp - 173, // 308: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviews:output_type -> crabs.evening_detective_server.GetGameReviewsRsp - 173, // 309: crabs.evening_detective_server.EveningDetectiveServer.GetScenarioReviews:output_type -> crabs.evening_detective_server.GetGameReviewsRsp - 173, // 310: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviewsForModeration:output_type -> crabs.evening_detective_server.GetGameReviewsRsp - 173, // 311: crabs.evening_detective_server.EveningDetectiveServer.GetReviewsForModeration:output_type -> crabs.evening_detective_server.GetGameReviewsRsp - 175, // 312: crabs.evening_detective_server.EveningDetectiveServer.ModerateGameReview:output_type -> crabs.evening_detective_server.ModerateGameReviewRsp - 177, // 313: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:output_type -> crabs.evening_detective_server.UpdateGameRsp - 179, // 314: crabs.evening_detective_server.EveningDetectiveServer.StartGame:output_type -> crabs.evening_detective_server.StartGameRsp - 181, // 315: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:output_type -> crabs.evening_detective_server.PauseGameRsp - 183, // 316: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:output_type -> crabs.evening_detective_server.PlayGameRsp - 185, // 317: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:output_type -> crabs.evening_detective_server.FinishGameRsp - 187, // 318: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:output_type -> crabs.evening_detective_server.ResetGameRsp - 189, // 319: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:output_type -> crabs.evening_detective_server.DeleteGameRsp - 191, // 320: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:output_type -> crabs.evening_detective_server.AddTeamRsp - 193, // 321: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:output_type -> crabs.evening_detective_server.UpdateTeamRsp - 195, // 322: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:output_type -> crabs.evening_detective_server.DeleteTeamRsp - 197, // 323: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:output_type -> crabs.evening_detective_server.GiveTeamApplicationsRsp - 199, // 324: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:output_type -> crabs.evening_detective_server.GetTeamStoryRsp - 202, // 325: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:output_type -> crabs.evening_detective_server.AddTeamActionRsp - 204, // 326: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:output_type -> crabs.evening_detective_server.DeleteLastTeamActionRsp - 206, // 327: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:output_type -> crabs.evening_detective_server.GetTeamAnswersRsp - 208, // 328: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:output_type -> crabs.evening_detective_server.SubmitTeamAnswersRsp - 210, // 329: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:output_type -> crabs.evening_detective_server.SetTeamAnswerScoreRsp - 219, // [219:330] is the sub-list for method output_type - 108, // [108:219] is the sub-list for method input_type + 159, // 193: crabs.evening_detective_server.EveningDetectiveServer.DownloadGamePrintMaterials:input_type -> crabs.evening_detective_server.GetGameReq + 159, // 194: crabs.evening_detective_server.EveningDetectiveServer.DownloadTeamQR:input_type -> crabs.evening_detective_server.GetGameReq + 161, // 195: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:input_type -> crabs.evening_detective_server.GetGameAnswersReq + 163, // 196: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:input_type -> crabs.evening_detective_server.GetGameResultsReq + 168, // 197: crabs.evening_detective_server.EveningDetectiveServer.CreateGameReview:input_type -> crabs.evening_detective_server.CreateGameReviewReq + 170, // 198: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviews:input_type -> crabs.evening_detective_server.GetGameReviewsReq + 171, // 199: crabs.evening_detective_server.EveningDetectiveServer.GetScenarioReviews:input_type -> crabs.evening_detective_server.GetScenarioReviewsReq + 170, // 200: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviewsForModeration:input_type -> crabs.evening_detective_server.GetGameReviewsReq + 172, // 201: crabs.evening_detective_server.EveningDetectiveServer.GetReviewsForModeration:input_type -> crabs.evening_detective_server.GetReviewsForModerationReq + 174, // 202: crabs.evening_detective_server.EveningDetectiveServer.ModerateGameReview:input_type -> crabs.evening_detective_server.ModerateGameReviewReq + 176, // 203: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:input_type -> crabs.evening_detective_server.UpdateGameReq + 178, // 204: crabs.evening_detective_server.EveningDetectiveServer.StartGame:input_type -> crabs.evening_detective_server.StartGameReq + 180, // 205: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:input_type -> crabs.evening_detective_server.PauseGameReq + 182, // 206: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:input_type -> crabs.evening_detective_server.PlayGameReq + 184, // 207: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:input_type -> crabs.evening_detective_server.FinishGameReq + 186, // 208: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:input_type -> crabs.evening_detective_server.ResetGameReq + 188, // 209: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:input_type -> crabs.evening_detective_server.DeleteGameReq + 190, // 210: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:input_type -> crabs.evening_detective_server.AddTeamReq + 192, // 211: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:input_type -> crabs.evening_detective_server.UpdateTeamReq + 194, // 212: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:input_type -> crabs.evening_detective_server.DeleteTeamReq + 196, // 213: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:input_type -> crabs.evening_detective_server.GiveTeamApplicationsReq + 198, // 214: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:input_type -> crabs.evening_detective_server.GetTeamStoryReq + 201, // 215: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:input_type -> crabs.evening_detective_server.AddTeamActionReq + 203, // 216: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:input_type -> crabs.evening_detective_server.DeleteLastTeamActionReq + 205, // 217: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:input_type -> crabs.evening_detective_server.GetTeamAnswersReq + 207, // 218: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:input_type -> crabs.evening_detective_server.SubmitTeamAnswersReq + 209, // 219: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:input_type -> crabs.evening_detective_server.SetTeamAnswerScoreReq + 237, // 220: crabs.evening_detective_server.EveningDetectiveServer.DeviceHeartbeat:output_type -> crabs.evening_detective_server.DeviceHeartbeatRsp + 239, // 221: crabs.evening_detective_server.EveningDetectiveServer.GetDevices:output_type -> crabs.evening_detective_server.GetDevicesRsp + 224, // 222: crabs.evening_detective_server.EveningDetectiveServer.GetTasks:output_type -> crabs.evening_detective_server.GetTasksRsp + 226, // 223: crabs.evening_detective_server.EveningDetectiveServer.GetTaskAssignees:output_type -> crabs.evening_detective_server.GetTaskAssigneesRsp + 231, // 224: crabs.evening_detective_server.EveningDetectiveServer.CreateTask:output_type -> crabs.evening_detective_server.TaskRsp + 231, // 225: crabs.evening_detective_server.EveningDetectiveServer.UpdateTask:output_type -> crabs.evening_detective_server.TaskRsp + 231, // 226: crabs.evening_detective_server.EveningDetectiveServer.SetTaskStatus:output_type -> crabs.evening_detective_server.TaskRsp + 232, // 227: crabs.evening_detective_server.EveningDetectiveServer.DeleteTask:output_type -> crabs.evening_detective_server.DeleteTaskRsp + 157, // 228: crabs.evening_detective_server.EveningDetectiveServer.GetParticipation:output_type -> crabs.evening_detective_server.ParticipationRsp + 157, // 229: crabs.evening_detective_server.EveningDetectiveServer.AnswerParticipation:output_type -> crabs.evening_detective_server.ParticipationRsp + 5, // 230: crabs.evening_detective_server.EveningDetectiveServer.Ping:output_type -> crabs.evening_detective_server.PingRsp + 7, // 231: crabs.evening_detective_server.EveningDetectiveServer.Echo:output_type -> crabs.evening_detective_server.EchoRsp + 9, // 232: crabs.evening_detective_server.EveningDetectiveServer.Signup:output_type -> crabs.evening_detective_server.SignupRsp + 11, // 233: crabs.evening_detective_server.EveningDetectiveServer.RefreshPassword:output_type -> crabs.evening_detective_server.RefreshPasswordRsp + 13, // 234: crabs.evening_detective_server.EveningDetectiveServer.Login:output_type -> crabs.evening_detective_server.LoginRsp + 15, // 235: crabs.evening_detective_server.EveningDetectiveServer.Refresh:output_type -> crabs.evening_detective_server.RefreshRsp + 17, // 236: crabs.evening_detective_server.EveningDetectiveServer.DeleteAccount:output_type -> crabs.evening_detective_server.DeleteAccountRsp + 19, // 237: crabs.evening_detective_server.EveningDetectiveServer.GetTerms:output_type -> crabs.evening_detective_server.GetTermsRsp + 21, // 238: crabs.evening_detective_server.EveningDetectiveServer.GetPrivacy:output_type -> crabs.evening_detective_server.GetPrivacyRsp + 23, // 239: crabs.evening_detective_server.EveningDetectiveServer.GetConsent:output_type -> crabs.evening_detective_server.GetConsentRsp + 25, // 240: crabs.evening_detective_server.EveningDetectiveServer.GetUsers:output_type -> crabs.evening_detective_server.GetUsersRsp + 28, // 241: crabs.evening_detective_server.EveningDetectiveServer.GetUserByID:output_type -> crabs.evening_detective_server.GetUserByIDRsp + 30, // 242: crabs.evening_detective_server.EveningDetectiveServer.GetMe:output_type -> crabs.evening_detective_server.GetMeRsp + 32, // 243: crabs.evening_detective_server.EveningDetectiveServer.ChangeUserPassword:output_type -> crabs.evening_detective_server.ChangeUserPasswordRsp + 34, // 244: crabs.evening_detective_server.EveningDetectiveServer.AddUserRole:output_type -> crabs.evening_detective_server.AddUserRoleRsp + 36, // 245: crabs.evening_detective_server.EveningDetectiveServer.DeleteUserRole:output_type -> crabs.evening_detective_server.DeleteUserRoleRsp + 38, // 246: crabs.evening_detective_server.EveningDetectiveServer.GetPermissions:output_type -> crabs.evening_detective_server.GetPermissionsRsp + 40, // 247: crabs.evening_detective_server.EveningDetectiveServer.UploadFile:output_type -> crabs.evening_detective_server.UploadFileRsp + 245, // 248: crabs.evening_detective_server.EveningDetectiveServer.DownloadFile:output_type -> google.api.HttpBody + 45, // 249: crabs.evening_detective_server.EveningDetectiveServer.GetPlaylists:output_type -> crabs.evening_detective_server.GetPlaylistsRsp + 47, // 250: crabs.evening_detective_server.EveningDetectiveServer.CreatePlaylist:output_type -> crabs.evening_detective_server.CreatePlaylistRsp + 49, // 251: crabs.evening_detective_server.EveningDetectiveServer.UpdatePlaylist:output_type -> crabs.evening_detective_server.UpdatePlaylistRsp + 51, // 252: crabs.evening_detective_server.EveningDetectiveServer.DeletePlaylist:output_type -> crabs.evening_detective_server.DeletePlaylistRsp + 53, // 253: crabs.evening_detective_server.EveningDetectiveServer.UploadPlaylistTrack:output_type -> crabs.evening_detective_server.UploadPlaylistTrackRsp + 55, // 254: crabs.evening_detective_server.EveningDetectiveServer.DeletePlaylistTrack:output_type -> crabs.evening_detective_server.DeletePlaylistTrackRsp + 57, // 255: crabs.evening_detective_server.EveningDetectiveServer.SetScenarioPlaylist:output_type -> crabs.evening_detective_server.SetScenarioPlaylistRsp + 59, // 256: crabs.evening_detective_server.EveningDetectiveServer.AddScenario:output_type -> crabs.evening_detective_server.AddScenarioRsp + 61, // 257: crabs.evening_detective_server.EveningDetectiveServer.GetMyScenarios:output_type -> crabs.evening_detective_server.GetMyScenariosRsp + 63, // 258: crabs.evening_detective_server.EveningDetectiveServer.GetScenariosCatalog:output_type -> crabs.evening_detective_server.GetScenariosCatalogRsp + 65, // 259: crabs.evening_detective_server.EveningDetectiveServer.GetFullScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp + 65, // 260: crabs.evening_detective_server.EveningDetectiveServer.GetScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp + 79, // 261: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenario:output_type -> crabs.evening_detective_server.UpdateScenarioRsp + 81, // 262: crabs.evening_detective_server.EveningDetectiveServer.PublicScenario:output_type -> crabs.evening_detective_server.PublicScenarioRsp + 83, // 263: crabs.evening_detective_server.EveningDetectiveServer.DraftScenario:output_type -> crabs.evening_detective_server.DraftScenarioRsp + 85, // 264: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenario:output_type -> crabs.evening_detective_server.DeleteScenarioRsp + 87, // 265: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioPlace:output_type -> crabs.evening_detective_server.AddScenarioPlaceRsp + 89, // 266: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioPlace:output_type -> crabs.evening_detective_server.UpdateScenarioPlaceRsp + 91, // 267: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioPlace:output_type -> crabs.evening_detective_server.DeleteScenarioPlaceRsp + 97, // 268: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioQuestion:output_type -> crabs.evening_detective_server.AddScenarioQuestionRsp + 99, // 269: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioQuestion:output_type -> crabs.evening_detective_server.UpdateScenarioQuestionRsp + 101, // 270: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioQuestion:output_type -> crabs.evening_detective_server.DeleteScenarioQuestionRsp + 212, // 271: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioBackground:output_type -> crabs.evening_detective_server.UpdateScenarioBackgroundRsp + 95, // 272: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioIntro:output_type -> crabs.evening_detective_server.UpdateScenarioIntroRsp + 93, // 273: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioConclusion:output_type -> crabs.evening_detective_server.UpdateScenarioConclusionRsp + 245, // 274: crabs.evening_detective_server.EveningDetectiveServer.DownloadScenarioArchive:output_type -> google.api.HttpBody + 103, // 275: crabs.evening_detective_server.EveningDetectiveServer.UploadScenarioArchive:output_type -> crabs.evening_detective_server.UploadScenarioArchiveRsp + 105, // 276: crabs.evening_detective_server.EveningDetectiveServer.AddGame:output_type -> crabs.evening_detective_server.AddGameRsp + 107, // 277: crabs.evening_detective_server.EveningDetectiveServer.GetGames:output_type -> crabs.evening_detective_server.GetGamesRsp + 160, // 278: crabs.evening_detective_server.EveningDetectiveServer.GetGame:output_type -> crabs.evening_detective_server.GetGameRsp + 110, // 279: crabs.evening_detective_server.EveningDetectiveServer.ImportGame:output_type -> crabs.evening_detective_server.GameImportRsp + 245, // 280: crabs.evening_detective_server.EveningDetectiveServer.DownloadGameResultsPackage:output_type -> google.api.HttpBody + 235, // 281: crabs.evening_detective_server.EveningDetectiveServer.ApplyGameResultsPackage:output_type -> crabs.evening_detective_server.GameResultsPackageRsp + 113, // 282: crabs.evening_detective_server.EveningDetectiveServer.StartGameExport:output_type -> crabs.evening_detective_server.GameExportRsp + 113, // 283: crabs.evening_detective_server.EveningDetectiveServer.GetGameExport:output_type -> crabs.evening_detective_server.GameExportRsp + 214, // 284: crabs.evening_detective_server.EveningDetectiveServer.IssueGameTickets:output_type -> crabs.evening_detective_server.IssueGameTicketsRsp + 217, // 285: crabs.evening_detective_server.EveningDetectiveServer.GetTicket:output_type -> crabs.evening_detective_server.GetTicketRsp + 219, // 286: crabs.evening_detective_server.EveningDetectiveServer.AcceptTicket:output_type -> crabs.evening_detective_server.AcceptTicketRsp + 116, // 287: crabs.evening_detective_server.EveningDetectiveServer.CreateScreenSession:output_type -> crabs.evening_detective_server.CreateScreenSessionRsp + 118, // 288: crabs.evening_detective_server.EveningDetectiveServer.GetScreenState:output_type -> crabs.evening_detective_server.GetScreenStateRsp + 120, // 289: crabs.evening_detective_server.EveningDetectiveServer.GetGameScreens:output_type -> crabs.evening_detective_server.GetGameScreensRsp + 123, // 290: crabs.evening_detective_server.EveningDetectiveServer.GetGameMusic:output_type -> crabs.evening_detective_server.GetGameMusicRsp + 125, // 291: crabs.evening_detective_server.EveningDetectiveServer.SetGameMusic:output_type -> crabs.evening_detective_server.SetGameMusicRsp + 127, // 292: crabs.evening_detective_server.EveningDetectiveServer.AdvanceScreenMusic:output_type -> crabs.evening_detective_server.AdvanceScreenMusicRsp + 129, // 293: crabs.evening_detective_server.EveningDetectiveServer.BindScreen:output_type -> crabs.evening_detective_server.BindScreenRsp + 131, // 294: crabs.evening_detective_server.EveningDetectiveServer.UnbindScreen:output_type -> crabs.evening_detective_server.UnbindScreenRsp + 133, // 295: crabs.evening_detective_server.EveningDetectiveServer.SetScreenSound:output_type -> crabs.evening_detective_server.SetScreenSoundRsp + 136, // 296: crabs.evening_detective_server.EveningDetectiveServer.GetGameMap:output_type -> crabs.evening_detective_server.GetGameMapRsp + 138, // 297: crabs.evening_detective_server.EveningDetectiveServer.UpdateGameMapItem:output_type -> crabs.evening_detective_server.UpdateGameMapItemRsp + 140, // 298: crabs.evening_detective_server.EveningDetectiveServer.SetScreenMode:output_type -> crabs.evening_detective_server.SetScreenModeRsp + 142, // 299: crabs.evening_detective_server.EveningDetectiveServer.SetScreenTimer:output_type -> crabs.evening_detective_server.SetScreenTimerRsp + 144, // 300: crabs.evening_detective_server.EveningDetectiveServer.SendScreenCommand:output_type -> crabs.evening_detective_server.SendScreenCommandRsp + 148, // 301: crabs.evening_detective_server.EveningDetectiveServer.GetScreenPlan:output_type -> crabs.evening_detective_server.GetScreenPlanRsp + 150, // 302: crabs.evening_detective_server.EveningDetectiveServer.SaveScreenPlan:output_type -> crabs.evening_detective_server.SaveScreenPlanRsp + 152, // 303: crabs.evening_detective_server.EveningDetectiveServer.SetScreenPlanStep:output_type -> crabs.evening_detective_server.SetScreenPlanStepRsp + 154, // 304: crabs.evening_detective_server.EveningDetectiveServer.GetScreenResults:output_type -> crabs.evening_detective_server.GetScreenResultsRsp + 245, // 305: crabs.evening_detective_server.EveningDetectiveServer.DownloadGamePrintMaterials:output_type -> google.api.HttpBody + 245, // 306: crabs.evening_detective_server.EveningDetectiveServer.DownloadTeamQR:output_type -> google.api.HttpBody + 162, // 307: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:output_type -> crabs.evening_detective_server.GetGameAnswersRsp + 166, // 308: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:output_type -> crabs.evening_detective_server.GetGameResultsRsp + 169, // 309: crabs.evening_detective_server.EveningDetectiveServer.CreateGameReview:output_type -> crabs.evening_detective_server.CreateGameReviewRsp + 173, // 310: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviews:output_type -> crabs.evening_detective_server.GetGameReviewsRsp + 173, // 311: crabs.evening_detective_server.EveningDetectiveServer.GetScenarioReviews:output_type -> crabs.evening_detective_server.GetGameReviewsRsp + 173, // 312: crabs.evening_detective_server.EveningDetectiveServer.GetGameReviewsForModeration:output_type -> crabs.evening_detective_server.GetGameReviewsRsp + 173, // 313: crabs.evening_detective_server.EveningDetectiveServer.GetReviewsForModeration:output_type -> crabs.evening_detective_server.GetGameReviewsRsp + 175, // 314: crabs.evening_detective_server.EveningDetectiveServer.ModerateGameReview:output_type -> crabs.evening_detective_server.ModerateGameReviewRsp + 177, // 315: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:output_type -> crabs.evening_detective_server.UpdateGameRsp + 179, // 316: crabs.evening_detective_server.EveningDetectiveServer.StartGame:output_type -> crabs.evening_detective_server.StartGameRsp + 181, // 317: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:output_type -> crabs.evening_detective_server.PauseGameRsp + 183, // 318: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:output_type -> crabs.evening_detective_server.PlayGameRsp + 185, // 319: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:output_type -> crabs.evening_detective_server.FinishGameRsp + 187, // 320: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:output_type -> crabs.evening_detective_server.ResetGameRsp + 189, // 321: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:output_type -> crabs.evening_detective_server.DeleteGameRsp + 191, // 322: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:output_type -> crabs.evening_detective_server.AddTeamRsp + 193, // 323: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:output_type -> crabs.evening_detective_server.UpdateTeamRsp + 195, // 324: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:output_type -> crabs.evening_detective_server.DeleteTeamRsp + 197, // 325: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:output_type -> crabs.evening_detective_server.GiveTeamApplicationsRsp + 199, // 326: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:output_type -> crabs.evening_detective_server.GetTeamStoryRsp + 202, // 327: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:output_type -> crabs.evening_detective_server.AddTeamActionRsp + 204, // 328: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:output_type -> crabs.evening_detective_server.DeleteLastTeamActionRsp + 206, // 329: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:output_type -> crabs.evening_detective_server.GetTeamAnswersRsp + 208, // 330: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:output_type -> crabs.evening_detective_server.SubmitTeamAnswersRsp + 210, // 331: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:output_type -> crabs.evening_detective_server.SetTeamAnswerScoreRsp + 220, // [220:332] is the sub-list for method output_type + 108, // [108:220] is the sub-list for method input_type 108, // [108:108] is the sub-list for extension type_name 108, // [108:108] is the sub-list for extension extendee 0, // [0:108] is the sub-list for field type_name diff --git a/proto/main.pb.gw.go b/proto/main.pb.gw.go index 39afd83..bd3c8d9 100644 --- a/proto/main.pb.gw.go +++ b/proto/main.pb.gw.go @@ -3185,6 +3185,45 @@ func local_request_EveningDetectiveServer_GetScreenResults_0(ctx context.Context return msg, metadata, err } +func request_EveningDetectiveServer_DownloadGamePrintMaterials_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetGameReq + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["ID"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID") + } + protoReq.ID, err = runtime.Int32(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err) + } + msg, err := client.DownloadGamePrintMaterials(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_EveningDetectiveServer_DownloadGamePrintMaterials_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetGameReq + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["ID"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "ID") + } + protoReq.ID, err = runtime.Int32(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "ID", err) + } + msg, err := server.DownloadGamePrintMaterials(ctx, &protoReq) + return msg, metadata, err +} + func request_EveningDetectiveServer_DownloadTeamQR_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetGameReq @@ -5997,6 +6036,26 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti } forward_EveningDetectiveServer_GetScreenResults_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_DownloadGamePrintMaterials_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGamePrintMaterials", runtime.WithHTTPPathPattern("/api/games/{ID}/print-materials")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_EveningDetectiveServer_DownloadGamePrintMaterials_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_EveningDetectiveServer_DownloadGamePrintMaterials_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_DownloadTeamQR_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -8002,6 +8061,23 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti } forward_EveningDetectiveServer_GetScreenResults_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_DownloadGamePrintMaterials_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGamePrintMaterials", runtime.WithHTTPPathPattern("/api/games/{ID}/print-materials")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_EveningDetectiveServer_DownloadGamePrintMaterials_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_EveningDetectiveServer_DownloadGamePrintMaterials_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_DownloadTeamQR_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -8533,6 +8609,7 @@ var ( pattern_EveningDetectiveServer_SaveScreenPlan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "screen-plan"}, "")) pattern_EveningDetectiveServer_SetScreenPlanStep_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "gameID", "screen-plan", "step"}, "")) pattern_EveningDetectiveServer_GetScreenResults_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "screens", "results"}, "")) + pattern_EveningDetectiveServer_DownloadGamePrintMaterials_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "print-materials"}, "")) pattern_EveningDetectiveServer_DownloadTeamQR_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "ID", "teams", "qr.pdf"}, "")) pattern_EveningDetectiveServer_GetGameAnswers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "answers"}, "")) pattern_EveningDetectiveServer_GetGameResults_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "results"}, "")) @@ -8647,6 +8724,7 @@ var ( forward_EveningDetectiveServer_SaveScreenPlan_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_SetScreenPlanStep_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetScreenResults_0 = runtime.ForwardResponseMessage + forward_EveningDetectiveServer_DownloadGamePrintMaterials_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_DownloadTeamQR_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetGameAnswers_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetGameResults_0 = runtime.ForwardResponseMessage diff --git a/proto/main_grpc.pb.go b/proto/main_grpc.pb.go index 7a91f95..a061911 100644 --- a/proto/main_grpc.pb.go +++ b/proto/main_grpc.pb.go @@ -111,6 +111,7 @@ const ( EveningDetectiveServer_SaveScreenPlan_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SaveScreenPlan" EveningDetectiveServer_SetScreenPlanStep_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SetScreenPlanStep" EveningDetectiveServer_GetScreenResults_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenResults" + EveningDetectiveServer_DownloadGamePrintMaterials_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGamePrintMaterials" EveningDetectiveServer_DownloadTeamQR_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadTeamQR" EveningDetectiveServer_GetGameAnswers_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGameAnswers" EveningDetectiveServer_GetGameResults_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGameResults" @@ -240,6 +241,7 @@ type EveningDetectiveServerClient interface { SaveScreenPlan(ctx context.Context, in *SaveScreenPlanReq, opts ...grpc.CallOption) (*SaveScreenPlanRsp, error) SetScreenPlanStep(ctx context.Context, in *SetScreenPlanStepReq, opts ...grpc.CallOption) (*SetScreenPlanStepRsp, error) GetScreenResults(ctx context.Context, in *GetScreenResultsReq, opts ...grpc.CallOption) (*GetScreenResultsRsp, error) + DownloadGamePrintMaterials(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) DownloadTeamQR(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) GetGameAnswers(ctx context.Context, in *GetGameAnswersReq, opts ...grpc.CallOption) (*GetGameAnswersRsp, error) GetGameResults(ctx context.Context, in *GetGameResultsReq, opts ...grpc.CallOption) (*GetGameResultsRsp, error) @@ -1126,6 +1128,16 @@ func (c *eveningDetectiveServerClient) GetScreenResults(ctx context.Context, in return out, nil } +func (c *eveningDetectiveServerClient) DownloadGamePrintMaterials(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(httpbody.HttpBody) + err := c.cc.Invoke(ctx, EveningDetectiveServer_DownloadGamePrintMaterials_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *eveningDetectiveServerClient) DownloadTeamQR(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(httpbody.HttpBody) @@ -1487,6 +1499,7 @@ type EveningDetectiveServerServer interface { SaveScreenPlan(context.Context, *SaveScreenPlanReq) (*SaveScreenPlanRsp, error) SetScreenPlanStep(context.Context, *SetScreenPlanStepReq) (*SetScreenPlanStepRsp, error) GetScreenResults(context.Context, *GetScreenResultsReq) (*GetScreenResultsRsp, error) + DownloadGamePrintMaterials(context.Context, *GetGameReq) (*httpbody.HttpBody, error) DownloadTeamQR(context.Context, *GetGameReq) (*httpbody.HttpBody, error) GetGameAnswers(context.Context, *GetGameAnswersReq) (*GetGameAnswersRsp, error) GetGameResults(context.Context, *GetGameResultsReq) (*GetGameResultsRsp, error) @@ -1778,6 +1791,9 @@ func (UnimplementedEveningDetectiveServerServer) SetScreenPlanStep(context.Conte func (UnimplementedEveningDetectiveServerServer) GetScreenResults(context.Context, *GetScreenResultsReq) (*GetScreenResultsRsp, error) { return nil, status.Error(codes.Unimplemented, "method GetScreenResults not implemented") } +func (UnimplementedEveningDetectiveServerServer) DownloadGamePrintMaterials(context.Context, *GetGameReq) (*httpbody.HttpBody, error) { + return nil, status.Error(codes.Unimplemented, "method DownloadGamePrintMaterials not implemented") +} func (UnimplementedEveningDetectiveServerServer) DownloadTeamQR(context.Context, *GetGameReq) (*httpbody.HttpBody, error) { return nil, status.Error(codes.Unimplemented, "method DownloadTeamQR not implemented") } @@ -3408,6 +3424,24 @@ func _EveningDetectiveServer_GetScreenResults_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } +func _EveningDetectiveServer_DownloadGamePrintMaterials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGameReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EveningDetectiveServerServer).DownloadGamePrintMaterials(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EveningDetectiveServer_DownloadGamePrintMaterials_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EveningDetectiveServerServer).DownloadGamePrintMaterials(ctx, req.(*GetGameReq)) + } + return interceptor(ctx, in, info, handler) +} + func _EveningDetectiveServer_DownloadTeamQR_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetGameReq) if err := dec(in); err != nil { @@ -4223,6 +4257,10 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetScreenResults", Handler: _EveningDetectiveServer_GetScreenResults_Handler, }, + { + MethodName: "DownloadGamePrintMaterials", + Handler: _EveningDetectiveServer_DownloadGamePrintMaterials_Handler, + }, { MethodName: "DownloadTeamQR", Handler: _EveningDetectiveServer_DownloadTeamQR_Handler,