From d9160148721d6888b1b53034dc4555a2a6c3c43d Mon Sep 17 00:00:00 2001 From: code002lover Date: Thu, 18 Dec 2025 20:04:50 +0100 Subject: [PATCH] add batch game info request --- backend/src/main.rs | 16 ++++- frontend/items.ts | 132 ++++++++++++++++++++++++++++++++++++ frontend/src/GameFilter.tsx | 55 ++++++++------- frontend/src/GameList.tsx | 2 +- protobuf/items.proto | 41 +++++++---- 5 files changed, 206 insertions(+), 40 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index 6232937..eb88964 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -44,6 +44,19 @@ async fn get_game( games.iter().find(|g| g.title == title).cloned() } +#[post("/games/batch", data = "")] +async fn get_games_batch( + _token: auth::Token, + game_list: &rocket::State>>, + req: proto_utils::Proto, +) -> items::GameList { + let games = game_list.lock().await; + let req = req.into_inner(); + let mut games = games.clone(); + games.retain(|g| req.games.contains(&g.title)); + items::GameList { games } +} + #[get("/games")] async fn get_games( _token: auth::Token, @@ -319,7 +332,8 @@ async fn main() -> Result<(), std::io::Error> { add_opinion, remove_opinion, add_game, - get_game_thumbnail + get_game_thumbnail, + get_games_batch ], ) .mount( diff --git a/frontend/items.ts b/frontend/items.ts index 78fbf8a..bffc235 100644 --- a/frontend/items.ts +++ b/frontend/items.ts @@ -116,6 +116,14 @@ export interface RemoveOpinionRequest { gameTitle: string; } +export interface GetGameInfoRequest { + games: string[]; +} + +export interface GameInfoResponse { + games: Game[]; +} + function createBasePerson(): Person { return { name: "", opinion: [] }; } @@ -1213,6 +1221,122 @@ export const RemoveOpinionRequest: MessageFns = { }, }; +function createBaseGetGameInfoRequest(): GetGameInfoRequest { + return { games: [] }; +} + +export const GetGameInfoRequest: MessageFns = { + encode(message: GetGameInfoRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.games) { + writer.uint32(10).string(v!); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GetGameInfoRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGetGameInfoRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.games.push(reader.string()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GetGameInfoRequest { + return { games: globalThis.Array.isArray(object?.games) ? object.games.map((e: any) => globalThis.String(e)) : [] }; + }, + + toJSON(message: GetGameInfoRequest): unknown { + const obj: any = {}; + if (message.games?.length) { + obj.games = message.games; + } + return obj; + }, + + create, I>>(base?: I): GetGameInfoRequest { + return GetGameInfoRequest.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): GetGameInfoRequest { + const message = createBaseGetGameInfoRequest(); + message.games = object.games?.map((e) => e) || []; + return message; + }, +}; + +function createBaseGameInfoResponse(): GameInfoResponse { + return { games: [] }; +} + +export const GameInfoResponse: MessageFns = { + encode(message: GameInfoResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.games) { + Game.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): GameInfoResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseGameInfoResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.games.push(Game.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): GameInfoResponse { + return { games: globalThis.Array.isArray(object?.games) ? object.games.map((e: any) => Game.fromJSON(e)) : [] }; + }, + + toJSON(message: GameInfoResponse): unknown { + const obj: any = {}; + if (message.games?.length) { + obj.games = message.games.map((e) => Game.toJSON(e)); + } + return obj; + }, + + create, I>>(base?: I): GameInfoResponse { + return GameInfoResponse.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): GameInfoResponse { + const message = createBaseGameInfoResponse(); + message.games = object.games?.map((e) => Game.fromPartial(e)) || []; + return message; + }, +}; + /** Authentication service */ export interface AuthService { Login(request: LoginRequest): Promise; @@ -1255,6 +1379,7 @@ export interface MainService { GetGames(request: GetGamesRequest): Promise; AddGame(request: Game): Promise; AddOpinion(request: AddOpinionRequest): Promise; + GetGameInfo(request: GetGameInfoRequest): Promise; } export const MainServiceServiceName = "items.MainService"; @@ -1268,6 +1393,7 @@ export class MainServiceClientImpl implements MainService { this.GetGames = this.GetGames.bind(this); this.AddGame = this.AddGame.bind(this); this.AddOpinion = this.AddOpinion.bind(this); + this.GetGameInfo = this.GetGameInfo.bind(this); } GetGame(request: GameRequest): Promise { const data = GameRequest.encode(request).finish(); @@ -1292,6 +1418,12 @@ export class MainServiceClientImpl implements MainService { const promise = this.rpc.request(this.service, "AddOpinion", data); return promise.then((data) => Person.decode(new BinaryReader(data))); } + + GetGameInfo(request: GetGameInfoRequest): Promise { + const data = GetGameInfoRequest.encode(request).finish(); + const promise = this.rpc.request(this.service, "GetGameInfo", data); + return promise.then((data) => GameInfoResponse.decode(new BinaryReader(data))); + } } interface Rpc { diff --git a/frontend/src/GameFilter.tsx b/frontend/src/GameFilter.tsx index 03903e6..7313e87 100644 --- a/frontend/src/GameFilter.tsx +++ b/frontend/src/GameFilter.tsx @@ -3,6 +3,8 @@ import { Person, PersonList as PersonListProto, Game as GameProto, + GetGameInfoRequest, + GameInfoResponse, } from "../items"; import { apiFetch } from "./api"; import { Link } from "react-router-dom"; @@ -72,31 +74,38 @@ export function GameFilter() { .filter(([, players]) => players.size === 0) .map(([game]) => game); - const games = game_titles.map(async (title) => { - if (metaData[title]) { - console.log("returned cached metadata"); - return metaData[title]; - } - return await apiFetch(`/api/game/${encodeURIComponent(title)}`) - .then((res) => res.arrayBuffer()) - .then((buffer) => { - const game = GameProto.decode(new Uint8Array(buffer)) as GameProto; - metaData[title] = game; - return game; - }) - .catch((err) => console.error("Failed to fetch game:", err)); - }); + let games = game_titles.filter((title) => metaData[title]).map((title) => metaData[title]); + const gamesToFetch = GetGameInfoRequest.encode( + GetGameInfoRequest.create({ + games: game_titles.filter((title) => !metaData[title]), + }) + ).finish(); - Promise.all(games).then((games) => { - const filteredGames = games.filter((g) => { - const game = g as GameProto; - return ( - game.maxPlayers >= selectedPeople.size && - game.minPlayers <= selectedPeople.size - ); + apiFetch("/api/games/batch", { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + }, + body: gamesToFetch, + }) + .then((res) => res.arrayBuffer()) + .then((buffer) => { + const list = GameInfoResponse.decode(new Uint8Array(buffer)); + games = games.concat(list.games); + + games.forEach((game) => { + metaData[game.title] = game; + }); + + const filteredGames = games.filter((g) => { + const game = g as GameProto; + return ( + game.maxPlayers >= selectedPeople.size && + game.minPlayers <= selectedPeople.size + ); + }); + setFilteredGames(filteredGames.map((g) => (g as GameProto).title)); }); - setFilteredGames(filteredGames.map((g) => (g as GameProto).title)); - }); }, [selectedPeople, people, metaData]); const togglePerson = (name: string) => { diff --git a/frontend/src/GameList.tsx b/frontend/src/GameList.tsx index 1bfb1a5..87620aa 100644 --- a/frontend/src/GameList.tsx +++ b/frontend/src/GameList.tsx @@ -459,7 +459,7 @@ export function GameList({ onShowToast }: Props) { "Content-Type": "application/octet-stream", }, body: RemoveOpinionRequest.encode( - AddOpinionRequest.create({ + RemoveOpinionRequest.create({ gameTitle: title, }) ).finish(), diff --git a/protobuf/items.proto b/protobuf/items.proto index 298cc6a..385013e 100644 --- a/protobuf/items.proto +++ b/protobuf/items.proto @@ -3,31 +3,32 @@ syntax = "proto3"; package items; message Person { - string name = 1; + string name = 1; repeated Opinion opinion = 2; } message Opinion { - string title = 1; - bool would_play = 2; + string title = 1; + bool would_play = 2; } message Game { reserved 3; - string title = 1; - Source source = 2; + string title = 1; + Source source = 2; uint32 min_players = 4; uint32 max_players = 5; - uint32 price = 6; - uint64 remote_id = 7; + uint32 price = 6; + uint64 remote_id = 7; } enum Source { - STEAM = 0; + STEAM = 0; ROBLOX = 1; } message PersonList { repeated Person person = 1; } + message GameList { repeated Game games = 1; } // Authentication messages @@ -37,24 +38,24 @@ message LoginRequest { } message LoginResponse { - string token = 1; - bool success = 2; + string token = 1; + bool success = 2; string message = 3; } message LogoutRequest { string token = 1; } message LogoutResponse { - bool success = 1; + bool success = 1; string message = 2; } message AuthStatusRequest { string token = 1; } message AuthStatusResponse { - bool authenticated = 1; - string username = 2; - string message = 3; + bool authenticated = 1; + string username = 2; + string message = 3; } // Authentication service @@ -65,18 +66,28 @@ service AuthService { } message GameRequest { string title = 1; } + message GetGamesRequest {} message AddOpinionRequest { string game_title = 1; - bool would_play = 2; + bool would_play = 2; } message RemoveOpinionRequest { string game_title = 1; } +message GetGameInfoRequest { + repeated string games = 1; +} + +message GameInfoResponse { + repeated Game games = 1; +} + service MainService { rpc GetGame(GameRequest) returns (Game); rpc GetGames(GetGamesRequest) returns (GameList); rpc AddGame(Game) returns (Game); rpc AddOpinion(AddOpinionRequest) returns (Person); + rpc GetGameInfo(GetGameInfoRequest) returns (GameInfoResponse); } \ No newline at end of file