Refactor repository layer (and implement garage)

This commit is contained in:
Tau
2019-04-14 21:29:26 -04:00
parent f224e466c5
commit a4a6550abb
31 changed files with 452 additions and 238 deletions

View File

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

View File

@@ -29,10 +29,10 @@ export function saveProfile(buf: Buffer): SaveProfileRequest {
storyRows.push(row);
}
const coursePlays = new Array();
const coursePlays = new Map<number, number>();
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 {

View File

@@ -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<Profile>,
backgrounds: bitmap(buf.slice(0x0008, 0x002c)) as BackgroundCode[],
selectedCar: buf.readUInt16LE(0x009c) as CarSelector,
chara: chara(buf.slice(0x009e, 0x00b2)),
};
}

View File

@@ -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);

View File

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

View File

@@ -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<GenericResponse> {
const profileId = await w.profile().generateId();
const profile: Profile = {
id: 1 as Id<Profile>,
teamId: 2 as Id<Team>,
id: profileId,
teamId: 2 as Id<Team>, // 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),
]);

View File

@@ -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<LoadGarageResponse> {
throw new Error("TODO");
return {
type: "load_garage_res",
cars: await w.car().loadAllCars(req.profileId),
};
}

View File

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

View File

@@ -6,7 +6,7 @@ export async function loadStocker(
w: World,
req: LoadStockerRequest
): Promise<LoadStockerResponse> {
const backgrounds = await w.backgrounds().load(req.profileId);
const backgrounds = await w.backgrounds().loadAll(req.profileId);
return {
type: "load_stocker_res",

View File

@@ -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),
]);

View File

@@ -6,8 +6,11 @@ export async function saveStocker(
w: World,
req: SaveStockerRequest
): Promise<GenericResponse> {
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" };
}

View File

@@ -7,21 +7,12 @@ export async function saveTimeAttack(
req: SaveTimeAttackRequest
): Promise<SaveTimeAttackResponse> {
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);
}
}

View File

@@ -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<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" };
export type TitleCode = number & { __brand: "titleCode" };

View File

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

View File

@@ -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";

View File

@@ -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[];
}

View File

@@ -17,7 +17,7 @@ export interface SaveProfileRequest {
title: TitleCode;
titles: TitleCode[];
background: BackgroundCode;
coursePlays: number[];
coursePlays: Map<number, number>;
missions: MissionState;
car: Car;
story: Story;

View File

@@ -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<Profile>;
selectedCar: CarSelector;
backgrounds: BackgroundCode[];
chara: Chara;
}

View File

@@ -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<Profile>; // u32
dayNight: number;
payload: TimeAttackCourse;
payload: TimeAttackScore;
field_0002: number; // u16, always 1
field_0008: number; // u32
field_000C: number; // u16, car type?

View File

@@ -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<number, number>;
missions: MissionState;
timeAttack: TimeAttackState;
timeAttack: TimeAttackScore[];
car: Car;
carCount: number;
story: Story;
unlocks: Unlocks;
// giga TODO

20
src/idz/world/_facet.ts Normal file
View File

@@ -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<T> implements FacetRepository<T> {
private _path: string;
constructor(root: string, name: string) {
this._path = `${root}/${name}.json`;
}
load(id: Id<Profile>): Promise<T> {
return loadJson(this._path);
}
save(id: Id<Profile>, obj: T) {
return saveJson(this._path, obj);
}
}

21
src/idz/world/_flag.ts Normal file
View File

@@ -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<T extends number>
implements FlagRepository<T> {
private _path: string;
constructor(root: string, name: string) {
this._path = `${root}/${name}.txt`;
}
loadAll(profileId: Id<Profile>): Promise<T[]> {
return loadNumberList(this._path);
}
saveAll(profileId: Id<Profile>, codes: T[]): Promise<void> {
return saveNumberList(this._path, codes);
}
}

View File

@@ -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<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").filter(x => x !== "");
const codes = lines.map(x => parseInt(x)); // .map(parseInt) yields NaNs??
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" });
}
}

48
src/idz/world/_util.ts Normal file
View File

