Skip to content

Releases: OmniacDev/MCBE-IPC

Release v3.0.0

14 Jan 19:58
Compare
Choose a tag to compare
Release v3.0.0 Pre-release
Pre-release

Moving from JSON to Binary

With v3.0.0, much of the library has been restructured to use binary serialization, rather than the previous JSON strings.

This brings a few performance improvements for complex value types, such as nested objects, and it also has a reduced footprint in the scriptevent messages, allowing for more data to be sent in a set amount of time, since less space is wasted with unnecessary data.

I personally find this version way nicer than previous versions, but I would like to hear everyone's opinions, and any suggestions I could make to further improve upon this.

This version will of course NOT be compatible with previous versions.

Serializable

Lets begin by exploring the "Serializable" types. These are types that can be serialized/deserialized, to and from their binary forms. v3.0.0 comes with a few of these already implemented (These are exported by the Proto class);

  • Int8
  • Int16
  • Int32
  • UInt8
  • UInt16
  • UInt32
  • Float32
  • Float64
  • VarInt
  • String
  • Boolean
  • UInt8Array
  • Date
  • Object
  • Array
  • Tuple
  • Optional
  • Map
  • Set

You can use these serializable types to create serializers for almost any type of value. Most of the types are simple static Serializable instances, however, a few of them are methods, used to create a new Serializable that reflects the input type structure.

For example, you can use the Proto.Object method, to create a serializer for an arbitrary object type, such as the ConnectionSerializer used by the Connection & ConnectionManager classes for communication;

const ConnectionSerializer = Proto.Object({
  from: Proto.String,
  bytes: Proto.UInt8Array
})

Which is then used internally with NET.emit;

yield* NET.emit(`ipc:${$._to}:${channel}:send`, ConnectionSerializer, {
  from: $._from, // $._from is a string
  bytes // bytes is a UInt8Array, maybe encrypted
})

Advanced

The Serializable system is pretty flexible, and allows users to define fully custom serializers. The Serializable interface is simple, having only two methods, serialize & deserialize. These are both generator functions;

export interface Serializable<T = any> {
  serialize(value: T, stream: ByteArray): Generator<void, void, void>
  deserialize(stream: ByteArray): Generator<void, T, void>
}

The ByteArray class is a simple dynamic wrapper over a UInt8Array, with a few extra methods, such as read and write.

read takes a parameter for the number of bytes to read, and then returns an array of bytes (numbers in range 0-255) with that many elements. If the number of elements requested is more than the remaining elements in the array, it only returns the remaining ones.

write takes any amount of bytes (numbers in range 0-255), and writes them onto the end of the buffer.

General

Now that we've explored the Serializables a bit, we can move on to the changes to the methods within the IPC & NET modules.

All the IPC methods now take a Serializable as an argument, with some taking more than one;

// serializer argument for the value being sent
function send<T>(channel: string, serializer: NET.Serializable<T>, value: T): void
// serializer argument for value being sent, and deserializer for expected return value.
function invoke<T, R>(channel: string, serializer: NET.Serializable<T>, value: T, deserializer: NET.Serializable<R>): Promise<R>;

These changes apply for all methods, including those in Connection and ConnectionManager classes.

Similarly, the NET.emit and NET.listen functions also take serializers as an argument.

Type Distribution

In previous versions, sharing types was essentially non-existant, due to the annoying way JS works. However, in this version, it has become INCREDIBLY easy. Since the system now relies on serializers which exist at runtime, we can create and export them from common files, that can be easily distributed to other users.

For example, this simple file can be placed in the same folder as the main IPC files, and can then be used in methods etc. This can then be shared as a file that the user just has to drop in, with no annoying modifications to the main IPC files needed to get it working.

import { Proto } from "./ipc";

export const TestSerializer = Proto.Object({
    value: Proto.String,
    tick: Proto.VarInt
})

What's Changed

Full Changelog: v2.0.0...v3.0.0

v2.0.0

02 Nov 02:10
Compare
Choose a tag to compare

Release v2.0.0

A lot has been optimized and re-implemented, so this version will be incompatible with any previous versions.

NET Module

The NET module has been heavily refactored, some key changes including:

  • Custom data serialization, fully optimized for scriptevents
  • Payload has been moved into the scriptevent ID, to maximize data throughput
  • Scriptevent ID is now fully serialized, allowing completely custom namespaces, and supporting the payload
  • Event listeners have been optimized to minimize delay
  • Data fragmentation has been heavily optimized, reducing memory and time usage

For advanced users, the NET module is also now available for use.

import { system } from '@minecraft/server'
import { NET } from './IPC/ipc'

// This listener will behave like IPC.once
const listen_terminator = NET.listen('namespace', 'event', 'channel', function* (args) {
    console.log(args[0]) // 'args'
    listen_terminator() // terminate listener
})

system.runJob(NET.emit('namespace', 'event', 'channel', ['args']))

What's Changed

Full Changelog: v1.7.0...v2.0.0

v1.7.0

22 Oct 08:35
9b4d4d8
Compare
Choose a tag to compare

Release v1.7.0

Optimizations

  • Fixed a few oversights in previous versions, where sending large amounts of encrypted data caused performance issues.
  • Other functions have also been optimized to increase their efficiency when handling large amounts of data.
  • Adjusted the fragmentation to only limit the scriptevent's message size not the entire command.

