-- startup.lua — Hypertube catcher (CC:Tweaked + Advanced Peripherals)
-- Stack (bottom→top, adjacent to hypertube exit): Player Detector, Computer, Redstone Integrator, Piston
-- ===== CONFIG =====
local CONFIG = {
pistonY = 72, -- absolute Y of the piston face level
side = "up", -- Redstone Integrator side wired to the piston (e.g., "up","north","east"...)
range = 12, -- detector radius to watch below/around the exit
holdSec = 0.30, -- how long to keep piston extended
pistonExtendSec = 0.10, -- piston actuation latency (~2 ticks ≈ 0.10 s)
sampleDt = 0.05, -- sampling interval (seconds). 1 MC tick = 0.05 s
minUpVel = 0.20, -- require at least this upward m/s before arming
fireOncePerPass = true, -- avoid multiple triggers until the player clears the zone
targetPlayer = nil, -- nil=any player; or set a specific player name
}
-- ===================
-- Peripherals
local det = peripheral.find("playerDetector") or error("No playerDetector")
local ri = peripheral.find("redstoneIntegrator") or error("No redstoneIntegrator")
-- Redstone helpers (boolean API works on AP integrator)
local function piston(on) ri.setOutput(CONFIG.side, on and true or false) end
-- Simple per-player tracker
local tracks = {} -- name -> {hist = { {t=.., y=.., v=..} ... max 3}, armed=true/false, cool=false}
local function clamp(n,a,b) if n<a then return a elseif n>b then return b else return n end end
-- Quadratic arrival solve: y(t) = y0 + v0 t + 0.5 a t^2 → find t where y(t)=Ypiston
local function arrivalTime(y0, v0, a, yTarget)
local dy = yTarget - y0
if math.abs(a) < 1e-5 then
if v0 <= 0 then return nil end
return dy / v0
end
-- 0.5 a t^2 + v0 t - dy = 0
local A = 0.5 * a
local B = v0
local C = -dy
local disc = B*B - 4*A*C
if disc < 0 then return nil end
local sqrtD = math.sqrt(disc)
local t1 = (-B + sqrtD) / (2*A)
local t2 = (-B - sqrtD) / (2*A)
local t = math.huge
if t1 and t1 > 0 then t = math.min(t, t1) end
if t2 and t2 > 0 then t = math.min(t, t2) end
if t == math.huge then return nil end
return t
end
-- Update one player's kinematics
local function updateTrack(name, yNow, tNow)
local tr = tracks[name]
if not tr then tr = { hist = {}, armed = true, cool = false } tracks[name] = tr end
local h = tr.hist
-- push sample
local vNow = nil
if #h >= 1 then
local dt = math.max(1e-3, tNow - h[#h].t)
vNow = (yNow - h[#h].y) / dt
end
table.insert(h, { t = tNow, y = yNow, v = vNow })
while #h > 3 do table.remove(h, 1) end
-- compute a from last two velocities if available
local a = 0
if #h >= 3 and h[#h-1].v and h[#h].v then
local dv = h[#h].v - h[#h-1].v
local dt = math.max(1e-3, h[#h].t - h[#h-1].t)
a = dv / dt
elseif #h >= 2 and h[#h].v then
a = 0 -- fallback linear
end
return tr, h[#h].v or 0, a
end
-- Fire control with simple pass-based cooldown
local function shouldArm(tr, y, v)
if CONFIG.fireOncePerPass then
if tr.cool then
-- re-arm after player leaves the band around piston
if y > CONFIG.pistonY + 2 or y < CONFIG.pistonY - 6 then
tr.cool = false; tr.armed = true
end
end
return tr.armed
end
return true
end
local function markFired(tr)
if CONFIG.fireOncePerPass then
tr.armed = false
tr.cool = true
end
end
-- Main loop
print("Hypertube catcher armed. Watching for players...")
while true do
local t0 = os.epoch("utc") / 1000.0
-- enumerate players in range (prefer exact name if set)
local names = {}
if CONFIG.targetPlayer then
if det.isPlayerInRange and det.isPlayerInRange(CONFIG.targetPlayer, CONFIG.range) then
names = { CONFIG.targetPlayer }
end
else
if det.getPlayersInRange then
names = det.getPlayersInRange(CONFIG.range) or {}
elseif det.getOnlinePlayers then
-- fallback: filter by distance manually if only positions are available
local online = det.getOnlinePlayers() or {}
for _,n in ipairs(online) do
local p = det.getPlayerPos(n); if p then
local dx = 0 - (p.x or 0) -- unknown computer pos; range check only on Y/Z is fine for a vertical column
local dz = 0 - (p.z or 0)
if math.sqrt(dx*dx + dz*dz) <= CONFIG.range then table.insert(names, n) end
end
end
end
end
for _,name in ipairs(names) do
local pos = det.getPlayerPos and det.getPlayerPos(name)
if pos and pos.y then
local tr, v, a = updateTrack(name, pos.y, t0)
-- Only consider players rising toward the piston from below
if pos.y <= CONFIG.pistonY + 1 and v and v > CONFIG.minUpVel and shouldArm(tr, pos.y, v) then
local tArr = arrivalTime(pos.y, v, a, CONFIG.pistonY)
if tArr then
local fireIn = tArr - CONFIG.pistonExtendSec
if fireIn <= 0 then
-- immediate fire
piston(true)
sleep(CONFIG.holdSec)
piston(false)
markFired(tr)
else
-- schedule within this loop tick (bounded by our sampleDt)
local wait = clamp(fireIn, 0, CONFIG.sampleDt)
sleep(wait)
piston(true)
sleep(CONFIG.holdSec)
piston(false)
markFired(tr)
end
end
end
end
end
sleep(CONFIG.sampleDt)
end
Comments