feat: Add metro transit, gadgets, shop, social apps, and traffic models checkpoint
This commit is contained in:
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)
|
||||
|
||||
Reference in new issue
Block a user