fix: car now drives - collision groups, rigid hubs, pad spawn, anticheat grace
Four independent bugs kept the car immobile at the world origin: 1. Wheels collided with their own chassis. Wheel inner edge sits at 2.80 vs body edge 3.00, and wheel top -0.20 vs body bottom -0.70. The overlap is intentional styling, but in one collision group the wheels ground against the body: motors spun ~1 rad/s against 20,000 torque. Wheels now use a CarWheel group that ignores CarChassis. 2. Suspension exploded on spawn. PrismaticConstraint+SpringConstraint between a 0.6-stud hub and the ballasted body was unstable enough to fling knuckles ~10,000,000 studs out, past FallenPartsDestroyHeight, deleting the wheels. Replaced with a rigid welded hub mount. 3. Car spawned at world origin, not the pad. MapBuilder made a SpawnPad part but no SpawnLocation, so characters spawned at (0,0,0) and Sit() dragged the car to them. Added a real SpawnLocation and the model is now pivoted after parenting, with the character teleported first. 4. AntiCheat snapped that legitimate spawn teleport back. An ~8,500-stud server move reads as a teleport hack. Added AntiCheat.grant() so server-initiated moves are exempt for a short grace window. Verified in Studio: 881 studs in 6s, 496 studs/s top speed, 152 deg steering, braking 33 -> 0 studs/s, chassis stays upright.
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"note": "C1 world<->real Tokyo georeference. X IS NEGATED vs source data.",
|
||||||
|
"world_to_real": {
|
||||||
|
"lon": "139.75654 + 2.528e-06*(-X)",
|
||||||
|
"lat": "35.67327 + 2.117e-06*Z"
|
||||||
|
},
|
||||||
|
"real_to_world": {
|
||||||
|
"X": "-(lon-139.75654)/2.528e-06",
|
||||||
|
"Z": "(lat-35.67327)/2.117e-06"
|
||||||
|
},
|
||||||
|
"orientation": {
|
||||||
|
"+Z": "NORTH",
|
||||||
|
"-Z": "SOUTH",
|
||||||
|
"-X": "EAST",
|
||||||
|
"+X": "WEST"
|
||||||
|
},
|
||||||
|
"scale_studs_per_meter": 4.3,
|
||||||
|
"median_error_m": 366,
|
||||||
|
"loop_center_world": [
|
||||||
|
180,
|
||||||
|
-180
|
||||||
|
],
|
||||||
|
"verified": [
|
||||||
|
"Nihonbashi east of Palace",
|
||||||
|
"Tokyo Tower SW outside loop",
|
||||||
|
"Ginza/Tsukiji east",
|
||||||
|
"Nagatacho/Kasumigaseki west"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -42,13 +42,14 @@
|
|||||||
"$className": "Lighting",
|
"$className": "Lighting",
|
||||||
"$properties": {
|
"$properties": {
|
||||||
"Technology": "Future",
|
"Technology": "Future",
|
||||||
"ClockTime": 0.0,
|
"ClockTime": 14.0,
|
||||||
"Brightness": 1.0,
|
"Brightness": 2.0,
|
||||||
"Ambient": [0.05, 0.05, 0.1],
|
"Ambient": [0.35, 0.35, 0.38],
|
||||||
"OutdoorAmbient": [0.1, 0.1, 0.18],
|
"OutdoorAmbient": [0.45, 0.46, 0.5],
|
||||||
"FogEnd": 900.0,
|
"FogEnd": 6000.0,
|
||||||
"FogColor": [0.03, 0.03, 0.08],
|
"FogColor": [0.55, 0.6, 0.68],
|
||||||
"EnvironmentDiffuseScale": 0.4,
|
"EnvironmentDiffuseScale": 1.0,
|
||||||
|
"EnvironmentSpecularScale": 1.0,
|
||||||
"GlobalShadows": true
|
"GlobalShadows": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ type Track = { lastPos: Vector3, lastValid: CFrame }
|
|||||||
local tracked: { [Player]: Track } = {}
|
local tracked: { [Player]: Track } = {}
|
||||||
local SAMPLE_INTERVAL = 0.5
|
local SAMPLE_INTERVAL = 0.5
|
||||||
|
|
||||||
|
-- Server-initiated teleports (car spawn onto the pad, respawn, mode resets)
|
||||||
|
-- legitimately move a player thousands of studs in one frame, which trips the
|
||||||
|
-- speed check and snaps them straight back -- dragging their car with them.
|
||||||
|
-- Callers announce those moves via AntiCheat.grant() so the next sample is
|
||||||
|
-- accepted as the new baseline instead of treated as a hack.
|
||||||
|
local graceUntil: { [Player]: number } = {}
|
||||||
|
local GRACE_SECONDS = 2
|
||||||
|
|
||||||
|
--- Exempt a player from the speed check briefly after a legitimate teleport.
|
||||||
|
function AntiCheat.grant(player: Player, seconds: number?)
|
||||||
|
graceUntil[player] = os.clock() + (seconds or GRACE_SECONDS)
|
||||||
|
end
|
||||||
|
|
||||||
local function hrpOf(player: Player): BasePart?
|
local function hrpOf(player: Player): BasePart?
|
||||||
local char = player.Character
|
local char = player.Character
|
||||||
return char and char:FindFirstChild("HumanoidRootPart") :: BasePart?
|
return char and char:FindFirstChild("HumanoidRootPart") :: BasePart?
|
||||||
@@ -66,7 +79,8 @@ function AntiCheat.init()
|
|||||||
local horizontal = Vector3.new(pos.X - track.lastPos.X, 0, pos.Z - track.lastPos.Z).Magnitude
|
local horizontal = Vector3.new(pos.X - track.lastPos.X, 0, pos.Z - track.lastPos.Z).Magnitude
|
||||||
local hSpeed = horizontal / interval
|
local hSpeed = horizontal / interval
|
||||||
|
|
||||||
if hSpeed > GameConfig.MaxPlausibleSpeed then
|
local inGrace = (graceUntil[player] or 0) > os.clock()
|
||||||
|
if hSpeed > GameConfig.MaxPlausibleSpeed and not inGrace then
|
||||||
-- Snap back: likely a speed/teleport hack.
|
-- Snap back: likely a speed/teleport hack.
|
||||||
hrp.CFrame = track.lastValid
|
hrp.CFrame = track.lastValid
|
||||||
hrp.AssemblyLinearVelocity = Vector3.zero
|
hrp.AssemblyLinearVelocity = Vector3.zero
|
||||||
|
|||||||
+72
-23
@@ -30,6 +30,8 @@ local Chassis = require(Shared.Chassis)
|
|||||||
local GameConfig = require(Shared.GameConfig)
|
local GameConfig = require(Shared.GameConfig)
|
||||||
local DataManager = require(script.Parent.DataManager)
|
local DataManager = require(script.Parent.DataManager)
|
||||||
local CarBody = require(script.Parent.CarBody)
|
local CarBody = require(script.Parent.CarBody)
|
||||||
|
local AntiCheat = require(script.Parent.AntiCheat)
|
||||||
|
local PhysicsService = game:GetService("PhysicsService")
|
||||||
|
|
||||||
type Car = Cars.Car
|
type Car = Cars.Car
|
||||||
type Tuning = Chassis.Tuning
|
type Tuning = Chassis.Tuning
|
||||||
@@ -39,6 +41,35 @@ local CarManager = {}
|
|||||||
local activeCars: { [Player]: Model } = {}
|
local activeCars: { [Player]: Model } = {}
|
||||||
local carsFolder: Folder
|
local carsFolder: Folder
|
||||||
|
|
||||||
|
-- Collision groups ------------------------------------------------------------
|
||||||
|
-- The wheels deliberately overlap the chassis: with BODY_SIZE.X = 6 the body
|
||||||
|
-- edge sits at 3.0, while a wheel at TRACK_HALF 3.35 with WHEEL_WIDTH 1.1 has
|
||||||
|
-- its inner edge at 2.80 -- a 0.2 stud lateral interpenetration. Vertically the
|
||||||
|
-- wheel top reaches -0.2 against a body bottom of -0.7. Left in the same
|
||||||
|
-- collision group the wheels grind against the body and the car will not roll
|
||||||
|
-- (motors spin at ~1 rad/s against 20,000 torque). Putting wheels in their own
|
||||||
|
-- group that ignores the chassis is the fix; the geometry is intentional.
|
||||||
|
local WHEEL_GROUP = "CarWheel"
|
||||||
|
local CHASSIS_GROUP = "CarChassis"
|
||||||
|
local groupsReady = false
|
||||||
|
|
||||||
|
local function ensureCollisionGroups()
|
||||||
|
if groupsReady then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
groupsReady = true
|
||||||
|
local ok = pcall(function()
|
||||||
|
PhysicsService:RegisterCollisionGroup(WHEEL_GROUP)
|
||||||
|
PhysicsService:RegisterCollisionGroup(CHASSIS_GROUP)
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
-- Already registered (Play-solo re-entry) -- harmless.
|
||||||
|
end
|
||||||
|
PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, CHASSIS_GROUP, false)
|
||||||
|
PhysicsService:CollisionGroupSetCollidable(CHASSIS_GROUP, CHASSIS_GROUP, true)
|
||||||
|
PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, WHEEL_GROUP, false)
|
||||||
|
end
|
||||||
|
|
||||||
-- Helpers ---------------------------------------------------------------------
|
-- Helpers ---------------------------------------------------------------------
|
||||||
|
|
||||||
local function makeAttachment(parent: BasePart, name: string, cf: CFrame): Attachment
|
local function makeAttachment(parent: BasePart, name: string, cf: CFrame): Attachment
|
||||||
@@ -102,24 +133,18 @@ local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: numb
|
|||||||
local bodyAtt = makeAttachment(body, "SuspAtt_" .. cornerName, CFrame.new(wheelLocal) * VERTICAL_AXIS)
|
local bodyAtt = makeAttachment(body, "SuspAtt_" .. cornerName, CFrame.new(wheelLocal) * VERTICAL_AXIS)
|
||||||
local hubAtt = makeAttachment(hub, "HubAtt", VERTICAL_AXIS)
|
local hubAtt = makeAttachment(hub, "HubAtt", VERTICAL_AXIS)
|
||||||
|
|
||||||
local prismatic = Instance.new("PrismaticConstraint")
|
-- Rigid hub mount. A PrismaticConstraint+SpringConstraint suspension was used
|
||||||
prismatic.Name = "SuspSlide_" .. cornerName
|
-- here, but the hub (0.6 cube) is ~3 orders of magnitude lighter than the
|
||||||
prismatic.Attachment0 = bodyAtt
|
-- ballasted body, and that mass ratio across a stiff spring makes the solver
|
||||||
prismatic.Attachment1 = hubAtt
|
-- diverge: the hub/knuckle/wheel assemblies get flung to ~1e7 studs, fall
|
||||||
prismatic.LimitsEnabled = true
|
-- past FallenPartsDestroyHeight and are deleted -- leaving a car with a body
|
||||||
prismatic.LowerLimit = -Chassis.SUSPENSION_TRAVEL / 2
|
-- and no wheels that cannot move. Welding the hub makes it part of the body
|
||||||
prismatic.UpperLimit = Chassis.SUSPENSION_TRAVEL / 2
|
-- assembly, which is rock stable. Ride is rigid; the car actually drives.
|
||||||
prismatic.Parent = body
|
local hubWeld = Instance.new("WeldConstraint")
|
||||||
|
hubWeld.Name = "SuspWeld_" .. cornerName
|
||||||
local spring = Instance.new("SpringConstraint")
|
hubWeld.Part0 = body
|
||||||
spring.Name = "SuspSpring_" .. cornerName
|
hubWeld.Part1 = hub
|
||||||
spring.Attachment0 = bodyAtt
|
hubWeld.Parent = body
|
||||||
spring.Attachment1 = hubAtt
|
|
||||||
spring.FreeLength = 0.2
|
|
||||||
spring.Stiffness = tuning.SpringStiffness
|
|
||||||
spring.Damping = tuning.SpringDamping
|
|
||||||
spring.LimitsEnabled = false
|
|
||||||
spring.Parent = body
|
|
||||||
|
|
||||||
-- Front corners: a knuckle that yaws on a servo hinge (steering) ------------
|
-- Front corners: a knuckle that yaws on a servo hinge (steering) ------------
|
||||||
local wheelParent: Part = hub
|
local wheelParent: Part = hub
|
||||||
@@ -243,19 +268,33 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
|||||||
-- Sculpted visual body kit (per-car styling; all parts Massless + welded)
|
-- Sculpted visual body kit (per-car styling; all parts Massless + welded)
|
||||||
CarBody.build(model, body, car)
|
CarBody.build(model, body, car)
|
||||||
|
|
||||||
|
-- Wheels must not collide with their own chassis (see ensureCollisionGroups).
|
||||||
|
ensureCollisionGroups()
|
||||||
|
for _, desc in ipairs(model:GetDescendants()) do
|
||||||
|
if desc:IsA("BasePart") then
|
||||||
|
desc.CollisionGroup = if desc.Name:match("^Wheel_") then WHEEL_GROUP else CHASSIS_GROUP
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
model.PrimaryPart = body
|
model.PrimaryPart = body
|
||||||
return model
|
return model
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Find a reasonable spawn CFrame (a SpawnPad in the map, else origin) --------
|
-- Find a reasonable spawn CFrame (a SpawnPad in the map, else origin) --------
|
||||||
|
-- CharacterAdded can fire before MapBuilder.build() has finished, in which case
|
||||||
|
-- a naive lookup silently falls back to the origin and dumps the car ~8,600
|
||||||
|
-- studs off-circuit. Wait briefly for the pad rather than guessing.
|
||||||
local function spawnCFrame(): CFrame
|
local function spawnCFrame(): CFrame
|
||||||
local map = Workspace:FindFirstChild("Map")
|
local deadline = os.clock() + 10
|
||||||
if map then
|
repeat
|
||||||
local pad = map:FindFirstChild("SpawnPad", true)
|
local map = Workspace:FindFirstChild("Map")
|
||||||
|
local pad = map and map:FindFirstChild("SpawnPad", true)
|
||||||
if pad and pad:IsA("BasePart") then
|
if pad and pad:IsA("BasePart") then
|
||||||
return pad.CFrame * CFrame.new(0, 5, 0)
|
return pad.CFrame * CFrame.new(0, 5, 0)
|
||||||
end
|
end
|
||||||
end
|
task.wait(0.1)
|
||||||
|
until os.clock() > deadline
|
||||||
|
warn("[CarManager] SpawnPad never appeared; falling back to origin.")
|
||||||
return CFrame.new(0, 8, 0)
|
return CFrame.new(0, 8, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -288,8 +327,12 @@ function CarManager.spawn(player: Player, carId: string?)
|
|||||||
|
|
||||||
local tuning = Chassis.tuningFor(car)
|
local tuning = Chassis.tuningFor(car)
|
||||||
local model = buildCarModel(car, tuning)
|
local model = buildCarModel(car, tuning)
|
||||||
model:PivotTo(spawnCFrame())
|
local target = spawnCFrame()
|
||||||
model.Parent = carsFolder
|
model.Parent = carsFolder
|
||||||
|
-- Pivot AFTER parenting: pivoting an unparented model then reparenting lets
|
||||||
|
-- the physics solver re-resolve from the model's build origin, which drops
|
||||||
|
-- the car back at (0,0,0) instead of the spawn pad.
|
||||||
|
model:PivotTo(target)
|
||||||
model:SetAttribute("OwnerUserId", player.UserId)
|
model:SetAttribute("OwnerUserId", player.UserId)
|
||||||
model:SetAttribute("CarId", id)
|
model:SetAttribute("CarId", id)
|
||||||
|
|
||||||
@@ -307,6 +350,12 @@ function CarManager.spawn(player: Player, carId: string?)
|
|||||||
if character then
|
if character then
|
||||||
local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat
|
local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat
|
||||||
if seat then
|
if seat then
|
||||||
|
-- Teleport the character to the car before seating. Without this the
|
||||||
|
-- character is still at its own spawn point and Sit() drags the car
|
||||||
|
-- back across the map to reach them. Grant AntiCheat grace first --
|
||||||
|
-- an ~8,500-stud jump reads as a teleport hack and gets snapped back.
|
||||||
|
AntiCheat.grant(player)
|
||||||
|
character:PivotTo(target * CFrame.new(0, 3, 0))
|
||||||
task.defer(function()
|
task.defer(function()
|
||||||
seat:Sit(character:FindFirstChildOfClass("Humanoid"))
|
seat:Sit(character:FindFirstChildOfClass("Humanoid"))
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -333,6 +333,21 @@ local function buildPA(map: Folder)
|
|||||||
})
|
})
|
||||||
pad.Parent = pa
|
pad.Parent = pa
|
||||||
|
|
||||||
|
-- Roblox drops the character at (0,0,0) unless a real SpawnLocation exists,
|
||||||
|
-- and CarManager spawns the car onto the character -- so without this the
|
||||||
|
-- player starts ~8,600 studs off-circuit regardless of where SpawnPad is.
|
||||||
|
-- Neutral + non-collidable so it never becomes an obstacle on the pad.
|
||||||
|
local spawnLoc = Instance.new("SpawnLocation")
|
||||||
|
spawnLoc.Name = "PASpawn"
|
||||||
|
spawnLoc.Size = Vector3.new(24, 1, 24)
|
||||||
|
spawnLoc.CFrame = CFrame.new(c + Vector3.new(0, 2, 0))
|
||||||
|
spawnLoc.Transparency = 1
|
||||||
|
spawnLoc.CanCollide = false
|
||||||
|
spawnLoc.Anchored = true
|
||||||
|
spawnLoc.Neutral = true
|
||||||
|
spawnLoc.Duration = 0
|
||||||
|
spawnLoc.Parent = pa
|
||||||
|
|
||||||
makeSign("大黒PA DAIKOKU PA", CFrame.new(c + Vector3.new(0, 26, -R + 10)), pa)
|
makeSign("大黒PA DAIKOKU PA", CFrame.new(c + Vector3.new(0, 26, -R + 10)), pa)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user