mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-27 04:54:25 -05:00
Fix canceled SendouQ match weapons counting towards weapon stats
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory";
|
||||
import * as SQReportedWeaponFactory from "~/db/seed/factories/SQReportedWeaponFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import { invariant } from "~/utils/invariant";
|
||||
import * as ReportedWeaponRepository from "./ReportedWeaponRepository.server";
|
||||
import * as SQMatchRepository from "./SQMatchRepository.server";
|
||||
|
||||
vi.mock("~/features/chat/ChatSystemMessage.server", () => ({
|
||||
send: vi.fn(),
|
||||
notifyStatusChanged: vi.fn(),
|
||||
notifyNotificationsChanged: vi.fn(),
|
||||
notifyRoomsChangedByRoomIds: vi.fn(),
|
||||
}));
|
||||
|
||||
const SEASON = Seasons.currentOrPrevious()!.nth;
|
||||
const WEAPON_SPL_ID = 40;
|
||||
|
||||
const users = UserFactory.pool();
|
||||
const playerId = () => users.id(1);
|
||||
const enemyId = () => users.id(FULL_GROUP_SIZE + 1);
|
||||
|
||||
describe("ReportedWeaponRepository.findAllWeaponUsageStats", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(FULL_GROUP_SIZE * 2);
|
||||
});
|
||||
|
||||
test("counts a won map of a match in progress", async () => {
|
||||
const { map } = await playFirstMapWithWeapon();
|
||||
|
||||
expect(await usageStatsOn(map)).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "SELF",
|
||||
weaponSplId: WEAPON_SPL_ID,
|
||||
count: 1,
|
||||
wins: 1,
|
||||
losses: 0,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("ignores maps of a match both teams agreed to cancel", async () => {
|
||||
const { matchId, map } = await playFirstMapWithWeapon();
|
||||
|
||||
await SQMatchRepository.requestCancelMatch({
|
||||
matchId,
|
||||
requestedByUserId: playerId(),
|
||||
reason: "Disconnect",
|
||||
nominatedUserIds: [],
|
||||
});
|
||||
const acceptance = await SQMatchRepository.acceptCancelMatch({
|
||||
matchId,
|
||||
acceptedByUserId: enemyId(),
|
||||
reason: "Disconnect",
|
||||
nominatedUserIds: [],
|
||||
});
|
||||
expect(acceptance.status).toBe("ACCEPTED");
|
||||
|
||||
expect(await usageStatsOn(map)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
async function playFirstMapWithWeapon() {
|
||||
const match = await SQMatchFactory.create({
|
||||
alphaUserIds: users.ids().slice(0, FULL_GROUP_SIZE),
|
||||
bravoUserIds: users.ids().slice(FULL_GROUP_SIZE),
|
||||
});
|
||||
|
||||
const report = await SQMatchRepository.reportMapWinner({
|
||||
matchId: match.id,
|
||||
winnerId: match.alphaGroup.id,
|
||||
reportedByUserId: playerId(),
|
||||
reportedCount: 0,
|
||||
});
|
||||
expect(report.status).toBe("MAP_REPORTED");
|
||||
|
||||
await SQReportedWeaponFactory.createMany(1, () => ({
|
||||
groupMatchId: match.id,
|
||||
mapIndex: 0,
|
||||
userId: playerId(),
|
||||
weaponSplId: WEAPON_SPL_ID,
|
||||
}));
|
||||
|
||||
const fetched = await SQMatchRepository.findById(match.id);
|
||||
invariant(fetched, "Match not found");
|
||||
|
||||
return { matchId: match.id, map: fetched.mapList[0] };
|
||||
}
|
||||
|
||||
function usageStatsOn(map: { mode: ModeShort; stageId: StageId }) {
|
||||
return ReportedWeaponRepository.findAllWeaponUsageStats({
|
||||
userId: playerId(),
|
||||
mode: map.mode,
|
||||
stageId: map.stageId,
|
||||
season: SEASON,
|
||||
});
|
||||
}
|
||||
@@ -49,6 +49,13 @@ export async function replaceByMatchId(
|
||||
await executor.insertInto("ReportedWeapon").values(weapons).execute();
|
||||
}
|
||||
|
||||
export async function deleteByMatchId(matchId: number, trx?: Transaction<DB>) {
|
||||
await (trx ?? db)
|
||||
.deleteFrom("ReportedWeapon")
|
||||
.where("groupMatchId", "=", matchId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function deleteOwnByMapIndex({
|
||||
matchId,
|
||||
mapIndex,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { backdate } from "~/db/seed/core/backdate";
|
||||
import * as SplatoonFaker from "~/db/seed/core/SplatoonFaker";
|
||||
import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory";
|
||||
import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory";
|
||||
import * as SQReportedWeaponFactory from "~/db/seed/factories/SQReportedWeaponFactory";
|
||||
import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
@@ -20,6 +21,7 @@ import { dateToYYYYMMDD } from "~/utils/dates";
|
||||
import { invariant } from "~/utils/invariant";
|
||||
import { withUserId } from "~/utils/Test";
|
||||
import * as GroupMatchContinueVoteRepository from "./GroupMatchContinueVoteRepository.server";
|
||||
import * as ReportedWeaponRepository from "./ReportedWeaponRepository.server";
|
||||
import * as SQMatchRepository from "./SQMatchRepository.server";
|
||||
|
||||
const setupMatch = async ({
|
||||
@@ -201,6 +203,19 @@ describe("lockMatchWithoutSkillChange", () => {
|
||||
|
||||
expect((await fetchChatRoom(match.chatRoomId!)).inactive).toBe(1);
|
||||
});
|
||||
|
||||
test("deletes the match's reported weapons", async () => {
|
||||
const { match, alphaMembers } = await setupMatch();
|
||||
await SQReportedWeaponFactory.createMany(1, () => ({
|
||||
groupMatchId: match.id,
|
||||
mapIndex: 0,
|
||||
userId: alphaMembers[0].id,
|
||||
}));
|
||||
|
||||
await SQMatchRepository.lockMatchWithoutSkillChange(match);
|
||||
|
||||
expect(await ReportedWeaponRepository.findByMatchId(match.id)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelMatch", () => {
|
||||
|
||||
@@ -1178,6 +1178,7 @@ export async function lockMatchWithoutSkillChange(
|
||||
})
|
||||
.execute();
|
||||
await ChatRepository.updateRoomsInactive([match.chatRoomId], true, trx);
|
||||
await ReportedWeaponRepository.deleteByMatchId(match.id, trx);
|
||||
}
|
||||
|
||||
export type CancelMatchResult =
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import type { reportWeaponSchema } from "~/components/match-page/match-page-schemas";
|
||||
import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
|
||||
import { assertResponseErrored, wrappedAction } from "~/utils/Test";
|
||||
import * as ReportedWeaponRepository from "../ReportedWeaponRepository.server";
|
||||
import * as SQMatchRepository from "../SQMatchRepository.server";
|
||||
import { action as rawMatchAction } from "./q.match.$id.server";
|
||||
|
||||
vi.mock("~/features/chat/ChatSystemMessage.server", () => ({
|
||||
send: vi.fn(),
|
||||
notifyStatusChanged: vi.fn(),
|
||||
notifyNotificationsChanged: vi.fn(),
|
||||
notifyRoomsChangedByRoomIds: vi.fn(),
|
||||
}));
|
||||
|
||||
const WEAPON_SPL_ID = 40;
|
||||
|
||||
const matchAction = wrappedAction<typeof reportWeaponSchema>({
|
||||
action: rawMatchAction,
|
||||
});
|
||||
|
||||
const users = UserFactory.pool();
|
||||
const playerId = () => users.id(1);
|
||||
const enemyId = () => users.id(FULL_GROUP_SIZE + 1);
|
||||
|
||||
describe("REPORT_WEAPON", () => {
|
||||
let matchId: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
await users.create(FULL_GROUP_SIZE * 2);
|
||||
|
||||
const match = await SQMatchFactory.create({
|
||||
alphaUserIds: users.ids().slice(0, FULL_GROUP_SIZE),
|
||||
bravoUserIds: users.ids().slice(FULL_GROUP_SIZE),
|
||||
});
|
||||
matchId = match.id;
|
||||
});
|
||||
|
||||
const reportWeapon = () =>
|
||||
matchAction(
|
||||
{ _action: "REPORT_WEAPON", mapIndex: 0, weaponSplId: WEAPON_SPL_ID },
|
||||
{ user: playerId(), params: { id: String(matchId) } },
|
||||
);
|
||||
|
||||
test("saves the weapon of a match in progress", async () => {
|
||||
await reportWeapon();
|
||||
|
||||
expect(await ReportedWeaponRepository.findByMatchId(matchId)).toEqual([
|
||||
expect.objectContaining({
|
||||
mapIndex: 0,
|
||||
userId: playerId(),
|
||||
weaponSplId: WEAPON_SPL_ID,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("refuses a weapon report for a canceled match", async () => {
|
||||
await SQMatchRepository.requestCancelMatch({
|
||||
matchId,
|
||||
requestedByUserId: playerId(),
|
||||
reason: "Disconnect",
|
||||
nominatedUserIds: [],
|
||||
});
|
||||
const acceptance = await SQMatchRepository.acceptCancelMatch({
|
||||
matchId,
|
||||
acceptedByUserId: enemyId(),
|
||||
reason: "Disconnect",
|
||||
nominatedUserIds: [],
|
||||
});
|
||||
expect(acceptance.status).toBe("ACCEPTED");
|
||||
|
||||
assertResponseErrored(await reportWeapon(), "Match was canceled");
|
||||
expect(await ReportedWeaponRepository.findByMatchId(matchId)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -285,6 +285,8 @@ export const action = defineAction(
|
||||
break;
|
||||
}
|
||||
case "REPORT_WEAPON": {
|
||||
errorToastIfFalsy(!match.isCanceled, "Match was canceled");
|
||||
|
||||
await ReportedWeaponRepository.upsertOwn({
|
||||
groupMatchId: matchId,
|
||||
mapIndex: body.mapIndex,
|
||||
|
||||
5
changelog/2026-10-16-weapon-usage-canceled-matches.md
Normal file
5
changelog/2026-10-16-weapon-usage-canceled-matches.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
navItem: sendouq
|
||||
type: bug
|
||||
---
|
||||
Fixed weapons reported in canceled SendouQ matches counting towards weapon stats, such as season stats and weapon usage
|
||||
19
migrations/20261016163419-delete-canceled-match-weapons.ts
Normal file
19
migrations/20261016163419-delete-canceled-match-weapons.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Kysely } from "kysely";
|
||||
|
||||
/** Canceling a SendouQ match now deletes its reported weapons, this clears the ones canceled before that. */
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.deleteFrom("ReportedWeapon")
|
||||
.where(
|
||||
"groupMatchId",
|
||||
"in",
|
||||
trx
|
||||
.selectFrom("Skill")
|
||||
.select("Skill.groupMatchId")
|
||||
.where("Skill.season", "=", -1)
|
||||
.where("Skill.groupMatchId", "is not", null),
|
||||
)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user