16 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
Tau 52f142a2b9 Bump deps so npm audit will shut up 2019-11-08 19:46:19 -05:00
Tau 3a7132d276 Drop ts-node
Stopped using it a while ago, `tsc --watch` is more convenient.
2019-11-08 19:43:59 -05:00
Tau 4f89894fd1 chunithm: Raise POST payload size limit 2019-11-08 19:39:03 -05:00
47 changed files with 500 additions and 84 deletions
+10 -41
View File
@@ -666,12 +666,6 @@
"normalize-path": "^2.1.1"
}
},
"arg": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz",
"integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==",
"dev": true
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -1196,9 +1190,9 @@
}
},
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"optional": true
},
@@ -1481,12 +1475,6 @@
"integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
"dev": true
},
"diff": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
"integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==",
"dev": true
},
"diff-sequences": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz",
@@ -2626,9 +2614,9 @@
"dev": true
},
"handlebars": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz",
"integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@@ -5348,19 +5336,6 @@
}
}
},
"ts-node": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz",
"integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==",
"dev": true,
"requires": {
"arg": "^4.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.6",
"yn": "^3.0.0"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -5401,13 +5376,13 @@
"dev": true
},
"uglify-js": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz",
"integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==",
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.2.tgz",
"integrity": "sha512-uhRwZcANNWVLrxLfNFEdltoPNhECUR3lc+UdJoG9CBpMcSnKyWA94tc3eAujB1GcMY5Uwq8ZMp4qWpxWYDQmaA==",
"dev": true,
"optional": true,
"requires": {
"commander": "2.20.0",
"commander": "~2.20.3",
"source-map": "~0.6.1"
}
},
@@ -5707,12 +5682,6 @@
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}
-1
View File
@@ -33,7 +33,6 @@
"jest-resolve": "^24.5.0",
"prettier": "^1.16.4",
"ts-jest": "^24.0.0",
"ts-node": "^8.0.3",
"typescript": "^3.6.4",
"utility-types": "^3.6.1"
},
+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" (
"id" integer primary key not null
references "cm_user_data"("id")
+2
View File
@@ -98,6 +98,7 @@ create table "idz_settings" (
on delete cascade,
"music" integer not null,
"pack" integer not null,
"aura" integer not null,
"paper_cup" integer not null, -- Not a boolean, oddly enough
"gauges" integer not null
);
@@ -192,6 +193,7 @@ create table "idz_unlocks" (
"id" integer primary key not null
references "idz_profile"("id")
on delete cascade,
"auras" integer not null,
"cup" integer not null,
"gauges" 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 { GetUserCourseRequest } from "../request/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(
rep: Repositories,
req: GetUserCourseRequest
): 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 {
userId: req.userId,
length: "0",
nextIndex: "-1",
userCourseList: [
// This will get saved at some point I think
],
length: items.length.toString(),
nextIndex: paginationCookie(items, { maxCount, nextIndex }),
userCourseList: items.map(writeUserCourse)
};
}
+11 -2
View File
@@ -1,14 +1,23 @@
import { Repositories } from "../repo";
import { GetUserDuelRequest } from "../request/getUserDuel";
import { GetUserDuelResponse } from "../response/getUserDuel";
import { readAimeId } from "../proto/base";
import { writeUserDuelList } from "../proto/userDuelList";
export default async function getUserDuel(
rep: Repositories,
req: GetUserDuelRequest
): 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 {
userId: req.userId,
length: "0",
userDuelList: [],
length: items.length.toString(),
userDuelList: items.map(writeUserDuelList),
};
}
+9 -3
View File
@@ -1,16 +1,22 @@
import { Repositories } from "../repo";
import { GetUserRecentRatingRequest } from "../request/getUserRecentRating";
import { GetUserRecentRatingResponse } from "../response/getUserRecentRating";
import { readAimeId } from "../proto/base";
import { writeUserRecentRatingFromLog } from "../proto/userRecentRating";
export default async function getUserRecentRating(
rep: Repositories,
req: GetUserRecentRatingRequest
): 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 {
userId: req.userId,
length: "0",
userRecentRatingList: [],
length: items.length.toString(),
userRecentRatingList: items.map(writeUserRecentRatingFromLog),
};
}
+1 -1
View File
@@ -117,7 +117,7 @@ export default function chunithm(db: DataSource) {
app.use(quirks);
app.use(compression({ threshold: 0 }));
app.use(express.json());
app.use(express.json({ limit: "50mb" })); // that ought to be enough
app.use(trace);
app.use("/ChuniServlet", wrapper);
+10
View File
@@ -11,7 +11,9 @@ import { readUserItem } from "../proto/userItem";
import { readUserMusicDetail } from "../proto/userMusic";
import { readUserActivity } from "../proto/userActivity";
import { readUserDataEx } from "../proto/userDataEx";
import { readUserDuelList } from "../proto/userDuelList";
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
// 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));
}
for (const item of payload.userCourseList || []) {
await rep.userCourse().save(profileId, readUserCourse(item));
}
for (const item of payload.userDataEx || []) {
await rep.userDataEx().save(profileId, readUserDataEx(item));
}
for (const item of payload.userDuelList || []) {
await rep.userDuelList().save(profileId, readUserDuelList(item));
}
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 { UserRecentRatingItem } from "../model/userRecentRating";
@@ -19,3 +20,15 @@ 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(),
};
}
+7
View File
@@ -1,9 +1,12 @@
import { UserCourseRepository } from "./userCourse";
export { Page } from "./_defs";
import { UserActivityRepository } from "./userActivity";
import { UserCharacterRepository } from "./userCharacter";
import { UserDataRepository } from "./userData";
import { UserDataExRepository } from "./userDataEx";
import { UserDuelListRepository } from "./userDuelList";
import { UserGameOptionRepository } from "./userGameOption";
import { UserGameOptionExRepository } from "./userGameOptionEx";
import { UserItemRepository } from "./userItem";
@@ -20,6 +23,8 @@ export interface Repositories {
userDataEx(): UserDataExRepository;
userDuelList(): UserDuelListRepository;
userGameOption(): UserGameOptionRepository;
userGameOptionEx(): UserGameOptionExRepository;
@@ -31,4 +36,6 @@ export interface Repositories {
userMusic(): UserMusicRepository;
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.
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 { UserRecentRatingJson } from "../proto/userRecentRating";
import { UserPlaylogJson } from "../proto/userPlaylog";
import { UserCourseJson } from "../proto/userCourse";
import { UserDataExJson } from "../proto/userDataEx";
import { UserDuelListJson } from "../proto/userDuelList";
export interface UpsertUserAllRequest {
/** Integer, AiMe ID */
@@ -25,7 +27,9 @@ export interface UpsertUserAllRequest {
userActivityList?: UserActivityJson[];
userRecentRatingList?: UserRecentRatingJson[];
userPlaylogList?: UserPlaylogJson[];
userCourseList?: UserCourseJson[];
userDataEx?: UserDataExJson[];
userDuelList?: UserDuelListJson[];
/** String of binary digits */
isNewMapList: string;
+3 -2
View File
@@ -1,3 +1,5 @@
import { UserCourseJson } from "../proto/userCourse";
export interface GetUserCourseResponse {
/** Integer, AiMe ID */
userId: string;
@@ -8,6 +10,5 @@ export interface GetUserCourseResponse {
/** Integer, pagination cookie */
nextIndex: string;
/** TBD */
userCourseList: [];
userCourseList: UserCourseJson[];
}
+7 -2
View File
@@ -1,3 +1,5 @@
import { UserDuelListJson } from "../proto/userDuelList";
export interface GetUserDuelResponse {
/** Integer, AiMe ID */
userId: string;
@@ -5,6 +7,9 @@ export interface GetUserDuelResponse {
/** Integer, number of results returned */
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 { SqlUserDataRepository } from "./userData";
import { SqlUserDataExRepository } from "./userDataEx";
import { SqlUserDuelListRepository } from "./userDuelList";
import { SqlUserGameOptionRepository } from "./userGameOption";
import { SqlUserGameOptionExRepository } from "./userGameOptionEx";
import { SqlUserItemRepository } from "./userItem";
@@ -13,6 +14,7 @@ import { UserActivityRepository } from "../repo/userActivity";
import { UserCharacterRepository } from "../repo/userCharacter";
import { UserDataRepository } from "../repo/userData";
import { UserDataExRepository } from "../repo/userDataEx";
import { UserDuelListRepository } from "../repo/userDuelList";
import { UserGameOptionRepository } from "../repo/userGameOption";
import { UserGameOptionExRepository } from "../repo/userGameOptionEx";
import { UserItemRepository } from "../repo/userItem";
@@ -20,6 +22,8 @@ import { UserMapRepository } from "../repo/userMap";
import { UserMusicRepository } from "../repo/userMusic";
import { UserPlaylogRepository } from "../repo/userPlaylog";
import { Transaction } from "../../sql";
import { UserCourseRepository } from "../repo/userCourse";
import { SqlUserCourseRepository } from "./userCourse";
export class SqlRepositories implements Repositories {
constructor(private readonly _txn: Transaction) {}
@@ -40,6 +44,10 @@ export class SqlRepositories implements Repositories {
return new SqlUserDataExRepository(this._txn);
}
userDuelList(): UserDuelListRepository {
return new SqlUserDuelListRepository(this._txn);
}
userGameOption(): UserGameOptionRepository {
return new SqlUserGameOptionRepository(this._txn);
}
@@ -63,4 +71,8 @@ export class SqlRepositories implements Repositories {
userPlaylog(): UserPlaylogRepository {
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 { T, createSqlMapper } from "../../sql/util";
const { writeRow } = createSqlMapper({
const { readRow, writeRow } = createSqlMapper({
orderId: T.number,
sortNumber: T.number,
placeId: T.number,
@@ -72,4 +72,16 @@ export class SqlUserPlaylogRepository implements UserPlaylogRepository {
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,
},
unlocks: {
auras: buf.readUInt16LE(0x010c),
cup: buf.readUInt8(0x0110),
gauges: buf.readUInt16LE(0x0114),
music: buf.readUInt16LE(0x0140),
@@ -90,6 +91,7 @@ export function saveProfile2(buf: Buffer): SaveProfileRequest2 {
settings: {
music: buf.readUInt16LE(0x045a),
pack: buf.readUInt32LE(0x0034),
aura: buf.readUInt8(0x002c),
paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7),
},
+2
View File
@@ -69,6 +69,7 @@ export function saveProfile3(buf: Buffer): SaveProfileRequest2 {
rows: storyRows,
},
unlocks: {
auras: buf.readUInt16LE(0x010c),
cup: buf.readUInt8(0x0110),
gauges: buf.readUInt16LE(0x0114),
music: buf.readUInt16LE(0x0140),
@@ -92,6 +93,7 @@ export function saveProfile3(buf: Buffer): SaveProfileRequest2 {
settings: {
music: buf.readUInt16LE(0x04ee),
pack: buf.readUInt32LE(0x0034),
aura: buf.readUInt8(0x002c),
paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7),
},
+1 -1
View File
@@ -16,8 +16,8 @@ export function saveSettings(buf: Buffer): SaveSettingsRequest {
pack: buf.readUInt32LE(0x000c),
paperCup: buf.readUInt8(0x0011),
gauges: buf.readUInt8(0x0012),
aura: buf.readUInt8(0x0013),
},
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(res.unlocks.auras, 0x00b0);
buf.writeUInt8(res.unlocks.cup, 0x00b4);
buf.writeUInt16LE(res.unlocks.gauges, 0x00b8);
buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x01e8);
@@ -92,6 +93,7 @@ export function loadProfile2(res: LoadProfileResponse2) {
encodeMission(res.missions.solo).copy(buf, 0x06e4);
encodeChara(res.chara).copy(buf, 0x070c);
encodeBitmap(res.titles, 0xb4).copy(buf, 0x720);
buf.writeUInt8(res.settings.aura, 0x07d6);
buf.writeUInt8(res.settings.paperCup, 0x07d9);
buf.writeUInt8(res.settings.gauges, 0x07da);
buf.writeUInt32LE(res.teamId || 0xffffffff, 0x07e0);
+2
View File
@@ -71,6 +71,7 @@ export function loadProfile3(res: LoadProfileResponse3) {
}
buf.writeUInt16LE(0x012e, 0x0000);
buf.writeUInt16LE(res.unlocks.auras, 0x00b0);
buf.writeUInt8(res.unlocks.cup, 0x00b4);
buf.writeUInt16LE(res.unlocks.gauges, 0x00b8);
buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x0218);
@@ -92,6 +93,7 @@ export function loadProfile3(res: LoadProfileResponse3) {
encodeMission(res.missions.solo).copy(buf, 0x0858);
encodeChara(res.chara).copy(buf, 0x0880);
encodeBitmap(res.titles, 0xb4).copy(buf, 0x0894);
buf.writeUInt8(res.settings.aura, 0x094a);
buf.writeUInt8(res.settings.paperCup, 0x094d);
buf.writeUInt8(res.settings.gauges, 0x094e);
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_10, innerOff + 0x0010);
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);
buf.writeUInt32LE(row.field_74, innerOff + 0x0074);
buf.writeUInt16LE(row.field_78, innerOff + 0x0078);
buf.writeUInt32LE(row.team.nameBg, innerOff + 0x0074);
buf.writeUInt16LE(row.team.nameFx, innerOff + 0x0078);
buf.writeUInt8(row.field_7C, innerOff + 0x007c);
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 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 unlocks: Unlocks = {
auras: 1,
cup: 0,
gauges: 1 << 5,
music: 0,
+2 -4
View File
@@ -18,7 +18,7 @@ export async function loadTopTen(
}
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) {
continue;
@@ -33,10 +33,8 @@ export async function loadTopTen(
field_0F: false,
field_10: 0,
driverName: srcItem.driverName,
teamName: process.env.TEAM_NAME || "",
shopName: process.env.SHOP_NAME || "",
field_74: 0,
field_78: 0,
team: srcItem.team,
field_7C: 0,
field_7D: 0,
ta: srcItem.ta,
+1
View File
@@ -1,6 +1,7 @@
export interface Settings {
music: number;
pack: number;
aura: number;
paperCup: number;
gauges: number;
}
+1
View File
@@ -1,4 +1,5 @@
export interface Unlocks {
auras: number;
cup: number;
gauges: number;
music: number;
+4 -2
View File
@@ -111,13 +111,15 @@ export interface TeamReservationRepository {
// TODO extend and factorize
export interface TopTenResult {
driverName: string;
team: Model.Team;
ta: Model.TimeAttackScore;
}
export interface TimeAttackRepository {
loadTopTen(
loadTop(
routeNo: Model.RouteNo,
minTimestamp: Date
minTimestamp: Date,
limit: number
): Promise<TopTenResult[]>;
loadAll(profileId: Id<Model.Profile>): Promise<Model.TimeAttackScore[]>;
-1
View File
@@ -7,5 +7,4 @@ export interface SaveSettingsRequest {
dpoint: number; // ?? why
settings: Settings;
field_0010: number;
field_0013: number;
}
+5 -3
View File
@@ -7,10 +7,12 @@ export interface LoadTopTenResponseRow {
field_0F: boolean;
field_10: number;
driverName: string;
teamName: string;
shopName: string;
field_74: number;
field_78: number;
team: {
name: string;
nameBg: number;
nameFx: number;
};
field_7C: number;
field_7D: number;
ta: TimeAttackScore;
+3 -1
View File
@@ -24,6 +24,7 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
return {
music: parseInt(row.music),
pack: parseInt(row.pack),
aura: parseInt(row.aura),
paperCup: parseInt(row.paper_cup),
gauges: parseInt(row.gauges),
};
@@ -35,11 +36,12 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
id: profileId,
music: settings.music,
pack: settings.pack,
aura: settings.aura,
paper_cup: settings.paperCup,
gauges: settings.gauges,
})
.onConflict("id")
.doUpdate(["music", "pack", "paper_cup", "gauges"]);
.doUpdate(["music", "pack", "aura", "paper_cup", "gauges"]);
await this._txn.modify(saveSql);
}
+1 -1
View File
@@ -74,7 +74,7 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
.where("id", teamId)
.forUpdate();
await this._txn.modify(lockSql);
await this._txn.fetchRow(lockSql);
// 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
+35 -6
View File
@@ -1,7 +1,8 @@
import sql from "sql-bricks-postgres";
import { RouteNo } from "../model/base";
import { ExtId, RouteNo } from "../model/base";
import { CarSelector } from "../model/car";
import { Team } from "../model/team";
import { Profile } from "../model/profile";
import { TimeAttackScore } from "../model/timeAttack";
import { TimeAttackRepository, TopTenResult } from "../repo";
@@ -23,23 +24,51 @@ function _extractRow(row: Row): TimeAttackScore {
export class SqlTimeAttackRepository implements TimeAttackRepository {
constructor(private readonly _txn: Transaction) {}
async loadTopTen(
async loadTop(
routeNo: RouteNo,
minTimestamp: Date
minTimestamp: Date,
limit: number
): 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
.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")
.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(sql.gt("ta.timestamp", minTimestamp))
.orderBy(["ta.total_time asc", "ta.timestamp asc"])
.limit(10);
.limit(limit);
const rows = await this._txn.fetchRows(loadSql);
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),
}));
}
+3 -1
View File
@@ -22,6 +22,7 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
}
return {
auras: parseInt(row.auras),
cup: parseInt(row.cup),
gauges: parseInt(row.gauges),
music: parseInt(row.music),
@@ -33,13 +34,14 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
const saveSql = sql
.insert("idz_unlocks", {
id: profileId,
auras: unlocks.auras,
cup: unlocks.cup,
gauges: unlocks.gauges,
music: unlocks.music,
last_mileage_reward: unlocks.lastMileageReward,
})
.onConflict("id")
.doUpdate(["cup", "gauges", "music", "last_mileage_reward"]);
.doUpdate(["auras", "cup", "gauges", "music", "last_mileage_reward"]);
await this._txn.modify(saveSql);
}
+5 -1
View File
@@ -1,5 +1,7 @@
@echo off
pushd %~dp0
where /q node
if errorlevel 1 (
echo NodeJS does not appear to be installed.
@@ -40,8 +42,10 @@ if not exist node_modules (
echo.
)
rem Project Diva requires TLSv1.0
set DEBUG=app:*
node .\bin\index.js
node --tls-min-v1.0 .\bin\index.js
:end
echo.