// Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using Framework.Constants; using Framework.Database; using Game.DataStorage; using Game.Entities; using Game.Movement; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; namespace Game.Maps { public class TransportManager : Singleton { TransportManager() { } void Unload() { _transportTemplates.Clear(); } public void LoadTransportTemplates() { uint oldMSTime = Time.GetMSTime(); SQLResult result = DB.World.Query("SELECT entry FROM gameobject_template WHERE type = 15 ORDER BY entry ASC"); if (result.IsEmpty()) { Log.outInfo(LogFilter.ServerLoading, "Loaded 0 transports templates. DB table `gameobject_template` has no transports!"); return; } uint count = 0; do { uint entry = result.Read(0); GameObjectTemplate goInfo = Global.ObjectMgr.GetGameObjectTemplate(entry); if (goInfo == null) { Log.outError(LogFilter.Sql, "Transport {0} has no associated GameObjectTemplate from `gameobject_template` , skipped.", entry); continue; } if (!DB2Manager.TaxiPathNodesByPath.ContainsKey(goInfo.MoTransport.taxiPathID)) { Log.outError(LogFilter.Sql, "Transport {0} (name: {1}) has an invalid path specified in `gameobject_template`.`data0` ({2}) field, skipped.", entry, goInfo.name, goInfo.MoTransport.taxiPathID); continue; } if (goInfo.MoTransport.taxiPathID == 0) continue; // paths are generated per template, saves us from generating it again in case of instanced transports TransportTemplate transport = new(); GeneratePath(goInfo, transport); _transportTemplates[entry] = transport; ++count; } while (result.NextRow()); Log.outInfo(LogFilter.ServerLoading, "Loaded {0} transports in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime)); } public void LoadTransportAnimationAndRotation() { foreach (TransportAnimationRecord anim in CliDB.TransportAnimationStorage.Values) AddPathNodeToTransport(anim.TransportID, anim.TimeIndex, anim); foreach (TransportRotationRecord rot in CliDB.TransportRotationStorage.Values) AddPathRotationToTransport(rot.GameObjectsID, rot.TimeIndex, rot); } public void LoadTransportSpawns() { if (_transportTemplates.Empty()) return; uint oldMSTime = Time.GetMSTime(); SQLResult result = DB.World.Query("SELECT guid, entry, phaseUseFlags, phaseid, phasegroup FROM transports"); uint count = 0; if (!result.IsEmpty()) { do { ulong guid = result.Read(0); uint entry = result.Read(1); PhaseUseFlagsValues phaseUseFlags = (PhaseUseFlagsValues)result.Read(2); uint phaseId = result.Read(3); uint phaseGroupId = result.Read(4); TransportTemplate transportTemplate = GetTransportTemplate(entry); if (transportTemplate == null) { Log.outError(LogFilter.Sql, $"Table `transports` have transport (GUID: {guid} Entry: {entry}) with unknown gameobject `entry` set, skipped."); continue; } if ((phaseUseFlags & ~PhaseUseFlagsValues.All) != 0) { Log.outError(LogFilter.Sql, $"Table `transports` have transport (GUID: {guid} Entry: {entry}) with unknown `phaseUseFlags` set, removed unknown value."); phaseUseFlags &= PhaseUseFlagsValues.All; } if (phaseUseFlags.HasFlag(PhaseUseFlagsValues.AlwaysVisible) && phaseUseFlags.HasFlag(PhaseUseFlagsValues.Inverse)) { Log.outError(LogFilter.Sql, $"Table `transports` have transport (GUID: {guid} Entry: {entry}) has both `phaseUseFlags` PHASE_USE_FLAGS_ALWAYS_VISIBLE and PHASE_USE_FLAGS_INVERSE, removing PHASE_USE_FLAGS_INVERSE."); phaseUseFlags &= ~PhaseUseFlagsValues.Inverse; } if (phaseGroupId != 0 && phaseId != 0) { Log.outError(LogFilter.Sql, $"Table `transports` have transport (GUID: {guid} Entry: {entry}) with both `phaseid` and `phasegroup` set, `phasegroup` set to 0"); phaseGroupId = 0; } if (phaseId != 0) { if (!CliDB.PhaseStorage.ContainsKey(phaseId)) { Log.outError(LogFilter.Sql, $"Table `transports` have transport (GUID: {guid} Entry: {entry}) with `phaseid` {phaseId} does not exist, set to 0"); phaseId = 0; } } if (phaseGroupId != 0) { if (Global.DB2Mgr.GetPhasesForGroup(phaseGroupId) == null) { Log.outError(LogFilter.Sql, $"Table `transports` have transport (GUID: {guid} Entry: {entry}) with `phaseGroup` {phaseGroupId} does not exist, set to 0"); phaseGroupId = 0; } } TransportSpawn spawn = new(); spawn.SpawnId = guid; spawn.TransportGameObjectId = entry; spawn.PhaseUseFlags = phaseUseFlags; spawn.PhaseId = phaseId; spawn.PhaseGroup = phaseGroupId; foreach (uint mapId in transportTemplate.MapIds) _transportsByMap.Add(mapId, spawn); _transportSpawns[guid] = spawn; count++; } while (result.NextRow()); } Log.outInfo(LogFilter.ServerLoading, $"Spawned {count} continent transports in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); } static void InitializeLeg(TransportPathLeg leg, List outEvents, List pathPoints, List pauses, List events, GameObjectTemplate goInfo, ref uint totalTime) { List splinePath = new(pathPoints.Select(node => new Vector3(node.Loc.X, node.Loc.Y, node.Loc.Z))); SplineRawInitializer initer = new(splinePath); leg.Spline = new Spline(); leg.Spline.set_steps_per_segment(20); leg.Spline.InitSplineCustom(initer); leg.Spline.InitLengths(); uint legTimeAccelDecel(double dist) { double speed = (double)goInfo.MoTransport.moveSpeed; double accel = (double)goInfo.MoTransport.accelRate; double accelDist = 0.5 * speed * speed / accel; if (accelDist >= dist * 0.5) return (uint)(Math.Sqrt(dist / accel) * 2000.0); else return (uint)((dist - (accelDist + accelDist)) / speed * 1000.0 + speed / accel * 2000.0); } uint legTimeAccel(double dist) { double speed = (double)goInfo.MoTransport.moveSpeed; double accel = (double)goInfo.MoTransport.accelRate; double accelDist = 0.5 * speed * speed / accel; if (accelDist >= dist) return (uint)(Math.Sqrt((dist + dist) / accel) * 1000.0); else return (uint)(((dist - accelDist) / speed + speed / accel) * 1000.0); }; // Init segments int pauseItr = 0; int eventItr = 0; double splineLengthToPreviousNode = 0.0; uint delaySum = 0; if (!pauses.Empty()) { for (; pauseItr < pauses.Count; ++pauseItr) { var pausePointIndex = pathPoints.IndexOf(pauses[pauseItr]); if (pausePointIndex == pathPoints.Count - 1) // last point is a "fake" spline point, its position can never be reached so transport cannot stop there break; for (; eventItr < events.Count; ++eventItr) { var eventPointIndex = pathPoints.IndexOf(events[eventItr]); if (eventPointIndex > pausePointIndex) break; double eventLength = leg.Spline.Length(eventPointIndex) - splineLengthToPreviousNode; uint eventSplineTime = 0; if (pauseItr != 0) eventSplineTime = legTimeAccelDecel(eventLength); else eventSplineTime = legTimeAccel(eventLength); if (pathPoints[eventPointIndex].ArrivalEventID != 0) { TransportPathEvent Event = new(); Event.Timestamp = totalTime + eventSplineTime + leg.Duration + delaySum; Event.EventId = pathPoints[eventPointIndex].ArrivalEventID; outEvents.Add(Event); } if (pathPoints[eventPointIndex].DepartureEventID != 0) { TransportPathEvent Event = new(); Event.Timestamp = totalTime + eventSplineTime + leg.Duration + delaySum + (pausePointIndex == eventPointIndex ? pathPoints[eventPointIndex].Delay * Time.InMilliseconds : 0); Event.EventId = pathPoints[eventPointIndex].DepartureEventID; outEvents.Add(Event); } } double splineLengthToCurrentNode = leg.Spline.Length(pausePointIndex); double length1 = splineLengthToCurrentNode - splineLengthToPreviousNode; uint movementTime = 0; if (pauseItr != 0) movementTime = legTimeAccelDecel(length1); else movementTime = legTimeAccel(length1); leg.Duration += movementTime; TransportPathSegment segment = new(); segment.SegmentEndArrivalTimestamp = leg.Duration + delaySum; segment.Delay = pathPoints[pausePointIndex].Delay * Time.InMilliseconds; segment.DistanceFromLegStartAtEnd = splineLengthToCurrentNode; leg.Segments.Add(segment); delaySum += pathPoints[pausePointIndex].Delay * Time.InMilliseconds; splineLengthToPreviousNode = splineLengthToCurrentNode; } } // Process events happening after last pause for (; eventItr < events.Count; ++eventItr) { var eventPointIndex = pathPoints.IndexOf(events[eventItr]); if (eventPointIndex == -1) // last point is a "fake" spline node, events cannot happen there break; double eventLength = leg.Spline.Length(eventPointIndex) - splineLengthToPreviousNode; uint eventSplineTime = 0; if (pauseItr != 0) eventSplineTime = legTimeAccel(eventLength); else eventSplineTime = (uint)(eventLength / (double)goInfo.MoTransport.moveSpeed * 1000.0); if (pathPoints[eventPointIndex].ArrivalEventID != 0) { TransportPathEvent Event = new(); Event.Timestamp = totalTime + eventSplineTime + leg.Duration + delaySum; Event.EventId = pathPoints[eventPointIndex].ArrivalEventID; outEvents.Add(Event); } if (pathPoints[eventPointIndex].DepartureEventID != 0) { TransportPathEvent Event = new(); Event.Timestamp = totalTime + eventSplineTime + leg.Duration + delaySum; Event.EventId = pathPoints[eventPointIndex].DepartureEventID; outEvents.Add(Event); } } // Add segment after last pause double length = leg.Spline.Length() - splineLengthToPreviousNode; uint splineTime = 0; if (pauseItr != 0) splineTime = legTimeAccel(length); else splineTime = (uint)(length / (double)goInfo.MoTransport.moveSpeed * 1000.0); leg.StartTimestamp = totalTime; leg.Duration += splineTime + delaySum; TransportPathSegment pauseSegment = new(); pauseSegment.SegmentEndArrivalTimestamp = leg.Duration; pauseSegment.Delay = 0; pauseSegment.DistanceFromLegStartAtEnd = leg.Spline.Length(); leg.Segments.Add(pauseSegment); totalTime += leg.Segments[pauseItr].SegmentEndArrivalTimestamp + leg.Segments[pauseItr].Delay; for (var i = 0; i < leg.Segments.Count; ++i) leg.Segments[i].SegmentEndArrivalTimestamp += leg.StartTimestamp; } void GeneratePath(GameObjectTemplate goInfo, TransportTemplate transport) { uint pathId = goInfo.MoTransport.taxiPathID; TaxiPathNodeRecord[] path = DB2Manager.TaxiPathNodesByPath[pathId]; transport.Speed = (double)goInfo.MoTransport.moveSpeed; transport.AccelerationRate = (double)goInfo.MoTransport.accelRate; transport.AccelerationTime = transport.Speed / transport.AccelerationRate; transport.AccelerationDistance = 0.5 * transport.Speed * transport.Speed / transport.AccelerationRate; List pathPoints = new(); List pauses = new(); List events = new(); transport.PathLegs.Add(new TransportPathLeg()); TransportPathLeg leg = transport.PathLegs[0]; leg.MapId = path[0].ContinentID; bool prevNodeWasTeleport = false; uint totalTime = 0; foreach (TaxiPathNodeRecord node in path) { if (node.ContinentID != leg.MapId || prevNodeWasTeleport) { InitializeLeg(leg, transport.Events, pathPoints, pauses, events, goInfo, ref totalTime); leg = new(); leg.MapId = node.ContinentID; pathPoints.Clear(); pauses.Clear(); events.Clear(); transport.PathLegs.Add(leg); } prevNodeWasTeleport = node.HasFlag(TaxiPathNodeFlags.Teleport); pathPoints.Add(node); if (node.HasFlag(TaxiPathNodeFlags.Stop)) pauses.Add(node); if (node.ArrivalEventID != 0 || node.DepartureEventID != 0) events.Add(node); transport.MapIds.Add(node.ContinentID); } if (leg.Spline == null) InitializeLeg(leg, transport.Events, pathPoints, pauses, events, goInfo, ref totalTime); if (transport.MapIds.Count > 1) { foreach (uint mapId in transport.MapIds) Cypher.Assert(!CliDB.MapStorage.LookupByKey(mapId).Instanceable()); } transport.TotalPathTime = totalTime; } public void AddPathNodeToTransport(uint transportEntry, uint timeSeg, TransportAnimationRecord node) { if (!_transportAnimations.ContainsKey(transportEntry)) _transportAnimations[transportEntry] = new(); TransportAnimation animNode = _transportAnimations[transportEntry]; if (animNode.TotalTime < timeSeg) animNode.TotalTime = timeSeg; animNode.Path[timeSeg] = node; } public void AddPathRotationToTransport(uint transportEntry, uint timeSeg, TransportRotationRecord node) { if (!_transportAnimations.ContainsKey(transportEntry)) _transportAnimations[transportEntry] = new(); TransportAnimation animNode = _transportAnimations[transportEntry]; animNode.Rotations[timeSeg] = node; if (animNode.Path.Empty() && animNode.TotalTime < timeSeg) animNode.TotalTime = timeSeg; } public Transport CreateTransport(uint entry, Map map, ulong guid = 0, PhaseUseFlagsValues phaseUseFlags = 0, uint phaseId = 0, uint phaseGroupId = 0) { // SetZoneScript() is called after adding to map, so fetch the script using map InstanceMap instanceMap = map.ToInstanceMap(); if (instanceMap != null) { InstanceScript instance = instanceMap.GetInstanceScript(); if (instance != null) entry = instance.GetGameObjectEntry(0, entry); } if (entry == 0) return null; TransportTemplate tInfo = GetTransportTemplate(entry); if (tInfo == null) { Log.outError(LogFilter.Sql, "Transport {0} will not be loaded, `transport_template` missing", entry); return null; } if (!tInfo.MapIds.Contains(map.GetId())) { Log.outError(LogFilter.Transport, $"Transport {entry} attempted creation on map it has no path for {map.GetId()}!"); return null; } Position startingPosition = tInfo.ComputePosition(0, out _, out _); if (startingPosition == null) { Log.outError(LogFilter.Sql, $"Transport {entry} will not be loaded, failed to compute starting position"); return null; } // create transport... Transport trans = new(); // ...at first waypoint float x = startingPosition.GetPositionX(); float y = startingPosition.GetPositionY(); float z = startingPosition.GetPositionZ(); float o = startingPosition.GetOrientation(); // initialize the gameobject base ulong guidLow = guid != 0 ? guid : map.GenerateLowGuid(HighGuid.Transport); if (!trans.Create(guidLow, entry, x, y, z, o)) return null; PhasingHandler.InitDbPhaseShift(trans.GetPhaseShift(), phaseUseFlags, phaseId, phaseGroupId); // use preset map for instances (need to know which instance) trans.SetMap(map); if (instanceMap != null) trans.m_zoneScript = instanceMap.GetInstanceScript(); // Passengers will be loaded once a player is near map.AddToMap(trans); return trans; } public void CreateTransportsForMap(Map map) { var mapTransports = _transportsByMap.LookupByKey(map.GetId()); // no transports here if (mapTransports.Empty()) return; // create transports foreach (var transport in mapTransports) CreateTransport(transport.TransportGameObjectId, map, transport.SpawnId, transport.PhaseUseFlags, transport.PhaseId, transport.PhaseGroup); } public TransportTemplate GetTransportTemplate(uint entry) { return _transportTemplates.LookupByKey(entry); } public TransportAnimation GetTransportAnimInfo(uint entry) { return _transportAnimations.LookupByKey(entry); } public TransportSpawn GetTransportSpawn(ulong spawnId) { return _transportSpawns.LookupByKey(spawnId); } Dictionary _transportTemplates = new(); MultiMap _transportsByMap = new(); Dictionary _transportAnimations = new(); Dictionary _transportSpawns = new(); } public class TransportPathSegment { public uint SegmentEndArrivalTimestamp; public uint Delay; public double DistanceFromLegStartAtEnd; } public struct TransportPathEvent { public uint Timestamp; public uint EventId; } public class TransportPathLeg { public uint MapId; public Spline Spline; public uint StartTimestamp; public uint Duration; public List Segments = new(); } public class TransportTemplate { public uint TotalPathTime; public double Speed; public double AccelerationRate; public double AccelerationTime; public double AccelerationDistance; public List PathLegs = new(); public List Events = new(); public HashSet MapIds = new(); public Position ComputePosition(uint time, out TransportMovementState moveState, out int legIndex) { moveState = TransportMovementState.Moving; legIndex = 0; time %= TotalPathTime; // find leg TransportPathLeg leg = GetLegForTime(time); if (leg == null) return null; // find segment uint prevSegmentTime = leg.StartTimestamp; var segmentIndex = 0; double distanceMoved = 0.0; bool isOnPause = false; for (segmentIndex = 0; segmentIndex < leg.Segments.Count - 1; ++segmentIndex) { var segment = leg.Segments[segmentIndex]; if (time < segment.SegmentEndArrivalTimestamp) break; distanceMoved = segment.DistanceFromLegStartAtEnd; if (time < segment.SegmentEndArrivalTimestamp + segment.Delay) { isOnPause = true; break; } prevSegmentTime = segment.SegmentEndArrivalTimestamp + segment.Delay; } var pathSegment = leg.Segments[segmentIndex]; if (!isOnPause) distanceMoved += CalculateDistanceMoved( (double)(time - prevSegmentTime) * 0.001, (double)(pathSegment.SegmentEndArrivalTimestamp - prevSegmentTime) * 0.001, segmentIndex == 0, segmentIndex == leg.Segments.Count - 1); int splineIndex = 0; float splinePointProgress = 0; leg.Spline.ComputeIndex((float)Math.Min(distanceMoved / leg.Spline.Length(), 1.0), ref splineIndex, ref splinePointProgress); Vector3 pos, dir; leg.Spline.Evaluate_Percent(splineIndex, splinePointProgress, out pos); leg.Spline.Evaluate_Derivative(splineIndex, splinePointProgress, out dir); moveState = isOnPause ? TransportMovementState.WaitingOnPauseWaypoint : TransportMovementState.Moving; legIndex = PathLegs.IndexOf(leg); return new Position(pos.X, pos.Y, pos.Z, MathF.Atan2(dir.Y, dir.X) + MathF.PI); } public TransportPathLeg GetLegForTime(uint time) { int legIndex = 0; while (PathLegs[legIndex].StartTimestamp + PathLegs[legIndex].Duration <= time) { ++legIndex; if (legIndex >= PathLegs.Count) return null; } return PathLegs[legIndex]; } public uint GetNextPauseWaypointTimestamp(uint time) { TransportPathLeg leg = GetLegForTime(time); if (leg == null) return time; int segmentIndex = 0; for (; segmentIndex != leg.Segments.Count - 1; ++segmentIndex) if (time < leg.Segments[segmentIndex].SegmentEndArrivalTimestamp + leg.Segments[segmentIndex].Delay) break; return leg.Segments[segmentIndex].SegmentEndArrivalTimestamp + leg.Segments[segmentIndex].Delay; } double CalculateDistanceMoved(double timePassedInSegment, double segmentDuration, bool isFirstSegment, bool isLastSegment) { if (isFirstSegment) { if (!isLastSegment) { double accelerationTime = Math.Min(AccelerationTime, segmentDuration); double segmentTimeAtFullSpeed = segmentDuration - accelerationTime; if (timePassedInSegment <= segmentTimeAtFullSpeed) { return timePassedInSegment * Speed; } else { double segmentAccelerationTime = timePassedInSegment - segmentTimeAtFullSpeed; double segmentAccelerationDistance = AccelerationRate * accelerationTime; double segmentDistanceAtFullSpeed = segmentTimeAtFullSpeed * Speed; return (2.0 * segmentAccelerationDistance - segmentAccelerationTime * AccelerationRate) * 0.5 * segmentAccelerationTime + segmentDistanceAtFullSpeed; } } return timePassedInSegment * Speed; } if (isLastSegment) { if (!isFirstSegment) { if (timePassedInSegment <= Math.Min(AccelerationTime, segmentDuration)) return AccelerationRate * timePassedInSegment * 0.5 * timePassedInSegment; else return (timePassedInSegment - AccelerationTime) * Speed + AccelerationDistance; } return timePassedInSegment * Speed; } double accelerationTime1 = Math.Min(segmentDuration * 0.5, AccelerationTime); if (timePassedInSegment <= segmentDuration - accelerationTime1) { if (timePassedInSegment <= accelerationTime1) return AccelerationRate * timePassedInSegment * 0.5 * timePassedInSegment; else return (timePassedInSegment - AccelerationTime) * Speed + AccelerationDistance; } else { double segmentTimeSpentAccelerating = timePassedInSegment - (segmentDuration - accelerationTime1); return (segmentDuration - 2 * accelerationTime1) * Speed + AccelerationRate * accelerationTime1 * 0.5 * accelerationTime1 + (2.0 * AccelerationRate * accelerationTime1 - segmentTimeSpentAccelerating * AccelerationRate) * 0.5 * segmentTimeSpentAccelerating; } } } public class SplineRawInitializer { public SplineRawInitializer(List points) { _points = points; } public void Initialize(ref EvaluationMode mode, ref bool cyclic, ref Vector3[] points, ref int lo, ref int hi) { mode = EvaluationMode.Catmullrom; cyclic = false; points = new Vector3[_points.Count]; for (var i = 0; i < _points.Count; ++i) points[i] = _points[i]; lo = 1; hi = points.Length - 2; } List _points; } public class TransportAnimation { public SortedList Path = new(); public SortedList Rotations = new(); public uint TotalTime; public TransportAnimationRecord GetPrevAnimNode(uint time) { if (Path.Empty()) return null; int reqIndex = Path.IndexOfKey(time); if (reqIndex != -1) return Path.GetValueAtIndex(reqIndex - 1); return Path.LastOrDefault().Value; } public TransportRotationRecord GetPrevAnimRotation(uint time) { if (Rotations.Empty()) return null; int reqIndex = Rotations.IndexOfKey(time); if (reqIndex != -1) return Rotations.GetValueAtIndex(reqIndex - 1); return Rotations.LastOrDefault().Value; } public TransportAnimationRecord GetNextAnimNode(uint time) { if (Path.Empty()) return null; if (Path.TryGetValue(time, out TransportAnimationRecord record)) return record; return Path.FirstOrDefault().Value; } public TransportRotationRecord GetNextAnimRotation(uint time) { if (Rotations.Empty()) return null; if (Rotations.TryGetValue(time, out TransportRotationRecord record)) return record; return Rotations.FirstOrDefault().Value; } } public class TransportSpawn { public ulong SpawnId; public uint TransportGameObjectId; // entry in respective _template table public PhaseUseFlagsValues PhaseUseFlags; public uint PhaseId; public uint PhaseGroup; } }