I found myself with a bit of a unique, thread-sensitive problem to solve today, and devised what I feel is a fairly clever and non-obvious method of solving it. I'm posting it here: (1) so other experienced scripters can look over it and see if it has any issues. I have a wealth of scripting experience, but I'm fairly new to Papyrus as a language, so it's possible that some aspects of what I've written will not behave in the way I'm expecting. And (2), so that it may serve as a resource to other modders dealing with a similar issue.
This solution addresses a situation in which multiple threads need to call a function in one script instance at the same time, and none of the calls may be ignored.
One of the earliest state tutorials most new Papyrus users probably see (certainly the first one I saw) is the one about the toggleable lever that uses states to block its activation while it's animating:
State Active Event OnActivate(ObjectReference akActivatorRef) GoToState("Inactive") ;do stuff, wait for animation to finish GoToState("Active") endEventendState State Inactive Event OnActivate(ObjectReference akActivatorRef) ;do nothing endEventendState
This obviously won't work in this situation, as we're ignoring function calls by other threads, and we're trying to have all function calls acknowledged. One solution would be to force the threads doing the calling to queue for the Active state. To illustrate, I have to swap out events for a function definition; let's say I have a function foo that does thread-sensitive work. To force the callers to queue for this function in the Active state, I could do something like this:
Scriptname ScriptA extends Quest ;or whatever you needState Active bool Function foo(int i) GoToState("Inactive") ;does some thread-sensitive work GoToState("Active") return true ;by returning true, I tell the caller that its request for this function was acknowledged endFunctionendState State Inactive bool Function foo(int i) return false ;by returning false, I tell the caller that its request for this function was denied endFunctionendState
And, in whatever is doing the calling:
Scriptname ScriptB extends ReferenceAlias ;or whatever you need ScriptA Property A Auto;...int j = somevalue ;an integer of interest to ScriptBwhile !A.foo(j) wait(0.1)endWhile
But I don't like this solution, because it involves busywait. Instead, I came up with this:
Scriptname ScriptA extends Quest ;or whatever you need int Property i = 0 Auto State Active Function foo(int i) GoToState("Inactive") ;does some thread-sensitive work GoToState("Active") endFunction Event OnBeginState() int temp = iAccumulator iAccumulator = 0 if(temp > 0) foo(temp) endif endEventendState State Inactive Function foo(int i) iAccumulator += i endFunctionendState
The first thread to gain access now gets exclusive access to the Active state. If the first thread unlocks the script by calling an external function while doing its thread-sensitive work in foo, then any other threads that get in will deposit the value they need added in iAccumulator. (This of course only works when an accumulator is appropriate - otherwise, you could push values onto an array of parameters. And if each caller is expecting a return value from foo, it of course goes without saying that this won't work.) When the first thread's instance of foo finishes its work and returns to the Active state, the OnBeginState() event fires and evaluates whether any other threads have deposited their values in the accumulator. If so, it runs foo again, making sure that all callers' values are evaluated.
Here's the trick, and the bit that makes it thread-safe: GoToState doesn't return until the OnEndState() event of the old state has finished firing and the OnBeginState() event of the new state has finished firing. So if thread 1's foo reaches GoToState("Active"), and other threads have deposited values in the accumulator, foo and GoToState begin recursing as a pair, with thread 1 still in control. Thread 1's foo reaches GoToState, which won't return until OnBeginState finishes. OnBeginState sees that there is a value in the accumulator and launches another instance of foo. The second instance of foo launches a second instance of GoToState, which waits to return until the next OnBeginState has finished. These two functions recurse like this until they reach a point where no external threads have deposited anything in the accumulator during the latest foo run, at which point OnBeginState doesn't launch another foo instance, which causes the previous GoToState to return, which causes its foo instance to return... all the way down the stack, until every foo and GoToState has been popped off, at which point thread 1 unlocks the script and the script is now waiting in the Active state for another caller to call foo.
Pretty nifty, yes? I just hope it works; it's 2AM here, and I haven't had a chance to test it thoroughly.