/* * Copyright (C) 2012-2020 CypherCore * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 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 (!CliDB.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(); transport.entry = entry; GeneratePath(goInfo, transport); _transportTemplates[entry] = transport; // transports in instance are only on one map if (transport.inInstance) _instanceTransports.Add(transport.mapsUsed.First(), entry); ++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); if (GetTransportTemplate(entry) == 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; _transportSpawns[guid] = spawn; } while (result.NextRow()); } Log.outInfo(LogFilter.ServerLoading, $"Spawned {count} continent transports in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); } void GeneratePath(GameObjectTemplate goInfo, TransportTemplate transport) { uint pathId = goInfo.MoTransport.taxiPathID; var path = CliDB.TaxiPathNodesByPath[pathId]; List keyFrames = transport.keyFrames; List splinePath = new(); List allPoints = new(); bool mapChange = false; for (uint i = 0; i < path.Length; ++i) allPoints.Add(new Vector3(path[i].Loc.X, path[i].Loc.Y, path[i].Loc.Z)); // Add extra points to allow derivative calculations for all path nodes allPoints.Insert(0, Vector3.Lerp(allPoints.First(), allPoints[1], -0.2f)); allPoints.Add(Vector3.Lerp(allPoints.Last(), allPoints[^2], -0.2f)); allPoints.Add(Vector3.Lerp(allPoints.Last(), allPoints[^2], -1.0f)); SplineRawInitializer initer = new(allPoints); Spline orientationSpline = new(); orientationSpline.InitSplineCustom(initer); orientationSpline.InitLengths(); for (uint i = 0; i < path.Length; ++i) { if (!mapChange) { var node_i = path[i]; if (i != path.Length - 1 && (node_i.Flags.HasAnyFlag(TaxiPathNodeFlags.Teleport) || node_i.ContinentID != path[i + 1].ContinentID)) { keyFrames.Last().Teleport = true; mapChange = true; } else { KeyFrame k = new(node_i); Vector3 h; orientationSpline.Evaluate_Derivative((int)(i + 1), 0.0f, out h); k.InitialOrientation = Position.NormalizeOrientation((float)Math.Atan2(h.Y, h.X) + MathFunctions.PI); keyFrames.Add(k); splinePath.Add(new Vector3(node_i.Loc.X, node_i.Loc.Y, node_i.Loc.Z)); if (!transport.mapsUsed.Contains(k.Node.ContinentID)) transport.mapsUsed.Add(k.Node.ContinentID); } } else mapChange = false; } if (splinePath.Count >= 2) { // Remove special catmull-rom spline points if (!keyFrames.First().IsStopFrame() && keyFrames.First().Node.ArrivalEventID == 0 && keyFrames.First().Node.DepartureEventID == 0) { splinePath.RemoveAt(0); keyFrames.RemoveAt(0); } if (!keyFrames.Last().IsStopFrame() && keyFrames.Last().Node.ArrivalEventID == 0 && keyFrames.Last().Node.DepartureEventID == 0) { splinePath.RemoveAt(splinePath.Count - 1); keyFrames.RemoveAt(keyFrames.Count - 1); } } Cypher.Assert(!keyFrames.Empty()); if (transport.mapsUsed.Count > 1) { foreach (var mapId in transport.mapsUsed) Cypher.Assert(!CliDB.MapStorage.LookupByKey(mapId).Instanceable()); transport.inInstance = false; } else transport.inInstance = CliDB.MapStorage.LookupByKey(transport.mapsUsed.First()).Instanceable(); // last to first is always "teleport", even for closed paths keyFrames.Last().Teleport = true; float speed = goInfo.MoTransport.moveSpeed; float accel = goInfo.MoTransport.accelRate; float accel_dist = 0.5f * speed * speed / accel; transport.accelTime = speed / accel; transport.accelDist = accel_dist; int firstStop = -1; int lastStop = -1; // first cell is arrived at by teleportation :S keyFrames[0].DistFromPrev = 0; keyFrames[0].Index = 1; if (keyFrames[0].IsStopFrame()) { firstStop = 0; lastStop = 0; } // find the rest of the distances between key points // Every path segment has its own spline int start = 0; for (int i = 1; i < keyFrames.Count; ++i) { if (keyFrames[i - 1].Teleport || i + 1 == keyFrames.Count) { int extra = !keyFrames[i - 1].Teleport ? 1 : 0; Spline spline = new(); Span span = splinePath.ToArray(); spline.InitSpline(span[start..], i - start + extra, Spline.EvaluationMode.Catmullrom); spline.InitLengths(); for (int j = start; j < i + extra; ++j) { keyFrames[j].Index = (uint)(j - start + 1); keyFrames[j].DistFromPrev = spline.Length(j - start, j + 1 - start); if (j > 0) keyFrames[j - 1].NextDistFromPrev = keyFrames[j].DistFromPrev; keyFrames[j].Spline = spline; } if (keyFrames[i - 1].Teleport) { keyFrames[i].Index = (uint)(i - start + 1); keyFrames[i].DistFromPrev = 0.0f; keyFrames[i - 1].NextDistFromPrev = 0.0f; keyFrames[i].Spline = spline; } start = i; } if (keyFrames[i].IsStopFrame()) { // remember first stop frame if (firstStop == -1) firstStop = i; lastStop = i; } } keyFrames.Last().NextDistFromPrev = keyFrames.First().DistFromPrev; if (firstStop == -1 || lastStop == -1) firstStop = lastStop = 0; // at stopping keyframes, we define distSinceStop == 0, // and distUntilStop is to the next stopping keyframe. // this is required to properly handle cases of two stopping frames in a row (yes they do exist) float tmpDist = 0.0f; for (int i = 0; i < keyFrames.Count; ++i) { int j = (i + lastStop) % keyFrames.Count; if (keyFrames[j].IsStopFrame() || j == lastStop) tmpDist = 0.0f; else tmpDist += keyFrames[j].DistFromPrev; keyFrames[j].DistSinceStop = tmpDist; } tmpDist = 0.0f; for (int i = (keyFrames.Count - 1); i >= 0; i--) { int j = (i + firstStop) % keyFrames.Count; tmpDist += keyFrames[(j + 1) % keyFrames.Count].DistFromPrev; keyFrames[j].DistUntilStop = tmpDist; if (keyFrames[j].IsStopFrame() || j == firstStop) tmpDist = 0.0f; } for (int i = 0; i < keyFrames.Count; ++i) { float total_dist = keyFrames[i].DistSinceStop + keyFrames[i].DistUntilStop; if (total_dist < 2 * accel_dist) // won't reach full speed { if (keyFrames[i].DistSinceStop < keyFrames[i].DistUntilStop) // is still accelerating { // calculate accel+brake time for this short segment float segment_time = 2.0f * (float)Math.Sqrt((keyFrames[i].DistUntilStop + keyFrames[i].DistSinceStop) / accel); // substract acceleration time keyFrames[i].TimeTo = segment_time - (float)Math.Sqrt(2 * keyFrames[i].DistSinceStop / accel); } else // slowing down keyFrames[i].TimeTo = (float)Math.Sqrt(2 * keyFrames[i].DistUntilStop / accel); } else if (keyFrames[i].DistSinceStop < accel_dist) // still accelerating (but will reach full speed) { // calculate accel + cruise + brake time for this long segment float segment_time = (keyFrames[i].DistUntilStop + keyFrames[i].DistSinceStop) / speed + (speed / accel); // substract acceleration time keyFrames[i].TimeTo = segment_time - (float)Math.Sqrt(2 * keyFrames[i].DistSinceStop / accel); } else if (keyFrames[i].DistUntilStop < accel_dist) // already slowing down (but reached full speed) keyFrames[i].TimeTo = (float)Math.Sqrt(2 * keyFrames[i].DistUntilStop / accel); else // at full speed keyFrames[i].TimeTo = (keyFrames[i].DistUntilStop / speed) + (0.5f * speed / accel); } // calculate tFrom times from tTo times float segmentTime = 0.0f; for (int i = 0; i < keyFrames.Count; ++i) { int j = (i + lastStop) % keyFrames.Count; if (keyFrames[j].IsStopFrame() || j == lastStop) segmentTime = keyFrames[j].TimeTo; keyFrames[j].TimeFrom = segmentTime - keyFrames[j].TimeTo; } // calculate path times keyFrames[0].ArriveTime = 0; float curPathTime = 0.0f; if (keyFrames[0].IsStopFrame()) { curPathTime = keyFrames[0].Node.Delay; keyFrames[0].DepartureTime = (uint)(curPathTime * Time.InMilliseconds); } for (int i = 1; i < keyFrames.Count; ++i) { curPathTime += keyFrames[i - 1].TimeTo; if (keyFrames[i].IsStopFrame()) { keyFrames[i].ArriveTime = (uint)(curPathTime * Time.InMilliseconds); keyFrames[i - 1].NextArriveTime = keyFrames[i].ArriveTime; curPathTime += keyFrames[i].Node.Delay; keyFrames[i].DepartureTime = (uint)(curPathTime * Time.InMilliseconds); } else { curPathTime -= keyFrames[i].TimeTo; keyFrames[i].ArriveTime = (uint)(curPathTime * Time.InMilliseconds); keyFrames[i - 1].NextArriveTime = keyFrames[i].ArriveTime; keyFrames[i].DepartureTime = keyFrames[i].ArriveTime; } } keyFrames.Last().NextArriveTime = keyFrames.Last().DepartureTime; transport.pathTime = keyFrames.Last().DepartureTime; if (transport.pathTime == 0) { } } public void AddPathNodeToTransport(uint transportEntry, uint timeSeg, TransportAnimationRecord node) { TransportAnimation animNode = new(); if (animNode.TotalTime < timeSeg) animNode.TotalTime = timeSeg; animNode.Path[timeSeg] = node; _transportAnimations[transportEntry] = animNode; } 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, ulong guid = 0, Map map = null, PhaseUseFlagsValues phaseUseFlags = 0, uint phaseId = 0, uint phaseGroupId = 0) { // instance case, execute GetGameObjectEntry hook if (map != null) { // SetZoneScript() is called after adding to map, so fetch the script using map if (map.IsDungeon()) { InstanceScript instance = ((InstanceMap)map).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; } // create transport... Transport trans = new(); // ...at first waypoint TaxiPathNodeRecord startNode = tInfo.keyFrames.First().Node; uint mapId = startNode.ContinentID; float x = startNode.Loc.X; float y = startNode.Loc.Y; float z = startNode.Loc.Z; float o = tInfo.keyFrames.First().InitialOrientation; // initialize the gameobject base ulong guidLow = guid != 0 ? guid : map.GenerateLowGuid(HighGuid.Transport); if (!trans.Create(guidLow, entry, mapId, x, y, z, o, 255)) return null; PhasingHandler.InitDbPhaseShift(trans.GetPhaseShift(), phaseUseFlags, phaseId, phaseGroupId); MapRecord mapEntry = CliDB.MapStorage.LookupByKey(mapId); if (mapEntry != null) { if (mapEntry.Instanceable() != tInfo.inInstance) { Log.outError(LogFilter.Transport, "Transport {0} (name: {1}) attempted creation in instance map (id: {2}) but it is not an instanced transport!", entry, trans.GetName(), mapId); //return null; } } // use preset map for instances (need to know which instance) trans.SetMap(map != null ? map : Global.MapMgr.CreateMap(mapId, null)); if (map != null && map.IsDungeon()) trans.m_zoneScript = map.ToInstanceMap().GetInstanceScript(); // Passengers will be loaded once a player is near Global.ObjAccessor.AddObject(trans); trans.GetMap().AddToMap(trans); return trans; } public void SpawnContinentTransports() { uint oldMSTime = Time.GetMSTime(); uint count = 0; foreach (var pair in _transportSpawns) if (!GetTransportTemplate(pair.Value.TransportGameObjectId).inInstance) if (CreateTransport(pair.Value.TransportGameObjectId, pair.Value.SpawnId, null, pair.Value.PhaseUseFlags, pair.Value.PhaseId, pair.Value.PhaseGroup)) ++count; Log.outInfo(LogFilter.ServerLoading, "Spawned {0} continent transports in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime)); } public void CreateInstanceTransports(Map map) { var mapTransports = _instanceTransports.LookupByKey(map.GetId()); // no transports here if (mapTransports.Empty()) return; // create transports foreach (var transportGameObjectId in mapTransports) CreateTransport(transportGameObjectId, 0, map); } 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 _instanceTransports = new(); Dictionary _transportAnimations = new(); Dictionary _transportSpawns = new(); } public class SplineRawInitializer { public SplineRawInitializer(List points) { _points = points; } public void Initialize(ref Spline.EvaluationMode mode, ref bool cyclic, ref Vector3[] points, ref int lo, ref int hi) { mode = Spline.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 KeyFrame { public KeyFrame(TaxiPathNodeRecord _node) { Node = _node; DistSinceStop = -1.0f; DistUntilStop = -1.0f; DistFromPrev = -1.0f; TimeFrom = 0.0f; TimeTo = 0.0f; Teleport = false; ArriveTime = 0; DepartureTime = 0; Spline = null; NextDistFromPrev = 0.0f; NextArriveTime = 0; } public uint Index; public TaxiPathNodeRecord Node; public float InitialOrientation; public float DistSinceStop; public float DistUntilStop; public float DistFromPrev; public float TimeFrom; public float TimeTo; public bool Teleport; public uint ArriveTime; public uint DepartureTime; public Spline Spline; // Data needed for next frame public float NextDistFromPrev; public uint NextArriveTime; public bool IsTeleportFrame() { return Teleport; } public bool IsStopFrame() { return Node.Flags.HasAnyFlag(TaxiPathNodeFlags.Stop); } } public class TransportTemplate { public TransportTemplate() { pathTime = 0; accelTime = 0.0f; accelDist = 0.0f; } public List mapsUsed = new(); public bool inInstance; public uint pathTime; public List keyFrames = new(); public float accelTime; public float accelDist; public uint entry; } public class TransportAnimation { public Dictionary Path = new(); public Dictionary Rotations = new(); public uint TotalTime; public TransportAnimationRecord GetPrevAnimNode(uint time) { if (Path.Empty()) return null; List lKeys = Path.Keys.ToList(); int reqIndex = lKeys.IndexOf(time) - 1; if (reqIndex != -1) return Path[lKeys[reqIndex]]; return Path.LastOrDefault().Value; } public TransportRotationRecord GetPrevAnimRotation(uint time) { if (Rotations.Empty()) return null; List lKeys = Rotations.Keys.ToList(); int reqIndex = lKeys.IndexOf(time) - 1; if (reqIndex != -1) return Rotations[lKeys[reqIndex]]; 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; } }