diff --git a/src/server/MapBuilder.luau b/src/server/MapBuilder.luau index acc76b3..d02d2e0 100644 --- a/src/server/MapBuilder.luau +++ b/src/server/MapBuilder.luau @@ -923,25 +923,110 @@ local function buildPA(map: Folder) }) slab.Parent = pa - -- painted bays - for i = -4, 4 do - local bay = part({ - Name = "Bay", - Size = Vector3.new(1, 2.2, 90), - CFrame = CFrame.new(c + Vector3.new(i * 34, 0.1, 0)), - Color = Color3.fromRGB(235, 235, 240), + -- REAL DAIKOKU FUTO PA LAYOUT --------------------------------------------- + -- Footprint measured from OpenStreetMap way 59608205 (大黒ふ頭パーキングエリア): + -- the apron is 181 m x 174 m (1.6 ha) -- a big, essentially SQUARE lot, not + -- a circular one. The spiral ramps sweep around and over it, which is the + -- "螺旋状の道路" the PA is famous for as a photo backdrop. + -- + -- Capacity from ja.wikipedia 大黒パーキングエリア (首都高速で最大のPA, opened + -- 1989-09-27): 小型 341 + 大型 59 + 身障者用 4 = 404 spaces. Those three + -- counts are reproduced EXACTLY below rather than approximated, because the + -- lot's character is the sheer amount of parking. + -- + -- Stall dimensions (小型 2.5 x 5.0 m, 大型 3.3 x 12 m, 身障者用 3.5 x 5.0 m, + -- 6 m aisles) are conventional Japanese parking-design values -- NOT read + -- from a primary standard, unlike the road markings. Paint is 15 cm, which + -- does match the 全標協 line width used on the mainline. + local APRON_X = 181 * M + local APRON_Z = 174 * M + local STALL_W = 2.5 * M + local STALL_D = 5.0 * M + local BIG_W = 3.3 * M + local BIG_D = 12.0 * M + local ACC_W = 3.5 * M + local AISLE = 6.0 * M + local LINE_W = 0.15 * M + local DECK_Y = c.Y + 1 -- top face of the apron slab + local C_PAINT = Color3.fromRGB(235, 235, 240) + + local function paint(name, size, x, z, colour) + local m = part({ + Name = name, + Size = size, + CFrame = CFrame.new(x, DECK_Y + 0.1, z), + Color = colour or C_PAINT, Material = Enum.Material.SmoothPlastic, }) - bay.Parent = pa + m.Parent = pa + return m end - -- light towers + -- One row of stalls: n+1 divider lines perpendicular to the aisle, plus the + -- head line the cars nose up to. `sign` is the direction the stalls extend + -- away from the aisle edge. + local function markRow(name, x0, zEdge, n, w, d, sign) + local zMid = zEdge + sign * d / 2 + for k = 0, n do + paint(name .. "Line", Vector3.new(LINE_W, 2.2, d), x0 + k * w, zMid) + end + paint(name .. "Head", Vector3.new(n * w, 2.2, LINE_W), x0 + n * w / 2, zEdge) + end + + -- 小型 341: eleven rows of 31, stacked as back-to-back pairs with 6 m aisles. + -- Z origin is derived, not eyeballed: the block starts one stall-depth south + -- of the 二番館 frontage and must finish north of the 大型 rows. Placing it by + -- hand overlapped the truck bays by 42 studs -- see verify_daikoku_pa. + local SMALL_PER_ROW = 31 + local SMALL_ROWS = 11 -- 31 x 11 = 341 exactly + local smallX0 = c.X - 20 * M + local pairPitch = 2 * STALL_D + AISLE + local nibanFace = c.Z - APRON_Z / 2 + 11 * M + (13 * M) / 2 + local smallZ0 = nibanFace + STALL_D + 8 + local placedSmall = 0 + for row = 0, SMALL_ROWS - 1 do + local pairIdx = math.floor(row / 2) + local zc = smallZ0 + pairIdx * pairPitch + local sign = (row % 2 == 0) and -1 or 1 + markRow("Stall", smallX0, zc, SMALL_PER_ROW, STALL_W, STALL_D, sign) + placedSmall += SMALL_PER_ROW + end + + -- 大型 59: trucks and buses park along the south edge where the bays are + -- long enough -- three rows of 20/20/19. + local bigZ = c.Z + APRON_Z / 2 - BIG_D - 14 + local bigCounts = { 20, 20, 19 } -- = 59 + local placedBig = 0 + for r, n in ipairs(bigCounts) do + markRow("BigStall", c.X - APRON_X / 2 + 18 * M, bigZ - (r - 1) * (BIG_D + AISLE), n, BIG_W, BIG_D, 1) + placedBig += n + end + + -- 身障者用 4: hard against the 本館 entrance, as they are in reality. + local accX = c.X - APRON_X / 2 + 34 * M + markRow("AccStall", accX, c.Z - 2 * ACC_W, 4, ACC_W, STALL_D, 1) + local placedAcc = 4 + + -- Recorded so the headless verifier can assert the real capacity. + local cap = Instance.new("StringValue") + cap.Name = "PACapacity" + cap.Value = ("small=%d,large=%d,accessible=%d,total=%d"):format( + placedSmall, + placedBig, + placedAcc, + placedSmall + placedBig + placedAcc + ) + cap.Parent = pa + + -- Light towers at the apron corners. for _, sx in ipairs({ -1, 1 }) do for _, sz in ipairs({ -1, 1 }) do + local lx = c.X + sx * (APRON_X / 2 - 10 * M) + local lz = c.Z + sz * (APRON_Z / 2 - 10 * M) local mast = part({ Name = "PAMast", Size = Vector3.new(3, 60, 3), - CFrame = CFrame.new(c + Vector3.new(sx * (R - 30), 30, sz * (R - 30))), + CFrame = CFrame.new(lx, DECK_Y + 30, lz), Color = Color3.fromRGB(70, 70, 78), Material = Enum.Material.Metal, }) @@ -949,7 +1034,7 @@ local function buildPA(map: Folder) local head = part({ Name = "PALight", Size = Vector3.new(10, 2, 10), - CFrame = CFrame.new(c + Vector3.new(sx * (R - 30), 60, sz * (R - 30))), + CFrame = CFrame.new(lx, DECK_Y + 60, lz), Color = Color3.fromRGB(255, 240, 200), Material = Enum.Material.Neon, }) @@ -1088,6 +1173,78 @@ local function buildPA(map: Folder) spawnLoc.Duration = 0 spawnLoc.Parent = pa + -- BUILDINGS --------------------------------------------------------------- + -- ja.wikipedia: 本館 sits on the WEST side of the area and ベイサイドテラス + -- 二番館 on the NORTH, and both are reachable from every direction. The + -- 本館 is genuinely three storeys (3F closed off, 2F a rest/meeting space + -- reopened 2025-10-30, 1F the shop/toilet block). Footprint sizes here are + -- eyeballed to the aerial -- unlike the apron extent and stall counts, they + -- are NOT measured, so treat them as art direction. + local C_BLDG = Color3.fromRGB(214, 210, 200) + local C_GLASS = Color3.fromRGB(120, 150, 165) + + local function building(name, sx, sy, sz, x, z) + local b = part({ + Name = name, + Size = Vector3.new(sx, sy, sz), + CFrame = CFrame.new(x, DECK_Y + sy / 2, z), + Color = C_BLDG, + Material = Enum.Material.Concrete, + }) + b.Parent = pa + -- Ground-floor glazing so it reads as a rest stop, not a warehouse. + local glass = part({ + Name = name .. "Glazing", + Size = Vector3.new(sx + 0.6, 4 * M, sz + 0.6), + CFrame = CFrame.new(x, DECK_Y + 2.6 * M, z), + Color = C_GLASS, + Material = Enum.Material.Glass, + Transparency = 0.35, + }) + glass.Parent = pa + return b + end + + -- 本館 (main building), west side, three storeys. + local honkanX = c.X - APRON_X / 2 + 13 * M + building("PAHonkan", 22 * M, 13 * M, 46 * M, honkanX, c.Z) + makeSign( + "本館 MAIN BUILDING", + CFrame.new(honkanX + 12 * M, DECK_Y + 15 * M, c.Z) * CFrame.Angles(0, math.pi / 2, 0), + pa + ) + + -- ベイサイドテラス二番館 (Bayside Terrace No.2), north side, single storey. + local nibanZ = c.Z - APRON_Z / 2 + 11 * M + building("PANibankan", 34 * M, 7 * M, 13 * M, c.X + 18 * M, nibanZ) + makeSign("ベイサイドテラス二番館", CFrame.new(c.X + 18 * M, DECK_Y + 9 * M, nibanZ + 7 * M), pa) + + -- EV rapid charger: first on any Japanese expressway, installed 2008-10-10. + for k = 0, 1 do + local ev = part({ + Name = "PAEVCharger", + Size = Vector3.new(1.6 * M, 2.2 * M, 0.9 * M), + CFrame = CFrame.new(honkanX + 14 * M + k * 4 * M, DECK_Y + 1.1 * M, c.Z + 26 * M), + Color = Color3.fromRGB(60, 130, 90), + Material = Enum.Material.Metal, + }) + ev.Parent = pa + end + + -- 段差: the real PA had bumps retrofitted specifically to keep illegally + -- lowered 車高短 cars out. Deliberately shallow here (~0.1 m) -- enough to + -- be visible and to unsettle a stiff car, not enough to beach anything. + for k = -1, 1 do + local bump = part({ + Name = "PALowCarBump", + Size = Vector3.new(26 * M, 0.1 * M, 0.8 * M), + CFrame = CFrame.new(c.X + k * 28 * M, DECK_Y + 0.05 * M, c.Z + APRON_Z / 2 - 4 * M), + Color = Color3.fromRGB(200, 190, 90), + Material = Enum.Material.Concrete, + }) + bump.Parent = pa + end + makeSign("大黒PA DAIKOKU PA", CFrame.new(c + Vector3.new(0, 26, -R + 10)), pa) end diff --git a/tools/push_mapbuilder.py b/tools/push_mapbuilder.py index 93bc33a..7838aa6 100644 --- a/tools/push_mapbuilder.py +++ b/tools/push_mapbuilder.py @@ -59,13 +59,17 @@ print(call('local rs=game:GetService("ReplicatedStorage")\n' 'return "buffer reset"')) for i, c in enumerate(chunks): + # Lua long-bracket literals SWALLOW a newline that immediately follows the + # opening bracket. When a chunk happened to start with "\n" that byte was + # silently lost, giving assembled=65158 vs expected=65159. Emit a deliberate + # leading newline for Lua to eat so the payload always survives intact. code = ( 'local rs=game:GetService("ReplicatedStorage")\n' 'local b=rs:WaitForChild("MBBuf")\n' f'local n="p{i}"\n' "local sv=b:FindFirstChild(n) or Instance.new(\"StringValue\")\n" "sv.Name=n sv.Parent=b\n" - f"sv.Value=[{eq}[{c}]{eq}]\n" + f"sv.Value=[{eq}[\n{c}]{eq}]\n" f'return "p{i} "..#sv.Value' ) print(f" chunk {i+1}/{len(chunks)}: {call(code)}") diff --git a/tools/verify_daikoku_pa.luau b/tools/verify_daikoku_pa.luau new file mode 100644 index 0000000..607d394 --- /dev/null +++ b/tools/verify_daikoku_pa.luau @@ -0,0 +1,176 @@ +-- Verify the Daikoku Futo PA rebuild against SOURCED real-world figures. +-- +-- Sources: +-- * ja.wikipedia 大黒パーキングエリア -- 小型 341 / 大型 59 / 身障者用 4 = 404 +-- * OpenStreetMap way 59608205 -- apron 181 m x 174 m +-- +-- Runs on a CLONE of MapBuilder so it never disturbs the live map, matching +-- the pattern used by verify_c1_fixes3. +local ServerScriptService = game:GetService("ServerScriptService") +local out = {} +local function log(...) + table.insert(out, table.concat({ ... }, " ")) +end +local pass, fail = 0, 0 +local function check(name, ok, detail) + if ok then + pass += 1 + log("PASS", name, detail or "") + else + fail += 1 + log("FAIL", name, detail or "") + end +end + +local src = ServerScriptService:FindFirstChild("Server") +src = src and src:FindFirstChild("MapBuilder") or ServerScriptService:FindFirstChild("MapBuilder", true) +if not src then + return "FATAL: MapBuilder module not found" +end +local MapBuilder = require(src:Clone()) +local map = MapBuilder.build and MapBuilder.build() or MapBuilder.Build() +local pa = map:FindFirstChild("ParkingArea", true) +if not pa then + return "FATAL: ParkingArea folder not built" +end + +-- 1. Capacity must be EXACTLY the published numbers ------------------------- +local cap = pa:FindFirstChild("PACapacity") +check("capacity value present", cap ~= nil, cap and cap.Value or "missing") +if cap then + check("capacity == 341/59/4/404", cap.Value == "small=341,large=59,accessible=4,total=404", cap.Value) +end + +-- 2. Stall line counts must agree with those numbers ------------------------ +-- markRow paints n+1 divider lines per row of n stalls. +local counts = {} +for _, d in ipairs(pa:GetDescendants()) do + counts[d.Name] = (counts[d.Name] or 0) + 1 +end +-- 小型: 11 rows x (31+1) = 352 dividers, 11 head lines +check("small stall dividers = 352", counts.StallLine == 352, tostring(counts.StallLine)) +check("small stall head lines = 11", counts.StallHead == 11, tostring(counts.StallHead)) +-- 大型: rows of 20,20,19 -> 21+21+20 = 62 dividers, 3 heads +check("large stall dividers = 62", counts.BigStallLine == 62, tostring(counts.BigStallLine)) +check("large stall head lines = 3", counts.BigStallHead == 3, tostring(counts.BigStallHead)) +-- 身障者用: 1 row of 4 -> 5 dividers +check("accessible dividers = 5", counts.AccStallLine == 5, tostring(counts.AccStallLine)) + +-- 3. Parking geometry must sit ON the apron slab --------------------------- +-- NOTE: scoped to parking furniture only. The first version of this test swept +-- every descendant and flagged PARingWall/PARingRail -- the ELEVATED ring road, +-- which is supposed to sit at/over the perimeter. That was a bad test, not a +-- bad build. +local floor = pa:FindFirstChild("PAFloor") +check("PAFloor exists", floor ~= nil) +local function isParkingFurniture(d) + return d.Name:find("Stall") + or d.Name:find("PAHonkan") + or d.Name:find("PANibankan") + or d.Name == "PAEVCharger" + or d.Name == "PALowCarBump" + or d.Name == "PAMast" +end +local offSlab, worst = 0, 0 +if floor then + local hx, hz = floor.Size.X / 2, floor.Size.Z / 2 + local fc = floor.Position + for _, d in ipairs(pa:GetDescendants()) do + if d:IsA("BasePart") and d ~= floor and isParkingFurniture(d) then + local dx = math.abs(d.Position.X - fc.X) + d.Size.X / 2 + local dz = math.abs(d.Position.Z - fc.Z) + d.Size.Z / 2 + local over = math.max(dx - hx, dz - hz) + if over > 0.5 then + offSlab += 1 + worst = math.max(worst, over) + if offSlab <= 5 then + log(" off-slab:", d.Name, string.format("by %.1f studs", over)) + end + end + end + end +end +check( + "no parking geometry overhangs the apron", + offSlab == 0, + string.format("%d parts, worst %.1f studs", offSlab, worst) +) + +-- 4. The 小型 block must not collide with the 大型 block -------------------- +-- The original test compared consecutive head-line gaps, which is always 0 for +-- back-to-back rows (they share a nose line) -- meaningless. It also hid a REAL +-- 42-stud overlap between the small rows and the truck bays. Compare Z spans. +local function zSpan(prefix) + local lo, hi = math.huge, -math.huge + for _, d in ipairs(pa:GetDescendants()) do + if d:IsA("BasePart") and d.Name:sub(1, #prefix) == prefix then + lo = math.min(lo, d.Position.Z - d.Size.Z / 2) + hi = math.max(hi, d.Position.Z + d.Size.Z / 2) + end + end + return lo, hi +end +local _, sHi = zSpan("Stall") +local bLo, bHi = zSpan("BigStall") +check( + "小型 block clears 大型 block", + sHi < bLo, + string.format("small ends %.1f, large starts %.1f (gap %.1f)", sHi, bLo, bLo - sHi) +) +check( + "大型 block within apron", + floor and bHi < floor.Position.Z + floor.Size.Z / 2, + string.format("large ends %.1f", bHi) +) + +-- 5. Buildings on the correct sides ----------------------------------------- +local honkan = pa:FindFirstChild("PAHonkan") +local niban = pa:FindFirstChild("PANibankan") +check("本館 exists", honkan ~= nil) +check("二番館 exists", niban ~= nil) +if honkan and floor then + check( + "本館 on the WEST side", + honkan.Position.X < floor.Position.X, + string.format("dx %.0f", honkan.Position.X - floor.Position.X) + ) +end +if niban and floor then + check( + "二番館 on the NORTH side", + niban.Position.Z < floor.Position.Z, + string.format("dz %.0f", niban.Position.Z - floor.Position.Z) + ) +end + +-- 6. Nothing may intersect the buildings ------------------------------------ +local clash = 0 +for _, b in ipairs({ honkan, niban }) do + if b then + local hx, hz = b.Size.X / 2, b.Size.Z / 2 + for _, d in ipairs(pa:GetDescendants()) do + if d:IsA("BasePart") and d ~= b and d.Name:find("Stall") then + local dx = math.abs(d.Position.X - b.Position.X) - (hx + d.Size.X / 2) + local dz = math.abs(d.Position.Z - b.Position.Z) - (hz + d.Size.Z / 2) + if dx < 0 and dz < 0 then + clash += 1 + if clash <= 5 then + log(" clash:", d.Name, b.Name) + end + end + end + end + end +end +check("no stalls inside a building", clash == 0, tostring(clash) .. " overlaps") + +-- 7. Documented amenities --------------------------------------------------- +check("EV chargers present (2008 first-in-Japan)", (counts.PAEVCharger or 0) >= 1, tostring(counts.PAEVCharger)) +check("low-car bumps present (段差)", (counts.PALowCarBump or 0) >= 1, tostring(counts.PALowCarBump)) +check("light masts = 4", counts.PAMast == 4, tostring(counts.PAMast)) +check("PASpawn survived", pa:FindFirstChildOfClass("SpawnLocation") ~= nil) + +map:Destroy() +log("") +log(string.format("RESULT %d passed, %d failed", pass, fail)) +return table.concat(out, "\n")