Jump to content

Recommended Posts

Posted

@Fraying9981

 

As you point out, the functionality *can* be implemented in SLTScript, but is painful and slow and lacks some ease of use. So what if the notion of objtrackers was introduced via some new functions. A set of bindings available in SLTScript that are implemented in Papyrus by SLTR for performance and handle a lot of the bookkeeping for you. Global in scope, tied to FormID to avoid needing to load objects for reference, along with some syntactic sugary goodness to add ease of use.

 

objtracker functions:
- globally accessible object based tracking
- objects tracked by formID
- can add/delete values that will be tracked
- a suite of functions to manipulate them
- mostly syntactic sugar
- SLTR handles the bookkeeping
- global (spans scripts, persists across saves, just like globals)
- can have multiple objtrackers (based on objtracker key)

 

; objtracker management
objtracker_create <objtracker_key> ; create an objtracker identified by the key
objtracker_delete <objtracker_key> ; delete an objtracker identified by the key
objtracker_exists <objtracker_key> ; return true if it exists, false otherwise

 

; objtracker object management (i.e. add/remove/list objects being tracked)

objtracker_object_add <objtracker_key> <object OR formID> ; add the object to be tracked by the identified objtracker; ignores duplicates
objtracker_object_delete <objtracker_key> <object OR formID> ; remove an object from the objtracker identified by the objtracker_key
objtracker_object_count <objtracker_key> ; return the count of tracked objects for this objtracker
objtracker_object_is_tracked <objtracker_key> <object OR formID> ; return true if the object specified is being tracked by this objtracker, false otherwise
; the next two may be of dubious usefulness, I dunno
objtracker_object_list <objtracker_key> ; return a Form[] list of the tracked objects for this objtracker
objtracker_object_get <objtracker_key> <index> ; return the Form at the index-th position for this objtracker

 

; objtracker tracked value management (i.e. which values are we tracking)

objtracker_value_create <objtracker_key> <value_name> <value_type> ; start tracking the value identified by value_name, of the indicated type (i.e. int, float, etc) for all tracked objects for the specified objtracker
objtracker_value_delete <objtracker_key> <value_name> ; stop tracking the value identified by value_name, for the specified objtracker (frees up memory, in case you dun goofed)

 

; setting/adjusting specific values
objtracker_value_set <objtracker_key> <object OR formID> <value_name> <new value> ; sets the value for the tracked value for the tracked object for the identified objtracker
objtracker_value_adjust <objtracker_key> <object OR formID> <value_name> <adjust value> ; adjusts the value likewise, usable only for reasonable types like int/float

 

; setting/adjusting aggregate values (i.e. for all tracked objects)

objtracker_valueall_set <objtracker_key> <value_name> <new value> ; for all tracked objects for this objtracker, for the indicated value, set them to the new value
objtracker_valueall_adjust <objtracker_key> <value_name> <adjust value> ; for all tracked objects for this objtracker, for the indicated value, perform the indicated adjustment (usable only for things like int and float)

 

; fetching tracked objects matching basic criteria (i.e. all objects with tracked value matching/not matching/greater than/less than specified value)

objtracker_object_valueget_matching <objtracker_key> <value_name> <value to compare> ; returns all tracked objects where the tracked value matches the value to compare
objtracker_object_valueget_not_matching <objtracker_key> <value_name> <value to compare> ; returns all tracked objects where the tracked value does not match the value to compare
objtracker_object_valueget_above <objtracker_key> <value_name> <value to compare> ; same, but if value is greater than value to compare
objtracker_object_valueget_below <objtracker_key> <value_name> <value to compare> ; same, but if value is less than value to compare


Use case like your DD time worn tracking.

 

; OnSessionStart.sltscript

;================

; create the tracker if it doesn't exist
; this could go into an OnSessionStart Core trigger which would run at each startup
objtracker_exists "dd_time_worn_tracker"
if $$ != true
    objtracker_create "dd_time_worn_tracker"
    ; note that I was originally, for this example, going to call the tracker "dd_time_worn" but didn't want to confuse this step
    ; there would, however, be nothing wrong with having a value name be the same as the objtracker name, just saying
    objtracker_value_create "dd_time_worn_tracker" "dd_time_worn" float
endif
; now an objtracker named "dd_time_worn_tracker" exists and all tracked objects it contains will have a float value associated called "dd_time_worn"

 

; OnActorAdd.sltscript

;================
; now in your accumulator script where you add tracked actors
; just verify that the actor matches the required criteria
; no worries about duplicating objects in the list
set $trackable_actor $system.self
; check if $trackable_actor *should* be tracked
; note that you *CAN* use objtracker_object_is_tracked here *if you want*
; for example to prevent yourself doing all the *other* checks for trackability 
; but you *do not have to* because duplicates will be ignored
; in this example, this could take place when an actor has a piece of DD gear applied
if $should_be_tracked
    ; again, note that it won't matter
    objtracker_object_add "dd_time_worn_tracker" $trackable_actor
endif

 

; OnHourly.sltscript

;================

