/** * Creates an SFX of the given source, using the current HXE executable, to the given target. */ public static void Compile(Configuration configuration) { var source = configuration.Source; var target = configuration.Target; var filter = configuration.Filter; target.Create(); /** * We will create an SFX for all of the files residing in the given source directory. Our goal is to contain all * of the data in a single file, and subsequently reproduce the directory tree & file contents. * * For a reasonable balance between space savings and process time, we will DEFLATE each file before including it * in the output SFX binary. * * The structure of the output SFX is essentially: * * [pe executable] + [deflate data] + [sfx object] + [sfx object length] * |-------------| |------------| |----------| |-----------------| * | | | | * | | | +- used for seeking the start of this sfx * | | | object, by reading backwards from EOF * | | | * | | +----------------------- specifies the deflate offsets & output * | | file name + path on the filesystem * | | * | +-------------------------------------- deflate data for each discovered file in * | the source directory * | * +------------------------------------------------------- contents of this executable; essentially * we append the aforementioned data to the * end of this executable */ var sfx = new SFX(); var files = source.GetFiles(filter, SearchOption.AllDirectories); var sourceExe = configuration.Executable; var targetExe = new FileInfo(Combine(target.FullName, sourceExe.Name)); Info($"Source: {source.FullName}"); Info($"Target: {target.FullName}"); Info($"Source EXE: {sourceExe.FullName}"); Info($"Target SFX: {targetExe.FullName}"); if (targetExe.Exists) { targetExe.Delete(); } var sourceSize = files.Sum(x => x.Length); var sfxSize = 0L; /** * We create a copy of this executable for subsequent appending of the DEFLATE & SFX data. We refresh the FileInfo * object to ensure that we have the real length of the executable on the fs. */ sourceExe.CopyTo(targetExe.FullName); targetExe.Refresh(); Info($"Copied {targetExe.Length} bytes to: {targetExe.Length}"); /** * For each discovered file in the given source directory, we will: * * 1. Append a DEFLATE representation of it to the copied HXE executable; * 2. Create an SFX entry specifying: * a) the file name & path on the fs * b) the file length on the fs * c) offset of the DEFLATE data in SFX */ using (var oStream = System.IO.File.Open(targetExe.FullName, Append)) { for (var i = 0; i < files.Length; i++) { var file = files[i]; Info($"Packaging file: {file.Name}"); /** * We append the DEFLATE data to the SFX binary. After the procedure is done, we will refresh the FileInfo * for the SFX to retrieve its new length. This length will be used to determine length of the DEFALTE and thus * its offset in the SFX binary. */ var length = file.Length; long deflateLength; { using (var iStream = file.OpenRead()) using (var dStream = new DeflateStream(oStream, Compress, true)) { iStream.CopyTo(dStream); dStream.Close(); oStream.Flush(true); } WriteLine(NewLine + new string('-', 80)); var oldLength = targetExe.Length; targetExe.Refresh(); var newLength = targetExe.Length; Info($"SFX increased from {oldLength} to {newLength} bytes."); deflateLength = newLength - oldLength; sfxSize += deflateLength; if (deflateLength < length) { Info($"DEFLATE length is thus {(decimal) deflateLength / length * 100:##.##}% of {length} bytes."); } else { Warn($"DEFLATE length is higher by {deflateLength - length} bytes than the raw file length."); } } /** * We will add an entry for the current file and its DEFLATE representation to teach the SFX how to recreate * the file down the line. * * The entries are designed to recreate the structure of the source directory, in a given arbitrary target * directory. As such, we will avoid absoltue paths for the files and instead infer paths relative to the source * directory. * * Each DEFLATE entry will be appended at the end of the SFX binary. To determine where each file's DEFLATE * representation starts, we determine the offset by extracting the DEFLATE length, from the SFX binary * length. */ { var name = file.Name; var offset = targetExe.Length - deflateLength; var path = file.DirectoryName != null && file.DirectoryName.Equals(source.FullName) ? string.Empty : file.DirectoryName?.Substring(source.FullName.Length + 1); Info($"Acknowledging new entry: {targetExe.Name} <= {path}\\{name}"); Info($"DEFLATE starts at offset 0x{offset:x8} in the SFX binary."); sfx.Entries.Add(new Entry { Name = name, Path = path, Length = length, Offset = offset }); targetExe.Refresh(); } WriteLine(NewLine + new string('-', 80)); Info($"Finished packaging file: {file.Name}"); Info($"{files.Length - (i + 1)} files are currently remaining."); WriteLine(NewLine + new string('=', 80)); } /** * Once we have created & appended the DEFLATE data for each file, and also populated the SFX object, we will * serialise it to a byte array which in turn gets appended to the SFX binary as well. This makes the SFX * completely self-contained and portable. * * Because an SFX object's array representation is a variable length, we will append the said length to the binary * as well. This will allow HXE to determine both the start and end of the SFX data when deserialising it, by * seeking backwards from the EOF. By seeking as many bytes backwards as specified in the length value, the start * of the SFX data can be determined. */ { var sfxData = sfx.Serialise(); var sfxOffset = targetExe.Length; Info($"Appending SFX length ({sfxData.Length}) at offset 0x{sfxOffset:x8}."); using (var binWriter = new BinaryWriter(oStream)) { binWriter.Write(sfxData); binWriter.Write(sfxOffset); oStream.Flush(true); binWriter.Close(); } } } { WriteLine(NewLine + new string('*', 80)); Info($"Finished packaging {targetExe.Name} with {files.Length} files."); var percentage = (double)sfxSize / sourceSize * 100; Info($"Source directory size: {sourceSize} bytes"); Info($"SFX DEFLATE data size: {sfxSize} bytes"); Info($"Data has been compressed down to {percentage:##.##}%"); } }
/// <summary> /// Console API to the HXE kernel, installer and compiler. /// </summary> /// <param name="args"> /// --config Opens configuration GUI /// --load Initiates HCE/SPV3 /// --install=VALUE Installs HCE/SPV3 to destination /// --compile=VALUE Compiles HCE/SPV3 to destination /// --console Loads HCE with console mode /// --devmode Loads HCE with developer mode /// --screenshot Loads HCE with screenshot ability /// --window Loads HCE in window mode /// --nogamma Loads HCE without gamma overriding /// --adapter=VALUE Loads HCE on monitor X /// --path=VALUE Loads HCE with custom profile path /// --vidmode=VALUE Loads HCE with video mode /// </param> private static void InvokeProgram(string[] args) { Directory.CreateDirectory(Paths.Directory); var help = false; /* Displays commands list */ var config = false; /* Opens configuration GUI */ var positions = false; /* Opens positions GUI */ var install = string.Empty; /* Installs HCE/SPV3 to destination */ var compile = string.Empty; /* Compiles HCE/SPV3 to destination */ var update = string.Empty; /* Updates directory using manifest */ var registry = string.Empty; /* Write to Windows Registry */ var infer = false; /* Infer the running Halo executable */ var console = false; /* Loads HCE with console mode */ var devmode = false; /* Loads HCE with developer mode */ var screenshot = false; /* Loads HCE with screenshot ability */ var window = false; /* Loads HCE in window mode */ var nogamma = false; /* Loads HCE without gamma overriding */ var adapter = string.Empty; /* Loads HCE on monitor X */ var path = string.Empty; /* Loads HCE with custom profile path */ var exec = string.Empty; /* Loads HCE with custom init file */ var vidmode = string.Empty; /* Loads HCE with custom res. and Hz */ var refresh = string.Empty; /* Loads HCE with custom refresh rate */ var options = new OptionSet() .Add("help", "Displays commands list", s => help = s != null) /* hxe command */ .Add("config", "Opens configuration GUI", s => config = s != null) /* hxe command */ .Add("positions", "Opens positions GUI", s => positions = s != null) /* hxe command */ .Add("install=", "Installs HCE/SPV3 to destination", s => install = s) /* hxe parameter */ .Add("compile=", "Compiles HCE/SPV3 to destination", s => compile = s) /* hxe parameter */ .Add("update=", "Updates directory using manifest", s => update = s) /* hxe parameter */ .Add("registry=", "Create Registry keys for Retail, Custom, Trial, or HEK", s => registry = s) /* hxe parameter */ .Add("infer", "Infer the running Halo executable", s => infer = s != null) /* hxe parameter */ .Add("console", "Loads HCE with console mode", s => console = s != null) /* hce parameter */ .Add("devmode", "Loads HCE with developer mode", s => devmode = s != null) /* hce parameter */ .Add("screenshot", "Loads HCE with screenshot ability", s => screenshot = s != null) /* hce parameter */ .Add("window", "Loads HCE in window mode", s => window = s != null) /* hce parameter */ .Add("nogamma", "Loads HCE without gamma overriding", s => nogamma = s != null) /* hce parameter */ .Add("adapter=", "Loads HCE on monitor X", s => adapter = s) /* hce parameter */ .Add("path=", "Loads HCE with custom profile path", s => path = s) /* hce parameter */ .Add("exec=", "Loads HCE with custom init file", s => exec = s) /* hce parameter */ .Add("vidmode=", "Loads HCE with custom res. and Hz", s => vidmode = s) /* hce parameter */ .Add("refresh=", "Loads HCE with custom refresh rate", s => refresh = s); /* hce parameter */ var input = options.Parse(args); foreach (var i in input) { Info("Discovered CLI command: " + i); } var hce = new Executable(); if (help) { options.WriteOptionDescriptions(Out); Exit(0); } if (config) { new Application().Run(new Settings()); Exit(0); } if (positions) { new Application().Run(new Positions()); Exit(0); } if (infer) { var descriptions = new Dictionary <Process.Type, string> { { Process.Type.Unknown, "N/A" }, { Process.Type.Retail, "Halo: Combat Evolved" }, { Process.Type.HCE, "Halo: Custom Edition" }, { Process.Type.Steam, "Halo: MCC - CEA (Steam)" }, { Process.Type.Store, "Halo: MCC - CEA (Store)" }, }; Info($"Inferred the following Halo process: {descriptions[Process.Infer()]}"); Info("Press any key to exit."); ReadLine(); Exit(0); } if (!string.IsNullOrWhiteSpace(install)) { Run(() => { SFX.Extract(new SFX.Configuration { Target = new DirectoryInfo(install) }); }); } if (!string.IsNullOrWhiteSpace(compile)) { Run(() => { SFX.Compile(new SFX.Configuration { Source = new DirectoryInfo(CurrentDirectory), Target = new DirectoryInfo(compile) }); }); } if (!string.IsNullOrWhiteSpace(update)) { Run(() => { var updateModule = new Update(); updateModule.Import(update); updateModule.Commit(); }); } if (!string.IsNullOrWhiteSpace(registry)) { } /** * Implicit verification for legal HCE installations. */ try { hce = Executable.Detect(); } catch (Exception e) { var msg = " -- Legal copy of HCE needs to be installed for loading!\n Error: " + e.ToString() + "\n"; var log = (File)Paths.Exception; log.AppendAllText(msg); Error(msg); } if (console) { hce.Debug.Console = true; } if (devmode) { hce.Debug.Developer = true; } if (screenshot) { hce.Debug.Screenshot = true; } if (window) { hce.Video.Window = true; } if (nogamma) { hce.Video.NoGamma = true; } if (!string.IsNullOrWhiteSpace(adapter)) { hce.Video.Adapter = byte.Parse(adapter); } if (!string.IsNullOrWhiteSpace(path)) { hce.Profile.Path = path; } if (!string.IsNullOrWhiteSpace(exec)) { hce.Debug.Initiation = exec; } if (!string.IsNullOrWhiteSpace(vidmode)) { var a = vidmode.Split(','); if (a.Length < 2) { return; } hce.Video.DisplayMode = true; hce.Video.Width = ushort.Parse(a[0]); hce.Video.Height = ushort.Parse(a[1]); if (a.Length > 2) /* optional refresh rate */ { hce.Video.Refresh = ushort.Parse(a[2]); } } if (!string.IsNullOrWhiteSpace(refresh)) { hce.Video.Refresh = ushort.Parse(refresh); } /** * Implicitly invoke the HXE kernel with the HCE loading procedure. */ Run(() => { Kernel.Invoke(hce); }); /** * This method is used for running code asynchronously and catching exceptions at the highest level. */ void Run(Action action) { try { Task.Run(action).GetAwaiter().GetResult(); WithCode(Code.Success); } catch (Exception e) { var msg = " -- EXEC.START HALTED\n Error: " + e.ToString() + "\n"; var log = (File)Paths.Exception; log.AppendAllText(msg); Error(msg); System.Console.Error.WriteLine("\n\n" + e.StackTrace); WithCode(Code.Exception); } } }
/** * Extracts the SFX contents of the current HXE executable to the given target. */ public static void Extract(Configuration configuration) { var target = configuration.Target; target.Create(); /** * We will assume that the current HXE executable contains SFX data to be extracted. */ var exe = configuration.Executable; var sfx = new SFX(); Info($"Determining SFX structure in {exe.Name}"); /** * We first hydrate the SFX object with information from the SFX binary. The start of the SFX data is inferred * from the length specified at the EOF. We also need to keep in mind the length of the variable that specifies * the length of the SFX data. Since it's a long, the size would be 8. * * With the above confusion in mind: * - Starting offset of the SFX data = (hxe length - sizeof(long) - sfx data length) * - Ending offset of the SFX data = (hxe length - sizeof(long)) */ using (var binReader = new BinaryReader(exe.OpenRead())) { binReader.BaseStream.Seek(exe.Length - sizeof(long), Begin); binReader.BaseStream.Seek(binReader.ReadInt64(), Begin); sfx.Deserialise ( binReader.ReadBytes ( (int)exe.Length - (int)binReader.BaseStream.Position - sizeof(long) ) ); } /** * For each entry specified in the SFX object, we seek its DEFLATE data and extract it to the fs at the given path * and filename. Because .NET doesn't support copying a specified amount of bytes from a stream to another, we are * implementing our own method for copying the data using buffers. */ for (var i = 0; i < sfx.Entries.Count; i++) { var entry = sfx.Entries[i]; var original = new FileInfo(Combine(target.FullName, entry.Path, entry.Name)); original.Directory?.Create(); Info($"Found {entry.Length} bytes at 0x{entry.Offset:x8}: {entry.Name}"); Info($"Extracting its DEFLATE data to: {original.FullName}"); using (var iStream = exe.OpenRead()) using (var oStream = original.Create()) using (var dStream = new DeflateStream(iStream, Decompress)) { dStream.BaseStream.Seek(entry.Offset, Begin); var bytes = entry.Length; var buffer = new byte[0x8000]; int read; while (bytes > 0 && (read = dStream.Read(buffer, 0, Math.Min(buffer.Length, (int)bytes))) > 0) { oStream.Write(buffer, 0, read); bytes -= read; } } WriteLine(NewLine + new string('-', 80)); Info($"Finished extracting file: {entry.Name}"); Info($"{sfx.Entries.Count - (i + 1)} files are currently remaining."); WriteLine(NewLine + new string('=', 80)); } }