Skip to content

Commit 436629b

Browse files
committed
Support load custom rulesets
1 parent 76929de commit 436629b

13 files changed

Lines changed: 239 additions & 67 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,3 +876,4 @@ $RECYCLE.BIN/
876876
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudio,visualstudiocode,jetbrains+all,csharp,aspnetcore
877877

878878
beatmaps/
879+
rulesets/

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
2626
ENV ASPNETCORE_URLS=http://0.0.0.0:8080 \
2727
SAVE_BEATMAP_FILES=false \
2828
BEATMAPS_PATH=/data/beatmaps \
29+
RULESETS_PATH=/data/rulesets \
2930
MAX_BEATMAP_FILE_SIZE=5242880
3031

3132
VOLUME ["/data"]

PerformanceServer/AppSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ public static class AppSettings
77
{
88
public static bool SaveBeatmapFiles { get; set; }
99
public static string BeatmapsPath { get; set; } = "./beatmaps";
10+
public static string RulesetsPath { get; set; } = "./rulesets";
1011
public static string OsuFileWebUrl { get; set; } = "https://osu.ppy.sh/osu/{0}";
1112
public static int MaxBeatmapFileSize { get; set; } = 5 * 1024 * 1024; // 5 MB
1213

1314
static AppSettings()
1415
{
1516
SaveBeatmapFiles = Environment.GetEnvironmentVariable("SAVE_BEATMAP_FILES")?.ToLower() == "true";
1617
BeatmapsPath = Environment.GetEnvironmentVariable("BEATMAPS_PATH") ?? BeatmapsPath;
18+
RulesetsPath = Environment.GetEnvironmentVariable("RULESETS_PATH") ?? RulesetsPath;
1719
OsuFileWebUrl = Environment.GetEnvironmentVariable("OSU_FILE_WEB_URL") ?? OsuFileWebUrl;
1820
if (int.TryParse(Environment.GetEnvironmentVariable("MAX_BEATMAP_FILE_SIZE"), out int size))
1921
{

PerformanceServer/BeatmapDifficultyController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using osu.Game.Rulesets;
99
using osu.Game.Rulesets.Difficulty;
1010
using osu.Game.Rulesets.Mods;
11+
using PerformanceServer.Rulesets;
1112

1213
namespace PerformanceServer
1314
{
@@ -23,7 +24,7 @@ public class DifficultyRequestBody : INeedsRuleset
2324

2425
[ApiController]
2526
[Route("difficulty")]
26-
public class BeatmapDifficultyController : ControllerBase
27+
public class BeatmapDifficultyController(IRulesetManager manager) : ControllerBase
2728
{
2829
[HttpPost]
2930
[Consumes("application/json")]
@@ -52,7 +53,7 @@ public async Task<ActionResult<DifficultyAttributes>> CalculateDifficulty([FromB
5253
}
5354
}
5455

55-
Ruleset ruleset = Helper.GetRuleset(body, workingBeatmap.BeatmapInfo.Ruleset.OnlineID);
56+
Ruleset ruleset = manager.GetRuleset(body, workingBeatmap.BeatmapInfo.Ruleset.OnlineID);
5657
Mod[] mods = body.Mods.Select(m => m.ToMod(ruleset)).ToArray();
5758
DifficultyAttributes? difficultyAttributes =
5859
ruleset.CreateDifficultyCalculator(workingBeatmap).Calculate(mods);

PerformanceServer/Helper.cs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,53 +12,6 @@ namespace PerformanceServer
1212
{
1313
public static class Helper
1414
{
15-
public static Ruleset GetRuleset(INeedsRuleset body, int defaultRulesetId = -1)
16-
{
17-
Ruleset ruleset;
18-
if (!string.IsNullOrEmpty(body.RulesetName))
19-
{
20-
ruleset = GetRuleset(body.RulesetName);
21-
}
22-
else if (body.RulesetId != null)
23-
{
24-
ruleset = GetRuleset(body.RulesetId.Value);
25-
}
26-
else if (defaultRulesetId >= -1)
27-
{
28-
ruleset = GetRuleset(defaultRulesetId);
29-
}
30-
else
31-
{
32-
throw new ArgumentException("No ruleset provided.");
33-
}
34-
35-
return ruleset;
36-
}
37-
38-
public static Ruleset GetRuleset(int rulesetId)
39-
{
40-
return rulesetId switch
41-
{
42-
0 => GetRuleset("osu"),
43-
1 => GetRuleset("taiko"),
44-
2 => GetRuleset("fruits"),
45-
3 => GetRuleset("mania"),
46-
_ => throw new ArgumentException("Invalid ruleset ID provided.")
47-
};
48-
}
49-
50-
public static Ruleset GetRuleset(string shortName)
51-
{
52-
return shortName switch
53-
{
54-
"osu" => new OsuRuleset(),
55-
"taiko" => new TaikoRuleset(),
56-
"fruits" or "catch" => new CatchRuleset(),
57-
"mania" => new ManiaRuleset(),
58-
_ => throw new ArgumentException("Invalid ruleset name provided.")
59-
};
60-
}
61-
6215
public static string ComputeMd5(string input)
6316
{
6417
byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input);

PerformanceServer/PerformanceController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using osu.Game.Rulesets.Mods;
1111
using osu.Game.Rulesets.Scoring;
1212
using osu.Game.Scoring;
13+
using PerformanceServer.Rulesets;
1314

1415
namespace PerformanceServer
1516
{
@@ -29,7 +30,7 @@ public class PerformanceRequestBody : INeedsRuleset
2930

3031
[ApiController]
3132
[Route("performance")]
32-
public class PerformanceController : ControllerBase
33+
public class PerformanceController(IRulesetManager manager) : ControllerBase
3334
{
3435
[HttpPost]
3536
[Consumes("application/json")]
@@ -43,7 +44,7 @@ public async Task<ActionResult<PerformanceAttributes>> CalculatePerformance(
4344
Ruleset ruleset;
4445
try
4546
{
46-
ruleset = Helper.GetRuleset(body);
47+
ruleset = manager.GetRuleset(body);
4748
}
4849
catch (ArgumentException e)
4950
{

PerformanceServer/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
// Copyright (c) 2025 GooGuTeam
22
// Licensed under the MIT Licence. See the LICENCE file in the repository root for full licence text.
33

4+
using PerformanceServer.Rulesets;
5+
46
namespace PerformanceServer
57
{
68
public static class Program
79
{
810
public static void Main(string[] args)
911
{
1012
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
13+
builder.Logging.AddConsole();
14+
builder.Services.AddSingleton<IRulesetManager, RulesetManager>();
15+
builder.Services.AddHostedService<RulesetInitializer>();
1116
builder.Services.AddControllers()
1217
.AddNewtonsoftJson(options =>
1318
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2025 GooGuTeam
2+
// Licensed under the MIT Licence. See the LICENCE file in the repository root for full licence text.
3+
4+
using osu.Game.Rulesets;
5+
6+
namespace PerformanceServer.Rulesets
7+
{
8+
public interface IRulesetManager
9+
{
10+
public Ruleset GetRuleset(int rulesetId);
11+
public Ruleset GetRuleset(string shortName);
12+
public Ruleset GetRuleset(INeedsRuleset body, int defaultRulesetId = -1);
13+
}
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2025 GooGuTeam
2+
// Licensed under the MIT Licence. See the LICENCE file in the repository root for full licence text.
3+
4+
namespace PerformanceServer.Rulesets
5+
{
6+
public class RulesetInitializer(IRulesetManager rulesetManager, ILogger<RulesetInitializer> logger)
7+
: IHostedService
8+
{
9+
private readonly IRulesetManager _rulesetManager = rulesetManager;
10+
11+
public Task StartAsync(CancellationToken cancellationToken)
12+
{
13+
logger.LogInformation("Initialized all rulesets");
14+
return Task.CompletedTask;
15+
}
16+
17+
public Task StopAsync(CancellationToken cancellationToken)
18+
{
19+
return Task.CompletedTask;
20+
}
21+
}
22+
23+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) 2025 GooGuTeam
2+
// Licensed under the MIT Licence. See the LICENCE file in the repository root for full licence text.
3+
4+
using osu.Game.Rulesets;
5+
using osu.Game.Rulesets.Catch;
6+
using osu.Game.Rulesets.Mania;
7+
using osu.Game.Rulesets.Osu;
8+
using osu.Game.Rulesets.Taiko;
9+
using System.Reflection;
10+
11+
namespace PerformanceServer.Rulesets
12+
{
13+
public class RulesetManager : IRulesetManager
14+
{
15+
private readonly ILogger<RulesetManager> _logger;
16+
17+
private const string RulesetLibraryPrefix = "osu.Game.Rulesets";
18+
19+
private readonly Dictionary<string, Ruleset> _rulesets = new();
20+
private readonly Dictionary<int, Ruleset> _rulesetsById = new();
21+
22+
public RulesetManager(ILogger<RulesetManager> logger)
23+
{
24+
_logger = logger;
25+
LoadOfficialRulesets();
26+
LoadFromDisk();
27+
}
28+
29+
private void AddRuleset(Ruleset ruleset)
30+
{
31+
if (!_rulesets.TryAdd(ruleset.ShortName, ruleset))
32+
{
33+
_logger.LogWarning("Ruleset with short name {shortName} already exists, skipping.", ruleset.ShortName);
34+
return;
35+
}
36+
37+
if (ruleset is not ILegacyRuleset legacyRuleset)
38+
{
39+
return;
40+
}
41+
42+
if (!_rulesetsById.TryAdd(legacyRuleset.LegacyID, ruleset))
43+
{
44+
_logger.LogWarning("Ruleset with ID {id} already exists, skipping.", legacyRuleset.LegacyID);
45+
}
46+
}
47+
48+
private void LoadOfficialRulesets()
49+
{
50+
foreach (Ruleset ruleset in (List<Ruleset>)
51+
[new OsuRuleset(), new TaikoRuleset(), new CatchRuleset(), new ManiaRuleset()])
52+
{
53+
AddRuleset(ruleset);
54+
}
55+
56+
_rulesets["catch"] = _rulesets["fruits"];
57+
}
58+
59+
private void LoadFromDisk()
60+
{
61+
if (!Directory.Exists(AppSettings.RulesetsPath))
62+
{
63+
return;
64+
}
65+
66+
string[] rulesets = Directory.GetFiles(AppSettings.RulesetsPath, $"{RulesetLibraryPrefix}.*.dll");
67+
68+
foreach (string ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
69+
{
70+
try
71+
{
72+
Assembly assembly = Assembly.LoadFrom(ruleset);
73+
Type? rulesetType = assembly.GetTypes()
74+
.FirstOrDefault(t => t.IsSubclassOf(typeof(Ruleset)) && !t.IsAbstract);
75+
76+
if (rulesetType == null)
77+
{
78+
continue;
79+
}
80+
81+
Ruleset instance = (Ruleset)Activator.CreateInstance(rulesetType)!;
82+
_logger.LogInformation("Loading ruleset {ruleset}", ruleset);
83+
AddRuleset(instance);
84+
}
85+
catch (Exception ex)
86+
{
87+
_logger.LogWarning("Failed to load ruleset from {ruleset}: {ex}", ruleset, ex);
88+
}
89+
}
90+
}
91+
92+
public Ruleset GetRuleset(int rulesetId)
93+
{
94+
return _rulesetsById.TryGetValue(rulesetId, out Ruleset? ruleset)
95+
? ruleset
96+
: throw new ArgumentException("Invalid ruleset ID provided.");
97+
}
98+
99+
public Ruleset GetRuleset(string shortName)
100+
{
101+
return _rulesets.TryGetValue(shortName, out Ruleset? ruleset)
102+
? ruleset
103+
: throw new ArgumentException("Invalid ruleset name provided.");
104+
}
105+
106+
public Ruleset GetRuleset(INeedsRuleset body, int defaultRulesetId = -1)
107+
{
108+
Ruleset ruleset;
109+
if (!string.IsNullOrEmpty(body.RulesetName))
110+
{
111+
ruleset = GetRuleset(body.RulesetName);
112+
}
113+
else if (body.RulesetId != null)
114+
{
115+
ruleset = GetRuleset(body.RulesetId.Value);
116+
}
117+
else if (defaultRulesetId >= -1)
118+
{
119+
ruleset = GetRuleset(defaultRulesetId);
120+
}
121+
else
122+
{
123+
throw new ArgumentException("No ruleset provided.");
124+
}
125+
126+
return ruleset;
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)