0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 05:30:43 -05:00

Add JSONC support to server configs + use by modders (!112)

Replaced calls (where possible) to JSON.parse/stringify with use of `jsonUtil` functions
`VFS.ts` was tricky, it can't be updated as it'd create a circular dependency

Also add json5 to package.json for modders to have access to

Co-authored-by: Dev <dev@dev.sp-tarkov.com>
Reviewed-on: SPT-AKI/Server#112
This commit is contained in:
chomp 2023-08-09 10:49:45 +00:00
parent ba06629577
commit c1a4c544bc
15 changed files with 161 additions and 60 deletions

View File

@ -80,7 +80,7 @@ const packagingBleeding = async () => pkg.exec([entries.bleeding, "--compression
// Assets // Assets
const addAssets = async (cb) => const addAssets = async (cb) =>
{ {
await gulp.src(["assets/**/*.json", "assets/**/*.png", "assets/**/*.ico"]).pipe(gulp.dest(dataDir)); await gulp.src(["assets/**/*.json", "assets/**/*.json5", "assets/**/*.png", "assets/**/*.ico"]).pipe(gulp.dest(dataDir));
await gulp.src([licenseFile]).pipe(rename("LICENSE-Server.txt")).pipe(gulp.dest(buildDir)); await gulp.src([licenseFile]).pipe(rename("LICENSE-Server.txt")).pipe(gulp.dest(buildDir));
// Write dynamic hashed of asset files for the build // Write dynamic hashed of asset files for the build
const hashFileDir = `${dataDir}\\checks.dat`; const hashFileDir = `${dataDir}\\checks.dat`;

View File

@ -65,7 +65,8 @@
"gulp-execa": "5.0.0", "gulp-execa": "5.0.0",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"jest": "29.6.2", "jest": "29.6.2",
"json5": "^2.2.3", "json5": "2.2.3",
"jsonc": "2.0.0",
"madge": "6.1.0", "madge": "6.1.0",
"pkg": "5.8.1", "pkg": "5.8.1",
"pkg-fetch": "3.5.2", "pkg-fetch": "3.5.2",

View File

@ -8,6 +8,7 @@ import { INotifierChannel } from "../models/eft/notifier/INotifier";
import { ISelectProfileRequestData } from "../models/eft/notifier/ISelectProfileRequestData"; import { ISelectProfileRequestData } from "../models/eft/notifier/ISelectProfileRequestData";
import { ISelectProfileResponse } from "../models/eft/notifier/ISelectProfileResponse"; import { ISelectProfileResponse } from "../models/eft/notifier/ISelectProfileResponse";
import { HttpResponseUtil } from "../utils/HttpResponseUtil"; import { HttpResponseUtil } from "../utils/HttpResponseUtil";
import { JsonUtil } from "../utils/JsonUtil";
@injectable() @injectable()
export class NotifierCallbacks export class NotifierCallbacks
@ -15,6 +16,7 @@ export class NotifierCallbacks
constructor( constructor(
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper, @inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, @inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("NotifierController") protected notifierController: NotifierController) @inject("NotifierController") protected notifierController: NotifierController)
{ } { }
@ -35,7 +37,7 @@ export class NotifierCallbacks
* be sent to client as NEWLINE separated strings... yup. * be sent to client as NEWLINE separated strings... yup.
*/ */
this.notifierController.notifyAsync(tmpSessionID) this.notifierController.notifyAsync(tmpSessionID)
.then((messages: any) => messages.map((message: any) => JSON.stringify(message)).join("\n")) .then((messages: any) => messages.map((message: any) => this.jsonUtil.serialize(message)).join("\n"))
.then((text) => this.httpServerHelper.sendTextJson(resp, text)); .then((text) => this.httpServerHelper.sendTextJson(resp, text));
} }

View File

@ -1183,7 +1183,7 @@ export class RepeatableQuestController
protected probabilityObjectArray<K, V>(configArrayInput: ProbabilityObject<K, V>[]): ProbabilityObjectArray<K, V> protected probabilityObjectArray<K, V>(configArrayInput: ProbabilityObject<K, V>[]): ProbabilityObjectArray<K, V>
{ {
const configArray = this.jsonUtil.clone(configArrayInput); const configArray = this.jsonUtil.clone(configArrayInput);
const probabilityArray = new ProbabilityObjectArray<K, V>(this.mathUtil); const probabilityArray = new ProbabilityObjectArray<K, V>(this.mathUtil, this.jsonUtil);
for (const configObject of configArray) for (const configObject of configArray)
{ {
probabilityArray.push(new ProbabilityObject(configObject.key, configObject.relativeProbability, configObject.data)); probabilityArray.push(new ProbabilityObject(configObject.key, configObject.relativeProbability, configObject.data));

View File

@ -22,6 +22,7 @@ import { ConfigServer } from "../servers/ConfigServer";
import { RagfairServer } from "../servers/RagfairServer"; import { RagfairServer } from "../servers/RagfairServer";
import { LocalisationService } from "../services/LocalisationService"; import { LocalisationService } from "../services/LocalisationService";
import { HttpResponseUtil } from "../utils/HttpResponseUtil"; import { HttpResponseUtil } from "../utils/HttpResponseUtil";
import { JsonUtil } from "../utils/JsonUtil";
@injectable() @injectable()
class TradeController class TradeController
@ -35,6 +36,7 @@ class TradeController
@inject("TradeHelper") protected tradeHelper: TradeHelper, @inject("TradeHelper") protected tradeHelper: TradeHelper,
@inject("ItemHelper") protected itemHelper: ItemHelper, @inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("RagfairServer") protected ragfairServer: RagfairServer, @inject("RagfairServer") protected ragfairServer: RagfairServer,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, @inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@ -70,7 +72,7 @@ class TradeController
return this.httpResponse.appendErrorToOutput(output, errorMessage); return this.httpResponse.appendErrorToOutput(output, errorMessage);
} }
this.logger.debug(JSON.stringify(offer, null, 2)); this.logger.debug(this.jsonUtil.serializeAdvanced(offer, null, 2));
const buyData: IProcessBuyTradeRequestData = { const buyData: IProcessBuyTradeRequestData = {
Action: "TradingConfirm", Action: "TradingConfirm",

View File

@ -167,7 +167,7 @@ export class LocationGenerator
protected getWeightedCountOfContainerItems(containerTypeId: string, staticLootDist: Record<string, IStaticLootDetails>, locationName: string): number protected getWeightedCountOfContainerItems(containerTypeId: string, staticLootDist: Record<string, IStaticLootDetails>, locationName: string): number
{ {
// Create probability array to calcualte the total count of lootable items inside container // Create probability array to calcualte the total count of lootable items inside container
const itemCountArray = new ProbabilityObjectArray<number>(this.mathUtil); const itemCountArray = new ProbabilityObjectArray<number>(this.mathUtil, this.jsonUtil);
for (const itemCountDistribution of staticLootDist[containerTypeId].itemcountDistribution) for (const itemCountDistribution of staticLootDist[containerTypeId].itemcountDistribution)
{ {
// Add each count of items into array // Add each count of items into array
@ -191,7 +191,7 @@ export class LocationGenerator
const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled(); const seasonalEventActive = this.seasonalEventService.seasonalEventEnabled();
const seasonalItemTplBlacklist = this.seasonalEventService.getSeasonalEventItemsToBlock(); const seasonalItemTplBlacklist = this.seasonalEventService.getSeasonalEventItemsToBlock();
const itemDistribution = new ProbabilityObjectArray<string>(this.mathUtil); const itemDistribution = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil);
for (const icd of staticLootDist[containerTypeId].itemDistribution) for (const icd of staticLootDist[containerTypeId].itemDistribution)
{ {
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(icd.tpl)) if (!seasonalEventActive && seasonalItemTplBlacklist.includes(icd.tpl))
@ -241,7 +241,7 @@ export class LocationGenerator
) )
); );
const spawnpointArray = new ProbabilityObjectArray<string, Spawnpoint>(this.mathUtil); const spawnpointArray = new ProbabilityObjectArray<string, Spawnpoint>(this.mathUtil, this.jsonUtil);
for (const si of dynamicSpawnPoints) for (const si of dynamicSpawnPoints)
{ {
spawnpointArray.push( spawnpointArray.push(
@ -269,7 +269,7 @@ export class LocationGenerator
const seasonalItemTplBlacklist = this.seasonalEventService.getSeasonalEventItemsToBlock(); const seasonalItemTplBlacklist = this.seasonalEventService.getSeasonalEventItemsToBlock();
for (const spawnPoint of spawnPoints) for (const spawnPoint of spawnPoints)
{ {
const itemArray = new ProbabilityObjectArray<string>(this.mathUtil); const itemArray = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil);
for (const itemDist of spawnPoint.itemDistribution) for (const itemDist of spawnPoint.itemDistribution)
{ {
if (!seasonalEventActive && seasonalItemTplBlacklist.includes(spawnPoint.template.Items.find(x => x._id === itemDist.composedKey.key)._tpl)) if (!seasonalEventActive && seasonalItemTplBlacklist.includes(spawnPoint.template.Items.find(x => x._id === itemDist.composedKey.key)._tpl))
@ -320,7 +320,7 @@ export class LocationGenerator
} }
// Create probability array of all spawn positions for this spawn id // Create probability array of all spawn positions for this spawn id
const spawnpointArray = new ProbabilityObjectArray<string, SpawnpointsForced>(this.mathUtil); const spawnpointArray = new ProbabilityObjectArray<string, SpawnpointsForced>(this.mathUtil, this.jsonUtil);
for (const si of items) for (const si of items)
{ {
// use locationId as template.Id is the same across all items // use locationId as template.Id is the same across all items

View File

@ -954,7 +954,7 @@ class ItemHelper
protected drawAmmoTpl(caliber: string, staticAmmoDist: Record<string, IStaticAmmoDetails[]>): string protected drawAmmoTpl(caliber: string, staticAmmoDist: Record<string, IStaticAmmoDetails[]>): string
{ {
const ammoArray = new ProbabilityObjectArray<string>(this.mathUtil); const ammoArray = new ProbabilityObjectArray<string>(this.mathUtil, this.jsonUtil);
for (const icd of staticAmmoDist[caliber]) for (const icd of staticAmmoDist[caliber])
{ {
ammoArray.push( ammoArray.push(

View File

@ -87,14 +87,16 @@ export class PreAkiModLoader implements IModLoader
if (!this.vfs.exists(this.modOrderPath)) if (!this.vfs.exists(this.modOrderPath))
{ {
this.logger.info(this.localisationService.getText("modloader-mod_order_missing")); this.logger.info(this.localisationService.getText("modloader-mod_order_missing"));
this.vfs.writeFile(this.modOrderPath, JSON.stringify({order: []}, null, 4));
// Write file with empty order array to disk
this.vfs.writeFile(this.modOrderPath, this.jsonUtil.serializeAdvanced({order: []}, null, 4));
} }
else else
{ {
const modOrder = this.vfs.readFile(this.modOrderPath, { encoding: "utf8" }); const modOrder = this.vfs.readFile(this.modOrderPath, { encoding: "utf8" });
try try
{ {
JSON.parse(modOrder).order.forEach((mod: string, index: number) => this.jsonUtil.deserialize<any>(modOrder).order.forEach((mod: string, index: number) =>
{ {
this.order[mod] = index; this.order[mod] = index;
}); });

View File

@ -4,6 +4,7 @@ import { inject, injectable } from "tsyringe";
import { NotifierController } from "../../controllers/NotifierController"; import { NotifierController } from "../../controllers/NotifierController";
import { Serializer } from "../../di/Serializer"; import { Serializer } from "../../di/Serializer";
import { HttpServerHelper } from "../../helpers/HttpServerHelper"; import { HttpServerHelper } from "../../helpers/HttpServerHelper";
import { JsonUtil } from "../../utils/JsonUtil";
@injectable() @injectable()
export class NotifySerializer extends Serializer export class NotifySerializer extends Serializer
@ -11,6 +12,7 @@ export class NotifySerializer extends Serializer
constructor( constructor(
@inject("NotifierController") protected notifierController: NotifierController, @inject("NotifierController") protected notifierController: NotifierController,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper @inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper
) )
{ {
@ -28,7 +30,7 @@ export class NotifySerializer extends Serializer
* be sent to client as NEWLINE separated strings... yup. * be sent to client as NEWLINE separated strings... yup.
*/ */
this.notifierController.notifyAsync(tmpSessionID) this.notifierController.notifyAsync(tmpSessionID)
.then((messages: any) => messages.map((message: any) => JSON.stringify(message)).join("\n")) .then((messages: any) => messages.map((message: any) => this.jsonUtil.serialize(message)).join("\n"))
.then((text) => this.httpServerHelper.sendTextJson(resp, text)); .then((text) => this.httpServerHelper.sendTextJson(resp, text));
} }

View File

@ -2,14 +2,15 @@ import { JsonUtil } from "../utils/JsonUtil";
import { VFS } from "../utils/VFS"; import { VFS } from "../utils/VFS";
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { ILogger } from "../models/spt/utils/ILogger";
import { ConfigTypes } from "../models/enums/ConfigTypes"; import { ConfigTypes } from "../models/enums/ConfigTypes";
import { ICoreConfig } from "../models/spt/config/ICoreConfig"; import { ICoreConfig } from "../models/spt/config/ICoreConfig";
import { ILogger } from "../models/spt/utils/ILogger";
@injectable() @injectable()
export class ConfigServer export class ConfigServer
{ {
protected configs: Record<string, any> = {}; protected configs: Record<string, any> = {};
protected readonly acceptableFileExtensions: string[] = ["json", "jsonc"];
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@ -34,18 +35,20 @@ export class ConfigServer
{ {
this.logger.debug("Importing configs..."); this.logger.debug("Importing configs...");
// get all filepaths // Get all filepaths
const filepath = (globalThis.G_RELEASE_CONFIGURATION) ? "Aki_Data/Server/configs/" : "./assets/configs/"; const filepath = (globalThis.G_RELEASE_CONFIGURATION)
? "Aki_Data/Server/configs/"
: "./assets/configs/";
const files = this.vfs.getFiles(filepath); const files = this.vfs.getFiles(filepath);
// add file content to result // Add file content to result
for (const file of files) for (const file of files)
{ {
if (this.vfs.getFileExtension(file) === "json") if (this.acceptableFileExtensions.includes(this.vfs.getFileExtension(file.toLowerCase())))
{ {
const filename = this.vfs.stripExtension(file); const fileName = this.vfs.stripExtension(file);
const filePathAndName = `${filepath}${file}`; const filePathAndName = `${filepath}${file}`;
this.configs[`aki-${filename}`] = this.jsonUtil.deserializeWithCacheCheck(this.vfs.readFile(filePathAndName), filePathAndName); this.configs[`aki-${fileName}`] = this.jsonUtil.deserializeJsonC<any>(this.vfs.readFile(filePathAndName), filePathAndName);
} }
} }

View File

@ -8,6 +8,7 @@ import { ConfigTypes } from "../models/enums/ConfigTypes";
import { IHttpConfig } from "../models/spt/config/IHttpConfig"; import { IHttpConfig } from "../models/spt/config/IHttpConfig";
import { ILogger } from "../models/spt/utils/ILogger"; import { ILogger } from "../models/spt/utils/ILogger";
import { LocalisationService } from "../services/LocalisationService"; import { LocalisationService } from "../services/LocalisationService";
import { JsonUtil } from "../utils/JsonUtil";
import { RandomUtil } from "../utils/RandomUtil"; import { RandomUtil } from "../utils/RandomUtil";
import { ConfigServer } from "./ConfigServer"; import { ConfigServer } from "./ConfigServer";
@ -20,6 +21,7 @@ export class WebSocketServer
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper @inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper
) )
{ {
@ -56,7 +58,7 @@ export class WebSocketServer
{ {
if (this.isConnectionWebSocket(sessionID)) if (this.isConnectionWebSocket(sessionID))
{ {
this.webSockets[sessionID].send(JSON.stringify(output)); this.webSockets[sessionID].send(this.jsonUtil.serialize(output));
this.logger.debug(this.localisationService.getText("websocket-message_sent")); this.logger.debug(this.localisationService.getText("websocket-message_sent"));
} }
else else
@ -141,7 +143,7 @@ export class WebSocketServer
if (ws.readyState === WebSocket.OPEN) if (ws.readyState === WebSocket.OPEN)
{ {
ws.send(JSON.stringify(this.defaultNotification)); ws.send(this.jsonUtil.serialize(this.defaultNotification));
} }
else else
{ {

View File

@ -126,14 +126,14 @@ export class AkiHttpListener implements IHttpListener
let data: any; let data: any;
try try
{ {
data = JSON.parse(output); data = this.jsonUtil.deserialize(output);
} }
catch (e) catch (e)
{ {
data = output; data = output;
} }
const log = new Response(req.method, data); const log = new Response(req.method, data);
this.requestsLogger.info(`RESPONSE=${JSON.stringify(log)}`); this.requestsLogger.info(`RESPONSE=${this.jsonUtil.serialize(log)}`);
} }
} }
@ -145,10 +145,10 @@ export class AkiHttpListener implements IHttpListener
// Parse quest info into object // Parse quest info into object
const data = (typeof info === "object") const data = (typeof info === "object")
? info ? info
: JSON.parse(info); : this.jsonUtil.deserialize(info);
const log = new Request(req.method, new RequestData(req.url, req.headers, data)); const log = new Request(req.method, new RequestData(req.url, req.headers, data));
this.requestsLogger.info(`REQUEST=${JSON.stringify(log)}`); this.requestsLogger.info(`REQUEST=${this.jsonUtil.serialize(log)}`);
} }
let output = this.httpRouter.getResponse(req, info, sessionID); let output = this.httpRouter.getResponse(req, info, sessionID);

View File

@ -63,7 +63,7 @@ export class DatabaseImporter implements OnLoad
const fileWithPath = `${this.filepath}${file}`; const fileWithPath = `${this.filepath}${file}`;
if (this.vfs.exists(fileWithPath)) if (this.vfs.exists(fileWithPath))
{ {
this.hashedFile = this.jsonUtil.deserialize(this.encodingUtil.fromBase64(this.vfs.readFile(fileWithPath))); this.hashedFile = this.jsonUtil.deserialize(this.encodingUtil.fromBase64(this.vfs.readFile(fileWithPath)), file);
} }
else else
{ {

View File

@ -1,6 +1,7 @@
import fixJson from "json-fixer"; import fixJson from "json-fixer";
import { jsonc } from "jsonc";
import { IParseOptions, IStringifyOptions, Reviver } from "jsonc/lib/interfaces";
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { ILogger } from "../models/spt/utils/ILogger"; import { ILogger } from "../models/spt/utils/ILogger";
import { HashUtil } from "./HashUtil"; import { HashUtil } from "./HashUtil";
import { VFS } from "./VFS"; import { VFS } from "./VFS";
@ -10,6 +11,7 @@ export class JsonUtil
{ {
protected fileHashes = null; protected fileHashes = null;
protected jsonCacheExists = false; protected jsonCacheExists = false;
protected jsonCachePath = "./user/cache/jsonCache.json";
constructor( constructor(
@inject("VFS") protected vfs: VFS, @inject("VFS") protected vfs: VFS,
@ -21,10 +23,10 @@ export class JsonUtil
/** /**
* From object to string * From object to string
* @param data object to turn into JSON * @param data object to turn into JSON
* @param prettify Should output be prettified? * @param prettify Should output be prettified
* @returns string * @returns string
*/ */
public serialize<T>(data: T, prettify = false): string public serialize(data: any, prettify = false): string
{ {
if (prettify) if (prettify)
{ {
@ -36,20 +38,78 @@ export class JsonUtil
} }
} }
/**
* From object to string
* @param data object to turn into JSON
* @param replacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.
* @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
* @returns string
*/
public serializeAdvanced(data: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string
{
return JSON.stringify(data, replacer, space);
}
/**
* From object to string
* @param data object to turn into JSON
* @param filename Name of file being serialized
* @param options Stringify options or a replacer.
* @returns The string converted from the JavaScript value
*/
public serializeJsonC(
data: any,
filename?: string | null,
options?: IStringifyOptions | Reviver): string
{
try
{
return jsonc.stringify(data, options);
}
catch (error)
{
this.logger.error(`unable to stringify jsonC file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
/** /**
* From string to object * From string to object
* @param jsonString json string to turn into object * @param jsonString json string to turn into object
* @param filename Name of file being deserialized
* @returns object * @returns object
*/ */
public deserialize<T>(jsonString: string, filename = ""): T public deserialize<T>(jsonString: string, filename = ""): T
{ {
const { data, changed } = fixJson(`${jsonString}`); try
if (changed)
{ {
this.logger.error(`Invalid JSON ${filename} was detected and automatically fixed, please ensure any edits performed recently are valid, always run your JSON through an online JSON validator prior to starting the server`); return JSON.parse(jsonString);
}
catch (error)
{
this.logger.error(`unable to parse json file: ${filename} message: ${error.message}, stack: ${error.stack}`);
}
}
/**
* From string to object
* @param jsonString json string to turn into object
* @param filename Name of file being deserialized
* @param options Parsing options
* @returns object
*/
public deserializeJsonC<T>(jsonString: string, filename = "", options?: IParseOptions): T
{
try
{
return jsonc.parse(jsonString, options);
}
catch (error)
{
this.logger.error(`unable to parse jsonC file: ${filename} message: ${error.message}, stack: ${error.stack}`);
} }
return data;
} }
public async deserializeWithCacheCheckAsync<T>(jsonString: string, filePath: string): Promise<T> public async deserializeWithCacheCheckAsync<T>(jsonString: string, filePath: string): Promise<T>
@ -60,30 +120,20 @@ export class JsonUtil
}); });
} }
/**
* From json string to object
* @param jsonString String to turn into object
* @param filePath Path to json file being processed
* @returns Object
*/
public deserializeWithCacheCheck<T>(jsonString: string, filePath: string): T public deserializeWithCacheCheck<T>(jsonString: string, filePath: string): T
{ {
// get json cache file and ensure it exists, create if it doesnt this.ensureJsonCacheExists(this.jsonCachePath);
const jsonCachePath = "./user/cache/jsonCache.json"; this.hydrateJsonCache(this.jsonCachePath);
if (!this.jsonCacheExists)
{
if (!this.vfs.exists(jsonCachePath))
{
this.vfs.writeFile(jsonCachePath, "{}");
}
this.jsonCacheExists = true;
}
// Generate hash of string // Generate hash of string
const generatedHash = this.hashUtil.generateSha1ForData(jsonString); const generatedHash = this.hashUtil.generateSha1ForData(jsonString);
// Get all file hashes
if (!this.fileHashes)
{
this.fileHashes = this.deserialize(this.vfs.readFile(`${jsonCachePath}`));
}
// Get hash of file and check if missing or hash mismatch // Get hash of file and check if missing or hash mismatch
let savedHash = this.fileHashes[filePath]; let savedHash = this.fileHashes[filePath];
if (!savedHash || savedHash !== generatedHash) if (!savedHash || savedHash !== generatedHash)
@ -99,7 +149,7 @@ export class JsonUtil
{ {
// data valid, save hash and call function again // data valid, save hash and call function again
this.fileHashes[filePath] = generatedHash; this.fileHashes[filePath] = generatedHash;
this.vfs.writeFile(jsonCachePath, this.serialize(this.fileHashes, true)); this.vfs.writeFile(this.jsonCachePath, this.serialize(this.fileHashes, true));
savedHash = generatedHash; savedHash = generatedHash;
} }
return data as T; return data as T;
@ -119,11 +169,47 @@ export class JsonUtil
} }
// Match! // Match!
return JSON.parse(jsonString) as T; return this.deserialize<T>(jsonString);
} }
public clone<T>(data: T): T
/**
* Create file if nothing found
* @param jsonCachePath path to cache
*/
protected ensureJsonCacheExists(jsonCachePath: string): void
{ {
return JSON.parse(JSON.stringify(data)); if (!this.jsonCacheExists)
{
if (!this.vfs.exists(jsonCachePath))
{
// Create empty object at path
this.vfs.writeFile(jsonCachePath, "{}");
}
this.jsonCacheExists = true;
}
}
/**
* Read contents of json cache and add to class field
* @param jsonCachePath Path to cache
*/
protected hydrateJsonCache(jsonCachePath: string) : void
{
// Get all file hashes
if (!this.fileHashes)
{
this.fileHashes = this.deserialize(this.vfs.readFile(`${jsonCachePath}`));
}
}
/**
* Convert into string and back into object to clone object
* @param objectToClone Item to clone
* @returns Cloned parameter
*/
public clone<T>(objectToClone: T): T
{
return this.deserialize<T>(this.serialize(objectToClone));
} }
} }

View File

@ -23,6 +23,7 @@ export class ProbabilityObjectArray<K, V=undefined> extends Array<ProbabilityObj
{ {
constructor( constructor(
private mathUtil: MathUtil, private mathUtil: MathUtil,
private jsonUtil: JsonUtil,
...items: ProbabilityObject<K, V>[]) ...items: ProbabilityObject<K, V>[])
{ {
super(); super();
@ -31,7 +32,7 @@ export class ProbabilityObjectArray<K, V=undefined> extends Array<ProbabilityObj
filter(callbackfn: (value: ProbabilityObject<K, V>, index: number, array: ProbabilityObject<K, V>[]) => any): ProbabilityObjectArray<K, V> filter(callbackfn: (value: ProbabilityObject<K, V>, index: number, array: ProbabilityObject<K, V>[]) => any): ProbabilityObjectArray<K, V>
{ {
return new ProbabilityObjectArray(this.mathUtil, ...super.filter(callbackfn)); return new ProbabilityObjectArray(this.mathUtil, this.jsonUtil, ...super.filter(callbackfn));
} }
/** /**
@ -53,8 +54,8 @@ export class ProbabilityObjectArray<K, V=undefined> extends Array<ProbabilityObj
*/ */
clone(): ProbabilityObjectArray<K, V> clone(): ProbabilityObjectArray<K, V>
{ {
const clone = JSON.parse(JSON.stringify(this)); const clone = this.jsonUtil.clone(this);
const probabliltyObjects = new ProbabilityObjectArray<K, V>(this.mathUtil); const probabliltyObjects = new ProbabilityObjectArray<K, V>(this.mathUtil, this.jsonUtil);
for (const ci of clone) for (const ci of clone)
{ {
probabliltyObjects.push(new ProbabilityObject(ci.key, ci.relativeProbability, ci.data)); probabliltyObjects.push(new ProbabilityObject(ci.key, ci.relativeProbability, ci.data));