Compare commits

..

No commits in common. "master" and "v008" have entirely different histories.
master ... v008

407 changed files with 4102 additions and 10420 deletions

View File

@ -1,16 +0,0 @@
image: node:12-buster
cache:
paths:
- node_modules/
stages:
- build
do_build:
stage: build
script:
- npm install
- npm run build
- npm test

View File

@ -1,8 +1,5 @@
{
"editor.formatOnSave": true,
"editor.rulers": [79],
"typescript.tsdk": "node_modules/typescript/lib",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -1,70 +0,0 @@
# CHANGELOG
## v016
- IDZ: Fix SQL syntax error (Tau)
## v015
- IDZ: Team fixes (BemaniWitch)
## v014
- IDZ: Add support for Initial D v2.1 (BemaniWitch)
## v013
- IDZ: Miscellaneous refactoring (Tau)
- Chunithm: Add stubs to support Amazon Plus (Felix)
## v012
- Chunithm: Fix large data loads (NeumPhis)
- Chunithm: Add some support for older versions (NeumPhis)
- Chunithm: Fix large data loads (Felix)
- Chunithm: Enable all events (Felix)
- Chunithm: Remove server maintenance period (Felix)
- 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

5913
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"main": "src/index.js",
"private": true,
"dependencies": {
"@decafcode/sqlite": "^2.0.0",
"better-sqlite3": "^5.4.3",
"compression": "^1.7.3",
"date-fns": "^1.30.1",
"debug": "^4.1.1",
@ -20,6 +20,7 @@
"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",
@ -27,12 +28,12 @@
"@types/jest": "^24.0.11",
"@types/multiparty": "^0.0.32",
"@types/node": "^12.11.7",
"jest": "^26.4.2",
"jest-haste-map": "^26.3.0",
"jest": "^24.5.0",
"jest-haste-map": "^24.5.0",
"jest-resolve": "^24.5.0",
"prettier": "^1.16.4",
"ts-jest": "^26.3.0",
"typescript": "^3.8.3",
"ts-jest": "^24.0.0",
"typescript": "^3.6.4",
"utility-types": "^3.6.1"
},
"scripts": {

View File

@ -82,44 +82,6 @@ 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")
@ -160,23 +122,6 @@ 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
@ -357,16 +302,3 @@ 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")
);

View File

@ -7,10 +7,6 @@ create table "idz_profile" (
"player_id" integer not null
references "aime_player"("id")
on delete cascade,
-- Major version of Initial D Zero, either 1 or 2.
-- The two major versions are incompatible with each other and do not
-- permit the player to carry progress over from one to the other.
"version" integer not null,
-- TODO shop_id
"name" text not null,
"lv" integer not null,
@ -20,7 +16,7 @@ create table "idz_profile" (
"mileage" integer not null,
"register_time" timestamp not null,
"access_time" timestamp not null,
constraint "idz_profile_player_uq" unique ("player_id", "version")
constraint "idz_profile_player_uq" unique ("player_id")
);
create table "idz_chara" (
@ -104,27 +100,7 @@ create table "idz_settings" (
"pack" integer not null,
"aura" integer not null,
"paper_cup" integer not null, -- Not a boolean, oddly enough
"gauges" integer not null,
"driving_style" integer not null
);
create table "idz_stamp_selections" (
"id" integer primary key not null
references "idz_profile"("id")
on delete cascade,
"stamp_01" integer not null,
"stamp_02" integer not null,
"stamp_03" integer not null,
"stamp_04" integer not null
);
create table "idz_stamp_unlock" (
"id" integer primary key not null,
"profile_id" integer not null
references "idz_profile"("id")
on delete cascade,
"stamp_no",
constraint "idz_stamp_unlock_uq" unique ("profile_id", "stamp_no")
"gauges" integer not null
);
create table "idz_story_state" (
@ -144,7 +120,6 @@ create table "idz_story_cell_state" (
"col_no" integer not null,
"a" integer not null,
"b" integer not null,
"c" integer not null,
constraint "idz_story_cell_state_uq" unique (
"profile_id",
"row_no",
@ -227,13 +202,12 @@ create table "idz_unlocks" (
create table "idz_team" (
"id" integer primary key not null,
"version" integer not null,
"ext_id" integer not null,
"name" text not null,
"name_bg" integer not null,
"name_fx" integer not null,
"register_time" timestamp not null,
constraint "idz_team_uq" unique ("version", "ext_id")
constraint "idz_team_uq" unique ("ext_id")
);
create table "idz_team_auto" (
@ -241,7 +215,8 @@ create table "idz_team_auto" (
references "idz_team"("id")
on delete cascade,
"serial_no" integer not null,
"name_idx" integer not null
"name_idx" integer not null,
constraint "idz_team_auto_uq" unique ("serial_no", "name_idx")
);
create table "idz_team_member" (
@ -265,25 +240,3 @@ create table "idz_team_reservation" (
"join_time" timestamp not null,
"leader" boolean not null
);
create table "idz_weekly_missions" (
"id" integer primary key not null
references "idz_profile"("id")
on delete cascade,
"weekly_reset" timestamp not null,
"mission_left" integer not null,
"progress_left" integer not null,
"params_left" integer not null,
"mission_right" integer not null,
"progress_right" integer not null,
"params_right" integer not null
);
create table "idz_my_chara" (
"id" integer primary key not null,
"profile_id" integer not null
references "idz_profile"("id")
on delete cascade,
"my_chara_no" integer not null,
constraint "idz_my_chara_uq" unique ("profile_id", "my_chara_no")
);

View File

@ -1,23 +0,0 @@
-- 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")
);

View File

@ -1,17 +0,0 @@
-- 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")
);

View File

@ -1,14 +0,0 @@
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")
);

View File

@ -1,31 +0,0 @@
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;

View File

@ -1,77 +0,0 @@
create table "new_idz_profile" (
"id" integer primary key not null,
"player_id" integer not null
references "aime_player"("id")
on delete cascade,
"version" integer not null,
"name" text not null,
"lv" integer not null,
"exp" integer not null,
"fame" integer not null,
"dpoint" integer not null,
"mileage" integer not null,
"register_time" timestamp not null,
"access_time" timestamp not null,
constraint "idz_profile_player_uq" unique ("player_id", "version")
);
create table "new_idz_team" (
"id" integer primary key not null,
"version" integer not null,
"ext_id" integer not null,
"name" text not null,
"name_bg" integer not null,
"name_fx" integer not null,
"register_time" timestamp not null,
constraint "idz_team_uq" unique ("version", "ext_id")
);
insert into "new_idz_profile" (
"id",
"player_id",
"version",
"name",
"lv",
"exp",
"fame",
"dpoint",
"mileage",
"register_time",
"access_time"
) select
x."id",
x."player_id",
1,
x."name",
x."lv",
x."exp",
x."fame",
x."dpoint",
x."mileage",
x."register_time",
x."access_time"
from "idz_profile" as "x";
insert into "new_idz_team" (
"id",
"version",
"ext_id",
"name",
"name_bg",
"name_fx",
"register_time"
) select
x."id",
1,
x."ext_id",
x."name",
x."name_bg",
x."name_fx",
x."register_time"
from "idz_team" as "x";
drop table "idz_profile";
drop table "idz_team";
alter table "new_idz_profile" rename to "idz_profile";
alter table "new_idz_team" rename to "idz_team";

View File

@ -1,111 +0,0 @@
create table "idz_my_chara" (
"id" integer primary key not null,
"profile_id" integer not null
references "idz_profile"("id")
on delete cascade,
"my_chara_no" integer not null,
constraint "idz_my_chara_uq" unique ("profile_id", "my_chara_no")
);
create table "new_idz_settings" (
"id" integer primary key not null
references "idz_profile"("id")
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,
"driving_style" integer not null
);
create table "idz_stamp_selections" (
"id" integer primary key not null
references "idz_profile"("id")
on delete cascade,
"stamp_01" integer not null,
"stamp_02" integer not null,
"stamp_03" integer not null,
"stamp_04" integer not null
);
create table "idz_stamp_unlock" (
"id" integer primary key not null,
"profile_id" integer not null
references "idz_profile"("id")
on delete cascade,
"stamp_no",
constraint "idz_stamp_unlock_uq" unique ("profile_id", "stamp_no")
);
create table "new_idz_story_cell_state" (
"id" integer primary key not null,
"profile_id" integer not null
references "idz_profile"("id")
on delete cascade,
"row_no" integer not null,
"col_no" integer not null,
"a" integer not null,
"b" integer not null,
"c" integer not null,
constraint "idz_story_cell_state_uq" unique (
"profile_id",
"row_no",
"col_no"
)
);
create table "idz_weekly_missions" (
"id" integer primary key not null
references "idz_profile"("id")
on delete cascade,
"weekly_reset" timestamp not null,
"mission_left" integer not null,
"progress_left" integer not null,
"params_left" integer not null,
"mission_right" integer not null,
"progress_right" integer not null,
"params_right" integer not null
);
insert into "new_idz_settings" (
"id",
"music",
"pack",
"aura",
"paper_cup",
"gauges",
"driving_style"
) select
x."id",
x."music",
x."pack",
x."aura",
x."paper_cup",
x."gauges",
0
from "idz_settings" as x;
insert into "new_idz_story_cell_state" (
"id",
"profile_id",
"row_no",
"col_no",
"a",
"b",
"c"
) select
x."id",
x."profile_id",
x."row_no",
x."col_no",
x."a",
x."b",
0
from "idz_story_cell_state" as x;
drop table "idz_settings";
drop table "idz_story_cell_state";
alter table "new_idz_settings" rename to "idz_settings";
alter table "new_idz_story_cell_state" rename to "idz_story_cell_state";

View File

@ -1,13 +0,0 @@
create table "new_idz_team_auto" (
"id" integer primary key not null
references "idz_team"("id")
on delete cascade,
"serial_no" integer not null,
"name_idx" integer not null
);
insert into "new_idz_team_auto"
select "id", "serial_no", "name_idx" from "idz_team_auto";
drop table "idz_team_auto";
alter table "new_idz_team_auto" rename to "idz_team_auto";

View File

@ -105,7 +105,7 @@ test("decode lookup", () => {
const obj = decode(req);
expect(obj.type).toBe("lookup2");
expect(obj.type).toBe("lookup");
expect(obj.gameId).toBe("SDBT");
expect(obj.keychipId).toBe("A69E01A9999");
expect(obj.luid).toBe("01036495850523030676");

View File

@ -31,15 +31,6 @@ 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.
@ -107,7 +98,6 @@ 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);

View File

@ -14,7 +14,7 @@ registerLevels.set("segaid", 2);
function begin(length: number) {
const buf = Buffer.alloc(length);
buf.writeUInt16LE(0xa13e, 0x0000); // Magic: aime
buf.writeUInt16LE(0xa13e, 0x0000); // Magic?
buf.writeUInt16LE(0x3087, 0x0002); // ???
buf.writeUInt16LE(length, 0x0006);
@ -43,18 +43,6 @@ 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

View File

@ -46,28 +46,6 @@ 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,
@ -137,9 +115,6 @@ 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);

View File

@ -9,12 +9,6 @@ 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;
@ -56,7 +50,6 @@ export interface GoodbyeRequest {
export type AimeRequest =
| FeliCaLookupRequest
| FeliCaLookup2Request
| CampaignRequest
| GoodbyeRequest
| HelloRequest

View File

@ -11,12 +11,6 @@ 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";
}
@ -50,7 +44,6 @@ export interface RegisterResponse extends AimeResponseBase {
export type AimeResponse =
| FeliCaLookupResponse
| FeliCaLookup2Response
| CampaignResponse
| HelloResponse
| LogResponse

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")

View File

@ -104,32 +104,30 @@ async function migratedb(
}
export default async function checkdb(db: DataSource): Promise<void> {
let schemaver: number | undefined;
const stmt = sql.select("schemaver").from("meta");
let maybe: number | undefined;
await db.maintenance(async txn => {
const stmt = sql.select("schemaver").from("meta");
try {
const row = await db.transaction(txn => txn.fetchRow(stmt));
try {
const row = await txn.fetchRow(stmt);
if (row !== undefined) {
schemaver = parseInt(row.schemaver!);
}
} catch (e) {
return await initdb(txn);
if (row !== undefined) {
maybe = parseInt(row.schemaver);
}
} catch (e) {
return db.transaction(initdb);
}
if (schemaver === undefined) {
throw new Error(
"Database corrupted: `meta` table singleton row is missing"
);
}
if (maybe === undefined) {
throw new Error(
"Database corrupted: `meta` table singleton row is missing"
);
}
schemaver = await migratedb(txn, schemaver);
});
const schemaver = maybe;
const newver = await db.transaction(txn => migratedb(txn, schemaver));
if (schemaver !== undefined) {
debug("Upgraded database to version %s", schemaver);
if (newver !== undefined) {
debug("Upgraded database to version %s", newver);
await db.vacuum();

View File

@ -1,30 +1,13 @@
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: gameChargeList.length.toString(),
gameChargeList,
length: "0",
gameChargeList: [],
};
}

View File

@ -1,27 +1,14 @@
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: gameEventList.length.toString(),
gameEventList,
length: "0",
gameEventList: [],
};
}

View File

@ -1,4 +1,3 @@
import { writeDate } from "../proto/base";
import { Repositories } from "../repo";
import { GetGameSettingRequest } from "../request/getGameSetting";
import { GetGameSettingResponse } from "../response/getGameSetting";
@ -7,25 +6,19 @@ 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: writeDate(rebootStartTime),
rebootEndTime: writeDate(rebootEndTime),
rebootStartTime: "0",
rebootEndTime: "0",
isBackgroundDistribute: "false",
maxCountCharacter: "300",
maxCountItem: "300",
maxCountMusic: "100",
maxCountCharacter: "999",
maxCountItem: "999",
maxCountMusic: "999",
},
isDumpUpload: "false",
isAou: "true",
isAou: "false",
};
}

View File

@ -1,5 +1,3 @@
import { readAimeId } from "../proto/base";
import { writeUserCharge } from "../proto/userCharge";
import { Repositories } from "../repo";
import { GetUserChargeRequest } from "../request/getUserCharge";
import { GetUserChargeResponse } from "../response/getUserCharge";
@ -8,14 +6,9 @@ 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: items.length.toString(),
userChargeList: items.map(writeUserCharge),
length: "0",
userChargeList: [],
};
}

View File

@ -18,6 +18,7 @@ export default async function getUserCourse(
.userCourse()
.load(profileId, { limit: maxCount, offset: nextIndex });
return {
userId: req.userId,
length: items.length.toString(),

View File

@ -1,23 +0,0 @@
import { Repositories } from "../repo";
import { GetUserFavoriteMusicRequest } from "../request/getUserFavoriteMusic";
import { GetUserFavoriteMusicResponse } from "../response/getUserFavoriteMusic";
import { readAimeId } from "../proto/base";
export default async function getUserFavoriteMusic(
rep: Repositories,
req: GetUserFavoriteMusicRequest
): Promise<GetUserFavoriteMusicResponse> {
const aimeId = readAimeId(req.userId);
const profileId = await rep.userData().lookup(aimeId);
/*
* `Chunithm Amazon Plus` does not appear to save a favorites list and there
* is no user-accessible favorites function from what I can tell.
*/
return {
userId: req.userId,
length: "0",
userFavoriteMusicList: [],
};
}

View File

@ -47,7 +47,7 @@ export default async function getUserItem(
// Pack the next pagination cookie into bigint compound form
const xout = itemKindN * itemKindMul + nextIndexN + BigInt(items.length);
const xout = itemKindN * itemKindMul + BigInt(maxCount + items.length);
// Done:

View File

@ -2,7 +2,7 @@ import { Repositories } from "../repo";
import { GetUserRecentRatingRequest } from "../request/getUserRecentRating";
import { GetUserRecentRatingResponse } from "../response/getUserRecentRating";
import { readAimeId } from "../proto/base";
import { writeUserRecentRating } from "../proto/userRecentRating";
import { writeUserRecentRatingFromLog } from "../proto/userRecentRating";
export default async function getUserRecentRating(
rep: Repositories,
@ -12,11 +12,11 @@ export default async function getUserRecentRating(
const profileId = await rep.userData().lookup(aimeId);
// Return recent 30 plays to calculate rating
const items = await rep.userRecentRating().load(profileId);
const items = await rep.userPlaylog().loadLatest(profileId, 30);
return {
userId: req.userId,
length: items.length.toString(),
userRecentRatingList: items.map(writeUserRecentRating),
userRecentRatingList: items.map(writeUserRecentRatingFromLog),
};
}

View File

@ -18,7 +18,6 @@ import getUserCourse from "./getUserCourse";
import getUserData from "./getUserData";
import getUserDataEx from "./getUserDataEx";
import getUserDuel from "./getUserDuel";
import getUserFavoriteMusic from "./getUserFavoriteMusic";
import getUserItem from "./getUserItem";
import getUserMap from "./getUserMap";
import getUserMusic from "./getUserMusic";
@ -33,7 +32,6 @@ 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";
@ -100,14 +98,12 @@ export default function chunithm(db: DataSource) {
wrapper.rpc("/GetUserDataApi", getUserData);
wrapper.rpc("/GetUserDataExApi", getUserDataEx);
wrapper.rpc("/GetUserDuelApi", getUserDuel);
wrapper.rpc("/GetUserFavoriteMusicApi", getUserFavoriteMusic);
wrapper.rpc("/GetUserItemApi", getUserItem);
wrapper.rpc("/GetUserMapApi", getUserMap);
wrapper.rpc("/GetUserMusicApi", getUserMusic);
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);
@ -116,7 +112,6 @@ 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();

View File

@ -13,8 +13,6 @@ 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
@ -69,10 +67,6 @@ 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));
}
@ -85,12 +79,5 @@ export default async function upsertUserAll(
await rep.userDuelList().save(profileId, readUserDuelList(item));
}
await rep
.userRecentRating()
.save(
profileId,
(payload.userRecentRatingList || []).map(readUserRecentRating)
);
return { returnCode: "1" };
}

View File

@ -1,12 +0,0 @@
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",
};
}

View File

@ -1,9 +0,0 @@
export interface UserChargeItem {
chargeId: number;
stock: number;
purchaseDate: Date;
validDate: Date;
param1: number;
param2: number;
paramDate: Date;
}

View File

@ -1,20 +0,0 @@
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);
}

