12 Commits
47 changed files with 580 additions and 151 deletions
+24 -86
View File
@@ -192,6 +192,14 @@
} }
} }
}, },
"@decafcode/sqlite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@decafcode/sqlite/-/sqlite-2.0.0.tgz",
"integrity": "sha512-CvDlc1G2ExETrRXVjjHRZmZiDCY56pw3xU3pxWnOzkQ4j4bi+DUAYlQbFKwpb4BzRdFo84e7H+T2kmoEBf90JA==",
"requires": {
"node-gyp-build": "^4.2.0"
}
},
"@jest/console": { "@jest/console": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz",
@@ -401,15 +409,6 @@
"@babel/types": "^7.3.0" "@babel/types": "^7.3.0"
} }
}, },
"@types/better-sqlite3": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-5.4.0.tgz",
"integrity": "sha512-nzm7lJ7l3jBmGUbtkL8cdOMhPkN6Pw2IM+b0V7iIKba+YKiLrjkIy7vVLsBIVnd7+lgzBzrHsXZxCaFTcmw5Ow==",
"dev": true,
"requires": {
"@types/integer": "*"
}
},
"@types/body-parser": { "@types/body-parser": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz",
@@ -474,12 +473,6 @@
"@types/range-parser": "*" "@types/range-parser": "*"
} }
}, },
"@types/integer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/integer/-/integer-1.0.0.tgz",
"integrity": "sha512-3viiRKLoSP2Qr78nMoQjkDc0fan4BgmpOyV1+1gKjE8wWXo3QQ78WItO6f9WuBf3qe3ymDYhM65oqHTOZ0rFxw==",
"dev": true
},
"@types/istanbul-lib-coverage": { "@types/istanbul-lib-coverage": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@@ -889,15 +882,6 @@
"tweetnacl": "^0.14.3" "tweetnacl": "^0.14.3"
} }
}, },
"better-sqlite3": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-5.4.3.tgz",
"integrity": "sha512-fPp+8f363qQIhuhLyjI4bu657J/FfMtgiiHKfaTsj3RWDkHlWC1yT7c6kHZDnBxzQVoAINuzg553qKmZ4F1rEw==",
"requires": {
"integer": "^2.1.0",
"tar": "^4.4.10"
}
},
"body-parser": { "body-parser": {
"version": "1.19.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -1104,11 +1088,6 @@
} }
} }
}, },
"chownr": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz",
"integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw=="
},
"ci-info": { "ci-info": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
@@ -1983,14 +1962,6 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
}, },
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2614,9 +2585,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",
@@ -2770,11 +2741,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}, },
"integer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/integer/-/integer-2.1.0.tgz",
"integrity": "sha512-vBtiSgrEiNocWvvZX1RVfeOKa2mCHLZQ2p9nkQkQZ/BvEiY+6CcUz0eyjvIiewjJoeNidzg2I+tpPJvpyspL1w=="
},
"invariant": { "invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -3818,24 +3784,8 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
}, "dev": true
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
}, },
"mixin-deep": { "mixin-deep": {
"version": "1.3.2", "version": "1.3.2",
@@ -3862,6 +3812,7 @@
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -3968,6 +3919,11 @@
"lower-case": "^1.1.1" "lower-case": "^1.1.1"
} }
}, },
"node-gyp-build": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.0.tgz",
"integrity": "sha512-4oiumOLhCDU9Rronz8PZ5S4IvT39H5+JEv/hps9V8s7RSLhsac0TCP78ulnHXOo8X1wdpPiTayGlM1jr4IbnaQ=="
},
"node-int64": { "node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4634,7 +4590,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
"dev": true
}, },
"safe-regex": { "safe-regex": {
"version": "1.1.0", "version": "1.1.0",
@@ -5191,20 +5148,6 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true "dev": true
}, },
"tar": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.8.6",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
}
},
"test-exclude": { "test-exclude": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz",
@@ -5376,9 +5319,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": {
@@ -5650,11 +5593,6 @@
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true "dev": true
}, },
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"yargs": { "yargs": {
"version": "13.3.0", "version": "13.3.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
+1 -2
View File
@@ -5,7 +5,7 @@
"main": "src/index.js", "main": "src/index.js",
"private": true, "private": true,
"dependencies": { "dependencies": {
"better-sqlite3": "^5.4.3", "@decafcode/sqlite": "^2.0.0",
"compression": "^1.7.3", "compression": "^1.7.3",
"date-fns": "^1.30.1", "date-fns": "^1.30.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -20,7 +20,6 @@
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^5.4.0",
"@types/compression": "^0.0.36", "@types/compression": "^0.0.36",
"@types/debug": "^4.1.5", "@types/debug": "^4.1.5",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
+40
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")
@@ -122,6 +145,23 @@ create table "cm_user_data_ex" (
"ext_long5" integer not null "ext_long5" integer not null
); );
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")
);
create table "cm_user_character" ( create table "cm_user_character" (
"id" integer primary key not null, "id" integer primary key not null,
"profile_id" integer not null "profile_id" integer not null
+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")
);
@@ -0,0 +1,17 @@
-- This was also not present in the db init scripts...
create table if not exists "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")
);
+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);
} }
+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);
} }
+38 -21
View File
@@ -1,14 +1,10 @@
import Database from "better-sqlite3"; import Database, { ResultRow } from "@decafcode/sqlite";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import * as sql from "sql-bricks-postgres"; import * as sql from "sql-bricks-postgres";
import { DataSource, Row, Transaction } from "./api"; import { DataSource, Row, Transaction } from "./api";
import { Id } from "../model"; import { Id } from "../model";
type MixedRow = {
[key: string]: any;
};
// bless me father for i have sinned // bless me father for i have sinned
const fuFixup = new RegExp(" FOR UPDATE$"); const fuFixup = new RegExp(" FOR UPDATE$");
@@ -41,7 +37,7 @@ function _preprocess(stmt: sql.Statement) {
}; };
} }
function _postprocess(obj: MixedRow): Row { function _postprocess(obj: ResultRow): Row {
const result = {}; const result = {};
for (const [k, v] of Object.entries(obj)) { for (const [k, v] of Object.entries(obj)) {
@@ -58,7 +54,7 @@ function _postprocess(obj: MixedRow): Row {
} }
class SqliteTransaction implements Transaction { class SqliteTransaction implements Transaction {
constructor(private readonly _db: Database.Database) {} constructor(private readonly _db: Database) {}
generateId<T>(): Id<T> { generateId<T>(): Id<T> {
const buf = randomBytes(8); const buf = randomBytes(8);
@@ -73,26 +69,43 @@ class SqliteTransaction implements Transaction {
modify(stmt: sql.Statement): Promise<void> { modify(stmt: sql.Statement): Promise<void> {
const params = _preprocess(stmt); const params = _preprocess(stmt);
const prepared = this._db.prepare(params.text);
this._db.prepare(params.text).run(...params.values); try {
prepared.run(params.values);
} finally {
prepared.close();
}
return Promise.resolve(); return Promise.resolve();
} }
fetchRow(stmt: sql.SelectStatement): Promise<Row | undefined> { fetchRow(stmt: sql.SelectStatement): Promise<Row | undefined> {
const params = _preprocess(stmt); const params = _preprocess(stmt);
const raw = this._db.prepare(params.text).get(...params.values); const prepared = this._db.prepare(params.text);
const result = raw && _postprocess(raw);
return Promise.resolve(result); try {
const raw = prepared.one(params.values);
const result = raw && _postprocess(raw);
return Promise.resolve(result);
} finally {
prepared.close();
}
} }
fetchRows(stmt: sql.SelectStatement): Promise<Row[]> { fetchRows(stmt: sql.SelectStatement): Promise<Row[]> {
const params = _preprocess(stmt); const params = _preprocess(stmt);
const raw = this._db.prepare(params.text).all(...params.values); const prepared = this._db.prepare(params.text);
const result = raw.map(_postprocess);
return Promise.resolve(result); try {
const raw = prepared.all(params.values);
const result = raw.map(_postprocess);
return Promise.resolve(result);
} finally {
prepared.close();
}
} }
raw(sql: string): Promise<void> { raw(sql: string): Promise<void> {
@@ -110,28 +123,32 @@ class SqliteDataSource implements DataSource {
): Promise<T> { ): Promise<T> {
const db = new Database(this._path); const db = new Database(this._path);
db.defaultSafeIntegers();
db.prepare("pragma foreign_keys = on").run();
db.prepare("begin").run();
try { try {
db.exec("begin");
const txn = new SqliteTransaction(db); const txn = new SqliteTransaction(db);
const result = await callback(txn); const result = await callback(txn);
db.prepare("commit").run(); db.exec("commit");
return result; return result;
} catch (e) { } catch (e) {
db.prepare("rollback").run(); db.exec("rollback");
return Promise.reject(e); return Promise.reject(e);
} finally {
db.close();
} }
} }
vacuum(): Promise<void> { vacuum(): Promise<void> {
const db = new Database(this._path); const db = new Database(this._path);
db.prepare("vacuum").run(); try {
db.exec("vacuum");
} finally {
db.close();
}
return Promise.resolve(); return Promise.resolve();
} }
-3
View File
@@ -22,9 +22,6 @@ if not exist node_modules (
echo The server's dependencies will now be installed. echo The server's dependencies will now be installed.
echo This process requires an internet connection and may take some time. echo This process requires an internet connection and may take some time.
echo. echo.
echo NOTE: If install fails try running the following command as admin:
echo npm install -g windows-build-tools
echo.
pause pause
call npm install call npm install