Skip to content
Open
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
10 changes: 9 additions & 1 deletion Abstracts/CustomAncientModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public CustomAncientModel(bool autoAdd = true, bool logDialogueLoad = false)
public virtual List<(string, string)>? Localization => null;

/// <summary>
/// Suggested to check act.ActNumber == 2 or 3.
/// Suggested to check act.ActNumber == 1 or 2 or 3.
///
/// If you want the Ancient to spawn in Act 1, additionally override IsAct1Ancient and return true.
///
/// If you are overriding ShouldForceSpawn, you should override this and return false.
/// </summary>
Expand All @@ -39,6 +41,12 @@ public CustomAncientModel(bool autoAdd = true, bool logDialogueLoad = false)
/// <returns></returns>
public virtual bool ShouldForceSpawn(ActModel act, AncientEventModel? rngChosenAncient) => false;

/// <summary>
/// Return true if this should be an Act 1 Ancient.
/// </summary>
/// <returns></returns>
public virtual bool IsAct1Ancient() => false;

/// <summary>
/// Set up a new OptionPools with 1, 2, or 3 pools using MakePool for each pool.
/// If there is 1 pool, all ancient options will be chosen randomly from this pool.
Expand Down
224 changes: 224 additions & 0 deletions Patches/Content/ContentPatches.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
using BaseLib.Abstracts;
using BaseLib.Extensions;
Expand All @@ -10,6 +12,7 @@
using MegaCrit.Sts2.Core.Modding;
using MegaCrit.Sts2.Core.Models;
using MegaCrit.Sts2.Core.Models.Acts;
using MegaCrit.Sts2.Core.Models.Events;
using MegaCrit.Sts2.Core.Models.Relics;
using MegaCrit.Sts2.Core.Random;
using MegaCrit.Sts2.Core.Rooms;
Expand Down Expand Up @@ -290,6 +293,227 @@ static void ForceAncientToSpawn(ActModel __instance)
}
}

[HarmonyPatch]
static class AddAct1AncientsToModelPool
{
[HarmonyTargetMethods]
public static IEnumerable<MethodBase> TargetMethods()
{
MethodInfo abstractMethod = AccessTools.DeclaredMethod(
typeof(ActModel),
nameof(ActModel.GetUnlockedAncients),
[typeof(UnlockState)]);

return AccessTools.AllTypes()
.Where(type =>
type != typeof(ActModel) &&
typeof(ActModel).IsAssignableFrom(type))
.Select(type => AccessTools.Method(
type,
nameof(ActModel.GetUnlockedAncients),
[typeof(UnlockState)]))
.Where(method =>
method is not null &&
!method.IsAbstract &&
method.GetBaseDefinition() == abstractMethod)
.Distinct();
}

[HarmonyPostfix]
private static IEnumerable<AncientEventModel> Postfix(
IEnumerable<AncientEventModel> ancients,
ActModel __instance)
{
if (__instance.ActNumber() != 1)
return ancients;

List<AncientEventModel> result = ancients.ToList();

foreach (CustomAncientModel ancient in CustomContentDictionary.CustomAncients)
{
if (ancient.IsAct1Ancient() && ancient.IsValidForAct(__instance))
{
result.Add(ancient);
}
}

return result;
}
}

