Added everything (gathering/tournaments/home/apis)

This commit is contained in:
Rambo6Glaz 2023-07-06 22:58:29 +02:00
parent 129d5fdf84
commit aac9bed0e2
27 changed files with 1747 additions and 110 deletions

3
.gitignore vendored
View file

@ -34,4 +34,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
app.config.ts
app.config.ts
public/assets/*/

View file

@ -1,3 +1,127 @@
export default async function AdminPage() {
return <h1>Hello, Admin Page!</h1>
}
'use client'
import { KickAllUsersResponse } from "@/helpers/proto/amkj_service";
import { useState } from "react";
import { Alert, Button, InputGroup } from "react-bootstrap";
const AdminPage = () => {
const [startMaintenance, setStartMaintenance] = useState<string>("");
const [endMaintenance, setEndMaintenance] = useState<string>("");
const [isStartMaintenanceDone, setIsStartMaintenanceDone] = useState<boolean>(true);
const [isEndMaintenanceDone, setIsEndMaintenanceDone] = useState<boolean>(true);
const [isKickAllDone, setIsKickAllDone] = useState<boolean>(true);
function startMaintenancePressed() {
const startDate = new Date(startMaintenance);
const isStartInvalid = isNaN(startDate.getTime());
if (isStartInvalid) {
alert("Start maintenance value is invalid!");
return;
}
const endDate = new Date(endMaintenance);
const isEndInvalid = isNaN(endDate.getTime());
if (isEndInvalid) {
alert("End maintenance value is invalid!");
return;
}
setIsStartMaintenanceDone(false);
try {
fetch('/api/admin/maintenance/start', {
method: 'POST', headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
startTime: startDate.getTime(),
endTime: endDate.getTime(),
})
}).then((res) => {
if (res.status == 200) {
alert("Success!");
} else {
alert(`Failure... with HTTP status ${res.status} ${res.statusText}`);
}
});
setIsStartMaintenanceDone(true);
} catch (err) {
setIsStartMaintenanceDone(true);
}
}
function endMaintenancePressed() {
setIsEndMaintenanceDone(false);
try {
fetch('/api/admin/maintenance/end', {
method: 'POST', headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({})
}).then((res) => {
if (res.status == 200) {
alert("Success!");
} else {
alert(`Failure... with HTTP status ${res.status} ${res.statusText}`);
}
});
setIsEndMaintenanceDone(true);
} catch (err) {
setIsEndMaintenanceDone(true);
}
}
function kickAllPressed() {
setIsKickAllDone(false);
try {
fetch('/api/admin/kick/all', {
method: 'POST', headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({})
}).then((res) => {
if (res.status != 200) {
alert(`Failure... with HTTP status ${res.status} ${res.statusText}`);
}
res.json().then((data: KickAllUsersResponse) => {
alert(`Succes! Kicked ${data.numKicked} players.`);
});
});
setIsKickAllDone(true);
} catch (err) {
setIsKickAllDone(true);
}
}
return (
<>
<div className="container-fluid m-5 h-100 d-flex justify-content-center align-items-center">
<form className="text-center w-100 bg-light p-5 rounded-1 shadow-lg">
<h1 className="mb-3">Adminstration panel</h1>
<Alert variant="info">All datetimes you enter should be in your local time, they are automatically transformed to UTC.</Alert>
<InputGroup className="mb-3 justify-content-center align-items-center">
<InputGroup.Text>Maintenance start time</InputGroup.Text>
<input type="datetime-local" onChange={(event) => setStartMaintenance(event.target.value)} />
</InputGroup>
<InputGroup className="mb-3 justify-content-center align-items-center">
<InputGroup.Text>Estimated maintenance end time</InputGroup.Text>
<input type="datetime-local" onChange={(event) => setEndMaintenance(event.target.value)} />
</InputGroup>
<div className="d-flex flex-column justify-content-center align-items-center">
<Button className="mb-3" disabled={!isStartMaintenanceDone} onClick={startMaintenancePressed}>Start maintenance</Button>
<Button className="mb-3" disabled={!isEndMaintenanceDone} onClick={endMaintenancePressed}>End maintenance</Button>
</div>
<hr />
<Button className="mb-3" disabled={!isKickAllDone} onClick={kickAllPressed}>Kick all players</Button>
<Alert variant="danger">{"I know it's tempting but don't please lol"}</Alert>
</form>
</div>
</>
)
}
export default AdminPage;

View file

@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { JWTTokenPayload, getMK8TokenEx } from "@/helpers/types/JWTTokenPayload";
import { amkj_grpc_client } from "@/helpers/grpc";
import { Metadata } from "nice-grpc";
import app_config from "@/app.config";
import { KickAllUsersResponse } from "@/helpers/proto/amkj_service";
export async function POST(request: Request) {
try {
const cookieStore = cookies();
const mk8_token = cookieStore.get("mk8_token");
if (!mk8_token) {
return new NextResponse("{}", { status: 401 });
}
const token: JWTTokenPayload | null = await getMK8TokenEx(mk8_token.value);
if (!token) {
return new NextResponse("{}", { status: 401 });
}
const res = await amkj_grpc_client.kickAllUsers({}, {
metadata: Metadata({
"X-API-Key": app_config.grpc_api_key
})
});
return NextResponse.json(res as KickAllUsersResponse);
} catch (err) {
return new NextResponse("{}", { status: 500 });
}
}

View file

@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { JWTTokenPayload, getMK8TokenEx } from "@/helpers/types/JWTTokenPayload";
import { amkj_grpc_client } from "@/helpers/grpc";
import { Metadata } from "nice-grpc";
import app_config from "@/app.config";
export async function POST(request: Request) {
try {
const cookieStore = cookies();
const mk8_token = cookieStore.get("mk8_token");
if (!mk8_token) {
return new NextResponse("{}", { status: 401 });
}
const token: JWTTokenPayload | null = await getMK8TokenEx(mk8_token.value);
if (!token) {
return new NextResponse("{}", { status: 401 });
}
const res = await amkj_grpc_client.endMaintenance({}, {
metadata: Metadata({
"X-API-Key": app_config.grpc_api_key
})
});
return NextResponse.json(res);
} catch (err) {
return new NextResponse("{}", { status: 500 });
}
}

View file

@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { JWTTokenPayload, getMK8TokenEx } from "@/helpers/types/JWTTokenPayload";
import { amkj_grpc_client } from "@/helpers/grpc";
import { Metadata } from "nice-grpc";
import app_config from "@/app.config";
export async function POST(request: Request) {
try {
const cookieStore = cookies();
const mk8_token = cookieStore.get("mk8_token");
if (!mk8_token) {
return new NextResponse("{}", { status: 401 });
}
const token: JWTTokenPayload | null = await getMK8TokenEx(mk8_token.value);
if (!token) {
return new NextResponse("{}", { status: 401 });
}
const input = await request.json();
const startDate = new Date();
const endDate = new Date();
startDate.setTime(input.startTime);
endDate.setTime(input.endTime);
const res = await amkj_grpc_client.startMaintenance({
utcStartMaintenanceTime: startDate,
utcEndMaintenanceTime: endDate
}, {
metadata: Metadata({
"X-API-Key": app_config.grpc_api_key
})
});
return NextResponse.json(res);
} catch (err) {
return new NextResponse("{}", { status: 500 });
}
}

View file

@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { JWTTokenPayload, getMK8TokenEx } from "@/helpers/types/JWTTokenPayload";
import { amkj_grpc_client } from "@/helpers/grpc";
import { Metadata } from "nice-grpc";
import app_config from "@/app.config";
export async function GET(request: Request) {
try {
const cookieStore = cookies();
const mk8_token = cookieStore.get("mk8_token");
if (!mk8_token) {
return new NextResponse("{}", { status: 401 });
}
const token: JWTTokenPayload | null = await getMK8TokenEx(mk8_token.value);
if (!token) {
return new NextResponse("{}", { status: 401 });
}
const userData = await amkj_grpc_client.getUnlocks({ pid: token.pid }, {
metadata: Metadata({
"X-API-Key": app_config.grpc_api_key
})
});
return NextResponse.json(userData);
} catch (err) {
return new NextResponse("{}", { status: 500 });
}
}

View file

@ -0,0 +1,44 @@
import app_config from "@/app.config";
import { amkj_grpc_client } from "@/helpers/grpc";
import { GetAllTournamentsResponse, Tournament } from "@/helpers/proto/amkj_service";
import { NextResponse } from "next/server";
import { Metadata } from "nice-grpc";
var allTournaments: GetAllTournamentsResponse | null = null;
var lastAllTournamentsTime: Date = new Date();
function sortAllTournaments(myList: Tournament[]): Tournament[] {
return myList.sort((t1, t2) => {
// Check if attributes[13] is equal to 2
const t1_isOfficial = t1.attributes[12] === 2;
const t2_isOfficial = t2.attributes[12] === 2;
if (t1_isOfficial && !t2_isOfficial) {
return -1;
} else if (!t1_isOfficial && t2_isOfficial) {
return 1;
} else {
return t1.attributes[12] !== 2 ? t2.totalParticipants - t1.totalParticipants : 0;
}
});
}
export async function GET(request: Request) {
request.url; // https://nextjs.org/docs/app/building-your-application/routing/router-handlers#dynamic-route-handlers
try {
if (!allTournaments || ((new Date().getTime() - lastAllTournamentsTime.getTime()) > 5000)) {
allTournaments = await amkj_grpc_client.getAllTournaments({ offset: 0, limit: -1 }, {
metadata: Metadata({
"X-API-Key": app_config.grpc_api_key
})
});
sortAllTournaments(allTournaments.tournaments);
lastAllTournamentsTime = new Date();
}
return NextResponse.json(allTournaments);
} catch (err) {
return new NextResponse("{}", { status: 500 });
}
}

View file

@ -1,3 +1,166 @@
export default async function DashboardPage() {
return <h1>Hello, Dashboard Page!</h1>
}
'use client'
import { GetUnlocksResponse } from "@/helpers/proto/amkj_service"
import Image from "next/image";
import { useEffect, useState } from "react";
import { Alert, ListGroup, Spinner } from "react-bootstrap";
const DashboardPage = () => {
const [userData, setUserData] = useState<GetUnlocksResponse | null>(null);
const [userDataResponse, setUserDataResponse] = useState<Response | null>(null);
const AMKJ_CHARACTERS: string[] = [
'Mario', 'Luigi', 'Peach',
'Daisy', 'Yoshi', 'Kinopio',
'Kinopico', 'Nokonoko', 'Koopa',
'DK', 'Wario', 'Waluigi',
'Rosetta', 'MetalMario', 'MetalPeach',
'Jugem', 'Heyho', 'BbMario',
'BbLuigi', 'BbPeach', 'BbDaisy',
'BbRosetta', 'Larry', 'Lemmy',
'Wendy', 'Ludwig', 'Iggy',
'Roy', 'Morton', 'Mii',
'TanukiMario', 'Link', 'AnimalBoyA',
'Shizue', 'CatPeach', 'HoneKoopa',
'AnimalGirlA'
];
const AMKJ_KART_BODIES: string[] = [
'K_Std', 'K_Skl', 'K_Ufo', 'K_Sbm',
'K_Cat', 'K_Fml', 'K_Tri', 'K_Wld',
'K_Pch', 'K_Ten', 'K_Shp', 'K_Snk',
'K_Spo', 'K_Gld', 'B_Std', 'B_Fro',
'B_Mgp', 'B_Big', 'B_Amb', 'B_Mix',
'B_Kid', 'B_Jet', 'B_Ysi', 'V_Atv',
'V_Hnc', 'V_Bea', 'K_Gla', 'K_Slv',
'K_Rst', 'K_Bfl', 'K_Tnk', 'K_Bds',
'B_Zlb', 'K_A00', 'K_A01', 'K_Btl',
'K_Pwc', 'B_Sct', 'V_Drb'
];
const AMKJ_KART_TIRES: string[] = [
'T_Std', 'T_Big', 'T_Sml', 'T_Rng',
'T_Slk', 'T_Mtl', 'T_Btn', 'T_Ofr',
'T_Spg', 'T_Wod', 'T_Fun', 'T_Zst',
'T_Zbi', 'T_Zsm', 'T_Zrn', 'T_Zsl',
'T_Zof', 'T_Gld', 'T_Gla', 'T_Tri',
'T_Anm'
];
const AMKJ_KART_GLIDERS: string[] = [
'G_Std', 'G_Jgm', 'G_Wlo', 'G_Zng',
'G_Umb', 'G_Prc', 'G_Prf', 'G_Flw',
'G_Kpa', 'G_Spl', 'G_Ptv', 'G_Gld',
'G_Hyr', 'G_Pap',
];
const getImageURL = (type: string, idx: number): string => {
let list: string[] | null = null;
if (type === "chara") {
list = AMKJ_CHARACTERS;
} else if (type === "body") {
list = AMKJ_KART_BODIES;
} else if (type === "tire") {
list = AMKJ_KART_TIRES;
} else if (type === "glider") {
list = AMKJ_KART_GLIDERS;
}
if (list) {
if (idx >= list.length) {
return '/assets/chara/Invalid.png';
}
return `/assets/${type}/${list[idx]}.png`;
}
return '/assets/chara/Invalid.png';
}
useEffect(() => {
try {
fetch('/api/admin/userdata', { cache: "no-store" })
.then((res) => {
setUserDataResponse(res);
res.json().then((data) => setUserData(data as GetUnlocksResponse));
});
} catch (error) {
console.error('Failed to fetch user data:', error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getPageJSX = () => {
if (userDataResponse) {
if (userData && userDataResponse.status == 200) {
if (userData.hasData) {
return (
<>
<Alert>Last update: <strong>{userData.lastUpdate && new Date(userData.lastUpdate).toLocaleString()}</strong></Alert>
<div className="mt-3 pb-3 border border-dark border-2">
<h2 className="mb-3 mt-2">Driver unlocks</h2>
<div className="d-flex flex-wrap justify-content-center">
{
userData.driverUnlocks.map((isUnlocked, idx) => {
return <Image src={getImageURL("chara", idx)} width={52} height={52} alt="Driver icon" style={{ opacity: isUnlocked ? '100%' : '40%' }} key={`chara_${idx}`} />;
})
}
</div>
</div>
<div className="mt-3 pb-3 border border-dark border-2">
<h2 className="mb-3 mt-2">Body unlocks</h2>
<div className="d-flex flex-wrap justify-content-center">
{
userData.bodyUnlocks.map((isUnlocked, idx) => {
return <Image src={getImageURL("body", idx)} width={100} height={64} alt="Body icon" style={{ opacity: isUnlocked ? '100%' : '40%' }} key={`body_${idx}`} />;
})
}
</div>
</div>
<div className="mt-3 pb-3 border border-dark border-2">
<h2 className="mb-3 mt-2">Tire unlocks</h2>
<div className="d-flex flex-wrap justify-content-center">
{
userData.tireUnlocks.map((isUnlocked, idx) => {
return <Image src={getImageURL("tire", idx)} width={100} height={64} alt="Tire icon" style={{ opacity: isUnlocked ? '100%' : '40%' }} key={`tire_${idx}`} />;
})
}
</div>
</div>
<div className="mt-3 pb-3 border border-dark border-2">
<h2 className="mb-3 mt-2">Glider unlocks</h2>
<div className="d-flex flex-wrap justify-content-center">
{
userData.wingUnlocks.map((isUnlocked, idx) => {
return <Image src={getImageURL("glider", idx)} width={100} height={64} alt="Glider icon" style={{ opacity: isUnlocked ? '100%' : '40%' }} key={`glider_${idx}`} />;
})
}
</div>
</div>
</>
);
} else {
return (<Alert variant="info">{"We don't have your user data! You must at least play once online on the Mario Kart 8 server!"}</Alert>);
}
} else {
return (<Alert variant="danger">An error has occured because the NEX server may be down. Be patient until the issue is fixed! <strong>{`(Error: ${userDataResponse.status} ${userDataResponse.statusText})`}</strong></Alert>);
}
} else {
return (<Spinner animation="border" role="status" />);
}
}
return (
<>
<div className="container-fluid m-5 h-100 d-flex justify-content-center align-items-center">
<form className="text-center w-100 bg-light p-5 rounded-1 shadow-lg">
<h1 className="mb-3">User dashboard</h1>
<div>
{getPageJSX()}
</div>
</form>
</div>
</>
)
}
export default DashboardPage;

View file

@ -26,11 +26,18 @@ export default function GatheringsPage() {
}
useEffect(() => {
fetchAllGatherings();
const fetchInterval = setInterval(fetchAllGatherings, updateTime * 1000);
const fetchAllGatheringsAndStartTimer = async () => {
await fetchAllGatherings();
setTimer(updateTime - 1);
};
// Call fetchAllGatherings when the page is loaded
fetchAllGatheringsAndStartTimer();
const timerInterval = setInterval(() => {
setTimer((timer) => {
if (timer == 0) {
if (timer === 0) {
fetchAllGatheringsAndStartTimer();
return updateTime - 1;
} else {
return timer - 1;
@ -38,13 +45,11 @@ export default function GatheringsPage() {
});
}, 1000);
return () => {
clearInterval(fetchInterval);
clearInterval(timerInterval)
}
return () => clearInterval(timerInterval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getPageJSX = () => {
if (!allGatheringsResponse) {
return <Spinner animation="border" role="status" />;
@ -54,7 +59,7 @@ export default function GatheringsPage() {
<Alert variant="danger" onClick={() => { document.location.reload() }}>
<Alert.Heading>Error fetching gatherings</Alert.Heading>
<p>
This error should never happen, try refreshing the page!
This error should only happen if the NEX server is down, try refreshing the page.
</p>
<hr />
<p>

View file

@ -1,4 +1,4 @@
import { NEXStatus } from '@/components/NEXStatus';
import Footer from '@/components/Footer';
import NavigationBar from '@/components/NavigationBar';
import 'bootstrap/dist/css/bootstrap.min.css';
@ -17,7 +17,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{children}
</div>
</div>
<NEXStatus />
<Footer />
</body>
</html>
)

View file

@ -1,5 +1,159 @@
import Link from "next/link";
'use client'
export default function HomePage() {
return <h1>Hello, Dashboard Page!</h1>
}
import Image from "next/image";
import Link from "next/link";
import { Alert, Badge, Carousel, ListGroup, Tab, Tabs } from "react-bootstrap";
const HomePage = () => {
return (
<div className="container-fluid m-5 h-100 d-flex justify-content-center align-items-center">
<form className="text-center w-100 bg-light p-5 rounded-1 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>
<Tabs defaultActiveKey="home" className="mb-3">
<Tab eventKey="home" title="Overview">
<Carousel variant="dark">
<Carousel.Item>
<Image
src="/compe1.png"
alt="First slide"
width={1280}
height={720}
/>
<Carousel.Caption>
<Badge bg="dark"><h3 className="m-2">Tournament rankings example</h3></Badge>
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<Image
src="/mktv1.png"
alt="Second slide"
width={1280}
height={720}
/>
<Carousel.Caption>
<Badge bg="dark"><h3 className="m-2">Mario Kart TV finally works again!</h3></Badge>
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<Image
src="/compe2.png"
alt="Third slide"
width={1280}
height={720}
/>
<Carousel.Caption>
<Badge bg="dark"><h3 className="m-2">Tournament rankings with teams!</h3><p>{"I'm aware the count doesn't match, and the bug is fixed."}</p></Badge>
</Carousel.Caption>
</Carousel.Item>
</Carousel>
<p>{"Astute readers may have noticed how I (Rambo6Glaz) won both tournaments."}</p>
<Alert>
<Alert.Heading>How to play on this server?</Alert.Heading>
<hr />
<p className="mb-0">
<li>Follow the steps to join the Pretendo Network on <Link href="https://pretendo.network">our website</Link>!</li>
<li>Create / log on your PNID account and simply start Mario Kart 8</li>
</p>
</Alert>
<Alert variant="success">
<Alert.Heading>Is there any rules?</Alert.Heading>
<hr />
<p className="mb-0">
<li>You may only use cheats in friend rooms</li>
<li>You should not disrupt gameplay or server uptime</li>
<li>{"Do not generate any user-content (tournament, Mii names) with injury or NSFW content."}</li>
<li>You may have fun. {"(If red shells allow you)"}</li>
<li>Have common sense, we may issue a ban even for something that is not on list</li>
</p>
<hr></hr>
<p className="mb-0">
<strong>Breaching the rules can get your account or console temporarily/permanently banned from our services</strong> (🤓)
</p>
</Alert>
</Tab>
<Tab eventKey="features" title="Features">
<h3>{"What's implemented ?"}</h3>
<div className="mx-auto w-75">
<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="danger">Does NOT work</ListGroup.Item>
</strong>
</ListGroup>
<hr />
<ListGroup className="mt-3 mb-3">
<strong>
<ListGroup.Item variant="primary">Global matchmaking</ListGroup.Item>
<ListGroup.Item variant="primary">Regional matchmaking</ListGroup.Item>
<ListGroup.Item variant="primary">Friend rooms</ListGroup.Item>
<ListGroup.Item variant="primary">Join a friend by the friend list / friend presence</ListGroup.Item>
<ListGroup.Item variant="primary">Join a recent player</ListGroup.Item>
</strong>
</ListGroup>
<hr />
<ListGroup className="mt-3 mb-3">
<strong>
<ListGroup.Item variant="info">Tournaments creation / deletion / update</ListGroup.Item>
<ListGroup.Item variant="primary">Tournament participation</ListGroup.Item>
<ListGroup.Item variant="primary">Tournament search by filter or code</ListGroup.Item>
<ListGroup.Item variant="primary">Tournament rankings (team and normal)</ListGroup.Item>
<ListGroup.Item variant="info">Post in the Tournament community</ListGroup.Item>
<ListGroup.Item variant="info">View posts from the tournament community</ListGroup.Item>
</strong>
</ListGroup>
<hr />
<ListGroup className="mt-3 mb-3">
<strong>
<ListGroup.Item variant="primary">Rankings</ListGroup.Item>
<ListGroup.Item variant="primary">Rankings ghost download</ListGroup.Item>
<ListGroup.Item variant="info">Rankings Miiverse posts download</ListGroup.Item>
<ListGroup.Item variant="info">Rankings ghost upload</ListGroup.Item>
</strong>
</ListGroup>
<hr />
<ListGroup className="mt-3 mb-3">
<strong>
<ListGroup.Item variant="primary">Mario Kart TV</ListGroup.Item>
<ListGroup.Item variant="primary">Mario Kart TV global / popular / recent highlights</ListGroup.Item>
<ListGroup.Item variant="info">Mario Kart TV highlight download</ListGroup.Item>
<ListGroup.Item variant="primary">Mario Kart TV highlight upload</ListGroup.Item>
<ListGroup.Item variant="danger">Mario Kart TV highlight upload on YouTube</ListGroup.Item>
</strong>
</ListGroup>
<hr />
</div>
</Tab>
<Tab eventKey="contact" title="CEMU Users">
<Alert variant="info">
<li>Use CEMU 2.0-43 experimental or higher</li>
<li>{"We don't support piracy and we recommend dumping files from your own console."}</li>
<li>{"Files downloaded from the website aren't supported anymore"}</li>
</Alert>
</Tab>
</Tabs>
</form>
</div>
);
/*
Mario Kart TV highlight automatic search (on mktv boot, global, popular/recent)
Mario Kart TV highlight download/upload
Mario Kart TV highlight post reply upload/download
*/
}
export default HomePage;

