/// <summary> /// Compiles the specified addon into the specified stream. /// </summary> /// <param name="addon">The addon to compile.</param> /// <param name="stream">The stream which the result should be written to.</param> /// <exception cref="IOExecption">Happens if there is a problem with the specified stream.</exception> public static void Create(Addon addon, Stream stream) { var writer = new BinaryWriter(stream); writer.BaseStream.Seek(0, SeekOrigin.Begin); writer.BaseStream.SetLength(0); // Header (5) writer.Write(Addon.Magic.ToCharArray()); // Ident (4) writer.Write(Addon.Version); // Version (1) // SteamID (8) [unused] writer.Write((ulong)0); // TimeStamp (8) writer.Write((ulong) (DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0).ToLocalTime()).TotalSeconds); // Required content (a list of strings) writer.Write((char)0); // signifies nothing // Addon Name (n) writer.WriteNullTerminatedString(addon.Title); // Addon Description (n) writer.WriteNullTerminatedString(addon.DescriptionJson); // Addon Author (n) [unused] writer.WriteNullTerminatedString("Author Name"); // Addon version (4) [unused] writer.Write(1); // File list uint fileNum = 0; foreach (var f in addon.Files) { // Remove prefix / from filename var file = f.Path.TrimStart('/'); fileNum++; writer.Write(fileNum); // File number (4) writer.WriteNullTerminatedString(file.ToLowerInvariant()); // File name (all lower case!) (n) writer.Write(f.Size); // File size (8) unsigned long writer.Write(f.Crc); // File CRC (4) long long } writer.Flush(); // Zero to signify the end of files fileNum = 0; writer.Write(fileNum); // The files foreach (var f in addon.Files) { writer.Write(f.Content); writer.Flush(); } // CRC what we've written (to verify that the download isn't s*****d) (4) writer.Seek(0, SeekOrigin.Begin); var buffer_whole = new byte[writer.BaseStream.Length]; writer.BaseStream.Read(buffer_whole, 0, (int)writer.BaseStream.Length); ulong addonCRC = CRC32.ComputeChecksum(buffer_whole); writer.Write(addonCRC); writer.Flush(); writer.BaseStream.Seek(0, SeekOrigin.Begin); }
/// <summary> /// Creates a JSON string using the properties of the provided Addon. /// </summary> /// <param name="addon">The addon which metadata is to be used.</param> /// <returns>The compiled JSON string.</returns> /// <exception cref="AddonJsonException">Errors regarding creating the JSON.</exception> public static string BuildDescription(Addon addon) { var tree = new DescriptionJson { Description = addon.Description }; // Load the addon type if (addon.Type.ToLowerInvariant() == string.Empty || addon.Type.ToLowerInvariant() == null) { throw new AddonJsonException("type is empty!"); } if (!Greed.Tags.TypeExists(addon.Type.ToLowerInvariant())) { throw new AddonJsonException("type isn't a supported type!"); } tree.Type = addon.Type.ToLowerInvariant(); // Parse the tags tree.Tags = new List <string>(); if (addon.Tags.Count > 2) { throw new AddonJsonException("too many tags - specify 2 only!"); } foreach (var tag in addon.Tags) { if (string.IsNullOrEmpty(tag)) { continue; } if (!Greed.Tags.TagExists(tag.ToLowerInvariant())) { throw new AddonJsonException("tag isn't a supported word!"); } tree.Tags.Add(tag.ToLowerInvariant()); } string strOutput; using (var stream = new MemoryStream()) { var jsonFormatter = new DataContractJsonSerializer(typeof(DescriptionJson)); try { jsonFormatter.WriteObject(stream, tree); } catch (SerializationException ex) { throw new AddonJsonException("Couldn't create json", ex); } stream.Seek(0, SeekOrigin.Begin); var bytes = new byte[stream.Length]; stream.Read(bytes, 0, (int)stream.Length); strOutput = Encoding.ASCII.GetString(bytes); strOutput = strOutput.Replace("\\u000d", "").Replace("\\u0009", "\\t").Replace("\\u000a", "\\n"); } return(strOutput); }
/// <summary> /// Legacy GMAD operation to extract an addon file to a specified folder. /// </summary> /// <param name="strFile">The file path of the GMA to extract.</param> /// <param name="strOutPath">The folder where the addon is to be extracted to.</param> /// <param name="gmod12">True if the extract should also create a legacy info.txt file.</param> /// <returns>Integer error code: 0 if success, 1 if error.</returns> private static int ExtractAddonFile(string strFile, string strOutPath = "", bool gmod12 = false) { Console.WriteLine("Opening " + strFile); // // If an out path hasn't been provided, make our own // if (strOutPath == string.Empty) { strOutPath = Path.GetFileNameWithoutExtension(strFile); } // // Remove slash, add slash (enforces a slash) // strOutPath = strOutPath.TrimEnd('/'); strOutPath = strOutPath + '/'; Addon addon; try { var fs = new FileStream(strFile, FileMode.Open, FileAccess.Read, FileShare.None); addon = new Addon(new Reader(fs)); } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("There was a problem opening or parsing the file"); Console.ResetColor(); Console.WriteLine(ex.Message); return(1); } Console.WriteLine("Extracting Files:"); foreach (var entry in addon.Files) { Console.WriteLine("\t" + entry.Path + " [" + ((int)entry.Size).HumanReadableSize() + "]"); // Make sure folder exists try { Directory.CreateDirectory(strOutPath + Path.GetDirectoryName(entry.Path)); } catch (Exception) { // Noop } // Write the file to the disk try { using (var file = new FileStream(strOutPath + entry.Path, FileMode.Create, FileAccess.Write)) { file.Write(entry.Content, 0, (int)entry.Size); } } catch (Exception) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\t\tCouldn't extract!"); Console.ResetColor(); } } if (gmod12) // Write a legacy info.txt schema { // The description has paramteres if the addon was created by a conversion. // Extract them out. var regex = new Regex("^# ([\\s\\S]*?): ([\\s\\S]*?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline); var matches = regex.Matches(addon.Description); // info.txt/addon.txt files usually have these values not directly mapped into GMAs as well. var AuthorName = string.Empty; var AuthorEmail = string.Empty; var AuthorURL = string.Empty; var Version = string.Empty; var Date = string.Empty; foreach (Match keyMatch in matches) { if (keyMatch.Groups.Count == 3) { // All match should have 2 groups matched (the 0th group is the whole match.) switch (keyMatch.Groups[1].Value.ToLowerInvariant()) { case "version": Version = keyMatch.Groups[2].Value.TrimEnd('\n', '\r', '\t'); break; case "date": Date = keyMatch.Groups[2].Value.TrimEnd('\n', '\r', '\t'); break; case "authorname": AuthorName = keyMatch.Groups[2].Value.TrimEnd('\n', '\r', '\t'); break; case "authoremail": AuthorEmail = keyMatch.Groups[2].Value.TrimEnd('\n', '\r', '\t'); break; case "authorurl": AuthorURL = keyMatch.Groups[2].Value.TrimEnd('\n', '\r', '\t'); break; } } } var endConversionInfo = "## End conversion info"; var description = addon.Description; if (addon.Description.IndexOf(endConversionInfo) > 0) { description = addon.Description.Substring(addon.Description.IndexOf(endConversionInfo) + endConversionInfo.Length); description = description.TrimStart('\r', '\n'); } File.WriteAllText(strOutPath + "info.txt", "\"AddonInfo\"\n" + "{\n" + "\t" + "\"name\"" + "\t" + "\"" + addon.Title + "\"\n" + "\t" + "\"version\"" + "\t" + "\"" + Version + "\"\n" + "\t" + "\"up_date\"" + "\t" + "\"" + (string.IsNullOrWhiteSpace(Date) ? addon.Timestamp.ToString("ddd MM dd hh:mm:ss yyyy", CultureInfo.InvariantCulture) : DateTime.Now.ToString("ddd MM dd hh:mm:ss yyyy", CultureInfo.InvariantCulture) + " (+" + TimeZoneInfo.Local.BaseUtcOffset.ToString("hhmm") + ")") + "\"\n" + "\t" + "\"author_name\"" + "\t" + "\"" + AuthorName + "\"\n" + // addon.Author would be nice "\t" + "\"author_email\"" + "\t" + "\"" + AuthorEmail + "\"\n" + "\t" + "\"author_url\"" + "\t" + "\"" + AuthorURL + "\"\n" + "\t" + "\"info\"" + "\t" + "\"" + description + "\"\n" + "\t" + "\"override\"" + "\t" + "\"1\"\n" + "}"); } Console.WriteLine("Done!"); return(0); }
/// <summary> /// Sets the tags of an addon. /// </summary> /// <param name="addon">The addon to modify.</param> /// <param name="tagsInput">Optional. The new tags the addon should have.</param> private static void SetTags(Addon addon, string[] tagsInput = null) { var tags = new List <string>(2); if (tagsInput == null || tagsInput.Length == 0 || tagsInput[0] == string.Empty) { var allTagsValid = false; while (!allTagsValid) { tags.Clear(); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Tags? "); Console.ResetColor(); Console.Write("Please choose ZERO, ONE or TWO from the following: "); Console.WriteLine(string.Join(" ", Tags.Misc)); tagsInput = Console.ReadLine().Split(' '); allTagsValid = true; if (tagsInput[0] != string.Empty) { // More than zero (one or two) elements: add the first one. if (tagsInput.Length > 0) { if (!Tags.TagExists(tagsInput[0])) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("The specified tag \"" + tagsInput[0] + "\" is not valid."); Console.ResetColor(); allTagsValid = false; continue; } tags.Add(tagsInput[0]); } // More than one (two) elements: add the second one too. if (tagsInput.Length > 1) { if (!Tags.TagExists(tagsInput[1])) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("The specified tag \"" + tagsInput[1] + "\" is not valid."); Console.ResetColor(); allTagsValid = false; continue; } tags.Add(tagsInput[1]); } if (tagsInput.Length > 2) { Console.ForegroundColor = ConsoleColor.DarkYellow; Console.WriteLine("More than two tags specified. Only the first two is saved."); Console.ResetColor(); } } } } else { if (tagsInput[0] != string.Empty) { // More than zero (one or two) elements: add the first one. if (tagsInput.Length > 0) { if (!Tags.TagExists(tagsInput[0])) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("The specified tag \"" + tagsInput[0] + "\" is not valid."); Console.ResetColor(); return; } tags.Add(tagsInput[0]); } // More than one (two) elements: add the second one too. if (tagsInput.Length > 1) { if (!Tags.TagExists(tagsInput[1])) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("The specified tag \"" + tagsInput[1] + "\" is not valid."); Console.ResetColor(); return; } tags.Add(tagsInput[1]); } if (tagsInput.Length > 2) { Console.ForegroundColor = ConsoleColor.DarkYellow; Console.WriteLine("More than two tags specified. Only the first two is saved."); Console.ResetColor(); } } } addon.Tags = tags; }
/// <summary> /// Legacy GMAD operation to create an addon file using contents of a specified folder. /// </summary> /// <param name="strFolder">The folder containing the raw content.</param> /// <param name="strOutfile">The path of the addon file to write.</param> /// <param name="warnInvalid">Whether there should be a warning for files failing to validate /// instead of a full exception halt.</param> /// <returns>Integer error code: 0 if success, 1 if error.</returns> private static int CreateAddonFile(string strFolder, string strOutfile, bool warnInvalid) { // // Make sure there's a slash on the end // strFolder = strFolder.TrimEnd('/'); strFolder = strFolder + "/"; // // Make sure OutFile ends in .gma // strOutfile = Path.GetFileNameWithoutExtension(strOutfile); strOutfile += ".gma"; Console.WriteLine("Looking in folder \"" + strFolder + "\""); Addon addon = null; if (File.Exists(strFolder + Path.DirectorySeparatorChar + "addon.json")) { // Use addon.json for metadata if it exists if (File.Exists(strFolder + Path.DirectorySeparatorChar + "info.txt") || File.Exists(strFolder + Path.DirectorySeparatorChar + "addon.txt")) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Both addon.json and legacy info.txt/addon.txt found in source folder."); Console.WriteLine("addon.json takes priority"); Console.ResetColor(); } // // Load the Addon Info file // Json addonInfo; try { addonInfo = new Json(strFolder + "addon.json"); } catch (AddonJsonException ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(strFolder + "addon.json error: " + ex.Message); Console.ResetColor(); return(1); } addon = new Addon(addonInfo); } else if (File.Exists(strFolder + Path.DirectorySeparatorChar + "info.txt") || File.Exists(strFolder + Path.DirectorySeparatorChar + "addon.txt")) { // Load the addon metadata from the old file structure: info.txt/addon.txt var legacyInfo = string.Empty; try { if (File.Exists(strFolder + Path.DirectorySeparatorChar + "info.txt")) { legacyInfo = File.ReadAllText(strFolder + Path.DirectorySeparatorChar + "info.txt"); } else if (File.Exists(strFolder + Path.DirectorySeparatorChar + "addon.txt")) { legacyInfo = File.ReadAllText(strFolder + Path.DirectorySeparatorChar + "addon.txt"); } } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Failed to read metadata."); Console.ResetColor(); Console.WriteLine(ex.Message); return(1); } addon = new Addon(); // Parse the read data var regex = new Regex("\"([A-Za-z_\r\n]*)\"[\\s]*\"([\\s\\S]*?)\"", RegexOptions.IgnoreCase | RegexOptions.Multiline); var matches = regex.Matches(legacyInfo); // info.txt/addon.txt files usually have these values not directly mapped into GMAs as well. var AuthorName = string.Empty; var AuthorEmail = string.Empty; var AuthorURL = string.Empty; var Version = string.Empty; var Date = string.Empty; foreach (Match keyMatch in matches) { if (keyMatch.Groups.Count == 3) { // All match should have 2 groups matched (the 0th group is the whole match.) switch (keyMatch.Groups[1].Value.ToLowerInvariant()) { case "name": addon.Title = keyMatch.Groups[2].Value; break; case "version": Version = keyMatch.Groups[2].Value; break; case "up_date": Date = keyMatch.Groups[2].Value; break; case "author_name": //addon.Author = keyMatch.Groups[2].Value; // GMAD writer only writes "Author Name" right now... AuthorName = keyMatch.Groups[2].Value; break; case "author_email": AuthorEmail = keyMatch.Groups[2].Value; break; case "author_url": AuthorURL = keyMatch.Groups[2].Value; break; case "info": addon.Description = keyMatch.Groups[2].Value; break; } } } // Prettify the loaded Description. var newDescription = string.Empty; var hasNewDescription = !string.IsNullOrWhiteSpace(AuthorName) || !string.IsNullOrWhiteSpace(AuthorEmail) || !string.IsNullOrWhiteSpace(AuthorURL) || !string.IsNullOrWhiteSpace(Version) || !string.IsNullOrWhiteSpace(Date); if (hasNewDescription) { newDescription = "## Converted by SharpGMad " + Shared.PrettyVersion + " at " + DateTime.Now.ToString("yyyy. MM. dd. hh:mm:ss") + " (+" + TimeZoneInfo.Local.BaseUtcOffset.ToString("hhmm") + ")"; } if (!string.IsNullOrWhiteSpace(AuthorName)) { newDescription += "\n# AuthorName: " + AuthorName; } if (!string.IsNullOrWhiteSpace(AuthorEmail)) { newDescription += "\n# AuthorEmail: " + AuthorEmail; } if (!string.IsNullOrWhiteSpace(AuthorURL)) { newDescription += "\n# AuthorURL: " + AuthorURL; } if (!string.IsNullOrWhiteSpace(Version)) { newDescription += "\n# Version: " + Version; } if (!string.IsNullOrWhiteSpace(Date)) { newDescription += "\n# Date: " + Date; } if (hasNewDescription) { // If anything was added to the prettifiction newDescription += "\n## End conversion info"; addon.Description = newDescription + (!string.IsNullOrWhiteSpace(addon.Description) ? Environment.NewLine + addon.Description : null); } Console.WriteLine("Addon: " + addon.Title); if (hasNewDescription) { Console.WriteLine(newDescription); } Console.ForegroundColor = ConsoleColor.Magenta; Console.WriteLine("You need to set the title, and optionally, the tags for this addon!"); Console.ResetColor(); SetType(addon); SetTags(addon); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Failed to find addon metadata file \"addon.json\", \"info.txt\" or \"addon.txt\""); Console.ResetColor(); return(1); } // // Get a list of files in the specified folder // foreach (var f in Directory.GetFiles(strFolder, "*", SearchOption.AllDirectories)) { var file = f; file = file.Replace(strFolder, string.Empty); file = file.Replace('\\', '/'); if (file == "addon.json" || file == "info.txt") { continue; // Don't read the metadata file } Console.WriteLine("\t" + file); try { addon.CheckRestrictions(file); addon.AddFile(file, File.ReadAllBytes(f)); } catch (IOException) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Unable to read file " + file); Console.ResetColor(); } catch (IgnoredException) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\t\t[Ignored]"); Console.ResetColor(); } catch (WhitelistException) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\t\t[Not allowed by whitelist]"); Console.ResetColor(); if (!warnInvalid) { return(1); } } } // // Sort the list into alphabetical order, no real reason - we're just ODC // addon.Sort(); // // Create an addon file in a buffer // // // Save the buffer to the provided name // FileStream fs; try { fs = new FileStream(strOutfile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); } catch (Exception) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Couldn't save to file \"" + strOutfile + "\""); Console.ResetColor(); return(1); } fs.SetLength(0); try { Writer.Create(addon, fs); } catch (Exception) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Failed to create the addon."); Console.ResetColor(); return(1); } fs.Flush(); // // Success! // Console.WriteLine("Successfully saved to \"" + strOutfile + "\" [" + ((int)fs.Length).HumanReadableSize() + "]"); fs.Dispose(); return(0); }