using System; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using System.Drawing; using System.Reflection; using UndertaleModLib.Models; using UndertaleModLib.Util; using UndertaleModLib.Decompiler; int progress = 0; string projFolder = GetFolder(FilePath) + "Export_Project" + Path.DirectorySeparatorChar; TextureWorker worker = new TextureWorker(); ThreadLocal DECOMPILE_CONTEXT = new ThreadLocal(() => new DecompileContext(Data, false)); string gmxDeclaration = "This Document is generated by GameMaker, if you edit it by hand then you do so at your own risk!"; // if (Directory.Exists(projFolder)) // { // ScriptError("A project export already exists. Please remove it.", "Error"); // return; // } Directory.CreateDirectory(projFolder); // --------------- Start exporting --------------- var resourceNum = Data.Sprites.Count + Data.Backgrounds.Count + Data.GameObjects.Count + Data.Rooms.Count + Data.Sounds.Count + Data.Scripts.Count + Data.Fonts.Count + Data.Paths.Count + Data.Timelines.Count; // // Export sprites // await ExportSprites(); // // Export backgrounds // await ExportBackground(); // // Export objects // await ExportGameObjects(); // Export rooms await ExportRooms(); // Export sounds await ExportSounds(); // Export scripts await ExportScripts(); // Export fonts await ExportFonts(); // Export paths await ExportPaths(); // Export timelines await ExportTimelines(); // Generate project file GenerateProjectFile(); // --------------- Export completed --------------- worker.Cleanup(); HideProgressBar(); ScriptMessage("Export Complete.\n\nLocation: " + projFolder); string GetFolder(string path) { return Path.GetDirectoryName(path) + Path.DirectorySeparatorChar; } string BoolToString(bool value) { // In the GMX file, -1 is true and 0 is false. return value ? "-1" : "0"; } // --------------- Export Sprite --------------- async Task ExportSprites() { Directory.CreateDirectory(projFolder + "/sprites/images"); await Task.Run(() => Parallel.ForEach(Data.Sprites, ExportSprite)); } void ExportSprite(UndertaleSprite sprite) { UpdateProgressBar(null, $"Exporting sprite: {sprite.Name.Content}", progress++, resourceNum); // Save the sprite GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("sprite", new XElement("type", "0"), new XElement("xorig", sprite.OriginX.ToString()), new XElement("yorigin", sprite.OriginY.ToString()), new XElement("colkind", sprite.BBoxMode.ToString()), new XElement("coltolerance", "0"), new XElement("sepmasks", sprite.SepMasks.ToString("D")), new XElement("bboxmode", sprite.BBoxMode.ToString()), new XElement("bbox_left", sprite.MarginLeft.ToString()), new XElement("bbox_right", sprite.MarginRight.ToString()), new XElement("bbox_top", sprite.MarginTop.ToString()), new XElement("bbox_bottom", sprite.MarginBottom.ToString()), new XElement("HTile", "0"), new XElement("VTile", "0"), new XElement("TextureGroups", new XElement("TextureGroup0", "0") ), new XElement("For3D", "0"), new XElement("width", sprite.Width.ToString()), new XElement("height", sprite.Height.ToString()), new XElement("frames"), new XElement("bbox_right", sprite.MarginRight.ToString()) ) ); for (int i = 0; i < sprite.Textures.Count; i++) { if (sprite.Textures[i]?.Texture != null) { gmx.Element("sprite").Element("frames").Add( new XElement( "frame", new XAttribute("index", i.ToString()), "images\\" + sprite.Name.Content + "_" + i + ".png" ) ); } } File.WriteAllText(projFolder + "/sprites/" + sprite.Name.Content + ".sprite.gmx", gmx.ToString()); // Save sprite images for (int i = 0; i < sprite.Textures.Count; i++) { if (sprite.Textures[i]?.Texture != null) { // Fix sprite size var bitmapNew = new Bitmap((int)sprite.Width, (int)sprite.Height); var bitmapOrigin = worker.GetTextureFor(sprite.Textures[i].Texture, Path.GetFileNameWithoutExtension(projFolder + "/sprites/images/" + sprite.Name.Content + "_" + i + ".png")); //worker.ExportAsPNG(sprite.Textures[i].Texture, projFolder + "/sprites/images/" + sprite.Name.Content + "_" + i + ".png"); var g = Graphics.FromImage(bitmapNew); g.DrawImage(bitmapOrigin, (int)sprite.Textures[i].Texture.TargetX, (int)sprite.Textures[i].Texture.TargetY); bitmapNew.Save(projFolder + "/sprites/images/" + sprite.Name.Content + "_" + i + ".png"); bitmapNew.Dispose(); bitmapOrigin.Dispose(); } } } // --------------- Export Background --------------- async Task ExportBackground() { Directory.CreateDirectory(projFolder + "/background/images"); await Task.Run(() => Parallel.ForEach(Data.Backgrounds, ExportBackground)); } void ExportBackground(UndertaleBackground background) { UpdateProgressBar(null, $"Exporting background: {background.Name.Content}", progress++, resourceNum); // Save the backgound GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("background", new XElement("istileset", "-1"), new XElement("tilewidth", background.Texture.BoundingWidth.ToString()), new XElement("tileheight", background.Texture.BoundingHeight.ToString()), new XElement("tilexoff", "0"), new XElement("tileyoff", "0"), new XElement("tilehsep", "0"), new XElement("tilevsep", "0"), new XElement("HTile", "-1"), new XElement("VTile", "-1"), new XElement("TextureGroups", new XElement("TextureGroup0", "0") ), new XElement("For3D", "0"), new XElement("width", background.Texture.BoundingWidth.ToString()), new XElement("height", background.Texture.BoundingHeight.ToString()), new XElement("data", "images\\" + background.Name.Content + ".png") ) ); File.WriteAllText(projFolder + "/background/" + background.Name.Content + ".background.gmx", gmx.ToString()); // Save background images worker.ExportAsPNG(background.Texture, projFolder + "/background/images/" + background.Name.Content + ".png"); } // --------------- Export Object --------------- async Task ExportGameObjects() { Directory.CreateDirectory(projFolder + "/objects"); await Task.Run(() => Parallel.ForEach(Data.GameObjects, ExportGameObject)); } void ExportGameObject(UndertaleGameObject gameObject) { UpdateProgressBar(null, $"Exporting object: {gameObject.Name.Content}", progress++, resourceNum); // Save the object GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("object", new XElement("spriteName", gameObject.Sprite is null ? "" : gameObject.Sprite.Name.Content), new XElement("solid", BoolToString(gameObject.Solid)), new XElement("visible", BoolToString(gameObject.Visible)), new XElement("depth", gameObject.Depth.ToString()), new XElement("persistent", BoolToString(gameObject.Persistent)), new XElement("parentName", gameObject.ParentId is null ? "" : gameObject.ParentId.Name.Content), new XElement("maskName", gameObject.TextureMaskId is null ? "" : gameObject.TextureMaskId.Name.Content), new XElement("events") ) ); // Traversing the event type list for (int i = 0; i < gameObject.Events.Count; i++) { // Determine if an event is empty if (gameObject.Events[i].Count > 0) { // Traversing event list foreach (var j in gameObject.Events[i]) { var eventsNode = gmx.Element("object").Element("events"); var eventNode = new XElement("event", new XAttribute("eventtype", i.ToString()) ); if (j.EventSubtype == 4) { // To get the actual name of the collision object when the event type is a collision event eventNode.Add(new XAttribute("ename", Data.GameObjects[(int)j.EventSubtype].Name.Content)); } else { // Get the sub-event number directly eventNode.Add(new XAttribute("enumb", j.EventSubtype.ToString())); } // Save action var actionNode = new XElement("action"); // Traversing the action list foreach (var k in j.Actions) { actionNode.Add( new XElement("libid", k.LibID.ToString()), new XElement("id", k.ID.ToString()), new XElement("kind", k.Kind.ToString()), new XElement("userelative", BoolToString(k.UseRelative)), new XElement("isquestion", BoolToString(k.IsQuestion)), new XElement("useapplyto", BoolToString(k.UseApplyTo)), new XElement("exetype", k.ExeType.ToString()), new XElement("functionname", k.ActionName.Content), new XElement("codestring", ""), new XElement("whoName", "self"), new XElement("relative", BoolToString(k.Relative)), new XElement("isnot", BoolToString(k.IsNot)), new XElement("arguments", new XElement("argument", new XElement("kind", "1"), new XElement("string", k.CodeId != null ? Decompiler.Decompile(k.CodeId, DECOMPILE_CONTEXT.Value) : "") ) ) ); } eventNode.Add(actionNode); eventsNode.Add(eventNode); // TODO:Physics } } } File.WriteAllText(projFolder + "/objects/" + gameObject.Name.Content + ".object.gmx", gmx.ToString()); } // --------------- Export Room --------------- async Task ExportRooms() { Directory.CreateDirectory(projFolder + "/rooms"); await Task.Run(() => Parallel.ForEach(Data.Rooms, ExportRoom)); } void ExportRoom(UndertaleRoom room) { UpdateProgressBar(null, $"Exporting room: {room.Name.Content}", progress++, resourceNum); // Save the room GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("room", new XElement("caption", room.Caption.Content), new XElement("width", room.Width.ToString()), new XElement("height", room.Height.ToString()), new XElement("vsnap", "32"), new XElement("hsnap", "32"), new XElement("isometric", "0"), new XElement("speed", room.Speed.ToString()), new XElement("persistent", BoolToString(room.Persistent)), new XElement("colour", room.BackgroundColor.ToString()), new XElement("code", room.CreationCodeId != null ? Decompiler.Decompile(room.CreationCodeId, DECOMPILE_CONTEXT.Value) : ""), new XElement("enableViews", BoolToString(room.Flags.HasFlag(UndertaleRoom.RoomEntryFlags.EnableViews))), new XElement("clearViewBackground", BoolToString(room.Flags.HasFlag(UndertaleRoom.RoomEntryFlags.ShowColor))), new XElement("clearDisplayBuffer", BoolToString(room.Flags.HasFlag(UndertaleRoom.RoomEntryFlags.ClearDisplayBuffer))) ) ); // TODO:MakerSettings // Room backgrounds var backgroundsNode = new XElement("backgrounds"); foreach (var i in room.Backgrounds) { var backgroundNode = new XElement("background", new XAttribute("visible", BoolToString(i.Enabled)), new XAttribute("foreground", BoolToString(i.Foreground)), new XAttribute("name", i.BackgroundDefinition is null ? "" : i.BackgroundDefinition.Name.Content), new XAttribute("x", i.X.ToString()), new XAttribute("y", i.Y.ToString()), new XAttribute("htiled", i.TileX.ToString()), new XAttribute("vtiled", i.TileY.ToString()), new XAttribute("hspeed", i.SpeedX.ToString()), new XAttribute("vspeed", i.SpeedY.ToString()), new XAttribute("stretch", "0") ); backgroundsNode.Add(backgroundNode); } gmx.Element("room").Add(backgroundsNode); // Room views var viewsNode = new XElement("views"); foreach (var i in room.Views) { var viewNode = new XElement("view", new XAttribute("visible", BoolToString(i.Enabled)), new XAttribute("objName", i.ObjectId is null ? "" : i.ObjectId.Name.Content), new XAttribute("xview", i.ViewX.ToString()), new XAttribute("yview", i.ViewY.ToString()), new XAttribute("wview", i.ViewHeight.ToString()), new XAttribute("xport", i.PortX.ToString()), new XAttribute("yport", i.PortY.ToString()), new XAttribute("wport", i.PortWidth.ToString()), new XAttribute("hport", i.PortHeight.ToString()), new XAttribute("hborder", i.BorderX.ToString()), new XAttribute("vborder", i.BorderY.ToString()), new XAttribute("hspeed", i.SpeedX.ToString()), new XAttribute("vspeed", i.SpeedY.ToString()) ); viewsNode.Add(viewNode); } gmx.Element("room").Add(viewsNode); // Room instances var instancesNode = new XElement("instances"); foreach (var i in room.GameObjects) { var instanceNode = new XElement("instance", new XAttribute("objName", i.ObjectDefinition.Name.Content), new XAttribute("x", i.X.ToString()), new XAttribute("y", i.Y.ToString()), new XAttribute("name", "inst_" + i.InstanceID.ToString("X")), new XAttribute("locked", "0"), new XAttribute("code", i.CreationCode != null ? Decompiler.Decompile(i.CreationCode, DECOMPILE_CONTEXT.Value) : ""), new XAttribute("scaleX", i.ScaleX.ToString()), new XAttribute("scaleY", i.ScaleY.ToString()), new XAttribute("colour", i.Color.ToString()), new XAttribute("rotation", i.Rotation.ToString()) ); instancesNode.Add(instanceNode); } gmx.Element("room").Add(instancesNode); // Room tiles var tilesNode = new XElement("tiles"); foreach (var i in room.Tiles) { var tileNode = new XElement("tile", new XAttribute("bgName", i.BackgroundDefinition is null ? "" : i.BackgroundDefinition.Name.Content), new XAttribute("x", i.X.ToString()), new XAttribute("y", i.Y.ToString()), new XAttribute("w", i.Width.ToString()), new XAttribute("h", i.Height.ToString()), new XAttribute("xo", i.SourceX.ToString()), new XAttribute("yo", i.SourceY.ToString()), new XAttribute("id", i.InstanceID.ToString()), new XAttribute("name", "inst_" + i.InstanceID.ToString("X")), new XAttribute("depth", i.TileDepth.ToString()), new XAttribute("locked", "0"), new XAttribute("colour", i.Color.ToString()), new XAttribute("scaleX", i.ScaleX.ToString()), new XAttribute("scaleY", i.ScaleY.ToString()) ); tilesNode.Add(tileNode); } gmx.Element("room").Add(tilesNode); // TODO:Room physics File.WriteAllText(projFolder + "/rooms/" + room.Name.Content + ".room.gmx", gmx.ToString()); } // --------------- Export Sound --------------- async Task ExportSounds() { Directory.CreateDirectory(projFolder + "/sound/audio"); await Task.Run(() => Parallel.ForEach(Data.Sounds, ExportSound)); } void ExportSound(UndertaleSound sound) { UpdateProgressBar(null, $"Exporting sound: {sound.Name.Content}", progress++, resourceNum); // Save the sound GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("sound", new XElement("kind", Path.GetExtension(sound.File.Content) == ".ogg" ? "3" : "0"), new XElement("extension", Path.GetExtension(sound.File.Content)), new XElement("origname", "sound\\audio\\" + sound.File.Content), new XElement("effects", sound.Effects.ToString()), new XElement("volume", sound.Volume.ToString()), new XElement("pan", "0"), new XElement("bitRates", "192"), new XElement("sampleRates", new XElement("sampleRate", "44100") ), new XElement("types", new XElement("type", "1") ), new XElement("bitDepths", new XElement("bitDepth", "16") ), new XElement("preload", "-1"), new XElement("data", Path.GetFileName(sound.File.Content)), new XElement("compressed", Path.GetExtension(sound.File.Content) == ".ogg" ? "1" : "0"), new XElement("streamed", Path.GetExtension(sound.File.Content) == ".ogg" ? "1" : "0"), new XElement("uncompressOnLoad", "0"), new XElement("audioGroup", "0") ) ); File.WriteAllText(projFolder + "/sound/" + sound.Name.Content + ".sound.gmx", gmx.ToString()); // Save sound files if (sound.AudioFile != null) File.WriteAllBytes(projFolder + "/sound/audio/" + sound.File.Content, sound.AudioFile.Data); } // --------------- Export Script --------------- async Task ExportScripts() { Directory.CreateDirectory(projFolder + "/scripts/"); await Task.Run(() => Parallel.ForEach(Data.Scripts, ExportScript)); } void ExportScript(UndertaleScript script) { UpdateProgressBar(null, $"Exporting script: {script.Name.Content}", progress++, resourceNum); // Save GML files File.WriteAllText(projFolder + "/scripts/" + script.Name.Content + ".gml", (script.Code != null ? Decompiler.Decompile(script.Code, DECOMPILE_CONTEXT.Value) : "")); } // --------------- Export Font --------------- async Task ExportFonts() { Directory.CreateDirectory(projFolder + "/fonts/"); await Task.Run(() => Parallel.ForEach(Data.Fonts, ExportFont)); } void ExportFont(UndertaleFont font) { UpdateProgressBar(null, $"Exporting font: {font.Name.Content}", progress++, resourceNum); // Save the font GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("font", new XElement("name", font.Name.Content), new XElement("size", font.EmSize.ToString()), new XElement("bold", BoolToString(font.Bold)), new XElement("renderhq", "-1"), new XElement("italic", BoolToString(font.Italic)), new XElement("charset", font.Charset.ToString()), new XElement("aa", font.AntiAliasing.ToString()), new XElement("includeTTF", "0"), new XElement("TTFName", ""), new XElement("texgroups", new XElement("texgroup", "0") ), new XElement("ranges", new XElement("range0", font.RangeStart.ToString() + "," + font.RangeEnd.ToString()) ), new XElement("glyphs"), new XElement("kerningPairs"), new XElement("image", font.Name.Content + ".png") ) ); var glyphsNode = gmx.Element("font").Element("glyphs"); foreach (var i in font.Glyphs) { var glyphNode = new XElement("glyph"); glyphNode.Add(new XAttribute("character", i.Character.ToString())); glyphNode.Add(new XAttribute("x", i.SourceX.ToString())); glyphNode.Add(new XAttribute("y", i.SourceY.ToString())); glyphNode.Add(new XAttribute("w", i.SourceWidth.ToString())); glyphNode.Add(new XAttribute("h", i.SourceHeight.ToString())); glyphNode.Add(new XAttribute("shift", i.Shift.ToString())); glyphNode.Add(new XAttribute("offset", i.Offset.ToString())); glyphsNode.Add(glyphNode); } File.WriteAllText(projFolder + "/fonts/" + font.Name.Content + ".font.gmx", gmx.ToString()); // Save font textures worker.ExportAsPNG(font.Texture, projFolder + "/fonts/" + font.Name.Content + ".png"); } // --------------- Export Paths --------------- async Task ExportPaths() { Directory.CreateDirectory(projFolder + "/paths"); await Task.Run(() => Parallel.ForEach(Data.Paths, ExportPath)); } void ExportPath(UndertalePath path) { UpdateProgressBar(null, $"Exporting path: {path.Name.Content}", progress++, resourceNum); // Save the path GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("path", new XElement("kind", "0"), new XElement("closed", BoolToString(path.IsClosed)), new XElement("precision", path.Precision.ToString()), new XElement("backroom", "-1"), new XElement("hsnap", "16"), new XElement("vsnap", "16"), new XElement("points") ) ); foreach (var i in path.Points) { var pointsNode = gmx.Element("path").Element("points"); pointsNode.Add( new XElement("point", $"{i.X.ToString()},{i.Y.ToString()},{i.Speed.ToString()}") ); } File.WriteAllText(projFolder + "/paths/" + path.Name.Content + ".path.gmx", gmx.ToString()); } // --------------- Export Timelines --------------- async Task ExportTimelines() { Directory.CreateDirectory(projFolder + "/timelines"); await Task.Run(() => Parallel.ForEach(Data.Timelines, ExportTimeline)); } void ExportTimeline(UndertaleTimeline timeline) { UpdateProgressBar(null, $"Exporting timeline: {timeline.Name.Content}", progress++, resourceNum); // Save the timeline GMX var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("timeline") ); foreach (var i in timeline.Moments) { var entryNode = new XElement("entry"); entryNode.Add(new XElement("step", i.Item1)); entryNode.Add(new XElement("event")); foreach (var j in i.Item2) { entryNode.Element("event").Add( new XElement("action", new XElement("libid", j.LibID.ToString()), new XElement("id", j.ID.ToString()), new XElement("kind", j.Kind.ToString()), new XElement("userelative", BoolToString(j.UseRelative)), new XElement("isquestion", BoolToString(j.IsQuestion)), new XElement("useapplyto", BoolToString(j.UseApplyTo)), new XElement("exetype", j.ExeType.ToString()), new XElement("functionname", j.ActionName.Content), new XElement("codestring", ""), new XElement("whoName", "self"), new XElement("relative", BoolToString(j.Relative)), new XElement("isnot", BoolToString(j.IsNot)), new XElement("arguments", new XElement("argument", new XElement("kind", "1"), new XElement("string", j.CodeId != null ? Decompiler.Decompile(j.CodeId, DECOMPILE_CONTEXT.Value) : "") ) ) ) ); } gmx.Element("timeline").Add(entryNode); } File.WriteAllText(projFolder + "/timelines/" + timeline.Name.Content + ".timeline.gmx", gmx.ToString()); } // --------------- Generate project file --------------- void GenerateProjectFile() { UpdateProgressBar(null, $"Generating project file...", progress++, resourceNum); var gmx = new XDocument( new XComment(gmxDeclaration), new XElement("assets") ); // Write all resource indexes to project.gmx WriteIndexes(gmx.Element("assets"), "sounds", "sound", Data.Sounds, "sound", "sound\\"); WriteIndexes(gmx.Element("assets"), "sprites", "sprites", Data.Sprites, "sprite", "sprites\\"); WriteIndexes(gmx.Element("assets"), "backgrounds", "background", Data.Backgrounds, "background", "background\\"); WriteIndexes(gmx.Element("assets"), "scripts", "scripts", Data.Scripts, "script", "scripts\\", ".gml"); WriteIndexes(gmx.Element("assets"), "fonts", "fonts", Data.Fonts, "font", "fonts\\"); WriteIndexes(gmx.Element("assets"), "objects", "objects", Data.GameObjects, "object", "objects\\"); WriteIndexes(gmx.Element("assets"), "rooms", "rooms", Data.Rooms, "room", "rooms\\"); WriteIndexes(gmx.Element("assets"), "paths", "paths", Data.Paths, "path", "paths\\"); WriteIndexes(gmx.Element("assets"), "timelines", "timelines", Data.Timelines, "timeline", "timelines\\"); File.WriteAllText(projFolder + "Export_Project.project.gmx", gmx.ToString()); } void WriteIndexes(XElement rootNode, string elementName, string attributeName, IList dataList, string oneName, string resourcePath, string fileExtension = "") { var resourcesNode = new XElement(elementName, new XAttribute("name", attributeName) ); foreach (UndertaleNamedResource i in dataList) { var resourceNode = new XElement(oneName, resourcePath + i.Name.Content + fileExtension); resourcesNode.Add(resourceNode); } rootNode.Add(resourcesNode); }