Skip to content

Commit 948d9b3

Browse files
committed
Merge branch 'dev' into beta
2 parents 2cc15b9 + a2a360f commit 948d9b3

18 files changed

Lines changed: 1019 additions & 150 deletions

Celeste.Mod.mm/Celeste.Mod.mm.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,9 @@
9393
<Move SourceFiles="@(SplashBinaries)" DestinationFolder="$(PublishDir)\EverestSplash\" />
9494
</Target>
9595

96+
<!-- Anonymize paths -->
97+
<PropertyGroup>
98+
<PathMap>$(MSBuildProjectDirectory)=Celeste.Mod.mm/</PathMap>
99+
</PropertyGroup>
100+
96101
</Project>

Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Celeste.Mod.Core;
33
using Celeste.Mod.Entities;
44
using Celeste.Mod.Helpers;
5+
using Celeste.Mod.Registry;
56
using MAB.DotIgnore;
67
using Microsoft.Xna.Framework;
78
using Monocle;
@@ -609,36 +610,68 @@ internal static void ProcessAssembly(EverestModuleMetadata meta, Assembly asm, T
609610

610611
patch_Level.EntityLoader loader = null;
611612

612-
ConstructorInfo ctor;
613+
ConstructorInfo ctor = null;
613614
MethodInfo gen;
614615

615616
gen = type.GetMethod(genName, new Type[] { typeof(Level), typeof(LevelData), typeof(Vector2), typeof(EntityData) });
616617
if (gen != null && gen.IsStatic && gen.ReturnType.IsCompatible(typeof(Entity))) {
617-
loader = (level, levelData, offset, entityData) => (Entity) gen.Invoke(null, new object[] { level, levelData, offset, entityData });
618+
loader = (level, levelData, offset, entityData) => {
619+
var entityId = ((patch_Level)level).CreateEntityId(levelData, entityData);
620+
var entity = (patch_Entity) gen.Invoke(null, new object[] { level, levelData, offset, entityData });
621+
if (entity != null) {
622+
entity.SourceData = entityData;
623+
entity.SourceId = entityId;
624+
}
625+
626+
return entity;
627+
};
618628
goto RegisterEntityLoader;
619629
}
620630

621631
ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2), typeof(EntityID) });
622632
if (ctor != null) {
623-
loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset, new EntityID(levelData.Name, entityData.ID + (patch_Level._isLoadingTriggers ? 10000000 : 0)) });
633+
loader = (level, levelData, offset, entityData) => {
634+
var entityId = ((patch_Level)level).CreateEntityId(levelData, entityData);
635+
var entity = (patch_Entity) ctor.Invoke(new object[] { entityData, offset, entityId });
636+
entity.SourceData = entityData;
637+
entity.SourceId = entityId;
638+
639+
return entity;
640+
};
624641
goto RegisterEntityLoader;
625642
}
626643

627644
ctor = type.GetConstructor(new Type[] { typeof(EntityData), typeof(Vector2) });
628645
if (ctor != null) {
629-
loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { entityData, offset });
646+
loader = (level, levelData, offset, entityData) => {
647+
var entity = (patch_Entity)ctor.Invoke(new object[] { entityData, offset });
648+
entity.SourceData = entityData;
649+
entity.SourceId = ((patch_Level)level).CreateEntityId(levelData, entityData);
650+
651+
return entity;
652+
};
630653
goto RegisterEntityLoader;
631654
}
632655

633656
ctor = type.GetConstructor(new Type[] { typeof(Vector2) });
634657
if (ctor != null) {
635-
loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(new object[] { offset });
658+
loader = (level, levelData, offset, entityData) => {
659+
var entity = (patch_Entity)ctor.Invoke(new object[] { offset });
660+
entity.SourceData = entityData;
661+
entity.SourceId = ((patch_Level)level).CreateEntityId(levelData, entityData);
662+
return entity;
663+
};
636664
goto RegisterEntityLoader;
637665
}
638666

639667
ctor = type.GetConstructor(Type.EmptyTypes);
640668
if (ctor != null) {
641-
loader = (level, levelData, offset, entityData) => (Entity) ctor.Invoke(null);
669+
loader = (level, levelData, offset, entityData) => {
670+
var entity = (patch_Entity)ctor.Invoke(null);
671+
entity.SourceData = entityData;
672+
entity.SourceId = ((patch_Level)level).CreateEntityId(levelData, entityData);
673+
return entity;
674+
};
642675
goto RegisterEntityLoader;
643676
}
644677

