Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions schemas/s4tk-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/contributions/commands/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
2 changes: 1 addition & 1 deletion src/core/building/builder.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
11 changes: 11 additions & 0 deletions src/core/helpers/fs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync } from "fs";
import sanitize from "sanitize-filename";
import * as vscode from "vscode";

/**
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/core/tuning/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)) {
Expand Down
93 changes: 65 additions & 28 deletions src/core/workspace/folder-to-project.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<bigint, [string, string]>();

matches.forEach((sourcePath: string) => {
_processSourceFile(sourcePath, destFolderUri.fsPath);
_processSourceFile(workspace, sourcePath, destFolderUri.fsPath, instanceMap);
});
}

Expand Down Expand Up @@ -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;
Expand All @@ -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<bigint, [string, string]>
) {
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<RawResource>(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<bigint, [string, string]>,
tuning: boolean
) {
Package.extractResources<RawResource>(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<bigint, [string, string]>
) {
const getSubfolder = (...args: string[]) => _appendFolder(destFolder, ...args);

if (key.type in TuningResourceType) {
Expand All @@ -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
Expand All @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions src/core/workspace/s4tk-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface S4TKConfig {

workspaceSettings: {
overrideIndexRoot?: string;
setSimDataNames: boolean;
};
}

Expand Down Expand Up @@ -89,7 +90,9 @@ const _CONFIG_TRANSFORMER: ConfigTransformer = {
},
},
workspaceSettings: {
defaults: {},
defaults: {
setSimDataNames: false,
},
},
};

Expand Down Expand Up @@ -186,8 +189,7 @@ function _getObjectProxy<T extends object>(target: T | undefined, {
getConverter = (_, value) => value
}: ConfigPropertyTransformer<T>): T {
return new Proxy<T>(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<keyof T, number>) {
return getConverter(prop, target[prop] ?? defaults[prop]);
},
}) as T;
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"#stbls/*": ["core/stbls/*"],
"#tuning/*": ["core/tuning/*"],
"#workspace/*": ["core/workspace/*"],
}
},
"esModuleInterop": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", ".vscode-test"],
Expand Down