···23## Ben
45-- [ ] Transport : Packet splitting
6-- [ ] State : Event history tracking
7-- [ ] State : Post game sync
8-- [ ] API : Handling Profile Syncing
9-- [ ] ALL : State Update Events
10-- [ ] ALL : Game Replay Screen
11-- [ ] Frontend : Scaffolding
12-- [ ] Meta : CI Setup
13-- [ ] Meta : README Instructions
14-- [x] Meta : Recipes for type binding generation
···23## Ben
45+- [ ] Transport : Packet splitting
6+- [ ] State : Event history tracking
7+- [ ] State : Post game sync
8+- [ ] API : Handling Profile Syncing
9+- [ ] ALL : State Update Events
10+- [ ] ALL : Game Replay Screen
11+- [ ] Frontend : Scaffolding
12+- [ ] Meta : CI Setup
13+- [ ] Meta : README Instructions
14+- [x] Meta : Recipes for type binding generation
+4-4
backend/src/export_types.rs
···1-use std::path::PathBuf;
23use manhunt_app_lib::mk_specta;
4use specta_typescript::Typescript;
···10 .canonicalize()
11 .expect("Failed to canonicalize path");
12 let specta = mk_specta();
13- specta
14- .export(Typescript::default(), &path)
15- .expect("Failed to export types");
16 println!(
17 "Successfully exported type and commands to {}",
18 path.to_str().unwrap()
···1+use std::{borrow::Cow, path::PathBuf};
23use manhunt_app_lib::mk_specta;
4use specta_typescript::Typescript;
···10 .canonicalize()
11 .expect("Failed to canonicalize path");
12 let specta = mk_specta();
13+ let mut lang = Typescript::new();
14+ lang.header = Cow::Borrowed("/* eslint @typescript-eslint/no-unused-vars: 0 */\n/* eslint @typescript-eslint/no-explicit-any: 0 */");
15+ specta.export(lang, &path).expect("Failed to export types");
16 println!(
17 "Successfully exported type and commands to {}",
18 path.to_str().unwrap()
···1-02// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
34/** user-defined commands **/
56-7export const commands = {
8-/**
9- * (Screen: Menu) Start/Join a new lobby, set `join_code` to `null` to be host,
10- * set it to a join code to be a client. This triggers a screen change to [AppScreen::Lobby]
11- */
12-async startLobby(joinCode: string | null, settings: GameSettings) : Promise<Result<null, string>> {
13- try {
14- return { status: "ok", data: await TAURI_INVOKE("start_lobby", { joinCode, settings }) };
15-} catch (e) {
16- if(e instanceof Error) throw e;
17- else return { status: "error", error: e as any };
18-}
19-},
20-/**
21- * Quit a running game or leave a lobby
22- */
23-async quitGameOrLobby() : Promise<Result<null, string>> {
24- try {
25- return { status: "ok", data: await TAURI_INVOKE("quit_game_or_lobby") };
26-} catch (e) {
27- if(e instanceof Error) throw e;
28- else return { status: "error", error: e as any };
29-}
30-},
31-/**
32- * Get the screen the app should currently be on, returns [AppScreen]
33- */
34-async getCurrentScreen() : Promise<Result<AppScreen, string>> {
35- try {
36- return { status: "ok", data: await TAURI_INVOKE("get_current_screen") };
37-} catch (e) {
38- if(e instanceof Error) throw e;
39- else return { status: "error", error: e as any };
40-}
41-},
42-/**
43- * (Screen: Menu) Update the player's profile and persist it
44- */
45-async updateProfile(newProfile: PlayerProfile) : Promise<Result<null, string>> {
46- try {
47- return { status: "ok", data: await TAURI_INVOKE("update_profile", { newProfile }) };
48-} catch (e) {
49- if(e instanceof Error) throw e;
50- else return { status: "error", error: e as any };
51-}
52-},
53-/**
54- * (Screen: Lobby) Get the current state of the lobby, call after receiving an update event
55- */
56-async getLobbyState() : Promise<Result<LobbyState, string>> {
57- try {
58- return { status: "ok", data: await TAURI_INVOKE("get_lobby_state") };
59-} catch (e) {
60- if(e instanceof Error) throw e;
61- else return { status: "error", error: e as any };
62-}
63-},
64-/**
65- * (Screen: Lobby) HOST ONLY: Push new settings to everyone, does nothing on clients. Returns the
66- * new lobby state
67- */
68-async hostUpdateSettings(settings: GameSettings) : Promise<Result<LobbyState, string>> {
69- try {
70- return { status: "ok", data: await TAURI_INVOKE("host_update_settings", { settings }) };
71-} catch (e) {
72- if(e instanceof Error) throw e;
73- else return { status: "error", error: e as any };
74-}
75-},
76-/**
77- * (Screen: Lobby) Switch teams between seekers and hiders, returns the new [LobbyState]
78- */
79-async switchTeams(seeker: boolean) : Promise<Result<LobbyState, string>> {
80- try {
81- return { status: "ok", data: await TAURI_INVOKE("switch_teams", { seeker }) };
82-} catch (e) {
83- if(e instanceof Error) throw e;
84- else return { status: "error", error: e as any };
85-}
86-},
87-/**
88- * (Screen: Lobby) HOST ONLY: Start the game, stops anyone else from joining and switched screen
89- * to AppScreen::Game.
90- */
91-async hostStartGame() : Promise<Result<null, string>> {
92- try {
93- return { status: "ok", data: await TAURI_INVOKE("host_start_game") };
94-} catch (e) {
95- if(e instanceof Error) throw e;
96- else return { status: "error", error: e as any };
97-}
98-},
99-/**
100- * (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state
101- */
102-async markCaught() : Promise<Result<GameState, string>> {
103- try {
104- return { status: "ok", data: await TAURI_INVOKE("mark_caught") };
105-} catch (e) {
106- if(e instanceof Error) throw e;
107- else return { status: "error", error: e as any };
108-}
109-},
110-/**
111- * (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of
112- * the powerup. Returns the new game state after rolling for the powerup
113- */
114-async grabPowerup() : Promise<Result<GameState, string>> {
115- try {
116- return { status: "ok", data: await TAURI_INVOKE("grab_powerup") };
117-} catch (e) {
118- if(e instanceof Error) throw e;
119- else return { status: "error", error: e as any };
120-}
121-},
122-/**
123- * (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the
124- * player has none. Returns the updated game state
125- */
126-async usePowerup() : Promise<Result<GameState, string>> {
127- try {
128- return { status: "ok", data: await TAURI_INVOKE("use_powerup") };
129-} catch (e) {
130- if(e instanceof Error) throw e;
131- else return { status: "error", error: e as any };
132-}
133-}
134-}
000000135136/** user-defined events **/
137138-139-140/** user-defined constants **/
141142-143-144/** user-defined types **/
145146-export type AppScreen = "Setup" | "Menu" | "Lobby" | "Game"
147/**
148 * Settings for the game, host is the only person able to change these
149 */
150-export type GameSettings = {
151-/**
152- * The random seed used for shared rng
153- */
154-random_seed: number;
155-/**
156- * The number of seconds to wait before seekers are allowed to go
157- */
158-hiding_time_seconds: number;
159-/**
160- * Condition to wait for global pings to begin
161- */
162-ping_start: PingStartCondition;
163-/**
164- * Time between pings after the condition is met (first ping is either after the interval or
165- * instantly after the condition is met depending on the condition)
166- */
167-ping_minutes_interval: number;
168-/**
169- * Condition for powerups to start spawning
170- */
171-powerup_start: PingStartCondition;
172-/**
173- * Chance every minute of a powerup spawning, out of 100
174- */
175-powerup_chance: number;
176-/**
177- * Hard cooldown between powerups spawning
178- */
179-powerup_minutes_cooldown: number;
180-/**
181- * Locations that powerups may spawn at
182- */
183-powerup_locations: Location[] }
0184/**
185 * This struct handles all logic regarding state updates
186 */
187-export type GameState = {
188-/**
189- * The id of this player in this game
190- */
191-id: string;
192-/**
193- * The powerup the player is currently holding
194- */
195-held_powerup: PowerUpType | null;
196-/**
197- * When the game started
198- */
199-game_started: string;
200-/**
201- * When seekers were allowed to begin
202- */
203-seekers_started: string | null;
204-/**
205- * Last time we pinged all players
206- */
207-last_global_ping: string | null;
208-/**
209- * Last time a powerup was spawned
210- */
211-last_powerup_spawn: string | null;
212-/**
213- * Hashmap tracking if a player is a seeker (true) or a hider (false)
214- */
215-caught_state: Partial<{ [key in string]: boolean }>;
216-/**
217- * A map of the latest global ping results for each player
218- */
219-pings: Partial<{ [key in string]: PlayerPing }>;
220-/**
221- * Powerup on the map that players can grab. Only one at a time
222- */
223-available_powerup: Location | null }
224-export type LobbyState = { profiles: Partial<{ [key in string]: PlayerProfile }>; join_code: string;
225-/**
226- * True represents seeker, false hider
227- */
228-teams: Partial<{ [key in string]: boolean }>; self_seeker: boolean; settings: GameSettings }
000000229/**
230 * Some location in the world as gotten from a Geolocation API
231 */
232-export type Location = {
233-/**
234- * Latitude
235- */
236-lat: number;
237-/**
238- * Longitude
239- */
240-long: number;
241-/**
242- * The bearing (float normalized from 0 to 1) optional as GPS can't always determine
243- */
244-heading: number | null }
0245/**
246 * The starting condition for global pings to begin
247 */
248-export type PingStartCondition =
249-/**
250- * Wait For X players to be caught before beginning global pings
251- */
252-{ Players: number } |
253-/**
254- * Wait for X minutes after game start to begin global pings
255- */
256-{ Minutes: number } |
257-/**
258- * Don't wait at all, ping location after seekers are released
259- */
260-"Instant"
261/**
262 * An on-map ping of a player
263 */
264-export type PlayerPing = {
265-/**
266- * Location of the ping
267- */
268-loc: Location;
269-/**
270- * Time the ping happened
271- */
272-timestamp: string;
273-/**
274- * The player to display as
275- */
276-display_player: string;
277-/**
278- * The actual player that initialized this ping
279- */
280-real_player: string }
281-export type PlayerProfile = { display_name: string; pfp_base64: string | null }
0282/**
283 * Type of powerup
284 */
285-export type PowerUpType =
286-/**
287- * Ping a random seeker instead of a hider
288- */
289-"PingSeeker" |
290-/**
291- * Pings all seekers locations on the map for hiders
292- */
293-"PingAllSeekers" |
294-/**
295- * Ping another random hider instantly
296- */
297-"ForcePingOther"
298299/** tauri-specta globals **/
300301-import {
302- invoke as TAURI_INVOKE,
303- Channel as TAURI_CHANNEL,
304-} from "@tauri-apps/api/core";
305import * as TAURI_API_EVENT from "@tauri-apps/api/event";
306import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
307308type __EventObj__<T> = {
309- listen: (
310- cb: TAURI_API_EVENT.EventCallback<T>,
311- ) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
312- once: (
313- cb: TAURI_API_EVENT.EventCallback<T>,
314- ) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
315- emit: null extends T
316- ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
317- : (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
318};
319320-export type Result<T, E> =
321- | { status: "ok"; data: T }
322- | { status: "error"; error: E };
323324-function __makeEvents__<T extends Record<string, any>>(
325- mappings: Record<keyof T, string>,
326-) {
327- return new Proxy(
328- {} as unknown as {
329- [K in keyof T]: __EventObj__<T[K]> & {
330- (handle: __WebviewWindow__): __EventObj__<T[K]>;
331- };
332- },
333- {
334- get: (_, event) => {
335- const name = mappings[event as keyof T];
336337- return new Proxy((() => {}) as any, {
338- apply: (_, __, [window]: [__WebviewWindow__]) => ({
339- listen: (arg: any) => window.listen(name, arg),
340- once: (arg: any) => window.once(name, arg),
341- emit: (arg: any) => window.emit(name, arg),
342- }),
343- get: (_, command: keyof __EventObj__<any>) => {
344- switch (command) {
345- case "listen":
346- return (arg: any) => TAURI_API_EVENT.listen(name, arg);
347- case "once":
348- return (arg: any) => TAURI_API_EVENT.once(name, arg);
349- case "emit":
350- return (arg: any) => TAURI_API_EVENT.emit(name, arg);
351- }
352- },
353- });
354- },
355- },
356- );
357}
···1+/* eslint @typescript-eslint/no-unused-vars: 0 */
2+/* eslint @typescript-eslint/no-explicit-any: 0 */
3// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
45/** user-defined commands **/
607export const commands = {
8+ /**
9+ * (Screen: Menu) Start/Join a new lobby, set `join_code` to `null` to be host,
10+ * set it to a join code to be a client. This triggers a screen change to [AppScreen::Lobby]
11+ */
12+ async startLobby(
13+ joinCode: string | null,
14+ settings: GameSettings
15+ ): Promise<Result<null, string>> {
16+ try {
17+ return {
18+ status: "ok",
19+ data: await TAURI_INVOKE("start_lobby", { joinCode, settings })
20+ };
21+ } catch (e) {
22+ if (e instanceof Error) throw e;
23+ else return { status: "error", error: e as any };
24+ }
25+ },
26+ /**
27+ * Quit a running game or leave a lobby
28+ */
29+ async quitGameOrLobby(): Promise<Result<null, string>> {
30+ try {
31+ return { status: "ok", data: await TAURI_INVOKE("quit_game_or_lobby") };
32+ } catch (e) {
33+ if (e instanceof Error) throw e;
34+ else return { status: "error", error: e as any };
35+ }
36+ },
37+ /**
38+ * Get the screen the app should currently be on, returns [AppScreen]
39+ */
40+ async getCurrentScreen(): Promise<Result<AppScreen, string>> {
41+ try {
42+ return { status: "ok", data: await TAURI_INVOKE("get_current_screen") };
43+ } catch (e) {
44+ if (e instanceof Error) throw e;
45+ else return { status: "error", error: e as any };
46+ }
47+ },
48+ /**
49+ * (Screen: Menu) Update the player's profile and persist it
50+ */
51+ async updateProfile(newProfile: PlayerProfile): Promise<Result<null, string>> {
52+ try {
53+ return { status: "ok", data: await TAURI_INVOKE("update_profile", { newProfile }) };
54+ } catch (e) {
55+ if (e instanceof Error) throw e;
56+ else return { status: "error", error: e as any };
57+ }
58+ },
59+ /**
60+ * (Screen: Lobby) Get the current state of the lobby, call after receiving an update event
61+ */
62+ async getLobbyState(): Promise<Result<LobbyState, string>> {
63+ try {
64+ return { status: "ok", data: await TAURI_INVOKE("get_lobby_state") };
65+ } catch (e) {
66+ if (e instanceof Error) throw e;
67+ else return { status: "error", error: e as any };
68+ }
69+ },
70+ /**
71+ * (Screen: Lobby) HOST ONLY: Push new settings to everyone, does nothing on clients. Returns the
72+ * new lobby state
73+ */
74+ async hostUpdateSettings(settings: GameSettings): Promise<Result<LobbyState, string>> {
75+ try {
76+ return { status: "ok", data: await TAURI_INVOKE("host_update_settings", { settings }) };
77+ } catch (e) {
78+ if (e instanceof Error) throw e;
79+ else return { status: "error", error: e as any };
80+ }
81+ },
82+ /**
83+ * (Screen: Lobby) Switch teams between seekers and hiders, returns the new [LobbyState]
84+ */
85+ async switchTeams(seeker: boolean): Promise<Result<LobbyState, string>> {
86+ try {
87+ return { status: "ok", data: await TAURI_INVOKE("switch_teams", { seeker }) };
88+ } catch (e) {
89+ if (e instanceof Error) throw e;
90+ else return { status: "error", error: e as any };
91+ }
92+ },
93+ /**
94+ * (Screen: Lobby) HOST ONLY: Start the game, stops anyone else from joining and switched screen
95+ * to AppScreen::Game.
96+ */
97+ async hostStartGame(): Promise<Result<null, string>> {
98+ try {
99+ return { status: "ok", data: await TAURI_INVOKE("host_start_game") };
100+ } catch (e) {
101+ if (e instanceof Error) throw e;
102+ else return { status: "error", error: e as any };
103+ }
104+ },
105+ /**
106+ * (Screen: Game) Mark this player as caught, this player will become a seeker. Returns the new game state
107+ */
108+ async markCaught(): Promise<Result<GameState, string>> {
109+ try {
110+ return { status: "ok", data: await TAURI_INVOKE("mark_caught") };
111+ } catch (e) {
112+ if (e instanceof Error) throw e;
113+ else return { status: "error", error: e as any };
114+ }
115+ },
116+ /**
117+ * (Screen: Game) Grab a powerup on the map, this should be called when the user is *in range* of
118+ * the powerup. Returns the new game state after rolling for the powerup
119+ */
120+ async grabPowerup(): Promise<Result<GameState, string>> {
121+ try {
122+ return { status: "ok", data: await TAURI_INVOKE("grab_powerup") };
123+ } catch (e) {
124+ if (e instanceof Error) throw e;
125+ else return { status: "error", error: e as any };
126+ }
127+ },
128+ /**
129+ * (Screen: Game) Use the currently held powerup in the player's held_powerup. Does nothing if the
130+ * player has none. Returns the updated game state
131+ */
132+ async usePowerup(): Promise<Result<GameState, string>> {
133+ try {
134+ return { status: "ok", data: await TAURI_INVOKE("use_powerup") };
135+ } catch (e) {
136+ if (e instanceof Error) throw e;
137+ else return { status: "error", error: e as any };
138+ }
139+ }
140+};
141142/** user-defined events **/
14300144/** user-defined constants **/
14500146/** user-defined types **/
147148+export type AppScreen = "Setup" | "Menu" | "Lobby" | "Game";
149/**
150 * Settings for the game, host is the only person able to change these
151 */
152+export type GameSettings = {
153+ /**
154+ * The random seed used for shared rng
155+ */
156+ random_seed: number;
157+ /**
158+ * The number of seconds to wait before seekers are allowed to go
159+ */
160+ hiding_time_seconds: number;
161+ /**
162+ * Condition to wait for global pings to begin
163+ */
164+ ping_start: PingStartCondition;
165+ /**
166+ * Time between pings after the condition is met (first ping is either after the interval or
167+ * instantly after the condition is met depending on the condition)
168+ */
169+ ping_minutes_interval: number;
170+ /**
171+ * Condition for powerups to start spawning
172+ */
173+ powerup_start: PingStartCondition;
174+ /**
175+ * Chance every minute of a powerup spawning, out of 100
176+ */
177+ powerup_chance: number;
178+ /**
179+ * Hard cooldown between powerups spawning
180+ */
181+ powerup_minutes_cooldown: number;
182+ /**
183+ * Locations that powerups may spawn at
184+ */
185+ powerup_locations: Location[];
186+};
187/**
188 * This struct handles all logic regarding state updates
189 */
190+export type GameState = {
191+ /**
192+ * The id of this player in this game
193+ */
194+ id: string;
195+ /**
196+ * The powerup the player is currently holding
197+ */
198+ held_powerup: PowerUpType | null;
199+ /**
200+ * When the game started
201+ */
202+ game_started: string;
203+ /**
204+ * When seekers were allowed to begin
205+ */
206+ seekers_started: string | null;
207+ /**
208+ * Last time we pinged all players
209+ */
210+ last_global_ping: string | null;
211+ /**
212+ * Last time a powerup was spawned
213+ */
214+ last_powerup_spawn: string | null;
215+ /**
216+ * Hashmap tracking if a player is a seeker (true) or a hider (false)
217+ */
218+ caught_state: Partial<{ [key in string]: boolean }>;
219+ /**
220+ * A map of the latest global ping results for each player
221+ */
222+ pings: Partial<{ [key in string]: PlayerPing }>;
223+ /**
224+ * Powerup on the map that players can grab. Only one at a time
225+ */
226+ available_powerup: Location | null;
227+};
228+export type LobbyState = {
229+ profiles: Partial<{ [key in string]: PlayerProfile }>;
230+ join_code: string;
231+ /**
232+ * True represents seeker, false hider
233+ */
234+ teams: Partial<{ [key in string]: boolean }>;
235+ self_seeker: boolean;
236+ settings: GameSettings;
237+};
238/**
239 * Some location in the world as gotten from a Geolocation API
240 */
241+export type Location = {
242+ /**
243+ * Latitude
244+ */
245+ lat: number;
246+ /**
247+ * Longitude
248+ */
249+ long: number;
250+ /**
251+ * The bearing (float normalized from 0 to 1) optional as GPS can't always determine
252+ */
253+ heading: number | null;
254+};
255/**
256 * The starting condition for global pings to begin
257 */
258+export type PingStartCondition =
259+ /**
260+ * Wait For X players to be caught before beginning global pings
261+ */
262+ | { Players: number }
263+ /**
264+ * Wait for X minutes after game start to begin global pings
265+ */
266+ | { Minutes: number }
267+ /**
268+ * Don't wait at all, ping location after seekers are released
269+ */
270+ | "Instant";
271/**
272 * An on-map ping of a player
273 */
274+export type PlayerPing = {
275+ /**
276+ * Location of the ping
277+ */
278+ loc: Location;
279+ /**
280+ * Time the ping happened
281+ */
282+ timestamp: string;
283+ /**
284+ * The player to display as
285+ */
286+ display_player: string;
287+ /**
288+ * The actual player that initialized this ping
289+ */
290+ real_player: string;
291+};
292+export type PlayerProfile = { display_name: string; pfp_base64: string | null };
293/**
294 * Type of powerup
295 */
296+export type PowerUpType =
297+ /**
298+ * Ping a random seeker instead of a hider
299+ */
300+ | "PingSeeker"
301+ /**
302+ * Pings all seekers locations on the map for hiders
303+ */
304+ | "PingAllSeekers"
305+ /**
306+ * Ping another random hider instantly
307+ */
308+ | "ForcePingOther";
309310/** tauri-specta globals **/
311312+import { invoke as TAURI_INVOKE, Channel as TAURI_CHANNEL } from "@tauri-apps/api/core";
000313import * as TAURI_API_EVENT from "@tauri-apps/api/event";
314import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow";
315316type __EventObj__<T> = {
317+ listen: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
318+ once: (cb: TAURI_API_EVENT.EventCallback<T>) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
319+ emit: null extends T
320+ ? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
321+ : (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
0000322};
323324+export type Result<T, E> = { status: "ok"; data: T } | { status: "error"; error: E };
00325326+function __makeEvents__<T extends Record<string, any>>(mappings: Record<keyof T, string>) {
327+ return new Proxy(
328+ {} as unknown as {
329+ [K in keyof T]: __EventObj__<T[K]> & {
330+ (handle: __WebviewWindow__): __EventObj__<T[K]>;
331+ };
332+ },
333+ {
334+ get: (_, event) => {
335+ const name = mappings[event as keyof T];
00336337+ return new Proxy((() => {}) as any, {
338+ apply: (_, __, [window]: [__WebviewWindow__]) => ({
339+ listen: (arg: any) => window.listen(name, arg),
340+ once: (arg: any) => window.once(name, arg),
341+ emit: (arg: any) => window.emit(name, arg)
342+ }),
343+ get: (_, command: keyof __EventObj__<any>) => {
344+ switch (command) {
345+ case "listen":
346+ return (arg: any) => TAURI_API_EVENT.listen(name, arg);
347+ case "once":
348+ return (arg: any) => TAURI_API_EVENT.once(name, arg);
349+ case "emit":
350+ return (arg: any) => TAURI_API_EVENT.emit(name, arg);
351+ }
352+ }
353+ });
354+ }
355+ }
356+ );
357}