public static void main(String[] args) { LogUtils.configure(); FieldTypeMap ftm = StructDatabase.getInstance().getFieldTypeMap(); Set<AssetFieldType> fieldNodes = new HashSet<>(); Set<UnityVersion> versions = new TreeSet<>(); Set<Integer> classIDs = new TreeSet<>(); for (Map.Entry<Pair<Integer, UnityVersion>, AssetFieldType> entry : ftm.entrySet()) { versions.add(entry.getKey().getRight()); classIDs.add(entry.getKey().getLeft()); fieldNodes.add(entry.getValue()); } L.log(Level.INFO, "Class IDs: {0}", classIDs.size()); L.log(Level.INFO, "Versions: {0}", versions.size()); L.log(Level.INFO, "Fields: {0}", fieldNodes.size()); System.out.println(); System.out.print(" |"); for (Integer classID : classIDs) { System.out.print(StringUtils.leftPad(String.valueOf(classID), 4)); System.out.print(" |"); } System.out.println(); System.out.print("--------|"); System.out.print(StringUtils.repeat("-----|", classIDs.size())); System.out.println(); for (UnityVersion rev : versions) { System.out.print(rev); System.out.print(" |"); for (Integer classID : classIDs) { System.out.print(" "); if (ftm.containsKey(new Pair(classID, rev))) { System.out.print("x"); } else { System.out.print(" "); } System.out.print(" |"); } System.out.println(); } System.out.println(); for (Integer classID : classIDs) { String className = ClassID.getNameForID(classID); String classIDStr = StringUtils.rightPad(String.valueOf(classID), 3); if (className == null) { System.out.printf("%s : ???\n", classIDStr); } else { System.out.printf("%s : %s\n", classIDStr, className); } } }
/** * Debug command that deserializes and writes copies of asset files for validation. * * @author Nico Bergemann <barracuda415 at yahoo.de> */ @Parameters( commandNames = "debug-asset-test", commandDescription = "Test asset file deserialization and writing.") public class DebugAssetTest extends MultiFileCommand { private static final Logger L = LogUtils.getLogger(); @Override public void handleFile(Path file) throws IOException { AssetFile asset = new AssetFile(); asset.load(file); try { testDeserialize(asset); } catch (Throwable t) { L.log( Level.WARNING, "{0} failed deserialization test: {1}", new Object[] {asset.getSourceFile(), t.getMessage()}); } try { testWrite(asset); } catch (Throwable t) { L.log( Level.WARNING, "{0} failed write test: {1}", new Object[] {asset.getSourceFile(), t.getMessage()}); } } private void testDeserialize(AssetFile asset) throws IOException { for (ObjectData obj : asset.objects()) { obj.instance(); } } private void testWrite(AssetFile asset) throws IOException { Path tmpFile = Files.createTempFile("disunity", null); try { asset.save(tmpFile); if (!FileUtils.contentEquals(asset.getSourceFile().toFile(), tmpFile.toFile())) { throw new IOException("Files are not equal"); } } finally { Files.deleteIfExists(tmpFile); } } }
/** * Extractor for asset files. * * @author Nico Bergemann <barracuda415 at yahoo.de> */ public class AssetExtractor { private static final Logger L = LogUtils.getLogger(); /** * Tries to get the name of an object by deserializing it and looking for the field "m_Name". If * it exists, return its value. * * @param asset asset file * @param path object path * @return Name string of the object or null if it doesn't have a name or if the deserialization * failed. */ public static String getObjectName(AssetFile asset, ObjectPath path) { Deserializer deser = new Deserializer(asset); String name = null; try { UnityObject obj = deser.deserialize(path); name = obj.getValue("m_Name"); } catch (OutOfMemoryError ex) { // Deserializer choked on an array size and clogged the heap, try // to clean up this mess deser = null; System.gc(); } catch (Throwable ex) { } return name; } private final AssetFile asset; private final Map<String, AssetExtractHandler> extractHandlerMap = new HashMap<>(); private ClassFilter cf; private Path outputDir; public AssetExtractor(AssetFile asset) { this.asset = asset; addHandler("AudioClip", new AudioClipHandler()); addHandler("Shader", new TextAssetHandler("shader")); addHandler("SubstanceArchive", new SubstanceArchiveHandler()); addHandler("Texture2D", new Texture2DHandler()); addHandler("Cubemap", new Texture2DHandler()); addHandler("Font", new FontHandler()); addHandler("TextAsset", new TextAssetHandler("txt")); addHandler("MovieTexture", new MovieTextureHandler()); addHandler("Mesh", new MeshHandler()); } public final void addHandler(String className, AssetExtractHandler handler) { handler.setClassName(className); extractHandlerMap.put(className, handler); } public final AssetExtractHandler getHandler(String className) { return extractHandlerMap.get(className); } public final void clearHandlers() { extractHandlerMap.clear(); } public Path getOutputDir() { return outputDir; } public void setOutputDir(Path outputDir) { this.outputDir = outputDir; } public ClassFilter getClassFilter() { return cf; } public void setClassFilter(ClassFilter cf) { this.cf = cf; } public void extract(boolean raw) throws IOException { List<ObjectPath> paths = asset.getPaths(); Deserializer deser = new Deserializer(asset); for (AssetExtractHandler extractHandler : extractHandlerMap.values()) { extractHandler.setAssetFile(asset); extractHandler.setOutputDir(outputDir); } for (ObjectPath path : paths) { // skip filtered classes if (cf != null && !cf.accept(path)) { continue; } String className = ClassID.getNameForID(path.getClassID(), true); // write just the serialized object data or parsed and extracted content? if (raw) { String assetFileName = String.format("%06d.bin", path.getPathID()); Path classDir = outputDir.resolve(className); if (Files.notExists(classDir)) { Files.createDirectories(classDir); } Path assetFile = classDir.resolve(assetFileName); L.log(Level.INFO, "Writing {0} {1}", new Object[] {className, assetFileName}); ByteBuffer bbAsset = asset.getPathBuffer(path); try { ByteBufferUtils.save(assetFile, bbAsset); } catch (Exception ex) { L.log(Level.WARNING, "Can't write " + path + " to " + assetFile, ex); } } else { AssetExtractHandler handler = getHandler(className); if (handler != null) { UnityObject obj; try { obj = deser.deserialize(path); } catch (Exception ex) { L.log(Level.WARNING, "Can't deserialize " + path, ex); continue; } try { handler.setObjectPath(path); handler.extract(obj); } catch (Exception ex) { L.log(Level.WARNING, "Can't extract " + path, ex); } } } } } public void split() throws IOException { List<ObjectPath> pathTable = asset.getPaths(); TypeTree typeTree = asset.getTypeTree(); // assets with just one object can't be split any further if (pathTable.size() == 1) { L.warning("Asset doesn't contain sub-assets!"); return; } for (ObjectPath path : pathTable) { // skip filtered classes if (cf != null && !cf.accept(path)) { continue; } String className = ClassID.getNameForID(path.getClassID(), true); AssetFile subAsset = new AssetFile(); subAsset.getHeader().setFormat(asset.getHeader().getFormat()); ObjectPath subFieldPath = new ObjectPath(); subFieldPath.setClassID1(path.getClassID1()); subFieldPath.setClassID2(path.getClassID2()); subFieldPath.setLength(path.getLength()); subFieldPath.setOffset(0); subFieldPath.setPathID(1); subAsset.getPaths().add(subFieldPath); TypeTree subTypeTree = subAsset.getTypeTree(); subTypeTree.setEngineVersion(typeTree.getEngineVersion()); subTypeTree.setVersion(-2); subTypeTree.setFormat(typeTree.getFormat()); subTypeTree.getFields().put(path.getClassID(), typeTree.getFields().get(path.getClassID())); subAsset.setDataBuffer(asset.getPathBuffer(path)); Path subAssetDir = outputDir.resolve(className); if (Files.notExists(subAssetDir)) { Files.createDirectories(subAssetDir); } // probe asset name String subAssetName = getObjectName(asset, path); if (subAssetName != null) { // remove any chars that could cause troubles on various file systems subAssetName = FilenameSanitizer.sanitizeName(subAssetName); } else { // use numeric names subAssetName = String.format("%06d", path.getPathID()); } subAssetName += ".asset"; Path subAssetFile = subAssetDir.resolve(subAssetName); if (Files.notExists(subAssetFile)) { L.log(Level.INFO, "Writing {0}", subAssetFile); subAsset.save(subAssetFile); } } } }
/** @author Nico Bergemann <barracuda415 at yahoo.de> */ public class FixReferencesAction extends Action { private static final Logger L = LogUtils.getLogger(); @Override public boolean supportsAssets() { return true; } @Override public boolean supportsAssetBundes() { return false; } @Override public void processAsset(AssetFile asset) throws IOException { Path assetFile = asset.getSourceFile(); Path assetDir = assetFile.getParent(); String assetFileName = assetFile.getFileName().toString(); String assetPath; if (assetDir == null) { assetPath = ""; } else { assetPath = assetDir.toAbsolutePath().toString(); assetPath = FilenameUtils.separatorsToUnix(assetPath) + "/"; } // fix path for all assets with (shared)assets extension boolean changed = false; for (AssetRef ref : asset.getReferences()) { Path refFile = Paths.get(ref.getFilePath()); String refExt = FilenameUtils.getExtension(refFile.getFileName().toString()); if (refExt.endsWith("assets") && Files.notExists(refFile)) { String filePathOld = ref.getFilePath(); String filePathNew = assetPath + FilenameUtils.getName(ref.getFilePath()); Path refFileNew = Paths.get(filePathNew); if (Files.exists(refFileNew)) { L.log(Level.FINE, "Fixed reference: {0} -> {1}", new Object[] {filePathOld, filePathNew}); ref.setFilePath(filePathNew); changed = true; } else { L.log(Level.FINE, "Fixed reference not found: {0}", refFileNew); } } } if (!changed) { L.fine("No references changed, skipping saving"); return; } // create backup by renaming the original file Path assetFileBackup = assetFile.resolveSibling(assetFileName + ".bak"); Files.move(assetFile, assetFileBackup, StandardCopyOption.REPLACE_EXISTING); // save asset asset.save(assetFile); } }