Skip to content

Commit e27a792

Browse files
Alexejherojs6pak
andauthored
Add ReactorCredits (#87)
Co-authored-by: js6pak <[email protected]>
1 parent 2c1eeaf commit e27a792

File tree

6 files changed

+215
-7
lines changed

6 files changed

+215
-7
lines changed

Reactor.Example/ExamplePlugin.cs

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public partial class ExamplePlugin : BasePlugin
2525

2626
public override void Load()
2727
{
28+
ReactorCredits.Register<ExamplePlugin>(ReactorCredits.AlwaysShow);
29+
2830
this.AddComponent<ExampleComponent>();
2931

3032
_helloStringName = CustomStringName.CreateAndRegister("Hello!");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using HarmonyLib;
3+
using Reactor.Utilities;
4+
5+
namespace Reactor.Patches.Miscellaneous;
6+
7+
[HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))]
8+
internal static class PingTrackerPatch
9+
{
10+
[HarmonyPostfix]
11+
[HarmonyPriority(Priority.Last)]
12+
public static void Postfix(PingTracker __instance)
13+
{
14+
var extraText = ReactorCredits.GetText(ReactorCredits.Location.PingTracker);
15+
if (extraText != null)
16+
{
17+
if (!__instance.text.text.EndsWith("\n", StringComparison.InvariantCulture)) __instance.text.text += "\n";
18+
__instance.text.text += extraText;
19+
}
20+
}
21+
}

Reactor/Patches/ReactorVersionShower.cs

+10-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using BepInEx;
33
using BepInEx.Unity.IL2CPP;
44
using HarmonyLib;
5+
using Reactor.Utilities;
56
using Reactor.Utilities.Extensions;
67
using TMPro;
78
using UnityEngine;
@@ -89,20 +90,22 @@ internal static void Initialize()
8990
}));
9091
}
9192

