-
Notifications
You must be signed in to change notification settings - Fork 37
Feature Overview
First and foremost, the game will not crash no matter what data you pass into JContainer functions. The following happens if a function gets called with invalid input (when input cannot be handled properly):
- All functions returning new containers return zero identifier. For. ex
JValue.readFromFile("")returns 0 because of an invalid file path. Zero identifier means non-existing object. It’s ok to pass it into other functions - in that case the function will return the default value. - All functions that read container contents (such as
getFlt,solveFlt,getStr,count,allKeys,getObj,solveObjand etc.) return the default value. For function that return an integer or float, the default value is 0, for functions that return a string the default value is "", for functions that return a form the default value isNone, and for functions that return a container the default value is 0.
Every container object persists in save file until the container gets destroyed. When a save is performed, all objects are saved and all objects are resurrected when the save file gets loaded.
The feature allows store or load a container's contents (and every container it references) into or from a JSON-formatted, UTF-8 encoded (without BOM) file.
Most of Papyrus-JSON conversion is straightforward:
| Papyrus | JSON |
|---|---|
| JArray | array, [] |
| JMap | object, {} |
| JFormMap | object, {"__metaInfo": {"typeName": "JFormMap" } } |
| JIntMap | object, {"__metaInfo": {"typeName": "JIntMap" } } |
| Form | string, "__formData|PluginName.esp|formId" or "__formData||formId" in case of dynamic, 0xff* forms. formId is either hex number prefixed with 0x either decimal number. |
| Integer | number |
| Real | number |
| String | string |
| None | null |
| Integer, if true then 1 otherwise 0 | boolean, JSON -> Papyrus direction only |
| Link | string, "__reference|.path.to[10].another.object" |
JSON is a tree structure, but JC allows serialize graphs, even cyclic one. JC does not create duplicates during serialization - if an object was already serialized, and can be met few times during serialization traversal, a link to already serialized object gets created. For example, an array which references itself will be encoded into:
["__reference|"]Another example:
{
"key1": [[]],
"link-to-inner-array": "__reference|.key1[0]"
}More examples. In a result of this:
int playerData = JMap.object()
JMap.setForm(playerData, "actor", Game.GetPlayer())
JMap.setForm(playerData, "name", Game.GetPlayer().GetName())
JMap.setInt(playerData, "level", Game.GetPlayer().GetLevel())
JValue.writeToFile(playerData, JContainers.userDirectory() + "playerInfo.txt")we'll got a file at MyGames/Skyrim/JCUser/playerInfo.txt containing following lines:
{
"actor": "__formData|Skyrim.esm|0x14",
"name": "Elsa",
"level": 2
}Deserialization example:
int data = JValue.readFromFile(JContainers.userDirectory() + "playerInfo.txt")
int level = JMap.getInt(data, "level")
form player = JMap.getForm(data, "actor")
string name = JMap.getStr(data, "name")Given the following structure and we need to retrieve "form" value of Mark's eyes:
{
"Mark": {
"eyes": {
"form": "__formData|Skyrim.esm|0xe1e2",
"enabled": false,
"probability": 89
},
"beard": {
"form": "__formData|Skyrim.esm|0xbea1d",
"enabled": false,
"probability": 43
}
}
}int root = ...
Form eyeForm = JMap.getForm(JMap.getObj(JMap.getObj(root, "Mark"), "eyes"), "form")Wouldn't it be simpler to retrieve this value by specifying a path, like ".Mark.eyes.form", rather than doing this? Obviously, this is where path resolving comes into the scene.
Path resolving is the feature that allows travel from root objects to their sub-objects and their values, obtain or assign values. Everything in JContainers is an object (and a container), thus everything supports path resolving. This includes JDB and JFormDB.
The feature implemented via groups of solve<Type> and solve<Type>Setter functions. All the functions accept path parameter, a string - it is a set of joined path elements: element1, element2, .. elementN and each path element is either [Index], [formId], [__formData|Plugin|formId"] in case element accesses JArray, JFormMap or JIntMap container or .stringKey in case element accesses JMap container.
The functions will fail to obtain or set a value if:
- One of the elements in the path doesn't exist in the structure. Exception is
solve<Type>Setterfunctions which are allowed to alter the structure in some cases. Read below. - A path tries to access a value of type
TypeA, but real type of the value isTypeB. Exception is Float, Int types, andsolve<Type>Setterfunctions which are allowed to change value type. - Path element like
[N]accesses an empty array or the value ofNis greater than the size of the array. Out of bounds access. - Path element like
.keyaccesses JArray, JIntMap or JFormMap containers. - Path element like
[N]accesses JMap container.
Usage of solveForm, solveIntSetter:
Form eyeForm = JValue.solveForm(root, ".Mark.eyes.form")
Bool succeed = JValue.solveIntSetter(root, ".Mark.beard.probability", 25)The group of solve<Type>Setter functions changes (assigns) values. The functions accept path and an optional createMissingKeys argument - if set to True, it will try to modify a structure, insert missing (key, value) pairs into it, create and insert any missing JMap, JIntMap, JFormMap containers in its attempt to not fail. The functions still may fail. For instance, it's simply impossible to insert (string-keyA, value) pair into JArray container.
For instance
JValue.solveIntSetter(root, ".Mark.beard.probability", 25)
-- Creates missing nested objects and (key, value) pairs
JValue.solveIntSetter(root, ".Maria.beard.probability", 0, createMissingKeys=True){
"Maria": {
"beard": {
"probability": 0
}
},
"Mark": {
"eyes": {
"form": "__formData|Skyrim.esm|0xe1e2",
"enabled": false,
"probability": 25
},
"beard": {
"form": "__formData|Skyrim.esm|0xbea1d",
"enabled": false,
"probability": 43
}
}
}This feature allows apply function of two arguments cumulatively to the items of collection, from left to right, so as to reduce the collection to a single value.
For example:
obj = [1,2,3,4,5,6]
-- will return 6
JValue.evalLuaFlt(obj, "return jc.accumulateValues(jobject, math.max)")
-- returns 21, summ
JValua.evalLuaFlt(obj, "return jc.accumulateValues(jobject, function(a,b) return a + b end)")
obj = [
{"magnitude": -9},
{"magnitude": 11},
{"magnitude": 3}
]
-- returns 11 - the maximum value
JValue.evalLuaFlt(obj, "return jc.accumulateValues(jobject, math.max, '.magnitude')")
obj = {
"a": 10,
"c": "x"
}
-- concatenates keys - and returns "ac" sting
JValue.evalLuaString(obj, "return jc.accumulateKeys(jobject, function(a,b) return a .. b end)")In order to make path resolving and collection operators function properly, string keys should consist of ASCII characters and should not contain the decimal character, square brackets, or the @ character. For instance, the following code will fail to work:
obj = { "invalid.key" : {"k": 10} }
solveInt(map, ".invalid.key.k") is 0
-- although it's still possible to access that value in the traditional way:
getObj(map, "invalid.key") is {"k": 10}This convention applies to every key string, not just the JMap key. It affects JFormDB storage name and keys as well as JDB.setObj key. Key naming shouldn't matter if you don't use path resolving.
Functions that handle numbers (getFlt, solveFlt, getInt, solveInt) will convert the numbers they handle into their respective types. For example, getFlt will return a float 1.0 if the number passed to it is the int 1. On the other hand, the rest of the get* and solve* functions may fail to perform conversions and will return default values.
Since 3.0 JContainers embeds Lua. Benefits of using Lua:
- any standard lua library functionality available (bitwise operations, math, string manipulation, operating system facilities and etc)
- seek, sort (in development) JArray with user specified predicate
- move some cumbersome Papyrus code into more compact Lua (see
frostfall.uuidfunction in example below)
Important Lua feature status is highly experimental. It's API may change when more functionality will be added.
Typical usage may look like:
- you invoke any lua function with
JValue.evalLuaFlt/Int/Str/Form/Obj:
float pi = JValue.evalLuaFlt(0, "return math.pi")
JValue.evalLuaInt(0, "return bit32.bxor(8, 2, 10)") -- returns 8 xor 2 xor 10obj = [
{ "theSearchString": "a",
"theSearchForm" : "__formData|A|0x14"
},
{ "theSearchString": "b",
"theSearchForm" : "__formData|A|0x15"
}
]
-- returns 1 - an array index where `arrayItem.theSearchString == 'b'`
JValue.evalLuaInt(obj, "return jc.find(jobject, function(x) return x.theSearchString == 'b' end")- you write your own functionality in a Data/SKSE/Plugins/JCData/lua/frostfall/init.lua file:
-- frostfall module depends on jc.count function from 'JCData/lua/jc/init.lua'
local jc = require 'jc'
local frostfall = {}
function frostfall.countItemsLessAndGreaterThan(collection, less, greater)
return jc.count(collection, function(x)
return x < less and x > greater
end)
end
-- generates random guid-string (may return 'd6cce35c-487a-458f-bab2-9032c2621f38' once per billion years)
function frostfall.uuid()
local random = math.random
local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
return string.gsub(template, '[xy]', function (c)
local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
return string.format('%x', v)
end)
end
return frostfallPapyrus:
JValue.evalLuaInt(obj, "return frostfall.countItemsLessAndGreaterThan(jobject, 60, -5)")
string guid = JValue.evalLuaStr(0, "return frostfall.uuid()")Sign into GitHub and press Edit at the top to add any missing information or fix typos and errors. Thanks!