View file

@ -1,3 +1,12 @@
export default async function RankingsPage() {
return <h1>Hello, Rankings Page!</h1>
return (
<div className="container-fluid m-5 h-100 d-flex justify-content-center align-items-center">
<form className="text-center w-100 bg-light p-5 rounded-1 shadow-lg">
<h1 className="mb-3">Rankings</h1>
<div className="alert alert-info" role="alert">
Not implemented yet.. come back later!
</div>
</form>
</div>
);
}

View file

@ -1,3 +1,124 @@
export default async function TournamentsPage() {
return <h1>Hello, Tournaments Page!</h1>
'use client'
import { TournamentEntry } from '@/components/TournamentEntry';
import { Tournament } from '@/helpers/proto/amkj_service';
import { useEffect, useState } from 'react';
import { Alert, Spinner } from 'react-bootstrap';
export default function TournamentsPage() {
const updateTime = 20;
const [allTournamentsResponse, setAllTournamentsResponse] = useState<Response | null>(null);
const [allTournaments, setAllTournaments] = useState<Tournament[]>([]);
const [timer, setTimer] = useState<number>(updateTime - 1);
const fetchAllTournaments = async () => {
try {
const response = await fetch('/api/tournaments?type=all', { cache: "no-store" });
setAllTournamentsResponse(response);
const data = await response.json();
setAllTournaments(data.tournaments);
} catch (error) {
console.error('Failed to fetch all tournaments:', error);
setAllTournamentsResponse(null);
}
}
useEffect(() => {
const fetchAllTournamentsAndStartTimer = async () => {
await fetchAllTournaments();
setTimer(updateTime - 1);
};
fetchAllTournamentsAndStartTimer();
const timerInterval = setInterval(() => {
setTimer((timer) => {
if (timer === 0) {
fetchAllTournamentsAndStartTimer();
return updateTime - 1;
} else {
return timer - 1;
}
});
}, 1000);
return () => clearInterval(timerInterval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getPageJSX = () => {
if (!allTournamentsResponse) {
return <Spinner animation="border" role="status" />;
} else {
if (allTournamentsResponse.status != 200) {
return (
<Alert variant="danger" onClick={() => { document.location.reload() }}>
<Alert.Heading>Error fetching tournaments</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>
);
} else {
if (allTournaments.length > 0) {
return (
<>
<Alert variant="info">
<p>
{"The times displayed here are in your local time. If they mismatch from the times you've entered in game, either:"}
</p>
<li>Your account was registered in a different timezone</li>
<li>You are using an outdated CEMU version, please use <strong>2.0-43 or higher</strong>.</li>
</Alert>
<div className="d-flex flex-column justify-content-center align-items-center">
{
allTournaments.map((tournament, idx) => {
return (
<>
<div className="ms-5 me-5 mb-3" key={idx}>
<TournamentEntry tournament={tournament} />
</div>
</>
);
})
}
</div>
</>
);
} else {
return (
<Alert variant="primary" onClick={() => { document.location.reload() }}>
<Alert.Heading>No tournaments!</Alert.Heading>
<hr />
<p>
This means no tournaments were registered yet.
</p>
</Alert>
)
}
}
}
}
return (
<>
<div className="container-fluid m-5 h-100 d-flex justify-content-center align-items-center">
<form className="text-center w-100 bg-light p-5 rounded-1 shadow-lg">
<h6><small className="text-muted">Refreshing in {timer} seconds ...</small></h6>
<h1 className="mb-3">Tournaments</h1>
<div>
{getPageJSX()}
</div>
</form>
</div>
</>
)
}

13
components/Footer.tsx Normal file
View file

@ -0,0 +1,13 @@
'use client'
const Footer = () => {
return (
<div className="container-fluid m-3 h-100 d-flex justify-content-center align-items-center">
<form className="text-center w-100 bg-light p-3 rounded-1 shadow-lg">
<h6><small className="text-muted">Made with dedication by Rambo6Glaz and the Pretendo Network team!</small></h6>
</form>
</div>
);
}
export default Footer;

View file

@ -40,7 +40,7 @@ export const GatheringEntry: React.FC<GatheringEntryProps> = ({ gathering }) =>
}
const popover = (
<Popover id="popover-basic" className="text-center">
<Popover className="text-center">
<Popover.Body>
<Table striped bordered hover>
<thead>

View file

@ -4,9 +4,13 @@ import Link from 'next/link';
import { GetServerStatusResponse } from '@/helpers/proto/amkj_service';
import { useEffect, useState } from 'react';
import Spinner from 'react-bootstrap/Spinner';
import { StaticPopover } from './StaticPopover';
import { StaticToast } from './StaticToast';
export const NEXStatus = () => {
export interface NEXStatusProps {
responseHeaderCallback?: (headers: Headers) => void;
}
export const NEXStatus: React.FC<NEXStatusProps> = ({ responseHeaderCallback }) => {
const [statusResponse, setStatusResponse] = useState<Response | null>(null);
const [nexStatus, setNEXStatus] = useState<GetServerStatusResponse | null>(null);
@ -30,10 +34,17 @@ export const NEXStatus = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (responseHeaderCallback && statusResponse) {
responseHeaderCallback(statusResponse.headers);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [statusResponse]);
if (!statusResponse) {
return (
<StaticPopover
<StaticToast
title={
<strong className="me-auto">Fetching server status</strong>
}
@ -44,7 +55,7 @@ export const NEXStatus = () => {
} else {
if (statusResponse.status != 200 || !nexStatus) {
return (
<StaticPopover
<StaticToast
title={
<strong className="me-auto">🔴 Error fetching server status</strong>
}
@ -85,7 +96,7 @@ export const NEXStatus = () => {
}
}
return (
<StaticPopover
<StaticToast
title={
<strong className="me-auto">{text}</strong>
}
@ -101,7 +112,7 @@ export const NEXStatus = () => {
);
} else if (nexStatus.isWhitelist) {
return (
<StaticPopover
<StaticToast
title={
<strong className="me-auto"> Whitelist mode</strong>
}
@ -118,7 +129,7 @@ export const NEXStatus = () => {
);
} else {
return (
<StaticPopover
<StaticToast
title={
<strong className="me-auto">🟢 {`Online with ${nexStatus.numClients} player${(nexStatus.numClients == 0 || nexStatus.numClients > 1) ? 's' : ''}`}</strong>
}

View file

@ -2,12 +2,14 @@
import Link from 'next/link';
import Image from 'next/image';
import { Navbar, Nav } from 'react-bootstrap';
import { Navbar, Nav, Spinner } from 'react-bootstrap';
import { usePathname, } from 'next/navigation';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { MdLogin } from 'react-icons/md';
import { MdLogin, MdLogout } from 'react-icons/md';
import { isBrowser } from 'react-device-detect';
import { NEXStatus } from './NEXStatus';
import { useState } from 'react';
interface NavigationBarEntryProps {
name: string;
@ -20,7 +22,7 @@ const NavigationBarEntry: React.FC<NavigationBarEntryProps> = ({ name, path, des
const isActive = pathname === path;
const popover = (
<Popover id="popover-basic" className="text-center">
<Popover className="text-center">
<Popover.Header as="h3">{name}</Popover.Header>
<Popover.Body>
{desc}
@ -40,33 +42,50 @@ const NavigationBarEntry: React.FC<NavigationBarEntryProps> = ({ name, path, des
interface NavigationBarProps { }
const NavigationBar: React.FC<NavigationBarProps> = ({ }) => {
const pathname = usePathname();
const [responseHeaders, setResponseHeaders] = useState<Headers | null>(null);
const isLoggedIn = responseHeaders && responseHeaders.has("x-mk8-pretendo-imageurl");
const userName = isLoggedIn && responseHeaders.get("x-mk8-pretendo-username");
const aclHeader = isLoggedIn && responseHeaders.get("x-mk8-pretendo-acl");
const isAdmin = aclHeader && parseInt(aclHeader) >= 3;
const getUserProfileJSX = () => {
/*
if (statusResponse) {
if (statusResponse.headers.has("x-mk8-pretendo-imageurl")) {
if (responseHeaders) {
if (isLoggedIn) {
return (
<Link href="#" className="nav-link me-5" prefetch={false} onClick={() => {
/*
<Link href="#" className="nav-link me-5" onClick={() => {
fetch('/logout').then(() => window.location.reload());
}}>
<Image src={statusResponse.headers.get("x-mk8-pretendo-imageurl") as string} width={48} height={48} alt="Mii profile picture" className="mb-2" />
<strong>{statusResponse.headers.get("x-mk8-pretendo-username")}</strong>
<Image src={responseHeaders.get("x-mk8-pretendo-imageurl") as string} width={48} height={48} alt="Mii profile picture" className="mb-2" />
<strong>{responseHeaders.get("x-mk8-pretendo-username")}</strong>
</Link>
*/
<div className="d-flex flex-row justify-content-center align-items-center">
<div className="d-flex flex-column justify-content-center align-items-center">
<Image src={responseHeaders.get("x-mk8-pretendo-imageurl") as string} width={48} height={48} alt="Mii profile picture" className="mb-2" />
<strong>{responseHeaders.get("x-mk8-pretendo-username")}</strong>
</div>
<Nav.Link href="/logout" className="nav-link ms-3 me-3">
<MdLogout size={32} className="text-danger" />
</Nav.Link>
</div>
);
} else {
return (
<Link href={`https://pretendo.network/account/login?redirect=${window.location.origin + pathname}`} className="active nav-link me-5" prefetch={false}>
<strong className="me-2">Login</strong>
<MdLogin size={25} />
</Link>
<Link href={`https://pretendo.network/account/login?redirect=${window.location.origin + pathname}`} className="active nav-link me-5">
<strong className="me-2 text-primary">Login<MdLogin size={25} className="ms-2" /></strong>
</Link >
);
}
}*/
return <></>;
}
return <Spinner animation="border" role="status" className="me-5" />;
}
const pathname = usePathname();
return (
<div>
<NEXStatus responseHeaderCallback={(headers: Headers) => setResponseHeaders(headers)} />
<Navbar expand="lg" style={{ borderBottom: '1px solid #e2edff', backgroundColor: '#e2edff' }}>
<Navbar.Brand>
<Link href='/' style={{ textDecoration: 'none' }}>
@ -75,7 +94,7 @@ const NavigationBar: React.FC<NavigationBarProps> = ({ }) => {
<Image src="/assets/pretendo-logo.png" alt="Pretendo logo" width={35} height={35} />
</div>
<div className="d-flex flex-column">
<span className="text-muted fs-6">Mario Kart 8 Pretendo website</span>
<span className="text-muted fs-6"><strong>Mario Kart 8 Pretendo website</strong></span>
</div>
</div>
</Link>
@ -84,18 +103,23 @@ const NavigationBar: React.FC<NavigationBarProps> = ({ }) => {
{isBrowser && <div className="ms-2 vr" />}
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav" className="justify-content-between me-3 ms-2">
<Navbar.Collapse className="justify-content-between me-3 ms-2">
<Nav className="ms-3">
<NavigationBarEntry path="/" name="Home" desc="Main page of the website" />
<NavigationBarEntry path="/gatherings" name="Gatherings" desc="List of matchmaking sessions (worldwide, regional, friend rooms, tournaments)" />
<NavigationBarEntry path="/tournaments" name="Tournaments" desc="List of tournaments" />
<NavigationBarEntry path="/rankings" name="Rankings" desc="Time trial rankings" />
</Nav>
<Nav className="ms-3 justify-content-between align-items-center">
{isAdmin && <NavigationBarEntry path="/admin" name="Admin panel" desc="Admin panel" />}
{isLoggedIn && <NavigationBarEntry path="/dashboard" name="User data" desc="User dashboard" />}
{getUserProfileJSX()}
</Nav>
</Navbar.Collapse>
<Navbar.Collapse className="justify-content-end">
{getUserProfileJSX()}
</Navbar.Collapse>
</Navbar>
</div>
)

View file

@ -7,7 +7,7 @@ export interface StaticPopoverProps {
align_center?: boolean;
}
export const StaticPopover: React.FC<StaticPopoverProps> = ({ title, body, align_center }) => {
export const StaticToast: React.FC<StaticPopoverProps> = ({ title, body, align_center }) => {
return (
<ToastContainer className={"p-3" + (align_center ? " text-center" : "")} position="bottom-end" style={{ zIndex: 1, position: 'fixed' }}>
<Toast>

View file

@ -0,0 +1,220 @@
import { Tournament } from '@/helpers/proto/amkj_service';
import Image from 'next/image';
import { Badge, Card, OverlayTrigger, Popover } from 'react-bootstrap';
import { BsFillPersonFill, BsGlobe, BsSpeedometer2 } from 'react-icons/bs';
import { AiFillShopping } from 'react-icons/ai';
import { RiTeamFill } from 'react-icons/ri';
export interface TournamentEntryProps {
tournament: Tournament;
}
export const TournamentEntry: React.FC<TournamentEntryProps> = ({ tournament }) => {
const AMKJ_TOURNAMENT_ICONS: string[] = [
'Ch_Mro', 'Ch_Lig', 'Ch_Pch', 'Ch_Dsy', 'Ch_Rst',
'Ch_MroM', 'Ch_Ysi0', 'Ch_Kno', 'Ch_Nok', 'Ch_Hyh0',
'Ch_Jgm', 'Ch_Knc', 'Ch_MroB', 'Ch_LigB', 'Ch_PchB',
'Ch_DsyB', 'Ch_RstB', 'Ch_PchG', 'Ch_Kop', 'Ch_Dkg',
'Ch_Wro', 'Ch_Wlg', 'Ch_Igy', 'Ch_Roy', 'Ch_Lmy',
'Ch_Lry', 'Ch_Wdy', 'Ch_Ldw', 'Ch_Mtn', 'Ch_Mii',
'It_Msh', 'It_Msh3', 'It_Kor', 'It_Kor3', 'It_KorR',
'It_KorR3', 'It_Bnn', 'It_Bnn3', 'It_Flw', 'It_Bom',
'It_Gso', 'It_MshP', 'It_Kil', 'It_Thn', 'It_Tgz',
'It_Str', 'It_Coin', 'It_Bmr', 'It_Pkn', 'It_SHorn',
'It_SP8', 'Kt_StdK', 'Kt_Ten', 'Kt_Ufo', 'Kt_Wld',
'Kt_StdB', 'Kt_Mgp', 'Kt_StdV', 'Cp_Msh', 'Cp_Flw',
'Cp_Str', 'Cp_Spc', 'Cp_Kor', 'Cp_Bnn', 'Cp_Knh',
'Cp_Thn', 'Cl_50', 'Cl_100', 'Cl_150', 'Cl_Mir',
'Sb_FMro', 'Sb_FLgi', 'Sb_FPch', 'Sb_FYsi', 'Sb_FKno',
'Sb_FKop', 'Sb_FWro', 'Ot_Bln', 'Ot_Hdl', 'Ot_Flag',
'Cl_200'
];
function getTournamentTimeWeekly(value: number): string {
const tournamentDays: number[] = [6, 0, 1, 2, 3, 4, 5];
const day = tournamentDays[(value >> 16)];
const hours = Math.floor((value & 0xffff) / 100) % 24;
const minutes = ((value & 0xffff) % 100) % 60;
const utcDate = new Date(2023, 6, 5 + day);
utcDate.getDay()
utcDate.setUTCHours(hours, minutes);
const options: Intl.DateTimeFormatOptions = { weekday: 'long', hour: 'numeric', minute: 'numeric' };
const formatter = new Intl.DateTimeFormat(undefined, options);
const formattedDate = formatter.format(utcDate);
return formattedDate;
}
function getTournamentTimeDaily(value: number): string {
const hours = Math.floor(value / 100) % 24;
const minutes = (value % 100) % 60;
const utcDate = new Date();
utcDate.setUTCHours(hours, minutes);
const formattedHours = utcDate.getHours().toString().padStart(2, '0');
const formattedMinutes = utcDate.getMinutes().toString().padStart(2, '0');
return `Every day at ${formattedHours}:${formattedMinutes}`;
}
function getTournamentTimeFixed(value: Date): string {
return value.toLocaleString();
}
function getTournamentStart(): string {
switch (tournament.repeatType) {
case 1:
return getTournamentTimeWeekly(tournament.startDayTime);
case 2:
return getTournamentTimeDaily(tournament.startTime);
case 3:
if (tournament.startDateTime) {
return getTournamentTimeFixed(new Date(tournament.startDateTime));
}
default:
return "Unknown start time";
}
}
function getTournamentEnd(): string {
switch (tournament.repeatType) {
case 1:
return getTournamentTimeWeekly(tournament.endDayTime);
case 2:
return getTournamentTimeDaily(tournament.endTime);
case 3:
if (tournament.endDateTime) {
return getTournamentTimeFixed(new Date(tournament.endDateTime));
}
default:
return "Unknown end time";
}
}
function getTournamentIconURL(): string {
if (tournament.iconType < 0 || tournament.iconType >= AMKJ_TOURNAMENT_ICONS.length) {
return `/assets/compe_icon/${AMKJ_TOURNAMENT_ICONS[0]}.png`;
}
return `/assets/compe_icon/${AMKJ_TOURNAMENT_ICONS[tournament.iconType]}.png`;
}
const isOfficial = (tournament.attributes[12] == 2);
const borderType = isOfficial ? "border-warning" : "border-dark";
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
if (1.5 * vw < vh) {
var cardWidth = "80vw";
} else if (1.5 * vh < vw) {
var cardWidth = "40vw";
} else {
var cardWidth = "60vw";
}
const AMKJ_TOURNAMENT_RACE_MODE = ["200cc", "50cc", "100cc", "150cc", "Mirror", "Battle"];
const AMKJ_TOURNAMENT_AVAILABLE_COURSES = ["Base", "Base + DLC 1", "Base + DLC 2", "Base + DLC 1+2", "Only DLC 1+2"];
const AMKJ_REGION_TYPE = ["Invalid", "Global", "Regional"];
function getRaceMode(): string {
if (tournament.attributes[2] >= AMKJ_TOURNAMENT_RACE_MODE.length) {
return "Invalid";
}
return AMKJ_TOURNAMENT_RACE_MODE[tournament.attributes[2]];
}
function getAvailableCourses(): string {
if (tournament.attributes[9] >= AMKJ_TOURNAMENT_AVAILABLE_COURSES.length) {
return "Invalid";
}
return AMKJ_TOURNAMENT_AVAILABLE_COURSES[tournament.attributes[9]];
}
function getRegion(): string {
if (tournament.attributes[7] >= AMKJ_REGION_TYPE.length) {
return "Invalid";
}
return AMKJ_REGION_TYPE[tournament.attributes[7]];
}
function getTeams(): string | JSX.Element {
if (tournament.attributes[4] == 2) {
return (
<>
<strong><span className="text-danger me-1">{tournament.redTeam}</span>vs<span className="text-primary ms-1">{tournament.blueTeam}</span></strong>
</>
);
}
return "No teams";
}
const tournamentDescPopover = (
<Popover className="text-center">
<Popover.Header as="h3">Tournament details</Popover.Header>
<Popover.Body>
<Card>
<Card.Header>
<div className="d-flex align-items-center justify-content-center">
<BsSpeedometer2 size={25} className="me-3 text-primary" />{getRaceMode()}
</div>
</Card.Header>
<Card.Header>
<div className="d-flex align-items-center justify-content-center">
<AiFillShopping size={25} className="me-3 text-primary" />{getAvailableCourses()}
</div>
</Card.Header>
<Card.Header>
<div className="d-flex align-items-center justify-content-center">
<BsGlobe size={25} className="me-3 text-primary" />{getRegion()}
</div>
</Card.Header>
<Card.Header>
<div className="d-flex align-items-center justify-content-center">
<RiTeamFill size={25} className="me-3 text-primary" />{getTeams()}
</div>
</Card.Header>
</Card>
</Popover.Body>
</Popover>
);
return (
<OverlayTrigger trigger={["hover", "focus"]} placement="top" overlay={tournamentDescPopover}>
<Card style={{ width: cardWidth }} className={`border ${borderType}`}>
<Card.Header className="text-center">
<div className="d-flex d-row justify-content-between">
<strong>Tournament #{tournament.id}</strong>
<strong>Season #{tournament.seasonId}</strong>
</div>
</Card.Header>
<Card.Body>
<div className="d-flex d-row justify-content-between">
<div>
<Image className="border" src={getTournamentIconURL()} width={72} height={72} alt={`Tournament ${tournament.id}`} />
</div>
<div className="d-flex flex-column text-center align-self-center">
{isOfficial && (
<div className="align-items-center">
<Badge>Official</Badge>
</div>
)}
<h4>{tournament.name}</h4>
<h6>{tournament.description}</h6>
</div>
<div className="align-self-center">
<BsFillPersonFill size={25} />{tournament.totalParticipants}
</div>
</div>
</Card.Body>
<Card.Footer className="text-center">Start: <strong>{getTournamentStart()}</strong></Card.Footer>
<Card.Footer className="text-center">End: <strong>{getTournamentEnd()}</strong></Card.Footer>
<Card.Footer className="text-center">Community code: <strong>{tournament.communityCode.match(/\d{4}/g)?.join('-')}</strong></Card.Footer>
</Card>
</OverlayTrigger>
)
}

View file

@ -23,6 +23,8 @@ service AmkjService {
rpc GetAllGatherings(GetAllGatheringsRequest) returns (GetAllGatheringsResponse) {}
rpc GetAllTournaments(GetAllTournamentsRequest) returns (GetAllTournamentsResponse) {}
rpc GetUnlocks(GetUnlocksRequest) returns (GetUnlocksResponse) {}
}
// ========================================================
@ -40,7 +42,8 @@ message GetServerStatusResponse {
// ========================================================
message StartMaintenanceRequest {
google.protobuf.Timestamp projected_utc_end_maintenance_time = 1;
google.protobuf.Timestamp utc_start_maintenance_time = 1;
google.protobuf.Timestamp utc_end_maintenance_time = 2;
}
message StartMaintenanceResponse {}
@ -129,18 +132,25 @@ message Tournament {
uint32 id = 1;
uint32 owner = 2;
repeated uint32 attributes = 3;
bytes app_data = 4;
int64 total_participants = 5;
int64 season_id = 6;
string name = 7;
string description = 8;
string red_team = 9;
string blue_team = 10;
uint32 repeat_type = 11;
uint32 gameset_num = 12;
uint32 icon_type = 13;
uint32 battle_time = 14;
uint32 update_date = 15;
string community_code = 4;
bytes app_data = 5;
int64 total_participants = 6;
int64 season_id = 7;
string name = 8;
string description = 9;
string red_team = 10;
string blue_team = 11;
uint32 repeat_type = 12;
uint32 gameset_num = 13;
uint32 icon_type = 14;
uint32 battle_time = 15;
uint32 update_date = 16;
uint32 start_day_time = 17;
uint32 end_day_time = 18;
uint32 start_time = 19;
uint32 end_time = 20;
google.protobuf.Timestamp start_date_time = 21;
google.protobuf.Timestamp end_date_time = 22;
}
message GetAllTournamentsRequest {
@ -150,3 +160,24 @@ message GetAllTournamentsRequest {
message GetAllTournamentsResponse {
repeated Tournament tournaments = 1;
}
// ========================================================
message GetUnlocksRequest {
uint32 pid = 1;
}
message GetUnlocksResponse {
bool has_data = 1;
double vr_rate = 2;
double br_rate = 3;
google.protobuf.Timestamp last_update = 4;
repeated uint32 gp_unlocks = 5;
repeated uint32 engine_unlocks = 6;
repeated uint32 driver_unlocks = 7;
repeated uint32 body_unlocks = 8;
repeated uint32 tire_unlocks = 9;
repeated uint32 wing_unlocks = 10;
repeated uint32 stamp_unlocks = 11;
repeated uint32 dlc_unlocks = 12;
}

View file

@ -19,7 +19,8 @@ export interface GetServerStatusResponse {
}
export interface StartMaintenanceRequest {
projectedUtcEndMaintenanceTime: Date | undefined;
utcStartMaintenanceTime: Date | undefined;
utcEndMaintenanceTime: Date | undefined;
}
export interface StartMaintenanceResponse {
@ -106,6 +107,7 @@ export interface Tournament {
id: number;
owner: number;
attributes: number[];
communityCode: string;
appData: Uint8Array;
totalParticipants: number;
seasonId: number;
@ -118,6 +120,12 @@ export interface Tournament {
iconType: number;
battleTime: number;
updateDate: number;
startDayTime: number;
endDayTime: number;
startTime: number;
endTime: number;
startDateTime: Date | undefined;
endDateTime: Date | undefined;
}
export interface GetAllTournamentsRequest {
@ -129,6 +137,25 @@ export interface GetAllTournamentsResponse {
tournaments: Tournament[];
}
export interface GetUnlocksRequest {
pid: number;
}
export interface GetUnlocksResponse {
hasData: boolean;
vrRate: number;
brRate: number;
lastUpdate: Date | undefined;
gpUnlocks: number[];
engineUnlocks: number[];
driverUnlocks: number[];
bodyUnlocks: number[];
tireUnlocks: number[];
wingUnlocks: number[];
stampUnlocks: number[];
dlcUnlocks: number[];
}
function createBaseGetServerStatusRequest(): GetServerStatusRequest {
return {};
}
@ -307,13 +334,16 @@ export const GetServerStatusResponse = {
};
function createBaseStartMaintenanceRequest(): StartMaintenanceRequest {
return { projectedUtcEndMaintenanceTime: undefined };
return { utcStartMaintenanceTime: undefined, utcEndMaintenanceTime: undefined };
}
export const StartMaintenanceRequest = {
encode(message: StartMaintenanceRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.projectedUtcEndMaintenanceTime !== undefined) {
Timestamp.encode(toTimestamp(message.projectedUtcEndMaintenanceTime), writer.uint32(10).fork()).ldelim();
if (message.utcStartMaintenanceTime !== undefined) {
Timestamp.encode(toTimestamp(message.utcStartMaintenanceTime), writer.uint32(10).fork()).ldelim();
}
if (message.utcEndMaintenanceTime !== undefined) {
Timestamp.encode(toTimestamp(message.utcEndMaintenanceTime), writer.uint32(18).fork()).ldelim();
}
return writer;
},
@ -330,7 +360,14 @@ export const StartMaintenanceRequest = {
break;
}
message.projectedUtcEndMaintenanceTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
message.utcStartMaintenanceTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
case 2:
if (tag !== 18) {
break;
}
message.utcEndMaintenanceTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) === 4 || tag === 0) {
@ -343,16 +380,21 @@ export const StartMaintenanceRequest = {
fromJSON(object: any): StartMaintenanceRequest {
return {
projectedUtcEndMaintenanceTime: isSet(object.projectedUtcEndMaintenanceTime)
? fromJsonTimestamp(object.projectedUtcEndMaintenanceTime)
utcStartMaintenanceTime: isSet(object.utcStartMaintenanceTime)
? fromJsonTimestamp(object.utcStartMaintenanceTime)
: undefined,
utcEndMaintenanceTime: isSet(object.utcEndMaintenanceTime)
? fromJsonTimestamp(object.utcEndMaintenanceTime)
: undefined,
};
},
toJSON(message: StartMaintenanceRequest): unknown {
const obj: any = {};
message.projectedUtcEndMaintenanceTime !== undefined &&
(obj.projectedUtcEndMaintenanceTime = message.projectedUtcEndMaintenanceTime.toISOString());
message.utcStartMaintenanceTime !== undefined &&
(obj.utcStartMaintenanceTime = message.utcStartMaintenanceTime.toISOString());
message.utcEndMaintenanceTime !== undefined &&
(obj.utcEndMaintenanceTime = message.utcEndMaintenanceTime.toISOString());
return obj;
},
@ -362,7 +404,8 @@ export const StartMaintenanceRequest = {
fromPartial(object: DeepPartial<StartMaintenanceRequest>): StartMaintenanceRequest {
const message = createBaseStartMaintenanceRequest();
message.projectedUtcEndMaintenanceTime = object.projectedUtcEndMaintenanceTime ?? undefined;
message.utcStartMaintenanceTime = object.utcStartMaintenanceTime ?? undefined;
message.utcEndMaintenanceTime = object.utcEndMaintenanceTime ?? undefined;
return message;
},
};
@ -1586,6 +1629,7 @@ function createBaseTournament(): Tournament {
id: 0,
owner: 0,
attributes: [],
communityCode: "",
appData: new Uint8Array(0),
totalParticipants: 0,
seasonId: 0,
@ -1598,6 +1642,12 @@ function createBaseTournament(): Tournament {
iconType: 0,
battleTime: 0,
updateDate: 0,
startDayTime: 0,
endDayTime: 0,
startTime: 0,
endTime: 0,
startDateTime: undefined,
endDateTime: undefined,
};
}
@ -1614,41 +1664,62 @@ export const Tournament = {
writer.uint32(v);
}
writer.ldelim();
if (message.communityCode !== "") {
writer.uint32(34).string(message.communityCode);
}
if (message.appData.length !== 0) {
writer.uint32(34).bytes(message.appData);
writer.uint32(42).bytes(message.appData);
}
if (message.totalParticipants !== 0) {
writer.uint32(40).int64(message.totalParticipants);
writer.uint32(48).int64(message.totalParticipants);
}
if (message.seasonId !== 0) {
writer.uint32(48).int64(message.seasonId);
writer.uint32(56).int64(message.seasonId);
}
if (message.name !== "") {
writer.uint32(58).string(message.name);
writer.uint32(66).string(message.name);
}
if (message.description !== "") {
writer.uint32(66).string(message.description);
writer.uint32(74).string(message.description);
}
if (message.redTeam !== "") {
writer.uint32(74).string(message.redTeam);
writer.uint32(82).string(message.redTeam);
}
if (message.blueTeam !== "") {
writer.uint32(82).string(message.blueTeam);
writer.uint32(90).string(message.blueTeam);
}
if (message.repeatType !== 0) {
writer.uint32(88).uint32(message.repeatType);
writer.uint32(96).uint32(message.repeatType);
}
if (message.gamesetNum !== 0) {
writer.uint32(96).uint32(message.gamesetNum);
writer.uint32(104).uint32(message.gamesetNum);
}
if (message.iconType !== 0) {
writer.uint32(104).uint32(message.iconType);
writer.uint32(112).uint32(message.iconType);
}
if (message.battleTime !== 0) {
writer.uint32(112).uint32(message.battleTime);
writer.uint32(120).uint32(message.battleTime);
}
if (message.updateDate !== 0) {
writer.uint32(120).uint32(message.updateDate);
writer.uint32(128).uint32(message.updateDate);
}
if (message.startDayTime !== 0) {
writer.uint32(136).uint32(message.startDayTime);
}
if (message.endDayTime !== 0) {
writer.uint32(144).uint32(message.endDayTime);
}
if (message.startTime !== 0) {
writer.uint32(152).uint32(message.startTime);
}
if (message.endTime !== 0) {
writer.uint32(160).uint32(message.endTime);
}
if (message.startDateTime !== undefined) {
Timestamp.encode(toTimestamp(message.startDateTime), writer.uint32(170).fork()).ldelim();
}
if (message.endDateTime !== undefined) {
Timestamp.encode(toTimestamp(message.endDateTime), writer.uint32(178).fork()).ldelim();
}
return writer;
},
@ -1696,85 +1767,134 @@ export const Tournament = {
break;
}
message.appData = reader.bytes();
message.communityCode = reader.string();
continue;
case 5:
if (tag !== 40) {
if (tag !== 42) {
break;
}
message.totalParticipants = longToNumber(reader.int64() as Long);
message.appData = reader.bytes();
continue;
case 6:
if (tag !== 48) {
break;
}
message.seasonId = longToNumber(reader.int64() as Long);
message.totalParticipants = longToNumber(reader.int64() as Long);
continue;
case 7:
if (tag !== 58) {
if (tag !== 56) {
break;
}
message.name = reader.string();
message.seasonId = longToNumber(reader.int64() as Long);
continue;
case 8:
if (tag !== 66) {
break;
}
message.description = reader.string();
message.name = reader.string();
continue;
case 9:
if (tag !== 74) {
break;
}
message.redTeam = reader.string();
message.description = reader.string();
continue;
case 10:
if (tag !== 82) {
break;
}
message.blueTeam = reader.string();
message.redTeam = reader.string();
continue;
case 11:
if (tag !== 88) {
if (tag !== 90) {
break;
}
message.repeatType = reader.uint32();
message.blueTeam = reader.string();
continue;
case 12:
if (tag !== 96) {
break;
}
message.gamesetNum = reader.uint32();
message.repeatType = reader.uint32();
continue;
case 13:
if (tag !== 104) {
break;
}
message.iconType = reader.uint32();
message.gamesetNum = reader.uint32();
continue;
case 14:
if (tag !== 112) {
break;
}
message.battleTime = reader.uint32();
message.iconType = reader.uint32();
continue;
case 15:
if (tag !== 120) {
break;
}
message.battleTime = reader.uint32();
continue;
case 16:
if (tag !== 128) {
break;
}
message.updateDate = reader.uint32();
continue;
case 17:
if (tag !== 136) {
break;
}
message.startDayTime = reader.uint32();
continue;
case 18:
if (tag !== 144) {
break;
}
message.endDayTime = reader.uint32();
continue;
case 19:
if (tag !== 152) {
break;
}
message.startTime = reader.uint32();
continue;
case 20:
if (tag !== 160) {
break;
}
message.endTime = reader.uint32();
continue;
case 21:
if (tag !== 170) {
break;
}
message.startDateTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
case 22:
if (tag !== 178) {
break;
}
message.endDateTime = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -1789,6 +1909,7 @@ export const Tournament = {
id: isSet(object.id) ? Number(object.id) : 0,
owner: isSet(object.owner) ? Number(object.owner) : 0,
attributes: Array.isArray(object?.attributes) ? object.attributes.map((e: any) => Number(e)) : [],
communityCode: isSet(object.communityCode) ? String(object.communityCode) : "",
appData: isSet(object.appData) ? bytesFromBase64(object.appData) : new Uint8Array(0),
totalParticipants: isSet(object.totalParticipants) ? Number(object.totalParticipants) : 0,
seasonId: isSet(object.seasonId) ? Number(object.seasonId) : 0,
@ -1801,6 +1922,12 @@ export const Tournament = {
iconType: isSet(object.iconType) ? Number(object.iconType) : 0,
battleTime: isSet(object.battleTime) ? Number(object.battleTime) : 0,
updateDate: isSet(object.updateDate) ? Number(object.updateDate) : 0,
startDayTime: isSet(object.startDayTime) ? Number(object.startDayTime) : 0,
endDayTime: isSet(object.endDayTime) ? Number(object.endDayTime) : 0,
startTime: isSet(object.startTime) ? Number(object.startTime) : 0,
endTime: isSet(object.endTime) ? Number(object.endTime) : 0,
startDateTime: isSet(object.startDateTime) ? fromJsonTimestamp(object.startDateTime) : undefined,
endDateTime: isSet(object.endDateTime) ? fromJsonTimestamp(object.endDateTime) : undefined,
};
},
@ -1813,6 +1940,7 @@ export const Tournament = {
} else {
obj.attributes = [];
}
message.communityCode !== undefined && (obj.communityCode = message.communityCode);
message.appData !== undefined &&
(obj.appData = base64FromBytes(message.appData !== undefined ? message.appData : new Uint8Array(0)));
message.totalParticipants !== undefined && (obj.totalParticipants = Math.round(message.totalParticipants));
@ -1826,6 +1954,12 @@ export const Tournament = {
message.iconType !== undefined && (obj.iconType = Math.round(message.iconType));
message.battleTime !== undefined && (obj.battleTime = Math.round(message.battleTime));
message.updateDate !== undefined && (obj.updateDate = Math.round(message.updateDate));
message.startDayTime !== undefined && (obj.startDayTime = Math.round(message.startDayTime));
message.endDayTime !== undefined && (obj.endDayTime = Math.round(message.endDayTime));
message.startTime !== undefined && (obj.startTime = Math.round(message.startTime));
message.endTime !== undefined && (obj.endTime = Math.round(message.endTime));
message.startDateTime !== undefined && (obj.startDateTime = message.startDateTime.toISOString());
message.endDateTime !== undefined && (obj.endDateTime = message.endDateTime.toISOString());
return obj;
},
@ -1838,6 +1972,7 @@ export const Tournament = {
message.id = object.id ?? 0;
message.owner = object.owner ?? 0;
message.attributes = object.attributes?.map((e) => e) || [];
message.communityCode = object.communityCode ?? "";
message.appData = object.appData ?? new Uint8Array(0);
message.totalParticipants = object.totalParticipants ?? 0;
message.seasonId = object.seasonId ?? 0;
@ -1850,6 +1985,12 @@ export const Tournament = {
message.iconType = object.iconType ?? 0;
message.battleTime = object.battleTime ?? 0;
message.updateDate = object.updateDate ?? 0;
message.startDayTime = object.startDayTime ?? 0;
message.endDayTime = object.endDayTime ?? 0;
message.startTime = object.startTime ?? 0;
message.endTime = object.endTime ?? 0;
message.startDateTime = object.startDateTime ?? undefined;
message.endDateTime = object.endDateTime ?? undefined;
return message;
},
};
@ -1987,6 +2128,404 @@ export const GetAllTournamentsResponse = {
},
};
function createBaseGetUnlocksRequest(): GetUnlocksRequest {
return { pid: 0 };
}
export const GetUnlocksRequest = {
encode(message: GetUnlocksRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.pid !== 0) {
writer.uint32(8).uint32(message.pid);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): GetUnlocksRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseGetUnlocksRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}
message.pid = reader.uint32();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): GetUnlocksRequest {
return { pid: isSet(object.pid) ? Number(object.pid) : 0 };
},
toJSON(message: GetUnlocksRequest): unknown {
const obj: any = {};
message.pid !== undefined && (obj.pid = Math.round(message.pid));
return obj;
},
create(base?: DeepPartial<GetUnlocksRequest>): GetUnlocksRequest {
return GetUnlocksRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<GetUnlocksRequest>): GetUnlocksRequest {
const message = createBaseGetUnlocksRequest();
message.pid = object.pid ?? 0;
return message;
},
};
function createBaseGetUnlocksResponse(): GetUnlocksResponse {
return {
hasData: false,
vrRate: 0,
brRate: 0,
lastUpdate: undefined,
gpUnlocks: [],
engineUnlocks: [],
driverUnlocks: [],
bodyUnlocks: [],
tireUnlocks: [],
wingUnlocks: [],
stampUnlocks: [],
dlcUnlocks: [],
};
}
export const GetUnlocksResponse = {
encode(message: GetUnlocksResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.hasData === true) {
writer.uint32(8).bool(message.hasData);
}
if (message.vrRate !== 0) {
writer.uint32(17).double(message.vrRate);
}
if (message.brRate !== 0) {
writer.uint32(25).double(message.brRate);
}
if (message.lastUpdate !== undefined) {
Timestamp.encode(toTimestamp(message.lastUpdate), writer.uint32(34).fork()).ldelim();
}
writer.uint32(42).fork();
for (const v of message.gpUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(50).fork();
for (const v of message.engineUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(58).fork();
for (const v of message.driverUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(66).fork();
for (const v of message.bodyUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(74).fork();
for (const v of message.tireUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(82).fork();
for (const v of message.wingUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(90).fork();
for (const v of message.stampUnlocks) {
writer.uint32(v);
}
writer.ldelim();
writer.uint32(98).fork();
for (const v of message.dlcUnlocks) {
writer.uint32(v);
}
writer.ldelim();
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): GetUnlocksResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseGetUnlocksResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}
message.hasData = reader.bool();
continue;
case 2:
if (tag !== 17) {
break;
}
message.vrRate = reader.double();
continue;
case 3:
if (tag !== 25) {
break;
}
message.brRate = reader.double();
continue;
case 4:
if (tag !== 34) {
break;
}
message.lastUpdate = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
continue;
case 5:
if (tag === 40) {
message.gpUnlocks.push(reader.uint32());
continue;
}
if (tag === 42) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.gpUnlocks.push(reader.uint32());
}
continue;
}
break;
case 6:
if (tag === 48) {
message.engineUnlocks.push(reader.uint32());
continue;
}
if (tag === 50) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.engineUnlocks.push(reader.uint32());
}
continue;
}
break;
case 7:
if (tag === 56) {
message.driverUnlocks.push(reader.uint32());
continue;
}
if (tag === 58) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.driverUnlocks.push(reader.uint32());
}
continue;
}
break;
case 8:
if (tag === 64) {
message.bodyUnlocks.push(reader.uint32());
continue;
}
if (tag === 66) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.bodyUnlocks.push(reader.uint32());
}
continue;
}
break;
case 9:
if (tag === 72) {
message.tireUnlocks.push(reader.uint32());
continue;
}
if (tag === 74) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.tireUnlocks.push(reader.uint32());
}
continue;
}
break;
case 10:
if (tag === 80) {
message.wingUnlocks.push(reader.uint32());
continue;
}
if (tag === 82) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.wingUnlocks.push(reader.uint32());
}
continue;
}
break;
case 11:
if (tag === 88) {
message.stampUnlocks.push(reader.uint32());
continue;
}
if (tag === 90) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.stampUnlocks.push(reader.uint32());
}
continue;
}
break;
case 12:
if (tag === 96) {
message.dlcUnlocks.push(reader.uint32());
continue;
}
if (tag === 98) {
const end2 = reader.uint32() + reader.pos;
while (reader.pos < end2) {
message.dlcUnlocks.push(reader.uint32());
}
continue;
}
break;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): GetUnlocksResponse {
return {
hasData: isSet(object.hasData) ? Boolean(object.hasData) : false,
vrRate: isSet(object.vrRate) ? Number(object.vrRate) : 0,
brRate: isSet(object.brRate) ? Number(object.brRate) : 0,
lastUpdate: isSet(object.lastUpdate) ? fromJsonTimestamp(object.lastUpdate) : undefined,
gpUnlocks: Array.isArray(object?.gpUnlocks) ? object.gpUnlocks.map((e: any) => Number(e)) : [],
engineUnlocks: Array.isArray(object?.engineUnlocks) ? object.engineUnlocks.map((e: any) => Number(e)) : [],
driverUnlocks: Array.isArray(object?.driverUnlocks) ? object.driverUnlocks.map((e: any) => Number(e)) : [],
bodyUnlocks: Array.isArray(object?.bodyUnlocks) ? object.bodyUnlocks.map((e: any) => Number(e)) : [],
tireUnlocks: Array.isArray(object?.tireUnlocks) ? object.tireUnlocks.map((e: any) => Number(e)) : [],
wingUnlocks: Array.isArray(object?.wingUnlocks) ? object.wingUnlocks.map((e: any) => Number(e)) : [],
stampUnlocks: Array.isArray(object?.stampUnlocks) ? object.stampUnlocks.map((e: any) => Number(e)) : [],
dlcUnlocks: Array.isArray(object?.dlcUnlocks) ? object.dlcUnlocks.map((e: any) => Number(e)) : [],
};
},
toJSON(message: GetUnlocksResponse): unknown {
const obj: any = {};
message.hasData !== undefined && (obj.hasData = message.hasData);
message.vrRate !== undefined && (obj.vrRate = message.vrRate);
message.brRate !== undefined && (obj.brRate = message.brRate);
message.lastUpdate !== undefined && (obj.lastUpdate = message.lastUpdate.toISOString());
if (message.gpUnlocks) {
obj.gpUnlocks = message.gpUnlocks.map((e) => Math.round(e));
} else {
obj.gpUnlocks = [];
}
if (message.engineUnlocks) {
obj.engineUnlocks = message.engineUnlocks.map((e) => Math.round(e));
} else {
obj.engineUnlocks = [];
}
if (message.driverUnlocks) {
obj.driverUnlocks = message.driverUnlocks.map((e) => Math.round(e));
} else {
obj.driverUnlocks = [];
}
if (message.bodyUnlocks) {
obj.bodyUnlocks = message.bodyUnlocks.map((e) => Math.round(e));
} else {
obj.bodyUnlocks = [];
}
if (message.tireUnlocks) {
obj.tireUnlocks = message.tireUnlocks.map((e) => Math.round(e));
} else {
obj.tireUnlocks = [];
}
if (message.wingUnlocks) {
obj.wingUnlocks = message.wingUnlocks.map((e) => Math.round(e));
} else {
obj.wingUnlocks = [];
}
if (message.stampUnlocks) {
obj.stampUnlocks = message.stampUnlocks.map((e) => Math.round(e));
} else {
obj.stampUnlocks = [];
}
if (message.dlcUnlocks) {
obj.dlcUnlocks = message.dlcUnlocks.map((e) => Math.round(e));
} else {
obj.dlcUnlocks = [];
}
return obj;
},
create(base?: DeepPartial<GetUnlocksResponse>): GetUnlocksResponse {
return GetUnlocksResponse.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<GetUnlocksResponse>): GetUnlocksResponse {
const message = createBaseGetUnlocksResponse();
message.hasData = object.hasData ?? false;
message.vrRate = object.vrRate ?? 0;
message.brRate = object.brRate ?? 0;
message.lastUpdate = object.lastUpdate ?? undefined;
message.gpUnlocks = object.gpUnlocks?.map((e) => e) || [];
message.engineUnlocks = object.engineUnlocks?.map((e) => e) || [];
message.driverUnlocks = object.driverUnlocks?.map((e) => e) || [];
message.bodyUnlocks = object.bodyUnlocks?.map((e) => e) || [];
message.tireUnlocks = object.tireUnlocks?.map((e) => e) || [];
message.wingUnlocks = object.wingUnlocks?.map((e) => e) || [];
message.stampUnlocks = object.stampUnlocks?.map((e) => e) || [];
message.dlcUnlocks = object.dlcUnlocks?.map((e) => e) || [];
return message;
},
};
export type AmkjServiceDefinition = typeof AmkjServiceDefinition;
export const AmkjServiceDefinition = {
name: "AmkjService",
@ -2088,6 +2627,14 @@ export const AmkjServiceDefinition = {
responseStream: false,
options: {},
},
getUnlocks: {
name: "GetUnlocks",
requestType: GetUnlocksRequest,
requestStream: false,
responseType: GetUnlocksResponse,
responseStream: false,
options: {},
},
},
} as const;
@ -2137,6 +2684,10 @@ export interface AmkjServiceImplementation<CallContextExt = {}> {
request: GetAllTournamentsRequest,
context: CallContext & CallContextExt,
): Promise<DeepPartial<GetAllTournamentsResponse>>;
getUnlocks(
request: GetUnlocksRequest,
context: CallContext & CallContextExt,
): Promise<DeepPartial<GetUnlocksResponse>>;
}
export interface AmkjServiceClient<CallOptionsExt = {}> {
@ -2185,6 +2736,10 @@ export interface AmkjServiceClient<CallOptionsExt = {}> {
request: DeepPartial<GetAllTournamentsRequest>,
options?: CallOptions & CallOptionsExt,
): Promise<GetAllTournamentsResponse>;
getUnlocks(
request: DeepPartial<GetUnlocksRequest>,
options?: CallOptions & CallOptionsExt,
): Promise<GetUnlocksResponse>;
}
declare var self: any | undefined;

View file

@ -25,6 +25,15 @@ export async function getMK8Token(request: NextRequest): Promise<JWTTokenPayload
return null;
}
export async function getMK8TokenEx(mk8_token: string): Promise<JWTTokenPayload | null> {
try {
const verifyResult = await jwtVerify(mk8_token, new TextEncoder().encode(app_config.jwt_secret));
return verifyResult.payload as JWTTokenPayload;
} catch (error) { }
return null;
}
export async function getMK8TokenFromAccountAPI(request: NextRequest): Promise<{ token: JWTTokenPayload, jwt_token: string } | null> {
const token_type = request.cookies.get("token_type")?.value;

View file

@ -4,11 +4,12 @@ import { type JWTTokenPayload, getMK8Token, getMK8TokenFromAccountAPI } from './
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/logout')) {
const referer: string | null = request.headers.get("referer");
const url = new URL(referer ? referer : '/', request.url);
const nextPathname = request.nextUrl.pathname;
if (nextPathname.startsWith('/logout')) {
const url = new URL('/', request.url);
const response = NextResponse.redirect(url);
response.cookies.set("mk8_token", "", { maxAge: 0, domain: ".pretendo.network" });
response.cookies.set("access_token", "", { maxAge: 0, domain: ".pretendo.network" });
response.cookies.set("refresh_token", "", { maxAge: 0, domain: ".pretendo.network" });
@ -23,7 +24,7 @@ export async function middleware(request: NextRequest) {
let res;
if (!mk8_token) {
res = await getMK8TokenFromAccountAPI(request);
if (!res && request.nextUrl.pathname.startsWith('/admin')) {
if (!res && nextPathname.startsWith('/admin')) {
const response = NextResponse.redirect(redirect_login_url);
if (request.cookies.has("mk8_token")) {
response.cookies.delete("mk8_token");
@ -35,7 +36,20 @@ export async function middleware(request: NextRequest) {
}
}
if (request.nextUrl.pathname.startsWith('/admin')) {
if (nextPathname.startsWith('/dashboard') && !mk8_token) {
const url = new URL('/', request.url);
return NextResponse.redirect(url);
}
if (nextPathname.startsWith('/api/admin')) {
if (mk8_token && mk8_token.access_level >= 3) {
return NextResponse.next();
} else {
return new NextResponse("{}", { status: 401 });
}
}
if (nextPathname.startsWith('/admin')) {
if (mk8_token) {
if (mk8_token.access_level < 3) {
var response = NextResponse.redirect(new URL('/', request.url));
@ -43,7 +57,7 @@ export async function middleware(request: NextRequest) {
var response = NextResponse.next();
}
response.headers.set("X-MK8-Pretendo-SAL", mk8_token.server_access_level);
response.headers.set("X-MK8-Pretendo-ACL", mk8_token.access_level.toString());
response.headers.set("X-MK8-Pretendo-Username", mk8_token.pnid);
response.headers.set("X-MK8-Pretendo-ImageURL", mk8_token.mii_image_url);
response.headers.set("X-MK8-Pretendo-PID", mk8_token.pid.toString());
@ -62,7 +76,7 @@ export async function middleware(request: NextRequest) {
} else {
const response = NextResponse.next();
if (mk8_token) {
response.headers.set("X-MK8-Pretendo-SAL", mk8_token.server_access_level);
response.headers.set("X-MK8-Pretendo-ACL", mk8_token.access_level.toString());
response.headers.set("X-MK8-Pretendo-Username", mk8_token.pnid);
response.headers.set("X-MK8-Pretendo-ImageURL", mk8_token.mii_image_url);
response.headers.set("X-MK8-Pretendo-PID", mk8_token.pid.toString());

BIN
public/compe1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

BIN
public/compe2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

BIN
public/mktv1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB