Skip to content

Commit

Permalink
[Feat] Use Legendary API to get SDL List (Fix Fortnite install and mo…
Browse files Browse the repository at this point in the history
…re) (#2746)

* feat: implement legendary api calls and types

* feat: replace old SDL handler with the new api call

* fix: Browser option on non-sideloaded games

* fix: check if correct response when calling api

* chore: do not show error dialog for memory errors

* fix: disable node integration for browser games

* chore: dead code

* chore: add methods inside try catch block

* feat: cache legendary response api
  • Loading branch information
flavioislima authored May 31, 2023
1 parent 8eb4203 commit 6713384
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 95 deletions.
5 changes: 5 additions & 0 deletions src/backend/api/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@ export const handleRecentGamesChanged = (callback: any) => {
}

export const addNewApp = (args: GameInfo) => ipcRenderer.send('addNewApp', args)

export const getGameOverride = async () => ipcRenderer.invoke('getGameOverride')

export const getGameSdl = async (appName: string) =>
ipcRenderer.invoke('getGameSdl', appName)
11 changes: 9 additions & 2 deletions src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ import {
} from './storeManagers'
import { setupUbisoftConnect } from 'backend/storeManagers/legendary/setup'

import { logFileLocation as getLogFileLocation } from './storeManagers/storeManagerCommon/games'
import { addNewApp } from './storeManagers/sideload/library'
import {
getGameOverride,
getGameSdl
} from 'backend/storeManagers/legendary/library'

app.commandLine?.appendSwitch('remote-debugging-port', '9222')

const { showOpenDialog } = dialog
Expand Down Expand Up @@ -636,6 +643,8 @@ ipcMain.handle('getLegendaryVersion', getLegendaryVersion)
ipcMain.handle('getGogdlVersion', getGogdlVersion)
ipcMain.handle('isFullscreen', () => isSteamDeckGameMode || isCLIFullscreen)
ipcMain.handle('isFlatpak', () => isFlatpak)
ipcMain.handle('getGameOverride', async () => getGameOverride())
ipcMain.handle('getGameSdl', async (event, appName) => getGameSdl(appName))

ipcMain.handle('getPlatform', () => process.platform)

Expand Down Expand Up @@ -1672,5 +1681,3 @@ import './downloadmanager/ipc_handler'
import './utils/ipc_handler'
import './wiki_game_info/ipc_handler'
import './recent_games/ipc_handler'
import { logFileLocation as getLogFileLocation } from './storeManagers/storeManagerCommon/games'
import { addNewApp } from './storeManagers/sideload/library'
22 changes: 17 additions & 5 deletions src/backend/storeManagers/legendary/electronStores.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import CacheStore from '../../cache'
import { ExtraInfo, GameInfo } from 'common/types'
import { LegendaryInstallInfo } from 'common/types/legendary'
import { GameOverride, LegendaryInstallInfo } from 'common/types/legendary'

const installStore = new CacheStore<LegendaryInstallInfo>(
export const installStore = new CacheStore<LegendaryInstallInfo>(
'legendary_install_info'
)
const libraryStore = new CacheStore<GameInfo[], 'library'>(
export const libraryStore = new CacheStore<GameInfo[], 'library'>(
'legendary_library',
null
)

const gameInfoStore = new CacheStore<ExtraInfo>('legendary_gameinfo')
/**
* Store for the games override
* Lasts for 7 days
* @type {CacheStore<GameOverride, 'gamesOverride'>}
* @memberof module:storeManagers/legendary
* @inner
* @instance
**/
export const gamesOverrideStore: CacheStore<GameOverride, 'gamesOverride'> =
new CacheStore<GameOverride, 'gamesOverride'>(
'legendary_games_override',
60 * 24 * 7
)

export { gameInfoStore, installStore, libraryStore }
export const gameInfoStore = new CacheStore<ExtraInfo>('legendary_gameinfo')
75 changes: 73 additions & 2 deletions src/backend/storeManagers/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
InstalledJsonMetadata,
GameMetadata,
LegendaryInstallInfo,
LegendaryInstallPlatform
LegendaryInstallPlatform,
ResponseDataLegendaryAPI,
SelectiveDownload,
GameOverride
} from 'common/types/legendary'
import { LegendaryUser } from './user'
import {
Expand All @@ -37,11 +40,16 @@ import {
LogPrefix,
logWarning
} from '../../logger/logger'
import { installStore, libraryStore } from './electronStores'
import {
gamesOverrideStore,
installStore,
libraryStore
} from './electronStores'
import { callRunner } from '../../launcher'
import { dirname, join } from 'path'
import { isOnline } from 'backend/online_monitor'
import { update } from './games'
import axios from 'axios'

const allGames: Set<string> = new Set()
let installedGames: Map<string, InstalledJsonMetadata> = new Map()
Expand Down Expand Up @@ -634,3 +642,66 @@ export async function runRunnerCommand(
}
)
}

export async function getGameOverride(): Promise<GameOverride> {
const cached = gamesOverrideStore.get('gamesOverride')
if (cached) {
return cached
}

try {
const response = await axios.get<ResponseDataLegendaryAPI>(
'https://heroic.legendary.gl/v1/version.json'
)

if (response.data.game_overrides) {
gamesOverrideStore.set('gamesOverride', response.data.game_overrides)
}

return response.data.game_overrides
} catch (error) {
logWarning(['Error fetching Legendary API:', error], LogPrefix.Legendary)
throw error
}
}

export async function getGameSdl(
appName: string
): Promise<SelectiveDownload[]> {
try {
const response = await axios.get<Record<string, SelectiveDownload>>(
`https://heroic.legendary.gl/v1/sdl/${appName}.json`
)

// if data type is not a json return empty array
if (response.headers['content-type'] !== 'application/json') {
logInfo(
['No Selective Download data found for', appName],
LogPrefix.Legendary
)
return []
}

const list = Object.keys(response.data)
const sdlList: SelectiveDownload[] = []

list.forEach((key) => {
const { name, description, tags } = response.data[
key
] as SelectiveDownload
if (key === '__required') {
sdlList.unshift({ name, description, tags, required: true })
} else {
sdlList.push({ name, description, tags })
}
})

return sdlList
} catch (error) {
logWarning(
['Error fetching Selective Download data for', appName, error],
LogPrefix.Legendary
)
return []
}
}
5 changes: 1 addition & 4 deletions src/backend/storeManagers/storeManagerCommon/games.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ const openNewBrowserGameWindow = async (
icon: icon,
fullscreen: true,
webPreferences: {
partition: `persist:${hostname}`,
webviewTag: true,
contextIsolation: true,
nodeIntegration: true
partition: `persist:${hostname}`
}
})