View File

@ -20,3 +20,15 @@ export function writeUserRecentRating(
): UserRecentRatingJson {
return writeObject(obj);
}
export function writeUserRecentRatingFromLog(
obj: UserPlaylogItem
): UserRecentRatingJson {
return {
musicId: obj.musicId.toString(),
difficultId: obj.level.toString(),
// game version not saved in play log, just return a fixed version now
romVersionCode: "1030000",
score: obj.score.toString(),
};
}

View File

@ -1,8 +1,9 @@
import { UserCourseRepository } from "./userCourse";
export { Page } from "./_defs";
import { UserActivityRepository } from "./userActivity";
import { UserCharacterRepository } from "./userCharacter";
import { UserCourseRepository } from "./userCourse";
import { UserDataRepository } from "./userData";
import { UserDataExRepository } from "./userDataEx";
import { UserDuelListRepository } from "./userDuelList";
@ -12,16 +13,12 @@ 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;
@ -40,9 +37,5 @@ export interface Repositories {
userPlaylog(): UserPlaylogRepository;
userCharge(): UserChargeRepository;
userCourse(): UserCourseRepository;
userRecentRating(): UserRecentRatingRepository;
}

View File

@ -1,4 +0,0 @@
import { UserChargeItem } from "../model/userCharge";
import { RepositoryN } from "./_defs";
export type UserChargeRepository = RepositoryN<UserChargeItem>;

