// /
import { HZEngineCore, Platform } from "../index.js";
// import * as this._core.platform from "@zos/fs";
import Path from "../utils/path.js";
// import { isFileSync } from "./fs.js";
export class Storage {
constructor(private _core: HZEngineCore) {}
projectRoot: string | null = null;
cacheRoot: string | null = null;
saveRoot: string | null = null;
preloadedData: NonNullable | null = null;
packageData: NonNullable | null = null;
loadProject(options: {
projectPath: string;
cachePath: string;
savePath: string;
}) {
this._core.emit("beforeLoadProject");
if (!this._core.platform.readdirSync({ path: options.projectPath })) {
throw "Dir not exist";
}
this.projectRoot = options.projectPath;
this.cacheRoot = options.cachePath;
this.saveRoot = options.savePath;
this.loadPackageData();
this.preload();
this._core.emit("afterLoadProject");
}
loadPackageData() {
if (!this.projectRoot) throw "projectDir is null";
this._core.debug.log(
`loadPackageData ${this.projectRoot} ${Path.join(
this.projectRoot,
"hz_package.json"
)}`
);
if (
!this._core.platform.statSync({
path: Path.join(this.projectRoot, "hz_package.json"),
})
) {
throw "HZEngine Package File (hz_package.json) not exist";
}
this.packageData = JSON.parse(
this._core.platform.readFileSync({
path: Path.join(this.projectRoot, "hz_package.json"),
options: { encoding: "utf8" },
}) as string
);
}
// Storage Data
/**
* 全局数据
* 其中的数据不会跟随存档保存,而是直接存储在全局数据文件中
* 如:设置、CG解锁情况等
*/
private _globalData: NonNullable | null = null;
get globalData() {
if (!this._globalData) {
this.loadGlobalData();
}
if (this._globalData == null) throw `[HZEngine] GlobalData is null`;
return this._globalData;
}
/**
* alias globalData
*/
get gd() {
return this.globalData;
}
/**
* 存档数据
* 其中的数据会跟随存档保存
* 如:脚本执行位置即调用栈,攻略度等
*/
private _archiveData: NonNullable | null = null;
get archiveData(): NonNullable {
if (!this._archiveData) {
this.loadArchiveData();
}
if (this._archiveData == null) throw `[HZEngine] ArchiveData is null`;
return this._archiveData;
}
/**
* alias archiveData
*/
get sd() {
return this.archiveData;
}
loadGlobalData() {
if (!this.saveRoot) {
throw "saveDir is null, please loadProject first";
}
this._core.emit("beforeLoadGlobalData");
if (
this._core.platform.statSync({
path: Path.join(this.saveRoot, "globalData.json"),
})
) {
this._globalData = JSON.parse(
this._core.platform.readFileSync({
path: Path.join(this.saveRoot, "globalData.json"),
options: {
encoding: "utf8",
},
}) as string
);
if (this._globalData == null) {
this._globalData = {}; // TODO initial GlobalData value
this._core.emit("initGlobalData");
}
} else {
this._core.debug.log(`globalData.json not exist, create it.`);
this._globalData = {}; // Initial GlobalData value
this._core.emit("initGlobalData");
this._core.platform.writeFileSync({
path: Path.join(this.saveRoot, "globalData.json"),
data: JSON.stringify(this._globalData),
});
}
this._core.emit("afterLoadGlobalData");
}
/**
* 保存全局数据
* 可以多次調用,實際上會異步儲存,也就是在一個宏任務中即使調用多次,也只會在宏任務結束後儲存一次
*/
private _saveGlobalDataTimerId: number | null = null;
saveGlobalData() {
if (!this.projectRoot) {
throw "projectDir is null, please loadProject first";
}
if (this._saveGlobalDataTimerId) return;
this._saveGlobalDataTimerId = setTimeout(() => {
this._saveGlobalDataTimerId = null;
if (!this.projectRoot) {
throw "projectDir is null, please loadProject first";
}
this._core.emit("beforeSaveGlobalData");
let res = this._core.platform.writeFileSync({
path: Path.join(this.saveRoot!, "globalData.json"),
data: JSON.stringify(this._globalData),
}) as unknown as number; // TODO
if (res < 0)
throw `[HZEngine] save globalData to globalData.json failed, code = ${res}`;
this._core.debug.log(`save globalData to globalData.json`);
this._core.emit("afterSaveGlobalData");
}, 0) as unknown as number;
}
loadArchiveData(archiveFile?: string) {
this._core.emit("beforeLoadArchive");
if (archiveFile) {
if (!this.saveRoot) throw `saveRoot is null, please loadProject first`;
if (
!this._core.platform.statSync({
path: Path.join(this.saveRoot, archiveFile),
})
) {
throw `Archive [${archiveFile}] not exist`;
}
let archiveData: Storage.JSONValue = JSON.parse(
this._core.platform.readFileSync({
path: Path.join(this.saveRoot, archiveFile),
options: {
encoding: "utf8",
},
}) as string
);
if (archiveData == null) throw `[HZEngine] ArchiveData is null`;
this._archiveData = archiveData;
this._core.debug.log(`load archiveData from ${archiveFile}`);
this._core.emit("afterLoadArchive");
} else {
this._core.debug.log(`load archiveData from empty template`);
this._archiveData = {};
this._core.emit("initArchiveData");
}
}
_saveArchiveDataTimerId: number | null = null;
/**
* 保存存档数据
* 可以多次調用,實際上會異步儲存,也就是在一個宏任務中即使調用多次,也只會在宏任務結束後儲存一次
* @param archiveFile 存档文件目錄及名字
*/
saveArchiveData(archiveFile: string, immediate = false) {
if (!this.saveRoot) throw `saveRoot is null, please loadProject first`;
this._core.emit("beforeSaveArchive");
this._core.debug.log("Will save archiveData to " + archiveFile);
let saveFunc = () => {
this._core.debug.log("Saving archiveData to " + archiveFile);
if (!this.saveRoot) throw `projectDir is null, please loadProject first`;
let res = this._core.platform.writeFileSync({
path: Path.join(this.saveRoot!, archiveFile),
data: JSON.stringify(this._archiveData),
}) as unknown as number; //TODO
if (res < 0)
throw `[HZEngine] save archiveData to ${archiveFile} failed, code = ${res}`;
this._core.debug.log(`Save archiveData to ${archiveFile}`);
this._core.emit("afterSaveArchive");
};
if (immediate) {
saveFunc();
if (this._saveArchiveDataTimerId) {
clearTimeout(this._saveArchiveDataTimerId);
this._saveArchiveDataTimerId = null;
}
} else
this._saveArchiveDataTimerId = setTimeout(() => {
this._saveArchiveDataTimerId = null;
saveFunc();
}, 0) as unknown as number;
}
getSaveableData(
data: Storage.JSONValue,
auto_correct: boolean,
...key_chain: string[]
): NonNullable {
let obj = data;
// if(obj == null) throw `[HZEngine] saveable data is null`
for (let key of key_chain) {
if (obj == null) throw Error(`[HZEngine] saveable data is null`);
if (typeof obj !== "object")
throw Error(`[HZEngine] saveable data is not object`);
if (Array.isArray(obj)) throw Error(`[HZEngine] saveable data is array`);
if (!obj[key]) {
if (auto_correct) {
obj[key] = {};
} else throw Error(`[HZEngine] saveable data key ${key} not exist`);
}
obj = obj[key];
}
if (obj == null) throw Error(`[HZEngine] saveable data result obj is null`);
// if (typeof obj !== "object")
// throw Error(`[HZEngine] saveable data result obj is not array or object`);
// if (Array.isArray(obj)) throw Error(`[HZEngine] saveable data result is an array, key_chain = ${key_chain.join(".")}, res = ${JSON.stringify(obj)}`);
return obj;
}
setSaveableData(
data: Storage.JSONValue,
auto_correct: boolean,
value: Storage.JSONValue,
...key_chain: string[]
) {
this._core.debug.log(
`setSaveableData ${key_chain} => ${JSON.stringify(value)}`
);
if (key_chain.length == 0) throw `key_chain is empty`;
let parentObj = this.getSaveableData(
data,
auto_correct,
...key_chain.slice(0, -1)
);
if (parentObj == null) throw `[HZEngine] saveable data is null`;
if (typeof parentObj !== "object")
throw `[HZEngine] saveable data is not object`;
if (Array.isArray(parentObj)) throw `[HZEngine] saveable data is array`;
parentObj[key_chain[key_chain.length - 1]] = value;
}
checkSaveableData(data: Storage.JSONValue, ...key_chain: string[]) {
return this.getSaveableData(data, false, ...key_chain);
}
// Preload
preload() {
if (!this.cacheRoot) {
throw "cacheRoot is null, please loadProject first";
}
// writeFileSync({path: "data://test.json", data: "awa", options: {encoding: "utf8"}});
if (
this._core.platform.statSync({
path: Path.join(this.cacheRoot, "preloaded.json"),
})
) {
// 已经预加载过了,退出
this.preloadedData = JSON.parse(
this._core.platform.readFileSync({
path: Path.join(this.cacheRoot, "preloaded.json"),
options: {
encoding: "utf8",
},
}) as string
);
return;
}
this.preloadedData = {
script: {
labelMap: {},
hzsInfoMap: {},
},
image: {
nameMap: {},
},
animation: {
profileMap: {},
},
};
this.preloadScript();
this.preloadImage();
this.preloadAnimation();
// console.log(JSON.stringify(this.preloadedData));
this._core.platform.writeFileSync({
path: Path.join(this.cacheRoot, "preloaded.json"),
data: JSON.stringify(this.preloadedData),
});
// console.log(
// `${Path.join(this.projectDir, "preloaded.json")} = ${readFileSync({
// path: Path.join(this.projectDir, "preloaded.json"),
// options:{encoding:"utf8"}
// })}`
// );
}
/**
* 预加载脚本
* 遍历出所有hzs文件和所有label,建立map
*/
preloadScript() {
// TODO
// 记录脚本label的位置
// 这里的index是以0开始计数的行数
let labelMap: Record =
this.preloadedData.script.labelMap;
let scriptDir = Path.join(this.projectRoot!, "script");
let hzsInfoMap: Record =
this.preloadedData.script.hzsInfoMap;
if (!this._core.platform.readdirSync({ path: scriptDir }))
throw "项目文件夹中script文件夹不存在";
/**
* 1. 预加载所有的label并检查冲突
* 2. 记录所有脚本文件的行数
*/
const preloadHzs = (path: string) => {
// let fd = this._core.platform.openSync({ path });
// if (fd < 0) throw "Fd<0";
// let size = this._core.platform.statSync({ path })!.size;
// let arrbuf = new ArrayBuffer(size);
// this._core.platform.readSync({ fd, buffer: arrbuf });
// let buffer = Buffer.from(arrbuf);
// let contentStr = buffer.toString();
let contentStr = this._core.platform.readFileSync({
path,
options: { encoding: "utf8" },
}) as string;
let contentLines = contentStr.split("\n");
let totalLines = contentLines.length;
hzsInfoMap[path] = { totalLines };
for (let i = 0; i < totalLines; ++i) {
let line = contentLines[i].trim();
if (line.startsWith("*")) {
let len = line.length,
p = 1,
q;
while (p < len && line.charAt(p) === " ") ++p;
if (p === len)
throw `Lost Label Name at file(${path}) line(${i + 1})`;
q = p;
while (q < len && line.charAt(q) !== " ") ++q;
// [p, q)
let label = line.slice(p, q);
if (labelMap[label]) {
throw `Label name "${label}" conflict : \
at [${labelMap[label][0]}(line ${labelMap[label][1]})] \
[${path}(line ${i + 1})]`;
}
labelMap[label] = [path, i];
}
}
};
// 遍历所有hzs文件
const traverseScript = (path: string) => {
let dirs = this._core.platform.readdirSync({ path });
// console.log(dirs);
for (let dir of dirs!) {
let subpath = Path.join(path, dir);
if (this._core.platform.isFileSync({ path: subpath })) {
// 是文件
if (dir.endsWith(".hzs")) {
preloadHzs(subpath);
}
} else {
// 是目录
traverseScript(subpath);
}
}
};
traverseScript(scriptDir);
}
/**
* 预加载资源
* 遍历所有png文件,计算对应的name key,建立map
*/
preloadImage() {
let nameMap: Record =
this.preloadedData.image.nameMap;
let imageDir = Path.join(this.projectRoot!, "image");
if (!this._core.platform.readdirSync({ path: imageDir }))
throw "项目文件夹中image文件夹不存在";
/**
* 1. 预加载所有的image并检查冲突
* 2. 记录所有image的name key和路径
*/
function preloadImage(path: string) {
let raw_name = Path.parse(path).name;
let name_key = raw_name
.trim()
.replace("_", " ")
.replace(/ +/, " ")
.toLowerCase();
if (nameMap[name_key])
throw `Image name key conflict [${name_key}], at file [${path}] and [${nameMap[name_key]}]`;
nameMap[name_key] = [path];
}
// 遍历所有hzs文件
const traverseImage = (path: string) => {
let dirs = this._core.platform.readdirSync({ path });
// console.log(dirs);
for (let dir of dirs!) {
let subpath = Path.join(path, dir);
if (this._core.platform.isFileSync({ path: subpath })) {
// 是文件
if (dir.endsWith(".png")) {
preloadImage(subpath);
}
} else {
// 是目录
traverseImage(subpath);
}
}
};
traverseImage(imageDir);
// console.log(
// `Preloaded image: ${JSON.stringify(this.preloadedData.image.nameMap)}`
// );
}
/**
* 預加載動畫profile
* 遍歷animation文件夾下的所有json文件,以文件名為key,json内容為value
*/
preloadAnimation() {
let profileMap: Record =
this.preloadedData.animation.profileMap;
let animationDir = Path.join(this.projectRoot!, "animation");
if (!this._core.platform.readdirSync({ path: animationDir })) {
// throw "项目文件夹中animation文件夹不存在";
console.log("Warning: 项目文件夹中animation文件夹不存在");
return; // allow project without animation folder
}
function preloadAnimation(path: string) {
let raw_name = Path.parse(path).name;
let name_key = raw_name.trim().replace(/ +/, "_");
if (profileMap[name_key])
throw `Animation profile name key conflict [${name_key}], at file [${path}] and [${profileMap[name_key]}]`;
profileMap[name_key] = [path];
}
const traverseAnimation = (path: string) => {
let dirs = this._core.platform.readdirSync({ path });
// console.log(dirs);
for (let dir of dirs!) {
let subpath = Path.join(path, dir);
if (this._core.platform.isFileSync({ path: subpath })) {
// 是文件
if (dir.endsWith(".json")) {
preloadAnimation(subpath);
}
} else {
// 是目录
traverseAnimation(subpath);
}
}
};
traverseAnimation(animationDir);
}
// Decorator Field
// _archiveStateSetterRegisteredList: Set = new Set();
// _archiveStateGetterRegisteredList: Set = new Set();
}
// 记录脚本文件信息
export declare type HzsInfo = {
totalLines: number;
};
export namespace Storage {
export type JSONBaseType = number | string | boolean | null;
export type JSONValue =
| JSONBaseType
| { [key: string]: JSONValue }
| JSONValue[];
export type Saveable =
| {
[P in keyof T]: T[P] extends JSONValue
? T[P]
: T[P] extends NotAssignableToJson
? never
: Saveable;
}
| JSONValue[]
| JSONValue;
type NotAssignableToJson = bigint | symbol | Function;
}