Expand Down
5 changes: 5 additions & 0 deletions src/backend/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,11 @@ async function errorHandler({
}

if (legendaryRegex.test(error)) {
const MemoryError = 'MemoryError: '
if (error.includes(MemoryError)) {
return
}

return showDialogBoxModalAuto({
title: plat,
message: i18next.t(
Expand Down
4 changes: 3 additions & 1 deletion src/common/typedefs/ipcBridge.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
ExtraInfo,
LaunchOption
} from 'common/types'
import { LegendaryInstallInfo } from 'common/types/legendary'
import { LegendaryInstallInfo, SelectiveDownload } from 'common/types/legendary'
import { GOGCloudSavesLocation, GogInstallInfo } from 'common/types/gog'

/**
Expand Down Expand Up @@ -241,6 +241,8 @@ interface AsyncIPCFunctions {
toggleDXVK: (args: ToolArgs) => Promise<boolean>
pathExists: (path: string) => Promise<boolean>
getGOGLaunchOptions: (appName: string) => Promise<LaunchOption[]>
getGameOverride: () => Promise<GameOverride | null>
getGameSdl: (appName: string) => Promise<SelectiveDownload[]>
}

// This is quite ugly & throws a lot of errors in a regular .ts file
Expand Down
54 changes: 54 additions & 0 deletions src/common/types/legendary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,57 @@ interface TagInfo {
// How big the tag is (in bytes)
size: number
}

// types for the Legendary API https://heroic.legendary.gl/v1/version.json
/* export type CxBottle = {
base_url: string | null
compatible_apps: string[]
cx_system: string
cx_version: string
cx_versions: string[]
description: string
is_default: boolean
manifest: string
name: string
version: number
}
export type GameWiki = Record<string, Record<string, string>> */

export type GameOverride = {
executable_override: Record<string, Record<string, string>>
reorder_optimization: Record<string, string[]>
sdl_config: Record<string, number>
}

type LegendaryConfig = {
webview_killswitch: boolean
}

/* export type ReleaseInfoLegendaryAPI = {
critical: boolean
download_hashes: Record<string, string>
downloads: Record<string, string>
gh_url: string
name: string
summary: string
version: string
} */

export type ResponseDataLegendaryAPI = {
// cx_bottles: CxBottle[]
egl_config: Record<string, unknown>
game_overrides: GameOverride
// game_wiki: GameWiki
legendary_config: LegendaryConfig
// release_info: ReleaseInfoLegendaryAPI
runtimes: unknown[]
}

export interface SelectiveDownload {
tags: Array<string>
name: string
description: string
required?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const DLCDownloadListing: React.FC<Props> = ({
htmlId={`dlc-${index}`}
value={dlcsToInstall.includes(app_name)}
title={title}
extraClass="InstallModal__toggle--sdl"
handleChange={() => handleDlcToggle(index)}
/>
</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
WineInstallation
} from 'common/types'
import { GogInstallInfo } from 'common/types/gog'
import { LegendaryInstallInfo } from 'common/types/legendary'
import { LegendaryInstallInfo, SelectiveDownload } from 'common/types/legendary'
import {
PathSelectionBox,
SelectField,
Expand Down Expand Up @@ -44,7 +44,6 @@ import React, {
} from 'react'
import { useTranslation } from 'react-i18next'
import { AvailablePlatforms } from '../index'
import { SDL_GAMES, SelectiveDownload } from '../selective_dl'
import { configStore } from 'frontend/helpers/electronStores'
import DLCDownloadListing from './DLCDownloadListing'

Expand Down Expand Up @@ -89,7 +88,10 @@ function getInstallLanguage(
}

function getUniqueKey(sdl: SelectiveDownload) {
return sdl.tags.join(',')
if (sdl.tags) {
return sdl.tags.join(',')
}
return ''
}

const userHome = configStore.get('userHome', '')
Expand Down Expand Up @@ -136,6 +138,7 @@ export default function DownloadDialog({

const [dlcsToInstall, setDlcsToInstall] = useState<string[]>([])
const [installAllDlcs, setInstallAllDlcs] = useState(false)
const [sdls, setSdls] = useState<SelectiveDownload[]>([])
const [selectedSdls, setSelectedSdls] = useState<{ [key: string]: boolean }>(
{}
)
Expand All @@ -153,14 +156,13 @@ export default function DownloadDialog({
const { i18n, t } = useTranslation('gamepage')
const { t: tr } = useTranslation()

const sdls: SelectiveDownload[] | undefined = SDL_GAMES[appName]
const haveSDL = Array.isArray(sdls) && sdls.length !== 0
const haveSDL = sdls.length > 0

const sdlList = useMemo(() => {
const list = []
if (sdls) {
if (haveSDL) {
for (const sdl of sdls) {
if (sdl.mandatory || selectedSdls[getUniqueKey(sdl)]) {
if (sdl.required || selectedSdls[getUniqueKey(sdl)]) {
if (Array.isArray(sdl.tags)) {
list.push(...sdl.tags)
}
Expand Down Expand Up @@ -269,6 +271,21 @@ export default function DownloadDialog({
getIinstInfo()
}, [appName, i18n.languages, platformToInstall])

useEffect(() => {
const getGameSdl = async () => {
if (runner === 'legendary') {
const { sdl_config } = await window.api.getGameOverride()
if (sdl_config && sdl_config[appName]) {
const sdl = await window.api.getGameSdl(appName)
if (sdl.length > 0) {
setSdls(sdl)
}
}
}
}
getGameSdl()
}, [appName, runner])

useEffect(() => {
const getSpace = async () => {
const { message, free, validPath } = await window.api.checkDiskSpace(
Expand Down Expand Up @@ -518,11 +535,11 @@ export default function DownloadDialog({
<ToggleSwitch
htmlId={`sdls-${idx}`}
title={sdl.name}
value={!!sdl.mandatory || !!selectedSdls[getUniqueKey(sdl)]}
disabled={sdl.mandatory}
extraClass="InstallModal__toggle--sdl"
value={!!sdl.required || !!selectedSdls[getUniqueKey(sdl)]}
disabled={sdl.required}
handleChange={(e) => handleSdl(sdl, e.target.checked)}
/>
<span>{sdl.name}</span>
</label>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
.InstallModal__dlcs,
.InstallModal__sdls {
margin: 0 var(--space-md);

.InstallModal__toggle--sdl {
padding: 0 !important;
}
}

.InstallModal__dlcsList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from 'common/types'
import { Dialog } from 'frontend/components/UI/Dialog'

import './index.css'
import './index.scss'

import DownloadDialog from './DownloadDialog'
import SideloadDialog from './SideloadDialog'
Expand Down Expand Up @@ -77,7 +77,7 @@ export default React.memo(function InstallModal({
},
{
name: 'Browser',
available: true,
available: isSideload,
value: 'Browser',
icon: faGlobe
}
Expand Down
Loading

0 comments on commit 6713384

Please sign in to comment.