Additions

  • ConnectionManager has two new methods; send and invoke
  • Connection has three new methods; on, once, and handle
  • Connection also has a new terminate method. This can be used to close existing connections.

Other

Removed the CONFIG export, all values are now hardcoded.
Since the project is getting large, I've added a minified JS file for those who want to keep their pack sizes low.

What's Changed

Full Changelog: v1.6.0...v1.7.0

v1.6.0

02 Oct 00:58
5e9da3d
Compare
Choose a tag to compare

Auto Sizing

v1.6.0 addresses issues with the command length limit.

IPC will now dynamically resize the data being sent so it never goes above the MAX_CMD_LENGTH value in CONFIG.FRAGMENTATION (default: 2048, modification is not recommended).

Previously, it tested the data string against a static MAX_STR_LENGTH value, however, this didn't take into account the other parts of the command, such as the channel, which resulted in inconsistencies and lost efficiency.

Other

  • Only accept script events from server

What's Changed

Full Changelog: v1.5.0...v1.6.0

v1.5.0

23 Sep 12:29
8a66689
Compare
Choose a tag to compare

Encryption

This update introduces encryption for direct IPC connections.

Usage

ConnectionManager

The ConnectionManager constructor now accepts an optional force_encryption parameter. By enabling it, every incoming connection must be encrypted.

IPC.ConnectionManager.constructor(id: string, force_encryption?: boolean)

Example:

import IPC from 'ipc'

// Encrypted ConnectionManager. All incoming connections are encrypted
const encrypted_manager = new IPC.ConnectionManager('UUID', true)

// Non-encrypted ConnectionManager. All incoming connections are raw
const manager = new IPC.ConnectionManager('UUID'/*, false*/)

Connection

The connect() method on the ConnectionManager now accepts an optional encrypted parameter.

IPC.ConnectionManager.connect(to: string, encrypted?: boolean, timeout?: number): Promise<Connection>

Example:

import IPC from 'ipc'

const manager = new IPC.ConnectionManager('UUID')

manager.connect('Other UUID', true).then(connection => {
  // connection is encrypted
})

manager.connect('Other UUID'/*, false*/).then(connection => {
  // connection is not encrypted
})

Note

If a ConnectionManager has force_encryption enabled, this does NOT mean each Connection made by that manager is also encrypted, as force_encryption only applies to incoming connections, not outgoing ones.

How It Works

A Diffie-Hellman Key Exchange is used during the 2-way handshake between the ConnectionManagers to generate a shared private key.

image

What's Changed

Full Changelog: v1.4.1...v1.5.0

v1.4.1

07 Sep 03:07
Compare
Choose a tag to compare

Patch: Direct handle returns incorrect argument

Full Changelog: v1.4.0...v1.4.1

v1.4.0

05 Sep 07:29
48bcc3f
Compare
Choose a tag to compare
  • Added functionality for direct communication between packs.

    Both packs must use the ConnectionManager for direct communication

    // Manages direct communication. Input should be the UUID of your pack, or some other unique id
    const manager = new IPC.ConnectionManager('UUID')
    
    // ConnectionManager handles listening to channels
    manager.on('channel', args => {}) 
    manager.once('channel', args => {})
    manager.handle('channel', args => {})
    
    // Connect to other packs using their unique ID
    manager.connect('Other UUID').then(connection => {
      // Connections handle sending
      connection.send('channel', 'test_message')
      connection.invoke('channel').then(result => {})
    })

    Example

    // PACK 1
    const manager = new IPC.ConnectionManager('5a456e5c-b064-4ea4-9626-8bb59b7dec0c')
    
    // Connect to Pack 2
    manager.connect('6aad99ef-937d-49ac-a7c2-6d5528eda113').then(() => {
        console.warn('Connected to Pack 2')
    })
    
    // Listen for messages on direct channel
    manager.on('direct_message', message => {
        console.warn(message)
    })
    // PACK 2
    const manager = new IPC.ConnectionManager('6aad99ef-937d-49ac-a7c2-6d5528eda113')
    
    // Connect to Pack 1
    manager.connect('5a456e5c-b064-4ea4-9626-8bb59b7dec0c').then(connection => {
        console.warn('Connected to Pack 1')
    
        // Send a direct message to Pack 1
        connection.send('direct_message', 'Direct Message from Pack 2 to Pack 1')
    })

Full Changelog: v1.3.1...v1.4.0

v1.3.1

02 Sep 09:36
Compare
Choose a tag to compare

Patch: use correct default size

Full Changelog: v1.3.0...v1.3.1

v1.3.0

02 Sep 07:44
Compare
Choose a tag to compare
  • Many optimisations
  • Increased MAX_STR_SIZE for payloads

Full Changelog: v1.2.0...v1.3.0

v1.2.0

31 Aug 15:46
Compare
Choose a tag to compare
  • Removed Header, and moved necessary information into main payload
  • Renamed Contents to Payload, now includes the channel and if it is the last payload

Slightly optimizes the size of the scriptevent messages, and removes one unnecessary scriptevent call

Full Changelog: v1.1.1...v1.2.0