Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ReactorCredits #87

Merged
merged 13 commits into from
Aug 22, 2024
2 changes: 2 additions & 0 deletions Reactor.Example/ExamplePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public partial class ExamplePlugin : BasePlugin

public override void Load()
{
ReactorCredits.Register<ExamplePlugin>(ReactorCredits.AlwaysShow);

this.AddComponent<ExampleComponent>();

_helloStringName = CustomStringName.CreateAndRegister("Hello!");
Expand Down
21 changes: 21 additions & 0 deletions Reactor/Patches/Miscellaneous/PingTrackerPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using HarmonyLib;
using Reactor.Utilities;

namespace Reactor.Patches.Miscellaneous;

[HarmonyPatch(typeof(PingTracker), nameof(PingTracker.Update))]
internal static class PingTrackerPatch
{
[HarmonyPostfix]
[HarmonyPriority(Priority.Last)]
public static void Postfix(PingTracker __instance)
{
var extraText = ReactorCredits.GetText(ReactorCredits.Location.PingTracker);
if (extraText != null)
{
if (!__instance.text.text.EndsWith("\n", StringComparison.InvariantCulture)) __instance.text.text += "\n";
__instance.text.text += extraText;
}
}
}
17 changes: 10 additions & 7 deletions Reactor/Patches/ReactorVersionShower.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
Expand Down Expand Up @@ -89,20 +90,22 @@ internal static void Initialize()
}));
}

private static string ToStringWithoutBuild(Version version)
{
return $"{version.Major}.{version.Minor}.{version.Patch}{(version.PreRelease == null ? string.Empty : $"-{version.PreRelease}")}";
}

/// <summary>
/// Updates <see cref="Text"/> with reactor version and fires <see cref="TextUpdated"/>.
/// </summary>
public static void UpdateText()
{
if (Text == null) return;
Text.text = "Reactor " + ReactorPlugin.Version;
Text.text += "\nBepInEx " + ToStringWithoutBuild(Paths.BepInExVersion);
Text.text = "Reactor " + Version.Parse(ReactorPlugin.Version).WithoutBuild();
Text.text += "\nBepInEx " + Paths.BepInExVersion.WithoutBuild();
Text.text += "\nMods: " + IL2CPPChainloader.Instance.Plugins.Count;

var creditsText = ReactorCredits.GetText(ReactorCredits.Location.MainMenu);
if (creditsText != null)
{
Text.text += "\n" + creditsText;
}

TextUpdated?.Invoke(Text);
}

Expand Down
40 changes: 40 additions & 0 deletions Reactor/Utilities/Extensions/RichTextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace Reactor.Utilities.Extensions;

/// <summary>
/// Provides extension methods for TestMeshPro's Rich Text.
/// </summary>
internal static class RichTextExtensions
{
private static string Wrap(this string text, string tag)
{
return $"<{tag}>{text}</{tag}>";
}

private static string Wrap(this string text, string tag, string value)
{
return $"<{tag}={value}>{text}</{tag}>";
}

public static string Align(this string text, string value)
{
return text.Wrap("align", value);
}

public static string Color(this string text, string value)
{
return text.Wrap("color", value);
}

public static string Size(this string text, string value)
{
return text.Wrap("size", value);
}

public static string EscapeRichText(this string text)
{
return text
.Replace("<noparse>", string.Empty)
.Replace("</noparse>", string.Empty)
.Wrap("noparse");
}
}
19 changes: 19 additions & 0 deletions Reactor/Utilities/Extensions/VersionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using SemanticVersioning;

namespace Reactor.Utilities.Extensions;

/// <summary>
/// Provides extension methods for <see cref="SemanticVersioning.Version"/>.
/// </summary>
public static class VersionExtensions
{
/// <summary>
/// Gets the provided <paramref name="version"/> without the build string (everything after the + symbol like the commit hash is stripped).
/// </summary>
/// <param name="version">The <see cref="SemanticVersioning.Version"/>.</param>
/// <returns>The <paramref name="version"/> without build.</returns>
public static Version WithoutBuild(this Version version)
{
return new Version(version.Major, version.Minor, version.Patch, version.PreRelease);
}
}
123 changes: 123 additions & 0 deletions Reactor/Utilities/ReactorCredits.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Unity.IL2CPP;
using Reactor.Patches;
using Reactor.Utilities.Extensions;

