Skip to content

Commit

Permalink
Restructure patch building
Browse files Browse the repository at this point in the history
Lots of changes
* Parsing/validating patch is now separate code
* Less code in the patch extractor (may even be able to go away entirely with some simplifications)
* Pass specifier is now an explicit concept
* Needs checker is now an object and has a cleaner interface
* Some things which were errors before are now just warnings
  * If there is more than one pass specifier it will take the first one and warn
* Syntax for root patch names is much more formal now, this might break some unusual cases that are silently accepted now
  • Loading branch information
blowfishpro committed Jul 9, 2018
1 parent ee3e340 commit f5127b1
Show file tree
Hide file tree
Showing 43 changed files with 3,343 additions and 1,838 deletions.
40 changes: 17 additions & 23 deletions ModuleManager/MMPatchLoader.cs
Expand Up @@ -15,7 +15,9 @@
using ModuleManager.Logging;
using ModuleManager.Extensions;
using ModuleManager.Collections;
using ModuleManager.Tags;
using ModuleManager.Threading;
using ModuleManager.Patches;
using ModuleManager.Progress;
using NodeStack = ModuleManager.Collections.ImmutableStack<ConfigNode>;

Expand Down Expand Up @@ -156,35 +158,24 @@ private IEnumerator ProcessPatch()

LoadPhysicsConfig();

#region Check Needs



// Do filtering with NEEDS
status = "Checking NEEDS.";
logger.Info(status);
yield return null;
NeedsChecker.CheckNeeds(GameDatabase.Instance.root, mods, progress, logger);

#endregion Check Needs

#region Sorting Patches

status = "Sorting patches";
status = "Extracting patches";
logger.Info(status);

yield return null;

// PatchList patchList = PatchExtractor.SortAndExtractPatches(GameDatabase.Instance.root, mods, progress);

PatchList patchList = new PatchList(mods);
PatchExtractor extractor = new PatchExtractor(patchList, progress, logger);
UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == "");
INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, logger);
ITagListParser tagListParser = new TagListParser();
IProtoPatchBuilder protoPatchBuilder = new ProtoPatchBuilder(progress);
IPatchCompiler patchCompiler = new PatchCompiler();
PatchExtractor extractor = new PatchExtractor(progress, logger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler);

// Have to convert to an array because we will be removing patches
foreach (UrlDir.UrlConfig urlConfig in GameDatabase.Instance.root.AllConfigs.ToArray())
{
extractor.ExtractPatch(urlConfig);
}
UrlDir.UrlConfig[] allConfigs = GameDatabase.Instance.root.AllConfigs.ToArray();
IEnumerable<IPatch> extractedPatches = allConfigs.Select(urlConfig => extractor.ExtractPatch(urlConfig));
PatchList patchList = new PatchList(mods, extractedPatches.Where(patch => patch != null), progress);

#endregion

Expand All @@ -198,11 +189,14 @@ private IEnumerator ProcessPatch()
MessageQueue<ILogMessage> logQueue = new MessageQueue<ILogMessage>();
IBasicLogger patchLogger = new QueueLogger(logQueue);
IPatchProgress threadPatchProgress = new PatchProgress(progress, patchLogger);
PatchApplier applier = new PatchApplier(patchList, GameDatabase.Instance.root, threadPatchProgress, patchLogger);
PatchApplier applier = new PatchApplier(threadPatchProgress, patchLogger);

logger.Info("Starting patch thread");

ITaskStatus patchThread = BackgroundTask.Start(applier.ApplyPatches);
ITaskStatus patchThread = BackgroundTask.Start(delegate
{
applier.ApplyPatches(GameDatabase.Instance.root.AllConfigFiles.ToArray(), patchList);
});

float nextYield = Time.realtimeSinceStartup + yieldInterval;

Expand Down
10 changes: 10 additions & 0 deletions ModuleManager/ModuleManager.csproj
Expand Up @@ -63,11 +63,21 @@
<Compile Include="Pass.cs" />
<Compile Include="PatchApplier.cs" />
<Compile Include="PatchContext.cs" />
<Compile Include="Patches\PassSpecifiers\AfterPassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\BeforePassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\FinalPassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\FirstPassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\ForPassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\InsertPassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\IPassSpecifier.cs" />
<Compile Include="Patches\PassSpecifiers\LegacyPassSpecifier.cs" />
<Compile Include="Patches\CopyPatch.cs" />
<Compile Include="Patches\DeletePatch.cs" />
<Compile Include="Patches\EditPatch.cs" />
<Compile Include="Patches\IPatch.cs" />
<Compile Include="Patches\PatchCompiler.cs" />
<Compile Include="Patches\ProtoPatch.cs" />
<Compile Include="Patches\ProtoPatchBuilder.cs" />
<Compile Include="PatchExtractor.cs" />
<Compile Include="PatchList.cs" />
<Compile Include="Progress\ProgressCounter.cs" />
Expand Down
227 changes: 101 additions & 126 deletions ModuleManager/NeedsChecker.cs
Expand Up @@ -8,105 +8,140 @@

