ogi-addon 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.ts CHANGED
@@ -7,6 +7,9 @@ import EventResponse from './EventResponse';
7
7
  import type { SearchResult } from './SearchEngine';
8
8
  import Fuse, { IFuseOptions } from 'fuse.js';
9
9
 
10
+ /**
11
+ * Exposed events that the programmer can use to listen to and emit events.
12
+ */
10
13
  export type OGIAddonEvent =
11
14
  | 'connect'
12
15
  | 'disconnect'
@@ -19,8 +22,12 @@ export type OGIAddonEvent =
19
22
  | 'exit'
20
23
  | 'check-for-updates'
21
24
  | 'request-dl'
22
- | 'catalog';
25
+ | 'catalog'
26
+ | 'launch-app';
23
27
 
28
+ /**
29
+ * The events that the client can send to the server and are handled by the server.
30
+ */
24
31
  export type OGIAddonClientSentEvent =
25
32
  | 'response'
26
33
  | 'authenticate'
@@ -33,10 +40,15 @@ export type OGIAddonClientSentEvent =
33
40
  | 'flag'
34
41
  | 'task-update';
35
42
 
43
+ /**
44
+ * The events that the server sends to the client
45
+ * This is the events that the server can send to the client and are handled by the client.
46
+ */
36
47
  export type OGIAddonServerSentEvent =
37
48
  | 'authenticate'
38
49
  | 'configure'
39
50
  | 'config-update'
51
+ | 'launch-app'
40
52
  | 'search'
41
53
  | 'setup'
42
54
  | 'response'
@@ -122,7 +134,16 @@ export interface CatalogWithCarousel {
122
134
  carousel?: Record<string, CatalogCarouselItem> | CatalogCarouselItem[];
123
135
  }
124
136
 
125
- export type CatalogResponse = Record<string, CatalogSection> | CatalogWithCarousel;
137
+ export type CatalogResponse =
138
+ | Record<string, CatalogSection>
139
+ | CatalogWithCarousel;
140
+
141
+ /**
142
+ * UMU ID format: 'steam:${number}' or 'umu:${number}'
143
+ * - steam:${number} → maps to umu-${number} for Steam games
144
+ * - umu:${number} → maps to umu-${number} for non-Steam games
145
+ */
146
+ export type UmuId = `steam:${number}` | `umu:${number}`;
126
147
 
127
148
  export type SetupEventResponse = Omit<
128
149
  LibraryInfo,
@@ -138,6 +159,35 @@ export type SetupEventResponse = Omit<
138
159
  name: string;
139
160
  path: string;
140
161
  }[];
162
+ /**
163
+ * UMU Proton integration configuration
164
+ */
165
+ umu?: {
166
+ /**
167
+ * UMU ID for the game. Format: 'steam:${number}' or 'umu:${number}'
168
+ * - steam:${number} → maps to umu-${number} for Steam games
169
+ * - umu:${number} → maps to umu-${number} for non-Steam games
170
+ */
171
+ umuId: UmuId;
172
+ /**
173
+ * Optional DLL overrides. These are relative to the game's cwd.
174
+ * System automatically prepends cwd and sets WINEDLLOVERRIDES="dll=n,b"
175
+ */
176
+ dllOverrides?: string[];
177
+ /**
178
+ * Optional Proton version to use (e.g., 'GE-Proton9-5', 'GE-Proton')
179
+ * If not specified, uses latest UMU-Proton
180
+ */
181
+ protonVersion?: string;
182
+ /**
183
+ * Optional store identifier for protonfixes (e.g., 'gog', 'egs', 'none')
184
+ */
185
+ store?: string;
186
+ /**
187
+ * Cached Steam shortcut app ID after adding UMU game to Steam (avoids re-adding on each launch)
188
+ */
189
+ steamShortcutId?: number;
190
+ };
141
191
  };
142
192
 
