diff --git a/src/api/core/Constants.ts b/src/api/core/Constants.ts index 2102a79..5e3c500 100644 --- a/src/api/core/Constants.ts +++ b/src/api/core/Constants.ts @@ -53,6 +53,47 @@ export interface Simulation { selected_monitoring: string } +// Learning packages ============================================== + +/** + * Inteface to make manipulation of the json file easier + * these are incomplete and do not represent the full structure of the json file + * but contain what is necessary to parse them + */ +export interface VU_MODEL_SETTING_JSON { + type: "json_settings"; + name: string; + splashscreen: string; + model_file_path: string; + experiment_name: string; + minimal_players: string; + maximal_players: string; + selected_monitoring?: 'gama_screen'; +} + +export interface VU_CATALOG_SETTING_JSON { + type: "catalog"; + name: string; + splashscreen?: string; + entries: VU_MODEL_SETTING_JSON[] | VU_CATALOG_SETTING_JSON[]; +} + +// Simplier version used to send useful information only to Monitor clients +export interface MIN_VU_MODEL_SETTING_JSON { + type: string; + name: string; + splashscreen: string; + model_index: number; +} + +// Simplier version used to send useful information only to Monitor clients +export interface MIN_VU_CATALOG_SETTING_JSON { + type: string; + name: string; + splashscreen?: string; + entries: MIN_VU_MODEL_SETTING_JSON[]|MIN_VU_CATALOG_SETTING_JSON; +} + // Internal message exchange ============================================== export interface PlayerState { diff --git a/src/api/monitoring/MonitorServer.ts b/src/api/monitoring/MonitorServer.ts index cd0e0ce..50bd55a 100644 --- a/src/api/monitoring/MonitorServer.ts +++ b/src/api/monitoring/MonitorServer.ts @@ -144,7 +144,7 @@ export class MonitorServer { logger.trace("Sending simulation"); const simulationFromStream = JSON.parse(Buffer.from(message).toString()); - this.controller.model_manager.setActiveModelByFilePath(simulationFromStream.simulation.model_file_path); + this.controller.model_manager.setActiveModelByIndex(simulationFromStream.simulation.model_index); const selectedSimulation: Model = this.controller.model_manager.getActiveModel(); logger.debug("Selected simulation sent to gama: {json}", { json: selectedSimulation.getJsonSettings() }); this.sendMessageByWs({ @@ -245,7 +245,7 @@ export class MonitorServer { * @param clientWsId (optional) WS to send the message to * @return void */ - sendMessageByWs(message: string, clientWsId?: any): void { + sendMessageByWs(message: any, clientWsId?: any): void { if (this.wsClients !== undefined) { this.wsClients.forEach((client) => { if (clientWsId == undefined || clientWsId == client) { diff --git a/src/api/simulation/Model.ts b/src/api/simulation/Model.ts index 7dbb78c..aca63b7 100644 --- a/src/api/simulation/Model.ts +++ b/src/api/simulation/Model.ts @@ -1,30 +1,30 @@ import path from 'path'; -import { JsonSettings } from "../core/Constants.ts"; +import {VU_MODEL_SETTING_JSON} from "../core/Constants.ts"; import { Logger, getLogger } from '@logtape/logtape'; +import fs from "fs"; -const logger: Logger = getLogger(["api", "simulation"]); +const logger: Logger = getLogger(["simulation", "Model"]); class Model { - readonly #jsonSettings: SETTINGS_FILE_JSON; - readonly #modelFilePath: string; + readonly #jsonSettings: VU_MODEL_SETTING_JSON; /** * Creates a Model object based on VU founded by the ModelManager * @param {string} settingsPath - Path to the settings file - * @param {string} jsonSettings - Json content _Stringyfied_ of the settings file + * @param {VU_MODEL_SETTING_JSON} jsonSettings - Json content _Stringyfied_ of the settings file */ - constructor(settingsPath: string, jsonSettings: string) { - this.#jsonSettings = JSON.parse(jsonSettings); + constructor(settingsPath: string, jsonSettings: VU_MODEL_SETTING_JSON) { + this.#jsonSettings = jsonSettings; - logger.debug("Parsing {json}", { json: this.#jsonSettings }); + logger.debug("Parsing new model {json}", { json: this.#jsonSettings.name }); //if the path is relative, we rebuild it using the path of the settings.json it is found in - const modelFilePath = this.#jsonSettings.model_file_path; - if (path.isAbsolute(modelFilePath)) { - this.#modelFilePath = modelFilePath; - } else { - this.#modelFilePath = path.join(path.dirname(settingsPath), this.#jsonSettings.model_file_path); - } + const absoluteModelFilePath = path.isAbsolute(this.#jsonSettings.model_file_path) ? this.#jsonSettings.model_file_path : path.join(path.dirname(settingsPath), this.#jsonSettings.model_file_path); + + if (!fs.existsSync(absoluteModelFilePath)) + logger.error(`GAML model for VU ${this.#jsonSettings.name} can't be found at ${absoluteModelFilePath}. Please check the path in the settings.json file.`) + + this.#jsonSettings.model_file_path = absoluteModelFilePath; } // Getters @@ -34,7 +34,7 @@ class Model { * @returns {string} - The path to the model file */ getModelFilePath(): string { - return this.#modelFilePath; + return this.#jsonSettings.model_file_path; } /** @@ -47,9 +47,9 @@ class Model { /** * Gets the JSON settings - * @returns {SETTINGS_FILE_JSON} - The JSON settings + * @returns {VU_MODEL_SETTING_JSON} - The JSON settings */ - getJsonSettings(): SETTINGS_FILE_JSON { + getJsonSettings(): VU_MODEL_SETTING_JSON { return this.#jsonSettings; } @@ -63,12 +63,12 @@ class Model { return { type: "json_simulation_list", jsonSettings: this.#jsonSettings, - modelFilePath: this.#modelFilePath + modelFilePath: this.#jsonSettings.model_file_path }; } toString() { - return this.#modelFilePath; + return this.#jsonSettings.model_file_path; } diff --git a/src/api/simulation/ModelManager.ts b/src/api/simulation/ModelManager.ts index 7cd0a12..5077ff6 100644 --- a/src/api/simulation/ModelManager.ts +++ b/src/api/simulation/ModelManager.ts @@ -1,27 +1,11 @@ import fs from 'fs'; import path from 'path'; import { isAbsolute } from 'path'; -import { ANSI_COLORS as color } from '../core/Constants.ts'; +import { VU_MODEL_SETTING_JSON, VU_CATALOG_SETTING_JSON, MIN_VU_MODEL_SETTING_JSON, MIN_VU_CATALOG_SETTING_JSON } from '../core/Constants.ts'; import Model from './Model.ts'; import Controller from "../core/Controller.ts"; import {getLogger} from "@logtape/logtape"; -/** - * Inteface to make manipulation of the json file easier - * these are incomplete and do not represent the full structure of the json file - * but contain what is necessary to parse them - */ -interface Settings { - type: "json_settings"; - model_file_path: string; - name: string; -} -interface Catalog { - type: "catalog"; - name: string; - entries: Settings[] | Catalog[]; -} - // Override the log function const logger= getLogger(["simulation", "ModelManager"]); @@ -29,7 +13,7 @@ class ModelManager { controller: Controller; models: Model[]; activeModel: Model | undefined; - jsonList: string[] = []; // List of all the models as written in each settings.json file, useful to keep the structure of subprojects + monitorNestedModels: any[] = []; /** * Creates the model manager @@ -37,21 +21,18 @@ class ModelManager { */ constructor(controller: Controller) { this.controller = controller; - this.models = this.#initModelsList(); + this.models = [] + this.#initModelsList(); } - - /** * Initialize the models list by scanning the learning packages * checks for the type of settings * if it is a single model, it is directly added to the modelList - * if it is a catalog, it will parse it and it's sub objects + * if it is a catalog, it will parse it, and it's sub objects * if it is an array, it will parse the array and create a model for each object, and read catalogs if any - * @returns {Model[]} - List of models */ - #initModelsList(): Model[] { - let modelList: Model[] = []; + #initModelsList(): void { const directoriesWithProjects: string[] = []; directoriesWithProjects.push(isAbsolute(process.env.LEARNING_PACKAGE_PATH!) ? process.env.LEARNING_PACKAGE_PATH! : path.join(process.cwd(), process.env.LEARNING_PACKAGE_PATH!)); @@ -63,7 +44,7 @@ class ModelManager { directoriesWithProjects.forEach((packageRootDir) => { const packageFolder: string[] = ["."].concat(fs.readdirSync(packageRootDir)); - // Browse in learning package folder to find available packages + // Browse in the learning package folder to find available packages packageFolder.forEach((file) => { const folderPath = path.join(packageRootDir, file); @@ -73,59 +54,130 @@ class ModelManager { if (fs.existsSync(settingsPath)) { logger.debug(`Append new package to ModelManager: ${folderPath}`); - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - this.jsonList.push(settings); // add the settings file to the list of json files - - logger.trace(`{jsonList}`, {jsonList: this.jsonList}); - logger.debug(`Found settings file in ${folderPath}`); - - if (settings.type === "catalog") { //it's a catalog, i.e it contains a subset of catalogs and models - logger.debug(`Found catalog in ${folderPath}`); - this.parseCatalog(settings, modelList, settingsPath); - } else if (Array.isArray(settings)) { - logger.debug(`Found array in ${color.cyan}${folderPath}${color.reset},iterating through`); - - for (const item of settings) { - logger.debug(`\titem: ${item.type}`); - this.parseCatalog(item, modelList, settingsPath); - } - - } else if (settings.type === "json_settings") { - logger.debug("{settings}", {settings}); - - modelList = modelList.concat( - new Model(settingsPath, JSON.stringify(settings)) - ); + const settings: VU_MODEL_SETTING_JSON|VU_CATALOG_SETTING_JSON = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + + switch (settings.type) { + //it's a catalog, i.e it contains a subset of catalogs and models + case "catalog": + logger.debug(`Found catalog in ${folderPath}`); + + // Save final recursion in variable + this.monitorNestedModels.push({ + "type": "catalog", + "name": settings.name, + "entries": this.parseCatalog(settings, settingsPath), + ...(settings.splashscreen !== undefined && { "splashscreen": settings.splashscreen }) + }); + + break; + + case "json_settings": + logger.debug(`Found single game settings in ${folderPath}`); + + // Directly save new models not in catalog + this.monitorNestedModels.push( + this.saveNewModel(settingsPath, settings) + ); + + break; + + default: + // TODO: Remove ? + if (Array.isArray(settings)) { + logger.debug(`Found array in ${folderPath},iterating through`); + // @ts-expect-error I don't know what this code is supposed to catch + // Will probably remove it soon... + for (const item of settings) { + logger.debug(`\titem: ${item.type}`); + this.parseCatalog(item, settingsPath); + } + } else { + logger.error(`Can't identify setting's type from ${settingsPath}`); + logger.error(`{settings}`, {settings}); + } } - logger.trace(modelList.toString()); } else { logger.warn(`Couldn't find settings file in folder ${folderPath}`); } } }); - }) + }); + } - return modelList; + /** + * recursively parse a Json catalog passed in parameter + * adds the list of model to a list provided in parameter. + * declared as a separate function to be used recursively + * @param catalog a json catalog object containing catalogs or settings + * @param settingsPath the path of the current settings being parsed, used for creating models in the constructor + */ + parseCatalog(catalog: VU_CATALOG_SETTING_JSON, settingsPath: string) { + const catalogName: string = catalog.name; + + logger.debug(`Start parsing catalog: ${catalogName}`); + logger.trace(`{catalog}`,{catalog}); + + let cleanedEntry: MIN_VU_MODEL_SETTING_JSON[] = []; + let cleanedCatalog: MIN_VU_CATALOG_SETTING_JSON[] = []; + + for (const entry of catalog.entries) { + logger.info(`[${catalogName}] Parsing entry found: {entry}`, {entry: entry.name}); + switch (entry.type) { + case "catalog": + logger.debug(`[${catalogName}] Found catalog, parsing it recursively`) + cleanedCatalog.push({ + "type": "catalog", + "name": entry.name, + // @ts-expect-error Can't properly set what are entries since it can be a list of any MIN_VU + "entries": this.parseCatalog(entry, settingsPath), + ...(entry.splashscreen !== undefined && { "splashscreen": entry.splashscreen }) + }) + break; + // @ts-expect-error If unknown, trying to parse it as a legacy entry... + default: + logger.warn(`[${catalogName}] Unknown type for this entry: {entry}`, {entry}); + logger.warn(`[${catalogName}] Trying to parse it as a legacy entry...`); + case "json_settings": + try { + logger.debug(`[${catalogName}] Parsing json_settings entry`); + + cleanedEntry.push( + this.saveNewModel(settingsPath, entry) + ); + } catch (e) { + logger.error(`[${catalogName}] Couldn't parse catalog entry: {entry}, error: {e}`, {entry, e}); + } + } + } + + return [...cleanedEntry, ...cleanedCatalog]; } // ------------------- - setActiveModelByIndex(index: number) { - this.activeModel = this.models[index]; + saveNewModel(settingsPath: string, settings: VU_MODEL_SETTING_JSON): MIN_VU_MODEL_SETTING_JSON { + logger.debug(`Saving new model: ${settings.name}`); + logger.trace(`{settings}`,{settings}); + + // TODO Check that settings if of type VU_MODEL_SETTING_JSON + const newModel: Model = new Model(settingsPath, settings); + const cleanedJson: VU_MODEL_SETTING_JSON = newModel.getJsonSettings(); + + return { + type: "json_settings", + name: cleanedJson.name, + splashscreen: cleanedJson.splashscreen, + // -1 as `push` return the new array size, not the index + // Add full Model object for GAMA + model_index: (this.models.push(newModel) - 1) + } } // ------------------- - /** - *returns the model with the model_file_path specified - * @filepath the path of the model, specified in the settings.json of the model - * @returns {Model} sets the active model to the model found - */ - setActiveModelByFilePath(filePath: string) { - logger.debug("trying to set active model using file path: {filepath}",{filepath: filePath}) - const modelFound = this.models.find(model => model.getJsonSettings().model_file_path === filePath); - logger.debug("found model: {model}",{model: modelFound?.toString()}) - return this.activeModel = modelFound + setActiveModelByIndex(index: number) { + logger.debug(`Setting active model to index ${index}`); + this.activeModel = this.models[index]; } getActiveModel() { @@ -134,20 +186,12 @@ class ModelManager { // ------------------- - /** - * Converts the model list to JSON format - * @returns {string} - JSON string of models - */ - getModelListJSON(): string { - const jsonSafeModels = this.models.map(model => model.toJSON()); - return JSON.stringify(jsonSafeModels); - } /** * used to send the models structure to the front end for proper display * @returns {string} - JSON string of the list of models as written in each settings.json file */ getCatalogListJSON(): string { - return JSON.stringify(this.jsonList); + return JSON.stringify(this.monitorNestedModels); } /** @@ -157,32 +201,6 @@ class ModelManager { getModelList(): Model[] { return this.models; } - /** - * recursively parse a Json catalog passed in parameter - * adds the list of model to a list provided in parameter. - * declared as a separate function to be used recursively - * @param catalog a json catalog object containing catalogs or settings - * @param list the list of models containing all parsed models throughout all the settings files - * @param settingsPath the path of the current settings being parsed, used for creating models in the constructor - */ - parseCatalog(catalog: Catalog, list: Model[], settingsPath: string) { - for (const entry of catalog.entries) { - if ('type' in entry) { - logger.info(`entry found: ${entry}`) - if (entry.type === "json_settings") { - logger.info(`${entry.name}`) - - const model = new Model(settingsPath, JSON.stringify(entry)); - logger.debug(model.toString()); - list.push(model); - } else if (entry.type === "catalog") { - this.parseCatalog(entry, list, settingsPath); - } - } - } - } } - - export default ModelManager;