Skyrim Special Edition

Required reading: API Rundown: the basics & OSex's Animation labeling and tagging system explained & JContainers API 

In OStim 1.0, there was no way to perform a "lookup" to get animation data. If you wanted a random blowjob animation to play, you had to have the animation itself hardcoded into an addon.

In OStim 2.0, a useful new script called ODatabase is included. It uses JContainers' JArrays to store JMaps, with each JMap containing animation data. 
Why do we need to use Jcontainers instead of normal papyrus arrays? Well for starters, papyrus arrays are limited to 128 objects, and OSex has far more animations than that. Papyrus also doesn't have anything like a "dictionary", but that's exactly what a JMap is, which is super helpful for storing animation data.
Quick note: JArrays are called OArrays in the OStim source code, and JMaps are called OMaps. They're the exact same thing, so whenever you see OMap or OArray, it's the exact same thing as a JMap and a JArray, just a different name.

To start working with ODatabase, simply call getODatabase() from the main OStim script.

Let's look at what information ODatabase stores about each animation in OSex. Here is an animation pulled from the database

Name: Money Shot | Jerk 
Scene ID: BB|Sy6!KNy9|HhPo|MoShoPo
Number of actors: 2
Position Data: Sy6!KNy9
Source Module: BB
Animation Class: HhPo
Animation ID 0: 0SxBB_HhPo-MoShoPo_S0
Animation ID 1: 0SxBB_HhPo-MoShoPo_S1
Animation ID 2: 0SxBB_HhPo-MoShoPo_S2
Animation ID 3: 0SxBB_HhPo-MoShoPo_S3
Animation ID 4: 0SxBB_HhPo-MoShoPo_S4
Animation ID 5: 0SxBB_HhPo-MoShoPo_S5
Is Aggressive: TRUE
Is Transitory Animation: False| Transitory animations are the "traveling" animations. If an animation is neither transitory nor a Hub, it is a sexual animation
Is Hub Animation: False| hub animations are non-sexual animations where the characters stand around, like the very first animation OSex starts in
Main controlling actor: 0| 0 = dom actor is doing most of the work, 1 = sub actor is doing most of the work (like in a blowjob)
Minimum Speed: 0
Maximum Speed: 5
Has Idle Speed: False| if the slowest speed is called "Idle"

The above is an OMap of a single animation. If we call Odatabase.getDatabaseOArray(), we get an OArray containing EVERY OSex animation installed in the game. 

To sort through the massive JArray, ODatabase gives you a number of tools to narrow our search down. Essentially, to find a specific animation, we will start with the OArray containing every animation in the game, and then run a search on it with an Odatabase tool to return a smaller array closer to what we're looking for, and repeat this as many times as needed. See below!

int function getAllSexualAnimations()
int animations
animations = ODatabase.getDatabaseOArray() ;Get every single OSex animation in the game
animations = odatabase.getAnimationsWithActorCount(animations, 2) ; start by filtering all but the animations for 2 people
animations = odatabase.getHubAnimations(animations, false) ;take the results above, and filter out hub animations
animations = odatabase.getTransitoryAnimations(animations, false) ;again take the results above, and further filter out transitory animations

return animations ;this JArray contains all sexy time animations

string function getRandomBlowjobAnimation()
int animations = getAllSexualAnimations() ; get all animations with action in them

int numAnimations = ODatabase.getLengthOArray(animations) ; Odatabase contains a number of tools to help you work with JArrays

int i = 0
int max = 50
while (i < 50) 
int animation = ODatabase.getObjectOArray(animations, utility.RandomInt(0, numAnimations)) ; pick a random animation from the list above
console("Trying animation: " + ODatabase.getFullName(animation))

if (ODatabase.getAnimationClass(animation) == "BJ") ||  (ODatabase.getAnimationClass(animation) == "ApPJ") ; see if it has a blowjob class
return odatabase.getSceneID(animation) ;it does, send it 
i += 1

Can you think of a better way to do the above function? We could also take the list of all sexual animations, make one copy that is the animations filtered by the BJ class, then make another copy which is all animations filtered by the ApPJ class, then merge the two OArrays and pick a random one. 

It's a bit complicated, but quite powerful. Another example:

int function removeStandingAnimations(int animations) ;give this function an OArray of animations, and it will return a new OArray with all of the "standing' animations removed
int i = 0
int max = ODatabase.getLengthOArray(animations)
int ret = ODatabase.newOArray() ;easy way to get a new JArray/OArray

while i < max
string pos = ODatabase.getPositionData(ODatabase.getObjectOArray(animations, i)) 
if StringUtil.Find(pos, "S") == -1 ;This line just means, if the string "pos" does not contain the letter "S". Position data with the letter S marks it as "standing"
ODatabase.appendObjectOArray(ret, ODatabase.getObjectOArray(animations, i)) ;Append that animation to the OArray we're going to return
i += 1
return ret

Essentially, you always start by running ODatabase.getDatabaseOArray() to get the OArray containing all of the animations, and then run filters on that until you get what you need. See OAiScript for these types of examples. To see all of the tools ODatabase gives you, open up Odatabasescript and start reading!

2 more notes: the database is written to disk and unloaded from memory when an animation is not running to save space. You can read the database file at ~\Documents\My Games\Skyrim Special Edition\JCUser\ODatabase.json
If you ever have to read data from the database while no animation is running, please run ODatabase.unload() when you're done. If you leave the database in memory, and the user saves, it will keep ~200 kbs of data to their SKSE cosave until they eventually start another scene and let the database unload. Having 200 extra kbs in your cosave isn't a huge deal, but it may slow down save times by about 1 second.

Article information

Added on

Edited on

Written by