Skip to content

Commit

Permalink
Merge pull request #1 from Carnagion/development
Browse files Browse the repository at this point in the history
Merge v1.0.0 into stable
  • Loading branch information
Carnagion authored Jun 18, 2022
2 parents 2bebb0f + ab8efba commit 916eee5
Show file tree
Hide file tree
Showing 9 changed files with 805 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Carnagion

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
267 changes: 267 additions & 0 deletions Mod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;

using JetBrains.Annotations;

using Godot.Serialization;

namespace Godot.Modding
{
/// <summary>
/// Represents a modular component loaded at runtime, with its own assemblies, resource packs, and data.
/// </summary>
[PublicAPI]
public sealed record Mod
{
/// <summary>
/// Initializes a new <see cref="Mod"/> using <paramref name="metadata"/>.
/// </summary>
/// <param name="metadata">The <see cref="Metadata"/> to use. Assemblies, resource packs, and data are all loaded according to the directory specified in the metadata.</param>
public Mod(Metadata metadata)
{
this.Meta = metadata;
this.Assemblies = this.LoadAssemblies();
this.Data = this.LoadData();
this.LoadResources();
}

/// <summary>
/// The metadata of the <see cref="Mod"/>, such as its ID, name, load order, etc.
/// </summary>
public Metadata Meta
{
get;
}

/// <summary>
/// The assemblies of the <see cref="Mod"/>.
/// </summary>
public IEnumerable<Assembly> Assemblies
{
get;
}

/// <summary>
/// The XML data of the <see cref="Mod"/>, combined into a single <see cref="XmlNode"/> as its children.
/// </summary>
public XmlNode? Data
{
get;
}

private IEnumerable<Assembly> LoadAssemblies()
{
string assembliesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Assemblies";

return System.IO.Directory.Exists(assembliesPath)
? from assemblyPath in System.IO.Directory.GetFiles(assembliesPath, "*.dll", SearchOption.AllDirectories)
select Assembly.LoadFile(assemblyPath)
: Enumerable.Empty<Assembly>();
}

private XmlNode? LoadData()
{
IEnumerable<XmlDocument> documents = this.LoadDocuments().ToArray();
if (!documents.Any())
{
return null;
}

XmlDocument data = new();
data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), data.DocumentElement);
(from document in documents
from node in document.Cast<XmlNode>()
where node.NodeType is not XmlNodeType.XmlDeclaration
select node).ForEach(node => data.AppendChild(node));
return data;
}

private IEnumerable<XmlDocument> LoadDocuments()
{
string dataPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Data";

if (!System.IO.Directory.Exists(dataPath))
{
yield break;
}

foreach (string xmlPath in System.IO.Directory.GetFiles(dataPath, "*.xml", SearchOption.AllDirectories))
{
XmlDocument document = new();
document.Load(xmlPath);
yield return document;
}
}

private void LoadResources()
{
string resourcesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Resources";

if (!System.IO.Directory.Exists(resourcesPath))
{
return;
}

foreach (string resourcePath in System.IO.Directory.GetFiles(resourcesPath, "*.pck", SearchOption.AllDirectories))
{
if (!ProjectSettings.LoadResourcePack(resourcePath))
{
throw new ModLoadException(this.Meta.Directory, $"Error loading resource pack at {resourcePath}");
}
}
}

