When to use <transacts> specifier?

Hi,

I have a problem understanding when the specifier should be used. How do I know if the behavior inside a function can be rolled back or not?

For example, according to the docs:
The transacts effect is required any time a mutable variable (var) is written.

Does that mean it’s required when,
a. a mutable variable is declared inside the function?
b. any mutable variable is mutated (set) from inside the function? (even if it’s declared outside the function scope)

Overall I’m just quite confused :pensive:

1 Like

Basically, the transacts effect is needed whenever you want to update a class variable. WIthout this effect, the updated value in the var will only be visible within the current scope.

Consider this example:

My_Device := class(creative_device):
    var CurrentFrame : int = -1
    
    OnBegin<override>()<suspends>:void=
        loop:
            set CurrentFrame += 1
            Sleep(0.0)

    GetCurrentFrame():int=
        return CurrentFrame

If you were to use this code, and call GetCurrentFrame() from some other place at a later time, it will always be -1. Or maybe it will be different but it won’t be accurate/reliable (unsure, not tested). This can be solved with a “Setter method” which does that transaction for you. Since a lot of other API calls can’t be used under the <transacts> effect, Setter methods are something I use a lot.

Fixed version of the above example:

My_Device := class(creative_device):
    var CurrentFrame : int = -1
    TickFrameCounter()<transacts>:void = { set CurrentFrame += 1 }
    
    OnBegin<override>()<suspends>:void=
        loop:
            TickFrameCounter()
            Sleep(0.0) # wait for next server tick

    GetCurrentFrame():int=
        return CurrentFrame

Hope that makes sense!

Note that this is just a super basic example. Using a “setter method” is not always ideal, it’s kind of old-school - but in cases like this it might make sense because you just want to get the current frame number from all kinds of places but with this design you only need one counter.

So in summary, transacts effect is for making sure a class variable update is visible in every scope. If you’re wondering why this is even a thing, my educated guess is because that “making it visible in every scope” part is relatively expensive - so Verse requires us to specify it explicitly for performance reasons. So, as a final tip, try to use as little transacts functions as possible (i.e. combine as much code as possible into one transacts function, but try to keep those transacts functions as concise as possible). Probably not a huge deal unless you’re doing many transactions per frame though.

3 Likes

Are you sure about all of this ? I’m never using <transacts> and I still get to update tens of variables in seperated scopes even from <suspends> functions.

Also, you didn’t talk about rollback, for me the <transacts> allows you to rollback all the edits you’ve made if your function were ever gonna fail, and that’s why all your inner calls need to <transacts> too.

However, it is said in the doc that

The transacts effect is required any time a mutable variable (`var`) is written

So I wanna believe you, but go ahead and try that :

var Test:int = 0

PrintTestVal()<suspends>:void=
    Print("{Test}")

OnBegin<override>()<suspends> : void =
    set Test = 1
    spawn{PrintTestVal()}

Scopes are different, we even spawned a thread, still prints 1 though…

Yeah “scope” is the wrong term, apologies. What I meant was “entry-point”. If I recall correctly you’ll need it for variable synchronization in examples like:
a) Having a loop in OnBegin that waits forever until a game Event sets some logic to true;
b) Referencing another creative_prop in your code, and trying to get values from it during runtime;

Something like that. I’ve just taken to the habit of it, because it has been an issue for me.

As for no_rollback, thank you for sharing that concise tidbit - I’m still getting to grips with the speculative execution stuff. What you said made a LOT of sense to me though, so perhaps my understanding of <transacts> is not quite correct after all.

1 Like

Did you guys figure it out? Is transacts necessary any time a mutable data is written or only when you need the mutable data to avoid race conditions?

It’s only when you need a method to roll back - accessing and setting variables in methods without <transacts> like this works just fine:

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

my_device := class(creative_device):

    @editable
    Button:button_device = button_device{}

    var Pressed:logic = false

    OnBegin<override>()<suspends>:void=
        Button.InteractedWithEvent.Subscribe(OnButtonPressed)
        loop:
            Sleep(0.0)
            if (Pressed?):
                Print("Pressed!")
                break

    OnButtonPressed(Agent:agent):void=
        set Pressed = true

I had an issue which I thought might be caused by this. It turned out it wasn’t, instead, it was because I was setting a variable on a referenced device before its OnBegin. Even though I wasn’t doing anything in the OnBegin of the class, I believe the class was setting its variables to initial values at this time after I’d set them from another device.

While I was trying to replicate in an isolated example I created the code below, which does appear to show that transacts is not needed to change variables on referenced devices or have mutable variables in functions.

Output:

RefDevice.RefDeviceVariable: 1.00000000
RefDevice.RefDeviceVariable: 1.00000000