; each hour, just tell the objtracker to adjust the time worn
objtracker_valueall_adjust "dd_time_worn_tracker" "dd_time_worn" 1.0

 

; OnActorRemovedDD.sltscript

;================

; presumably if the actor no longer has any DD gear on them, you would remove them from the tracker
; in this case, the script would check to see if any remaining gear is on the actor in question
; and if not, removes them
if $no_remaining_dd_gear
    objtracker_object_delete "dd_time_worn_tracker" $system.self
endif

 

; ListCheck.sltscript

;================

; Fetch any actor (formID most likely is what I will store but idk) where dd_time_worn is over 24 hours
set $bondage_lovers resultfrom objtracker_object_valueget_above "dd_time_worn_tracker" "dd_time_worn" 24.0
set $list_count resultfrom listcount $bondage_lovers
msg_notify $"You are currently tracking ${list_count} bondage lovers."

 

 

Thoughts?

Posted
2 hours ago, hextun said:

Thoughts?

 

yes that would be even more awesome. because like you said, better performance-wise + no need to write all the code i shared.

 

2 hours ago, hextun said:

objtracker_value_create <objtracker_key> <value_name> <value_type> ; start tracking the value identified by value_name, of the indicated type (i.e. int, float, etc) for all tracked objects for the specified objtracker

 

There is one thing you need to decide imo that I didn't see fully in your reply (also applies to my initial proposition): typing.

You need to decide which type(s) you cover. Strings, ints. timesamps? nested arrays?

in my example: its an array of ints. The date one I'm not too sure how i hacked it lol (thank you claude).

this is an important design choice to make now because it will affect for example what you said about comparing values.

 

I would recommend a MINIMAL number of types handled. maybe just strings and ints. 

 

And another consideration - for this one as well, you are the judge whether relevant or not:

  • a key principle of this system we are talking about is top of the hour/regular updates
  • therefore it needs to be optimized to handle a batch update of 1 to n characters

this comes into play at 2 levels:

  1. batch logic. the base form is the for loop = for 1 to n ... update record (every time, all records in the array checked). i suppose that's good enough and probably the only way
  2. time logic
    1. = how do you track the last update of each variable?
    2. and should you update all values at once every time? should you check when they were last updated? pointing this out because values are somewhat independent in the array. and they are both updated by top of the hour, and individually during and after SL scenes
    3. my version is a bit primitive: i have booleans for whether a DD is worn RIGHT NOW, and a time field for each DD slot to know when it was last equipped
Posted

I'm going to post this in the club, too, and probably add it to the main page as well, because it's pretty important for performance tweaking.

 

SLTriggers Redux is *extremely* Papyrus intense, so anything you can do to improve scripting performance will have a *dramatic* impact on SLTScript performance.

 

To that end, I strongly recommend the following to maximize the performance of your SLTScripts.

 

- Install Papyrus Tweaks NG

- In SKSE/Plugins/PapyrusTweaks.ini (a reference copy of which is available in the Files tab on Nexusmods) make the following changes:

- iMaxOpsPerFrame = 2000

- bSpeedUpNativeCalls = true

- bIgnoreMemoryLimit = true

 

For reference, I use several modlists for testing, including Skyrim Modding Essentials (SME), available on Wabbajack. This is a somewhat bare list meant as a springboard for creating your own modlist. While it includes Papyrus Tweaks NG, it only includes the unmodified reference copy of PapyrusTweaks.ini.

 

I install SLTriggers Redux with the "Test Scripts" enabled. To run them, open your console in game and:

- prid player

- slt run zz_sltr_regression_test

 

This will begin a set of SLTScripts which will send output to your sl-triggers.log file. I keep Notepad++ running, use "View | Always On Top", and "View | Monitoring (tail -f)" on sl-triggers.log so that I can see the output in real time and watch for failures, as well as knowing when the scripts have completed.

 

Using the baseline reference copy of PapyrusTweaks.ini on SME, the full suite of regression test scripts takes roughly 140 seconds to run.

 

Using just those 3 changes to PapyrusTweaks.ini, the execution time drops to just over 14 seconds.

 

Not a 10% reduction in execution time but a whopping 90% reduction.

 

I totally agree that SLTScripts are slow compared to actual compiled Papyrus, but you can get a lot of performance by making these changes.

 

 

Posted

v0.985 is out. Out and about. Give it a shout. See what it's about. It doesn't have gout. Oh, please, don't pout. Out.

 

Yeeeeahh... so anyway, that was weird, right? Well, on to what this drop is about.

 

The wait loop is removed. It turned out to be more straightforward than I thought. I saw a roughly 10% improvement in performance in my very basic testing. Also the timings I mentioned in the post above were using this version.

 

Yeah, that's pretty much it. I felt it was important enough to go ahead and release.

 

We are now quiesced.

Posted
1 hour ago, hextun said:

v0.985 is out. Out and about. Give it a shout. See what it's about. It doesn't have gout. Oh, please, don't pout. Out.

 

Yeeeeahh... so anyway, that was weird, right? Well, on to what this drop is about.

 

The wait loop is removed. It turned out to be more straightforward than I thought. I saw a roughly 10% improvement in performance in my very basic testing. Also the timings I mentioned in the post above were using this version.

 

