13 Commits
Author SHA1 Message Date
Tau 0d8d5845c9 schema: Fix M0011 for newly-created databases 2019-12-09 23:06:20 -05:00
Tau 143f534ba1 npm audit 2019-12-09 22:55:25 -05:00
Tau 4db3d94ee8 idz: Un-hardcode result limit in repo layer
Minor design nit but I should probably take my own code review
advice.
2019-11-30 14:13:49 -05:00
Tau e195cf77cd idz: Wire up TA topTen team results 2019-11-30 14:05:36 -05:00
Tau ac9622f1bd idz: Extend Time Attack topTen result set 2019-11-30 14:05:14 -05:00
Tau e8535b1a80 idz: Label additional team info in topTen response 2019-11-30 13:35:37 -05:00
07bea96aed831abce88a9591d3ff86af19babd79 d81b763d9b chuni: save and load user course result 2019-11-28 11:19:39 -05:00
Matt Bilker 1c5726be4c chunithm: Save and load user duel list 2019-11-28 07:04:33 +00:00
85be42b2ce67a8e34b32793360824b056c0c2444 6da5abcefc IDZ: Add support for player auras 2019-11-27 02:22:03 +00:00
07bea96aed831abce88a9591d3ff86af19babd79 c5e39f104f chuni: Add recent rating response to fix rating calculation 2019-11-27 01:49:05 +00:00
Tau 60d2f7cfd3 Fix working directory issues in startup batch file 2019-11-09 14:03:16 -05:00
Tau 0ac8b44ce0 idz/sql/teamMember.ts: Fix another ignored result set
That should cover all the leftover uses of forUpdate() ...
2019-11-09 14:00:24 -05:00
Tau 59fec74d08 Add Project DIVA workaround to startup script 2019-11-09 13:42:40 -05:00
45 changed files with 495 additions and 47 deletions
+6 -6
View File
@@ -2614,9 +2614,9 @@
"dev": true "dev": true
}, },
"handlebars": { "handlebars": {
"version": "4.5.1", "version": "4.5.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
"integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
"dev": true, "dev": true,
"requires": { "requires": {
"neo-async": "^2.6.0", "neo-async": "^2.6.0",
@@ -5376,9 +5376,9 @@
"dev": true "dev": true
}, },
"uglify-js": { "uglify-js": {
"version": "3.6.8", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.2.tgz",
"integrity": "sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ==", "integrity": "sha512-uhRwZcANNWVLrxLfNFEdltoPNhECUR3lc+UdJoG9CBpMcSnKyWA94tc3eAujB1GcMY5Uwq8ZMp4qWpxWYDQmaA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
+23
View File
@@ -82,6 +82,29 @@ create table "cm_user_activity" (
) )
); );
create table "cm_user_course" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"course_id" integer not null,
"class_id" integer not null,
"play_count" integer not null,
"score_max" integer not null,
"is_full_combo" text not null,
"is_all_justice" text not null,
"is_success" text not null,
"score_rank" integer not null,
"event_id" integer not null,
"last_play_date" text not null,
"param1" integer not null,
"param2" integer not null,
"param3" integer not null,
"param4" integer not null,
"is_clear" text not null,
constraint "cm_user_course_uq" unique ("profile_id", "course_id")
);
create table "cm_user_data_ex" ( create table "cm_user_data_ex" (
"id" integer primary key not null "id" integer primary key not null
references "cm_user_data"("id") references "cm_user_data"("id")
+2
View File
@@ -98,6 +98,7 @@ create table "idz_settings" (
on delete cascade, on delete cascade,
"music" integer not null, "music" integer not null,
"pack" integer not null, "pack" integer not null,
"aura" integer not null,
"paper_cup" integer not null, -- Not a boolean, oddly enough "paper_cup" integer not null, -- Not a boolean, oddly enough
"gauges" integer not null "gauges" integer not null
); );
@@ -192,6 +193,7 @@ create table "idz_unlocks" (
"id" integer primary key not null "id" integer primary key not null
references "idz_profile"("id") references "idz_profile"("id")
on delete cascade, on delete cascade,
"auras" integer not null,
"cup" integer not null, "cup" integer not null,
"gauges" integer not null, "gauges" integer not null,
"music" integer not null, "music" integer not null,
+5
View File
@@ -0,0 +1,5 @@
-- This adds the required columns to save player aura settings and unlocks
-- Default values are set according to what the game expects
alter table "idz_settings" add column "aura" integer NOT NULL DEFAULT '0';
alter table "idz_unlocks" add column "auras" integer NOT NULL DEFAULT '1';
@@ -0,0 +1,16 @@
create table "cm_user_duel_list" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"duel_id" integer not null,
"progress" integer not null,
"point" integer not null,
"is_clear" boolean not null,
"last_play_date" text not null,
"param1" integer not null,
"param2" integer not null,
"param3" integer not null,
"param4" integer not null,
constraint "cm_user_duel_list_uq" unique ("profile_id", "duel_id")
);
+22
View File
@@ -0,0 +1,22 @@
create table "cm_user_course" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"course_id" integer not null,
"class_id" integer not null,
"play_count" integer not null,
"score_max" integer not null,
"is_full_combo" text not null,
"is_all_justice" text not null,
"is_success" text not null,
"score_rank" integer not null,
"event_id" integer not null,
"last_play_date" text not null,
"param1" integer not null,
"param2" integer not null,
"param3" integer not null,
"param4" integer not null,
"is_clear" text not null,
constraint "cm_user_course_uq" unique ("profile_id", "course_id")
);
@@ -0,0 +1,23 @@
-- This was not present in new DBs initialized to schema version 11.
create table if not exists "cm_user_course" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"course_id" integer not null,
"class_id" integer not null,
"play_count" integer not null,
"score_max" integer not null,
"is_full_combo" text not null,
"is_all_justice" text not null,
"is_success" text not null,
"score_rank" integer not null,
"event_id" integer not null,
"last_play_date" text not null,
"param1" integer not null,
"param2" integer not null,
"param3" integer not null,
"param4" integer not null,
"is_clear" text not null,
constraint "cm_user_course_uq" unique ("profile_id", "course_id")
);
+16 -5
View File
@@ -1,17 +1,28 @@
import { Repositories } from "../repo"; import { Repositories } from "../repo";
import { GetUserCourseRequest } from "../request/getUserCourse"; import { GetUserCourseRequest } from "../request/getUserCourse";
import { GetUserCourseResponse } from "../response/getUserCourse"; import { GetUserCourseResponse } from "../response/getUserCourse";
import { readAimeId } from "../proto/base";
import { paginationCookie } from "./_util";
import { writeUserCourse } from "../proto/userCourse";
export default async function getUserCourse( export default async function getUserCourse(
rep: Repositories, rep: Repositories,
req: GetUserCourseRequest req: GetUserCourseRequest
): Promise<GetUserCourseResponse> { ): Promise<GetUserCourseResponse> {
const aimeId = readAimeId(req.userId);
const maxCount = parseInt(req.maxCount);
const nextIndex = parseInt(req.nextIndex);
const profileId = await rep.userData().lookup(aimeId);
const items = await rep
.userCourse()
.load(profileId, { limit: maxCount, offset: nextIndex });
return { return {
userId: req.userId, userId: req.userId,
length: "0", length: items.length.toString(),
nextIndex: "-1", nextIndex: paginationCookie(items, { maxCount, nextIndex }),
userCourseList: [ userCourseList: items.map(writeUserCourse)
// This will get saved at some point I think
],
}; };
} }
+11 -2
View File
@@ -1,14 +1,23 @@
import { Repositories } from "../repo"; import { Repositories } from "../repo";
import { GetUserDuelRequest } from "../request/getUserDuel"; import { GetUserDuelRequest } from "../request/getUserDuel";
import { GetUserDuelResponse } from "../response/getUserDuel"; import { GetUserDuelResponse } from "../response/getUserDuel";
import { readAimeId } from "../proto/base";
import { writeUserDuelList } from "../proto/userDuelList";
export default async function getUserDuel( export default async function getUserDuel(
rep: Repositories, rep: Repositories,
req: GetUserDuelRequest req: GetUserDuelRequest
): Promise<GetUserDuelResponse> { ): Promise<GetUserDuelResponse> {
const aimeId = readAimeId(req.userId);
// Get all duel entities if the client specifies to.
const duelId = req.isAllDuel ? undefined : parseInt(req.duelId);
const profileId = await rep.userData().lookup(aimeId);
const items = await rep.userDuelList().load(profileId, duelId);
return { return {
userId: req.userId, userId: req.userId,
length: "0", length: items.length.toString(),
userDuelList: [], userDuelList: items.map(writeUserDuelList),
}; };
} }
+9 -3
View File
@@ -1,16 +1,22 @@
import { Repositories } from "../repo"; import { Repositories } from "../repo";
import { GetUserRecentRatingRequest } from "../request/getUserRecentRating"; import { GetUserRecentRatingRequest } from "../request/getUserRecentRating";
import { GetUserRecentRatingResponse } from "../response/getUserRecentRating"; import { GetUserRecentRatingResponse } from "../response/getUserRecentRating";
import { readAimeId } from "../proto/base";
import { writeUserRecentRatingFromLog } from "../proto/userRecentRating";
export default async function getUserRecentRating( export default async function getUserRecentRating(
rep: Repositories, rep: Repositories,
req: GetUserRecentRatingRequest req: GetUserRecentRatingRequest
): Promise<GetUserRecentRatingResponse> { ): Promise<GetUserRecentRatingResponse> {
// I don't really know what subset of data to return here. const aimeId = readAimeId(req.userId);
const profileId = await rep.userData().lookup(aimeId);
// Return recent 30 plays to calculate rating
const items = await rep.userPlaylog().loadLatest(profileId, 30);
return { return {
userId: req.userId, userId: req.userId,
length: "0", length: items.length.toString(),
userRecentRatingList: [], userRecentRatingList: items.map(writeUserRecentRatingFromLog),
}; };
} }
+10
View File
@@ -11,7 +11,9 @@ import { readUserItem } from "../proto/userItem";
import { readUserMusicDetail } from "../proto/userMusic"; import { readUserMusicDetail } from "../proto/userMusic";
import { readUserActivity } from "../proto/userActivity"; import { readUserActivity } from "../proto/userActivity";
import { readUserDataEx } from "../proto/userDataEx"; import { readUserDataEx } from "../proto/userDataEx";
import { readUserDuelList } from "../proto/userDuelList";
import { readUserPlaylog } from "../proto/userPlaylog"; import { readUserPlaylog } from "../proto/userPlaylog";
import { readUserCourse } from "../proto/userCourse";
// It shouldn't need to be said really, but seeing as this message (A) requires // It shouldn't need to be said really, but seeing as this message (A) requires
// fairly lengthy processing and (B) is only sent right at the end of a credit, // fairly lengthy processing and (B) is only sent right at the end of a credit,
@@ -65,9 +67,17 @@ export default async function upsertUserAll(
await rep.userPlaylog().save(profileId, readUserPlaylog(item)); await rep.userPlaylog().save(profileId, readUserPlaylog(item));
} }
for (const item of payload.userCourseList || []) {
await rep.userCourse().save(profileId, readUserCourse(item));
}
for (const item of payload.userDataEx || []) { for (const item of payload.userDataEx || []) {
await rep.userDataEx().save(profileId, readUserDataEx(item)); await rep.userDataEx().save(profileId, readUserDataEx(item));
} }
for (const item of payload.userDuelList || []) {
await rep.userDuelList().save(profileId, readUserDuelList(item));
}
return { returnCode: "1" }; return { returnCode: "1" };
} }
+17
View File
@@ -0,0 +1,17 @@
export interface UserCourseItem {
courseId: number;
classId: number;
playCount: number;
scoreMax: number;
isFullCombo: boolean;
isAllJustice: boolean;
isSuccess: boolean;
scoreRank: number;
eventId: number;
lastPlayDate: Date;
param1: number;
param2: number;
param3: number;
param4: number;
isClear: boolean;
}
+11
View File
@@ -0,0 +1,11 @@
export interface UserDuelListItem {
duelId: number;
progress: number;
point: number;
isClear: boolean;
lastPlayDate: Date;
param1: number;
param2: number;
param3: number;
param4: number;
}
+28
View File
@@ -0,0 +1,28 @@
import { UserCourseItem } from "../model/userCourse";
import { Crush, readBoolean, readDate, writeObject } from "./base";
export type UserCourseJson = Crush<UserCourseItem>;
export function readUserCourse(json: UserCourseJson): UserCourseItem {
return {
classId: parseInt(json.classId),
courseId: parseInt(json.courseId),
eventId: parseInt(json.eventId),
isAllJustice: readBoolean(json.isAllJustice),
isClear: readBoolean(json.isClear),
isFullCombo: readBoolean(json.isFullCombo),
isSuccess: readBoolean(json.isSuccess),
lastPlayDate: readDate(json.lastPlayDate),
param1: parseInt(json.param1),
param2: parseInt(json.param2),
param3: parseInt(json.param3),
param4: parseInt(json.param4),
playCount: parseInt(json.playCount),
scoreMax: parseInt(json.scoreMax),
scoreRank: parseInt(json.scoreRank)
}
}
export function writeUserCourse(obj: UserCourseItem): UserCourseJson {
return writeObject(obj);
}
+26
View File
@@ -0,0 +1,26 @@
import { Crush, readBoolean, readDate, writeObject } from "./base";
import { UserDuelListItem } from "../model/userDuelList";
export type UserDuelListJson = Crush<UserDuelListItem>;
export function readUserDuelList(
json: UserDuelListJson
): UserDuelListItem {
return {
duelId: parseInt(json.duelId),
progress: parseInt(json.progress),
point: parseInt(json.point),
isClear: readBoolean(json.isClear),
lastPlayDate: readDate(json.lastPlayDate),
param1: parseInt(json.param1),
param2: parseInt(json.param2),
param3: parseInt(json.param3),
param4: parseInt(json.param4),
};
}
export function writeUserDuelList(
obj: UserDuelListItem
): UserDuelListJson {
return writeObject(obj);
}
+13
View File
@@ -1,3 +1,4 @@
import { UserPlaylogItem } from "./../model/userPlaylog";
import { Crush, writeObject } from "./base"; import { Crush, writeObject } from "./base";
import { UserRecentRatingItem } from "../model/userRecentRating"; import { UserRecentRatingItem } from "../model/userRecentRating";
@@ -19,3 +20,15 @@ export function writeUserRecentRating(
): UserRecentRatingJson { ): UserRecentRatingJson {
return writeObject(obj); 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(),
};
}
+7
View File
@@ -1,9 +1,12 @@
import { UserCourseRepository } from "./userCourse";
export { Page } from "./_defs"; export { Page } from "./_defs";
import { UserActivityRepository } from "./userActivity"; import { UserActivityRepository } from "./userActivity";
import { UserCharacterRepository } from "./userCharacter"; import { UserCharacterRepository } from "./userCharacter";
import { UserDataRepository } from "./userData"; import { UserDataRepository } from "./userData";
import { UserDataExRepository } from "./userDataEx"; import { UserDataExRepository } from "./userDataEx";
import { UserDuelListRepository } from "./userDuelList";
import { UserGameOptionRepository } from "./userGameOption"; import { UserGameOptionRepository } from "./userGameOption";
import { UserGameOptionExRepository } from "./userGameOptionEx"; import { UserGameOptionExRepository } from "./userGameOptionEx";
import { UserItemRepository } from "./userItem"; import { UserItemRepository } from "./userItem";
@@ -20,6 +23,8 @@ export interface Repositories {
userDataEx(): UserDataExRepository; userDataEx(): UserDataExRepository;
userDuelList(): UserDuelListRepository;
userGameOption(): UserGameOptionRepository; userGameOption(): UserGameOptionRepository;
userGameOptionEx(): UserGameOptionExRepository; userGameOptionEx(): UserGameOptionExRepository;
@@ -31,4 +36,6 @@ export interface Repositories {
userMusic(): UserMusicRepository; userMusic(): UserMusicRepository;
userPlaylog(): UserPlaylogRepository; userPlaylog(): UserPlaylogRepository;
userCourse(): UserCourseRepository;
} }
+4
View File
@@ -0,0 +1,4 @@
import { UserCourseItem } from "../model/userCourse";
import { RepositoryN } from "./_defs";
export type UserCourseRepository = RepositoryN<UserCourseItem>;
+13
View File
@@ -0,0 +1,13 @@
import { Page } from "./_defs";
import { UserDataItem } from "../model/userData";
import { UserDuelListItem } from "../model/userDuelList";
import { Id } from "../../model";
export interface UserDuelListRepository {
load(
profileId: Id<UserDataItem>,
duelId?: number,
): Promise<UserDuelListItem[]>;
save(profileId: Id<UserDataItem>, obj: UserDuelListItem): Promise<void>;
}
+1
View File
@@ -7,4 +7,5 @@ export interface UserPlaylogRepository {
// is nice to have. // is nice to have.
save(profileId: Id<UserDataItem>, obj: UserPlaylogItem): Promise<void>; save(profileId: Id<UserDataItem>, obj: UserPlaylogItem): Promise<void>;
loadLatest(profileId: Id<UserDataItem>, size: number): Promise<UserPlaylogItem[]>;
} }
+4
View File
@@ -8,7 +8,9 @@ import { UserMusicDetailJson } from "../proto/userMusic";
import { UserActivityJson } from "../proto/userActivity"; import { UserActivityJson } from "../proto/userActivity";
import { UserRecentRatingJson } from "../proto/userRecentRating"; import { UserRecentRatingJson } from "../proto/userRecentRating";
import { UserPlaylogJson } from "../proto/userPlaylog"; import { UserPlaylogJson } from "../proto/userPlaylog";
import { UserCourseJson } from "../proto/userCourse";
import { UserDataExJson } from "../proto/userDataEx"; import { UserDataExJson } from "../proto/userDataEx";
import { UserDuelListJson } from "../proto/userDuelList";
export interface UpsertUserAllRequest { export interface UpsertUserAllRequest {
/** Integer, AiMe ID */ /** Integer, AiMe ID */
@@ -25,7 +27,9 @@ export interface UpsertUserAllRequest {
userActivityList?: UserActivityJson[]; userActivityList?: UserActivityJson[];
userRecentRatingList?: UserRecentRatingJson[]; userRecentRatingList?: UserRecentRatingJson[];
userPlaylogList?: UserPlaylogJson[]; userPlaylogList?: UserPlaylogJson[];
userCourseList?: UserCourseJson[];
userDataEx?: UserDataExJson[]; userDataEx?: UserDataExJson[];
userDuelList?: UserDuelListJson[];
/** String of binary digits */ /** String of binary digits */
isNewMapList: string; isNewMapList: string;
+3 -2
View File
@@ -1,3 +1,5 @@
import { UserCourseJson } from "../proto/userCourse";
export interface GetUserCourseResponse { export interface GetUserCourseResponse {
/** Integer, AiMe ID */ /** Integer, AiMe ID */
userId: string; userId: string;
@@ -8,6 +10,5 @@ export interface GetUserCourseResponse {
/** Integer, pagination cookie */ /** Integer, pagination cookie */
nextIndex: string; nextIndex: string;
/** TBD */ userCourseList: UserCourseJson[];
userCourseList: [];
} }
+7 -2
View File
@@ -1,3 +1,5 @@
import { UserDuelListJson } from "../proto/userDuelList";
export interface GetUserDuelResponse { export interface GetUserDuelResponse {
/** Integer, AiMe ID */ /** Integer, AiMe ID */
userId: string; userId: string;
@@ -5,6 +7,9 @@ export interface GetUserDuelResponse {
/** Integer, number of results returned */ /** Integer, number of results returned */
length: string; length: string;
/** TBD */ /**
userDuelList: []; * List of duel entities, either all duel entities associated with `userId`
* or single requested by `duelId` in request.
*/
userDuelList: UserDuelListJson[];
} }
+12
View File
@@ -2,6 +2,7 @@ import { SqlUserActivityRepository } from "./userActivity";
import { SqlUserCharacterRepository } from "./userCharacter"; import { SqlUserCharacterRepository } from "./userCharacter";
import { SqlUserDataRepository } from "./userData"; import { SqlUserDataRepository } from "./userData";
import { SqlUserDataExRepository } from "./userDataEx"; import { SqlUserDataExRepository } from "./userDataEx";
import { SqlUserDuelListRepository } from "./userDuelList";
import { SqlUserGameOptionRepository } from "./userGameOption"; import { SqlUserGameOptionRepository } from "./userGameOption";
import { SqlUserGameOptionExRepository } from "./userGameOptionEx"; import { SqlUserGameOptionExRepository } from "./userGameOptionEx";
import { SqlUserItemRepository } from "./userItem"; import { SqlUserItemRepository } from "./userItem";
@@ -13,6 +14,7 @@ import { UserActivityRepository } from "../repo/userActivity";
import { UserCharacterRepository } from "../repo/userCharacter"; import { UserCharacterRepository } from "../repo/userCharacter";
import { UserDataRepository } from "../repo/userData"; import { UserDataRepository } from "../repo/userData";
import { UserDataExRepository } from "../repo/userDataEx"; import { UserDataExRepository } from "../repo/userDataEx";
import { UserDuelListRepository } from "../repo/userDuelList";
import { UserGameOptionRepository } from "../repo/userGameOption"; import { UserGameOptionRepository } from "../repo/userGameOption";
import { UserGameOptionExRepository } from "../repo/userGameOptionEx"; import { UserGameOptionExRepository } from "../repo/userGameOptionEx";
import { UserItemRepository } from "../repo/userItem"; import { UserItemRepository } from "../repo/userItem";
@@ -20,6 +22,8 @@ import { UserMapRepository } from "../repo/userMap";
import { UserMusicRepository } from "../repo/userMusic"; import { UserMusicRepository } from "../repo/userMusic";
import { UserPlaylogRepository } from "../repo/userPlaylog"; import { UserPlaylogRepository } from "../repo/userPlaylog";
import { Transaction } from "../../sql"; import { Transaction } from "../../sql";
import { UserCourseRepository } from "../repo/userCourse";
import { SqlUserCourseRepository } from "./userCourse";
export class SqlRepositories implements Repositories { export class SqlRepositories implements Repositories {
constructor(private readonly _txn: Transaction) {} constructor(private readonly _txn: Transaction) {}
@@ -40,6 +44,10 @@ export class SqlRepositories implements Repositories {
return new SqlUserDataExRepository(this._txn); return new SqlUserDataExRepository(this._txn);
} }
userDuelList(): UserDuelListRepository {
return new SqlUserDuelListRepository(this._txn);
}
userGameOption(): UserGameOptionRepository { userGameOption(): UserGameOptionRepository {
return new SqlUserGameOptionRepository(this._txn); return new SqlUserGameOptionRepository(this._txn);
} }
@@ -63,4 +71,8 @@ export class SqlRepositories implements Repositories {
userPlaylog(): UserPlaylogRepository { userPlaylog(): UserPlaylogRepository {
return new SqlUserPlaylogRepository(this._txn); return new SqlUserPlaylogRepository(this._txn);
} }
userCourse(): UserCourseRepository {
return new SqlUserCourseRepository(this._txn);
}
} }
+62
View File
@@ -0,0 +1,62 @@
import sql from "sql-bricks-postgres";
import { createSqlMapper, T } from "../../sql/util";
import { UserCourseRepository } from "../repo/userCourse";
import { Id } from "../../model";
import { Page } from "../repo";
import { UserDataItem } from "../model/userData";
import { UserCourseItem } from "../model/userCourse";
import { Transaction } from "../../sql";
const { readRow, writeRow, colNames } = createSqlMapper({
courseId: T.number,
classId: T.number,
playCount: T.number,
scoreMax: T.number,
isFullCombo: T.boolean,
isAllJustice: T.boolean,
isSuccess: T.boolean,
scoreRank: T.number,
eventId: T.number,
lastPlayDate: T.Date,
param1: T.number,
param2: T.number,
param3: T.number,
param4: T.number,
isClear: T.boolean
});
export class SqlUserCourseRepository implements UserCourseRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
page?: Page
): Promise<UserCourseItem[]> {
const stmt = sql
.select("*")
.from("cm_user_course")
.where("profile_id", profileId);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserCourseItem): Promise<void> {
const stmt = sql
.insert("cm_user_course", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "course_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+56
View File
@@ -0,0 +1,56 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserDuelListItem } from "../model/userDuelList";
import { Page } from "../repo";
import { UserDuelListRepository } from "../repo/userDuelList";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
duelId: T.number,
progress: T.number,
point: T.number,
isClear: T.boolean,
lastPlayDate: T.Date,
param1: T.number,
param2: T.number,
param3: T.number,
param4: T.number,
});
export class SqlUserDuelListRepository implements UserDuelListRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
duelId?: number
): Promise<UserDuelListItem[]> {
const stmt = sql
.select("*")
.from("cm_user_duel_list")
.where("profile_id", profileId);
if (duelId !== undefined) {
stmt.where("duel_id", duelId);
}
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserDuelListItem): Promise<void> {
const stmt = sql
.insert("cm_user_duel_list", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "duel_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+13 -1
View File
@@ -7,7 +7,7 @@ import { UserPlaylogRepository } from "../repo/userPlaylog";
import { Transaction } from "../../sql"; import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util"; import { T, createSqlMapper } from "../../sql/util";
const { writeRow } = createSqlMapper({ const { readRow, writeRow } = createSqlMapper({
orderId: T.number, orderId: T.number,
sortNumber: T.number, sortNumber: T.number,
placeId: T.number, placeId: T.number,
@@ -72,4 +72,16 @@ export class SqlUserPlaylogRepository implements UserPlaylogRepository {
return this._txn.modify(stmt); return this._txn.modify(stmt);
} }
async loadLatest(profileId: Id<UserDataItem>, size: number): Promise<UserPlaylogItem[]> {
const stmt = sql
.select("music_id", "score", "level","user_play_date")
.from("cm_user_playlog")
.where("profile_id", profileId)
.limit(size).order("user_play_date DESC");
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
} }
+2
View File
@@ -67,6 +67,7 @@ export function saveProfile2(buf: Buffer): SaveProfileRequest2 {
rows: storyRows, rows: storyRows,
}, },
unlocks: { unlocks: {
auras: buf.readUInt16LE(0x010c),
cup: buf.readUInt8(0x0110), cup: buf.readUInt8(0x0110),
gauges: buf.readUInt16LE(0x0114), gauges: buf.readUInt16LE(0x0114),
music: buf.readUInt16LE(0x0140), music: buf.readUInt16LE(0x0140),
@@ -90,6 +91,7 @@ export function saveProfile2(buf: Buffer): SaveProfileRequest2 {
settings: { settings: {
music: buf.readUInt16LE(0x045a), music: buf.readUInt16LE(0x045a),
pack: buf.readUInt32LE(0x0034), pack: buf.readUInt32LE(0x0034),
aura: buf.readUInt8(0x002c),
paperCup: buf.readUInt8(0x00f6), paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7), gauges: buf.readUInt8(0x00f7),
}, },
+2
View File
@@ -69,6 +69,7 @@ export function saveProfile3(buf: Buffer): SaveProfileRequest2 {
rows: storyRows, rows: storyRows,
}, },
unlocks: { unlocks: {
auras: buf.readUInt16LE(0x010c),
cup: buf.readUInt8(0x0110), cup: buf.readUInt8(0x0110),
gauges: buf.readUInt16LE(0x0114), gauges: buf.readUInt16LE(0x0114),
music: buf.readUInt16LE(0x0140), music: buf.readUInt16LE(0x0140),
@@ -92,6 +93,7 @@ export function saveProfile3(buf: Buffer): SaveProfileRequest2 {
settings: { settings: {
music: buf.readUInt16LE(0x04ee), music: buf.readUInt16LE(0x04ee),
pack: buf.readUInt32LE(0x0034), pack: buf.readUInt32LE(0x0034),
aura: buf.readUInt8(0x002c),
paperCup: buf.readUInt8(0x00f6), paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7), gauges: buf.readUInt8(0x00f7),
}, },
+1 -1
View File
@@ -16,8 +16,8 @@ export function saveSettings(buf: Buffer): SaveSettingsRequest {
pack: buf.readUInt32LE(0x000c), pack: buf.readUInt32LE(0x000c),
paperCup: buf.readUInt8(0x0011), paperCup: buf.readUInt8(0x0011),
gauges: buf.readUInt8(0x0012), gauges: buf.readUInt8(0x0012),
aura: buf.readUInt8(0x0013),
}, },
field_0010: buf.readUInt8(0x0010), field_0010: buf.readUInt8(0x0010),
field_0013: buf.readUInt8(0x0013),
}; };
} }
+2
View File
@@ -71,6 +71,7 @@ export function loadProfile2(res: LoadProfileResponse2) {
} }
buf.writeUInt16LE(0x0065, 0x0000); buf.writeUInt16LE(0x0065, 0x0000);
buf.writeUInt16LE(res.unlocks.auras, 0x00b0);
buf.writeUInt8(res.unlocks.cup, 0x00b4); buf.writeUInt8(res.unlocks.cup, 0x00b4);
buf.writeUInt16LE(res.unlocks.gauges, 0x00b8); buf.writeUInt16LE(res.unlocks.gauges, 0x00b8);
buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x01e8); buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x01e8);
@@ -92,6 +93,7 @@ export function loadProfile2(res: LoadProfileResponse2) {
encodeMission(res.missions.solo).copy(buf, 0x06e4); encodeMission(res.missions.solo).copy(buf, 0x06e4);
encodeChara(res.chara).copy(buf, 0x070c); encodeChara(res.chara).copy(buf, 0x070c);
encodeBitmap(res.titles, 0xb4).copy(buf, 0x720); encodeBitmap(res.titles, 0xb4).copy(buf, 0x720);
buf.writeUInt8(res.settings.aura, 0x07d6);
buf.writeUInt8(res.settings.paperCup, 0x07d9); buf.writeUInt8(res.settings.paperCup, 0x07d9);
buf.writeUInt8(res.settings.gauges, 0x07da); buf.writeUInt8(res.settings.gauges, 0x07da);
buf.writeUInt32LE(res.teamId || 0xffffffff, 0x07e0); buf.writeUInt32LE(res.teamId || 0xffffffff, 0x07e0);
+2
View File
@@ -71,6 +71,7 @@ export function loadProfile3(res: LoadProfileResponse3) {
} }
buf.writeUInt16LE(0x012e, 0x0000); buf.writeUInt16LE(0x012e, 0x0000);
buf.writeUInt16LE(res.unlocks.auras, 0x00b0);
buf.writeUInt8(res.unlocks.cup, 0x00b4); buf.writeUInt8(res.unlocks.cup, 0x00b4);
buf.writeUInt16LE(res.unlocks.gauges, 0x00b8); buf.writeUInt16LE(res.unlocks.gauges, 0x00b8);
buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x0218); buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x0218);
@@ -92,6 +93,7 @@ export function loadProfile3(res: LoadProfileResponse3) {
encodeMission(res.missions.solo).copy(buf, 0x0858); encodeMission(res.missions.solo).copy(buf, 0x0858);
encodeChara(res.chara).copy(buf, 0x0880); encodeChara(res.chara).copy(buf, 0x0880);
encodeBitmap(res.titles, 0xb4).copy(buf, 0x0894); encodeBitmap(res.titles, 0xb4).copy(buf, 0x0894);
buf.writeUInt8(res.settings.aura, 0x094a);
buf.writeUInt8(res.settings.paperCup, 0x094d); buf.writeUInt8(res.settings.paperCup, 0x094d);
buf.writeUInt8(res.settings.gauges, 0x094e); buf.writeUInt8(res.settings.gauges, 0x094e);
buf.writeUInt32LE(res.teamId || 0xffffffff, 0x0954); buf.writeUInt32LE(res.teamId || 0xffffffff, 0x0954);
+4 -4
View File
@@ -50,13 +50,13 @@ export function loadTopTen(res: LoadTopTenResponse): Buffer {
buf.writeUInt8(row.field_0F ? 1 : 0, innerOff + 0x000f); // Boolean buf.writeUInt8(row.field_0F ? 1 : 0, innerOff + 0x000f); // Boolean
buf.writeUInt8(row.field_10, innerOff + 0x0010); buf.writeUInt8(row.field_10, innerOff + 0x0010);
iconv.encode(row.driverName, "shift_jis").copy(buf, innerOff + 0x0014); iconv.encode(row.driverName, "shift_jis").copy(buf, innerOff + 0x0014);
iconv.encode(row.teamName, "shift_jis").copy(buf, innerOff + 0x0028); iconv.encode(row.team.name, "shift_jis").copy(buf, innerOff + 0x0028);
iconv.encode(row.shopName, "shift_jis").copy(buf, innerOff + 0x0048); iconv.encode(row.shopName, "shift_jis").copy(buf, innerOff + 0x0048);
buf.writeUInt32LE(row.field_74, innerOff + 0x0074); buf.writeUInt32LE(row.team.nameBg, innerOff + 0x0074);
buf.writeUInt16LE(row.field_78, innerOff + 0x0078); buf.writeUInt16LE(row.team.nameFx, innerOff + 0x0078);
buf.writeUInt8(row.field_7C, innerOff + 0x007c); buf.writeUInt8(row.field_7C, innerOff + 0x007c);
buf.writeUInt8(row.field_7D, innerOff + 0x007d); buf.writeUInt8(row.field_7D, innerOff + 0x007d);
// 66 bytes of fucking nothing what // 66 bytes of empty space at the end. Maybe a string used to live here?
} }
} }
+2 -1
View File
@@ -27,9 +27,10 @@ export async function createProfile(
}; };
const missions: MissionState = { team: [], solo: [] }; const missions: MissionState = { team: [], solo: [] };
const settings: Settings = { music: 0, pack: 13640, paperCup: 0, gauges: 5 }; const settings: Settings = { music: 0, pack: 13640, aura: 0, paperCup: 0, gauges: 5 };
const story: Story = { x: 0, y: 0, rows: [] }; const story: Story = { x: 0, y: 0, rows: [] };
const unlocks: Unlocks = { const unlocks: Unlocks = {
auras: 1,
cup: 0, cup: 0,
gauges: 1 << 5, gauges: 1 << 5,
music: 0, music: 0,
+2 -4
View File
@@ -18,7 +18,7 @@ export async function loadTopTen(
} }
const { routeNo, minTimestamp } = selector; const { routeNo, minTimestamp } = selector;
const src = await w.timeAttack().loadTopTen(routeNo, minTimestamp); const src = await w.timeAttack().loadTop(routeNo, minTimestamp, 10);
if (src.length === 0) { if (src.length === 0) {
continue; continue;
@@ -33,10 +33,8 @@ export async function loadTopTen(
field_0F: false, field_0F: false,
field_10: 0, field_10: 0,
driverName: srcItem.driverName, driverName: srcItem.driverName,
teamName: process.env.TEAM_NAME || "",
shopName: process.env.SHOP_NAME || "", shopName: process.env.SHOP_NAME || "",
field_74: 0, team: srcItem.team,
field_78: 0,
field_7C: 0, field_7C: 0,
field_7D: 0, field_7D: 0,
ta: srcItem.ta, ta: srcItem.ta,
+1
View File
@@ -1,6 +1,7 @@
export interface Settings { export interface Settings {
music: number; music: number;
pack: number; pack: number;
aura: number;
paperCup: number; paperCup: number;
gauges: number; gauges: number;
} }
+1
View File
@@ -1,4 +1,5 @@
export interface Unlocks { export interface Unlocks {
auras: number;
cup: number; cup: number;
gauges: number; gauges: number;
music: number; music: number;
+4 -2
View File
@@ -111,13 +111,15 @@ export interface TeamReservationRepository {
// TODO extend and factorize // TODO extend and factorize
export interface TopTenResult { export interface TopTenResult {
driverName: string; driverName: string;
team: Model.Team;
ta: Model.TimeAttackScore; ta: Model.TimeAttackScore;
} }
export interface TimeAttackRepository { export interface TimeAttackRepository {
loadTopTen( loadTop(
routeNo: Model.RouteNo, routeNo: Model.RouteNo,
minTimestamp: Date minTimestamp: Date,
limit: number
): Promise<TopTenResult[]>; ): Promise<TopTenResult[]>;
loadAll(profileId: Id<Model.Profile>): Promise<Model.TimeAttackScore[]>; loadAll(profileId: Id<Model.Profile>): Promise<Model.TimeAttackScore[]>;
-1
View File
@@ -7,5 +7,4 @@ export interface SaveSettingsRequest {
dpoint: number; // ?? why dpoint: number; // ?? why
settings: Settings; settings: Settings;
field_0010: number; field_0010: number;
field_0013: number;
} }
+5 -3
View File
@@ -7,10 +7,12 @@ export interface LoadTopTenResponseRow {
field_0F: boolean; field_0F: boolean;
field_10: number; field_10: number;
driverName: string; driverName: string;
teamName: string;
shopName: string; shopName: string;
field_74: number; team: {
field_78: number; name: string;
nameBg: number;
nameFx: number;
};
field_7C: number; field_7C: number;
field_7D: number; field_7D: number;
ta: TimeAttackScore; ta: TimeAttackScore;
+3 -1
View File
@@ -24,6 +24,7 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
return { return {
music: parseInt(row.music), music: parseInt(row.music),
pack: parseInt(row.pack), pack: parseInt(row.pack),
aura: parseInt(row.aura),
paperCup: parseInt(row.paper_cup), paperCup: parseInt(row.paper_cup),
gauges: parseInt(row.gauges), gauges: parseInt(row.gauges),
}; };
@@ -35,11 +36,12 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
id: profileId, id: profileId,
music: settings.music, music: settings.music,
pack: settings.pack, pack: settings.pack,
aura: settings.aura,
paper_cup: settings.paperCup, paper_cup: settings.paperCup,
gauges: settings.gauges, gauges: settings.gauges,
}) })
.onConflict("id") .onConflict("id")
.doUpdate(["music", "pack", "paper_cup", "gauges"]); .doUpdate(["music", "pack", "aura", "paper_cup", "gauges"]);
await this._txn.modify(saveSql); await this._txn.modify(saveSql);
} }
+1 -1
View File
@@ -74,7 +74,7 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
.where("id", teamId) .where("id", teamId)
.forUpdate(); .forUpdate();
await this._txn.modify(lockSql); await this._txn.fetchRow(lockSql);
// Double-check (with lock held) that there is room to join this team. // Double-check (with lock held) that there is room to join this team.
// If this fails then the error will propagate to the client and it will // If this fails then the error will propagate to the client and it will
+35 -6
View File
@@ -1,7 +1,8 @@
import sql from "sql-bricks-postgres"; import sql from "sql-bricks-postgres";
import { RouteNo } from "../model/base"; import { ExtId, RouteNo } from "../model/base";
import { CarSelector } from "../model/car"; import { CarSelector } from "../model/car";
import { Team } from "../model/team";
import { Profile } from "../model/profile"; import { Profile } from "../model/profile";
import { TimeAttackScore } from "../model/timeAttack"; import { TimeAttackScore } from "../model/timeAttack";
import { TimeAttackRepository, TopTenResult } from "../repo"; import { TimeAttackRepository, TopTenResult } from "../repo";
@@ -23,23 +24,51 @@ function _extractRow(row: Row): TimeAttackScore {
export class SqlTimeAttackRepository implements TimeAttackRepository { export class SqlTimeAttackRepository implements TimeAttackRepository {
constructor(private readonly _txn: Transaction) {} constructor(private readonly _txn: Transaction) {}
async loadTopTen( async loadTop(
routeNo: RouteNo, routeNo: RouteNo,
minTimestamp: Date minTimestamp: Date,
limit: number
): Promise<TopTenResult[]> { ): Promise<TopTenResult[]> {
// We're not using an ORM here so this join-heavy SQL is unfortunately
// going to be a boilerplated mess.
const loadSql = sql const loadSql = sql
.select("p.name", "ta.*") .select(
// Profile
"p.name as profile_name",
// Team
"t.ext_id as team_ext_id",
"t.name as team_name",
"t.name_bg as team_name_bg",
"t.name_fx as team_name_fx",
"t.register_time as team_register_time",
// Time Attack
"ta.*"
)
.from("idz_ta_best ta") .from("idz_ta_best ta")
.join("idz_profile p", { "ta.profile_id": "p.id" }) .join("idz_profile p", { "ta.profile_id": "p.id" })
// This is an inner join, so it will exclude anybody who is not currently
// a member of a team from the leader boards. Since the only way to play
// without a team is to be ejected from one we can consider this an edge
// case that we probably don't need to worry about.
.join("idz_team_member tm", { "ta.profile_id": "tm.id" })
.join("idz_team t", { "tm.team_id": "t.id" })
.where("ta.route_no", routeNo) .where("ta.route_no", routeNo)
.where(sql.gt("ta.timestamp", minTimestamp)) .where(sql.gt("ta.timestamp", minTimestamp))
.orderBy(["ta.total_time asc", "ta.timestamp asc"]) .orderBy(["ta.total_time asc", "ta.timestamp asc"])
.limit(10); .limit(limit);
const rows = await this._txn.fetchRows(loadSql); const rows = await this._txn.fetchRows(loadSql);
return rows.map(row => ({ return rows.map(row => ({
driverName: row.name, driverName: row.profile_name,
team: {
extId: parseInt(row.team_ext_id) as ExtId<Team>,
name: row.team_name,
nameBg: parseInt(row.team_name_bg),
nameFx: parseInt(row.team_name_fx),
registerTime: new Date(row.team_register_time),
},
ta: _extractRow(row), ta: _extractRow(row),
})); }));
} }
+3 -1
View File
@@ -22,6 +22,7 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
} }
return { return {
auras: parseInt(row.auras),
cup: parseInt(row.cup), cup: parseInt(row.cup),
gauges: parseInt(row.gauges), gauges: parseInt(row.gauges),
music: parseInt(row.music), music: parseInt(row.music),
@@ -33,13 +34,14 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
const saveSql = sql const saveSql = sql
.insert("idz_unlocks", { .insert("idz_unlocks", {
id: profileId, id: profileId,
auras: unlocks.auras,
cup: unlocks.cup, cup: unlocks.cup,
gauges: unlocks.gauges, gauges: unlocks.gauges,
music: unlocks.music, music: unlocks.music,
last_mileage_reward: unlocks.lastMileageReward, last_mileage_reward: unlocks.lastMileageReward,
}) })
.onConflict("id") .onConflict("id")
.doUpdate(["cup", "gauges", "music", "last_mileage_reward"]); .doUpdate(["auras", "cup", "gauges", "music", "last_mileage_reward"]);
await this._txn.modify(saveSql); await this._txn.modify(saveSql);
} }
+5 -1
View File
@@ -1,5 +1,7 @@
@echo off @echo off
pushd %~dp0
where /q node where /q node
if errorlevel 1 ( if errorlevel 1 (
echo NodeJS does not appear to be installed. echo NodeJS does not appear to be installed.
@@ -40,8 +42,10 @@ if not exist node_modules (
echo. echo.
) )
rem Project Diva requires TLSv1.0
set DEBUG=app:* set DEBUG=app:*
node .\bin\index.js node --tls-min-v1.0 .\bin\index.js
:end :end
echo. echo.