View File

@ -1,12 +0,0 @@
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>;
}

View File

@ -1,4 +0,0 @@
export interface GetUserFavoriteMusicRequest {
/** Integer, AiMe ID */
userId: string;
}

View File

@ -8,7 +8,6 @@ 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";
@ -28,7 +27,6 @@ export interface UpsertUserAllRequest {
userActivityList?: UserActivityJson[];
userRecentRatingList?: UserRecentRatingJson[];
userPlaylogList?: UserPlaylogJson[];
userChargeList?: UserChargeJson[];
userCourseList?: UserCourseJson[];
userDataEx?: UserDataExJson[];
userDuelList?: UserDuelListJson[];

View File

@ -1,24 +0,0 @@
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
};
}

View File

@ -1,5 +1,3 @@
import { UserChargeJson } from "../proto/userCharge";
export interface GetUserChargeResponse {
/** Integer, AiMe ID */
userId: string;
@ -7,5 +5,6 @@ export interface GetUserChargeResponse {
/** Integer, number of results returned */
length: string;
userChargeList: UserChargeJson[];
/** TBD */
userChargeList: [];
}

View File

@ -1,10 +0,0 @@
export interface GetUserFavoriteMusicResponse {
/** Integer, AiMe ID */
userId: string;
/** Integer, number of results returned */
length: string;
/** TBD */
userFavoriteMusicList: [];
}

View File

@ -1,3 +0,0 @@
export interface UpsertUserChargelogApiResponse {
returnCode: string;
}

View File

@ -1,7 +1,5 @@
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";
@ -11,12 +9,9 @@ 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";
@ -26,8 +21,9 @@ 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";
import { UserCourseRepository } from "../repo/userCourse";
import { SqlUserCourseRepository } from "./userCourse";
export class SqlRepositories implements Repositories {
constructor(private readonly _txn: Transaction) {}
@ -40,14 +36,6 @@ 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);
}
@ -84,7 +72,7 @@ export class SqlRepositories implements Repositories {
return new SqlUserPlaylogRepository(this._txn);
}
userRecentRating(): UserRecentRatingRepository {
return new SqlUserRecentRatingRepository(this._txn);
userCourse(): UserCourseRepository {
return new SqlUserCourseRepository(this._txn);
}
}

View File

@ -28,9 +28,7 @@ export class SqlUserActivityRepository implements UserActivityRepository {
.select("*")
.from("cm_user_activity")
.where("profile_id", profileId)
.where("kind", kind)
.orderBy("sort_number DESC")
.limit(100);
.where("kind", kind);
const rows = await this._txn.fetchRows(stmt);

View File

@ -1,47 +0,0 @@
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);
}
}

