Compare commits

..

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

365 changed files with 3877 additions and 8721 deletions

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 +1,53 @@
# 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)
* 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)
* 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)
* 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)
* 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)
* 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)
* Fix IDZ team creation (Tau)
* Fix startup script working directory (Tau)
## v006
- Fix Chunithm HTTP POST size limit (Tau)
* 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
* Initial public release (Tau)
* Chunithm v1.30.00 Amazon
* Initial D Zero v1.21.00
* Initial D Zero v1.31.00

5831
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,12 +27,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

@ -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,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

@ -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,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

@ -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";
@ -100,7 +99,6 @@ 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);

View File

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

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

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

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),
};
}

View File

@ -0,0 +1,28 @@
import {
UpdateStoryClearNumRequest1,
UpdateStoryClearNumRequest2,
} from "../request/updateStoryClearNum";
updateStoryClearNum1.msgCode = 0x007f;
updateStoryClearNum1.msgLen = 0x0010;
export function updateStoryClearNum1(
buf: Buffer
): UpdateStoryClearNumRequest1 {
return {
type: "update_story_clear_num_req",
format: 1,
};
}
updateStoryClearNum2.msgCode = 0x013d;
updateStoryClearNum2.msgLen = 0x0010;
export function updateStoryClearNum2(
buf: Buffer
): UpdateStoryClearNumRequest2 {
return {
type: "update_story_clear_num_req",
format: 2,
};
}

View File

@ -0,0 +1,16 @@
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { UpdateTeamLeaderRequest } from "../request/updateTeamLeader";
import { AimeId } from "../../model";
updateTeamLeader.msgCode = 0x008a;
updateTeamLeader.msgLen = 0x0020;
export function updateTeamLeader(buf: Buffer): UpdateTeamLeaderRequest {
return {
type: "update_team_leader_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
teamExtId: buf.readUInt32LE(0x0008) as ExtId<Team>,
field_000C: buf.slice(0x000c, buf.indexOf("\0", 0x000c)).toString("ascii"),
};
}

View File

@ -0,0 +1,16 @@
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { UpdateTeamMemberRequest } from "../request/updateTeamMember";
import { AimeId } from "../../model";
updateTeamMember.msgCode = 0x0073;
updateTeamMember.msgLen = 0x0010;
export function updateTeamMember(buf: Buffer): UpdateTeamMemberRequest {
return {
type: "update_team_member_req",
action: buf.readUInt8(0x0004) === 0 ? "add" : "remove",
aimeId: buf.readUInt32LE(0x0008) as AimeId,
teamExtId: buf.readUInt32LE(0x000c) as ExtId<Team>,
};
}

View File

@ -0,0 +1,12 @@
import { UpdateTeamPointsRequest } from "../request/updateTeamPoints";
updateTeamPoints.msgCode = 0x0081;
updateTeamPoints.msgLen = 0x0010;
export function updateTeamPoints(buf: Buffer): UpdateTeamPointsRequest {
return {
type: "update_team_points_req",
field_0004: buf.readUInt32LE(0x0004),
field_0008: buf.readUInt32LE(0x0008),
};
}

View File

@ -0,0 +1,12 @@
import { UpdateUiReportRequest } from "../request/updateUiReport";
updateUiReport.msgCode = 0x0084;
updateUiReport.msgLen = 0x0410;
export function updateUiReport(buf: Buffer): UpdateUiReportRequest {
return {
type: "update_ui_report_req",
field_02: buf.readUInt16LE(0x0002),
field_04: buf.readUInt16LE(0x0004),
};
}

View File

@ -0,0 +1,8 @@
import { UpdateUserLogRequest } from "../request/updateUserLog";
updateUserLog.msgCode = 0x00bd;
updateUserLog.msgLen = 0x0050;
export function updateUserLog(buf: Buffer): UpdateUserLogRequest {
return { type: "update_user_log_req" };
}

18
src/idz/encoder/_chara.ts Normal file
View File

