34 Commits
Author SHA1 Message Date
Tau 1869c37949 Add CHANGELOG up to v012 2020-08-17 21:26:40 -04:00
e76cd2ec3a63f11710dc9fa0bc5b66176521af0a 1d8fd582c9 chuni: sort user activity response 2020-08-17 21:11:00 -04:00
e76cd2ec3a63f11710dc9fa0bc5b66176521af0a 9b22318934 chuni: Fix upsert with an empty userRecentRating 2020-08-09 01:18:31 -07:00
e76cd2ec3a63f11710dc9fa0bc5b66176521af0a 71a7cd7eed chuni: Correctly implement recent rating requests 2020-08-02 12:22:01 -04:00
5ebef5658c5fcb6f5a62e75318bdc7fad90b21bc 6c22c4287e chuni: implement UserCharge APIs, add table for purchased tickets 2020-08-02 11:24:21 -04:00
5ebef5658c5fcb6f5a62e75318bdc7fad90b21bc a8539b475e chuni: enable ticket system 2020-08-02 11:24:21 -04:00
340f81fd089d20627f63fce796e939b691538bc5 abcccfb9a0 chuni: add new felica lookup mode 2020-08-01 12:55:25 -04:00
643161de4220f1bc925fbb05bde805fd8ec95c63 b2288b9b8f idz: Fix team-leader, add leader name and shop name 2020-08-01 12:50:09 -04:00
cd6c1fd2d946dff292b101ca3305fc7f31d2dce5 e61b103221 chuni: Changes to make compatible with old CHUNITHM 2020-08-01 12:41:55 -04:00
Tau 94ed27503a Add tests to CI build 2020-08-01 12:38:47 -04:00
Tau ad1b48c210 Fix? long-broken test 2020-08-01 12:38:47 -04:00
Tau 75b35e09e2 Add GitLab CI 2020-08-01 12:38:47 -04:00
cd6c1fd2d946dff292b101ca3305fc7f31d2dce5 834804b2ea chuni: fix lost scores for real 2020-04-19 23:03:31 +08:00
Matt Bilker ac9cf50d28 chuni: Fix maintenance timer 2020-03-22 23:16:33 +00:00
Matt Bilker cb4743f86f chuni: Add remaining Chunithm Amazon event IDs 2020-03-22 23:16:30 +00:00
Matt Bilker cef7e99e46 chuni: Put event IDs in separate static file 2020-03-22 23:15:01 +00:00
Matt Bilker 4b007b446d chuni: Enable all events 2020-03-22 23:15:00 +00:00
Matt Bilker 4c5122d163 chuni: Prevent recursive item calls if user has more than 600 items 2020-03-22 23:15:00 +00:00
Tau 01198d546a sql: Add NULL support to database wrappers 2020-03-22 22:38:54 +00:00
07bea96aed831abce88a9591d3ff86af19babd79 bc5f26d768 chuni: Add the api endpoint use in older chunithm 2020-01-30 18:26:36 +00:00
07bea96aed831abce88a9591d3ff86af19babd79 9a652e1ace chuni: lower max result per page to prevent game fail to read content. Fix #6 2020-01-30 16:52:31 +00:00
Tau 7b21a0bdae idz: Implement multi-page Garage fetch 2020-01-25 19:26:42 -05:00
Tau 5311618d1d Fix new DB initialization, AGAIN. 2020-01-05 10:45:40 -05:00
Tau 37ce14dda7 Switch to @decafcode/sqlite for database access 2020-01-05 10:00:12 -05:00
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
98 changed files with 2111 additions and 270 deletions
+16
View File
@@ -0,0 +1,16 @@
image: node:12-buster
cache:
paths:
- node_modules/
stages:
- build
do_build:
stage: build
script:
- npm install
- npm run build
- npm test
+53
View File
@@ -0,0 +1,53 @@
# CHANGELOG
## v012
* Chunithm: Fix large data loads (NeumPhis)
* Chunithm: Add some support for older versions (NeumPhis)
* Chunithm: Fix large data loads (Matt Bilker)
* Chunithm: Enable all events (Matt Bilker)
* Chunithm: Remove server maintenance period (Matt Bilker)
* Chunithm: Fix data loss (esterTion)
* Chunithm: Add some support for older versions (esterTion)
* IDZ: Team-related fixes (Emi Midnight)
* Add support for newer AIME card services (cxm)
* Chunithm: Enable ticket system (Rob)
* Chunithm: Implement Recent Rating feature (seika1)
* Chunithm: Fix User Activity feature (seika1)
## v011
* IDZ: Fix loading of >10 cars in garage (Tau)
## v010
* Chunithm: Bug fix: Fix new database creation and repair affected databases. Again. (Tau)
Switch to a different SQLite native code extension. C compiler is no longer required. (Tau)
## v009
* IDZ: Display team banners in attract loop rankings (Tau)
* Chunithm: Bug fix: Fix new database creation and repair affected databases (Tau)
## v008
* Implement auras in IDZ (BemaniWitch)
* Fix user ratings (NeumPhis)
* Load and save course results (NeumPhis)
* Load and save user duel list (Felix)
## v007
* Fix IDZ team creation (Tau)
* Fix startup script working directory (Tau)
## v006
* Fix Chunithm HTTP POST size limit (Tau)
## v005
* Initial public release (Tau)
* Chunithm v1.30.00 Amazon
* Initial D Zero v1.21.00
* Initial D Zero v1.31.00
+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": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz",
@@ -401,15 +409,6 @@
"@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": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz",
@@ -474,12 +473,6 @@
"@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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@@ -889,15 +882,6 @@
"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": {
"version": "1.19.0",
"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": {
"version": "2.0.0",
"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",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2614,9 +2585,9 @@
"dev": true
},
"handlebars": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz",
"integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==",
"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",
@@ -2770,11 +2741,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"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": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -3818,24 +3784,8 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"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"
}
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"mixin-deep": {
"version": "1.3.2",
@@ -3862,6 +3812,7 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3968,6 +3919,11 @@
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -4634,7 +4590,8 @@
"safe-buffer": {
"version": "5.2.0",
"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": {
"version": "1.1.0",
@@ -5191,20 +5148,6 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"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": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz",
@@ -5376,9 +5319,9 @@
"dev": true
},
"uglify-js": {
"version": "3.6.8",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz",
"integrity": "sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ==",
"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": {
@@ -5650,11 +5593,6 @@
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"yargs": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
+1 -2
View File
@@ -5,7 +5,7 @@
"main": "src/index.js",
"private": true,
"dependencies": {
"better-sqlite3": "^5.4.3",
"@decafcode/sqlite": "^2.0.0",
"compression": "^1.7.3",
"date-fns": "^1.30.1",
"debug": "^4.1.1",
@@ -20,7 +20,6 @@
"supports-color": "^7.1.0"
},
"devDependencies": {
"@types/better-sqlite3": "^5.4.0",
"@types/compression": "^0.0.36",
"@types/debug": "^4.1.5",
"@types/dotenv": "^6.1.1",
+68
View File
@@ -82,6 +82,44 @@ create table "cm_user_activity" (
)
);
create table "cm_user_charge" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"charge_id" integer not null,
"stock" integer not null,
"purchase_date" text not null,
"valid_date" text not null,
"param1" integer not null,
"param2" integer not null,
"param_date" text not null,
constraint "cm_user_charge_uq" unique ("profile_id", "charge_id")
);
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")
@@ -122,6 +160,23 @@ create table "cm_user_data_ex" (
"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" (
"id" integer primary key not null,
"profile_id" integer not null
@@ -302,3 +357,16 @@ create table "cm_user_playlog" (
"place_name" text not null,
"is_maimai" text not null
);
create table "cm_user_recent_rating" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"sort_order" integer not null,
"music_id" integer not null,
"difficult_id" integer not null,
"rom_version_code" integer not null,
"score" integer not null,
constraint "cm_user_recent_rating_uq" unique ("profile_id", "sort_order")
);
+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")
);
@@ -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")
);
+14
View File
@@ -0,0 +1,14 @@
create table if not exists "cm_user_charge" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"charge_id" integer not null,
"stock" integer not null,
"purchase_date" text not null,
"valid_date" text not null,
"param1" integer not null,
"param2" integer not null,
"param_date" text not null,
constraint "cm_user_charge_uq" unique ("profile_id", "charge_id")
);
@@ -0,0 +1,31 @@
create table if not exists "cm_user_recent_rating" (
"id" integer primary key not null,
"profile_id" integer not null
references "cm_user_data"("id")
on delete cascade,
"sort_order" integer not null,
"music_id" integer not null,
"difficult_id" integer not null,
"rom_version_code" integer not null,
"score" integer not null,
constraint "cm_user_recent_rating_uq" unique ("profile_id", "sort_order")
);
-- Prepopulate this table by backfilling the most recent 30 scores per user.
-- This isn't exactly correct since not every recent score should go in here, but it's probably close enough.
INSERT INTO cm_user_recent_rating (profile_id, sort_order, music_id, difficult_id, rom_version_code, score)
SELECT * FROM (
SELECT
profile_id,
row_number()
OVER (PARTITION BY profile_id ORDER BY user_play_date DESC) AS sort_order,
music_id,
level AS difficult_id,
'1030000' AS rom_version_code,
score
FROM cm_user_playlog
WHERE
difficult_id < 4 -- skip world's end
ORDER BY profile_id ASC, user_play_date DESC
)
WHERE sort_order <= 30;
+1 -1
View File
@@ -105,7 +105,7 @@ test("decode lookup", () => {
const obj = decode(req);
expect(obj.type).toBe("lookup");
expect(obj.type).toBe("lookup2");
expect(obj.gameId).toBe("SDBT");
expect(obj.keychipId).toBe("A69E01A9999");
expect(obj.luid).toBe("01036495850523030676");
+10
View File
@@ -31,6 +31,15 @@ function readFeliCaLookupRequest(msg: Buffer): Request.FeliCaLookupRequest {
};
}
function readFeliCaLookupRequest2(msg: Buffer): Request.FeliCaLookup2Request {
return {
...begin(msg),
type: "felica_lookup2",
idm: msg.slice(0x0030, 0x0038).toString("hex"),
pmm: msg.slice(0x0038, 0x0040).toString("hex"),
};
}
function readLogRequest(msg: Buffer): Request.LogRequest {
// idk what any of this stuff means yet
// field20 and field28 appear to be an aime id but that is all.
@@ -98,6 +107,7 @@ readers.set(0x0009, readLogRequest);
readers.set(0x000b, readCampaignRequest);
readers.set(0x000d, readRegisterRequest);
readers.set(0x000f, readLookupRequest2);
readers.set(0x0011, readFeliCaLookupRequest2);
readers.set(0x0064, readHelloRequest);
readers.set(0x0066, readGoodbyeRequest);
+13 -1
View File
@@ -14,7 +14,7 @@ registerLevels.set("segaid", 2);
function begin(length: number) {
const buf = Buffer.alloc(length);
buf.writeUInt16LE(0xa13e, 0x0000); // Magic?
buf.writeUInt16LE(0xa13e, 0x0000); // Magic: aime
buf.writeUInt16LE(0x3087, 0x0002); // ???
buf.writeUInt16LE(length, 0x0006);
@@ -43,6 +43,18 @@ export class Encoder extends Transform {
break;
case "felica_lookup2":
buf = begin(0x0140);
buf.writeUInt16LE(0x0012, 0x0004); // cmd code
buf.writeUInt16LE(msg.status, 0x0008);
buf.writeInt32LE(msg.aimeId || -1, 0x0020);
buf.writeUInt32LE(0xffffffff, 0x0024); // FF
buf.writeUInt32LE(0xffffffff, 0x0028); // FF
buf.write(msg.accessCode, 0x002c, "hex");
buf.writeUInt16LE(0x0001, 0x0037); // 00 01
break;
case "hello":
buf = begin(0x0020);
buf.writeUInt16LE(0x0065, 0x0004); // cmd code
+25
View File
@@ -46,6 +46,28 @@ function feliCaLookup(
return { type: req.type, status: 1, accessCode };
}
async function feliCaLookup2(
rep: Repositories,
req: Req.FeliCaLookup2Request,
now: Date
): Promise<Res.FeliCaLookup2Response> {
debug("FeliCa access code lookup");
const num = BigInt("0x" + req.idm);
let accessCode = num.toString();
while (accessCode.length < 20) {
accessCode = "0" + accessCode;
}
return {
type: req.type,
status: 1,
accessCode,
aimeId: await rep.cards().lookup(accessCode, now),
};
}
async function lookup(
rep: Repositories,
req: Req.LookupRequest,
@@ -115,6 +137,9 @@ export async function dispatch(
case "felica_lookup":
return feliCaLookup(rep, req, now);
case "felica_lookup2":
return feliCaLookup2(rep, req, now);
case "lookup":
return lookup(rep, req, now);
+7
View File
@@ -9,6 +9,12 @@ export interface FeliCaLookupRequest extends AimeRequestBase {
pmm: string;
}
export interface FeliCaLookup2Request extends AimeRequestBase {
type: "felica_lookup2";
idm: string;
pmm: string;
}
export interface RegisterRequest extends AimeRequestBase {
type: "register";
luid: string;
@@ -50,6 +56,7 @@ export interface GoodbyeRequest {
export type AimeRequest =
| FeliCaLookupRequest
| FeliCaLookup2Request
| CampaignRequest
| GoodbyeRequest
| HelloRequest
+7
View File
@@ -11,6 +11,12 @@ export interface FeliCaLookupResponse extends AimeResponseBase {
accessCode: string;
}
export interface FeliCaLookup2Response extends AimeResponseBase {
type: "felica_lookup2";
accessCode: string;
aimeId?: AimeId;
}
export interface CampaignResponse extends AimeResponseBase {
type: "campaign";
}
@@ -44,6 +50,7 @@ export interface RegisterResponse extends AimeResponseBase {
export type AimeResponse =
| FeliCaLookupResponse
| FeliCaLookup2Response
| CampaignResponse
| HelloResponse
| LogResponse
+2 -2
View File
@@ -19,8 +19,8 @@ class CardRepositoryImpl implements CardRepository {
return undefined;
}
const id = row.id;
const extId = row.ext_id;
const id = row.id!;
const extId = row.ext_id!;
const touchSql = sql
.update("aime_player")
+1 -1
View File
@@ -111,7 +111,7 @@ export default async function checkdb(db: DataSource): Promise<void> {
const row = await db.transaction(txn => txn.fetchRow(stmt));
if (row !== undefined) {
maybe = parseInt(row.schemaver);
maybe = parseInt(row.schemaver!);
}
} catch (e) {
return db.transaction(initdb);
+19 -2
View File
@@ -1,13 +1,30 @@
import { GameChargeJson } from "../proto/gameCharge";
import { Repositories } from "../repo";
import { GetGameChargeRequest } from "../request/getGameCharge";
import { GetGameChargeResponse } from "../response/getGameCharge";
import { CHARGE_IDS } from "../static/charge";
export default async function getGameCharge(
rep: Repositories,
req: GetGameChargeRequest
): Promise<GetGameChargeResponse> {
const gameChargeList: GameChargeJson[] = [];
for (const [i, charge] of CHARGE_IDS.entries()) {
gameChargeList.push({
chargeId: charge.id.toString(),
orderId: (i+1).toString(),
price: charge.price.toString(),
salePrice: charge.salePrice.toString(),
startDate: "2017-12-05 07:00:00.0",
endDate: "2029-12-31 23:59:59.0",
saleStartDate: "2017-12-05 07:00:00.0",
saleEndDate: "2030-12-31 23:59:59.0"
});
}
return {
length: "0",
gameChargeList: [],
length: gameChargeList.length.toString(),
gameChargeList,
};
}
+15 -2
View File
@@ -1,14 +1,27 @@
import { GameEventJson } from "../proto/gameEvent";
import { Repositories } from "../repo";
import { GetGameEventRequest } from "../request/getGameEvent";
import { GetGameEventResponse } from "../response/getGameEvent";
import { EVENT_IDS } from "../static/event";
export default async function getGameEvent(
rep: Repositories,
req: GetGameEventRequest
): Promise<GetGameEventResponse> {
const gameEventList: GameEventJson[] = [];
for (const id of EVENT_IDS) {
gameEventList.push({
type: req.type,
id: id.toString(),
startDate: "2017-12-05 07:00:00.0",
endDate: "2099-12-31 00:00:00.0",
});
}
return {
type: req.type,
length: "0",
gameEventList: [],
length: gameEventList.length.toString(),
gameEventList,
};
}
+13 -6
View File
@@ -1,3 +1,4 @@
import { writeDate } from "../proto/base";
import { Repositories } from "../repo";
import { GetGameSettingRequest } from "../request/getGameSetting";
import { GetGameSettingResponse } from "../response/getGameSetting";
@@ -6,19 +7,25 @@ export default async function getGameSetting(
rep: Repositories,
req: GetGameSettingRequest
): Promise<GetGameSettingResponse> {
const rebootStartTime = new Date();
rebootStartTime.setHours(rebootStartTime.getHours() - 3);
const rebootEndTime = new Date();
rebootEndTime.setHours(rebootEndTime.getHours() - 2);
return {
gameSetting: {
dataVersion: "1",
isMaintenance: "false",
requestInterval: "10",
rebootStartTime: "0",
rebootEndTime: "0",
rebootStartTime: writeDate(rebootStartTime),
rebootEndTime: writeDate(rebootEndTime),
isBackgroundDistribute: "false",
maxCountCharacter: "999",
maxCountItem: "999",
maxCountMusic: "999",
maxCountCharacter: "300",
maxCountItem: "300",
maxCountMusic: "100",
},
isDumpUpload: "false",
isAou: "false",
isAou: "true",
};
}
+9 -2
View File
@@ -1,3 +1,5 @@
import { readAimeId } from "../proto/base";
import { writeUserCharge } from "../proto/userCharge";
import { Repositories } from "../repo";
import { GetUserChargeRequest } from "../request/getUserCharge";
import { GetUserChargeResponse } from "../response/getUserCharge";
@@ -6,9 +8,14 @@ export default async function getUserCharge(
rep: Repositories,
req: GetUserChargeRequest
): Promise<GetUserChargeResponse> {
const aimeId = readAimeId(req.userId);
const profileId = await rep.userData().lookup(aimeId);
const items = await rep.userCharge().load(profileId);
return {
userId: req.userId,
length: "0",
userChargeList: [],
length: items.length.toString(),
userChargeList: items.map(writeUserCharge),
};
}
+15 -5
View File
@@ -1,17 +1,27 @@
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),
};
}
+1 -1
View File
@@ -47,7 +47,7 @@ export default async function getUserItem(
// Pack the next pagination cookie into bigint compound form
const xout = itemKindN * itemKindMul + BigInt(maxCount + items.length);
const xout = itemKindN * itemKindMul + nextIndexN + BigInt(items.length);
// Done:
+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 { writeUserRecentRating } 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.userRecentRating().load(profileId);
return {
userId: req.userId,
length: "0",
userRecentRatingList: [],
length: items.length.toString(),
userRecentRatingList: items.map(writeUserRecentRating),
};
}
+3
View File
@@ -32,6 +32,7 @@ import upsertClientError from "./upsertClientError";
import upsertClientSetting from "./upsertClientSetting";
import upsertClientTestmode from "./upsertClientTestmode";
import upsertUserAll from "./upsertUserAll";
import upsertUserChargelogApi from "./upsertUserChargelogApi";
import createSqlWrapper from "../sql";
import { DataSource } from "../../sql";
@@ -104,6 +105,7 @@ export default function chunithm(db: DataSource) {
wrapper.rpc("/GetUserOptionApi", getUserOption);
wrapper.rpc("/GetUserOptionExApi", getUserOptionEx);
wrapper.rpc("/GetUserPreviewApi", getUserPreview);
wrapper.rpc("/GetUserRecentPlayerApi", getUserRecentRating);
wrapper.rpc("/GetUserRecentRatingApi", getUserRecentRating);
wrapper.rpc("/GetUserRegionApi", getUserRegion);
wrapper.rpc("/UpsertClientBookkeepingApi", upsertClientBookkeeping);
@@ -112,6 +114,7 @@ export default function chunithm(db: DataSource) {
wrapper.rpc("/UpsertClientSettingApi", upsertClientSetting);
wrapper.rpc("/UpsertClientTestmodeApi", upsertClientTestmode);
wrapper.rpc("/UpsertUserAllApi", upsertUserAll);
wrapper.rpc("/UpsertUserChargelogApi", upsertUserChargelogApi)
const app = express();
+23
View File
@@ -11,7 +11,11 @@ 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 { readUserCharge } from "../proto/userCharge";
import { readUserRecentRating } from "../proto/userRecentRating";
import { readUserCourse } from "../proto/userCourse";
// It shouldn't need to be said really, but seeing as this message (A) requires
// fairly lengthy processing and (B) is only sent right at the end of a credit,
@@ -65,9 +69,28 @@ export default async function upsertUserAll(
await rep.userPlaylog().save(profileId, readUserPlaylog(item));
}
for (const item of payload.userChargeList || []) {
await rep.userCharge().save(profileId, readUserCharge(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));
}
await rep
.userRecentRating()
.save(
profileId,
(payload.userRecentRatingList || []).map(readUserRecentRating)
);
return { returnCode: "1" };
}
@@ -0,0 +1,12 @@
import { Repositories } from "../repo";
import { UpsertUserChargelogApiRequest } from "../request/upsertUserChargelogApi";
import { UpsertUserChargelogApiResponse } from "../response/upsertUserChargelogApi";
export default async function UpsertUserChargelogApi(
rep: Repositories,
req: UpsertUserChargelogApiRequest
): Promise<UpsertUserChargelogApiResponse> {
return {
returnCode: "1",
};
}
+9
View File
@@ -0,0 +1,9 @@
export interface UserChargeItem {
chargeId: number;
stock: number;
purchaseDate: Date;
validDate: Date;
param1: number;
param2: number;
paramDate: Date;
}
+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;
}
+20
View File
@@ -0,0 +1,20 @@
import { Crush, readDate, writeObject } from "./base";
import { UserChargeItem } from "../model/userCharge";
export type UserChargeJson = Crush<UserChargeItem>;
export function readUserCharge(json: UserChargeJson): UserChargeItem {
return {
chargeId: parseInt(json.chargeId),
stock: parseInt(json.stock),
purchaseDate: readDate(json.purchaseDate),
validDate: readDate(json.validDate),
param1: parseInt(json.param1),
param2: parseInt(json.param2),
paramDate: readDate(json.paramDate),
};
}
export function writeUserCharge(obj: UserChargeItem): UserChargeJson {
return writeObject(obj);
}
+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);
}
+1
View File
@@ -1,3 +1,4 @@
import { UserPlaylogItem } from "./../model/userPlaylog";
import { Crush, writeObject } from "./base";
import { UserRecentRatingItem } from "../model/userRecentRating";
+14
View File
@@ -2,24 +2,32 @@ export { Page } from "./_defs";
import { UserActivityRepository } from "./userActivity";
import { UserCharacterRepository } from "./userCharacter";
import { UserCourseRepository } from "./userCourse";
import { UserDataRepository } from "./userData";
import { UserDataExRepository } from "./userDataEx";
import { UserDuelListRepository } from "./userDuelList";
import { UserGameOptionRepository } from "./userGameOption";
import { UserGameOptionExRepository } from "./userGameOptionEx";
import { UserItemRepository } from "./userItem";
import { UserMapRepository } from "./userMap";
import { UserMusicRepository } from "./userMusic";
import { UserPlaylogRepository } from "./userPlaylog";
import { UserChargeRepository } from "./userCharge";
import { UserRecentRatingRepository } from "./userRecentRating";
export interface Repositories {
userActivity(): UserActivityRepository;
userCharacter(): UserCharacterRepository;
userCourse(): UserCourseRepository;
userData(): UserDataRepository;
userDataEx(): UserDataExRepository;
userDuelList(): UserDuelListRepository;
userGameOption(): UserGameOptionRepository;
userGameOptionEx(): UserGameOptionExRepository;
@@ -31,4 +39,10 @@ export interface Repositories {
userMusic(): UserMusicRepository;
userPlaylog(): UserPlaylogRepository;
userCharge(): UserChargeRepository;
userCourse(): UserCourseRepository;
userRecentRating(): UserRecentRatingRepository;
}
+4
View File
@@ -0,0 +1,4 @@
import { UserChargeItem } from "../model/userCharge";
import { RepositoryN } from "./_defs";
export type UserChargeRepository = RepositoryN<UserChargeItem>;
+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[]>;
}
+12
View File
@@ -0,0 +1,12 @@
import { UserDataItem } from "../model/userData";
import { UserRecentRatingItem } from "../model/userRecentRating";
import { Id } from "../../model";
export interface UserRecentRatingRepository {
load(profileId: Id<UserDataItem>): Promise<UserRecentRatingItem[]>;
save(
profileId: Id<UserDataItem>,
objs: UserRecentRatingItem[]
): Promise<void>;
}
+6
View File
@@ -8,7 +8,10 @@ import { UserMusicDetailJson } from "../proto/userMusic";
import { UserActivityJson } from "../proto/userActivity";
import { UserRecentRatingJson } from "../proto/userRecentRating";
import { UserPlaylogJson } from "../proto/userPlaylog";
import { UserChargeJson } from "../proto/userCharge";
import { UserCourseJson } from "../proto/userCourse";
import { UserDataExJson } from "../proto/userDataEx";
import { UserDuelListJson } from "../proto/userDuelList";
export interface UpsertUserAllRequest {
/** Integer, AiMe ID */
@@ -25,7 +28,10 @@ export interface UpsertUserAllRequest {
userActivityList?: UserActivityJson[];
userRecentRatingList?: UserRecentRatingJson[];
userPlaylogList?: UserPlaylogJson[];
userChargeList?: UserChargeJson[];
userCourseList?: UserCourseJson[];
userDataEx?: UserDataExJson[];
userDuelList?: UserDuelListJson[];
/** String of binary digits */
isNewMapList: string;
@@ -0,0 +1,24 @@
export interface UpsertUserChargelogApiRequest {
userId: string;
userChargelog: {
chargeId: string,
price: string,
purchaseDate: Date,
playCount: string,
playerRating: string,
placeId: string,
regionId: string,
clientId: string
};
userCharge: {
chargeId: string,
stock: string,
purchaseDate: Date,
validDate: Date,
param1: string,
param2: string,
paramDate: Date
};
}
+3 -2
View File
@@ -1,3 +1,5 @@
import { UserChargeJson } from "../proto/userCharge";
export interface GetUserChargeResponse {
/** Integer, AiMe ID */
userId: string;
@@ -5,6 +7,5 @@ export interface GetUserChargeResponse {
/** Integer, number of results returned */
length: string;
/** TBD */
userChargeList: [];
userChargeList: UserChargeJson[];
}
+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[];
}
@@ -0,0 +1,3 @@
export interface UpsertUserChargelogApiResponse {
returnCode: string;
}
+24
View File
@@ -1,24 +1,32 @@
import { SqlUserActivityRepository } from "./userActivity";
import { SqlUserCharacterRepository } from "./userCharacter";
import { SqlUserChargeRepository } from "./userCharge";
import { SqlUserCourseRepository } from "./userCourse";
import { SqlUserDataRepository } from "./userData";
import { SqlUserDataExRepository } from "./userDataEx";
import { SqlUserDuelListRepository } from "./userDuelList";
import { SqlUserGameOptionRepository } from "./userGameOption";
import { SqlUserGameOptionExRepository } from "./userGameOptionEx";
import { SqlUserItemRepository } from "./userItem";
import { SqlUserMapRepository } from "./userMap";
import { SqlUserMusicRepository } from "./userMusic";
import { SqlUserPlaylogRepository } from "./userPlaylog";
import { SqlUserRecentRatingRepository } from "./userRecentRating";
import { Repositories } from "../repo";
import { UserActivityRepository } from "../repo/userActivity";
import { UserCharacterRepository } from "../repo/userCharacter";
import { UserChargeRepository } from "../repo/userCharge";
import { UserCourseRepository } from "../repo/userCourse";
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";
import { UserMapRepository } from "../repo/userMap";
import { UserMusicRepository } from "../repo/userMusic";
import { UserPlaylogRepository } from "../repo/userPlaylog";
import { UserRecentRatingRepository } from "../repo/userRecentRating";
import { Transaction } from "../../sql";
export class SqlRepositories implements Repositories {
@@ -32,6 +40,14 @@ export class SqlRepositories implements Repositories {
return new SqlUserCharacterRepository(this._txn);
}
userCharge(): UserChargeRepository {
return new SqlUserChargeRepository(this._txn);
}
userCourse(): UserCourseRepository {
return new SqlUserCourseRepository(this._txn);
}
userData(): UserDataRepository {
return new SqlUserDataRepository(this._txn);
}
@@ -40,6 +56,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 +83,8 @@ export class SqlRepositories implements Repositories {
userPlaylog(): UserPlaylogRepository {
return new SqlUserPlaylogRepository(this._txn);
}
userRecentRating(): UserRecentRatingRepository {
return new SqlUserRecentRatingRepository(this._txn);
}
}
+2 -1
View File
@@ -28,7 +28,8 @@ export class SqlUserActivityRepository implements UserActivityRepository {
.select("*")
.from("cm_user_activity")
.where("profile_id", profileId)
.where("kind", kind);
.where("kind", kind)
.orderBy("sort_number DESC");
const rows = await this._txn.fetchRows(stmt);
+47
View File
@@ -0,0 +1,47 @@
import sql from "sql-bricks-postgres";
import { createSqlMapper, T } from "../../sql/util";
import { UserChargeRepository } from "../repo/userCharge";
import { Id } from "../../model";
import { Page } from "../repo";
import { UserDataItem } from "../model/userData";
import { UserChargeItem } from "../model/userCharge";
import { Transaction } from "../../sql";
const { readRow, writeRow, colNames } = createSqlMapper({
chargeId: T.number,
stock: T.number,
purchaseDate: T.Date,
validDate: T.Date,
param1: T.number,
param2: T.number,
paramDate: T.Date,
});
export class SqlUserChargeRepository implements UserChargeRepository {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<UserDataItem>): Promise<UserChargeItem[]> {
const stmt = sql
.select("*")
.from("cm_user_charge")
.where("profile_id", profileId);
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserChargeItem): Promise<void> {
const stmt = sql
.insert("cm_user_charge", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "charge_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+65
View File
@@ -0,0 +1,65 @@
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);
/**
* UserCourse has no paging before CHUNITHM Amazon
*/
if (page && !isNaN(page.limit)) {
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);
}
}
+41 -1
View File
@@ -56,7 +56,47 @@ export class SqlUserDataExRepository implements UserDataExRepository {
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
throw new Error("UserDataEx record not found");
/**
* When upgrading from CHUNITHM Star Plus & earlier
* to Amazon, there is no UserDataEx, so we return
* a "default" row.
*/
return {
compatibleCmVersion: "",
medal: 0,
mapIconId: 0,
voiceId: 0,
ext1: 0,
ext2: 0,
ext3: 0,
ext4: 0,
ext5: 0,
ext6: 0,
ext7: 0,
ext8: 0,
ext9: 0,
ext10: 0,
ext11: 0,
ext12: 0,
ext13: 0,
ext14: 0,
ext15: 0,
ext16: 0,
ext17: 0,
ext18: 0,
ext19: 0,
ext20: 0,
extStr1: "",
extStr2: "",
extStr3: "",
extStr4: "",
extStr5: "",
extLong1: 0n,
extLong2: 0n,
extLong3: 0n,
extLong4: 0n,
extLong5: 0n,
};
}
return readRow(row);
+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 -5
View File
@@ -34,14 +34,22 @@ export class SqlUserMusicRepository implements UserMusicRepository {
profileId: Id<UserDataItem>,
page?: Page
): Promise<UserMusicDetailItem[]> {
const preStmt = sql
.select("DISTINCT(music_id)")
.from("cm_user_music")
.where("profile_id", profileId)
.orderBy("music_id");
if (page) {
preStmt.limit(page.limit).offset(page.offset);
}
const preRows = await this._txn.fetchRows(preStmt);
const musicIds = preRows.map(r => r.music_id);
const stmt = sql
.select("*")
.from("cm_user_music")
.where("profile_id", profileId);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
.where("profile_id", profileId)
.and(sql.in('music_id', musicIds));
const rows = await this._txn.fetchRows(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);
}
}
+60
View File
@@ -0,0 +1,60 @@
import sql from "sql-bricks-postgres";
import { Id } from "../../model";
import { UserDataItem } from "../model/userData";
import { UserRecentRatingItem } from "../model/userRecentRating";
import { UserRecentRatingRepository } from "../repo/userRecentRating";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
musicId: T.number,
difficultId: T.number,
romVersionCode: T.number,
score: T.number,
});
export class SqlUserRecentRatingRepository
implements UserRecentRatingRepository {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<UserDataItem>): Promise<UserRecentRatingItem[]> {
const stmt = sql
.select("music_id", "difficult_id", "rom_version_code", "score")
.from("cm_user_recent_rating")
.where("profile_id", profileId)
.order("sort_order ASC");
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(
profileId: Id<UserDataItem>,
objs: UserRecentRatingItem[]
): Promise<void> {
// Don't do anything if there's nothing to save,
// since trying to execute an empty insert statement fails.
if (objs.length === 0) {
return Promise.resolve();
}
const stmt = sql
.insert(
"cm_user_recent_rating",
objs.map((obj, idx) => {
return {
id: this._txn.generateId(),
profile_id: profileId,
sort_order: idx + 1,
...writeRow(obj),
};
})
)
.onConflict("profile_id", "sort_order")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+17
View File
@@ -0,0 +1,17 @@
export const CHARGE_IDS = Object.freeze([
{
id: 2310, //World's End ticket x5
price: 1,
salePrice: 1,
},
{
id: 2060, //4x bonus ticket
price: 1,
salePrice: 1,
},
{
id: 2230, //6 songs premium ticket
price: 2,
salePrice: 2,
},
]);
+854
View File
@@ -0,0 +1,854 @@
export const EVENT_IDS = Object.freeze([
// A000
0,
1506,
1604,
1702,
1953,
2206,
2353,
2553,
2804,
2999,
3000,
3001,
3002,
3003,
3006,
3007,
3008,
3009,
3010,
3011,
3012,
3013,
3014,
3015,
3016,
3017,
3018,
3019,
3020,
3021,
3026,
3027,
20003,
// A014
3004,
3005,
3022,
3023,
3024,
3025,
3028,
3029,
3030,
3031,
3032,
3100,
3101,
3102,
3103,
3104,
3150,
3151,
3152,
3153,
3154,
3155,
3156,
3157,
3158,
3159,
3160,
3161,
3162,
3163,
3164,
3165,
3166,
3167,
3168,
3169,
3200,
3201,
3202,
3203,
3204,
3205,
3206,
3207,
3208,
3209,
3210,
3211,
3212,
3213,
3214,
3215,
3216,
3217,
3218,
3219,
3250,
3251,
3252,
3253,
3254,
3255,
3256,
3257,
3258,
3259,
3260,
3261,
3262,
3263,
3300,
3301,
3302,
3303,
3304,
3305,
3306,
3307,
3308,
3309,
3310,
3311,
3350,
3351,
3352,
3353,
3400,
3401,
3402,
3403,
3404,
3405,
3406,
3407,
3408,
3409,
3410,
3411,
3412,
3413,
3414,
3415,
3416,
3417,
3450,
3451,
3452,
3453,
3454,
3455,
// A017
3500,
3501,
3502,
3503,
3504,
3505,
3506,
3507,
3508,
3509,
3510,
3511,
3512,
3513,
3514,
3515,
3550,
3551,
3552,
3553,
3554,
3555,
3556,
3557,
3580,
3581,
// Other event IDs for Chunithm Amazon
1,
10,
11,
100,
101,
102,
103,
200,
201,
202,
203,
300,
301,
400,
401,
500,
501,
700,
701,
810,
820,
821,
830,
831,
832,
834,
835,
840,
841,
842,
843,
850,
851,
852,
853,
860,
861,
862,
863,
864,
865,
870,
871,
872,
873,
880,
890,
891,
900,
901,
902,
903,
904,
905,
906,
907,
908,
909,
910,
911,
912,
913,
914,
915,
916,
917,
918,
930,
931,
932,
933,
934,
935,
936,
937,
938,
939,
940,
941,
942,
943,
944,
945,
946,
947,
960,
961,
962,
963,
964,
965,
966,
967,
968,
969,
990,
991,
992,
993,
994,
995,
996,
997,
998,
999,
1000,
1001,
1002,
1003,
1004,
1005,
1006,
1020,
1021,
1022,
1023,
1024,
1025,
1026,
1027,
1028,
1029,
1030,
1031,
1050,
1051,
1052,
1053,
1054,
1055,
1056,
1057,
1058,
1059,
1060,
1061,
1070,
1098,
1099,
1100,
1101,
1102,
1103,
1104,
1105,
1106,
1107,
1108,
1109,
1110,
1111,
1112,
1113,
1114,
1130,
1131,
1132,
1133,
1134,
1135,
1136,
1137,
1138,
1159,
1160,
1161,
1162,
1163,
1164,
1165,
1166,
1167,
1168,
1169,
1170,
1171,
1172,
1173,
1174,
1175,
1190,
1191,
1192,
1193,
1194,
1195,
1196,
1197,
1198,
1199,
1200,
1201,
1220,
1221,
1222,
1223,
1224,
1225,
1226,
1227,
1228,
1229,
1230,
1231,
1232,
1233,
1250,
1251,
1252,
1253,
1254,
1255,
1256,
1257,
1258,
1259,
1260,
1261,
1279,
1280,
1281,
1282,
1283,
1284,
1285,
1286,
1287,
1288,
1289,
1290,
1291,
1292,
1293,
1294,
1295,
1296,
1297,
1298,
1299,
1310,
1311,
1312,
1313,
1314,
1315,
1316,
1317,
1318,
1319,
1320,
1321,
1322,
1323,
1324,
1325,
1326,
1327,
1328,
1329,
1340,
1341,
1342,
1343,
1344,
1345,
1346,
1347,
1348,
1349,
1350,
1351,
1352,
1353,
1354,
1355,
1356,
1357,
1370,
1371,
1372,
1373,
1374,
1375,
1376,
1377,
1378,
1379,
1380,
1381,
1382,
1383,
1384,
1385,
1386,
1387,
1388,
1400,
1401,
1402,
1403,
1404,
1405,
1406,
1407,
1408,
1409,
1410,
1411,
1412,
1413,
1414,
1415,
1416,
1430,
1431,
1432,
1433,
1434,
1435,
1436,
1437,
1438,
1439,
1440,
1441,
1442,
1443,
1444,
1445,
1446,
1447,
1460,
1461,
1462,
1463,
1464,
1465,
1500,
1501,
1502,
1504,
1505,
1507,
1508,
1509,
1510,
1511,
1512,
1513,
1514,
1515,
1516,
1517,
1518,
1519,
1520,
1521,
1522,
1523,
1524,
1600,
1601,
1603,
1605,
1606,
1650,
1651,
1652,
1653,
1654,
1655,
1656,
1657,
1658,
1659,
1660,
1661,
1700,
1701,
1703,
1750,
1751,
1752,
1753,
1754,
1755,
1756,
1757,
1758,
1759,
1760,
1761,
1762,
1763,
1764,
1765,
1766,
1767,
1768,
1769,
1770,
1800,
1801,
1802,
1803,
1804,
1805,
1850,
1851,
1852,
1853,
1854,
1855,
1856,
1857,
1880,
1881,
1882,
1883,
1884,
1885,
1886,
1887,
1888,
1889,
1890,
1900,
1901,
1903,
1904,
1905,
1906,
1907,
1908,
1909,
1910,
1911,
1912,
1913,
1950,
1951,
1952,
1954,
1955,
1956,
2000,
2001,
2003,
2004,
2005,
2006,
2007,
2008,
2009,
2010,
2011,
2012,
2013,
2050,
2051,
2052,
2053,
2054,
2055,
2056,
2100,
2101,
2103,
2104,
2105,
2106,
2107,
2108,
2109,
2110,
2111,
2150,
2151,
2152,
2153,
2154,
2155,
2156,
2200,
2201,
2202,
2203,
2204,
2205,
2207,
2208,
2209,
2210,
2211,
2212,
2213,
2214,
2215,
2216,
2217,
2218,
2219,
2220,
2221,
2222,
2223,
2224,
2225,
2226,
2250,
2251,
2252,
2253,
2280,
2281,
2282,
2283,
2300,
2301,
2302,
2303,
2304,
2305,
2306,
2307,
2308,
2350,
2351,
2352,
2354,
2355,
2356,
2380,
2381,
2382,
2383,
2400,
2401,
2402,
2403,
2404,
2405,
2406,
2407,
2408,
2409,
2410,
2411,
2412,
2413,
2414,
2415,
2416,
2417,
2418,
2419,
2420,
2421,
2450,
2451,
2452,
2453,
2454,
2455,
2456,
2457,
2458,
2459,
2500,
2501,
2502,
2503,
2504,
2505,
2506,
2507,
2508,
2509,
2510,
2511,
2550,
2551,
2552,
2554,
2555,
2556,
2557,
2558,
2559,
2560,
2561,
2562,
2600,
2601,
2602,
2603,
2604,
2605,
2606,
2607,
2608,
2609,
2610,
2611,
2612,
2613,
2614,
2615,
2650,
2651,
2652,
2653,
2654,
2655,
2656,
2657,
2658,
2659,
2660,
2700,
2701,
2702,
2703,
2704,
2705,
2706,
2707,
2708,
2709,
2710,
2711,
2750,
2751,
2752,
2753,
2754,
2755,
2800,
2801,
2802,
2803,
2805,
2806,
2807,
2808,
2809,
2810,
2811,
2812,
2813,
2814,
2815,
2816,
2817,
2818,
2850,
2851,
2852,
2853,
2854,
2855,
2856,
2857,
2900,
2901,
2902,
2903,
2950,
2951,
2952,
2953,
8001,
20000,
20001,
20002,
20004,
]);
+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),
};
}
+3 -2
View File
@@ -13,10 +13,11 @@ export function _team(res: CreateAutoTeamResponse | LoadTeamResponse) {
buf.writeInt16LE(0x0078, 0x0000);
}
const leader = res.members.find(item => item.leader);
const leader = res.members.find(item => item.leader === true);
buf.writeUInt32LE(res.team.extId, 0x000c);
iconv.encode(leader ? leader.profile.name : "Error\0", "shift_jis").copy(buf, 0x0010);
iconv.encode(res.team.name, "shift_jis").copy(buf, 0x0024);
iconv.encode(process.env.SHOP_NAME ? process.env.SHOP_NAME : "\0", "shift_jis").copy(buf, 0x0044);
buf.writeUInt32LE(res.team.nameBg, 0x00d8);
buf.writeUInt32LE(res.team.nameFx, 0x00dc);
buf.fill(0xff, 0x00e0, 0x00f9); // Bitset: Unlocked BGs probably
+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,
+1 -1
View File
@@ -10,6 +10,6 @@ export async function loadGarage(
return {
type: "load_garage_res",
cars: await w.car().loadAllCars(profileId),
cars: await w.car().loadCars(profileId, 10, req.fetchOffset),
};
}
+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;
+9 -3
View File
@@ -11,7 +11,11 @@ export type TeamSpec = Subtract<
export interface CarRepository {
countCars(profileId: Id<Model.Profile>): Promise<number>;
loadAllCars(profileId: Id<Model.Profile>): Promise<Model.Car[]>;
loadCars(
profileId: Id<Model.Profile>,
limit: number,
offset: number
): Promise<Model.Car[]>;
loadSelectedCar(profileId: Id<Model.Profile>): Promise<Model.Car>;
@@ -111,13 +115,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;
+1 -1
View File
@@ -21,7 +21,7 @@ export class SqlBackgroundsRepository
const result = new Set<BackgroundCode>();
for (const row of rows) {
result.add(parseInt(row.background_no) as BackgroundCode);
result.add(parseInt(row.background_no!) as BackgroundCode);
}
return result;
+25 -18
View File
@@ -8,21 +8,21 @@ import { Row, Transaction } from "../../sql";
function _extractRow(row: Row): Car {
return {
selector: parseInt(row.selector) as CarSelector,
field_00: parseInt(row.field_00),
field_02: parseInt(row.field_02),
field_04: row.field_04.split(",").map((x: string) => parseInt(x)),
field_46: parseInt(row.field_46),
field_48: parseInt(row.field_48),
field_4a: parseInt(row.field_4a),
field_4c: parseInt(row.field_4c),
field_50_lo: parseInt(row.field_50_lo),
field_50_hi: parseInt(row.field_50_hi),
field_58: parseInt(row.field_58),
field_5a: parseInt(row.field_5a),
field_5b: parseInt(row.field_5b),
field_5c: parseInt(row.field_5c),
field_5e: parseInt(row.field_5e),
selector: parseInt(row.selector!) as CarSelector,
field_00: parseInt(row.field_00!),
field_02: parseInt(row.field_02!),
field_04: row.field_04!.split(",").map((x: string) => parseInt(x)),
field_46: parseInt(row.field_46!),
field_48: parseInt(row.field_48!),
field_4a: parseInt(row.field_4a!),
field_4c: parseInt(row.field_4c!),
field_50_lo: parseInt(row.field_50_lo!),
field_50_hi: parseInt(row.field_50_hi!),
field_58: parseInt(row.field_58!),
field_5a: parseInt(row.field_5a!),
field_5b: parseInt(row.field_5b!),
field_5c: parseInt(row.field_5c!),
field_5e: parseInt(row.field_5e!),
};
}
@@ -37,14 +37,21 @@ export class SqlCarRepository implements CarRepository {
const row = await this._txn.fetchRow(countSql);
return parseInt(row!.result);
return parseInt(row!.result!);
}
async loadAllCars(profileId: Id<Profile>): Promise<Car[]> {
async loadCars(
profileId: Id<Profile>,
limit: number,
offset: number
): Promise<Car[]> {
const loadSql = sql
.select("c.*")
.from("idz_car c")
.where("c.profile_id", profileId);
.where("c.profile_id", profileId)
.orderBy("selector asc")
.limit(limit)
.offset(offset);
const rows = await this._txn.fetchRows(loadSql);
+10 -10
View File
@@ -9,16 +9,16 @@ import { Row, Transaction } from "../../sql";
export function _extractChara(row: Row): Chara {
return {
gender: row.gender as Gender,
field_02: parseInt(row.field_02),
field_04: parseInt(row.field_04),
field_06: parseInt(row.field_06),
field_08: parseInt(row.field_08),
field_0a: parseInt(row.field_0a),
field_0c: parseInt(row.field_0c),
field_0e: parseInt(row.field_0e),
title: parseInt(row.title) as TitleCode,
background: parseInt(row.background) as BackgroundCode,
gender: row.gender! as Gender,
field_02: parseInt(row.field_02!),
field_04: parseInt(row.field_04!),
field_06: parseInt(row.field_06!),
field_08: parseInt(row.field_08!),
field_0a: parseInt(row.field_0a!),
field_0c: parseInt(row.field_0c!),
field_0e: parseInt(row.field_0e!),
title: parseInt(row.title!) as TitleCode,
background: parseInt(row.background!) as BackgroundCode,
};
}
+2 -2
View File
@@ -19,8 +19,8 @@ export class SqlCoursePlaysRepository implements CoursePlaysRepository {
const result = new Map<CourseNo, number>();
for (const row of rows) {
const courseNo = parseInt(row.course_no) as CourseNo;
const count = parseInt(row.count);
const courseNo = parseInt(row.course_no!) as CourseNo;
const count = parseInt(row.count!);
result.set(courseNo, count);
}
+3 -3
View File
@@ -36,9 +36,9 @@ export class SqlMissionsRepository implements FacetRepository<MissionState> {
const rows = await this._txn.fetchRows(loadSoloSql);
for (const row of rows) {
const gridNo = parseInt(row.grid_no);
const cellNo = parseInt(row.cell_no);
const value = parseInt(row.value);
const gridNo = parseInt(row.grid_no!);
const cellNo = parseInt(row.cell_no!);
const value = parseInt(row.value!);
result.solo[gridNo].cells[cellNo] = value;
}
+9 -9
View File
@@ -7,15 +7,15 @@ import { Row, Transaction } from "../../sql";
export function _extractProfile(row: Row): Profile {
return {
aimeId: parseInt(row.aime_id) as AimeId,
name: row.name,
lv: parseInt(row.lv),
exp: parseInt(row.exp),
fame: parseInt(row.fame),
dpoint: parseInt(row.dpoint),
mileage: parseInt(row.mileage),
accessTime: new Date(row.access_time),
registerTime: new Date(row.register_time),
aimeId: parseInt(row.aime_id!) as AimeId,
name: row.name!,
lv: parseInt(row.lv!),
exp: parseInt(row.exp!),
fame: parseInt(row.fame!),
dpoint: parseInt(row.dpoint!),
mileage: parseInt(row.mileage!),
accessTime: new Date(row.access_time!),
registerTime: new Date(row.register_time!),
};
}
+7 -5
View File
@@ -22,10 +22,11 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
}
return {
music: parseInt(row.music),
pack: parseInt(row.pack),
paperCup: parseInt(row.paper_cup),
gauges: parseInt(row.gauges),
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);
}
+6 -6
View File
@@ -20,8 +20,8 @@ export class SqlStoryRepository implements FacetRepository<Story> {
// Must succeed even if nonexistent (required by save method below)
const result = {
x: header !== undefined ? parseInt(header.x) : 0,
y: header !== undefined ? parseInt(header.y) : 0,
x: header !== undefined ? parseInt(header.x!) : 0,
y: header !== undefined ? parseInt(header.y!) : 0,
rows: new Array<StoryRow>(),
};
@@ -43,12 +43,12 @@ export class SqlStoryRepository implements FacetRepository<Story> {
const rows = await this._txn.fetchRows(loadCellSql);
for (const row of rows) {
const rowNo = parseInt(row.row_no);
const colNo = parseInt(row.col_no);
const rowNo = parseInt(row.row_no!);
const colNo = parseInt(row.col_no!);
const cell = result.rows[rowNo].cells[colNo];
cell.a = parseInt(row.a);
cell.b = parseInt(row.b);
cell.a = parseInt(row.a!);
cell.b = parseInt(row.b!);
}
return result;
+5 -5
View File
@@ -37,11 +37,11 @@ export class SqlTeamRepository implements TeamRepository {
}
return {
extId: parseInt(row.ext_id) as ExtId<Team>,
name: row.name,
nameBg: parseInt(row.name_bg),
nameFx: parseInt(row.name_fx),
registerTime: new Date(row.register_time),
extId: parseInt(row.ext_id!) as ExtId<Team>,
name: row.name!,
nameBg: parseInt(row.name_bg!),
nameFx: parseInt(row.name_fx!),
registerTime: new Date(row.register_time!),
};
}
+2 -2
View File
@@ -20,8 +20,8 @@ export class SqlTeamAutoRepository implements TeamAutoRepository {
return (
row && [
{
serialNo: parseInt(row.serial_no),
nameIdx: parseInt(row.name_idx),
serialNo: parseInt(row.serial_no!),
nameIdx: parseInt(row.name_idx!),
},
row.id as Id<Team>,
]
+3 -3
View File
@@ -56,8 +56,8 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
return rows.map(row => ({
profile: _extractProfile(row),
chara: _extractChara(row),
leader: !!row.leader,
joinTime: new Date(row.join_time),
leader: row.leader === "true",
joinTime: new Date(row.join_time!),
}));
}
@@ -92,7 +92,7 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
const row = await this._txn.fetchRow(countSql);
if (parseInt(row!.count) >= 6) {
if (parseInt(row!.count!) >= 6) {
throw new Error(`Team ${teamId} is full`);
}
+2 -2
View File
@@ -28,7 +28,7 @@ export class SqlTeamReservationRepository
.where("tm.team_id", teamId);
const memberRes = await this._txn.fetchRow(memberSql);
const memberCount = parseInt(memberRes!.count);
const memberCount = parseInt(memberRes!.count!);
const reservSql = sql
.select("count(*) as count")
@@ -36,7 +36,7 @@ export class SqlTeamReservationRepository
.where("tr.team_id", teamId);
const reservRes = await this._txn.fetchRow(reservSql);
const reservCount = parseInt(reservRes!.count);
const reservCount = parseInt(reservRes!.count!);
return memberCount + reservCount;
}
+1 -1
View File
@@ -21,7 +21,7 @@ export class SqlTicketsRepository implements FacetRepository<Tickets> {
return {
freeCar: row && {
validFrom: new Date(row.valid_from),
validFrom: new Date(row.valid_from!),
},
};
}
+43 -14
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";
@@ -10,36 +11,64 @@ import { Row, Transaction } from "../../sql";
function _extractRow(row: Row): TimeAttackScore {
return {
routeNo: parseInt(row.route_no) as RouteNo,
timestamp: new Date(row.timestamp),
flags: parseInt(row.flags),
totalTime: parseFloat(row.total_time),
sectionTimes: row.section_times.split(",").map(parseFloat),
grade: parseInt(row.grade),
carSelector: parseInt(row.car_selector) as CarSelector,
routeNo: parseInt(row.route_no!) as RouteNo,
timestamp: new Date(row.timestamp!),
flags: parseInt(row.flags!),
totalTime: parseFloat(row.total_time!),
sectionTimes: row.section_times!.split(",").map(parseFloat),
grade: parseInt(row.grade!),
carSelector: parseInt(row.car_selector!) as CarSelector,
};
}
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),
}));
}
@@ -93,7 +122,7 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
await this._txn.modify(insertSql);
} else {
if (score.totalTime < parseFloat(row.total_time)) {
if (score.totalTime < parseFloat(row.total_time!)) {
const updateSql = sql
.update("idz_ta_best", {
total_time: score.totalTime,
+1 -1
View File
@@ -19,7 +19,7 @@ export class SqlTitlesRepository implements FlagRepository<TitleCode> {
const result = new Set<TitleCode>();
for (const row of rows) {
result.add(parseInt(row.title_no) as TitleCode);
result.add(parseInt(row.title_no!) as TitleCode);
}
return result;
+7 -5
View File
@@ -22,10 +22,11 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
}
return {
cup: parseInt(row.cup),
gauges: parseInt(row.gauges),
music: parseInt(row.music),
lastMileageReward: parseInt(row.last_mileage_reward),
auras: parseInt(row.auras!),
cup: parseInt(row.cup!),
gauges: parseInt(row.gauges!),
music: parseInt(row.music!),
lastMileageReward: parseInt(row.last_mileage_reward!),
};
}
@@ -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);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import * as sql from "sql-bricks-postgres";
import { Id } from "../model";
export interface Row {
[key: string]: string;
[key: string]: string | null;
}
export interface Transaction {
+38 -21
View File
@@ -1,14 +1,10 @@
import Database from "better-sqlite3";
import Database, { ResultRow } from "@decafcode/sqlite";
import { randomBytes } from "crypto";
import * as sql from "sql-bricks-postgres";
import { DataSource, Row, Transaction } from "./api";
import { Id } from "../model";
type MixedRow = {
[key: string]: any;
};
// bless me father for i have sinned
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 = {};
for (const [k, v] of Object.entries(obj)) {
@@ -58,7 +54,7 @@ function _postprocess(obj: MixedRow): Row {
}
class SqliteTransaction implements Transaction {
constructor(private readonly _db: Database.Database) {}
constructor(private readonly _db: Database) {}
generateId<T>(): Id<T> {
const buf = randomBytes(8);
@@ -73,26 +69,43 @@ class SqliteTransaction implements Transaction {
modify(stmt: sql.Statement): Promise<void> {
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();
}
fetchRow(stmt: sql.SelectStatement): Promise<Row | undefined> {
const params = _preprocess(stmt);
const raw = this._db.prepare(params.text).get(...params.values);
const result = raw && _postprocess(raw);
const prepared = this._db.prepare(params.text);
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[]> {
const params = _preprocess(stmt);
const raw = this._db.prepare(params.text).all(...params.values);
const result = raw.map(_postprocess);
const prepared = this._db.prepare(params.text);
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> {
@@ -110,28 +123,32 @@ class SqliteDataSource implements DataSource {
): Promise<T> {
const db = new Database(this._path);
db.defaultSafeIntegers();
db.prepare("pragma foreign_keys = on").run();
db.prepare("begin").run();
try {
db.exec("begin");
const txn = new SqliteTransaction(db);
const result = await callback(txn);
db.prepare("commit").run();
db.exec("commit");
return result;
} catch (e) {
db.prepare("rollback").run();
db.exec("rollback");
return Promise.reject(e);
} finally {
db.close();
}
}
vacuum(): Promise<void> {
const db = new Database(this._path);
db.prepare("vacuum").run();
try {
db.exec("vacuum");
} finally {
db.close();
}
return Promise.resolve();
}
+21 -7
View File
@@ -3,15 +3,23 @@ import snakeCase from "snake-case";
import { Row } from "./api";
interface ColMapper<F> {
_read(str: string): F;
_read(str: string | null): F;
_write(val: F): string;
_write(val: F): string | null;
}
type Spec<R> = {
[K in keyof R]: ColMapper<R[K]>;
};
function _nn(str: string | null): string {
if (str === null) {
throw new Error("Unexpected NULL returned from database");
}
return str;
}
/**
* Function objects describing the precise way in which our SQL driver
* transmits and receives values to the database as strings. Note that we could
@@ -20,25 +28,31 @@ type Spec<R> = {
*/
export const T = {
bigint: {
_read: (str: string) => BigInt(str),
_read: (str: string) => BigInt(_nn(str)),
_write: (val: bigint) => val.toString(),
},
boolean: {
_read: (str: string) => str === "true",
_read: (str: string) => _nn(str) === "true",
_write: (val: boolean) => val.toString(),
},
number: {
_read: (str: string) => parseInt(str),
_read: (str: string) => parseInt(_nn(str)),
_write: (val: number) => val.toString(),
},
string: {
_read: (str: string) => str,
_read: (str: string) => _nn(str),
_write: (val: string) => val,
},
Date: {
_read: (str: string) => new Date(str),
_read: (str: string) => new Date(_nn(str)),
_write: (val: Date) => val.toISOString(),
},
nullable: <F>(inner: ColMapper<F>) => ({
_read: (str: string | null) =>
str !== null ? inner._read(str) : undefined,
_write: (val: F | undefined) =>
val !== undefined ? inner._write(val) : null,
}),
};
/**
-3
View File
@@ -22,9 +22,6 @@ if not exist node_modules (
echo The server's dependencies will now be installed.
echo This process requires an internet connection and may take some time.
echo.
echo NOTE: If install fails try running the following command as admin:
echo npm install -g windows-build-tools
echo.
pause
call npm install
+1 -1
View File
@@ -7,7 +7,7 @@ declare module "sql-bricks-postgres" {
}
interface LimitClause extends sql.SelectStatement {
offset(value: number): sql.Statement;
offset(value: number): sql.SelectStatement;
}
export interface PgInsertStatement extends sql.InsertStatement {