-- 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 nb 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