Skip to content

Commit 916eee5

Browse files
authored
Merge pull request #1 from Carnagion/development
Merge v1.0.0 into stable
2 parents 2bebb0f + ab8efba commit 916eee5

File tree

9 files changed

+805
-0
lines changed

9 files changed

+805
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Carnagion
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Mod.cs

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Xml;
7+
8+
using JetBrains.Annotations;
9+
10+
using Godot.Serialization;
11+
12+
namespace Godot.Modding
13+
{
14+
/// <summary>
15+
/// Represents a modular component loaded at runtime, with its own assemblies, resource packs, and data.
16+
/// </summary>
17+
[PublicAPI]
18+
public sealed record Mod
19+
{
20+
/// <summary>
21+
/// Initializes a new <see cref="Mod"/> using <paramref name="metadata"/>.
22+
/// </summary>
23+
/// <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>
24+
public Mod(Metadata metadata)
25+
{
26+
this.Meta = metadata;
27+
this.Assemblies = this.LoadAssemblies();
28+
this.Data = this.LoadData();
29+
this.LoadResources();
30+
}
31+
32+
/// <summary>
33+
/// The metadata of the <see cref="Mod"/>, such as its ID, name, load order, etc.
34+
/// </summary>
35+
public Metadata Meta
36+
{
37+
get;
38+
}
39+
40+
/// <summary>
41+
/// The assemblies of the <see cref="Mod"/>.
42+
/// </summary>
43+
public IEnumerable<Assembly> Assemblies
44+
{
45+
get;
46+
}
47+
48+
/// <summary>
49+
/// The XML data of the <see cref="Mod"/>, combined into a single <see cref="XmlNode"/> as its children.
50+
/// </summary>
51+
public XmlNode? Data
52+
{
53+
get;
54+
}
55+
56+
private IEnumerable<Assembly> LoadAssemblies()
57+
{
58+
string assembliesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Assemblies";
59+
60+
return System.IO.Directory.Exists(assembliesPath)
61+
? from assemblyPath in System.IO.Directory.GetFiles(assembliesPath, "*.dll", SearchOption.AllDirectories)
62+
select Assembly.LoadFile(assemblyPath)
63+
: Enumerable.Empty<Assembly>();
64+
}
65+
66+
private XmlNode? LoadData()
67+
{
68+
IEnumerable<XmlDocument> documents = this.LoadDocuments().ToArray();
69+
if (!documents.Any())
70+
{
71+
return null;
72+
}
73+
74+
XmlDocument data = new();
75+
data.InsertBefore(data.CreateXmlDeclaration("1.0", "UTF-8", null), data.DocumentElement);
76+
(from document in documents
77+
from node in document.Cast<XmlNode>()
78+
where node.NodeType is not XmlNodeType.XmlDeclaration
79+
select node).ForEach(node => data.AppendChild(node));
80+
return data;
81+
}
82+
83+
private IEnumerable<XmlDocument> LoadDocuments()
84+
{
85+
string dataPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Data";
86+
87+
if (!System.IO.Directory.Exists(dataPath))
88+
{
89+
yield break;
90+
}
91+
92+
foreach (string xmlPath in System.IO.Directory.GetFiles(dataPath, "*.xml", SearchOption.AllDirectories))
93+
{
94+
XmlDocument document = new();
95+
document.Load(xmlPath);
96+
yield return document;
97+
}
98+
}
99+
100+
private void LoadResources()
101+
{
102+
string resourcesPath = $"{this.Meta.Directory}{System.IO.Path.DirectorySeparatorChar}Resources";
103+
104+
if (!System.IO.Directory.Exists(resourcesPath))
105+
{
106+
return;
107+
}
108+
109+
foreach (string resourcePath in System.IO.Directory.GetFiles(resourcesPath, "*.pck", SearchOption.AllDirectories))
110+
{
111+
if (!ProjectSettings.LoadResourcePack(resourcePath))
112+
{
113+
throw new ModLoadException(this.Meta.Directory, $"Error loading resource pack at {resourcePath}");
114+
}
115+
}
116+
}
117+
118+
/// <summary>
119+
/// Represents the metadata of a <see cref="Mod"/>, such as its unique ID, name, author, load order, etc.
120+
/// </summary>
121+
[PublicAPI]
122+
public sealed record Metadata
123+
{
124+
[UsedImplicitly]
125+
private Metadata()
126+
{
127+
}
128+
129+
/// <summary>
130+
/// The directory where the <see cref="Metadata"/> was loaded from.
131+
/// </summary>
132+
[Serialize]
133+
public string Directory
134+
{
135+
get;
136+
[UsedImplicitly]
137+
private set;
138+
} = null!;
139+
140+
/// <summary>
141+
/// The unique ID of the <see cref="Mod"/>.
142+
/// </summary>
143+
[Serialize]
144+
public string Id
145+
{
146+
get;
147+
[UsedImplicitly]
148+
private set;
149+
} = null!;
150+
151+
/// <summary>
152+
/// The name of the <see cref="Mod"/>.
153+
/// </summary>
154+
[Serialize]
155+
public string Name
156+
{
157+
get;
158+
[UsedImplicitly]
159+
private set;
160+
} = null!;
161+
162+
/// <summary>
163+
/// The individual or group that created the <see cref="Mod"/>.
164+
/// </summary>
165+
[Serialize]
166+
public string Author
167+
{
168+
get;
169+
[UsedImplicitly]
170+
private set;
171+
} = null!;
172+
173+
/// <summary>
174+
/// The unique IDs of all other <see cref="Mod"/>s that the <see cref="Mod"/> depends on.
175+
/// </summary>
176+
public IEnumerable<string> Dependencies
177+
{
178+
get;
179+
[UsedImplicitly]
180+
private set;
181+
} = Enumerable.Empty<string>();
182+
183+
/// <summary>
184+
/// The unique IDs of all other <see cref="Mod"/>s that should be loaded before the <see cref="Mod"/>.
185+
/// </summary>
186+
public IEnumerable<string> Before
187+
{
188+
get;
189+
[UsedImplicitly]
190+
private set;
191+
} = Enumerable.Empty<string>();
192+
193+
/// <summary>
194+
/// The unique IDs of all other <see cref="Mod"/>s that should be loaded after the <see cref="Mod"/>.
195+
/// </summary>
196+
public IEnumerable<string> After
197+
{
198+
get;
199+
[UsedImplicitly]
200+
private set;
201+
} = Enumerable.Empty<string>();
202+
203+
/// <summary>
204+
/// The unique IDs of all other <see cref="Mod"/>s that are incompatible with the <see cref="Mod"/>.
205+
/// </summary>
206+
public IEnumerable<string> Incompatible
207+
{
208+
get;
209+
[UsedImplicitly]
210+
private set;
211+
} = Enumerable.Empty<string>();
212+
213+
/// <summary>
214+
/// Loads a <see cref="Metadata"/> from <paramref name="directoryPath"/>.
215+
/// </summary>
216+
/// <param name="directoryPath">The directory path. It must contain a "Mod.xml" file inside it with valid metadata.</param>
217+
/// <returns>A <see cref="Metadata"/> loaded from <paramref name="directoryPath"/>.</returns>
218+
/// <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>
219+
public static Metadata Load(string directoryPath)
220+
{
221+
string metadataFilePath = $"{directoryPath}{System.IO.Path.DirectorySeparatorChar}Mod.xml";
222+
223+
if (!System.IO.File.Exists(metadataFilePath))
224+
{
225+
throw new ModLoadException(directoryPath, new FileNotFoundException($"Mod metadata file {metadataFilePath} does not exist"));
226+
}
227+
228+
try
229+
{
230+
XmlDocument document = new();
231+
document.Load(metadataFilePath);
232+
if (document.DocumentElement?.Name is not "Mod")
233+
{
234+
throw new ModLoadException(directoryPath, "Root XML node \"Mod\" for serializing mod metadata does not exist");
235+
}
236+
237+
XmlNode directoryNode = document.CreateNode(XmlNodeType.Element, "Directory", null);
238+
directoryNode.InnerText = directoryPath;
239+
document.DocumentElement.AppendChild(directoryNode);
240+
241+
Metadata metadata = new Serializer().Deserialize<Metadata>(document.DocumentElement)!;
242+
return metadata.IsValid() ? metadata : throw new ModLoadException(directoryPath, "Invalid metadata");
243+
}
244+
catch (Exception exception) when (exception is not ModLoadException)
245+
{
246+
throw new ModLoadException(directoryPath, exception);
247+
}
248+
}
249+
250+
private bool IsValid()
251+
{
252+
// Check that the incompatible, load before, and load after lists don't have anything in common or contain the mod's own ID
253+
bool invalidLoadOrder = this.Id.Yield()
254+
.Concat(this.Incompatible)
255+
.Concat(this.Before)
256+
.Concat(this.After)
257+
.Indistinct()
258+
.Any();
259+
// Check that the dependency and incompatible lists don't have anything in common
260+
bool invalidDependencies = this.Dependencies
261+
.Intersect(this.Incompatible)
262+
.Any();
263+
return !(invalidLoadOrder || invalidDependencies);
264+
}
265+
}
266+
}
267+
}

ModLoadException.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
3+
namespace Godot.Modding
4+
{
5+
/// <summary>
6+
/// The exception thrown when an error occurs while loading a <see cref="Mod"/>.
7+
/// </summary>
8+
public class ModLoadException : Exception
9+
{
10+
/// <summary>
11+
/// Initializes a new <see cref="ModLoadException"/> with the specified arguments.
12+
/// </summary>
13+
/// <param name="directoryPath">The directory path from where an attempt was made to load the <see cref="Mod"/>.</param>
14+
/// <param name="message">A brief description of the issue.</param>
15+
public ModLoadException(string directoryPath, string message) : base($"Could not load mod at {directoryPath}: {message}")
16+
{
17+
}
18+
19+
/// <summary>
20+
/// Initializes a new <see cref="ModLoadException"/> with the specified arguments.
21+
/// </summary>
22+
/// <param name="directoryPath">The directory path from where an attempt was made to load the <see cref="Mod"/>.</param>
23+
/// <param name="cause">The <see cref="Exception"/> that caused the loading to fail.</param>
24+
public ModLoadException(string directoryPath, Exception cause) : base($"Could not load mod at {directoryPath}.{System.Environment.NewLine}{cause}")
25+
{
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)