Skip to content

[Feat] Use Legendary API to get SDL List (Fix Fortnite install and more) #2746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 31, 2023
Merged
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