mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-04-05 22:01:33 +00:00
368 lines
15 KiB
C#
368 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using SabreTools.Data.Models.WiseInstaller;
|
|
using SabreTools.Data.Models.WiseInstaller.Actions;
|
|
using SabreTools.Numerics.Extensions;
|
|
|
|
namespace SabreTools.Wrappers
|
|
{
|
|
public partial class WiseScript : WrapperBase<ScriptFile>
|
|
{
|
|
#region Descriptive Properties
|
|
|
|
/// <inheritdoc/>
|
|
public override string DescriptionString => "Wise Installer Script File";
|
|
|
|
#endregion
|
|
|
|
#region Extension Properties
|
|
|
|
/// <inheritdoc cref="ScriptHeader.DateTime"/>
|
|
public uint DateTime => Model.Header.DateTime;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.Flags"/>
|
|
public ushort Flags => Model.Header.Flags;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.FontSize"/>
|
|
public uint FontSize => Model.Header.FontSize;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.FTPURL"/>
|
|
public string? FTPURL => Model.Header.FTPURL;
|
|
|
|
/// <inheritdoc cref="ScriptFile.HeaderStrings"/>
|
|
public string[]? HeaderStrings => Model.Header.HeaderStrings;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.LanguageCount"/>
|
|
public byte LanguageCount => Model.Header.LanguageCount;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.LogPathname"/>
|
|
public string? LogPathname => Model.Header.LogPathname;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.MessageFont"/>
|
|
public string? MessageFont => Model.Header.MessageFont;
|
|
|
|
/// <inheritdoc cref="ScriptFile.States"/>
|
|
public MachineState[] States => Model.States;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.UnknownU16_1"/>
|
|
public ushort UnknownU16_1 => Model.Header.UnknownU16_1;
|
|
|
|
/// <inheritdoc cref="ScriptHeader.UnknownU16_2"/>
|
|
public ushort UnknownU16_2 => Model.Header.UnknownU16_2;
|
|
|
|
/// <inheritdoc cref="ScriptFile.VariableLengthData"/>
|
|
public byte[]? VariableLengthData => Model.Header.VariableLengthData;
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <inheritdoc/>
|
|
public WiseScript(ScriptFile model, byte[] data) : base(model, data) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WiseScript(ScriptFile model, byte[] data, int offset) : base(model, data, offset) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WiseScript(ScriptFile model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WiseScript(ScriptFile model, Stream data) : base(model, data) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WiseScript(ScriptFile model, Stream data, long offset) : base(model, data, offset) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WiseScript(ScriptFile model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
|
|
|
#endregion
|
|
|
|
#region Static Constructors
|
|
|
|
/// <summary>
|
|
/// Create a Wise installer script file from a byte array and offset
|
|
/// </summary>
|
|
/// <param name="data">Byte array representing the script</param>
|
|
/// <param name="offset">Offset within the array to parse</param>
|
|
/// <returns>A Wise installer script file wrapper on success, null on failure</returns>
|
|
public static WiseScript? Create(byte[]? data, int offset)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || data.Length == 0)
|
|
return null;
|
|
|
|
// If the offset is out of bounds
|
|
if (offset < 0 || offset >= data.Length)
|
|
return null;
|
|
|
|
// Create a memory stream and use that
|
|
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
|
return Create(dataStream);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a Wise installer script file from a Stream
|
|
/// </summary>
|
|
/// <param name="data">Stream representing the script</param>
|
|
/// <returns>A Wise installer script file wrapper on success, null on failure</returns>
|
|
public static WiseScript? Create(Stream? data)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || !data.CanRead)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
// Cache the current offset
|
|
long currentOffset = data.Position;
|
|
|
|
var model = new Serialization.Readers.WiseScript().Deserialize(data);
|
|
if (model is null)
|
|
return null;
|
|
|
|
return new WiseScript(model, data, currentOffset);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Processing
|
|
|
|
/// <summary>
|
|
/// Process the state machine and perform all required actions
|
|
/// </summary>
|
|
/// <param name="header">Overlay header used for reference</param>
|
|
/// <param name="sourceDirectory">Directory where installer files live, if possible</param>
|
|
/// <param name="outputDirectory">Output directory to write to</param>
|
|
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
|
/// <returns>True if there were no errors during processing, false otherwise</returns>
|
|
public bool ProcessStateMachine(WiseOverlayHeader header,
|
|
string? sourceDirectory,
|
|
string outputDirectory,
|
|
bool includeDebug)
|
|
{
|
|
// If the state machine is invalid
|
|
if (States.Length == 0)
|
|
return false;
|
|
|
|
// Initialize important loop information
|
|
int normalFileCount = 0;
|
|
Dictionary<string, string> environment = [];
|
|
if (sourceDirectory is not null)
|
|
environment.Add("INST", sourceDirectory);
|
|
|
|
// Loop through the state machine and process
|
|
foreach (var state in States)
|
|
{
|
|
#pragma warning disable IDE0010
|
|
switch (state.Op)
|
|
{
|
|
case OperationCode.InstallFile:
|
|
if (state.Data is not InstallFile fileHeader)
|
|
return false;
|
|
|
|
// Try to extract to the output directory
|
|
header.ExtractFile(fileHeader, ++normalFileCount, outputDirectory, includeDebug);
|
|
break;
|
|
|
|
case OperationCode.EditIniFile:
|
|
if (state.Data is not EditIniFile editIniFile)
|
|
return false;
|
|
|
|
// Try to write to the output directory
|
|
WriteIniData(editIniFile, outputDirectory, ++normalFileCount);
|
|
break;
|
|
|
|
case OperationCode.DisplayBillboard:
|
|
if (state.Data is not DisplayBillboard displayBillboard)
|
|
return false;
|
|
|
|
// Try to extract to the output directory
|
|
header.ExtractFile(displayBillboard, outputDirectory, includeDebug);
|
|
break;
|
|
|
|
case OperationCode.DeleteFile:
|
|
if (state.Data is not DeleteFile deleteFile)
|
|
return false;
|
|
|
|
if (includeDebug) Console.WriteLine($"File {deleteFile.Pathname} is supposed to be deleted");
|
|
break;
|
|
|
|
case OperationCode.CreateDirectory:
|
|
if (state.Data is not CreateDirectory createDirectory)
|
|
return false;
|
|
if (createDirectory.Pathname is null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
if (includeDebug) Console.WriteLine($"Directory {createDirectory.Pathname} is being created");
|
|
|
|
// Ensure directory separators are consistent
|
|
string newDirectoryName = Path.Combine(outputDirectory, createDirectory.Pathname);
|
|
if (Path.DirectorySeparatorChar == '\\')
|
|
newDirectoryName = newDirectoryName.Replace('/', '\\');
|
|
else if (Path.DirectorySeparatorChar == '/')
|
|
newDirectoryName = newDirectoryName.Replace('\\', '/');
|
|
|
|
// Perform path replacements
|
|
foreach (var kvp in environment)
|
|
{
|
|
newDirectoryName = newDirectoryName.Replace($"%{kvp.Key}%", kvp.Value);
|
|
}
|
|
|
|
newDirectoryName = newDirectoryName.Replace("%", string.Empty);
|
|
|
|
// Remove wildcards from end of the path
|
|
if (newDirectoryName.EndsWith("*.*"))
|
|
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
|
|
newDirectoryName = newDirectoryName[..^4];
|
|
#else
|
|
newDirectoryName = newDirectoryName.Substring(0, newDirectoryName.Length - 4);
|
|
#endif
|
|
|
|
Directory.CreateDirectory(newDirectoryName);
|
|
}
|
|
catch
|
|
{
|
|
if (includeDebug) Console.WriteLine($"Directory {createDirectory.Pathname} could not be created!");
|
|
}
|
|
|
|
break;
|
|
|
|
case OperationCode.CopyLocalFile:
|
|
if (state.Data is not CopyLocalFile copyLocalFile)
|
|
return false;
|
|
if (copyLocalFile.Source is null)
|
|
return false;
|
|
if (copyLocalFile.Destination is null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
if (includeDebug) Console.WriteLine($"File {copyLocalFile.Source} is being copied to {copyLocalFile.Destination}");
|
|
|
|
// Ensure directory separators are consistent
|
|
string oldFilePath = copyLocalFile.Source;
|
|
if (Path.DirectorySeparatorChar == '\\')
|
|
oldFilePath = oldFilePath.Replace('/', '\\');
|
|
else if (Path.DirectorySeparatorChar == '/')
|
|
oldFilePath = oldFilePath.Replace('\\', '/');
|
|
|
|
// Perform path replacements
|
|
foreach (var kvp in environment)
|
|
{
|
|
oldFilePath = oldFilePath.Replace($"%{kvp.Key}%", kvp.Value);
|
|
}
|
|
|
|
oldFilePath = oldFilePath.Replace("%", string.Empty);
|
|
|
|
// Sanity check
|
|
if (!File.Exists(oldFilePath))
|
|
{
|
|
if (includeDebug) Console.WriteLine($"File {copyLocalFile.Source} is supposed to be copied to {copyLocalFile.Destination}, but it does not exist!");
|
|
break;
|
|
}
|
|
|
|
// Ensure directory separators are consistent
|
|
string newFilePath = Path.Combine(outputDirectory, copyLocalFile.Destination);
|
|
if (Path.DirectorySeparatorChar == '\\')
|
|
newFilePath = newFilePath.Replace('/', '\\');
|
|
else if (Path.DirectorySeparatorChar == '/')
|
|
newFilePath = newFilePath.Replace('\\', '/');
|
|
|
|
// Perform path replacements
|
|
foreach (var kvp in environment)
|
|
{
|
|
newFilePath = newFilePath.Replace($"%{kvp.Key}%", kvp.Value);
|
|
}
|
|
|
|
newFilePath = newFilePath.Replace("%", string.Empty);
|
|
|
|
// Sanity check
|
|
string? newFileDirectory = Path.GetDirectoryName(newFilePath);
|
|
if (newFileDirectory is not null && !Directory.Exists(newFileDirectory))
|
|
Directory.CreateDirectory(newFileDirectory);
|
|
|
|
File.Copy(oldFilePath, newFilePath);
|
|
}
|
|
catch
|
|
{
|
|
if (includeDebug) Console.WriteLine($"File {copyLocalFile.Source} could not be copied!");
|
|
}
|
|
|
|
break;
|
|
|
|
case OperationCode.CustomDialogSet:
|
|
if (state.Data is not CustomDialogSet customDialogSet)
|
|
return false;
|
|
|
|
// Try to extract to the output directory
|
|
++normalFileCount;
|
|
header.ExtractFile(customDialogSet, outputDirectory, includeDebug);
|
|
break;
|
|
|
|
case OperationCode.GetTemporaryFilename:
|
|
if (state.Data is not GetTemporaryFilename getTemporaryFilename)
|
|
return false;
|
|
|
|
if (getTemporaryFilename.Variable is not null)
|
|
environment[getTemporaryFilename.Variable] = Guid.NewGuid().ToString();
|
|
break;
|
|
|
|
case OperationCode.AddTextToInstallLog:
|
|
if (state.Data is not AddTextToInstallLog addTextToInstallLog)
|
|
return false;
|
|
|
|
if (includeDebug) Console.WriteLine($"INSTALL.LOG: {addTextToInstallLog.Text}");
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
#pragma warning restore IDE0010
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to write INI data to the correct file
|
|
/// </summary>
|
|
/// <param name="obj">INI information</param>
|
|
/// <param name="outputDirectory">Output directory to write to</param>
|
|
/// <param name="index">File index for automatic naming</param>
|
|
private static void WriteIniData(EditIniFile obj, string outputDirectory, int index)
|
|
{
|
|
// Ensure directory separators are consistent
|
|
string iniFilePath = obj.Pathname ?? $"WISE{index:X4}.ini";
|
|
if (Path.DirectorySeparatorChar == '\\')
|
|
iniFilePath = iniFilePath.Replace('/', '\\');
|
|
else if (Path.DirectorySeparatorChar == '/')
|
|
iniFilePath = iniFilePath.Replace('\\', '/');
|
|
|
|
// Ignore path replacements
|
|
iniFilePath = iniFilePath.Replace("%", string.Empty);
|
|
|
|
// Ensure the full output directory exists
|
|
iniFilePath = Path.Combine(outputDirectory, iniFilePath);
|
|
var directoryName = Path.GetDirectoryName(iniFilePath);
|
|
if (directoryName is not null && !Directory.Exists(directoryName))
|
|
Directory.CreateDirectory(directoryName);
|
|
|
|
using var iniFile = File.Open(iniFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
|
|
iniFile.Write(Encoding.ASCII.GetBytes($"[{obj.Section}]\n"));
|
|
iniFile.Write(Encoding.ASCII.GetBytes($"{obj.Values ?? string.Empty}\n"));
|
|
iniFile.Flush();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|