@@ -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<T>(path: string): Promise<T> {
console.log("Idz: Repository: Loading ", path);
const json = await readFile(path, { encoding: "utf8" });
return JSON.parse(json);
}
export async function saveJson<T>(path: string, obj: T): Promise<void> {
console.log("Idz: Repository: Saving ", path);
const json = JSON.stringify(obj, undefined, 2);
return writeFile(path, json, { encoding: "utf8" });
}
export async function loadNumberList<T extends number[]>(
path: string
): Promise<T> {
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<T extends number[]>(
path: string,
codes: T
): Promise<void> {
console.log("Idz: Repository: Saving ", path);
const text = codes.map(code => code.toString()).join("\n") + "\n";
return writeFile(path, text, { encoding: "ascii" });
}

View File

@@ -1,40 +0,0 @@
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>;
coursePlays(): Repository<number[], Model.Profile>;
missions(): Repository<Model.MissionState, Model.Profile>;
profile(): ProfileRepository;
settings(): Repository<Model.Settings, Model.Profile>;
story(): Repository<Model.Story, Model.Profile>;
// this is godawful do a real repo layer already jfc
timeAttack(): Repository<Model.TimeAttackState, Model.Profile>;
titles(): Repository<Model.TitleCode[], Model.Profile>;
unlocks(): Repository<Model.Unlocks, Model.Profile>;
}

66
src/idz/world/car.ts Normal file
View File

@@ -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<Profile>): Promise<number> {
const garage: GarageJson = await loadJson(this._path);
return garage.cars.length;
}
async loadAllCars(profileId: Id<Profile>): Promise<Car[]> {
const garage: GarageJson = await loadJson(this._path);
return garage.cars;
}
async loadSelectedCar(profileId: Id<Profile>): Promise<Car> {
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<Profile>, car: Car): Promise<void> {
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<Profile>,
selection: CarSelector
): Promise<void> {
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);
}
}

View File

@@ -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<Profile>): Promise<Map<number, number>> {
const lines = await loadNumberList(this._path);
const map = new Map<number, number>();
for (let i = 0; i < lines.length; i++) {
map.set(i, lines[i]);
}
return map;
}
saveAll(profileId: Id<Profile>, counts: Map<number, number>): Promise<void> {
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);
}
}

92
src/idz/world/defs.ts Normal file
View File

@@ -0,0 +1,92 @@
import * as Model from "../model";
export interface CarRepository {
countCars(profileId: Model.Id<Model.Profile>): Promise<number>;
loadAllCars(profileId: Model.Id<Model.Profile>): Promise<Model.Car[]>;
loadSelectedCar(profileId: Model.Id<Model.Profile>): Promise<Model.Car>;
saveCar(profileId: Model.Id<Model.Profile>, car: Model.Car): Promise<void>;
saveSelection(
profileId: Model.Id<Model.Profile>,
selector: Model.CarSelector
): Promise<void>;
}
export interface CoursePlaysRepository {
loadAll(profileId: Model.Id<Model.Profile>): Promise<Map<number, number>>;
saveAll(
profileId: Model.Id<Model.Profile>,
counts: Map<number, number>
): Promise<void>;
}
export interface FacetRepository<T> {
load(profileId: Model.Id<Model.Profile>): Promise<T>;
save(profileId: Model.Id<Model.Profile>, facet: T): Promise<void>;
}
export interface FlagRepository<T extends number> {
loadAll(profileId: Model.Id<Model.Profile>): Promise<T[]>;
saveAll(profileId: Model.Id<Model.Profile>, items: T[]): Promise<void>;
}
export interface ProfileRepository {
// Might want to come up with something better here
generateId(): Promise<Model.Id<Model.Profile>>;
discoverByAimeId(id: Model.AimeId): Promise<boolean>;
loadByAimeId(id: Model.AimeId): Promise<Model.Profile>;
load(id: Model.Id<Model.Profile>): Promise<Model.Profile>;
save(id: Model.Id<Model.Profile>, profile: Model.Profile): Promise<void>;
}
export interface TimeAttackRepository {
loadAll(
profileId: Model.Id<Model.Profile>
): Promise<Model.TimeAttackScore[]>;
load(
profileId: Model.Id<Model.Profile>,
courseId: number
): Promise<Model.TimeAttackScore | undefined>;
save(
profileId: Model.Id<Model.Profile>,
score: Model.TimeAttackScore
): Promise<void>;
}
export interface World {
backgrounds(): FlagRepository<Model.BackgroundCode>;
car(): CarRepository;
chara(): FacetRepository<Model.Chara>;
coursePlays(): CoursePlaysRepository;
// not really a facet tbh
missions(): FacetRepository<Model.MissionState>;
profile(): ProfileRepository;
settings(): FacetRepository<Model.Settings>;
// Also not a facet. w/e one step at a time.
story(): FacetRepository<Model.Story>;
timeAttack(): TimeAttackRepository;
titles(): FlagRepository<Model.TitleCode>;
unlocks(): FacetRepository<Model.Unlocks>;
}

