Malachite Peripherals
Malachite Peripherals is a NeoForge mod for Minecraft 1.21.1 that connects computer-controlled machines to the physics of Sable constructions. It provides a non-blocking CC: Tweaked bridge for Synaxis devices, a global telemetry API, a radar, a peripheral hub, a ballast block with configurable mass and a chunk loader for flying constructions.
The mod is intended for players who build aircraft, missiles, turrets and other vehicles and control them with Lua programs, and for server operators who need such vehicles to remain loaded and to behave predictably.
Contents
- Features
- Dependencies
- Blocks and items
- Synaxis bridge
- Telemetry API
- Radar
- Peripheral hub
- Ballast
- Chunk loader
- Coordinate frames and units
- Threading model
- Configuration
- Known limitations
- Notes for modpack developers
- Credits
- License
Features
- Synaxis bridge. Synaxis flaps, jet thrusters, turret drive motors and kinetic resistors become CC: Tweaked peripherals. Every setter returns immediately, so a control program can command all devices of a vehicle within a single loop iteration instead of waiting one game tick per call. Turret targets, torques and locks are written directly to the thread-safe Synaxis control ports and take effect without delay.
- Telemetry API. Every computer receives a global
telemetrytable that reports the position, linear and angular velocity, orientation, mass, centre of mass and inertia tensor of the Sable construction on which the computer is located. No block is required, and any number of vehicles can read their own state simultaneously. - Radar. A block that detects Sable constructions and Create contraptions within a configurable range, height and field of view, reports each contact with its position, velocity and size, and notifies attached computers after every update.
- Peripheral hub. A block that relays up to 32 linked peripherals to an adjacent computer through the standard CC: Tweaked wired-modem protocol. Links are stored relative to the hub, so a construction copied with a schematic keeps every link and every peripheral name. The hub also reports the position and orientation of each linked device in the construction's local frame, which allows flight control programs to compute lever arms directly.
- Ballast. An invisible block with a mass between 0.01 and 100000 Sable units, set through an in-game screen or from a computer. The mass contributes to the mass, centre of mass and inertia tensor of the construction, survives saves, blueprints and sub-level assembly, and breaking the block never destroys the construction.
- Chunk loader. A block that keeps a flying Sable construction loaded regardless of its distance from players and, optionally, a ring of world chunks around it.
Dependencies
The mod requires only Minecraft 1.21.1 and NeoForge 21.1.248 or later. All other mods are optional; each one enables a set of features. The blocks are always registered, so worlds remain valid when an optional mod is added or removed; a block whose integration is absent remains in place but has no effect.
| Mod | Version | Enables |
|---|---|---|
| CC: Tweaked | 1.115 or later | All peripherals, the telemetry API, the peripheral hub and the Hub Linker |
| Synaxis | 1.4.3 or later | Peripherals for Synaxis flaps, jet thrusters, dynamic motors and kinetic resistors (together with CC: Tweaked) |
| Sable | any | The chunk loader, ballast mass, telemetry data and radar detection of Sable constructions |
| Create | any | Radar detection of Create contraptions |
The following combinations are useful without the other mods:
- Sable alone: the ballast block (with its in-game mass screen) and the chunk loader.
- CC: Tweaked alone: the peripheral hub.
- CC: Tweaked with Create: the radar, detecting Create contraptions.
When the game starts, the log contains one line for every absent integration, stating which features are disabled.
The mod must be installed on both the client and the server.
Blocks and items
The mod does not currently define crafting recipes. The blocks are available in the Functional Blocks creative tab, the Hub Linker in the Tools and Utilities creative tab, and all of them through the /give command.
| Block or item | Identifier | Purpose |
|---|---|---|
| Radar | malperiph:radar |
Detects moving constructions; peripheral type radar. Its front face indicates the axis of the field of view. |
| Peripheral Hub | malperiph:peripheral_hub |
Relays linked peripherals to an adjacent computer; peripheral types malahit_hub and peripheral_hub. |
| Hub Linker | malperiph:hub_linker |
Creates and removes hub links. |
| Ballast | malperiph:ballast |
Adds a configurable mass to a Sable construction; peripheral type ballast. |
| Chunk Loader | malperiph:chunk_loader |
Keeps a Sable construction and, optionally, the surrounding world chunks loaded. |
Synaxis bridge
The bridge attaches peripherals to existing Synaxis blocks; it adds no blocks of its own. Place a computer, or a wired modem connected to a computer, against a Synaxis device, or link the device to a peripheral hub.
In the tables below, Immediate methods return without waiting for a tick. Queued methods return immediately and are applied on the next server tick in the order in which they were called. Main thread methods wait for the next server tick and should not be called in a fast control loop.
Compact flap (compact_flap)
| Method | Execution | Description |
|---|---|---|
setAngle(degrees) |
Queued | Deflection angle of the flap. |
setTilt(degrees) |
Queued | Tilt of the flap about its tilt axis. |
setLift(k) |
Queued | Lift coefficient, as displayed in the Synaxis interface. Raises an error for a negative or non-finite value. |
setDrag(k) |
Queued | Drag coefficient, as displayed in the Synaxis interface. Raises an error for a negative or non-finite value. |
getLift(), getDrag() |
Immediate | Current coefficients. The value may lag one tick behind a setter that has just been queued. |
getAngle(), getTilt() |
Main thread | Current angle and tilt in degrees. |
A newly placed flap has a lift coefficient of 150 and a drag coefficient of 30. Coefficients set from a computer are saved with the block and appear in the Synaxis interface.
Jet thruster (synaxis_jet)
| Method | Execution | Description |
|---|---|---|
setThrust(newtons) |
Queued | Thrust. |
setHorizontalAngle(radians), setVerticalAngle(radians) |
Queued | Thrust vectoring angles. They take effect only when the thruster has a Synaxis jet rudder and are limited to ±π/2 by Synaxis. |
getThrust(), getHorizontalAngle(), getVerticalAngle() |
Main thread | Current values. |
Dynamic motors (synaxis_revolute_motor, synaxis_joint_motor)
The Synaxis revolute motor (horizontal axis, for example turret traverse) and joint motor (vertical axis, for example gun elevation) share one interface. The peripheral types differ, so a program can locate each axis with peripheral.find.
| Method | Execution | Description |
|---|---|---|
setTarget(value) |
Immediate | Controller target: an angle in radians in angle mode, or an angular velocity in rad/s in speed mode. |
setTorque(newtonMetres) |
Immediate | Feed-forward torque added to the controller output in the force backend. |
setLock(locked) |
Immediate | Manual lock request; holds the current angle. |
getKind() |
Immediate | "revolute" or "joint". |
getAngle(), getSpeed() |
Immediate | Current angle (rad) and angular velocity (rad/s); suitable for polling every tick. |
getAppliedTorque() |
Immediate | Torque applied by the motor during the last tick (N·m). |
getTarget(), getTorque() |
Immediate | Current target and feed-forward torque. |
isConnected(), isLocked(), isAutoLocked() |
Immediate | Assembly and lock state. |
getMode() |
Immediate | true in angle mode, false in speed mode. |
getDriveBackend() |
Immediate | "force" or "joint". |
getStatus() |
Immediate | Table {kind, angle, speed, appliedTorque, target, torque, connected, locked, autoLocked, angleMode, driveBackend}. |
setMode(angleMode) |
Queued | Selects angle mode (true) or speed mode (false); resets the controllers. |
setDriveBackend(name) |
Queued | "force" (PID torque) or "joint" (physics engine joint motor); resets the gains to the recommended profile. |
setGainP(p), setGainI(i), setGainD(d), setPID(p, i, d) |
Queued | PID gains of the force backend. |
setJointPD(p, d) |
Queued | Gains of the joint backend. |
setGravityCompensation(enabled), setJointGravityCompensation(enabled) |
Queued | Gravity compensation in the force and joint backends. |
setCounterTorque(enabled) |
Queued | Reaction torque on the motor's own body in the force backend (enabled by default). |
setJointContacts(enabled) |
Queued | Collisions between the bodies connected by the joint. |
setAutoLock(enabled) |
Queued | Automatic lock at the target angle or at a zero target speed. |
setAutoLockThresholds(enter, exit, speed) |
Queued | Automatic lock thresholds: entry angle (rad), exit angle (rad), target speed (rad/s). |
setJointLimit(min, max[, acrossWrap]) |
Queued | Angle limit of the joint backend, in radians within [-π, π]; acrossWrap permits an arc across ±π. |
clearJointLimit() |
Queued | Removes the angle limit. |
getGainP(), getGainI(), getGainD(), getGravityCompensation() |
Main thread | Current configuration. |
getJointLimit() |
Main thread | Table {enabled, min, max, acrossWrap, angleOffset}. |
collect() |
Main thread | The complete configuration and state in one table. |
local azimuth = peripheral.find("synaxis_revolute_motor")
local elevation = peripheral.find("synaxis_joint_motor")
azimuth.setPID(4.0, 0.1, 0.5)
elevation.setPID(4.0, 0.1, 0.5)
while true do
azimuth.setTarget(math.rad(90))
elevation.setTarget(math.rad(15))
print(azimuth.getAngle(), elevation.getAngle())
sleep(0.05)
end
Kinetic resistor (synaxis_kinetic_resistor)
The kinetic resistor is a transmission on Create shafts: output speed = input speed × ratio.
| Method | Execution | Description |
|---|---|---|
setRatio(ratio) |
Immediate | Gear ratio. Zero stops the output shaft; a negative value reverses it. Synaxis 1.5.0 clamps the ratio to [-2, 2] and replaces a non-finite value with 1. Applied on the next tick. |
getRatio() |
Immediate | Applied ratio. |
getSourceSpeed(), getOutputSpeed() |
Immediate | Input and output shaft speeds in RPM. |
getStatus() |
Immediate | Table {ratio, sourceSpeed, outputSpeed}. |
Telemetry API
Every computer has a global telemetry table, in the same way as os or redstone. It describes the Sable construction on which the computer is located. All methods are immediate: the data is refreshed once per tick and read without waiting.
| Method | Returns |
|---|---|
isValid() |
true if the computer is on a Sable construction and the data is valid. |
getId() |
Stable UUID of the construction, or an empty string. |
getName() |
Name of the construction. |
getPosition() |
{x, y, z}: world position of the computer itself. |
getVelocity() |
{x, y, z}: linear velocity of the construction's centre of mass, world axes, blocks per second. |
getSpeed() |
Magnitude of the linear velocity. |
getAngularVelocity() |
{x, y, z}: angular velocity, world axes, rad/s. The direction is the rotation axis and the magnitude is the rotation rate. |
getOrientation() |
{x, y, z, w}: quaternion that rotates the construction's local frame into world axes. |
getSize() |
{x, y, z}: extent of the construction's bounding box. |
getMass(), getInverseMass() |
Mass in Sable units (an ordinary block has a mass of 1) and its inverse (0 if not available). |
getCenterOfMass() |
{x, y, z}: centre of mass in the construction's local frame. |
getInertiaTensor(), getInverseInertiaTensor() |
3 × 3 matrix as t[row][column], about the centre of mass, local axes. |
getMassProperties() |
{mass, inverseMass, centerOfMass, inertiaTensor, inverseInertiaTensor}. |
getTelemetry() |
{valid, id, name, position, velocity, angularVelocity, orientation, mass, centerOfMass, size}. |
If the computer is not on a Sable construction, or Sable is not installed, isValid() returns false, getPosition() returns the position of the computer block and the remaining methods return neutral values: zero vectors, zero mass, the identity orientation and identity tensors.
if telemetry.isValid() then
local p = telemetry.getPosition()
print(("altitude %.0f speed %.1f mass %.1f"):format(p.y, telemetry.getSpeed(), telemetry.getMass()))
end
Radar
The radar detects Sable constructions (when Sable is installed) and Create contraptions (when Create is installed). It uses flat lists of active constructions rather than a spatial scan, so its range and height are limited only by its settings. The construction that carries the radar is excluded from its contacts. All methods except getPosition are immediate.
| Method | Description |
|---|---|
getContacts(), scan() |
List of contacts from the last update, sorted by distance. |
getContactCount() |
Number of contacts from the last update. |
getPosition() |
{x, y, z}: world position of the radar. Runs on the main thread. |
getConfig() |
{range, height, angle, freq, type, sideBound}. |
getRange(), setRange(blocks) |
Horizontal detection range, 1 to 100000 (default 20000). |
getHeight(), setHeight(blocks) |
Vertical detection limit above and below the radar, 1 to 100000 (default 20000). |
getAngle(), setAngle(degrees) |
Full aperture of the detection cone, 1 to 360 (default 360). |
getFrequency(), setFrequency(ticks) |
Update period, 1 to 200 ticks (default 5). |
getScanType(), setScanType(type) |
"all" (default), "sable" or "contraption". |
isSideBound(), setSideBound(enabled) |
Directional mode: when enabled and the angle is below 360, the cone is centred on the radar's front face. |
setConfig(table) |
Sets several parameters at once, for example setConfig{range = 1500, angle = 120, sideBound = true}. |
Values outside the permitted ranges are clamped. The settings are stored in the block and survive world reloads.
Each contact is a table with the fields kind ("sable" or "contraption"), id, name, world position x, y, z (centre of the bounding box), velocity vx, vy, vz (blocks per second), bounding-box size sx, sy, sz and distance dist from the radar.
After every update each attached computer receives the event radar_contacts with the radar's attachment name and the number of contacts. A single radar may serve several computers.
local radar = peripheral.find("radar")
radar.setConfig{range = 1500, angle = 120, sideBound = true}
while true do
os.pullEvent("radar_contacts")
for _, c in ipairs(radar.getContacts()) do
print(("%s %s %.0f m"):format(c.kind, c.name, c.dist))
end
end
Peripheral hub
The peripheral hub is placed against a computer. It exposes the additional peripheral type peripheral_hub, which CC: Tweaked treats in the same way as a wired modem: peripheral.getNames, peripheral.find, peripheral.wrap, peripheral.getType, peripheral.hasType, peripheral.getMethods, peripheral.call and rednet.open work with linked devices as if they were attached to the computer directly. No changes to programs are required.
Linking
Links are created with the Hub Linker.
| Action | Result |
|---|---|
| Right-click a hub | Selects the hub. |
| Right-click a peripheral | Links the peripheral to the selected hub, or unlinks it if it is already linked. |
| Shift + right-click a hub | Lists the hub's links in chat. |
| Shift + right-click the air | Clears the selection. |
A hub supports up to 32 links, each within 16 blocks of the hub on every axis. Other hubs and wired modems cannot be linked. Each linked device receives the name <type>_<n>, where n is the lowest free number within the hub.
Links are stored in the hub block as offsets in the hub's own frame. A construction copied with a schematic or a structure block, rotated or mirrored, therefore keeps every link and every name, and Sable sub-level assembly does not affect them. The hub re-resolves its links every 10 ticks and raises the peripheral and peripheral_detach events when the set of available devices changes. A device that remains the same object keeps its wrapper, so the rednet channels of a linked modem are not closed.
Methods
In addition to the wired-modem protocol methods (getNamesRemote, isPresentRemote, getTypeRemote, hasTypeRemote, getMethodsRemote, callRemote), the hub provides the following immediate methods:
| Method | Description |
|---|---|
getLinks() |
All links, including offline ones: {name, type, block, online, x, y, z, side}. The offsets are in the hub's frame. |
getGeometry([name]) |
Geometry of all links, or of the named link (nil if it does not exist). |
getPosition() |
{x, y, z}: centre of the hub in the construction's local frame. |
refresh() |
Re-resolves the links on the next tick. |
Each geometry record contains:
| Field | Description |
|---|---|
name, type, block, online |
Link name, peripheral type, block identifier and availability. |
pos |
{x, y, z}: centre of the device block in the construction's local frame. |
offset |
{x, y, z}: pos minus the centre of the hub, local axes. |
facing, dir |
Orientation of the block: side name and unit vector, if the block has an orientation. |
front, left |
Basis vectors of an online Synaxis device. For a flap, front is the angle axis and left is the tilt axis; for a jet thruster, front is the thrust direction. |
normal |
Compact flap only: plate normal at zero angle and tilt. |
The geometry is refreshed every 10 ticks.
local hub = peripheral.find("malahit_hub")
local com = telemetry.getCenterOfMass()
for _, g in ipairs(hub.getGeometry()) do
local r = {x = g.pos.x - com.x, y = g.pos.y - com.y, z = g.pos.z - com.z}
print(g.name, r.x, r.y, r.z)
end
Ballast
The ballast block is invisible and has a configurable mass between 0.01 and 100000 Sable units (default 1; an ordinary block has a mass of 1). With Sable installed, the mass contributes to the mass, centre of mass and inertia tensor of the construction that contains the block.
- Visibility. A marker is displayed over ballast blocks while a ballast item is held. The block can be targeted and broken only while a ballast item or a Hub Linker is held; otherwise it obstructs neither view nor clicks. Its collision shape is always a full block.
- Setting the mass. Right-click a ballast block with a ballast item to open the mass screen; Enter or Done applies the value. Shift + right-click places a block adjacent to the ballast as usual.
- Persistence. The mass is stored in the block and is preserved through world saves, Sable blueprints and sub-level assembly. A broken ballast block drops an item that retains its mass, and the mass is shown in the item tooltip.
- Safety. Breaking a ballast block removes exactly the mass that it contributed, so the construction is never destroyed.
| Method | Description |
|---|---|
setMass(mass) |
Sets the mass. Raises an error for a value outside [0.01, 100000]. Applied on the next tick. |
getMass() |
Configured mass. |
getAppliedMass() |
Mass currently accounted by Sable; equals getMass() one tick after a change, and 1 while the block is not on a construction. |
getMassRange() |
{min = 0.01, max = 100000, default = 1}. |
All ballast methods are immediate.
Chunk loader
The chunk loader has no interface and no in-game settings. When placed on a Sable construction, it keeps the construction loaded regardless of the distance from players, including after a server restart. Each chunk loader uses its own ticket, so removing one loader does not unload other constructions.
It can additionally keep a ring of ordinary world chunks loaded around the construction's current position, so that terrain beneath a distant vehicle remains loaded. The ring is configured in config/malperiph-common.toml (see Configuration). The tickets are released when the block is broken or destroyed.
Coordinate frames and units
- World frame.
telemetry.getPosition(),getVelocity(),getAngularVelocity(), radar contacts andradar.getPosition()use world coordinates and world axes. - Local frame.
telemetry.getCenterOfMass(),telemetry.getInertiaTensor(),hub.getGeometry()andhub.getPosition()use the local frame of the Sable construction (its plot coordinates). A lever arm is thereforepos - centerOfMass, and a local vector is converted into world axes asv_world = q · v_local, whereq = telemetry.getOrientation(). - Units. Distances in blocks, velocities in blocks per second, angles in radians, angular velocities in rad/s, forces in newtons and torques in N·m. The flap methods use degrees, in accordance with the Synaxis interface. Masses are in Sable units, where an ordinary block has a mass of 1.
Threading model
CC: Tweaked runs Lua programs on its own threads. Methods that must touch the world are usually executed on the server thread, which costs one tick per call. The mod avoids this cost:
- Device setters are placed in a queue and applied once per tick on the server thread, in call order.
- Turret targets, torques, locks and resistor ratios are written through the thread-safe ports that Synaxis itself uses from its physics thread.
- Telemetry, radar contacts and hub geometry are assembled on the server thread once per tick or per update and published as immutable snapshots, which Lua reads without waiting.
Only the methods marked "Main thread" above wait for a tick.
Configuration
config/malperiph-common.toml, section chunk_loader:
| Key | Default | Range | Description |
|---|---|---|---|
worldChunks |
true |
Whether world chunks around the construction are kept loaded. false keeps only the construction itself. |
|
surroundingChunkRadius |
0 |
0 to 8 | Ring radius in chunks: 0 is the chunk under the construction, 1 is 3 × 3, 2 is 5 × 5. |
positionCheckIntervalTicks |
2 |
1 to 20 | How often a moving loader checks whether it has entered another chunk. |
chunkTicketChangesPerTick |
4 |
1 to 64 | Maximum number of chunk tickets a loader may add or remove per tick. |
newChunksPerTick |
1 |
1 to 8 | Of those, the maximum number of previously unloaded chunks per tick (terrain generation). |
For small, fast vehicles a radius of 0 or 1 is recommended.
Known limitations
- The ring of world chunks cannot keep pace with constructions that travel faster than approximately one chunk per tick. A larger radius increases the cost without extending coverage ahead of the construction; the construction itself remains loaded in any case.
- Hub links are stored in the hub block and are lost if the hub is broken and placed again. A hub cannot be rotated in place.
- Links are identified by their offset from the hub. A linked device that is moved, even by one block, must be linked again.
- The radar does not check line of sight. A construction behind terrain is reported if it is within the detection volume.
Notes for modpack developers
- The Synaxis bridge accesses Synaxis through block entity identifiers and reflection. When the game starts, the log reports how many Synaxis device types were found. If a Synaxis update renames a device or a method, the affected peripheral stops responding and a warning prefixed with
[malperiph]is written to the log; other features are unaffected. - The ballast mass is applied to Sable through a mixin, which is loaded only when Sable is installed.
Credits
Malachite Peripherals integrates with Synaxis by Rew1nd, CC: Tweaked by SquidDev and contributors, Create by simibubi and the Create team, and Sable by RyanHCode. It contains no code or assets of these mods.
License
Copyright (c) 2026 Graide (Malachite_Project). All Rights Reserved.
The mod may be used in single-player worlds and on servers, shown in videos and streams, and included in modpacks that obtain the unmodified file from an official page of the mod. Programs that use the mod's API may be published under any licence. See LICENSE.txt for the full terms.

