Difference between revisions of "Creating Multithreaded Skyrim Mods Part 3 - Callbacks"

Updated Thread Manager section.
imported>Chesko
(Updated Thread section.)
imported>Chesko
(Updated Thread Manager section.)
Line 101: Line 101:
;Called from Event OnGuardPlacement
;Called from Event OnGuardPlacement
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
function MoveGuardMarkerNearPlayer(ObjectReference akMarker)
;Move the marker away from the player a random distance and direction in 75.0 game unit increments
;Some difficult calculations, etc
Actor player = Game.GetPlayer()
Float A = player.GetAngleZ() + (Utility.RandomInt(1, 24) * 15.0)
Float YDist = math.Sin(A)
Float XDist = math.Cos(A)
XDist *= (Utility.RandomInt(1, 5) * 75.0)
YDist *= (Utility.RandomInt(1, 5) * 75.0)
akMarker.MoveTo(player, XDist, YDist)
EndFunction
EndFunction
   
   
Line 187: Line 180:
<source lang="papyrus">
<source lang="papyrus">
scriptname GuardPlacementThreadManager extends Quest
scriptname GuardPlacementThreadManager extends Quest
 
Quest property GuardPlacementQuest auto
Quest property GuardPlacementQuest auto
{The name of the thread management quest.}
{The name of the thread management quest.}
 
Activator property GuardPlacementFutureActivator auto
{Our Future object.}
 
ObjectReference property GuardPlacementFutureAnchor auto
{Our Future Anchor object reference.}
 
