From a4a6550abb723004f685e45b6e25697da85e8b35 Mon Sep 17 00:00:00 2001 From: Tau Date: Sun, 14 Apr 2019 21:29:26 -0400 Subject: [PATCH] Refactor repository layer (and implement garage) --- src/idz/decoder/_car.ts | 4 +- src/idz/decoder/saveProfile.ts | 6 +- src/idz/decoder/saveStocker.ts | 3 + src/idz/encoder/_car.ts | 2 +- src/idz/encoder/loadProfile2.ts | 22 +++++--- src/idz/handler/createProfile.ts | 13 +++-- src/idz/handler/loadGarage.ts | 7 ++- src/idz/handler/loadProfile.ts | 10 ++-- src/idz/handler/loadStocker.ts | 2 +- src/idz/handler/saveProfile.ts | 6 +- src/idz/handler/saveStocker.ts | 7 ++- src/idz/handler/saveTimeAttack.ts | 17 ++---- src/idz/model/base.ts | 6 +- src/idz/model/car.ts | 5 +- src/idz/model/index.ts | 4 +- src/idz/model/timeAttack.ts | 6 +- src/idz/request/saveProfile.ts | 2 +- src/idz/request/saveStocker.ts | 2 + src/idz/request/saveTimeAttack.ts | 4 +- src/idz/response/loadProfile2.ts | 7 ++- src/idz/world/_facet.ts | 20 +++++++ src/idz/world/_flag.ts | 21 +++++++ src/idz/world/_singleton.ts | 82 --------------------------- src/idz/world/_util.ts | 48 ++++++++++++++++ src/idz/world/base.ts | 40 -------------- src/idz/world/car.ts | 66 ++++++++++++++++++++++ src/idz/world/coursePlays.ts | 43 +++++++++++++++ src/idz/world/defs.ts | 92 +++++++++++++++++++++++++++++++ src/idz/world/index.ts | 62 ++++++--------------- src/idz/world/profile.ts | 34 +++++++++--- src/idz/world/timeAttack.ts | 47 ++++++++++++++++ 31 files changed, 452 insertions(+), 238 deletions(-) create mode 100644 src/idz/world/_facet.ts create mode 100644 src/idz/world/_flag.ts delete mode 100644 src/idz/world/_singleton.ts create mode 100644 src/idz/world/_util.ts delete mode 100644 src/idz/world/base.ts create mode 100644 src/idz/world/car.ts create mode 100644 src/idz/world/coursePlays.ts create mode 100644 src/idz/world/defs.ts create mode 100644 src/idz/world/timeAttack.ts diff --git a/src/idz/decoder/_car.ts b/src/idz/decoder/_car.ts index 7b9abb8..8514999 100644 --- a/src/idz/decoder/_car.ts +++ b/src/idz/decoder/_car.ts @@ -1,4 +1,4 @@ -import { Car } from "../model/car"; +import { Car, CarSelector } from "../model/car"; export function car(buf: Buffer): Car { const field_04: number[] = []; @@ -11,7 +11,7 @@ export function car(buf: Buffer): Car { field_00: buf.readUInt16LE(0x0000), field_02: buf.readUInt16LE(0x0002), field_04, - field_44: buf.readUInt16LE(0x0044), + selector: buf.readUInt16LE(0x0044) as CarSelector, field_46: buf.readUInt16LE(0x0046), field_48: buf.readUInt16LE(0x0048), field_4A: buf.readUInt16LE(0x004a), diff --git a/src/idz/decoder/saveProfile.ts b/src/idz/decoder/saveProfile.ts index 13b7e17..0d29d7d 100644 --- a/src/idz/decoder/saveProfile.ts +++ b/src/idz/decoder/saveProfile.ts @@ -29,10 +29,10 @@ export function saveProfile(buf: Buffer): SaveProfileRequest { storyRows.push(row); } - const coursePlays = new Array(); + const coursePlays = new Map(); - for (let i = 0x0000; i < 0x0020; i += 2) { - coursePlays.push(buf.readUInt16LE(0x04c0 + i)); + for (let i = 0; i < 16; i++) { + coursePlays.set(i, buf.readUInt16LE(0x04c0 + 2 * i)); } return { diff --git a/src/idz/decoder/saveStocker.ts b/src/idz/decoder/saveStocker.ts index 0d5903c..e274b7f 100644 --- a/src/idz/decoder/saveStocker.ts +++ b/src/idz/decoder/saveStocker.ts @@ -2,6 +2,7 @@ import { bitmap } from "./_bitmap"; import { chara } from "./_chara"; import { RequestCode } from "./_defs"; import { BackgroundCode, Id } from "../model/base"; +import { CarSelector } from "../model/car"; import { Profile } from "../model/profile"; import { SaveStockerRequest } from "../request/saveStocker"; @@ -12,7 +13,9 @@ export function saveStocker(buf: Buffer): SaveStockerRequest { return { type: "save_stocker_req", profileId: buf.readUInt32LE(0x0004) as Id, + backgrounds: bitmap(buf.slice(0x0008, 0x002c)) as BackgroundCode[], + selectedCar: buf.readUInt16LE(0x009c) as CarSelector, chara: chara(buf.slice(0x009e, 0x00b2)), }; } diff --git a/src/idz/encoder/_car.ts b/src/idz/encoder/_car.ts index a056183..ee58f34 100644 --- a/src/idz/encoder/_car.ts +++ b/src/idz/encoder/_car.ts @@ -13,7 +13,7 @@ export function car(car: Car): Buffer { ); } - buf.writeUInt16LE(car.field_44, 0x0044); + buf.writeUInt16LE(car.selector, 0x0044); buf.writeUInt16LE(car.field_46, 0x0046); buf.writeUInt16LE(car.field_48, 0x0048); buf.writeUInt16LE(car.field_4A, 0x004a); diff --git a/src/idz/encoder/loadProfile2.ts b/src/idz/encoder/loadProfile2.ts index 14de54a..7c2a935 100644 --- a/src/idz/encoder/loadProfile2.ts +++ b/src/idz/encoder/loadProfile2.ts @@ -13,21 +13,21 @@ export function loadProfile2(res: LoadProfileResponse2) { //buf.fill(0xff, 0x0000, 0x08f4); //buf.fill(0xff, 0x0c20, 0x0c2a); - for (const course of res.timeAttack.courses) { - const { courseId } = course; + for (const score of res.timeAttack) { + const { courseId } = score; buf.writeUInt32LE( - (new Date(course.timestamp).getTime() / 1000) | 0, // Date ctor hack + (new Date(score.timestamp).getTime() / 1000) | 0, // Date ctor hack 0x00e4 + courseId * 4 ); buf.writeUInt16LE(0xffff, 0x0164 + 2 * courseId); // National rank - buf.writeUInt32LE(course.totalMsec, 0x04e0 + 4 * courseId); - buf.writeUInt8(course.flags, 0x05a0 + courseId); + buf.writeUInt32LE(score.totalMsec, 0x04e0 + 4 * courseId); + buf.writeUInt8(score.flags, 0x05a0 + courseId); for (let i = 0; i < 3; i++) { buf.writeUInt16LE( - course.stageMsec[i] >> 2, + score.stageMsec[i] >> 2, 0x05c0 + 6 * courseId + 2 * i ); } @@ -46,8 +46,12 @@ export function loadProfile2(res: LoadProfileResponse2) { } } - for (let i = 0; i < 16 && i < res.coursePlays.length; i++) { - buf.writeUInt16LE(res.coursePlays[i], 0x0460 + 2 * i); + for (const [courseId, playCount] of res.coursePlays.entries()) { + if (courseId < 0 || courseId >= 16) { + throw new Error(`Course id out of range: ${courseId}`); + } + + buf.writeUInt16LE(playCount, 0x0460 + 2 * courseId); } buf.writeUInt16LE(0x0065, 0x0000); @@ -74,7 +78,7 @@ export function loadProfile2(res: LoadProfileResponse2) { buf.writeUInt8(res.settings.gauges, 0x07da); buf.writeUInt32LE(res.teamId || 0xffffffff, 0x07e0); car(res.car).copy(buf, 0x0c5c); - buf.writeUInt32LE(1, 0x0c58); // Cars in garage + buf.writeUInt32LE(res.carCount, 0x0c58); // [1] Currently unknown, but if this field is zero then the player will have // a "model record" emblem in their profile card. diff --git a/src/idz/handler/createProfile.ts b/src/idz/handler/createProfile.ts index 6ef41b3..4368b0e 100644 --- a/src/idz/handler/createProfile.ts +++ b/src/idz/handler/createProfile.ts @@ -4,7 +4,7 @@ import { Profile } from "../model/profile"; import { Settings } from "../model/settings"; import { Story } from "../model/story"; import { Team } from "../model/team"; -import { TimeAttackState } from "../model/timeAttack"; +import { TimeAttackScore } from "../model/timeAttack"; import { Unlocks } from "../model/unlocks"; import { CreateProfileRequest } from "../request/createProfile"; import { GenericResponse } from "../response/generic"; @@ -14,9 +14,11 @@ export async function createProfile( w: World, req: CreateProfileRequest ): Promise { + const profileId = await w.profile().generateId(); + const profile: Profile = { - id: 1 as Id, - teamId: 2 as Id, + id: profileId, + teamId: 2 as Id, // TODO name: req.name, lv: 1, exp: 0, @@ -28,17 +30,16 @@ export async function createProfile( const missions: MissionState = { team: [], solo: [] }; const settings: Settings = { music: 0, pack: 13640, paperCup: 0, gauges: 5 }; const story: Story = { x: 0, y: 0, rows: [] }; - const timeAttack: TimeAttackState = { courses: [] }; const unlocks: Unlocks = { cup: 0, gauges: 0, music: 0 }; await Promise.all([ w.profile().save(profile.id, profile), w.chara().save(profile.id, req.chara), - w.car().save(profile.id, req.car), + w.car().saveCar(profile.id, req.car), + w.car().saveSelection(profile.id, req.car.selector), w.missions().save(profile.id, missions), w.settings().save(profile.id, settings), w.story().save(profile.id, story), - w.timeAttack().save(profile.id, timeAttack), w.unlocks().save(profile.id, unlocks), ]); diff --git a/src/idz/handler/loadGarage.ts b/src/idz/handler/loadGarage.ts index 3829abe..f30ef31 100644 --- a/src/idz/handler/loadGarage.ts +++ b/src/idz/handler/loadGarage.ts @@ -1,12 +1,13 @@ -import { Car } from "../model"; import { LoadGarageRequest } from "../request/loadGarage"; import { LoadGarageResponse } from "../response/loadGarage"; import { World } from "../world"; -import { loadJson } from "../world/_util"; export async function loadGarage( w: World, req: LoadGarageRequest ): Promise { - throw new Error("TODO"); + return { + type: "load_garage_res", + cars: await w.car().loadAllCars(req.profileId), + }; } diff --git a/src/idz/handler/loadProfile.ts b/src/idz/handler/loadProfile.ts index 34c3af1..7be088f 100644 --- a/src/idz/handler/loadProfile.ts +++ b/src/idz/handler/loadProfile.ts @@ -12,12 +12,13 @@ export async function loadProfile( 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 coursePlays = await w.coursePlays().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().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().load(profile.id); + const timeAttack = await w.timeAttack().loadAll(profile.id); const unlocks = await w.unlocks().load(profile.id); return { @@ -37,6 +38,7 @@ export async function loadProfile( missions, timeAttack, car, + carCount, story, unlocks, }; diff --git a/src/idz/handler/loadStocker.ts b/src/idz/handler/loadStocker.ts index d00a6f7..80e6164 100644 --- a/src/idz/handler/loadStocker.ts +++ b/src/idz/handler/loadStocker.ts @@ -6,7 +6,7 @@ export async function loadStocker( w: World, req: LoadStockerRequest ): Promise { - const backgrounds = await w.backgrounds().load(req.profileId); + const backgrounds = await w.backgrounds().loadAll(req.profileId); return { type: "load_stocker_res", diff --git a/src/idz/handler/saveProfile.ts b/src/idz/handler/saveProfile.ts index 6f63c89..a3280bf 100644 --- a/src/idz/handler/saveProfile.ts +++ b/src/idz/handler/saveProfile.ts @@ -23,11 +23,11 @@ export async function saveProfile( title: req.title, background: req.background, }), - w.car().save(req.profileId, req.car), - w.coursePlays().save(req.profileId, req.coursePlays), + w.car().saveCar(req.profileId, req.car), + w.coursePlays().saveAll(req.profileId, req.coursePlays), w.missions().save(req.profileId, req.missions), w.story().save(req.profileId, req.story), - w.titles().save(req.profileId, req.titles), + w.titles().saveAll(req.profileId, req.titles), w.unlocks().save(req.profileId, req.unlocks), w.settings().save(req.profileId, req.settings), ]); diff --git a/src/idz/handler/saveStocker.ts b/src/idz/handler/saveStocker.ts index 8a3830c..5e09f6a 100644 --- a/src/idz/handler/saveStocker.ts +++ b/src/idz/handler/saveStocker.ts @@ -6,8 +6,11 @@ export async function saveStocker( w: World, req: SaveStockerRequest ): Promise { - await w.backgrounds().save(req.profileId, req.backgrounds); - await w.chara().save(req.profileId, req.chara); + await Promise.all([ + w.backgrounds().saveAll(req.profileId, req.backgrounds), + w.chara().save(req.profileId, req.chara), + w.car().saveSelection(req.profileId, req.selectedCar), + ]); return { type: "generic_res" }; } diff --git a/src/idz/handler/saveTimeAttack.ts b/src/idz/handler/saveTimeAttack.ts index 31dbad9..d47a288 100644 --- a/src/idz/handler/saveTimeAttack.ts +++ b/src/idz/handler/saveTimeAttack.ts @@ -7,21 +7,12 @@ export async function saveTimeAttack( req: SaveTimeAttackRequest ): Promise { if (req.payload.totalMsec > 0) { - const state = await w.timeAttack().load(req.profileId); - const existing = state.courses.find( - course => course.courseId === req.payload.courseId - ); + const existing = await w + .timeAttack() + .load(req.profileId, req.payload.courseId); if (existing === undefined || existing.totalMsec > req.payload.totalMsec) { - const newCourses = state.courses.filter( - course => course.courseId !== req.payload.courseId - ); - - newCourses.push(req.payload); - - const newState = { courses: newCourses }; - - await w.timeAttack().save(req.profileId, newState); + await w.timeAttack().save(req.profileId, req.payload); } } diff --git a/src/idz/model/base.ts b/src/idz/model/base.ts index 0cb793a..b6b81c8 100644 --- a/src/idz/model/base.ts +++ b/src/idz/model/base.ts @@ -1,7 +1,7 @@ // Quasi-nominative "brands". These double-underscore properties never actually // exist at run time, we just pretend they do. +export type AimeId = number & { __brand: "aimeId" }; +export type BackgroundCode = number & { __brand: "backgroundCode" }; export type Id = number & { __id: T }; -export type AimeId = number & { __brand: "aime_id" }; -export type BackgroundCode = number & { __brand: "background_code" }; -export type TitleCode = number & { __brand: "title_code" }; +export type TitleCode = number & { __brand: "titleCode" }; diff --git a/src/idz/model/car.ts b/src/idz/model/car.ts index 6f946a9..7845662 100644 --- a/src/idz/model/car.ts +++ b/src/idz/model/car.ts @@ -1,8 +1,11 @@ +// See base.ts for information about branding. +export type CarSelector = number & { __brand: "car_selector" }; + export interface Car { field_00: number; field_02: number; field_04: number[]; - field_44: number; + selector: CarSelector; field_46: number; field_48: number; field_4A: number; diff --git a/src/idz/model/index.ts b/src/idz/model/index.ts index 007c696..35f200d 100644 --- a/src/idz/model/index.ts +++ b/src/idz/model/index.ts @@ -1,10 +1,10 @@ export { AimeId, BackgroundCode, Id, TitleCode } from "./base"; -export { Car } from "./car"; +export { Car, CarSelector } from "./car"; export { Chara } from "./chara"; export { MissionState, MissionGrid } from "./mission"; export { Profile } from "./profile"; export { Settings } from "./settings"; export { Story } from "./story"; export { Team } from "./team"; -export { TimeAttackState, TimeAttackCourse } from "./timeAttack"; +export { TimeAttackScore } from "./timeAttack"; export { Unlocks } from "./unlocks"; diff --git a/src/idz/model/timeAttack.ts b/src/idz/model/timeAttack.ts index 5cd0f0a..c820c86 100644 --- a/src/idz/model/timeAttack.ts +++ b/src/idz/model/timeAttack.ts @@ -1,11 +1,7 @@ -export interface TimeAttackCourse { +export interface TimeAttackScore { courseId: number; timestamp: string; // hack flags: number; totalMsec: number; stageMsec: number[]; } - -export interface TimeAttackState { - courses: TimeAttackCourse[]; -} diff --git a/src/idz/request/saveProfile.ts b/src/idz/request/saveProfile.ts index 4611218..b449c83 100644 --- a/src/idz/request/saveProfile.ts +++ b/src/idz/request/saveProfile.ts @@ -17,7 +17,7 @@ export interface SaveProfileRequest { title: TitleCode; titles: TitleCode[]; background: BackgroundCode; - coursePlays: number[]; + coursePlays: Map; missions: MissionState; car: Car; story: Story; diff --git a/src/idz/request/saveStocker.ts b/src/idz/request/saveStocker.ts index 6a456be..cb2c0ee 100644 --- a/src/idz/request/saveStocker.ts +++ b/src/idz/request/saveStocker.ts @@ -1,10 +1,12 @@ import { BackgroundCode, Id } from "../model/base"; +import { CarSelector } from "../model/car"; import { Chara } from "../model/chara"; import { Profile } from "../model/profile"; export interface SaveStockerRequest { type: "save_stocker_req"; profileId: Id; + selectedCar: CarSelector; backgrounds: BackgroundCode[]; chara: Chara; } diff --git a/src/idz/request/saveTimeAttack.ts b/src/idz/request/saveTimeAttack.ts index 4ce45d8..9ecc9cc 100644 --- a/src/idz/request/saveTimeAttack.ts +++ b/src/idz/request/saveTimeAttack.ts @@ -1,12 +1,12 @@ import { Id } from "../model/base"; import { Profile } from "../model/profile"; -import { TimeAttackCourse } from "../model/timeAttack"; +import { TimeAttackScore } from "../model/timeAttack"; export interface SaveTimeAttackRequest { type: "save_time_attack_req"; profileId: Id; // u32 dayNight: number; - payload: TimeAttackCourse; + payload: TimeAttackScore; field_0002: number; // u16, always 1 field_0008: number; // u32 field_000C: number; // u16, car type? diff --git a/src/idz/response/loadProfile2.ts b/src/idz/response/loadProfile2.ts index dc416fa..66aa4f1 100644 --- a/src/idz/response/loadProfile2.ts +++ b/src/idz/response/loadProfile2.ts @@ -5,7 +5,7 @@ import { MissionState } from "../model/mission"; import { Profile } from "../model/profile"; import { Settings } from "../model/settings"; import { Story } from "../model/story"; -import { TimeAttackState } from "../model/timeAttack"; +import { TimeAttackScore } from "../model/timeAttack"; import { Unlocks } from "../model/unlocks"; export interface LoadProfileResponse2 { @@ -21,10 +21,11 @@ export interface LoadProfileResponse2 { settings: Settings; chara: Chara; titles: TitleCode[]; - coursePlays: number[]; + coursePlays: Map; missions: MissionState; - timeAttack: TimeAttackState; + timeAttack: TimeAttackScore[]; car: Car; + carCount: number; story: Story; unlocks: Unlocks; // giga TODO diff --git a/src/idz/world/_facet.ts b/src/idz/world/_facet.ts new file mode 100644 index 0000000..e8960b4 --- /dev/null +++ b/src/idz/world/_facet.ts @@ -0,0 +1,20 @@ +import { loadJson, saveJson } from "./_util"; +import { FacetRepository } from "./defs"; +import { Id } from "../model/base"; +import { Profile } from "../model/profile"; + +export class FacetRepositoryImpl implements FacetRepository { + private _path: string; + + constructor(root: string, name: string) { + this._path = `${root}/${name}.json`; + } + + load(id: Id): Promise { + return loadJson(this._path); + } + + save(id: Id, obj: T) { + return saveJson(this._path, obj); + } +} diff --git a/src/idz/world/_flag.ts b/src/idz/world/_flag.ts new file mode 100644 index 0000000..ff30554 --- /dev/null +++ b/src/idz/world/_flag.ts @@ -0,0 +1,21 @@ +import { loadNumberList, saveNumberList } from "./_util"; +import { FlagRepository } from "./defs"; +import { Id } from "../model/base"; +import { Profile } from "../model/profile"; + +export class FlagRepositoryImpl + implements FlagRepository { + private _path: string; + + constructor(root: string, name: string) { + this._path = `${root}/${name}.txt`; + } + + loadAll(profileId: Id): Promise { + return loadNumberList(this._path); + } + + saveAll(profileId: Id, codes: T[]): Promise { + return saveNumberList(this._path, codes); + } +} diff --git a/src/idz/world/_singleton.ts b/src/idz/world/_singleton.ts deleted file mode 100644 index 89b93e5..0000000 --- a/src/idz/world/_singleton.ts +++ /dev/null @@ -1,82 +0,0 @@ -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 implements Repository { - private _path: string; - - constructor(root: string, name: string) { - this._path = `${root}/${name}.json`; - } - - discover(id: Id): Promise { - return this._discoverSingleton(); - } - - load(id: Id): Promise { - return this._loadSingleton(); - } - - save(id: Id, 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 { - return exists(this._path); - } - - protected async _loadSingleton(): Promise { - console.log("Idz: Repository: Loading ", this._path); - - const json = await readFile(this._path, { encoding: "utf8" }); - - return JSON.parse(json); - } -} - -export class SingletonTextRepo - implements Repository { - private _path: string; - - constructor(root: string, name: string) { - this._path = `${root}/${name}.txt`; - } - - discover(id: Id): Promise { - return exists(this._path); - } - - async load(id: Id): Promise { - 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").filter(x => x !== ""); - const codes = lines.map(x => parseInt(x)); // .map(parseInt) yields NaNs?? - - return codes as T; - } - - save(id: Id, codes: T): Promise { - console.log("Idz: Repository: Saving ", this._path); - - const text = codes.map(code => code.toString()).join("\n") + "\n"; - - return writeFile(this._path, text, { encoding: "utf8" }); - } -} diff --git a/src/idz/world/_util.ts b/src/idz/world/_util.ts new file mode 100644 index 0000000..a962724 --- /dev/null +++ b/src/idz/world/_util.ts @@ -0,0 +1,48 @@ +import * as fs from "fs"; +import { promisify } from "util"; + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +export async function loadJson(path: string): Promise { + console.log("Idz: Repository: Loading ", path); + + const json = await readFile(path, { encoding: "utf8" }); + + return JSON.parse(json); +} + +export async function saveJson(path: string, obj: T): Promise { + console.log("Idz: Repository: Saving ", path); + + const json = JSON.stringify(obj, undefined, 2); + + return writeFile(path, json, { encoding: "utf8" }); +} + +export async function loadNumberList( + path: string +): Promise { + console.log("Idz: Repository: Loading ", path); + + if (!fs.existsSync(path)) { + return new Array() as T; + } + + const text = await readFile(path, { encoding: "ascii" }); + const lines = text.split("\n").filter(x => x !== ""); + const codes = lines.map(Number); // parseInt fails due to radix arg from map + + return codes as T; +} + +export async function saveNumberList( + path: string, + codes: T +): Promise { + console.log("Idz: Repository: Saving ", path); + + const text = codes.map(code => code.toString()).join("\n") + "\n"; + + return writeFile(path, text, { encoding: "ascii" }); +} diff --git a/src/idz/world/base.ts b/src/idz/world/base.ts deleted file mode 100644 index b6eb81e..0000000 --- a/src/idz/world/base.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as Model from "../model"; - -export interface Repository { - discover(id: Model.Id): Promise; - - load(id: Model.Id): Promise; - - save(id: Model.Id, obj: T): Promise; -} - -export interface ProfileRepository extends Repository { - discoverByAimeId(id: Model.AimeId): Promise; - - loadByAimeId(id: Model.AimeId): Promise; -} - -export interface World { - backgrounds(): Repository; - - car(): Repository; - - chara(): Repository; - - coursePlays(): Repository; - - missions(): Repository; - - profile(): ProfileRepository; - - settings(): Repository; - - story(): Repository; - - // this is godawful do a real repo layer already jfc - timeAttack(): Repository; - - titles(): Repository; - - unlocks(): Repository; -} diff --git a/src/idz/world/car.ts b/src/idz/world/car.ts new file mode 100644 index 0000000..452939b --- /dev/null +++ b/src/idz/world/car.ts @@ -0,0 +1,66 @@ +import { loadJson, saveJson } from "./_util"; +import { CarRepository } from "./defs"; +import { Id } from "../model/base"; +import { Car, CarSelector } from "../model/car"; +import { Profile } from "../model/profile"; + +interface GarageJson { + selected: CarSelector; + cars: Car[]; +} + +export class CarRepositoryImpl implements CarRepository { + private readonly _path: string; + + constructor(root: string) { + this._path = root + "/garage.json"; + } + + async countCars(profileId: Id): Promise { + const garage: GarageJson = await loadJson(this._path); + + return garage.cars.length; + } + + async loadAllCars(profileId: Id): Promise { + const garage: GarageJson = await loadJson(this._path); + + return garage.cars; + } + + async loadSelectedCar(profileId: Id): Promise { + const garage: GarageJson = await loadJson(this._path); + const sel = garage.cars.find(item => item.selector === garage.selected); + + if (sel === undefined) { + throw new Error("No car in garage matching current selector"); + } + + return sel; + } + + async saveCar(profileId: Id, car: Car): Promise { + const garage: GarageJson = await loadJson(this._path); + const rest = garage.cars.filter(item => item.selector != car.selector); + + const updated = { ...garage, cars: [...rest, car] }; + + return saveJson(this._path, updated); + } + + async saveSelection( + profileId: Id, + selection: CarSelector + ): Promise { + const garage: GarageJson = await loadJson(this._path); + const match = garage.cars.find(item => item.selector === selection); + + if (!match) { + throw new Error(`No match for selector ${selection} in garage`); + } + + garage.selected = selection; + + return saveJson(this._path, garage); + } +} diff --git a/src/idz/world/coursePlays.ts b/src/idz/world/coursePlays.ts new file mode 100644 index 0000000..c0e8f98 --- /dev/null +++ b/src/idz/world/coursePlays.ts @@ -0,0 +1,43 @@ +import { loadNumberList, saveNumberList } from "./_util"; +import { CoursePlaysRepository } from "./defs"; +import { Id } from "../model/base"; +import { Profile } from "../model/profile"; + +export class CoursePlaysRepositoryImpl implements CoursePlaysRepository { + private readonly _path; + + constructor(root) { + this._path = root + "/coursePlays.txt"; + } + + async loadAll(profileId: Id): Promise> { + const lines = await loadNumberList(this._path); + const map = new Map(); + + for (let i = 0; i < lines.length; i++) { + map.set(i, lines[i]); + } + + return map; + } + + saveAll(profileId: Id, counts: Map): Promise { + let max = 0; + + for (const key of counts.keys()) { + if (max < key) { + max = key; + } + } + + const array = new Array(max + 1); + + array.fill(0); + + for (const [k, v] of counts) { + array[k] = v; + } + + return saveNumberList(this._path, array); + } +} diff --git a/src/idz/world/defs.ts b/src/idz/world/defs.ts new file mode 100644 index 0000000..df8bd36 --- /dev/null +++ b/src/idz/world/defs.ts @@ -0,0 +1,92 @@ +import * as Model from "../model"; + +export interface CarRepository { + countCars(profileId: Model.Id): Promise; + + loadAllCars(profileId: Model.Id): Promise; + + loadSelectedCar(profileId: Model.Id): Promise; + + saveCar(profileId: Model.Id, car: Model.Car): Promise; + + saveSelection( + profileId: Model.Id, + selector: Model.CarSelector + ): Promise; +} + +export interface CoursePlaysRepository { + loadAll(profileId: Model.Id): Promise>; + + saveAll( + profileId: Model.Id, + counts: Map + ): Promise; +} + +export interface FacetRepository { + load(profileId: Model.Id): Promise; + + save(profileId: Model.Id, facet: T): Promise; +} + +export interface FlagRepository { + loadAll(profileId: Model.Id): Promise; + + saveAll(profileId: Model.Id, items: T[]): Promise; +} + +export interface ProfileRepository { + // Might want to come up with something better here + generateId(): Promise>; + + discoverByAimeId(id: Model.AimeId): Promise; + + loadByAimeId(id: Model.AimeId): Promise; + + load(id: Model.Id): Promise; + + save(id: Model.Id, profile: Model.Profile): Promise; +} + +export interface TimeAttackRepository { + loadAll( + profileId: Model.Id + ): Promise; + + load( + profileId: Model.Id, + courseId: number + ): Promise; + + save( + profileId: Model.Id, + score: Model.TimeAttackScore + ): Promise; +} + +export interface World { + backgrounds(): FlagRepository; + + car(): CarRepository; + + chara(): FacetRepository; + + coursePlays(): CoursePlaysRepository; + + // not really a facet tbh + missions(): FacetRepository; + + profile(): ProfileRepository; + + settings(): FacetRepository; + + // Also not a facet. w/e one step at a time. + story(): FacetRepository; + + timeAttack(): TimeAttackRepository; + + titles(): FlagRepository; + + unlocks(): FacetRepository; +} diff --git a/src/idz/world/index.ts b/src/idz/world/index.ts index 7ebe539..3ef1a12 100644 --- a/src/idz/world/index.ts +++ b/src/idz/world/index.ts @@ -1,49 +1,40 @@ import * as fs from "fs"; -import { promisify } from "util"; -import { World } from "./base"; +import { FacetRepositoryImpl } from "./_facet"; +import { FlagRepositoryImpl } from "./_flag"; +import { CarRepositoryImpl } from "./car"; +import { CoursePlaysRepositoryImpl } from "./coursePlays"; +import { World } from "./defs"; import { ProfileRepositoryImpl } from "./profile"; -import { SingletonJsonRepo, SingletonTextRepo } from "./_singleton"; +import { TimeAttackRepositoryImpl } from "./timeAttack"; 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( + return new FlagRepositoryImpl( this._root, "backgrounds" ); } car() { - return new SingletonJsonRepo(this._root, "car"); + return new CarRepositoryImpl(this._root); } chara() { - return new SingletonJsonRepo( - this._root, - "chara" - ); + return new FacetRepositoryImpl(this._root, "chara"); } coursePlays() { - return new SingletonTextRepo( - this._root, - "coursePlays" - ); + return new CoursePlaysRepositoryImpl(this._root); } missions() { - return new SingletonJsonRepo( - this._root, - "missions" - ); + return new FacetRepositoryImpl(this._root, "missions"); } profile() { @@ -51,46 +42,29 @@ class WorldImpl implements World { } settings() { - return new SingletonJsonRepo( - this._root, - "settings" - ); + return new FacetRepositoryImpl(this._root, "settings"); } story() { - return new SingletonJsonRepo( - this._root, - "story" - ); + return new FacetRepositoryImpl(this._root, "story"); } timeAttack() { - return new SingletonJsonRepo( - this._root, - "timeAttack" - ); + return new TimeAttackRepositoryImpl(this._root); } titles() { - return new SingletonTextRepo( - this._root, - "titles" - ); + return new FlagRepositoryImpl(this._root, "titles"); } unlocks() { - return new SingletonJsonRepo( - this._root, - "unlocks" - ); + return new FacetRepositoryImpl(this._root, "unlocks"); } } export async function createWorld(root: string): Promise { - const doesExist = await exists(root); - - if (!doesExist) { - await mkdir(root); + if (!fs.existsSync(root)) { + fs.mkdirSync(root); } return new WorldImpl(root); diff --git a/src/idz/world/profile.ts b/src/idz/world/profile.ts index cf9849f..595ea12 100644 --- a/src/idz/world/profile.ts +++ b/src/idz/world/profile.ts @@ -1,19 +1,37 @@ -import { SingletonJsonRepo } from "./_singleton"; -import { ProfileRepository } from "./base"; -import { AimeId } from "../model/base"; +import * as fs from "fs"; +import { promisify } from "util"; + +import { loadJson, saveJson } from "./_util"; +import { ProfileRepository } from "./defs"; +import { AimeId, Id } from "../model/base"; import { Profile } from "../model/profile"; -export class ProfileRepositoryImpl extends SingletonJsonRepo - implements ProfileRepository { +const exists = promisify(fs.exists); + +export class ProfileRepositoryImpl implements ProfileRepository { + private readonly _path: string; + constructor(root) { - super(root, "profile"); + this._path = root + "/profile.json"; + } + + async generateId(): Promise> { + return 1 as Id; } discoverByAimeId(id: AimeId): Promise { - return this._discoverSingleton(); + return exists(this._path); } loadByAimeId(id: AimeId): Promise { - return this._loadSingleton(); + return loadJson(this._path); + } + + load(id: Id): Promise { + return loadJson(this._path); + } + + save(id: Id, profile: Profile): Promise { + return saveJson(this._path, profile); } } diff --git a/src/idz/world/timeAttack.ts b/src/idz/world/timeAttack.ts new file mode 100644 index 0000000..e943532 --- /dev/null +++ b/src/idz/world/timeAttack.ts @@ -0,0 +1,47 @@ +import * as fs from "fs"; + +import { loadJson, saveJson } from "./_util"; +import { TimeAttackRepository } from "./defs"; +import { Id } from "../model/base"; +import { Profile } from "../model/profile"; +import { TimeAttackScore } from "../model/timeAttack"; + +interface TimeAttackJson { + scores: TimeAttackScore[]; +} + +export class TimeAttackRepositoryImpl implements TimeAttackRepository { + private readonly _path: string; + + constructor(root) { + this._path = root + "/timeAttack.json"; + } + + async loadAll(profileId: Id): Promise { + if (!fs.existsSync(this._path)) { + return []; + } + + const json: TimeAttackJson = await loadJson(this._path); + + return json.scores; + } + + async load( + profileId: Id, + courseId: number + ): Promise { + const scores = await this.loadAll(profileId); + + return scores.find(item => item.courseId === courseId); + } + + async save(profileId: Id, score: TimeAttackScore): Promise { + const scores = await this.loadAll(profileId); + const rest = scores.filter(item => item.courseId !== score.courseId); + + const updated: TimeAttackJson = { scores: [...rest, score] }; + + return saveJson(this._path, updated); + } +}