diff --git a/schema/init/cm.sql b/schema/init/cm.sql index 815ca88..a0b86ac 100644 --- a/schema/init/cm.sql +++ b/schema/init/cm.sql @@ -357,3 +357,16 @@ create table "cm_user_playlog" ( "place_name" text not null, "is_maimai" text not null ); + +create table "cm_user_recent_rating" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "sort_order" integer not null, + "music_id" integer not null, + "difficult_id" integer not null, + "rom_version_code" integer not null, + "score" integer not null, + constraint "cm_user_recent_rating_uq" unique ("profile_id", "sort_order") +); diff --git a/schema/migrate/M0015-cm-user-recent-rating.sql b/schema/migrate/M0015-cm-user-recent-rating.sql new file mode 100644 index 0000000..351419f --- /dev/null +++ b/schema/migrate/M0015-cm-user-recent-rating.sql @@ -0,0 +1,31 @@ +create table if not exists "cm_user_recent_rating" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "sort_order" integer not null, + "music_id" integer not null, + "difficult_id" integer not null, + "rom_version_code" integer not null, + "score" integer not null, + constraint "cm_user_recent_rating_uq" unique ("profile_id", "sort_order") +); + +-- Prepopulate this table by backfilling the most recent 30 scores per user. +-- This isn't exactly correct since not every recent score should go in here, but it's probably close enough. +INSERT INTO cm_user_recent_rating (profile_id, sort_order, music_id, difficult_id, rom_version_code, score) +SELECT * FROM ( + SELECT + profile_id, + row_number() + OVER (PARTITION BY profile_id ORDER BY user_play_date DESC) AS sort_order, + music_id, + level AS difficult_id, + '1030000' AS rom_version_code, + score + FROM cm_user_playlog + WHERE + difficult_id < 4 -- skip world's end + ORDER BY profile_id ASC, user_play_date DESC +) +WHERE sort_order <= 30; diff --git a/src/chunithm/handler/getUserRecentRating.ts b/src/chunithm/handler/getUserRecentRating.ts index e2a3d86..1a5d020 100644 --- a/src/chunithm/handler/getUserRecentRating.ts +++ b/src/chunithm/handler/getUserRecentRating.ts @@ -2,7 +2,7 @@ import { Repositories } from "../repo"; import { GetUserRecentRatingRequest } from "../request/getUserRecentRating"; import { GetUserRecentRatingResponse } from "../response/getUserRecentRating"; import { readAimeId } from "../proto/base"; -import { writeUserRecentRatingFromLog } from "../proto/userRecentRating"; +import { writeUserRecentRating } from "../proto/userRecentRating"; export default async function getUserRecentRating( rep: Repositories, @@ -12,11 +12,11 @@ export default async function getUserRecentRating( const profileId = await rep.userData().lookup(aimeId); // Return recent 30 plays to calculate rating - const items = await rep.userPlaylog().loadLatest(profileId, 30); + const items = await rep.userRecentRating().load(profileId); return { userId: req.userId, length: items.length.toString(), - userRecentRatingList: items.map(writeUserRecentRatingFromLog), + userRecentRatingList: items.map(writeUserRecentRating), }; } diff --git a/src/chunithm/handler/upsertUserAll.ts b/src/chunithm/handler/upsertUserAll.ts index 2779d83..16c8981 100644 --- a/src/chunithm/handler/upsertUserAll.ts +++ b/src/chunithm/handler/upsertUserAll.ts @@ -14,6 +14,7 @@ import { readUserDataEx } from "../proto/userDataEx"; import { readUserDuelList } from "../proto/userDuelList"; import { readUserPlaylog } from "../proto/userPlaylog"; import { readUserCharge } from "../proto/userCharge"; +import { readUserRecentRating } from "../proto/userRecentRating"; import { readUserCourse } from "../proto/userCourse"; // It shouldn't need to be said really, but seeing as this message (A) requires @@ -84,5 +85,12 @@ export default async function upsertUserAll( await rep.userDuelList().save(profileId, readUserDuelList(item)); } + await rep + .userRecentRating() + .save( + profileId, + (payload.userRecentRatingList || []).map(readUserRecentRating) + ); + return { returnCode: "1" }; } diff --git a/src/chunithm/proto/userRecentRating.ts b/src/chunithm/proto/userRecentRating.ts index e55303f..dd4526a 100644 --- a/src/chunithm/proto/userRecentRating.ts +++ b/src/chunithm/proto/userRecentRating.ts @@ -20,15 +20,3 @@ export function writeUserRecentRating( ): UserRecentRatingJson { return writeObject(obj); } - -export function writeUserRecentRatingFromLog( - obj: UserPlaylogItem -): UserRecentRatingJson { - return { - musicId: obj.musicId.toString(), - difficultId: obj.level.toString(), - // game version not saved in play log, just return a fixed version now - romVersionCode: "1030000", - score: obj.score.toString(), - }; -} diff --git a/src/chunithm/repo/index.ts b/src/chunithm/repo/index.ts index 3d06e36..1eb84ee 100644 --- a/src/chunithm/repo/index.ts +++ b/src/chunithm/repo/index.ts @@ -2,6 +2,7 @@ export { Page } from "./_defs"; import { UserActivityRepository } from "./userActivity"; import { UserCharacterRepository } from "./userCharacter"; +import { UserCourseRepository } from "./userCourse"; import { UserDataRepository } from "./userData"; import { UserDataExRepository } from "./userDataEx"; import { UserDuelListRepository } from "./userDuelList"; @@ -12,13 +13,15 @@ import { UserMapRepository } from "./userMap"; import { UserMusicRepository } from "./userMusic"; import { UserPlaylogRepository } from "./userPlaylog"; import { UserChargeRepository } from "./userCharge"; -import { UserCourseRepository } from "./userCourse"; +import { UserRecentRatingRepository } from "./userRecentRating"; export interface Repositories { userActivity(): UserActivityRepository; userCharacter(): UserCharacterRepository; + userCourse(): UserCourseRepository; + userData(): UserDataRepository; userDataEx(): UserDataExRepository; @@ -40,4 +43,6 @@ export interface Repositories { userCharge(): UserChargeRepository; userCourse(): UserCourseRepository; + + userRecentRating(): UserRecentRatingRepository; } diff --git a/src/chunithm/repo/userRecentRating.ts b/src/chunithm/repo/userRecentRating.ts new file mode 100644 index 0000000..7faad1a --- /dev/null +++ b/src/chunithm/repo/userRecentRating.ts @@ -0,0 +1,12 @@ +import { UserDataItem } from "../model/userData"; +import { UserRecentRatingItem } from "../model/userRecentRating"; +import { Id } from "../../model"; + +export interface UserRecentRatingRepository { + load(profileId: Id): Promise; + + save( + profileId: Id, + objs: UserRecentRatingItem[] + ): Promise; +} diff --git a/src/chunithm/sql/_factory.ts b/src/chunithm/sql/_factory.ts index cacb15e..9557029 100644 --- a/src/chunithm/sql/_factory.ts +++ b/src/chunithm/sql/_factory.ts @@ -11,6 +11,7 @@ import { SqlUserItemRepository } from "./userItem"; import { SqlUserMapRepository } from "./userMap"; import { SqlUserMusicRepository } from "./userMusic"; import { SqlUserPlaylogRepository } from "./userPlaylog"; +import { SqlUserRecentRatingRepository } from "./userRecentRating"; import { Repositories } from "../repo"; import { UserActivityRepository } from "../repo/userActivity"; import { UserCharacterRepository } from "../repo/userCharacter"; @@ -25,6 +26,7 @@ import { UserItemRepository } from "../repo/userItem"; import { UserMapRepository } from "../repo/userMap"; import { UserMusicRepository } from "../repo/userMusic"; import { UserPlaylogRepository } from "../repo/userPlaylog"; +import { UserRecentRatingRepository } from "../repo/userRecentRating"; import { Transaction } from "../../sql"; export class SqlRepositories implements Repositories { @@ -81,4 +83,8 @@ export class SqlRepositories implements Repositories { userPlaylog(): UserPlaylogRepository { return new SqlUserPlaylogRepository(this._txn); } + + userRecentRating(): UserRecentRatingRepository { + return new SqlUserRecentRatingRepository(this._txn); + } } diff --git a/src/chunithm/sql/userRecentRating.ts b/src/chunithm/sql/userRecentRating.ts new file mode 100644 index 0000000..156fb74 --- /dev/null +++ b/src/chunithm/sql/userRecentRating.ts @@ -0,0 +1,54 @@ +import sql from "sql-bricks-postgres"; + +import { Id } from "../../model"; +import { UserDataItem } from "../model/userData"; +import { UserRecentRatingItem } from "../model/userRecentRating"; +import { UserRecentRatingRepository } from "../repo/userRecentRating"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + musicId: T.number, + difficultId: T.number, + romVersionCode: T.number, + score: T.number, +}); + +export class SqlUserRecentRatingRepository + implements UserRecentRatingRepository { + constructor(private readonly _txn: Transaction) {} + + async load(profileId: Id): Promise { + const stmt = sql + .select("music_id", "difficult_id", "rom_version_code", "score") + .from("cm_user_recent_rating") + .where("profile_id", profileId) + .order("sort_order ASC"); + + const rows = await this._txn.fetchRows(stmt); + + return rows.map(readRow); + } + + save( + profileId: Id, + objs: UserRecentRatingItem[] + ): Promise { + const stmt = sql + .insert( + "cm_user_recent_rating", + objs.map((obj, idx) => { + return { + id: this._txn.generateId(), + profile_id: profileId, + sort_order: idx + 1, + ...writeRow(obj), + }; + }) + ) + .onConflict("profile_id", "sort_order") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +}