43 Commits
Author SHA1 Message Date
Tau ff4545d468 aimedb: Return decimal IDm as access code 2019-11-06 17:28:30 -05:00
Tau 7fd6f74986 db: Fix expected schema version 2019-11-06 17:28:30 -05:00
Tau 2478bc5ceb idz: Implement team leadership transfer 2019-11-06 17:28:30 -05:00
Tau 3bee10d948 idz: Implement team member add/remove 2019-11-06 17:28:30 -05:00
Tau 2bd9ce919a idz: Implement saveTeamBanner 2019-11-06 17:28:30 -05:00
Tau a3df4df277 idz: Wire up team create and load 2019-11-06 17:28:30 -05:00
Tau ec214d5beb idz: Add team "reservation" repository
This is a hack necessary to support certain quirks of the
Initial D Zero profile creation process.
2019-11-06 17:28:30 -05:00
Tau a5dbdef33f idz: Add TeamAutoRepository 2019-11-06 17:28:30 -05:00
Tau be48619a4a idz: Add TeamMemberRepository 2019-11-06 17:28:30 -05:00
Tau c6a67bd647 idz: Add TeamRepository 2019-11-06 17:28:30 -05:00
Tau d1c3051a5e idz: Add initial team tables 2019-11-06 17:28:30 -05:00
Tau f874f56816 idz: Expose some row mappers 2019-11-06 17:28:30 -05:00
Tau 4f044de9fb idz: Add "Auto team" data model
New players get packed into automatically-created teams whose
names follow an automated pattern. Add data models to support this.
2019-11-06 17:28:30 -05:00
Tau 33e2f55dce idz: Clean up Team and TeamMember data models 2019-11-06 17:28:30 -05:00
Tau 85510ee0e1 idz: Tweak ProfileRepository interface 2019-11-06 17:28:30 -05:00
Tau 9c06d6635f idz: Rename encoder funcs
Avoid name collision between nouns and verbs.
2019-11-06 17:28:30 -05:00
Tau c2b0bcabc4 idz: Remove profile ext id
Turns out (while researching team creation) the profile ext id
is just the aime id. After creating a team a profile discovery
request is immediately issued with the "profile id" in the field
that holds the aime id during initial profile load.
2019-11-06 17:28:30 -05:00
Tau 7c96253160 idz: Trim trailing NULs in team name 2019-11-06 17:28:30 -05:00
Tau faaeb53cd9 idz: Label banner field in team creation 2019-11-06 17:28:30 -05:00
Tau 1aaae51eab Add ID generation sprocs for manual db interventions 2019-11-06 17:28:30 -05:00
Tau 98f6fb9d5b idz: Fix backwards compatibility
Silently drop OOB course IDs instead of erroring. Those IDs may
come from later versions which have bigger arrays in that spot.
2019-11-06 17:28:30 -05:00
Tau 4d7a0d64a1 idz: Fix loadTopTen harder
Unconditionally return a response; the team ID and profile ID
don't actually seem to affect what the client expects to receive
(world top ten must be returned in all cases).
2019-11-06 17:28:30 -05:00
Tau 9dd45558ec aimedb: Stub a FeliCa lookup response 2019-11-06 17:28:30 -05:00
Tau af8cdf3072 idz: TA world record fixes 2019-11-06 17:28:30 -05:00
Tau a3705730ce idz: Stub new UpdateStoryClearNum 2019-11-06 17:28:30 -05:00
Tau 83ddfcc022 idz: Decode new LoadTopTenRequest
Response code and presumably layout is still the same.
2019-11-06 17:28:30 -05:00
Tau 5c6b8f27a4 idz: Add LockProfileExtend 2019-11-06 17:28:30 -05:00
Tau 6a89e6a15a idz: Fix LockProfileRequest type name 2019-11-06 17:28:30 -05:00
Tau 1e809f59a3 idz: Implement new req code for TA save..? 2019-11-06 17:28:30 -05:00
Tau cb5f281236 TA load fixup 2019-11-06 17:28:30 -05:00
Tau 2a365651a8 Fix v3 story 2019-11-06 17:28:30 -05:00
Tau 2f48efdbb9 idz: Add new saveExpedition msg format 2019-11-06 17:28:30 -05:00
Tau 0f67c00664 db: Add connectivity and schema version check 2019-11-06 17:28:30 -05:00
Tau 2fd8081a8f idz: Add "loadGacha" message implementation 2019-11-06 17:28:30 -05:00
Tau ddad7efd67 Profile re-enable 2019-11-06 17:28:30 -05:00
Tau 6be956ad3c *cries in spanish* 2019-11-06 17:28:30 -05:00
Tau 5d8e135c07 idz: Add LoadGeneralRewardRequest detail 2019-11-06 17:28:30 -05:00
Tau 2392aaa205 idz: Deal with multiple load2on2 formats 2019-11-06 17:28:30 -05:00
Tau dfdd775370 idz: Fill in Load2on2Request details 2019-11-06 17:28:30 -05:00
Tau 5b6534ae9a wip 2019-11-06 17:28:30 -05:00
Tau fecc0e7981 wip 2019-11-06 17:28:30 -05:00
Tau 5bac5b6abc idz: Tweak how request variants are modelled 2019-11-06 17:28:30 -05:00
Tau 6b58c4d722 idz: Add server version to config message
This allows idz v1.31 to proceed with network startup.
2019-11-06 17:28:30 -05:00
158 changed files with 2611 additions and 614 deletions
+52 -3
View File
@@ -12,7 +12,6 @@ create table "idz"."profile" (
references "aime"."player"("id")
on delete cascade,
-- TODO shop_id
"ext_id" integer not null,
"name" text not null,
"lv" smallint not null,
"exp" integer not null,
@@ -21,8 +20,7 @@ create table "idz"."profile" (
"mileage" integer not null,
"register_time" timestamp not null,
"access_time" timestamp not null,
constraint "profile_player_uq" unique ("player_id"),
constraint "profile_ext_id_uq" unique ("ext_id")
constraint "profile_player_uq" unique ("player_id")
);
create table "idz"."chara" (
@@ -192,3 +190,54 @@ create table "idz"."unlocks" (
"music" integer not null,
"last_mileage_reward" integer not null
);
create table "idz"."team" (
"id" bigint primary key not null,
"ext_id" integer not null,
"name" text not null,
"name_bg" smallint not null,
"name_fx" smallint not null,
"register_time" timestamp not null,
constraint "team_uq" unique ("ext_id")
);
create table "idz"."team" (
"id" bigint primary key not null,
"ext_id" integer not null,
"name" text not null,
"name_bg" smallint not null,
"name_fx" smallint not null,
"register_time" timestamp not null,
constraint "team_uq" unique ("ext_id")
);
create table "idz"."team_auto" (
"id" bigint primary key not null
references "idz"."team"("id")
on delete cascade,
"serial_no" smallint not null,
"name_idx" smallint not null,
constraint "team_auto_uq" unique ("serial_no", "name_idx")
);
create table "idz"."team_member" (
"id" bigint primary key not null
references "idz"."profile"("id")
on delete cascade,
"team_id" bigint not null
references "idz"."team"("id")
on delete cascade,
"join_time" timestamp not null,
"leader" boolean not null
);
create table "idz"."team_reservation" (
"id" bigint primary key not null
references "aime"."player"("id")
on delete cascade,
"team_id" bigint not null
references "idz"."team"("id")
on delete cascade,
"join_time" timestamp not null,
"leader" boolean not null
);
+1 -1
View File
@@ -1,2 +1,2 @@
create table "meta" ("schemaver" integer not null);
insert into "meta" values (0);
insert into "meta" values (3);
+81
View File
@@ -0,0 +1,81 @@
-- Helper functions which might be useful in one-off queries.
-- These are not utilised by any services; those have their own ID generators.
create or replace function idnum(
str text)
returns bigint as $$
declare
num bigint;
x integer;
begin
num := 0;
for i in 1..11 loop
x := ascii(substr(str, i, 1));
num := num << 6;
if x >= 65 and x <= 90 then
-- A to Z
num = num | (x - 65);
elsif x >= 97 and x <= 122 then
-- a to z
num = num | (x - 71);
elsif x >= 48 and x <= 58 then
-- 0 to 9
num = num | (x + 4);
elsif x = 45 then
-- Dash
num = num | 62;
elsif x = 95 then
-- Underscore
num = num | 63;
else
raise 'Bad input';
end if;
end loop;
return num;
end;
$$ language plpgsql;
create or replace function idstr(
num bigint)
returns text as $$
declare
a text;
pos integer;
str text;
begin
a := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
str := '';
for i in 1..11 loop
pos := (num & 63);
str := substr(a, pos + 1, 1) || str;
num := num >> 6;
end loop;
return str;
end;
$$ language plpgsql;
create or replace function newid()
returns bigint as $$
declare
bytes bytea;
result bigint;
begin
bytes := gen_random_bytes(8);
result := 0;
for i in 0..7 loop
result := (result << 8) | get_byte(bytes, i);
end loop;
-- Truncate high bit to ensure result is positive
-- This value is just the decimal representation of 0x7FFFFFFF`FFFFFFFF.
return result & 9223372036854775807;
end;
$$ language plpgsql;
+81
View File
@@ -0,0 +1,81 @@
-- Helper functions which might be useful in one-off queries.
-- These are not utilised by any services; those have their own ID generators.
create or replace function idnum(
str text)
returns bigint as $$
declare
num bigint;
x integer;
begin
num := 0;
for i in 1..11 loop
x := ascii(substr(str, i, 1));
num := num << 6;
if x >= 65 and x <= 90 then
-- A to Z
num = num | (x - 65);
elsif x >= 97 and x <= 122 then
-- a to z
num = num | (x - 71);
elsif x >= 48 and x <= 58 then
-- 0 to 9
num = num | (x + 4);
elsif x = 45 then
-- Dash
num = num | 62;
elsif x = 95 then
-- Underscore
num = num | 63;
else
raise 'Bad input';
end if;
end loop;
return num;
end;
$$ language plpgsql;
create or replace function idstr(
num bigint)
returns text as $$
declare
a text;
pos integer;
str text;
begin
a := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
str := '';
for i in 1..11 loop
pos := (num & 63);
str := substr(a, pos + 1, 1) || str;
num := num >> 6;
end loop;
return str;
end;
$$ language plpgsql;
create or replace function newid()
returns bigint as $$
declare
bytes bytea;
result bigint;
begin
bytes := gen_random_bytes(8);
result := 0;
for i in 0..7 loop
result := (result << 8) | get_byte(bytes, i);
end loop;
-- Truncate high bit to ensure result is positive
-- This value is just the decimal representation of 0x7FFFFFFF`FFFFFFFF.
return result & 9223372036854775807;
end;
$$ language plpgsql;
@@ -0,0 +1,3 @@
alter table "idz"."profile" drop column "ext_id";
update "meta" set "schemaver" = 1;
+42
View File
@@ -0,0 +1,42 @@
create table "idz"."team" (
"id" bigint primary key not null,
"ext_id" integer not null,
"name" text not null,
"name_bg" smallint not null,
"name_fx" smallint not null,
"register_time" timestamp not null,
constraint "team_uq" unique ("ext_id")
);
create table "idz"."team_auto" (
"id" bigint primary key not null
references "idz"."team"("id")
on delete cascade,
"serial_no" smallint not null,
"name_idx" smallint not null,
constraint "team_auto_uq" unique ("serial_no", "name_idx")
);
create table "idz"."team_member" (
"id" bigint primary key not null
references "idz"."profile"("id")
on delete cascade,
"team_id" bigint not null
references "idz"."team"("id")
on delete cascade,
"join_time" timestamp not null,
"leader" boolean not null
);
create table "idz"."team_reservation" (
"id" bigint primary key not null
references "aime"."player"("id")
on delete cascade,
"team_id" bigint not null
references "idz"."team"("id")
on delete cascade,
"join_time" timestamp not null,
"leader" boolean not null
);
update "meta" set "schemaver" = 2;
+10
View File
@@ -19,6 +19,15 @@ function readRegisterRequest(msg: Buffer): Request.RegisterRequest {
};
}
function readFeliCaLookupRequest(msg: Buffer): Request.FeliCaLookupRequest {
return {
...begin(msg),
type: "felica_lookup",
idm: msg.slice(0x0020, 0x0028).toString("hex"),
pmm: msg.slice(0x0028, 0x0030).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.
@@ -79,6 +88,7 @@ function readGoodbyeRequest(msg: Buffer): Request.GoodbyeRequest {
const readers = new Map<number, (msg: Buffer) => Request.AimeRequest>();
readers.set(0x0001, readFeliCaLookupRequest);
readers.set(0x0004, readLookupRequest);
readers.set(0x0005, readRegisterRequest);
readers.set(0x0009, readLogRequest);
+10
View File
@@ -32,6 +32,14 @@ export class Encoder extends Transform {
let buf: Buffer;
switch (msg.type) {
case "felica_lookup":
buf = begin(0x0030);
buf.writeUInt16LE(0x0003, 0x0004); // cmd code
buf.writeUInt16LE(msg.status, 0x0008);
buf.write(msg.accessCode, 0x0024, "hex");
break;
case "hello":
buf = begin(0x0020);
buf.writeUInt16LE(0x0065, 0x0004); // cmd code
@@ -92,6 +100,8 @@ export class Encoder extends Transform {
break;
default:
const exhaust: never = msg;
return callback(new Error("Unimplemented response type"));
}
+25
View File
@@ -22,6 +22,26 @@ function campaign(
return { type: req.type, status: 1 };
}
function feliCaLookup(
rep: Repositories,
req: Req.FeliCaLookupRequest,
now: Date
): Res.FeliCaLookupResponse {
console.log("Aimedb: FeliCa access code lookup");
// Well, this access code transformation is the million dollar question eh
// Return a decimal representation for now.
const num = BigInt("0x" + req.idm);
let accessCode = num.toString();
while (accessCode.length < 20) {
accessCode = "0" + accessCode;
}
return { type: req.type, status: 1, accessCode };
}
async function lookup(
rep: Repositories,
req: Req.LookupRequest,
@@ -88,6 +108,9 @@ export async function dispatch(
case "campaign":
return campaign(rep, req, now);
case "felica_lookup":
return feliCaLookup(rep, req, now);
case "lookup":
return lookup(rep, req, now);
@@ -106,6 +129,8 @@ export async function dispatch(
return undefined;
default:
const exhaust: never = req;
throw new Error("Aimedb: Handler not implemented!");
}
}
+7
View File
@@ -3,6 +3,12 @@ export interface AimeRequestBase {
keychipId: string;
}
export interface FeliCaLookupRequest extends AimeRequestBase {
type: "felica_lookup";
idm: string;
pmm: string;
}
export interface RegisterRequest extends AimeRequestBase {
type: "register";
luid: string;
@@ -43,6 +49,7 @@ export interface GoodbyeRequest {
}
export type AimeRequest =
| FeliCaLookupRequest
| CampaignRequest
| GoodbyeRequest
| HelloRequest
+6
View File
@@ -6,6 +6,11 @@ export interface AimeResponseBase {
status: number;
}
export interface FeliCaLookupResponse extends AimeResponseBase {
type: "felica_lookup";
accessCode: string;
}
export interface CampaignResponse extends AimeResponseBase {
type: "campaign";
}
@@ -38,6 +43,7 @@ export interface RegisterResponse extends AimeResponseBase {
}
export type AimeResponse =
| FeliCaLookupResponse
| CampaignResponse
| HelloResponse
| LogResponse
+25 -1
View File
@@ -3,9 +3,14 @@ import { Pool, PoolClient } from "pg";
export type Id<T> = bigint & { __id: T };
const currentSchemaVer = 2;
const pool = new Pool();
const fence = testConnection();
export async function connect(): Promise<PoolClient> {
await fence;
export function connect(): Promise<PoolClient> {
return pool.connect();
}
@@ -30,3 +35,22 @@ export function generateExtId(): number {
return buf.readUInt32BE(0);
}
async function testConnection(): Promise<void> {
const conn = await pool.connect();
try {
const { rows } = await conn.query("select schemaver from meta");
const { schemaver } = rows[0];
if (schemaver !== currentSchemaVer) {
throw new Error(
`Expected schema version ${currentSchemaVer}, db is on ${schemaver}`
);
}
console.log("SQL DB: Connection established");
} finally {
conn.release();
}
}
-25
View File
@@ -1,25 +0,0 @@
import * as sql from "sql-bricks";
import { ClientBase } from "pg";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { Id } from "../../db";
export async function _findProfile(
conn: ClientBase,
extId: ExtId<Profile>
): Promise<Id<Profile>> {
const lookupSql = sql
.select("r.id")
.from("idz.profile r")
.where("r.ext_id", extId)
.toParams();
const { rows } = await conn.query(lookupSql);
if (rows.length > 0) {
return rows[0].id as Id<Profile>;
} else {
throw new Error("Profile not found");
}
}
+6 -8
View File
@@ -1,22 +1,21 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks";
import { _findProfile } from "./_util";
import { BackgroundCode, ExtId } from "../model/base";
import { BackgroundCode } from "../model/base";
import { Profile } from "../model/profile";
import { FlagRepository } from "../repo";
import { generateId } from "../../db";
import { generateId, Id } from "../../db";
export class SqlBackgroundsRepository
implements FlagRepository<BackgroundCode> {
constructor(private readonly _conn: ClientBase) {}
async loadAll(extId: ExtId<Profile>): Promise<Set<BackgroundCode>> {
async loadAll(id: Id<Profile>): Promise<Set<BackgroundCode>> {
const loadSql = sql
.select("bg.background_no")
.from("idz.background_unlock bg")
.join("idz.profile p", { "bg.profile_id": "p.id" })
.where("p.ext_id", extId)
.where("p.id", id)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -30,11 +29,10 @@ export class SqlBackgroundsRepository
}
async saveAll(
extId: ExtId<Profile>,
profileId: Id<Profile>,
flags: Set<BackgroundCode>
): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
const existing = await this.loadAll(extId);
const existing = await this.loadAll(profileId);
for (const flag of flags) {
if (existing.has(flag)) {
+10 -17
View File
@@ -1,12 +1,10 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Car, CarSelector } from "../model/car";
import { Profile } from "../model/profile";
import { CarRepository } from "../repo";
import { generateId } from "../../db";
import { generateId, Id } from "../../db";
function _extractRow(row: any): Car {
return {
@@ -31,12 +29,11 @@ function _extractRow(row: any): Car {
export class SqlCarRepository implements CarRepository {
constructor(private readonly _conn: ClientBase) {}
async countCars(extId: ExtId<Profile>): Promise<number> {
async countCars(profileId: Id<Profile>): Promise<number> {
const countSql = sql
.select("count(*) result")
.from("idz.car c")
.join("idz.profile p", { "c.profile_id": "p.id" })
.where("p.ext_id", extId)
.where("c.profile_id", profileId)
.toParams();
const { rows } = await this._conn.query(countSql);
@@ -45,12 +42,11 @@ export class SqlCarRepository implements CarRepository {
return parseInt(row.result, 10);
}
async loadAllCars(extId: ExtId<Profile>): Promise<Car[]> {
async loadAllCars(profileId: Id<Profile>): Promise<Car[]> {
const loadSql = sql
.select("c.*")
.from("idz.car c")
.join("idz.profile p", { "c.profile_id": "p.id" })
.where("p.ext_id", extId)
.where("c.profile_id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -58,13 +54,12 @@ export class SqlCarRepository implements CarRepository {
return rows.map(_extractRow);
}
async loadSelectedCar(extId: ExtId<Profile>): Promise<Car> {
async loadSelectedCar(profileId: Id<Profile>): Promise<Car> {
const loadSql = sql
.select("c.*")
.from("idz.car c")
.join("idz.car_selection s", { "c.id": "s.car_id" })
.join("idz.profile p", { "s.id": "p.id" })
.where("p.ext_id", extId)
.where("s.id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -72,11 +67,11 @@ export class SqlCarRepository implements CarRepository {
return _extractRow(rows[0]);
}
async saveCar(extId: ExtId<Profile>, car: Car): Promise<void> {
async saveCar(profileId: Id<Profile>, car: Car): Promise<void> {
const saveSql = sql
.insert("idz.car", {
id: generateId(),
profile_id: await _findProfile(this._conn, extId),
profile_id: profileId,
selector: car.selector,
field_00: car.field_00,
field_02: car.field_02,
@@ -116,11 +111,9 @@ export class SqlCarRepository implements CarRepository {
}
async saveSelection(
extId: ExtId<Profile>,
profileId: Id<Profile>,
selector: CarSelector
): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
const findSql = sql
.select("c.id")
.from("idz.car c")
+21 -21
View File
@@ -1,43 +1,43 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Chara } from "../model/chara";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
export function _extractChara(row: any): Chara {
return {
gender: row.gender,
field_02: row.field_02,
field_04: row.field_04,
field_06: row.field_06,
field_08: row.field_08,
field_0A: row.field_0A,
field_0C: row.field_0C,
field_0E: row.field_0E,
title: row.title,
background: row.background,
};
}
export class SqlCharaRepository implements FacetRepository<Chara> {
constructor(private readonly _conn: ClientBase) {}
async load(extId: ExtId<Profile>): Promise<Chara> {
async load(profileId: Id<Profile>): Promise<Chara> {
const loadSql = sql
.select("c.*")
.from("idz.profile p")
.join("idz.chara c", { "p.id": "c.id" })
.where("p.ext_id", extId)
.from("idz.chara c")
.where("c.id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
return {
gender: row.gender,
field_02: row.field_02,
field_04: row.field_04,
field_06: row.field_06,
field_08: row.field_08,
field_0A: row.field_0A,
field_0C: row.field_0C,
field_0E: row.field_0E,
title: row.title,
background: row.background,
};
return _extractChara(row);
}
async save(extId: ExtId<Profile>, chara: Chara): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
async save(profileId: Id<Profile>, chara: Chara): Promise<void> {
const saveSql = sql
.insert("idz.chara", {
id: profileId,
+4 -8
View File
@@ -1,21 +1,19 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { CourseNo, ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { CoursePlaysRepository } from "../repo";
import { generateId } from "../../db";
import { generateId, Id } from "../../db";
export class SqlCoursePlaysRepository implements CoursePlaysRepository {
constructor(private readonly _conn: ClientBase) {}
async loadAll(extId: ExtId<Profile>): Promise<Map<CourseNo, number>> {
async loadAll(profileId: Id<Profile>): Promise<Map<CourseNo, number>> {
const loadSql = sql
.select("cp.course_no", "cp.count")
.from("idz.course_plays cp")
.join("idz.profile p", { "cp.profile_id": "p.id" })
.where("p.ext_id", extId)
.where("cp.profile_id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -29,11 +27,9 @@ export class SqlCoursePlaysRepository implements CoursePlaysRepository {
}
async saveAll(
extId: ExtId<Profile>,
profileId: Id<Profile>,
plays: Map<CourseNo, number>
): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
for (const [k, v] of plays) {
const saveSql = sql
.insert("idz.course_plays", {
+20
View File
@@ -8,6 +8,10 @@ import { SqlMissionsRepository } from "./missions";
import { SqlProfileRepository } from "./profile";
import { SqlSettingsRepository } from "./settings";
import { SqlStoryRepository } from "./story";
import { SqlTeamRepository } from "./team";
import { SqlTeamAutoRepository } from "./teamAuto";
import { SqlTeamMemberRepository } from "./teamMember";
import { SqlTeamReservationRepository } from "./teamReservation";
import { SqlTicketsRepository } from "./tickets";
import { SqlTimeAttackRepository } from "./timeAttack";
import { SqlTitlesRepository } from "./titles";
@@ -51,6 +55,22 @@ class TransactionImpl implements Repo.Transaction {
return new SqlStoryRepository(this._conn);
}
teams(): Repo.TeamRepository {
return new SqlTeamRepository(this._conn);
}
teamAuto(): Repo.TeamAutoRepository {
return new SqlTeamAutoRepository(this._conn);
}
teamMembers(): Repo.TeamMemberRepository {
return new SqlTeamMemberRepository(this._conn);
}
teamReservations(): Repo.TeamReservationRepository {
return new SqlTeamReservationRepository(this._conn);
}
tickets(): Repo.FacetRepository<Model.Tickets> {
return new SqlTicketsRepository(this._conn);
}
+4 -16
View File
@@ -1,8 +1,6 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { MissionGrid, MissionState } from "../model/mission";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
@@ -11,9 +9,7 @@ import { generateId, Id } from "../../db";
export class SqlMissionsRepository implements FacetRepository<MissionState> {
constructor(private readonly _conn: ClientBase) {}
private async _load(
extId: ExtId<Profile>
): Promise<[MissionState, Id<Profile>]> {
async load(profileId: Id<Profile>): Promise<MissionState> {
const result: MissionState = {
solo: new Array<MissionGrid>(),
team: new Array<MissionGrid>(),
@@ -32,8 +28,6 @@ export class SqlMissionsRepository implements FacetRepository<MissionState> {
result.team.push(teamGrid);
}
const profileId = await _findProfile(this._conn, extId);
const loadSoloSql = sql
.select("sm.*")
.from("idz.solo_mission_state sm")
@@ -46,17 +40,11 @@ export class SqlMissionsRepository implements FacetRepository<MissionState> {
result.solo[row.grid_no].cells[row.cell_no] = row.value;
}
return [result, profileId];
return result;
}
async load(extId: ExtId<Profile>): Promise<MissionState> {
const [mission] = await this._load(extId);
return mission;
}
async save(extId: ExtId<Profile>, mission: MissionState): Promise<void> {
const [existing, profileId] = await this._load(extId);
async save(profileId: Id<Profile>, mission: MissionState): Promise<void> {
const existing = await this.load(profileId);
for (let i = 0; i < mission.solo.length; i++) {
const exGrid = existing.solo[i].cells;
+32 -47
View File
@@ -1,35 +1,41 @@
import * as sql from "sql-bricks";
import { ClientBase } from "pg";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { Team } from "../model/team";
import { ProfileSpec, ProfileRepository } from "../repo";
import { generateExtId, generateId, Id } from "../../db";
import { ProfileRepository } from "../repo";
import { generateId, Id } from "../../db";
import { AimeId } from "../../model";
function _extractRow(row: any): Profile {
export function _extractProfile(row: any): Profile {
return {
id: row.ext_id,
teamId: 2 as ExtId<Team>, // TODO
aimeId: row.aime_id,
name: row.name,
lv: row.lv,
exp: row.exp,
fame: row.fame,
dpoint: row.dpoint,
mileage: row.mileage,
accessTime: row.access_time,
registerTime: row.register_time,
};
}
export class SqlProfileRepository implements ProfileRepository {
constructor(private readonly _conn: ClientBase) {}
private async _tryLoadByAimeId(
aimeId: AimeId
): Promise<Profile | undefined> {
async find(aimeId: AimeId): Promise<Id<Profile>> {
const profileId = await this.peek(aimeId);
if (profileId === undefined) {
throw new Error(`Profile not found for Aime ID ${aimeId}`);
}
return profileId;
}
async peek(aimeId: AimeId): Promise<Id<Profile> | undefined> {
const lookupSql = sql
.select("p.*")
.select("p.id")
.from("idz.profile p")
.join("aime.player r", { "p.player_id": "r.id" })
.where("r.ext_id", aimeId)
@@ -42,38 +48,23 @@ export class SqlProfileRepository implements ProfileRepository {
return undefined;
}
return _extractRow(row);
return row.id;
}
async discoverByAimeId(aimeId: AimeId): Promise<boolean> {
const result = await this._tryLoadByAimeId(aimeId);
return result !== undefined;
}
async loadByAimeId(aimeId: AimeId): Promise<Profile> {
const result = await this._tryLoadByAimeId(aimeId);
if (result === undefined) {
throw new Error("Profile not found for Aime ID");
}
return result;
}
async load(extId: ExtId<Profile>): Promise<Profile> {
async load(id: Id<Profile>): Promise<Profile> {
const loadSql = sql
.select("p.*")
.select("p.*", "r.ext_id as aime_id")
.from("idz.profile p")
.where("ext_id", extId)
.join("aime.player r", { "p.player_id": "r.id" })
.where("p.id", id)
.toParams();
const { rows } = await this._conn.query(loadSql);
return _extractRow(rows[0]);
return _extractProfile(rows[0]);
}
async save(profile: Profile, timestamp: Date): Promise<void> {
async save(id: Id<Profile>, profile: Profile): Promise<void> {
const saveSql = sql
.update("idz.profile", {
lv: profile.lv,
@@ -81,23 +72,19 @@ export class SqlProfileRepository implements ProfileRepository {
fame: profile.fame,
dpoint: profile.dpoint,
mileage: profile.mileage,
access_time: timestamp,
access_time: profile.accessTime,
})
.where("ext_id", profile.id)
.where("id", id)
.toParams();
await this._conn.query(saveSql);
}
async create(
aimeId: AimeId,
profile: ProfileSpec,
timestamp: Date
): Promise<ExtId<Profile>> {
async create(profile: Profile): Promise<Id<Profile>> {
const findSql = sql
.select("r.id")
.from("aime.player r")
.where("r.ext_id", aimeId)
.where("r.ext_id", profile.aimeId)
.toParams();
const { rows } = await this._conn.query(findSql);
@@ -108,27 +95,25 @@ export class SqlProfileRepository implements ProfileRepository {
}
const id = generateId();
const extId = generateExtId() as ExtId<Profile>;
const playerId = row.id;
const createSql = sql
.insert("idz.profile", {
id: id,
player_id: playerId,
ext_id: extId,
name: profile.name,
lv: profile.lv,
exp: profile.exp,
fame: profile.fame,
dpoint: profile.dpoint,
mileage: profile.mileage,
register_time: timestamp,
access_time: timestamp,
register_time: profile.registerTime,
access_time: profile.accessTime,
})
.toParams();
await this._conn.query(createSql);
return extId;
return id as Id<Profile>;
}
}
+5 -9
View File
@@ -1,21 +1,19 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Settings } from "../model/settings";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
export class SqlSettingsRepository implements FacetRepository<Settings> {
constructor(private readonly _conn: ClientBase) {}
async load(extId: ExtId<Profile>): Promise<Settings> {
async load(profileId: Id<Profile>): Promise<Settings> {
const loadSql = sql
.select("s.*")
.from("idz.profile p")
.join("idz.settings s", { "p.id": "s.id" })
.where("p.ext_id", extId)
.from("idz.settings s")
.where("s.id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -29,9 +27,7 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
};
}
async save(extId: ExtId<Profile>, settings: Settings): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
async save(profileId: Id<Profile>, settings: Settings): Promise<void> {
const saveSql = sql
.insert("idz.settings", {
id: profileId,
+5 -15
View File
@@ -1,8 +1,6 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { Story, StoryRow, StoryCell } from "../model/story";
import { FacetRepository } from "../repo";
@@ -11,9 +9,7 @@ import { generateId, Id } from "../../db";
export class SqlStoryRepository implements FacetRepository<Story> {
constructor(private readonly _conn: ClientBase) {}
private async _load(extId: ExtId<Profile>): Promise<[Story, Id<Profile>]> {
const profileId = await _findProfile(this._conn, extId);
async load(profileId: Id<Profile>): Promise<Story> {
const loadSql = sql
.select("s.*")
.from("idz.story_state s")
@@ -31,7 +27,7 @@ export class SqlStoryRepository implements FacetRepository<Story> {
rows: new Array<StoryRow>(),
};
for (let i = 0; i < 9; i++) {
for (let i = 0; i < 27; i++) {
const row: StoryRow = { cells: new Array<StoryCell>() };
for (let j = 0; j < 9; j++) {
@@ -56,17 +52,11 @@ export class SqlStoryRepository implements FacetRepository<Story> {
cell.b = row.b;
}
return [result, profileId];
return result;
}
async load(extId: ExtId<Profile>): Promise<Story> {
const [story] = await this._load(extId);
return story;
}
async save(extId: ExtId<Profile>, story: Story): Promise<void> {
const [existing, profileId] = await this._load(extId);
async save(profileId: Id<Profile>, story: Story): Promise<void> {
const existing = await this.load(profileId);
const headSql = sql
.insert("idz.story_state", {
+92
View File
@@ -0,0 +1,92 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { TeamSpec, TeamRepository } from "../repo";
import { Id, generateExtId, generateId } from "../../db";
export class SqlTeamRepository implements TeamRepository {
constructor(private readonly _conn: ClientBase) {}
async find(extId: ExtId<Team>): Promise<Id<Team>> {
const findSql = sql
.select("t.id")
.from("idz.team t")
.where("t.ext_id", extId)
.toParams();
const { rows } = await this._conn.query(findSql);
const row = rows[0];
if (row === undefined) {
throw new Error(`Team not found for ExtID ${extId}`);
}
return row.id;
}
async load(id: Id<Team>): Promise<Team> {
const loadSql = sql
.select("t.*")
.from("idz.team t")
.where("t.id", id)
.toParams();
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
if (row == undefined) {
throw new Error("Team not found");
}
return {
extId: row.ext_id,
name: row.name,
nameBg: row.name_bg,
nameFx: row.name_fx,
registerTime: new Date(row.register_time),
};
}
async save(id: Id<Team>, team: Team): Promise<void> {
const saveSql = sql
.update("idz.team", {
name_bg: team.nameBg,
name_fx: team.nameFx,
})
.where("id", id)
.toParams();
await this._conn.query(saveSql);
}
async create(team: TeamSpec): Promise<[Id<Team>, ExtId<Team>]> {
const id = generateId() as Id<Team>;
const extId = generateExtId() as ExtId<Team>;
const createSql = sql
.insert("idz.team", {
id: id,
ext_id: extId,
name: team.name,
name_bg: team.nameBg,
name_fx: team.nameFx,
register_time: team.registerTime,
})
.toParams();
await this._conn.query(createSql);
return [id, extId];
}
async delete(id: Id<Team>): Promise<void> {
const deleteSql = sql
.delete("idz.team")
.where("id", id)
.toParams();
await this._conn.query(deleteSql);
}
}
+44
View File
@@ -0,0 +1,44 @@
import * as sql from "sql-bricks-postgres";
import { ClientBase } from "pg";
import { Team, TeamAuto } from "../model/team";
import { TeamAutoRepository } from "../repo";
import { Id } from "../../db";
export class SqlTeamAutoRepository implements TeamAutoRepository {
constructor(private readonly _conn: ClientBase) {}
async peek(): Promise<[TeamAuto, Id<Team>] | undefined> {
const peekSql = sql
.select("tt.*")
.from("idz.team_auto tt")
.orderBy("serial_no desc", "name_idx desc")
.limit(1)
.toParams();
const { rows } = await this._conn.query(peekSql);
const row = rows[0];
return (
row && [
{
serialNo: row.serial_no,
nameIdx: row.name_idx,
},
row.id,
]
);
}
async push(teamId: Id<Team>, auto: TeamAuto): Promise<void> {
const pushSql = sql
.insert("idz.team_auto", {
id: teamId,
serial_no: auto.serialNo,
name_idx: auto.nameIdx,
})
.toParams();
await this._conn.query(pushSql);
}
}
+153
View File
@@ -0,0 +1,153 @@
import * as sql from "sql-bricks-postgres";
import { ClientBase } from "pg";
import { Profile } from "../model/profile";
import { Team, TeamMember } from "../model/team";
import { TeamMemberRepository } from "../repo";
import { Id, generateId } from "../../db";
import { _extractProfile } from "./profile";
import { _extractChara } from "./chara";
export class SqlTeamMemberRepository implements TeamMemberRepository {
constructor(private readonly _conn: ClientBase) {}
async findTeam(profileId: Id<Profile>): Promise<Id<Team> | undefined> {
const findSql = sql
.select("tm.team_id")
.from("idz.team_member tm")
.where("tm.id", profileId)
.toParams();
const { rows } = await this._conn.query(findSql);
const row = rows[0];
if (row === undefined) {
return undefined;
}
return row.team_id;
}
async findLeader(teamId: Id<Team>): Promise<Id<Profile> | undefined> {
const findSql = sql
.select("tm.id")
.from("idz.team_member tm")
.where("tm.team_id", teamId)
.where("tm.leader", true)
.toParams();
const { rows } = await this._conn.query(findSql);
const row = rows[0];
if (row === undefined) {
return undefined;
}
return row.id;
}
async loadRoster(teamId: Id<Team>): Promise<TeamMember[]> {
const loadSql = sql
.select("tm.*", "p.*", "c.*", "r.ext_id as aime_id")
.from("idz.team_member tm")
.join("idz.profile p", { "tm.id": "p.id" })
.join("idz.chara c", { "tm.id": "c.id" })
.join("aime.player r", { "p.player_id": "r.id" })
.where("tm.team_id", teamId)
.toParams();
const { rows } = await this._conn.query(loadSql);
return rows.map((row: any) => ({
profile: _extractProfile(row),
chara: _extractChara(row),
leader: row.leader,
joinTime: new Date(row.join_time),
}));
}
async join(
teamId: Id<Team>,
profileId: Id<Profile>,
timestamp: Date
): Promise<void> {
// Lock the team record to avoid race conditions. This way
const lockSql = sql
.select("id")
.from("idz.team")
.where("id", teamId)
.forUpdate()
.toParams();
await this._conn.query(lockSql);
// Double-check (with lock held) that there is room to join this team.
// If this fails then the error will propagate to the client and it will
// retry, and, assuming we have a race between two new registrations to
// take up the last slot in the current auto-team, hopefully succeed.
//
// There is arguably some business logic pollution here, since we have a
// hard-coded maximum team size imposed by the protocol. This is why a
// three-layered server would be better than our two-layered server.
const countSql = sql
.select("count(*) as count")
.from("idz.team_member")
.where("team_id", teamId)
.toParams();
const { rows } = await this._conn.query(countSql);
const row = rows[0];
if (row.count >= 6) {
throw new Error(`Team ${teamId} is full`);
}
// Do upsert
const joinSql = sql
.insert("idz.team_member", {
id: profileId,
team_id: teamId,
leader: false,
join_time: timestamp,
})
.onConflict("id")
.doUpdate(["team_id", "leader", "join_time"])
.toParams();
await this._conn.query(joinSql);
}
async leave(teamId: Id<Team>, profileId: Id<Profile>): Promise<void> {
const leaveSql = sql
.delete("idz.team_member")
.where("team_id", teamId)
.where("id", profileId)
.toParams();
await this._conn.query(leaveSql);
}
async makeLeader(teamId: Id<Team>, profileId: Id<Profile>): Promise<void> {
const clearSql = sql
.update("idz.team_member", {
leader: false,
})
.where("team_id", teamId)
.toParams();
await this._conn.query(clearSql);
const setSql = sql
.update("idz.team_member", {
leader: true,
})
.where("id", profileId)
.where("team_id", teamId)
.toParams();
await this._conn.query(setSql);
}
}
+122
View File
@@ -0,0 +1,122 @@
import * as sql from "sql-bricks-postgres";
import { ClientBase } from "pg";
import { Team } from "../model/team";
import { TeamReservationRepository } from "../repo";
import { Id } from "../../db";
import { AimeId } from "../../model";
export class SqlTeamReservationRepository
implements TeamReservationRepository {
constructor(private readonly _conn: ClientBase) {}
private async _lockTeam(teamId: Id<Team>): Promise<void> {
const lockSql = sql
.select("t.id")
.from("idz.team t")
.where("t.id", teamId)
.forUpdate()
.toParams();
await this._conn.query(lockSql);
}
async occupancyHack(teamId: Id<Team>): Promise<number> {
await this._lockTeam(teamId);
// counts get returned as strings, so 1 + 0 = 10.
// it hardly needs to be said but fuck javascript.
const memberSql = sql
.select("count(*) as count")
.from("idz.team_member tm")
.where("tm.team_id", teamId)
.toParams();
const memberRes = await this._conn.query(memberSql);
const memberCount = parseInt(memberRes.rows[0].count, 10);
const reservSql = sql
.select("count(*) as count")
.from("idz.team_reservation tr")
.where("tr.team_id", teamId)
.toParams();
const reservRes = await this._conn.query(reservSql);
const reservCount = parseInt(reservRes.rows[0].count, 10);
return memberCount + reservCount;
}
async reserveHack(
teamId: Id<Team>,
aimeId: AimeId,
timestamp: Date,
leader?: "leader"
): Promise<void> {
const lookupSql = sql
.select("r.id")
.from("aime.player r")
.where("r.ext_id", aimeId)
.toParams();
const { rows } = await this._conn.query(lookupSql);
const row = rows[0];
if (row === undefined) {
throw new Error(`Unknown Aime ID ${aimeId}`);
}
const playerId = row.id;
const insertSql = sql
.insert("idz.team_reservation", {
id: playerId,
team_id: teamId,
join_time: timestamp,
leader: leader === "leader",
})
.onConflict("id")
.doUpdate(["team_id"])
.toParams();
await this._conn.query(insertSql);
}
async commitHack(aimeId: AimeId): Promise<void> {
const lookupSql = sql
.select("p.id as profile_id", "tr.*")
.from("idz.profile p")
.join("aime.player r", { "p.player_id": "r.id" })
.join("idz.team_reservation tr", { "r.id": "tr.id" })
.where("r.ext_id", aimeId)
.toParams();
const { rows } = await this._conn.query(lookupSql);
const row = rows[0];
if (row === undefined) {
throw new Error(`Reservation not found for Aime ID ${aimeId}`);
}
console.log(row);
const insertSql = sql
.insert("idz.team_member", {
id: row.profile_id,
team_id: row.team_id,
join_time: row.join_time,
leader: row.leader,
})
.toParams();
await this._conn.query(insertSql);
const cleanupSql = sql
.delete("idz.team_reservation")
.where("id", row.id)
.toParams();
await this._conn.query(cleanupSql);
}
}
+5 -8
View File
@@ -1,23 +1,21 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { Tickets } from "../model/tickets";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
// TODO free continue
export class SqlTicketsRepository implements FacetRepository<Tickets> {
constructor(private readonly _conn: ClientBase) {}
async load(extId: ExtId<Profile>): Promise<Tickets> {
async load(profileId: Id<Profile>): Promise<Tickets> {
const loadSql = sql
.select("fc.*")
.from("idz.profile p")
.join("idz.free_car fc", { "p.id": "fc.id" })
.where("p.ext_id", extId)
.from("idz.free_car fc")
.where("fc.id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -30,8 +28,7 @@ export class SqlTicketsRepository implements FacetRepository<Tickets> {
};
}
async save(extId: ExtId<Profile>, tickets: Tickets): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
async save(profileId: Id<Profile>, tickets: Tickets): Promise<void> {
const { freeCar } = tickets;
if (!freeCar) {
+10 -10
View File
@@ -1,22 +1,25 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId, RouteNo } from "../model/base";
import { RouteNo } from "../model/base";
import { Profile } from "../model/profile";
import { TimeAttackScore } from "../model/timeAttack";
import { TimeAttackRepository, TopTenResult } from "../repo";
import { generateId } from "../../db";
import { generateId, Id } from "../../db";
export class SqlTimeAttackRepository implements TimeAttackRepository {
constructor(private readonly _conn: ClientBase) {}
async loadTopTen(routeNo: RouteNo): Promise<TopTenResult[]> {
async loadTopTen(
routeNo: RouteNo,
minTimestamp: Date
): Promise<TopTenResult[]> {
const loadSql = sql
.select("p.name", "ta.*")
.from("idz.ta_best ta")
.join("idz.profile p", { "ta.profile_id": "p.id" })
.where("ta.route_no", routeNo)
.where(sql.gt("ta.timestamp", minTimestamp))
.orderBy(["ta.total_time asc", "ta.timestamp asc"])
.limit(10)
.toParams();
@@ -37,12 +40,11 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
}));
}
async loadAll(extId: ExtId<Profile>): Promise<TimeAttackScore[]> {
async loadAll(profileId: Id<Profile>): Promise<TimeAttackScore[]> {
const loadSql = sql
.select("ta.*")
.from("idz.ta_best ta")
.join("idz.profile p", { "ta.profile_id": "p.id" })
.where("p.ext_id", extId)
.where("ta.profile_id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -58,9 +60,7 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
}));
}
async save(extId: ExtId<Profile>, score: TimeAttackScore): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
async save(profileId: Id<Profile>, score: TimeAttackScore): Promise<void> {
const logSql = sql
.insert("idz.ta_result", {
id: generateId(),
+5 -8
View File
@@ -1,21 +1,19 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks";
import { _findProfile } from "./_util";
import { TitleCode, ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { FlagRepository } from "../repo";
import { generateId } from "../../db";
import { generateId, Id } from "../../db";
export class SqlTitlesRepository implements FlagRepository<TitleCode> {
constructor(private readonly _conn: ClientBase) {}
async loadAll(extId: ExtId<Profile>): Promise<Set<TitleCode>> {
async loadAll(profileId: Id<Profile>): Promise<Set<TitleCode>> {
const loadSql = sql
.select("t.title_no")
.from("idz.title_unlock t")
.join("idz.profile p", { "t.profile_id": "p.id" })
.where("p.ext_id", extId)
.where("t.profile_id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -28,9 +26,8 @@ export class SqlTitlesRepository implements FlagRepository<TitleCode> {
return result;
}
async saveAll(extId: ExtId<Profile>, flags: Set<TitleCode>): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
const existing = await this.loadAll(extId);
async saveAll(profileId: Id<Profile>, flags: Set<TitleCode>): Promise<void> {
const existing = await this.loadAll(profileId);
for (const flag of flags) {
if (existing.has(flag)) {
+5 -8
View File
@@ -1,21 +1,20 @@
import { ClientBase } from "pg";
import * as sql from "sql-bricks-postgres";
import { _findProfile } from "./_util";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { Unlocks } from "../model/unlocks";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
constructor(private readonly _conn: ClientBase) {}
async load(extId: ExtId<Profile>): Promise<Unlocks> {
async load(profileId: Id<Profile>): Promise<Unlocks> {
const loadSql = sql
.select("u.*")
.from("idz.profile p")
.join("idz.unlocks u", { "p.id": "u.id" })
.where("p.ext_id", extId)
.from("idz.unlocks u")
.where("u.id", profileId)
.toParams();
const { rows } = await this._conn.query(loadSql);
@@ -29,9 +28,7 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
};
}
async save(extId: ExtId<Profile>, unlocks: Unlocks): Promise<void> {
const profileId = await _findProfile(this._conn, extId);
async save(profileId: Id<Profile>, unlocks: Unlocks): Promise<void> {
const saveSql = sql
.insert("idz.unlocks", {
id: profileId,
+15
View File
@@ -0,0 +1,15 @@
import { RequestCode } from "./_defs";
import { CreateAutoTeamRequest } from "../request/createAutoTeam";
import { AimeId } from "../../model";
createAutoTeam.msgCode = 0x007b as RequestCode;
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),
};
}
+8 -6
View File
@@ -1,9 +1,8 @@
import iconv = require("iconv-lite");
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { CreateTeamRequest } from "../request/createTeam";
import { AimeId } from "../../model";
createTeam.msgCode = 0x0071 as RequestCode;
createTeam.msgLen = 0x0050;
@@ -11,13 +10,16 @@ createTeam.msgLen = 0x0050;
export function createTeam(buf: Buffer): CreateTeamRequest {
return {
type: "create_team_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
teamName: iconv.decode(buf.slice(0x0008, 0x0028), "shift_jis"),
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),
field_0030: buf.readUInt8(0x0030),
nameBg: buf.readUInt8(0x0030),
field_0032: buf.readUInt16LE(0x0032),
prevTeamId: buf.readUInt32LE(0x0034),
field_0038: buf.slice(0x0038, 0x0045),
pcbId: buf.slice(0x0038, buf.indexOf("\0", 0x0038)).toString("ascii"),
};
}
+43 -18
View File
@@ -3,41 +3,52 @@ import { Transform } from "stream";
import { checkTeamName } from "./checkTeamName";
import { createProfile } from "./createProfile";
import { createTeam } from "./createTeam";
import { joinAutoTeam } from "./joinAutoTeam";
import { createAutoTeam } from "./createAutoTeam";
import { discoverProfile } from "./discoverProfile";
import { load2on2 } from "./load2on2";
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 { loadGeneralReward } from "./loadGeneralReward";
import { loadGeneralReward1, loadGeneralReward2 } from "./loadGeneralReward";
import { loadGhost } from "./loadGhost";
import { loadProfile } from "./loadProfile";
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 { saveExpedition } from "./saveExpedition";
import { saveExpedition1, saveExpedition2 } from "./saveExpedition";
import { saveGarage } from "./saveGarage";
import { saveNewCar } from "./saveNewCar";
import { saveProfile } from "./saveProfile";
import { saveProfile2 } from "./saveProfile2";
import { saveProfile3 } from "./saveProfile3";
import { saveSettings } from "./saveSettings";
import { saveStocker } from "./saveStocker";
import { saveTimeAttack } from "./saveTimeAttack";
import { saveTeamBanner } from "./saveTeamBanner";
import { saveTimeAttack1, saveTimeAttack2 } 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 {
updateStoryClearNum1,
updateStoryClearNum2,
} from "./updateStoryClearNum";
import { RequestCode } from "./_defs";
import { Request } from "../request";
import { loadTopTen } from "./loadTopTen";
import { updateResult } from "./updateResult";
import { updateTeamPoints } from "./updateTeamPoints";
import { updateUiReport } from "./updateUiReport";
import { updateUserLog } from "./updateUserLog";
import { lockProfileExtend } from "./lockProfileExtend";
export type ReaderFn = ((buf: Buffer) => Request) & {
msgCode: RequestCode;
@@ -46,39 +57,53 @@ export type ReaderFn = ((buf: Buffer) => Request) & {
const funcList: ReaderFn[] = [
checkTeamName,
createAutoTeam,
createProfile,
createTeam,
joinAutoTeam,
discoverProfile,
load2on2,
load2on2_v1,
load2on2_v2,
loadConfig,
loadConfig2,
loadEventInfo,
loadGacha,
loadGarage,
loadGeneralReward,
loadGeneralReward1,
loadGeneralReward2,
loadGhost,
loadProfile,
loadProfile2,
loadProfile3,
loadRewardTable,
loadServerList,
loadStocker,
loadTeam,
loadTeamRanking,
loadTeamRanking2,
loadTopTen,
loadTopTen1,
loadTopTen2,
lockGarage,
lockProfile,
lockProfileExtend,
msg00AD,
saveExpedition,
saveExpedition1,
saveExpedition2,
saveGarage,
saveNewCar,
saveProfile,
saveProfile2,
saveProfile3,
saveSettings,
saveStocker,
saveTimeAttack,
saveTeamBanner,
saveTimeAttack1,
saveTimeAttack2,
saveTopic,
unlockProfile,
updateProvisionalStoreRank,
updateResult,
updateStoryClearNum,
updateStoryClearNum1,
updateStoryClearNum2,
updateTeamLeader,
updateTeamMember,
updateTeamPoints,
updateUiReport,
updateUserLog,
-14
View File
@@ -1,14 +0,0 @@
import { RequestCode } from "./_defs";
import { JoinAutoTeamRequest } from "../request/joinAutoTeam";
joinAutoTeam.msgCode = 0x007b as RequestCode;
joinAutoTeam.msgLen = 0x0010;
export function joinAutoTeam(buf: Buffer): JoinAutoTeamRequest {
return {
type: "join_auto_team_req",
field_0004: buf.readUInt32LE(0x0004),
field_0008: buf.readUInt32LE(0x0008),
field_000C: buf.readUInt8(0x000c),
};
}
+23 -6
View File
@@ -1,14 +1,31 @@
import { RequestCode } from "./_defs";
import { Load2on2Request } from "../request/load2on2";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { Load2on2Request1, Load2on2Request2 } from "../request/load2on2";
import { AimeId } from "../../model";
load2on2.msgCode = 0x00b0 as RequestCode;
load2on2.msgLen = 0x0010;
load2on2_v1.msgCode = 0x00b0 as RequestCode;
load2on2_v1.msgLen = 0x0010;
export function load2on2(buf: Buffer): Load2on2Request {
export function load2on2_v1(buf: Buffer): Load2on2Request1 {
return {
type: "load_2on2_req",
format: 1,
field_0002: buf.readUInt16LE(0x0002),
field_0004: buf.readUInt32LE(0x0004),
field_0008: buf.readUInt32LE(0x0008),
aimeId: buf.readUInt32LE(0x0004) as AimeId,
teamId: buf.readUInt32LE(0x0008) as ExtId<Team>,
};
}
load2on2_v2.msgCode = 0x0132 as RequestCode;
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>,
};
}
+13
View File
@@ -0,0 +1,13 @@
import { RequestCode } from "./_defs";
import { LoadEventInfoRequest } from "../request/loadEventInfo";
import { AimeId } from "../../model";
loadEventInfo.msgCode = 0x00be as RequestCode;
loadEventInfo.msgLen = 0x0010;
export function loadEventInfo(buf: Buffer): LoadEventInfoRequest {
return {
type: "load_event_info_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
+13
View File
@@ -0,0 +1,13 @@
import { RequestCode } from "./_defs";
import { LoadGachaRequest } from "../request/loadGacha";
import { AimeId } from "../../model";
loadGacha.msgCode = 0x00c1 as RequestCode;
loadGacha.msgLen = 0x0010;
export function loadGacha(buf: Buffer): LoadGachaRequest {
return {
type: "load_gacha_req",
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
+2 -3
View File
@@ -1,7 +1,6 @@
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { LoadGarageRequest } from "../request/loadGarage";
import { AimeId } from "../../model";
loadGarage.msgCode = 0x0090 as RequestCode;
loadGarage.msgLen = 0x0010;
@@ -9,7 +8,7 @@ loadGarage.msgLen = 0x0010;
export function loadGarage(buf: Buffer): LoadGarageRequest {
return {
type: "load_garage_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
fetchOffset: buf.readUInt8(0x0008),
field_000A: buf.readUInt16LE(0x000a),
};
+21 -5
View File
@@ -1,12 +1,28 @@
import { RequestCode } from "./_defs";
import { LoadGeneralRewardRequest } from "../request/loadGeneralReward";
import {
LoadGeneralRewardRequest1,
LoadGeneralRewardRequest2,
} from "../request/loadGeneralReward";
import { AimeId } from "../../model";
loadGeneralReward.msgCode = 0x009c as RequestCode;
loadGeneralReward.msgLen = 0x0010;
loadGeneralReward1.msgCode = 0x009c as RequestCode;
loadGeneralReward1.msgLen = 0x0010;
export function loadGeneralReward(buf: Buffer): LoadGeneralRewardRequest {
export function loadGeneralReward1(buf: Buffer): LoadGeneralRewardRequest1 {
return {
type: "load_general_reward_req",
field_0004: buf.readUInt32LE(0x0004),
format: 1,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
loadGeneralReward2.msgCode = 0x013b as RequestCode;
loadGeneralReward2.msgLen = 0x0010;
export function loadGeneralReward2(buf: Buffer): LoadGeneralRewardRequest2 {
return {
type: "load_general_reward_req",
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
+20 -4
View File
@@ -1,13 +1,29 @@
import { RequestCode } from "./_defs";
import { LoadProfileRequest } from "../request/loadProfile";
import {
LoadProfileRequest2,
LoadProfileRequest3,
} from "../request/loadProfile";
import { AimeId } from "../../model";
loadProfile.msgCode = 0x0067 as RequestCode;
loadProfile.msgLen = 0x0020;
loadProfile2.msgCode = 0x0067 as RequestCode;
loadProfile2.msgLen = 0x0020;
export function loadProfile(buf: Buffer): LoadProfileRequest {
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 as RequestCode;
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"),
};
+2 -3
View File
@@ -1,7 +1,6 @@
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { LoadStockerRequest } from "../request/loadStocker";
import { AimeId } from "../../model";
loadStocker.msgCode = 0x00a7 as RequestCode;
loadStocker.msgLen = 0x0010;
@@ -9,6 +8,6 @@ loadStocker.msgLen = 0x0010;
export function loadStocker(buf: Buffer): LoadStockerRequest {
return {
type: "load_stocker_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
+6 -2
View File
@@ -1,13 +1,17 @@
import { RequestCode } from "./_defs";
import { LoadTeamRequest } from "../request/loadTeam";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
loadTeam.msgCode = 0x0077 as RequestCode;
loadTeam.msgLen = 0x0010;
export function loadTeam(buf: Buffer): LoadTeamRequest {
const extId = buf.readUInt32LE(0x0008);
return {
type: "load_team_req",
profileId: buf.readUInt32LE(0x0004),
teamId: buf.readUInt32LE(0x0008),
aimeId: buf.readUInt32LE(0x0004),
teamExtId: extId !== 0xffffffff ? (extId as ExtId<Team>) : undefined,
};
}
@@ -1,22 +1,22 @@
import { RequestCode } from "./_defs";
import { ExtId, RouteNo } from "../model/base";
import { Profile } from "../model/profile";
import { Team } from "../model/team";
import {
LoadTopTenRequest,
LoadTopTenRequestSelector,
} from "../request/loadTopTen";
import { AimeId } from "../../model";
loadTopTen.msgCode = 0x00b5 as RequestCode;
loadTopTen.msgLen = 0x00e0;
loadTopTen1.msgCode = 0x00b5 as RequestCode;
loadTopTen1.msgLen = 0x00e0;
export function loadTopTen(buf: Buffer): LoadTopTenRequest {
export function loadTopTen1(buf: Buffer): LoadTopTenRequest {
const selectors = new Array<LoadTopTenRequestSelector>();
for (let i = 0; i < 32; i++) {
selectors.push({
routeNo: (buf.readUInt16LE(0x0004 + 2 * i) >> 1) as RouteNo,
field_44: buf.readUInt32LE(0x0044 + 4 * i),
minTimestamp: new Date(buf.readUInt32LE(0x0044 + 4 * i) * 1000 + 1000),
});
}
@@ -30,7 +30,7 @@ export function loadTopTen(buf: Buffer): LoadTopTenRequest {
field_C4: buf.readUInt8(0x00c4), // Boolean, true if profile ID is set
field_C5: buf.readUInt8(0x00c5), // Always zero
field_C6: buf.readUInt16LE(0x00c6),
profileId: profileId !== 0 ? (profileId as ExtId<Profile>) : undefined,
aimeId: profileId !== 0 ? (profileId as AimeId) : undefined,
teamId: teamId !== 0xffffffff ? (teamId as ExtId<Team>) : undefined,
};
}
+36
View File
@@ -0,0 +1,36 @@
import { RequestCode } from "./_defs";
import { ExtId, RouteNo } from "../model/base";
import { Team } from "../model/team";
import {
LoadTopTenRequest,
LoadTopTenRequestSelector,
} from "../request/loadTopTen";
import { AimeId } from "../../model";
loadTopTen2.msgCode = 0x012c as RequestCode;
loadTopTen2.msgLen = 0x0110;
export function loadTopTen2(buf: Buffer): LoadTopTenRequest {
const selectors = new Array<LoadTopTenRequestSelector>();
for (let i = 0; i < 40; i++) {
selectors.push({
routeNo: (buf.readUInt16LE(0x0004 + 2 * i) >> 1) as RouteNo,
minTimestamp: new Date(buf.readUInt32LE(0x0054 + 4 * i) * 1000 + 1000),
});
}
const profileId = buf.readUInt32LE(0x00f8);
const teamId = buf.readUInt32LE(0x00fc);
return {
type: "load_top_ten_req",
field_2: buf.readUInt16LE(0x0002), // Bitmask selector
selectors,
field_C4: buf.readUInt8(0x00f4), // Boolean, true if profile ID is set
field_C5: buf.readUInt8(0x00f5), // Always zero
field_C6: buf.readUInt16LE(0x00f6),
aimeId: profileId !== 0 ? (profileId as AimeId) : undefined,
teamId: teamId !== 0xffffffff ? (teamId as ExtId<Team>) : undefined,
};
}
+3 -3
View File
@@ -1,13 +1,13 @@
import { RequestCode } from "./_defs";
import { LockAccountRequest } from "../request/lockProfile";
import { LockProfileRequest } from "../request/lockProfile";
lockProfile.msgCode = 0x0069 as RequestCode;
lockProfile.msgLen = 0x0020;
export function lockProfile(buf: Buffer): LockAccountRequest {
export function lockProfile(buf: Buffer): LockProfileRequest {
return {
type: "lock_profile_req",
profileId: buf.readUInt32LE(0x0004),
aimeId: buf.readUInt32LE(0x0004),
pcbId: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
field_0018: buf.readUInt16LE(0x0018),
};
+14
View File
@@ -0,0 +1,14 @@
import { RequestCode } from "./_defs";
import { LockProfileExtendRequest } from "../request/lockProfileExtend";
import { AimeId } from "../../model";
lockProfileExtend.msgCode = 0x006d as RequestCode;
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"),
};
}
+19 -4
View File
@@ -1,12 +1,27 @@
import { RequestCode } from "./_defs";
import { SaveExpeditionRequest } from "../request/saveExpedition";
import {
SaveExpeditionRequest1,
SaveExpeditionRequest2,
} from "../request/saveExpedition";
saveExpedition.msgCode = 0x008c as RequestCode;
saveExpedition.msgLen = 0x0010;
saveExpedition1.msgCode = 0x008c as RequestCode;
saveExpedition1.msgLen = 0x0010;
export function saveExpedition(buf: Buffer): SaveExpeditionRequest {
export function saveExpedition1(buf: Buffer): SaveExpeditionRequest1 {
return {
type: "save_expedition_req",
format: 1,
field_0004: buf.readUInt32LE(0x0004),
};
}
saveExpedition2.msgCode = 0x013f as RequestCode;
saveExpedition2.msgLen = 0x0010;
export function saveExpedition2(buf: Buffer): SaveExpeditionRequest2 {
return {
type: "save_expedition_req",
format: 2,
field_0004: buf.readUInt32LE(0x0004),
};
}
+1 -1
View File
@@ -14,7 +14,7 @@ export function saveGarage(buf: Buffer): SaveGarageRequest {
return {
type: "save_garage_req",
profileId: buf.readUInt32LE(0x0004),
aimeId: buf.readUInt32LE(0x0004),
payload: car(buf.slice(0x0008, 0x0068)),
field_0068,
field_0080: buf.readUInt8(0x0080),
+2 -3
View File
@@ -1,8 +1,7 @@
import { car } from "./_car";
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { SaveNewCarRequest } from "../request/saveNewCar";
import { AimeId } from "../../model";
saveNewCar.msgCode = 0x0079 as RequestCode;
saveNewCar.msgLen = 0x0090;
@@ -10,7 +9,7 @@ saveNewCar.msgLen = 0x0090;
export function saveNewCar(buf: Buffer): SaveNewCarRequest {
return {
type: "save_new_car_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
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),
@@ -2,14 +2,14 @@ import { car } from "./_car";
import { mission } from "./_mission";
import { RequestCode } from "./_defs";
import { BackgroundCode, CourseNo, ExtId, TitleCode } from "../model/base";
import { Profile } from "../model/profile";
import { SaveProfileRequest } from "../request/saveProfile";
import { SaveProfileRequest2 } from "../request/saveProfile";
import { bitmap } from "./_bitmap";
import { AimeId } from "../../model";
saveProfile.msgCode = 0x0068 as RequestCode;
saveProfile.msgLen = 0x0940;
saveProfile2.msgCode = 0x0068 as RequestCode;
saveProfile2.msgLen = 0x0940;
export function saveProfile(buf: Buffer): SaveProfileRequest {
export function saveProfile2(buf: Buffer): SaveProfileRequest2 {
const storyRows = new Array();
for (let i = 0; i < 9; i++) {
@@ -46,7 +46,8 @@ export function saveProfile(buf: Buffer): SaveProfileRequest {
return {
type: "save_profile_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
lv: buf.readUInt16LE(0x0026),
exp: buf.readUInt32LE(0x0028),
fame: buf.readUInt32LE(0x0468),
+100
View File
@@ -0,0 +1,100 @@
import { car } from "./_car";
import { mission } from "./_mission";
import { RequestCode } from "./_defs";
import { BackgroundCode, CourseNo, TitleCode } from "../model/base";
import { SaveProfileRequest2 } from "../request/saveProfile";
import { bitmap } from "./_bitmap";
import { AimeId } from "../../model";
saveProfile3.msgCode = 0x0138 as RequestCode;
saveProfile3.msgLen = 0x0a70;
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 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 cell = { a, b };
cells.push(cell);
}
const row = { cells };
storyRows.push(row);
}
const coursePlays = new Map<CourseNo, number>();
for (let i = 0; i < 20; i++) {
coursePlays.set(i as CourseNo, buf.readUInt16LE(0x0554 + 2 * i));
}
const freeCar = {
validFrom: buf.readUInt32LE(0x0138),
};
const freeContinue = {
validFrom: buf.readUInt32LE(0x0038),
validTo: buf.readUInt32LE(0x003c),
};
return {
type: "save_profile_req",
format: 2,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
lv: buf.readUInt16LE(0x0026),
exp: buf.readUInt32LE(0x0028),
fame: buf.readUInt32LE(0x04fc),
dpoint: buf.readUInt32LE(0x04f8),
mileage: buf.readUInt32LE(0x0008),
title: buf.readUInt16LE(0x0040) as TitleCode,
titles: bitmap(buf.slice(0x0042, 0x00f6)),
background: buf.readUInt8(0x0874) as BackgroundCode,
coursePlays,
missions: {
team: mission(buf.slice(0x0430, 0x0452)),
solo: mission(buf.slice(0x0848, 0x086a)),
},
car: car(buf.slice(0x0958, 0x09b8)),
story: {
x: buf.readUInt16LE(0x0818),
y: buf.readUInt8(0x07fc),
rows: storyRows,
},
unlocks: {
cup: buf.readUInt8(0x0110),
gauges: buf.readUInt16LE(0x0114),
music: buf.readUInt16LE(0x0140),
lastMileageReward: buf.readUInt32LE(0x013c),
},
tickets: {
freeCar:
freeCar.validFrom !== 0
? {
validFrom: new Date(freeCar.validFrom * 1000),
}
: undefined,
freeContinue:
freeContinue.validFrom !== 0 && freeContinue.validTo !== 0
? {
validFrom: new Date(freeContinue.validFrom * 1000),
validTo: new Date(freeContinue.validTo * 1000),
}
: undefined,
},
settings: {
music: buf.readUInt16LE(0x04ee),
pack: buf.readUInt32LE(0x0034),
paperCup: buf.readUInt8(0x00f6),
gauges: buf.readUInt8(0x00f7),
},
};
}
+2 -3
View File
@@ -1,7 +1,6 @@
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { SaveSettingsRequest } from "../request/saveSettings";
import { AimeId } from "../../model";
saveSettings.msgCode = 0x00a5 as RequestCode;
saveSettings.msgLen = 0x0020;
@@ -13,7 +12,7 @@ export function saveSettings(buf: Buffer): SaveSettingsRequest {
return {
type: "save_settings_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
dpoint: buf.readUInt32LE(0x0008),
settings: {
music: buf.readUInt16LE(0x0002),
+2 -3
View File
@@ -1,10 +1,9 @@
import { bitmap } from "./_bitmap";
import { chara } from "./_chara";
import { RequestCode } from "./_defs";
import { BackgroundCode, ExtId } from "../model/base";
import { CarSelector } from "../model/car";
import { Profile } from "../model/profile";
import { SaveStockerRequest } from "../request/saveStocker";
import { AimeId } from "../../model";
saveStocker.msgCode = 0x00a6 as RequestCode;
saveStocker.msgLen = 0x00c0;
@@ -12,7 +11,7 @@ saveStocker.msgLen = 0x00c0;
export function saveStocker(buf: Buffer): SaveStockerRequest {
return {
type: "save_stocker_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
backgrounds: bitmap(buf.slice(0x0008, 0x002c)),
selectedCar: buf.readUInt16LE(0x009c) as CarSelector,
+16
View File
@@ -0,0 +1,16 @@
import { RequestCode } from "./_defs";
import { SaveTeamBannerRequest } from "../request/saveTeamBanner";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
saveTeamBanner.msgCode = 0x0089 as RequestCode;
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),
};
}
+22 -7
View File
@@ -1,16 +1,13 @@
import { RequestCode } from "./_defs";
import { ExtId, RouteNo } from "../model/base";
import { RouteNo } from "../model/base";
import { CarSelector } from "../model/car";
import { Profile } from "../model/profile";
import { SaveTimeAttackRequest } from "../request/saveTimeAttack";
import { AimeId } from "../../model";
saveTimeAttack.msgCode = 0x00cd as RequestCode;
saveTimeAttack.msgLen = 0x0080;
export function saveTimeAttack(buf: Buffer): SaveTimeAttackRequest {
function saveTimeAttack(buf: Buffer): SaveTimeAttackRequest {
return {
type: "save_time_attack_req",
profileId: buf.readUInt32LE(0x0004) as ExtId<Profile>,
aimeId: buf.readUInt32LE(0x0004) as AimeId,
dayNight: buf.readUInt8(0x0054) & 1,
payload: {
routeNo: (buf.readUInt8(0x0054) >> 1) as RouteNo,
@@ -34,3 +31,21 @@ export function saveTimeAttack(buf: Buffer): SaveTimeAttackRequest {
field_0060: buf.readUInt16LE(0x0060),
};
}
// There is ... literally no difference between these messages other than their
// request code..? Even the response uses the same response code, despite
// the request codes differing.
saveTimeAttack1.msgCode = 0x00cd as RequestCode;
saveTimeAttack1.msgLen = 0x0080;
export function saveTimeAttack1(buf: Buffer): SaveTimeAttackRequest {
return saveTimeAttack(buf);
}
saveTimeAttack2.msgCode = 0x0136 as RequestCode;
saveTimeAttack2.msgLen = 0x0080;
export function saveTimeAttack2(buf: Buffer): SaveTimeAttackRequest {
return saveTimeAttack(buf);
}
+1 -1
View File
@@ -7,7 +7,7 @@ unlockProfile.msgLen = 0x0020;
export function unlockProfile(buf: Buffer): UnlockProfileRequest {
return {
type: "unlock_profile_req",
profileId: buf.readUInt32LE(0x0004),
aimeId: buf.readUInt32LE(0x0004),
pcbId: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
};
}
+22 -4
View File
@@ -1,11 +1,29 @@
import { RequestCode } from "./_defs";
import { UpdateStoryClearNumRequest } from "../request/updateStoryClearNum";
import {
UpdateStoryClearNumRequest1,
UpdateStoryClearNumRequest2,
} from "../request/updateStoryClearNum";
updateStoryClearNum.msgCode = 0x007f as RequestCode;
updateStoryClearNum.msgLen = 0x0010;
updateStoryClearNum1.msgCode = 0x007f as RequestCode;
updateStoryClearNum1.msgLen = 0x0010;
export function updateStoryClearNum(buf: Buffer): UpdateStoryClearNumRequest {
export function updateStoryClearNum1(
buf: Buffer
): UpdateStoryClearNumRequest1 {
return {
type: "update_story_clear_num_req",
format: 1,
};
}
updateStoryClearNum2.msgCode = 0x013d as RequestCode;
updateStoryClearNum2.msgLen = 0x0010;
export function updateStoryClearNum2(
buf: Buffer
): UpdateStoryClearNumRequest2 {
return {
type: "update_story_clear_num_req",
format: 2,
};
}
+17
View File
@@ -0,0 +1,17 @@
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { UpdateTeamLeaderRequest } from "../request/updateTeamLeader";
import { AimeId } from "../../model";
updateTeamLeader.msgCode = 0x008a as RequestCode;
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"),
};
}
+17
View File
@@ -0,0 +1,17 @@
import { RequestCode } from "./_defs";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { UpdateTeamMemberRequest } from "../request/updateTeamMember";
import { AimeId } from "../../model";
updateTeamMember.msgCode = 0x0073 as RequestCode;
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>,
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
export function bitmap(items: Set<number>, nbytes: number): Buffer {
export function encodeBitmap(items: Set<number>, nbytes: number): Buffer {
const buf = Buffer.alloc(nbytes);
for (const item of items) {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Car } from "../model/car";
export function car(car: Car): Buffer {
export function encodeCar(car: Car): Buffer {
const buf = Buffer.alloc(0x0060);
buf.writeUInt16LE(car.field_00, 0x0000);
+1 -1
View File
@@ -1,6 +1,6 @@
import { Chara } from "../model/chara";
export function chara(chara: Chara): Buffer {
export function encodeChara(chara: Chara): Buffer {
const buf = Buffer.alloc(0x0014);
buf.writeUInt8(chara.gender === "male" ? 0 : 1, 0x00);
+1 -1
View File
@@ -1,6 +1,6 @@
import { MissionGrid } from "../model/mission";
export function mission(grids: MissionGrid[]): Buffer {
export function encodeMission(grids: MissionGrid[]): Buffer {
const buf = Buffer.alloc(0x24);
for (let gridNo = 0; gridNo < grids.length; gridNo++) {
+38 -22
View File
@@ -1,42 +1,58 @@
import iconv = require("iconv-lite");
import { JoinAutoTeamResponse } from "../response/joinAutoTeam";
import { LoadTeamResponse } from "../response/loadTeam";
export function _team(res: JoinAutoTeamResponse | LoadTeamResponse) {
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);
buf.writeUInt32LE(res.team.id, 0x000c);
if (res.type === "create_auto_team_res") {
buf.writeInt16LE(0x007c, 0x0000);
} else {
buf.writeInt16LE(0x0078, 0x0000);
}
const leader = res.members.find(item => item.leader);
buf.writeUInt32LE(res.team.extId, 0x000c);
iconv.encode(res.team.name, "shift_jis").copy(buf, 0x0024);
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 * 0x004c;
const base = 0x011c + i * 0x005c;
const member = res.members[i];
if (member === undefined) {
break;
}
buf.writeInt32LE(1, base + 0x0000); // Presence
iconv.encode(member.name + "\0", "shift_jis").copy(buf, base + 0x0004);
buf.writeInt32LE(member.lv, base + 0x0018);
buf.writeInt32LE(member.monthPoints, base + 0x0024);
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);
}
// xM
// Team Time Attack:
/*
buf.writeInt16LE(0x00001, 0x0344 + 0x0000);
buf.writeInt8(0x02, 0x0344 + 0x0003);
buf.writeInt32LE(0x00000003, 0x0344 + 0x0004);
iconv.encode("str\0", sjis).copy(buf, 0x0344 + 0x0008);
buf.writeInt32LE(0x00000004, 0x0344 + 0x001c);
*/
/*for (let i = 0; i < 6; i++) {
const base = 0x0344 + 0x20 * i;
if (res.type === "join_auto_team_res") {
buf.writeInt16LE(0x007c, 0x0000);
} else {
buf.writeInt16LE(0x0078, 0x0000);
}
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;
}
+1 -1
View File
@@ -5,7 +5,7 @@ export function createTeam(res: CreateTeamResponse): Buffer {
buf.writeUInt16LE(0x0072, 0x0000);
buf.writeUInt32LE(res.status, 0x0004);
buf.writeUInt32LE(res.teamId, 0x0008);
buf.writeUInt32LE(res.teamExtId, 0x0008);
return buf;
}
+24 -8
View File
@@ -6,14 +6,16 @@ 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 { loadProfile2 } from "./loadProfile2";
import { loadRewardTable } from "./loadRewardTable";
import { loadServerList } from "./loadServerList";
import { loadStocker } from "./loadStocker";
@@ -27,6 +29,8 @@ 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";
function encode(res: Response): Buffer {
@@ -34,15 +38,15 @@ function encode(res: Response): Buffer {
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 "join_auto_team_res":
return _team(res);
case "generic_res":
return generic(res);
@@ -55,6 +59,12 @@ function encode(res: Response): Buffer {
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);
@@ -64,12 +74,9 @@ function encode(res: Response): Buffer {
case "load_ghost_res":
return loadGhost(res);
case "load_profile_v1_res":
case "load_profile_res":
return loadProfile(res);
case "load_profile_v2_res":
return loadProfile2(res);
case "load_reward_table_res":
return loadRewardTable(res);
@@ -88,6 +95,9 @@ function encode(res: Response): Buffer {
case "load_top_ten_res":
return loadTopTen(res);
case "lock_profile_extend_res":
return lockProfileExtend(res);
case "lock_profile_res":
return lockProfile(res);
@@ -112,6 +122,12 @@ function encode(res: Response): Buffer {
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);
+30 -2
View File
@@ -1,9 +1,37 @@
import { Load2on2Response } from "../response/load2on2";
import {
Load2on2Response,
Load2on2Response1,
Load2on2Response2,
} from "../response/load2on2";
export function load2on2(res: Load2on2Response) {
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;
}
+1
View File
@@ -5,6 +5,7 @@ export function loadConfig(res: LoadConfigResponse) {
buf.writeInt16LE(0x0005, 0x0000);
buf.writeInt8(res.status, 0x0002);
buf.writeUInt16LE(res.serverVersion, 0x0016);
return buf;
}
+9
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;
}
+10
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;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { car } from "./_car";
import { encodeCar } from "./_car";
import { LoadGarageResponse } from "../response/loadGarage";
export function loadGarage(res: LoadGarageResponse): Buffer {
@@ -8,7 +8,7 @@ export function loadGarage(res: LoadGarageResponse): Buffer {
buf.writeUInt16LE(res.cars.length, 0x0002);
for (let i = 0; i < res.cars.length; i++) {
car(res.cars[i]).copy(buf, 0x0004 + 0x0060 * i);
encodeCar(res.cars[i]).copy(buf, 0x0004 + 0x0060 * i);
}
return buf;
+16 -4
View File
@@ -1,10 +1,22 @@
import { loadProfile1 } from "./loadProfile1";
import { loadProfile2 } from "./loadProfile2";
import { loadProfile3 } from "./loadProfile3";
import { LoadProfileResponse } from "../response/loadProfile";
// Sending this causes an error
export function loadProfile(res: LoadProfileResponse) {
const buf = Buffer.alloc(0x0c60);
switch (res.format) {
case 1:
return loadProfile1(res);
buf.writeInt16LE(0x0064, 0x0000);
case 2:
return loadProfile2(res);
return buf;
case 3:
return loadProfile3(res);
default:
const exhaust: never = res;
throw new Error(`Unsupported profile response format ${res["format"]}`);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { LoadProfileResponse1 } from "../response/loadProfile";
// Sending this causes an error in v1.21, so it is currently unmapped and
// unimplemented.
export function loadProfile1(res: LoadProfileResponse1) {
const buf = Buffer.alloc(0x0c60);
buf.writeInt16LE(0x0064, 0x0000);
return buf;
}
+14 -14
View File
@@ -1,10 +1,10 @@
import iconv = require("iconv-lite");
import { bitmap } from "./_bitmap";
import { car } from "./_car";
import { chara } from "./_chara";
import { mission } from "./_mission";
import { LoadProfileResponse2 } from "../response/loadProfile2";
import { encodeBitmap } from "./_bitmap";
import { encodeCar } from "./_car";
import { encodeChara } from "./_chara";
import { encodeMission } from "./_mission";
import { LoadProfileResponse2 } from "../response/loadProfile";
export function loadProfile2(res: LoadProfileResponse2) {
const buf = Buffer.alloc(0x0d30);
@@ -53,7 +53,7 @@ export function loadProfile2(res: LoadProfileResponse2) {
for (const [courseId, playCount] of res.coursePlays.entries()) {
if (courseId < 0 || courseId >= 16) {
throw new Error(`Course id out of range: ${courseId}`);
continue;
}
buf.writeUInt16LE(playCount, 0x0460 + 2 * courseId);
@@ -75,10 +75,10 @@ export function loadProfile2(res: LoadProfileResponse2) {
buf.writeUInt16LE(res.unlocks.gauges, 0x00b8);
buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x01e8);
buf.writeUInt16LE(res.unlocks.music, 0x01ec);
buf.writeUInt16LE(0, 0x037c); // Team leader
mission(res.missions.team).copy(buf, 0x038a);
buf.writeUInt16LE(res.teamLeader ? 1 : 0, 0x037c);
encodeMission(res.missions.team).copy(buf, 0x038a);
buf.writeUInt16LE(0xffff, 0x0388); // [1]
buf.writeUInt32LE(res.profileId, 0x03b8);
buf.writeUInt32LE(res.aimeId, 0x03b8);
buf.writeUInt32LE(res.mileage, 0x03bc);
buf.writeUInt16LE(res.settings.music, 0x03c8);
buf.writeUInt16LE(res.lv, 0x03cc);
@@ -87,15 +87,15 @@ export function loadProfile2(res: LoadProfileResponse2) {
buf.writeUInt32LE(res.dpoint, 0x03e8);
buf.writeUInt32LE(res.fame, 0x0404);
iconv.encode(res.name + "\0", "shift_jis").copy(buf, 0x03ee);
buf.writeUInt16LE(res.story.x, 0x06bc);
buf.writeUInt8(res.story.y, 0x0670);
mission(res.missions.solo).copy(buf, 0x06e4);
chara(res.chara).copy(buf, 0x070c);
bitmap(res.titles, 0xb4).copy(buf, 0x720);
buf.writeUInt16LE(res.story.x, 0x06bc);
encodeMission(res.missions.solo).copy(buf, 0x06e4);
encodeChara(res.chara).copy(buf, 0x070c);
encodeBitmap(res.titles, 0xb4).copy(buf, 0x720);
buf.writeUInt8(res.settings.paperCup, 0x07d9);
buf.writeUInt8(res.settings.gauges, 0x07da);
buf.writeUInt32LE(res.teamId || 0xffffffff, 0x07e0);
car(res.car).copy(buf, 0x0c5c);
encodeCar(res.car).copy(buf, 0x0c5c);
buf.writeUInt32LE(res.carCount, 0x0c58);
// [1] Currently unknown, but if this field is zero then the player will have
+105
View File
@@ -0,0 +1,105 @@
import iconv = require("iconv-lite");
import { encodeBitmap } from "./_bitmap";
import { encodeCar } from "./_car";
import { encodeChara } from "./_chara";
import { encodeMission } from "./_mission";
import { LoadProfileResponse3 } from "../response/loadProfile";
export function loadProfile3(res: LoadProfileResponse3) {
const buf = Buffer.alloc(0x0ea0);
// Initialize all TA grades to uhh... fuck knows
buf.fill(0xff, 0x07e4, 0x080c);
for (const score of res.timeAttack) {
const { routeNo } = score;
buf.writeUInt32LE(
(new Date(score.timestamp).getTime() / 1000) | 0, // Date ctor hack
0x00e4 + routeNo * 4
);
buf.writeUInt16LE(0, 0x067c + 2 * routeNo); // ???
buf.writeUInt16LE(0xffff, 0x0184 + 2 * routeNo); // National rank
buf.writeUInt32LE((score.totalTime * 1000) | 0, 0x05dc + 4 * routeNo);
buf.writeUInt8(score.flags, 0x06cc + routeNo);
buf.writeUInt8(score.grade, 0x07e4 + routeNo);
for (let i = 0; i < 3; i++) {
buf.writeUInt16LE(
(score.sectionTimes[i] * 1000) >> 2,
0x06f4 + 6 * routeNo + 2 * i
);
}
}
// Not sure it actually goes up to 27, but there seem to be 512 bytes of
// 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 < res.story.rows.length; i++) {
const row = res.story.rows[i];
const rowOffset = 0x0256 + i * 0x13;
for (let j = 0; j < 9 && j < row.cells.length; j++) {
const cell = row.cells[j];
const cellOffset = rowOffset + j * 2;
buf.writeUInt8(cell.a, cellOffset + 0);
buf.writeUInt8(cell.b, cellOffset + 1);
}
}
for (const [courseId, playCount] of res.coursePlays.entries()) {
if (courseId < 0 || courseId >= 20) {
throw new Error(`Course id out of range: ${courseId}`);
}
buf.writeUInt16LE(playCount, 0x053c + 2 * courseId);
}
const { freeCar, freeContinue } = res.tickets;
if (freeCar) {
buf.writeUInt32LE((freeCar.validFrom.getTime() / 1000) | 0, 0x0214);
}
if (freeContinue) {
buf.writeUInt32LE((freeContinue.validFrom.getTime() / 1000) | 0, 0x04b8);
buf.writeUInt32LE((freeContinue.validTo.getTime() / 1000) | 0, 0x04bc);
}
buf.writeUInt16LE(0x012e, 0x0000);
buf.writeUInt8(res.unlocks.cup, 0x00b4);
buf.writeUInt16LE(res.unlocks.gauges, 0x00b8);
buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x0218);
buf.writeUInt16LE(res.unlocks.music, 0x021c);
buf.writeUInt16LE(res.teamLeader ? 1 : 0, 0x0456); // Team leader
encodeMission(res.missions.team).copy(buf, 0x0460);
buf.writeUInt16LE(0xffff, 0x0462); // [1]
buf.writeUInt32LE(res.aimeId, 0x0494);
buf.writeUInt32LE(res.mileage, 0x0498);
buf.writeUInt16LE(res.settings.music, 0x04a4);
buf.writeUInt16LE(res.lv, 0x04a8);
buf.writeUInt32LE(res.exp, 0x04ac);
buf.writeUInt32LE(res.settings.pack, 0x04b4);
buf.writeUInt32LE(res.dpoint, 0x04c4);
buf.writeUInt32LE(res.fame, 0x04e0);
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);
encodeChara(res.chara).copy(buf, 0x0880);
encodeBitmap(res.titles, 0xb4).copy(buf, 0x0894);
buf.writeUInt8(res.settings.paperCup, 0x094d);
buf.writeUInt8(res.settings.gauges, 0x094e);
buf.writeUInt32LE(res.teamId || 0xffffffff, 0x0954);
buf.writeUInt32LE(res.carCount, 0x0dd0);
encodeCar(res.car).copy(buf, 0x0dd4);
// [1] Currently unknown, but if this field is zero then the player will have
// a "model record" emblem in their profile card.
return buf;
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { bitmap } from "./_bitmap";
import { encodeBitmap } from "./_bitmap";
import { LoadStockerResponse } from "../response/loadStocker";
export function loadStocker(res: LoadStockerResponse) {
@@ -6,7 +6,7 @@ export function loadStocker(res: LoadStockerResponse) {
buf.writeInt16LE(0x00a8, 0x0000);
buf.writeUInt8(res.status, 0x0002);
bitmap(res.backgrounds, 0x24).copy(buf, 0x0003);
encodeBitmap(res.backgrounds, 0x24).copy(buf, 0x0003);
return buf;
}
+10
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;
}
+30 -2
View File
@@ -1,6 +1,25 @@
import { SaveExpeditionResponse } from "../response/saveExpedition";
import {
SaveExpeditionResponse,
SaveExpeditionResponse1,
SaveExpeditionResponse2,
} from "../response/saveExpedition";
export function saveExpedition(res: SaveExpeditionResponse) {
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);
@@ -8,3 +27,12 @@ export function saveExpedition(res: SaveExpeditionResponse) {
return buf;
}
function saveExpedition2(res: SaveExpeditionResponse2): Buffer {
// absolute unit
const buf = Buffer.alloc(0x18ac);
buf.writeUInt16LE(0x0140, 0x0000);
return buf;
}
+29 -2
View File
@@ -1,9 +1,36 @@
import { UpdateStoryClearNumResponse } from "../response/updateStoryClearNum";
import {
UpdateStoryClearNumResponse,
UpdateStoryClearNumResponse1,
UpdateStoryClearNumResponse2,
} from "../response/updateStoryClearNum";
export function updateStoryClearNum(res: UpdateStoryClearNumResponse) {
export function updateStoryClearNum(res: UpdateStoryClearNumResponse): Buffer {
switch (res.format) {
case 1:
return updateStoryClearNum1(res);
case 2:
return updateStoryClearNum2(res);
default:
const exhaust: never = res;
throw new Error(`Unsupported data format ${res["format"]}`);
}
}
function updateStoryClearNum1(res: UpdateStoryClearNumResponse1): Buffer {
const buf = Buffer.alloc(0x0220);
buf.writeInt16LE(0x0080, 0x0000);
return buf;
}
function updateStoryClearNum2(res: UpdateStoryClearNumResponse2): Buffer {
const buf = Buffer.alloc(0x04f0);
buf.writeInt16LE(0x013e, 0x0000);
return buf;
}
+10
View File
@@ -0,0 +1,10 @@
import { UpdateTeamLeaderResponse } from "../response/updateTeamLeader";
export function updateTeamLeader(res: UpdateTeamLeaderResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x008b, 0x0000);
buf.writeUInt32LE(res.status, 0x0004);
return buf;
}
+10
View File
@@ -0,0 +1,10 @@
import { UpdateTeamMemberResponse } from "../response/updateTeamMember";
export function updateTeamMember(res: UpdateTeamMemberResponse): Buffer {
const buf = Buffer.alloc(0x0010);
buf.writeUInt16LE(0x0074, 0x0000);
buf.writeUInt32LE(res.status, 0x0004);
return buf;
}
+26 -19
View File
@@ -1,26 +1,33 @@
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { JoinAutoTeamRequest } from "../request/joinAutoTeam";
import { LoadTeamRequest } from "../request/loadTeam";
import { JoinAutoTeamResponse } from "../response/joinAutoTeam";
import { LoadTeamResponse } from "../response/loadTeam";
import { Repositories } from "../repo";
import { Id } from "../../db";
export function _team(
// Bleh. This factorization is kind of messy.
export async function _fixupPrevTeam(
w: Repositories,
req: JoinAutoTeamRequest | LoadTeamRequest
): JoinAutoTeamResponse | LoadTeamResponse {
const bits = {
team: {
id: 2 as ExtId<Team>,
name: process.env.TEAM_NAME || "",
},
members: [],
};
prevTeamId: Id<Team> | undefined
): Promise<void> {
if (prevTeamId === undefined) {
return;
}
if (req.type === "join_auto_team_req") {
return { type: "join_auto_team_res", ...bits };
} else {
return { type: "load_team_res", ...bits };
const remaining = await w.teamMembers().loadRoster(prevTeamId);
if (remaining.length === 0) {
// Last member left, GC previous team
await w.teams().delete(prevTeamId);
} else if (remaining.find(member => member.leader) === undefined) {
// Leader left, appoint new leader by seniority
remaining.sort((x, y) => x.joinTime.getTime() - y.joinTime.getTime());
// (need to look up new leader's db id from aime id. ick)
const newLeader = remaining[remaining.length - 1];
const newLeaderId = await w.profile().find(newLeader.profile.aimeId);
await w.teamMembers().makeLeader(prevTeamId, newLeaderId);
}
}
+121
View File
@@ -0,0 +1,121 @@
import { TeamAuto } from "../model/team";
import { Repositories } from "../repo";
import { CreateAutoTeamRequest } from "../request/createAutoTeam";
import { CreateAutoTeamResponse } from "../response/createAutoTeam";
interface AutoTeamTemplate {
prefix: string;
nameBg: number;
}
// Hard-code these for the time being, since AFAIK there are only three.
// These are all references to teams in the Initial D manga/anime.
// If any more auto-teams are added/discovered they *MUST* be added to the
// *END* of this list. Otherwise duplicate auto-teams will be created.
const autoTeams: AutoTeamTemplate[] = [
{
// "Speed Stars"
prefix: "スピードスターズ",
nameBg: 0,
},
{
// "Red Suns"
prefix: "レッドサンズ",
nameBg: 1,
},
{
// "Night Kids" (even though it's written like "Night Keys"...)
prefix: "ナイトキッズ",
nameBg: 2,
},
];
function incrementAuto(prev: TeamAuto): TeamAuto {
if (prev.nameIdx < autoTeams.length - 1) {
return { nameIdx: prev.nameIdx + 1, serialNo: prev.serialNo };
} else {
return { nameIdx: 0, serialNo: prev.serialNo + 1 };
}
}
export async function createAutoTeam(
w: Repositories,
req: CreateAutoTeamRequest
): Promise<CreateAutoTeamResponse> {
const now = new Date();
const { aimeId } = req;
const peek = await w.teamAuto().peek();
let nextAuto: TeamAuto;
//
// Determine if we need to create a new team or not
//
if (peek !== undefined) {
// Look at the highest-numbered auto team. Is it full?
const [lastAuto, lastTeamId] = peek;
const occupancy = await w.teamReservations().occupancyHack(lastTeamId);
console.log(occupancy);
if (occupancy < 6) {
// Team isn't full, so return this one
await w.teamReservations().reserveHack(lastTeamId, aimeId, now);
return {
type: "create_auto_team_res",
team: await w.teams().load(lastTeamId),
members: await w.teamMembers().loadRoster(lastTeamId),
};
}
// Team full, need to create a new one
nextAuto = incrementAuto(lastAuto);
} else {
// No teams exist at all, seed the system with SpeedStars001.
nextAuto = { serialNo: 1, nameIdx: 0 };
}
//
// Build the new team
//
// Make a three-digit serial number using full-width digits
let { serialNo, nameIdx } = nextAuto;
let name = "";
for (let i = 0; i < 3; i++) {
name = String.fromCodePoint(0xff10 + (serialNo % 10)) + name;
serialNo = (serialNo / 10) | 0;
}
// Prepend the name prefix
name = autoTeams[nameIdx].prefix + name;
// Register the new team, make the requestor its leader
const spec = {
name,
nameBg: autoTeams[nameIdx].nameBg,
nameFx: 0,
registerTime: now,
};
const [newTeamId, newTeamExtId] = await w.teams().create(spec);
await w.teamAuto().push(newTeamId, nextAuto);
await w.teamReservations().reserveHack(newTeamId, aimeId, now, "leader");
return {
type: "create_auto_team_res",
team: { ...spec, extId: newTeamExtId },
members: [],
};
}
+13 -8
View File
@@ -1,26 +1,29 @@
import { ExtId } from "../model/base";
import { MissionState } from "../model/mission";
import { Profile } from "../model/profile";
import { Settings } from "../model/settings";
import { Story } from "../model/story";
import { Team } from "../model/team";
import { Unlocks } from "../model/unlocks";
import { CreateProfileRequest } from "../request/createProfile";
import { GenericResponse } from "../response/generic";
import { ProfileSpec, Repositories } from "../repo";
import { Repositories } from "../repo";
export async function createProfile(
w: Repositories,
req: CreateProfileRequest
): Promise<GenericResponse> {
const { aimeId, name } = req;
const now = new Date();
const profile: ProfileSpec = {
teamId: 2 as ExtId<Team>, // TODO
name: req.name,
const profile: Profile = {
aimeId,
name,
lv: 1,
exp: 0,
fame: 0,
dpoint: 0,
mileage: 0,
accessTime: now,
registerTime: now,
};
const missions: MissionState = { team: [], solo: [] };
@@ -33,7 +36,7 @@ export async function createProfile(
lastMileageReward: 0,
};
const profileId = await w.profile().create(req.aimeId, profile, now);
const profileId = await w.profile().create(profile);
await w.chara().save(profileId, req.chara);
await w.car().saveCar(profileId, req.car);
@@ -44,8 +47,10 @@ export async function createProfile(
await w.unlocks().save(profileId, unlocks);
await w.tickets().save(profileId, {});
await w.teamReservations().commitHack(aimeId);
return {
type: "generic_res",
status: profileId, // "Generic response" my fucking *ass*
status: aimeId, // "Generic response" my fucking *ass*
};
}
+47 -5
View File
@@ -1,16 +1,58 @@
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { _fixupPrevTeam } from "./_team";
import { CreateTeamRequest } from "../request/createTeam";
import { CreateTeamResponse } from "../response/createTeam";
import { Repositories } from "../repo";
export function createTeam(
export async function createTeam(
w: Repositories,
req: CreateTeamRequest
): CreateTeamResponse {
): Promise<CreateTeamResponse> {
const profileId = await w.profile().find(req.aimeId);
const prevTeamId = await w.teamMembers().findTeam(profileId);
const now = new Date();
// Create the new team...
const teamSpec = {
name: req.teamName,
nameBg: req.nameBg,
nameFx: 0,
registerTime: now,
};
const [teamId, teamExtId] = await w.teams().create(teamSpec);
await w.teamMembers().join(teamId, profileId, now);
await w.teamMembers().makeLeader(teamId, profileId);
await _fixupPrevTeam(w, prevTeamId);
// Fix up previous team. The previous team's extid is explicitly sent in the
// request, but why rely on it if you don't have to?
if (prevTeamId !== undefined) {
const remaining = await w.teamMembers().loadRoster(prevTeamId);
if (remaining.length === 0) {
// Last member left, GC previous team
await w.teams().delete(prevTeamId);
} else if (remaining.find(member => member.leader) === undefined) {
// Leader left, appoint new leader by seniority
remaining.sort((x, y) => x.joinTime.getTime() - y.joinTime.getTime());
// (need to look up new leader's db id from aime id. ick)
const newLeader = remaining[remaining.length - 1];
const newLeaderId = await w.profile().find(newLeader.profile.aimeId);
await w.teamMembers().makeLeader(prevTeamId, newLeaderId);
}
}
return {
type: "create_team_res",
status: 0,
teamId: 3 as ExtId<Team>,
teamExtId,
};
}
+3 -1
View File
@@ -6,8 +6,10 @@ export async function discoverProfile(
w: Repositories,
req: DiscoverProfileRequest
): Promise<DiscoverProfileResponse> {
const profileId = await w.profile().peek(req.aimeId);
return {
type: "discover_profile_res",
exists: await w.profile().discoverByAimeId(req.aimeId),
exists: profileId !== undefined,
};
}
+30 -5
View File
@@ -1,11 +1,13 @@
import { _team } from "./_team";
import { checkTeamName } from "./checkTeamName";
import { createAutoTeam } from "./createAutoTeam";
import { createProfile } from "./createProfile";
import { createTeam } from "./createTeam";
import { discoverProfile } from "./discoverProfile";
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";
@@ -13,10 +15,12 @@ import { loadProfile } from "./loadProfile";
import { loadReward as loadRewardTable } from "./loadRewardTable";
import { loadServerList } from "./loadServerList";
import { loadStocker } from "./loadStocker";
import { loadTeam } from "./loadTeam";
import { loadTeamRanking } from "./loadTeamRanking";
import { loadTopTen } from "./loadTopTen";
import { lockGarage } from "./lockGarage";
import { lockProfile } from "./lockProfile";
import { lockProfileExtend } from "./lockProfileExtend";
import { msg00AD } from "./msg00AD";
import { saveExpedition } from "./saveExpedition";
import { saveGarage } from "./saveGarage";
@@ -24,12 +28,15 @@ import { saveNewCar } from "./saveNewCar";
import { saveProfile } from "./saveProfile";
import { saveSettings } from "./saveSettings";
import { saveStocker } from "./saveStocker";
import { saveTeamBanner } from "./saveTeamBanner";
import { saveTimeAttack } from "./saveTimeAttack";
import { saveTopic } from "./saveTopic";
import { unlockProfile } from "./unlockProfile";
import { updateProvisionalStoreRank } from "./updateProvisionalStoreRank";
import { updateResult } from "./updateResult";
import { updateStoryClearNum } from "./updateStoryClearNum";
import { updateTeamLeader } from "./updateTeamLeader";
import { updateTeamMember } from "./updateTeamMember";
import { updateTeamPoints } from "./updateTeamPoints";
import { updateUiReport } from "./updateUiReport";
import { updateUserLog } from "./updateUserLog";
@@ -45,15 +52,15 @@ export async function dispatch(
case "check_team_name_req":
return checkTeamName(w, req);
case "create_auto_team_req":
return createAutoTeam(w, req);
case "create_profile_req":
return createProfile(w, req);
case "create_team_req":
return createTeam(w, req);
case "join_auto_team_req":
return _team(w, req);
case "load_2on2_req":
return load2on2(w, req);
@@ -66,6 +73,12 @@ export async function dispatch(
case "discover_profile_req":
return discoverProfile(w, req);
case "load_event_info_req":
return loadEventInfo(w, req);
case "load_gacha_req":
return loadGacha(w, req);
case "load_garage_req":
return loadGarage(w, req);
@@ -88,7 +101,7 @@ export async function dispatch(
return loadStocker(w, req);
case "load_team_req":
return _team(w, req);
return loadTeam(w, req);
case "load_top_ten_req":
return loadTopTen(w, req);
@@ -96,6 +109,9 @@ export async function dispatch(
case "lock_garage_request":
return lockGarage(w, req);
case "lock_profile_extend_req":
return lockProfileExtend(w, req);
case "lock_profile_req":
return lockProfile(w, req);
@@ -126,6 +142,9 @@ export async function dispatch(
case "save_stocker_req":
return saveStocker(w, req);
case "save_team_banner_req":
return saveTeamBanner(w, req);
case "save_time_attack_req":
return saveTimeAttack(w, req);
@@ -141,6 +160,12 @@ export async function dispatch(
case "update_story_clear_num_req":
return updateStoryClearNum(w, req);
case "update_team_leader_req":
return updateTeamLeader(w, req);
case "update_team_member_req":
return updateTeamMember(w, req);
case "update_team_points_req":
return updateTeamPoints(w, req);
+1
View File
@@ -8,5 +8,6 @@ export function load2on2(
): Load2on2Response {
return {
type: "load_2on2_res",
format: req.format as any,
};
}
+1
View File
@@ -9,5 +9,6 @@ export function loadConfig(
return {
type: "load_config_res",
status: 1,
serverVersion: 130,
};
}
+12
View File
@@ -0,0 +1,12 @@
import { Repositories } from "../repo";
import { LoadEventInfoRequest } from "../request/loadEventInfo";
import { LoadEventInfoResponse } from "../response/loadEventInfo";
export function loadEventInfo(
w: Repositories,
req: LoadEventInfoRequest
): LoadEventInfoResponse {
return {
type: "load_event_info_res",
};
}
+13
View File
@@ -0,0 +1,13 @@
import { Repositories } from "../repo";
import { LoadGachaRequest } from "../request/loadGacha";
import { LoadGachaResponse } from "../response/loadGacha";
export function loadGacha(
w: Repositories,
req: LoadGachaRequest
): LoadGachaResponse {
return {
type: "load_gacha_res",
awardedToday: true, // Disable for now, not even mapped out yet.
};
}
+3 -1
View File
@@ -6,8 +6,10 @@ export async function loadGarage(
w: Repositories,
req: LoadGarageRequest
): Promise<LoadGarageResponse> {
const profileId = await w.profile().find(req.aimeId);
return {
type: "load_garage_res",
cars: await w.car().loadAllCars(req.profileId),
cars: await w.car().loadAllCars(profileId),
};
}
+2 -3
View File
@@ -1,12 +1,11 @@
import { LoadGeneralRewardRequest } from "../request/loadGeneralReward";
import { GenericResponse } from "../response/generic";
import { LoadGeneralRewardResponse } from "../response/loadGeneralReward";
import { Repositories } from "../repo";
export function loadGeneralReward(
w: Repositories,
req: LoadGeneralRewardRequest
): LoadGeneralRewardResponse | GenericResponse {
// A non-generic response is also accepted, but why bother
): GenericResponse {
// A version-specific response is also accepted. Format TBD.
return { type: "generic_res" };
}
+26 -17
View File
@@ -1,37 +1,46 @@
import { LoadProfileRequest } from "../request/loadProfile";
import { LoadProfileResponse2 } from "../response/loadProfile2";
import { LoadProfileResponse } from "../response/loadProfile";
import { Repositories } from "../repo";
export async function loadProfile(
w: Repositories,
req: LoadProfileRequest
): Promise<LoadProfileResponse2> {
): Promise<LoadProfileResponse> {
const { aimeId } = req;
const profileId = await w.profile().find(aimeId);
const teamId = await w.teamMembers().findTeam(profileId);
const leaderId = teamId && (await w.teamMembers().findLeader(teamId));
// Promise.all would be messy here, who cares anyway this isn't supposed to
// be a high-performance server.
const profile = await w.profile().loadByAimeId(req.aimeId);
const settings = await w.settings().load(profile.id);
const chara = await w.chara().load(profile.id);
const titles = await w.titles().loadAll(profile.id);
const coursePlays = await w.coursePlays().loadAll(profile.id);
const missions = await w.missions().load(profile.id);
const car = await w.car().loadSelectedCar(profile.id);
const carCount = await w.car().countCars(profile.id);
const story = await w.story().load(profile.id);
const timeAttack = await w.timeAttack().loadAll(profile.id);
const unlocks = await w.unlocks().load(profile.id);
const tickets = await w.tickets().load(profile.id);
const profile = await w.profile().load(profileId);
const settings = await w.settings().load(profileId);
const chara = await w.chara().load(profileId);
const titles = await w.titles().loadAll(profileId);
const coursePlays = await w.coursePlays().loadAll(profileId);
const missions = await w.missions().load(profileId);
const car = await w.car().loadSelectedCar(profileId);
const carCount = await w.car().countCars(profileId);
const story = await w.story().load(profileId);
const timeAttack = await w.timeAttack().loadAll(profileId);
const unlocks = await w.unlocks().load(profileId);
const tickets = await w.tickets().load(profileId);
const team = teamId && (await w.teams().load(teamId));
return {
type: "load_profile_v2_res",
type: "load_profile_res",
format: req.format as any, // TS fart
name: profile.name,
profileId: profile.id,
aimeId,
lv: profile.lv,
exp: profile.exp,
fame: profile.fame,
dpoint: profile.dpoint,
mileage: profile.mileage,
teamId: profile.teamId,
teamId: team && team.extId,
teamLeader: profileId === leaderId,
settings,
chara,
titles,
+2 -1
View File
@@ -6,7 +6,8 @@ export async function loadStocker(
w: Repositories,
req: LoadStockerRequest
): Promise<LoadStockerResponse> {
const backgrounds = await w.backgrounds().loadAll(req.profileId);
const profileId = await w.profile().find(req.aimeId);
const backgrounds = await w.backgrounds().loadAll(profileId);
return {
type: "load_stocker_res",
+37
View File
@@ -0,0 +1,37 @@
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { LoadTeamRequest } from "../request/loadTeam";
import { LoadTeamResponse } from "../response/loadTeam";
import { Repositories } from "../repo";
// Even if a profile does not belong to a team, a team must still be loaded
// (and then ignored by the client).
const dummyResp: LoadTeamResponse = {
type: "load_team_res",
team: {
extId: 0 as ExtId<Team>,
name: "",
nameBg: 0,
nameFx: 0,
registerTime: new Date(0),
},
members: [],
};
export async function loadTeam(
w: Repositories,
req: LoadTeamRequest
): Promise<LoadTeamResponse> {
if (req.teamExtId === undefined) {
return dummyResp;
}
const teamId = await w.teams().find(req.teamExtId);
return {
type: "load_team_res",
team: await w.teams().load(teamId),
members: await w.teamMembers().loadRoster(teamId),
};
}
+5 -4
View File
@@ -4,7 +4,7 @@ import {
LoadTopTenResponseCourse,
LoadTopTenResponseRow,
} from "../response/loadTopTen";
import { Repositories } from "../repo";
import { Repositories, TopTenResult } from "../repo";
export async function loadTopTen(
w: Repositories,
@@ -17,12 +17,13 @@ export async function loadTopTen(
break;
}
if (selector.field_44 === 0) {
const { routeNo, minTimestamp } = selector;
const src = await w.timeAttack().loadTopTen(routeNo, minTimestamp);
if (src.length === 0) {
continue;
}
const { routeNo } = selector;
const src = await w.timeAttack().loadTopTen(routeNo);
const dest = new Array<LoadTopTenResponseRow>();
for (const srcItem of src) {
+2 -2
View File
@@ -1,10 +1,10 @@
import { LockAccountRequest } from "../request/lockProfile";
import { LockProfileRequest } from "../request/lockProfile";
import { LockProfileResponse } from "../response/lockProfile";
import { Repositories } from "../repo";
export function lockProfile(
w: Repositories,
req: LockAccountRequest
req: LockProfileRequest
): LockProfileResponse {
return {
type: "lock_profile_res",
+13
View File
@@ -0,0 +1,13 @@
import { Repositories } from "../repo";
import { LockProfileExtendRequest } from "../request/lockProfileExtend";
import { LockProfileExtendResponse } from "../response/lockProfileExtend";
export function lockProfileExtend(
w: Repositories,
req: LockProfileExtendRequest
): LockProfileExtendResponse {
return {
type: "lock_profile_extend_res",
status: 1,
};
}
+1
View File
@@ -15,6 +15,7 @@ export function saveExpedition(
} else {
return {
type: "save_expedition_res",
format: req.format as any,
};
}
}

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