Add time trial rankings to mk8 website

This commit is contained in:
Rambo6Glaz 2024-04-08 07:13:59 +02:00
parent d38f258594
commit 89026ac40f
11 changed files with 3991 additions and 3093 deletions

7
.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"useTabs": true,
"printWidth": 150,
"tabWidth": 4,
"trailingComma": "all",
"singleQuote": false
}

View file

@ -0,0 +1,33 @@
import app_config from "@/app.config";
import { amkj_grpc_client } from "@/helpers/grpc";
import { type NextRequest } from 'next/server'
import { NextResponse } from "next/server";
import { Metadata } from "nice-grpc";
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
request.url; // https://nextjs.org/docs/app/building-your-application/routing/router-handlers#dynamic-route-handlers
try {
const trackId = parseInt(params.id);
if (isNaN(trackId)) {
throw new Error("Invalid track ID");
}
const searchParams = request.nextUrl.searchParams;
let ascFilter = true;
if (searchParams.has("desc")) {
ascFilter = false;
}
const response = await amkj_grpc_client.getTimeTrialRanking({ track: trackId, limit: 10, asc: ascFilter }, {
metadata: Metadata({
"X-API-Key": app_config.grpc_api_key
})
});
return NextResponse.json(response, { status: 200 });
} catch (err) {
return new NextResponse("{}", { status: 500 });
}
}

View file