namespace ModuleManager
{
public static class NeedsChecker
public interface INeedsChecker
{
public static void CheckNeeds(UrlDir gameDatabaseRoot, IEnumerable<string> mods, IPatchProgress progress, IBasicLogger logger)
bool CheckNeeds(string mod);
bool CheckNeedsExpression(string needsString);
void CheckNeedsRecursive(ConfigNode node, UrlDir.UrlConfig urlConfig);
}

public class NeedsChecker : INeedsChecker
{
private readonly IEnumerable<string> mods;
private readonly UrlDir gameData;
private readonly IPatchProgress progress;
private readonly IBasicLogger logger;

public NeedsChecker(IEnumerable<string> mods, UrlDir gameData, IPatchProgress progress, IBasicLogger logger)
{
UrlDir gameData = gameDatabaseRoot.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == "");
this.mods = mods ?? throw new ArgumentNullException(nameof(mods));
this.gameData = gameData ?? throw new ArgumentNullException(nameof(gameData));
this.progress = progress ?? throw new ArgumentNullException(nameof(progress));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

foreach (UrlDir.UrlConfig mod in gameDatabaseRoot.AllConfigs.ToArray())
public bool CheckNeeds(string mod)
{
if (mod == null) throw new ArgumentNullException(nameof(mod));
if (mod == string.Empty) throw new ArgumentException("can't be empty", nameof(mod));
return mods.Contains(mod, StringComparer.InvariantCultureIgnoreCase);
}

public bool CheckNeedsExpression(string needsExpression)
{
if (needsExpression == null) throw new ArgumentNullException(nameof(needsExpression));
if (needsExpression == string.Empty) throw new ArgumentException("can't be empty", nameof(needsExpression));

foreach (string andDependencies in needsExpression.Split(',', '&'))
{
UrlDir.UrlConfig currentMod = mod;
try
bool orMatch = false;
foreach (string orDependency in andDependencies.Split('|'))
{
if (mod.config.name == null)
{
progress.Error(currentMod, "Error - Node in file " + currentMod.parent.url + " subnode: " + currentMod.type +
" has config.name == null");
}
if (orDependency.Length == 0)
continue;

UrlDir.UrlConfig newMod;
bool not = orDependency[0] == '!';
string toFind = not ? orDependency.Substring(1) : orDependency;

if (currentMod.type.IndexOf(":NEEDS[", StringComparison.OrdinalIgnoreCase) >= 0)
{
string type = currentMod.type;

if (CheckNeeds(ref type, mods, gameData))
{

ConfigNode copy = new ConfigNode(type);
copy.ShallowCopyFrom(currentMod.config);
int index = mod.parent.configs.IndexOf(currentMod);
newMod = new UrlDir.UrlConfig(currentMod.parent, copy);
mod.parent.configs[index] = newMod;
}
else
{
progress.NeedsUnsatisfiedRoot(currentMod);
mod.parent.configs.Remove(currentMod);
continue;
}
}
else
{
newMod = currentMod;
}
bool found = CheckNeedsWithDirectories(toFind);

// Recursively check the contents
PatchContext context = new PatchContext(newMod, gameDatabaseRoot, logger, progress);
CheckNeeds(new NodeStack(newMod.config), context, mods, gameData);
}
catch (Exception ex)
{
try
{
mod.parent.configs.Remove(currentMod);
}
catch(Exception ex2)
if (not == !found)
{
logger.Exception("Exception while attempting to ensure config removed" ,ex2);
orMatch = true;
break;
}
}
if (!orMatch)
return false;
}

try
{
progress.Exception(mod, "Exception while checking needs on root node :\n" + mod.PrettyPrint(), ex);
}
catch (Exception ex2)
return true;
}

public void CheckNeedsRecursive(ConfigNode node, UrlDir.UrlConfig urlConfig)
{
if (node == null) throw new ArgumentNullException(nameof(node));
if (urlConfig == null) throw new ArgumentNullException(nameof(urlConfig));
CheckNeedsRecursive(new NodeStack(node), urlConfig);
}

private bool CheckNeedsWithDirectories(string mod)
{
if (CheckNeeds(mod)) return true;
if (mod.Contains('/'))
{
string[] splits = mod.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

bool result = true;
UrlDir current = gameData;
for (int i = 0; i < splits.Length; i++)
{
current = current.children.FirstOrDefault(dir => dir.name == splits[i]);
if (current == null)
{
progress.Exception("Exception while attempting to log an exception", ex2);
result = false;
break;
}
}
return result;
}
return false;
}

private bool CheckNeedsName(ref string name)
{
if (name == null)
return true;

int idxStart = name.IndexOf(":NEEDS[", StringComparison.OrdinalIgnoreCase);
if (idxStart < 0)
return true;
int idxEnd = name.IndexOf(']', idxStart + 7);
string needsString = name.Substring(idxStart + 7, idxEnd - idxStart - 7);

name = name.Substring(0, idxStart) + name.Substring(idxEnd + 1);

return CheckNeedsExpression(needsString);
}

private static void CheckNeeds(NodeStack stack, PatchContext context, IEnumerable<string> mods, UrlDir gameData)
private void CheckNeedsRecursive(NodeStack nodeStack, UrlDir.UrlConfig urlConfig)
{
ConfigNode original = stack.value;
ConfigNode original = nodeStack.value;
for (int i = 0; i < original.values.Count; ++i)
{
ConfigNode.Value val = original.values[i];
string valname = val.name;
try
{
if (CheckNeeds(ref valname, mods, gameData))
if (CheckNeedsName(ref valname))
{
val.name = valname;
}
else
{
original.values.Remove(val);
i--;
context.progress.NeedsUnsatisfiedValue(context.patchUrl, stack, val.name);
progress.NeedsUnsatisfiedValue(urlConfig, nodeStack.GetPath() + '/' + val.name);
}
}
catch (ArgumentOutOfRangeException e)
{
context.progress.Exception("ArgumentOutOfRangeException in CheckNeeds for value \"" + val.name + "\"", e);
progress.Exception("ArgumentOutOfRangeException in CheckNeeds for value \"" + val.name + "\"", e);
throw;
}
catch (Exception e)
{
context.progress.Exception("General Exception in CheckNeeds for value \"" + val.name + "\"", e);
progress.Exception("General Exception in CheckNeeds for value \"" + val.name + "\"", e);
throw;
}
}
Expand All @@ -118,94 +153,34 @@ private static void CheckNeeds(NodeStack stack, PatchContext context, IEnumerabl

if (nodeName == null)
{
context.progress.Error(context.patchUrl, "Error - Node in file " + context.patchUrl.SafeUrl() + " subnode: " + stack.GetPath() +
" has config.name == null");
progress.Error(urlConfig, "Error - Node in file " + urlConfig.SafeUrl() + " subnode: " + nodeStack.GetPath() + " has config.name == null");
}

try
{
if (CheckNeeds(ref nodeName, mods, gameData))
if (CheckNeedsName(ref nodeName))
{
node.name = nodeName;
CheckNeeds(stack.Push(node), context, mods, gameData);
CheckNeedsRecursive(nodeStack.Push(node), urlConfig);
}
else
{
original.nodes.Remove(node);
i--;
context.progress.NeedsUnsatisfiedNode(context.patchUrl, stack.Push(node));
progress.NeedsUnsatisfiedNode(urlConfig, nodeStack.GetPath() + '/' + node.name);
}
}
catch (ArgumentOutOfRangeException e)
{
context.progress.Exception("ArgumentOutOfRangeException in CheckNeeds for node \"" + node.name + "\"", e);
progress.Exception("ArgumentOutOfRangeException in CheckNeeds for node \"" + node.name + "\"", e);
throw;
}
catch (Exception e)
{
context.progress.Exception("General Exception " + e.GetType().Name + " for node \"" + node.name + "\"", e);
progress.Exception("General Exception " + e.GetType().Name + " for node \"" + node.name + "\"", e);
throw;
}
}
}

/// <summary>
/// Returns true if needs are satisfied.
/// </summary>
private static bool CheckNeeds(ref string name, IEnumerable<string> mods, UrlDir gameData)
{
if (name == null)
return true;

int idxStart = name.IndexOf(":NEEDS[", StringComparison.OrdinalIgnoreCase);
if (idxStart < 0)
return true;
int idxEnd = name.IndexOf(']', idxStart + 7);
string needsString = name.Substring(idxStart + 7, idxEnd - idxStart - 7);

name = name.Substring(0, idxStart) + name.Substring(idxEnd + 1);

// Check to see if all the needed dependencies are present.
foreach (string andDependencies in needsString.Split(',', '&'))
{
bool orMatch = false;
foreach (string orDependency in andDependencies.Split('|'))
{
if (orDependency.Length == 0)
continue;

bool not = orDependency[0] == '!';
string toFind = not ? orDependency.Substring(1) : orDependency;

bool found = mods.Contains(toFind.ToUpper(), StringComparer.OrdinalIgnoreCase);
if (!found && toFind.Contains('/'))
{
string[] splits = toFind.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

found = true;
UrlDir current = gameData;
for (int i = 0; i < splits.Length; i++)
{
current = current.children.FirstOrDefault(dir => dir.name == splits[i]);
if (current == null)
{
found = false;
break;
}
}
}

if (not == !found)
{
orMatch = true;
break;
}
}
if (!orMatch)
return false;
}

return true;
}
}
}

0 comments on commit f5127b1

Please sign in to comment.