Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8
dotnet-version: 9

- name: Restore NuGet packages
run: dotnet restore
Expand Down
23 changes: 10 additions & 13 deletions NeuroInscryption/Automation/Cards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,21 @@ public async UniTask<Card> ChooseAddToDeck(CardChoicesSequencer choicer, Cancell
decision.AddAction(chooseAction);
decision.AddAction(rerollAction);

await decision.Send(ct);
ActionBase pickedAction = await decision.Send(ct);

if (chooseAction.Matches(decision, out SelectableCardData? cardData))
if (pickedAction == chooseAction)
{
SelectableCardData cardData = chooseAction.GetResult();
Plugin.Logger.LogDebug($"Picked {cardData.Identifier}");
return cardData.TargetCard;
}
if (rerollAction.Matches(decision))
{
AutomationManager.Assert(clover, $"Ended up in reroll path on non-rerollable choicer {choicer.GetType().Name} - schema enforcement fail?");
await NeuroMouse.MoveAndClick(clover, ct);
// choicer sets this back to false when ready for input again
await UniTask.WaitUntil(() => !rerollable!.choicesRerolled, cancellationToken: ct);
return await ChooseAddToDeck(choicer, ct);
}

decision.ThrowInvalid();
throw new InvalidOperationException($"Unreachable in {nameof(ChooseAddToDeck)} after ThrowInvalid");
AutomationManager.Assert(pickedAction == rerollAction, "Sanity: picked non-existent action");
AutomationManager.Assert(clover, $"Ended up in reroll path on non-rerollable choicer {choicer.GetType().Name} - schema enforcement fail?");
await NeuroMouse.MoveAndClick(clover, ct);
// choicer sets this back to false when ready for input again
await UniTask.WaitUntil(() => !rerollable!.choicesRerolled, cancellationToken: ct);
return await ChooseAddToDeck(choicer, ct);
}

