CI / quality (push) Failing after 1m9s
MapBuilder.luau
- Suppress the aux lane across samples 525-550 (AUX_SUPPRESS_FROM/TO),
honoring the user's hand deletion in Studio so a rebuild no longer
regenerates it.
- Smoothstep the width ramp over AUX_BLEND=5 samples on each side. A hard
cut produced a 26-stud lateral fence jog; the blend removes it.
- DO NOT remove this suppression. Re-enabling it resurrects geometry the
user deleted by hand.
rokit.toml
- rojo 7.5.1 -> 7.7.0 to match the auto-updating Studio plugin.
Server older than plugin fails the handshake with
"attempt to index number with 'protocolVersion'".
tools/
- 34 read-only probe/verify/capture scripts used to measure the datamodel
instead of reasoning from source.
Verified-good baseline at this commit:
total parts 16788
PAWestSlip 60 segs, 34.4 deg total bend, worst 1.4, 0 kinks >5
PAEastSlip 60 segs, 30.7 deg total bend, worst 1.3, 0 kinks >5
AuxLane parts 30 (samples 525-550 deliberately absent)
barrier coverage 823 samples, 0 unguarded
fence jogs >10st 0
195 lines
6.9 KiB
Luau
195 lines
6.9 KiB
Luau
-- Verify the speed-change (auxiliary) lanes and that the slips no longer cut
|
|
-- across mainline traffic.
|
|
--
|
|
-- Rebuilds from a CLONED module first (require() cache bypass), then measures.
|
|
-- Every check below can FAIL -- and asserts it actually tested something, so a
|
|
-- lookup returning nothing reports FAIL rather than vacuously passing.
|
|
local sss = game:GetService("ServerScriptService")
|
|
local ss = game:GetService("ServerStorage")
|
|
local ws = game:GetService("Workspace")
|
|
local RS = game:GetService("ReplicatedStorage")
|
|
|
|
local realMod = sss:FindFirstChild("MapBuilder", true)
|
|
if not realMod then return "MapBuilder not found" end
|
|
|
|
local bust = ss:FindFirstChild("_cachebust")
|
|
if not bust then
|
|
bust = Instance.new("Folder")
|
|
bust.Name = "_cachebust"
|
|
bust.Parent = ss
|
|
end
|
|
for _, ch in ipairs(bust:GetChildren()) do
|
|
ch:Destroy()
|
|
end
|
|
local clone = realMod:Clone()
|
|
clone.Name = "MapBuilder_aux_" .. tostring(math.random(1e6, 9e6))
|
|
clone.Parent = bust
|
|
|
|
local mb = require(clone)
|
|
local old = ws:FindFirstChild("Map")
|
|
if old then old:Destroy() end
|
|
local ok, err = pcall(function() mb.build() end)
|
|
if not ok then return "BUILD ERROR: " .. tostring(err) end
|
|
|
|
local map = ws:FindFirstChild("Map")
|
|
if not map then return "no Map after build" end
|
|
|
|
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
|
|
local out = {}
|
|
local function say(s) table.insert(out, s) end
|
|
|
|
-- Collect the aux lane decks and the slip decks.
|
|
local aux = {}
|
|
local decks = { PAWestSlip = {}, PAEastSlip = {} }
|
|
for _, d in ipairs(map:GetDescendants()) do
|
|
if d:IsA("BasePart") then
|
|
local ai = d.Name:match("^AuxLane_(%d+)$")
|
|
if ai then
|
|
table.insert(aux, { i = tonumber(ai), p = d.Position, s = d.Size, cf = d.CFrame })
|
|
end
|
|
for slipName, list in pairs(decks) do
|
|
local idx = d.Name:match("^" .. slipName .. "_Deck_(%d+)$")
|
|
if idx then
|
|
table.insert(list, { i = tonumber(idx), p = d.Position, s = d.Size, cf = d.CFrame })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 1. The aux lanes must exist at all.
|
|
if #aux == 0 then
|
|
say("FAIL: no AuxLane_* parts were built")
|
|
return table.concat(out, "\n")
|
|
end
|
|
say(("aux lane segments built: %d"):format(#aux))
|
|
|
|
-- 2. Every aux segment must sit OUTSIDE the mainline deck, not on top of it.
|
|
-- Measure the gap between the mainline centreline and the aux centreline,
|
|
-- and compare against that sample's own half-width.
|
|
local samples = Circuit.samples
|
|
local minClear, minAt = math.huge, -1
|
|
local tested = 0
|
|
for _, a in ipairs(aux) do
|
|
local s = samples[a.i]
|
|
if s then
|
|
tested += 1
|
|
local d = (Vector3.new(a.p.X, 0, a.p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
|
|
-- inner edge of aux lane relative to mainline centre
|
|
local innerEdge = d - a.s.X / 2
|
|
if innerEdge < minClear then
|
|
minClear, minAt = innerEdge, a.i
|
|
end
|
|
end
|
|
end
|
|
if tested == 0 then
|
|
say("FAIL: no aux segment matched a circuit sample (harness looked up nothing)")
|
|
else
|
|
say(("aux inner edge vs mainline centre: min %.1f studs at sample %d (tested %d)")
|
|
:format(minClear, minAt, tested))
|
|
end
|
|
|
|
-- 3. The slip decks must START at the aux lane offset, not on the centreline.
|
|
-- This is the actual bug the user reported: lat began at 0.
|
|
for slipName, list in pairs(decks) do
|
|
table.sort(list, function(x, y) return x.i < y.i end)
|
|
if #list < 50 then
|
|
say(("%s FAIL: only %d deck segments"):format(slipName, #list))
|
|
else
|
|
local first = list[1]
|
|
-- Find the nearest mainline sample to the slip's first segment.
|
|
local bestD, bestI = math.huge, -1
|
|
for i = 1, #samples do
|
|
local d = (Vector3.new(first.p.X, 0, first.p.Z) - Vector3.new(samples[i].pos.X, 0, samples[i].pos.Z)).Magnitude
|
|
if d < bestD then bestD, bestI = d, i end
|
|
end
|
|
local halfW = 0
|
|
do
|
|
-- Deck half-width at that sample, read off the built road itself.
|
|
local road = map:FindFirstChild("Road_" .. bestI, true)
|
|
halfW = road and road.Size.X / 2 or 0
|
|
end
|
|
local innerEdge = bestD - first.s.X / 2
|
|
local verdict = (halfW > 0 and innerEdge >= halfW - 1.5) and "PASS" or "FAIL"
|
|
say(("%s start: centre %.1f from mainline centre, inner edge %.1f, deck half-width %.1f -> %s")
|
|
:format(slipName, bestD, innerEdge, halfW, verdict))
|
|
end
|
|
end
|
|
|
|
-- 4. Continuity: the slip mouth and the nearest aux segment must be adjacent
|
|
-- (end-to-end), not overlapping and not separated by a hole.
|
|
for slipName, list in pairs(decks) do
|
|
if #list >= 50 then
|
|
local first = list[1]
|
|
local bestGap, bestI = math.huge, -1
|
|
for _, a in ipairs(aux) do
|
|
local g = (a.p - first.p).Magnitude
|
|
if g < bestGap then bestGap, bestI = g, a.i end
|
|
end
|
|
local verdict = bestGap <= 90 and "PASS" or "FAIL"
|
|
say(("%s mouth -> nearest aux segment: %.1f studs (sample %d) -> %s")
|
|
:format(slipName, bestGap, bestI, verdict))
|
|
end
|
|
end
|
|
|
|
-- 5. The aux lane must be level with the mainline deck at the mouth.
|
|
local maxStep, stepAt = 0, -1
|
|
for _, a in ipairs(aux) do
|
|
local road = map:FindFirstChild("Road_" .. a.i, true)
|
|
if road then
|
|
local step = math.abs((a.p.Y + a.s.Y / 2) - (road.Position.Y + road.Size.Y / 2))
|
|
if step > maxStep then maxStep, stepAt = step, a.i end
|
|
end
|
|
end
|
|
say(("aux/mainline surface step: max %.2f studs at sample %d -> %s")
|
|
:format(maxStep, stepAt, maxStep <= 0.05 and "PASS" or "FAIL"))
|
|
|
|
-- 6. The taper must actually taper: widths should fall to near zero at the end.
|
|
local wmin, wmax = math.huge, 0
|
|
for _, a in ipairs(aux) do
|
|
wmin = math.min(wmin, a.s.X)
|
|
wmax = math.max(wmax, a.s.X)
|
|
end
|
|
say(("aux width range: %.1f .. %.1f studs -> %s")
|
|
:format(wmin, wmax, (wmax >= 25 and wmin <= 8) and "PASS (full lane + taper)" or "CHECK"))
|
|
|
|
-- 7. DECELERATION LANES. Each mouth must now have lane on BOTH sides: one
|
|
-- upstream (decel, for traffic leaving) and one downstream (accel, for
|
|
-- traffic joining). Group the aux segments into runs and check each slip
|
|
-- mouth has a run on either side of it.
|
|
table.sort(aux, function(x, y) return x.i < y.i end)
|
|
local runs, cur = {}, { aux[1] }
|
|
for k = 2, #aux do
|
|
if aux[k].i - aux[k - 1].i > 3 then
|
|
table.insert(runs, cur); cur = {}
|
|
end
|
|
table.insert(cur, aux[k])
|
|
end
|
|
table.insert(runs, cur)
|
|
say(("aux runs: %d (expect 4 = 2 mouths x {decel, accel})"):format(#runs))
|
|
for ri, r in ipairs(runs) do
|
|
local wlo, whi = math.huge, 0
|
|
for _, a in ipairs(r) do
|
|
wlo = math.min(wlo, a.s.X); whi = math.max(whi, a.s.X)
|
|
end
|
|
say((" run %d: samples %d..%d (%d segs) width %.1f..%.1f")
|
|
:format(ri, r[1].i, r[#r].i, #r, wlo, whi))
|
|
end
|
|
-- Each slip join must sit between a decel run and an accel run.
|
|
for _, slip in ipairs({ { "PAWestSlip", 525 }, { "PAEastSlip", 551 } }) do
|
|
local name, join = slip[1], slip[2]
|
|
local before, after = false, false
|
|
for _, r in ipairs(runs) do
|
|
if r[#r].i < join and join - r[#r].i <= 6 then before = true end
|
|
if r[1].i > join and r[1].i - join <= 6 then after = true end
|
|
if r[1].i <= join and r[#r].i >= join then
|
|
-- run spans the join: counts as adjacent on both sides
|
|
before = true; after = true
|
|
end
|
|
end
|
|
say(("%s join=%d: decel-side run %s, accel-side run %s -> %s")
|
|
:format(name, join, tostring(before), tostring(after),
|
|
(before and after) and "PASS" or "FAIL"))
|
|
end
|
|
|
|
return table.concat(out, "\n")
|