Static property XMarker auto
Static property XMarker auto
{Something a thread needs; our threads don't declare their own properties.}
{Tedious to define properties in the threads and hook up in CK over and over, so define things we need here. MoveGuardMarkerNearPlayer() needs XMarkers.}


GuardPlacementThread01 thread01
GuardPlacementThread01 thread01
Line 205: Line 192:
GuardPlacementThread09 thread09
GuardPlacementThread09 thread09
GuardPlacementThread10 thread10
GuardPlacementThread10 thread10
 
Event OnInit()
Event OnInit()
     ;Register for the event that will start all threads
     ;Register for the event that will start all threads
Line 218: Line 205:
     thread10 = GuardPlacementQuest as GuardPlacementThread10
     thread10 = GuardPlacementQuest as GuardPlacementThread10
EndEvent
EndEvent
 
;The 'public-facing' function that our MagicEffect script will interact with.
;The 'public-facing' function that our MagicEffect script will interact with.
ObjectReference function PlaceConjuredGuardAsync(ActorBase akGuard)
function PlaceConjuredGuardAsync(ActorBase akGuard)
        int i = 0
    if !thread01.queued()
ObjectReference future
        debug.trace("[Callback] Selected thread01")
while !future
        thread01.get_async(akGuard, XMarker)
if !thread01.queued()
    elseif !thread02.queued()
future = thread01.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
        debug.trace("[Callback] Selected thread02")
elseif !thread02.queued()
thread02.get_async(akGuard, XMarker)
future = thread02.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
    ;...and so on
...
    elseif !thread09.queued()
elseif !thread09.queued()
        debug.trace("[Callback] Selected thread09")
future = thread09.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
        thread09.get_async(akGuard, XMarker)
elseif !thread10.queued()
    elseif !thread10.queued()
future = thread10.get_async(GuardPlacementFutureActivator, GuardPlacementFutureAnchor, akGuard, XMarker)
        debug.trace("[Callback] Selected thread10")
else
        thread10.get_async(akGuard, XMarker)
;All threads are queued; start all threads, wait, and try again.
    else
                        wait_all()
;All threads are queued; start all threads, wait, and try again.
endif
        wait_all()
endWhile
        PlaceConjuredGuardAsync(akGuard)
 
endif
return future
endFunction
endFunction
 
function wait_all()
function wait_all()
     RaiseEvent_OnGuardPlacement()
     RaiseEvent_OnGuardPlacement()
Line 267: Line 253:
endFunction
endFunction


;A helper function that can avert permanent thread failure if something goes wrong
;Create the ModEvent that will start this thread
function TryToUnlockThread(ObjectReference akFuture)
    bool success = false
    if thread01.has_future(akFuture)
        success = thread01.force_unlock()
    elseif thread02.has_future(akFuture)
        success = thread02.force_unlock()
    ;...and so on
    elseif thread09.has_future(akFuture)
        success = thread09.force_unlock()
    elseif thread10.has_future(akFuture)
        success = thread10.force_unlock()
    endif
   
    if !success
        debug.trace("Error: A thread has encountered an error and has become unresponsive.")
    else
        debug.trace("Warning: An unresponsive thread was successfully unlocked.")
    endif
endFunction
 
;Create the ModEvent that will start all threads
function RaiseEvent_OnGuardPlacement()
function RaiseEvent_OnGuardPlacement()
     int handle = ModEvent.Create("MyMod_OnGuardPlacement")
     int handle = ModEvent.Create("MyMod_OnGuardPlacement")
Line 300: Line 265:




The PlaceConjuredGuardAsync() function handles making sure that our work gets delegated to an available thread. The function then returns a <code>Future</code> once an available thread is found.
The PlaceConjuredGuardAsync() function handles making sure that our work gets delegated to an available thread.




Line 311: Line 276:


image here
image here
== Back to the Future ==
'''Futures''' are [https://cloud.google.com/appengine/docs/python/ndb/futureclass a concept from parallel processing]. It can be thought of as a placeholder in lieu of your result until your result has arrived. Like the Google App Engine version that this was inspired by, when the Future is created, it will probably not have any results yet. Your script can store a <code>Future</code> and later call the <code>Future</code> object's <code>get_result()</code> function, which should return your results immediately.
{{InDepth|A few notes about Futures:
* Futures '''contain the result''' of a thread that has finished.
* Futures are lightweight Activator ObjectReferences placed in an unloaded cell.
* A <code>Future</code> is '''temporary''', and exists until the result is read, after which, the <code>Future</code> is destroyed. Make sure to save your results to your own variable if you will need them later, since the <code>Future</code> will no longer exist after calling <code>get_result()</code>. This keeps the number of ObjectReferences created under control, and helps prevent save game size bloat.
* <code>get_result()</code> is technically ''blocking'', meaning it waits until results are received from the thread, and then returns. However, since <code>wait_all()</code> waits for all threads to complete, there should be no reason you should have to wait on your results when calling this function, unless something went wrong.
* The result of a <code>Future</code> is the same as the result of any other function in Papyrus, and can return None, false, etc if an error is encountered. Code the result of a <code>Future</code> like you would the result of anything else and anticipate errors accordingly.
* Futures will attempt to unlock threads that have become unresponsive.}}
Let's create our Future:
<source lang="papyrus">
scriptname GuardPlacementFuture extends ObjectReference
Quest property GuardPlacementQuest auto
ObjectReference r
ObjectReference property result hidden
function set(ObjectReference akResult)
done = true
r = akResult
endFunction
endProperty
bool done = false
bool function done()
return done
endFunction
ObjectReference function get_result()
;Terminate the request after 10 seconds, or as soon as we have a result
int i = 0
while !done && i < 100
i += 1
utility.wait(0.1)
endWhile
RegisterForSingleUpdate(0.1)
       
        if i >= 100
                ;Our thread probably encountered an error and is locked up; we need to unlock it.
                (GuardPlacementQuest as GuardPlacementThreadManager).TryToUnlockThread(self as ObjectReference)
        endif
return r
endFunction
Event OnUpdate()
self.Disable()
self.Delete()
endEvent
</source>
This script should be '''compiled and attached to the Future Activator''' object we created earlier. After you've attached it, make sure to '''fill the properties.'''
{{ProTip|Note the Type of the result; this could be changed to any data type you need to return.}}
{{ProTip|As a best practice, only interface with the <code>Future</code> using its member functions, <code>done()</code> and <code>get_result()</code>.}}
=== Quick Detour: Revisiting the Thread Script ===
There was a line from our thread script that was commented out, because our Future script didn't exist yet:
<source lang="papyrus">
  ;(future as GuardPlacementFuture).result = result
</source>
Go back and uncomment this line and recompile the parent thread script. You don't need to recompile all of the children.
{{WarningBox|This is important! If you don't uncomment this line, your thread will never return results to the Future!}}




Anonymous user