mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-23 16:59:45 +00:00
1275 lines
39 KiB
JavaScript
1275 lines
39 KiB
JavaScript
"use strict";
|
|
|
|
const vm = require("vm");
|
|
const fs = require("fs");
|
|
|
|
const { NEEDED_FILES, BASE_PATH, error, loadCSV } = require("./Common.js");
|
|
const common = require("./Common.js");
|
|
|
|
/**
|
|
* Checks for {@link AssetDefinition.DynamicGroupName}
|
|
* @param {readonly AssetGroupDefinition[]} groupDefinitions
|
|
*/
|
|
function testDynamicGroupName(groupDefinitions) {
|
|
/** @type {[AssetGroupDefinition, AssetDefinition][]} */
|
|
const assetsList = [];
|
|
for (const groupDef of groupDefinitions) {
|
|
for (const asset of groupDef.Asset) {
|
|
if (typeof asset !== "string") {
|
|
assetsList.push([groupDef, asset]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If `DynamicGroupName` is set, check whether the dynamically referenced item actually exists
|
|
for (const [groupDef, assetDef] of assetsList) {
|
|
const DynamicGroupName = (assetDef.DynamicGroupName !== undefined) ? assetDef.DynamicGroupName : groupDef.DynamicGroupName;
|
|
if (
|
|
DynamicGroupName !== undefined
|
|
&& DynamicGroupName !== groupDef.Group
|
|
&& !assetsList.some(([g, a]) => a.Name === assetDef.Name && g.Group === DynamicGroupName)
|
|
) {
|
|
error(`${groupDef.Group}:${assetDef.Name}: Missing DynamicGroupName-referenced item: ${DynamicGroupName}:${assetDef.Name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flatten and yield all combined Asset/Group configs and names
|
|
* @param {ExtendedItemMainConfig} extendedItemConfig
|
|
*/
|
|
function* flattenExtendedConfig(extendedItemConfig) {
|
|
for (const [groupName, groupConfig] of Object.entries(extendedItemConfig)) {
|
|
for (const [assetName, assetConfig] of Object.entries(groupConfig)) {
|
|
yield { groupName, assetName, groupConfig, assetConfig };
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks for the option names of typed items
|
|
* @param {ExtendedItemMainConfig} extendedItemConfig
|
|
* @param {string[][]} dialogArray
|
|
*/
|
|
function testExtendedItemDialog(extendedItemConfig, dialogArray) {
|
|
const dialogSet = new Set(dialogArray.map(i => i[0]));
|
|
for (const { groupName, assetName, assetConfig } of flattenExtendedConfig(extendedItemConfig)) {
|
|
// Skip if dialog keys if they are set via CopyConfig;
|
|
// they will be checked when validating the parent item
|
|
if (assetConfig.CopyConfig && !assetConfig?.DialogPrefix) {
|
|
continue;
|
|
}
|
|
|
|
/** @type {Set<string>} */
|
|
let missingDialog = new Set();
|
|
switch (assetConfig.Archetype) {
|
|
case "typed":
|
|
missingDialog = testTypedItemDialog(groupName, assetName, assetConfig, dialogSet);
|
|
break;
|
|
case "modular":
|
|
missingDialog = testModularItemDialog(groupName, assetName, assetConfig, dialogSet);
|
|
break;
|
|
}
|
|
|
|
if (missingDialog.size !== 0) {
|
|
const missingString = Array.from(missingDialog).sort();
|
|
error(`${groupName}:${assetName}: found ${missingDialog.size} missing dialog keys: ${missingString}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct all prefix/suffix combinations and add them to `diffSet` if they are not part of `referenceSet`.
|
|
* Performs an inplace update of `diffSet`.
|
|
* @param {readonly (undefined | string)[]} prefixIter
|
|
* @param {readonly (undefined | string)[]} suffixIter
|
|
* @param {Readonly<Set<string>>} referenceSet
|
|
* @param {Set<string>} diffSet
|
|
*/
|
|
function gatherDifference(prefixIter, suffixIter, referenceSet, diffSet) {
|
|
for (const prefix of prefixIter) {
|
|
if (prefix === undefined) {
|
|
continue;
|
|
}
|
|
for (const suffix of suffixIter) {
|
|
if (suffix === undefined) {
|
|
continue;
|
|
}
|
|
const key =`${prefix}${suffix}`;
|
|
if (!referenceSet.has(key)) {
|
|
diffSet.add(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Version of {@link gatherDifference} designed for handling the `fromTo` typed item chat setting.
|
|
* @param {readonly (undefined | string)[]} prefixIter
|
|
* @param {readonly (undefined | string)[]} suffixIter
|
|
* @param {Readonly<Set<string>>} referenceSet
|
|
* @param {Set<string>} diffSet
|
|
*/
|
|
function gatherDifferenceFromTo(prefixIter, suffixIter, referenceSet, diffSet) {
|
|
for (const prefix of prefixIter) {
|
|
if (prefix === undefined) {
|
|
continue;
|
|
}
|
|
for (const suffix1 of suffixIter) {
|
|
for (const suffix2 of suffixIter) {
|
|
if (suffix1 === suffix2 || suffix1 === undefined || suffix2 === undefined) {
|
|
continue;
|
|
}
|
|
const key =`${prefix}${suffix1}To${suffix2}`;
|
|
if (!referenceSet.has(key)) {
|
|
diffSet.add(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a set of all expected typed item dialog keys that are absent for a given iten.
|
|
* Helper function for {@link testExtendedItemDialog}.
|
|
* @param {string} groupName
|
|
* @param {string} assetName
|
|
* @param {TypedItemConfig} config
|
|
* @param {Readonly<Set<string>>} dialogSet
|
|
* @returns {Set<string>}
|
|
*/
|
|
function testTypedItemDialog(groupName, assetName, config, dialogSet) {
|
|
const chatSetting = config.ChatSetting ?? "default";
|
|
/** @type {Partial<TypedItemConfig["DialogPrefix"]>} */
|
|
const dialogConfig = {
|
|
Header: `${groupName}${assetName}Select`,
|
|
Option: `${groupName}${assetName}`,
|
|
Chat: `${groupName}${assetName}Set`,
|
|
...(config.DialogPrefix ?? {}),
|
|
};
|
|
if (
|
|
typeof dialogConfig.Chat === "function" // Can't validate callables via the CI
|
|
|| chatSetting === "silent" // No dialog when silent
|
|
|| !groupName.includes("Item") // Type changes of clothing based items never have a chat message
|
|
) {
|
|
dialogConfig.Chat = undefined;
|
|
}
|
|
|
|
/** @type {Set<string>} */
|
|
const ret = new Set();
|
|
const optionNames = config.Options?.map(o => !o.HasSubscreen ? o.Name : undefined) ?? [];
|
|
gatherDifference([dialogConfig.Option], optionNames, dialogSet, ret);
|
|
gatherDifference([dialogConfig.Header], [""], dialogSet, ret);
|
|
if (chatSetting === "default") {
|
|
gatherDifference([dialogConfig.Chat], optionNames, dialogSet, ret);
|
|
} else if (chatSetting === "fromTo") {
|
|
gatherDifferenceFromTo([dialogConfig.Chat], optionNames, dialogSet, ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Return a set of all expected modular item dialog keys that are absent for a given iten.
|
|
* Helper function for {@link testExtendedItemDialog}.
|
|
* @param {string} groupName
|
|
* @param {string} assetName
|
|
* @param {ModularItemConfig} config
|
|
* @param {Readonly<Set<string>>} dialogSet
|
|
* @returns {Set<string>}
|
|
*/
|
|
function testModularItemDialog(groupName, assetName, config, dialogSet) {
|
|
const chatSetting = config.ChatSetting ?? "default";
|
|
/** @type {Partial<ModularItemConfig["DialogPrefix"]>} */
|
|
const dialogConfig = {
|
|
Header: `${groupName}${assetName}Select`,
|
|
Module: `${groupName}${assetName}Module`,
|
|
Option: `${groupName}${assetName}Option`,
|
|
Chat: `${groupName}${assetName}Set`,
|
|
...(config.DialogPrefix ?? {}),
|
|
};
|
|
if (typeof dialogConfig.Chat === "function" || !groupName.includes("Item")) {
|
|
dialogConfig.Chat = undefined;
|
|
}
|
|
|
|
const modulesNames = config.Modules?.map(m => m.Name) ?? [];
|
|
/** @type {(string | undefined)[]} */
|
|
const optionNames = [];
|
|
for (const module of config.Modules ?? []) {
|
|
optionNames.push(...(module.Options.map((o, i) => !o.HasSubscreen ? `${module.Key}${i}` : undefined) ?? []));
|
|
}
|
|
|
|
/** @type {Set<string>} */
|
|
const ret = new Set();
|
|
gatherDifference([dialogConfig.Header, dialogConfig.Module], modulesNames, dialogSet, ret);
|
|
gatherDifference([dialogConfig.Option], optionNames, dialogSet, ret);
|
|
if (chatSetting === "default") {
|
|
gatherDifference([dialogConfig.Chat], optionNames, dialogSet, ret);
|
|
} else if (chatSetting === "perModule") {
|
|
// Ignore a module if every single one of its options links to a subscreen
|
|
const modulesNamesNoSubscreen = config.Modules?.map(m => m.Options.every(o => o.HasSubscreen) ? undefined : m.Name) ?? [];
|
|
gatherDifference([dialogConfig.Chat], modulesNamesNoSubscreen, dialogSet, ret);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Check that all expected color-group entries are present in the .csv file
|
|
* @param {TestingStruct<string>[]} missingGroups A list of all missing color groups
|
|
*/
|
|
function testColorGroups(missingGroups) {
|
|
if (!Array.isArray(missingGroups)) {
|
|
error("MISSING_COLOR_GROUPS not found");
|
|
}
|
|
for (const { Group, Name, Invalid } of missingGroups) {
|
|
error(`${Group}:${Name}: Missing color group "${Invalid}"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that all expected color-layer entries are present in the .csv file
|
|
* @param {TestingStruct<string>[]} missingLayers A list of all missing color layers
|
|
*/
|
|
function testColorLayers(missingLayers) {
|
|
if (!Array.isArray(missingLayers)) {
|
|
error("MISSING_COLOR_LAYERS not found");
|
|
return;
|
|
}
|
|
for (const { Group, Name, Invalid } of missingLayers) {
|
|
error(`${Group}:${Name}: Missing color layer "${Invalid}"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test whether all asset default colors are valid.
|
|
* @param {TestingStruct<string[]>[]} invalidDefaults A list of all missing color layers
|
|
*/
|
|
function testDefaultColor(invalidDefaults) {
|
|
if (!Array.isArray(invalidDefaults)) {
|
|
error("TestingInvalidDefaultColor not found");
|
|
return;
|
|
}
|
|
for (const { Group, Name, Invalid } of invalidDefaults) {
|
|
error(`${Group}:${Name}: ${Invalid.length} invalid color defaults "${Invalid}"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gather all duplicate module/option names from the passed module/option list
|
|
* @param {readonly { Name: string }[]} options
|
|
* @returns {string[]}
|
|
*/
|
|
function gatherDuplicateOptionNames(options) {
|
|
/** @type {Record<string, number>} */
|
|
const nameCount = {};
|
|
for (const option of options) {
|
|
if (option.Name in nameCount) {
|
|
nameCount[option.Name] += 1;
|
|
} else {
|
|
nameCount[option.Name] = 1;
|
|
}
|
|
}
|
|
|
|
/** @type {string[]} */
|
|
const duplicates = [];
|
|
for (const [name, count] of Object.entries(nameCount)) {
|
|
if (count > 1) {
|
|
duplicates.push(name);
|
|
}
|
|
}
|
|
return duplicates;
|
|
}
|
|
|
|
/**
|
|
* Check whether all extended item options and modules are (at least) of length 1.
|
|
* @param {ExtendedItemMainConfig} config
|
|
*/
|
|
function testModuleOptionLength(config) {
|
|
for (const { groupName, assetName, assetConfig } of flattenExtendedConfig(config)) {
|
|
switch (assetConfig.Archetype) {
|
|
case "typed": {
|
|
testModuleOptionLengthTyped(groupName, assetName, assetConfig);
|
|
break;
|
|
}
|
|
case "modular": {
|
|
testModuleOptionLengthModular(groupName, assetName, assetConfig);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} groupName
|
|
* @param {string} assetName
|
|
* @param {TypedItemConfig} config
|
|
*/
|
|
function testModuleOptionLengthTyped(groupName, assetName, config) {
|
|
if (config.CopyConfig && !config?.Options) {
|
|
return;
|
|
}
|
|
|
|
const options = config.Options ?? [];
|
|
if (options.length === 0) {
|
|
error(`${groupName}:${assetName}: typed item require at least one option`);
|
|
}
|
|
|
|
const duplicateNames = gatherDuplicateOptionNames(options);
|
|
if (duplicateNames.length) {
|
|
error(`${groupName}:${assetName}: found ${duplicateNames.length} typed item options with a duplicate Name: ${duplicateNames}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} groupName
|
|
* @param {string} assetName
|
|
* @param {ModularItemConfig} config
|
|
*/
|
|
function testModuleOptionLengthModular(groupName, assetName, config) {
|
|
if (config.CopyConfig && !config?.Modules) {
|
|
return;
|
|
}
|
|
|
|
const modules = config.Modules ?? [];
|
|
if (modules.length === 0) {
|
|
error(`${groupName}:${assetName}: modular item requires at least one option`);
|
|
}
|
|
|
|
for (const mod of modules) {
|
|
if (mod.Options.length === 0) {
|
|
error(`${groupName}:${assetName}: modular item module "${mod}" requires at least one option`);
|
|
}
|
|
}
|
|
|
|
const duplicateNames = gatherDuplicateOptionNames(modules);
|
|
if (duplicateNames.length) {
|
|
error(`${groupName}:${assetName}: found ${duplicateNames.length} modular item modules with a duplicate Name: ${duplicateNames}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the intersection between the passed sets
|
|
* @param {Set<string>[]} args
|
|
* @returns {string[]}
|
|
*/
|
|
function getIntersection(...args) {
|
|
if (args.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const intersection = new Set();
|
|
const elementsVisited = new Set(args[0]);
|
|
for (const set of args.slice(1)) {
|
|
for (const i of set) {
|
|
if (elementsVisited.has(i)) {
|
|
intersection.add(i);
|
|
} else {
|
|
elementsVisited.add(i);
|
|
}
|
|
}
|
|
}
|
|
return Array.from(intersection).sort();
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, ModularItemData>} dataRecord
|
|
*/
|
|
function testModularItemPropertyDisjoint(dataRecord) {
|
|
if (dataRecord === null || typeof dataRecord !== "object") {
|
|
error("ModularItemDataLookup not found");
|
|
return;
|
|
}
|
|
|
|
// Non-array item properties with special casing for dealing with inter-module intersections
|
|
const propertyWhitelist = new Set(["Difficulty", "OverrideHeight"]);
|
|
const assetWhitelist = new Set([
|
|
// `Intensity` intersection explicitly handled by a `...SetOption` script hook
|
|
"ItemVulva:ClitAndDildoVibratorbelt",
|
|
]);
|
|
|
|
for (const data of Object.values(dataRecord)) {
|
|
if (assetWhitelist.has(`${data.asset.Group.Name}:${data.asset.Name}`)) {
|
|
continue;
|
|
}
|
|
|
|
/** @type {Record<string, Set<string>>} */
|
|
const properties = {};
|
|
for (const mod of data.modules) {
|
|
properties[mod.Key] = new Set();
|
|
for (const option of mod.Options) {
|
|
for (const [k, v] of Object.entries(option.Property || {})) {
|
|
if (!Array.isArray(v) && !propertyWhitelist.has(k)) {
|
|
properties[mod.Key].add(k);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const intersection = getIntersection(...Object.values(properties));
|
|
if (intersection.length > 0) {
|
|
error(
|
|
`${data.asset.Group.Name}:${data.asset.Name}: found ${intersection.length} intersecting `
|
|
+ `cross-module item properties: ${intersection}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @param {Set<string>} pathSet
|
|
* @param {(file: string) => boolean} filter
|
|
*/
|
|
function recursivelyListDir(dir, pathSet, filter) {
|
|
fs.readdirSync(dir).forEach(f => {
|
|
let dirPath = `${dir}/${f}`;
|
|
const isDirectory = fs.statSync(dirPath).isDirectory();
|
|
if (isDirectory) {
|
|
recursivelyListDir(dirPath, pathSet, filter);
|
|
} else if (filter(f)) {
|
|
const offset = BASE_PATH.length;
|
|
pathSet.add(`${dir.slice(offset)}/${f}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Record<ExtendedArchetype, Record<string, AssetArchetypeData>>} dataSuperRecord
|
|
*/
|
|
function testExtendedItemButtonImage(dataSuperRecord) {
|
|
// Gather all expected image paths
|
|
/** @type {Set<string>} */
|
|
const requiredImages = new Set();
|
|
for (const [archetype, dataRecord] of Object.entries(dataSuperRecord)) {
|
|
if (dataRecord === null || typeof dataRecord !== "object") {
|
|
error(`${archetype} data lookup not found`);
|
|
continue;
|
|
}
|
|
for (const data of Object.values(dataRecord)) {
|
|
if (data.archetype === "modular") {
|
|
// Check the modules' drawdata as well
|
|
for (const mod of data.modules) {
|
|
for (const buttonData of mod.drawData.elementData) {
|
|
if (buttonData.imagePath != null) {
|
|
requiredImages.add(buttonData.imagePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const buttonData of data.drawData.elementData) {
|
|
if ("imagePath" in buttonData && buttonData.imagePath != null) {
|
|
requiredImages.add(buttonData.imagePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Identify all missing and extra images
|
|
/** @type {Set<string>} */
|
|
const availableImages = new Set();
|
|
recursivelyListDir(`${BASE_PATH}Screens/Inventory`, availableImages, i => i.endsWith(".png"));
|
|
for (const imagePath of requiredImages) {
|
|
if (!availableImages.has(imagePath)) {
|
|
error(`Missing extended item button image ${imagePath}`);
|
|
}
|
|
}
|
|
for (const imagePath of availableImages) {
|
|
if (!requiredImages.has(imagePath)) {
|
|
error(`Unexpected extra extended item button image ${imagePath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {readonly Asset[]} assets
|
|
*/
|
|
function testAssetLayerNames(assets) {
|
|
if (assets === null || !Array.isArray(assets)) {
|
|
error("Asset not found");
|
|
return;
|
|
}
|
|
|
|
for (const asset of assets) {
|
|
/** @type {Record<string, number>} */
|
|
const layerCount = {};
|
|
for (const layer of asset.Layer) {
|
|
layerCount[layer.Name] = 1 + (layerCount[layer.Name] || 0);
|
|
}
|
|
|
|
const duplicateLayers = Object.entries(layerCount).filter(([_, j]) => j > 1).map(([i, _]) => i);
|
|
if (duplicateLayers.length > 0) {
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: found ${duplicateLayers.length} layers `
|
|
+ `with duplicate names: ${duplicateLayers}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test whether a subset of asset and extended item properties are disjoint.
|
|
* The subset concerns those properties that BC always checks at both the asset and item level.
|
|
* @param {Record<ExtendedArchetype, Record<string, AssetArchetypeData>>} dataSuperRecord
|
|
*/
|
|
function testAssetAndExtendedPropertyIsDisjoint(dataSuperRecord) {
|
|
/**
|
|
* All array-like properties that BC checks at both the asset and item level.
|
|
* @satisfies {readonly (keyof ItemProperties)[]}
|
|
*/
|
|
const propNames = /** @type {const} */([
|
|
"Effect",
|
|
"Hide",
|
|
"HideItem",
|
|
"HideItemExclude",
|
|
"Block",
|
|
"AllowActivityOn",
|
|
"Fetish",
|
|
]);
|
|
|
|
// Gather all expected image paths
|
|
for (const [archetype, dataRecord] of Object.entries(dataSuperRecord)) {
|
|
if (dataRecord === null || typeof dataRecord !== "object") {
|
|
error(`${archetype} data lookup not found`);
|
|
continue;
|
|
}
|
|
|
|
for (const data of Object.values(dataRecord)) {
|
|
/** @type {ExtendedItemOption[]} */
|
|
const options = [];
|
|
if (data.archetype === "modular") {
|
|
// Check the modules' drawdata as well
|
|
for (const mod of data.modules) {
|
|
options.push(...mod.Options);
|
|
}
|
|
} else {
|
|
if ("options" in data && Array.isArray(data.options)) {
|
|
options.push(...data.options);
|
|
}
|
|
}
|
|
|
|
const asset = data.asset;
|
|
for (const propName of propNames) {
|
|
if (propName === "Effect") {
|
|
testAssetAndExtendedEffectIsDisjoint(options, asset);
|
|
}
|
|
|
|
/** @type {Set<string>} */
|
|
const propSet = new Set();
|
|
for (const option of options) {
|
|
/** @type {string[]} */
|
|
const propArray = (option.Property && option.Property[propName]) || [];
|
|
for (const i of propArray) {
|
|
propSet.add(i);
|
|
}
|
|
}
|
|
|
|
/** @type {string[]} */
|
|
const intersection = (asset[propName] || []).filter(i => propSet.has(i)).sort();
|
|
if (intersection.length > 0) {
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: found ${intersection.length} intersection(s) `
|
|
+ `between asset and extended item option "${propName}" properties: ${intersection}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @type {Set<GagEffectName>} */
|
|
const GAG_EFFECTS = new Set([
|
|
"GagVeryLight",
|
|
"GagEasy",
|
|
"GagLight",
|
|
"GagNormal",
|
|
"GagMedium",
|
|
"GagHeavy",
|
|
"GagVeryHeavy",
|
|
"GagTotal",
|
|
"GagTotal2",
|
|
"GagTotal3",
|
|
"GagTotal4",
|
|
]);
|
|
|
|
/** @type {Set<BlindEffectName>} */
|
|
const BLIND_EFFECTS = new Set([
|
|
"BlindLight",
|
|
"BlindNormal",
|
|
"BlindHeavy",
|
|
"BlindTotal",
|
|
]);
|
|
|
|
|
|
/** @type {Set<BlurEffectName>} */
|
|
const BLUR_EFFECTS = new Set([
|
|
"BlurLight",
|
|
"BlurNormal",
|
|
"BlurHeavy",
|
|
"BlurTotal",
|
|
]);
|
|
|
|
|
|
/** @type {Set<DeafEffectName>} */
|
|
const DEAF_EFFECTS = new Set([
|
|
"DeafLight",
|
|
"DeafNormal",
|
|
"DeafHeavy",
|
|
"DeafTotal",
|
|
]);
|
|
|
|
/**
|
|
* A {@link Set.has} variant for string-based sets that returns a typeguard.
|
|
* @template {string} T
|
|
* @param {Set<T>} set
|
|
* @param {string} value
|
|
* @returns {value is T}
|
|
*/
|
|
function setHas(set, value) {
|
|
return set.has(/** @type {T} */(value));
|
|
}
|
|
|
|
/**
|
|
* A helper for {@link testAssetAndExtendedPropertyIsDisjoint} that performs a stricter check for `Effect` disjointment.
|
|
* More specifically, this checks whether only a single effect (at most) of each of the following effect categories is present:
|
|
*
|
|
* * Gag level
|
|
* * Blindness level
|
|
* * Deafness level
|
|
* * Blur level
|
|
* @param {readonly ExtendedItemOption[]} options
|
|
* @param {Asset} asset
|
|
*/
|
|
function testAssetAndExtendedEffectIsDisjoint(options, asset) {
|
|
/** @type {Set<string>} */
|
|
const propSet = new Set();
|
|
for (const option of options) {
|
|
const propArray = (option.Property && option.Property.Effect) || [];
|
|
for (const i of propArray) {
|
|
if (setHas(GAG_EFFECTS, i)) {
|
|
propSet.add("Gag level");
|
|
}
|
|
if (setHas(BLIND_EFFECTS, i)) {
|
|
propSet.add("Blind level");
|
|
}
|
|
if (setHas(BLUR_EFFECTS, i)) {
|
|
propSet.add("Blur level");
|
|
}
|
|
if (setHas(DEAF_EFFECTS, i)) {
|
|
propSet.add("Deaf level");
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @type {string[]} */
|
|
const intersection = [];
|
|
for (const i of asset.Effect) {
|
|
if (setHas(GAG_EFFECTS, i) && propSet.has("Gag level")) {
|
|
intersection.push("Gag level");
|
|
}
|
|
if (setHas(DEAF_EFFECTS, i) && propSet.has("Blind level")) {
|
|
intersection.push("Blind level");
|
|
}
|
|
if (setHas(BLUR_EFFECTS, i) && propSet.has("Blur level")) {
|
|
intersection.push("Blur level");
|
|
}
|
|
if (setHas(DEAF_EFFECTS, i) && propSet.has("Deaf level")) {
|
|
intersection.push("Deaf level");
|
|
}
|
|
}
|
|
if (intersection.length > 0) {
|
|
intersection.sort();
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: found ${intersection.length} intersection(s) `
|
|
+ `between asset and extended item option "Effect" properties categories: ${intersection}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, TypedItemData | ModularItemData>} dataRecord
|
|
*/
|
|
function testAllowTypes(dataRecord) {
|
|
for (const data of Object.values(dataRecord)) {
|
|
const asset = data.asset;
|
|
const allowType = /** @type {readonly string[]} */(asset.AllowTypes);
|
|
for (const layer of asset.Layer) {
|
|
if (!layer.AllowTypes) {
|
|
continue;
|
|
}
|
|
const invalid = layer.AllowTypes.filter(t => t == "" ? false : !allowType.includes(t));
|
|
if (invalid.length > 0) {
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: found ${invalid.length} invalid types `
|
|
+ `in the AllowTypes array of layer ${layer.Name}: ${invalid}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, ModularItemData>} dataRecord
|
|
*/
|
|
function testAllowModuleTypes(dataRecord) {
|
|
for (const data of Object.values(dataRecord)) {
|
|
const asset = data.asset;
|
|
const optionNames = data.modules.map(m => m.Options.map(o => o.Name)).flat();
|
|
for (const layer of asset.Layer) {
|
|
if (!layer.AllowModuleTypes) {
|
|
continue;
|
|
}
|
|
const invalid = layer.AllowModuleTypes.filter(t => {
|
|
for (const subType of (t.match(/([a-zA-Z]+\d+)/g) || [])) {
|
|
if (!optionNames.includes(subType)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
if (invalid.length > 0) {
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: found ${invalid.length} invalid (sub-)types `
|
|
+ `in the AllowModuleTypes array of layer ${layer.Name}: ${invalid}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {readonly Asset[]} assets
|
|
*/
|
|
function testCopyLayerColor(assets) {
|
|
for (const asset of assets) {
|
|
const layerNames = asset.Layer.map(l => l.Name);
|
|
for (const layer of asset.Layer) {
|
|
if (layer.CopyLayerColor == null) {
|
|
continue;
|
|
}
|
|
if (!layerNames.includes(layer.CopyLayerColor)) {
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: found an invalid `
|
|
+ `CopyLayerColor value in layer ${layer.Name}: ${layer.CopyLayerColor}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate whether all extended item prerequisites of the default option are a subset of the asset-level prerequisites.
|
|
* @param {Record<ExtendedArchetype, Record<string, AssetArchetypeData>>} dataSuperRecord
|
|
*/
|
|
function testExtendedPrerequisite(dataSuperRecord) {
|
|
for (const dataRecord of Object.values(dataSuperRecord)) {
|
|
for (const data of Object.values(dataRecord)) {
|
|
const asset = data.asset;
|
|
|
|
/** @type {AssetPrerequisite[]} */
|
|
const extendedPrereq = [];
|
|
switch (data.archetype) {
|
|
case "typed":
|
|
case "vibrating": {
|
|
const prereq = data.options[0].Prerequisite;
|
|
if (Array.isArray(prereq)) {
|
|
extendedPrereq.push(...prereq);
|
|
} else if (typeof prereq === "string") {
|
|
extendedPrereq.push(prereq);
|
|
}
|
|
break;
|
|
}
|
|
case "modular": {
|
|
for (const mod of data.modules) {
|
|
const prereq = mod.Options[0].Prerequisite;
|
|
if (Array.isArray(prereq)) {
|
|
extendedPrereq.push(...prereq);
|
|
} else if (typeof prereq === "string") {
|
|
extendedPrereq.push(prereq);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (extendedPrereq.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
/** @type {AssetPrerequisite[]} */
|
|
const invalidSuperset = [];
|
|
for (const prereq of extendedPrereq) {
|
|
if (!asset.Prerequisite.includes(prereq)) {
|
|
invalidSuperset.push(prereq);
|
|
}
|
|
}
|
|
|
|
if (invalidSuperset.length > 0) {
|
|
invalidSuperset.sort();
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: default extended item prerequisites must `
|
|
+ `be a subset of asset-level prerequisites; offending members: ${invalidSuperset}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a flattened list of pose names to a record mapping pose categories to their respective names.
|
|
* @param {readonly AssetPoseName[]} poses - A list of to-be mapped poses
|
|
* @param {Record<AssetPoseCategory, Set<AssetPoseName>>} referencePoseMap - A record containing all pose categories and all their respective members
|
|
* @returns {Partial<Record<AssetPoseCategory, AssetPoseName[]>>} - A record containing all the subset of pose categories and members member names as specified in `poses`
|
|
*/
|
|
function poseToPoseMap(poses, referencePoseMap) {
|
|
/** @type {Partial<Record<AssetPoseCategory, AssetPoseName[]>>} */
|
|
const poseMap = {};
|
|
for (const pose of poses) {
|
|
for (const [_category, poseSet] of Object.entries(referencePoseMap)) {
|
|
const category = /** @type {AssetPoseCategory} */(_category);
|
|
if (poseSet.has(pose)) {
|
|
poseMap[category] = [...(poseMap[category] || []), pose];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return poseMap;
|
|
}
|
|
|
|
/**
|
|
* Test that `AllowActivePose` is, on pose category by category basis, a superset of `PoseMapping` key.
|
|
* @param {readonly Asset[]} assets
|
|
* @param {Record<AssetPoseCategory, Set<AssetPoseName>>} poseMap
|
|
*/
|
|
function testActivePose(assets, poseMap) {
|
|
for (const asset of assets) {
|
|
// TODO: For archetypical items we have to check against a `AllowActivePose` union involving all extended item option allowed poses
|
|
if (!asset.AllowActivePose || asset.Archetype) {
|
|
continue;
|
|
}
|
|
|
|
const allowPoseMap = poseToPoseMap(/** @type {AssetPoseName[]} */(Object.keys(asset.PoseMapping)), poseMap);
|
|
const allowActivePoseMap = poseToPoseMap(asset.AllowActivePose ?? [], poseMap);
|
|
|
|
for (const [_category, allowActivePoseList] of Object.entries(allowActivePoseMap)) {
|
|
const category = /** @type {AssetPoseCategory} */(_category);
|
|
const allowPoseList = allowPoseMap[category];
|
|
if (!allowPoseList) {
|
|
continue;
|
|
}
|
|
|
|
const invalidSuperset = allowPoseList.filter(p => !allowActivePoseList.includes(p));
|
|
if (invalidSuperset.length > 0) {
|
|
invalidSuperset.sort();
|
|
error(
|
|
`${asset.Group.Name}:${asset.Name}: PoseMapping keys must be a subset of AllowActivePose for a given pose category; `
|
|
+ `found ${invalidSuperset.length} offending members in category "${category}": ${invalidSuperset}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that all assets have a preview image
|
|
* @param {readonly Asset[]} assets
|
|
*/
|
|
function testHasPreview(assets) {
|
|
for (const asset of assets) {
|
|
const path = `${BASE_PATH}Assets/${asset.Group.Family}/${asset.DynamicGroupName}/Preview/${asset.Name}.png`;
|
|
if (asset.Visible && asset.Group.AllowNone && !fs.existsSync(path)) {
|
|
error(`${asset.Group.Name}:${asset.Name}: Missing preview image at "${path}"`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that all assets have the expected .png images.
|
|
* @param {readonly Asset[]} assets
|
|
* @param {{ typed: Record<string, TypedItemData>, modular: Record<string, ModularItemData> }} extendedDataRecords
|
|
*/
|
|
function testAssetHasPNG(assets, extendedDataRecords) {
|
|
const assetGroups = /** @type {Record<AssetGroupName, AssetGroup>} */({});
|
|
const assetGroupMap = /** @type {Record<AssetGroupName, { F: string[], M: string[], FM: string[] }>} */({});
|
|
for (const asset of assets) {
|
|
const gender = asset.Gender ?? "FM";
|
|
if (!(asset.Group.Name in assetGroupMap)) {
|
|
assetGroupMap[asset.Group.Name] = { "F": [], "M": [], "FM": [] };
|
|
}
|
|
assetGroupMap[asset.Group.Name][gender].push(asset.Name);
|
|
assetGroups[asset.Group.Name] = asset.Group;
|
|
}
|
|
|
|
for (const asset of assets) {
|
|
if (!asset.Visible) {
|
|
continue;
|
|
}
|
|
|
|
for (const layer of asset.Layer) {
|
|
if (!layer.HasImage) {
|
|
continue;
|
|
}
|
|
|
|
// Combinations involving pose-specific directories
|
|
const poses = new Set(Object.values(layer.PoseMapping).filter(p => p !== "Hide"));
|
|
if (poses.size === 0) {
|
|
poses.add("");
|
|
}
|
|
|
|
// Combinations involving modular/typed item types
|
|
/** @type {{ type: string, option: ExtendedItemOption }[]} */
|
|
let layerOptions = [];
|
|
if (layer.HasType || layer.ModuleType) {
|
|
switch (asset.Archetype) {
|
|
case "typed": {
|
|
const data = extendedDataRecords.typed[`${asset.Group.Name}${asset.Name}`];
|
|
layerOptions = data.options.map((o, i) => {
|
|
const ret = { type: i === 0 ? "" : o.Name, option: o };
|
|
return (layer.AllowTypes?.includes(ret.type) ?? true) ? ret : /** @type {never} */(null);
|
|
}).filter(i => i != null);
|
|
break;
|
|
}
|
|
case "modular": {
|
|
if (!layer.ModuleType) {
|
|
break;
|
|
}
|
|
|
|
const data = extendedDataRecords.modular[`${asset.Group.Name}${asset.Name}`];
|
|
layerOptions = layer.ModuleType.flatMap(key => {
|
|
/** @type {null | string[]} */
|
|
let allowedTypes = layer.AllowModuleTypes?.filter(typ => key === typ.slice(0, key.length)) ?? [];
|
|
if (allowedTypes.length === 0) {
|
|
allowedTypes = null;
|
|
}
|
|
const module = data.modules.find(m => m.Key === key);
|
|
return (module?.Options ?? []).filter(o => allowedTypes?.includes(o.Name) ?? true).map(o => {
|
|
return { type: o.Name, option: o };
|
|
});
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extended item options can have a set of allowed poses distinct from their base asset
|
|
/** @type {[type: string | null, poses: Iterable<AssetPoseName | PoseType>][]} */
|
|
let layerTypes = [[null, poses]];
|
|
if (!layer.HasType && layer.AllowTypes && asset.Archetype === "typed") {
|
|
// We need special logic
|
|
const data = extendedDataRecords.typed[`${asset.Group.Name}${asset.Name}`];
|
|
/** @type {Set<AssetPoseName | PoseType>} */
|
|
const posesSubset = new Set();
|
|
layer.AllowTypes.forEach((name, i) => {
|
|
const option = i === 0 ? data.options[0] : data.options.find(o => o.Name === name);
|
|
for (const p of (option?.Property?.AllowActivePose ?? asset.AllowActivePose ?? [])) {
|
|
const mappedPose = layer.PoseMapping[p];
|
|
if (mappedPose && mappedPose !== "Hide") {
|
|
posesSubset.add(mappedPose);
|
|
}
|
|
}
|
|
});
|
|
if (posesSubset.size === 0) {
|
|
posesSubset.add("");
|
|
}
|
|
layerTypes = [[null, posesSubset]];
|
|
} else if (layerOptions.length !== 0) {
|
|
layerTypes = layerOptions.map(({ type, option }) => {
|
|
if (option.Property?.AllowActivePose) {
|
|
const allowedPoses = /** @type {Set<AssetPoseName | PoseType>} */(new Set(option.Property.AllowActivePose.filter(p => {
|
|
const mappedPose = layer.PoseMapping[p];
|
|
return mappedPose && mappedPose !== "Hide";
|
|
}).map(p => layer.PoseMapping[p])));
|
|
if (allowedPoses.size === 0) {
|
|
allowedPoses.add("");
|
|
}
|
|
return [type, allowedPoses];
|
|
} else {
|
|
return [type, poses];
|
|
}
|
|
});
|
|
}
|
|
|
|
// Combinations involving non-hex based colors (white, black, asian, etc)
|
|
/** @type {(string | null)[]} */
|
|
let colors;
|
|
if (layer.InheritColor) {
|
|
let colorParent = assetGroups[layer.InheritColor];
|
|
while (colorParent.InheritColor) {
|
|
colorParent = assetGroups[colorParent.InheritColor];
|
|
}
|
|
colors = colorParent.ColorSchema.map(i => layer.ColorSuffix?.[i] ?? i).filter(i => i !== "Default" && !i.startsWith("#"));
|
|
} else {
|
|
colors = asset.Group.ColorSchema.filter(i => i !== "Default" && !i.startsWith("#"));
|
|
}
|
|
if (colors.length === 0) {
|
|
colors = [null];
|
|
}
|
|
|
|
// Combinations involving parent group members
|
|
const parentNames = layer.ParentGroupName == null ? [null] : assetGroupMap[layer.ParentGroupName][asset.Gender ?? "FM"] ;
|
|
|
|
// Combinations involving expressions
|
|
/** @type {readonly (null | ExpressionName)[]} */
|
|
let expressions = layer.Asset.AllowExpression ?? [];
|
|
if (expressions.length === 0 || !layer.MirrorExpression) {
|
|
expressions = [null];
|
|
}
|
|
|
|
const root = `${BASE_PATH}Assets/${asset.Group.Family}/${asset.DynamicGroupName}/`;
|
|
for (const parent of parentNames) {
|
|
for (const color of colors) {
|
|
for (const expression of expressions) {
|
|
for (const [type, poseList] of layerTypes) {
|
|
for (const pose of poseList) {
|
|
const file = (
|
|
root
|
|
+ (pose ? `${pose}/` : "")
|
|
+ (expression ? `${expression}/` : "")
|
|
+ asset.Name
|
|
+ (parent ? `_${parent}` : "")
|
|
+ (type ?? "")
|
|
+ (color == null ? "" : `_${color}`)
|
|
+ (layer.Name == null ? ".png" : `_${layer.Name}.png`)
|
|
);
|
|
if (!fs.existsSync(file)) {
|
|
error(`${asset.Group.Name}:${asset.Name}:${layer.Name}: Missing asset "${file}"`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strigify and parse the passed object to get the correct Array and Object prototypes, because VM uses different ones.
|
|
* This unfortunately results in Functions being lost and replaced with a dummy function
|
|
* @param {any} input The to-be sanitized input
|
|
* @returns {any} The sanitized output
|
|
*/
|
|
function sanitizeVMOutput(input) {
|
|
return JSON.parse(
|
|
JSON.stringify(
|
|
input,
|
|
(key, value) => typeof value === "function" ? "__FUNCTION__" : value,
|
|
),
|
|
(key, value) => value === "__FUNCTION__" ? () => { return; } : value,
|
|
);
|
|
}
|
|
|
|
(function () {
|
|
const [commonFile, ...neededFiles] = NEEDED_FILES;
|
|
const context = vm.createContext({
|
|
OuterArray: Array,
|
|
Object: Object,
|
|
TestingColorLayers: new Set(loadCSV("Assets/Female3DCG/LayerNames.csv", 2).map(i => i[0])),
|
|
TestingColorGroups: new Set(loadCSV("Assets/Female3DCG/ColorGroups.csv", 2).map(i => i[0])),
|
|
});
|
|
vm.runInContext(fs.readFileSync(BASE_PATH + commonFile, { encoding: "utf-8" }), context, {
|
|
filename: commonFile,
|
|
});
|
|
|
|
// Only patch `CommonGet` after loading `Common`, lest our monkey patch will be overriden again
|
|
context.CommonGet = (file, callback) => {
|
|
const data = fs.readFileSync(`../../${file}`, "utf8");
|
|
const obj = {
|
|
status: 200,
|
|
responseText: data,
|
|
};
|
|
callback.bind(obj)(obj);
|
|
};
|
|
for (const file of neededFiles) {
|
|
vm.runInContext(fs.readFileSync(BASE_PATH + file, { encoding: "utf-8" }), context, {
|
|
filename: file,
|
|
});
|
|
}
|
|
|
|
/** @type {AssetGroupDefinition[]} */
|
|
const AssetFemale3DCG = sanitizeVMOutput(context.AssetFemale3DCG);
|
|
/** @type {ExtendedItemMainConfig} */
|
|
const AssetFemale3DCGExtended = sanitizeVMOutput(context.AssetFemale3DCGExtended);
|
|
/** @type {TestingStruct<string>[]} */
|
|
const missingColorLayers = sanitizeVMOutput(context.TestingMisingColorLayers);
|
|
/** @type {TestingStruct<string>[]} */
|
|
const missingColorGroups = sanitizeVMOutput(context.TestingMisingColorGroups);
|
|
/** @type {TestingStruct<string[]>[]} */
|
|
const invalidColorDefaults = sanitizeVMOutput(context.TestingInvalidDefaultColor);
|
|
/** @type {Record<string, ModularItemData>} */
|
|
const ModularItemDataLookup = context.TestingModularItemDataLookup;
|
|
/** @type {Record<string, TypedItemData>} */
|
|
const TypedItemDataLookup = context.TestingTypedItemDataLookup;
|
|
/** @type {Record<string, VibratingItemData>} */
|
|
const VibratingItemDataLookup = context.TestingVibratingItemDataLookup;
|
|
/** @type {Record<string, TextItemData>} */
|
|
const TextItemDataLookup = context.TestingVariableHeightItemDataLookup;
|
|
/** @type {Record<string, VariableHeightData>} */
|
|
const VariableHeightItemDataLookup = context.TestingTextItemDataLookup;
|
|
/** @type {Asset[]} */
|
|
const assetList = context.Asset;
|
|
/** @type {Record<AssetPoseCategory, Set<AssetPoseName>>} */
|
|
const poseMap = context.TestingPoseMap;
|
|
|
|
if (!Array.isArray(AssetFemale3DCG)) {
|
|
error("AssetFemale3DCG not found");
|
|
return;
|
|
}
|
|
|
|
const assetDescriptions = loadCSV("Assets/Female3DCG/Female3DCG.csv", 3);
|
|
const dialogArray = loadCSV("Screens/Character/Player/Dialog_Player.csv", 6);
|
|
|
|
// No further checks if initial data load failed
|
|
if (common.localError) {
|
|
return;
|
|
}
|
|
|
|
// Arrays of type-validated groups and assets
|
|
/** @type {AssetGroupDefinition[]} */
|
|
const Groups = [];
|
|
/** @type {Record<string, AssetDefinition[]>} */
|
|
const Assets = {};
|
|
|
|
// Check all groups
|
|
for (const Group of AssetFemale3DCG) {
|
|
common.localError = false;
|
|
|
|
Groups.push(Group);
|
|
/** @type {AssetDefinition[]} */
|
|
const GroupAssets = (Assets[Group.Group] = []);
|
|
|
|
// Check all assets in groups
|
|
for (const Asset of Group.Asset) {
|
|
if (typeof Asset === "string") {
|
|
GroupAssets.push({
|
|
Name: Asset
|
|
});
|
|
continue;
|
|
}
|
|
common.localError = false;
|
|
|
|
// Check any extended item config
|
|
if (Asset.Extended) {
|
|
const groupConfig = AssetFemale3DCGExtended[Group.Group] || {};
|
|
const assetConfig = groupConfig[Asset.Name];
|
|
if (assetConfig) {
|
|
const archetype = assetConfig.Archetype;
|
|
if (archetype) {
|
|
const propList = ["AllowEffect", "AllowBlock", "AllowHide", "AllowHideItem", "AllowType", "AllowLockType"];
|
|
for (const name of propList) {
|
|
if (Asset[name] !== undefined) {
|
|
error(`${Group.Group}:${Asset.Name}: Archetypical extended items must NOT set ${name}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(Asset.Layer)) {
|
|
if (Asset.Layer.length === 0) {
|
|
error(`${Group.Group}:${Asset.Name}: Asset must contain at least one layer when explicitly specified`);
|
|
}
|
|
for (const layerDef of Asset.Layer) {
|
|
// Check for conflicting layer properties
|
|
if (Array.isArray(layerDef.HideForAttribute) && Array.isArray(layerDef.ShowForAttribute)) {
|
|
for (const attribute of layerDef.HideForAttribute) {
|
|
if (layerDef.ShowForAttribute.includes(attribute)) {
|
|
error(`Layer ${Group.Group}:${Asset.Name}:${layerDef.Name}: Attribute "${attribute}" should NOT appear in both HideForAttribute and ShowForAttribute`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!common.localError) {
|
|
GroupAssets.push(Asset);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (common.globalError) {
|
|
console.log("WARNING: Type errors detected, skipping other checks");
|
|
return;
|
|
}
|
|
|
|
// Validate description order
|
|
{
|
|
let group = "";
|
|
for (let line = 0; line < assetDescriptions.length; line++) {
|
|
if (assetDescriptions[line][1] === "") {
|
|
group = assetDescriptions[line][0];
|
|
} else if (assetDescriptions[line][0] !== group) {
|
|
error(
|
|
`Asset descriptions line ${line + 1} not under it's matching group! ` +
|
|
`(${assetDescriptions[line][0]}:${assetDescriptions[line][1]} is in ${group} group)`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check all type-valid groups for specific data
|
|
for (const Group of Groups) {
|
|
// Description name
|
|
const descriptionIndex = assetDescriptions.findIndex(d => d[0] === Group.Group && d[1] === "");
|
|
if (descriptionIndex < 0) {
|
|
error(`No description for group "${Group.Group}"`);
|
|
} else {
|
|
assetDescriptions.splice(descriptionIndex, 1);
|
|
}
|
|
|
|
// Check all type-valid assets for specific data
|
|
for (const Asset of Assets[Group.Group]) {
|
|
// Description name
|
|
const descriptionIndexAsset = assetDescriptions.findIndex(d => d[0] === Group.Group && d[1] === Asset.Name);
|
|
if (descriptionIndexAsset < 0) {
|
|
error(`No description for asset "${Group.Group}:${Asset.Name}"`);
|
|
} else {
|
|
assetDescriptions.splice(descriptionIndexAsset, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for extra descriptions
|
|
for (const desc of assetDescriptions) {
|
|
error(`Unused Asset/Group description: ${desc.join(",")}`);
|
|
}
|
|
|
|
testDynamicGroupName(AssetFemale3DCG);
|
|
testExtendedItemDialog(AssetFemale3DCGExtended, dialogArray);
|
|
testColorGroups(missingColorGroups);
|
|
testColorLayers(missingColorLayers);
|
|
testModuleOptionLength(AssetFemale3DCGExtended);
|
|
testDefaultColor(invalidColorDefaults);
|
|
testModularItemPropertyDisjoint(ModularItemDataLookup);
|
|
testExtendedItemButtonImage({
|
|
modular: ModularItemDataLookup,
|
|
text: TextItemDataLookup,
|
|
typed: TypedItemDataLookup,
|
|
variableheight: VariableHeightItemDataLookup,
|
|
vibrating: VibratingItemDataLookup,
|
|
});
|
|
testAssetLayerNames(assetList);
|
|
testAssetAndExtendedPropertyIsDisjoint({
|
|
modular: ModularItemDataLookup,
|
|
text: TextItemDataLookup,
|
|
typed: TypedItemDataLookup,
|
|
variableheight: VariableHeightItemDataLookup,
|
|
vibrating: VibratingItemDataLookup,
|
|
});
|
|
testAllowTypes({ ...TypedItemDataLookup, ...ModularItemDataLookup });
|
|
testAllowModuleTypes(ModularItemDataLookup);
|
|
testCopyLayerColor(assetList);
|
|
testExtendedPrerequisite({
|
|
modular: ModularItemDataLookup,
|
|
text: TextItemDataLookup,
|
|
typed: TypedItemDataLookup,
|
|
variableheight: VariableHeightItemDataLookup,
|
|
vibrating: VibratingItemDataLookup,
|
|
});
|
|
testActivePose(assetList, poseMap);
|
|
testHasPreview(assetList);
|
|
testAssetHasPNG(assetList, {
|
|
modular: ModularItemDataLookup,
|
|
typed: TypedItemDataLookup,
|
|
});
|
|
})();
|