add idz repository layer

This commit is contained in:
Tau
2019-11-06 17:28:29 -05:00
parent 6beb3b2343
commit 8817029258
39 changed files with 375 additions and 99 deletions
+1
View File
@@ -1,3 +1,4 @@
bin/
node_modules/
pki/
state/
+15
View File
@@ -0,0 +1,15 @@
export function bitmap(buf: Buffer): number[] {
const result = new Array();
for (let byteNo = 0; byteNo < buf.length; byteNo++) {
const byte = buf.readUInt8(byteNo);
for (let bitNo = 0; bitNo < 8; bitNo++) {
if (byte & (1 << bitNo)) {
result.push(8 * byteNo + bitNo);
}
}
}
return result;
}
+3
View File
@@ -1,3 +1,4 @@
import { BackgroundCode, TitleCode } from "../model/base";
import { Chara } from "../model/chara";
export function chara(buf: Buffer): Chara {
@@ -10,5 +11,7 @@ export function chara(buf: Buffer): Chara {
field_0A: buf.readUInt16LE(0x0a),
field_0C: buf.readUInt16LE(0x0c),
field_0E: buf.readUInt16LE(0x0e),
title: buf.readUInt16LE(0x10) as TitleCode, // Swapped on load
background: buf.readUInt16LE(0x12) as BackgroundCode, // Swapped on load
};
}
+1 -3
View File
@@ -19,8 +19,6 @@ export function createProfile(buf: Buffer): CreateProfileRequest {
),
field_0034: buf.readUInt32LE(0x0034),
car: car(buf.slice(0x0040, 0x00a0)),
chara: chara(buf.slice(0x00a0, 0x00b0)),
field_00B0: buf.readUInt16LE(0x00b0),
field_00B2: buf.readUInt8(0x00b2),
chara: chara(buf.slice(0x00a0, 0x00b4)),
};
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { RequestCode } from "./_defs";
import { AimeId } from "../model/base";
import { DiscoverProfileRequest } from "../request/discoverProfile";
discoverProfile.msgCode = 0x006b as RequestCode;
@@ -7,6 +8,6 @@ discoverProfile.msgLen = 0x0010;
export function discoverProfile(buf: Buffer): DiscoverProfileRequest {
return {
type: "discover_profile_req",
aimeId: buf.readUInt32LE(0x0004),
aimeId: buf.readUInt32LE(0x0004) as AimeId,
};
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { RequestCode } from "./_defs";
import { AimeId } from "../model/base";
import { LoadProfileRequest } from "../request/loadProfile";
loadProfile.msgCode = 0x0067 as RequestCode;
@@ -7,7 +8,7 @@ loadProfile.msgLen = 0x0020;
export function loadProfile(buf: Buffer): LoadProfileRequest {
return {
type: "load_profile_req",
aimeId: buf.readUInt32LE(0x0004),
aimeId: buf.readUInt32LE(0x0004) as AimeId,
pcbId: buf.slice(0x0008, buf.indexOf("\0", 0x0008)).toString("ascii"),
};
}
+3 -1
View File
@@ -1,4 +1,6 @@
import { RequestCode } from "./_defs";
import { Id } from "../model/base";
import { Profile } from "../model/profile";
import { LoadStockerRequest } from "../request/loadStocker";
loadStocker.msgCode = 0x00a7 as RequestCode;
@@ -7,6 +9,6 @@ loadStocker.msgLen = 0x0010;
export function loadStocker(buf: Buffer): LoadStockerRequest {
return {
type: "load_stocker_req",
profileId: buf.readUInt32LE(0x0004),
profileId: buf.readUInt32LE(0x0004) as Id<Profile>,
};
}
+3 -1
View File
@@ -1,4 +1,6 @@
import { RequestCode } from "./_defs";
import { Id } from "../model/base";
import { Profile } from "../model/profile";
import { SaveSettingsRequest } from "../request/saveSettings";
saveSettings.msgCode = 0x00a5 as RequestCode;
@@ -11,7 +13,7 @@ export function saveSettings(buf: Buffer): SaveSettingsRequest {
return {
type: "save_settings_req",
profileId: buf.readUInt32LE(0x0004),
profileId: buf.readUInt32LE(0x0004) as Id<Profile>,
dpoint: buf.readUInt32LE(0x0008),
settings: {
bgMusic: buf.readUInt16LE(0x0002),
+7 -1
View File
@@ -1,4 +1,8 @@
import { bitmap } from "./_bitmap";
import { chara } from "./_chara";
import { RequestCode } from "./_defs";
import { BackgroundCode, Id } from "../model/base";
import { Profile } from "../model/profile";
import { SaveStockerRequest } from "../request/saveStocker";
saveStocker.msgCode = 0x00a6 as RequestCode;
@@ -7,6 +11,8 @@ saveStocker.msgLen = 0x00c0;
export function saveStocker(buf: Buffer): SaveStockerRequest {
return {
type: "save_stocker_req",
profileId: buf.readUInt32LE(0x0004),
profileId: buf.readUInt32LE(0x0004) as Id<Profile>,
backgrounds: bitmap(buf.slice(0x0008, 0x002c)) as BackgroundCode[],
chara: chara(buf.slice(0x009e, 0x00b2)),
};
}
+3 -1
View File
@@ -1,7 +1,7 @@
import { Chara } from "../model/chara";
export function chara(chara: Chara): Buffer {
const buf = Buffer.alloc(0x0010);
const buf = Buffer.alloc(0x0014);
buf.writeUInt8(chara.gender === "male" ? 0 : 1, 0x00);
buf.writeUInt16LE(chara.clothing, 0x02);
@@ -11,6 +11,8 @@ export function chara(chara: Chara): Buffer {
buf.writeUInt16LE(chara.field_0A, 0x0a);
buf.writeUInt16LE(chara.field_0C, 0x0c);
buf.writeUInt16LE(chara.field_0E, 0x0e);
buf.writeUInt16LE(chara.background, 0x10); // Swapped on save
buf.writeUInt16LE(chara.title, 0x12); // Swapped on save
return buf;
}
+2 -1
View File
@@ -5,7 +5,8 @@ import { LoadTeamResponse } from "../response/loadTeam";
export function _team(res: CreateTeamResponse | LoadTeamResponse) {
const buf = Buffer.alloc(0x0ca0);
iconv.encode(res.name, "shift_jis").copy(buf, 0x0024);
buf.writeUInt32LE(res.team.id, 0x000c);
iconv.encode(res.team.name, "shift_jis").copy(buf, 0x0024);
for (let i = 0; i < 6; i++) {
const base = 0x011c + i * 0x004c;
+2 -7
View File
@@ -1,17 +1,14 @@
import iconv = require("iconv-lite");
import { bitmap } from "./_bitmap";
import { car } from "./_car";
import { chara } from "./_chara";
import { LoadProfileResponse2 } from "../response/loadProfile2";
import { bitmap } from "./_bitmap";
export function loadProfile2(res: LoadProfileResponse2) {
// Story stuff
//buf.writeUInt32LE(0x1ff, 0x0228);
// Flairs (bitmap)
// buf.fill(0xff, 0x071c, 0x071c + 0xb4);
const settingsPack =
((res.settings.forceQuitEn ? 1 : 0) << 0) |
(res.settings.steeringForce << 1) |
@@ -33,10 +30,8 @@ export function loadProfile2(res: LoadProfileResponse2) {
buf.writeInt32LE(res.fame, 0x0404);
iconv.encode(res.name + "\0", "shift_jis").copy(buf, 0x03ee);
chara(res.chara).copy(buf, 0x070c);
buf.writeUInt16LE(res.background, 0x071c);
buf.writeUInt16LE(res.title, 0x071e);
bitmap(res.titles, 0xb4).copy(buf, 0x720);
buf.writeInt32LE(res.teamId, 0x07e0);
buf.writeInt32LE(res.teamId || 0, 0x07e0);
car(res.car).copy(buf, 0x0c5c);
return buf;
+6 -1
View File
@@ -1,3 +1,5 @@
import { Id } from "../model/base";
import { Team } from "../model/team";
import { CreateTeamRequest } from "../request/createTeam";
import { LoadTeamRequest } from "../request/loadTeam";
import { CreateTeamResponse } from "../response/createTeam";
@@ -9,7 +11,10 @@ export function _team(
req: CreateTeamRequest | LoadTeamRequest
): CreateTeamResponse | LoadTeamResponse {
const bits = {
name: "ASS GENTLEMEN",
team: {
id: 2 as Id<Team>,
name: "ASS GENTLEMEN",
},
members: [],
};
+33 -2
View File
@@ -1,11 +1,42 @@
import { Id } from "../model/base";
import { Profile } from "../model/profile";
import { Team } from "../model/team";
import { CreateProfileRequest } from "../request/createProfile";
import { GenericResponse } from "../response/generic";
import { World } from "../world";
export function createProfile(
export async function createProfile(
w: World,
req: CreateProfileRequest
): GenericResponse {
): Promise<GenericResponse> {
const profile: Profile = {
id: 1 as Id<Profile>,
teamId: 2 as Id<Team>,
name: req.name,
lv: 1,
fame: 0,
dpoint: 0,
};
const settings = {
bgMusic: 15,
forceQuitEn: false,
steeringForce: 4,
bgVolume: 10,
seVolume: 10,
cornerGuide: true,
lineGuide: false,
ghostEn: false,
taResultSkip: false,
};
await Promise.all([
w.profile().save(profile.id, profile),
w.chara().save(profile.id, req.chara),
w.car().save(profile.id, req.car),
w.settings().save(profile.id, settings),
]);
return {
type: "generic_res",
status: 1,
+3 -3
View File
@@ -2,12 +2,12 @@ import { DiscoverProfileRequest } from "../request/discoverProfile";
import { DiscoverProfileResponse } from "../response/discoverProfile";
import { World } from "../world";
export function discoverProfile(
export async function discoverProfile(
w: World,
req: DiscoverProfileRequest
): DiscoverProfileResponse {
): Promise<DiscoverProfileResponse> {
return {
type: "discover_profile_res",
exists: true,
exists: await w.profile().discoverByAimeId(req.aimeId),
};
}
+1 -1
View File
@@ -26,7 +26,7 @@ import { Request } from "../request";
import { Response } from "../response";
import { World } from "../world";
export function dispatch(w: World, req: Request): Response {
export async function dispatch(w: World, req: Request): Promise<Response> {
switch (req.type) {
case "create_profile_req":
return createProfile(w, req);
+18 -49
View File
@@ -2,58 +2,27 @@ import { LoadProfileRequest } from "../request/loadProfile";
import { LoadProfileResponse2 } from "../response/loadProfile2";
import { World } from "../world";
export function loadProfile(
export async function loadProfile(
w: World,
req: LoadProfileRequest
): LoadProfileResponse2 {
): Promise<LoadProfileResponse2> {
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().load(profile.id);
const car = await w.car().load(profile.id);
return {
type: "load_profile_v2_res",
name: "てすと",
profileId: 0x11223344,
lv: 99,
fame: 1234,
dpoint: 54321,
teamId: 0x22334455,
settings: {
bgMusic: 15,
forceQuitEn: false,
steeringForce: 4,
bgVolume: 10,
seVolume: 10,
cornerGuide: true,
lineGuide: false,
ghostEn: false,
taResultSkip: false,
},
chara: {
gender: "male",
clothing: 0,
mouth: 0,
field_06: 0,
field_08: 0,
field_0A: 0,
field_0C: 0,
field_0E: 0,
},
background: 0,
title: 0,
titles: [],
car: {
field_00: 0,
field_02: 0,
field_04: [],
model: 0,
color: 0,
transmission: 0,
field_4A: 0,
field_4C: 21527,
field_50_lo: 3674393,
field_50_hi: 6407,
field_58: 0,
field_5A: 0,
field_5B: 0,
field_5C: 1,
field_5E: 0,
},
name: profile.name,
profileId: profile.id,
lv: profile.lv,
fame: profile.fame,
dpoint: profile.dpoint,
teamId: profile.teamId,
settings,
chara,
titles,
car,
};
}
+5 -3
View File
@@ -2,13 +2,15 @@ import { LoadStockerRequest } from "../request/loadStocker";
import { LoadStockerResponse } from "../response/loadStocker";
import { World } from "../world";
export function loadStocker(
export async function loadStocker(
w: World,
req: LoadStockerRequest
): LoadStockerResponse {
): Promise<LoadStockerResponse> {
const backgrounds = await w.backgrounds().load(req.profileId);
return {
type: "load_stocker_res",
status: 1,
backgrounds: [],
backgrounds,
};
}
+4 -2
View File
@@ -2,10 +2,12 @@ import { SaveSettingsRequest } from "../request/saveSettings";
import { GenericResponse } from "../response/generic";
import { World } from "../world";
export function saveSettings(
export async function saveSettings(
w: World,
req: SaveSettingsRequest
): GenericResponse {
): Promise<GenericResponse> {
await w.settings().save(req.profileId, req.settings);
return {
type: "generic_res",
status: 1, // ignored but whatever
+5 -2
View File
@@ -2,10 +2,13 @@ import { SaveStockerRequest } from "../request/saveStocker";
import { GenericResponse } from "../response/generic";
import { World } from "../world";
export function saveStocker(
export async function saveStocker(
w: World,
req: SaveStockerRequest
): GenericResponse {
): Promise<GenericResponse> {
await w.backgrounds().save(req.profileId, req.backgrounds);
await w.chara().save(req.profileId, req.chara);
return {
type: "generic_res",
status: 1, // ignored
+3 -4
View File
@@ -2,18 +2,17 @@ import { Socket } from "net";
import { dispatch } from "./handler";
import { setup } from "./setup";
import { World } from "./world";
const world = new World();
import { createWorld } from "./world";
export default async function idz(socket: Socket) {
const world = await createWorld("./state");
const { input, output } = setup(socket);
console.log("Idz: Connection opened");
try {
for await (const req of input) {
output.write(dispatch(world, req));
output.write(await dispatch(world, req));
}
} catch (e) {
console.log("Idz: Error", e);
+7
View File
@@ -0,0 +1,7 @@
// Quasi-nominative "brands". These double-underscore properties never actually
// exist at run time, we just pretend they do.
export type Id<T> = number & { __id: T };
export type AimeId = number & { __brand: "aime_id" };
export type BackgroundCode = number & { __brand: "background_code" };
export type TitleCode = number & { __brand: "title_code" };
+4
View File
@@ -1,3 +1,5 @@
import { BackgroundCode, TitleCode } from "./base";
export interface Chara {
gender: "male" | "female";
clothing: number;
@@ -7,4 +9,6 @@ export interface Chara {
field_0A: number;
field_0C: number;
field_0E: number;
title: TitleCode;
background: BackgroundCode;
}
+6
View File
@@ -0,0 +1,6 @@
export { AimeId, BackgroundCode, Id, TitleCode } from "./base";
export { Car } from "./car";
export { Chara } from "./chara";
export { Profile } from "./profile";
export { Settings } from "./settings";
export { Team } from "./team";
+11
View File
@@ -0,0 +1,11 @@
import { Id } from "./base";
import { Team } from "./team";
export interface Profile {
id: Id<Profile>;
teamId?: Id<Team>;
name: string;
lv: number;
fame: number;
dpoint: number;
}
+6
View File
@@ -0,0 +1,6 @@
import { Id } from "./base";
export interface Team {
id: Id<Team>;
name: string;
}
-2
View File
@@ -9,6 +9,4 @@ export interface CreateProfileRequest {
field_0034: number;
car: Car;
chara: Chara;
field_00B0: number;
field_00B2: number;
}
+3 -1
View File
@@ -1,4 +1,6 @@
import { AimeId } from "../model";
export interface DiscoverProfileRequest {
type: "discover_profile_req";
aimeId: number;
aimeId: AimeId;
}
+3 -1
View File
@@ -1,5 +1,7 @@
import { AimeId } from "../model/base";
export interface LoadProfileRequest {
type: "load_profile_req";
aimeId: number;
aimeId: AimeId;
pcbId: string;
}
+4 -1
View File
@@ -1,4 +1,7 @@
import { Id } from "../model/base";
import { Profile } from "../model/profile";
export interface LoadStockerRequest {
type: "load_stocker_req";
profileId: number;
profileId: Id<Profile>;
}
+3 -1
View File
@@ -1,8 +1,10 @@
import { Id } from "../model/base";
import { Profile } from "../model/profile";
import { Settings } from "../model/settings";
export interface SaveSettingsRequest {
type: "save_settings_req";
profileId: number;
profileId: Id<Profile>;
dpoint: number; // ?? why
settings: Settings;
field_0010: number;
+7 -1
View File
@@ -1,4 +1,10 @@
import { BackgroundCode, Id } from "../model/base";
import { Chara } from "../model/chara";
import { Profile } from "../model/profile";
export interface SaveStockerRequest {
type: "save_stocker_req";
profileId: number;
profileId: Id<Profile>;
backgrounds: BackgroundCode[];
chara: Chara;
}
+3 -1
View File
@@ -1,3 +1,5 @@
import { Team } from "../model/team";
export interface TeamMember {
name: string;
lv: number;
@@ -5,7 +7,7 @@ export interface TeamMember {
}
export interface BaseTeamResponse {
name: string;
team: Team;
members: TeamMember[];
// giga TODO
}
+4 -4
View File
@@ -1,19 +1,19 @@
import { Id } from "../model/base";
import { Car } from "../model/car";
import { Chara } from "../model/chara";
import { Profile } from "../model/profile";
import { Settings } from "../model/settings";
export interface LoadProfileResponse2 {
type: "load_profile_v2_res";
name: string;
profileId: number;
profileId: Id<Profile>;
lv: number;
fame: number;
dpoint: number;
teamId: number;
teamId?: number;
settings: Settings;
chara: Chara;
background: number;
title: number;
titles: number[];
car: Car;
// giga TODO
-3
View File
@@ -1,3 +0,0 @@
export class World {
// Hand out references to some sort of backing store Repository objects here.
}
+82
View File
@@ -0,0 +1,82 @@
import * as fs from "fs";
import { promisify } from "util";
import { Repository } from "./base";
import { Id } from "../model/base";
const exists = promisify(fs.exists);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
export class SingletonJsonRepo<T, K = T> implements Repository<T, K> {
private _path: string;
constructor(root: string, name: string) {
this._path = `${root}/${name}.json`;
}
discover(id: Id<K>): Promise<boolean> {
return this._discoverSingleton();
}
load(id: Id<K>): Promise<T> {
return this._loadSingleton();
}
save(id: Id<K>, obj: T) {
console.log("Idz: Repository: Saving ", this._path);
const json = JSON.stringify(obj, undefined, 2);
return writeFile(this._path, json, { encoding: "utf8" });
}
protected _discoverSingleton(): Promise<boolean> {
return exists(this._path);
}
protected async _loadSingleton(): Promise<T> {
console.log("Idz: Repository: Loading ", this._path);
const json = await readFile(this._path, { encoding: "utf8" });
return JSON.parse(json);
}
}
export class SingletonTextRepo<T extends number[], K = T>
implements Repository<T, K> {
private _path: string;
constructor(root: string, name: string) {
this._path = `${root}/${name}.txt`;
}
discover(id: Id<K>): Promise<boolean> {
return exists(this._path);
}
async load(id: Id<K>): Promise<T> {
console.log("Idz: Repository: Loading ", this._path);
const existence = await exists(this._path);
if (!existence) {
return new Array() as T;
}
const text = await readFile(this._path, { encoding: "utf8" });
const lines = text.split("\n");
const codes = lines.map(parseInt);
return codes as T;
}
save(id: Id<K>, codes: T): Promise<void> {
console.log("Idz: Repository: Saving ", this._path);
const text = codes.map(code => code.toString()).join("\n") + "\n";
return writeFile(this._path, text, { encoding: "utf8" });
}
}
+29
View File
@@ -0,0 +1,29 @@
import * as Model from "../model";
export interface Repository<T, K = T> {
discover(id: Model.Id<K>): Promise<boolean>;
load(id: Model.Id<K>): Promise<T>;
save(id: Model.Id<K>, obj: T): Promise<void>;
}
export interface ProfileRepository extends Repository<Model.Profile> {
discoverByAimeId(id: Model.AimeId): Promise<boolean>;
loadByAimeId(id: Model.AimeId): Promise<Model.Profile>;
}
export interface World {
backgrounds(): Repository<Model.BackgroundCode[], Model.Profile>;
car(): Repository<Model.Car, Model.Profile>;
chara(): Repository<Model.Chara, Model.Profile>;
profile(): ProfileRepository;
settings(): Repository<Model.Settings, Model.Profile>;
titles(): Repository<Model.TitleCode[], Model.Profile>;
}
+62
View File
@@ -0,0 +1,62 @@
import * as fs from "fs";
import { promisify } from "util";
import { World } from "./base";
import { ProfileRepositoryImpl } from "./profile";
import { SingletonJsonRepo, SingletonTextRepo } from "./_singleton";
import * as Model from "../model";
export { World };
const exists = promisify(fs.exists);
const mkdir = promisify(fs.mkdir);
class WorldImpl implements World {
constructor(private readonly _root: string) {}
backgrounds() {
return new SingletonTextRepo<Model.BackgroundCode[], Model.Profile>(
this._root,
"backgrounds"
);
}
car() {
return new SingletonJsonRepo<Model.Car, Model.Profile>(this._root, "car");
}
chara() {
return new SingletonJsonRepo<Model.Chara, Model.Profile>(
this._root,
"chara"
);
}
profile() {
return new ProfileRepositoryImpl(this._root);
}
settings() {
return new SingletonJsonRepo<Model.Settings, Model.Profile>(
this._root,
"settings"
);
}
titles() {
return new SingletonTextRepo<Model.TitleCode[], Model.Profile>(
this._root,
"titles"
);
}
}
export async function createWorld(root: string): Promise<World> {
const doesExist = await exists(root);
if (!doesExist) {
await mkdir(root);
}
return new WorldImpl(root);
}
+19
View File
@@ -0,0 +1,19 @@
import { SingletonJsonRepo } from "./_singleton";
import { ProfileRepository } from "./base";
import { AimeId } from "../model/base";
import { Profile } from "../model/profile";
export class ProfileRepositoryImpl extends SingletonJsonRepo<Profile>
implements ProfileRepository {
constructor(root) {
super(root, "profile");
}
discoverByAimeId(id: AimeId): Promise<boolean> {
return this._discoverSingleton();
}
loadByAimeId(id: AimeId): Promise<Profile> {
return this._loadSingleton();
}
}