diff --git a/Scripts/Export_InkStyles.wscript b/Scripts/Export_InkStyles.wscript new file mode 100644 index 0000000..3101a51 --- /dev/null +++ b/Scripts/Export_InkStyles.wscript @@ -0,0 +1,245 @@ +// Export inkstyle files to Json / CSV files. +// +// @author Rayshader +// @version 1.0 +// +// @usage +// NOTE: it may not be entirely accurate. +// +// - change path of 'jsonExport' and 'csvExport' below if you wish +// - Open a project +// - Load assets +// - Run this script +// - Find styles in the generated files. + +import * as Logger from 'Logger.wscript'; + +/// Public globals /// + +const jsonExport = 'base\\InkStyles.json'; +const csvExport = 'base\\InkStyles.csv'; + +/// Private globals /// + +// Format data per engine's type. +const formatters = { + 'Int32': (value) => value, + 'Float': (value) => value, + + 'String': (value) => value, + 'CName': (value) => value.$value, + + 'Color': (value) => [value.Red, value.Green, value.Blue, value.Alpha], + 'HDRColor': (value) => [value.Red, value.Green, value.Blue, value.Alpha], + + 'inkStylePropertyReference': (value) => value.referencedPath.$value, + + 'rRef:rendFont': (value) => value.DepotPath.$value, + 'raRef:inkTextureAtlas': (value) => value.DepotPath.$value, +}; + +// Keep track of data type errors to prevent duplicate logs. +const errors = {}; + +/// Main /// + +Info('Start'); +wkit.SuspendFileWatcher(true); +Run(); +wkit.SuspendFileWatcher(false); +Info('Stop'); + +/// Logic /// + +function Run() { + let styles = ListInkStyles(); + + if (styles.length === 0) { + Error('You must "Load assets" to run this script.'); + return; + } + Info(`Found ${styles.length} inkstyle files`); + styles = styles.map((style) => { + return { + Json: FormatStyle(style.Json), + Path: style.Path + }; + }) + Info(`Formatted ${styles.length} inkstyle files`); + const style = MergeStyles(styles); + + if (style === null) { + Error('Failed to merge data: no styles to export!'); + Error('Ask author on the following channel: https://discord.com/channels/717692382849663036/1036574451237662780'); + return; + } + const json = JsonStringify(style); + + wkit.SaveToRaw(jsonExport, json); + const csv = CsvStringify(style); + + wkit.SaveToRaw(csvExport, csv); + Success(`Json is ready in: "raw\\${jsonExport}".`); + Success(`CSV is ready in: "raw\\${csvExport}".`); +} + +function ListInkStyles() { + Info('Listing inkstyle files...'); + const files = [...wkit.GetArchiveFiles()]; + + if (files.length === 0) { + return []; + } + return files + .filter((file) => file.Extension === '.inkstyle') + .map((file) => { + const json = wkit.GameFileToJson(file); + //const path = wkit.ChangeExtension(file.Name, '.json'); + + //wkit.SaveToRaw(path, json); + return { + Json: JSON.parse(json), + Path: file.Name + }; + }); +} + +function FormatStyle(json) { + const styles = json.Data.RootChunk.styles; + const data = {}; + + styles.filter((style) => style.$type === 'inkStyle' && style.properties.length > 0) + .forEach((style) => { + for (const property of style.properties) { + const key = property.propertyPath.$value; + const type = property.value.$type; + let value = property.value.Value; + + if (!(type in formatters)) { + if (errors[type] !== true) { + Error(`Type ${type} is not implemented:`); + Info(JSON.stringify(property)); + errors[type] = true; + } + } else { + value = formatters[type](value); + } + data[key] = value; + } + }); + return { + Header: json.Header, + Data: data, + }; +} + +// Group per file +function MergeStyles(styles) { + if (styles.length === 0) { + return null; + } + const files = {}; + + for (const style of styles) { + const path = style.Path; + + if (!files[path]) { + files[path] = []; + } + const data = style.Json.Data; + const items = files[path]; + + for (const key of Object.keys(data)) { + items.push([key, data[key]]); + } + items.sort((a, b) => a[0].localeCompare(b[0])); + } + + const result = { + Header: styles[styles.length - 1].Json.Header, + Schema: { + PropertyName: { + 'Int32': 42, + 'Float': 0.42, + + 'String': 'A message', + 'CName': 'MainColors.Red', + + 'Color': '[255, 127, 255, 255]', + 'HDRColor': '[1.0, 1.0, 1.0, 1.0]', + + 'rRef:rendFont': 'path\\to\\resource\\.font', + 'raRef:inkTextureAtlas': 'path\\to\\resource\\.inkatlas', + } + }, + Data: {} + }; + result.Header.ExportedDateTime = new Date().toISOString(); + result.Header.DataType = 'InkStyles'; + + for (const path of Object.keys(files)) { + if (!result.Data[path]) { + result.Data[path] = {}; + } + const file = files[path]; + const data = result.Data[path]; + + for (const item of file) { + data[item[0]] = item[1]; + } + } + return result; +} + +function JsonStringify(style) { + return JSON.stringify(style, (_, value) => (value instanceof Array) ? JSON.stringify(value) : value, ' ') + .replace(/\"\[/g, '[') + .replace(/\]\"/g,']'); +} + +function CsvStringify(style) { + let csv = ''; + + csv += CsvLine('WolvenKitVersion', style.Header.WolvenKitVersion); + csv += CsvLine('WKitJsonVersion', style.Header.WKitJsonVersion); + csv += CsvLine('GameVersion', style.Header.GameVersion); + csv += CsvLine('ExportedDateTime', style.Header.ExportedDateTime); + csv += CsvLine('DataType', style.Header.DataType); + + csv += CsvLine(''); + + for (const path of Object.keys(style.Data)) { + csv += CsvLine(path); + const file = style.Data[path]; + + for (const key of Object.keys(file)) { + const value = file[key]; + + csv += CsvLine(key, value); + } + csv += CsvLine(''); + } + return csv; +} + +function CsvLine(...args) { + return args.join(';') + '\n'; +} + +/// Local logger /// + +function Info(msg) { + Logger.Info(`[Export_InkStyles] ${msg}`); +} + +function Warn(msg) { + Logger.Warning(`[Export_InkStyles] ${msg}`); +} + +function Error(msg) { + Logger.Error(`[Export_InkStyles] ${msg}`); +} + +function Success(msg) { + Logger.Success(`[Export_InkStyles] ${msg}`); +} \ No newline at end of file diff --git a/Scripts/Export_Sector_embedsONLY.wscript b/Scripts/Export_Sector_embedsONLY.wscript new file mode 100644 index 0000000..446833e --- /dev/null +++ b/Scripts/Export_Sector_embedsONLY.wscript @@ -0,0 +1,269 @@ +// Exports Just the embedded files from StreamingSector files and all referenced files (recursively) +// @author Simarilius, DZK, Seberoth & manavortex +// @version 1.0 +// Requires 8.14 or higher +import * as Logger from 'Logger.wscript'; +import * as TypeHelper from 'TypeHelper.wscript'; +import * as Collision_Shapes from 'collision_shapes.wscript'; + +wkit.SuspendFileWatcher(true) +// Export all sectors in project? (set to false to only use sectors list below) +const add_from_project = true; + +// Set to true to disable warnings about failed meshes +const suppressLogFileOutput = false; + +// If you dont want mats then set this to false +const withmats=false; + +// If you only want files that are new exporting set this to true +const only_new=false + +// If you want mesh colliders extracting then set this to true +const mesh_colliders=false + +// format for images to get exported to +const imgFormat = 'png' + +/** + * Any sectors in the list below will be added to your project before exporting. List of sector files to add to your project, see wiki: https://tinyurl.com/cyberpunk2077worldsectors + * + * If the file is in default _compiled dir, file name is enough. Otherwise, put the full path (remember to double slashes)! + */ +let sectors = [ + // El Coyote + //'interior_-20_-16_0_1','interior_-39_-31_0_0','interior_-39_-32_0_0','interior_-40_-31_0_0','interior_-40_-32_0_0' + + /* V's apartment: H10 */ + // 'interior_-22_19_1_1', 'interior_-22_20_1_1', 'interior_-43_39_3_0', 'interior_-44_39_3_0', 'interior_-44_40_3_0', 'interior_-46_40_3_0', 'exterior_-22_19_1_0', 'quest_b705140105a75f58', 'quest_acd280b2b73c4d5b', 'quest_2467054678ccf8f6', 'quest_3509076113f76078', 'quest_e1ef450702659584', 'quest_2be595b225125038', 'quest_5eb84e72f3942283', 'quest_e1ef450702659584', 'quest_1fbb2ceaeeaac973', + + /* V's apartment: Loft */ + // 'exterior_-24_-16_1_0', 'interior_-24_-16_1_1', 'interior_-48_-31_2_0', 'quest_ca115e9713d725d7' + + /* V's apartment: Northside dump */ + // 'exterior_-24_34_0_0', 'interior_-48_68_0_0', 'interior_-12_17_0_2', 'exterior_-12_17_0_1', 'interior_-47_69_0_0', 'interior_-48_69_0_0', + + /* V's apartment: Corpo Plaza */ + // 'interior_-25_5_0_1', 'interior_-51_10_1_0', 'interior_-51_11_1_0', 'interior_-50_11_1_0', 'interior_-26_5_0_1', + + /* V's penthouse */ + // 'exterior_-21_18_1_0', 'interior_-11_9_0_2', 'interior_-21_18_1_1', 'interior_-22_18_1_1 ', 'interior_-43_37_3_0 ', 'interior_-43_39_3_0', 'quest_81387f43768bad6c', + + /* V's Japantown Apartment */ + // 'interior_-13_15_0_0', 'interior_-13_15_0_1', 'interior_-25_30_0_0', + + /* V's Dogtown apartment */ + // 'ep1\\interior_-70_-81_2_0', 'ep1\\exterior_-35_-40_1_0', 'ep1\\exterior_-35_-41_1_0', 'ep1\\exterior_-18_-21_0_1', 'ep1\\interior_-70_-80_2_0' +]; + +/* + * ===================================== Change below this line at own risk ======================================== + */ + + + +const fileTemplate = '{"Header":{"WKitJsonVersion":"0.0.7","DataType":"CR2W"},"Data":{"Version":195,"BuildVersion":0,"RootChunk":{},"EmbeddedFiles":[]}}'; + +let jsonExtensions = [".app", ".ent", ".mi", ".mt", ".streamingsector",".cfoliage"]; +let exportExtensions = [".mesh",".w2mesh",".physicalscene",".xbm"]; // need xbms for decals +let exportEmbeddedExtensions = [".mesh",".w2mesh", ".xbm", ".mlmask", ".mlsetup",".cfoliage", ".streamingsector",".cminimap"]; + +if (!withmats){ + Logger.Info('Withmats false') + jsonExtensions = [".app", ".ent", ".streamingsector",".cfoliage",".cminimap"]; + exportExtensions = [".mesh",".w2mesh",".physicalscene"]; + exportEmbeddedExtensions = [".mesh",".w2mesh",".cfoliage", ".streamingsector",".cminimap"]; +} + +const sectorPathInFiles = 'base\\worlds\\03_night_city\\_compiled\\default'; +for (let i = 0; i < sectors.length; i += 1) { + let sectorPath = sectors[i]; + if ((sectorPath.match(/\/\//g) || []).length < 2) { // one double slash is ok (for PL sectors) + sectorPath = `${sectorPathInFiles}\\${sectorPath}`; + } + if (!sectorPath.endsWith('.streamingsector')) { + sectorPath = `${sectorPath}.streamingsector`; + } + sectors[i] = sectorPath; +} + +if (add_from_project) { + for (let filename of wkit.GetProjectFiles('archive')) { + // Logger.Success(filename); + if (filename.split('.').pop() === "streamingsector") { + sectors.push(filename); + } + } + + // now remove duplicates + sectors = Array.from(new Set(sectors)); +} + +// sets of files that are parsed for processing +const parsedFiles = new Set(); +const projectSet = new Set(); +const exportSet = new Set(); +const jsonSet = new Set(); + +// loop over every sector in `sectors` +for (let sect in sectors) { + Logger.Info(`Parsing sector...\n${sectors[sect]}`); + ParseFile(sectors[sect], null); +} + +// save all our files to the project +Logger.Info(`Exporting ${projectSet.size} files to JSON. This might take a while...`); +for (const fileName of projectSet) { + let file; + if (wkit.FileExistsInProject(fileName)) { + file = wkit.GetFileFromProject(fileName, OpenAs.GameFile); + } else { + file = wkit.GetFileFromBase(fileName); + wkit.SaveToProject(fileName, file); + } + if (!!file && jsonSet.has(fileName) && jsonExtensions.includes(file.Extension)) { + const path = wkit.ChangeExtension(file.Name, `${file.Extension}.json`); + const json = wkit.GameFileToJson(file); + wkit.SaveToRaw(path, json); + } +} + +let exportsettings = {Mesh: { ExportType: "MeshOnly", WithMaterials: withmats, ImageType: imgFormat, LodFilter: true, Binary: true}} + + +// export all of our files with the default export settings +Logger.Info(`Exporting ${exportSet.size} files...`); +wkit.ExportFiles([...exportSet], exportsettings); + +wkit.SuspendFileWatcher(false) +Logger.Success("======================================\nSector export finished!\n\n") + +//#region helper_functions +function* GetPaths(jsonData) { + for (let [key, value] of Object.entries(jsonData || {})) { + if (value instanceof TypeHelper.ResourcePath && !value.isEmpty()) { + yield value.value; + } + if (typeof value === "object") { + yield* GetPaths(value); + } + } +} + +function export_filename(filename){ + // Define a mapping of extensions and their replacements + const extensionMap = { + 'mesh': 'glb', + 'xbm': imgFormat, + 'w2mesh':'glb', + 'physicalscene':'glb', + 'streamingsector':'streamingsector.json' + + // Add more extensions and replacements as needed + }; + + // Find the position of the last occurrence of '.' to get the extension + const dotIndex = filename.lastIndexOf('.'); + if (dotIndex === -1) { + // If no extension found, return the filename as it is + return filename; + } else { + // Extract the existing extension + const extension = filename.substring(dotIndex + 1); + + // Check if the extension exists in the mapping + if (extensionMap.hasOwnProperty(extension)) { + // Replace the existing extension with the mapped one + return filename.substring(0, dotIndex) + '.' + extensionMap[extension]; + } else { + // If extension not found in mapping, return the filename as it is + return filename; + } + } + +} + + +function convertEmbedded(embeddedFile) { + let data = TypeHelper.JsonParse(fileTemplate); + data["Data"]["RootChunk"] = embeddedFile["Content"]; + let jsonString = TypeHelper.JsonStringify(data); + + let cr2w = wkit.JsonToCR2W(jsonString); + wkit.SaveToProject(embeddedFile["FileName"].value, cr2w); +} + +// Parse a CR2W file +function ParseFile(fileName, parentFile) { + // check if we've already worked with this file and that it's actually a string + if (parsedFiles.has(fileName)) { + return; + } + parsedFiles.add(fileName); + + let extension = typeof (fileName) !== 'string' ? 'unknown' : `.${fileName.split('.').pop()}`; + + if (extension !== 'unknown') { + if (!(jsonExtensions.includes(extension) || exportExtensions.includes(extension))) { + return; + } + const embeddedFiles = parentFile?.Data?.EmbeddedFiles || []; + for (let embeddedFile of embeddedFiles) { + if (embeddedFile["FileName"].value === fileName) { + let embextension = typeof (fileName) !== 'string' ? 'unknown' : `.${fileName.split('.').pop()}`; + if (!only_new || (only_new && !wkit.FileExistsInRaw(export_filename(fileName)))){ + convertEmbedded(embeddedFile); + + // add nested file to export list + + if (jsonExtensions.includes(extension)) jsonSet.add(fileName); + if (exportEmbeddedExtensions.includes(embextension)) exportSet.add(fileName); + } + return; + } + } + } + + let isHash = false; + if (typeof (fileName) === 'bigint') { + isHash = true; + fileName = fileName.toString(); + } + + if (typeof (fileName) !== 'string') { + Logger.Error('Unknown path type'); + return; + } + + let file = null; + + // Load from project if it exists, otherwise get the base game file + if (wkit.FileExistsInProject(fileName)) { + file = wkit.GetFileFromProject(fileName, OpenAs.GameFile); + } else { + file = wkit.GetFileFromBase(fileName); + } + + // Let the user know if we failed to export the file + if (file === null && !suppressLogFileOutput) { + if (!isHash) { + Logger.Warning(`Skipping invalid export ${fileName}`); + } else { + Logger.Info(`Failed to retrieve file from hash: ${fileName}`); + } + return; + } + + + + // now check if there are referenced files and parse them + if (file.Extension !== ".xbm") { + let json = TypeHelper.JsonParse(wkit.GameFileToJson(file)); + for (let path of GetPaths(json["Data"]["RootChunk"])) { + ParseFile(path, json); + } + + } +} + +//#endregion diff --git a/Scripts/RemoveOcclusionFromSectors.wscript b/Scripts/RemoveOcclusionFromSectors.wscript new file mode 100644 index 0000000..09b6f22 --- /dev/null +++ b/Scripts/RemoveOcclusionFromSectors.wscript @@ -0,0 +1,63 @@ +// Geneates .xl file that removes all occlusion meshes from all streamingsectors in the project +// Special thanks to simarilius for writing most of the code +// @author spirit +// @version 1.0 + +import * as Logger from 'Logger.wscript'; +import * as TypeHelper from 'TypeHelper.wscript'; + +let RemoverStr = "streaming:\n sectors:\n" +let sectorNames = "OcclusionRemover_" +let sectors = [] + +// Gets FileName only from path +function getFileNameWithoutExt(filePath) { + const fileNameWithExt = filePath.split("\\").pop(); // Get last segment + const ext = fileNameWithExt.lastIndexOf('.'); + return ext === -1 ? fileNameWithExt : fileNameWithExt.slice(0, ext); // Remove extension +} +// Lists all streamingsectors in Projects +for (let filename of wkit.GetProjectFiles('archive')) { + if (filename.split('.').pop() === "streamingsector") { + sectors.push(filename); + } +} +// Goes through all streamingsectors +for (const fileName of sectors) { + + let node_data_indexs=[] + if (wkit.FileExistsInProject(fileName)) { + var file = wkit.GetFileFromProject(fileName, OpenAs.GameFile); + } else { + file = wkit.GetFileFromBase(fileName); + } + var json = TypeHelper.JsonParse(wkit.GameFileToJson(file)); + let nodeData = json["Data"]["RootChunk"]["nodeData"]["Data"] + // Generates the Path and expected Nodes section + RemoverStr += " - path: "+fileName+"\n"+" expectedNodes: "+nodeData.length.toString()+"\n"+" nodeDeletions:"+"\n"; + sectorNames += ""+getFileNameWithoutExt(fileName); + + Logger.Info('Identifying Occulder Nodes in '+fileName) + let nodes = json["Data"]["RootChunk"]["nodes"]; + + // Finds all Occluders in nodes + nodes.forEach((nodeInst, index) => { + if (nodeInst["Data"]["$type"] !== null ) { + if ( nodeInst["Data"]["$type"].includes("Occluder")) { + node_data_indexs.push({"index":index.toString(), "type":nodeInst["Data"]["$type"]}) + } + } + }); + // Gets Index of nodeData and nodeType from nodes + for (let index in nodeData) { + for (let index2 in node_data_indexs) { + if (nodeData[index]["NodeIndex"]==node_data_indexs[index2]["index"]) { + RemoverStr += " - index: "+index+"\n type: "+node_data_indexs[index2]["type"]+"\n"; + } + } + } + +} +// Saves the File to Resources +wkit.SaveToResources(sectorNames+".xl", RemoverStr); +Logger.Info("Saved "+sectorNames+".xl to resources!");