@ -0,0 +1,18 @@
import { Chara } from "../model/chara";
export function encodeChara(chara: Chara): Buffer {
const buf = Buffer.alloc(0x0014);
buf.writeUInt8(chara.gender === "male" ? 0 : 1, 0x00);
buf.writeUInt16LE(chara.field_02, 0x02);
buf.writeUInt16LE(chara.field_04, 0x04);
buf.writeUInt16LE(chara.field_06, 0x06);
buf.writeUInt16LE(chara.field_08, 0x08);
buf.writeUInt16LE(chara.field_0a, 0x0a);
buf.writeUInt16LE(chara.field_0c, 0x0c);
buf.writeUInt16LE(chara.field_0e, 0x0e);
buf.writeUInt16LE(chara.background, 0x10); // Swapped on save
buf.writeUInt16LE(chara.title, 0x12); // Swapped on save
return buf;
}

59
src/idz/encoder/_team.ts Normal file
View File

@ -0,0 +1,59 @@
import iconv from "iconv-lite";
import { CreateAutoTeamResponse } from "../response/createAutoTeam";
import { LoadTeamResponse } from "../response/loadTeam";
import { encodeChara } from "./_chara";
export function _team(res: CreateAutoTeamResponse | LoadTeamResponse) {
const buf = Buffer.alloc(0x0ca0);
if (res.type === "create_auto_team_res") {
buf.writeInt16LE(0x007c, 0x0000);
} else {
buf.writeInt16LE(0x0078, 0x0000);
}
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
buf.fill(0xff, 0x00f9, 0x0101); // Bitset: Unlocked FX probably
buf.writeUInt32LE(leader ? leader.profile.aimeId : 0, 0x0080);
for (let i = 0; i < 6; i++) {
const base = 0x011c + i * 0x005c;
const member = res.members[i];
if (member === undefined) {
break;
}
const { profile, chara } = member;
const accessTime = (profile.accessTime.getTime() / 1000) | 0;
buf.writeInt32LE(profile.aimeId, base + 0x0000);
iconv.encode(profile.name + "\0", "shift_jis").copy(buf, base + 0x0004);
buf.writeInt32LE(profile.lv, base + 0x0018);
buf.writeInt32LE(0, base + 0x0024); // Month points, TODO
buf.writeUInt32LE(accessTime, base + 0x0034);
encodeChara(chara).copy(buf, base + 0x0044);
}
// Team Time Attack:
/*for (let i = 0; i < 6; i++) {
const base = 0x0344 + 0x20 * i;
buf.writeInt16LE(0x00001, base + 0x0000);
buf.writeInt8(0x02, base + 0x0003);
buf.writeInt32LE(0x00000003, base + 0x0004);
iconv.encode("str\0", "shift_jis").copy(buf, base + 0x0008);
buf.writeInt32LE(0x00000004, base + 0x001c);
}*/
return buf;
}

View File

@ -0,0 +1,10 @@
import { CheckTeamNameResponse } from "../response/checkTeamName";
export function checkTeamName(res: CheckTeamNameResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x00a3, 0x0000);
buf.writeUInt32LE(res.status, 0x0004);
return buf;
}

View File

@ -0,0 +1,11 @@
import { CreateTeamResponse } from "../response/createTeam";
export function createTeam(res: CreateTeamResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x0072, 0x0000);
buf.writeUInt32LE(res.status, 0x0004);
buf.writeUInt32LE(res.teamExtId, 0x0008);
return buf;
}

View File

@ -0,0 +1,10 @@
import { DiscoverProfileResponse } from "../response/discoverProfile";
export function discoverProfile(res: DiscoverProfileResponse) {
const buf = Buffer.alloc(0x0010);
buf.writeInt16LE(0x006c, 0x0000);
buf.writeInt8(res.exists ? 1 : 0, 0x0004);
return buf;
}

View File

@ -0,0 +1,10 @@
import { GenericResponse } from "../response/generic";
export function generic(res: GenericResponse) {
const buf = Buffer.alloc(0x0020);
buf.writeInt16LE(0x0001, 0x0000);
buf.writeInt32LE(res.status || 0, 0x0004);
return buf;
}

167
src/idz/encoder/index.ts Normal file
View File