namespace Reactor.Utilities;

/// <summary>
/// Provides a way for mods to show their version information in-game.
/// </summary>
public static class ReactorCredits
{
private readonly struct ModIdentifier(string name, string version, Func<Location, bool>? shouldShow, bool isPreRelease)
{
private const string NormalColor = "#fff";
private const string PreReleaseColor = "#f00";

public string Name => name;

public string Text { get; } = $"{name} {version}".EscapeRichText().Color(isPreRelease ? PreReleaseColor : NormalColor);

public bool ShouldShow(Location location)
{
return shouldShow == AlwaysShow || shouldShow(location);
}
}

private static readonly List<ModIdentifier> _modIdentifiers = [];

/// <summary>
/// Represents the location of where the credit is shown.
/// </summary>
public enum Location
{
/// <summary>
/// In the main menu under Reactor/BepInEx versions.
/// </summary>
MainMenu,

/// <summary>
/// During game under the ping tracker.
/// </summary>
PingTracker,
}

/// <summary>
/// A special value indicating a mod should always show.
/// </summary>
public const Func<Location, bool>? AlwaysShow = null;

/// <summary>
/// Registers a mod with the <see cref="ReactorCredits"/>, adding it to the list of mods that will be displayed.
/// </summary>
/// <param name="name">The user-friendly name of the mod. Can contain spaces or special characters.</param>
/// <param name="version">The version of the mod.</param>
/// <param name="isPreRelease">If this version is a development or beta version. If true, it will display the mod in red.</param>
/// <param name="shouldShow">
/// This function will be called every frame to determine if the mod should be displayed or not.
/// This function should return false if your mod is currently disabled or has no effect on gameplay at the time.
/// If you want the mod to be displayed at all times, you can set this parameter to <see cref="ReactorCredits.AlwaysShow"/>.
/// </param>
public static void Register(string name, string version, bool isPreRelease, Func<Location, bool>? shouldShow)
{
const int MaxLength = 60;

if (name.Length + version.Length > MaxLength)
{
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.");
return;
}

if (_modIdentifiers.Any(m => m.Name == name))
{
Error($"Mod \"{name}\" is already registered in {nameof(ReactorCredits)}.");
return;
}

_modIdentifiers.Add(new ModIdentifier(name, version, shouldShow, isPreRelease));

_modIdentifiers.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));

if (!isPreRelease)
{
Info($"Mod \"{name}\" registered in {nameof(ReactorCredits)} with version {version}.");
}
else
{
Warning($"Mod \"{name}\" registered in {nameof(ReactorCredits)} with DEVELOPMENT/BETA version {version}.");
}

ReactorVersionShower.UpdateText();
}

/// <summary>
/// Registers a mod with the <see cref="ReactorCredits"/>, adding it to the list of mods that will be displayed.
/// </summary>
/// <typeparam name="T">The BepInEx plugin type to get the name and version from.</typeparam>
/// <param name="shouldShow"><inheritdoc cref="Register(string,string,bool,System.Func{Location,bool})" path="/param[@name='shouldShow']"/></param>
public static void Register<T>(Func<Location, bool>? shouldShow) where T : BasePlugin
{
var pluginInfo = IL2CPPChainloader.Instance.Plugins.Values.SingleOrDefault(p => p.TypeName == typeof(T).FullName)
?? throw new ArgumentException("Couldn't find the metadata for the provided plugin type", nameof(T));

var metadata = pluginInfo.Metadata;

Register(metadata.Name, metadata.Version.WithoutBuild().Clean(), metadata.Version.IsPreRelease, shouldShow);
}

internal static string? GetText(Location location)
{
var modTexts = _modIdentifiers.Where(m => m.ShouldShow(location)).Select(m => m.Text).ToArray();
if (modTexts.Length == 0) return null;

return location switch
{
Location.MainMenu => string.Join('\n', modTexts),
Location.PingTracker => ("<space=3em>" + string.Join(", ", modTexts)).Size("50%").Align("center"),
_ => throw new ArgumentOutOfRangeException(nameof(location), location, null),
};
}
}
Loading