92-
private static string ToStringWithoutBuild(Version version)
93-
{
94-
return $"{version.Major}.{version.Minor}.{version.Patch}{(version.PreRelease == null ? string.Empty : $"-{version.PreRelease}")}";
95-
}
96-
9793
/// <summary>
9894
/// Updates <see cref="Text"/> with reactor version and fires <see cref="TextUpdated"/>.
9995
/// </summary>
10096
public static void UpdateText()
10197
{
10298
if (Text == null) return;
103-
Text.text = "Reactor " + ReactorPlugin.Version;
104-
Text.text += "\nBepInEx " + ToStringWithoutBuild(Paths.BepInExVersion);
99+
Text.text = "Reactor " + Version.Parse(ReactorPlugin.Version).WithoutBuild();
100+
Text.text += "\nBepInEx " + Paths.BepInExVersion.WithoutBuild();
105101
Text.text += "\nMods: " + IL2CPPChainloader.Instance.Plugins.Count;
102+
103+
var creditsText = ReactorCredits.GetText(ReactorCredits.Location.MainMenu);
104+
if (creditsText != null)
105+
{
106+
Text.text += "\n" + creditsText;
107+
}
108+
106109
TextUpdated?.Invoke(Text);
107110
}
108111

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace Reactor.Utilities.Extensions;
2+
3+
/// <summary>
4+
/// Provides extension methods for TestMeshPro's Rich Text.
5+
/// </summary>
6+
internal static class RichTextExtensions
7+
{
8+
private static string Wrap(this string text, string tag)
9+
{
10+
return $"<{tag}>{text}</{tag}>";
11+
}
12+
13+
private static string Wrap(this string text, string tag, string value)
14+
{
15+
return $"<{tag}={value}>{text}</{tag}>";
16+
}
17+
18+
public static string Align(this string text, string value)
19+
{
20+
return text.Wrap("align", value);
21+
}
22+
23+
public static string Color(this string text, string value)
24+
{
25+
return text.Wrap("color", value);
26+
}
27+
28+
public static string Size(this string text, string value)
29+
{
30+
return text.Wrap("size", value);
31+
}
32+
33+
public static string EscapeRichText(this string text)
34+
{
35+
return text
36+
.Replace("<noparse>", string.Empty)
37+
.Replace("</noparse>", string.Empty)
38+
.Wrap("noparse");
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using SemanticVersioning;
2+
3+
namespace Reactor.Utilities.Extensions;
4+
5+
/// <summary>
6+
/// Provides extension methods for <see cref="SemanticVersioning.Version"/>.
7+
/// </summary>
8+
public static class VersionExtensions
9+
{
10+
/// <summary>
11+
/// Gets the provided <paramref name="version"/> without the build string (everything after the + symbol like the commit hash is stripped).
12+
/// </summary>
13+
/// <param name="version">The <see cref="SemanticVersioning.Version"/>.</param>
14+
/// <returns>The <paramref name="version"/> without build.</returns>
15+
public static Version WithoutBuild(this Version version)
16+
{
17+
return new Version(version.Major, version.Minor, version.Patch, version.PreRelease);
18+
}
19+
}

Reactor/Utilities/ReactorCredits.cs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using BepInEx.Unity.IL2CPP;
5+
using Reactor.Patches;
6+
using Reactor.Utilities.Extensions;
7+
8+
namespace Reactor.Utilities;
9+
10+
/// <summary>
11+
/// Provides a way for mods to show their version information in-game.
12+
/// </summary>
13+
public static class ReactorCredits
14+
{
15+
private readonly struct ModIdentifier(string name, string version, Func<Location, bool>? shouldShow, bool isPreRelease)
16+
{
17+
private const string NormalColor = "#fff";
18+
private const string PreReleaseColor = "#f00";
19+
20+
public string Name => name;
21+
22+
public string Text { get; } = $"{name} {version}".EscapeRichText().Color(isPreRelease ? PreReleaseColor : NormalColor);
23+
24+
public bool ShouldShow(Location location)
25+
{
26+
return shouldShow == AlwaysShow || shouldShow(location);
27+
}
28+
}
29+
30+
private static readonly List<ModIdentifier> _modIdentifiers = [];
31+
32+
/// <summary>
33+
/// Represents the location of where the credit is shown.
34+
/// </summary>
35+
public enum Location
36+
{
37+
/// <summary>
38+
/// In the main menu under Reactor/BepInEx versions.
39+
/// </summary>
40+
MainMenu,
41+
42+
/// <summary>
43+
/// During game under the ping tracker.
44+
/// </summary>
45+
PingTracker,
46+
}
47+
48+
/// <summary>
49+
/// A special value indicating a mod should always show.
50+
/// </summary>
51+
public const Func<Location, bool>? AlwaysShow = null;
52+
53+
/// <summary>
54+
/// Registers a mod with the <see cref="ReactorCredits"/>, adding it to the list of mods that will be displayed.
55+
/// </summary>
56+
/// <param name="name">The user-friendly name of the mod. Can contain spaces or special characters.</param>
57+
/// <param name="version">The version of the mod.</param>
58+
/// <param name="isPreRelease">If this version is a development or beta version. If true, it will display the mod in red.</param>
59+
/// <param name="shouldShow">
60+
/// This function will be called every frame to determine if the mod should be displayed or not.
61+
/// This function should return false if your mod is currently disabled or has no effect on gameplay at the time.
62+
/// If you want the mod to be displayed at all times, you can set this parameter to <see cref="ReactorCredits.AlwaysShow"/>.
63+
/// </param>
64+
public static void Register(string name, string version, bool isPreRelease, Func<Location, bool>? shouldShow)
65+
{
66+
const int MaxLength = 60;
67+
68+
if (name.Length + version.Length > MaxLength)
69+
{
70+
Error($"Not registering mod \"{name}\" with version \"{version}\" in {nameof(ReactorCredits)} because the combined length of the mod name and version is greater than {MaxLength} characters.");
71+
return;
72+
}
73+
74+
if (_modIdentifiers.Any(m => m.Name == name))
75+
{
76+
Error($"Mod \"{name}\" is already registered in {nameof(ReactorCredits)}.");
77+
return;
78+
}
79+
80+
_modIdentifiers.Add(new ModIdentifier(name, version, shouldShow, isPreRelease));
81+
82+
_modIdentifiers.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
83+
84+
if (!isPreRelease)
85+
{
86+
Info($"Mod \"{name}\" registered in {nameof(ReactorCredits)} with version {version}.");
87+
}
88+
else
89+
{
90+
Warning($"Mod \"{name}\" registered in {nameof(ReactorCredits)} with DEVELOPMENT/BETA version {version}.");
91+
}
92+
93+
ReactorVersionShower.UpdateText();
94+
}
95+
96+
/// <summary>
97+
/// Registers a mod with the <see cref="ReactorCredits"/>, adding it to the list of mods that will be displayed.
98+
/// </summary>
99+
/// <typeparam name="T">The BepInEx plugin type to get the name and version from.</typeparam>
100+
/// <param name="shouldShow"><inheritdoc cref="Register(string,string,bool,System.Func{Location,bool})" path="/param[@name='shouldShow']"/></param>
101+
public static void Register<T>(Func<Location, bool>? shouldShow) where T : BasePlugin
102+
{
103+
var pluginInfo = IL2CPPChainloader.Instance.Plugins.Values.SingleOrDefault(p => p.TypeName == typeof(T).FullName)
104+
?? throw new ArgumentException("Couldn't find the metadata for the provided plugin type", nameof(T));
105+
106+
var metadata = pluginInfo.Metadata;
107+
108+
Register(metadata.Name, metadata.Version.WithoutBuild().Clean(), metadata.Version.IsPreRelease, shouldShow);
109+
}
110+
111+
internal static string? GetText(Location location)
112+
{
113+
var modTexts = _modIdentifiers.Where(m => m.ShouldShow(location)).Select(m => m.Text).ToArray();
114+
if (modTexts.Length == 0) return null;
115+
116+
return location switch
117+
{
118+
Location.MainMenu => string.Join('\n', modTexts),
119+
Location.PingTracker => ("<space=3em>" + string.Join(", ", modTexts)).Size("50%").Align("center"),
120+
_ => throw new ArgumentOutOfRangeException(nameof(location), location, null),
121+
};
122+
}
123+
}

0 commit comments

Comments
 (0)