@ -0,0 +1,167 @@
import logger from "debug";
import { Transform } from "stream";
import { _team } from "./_team";
import { checkTeamName } from "./checkTeamName";
import { createTeam } from "./createTeam";
import { discoverProfile } from "./discoverProfile";
import { generic } from "./generic";
import { lockProfile } from "./lockProfile";
import { lockProfileExtend } from "./lockProfileExtend";
import { load2on2 } from "./load2on2";
import { loadConfig } from "./loadConfig";
import { loadConfig2 } from "./loadConfig2";
import { loadEventInfo } from "./loadEventInfo";
import { loadGacha } from "./loadGacha";
import { loadGarage } from "./loadGarage";
import { loadGeneralReward } from "./loadGeneralReward";
import { loadGhost } from "./loadGhost";
import { loadProfile } from "./loadProfile";
import { loadRewardTable } from "./loadRewardTable";
import { loadServerList } from "./loadServerList";
import { loadStocker } from "./loadStocker";
import { loadTeamRanking } from "./loadTeamRanking";
import { loadTopTen } from "./loadTopTen";
import { saveExpedition } from "./saveExpedition";
import { saveGarage } from "./saveGarage";
import { saveNewCar } from "./saveNewCar";
import { saveTimeAttack } from "./saveTimeAttack";
import { saveTopic } from "./saveTopic";
import { unlockProfile } from "./unlockProfile";
import { updateProvisionalStoreRank } from "./updateProvisionalStoreRank";
import { updateStoryClearNum } from "./updateStoryClearNum";
import { updateTeamLeader } from "./updateTeamLeader";
import { updateTeamMember } from "./updateTeamMember";
import { Response } from "../response";
const debug = logger("app:idz:encoder");
function encode(res: Response): Buffer {
switch (res.type) {
case "check_team_name_res":
return checkTeamName(res);
case "create_auto_team_res":
return _team(res);
case "create_team_res":
return createTeam(res);
case "discover_profile_res":
return discoverProfile(res);
case "generic_res":
return generic(res);
case "load_2on2_res":
return load2on2(res);
case "load_config_res":
return loadConfig(res);
case "load_config_v2_res":
return loadConfig2(res);
case "load_event_info_res":
return loadEventInfo(res);
case "load_gacha_res":
return loadGacha(res);
case "load_garage_res":
return loadGarage(res);
case "load_general_reward_res":
return loadGeneralReward(res);
case "load_ghost_res":
return loadGhost(res);
case "load_profile_res":
return loadProfile(res);
case "load_reward_table_res":
return loadRewardTable(res);
case "load_server_list_res":
return loadServerList(res);
case "load_stocker_res":
return loadStocker(res);
case "load_team_res":
return _team(res);
case "load_team_ranking_res":
return loadTeamRanking(res);
case "load_top_ten_res":
return loadTopTen(res);
case "lock_profile_extend_res":
return lockProfileExtend(res);
case "lock_profile_res":
return lockProfile(res);
case "save_expedition_res":
return saveExpedition(res);
case "save_garage_res":
return saveGarage(res);
case "save_new_car_res":
return saveNewCar(res);
case "save_time_attack_res":
return saveTimeAttack(res);
case "unlock_profile_res":
return unlockProfile(res);
case "update_provisional_store_rank_res":
return updateProvisionalStoreRank(res);
case "update_story_clear_num_res":
return updateStoryClearNum(res);
case "update_team_leader_res":
return updateTeamLeader(res);
case "update_team_member_res":
return updateTeamMember(res);
case "save_topic_res":
return saveTopic(res);
default:
const exhaustCheck: never = res;
throw new Error(`No writer fn for ${res["type"]}`);
}
}
export class Encoder extends Transform {
constructor() {
super({
readableObjectMode: true,
writableObjectMode: true,
});
}
_transform(res: Response, encoding, callback) {
debug("Object: %j", res);
const buf = encode(res);
if (debug.enabled) {
debug("Encoded: %s", buf.toString("hex"));
}
if (buf.readInt16LE(0) === 0) {
throw new Error("Missing message type code");
}
return callback(null, buf);
}
}

View File

@ -0,0 +1,37 @@
import {
Load2on2Response,
Load2on2Response1,
Load2on2Response2,
} from "../response/load2on2";
export function load2on2(res: Load2on2Response): Buffer {
switch (res.format) {
case 1:
return load2on2_v1(res);
case 2:
return load2on2_v2(res);
default:
const exhaust: never = res;
throw new Error(`Unsupported 2on2 response format ${res["format"]}`);
}
}
function load2on2_v1(res: Load2on2Response1): Buffer {
const buf = Buffer.alloc(0x04c0);
buf.writeInt16LE(0x00b1, 0x0000);
return buf;
}
// Same size but presumably incompatible somehow
function load2on2_v2(res: Load2on2Response2): Buffer {
const buf = Buffer.alloc(0x04c0);
buf.writeInt16LE(0x0133, 0x0000);
return buf;
}