Yeah, that's pretty much it. I felt it was important enough to go ahead and release.

 

We are now quiesced.

 

I feel like I'm underselling things a bit. So what is the "wait loop"?

 

Say you have an SLTScript like so:

 

1: set $counter 1
2: [loopy]
3: msg_notify $"At counter {counter}"
4: inc $counter 1
5: util_wait 5
6: goto [loopy]

 

Note: I added line numbers.

 

Ugly little thing that loops forever (I don't suggest you run this because it will literally never quit until/unless you reload an older save or perform an SLTR reset), it increments a counter an updates a message with the counter value each 5 seconds.

 

Lines 1, 2, 4, and 6 all execute "fully inside the engine". That is, during the execution loop, which goes line by line executing the command, those all use intrinsics that are directly performed without leaving that main loop.

 

Lines 3 and 5, however, call functions, specifically "msg_notify" and "util_wait". These functions, because they are not part of the core language, are sent to the plugin .dll, which finds the implementing Papyrus function and sends the request.

 

At this point, in v0.984 and older, two things start happening:

- the execution loop starts sitting inside a relatively tight loop ** this is the "wait loop" **, waiting for 0.01 seconds (which increments by 0.01 each loop, so first waiting 0.01 seconds, then 0.02 seconds, etc. up to 1 second wait times) until the requested operation completes 

- the implementing Papyrus function does its work and then notifies the execution loop it is complete

 

Once the completion flag is set, the "wait loop" stops and the execution loop proceeds.

 

This is, to say the least, non-optimal, and something I have been wanting to eliminate for awhile. But aside from long running commands (like, say, "util_wait 600", which would keep that wait loop running on 1 second loops for 10 minutes), most functions return quickly enough that the amount of time spent in the wait loop itself is relatively small. Still, it's script time that just wasn't doing anything.

 

When I had first implemented this, I had actually meant to implement this via events or some other non-loop-flag method, but for whatever reason it just didn't click.

 

The clickening has occurred however.

 

Now what happens when a function, like on lines 3 and 5, is reached:

- the execution loop just stops processing altogether, but leaves important flags set to let it know basically where to pick back up

- once the implementing Papyrus function completes, it's no longer just setting a flag for the wait loop to stop; instead it restarts the execution loop where it left off

 

No more "wait loop". Roughly 10% speed improvement in my tests and less script load overall.

Posted
1 hour ago, hextun said:

 

I feel like I'm underselling things a bit. So what is the "wait loop"?

 

Say you have an SLTScript like so:

 

1: set $counter 1
2: [loopy]
3: msg_notify $"At counter {counter}"
4: inc $counter 1
5: util_wait 5
6: goto [loopy]

 

Note: I added line numbers.

 

Ugly little thing that loops forever (I don't suggest you run this because it will literally never quit until/unless you reload an older save or perform an SLTR reset), it increments a counter an updates a message with the counter value each 5 seconds.

 

Lines 1, 2, 4, and 6 all execute "fully inside the engine". That is, during the execution loop, which goes line by line executing the command, those all use intrinsics that are directly performed without leaving that main loop.

 

Lines 3 and 5, however, call functions, specifically "msg_notify" and "util_wait". These functions, because they are not part of the core language, are sent to the plugin .dll, which finds the implementing Papyrus function and sends the request.

 

At this point, in v0.984 and older, two things start happening:

- the execution loop starts sitting inside a relatively tight loop ** this is the "wait loop" **, waiting for 0.01 seconds (which increments by 0.01 each loop, so first waiting 0.01 seconds, then 0.02 seconds, etc. up to 1 second wait times) until the requested operation completes 

- the implementing Papyrus function does its work and then notifies the execution loop it is complete

 

Once the completion flag is set, the "wait loop" stops and the execution loop proceeds.

 

This is, to say the least, non-optimal, and something I have been wanting to eliminate for awhile. But aside from long running commands (like, say, "util_wait 600", which would keep that wait loop running on 1 second loops for 10 minutes), most functions return quickly enough that the amount of time spent in the wait loop itself is relatively small. Still, it's script time that just wasn't doing anything.

 

When I had first implemented this, I had actually meant to implement this via events or some other non-loop-flag method, but for whatever reason it just didn't click.

 

The clickening has occurred however.

 

Now what happens when a function, like on lines 3 and 5, is reached:

- the execution loop just stops processing altogether, but leaves important flags set to let it know basically where to pick back up

- once the implementing Papyrus function completes, it's no longer just setting a flag for the wait loop to stop; instead it restarts the execution loop where it left off

 

No more "wait loop". Roughly 10% speed improvement in my tests and less script load overall.

 

sorry i didnt get it, how did you improve wait loop? it's faster now but we can still use wait right?

Posted
3 hours ago, Fraying9981 said:

 

sorry i didnt get it, how did you improve wait loop? it's faster now but we can still use wait right?

 

And I was so proud of my explanation. :)

 

So, this has nothing to do with the "util_wait" (or various other "wait" functions). 

 

Instead, this is an internal, SLTriggers Redux script execution engine level fix.

 

Before, anytime your .sltscript called a function documented here (Function Libraries · sltriggersredux Wiki), while that function was executing, the SLTriggers Redux "engine" was still idling, by sitting inside the equivalent of a "while executing_function/Utility.Wait(0.01)" loop. When the function in question would complete, it would set "executing_function" to false, breaking the tight loop and allowing the "engine" to move to the next line of your script. I call this the "wait loop" (and maybe I should have called it the "function execution wait loop" but in my head it was much easier to just think "wait loop"). The specifics were a *little* more complicated than that, but that's the general idea.

 

Now, with this latest change, when you call a function documented here (Function Libraries · sltriggersredux Wiki), the "engine" literally just stops running altogether. No more tight "Utility.wait(0.01)" loop. Instead, at the same point after a function finishes running where it would have previously set "executing_function" to false, it kick starts the "engine" again, letting it continue where it had left off.

 

Papyrus Code (yes, actual Papyrus code) incoming:

 

The "engine" of SLTriggers Redux is implemented by a pair of Papyrus functions in the "sl_triggersCmd.psc" script: "RunScript()" and "RunCommandLine()". They are pretty hefty and go beyond the scope of dumping here, but basically:

- RunScript(): iterates the currently running .sltscript and calls RunCommandLine() for each line in the script

- RunCommandLine(): parses the line and either runs the command internally (for things like "set") or, for functions documented here(Function Libraries · sltriggersredux Wiki), calls "RunOperationOnActor()" to ask the .dll to call the implementing Papyrus function.

 

So, when your script hits a function documented here (Function Libraries · sltriggersredux Wiki), it at some point calls my Papyrus function "RunOperationOnActor" with a string[] parameter that includes the tokens from the command to run. So for an .sltscript line that said:

 

  perk_addpoints 10

 

RunOperationOnActor would get called with string[] parameter:

  0: "perk_addpoints"

  1: "10"

 

== RunOperationOnActor: Before ================================================

Spoiler
Function RunOperationOnActor(string[] opCmdLine)
    if !opCmdLine.Length
        return
    endif
    if IsResetRequested || !SLT.IsEnabled || SLT.IsResetting
        SFI("SLTReset requested(" + IsResetRequested + ") / SLT.IsEnabled(" + SLT.IsEnabled + ") / SLT.IsResetting(" + SLT.IsResetting + ")")
        CleanupAndRemove()
        Return
    endif

    runOpReturnedValue = false
    runOpPending = true
    bool success = sl_triggers_internal.RunOperationOnActor(CmdTargetActor, self, opCmdLine)
    if !success
        runOpPending = false
        return
    endif
    float afDelay = 0.0

    while runOpPending && isExecuting
        if afDelay < 1.0
            afDelay += 0.01
        endif
        
        if IsResetRequested || !SLT.IsEnabled || SLT.IsResetting
            SFI("SLTReset requested(" + IsResetRequested + ") / SLT.IsEnabled(" + SLT.IsEnabled + ") / SLT.IsResetting(" + SLT.IsResetting + ")")
            CleanupAndRemove()
            Return
        endif

        Utility.Wait(afDelay)
    endwhile
EndFunction

 

 

== RunOperationOnActor: Now ================================================

Spoiler
Function RunOperationOnActor(string[] opCmdLine, bool runOpForResultfrom = false)
    if !opCmdLine.Length
        return
    endif
    if IsResetRequested || !SLT.IsEnabled || SLT.IsResetting
        SFI("SLTReset requested(" + IsResetRequested + ") / SLT.IsEnabled(" + SLT.IsEnabled + ") / SLT.IsResetting(" + SLT.IsResetting + ")")
        CleanupAndRemove()
        Return
    endif

    runOpReturnedValue = false
    runOpPending = true
    resultfromRunOpPending = runOpForResultfrom
    bool success = sl_triggers_internal.RunOperationOnActor(CmdTargetActor, self, opCmdLine)
    if !success
        runOpPending = false
        return
    endif
EndFunction

 

 

Note first, that mostly all that has happened is the removal of the whole big block of code at the end with the "while/Utility.Wait(afDelay)/endwhile" loop. There are also some bookkeeping flags that were added, but mostly the loop is gone. That's the "wait loop". Literally any time you ran a function that is documented here(Function Libraries · sltriggersredux Wiki), this loop was *also* running, waiting for the function to finish.

 

Note also, the call to "sl_triggers_internal.RunOperationOnActor"? That is telling the .dll to go find the Papyrus function that implements, in this case "perk_addpoints", and send it the arguments in question. Here is the Papyrus code that implements "perk_addpoints" (this hasn't changed):

 

== perk_addpoints: Unchanged ================================================

Spoiler
function perk_addpoints(Actor CmdTargetActor, ActiveMagicEffect _CmdPrimary, string[] param) global
	sl_triggersCmd CmdPrimary = _CmdPrimary as sl_triggersCmd

	if ParamLengthEQ(CmdPrimary, param.Length, 2)
		Game.AddPerkPoints(CmdPrimary.ResolveInt(param[1]))
	endif

	CmdPrimary.CompleteOperationOnActor()
endFunction

 

 

This is pretty close to the barebones you can get for the implementation of any function library call (i.e things that would be documented here, Function Libraries · sltriggersredux Wiki, and that are not part of the core script language).

 

Note in particular the call to "CmdPrimary.CompleteOperationOnActor()". That is what tells the engine "hey, I'm back, start processing the rest of the script".

 

== CompleteOperationOnActor: Before ================================================

Spoiler
Function CompleteOperationOnActor()
    runOpPending = false

    if !runOpReturnedValue
        InvalidateMostRecentResult()
    endif

    if IsResetRequested || !SLT.IsEnabled || SLT.IsResetting
        SFI("SLTReset requested(" + IsResetRequested + ") / SLT.IsEnabled(" + SLT.IsEnabled + ") / SLT.IsResetting(" + SLT.IsResetting + ")")
        CleanupAndRemove()
        Return
    endif
EndFunction

 

 

== CompleteOperationOnActor: After ================================================

Spoiler
Function CompleteOperationOnActor()
    runOpPending = false

    if !runOpReturnedValue
        InvalidateMostRecentResult()
    endif

    if IsResetRequested || !SLT.IsEnabled || SLT.IsResetting
        SFI("SLTReset requested(" + IsResetRequested + ") / SLT.IsEnabled(" + SLT.IsEnabled + ") / SLT.IsResetting(" + SLT.IsResetting + ")")
        CleanupAndRemove()
        Return
    endif

    RunScript()
EndFunction

 

 

Note the additional call to "RunScript()" at the end? That's pretty much it.

 

And that's it. That's (basically) the change. And because that tight "Utility.Wait()" loop is now gone, "RunScript()" isn't sitting there, using up execution cycles to wait in place while the Papyrus invoked by "RunOperationOnActor()" wraps up.

 

Final note: You may look at this and think "hey, is this somehow introducing a sort of recursion issue within the Papyrus VM?" Nope; each use of "sl_triggers_internal.RunOperationOnActor()" to call the .dll results in, basically, a new callstack within the VM because it is dispatching a new request. Functionally similar to dispatching an event. Say you had an .sltscript that was:

 

  perk_addpoints 1

  perk_addpoints 1

 

When the "CompleteOperationOnActor()" call for the first perk_addpoints ran "RunScript()", and the .sltscript reached the next line to call "sl_triggers_internal.RunOperationOnActor()" for the next "perk_addpoints" call, it's "RunScript()" would return immediately, basically ending that callstack. The new call dispatched by the .dll would start a new callstack that itself would finish with its own call to "CompleteOperationOnActor()" and when the end of the script was reached, that call to "RunScript()" would see that and properly terminate the entire .sltscript and tell the ActiveMagicEffect to dispel itself.

 

 

Posted (edited)

Working on expanding the Devious Devices NG bindings to include:

 

        dd_get_device_name: return the name of the worn device specified by slot OR keyword
        dd_is_wearing_device: return true if wearing a device (even if blockgeneric or questitem) specified by slot OR keyword
        dd_is_generic_device: return true if wearing a generic specified by slot OR keyword
        dd_unlock: unlocks the device specified by slot OR keyword
        dd_is_lock_jammed: returns true if device specified by slot OR keyword is jammed
        dd_jam_lock: jams the lock of the device specified by slot OR keyword is jammed
        dd_unjam_lock: unjams the lock of the device specified by slot OR keyword is jammed
        dd_is_lock_manipulated: returns true if device specified by slot OR keyword is manipulated
        dd_set_lock_manipulated: sets device specified by slot OR keyword to be manipulated
        dd_set_lock_unmanipulated: sets device specified by slot OR keyword to be unmanipulated
        dd_play_horny_animation: plays the horny animation for the specified actor
        dd_orgasm: performs a Devious Devices orgasm event
        dd_masturbate: start a Devious Devices masturbation scene
        dd_edge_actor: start a Devious Devices edge actor event
        dd_chastity_belt_struggle: start a Devious Devices chastity belt struggle event
        dd_trip: start a Devious Devices trip event
        dd_catch_breath: start a Devious Devices catch breath event
        dd_has_breasts_exposed: Returns true if breasts are exposed, false otherwise
        dd_inflate_anal_plug: inflates an anal plug
        dd_inflate_vaginal_plug: inflates a vaginal plug
        dd_inflate_random_plug: inflates a random plug
        dd_deflate_anal_plug: deflates an anal plug
        dd_deflate_vaginal_plug: deflates a vaginal plug
        dd_lock: lock an item in actor's inventory onto an actor

 

Note that dd_unlockslot already exists, but dd_unlock will allow for either slot or keyword specification as described above.

 

I've implemented them all but have only tested the first four (dd_get_device_name, dd_is_wearing_device, dd_is_generic_device, and dd_unlock).

 

Also, new canned script, UnlockSelectedDevice.sltscript, which will use UILIB to popup a list of currently worn generic devices to give you the option to cancel, unlock all, or unlock the selected device.

 

(fourth edit?) Though now I think about it, I could also create a "Device Management" script that lets you choose one of the options above (unlock, jam, unjam, manipulate, unmanipulate) or other general actions (orgasm, masturbate, inflate, trip, struggle) with the associated device picker (if needed).

Edited by hextun
Posted

Okay, reporting progress:

 

I've managed to test all of the newly minted DD bindings to my satisfaction. Jams, Unjams, Manipulates, Unmanipulates, Locks, Unlocks. My poor DB probably wonders why those pesky collars keep acting up! And that's not to mention the inflatable plugs *brr*. And the trips, breath catching, chastity belt struggles (which, of course, you have to be wearing a belt for that to work), and other animations.

 

One fun fact: with my removal of the "wait loop" logic I messed up handling of unknown functions. Right now if you are on 985, and your script contains a typo (think "mgs_nytofi" instead of "msg_notify"), instead of just emitting a warning and the script moving on, the script just stops. The magic effect will just sit there not doing anything (so there's no script or engine performance impact) and will silently end after accruing 24 hours of real time, but will otherwise just be taking up one of the 30 available slots for that actor. This probably won't be a problem unless the same script keeps getting run and hanging. An SLT reset should fix it but of course it will occur again if you run the script again.

 

Anyhow, next release will have a fix for that.

 

Funny story: I discovered it while running my test scripts. I accidentally tried to run "dd_is_lock_UNmanipulated" (when there is only "dd_is_lock_manipulated") and had to take a moment to work out why my script just started dying there.

 

Other funny story: I spent more than a little time troubleshooting "dd_is_lock_jammed", which should be a ridiculously simple implementation, because in my test script I had not included the target actor parameter. It wasn't until I dug into sl-triggers.log that I realized what was going on. :P

 

Posted

"Sir... they've... they've just deployed v0.986"

 

..

 

"God save us all."

 

Alriiiiiiiighty then... v0.986 includes two things:

- a bugfix for the Land of Mistyped Functions. Way way long ago.. back in the olden times of v0.985 (but not in v0.984 or earlier, c'mon), when I removed the "wait loop", it introduced a somewhat subtle bug where, if you tried to call an instruction that the engine didn't recognize as either an intrinsic (e.g. "set", "inc") or a function (e.g. "util_wait", "sl_isin"), it would permanently hang the magic effect running that script. This would happen if, say, you typoed the instruction name (e.g. "ste" instead of "set", "ulit_wat" instead of "util_wait"). An SLTR Reset *should* clear it out as that sends an event which should terminate them, but even if you choose not to do that, it will terminate on its own after 24 hours of wall clock time have passed (i.e. 24 hours of active butt in seat play time, not game time). In the meantime it will take up one of the 30 slots scripts can execute with. Probably not a problem for most, but if you have rapid firing scripts, they may very well have already consumed all 30 slots with hung magic effects. Worst case scenario: revert to an older save OR do the "remove SLTR->load->save->optional ReSaver cleanup->add SLTR back" cycle.

 

- Devious Devices bindings have been added. Many, many bindings. 24 new bindings in fact. Me-tested, me-approved. Basically this is to allow lock/unlock/jam/unjam/manipulate/unmanipulate/inflate/deflate of devices, along with triggering animations/scenes such as orgasm/masturbate/trip/chastity belt struggle/catch breath/edge actor/horny. See the CHANGELOG or the Function Libraries - Devious Devices wiki page section for more details.

Posted
3 minutes ago, hextun said:

"Sir... they've... they've just deployed v0.986"

 

..

 

"God save us all."

 

Alriiiiiiiighty then... v0.986 includes two things:

- a bugfix for the Land of Mistyped Functions. Way way long ago.. back in the olden times of v0.985 (but not in v0.984 or earlier, c'mon), when I removed the "wait loop", it introduced a somewhat subtle bug where, if you tried to call an instruction that the engine didn't recognize as either an intrinsic (e.g. "set", "inc") or a function (e.g. "util_wait", "sl_isin"), it would permanently hang the magic effect running that script. This would happen if, say, you typoed the instruction name (e.g. "ste" instead of "set", "ulit_wat" instead of "util_wait"). An SLTR Reset *should* clear it out as that sends an event which should terminate them, but even if you choose not to do that, it will terminate on its own after 24 hours of wall clock time have passed (i.e. 24 hours of active butt in seat play time, not game time). In the meantime it will take up one of the 30 slots scripts can execute with. Probably not a problem for most, but if you have rapid firing scripts, they may very well have already consumed all 30 slots with hung magic effects. Worst case scenario: revert to an older save OR do the "remove SLTR->load->save->optional ReSaver cleanup->add SLTR back" cycle.

 

- Devious Devices bindings have been added. Many, many bindings. 24 new bindings in fact. Me-tested, me-approved. Basically this is to allow lock/unlock/jam/unjam/manipulate/unmanipulate/inflate/deflate of devices, along with triggering animations/scenes such as orgasm/masturbate/trip/chastity belt struggle/catch breath/edge actor/horny. See the CHANGELOG or the Function Libraries - Devious Devices wiki page section for more details.

 

that's awesome!

Posted
5 hours ago, Fraying9981 said:

 

that's awesome!

 

Thank you! :)

 

I want to also mention something I forgot to add to the CHANGELOG in the latest release: a new shipped SLTScript that comes with either SexLab selection.

 

UnlockSelectedDevice.sltscript

 

It checks each slot for a devious generic (non-quest/non-blocking) device and presents a list of any found, along with options to Cancel or to unlock All, and based on your selection, unlocks the indicated device.

 

You can either attach this to a hotkey trigger or use the console and select any actor, including the player, and use "slt run UnlockSelectedDevice" to get the popup.

 

It makes use of the newly shipped dd_* (Devious Devices) bindings.

 

Have fun!

Posted
4 minutes ago, hextun said:

 

Thank you! :)

 

I want to also mention something I forgot to add to the CHANGELOG in the latest release: a new shipped SLTScript that comes with either SexLab selection.

 

UnlockSelectedDevice.sltscript

 

It checks each slot for a devious generic (non-quest/non-blocking) device and presents a list of any found, along with options to Cancel or to unlock All, and based on your selection, unlocks the indicated device.

 

You can either attach this to a hotkey trigger or use the console and select any actor, including the player, and use "slt run UnlockSelectedDevice" to get the popup.

 

It makes use of the newly shipped dd_* (Devious Devices) bindings.

 

Have fun!

 

wait so for example could you bind this to a key and use it on any npc to clean them up?

Posted (edited)
53 minutes ago, Fraying9981 said:

 

wait so for example could you bind this to a key and use it on any npc to clean them up?

 

Hmm... it currently uses $system.self, and for hotkeys that *always* targets the player.

 

But if you copied it out and added this at the top:

 

set $target_actor resultfrom cursor_ref

 

Then changed references from $system.self to $target_actor, using the hotkey would then target whichever actor is located under the crosshairs.

 

And if instead you went further and instead added *this* to the top:

 

set $target_actor resultfrom cursor_ref

if $target_actor == none

  set $target_actor $system.player

endif

 

And then changed references from $system.self to $target_actor, using the hotkey would then target whichever actor is located under the crosshairs and if no one is selected, would use whomever the script is targeted at, the player in case of hotkeys.

 

Here's the latter modified version:

 

Spoiler
set $target_actor resultfrom cursor_ref
if $target_actor == none
    set $target_actor $system.player
endif

string[] $labels
int[] $slots

listadd $labels "- Cancel -" "- All -" 
listadd $slots 0 1

set $check_slot 30
while $check_slot < 62
	set $is_generic_device resultfrom dd_is_generic_device $target_actor slot $check_slot
	if $is_generic_device
		set $device_name resultfrom dd_get_device_name $target_actor slot $check_slot
		listadd $labels $device_name
		listadd $slots $check_slot
	endif
	
	inc $check_slot 1
endwhile

set $target_name resultfrom actor_name $target_actor
set $sel_index resultfrom uilib_showlist $"Select a Slot to Unlock for {target_name}" $labels 0 0
if $sel_index == 0
	uilib_shownotification "Unlock cancelled"
	return
endif

if $sel_index == 1
	dd_unlockall $target_actor
	return
endif

dd_unlock $target_actor slot $slots[$sel_index]

 

 

With that combined version, (which I tested by mapping to the '=' key), if you have an NPC under your crosshairs, it will pop up for them; with no NPC under the crosshairs, it will pop up for the player.

 

As an aside, something odd I noticed in my test was that the hotkey capability stopped working, at least temporarily, *if* I tabbed out of the game, *until* I reentered the SLTriggers Redux MCM specifically and exited, whether I made any actual MCM changes or not.

 

But otherwise things seemed to work normally provided I didn't tab out.

 

ETA: I didn't think to ship this version because for myself I'm mostly manipulating my own devices, and since it targeted $system.self. And since I rarely use it, and even more rarely on an NPC, I didn't think to set it up explicitly for hotkey usage with the intent of targeting NPCs, too.

Edited by hextun
Posted
8 hours ago, Fraying9981 said:

@hextunnice!
do you have a code snippet for cell scan?

I'd like to scan NPCs within a certain range only

 

; using afRadius 1000.0 which seems to be maybe 10 meters equivalent?
set $candidates resultfrom util_scan_cell_npcs $system.player 1000.0

Posted
1 hour ago, adragtobearound said:

This mod has file overwrites/potential incompatibilities with Violens Killmove Mod and Nethers Follow Framework. Attached are the files with overwrites
 

Screenshot 2026-07-25 045013.png


This is a common misconception regarding UILIB, which is *explicitly* intended to be included by each mod that needs it. 
 

The GitHub wiki goes into more detail: https://github.com/schlangster/skyui-lib/wiki/How-to
 

Posted
1 hour ago, hextun said:


This is a common misconception regarding UILIB, which is *explicitly* intended to be included by each mod that needs it. 
 

The GitHub wiki goes into more detail: https://github.com/schlangster/skyui-lib/wiki/How-to
 

So, if a mod has the API script attached in its UILIB, it won't conflict? Am I right to assume that its like a callback to system files rather than runtime game files? IDK

Also, does this mean that I can ignore the MO2 overwrite notification, or should one overwrite the other?

Thanks for the information.

Posted
39 minutes ago, adragtobearound said:

So, if a mod has the API script attached in its UILIB, it won't conflict? Am I right to assume that its like a callback to system files rather than runtime game files? IDK

Also, does this mean that I can ignore the MO2 overwrite notification, or should one overwrite the other?

Thanks for the information.

Yes on both counts. 
 

loose files override BSA packed files and later mods override earlier in your modlist load order. 
 

But since any mod including UILIB files should be shipping identical functionality, it shouldn't matter. It should be like replacing one red 2x3 LEGO brick with another 2x3 LEGO brick. 
 

And as a result you should be able to ignore any override errors *specifically for UILIB because that's how it works*. Other overrides should be considered as usual. 

Posted
55 minutes ago, hextun said:

Yes on both counts. 
 

loose files override BSA packed files and later mods override earlier in your modlist load order. 
 

But since any mod including UILIB files should be shipping identical functionality, it shouldn't matter. It should be like replacing one red 2x3 LEGO brick with another 2x3 LEGO brick. 
 

And as a result you should be able to ignore any override errors *specifically for UILIB because that's how it works*. Other overrides should be considered as usual. 

Thank you for the in depth explanation.

Posted (edited)

How might I make a script that tells the actor to get naked first? I am trying to create a masturbation upon viewing sex script. Also, how do I specifically end a scene?

Edited by adragtobearound
Posted
2 hours ago, Fraying9981 said:

hey @hextun this mod has an API that can trigger events based on time.

Lets you create rules that trigger events after minutes, hours, days, or weeks, with loops, delays, notifications, and wait/sleep support.

fyi

 

https://www.nexusmods.com/skyrimspecialedition/mods/182291

 

Interesting. Bit of a pickle for me though. On the one hand, if I stick to the current architecture, being as this type of event wouldn't belong in "Core" (not being Vanilla or SKSE) nor is it tied to SexLab nor OStim, nor is it "adult general", it would properly belong in its own .esp (lite). On the other hand, that's yet another .esp in the load order, even if it is lite flagged. All to have the listener set up to trigger events.

 

*If* you're feeling froggy... and don't want to wait for me to decide whether/where to put such a listener in SLTriggers... *and* if you're up to doing some Papyrus development, there is something that can be done.

 

Note: I need to rewrite the wiki's Integration section to reflect current reality. At minimum the event name is wrong.

 

https://www.nexusmods.com/skyrimspecialedition/mods/65629 is a mod that allows you to use a configuration file to attach custom Papyrus code (presumably of your own making) to any object on load. So, for example, if you wrote a bit of Papyrus code that had a "OnTimeIsTicking" handler that, in turn, used the SLTriggers API or ModEvent integration to request a suitable script be invoked, you wouldn't need to wait and could get going immediately. In fact you could do this with any mod that lacks existing integration.

 

Say you wrote this Papyrus file, call it TimeTickingIntegrator.psc:

Spoiler
scriptname TimeIsTickingIntegrator extends ObjectReference

Event OnInit()
	RegisterForModEvent("TimeIsTickingEvent", "OnTimeIsTicking")
EndEvent

Event OnTimeIsTicking(String eventName, String ruleId, Float activationCount, Form sender)
	If ruleId == "my_custom_rule"
		; this will call my_custom_sltscript.sltscript via SLTriggers Redux
		Game.GetPlayer().SendModEvent("OnSLTRequestCommand", "my_custom_sltscript")
	EndIf
EndEvent

 

Note that it does not contain any custom objects so you should be able to just compile it without needing to set up any special includes outside of the basic objects such as Quest, Game, and Event.

 

Compile that to a .pex and make sure it's available in your load order. Note that NoESP says it will not work with .pex files inside BSA, so it should remain a loose file.

 

The NoESP configuration should look like this, which will attach this to the Player:

Spoiler
TimeIsTickingIntegrator

 

Yeah, that's it. :) This would look for TimeIsTickingIntegrator.pex and attach it to the Player, letting the OnInit run, which would configure the OnTimeIsTicking listener.

 

And that would do it. Any time the TimeIsTicking rule with ruleId "my_custom_rule" runs, it should send a ModEvent to SLTriggers to run "my_custom_sltscript.sltscript".

 

This is all theoretical, which is to say I have not compiled any of this nor tested it, but:

- The SLTriggers event "OnSLTRequestCommand" is right and is part of the regression tests, so that part should be fine

- Everything else is based on documentation for NoESP and TimeIsTicking

 

*If* you choose to try this out, post and let me (and everyone else) know. :)

 

I'll consider whether I want to add another integration. If I do and if I make it a separate Quest, in the MCM it would presumably be under "Mod Integrations" sitting alongside "Core", "SexLab", "OStim", and "Adult General". I would probably make it a selectable option in the FOMOD since it would create yet another .esp in the load order and if you aren't using it that seems excessive.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...