ItsDeltin / Overwatch-Script-To-Workshop

Converts scripts to Overwatch workshops.
207 stars 24 forks source link

Cannot cast ArrayElement() to Struct #431

Open mriswithe opened 1 year ago

mriswithe commented 1 year ago

I am trying to write a library for triggers (Causing a callback to get triggered if the player hits a/some button(s)). I think I am doing something wrong like unscrewing something with a carrot.

The error I get from the compiler (running in VSCode with OSTW (v3.5.1) is

An exception was thrown while translating to workshop.
System.InvalidCastException: Unable to cast object of type 'Deltin.Deltinteger.Elements.Element' to type 'Deltin.Deltinteger.Parse.IStructValue'.
   at Deltin.Deltinteger.Parse.StructInstance.AddObjectVariablesToAssigner(ToWorkshop toWorkshop, IWorkshopTree reference, VarIndexAssigner assigner) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Types/Structs/StructInstance.cs:line 166
   at Deltin.Deltinteger.Parse.ExpressionTree.ParseTree(ActionSet actionSet, Boolean expectingValue) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/ExpressionTree.cs:line 223
   at Deltin.Deltinteger.Parse.ExpressionTree.Parse(ActionSet actionSet) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/ExpressionTree.cs:line 165
   at Deltin.Deltinteger.Parse.IInvokeResult.GetParameterValuesAsWorkshop(ActionSet actionSet, IInvokeResult invokeResult) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Semantics/IInvokeInfo.cs:line 173
   at Deltin.Deltinteger.Parse.FunctionInvokeResult.Parse(ActionSet actionSet) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Semantics/IInvokeInfo.cs:line 215
   at Deltin.Deltinteger.Parse.Functions.Builder.Virtual.MacroContentBuilder.OnlyOne() in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Builder/Virtual/MacroContentBuilder.cs:line 30
   at Deltin.Deltinteger.Parse.Functions.Builder.Virtual.AbstractVirtualContentBuilder`1.Build() in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Builder/Virtual/AbstractVirtualContentBuilder.cs:line 27
   at Deltin.Deltinteger.Parse.Functions.Builder.User.MacroBuilder.CallMacroFunction(ActionSet actionSet, DefinedMethodInstance macro, MethodCall methodCall) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Builder/User/Macro.cs:line 35
   at Deltin.Deltinteger.Parse.IInvokeResult.GetParameterValuesAsWorkshop(ActionSet actionSet, IInvokeResult invokeResult) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Semantics/IInvokeInfo.cs:line 173
   at Deltin.Deltinteger.Parse.FunctionInvokeResult.Parse(ActionSet actionSet) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Semantics/IInvokeInfo.cs:line 215
   at Deltin.Deltinteger.Parse.Functions.Builder.Virtual.MacroContentBuilder.OnlyOne() in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Builder/Virtual/MacroContentBuilder.cs:line 30
   at Deltin.Deltinteger.Parse.Functions.Builder.Virtual.AbstractVirtualContentBuilder`1.Build() in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Builder/Virtual/AbstractVirtualContentBuilder.cs:line 27
   at Deltin.Deltinteger.Parse.Functions.Builder.User.MacroBuilder.CallMacroFunction(ActionSet actionSet, DefinedMethodInstance macro, MethodCall methodCall) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Functions/Builder/User/Macro.cs:line 35
   at Deltin.Deltinteger.Parse.TranslateRule.GetConditions(RuleAction ruleAction) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/TranslateRule.cs:line 70
   at Deltin.Deltinteger.Parse.TranslateRule..ctor(DeltinScript deltinScript, RuleAction ruleAction) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/TranslateRule.cs:line 41
   at Deltin.Deltinteger.Parse.DeltinScript.ToWorkshop(Func`2 addRules) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Translate.cs:line 260
   at Deltin.Deltinteger.Parse.DeltinScript..ctor(TranslateSettings translateSettings) in /home/runner/work/Overwatch-Script-To-Workshop/Overwatch-Script-To-Workshop/Deltinteger/Deltinteger/Parse/Translate.cs:line 57

The code that produces the error:


playervar Trigger[] _watch_triggers=[];

struct Trigger
{
public Button[] toHold;
public ()=>void toUse;
public Player player;
} 
Boolean triggerHeld(Any t): IsTrueForAll((<Trigger>t).toHold, IsButtonHeld(Button: ArrayElement())) ;
Boolean anyTriggersHeld(Trigger[] t): IsTrueForAny(t, triggerHeld(ArrayElement()));
Boolean has_triggers():_watch_triggers.Length!=0;
void add_trigger(in Player p, in Button[] buttonsToHold, in ()=>void cb) { 
    CreateHudText(HostPlayer(), $"Adding Trigger to {p}");
    Trigger t = { toHold: buttonsToHold,toUse: cb, player: p};
    p._watch_triggers.ModAppend(t); 
    CreateHudText(HostPlayer(), $"Done Trigger to {p}, {p._watch_triggers.Length}");
} 

rule: "Global crap"
{
    Wait(15);
    define p = HostPlayer();
    define l = ()=>BigMessage(p, "You did it! You pressed buttons!");
    l();
    add_trigger(p, [Button.Crouch, Button.Interact], l);
}

rule: 'Check players for triggers'
Event.OngoingPlayer
if(IsAlive()&&has_triggers())
if(anyTriggersHeld(_watch_triggers))
{
    Player p = EventPlayer();
    foreach(Trigger t in _watch_triggers) {
    if (IsTrueForAll(t.toHold, IsButtonHeld(Button: ArrayElement())))
    {
        t.toUse();
    }
}
}

If I comment out the call to if(anyTriggersHeld(_watch_triggers)), the error goes away, so it seems my problem is the anyTriggersHeld macro calling the triggerHeld macro?

Also, if my programming pattern is sad and wrong, please let me know. What I would like is to be able to create a rule when I create a class, but that doesn't seem to be a thing?

Thanks! Mriswithe

ItsDeltin commented 1 year ago

Ostw has almost zero restrictions when casting. This means that you can cast anything to anything, even if they are completely incompatible causing an exception when compiling. There isn't much of a reason for this anymore so I'll need to tighten up the casting restrictions, especially for structs. Casting is done very rarely, which is probably why I haven't run into this issue.

Additionally, instead of using the workshop functions (IsTrueForAll(array, condition), IsTrueForAny(array, condition), CountOf(array), etc.) instead use the methods inside an array type by doing: array.IsTrueForAll(v => condition), array.IsTrueForAny(v => condition), array.Length, etc. I will elaborate on why this is later.

Lets fix the code.


First, lets look at triggerHeld:

Boolean triggerHeld(Any t): IsTrueForAll((<Trigger>t).toHold, IsButtonHeld(Button: ArrayElement())) ;
// to
Boolean triggerHeld(Trigger t): t.toHold.IsTrueForAll(b => IsButtonHeld(EventPlayer(), b));

The t parameter is given the type Trigger instead of Any, removing the need for casting. Now the compiler won't crash, yay!


Second, anyTriggersHeld. The workshop has a limitation where you can't use an array filter/sort/map inside a filter/sort/map like this: x.FilteredArray(value => value.Inner.FilteredArray(...)). Luckily in this case there are only a few values at most so we can explicitly check each index manually. This won't cause too much of a performance hit because the workshop will short-circuit once a button is detected, so we are pretty much implementing IsTrueForAny manually. I did this by adding these definitions to the Trigger struct:

struct Trigger
{
    public Button[] toHold;
    public ()=>void toUse;
    public Player player;

    // If you expect the number of values in toHold to exceed 3, add another ', b(x)' right after the '2'.
    public Boolean isTriggerHeld(): b(0, b(1, b(2)));

    Boolean b(Number i, Boolean next = true): (i >= toHold.Length || (IsButtonHeld(EventPlayer(), toHold[i]) && next));
}
// ...
// then the new anyTriggersHeld looks like this:
Boolean anyTriggersHeld(Trigger[] triggers): triggers.IsTrueForAny(t => t.isTriggerHeld());

These changes are enough to make the project compile. Something to note is this line:

p._watch_triggers.ModAppend(t);

There is a bit of a trap here, because of the way that workshop handles appending values, you will want to change this to:

p._watch_triggers.ModAppend([t]);
// or this, which does the same thing. I think it looks nicer
p._watch_triggers += [t];

This is only required if a struct has an array type or lambda type inside it, and yours has both. OSTW will need to automatically wrap it without user intervention, so that's on me.

With all of this done, it now works correctly. Pressing crouch + interact will display "You did it! You pressed buttons!". Here is the fixed code:

Fixed code ```cs playervar Trigger[] _watch_triggers=[]; struct Trigger { public Button[] toHold; public ()=>void toUse; public Player player; public Boolean isTriggerHeld(): b(0, b(1, b(2))); Boolean b(Number i, Boolean next = true): (i >= toHold.Length || (IsButtonHeld(EventPlayer(), toHold[i]) && next)); } Boolean triggerHeld(Trigger t): t.toHold.IsTrueForAll(b => IsButtonHeld(EventPlayer(), b)); Boolean anyTriggersHeld(Trigger[] triggers): triggers.IsTrueForAny(t => t.isTriggerHeld()); Boolean has_triggers():_watch_triggers.Length!=0; void add_trigger(in Player p, in Button[] buttonsToHold, in ()=>void cb) { CreateHudText(HostPlayer(), $"Adding Trigger to {p}"); Trigger t = { toHold: buttonsToHold,toUse: cb, player: p}; p._watch_triggers += [t]; CreateHudText(HostPlayer(), $"Done Trigger to {p}, {p._watch_triggers.Length}"); } rule: "Global crap" { Wait(15); define p = HostPlayer(); define l = ()=>BigMessage(p, "You did it! You pressed buttons!"); l(); add_trigger(p, [Button.Crouch, Button.Interact], l); } rule: 'Check players for triggers' Event.OngoingPlayer if(IsAlive() && has_triggers()) if(anyTriggersHeld(_watch_triggers)) { Player p = EventPlayer(); foreach(Trigger t in _watch_triggers) { if (IsTrueForAll(t.toHold, IsButtonHeld(Button: ArrayElement()))) { t.toUse(); } } } ```

As for why you should use x.FilteredArray(...) instead of FilteredArray(x, ...), the latter is automatically generated from the workshop metadata and can't handle special cases required with structs. It still exists because the decompiler can't detect how workshop variables are used when decompiling workshop code, so it uses the legacy functions. Hopefully one day :)

Using ostw for so long made me subconsciously avoid a couple pitfalls that new users will run into. This has given me a few things I'll need to do.

  1. Make sure type casts are compatible and valid.
  2. Elaborate about the array methods on the wiki.
  3. Make sure legacy array operators are displayed as obsolete in vscode.
  4. Make ostw handle the struct appending tidbit on its own.
mriswithe commented 1 year ago

This is awesome feedback, I will see what I can do about trying to help with the doc side, I did a lot of going back and forth, documenting an entire language is not a one person task. I will verify this tonight once I get a chance, and see what I can try and spruce up in the wiki.

As for my goal, mildly flexible triggers like this, does this seem like a mildly performant path ? I don't know the "best" way to poll inputs as far as Overwatch is concerned. This just seemed like a simple starter task to start rolling up into more easy mode functions for my goals, make better/easier training/practice levels hehe.