View File

@ -37,10 +37,7 @@ export class SqlUserCourseRepository implements UserCourseRepository {
.from("cm_user_course")
.where("profile_id", profileId);
/**
* UserCourse has no paging before CHUNITHM Amazon
*/
if (page && !isNaN(page.limit)) {
if (page) {
stmt.limit(page.limit).offset(page.offset);
}

View File

@ -56,47 +56,7 @@ export class SqlUserDataExRepository implements UserDataExRepository {
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
/**
* 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,
};
throw new Error("UserDataEx record not found");
}
return readRow(row);

View File

@ -34,22 +34,14 @@ 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)
.and(sql.in('music_id', musicIds));
.where("profile_id", profileId);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
const rows = await this._txn.fetchRows(stmt);

View File

@ -1,60 +0,0 @@
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);
}
}

View File

@ -1,17 +0,0 @@
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,
},
]);

View File

@ -1,854 +0,0 @@
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,
]);

View File

@ -1,17 +1,4 @@
export function readBigInt(buf: Buffer) {
let result = 0n;
for (let i = 0; i < buf.length; i++) {
const shift = 8n * BigInt(i);
const byte = buf.readUInt8(i);
result |= BigInt(byte) << shift;
}
return result;
}
export function writeBigInt(n: bigint, length: number) {
export function byteString(n: bigint, length: number) {
const result = Buffer.alloc(length);
for (let i = 0; i < length; i++) {

View File

@ -1,43 +0,0 @@
import { Decipher, Cipher, createCipheriv, createDecipheriv } from "crypto";
import { ByteStream } from "../../util/stream";
export const BLOCK_SIZE = 16;
export default class AesEcbStream implements ByteStream {
private readonly _dec: Decipher;
private readonly _enc: Cipher;
constructor(private _stm: ByteStream, keybuf: Buffer) {
this._dec = createDecipheriv("aes-128-ecb", keybuf, null).setAutoPadding(
false
);
this._enc = createCipheriv("aes-128-ecb", keybuf, null).setAutoPadding(
false
);
}
close() {
this._stm.close();
}
async read(nbytes: number): Promise<Buffer> {
if (nbytes % BLOCK_SIZE !== 0) {
throw new Error("Attempted to read partial cipher block");
}
const ciphertext = await this._stm.read(nbytes);
return this._dec.update(ciphertext);
}
async write(buf: Buffer): Promise<void> {
if (buf.length % BLOCK_SIZE !== 0) {
throw new Error("Attempted to write partial cipher block");
}
const ciphertext = this._enc.update(buf);
await this._stm.write(ciphertext);
}
}

View File

@ -1,5 +0,0 @@
import setup from "./setup";
export { BLOCK_SIZE } from "./aes";
export { ClientHello } from "./setup";
export default setup;

View File

@ -1,87 +0,0 @@
import { Socket } from "net";
import AesEcbStream from "./aes";
import { readBigInt, modPow, writeBigInt } from "./bigint";
import IoByteStream, { ByteStream } from "../../util/stream";
interface RsaKey {
N: bigint;
d: bigint;
e: bigint;
hashN: number;
}
export interface ClientHello {
pcbId: string;
protocol: string;
model: string;
}
export interface IdzConnection {
aesStream: ByteStream;
clientHello: ClientHello;
}
// Proof-of-concept, so we only ever use one of the ten RSA key pairs
const rsaKey = {
N: 4922323266120814292574970172377860734034664704992758249880018618131907367614177800329506877981986877921220485681998287752778495334541127048495486311792061n,
d: 1163847742215766215216916151663017691387519688859977157498780867776436010396072628219119707788340687440419444081289736279466637153082223960965411473296473n,
e: 3961365081960959178294197133768419551060435043430437330799371731939550352626564261219865471710058480523874787120718634318364066605378505537556570049131337n,
hashN: 2662304617,
};
// Proof-of-concept, so we only use one fixed session key
const aesKey = Buffer.from("ffddeeccbbaa99887766554433221100", "hex");
function writeServerHello(aesKey: Buffer, rsaKey: RsaKey): Buffer {
const M = readBigInt(aesKey);
const keyEnc = modPow(M, rsaKey.e, rsaKey.N);
const result = Buffer.alloc(0x48);
result.set(writeBigInt(keyEnc, 0x40), 0x00);
result.writeUInt32LE(0x01020304, 0x40); // Meaning is unknown
result.writeUInt32LE(rsaKey.hashN, 0x44);
return result;
}
function readClientHello(buf: Buffer) {
const magic = buf.readUInt32LE(0x00);
if (magic === 0xFE78571D) {
throw new Error(
"Server Box data, ignore."
);
}
if (magic !== 0x01020304) {
throw new Error(
"Invalid magic number, cryptographic processing probably incorrect."
);
}
return {
pcbId: buf.slice(0x04, 0x0f).toString("ascii"),
protocol: buf.slice(0x10, 0x13).toString("ascii"),
model: buf.slice(0x18, 0x0c).toString("ascii"),
};
}
export default async function setup(socket: Socket): Promise<IdzConnection> {
const tcpStream = new IoByteStream(socket);
const serverHello = writeServerHello(aesKey, rsaKey);
await tcpStream.write(serverHello);
const aesStream = new AesEcbStream(tcpStream, aesKey);
const response = await aesStream.read(0x30);
if (response.length !== 0x30) {
throw new Error("Truncated client hello");
}
const clientHello = readClientHello(response);
return { aesStream, clientHello };
}

View File

@ -0,0 +1,8 @@
import { CheckTeamNameRequest } from "../request/checkTeamName";
checkTeamName.msgCode = 0x00a2;
checkTeamName.msgLen = 0x0040;
export function checkTeamName(buf: Buffer): CheckTeamNameRequest {
return { type: "check_team_name_req" };
}

View File

@ -0,0 +1,14 @@
import { CreateAutoTeamRequest } from "../request/createAutoTeam";
import { AimeId } from "../../model";
createAutoTeam.msgCode = 0x007b;
createAutoTeam.msgLen = 0x0010;
export function createAutoTeam(buf: Buffer): CreateAutoTeamRequest {
return {
type: "create_auto_team_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
field_0008: buf.readUInt32LE(0x0008),
field_000C: buf.readUInt8(0x000c),
};
}

View File

@ -0,0 +1,24 @@
import iconv from "iconv-lite";
import { car } from "./_car";
import { chara } from "./_chara";
import { CreateProfileRequest } from "../request/createProfile";
import { AimeId } from "../../model";
createProfile.msgCode = 0x0066;
createProfile.msgLen = 0x00c0;
export function createProfile(buf: Buffer): CreateProfileRequest {
return {
type: "create_profile_req",
aimeId: buf.readInt32LE(0x0004) as AimeId,
luid: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
name: iconv.decode(
buf.slice(0x001e, buf.indexOf("\0", 0x001e)),
"shift_jis"
),
field_0034: buf.readUInt32LE(0x0034),
car: car(buf.slice(0x0040, 0x00a0)),
chara: chara(buf.slice(0x00a0, 0x00b4)),
};
}

View File

@ -0,0 +1,24 @@
import iconv from "iconv-lite";
import { CreateTeamRequest } from "../request/createTeam";
import { AimeId } from "../../model";
createTeam.msgCode = 0x0071;
createTeam.msgLen = 0x0050;
export function createTeam(buf: Buffer): CreateTeamRequest {
return {
type: "create_team_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
teamName: iconv.decode(
buf.slice(0x0008, buf.indexOf("\0", 0x0008)),
"shift_jis"
),
field_0028: buf.readUInt16LE(0x0028),
field_002C: buf.readUInt32LE(0x002c),
nameBg: buf.readUInt8(0x0030),
field_0032: buf.readUInt16LE(0x0032),
prevTeamId: buf.readUInt32LE(0x0034),
pcbId: buf.slice(0x0038, buf.indexOf("\0", 0x0038)).toString("ascii"),
};
}

View File

@ -0,0 +1,12 @@
import { DiscoverProfileRequest } from "../request/discoverProfile";
import { AimeId } from "../../model";
discoverProfile.msgCode = 0x006b;
discoverProfile.msgLen = 0x0010;
export function discoverProfile(buf: Buffer): DiscoverProfileRequest {
return {
type: "discover_profile_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}

206
src/idz/decoder/index.ts Normal file
View File

@ -0,0 +1,206 @@
import logger from "debug";
import { Transform } from "stream";
import { checkTeamName } from "./checkTeamName";
import { createProfile } from "./createProfile";
import { createTeam } from "./createTeam";
import { createAutoTeam } from "./createAutoTeam";
import { discoverProfile } from "./discoverProfile";
import { load2on2_v1, load2on2_v2 } from "./load2on2";
import { loadConfig } from "./loadConfig";
import { loadConfig2 } from "./loadConfig2";
import { loadEventInfo } from "./loadEventInfo";
import { loadGacha } from "./loadGacha";
import { loadGarage } from "./loadGarage";
import { loadGeneralReward1, loadGeneralReward2 } from "./loadGeneralReward";
import { loadGhost } from "./loadGhost";
import { loadProfile2, loadProfile3 } from "./loadProfile";
import { loadRewardTable } from "./loadRewardTable";
import { loadServerList } from "./loadServerList";
import { loadStocker } from "./loadStocker";
import { loadTeam } from "./loadTeam";
import { loadTeamRanking, loadTeamRanking2 } from "./loadTeamRanking";
import { loadTopTen1 } from "./loadTopTen1";
import { loadTopTen2 } from "./loadTopTen2";
import { lockGarage } from "./lockGarage";
import { lockProfile } from "./lockProfile";
import { msg00AD } from "./msg00AD";
import { saveExpedition1, saveExpedition2 } from "./saveExpedition";
import { saveGarage } from "./saveGarage";
import { saveNewCar } from "./saveNewCar";
import { saveProfile2 } from "./saveProfile2";
import { saveProfile3 } from "./saveProfile3";
import { saveSettings } from "./saveSettings";
import { saveStocker } from "./saveStocker";
import { saveTeamBanner } from "./saveTeamBanner";
import { saveTimeAttack1, saveTimeAttack2 } from "./saveTimeAttack";
import { saveTopic } from "./saveTopic";
import { unlockProfile } from "./unlockProfile";
import { updateProvisionalStoreRank } from "./updateProvisionalStoreRank";
import { updateTeamLeader } from "./updateTeamLeader";
import { updateTeamMember } from "./updateTeamMember";
import {
updateStoryClearNum1,
updateStoryClearNum2,
} from "./updateStoryClearNum";
import { Request } from "../request";
import { updateResult } from "./updateResult";
import { updateTeamPoints } from "./updateTeamPoints";
import { updateUiReport } from "./updateUiReport";
import { updateUserLog } from "./updateUserLog";
import { lockProfileExtend } from "./lockProfileExtend";
const debug = logger("app:idz:decoder");
export type ReaderFn = ((buf: Buffer) => Request) & {
msgCode: number;
msgLen: number;
};
const funcList: ReaderFn[] = [
checkTeamName,
createAutoTeam,
createProfile,
createTeam,
discoverProfile,
load2on2_v1,
load2on2_v2,
loadConfig,
loadConfig2,
loadEventInfo,
loadGacha,
loadGarage,
loadGeneralReward1,
loadGeneralReward2,
loadGhost,
loadProfile2,
loadProfile3,
loadRewardTable,
loadServerList,
loadStocker,
loadTeam,
loadTeamRanking,
loadTeamRanking2,
loadTopTen1,
loadTopTen2,
lockGarage,
lockProfile,
lockProfileExtend,
msg00AD,
saveExpedition1,
saveExpedition2,
saveGarage,
saveNewCar,
saveProfile2,
saveProfile3,
saveSettings,
saveStocker,
saveTeamBanner,
saveTimeAttack1,
saveTimeAttack2,
saveTopic,
unlockProfile,
updateProvisionalStoreRank,
updateResult,
updateStoryClearNum1,
updateStoryClearNum2,
updateTeamLeader,
updateTeamMember,
updateTeamPoints,
updateUiReport,
updateUserLog,
];
const readerFns = new Map<number, ReaderFn>();
const msgLengths = new Map<number, number>();
for (const fn of funcList) {
readerFns.set(fn.msgCode, fn);
msgLengths.set(fn.msgCode, fn.msgLen);
}
function readHeader(buf: Buffer) {
return {
blah: "blah",
};
}
export class Decoder extends Transform {
state: Buffer;
constructor() {
super({
readableObjectMode: true,
writableObjectMode: true,
});
this.state = Buffer.alloc(0);
}
_transform(chunk: Buffer, encoding, callback) {
this.state = Buffer.concat([this.state, chunk]);
// Read header
if (this.state.length < 0x04) {
return callback(null);
}
const magic = this.state.readUInt32LE(0);
if (magic !== 0x01020304) {
return callback(
new Error(
"Invalid magic number, cryptographic processing probably incorrect."
)
);
}
if (this.state.length < 0x30) {
return callback(null);
}
const header = readHeader(this.state);
if (this.state.length < 0x32) {
return callback(null);
}
const msgCode = this.state.readUInt16LE(0x30);
const msgLen = msgLengths.get(msgCode);
if (msgLen === undefined) {
return callback(
new Error(
`Unknown command code ${msgCode.toString(16)}, cannot continue`
)
);
}
if (this.state.length < 0x30 + msgLen) {
return callback(null);
}
const reqBuf = this.state.slice(0, 0x30 + msgLen);
const payloadBuf = reqBuf.slice(0x30);
if (debug.enabled) {
debug("Raw: %s", reqBuf.toString("hex"));
debug("Header: %j", header);
}
const reader = readerFns.get(msgCode);
if (reader === undefined) {
return callback(
new Error(`No reader for command code ${msgCode.toString(16)}`)
);
}
const payload = reader(payloadBuf);
debug("Payload: %j", payload);
return callback(null, payload);
}
}

View File

@ -0,0 +1,30 @@
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { Load2on2Request1, Load2on2Request2 } from "../request/load2on2";
import { AimeId } from "../../model";
load2on2_v1.msgCode = 0x00b0;
load2on2_v1.msgLen = 0x0010;
export function load2on2_v1(buf: Buffer): Load2on2Request1 {
return {
type: "load_2on2_req",
format: 1,
field_0002: buf.readUInt16LE(0x0002),
aimeId: buf.readUInt32LE(0x0004) as AimeId,
teamId: buf.readUInt32LE(0x0008) as ExtId<Team>,
};
}
load2on2_v2.msgCode = 0x0132;
load2on2_v2.msgLen = 0x0010;
export function load2on2_v2(buf: Buffer): Load2on2Request2 {
return {
type: "load_2on2_req",
format: 2,
field_0002: buf.readUInt16LE(0x0002),
aimeId: buf.readUInt32LE(0x0004) as AimeId,
teamId: buf.readUInt32LE(0x0008) as ExtId<Team>,
};
}

View File

@ -0,0 +1,8 @@
import { LoadConfigRequest } from "../request/loadConfig";
loadConfig.msgCode = 0x0004;
loadConfig.msgLen = 0x0050;
export function loadConfig(): LoadConfigRequest {
return { type: "load_config_req" };
}

View File

@ -0,0 +1,8 @@
import { LoadConfigRequest2 } from "../request/loadConfig2";
loadConfig2.msgCode = 0x00ab;
loadConfig2.msgLen = 0x0010;
export function loadConfig2(): LoadConfigRequest2 {
return { type: "load_config_v2_req" };
}

View File

@ -0,0 +1,12 @@
import { LoadEventInfoRequest } from "../request/loadEventInfo";
import { AimeId } from "../../model";
loadEventInfo.msgCode = 0x00be;
loadEventInfo.msgLen = 0x0010;
export function loadEventInfo(buf: Buffer): LoadEventInfoRequest {
return {
type: "load_event_info_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}

View File

@ -0,0 +1,12 @@
import { LoadGachaRequest } from "../request/loadGacha";
import { AimeId } from "../../model";
loadGacha.msgCode = 0x00c1;
loadGacha.msgLen = 0x0010;
export function loadGacha(buf: Buffer): LoadGachaRequest {
return {
type: "load_gacha_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}

View File

@ -0,0 +1,14 @@
import { LoadGarageRequest } from "../request/loadGarage";
import { AimeId } from "../../model";
loadGarage.msgCode = 0x0090;
loadGarage.msgLen = 0x0010;
export function loadGarage(buf: Buffer): LoadGarageRequest {
return {
type: "load_garage_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
fetchOffset: buf.readUInt8(0x0008),
field_000A: buf.readUInt16LE(0x000a),
};
}

View File

@ -1,32 +1,27 @@
import { LoadGeneralRewardRequest } from "../request/loadGeneralReward";
import { AimeId } from "../../../model";
import {
LoadGeneralRewardRequest1,
LoadGeneralRewardRequest2,
} from "../request/loadGeneralReward";
import { AimeId } from "../../model";
loadGeneralReward1.msgCode = 0x009c;
loadGeneralReward1.msgLen = 0x0010;
export function loadGeneralReward1(buf: Buffer): LoadGeneralRewardRequest {
export function loadGeneralReward1(buf: Buffer): LoadGeneralRewardRequest1 {
return {
type: "load_general_reward_req",
format: 1,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
loadGeneralReward2.msgCode = 0x093b;
loadGeneralReward2.msgCode = 0x013b;
loadGeneralReward2.msgLen = 0x0010;
export function loadGeneralReward2(buf: Buffer): LoadGeneralRewardRequest {
return {
type: "load_general_reward_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
loadGeneralReward3.msgCode = 0x013b;
loadGeneralReward3.msgLen = 0x0010;
export function loadGeneralReward3(buf: Buffer): LoadGeneralRewardRequest {
export function loadGeneralReward2(buf: Buffer): LoadGeneralRewardRequest2 {
return {
type: "load_general_reward_req",
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}

View File

@ -0,0 +1,15 @@
import { LoadGhostRequest } from "../request/loadGhost";
loadGhost.msgCode = 0x00a0;
loadGhost.msgLen = 0x0010;
export function loadGhost(buf: Buffer): LoadGhostRequest {
return {
type: "load_ghost_req",
field_0002: buf.readUInt16LE(0x0002),
field_0004: buf.readUInt16LE(0x0004),
field_0008: buf.readUInt32LE(0x0008),
field_000C: buf.readUInt16LE(0x000c),
field_000E: buf.readUInt16LE(0x000e),
};
}

View File

@ -0,0 +1,29 @@
import {
LoadProfileRequest2,
LoadProfileRequest3,
} from "../request/loadProfile";
import { AimeId } from "../../model";
loadProfile2.msgCode = 0x0067;
loadProfile2.msgLen = 0x0020;
export function loadProfile2(buf: Buffer): LoadProfileRequest2 {
return {
type: "load_profile_req",
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
luid: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
};
}
loadProfile3.msgCode = 0x0012f;
loadProfile3.msgLen = 0x0020;
export function loadProfile3(buf: Buffer): LoadProfileRequest3 {
return {
type: "load_profile_req",
format: 3,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
luid: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
};
}

View File

@ -0,0 +1,10 @@
import { LoadRewardTableRequest } from "../request/loadRewardTable";
loadRewardTable.msgCode = 0x0086;
loadRewardTable.msgLen = 0x0010;
export function loadRewardTable(): LoadRewardTableRequest {
return {
type: "load_reward_table_req",
};
}

View File

@ -0,0 +1,12 @@
import { LoadStockerRequest } from "../request/loadStocker";
import { AimeId } from "../../model";
loadStocker.msgCode = 0x00a7;
loadStocker.msgLen = 0x0010;
export function loadStocker(buf: Buffer): LoadStockerRequest {
return {
type: "load_stocker_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}

View File

@ -0,0 +1,16 @@
import { LoadTeamRequest } from "../request/loadTeam";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
loadTeam.msgCode = 0x0077;
loadTeam.msgLen = 0x0010;
export function loadTeam(buf: Buffer): LoadTeamRequest {
const extId = buf.readUInt32LE(0x0008);
return {
type: "load_team_req",
aimeId: buf.readUInt32LE(0x0004),
teamExtId: extId !== 0xffffffff ? (extId as ExtId<Team>) : undefined,
};
}

View File

@ -0,0 +1,21 @@
import { LoadTeamRankingRequest } from "../request/loadTeamRanking";
loadTeamRanking.msgCode = 0x00b9;
loadTeamRanking.msgLen = 0x0010;
export function loadTeamRanking(buf: Buffer): LoadTeamRankingRequest {
return {
type: "load_team_ranking_req",
};
}
// not sure what the difference is...
loadTeamRanking2.msgCode = 0x00bb;
loadTeamRanking2.msgLen = 0x0010;
export function loadTeamRanking2(buf: Buffer): LoadTeamRankingRequest {
return {
type: "load_team_ranking_req",
};
}

View File

@ -4,7 +4,7 @@ import {
LoadTopTenRequest,
LoadTopTenRequestSelector,
} from "../request/loadTopTen";
import { AimeId } from "../../../model";
import { AimeId } from "../../model";
loadTopTen1.msgCode = 0x00b5;
loadTopTen1.msgLen = 0x00e0;
@ -24,7 +24,6 @@ export function loadTopTen1(buf: Buffer): LoadTopTenRequest {
return {
type: "load_top_ten_req",
version: 1,
field_2: buf.readUInt16LE(0x0002), // Bitmask selector
selectors,
field_C4: buf.readUInt8(0x00c4), // Boolean, true if profile ID is set

View File

@ -4,7 +4,7 @@ import {
LoadTopTenRequest,
LoadTopTenRequestSelector,
} from "../request/loadTopTen";
import { AimeId } from "../../../model";
import { AimeId } from "../../model";
loadTopTen2.msgCode = 0x012c;
loadTopTen2.msgLen = 0x0110;
@ -24,7 +24,6 @@ export function loadTopTen2(buf: Buffer): LoadTopTenRequest {
return {
type: "load_top_ten_req",
version: 1,
field_2: buf.readUInt16LE(0x0002), // Bitmask selector
selectors,
field_C4: buf.readUInt8(0x00f4), // Boolean, true if profile ID is set
@ -34,34 +33,3 @@ export function loadTopTen2(buf: Buffer): LoadTopTenRequest {
teamId: teamId !== 0xffffffff ? (teamId as ExtId<Team>) : undefined,
};
}
loadTopTen3.msgCode = 0x012c;
loadTopTen3.msgLen = 0x0110;
export function loadTopTen3(buf: Buffer): LoadTopTenRequest {
const selectors = new Array<LoadTopTenRequestSelector>();
for (let i = 0; i < 40; i++) {
selectors.push({
routeNo: (buf.readUInt16LE(0x0004 + 2 + 2 * i) >> 1) as RouteNo,
minTimestamp: new Date(
buf.readUInt32LE(0x0054 + 4 + 4 * i) * 1000 + 1000
),
});
}
const profileId = buf.readUInt32LE(0x00fc);
const teamId = buf.readUInt32LE(0x0100);
return {
type: "load_top_ten_req",
version: 2,
field_2: buf.readUInt16LE(0x0004), // Bitmask selector
selectors,
field_C4: buf.readUInt8(0x00f8), // Boolean, true if profile ID is set
field_C5: buf.readUInt8(0x00f9), // Always zero
field_C6: buf.readUInt16LE(0x00fa),
aimeId: profileId !== 0 ? (profileId as AimeId) : undefined,
teamId: teamId !== 0xffffffff ? (teamId as ExtId<Team>) : undefined,
};
}

View File

@ -0,0 +1,11 @@
import { LockGarageRequest } from "../request/lockGarage";
lockGarage.msgCode = 0x00a9;
lockGarage.msgLen = 0x0010;
export function lockGarage(buf: Buffer): LockGarageRequest {
return {
type: "lock_garage_request",
field_0004: buf.readUInt32LE(0x0004),
};
}

View File

@ -0,0 +1,13 @@
import { LockProfileRequest } from "../request/lockProfile";
lockProfile.msgCode = 0x0069;
lockProfile.msgLen = 0x0020;
export function lockProfile(buf: Buffer): LockProfileRequest {
return {
type: "lock_profile_req",
aimeId: buf.readUInt32LE(0x0004),
pcbId: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
field_0018: buf.readUInt16LE(0x0018),
};
}

View File

@ -0,0 +1,13 @@
import { LockProfileExtendRequest } from "../request/lockProfileExtend";
import { AimeId } from "../../model";
lockProfileExtend.msgCode = 0x006d;
lockProfileExtend.msgLen = 0x0020;
export function lockProfileExtend(buf: Buffer): LockProfileExtendRequest {
return {
type: "lock_profile_extend_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
luid: buf.slice(0x0008, buf.indexOf("\0")).toString("ascii"),
};
}

View File

@ -0,0 +1,8 @@
import { Message00AD } from "../request/msg00AD";
msg00AD.msgCode = 0x00ad;
msg00AD.msgLen = 0x061c;
export function msg00AD(buf: Buffer): Message00AD {
return { type: "msg_00AD_req" };
}

View File

@ -1,11 +1,15 @@
import { SaveExpeditionRequest } from "../request/saveExpedition";
import {
SaveExpeditionRequest1,
SaveExpeditionRequest2,
} from "../request/saveExpedition";
saveExpedition1.msgCode = 0x008c;
saveExpedition1.msgLen = 0x0010;
export function saveExpedition1(buf: Buffer): SaveExpeditionRequest {
export function saveExpedition1(buf: Buffer): SaveExpeditionRequest1 {
return {
type: "save_expedition_req",
format: 1,
field_0004: buf.readUInt32LE(0x0004),
};
}
@ -13,9 +17,10 @@ export function saveExpedition1(buf: Buffer): SaveExpeditionRequest {
saveExpedition2.msgCode = 0x013f;
saveExpedition2.msgLen = 0x0010;
export function saveExpedition2(buf: Buffer): SaveExpeditionRequest {
export function saveExpedition2(buf: Buffer): SaveExpeditionRequest2 {
return {
type: "save_expedition_req",
format: 2,
field_0004: buf.readUInt32LE(0x0004),
};
}

View File

@ -0,0 +1,22 @@
import { car } from "./_car";
import { SaveGarageRequest } from "../request/saveGarage";
saveGarage.msgCode = 0x008e;
saveGarage.msgLen = 0x0090;
export function saveGarage(buf: Buffer): SaveGarageRequest {
const field_0068: number[] = [];
for (let offset = 0x0068; offset < 0x007c; offset += 2) {
field_0068.push(buf.readUInt16LE(offset));
}
return {
type: "save_garage_req",
aimeId: buf.readUInt32LE(0x0004),
payload: car(buf.slice(0x0008, 0x0068)),
field_0068,
field_0080: buf.readUInt8(0x0080),
field_0081: buf.readUInt8(0x0081) !== 0,
};
}

View File

@ -0,0 +1,16 @@
import { car } from "./_car";
import { SaveNewCarRequest } from "../request/saveNewCar";
import { AimeId } from "../../model";
saveNewCar.msgCode = 0x0079;
saveNewCar.msgLen = 0x0090;
export function saveNewCar(buf: Buffer): SaveNewCarRequest {
return {
type: "save_new_car_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
luid: buf.slice(0x0008, buf.indexOf(0, 0x0008)).toString("ascii"),
car: car(buf.slice(0x0020, 0x0080)),
field_0080: buf.readUInt32LE(0x0080),
};
}

View File

@ -1,33 +1,31 @@
import { car } from "./_car";
import { mission } from "./_mission";
import { BackgroundCode, CourseNo, TitleCode } from "../model/base";
import { StoryCell, StoryRow } from "../model/story";
import { SaveProfileRequest } from "../request/saveProfile";
import { SaveProfileRequest2 } from "../request/saveProfile";
import { bitmap } from "./_bitmap";
import { AimeId } from "../../../model";
import { AimeId } from "../../model";
saveProfile2.msgCode = 0x0068;
saveProfile2.msgLen = 0x0940;
export function saveProfile2(buf: Buffer): SaveProfileRequest {
const storyRows = new Map<number, StoryRow>();
export function saveProfile2(buf: Buffer): SaveProfileRequest2 {
const storyRows = new Array();
for (let i = 0; i < 9; i++) {
const cells = new Map<number, StoryCell>();
const cells = new Array();
const rowOffset = 0x01a8 + i * 0x3c;
for (let j = 0; j < 9; j++) {
const a = buf.readUInt32LE(rowOffset + 0x04 + j * 4);
const b = buf.readUInt16LE(rowOffset + 0x28 + j * 2);
const c = 0; // Added in idz2
const cell = { a, b, c };
const cell = { a, b };
cells.set(j, cell);
cells.push(cell);
}
const row = { cells };
storyRows.set(i, row);
storyRows.push(row);
}
const coursePlays = new Map<CourseNo, number>();
@ -47,8 +45,8 @@ export function saveProfile2(buf: Buffer): SaveProfileRequest {
return {
type: "save_profile_req",
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
version: 1,
lv: buf.readUInt16LE(0x0026),
exp: buf.readUInt32LE(0x0028),
fame: buf.readUInt32LE(0x0468),
@ -96,7 +94,6 @@ export function saveProfile2(buf: Buffer): SaveProfileRequest {
aura: buf.readUInt8(0x002c),
paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7),
drivingStyle: 0, // Not supported until idz2
},
};
}

View File

@ -1,35 +1,33 @@
import { car } from "./_car";
import { mission } from "./_mission";
import { BackgroundCode, CourseNo, TitleCode } from "../model/base";
import { StoryCell, StoryRow } from "../model/story";
import { SaveProfileRequest } from "../request/saveProfile";
import { SaveProfileRequest2 } from "../request/saveProfile";
import { bitmap } from "./_bitmap";
import { AimeId } from "../../../model";
import { AimeId } from "../../model";
saveProfile3.msgCode = 0x0138;
saveProfile3.msgLen = 0x0a70;
export function saveProfile3(buf: Buffer): SaveProfileRequest {
const storyRows = new Map<number, StoryRow>();
export function saveProfile3(buf: Buffer): SaveProfileRequest2 {
const storyRows = new Array();
// Story layout has changed somewhat...
for (let i = 0; i < 27; i++) {
const cells = new Map<number, StoryCell>();
const cells = new Array();
const rowOffset = 0x01ac + i * 0x18;
for (let j = 0; j < 9; j++) {
const a = buf.readUInt8(rowOffset + 0x00 + j);
const b = buf.readUInt8(rowOffset + 0x09 + j);
const c = 0; // Added in idz2
const cell = { a, b, c };
const cell = { a, b };
cells.set(j, cell);
cells.push(cell);
}
const row = { cells };
storyRows.set(i, row);
storyRows.push(row);
}
const coursePlays = new Map<CourseNo, number>();
@ -49,8 +47,8 @@ export function saveProfile3(buf: Buffer): SaveProfileRequest {
return {
type: "save_profile_req",
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
version: 1,
lv: buf.readUInt16LE(0x0026),
exp: buf.readUInt32LE(0x0028),
fame: buf.readUInt32LE(0x04fc),
@ -98,7 +96,6 @@ export function saveProfile3(buf: Buffer): SaveProfileRequest {
aura: buf.readUInt8(0x002c),
paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7),
drivingStyle: 0, // Not supported until idz2
},
};
}

View File

@ -0,0 +1,23 @@
import { SaveSettingsRequest } from "../request/saveSettings";
import { AimeId } from "../../model";
saveSettings.msgCode = 0x00a5;
saveSettings.msgLen = 0x0020;
export function saveSettings(buf: Buffer): SaveSettingsRequest {
const pack = buf.readUInt32LE(0x000c);
return {
type: "save_settings_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
dpoint: buf.readUInt32LE(0x0008),
settings: {
music: buf.readUInt16LE(0x0002),
pack: buf.readUInt32LE(0x000c),
paperCup: buf.readUInt8(0x0011),
gauges: buf.readUInt8(0x0012),
aura: buf.readUInt8(0x0013),
},
field_0010: buf.readUInt8(0x0010),
};
}

View File

@ -0,0 +1,19 @@
import { bitmap } from "./_bitmap";
import { chara } from "./_chara";
import { CarSelector } from "../model/car";
import { SaveStockerRequest } from "../request/saveStocker";
import { AimeId } from "../../model";
saveStocker.msgCode = 0x00a6;
saveStocker.msgLen = 0x00c0;
export function saveStocker(buf: Buffer): SaveStockerRequest {
return {
type: "save_stocker_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
backgrounds: bitmap(buf.slice(0x0008, 0x002c)),
selectedCar: buf.readUInt16LE(0x009c) as CarSelector,
chara: chara(buf.slice(0x009e, 0x00b2)),
};
}

View File

@ -0,0 +1,15 @@
import { SaveTeamBannerRequest } from "../request/saveTeamBanner";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
saveTeamBanner.msgCode = 0x0089;
saveTeamBanner.msgLen = 0x0010;
export function saveTeamBanner(buf: Buffer): SaveTeamBannerRequest {
return {
type: "save_team_banner_req",
teamExtId: buf.readUInt32LE(0x0004) as ExtId<Team>,
nameBg: buf.readUInt32LE(0x0008),
nameFx: buf.readUInt32LE(0x000c),
};
}

View File

@ -1,13 +1,12 @@
import { RouteNo } from "../model/base";
import { CarSelector } from "../model/car";
import { SaveTimeAttackRequest } from "../request/saveTimeAttack";
import { AimeId } from "../../../model";
import { AimeId } from "../../model";
function saveTimeAttack(buf: Buffer): SaveTimeAttackRequest {
return {
type: "save_time_attack_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
version: 1,
dayNight: buf.readUInt8(0x0054) & 1,
payload: {
routeNo: (buf.readUInt8(0x0054) >> 1) as RouteNo,
@ -49,35 +48,3 @@ saveTimeAttack2.msgLen = 0x0080;
export function saveTimeAttack2(buf: Buffer): SaveTimeAttackRequest {
return saveTimeAttack(buf);
}
saveTimeAttack3.msgCode = 0x0136;
saveTimeAttack3.msgLen = 0x0080;
export function saveTimeAttack3(buf: Buffer): SaveTimeAttackRequest {
return {
type: "save_time_attack_req",
aimeId: buf.readUInt32LE(0x0008) as AimeId,
version: 2,
dayNight: buf.readUInt8(0x0058) & 1,
payload: {
routeNo: (buf.readUInt8(0x0058) >> 1) as RouteNo,
timestamp: new Date(buf.readUInt32LE(0x005c) * 1000),
flags: buf.readUInt8(0x0060),
totalTime: buf.readUInt32LE(0x001c) / 1000,
sectionTimes: [
buf.readUInt32LE(0x0028) / 1000,
buf.readUInt32LE(0x002c) / 1000,
buf.readUInt32LE(0x0030) / 1000,
],
grade: buf.readUInt8(0x0066),
carSelector: buf.readUInt16LE(0x0010) as CarSelector,
},
field_0002: buf.readUInt16LE(0x0004),
field_0008: buf.readUInt32LE(0x000c),
field_0012: buf.readUInt8(0x0016),
field_0015: buf.readUInt8(0x0019),
field_005D: buf.readUInt8(0x0061),
field_005E: buf.readUInt16LE(0x0062),
field_0060: buf.readUInt16LE(0x0064),
};
}

View File

@ -0,0 +1,13 @@
import { SaveTopicRequest } from "../request/saveTopic";
saveTopic.msgCode = 0x009a;
saveTopic.msgLen = 0x0010;
export function saveTopic(buf: Buffer): SaveTopicRequest {
const aimeId = buf.readUInt32LE(0x0004);
return {
type: "save_topic_req",
aimeId: aimeId !== 0xffffffff ? aimeId : undefined,
};
}

View File

@ -0,0 +1,12 @@
import { UnlockProfileRequest } from "../request/unlockProfile";
unlockProfile.msgCode = 0x006f;
unlockProfile.msgLen = 0x0020;
export function unlockProfile(buf: Buffer): UnlockProfileRequest {
return {
type: "unlock_profile_req",
aimeId: buf.readUInt32LE(0x0004),
pcbId: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
};
}

View File

@ -0,0 +1,13 @@
import { UpdateProvisionalStoreRankRequest } from "../request/updateProvisionalStoreRank";
updateProvisionalStoreRank.msgCode = 0x0082;
updateProvisionalStoreRank.msgLen = 0x0010;
export function updateProvisionalStoreRank(
buf: Buffer
): UpdateProvisionalStoreRankRequest {
return {
type: "update_provisional_store_rank_req",
aimeId: buf.readUInt32LE(0x0004),
};
}

Some files were not shown because too many files have changed in this diff Show More