#region Copyright & License Information /* * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation. For more information, * see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using OpenRA.FileFormats; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Scripting; using OpenRA.Traits; namespace OpenRA.Utility { public static class Command { static IEnumerable GlobArgs(string[] args, int startIndex = 1) { for (var i = startIndex; i < args.Length; i++) foreach (var path in Glob.Expand(args[i])) yield return path; } [Desc("KEY", "Get value of KEY from settings.yaml")] public static void Settings(string[] args) { if (args.Length < 2) { Console.WriteLine("Error: Invalid syntax"); return; } var section = args[1].Split('.')[0]; var field = args[1].Split('.')[1]; var settings = new Settings(Platform.SupportDir + "settings.yaml", Arguments.Empty); var result = settings.Sections[section].GetType().GetField(field).GetValue(settings.Sections[section]); Console.WriteLine(result); } [Desc("PNGFILE [PNGFILE ...]", "Combine a list of PNG images into a SHP")] public static void ConvertPngToShp(string[] args) { var inputFiles = GlobArgs(args).OrderBy(a => a).ToList(); var dest = inputFiles[0].Split('-').First() + ".shp"; var frames = inputFiles.Select(a => PngLoader.Load(a)); var size = frames.First().Size; if (frames.Any(f => f.Size != size)) throw new InvalidOperationException("All frames must be the same size"); using (var destStream = File.Create(dest)) ShpReader.Write(destStream, size, frames.Select(f => f.ToBytes())); Console.WriteLine(dest + " saved."); } static byte[] ToBytes(this Bitmap bitmap) { var data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); var bytes = new byte[bitmap.Width * bitmap.Height]; for (var i = 0; i < bitmap.Height; i++) Marshal.Copy(new IntPtr(data.Scan0.ToInt64() + i * data.Stride), bytes, i * bitmap.Width, bitmap.Width); bitmap.UnlockBits(data); return bytes; } [Desc("SPRITEFILE PALETTE [--noshadow] [--nopadding]", "Convert a shp/tmp/R8 to a series of PNGs, optionally removing shadow")] public static void ConvertSpriteToPng(string[] args) { var src = args[1]; var shadowIndex = new int[] { }; if (args.Contains("--noshadow")) { Array.Resize(ref shadowIndex, shadowIndex.Length + 3); shadowIndex[shadowIndex.Length - 1] = 1; shadowIndex[shadowIndex.Length - 2] = 3; shadowIndex[shadowIndex.Length - 3] = 4; } var palette = new ImmutablePalette(args[2], shadowIndex); ISpriteSource source; using (var stream = File.OpenRead(src)) source = SpriteSource.LoadSpriteSource(stream, src); // The r8 padding requires external information that we can't access here. var usePadding = !(args.Contains("--nopadding") || source is R8Reader); var count = 0; var prefix = Path.GetFileNameWithoutExtension(src); foreach (var frame in source.Frames) { var frameSize = usePadding ? frame.FrameSize : frame.Size; var offset = usePadding ? (frame.Offset - 0.5f * new float2(frame.Size - frame.FrameSize)).ToInt2() : int2.Zero; // shp(ts) may define empty frames if (frameSize.Width == 0 && frameSize.Height == 0) { count++; continue; } using (var bitmap = new Bitmap(frameSize.Width, frameSize.Height, PixelFormat.Format8bppIndexed)) { bitmap.Palette = palette.AsSystemPalette(); var data = bitmap.LockBits(new Rectangle(0, 0, frameSize.Width, frameSize.Height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); // Clear the frame if (usePadding) { var clearRow = new byte[data.Stride]; for (var i = 0; i < frameSize.Height; i++) Marshal.Copy(clearRow, 0, new IntPtr(data.Scan0.ToInt64() + i * data.Stride), data.Stride); } for (var i = 0; i < frame.Size.Height; i++) { var destIndex = new IntPtr(data.Scan0.ToInt64() + (i + offset.Y) * data.Stride + offset.X); Marshal.Copy(frame.Data, i * frame.Size.Width, destIndex, frame.Size.Width); } bitmap.UnlockBits(data); var filename = "{0}-{1:D4}.png".F(prefix, count++); bitmap.Save(filename); } } Console.WriteLine("Saved {0}-[0..{1}].png", prefix, count - 1); } [Desc("MOD FILES", "Extract files from mod packages to the current directory")] public static void ExtractFiles(string[] args) { var mod = args[1]; var files = args.Skip(2); var manifest = new Manifest(mod); GlobalFileSystem.LoadFromManifest(manifest); foreach (var f in files) { var src = GlobalFileSystem.Open(f); if (src == null) throw new InvalidOperationException("File not found: {0}".F(f)); var data = src.ReadAllBytes(); File.WriteAllBytes(f, data); Console.WriteLine(f + " saved."); } } static int ColorDistance(uint a, uint b) { var ca = Color.FromArgb((int)a); var cb = Color.FromArgb((int)b); return Math.Abs((int)ca.R - (int)cb.R) + Math.Abs((int)ca.G - (int)cb.G) + Math.Abs((int)ca.B - (int)cb.B); } [Desc("SRCMOD:PAL DESTMOD:PAL SRCSHP DESTSHP", "Remap SHPs to another palette")] public static void RemapShp(string[] args) { var remap = new Dictionary(); /* the first 4 entries are fixed */ for (var i = 0; i < 4; i++) remap[i] = i; var srcMod = args[1].Split(':')[0]; Game.modData = new ModData(srcMod); GlobalFileSystem.LoadFromManifest(Game.modData.Manifest); var srcRules = Game.modData.RulesetCache.LoadDefaultRules(); var srcPaletteInfo = srcRules.Actors["player"].Traits.Get(); var srcRemapIndex = srcPaletteInfo.RemapIndex; var destMod = args[2].Split(':')[0]; Game.modData = new ModData(destMod); GlobalFileSystem.LoadFromManifest(Game.modData.Manifest); var destRules = Game.modData.RulesetCache.LoadDefaultRules(); var destPaletteInfo = destRules.Actors["player"].Traits.Get(); var destRemapIndex = destPaletteInfo.RemapIndex; var shadowIndex = new int[] { }; // the remap range is always 16 entries, but their location and order changes for (var i = 0; i < 16; i++) remap[PlayerColorRemap.GetRemapIndex(srcRemapIndex, i)] = PlayerColorRemap.GetRemapIndex(destRemapIndex, i); // map everything else to the best match based on channel-wise distance var srcPalette = new ImmutablePalette(args[1].Split(':')[1], shadowIndex); var destPalette = new ImmutablePalette(args[2].Split(':')[1], shadowIndex); for (var i = 0; i < Palette.Size; i++) if (!remap.ContainsKey(i)) remap[i] = Enumerable.Range(0, Palette.Size) .Where(a => !remap.ContainsValue(a)) .MinBy(a => ColorDistance(destPalette[a], srcPalette[i])); var srcImage = ShpReader.Load(args[3]); using (var destStream = File.Create(args[4])) ShpReader.Write(destStream, srcImage.Size, srcImage.Frames.Select(im => im.Data.Select(px => (byte)remap[px]).ToArray())); } [Desc("SRCSHP DESTSHP START N M [START N M ...]", "Transpose the N*M block of frames starting at START.")] public static void TransposeShp(string[] args) { var srcImage = ShpReader.Load(args[1]); var srcFrames = srcImage.Frames; var destFrames = srcImage.Frames.ToArray(); for (var z = 3; z < args.Length - 2; z += 3) { var start = Exts.ParseIntegerInvariant(args[z]); var m = Exts.ParseIntegerInvariant(args[z + 1]); var n = Exts.ParseIntegerInvariant(args[z + 2]); for (var i = 0; i < m; i++) for (var j = 0; j < n; j++) destFrames[start + i * n + j] = srcFrames[start + j * m + i]; } using (var destStream = File.Create(args[2])) ShpReader.Write(destStream, srcImage.Size, destFrames.Select(f => f.Data)); } static string FriendlyTypeName(Type t) { if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)) return "Dictionary<{0},{1}>".F(t.GetGenericArguments().Select(FriendlyTypeName).ToArray()); if (t.IsSubclassOf(typeof(Array))) return "Multiple {0}".F(FriendlyTypeName(t.GetElementType())); if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(OpenRA.Primitives.Cache<,>)) return "Cached<{0},{1}>".F(t.GetGenericArguments().Select(FriendlyTypeName).ToArray()); if (t == typeof(int) || t == typeof(uint)) return "Integer"; if (t == typeof(int2)) return "2D Integer"; if (t == typeof(float) || t == typeof(decimal)) return "Real Number"; if (t == typeof(float2)) return "2D Real Number"; if (t == typeof(CPos)) return "2D Cell Position"; if (t == typeof(CVec)) return "2D Cell Vector"; if (t == typeof(WAngle)) return "1D World Angle"; if (t == typeof(WRot)) return "3D World Rotation"; if (t == typeof(WPos)) return "3D World Position"; if (t == typeof(WRange)) return "1D World Range"; if (t == typeof(WVec)) return "3D World Vector"; return t.Name; } [Desc("MOD", "Generate trait documentation in MarkDown format.")] public static void ExtractTraitDocs(string[] args) { Game.modData = new ModData(args[1]); Console.WriteLine( "This documentation is aimed at modders. It displays all traits with default values and developer commentary. " + "Please do not edit it directly, but add new `[Desc(\"String\")]` tags to the source code. This file has been " + "automatically generated for version {0} of OpenRA.", Game.modData.Manifest.Mod.Version); Console.WriteLine(); var toc = new StringBuilder(); var doc = new StringBuilder(); foreach (var t in Game.modData.ObjectCreator.GetTypesImplementing().OrderBy(t => t.Namespace)) { if (t.ContainsGenericParameters || t.IsAbstract) continue; // skip helpers like TraitInfo var traitName = t.Name.EndsWith("Info") ? t.Name.Substring(0, t.Name.Length - 4) : t.Name; toc.AppendLine("* [{0}](#{1})".F(traitName, traitName.ToLowerInvariant())); var traitDescLines = t.GetCustomAttributes(false).SelectMany(d => d.Lines); doc.AppendLine(); doc.AppendLine("### {0}".F(traitName)); foreach (var line in traitDescLines) doc.AppendLine(line); var infos = FieldLoader.GetTypeLoadInfo(t); if (!infos.Any()) continue; doc.AppendLine(""); doc.AppendLine(""); var liveTraitInfo = Game.modData.ObjectCreator.CreateBasic(t); foreach (var info in infos) { var fieldDescLines = info.Field.GetCustomAttributes(true).SelectMany(d => d.Lines); var fieldType = FriendlyTypeName(info.Field.FieldType); var defaultValue = FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value; doc.Append("".F(info.YamlName, defaultValue, fieldType)); doc.Append(""); } doc.AppendLine("
PropertyDefault ValueTypeDescription
{0}{1}{2}"); foreach (var line in fieldDescLines) doc.Append(line + " "); doc.AppendLine("
"); } Console.Write(toc.ToString()); Console.Write(doc.ToString()); } static string[] RequiredTraitNames(Type t) { // Returns the inner types of all the Requires interfaces on this type var outer = t.GetInterfaces() .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>)); // Get the inner types var inner = outer.SelectMany(i => i.GetGenericArguments()).ToArray(); // Remove the namespace and the trailing "Info" return inner.Select(i => i.Name.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()) .Select(s => s.EndsWith("Info") ? s.Remove(s.Length - 4, 4) : s) .ToArray(); } [Desc("MOD", "Generate Lua API documentation in MarkDown format.")] public static void ExtractLuaDocs(string[] args) { Game.modData = new ModData(args[1]); Console.WriteLine("This is an automatically generated lising of the new Lua map scripting API, generated for {0} of OpenRA.", Game.modData.Manifest.Mod.Version); Console.WriteLine(); Console.WriteLine("OpenRA allows custom maps and missions to be scripted using Lua 5.1.\n" + "These scripts run in a sandbox that prevents access to unsafe functions (e.g. OS or file access), " + "and limits the memory and CPU usage of the scripts."); Console.WriteLine(); Console.WriteLine("You can access this interface by adding the [LuaScript](Traits#luascript) trait to the world actor in your map rules (note, you must replace the spaces in the snippet below with a single tab for each level of indentation):"); Console.WriteLine("```\nRules:\n\tWorld:\n\t\tLuaScript:\n\t\t\tScripts: myscript.lua\n```"); Console.WriteLine(); Console.WriteLine("Map scripts can interact with the game engine in three ways:\n" + "* Global tables provide functions for interacting with the global world state, or performing general helper tasks.\n" + "They exist in the global namespace, and can be called directly using ```.```.\n" + "* Individual actors expose a collection of properties and commands that query information of modify their state.\n" + " * Some commands, marked as queued activity, are asynchronous. Activities are queued on the actor, and will run in " + "sequence until the queue is empty or the Stop command is called. Actors that are not performing an activity are Idle " + "(actor.IsIdle will return true). The properties and commands available on each actor depends on the traits that the actor " + "specifies in its rule definitions.\n" + "* Individual players explose a collection of properties and commands that query information of modify their state.\n" + "The properties and commands available on each actor depends on the traits that the actor specifies in its rule definitions.\n"); Console.WriteLine(); var tables = Game.modData.ObjectCreator.GetTypesImplementing() .OrderBy(t => t.Name); Console.WriteLine("

Global Tables

"); foreach (var t in tables) { var name = t.GetCustomAttributes(true).First().Name; var members = ScriptMemberWrapper.WrappableMembers(t); Console.WriteLine("
", name); foreach (var m in members.OrderBy(m => m.Name)) { var desc = m.HasAttribute() ? m.GetCustomAttributes(true).First().Lines.JoinWith("\n") : ""; Console.WriteLine("".F(m.LuaDocString(), desc)); } Console.WriteLine("
{0}
{0}{1}
"); } Console.WriteLine("

Actor Properties / Commands

"); var actorCategories = Game.modData.ObjectCreator.GetTypesImplementing().SelectMany(cg => { var catAttr = cg.GetCustomAttributes(false).FirstOrDefault(); var category = catAttr != null ? catAttr.Category : "Unsorted"; var required = RequiredTraitNames(cg); return ScriptMemberWrapper.WrappableMembers(cg).Select(mi => Tuple.Create(category, mi, required)); }).GroupBy(g => g.Item1).OrderBy(g => g.Key); foreach (var kv in actorCategories) { Console.WriteLine("", kv.Key); foreach (var property in kv.OrderBy(p => p.Item2.Name)) { var mi = property.Item2; var required = property.Item3; var hasDesc = mi.HasAttribute(); var hasRequires = required.Any(); var isActivity = mi.HasAttribute(); Console.WriteLine(""); } Console.WriteLine("
{0}
{0}", mi.LuaDocString()); if (isActivity) Console.WriteLine("
Queued Activity"); Console.WriteLine("
"); if (hasDesc) Console.WriteLine(mi.GetCustomAttributes(false).First().Lines.JoinWith("\n")); if (hasDesc && hasRequires) Console.WriteLine("
"); if (hasRequires) Console.WriteLine("Requires {1}: {0}".F(required.JoinWith(", "), required.Length == 1 ? "Trait" : "Traits")); Console.WriteLine("
"); } Console.WriteLine("

Player Properties / Commands

"); var playerCategories = Game.modData.ObjectCreator.GetTypesImplementing().SelectMany(cg => { var catAttr = cg.GetCustomAttributes(false).FirstOrDefault(); var category = catAttr != null ? catAttr.Category : "Unsorted"; var required = RequiredTraitNames(cg); return ScriptMemberWrapper.WrappableMembers(cg).Select(mi => Tuple.Create(category, mi, required)); }).GroupBy(g => g.Item1).OrderBy(g => g.Key); foreach (var kv in playerCategories) { Console.WriteLine("", kv.Key); foreach (var property in kv.OrderBy(p => p.Item2.Name)) { var mi = property.Item2; var required = property.Item3; var hasDesc = mi.HasAttribute(); var hasRequires = required.Any(); var isActivity = mi.HasAttribute(); Console.WriteLine(""); } Console.WriteLine("
{0}
{0}", mi.LuaDocString()); if (isActivity) Console.WriteLine("
Queued Activity"); Console.WriteLine("
"); if (hasDesc) Console.WriteLine(mi.GetCustomAttributes(false).First().Lines.JoinWith("\n")); if (hasDesc && hasRequires) Console.WriteLine("
"); if (hasRequires) Console.WriteLine("Requires {1}: {0}".F(required.JoinWith(", "), required.Length == 1 ? "Trait" : "Traits")); Console.WriteLine("
"); } } [Desc("MAPFILE", "Generate hash of specified oramap file.")] public static void GetMapHash(string[] args) { var result = new Map(args[1]).Uid; Console.WriteLine(result); } [Desc("MAPFILE", "Render PNG minimap of specified oramap file.")] public static void GenerateMinimap(string[] args) { var map = new Map(args[1]); Game.modData = new ModData(map.RequiresMod); GlobalFileSystem.UnmountAll(); foreach (var dir in Game.modData.Manifest.Folders) GlobalFileSystem.Mount(dir); var minimap = Minimap.RenderMapPreview(map.Rules.TileSets[map.Tileset], map, true); var dest = Path.GetFileNameWithoutExtension(args[1]) + ".png"; minimap.Save(dest); Console.WriteLine(dest + " saved."); } [Desc("MAPFILE", "MOD", "Upgrade a version 5 map to version 6.")] public static void UpgradeV5Map(string[] args) { var map = args[1]; var mod = args[2]; Game.modData = new ModData(mod); new Map(map, mod); } [Desc("MOD", "FILENAME", "Convert a legacy INI/MPR map to the OpenRA format.")] public static void ImportLegacyMap(string[] args) { var mod = args[1]; var filename = args[2]; Game.modData = new ModData(mod); var rules = Game.modData.RulesetCache.LoadDefaultRules(); var map = LegacyMapImporter.Import(filename, rules, e => Console.WriteLine(e)); map.RequiresMod = mod; map.MakeDefaultPlayers(); map.FixOpenAreas(rules); var dest = map.Title + ".oramap"; map.Save(dest); Console.WriteLine(dest + " saved."); } } }