// modified Hat_TrainPoint to work with the custom Hat_Hazard_Train class mcu8_TrainPoint extends Hat_TrainPoint_Base placeable; // The point the train will move to next after reaching this point. var() mcu8_TrainPoint Next; // Train will increase/decrease to this speed when approaching this point, starting from the previous point. Set this to 0 if you want the train to come to a gradual stop. var(Debug) float SpeedMultiplier; // If enabled, train will teleport directly to the next point on departure from this point. This will be enabled automatically on routes that are dead-ends. var() bool Teleport; // Train will stop at this point and wait until this amount of seconds has elapsed. Leave at 0 for no stop. var() float WaitTime; enum Hat_TrackColor { Blue, Green, Yellow, Purple, }; var Array HoloMaterials; var() Hat_TrackColor GeneratedTrackColor; // Train will sync up with other trains at this point. var() Array WaitForSync; // Will attempt to skip this many wait cycles... Just increase this value if trains on this track are sitting idle for too long at a teleport waiting for syncs. var() int SkipSyncWaitCycles; /*var() bool EnableTickOptimize;*/ // Hidden list of points in the opposite direction, populated automatically. var mcu8_TrainPoint Prev; // Editor-only var Array WholeTrack; var mcu8_TrainPoint TrackMaster; var mcu8_TrainPoint EditorLineNext; var Texture2D TrackMasterIcon; var(Debug) Array SubPoints; var Hat_DrawCatapultComponent NextLineComponent; var Hat_DrawCatapultComponent SyncLineComponent; // Adjusted speed calculated to keep syncs var(Debug) float SyncSpeedMultiplier; // Extra time calculated to keep syncs var(Debug) float SyncTeleportWaitTime; // Position offset at start to line up syncs var(Debug) float SyncStartOffset; var(Debug) TrainPointCalcState EditorTimeTrackCalculationState; // Entire physical track length var(Debug) float WholeTrackLength; // Track "length" including teleport time and sync adjustments var(Debug) float FinalTrackLength; // Physical length of this track segment var(Debug) float SegmentLength; // Whether the track is mostly in a straight line or not var(Debug) bool TrackIsStraight; var(Debug) transient Array SyncedMasterListCache; enum TrainPointCalcState { TrainPointCalcState_None, TrainPointCalcState_TrackLength, TrainPointCalcState_Complete, }; function float GetWholeTrackLength(optional bool final) { if (TrackMaster != None) return (final && TrackMaster.FinalTrackLength > 0) ? TrackMaster.FinalTrackLength : TrackMaster.WholeTrackLength; return 0; } function DirtyGeneratedTrackMesh() { local mcu8_TrainPoint tp; foreach WholeTrack(tp) tp.GeneratedTrackMeshDirty = true; } event editoronly OnEditorPropertyChanged(Name PropertyName) { local mcu8_TrainPoint tp; if (PropertyName == 'Next') Teleport = false; if (PropertyName == 'Next' || PropertyName == 'Teleport') ConnectTrackPoints(); if (PropertyName == 'Next' || PropertyName == 'Teleport' || PropertyName == 'WaitTime') { PerformEditorTimeTrackCalculation(); DrawEditorGuide(); } if (PropertyName == 'WaitForSync') { UpdateMySyncPoints(true); CalculateSync(); } // Track mesh generation if (PropertyName == 'GenerateTrack') foreach WholeTrack(tp) tp.GenerateTrack = GenerateTrack; if (PropertyName == 'GeneratedTrackColor') { foreach WholeTrack(tp) { tp.GeneratedTrackColor = GeneratedTrackColor; tp.GeneratedTrackMeshHoloMaterial = HoloMaterials[GeneratedTrackColor]; // color all the lines too switch (tp.GeneratedTrackColor) { case Green: tp.NextLineComponent.SphereColor = MakeColor(0,255,0); break; case Yellow: tp.NextLineComponent.SphereColor = MakeColor(255,150,0); break; case Purple: tp.NextLineComponent.SphereColor = MakeColor(150,0,255); break; default: tp.NextLineComponent.SphereColor = MakeColor(0,150,255); break; } tp.ForceUpdateComponents(false, false); } } if (PropertyName == 'GenerateTrack' || PropertyName == 'GenerateSegment' || PropertyName == 'Next' || PropertyName == 'Teleport' || PropertyName == 'GeneratedTrackColor') DirtyGeneratedTrackMesh(); if (PropertyName == 'SkipSyncWaitCycles') { foreach WholeTrack(tp) tp.SkipSyncWaitCycles = SkipSyncWaitCycles; // sync calculations changed, so recalculate them PerformEditorTimeTrackCalculation(); } if (PropertyName == 'GenerateTrack' || PropertyName == 'GenerateSegment') PerformEditorTimeTrackCalculation();// this changes a bool in subpoints, so recalculate } event editoronly OnEditorMoved() { ConnectTrackPoints(); if (Next != None && Prev != None) PerformEditorTimeTrackCalculation(); DrawEditorGuide(); DirtyGeneratedTrackMesh(); } function expensive editoronly ConnectTrackPoints() { local mcu8_TrainPoint tp; local Array CheckPoints; CheckPoints = GetRelevantPoints(); foreach CheckPoints(tp) tp.Prev = None; foreach CheckPoints(tp) if (tp.Next != None) tp.Next.Prev = tp; foreach CheckPoints(tp) tp.FixDuplicateNext(CheckPoints); foreach CheckPoints(tp) tp.ConnectDeadEnd(); } function editoronly FixAllBrokenPoints() { local mcu8_TrainPoint tp; local int i; foreach AllActors(class'mcu8_TrainPoint', tp) { // wipe any None syncs for (i = 0; i < tp.WaitForSync.Length; i++) { if (tp.WaitForSync[i] == None) { tp.WaitForSync.Remove(i, 1); i--; } } if (tp.TrackMaster != None) tp.TrackMaster.ConditionalPerformEditorTimeTrackCalculation(); else tp.ConditionalPerformEditorTimeTrackCalculation(); } } // get all connected points, or points that should be connected (or shouldn't be connected, but are) function editoronly Array GetRelevantPoints() { local mcu8_TrainPoint tp; local Array r; if (Next != None) { Next.UpdateWholeTrackArray(); r = Next.WholeTrack; // add any previous points that might have been missed tp = Next; while (tp != None) { if (r.Find(tp) != INDEX_NONE) break; r.AddItem(tp); tp = tp.Prev; } } tp = self; while (tp != None) { if (r.Find(tp) != INDEX_NONE) break; r.AddItem(tp); tp = tp.Prev; } // anything with the same master too foreach AllActors(class'mcu8_TrainPoint', tp) if (tp.TrackMaster == TrackMaster) if (r.Find(tp) == INDEX_NONE) r.AddItem(tp); return r; } function editoronly UpdateWholeTrackArray() { local mcu8_TrainPoint tp; WholeTrack.Length = 0; tp = self; while (tp.Next != None) { if (WholeTrack.Length > 0 && tp == WholeTrack[0])// looped return; if (WholeTrack.Find(tp) != INDEX_NONE)// improper loop { WholeTrack.Length = 0; return; } WholeTrack.AddItem(tp); tp = tp.Next; } } function editoronly FixDuplicateNext(Array CheckPoints) { local mcu8_TrainPoint tp; local Array split; local int mynum; if (Next == None || Prev == None) return; split = SplitString(string(Name), "_", true); mynum = int(split[split.Length-1]); foreach CheckPoints(tp) { if (tp.Next == tp) tp.Next = None; if (tp.Next != Next || tp.Next == None || tp == self) continue; split = SplitString(string(tp.Name), "_", true); if (tp.Prev == None || mynum < int(split[split.Length-1])) { Next.Prev = tp; tp.Next = Next; tp.Prev = self; Next = tp; } } } function editoronly ConnectDeadEnd() { local mcu8_TrainPoint tp; local Array checked; if (Prev != None) return; tp = self; while (tp.Next != None) { tp = tp.Next; if (tp == self) return;// looped! if (checked.Find(tp) != INDEX_NONE) return;// weird loop! checked.AddItem(tp); } if (tp == self) return;// not connected to anything! Prev = tp; tp.Teleport = true; tp.Next = self; } function editoronly UpdateMySyncPoints(bool remove) { local mcu8_TrainPoint tp; local Array ChangedPoints; local int i; if (TrackMaster != None) ChangedPoints.AddItem(TrackMaster); foreach AllActors(class'mcu8_TrainPoint', tp) { // wipe any None syncs for (i = 0; i < tp.WaitForSync.Length; i++) { if (tp.WaitForSync[i] == None) { tp.WaitForSync.Remove(i, 1); i--; } } if (WaitForSync.Find(tp) == INDEX_NONE) { if (remove) { tp.WaitForSync.RemoveItem(self); tp.ForceUpdateComponents(false, false); if (tp.TrackMaster != None) ChangedPoints.AddItem(tp.TrackMaster); } } else if (tp.WaitForSync.Find(self) == INDEX_NONE) { tp.WaitForSync.AddItem(self); tp.ForceUpdateComponents(false, false); if (tp.TrackMaster != None) ChangedPoints.AddItem(tp.TrackMaster); } } foreach ChangedPoints(tp) tp.BuildAIPathsHook(); } event editoronly name GetCustomTickOptimizationGroup() { local Array TrackMasters; local name BestName; local int i, BestNameNumber, Number; if (TrackMaster == None) return ''; if (TrackMaster != self) return TrackMaster.GetCustomTickOptimizationGroup(); GetSyncedTrackMasterList(TrackMasters); // Pick the lowest of them. for (i = 0; i < TrackMasters.Length; i++) { Number = int(GetRightMost(String(TrackMasters[i].Name))); if (i == 0 || Number < BestNameNumber) { BestName = TrackMasters[i].Name; BestNameNumber = Number; } } return BestName; } event editoronly CheckForErrors(out Array ErrorMessages) { Super.CheckForErrors(ErrorMessages); if (!Teleport && WaitTime > 0) ErrorMessages.AddItem("WaitTime only supported for teleport."); if (TrackMaster != None && TrackMaster == self) { if (EditorTimeTrackCalculationState != TrainPointCalcState_Complete) ErrorMessages.AddItem("Track calculations incomplete. Move a track point!"); else if (SubPoints.Length == 0) ErrorMessages.AddItem("Track has no subpoints. Move a track point!"); } } function float GetCombinedWaitTime() { return WaitTime + SyncTeleportWaitTime; } event editoronly GetCustomEditorCommands(out Array Result) { local ActorCustomEditorCommand Item; if (Next != None && Prev != None) { Item.Text = "Reverse Track"; Item.Command = 'ReverseTrack'; Result.AddItem(Item); } if (Next != None && Prev != None) { Item.Text = "Split Track"; Item.Command = 'SplitTrack'; Result.AddItem(Item); } Item.Text = "Recalculate All Tracks"; Item.Command = 'RecalculateAll'; Item.bOnlyCallOnFirstSelected = true; Result.AddItem(Item); Item.Text = "Update All Optimization Groups"; Item.Command = 'UpdateOptimization'; Item.bOnlyCallOnFirstSelected = true; Result.AddItem(Item); Item.Text = "Generate Meshes"; Item.Command = 'GenerateMeshes'; Result.AddItem(Item); Item.Text = "Generate All Track Meshes"; Item.Command = 'GenerateAllTrackMeshes'; Result.AddItem(Item); } event editoronly OnEditorCustomCommand(Name Command, int Index) { if (Command == 'ReverseTrack') ReverseWholeTrack(); if (Command == 'SplitTrack') SplitTrackFromPoint(); if (Command == 'RecalculateAll') ForceRecalculateAll(); if (Command == 'UpdateOptimization') class'Hat_NPCManager'.static.ComputeTickOptimizationGroups(); if (Command == 'GenerateMeshes') GenerateMeshes(); if (Command == 'GenerateAllTrackMeshes') GenerateAllTrackMeshes(true); } function editoronly ReverseWholeTrack() { local mcu8_TrainPoint tp, curnext; local bool FirstTeleport; UpdateWholeTrackArray(); if (WholeTrack.Length <= 0) { `broadcast("Whole track not calculated"); ScriptTrace(); return; } foreach WholeTrack(tp) { curnext = tp.Next; tp.Next = tp.Prev; tp.Prev = curnext; } tp = Next; FirstTeleport = tp.Teleport; while (tp != self) { tp.Teleport = tp.Next == self ? FirstTeleport : tp.Next.Teleport; tp = tp.Next; } PerformEditorTimeTrackCalculation(); DrawEditorGuide(); } function editoronly SplitTrackFromPoint() { local mcu8_TrainPoint tp, NewPoint, FirstTeleport, FirstTeleportDest; UpdateWholeTrackArray(); if (WholeTrack.Length <= 0) { `broadcast("Whole track not calculated"); ScriptTrace(); return; } // find first teleport tp = Next; while (tp != self) { if (tp.Teleport) { FirstTeleport = tp; FirstTeleportDest = tp.Next; break; } tp = tp.Next; } if (FirstTeleport == None) { // no teleport, so just make one here FirstTeleport = Prev; FirstTeleportDest = self; } // copy this point NewPoint = Spawn(class,,, Location, Rotation); NewPoint.Prev = Prev; NewPoint.Prev.Next = NewPoint; Prev = FirstTeleport; Prev.Next = self; NewPoint.Next = FirstTeleportDest; NewPoint.Next.Prev = NewPoint; NewPoint.Teleport = true; NewPoint.GeneratedTrackColor = GeneratedTrackColor; // update track UpdateWholeTrackArray(); NewPoint.UpdateWholeTrackArray(); PerformEditorTimeTrackCalculation(); NewPoint.PerformEditorTimeTrackCalculation(); DrawEditorGuide(); } function editoronly GenerateMeshes() { local mcu8_TrainPoint tp; foreach WholeTrack(tp) tp.GenerateTrackMesh(true); } event editoronly Array GetWholeTrack() { return WholeTrack; } event editoronly Vector CalcTrackCurve(float Alpha) { return class'mcu8_Hazard_Train'.static.GetProgressPositionCurved(Alpha, self); } event editoronly bool IsTeleport() { return Teleport; } event editoronly bool ShouldHaveTrackMesh() { return TrackMaster == self; } function expensive int GetMySubPoint(optional Array SubPointList) { local int i; if (SubPointList.Length == 0 && TrackMaster != None && TrackMaster.SubPoints.Length != 0) SubPointList = TrackMaster.SubPoints; for (i = 0; i < SubPointList.Length; i++) if (VSizeSq(SubPointList[i].Location - Location) < 1) return i; return 0; } function editoronly Hat_TrainPoint_SubPoint GetSubPoint(int i, Array SubPointList) { return SubPointList[(i + SubPointList.Length) % SubPointList.Length]; } function editoronly int GetSubPointIndex(int i, Array SubPointList) { return (i + SubPointList.Length) % SubPointList.Length; } function editoronly ClearEditorTimeCalculations() { SyncSpeedMultiplier = default.SyncSpeedMultiplier; SyncTeleportWaitTime = default.SyncTeleportWaitTime; EditorTimeTrackCalculationState = TrainPointCalcState_None; SubPoints.Length = 0; WholeTrackLength = 0; FinalTrackLength = 0; SyncStartOffset = 0; SyncTeleportWaitTime = 0; SyncedMasterListCache.Length = 0; } function editoronly ConditionalPerformEditorTimeTrackCalculation() { if (EditorTimeTrackCalculationState == TrainPointCalcState_Complete) return; PerformEditorTimeTrackCalculation(); } function editoronly ForceRecalculateAll() { local mcu8_TrainPoint tp; // note: sync points must be updated in a separate pass that occurs before track calculations foreach AllActors(class'mcu8_TrainPoint', tp) tp.UpdateMySyncPoints(false); RunBuildAIPathsHooks(); } event editoronly BuildAIPathsHook() { //UpdateMySyncPoints(false); if (self == TrackMaster) PerformEditorTimeTrackCalculation(); } // Always does a full recalculation. If you only want to do a calculation if none exists, use ConditionalPerformEditorTimeTrackCalculation function editoronly PerformEditorTimeTrackCalculation(optional bool IncludePassB = true) { ClearEditorTimeCalculations(); // Let's determine the track master so we can call on him instead UpdateWholeTrackArray(); // Needed for DetermineTrackMasterForWholeTrack DetermineTrackMasterForWholeTrack(); if (TrackMaster == None) return; if (TrackMaster != self) { ClearEditorTimeCalculations(); // Clear our data since we're not the trackmaster TrackMaster.PerformEditorTimeTrackCalculation(IncludePassB); return; } PerformEditorTimeTrackCalculation_PassA(); // Calculate the track length if (IncludePassB) PerformEditorTimeTrackCalculation_PassB(); // Place the actual subpoints based on track length (& synced track's length) } function editoronly PerformEditorTimeTrackCalculation_PassA() { local mcu8_TrainPoint tp; if (TrackMaster != self) { // We're not the track master, so we shouldn't be performing the calculations. ScriptTrace(); return; } if (EditorTimeTrackCalculationState != TrainPointCalcState_None) return; UpdateWholeTrackArray(); // Clean slate foreach WholeTrack(tp) { if (tp == self) continue; tp.ClearEditorTimeCalculations(); } TrackIsStraight = IsTrackStraight(); // Calculate the whole track length CalculateWholeTrackLength(); EditorTimeTrackCalculationState = TrainPointCalcState_TrackLength; } function editoronly PerformEditorTimeTrackCalculation_PassB() { if (TrackMaster != self) { // We're not the track master, so we shouldn't be performing the calculations. ScriptTrace(); return; } // Requires PassA to have been completed if (EditorTimeTrackCalculationState != TrainPointCalcState_TrackLength) return; // Make adjustments to track for train syncing CalculateSync(); // Generate new subpoints based on the sync info PopulateSubPoints(SubPoints, true); // Get final track length (used for placing train duplicates) FinalTrackLength = CalculateSubPointTotalLength(SubPoints); EditorTimeTrackCalculationState = TrainPointCalcState_Complete; } function editoronly CalculateWholeTrackLength() { local mcu8_TrainPoint tp; local Array SubPointList; PopulateSubPoints(SubPointList, false); WholeTrackLength = CalculateSubPointTotalLength(SubPointList); foreach WholeTrack(tp) tp.CalculateSegmentDistance(SubPointList); } function editoronly float CalculateSubPointTotalLength(Array SubPointList) { local int i; local float Result; Result = 0; for (i = 0; i < SubPointList.Length; i++) Result += FMax(0, SubPointList[i].Distance); return Result; } function editoronly CalculateSegmentDistance(Array SubPointList) { local int i, n; if (SubPointList.Length == 0 && TrackMaster != None) SubPointList = TrackMaster.SubPoints; if (SubPointList.Length == 0) return; i = GetMySubPoint(SubPointList); n = Next.GetMySubPoint(SubPointList); SegmentLength = 0; while (i != n) { SegmentLength += FMax(0, SubPointList[i].Distance); i = GetSubPointIndex(i+1,SubPointList); } } function editoronly float GetSegmentDistance() { if (SegmentLength == 0) CalculateSegmentDistance(TrackMaster.SubPoints); return SegmentLength; } // We do 2 passes. First pass is just to get total track length, 2nd pass is the final subpoints for train movement function editoronly PopulateSubPoints(out Array SubPointList, optional bool FinalPass) { local int i; local mcu8_TrainPoint tp; local float density; local Vector midpoint; if (TrackMaster == None || TrackMaster != self) { // We're not the track master, so we shouldn't be performing the calculations. ScriptTrace(); return; } if (WholeTrack.Length <= 0) { // Whole track should be calculated at this point ScriptTrace(); return; } foreach WholeTrack(tp) { if (tp.Teleport) { AddSubPoint(SubPointList, tp.Location, FinalPass ? tp.GetCombinedWaitTime() : tp.WaitTime, true, tp.GenerateTrack && tp.GenerateSegment, FinalPass); } else if (TrackIsStraight) { AddSubPoint(SubPointList, tp.Location, tp.WaitTime, false, tp.GenerateTrack && tp.GenerateSegment, FinalPass); } else { midpoint = class'mcu8_Hazard_Train'.static.GetProgressPositionCurved(0.5, tp); density = VSize(Normal(midpoint-tp.Location) - Normal(tp.Next.Location-midpoint))*1.5; density *= VSize(tp.Location - tp.Next.Location)/GetConnectedTrainSpeed(false)*2; density = FCeil(Lerp(1,30,FClamp(density,0,1))); for (i = 0; i < density; i++) AddSubPoint(SubPointList, class'mcu8_Hazard_Train'.static.GetProgressPositionCurved(i/density, tp), i == 0 ? tp.WaitTime : 0.0, false, tp.GenerateTrack && tp.GenerateSegment, FinalPass); } } UpdateSubPointInfo(SubPointList); } function editoronly bool IsTrackStraight() { local mcu8_TrainPoint tp; local Vector midpoint; if (TrackMaster != None && TrackMaster != self) return TrackMaster.IsTrackStraight(); // only two points, so it's straight if (Next.Next == self) return true; // check if track has any curves in it foreach WholeTrack(tp) { if (tp.Teleport) continue; midpoint = class'mcu8_Hazard_Train'.static.GetProgressPositionCurved(0.5, tp); if (VSize(Normal(midpoint-tp.Location) - Normal(tp.Next.Location-midpoint)) >= 0.012) return false; } return true; } // Calculates distances and rotation, it was implemented this way to support wrap around for [length-1] function editoronly UpdateSubPointInfo(out Array SubPointList) { local int i; local Vector v; local bool PrevWasWaitTime; for (i = 0; i < SubPointList.Length; i++) { if (SubPointList[i].Distance == 0) SubPointList[i].Distance = VSize(GetSubPoint(i+1,SubPointList).Location - SubPointList[i].Location); if (SubPointList[i].bIsWaitTime) continue;// skip over wait time when averaging rotations, since no movement occurs PrevWasWaitTime = i > 0 && SubPointList[i-1].bIsWaitTime; v = vect(0,0,0); if (SubPointList[i].Distance != -1) v += Normal(GetSubPoint(i+1,SubPointList).Location - SubPointList[i].Location); if (GetSubPoint(PrevWasWaitTime ? i-2 : i-1,SubPointList).Distance != -1) v += Normal(SubPointList[i].Location - GetSubPoint(PrevWasWaitTime ? i-2 : i-1,SubPointList).Location); SubPointList[i].Rotation = Rotator(v); if (PrevWasWaitTime) SubPointList[i-1].Rotation = Rotator(v); } for (i = 0; i < SubPointList.Length; i++) if (SubPointList[i].bIsWaitTime) SubPointList[i].Rotation = GetSubPoint(i+1,SubPointList).Rotation; } function editoronly AddSubPoint(out Array SubPointList, vector loc, float wait, bool IsTeleport, bool GenTrack, bool FinalPass) { local Hat_TrainPoint_SubPoint sub; sub.Location = loc; sub.bHasGeneratedTrack = GenTrack; if (wait > 0) { sub.Distance = GetConnectedTrainSpeed(FinalPass)*wait;// train does wait time by traveling to this same location for the duration of the wait sub.bIsWaitTime = true; } else if (IsTeleport) sub.Distance = -1;// note: 0 distance isn't a teleport. -1 tells tracks not to curve and cars not to teleport backwards. SubPointList.AddItem(sub); if (wait > 0) AddSubPoint(SubPointList, loc, 0, IsTeleport, GenTrack, FinalPass); } function editoronly DrawEditorGuide() { local int i; local mcu8_TrainPoint tp; FlushPersistentDebugLines(); foreach WholeTrack(tp) { tp.EditorLineNext = None; tp.EditorLineNext = tp.Teleport ? None : tp.Next; tp.ForceUpdateComponents(false, false); if (tp.GetCombinedWaitTime() > 0) DrawDebugSphere(tp.Location, 70, 2, 0, 255, 50, true); for (i = 0; i < tp.SubPoints.Length; i++) { if (tp.SubPoints[i].Distance == -1) { DrawDebugLine(tp.SubPoints[i+1 < tp.SubPoints.Length ? i+1 : 0].Location, tp.SubPoints[i].Location, 100, 0, 255, true); DrawDebugSphere(tp.SubPoints[i].Location, 80, 2, 150, 0, 255, true); } else { DrawDebugLine(tp.SubPoints[i+1 < tp.SubPoints.Length ? i+1 : 0].Location, tp.SubPoints[i].Location, 255, 160, 0, true); DrawDebugSphere(tp.SubPoints[i].Location, 15, 2, 255, 210, 0, true); } } } } function editoronly mcu8_Hazard_Train GetConnectedTrain() { local mcu8_Hazard_Train ct; foreach DynamicActors(class'mcu8_Hazard_Train', ct) { if (ct.StartPoint == None) continue; if (ct.StartPoint.TrackMaster == None) continue; if (ct.StartPoint.TrackMaster.WholeTrack.Length <= 0) { // Whole track should be calculated at this point. If it isn't, it's not relevant continue; } // This point isn't associated with the train if (ct.StartPoint.TrackMaster.WholeTrack.Find(self) == INDEX_NONE) continue; return ct; } return None; } function editoronly DetermineTrackMasterForWholeTrack() { local mcu8_Hazard_Train ct; local mcu8_TrainPoint tp; ct = GetConnectedTrain(); if (ct != None) { foreach ct.StartPoint.WholeTrack(tp) tp.SetTrackMaster(ct.StartPoint); } else // if there's no train, just use the last accessed point { foreach WholeTrack(tp) tp.SetTrackMaster(self); } } function editoronly float GetConnectedTrainSpeed(bool WithSyncMultiplier) { local mcu8_Hazard_Train train; if (TrackMaster == None) return class'mcu8_Hazard_Train'.default.Speed; train = GetConnectedTrain(); if (train != None) return train.Speed * (WithSyncMultiplier ? TrackMaster.SyncSpeedMultiplier : 1.f); return class'mcu8_Hazard_Train'.default.Speed; } // What's the purpuose of this??? // function editoronly int GetConnectedDuplicates() // { // local mcu8_Hazard_Train train; // if (TrackMaster == None) return 1; // train = GetConnectedTrain(); // return 1; // } function editoronly SetTrackMaster(mcu8_TrainPoint tp) { local SpriteComponent sc; TrackMaster = tp; foreach ComponentList(class'SpriteComponent', sc) { if (sc == None) continue; sc.SetSprite(tp == self ? TrackMasterIcon : sc.default.Sprite); } } function editoronly mcu8_TrainPoint GetTeleportPointInTrack() { local mcu8_TrainPoint tp; if (TrackMaster != self) return TrackMaster.GetTeleportPointInTrack(); if (WholeTrack.Length <= 0) { `broadcast("Whole track not calculated"); ScriptTrace(); return None; } foreach WholeTrack(tp) { if (tp.Teleport) return tp; } return None; } function editoronly GetSyncedTrackMasterList(out Array TrackMasters) { local mcu8_TrainPoint tp; local int i; if (TrackMaster == None || TrackMaster.EditorTimeTrackCalculationState == TrainPointCalcState_None) PerformEditorTimeTrackCalculation(false); if (TrackMaster != self) { TrackMaster.GetSyncedTrackMasterList(TrackMasters); return; } if (SyncedMasterListCache.Length > 0) { TrackMasters = SyncedMasterListCache; return; } if (TrackMasters.Find(self) != INDEX_NONE) return;// found a sync loop, exiting TrackMasters.AddItem(self); foreach WholeTrack(tp) { for (i = 0; i < tp.WaitForSync.Length; i++) { if (tp.WaitForSync[i] == None) continue; tp.WaitForSync[i].GetSyncedTrackMasterList(TrackMasters); } } } function editoronly CalculateSync() { local Array TrackMasters; local mcu8_TrainPoint tp, TeleportPoint, OffsetMaster; local int i; local float LongestTrackTime, TrainSpeed, MyTrackTime, HighestStartWait, SyncOffsetOffset; local mcu8_Hazard_Train train; // Get all track masters in our connection chain GetSyncedTrackMasterList(TrackMasters); SyncedMasterListCache = TrackMasters; for (i = 0; i < TrackMasters.Length; i++) { // verify track is computed properly if (TrackMasters[i].EditorTimeTrackCalculationState == TrainPointCalcState_None) TrackMasters[i].PerformEditorTimeTrackCalculation_PassA(); if (TrackMasters[i].WholeTrackLength <= 0) { // Whole track length should be calculated at this point ScriptTrace(); continue; } // get longest track time TrainSpeed = TrackMasters[i].GetConnectedTrainSpeed(false); MyTrackTime = TrackMasters[i].WholeTrackLength / TrainSpeed; LongestTrackTime = FMax(LongestTrackTime, MyTrackTime); // wipe sync offsets TrackMasters[i].SyncStartOffset = 0; } for (i = 0; i < TrackMasters.Length; i++) { TrainSpeed = TrackMasters[i].GetConnectedTrainSpeed(false); MyTrackTime = TrackMasters[i].WholeTrackLength / TrainSpeed; TeleportPoint = TrackMasters[i].GetTeleportPointInTrack(); // Reset existing calculations foreach TrackMasters[i].WholeTrack(tp) { tp.SyncSpeedMultiplier = 1; tp.SyncTeleportWaitTime = 0; } if (TeleportPoint != None) { TeleportPoint.SyncTeleportWaitTime = LongestTrackTime - MyTrackTime; if (TrackMasters[i].SkipSyncWaitCycles > 0 && LongestTrackTime >= MyTrackTime*(TrackMasters[i].SkipSyncWaitCycles+1)) TeleportPoint.SyncTeleportWaitTime = LongestTrackTime/float(TrackMasters[i].SkipSyncWaitCycles+1) - MyTrackTime; } else TrackMasters[i].SyncSpeedMultiplier = MyTrackTime / LongestTrackTime; CalculateSyncStartOffset(TrackMasters[i]); } // pick an offset to offset all the offsets by (so at least one offset is zero), so the trains don't have to move so much at the start of the game OffsetMaster = self; for (i = 0; i < TrackMasters.Length; i++) { train = TrackMasters[i].GetConnectedTrain(); // get the train with the smallest sync offset if (HighestStartWait == 0 && TrackMasters[i].SyncStartOffset < OffsetMaster.SyncStartOffset) OffsetMaster = TrackMasters[i]; // use train with highest start wait, since the timing there is probably important if (train.GetAdjustedSpeed()*train.StartWait > HighestStartWait) { OffsetMaster = TrackMasters[i]; HighestStartWait = train.GetAdjustedSpeed()*train.StartWait; } } // offset all trains by highest start offset (relative to the speed of the OffsetMaster train) SyncOffsetOffset = OffsetMaster.SyncStartOffset; for (i = 0; i < TrackMasters.Length; i++) TrackMasters[i].SyncStartOffset -= SyncOffsetOffset * (TrackMasters[i].GetConnectedTrainSpeed(true)/OffsetMaster.GetConnectedTrainSpeed(true)); } function editoronly CalculateSyncStartOffset(mcu8_TrainPoint master) { local mcu8_TrainPoint tp, tpp; local float dist, FirstSyncDist; local mcu8_Hazard_Train train; local Array checked; train = master.GetConnectedTrain(); tp = train.StartPoint;// doesn't matter which train FirstSyncDist = -1; while (tp.Next != None) { if (tp.WaitForSync.Length > 0)// we hit a sync point { if (FirstSyncDist == -1) { // first sync point, move the train to it master.SyncStartOffset += dist; FirstSyncDist = dist; } else { // any sync point past the first, offset all other connected trains backwards recursively to line up checked.Length = 0; checked.AddItem(master); foreach tp.WaitForSync(tpp) RecursiveSyncOffset(tpp.TrackMaster, FirstSyncDist - dist, checked); } } dist += tp.GetSegmentDistance(); tp = tp.Next; if (tp == train.StartPoint) break; } } function editoronly RecursiveSyncOffset(mcu8_TrainPoint master, float dist, out Array checked) { local mcu8_TrainPoint tp, tpp; master.SyncStartOffset += dist; checked.AddItem(master); foreach master.WholeTrack(tp) foreach tp.WaitForSync(tpp) if (checked.Find(tpp.TrackMaster) == INDEX_NONE) RecursiveSyncOffset(tpp.TrackMaster, dist, checked); } defaultproperties { Begin Object Name=Sprite Sprite=Texture2D'HatInTime_Editor.Textures.traintrack2' Scale=0.2 HiddenGame=true AlwaysLoadOnClient=false AlwaysLoadOnServer=false SpriteCategoryName="TargetPoint" MaxDrawDistance = 6000; End Object Begin Object Class=Hat_DrawCatapultComponent Name=DrawCatapultComponent0 PropertyName_CatapultTargetActor = "EditorLineNext" LineWidth = 30 SphereColor=(R=0,G=255,B=255,A=255) bSelectable=false HiddenGame=true MaxDrawDistance = 4000; End Object Components.Add(DrawCatapultComponent0) NextLineComponent=DrawCatapultComponent0 Begin Object Class=Hat_DrawCatapultComponent Name=DrawCatapultComponent1 PropertyName_CatapultTargetActor = "WaitForSync" LineWidth = 15 SphereColor=(R=255,G=0,B=255,A=255) bSelectable=false HiddenGame=true MaxDrawDistance = 4000; End Object Components.Add(DrawCatapultComponent1) SyncLineComponent=DrawCatapultComponent1 bStatic = true bNoDelete = true bMovable = false bHidden = false SpeedMultiplier = 1 TrackMasterIcon = Texture2D'HatInTime_Editor.Textures.TrainTrackicon' /*EnableTickOptimize = true*/ // note: train points don't actually tick, but shares an optimization group with trains to keep them running when out of sight TickOptimize = TickOptimize_View TickOptimize_TimingSensitive = true SingleTrackMesh = StaticMesh'HatinTime_Metro_Generation.models.metro_hologram_rail' TrackOverlap = 18.4 HoloMaterials[0] = Material'HatInTime_Levels_Metro_J.Materials.tracks_holo' HoloMaterials[1] = MaterialInstanceConstant'HatInTime_Levels_Metro_J.Materials.tracks_holo_green' HoloMaterials[2] = MaterialInstanceConstant'HatInTime_Levels_Metro_J.Materials.tracks_holo_yellow' HoloMaterials[3] = MaterialInstanceConstant'HatInTime_Levels_Metro_J.Materials.tracks_holo_purple' GeneratedTrackMeshHoloMaterial = Material'HatInTime_Levels_Metro_J.Materials.tracks_holo' SyncSpeedMultiplier = 1; }