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