diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a27b2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,360 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*[.json, .xml, .info] + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# End of https://www.toptal.com/developers/gitignore/api/visualstudio \ No newline at end of file diff --git a/GMExtensionPacker.sln b/GMExtensionPacker.sln new file mode 100644 index 0000000..3d5c9a7 --- /dev/null +++ b/GMExtensionPacker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30717.126 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GMExtensionPacker", "GMExtensionPacker\GMExtensionPacker.csproj", "{1FD57DBF-C68A-4E1C-8F6F-4E300E6CBC0A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1FD57DBF-C68A-4E1C-8F6F-4E300E6CBC0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FD57DBF-C68A-4E1C-8F6F-4E300E6CBC0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FD57DBF-C68A-4E1C-8F6F-4E300E6CBC0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FD57DBF-C68A-4E1C-8F6F-4E300E6CBC0A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {076D5823-3762-4539-B3A1-AC72989DC6F1} + EndGlobalSection +EndGlobal diff --git a/GMExtensionPacker/App.config b/GMExtensionPacker/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/GMExtensionPacker/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/GMExtensionPacker/AssetPackageBuilder.cs b/GMExtensionPacker/AssetPackageBuilder.cs new file mode 100644 index 0000000..6affabc --- /dev/null +++ b/GMExtensionPacker/AssetPackageBuilder.cs @@ -0,0 +1,155 @@ +using GMExtensionPacker.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; + +namespace GMExtensionPacker +{ + internal sealed class AssetPackageBuilder + { + private readonly List resources; + private readonly string workingDirectory; + private readonly string scriptsDirectory; + + private readonly string packageId; + + private AssetPackageBuilder(string workingDirectory, string packageId) + { + this.workingDirectory = workingDirectory ?? throw new ArgumentNullException(nameof(workingDirectory)); + this.packageId = packageId ?? throw new ArgumentNullException(nameof(packageId)); + + scriptsDirectory = Path.Combine(workingDirectory, "scripts"); + + resources = new List(); + } + + public static void CreateFromExtension(string inputPath, string outputPath) + { + using (var workingDirectory = WorkingDirectory.Create()) + { + var packageId = Path.GetFileNameWithoutExtension(outputPath); + var assetPackage = new AssetPackageBuilder(workingDirectory, packageId); + assetPackage.CreateFromExtension(inputPath); + + ZipFile.CreateFromDirectory(workingDirectory, outputPath); + } + } + + private void CreateFromExtension(string inputPath) + { + var extensionDirectory = Path.GetDirectoryName(inputPath); + var extension = Json.Deserialize(inputPath); + + Directory.CreateDirectory(scriptsDirectory); + + foreach (var file in extension.files) + { + if (!file.filename.EndsWith(".gml")) + throw new NotSupportedException($"Cannot convert extension file '{file.filename}' to package"); + + ConvertGMLFile(file, extensionDirectory); + ConvertConstants(file); + ConvertInit(file); + ConvertFinal(file); + } + + var assetPackagePath = Path.Combine(workingDirectory, "assetpackage.yy"); + Json.SerializeToFile(assetPackagePath, new Models.Gm22.AssetPackageModel + { + name = packageId, + packageID = packageId, + publisherName = extension.author, + resources = resources + }); + } + + private void ConvertGMLFile(Models.Gm22.GMExtensionFileModel file, string extensionDirectory) + { + var inFilePath = Path.Combine(extensionDirectory, file.filename); + using (var fsIn = File.OpenRead(inFilePath)) + using (var reader = new StreamReader(fsIn)) + { + FileStream fsOut = null; + StreamWriter writer = null; + + string line; + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("#define")) + { + writer?.Dispose(); + fsOut?.Close(); + + var scriptName = line.Split(' ')[1]; + fsOut = CreateScriptFile(scriptsDirectory, scriptName); + writer = new StreamWriter(fsOut); + continue; + } + + if (writer != null) + writer.WriteLine(line); + } + + writer?.Dispose(); + fsOut?.Close(); + } + } + + private void ConvertConstants(Models.Gm22.GMExtensionFileModel file) + { + if (file.constants == null || file.constants.Count <= 0) + return; + + var scriptName = Path.GetFileNameWithoutExtension(file.filename) + "_ext_macros"; + using (var stream = CreateScriptFile(scriptsDirectory, scriptName)) + using (var writer = new StreamWriter(stream)) + { + foreach (var constant in file.constants) + writer.WriteLine($"#macro {constant.constantName} {constant.value}"); + } + } + + private void ConvertInit(Models.Gm22.GMExtensionFileModel file) + { + if (string.IsNullOrEmpty(file.init)) + return; + + var scriptName = Path.GetFileNameWithoutExtension(file.filename) + "_ext_init"; + using (var stream = CreateScriptFile(scriptsDirectory, scriptName)) + using (var writer = new StreamWriter(stream)) + { + writer.WriteLine($"gml_pragma(\"global\", \"{file.init}();\");"); + } + } + + private void ConvertFinal(Models.Gm22.GMExtensionFileModel file) + { + if (string.IsNullOrEmpty(file.final)) + return; + + Console.WriteLine($"Warning: The finalizer '{file.final}' will not run in resulting package."); + } + + private FileStream CreateScriptFile(string scriptsDirectory, string scriptName) + { + // TODO scriptsDirectory as a field + + var directory = Path.Combine(scriptsDirectory, scriptName); + var gmlPath = Path.Combine(directory, scriptName + ".gml"); + var yyPath = Path.Combine(directory, scriptName + ".yy"); + + Directory.CreateDirectory(directory); + + resources.Add(Models.Gm22.AssetPackageModel.Resource.NewScript(scriptName, packageId)); + Json.SerializeToFile(yyPath, new Models.Gm22.GMScriptModel + { + name = scriptName, + IsCompatibility = false, + IsDnD = false + }); + + return File.OpenWrite(gmlPath); + } + } +} diff --git a/GMExtensionPacker/ExtensionBuilder.cs b/GMExtensionPacker/ExtensionBuilder.cs new file mode 100644 index 0000000..1c18d92 --- /dev/null +++ b/GMExtensionPacker/ExtensionBuilder.cs @@ -0,0 +1,159 @@ +using GMExtensionPacker.Models.Common; +using GMExtensionPacker.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace GMExtensionPacker +{ + internal sealed class ExtensionBuilder + { + public static void Do(string inputPath, string outputPath) + { + var extension = new Models.Gm22.GMExtensionFileModel + { + filename = Path.GetFileNameWithoutExtension(outputPath) + ".gml", + kind = Models.Gm22.ExtensionKind.Gml, + uncompress = false, + copyToTargets = TargetPlatforms.AllPlatforms, + + constants = new List(), + functions = new List(), + ProxyFiles = new List() + }; + + using (var workingDirectory = WorkingDirectory.Create()) + { + ZipFile.ExtractToDirectory(inputPath, workingDirectory); + + var assetPackagePath = Path.Combine(workingDirectory, "assetpackage.yy"); + var assetPackage = Json.Deserialize(assetPackagePath); + + var initScriptName = assetPackage.packageID + "_ext_init"; + var macrosScriptName = assetPackage.packageID + "_ext_macros"; + + var outputDirectory = Path.GetDirectoryName(outputPath); + var gmlPath = Path.Combine(outputDirectory, assetPackage.packageID + ".gml"); + var fsGml = File.OpenWrite(gmlPath); + var gmlWriter = new StreamWriter(fsGml); + + foreach (var resource in assetPackage.resources) + { + var scriptPath = Path.Combine(workingDirectory, resource.resourcePath); + scriptPath = Path.ChangeExtension(scriptPath, "gml"); + var scriptName = Path.GetFileNameWithoutExtension(scriptPath); + + if (!File.Exists(scriptPath)) + continue; + + var gmlContent = File.ReadAllText(scriptPath); + + if (scriptName == initScriptName) + { + const string Preamble = "gml_pragma(\"global\""; + if (!gmlContent.StartsWith(Preamble)) + { + Console.WriteLine("Warning: Extension Init script does not start with gml_pragma global"); + continue; + } + + var l = gmlContent.IndexOf('\"', Preamble.Length) + 1; + var r = gmlContent.IndexOf('(', Preamble.Length); + extension.init = gmlContent.Substring(l, r - l); + continue; + } + else if (scriptName == macrosScriptName) + { + const string Preamble = "#macro "; + using (var reader = new StringReader(gmlContent)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + if (!line.StartsWith(Preamble)) + continue; + + var i1 = line.IndexOf(' ') + 1; + var i2 = line.IndexOf(' ', i1) + 1; + if (i1 <= 0 || i2 <= 0) + continue; + + extension.constants.Add(new Models.Gm22.GMExtensionConstantModel + { + constantName = line.Substring(i1, i2 - i1 - 1), + value = line.Substring(i2, line.Length - i2), + hidden = false + }); + } + } + continue; + } + else + { + var jsDoc = JsDocParser.Parse(gmlContent); + + if (jsDoc.ReturnType == VariableType.None) + { + Console.WriteLine($"Warning: Script '{scriptName}' did not specify a return type. Defaulting to 'double'"); + jsDoc.ReturnType = VariableType.Double; + } + + gmlWriter.WriteLine($"#define {scriptName}"); + gmlWriter.WriteLine(gmlContent); + + extension.functions.Add(new Models.Gm22.GMExtensionFunctionModel + { + externalName = scriptName, + kind = Models.Gm22.ExtensionKind.Gml, + name = scriptName, + help = jsDoc.HelpString, + hidden = jsDoc.IsHidden, + returnType = jsDoc.ReturnType, + argCount = jsDoc.ArgumentCount, + args = jsDoc.Arguments + }); + } + } + + gmlWriter.Dispose(); + fsGml.Close(); + + extension.order = extension.functions.Select(x => x.id).ToList(); + Json.SerializeToFile(outputPath, new Models.Gm22.GMExtensionModel + { + name = Path.GetFileNameWithoutExtension(outputPath), + extensionName = "", + version = assetPackage.version, + packageID = "", + productID = "", + author = "", + date = DateTime.UtcNow, + license = "", + description = "", + helpfile = "", + iosProps = false, + androidProps = false, + installdir = "", + files = new List { extension }, + classname = "", + androidclassname = "", + sourcedir = "", + macsourcedir = "", + maccompilerflags = "", + maclinkerflags = "", + iosplistinject = "", + androidinject = "", + androidmanifestinject = "", + androidactivityinject = "", + gradleinject = "", + iosSystemFrameworkEntries = new List(), + iosThirdPartyFrameworkEntries = new List(), + IncludedResources = new List(), // TODO Include Files + copyToTargets = TargetPlatforms.AllPlatforms + }); + } + } + } +} diff --git a/GMExtensionPacker/GMExtensionPacker.csproj b/GMExtensionPacker/GMExtensionPacker.csproj new file mode 100644 index 0000000..f683b2a --- /dev/null +++ b/GMExtensionPacker/GMExtensionPacker.csproj @@ -0,0 +1,61 @@ + + + + + Debug + AnyCPU + {1FD57DBF-C68A-4E1C-8F6F-4E300E6CBC0A} + Exe + GMExtensionPacker + GMExtensionPacker + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GMExtensionPacker/JsDocParser.cs b/GMExtensionPacker/JsDocParser.cs new file mode 100644 index 0000000..741edb7 --- /dev/null +++ b/GMExtensionPacker/JsDocParser.cs @@ -0,0 +1,211 @@ +using GMExtensionPacker.Models.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GMExtensionPacker +{ + public static class JsDocParser + { + public static JsDoc Parse(string content) + { + if (content == null) + throw new ArgumentNullException("content"); + + JsDoc jsDoc = new JsDoc(); + + foreach (var line in YieldLines(content)) + { + Directive directive; + string arguments; + + if (!IdentifyDirective(line, out directive, out arguments)) + continue; + + switch (directive) + { + case Directive.Description: + jsDoc.Description += arguments; + break; + + case Directive.Parameter: + jsDoc.Parameters.Add(ParseType(arguments, true)); + break; + + case Directive.Return: + jsDoc.ReturnType = ParseType(arguments, false).Type; + break; + + case Directive.Hidden: + jsDoc.IsHidden = true; + break; + } + } + + return jsDoc; + } + + private static JsDoc.Parameter ParseType(string content, bool takeName) + { + if (content == null) + throw new ArgumentNullException("content"); + + if (content.Length == 0) + return new JsDoc.Parameter(VariableType.None, "", ""); + + VariableType gmlType = VariableType.None; + int startBraceIdx = content.IndexOf("{", StringComparison.Ordinal); + int endBraceIdx = content.IndexOf("}", startBraceIdx + 1, StringComparison.Ordinal); + + if (startBraceIdx != -1 && endBraceIdx != -1) + { + string type = content.Substring(startBraceIdx + 1, endBraceIdx - startBraceIdx - 1).ToLowerInvariant(); + + VariableType scratch; + if (Enum.TryParse(type, true, out scratch)) + gmlType = scratch; + } + + string name = ""; + string desc; + + int startNameIdx = Math.Max(endBraceIdx, 0); + + if (takeName) + { + int endNameIdx = content.IndexOfAny(new[] { ' ', '\t' }, Math.Min(startNameIdx + 2, content.Length)); + if (endNameIdx == -1) + endNameIdx = content.Length; + + name = content.Substring(startNameIdx + 1, endNameIdx - startNameIdx - 1).Trim(); + desc = endBraceIdx == -1 ? content : content.Substring(endNameIdx).Trim(); + } + else + desc = content.Substring(startNameIdx + 1).Trim(); + + if (desc.Length == 0) + desc = name; + + return new JsDoc.Parameter(gmlType, name, desc); + } + + private static bool IdentifyDirective(string line, out Directive directive, out string arguments) + { + if (line == null) + throw new ArgumentNullException(nameof(line)); + + if (!line.StartsWith("@")) + { + directive = Directive.Description; + arguments = line; + return true; + } + + var token = new string(line.TakeWhile(x => char.IsLetter(x) || x == '@').ToArray()).ToLowerInvariant(); + switch (token) + { + case "@desc": + case "@description": + directive = Directive.Description; + break; + + case "@param": + case "@arg": + case "@argument": + directive = Directive.Parameter; + break; + + case "@returns": + case "@return": + directive = Directive.Return; + break; + + case "@hidden": + directive = Directive.Hidden; + break; + + default: + directive = Directive.None; + arguments = null; + return false; + } + + arguments = line.Substring(token.Length); + arguments = arguments.TrimStart(); + + return true; + } + + private static IEnumerable YieldLines(string content) + { + if (content == null) + throw new ArgumentNullException(nameof(content)); + + using (var reader = new StringReader(content)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + int atSymbolIdx = line.IndexOf("@", StringComparison.Ordinal); + if (atSymbolIdx == -1) + continue; + + line = line.Substring(atSymbolIdx); + line = line.TrimEnd(); + + yield return line; + } + } + } + + private enum Directive + { + None, + Description, + Parameter, + Return, + Hidden + } + } + + public sealed class JsDoc + { + public string Description { get; set; } + + public string HelpString => string.Join(", ", Parameters.Select(x => x.Name)); + + public bool IsHidden { get; set; } + + public VariableType ReturnType { get; set; } + + public List Arguments => Parameters.Select(x => x.Type).ToList(); + + public int ArgumentCount => Parameters.Count; + + public List Parameters { get; } + + public JsDoc() + { + Parameters = new List(); + } + + public sealed class Parameter + { + public VariableType Type { get; } + + public string Name { get; } + + public string Description { get; } + + public Parameter(VariableType type, string name, string description) + { + Type = type; + Name = name; + Description = description; + } + } + } +} diff --git a/GMExtensionPacker/Models/Common/Enums.cs b/GMExtensionPacker/Models/Common/Enums.cs new file mode 100644 index 0000000..a8fe646 --- /dev/null +++ b/GMExtensionPacker/Models/Common/Enums.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GMExtensionPacker.Models.Common +{ + public enum VariableType + { + None = 0, + String = 1, + Double = 2 + } + + // Common + [Flags] + public enum TargetPlatforms : long + { + MacOsX = 2, + iOS = 4, + Android = 8, + Html5 = 32, + Windows = 64, + Ubuntu = 128, + WindowsPhone8 = 4096, + SteamWorkshop = 16384, + Windows8Javascript = 32768, + TizenJavascript = 65536, + Windows_YYC = 1048576, + Android_YYC = 2097152, + Windows8 = 4194304, + TizenNative = 8388608, + Tizen_YYC = 16777216, + iOS_YYC = 33554432, + MacOsX_YYC = 67108864, + Ubuntu_YYC = 134217728, + WindowsPhone8_YYC = 268435456, + Windows8_YYC = 536870912, + PSVita = 2147483648, + PS4 = 4294967296, + XboxOne = 34359738368, + PSVita_YYC = 68719476736, + PS4_YYC = 137438953472, + XboxOne_YYC = 1099511627776, + PS3 = 2199023255552, + PS3_YYC = 4398046511104, + GameMakerPlayer = 17592186044416, + MicrosoftUAP = 35184372088832, + MicrosoftUAP_YYC = 70368744177664, + AndroidTV = 140737488355328, + AndroidTV_YYC = 281474976710656, + AmazonFireTV = 562949953421312, + AmazonFireTV_YYC = 1125899906842624, + tvOS = 9007199254740992, + tvOS_YYC = 18014398509481984, + AllPlatforms = -1// tvOS_YYC | tvOS | AmazonFireTV_YYC | AmazonFireTV | AndroidTV_YYC | AndroidTV | MicrosoftUAP_YYC | MicrosoftUAP | GameMakerPlayer | PS3_YYC | PS3 | XboxOne_YYC | PS4_YYC | PSVita_YYC | XboxOne | PS4 | PSVita | Windows8_YYC | WindowsPhone8_YYC | Ubuntu_YYC | MacOsX_YYC | iOS_YYC | Tizen_YYC | TizenNative | Windows8 | Android_YYC | Windows_YYC | TizenJavascript | Windows8Javascript | SteamWorkshop | WindowsPhone8 | Ubuntu | Windows | Html5 | Android | iOS | MacOsX + } +} diff --git a/GMExtensionPacker/Models/Gm22/AssetPackageModel.cs b/GMExtensionPacker/Models/Gm22/AssetPackageModel.cs new file mode 100644 index 0000000..a1edc95 --- /dev/null +++ b/GMExtensionPacker/Models/Gm22/AssetPackageModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace GMExtensionPacker.Models.Gm22 +{ + [DataContract] + internal sealed class AssetPackageModel + { + [DataMember] + public string description { get; set; } + + [DataMember] + public string helpfile { get; set; } + + [DataMember] + public string license { get; set; } + + [DataMember] + public string name { get; set; } + + [DataMember] + public string packageID { get; set; } + + [DataMember] + public string packageType { get; set; } + + [DataMember] + public string projectType { get; set; } + + [DataMember] + public string publisherName { get; set; } + + [DataMember] + public List resources { get; set; } + + [DataMember] + public string version { get; set; } + + public AssetPackageModel() + { + projectType = ""; + resources = new List(); + version = "1.0.0"; + } + + [DataContract] + public sealed class Resource + { + [DataMember] + public Guid id { get; set; } + + [DataMember] + public string resourcePath { get; set; } + + [DataMember] + public string resourceType { get; set; } + + [DataMember] + public string viewPath { get; set; } + + public Resource() + { + id = Guid.NewGuid(); + } + + public static Resource NewScript(string name, string view) + { + return new Resource + { + resourcePath = $"scripts\\{name}\\{name}.yy", + resourceType = "GMScript", + viewPath = $"scripts\\{view}" + }; + } + } + } +} diff --git a/GMExtensionPacker/Models/Gm22/GMExtensionModel.cs b/GMExtensionPacker/Models/Gm22/GMExtensionModel.cs new file mode 100644 index 0000000..aa0fb4c --- /dev/null +++ b/GMExtensionPacker/Models/Gm22/GMExtensionModel.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using GMExtensionPacker.Models.Common; + +namespace GMExtensionPacker.Models.Gm22 +{ + [DataContract] + internal abstract class ModelBase + { + [DataMember] + public Guid id { get; set; } + + [DataMember] + public string mvc { get; private set; } + + [DataMember] + public string modelName { get; private set; } + + protected ModelBase() + { + // NOTE Empty ctor for deserialization + } + + protected ModelBase(string modelName, string mvc) + { + if (modelName == null) + throw new ArgumentNullException(nameof(modelName)); + + if (mvc == null) + throw new ArgumentNullException(nameof(mvc)); + + this.id = Guid.NewGuid(); + this.mvc = mvc; + this.modelName = modelName; + } + } + + [DataContract] + internal sealed class GMExtensionModel : ModelBase + { + [DataMember] + public string name { get; set; } + + // NOTE Deprecated + [DataMember] + public string extensionName { get; set; } + + [DataMember] + public string version { get; set; } + + // NOTE Deprecated + [DataMember] + public string packageID { get; set; } + + // NOTE Deprecated + [DataMember] + public string productID { get; set; } + + // NOTE Deprecated + [DataMember] + public string author { get; set; } + + [DataMember] + public DateTime date { get; set; } + + // NOTE Deprecated + [DataMember] + public string license { get; set; } + + // NOTE Deprecated + [DataMember] + public string description { get; set; } + + // NOTE Deprecated + [DataMember] + public string helpfile { get; set; } + + [DataMember] + public bool iosProps { get; set; } + + [DataMember] + public bool androidProps { get; set; } + + // NOTE Deprecated + [DataMember] + public string installdir { get; set; } + + [DataMember] + public List files { get; set; } + + [DataMember] + public string classname { get; set; } + + [DataMember] + public string androidclassname { get; set; } + + [DataMember] + public string sourcedir { get; set; } + + [DataMember] + public string macsourcedir { get; set; } + + [DataMember] + public string maccompilerflags { get; set; } + + [DataMember] + public string maclinkerflags { get; set; } + + [DataMember] + public string iosplistinject { get; set; } + + [DataMember] + public string androidinject { get; set; } + + [DataMember] + public string androidmanifestinject { get; set; } + + [DataMember] + public string androidactivityinject { get; set; } + + [DataMember] + public string gradleinject { get; set; } + + [DataMember] + public List iosSystemFrameworkEntries { get; set; } + + [DataMember] + public List iosThirdPartyFrameworkEntries { get; set; } + + [DataMember] + public List IncludedResources { get; set; } + + [DataMember] + public List androidPermissions { get; set; } + + [DataMember] + public TargetPlatforms copyToTargets { get; set; } + + public GMExtensionModel() + : base("GMExtension", "1.0") + { + + } + } + + [DataContract] + internal sealed class GMExtensionFileModel : ModelBase + { + [DataMember] + public string filename { get; set; } + + [DataMember] + public string origname { get; set; } + + [DataMember] + public string init { get; set; } + + [DataMember] + public string final { get; set; } + + [DataMember] + public ExtensionKind kind { get; set; } + + [DataMember] + public bool uncompress { get; set; } + + [DataMember] + public List functions { get; set; } + + [DataMember] + public List constants { get; set; } + + [DataMember] + public List ProxyFiles { get; set; } + + [DataMember] + public TargetPlatforms copyToTargets { get; set; } + + [DataMember] + public List order { get; set; } + + public GMExtensionFileModel() + : base("GMExtensionFile", "1.0") + { + + } + } + + [DataContract] + internal sealed class GMExtensionFunctionModel : ModelBase + { + [DataMember] + public string externalName { get; set; } + + [DataMember] + public ExtensionKind kind { get; set; } + + [DataMember] + public string name { get; set; } + + [DataMember] + public string help { get; set; } + + [DataMember] + public bool hidden { get; set; } + + [DataMember] + public VariableType returnType { get; set; } + + [DataMember] + public int argCount { get; set; } + + [DataMember] + public List args { get; set; } + + public GMExtensionFunctionModel() + : base("GMExtensionFunction", "1.0") + { + + } + } + + [DataContract] + internal sealed class GMExtensionConstantModel : ModelBase + { + [DataMember] + public string constantName { get; set; } + + [DataMember] + public string value { get; set; } + + [DataMember] + public bool hidden { get; set; } + + public GMExtensionConstantModel() + : base("GMExtensionConstant", "1.0") + { + + } + } + + [DataContract] + internal sealed class GMProxyFileModel : ModelBase + { + [DataMember] + public string proxyName { get; set; } + + [DataMember] + public TargetPlatforms TargetMask { get; set; } + + public GMProxyFileModel() + : base("GMProxyFile", "1.0") + { + + } + } + + [DataContract] + internal sealed class GMExtensionFrameworkEntryModel : ModelBase + { + [DataMember] + public string frameworkName { get; set; } + + [DataMember] + public bool weakReference { get; set; } + + public GMExtensionFrameworkEntryModel() + : base("GMExtensionFrameworkEntry", "1.0") + { + + } + } + + public enum ExtensionKind + { + Undefined = 0, + Dll = 1, + Gml = 2, + Lib = 3, + Other = 4, + Js = 5, + Stdcall = 11, + Cdecl = 12 + } +} diff --git a/GMExtensionPacker/Models/Gm22/GMScriptModel.cs b/GMExtensionPacker/Models/Gm22/GMScriptModel.cs new file mode 100644 index 0000000..de97b65 --- /dev/null +++ b/GMExtensionPacker/Models/Gm22/GMScriptModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace GMExtensionPacker.Models.Gm22 +{ + [DataContract] + internal sealed class GMScriptModel : ModelBase + { + [DataMember] + public string name { get; set; } + + [DataMember] + public bool IsCompatibility { get; set; } + + [DataMember] + public bool IsDnD { get; set; } + + public GMScriptModel() + : base("GMScript", "1.0") + { + + } + } +} diff --git a/GMExtensionPacker/Program.cs b/GMExtensionPacker/Program.cs new file mode 100644 index 0000000..5f362c4 --- /dev/null +++ b/GMExtensionPacker/Program.cs @@ -0,0 +1,33 @@ +using System; +using System.Text; +using System.Threading.Tasks; + +namespace GMExtensionPacker +{ + internal static class Program + { + public static void Main(string[] args) + { + // gmextpack -v23 extension.yy + // gmextpack -v22 package.yymps + + var config = RuntimeConfig.FromCommandLine(args); + if (config == null) + { + RuntimeConfig.ShowHelp(); + return; + } + + if (config.ModeConvertToPackage) + { + AssetPackageBuilder.CreateFromExtension(config.InputPath, config.OutputPath); + Console.WriteLine($"Saved to '{config.OutputPath}'..."); + } + else + { + ExtensionBuilder.Do(config.InputPath, config.OutputPath); + Console.WriteLine($"Saved to '{config.OutputPath}'..."); + } + } + } +} diff --git a/GMExtensionPacker/RuntimeConfig.cs b/GMExtensionPacker/RuntimeConfig.cs new file mode 100644 index 0000000..cbe3596 --- /dev/null +++ b/GMExtensionPacker/RuntimeConfig.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; + +namespace GMExtensionPacker +{ + internal sealed class RuntimeConfig + { + public string InputPath { get; private set; } + + public string OutputPath { get; private set; } + + public GmVersion Version { get; private set; } + + public bool ModeConvertToPackage => InputPath.EndsWith(".yy"); + + private RuntimeConfig() + { + // NOTE Private ctor to enforce factory pattern + } + + public static RuntimeConfig FromCommandLine(string[] args) + { + var config = new RuntimeConfig(); + foreach (var arg in args) + { + if (arg.StartsWith("-")) + { + if (arg == "-v22") + { + config.Version = GmVersion.Gm22; + continue; + } + else if (arg == "-v23") + { + config.Version = GmVersion.Gm23; + continue; + } + } + + if (config.InputPath == null) + { + config.InputPath = arg; + continue; + } + else if (config.OutputPath == null) + { + config.OutputPath = arg; + continue; + } + + Console.WriteLine($"Unknown option: {arg}"); + } + + if (config.Version == GmVersion.None && config.InputPath != null) + { + if (config.InputPath.EndsWith(".yymps")) + config.Version = GmVersion.Gm23; + else if (config.InputPath.EndsWith(".yymp")) + config.Version = GmVersion.Gm22; + } + + if (config.OutputPath == null && config.InputPath != null) + { + if (config.InputPath.EndsWith(".yymps") || config.InputPath.EndsWith(".yymp")) + config.OutputPath = Path.ChangeExtension(config.InputPath, "yy"); + else if (config.InputPath.EndsWith(".yy")) + { + if (config.Version == GmVersion.Gm22) + config.OutputPath = Path.ChangeExtension(config.InputPath, "yymp"); + else if (config.Version == GmVersion.Gm23) + config.OutputPath = Path.ChangeExtension(config.InputPath, "yymps"); + } + } + + if (config.Version == GmVersion.None || config.InputPath == null || config.OutputPath == null) + return null; + + return config; + } + + public static void ShowHelp() + { + Console.WriteLine("gmextpack input [output]"); + Console.WriteLine("Options"); + Console.WriteLine(" -v22 Generate 2.2.5 compatible files"); + Console.WriteLine(" -v23 Generate 2.3.1 compatible files"); + Console.WriteLine(); + Console.WriteLine("input Path to a *.yymp, *.yymps, or extension *.yy file"); + Console.WriteLine("[output] Path to output file. OPTIONAL"); + Console.WriteLine(" If not provided then input filename is used to generate the output name"); + Console.WriteLine(" Output for yymp/s will be to a directory, not file"); + } + } + + internal enum GmVersion + { + None, + Gm22, + Gm23 + } +} diff --git a/GMExtensionPacker/Utility/FileSystemUtility.cs b/GMExtensionPacker/Utility/FileSystemUtility.cs new file mode 100644 index 0000000..8422085 --- /dev/null +++ b/GMExtensionPacker/Utility/FileSystemUtility.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace GMExtensionPacker.Utility +{ + public static class FileSystemUtility + { + + } + + internal sealed class WorkingDirectory : IDisposable + { + private readonly string directory; + + private WorkingDirectory(string directory) + { + this.directory = directory ?? throw new ArgumentNullException(nameof(directory)); + } + + public static implicit operator string(WorkingDirectory workingDirectory) + { + return workingDirectory.directory; + } + + public void Dispose() + { + Directory.Delete(directory, true); + } + + public static WorkingDirectory Create() + { + string directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(directory); + + return new WorkingDirectory(directory); + } + } +} diff --git a/GMExtensionPacker/Utility/Json.cs b/GMExtensionPacker/Utility/Json.cs new file mode 100644 index 0000000..c872b1a --- /dev/null +++ b/GMExtensionPacker/Utility/Json.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace GMExtensionPacker.Utility +{ + [DebuggerStepThrough] + public static class Json + { + public static bool PrettyPrint = true; + public static int IndentLength = 4; + + private readonly static Encoding Encoding = Encoding.Default; + + public static void SerializeToFile(string path, T value) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + File.WriteAllText( + path, + Serialize(value), + Encoding + ); + } + + public static string Serialize(T value) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + using (var stream = new MemoryStream()) + { + var json = CreateSerialize(value.GetType()); + json.WriteObject(stream, value); + + var serializedJson = Encoding.GetString(stream.ToArray()); + if (PrettyPrint) + serializedJson = PrettyPrintJson(serializedJson); + + return serializedJson; + } + } + + public static object Deserialize(Type type, string path) + { + using (var stream = File.OpenRead(path)) + return Deserialize(type, stream); + } + + public static T Deserialize(string path) + where T : new() + { + using (var stream = File.OpenRead(path)) + return Deserialize(stream); + } + + public static object Deserialize(Type type, Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + var json = CreateSerialize(type); + return json.ReadObject(stream); + } + + public static T Deserialize(Stream stream) + where T : new() + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + var json = CreateSerialize(typeof(T)); + return (T)json.ReadObject(stream); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static DataContractJsonSerializer CreateSerialize(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + return new DataContractJsonSerializer( + type, + new DataContractJsonSerializerSettings + { + DateTimeFormat = new DateTimeFormat("yyyy-mm-dd hh:MM:ss"), + IgnoreExtensionDataObject = true, + EmitTypeInformation = EmitTypeInformation.Never, + KnownTypes = new[] + { + typeof(List) + } + } + ); + } + + private static string PrettyPrintJson(string str) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + + var indent = 0; + var quoted = false; + var sb = new StringBuilder(str.Length); + + for (var i = 0; i < str.Length; i++) + { + var ch = str[i]; + switch (ch) + { + case '{': + case '[': + sb.Append(ch); + if (!quoted) + { + sb.AppendLine(); + sb.Append(new string(' ', ++indent * IndentLength)); + } + break; + case '}': + case ']': + if (!quoted) + { + sb.AppendLine(); + sb.Append(new string(' ', --indent * IndentLength)); + } + sb.Append(ch); + break; + case '"': + sb.Append(ch); + bool escaped = false; + var index = i; + while (index > 0 && str[--index] == '\\') + escaped = !escaped; + if (!escaped) + quoted = !quoted; + break; + case ',': + sb.Append(ch); + if (!quoted) + { + sb.AppendLine(); + sb.Append(new string(' ', indent * IndentLength)); + } + break; + case ':': + sb.Append(ch); + if (!quoted) + sb.Append(' '); + break; + default: + sb.Append(ch); + break; + } + } + + return sb.ToString(); + } + } + + // NOTE Do not add IDictionary to this class, for whatever reason this will cause the + // serializer to throw out our hard work to make it behave itself + [Serializable] + [DebuggerStepThrough] + public class JsonDictionary : ISerializable, IEnumerable + { + public ICollection Keys => dictionary.Keys; + + public ICollection Values => dictionary.Values; + + public int Count => dictionary.Count; + + public object this[string key] + { + get { return dictionary[key]; } + set { dictionary[key] = value; } + } + + private readonly Dictionary dictionary; + + public JsonDictionary() + { + dictionary = new Dictionary(); + } + + protected JsonDictionary(SerializationInfo info, StreamingContext context) + { + foreach (var entry in info) + dictionary.Add(entry.Name, entry.Value); + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + foreach (string key in dictionary.Keys) + info.AddValue(key, dictionary[key]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(string key, object value) + { + dictionary.Add(key, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(string key) + { + return dictionary.Remove(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + dictionary.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(string key, out object value) + { + return dictionary.TryGetValue(key, out value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsKey(string key) + { + return dictionary.ContainsKey(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerator> GetEnumerator() + { + return dictionary.GetEnumerator(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..74f5a21 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + +==== + +Copyright (c) 2021 Zach Reedy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b72ff7 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# GameMaker Extension Packer + +This tool gives you the ability to convert GameMaker Local Packages into Extensions, and Extensions into Local Packages. This allows you to develop your extensions inside of the IDE as a collection of scripts, and then package as an extension. Additionally, it enables you to modify existing extensions easily by importing them into GameMaker as scripts. + +## Getting Started +Download and extract the latest [Release](https://github.com/DatZach/GMExtensionPacker/releases). If you intend to use via the command line then it's recommended that you place the files somewhere inside your `PATH` environment variable. + +## Usage +### Command Line Usage +``` +gmextpack input [output] +Options + -v22 Generate 2.2.5 compatible files + -v23 Generate 2.3.1 compatible files + +input Path to a *.yymp, *.yymps, or extension *.yy file +[output] Path to output file. OPTIONAL + If not provided then input filename is used to generate the output name + Output for yymp/s will be to a directory, not file +``` + +### Shell Usage +Drag and drop a `*.yymp`, `*.yymps`, or extension `*.yy` file onto the EXE. A console window should flash up before disappearing. If successful, the result will be present in the same directory as the file you dragged in. When used like this, a Local Package will result in an Extension `*.yy` file. + +### Workflow for creating an extension +Develop the scripts for your plugin inside of a standard GameMaker project. When you're ready to create an extension select `Tools > Create Local Package` and select all the relevant scripts. Once complete, pass the file to GM Extension Packer. The resulting `.yy` file can be added to GameMaker projects by right clicking on `Extensions` and selecting `Add Existing`. + +#### JSDoc +When converting to an extension the jsdoc header is parsed and used to correctly build the extension function information. At a minimum, you must document each argument and its type. Argument names, if provided will be used to build the help string for the IDE's autocomplete feature. Return should also be documented, otherwise a double will be assumed as the return value. The name of the function is determined by the script's name, not jsdoc. Additonally you can specify a special attribute `@hidden` to hide the extension function from the IDE. + +Script `add` +```gml +/// @param {real} a +/// @param {real} b +/// @returns {real} result +/// @hidden + +return argument0 + argument1; +``` + +#### Macros / Constants +When converting to an extension, any macros specified the script `_ext_macros` will be converted to constants in the extension. If this script does not exist, or macros are declared in other scripts, then no constants will be added to the extension. + +#### Initializer / Finalizer +When converting to an extension, initializers and finalizers can be specified via `gml_pragma("global", ...)` in the script `_ext_init` and `_ext_final`. If these scripts do not exist, then no initializer or finalizer respectively will be declared in the extension. + +Script `Example_ext_init` +```gml +gml_pragma("global", "example_initialize();"); +``` + +### Workflow for importing an extension as scripts +Run the `*.yy` extension through the tool. Then in the GameMaker IDE select `Tools > Import Local Package`, add all files and accept. If there are macros or initializers/finalizers they will be imported as scripts with the names documented above. + +Some of the reasons you might want to import a GML extension like this: + - Improved compile times. Extension functions compile slower than standard scripts. + - Modify buggy code in extensions. + +## Known Issues + - Version 2.3 support is not yet implemented. + - Poor error handling. + +## Authors + +* **Zach Reedy** - *Primary developer* - [DatZach](https://github.com/DatZach) + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details \ No newline at end of file