mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Migrate SQ match queries to Kysely (#2782)
This commit is contained in:
parent
3a6dc4ace5
commit
7b71abfe53
|
|
@ -29,13 +29,10 @@ import {
|
|||
summarizeMaps,
|
||||
summarizePlayerResults,
|
||||
} from "~/features/sendouq-match/core/summarizer.server";
|
||||
import * as PlayerStatRepository from "~/features/sendouq-match/PlayerStatRepository.server";
|
||||
import { winnersArrayToWinner } from "~/features/sendouq-match/q-match-utils";
|
||||
import { addMapResults } from "~/features/sendouq-match/queries/addMapResults.server";
|
||||
import { addPlayerResults } from "~/features/sendouq-match/queries/addPlayerResults.server";
|
||||
import { addReportedWeapons } from "~/features/sendouq-match/queries/addReportedWeapons.server";
|
||||
import { addSkills } from "~/features/sendouq-match/queries/addSkills.server";
|
||||
import { reportScore } from "~/features/sendouq-match/queries/reportScore.server";
|
||||
import { setGroupAsInactive } from "~/features/sendouq-match/queries/setGroupAsInactive.server";
|
||||
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
|
||||
import * as SkillRepository from "~/features/sendouq-match/SkillRepository.server";
|
||||
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
|
||||
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
|
||||
|
|
@ -2365,28 +2362,28 @@ async function playedMatches() {
|
|||
groupId: match.bravoGroupId,
|
||||
})),
|
||||
];
|
||||
sql.transaction(() => {
|
||||
reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId:
|
||||
faker.number.float(1) > 0.5
|
||||
? groupAlphaMembers[0]
|
||||
: groupBravoMembers[0],
|
||||
winners,
|
||||
});
|
||||
addSkills({
|
||||
skills: newSkills,
|
||||
differences,
|
||||
groupMatchId: match.id,
|
||||
oldMatchMemento: { users: {}, groups: {}, pools: [] },
|
||||
});
|
||||
setGroupAsInactive(groupAlpha);
|
||||
setGroupAsInactive(groupBravo);
|
||||
addMapResults(summarizeMaps({ match: finishedMatch, members, winners }));
|
||||
addPlayerResults(
|
||||
summarizePlayerResults({ match: finishedMatch, members, winners }),
|
||||
);
|
||||
})();
|
||||
await SQMatchRepository.updateScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId:
|
||||
faker.number.float(1) > 0.5
|
||||
? groupAlphaMembers[0]
|
||||
: groupBravoMembers[0],
|
||||
winners,
|
||||
});
|
||||
await SkillRepository.createMatchSkills({
|
||||
skills: newSkills,
|
||||
differences,
|
||||
groupMatchId: match.id,
|
||||
oldMatchMemento: { users: {}, groups: {}, pools: [] },
|
||||
});
|
||||
await SQGroupRepository.setAsInactive(groupAlpha);
|
||||
await SQGroupRepository.setAsInactive(groupBravo);
|
||||
await PlayerStatRepository.upsertMapResults(
|
||||
summarizeMaps({ match: finishedMatch, members, winners }),
|
||||
);
|
||||
await PlayerStatRepository.upsertPlayerResults(
|
||||
summarizePlayerResults({ match: finishedMatch, members, winners }),
|
||||
);
|
||||
|
||||
// -> add weapons for 90% of matches
|
||||
if (faker.number.float(1) > 0.9) continue;
|
||||
|
|
@ -2395,7 +2392,7 @@ async function playedMatches() {
|
|||
finishedMatch.mapList.map((m) => ({ map: m, user: u })),
|
||||
);
|
||||
|
||||
addReportedWeapons(
|
||||
await ReportedWeaponRepository.createMany(
|
||||
mapsWithUsers.map((mu) => {
|
||||
const weapon = () => {
|
||||
if (faker.number.float(1) < 0.9) return defaultWeapons[mu.user];
|
||||
|
|
|
|||
58
app/features/sendouq-match/PlayerStatRepository.server.ts
Normal file
58
app/features/sendouq-match/PlayerStatRepository.server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { Transaction } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
|
||||
export function upsertMapResults(
|
||||
results: Pick<
|
||||
Tables["MapResult"],
|
||||
"losses" | "wins" | "userId" | "mode" | "stageId" | "season"
|
||||
>[],
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
if (results.length === 0) return;
|
||||
|
||||
const executor = trx ?? db;
|
||||
|
||||
return executor
|
||||
.insertInto("MapResult")
|
||||
.values(results)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(["userId", "stageId", "mode", "season"]).doUpdateSet((eb) => ({
|
||||
wins: eb("MapResult.wins", "+", eb.ref("excluded.wins")),
|
||||
losses: eb("MapResult.losses", "+", eb.ref("excluded.losses")),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function upsertPlayerResults(
|
||||
results: Tables["PlayerResult"][],
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
if (results.length === 0) return;
|
||||
|
||||
const executor = trx ?? db;
|
||||
|
||||
return executor
|
||||
.insertInto("PlayerResult")
|
||||
.values(results)
|
||||
.onConflict((oc) =>
|
||||
oc
|
||||
.columns(["ownerUserId", "otherUserId", "type", "season"])
|
||||
.doUpdateSet((eb) => ({
|
||||
mapWins: eb("PlayerResult.mapWins", "+", eb.ref("excluded.mapWins")),
|
||||
mapLosses: eb(
|
||||
"PlayerResult.mapLosses",
|
||||
"+",
|
||||
eb.ref("excluded.mapLosses"),
|
||||
),
|
||||
setWins: eb("PlayerResult.setWins", "+", eb.ref("excluded.setWins")),
|
||||
setLosses: eb(
|
||||
"PlayerResult.setLosses",
|
||||
"+",
|
||||
eb.ref("excluded.setLosses"),
|
||||
),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import type { NotNull, Transaction } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, TablesInsertable } from "~/db/tables";
|
||||
|
||||
export function createMany(
|
||||
weapons: TablesInsertable["ReportedWeapon"][],
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
if (weapons.length === 0) return;
|
||||
|
||||
return (trx ?? db).insertInto("ReportedWeapon").values(weapons).execute();
|
||||
}
|
||||
|
||||
export async function replaceByMatchId(
|
||||
matchId: number,
|
||||
weapons: TablesInsertable["ReportedWeapon"][],
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
const executor = trx ?? db;
|
||||
|
||||
const groupMatchMaps = await executor
|
||||
.selectFrom("GroupMatchMap")
|
||||
.select("id")
|
||||
.where("matchId", "=", matchId)
|
||||
.execute();
|
||||
|
||||
if (groupMatchMaps.length > 0) {
|
||||
await executor
|
||||
.deleteFrom("ReportedWeapon")
|
||||
.where(
|
||||
"groupMatchMapId",
|
||||
"in",
|
||||
groupMatchMaps.map((m) => m.id),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
if (weapons.length > 0) {
|
||||
await executor.insertInto("ReportedWeapon").values(weapons).execute();
|
||||
}
|
||||
}
|
||||
|
||||
export async function findByMatchId(matchId: number) {
|
||||
const rows = await db
|
||||
.selectFrom("ReportedWeapon")
|
||||
.innerJoin(
|
||||
"GroupMatchMap",
|
||||
"GroupMatchMap.id",
|
||||
"ReportedWeapon.groupMatchMapId",
|
||||
)
|
||||
.select([
|
||||
"ReportedWeapon.groupMatchMapId",
|
||||
"ReportedWeapon.weaponSplId",
|
||||
"ReportedWeapon.userId",
|
||||
"GroupMatchMap.index as mapIndex",
|
||||
])
|
||||
.where("GroupMatchMap.matchId", "=", matchId)
|
||||
.orderBy("GroupMatchMap.index", "asc")
|
||||
.orderBy("ReportedWeapon.userId", "asc")
|
||||
.$narrowType<{ groupMatchMapId: NotNull }>()
|
||||
.execute();
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
return rows;
|
||||
}
|
||||
456
app/features/sendouq-match/SQMatchRepository.server.test.ts
Normal file
456
app/features/sendouq-match/SQMatchRepository.server.test.ts
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { db } from "~/db/sql";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import { dbInsertUsers, dbReset } from "~/utils/Test";
|
||||
import * as SQGroupRepository from "../sendouq/SQGroupRepository.server";
|
||||
import * as SQMatchRepository from "./SQMatchRepository.server";
|
||||
|
||||
const { mockSeasonCurrentOrPrevious } = vi.hoisted(() => ({
|
||||
mockSeasonCurrentOrPrevious: vi.fn(() => ({
|
||||
nth: 1,
|
||||
starts: new Date("2023-01-01"),
|
||||
ends: new Date("2030-12-31"),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("~/features/mmr/core/Seasons", () => ({
|
||||
currentOrPrevious: mockSeasonCurrentOrPrevious,
|
||||
}));
|
||||
|
||||
const createGroup = async (userIds: number[]) => {
|
||||
const groupResult = await SQGroupRepository.createGroup({
|
||||
status: "ACTIVE",
|
||||
userId: userIds[0],
|
||||
});
|
||||
|
||||
for (let i = 1; i < userIds.length; i++) {
|
||||
await SQGroupRepository.addMember(groupResult.id, {
|
||||
userId: userIds[i],
|
||||
role: "REGULAR",
|
||||
});
|
||||
}
|
||||
|
||||
return groupResult.id;
|
||||
};
|
||||
|
||||
const createMatch = async (alphaGroupId: number, bravoGroupId: number) => {
|
||||
const match = await db
|
||||
.insertInto("GroupMatch")
|
||||
.values({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
chatCode: "test-chat-code",
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const mapList: Array<{
|
||||
mode: ModeShort;
|
||||
stageId: StageId;
|
||||
}> = [
|
||||
{ mode: "SZ", stageId: 1 },
|
||||
{ mode: "TC", stageId: 2 },
|
||||
{ mode: "RM", stageId: 3 },
|
||||
{ mode: "CB", stageId: 4 },
|
||||
{ mode: "SZ", stageId: 5 },
|
||||
{ mode: "TC", stageId: 6 },
|
||||
{ mode: "RM", stageId: 7 },
|
||||
];
|
||||
|
||||
await db
|
||||
.insertInto("GroupMatchMap")
|
||||
.values(
|
||||
mapList.map((map, i) => ({
|
||||
matchId: match.id,
|
||||
index: i,
|
||||
mode: map.mode,
|
||||
stageId: map.stageId,
|
||||
source: "TIEBREAKER",
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
const fetchMatch = async (matchId: number) => {
|
||||
return db
|
||||
.selectFrom("GroupMatch")
|
||||
.selectAll()
|
||||
.where("id", "=", matchId)
|
||||
.executeTakeFirst();
|
||||
};
|
||||
|
||||
const fetchMapResults = async (matchId: number) => {
|
||||
return db
|
||||
.selectFrom("GroupMatchMap")
|
||||
.selectAll()
|
||||
.where("matchId", "=", matchId)
|
||||
.orderBy("index", "asc")
|
||||
.execute();
|
||||
};
|
||||
|
||||
const fetchGroup = async (groupId: number) => {
|
||||
return db
|
||||
.selectFrom("Group")
|
||||
.selectAll()
|
||||
.where("id", "=", groupId)
|
||||
.executeTakeFirst();
|
||||
};
|
||||
|
||||
const fetchSkills = async (matchId: number) => {
|
||||
return db
|
||||
.selectFrom("Skill")
|
||||
.selectAll()
|
||||
.where("groupMatchId", "=", matchId)
|
||||
.execute();
|
||||
};
|
||||
|
||||
const fetchReportedWeapons = async (matchId: number) => {
|
||||
return db
|
||||
.selectFrom("ReportedWeapon")
|
||||
.innerJoin(
|
||||
"GroupMatchMap",
|
||||
"GroupMatchMap.id",
|
||||
"ReportedWeapon.groupMatchMapId",
|
||||
)
|
||||
.selectAll("ReportedWeapon")
|
||||
.where("GroupMatchMap.matchId", "=", matchId)
|
||||
.execute();
|
||||
};
|
||||
|
||||
describe("updateScore", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(8);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("updates match reportedAt and reportedByUserId", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.updateScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
});
|
||||
|
||||
const updatedMatch = await fetchMatch(match.id);
|
||||
expect(updatedMatch?.reportedAt).not.toBeNull();
|
||||
expect(updatedMatch?.reportedByUserId).toBe(1);
|
||||
});
|
||||
|
||||
test("sets winners correctly for each map", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.updateScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "BRAVO", "ALPHA", "BRAVO"],
|
||||
});
|
||||
|
||||
const maps = await fetchMapResults(match.id);
|
||||
expect(maps[0].winnerGroupId).toBe(alphaGroupId);
|
||||
expect(maps[1].winnerGroupId).toBe(bravoGroupId);
|
||||
expect(maps[2].winnerGroupId).toBe(alphaGroupId);
|
||||
expect(maps[3].winnerGroupId).toBe(bravoGroupId);
|
||||
expect(maps[4].winnerGroupId).toBeNull();
|
||||
});
|
||||
|
||||
test("clears previous winners before setting new ones", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.updateScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "ALPHA", "ALPHA"],
|
||||
});
|
||||
|
||||
await SQMatchRepository.updateScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 5,
|
||||
winners: ["BRAVO", "BRAVO", "BRAVO", "BRAVO"],
|
||||
});
|
||||
|
||||
const maps = await fetchMapResults(match.id);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(maps[i].winnerGroupId).toBe(bravoGroupId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("lockMatchWithoutSkillChange", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(8);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("inserts dummy skill to lock match", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.lockMatchWithoutSkillChange(match.id);
|
||||
|
||||
const skills = await fetchSkills(match.id);
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].season).toBe(-1);
|
||||
expect(skills[0].mu).toBe(-1);
|
||||
expect(skills[0].sigma).toBe(-1);
|
||||
expect(skills[0].ordinal).toBe(-1);
|
||||
expect(skills[0].userId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("adminReport", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(8);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("sets both groups as inactive", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.adminReport({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
});
|
||||
|
||||
const alphaGroup = await fetchGroup(alphaGroupId);
|
||||
const bravoGroup = await fetchGroup(bravoGroupId);
|
||||
expect(alphaGroup?.status).toBe("INACTIVE");
|
||||
expect(bravoGroup?.status).toBe("INACTIVE");
|
||||
|
||||
const updatedMatch = await fetchMatch(match.id);
|
||||
expect(updatedMatch?.reportedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
test("creates skills to lock the match", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.adminReport({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
});
|
||||
|
||||
const skills = await fetchSkills(match.id);
|
||||
expect(skills.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reportScore", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(8);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("first report sets reporter group as inactive", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
const groupMatchMaps = await db
|
||||
.selectFrom("GroupMatchMap")
|
||||
.select(["id", "index"])
|
||||
.where("matchId", "=", match.id)
|
||||
.orderBy("index", "asc")
|
||||
.execute();
|
||||
|
||||
const result = await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [
|
||||
{
|
||||
groupMatchMapId: groupMatchMaps[0].id,
|
||||
weaponSplId: 40,
|
||||
userId: 1,
|
||||
mapIndex: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.status).toBe("REPORTED");
|
||||
expect(result.shouldRefreshCaches).toBe(false);
|
||||
|
||||
const alphaGroup = await fetchGroup(alphaGroupId);
|
||||
expect(alphaGroup?.status).toBe("INACTIVE");
|
||||
|
||||
const bravoGroup = await fetchGroup(bravoGroupId);
|
||||
expect(bravoGroup?.status).toBe("ACTIVE");
|
||||
|
||||
const weapons = await fetchReportedWeapons(match.id);
|
||||
expect(weapons).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("matching second report confirms score and creates skills", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
const result = await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 5,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
expect(result.status).toBe("CONFIRMED");
|
||||
expect(result.shouldRefreshCaches).toBe(true);
|
||||
|
||||
const skills = await fetchSkills(match.id);
|
||||
expect(skills.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("different score returns DIFFERENT status", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
const result = await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 5,
|
||||
winners: ["BRAVO", "BRAVO", "BRAVO", "BRAVO"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
expect(result.status).toBe("DIFFERENT");
|
||||
expect(result.shouldRefreshCaches).toBe(false);
|
||||
});
|
||||
|
||||
test("duplicate report returns DUPLICATE status", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
const result = await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 2,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
expect(result.status).toBe("DUPLICATE");
|
||||
expect(result.shouldRefreshCaches).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelMatch", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(8);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("first cancel report sets group inactive", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
const result = await SQMatchRepository.cancelMatch({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("CANCEL_REPORTED");
|
||||
expect(result.shouldRefreshCaches).toBe(false);
|
||||
|
||||
const alphaGroup = await fetchGroup(alphaGroupId);
|
||||
expect(alphaGroup?.status).toBe("INACTIVE");
|
||||
});
|
||||
|
||||
test("matching cancel confirms and locks match", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.cancelMatch({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
});
|
||||
|
||||
const result = await SQMatchRepository.cancelMatch({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 5,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("CANCEL_CONFIRMED");
|
||||
expect(result.shouldRefreshCaches).toBe(true);
|
||||
|
||||
const alphaGroup = await fetchGroup(alphaGroupId);
|
||||
const bravoGroup = await fetchGroup(bravoGroupId);
|
||||
expect(alphaGroup?.status).toBe("INACTIVE");
|
||||
expect(bravoGroup?.status).toBe("INACTIVE");
|
||||
|
||||
const skills = await fetchSkills(match.id);
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].season).toBe(-1);
|
||||
});
|
||||
|
||||
test("cant cancel after score reported", async () => {
|
||||
const alphaGroupId = await createGroup([1, 2, 3, 4]);
|
||||
const bravoGroupId = await createGroup([5, 6, 7, 8]);
|
||||
const match = await createMatch(alphaGroupId, bravoGroupId);
|
||||
|
||||
await SQMatchRepository.reportScore({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 1,
|
||||
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
|
||||
weapons: [],
|
||||
});
|
||||
|
||||
const result = await SQMatchRepository.cancelMatch({
|
||||
matchId: match.id,
|
||||
reportedByUserId: 5,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("CANT_CANCEL");
|
||||
expect(result.shouldRefreshCaches).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import * as R from "remeda";
|
|||
import { db } from "~/db/sql";
|
||||
import type { DB, ParsedMemento } from "~/db/tables";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator/types";
|
||||
import { mostPopularArrayElement } from "~/utils/arrays";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
|
|
@ -18,7 +19,19 @@ import {
|
|||
} from "~/utils/kysely.server";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import { FULL_GROUP_SIZE } from "../sendouq/q-constants";
|
||||
import * as SQGroupRepository from "../sendouq/SQGroupRepository.server";
|
||||
import { MATCHES_PER_SEASONS_PAGE } from "../user-page/user-page-constants";
|
||||
import { compareMatchToReportedScores } from "./core/match.server";
|
||||
import { mergeReportedWeapons } from "./core/reported-weapons.server";
|
||||
import { calculateMatchSkills } from "./core/skills.server";
|
||||
import {
|
||||
summarizeMaps,
|
||||
summarizePlayerResults,
|
||||
} from "./core/summarizer.server";
|
||||
import * as PlayerStatRepository from "./PlayerStatRepository.server";
|
||||
import { winnersArrayToWinner } from "./q-match-utils";
|
||||
import * as ReportedWeaponRepository from "./ReportedWeaponRepository.server";
|
||||
import * as SkillRepository from "./SkillRepository.server";
|
||||
|
||||
export async function findById(id: number) {
|
||||
const result = await db
|
||||
|
|
@ -492,3 +505,332 @@ async function validateCreatedMatch(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateScore(
|
||||
{
|
||||
matchId,
|
||||
reportedByUserId,
|
||||
winners,
|
||||
}: {
|
||||
matchId: number;
|
||||
reportedByUserId: number;
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
},
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
const executor = trx ?? db;
|
||||
|
||||
const match = await executor
|
||||
.updateTable("GroupMatch")
|
||||
.set({
|
||||
reportedAt: dateToDatabaseTimestamp(new Date()),
|
||||
reportedByUserId,
|
||||
})
|
||||
.where("id", "=", matchId)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await executor
|
||||
.updateTable("GroupMatchMap")
|
||||
.set({ winnerGroupId: null })
|
||||
.where("matchId", "=", matchId)
|
||||
.execute();
|
||||
|
||||
for (const [index, winner] of winners.entries()) {
|
||||
await executor
|
||||
.updateTable("GroupMatchMap")
|
||||
.set({
|
||||
winnerGroupId:
|
||||
winner === "ALPHA" ? match.alphaGroupId : match.bravoGroupId,
|
||||
})
|
||||
.where("matchId", "=", matchId)
|
||||
.where("index", "=", index)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
export function lockMatchWithoutSkillChange(
|
||||
groupMatchId: number,
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
return (trx ?? db)
|
||||
.insertInto("Skill")
|
||||
.values({
|
||||
groupMatchId,
|
||||
identifier: null,
|
||||
mu: -1,
|
||||
season: -1,
|
||||
sigma: -1,
|
||||
ordinal: -1,
|
||||
userId: null,
|
||||
matchesCount: 0,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export type ReportScoreResult =
|
||||
| { status: "REPORTED"; shouldRefreshCaches: false }
|
||||
| { status: "CONFIRMED"; shouldRefreshCaches: true }
|
||||
| { status: "DIFFERENT"; shouldRefreshCaches: false }
|
||||
| { status: "DUPLICATE"; shouldRefreshCaches: false };
|
||||
|
||||
export type CancelMatchResult =
|
||||
| { status: "CANCEL_REPORTED"; shouldRefreshCaches: false }
|
||||
| { status: "CANCEL_CONFIRMED"; shouldRefreshCaches: true }
|
||||
| { status: "CANT_CANCEL"; shouldRefreshCaches: false }
|
||||
| { status: "DUPLICATE"; shouldRefreshCaches: false };
|
||||
|
||||
type WeaponInput = {
|
||||
groupMatchMapId: number;
|
||||
weaponSplId: MainWeaponId;
|
||||
userId: number;
|
||||
mapIndex: number;
|
||||
};
|
||||
|
||||
export async function adminReport({
|
||||
matchId,
|
||||
reportedByUserId,
|
||||
winners,
|
||||
}: {
|
||||
matchId: number;
|
||||
reportedByUserId: number;
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
}): Promise<void> {
|
||||
const match = await findById(matchId);
|
||||
invariant(match, "Match not found");
|
||||
|
||||
const members = buildMembers(match);
|
||||
const winner = winnersArrayToWinner(winners);
|
||||
const winnerGroupId =
|
||||
winner === "ALPHA" ? match.groupAlpha.id : match.groupBravo.id;
|
||||
const loserGroupId =
|
||||
winner === "ALPHA" ? match.groupBravo.id : match.groupAlpha.id;
|
||||
|
||||
const { newSkills, differences } = calculateMatchSkills({
|
||||
groupMatchId: match.id,
|
||||
winner: (match.groupAlpha.id === winnerGroupId
|
||||
? match.groupAlpha
|
||||
: match.groupBravo
|
||||
).members.map((m) => m.id),
|
||||
loser: (match.groupAlpha.id === loserGroupId
|
||||
? match.groupAlpha
|
||||
: match.groupBravo
|
||||
).members.map((m) => m.id),
|
||||
winnerGroupId,
|
||||
loserGroupId,
|
||||
});
|
||||
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await updateScore({ matchId, reportedByUserId, winners }, trx);
|
||||
await SQGroupRepository.setAsInactive(match.groupAlpha.id, trx);
|
||||
await SQGroupRepository.setAsInactive(match.groupBravo.id, trx);
|
||||
await PlayerStatRepository.upsertMapResults(
|
||||
summarizeMaps({ match, members, winners }),
|
||||
trx,
|
||||
);
|
||||
await PlayerStatRepository.upsertPlayerResults(
|
||||
summarizePlayerResults({ match, members, winners }),
|
||||
trx,
|
||||
);
|
||||
await SkillRepository.createMatchSkills(
|
||||
{
|
||||
skills: newSkills,
|
||||
differences,
|
||||
groupMatchId: match.id,
|
||||
oldMatchMemento: match.memento,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function reportScore({
|
||||
matchId,
|
||||
reportedByUserId,
|
||||
winners,
|
||||
weapons,
|
||||
}: {
|
||||
matchId: number;
|
||||
reportedByUserId: number;
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
weapons: WeaponInput[];
|
||||
}): Promise<ReportScoreResult> {
|
||||
const match = await findById(matchId);
|
||||
invariant(match, "Match not found");
|
||||
|
||||
const members = buildMembers(match);
|
||||
const reporterGroupId = members.find(
|
||||
(m) => m.id === reportedByUserId,
|
||||
)?.groupId;
|
||||
invariant(reporterGroupId, "Reporter is not a member of any group");
|
||||
|
||||
const previousReporterGroupId = match.reportedByUserId
|
||||
? members.find((m) => m.id === match.reportedByUserId)?.groupId
|
||||
: undefined;
|
||||
|
||||
const compared = compareMatchToReportedScores({
|
||||
match,
|
||||
winners,
|
||||
newReporterGroupId: reporterGroupId,
|
||||
previousReporterGroupId,
|
||||
});
|
||||
|
||||
const oldReportedWeapons =
|
||||
(await ReportedWeaponRepository.findByMatchId(matchId)) ?? [];
|
||||
const mergedWeapons = mergeReportedWeapons({
|
||||
oldWeapons: oldReportedWeapons,
|
||||
newWeapons: weapons,
|
||||
newReportedMapsCount: winners.length,
|
||||
});
|
||||
const weaponsForDb = mergedWeapons.map((w) => ({
|
||||
groupMatchMapId: w.groupMatchMapId,
|
||||
userId: w.userId,
|
||||
weaponSplId: w.weaponSplId,
|
||||
}));
|
||||
|
||||
if (compared === "DUPLICATE") {
|
||||
await ReportedWeaponRepository.replaceByMatchId(matchId, weaponsForDb);
|
||||
return { status: "DUPLICATE", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
if (compared === "DIFFERENT") {
|
||||
await SQGroupRepository.setAsInactive(reporterGroupId);
|
||||
return { status: "DIFFERENT", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
if (compared === "FIRST_REPORT") {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await updateScore({ matchId, reportedByUserId, winners }, trx);
|
||||
await SQGroupRepository.setAsInactive(reporterGroupId, trx);
|
||||
if (weaponsForDb.length > 0) {
|
||||
await ReportedWeaponRepository.createMany(weaponsForDb, trx);
|
||||
}
|
||||
});
|
||||
return { status: "REPORTED", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
if (compared === "FIX_PREVIOUS") {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await updateScore({ matchId, reportedByUserId, winners }, trx);
|
||||
await ReportedWeaponRepository.replaceByMatchId(
|
||||
matchId,
|
||||
weaponsForDb,
|
||||
trx,
|
||||
);
|
||||
});
|
||||
return { status: "REPORTED", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
const winner = winnersArrayToWinner(winners);
|
||||
const winnerGroupId =
|
||||
winner === "ALPHA" ? match.groupAlpha.id : match.groupBravo.id;
|
||||
const loserGroupId =
|
||||
winner === "ALPHA" ? match.groupBravo.id : match.groupAlpha.id;
|
||||
|
||||
const { newSkills, differences } = calculateMatchSkills({
|
||||
groupMatchId: match.id,
|
||||
winner: (match.groupAlpha.id === winnerGroupId
|
||||
? match.groupAlpha
|
||||
: match.groupBravo
|
||||
).members.map((m) => m.id),
|
||||
loser: (match.groupAlpha.id === loserGroupId
|
||||
? match.groupAlpha
|
||||
: match.groupBravo
|
||||
).members.map((m) => m.id),
|
||||
winnerGroupId,
|
||||
loserGroupId,
|
||||
});
|
||||
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await SQGroupRepository.setAsInactive(reporterGroupId, trx);
|
||||
await PlayerStatRepository.upsertMapResults(
|
||||
summarizeMaps({ match, members, winners }),
|
||||
trx,
|
||||
);
|
||||
await PlayerStatRepository.upsertPlayerResults(
|
||||
summarizePlayerResults({ match, members, winners }),
|
||||
trx,
|
||||
);
|
||||
await SkillRepository.createMatchSkills(
|
||||
{
|
||||
skills: newSkills,
|
||||
differences,
|
||||
groupMatchId: match.id,
|
||||
oldMatchMemento: match.memento,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
await ReportedWeaponRepository.replaceByMatchId(matchId, weaponsForDb, trx);
|
||||
});
|
||||
|
||||
return { status: "CONFIRMED", shouldRefreshCaches: true };
|
||||
}
|
||||
|
||||
export async function cancelMatch({
|
||||
matchId,
|
||||
reportedByUserId,
|
||||
}: {
|
||||
matchId: number;
|
||||
reportedByUserId: number;
|
||||
}): Promise<CancelMatchResult> {
|
||||
const match = await findById(matchId);
|
||||
invariant(match, "Match not found");
|
||||
|
||||
const members = buildMembers(match);
|
||||
const reporterGroupId = members.find(
|
||||
(m) => m.id === reportedByUserId,
|
||||
)?.groupId;
|
||||
invariant(reporterGroupId, "Reporter is not a member of any group");
|
||||
|
||||
const previousReporterGroupId = match.reportedByUserId
|
||||
? members.find((m) => m.id === match.reportedByUserId)?.groupId
|
||||
: undefined;
|
||||
|
||||
const compared = compareMatchToReportedScores({
|
||||
match,
|
||||
winners: [],
|
||||
newReporterGroupId: reporterGroupId,
|
||||
previousReporterGroupId,
|
||||
});
|
||||
|
||||
if (compared === "DUPLICATE") {
|
||||
return { status: "DUPLICATE", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
if (compared === "DIFFERENT") {
|
||||
await SQGroupRepository.setAsInactive(reporterGroupId);
|
||||
return { status: "CANT_CANCEL", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
if (compared === "FIRST_REPORT" || compared === "FIX_PREVIOUS") {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await updateScore({ matchId, reportedByUserId, winners: [] }, trx);
|
||||
await SQGroupRepository.setAsInactive(reporterGroupId, trx);
|
||||
if (compared === "FIX_PREVIOUS") {
|
||||
await ReportedWeaponRepository.replaceByMatchId(matchId, [], trx);
|
||||
}
|
||||
});
|
||||
return { status: "CANCEL_REPORTED", shouldRefreshCaches: false };
|
||||
}
|
||||
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await SQGroupRepository.setAsInactive(reporterGroupId, trx);
|
||||
await lockMatchWithoutSkillChange(match.id, trx);
|
||||
});
|
||||
return { status: "CANCEL_CONFIRMED", shouldRefreshCaches: true };
|
||||
}
|
||||
|
||||
function buildMembers(
|
||||
match: NonNullable<Awaited<ReturnType<typeof findById>>>,
|
||||
) {
|
||||
return [
|
||||
...match.groupAlpha.members.map((m) => ({
|
||||
...m,
|
||||
groupId: match.groupAlpha.id,
|
||||
})),
|
||||
...match.groupBravo.members.map((m) => ({
|
||||
...m,
|
||||
groupId: match.groupBravo.id,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
139
app/features/sendouq-match/SkillRepository.server.ts
Normal file
139
app/features/sendouq-match/SkillRepository.server.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import type { Transaction } from "kysely";
|
||||
import { ordinal } from "openskill";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, ParsedMemento, Tables } from "~/db/tables";
|
||||
import { identifierToUserIds } from "~/features/mmr/mmr-utils";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import type { MementoSkillDifferences } from "./core/skills.server";
|
||||
|
||||
export async function createMatchSkills(
|
||||
{
|
||||
groupMatchId,
|
||||
skills,
|
||||
oldMatchMemento,
|
||||
differences,
|
||||
}: {
|
||||
groupMatchId: number;
|
||||
skills: Pick<
|
||||
Tables["Skill"],
|
||||
"groupMatchId" | "identifier" | "mu" | "season" | "sigma" | "userId"
|
||||
>[];
|
||||
oldMatchMemento: ParsedMemento | null;
|
||||
differences: MementoSkillDifferences;
|
||||
},
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
const executor = trx ?? db;
|
||||
const createdAt = databaseTimestampNow();
|
||||
|
||||
for (const skill of skills) {
|
||||
const insertedSkill = await insertSkillWithOrdinal(
|
||||
{
|
||||
...skill,
|
||||
createdAt,
|
||||
ordinal: ordinal(skill),
|
||||
},
|
||||
executor,
|
||||
);
|
||||
|
||||
if (insertedSkill.identifier) {
|
||||
for (const userId of identifierToUserIds(insertedSkill.identifier)) {
|
||||
await executor
|
||||
.insertInto("SkillTeamUser")
|
||||
.values({
|
||||
skillId: insertedSkill.id,
|
||||
userId,
|
||||
})
|
||||
.onConflict((oc) => oc.columns(["skillId", "userId"]).doNothing())
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!oldMatchMemento) return;
|
||||
|
||||
const newMemento: ParsedMemento = {
|
||||
...oldMatchMemento,
|
||||
groups: {},
|
||||
users: {},
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(oldMatchMemento.users)) {
|
||||
newMemento.users[key as unknown as number] = {
|
||||
...value,
|
||||
skillDifference:
|
||||
differences.users[key as unknown as number]?.skillDifference,
|
||||
};
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(oldMatchMemento.groups)) {
|
||||
newMemento.groups[key as unknown as number] = {
|
||||
...value,
|
||||
skillDifference:
|
||||
differences.groups[key as unknown as number]?.skillDifference,
|
||||
};
|
||||
}
|
||||
|
||||
await executor
|
||||
.updateTable("GroupMatch")
|
||||
.set({ memento: JSON.stringify(newMemento) })
|
||||
.where("id", "=", groupMatchId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function insertSkillWithOrdinal(
|
||||
skill: {
|
||||
groupMatchId: number | null;
|
||||
identifier: string | null;
|
||||
mu: number;
|
||||
season: number;
|
||||
sigma: number;
|
||||
userId: number | null;
|
||||
createdAt: number;
|
||||
ordinal: number;
|
||||
},
|
||||
executor: Transaction<DB> | typeof db,
|
||||
) {
|
||||
const isUserSkill = skill.userId !== null;
|
||||
const isTeamSkill = skill.identifier !== null;
|
||||
|
||||
let previousMatchesCount = 0;
|
||||
|
||||
if (isUserSkill) {
|
||||
const previousSkill = await executor
|
||||
.selectFrom("Skill")
|
||||
.select(({ fn }) => fn.max("matchesCount").as("maxMatchesCount"))
|
||||
.where("userId", "=", skill.userId)
|
||||
.where("season", "=", skill.season)
|
||||
.executeTakeFirst();
|
||||
|
||||
previousMatchesCount = previousSkill?.maxMatchesCount ?? 0;
|
||||
} else if (isTeamSkill) {
|
||||
const previousSkill = await executor
|
||||
.selectFrom("Skill")
|
||||
.select(({ fn }) => fn.max("matchesCount").as("maxMatchesCount"))
|
||||
.where("identifier", "=", skill.identifier)
|
||||
.where("season", "=", skill.season)
|
||||
.executeTakeFirst();
|
||||
|
||||
previousMatchesCount = previousSkill?.maxMatchesCount ?? 0;
|
||||
}
|
||||
|
||||
const insertedSkill = await executor
|
||||
.insertInto("Skill")
|
||||
.values({
|
||||
groupMatchId: skill.groupMatchId,
|
||||
identifier: skill.identifier,
|
||||
mu: skill.mu,
|
||||
season: skill.season,
|
||||
sigma: skill.sigma,
|
||||
ordinal: skill.ordinal,
|
||||
userId: skill.userId,
|
||||
createdAt: skill.createdAt,
|
||||
matchesCount: previousMatchesCount + 1,
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return insertedSkill;
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import type { ActionFunctionArgs } from "react-router";
|
||||
import { redirect } from "react-router";
|
||||
import { sql } from "~/db/sql";
|
||||
import type { ReportedWeapon } from "~/db/tables";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
|
||||
|
|
@ -13,6 +12,7 @@ import {
|
|||
} from "~/features/sendouq/core/SendouQ.server";
|
||||
import * as PrivateUserNoteRepository from "~/features/sendouq/PrivateUserNoteRepository.server";
|
||||
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
|
||||
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
|
||||
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
|
|
@ -25,25 +25,8 @@ import {
|
|||
} from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { SENDOUQ_PREPARING_PAGE, sendouQMatchPage } from "~/utils/urls";
|
||||
import { compareMatchToReportedScores } from "../core/match.server";
|
||||
import { mergeReportedWeapons } from "../core/reported-weapons.server";
|
||||
import { calculateMatchSkills } from "../core/skills.server";
|
||||
import {
|
||||
summarizeMaps,
|
||||
summarizePlayerResults,
|
||||
} from "../core/summarizer.server";
|
||||
import { matchSchema, qMatchPageParamsSchema } from "../q-match-schemas";
|
||||
import { winnersArrayToWinner } from "../q-match-utils";
|
||||
import { addDummySkill } from "../queries/addDummySkill.server";
|
||||
import { addMapResults } from "../queries/addMapResults.server";
|
||||
import { addPlayerResults } from "../queries/addPlayerResults.server";
|
||||
import { addReportedWeapons } from "../queries/addReportedWeapons.server";
|
||||
import { addSkills } from "../queries/addSkills.server";
|
||||
import { deleteReporterWeaponsByMatchId } from "../queries/deleteReportedWeaponsByMatchId.server";
|
||||
import { findMatchById } from "../queries/findMatchById.server";
|
||||
import { reportedWeaponsByMatchId } from "../queries/reportedWeaponsByMatchId.server";
|
||||
import { reportScore } from "../queries/reportScore.server";
|
||||
import { setGroupAsInactive } from "../queries/setGroupAsInactive.server";
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
const matchId = parseParams({
|
||||
|
|
@ -58,30 +41,27 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
|
||||
switch (data._action) {
|
||||
case "REPORT_SCORE": {
|
||||
const reportWeapons = () => {
|
||||
const oldReportedWeapons = reportedWeaponsByMatchId(matchId) ?? [];
|
||||
|
||||
const mergedWeapons = mergeReportedWeapons({
|
||||
oldWeapons: oldReportedWeapons,
|
||||
newWeapons: data.weapons as (ReportedWeapon & {
|
||||
mapIndex: number;
|
||||
groupMatchMapId: number;
|
||||
})[],
|
||||
newReportedMapsCount: data.winners.length,
|
||||
});
|
||||
|
||||
sql.transaction(() => {
|
||||
deleteReporterWeaponsByMatchId(matchId);
|
||||
addReportedWeapons(mergedWeapons);
|
||||
})();
|
||||
};
|
||||
|
||||
const unmappedMatch = notFoundIfFalsy(
|
||||
await SQMatchRepository.findById(matchId),
|
||||
);
|
||||
const match = SendouQ.mapMatch(unmappedMatch, user);
|
||||
|
||||
if (match.isLocked) {
|
||||
reportWeapons();
|
||||
const oldReportedWeapons =
|
||||
(await ReportedWeaponRepository.findByMatchId(matchId)) ?? [];
|
||||
const mergedWeapons = mergeReportedWeapons({
|
||||
oldWeapons: oldReportedWeapons,
|
||||
newWeapons: data.weapons,
|
||||
newReportedMapsCount: data.winners.length,
|
||||
});
|
||||
await ReportedWeaponRepository.replaceByMatchId(
|
||||
matchId,
|
||||
mergedWeapons.map((w) => ({
|
||||
groupMatchMapId: w.groupMatchMapId,
|
||||
userId: w.userId,
|
||||
weaponSplId: w.weaponSplId,
|
||||
})),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +69,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
!data.adminReport || user.roles.includes("STAFF"),
|
||||
"Only mods can report scores as admin",
|
||||
);
|
||||
|
||||
const members = [
|
||||
...match.groupAlpha.members.map((m) => ({
|
||||
...m,
|
||||
|
|
@ -99,147 +80,116 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
groupId: match.groupBravo.id,
|
||||
})),
|
||||
];
|
||||
|
||||
const groupMemberOfId = members.find((m) => m.id === user.id)?.groupId;
|
||||
invariant(
|
||||
groupMemberOfId || data.adminReport,
|
||||
members.some((m) => m.id === user.id) || data.adminReport,
|
||||
"User is not a member of any group",
|
||||
);
|
||||
|
||||
const winner = winnersArrayToWinner(data.winners);
|
||||
const winnerGroupId =
|
||||
winner === "ALPHA" ? match.groupAlpha.id : match.groupBravo.id;
|
||||
const loserGroupId =
|
||||
winner === "ALPHA" ? match.groupBravo.id : match.groupAlpha.id;
|
||||
if (data.adminReport) {
|
||||
await SQMatchRepository.adminReport({
|
||||
matchId,
|
||||
reportedByUserId: user.id,
|
||||
winners: data.winners,
|
||||
});
|
||||
|
||||
// when admin reports match gets locked right away
|
||||
const compared = data.adminReport
|
||||
? "SAME"
|
||||
: compareMatchToReportedScores({
|
||||
match,
|
||||
winners: data.winners,
|
||||
newReporterGroupId: groupMemberOfId!,
|
||||
previousReporterGroupId: match.reportedByUserId
|
||||
? members.find((m) => m.id === match.reportedByUserId)!.groupId
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// same group reporting same score, probably by mistake
|
||||
if (compared === "DUPLICATE") {
|
||||
reportWeapons();
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchIsBeingCanceled = data.winners.length === 0;
|
||||
|
||||
const { newSkills, differences } =
|
||||
compared === "SAME" && !matchIsBeingCanceled
|
||||
? calculateMatchSkills({
|
||||
groupMatchId: match.id,
|
||||
winner: (match.groupAlpha.id === winnerGroupId
|
||||
? match.groupAlpha
|
||||
: match.groupBravo
|
||||
).members.map((m) => m.id),
|
||||
loser: (match.groupAlpha.id === loserGroupId
|
||||
? match.groupAlpha
|
||||
: match.groupBravo
|
||||
).members.map((m) => m.id),
|
||||
winnerGroupId,
|
||||
loserGroupId,
|
||||
})
|
||||
: { newSkills: null, differences: null };
|
||||
|
||||
const shouldLockMatchWithoutChangingRecords =
|
||||
compared === "SAME" && matchIsBeingCanceled;
|
||||
|
||||
let clearCaches = false;
|
||||
sql.transaction(() => {
|
||||
if (
|
||||
compared === "FIX_PREVIOUS" ||
|
||||
compared === "FIRST_REPORT" ||
|
||||
data.adminReport
|
||||
) {
|
||||
reportScore({
|
||||
matchId,
|
||||
reportedByUserId: user.id,
|
||||
winners: data.winners,
|
||||
});
|
||||
}
|
||||
// own group gets set inactive
|
||||
if (groupMemberOfId) setGroupAsInactive(groupMemberOfId);
|
||||
// skills & map/player results only update after both teams have reported
|
||||
if (newSkills) {
|
||||
addMapResults(
|
||||
summarizeMaps({ match, members, winners: data.winners }),
|
||||
);
|
||||
addPlayerResults(
|
||||
summarizePlayerResults({ match, members, winners: data.winners }),
|
||||
);
|
||||
addSkills({
|
||||
skills: newSkills,
|
||||
differences,
|
||||
groupMatchId: match.id,
|
||||
oldMatchMemento: match.memento,
|
||||
});
|
||||
clearCaches = true;
|
||||
}
|
||||
if (shouldLockMatchWithoutChangingRecords) {
|
||||
addDummySkill(match.id);
|
||||
clearCaches = true;
|
||||
}
|
||||
// fix edge case where they 1) report score 2) report weapons 3) report score again, but with different amount of maps played
|
||||
if (compared === "FIX_PREVIOUS") {
|
||||
deleteReporterWeaponsByMatchId(matchId);
|
||||
}
|
||||
// admin reporting, just set both groups inactive
|
||||
if (data.adminReport) {
|
||||
setGroupAsInactive(match.groupAlpha.id);
|
||||
setGroupAsInactive(match.groupBravo.id);
|
||||
}
|
||||
})();
|
||||
|
||||
if (clearCaches) {
|
||||
// this is kind of useless to do when admin reports since skills don't change
|
||||
// but it's not the most common case so it's ok
|
||||
try {
|
||||
refreshUserSkills(Seasons.currentOrPrevious()!.nth);
|
||||
} catch (error) {
|
||||
logger.warn("Error refreshing user skills", error);
|
||||
}
|
||||
refreshStreamsCache();
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
if (match.chatCode) {
|
||||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
type: "SCORE_CONFIRMED",
|
||||
context: { name: user.username },
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const matchIsBeingCanceled = data.winners.length === 0;
|
||||
|
||||
if (matchIsBeingCanceled) {
|
||||
const result = await SQMatchRepository.cancelMatch({
|
||||
matchId,
|
||||
reportedByUserId: user.id,
|
||||
});
|
||||
|
||||
if (result.shouldRefreshCaches) {
|
||||
try {
|
||||
refreshUserSkills(Seasons.currentOrPrevious()!.nth);
|
||||
} catch (error) {
|
||||
logger.warn("Error refreshing user skills", error);
|
||||
}
|
||||
refreshStreamsCache();
|
||||
}
|
||||
|
||||
if (result.status === "CANT_CANCEL") {
|
||||
return { error: "cant-cancel" as const };
|
||||
}
|
||||
|
||||
if (result.status === "DUPLICATE") {
|
||||
break;
|
||||
}
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
if (match.chatCode) {
|
||||
const type: NonNullable<ChatMessage["type"]> =
|
||||
result.status === "CANCEL_CONFIRMED"
|
||||
? "CANCEL_CONFIRMED"
|
||||
: "CANCEL_REPORTED";
|
||||
|
||||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
type,
|
||||
context: { name: user.username },
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const result = await SQMatchRepository.reportScore({
|
||||
matchId,
|
||||
reportedByUserId: user.id,
|
||||
winners: data.winners,
|
||||
weapons: data.weapons as (ReportedWeapon & {
|
||||
mapIndex: number;
|
||||
groupMatchMapId: number;
|
||||
})[],
|
||||
});
|
||||
|
||||
if (result.shouldRefreshCaches) {
|
||||
try {
|
||||
refreshUserSkills(Seasons.currentOrPrevious()!.nth);
|
||||
} catch (error) {
|
||||
logger.warn("Error refreshing user skills", error);
|
||||
}
|
||||
refreshStreamsCache();
|
||||
}
|
||||
|
||||
if (compared === "DIFFERENT") {
|
||||
return {
|
||||
error: matchIsBeingCanceled
|
||||
? ("cant-cancel" as const)
|
||||
: ("different" as const),
|
||||
};
|
||||
if (result.status === "DIFFERENT") {
|
||||
return { error: "different" as const };
|
||||
}
|
||||
|
||||
// in a different transaction but it's okay
|
||||
reportWeapons();
|
||||
if (result.status !== "DUPLICATE") {
|
||||
await refreshSendouQInstance();
|
||||
}
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
if (match.chatCode) {
|
||||
const type = (): NonNullable<ChatMessage["type"]> => {
|
||||
if (compared === "SAME") {
|
||||
return matchIsBeingCanceled
|
||||
? "CANCEL_CONFIRMED"
|
||||
: "SCORE_CONFIRMED";
|
||||
}
|
||||
|
||||
return matchIsBeingCanceled ? "CANCEL_REPORTED" : "SCORE_REPORTED";
|
||||
};
|
||||
if (match.chatCode && result.status !== "DUPLICATE") {
|
||||
const type: NonNullable<ChatMessage["type"]> =
|
||||
result.status === "CONFIRMED" ? "SCORE_CONFIRMED" : "SCORE_REPORTED";
|
||||
|
||||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
type: type(),
|
||||
context: {
|
||||
name: user.username,
|
||||
},
|
||||
type,
|
||||
context: { name: user.username },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -282,10 +232,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
throw redirect(SENDOUQ_PREPARING_PAGE);
|
||||
}
|
||||
case "REPORT_WEAPONS": {
|
||||
const match = notFoundIfFalsy(findMatchById(matchId));
|
||||
const match = notFoundIfFalsy(await SQMatchRepository.findById(matchId));
|
||||
errorToastIfFalsy(match.reportedAt, "Match has not been reported yet");
|
||||
|
||||
const oldReportedWeapons = reportedWeaponsByMatchId(matchId) ?? [];
|
||||
const oldReportedWeapons =
|
||||
(await ReportedWeaponRepository.findByMatchId(matchId)) ?? [];
|
||||
|
||||
const mergedWeapons = mergeReportedWeapons({
|
||||
oldWeapons: oldReportedWeapons,
|
||||
|
|
@ -295,10 +246,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
})[],
|
||||
});
|
||||
|
||||
sql.transaction(() => {
|
||||
deleteReporterWeaponsByMatchId(matchId);
|
||||
addReportedWeapons(mergedWeapons);
|
||||
})();
|
||||
await ReportedWeaponRepository.replaceByMatchId(
|
||||
matchId,
|
||||
mergedWeapons.map((w) => ({
|
||||
groupMatchMapId: w.groupMatchMapId,
|
||||
userId: w.userId,
|
||||
weaponSplId: w.weaponSplId,
|
||||
})),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,7 +303,10 @@ export function compareMatchToReportedScores({
|
|||
newReporterGroupId,
|
||||
previousReporterGroupId,
|
||||
}: {
|
||||
match: SQMatch;
|
||||
match: Pick<SQMatch, "reportedByUserId" | "mapList"> & {
|
||||
groupAlpha: { id: number };
|
||||
groupBravo: { id: number };
|
||||
};
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
newReporterGroupId: number;
|
||||
previousReporterGroupId?: number;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { SQMatchGroup } from "~/features/sendouq/core/SendouQ.server";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import type { MatchById } from "../queries/findMatchById.server";
|
||||
import type { reportedWeaponsByMatchId } from "../queries/reportedWeaponsByMatchId.server";
|
||||
import type * as ReportedWeaponRepository from "../ReportedWeaponRepository.server";
|
||||
import type * as SQMatchRepository from "../SQMatchRepository.server";
|
||||
|
||||
export type ReportedWeaponForMerging = {
|
||||
weaponSplId?: MainWeaponId;
|
||||
|
|
@ -65,8 +65,12 @@ export function reportedWeaponsToArrayOfArrays({
|
|||
groupAlpha,
|
||||
groupBravo,
|
||||
}: {
|
||||
reportedWeapons: ReturnType<typeof reportedWeaponsByMatchId>;
|
||||
mapList: MatchById["mapList"];
|
||||
reportedWeapons: Awaited<
|
||||
ReturnType<typeof ReportedWeaponRepository.findByMatchId>
|
||||
>;
|
||||
mapList: NonNullable<
|
||||
Awaited<ReturnType<typeof SQMatchRepository.findById>>
|
||||
>["mapList"];
|
||||
groupAlpha: SQMatchGroup;
|
||||
groupBravo: SQMatchGroup;
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
import type { Tables } from "~/db/tables";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import type { SQMatch } from "~/features/sendouq/core/SendouQ.server";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { winnersArrayToWinner } from "../q-match-utils";
|
||||
|
||||
type MatchForSummarizing = {
|
||||
mapList: Array<{ mode: ModeShort; stageId: StageId }>;
|
||||
groupAlpha: { id: number };
|
||||
groupBravo: { id: number };
|
||||
};
|
||||
|
||||
export function summarizeMaps({
|
||||
match,
|
||||
winners,
|
||||
members,
|
||||
}: {
|
||||
match: SQMatch;
|
||||
match: MatchForSummarizing;
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
members: { id: number; groupId: number }[];
|
||||
}) {
|
||||
|
|
@ -59,7 +65,7 @@ export function summarizePlayerResults({
|
|||
winners,
|
||||
members,
|
||||
}: {
|
||||
match: SQMatch;
|
||||
match: MatchForSummarizing;
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
members: { id: number; groupId: number }[];
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { getUser } from "~/features/auth/core/user.server";
|
|||
import { SendouQ } from "~/features/sendouq/core/SendouQ.server";
|
||||
import * as PrivateUserNoteRepository from "~/features/sendouq/PrivateUserNoteRepository.server";
|
||||
import { reportedWeaponsToArrayOfArrays } from "~/features/sendouq-match/core/reported-weapons.server";
|
||||
import { reportedWeaponsByMatchId } from "~/features/sendouq-match/queries/reportedWeaponsByMatchId.server";
|
||||
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
|
||||
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
|
||||
import { qMatchPageParamsSchema } from "../q-match-schemas";
|
||||
|
|
@ -29,7 +29,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
|||
const match = SendouQ.mapMatch(matchUnmapped, user, privateNotes);
|
||||
|
||||
const rawReportedWeapons = match.reportedAt
|
||||
? reportedWeaponsByMatchId(matchId)
|
||||
? await ReportedWeaponRepository.findByMatchId(matchId)
|
||||
: null;
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const weapons = z.preprocess(
|
|||
groupMatchMapId: id,
|
||||
}),
|
||||
)
|
||||
.nullish()
|
||||
.optional()
|
||||
.default([]),
|
||||
);
|
||||
export const matchSchema = z.union([
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
insert into "Skill" ("groupMatchId", "identifier", "mu", "season", "sigma", "ordinal", "userId", "matchesCount")
|
||||
values (
|
||||
@groupMatchId,
|
||||
null,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
null,
|
||||
0
|
||||
)
|
||||
`);
|
||||
|
||||
/** Adds a placeholder skill that makes the match locked */
|
||||
export function addDummySkill(groupMatchId: number) {
|
||||
stm.run({ groupMatchId });
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
|
||||
const addMapResultDeltaStm = sql.prepare(/* sql */ `
|
||||
insert into "MapResult" (
|
||||
"mode",
|
||||
"stageId",
|
||||
"userId",
|
||||
"wins",
|
||||
"losses",
|
||||
"season"
|
||||
) values (
|
||||
@mode,
|
||||
@stageId,
|
||||
@userId,
|
||||
@wins,
|
||||
@losses,
|
||||
@season
|
||||
) on conflict ("userId", "stageId", "mode", "season") do
|
||||
update
|
||||
set
|
||||
"wins" = "wins" + @wins,
|
||||
"losses" = "losses" + @losses
|
||||
`);
|
||||
|
||||
export function addMapResults(
|
||||
results: Array<
|
||||
Pick<Tables["MapResult"], "losses" | "wins" | "userId" | "mode" | "stageId">
|
||||
>,
|
||||
) {
|
||||
for (const result of results) {
|
||||
addMapResultDeltaStm.run(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
|
||||
const addPlayerResultDeltaStm = sql.prepare(/* sql */ `
|
||||
insert into "PlayerResult" (
|
||||
"ownerUserId",
|
||||
"otherUserId",
|
||||
"mapWins",
|
||||
"mapLosses",
|
||||
"setWins",
|
||||
"setLosses",
|
||||
"type",
|
||||
"season"
|
||||
) values (
|
||||
@ownerUserId,
|
||||
@otherUserId,
|
||||
@mapWins,
|
||||
@mapLosses,
|
||||
@setWins,
|
||||
@setLosses,
|
||||
@type,
|
||||
@season
|
||||
) on conflict ("ownerUserId", "otherUserId", "type", "season") do
|
||||
update
|
||||
set
|
||||
"mapWins" = "mapWins" + @mapWins,
|
||||
"mapLosses" = "mapLosses" + @mapLosses,
|
||||
"setWins" = "setWins" + @setWins,
|
||||
"setLosses" = "setLosses" + @setLosses
|
||||
`);
|
||||
|
||||
export function addPlayerResults(results: Array<Tables["PlayerResult"]>) {
|
||||
for (const result of results) {
|
||||
addPlayerResultDeltaStm.run({
|
||||
ownerUserId: result.ownerUserId,
|
||||
otherUserId: result.otherUserId,
|
||||
mapWins: result.mapWins,
|
||||
mapLosses: result.mapLosses,
|
||||
setWins: result.setWins,
|
||||
setLosses: result.setLosses,
|
||||
type: result.type,
|
||||
season: result.season,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
|
||||
const insertStm = sql.prepare(/* sql */ `
|
||||
insert into "ReportedWeapon"
|
||||
("groupMatchMapId", "weaponSplId", "userId")
|
||||
values (@groupMatchMapId, @weaponSplId, @userId)
|
||||
`);
|
||||
|
||||
export const addReportedWeapons = (
|
||||
args: {
|
||||
groupMatchMapId: number;
|
||||
weaponSplId: MainWeaponId;
|
||||
userId: number;
|
||||
}[],
|
||||
) => {
|
||||
for (const { groupMatchMapId, userId, weaponSplId } of args) {
|
||||
insertStm.run({ groupMatchMapId, userId, weaponSplId });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import { ordinal } from "openskill";
|
||||
import { sql } from "~/db/sql";
|
||||
import type { ParsedMemento, Tables } from "~/db/tables";
|
||||
import { identifierToUserIds } from "~/features/mmr/mmr-utils";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import type { MementoSkillDifferences } from "../core/skills.server";
|
||||
|
||||
const getStm = (type: "user" | "team") =>
|
||||
sql.prepare(/* sql */ `
|
||||
insert into "Skill" ("groupMatchId", "identifier", "mu", "season", "sigma", "ordinal", "userId", "createdAt", "matchesCount")
|
||||
values (
|
||||
@groupMatchId,
|
||||
@identifier,
|
||||
@mu,
|
||||
@season,
|
||||
@sigma,
|
||||
@ordinal,
|
||||
@userId,
|
||||
@createdAt,
|
||||
1 + coalesce((
|
||||
select max("matchesCount") from "Skill"
|
||||
where
|
||||
${type === "user" ? /* sql */ `"userId" = @userId` : ""}
|
||||
${type === "team" ? /* sql */ `"identifier" = @identifier` : ""}
|
||||
and "season" = @season
|
||||
group by ${
|
||||
type === "user" ? /* sql */ `"userId"` : /* sql */ `"identifier"`
|
||||
}
|
||||
), 0)
|
||||
) returning *
|
||||
`);
|
||||
|
||||
const addSkillTeamUserStm = sql.prepare(/* sql */ `
|
||||
insert into "SkillTeamUser" (
|
||||
"skillId",
|
||||
"userId"
|
||||
) values (
|
||||
@skillId,
|
||||
@userId
|
||||
) on conflict("skillId", "userId") do nothing
|
||||
`);
|
||||
|
||||
const userStm = getStm("user");
|
||||
const teamStm = getStm("team");
|
||||
|
||||
const updateMatchMementoStm = sql.prepare(/* sql */ `
|
||||
update "GroupMatch"
|
||||
set "memento" = @memento
|
||||
where "id" = @id
|
||||
`);
|
||||
|
||||
export function addSkills({
|
||||
groupMatchId,
|
||||
skills,
|
||||
oldMatchMemento,
|
||||
differences,
|
||||
}: {
|
||||
groupMatchId: number;
|
||||
skills: Pick<
|
||||
Tables["Skill"],
|
||||
"groupMatchId" | "identifier" | "mu" | "season" | "sigma" | "userId"
|
||||
>[];
|
||||
oldMatchMemento: ParsedMemento | null;
|
||||
differences: MementoSkillDifferences;
|
||||
}) {
|
||||
for (const skill of skills) {
|
||||
const stm = skill.userId ? userStm : teamStm;
|
||||
const insertedSkill = stm.get({
|
||||
...skill,
|
||||
createdAt: databaseTimestampNow(),
|
||||
ordinal: ordinal(skill),
|
||||
}) as Tables["Skill"];
|
||||
|
||||
if (insertedSkill.identifier) {
|
||||
for (const userId of identifierToUserIds(insertedSkill.identifier)) {
|
||||
addSkillTeamUserStm.run({
|
||||
skillId: insertedSkill.id,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!oldMatchMemento) return;
|
||||
|
||||
const newMemento: ParsedMemento = {
|
||||
...oldMatchMemento,
|
||||
groups: {},
|
||||
users: {},
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(oldMatchMemento.users)) {
|
||||
newMemento.users[key as any] = {
|
||||
...value,
|
||||
skillDifference: differences.users[key as any]?.skillDifference,
|
||||
};
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(oldMatchMemento.groups)) {
|
||||
newMemento.groups[key as any] = {
|
||||
...value,
|
||||
skillDifference: differences.groups[key as any]?.skillDifference,
|
||||
};
|
||||
}
|
||||
|
||||
updateMatchMementoStm.run({
|
||||
id: groupMatchId,
|
||||
memento: JSON.stringify(newMemento),
|
||||
});
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
|
||||
const deleteStm = sql.prepare(/* sql */ `
|
||||
delete from "ReportedWeapon"
|
||||
where "groupMatchMapId" = @groupMatchMapId
|
||||
`);
|
||||
|
||||
const getGroupMatchMapsStm = sql.prepare(/* sql */ `
|
||||
select "id" from "GroupMatchMap"
|
||||
where "matchId" = @matchId
|
||||
`);
|
||||
|
||||
export const deleteReporterWeaponsByMatchId = (matchId: number) => {
|
||||
const groupMatchMaps = getGroupMatchMapsStm.all({ matchId }) as Array<{
|
||||
id: number;
|
||||
}>;
|
||||
|
||||
for (const { id } of groupMatchMaps) {
|
||||
deleteStm.run({ groupMatchMapId: id });
|
||||
}
|
||||
};
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { ParsedMemento, Tables } from "~/db/tables";
|
||||
import { parseDBJsonArray } from "~/utils/sql";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
select
|
||||
"GroupMatch"."id",
|
||||
"GroupMatch"."alphaGroupId",
|
||||
"GroupMatch"."bravoGroupId",
|
||||
"GroupMatch"."createdAt",
|
||||
"GroupMatch"."reportedAt",
|
||||
"GroupMatch"."reportedByUserId",
|
||||
"GroupMatch"."chatCode",
|
||||
"GroupMatch"."memento",
|
||||
(select exists (select 1 from "Skill" where "Skill"."groupMatchId" = @id)) as "isLocked",
|
||||
json_group_array(
|
||||
json_object(
|
||||
'id', "GroupMatchMap"."id",
|
||||
'mode', "GroupMatchMap"."mode",
|
||||
'stageId', "GroupMatchMap"."stageId",
|
||||
'source', "GroupMatchMap"."source",
|
||||
'winnerGroupId', "GroupMatchMap"."winnerGroupId"
|
||||
)
|
||||
) as "mapList"
|
||||
from "GroupMatch"
|
||||
left join "GroupMatchMap" on "GroupMatchMap"."matchId" = "GroupMatch"."id"
|
||||
where "GroupMatch"."id" = @id
|
||||
group by "GroupMatch"."id"
|
||||
order by "GroupMatchMap"."index" asc
|
||||
`);
|
||||
|
||||
export interface MatchById {
|
||||
id: Tables["GroupMatch"]["id"];
|
||||
alphaGroupId: Tables["GroupMatch"]["alphaGroupId"];
|
||||
bravoGroupId: Tables["GroupMatch"]["bravoGroupId"];
|
||||
createdAt: Tables["GroupMatch"]["createdAt"];
|
||||
reportedAt: Tables["GroupMatch"]["reportedAt"];
|
||||
reportedByUserId: Tables["GroupMatch"]["reportedByUserId"];
|
||||
chatCode: Tables["GroupMatch"]["chatCode"];
|
||||
isLocked: boolean;
|
||||
memento: ParsedMemento;
|
||||
mapList: Array<
|
||||
Pick<
|
||||
Tables["GroupMatchMap"],
|
||||
"id" | "mode" | "stageId" | "source" | "winnerGroupId"
|
||||
>
|
||||
>;
|
||||
}
|
||||
|
||||
export function findMatchById(id: number) {
|
||||
const row = stm.get({ id }) as any;
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
...row,
|
||||
mapList: parseDBJsonArray(row.mapList),
|
||||
isLocked: Boolean(row.isLocked),
|
||||
memento: row.memento ? JSON.parse(row.memento) : null,
|
||||
} as MatchById;
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
|
||||
const updateMatchStm = sql.prepare(/* sql */ `
|
||||
update "GroupMatch"
|
||||
set "reportedAt" = @reportedAt,
|
||||
"reportedByUserId" = @reportedByUserId
|
||||
where "id" = @matchId
|
||||
returning *
|
||||
`);
|
||||
|
||||
const clearMatchMapWinnersStm = sql.prepare(/* sql */ `
|
||||
update "GroupMatchMap"
|
||||
set "winnerGroupId" = null
|
||||
where "matchId" = @matchId
|
||||
`);
|
||||
|
||||
const updateMatchMapStm = sql.prepare(/* sql */ `
|
||||
update "GroupMatchMap"
|
||||
set "winnerGroupId" = @winnerGroupId
|
||||
where "matchId" = @matchId and "index" = @index
|
||||
`);
|
||||
|
||||
export const reportScore = ({
|
||||
reportedByUserId,
|
||||
winners,
|
||||
matchId,
|
||||
}: {
|
||||
reportedByUserId: number;
|
||||
winners: ("ALPHA" | "BRAVO")[];
|
||||
matchId: number;
|
||||
}) => {
|
||||
const updatedMatch = updateMatchStm.get({
|
||||
reportedAt: dateToDatabaseTimestamp(new Date()),
|
||||
reportedByUserId,
|
||||
matchId,
|
||||
}) as Tables["GroupMatch"];
|
||||
|
||||
clearMatchMapWinnersStm.run({ matchId });
|
||||
|
||||
for (const [index, winner] of winners.entries()) {
|
||||
updateMatchMapStm.run({
|
||||
winnerGroupId:
|
||||
winner === "ALPHA"
|
||||
? updatedMatch.alphaGroupId
|
||||
: updatedMatch.bravoGroupId,
|
||||
matchId,
|
||||
index,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
select
|
||||
"ReportedWeapon"."groupMatchMapId",
|
||||
"ReportedWeapon"."weaponSplId",
|
||||
"ReportedWeapon"."userId",
|
||||
"GroupMatchMap"."index" as "mapIndex"
|
||||
from
|
||||
"ReportedWeapon"
|
||||
left join "GroupMatchMap" on "GroupMatchMap"."id" = "ReportedWeapon"."groupMatchMapId"
|
||||
where "GroupMatchMap"."matchId" = @matchId
|
||||
`);
|
||||
|
||||
export function reportedWeaponsByMatchId(matchId: number) {
|
||||
const rows = stm.all({ matchId }) as Array<
|
||||
Tables["ReportedWeapon"] & {
|
||||
mapIndex: Tables["GroupMatchMap"]["index"];
|
||||
groupMatchMapId: number;
|
||||
}
|
||||
>;
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
|
||||
const groupToInactiveStm = sql.prepare(/* sql */ `
|
||||
update "Group"
|
||||
set "status" = 'INACTIVE'
|
||||
where "id" = @groupId
|
||||
`);
|
||||
|
||||
export function setGroupAsInactive(groupId: number) {
|
||||
groupToInactiveStm.run({ groupId });
|
||||
}
|
||||
|
|
@ -695,3 +695,11 @@ export function setPreparingGroupAsActive(groupId: number) {
|
|||
.where("status", "=", "PREPARING")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function setAsInactive(groupId: number, trx?: Transaction<DB>) {
|
||||
return (trx ?? db)
|
||||
.updateTable("Group")
|
||||
.set({ status: "INACTIVE" })
|
||||
.where("id", "=", groupId)
|
||||
.execute();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import { db } from "~/db/sql";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { addDummySkill } from "~/features/sendouq-match/queries/addDummySkill.server";
|
||||
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ async function main() {
|
|||
.execute();
|
||||
|
||||
for (const match of allMatches) {
|
||||
addDummySkill(match.id);
|
||||
await SQMatchRepository.lockMatchWithoutSkillChange(match.id);
|
||||
}
|
||||
|
||||
logger.info(`All done with nuking the season (${allMatches.length} matches)`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user