@@ -647,6 +680,13 @@ internal static void ProcessAssembly(EverestModuleMetadata meta, Assembly asm, T
647680
Logger.Warn("core", $"Found custom entity without suitable constructor / {genName}(Level, LevelData, Vector2, EntityData): {id} ({type.FullName})");
648681
continue;
649682
}
683+
684+
// Immediately register the connection when we're calling the ctor,
685+
// since we know the return type upfront.
686+
if (ctor != null) {
687+
EntityRegistry.RegisterSidToTypeConnection(id, ctor.DeclaringType);
688+
}
689+
650690
patch_Level.EntityLoaders[id] = loader;
651691
}
652692
}

Celeste.Mod.mm/Mod/Everest/Everest.Relinker.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ internal static Assembly GetRelinkedAssembly(EverestModuleMetadata meta, string
111111
Assembly asm = null;
112112

113113
// Try to load the assembly from the cache
114-
if (TryLoadCachedAssembly(meta, asmname, path, symPath, cachePath, cacheChecksumPath, out string[] checksums) is not Assembly cacheAsm) {
114+
if (TryLoadCachedAssembly(meta, asmname, path, symPath, cachePath, cacheChecksumPath, out EverestModuleAssemblyContext.AsmChecksums checksums) is not Assembly cacheAsm) {
115115
// Delete cached files
116116
File.Delete(cachePath);
117117
File.Delete(cacheChecksumPath);
@@ -130,7 +130,7 @@ internal static Assembly GetRelinkedAssembly(EverestModuleMetadata meta, string
130130
// Write the checksums for the cached assembly to be loaded in the future
131131
// Skip this step if the relinker had to fall back to using a temporary output file
132132
if (tmpOutPath == null)
133-
File.WriteAllLines(cacheChecksumPath, checksums);
133+
checksums.WriteToFile(cacheChecksumPath);
134134
} catch (Exception e) {
135135
Logger.Warn("relinker", $"Failed relinking {meta} - {asmname}");
136136
Logger.LogDetailed(e);
@@ -145,21 +145,15 @@ internal static Assembly GetRelinkedAssembly(EverestModuleMetadata meta, string
145145
}
146146
}
147147

148-
private static Assembly TryLoadCachedAssembly(EverestModuleMetadata meta, string asmName, string inPath, string inSymPath, string cachePath, string cacheChecksumsPath, out string[] curChecksums) {
148+
private static Assembly TryLoadCachedAssembly(EverestModuleMetadata meta, string asmName, string inPath, string inSymPath, string cachePath, string cacheChecksumsPath, out EverestModuleAssemblyContext.AsmChecksums curChecksums) {
149149
// Calculate checksums
150-
// If the stream originates from a
151-
List<string> checksums = new List<string>();
152-
checksums.Add(GameChecksum);
153-
154-
meta.AssemblyContext.CalcAssemblyCacheChecksums(checksums, inPath, inSymPath);
155-
156-
curChecksums = checksums.ToArray();
150+
curChecksums = meta.AssemblyContext.CalcAssemblyCacheChecksums(inPath, !string.IsNullOrEmpty(meta.PathArchive) ? null : inSymPath);
157151

158152
// Check if the cached assembly + its checksums exist on disk, and if the checksums match
159153
if (!File.Exists(cachePath) || !File.Exists(cacheChecksumsPath))
160154
return null;
161155

162-
if (!ChecksumsEqual(curChecksums, File.ReadAllLines(cacheChecksumsPath)))
156+
if (!curChecksums.IsValidWith(EverestModuleAssemblyContext.AsmChecksums.ReadFromFile(cacheChecksumsPath)))
163157
return null;
164158

165159
Logger.Verbose("relinker", $"Loading cached assembly for {meta} - {asmName}");

Celeste.Mod.mm/Mod/Everest/Everest.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Celeste.Mod.Core;
22
using Celeste.Mod.Helpers;
33
using Celeste.Mod.Helpers.LegacyMonoMod;
4+
using Celeste.Mod.Registry;
45
using Celeste.Mod.UI;
56
using Microsoft.Xna.Framework;
67
using Monocle;
@@ -53,7 +54,7 @@ public static partial class Everest {
5354
/// </summary>
5455
public readonly static string VersionTag;
5556
/// <summary>
56-
/// The currently installed Everest version tag. For "1.2.3-a-b", this is "b"
57+
/// The currently installed Everest version commit. For "1.2.3-a-b", this is "b"
5758
/// </summary>
5859
public readonly static string VersionCommit;
5960

@@ -789,6 +790,9 @@ internal static void Unregister(this EverestModule module) {
789790
((Monocle.patch_Commands) Engine.Commands).ReloadCommandsList();
790791
}
791792

793+
if (module is not NullModule)
794+
EntityRegistry.OnModAssemblyUnload(module.GetType().Assembly);
795+
792796
InvalidateInstallationHash();
793797

794798
module.LogUnregistration();

Celeste.Mod.mm/Mod/Module/EverestModuleAssemblyContext.cs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,180 @@ public void CalcAssemblyCacheChecksums(List<string> checksums, string path, stri
199199
throw new UnreachableException();
200200
}
201201

202+
private string CalcChecksumForFile(string path) {
203+
if (!string.IsNullOrEmpty(ModuleMeta.PathArchive)) {
204+
return ModuleMeta.Hash.ToHexadecimalString();
205+
} else if (!string.IsNullOrEmpty(ModuleMeta.PathDirectory)) {
206+
return Everest.GetChecksum(path).ToHexadecimalString();
207+
} else throw new UnreachableException();
208+
}
209+
210+
/// <summary>
211+
/// Calculates the checksums used to cache the assembly in any relevant
212+
/// caches (like the relinker cache).
213+
/// </summary>
214+
/// <param name="path">The path of the assembly inside of the mod.</param>
215+
/// <param name="symPath">The path of the assembly's symbols inside of mod, or null.</param>
216+
/// <returns>The calculated checksums for the file.</returns>
217+
public AsmChecksums CalcAssemblyCacheChecksums(string path, string symPath) {
218+
Dictionary<string, string> depChecksums = new();
219+
FillChecksums(depChecksums, ModuleMeta.Dependencies);
220+
221+
Dictionary<string, string> optDepChecksums = new();
222+
FillChecksums(optDepChecksums, ModuleMeta.OptionalDependencies);
223+
224+
return new AsmChecksums(
225+
Everest.Relinker.GameChecksum,
226+
CalcChecksumForFile(path),
227+
symPath != null ? CalcChecksumForFile(symPath) : "",
228+
depChecksums,
229+
optDepChecksums);
230+
231+
static void FillChecksums(Dictionary<string, string> dict, List<EverestModuleMetadata> deps) {
232+
foreach (EverestModuleMetadata depMeta in deps) {
233+
if (depMeta.Name == CoreModule.NETCoreMetaName || depMeta.Name == "Everest") continue;
234+
EverestModule actualModule = Everest.Modules.FirstOrDefault(m => m.Metadata.Name == depMeta.Name, null);
235+
if (actualModule == null) continue;
236+
dict[depMeta.Name] = actualModule.Metadata.Hash.ToHexadecimalString();
237+
}
238+
}
239+
}
240+
241+
/// <summary>
242+
/// Represents all the checksum data for an assembly, either from the cache or
243+
/// from one that's about to be loaded.
244+
/// </summary>
245+
public sealed record AsmChecksums(
246+
string GameChecksum,
247+
string FileChecksum,
248+
string SymFileChecksum = "",
249+
Dictionary<string, string> DepHashes = null,
250+
Dictionary<string, string> OptDepHashes = null) {
251+
252+
public static readonly AsmChecksums Empty = new("", "");
253+
254+
/// <summary>
255+
/// Writes the data to a file, see <see cref="ReadFromFile"/> for a description
256+
/// of the file format.
257+
/// </summary>
258+
/// <param name="path">The path to the file to write to.</param>
259+
public void WriteToFile(string path) {
260+
List<string> lines = new();
261+
lines.Add(GameChecksum);
262+
lines.Add(FileChecksum);
263+
lines.Add(SymFileChecksum ?? "");
264+
265+
WriteAsLines(DepHashes);
266+
267+
lines.Add("");
268+
269+
WriteAsLines(OptDepHashes);
270+
271+
File.WriteAllLines(path, lines);
272+
return;
273+
274+
void WriteAsLines(Dictionary<string, string> dict) {
275+
foreach ((string metaName, string hash) in dict) {
276+
lines.Add($"{metaName}:{hash}");
277+
}
278+
}
279+
}
280+
281+
/// <summary>
282+
/// Creates an instance from a file, format is as following:
283+
/// We take all the data and partition it on each new line character into strings, keeping empty strings.
284+
/// Then, the first element will be the game checksum, the second one, the file checksum,
285+
/// the third one, the symbol file (pdb) checksum, or empty an empty string if it doesn't exist or we are working with
286+
/// archives (zips).
287+
/// After the third element, each element is of the format `Name:Hash`, where Name is the meta name and
288+
/// Hash is the hash for that meta, this represents a dependency. Until an empty element, which marks the
289+
/// end of the dependencies list. After this element and until the end of file, the optional dependencies list follow
290+
/// in the same format.
291+
/// </summary>
292+
/// <param name="path">Path to read from.</param>
293+
/// <returns>A populated instance.</returns>
294+
public static AsmChecksums ReadFromFile(string path) {
295+
string[] data = File.ReadAllLines(path);
296+
if (data.Length < 2) return Empty;
297+
298+
string gameChecksum = data[0];
299+
string fileChecksum = data[1];
300+
if (data.Length <= 2) return new AsmChecksums(gameChecksum, fileChecksum);
301+
string symChecksum = data[2];
302+
if (data.Length <= 3) return new AsmChecksums(gameChecksum, fileChecksum, symChecksum);
303+
Dictionary<string, string> depChecksums = new();
304+
Dictionary<string, string> optDepChecksums = new();
305+
306+
Dictionary<string, string> currentDict = depChecksums;
307+
int index = 3;
308+
while (index < data.Length) {
309+
if (data[index] == "") {
310+
if (currentDict == depChecksums) {
311+
currentDict = optDepChecksums;
312+
index++;
313+
continue;
314+
} else {
315+
// Malformed cache, return minimum data
316+
return new AsmChecksums(gameChecksum, fileChecksum, symChecksum);
317+
}
318+
}
319+
ReadOnlySpan<char> curr = data[index];
320+
int sepIdx = curr.LastIndexOf(':');
321+
if (sepIdx == -1) {
322+
// Malformed cache, return minimum data
323+
return new AsmChecksums(gameChecksum, fileChecksum, symChecksum);
324+
}
325+
326+
ReadOnlySpan<char> metaName = curr[..sepIdx];
327+
ReadOnlySpan<char> metaHash = curr[(sepIdx + 1)..];
328+
currentDict[metaName.ToString()] = metaHash.ToString();
329+
330+
index++;
331+
}
332+
return new AsmChecksums(gameChecksum, fileChecksum, symChecksum, depChecksums, optDepChecksums);
333+
}
334+
335+
/// <summary>
336+
/// Verifies whether the current instance is a valid in respect to the one passed in:
337+
/// Game, file, and symbol checksums must always match for it to be valid, but
338+
/// the requirements for dependencies and optional dependencies are different.
339+
/// We consider this instance valid with respect to the one passed in, if for every dependency D present
340+
/// in this instance, D is also present in <paramref name="other"/>, and it is associated with the same checksum.
341+
/// The same reasoning goes with optional dependencies.
342+
/// </summary>
343+
/// <param name="other">The other cache to check with.</param>
344+
/// <returns>Whether this is compatible with <paramref name="other"/></returns>
345+
public bool IsValidWith(AsmChecksums other) {
346+
if (other == null) return false;
347+
if (GameChecksum != other.GameChecksum) return false;
348+
if (FileChecksum != other.FileChecksum) return false;
349+
if (SymFileChecksum != other.SymFileChecksum) return false;
350+
351+
if (!Check(DepHashes, other.DepHashes)) return false;
352+
353+
if (!Check(OptDepHashes, other.OptDepHashes)) return false;
354+
355+
return true;
356+
357+
static bool Check(Dictionary<string, string> dict, Dictionary<string, string> otherDict) {
358+
// A null list indicates no dependencies, thus, it always satisfies the requirements
359+
if (dict != null) {
360+
if (otherDict != null) {
361+
foreach ((string metaName, string hash) in dict) {
362+
if (!otherDict.TryGetValue(metaName, out string otherHash)) {
363+
return false;
364+
}
365+
if (hash != otherHash) return false;
366+
}
367+
} else if (dict.Count != 0)
368+
// Having an empty and null list should be equivalent
369+
return false;
370+
}
371+
return true;
372+
}
373+
}
374+
}
375+
202376
/// <summary>
203377
/// Tries to load an assembly from a given path inside the mod.
204378
/// This path is an absolute path if the the mod was loaded from a directory, or a path into the mod ZIP otherwise.

0 commit comments

Comments
 (0)