@ -1,15 +1,59 @@
/* eslint-disable @next/next/no-img-element */
'use client'
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Alert, Badge, Carousel, ListGroup, Tab, Tabs } from "react-bootstrap";
const HomePage = () => {
function getTimeLeftUntilEnd(): string | null {
const currentDate = new Date();
const currentUTCDate = new Date(currentDate.getTime() + currentDate.getTimezoneOffset() * 60000);
// Define the target date (8th April 4 PM PDT)
const targetDate = new Date("2024-04-08T16:00:00-07:00");
if (currentUTCDate > targetDate) {
return null;
}
const differenceMs = targetDate.getTime() - currentUTCDate.getTime();
const hours = Math.floor(differenceMs / (1000 * 60 * 60));
const minutes = Math.floor((differenceMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((differenceMs % (1000 * 60)) / 1000);
const formattedTime = `${hours} hours, ${minutes} minutes, ${seconds} seconds`;
return formattedTime;
}
const [timeLeft, setTimeLeft] = useState(getTimeLeftUntilEnd());
useEffect(() => {
const interval = setInterval(() => {
setTimeLeft(getTimeLeftUntilEnd());
}, 1000);
return () => clearInterval(interval);
});
return (
<div className="container-fluid mt-5 mb-3 h-100 p-0 d-flex justify-content-center align-items-center">
<form className="text-center w-100 p-5 bg-light shadow-lg">
<h1 className="mb-3">Welcome to the Pretendo Mario Kart 8 website</h1>
<h6><small className="text-muted">A full game server replacement for MK8</small></h6>
<h6>
<small className="text-muted">A full game server replacement for MK8</small>
</h6>
{
!timeLeft ? (
<Alert variant="primary" className="mt-3">
The official server will be closed in: <strong>{timeLeft}</strong>
</Alert>
) : (
<Alert variant="danger" className="mt-3">
<strong>The official Nintendo server closed on the 8th of April at 4pm EDT</strong>
</Alert>
)
}
<Tabs defaultActiveKey="home" className="mb-3">
<Tab eventKey="home" title="Overview">
<Carousel className="mb-3" variant="dark">
@ -50,7 +94,9 @@ const HomePage = () => {
<Alert>
<Alert.Heading>How to play on this server?</Alert.Heading>
<hr />
<p> Follow the steps to join the Pretendo Network on <Link href="https://pretendo.network">our website</Link>!</p>
<p>
Follow the steps to join the Pretendo Network on <Link href="https://pretendo.network">our website</Link>!
</p>
<p> Create / log on your PNID account and simply start Mario Kart 8</p>
</Alert>
<Alert variant="success">
@ -63,7 +109,8 @@ const HomePage = () => {
<p> Have common sense, we may issue a ban even for something that is not on list</p>
<hr />
<p className="mb-0">
<strong>Breaching the rules can get your account or console temporarily/permanently banned from our services</strong> (🤓)
<strong>Breaching the rules can get your account or console temporarily/permanently banned from our services</strong>{" "}
(🤓)
</p>
</Alert>
</Tab>
@ -74,7 +121,9 @@ const HomePage = () => {
<ListGroup className="mb-3">
<strong>
<ListGroup.Item variant="primary">Works for everyone</ListGroup.Item>
<ListGroup.Item variant="info">Fully/partially a Miiverse related feature, might no be available on Cemu, might require a Tester account.</ListGroup.Item>
<ListGroup.Item variant="info">
Fully/partially a Miiverse related feature, might no be available on Cemu, might require a Tester account.
</ListGroup.Item>
<ListGroup.Item variant="danger">Does NOT work</ListGroup.Item>
</strong>
</ListGroup>
@ -130,15 +179,21 @@ const HomePage = () => {
<p> Use CEMU 2.0-43 experimental or higher</p>
<p> {"We don't support piracy and we recommend dumping files from your own console."}</p>
<p> {"Files downloaded from the website aren't supported anymore"}</p>
<p> {"If you have unstable frames, plesase don't go online, it ruins the experience for everyone, if it happens too much you will be banned."}</p>
<p>
{" "}
{
"If you have unstable frames, plesase don't go online, it ruins the experience for everyone, if it happens too much you will be banned."
}
</p>
</Alert>
<Alert>
<p>The server is available on console and CEMU, follow the tutorial on <Link href="https://pretendo.network">the website</Link> to get Pretendo Network on your device.</p>
<p>
The server is available on console and CEMU, follow the tutorial on{" "}
<Link href="https://pretendo.network">the website</Link> to get Pretendo Network on your device.
</p>
</Alert>
</Tab>
</Tabs>
</form>
</div>
);
@ -150,6 +205,6 @@ const HomePage = () => {
Mario Kart TV highlight download/upload
Mario Kart TV highlight post reply upload/download
*/
}
};
export default HomePage;
export default HomePage;

157
app/rankings/[id]/page.tsx Normal file
View file

@ -0,0 +1,157 @@
"use client";
import { GetTimeTrialRankingResponse, TimeTrialRanking } from "@/helpers/proto/amkj_service";
import TrackList from "@/helpers/types/TrackList";
import { useEffect, useState } from "react";
import { Alert, Form, Spinner, Table } from "react-bootstrap";
export default function TrackRankingPage({ params }: { params: { id: string } }) {
const [rankingResponse, setRankingResponse] = useState<Response | null>(null);
const [worstRankings, setWorstRankings] = useState<TimeTrialRanking[]>([]);
const [worstLastFetch, setWorstLastFetch] = useState<Date | null>(null);
const [bestRankings, setBestRankings] = useState<TimeTrialRanking[]>([]);
const [bestLastFetch, setBestLastFetch] = useState<Date | null>(null);
const [filterAsc, setFilterAsc] = useState<boolean>(true);
async function fetchRankings(asc: boolean) {
if (asc) {
if (bestLastFetch && new Date().getTime() - bestLastFetch.getTime() < 15000) {
return;
}
} else {
if (worstLastFetch && new Date().getTime() - worstLastFetch.getTime() < 15000) {
return;
}
}
try {
const response = await fetch(`/api/rankings/${params.id}?${asc ? "" : "desc"}`, { cache: "no-store" });
setRankingResponse(response);
const data: GetTimeTrialRankingResponse = await response.json();
if (asc) {
setBestRankings(data.rankings);
setBestLastFetch(new Date());
} else {
setWorstRankings(data.rankings);
setWorstLastFetch(new Date());
}
} catch (error) {
console.error('Failed to fetch rankings:', error);
setRankingResponse(null);
}
}
function scoreToTime(score: number): string {
const milliseconds = score % 1000;
const seconds = Math.floor(score / 1000) % 60;
const minutes = Math.floor(Math.floor(score / 1000) / 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}.${milliseconds.toString().padStart(3, "0")}`;
}
function commonDataToMiiName(data: { data: number[] }): string {
const str = data.data;
const account_related_data = str.slice(0x14, 0x74);
const mii_name = account_related_data.slice(0x1a, 0x2e);
const u16Array = new Uint16Array(mii_name);
let name = "";
for (let i = 0; i < u16Array.length; i++) {
name += String.fromCharCode(u16Array[i]);
}
return name;
}
useEffect(() => {
fetchRankings(filterAsc);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterAsc]);
const trackId = parseInt(params.id);
if (isNaN(trackId)) {
return (
<div className="container-fluid mt-5 mb-3 h-100 p-0 d-flex justify-content-center align-items-center">
<form className="text-center w-100 p-4 bg-light shadow-lg">
<Alert variant="danger">Invalid track ID! (Not a number)</Alert>
</form>
</div>);
}
const track = TrackList.find((cup) => cup.tracks.find((track) => track.id === trackId))?.tracks.find((track) => track.id === trackId);
if (!track) {
return (
<div className="container-fluid mt-5 mb-3 h-100 p-0 d-flex justify-content-center align-items-center">
<form className="text-center w-100 p-4 bg-light shadow-lg">
<Alert variant="danger">Invalid track ID! (ID does not exist)</Alert>
</form>
</div>);
}
const rankings = filterAsc ? bestRankings : worstRankings;
return (
<div className="container-fluid mt-5 mb-3 h-100 p-0 d-flex justify-content-center align-items-center">
<form className="text-center w-100 p-4 bg-light shadow-lg">
<h1 className="mb-3">Rankings: {track.name}</h1>
<Form.Select
defaultValue={"0"}
className="mx-auto"
style={{ width: "18rem" }}
onChange={(event) => {
const value = parseInt(event.target.value);
setFilterAsc(value === 0);
fetchRankings(value === 0);
}}>
<option value="0">Filter by best time</option>
<option value="1">Filter by worst time</option>
</Form.Select>
<div className="mt-3">
{!rankingResponse && <Spinner animation="border" role="status" />}
{rankingResponse && rankingResponse.status !== 200 && (
<Alert variant="danger">
<Alert.Heading>Error fetching rankings</Alert.Heading>
<p>
This error should only happen if the NEX server is down, try refreshing the page.
</p>
<hr />
<p>
If this happens again, contact the developers on the <Alert.Link href="https://discord.gg/pretendo">Pretendo Discord</Alert.Link>
</p>
</Alert>
)}
{rankingResponse && rankingResponse.status === 200 && rankings && rankings.length === 0 && <Alert variant="info">No rankings for this track yet!</Alert>}
{rankingResponse && rankingResponse.status === 200 && rankings && rankings.length !== 0 && (
<Table striped bordered hover responsive className="mx-auto" style={{ maxWidth: "48rem" }}>
<thead>
<tr>
<th>Rank</th>
<th>PID</th>
<th>Mii Name</th>
<th>Score</th>
</tr>
</thead>
<tbody>
{rankings.map((ranking, idx) => (
<tr key={idx}>
<td>{ranking.rank}</td>
<td>{ranking.pid}</td>
<td>{commonDataToMiiName(ranking.commonData as any)}</td>
<td>{scoreToTime(ranking.score)}</td>
</tr>
))}
</tbody>
</Table>
)}
</div>
</form>
</div>
);
}

View file

@ -1,12 +1,52 @@
export default async function RankingsPage() {
"use client";
import Image from "next/image";
import TrackList from "@/helpers/types/TrackList";
import { useState } from "react";
import { Card } from "react-bootstrap";
import Link from "next/link";
export default function RankingsPage() {
const [currentCup, setCurrentCup] = useState<number>(0);
return (
<div className="container-fluid mt-5 mb-3 h-100 p-0 d-flex justify-content-center align-items-center">
<form className="text-center w-100 p-5 bg-light shadow-lg">
<form className="text-center w-100 p-4 bg-light shadow-lg">
<h1 className="mb-3">Rankings</h1>
<div className="alert alert-info" role="alert">
Not implemented on the website yet.. come back later! (They work in the game though)
<h6>Select a cup</h6>
<div className="d-flex flex-wrap justify-content-around btn-group">
{TrackList.map((cup, cupIdx) => {
return (
<button
type="button"
key={cup.internalName}
className={`d-flex flex-column justify-content-between align-items-center btn btn-outline-primary`}
style={{ width: '8rem', whiteSpace: "nowrap", backgroundColor: `${currentCup === cupIdx ? "#0D6EFD80" : "inherit"}` }}
onClick={() => setCurrentCup(cupIdx)}
>
<Image className="rounded-circle" src={`/assets/cup/${cup.internalName}.png`} alt={cup.name + " Icon"} width={100} height={100} />
<p className="text-dark"><strong>{cup.name}</strong></p>
</button>
)
})}
</div>
</form>
</div>
<hr />
<div className="d-flex flex-column justify-content-between align-items-center">
{TrackList[currentCup].tracks.map((track) => {
return (
<Link href={`/rankings/${track.id}`} passHref key={track.internalName} style={{ textDecoration: "none" }}>
<Card className="mb-3" style={{ width: '20rem' }}>
<Card.Img variant="top" src={`/assets/track/${track.internalName}.png`} alt={track.name + " Icon"} />
<Card.Header>
<strong>{track.name}</strong>
</Card.Header>
</Card>
</Link>
);
})}
</div>
</form >
</div >
);
}

View file

@ -25,6 +25,8 @@ service AmkjService {
rpc GetAllTournaments(GetAllTournamentsRequest) returns (GetAllTournamentsResponse) {}
rpc GetUnlocks(GetUnlocksRequest) returns (GetUnlocksResponse) {}
rpc GetTimeTrialRanking(GetTimeTrialRankingRequest) returns (GetTimeTrialRankingResponse) {}
}
// ========================================================
@ -185,4 +187,24 @@ message GetUnlocksResponse {
repeated uint32 wing_unlocks = 10;
repeated uint32 stamp_unlocks = 11;
repeated uint32 dlc_unlocks = 12;
}
// ========================================================
message TimeTrialRanking {
uint32 rank = 1;
google.protobuf.Timestamp datetime = 2;
uint32 score = 3;
uint32 pid = 4;
bytes common_data = 5;
}
message GetTimeTrialRankingRequest {
uint32 track = 1;
int32 limit = 2;
bool asc = 3;
}
message GetTimeTrialRankingResponse {
repeated TimeTrialRanking rankings = 1;
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
/* eslint-disable */
import Long from "long";
import _m0 from "protobufjs/minimal";
import * as _m0 from "protobufjs/minimal";
export const protobufPackage = "google.protobuf";
@ -55,6 +55,7 @@ export const protobufPackage = "google.protobuf";
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
* .setNanos((int) ((millis % 1000) * 1000000)).build();
*
*
* Example 5: Compute Timestamp from Java `Instant.now()`.
*
* Instant now = Instant.now();
@ -63,6 +64,7 @@ export const protobufPackage = "google.protobuf";
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
* .setNanos(now.getNano()).build();
*
*
* Example 6: Compute Timestamp from current time in Python.
*
* timestamp = Timestamp()
@ -96,130 +98,116 @@ export const protobufPackage = "google.protobuf";
* ) to obtain a formatter capable of generating timestamps in this format.
*/
export interface Timestamp {
/**
* Represents seconds of UTC time since Unix epoch
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
* 9999-12-31T23:59:59Z inclusive.
*/
seconds: number;
/**
* Non-negative fractions of a second at nanosecond resolution. Negative
* second values with fractions must still have non-negative nanos values
* that count forward in time. Must be from 0 to 999,999,999
* inclusive.
*/
nanos: number;
/**
* Represents seconds of UTC time since Unix epoch
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
* 9999-12-31T23:59:59Z inclusive.
*/
seconds: number;
/**
* Non-negative fractions of a second at nanosecond resolution. Negative
* second values with fractions must still have non-negative nanos values
* that count forward in time. Must be from 0 to 999,999,999
* inclusive.
*/
nanos: number;
}
function createBaseTimestamp(): Timestamp {
return { seconds: 0, nanos: 0 };
return { seconds: 0, nanos: 0 };
}
export const Timestamp = {
encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.seconds !== 0) {
writer.uint32(8).int64(message.seconds);
}
if (message.nanos !== 0) {
writer.uint32(16).int32(message.nanos);
}
return writer;
},
encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.seconds !== 0) {
writer.uint32(8).int64(message.seconds);
}
if (message.nanos !== 0) {
writer.uint32(16).int32(message.nanos);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): Timestamp {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseTimestamp();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}
decode(input: _m0.Reader | Uint8Array, length?: number): Timestamp {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseTimestamp();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.seconds = longToNumber(reader.int64() as Long);
break;
case 2:
message.nanos = reader.int32();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
message.seconds = longToNumber(reader.int64() as Long);
continue;
case 2:
if (tag !== 16) {
break;
}
fromJSON(object: any): Timestamp {
return {
seconds: isSet(object.seconds) ? Number(object.seconds) : 0,
nanos: isSet(object.nanos) ? Number(object.nanos) : 0,
};
},
message.nanos = reader.int32();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
toJSON(message: Timestamp): unknown {
const obj: any = {};
message.seconds !== undefined && (obj.seconds = Math.round(message.seconds));
message.nanos !== undefined && (obj.nanos = Math.round(message.nanos));
return obj;
},
fromJSON(object: any): Timestamp {
return {
seconds: isSet(object.seconds) ? Number(object.seconds) : 0,
nanos: isSet(object.nanos) ? Number(object.nanos) : 0,
};
},
toJSON(message: Timestamp): unknown {
const obj: any = {};
message.seconds !== undefined && (obj.seconds = Math.round(message.seconds));
message.nanos !== undefined && (obj.nanos = Math.round(message.nanos));
return obj;
},
create(base?: DeepPartial<Timestamp>): Timestamp {
return Timestamp.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<Timestamp>): Timestamp {
const message = createBaseTimestamp();
message.seconds = object.seconds ?? 0;
message.nanos = object.nanos ?? 0;
return message;
},
fromPartial(object: DeepPartial<Timestamp>): Timestamp {
const message = createBaseTimestamp();
message.seconds = object.seconds ?? 0;
message.nanos = object.nanos ?? 0;
return message;
},
};
declare var self: any | undefined;
declare var window: any | undefined;
declare var global: any | undefined;
var tsProtoGlobalThis: any = (() => {
if (typeof globalThis !== "undefined") {
return globalThis;
}
if (typeof self !== "undefined") {
return self;
}
if (typeof window !== "undefined") {
return window;
}
if (typeof global !== "undefined") {
return global;
}
throw "Unable to locate global object";
var globalThis: any = (() => {
if (typeof globalThis !== "undefined") return globalThis;
if (typeof self !== "undefined") return self;
if (typeof window !== "undefined") return window;
if (typeof global !== "undefined") return global;
throw "Unable to locate global object";
})();
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new tsProtoGlobalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
return long.toNumber();
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
return long.toNumber();
}
// If you get a compile-error about 'Constructor<Long> and ... have no overlap',
// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'.
if (_m0.util.Long !== Long) {
_m0.util.Long = Long as any;
_m0.configure();
_m0.util.Long = Long as any;
_m0.configure();
}
function isSet(value: any): boolean {
return value !== null && value !== undefined;
return value !== null && value !== undefined;
}

124
helpers/types/TrackList.ts Normal file
View file

@ -0,0 +1,124 @@
const TrackList = [
{
name: "Mushroom Cup",
internalName: "Mushroom",
tracks: [
{ id: 27, name: "Mario Kart Stadium", internalName: "Gu_FirstCircuit" },
{ id: 28, name: "Water Park", internalName: "Gu_WaterPark" },
{ id: 19, name: "Sweet Sweet Canyon", internalName: "Gu_Cake" },
{ id: 17, name: "Thwomp Ruins", internalName: "Gu_DossunIseki" },
],
},
{
name: "Flower Cup",
internalName: "Flower",
tracks: [
{ id: 16, name: "Mario Circuit", internalName: "Gu_MarioCircuit" },
{ id: 18, name: "Toad Harbour", internalName: "Gu_City" },
{ id: 20, name: "Twisted Mansion", internalName: "Gu_HorrorHouse" },
{ id: 21, name: "Shy Guy Falls", internalName: "Gu_Expert" },
],
},
{
name: "Star Cup",
internalName: "Star",
tracks: [
{ id: 26, name: "Sunshine Airport", internalName: "Gu_Airport" },
{ id: 29, name: "Dolphin Shoals", internalName: "Gu_Ocean" },
{ id: 25, name: "Electrodrome", internalName: "Gu_Techno" },
{ id: 24, name: "Mount Wario", internalName: "Gu_SnowMountain" },
],
},
{
name: "Special Cup",
internalName: "Special",
tracks: [
{ id: 23, name: "Cloudtop Cruise", internalName: "Gu_Cloud" },
{ id: 22, name: "Bone Dry Dunes", internalName: "Gu_Desert" },
{ id: 30, name: "Bowser's Castle", internalName: "Gu_BowserCastle" },
{ id: 31, name: "Rainbow Road", internalName: "Gu_RainbowRoad" },
],
},
{
name: "Shell Cup",
internalName: "Shell",
tracks: [
{ id: 33, name: "Wii Moo Moo Meadows", internalName: "Gwii_MooMooMeadows" },
{ id: 38, name: "GBA Mario Circuit", internalName: "Gagb_MarioCircuit" },
{ id: 36, name: "DS Cheep Cheep Beach", internalName: "Gds_PukupukuBeach" },
{ id: 35, name: "N64 Toad's Turnpike", internalName: "G64_KinopioHighway" },
],
},
{
name: "Banana Cup",
internalName: "Banana",
tracks: [
{ id: 42, name: "GCN Dry Dry Desert", internalName: "Ggc_DryDryDesert" },
{ id: 41, name: "SNES Donut Plains 3", internalName: "Gsfc_DonutsPlain3" },
{ id: 34, name: "N64 Royal Raceway", internalName: "G64_PeachCircuit" },
{ id: 32, name: "3DS DK Jungle", internalName: "G3ds_DKJungle" },
],
},
{
name: "Leaf Cup",
internalName: "Leaf",
tracks: [
{ id: 46, name: "DS Wario Stadium", internalName: "Gds_WarioStadium" },
{ id: 37, name: "GCN Sherbet Land", internalName: "Ggc_SherbetLand" },
{ id: 39, name: "3DS Melody Motorway", internalName: "G3ds_MusicPark" },
{ id: 45, name: "N64 Yoshi Valley", internalName: "G64_YoshiValley" },
],
},
{
name: "Lightning Cup",
internalName: "Thunder",
tracks: [
{ id: 44, name: "DS Tick-Tock Clock", internalName: "Gds_TickTockClock" },
{ id: 43, name: "3DS Piranha Plant Pipeway", internalName: "G3ds_PackunSlider" },
{ id: 40, name: "Wii Grumble Volcano", internalName: "Gwii_GrumbleVolcano" },
{ id: 47, name: "N64 Rainbow Road", internalName: "G64_RainbowRoad" },
],
},
{
name: "Egg Cup",
internalName: "DLC02",
tracks: [
{ id: 56, name: "GCN Yoshi Circuit", internalName: "Dgc_YoshiCircuit" },
{ id: 53, name: "Excitebike Arena", internalName: "Du_ExciteBike" },
{ id: 50, name: "Dragon Driftway", internalName: "Du_DragonRoad" },
{ id: 49, name: "Mute City", internalName: "Du_MuteCity" },
],
},
{
name: "Triforce Cup",
internalName: "DLC03",
tracks: [
{ id: 57, name: "Wii Wario's Gold Mine", internalName: "Dwii_WariosMine" },
{ id: 58, name: "SNES Rainbow Road", internalName: "Dsfc_RainbowRoad" },
{ id: 55, name: "Ice Ice Outpost", internalName: "Du_IcePark" },
{ id: 51, name: "Hyrule Circuit", internalName: "Du_Hyrule" },
],
},
{
name: "Crossing Cup",
internalName: "DLC04",
tracks: [
{ id: 61, name: "GCN Baby Park", internalName: "Dgc_BabyPark" },
{ id: 62, name: "GBA Cheese Land", internalName: "Dagb_CheeseLand" },
{ id: 54, name: "Wild Woods", internalName: "Du_Woods" },
{ id: 52, name: "Animal Crossing", internalName: "Du_Animal" },
],
},
{
name: "Bell Cup",
internalName: "DLC05",
tracks: [
{ id: 60, name: "3DS Koopa City", internalName: "D3ds_NeoBowserCity" },
{ id: 59, name: "GBA Ribbon Road", internalName: "Dagb_RibbonRoad" },
{ id: 48, name: "Super Bell Subway", internalName: "Du_Metro" },
{ id: 63, name: "Big Blue", internalName: "Du_BigBlue" },
],
},
];
export default TrackList;

1765
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,21 +15,21 @@
"@types/react-dom": "18.2.6",
"bootstrap": "^5.3.0",
"eslint": "8.43.0",
"eslint-config-next": "13.4.7",
"eslint-config-next": "^14.1.4",
"jose": "^4.14.4",
"long": "^5.2.3",
"next": "13.4.7",
"nice-grpc": "^2.1.4",
"next": "^14.1.4",
"nice-grpc": "^2.1.8",
"protobufjs": "^7.2.4",
"react": "18.2.0",
"react": "^18.2.0",
"react-bootstrap": "^2.8.0",
"react-device-detect": "^2.2.3",
"react-dom": "18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.10.1",
"typescript": "5.1.3"
},
"devDependencies": {
"grpc-tools": "^1.12.4",
"ts-proto": "^1.150.1"
"ts-proto": "^1.112.0"
}
}
}