1654 lines
70 KiB
C#
1654 lines
70 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Xml.Linq;
|
|
using JD.Core.ProjectManaging;
|
|
using JD.LogService;
|
|
using NUnit.Framework;
|
|
|
|
using Packager.Parameters;
|
|
using Ubisoft.Collections;
|
|
|
|
namespace Packager.Tasks
|
|
{
|
|
|
|
[DisplayName("Generate Package Config")]
|
|
public class GeneratePackageConfigTask : Task
|
|
{
|
|
[Input("Required files", IOCardinality.Multi, IOType.File, Flags = InputFlags.Optional)]
|
|
public ReferenceList RequiredFiles { get; set; }
|
|
[Input("Optional files", IOCardinality.Multi, IOType.File, Flags = InputFlags.Optional)]
|
|
public ReferenceList OptionalFiles { get; set; }
|
|
[Input("Update files", IOCardinality.Multi, IOType.File, Flags = InputFlags.Optional)]
|
|
public ReferenceList UpdateFiles { get; set; }
|
|
[Input("Launch files", IOCardinality.Multi, IOType.File, Flags = InputFlags.Optional)]
|
|
public ReferenceList LaunchFiles { get; set; }
|
|
[Input("Template file", IOCardinality.Single, IOType.File)]
|
|
public SingleReference TemplateFile { get; set; }
|
|
[Input("Package order file", IOCardinality.Single, IOType.File)]
|
|
public SingleReference PackageOrderFile { get; set; }
|
|
|
|
[Output("Package Config", IOType.File)]
|
|
public ReferenceList PackageConfig = new ReferenceList();
|
|
|
|
[Output("Id Table", IOType.File)]
|
|
public ReferenceList IdTable = new ReferenceList();
|
|
|
|
[Output("Updated package order", IOType.File)]
|
|
public ReferenceList UpdatedPackageOrder = new ReferenceList();
|
|
|
|
|
|
private struct PackageOrderSection
|
|
{
|
|
public readonly int Begin;
|
|
public readonly int End;
|
|
public readonly bool AllowSharedIds;
|
|
public static int MinValue = 1;
|
|
public static int MaxValue = 32767;
|
|
|
|
public PackageOrderSection(int b, int e, bool shared)
|
|
{
|
|
Begin = b;
|
|
End = e;
|
|
AllowSharedIds = shared;
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, PackageOrderSection> _packageOrderSections;
|
|
|
|
private Dictionary<string, int> _fileOrderIndices;
|
|
|
|
private PackageConfigTemplate _template;
|
|
|
|
private int _jdmMapsCommonChunkId;
|
|
|
|
private enum SectionType
|
|
{
|
|
DET_UPDATE,
|
|
|
|
DET_REQUIRED,
|
|
|
|
DET_OPTIONAL
|
|
};
|
|
|
|
private readonly string[] _sectionNames = { "UpdateFiles", "RequiredFiles", "OptionalFiles" };
|
|
|
|
// read this from the package order file
|
|
private const string _nonBundledEntryName = "_NONBUNDLEDFILES_";
|
|
|
|
public GeneratePackageConfigTask()
|
|
{
|
|
RequiredFiles = new ReferenceList();
|
|
OptionalFiles = new ReferenceList();
|
|
UpdateFiles = new ReferenceList();
|
|
TemplateFile = new SingleReference();
|
|
PackageOrderFile = new SingleReference();
|
|
LaunchFiles = new ReferenceList();
|
|
|
|
_packageOrderSections = null;
|
|
_fileOrderIndices = null;
|
|
_template = null;
|
|
}
|
|
|
|
public override bool NeedCooker => true;
|
|
|
|
protected override void StartInternal(BuildConfiguration profile, cookerWrapper cooker)
|
|
{
|
|
WorkerFunc(profile, cooker, TaskOutputFolder);
|
|
}
|
|
|
|
protected void WorkerFunc(BuildConfiguration _profile, cookerWrapper _cooker, string _outputFolder)
|
|
{
|
|
try
|
|
{
|
|
var platform = _profile.PlatformList.First();
|
|
|
|
if (string.Equals(platform, "Orbis", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(platform, "PS3", StringComparison.OrdinalIgnoreCase)
|
|
)
|
|
{
|
|
// Generate both SCEA & SCEE GP4 files for Orbis and GP3 files for PS3Q
|
|
GenerateOutput(_cooker, _outputFolder, platform, "SCEA");
|
|
GenerateOutput(_cooker, _outputFolder, platform, "SCEE");
|
|
}
|
|
else
|
|
{
|
|
GenerateOutput(_cooker, _outputFolder, platform, null);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage(true, $"System exception: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void GenerateOutput(cookerWrapper cooker, string outputFolder, string platform, string sku)
|
|
{
|
|
try
|
|
{
|
|
string chunkMappingFile = DataPath.GetAbsolutePath(TemplateFile.Value);
|
|
string packageOrderFile = DataPath.GetAbsolutePath(PackageOrderFile.Value);
|
|
|
|
_template = PackageConfigTemplate.Open(chunkMappingFile, platform, sku);
|
|
|
|
if (OpenPackageOrder(packageOrderFile, platform) && _template != null)
|
|
{
|
|
_template.Platform = platform;
|
|
|
|
string destFile = Path.Combine(outputFolder, _template.OutputFilename);
|
|
|
|
using (var file = new StreamWriter(destFile, false))
|
|
{
|
|
ProcessLaunchFiles();
|
|
ProcessOptionalFiles();
|
|
ProcessRequiredFiles();
|
|
ProcessUpdatedFiles();
|
|
ProcessOptionalJdmMapFilesOnOrbis();
|
|
|
|
// order the input by ID
|
|
var orderedFilenames = new List<KeyValuePair<int, string>>();
|
|
// enumerate the entries, excluding k_NonBundledEntryName entries
|
|
var packageOrderOfSection = _fileOrderIndices.Where(id => (! string.Equals(id.Key, _nonBundledEntryName, StringComparison.OrdinalIgnoreCase)));
|
|
foreach (var existingFile in packageOrderOfSection)
|
|
{
|
|
orderedFilenames.Add(new KeyValuePair<int, string>(existingFile.Value, existingFile.Key));
|
|
}
|
|
|
|
orderedFilenames.Sort((a, b) => { return a.Key.CompareTo(b.Key); });
|
|
|
|
if (string.Equals(platform, "Durango", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var durangoTemplate = (PackageConfigTemplateDurango)_template;
|
|
durangoTemplate.LastRequiredChunk = GetLastRequiredId();
|
|
}
|
|
else if (string.Equals(platform, "Scarlett", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var scarlettTemplate = (PackageConfigTemplateScarlett)_template;
|
|
scarlettTemplate.LastRequiredChunk = GetLastRequiredId();
|
|
}
|
|
else if (string.Equals(platform, "Pc", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var pcTemplate = (PackageConfigTemplatePC)_template;
|
|
pcTemplate.LastRequiredChunk = GetLastRequiredId();
|
|
}
|
|
|
|
_template.Process(file, orderedFilenames);
|
|
|
|
PackageConfig.Add(destFile);
|
|
}
|
|
|
|
var updatedPackageOrderFilename = Path.Combine(outputFolder, "UpdatedPackageOrder.xml");
|
|
SaveUpdatedPackageOrder(packageOrderFile, updatedPackageOrderFilename, platform);
|
|
UpdatedPackageOrder.Add(updatedPackageOrderFilename);
|
|
|
|
var idTableOutFilename = Path.Combine(outputFolder, "IdTable.idt");
|
|
{
|
|
List<KeyValuePair<string, int>> sortedIds = _fileOrderIndices.ToList();
|
|
sortedIds.Sort((firstPair, nextPair) => firstPair.Value.CompareTo(nextPair.Value));
|
|
cooker.generateInstallIdTable(idTableOutFilename, sortedIds);
|
|
}
|
|
IdTable.Add(idTableOutFilename);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex is PackageConfigException)
|
|
{
|
|
HandlePackageConfigException(ex);
|
|
}
|
|
else
|
|
{
|
|
LogMessage(true, $"System exception: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandlePackageConfigException(Exception ex)
|
|
{
|
|
var packageConfigException = (PackageConfigException)ex;
|
|
|
|
if (packageConfigException.Fatal)
|
|
{
|
|
LogMessage(ex.Message, LogCategory.Error);
|
|
}
|
|
else
|
|
{
|
|
LogMessage("SKIPPING: " + ex.Message, LogCategory.Warning);
|
|
}
|
|
}
|
|
|
|
private void ProcessUpdatedFiles()
|
|
{
|
|
LoopAllFiles(UpdateFiles, SectionType.DET_UPDATE);
|
|
}
|
|
|
|
private void ProcessLaunchFiles()
|
|
{
|
|
var input = OptionalFiles.Select(Path.GetFileName).Where(IsJdLaunchFile).OrderBy(Path.GetFileNameWithoutExtension);
|
|
input.ForEach(AddToRequiredFiles);
|
|
}
|
|
|
|
private void ProcessRequiredFiles()
|
|
{
|
|
LoopAllFiles(RequiredFiles, SectionType.DET_REQUIRED);
|
|
}
|
|
|
|
private void ProcessOptionalFiles()
|
|
{
|
|
var input = OptionalFiles.Where(x => (!IsOrbisJdmMap(Path.GetFileName(x)) && !IsJdLaunchFile(Path.GetFileName(x)))).OrderBy(Path.GetFileNameWithoutExtension);
|
|
|
|
LoopAllFiles(input, SectionType.DET_OPTIONAL);
|
|
}
|
|
|
|
private void ProcessOptionalJdmMapFilesOnOrbis()
|
|
{
|
|
var sec = _packageOrderSections[_sectionNames[(int)SectionType.DET_OPTIONAL]];
|
|
var maxValidDatachunkId = sec.End;
|
|
|
|
_jdmMapsCommonChunkId = _fileOrderIndices.Values.Max() + 1;
|
|
|
|
if (_jdmMapsCommonChunkId >= maxValidDatachunkId)
|
|
{
|
|
LogMessage("The Common Chunk Id for Jdm Maps exceeds the maximum optional file limit defined in packageorder.xml file", LogCategory.Error);
|
|
return;
|
|
}
|
|
|
|
var input = OptionalFiles.Select(Path.GetFileName).Where(IsOrbisJdmMap).OrderBy(Path.GetFileNameWithoutExtension);
|
|
input.ForEach(AddJdmMapsToCommonChunk);
|
|
}
|
|
|
|
private void LoopAllFiles(IEnumerable<string> input, SectionType type)
|
|
{
|
|
foreach (var inputFile in input)
|
|
{
|
|
if (!String.IsNullOrEmpty(inputFile))
|
|
{
|
|
string inputFilename = Path.GetFileName(inputFile);
|
|
AddPackageOrderEntry(inputFilename, type);
|
|
}
|
|
}
|
|
}
|
|
|
|
#region PackageOrder
|
|
|
|
protected bool OpenPackageOrder(string _filename, string _platform)
|
|
{
|
|
try
|
|
{
|
|
using (var file = File.OpenText(_filename))
|
|
{
|
|
_packageOrderSections = new Dictionary<string, PackageOrderSection>(StringComparer.InvariantCultureIgnoreCase);
|
|
_fileOrderIndices = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
|
|
|
|
var packageOrderFile = XElement.Load(file);
|
|
|
|
IEnumerable<XElement> platformEntries =
|
|
from elem in packageOrderFile.Elements("Platform")
|
|
where (elem.Attribute("Name").Value.ToLower() == _platform.ToLower())
|
|
select elem;
|
|
|
|
if (!platformEntries.Any())
|
|
{
|
|
throw new PackageConfigException("Current platform not specified in package order", false);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
var idRangeElem = platformEntries.First().Element("IdRange");
|
|
if (idRangeElem == null)
|
|
throw new PackageConfigException($"PackageOrder for {_platform} contains no IdRange element", true);
|
|
int? minValue = (int?)idRangeElem.Attribute("Min");
|
|
int? maxValue = (int?)idRangeElem.Attribute("Max");
|
|
if (minValue == null) throw new PackageConfigException("IdRange definition in package order missing Min attribute", true);
|
|
if (maxValue == null) throw new PackageConfigException("IdRange definition in package order missing Max attribute", true);
|
|
PackageOrderSection.MinValue = minValue.GetValueOrDefault(-1);
|
|
PackageOrderSection.MaxValue = maxValue.GetValueOrDefault(-1);
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
IEnumerable<XElement> sections =
|
|
from elem in platformEntries.Elements("Section")
|
|
select elem;
|
|
|
|
foreach (var section in sections)
|
|
{
|
|
int? beginValue = (int?)section.Attribute("Begin");
|
|
int? endValue = (int?)section.Attribute("End");
|
|
bool canShare = (section.Attribute("AllowSharedIds") != null && string.Equals(section.Attribute("AllowSharedIds").Value, "true", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (beginValue == null) throw new PackageConfigException("Section definition in package order missing Begin attribute", true);
|
|
if (endValue == null) throw new PackageConfigException("Section definition in package order missing End attribute", true);
|
|
|
|
_packageOrderSections[section.Attribute("Name").Value] = new PackageOrderSection(
|
|
beginValue.GetValueOrDefault(PackageOrderSection.MinValue),
|
|
endValue.GetValueOrDefault(PackageOrderSection.MaxValue),
|
|
canShare
|
|
);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
var entries =
|
|
from elem in platformEntries.Elements("File")
|
|
select elem;
|
|
|
|
foreach (var entry in entries)
|
|
{
|
|
var entryName = (string)entry.Attribute("Name");
|
|
int? id = (int?)entry.Attribute("Id");
|
|
|
|
if (entryName == null) throw new PackageConfigException("File entry in package order missing Name attribute", true);
|
|
if (id == null) throw new PackageConfigException("File entry in package order missing Id attribute", true);
|
|
|
|
_fileOrderIndices[entryName] = id.GetValueOrDefault(-1);
|
|
}
|
|
|
|
var launchFiles = packageOrderFile.Elements("LaunchFiles").First().Elements("File");
|
|
|
|
foreach (var launchFile in launchFiles)
|
|
{
|
|
var entryName = (string)launchFile.Attribute("Name");
|
|
entryName = $"{entryName}_{_platform}.ipk";
|
|
LaunchFiles.Add(entryName.ToLower());
|
|
}
|
|
|
|
ValidatePackageOrder(_platform);
|
|
return true;
|
|
}
|
|
}
|
|
catch (DirectoryNotFoundException /*ex*/) { throw new PackageConfigException($"Package order file '{_filename}' not found", true); }
|
|
catch (FileNotFoundException /*ex*/) { throw new PackageConfigException($"Package order file '{_filename}' not found", true); }
|
|
catch (FormatException /*ex*/) { }
|
|
catch (OverflowException /*ex*/) { }
|
|
|
|
return false;
|
|
}
|
|
|
|
protected void ValidatePackageOrder(string platform)
|
|
{
|
|
if (string.Equals(platform, "Durango", StringComparison.OrdinalIgnoreCase) || string.Equals(platform, "Scarlett", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ValidatePackageOrderDurango();
|
|
}
|
|
else if (string.Equals(platform, "Orbis", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(platform, "PS3", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(platform, "Prospero", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ValidatePackageOrderOrbis();
|
|
}
|
|
}
|
|
|
|
protected void ValidatePackageOrderDurango()
|
|
{
|
|
// Rules:
|
|
// - Must contain all types of file listed in k_SectionNames
|
|
// - Sections must be in the order specified in k_SectionNames based on their range limits
|
|
// - Ranges of sections must not overlap, unless AllowSharedIds is true
|
|
// - Each file entry must lie in a range
|
|
foreach (var sectionName in _sectionNames)
|
|
{
|
|
if (!_packageOrderSections.ContainsKey(sectionName)) throw new PackageConfigException($"Package order doesn't contain section {sectionName}", true);
|
|
|
|
var sec = _packageOrderSections[sectionName];
|
|
if (sec.Begin < PackageOrderSection.MinValue) throw new PackageConfigException($"Package order section {sectionName} has an invalid range - Begin less than Min value ({PackageOrderSection.MinValue})", true);
|
|
if (sec.End > PackageOrderSection.MaxValue) throw new PackageConfigException($"Package order section {sectionName} has an invalid range - End greater than Max value ({PackageOrderSection.MaxValue})", true);
|
|
if (sec.Begin > sec.End) throw new PackageConfigException($"Package order section {sectionName} has an invalid range - Begin greater than End", true);
|
|
}
|
|
|
|
for (int i = 0; i < _sectionNames.Count() - 1; i++)
|
|
{
|
|
var currentSectionName = _sectionNames[i];
|
|
var nextSectionName = _sectionNames[i + 1];
|
|
var currentSection = _packageOrderSections[currentSectionName];
|
|
var nextSection = _packageOrderSections[nextSectionName];
|
|
|
|
if ((!currentSection.AllowSharedIds || !nextSection.AllowSharedIds) && currentSection.End >= nextSection.Begin)
|
|
throw new PackageConfigException($"Package order sections {currentSectionName} and {nextSectionName} have overlapping ranges", true);
|
|
}
|
|
|
|
foreach (var fileEntry in _fileOrderIndices)
|
|
{
|
|
if (string.Equals(fileEntry.Key, _nonBundledEntryName, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
bool contained = false;
|
|
foreach (var sectionName in _sectionNames)
|
|
{
|
|
PackageOrderSection sec = _packageOrderSections[sectionName];
|
|
if (fileEntry.Value >= sec.Begin && fileEntry.Value <= sec.End)
|
|
{
|
|
contained = true;
|
|
}
|
|
}
|
|
|
|
if (contained == false) throw new PackageConfigException($"Package order file entry {fileEntry.Key} - Id outside of all section ranges", true);
|
|
}
|
|
|
|
if (!_fileOrderIndices.ContainsKey(_nonBundledEntryName))
|
|
throw new PackageConfigException($"Package order missing required entry \"{_nonBundledEntryName}\"", true);
|
|
}
|
|
|
|
protected void ValidatePackageOrderOrbis()
|
|
{
|
|
// Rules:
|
|
// - Must contain all types of file listed in k_SectionNames
|
|
// - Sections must be in the order specified in k_SectionNames based on their range limits
|
|
// - Ranges of sections must not overlap, unless AllowSharedIds is true
|
|
// - Each file entry must lie in a range
|
|
foreach (var sectionName in _sectionNames)
|
|
{
|
|
if (!_packageOrderSections.ContainsKey(sectionName)) throw new PackageConfigException($"Package order doesn't contain section {sectionName}", true);
|
|
|
|
var sec = _packageOrderSections[sectionName];
|
|
if (sec.Begin < PackageOrderSection.MinValue) throw new PackageConfigException($"Package order section {sectionName} has an invalid range - Begin less than Min value ({PackageOrderSection.MinValue})", true);
|
|
if (sec.End > PackageOrderSection.MaxValue) throw new PackageConfigException($"Package order section {sectionName} has an invalid range - End greater than Max value ({PackageOrderSection.MaxValue})", true);
|
|
if (sec.Begin > sec.End) throw new PackageConfigException($"Package order section {sectionName} has an invalid range - Begin greater than End", true);
|
|
}
|
|
|
|
for (var i = 0; i < _sectionNames.Count() - 1; i++)
|
|
{
|
|
var currentSectionName = _sectionNames[i];
|
|
var nextSectionName = _sectionNames[i + 1];
|
|
var currentSection = _packageOrderSections[currentSectionName];
|
|
var nextSection = _packageOrderSections[nextSectionName];
|
|
|
|
if ((!currentSection.AllowSharedIds || !nextSection.AllowSharedIds) && currentSection.End >= nextSection.Begin)
|
|
throw new PackageConfigException($"Package order sections {currentSectionName} and {nextSectionName} have overlapping ranges", true);
|
|
}
|
|
|
|
foreach (var fileEntry in _fileOrderIndices)
|
|
{
|
|
if (string.Equals(fileEntry.Key, _nonBundledEntryName, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
bool contained = false;
|
|
foreach (var sectionName in _sectionNames)
|
|
{
|
|
PackageOrderSection sec = _packageOrderSections[sectionName];
|
|
if (fileEntry.Value >= sec.Begin && fileEntry.Value <= sec.End)
|
|
{
|
|
contained = true;
|
|
}
|
|
}
|
|
|
|
if (contained == false) throw new PackageConfigException($"Package order file entry {fileEntry.Key} - Id outside of all section ranges", true);
|
|
}
|
|
|
|
if (!_fileOrderIndices.ContainsKey(_nonBundledEntryName))
|
|
throw new PackageConfigException($"Package order missing required entry \"{_nonBundledEntryName}\"", true);
|
|
}
|
|
|
|
private void AddJdmMapsToCommonChunk(string fileName)
|
|
{
|
|
if (_fileOrderIndices.ContainsKey(fileName))
|
|
return;
|
|
|
|
_fileOrderIndices[fileName] = _jdmMapsCommonChunkId;
|
|
}
|
|
|
|
private void AddToRequiredFiles(string fileName)
|
|
{
|
|
RequiredFiles.Add(fileName);
|
|
}
|
|
|
|
private int AddPackageOrderEntry(string filename, SectionType type)
|
|
{
|
|
var res = -1;
|
|
var sec = _packageOrderSections[_sectionNames[(int)type]];
|
|
|
|
if (_fileOrderIndices.ContainsKey(filename)) //this should only happen when rebundling with the package order from a previous bundling.
|
|
{
|
|
res = _fileOrderIndices[filename];
|
|
}
|
|
else
|
|
{
|
|
int nextKey = sec.Begin;
|
|
int endKey = sec.End;
|
|
if (!sec.AllowSharedIds) //reuse Begin if sharing is allowed
|
|
{
|
|
while (nextKey <= endKey && _fileOrderIndices.ContainsValue(nextKey))
|
|
{
|
|
nextKey++;
|
|
}
|
|
}
|
|
|
|
if (nextKey > endKey)
|
|
{
|
|
throw new PackageConfigException($"Couldn't add package order entry for {filename}: No free entries of type {_sectionNames[(int)type]}. Aborting.", true);
|
|
}
|
|
|
|
_fileOrderIndices[filename] = nextKey;
|
|
res = nextKey;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
private bool IsOrbisJdmMap(string filename)
|
|
{
|
|
var pattern = new Regex(@"^jdm\w+_ORBIS\.ipk$", RegexOptions.IgnoreCase);
|
|
return pattern.IsMatch(filename);
|
|
}
|
|
|
|
private bool IsJdLaunchFile(string filename)
|
|
{
|
|
var file = filename.ToLower();
|
|
return LaunchFiles.Contains(file);
|
|
}
|
|
|
|
protected int GetLastRequiredId()
|
|
{
|
|
var section = _packageOrderSections[_sectionNames[(int)SectionType.DET_REQUIRED]];
|
|
var req = _fileOrderIndices.Where(a => (a.Value >= section.Begin && a.Value <= section.End));
|
|
var maxReq = -1;
|
|
foreach (var kvp in req)
|
|
{
|
|
maxReq = (kvp.Value > maxReq) ? kvp.Value : maxReq;
|
|
}
|
|
return maxReq;
|
|
}
|
|
|
|
protected void SaveUpdatedPackageOrder(string filenameIn, string filenameOut, string platform)
|
|
{
|
|
XElement packageOrderFile = null;
|
|
|
|
// open the package order and read in the whole lot.
|
|
using (var file = File.OpenText(filenameIn))
|
|
{
|
|
packageOrderFile = XElement.Load(file);
|
|
}
|
|
|
|
{
|
|
// replace the current platform with the updated package order
|
|
IEnumerable<XElement> platformEntries =
|
|
from elem in packageOrderFile.Elements("Platform")
|
|
where (elem.Attribute("Name").Value.ToLower() == platform.ToLower())
|
|
select elem;
|
|
|
|
foreach (var platformEntry in platformEntries)
|
|
{
|
|
platformEntry.Remove();
|
|
}
|
|
|
|
XElement platformElement = new XElement("Platform", new XAttribute("Name", platform));
|
|
packageOrderFile.Add(platformElement);
|
|
|
|
platformElement.Add(
|
|
new XElement("IdRange",
|
|
new XAttribute("Min", PackageOrderSection.MinValue),
|
|
new XAttribute("Max", PackageOrderSection.MaxValue)
|
|
)
|
|
);
|
|
|
|
foreach (var section in _packageOrderSections)
|
|
{
|
|
platformElement.Add(
|
|
new XElement("Section",
|
|
new XAttribute("Name", section.Key),
|
|
new XAttribute("Begin", section.Value.Begin),
|
|
new XAttribute("End", section.Value.End)
|
|
)
|
|
);
|
|
}
|
|
|
|
platformElement.Add(new XComment("The file entries are generated by the tool when unlisted files are encountered, but can be modified by hand for extra control"));
|
|
|
|
foreach (var fileEntry in _fileOrderIndices)
|
|
{
|
|
platformElement.Add(
|
|
new XElement("File",
|
|
new XAttribute("Name", fileEntry.Key),
|
|
new XAttribute("Id", fileEntry.Value)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
using (var file = new StreamWriter(filenameOut, false))
|
|
{
|
|
packageOrderFile.Save(file);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
// For debugging purposes - a text version of the IdTable.idt binary file
|
|
// #region IdTable
|
|
//
|
|
// protected void WriteIdTable(string _filename)
|
|
// {
|
|
// List<KeyValuePair<string, int>> sortedIds = m_fileOrderIndices.ToList();
|
|
// sortedIds.Sort((firstPair, nextPair) => { return firstPair.Value.CompareTo(nextPair.Value); });
|
|
//
|
|
// try
|
|
// {
|
|
// using (var file = new System.IO.StreamWriter(_filename, false))
|
|
// {
|
|
// foreach (var entry in sortedIds)
|
|
// {
|
|
// file.WriteLine(entry.Key + "\t\t\t" + Convert.ToString(entry.Value));
|
|
// }
|
|
// }
|
|
// }
|
|
// catch (System.Exception ex)
|
|
// {
|
|
// throw new System.ApplicationException("Error writing id table: " + ex.Message, ex);
|
|
// }
|
|
// }
|
|
//
|
|
// #endregion
|
|
|
|
#region Template
|
|
|
|
protected abstract class PackageConfigTemplate
|
|
{
|
|
protected XElement _platformTemplate;
|
|
protected List<KeyValuePair<int, string>> _entries;
|
|
protected Dictionary<string, string> _defaultTagValues;
|
|
protected Dictionary<string, string> _tagValues;
|
|
protected string _outputFilename;
|
|
|
|
public string Platform
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public string OutputFilename => _outputFilename;
|
|
|
|
public static PackageConfigTemplate Open(string filename, string platform, string sku)
|
|
{
|
|
try
|
|
{
|
|
using (var file = File.OpenText(filename))
|
|
{
|
|
var xmlTemplate = XElement.Load(file);
|
|
|
|
IEnumerable<XElement> platformEntries =
|
|
from elem in xmlTemplate.Elements("Platform")
|
|
where (elem.Attribute("Name").Value.ToLower() == platform.ToLower())
|
|
select elem;
|
|
|
|
if (!platformEntries.Any())
|
|
{
|
|
throw new PackageConfigException("Current platform not specified in template - skipping", false);
|
|
}
|
|
|
|
if (string.Equals(platform, "Durango", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new PackageConfigTemplateDurango(platformEntries.First());
|
|
}
|
|
if (string.Equals(platform, "Scarlett", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new PackageConfigTemplateScarlett(platformEntries.First());
|
|
}
|
|
if (string.Equals(platform, "Pc", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new PackageConfigTemplatePC(platformEntries.First());
|
|
}
|
|
|
|
//Currently PackageConfigTemplate is same for Orbis and PS3. If any change comes, PS3 will require different template
|
|
if (string.Equals(platform, "Orbis", StringComparison.OrdinalIgnoreCase) || string.Equals(platform, "PS3", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
XElement el = platformEntries.First();
|
|
foreach (XElement element in el.Elements())
|
|
{
|
|
if (element.Name == sku)
|
|
{
|
|
return new PackageConfigTemplateOrbis(element);
|
|
}
|
|
}
|
|
|
|
throw new PackageConfigException($"SKU '{sku}' not found", true);
|
|
}
|
|
|
|
if (string.Equals(platform, "Prospero", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new PackageConfigTemplateProspero(platformEntries.First());
|
|
}
|
|
}
|
|
}
|
|
catch (DirectoryNotFoundException /*ex*/) { throw new PackageConfigException($"Template file '{filename}' not found", true); }
|
|
catch (FileNotFoundException /*ex*/) { throw new PackageConfigException($"Template file '{filename}' not found", true); }
|
|
catch (FormatException /*ex*/) { }
|
|
catch (OverflowException /*ex*/) { }
|
|
|
|
return null;
|
|
}
|
|
|
|
protected PackageConfigTemplate(XElement platformTemplate)
|
|
{
|
|
this._platformTemplate = platformTemplate;
|
|
_entries = null;
|
|
_tagValues = null;
|
|
|
|
XElement outFnElem = platformTemplate.Element("OutputFilename");
|
|
if (outFnElem == null)
|
|
{
|
|
throw new PackageConfigException("No output filename specified", true);
|
|
}
|
|
_outputFilename = outFnElem.Value;
|
|
|
|
LoadDefaultTags();
|
|
Validate();
|
|
}
|
|
|
|
protected void LoadDefaultTags()
|
|
{
|
|
_defaultTagValues = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
|
|
|
|
// load default tags from the template
|
|
var defaultsElement = _platformTemplate.Element("Defaults");
|
|
if (defaultsElement != null)
|
|
{
|
|
var defaultTagElements =
|
|
from elem in defaultsElement.Elements("Default")
|
|
select elem;
|
|
foreach (var tag in defaultTagElements)
|
|
{
|
|
var nameAttr = tag.Attribute("Name");
|
|
var valueAttr = tag.Attribute("Value");
|
|
if (nameAttr == null)
|
|
{
|
|
throw new PackageConfigException("Template contains <Default> element with no 'Name' attribute", true);
|
|
}
|
|
if (valueAttr == null)
|
|
{
|
|
throw new PackageConfigException("Template contains <Default> element with no 'Value' attribute", true);
|
|
}
|
|
_defaultTagValues.Add(nameAttr.Value, valueAttr.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void Validate()
|
|
{
|
|
// Ensure all tags contained in Attrib elements are in the list returned from SupportedTags()
|
|
var supportedTags = SupportedTags();
|
|
|
|
var attribs =
|
|
from elem in _platformTemplate.Descendants("Attrib")
|
|
select elem;
|
|
foreach (var attrib in attribs)
|
|
{
|
|
var attribName = attrib.Attribute("Name");
|
|
if (attribName == null)
|
|
throw new PackageConfigException("Template contains <Attrib> element with no 'Name' attribute", true);
|
|
|
|
var attribValue = attrib.Attribute("Value");
|
|
if (attribValue == null)
|
|
throw new PackageConfigException($"Template contains <Attrib> element '{attribName.Value}' with no 'Value' attribute", true);
|
|
|
|
var parsedTags = ParseTags(attribValue.Value);
|
|
|
|
foreach (var tag in parsedTags)
|
|
{
|
|
if (!supportedTags.Contains(tag))
|
|
throw new PackageConfigException(
|
|
$"Template contains <Attrib> element '{attrib.Attribute("Name").Value}' using unsupported tag '{tag}'", true);
|
|
}
|
|
}
|
|
|
|
var elemNodes = _platformTemplate.Descendants("Elem");
|
|
foreach (var elemNode in elemNodes)
|
|
{
|
|
if (elemNode.Attribute("Name") == null)
|
|
throw new PackageConfigException("Elem node doesn't contain 'Name' attribute", true);
|
|
}
|
|
|
|
var loopChunkIdsNodes = _platformTemplate.Descendants("LoopChunkIds");
|
|
foreach (var loopNode in loopChunkIdsNodes)
|
|
{
|
|
if (loopNode.Ancestors("LoopChunkIds").Any())
|
|
throw new PackageConfigException("LoopChunkIds has an ancestor element of type LoopChunkIds", true);
|
|
|
|
if (loopNode.Ancestors("LoopFiles").Any())
|
|
throw new PackageConfigException("LoopChunkIds cannot have an element of type LoopFiles as an ancestor", true);
|
|
}
|
|
|
|
var loopFilesNodes = _platformTemplate.Descendants("LoopFiles");
|
|
foreach (var loopNode in loopFilesNodes)
|
|
{
|
|
if (loopNode.Ancestors("LoopFiles").Any())
|
|
throw new PackageConfigException("LoopFiles has an ancestor element of type LoopFiles", true);
|
|
|
|
var filterByChunkIdAttr = loopNode.Attribute("FilterByChunkId");
|
|
if (filterByChunkIdAttr != null)
|
|
{
|
|
if (string.Equals(filterByChunkIdAttr.Value, "true", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!loopNode.Ancestors("LoopChunkIds").Any())
|
|
{
|
|
throw new PackageConfigException("LoopFiles is set to FilterByChunkId but has no ancestor element of type LoopChunkIds", true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var predef = _platformTemplate.Element("Predefined");
|
|
if (predef != null)
|
|
{
|
|
var predefEntries =
|
|
from elem in predef.Elements("Entry")
|
|
select elem;
|
|
|
|
foreach (var entry in predefEntries)
|
|
{
|
|
foreach (var tagValue in entry.Attributes())
|
|
{
|
|
if (!supportedTags.Contains(tagValue.Name.LocalName))
|
|
{
|
|
throw new PackageConfigException($"Predefined Entry contains unsupported tag '{tagValue.Name.LocalName}'", true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Process(StreamWriter outputFile, List<KeyValuePair<int, string>> entries)
|
|
{
|
|
if (entries == null)
|
|
throw new PackageConfigException("No entries passed to Xml generation", true);
|
|
|
|
_entries = entries;
|
|
|
|
_tagValues = new Dictionary<string, string>();
|
|
AddGlobalTags();
|
|
XElement rootElem = ProcessRoot(_platformTemplate.Element("Elem"));
|
|
_tagValues = null;
|
|
|
|
rootElem.Save(outputFile);
|
|
}
|
|
|
|
protected XElement ProcessRoot(XElement elemNode)
|
|
{
|
|
string rootName = elemNode.Attribute("Name").Value;
|
|
XElement rootElem = new XElement(rootName);
|
|
|
|
ProcessChildren(elemNode, rootElem, true, true);
|
|
|
|
return rootElem;
|
|
}
|
|
|
|
protected void ProcessChildren(XElement templateParent, XElement outputParent, bool supportElem, bool supportAttrib)
|
|
{
|
|
IEnumerable<XElement> allElements = templateParent.Elements();
|
|
foreach (var elem in allElements)
|
|
{
|
|
if (elem.Name.LocalName == "Elem")
|
|
{
|
|
if (!supportElem)
|
|
{
|
|
throw new PackageConfigException($"'Elem' element is not supported within {templateParent.Name.LocalName} element", true);
|
|
}
|
|
ProcessElem(elem, outputParent);
|
|
}
|
|
else if (elem.Name.LocalName == "Attrib")
|
|
{
|
|
if (!supportAttrib)
|
|
{
|
|
throw new PackageConfigException($"'Attrib' element is not supported within {templateParent.Name.LocalName} element", true);
|
|
}
|
|
ProcessAttrib(elem, outputParent);
|
|
}
|
|
else if (elem.Name.LocalName == "ElemText")
|
|
{
|
|
ProcessElemText(elem, outputParent);
|
|
}
|
|
else if (elem.Name.LocalName == "LoopChunkIds")
|
|
{
|
|
ProcessLoopChunkIds(elem, outputParent);
|
|
}
|
|
else if (elem.Name.LocalName == "LoopFiles")
|
|
{
|
|
ProcessLoopFiles(elem, outputParent);
|
|
}
|
|
else
|
|
{
|
|
throw new PackageConfigException($"Element with name '{elem.Name.LocalName}' is not supported", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void ProcessElem(XElement elemNode, XElement outputParent)
|
|
{
|
|
// simply write the element
|
|
string elemName = elemNode.Attribute("Name").Value;
|
|
|
|
XElement elemToAdd = new XElement(elemName);
|
|
|
|
ProcessChildren(elemNode, elemToAdd, true, true);
|
|
outputParent.Add(elemToAdd);
|
|
}
|
|
|
|
protected virtual void ProcessAttrib(XElement attribNode, XElement outputParent)
|
|
{
|
|
// determine attribute value by parsing the tags from value, then replacing
|
|
// each tag with its value.
|
|
string attrValue = attribNode.Attribute("Value").Value;
|
|
attrValue = ReplaceTags(attrValue);
|
|
|
|
outputParent.Add(new XAttribute(attribNode.Attribute("Name").Value, attrValue));
|
|
|
|
// called simply for error checking as no Attrib element should have children, hence the [false, false] parameters
|
|
ProcessChildren(attribNode, null, false, false);
|
|
}
|
|
|
|
protected virtual void ProcessElemText(XElement _elemTextNode, XElement _outputParent)
|
|
{
|
|
string elemTextValue = _elemTextNode.Attribute("Value").Value;
|
|
elemTextValue = ReplaceTags(elemTextValue);
|
|
|
|
_outputParent.Value = elemTextValue;
|
|
|
|
// called simply for error checking as no element text should have children, hence the [false, false] parameters
|
|
ProcessChildren(_elemTextNode, null, false, false);
|
|
}
|
|
|
|
protected void ProcessLoopChunkIds(XElement _elemNode, XElement _outputParent)
|
|
{
|
|
SortedSet<int> uniqueChunkIds = new SortedSet<int>(); //automatically ignores duplicates
|
|
_entries.ForEach(entry => uniqueChunkIds.Add(entry.Key));
|
|
|
|
// add predefined chunkIds to the list
|
|
XElement predefined = _platformTemplate.Element("Predefined");
|
|
if (predefined != null)
|
|
{
|
|
foreach (var predefEntry in predefined.Elements("Entry"))
|
|
{
|
|
XAttribute chunkIdAttr = predefEntry.Attribute("ChunkId");
|
|
if (chunkIdAttr == null)
|
|
{
|
|
throw new PackageConfigException("Predefined entry contains no chunk id", true);
|
|
}
|
|
uniqueChunkIds.Add(Convert.ToInt32(chunkIdAttr.Value));
|
|
}
|
|
}
|
|
|
|
foreach (int id in uniqueChunkIds)
|
|
{
|
|
Dictionary<string, string> prevTags = _tagValues;
|
|
//create a new dictionary of tags, duplicating existing tags if they exist
|
|
_tagValues = (prevTags != null)
|
|
? new Dictionary<string, string>(prevTags, StringComparer.InvariantCultureIgnoreCase)
|
|
: new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
|
|
|
|
_tagValues["ChunkId"] = Convert.ToString(id);
|
|
|
|
ProcessChildren(_elemNode, _outputParent, true, false);
|
|
_tagValues = prevTags;
|
|
}
|
|
}
|
|
|
|
protected void ProcessLoopFiles(XElement _elemNode, XElement _outputParent)
|
|
{
|
|
int filterByChunkId = -1;
|
|
|
|
XAttribute filterByChunkIdAttr = _elemNode.Attribute("FilterByChunkId");
|
|
if (filterByChunkIdAttr != null && String.Equals(filterByChunkIdAttr.Value, "true", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (_tagValues == null)
|
|
{
|
|
throw new PackageConfigException("FilterByChunkId used when outside LoopChunkIds block", true);
|
|
}
|
|
filterByChunkId = Convert.ToInt32(_tagValues["ChunkId"]);
|
|
}
|
|
|
|
bool filterByContentLabel = false;
|
|
XAttribute filterByContentLabelAttr = _elemNode.Attribute("FilterByContentLabel");
|
|
if (filterByContentLabelAttr != null && String.Equals(filterByContentLabelAttr.Value, "true", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
filterByContentLabel = true;
|
|
}
|
|
|
|
List<KeyValuePair<int, Dictionary<string, string>>> filesToLoop = new List<KeyValuePair<int, Dictionary<string, string>>>();
|
|
|
|
// add predefined entries
|
|
XElement predefined = _platformTemplate.Element("Predefined");
|
|
if (predefined != null)
|
|
{
|
|
foreach (var predefEntry in predefined.Elements("Entry"))
|
|
{
|
|
XAttribute chunkIdAttr = predefEntry.Attribute("ChunkId");
|
|
if (chunkIdAttr == null)
|
|
{
|
|
throw new PackageConfigException("Predefined entry contains no chunk id", true);
|
|
}
|
|
|
|
int chunkId = Convert.ToInt32(chunkIdAttr.Value);
|
|
if (filterByChunkId != -1 && filterByChunkId != chunkId)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
XAttribute configLabelAttr = predefEntry.Attribute("ConfigLabel");
|
|
if ((filterByContentLabel && configLabelAttr == null) || (!filterByContentLabel && configLabelAttr != null))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Dictionary<string, string> predefEntryTags = new Dictionary<string, string>();
|
|
filesToLoop.Add(new KeyValuePair<int, Dictionary<string, string>>(chunkId, predefEntryTags));
|
|
|
|
foreach (XAttribute attr in predefEntry.Attributes())
|
|
{
|
|
predefEntryTags.Add(attr.Name.LocalName, attr.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add entries from tool
|
|
foreach (var entry in _entries)
|
|
{
|
|
int chunkId = entry.Key;
|
|
if (filterByChunkId != -1 && filterByChunkId != chunkId)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (filterByContentLabel)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//create a new dictionary of tags, duplicating existing tags if they exist
|
|
Dictionary<string, string> entryTags = (_tagValues != null)
|
|
? new Dictionary<string, string>(_tagValues)
|
|
: new Dictionary<string, string>();
|
|
|
|
if (string.Equals(Platform, "PS3", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
//On PS3, the bundle names are required to be in lowercase.
|
|
entryTags.Add("EntryFileName", entry.Value.ToLower());
|
|
}
|
|
else
|
|
{
|
|
entryTags.Add("EntryFileName", entry.Value);
|
|
}
|
|
|
|
filesToLoop.Add(new KeyValuePair<int, Dictionary<string, string>>(chunkId, entryTags));
|
|
}
|
|
|
|
//// sort by chunk id, keeping elements in original order inside each chunk - i.e. use stable OrderBy(), not unstable Sort()
|
|
filesToLoop.OrderBy((pair) => { return pair.Key; });
|
|
foreach (var loopFile in filesToLoop)
|
|
{
|
|
Dictionary<string, string> prevTags = _tagValues;
|
|
_tagValues = loopFile.Value;
|
|
_tagValues["ChunkId"] = Convert.ToString(loopFile.Key);
|
|
|
|
ProcessChildren(_elemNode, _outputParent, true, false);
|
|
_tagValues = prevTags;
|
|
}
|
|
}
|
|
|
|
protected virtual List<string> SupportedTags()
|
|
{
|
|
var retList = new List<string>();
|
|
|
|
retList.AddRange(_defaultTagValues.Keys);
|
|
|
|
retList.Add("ChunkId");
|
|
retList.Add("EntryFileName");
|
|
retList.Add("ChunkCount");
|
|
retList.Add("FSCompress");
|
|
|
|
return retList;
|
|
}
|
|
|
|
protected virtual void AddGlobalTags()
|
|
{
|
|
SortedSet<int> uniqueIds = new SortedSet<int>();
|
|
foreach (var entry in _entries)
|
|
{
|
|
uniqueIds.Add(entry.Key);
|
|
}
|
|
_tagValues.Add("ChunkCount", Convert.ToString(uniqueIds.Count));
|
|
}
|
|
|
|
protected string ReplaceTags(string taggedString)
|
|
{
|
|
string retStr = taggedString;
|
|
List<string> parsedTags = ParseTags(retStr);
|
|
foreach (string tag in parsedTags)
|
|
{
|
|
if (_tagValues != null && _tagValues.ContainsKey(tag))
|
|
{
|
|
retStr = retStr.Replace($"%{tag}%", _tagValues[tag]);
|
|
}
|
|
else if (_defaultTagValues.ContainsKey(tag))
|
|
{
|
|
retStr = retStr.Replace($"%{tag}%", _defaultTagValues[tag]);
|
|
}
|
|
}
|
|
return retStr;
|
|
}
|
|
|
|
protected List<string> ParseTags(string _val)
|
|
{
|
|
List<string> retList = new List<string>();
|
|
string remaining = _val;
|
|
while (remaining.IndexOf('%') != remaining.LastIndexOf('%')) //breaks when either 0 or 1 % in string
|
|
{
|
|
int first = remaining.IndexOf('%');
|
|
int second = remaining.IndexOf('%', first + 1);
|
|
|
|
first++;
|
|
if ((second - 1) < first)
|
|
{
|
|
// seem to have found consecutive %s, skip the first
|
|
remaining = remaining.Substring(first);
|
|
continue;
|
|
}
|
|
|
|
string tag = remaining.Substring(first, second - first);
|
|
if (tag.Split().First() != tag)
|
|
{
|
|
//contains whitespace so continue from second
|
|
remaining = remaining.Substring(second);
|
|
continue;
|
|
}
|
|
retList.Add(tag);
|
|
|
|
int next = second + 1;
|
|
if (next >= remaining.Length)
|
|
break;
|
|
remaining = remaining.Substring(next);
|
|
}
|
|
|
|
return retList;
|
|
}
|
|
}
|
|
|
|
protected class PackageConfigTemplateDurango : PackageConfigTemplate
|
|
{
|
|
private int m_lastRequiredChunk;
|
|
public int LastRequiredChunk { set => m_lastRequiredChunk = value; }
|
|
|
|
public PackageConfigTemplateDurango(XElement _platformTemplate)
|
|
: base(_platformTemplate)
|
|
{
|
|
m_lastRequiredChunk = -1;
|
|
}
|
|
|
|
protected override List<string> SupportedTags()
|
|
{
|
|
List<string> retList = base.SupportedTags();
|
|
|
|
// insert any Durango specific tags here...
|
|
|
|
return retList;
|
|
}
|
|
|
|
protected override void ProcessAttrib(XElement attribNode, XElement outputParent)
|
|
{
|
|
var nameAttr = attribNode.Attribute("Name");
|
|
if (string.Equals(nameAttr.Value, "Marker"))
|
|
{
|
|
var condAttr = attribNode.Attribute("Condition");
|
|
if (string.Equals(condAttr.Value, "LastRequired", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// add the marker tag to the tag dictionary
|
|
if (_tagValues == null)
|
|
{
|
|
throw new PackageConfigException("Marker attribute used outside of LoopChunkIds", true);
|
|
}
|
|
|
|
var currentChunkId = Convert.ToInt32(_tagValues["ChunkId"]);
|
|
if (currentChunkId != m_lastRequiredChunk || m_lastRequiredChunk == -1)
|
|
{
|
|
return; //don't process this Attrib
|
|
}
|
|
}
|
|
else if (string.Equals(condAttr.Value, "Last", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// add the marker tag to the tag dictionary
|
|
if (_tagValues == null)
|
|
{
|
|
throw new PackageConfigException("Marker attribute used outside of LoopChunkIds", true);
|
|
}
|
|
|
|
var currentChunkId = Convert.ToInt32(_tagValues["ChunkId"]);
|
|
if (currentChunkId != 1073741823) // Alignment chunk is always last, and this ID
|
|
{
|
|
return; //don't process this Attrib
|
|
}
|
|
}
|
|
}
|
|
|
|
base.ProcessAttrib(attribNode, outputParent);
|
|
}
|
|
}
|
|
|
|
protected class PackageConfigTemplateScarlett : PackageConfigTemplate
|
|
{
|
|
private int m_lastRequiredChunk;
|
|
public int LastRequiredChunk { set => m_lastRequiredChunk = value; }
|
|
|
|
public PackageConfigTemplateScarlett(XElement platformTemplate)
|
|
: base(platformTemplate)
|
|
{
|
|
m_lastRequiredChunk = -1;
|
|
}
|
|
|
|
protected override List<string> SupportedTags()
|
|
{
|
|
List<string> retList = base.SupportedTags();
|
|
|
|
// insert any Scarlett specific tags here...
|
|
|
|
return retList;
|
|
}
|
|
|
|
protected override void ProcessAttrib(XElement attribNode, XElement outputParent)
|
|
{
|
|
var nameAttr = attribNode.Attribute("Name");
|
|
if (string.Equals(nameAttr.Value, "Marker"))
|
|
{
|
|
var condAttr = attribNode.Attribute("Condition");
|
|
if (string.Equals(condAttr.Value, "LastRequired", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// add the marker tag to the tag dictionary
|
|
if (_tagValues == null)
|
|
{
|
|
throw new PackageConfigException("Marker attribute used outside of LoopChunkIds", true);
|
|
}
|
|
|
|
var currentChunkId = Convert.ToInt32(_tagValues["ChunkId"]);
|
|
if (currentChunkId != m_lastRequiredChunk || m_lastRequiredChunk == -1)
|
|
{
|
|
return; //don't process this Attrib
|
|
}
|
|
}
|
|
else if (string.Equals(condAttr.Value, "Last", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// add the marker tag to the tag dictionary
|
|
if (_tagValues == null)
|
|
{
|
|
throw new PackageConfigException("Marker attribute used outside of LoopChunkIds", true);
|
|
}
|
|
|
|
var currentChunkId = Convert.ToInt32(_tagValues["ChunkId"]);
|
|
if (currentChunkId != 1073741823) // Alignment chunk is always last, and this ID
|
|
{
|
|
return; //don't process this Attrib
|
|
}
|
|
}
|
|
}
|
|
|
|
base.ProcessAttrib(attribNode, outputParent);
|
|
}
|
|
}
|
|
|
|
protected class PackageConfigTemplatePC : PackageConfigTemplate
|
|
{
|
|
private int _lastRequiredChunk;
|
|
public int LastRequiredChunk { set { _lastRequiredChunk = value; } }
|
|
|
|
public PackageConfigTemplatePC(XElement platformTemplate)
|
|
: base(platformTemplate)
|
|
{
|
|
_lastRequiredChunk = -1;
|
|
}
|
|
|
|
protected override void ProcessAttrib(XElement attribNode, XElement outputParent)
|
|
{
|
|
var nameAttr = attribNode.Attribute("Name");
|
|
if (string.Equals(nameAttr.Value, "required"))
|
|
{
|
|
var condAttr = attribNode.Attribute("Condition");
|
|
if (condAttr != null && string.Equals(condAttr.Value, "Required", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (_tagValues == null)
|
|
{
|
|
throw new PackageConfigException("Marker attribute used outside of LoopChunkIds", true);
|
|
}
|
|
|
|
var currentChunkId = Convert.ToInt32(_tagValues["ChunkId"]);
|
|
if (currentChunkId <= _lastRequiredChunk || _lastRequiredChunk == -1)
|
|
{
|
|
attribNode.SetAttributeValue("Value", "true");
|
|
}
|
|
else
|
|
{
|
|
attribNode.SetAttributeValue("Value", "false");
|
|
}
|
|
}
|
|
}
|
|
|
|
base.ProcessAttrib(attribNode, outputParent);
|
|
}
|
|
}
|
|
|
|
protected class PackageConfigTemplateOrbis : PackageConfigTemplate
|
|
{
|
|
public PackageConfigTemplateOrbis(XElement platformTemplate)
|
|
: base(platformTemplate)
|
|
{
|
|
}
|
|
|
|
protected override List<string> SupportedTags()
|
|
{
|
|
List<string> retList = base.SupportedTags();
|
|
|
|
// insert any Orbis specific tags here...
|
|
retList.Add("ScenarioAllChunks");
|
|
|
|
return retList;
|
|
}
|
|
|
|
protected override void AddGlobalTags()
|
|
{
|
|
base.AddGlobalTags();
|
|
|
|
// Generate the scenario chunk list. Format example: 0-3 5 8-10
|
|
var uniqueIds = new SortedSet<int>();
|
|
foreach (var entry in _entries)
|
|
{
|
|
uniqueIds.Add(entry.Key);
|
|
}
|
|
|
|
var scenarioAllChunks = "";
|
|
var startId = uniqueIds.First();
|
|
var lastId = startId;
|
|
foreach (int entryId in uniqueIds)
|
|
{
|
|
if (entryId > (lastId + 1)) // test for non-consecutive Ids
|
|
{
|
|
if (lastId == startId)
|
|
scenarioAllChunks += $"{lastId} ";
|
|
else
|
|
scenarioAllChunks += $"{startId}-{lastId} ";
|
|
|
|
startId = entryId;
|
|
lastId = entryId;
|
|
}
|
|
else
|
|
{
|
|
lastId = entryId;
|
|
}
|
|
}
|
|
// add the last ids
|
|
if (lastId == startId)
|
|
scenarioAllChunks += $"{lastId} ";
|
|
else
|
|
scenarioAllChunks += $"{startId}-{lastId} ";
|
|
|
|
scenarioAllChunks = scenarioAllChunks.TrimEnd(' ');
|
|
_tagValues.Add("ScenarioAllChunks", scenarioAllChunks);
|
|
}
|
|
}
|
|
|
|
protected class PackageConfigTemplateProspero : PackageConfigTemplate
|
|
{
|
|
public PackageConfigTemplateProspero(XElement platformTemplate)
|
|
: base(platformTemplate)
|
|
{
|
|
}
|
|
|
|
protected override List<string> SupportedTags()
|
|
{
|
|
List<string> retList = base.SupportedTags();
|
|
|
|
// insert any Prospero specific tags here...
|
|
retList.Add("ScenarioAllChunks");
|
|
retList.Add("ConfigLabel");
|
|
|
|
return retList;
|
|
}
|
|
|
|
protected override void AddGlobalTags()
|
|
{
|
|
base.AddGlobalTags();
|
|
|
|
// Generate the scenario chunk list. Format example: 0-3 5 8-10
|
|
var uniqueIds = new SortedSet<int>();
|
|
foreach (var entry in _entries)
|
|
{
|
|
uniqueIds.Add(entry.Key);
|
|
}
|
|
|
|
var scenarioAllChunks = "";
|
|
var startId = uniqueIds.First();
|
|
var lastId = startId;
|
|
foreach (int entryId in uniqueIds)
|
|
{
|
|
if (entryId > (lastId + 1)) // test for non-consecutive Ids
|
|
{
|
|
if (lastId == startId)
|
|
scenarioAllChunks += $"{lastId} ";
|
|
else
|
|
scenarioAllChunks += $"{startId}-{lastId} ";
|
|
|
|
startId = entryId;
|
|
lastId = entryId;
|
|
}
|
|
else
|
|
{
|
|
lastId = entryId;
|
|
}
|
|
}
|
|
// add the last ids
|
|
if (lastId == startId)
|
|
scenarioAllChunks += $"{lastId} ";
|
|
else
|
|
scenarioAllChunks += $"{startId}-{lastId} ";
|
|
|
|
scenarioAllChunks = scenarioAllChunks.TrimEnd(' ');
|
|
_tagValues.Add("ScenarioAllChunks", scenarioAllChunks);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nested
|
|
|
|
private class PackageConfigException : ApplicationException
|
|
{
|
|
protected bool fatal;
|
|
public bool Fatal { get; private set; }
|
|
public PackageConfigException(bool _fatal) : base() { fatal = _fatal; }
|
|
public PackageConfigException(string _message, bool _fatal) : base(_message) { fatal = _fatal; }
|
|
public PackageConfigException(string _message, Exception _innerEx, bool _fatal) : base(_message, _innerEx) { fatal = _fatal; }
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
[TestFixture]
|
|
public class GeneratePackageConfigTaskTest
|
|
{
|
|
private readonly GeneratePackageConfigTask _generatePackageConfig = new GeneratePackageConfigTask();
|
|
private readonly GeneratePackageConfigTask _generatePackageConfigPC = new GeneratePackageConfigTask();
|
|
private readonly TestBuildConfiguration _testProfile;
|
|
private readonly TestBuildConfiguration _testProfilePC;
|
|
|
|
public GeneratePackageConfigTaskTest()
|
|
{
|
|
_generatePackageConfig.OptionalFiles.Add(CreateBundleFiles());
|
|
_generatePackageConfig.PackageOrderFile.Value = DataPath.GetAbsolutePath(@"EngineData\GameToolPackageTask\packageorder.xml");
|
|
_generatePackageConfig.TemplateFile.Value = DataPath.GetAbsolutePath(@"EngineData\GameToolPackageTask\chunkmappingtemplate.xml");
|
|
_testProfile = new TestBuildConfiguration(new[] { @"dataversion=0;platform=ORBIS;" });
|
|
|
|
//Test case for Uplay PC manifest.xml
|
|
_generatePackageConfigPC.RequiredFiles.Add(CreateRequiredBundleFilesPC());
|
|
_generatePackageConfigPC.OptionalFiles.Add(CreateOptionalBundleFilesPC());
|
|
_generatePackageConfigPC.PackageOrderFile.Value = DataPath.GetAbsolutePath(@"EngineData\GameToolPackageTask\packageorder.xml");
|
|
_generatePackageConfigPC.TemplateFile.Value = DataPath.GetAbsolutePath(@"EngineData\GameToolPackageTask\chunkmappingtemplate.xml");
|
|
_testProfilePC = new TestBuildConfiguration(new[] { @"dataversion=0;platform=Pc;" });
|
|
}
|
|
|
|
[Test]
|
|
public void TestPcManifestGeneration()
|
|
{
|
|
if (!Directory.Exists(_generatePackageConfigPC.TaskOutputFolder))
|
|
Directory.CreateDirectory(_generatePackageConfigPC.TaskOutputFolder);
|
|
|
|
_generatePackageConfigPC.StartTaskTest(_testProfilePC);
|
|
Assert.IsTrue(Directory.Exists(_generatePackageConfigPC.TaskOutputFolder + "mainfest.xml"));
|
|
}
|
|
|
|
[Test]
|
|
public void TestPuttingAllJdmMachineMapsInCommonChunk()
|
|
{
|
|
if (!Directory.Exists(_generatePackageConfig.TaskOutputFolder))
|
|
Directory.CreateDirectory(_generatePackageConfig.TaskOutputFolder);
|
|
|
|
File.Create(Path.Combine(_generatePackageConfig.TaskOutputFolder, @"ChunkMapping_SCEA.gp4"));
|
|
|
|
_generatePackageConfig.StartTaskTest(_testProfile);
|
|
|
|
var outputFileData = File.ReadAllLines(Path.Combine(_generatePackageConfig.TaskOutputFolder, "UpdatedPackageOrder.xml"));
|
|
var regularMapPattern = new Regex("<File Name=\\\"(?<name>[^jdm]\\w+_ORBIS\\.ipk\\\")\\s+Id=\\\"(?<Id>\\d+)\\\"", RegexOptions.IgnoreCase);
|
|
int maxRegularMapChunkId = 0;
|
|
outputFileData.ForEach(x =>
|
|
{
|
|
var match = regularMapPattern.Match(x);
|
|
if (match.Success)
|
|
maxRegularMapChunkId = int.Parse(match.Groups["Id"].Value);
|
|
});
|
|
|
|
var jdmMapPattern = new Regex("<File Name=\\\"(?<name>jdm\\w+_ORBIS\\.ipk)\\s+Id=(?<ID>\\\"\\d+\\\")", RegexOptions.IgnoreCase);
|
|
outputFileData.ForEach(x =>
|
|
{
|
|
var match = jdmMapPattern.Match(x);
|
|
if (match.Success)
|
|
Assert.AreEqual(int.Parse(match.Groups["ID"].Value), maxRegularMapChunkId + 1);
|
|
});
|
|
}
|
|
|
|
private string[] CreateRequiredBundleFilesPC()
|
|
{
|
|
return new[] {
|
|
"bundle_pc.ipk",
|
|
"bundlelogic_pc.ipk",
|
|
"secure_fat.gf"
|
|
};
|
|
}
|
|
|
|
private string[] CreateOptionalBundleFilesPC()
|
|
{
|
|
return new[] {
|
|
"allaboutus_pc.ipk",
|
|
"bailar_pc.ipk",
|
|
"bang_pc.ipk",
|
|
"bonbon_pc.ipk",
|
|
"cakebytheocean_pc.ipk",
|
|
"cakebytheoceanalt_pc.ipk",
|
|
"cantfeelmyface_pc.ipk"
|
|
};
|
|
}
|
|
|
|
private string[] CreateBundleFiles()
|
|
{
|
|
return new[]
|
|
{
|
|
"_testmapshort_ORBIS.ipk",
|
|
"_testmapshort2_ORBIS.ipk",
|
|
"8bitclassical_ORBIS.ipk",
|
|
"bang_ORBIS.ipk",
|
|
"cantfeelmyface_ORBIS.ipk",
|
|
"cantfeelmyfacemu_ORBIS.ipk",
|
|
"cheapthrills_ORBIS.ipk",
|
|
"circusmu_ORBIS.ipk",
|
|
"colasong_ORBIS.ipk",
|
|
"daddy_ORBIS.ipk",
|
|
"daddyalt_ORBIS.ipk",
|
|
"daddymu_ORBIS.ipk",
|
|
"dontstopme_ORBIS.ipk",
|
|
"dontstopmealt_ORBIS.ipk",
|
|
"dontstopmemu_ORBIS.ipk",
|
|
"dragosteadintei_ORBIS.ipk",
|
|
"eltiki_ORBIS.ipk",
|
|
"ghostinthekeys_ORBIS.ipk",
|
|
"ghostinthekeysmu_ORBIS.ipk",
|
|
"groove_ORBIS.ipk",
|
|
"hips_ORBIS.ipk",
|
|
"hipsalt_ORBIS.ipk",
|
|
"hipsmu_ORBIS.ipk",
|
|
"itsybitsy_ORBIS.ipk",
|
|
"jdmafricandance_ORBIS.ipk",
|
|
"jdmapplauseconcert_ORBIS.ipk",
|
|
"jdmballerina_ORBIS.ipk",
|
|
"jdmballerinaalt01_ORBIS.ipk",
|
|
"jdmbollywood_ORBIS.ipk",
|
|
"jdmbollywoodalt01_ORBIS.ipk",
|
|
"jdmbrucelee_ORBIS.ipk",
|
|
"jdmbsballerina_ORBIS.ipk",
|
|
"jdmbscharleston_ORBIS.ipk",
|
|
"jdmbsegyptian_ORBIS.ipk",
|
|
"jdmbslimbo_ORBIS.ipk",
|
|
"jdmbsrunningman_ORBIS.ipk",
|
|
"jdmbstwerk_ORBIS.ipk",
|
|
"jdmcharleston_ORBIS.ipk",
|
|
"jdmcharlestonalt01_ORBIS.ipk",
|
|
"jdmcheerleader_ORBIS.ipk",
|
|
"jdmcircuslion_ORBIS.ipk",
|
|
"jdmegyptian_ORBIS.ipk",
|
|
"jdmflamenco_ORBIS.ipk",
|
|
"jdmflamencoalt01_ORBIS.ipk",
|
|
"jdmfrenchcancan_ORBIS.ipk",
|
|
"jdmgi_ORBIS.ipk",
|
|
"jdmgialt01_ORBIS.ipk",
|
|
"jdmgymnast_ORBIS.ipk",
|
|
"jdmgymnastalt01_ORBIS.ipk",
|
|
"jdmhaka_ORBIS.ipk",
|
|
"jdmisidora_ORBIS.ipk",
|
|
"jdmkungfu_ORBIS.ipk",
|
|
"jdmkungfualt01_ORBIS.ipk",
|
|
"jdmlimbo_ORBIS.ipk",
|
|
"jdmmaestro_ORBIS.ipk",
|
|
"jdmmagicmike_ORBIS.ipk",
|
|
"jdmmetaldrum_ORBIS.ipk",
|
|
"jdmmetalguitar_ORBIS.ipk",
|
|
"jdmmetalheadbang_ORBIS.ipk",
|
|
"jdmnaenae_ORBIS.ipk",
|
|
"jdmorientaldance_ORBIS.ipk",
|
|
"jdmrapper_ORBIS.ipk",
|
|
"jdmraveparty_ORBIS.ipk",
|
|
"jdmrobot_ORBIS.ipk",
|
|
"jdmrunningman_ORBIS.ipk",
|
|
"jdmsaloon_ORBIS.ipk",
|
|
"jdmsirtaki_ORBIS.ipk",
|
|
"jdmstripteaseburlesque_ORBIS.ipk",
|
|
"jdmtectonic_ORBIS.ipk",
|
|
"jdmtwerk_ORBIS.ipk",
|
|
"jdmunderwater_ORBIS.ipk",
|
|
"jdmwestcoast_ORBIS.ipk",
|
|
"jdmyodel_ORBIS.ipk",
|
|
"jdmyoga_ORBIS.ipk",
|
|
"leanon_ORBIS.ipk",
|
|
"leanonalt_ORBIS.ipk",
|
|
"leanonmu_ORBIS.ipk",
|
|
"leila_ORBIS.ipk",
|
|
"oishii_ORBIS.ipk",
|
|
"popipo_ORBIS.ipk",
|
|
"radical_ORBIS.ipk",
|
|
"radicalalt_ORBIS.ipk",
|
|
"radicalmu_ORBIS.ipk",
|
|
"runthenight_ORBIS.ipk",
|
|
"samba_ORBIS.ipk",
|
|
"screamnshout_ORBIS.ipk",
|
|
"screamnshoutalt_ORBIS.ipk",
|
|
"screamnshoutmu_ORBIS.ipk",
|
|
"september_ORBIS.ipk",
|
|
"septembermu_ORBIS.ipk",
|
|
"sorry_ORBIS.ipk",
|
|
"sorryalt_ORBIS.ipk",
|
|
"sorrymu_ORBIS.ipk",
|
|
"ticotico_ORBIS.ipk",
|
|
"ticoticomu_ORBIS.ipk",
|
|
"titanium_ORBIS.ipk",
|
|
"titaniumalt_ORBIS.ipk",
|
|
"whatislove_ORBIS.ipk",
|
|
"whatislovealt_ORBIS.ipk",
|
|
"whatislovemu_ORBIS.ipk",
|
|
"worthit_ORBIS.ipk",
|
|
"worthitalt_ORBIS.ipk",
|
|
"worthitmu_ORBIS.ipk"
|
|
};
|
|
}
|
|
}
|
|
}
|