View File

@ -0,0 +1,11 @@
import { LoadConfigResponse } from "../response/loadConfig";
export function loadConfig(res: LoadConfigResponse) {
const buf = Buffer.alloc(0x01a0);
buf.writeInt16LE(0x0005, 0x0000);
buf.writeInt8(res.status, 0x0002);
buf.writeUInt16LE(res.serverVersion, 0x0016);
return buf;
}

View File

@ -0,0 +1,10 @@
import { LoadConfigResponse2 } from "../response/loadConfig2";
export function loadConfig2(res: LoadConfigResponse2) {
const buf = Buffer.alloc(0x0230);
buf.writeInt16LE(0x00ac, 0x0000);
buf.writeInt8(res.status, 0x0002);
return buf;
}

View File

@ -0,0 +1,9 @@
import { LoadEventInfoResponse } from "../response/loadEventInfo";
export function loadEventInfo(res: LoadEventInfoResponse): Buffer {
const buf = Buffer.alloc(0x01b0);
buf.writeUInt16LE(0x00bf, 0x0000);
return buf;
}

View File

@ -0,0 +1,10 @@
import { LoadGachaResponse } from "../response/loadGacha";
export function loadGacha(res: LoadGachaResponse): Buffer {
const buf = Buffer.alloc(0x0090);
buf.writeUInt16LE(0x00c2, 0x0000);
buf.writeUInt8(res.awardedToday ? 0x01 : 0x00, 0x0002);
return buf;
}

View File

@ -0,0 +1,15 @@
import { encodeCar } from "./_car";
import { LoadGarageResponse } from "../response/loadGarage";
export function loadGarage(res: LoadGarageResponse): Buffer {
const buf = Buffer.alloc(0x03d0);
buf.writeUInt16LE(0x0091, 0x0000);
buf.writeUInt16LE(res.cars.length, 0x0002);
for (let i = 0; i < res.cars.length; i++) {
encodeCar(res.cars[i]).copy(buf, 0x0004 + 0x0060 * i);
}
return buf;
}

View File

