feat: Add metro transit, gadgets, shop, social apps, and traffic models checkpoint

This commit is contained in:
CAtlas0784 committed 2026-10-29 01:55:01 +07:00
1 parent caa42b84ed
commit 74e2963a12
79 files changed
+527962 -496

No files matched your search

@@ -178,7 +178,8 @@ internal sealed partial class GameRouter
/// rightOffset meters to the right of the facing, +0.15m up.
/// </summary>
internal static async Task<(bool Ok, string Message, ulong EntityId, ulong Token, Vec3 SpawnPos)> SpawnDirectAsync(
TcpSession session, uint configId, Vec3 nearPos, float facing, float rightOffset, byte sourceType, string reason)
TcpSession session, uint configId, Vec3 nearPos, float facing, float rightOffset, byte sourceType, string reason,
bool pushSummonStart = false)
{
if (configId == 0)
return (false, "vehicle 0 does not exist", 0, 0, default);
@@ -229,8 +230,8 @@ internal sealed partial class GameRouter
CreateSourceType = sourceType,
EntityId = entityId,
VehicleConfigId = configId,
Parts = [],
SuitId = 0,
Parts = BuildAppearance(GetStateIfExists(session), configId, out var enterSuit, includeStatParts: false).ToList(),
SuitId = enterSuit,
Position = pos,
Facing = facing,
EulerAngles = euler,
@@ -255,6 +256,15 @@ internal sealed partial class GameRouter
await session.NotifyAsync(MethodId.SyncChangeVehicleInteractable, UxSerializer.Serialize(
new SceneMethods.SyncChangeVehicleInteractable { VehicleInstanceId = entityId, Interactable = true }),
CancellationToken.None);
// Register the car as the player's own summon (ApplyCarManager.IsMySummonVehicle
// reads DriveVehiclesManager.CurrentSummonVehicle, which only this packet fills) —
// the autoshop/tuning gates reject "not-own" cars without it.
if (pushSummonStart)
{
await session.NotifyAsync(MethodId.SyncStartSummonVehicle, UxSerializer.Serialize(
new SceneMethods.SummonVehicleResult { VehicleEntityId = entityId, TaskToken = token }),
CancellationToken.None);
}
session.Log.Info($"[VEHICLE] DIRECT_SPAWN reason={reason} config={configId} entity={entityId} token={token} seats={seatCount} spawn=({spawn.X:F1},{spawn.Y:F1},{spawn.Z:F1})");
return (true, "spawned", entityId, token, spawn);
}
@@ -269,20 +279,7 @@ internal sealed partial class GameRouter
session.Log.Info($"[VEHICLE] destroy entity={entityId} reason={reason}");
}
internal static SceneMethods.AskGetUnlockedVehiclesResult BuildUnlockedResult()
{
var fleet = PrivateServerConfigStore.Current.Gameplay.Vehicles.FleetIds;
return new SceneMethods.AskGetUnlockedVehiclesResult
{
Vehicles = fleet.Select(id => new SceneMethods.PlayerVehicleClientDetail
{
Id = id,
Parts = [],
SuitId = 0,
IsPersistent = true,
}).ToList(),
};
}
/// <summary>
/// Push the owned fleet right after world entry finalization (first gameplay movement),
@@ -303,7 +300,9 @@ internal sealed partial class GameRouter
}
if (!first)
return;
var result = BuildUnlockedResult();
// Restore persisted tuning purchases (parts/suits/session unlocks) from disk.
VehicleTuningStore.LoadInto(Profile.PlayerPid, world);
var result = BuildUnlockedResult(world);
await ctx.NotifyAsync(MethodId.SyncAllUnlockedVehicles, result);
ctx.Session.Log.Info($"[VEHICLE] garage auto-published count={result.Vehicles.Count}");
}
@@ -337,9 +336,7 @@ internal sealed partial class GameRouter
RaidId = raidId,
HasZoneGraph = hasZoneGraph,
ZoneStorageDataHandle = hasZoneGraph ? settings.ZoneStorageDataHandle : 0,
Intersections = [],
Vehicles = [],
StaticVehicles = [],
Intersections = BuildAetherIntersectionInitList(),
});
ctx.Session.Log.Info($"[VEHICLE] aether-init raid={raidId} zoneGraph={hasZoneGraph} lists=empty");
}
@@ -347,7 +344,7 @@ internal sealed partial class GameRouter
[Handler(MethodId.AskGetUnlockedVehicles, HandlerPacketKind.Invoke)]
private Task AskGetUnlockedVehicles(Connection conn, UxRpcMessage msg)
{
var result = BuildUnlockedResult();
var result = BuildUnlockedResult(GetStateIfExists(conn.Session));
conn.Log.Info($"[VEHICLE] unlocked list -> {result.Vehicles.Count} vehicles");
return conn.ReturnAsync(msg, result);
}
@@ -379,6 +376,16 @@ internal sealed partial class GameRouter
conn.Log.Info($"[VEHICLE] AskPlayerStartEnterOrExitVehicle vehicle={drive.VehicleEntityId} enter={drive.EnterOrLeave} seat={drive.SeatIndex}");
if (drive.EnterOrLeave)
{
// Aether traffic vehicles (server-driven cars + their NPC drivers) are NOT logic
// vehicles: running the logic-vehicle boarding flow on them crashes the client.
// The proper takeover is the borrow flow below (AskAetherAIBorrowVehicle), so a
// direct press here is only honoured for non-Aether vehicles.
if (Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.IsAetherTrafficVehicle(drive.VehicleEntityId)
|| Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.IsAetherTrafficNpc(drive.VehicleEntityId))
{
conn.Log.Info($"[VEHICLE] F-press on Aether traffic vehicle={drive.VehicleEntityId} deferred to borrow flow");
return;
}
Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.NotifyVehicleBoarding(drive.VehicleEntityId);
await ForceEnterVehicleAsync(conn.Session, drive.VehicleEntityId);
}
@@ -393,6 +400,61 @@ internal sealed partial class GameRouter
}
}
/// <summary>
/// Traffic-car takeover: the client asks to borrow an Aether vehicle, the server grants it
/// by detaching the car from the traffic simulation, removing its AI driver/passengers and
/// flipping the control type to RaidVehicle so the player can drive it.
/// </summary>
[Handler(MethodId.AskAetherAIBorrowVehicle, HandlerPacketKind.Invoke)]
private async Task AskAetherAIBorrowVehicle(Connection conn, UxRpcMessage msg)
{
var args = msg.GetArgs<SceneMethods.AskAetherAIBorrowVehicle4229938>();
conn.Log.Info($"[VEHICLE] borrow aether vehicle={args.VehicleInstanceId}");
await conn.ReturnEmptyOkAsync(msg);
var (found, driverNpcId, passengerIds) = Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.DetachForBorrow(args.VehicleInstanceId);
if (found)
{
var session = conn.Session;
if (driverNpcId != 0)
await TrafficAndCrowdDriver.Instance.RemoveAetherVehicleNpcAsync(session, driverNpcId);
foreach (var passengerId in passengerIds)
await TrafficAndCrowdDriver.Instance.RemoveAetherVehicleNpcAsync(session, passengerId);
}
await session_AckBorrowAsync(conn.Session, args.VehicleInstanceId);
}
private static async Task session_AckBorrowAsync(TcpSession session, ulong vehicleInstanceId)
{
await session.NotifyAsync(MethodId.SyncAetherAIBorrowVehicle, UxSerializer.Serialize(
new SceneMethods.SyncAetherAIBorrowVehicle4229938
{
VehicleInstanceId = vehicleInstanceId,
}), CancellationToken.None);
await session.NotifyAsync(MethodId.SyncAetherAIChangeVehicleControl, UxSerializer.Serialize(
new SceneMethods.SyncAetherAIChangeVehicleControlType4229938
{
VehicleId = vehicleInstanceId,
ControlType = 2, // VehicleControlType.RaidVehicle — player-drivable
}), CancellationToken.None);
}
/// <summary>Player left a borrowed car: flip it back to an Aether traffic vehicle.</summary>
[Handler(MethodId.AskAetherAIRaidVehicleRegressAether, HandlerPacketKind.Invoke)]
private async Task AskAetherAIRaidVehicleRegressAether(Connection conn, UxRpcMessage msg)
{
var args = msg.GetArgs<SceneMethods.AskAetherAIRaidVehicleRegressAether4229938>();
conn.Log.Info($"[VEHICLE] regress aether vehicle={args.VehicleInstanceId}");
await conn.ReturnEmptyOkAsync(msg);
await conn.Session.NotifyAsync(MethodId.SyncAetherAIChangeVehicleControl, UxSerializer.Serialize(
new SceneMethods.SyncAetherAIChangeVehicleControlType4229938
{
VehicleId = args.VehicleInstanceId,
ControlType = 1, // VehicleControlType.AetherVehicle — back to traffic
}), CancellationToken.None);
}
[Handler(MethodId.AskPlayerFinishEnterOrExitVehicle, HandlerPacketKind.Notify)]
[Handler(MethodId.AskVehicleStartMove, HandlerPacketKind.Notify)]
[Handler(MethodId.AskVehicleStopMove, HandlerPacketKind.Notify)]
@@ -537,20 +599,8 @@ internal sealed partial class GameRouter
}
// Metro / Subway System Handlers
[Handler(MethodId.AskGetAllMetroInfos, HandlerPacketKind.Invoke)]
private Task AskGetAllMetroInfos(Connection conn, UxRpcMessage msg)
{
conn.Log.Info("[METRO] GetAllMetroInfos requested -> returning active metro train lines");
// Active trains for rail lines 1, 2, 3, 4 across the city
var list = new List<MetroClientInfo>
{
new() { Id = 1001, LineId = 1, ElapsedTime = 12.0f, IsFinalTrain = false },
new() { Id = 1002, LineId = 2, ElapsedTime = 25.0f, IsFinalTrain = false },
new() { Id = 1003, LineId = 3, ElapsedTime = 38.0f, IsFinalTrain = false },
new() { Id = 1004, LineId = 4, ElapsedTime = 5.0f, IsFinalTrain = false }
};
return conn.ReturnAsync(msg, list);
}
// AskGetAllMetroInfos is handled by GameRouter.Metro.cs (real rail scheduling from the
// client's Rail*Config tables + a SyncRunningMetroInfos push loop).
[Handler(MethodId.AskMetroGadgetIds, HandlerPacketKind.Invoke)]
private Task AskMetroGadgetIds(Connection conn, UxRpcMessage msg)