diff --git a/package-lock.json b/package-lock.json index 06b4976..b03327c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "glob": "^10.2.2", "jsonschema": "^1.4.1", "jszip": "^3.10.1", + "sanitize-filename": "^1.6.3", "uuid": "^9.0.0" }, "devDependencies": { @@ -1138,6 +1139,14 @@ } ] }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -1345,6 +1354,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tsc-alias": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.4.tgz", @@ -1375,6 +1392,11 @@ "node": ">=12.20" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2261,6 +2283,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -2408,6 +2438,14 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, "tsc-alias": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.4.tgz", @@ -2428,6 +2466,11 @@ "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==", "dev": true }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index fa293e7..da129c9 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "glob": "^10.2.2", "jsonschema": "^1.4.1", "jszip": "^3.10.1", + "sanitize-filename": "^1.6.3", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/schemas/s4tk-config.schema.json b/schemas/s4tk-config.schema.json index 59d885e..6c46b26 100644 --- a/schemas/s4tk-config.schema.json +++ b/schemas/s4tk-config.schema.json @@ -237,6 +237,10 @@ "overrideIndexRoot": { "type": "string", "description": "The root to use for the resource index instead of `buildInstructions.source`. You likely do not have a use for this unless your project includes multiple packages that have files that override each other." + }, + "setSimDataNames": { + "type": "boolean", + "description": "Whether or not commands like 'Convert Folder to S4TK Project' are allowed to set SimData names to match their tuning." } } } diff --git a/src/contributions/commands/workspace.ts b/src/contributions/commands/workspace.ts index 1ed2b9d..dd73b9c 100644 --- a/src/contributions/commands/workspace.ts +++ b/src/contributions/commands/workspace.ts @@ -64,5 +64,8 @@ export default function registerWorkspaceCommands() { if (workspace) workspace.index.refresh(); }); - vscode.commands.registerCommand(S4TKCommand.workspace.folderToProject, convertFolderToProject); + vscode.commands.registerCommand(S4TKCommand.workspace.folderToProject, async () => { + const workspace = await S4TKWorkspaceManager.chooseWorkspace(); + if (workspace) convertFolderToProject(workspace); + }); } diff --git a/src/core/building/builder.ts b/src/core/building/builder.ts index 91ccf22..9540bfc 100644 --- a/src/core/building/builder.ts +++ b/src/core/building/builder.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import * as JSZip from "jszip"; +import JSZip from "jszip"; import * as models from "@s4tk/models"; import * as enums from "@s4tk/models/enums"; import * as types from "@s4tk/models/types"; diff --git a/src/core/helpers/fs.ts b/src/core/helpers/fs.ts index 1e3a0ec..816b133 100644 --- a/src/core/helpers/fs.ts +++ b/src/core/helpers/fs.ts @@ -1,4 +1,5 @@ import { existsSync } from "fs"; +import sanitize from "sanitize-filename"; import * as vscode from "vscode"; /** @@ -14,6 +15,16 @@ export function findOpenDocument(uri: vscode.Uri): vscode.TextDocument | undefin }); } +/** + * Sanitizes and removes the author prefix from the given filename. + * + * @param filename a raw filename + * @returns the simplified filename + */ +export function simplifyTuningFilename(filename: string) { + return sanitize(`${filename.replace(/^[^:]*:/, "")}.xml`, { replacement: '_', }) +} + /** * Replaces an entire document's contents using its editor, and returns whether * the edits could be made or not. diff --git a/src/core/tuning/commands.ts b/src/core/tuning/commands.ts index c413f65..90373b7 100644 --- a/src/core/tuning/commands.ts +++ b/src/core/tuning/commands.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode"; import { fnv64 } from "@s4tk/hashing"; import { formatAsHexString } from "@s4tk/hashing/formatting"; import { SimDataResource, XmlResource } from "@s4tk/models"; -import { replaceEntireDocument } from "#helpers/fs"; +import { replaceEntireDocument, simplifyTuningFilename } from "#helpers/fs"; import { insertXmlKeyOverrides } from "#indexing/inference"; import { reduceBits } from "#helpers/hashing"; import { maxBitsForClass } from "#diagnostics/helpers"; @@ -78,7 +78,7 @@ async function _renameTuningAndSimData(srcUri: vscode.Uri, operation: "clone" | const tuningFsPath = path.join( path.dirname(srcUri.fsPath), - `${newFilename.replace(/^[^:]*:/, "")}.xml` + simplifyTuningFilename(newFilename) ); if (fs.existsSync(tuningFsPath)) { diff --git a/src/core/workspace/folder-to-project.ts b/src/core/workspace/folder-to-project.ts index 2839222..a34cfb1 100644 --- a/src/core/workspace/folder-to-project.ts +++ b/src/core/workspace/folder-to-project.ts @@ -1,19 +1,21 @@ import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; -import { ResourceKey } from "@s4tk/models/types"; +import { ResourceFilter, ResourceKey } from "@s4tk/models/types"; import { Package, RawResource, SimDataResource, StringTableResource } from "@s4tk/models"; import { BinaryResourceType, SimDataGroup, TuningResourceType } from "@s4tk/models/enums"; import { formatResourceType, formatResourceKey, formatAsHexString } from "@s4tk/hashing/formatting"; import { findGlobMatches, parseKeyFromTgi } from "#building/resources"; -import StringTableJson from "#stbls/stbl-json"; +import { simplifyTuningFilename } from "#helpers/fs"; import * as inference from "#indexing/inference"; +import StringTableJson from "#stbls/stbl-json"; +import S4TKWorkspace from "#workspace/s4tk-workspace"; /** * Prompts the user for a folder containing packages and/or loose TGI files and * turns them into a structure that is easier to use with the S4TK extension. */ -export async function convertFolderToProject() { +export async function convertFolderToProject(workspace: S4TKWorkspace) { const sourceFolderUri = await _promptForFolder({ title: "Folder Containing TS4 Resources", openLabel: "Use as Source" @@ -40,8 +42,10 @@ export async function convertFolderToProject() { const sourcePattern = path.join(sourceFolderUri.fsPath, "**/*").replace(/\\/g, "/"); const matches = findGlobMatches([sourcePattern], undefined, "supported"); + var instanceMap = new Map(); + matches.forEach((sourcePath: string) => { - _processSourceFile(sourcePath, destFolderUri.fsPath); + _processSourceFile(workspace, sourcePath, destFolderUri.fsPath, instanceMap); }); } @@ -70,11 +74,12 @@ function _appendFolder(basepath: string, ...toAppend: string[]): string { } function _getDestFilename(destFolder: string, filename: string, ext: string): string { + const pfilename = filename.includes(":") + ? filename.split(":").slice(1).join(":") + : filename const baseDestPath = path.join( destFolder, - filename.includes(":") - ? filename.split(":").slice(1).join(":") - : filename + simplifyTuningFilename(pfilename) ); let index = 0; @@ -86,29 +91,56 @@ function _getDestFilename(destFolder: string, filename: string, ext: string): st return `${destPath}.${ext}`; } -function _processSourceFile(sourcePath: string, destFolder: string) { +function _processSourceFile( + workspace: S4TKWorkspace, + sourcePath: string, + destFolder: string, + instanceMap: Map +) { const sourceName = path.basename(sourcePath); if (sourceName.endsWith(".package")) { const packageName = sourceName.replace(/\.package/g, ""); const packageDest = _appendFolder(destFolder, "Packages", packageName); const buffer = fs.readFileSync(sourcePath); - Package.extractResources(buffer, { - loadRaw: true, - decompressBuffers: true, - }).forEach(entry => { - _processResource(entry.key, entry.value.buffer, packageDest); - }); + // Process the tuning types first to populate the instanceMap... + _processPackage(workspace, buffer, packageDest, instanceMap, true); + // ... so they'll be available when the SimData is seen + _processPackage(workspace, buffer, packageDest, instanceMap, false); } else { const key = parseKeyFromTgi(sourceName); if (!key) return; const buffer = fs.readFileSync(sourcePath); const resourceDest = _appendFolder(destFolder, "Loose Files"); - _processResource(key, buffer, resourceDest); + _processResource(workspace, key, buffer, resourceDest, instanceMap); } } -function _processResource(key: ResourceKey, buffer: Buffer, destFolder: string) { +function _processPackage( + workspace: S4TKWorkspace, + buffer: Buffer, + packageDest: string, + instanceMap: Map, + tuning: boolean +) { + Package.extractResources(buffer, { + loadRaw: true, + decompressBuffers: true, + resourceFilter(type, group, inst) { + return (type in TuningResourceType) == tuning; + } + }).forEach(entry => { + _processResource(workspace, entry.key, entry.value.buffer, packageDest, instanceMap); + }); +} + +function _processResource( + workspace: S4TKWorkspace, + key: ResourceKey, + buffer: Buffer, + destFolder: string, + instanceMap: Map +) { const getSubfolder = (...args: string[]) => _appendFolder(destFolder, ...args); if (key.type in TuningResourceType) { @@ -132,11 +164,11 @@ function _processResource(key: ResourceKey, buffer: Buffer, destFolder: string) xmlContent = inference.insertXmlKeyOverrides(xmlContent, overrides) ?? xmlContent; - // FIXME: remove creator name prefix - fs.writeFileSync( - _getDestFilename(subfolder, metadata.attrs?.n ?? "UnnamedTuning", "xml"), - xmlContent - ); + const name = metadata.attrs?.n ?? "UnnamedTuning" + const dest = _getDestFilename(subfolder, name, "xml"); + fs.writeFileSync(dest, xmlContent); + + instanceMap.set(key.instance, [name, dest]); } else if (key.type in BinaryResourceType) { if (key.type === BinaryResourceType.SimData) { const subfolder = key.group in SimDataGroup @@ -147,14 +179,19 @@ function _processResource(key: ResourceKey, buffer: Buffer, destFolder: string) ? SimDataResource.from(buffer) : SimDataResource.fromXml(buffer); - const xmlContent = simdata.toXmlDocument().toXml(); - - // TODO: insert group and instance override if tuning not found + var dest: string; + const val = instanceMap.get(key.instance); + if (val) { + if (workspace.config.workspaceSettings.setSimDataNames) + simdata.instance.name = val[0] + "_SimData"; + dest = val[1].replace(".xml", ".SimData.xml"); + } else { + // TODO: insert group and instance override instead of using formatResourceKey + dest = _getDestFilename(subfolder, formatResourceKey(key, "_"), "SimData.xml"); + } - fs.writeFileSync( - _getDestFilename(subfolder, simdata.instance.name, "SimData.xml"), - xmlContent - ); + const xmlContent = simdata.toXmlDocument().toXml(); + fs.writeFileSync(dest, xmlContent); } else if (key.type === BinaryResourceType.StringTable) { const subfolder = getSubfolder("StringTable"); const stbl = StringTableResource.from(buffer); diff --git a/src/core/workspace/s4tk-config.ts b/src/core/workspace/s4tk-config.ts index e2b0faa..404f521 100644 --- a/src/core/workspace/s4tk-config.ts +++ b/src/core/workspace/s4tk-config.ts @@ -53,6 +53,7 @@ export interface S4TKConfig { workspaceSettings: { overrideIndexRoot?: string; + setSimDataNames: boolean; }; } @@ -89,7 +90,9 @@ const _CONFIG_TRANSFORMER: ConfigTransformer = { }, }, workspaceSettings: { - defaults: {}, + defaults: { + setSimDataNames: false, + }, }, }; @@ -186,8 +189,7 @@ function _getObjectProxy(target: T | undefined, { getConverter = (_, value) => value }: ConfigPropertyTransformer): T { return new Proxy(target ?? {} as T, { - //@ts-ignore I genuinely do not understand why TS doesn't like this - get(target, prop: keyof T) { + get(target, prop: Exclude) { return getConverter(prop, target[prop] ?? defaults[prop]); }, }) as T; diff --git a/tsconfig.json b/tsconfig.json index 28570d7..9a1669c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "#stbls/*": ["core/stbls/*"], "#tuning/*": ["core/tuning/*"], "#workspace/*": ["core/workspace/*"], - } + }, + "esModuleInterop": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", ".vscode-test"],