mirror of
https://github.com/djhackersdev/minime.git
synced 2026-08-20 01:14:08 -05:00
Refactor repository layer (and implement garage)
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
20
src/idz/world/_facet.ts
Normal 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
21
src/idz/world/_flag.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
48
src/idz/world/_util.ts
Normal 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" });
|
||||
}
|
||||
@@ -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
66
src/idz/world/car.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
43
src/idz/world/coursePlays.ts
Normal file
43
src/idz/world/coursePlays.ts
Normal 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
92
src/idz/world/defs.ts
Normal 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>;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
47
src/idz/world/timeAttack.ts
Normal file
47
src/idz/world/timeAttack.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user