diff --git a/c1_georeference.json b/c1_georeference.json new file mode 100644 index 0000000..b0dc770 --- /dev/null +++ b/c1_georeference.json @@ -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" + ] +} \ No newline at end of file diff --git a/default.project.json b/default.project.json index a36e560..532cfb3 100644 --- a/default.project.json +++ b/default.project.json @@ -42,13 +42,14 @@ "$className": "Lighting", "$properties": { "Technology": "Future", - "ClockTime": 0.0, - "Brightness": 1.0, - "Ambient": [0.05, 0.05, 0.1], - "OutdoorAmbient": [0.1, 0.1, 0.18], - "FogEnd": 900.0, - "FogColor": [0.03, 0.03, 0.08], - "EnvironmentDiffuseScale": 0.4, + "ClockTime": 14.0, + "Brightness": 2.0, + "Ambient": [0.35, 0.35, 0.38], + "OutdoorAmbient": [0.45, 0.46, 0.5], + "FogEnd": 6000.0, + "FogColor": [0.55, 0.6, 0.68], + "EnvironmentDiffuseScale": 1.0, + "EnvironmentSpecularScale": 1.0, "GlobalShadows": true } } diff --git a/src/server/AntiCheat.luau b/src/server/AntiCheat.luau index 8e98e7e..c28c51d 100644 --- a/src/server/AntiCheat.luau +++ b/src/server/AntiCheat.luau @@ -25,6 +25,19 @@ type Track = { lastPos: Vector3, lastValid: CFrame } local tracked: { [Player]: Track } = {} 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 char = player.Character 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 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. hrp.CFrame = track.lastValid hrp.AssemblyLinearVelocity = Vector3.zero diff --git a/src/server/CarManager.luau b/src/server/CarManager.luau index 2d2349b..3255cc0 100644 --- a/src/server/CarManager.luau +++ b/src/server/CarManager.luau @@ -30,6 +30,8 @@ local Chassis = require(Shared.Chassis) local GameConfig = require(Shared.GameConfig) local DataManager = require(script.Parent.DataManager) local CarBody = require(script.Parent.CarBody) +local AntiCheat = require(script.Parent.AntiCheat) +local PhysicsService = game:GetService("PhysicsService") type Car = Cars.Car type Tuning = Chassis.Tuning @@ -39,6 +41,35 @@ local CarManager = {} local activeCars: { [Player]: Model } = {} 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 --------------------------------------------------------------------- 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 hubAtt = makeAttachment(hub, "HubAtt", VERTICAL_AXIS) - local prismatic = Instance.new("PrismaticConstraint") - prismatic.Name = "SuspSlide_" .. cornerName - prismatic.Attachment0 = bodyAtt - prismatic.Attachment1 = hubAtt - prismatic.LimitsEnabled = true - prismatic.LowerLimit = -Chassis.SUSPENSION_TRAVEL / 2 - prismatic.UpperLimit = Chassis.SUSPENSION_TRAVEL / 2 - prismatic.Parent = body - - local spring = Instance.new("SpringConstraint") - spring.Name = "SuspSpring_" .. cornerName - spring.Attachment0 = bodyAtt - spring.Attachment1 = hubAtt - spring.FreeLength = 0.2 - spring.Stiffness = tuning.SpringStiffness - spring.Damping = tuning.SpringDamping - spring.LimitsEnabled = false - spring.Parent = body + -- Rigid hub mount. A PrismaticConstraint+SpringConstraint suspension was used + -- here, but the hub (0.6 cube) is ~3 orders of magnitude lighter than the + -- ballasted body, and that mass ratio across a stiff spring makes the solver + -- diverge: the hub/knuckle/wheel assemblies get flung to ~1e7 studs, fall + -- past FallenPartsDestroyHeight and are deleted -- leaving a car with a body + -- and no wheels that cannot move. Welding the hub makes it part of the body + -- assembly, which is rock stable. Ride is rigid; the car actually drives. + local hubWeld = Instance.new("WeldConstraint") + hubWeld.Name = "SuspWeld_" .. cornerName + hubWeld.Part0 = body + hubWeld.Part1 = hub + hubWeld.Parent = body -- Front corners: a knuckle that yaws on a servo hinge (steering) ------------ 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) 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 return model end -- 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 map = Workspace:FindFirstChild("Map") - if map then - local pad = map:FindFirstChild("SpawnPad", true) + local deadline = os.clock() + 10 + repeat + local map = Workspace:FindFirstChild("Map") + local pad = map and map:FindFirstChild("SpawnPad", true) if pad and pad:IsA("BasePart") then return pad.CFrame * CFrame.new(0, 5, 0) 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) end @@ -288,8 +327,12 @@ function CarManager.spawn(player: Player, carId: string?) local tuning = Chassis.tuningFor(car) local model = buildCarModel(car, tuning) - model:PivotTo(spawnCFrame()) + local target = spawnCFrame() 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("CarId", id) @@ -307,6 +350,12 @@ function CarManager.spawn(player: Player, carId: string?) if character then local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat 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() seat:Sit(character:FindFirstChildOfClass("Humanoid")) end) diff --git a/src/server/MapBuilder.luau b/src/server/MapBuilder.luau index bb9c393..b774073 100644 --- a/src/server/MapBuilder.luau +++ b/src/server/MapBuilder.luau @@ -333,6 +333,21 @@ local function buildPA(map: Folder) }) 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) end