├── README.md └── src ├── Main.hx ├── Util.hx └── structure ├── Audio.hx ├── Chunk.hx ├── ListChunk.hx ├── Sond.hx ├── Sound.hx ├── Sprite.hx ├── Sprt.hx ├── Texture.hx └── TexturePage.hx /README.md: -------------------------------------------------------------------------------- 1 | # NOTICE: 2 | *This project is no longer being worked on and will not be updated. See this project's successor [GMS Explorer](https://github.com/puggsoy/GMS-Explorer).* 3 | 4 | # GMExtract 5 | This is a tool to extract sprites from the data.win file of games made with Game Maker Studio. 6 | Lots of credit to PoroCYon for the info [here](https://gitlab.com/snippets/14944) and Mirrawrs for the info [here](http://undertale.rawr.ws/unpacking), most of the knowledge needed for this came from them. 7 | 8 | ## Usage 9 | You can either double-click on the executable to bring up dialogues to choose the input file and output directory, or run it via the command-line: 10 | 11 | Usage: GMExtract inFile outDir [-s|-a] 12 | inFile: The data.win file to extract from 13 | outDir: The folder to save the extracted files to 14 | -s: Only extract sprites 15 | -a: Only extract audio 16 | 17 | Note that if run using the first method it will extract both sprites and audio. 18 | 19 | ## Compilation 20 | The code should be compiled as a Neko or C++ command-line program (only tested in Neko). 21 | -------------------------------------------------------------------------------- /src/Main.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import Util.*; 4 | import easyconsole.Begin; 5 | import easyconsole.End; 6 | import haxe.io.Path; 7 | import structure.Sond; 8 | import structure.Sprt; 9 | import sys.FileSystem; 10 | import sys.io.File; 11 | import sys.io.FileInput; 12 | import sys.io.FileSeek; 13 | import systools.Dialogs; 14 | 15 | class Main 16 | { 17 | private var inFile:String; 18 | private var outDir:String; 19 | 20 | private var sprites:Bool = false; 21 | private var audio:Bool = false; 22 | 23 | static function main() 24 | { 25 | new Main(); 26 | } 27 | 28 | public function new() 29 | { 30 | Sys.println('GMExtract v1.1 - by puggsoy\n'); 31 | 32 | var pName:String = Path.withoutExtension(Path.withoutDirectory(Sys.executablePath())); 33 | Begin.init(); 34 | Begin.usage = 'Usage: $pName inFile outDir [-s|-a]]\n inFile: The data.win file to extract from\n outDir: The folder to save the extracted files to\n -s: Only extract sprites\n -a: Only extract audio\n'; 35 | Begin.functions = [useUI, null, checkArgs]; 36 | Begin.parseArgs(); 37 | } 38 | 39 | private function checkArgs() 40 | { 41 | inFile = Begin.args[0]; 42 | 43 | if (!FileSystem.exists(inFile)) 44 | { 45 | End.terminate(1, "inFile must exist!"); 46 | } 47 | 48 | outDir = Begin.args[1]; 49 | 50 | if (FileSystem.exists(outDir) && !FileSystem.isDirectory(outDir)) 51 | { 52 | End.terminate(1, "outDir must be a directory!"); 53 | } 54 | 55 | checkOptions(Begin.args.slice(2)); 56 | 57 | outDir = Path.addTrailingSlash(outDir); 58 | 59 | //Start working 60 | extract(); 61 | 62 | End.anyKeyExit(0, "Done"); 63 | } 64 | 65 | private function useUI() 66 | { 67 | Sys.println(Begin.usage); 68 | 69 | var filters:FILEFILTERS = 70 | { 71 | count: 1, 72 | descriptions: ['.win files'], 73 | extensions: ['*.win'] 74 | }; 75 | 76 | inFile = Dialogs.openFile('Select data.win file', 'Select data.win file', filters)[0]; 77 | 78 | if (inFile == null) 79 | { 80 | End.terminate(1, 'No input file chosen!'); 81 | } 82 | 83 | outDir = Dialogs.folder('Select output directory...', 'Select output directory...'); 84 | 85 | if (outDir == null) 86 | { 87 | End.terminate(1, 'No output directory chosen!'); 88 | } 89 | 90 | sprites = audio = true; 91 | 92 | extract(); 93 | 94 | End.anyKeyExit(0, "Done"); 95 | } 96 | 97 | private function checkOptions(options:Array) 98 | { 99 | for (o in options) 100 | { 101 | switch(o) 102 | { 103 | case '-s': 104 | sprites = true; 105 | case '-a': 106 | audio = true; 107 | } 108 | } 109 | 110 | if (!sprites && !audio) 111 | sprites = audio = true; 112 | } 113 | 114 | private function extract() 115 | { 116 | var f:FileInput = File.read(inFile); 117 | f.bigEndian = false; 118 | f.seek(0, FileSeek.SeekEnd); 119 | var fLen:Int = f.tell(); 120 | f.seek(0, FileSeek.SeekBegin); 121 | 122 | if (sprites) 123 | { 124 | Sys.println('Extracting sprites...'); 125 | 126 | var sp:Sprt = Sprt.read(f); 127 | sp.extractAll(f, Path.join([outDir, 'sprites'])); 128 | } 129 | 130 | if (audio) 131 | { 132 | Sys.println('Extracting audio...'); 133 | 134 | var s:Sond = Sond.read(f); 135 | s.extractAll(f, Path.join([outDir, 'audio'])); 136 | } 137 | 138 | f.close(); 139 | } 140 | } -------------------------------------------------------------------------------- /src/Util.hx: -------------------------------------------------------------------------------- 1 | package; 2 | import format.png.Data; 3 | import format.png.Tools; 4 | import format.png.Writer; 5 | import haxe.ds.GenericStack; 6 | import haxe.io.Bytes; 7 | import haxe.io.Path; 8 | import openfl.display.BitmapData; 9 | import sys.FileSystem; 10 | import sys.io.File; 11 | import sys.io.FileInput; 12 | import sys.io.FileOutput; 13 | import sys.io.FileSeek; 14 | 15 | /** 16 | * This contains a bunch of utility functions that are either used in multiple places or are just nicer to have separately. 17 | */ 18 | class Util 19 | { 20 | /** 21 | * Stack of locations to jump back to. 22 | */ 23 | static private var jumpStack:GenericStack = new GenericStack(); 24 | 25 | /** 26 | * Jumps to an offset in a FileInput, storing the current position. Don't forget to jumpBack() afterwards! 27 | * @param f The FileInput to jump through 28 | * @param o The offset to jump to 29 | */ 30 | static public function jump(f:FileInput, o:Int) 31 | { 32 | jumpStack.add(f.tell()); 33 | f.seek(o, FileSeek.SeekBegin); 34 | } 35 | 36 | /** 37 | * Jumps back to the last position stored from a jump() call. 38 | * @param f The FileInput to jump through 39 | */ 40 | static public function jumpBack(f:FileInput) 41 | { 42 | if (jumpStack.isEmpty()) throw 'No jump back address'; 43 | 44 | f.seek(jumpStack.pop(), FileSeek.SeekBegin); 45 | } 46 | 47 | /** 48 | * Finds the offset of the given chunk. 49 | * @param f The FileInput to find the chunk in 50 | * @param chnkNm 4-character header of the chunk to find 51 | * @return The offset of the chunk 52 | */ 53 | static public function findChunk(f:FileInput, chnkNm:String):Int 54 | { 55 | jump(f, 0); 56 | 57 | var chunk:String; 58 | do 59 | { 60 | chunk = f.readString(4); 61 | var len:Int = f.readInt32(); 62 | 63 | if (chunk == 'FORM') continue; 64 | if (chunk == chnkNm) break; 65 | 66 | f.seek(len, FileSeek.SeekCur); 67 | } 68 | while (!f.eof()); 69 | 70 | var ret:Int = f.tell() - 8; 71 | 72 | jumpBack(f); 73 | return ret; 74 | } 75 | 76 | /** 77 | * Reads a string in the given FileInput. Removes the need to manually check the length. 78 | * @param f The FileInput to read from 79 | * @param o The offset of the string 80 | * @return The read string 81 | */ 82 | static public function getString(f:FileInput, o:Int):String 83 | { 84 | jump(f, o - 4); 85 | 86 | var len:Int = f.readInt32(); 87 | var r:String = f.readString(len); 88 | 89 | jumpBack(f); 90 | 91 | return r; 92 | } 93 | 94 | /** 95 | * Checks the size of a PNG file in a FileInput. 96 | * @param f The FileInput to read from 97 | * @param o The offset the PNG is at. If ommitted, simply reads from the current location 98 | * @return The size of the PNG 99 | */ 100 | static public function getPNGSize(f:FileInput, ?o:Int):Int 101 | { 102 | if (o == null) o = f.tell(); 103 | jump(f, o); 104 | var be:Bool = f.bigEndian; 105 | f.bigEndian = true; 106 | 107 | var fst:Int = f.readInt32(); 108 | var scd:Int = f.readInt32(); 109 | if (fst != 0x89504E47 || scd != 0x0D0A1A0A) throw 'Invalid PNG file! Offset: ${f.tell()}'; 110 | 111 | var chunk:String; 112 | var lenTotal:Int = 8; 113 | 114 | do 115 | { 116 | var chunkLen:Int = f.readInt32(); 117 | chunk = f.readString(4); 118 | f.seek(chunkLen, FileSeek.SeekCur); 119 | f.readInt32(); //CRC 120 | lenTotal += chunkLen + 12; 121 | } 122 | while (chunk != 'IEND'); 123 | 124 | f.bigEndian = be; 125 | jumpBack(f); 126 | 127 | return lenTotal; 128 | } 129 | 130 | /** 131 | * Saves a BitmapData to a PNG file. 132 | * @param path The path of the resulting file 133 | * @param bmp The BitmapData to save 134 | */ 135 | static public function savePNG(path:String, bmp:BitmapData) 136 | { 137 | if(Path.directory(path) != '') FileSystem.createDirectory(Path.directory(path)); 138 | 139 | var dat:Data = Tools.build32ARGB(bmp.width, bmp.height, Bytes.ofData(bmp.getPixels(bmp.rect))); 140 | var o:FileOutput = File.write(path); 141 | new Writer(o).write(dat); 142 | o.close(); 143 | } 144 | 145 | /** 146 | * Saves bytes to a file. 147 | * @param path The path of the resulting file 148 | * @param b The bytes to save 149 | */ 150 | static public function saveBytes(path:String, b:Bytes) 151 | { 152 | if (Path.directory(path) != '') FileSystem.createDirectory(Path.directory(path)); 153 | 154 | var fo:FileOutput = File.write(path); 155 | fo.write(b); 156 | fo.close(); 157 | } 158 | } -------------------------------------------------------------------------------- /src/structure/Audio.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import haxe.io.Bytes; 4 | import sys.io.FileInput; 5 | import sys.io.FileSeek; 6 | 7 | class Audio 8 | { 9 | static public function get(f:FileInput, index:Int):Bytes 10 | { 11 | jump(f, findChunk(f, 'AUDO')); 12 | 13 | f.readInt32(); //name 14 | f.readInt32(); //length 15 | 16 | var offsetCount:Int = f.readInt32(); 17 | 18 | if (index >= offsetCount || index < 0) throw 'Invalid audio index: $index'; 19 | 20 | f.seek(4 * index, FileSeek.SeekCur); //get offset for this index 21 | f.seek(f.readInt32(), FileSeek.SeekBegin); //go to offset 22 | var wavLen:Int = f.readInt32(); 23 | var ret:Bytes = f.read(wavLen); 24 | 25 | jumpBack(f); 26 | 27 | return ret; 28 | } 29 | } -------------------------------------------------------------------------------- /src/structure/Chunk.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import sys.io.FileInput; 4 | 5 | class Chunk 6 | { 7 | static public function read(f:FileInput, ?offset:Int):Chunk 8 | { 9 | if(offset != null) jump(f, offset); 10 | 11 | var name:String = f.readString(4); 12 | 13 | var c:Chunk = 14 | switch(name) 15 | { 16 | case 'SPRT': 17 | Sprt.read(f, offset); 18 | default: 19 | null; 20 | } 21 | 22 | if (offset != null) jumpBack(f); 23 | return c; 24 | } 25 | 26 | public var name(default, null):String; 27 | private var length:Int; 28 | 29 | public function new(name:String, length:Int) 30 | { 31 | this.name = name; 32 | this.length = length; 33 | } 34 | } -------------------------------------------------------------------------------- /src/structure/ListChunk.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import haxe.ds.Vector; 3 | import structure.Chunk; 4 | 5 | class ListChunk extends structure.Chunk 6 | { 7 | private var offsetCount:Int; 8 | private var offsets:Vector; 9 | private var objects:Vector; 10 | 11 | public function new(name:String, length:Int, offsetCount:Int) 12 | { 13 | super(name, length); 14 | 15 | this.offsetCount = offsetCount; 16 | offsets = new Vector(offsetCount); 17 | objects = new Vector(offsetCount); 18 | } 19 | } -------------------------------------------------------------------------------- /src/structure/Sond.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import structure.ListChunk; 4 | import sys.io.FileInput; 5 | 6 | class Sond extends structure.ListChunk 7 | { 8 | static public function read(f:FileInput, ?offset:Int):Sond 9 | { 10 | if (offset != null) jump(f, offset); 11 | else jump(f, findChunk(f, 'SOND')); 12 | 13 | var sond:Sond = new Sond(f.readString(4), f.readInt32(), f.readInt32()); 14 | 15 | if (sond.name != 'SOND') throw 'Trying to read SPRT at incorrect offset!'; 16 | 17 | for (i in 0...sond.offsetCount) 18 | { 19 | sond.offsets[i] = f.readInt32(); 20 | sond.objects[i] = Sound.read(f, sond.offsets[i]); 21 | } 22 | 23 | if (offset != null) jumpBack(f); 24 | return sond; 25 | } 26 | 27 | public function new(name:String, length:Int, offsetCount:Int) 28 | { 29 | super(name, length, offsetCount); 30 | } 31 | 32 | public function extractAll(f:FileInput, outDir:String) 33 | { 34 | for (i in 0...offsetCount) 35 | { 36 | objects[i].extract(f, outDir); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/structure/Sound.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import haxe.io.Bytes; 4 | import haxe.io.Path; 5 | import sys.io.FileInput; 6 | 7 | class Sound 8 | { 9 | static public function read(f:FileInput, ?offset:Int):Sound 10 | { 11 | if (offset != null) jump(f, offset); 12 | 13 | var s:Sound = new Sound(); 14 | 15 | s.name = getString(f, f.readInt32()); 16 | s.flags = f.readInt32(); 17 | s.type = getString(f, f.readInt32()); 18 | s.file = getString(f, f.readInt32()); 19 | s.unk = f.readInt32(); 20 | s.volume = f.readFloat(); 21 | s.pitch = f.readFloat(); 22 | s.groupID = f.readInt32(); 23 | s.audioID = f.readInt32(); 24 | 25 | if (offset != null) jumpBack(f); 26 | 27 | return s; 28 | } 29 | 30 | private var name:String; 31 | private var flags:Int; 32 | private var type:String; 33 | private var file:String; 34 | private var unk:Int; 35 | private var volume:Float; 36 | private var pitch:Float; 37 | private var groupID:Int; 38 | private var audioID:Int; 39 | 40 | public function new() {} 41 | 42 | public function extract(f:FileInput, outDir:String) 43 | { 44 | if (flags & 1 == 0) return; 45 | 46 | var outPath:String = Path.join([outDir, file]); 47 | var audio:Bytes = Audio.get(f, audioID); 48 | 49 | Sys.println('Saving $outPath'); 50 | saveBytes(outPath, audio); 51 | } 52 | } -------------------------------------------------------------------------------- /src/structure/Sprite.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import haxe.ds.Vector; 4 | import haxe.io.Path; 5 | import sys.io.FileInput; 6 | 7 | class Sprite 8 | { 9 | static public function read(f:FileInput, ?offset:Int):Sprite 10 | { 11 | if (offset != null) jump(f, offset); 12 | 13 | var s:Sprite = new Sprite(); 14 | 15 | s.name = getString(f, f.readInt32()); 16 | s.width = f.readInt32(); 17 | s.height = f.readInt32(); 18 | s.left = f.readInt32(); 19 | s.right = f.readInt32(); 20 | s.bottom = f.readInt32(); 21 | s.top = f.readInt32(); 22 | s.unk = new Vector(3); s.unk[0] = f.readInt32(); s.unk[1] = f.readInt32(); s.unk[2] = f.readInt32(); 23 | s.bBoxMode = f.readInt32(); 24 | s.sepMasks = f.readInt32(); 25 | s.originX = f.readInt32(); 26 | s.originY = f.readInt32(); 27 | s.textureCount = f.readInt32(); 28 | s.textureOffsets = new Vector(s.textureCount); 29 | 30 | for (i in 0...s.textureCount) 31 | { 32 | s.textureOffsets[i] = f.readInt32(); 33 | } 34 | 35 | if (offset != null) jumpBack(f); 36 | 37 | return s; 38 | } 39 | 40 | private var name:String; 41 | private var width:Int; 42 | private var height:Int; 43 | private var left:Int; 44 | private var right:Int; 45 | private var bottom:Int; 46 | private var top:Int; 47 | private var unk:Vector; 48 | private var bBoxMode:Int; 49 | private var sepMasks:Int; 50 | private var originX:Int; 51 | private var originY:Int; 52 | private var textureCount:Int; 53 | private var textureOffsets:Vector; 54 | 55 | public function new() {} 56 | 57 | public function extract(f:FileInput, outDir:String) 58 | { 59 | for (i in 0...textureCount) 60 | { 61 | var outPath:String = Path.join([outDir, name, name + '_$i.png']); 62 | var tpag:TexturePage = TexturePage.read(f, textureOffsets[i]); 63 | 64 | Sys.println('Saving $outPath'); 65 | savePNG(outPath, tpag.getBitmap(f)); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/structure/Sprt.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import structure.ListChunk; 4 | import sys.io.FileInput; 5 | 6 | class Sprt extends structure.ListChunk 7 | { 8 | static public function read(f:FileInput, ?offset:Int):Sprt 9 | { 10 | if (offset != null) jump(f, offset); 11 | else jump(f, findChunk(f, 'SPRT')); 12 | 13 | var sprt:Sprt = new Sprt(f.readString(4), f.readInt32(), f.readInt32()); 14 | 15 | if (sprt.name != 'SPRT') throw 'Trying to read SPRT at incorrect offset!'; 16 | 17 | for (i in 0...sprt.offsetCount) 18 | { 19 | sprt.offsets[i] = f.readInt32(); 20 | sprt.objects[i] = Sprite.read(f, sprt.offsets[i]); 21 | } 22 | 23 | if (offset != null) jumpBack(f); 24 | return sprt; 25 | } 26 | 27 | public function new(name:String, length:Int, offsetCount:Int) 28 | { 29 | super(name, length, offsetCount); 30 | } 31 | 32 | public function extractAll(f:FileInput, outDir:String) 33 | { 34 | for (i in 0...offsetCount) 35 | { 36 | objects[i].extract(f, outDir); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/structure/Texture.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import haxe.ds.Vector; 4 | import haxe.io.Bytes; 5 | import openfl.display.BitmapData; 6 | import openfl.utils.ByteArray; 7 | import sys.io.FileInput; 8 | import sys.io.FileSeek; 9 | 10 | class Texture 11 | { 12 | static private var textures:Vector; 13 | 14 | static private function loadTexture(f:FileInput, index:Int) 15 | { 16 | jump(f, findChunk(f, 'TXTR')); 17 | 18 | f.readInt32(); //name 19 | f.readInt32(); //length 20 | 21 | var offsetCount:Int = f.readInt32(); 22 | 23 | if (index >= offsetCount || index < 0) throw 'Invalid texture index: $index'; 24 | if (textures == null) textures = new Vector(offsetCount); 25 | 26 | f.seek(4 * index, FileSeek.SeekCur); //get offset for this index 27 | f.seek(f.readInt32() + 4, FileSeek.SeekBegin); //go to the blob offset 28 | f.seek(f.readInt32(), FileSeek.SeekBegin); //go to PNG blob 29 | 30 | var pngLen:Int = Util.getPNGSize(f); 31 | var bytes:Bytes = Bytes.alloc(pngLen); 32 | f.readBytes(bytes, 0, pngLen); 33 | textures[index] = BitmapData.fromBytes(ByteArray.fromBytes(bytes)); 34 | 35 | jumpBack(f); 36 | } 37 | 38 | static public function get(f:FileInput, index:Int):BitmapData 39 | { 40 | if (textures == null || textures[index] == null) loadTexture(f, index); 41 | 42 | return textures[index]; 43 | } 44 | } -------------------------------------------------------------------------------- /src/structure/TexturePage.hx: -------------------------------------------------------------------------------- 1 | package structure; 2 | import Util.*; 3 | import openfl.display.BitmapData; 4 | import openfl.geom.Point; 5 | import openfl.geom.Rectangle; 6 | import sys.io.FileInput; 7 | 8 | class TexturePage 9 | { 10 | static public function read(f:FileInput, ?offset:Int):TexturePage 11 | { 12 | if (offset != null) jump(f, offset); 13 | 14 | var t:TexturePage = new TexturePage(); 15 | 16 | t.x = f.readInt16(); 17 | t.y = f.readInt16(); 18 | t.width = f.readInt16(); 19 | t.height = f.readInt16(); 20 | t.renderX = f.readInt16(); 21 | t.renderY = f.readInt16(); 22 | t.boundingX = f.readInt16(); 23 | t.boundingY = f.readInt16(); 24 | t.boundingWidth = f.readInt16(); 25 | t.boundingHeight = f.readInt16(); 26 | t.spriteSheetID = f.readInt16(); 27 | 28 | if (offset != null) jumpBack(f); 29 | 30 | return t; 31 | } 32 | 33 | private var x:Int; 34 | private var y:Int; 35 | private var width:Int; 36 | private var height:Int; 37 | private var renderX:Int; 38 | private var renderY:Int; 39 | private var boundingX:Int; 40 | private var boundingY:Int; 41 | private var boundingWidth:Int; 42 | private var boundingHeight:Int; 43 | private var spriteSheetID:Int; 44 | 45 | public function new() {} 46 | 47 | public function getBitmap(f:FileInput):BitmapData 48 | { 49 | var source:BitmapData = Texture.get(f, spriteSheetID); 50 | var ret:BitmapData = new BitmapData(boundingWidth, boundingHeight, true, 0); 51 | ret.copyPixels(source, new Rectangle(x, y, width, height), new Point(renderX, renderY)); 52 | 53 | return ret; 54 | } 55 | } --------------------------------------------------------------------------------