name: Basic Space Autopilot
slots:
core:
class: CoreUnit
container:
class: FuelContainer
select: all
handlers:
unit:
start:
lua: |
-- Garbage collection fix added by wrap.lua
do
-- Set GC pause. This more or less means by how many % memory use should increase before a garbage collection is started. Lua default is 200
local newPause = 110
local oldPause = collectgarbage("setpause", newPause)
if oldPause < newPause then
-- DU now has a different default GC pause which is even lower. Revert back to it.
collectgarbage("setpause", oldPause)
end
end
-- error handling code added by wrap.lua
__wrap_lua__stopped = false
__wrap_lua__stopOnError = false
__wrap_lua__rethrowErrorAlways = false
__wrap_lua__rethrowErrorIfStopped = true
__wrap_lua__printError = true
__wrap_lua__showErrorOnScreens = true
function __wrap_lua__error (message)
if __wrap_lua__stopped then return end
-- make the traceback more readable and escape HTML syntax characters
message = tostring(message):gsub('"%-%- |STDERROR%-EVENTHANDLER[^"]*"', 'chunk'):gsub("&", "&"):gsub("<", "<"):gsub(">", ">")
local unit = unit or self or {}
if __wrap_lua__showErrorOnScreens then
for _, value in pairs(unit) do
if type(value) == "table" and value.setCenteredText and value.setHTML then -- value is a screen
if message:match("\n") then
value.setHTML([[
<pre style="color: white; background-color: black; font-family: Consolas,monospace; font-size: 4vh; white-space: pre-wrap; margin: 1em">
Error: ]] .. message .. [[
</pre>]])
else
value.setCenteredText(message)
end
end
end
end
if __wrap_lua__printError and system and system.print then
system.print("Error: " .. message:gsub("\n", "<br>"))
end
if __wrap_lua__stopOnError then
__wrap_lua__stopped = true
end
if __wrap_lua__stopped and unit and unit.exit then
unit.exit()
end
if __wrap_lua__rethrowErrorAlways or (__wrap_lua__stopped and __wrap_lua__rethrowErrorIfStopped) then
error(message)
end
end
-- in case traceback is removed or renamed
__wrap_lua__traceback = traceback or (debug and debug.traceback) or function (arg1, arg2) return arg2 or arg1 end
local ok, message = xpcall(function ()
-- script code
--------------------------------------------------------------------------------
-- basic space autopilot bundle begins
-- version: 2020-05-13 82cd7bd
-- content sha256: 73a16d5558
--------------------------------------------------------------------------------
__lbs__version = "2020-05-13 82cd7bd"
do
do
local _ENV = _ENV
package.preload[ "common.Switcher" ] = function( ... ) _ENV = _ENV;
---@generic TItem
---@param items TItem[]
---@param onSwitched nil | fun(item:TItem,index:number)
local function createSwitcher (items, onSwitched)
local self = {} ---@class Switcher
local index = 1
function self.switchToNext ()
if index == #items then
self.switchToIndex(1)
else
self.switchToIndex(index + 1)
end
end
function self.switchToPrevious ()
if index == 1 then
self.switchToIndex(#items)
else
self.switchToIndex(index - 1)
end
end
---@param newIndex number
function self.switchToIndex (newIndex)
local previousIndex = index
index = newIndex
if onSwitched then
onSwitched(items[index], items[previousIndex])
end
end
---@param item TItem
function self.switchToItem (item)
for itemIndex = 1, #items do
if items[itemIndex] == item then
return self.switchToIndex(itemIndex)
end
end
error("item was not found and cannot be switched to")
end
function self.getIndex ()
return index
end
function self.getCurrent ()
return items[index]
end
return self
end
return { new = createSwitcher }
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.add" ] = function( ... ) _ENV = _ENV;
--- Creates a new array that has items from all argument arrays added in sequence.
--- concat would be a better name, but there already is table.concat that does something else.
---@generic TItem
---@vararg TItem[]
---@return TItem[]
local function add (...)
local result = {}
local arrays = { ... }
for arrayIndex = 1, #arrays do
local array = arrays[arrayIndex]
for elementIndex = 1, #array do
result[#result + 1] = array[elementIndex]
end
end
return result
end
return add
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.filter" ] = function( ... ) _ENV = _ENV;
---@generic TItem
---@param arr TItem[]
---@param predicate fun(item:TItem,index:number):boolean
local function filter (arr, predicate)
local result = {} ---@type TItem[]
for index = 1, #arr do
local item = arr[index]
if predicate(item, index) then
result[#result + 1] = item
end
end
return result
end
return filter
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.findOneMatching" ] = function( ... ) _ENV = _ENV;
--- Returns the first item matching the predicate.
---@generic TItem
---@param items TItem[]
---@param predicate fun(item:TItem):boolean
---@return TItem|nil, number|nil
local function findOneMatching (items, predicate)
for index, item in ipairs(items) do
if predicate(item) then
return item, index
end
end
end
return findOneMatching
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.map" ] = function( ... ) _ENV = _ENV;
-- https://en.wikibooks.org/wiki/Lua_Functional_Programming/Functions
---@generic TSource, TResult
---@param array TSource[]
---@param func fun(item:TSource,index:number):TResult
---@return TResult[]
local function map (array, func)
local new_array = {}
for index, value in ipairs(array) do
new_array[index] = func(value,index)
end
return new_array
end
return map
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.orderedInsert" ] = function( ... ) _ENV = _ENV;
local format = string.format
local insert = table.insert
--- Insert one value into an already ordered array to a position determined by a weights dictionary.
--- This might be a bit more efficient than (re-)sorting a short array with a custom compare function.
--- For efficiency with larger arrays this function could use binary search (not implemented).
---@generic TItem, TWeight
---@param arr TItem[]
---@param weights table<TItem, TWeight>
---@param val TItem
---@return number Inserted value's index in the array.
local function orderedInsert (arr, weights, val)
if #arr == 0 then
arr[1] = val
return 1
end
local valWeight = weights[val]
if not valWeight then
error(format("value to be inserted (%s) is not in the weights table", val))
end
for i = 1, #arr do
local nextVal = arr[i]
local nextWeight = weights[nextVal]
if not nextWeight then
error(format("value at index %d (%s) is not in the weights table", i, nextVal))
end
if nextWeight > valWeight then
insert(arr, i, val)
return i
end
end
arr[#arr + 1] = val
return #arr
end
return orderedInsert
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.formatDecimal" ] = function( ... ) _ENV = _ENV;
local format, match = string.format, string.match
-- like string format "%.5f", except that trailing zeroes are removed
---@param number number
---@param maxDecimalDigits number
---@return string
local function formatDecimal (number, maxDecimalDigits)
local formatString = format("%%.%df", maxDecimalDigits)
local numberString = format(formatString, number)
if maxDecimalDigits < 2 then
return numberString
end
return match(numberString, "^([^.]*...-)0*$")
end
return formatDecimal
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.formatTimeWithUnits" ] = function( ... ) _ENV = _ENV;
local floor = math.floor
local concat = table.concat
local secondsInMinute = 60
local secondsInHour = secondsInMinute * 60
local secondsInDay = secondsInHour * 24
local secondsInYear = 365.2419 * secondsInDay
local minTotalSecondsToShowOnlyYears = secondsInYear * 10
---@param totalSeconds number
---@param maxComponents nil|number
local function formatTimeWithUnits (totalSeconds, maxComponents)
maxComponents = maxComponents or 2
local buffer = {}
if totalSeconds < 0 then
buffer[#buffer + 1] = "-"
totalSeconds = -totalSeconds
maxComponents = maxComponents + 1
end
local showOnlyYears = totalSeconds > minTotalSecondsToShowOnlyYears
local years = floor(totalSeconds / secondsInYear)
if years > 0 then buffer[#buffer + 1] = years .. "y" end
if #buffer < maxComponents and not showOnlyYears then
local days = floor(totalSeconds % secondsInYear / secondsInDay)
if days > 0 then buffer[#buffer + 1] = days .. "d" end
end
if #buffer < maxComponents and not showOnlyYears then
local hours = floor(totalSeconds % secondsInDay / secondsInHour)
if hours > 0 then buffer[#buffer + 1] = hours .. "h" end
end
if #buffer < maxComponents and not showOnlyYears then
local minutes = floor(totalSeconds % secondsInHour / secondsInMinute)
if minutes > 0 then buffer[#buffer + 1] = minutes .. "m" end
end
if #buffer < maxComponents and not showOnlyYears then
local seconds = floor(totalSeconds % secondsInMinute)
if seconds > 0 then buffer[#buffer + 1] = seconds .. "s" end
end
if #buffer == 0 then return "0s" end
return concat(buffer, " ")
end
return formatTimeWithUnits
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.json" ] = function( ... ) _ENV = _ENV;
-- Extracts values from a JSON string with pattern matching
-- This is faster than using dkjson when only a few fields are needed
-- Use this only with trusted data sources! Limitations:
-- * Character escapes are not supported
-- * Field nesting is ignored
local find, gsub = string.find, string.gsub
---@param json string
---@param key string
---@param init number|nil
---@return string|nil, number|nil, number|nil
local function extractStringJsonValue (json, key, init)
local pattern = [["]] .. key .. [["%s*:%s*"([^"]*)"]]
local startIndex, endIndex, valueStr = find(json, pattern, init)
return valueStr, startIndex, endIndex
end
---@param json string
---@param key string
---@param init number|nil
---@return number|nil, number|nil, number|nil
local function extractNumberJsonValue (json, key, init)
local pattern = [["]] .. key .. [["%s*:%s*(-?[0-9.e-]+)]]
local startIndex, endIndex, valueStr = find(json, pattern, init)
return tonumber(valueStr), startIndex, endIndex
end
---@param json string
---@param key string
---@param init number|nil
---@return boolean|nil, number|nil, number|nil
local function extractBooleanJsonValue (json, key, init)
local pattern = [["]] .. key .. [["%s*:%s*([truefals]+)]]
local startIndex, endIndex, valueStr = find(json, pattern, init)
if valueStr == "true" then
return true, startIndex, endIndex
elseif valueStr == "false" then
return false, startIndex, endIndex
else
return nil
end
end
---@param extractJsonValue function
---@param json string
---@param key string
---@param stopAfterIndex number|nil
---@param stopAfterValue any|nil
---@return any[]
local function extractAllJsonValues (extractJsonValue, json, key, stopAfterIndex, stopAfterValue)
local values = {}
local valuesLen = 0
local jsonPos = 1
local value, valueStartIndex, valueEndIndex -- luacheck: ignore valueStartIndex -- unused
repeat
value, valueStartIndex, valueEndIndex = extractJsonValue(json, key, jsonPos)
if value ~= nil then
valuesLen = valuesLen + 1
values[valuesLen] = value
jsonPos = valueEndIndex + 1
end
if value == stopAfterValue then break end
if valuesLen == stopAfterIndex then break end
until value == nil
return values
end
---@param json string
---@param key string
---@param stopAfterIndex number|nil
---@param stopAfterValue string|nil
---@return string[]
local function extractAllStringJsonValues (json, key, stopAfterIndex, stopAfterValue)
return extractAllJsonValues(extractStringJsonValue, json, key, stopAfterIndex, stopAfterValue)
end
---@param json string
---@param key string
---@param stopAfterIndex number|nil
---@param stopAfterValue number|nil
---@return number[]
local function extractAllNumberJsonValues (json, key, stopAfterIndex, stopAfterValue)
return extractAllJsonValues(extractNumberJsonValue, json, key, stopAfterIndex, stopAfterValue)
end
---@param json string
---@param key string
---@param stopAfterIndex number|nil
---@param stopAfterValue boolean|nil
---@return boolean[]
local function extractAllBooleanJsonValues (json, key, stopAfterIndex, stopAfterValue)
return extractAllJsonValues(extractBooleanJsonValue, json, key, stopAfterIndex, stopAfterValue)
end
---@param json string
---@param key string
---@return string
local function deleteAllStringJsonValues (json, key)
local pattern = [[%s*"]] .. key .. [["%s*:%s*"[^"]*"%s*,?]]
return (gsub(json, pattern, ""))
end
---@param json string
---@param key string
---@return string
local function deleteAllNumberJsonValues (json, key)
local pattern = [[%s*"]] .. key .. [["%s*:%s*-?[0-9.e-]+%s*,?]]
return (gsub(json, pattern, ""))
end
---@param json string
---@param key string
---@return string
local function deleteAllBooleanJsonValue (json, key)
local pattern = [[%s*"]] .. key .. [["%s*:%s*[truefals]+%s*,?]]
return (gsub(json, pattern, ""))
end
return {
extractStringJsonValue = extractStringJsonValue,
extractNumberJsonValue = extractNumberJsonValue,
extractBooleanJsonValue = extractBooleanJsonValue,
extractAllStringJsonValues = extractAllStringJsonValues,
extractAllNumberJsonValues = extractAllNumberJsonValues,
extractAllBooleanJsonValues = extractAllBooleanJsonValues,
deleteAllStringJsonValues = deleteAllStringJsonValues,
deleteAllNumberJsonValues = deleteAllNumberJsonValues,
deleteAllBooleanJsonValue = deleteAllBooleanJsonValue
}
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.makeCounter" ] = function( ... ) _ENV = _ENV;
local maxinteger = math.maxinteger
---@param startValue number
return function (startValue)
startValue = startValue or 0
assert(type(startValue) == "number", "startValue must be a number")
local value = startValue
return function ()
local valueToReturn = value
if value < maxinteger then
value = value + 1
else
value = 0
end
return valueToReturn
end
end
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.math.signedAngleBetween" ] = function( ... ) _ENV = _ENV;
local acos = math.acos
-- angle is positive if the cross product is in the same direction as the plane normal
local function signedAngleBetween(vec1, vec2, planeNormal)
local normVec1 = vec1:normalize()
local normVec2 = vec2:normalize()
local cosAngle = normVec1:dot(normVec2)
-- due to floating point inaccuracy dot product can end up slightly outside acos domain
if cosAngle > 1 then
cosAngle = 1
elseif cosAngle < -1 then
cosAngle = -1
end
local angle = acos(cosAngle)
local crossProduct = vec1:cross(vec2)
if crossProduct:dot(planeNormal) < 0 then
return -angle
else
return angle
end
end
return signedAngleBetween
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.string.firstLetterToUpperCase" ] = function( ... ) _ENV = _ENV;
---@param str string
local function firstLetterToUpperCase (str)
local result = str:gsub("^%l", function (ch)
return ch:upper()
end)
return result
end
return firstLetterToUpperCase
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.string.wordsToCamelCase" ] = function( ... ) _ENV = _ENV;
---@param str string
local function wordsToCamelCase (str)
local filteredStr = str
:lower()
:gsub("[^%a%d ]", " ")
:gsub(" +", " ")
local result = filteredStr:gsub(" (.)", function (ch)
return ch:upper()
end)
return result
end
return wordsToCamelCase
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.table.assign" ] = function( ... ) _ENV = _ENV;
-- Copies values of source tables into the target table and returns the target table
-- (like JavaScript's Object.assign)
---@generic TTargetTable, TCopiedTable
---@param targetTable TTargetTable
---@vararg TCopiedTable
---@return TTargetTable | TCopiedTable
local function assign (targetTable, ...)
local sourceTables = { ... }
for i = 1, #sourceTables do
for key, value in pairs(sourceTables[i]) do
targetTable[key] = value
end
end
return targetTable
end
return assign
end
end
do
local _ENV = _ENV
package.preload[ "du.CoordinateConverter" ] = function( ... ) _ENV = _ENV;
local unpack = table.unpack
local function createCoordinateConverter (params)
params = params or {}
local library = params.library or library
local rightAxis = { 0, 0, 0 }
local forwardAxis = { 0, 0, 0 }
local upAxis = { 0, 0, 0 }
local self = {} ---@class CoordinateConverter
---@param core Core
function self.setAxesFromCore (core)
rightAxis = core.getConstructWorldOrientationRight()
forwardAxis = core.getConstructWorldOrientationForward()
upAxis = core.getConstructWorldOrientationUp()
end
---@param gyro Gyro
function self.setAxesFromGyro (gyro)
rightAxis = gyro.worldRight()
forwardAxis = gyro.worldForward()
upAxis = gyro.worldUp()
end
function self.relWorldToRightForwardUp (relWorldCoords)
if relWorldCoords.x then
relWorldCoords = { relWorldCoords:unpack() }
end
return library.systemResolution3(rightAxis, forwardAxis, upAxis, relWorldCoords)
end
function self.rightForwardUpToRelWorld (rightForwardUpCoords)
if rightForwardUpCoords.x then
rightForwardUpCoords = { rightForwardUpCoords:unpack() }
end
local rightX, rightY, rightZ = unpack(rightAxis)
local forwardX, forwardY, forwardZ = unpack(forwardAxis)
local upX, upY, upZ = unpack(upAxis)
local rfuX, rfuY, rfuZ = unpack(rightForwardUpCoords)
-- rel = rfuX * right + rfuY * fwd + rfuZ * up
local relX = rfuX * rightX + rfuY * forwardX + rfuZ * upX
local relY = rfuX * rightY + rfuY * forwardY + rfuZ * upY
local relZ = rfuX * rightZ + rfuY * forwardZ + rfuZ * upZ
return { relX, relY, relZ }
end
return self
end
return { new = createCoordinateConverter }
end
end
do
local _ENV = _ENV
package.preload[ "du.Hal" ] = function( ... ) _ENV = _ENV;
-- "hardware abstraction layer" ;)
-- more exactly, a slot abstraction layer
local filter = require "common.utils.array.filter"
local findOneMatching = require "common.utils.array.findOneMatching"
local firstLetterToUpperCase = require "common.utils.string.firstLetterToUpperCase"
local orderedInsert = require "common.utils.array.orderedInsert"
local wordsToCamelCase = require "common.utils.string.wordsToCamelCase"
local sort = table.sort
---@type Control
local self = self or unit or {} -- in-game, self is the active control unit
---@class Hal
local Hal = {
classes = {
AtmoFuelContainer = "AtmoFuelContainer",
RocketFuelContainer = "RocketFuelContainer",
SpaceFuelContainer = "SpaceFuelContainer"
},
slotNames = {},
elementType = {},
elementInSlot = {},
elementSlotName = {},
elements = {}
-- containers, databanks, etc. are added later
}
local function isAntiGravityGenerator (element) return element.setBaseAltitude end
local function isControl (element) return element.setTimer and element.exit end
local function isContainer (element) return element.getItemsMass end
local function isCore (element) return element.spawnNumberSticker end
local function isDatabank (element) return element.getNbKeys end
local function isDynamicCore (element) return isCore(element) and element.getConstructCrossSection end
local function isElementWithState (element) return element.getState end
local function isEngine (element) return element.getMaxThrust end
local function isGyro (element) return element.worldUp end
local function isIndustry (element) return element.getCycleCountSinceStartup end
local function isLibrary (element) return element.systemResolution3 end
local function isPvpRadar (element) return element.getWidgetType and element.getWidgetType() == "radar" end
local function isRadar (element) return element.getEntries and element.getConstructWorldPos end
local function isScreen (element) return element.setCenteredText and element.setHTML end
local function isSystem (element) return element.getTime end
local function isTelemeter (element) return element.getDistance and not isEngine(element) end
local function isWeapon (element) return element.getWidgetType and element.getWidgetType() == "weapon" end
local function isMaybePressableElement (element)
return
isElementWithState(element)
and not isAntiGravityGenerator(element)
and not isEngine(element)
and not isGyro(element)
and not isScreen(element)
and not isTelemeter(element)
end
local elementTypes = {
{ predicate = isAntiGravityGenerator, singular = "anti gravity generator", plural = "anti gravity generators" },
{ predicate = isControl, singular = "control", plural = "controls" },
{ predicate = isContainer, singular = "container", plural = "containers" },
{ predicate = isCore, singular = "core", plural = "cores" },
{ predicate = isDatabank, singular = "databank", plural = "databanks" },
{ predicate = isDynamicCore, singular = "dynamic core", plural = "dynamic cores" },
{ predicate = isElementWithState, singular = "element with state", plural = "elements with state" },
{ predicate = isEngine, singular = "engine", plural = "engines" },
{ predicate = isGyro, singular = "gyro", plural = "gyros" },
{ predicate = isIndustry, singular = "industry", plural = "industries" },
{ predicate = isLibrary, singular = "library", plural = "libraries" },
{ predicate = isPvpRadar, singular = "PVP radar", plural = "PVP radars" },
{ predicate = isRadar, singular = "radar", plural = "radars" },
{ predicate = isScreen, singular = "screen", plural = "screens" },
{ predicate = isSystem, singular = "system", plural = "systems" },
{ predicate = isTelemeter, singular = "telemeter", plural = "telemeters" },
{ predicate = isWeapon, singular = "weapon", plural = "weapons" },
{ predicate = isMaybePressableElement, singular = "maybe pressable element", plural = "maybe pressable elements" }
}
-- set table names, getter function names and error messages for each element type
for _, elementType in pairs(elementTypes) do
elementType.singularCamelCase = wordsToCamelCase(elementType.singular)
elementType.singularPascalCase = firstLetterToUpperCase(elementType.singularCamelCase)
elementType.pluralCamelCase = wordsToCamelCase(elementType.plural)
elementType.pluralPascalCase = firstLetterToUpperCase(elementType.pluralCamelCase)
elementType.typeName = elementType.singularCamelCase
elementType.tableName = elementType.pluralCamelCase
elementType.requireOneFunctionName = "require" .. elementType.singularPascalCase
elementType.requireAtLeastOneFunctionName = "require" .. elementType.pluralPascalCase
elementType.requireOneErrorMessage = firstLetterToUpperCase(elementType.singular) .. " is not connected."
elementType.requireAtLeastOneErrorMessage = "No " .. elementType.plural .. " are connected."
end
-- add getter functions
for _, elementType in pairs(elementTypes) do
Hal[elementType.requireOneFunctionName] = function ()
return Hal[elementType.tableName][1] or error(elementType.requireOneErrorMessage)
end
Hal[elementType.requireAtLeastOneFunctionName] = function ()
local elements = Hal[elementType.tableName]
if #elements < 1 then error(elementType.requireAtLeastOneErrorMessage) end
return elements
end
end
-- detect elements and slot names
local unsortedElements = {}
for key, value in pairs(self) do
if type(key) == "string" and type(value) == "table" and type(value.export) == "table" then
local slotName, element = key, value
Hal.slotNames[#Hal.slotNames + 1] = slotName
Hal.elementInSlot[slotName] = element
Hal.elementSlotName[element] = slotName
unsortedElements[#unsortedElements + 1] = element
end
end
-- sort elements and slot names
sort(Hal.slotNames)
for _, element in ipairs(unsortedElements) do
orderedInsert(Hal.elements, Hal.elementSlotName, element)
end
-- organize elements by type
for _, elementType in ipairs(elementTypes) do
local elementTable = {}
local elementTypePredicate = elementType.predicate
Hal[elementType.tableName] = elementTable
for _, element in ipairs(Hal.elements) do
if elementTypePredicate(element) then
elementTable[#elementTable + 1] = element
Hal.elementType[element] = elementType.typeName
end
end
end
--- used by the annotation generator
---@private
Hal._elementTypes = elementTypes
---@param class string
function Hal.getElementClassPredicate (class)
---@param element Element
return function (element)
return element.getElementClass and element.getElementClass():match(class) and true or false
end
end
---@param class string
---@return Element
function Hal.getElementWithClass (class)
return findOneMatching(Hal.elements, Hal.getElementClassPredicate(class))
end
---@param class string
---@return Element[]
function Hal.getElementsWithClass (class)
return filter(Hal.elements, Hal.getElementClassPredicate(class))
end
return Hal
end
end
do
local _ENV = _ENV
package.preload[ "du.Timer" ] = function( ... ) _ENV = _ENV;
-- Sets a boolean to true each time the timer ticks
-- This allows, for example, handling all ticked timers in system update and calling system.setScreen only once
local getNextTimerId = require "du.getNextTimerId"
---@param timerPeriod number
---@param startDeactivated boolean
local function createTimer (timerPeriod, startDeactivated)
local self = { ticked = false } ---@class Timer
local timerId
function self.getIsActive ()
return timerId and true or false
end
function self.activate ()
if not timerId then
timerId = getNextTimerId()
unit.setTimer(timerId, timerPeriod)
end
end
function self.deactivate ()
if timerId then
unit.stopTimer(timerId)
timerId = nil
end
end
function self.toggle ()
if self.getIsActive() then
self.deactivate()
else
self.activate()
end
end
function self.onStart ()
if not startDeactivated then
self.activate()
end
end
function self.onStop ()
self.deactivate()
end
function self.onTick (tickedTimerId)
if tostring(tickedTimerId) == tostring(timerId) then
self.ticked = true
end
end
return self
end
return { new = createTimer }
end
end
do
local _ENV = _ENV
package.preload[ "du.data.planets" ] = function( ... ) _ENV = _ENV;
-- This file was generated automatically. Do not edit.
return {
{
id = 1,
name = "Madis",
type = "planet",
class = "hT",
gravity = 3.5325,
radius = 44300,
pos = {17465536, 22665536, -34464}
},
{
id = 10,
parentId = 1,
name = "Madis Moon 1",
type = "moon",
gravity = 0.785,
radius = 10000,
pos = {17448118.86, 22966848.03, 143079.98}
},
{
id = 11,
parentId = 1,
name = "Madis Moon 2",
type = "moon",
gravity = 0.942,
radius = 12000,
pos = {17194626, 22243633.88, -214962.81}
},
{
id = 12,
parentId = 1,
name = "Madis Moon 3",
type = "moon",
gravity = 1.1775,
radius = 15000,
pos = {17520617.44, 22184726.9, -309986.22}
},
{
id = 2,
name = "Alioth",
type = "planet",
class = "M",
gravity = 9.891,
radius = 126068,
pos = {-8, -8, -126303},
standardGravitationalParameter = 155900000000
},
{
id = 21,
parentId = 2,
name = "Alioth Moon 1",
type = "moon",
gravity = 2.355,
radius = 30000,
pos = {-564185.78, 233791, -167448}
},
{
id = 22,
parentId = 2,
name = "Alioth Moon 4",
type = "moon",
gravity = 2.380905,
radius = 30330,
pos = {-895203, 358389, -225602}
},
{
id = 3,
name = "Thades",
type = "planet",
class = "T",
gravity = 4.867,
radius = 49000,
pos = {29165536, 10865536, 65536}
},
{
id = 30,
parentId = 3,
name = "Thades Moon 1",
type = "moon",
gravity = 1.099,
radius = 14000,
pos = {29214403.49, 10907080.695, 433861.28}
},
{
id = 31,
parentId = 3,
name = "Thades Moon 2",
type = "moon",
gravity = 1.1775,
radius = 15000,
pos = {29404194.34, 10432766.6, 19553.824}
},
{
id = 4,
name = "Talemai",
type = "planet",
class = "M",
gravity = 4.553,
radius = 57500,
pos = {-13234464, 55765536, 465536}
},
{
id = 5,
name = "Feli",
type = "planet",
class = "M",
gravity = 4.71,
radius = 41800,
pos = {-43534464, 22565536, -48934464}
},
{
id = 50,
parentId = 5,
name = "Feli Moon 1",
type = "moon",
gravity = 1.099,
radius = 14000,
pos = {-43902841.78, 22261034.7, -48862386}
},
{
id = 6,
name = "Sicari",
type = "planet",
class = "M",
gravity = 4.0035,
radius = 51100,
pos = {52765536, 27165536, 52065536}
},
{
id = 7,
name = "Sinnen",
type = "planet",
class = "hT",
gravity = 4.3175,
radius = 54950,
pos = {58665536, 29665536, 58165536}
},
{
id = 70,
parentId = 7,
name = "Sinnen Moon 1",
type = "moon",
gravity = 1.3344999551773071,
radius = 17000,
pos = {58969618.12, 29797943.44, 57969448.98}
},
{
id = 8,
name = "Teoma",
type = "planet",
class = "M",
gravity = 4.7885,
radius = 62000,
pos = {80865536, 54665536, -934464}
},
{
id = 9,
name = "Jago",
type = "planet",
class = "M",
gravity = 4.9455,
radius = 61590,
pos = {-94134464, 12765536, -3634464}
},
{
id = 100,
name = "Lacobus",
type = "planet",
class = "hP",
gravity = 4.4745,
radius = 55650,
pos = {98865536, -13534464, -934464}
},
{
id = 101,
parentId = 100,
name = "Lacobus Moon 3",
type = "moon",
gravity = 1.1775,
radius = 15000,
pos = {98905290.17, -13950923.06, -647589.28}
},
{
id = 102,
parentId = 100,
name = "Lacobus Moon 1",
type = "moon",
gravity = 1.413,
radius = 18000,
pos = {99180967.44, -13783860.94, -926156.934}
},
{
id = 103,
parentId = 100,
name = "Lacobus Moon 2",
type = "moon",
gravity = 1.099,
radius = 14000,
pos = {99250054.22, -13629215.266, -1059341.74}
},
{
id = 110,
name = "Symeon",
type = "planet",
class = "hP",
gravity = 3.8465,
radius = 49050,
pos = {14165536, -85634464, -934464}
},
{
id = 120,
name = "Ion",
type = "planet",
class = "hP",
gravity = 3.5325,
radius = 44950,
pos = {2865536, -99034464, -934464}
},
{
id = 121,
parentId = 120,
name = "Ion Moon 1",
type = "moon",
gravity = 0.8635,
radius = 11000,
pos = {2472917.9, -99133746.266, -1133581.06}
},
{
id = 122,
parentId = 120,
name = "Ion Moon 2",
type = "moon",
gravity = 1.1775,
radius = 15000,
pos = {2995424.17, -99275008.73, -1378482.03}
}
}
end
end
do
local _ENV = _ENV
package.preload[ "du.formatDistance" ] = function( ... ) _ENV = _ENV;
local formatDecimal = require "common.utils.formatDecimal"
local M_IN_KM = 1000
local M_IN_SU = 200000
---@param distanceInMeters number
---@param kmDisplayThreshold nil|number
---@param suDisplayThreshold nil|number
local function formatDistance (distanceInMeters, kmDisplayThreshold, suDisplayThreshold)
kmDisplayThreshold = kmDisplayThreshold or M_IN_KM
suDisplayThreshold = suDisplayThreshold or M_IN_SU
if distanceInMeters > suDisplayThreshold or distanceInMeters < -suDisplayThreshold then
local distanceInSu = distanceInMeters / M_IN_SU
return formatDecimal(distanceInSu, 2) .. " su"
elseif distanceInMeters > kmDisplayThreshold or distanceInMeters < -kmDisplayThreshold then
local distanceInKm = distanceInMeters / M_IN_KM
return formatDecimal(distanceInKm, 2) .. " km"
else
return formatDecimal(distanceInMeters, 2) .. " m"
end
end
return formatDistance
end
end
do
local _ENV = _ENV
package.preload[ "du.getKnownPlanets" ] = function( ... ) _ENV = _ENV;
local assign = require "common.utils.table.assign"
local filter = require "common.utils.array.filter"
local map = require "common.utils.array.map"
local planetsData = require "du.data.planets"
---@class Planet
---@field id number
---@field parentId nil|number
---@field name string
---@field type string
---@field class nil|string
---@field gravity number
---@field radius number
---@field pos table vec3
---@field coreAltitudeOffset nil|number the difference between the altitude reported by the core and the "real" altitude
---@field standardGravitationalParameter nil|number a better estimate of the standard gravitational parameter
local function isPlanetDataPresent (planetData)
return
planetData and
planetData.gravity and
planetData.name and
planetData.radius and
planetData.pos
end
---@return Planet[]
local function getKnownPlanets ()
local planetsWithoutMissingAttributes = filter(planetsData, isPlanetDataPresent)
return map(planetsWithoutMissingAttributes, function (planet)
return assign({}, planet, {
pos = vec3(planet.pos)
})
end)
end
return getKnownPlanets
end
end
do
local _ENV = _ENV
package.preload[ "du.getNextTimerId" ] = function( ... ) _ENV = _ENV;
-- In v0.8, timer ids were shared by locally running scripts and max timer id was 2147483647 (2^31-1).
-- Since v0.10, timer ids are local to control units and are strings.
-- Integer ids still work and can be used with * filters, but need to be compared like tostring(setTimerId) == tostring(tickedTimerId).
if _timerIdCounter then
return _timerIdCounter
end
local makeCounter = require "common.utils.makeCounter"
local firstTimerId = (__sessionId or math.random(0, 999999999)) % 2000000000 // 10000 * 10000
_timerIdCounter = makeCounter(firstTimerId)
return function ()
return tostring(_timerIdCounter())
end
end
end
do
local _ENV = _ENV
package.preload[ "du.math.estimateBrakingDistance" ] = function( ... ) _ENV = _ENV;
local cos, sin, sqrt = math.cos, math.sin, math.sqrt
local c = 30000 / 3.6 -- m/s
---@param force number
---@param restMass number
---@param currentSpeed number
---@param brakeTime number
---@return number
local function estimateBrakingDistance (force, restMass, currentSpeed, brakeTime)
local F, m0, v0, u = force, restMass, currentSpeed, brakeTime
-- integrate [ c sin((F t)/(c m0) + sin^(-1)(v0/c)) dt ] t=0..u
-- d = (c (c m0 sqrt(1 - v0^2/c^2) - c m0 sqrt(1 - v0^2/c^2) cos((F u)/(c m0)) + m0 v0 sin((F u)/(c m0))))/F
-- a = c m0 sqrt(1 - v0^2/c^2); b = (F u)/(c m0); d = (c (a - a cos(b) + m0 v0 sin(b)))/F
local a = c * m0 * sqrt(1 - v0 * v0 / c / c)
local b = (F * u) / (c * m0)
return (c * (a - a * cos(b) + m0 * v0 * sin(b))) / F
end
return estimateBrakingDistance
end
end
do
local _ENV = _ENV
package.preload[ "du.math.estimateBrakingTime" ] = function( ... ) _ENV = _ENV;
local asin = math.asin
local c = 30000 / 3.6 -- m/s
---@param force number
---@param restMass number
---@param currentSpeed number
---@param targetSpeed number
---@return number
local function estimateBrakingTime (force, restMass, currentSpeed, targetSpeed)
local F, m0, v0, v_target = force, restMass, currentSpeed, targetSpeed
-- v'(t)=F / (m0 / sqrt(1 - ( (v(t))/c)**2) ), v(0)=v0
-- v(t)=c sin((F t)/(c m0) + sin^(-1)(v0/c))
-- solve [ c sin((F t)/(c m0) + sin^(-1)(v0/c)) = k ] for t
-- t = (c m0 sin^(-1)(k/c) - c m0 sin^(-1)(v0/c))/F
return (c * m0 * asin(v_target / c) - c * m0 * asin(v0 / c) ) / F
end
return estimateBrakingTime
end
end
do
local _ENV = _ENV
package.preload[ "pilot.components.CompositeComponent" ] = function( ... ) _ENV = _ENV;
---@param components table[]
local function createCompositeComponent (components)
assert(type(components) == "table", "components must be table")
local self = {} ---@class CompositeComponent
setmetatable(self, self)
self.__index = function (tbl, key)
local function callEventHandlers (...)
for i = 1, #components do
local eventHandler = components[i][key]
if eventHandler then eventHandler(...) end
end
end
tbl[key] = callEventHandlers
return callEventHandlers
end
return self
end
return { new = createCompositeComponent }
end
end
do
local _ENV = _ENV
package.preload[ "pilot.components.DefaultWidgetComponent" ] = function( ... ) _ENV = _ENV;
---@class DefaultWidgetGroup
---@field panelLabel string
---@field widgetType string
---@field elements Element[]
---@field widgetPerData boolean
--- Shows/hides default element widgets using the widget API.
--- This is similar to the _autoconf.displayCategoryPanel and hideCategoryPanel helper functions that are automatically prepended after running autoconf.
---@param system System
---@param groups DefaultWidgetGroup[]
local function createDefaultWidgetComponent (system, groups)
local panelIds = {} ---@type string[]
local self = {} --- @class DefaultWidgetComponent
self.onStart = function ()
for _, group in ipairs(groups) do
local panelId = system.createWidgetPanel(group.panelLabel)
if group.widgetPerData then
-- separate widget for each element
for _, element in ipairs(group.elements) do
local widgetId = system.createWidget(panelId, group.widgetType)
system.addDataToWidget(element.getDataId(), widgetId)
end
else
-- same widget for all elements
local widgetId = system.createWidget(panelId, group.widgetType)
for _, element in ipairs(group.elements) do
system.addDataToWidget(element.getDataId(), widgetId)
end
end
panelIds[#panelIds + 1] = panelId
end
end
self.onStop = function ()
for _, panelId in ipairs(panelIds) do
system.destroyWidgetPanel(panelId)
end
end
return self
end
return { new = createDefaultWidgetComponent }
end
end
do
local _ENV = _ENV
package.preload[ "pilot2.ConstructState" ] = function( ... ) _ENV = _ENV;
-- Represents construct's state during the flush event
-- Caches control/core/gyro function return values until reset
local signedAngleBetween = require "common.utils.math.signedAngleBetween"
local epsilon = constants.epsilon
local function makeReturnVec3 (fn)
if not fn then error("fn must be not be nil") end
return function () return vec3(fn()) end
end
local ConstructState = {}
function ConstructState.new (options)
options = options or {}
local self = setmetatable({ getters = {} }, ConstructState) -- lua-somewhat-minify: skip getters
if options.control then self:addControlGetters(options.control) end
if options.core then self:addCoreGetters(options.core) end
if options.gyro then self:addGyroGetters(options.gyro) end
if options.system then self:addSystemGetters(options.system) end
return self
end
---@param control Control
function ConstructState:addControlGetters (control)
local getters = self.getters
getters.atmosphereDensity = control.getAtmosphereDensity
getters.closestPlanetInfluence = control.getClosestPlanetInfluence
end
---@param core Core
function ConstructState:addCoreGetters (core)
local getters = self.getters
getters.constructMass = core.getConstructMass
getters.constructIMass = core.getConstructIMass
getters.constructId = core.getConstructId
getters.constructWorldPos = makeReturnVec3(core.getConstructWorldPos)
getters.constructCrossSection = makeReturnVec3(core.getConstructCrossSection)
getters.altitude = core.getAltitude
getters.g = core.g
getters.worldGravity = makeReturnVec3(core.getWorldGravity)
getters.worldVertical = makeReturnVec3(core.getWorldVertical)
getters.angularVelocity = makeReturnVec3(core.getAngularVelocity)
getters.worldAngularVelocity = makeReturnVec3(core.getWorldAngularVelocity)
getters.angularAcceleration = makeReturnVec3(core.getAngularAcceleration)
getters.worldAngularAcceleration = makeReturnVec3(core.getWorldAngularAcceleration)
getters.velocity = makeReturnVec3(core.getVelocity)
getters.worldVelocity = makeReturnVec3(core.getWorldVelocity)
getters.acceleration = makeReturnVec3(core.getAcceleration)
getters.worldAcceleration = makeReturnVec3(core.getWorldAcceleration)
getters.constructOrientationUp = makeReturnVec3(core.getConstructOrientationUp)
getters.constructOrientationRight = makeReturnVec3(core.getConstructOrientationRight)
getters.constructOrientationForward = makeReturnVec3(core.getConstructOrientationForward)
getters.constructWorldOrientationUp = makeReturnVec3(core.getConstructWorldOrientationUp)
getters.constructWorldOrientationRight = makeReturnVec3(core.getConstructWorldOrientationRight)
getters.constructWorldOrientationForward = makeReturnVec3(core.getConstructWorldOrientationForward)
getters.worldAirFrictionAcceleration = makeReturnVec3(core.getWorldAirFrictionAcceleration)
getters.worldAirFrictionAngularAcceleration = makeReturnVec3(core.getWorldAirFrictionAngularAcceleration)
-- TODO: getters for max KP for each axis
getters.maxKinematicsParameters = function () return core.getMaxKinematicsParametersAlongAxis("thrust analog longitudinal", { self.constructOrientationForward:unpack() } ) end
getters.atmoFMaxPlus = function () return self.maxKinematicsParameters[1] end
getters.atmoFMaxMinus = function () return self.maxKinematicsParameters[2] end
getters.spaceFMaxPlus = function () return self.maxKinematicsParameters[3] end
getters.spaceFMaxMinus = function () return self.maxKinematicsParameters[4] end
getters.accelerationMagnitude = function () return self.worldAcceleration:len() end
getters.velocityMagnitude = function () return self.worldVelocity:len() end
getters.worldVelocityDirection = function () return self.worldVelocity / self.velocityMagnitude end
getters.constructOrientationDown = function () return -self.constructOrientationUp end
getters.constructOrientationLeft = function () return -self.constructOrientationLeft end
getters.constructOrientationBackward = function () return -self.constrructOrientationForward end
getters.constructWorldOrientationDown = function () return -self.constructWorldOrientationUp end
getters.constructWorldOrientationLeft = function () return -self.constructWorldOrientationRight end
getters.constructWorldOrientationBackward = function () return -self.constructWorldOrientationForward end
end
---@param gyro Gyro
function ConstructState:addGyroGetters (gyro)
local getters = self.getters
getters.worldUp = makeReturnVec3(gyro.worldUp)
getters.worldForward = makeReturnVec3(gyro.worldForward)
getters.worldRight = makeReturnVec3(gyro.worldRight)
getters.worldDown = function () return -self.worldUp end
getters.worldBackward = function () return -self.worldForward end
getters.worldLeft = function () return -self.worldRight end
end
---@param system System
function ConstructState:addSystemGetters (system)
local getters = self.getters
getters.time = system.getTime
end
function ConstructState:addDerivedGetters ()
local getters = self.getters
getters.isMovingBackward = function ()
return self.worldVelocity:dot(self.constructWorldOrientationForward) < 0
end
getters.isMovingTowardsGravity = function ()
return self.worldVelocity:dot(self.worldGravity) > 0
end
getters.isUpsideDown = function ()
return self.constructWorldOrientationUp:dot(self.worldVertical) > 0
end
getters.speed = function ()
return self.velocityMagnitude * (self.isMovingBackward and -1 or 1)
end
getters.forwardVelocity = function ()
return self.worldVelocity:project_on(self.constructWorldOrientationForward)
end
getters.forwardSpeed = function ()
return self.forwardVelocity:len() * (self.isMovingBackward and -1 or 1)
end
getters.verticalVelocity = function ()
return self.worldVelocity:project_on(self.worldGravity)
end
getters.verticalSpeed = function ()
return self.verticalVelocity:len() * (self.isMovingTowardsGravity and -1 or 1)
end
getters.groundVelocity = function ()
if self.g < epsilon then return vec3.zero end
return self.worldVelocity:project_on_plane(self.worldVertical)
end
getters.groundSpeed = function ()
if self.g < epsilon then return 0 end
return self.groundVelocity:len() * (self.isMovingBackward and -1 or 1)
end
getters.groundRight = function ()
if self.g < epsilon then return vec3.zero end
return self.worldVertical:cross(self.constructWorldOrientationForward)
end
getters.groundForward = function ()
if self.g < epsilon then return vec3.zero end
return -self.worldVertical:cross(self.constructWorldOrientationRight)
end
getters.pitch = function () -- pitch up is positive
return signedAngleBetween(self.groundForward, self.constructWorldOrientationForward, self.constructWorldOrientationRight)
end
getters.roll = function () -- left roll is positive
return signedAngleBetween(self.constructWorldOrientationRight, self.groundRight, self.constructWorldOrientationForward)
end
end
function ConstructState:__index (key)
local getters = rawget(self, "getters") -- rawget prevents infinite recursion if getters get accidentally deleted
local getter = getters[key]
if not getter then
return rawget(ConstructState, key)
end
local value = getter()
self[key] = value
return value
end
function ConstructState:reset ()
for key, _ in pairs(self) do
if key ~= "getters" then
self[key] = nil
end
end
end
return ConstructState
end
end
do
local _ENV = _ENV
package.preload[ "scripts.flight-space-autopilot.BasicAutopilotFlightMode" ] = function( ... ) _ENV = _ENV;
local json = require "common.utils.json"
local CoordinateConverter = require "du.CoordinateConverter"
local ConstructState = require "pilot2.ConstructState"
local deg2rad, epsilon = constants.deg2rad, constants.epsilon
local extractNumberJsonValue = json.extractNumberJsonValue
local rangeMap = utils.map
local KM_H_TO_M_S = 1 / 3.6
-- auto-alignment settings
local minAlignmentVectorLen = 0.001
local radiansToSlowdownAfter = 90 * deg2rad
local rotationSpeed = 0.5
-- settings for converting angular velocity to angular acceleration
local torqueFactor = 10 --export: Force factor applied to reach the target angular velocity.
local maxAngularAcceleration = 1 --export: Decrease this value to prevent "torque overload" messages.
-- speed correction with engines
local minSpeedErrorToCorrect = 0.4 * KM_H_TO_M_S
local minSpeedErrorForMaxAcceleration = 7 * KM_H_TO_M_S
-- brake settings
local brakeSpeedFactor = 10 --export: When braking, brake acceleration will be equal to velocity multiplied by this number.
local minSpeedErrorForBraking = 0.1 * KM_H_TO_M_S
---@param input number
---@param inputForNonZeroOutput number
---@param inputForMaxOutput number
---@param maxOutput number
---@return number
local function getSpeedCorrection (input, inputForNonZeroOutput, inputForMaxOutput, maxOutput)
if input < inputForNonZeroOutput and input > -inputForNonZeroOutput then return 0 end
if input > inputForMaxOutput then return maxOutput end
if -input > inputForMaxOutput then return -maxOutput end
return rangeMap(input, inputForNonZeroOutput, inputForMaxOutput, 0, maxOutput)
end
--- Navigator.lua replacement for simple autopilot scripts.
---@param control Control
---@param core Core
---@param system System
---@param maxSpeed number|nil
local function createAutopilotFlightMode (control, core, system, maxSpeed)
maxSpeed = maxSpeed or 29999 * KM_H_TO_M_S
local state = ConstructState.new {
control = control,
core = core,
system = system
}
state:addDerivedGetters()
local coordConverter = CoordinateConverter.new()
local self = {
state = state,
coordConverter = coordConverter
} ---@class BasicAutopilotFlightMode
-- functions for computing angular acceleration
---@param targetAngularVelocity table vec3
function self.composeAngularAccelerationForAngularVelocity (targetAngularVelocity)
local currentAngularVelocity = state.worldAngularVelocity
local airFriction = state.worldAirFrictionAngularAcceleration
local angularVelocityError = targetAngularVelocity - currentAngularVelocity
local angularAcceleration = angularVelocityError * torqueFactor - airFriction
local angularAccelerationLength = angularAcceleration:len()
if angularAccelerationLength > maxAngularAcceleration then
angularAcceleration = angularAcceleration * (maxAngularAcceleration / angularAccelerationLength)
end
return angularAcceleration
end
---@param currentVector table vec3
---@param targetVector table vec3
---@param angularVelocityMultiplier number|nil
function self.composeAngularVelocityForAxisAlignment (currentVector, targetVector, angularVelocityMultiplier)
angularVelocityMultiplier = angularVelocityMultiplier or 1
local vectorsAvailable = currentVector:len() >= minAlignmentVectorLen and targetVector:len() >= minAlignmentVectorLen
if not vectorsAvailable then return vec3.zero, nil end
local rotationVector = currentVector:cross(targetVector):normalize_inplace()
local radiansToAlignment = vectorsAvailable and currentVector:angle_between(targetVector) or 0
local absRadiansToAlignment = radiansToAlignment < 0 and -radiansToAlignment or radiansToAlignment
local rotationIntensity = absRadiansToAlignment / radiansToSlowdownAfter
if rotationIntensity > 1 then rotationIntensity = 1 end
local angularVelocity = rotationVector * rotationSpeed * rotationIntensity * angularVelocityMultiplier
return angularVelocity, radiansToAlignment
end
-- functions for computing engine acceleration
---@param localAxis table vec3
---@param worldAxis table vec3
---@param isMainAxis boolean whether this is the main (usually longitudinal) axis
---@param currentSpeed number current speed along the axis
---@param targetSpeed number target speed alond the axis
function self.composeAxisAccelerationToSpeed (localAxis, worldAxis, isMainAxis, currentSpeed, targetSpeed)
local speedError = targetSpeed - currentSpeed
local maxKP = core.getMaxKinematicsParametersAlongAxis("thrust analog space_engine", { localAxis:unpack() })
local maxPlusThrust = maxKP[3]
local maxMinusThrust = -maxKP[4]
local maxThrust = speedError < 0 and maxMinusThrust or maxPlusThrust
if maxThrust < epsilon then maxThrust = maxPlusThrust > maxMinusThrust and maxPlusThrust or maxMinusThrust end
local maxAcceleration = maxThrust / state.constructMass
local speedCorrection = getSpeedCorrection(speedError, minSpeedErrorToCorrect, minSpeedErrorForMaxAcceleration, maxAcceleration)
local speedCorrectionSign = speedCorrection < 0 and -1 or 1
do
-- acceleration at near max speed works strangely. sometimes speed will not increase past 29998.4 km/h
local atNearMaxSpeed =
state.velocityMagnitude > maxSpeed - 1 * KM_H_TO_M_S and
state.worldVelocityDirection:dot(worldAxis * speedCorrectionSign) > -0.1
if atNearMaxSpeed then
if isMainAxis and state.velocityMagnitude < maxSpeed - 0.2 * KM_H_TO_M_S then
speedCorrection = maxAcceleration
else
speedCorrection = 0
end
end
end
return speedCorrection * worldAxis
end
---@param worldAxis table vec3
function self.composeAxisLiftAcceleration (worldAxis)
local gravityOnAxis = state.worldGravity:project_on(worldAxis)
return -gravityOnAxis
end
-- functions for computing brake acceleration
---@param currentSpeed number
---@param targetSpeed number
function self.composeBrakingAccelerationToSpeed (currentSpeed, targetSpeed)
local speedError = targetSpeed - currentSpeed
local shouldBrake =
-- moving too fast forward
targetSpeed >= 0 and currentSpeed > targetSpeed + minSpeedErrorForBraking or
-- moving too fast backward
targetSpeed <= 0 and currentSpeed < targetSpeed - minSpeedErrorForBraking or
-- should be moving forward, but moving backward
targetSpeed >= 0 and currentSpeed < -minSpeedErrorForBraking or
-- should be moving backward, but moving forward
targetSpeed <= 0 and currentSpeed > minSpeedErrorForBraking
if not shouldBrake then return vec3.zero end
local absSpeedError = speedError < 0 and -speedError or speedError
return -state.worldVelocityDirection * absSpeedError * brakeSpeedFactor
end
function self.composeBrakingAccelerationAgainstVelocity ()
return -state.worldVelocity * brakeSpeedFactor
end
function self.composeBrakingAccelerationAgainstGravity ()
local brakingAcceleration = -state.worldGravity
if state.verticalSpeed < 0 and state.g > epsilon then
brakingAcceleration = brakingAcceleration + state.worldVertical * state.verticalSpeed * brakeSpeedFactor
end
return brakingAcceleration
end
function self.getMaxBrakeForce ()
local controlData = control.getData()
return extractNumberJsonValue(controlData, "maxBrake") or 0 -- maxBrake is missing from getData() in r0.18 until the construct moves
end
function self.getMaxVerticalSpaceForce ()
local maxVerticalKP = core.getMaxKinematicsParametersAlongAxis("thrust analog vertical space_engine", core.getConstructOrientationUp())
return maxVerticalKP[3]
end
---@param tags string
---@param acceleration table
---@param angularAcceleration table
---@param keepForceCollinearity boolean|nil
---@param keepTorqueCollinearity boolean|nil
---@param priority1SubTags string|nil
---@param priority2SubTags string|nil
---@param priority3SubTags string|nil
---@param toleranceRatioToStopCommand number|nil
function self.setEngineCommand (tags, acceleration, angularAcceleration, keepForceCollinearity, keepTorqueCollinearity, priority1SubTags, priority2SubTags, priority3SubTags, toleranceRatioToStopCommand)
if acceleration.x then acceleration = { acceleration:unpack() } end
if angularAcceleration.x then angularAcceleration = { angularAcceleration:unpack() }end
if keepForceCollinearity == nil then keepForceCollinearity = true end
if keepTorqueCollinearity == nil then keepTorqueCollinearity = true end
if priority1SubTags == nil then priority1SubTags = "" end
if priority2SubTags == nil then priority2SubTags = "" end
if priority3SubTags == nil then priority3SubTags = "" end
if toleranceRatioToStopCommand == nil then toleranceRatioToStopCommand = 0.01 end
return control.setEngineCommand(tags, acceleration, angularAcceleration, keepForceCollinearity and 1 or 0, keepTorqueCollinearity and 1 or 0, priority1SubTags, priority2SubTags, priority3SubTags, toleranceRatioToStopCommand)
end
---@param tags string
---@param acceleration table
---@param keepForceCollinearity boolean|nil
---@param priority1SubTags string|nil
---@param priority2SubTags string|nil
---@param priority3SubTags string|nil
---@param toleranceRatioToStopCommand number|nil
function self.setEngineForceCommand (tags, acceleration, keepForceCollinearity, priority1SubTags, priority2SubTags, priority3SubTags, toleranceRatioToStopCommand)
return self.setEngineCommand(tags, acceleration, vec3.zero, keepForceCollinearity, true, priority1SubTags, priority2SubTags, priority3SubTags, toleranceRatioToStopCommand)
end
---@param tags string
---@param angularAcceleration table
---@param keepTorqueCollinearity boolean|nil
---@param priority1SubTags string|nil
---@param priority2SubTags string|nil
---@param priority3SubTags string|nil
---@param toleranceRatioToStopCommand number|nil
function self.setEngineTorqueCommand (tags, angularAcceleration, keepTorqueCollinearity, priority1SubTags, priority2SubTags, priority3SubTags, toleranceRatioToStopCommand)
return self.setEngineCommand(tags, vec3.zero, angularAcceleration, true, keepTorqueCollinearity, priority1SubTags, priority2SubTags, priority3SubTags, toleranceRatioToStopCommand)
end
function self.onBeforeFlush ()
state:reset()
coordConverter.setAxesFromCore(core)
end
return self
end
return { new = createAutopilotFlightMode }
end
end
do
local _ENV = _ENV
package.preload[ "scripts.flight-space-autopilot.Script" ] = function( ... ) _ENV = _ENV;
local add = require "common.utils.array.add"
local estimateBrakingDistance = require "du.math.estimateBrakingDistance"
local estimateBrakingTime = require "du.math.estimateBrakingTime"
local formatDecimal = require "common.utils.formatDecimal"
local formatDistance = require "du.formatDistance"
local formatTimeWithUnits = require "common.utils.formatTimeWithUnits"
local getKnownPlantets = require "du.getKnownPlanets"
local BasicAutopilotFlightMode = require "scripts.flight-space-autopilot.BasicAutopilotFlightMode"
local Switcher = require "common.Switcher"
local Timer = require "du.Timer"
local deg2rad, epsilon, rad2deg = constants.deg2rad, constants.epsilon, constants.rad2deg
local clamp, rangeMap = utils.clamp, utils.map
local M_S_TO_KM_H = 3.6
local KM_H_TO_M_S = 1 / 3.6
local minDistanceToDestination = 200000 -- 1 su (200 km)
local targetPlanetAltitude = 80000 --export: Target altitude when autopiloting to a planet. Note that some planets (such as Feli) have a very high atmosphere.
local targetMoonAltitude = 20000 --export: Target altitude when autopiloting to a moon.
local targetOtherDistance = 2000 --export: Target distance when autopiloting to something that is not a moon or a planet.
local minVerticalSpaceForce = 10000 -- this script requires vertical space engines for trajectory alignment
local maxBrakeForceSafetyMultiplier = 0.8 -- pretend that max brake force is smaller than it is when calculating braking distance and time
local extraSecondsToStop = 2 -- when calculating distance to start braking, subtract velocity multiplied by this number
local maxSpeed = 29999 * KM_H_TO_M_S
local destinations = add(
getKnownPlantets(),
{
-- {
-- name = 'Station "Aspire"',
-- pos = vec3(39878.2726, 142748.6596, 4991603.1015)
-- },
-- {
-- name = 'Station "Port Albatross"',
-- pos = vec3(358557.0366, 1837825.7063, -175376.8116)
-- },
-- {
-- name = 'Station "Myriad"',
-- pos = vec3(-127283.7579, 138622.4415, -80552.2078)
-- },
-- {
-- name = 'Station "Themis"',
-- pos = vec3(-378454.6922, 157235.9721, -155970.3064)
-- },
-- {
-- name = 'Station "ICSS"',
-- pos = vec3(15506477.2973, 11177900.3132, -10115.5026)
-- },
-- {
-- name = 'Station "Gravity Maze"',
-- pos = vec3(-414142.7786, 217120.8101, -166107.6882)
-- }
}
)
---@param control Control
---@param core Core
---@param system System
local function createScript (control, core, system)
local destSwitcher = Switcher.new(destinations)
local selectPrevAction = "option1"
local selectNextAction = "option2"
local confirmSelectionAction = "option3"
local screenUpdateTimer = Timer.new(1)
local flight = BasicAutopilotFlightMode.new(control, core, system, maxSpeed)
local coordConverter = flight.coordConverter
local state = flight.state
local targetDest
local targetPos
local rfuVelocity
local driftLen
local radiansToTargetVelocity
local radiansToDrift
local radiansToForwardAlignment
local distanceToDest
local distanceToBraking
local startedBraking = false
local arrived = false
---@param dest table
local function getTargetDistance (dest)
local distance = targetOtherDistance
if dest.type == "planet" then distance = targetPlanetAltitude end
if dest.type == "moon" then distance = targetMoonAltitude end
return distance + (dest.radius or 0)
end
---@param dest table
local function getPosNearDestination (dest)
local constructToDest = dest.pos - state.constructWorldPos
local constructToDestLen = constructToDest:len()
if constructToDestLen < (dest.radius or 0) + minDistanceToDestination then
return false, "too close"
end
local destToResultDir
if state.velocityMagnitude > 5000 * KM_H_TO_M_S then
destToResultDir = state.worldVelocity:project_on_plane(constructToDest)
end
if not destToResultDir or destToResultDir:len() < epsilon then
destToResultDir = state.constructWorldOrientationUp:cross(constructToDest)
end
destToResultDir:normalize_inplace()
local targetDistance = getTargetDistance(dest)
return dest.pos + destToResultDir * targetDistance
end
local function checkAutopilotForDeparture ()
if flight.getMaxVerticalSpaceForce() < minVerticalSpaceForce then
return false, "not enough vertical space engines"
end
if control.getAtmosphereDensity() > epsilon then
return false, "in atmo"
end
if control.getClosestPlanetInfluence() > 0.95 then
return false, "near surface"
end
return true
end
local function getDestinationSelectionHtml ()
local canAutopilot, reasonCannotAutopilot = checkAutopilotForDeparture()
if not canAutopilot then
return "Cannot autopilot (" .. reasonCannotAutopilot .. ")"
end
local gotPos, reasonForNoPos = getPosNearDestination(destSwitcher.getCurrent())
return [[
Select destination:<br>
<div style="font-weight: bold; padding: 0.2em 0">]] .. destSwitcher.getCurrent().name .. (gotPos and "" or " - " .. reasonForNoPos) .. [[</div>
[]] .. system.getActionKeyName(selectPrevAction) .. [[] Previous<br>
[]] .. system.getActionKeyName(selectNextAction) .. [[] Next<br>
[]] .. system.getActionKeyName(confirmSelectionAction) .. [[] Confirm<br>
]]
end
local function updateScreen ()
local versionStr = __lbs__version or "Unknown version"
local rfuVelocityStr = rfuVelocity and tostring(rfuVelocity) or "-"
local speedStr = formatDecimal(state.velocityMagnitude * M_S_TO_KM_H, 2)
local driftLenStr = driftLen and formatDecimal(driftLen * M_S_TO_KM_H, 2) or "-"
local gravityStr = formatDecimal(state.g, 3)
local radiansToTargetVelocityStr = radiansToTargetVelocity and formatDecimal(radiansToTargetVelocity * rad2deg, 2) or "-"
local radiansToForwardAlignmentStr = radiansToForwardAlignment and formatDecimal(radiansToForwardAlignment * rad2deg, 2) or "-"
local radiansToDriftStr = radiansToDrift and formatDecimal(radiansToDrift * rad2deg, 2) or "-"
local selectedDestStr = targetDest and targetDest.name or "-"
local distanceToDestStr = distanceToDest and formatDistance(distanceToDest) or "-"
local distanceToBrakingStr = distanceToBraking and formatDistance(distanceToBraking) or "-"
local timeToBrakingStr
if distanceToBraking and not startedBraking and state.speed > maxSpeed * 0.9 then
local timeToBraking = distanceToBraking / state.speed
timeToBrakingStr = formatTimeWithUnits(timeToBraking)
end
local brakingStr = timeToBrakingStr or distanceToBrakingStr
local stateHtml = not targetDest and getDestinationSelectionHtml() or ""
local screenHtml = [[
<style>
.space-autopilot-hud {
position: fixed;
left: 3vw;
top: 5vh;
margin: 0;
padding: 0;
font-size: 1.5vh;
}
.space-autopilot-hud span {
font-family: Consolas, monospace;
}
</style>
<div class="space-autopilot-hud">
<strong>Basic space autopilot</strong><br>
<span>]] .. versionStr .. [[</span><br>
<br>
Velocity: <span>]] .. rfuVelocityStr .. [[ m/s</span><br>
Speed: <span>]] .. speedStr .. [[ km/h</span><br>
Drift: <span>]] .. driftLenStr .. [[ km/h</span><br>
<br>
Gravity: <span>]] .. gravityStr .. [[ m/s<sup>2</sup></span><br>
<br>
Velocity to target: <span>]] .. radiansToTargetVelocityStr .. [[°</span><br>
Forward to target: <span>]] .. radiansToForwardAlignmentStr .. [[°</span><br>
Vertical to drift: <span>]] .. radiansToDriftStr .. [[°</span><br>
<br>
Destination: <span>]] .. selectedDestStr .. [[</span><br>
Distance: <span>]] .. distanceToDestStr .. [[</span><br>
Braking in: <span>]] .. brakingStr .. [[</span><br>
<br>
Started braking: <span>]] .. tostring(startedBraking) ..[[</span><br>
Arrived: <span>]] .. tostring(arrived) .. [[</span><br>
<br>
]] .. stateHtml .. [[</div>]]
system.setScreen(screenHtml)
end
local script = {} ---@class BasicSpaceAutopilotScript
function script.onStart ()
control.hide()
system.showScreen(1)
updateScreen()
screenUpdateTimer.onStart()
end
function script.onStop ()
system.showScreen(0)
system.setScreen("")
screenUpdateTimer.onStop()
end
---@param action string
function script.onActionStart (action)
if targetDest ~= nil or not checkAutopilotForDeparture() then return end
if action == selectPrevAction then
destSwitcher.switchToPrevious()
elseif action == selectNextAction then
destSwitcher.switchToNext()
elseif action == confirmSelectionAction then
local dest = destSwitcher.getCurrent()
local posNearDest = getPosNearDestination(dest)
if not posNearDest then return end
targetPos = posNearDest
targetDest = dest
end
updateScreen()
end
function script.onFlush ()
flight.onBeforeFlush()
rfuVelocity = vec3(coordConverter.relWorldToRightForwardUp(state.worldVelocity))
driftLen = nil
distanceToDest = nil
distanceToBraking = nil
radiansToTargetVelocity = nil
radiansToDrift = nil
radiansToForwardAlignment = nil
if not targetDest or arrived then
local brakingAcceleration = vec3.zero
if arrived or state.velocityMagnitude < maxSpeed * 0.5 then
brakingAcceleration = brakingAcceleration + flight.composeBrakingAccelerationAgainstGravity()
end
if arrived then
brakingAcceleration = brakingAcceleration + flight.composeBrakingAccelerationAgainstVelocity()
end
if state.atmosphereDensity > 0.8 then -- prevent adjustors from firing when the construct is probably landed
flight.setEngineTorqueCommand("torque", vec3.zero)
else
flight.setEngineTorqueCommand('torque', flight.composeAngularAccelerationForAngularVelocity(vec3.zero), true)
end
flight.setEngineForceCommand("thrust analog", vec3.zero, false)
flight.setEngineForceCommand("brake", brakingAcceleration, false)
return
end
local constructToTargetPos = targetPos - state.constructWorldPos
local constructToTargetPosLen = constructToTargetPos:len()
local constructToTargetPosDir = constructToTargetPos / constructToTargetPosLen
-- compute target speed for each axis
local targetVelocity = constructToTargetPosDir * maxSpeed
radiansToTargetVelocity = targetVelocity:angle_between(state.worldVelocity)
local drift = state.worldVelocity:project_on_plane(targetVelocity)
driftLen = drift:len()
radiansToDrift = state.constructWorldOrientationUp:angle_between(-drift)
local rfuTargetVelocity = vec3(coordConverter.relWorldToRightForwardUp(targetVelocity))
local targetLateralSpeed = rfuTargetVelocity.x
local targetVerticalSpeed = rfuTargetVelocity.z
local targetLongitudinalSpeed = not startedBraking and rfuTargetVelocity.y or nil
-- check braking distance
do
distanceToDest = constructToTargetPosLen
local speedToDest = state.worldVelocity:project_on(constructToTargetPos):len()
if state.worldVelocity:dot(constructToTargetPos) < 0 then speedToDest = speedToDest * -1 end
local brakeForce = flight.getMaxBrakeForce() * maxBrakeForceSafetyMultiplier
local brakingTime = estimateBrakingTime(-brakeForce, state.constructMass, state.velocityMagnitude, 0)
local brakingDistance = estimateBrakingDistance(-brakeForce, state.constructMass, state.velocityMagnitude, brakingTime)
distanceToBraking = distanceToDest - speedToDest * extraSecondsToStop - brakingDistance
arrived = startedBraking and (
distanceToDest < 100 or
radiansToTargetVelocity > 90 * deg2rad
)
end
-- compute braking acceleration
local brakingAcceleration = vec3.zero
if distanceToBraking <= 0 then
startedBraking = true
brakingAcceleration = flight.composeBrakingAccelerationAgainstVelocity()
elseif not startedBraking then
brakingAcceleration = flight.composeBrakingAccelerationToSpeed(state.speed, maxSpeed) -- brake if moving backward
-- reduce forward speed if velocity is not aligned with target
if rfuVelocity.y > maxSpeed * 0.95 and radiansToTargetVelocity > 2 * deg2rad then
brakingAcceleration = flight.composeBrakingAccelerationToSpeed(rfuVelocity.y, maxSpeed * 0.95)
end
-- greatly reduce speed if drifting a lot
if state.velocityMagnitude > maxSpeed * 0.5 and driftLen > 2500 * KM_H_TO_M_S then
brakingAcceleration = flight.composeBrakingAccelerationToSpeed(state.velocityMagnitude, maxSpeed * 0.5)
end
end
-- compute engine acceleration
local lateralAcceleration =
flight.composeAxisAccelerationToSpeed(state.constructOrientationRight, state.constructWorldOrientationRight, false, rfuVelocity.x, targetLateralSpeed) +
flight.composeAxisLiftAcceleration(state.constructWorldOrientationRight)
local verticalAcceleration = flight.composeAxisLiftAcceleration(state.constructWorldOrientationUp)
if radiansToDrift < 30 * deg2rad or radiansToDrift > (180 - 30) * deg2rad then
verticalAcceleration = verticalAcceleration +
flight.composeAxisAccelerationToSpeed(state.constructOrientationUp, state.constructWorldOrientationUp, false, rfuVelocity.z, targetVerticalSpeed)
end
local longitudinalAcceleration = flight.composeAxisLiftAcceleration(state.constructWorldOrientationForward)
if targetLongitudinalSpeed then
local shouldStopAcceleratingForward = state.speed > maxSpeed * 2/3 and driftLen > 10 * KM_H_TO_M_S
if not shouldStopAcceleratingForward then
longitudinalAcceleration = longitudinalAcceleration +
flight.composeAxisAccelerationToSpeed(state.constructOrientationForward, state.constructWorldOrientationForward, true, rfuVelocity.y, targetLongitudinalSpeed)
end
end
-- compute angular acceleration
local forwardAlignmentAngularVelocity
local verticalAlignmentAngularVelocity
local alignmentSpeedFactor = clamp(rangeMap(state.velocityMagnitude, maxSpeed * 0.6, maxSpeed, 1, 0.2), 0.2, 1)
local driftCorrectionFactor = clamp(rangeMap(driftLen, 0.1 * KM_H_TO_M_S, 10 * KM_H_TO_M_S, 0, 1), 0, 1)
forwardAlignmentAngularVelocity, radiansToForwardAlignment = flight.composeAngularVelocityForAxisAlignment(
state.constructWorldOrientationForward,
targetVelocity,
alignmentSpeedFactor)
verticalAlignmentAngularVelocity = flight.composeAngularVelocityForAxisAlignment(
state.constructWorldOrientationUp,
-drift * 1 - state.worldGravity * 15 + state.constructWorldOrientationUp * 2,
alignmentSpeedFactor * driftCorrectionFactor)
local angularVelocity = forwardAlignmentAngularVelocity + verticalAlignmentAngularVelocity:project_on(state.constructWorldOrientationForward)
local angularAcceleration = flight.composeAngularAccelerationForAngularVelocity(angularVelocity)
-- apply thrust and torque to engines
flight.setEngineForceCommand("brake", brakingAcceleration, false)
flight.setEngineForceCommand("thrust analog lateral", lateralAcceleration, true)
flight.setEngineForceCommand("thrust analog longitudinal", longitudinalAcceleration, true)
flight.setEngineForceCommand("thrust analog vertical", verticalAcceleration, true)
flight.setEngineTorqueCommand('torque', angularAcceleration, true)
end
function script.onUpdate()
if screenUpdateTimer.ticked then
screenUpdateTimer.ticked = false
updateScreen()
end
end
function script.onTick (timerId)
screenUpdateTimer.onTick(timerId)
end
return script
end
return { new = createScript }
end
end
end
local CompositeComponent = require "pilot.components.CompositeComponent"
local DefaultWidgetComponent = require "pilot.components.DefaultWidgetComponent"
local Script = require "scripts.flight-space-autopilot.Script"
local Hal = require "du.Hal"
local control = Hal.requireControl()
local core = Hal.requireDynamicCore()
local system = Hal.requireSystem()
script = CompositeComponent.new {
Script.new(control, core, system),
DefaultWidgetComponent.new(system, {
{
panelLabel = "Core",
widgetType = "core",
elements = { core }
},
{
panelLabel = "Space Fuel",
widgetType = "fuel_container",
elements = Hal.getElementsWithClass(Hal.classes.SpaceFuelContainer)
}
})
}
script.onStart()
--------------------------------------------------------------------------------
-- basic space autopilot bundle ends
--------------------------------------------------------------------------------
-- error handling code added by wrap.lua
end, __wrap_lua__traceback)
if not ok then
__wrap_lua__error(message)
if not script then script = {} end
end
stop:
lua: |
if not __wrap_lua__stopped and script.onStop then
local ok, message = xpcall(script.onStop,__wrap_lua__traceback,unit)
if not ok then __wrap_lua__error(message) end
end
tick(timerId):
lua: |
if not __wrap_lua__stopped and script.onTick then
local ok, message = xpcall(script.onTick,__wrap_lua__traceback,timerId,unit)
if not ok then __wrap_lua__error(message) end
end
system:
actionStart(action):
lua: |
if not __wrap_lua__stopped and script.onActionStart then
local ok, message = xpcall(script.onActionStart,__wrap_lua__traceback,action,system)
if not ok then __wrap_lua__error(message) end
end
actionStop(action):
lua: |
if not __wrap_lua__stopped and script.onActionStop then
local ok, message = xpcall(script.onActionStop,__wrap_lua__traceback,action,system)
if not ok then __wrap_lua__error(message) end
end
actionLoop(action):
lua: |
if not __wrap_lua__stopped and script.onActionLoop then
local ok, message = xpcall(script.onActionLoop,__wrap_lua__traceback,action,system)
if not ok then __wrap_lua__error(message) end
end
update:
lua: |
if not __wrap_lua__stopped and script.onUpdate then
local ok, message = xpcall(script.onUpdate,__wrap_lua__traceback,system)
if not ok then __wrap_lua__error(message) end
end
flush:
lua: |
if not __wrap_lua__stopped and script.onFlush then
local ok, message = xpcall(script.onFlush,__wrap_lua__traceback,system)
if not ok then __wrap_lua__error(message) end
end