public async UniTask UnflipCards(IEnumerable<SelectableCard> cards, CancellationToken ct = default)
Expand All @@ -87,7 +84,7 @@ public async UniTask UnflipCards(IEnumerable<SelectableCard> cards, Cancellation
/// <example>
/// <code>
/// Card cardToGloopBlurb = await Neuro.Cards.ChooseCardLoadout(cards,
/// description: new Description("choice_gloop_blurb", "Choose a card to gloop blurb. The card's immediate family will be murgled."),
/// decisionQuery: "Choose a card to gloop blurb. The card's immediate family will be murgled.",
/// additionalContext: new { MurgleInsensity = 5 }
/// );
/// </code>
Expand Down
18 changes: 3 additions & 15 deletions NeuroInscryption/Automation/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,11 @@ internal class ItemsContext<TData>(IEnumerable<TData> items, object? additionalC
/// Send a <see cref="Decision"/> that contains the given <paramref name="action"/> and <paramref name="additionalContext"/> and return the result from the response.
/// </summary>
/// <param name="decisionQuery">Query to send. This should describe what kind of choice is being made.</param>
/// <param name="state">Game state to send as context.</param>
/// <param name="additionalContext">
/// Any additional context to include in the state.
/// This can be an anonymous object, or you can pass a custom <see cref="ILoadoutContext"/>/<see cref="ICombatContext"/> instead.
/// </param>
/// <param name="loadout">
/// Whether to use the <see cref="LoadoutState"/> (if <see langword="true"/>)
/// or the <see cref="CombatState"/> (if <see langword="false"/>).
/// </param>
/// <returns>A result of type <typeparamref name="T"/> that validates the given <paramref name="action"/>'s schema.</returns>
public async UniTask<T> Choose<T>(ActionBase<T> action, string decisionQuery, IGameState state, CancellationToken ct = default,
object? additionalContext = null)
Expand All @@ -80,21 +77,12 @@ public async UniTask<T> Choose<T>(ActionBase<T> action, string decisionQuery, IG

await decision.Send(ct);

if (!action.Matches(decision, out T? result)) decision.ThrowInvalid();
T result = action.GetResult();

Plugin.Logger.LogDebug($"Picked {(result is IIdentifiable id ? id.Identifier : result.ToString())}");
Plugin.Logger.LogDebug($"Picked {(result is IIdentifiable id ? id.Identifier : result?.ToString())}");
return result;
}

public async UniTask<T> Choose<T>(ActionBase<T> action, string decisionQuery, CancellationToken ct = default,
object? additionalContext = null, bool useLoadoutState = true)
{
return await Choose(action, decisionQuery,
useLoadoutState ? LoadoutState.Calculate() : CombatState.Calculate(),
ct, additionalContext
);
}

public async UniTask<TData> ChooseItem<TData>(IReadOnlyCollection<TData> items, string decisionQuery, IGameState state,
CancellationToken ct = default, object? additionalContext = null)
where TData : class, IIdentifiable
Expand Down
16 changes: 8 additions & 8 deletions NeuroInscryption/Automation/Game/Combat/CardBattleFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,27 @@ protected override async UniTask ExecuteAsync(IEnumerator enumerator, Cancellati
decision.AddAction(acceptSurrenderAction);
decision.AddAction(endTurnAction);

await decision.Send(ct);
ActionBase pickedAction = await decision.Send(ct);

InteractableBase? target = null;
if (playCardAction.Matches(decision, out PlayableCardData? cardData))
if (pickedAction == playCardAction)
{
target = cardData.TargetCard;
target = playCardAction.GetResult().TargetCard;
}
else if (useItemAction.Matches(decision, out ItemData? itemData))
else if (pickedAction == useItemAction)
{
target = itemData.Slot!;
target = useItemAction.GetResult().Slot!;
}
else if (acceptSurrenderAction.Matches(decision))
else if (pickedAction == acceptSurrenderAction)
{
await UniTask.Delay(500, cancellationToken: ct); // branch takes time to extend, so the mouse aim gets thrown off
target = LeshyAnimationController.Instance.oliveBranchInteractable;
}
else if (endTurnAction.Matches(decision))
else if (pickedAction == endTurnAction)
{
target = ((BoardManager3D)BoardManager.Instance).Bell;
}
if (target == null) decision.ThrowInvalid();
AutomationManager.Assert(target != null, "Did not pick valid target");
await NeuroMouse.MoveAndClick(target, ct, msDelayBeforeClick: 500);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected override async UniTask ExecuteAsync(IEnumerator _, CancellationToken c

await decision.Send(ct);

if (!chooseAction.Matches(decision, out IEnumerable<SelectableCardData>? chosenCards)) decision.ThrowInvalid();
IEnumerable<SelectableCardData> chosenCards = chooseAction.GetResult();

foreach (SelectableCardData card in chosenCards)
{
Expand Down
2 changes: 1 addition & 1 deletion NeuroInscryption/Automation/Game/Map/TraderFlow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ protected override async UniTask ExecuteAsync(IEnumerator target, CancellationTo

await decision.Send(ct);

if (!chooseAction.Matches(decision, out IEnumerable<SelectableCardData>? chosenCards)) decision.ThrowInvalid();
IEnumerable<SelectableCardData> chosenCards = chooseAction.GetResult();

foreach (SelectableCardData card in chosenCards)
{
Expand Down
12 changes: 6 additions & 6 deletions NeuroInscryption/Automation/Loadout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ namespace NeuroInscryption.Automation;
[SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")]
internal sealed class Loadout
{
public record struct InteractableOption(BasicAction Action, InteractableBase Interactable)
public record struct InteractableOption(EmptyAction Action, InteractableBase Interactable)
{
public static implicit operator InteractableOption((string ActionName, InteractableBase Interactable) tuple)
{
return new InteractableOption(new BasicAction(tuple.ActionName), tuple.Interactable);
return new InteractableOption(new EmptyAction(tuple.ActionName), tuple.Interactable);
}

public static implicit operator InteractableOption((BasicAction Action, InteractableBase Interactable) tuple)
public static implicit operator InteractableOption((EmptyAction Action, InteractableBase Interactable) tuple)
{
return new InteractableOption(tuple.Action, tuple.Interactable);
}
Expand All @@ -39,10 +39,10 @@ public async UniTask<InteractableBase> ChooseInteractable(InteractableOption[] o
decision.AddAction(option.Action);
}

await decision.Send(ct);
ActionBase pickedAction = await decision.Send(ct);

InteractableOption chosenOption = Array.Find(options, opt => opt.Action.Matches(decision));
if (chosenOption == default) decision.ThrowInvalid();
InteractableOption chosenOption = Array.Find(options, opt => opt.Action == pickedAction);
AutomationManager.Assert(chosenOption != default, "Sanity: picked non-existent option");

Plugin.Logger.LogDebug($"Picked {chosenOption.Interactable} ({chosenOption.Action.Name})");

Expand Down
14 changes: 6 additions & 8 deletions NeuroInscryption/Automation/Trade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using DiskCardGame;
using NeuroInscryption.Automation.API;
using NeuroInscryption.Communication;
using NeuroInscryption.Communication.Actions;
using NeuroInscryption.Communication.Actions.Map;
Expand Down Expand Up @@ -63,20 +64,17 @@ async UniTask enumerable(IAsyncWriter<TrapperWareData> writer, CancellationToken
decision.AddAction(buyAction);
decision.AddAction(leaveAction);

await decision.Send(token);
ActionBase pickedAction = await decision.Send(token);

if (buyAction.Matches(decision, out TrapperWareData? trapperWare))
if (pickedAction == buyAction)
{
await writer.YieldAsync(trapperWare);
await writer.YieldAsync(buyAction.GetResult());
}
else if (leaveAction.Matches(decision))
else // picked leave
{
AutomationManager.Assert(pickedAction == leaveAction, "Sanity: picked a non-existent action");
return;
}
else
{
decision.ThrowInvalid();
}
}
}
}
Expand Down
34 changes: 26 additions & 8 deletions NeuroInscryption/Communication/Actions/ActionBase.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
using System;
using System.Diagnostics.CodeAnalysis;
using NeuroInscryption.Automation.API;
using NeuroSdk.Json;
using Newtonsoft.Json.Linq;

namespace NeuroInscryption.Communication.Actions;

public abstract class ActionBase(string name, string description = "") : IAction
public abstract class ActionBase(string name, string description = "")
{
/// <summary>If <see langword="false"/>, will not be registered as a possible action in the <see cref="NeuroSdk.Actions.ActionWindow"/>.</summary>
public virtual bool IsApplicable => true;
public virtual string Name => name;
public virtual string Description => description;
public abstract JsonSchema? GetDataSchema();

public bool Matches(Decision decision)
=> decision.Result?.PickedAction == Name;
public virtual bool Validate(Decision decision)
=> decision.RawResponse?.PickedAction == Name;
}

public abstract class ActionBase<T>(string name, string description = "") : ActionBase(name, description), IAction<T>
public abstract class ActionBase<T>(string name, string description = "") : ActionBase(name, description)
{
public abstract bool TryDeserialize(JToken? data, [NotNullWhen(true)] out T? response);
protected abstract bool TryDeserialize(JToken? data, [NotNullWhen(true)] out T? response);
private bool _hasResult; // used to just be a null check on _result but would break if T is a nullable reference type
private T? _result;
/// <summary>
/// Call this <b>only</b> after adding this action to a decision and awaiting it.
/// </summary>
public T GetResult()
{
AutomationManager.Assert(_hasResult, $"Tried to get result for action {Name} before result was set");
return _result!;
}

public bool Matches(Decision decision, [NotNullWhen(true)] out T? response)
public override bool Validate(Decision decision)
{
response = default;
return Matches(decision) && TryDeserialize(decision.Result?.Data, out response);
bool valid = base.Validate(decision) && TryDeserialize(decision.RawResponse?.Data, out _result);
if (valid)
{
if (_hasResult)
throw new InvalidOperationException($"Do not reuse action objects - tried to set result on action {Name} but it's already been validated before");
_hasResult = true;
}
return valid;
}
}
2 changes: 1 addition & 1 deletion NeuroInscryption/Communication/Actions/ChooseAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public sealed class ChooseAction<T>(IEnumerable<T> items, string name = ActionSt
{
public override bool IsApplicable => items.Any();

public override bool TryDeserialize(JToken? data, [NotNullWhen(true)] out T? response)
protected override bool TryDeserialize(JToken? data, [NotNullWhen(true)] out T? response)
{
string? identifier = data?.Value<string>("choice");
if (identifier == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public override JsonSchema GetDataSchema()
};
}

public override bool TryDeserialize(JToken? data, [NotNullWhen(true)] out IEnumerable<TSelectable>? response)
protected override bool TryDeserialize(JToken? data, [NotNullWhen(true)] out IEnumerable<TSelectable>? response)
{
IEnumerable<string?>? cardIdentifiers = data?["choices"]?.Values<string>();
if (cardIdentifiers == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace NeuroInscryption.Communication.Actions.Combat;

public sealed class AcceptSurrenderAction(bool? isOpponentSurrendering) : BasicAction(ActionStrings.AcceptSurrender)
public sealed class AcceptSurrenderAction(bool? isOpponentSurrendering) : EmptyAction(ActionStrings.AcceptSurrender)
{
public override bool IsApplicable => isOpponentSurrendering == true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public override JsonSchema GetDataSchema()
};
}

public override bool TryDeserialize(JToken? data, out bool drawFromSideDeck)
protected override bool TryDeserialize(JToken? data, out bool drawFromSideDeck)
{
string? strResponse = data?.Value<string>("deck");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace NeuroInscryption.Communication.Actions.Combat;

public sealed class EndTurnAction() : BasicAction(ActionStrings.EndTurn);
public sealed class EndTurnAction() : EmptyAction(ActionStrings.EndTurn);
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public override JsonSchema GetDataSchema()
};
}

public override bool TryDeserialize(JToken? data, out int response)
protected override bool TryDeserialize(JToken? data, out int response)
{
int? result = data?.Value<int>("lane");
if (!result.HasValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override JsonSchema GetDataSchema()
};
}

public override bool TryDeserialize(JToken? data, [NotNullWhen(true)] out IEnumerable<PlayableCardData>? response)
protected override bool TryDeserialize(JToken? data, [NotNullWhen(true)] out IEnumerable<PlayableCardData>? response)
{
IEnumerable<string?>? cardIdentifiers = data?.Value<string>("sacrifices")?.Split([", "], StringSplitOptions.None);
if (cardIdentifiers == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace NeuroInscryption.Communication.Actions;

/// <summary>Generic choosable action with no data.</summary>
/// <param name="actionName">The unique identifier for this action, to distinguish it from other potential actions.</param>
public class BasicAction(string actionName) : ActionBase(actionName)
public class EmptyAction(string actionName, string description = "") : ActionBase(actionName, description)
{
public override JsonSchema GetDataSchema() => QJS.ConstNull;
}
17 changes: 0 additions & 17 deletions NeuroInscryption/Communication/Actions/IAction.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public override JsonSchema GetDataSchema()
};
}

public override bool TryDeserialize(JToken? data, out int response)
protected override bool TryDeserialize(JToken? data, out int response)
{
int boulder = data?.Value<int>("boulder") ?? -1;
if (boulder is < 1 or > 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public override JsonSchema GetDataSchema()
};
}

public override bool TryDeserialize(JToken? data, out Response response)
protected override bool TryDeserialize(JToken? data, out Response response)
{
string? tribe = data?.Value<string>("tribe");
string? sigil = data?.Value<string>("sigil");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace NeuroInscryption.Communication.Actions.Map;

public sealed class CampfireSecondUpgradeAction() : ActionBase<bool>(ActionStrings.CampfireSecondUpgrade)
{
public override bool TryDeserialize(JToken? data, out bool response)
protected override bool TryDeserialize(JToken? data, out bool response)
{
bool? value = data?.Value<bool>("wantSecondUpgrade");
response = value ?? false;
Expand Down
2 changes: 1 addition & 1 deletion NeuroInscryption/Communication/Actions/Map/LeaveAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace NeuroInscryption.Communication.Actions.Map;

public sealed class LeaveAction() : BasicAction(ActionStrings.Leave);
public sealed class LeaveAction() : EmptyAction(ActionStrings.Leave);
2 changes: 1 addition & 1 deletion NeuroInscryption/Communication/Actions/Map/RerollAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace NeuroInscryption.Communication.Actions.Map;

public sealed class RerollAction(MainInputInteractable clover) : BasicAction(ActionStrings.Reroll)
public sealed class RerollAction(MainInputInteractable clover) : EmptyAction(ActionStrings.Reroll)
{
public override bool IsApplicable => clover && clover.Enabled && clover.gameObject.activeSelf;
}
Loading
Loading