Skip to content

Commit

Permalink
Add support for Stadium replays (#270)
Browse files Browse the repository at this point in the history
* add sopo icon

* add check for icon existing

* "under construction" pages for BTT/HRC

* progress

* add HRC distance to header

* rebase with main

* add target test stage image

* move sopo and sandbag icons to new location

* add HRC distance to stats page

* cleanup

* address suggestions

* change to switch

* add suggstions

Co-authored-by: Vince Au <vince@canva.com>
Update src/renderer/containers/ReplayBrowser/ReplayFile.tsx

add suggestion

Co-authored-by: Vince Au <vince@canva.com>
Update src/renderer/containers/ReplayBrowser/ReplayFile.tsx

add suggestion

Co-authored-by: Vince Au <vince@canva.com>
Update src/renderer/containers/ReplayFileStats/GameProfileHeader.tsx

add suggestion

Co-authored-by: Vince Au <vince@canva.com>
Update src/renderer/containers/ReplayFileStats/GameProfileHeader.tsx

add suggestion

Co-authored-by: Vince Au <vince@canva.com>
Update src/renderer/containers/ReplayFileStats/HomeRunProfile.tsx

add suggestion

Co-authored-by: Vince Au <vince@canva.com>
Update src/renderer/containers/ReplayFileStats/TargetTestProfile.tsx

add suggestion

Co-authored-by: Vince Au <vince@canva.com>

* update stock icon

* update icon

* better test for game type before calculating stats

---------

Co-authored-by: Vince Au <vince@canva.com>
  • Loading branch information
cnkeats and vinceau authored Feb 2, 2023
1 parent d6674eb commit 47549bd
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 36 deletions.
42 changes: 34 additions & 8 deletions src/renderer/containers/ReplayBrowser/ReplayFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import EventIcon from "@mui/icons-material/Event";
import LandscapeIcon from "@mui/icons-material/Landscape";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import TimerOutlinedIcon from "@mui/icons-material/TimerOutlined";
import SportsCricket from "@mui/icons-material/SportsCricket";
import TimerIcon from "@mui/icons-material/Timer";
import TrackChangesIcon from "@mui/icons-material/TrackChanges";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import type { FileResult } from "@replays/types";
import { stages as stageUtils } from "@slippi/slippi-js";
import { frameToGameTimer, GameMode, stages as stageUtils } from "@slippi/slippi-js";
import _ from "lodash";
import moment from "moment";
import React from "react";
Expand Down Expand Up @@ -73,6 +75,33 @@ export const ReplayFile: React.FC<ReplayFileProps> = ({
const stageInfo = settings.stageId !== null ? stageUtils.getStageInfo(settings.stageId) : null;
const stageImageUrl = stageInfo !== null && stageInfo.id !== -1 ? getStageImage(stageInfo.id) : undefined;
const stageName = stageInfo !== null ? stageInfo.name : "Unknown Stage";
const gameMode = settings.gameMode;
let gameModeIcon: React.ReactNode;

switch (gameMode) {
case GameMode.HOME_RUN_CONTEST:
gameModeIcon = <SportsCricket />;
break;
case GameMode.TARGET_TEST:
gameModeIcon = <TrackChangesIcon />;
break;
case GameMode.ONLINE:
case GameMode.VS:
default:
gameModeIcon = <LandscapeIcon />;
break;
}

let detailDisplay: { label: React.ReactNode; content: React.ReactNode } | null = null;
if (lastFrame !== null && gameMode !== GameMode.HOME_RUN_CONTEST) {
detailDisplay = {
label: <TimerIcon />,
content:
gameMode === GameMode.TARGET_TEST
? frameToGameTimer(lastFrame, settings)
: convertFrameCountToDurationString(lastFrame, "m[m] ss[s]"),
};
}

const teams: PlayerInfo[][] = _.chain(settings.players)
.groupBy((player) => (settings.isTeams ? player.teamId : player.port))
Expand Down Expand Up @@ -153,12 +182,9 @@ export const ReplayFile: React.FC<ReplayFileProps> = ({
>
<InfoItem label={<EventIcon />}>{monthDayHourFormat(moment(date))}</InfoItem>

{lastFrame !== null && (
<InfoItem label={<TimerOutlinedIcon />}>
{convertFrameCountToDurationString(lastFrame, "m[m] ss[s]")}
</InfoItem>
)}
<InfoItem label={<LandscapeIcon />}>{stageName}</InfoItem>
{detailDisplay && <InfoItem label={detailDisplay.label}>{detailDisplay.content}</InfoItem>}

<InfoItem label={gameModeIcon}>{stageName}</InfoItem>
</div>
<DraggableFile
filePaths={[fullPath]}
Expand Down
96 changes: 72 additions & 24 deletions src/renderer/containers/ReplayFileStats/GameProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { Straighten } from "@mui/icons-material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import EventIcon from "@mui/icons-material/Event";
import LandscapeIcon from "@mui/icons-material/Landscape";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import SportsCricket from "@mui/icons-material/SportsCricket";
import SportsEsportsIcon from "@mui/icons-material/SportsEsports";
import TimerOutlinedIcon from "@mui/icons-material/TimerOutlined";
import TimerIcon from "@mui/icons-material/Timer";
import TrackChangesIcon from "@mui/icons-material/TrackChanges";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import type { FileResult } from "@replays/types";
import type { GameStartType, MetadataType, StatsType } from "@slippi/slippi-js";
import { stages as stageUtils } from "@slippi/slippi-js";
import type { GameStartType, MetadataType, StadiumStatsType, StatsType } from "@slippi/slippi-js";
import { frameToGameTimer, GameMode, stages as stageUtils } from "@slippi/slippi-js";
import _ from "lodash";
import moment from "moment";
import React from "react";
Expand Down Expand Up @@ -96,10 +99,12 @@ export interface GameProfileHeaderProps {
onClose: () => void;
disabled?: boolean;
stats: StatsType | null;
stadiumStats: StadiumStatsType | null;
}

export const GameProfileHeader: React.FC<GameProfileHeaderProps> = ({
stats,
stadiumStats,
disabled,
file,
index,
Expand Down Expand Up @@ -149,7 +154,7 @@ export const GameProfileHeader: React.FC<GameProfileHeaderProps> = ({
</div>
<PlayerInfoDisplay metadata={metadata} settings={settings} />
</div>
<GameDetails file={file} stats={stats} />
<GameDetails file={file} stats={stats} stadiumStats={stadiumStats} settings={settings} />
</div>
<Controls disabled={disabled} index={index} total={total} onNext={onNext} onPrev={onPrev} onPlay={onPlay} />
</div>
Expand All @@ -158,8 +163,10 @@ export const GameProfileHeader: React.FC<GameProfileHeaderProps> = ({

const GameDetails: React.FC<{
file: FileResult;
settings: GameStartType;
stats: StatsType | null;
}> = ({ file, stats }) => {
stadiumStats: StadiumStatsType | null;
}> = ({ file, stats, stadiumStats }) => {
let stageName = "Unknown";
try {
stageName = stageUtils.getStageName(file.settings.stageId !== null ? file.settings.stageId : 0);
Expand All @@ -182,27 +189,68 @@ const GameDetails: React.FC<{
if (duration === null || duration === undefined) {
duration = _.get(stats, "lastFrame");
}

const durationLength =
duration !== null && duration !== undefined ? convertFrameCountToDurationString(duration, "m[m] ss[s]") : "Unknown";
duration !== null && duration !== undefined
? file.settings.gameMode == GameMode.TARGET_TEST && file.metadata
? frameToGameTimer(file.metadata?.lastFrame as number, file.settings)
: convertFrameCountToDurationString(duration, "m[m] ss[s]")
: "Unknown";

const distance = _.get(stadiumStats, "distance");
const units = _.get(stadiumStats, "units");

const eventDisplay = {
label: <EventIcon />,
content: monthDayHourFormat(moment(startAtDisplay)),
};

const timerDisplay = {
label: <TimerIcon />,
content: durationLength,
};

const stageDisplay = {
label: <LandscapeIcon />,
content: stageName,
};

const displayData = [
{
label: <EventIcon />,
content: monthDayHourFormat(moment(startAtDisplay)) as string,
},
{
label: <TimerOutlinedIcon />,
content: durationLength,
},
{
label: <LandscapeIcon />,
content: stageName,
},
{
label: <SportsEsportsIcon />,
content: platform,
},
];
const platformDisplay = {
label: <SportsEsportsIcon />,
content: platform,
};

const targetTestDisplay = {
label: <TrackChangesIcon />,
content: "Break the Targets",
};

const homerunDisplay = {
label: <SportsCricket />,
content: "Home Run Contest",
};

const distanceDisplay = {
label: <Straighten />,
content: `${distance} ${units}`,
};

const gameMode = file.settings.gameMode;

let displayData: { label: React.ReactNode; content: React.ReactNode }[];
switch (gameMode) {
case GameMode.HOME_RUN_CONTEST:
displayData = [eventDisplay, distanceDisplay, homerunDisplay, platformDisplay];
break;
case GameMode.TARGET_TEST:
displayData = [eventDisplay, timerDisplay, stageDisplay, targetTestDisplay, platformDisplay];
break;
case GameMode.ONLINE:
case GameMode.VS:
default:
displayData = [eventDisplay, timerDisplay, stageDisplay, platformDisplay];
break;
}

const metadataElements = displayData.map((details, i) => {
return (
Expand Down
25 changes: 25 additions & 0 deletions src/renderer/containers/ReplayFileStats/HomeRunProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Construction from "@mui/icons-material/Construction";
import type { FileResult } from "@replays/types";
import type { StadiumStatsType } from "@slippi/slippi-js";
import React from "react";

import { IconMessage } from "@/components/Message";

export interface GameProfileProps {
file: FileResult;
stats: StadiumStatsType | null;
}

const StatSection: React.FC<{
title: string;
}> = () => {
return <IconMessage Icon={Construction} label="This page is under construction" />;
};

export const HomeRunProfile: React.FC<GameProfileProps> = () => {
return (
<div style={{ flex: "1", margin: 20 }}>
<StatSection title="Home Run"></StatSection>
</div>
);
};
25 changes: 25 additions & 0 deletions src/renderer/containers/ReplayFileStats/TargetTestProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Construction from "@mui/icons-material/Construction";
import type { FileResult } from "@replays/types";
import type { StadiumStatsType } from "@slippi/slippi-js";
import React from "react";

import { IconMessage } from "@/components/Message";

export interface GameProfileProps {
file: FileResult;
stats: StadiumStatsType | null;
}

const StatSection: React.FC<{
title: string;
}> = () => {
return <IconMessage Icon={Construction} label="This page is under construction" />;
};

export const TargetTestProfile: React.FC<GameProfileProps> = () => {
return (
<div style={{ flex: "1", margin: 20 }}>
<StatSection title="Targets"></StatSection>
</div>
);
};
18 changes: 16 additions & 2 deletions src/renderer/containers/ReplayFileStats/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import type { FileResult } from "@replays/types";
import { GameMode } from "@slippi/slippi-js";
import _ from "lodash";
import React from "react";
import { useQuery } from "react-query";
Expand All @@ -23,6 +24,8 @@ import { withFont } from "@/styles/withFont";

import { GameProfile } from "./GameProfile";
import { GameProfileHeader } from "./GameProfileHeader";
import { HomeRunProfile } from "./HomeRunProfile";
import { TargetTestProfile } from "./TargetTestProfile";

const Outer = styled.div<{
backgroundImage?: any;
Expand Down Expand Up @@ -70,16 +73,22 @@ export const ReplayFileStats: React.FC<ReplayFileStatsProps> = (props) => {
const { dolphinService } = useServices();
const { viewReplays } = useDolphinActions(dolphinService);
const gameStatsQuery = useQuery(["loadStatsQuery", filePath], async () => {
const result = window.electron.replays.calculateGameStats(filePath);
const result = await window.electron.replays.calculateGameStats(filePath);
return result;
});

const loading = gameStatsQuery.isLoading;
const stadiumStatsQuery = useQuery(["loadStadiumStatsQuery", filePath], async () => {
const result = await window.electron.replays.calculateStadiumStats(filePath);
return result;
});

const loading = gameStatsQuery.isLoading && stadiumStatsQuery.isLoading;
const error = gameStatsQuery.error as any;

const file = gameStatsQuery.data?.file ?? props.file;
const numPlayers = file?.settings.players.length;
const gameStats = gameStatsQuery.data?.stats ?? null;
const stadiumStats = stadiumStatsQuery.data?.stadiumStats ?? null;

// Add key bindings
useMousetrap("escape", () => {
Expand Down Expand Up @@ -135,11 +144,16 @@ export const ReplayFileStats: React.FC<ReplayFileStatsProps> = (props) => {
file={file}
disabled={loading}
stats={gameStatsQuery.data?.stats ?? null}
stadiumStats={stadiumStatsQuery.data?.stadiumStats ?? null}
onPlay={props.onPlay}
/>
<Content>
{!file || loading ? (
<LoadingScreen message={"Crunching numbers..."} />
) : file.settings.gameMode == GameMode.TARGET_TEST ? (
<TargetTestProfile file={file} stats={stadiumStats}></TargetTestProfile>
) : file.settings.gameMode == GameMode.HOME_RUN_CONTEST ? (
<HomeRunProfile file={file} stats={stadiumStats}></HomeRunProfile>
) : numPlayers !== 2 ? (
<IconMessage Icon={ErrorIcon} label="Game stats for doubles is unsupported" />
) : error ? (
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const getCharacterIcon = (characterId: number | null, characterColor: num
export const getStageImage = (stageId: number): string => {
const stageInfo = stageUtils.getStageInfo(stageId);
if (stageInfo.id !== stageUtils.UnknownStage.id) {
if (stageInfo.id >= 33 && stageInfo.id <= 58) {
return stageIcons(`./targets.png`);
}
try {
return stageIcons(`./${stageId}.png`);
} catch (err) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/renderer/styles/images/stages/84.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/renderer/styles/images/stages/targets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/replays/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable import/no-default-export */
import {
ipc_calculateGameStats,
ipc_calculateStadiumStats,
ipc_initializeFolderTree,
ipc_loadProgressUpdatedEvent,
ipc_loadReplayFolder,
Expand All @@ -26,6 +27,10 @@ export default {
const { result } = await ipc_calculateGameStats.renderer!.trigger({ filePath });
return result;
},
async calculateStadiumStats(filePath: string) {
const { result } = await ipc_calculateStadiumStats.renderer!.trigger({ filePath });
return result;
},
onReplayLoadProgressUpdate(handle: (progress: Progress) => void) {
const { destroy } = ipc_loadProgressUpdatedEvent.renderer!.handle(async (progress) => {
handle(progress);
Expand Down
8 changes: 7 additions & 1 deletion src/replays/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { StatsType } from "@slippi/slippi-js";
import type { StadiumStatsType, StatsType } from "@slippi/slippi-js";
import { _, makeEndpoint } from "utils/ipc";

import type { FileLoadResult, FileResult, FolderResult, Progress } from "./types";
Expand All @@ -25,6 +25,12 @@ export const ipc_calculateGameStats = makeEndpoint.main(
<{ file: FileResult; stats: StatsType | null }>_,
);

export const ipc_calculateStadiumStats = makeEndpoint.main(
"calculateStadiumStats",
<{ filePath: string }>_,
<{ file: FileResult; stadiumStats: StadiumStatsType | null }>_,
);

// Events

export const ipc_loadProgressUpdatedEvent = makeEndpoint.renderer("replays_loadProgressUpdated", <Progress>_);
Expand Down
Loading

0 comments on commit 47549bd

Please sign in to comment.