143
193
  export interface EventListenerTypes {
@@ -272,12 +322,7 @@ export interface EventListenerTypes {
272
322
  * @param event
273
323
  * @returns
274
324
  */
275
- catalog: (
276
- event: Omit<
277
- EventResponse<CatalogResponse>,
278
- 'askForInput'
279
- >
280
- ) => void;
325
+ catalog: (event: Omit<EventResponse<CatalogResponse>, 'askForInput'>) => void;
281
326
 
282
327
  /**
283
328
  * This event is emitted when the client requests for an addon to check for updates. Addon should resolve the event with the update information.
@@ -297,6 +342,17 @@ export interface EventListenerTypes {
297
342
  }
298
343
  >
299
344
  ) => void;
345
+
346
+ /**
347
+ * This event is emitted when the client is going to launch an app. Addon should use this to perform any pre or post launch tasks.
348
+ * @param data {LibraryInfo} The library information for the app to be launched.
349
+ * @param launchType { 'pre' | 'post' } The type of launch task to perform.
350
+ * @param event {EventResponse<void>} The event response from the server.
351
+ */
352
+ 'launch-app': (
353
+ data: { libraryInfo: LibraryInfo; launchType: 'pre' | 'post' },
354
+ event: EventResponse<void>
355
+ ) => void;
300
356
  }
301
357
 
302
358
  export interface StoreData {
@@ -835,6 +891,36 @@ export const ZodLibraryInfo = z.object({
835
891
  addonsource: z.string(),
836
892
  coverImage: z.string(),
837
893
  titleImage: z.string().optional(),
894
+ /**
895
+ * UMU Proton integration configuration (Linux only)
896
+ */
897
+ umu: z
898
+ .object({
899
+ umuId: z
900
+ .string()
901
+ .regex(/^(steam|umu):\d+$/, 'Must be in format steam:{number} or umu:{number}'),
902
+ dllOverrides: z.array(z.string()).optional(),
903
+ protonVersion: z.string().optional(),
904
+ store: z.string().optional(),
905
+ winePrefixPath: z.string().optional(),
906
+ steamShortcutId: z.number().optional(),
907
+ })
908
+ .optional(),
909
+ /**
910
+ * Legacy mode flag for games using old Steam/flatpak wine system
911
+ */
912
+ legacyMode: z.boolean().optional(),
913
+ /**
914
+ * Redistributables to install (for backward compatibility)
915
+ */
916
+ redistributables: z
917
+ .array(
918
+ z.object({
919
+ name: z.string(),
920
+ path: z.string(),
921
+ })
922
+ )
923
+ .optional(),
838
924
  });
839
925
  export type LibraryInfo = z.infer<typeof ZodLibraryInfo>;
840
926
  interface Notification {
@@ -963,6 +1049,9 @@ class OGIAddonWSListener {
963
1049
  return await this.waitForResponseFromServer<U>(id);
964
1050
  }
965
1051
 
1052
+ /**
1053
+ * Registers the message receiver for the socket. This is used to receive messages from the server and handle them.
1054
+ */
966
1055
  private registerMessageReceiver() {
967
1056
  this.socket.on('message', async (data: string) => {
968
1057
  const message: WebsocketMessageServer = JSON.parse(data);
@@ -983,17 +1072,8 @@ class OGIAddonWSListener {
983
1072
  }
984
1073
  break;
985
1074
  case 'search':
986
- let searchResultEvent = new EventResponse<SearchResult[]>(
987
- (screen, name, description) =>
988
- this.userInputAsked(screen, name, description, this.socket)
989
- );
990
- this.eventEmitter.emit('search', message.args, searchResultEvent);
991
- const searchResult =
992
- await this.waitForEventToRespond(searchResultEvent);
993
- this.respondToMessage(
994
- message.id!!,
995
- searchResult.data,
996
- searchResultEvent
1075
+ await this.handleEventWithResponse<SearchResult[]>(message, (event) =>
1076
+ this.eventEmitter.emit('search', message.args, event)
997
1077
  );
998
1078
  break;
999
1079
  case 'setup': {
@@ -1019,74 +1099,28 @@ class OGIAddonWSListener {
1019
1099
  break;
1020
1100
  }
1021
1101
  case 'library-search':
1022
- let librarySearchEvent = new EventResponse<BasicLibraryInfo[]>(
1023
- (screen, name, description) =>
1024
- this.userInputAsked(screen, name, description, this.socket)
1025
- );
1026
- this.eventEmitter.emit(
1027
- 'library-search',
1028
- message.args,
1029
- librarySearchEvent
1030
- );
1031
- const librarySearchResult =
1032
- await this.waitForEventToRespond(librarySearchEvent);
1033
- this.respondToMessage(
1034
- message.id!!,
1035
- librarySearchResult.data,
1036
- librarySearchEvent
1102
+ await this.handleEventWithResponse<BasicLibraryInfo[]>(
1103
+ message,
1104
+ (event) =>
1105
+ this.eventEmitter.emit('library-search', message.args, event)
1037
1106
  );
1038
1107
  break;
1039
1108
  case 'game-details':
1040
- let gameDetailsEvent = new EventResponse<StoreData | undefined>(
1041
- (screen, name, description) =>
1042
- this.userInputAsked(screen, name, description, this.socket)
1043
- );
1044
- if (this.eventEmitter.listenerCount('game-details') === 0) {
1045
- this.respondToMessage(
1046
- message.id!!,
1047
- {
1048
- error: 'No event listener for game-details',
1049
- },
1050
- gameDetailsEvent
1051
- );
1052
- break;
1053
- }
1054
- this.eventEmitter.emit(
1055
- 'game-details',
1056
- message.args,
1057
- gameDetailsEvent
1058
- );
1059
- const gameDetailsResult =
1060
- await this.waitForEventToRespond(gameDetailsEvent);
1061
- this.respondToMessage(
1062
- message.id!!,
1063
- gameDetailsResult.data,
1064
- gameDetailsEvent
1109
+ await this.handleEventWithResponse<StoreData | undefined>(
1110
+ message,
1111
+ (event) =>
1112
+ this.eventEmitter.emit('game-details', message.args, event),
1113
+ {
1114
+ requireListener: 'game-details',
1115
+ noListenerError: 'No event listener for game-details',
1116
+ }
1065
1117
  );
1066
1118
  break;
1067
1119
  case 'check-for-updates':
1068
- let checkForUpdatesEvent = new EventResponse<
1069
- | {
1070
- available: true;
1071
- version: string;
1072
- }
1073
- | {
1074
- available: false;
1075
- }
1076
- >((screen, name, description) =>
1077
- this.userInputAsked(screen, name, description, this.socket)
1078
- );
1079
- this.eventEmitter.emit(
1080
- 'check-for-updates',
1081
- message.args,
1082
- checkForUpdatesEvent
1083
- );
1084
- const checkForUpdatesResult =
1085
- await this.waitForEventToRespond(checkForUpdatesEvent);
1086
- this.respondToMessage(
1087
- message.id!!,
1088
- checkForUpdatesResult.data,
1089
- checkForUpdatesEvent
1120
+ await this.handleEventWithResponse<
1121
+ { available: true; version: string } | { available: false }
1122
+ >(message, (event) =>
1123
+ this.eventEmitter.emit('check-for-updates', message.args, event)
1090
1124
  );
1091
1125
  break;
1092
1126
  case 'request-dl':
@@ -1131,10 +1165,10 @@ class OGIAddonWSListener {
1131
1165
  );
1132
1166
  break;
1133
1167
  case 'catalog':
1134
- let catalogEvent = new EventResponse<CatalogResponse>();
1135
- this.eventEmitter.emit('catalog', catalogEvent);
1136
- const catalogResult = await this.waitForEventToRespond(catalogEvent);
1137
- this.respondToMessage(message.id!!, catalogResult.data, catalogEvent);
1168
+ await this.handleEventWithResponseNoInput<CatalogResponse>(
1169
+ message,
1170
+ (event) => this.eventEmitter.emit('catalog', event)
1171
+ );
1138
1172
  break;
1139
1173
  case 'task-run': {
1140
1174
  let taskRunEvent = new EventResponse<void>(
@@ -1202,6 +1236,11 @@ class OGIAddonWSListener {
1202
1236
  this.respondToMessage(message.id!!, taskRunResult.data, taskRunEvent);
1203
1237
  break;
1204
1238
  }
1239
+ case 'launch-app':
1240
+ await this.handleEventWithResponse<void>(message, (event) =>
1241
+ this.eventEmitter.emit('launch-app', message.args, event)
1242
+ );
1243
+ break;
1205
1244
  }
1206
1245
  });
1207
1246
  }
@@ -1234,6 +1273,47 @@ class OGIAddonWSListener {
1234
1273
  });
1235
1274
  }
1236
1275
 
1276
+ /**
1277
+ * Common flow for events that use EventResponse with userInputAsked: create event, emit via callback, wait, respond.
1278
+ * If options.requireListener is set and that event has no listeners, responds with options.noListenerError and returns.
1279
+ */
1280
+ private async handleEventWithResponse<T>(
1281
+ message: WebsocketMessageServer,
1282
+ emit: (event: EventResponse<T>) => void,
1283
+ options?: { requireListener: string; noListenerError: string }
1284
+ ): Promise<void> {
1285
+ const event = new EventResponse<T>((screen, name, description) =>
1286
+ this.userInputAsked(screen, name, description, this.socket)
1287
+ );
1288
+ if (
1289
+ options &&
1290
+ this.eventEmitter.listenerCount(options.requireListener) === 0
1291
+ ) {
1292
+ this.respondToMessage(
1293
+ message.id!!,
1294
+ { error: options.noListenerError },
1295
+ event
1296
+ );
1297
+ return;
1298
+ }
1299
+ emit(event);
1300
+ const result = await this.waitForEventToRespond(event);
1301
+ this.respondToMessage(message.id!!, result.data, event);
1302
+ }
1303
+
1304
+ /**
1305
+ * Same as handleEventWithResponse but for events that don't need userInputAsked (e.g. catalog).
1306
+ */
1307
+ private async handleEventWithResponseNoInput<T>(
1308
+ message: WebsocketMessageServer,
1309
+ emit: (event: EventResponse<T>) => void
1310
+ ): Promise<void> {
1311
+ const event = new EventResponse<T>();
1312
+ emit(event);
1313
+ const result = await this.waitForEventToRespond(event);
1314
+ this.respondToMessage(message.id!!, result.data, event);
1315
+ }
1316
+
1237
1317
  public respondToMessage(
1238
1318
  messageID: string,
1239
1319
  response: any,