JD2022-TU1/main/tools/apps/Packager/Tasks/GeneratePackageConfigTask.cs

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"
};
}
}
}