protocol updates, use ProtoDef compiler
This commit is contained in:
parent
1a7205b3d1
commit
60e57fad44
10 changed files with 298 additions and 121 deletions
|
|
@ -1,4 +1,5 @@
|
|||
const fs = require('fs')
|
||||
const { ProtoDefCompiler } = require('protodef').Compiler
|
||||
|
||||
let compile
|
||||
try {
|
||||
|
|
@ -11,5 +12,23 @@ if (compile) {
|
|||
compile('./proto.yml', 'protocol.json')
|
||||
}
|
||||
|
||||
fs.writeFileSync( '../newproto.json', JSON.stringify({ types: require('./protocol.json') }, null, 2) )
|
||||
fs.unlinkSync('./protocol.json') //remove temp file
|
||||
fs.writeFileSync('../newproto.json', JSON.stringify({ types: require('./protocol.json') }, null, 2))
|
||||
fs.unlinkSync('./protocol.json') //remove temp file
|
||||
|
||||
function createProtocol() {
|
||||
const compiler = new ProtoDefCompiler()
|
||||
const protocol = require('../newproto.json').types
|
||||
compiler.addTypesToCompile(protocol)
|
||||
compiler.addTypes(require('../../src/datatypes/compiler-minecraft'))
|
||||
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
|
||||
|
||||
fs.writeFileSync('../read.js', 'module.exports = ' + compiler.readCompiler.generate())
|
||||
fs.writeFileSync('../write.js', 'module.exports = ' + compiler.writeCompiler.generate())
|
||||
fs.writeFileSync('../size.js', 'module.exports = ' + compiler.sizeOfCompiler.generate())
|
||||
|
||||
const compiledProto = compiler.compileProtoDefSync()
|
||||
return compiledProto
|
||||
}
|
||||
|
||||
console.log('Generating JS...')
|
||||
createProtocol()
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
!StartDocs: Types
|
||||
# !StartDocs: Types
|
||||
|
||||
BehaviourPackInfos: []li16
|
||||
uuid: string
|
||||
version: string
|
||||
size: lu64
|
||||
length: lu64
|
||||
content_key: string
|
||||
sub_pack_name: string
|
||||
content_identity: string
|
||||
|
|
@ -12,7 +12,7 @@ BehaviourPackInfos: []li16
|
|||
TexturePackInfos: []li16
|
||||
uuid: string
|
||||
version: string
|
||||
size: lu64
|
||||
length: lu64
|
||||
content_key: string
|
||||
sub_pack_name: string
|
||||
content_identity: string
|
||||
|
|
@ -71,11 +71,11 @@ Item:
|
|||
default:
|
||||
auxiliary_value: zigzag32
|
||||
has_nbt: lu16 =>
|
||||
0xffff: '1'
|
||||
0x0000: '0'
|
||||
_: has_nbt?
|
||||
if 1:
|
||||
nbt_version: u8
|
||||
0xffff: 'true'
|
||||
0x0000: 'false'
|
||||
nbt: has_nbt?
|
||||
if true:
|
||||
version: u8
|
||||
nbt: nbt
|
||||
default: void
|
||||
can_place_on: string[]zigzag32
|
||||
|
|
@ -150,10 +150,10 @@ BlockCoordinates: # mojang...
|
|||
z: zigzag32
|
||||
|
||||
PlayerAttributes: []varint
|
||||
min: lf32
|
||||
max: lf32
|
||||
current: lf32
|
||||
default: lf32
|
||||
min_value: lf32
|
||||
max_value: lf32
|
||||
current_value: lf32
|
||||
default_value: lf32
|
||||
name: string
|
||||
|
||||
Transaction:
|
||||
|
|
@ -360,11 +360,10 @@ ScoreEntries:
|
|||
1: player
|
||||
2: entity
|
||||
3: fake_player
|
||||
_: entry_type?
|
||||
if player or entity:
|
||||
entity_unique_id: zigzag64
|
||||
if fake_player:
|
||||
custom_name: string
|
||||
entity_unique_id: entry_type?
|
||||
if player or entity: zigzag64
|
||||
custom_name: entry_type?
|
||||
if fake_player: string
|
||||
|
||||
ScoreboardIdentityEntries:
|
||||
type: i8 =>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"name": "length",
|
||||
"type": "lu64"
|
||||
},
|
||||
{
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"name": "length",
|
||||
"type": "lu64"
|
||||
},
|
||||
{
|
||||
|
|
@ -281,24 +281,24 @@
|
|||
{
|
||||
"type": "lu16",
|
||||
"mappings": {
|
||||
"0": "0",
|
||||
"65535": "1"
|
||||
"0": "false",
|
||||
"65535": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"anon": true,
|
||||
"name": "nbt",
|
||||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "has_nbt",
|
||||
"fields": {
|
||||
"1": [
|
||||
"true": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "nbt_version",
|
||||
"name": "version",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
|
|
@ -431,6 +431,10 @@
|
|||
"type": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "key",
|
||||
"type": "varint"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": [
|
||||
|
|
@ -576,19 +580,19 @@
|
|||
"container",
|
||||
[
|
||||
{
|
||||
"name": "min",
|
||||
"name": "min_value",
|
||||
"type": "lf32"
|
||||
},
|
||||
{
|
||||
"name": "max",
|
||||
"name": "max_value",
|
||||
"type": "lf32"
|
||||
},
|
||||
{
|
||||
"name": "current",
|
||||
"name": "current_value",
|
||||
"type": "lf32"
|
||||
},
|
||||
{
|
||||
"name": "default",
|
||||
"name": "default_value",
|
||||
"type": "lf32"
|
||||
},
|
||||
{
|
||||
|
|
@ -771,7 +775,7 @@
|
|||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "../has_network_ids",
|
||||
"compareTo": "has_network_ids",
|
||||
"fields": {
|
||||
"true": "zigzag32"
|
||||
},
|
||||
|
|
@ -1386,7 +1390,7 @@
|
|||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "../type",
|
||||
"compareTo": "type",
|
||||
"fields": {
|
||||
"add": [
|
||||
"container",
|
||||
|
|
@ -1512,7 +1516,7 @@
|
|||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "../type",
|
||||
"compareTo": "type",
|
||||
"fields": {
|
||||
"remove": [
|
||||
"container",
|
||||
|
|
@ -1532,39 +1536,27 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"anon": true,
|
||||
"name": "entity_unique_id",
|
||||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "entry_type",
|
||||
"fields": {
|
||||
"player": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "entity_unique_id",
|
||||
"type": "zigzag64"
|
||||
}
|
||||
]
|
||||
],
|
||||
"entity": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "entity_unique_id",
|
||||
"type": "zigzag64"
|
||||
}
|
||||
]
|
||||
],
|
||||
"fake_player": [
|
||||
"container",
|
||||
[
|
||||
{
|
||||
"name": "custom_name",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
]
|
||||
"player": "zigzag64",
|
||||
"entity": "zigzag64"
|
||||
},
|
||||
"default": "void"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "custom_name",
|
||||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "entry_type",
|
||||
"fields": {
|
||||
"fake_player": "string"
|
||||
},
|
||||
"default": "void"
|
||||
}
|
||||
|
|
@ -1618,7 +1610,7 @@
|
|||
"type": [
|
||||
"switch",
|
||||
{
|
||||
"compareTo": "../type",
|
||||
"compareTo": "type",
|
||||
"fields": {
|
||||
"TYPE_REGISTER_IDENTITY": "zigzag64"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"jwt-simple": "^0.5.6",
|
||||
"lodash.merge": "^4.4.0",
|
||||
"prismarine-nbt": "github:extremeheat/prismarine-nbt#le",
|
||||
"protodef": "github:extremeheat/node-protodef#big",
|
||||
"protodef": "github:extremeheat/node-protodef#compiler",
|
||||
"raknet": "git+https://github.com/mhsjlw/node-raknet.git#master",
|
||||
"uuid-1345": "^0.99.7"
|
||||
},
|
||||
|
|
|
|||
39
src/datatypes/compiler-minecraft.js
Normal file
39
src/datatypes/compiler-minecraft.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
const UUID = require('uuid-1345')
|
||||
const minecraft = require('./minecraft')
|
||||
|
||||
module.exports = {
|
||||
Read: {
|
||||
UUID: ['native', (buffer, offset) => {
|
||||
return {
|
||||
value: UUID.stringify(buffer.slice(offset, 16 + offset)),
|
||||
size: 16
|
||||
}
|
||||
}],
|
||||
restBuffer: ['native', (buffer, offset) => {
|
||||
return {
|
||||
value: buffer.slice(offset),
|
||||
size: buffer.length - offset
|
||||
}
|
||||
}],
|
||||
nbt: ['native', minecraft.nbt[0]]
|
||||
},
|
||||
Write: {
|
||||
UUID: ['native', (value, buffer, offset) => {
|
||||
const buf = UUID.parse(value)
|
||||
buf.copy(buffer, offset)
|
||||
return offset + 16
|
||||
}],
|
||||
restBuffer: ['native', (value, buffer, offset) => {
|
||||
value.copy(buffer, offset)
|
||||
return offset + value.length
|
||||
}],
|
||||
nbt: ['native', minecraft.nbt[1]]
|
||||
},
|
||||
SizeOf: {
|
||||
UUID: ['native', 16],
|
||||
restBuffer: ['native', (value) => {
|
||||
return value.length
|
||||
}],
|
||||
nbt: ['native', minecraft.nbt[2]]
|
||||
}
|
||||
}
|
||||
12
src/options.js
Normal file
12
src/options.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Minimum supported version (< will be kicked)
|
||||
const MIN_VERSION = 422
|
||||
// Currently supported verson
|
||||
const CURRENT_VERSION = 422
|
||||
|
||||
|
||||
const defaultOptions = {
|
||||
// https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2
|
||||
version: CURRENT_VERSION
|
||||
}
|
||||
|
||||
module.exports = { defaultOptions, MIN_VERSION, CURRENT_VERSION }
|
||||
|
|
@ -1,29 +1,12 @@
|
|||
const Listener = require('@jsprismarine/raknet/listener')
|
||||
const { ProtoDef, Parser, Serializer } = require('protodef')
|
||||
const { EventEmitter } = require('events')
|
||||
const { createDeserializer, createSerializer } = require('./transforms/serializer')
|
||||
const { Encrypt } = require('./auth/encryption')
|
||||
const { decodeLoginJWT } = require('./auth/chains')
|
||||
const { Connection } = require('./connection')
|
||||
const Options = require('./options')
|
||||
|
||||
var protocol = require('../data/newproto.json').types;
|
||||
|
||||
function createProtocol() {
|
||||
var proto = new ProtoDef();
|
||||
proto.addTypes(require('./datatypes/minecraft'));
|
||||
proto.addTypes(protocol);
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
function createSerializer() {
|
||||
var proto = createProtocol()
|
||||
return new Serializer(proto, 'mcpe_packet');
|
||||
}
|
||||
|
||||
function createDeserializer() {
|
||||
var proto = createProtocol()
|
||||
return new Parser(proto, 'mcpe_packet');
|
||||
}
|
||||
const log = (...args) => console.log(...args)
|
||||
|
||||
class Player extends Connection {
|
||||
constructor(server, connection, options) {
|
||||
|
|
@ -102,20 +85,10 @@ class Player extends Connection {
|
|||
}
|
||||
}
|
||||
|
||||
// Minimum supported version (< will be kicked)
|
||||
const MIN_VERSION = 422
|
||||
// Currently supported verson
|
||||
const CURRENT_VERSION = 422
|
||||
|
||||
const defaultServerOptions = {
|
||||
// https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2
|
||||
version: CURRENT_VERSION,
|
||||
}
|
||||
|
||||
class Server extends EventEmitter {
|
||||
constructor(options) {
|
||||
super()
|
||||
this.options = { ...defaultServerOptions, options }
|
||||
this.options = { ...Options.defaultOptions, options }
|
||||
this.serializer = createSerializer()
|
||||
this.deserializer = createDeserializer()
|
||||
this.clients = {}
|
||||
|
|
@ -123,8 +96,8 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
validateOptions() {
|
||||
if (this.options.version < defaultServerOptions.version) {
|
||||
throw new Error(`Unsupported protocol version < ${defaultServerOptions.version}: ${this.options.version}`)
|
||||
if (this.options.version < Options.MIN_VERSION) {
|
||||
throw new Error(`Unsupported protocol version < ${Options.MIN_VERSION} : ${this.options.version}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +106,7 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
onOpenConnection = (conn) => {
|
||||
console.log('Got connection', conn)
|
||||
log('new connection', conn)
|
||||
const player = new Player(this, conn)
|
||||
this.clients[this.getAddrHash(conn.address)] = player
|
||||
|
||||
|
|
@ -141,12 +114,12 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
onCloseConnection = (inetAddr, reason) => {
|
||||
console.log('Close connection', inetAddr, reason)
|
||||
log('close connection', inetAddr, reason)
|
||||
delete this.clients[this.getAddrHash(inetAddr)]
|
||||
}
|
||||
|
||||
onEncapsulated = (encapsulated, inetAddr) => {
|
||||
console.log('Encapsulated', encapsulated, inetAddr)
|
||||
log(inetAddr.address, ': Encapsulated', encapsulated)
|
||||
const buffer = encapsulated.buffer
|
||||
const client = this.clients[this.getAddrHash(inetAddr)]
|
||||
if (!client) {
|
||||
|
|
@ -158,7 +131,7 @@ class Server extends EventEmitter {
|
|||
async create(serverIp, port) {
|
||||
this.listener = new Listener(this)
|
||||
this.raknet = await this.listener.listen(serverIp, port)
|
||||
console.log('Listening on', serverIp, port)
|
||||
log('Listening on', serverIp, port)
|
||||
|
||||
this.raknet.on('openConnection', this.onOpenConnection)
|
||||
this.raknet.on('closeConnection', this.onCloseConnection)
|
||||
|
|
|
|||
|
|
@ -42,21 +42,23 @@ server.on('connect', ({ client }) => {
|
|||
let ids = 0
|
||||
for (var item of CreativeItems) {
|
||||
let creativeitem = { runtime_id: items.length }
|
||||
const has_nbt = !!item.nbt_b64
|
||||
if (item.id != 0) {
|
||||
const hasNbt = !!item.nbt_b64
|
||||
creativeitem.item = {
|
||||
network_id: item.id,
|
||||
auxiliary_value: item.damage || 0,
|
||||
has_nbt: hasNbt|0,
|
||||
nbt_version: 1,
|
||||
has_nbt,
|
||||
nbt: {
|
||||
version: 1,
|
||||
},
|
||||
blocking_tick: 0,
|
||||
can_destroy: [],
|
||||
can_place_on: []
|
||||
}
|
||||
if (hasNbt) {
|
||||
if (has_nbt) {
|
||||
let nbtBuf = Buffer.from(item.nbt_b64, 'base64')
|
||||
let { result } = await NBT.parse(nbtBuf, 'little')
|
||||
creativeitem.item.nbt = result
|
||||
let { parsed } = await NBT.parse(nbtBuf, 'little')
|
||||
creativeitem.item.nbt.nbt = parsed
|
||||
}
|
||||
}
|
||||
items.push(creativeitem)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,52 @@
|
|||
'use strict';
|
||||
var ProtoDef = require('protodef').ProtoDef;
|
||||
var Serializer = require('protodef').Serializer;
|
||||
var Parser = require('protodef').Parser;
|
||||
|
||||
var protocol = require('../../data/protocol.json').types;
|
||||
const { ProtoDefCompiler, CompiledProtodef } = require('protodef').Compiler
|
||||
const { FullPacketParser, Serializer } = require('protodef')
|
||||
|
||||
function createProtocol() {
|
||||
var proto = new ProtoDef();
|
||||
proto.addTypes(require('../datatypes/minecraft'));
|
||||
proto.addTypes(protocol);
|
||||
const protocol = require('../../data/newproto.json').types
|
||||
console.log('Proto', protocol)
|
||||
var compiler = new ProtoDefCompiler()
|
||||
compiler.addTypesToCompile(protocol)
|
||||
compiler.addTypes(require('../datatypes/compiler-minecraft'))
|
||||
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
|
||||
|
||||
return proto;
|
||||
const compiledProto = compiler.compileProtoDefSync()
|
||||
return compiledProto
|
||||
}
|
||||
|
||||
|
||||
function getProtocol() {
|
||||
const compiler = new ProtoDefCompiler()
|
||||
compiler.addTypes(require('../datatypes/compiler-minecraft'))
|
||||
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
|
||||
|
||||
const compile = (compiler, file) => {
|
||||
global.native = compiler.native // eslint-disable-line
|
||||
const { PartialReadError } = require('protodef/src/utils') // eslint-disable-line
|
||||
return require(file)() // eslint-disable-line
|
||||
}
|
||||
|
||||
return new CompiledProtodef(
|
||||
compile(compiler.sizeOfCompiler, '../../data/size.js'),
|
||||
compile(compiler.writeCompiler, '../../data/write.js'),
|
||||
compile(compiler.readCompiler, '../../data/read.js')
|
||||
// compiler.sizeOfCompiler.compile(fs.readFileSync(__dirname + '/../../data/size.js', 'utf-8')),
|
||||
// compiler.writeCompiler.compile(fs.readFileSync(__dirname + '/../../data/write.js', 'utf-8')),
|
||||
// compiler.readCompiler.compile(fs.readFileSync(__dirname + '/../../data/read.js', 'utf-8'))
|
||||
)
|
||||
}
|
||||
|
||||
function createSerializer() {
|
||||
var proto = createProtocol();
|
||||
return new Serializer(proto, 'packet');
|
||||
var proto = getProtocol()
|
||||
return new Serializer(proto, 'mcpe_packet');
|
||||
}
|
||||
|
||||
function createDeserializer() {
|
||||
var proto = createProtocol();
|
||||
return new Parser(proto, 'packet');
|
||||
var proto = getProtocol()
|
||||
return new FullPacketParser(proto, 'mcpe_packet');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDeserializer: createDeserializer,
|
||||
createSerializer: createSerializer,
|
||||
createProtocol: createProtocol
|
||||
};
|
||||
}
|
||||
119
test/serialization.js
Normal file
119
test/serialization.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
const { createDeserializer, createSerializer } = require('../src/transforms/serializer')
|
||||
|
||||
function test() {
|
||||
const serializer = createSerializer()
|
||||
const deserializer = createDeserializer()
|
||||
|
||||
function write(name, params) {
|
||||
const packet = serializer.createPacketBuffer({ name, params })
|
||||
console.log('Encoded', packet)
|
||||
return packet
|
||||
}
|
||||
|
||||
function read(packet) {
|
||||
const des = deserializer.parsePacketBuffer(packet)
|
||||
return des
|
||||
}
|
||||
|
||||
async function creativeTest() {
|
||||
let CreativeItems = require('../../data/creativeitems.json')
|
||||
|
||||
let items = []
|
||||
let ids = 0
|
||||
for (var item of CreativeItems) {
|
||||
let creativeitem = { runtime_id: items.length }
|
||||
if (item.id != 0) {
|
||||
const hasNbt = !!item.nbt_b64
|
||||
creativeitem.item = {
|
||||
network_id: item.id,
|
||||
auxiliary_value: item.damage || 0,
|
||||
has_nbt: hasNbt,
|
||||
nbt: { version: 1 },
|
||||
blocking_tick: 0,
|
||||
can_destroy: [],
|
||||
can_place_on: []
|
||||
}
|
||||
if (hasNbt) {
|
||||
let nbtBuf = Buffer.from(item.nbt_b64, 'base64')
|
||||
let { result } = await NBT.parse(nbtBuf, 'little')
|
||||
|
||||
const buf = NBT.writeUncompressed(result, 'littleVarint')
|
||||
|
||||
console.log(nbtBuf, buf, JSON.stringify(result))
|
||||
|
||||
console.log('\n')
|
||||
|
||||
let res2 = await NBT.parse(buf, 'littleVarint')
|
||||
console.log(JSON.stringify(result), JSON.stringify(res2.result))
|
||||
console.assert(JSON.stringify(result) == JSON.stringify(res2.result), JSON.stringify(result), JSON.stringify(res2.result))
|
||||
|
||||
console.log('\n')
|
||||
|
||||
creativeitem.item.nbt.nbt = result
|
||||
}
|
||||
}
|
||||
|
||||
items.push(creativeitem)
|
||||
console.log(JSON.stringify(creativeitem))
|
||||
// console.log(JSON.stringify(creativeitem))
|
||||
|
||||
var s = write('creative_content', { items: [creativeitem] })
|
||||
var d = read(s).data.params
|
||||
|
||||
// console.log(JSON.stringify(d), JSON.stringify(s))
|
||||
// if (JSON.stringify(d) != JSON.stringify(creative_content)) throw 'mismatch'
|
||||
}
|
||||
}
|
||||
|
||||
async function creativeTst() {
|
||||
var creativeitem = {
|
||||
"runtime_id": 1166,
|
||||
"item": {
|
||||
"network_id": 403,
|
||||
"auxiliary_value": 0,
|
||||
"has_nbt": true,
|
||||
"nbt": {
|
||||
"version": 1,
|
||||
"nbt": {
|
||||
"type": "compound",
|
||||
"name": "",
|
||||
"value": {
|
||||
"ench": {
|
||||
"type": "list",
|
||||
"value": {
|
||||
"type": "compound",
|
||||
"value": [
|
||||
{
|
||||
"id": {
|
||||
"type": "short",
|
||||
"value": 0
|
||||
},
|
||||
"lvl": {
|
||||
"type": "short",
|
||||
"value": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"blocking_tick": 0,
|
||||
"can_destroy": [],
|
||||
"can_place_on": []
|
||||
}
|
||||
}
|
||||
|
||||
var s = write('creative_content', { items: [creativeitem] })
|
||||
var d = read(s).data.params
|
||||
console.log(JSON.stringify(d))
|
||||
}
|
||||
|
||||
|
||||
creativeTst()
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
test()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue