mirror of
https://github.com/osmarks/random-stuff
synced 2024-11-09 13:59:55 +00:00
327 lines
12 KiB
Lua
327 lines
12 KiB
Lua
|
CHANNEL_GPS = 65534
|
||
|
CHANNEL_SNMP = 999
|
||
|
|
||
|
local function trilaterate(A, B, C)
|
||
|
local a2b = B.pos - A.pos
|
||
|
local a2c = C.pos - A.pos
|
||
|
|
||
|
if math.abs(a2b:normalize():dot(a2c:normalize())) > 0.999 then
|
||
|
return nil
|
||
|
end
|
||
|
|
||
|
local d = a2b:length()
|
||
|
local ex = a2b:normalize()
|
||
|
local i = ex:dot(a2c)
|
||
|
local ey = (a2c - ex * i):normalize()
|
||
|
local j = ey:dot(a2c)
|
||
|
local ez = ex:cross(ey)
|
||
|
|
||
|
local r1 = A.distance
|
||
|
local r2 = B.distance
|
||
|
local r3 = C.distance
|
||
|
|
||
|
local x = (r1 * r1 - r2 * r2 + d * d) / (2 * d)
|
||
|
local y = (r1 * r1 - r3 * r3 - x * x + (x - i) * (x - i) + j * j) / (2 * j)
|
||
|
|
||
|
local result = A.pos + ex * x + ey * y
|
||
|
|
||
|
local zSquared = r1 * r1 - x * x - y * y
|
||
|
if zSquared > 0 then
|
||
|
local z = math.sqrt(zSquared)
|
||
|
local result1 = result + ez * z
|
||
|
local result2 = result - ez * z
|
||
|
|
||
|
return result1, result2
|
||
|
end
|
||
|
return result
|
||
|
end
|
||
|
|
||
|
-- from Opus somewhere
|
||
|
local function permutation(tbl, n)
|
||
|
local function permgen(a, n)
|
||
|
if n == 0 then
|
||
|
coroutine.yield(a)
|
||
|
else
|
||
|
for i=1,n do
|
||
|
a[n], a[i] = a[i], a[n]
|
||
|
permgen(a, n - 1)
|
||
|
a[n], a[i] = a[i], a[n]
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
local co = coroutine.create(function() permgen(tbl, n) end)
|
||
|
return function()
|
||
|
local _, res = coroutine.resume(co)
|
||
|
return res
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local known_opus_devices = {
|
||
|
["6_4_milo_2"] = { -3182, 62, -5125, dimension = "overworld" },
|
||
|
["NationalCenterForMissingTurtles #1"] = { -3174, 73, -5124, dimension = "overworld" },
|
||
|
["computer_19171"] = { -3178, 64, -5130, dimension = "overworld" },
|
||
|
["RangerStore"] = { 217, 72, 123, dimension = "overworld" },
|
||
|
["AlexMilo"] = { -1785, 50, -2759, dimension = "overworld" },
|
||
|
["Solar-newmilo"] = { -3073, 78, -3008, dimension = "overworld" },
|
||
|
["ScorchMilo"] = { 269, 58, 421, dimension = "overworld" },
|
||
|
["scorchsfurninator"] = { 269, 54, 421, dimension = "overworld" },
|
||
|
["OMGFurni"] = { 4874, 76, -1701, dimension = "overworld" },
|
||
|
["computer_21867"] = { -7118, 52, -7354, dimension = "overworld" },
|
||
|
["LeClercMilo"] = { -7116, 50, -7357, dimension = "overworld" },
|
||
|
["manager_of_ground_trap"] = { 291, 31, -11, dimension = "overworld" },
|
||
|
["CodedPythonMilo"] = { -2597, 65, 4998, dimension = "overworld" },
|
||
|
["DistantMilo2"] = { -417, 80, -3049, dimension = "overworld" },
|
||
|
["NationalCenterForMissingTurtles #2"] = { 6, 35, -41, dimension = "nether" },
|
||
|
["CobbleGen69"] = { 3996, 52, 2900, dimension = "end" },
|
||
|
["GTech Storage"] = { 3955, 35, -2914, dimension = "end" },
|
||
|
["NationalCenterForMissingTurtles #3"] = { 4858, 75, 1975, dimension = "end" },
|
||
|
["BoomStorage"] = { -6016, 71, 1248, dimension = "overworld" },
|
||
|
["TS-shack"] = { 69, 69, -69, dimension = "nether" }
|
||
|
}
|
||
|
|
||
|
local state = {
|
||
|
debug = false,
|
||
|
fixes = {},
|
||
|
modem_side = nil,
|
||
|
channel_gps_was_closed = nil,
|
||
|
channel_snmp_was_closed = nil,
|
||
|
listener_running = false,
|
||
|
use_saved_fixes = false,
|
||
|
mse_threshold = 0.01,
|
||
|
passive = false,
|
||
|
max_fixes = 4,
|
||
|
max_fix_age = nil,
|
||
|
actual_position = nil -- for testing of Opus device positions
|
||
|
}
|
||
|
|
||
|
function initialize(modem_side)
|
||
|
-- Find a modem
|
||
|
if modem_side == nil then
|
||
|
for _, side in ipairs(rs.getSides()) do
|
||
|
if peripheral.getType(side) == "modem" and peripheral.call(side, "isWireless") then
|
||
|
modem_side = side
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if modem_side == nil then
|
||
|
if state.debug then
|
||
|
print("No wireless modem attached")
|
||
|
end
|
||
|
return nil
|
||
|
end
|
||
|
if state.debug then
|
||
|
print("Using", modem_side, "modem")
|
||
|
end
|
||
|
|
||
|
state.modem_side = modem_side
|
||
|
|
||
|
local modem = peripheral.wrap(modem_side)
|
||
|
state.channel_gps_was_closed = false
|
||
|
if not modem.isOpen(CHANNEL_GPS) then
|
||
|
modem.open(CHANNEL_GPS)
|
||
|
state.channel_was_closed = true
|
||
|
if state.debug then print "Opened GPS" end
|
||
|
end
|
||
|
state.channel_snmp_was_closed = false
|
||
|
if not modem.isOpen(CHANNEL_SNMP) then
|
||
|
modem.open(CHANNEL_SNMP)
|
||
|
state.channel_snmp_was_closed = true
|
||
|
if state.debug then print "Opened SNMP" end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function teardown()
|
||
|
if state.modem_side and state.channel_gps_was_closed then
|
||
|
peripheral.call(state.modem_side, "close", CHANNEL_GPS)
|
||
|
end
|
||
|
if state.modem_side and state.channel_snmp_was_closed then
|
||
|
peripheral.call(state.modem_side, "close", CHANNEL_SNMP)
|
||
|
end
|
||
|
state.modem_side = nil
|
||
|
end
|
||
|
|
||
|
function listener()
|
||
|
state.listener_running = true
|
||
|
while true do
|
||
|
local e, side, channel, reply_channel, message, distance = os.pullEvent "modem_message"
|
||
|
if e == "modem_message" then
|
||
|
if side == state.modem_side and distance then
|
||
|
local fix
|
||
|
if channel == CHANNEL_GPS and reply_channel == CHANNEL_GPS and type(message) == "table" and #message == 3 and tonumber(message[1]) and tonumber(message[2]) and tonumber(message[3]) and message[1] == message[1] and message[2] == message[2] and message[3] == message[3] then
|
||
|
local vec = vector.new(message[1], message[2], message[3])
|
||
|
fix = { pos = vec, dim = message.dimension, src = "gps:" .. tostring(vec) }
|
||
|
elseif channel == CHANNEL_SNMP and type(message) == "table" and type(message.status) == "string" and type(message.label) == "string" and #message.status <= 100 and #message.label <= 32 then
|
||
|
local data = known_opus_devices[message.label]
|
||
|
if state.debug and data then
|
||
|
print(("Got Opus message %d %s %s"):format(reply_channel, message.label, message.status))
|
||
|
end
|
||
|
if data then
|
||
|
fix = { pos = vector.new(unpack(data)), dim = data.dimension, src = "opus:" .. message.label }
|
||
|
if state.actual_position then
|
||
|
local disrepancy = (fix.pos - state.actual_position):length() - distance
|
||
|
if disrepancy > 0.1 then
|
||
|
print("Disrepancy of ", disrepancy, "on", message.label)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if fix then
|
||
|
local time = os.clock()
|
||
|
fix.time = time
|
||
|
fix.distance = distance
|
||
|
for i, old_fix in pairs(state.fixes) do
|
||
|
if tostring(old_fix.pos) == tostring(fix.pos) then
|
||
|
table.remove(state.fixes, i)
|
||
|
if state.debug then
|
||
|
print("Duplicate fix, dropping old")
|
||
|
end
|
||
|
end
|
||
|
if state.max_fix_age then
|
||
|
if time > state.max_fix_age + fix.time then
|
||
|
table.remove(state.fixes, i)
|
||
|
if state.debug then
|
||
|
print("Fix over max age")
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if state.debug then
|
||
|
print(fix.distance .. " metres from " .. tostring(fix.pos))
|
||
|
if fix.dim then
|
||
|
print("Dimension", fix.dim)
|
||
|
end
|
||
|
end
|
||
|
table.insert(state.fixes, fix)
|
||
|
if #state.fixes > state.max_fixes then
|
||
|
table.remove(state.fixes, 1)
|
||
|
end
|
||
|
os.queueEvent "fix_acquired"
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function configure(args)
|
||
|
for k, v in pairs(args) do
|
||
|
state[k] = v
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function locate(timeout, _debug)
|
||
|
state.debug = _debug
|
||
|
-- Let command computers use their magic fourth-wall-breaking special abilities
|
||
|
if commands then
|
||
|
return commands.getBlockPosition()
|
||
|
end
|
||
|
|
||
|
if not state.modem_side then
|
||
|
initialize()
|
||
|
end
|
||
|
|
||
|
if state.debug then
|
||
|
print("Finding position...")
|
||
|
end
|
||
|
|
||
|
if not state.passive then peripheral.call(state.modem_side, "transmit", CHANNEL_GPS, CHANNEL_GPS, "PING") end
|
||
|
|
||
|
local spawn_listener = not state.listener_running
|
||
|
|
||
|
local pos
|
||
|
local dimension
|
||
|
|
||
|
if state.use_saved_fixes == false then
|
||
|
state.fixes = {}
|
||
|
end
|
||
|
|
||
|
local fns = {
|
||
|
function() sleep(timeout or 1) end,
|
||
|
function()
|
||
|
while true do
|
||
|
os.pullEvent "fix_acquired"
|
||
|
for _, fix in pairs(state.fixes) do
|
||
|
if fix.distance == 0 then
|
||
|
if state.debug then print("Distance 0 to", fix.pos) end
|
||
|
pos = fix.pos
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
if state.debug then
|
||
|
print("Fixes at", #state.fixes)
|
||
|
end
|
||
|
local candidate_positions = {}
|
||
|
local dimvotes = {}
|
||
|
for _, fix in pairs(state.fixes) do
|
||
|
if fix.dim then
|
||
|
dimvotes[fix.dim] = (dimvotes[fix.dim] or 0) + 1
|
||
|
end
|
||
|
end
|
||
|
local best
|
||
|
for dim, votes in pairs(dimvotes) do
|
||
|
if best == nil or votes > best then
|
||
|
dimension = dim
|
||
|
end
|
||
|
end
|
||
|
if #state.fixes >= 3 then
|
||
|
for fixes in permutation(state.fixes, 3) do
|
||
|
local pos1, pos2 = trilaterate(fixes[1], fixes[2], fixes[3])
|
||
|
if pos1 and pos1.x == pos1.x and pos1.y == pos1.y and pos1.z == pos1.z then candidate_positions[tostring(pos1)] = pos1 end
|
||
|
if pos2 and pos2.x == pos2.x and pos2.y == pos2.y and pos2.z == pos2.z then candidate_positions[tostring(pos2)] = pos2 end
|
||
|
end
|
||
|
local best_error, best_candidate
|
||
|
for key, candidate in pairs(candidate_positions) do
|
||
|
local total_square_error = 0
|
||
|
for _, fix in pairs(state.fixes) do
|
||
|
total_square_error = total_square_error + ((candidate - fix.pos):length() - fix.distance)^2
|
||
|
end
|
||
|
local mean_square_error = total_square_error / #state.fixes
|
||
|
if best_error == nil or mean_square_error < best_error then
|
||
|
best_candidate = candidate
|
||
|
best_error = mean_square_error
|
||
|
end
|
||
|
end
|
||
|
if best_error < state.mse_threshold and (#state.fixes > 3 or #candidate_positions == 1) then
|
||
|
if state.debug then
|
||
|
print("Best candidate position is", best_candidate, "with error", best_error)
|
||
|
end
|
||
|
pos = best_candidate
|
||
|
return
|
||
|
else
|
||
|
if state.debug then
|
||
|
print("Position fix above error threshold:", best_candidate, best_error)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
}
|
||
|
|
||
|
if spawn_listener then table.insert(fns, listener) end
|
||
|
|
||
|
parallel.waitForAny(unpack(fns))
|
||
|
|
||
|
if spawn_listener then
|
||
|
state.listener_running = false
|
||
|
end
|
||
|
|
||
|
teardown()
|
||
|
|
||
|
if pos then
|
||
|
if state.debug then
|
||
|
print("Position is " .. pos.x .. "," .. pos.y .. "," .. pos.z)
|
||
|
end
|
||
|
if state.debug and dimension then
|
||
|
print("Dimension is", dimension)
|
||
|
end
|
||
|
return pos.x, pos.y, pos.z, dimension
|
||
|
else
|
||
|
if debug then
|
||
|
print("Could not determine position")
|
||
|
end
|
||
|
return nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return { CHANNEL_GPS = CHANNEL_GPS, locate = locate, configure = configure, listener = listener }
|