From 73a9a6dbdb8c664f85dd598a26b76e8195c79304 Mon Sep 17 00:00:00 2001 From: templeofshadow Date: Thu, 25 Jun 2026 03:18:13 -0500 Subject: [PATCH] initial commit --- config.lua | 3 + docs.md | 202 +++++++++++++++++++ main.lua | 547 ++++++++++++++++++++++++++++++++++++++++++++++++++ thrusters.lua | 27 +++ 4 files changed, 779 insertions(+) create mode 100644 config.lua create mode 100644 docs.md create mode 100644 main.lua create mode 100644 thrusters.lua diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..ab8eb77 --- /dev/null +++ b/config.lua @@ -0,0 +1,3 @@ +return { + thrusterConfigPath = "/config/thrusters.lua" +} \ No newline at end of file diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..75f3482 --- /dev/null +++ b/docs.md @@ -0,0 +1,202 @@ +# Implementation Docs + +## Redstone Mode + +Total redstone inputs: 22 +8x tilt signals +6x accelerometer signals +4x direction signals +1x altitude signal +1x downwards thrust signal +1x fore thrust signal +1x aft thrust signal + +Total required redstone outputs: at least 8 +4x for four large thrusters +8x for at least eight small attitude thrusters (4x lateral, 4x pitch/roll) + +## Required functionality + +- Achieve target angles for tilt values (target is 0) +- Achieve target values for lateral velocity (when no thrust is being applied, 0) +- Achieve target values for for/aft velocity when thrust is applied +- When direction indicators are referenced and autopilot is on, keep all direction indicator values beside "fore" at 0 +- A monitor that displays the following values: + - Directional thrust values (0-15 & 0-100%) + - Rotational thrust values (0-15 & 0-100%) + - Velocity values + - Altitude values + - Navigation target values (if applicable) + - Autopilot controls + +## Create: Avionics relevant Peripheral methods + +When Create: Avionics is installed, all sensors get direct peripherals for CC:T + +Relevant peripherals: + +- Velocity Sensor +- Navigation Table +- Gimbal Sensor +- Altitude Sensor +- Analog Transmission + +### Altitude Sensor implementations + +> Peripheral ID: altitude_sensor + +- `getHeight()`: get the sensor's Y coordinate +- `getAirPressure()`: get the local air pressure at the sensor's altitude +- `getVerticalSpeed()`: get the sensor's vertical speed (positive is ascending, negative is descending) + +### Navigation Table implementations + +> Peripheral ID: navigation_table + +- `hasTarget()`: Check whether the table has resolved a target +- `getTargetType()`: returns a string target type ID, or nil if no item is held + +- `getTargetMetadata()`: returns per-type metadata for the target. + +> Metadata schema: +> +> - `simulated:compass`: `kind` ("lodestone"|"spawn"), `sublevel_id` (string, lodestone only) +> - `sublevel_id` is the tracker UUID +> - `simulated:recovery_compass`: `placer_uuid` (string, optional) +> - `placer_uuid` is absent until a player has placed the compass into the table +> - `simulated:map`: `map_id` (number, optional) +> - `map_id` is absent if the held map carries no MAP_ID data component (e.g. a blank map) +> - `simulated:magnet`: --- +> - static target (10 blocks north of the table) + +- `getRelativeAngle()`: get the relative angle to the target, in degrees + +- `getRelativeAngleRad()`: get the relative angle to the target, in radians + +- `getBearing()`: get the forward-error bearing to the target, in degrees + - Forward-error bearing: + - 0 -> target is straight ahead of the blocks arrow (direct fore) + - +90 -> target is to the right (direct starboard) + - -90 -> target is to the left (direct port) + - +-180 -> target is directly behind (direct aft) + +- `getBearingRad()`: get the forward-error bearing to the target, in randians + +- `getDistanceToTarget()`: get the distance to the resolved target + +- `getClosureRate()`: gets the rate at which the table is closing on the target in blocks/sec (positive is approaching, negative is leaving) + +- `getVerticalOffsetToTarget()`: gets the vertical offset between the target and the table (the difference of `target.y - self.y`) + +- `getOrientation()`: gets the host sub-level's orientation as a quaternion {x, y, z, w} + +> Matches JOML's constructor and the convention used by CC quaternion libraries (e.g. TechTastic/Advanced-Math) + +- `getHeading()`: Gets the host sub-level's heading in degrees + +> 0 degrees refers to world +Z, minecraft south + +- `getHeadingRad()`: gets the host sub-level's heading in radians + +### Gimbal Sensor + +> Peripheral ID: gimbal_sensor + +- `getAngles()`: get the contraption's pitch and roll angles in degrees + - xAngle: rotation about body-X (pitch). 0 degrees is aligned with world-up + - zAngle: rotation about body-Z (roll). 0 degrees is aligned with world-up + - returns {xAngle, zAngle} +- `getAnglesRad()`: get the contraption's pitch and roll angles in radians +- `getAngularRates()`: get the contraption's angular velocity in degrees/sec + - wx: pitch rate + - wy: yaw rate + - wz: roll rate + - returns {wx, wy, wz} +- `getAngularRatesRad()`: get the contraption's angular velocity in radians/sec +- `getGravity()`: gets the local gravity vector in body frame, in m/s^2 + - returns {gx, gy, gz} +- `getLinearAcceleration()`: get the contraption's proper acceleration in body frame, in m/s^2 + - returns {ax, ay, az} + +### Velocity Sensor + +> Peripheral ID: velocity_sensor + +- `getVelocity()`: gets the velocity component along the sensor's axis + - returns signed velocity in m/s + +> note: returns 0 if the magnitude is below 0.05 m/s + +- `getAxis()`: returns the body-frame axis the sensor measures along + - returns string "x", "y", and "z" + +### Analog Transmission + +> Peripheral ID: analog_transmission +> +> These are used as controllers for rotationally-powered thruster blocks + +- `getSignal()`: get the current signal driving the transmission ratio + - returns an integer between 0 and 15 +- `setSignal(signal)`: set the signal driving the transmission ratio + +> Note: flips externallyControlled, even when signal == current +> +> The documented way to get control without changing the value is calling `setSignal(getSignal())`. + +- `releaseSignal()`: Release external control and return signal driving to redstone +- `isExternallyControlled()`: Check whether the transmission is currently under script control +- `getRotationModifier()`: get the current rotation modifier (output:input speed ratio) +- `getOutputSpeed()`: get the output shaft speed +- `getOutputTheoreticalSpeed()`: get the output shaft's theoretical (target) speed +- `getOutputStressImpact()`: get the output-side stress impact (post-ratio kinetic accounting) +- `isOversaturated()`: check whether the transmission is oversaturated +- `getAxis()`: get the transmission's shaft axis name + - returns the axis as string "x", "y", or "z" +- `getSelfId()`: get this block's ID + +> Note: other peripherals' getSourceId or getSubnetworkAnchorId will return this ID when they refer to this block + +- `getSourceId()`: get the ID of the block immediately driving this one, or nil if theis block has no source +- `getSubnetworkAnchorId()`: get the ID of this block's speed-zone anchor -- the gearshift/clutch/speed controller/generator that defines the start of this speed zone. + +> Note: Two blocks share an anchor if they're in the same speed zone. A generator or split-shaft returns its own `getSelfId()` + +- `getNetworkId()`: get the ID of this block's kinetic network. +- `getKind()`: returns one of "generator", "split_shaft", "consumer", or "passthrough" +- `getSpeed()`: get the local rotational speed at this block. +- `hasSource()`: check whether this block is connected to a kinetic source +- `isOverstressed()`: check whether this block's network is overstressed +- `getStressImpact()`: get the stress impact of this block on its network. zero for sources and pure conduit blocks +- `getStressContribution()`: get this block's contribution to its network's stress capacity. Non-zero for sources only + +## Peripheral Notes + +- Analog Transmissions signal speed changes: + - 0: no change from input speed + - 14: maximum change from input speed + - 15: input and output rotational networks are decoupled (generally means no output rotation) + - All that is to say, when being driven properly, the minimum speed is at signal 15, and increasing the speed goes from the lowest nonzero RPM at signal 0 to the highest at signal 14. + +## Implementation + +On startup, call these methods: + +- All Analog Transmission's `getSelfId()` +- All rotationally controlled thruster's `getSubnetworkAnchorId()` + +If the ID returned by a thruster's `getSubnetworkAnchorId()` is also returned by an analog transmission's `getSelfId()`, associated that transmission with the thruster + +Also upon startup, check for a configuration file with other thruster's peripherals, or redstone-controlled thrusters + +Every cycle: + +- Check for new machines in the network. If new machines are found, initialize them similarly to the startup + +- Gimbal Sensor `getAngles()`, `getAngularRates()`, and `getLinearAcceleration()` +- Navigation Table `getHeading()`, `hasTarget()`, and depending on the output of `hasTarget()`: + - if true, call `getBearing()`, `getDistanceToTarget()`, `getClosureRate()`, and `getVerticalOffsetToTarget()` + - if false, continue to the next sensors +- Altitude Sensor `getHeight()`, `getAirPressure()`, `getVerticalSpeed()` +- Velocity Sensor `getAxis()`, `getVelocity` +- Poll input signal strengths for global downwards thrust, and fore and aft thrust diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..f8b0aaf --- /dev/null +++ b/main.lua @@ -0,0 +1,547 @@ +Thrusters = { + thruster0 = { + type = "rotator", + name = nil, + thruster = nil, + transmission = nil, + -- NOTE: + -- affectVectors will have values depending on how the thruster's + -- thrust vector interacts with the vessel's attitude + -- possible directions for yaw: "port"|"star" + -- possible directions for roll: "port"|"star" + -- possible directions for pitch: "up"|"down" + -- possible directions for lateral: "port"|"star"|"fore"|"aft"|"up"|"down" + affectVectors = { + yaw = nil, + pitch = nil, + roll = nil, + lateral = nil + }, + power = 0 + }, + thruster1 = { + type = "thruster", + name = nil, + thruster = nil, + affectVectors = { + yaw = nil, + pitch = nil, + roll = nil, + lateral = nil + }, + power = 0 + } +} -- Table of thruster tables + +ConfigPath = "/config/config.lua" +Config = {} + +-- Autopilot +AutopilotEngaged = false -- Is auto-yaw enabled? +AutoForeAft = false -- Is auto-thrust enabled? +AutopilotDesiredSpeed = 0 -- Signed thrust value to compare against VelocityVectors["x"] +AutopilotDesiredHeading = 0 -- If NavTableHasTarget == true this gets ignored in favor of TargetRelativeAngle + +-- Gimbal Sensor Values +Angles = { + 0, -- xAngle: rotation about body-X (pitch); 0 is aligned with world-up + 0 -- zAngle: rotation about body-Z (roll); 0 is aligned with world-up +} +AngularRates = { + 0, -- wx: pitch rate about body-X + 0, -- wy: yaw rate about body-Y + 0 -- wz: roll rate about body-Z +} +LinearAcceleration = { + 0, -- Acceleration along body-X + 0, -- Acceleration along body-Y + 0 -- Acceleration along body-Z +} + +-- Nav Table Values +Heading = 0 + +-- Nav Table Values (targets) +NavTableHasTarget = false +BearingToTarget = 0 +TargetClosureRate = 0 +TargetVerticalOffset = 0 +TargetRelativeAngle = 0 + +-- Altitude Sensor Values +Altitude = 0 +AirPressure = 0 +VerticalSpeed = 0 + +-- Velocity Sensors +VelocityVectors = { + x = 0, -- +X is east (port), +Y is up, +Z is south (aft) + y = 0, -- -X is west (star), -Y is down, -Z is north (fore) + z = 0 +} + +-- Throttle Inputs +Throttles = { + ForeThrottle = { + name = "", + side = "", + input = 0 + }, + DownThrottle = { + name = "", + side = "", + input = 0 + }, + AftThrottle = { + name = "", + side = "", + input = 0 + } +} + +local function getRelayStates() + local states = {} + local sides = { + "top", + "bottom", + "left", + "right", + "front", + "back" + } + + local names = peripheral.getNames() + for _, name in ipairs(names) do + if peripheral.getType(name) == "redstone_relay" then + local wrapper = peripheral.wrap(name) + for _, side in ipairs(sides) do + states[name][side] = wrapper.getAnalogInput(side) + end + end + end + + return states +end + +local function identifyRelay(label) + print("\nIdentifying "..label.." throttle") + print("\nPlease change the input signal for the throttle") + + local initialStates = getRelayStates + + while true do + os.pullEvent("redstone_relay") + + local currentStates = getRelayStates + + for pname, currentState in pairs(currentStates) do + local initialState = initialStates[pname] + for k, v in pairs(currentState) do + if initialState[k] ~= v then + return { + name = pname, + side = k + } + end + end + end + end +end + +function ThrottleInit() + Throttles.ForeThrottle = identifyRelay("fore") + Throttles.DownThrottle = identifyRelay("down") + Throttles.AftThrottle = identifyRelay("aft") +end + +function PropellerInit() + local propellerTypes = { + "gyroscopic_propeller_bearing", + "propeller", + "propeller_bearing" + } + + local transmissionTypes = { + "analog_transmission", + "Create_RotationSpeedController" + } + + local transmissions = {} + local propellers = {} + + for _, v in ipairs(transmissionTypes) do + table.insert(transmissions, peripheral.find(v)) + end + + for _, v in ipairs(propellerTypes) do + table.insert(propellers, peripheral.find(v)) + end + + for pi, pv in ipairs(propellers) do + for ti, tv in ipairs(transmissions) do + if pv.getSubnetworkAnchorId() == tv.getSelfId() then + if Thrusters[peripheral.getName(pv)] == nil then + Thrusters[peripheral.getName(pv)] = { + type = "rotator", + thruster = pv, + transmission = tv, + -- NOTE: + -- affectVectors will have values depending on how the thruster's + -- thrust vector interacts with the vessel's attitude + -- possible directions for yaw: "port"|"star" + -- possible directions for roll: "port"|"star" + -- possible directions for pitch: "up"|"down" + -- possible directions for lateral: "port"|"star"|"fore"|"aft"|"up"|"down" + affectVectors = { + yaw = nil, + pitch = nil, + roll = nil, + lateral = nil + }, + power = (tv.getSignal())/15 + } + end + if Thrusters[peripheral.getName(pv)] ~= nil then + if Thrusters[peripheral.getName(pv)].type == nil then Thrusters[peripheral.getName(pv)].type = "rotator" end + Thrusters[peripheral.getName(pv)].thruster = pv + Thrusters[peripheral.getName(pv)].transmission = tv + if Thrusters[peripheral.getName(pv)].affectVectors == nil then Thrusters[peripheral.getName(pv)].affectVectors = nil end + Thrusters[peripheral.getName(pv)].power = (tv.getSingnal())/15 + end + end + end + end +end + +function ThrusterInit() + local thrusterTypes = { + "thruster", + "ion_thruster", + "vector_thruster", + "liquid_vector_thruster" + } + + local thrusters = {} + + for _, v in ipairs(thrusterTypes) do + table.insert(thrusters, peripheral.find(v)) + end + + for ti, tv in ipairs(thrusters) do + if Thrusters[peripheral.getName(tv)] == nil then + Thrusters[peripheral.getName(tv)] = { + thruster = tv, + transmission = nil, + type = "thruster", + -- NOTE: + -- affectVectors will have values depending on how the thruster's + -- thrust vector interacts with the vessel's attitude + -- possible directions for yaw: "port"|"star" + -- possible directions for roll: "port"|"star" + -- possible directions for pitch: "up"|"down" + -- possible directions for lateral: "port"|"star"|"fore"|"aft"|"up"|"down" + affectVectors = { + yaw = nil, + pitch = nil, + roll = nil, + lateral = nil + }, + power = tv.getPower() + } + end + if Thrusters[peripheral.getName(tv)] ~= nil then + if Thrusters[peripheral.getName(tv)].type == nil then Thrusters[peripheral.getName(tv)].type = "thruster" end + Thrusters[peripheral.getName(tv)].thruster = tv + Thrusters[peripheral.getName(tv)].transmission = nil + if Thrusters[peripheral.getName(tv)].affectVectors == nil then Thrusters[peripheral.getName(tv)].affectVectors = nil end + Thrusters[peripheral.getName(tv)].power = tv.getPower() + end + end +end + +function Update() + PropellerInit() + ThrusterInit() +end + +local function checkIfThrusterIsIndexed() + local thrusterTypes = { + "thruster", + "ion_thruster", + "vector_thruster", + "liquid_vector_thruster" + } + + local thrusters = {} + local unindexedThrusters = {} + + for _, v in ipairs(thrusterTypes) do + table.insert(thrusters, peripheral.find(v)) + end + + local propellerTypes = { + "gyroscopic_propeller_bearing", + "propeller", + "propeller_bearing" + } + + for _, v in ipairs(propellerTypes) do + table.insert(thrusters, peripheral.find(v)) + end + + for _, v in ipairs(thrusters) do + if Thrusters[peripheral.getName(v)] == nil then + table.insert(unindexedThrusters, v) + end + end + + if #unindexedThrusters == 0 then + return nil + end + return unindexedThrusters +end + +local function partiallyUpdateThrusters(thrusterList) + local thrusterTypes = { + "thruster", + "ion_thruster", + "vector_thruster", + "liquid_vector_thruster" + } + + local propellerTypes = { + "gyroscopic_propeller_bearing", + "propeller", + "propeller_bearing" + } + + for _, tv in ipairs(thrusterList) do + for _, tt in ipairs(thrusterTypes) do + if peripheral.getType(tv) == tt then + Thrusters[peripheral.getName(tv)] = { + thruster = tv, + transmission = nil, + type = "thruster", + affectVectors = { + yaw = nil, + roll = nil, + pitch = nil, + lateral = nil + }, + power = tv.getPower() + } + end + end + end +end + +function UpdateStabilization() + +end + +function PollThrottle() + for _, v in ipairs(Throttles) do + v.input = peripheral.call(v.name, "getAnalogInput("..v.side..")") + end +end + +function PollSensors() + PollVelocity() + PollAltitude() + PollNavTable() + PollGimbal() +end + +function PollVelocity() + local velSensors = peripheral.find("velocity_sensor") + + -- Velocity Sensors + VelocityVectors = {} + for vi, vs in pairs(velSensors) do + local vsAxis = vs.getAxis() + local vsVelocity = vs.getVelocity() + VelocityVectors[vsAxis] = vsVelocity + end +end + +function PollAltitude() + local altSensor = peripheral.find("altitude_sensor") -- enforce one alt sensor + + -- Altitude Sensor + Altitude = altSensor.getHeight() + AirPressure = altSensor.getAirPressure() + VerticalSpeed = altSensor.getVerticalSpeed() +end + +function PollNavTable() + local navTable = peripheral.find("navigation_table") -- enforce one nav table + + -- Navigation Table + Heading = navTable.getHeading() + NavTableHasTarget = navTable.hasTarget() + if NavTableHasTarget then + BearingToTarget = navTable.getBearing() + TargetClosureRate = navTable.getClosureRate() + TargetVerticalOffset = navTable.getVerticalOffsetToTarget() + TargetRelativeAngle = navTable.getRelativeAngle() + end + if not NavTableHasTarget then + BearingToTarget = 0 + TargetClosureRate = 0 + TargetVerticalOffset = 0 + TargetRelativeAngle = 0 + end +end + +function PollGimbal() + local gimbalSensor = peripheral.find("gimbal_sensor") -- enforce one gimbal sensor + + -- Gimbal Sensor + Angles = gimbalSensor.getAngles() + AngularRates = gimbalSensor.getAngularRates() + LinearAcceleration = gimbalSensor.getLinearAcceleration() +end + +function Sigmoid(x) + local e = 2.718281828459045 + return 1 / (1 + e^(-x)) +end + +function CustomSigmoid(x) + return ((2 * Sigmoid(x)) - 1) * -1 +end + +-- thruster is a thruster type, power is a vector from 0.0 to 1.0 +function SetThrusterPower(thruster, power) + if thruster.type == "rotator" then + thruster.power = power + local actualPower = 0 + if peripheral.getType(thruster.transmission) == "Create_RotationSpeedController" then + if (math.ceil(power*256) - power*256 <= 0.5) then + actualPower = math.ceil(power*256) + end + if (math.ceil(power*256) - power*256) > 0.5 then + actualPower = math.floor(power*256) + end + + thruster.transmission.setTargetSpeed(actualPower) + end + if peripheral.getType(thruster.transmission) == "analog_transmission" then + if (math.ceil(power*15) - power*15) <= 0.5 then + actualPower = math.ceil(power*15) + end + if (math.ceil(power*15) - power*15) > 0.5 then + actualPower = math.floor(power*15) + end + + if actualPower < 1 then + actualPower = 15 -- signal 15 will decouple the speed and stop the motion + end + if actualPower <= 15 then + actualPower = actualPower - 1 + end + + thruster.transmission.setSignal(actualPower) + end + + end + if thruster.type == "thruster" then + thruster.power = power + thruster.thruster.setPowerNormalized(power) + end +end + +function UpdateGlobalThrust() + -- lateral thrust + local desiredLateralThrustVectors = {} + + for f, v in ipairs(VelocityVectors) do + desiredLateralThrustVectors[f] = CustomSigmoid(v) + end + + local thrustDirections = {} + for f, v in desiredLateralThrustVectors do + if f == "x" then + if v > 0 then table.insert(thrustDirections, { port = v }) else table.insert(thrustDirections, { star = math.abs(v) }) end + end + if f == "y" then + if v > 0 then table.insert(thrustDirections, { down = v }) else table.insert(thrustDirections, { up = math.abs(v) }) end + end + if f == "z" then + if v > 0 then table.insert(thrustDirections, { fore = v }) else table.insert(thrustDirections, { aft = math.abs(v) }) end + end + end + + for _, d in ipairs(thrustDirections) do + for _, t in ipairs(Thrusters) do + if t.affectVectors.lateral == d then + SetThrusterPower(d) + end + end + end + + -- angular thrust + local desiredAngluarThrustVectors = {} + + for f, v in ipairs(AngularRates) do + desiredAngluarThrustVectors[f] = CustomSigmoid(v) + end +end + +function Init() + ThrottleInit() + Thrusters = {} + + local configFile = io.open(ConfigPath, "r") + if configFile then + local configContent = configFile:read("*all") + local configChunk, err = load(configContent) + if configChunk then + Config = configChunk() + else + print("Error loading config string: "..err) + end + else + print("Could not open the config file") + end + configFile.close() + + if Config.thrusterConfigPath ~= nil then + local thrusterConfigFile = io.open(Config.thrusterConfigPath, "r") + if thrusterConfigFile then + local configContent = thrusterConfigFile:read("*all") + local configChunk, err = load(configContent) + if configChunk then + Thrusters = configChunk() + else + print("Error loading thruster config string: "..err) + end + + else + print("Could not open the thruster config file") + end + thrusterConfigFile.close() + end + + if Config.ThrusterConfigPath ~= nil then + local unindexedThrusters = checkIfThrusterIsIndexed() + + if unindexedThrusters ~= nil then + partiallyUpdateThrusters(unindexedThrusters) + end + end +end + +function Main() + Init() + + while true do + local unindexedThrusters = checkIfThrusterIsIndexed() + if unindexedThrusters ~= nil then + partiallyUpdateThrusters(unindexedThrusters) + end + + PollSensors() + UpdateGlobalThrust() + end +end \ No newline at end of file diff --git a/thrusters.lua b/thrusters.lua new file mode 100644 index 0000000..97b63c8 --- /dev/null +++ b/thrusters.lua @@ -0,0 +1,27 @@ +return { + gyroscopic_propeller_bearing_0 = { + type = "rotator", + name = "gyroscopic_propeller_bearing_0", + thruster = nil, + transmission = nil, + affectVectors = { + yaw = nil, + pitch = "up", + roll = "star", + lateral = nil + }, + power = nil + }, + gyroscopic_propeller_bearing_1 = { + type = "rotator", + name = "gyroscopic_propeller_bearing_1", + thruster = nil, + affectVectors = { + yaw = nil, + pitch = "up", + roll = "port", + lateral = nil + }, + power = nil + } +} \ No newline at end of file