random-stuff/computercraft/ni-ctl.lua

678 lines
24 KiB
Lua

local username = settings.get "username" or "gollark"
local ni = peripheral.wrap "back"
local speaker = peripheral.find "speaker"
local modem = peripheral.find "modem"
local offload_laser = settings.get "offload_laser"
local w, h = ni.canvas().getSize()
if _G.thing_group then
pcall(_G.thing_group.remove)
end
if _G.canvas3d_group then
pcall(_G.canvas3d_group.clear)
pcall(_G.canvas3d_group.remove)
end
local group
local group_3d
local function initialize_group_thing()
if group then pcall(group.remove) end
if group_3d then pcall(group_3d.remove) end
group = ni.canvas().addGroup({ w - 70, 10 })
ni.canvas3d().clear()
group_3d = ni.canvas3d().create()
_G.thing_group = group
_G.canvas3d_group = group_3d
end
initialize_group_thing()
local targets = {}
local use_spudnet = offload_laser
local spudnet_send, spudnet_background
if use_spudnet then
print "SPUDNET interface loading."
spudnet_send, spudnet_background = require "ni-ctl_spudnet_interface"()
end
local function offload_protocol(...)
spudnet_send { "exec", {...} }
end
local function is_target(name)
for target, type in pairs(targets) do
if name:lower():match(target) then return type end
end
end
local function vector_sqlength(self)
return self.x * self.x + self.y * self.y + self.z * self.z
end
local function project(line_start, line_dir, point)
local t = (point - line_start):dot(line_dir) / vector_sqlength(line_dir)
return line_start + line_dir * t, t
end
local function calc_yaw_pitch(v)
local x, y, z = v.x, v.y, v.z
local pitch = -math.atan2(y, math.sqrt(x * x + z * z))
local yaw = math.atan2(-x, z)
return math.deg(yaw), math.deg(pitch)
end
local settings_cfg = {
brake = { type = "bool", default = true, shortcode = "b" },
counter = { type = "bool", default = false, shortcode = "c" }, -- counterattack
highlight = { type = "bool", default = false, shortcode = "h" },
dodge = { type = "bool", default = true, shortcode = "d" },
power = { type = "number", default = 5, max = 5, min = 0.5 }, -- laser power
flight = { type = "string", default = "std", shortcode = "f", alias = { fly = true } },
drill = { type = "bool", default = false, shortcode = "D", persist = false },
show_acceleration = { type = "bool", default = false },
pitch_controls = { type = "bool", default = false },
ext_highlight = { type = "bool", default = false }
}
local SAVEFILE = "ni-ctl-settings"
if fs.exists(SAVEFILE) then
local f = fs.open(SAVEFILE, "r")
for key, value in pairs(textutils.unserialise(f.readAll())) do
settings_cfg[key].value = value
end
f.close()
end
local gsettings = {}
setmetatable(gsettings, {
__index = function(_, key)
local cfg = settings_cfg[key]
if cfg.value == nil then return cfg.default else return cfg.value end
end,
__newindex = function(_, key, value)
print("set", key, "to", value)
settings_cfg[key].value = value
if settings_cfg[key].persist ~= false then
local kv = {}
for key, cfg in pairs(settings_cfg) do
kv[key] = cfg.value
end
local f = fs.open(SAVEFILE, "w")
f.write(textutils.serialise(kv))
f.close()
end
os.queueEvent "settings_change"
end
})
local work_queue = {}
local addressed_lasers = {}
local function bool_to_yn(b)
if b == true then return "y"
elseif b == false then return "n"
else return "?" end
end
local status_lines = {}
local notices = {}
local function push_notice(t)
table.insert(notices, { t, os.epoch "utc" })
end
local function lase(entity)
local target_location = entity.s
for i = 1, 5 do
target_location = entity.s + entity.v * (target_location:length() / 1.5)
end
local y, p = calc_yaw_pitch(target_location)
if offload_laser then offload_protocol("fire", y, p, gsettings.power) else ni.fire(y, p, gsettings.power) end
end
local user_meta
local fast_mode_reqs = {}
local colortheme = {
status = 0xFFFFFFFF,
notice = 0xFF8800FF,
follow = 0xFF00FFFF,
watch = 0xFFFF00FF,
laser = 0xFF0000FF,
entity = 0x00FFFFFF,
select = 0x00FF00FF
}
local function schedule(fn, time, uniquename)
if uniquename then
work_queue[uniquename] = { os.clock() + time, fn }
else
table.insert(work_queue, { os.clock() + time, fn })
end
end
local function direction_vector(yaw, pitch)
return vector.new(
-math.sin(math.rad(yaw)) * math.cos(math.rad(pitch)),
-math.sin(math.rad(pitch)),
math.cos(math.rad(yaw)) * math.cos(math.rad(pitch))
)
end
--[[
local function ni.launch(yaw, pitch, power)
ni.ni.launch(yaw, pitch, power)
if user_meta then
local impulse = vector.new(
-math.sin(math.rad(yaw)) * math.cos(math.rad(pitch)),
math.cos(math.rad(yaw)) * math.cos(math.rad(pitch)),
-math.sin(math.rad(pitch))
)
if user_meta.isElytraFlying then
impulse = 0.4 * impulse
end
user_meta.velocity = user_meta.velocity + (impulse * power)
end
end]]
local gravity_motion_offset = 0.07840001
--[[
local inav_position = nil
local inav_delta = nil
local scaler = 20
local function navigation()
while true do
local real_pos = vector.new(gps.locate())
if inav_position then
print(inav_position - real_pos)
local real_delta = (inav_position - real_pos):length()
local delta_size = inav_delta:length()
print("calculated delta was", delta_size / real_delta, "of real")
end
inav_position = real_pos
inav_delta = vector.new(0, 0, 0)
sleep(3)
end
end
]]
local hud_entities = {}
local function render_hud()
local flags = {}
for key, cfg in pairs(settings_cfg) do
if cfg.shortcode and cfg.type == "bool" then
if gsettings[key] then
table.insert(flags, cfg.shortcode)
end
end
end
table.sort(flags)
status_lines.flags = "Flags: " .. table.concat(flags)
local i = 0
local ok, err = pcall(group.clear)
if not ok then
initialize_group_thing()
end
local time = os.epoch "utc"
for _, text in pairs(status_lines) do
group.addText({ 0, i * 7 }, text, colortheme.status, 0.6)
i = i + 1
end
for ix, info in pairs(notices) do
if time >= (info[2] + 2000) then notices[ix] = nil end
group.addText({ 0, i * 7 }, info[1], colortheme.notice, 0.6)
i = i + 1
end
for thing, count in pairs(hud_entities) do
local text = thing
if count ~= 1 then text = text .. " " .. count end
group.addText({ 0, i * 7 }, text, colortheme[is_target(thing) or "entity"], 0.6)
i = i + 1
end
end
local last_velocity
local last_time
local integrated_position = vector.new(0, 0, 0)
local function update_motion_vars(new_meta)
local time = os.clock()
if user_meta then
-- walking hack
if not (user_meta.isFlying or user_meta.isElytraFlying) then
if user_meta.isInWater then new_meta.motionY = new_meta.motionY + 0.02
else new_meta.motionY = new_meta.motionY + gravity_motion_offset end
end
user_meta.velocity = vector.new(new_meta.motionX, new_meta.motionY, new_meta.motionZ)
user_meta.real_velocity = vector.new(new_meta.deltaPosX, new_meta.deltaPosY, new_meta.deltaPosZ)
integrated_position = integrated_position + user_meta.real_velocity
user_meta.motionX = new_meta.motionX
user_meta.motionY = new_meta.motionY
user_meta.motionZ = new_meta.motionZ
user_meta.pitch = new_meta.pitch
user_meta.yaw = new_meta.yaw
if last_time and last_velocity then
local timestep = time - last_time
user_meta.acceleration = (user_meta.velocity - last_velocity) / timestep
end
last_velocity = user_meta.velocity
end
last_time = time
end
local function scan_entities()
while true do
fast_mode_reqs.laser = false
fast_mode_reqs.acting = false
local entities = ni.sense()
local maybe_players = {}
hud_entities = {}
local lasers = {}
for _, entity in pairs(entities) do
entity.s = vector.new(entity.x, entity.y, entity.z)
entity.v = vector.new(entity.motionX, entity.motionY, entity.motionZ)
if entity.deltaPosX then
entity.v = vector.new(entity.deltaPosX, entity.deltaPosY, entity.deltaPosZ)
end
if entity.displayName ~= username then
hud_entities[entity.displayName] = (hud_entities[entity.displayName] or 0) + 1
end
if entity.displayName ~= username and entity.displayName == entity.name and (math.floor(entity.yaw) ~= entity.yaw and math.floor(entity.pitch) ~= entity.pitch) then -- player, quite possibly
entity.v = entity.v + vector.new(0, gravity_motion_offset, 0)
table.insert(maybe_players, entity)
end
if entity.name == "plethora:laser" then
fast_mode_reqs.laser = true
end
if entity.name == "plethora:laser" and not addressed_lasers[entity.id] then
local closest_approach, param = project(entity.s, entity.v - user_meta.velocity, vector.new(0, 0, 0))
if param > 0 and vector_sqlength(closest_approach) < 5 then
push_notice "Laser detected"
fast_mode_reqs.laser = true
local time_to_impact = (entity.s:length() / (entity.v - user_meta.velocity):length()) / 20
print("got inbound laser", time_to_impact, vector_sqlength(closest_approach), param)
addressed_lasers[entity.id] = true
if gsettings.dodge then
schedule(function()
push_notice "Executing dodging"
local dir2d = vector.new(entity.motionX - user_meta.motionX, 0, entity.motionZ - user_meta.motionZ)
local perpendicular_dir2d = vector.new(1, 0, -dir2d.x / dir2d.z)
-- NaN contingency measures
if perpendicular_dir2d.x ~= perpendicular_dir2d.x or perpendicular_dir2d.z ~= perpendicular_dir2d.z then
perpendicular_dir2d = vector.new(-dir2d.z / dir2d.x, 0, 1)
end
if perpendicular_dir2d.x ~= perpendicular_dir2d.x or perpendicular_dir2d.z ~= perpendicular_dir2d.z then
local theta = math.random() * math.pi * 2
perpendicular_dir2d = vector.new(math.cos(theta), 0, math.sin(theta))
end
local y, p = calc_yaw_pitch(perpendicular_dir2d)
if math.random(1, 2) == 1 then p = -p end
ni.launch(y, p, 3)
end, math.max(0, time_to_impact / 2 - 0.1))
end
schedule(function() addressed_lasers[entity.id] = false end, 15)
table.insert(lasers, entity)
end
end
end
for _, laser in pairs(lasers) do
for _, player in pairs(maybe_players) do
local closest_approach, param = project(laser.s, laser.v, player.s)
print(player.displayName, closest_approach, param)
if param < 0 and vector_sqlength(closest_approach - player.s) < 8 and gsettings.counter then
print("execute counterattack", player.displayName)
push_notice(("Counterattack %s"):format(player.displayName))
targets[player.displayName:lower()] = "laser"
end
end
end
render_hud()
pcall(function()
group_3d.clear()
group_3d.recenter()
end)
for _, entity in pairs(entities) do
local action = is_target(entity.displayName)
if action then
if action == "laser" then
schedule(function() lase(entity) end, 0, entity.id)
elseif action == "watch" then
schedule(function() ni.look(calc_yaw_pitch(entity.s)) end, 0, entity.id)
elseif action == "follow" then
schedule(function()
local y, p = calc_yaw_pitch(entity.s)
ni.launch(y, p, math.min(entity.s:length() / 24, 2))
end, 0, entity.id)
end
fast_mode_reqs.acting = true
end
if gsettings.highlight and hud_entities[entity.displayName] and hud_entities[entity.displayName] < 20 then
local color = colortheme[action or "entity"]
local object = group_3d.addBox(entity.x - 0.25, entity.y - 0.25, entity.z - 0.25)
object.setColor(color)
object.setAlpha(128)
object.setDepthTested(false)
object.setSize(0.5, 0.5, 0.5)
if gsettings.ext_highlight then
local frame = group_3d.addFrame({entity.x - 0.25, entity.y + 0.25, entity.z - 0.25})
frame.setDepthTested(false)
frame.addText({0, 0}, entity.displayName, nil, 3)
end
end
end
local fast_mode = false
for _, m in pairs(fast_mode_reqs) do
fast_mode = fast_mode or m
end
--status_lines.fast_mode = "Fast scan: " .. bool_to_yn(fast_mode)
if fast_mode then sleep(0.1) else sleep(0.2) end
end
end
local flight_shortcodes = {
o = "off",
b = "brake",
h = "hpower",
l = "lpower",
s = "std",
a = "align",
v = "hover"
}
local flight_powers = {
std = 1,
lpower = 0.5,
hpower = 4,
align = 1,
hover = 1
}
local flight_target = nil
local function xz_plane(v)
return vector.new(v.x, 0, v.z)
end
-- As far as I can tell, a speed of more than 10 in the X/Z plane causes a reset of your velocity by the server and thus horrible rubberbanding.
local function maxvel_compensatory_launch(yaw, pitch, power)
local effective_power = (user_meta and user_meta.isElytraFlying) and (power * 0.4) or power
local impulse = direction_vector(yaw, pitch) * effective_power
local power_over_velocity_limit = math.max(xz_plane(user_meta.velocity + impulse):length() - 10, 0)
if user_meta and user_meta.isElytraFlying then
power = power - power_over_velocity_limit / 0.4
else
power = power - power_over_velocity_limit
end
power = math.min(math.max(power, 0), 4)
if power > 0 then
ni.launch(yaw, pitch, power)
end
end
local function run_flight()
if flight_shortcodes[gsettings.flight] then gsettings.flight = flight_shortcodes[gsettings.flight] end
local disp = gsettings.flight
if user_meta.deltaPosY < -0.3 and gsettings.brake then
ni.launch(0, 270, math.max(0.4, math.min(4, -user_meta.motionY / 1.5)))
--ni.launch(0, 270, 0.4)
--end
fast_mode_reqs.flying = true
disp = disp .. " F"
else
fast_mode_reqs.flying = false
end
if gsettings.flight == "std" or gsettings.flight == "hpower" or gsettings.flight == "lpower" or gsettings.flight == "align" or gsettings.flight == "hover" then
if user_meta.isElytraFlying or user_meta.isSneaking then
fast_mode_reqs.flying = true
end
if user_meta.isElytraFlying ~= user_meta.isSneaking then
if not user_meta.isAirborne and user_meta.pitch < -15 then
push_notice "Fast takeoff"
ni.launch(0, 270, 1)
end
local power = flight_powers[gsettings.flight]
if user_meta.isInWater then
power = power * 2
end
local yaw, pitch = user_meta.yaw, user_meta.pitch
if pitch == 90 and gsettings.pitch_controls then
local y, p = calc_yaw_pitch(-user_meta.velocity)
ni.launch(y, p, math.min(user_meta.velocity:length(), 4))
else
local raw_direction = direction_vector(yaw, pitch)
local impulse = vector.new(raw_direction.x, raw_direction.y * 1.5, raw_direction.z) * power
impulse = impulse + vector.new(0, 0.1, 0)
local y, p = calc_yaw_pitch(impulse)
maxvel_compensatory_launch(y, (gsettings.flight ~= "align" and p) or 10, math.min(4, impulse:length()))
end
end
elseif gsettings.flight == "brake" then
local y, p = calc_yaw_pitch(-user_meta.velocity)
ni.launch(y, p, math.min(user_meta.velocity:length(), 1))
fast_mode_reqs.flying = true
end
status_lines.flight = "Flight: " .. disp
end
local function ll_flight_control()
while true do
local ok, user_meta_temp
if ni.getMetaOwner then
ok, user_meta_temp = pcall(ni.getMetaOwner, username)
else
ok, user_meta_temp = pcall(ni.getMetaByName, username)
end
if not ok or not user_meta_temp then
speaker.playSound("entity.enderdragon.death")
user_meta = nil
for name, cfg in pairs(settings_cfg) do
if cfg.persist == false then
cfg.value = nil
end
end
work_queue = {}
ni = peripheral.wrap "back"
ni.canvas().clear()
error("Failed to fetch user metadata (assuming death): " .. tostring(user_meta_temp))
end
user_meta = user_meta_temp
update_motion_vars(user_meta)
if user_meta.acceleration and gsettings.show_acceleration then
status_lines.acceleration = ("Acc: %.2f/%.2f"):format(user_meta.acceleration:length(), user_meta.acceleration.y)
end
status_lines.vel = ("Vel: %.2f/%.2f"):format(user_meta.velocity:length(), user_meta.motionY)
render_hud()
local fast_mode = false
for _, m in pairs(fast_mode_reqs) do
fast_mode = fast_mode or m
end
--status_lines.fast_mode = "Fast scan: " .. bool_to_yn(fast_mode)
schedule(run_flight, 0, "flight")
if not fast_mode then sleep(0.1) end
end
end
local function queue_handler()
while true do
local init = os.clock()
for index, arg in pairs(work_queue) do
if arg[1] <= os.clock() then
arg[2]()
work_queue[index] = nil
end
end
if os.clock() == init then sleep() end
end
end
local function estimate_tps()
while true do
local game_time_start = os.epoch "utc"
sleep(5)
local game_time_end = os.epoch "utc"
local utc_elapsed_seconds = (game_time_end - game_time_start) / 5000
status_lines.tps = ("TPS: %.0f"):format(20 / utc_elapsed_seconds)
end
end
local function within_epsilon(a, b)
return math.abs(a - b) < 1
end
-- TODO: unified navigation framework
local function fly_to_target()
local last_s, last_t
while true do
while not user_meta do sleep() end
if flight_target then
local x, y, z = gps.locate()
if not y then push_notice "GPS error"
else
if y < 256 then
ni.launch(0, 270, 4)
end
local position = vector.new(x, 0, z)
local curr_t = os.clock()
local displacement = flight_target - position
status_lines.flight_target = ("%d from %d %d"):format(displacement:length(), flight_target.x, flight_target.z)
local real_displacement = displacement
if last_t then
local delta_t = curr_t - last_t
local delta_s = displacement - last_s
local deriv = delta_s * (1/delta_t)
displacement = displacement + deriv
end
local pow = math.max(math.min(4, displacement:length() / 40), 0)
local yaw, pitch = calc_yaw_pitch(displacement)
maxvel_compensatory_launch(yaw, pitch, pow)
--sleep(0)
last_t = curr_t
last_s = real_displacement
if within_epsilon(position.x, flight_target.x) and within_epsilon(position.z, flight_target.z) then flight_target = nil end
end
else
status_lines.flight_target = nil
end
sleep(0.1)
end
end
local function handle_commands()
while true do
local _, user, command, args = os.pullEvent "command"
if user == username then
if command == "lase" then
if args[1] then
targets[table.concat(args, " "):lower()] = "laser"
end
elseif command == "ctg" then
args[1] = args[1] or ".*"
local arg = table.concat(args, " ")
for k, v in pairs(targets) do
if k:lower():match(arg) then
chatbox.tell(user, k .. ": " .. v)
targets[k:lower()] = nil
end
end
elseif command == "watch" then
if args[1] then
targets[table.concat(args, " "):lower()] = "watch"
end
elseif command == "select" then
if args[1] then
targets[table.concat(args, " "):lower()] = "select"
end
elseif command == "follow" then
if args[1] then
targets[table.concat(args, " "):lower()] = "follow"
end
elseif command == "notice_test" then
push_notice(table.concat(args, " "))
elseif command == "flyto" then
if args[1] == "cancel" or args[1] == nil then
flight_target = nil
else
local x, z = tonumber(args[1]), tonumber(args[2])
if type(x) ~= "number" or type(z) ~= "number" then
chatbox.tell(user, "Usage: \\flyto x z")
else
flight_target = vector.new(x, 0, z)
end
end
elseif command == "update" then
local h, e = http.get "https://osmarks.net/stuff/ni-ctl.lua"
assert(h, "HTTP: " .. (e or ""))
local data = h.readAll()
h.close()
local file = fs.open(shell.getRunningProgram(), "w")
file.write(data)
file.close()
chatbox.tell(user, "Update updated.")
else
for key, cfg in pairs(settings_cfg) do
if key == command or cfg.shortcode == command or (cfg.alias and cfg.alias[command]) then
if cfg.type == "bool" then
if args[1] and (args[1]:match "y" or args[1]:match "t" or args[1]:match "on") then
gsettings[key] = true
elseif args[1] and (args[1]:match "f" or args[1]:match "^n") then
gsettings[key] = false
else
gsettings[key] = not gsettings[key]
end
chatbox.tell(user, ("%s: %s"):format(key, tostring(gsettings[key])))
elseif cfg.type == "number" then
local value = tonumber(args[1])
if not value then chatbox.tell(user, "Not a number") end
if cfg.max and value > cfg.max then chatbox.tell(user, ("Max is %d"):format(cfg.max)) end
if cfg.min and value < cfg.min then chatbox.tell(user, ("Max is %d"):format(cfg.min)) end
gsettings[key] = value
else
gsettings[key] = args[1]
end
break
end
end
end
end
end
end
local function drill()
while true do
if gsettings.drill then
repeat sleep() until user_meta
if offload_laser then
offload_protocol("fire", user_meta.yaw, user_meta.pitch, gsettings.power)
else
schedule(function() repeat sleep() until user_meta ni.fire(user_meta.yaw, user_meta.pitch, gsettings.power) end, 0, "drill")
end
sleep(0.1)
else
os.pullEvent "settings_change"
end
end
end
while true do
local cmds = {ll_flight_control, queue_handler, scan_entities, handle_commands, estimate_tps, fly_to_target, drill}
if spudnet_background then
table.insert(cmds, spudnet_background)
end
local ok, err = pcall(parallel.waitForAny, unpack(cmds))
if err == "Terminated" then break end
printError(err)
sleep(0.2)
end