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

feat: show sliderbreaks #7

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using Osu.Patcher.Hook.Patches.Relax;

namespace Osu.Patcher.Hook.Patches.ShowComboBreaks;

/// <summary>
/// Changes the following code in <c>osu.GameplayElements.HitObjectManager:vmethod_17(HitObject)</c>
/// to enable showing combo breaks on sliders using the data obtained through
/// <see cref="PatchTrackComboBreakingSliders" />.
/// <br /><br />
/// From:
/// <code><![CDATA[
/// if (increaseScoreType < (IncreaseScoreType)0) {
/// text2 = "hit0"; // string obfuscated
/// } else {
/// ...
/// }
/// ]]></code>
/// To:
/// <code><![CDATA[
/// if (PatchDrawSliderComboBreaks.IsObjectSliderComboBreaking(hitObject_arg1))
/// goto jmp_true:
/// if (increaseScoreType < (IncreaseScoreType)0) {
/// jmp_true:
/// text2 = "hit0"; // string obfuscated
/// } else {
/// ...
/// }
/// ]]></code>
/// </summary>
// [HarmonyPatch]
internal class PatchDrawSliderComboBreaks : BasePatch
{
[HarmonyTargetMethod]
private static MethodBase Target() =>
AllowRelaxDrawMisses.Target();

[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> Transpile(
IEnumerable<CodeInstruction> instructions,
ILGenerator generator)
{
// Right before the "increaseScoreType < 0"
var targetSignature = new[]
{
OpCodes.Ldc_I4_2,
OpCodes.Bgt_Un_S,
OpCodes.Ldc_I4,
OpCodes.Stloc_0,
OpCodes.Call,
OpCodes.Pop,
};

var elseJmpLabel = generator.DefineLabel();

var postInsertInstructions = InsertAfterSignature(
instructions,
targetSignature,
new[]
{
// Load the first argument onto the stack (Argument 0 is `this`)
new CodeInstruction(OpCodes.Ldarg_1),

// Call the method below to check w/ top of stack, result pushed to stack
CodeInstruction.Call(typeof(ShowComboBreaksUtil),
nameof(ShowComboBreaksUtil.IsHitObjectComboBreakingSlider)),

// Jmp to the if "true" block if result true
new CodeInstruction(OpCodes.Brtrue_S, elseJmpLabel),
}
);

// Find first inst of the if "true" block
// TODO: find this properly
postInsertInstructions.FirstOrDefault(i => i.opcode == OpCodes.Ldc_I4 && (int)i.operand == -0xFC85EE6)
?.labels.Add(elseJmpLabel);

return postInsertInstructions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Reflection;
using HarmonyLib;
using Osu.Patcher.Hook.Patches.Relax;
using Osu.Stubs;
using Osu.Stubs.Wrappers;

// ReSharper disable UnusedType.Global UnusedMember.Local InconsistentNaming

namespace Osu.Patcher.Hook.Patches.ShowComboBreaks;

// [HarmonyPatch]
internal class PatchTrackComboBreakingSliders : BasePatch
{
[HarmonyTargetMethod]
private static MethodBase Target() =>
AllowRelaxComboBreakSound.Target();

// Ruleset->CurrentScore->CurrentCombo
private static ushort GetCombo()
{
// Ruleset#CurrentScore
var fCurrentScore = AccessTools.Field("#=znsd474wIu4GAJ8swgEdrqaxwLN4O:#=zW5K7ouTMxMrX");
// Score#CurrentCombo
var fCurrentCombo = AccessTools.Field("#=zLBw8hg3V1k_gTYcAwmBw_YkrvWaEQzfUz_i_IrU=:#=zE49fcJU=");

return (ushort)fCurrentCombo.GetValue(fCurrentScore.GetValue(null));
}

[HarmonyPrefix]
private static void Before(
[HarmonyArgument(0)] int increaseScoreType,
[HarmonyArgument(1)] object hitObject)
{
// Check if slider & some sort of missed combo except for slider ends
if (increaseScoreType < 0 && SliderOsu.RuntimeType.IsInstanceOfType(hitObject))
{
Console.WriteLine($"IncreaseScoreHit pre: {increaseScoreType} {hitObject}");
// Console.WriteLine($"pre combo: {GetCombo()}");
// ShowComboBreaksUtil.ComboBreakSliders[hitObject.GetHashCode()] = true;

Notifications.ShowMessage("slider combo break", NotificationColor.Warning, 200);
}
}

// [HarmonyPostfix]
// private static void After(
// [HarmonyArgument(0)] int increaseScoreType,
// [HarmonyArgument(1)] object hitObject)
// {
// // Check if slider & some sort of missed combo except for slider ends
// if (increaseScoreType < 0 && SliderOsu.Class.IsInstanceOfType(hitObject))
// {
// Console.WriteLine($"post combo: {GetCombo()}");
// ShowComboBreaksUtil.ComboBreakSliders[hitObject.GetHashCode()] = true;
// }
// }

[HarmonyFinalizer]
private static Exception Finalizer(Exception __exception)
{
if (__exception != null)
{
Console.WriteLine("Exception while running patch " +
$"{nameof(PatchTrackComboBreakingSliders)}: {__exception}");
}

return null;

Check warning on line 68 in Osu.Patcher.Hook/Patches/ShowComboBreaks/PatchTrackComboBreakingSliders.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 68 in Osu.Patcher.Hook/Patches/ShowComboBreaks/PatchTrackComboBreakingSliders.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 68 in Osu.Patcher.Hook/Patches/ShowComboBreaks/PatchTrackComboBreakingSliders.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check warning on line 68 in Osu.Patcher.Hook/Patches/ShowComboBreaks/PatchTrackComboBreakingSliders.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}
}

// [HarmonyPatch]
// internal class PatchTrackComboBreakingSliders
// {
// [HarmonyTargetMethod]
// private static MethodBase Target()
// {
// // osu.GameModes.Play.Rulesets.Ruleset:OnIncreaseScoreHit(IncreaseScoreType, double hpIncrease, bool increaseCombo, HitObject)
// return AccessTools.DeclaredMethod("#=z04fOmc1I_BS0TV6TAo2QOUQvjceryuOcqoleWPg=:#=zBqtfszH0Yf2Z");
// }
//
// [HarmonyPostfix]
// private static void After(
// [HarmonyArgument(0)] object increaseScoreType,
// [HarmonyArgument(1)] double hpIncrease,
// [HarmonyArgument(2)] bool increaseCombo,
// [HarmonyArgument(3)] object hitObject)
// {
// Console.WriteLine($"OnIncreaseScoreHit: {increaseCombo} {hpIncrease} {increaseScoreType} {hitObject}");
// // Check if slider & some sort of missed combo except for slider ends
// if ((int)increaseScoreType < 0 && ShowComboBreaksUtil.TSliderOsu.IsInstanceOfType(hitObject))
// // Console.WriteLine($"OnIncreaseScoreHit: {increaseScoreType} {hitObject}");
// ShowComboBreaksUtil.ComboBreakSliders[hitObject.GetHashCode()] = true;
// }
// }
20 changes: 20 additions & 0 deletions Osu.Patcher.Hook/Patches/ShowComboBreaks/PatchTrackScoreClear.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Reflection;
using HarmonyLib;
using Osu.Stubs;

// ReSharper disable UnusedType.Global UnusedMember.Local

namespace Osu.Patcher.Hook.Patches.ShowComboBreaks;

/// <summary>
/// Clear our stored slider hit judgement results when the score is reset.
/// </summary>
[HarmonyPatch]
internal class PatchTrackScoreClear : BasePatch
{
[HarmonyTargetMethod]
private static MethodBase Target() => Ruleset.ResetScore.Reference;

[HarmonyPostfix]
private static void After() => ShowComboBreaksUtil.Reset();
}
65 changes: 65 additions & 0 deletions Osu.Patcher.Hook/Patches/ShowComboBreaks/ShowComboBreaksUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Osu.Stubs;

namespace Osu.Patcher.Hook.Patches.ShowComboBreaks;

internal static class ShowComboBreaksUtil
{
// Stored by SliderOsu#GetHashCode() -> "isComboBroken"
internal static readonly Dictionary<int, bool> ComboBreakSliders = new Dictionary<int, bool>();

// osu.GameplayElements.HitObjects.Osu.SliderOsu#sliderTicksHitStatus
private static readonly FieldInfo FSliderOsu_sliderTicksHitStatus;

// osu.GameplayElements.HitObjects.Osu.SliderOsu#sliderStartCircle
private static readonly FieldInfo FSliderOsu_sliderStartCircle;

// osu.GameplayElements.HitObjects.HitObject#IsHit
private static readonly FieldInfo FHitObject_IsHit;

static ShowComboBreaksUtil()
{
FSliderOsu_sliderTicksHitStatus = SliderOsu.RuntimeType.GetRuntimeFields()
.Single(f => f.FieldType.IsArray && f.FieldType.GetElementType() == typeof(bool));

FSliderOsu_sliderStartCircle =
AccessTools.Field("#=zoa4eC6Sw5rwIwh2Hbi$3M6X$Z6nv8UnYgxNpICPLicAP:#=z39rOeuDVskYOgRwpx3zjK74=");

FHitObject_IsHit = AccessTools.Field("#=zqwKEmiGIDb0qpLnR3aavM7g3al9jl960OQ==:#=zIDw7YQY=");

// HitCicleOsu = #=zOFC2jmTKji24ougWFWDM$GEqjzktzU4gcKFF$gYj32T9m7QUzQ==
}

public static void Reset()
{
Console.WriteLine("clearing");
ComboBreakSliders.Clear();
}

/// <summary>
/// Used by <see cref="PatchDrawSliderComboBreaks" /> in order to inject a call to this
/// to check whether a HitObject is an std slider that has caused broken combo.
/// </summary>
/// <param name="hitObject"></param>
/// <returns></returns>
public static bool IsHitObjectComboBreakingSlider(object hitObject)
{
if (SliderOsu.RuntimeType.IsInstanceOfType(hitObject))
{
var sliderTicks = (bool[])FSliderOsu_sliderTicksHitStatus.GetValue(hitObject);
var startCircle = FSliderOsu_sliderStartCircle.GetValue(hitObject);
var isStartCircleHit = FHitObject_IsHit.GetValue(startCircle);

Console.WriteLine(
$"sliderTicksHitStatus: {sliderTicks.Join()}; isStartCircleHit: {isStartCircleHit}");
}

return SliderOsu.RuntimeType.IsInstanceOfType(hitObject)
&& ComboBreakSliders.TryGetValue(hitObject.GetHashCode(), out var isBreaking)
&& isBreaking;
}
}
37 changes: 37 additions & 0 deletions Osu.Stubs/SliderOsu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using JetBrains.Annotations;
using Osu.Utils.Lazy;
using static System.Reflection.Emit.OpCodes;

namespace Osu.Stubs;

/// <summary>
/// Original: <c>osu.GameplayElements.HitObjects.Osu.SliderOsu</c>
/// b20240102.2: <c>#=zG_Fe30_qUmxpC_TyMiG8b0o7fXW0pNx67n$z1myOL9vw</c>
/// </summary>
[UsedImplicitly]
public static class SliderOsu
{
/// <summary>
/// The only constructor on this class
/// </summary>
private static readonly LazyMethod Constructor = new(
"SliderOsu#SliderOsu",
new[]
{
Ldc_I4_5,
Ldc_I4_5,
Ldc_I4_5,
Newobj,
Ldnull,
Newobj,
Stfld,
Ldarg_0,
Ldnull,
},
true
);

[UsedImplicitly]
public static Type RuntimeType => Constructor.Reference.DeclaringType!;
}
Loading