View File

@@ -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<Model.BackgroundCode[], Model.Profile>(
return new FlagRepositoryImpl<Model.BackgroundCode>(
this._root,
"backgrounds"
);
}
car() {
return new SingletonJsonRepo<Model.Car, Model.Profile>(this._root, "car");
return new CarRepositoryImpl(this._root);
}
chara() {
return new SingletonJsonRepo<Model.Chara, Model.Profile>(
this._root,
"chara"
);
return new FacetRepositoryImpl<Model.Chara>(this._root, "chara");
}
coursePlays() {
return new SingletonTextRepo<number[], Model.Profile>(
this._root,
"coursePlays"
);
return new CoursePlaysRepositoryImpl(this._root);
}
missions() {
return new SingletonJsonRepo<Model.MissionState, Model.Profile>(
this._root,
"missions"
);
return new FacetRepositoryImpl<Model.MissionState>(this._root, "missions");
}
profile() {
@@ -51,46 +42,29 @@ class WorldImpl implements World {
}
settings() {
return new SingletonJsonRepo<Model.Settings, Model.Profile>(
this._root,
"settings"
);
return new FacetRepositoryImpl<Model.Settings>(this._root, "settings");
}
story() {
return new SingletonJsonRepo<Model.Story, Model.Profile>(
this._root,
"story"
);
return new FacetRepositoryImpl<Model.Story>(this._root, "story");
}
timeAttack() {
return new SingletonJsonRepo<Model.TimeAttackState, Model.Profile>(
this._root,
"timeAttack"
);
return new TimeAttackRepositoryImpl(this._root);
}
titles() {
return new SingletonTextRepo<Model.TitleCode[], Model.Profile>(
this._root,
"titles"
);
return new FlagRepositoryImpl<Model.TitleCode>(this._root, "titles");
}
unlocks() {
return new SingletonJsonRepo<Model.Unlocks, Model.Profile>(
this._root,
"unlocks"
);
return new FacetRepositoryImpl<Model.Unlocks>(this._root, "unlocks");
}
}
export async function createWorld(root: string): Promise<World> {
const doesExist = await exists(root);
if (!doesExist) {
await mkdir(root);
if (!fs.existsSync(root)) {
fs.mkdirSync(root);
}
return new WorldImpl(root);

View File

@@ -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<Profile>
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<Id<Profile>> {
return 1 as Id<Profile>;
}
discoverByAimeId(id: AimeId): Promise<boolean> {
return this._discoverSingleton();
return exists(this._path);
}
loadByAimeId(id: AimeId): Promise<Profile> {
return this._loadSingleton();
return loadJson(this._path);
}
load(id: Id<Profile>): Promise<Profile> {
return loadJson(this._path);
}
save(id: Id<Profile>, profile: Profile): Promise<void> {
return saveJson(this._path, profile);
}
}

View File

@@ -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<Profile>): Promise<TimeAttackScore[]> {
if (!fs.existsSync(this._path)) {
return [];
}
const json: TimeAttackJson = await loadJson(this._path);
return json.scores;
}
async load(
profileId: Id<Profile>,
courseId: number
): Promise<TimeAttackScore | undefined> {
const scores = await this.loadAll(profileId);
return scores.find(item => item.courseId === courseId);
}
async save(profileId: Id<Profile>, score: TimeAttackScore): Promise<void> {
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);
}
}