@ -1,5 +1,6 @@
import iconv from "iconv-lite";
import { LoadGeneralRewardResponse } from "../response/loadGeneralReward";
import { writeSjisStr } from "../../util/bin";
export function loadGeneralReward(res: LoadGeneralRewardResponse) {
const buf = Buffer.alloc(0x0330);
@ -14,7 +15,7 @@ export function loadGeneralReward(res: LoadGeneralRewardResponse) {
const item = res.items[i];
const base = 0x04 + 0x50 * i;
writeSjisStr(buf, base + 0x04, base + 0x2c, item.field_04);
iconv.encode(item.field_04, "shift_jis").copy(buf, base + 0x04);
buf.writeUInt32LE(item.field_2C, base + 0x2c);
buf.writeUInt8(item.field_38, base + 0x38);
buf.writeUInt8(item.field_39, base + 0x39);

View File

@ -1,11 +1,11 @@
import { LoadGhostResponse } from "../response/loadGhost";
function _loadGhost(msgCode: number, res: LoadGhostResponse): Buffer {
export function loadGhost(res: LoadGhostResponse): Buffer {
const buf = Buffer.alloc(0x0070);
// No idea what any of this even does.
buf.writeUInt16LE(msgCode, 0x0000);
buf.writeUInt16LE(0x00a1, 0x0000);
buf.writeUInt16LE(0x0005, 0x0002); // Chunk presence flags: 4 | 1
for (let i = 0; i < 2; i++) {
@ -29,11 +29,3 @@ function _loadGhost(msgCode: number, res: LoadGhostResponse): Buffer {
return buf;
}
export function loadGhost1(res: LoadGhostResponse): Buffer {
return _loadGhost(0x00a1, res);
}
export function loadGhost2(res: LoadGhostResponse): Buffer {
return _loadGhost(0x0096, res);
}

View File

@ -0,0 +1,22 @@
import { loadProfile1 } from "./loadProfile1";
import { loadProfile2 } from "./loadProfile2";
import { loadProfile3 } from "./loadProfile3";
import { LoadProfileResponse } from "../response/loadProfile";
export function loadProfile(res: LoadProfileResponse) {
switch (res.format) {
case 1:
return loadProfile1(res);
case 2:
return loadProfile2(res);
case 3:
return loadProfile3(res);
default:
const exhaust: never = res;
throw new Error(`Unsupported profile response format ${res["format"]}`);
}
}

View File

@ -1,9 +1,9 @@
import { LoadProfileResponse } from "../response/loadProfile";
import { LoadProfileResponse1 } from "../response/loadProfile";
// Sending this causes an error in v1.21, so it is currently unmapped and
// unimplemented.
export function loadProfile1(res: LoadProfileResponse) {
export function loadProfile1(res: LoadProfileResponse1) {
const buf = Buffer.alloc(0x0c60);
buf.writeInt16LE(0x0064, 0x0000);

View File

@ -1,11 +1,12 @@
import iconv from "iconv-lite";
import { encodeBitmap } from "./_bitmap";
import { encodeCar } from "./_car";
import { encodeChara1 } from "./_chara";
import { encodeChara } from "./_chara";
import { encodeMission } from "./_mission";
import { LoadProfileResponse } from "../response/loadProfile";
import { writeSjisStr } from "../../util/bin";
import { LoadProfileResponse2 } from "../response/loadProfile";
export function loadProfile2(res: LoadProfileResponse) {
export function loadProfile2(res: LoadProfileResponse2) {
const buf = Buffer.alloc(0x0d30);
// FLAMETHROWER ANALYSIS (watch out for C strings)
@ -37,22 +38,14 @@ export function loadProfile2(res: LoadProfileResponse) {
}
}
for (let i = 0; i < 9; i++) {
const row = res.story.rows.get(i);
for (let i = 0; i < 9 && i < res.story.rows.length; i++) {
const row = res.story.rows[i];
const rowOffset = 0x0228 + i * 0x26;
if (row === undefined) {
continue;
}
for (let j = 0; j < 9; j++) {
const cell = row.cells.get(j);
for (let j = 0; j < 9 && j < row.cells.length; j++) {
const cell = row.cells[j];
const cellOffset = rowOffset + j * 4;
if (cell === undefined) {
continue;
}
buf.writeUInt16LE(cell.a, cellOffset + 0);
buf.writeUInt16LE(cell.b, cellOffset + 2);
}
@ -94,11 +87,11 @@ export function loadProfile2(res: LoadProfileResponse) {
buf.writeUInt32LE(res.settings.pack, 0x03d8);
buf.writeUInt32LE(res.dpoint, 0x03e8);
buf.writeUInt32LE(res.fame, 0x0404);
writeSjisStr(buf, 0x03ee, 0x40e, res.name);
iconv.encode(res.name + "\0", "shift_jis").copy(buf, 0x03ee);
buf.writeUInt8(res.story.y, 0x0670);
buf.writeUInt16LE(res.story.x, 0x06bc);
encodeMission(res.missions.solo).copy(buf, 0x06e4);
encodeChara1(res.chara).copy(buf, 0x070c);
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);

View File

@ -1,11 +1,12 @@
import iconv from "iconv-lite";
import { encodeBitmap } from "./_bitmap";
import { encodeCar } from "./_car";
import { encodeChara1 } from "./_chara";
import { encodeChara } from "./_chara";
import { encodeMission } from "./_mission";
import { LoadProfileResponse } from "../response/loadProfile";
import { writeSjisStr } from "../../util/bin";
import { LoadProfileResponse3 } from "../response/loadProfile";
export function loadProfile3(res: LoadProfileResponse) {
export function loadProfile3(res: LoadProfileResponse3) {
const buf = Buffer.alloc(0x0ea0);
// Initialize all TA grades to uhh... fuck knows
@ -37,22 +38,14 @@ export function loadProfile3(res: LoadProfileResponse) {
// space for story cells, and 27 rows * 19 bytes per row = 513 bytes, which
// is the max that will fit (the final byte of each row is unused).
for (let i = 0; i < 27; i++) {
const row = res.story.rows.get(i);
for (let i = 0; i < 27 && i < res.story.rows.length; i++) {
const row = res.story.rows[i];
const rowOffset = 0x0256 + i * 0x13;
if (row === undefined) {
continue;
}
for (let j = 0; j < 9; j++) {
const cell = row.cells.get(j);
for (let j = 0; j < 9 && j < row.cells.length; j++) {
const cell = row.cells[j];
const cellOffset = rowOffset + j * 2;
if (cell === undefined) {
continue;
}
buf.writeUInt8(cell.a, cellOffset + 0);
buf.writeUInt8(cell.b, cellOffset + 1);
}
@ -94,11 +87,11 @@ export function loadProfile3(res: LoadProfileResponse) {
buf.writeUInt32LE(res.settings.pack, 0x04b4);
buf.writeUInt32LE(res.dpoint, 0x04c4);
buf.writeUInt32LE(res.fame, 0x04e0);
writeSjisStr(buf, 0x04ca, 0x04ea, res.name);
iconv.encode(res.name + "\0", "shift_jis").copy(buf, 0x04ca);
buf.writeUInt8(res.story.y, 0x080c);
buf.writeUInt16LE(res.story.x, 0x0828);
encodeMission(res.missions.solo).copy(buf, 0x0858);
encodeChara1(res.chara).copy(buf, 0x0880);
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);

View File

@ -0,0 +1,9 @@
import { LoadRewardTableResponse } from "../response/loadRewardTable";
export function loadRewardTable(res: LoadRewardTableResponse) {
const buf = Buffer.alloc(0x01c0);
buf.writeInt16LE(0x0087, 0x0000);
return buf;
}

View File

@ -0,0 +1,31 @@
import { LoadServerListResponse } from "../response/loadServerList";
export function loadServerList(res: LoadServerListResponse) {
const buf = Buffer.alloc(0x04b0);
buf.writeInt16LE(0x0007, 0x0000);
buf.writeInt16LE(res.status, 0x0002);
buf.write(res.userDb.addr, 0x0004);
buf.writeInt16LE(res.userDb.tcp, 0x0084);
buf.writeInt16LE(res.userDb.http, 0x0086);
buf.write(res.matchAddr, 0x0088);
buf.writeInt16LE(res.matchPort.tcp, 0x0108);
buf.writeInt16LE(res.matchPort.udpSend, 0x010a);
buf.writeInt16LE(res.matchPort.udpRecv, 0x010c);
buf.writeInt16LE(res.tagMatchPort.tcp, 0x010e);
buf.writeInt16LE(res.tagMatchPort.udpSend, 0x0110);
buf.writeInt16LE(res.tagMatchPort.udpRecv, 0x0112);
buf.write(res.event.addr, 0x0114);
buf.writeInt16LE(res.event.tcp, 0x0194);
buf.write(res.screenshot.addr, 0x0198);
buf.writeInt16LE(res.screenshot.tcp, 0x0218);
buf.write(res.pingReturn, 0x021c);
buf.write(res.echo1.addr, 0x029c);
buf.write(res.echo2.addr, 0x031c);
buf.writeInt16LE(res.echo1.udp, 0x39c);
buf.writeInt16LE(res.echo2.udp, 0x39e);
buf.write(res.newsUrl, 0x03a0);
buf.write(res.reportErrorUrl, 0x0424);
return buf;
}

View File

@ -0,0 +1,12 @@
import { encodeBitmap } from "./_bitmap";
import { LoadStockerResponse } from "../response/loadStocker";
export function loadStocker(res: LoadStockerResponse) {
const buf = Buffer.alloc(0x00a0);
buf.writeInt16LE(0x00a8, 0x0000);
buf.writeUInt8(res.status, 0x0002);
encodeBitmap(res.backgrounds, 0x24).copy(buf, 0x0003);
return buf;
}

View File

@ -0,0 +1,10 @@
import { LoadTeamRankingResponse } from "../response/loadTeamRanking";
export function loadTeamRanking(res: LoadTeamRankingResponse): Buffer {
const buf = Buffer.alloc(0x0ba0);
buf.writeUInt16LE(0x00ba, 0x0000);
// Row stride is 0x94
return buf;
}

View File

@ -1,4 +1,5 @@
import { writeSjisStr } from "../../util/bin";
import iconv from "iconv-lite";
import { LoadTopTenResponse } from "../response/loadTopTen";
export function loadTopTen(res: LoadTopTenResponse): Buffer {
@ -48,9 +49,9 @@ export function loadTopTen(res: LoadTopTenResponse): Buffer {
buf.writeUInt8(row.field_0E ? 1 : 0, innerOff + 0x000e); // Boolean
buf.writeUInt8(row.field_0F ? 1 : 0, innerOff + 0x000f); // Boolean
buf.writeUInt8(row.field_10, innerOff + 0x0010);
writeSjisStr(buf, innerOff + 0x0014, innerOff + 0x0028, row.driverName);
writeSjisStr(buf, innerOff + 0x0028, innerOff + 0x0048, row.team.name);
writeSjisStr(buf, innerOff + 0x0048, innerOff + 0x0074, row.shopName);
iconv.encode(row.driverName, "shift_jis").copy(buf, innerOff + 0x0014);
iconv.encode(row.team.name, "shift_jis").copy(buf, innerOff + 0x0028);
iconv.encode(row.shopName, "shift_jis").copy(buf, innerOff + 0x0048);
buf.writeUInt32LE(row.team.nameBg, innerOff + 0x0074);
buf.writeUInt16LE(row.team.nameFx, innerOff + 0x0078);
buf.writeUInt8(row.field_7C, innerOff + 0x007c);
@ -69,7 +70,7 @@ export function loadTopTen(res: LoadTopTenResponse): Buffer {
buf.writeUInt8(trailer.courseId, offset + 0x02);
buf.writeUInt8(trailer.isNight ? 1 : 0, offset + 0x03);
buf.writeUInt32LE(trailer.totalMsec, offset + 0x04);
writeSjisStr(buf, offset + 0x08, offset + 0x1c, trailer.name);
iconv.encode(trailer.name, "shift_jis").copy(buf, offset + 0x08);
} else {
buf.writeUInt8(0xff, offset + 0x02);
}

View File

@ -6,7 +6,7 @@ const statusCodes = {
obsolete: 0x0002,
};
export function lockProfile1(res: LockProfileResponse) {
export function lockProfile(res: LockProfileResponse) {
const buf = Buffer.alloc(0x0020);
buf.writeInt16LE(0x006a, 0x0000);
@ -16,14 +16,3 @@ export function lockProfile1(res: LockProfileResponse) {
return buf;
}
export function lockProfile2(res: LockProfileResponse) {
const buf = Buffer.alloc(0x0020);
buf.writeInt16LE(0x0066, 0x0000);
buf.writeInt8(statusCodes[res.status], 0x0018);
buf.writeInt16LE(res.field_001A, 0x001a);
buf.writeInt32LE(res.lockExpiry.getTime() / 1000, 0x001c);
return buf;
}

View File

@ -0,0 +1,10 @@
import { LockProfileExtendResponse } from "../response/lockProfileExtend";
export function lockProfileExtend(res: LockProfileExtendResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x006e, 0x0000);
buf.writeUInt8(res.status, 0x0004);
return buf;
}

View File

@ -0,0 +1,38 @@
import {
SaveExpeditionResponse,
SaveExpeditionResponse1,
SaveExpeditionResponse2,
} from "../response/saveExpedition";
export function saveExpedition(res: SaveExpeditionResponse): Buffer {
switch (res.format) {
case 1:
return saveExpedition1(res);
case 2:
return saveExpedition2(res);
default:
const exhaust: never = res;
throw new Error(`Unsupported data format ${res["format"]}`);
}
}
function saveExpedition1(res: SaveExpeditionResponse1): Buffer {
// in awe of the size of this lad
const buf = Buffer.alloc(0x17c0);
buf.writeInt16LE(0x008d, 0x0000);
return buf;
}
function saveExpedition2(res: SaveExpeditionResponse2): Buffer {
// absolute unit
const buf = Buffer.alloc(0x18ac);
buf.writeUInt16LE(0x0140, 0x0000);
return buf;
}

View File

@ -0,0 +1,10 @@
import { SaveGarageResponse } from "../response/saveGarage";
export function saveGarage(res: SaveGarageResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x008f, 0x0000);
buf.writeUInt16LE(res.status, 0x0002);
return buf;
}

View File

@ -0,0 +1,10 @@
import { SaveNewCarResponse } from "../response/saveNewCar";
export function saveNewCar(res: SaveNewCarResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x007a, 0x0000);
buf.writeUInt8(res.status, 0x0002);
return buf;
}

View File

@ -0,0 +1,9 @@
import { SaveTimeAttackResponse } from "../response/saveTimeAttack";
export function saveTimeAttack(res: SaveTimeAttackResponse): Buffer {
const buf = Buffer.alloc(0x00b0);
buf.writeUInt16LE(0x00ce, 0x0000);
return buf;
}

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