Skip to content

Commit edb8b8a

Browse files
extremeheatrom1504
andauthored
Add primitive variables (#125)
* Add context variables to compiler * new `addVariable` to ProtodefCompiler, `setVariable` to CompiledProtodef * / at top of switch field will notate a context variable * add reserved field * add variables to interpreter, add example * add to API docs * update protodef submodule * update protodef submodule * test tests * Update package.json Co-authored-by: Romain Beaumont <romain.rom1@gmail.com>
1 parent 4431eef commit edb8b8a

File tree

9 files changed

+104
-14
lines changed

9 files changed

+104
-14
lines changed

doc/api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ The `path` is an array of namespace keys which select a path of namespaces to be
2626

2727
See full_protocol.js for an example of usage.
2828

29+
### ProtoDef.setVariable(name, value)
30+
31+
Sets a primitive variable type for the specified `name`, which can be dynamically updated. Can be refrenced in switch statements with the "/" prefix.
32+
2933
### ProtoDef.read(buffer, cursor, _fieldInfo, rootNodes)
3034

3135
Read the packet defined by `_fieldInfo` in `buffer` starting from `cursor` using the context `rootNodes`.
@@ -80,6 +84,10 @@ Add types in `protocol` recursively. The protocol object is an object with keys
8084

8185
The `path` is an array of namespace keys which select a path of namespaces to be added to the protodef object.
8286

87+
### ProtoDefCompiler.addVariable(name, value)
88+
89+
Adds a primitive variable type for the specified `name`, which can be dynamically updated. Can be refrenced in switch statements with the "/" prefix.
90+
8391
### ProtoDefCompiler.compileProtoDefSync(options = { printCode: false })
8492

8593
Compile and return a `ProtoDef` object, optionaly print the generated javascript code.
@@ -94,6 +102,11 @@ sizeOfCtx, writeCtx and readCtx are the compiled version of sizeOf, write and re
94102

95103
It can be used directly for easier debugging/using already compiled js.
96104

105+
### CompiledProtodef.setVariable(name, value)
106+
107+
Sets a primitive variable type for the specified `name`, which can be dynamically updated. Can be refrenced in switch statements with the "/" prefix.
108+
109+
97110
## utils
98111

99112
Some functions that can be useful to build new datatypes reader and writer.

examples/variable.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { ProtoDef } = require('protodef')
2+
const assert = require('assert')
3+
4+
// Create a protocol where DrawText is sent with a "opacity" field at the end only if the color isn't transparent.
5+
const protocol = {
6+
string: ['pstring', { countType: 'varint' }],
7+
ColorPalette: ['container', [{ name: 'palette', type: ['array', { countType: 'i32', type: 'string' }] }]],
8+
DrawText: ['container', [{ name: 'color', type: 'u8' }, { name: 'opacity', type: ['switch', { compareTo: 'color', fields: { '/color_transparent': 'void' }, default: 'u8' }] }]]
9+
}
10+
11+
function test () {
12+
// A "palette" here refers to a array of values, identified with their index in the array
13+
const palette = ['red', 'green', 'blue', 'transparent']
14+
const proto = new ProtoDef()
15+
proto.addTypes(protocol)
16+
// A "variable" is similar to a type, it's a primitive value that can be used in switch comparisons.
17+
proto.setVariable('color_transparent', palette.indexOf('transparent'))
18+
// An example usage is sending paletted IDs, with feild serialization based on those IDs
19+
proto.createPacketBuffer('ColorPalette', { palette })
20+
// Here, "opacity", 0x4 is written *only* if the color isn't transparent. In this case, it is, so 0x4 isn't written.
21+
// At the top is 0x3, the index of the "transparent" color.
22+
const s = proto.createPacketBuffer('DrawText', { color: palette.indexOf('transparent'), opacity: 4 })
23+
assert(s.equals(Buffer.from([3])))
24+
console.log(s)
25+
26+
// Here 4 should be written at the end
27+
const t = proto.createPacketBuffer('DrawText', { color: palette.indexOf('blue'), opacity: 4 })
28+
assert(t.equals(Buffer.from([2, 4])))
29+
}
30+
31+
test()

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"dependencies": {
1717
"lodash.get": "^4.4.2",
1818
"lodash.reduce": "^4.6.0",
19-
"protodef-validator": "^1.2.2",
19+
"protodef-validator": "^1.3.0",
2020
"readable-stream": "^3.0.3"
2121
},
2222
"engines": {

src/compiler.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ class ProtoDefCompiler {
3232
this.sizeOfCompiler.addProtocol(protocolData, path)
3333
}
3434

35+
addVariable (key, val) {
36+
this.readCompiler.addContextType(key, val)
37+
this.writeCompiler.addContextType(key, val)
38+
this.sizeOfCompiler.addContextType(key, val)
39+
}
40+
3541
compileProtoDefSync (options = { printCode: false }) {
3642
const sizeOfCode = this.sizeOfCompiler.generate()
3743
const writeCode = this.writeCompiler.generate()
@@ -70,6 +76,12 @@ class CompiledProtodef {
7076
return writeFn(value, buffer, cursor)
7177
}
7278

79+
setVariable (key, val) {
80+
this.sizeOfCtx[key] = val
81+
this.readCtx[key] = val
82+
this.writeCtx[key] = val
83+
}
84+
7385
sizeOf (value, type) {
7486
const sizeFn = this.sizeOfCtx[type]
7587
if (!sizeFn) { throw new Error('missing data type: ' + type) }
@@ -186,7 +198,7 @@ class Compiler {
186198
getField (name) {
187199
const path = name.split('/')
188200
let i = this.scopeStack.length - 1
189-
const reserved = ['value', 'enum', 'default', 'size']
201+
const reserved = ['value', 'enum', 'default', 'size', 'offset']
190202
while (path.length) {
191203
const scope = this.scopeStack[i]
192204
const field = path.shift()

src/datatypes/compiler-conditional.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ module.exports = {
1010
let code = `switch (${compare}) {\n`
1111
for (const key in struct.fields) {
1212
let val = key
13-
if (isNaN(val) && val !== 'true' && val !== 'false') val = `"${val}"`
13+
if (val.startsWith('/')) val = 'ctx.' + val.slice(1) // Root context variable
14+
else if (isNaN(val) && val !== 'true' && val !== 'false') val = `"${val}"`
1415
code += compiler.indent(`case ${val}: return ` + compiler.callType(struct.fields[key])) + '\n'
1516
}
1617
code += compiler.indent('default: return ' + compiler.callType(struct.default ? struct.default : 'void')) + '\n'
@@ -39,7 +40,8 @@ module.exports = {
3940
let code = `switch (${compare}) {\n`
4041
for (const key in struct.fields) {
4142
let val = key
42-
if (isNaN(val) && val !== 'true' && val !== 'false') val = `"${val}"`
43+
if (val.startsWith('/')) val = 'ctx.' + val.slice(1) // Root context variable
44+
else if (isNaN(val) && val !== 'true' && val !== 'false') val = `"${val}"`
4345
code += compiler.indent(`case ${val}: return ` + compiler.callType('value', struct.fields[key])) + '\n'
4446
}
4547
code += compiler.indent('default: return ' + compiler.callType('value', struct.default ? struct.default : 'void')) + '\n'
@@ -69,7 +71,8 @@ module.exports = {
6971
let code = `switch (${compare}) {\n`
7072
for (const key in struct.fields) {
7173
let val = key
72-
if (isNaN(val) && val !== 'true' && val !== 'false') val = `"${val}"`
74+
if (val.startsWith('/')) val = 'ctx.' + val.slice(1) // Root context variable
75+
else if (isNaN(val) && val !== 'true' && val !== 'false') val = `"${val}"`
7376
code += compiler.indent(`case ${val}: return ` + compiler.callType('value', struct.fields[key])) + '\n'
7477
}
7578
code += compiler.indent('default: return ' + compiler.callType('value', struct.default ? struct.default : 'void')) + '\n'

src/datatypes/conditional.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ module.exports = {
88
function readSwitch (buffer, offset, { compareTo, fields, compareToValue, default: defVal }, rootNode) {
99
compareTo = compareToValue !== undefined ? compareToValue : getField(compareTo, rootNode)
1010
if (typeof fields[compareTo] === 'undefined' && typeof defVal === 'undefined') { throw new Error(compareTo + ' has no associated fieldInfo in switch') }
11-
11+
for (const field in fields) {
12+
if (field.startsWith('/')) {
13+
fields[this.types[field.slice(1)]] = fields[field]
14+
delete fields[field]
15+
}
16+
}
1217
const caseDefault = typeof fields[compareTo] === 'undefined'
1318
const resultingType = caseDefault ? defVal : fields[compareTo]
1419
const fieldInfo = getFieldInfo(resultingType)
@@ -18,7 +23,12 @@ function readSwitch (buffer, offset, { compareTo, fields, compareToValue, defaul
1823
function writeSwitch (value, buffer, offset, { compareTo, fields, compareToValue, default: defVal }, rootNode) {
1924
compareTo = compareToValue !== undefined ? compareToValue : getField(compareTo, rootNode)
2025
if (typeof fields[compareTo] === 'undefined' && typeof defVal === 'undefined') { throw new Error(compareTo + ' has no associated fieldInfo in switch') }
21-
26+
for (const field in fields) {
27+
if (field.startsWith('/')) {
28+
fields[this.types[field.slice(1)]] = fields[field]
29+
delete fields[field]
30+
}
31+
}
2232
const caseDefault = typeof fields[compareTo] === 'undefined'
2333
const fieldInfo = getFieldInfo(caseDefault ? defVal : fields[compareTo])
2434
return tryDoc(() => this.write(value, buffer, offset, fieldInfo, rootNode), caseDefault ? 'default' : compareTo)
@@ -27,7 +37,12 @@ function writeSwitch (value, buffer, offset, { compareTo, fields, compareToValue
2737
function sizeOfSwitch (value, { compareTo, fields, compareToValue, default: defVal }, rootNode) {
2838
compareTo = compareToValue !== undefined ? compareToValue : getField(compareTo, rootNode)
2939
if (typeof fields[compareTo] === 'undefined' && typeof defVal === 'undefined') { throw new Error(compareTo + ' has no associated fieldInfo in switch') }
30-
40+
for (const field in fields) {
41+
if (field.startsWith('/')) {
42+
fields[this.types[field.slice(1)]] = fields[field]
43+
delete fields[field]
44+
}
45+
}
3146
const caseDefault = typeof fields[compareTo] === 'undefined'
3247
const fieldInfo = getFieldInfo(caseDefault ? defVal : fields[compareTo])
3348
return tryDoc(() => this.sizeOf(value, fieldInfo, rootNode), caseDefault ? 'default' : compareTo)

src/protodef.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ class ProtoDef {
103103
}
104104
}
105105

106+
setVariable (key, val) {
107+
this.types[key] = val
108+
}
109+
106110
read (buffer, cursor, _fieldInfo, rootNodes) {
107111
const { type, typeArgs } = getFieldInfo(_fieldInfo)
108112
const typeFunctions = this.types[type]

test/dataTypes/prepareTests.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,22 @@ function arrayToBuffer (arr) {
2828
}
2929

3030
function transformValues (type, values) {
31-
return values.map(value => ({
32-
buffer: arrayToBuffer(value.buffer),
33-
value: type.indexOf('buffer') === 0 ? arrayToBuffer(value.value) : value.value,
34-
description: value.description
35-
}))
31+
return values.map(val => {
32+
let value = val.value
33+
if (type.indexOf('buffer') === 0) {
34+
value = arrayToBuffer(value)
35+
} else if (value) {
36+
// we cannot use undefined type in JSON so need to convert it here to pass strictEquals test
37+
for (const key in value) {
38+
if (value[key] === 'undefined') value[key] = undefined
39+
}
40+
}
41+
return {
42+
buffer: arrayToBuffer(val.buffer),
43+
value,
44+
description: val.description
45+
}
46+
})
3647
}
3748

3849
testData.forEach(tests => {
@@ -47,6 +58,7 @@ testData.forEach(tests => {
4758
types[type] = subtype.type
4859
compiler.addTypesToCompile(types)
4960

61+
subtype.vars?.forEach(([k, v]) => { proto.setVariable(k, v); compiler.addVariable(k, v) })
5062
subtype.values = transformValues(test.type, subtype.values)
5163
subtype.type = type
5264
subTypes.push(subtype)

0 commit comments

Comments
 (0)