Aux-lane suppression + PA geometry fixes; probe/verify tooling
CI / quality (push) Failing after 1m9s
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
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
-- 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")
|
||||
Reference in New Issue
Block a user