/// <summary>
/// In base game the check for Neow is hardcoded. This patch is needed to properly set up the run.
/// </summary>
[HarmonyPatch]
public static class AddAct1Ancients_NeowChecks
{
private static readonly MethodInfo IsNeowLikeMethod =
AccessTools.Method(
typeof(AddAct1Ancients_NeowChecks),
nameof(IsNeowLike))
?? throw new MissingMethodException(
nameof(AddAct1Ancients_NeowChecks),
nameof(IsNeowLike));

/// <summary>
/// BeforeEventStarted is async, so its actual body is inside the
/// compiler-generated IAsyncStateMachine.MoveNext method.
/// </summary>
[HarmonyTargetMethod]
private static MethodBase TargetMethod()
{
MethodInfo beforeEventStarted =
AccessTools.Method(
typeof(AncientEventModel),
"BeforeEventStarted",
[typeof(bool)])
?? throw new MissingMethodException(
typeof(AncientEventModel).FullName,
"BeforeEventStarted");

AsyncStateMachineAttribute stateMachineAttribute =
beforeEventStarted.GetCustomAttribute<AsyncStateMachineAttribute>()
?? throw new InvalidOperationException(
"BeforeEventStarted does not have an AsyncStateMachineAttribute.");

return AccessTools.Method(
stateMachineAttribute.StateMachineType,
nameof(IAsyncStateMachine.MoveNext))
?? throw new MissingMethodException(
stateMachineAttribute.StateMachineType.FullName,
nameof(IAsyncStateMachine.MoveNext));
}

[HarmonyTranspiler]
private static IEnumerable<CodeInstruction> Transpiler(
IEnumerable<CodeInstruction> instructions)
{
int replacements = 0;

foreach (CodeInstruction instruction in instructions)
{
if (instruction.opcode == OpCodes.Isinst &&
instruction.operand is Type checkedType &&
checkedType == typeof(Neow))
{
/*
* Original:
*
* ancient
* isinst Neow
*
* Replacement:
*
* ancient
* dup
* isinst Neow
* call IsNeowLike
*
* Stack:
*
* ancient
* ancient, ancient
* ancient, originalResult
* extendedResult
*
* The original `isinst Neow` instruction remains unchanged,
* so transpilers running after this one can still find it.
*/
var duplicateInstruction =
new CodeInstruction(OpCodes.Dup);

var extendResultInstruction =
new CodeInstruction(
OpCodes.Call,
IsNeowLikeMethod);

/*
* A branch targeting the original isinst must execute the
* inserted dup first.
*/
instruction.MoveLabelsTo(duplicateInstruction);

/*
* Keep the complete inserted sequence inside the same
* exception region:
*
* - beginning boundaries go on the first instruction;
* - ending boundaries go on the final instruction.
*/
foreach (ExceptionBlock block in instruction.ExtractBlocks())
{
if (block.blockType == ExceptionBlockType.EndExceptionBlock)
{
extendResultInstruction.blocks.Add(block);
}
else
{
duplicateInstruction.blocks.Add(block);
}
}

yield return duplicateInstruction;
yield return instruction;
yield return extendResultInstruction;

replacements++;
continue;
}

yield return instruction;
}

if (replacements != 2)
{
throw new InvalidOperationException(
$"Expected to extend 2 Neow checks in " +
$"{nameof(AncientEventModel)}.BeforeEventStarted, " +
$"but extended {replacements}. The game method or another " +
$"transpiler may have changed.");
}
}

/// <summary>
/// Extends the result of the original `isinst Neow` instruction.
/// A non-null original result is retained. This also composes with
/// another transpiler that replaces the preserved isinst with a
/// compatible object-to-object check.
/// </summary>
private static object? IsNeowLike(
object? ancient,
object? originalResult)
{
return originalResult
?? (CustomContentDictionary.CustomAncients.Any(customAncient =>
customAncient.IsAct1Ancient()
&& customAncient.GetType().IsInstanceOfType(ancient))
? ancient
: null);
}
}

/// <summary>
/// Ensures that Neow is always used as the Act 1 Ancient in Custom and Daily runs.
///
/// Earlier patches cannot access the RunState, and their owner is always null, so this behavior must be applied by
/// patching RunManager.GenerateRooms.
///
/// If the run has at least one modifier, this patch overrides the Act 1 Ancient with Neow.
/// </summary>
[HarmonyPatch(typeof(RunManager), nameof(RunManager.GenerateRooms))]
class AddAct1Ancients_AlwaysUseNeowInCustomRuns
{
[HarmonyPostfix]
private static void Postfix(RunManager __instance)
{
if (__instance.State is { Modifiers.Count: > 0, Acts.Count: > 0 } &&
__instance.State.Acts[0].Ancient is not Neow)
{
__instance.State.Acts[0]._rooms.Ancient = ModelDb.Event<Neow>();
}
}
}

[HarmonyPatch(typeof(ModelDb), nameof(ModelDb.AllSharedEvents), MethodType.Getter)]
class CustomSharedEvents
{
Expand Down