Skip to content
This repository has been archived by the owner on Jul 27, 2022. It is now read-only.

Commit

Permalink
Merge pull request #69 from stromcon/feature/mods
Browse files Browse the repository at this point in the history
Feature/mods
  • Loading branch information
CapitaineJSparrow authored May 26, 2021
2 parents 8bf2743 + 5c5cc5f commit a313f25
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "emusak",
"productName": "emusak",
"version": "1.0.60",
"version": "1.0.61",
"description": "Saves, shaders, firmwares and keys manager for switch emulators",
"main": ".webpack/main",
"repository": "https://github.com/stromcon/emusak-ui.git",
Expand Down
25 changes: 25 additions & 0 deletions src/api/emusak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,18 @@ export interface IEmusakSaves {
[key: string]: string;
}

interface IEmusakMod {
name: string;
type: string;
mtime: string;
}

export type IEmusakMods = IEmusakMod[];

export const enum PATHS {
COUNT_SHADERS = '/api/shaders/ryujinx/count',
LIST_SAVES = '/api/saves',
LIST_MODS = '/mods/',
FIRMWARE_VERSION = '/api/firmware/version',
PROD_KEYS = '/api/keys',
FIRMWARE_DOWNLOAD = '/firmware/firmware.zip',
Expand All @@ -22,6 +31,22 @@ export const getEmusakShadersCount = async (): Promise<IEmusakShadersCount> => f

export const getEmusakSaves = async (): Promise<IEmusakSaves> => fetchWithRetries(`${process.env.EMUSAK_URL}${PATHS.LIST_SAVES}`).then((r: Response) => r.json())

export const getEmusakMods = async (): Promise<IEmusakMods> => fetchWithRetries(`${process.env.EMUSAK_CDN}${PATHS.LIST_MODS}`).then((r: Response) => r.json())

export const getEmusakModsVersionsForGame = async (titleId: string): Promise<IEmusakMods> => fetchWithRetries(`${process.env.EMUSAK_CDN}${PATHS.LIST_MODS}/${titleId}/`).then((r: Response) => r.json())

export const getEmusakModsForGameWithVersion = async (titleId: string, version: string): Promise<IEmusakMods> => {
return fetchWithRetries(encodeURI(`${process.env.EMUSAK_CDN}${PATHS.LIST_MODS}/${titleId}/${version}/`)).then((r: Response) => r.json());
}

export const getEmusakMod = async (titleId: string, version: string, mod: string): Promise<any> => {
return fetchWithRetries(encodeURI(`${process.env.EMUSAK_CDN}${PATHS.LIST_MODS}/${titleId}/${version}/${mod}/`)).then((r: Response) => r.json());
}

export const downloadEmusakMod = async (titleId: string, version: string, mod: string, file:string): Promise<any> => {
return fetchWithRetries(encodeURI(`${process.env.EMUSAK_CDN}${PATHS.LIST_MODS}/${titleId}/${version}/${mod}/${file}`)).then((r: Response) => r.text());
}

export const getEmusakFirmwareVersion = async (): Promise<string> => fetchWithRetries(`${process.env.EMUSAK_URL}${PATHS.FIRMWARE_VERSION}`).then((r: Response) => r.text()).then(v => v);

export const getEmusakProdKeys = async (): Promise<string> => fetchWithRetries(`${process.env.EMUSAK_URL}${PATHS.PROD_KEYS}`).then((r: Response) => r.text()).then(v => v);
Expand Down
17 changes: 17 additions & 0 deletions src/service/ryujinx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,20 @@ export const shareShader = async (
});
})
}