/// <summary>
/// Represents the metadata of a <see cref="Mod"/>, such as its unique ID, name, author, load order, etc.
/// </summary>
[PublicAPI]
public sealed record Metadata
{
[UsedImplicitly]
private Metadata()
{
}

/// <summary>
/// The directory where the <see cref="Metadata"/> was loaded from.
/// </summary>
[Serialize]
public string Directory
{
get;
[UsedImplicitly]
private set;
} = null!;

/// <summary>
/// The unique ID of the <see cref="Mod"/>.
/// </summary>
[Serialize]
public string Id
{
get;
[UsedImplicitly]
private set;
} = null!;

/// <summary>
/// The name of the <see cref="Mod"/>.
/// </summary>
[Serialize]
public string Name
{
get;
[UsedImplicitly]
private set;
} = null!;

/// <summary>
/// The individual or group that created the <see cref="Mod"/>.
/// </summary>
[Serialize]
public string Author
{
get;
[UsedImplicitly]
private set;
} = null!;

/// <summary>
/// The unique IDs of all other <see cref="Mod"/>s that the <see cref="Mod"/> depends on.
/// </summary>
public IEnumerable<string> Dependencies
{
get;
[UsedImplicitly]
private set;
} = Enumerable.Empty<string>();

/// <summary>
/// The unique IDs of all other <see cref="Mod"/>s that should be loaded before the <see cref="Mod"/>.
/// </summary>
public IEnumerable<string> Before
{
get;
[UsedImplicitly]
private set;
} = Enumerable.Empty<string>();

/// <summary>
/// The unique IDs of all other <see cref="Mod"/>s that should be loaded after the <see cref="Mod"/>.
/// </summary>
public IEnumerable<string> After
{
get;
[UsedImplicitly]
private set;
} = Enumerable.Empty<string>();

/// <summary>
/// The unique IDs of all other <see cref="Mod"/>s that are incompatible with the <see cref="Mod"/>.
/// </summary>
public IEnumerable<string> Incompatible
{
get;
[UsedImplicitly]
private set;
} = Enumerable.Empty<string>();

/// <summary>
/// Loads a <see cref="Metadata"/> from <paramref name="directoryPath"/>.
/// </summary>
/// <param name="directoryPath">The directory path. It must contain a "Mod.xml" file inside it with valid metadata.</param>
/// <returns>A <see cref="Metadata"/> loaded from <paramref name="directoryPath"/>.</returns>
/// <exception cref="ModLoadException">Thrown if the metadata file does not exist, or the metadata is invalid, or if there is another unexpected issue while trying to load the metadata.</exception>
public static Metadata Load(string directoryPath)
{
string metadataFilePath = $"{directoryPath}{System.IO.Path.DirectorySeparatorChar}Mod.xml";

if (!System.IO.File.Exists(metadataFilePath))
{
throw new ModLoadException(directoryPath, new FileNotFoundException($"Mod metadata file {metadataFilePath} does not exist"));
}

try
{
XmlDocument document = new();
document.Load(metadataFilePath);
if (document.DocumentElement?.Name is not "Mod")
{
throw new ModLoadException(directoryPath, "Root XML node \"Mod\" for serializing mod metadata does not exist");
}

XmlNode directoryNode = document.CreateNode(XmlNodeType.Element, "Directory", null);
directoryNode.InnerText = directoryPath;
document.DocumentElement.AppendChild(directoryNode);

Metadata metadata = new Serializer().Deserialize<Metadata>(document.DocumentElement)!;
return metadata.IsValid() ? metadata : throw new ModLoadException(directoryPath, "Invalid metadata");
}
catch (Exception exception) when (exception is not ModLoadException)
{
throw new ModLoadException(directoryPath, exception);
}
}

private bool IsValid()
{
// Check that the incompatible, load before, and load after lists don't have anything in common or contain the mod's own ID
bool invalidLoadOrder = this.Id.Yield()
.Concat(this.Incompatible)
.Concat(this.Before)
.Concat(this.After)
.Indistinct()
.Any();
// Check that the dependency and incompatible lists don't have anything in common
bool invalidDependencies = this.Dependencies
.Intersect(this.Incompatible)
.Any();
return !(invalidLoadOrder || invalidDependencies);
}
}
}
}
28 changes: 28 additions & 0 deletions ModLoadException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;

namespace Godot.Modding
{
/// <summary>
/// The exception thrown when an error occurs while loading a <see cref="Mod"/>.
/// </summary>
public class ModLoadException : Exception
{
/// <summary>
/// Initializes a new <see cref="ModLoadException"/> with the specified arguments.
/// </summary>
/// <param name="directoryPath">The directory path from where an attempt was made to load the <see cref="Mod"/>.</param>
/// <param name="message">A brief description of the issue.</param>
public ModLoadException(string directoryPath, string message) : base($"Could not load mod at {directoryPath}: {message}")
{
}

/// <summary>
/// Initializes a new <see cref="ModLoadException"/> with the specified arguments.
/// </summary>
/// <param name="directoryPath">The directory path from where an attempt was made to load the <see cref="Mod"/>.</param>
/// <param name="cause">The <see cref="Exception"/> that caused the loading to fail.</param>
public ModLoadException(string directoryPath, Exception cause) : base($"Could not load mod at {directoryPath}.{System.Environment.NewLine}{cause}")
{
}
}
}
Loading

0 comments on commit 916eee5

Please sign in to comment.