-
Notifications
You must be signed in to change notification settings - Fork 24
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
,solveObj
and 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")
This feature simplifies an access to values of nested objects via group of solve*
and solve*Setter
functions. Each function takes path specifier, which determines in which key to search for a value.
Path 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 or .stringKey
in case element accesses JMap.
For example:
solveInt(objectA, ".keyA[4].keyB")
retrieves a value which is associated with keyB of JMap, which located at 4-th index of JArray, which is associated with keyA of objectA-JMap. Huh.
solve*Setter
changes (assigns) a value. Also there is an optional createMissingKeys
argument - if enabled, will insert any missing JMap key during path traversal. For example, calling solveFltSetter(objectA, ".key1.key2", 3.14, true)
on an empty objectA will create new JMap B containing {"key2", 3.14}
pair and associate objectA with new JMap B (i.e. {"key1", {"key2": 3.14}}
structure will be created). solve*Setter
fails if createMissingKeys
is disabled and any key in the path is missing.
More examples:
info = {
"classicPreset" : {
"campfileLighting" : "Automatic"
},
"numbers" : [0, 1, 2, 3]
}
string lightingType = JValue.solveStr(info, ".classicPreset.campfileLighting")
JValue.solveStrSetter(info, ".classicPreset.campfileLighting", "Non-Automatic")
int firstNumber = JValue.solveInt(info, ".numbers[0]")
JValue.solveIntSetter(info, ".numbers[0]", 10)
Important Collection operators is deprecated feature and will be replaced with Lua. Avoid use of deprecate features!
This feature allows executing functions on collection (container) elements. It's accessible via solve* functions. Syntax:
- @function
- @function.path.to.element
- path.to.container@function
- path.to.container@function.path.to.element
path.to.container - the path to the collection you want to retrieve.
function - the function that will be applied on each element of the collection. Currently these functions are implemented:
- minNum, maxNum (search for min or max number, works with any number type (int or float))
- minFlt, maxFlt - the same as above, but accepts float values only
- minInt, maxInt - the same as above, but accepts integer values only
path.to.element - the path to the element you want to retrieve.
Examples (pseudo-code):
obj = [1,2,3,4,5,6]
solveFlt(obj, "@maxNum") is 6
solveFlt(obj, "@minNum") is 1
obj = { "a": [1], "b": {"k": -100}, "c": [3], "d": {"k": 100}, "e": [5], "f": [6] }
solveFlt(obj, "@maxNum.value[0]") is 6
solveFlt(obj, "@minNum.value[0]") is 1
solveFlt(obj, "@maxNum.value.k") is 100
solveFlt(obj, "@minNum.value.k") is -100
obj = {
"mapKey": { "a": [1], "b": {"k": -100}, "c": [3], "d": {"k": 100}, "e": [5], "f": [6] }
}
solveFlt(obj, ".mapKey@maxNum.value.k") is 100
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.uuid
function 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 10
obj = [
{ "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 frostfall
Papyrus:
JValue.evalLuaInt(obj, "return frostfall.countItemsLessAndGreaterThan(jobject, 60, -5)")
string guid = JValue.evalLuaStr(0, "return frostfall.uuid()")