export const installModToRyujinx = async (config: IRyujinxConfig, titleID: string, modName: string, modFileName: string, content: string) => {
let modPath = getRyujinxPath(config, 'mods', 'contents', titleID, modName, 'exefs');

const exists = await fs.promises.access(modPath).then(() => true).catch(() => false);

if (!exists) {
await fs.promises.mkdir(modPath, {recursive: true});
}

const filePath = path.resolve(modPath, modFileName);
await fs.promises.writeFile(filePath, content, 'utf-8');
await Swal.fire({
icon: 'success',
text: `${modName} successfully installed at ${filePath}`
})
}
6 changes: 1 addition & 5 deletions src/ui/changelog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ const Changelog = () => {
<h1 style={{ textAlign: 'center' }}>What's new ? v{version}</h1>
<br />
<ul style={{ marginLeft: 20 }}>
<li>Some debug & add game version when you share shaders</li>
<li>Added a loader when you share shaders and upload is pending</li>
<li>Added a threshold indicator to explain when you can share shaders</li>
<li>Make the new share shaders feature compatible for our linux users</li>
<li>When you click on the "Share Shaders" button, Emusak will now ask to open Ryujinx and open the game to read Ryujinx logs and be sure no errors occurred. This is designed to reduce workload on our side, because validation submissions is very time consuming</li>
<li>Mods support is finally here ! For now there is only mods from <a href="#" onClick={() => electron.shell.openExternal("https://github.com/theboy181/switch-ptchtxt-mods")}>TheBoy181</a></li>
</ul>
<br/>
<p>
Expand Down
17 changes: 16 additions & 1 deletion src/ui/page/ryujinx/RyuGameList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import {
import eshopData from "../../../assets/test.json";
import { IRyujinxConfig } from "../../../model/RyujinxModel";
import {
IEmusakMods,
IEmusakSaves,
IEmusakShadersCount
} from "../../../api/emusak";
import { DeleteOutline } from "@material-ui/icons";
import IconButton from "@material-ui/core/IconButton";
import ShadersList from "./gamelist/ShadersList";
import SaveList from "./gamelist/SaveList";
import ModsList from "./gamelist/ModsList";

interface IRyuGameListProps {
config: IRyujinxConfig;
Expand All @@ -45,6 +47,7 @@ interface IRyuGameListProps {
emusakShadersCount: IEmusakShadersCount;
emusakSaves: IEmusakSaves;
emusakFirmwareVersion: string;
emusakMods: IEmusakMods;
}

const useStyles = makeStyles((theme) => ({
Expand Down Expand Up @@ -85,7 +88,7 @@ function TabPanel(props: any) {



const RyuGameList = ({ config, onConfigDelete, threshold, customDatabase, emusakShadersCount, emusakSaves, emusakFirmwareVersion }: IRyuGameListProps) => {
const RyuGameList = ({ config, onConfigDelete, threshold, customDatabase, emusakShadersCount, emusakSaves, emusakFirmwareVersion, emusakMods }: IRyuGameListProps) => {
const classes = useStyles();
const [currentGame, setCurrentGame] = useState('');
const [games, setGames]: [string[], Function] = useState([]);
Expand Down Expand Up @@ -209,6 +212,14 @@ const RyuGameList = ({ config, onConfigDelete, threshold, customDatabase, emusak
triggerShadersDownload={triggerShadersDownload}
emusakSaves={emusakSaves}
/>
case 2:
return <ModsList
games={games}
extractNameFromID={extractNameFromID}
emusakMods={emusakMods}
config={config}
filter={filter}
/>
default:
return <ShadersList
games={games}
Expand Down Expand Up @@ -333,6 +344,7 @@ const RyuGameList = ({ config, onConfigDelete, threshold, customDatabase, emusak
<Tabs value={tabIndex} onChange={handleTabChange} aria-label="simple tabs example">
<Tab label="Shaders" />
<Tab label="Saves" />
<Tab label="Mods" />
</Tabs>
</AppBar>
<TabPanel value={`${tabIndex}`}>
Expand All @@ -341,6 +353,9 @@ const RyuGameList = ({ config, onConfigDelete, threshold, customDatabase, emusak
<TabPanel value={`${tabIndex}`}>
Saves
</TabPanel>
<TabPanel value={`${tabIndex}`}>
Mods
</TabPanel>

<Grid container style={{ margin: '0 0 20px 0' }}>
<Grid item xs={12}>
Expand Down
5 changes: 4 additions & 1 deletion src/ui/page/ryujinx/RyujinxHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Swal from "sweetalert2";
import HelpIcon from '@material-ui/icons/Help';

import {
getEmusakFirmwareVersion,
getEmusakFirmwareVersion, getEmusakMods,
getEmusakSaves,
getEmusakShadersCount,
IEmusakSaves,
Expand All @@ -24,6 +24,7 @@ const RyujinxHome = () => {
const [emusakShadersCount, setEmusakShadersCount]: [IEmusakShadersCount, Function] = useState(null);
const [emusakSaves, setEmusakSaves]: [IEmusakSaves, Function] = useState({});
const [emusakFirmwareVersion, setEmusakFirmwareVersion]: [string, Function] = useState('');
const [emusakMods, setEmusakMods] = useState([]);

/**
* When user pick a ryujinx folder, ensure it is valid (has Ryujinx file) and check if it is portable mode or not
Expand Down Expand Up @@ -79,6 +80,7 @@ const RyujinxHome = () => {

getEmusakSaves().then(s => setEmusakSaves(s));
getEmusakFirmwareVersion().then(v => setEmusakFirmwareVersion(v));
getEmusakMods().then(m => setEmusakMods(m));
}, []);

return (
Expand Down Expand Up @@ -123,6 +125,7 @@ const RyujinxHome = () => {
emusakShadersCount={emusakShadersCount}
emusakSaves={emusakSaves}
emusakFirmwareVersion={emusakFirmwareVersion}
emusakMods={emusakMods}
/>
)
}
Expand Down
154 changes: 154 additions & 0 deletions src/ui/page/ryujinx/gamelist/ModsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from "react";
import {
Avatar,
Button,
Dialog,
DialogTitle,
List,
ListItem,
ListItemAvatar, ListItemText,
TableBody,
TableCell,
TableRow
} from "@material-ui/core";
import {
downloadEmusakMod,
getEmusakMod,
getEmusakModsForGameWithVersion,
getEmusakModsVersionsForGame,
IEmusakMods
} from "../../../../api/emusak";
import FileCopyIcon from "@material-ui/icons/FileCopy";
import {IRyujinxConfig} from "../../../../model/RyujinxModel";
import {installModToRyujinx} from "../../../../service/ryujinx";

interface IModsListProps {
games: string[],
extractNameFromID: Function;
emusakMods: IEmusakMods;
config: IRyujinxConfig;
filter: string;
}

export default ({
games,
extractNameFromID,
emusakMods,
config,
filter,
}: IModsListProps) => {
const [dialogVersionOpen, setDialogVersionOpen] = React.useState(false);
const [modsDialogOpen, setModsDialogOpen] = React.useState(false);

const [modsName, setModsName]: [string[], Function] = React.useState([]);
const [modsVersions, setModsVersions]: [string[], Function] = React.useState([]);
const [pickedTitleId, setPickedTitleId]: [string | null, Function] = React.useState(null);
const [pickedVersion, setPickedVersion]: [string | null, Function] = React.useState(null);

const handleDialogVersionClose = () => setDialogVersionOpen(false)

const handleDialogModsClose = () => setModsDialogOpen(false)

const handleVersionPick = async (titleId: string) => {
let versions = await getEmusakModsVersionsForGame(titleId);
const gameVersions = versions.map(v => v.name);
setModsVersions(gameVersions);
setDialogVersionOpen(true);
setPickedTitleId(titleId);
}

const handleModVersionPick = async (version: string) => {
const modsResponse = await getEmusakModsForGameWithVersion(pickedTitleId, version);
const mods = modsResponse.map(m => m.name);
setDialogVersionOpen(false);
setModsDialogOpen(true);
setModsName(mods);
setPickedVersion(version);
}

const applyMod = async (modName: string) => {
const modFile: any[] = await getEmusakMod(pickedTitleId, pickedVersion, modName);
const mod = modFile.find(m => m.type === "file").name;
setModsDialogOpen(false);
const file = await downloadEmusakMod(pickedTitleId, pickedVersion, modName, mod);
await installModToRyujinx(config, pickedTitleId, modName, mod, file);
}

return (
<TableBody>

<Dialog onClose={handleDialogVersionClose} aria-labelledby="save-dialog-ryu" open={dialogVersionOpen}>
<DialogTitle id="save-dialog-ryu">
What is your game version ?
</DialogTitle>
<List>
{
modsVersions.map(version => (
<ListItem onClick={() => handleModVersionPick(version)} key={`ryu-mod-${version}`} button>
<ListItemAvatar>
<Avatar>
<FileCopyIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={version} />
</ListItem>
))
}
</List>
</Dialog>

<Dialog onClose={handleDialogModsClose} aria-labelledby="save-dialog-ryu" open={modsDialogOpen}>
<DialogTitle id="save-dialog-ryu">
Pick a mod
</DialogTitle>
<List>
{
modsName.map(m => (
<ListItem onClick={() => applyMod(m)} key={`ryu-mod-${m}`} button>
<ListItemAvatar>
<Avatar>
<FileCopyIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={m} />
</ListItem>
))
}
</List>
</Dialog>

{
games
.filter(titleId => titleId != '0000000000000000')
.map(titleId => ({titleId: titleId.toUpperCase(), name: extractNameFromID(titleId)}))
.sort((a, b) => a.name.localeCompare(b.name))
.map(({titleId, name}) => {

if (filter && name.toLowerCase().search(filter.toLowerCase()) === -1) {
return null;
}

return (
<TableRow key={`ryu-mods-list-${titleId}`}>
<TableCell>
<span>{name}</span>
<br />
<span><small>{titleId.toUpperCase()}</small></span>
</TableCell>
<TableCell>
<Button
onClick={() => handleVersionPick(titleId)}
disabled={!emusakMods.find(m => m.name === titleId)}
variant="contained"
color="primary"
>
Download mods
</Button>
</TableCell>
</TableRow>
)
})
}
</TableBody>
)
}
2 changes: 2 additions & 0 deletions webservices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
/api/shaders/ryujinx
/api/shaders/yuzu
/api/shaders/ryujinx/count
/api/ptchtxt
/api/romfs

D /api/keys
D /api/firmware
Expand Down

0 comments on commit a313f25

Please sign in to comment.