forked from ppy/osu
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRealmRulesetStore.cs
More file actions
204 lines (171 loc) · 9.46 KB
/
Copy pathRealmRulesetStore.cs
File metadata and controls
204 lines (171 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
namespace osu.Game.Rulesets
{
public class RealmRulesetStore : RulesetStore
{
private readonly RealmAccess realmAccess;
public override IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
public RealmRulesetStore(RealmAccess realmAccess, Storage? storage = null)
: base(storage)
{
this.realmAccess = realmAccess;
prepareDetachedRulesets();
informUserAboutBrokenRulesets();
}
[UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "InstantiationInfo is a trusted ruleset type name from loaded assemblies.")]
private void prepareDetachedRulesets()
{
realmAccess.Write(realm =>
{
var rulesets = realm.All<RulesetInfo>();
List<Ruleset> instances = LoadedAssemblies.Values
.Select(r => Activator.CreateInstance(r) as Ruleset)
.Where(r => r != null)
.Select(r => r.AsNonNull())
.ToList();
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
foreach (var r in instances.Where(r => r is ILegacyRuleset))
{
if (realm.All<RulesetInfo>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
}
// add any other rulesets which have assemblies present but are not yet in the database.
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
{
var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
if (existingSameShortName != null)
{
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
// in such cases, update the instantiation info of the existing entry to point to the new one.
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
}
else
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
}
}
List<RulesetInfo> detachedRulesets = new List<RulesetInfo>();
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
foreach (var r in rulesets.OrderBy(r => r.OnlineID))
{
try
{
var resolvedType = Type.GetType(r.InstantiationInfo);
if (resolvedType == null)
{
// ruleset DLL was probably deleted.
r.Available = false;
continue;
}
var instance = (Activator.CreateInstance(resolvedType) as Ruleset);
var instanceInfo = instance?.RulesetInfo
?? throw new RulesetLoadException(@"Instantiation failure");
if (!checkRulesetUpToDate(instance))
{
throw new ArgumentOutOfRangeException(nameof(instance.RulesetAPIVersionSupported),
$"Ruleset API version is too old (was {instance.RulesetAPIVersionSupported}, expected {Ruleset.CURRENT_RULESET_API_VERSION})");
}
if (r.OnlineID != instanceInfo.OnlineID)
throw new InvalidOperationException($@"Online ID mismatch for ruleset {r.ShortName}: database has {r.OnlineID}, constructed instance has {instanceInfo.OnlineID}");
if (r.OnlineID > 0 && rulesets.Any(otherRuleset => otherRuleset.ShortName != r.ShortName && otherRuleset.OnlineID == r.OnlineID))
throw new InvalidOperationException($@"Ruleset {r.ShortName} shares online ID {r.OnlineID} with another ruleset");
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
resolvedType.Assembly.GetTypes();
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
r.InstantiationInfo = instanceInfo.InstantiationInfo;
r.Available = true;
testRulesetCompatibility(r);
detachedRulesets.Add(r.Clone());
}
catch (Exception ex)
{
r.Available = false;
LogRulesetFailure(r, ex);
}
}
availableRulesets.AddRange(detachedRulesets.Order());
});
}
private bool checkRulesetUpToDate(Ruleset instance)
{
switch (instance.RulesetAPIVersionSupported)
{
// The default `virtual` implementation leaves the version string empty.
// Consider rulesets which haven't override the version as up-to-date for now.
// At some point (once ruleset devs add versioning), we'll probably want to disallow this for deployed builds.
case @"":
// Ruleset is up-to-date, all good.
case Ruleset.CURRENT_RULESET_API_VERSION:
return true;
default:
return false;
}
}
private void testRulesetCompatibility(RulesetInfo rulesetInfo)
{
// do various operations to ensure that we are in a good state.
// if we can avoid loading the ruleset at this point (rather than erroring later in runtime) then that is preferred.
var instance = rulesetInfo.CreateInstance();
instance.CreateAllMods();
instance.CreateIcon();
instance.CreateResourceStore();
var beatmap = new Beatmap();
var converter = instance.CreateBeatmapConverter(beatmap);
instance.CreateBeatmapProcessor(converter.Convert());
}
private void informUserAboutBrokenRulesets()
{
if (RulesetStorage == null)
return;
foreach (string brokenRulesetDll in RulesetStorage.GetFiles(@".", @"*.dll.broken"))
{
Logger.Log($"Ruleset '{Path.GetFileNameWithoutExtension(brokenRulesetDll)}' has been disabled due to causing a crash.\n\n"
+ "Please update the ruleset or report the issue to the developers of the ruleset if no updates are available.", level: LogLevel.Important);
}
}
internal void TryDisableCustomRulesetsCausing(Exception exception)
{
try
{
var stackTrace = new StackTrace(exception);
foreach (var frame in stackTrace.GetFrames())
{
var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly;
if (declaringAssembly == null)
continue;
if (UserRulesetAssemblies.Contains(declaringAssembly))
{
string sourceLocation = declaringAssembly.Location;
string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken");
if (File.Exists(sourceLocation))
{
Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken.");
File.Move(sourceLocation, destinationLocation);
}
}
}
}
catch (Exception ex)
{
Logger.Log($"Attempt to trace back crash to custom ruleset failed: {ex}");
}
}
}
}