diff --git a/source/LibRender2/BaseRenderer.cs b/source/LibRender2/BaseRenderer.cs index 1e31ad2d7..68a454d24 100644 --- a/source/LibRender2/BaseRenderer.cs +++ b/source/LibRender2/BaseRenderer.cs @@ -560,6 +560,17 @@ public void Reset() VisibleObjects.Clear(); } + /// Resets the renderer state selectively from a given track position. + /// The track position from which to clear objects. + public void ResetSelective(double startPosition) + { + currentHost.AnimatedObjectCollectionCache.Clear(); + // clear out world objects from the change point onwards so we can re-add them + StaticObjectStates.RemoveAll(s => s.TrackPosition >= startPosition || (s.TrackPosition == -1.0f && s.StartingDistance >= startPosition)); + DynamicObjectStates.RemoveAll(s => s.TrackPosition >= startPosition || (s.TrackPosition == -1.0f && s.StartingDistance >= startPosition)); + VisibleObjects.ClearSelective(startPosition); + } + public int CreateStaticObject(StaticObject Prototype, Vector3 Position, Transformation WorldTransformation, Transformation LocalTransformation, ObjectDisposalMode AccurateObjectDisposal, double AccurateObjectDisposalZOffset, double StartingDistance, double EndingDistance, double BlockLength, double TrackPosition) { Matrix4D Translate = Matrix4D.CreateTranslation(Position.X, Position.Y, -Position.Z); @@ -643,6 +654,7 @@ public int CreateStaticObject(Vector3 Position, StaticObject Prototype, Transfor Rotate = Rotate, StartingDistance = startingDistance, EndingDistance = endingDistance, + TrackPosition = (float)TrackPosition, WorldPosition = Position }); diff --git a/source/LibRender2/Objects/ObjectLibrary.cs b/source/LibRender2/Objects/ObjectLibrary.cs index a0d5dfc02..88dde206e 100644 --- a/source/LibRender2/Objects/ObjectLibrary.cs +++ b/source/LibRender2/Objects/ObjectLibrary.cs @@ -85,6 +85,24 @@ public void Clear() myOverlayAlphaFaces.Clear(); renderer.StaticObjectStates.Clear(); renderer.DynamicObjectStates.Clear(); + quadTree.Clear(); + } + } + + /// Selective cleanup of objects and spatial partitioning tree. + /// The track position from which to clear. + public void ClearSelective(double startPosition) + { + lock (LockObject) + { + // remove objects effectively from the modification point onwards + List toRemove = Objects.Keys.Where(s => s.StartingDistance >= startPosition).ToList(); + foreach (var state in toRemove) + { + RemoveObject(state); + } + // clear the tree so it gets rebuilt properly with the new world objects + quadTree.Clear(); } } diff --git a/source/OpenBveApi/Objects/ObjectState.cs b/source/OpenBveApi/Objects/ObjectState.cs index 37b5cc34f..4d2011176 100644 --- a/source/OpenBveApi/Objects/ObjectState.cs +++ b/source/OpenBveApi/Objects/ObjectState.cs @@ -1,4 +1,4 @@ -using System; +using System; using OpenBveApi.Math; namespace OpenBveApi.Objects @@ -79,6 +79,8 @@ public Matrix4D ModelMatrix public float StartingDistance; /// The ending track position, for static objects only. public float EndingDistance; + /// The track position marker (used for selective reload cleanup) + public float TrackPosition; /// Creates a new ObjectState public ObjectState() @@ -90,6 +92,7 @@ public ObjectState() TextureTranslation = Matrix4D.Identity; StartingDistance = 0.0f; EndingDistance = 0.0f; + TrackPosition = -1.0f; updateModelMatrix = false; } diff --git a/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObject.cs b/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObject.cs index d367e317a..b0615a899 100644 --- a/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObject.cs +++ b/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObject.cs @@ -760,6 +760,27 @@ public void Update(AbstractTrain Train, int CarIndex, double TrackPosition, Vect /// The index of the section if placed using a SigF command /// The absolute track position public void CreateObject(Vector3 Position, Transformation WorldTransformation, Transformation LocalTransformation, int sectionIndex, double TrackPosition) + { + CreateObject(Position, WorldTransformation, LocalTransformation, sectionIndex, 0.0f, 0.0f, TrackPosition); + } + + /// The starting distance + /// The ending distance + /// The absolute track position + public void CreateObject(Vector3 Position, Transformation WorldTransformation, Transformation LocalTransformation, int sectionIndex, double StartingDistance, double EndingDistance, double TrackPosition) + { + CreateObject(Position, WorldTransformation, LocalTransformation, sectionIndex, (float)StartingDistance, (float)EndingDistance, TrackPosition); + } + + /// Creates the animated object within the worldspace + /// The absolute position + /// The world transformation to apply (e.g. ground, rail) + /// The local transformation to apply in order to rotate the model + /// The index of the section if placed using a SigF command + /// The starting distance + /// The ending distance + /// The absolute track position + public void CreateObject(Vector3 Position, Transformation WorldTransformation, Transformation LocalTransformation, int sectionIndex, float StartingDistance, float EndingDistance, double TrackPosition) { int a = currentHost.AnimatedWorldObjectsUsed; @@ -770,6 +791,9 @@ public void CreateObject(Vector3 Position, Transformation WorldTransformation, T { var o = this.Clone(); currentHost.CreateDynamicObject(ref o.internalObject); + o.internalObject.StartingDistance = StartingDistance; + o.internalObject.EndingDistance = EndingDistance; + o.internalObject.TrackPosition = (float)TrackPosition; TrackFollowingObject currentObject = new TrackFollowingObject(currentHost) { Position = Position, @@ -814,6 +838,9 @@ public void CreateObject(Vector3 Position, Transformation WorldTransformation, T { var o = this.Clone(); currentHost.CreateDynamicObject(ref o.internalObject); + o.internalObject.StartingDistance = StartingDistance; + o.internalObject.EndingDistance = EndingDistance; + o.internalObject.TrackPosition = (float)TrackPosition; o.SectionIndex = sectionIndex; AnimatedWorldObject currentObject = new AnimatedWorldObject(currentHost) { diff --git a/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObjectCollection.cs b/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObjectCollection.cs index 599489730..a1e425ed8 100644 --- a/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObjectCollection.cs +++ b/source/OpenBveApi/Objects/ObjectTypes/AnimatedObject/AnimatedObjectCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using OpenBveApi.Hosts; using OpenBveApi.Math; @@ -75,7 +75,7 @@ public override void CreateObject(Vector3 Position, Transformation WorldTransfor } else { - Objects[i].Clone().CreateObject(Position, WorldTransformation, LocalTransformation, SectionIndex, TrackPosition); + Objects[i].Clone().CreateObject(Position, WorldTransformation, LocalTransformation, SectionIndex, StartingDistance, EndingDistance, TrackPosition); } } } @@ -86,7 +86,7 @@ public override void CreateObject(Vector3 Position, Transformation WorldTransfor { if (Objects[i].States.Length != 0) { - Objects[i].Clone().CreateObject(Position, WorldTransformation, LocalTransformation, SectionIndex, TrackPosition); + Objects[i].Clone().CreateObject(Position, WorldTransformation, LocalTransformation, SectionIndex, StartingDistance, EndingDistance, TrackPosition); } } } diff --git a/source/OpenBveApi/Objects/ObjectTypes/KeyframeAnimatedObject/KeyframeAnimatedObject.cs b/source/OpenBveApi/Objects/ObjectTypes/KeyframeAnimatedObject/KeyframeAnimatedObject.cs index 3c9941d39..9e171360f 100644 --- a/source/OpenBveApi/Objects/ObjectTypes/KeyframeAnimatedObject/KeyframeAnimatedObject.cs +++ b/source/OpenBveApi/Objects/ObjectTypes/KeyframeAnimatedObject/KeyframeAnimatedObject.cs @@ -99,6 +99,9 @@ public override void CreateObject(Vector3 position, Transformation worldTransfor Objects[i].WorldPosition = position; Objects[i].Rotate = Matrix4D.NoTransformation; Objects[i].Translation = Matrix4D.NoTransformation; + Objects[i].StartingDistance = (float)startingDistance; + Objects[i].EndingDistance = (float)endingDistance; + Objects[i].TrackPosition = (float)trackPosition; currentHost.ShowObject(Objects[i], ObjectType.Dynamic); Objects[i].Matricies = matriciesToShader; } diff --git a/source/OpenBveApi/Routes/RouteInterface.cs b/source/OpenBveApi/Routes/RouteInterface.cs index 970942c0d..d0ebfe5ff 100644 --- a/source/OpenBveApi/Routes/RouteInterface.cs +++ b/source/OpenBveApi/Routes/RouteInterface.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace OpenBveApi.Routes { @@ -41,5 +41,8 @@ public virtual void Unload() { } /// Holds the current loading progress public double CurrentProgress; + + /// The track position to start parsing from (for selective reloading) + public double StartTrackPosition = 0; } } diff --git a/source/Plugins/Route.CsvRw/CsvRwRouteParser.ApplyRouteData.cs b/source/Plugins/Route.CsvRw/CsvRwRouteParser.ApplyRouteData.cs index f47881be2..6b2451c35 100644 --- a/source/Plugins/Route.CsvRw/CsvRwRouteParser.ApplyRouteData.cs +++ b/source/Plugins/Route.CsvRw/CsvRwRouteParser.ApplyRouteData.cs @@ -633,7 +633,11 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl int gi = Data.Blocks[i].Cycle[ci]; if (Data.Structure.Ground.ContainsKey(gi)) { - Data.Structure.Ground[Data.Blocks[i].Cycle[ci]].CreateObject(Position + new Vector3(0.0, -Data.Blocks[i].Height, 0.0), GroundTransformation, StartingDistance, EndingDistance, StartingDistance); + // only re-create objects if they are in the modified section + if (StartingDistance >= StartTrackPosition) + { + Data.Structure.Ground[Data.Blocks[i].Cycle[ci]].CreateObject(Position + new Vector3(0.0, -Data.Blocks[i].Height, 0.0), GroundTransformation, StartingDistance, EndingDistance, StartingDistance); + } } } // ground-aligned free objects @@ -641,13 +645,19 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl { for (int j = 0; j < Data.Blocks[i].GroundFreeObj.Count; j++) { - Data.Blocks[i].GroundFreeObj[j].CreateGroundAligned(Data.Structure.FreeObjects, Position, GroundTransformation, Direction, Data.Blocks[i].Height, StartingDistance, EndingDistance); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].GroundFreeObj[j].CreateGroundAligned(Data.Structure.FreeObjects, Position, GroundTransformation, Direction, Data.Blocks[i].Height, StartingDistance, EndingDistance); + } } } if (!PreviewOnly && Data.Structure.WeatherObjects.ContainsKey(Data.Blocks[i].WeatherObject)) { - UnifiedObject obj = Data.Structure.WeatherObjects[Data.Blocks[i].WeatherObject]; - obj.CreateObject(Position, GroundTransformation, Data.Blocks[i].Height, StartingDistance, EndingDistance); + if (StartingDistance >= StartTrackPosition) + { + UnifiedObject obj = Data.Structure.WeatherObjects[Data.Blocks[i].WeatherObject]; + obj.CreateObject(Position, GroundTransformation, Data.Blocks[i].Height, StartingDistance, EndingDistance); + } } // rail-aligned objects { @@ -784,7 +794,10 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl if (Data.Structure.RailObjects.ContainsKey(Data.Blocks[i].RailType[railKey])) { - Data.Structure.RailObjects[Data.Blocks[i].RailType[railKey]]?.CreateObject(pos, RailTransformation, StartingDistance, EndingDistance, StartingDistance); + if (StartingDistance >= StartTrackPosition) + { + Data.Structure.RailObjects[Data.Blocks[i].RailType[railKey]]?.CreateObject(pos, RailTransformation, StartingDistance, EndingDistance, StartingDistance); + } } // points of interest @@ -822,19 +835,28 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl // poles if (Data.Blocks[i].RailPole.Length > railKey) { - Data.Blocks[i].RailPole[railKey].Create(Data.Structure.Poles, pos, RailTransformation, Direction, planar, updown, StartingDistance, EndingDistance); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].RailPole[railKey].Create(Data.Structure.Poles, pos, RailTransformation, Direction, planar, updown, StartingDistance, EndingDistance); + } } // walls if (Data.Blocks[i].RailWall.ContainsKey(railKey)) { - Data.Blocks[i].RailWall[railKey].Create(pos, RailTransformation, StartingDistance, EndingDistance); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].RailWall[railKey].Create(pos, RailTransformation, StartingDistance, EndingDistance); + } } // dikes if (Data.Blocks[i].RailDike.ContainsKey(railKey)) { - Data.Blocks[i].RailDike[railKey].Create(pos, RailTransformation, StartingDistance, EndingDistance); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].RailDike[railKey].Create(pos, RailTransformation, StartingDistance, EndingDistance); + } } // sounds @@ -873,7 +895,10 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl { for (int k = 0; k < Data.Blocks[i].RailFreeObj[railKey].Count; k++) { - Data.Blocks[i].RailFreeObj[railKey][k].CreateRailAligned(Data.Structure.FreeObjects, new Vector3(pos), RailTransformation, StartingDistance, EndingDistance); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].RailFreeObj[railKey][k].CreateRailAligned(Data.Structure.FreeObjects, new Vector3(pos), RailTransformation, StartingDistance, EndingDistance); + } } } @@ -891,9 +916,17 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl // patterns key off rail 0 while (Data.Blocks[i].PatternObjs[key].LastPlacement + Data.Blocks[i].PatternObjs[key].Interval < (i + 1) * Data.BlockInterval) { - if (!Data.Blocks[i].PatternObjs[key].CreateRailAligned(Data.Structure.FreeObjects, new Vector3(pos), RailTransformation, StartingDistance, EndingDistance)) + if (StartingDistance >= StartTrackPosition) { - break; + if (!Data.Blocks[i].PatternObjs[key].CreateRailAligned(Data.Structure.FreeObjects, new Vector3(pos), RailTransformation, StartingDistance, EndingDistance)) + { + break; + } + } + else + { + // we still need to "pretend" to place it to keep the state correct + Data.Blocks[i].PatternObjs[key].LastPlacement += Data.Blocks[i].PatternObjs[key].Interval; } } @@ -910,18 +943,27 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl { for (int k = 0; k < Data.Blocks[i].Transponders.Length; k++) { - double b = 0.25 + 0.75 * Data.GetBrightness(Data.Blocks[i].Transponders[k].TrackPosition); - Data.Blocks[i].Transponders[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, b, Data.Structure.Beacon); + if (StartingDistance >= StartTrackPosition) + { + double b = 0.25 + 0.75 * Data.GetBrightness(Data.Blocks[i].Transponders[k].TrackPosition); + Data.Blocks[i].Transponders[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, b, Data.Structure.Beacon); + } } for (int k = 0; k < Data.Blocks[i].DestinationChanges.Length; k++) { - Data.Blocks[i].DestinationChanges[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, Data.Structure.Beacon); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].DestinationChanges[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, Data.Structure.Beacon); + } } for (int k = 0; k < Data.Blocks[i].HornBlows.Length; k++) { - Data.Blocks[i].HornBlows[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, Data.Structure.Beacon); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].HornBlows[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, Data.Structure.Beacon); + } } } @@ -931,7 +973,10 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl // signals for (int k = 0; k < Data.Blocks[i].Signals.Length; k++) { - Data.Blocks[i].Signals[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, 0.27 + 0.75 * Data.GetBrightness(Data.Blocks[i].Signals[k].TrackPosition)); + if (StartingDistance >= StartTrackPosition) + { + Data.Blocks[i].Signals[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, 0.27 + 0.75 * Data.GetBrightness(Data.Blocks[i].Signals[k].TrackPosition)); + } } // sections @@ -957,8 +1002,11 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl { if (railKey == Data.Blocks[i].Limits[k].RailIndex) { - double b = 0.25 + 0.75 * Data.GetBrightness(Data.Blocks[i].Limits[k].TrackPosition); - Data.Blocks[i].Limits[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, b, Data.UnitOfSpeed); + if (StartingDistance >= StartTrackPosition) + { + double b = 0.25 + 0.75 * Data.GetBrightness(Data.Blocks[i].Limits[k].TrackPosition); + Data.Blocks[i].Limits[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, b, Data.UnitOfSpeed); + } } } @@ -967,8 +1015,11 @@ private void ApplyRouteData(string FileName, ref RouteData Data, bool PreviewOnl { for (int k = 0; k < Data.Blocks[i].StopPositions.Length; k++) { - double b = 0.25 + 0.75 * Data.GetBrightness(Data.Blocks[i].StopPositions[k].TrackPosition); - Data.Blocks[i].StopPositions[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, b); + if (StartingDistance >= StartTrackPosition) + { + double b = 0.25 + 0.75 * Data.GetBrightness(Data.Blocks[i].StopPositions[k].TrackPosition); + Data.Blocks[i].StopPositions[k].Create(new Vector3(pos), RailTransformation, StartingDistance, EndingDistance, b); + } } } } diff --git a/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs b/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs index ffa4f86a7..7b3e021b9 100644 --- a/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs +++ b/source/Plugins/Route.CsvRw/CsvRwRouteParser.cs @@ -24,6 +24,7 @@ internal partial class Parser { internal readonly bool IsRW; internal readonly Plugin Plugin; internal bool IsHmmsim; + internal double StartTrackPosition = 0; internal Parser(Plugin plugin, bool isRW) { @@ -486,7 +487,19 @@ private void ParseRouteForData(string FileName, System.Text.Encoding Encoding, E case "track": if (Enum.TryParse(Command, true, out TrackCommand parsedCommand)) { + // speed up reloads by skipping objects before the edit point (we already have them in the renderer) + // however we still need to parse geometry/signaling stuff to keep the state correct + if (Data.TrackPosition < StartTrackPosition) + { + switch (parsedCommand) + { + case TrackCommand.FreeObj: + case TrackCommand.SigF: + goto skipCommand; + } + } ParseTrackCommand(parsedCommand, Arguments, FileName, UnitOfLength, Expressions[j], ref Data, BlockIndex, PreviewOnly, IsRW); + skipCommand:; } else { diff --git a/source/Plugins/Route.CsvRw/Plugin.cs b/source/Plugins/Route.CsvRw/Plugin.cs index 9598ad6ab..889780ba0 100644 --- a/source/Plugins/Route.CsvRw/Plugin.cs +++ b/source/Plugins/Route.CsvRw/Plugin.cs @@ -125,6 +125,7 @@ public override bool LoadRoute(string path, Encoding textEncoding, string trainP try { Parser parser = new Parser(this, isRw); + parser.StartTrackPosition = StartTrackPosition; parser.ParseRoute(path, textEncoding, trainPath, objectPath, soundPath, PreviewOnly); IsLoading = false; return true; diff --git a/source/RouteViewer/GameR.cs b/source/RouteViewer/GameR.cs index 0387cfe64..6253a3924 100644 --- a/source/RouteViewer/GameR.cs +++ b/source/RouteViewer/GameR.cs @@ -34,7 +34,67 @@ internal static void Reset(bool resetRenderer = true) { { Program.Renderer.Reset(); } + // object manager + Program.Renderer.InitializeVisibility(); + ObjectManager.AnimatedWorldObjects = new WorldObject[4]; + ObjectManager.AnimatedWorldObjectsUsed = 0; + ResetInternal(); + } + + /// Selective reset for auto-reloading. + /// The track position from which to clear. + internal static void ResetSelective(double startPosition) + { + Program.Renderer.ResetSelective(startPosition); + // Selective cleanup of animated world objects: + // We need to explicitly hide each removed object from the renderer, + // otherwise its mesh faces stay in the draw lists and keep rendering as ghosts + int newCount = 0; + for (int i = 0; i < ObjectManager.AnimatedWorldObjectsUsed; i++) + { + if (ObjectManager.AnimatedWorldObjects[i].TrackPosition < startPosition) + { + // keep this one, it's before the edit point + ObjectManager.AnimatedWorldObjects[newCount++] = ObjectManager.AnimatedWorldObjects[i]; + } + else + { + // this object is at or past the edit point, hide it from the renderer + // so its faces get removed from the draw lists + var wo = ObjectManager.AnimatedWorldObjects[i]; + + // KeyframeWorldObject shadows Object with KeyframeAnimatedObject which has an Objects[] array + if (wo is KeyframeWorldObject kwo && kwo.Object?.Objects != null) + { + for (int j = 0; j < kwo.Object.Objects.Length; j++) + { + if (kwo.Object.Objects[j] != null) + { + Program.Renderer.VisibleObjects.HideObject(kwo.Object.Objects[j]); + Program.Renderer.DynamicObjectStates.Remove(kwo.Object.Objects[j]); + } + } + } + // all other WorldObject subtypes use the base AnimatedObject.internalObject + else if (wo.Object?.internalObject != null) + { + Program.Renderer.VisibleObjects.HideObject(wo.Object.internalObject); + Program.Renderer.DynamicObjectStates.Remove(wo.Object.internalObject); + } + } + } + ObjectManager.AnimatedWorldObjectsUsed = newCount; + for (int i = newCount; i < ObjectManager.AnimatedWorldObjects.Length; i++) + { + ObjectManager.AnimatedWorldObjects[i] = null; + } + + ResetInternal(); + } + + private static void ResetInternal() + { // track manager Program.CurrentRoute.Tracks = new Dictionary(); Program.CurrentRoute.Tracks.Add(0, new Track()); @@ -64,11 +124,6 @@ internal static void Reset(bool resetRenderer = true) { Program.Renderer.InfoTotalQuads = 0; Program.Renderer.InfoTotalQuadStrip = 0; Program.Renderer.InfoTotalPolygon = 0; - Program.Renderer.VisibleObjects.quadTree.Clear(); - // object manager - Program.Renderer.InitializeVisibility(); - ObjectManager.AnimatedWorldObjects = new WorldObject[4]; - ObjectManager.AnimatedWorldObjectsUsed = 0; // renderer / sound Program.Sounds.StopAllSounds(); GC.Collect(); diff --git a/source/RouteViewer/LoadingR.cs b/source/RouteViewer/LoadingR.cs index ed29a0d78..6d378c4ba 100644 --- a/source/RouteViewer/LoadingR.cs +++ b/source/RouteViewer/LoadingR.cs @@ -49,15 +49,23 @@ internal static bool Cancel internal static bool Complete; private static string CurrentRouteFile; private static Encoding CurrentRouteEncoding; + private static double currentStartPosition; internal static bool JobAvailable; // load - internal static void Load(string routeFile, Encoding routeEncoding, byte[] textureBytes) + internal static void Load(string routeFile, Encoding routeEncoding, byte[] textureBytes, double startPosition = 0) { Program.Renderer.GameWindow.TargetRenderFrequency = 0; // reset - Game.Reset(); + if (startPosition > 0) + { + Game.ResetSelective(startPosition); + } + else + { + Game.Reset(); + } Program.Renderer.Loading.InitLoading(Program.FileSystem.GetDataFolder("In-game"), typeof(NewRenderer).Assembly.GetName().Version.ToString(), Interface.CurrentOptions.LoadingLogo, Interface.CurrentOptions.LoadingProgressBar); if (textureBytes != null && textureBytes.Length > 0) { @@ -70,7 +78,7 @@ internal static void Load(string routeFile, Encoding routeEncoding, byte[] textu CurrentRouteFile = routeFile; CurrentRouteEncoding = routeEncoding; // thread - Loading.LoadAsynchronously(CurrentRouteFile, CurrentRouteEncoding); + Loading.LoadAsynchronously(CurrentRouteFile, CurrentRouteEncoding, startPosition); RouteViewer.LoadingScreenLoop(); } @@ -163,13 +171,14 @@ private static async Task LoadThreaded() } } - internal static void LoadAsynchronously(string RouteFile, Encoding RouteEncoding) + internal static void LoadAsynchronously(string RouteFile, Encoding RouteEncoding, double startPosition = 0) { // members Cancel = false; Complete = false; CurrentRouteFile = RouteFile; CurrentRouteEncoding = RouteEncoding; + currentStartPosition = startPosition; //Set the route and train folders in the info class // ReSharper disable once UnusedVariable @@ -191,6 +200,7 @@ private static void LoadEverythingThreaded() { if (Program.CurrentHost.Plugins[i].Route != null && Program.CurrentHost.Plugins[i].Route.CanLoadRoute(CurrentRouteFile)) { object Route = (object)Program.CurrentRoute; //must cast to allow us to use the ref keyword. + Program.CurrentHost.Plugins[i].Route.StartTrackPosition = currentStartPosition; if (Program.CurrentHost.Plugins[i].Route.LoadRoute(CurrentRouteFile, CurrentRouteEncoding, null, ObjectFolder, SoundFolder, false, ref Route)) { Program.CurrentRoute = (CurrentRoute) Route; diff --git a/source/RouteViewer/Options.cs b/source/RouteViewer/Options.cs index 68bd87573..7a5f97707 100644 --- a/source/RouteViewer/Options.cs +++ b/source/RouteViewer/Options.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.IO; using System.Reflection; @@ -41,6 +41,7 @@ public override void Save(string fileName) Builder.AppendLine("isUseNewRenderer = " + (IsUseNewRenderer ? "true" : "false")); Builder.AppendLine("viewingdistance = " + ViewingDistance); Builder.AppendLine("quadleafsize = " + QuadTreeLeafSize); + Builder.AppendLine("autoReloadObjects = " + (AutoReloadObjects ? "true" : "false")); Builder.AppendLine(); Builder.AppendLine("[quality]"); Builder.AppendLine("interpolation = " + Interpolation); @@ -106,6 +107,7 @@ internal static void LoadOptions() block.GetValue(OptionsKey.IsUseNewRenderer, out Interface.CurrentOptions.IsUseNewRenderer); block.TryGetValue(OptionsKey.ViewingDistance, ref Interface.CurrentOptions.ViewingDistance, NumberRange.Positive); block.TryGetValue(OptionsKey.QuadLeafSize, ref Interface.CurrentOptions.QuadTreeLeafSize, NumberRange.Positive); + block.GetValue(OptionsKey.AutoReloadObjects, out Interface.CurrentOptions.AutoReloadObjects); break; case OptionsSection.Quality: block.GetEnumValue(OptionsKey.Interpolation, out Interface.CurrentOptions.Interpolation); diff --git a/source/RouteViewer/ProgramR.cs b/source/RouteViewer/ProgramR.cs index c5b3bc15c..2b73274ce 100644 --- a/source/RouteViewer/ProgramR.cs +++ b/source/RouteViewer/ProgramR.cs @@ -43,6 +43,10 @@ internal static class Program { internal static string JumpToPositionValue = ""; internal static double MinimumJumpToPositionValue = 0; internal static bool processCommandLineArgs; + /// Last time the route was reloaded (UTC). + internal static DateTime LastReloadTime = DateTime.UtcNow; + private static double reloadCheckTimer; + private static string[] lastRouteFileLines; // keys private static bool ShiftPressed = false; @@ -191,13 +195,14 @@ internal static void Main(string[] args) Renderer.GameWindow.TargetRenderFrequency = 0; Renderer.GameWindow.Title = "Route Viewer"; processCommandLineArgs = true; + LastReloadTime = DateTime.UtcNow; Renderer.GameWindow.Run(); //Unload Sounds.DeInitialize(); } // load route - internal static bool LoadRoute(byte[] textureBytes = null) { + internal static bool LoadRoute(byte[] textureBytes = null, bool autoReload = false, double startPosition = 0) { if (string.IsNullOrEmpty(CurrentRouteFile)) { return false; @@ -207,13 +212,20 @@ internal static bool LoadRoute(byte[] textureBytes = null) { try { Encoding encoding = TextEncoding.GetSystemEncodingFromFile(CurrentRouteFile); - Loading.Load(CurrentRouteFile, encoding, textureBytes); + if (lastRouteFileLines == null) + { + lastRouteFileLines = System.IO.File.ReadAllLines(CurrentRouteFile, encoding); + } + Loading.Load(CurrentRouteFile, encoding, textureBytes, startPosition); result = true; } catch (Exception ex) { - MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Hand); + if (!autoReload) + { + MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Hand); + CurrentRouteFile = null; + } Game.Reset(); result = false; - CurrentRouteFile = null; } if (Loading.Cancel) @@ -255,6 +267,163 @@ internal static bool LoadRoute(byte[] textureBytes = null) { return result; } + /// Called to reload the current route file. + /// Whether this is an automatic reload (triggered by a file change). + /// The starting track position for a selective reload. + internal static void ReloadRoute(bool autoReload = false, double startPosition = 0) + { + if (CurrentRouteFile != null && CurrentlyLoading == false) + { + CurrentlyLoading = true; + lock (Renderer.VisibilityUpdateLock) + { + Renderer.OptionInterface = false; + UpdateCaption(); + byte[] textureBytes = { }; + if (!autoReload && !Interface.CurrentOptions.LoadingBackground) + { + Renderer.RenderScene(0.0); + Renderer.GameWindow.SwapBuffers(); + textureBytes = new byte[Renderer.Screen.Width * Renderer.Screen.Height * 4]; + GL.ReadPixels(0, 0, Renderer.Screen.Width, Renderer.Screen.Height, PixelFormat.Rgba, PixelType.UnsignedByte, textureBytes); + // GL.ReadPixels is reversed for what it wants as a texture, so we've got to flip it + byte[] tmp = new byte[Renderer.Screen.Width * 4]; // temp row + int currentLine = 0; + while (currentLine < Renderer.Screen.Height / 2) + { + int start = currentLine * Renderer.Screen.Width * 4; + int flipStart = (Renderer.Screen.Height - currentLine - 1) * Renderer.Screen.Width * 4; + Buffer.BlockCopy(textureBytes, start, tmp, 0, Renderer.Screen.Width * 4); + Buffer.BlockCopy(textureBytes, flipStart, textureBytes, start, Renderer.Screen.Width * 4); + Buffer.BlockCopy(tmp, 0, textureBytes, flipStart, Renderer.Screen.Width * 4); + currentLine++; + } + } + + // Note: Game.ResetSelective / Game.Reset is called inside Loading.Load, + // so we don't do it here. Doing it here would bypass the animated object cleanup. + CameraAlignment a = Renderer.Camera.Alignment; + + if (LoadRoute(textureBytes, autoReload, startPosition)) + { + Renderer.Camera.Alignment = a; + Renderer.CameraTrackFollower.UpdateAbsolute(-1.0, true, false); + Renderer.CameraTrackFollower.UpdateAbsolute(a.TrackPosition, true, false); + ObjectManager.UpdateAnimatedWorldObjects(0.0, true); + Renderer.InitializeVisibility(); + } + else + { + Renderer.Camera.Reset(CurrentRoute.Tracks[0].Direction == TrackDirection.Reverse); + Renderer.UpdateViewport(ViewportChangeMode.NoChange); + World.UpdateAbsoluteCamera(0.0); + Renderer.UpdateViewingDistances(CurrentRoute.CurrentBackground.BackgroundImageDistance); + } + } + + CurrentlyLoading = false; + Renderer.OptionInterface = true; + System.Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce; + GC.Collect(); + UpdateCaption(); + LastReloadTime = DateTime.UtcNow; + } + } + + /// Checks if the current route file has been updated externally. + internal static void CheckFileChanges(double timeElapsed) + { + if (Interface.CurrentOptions.AutoReloadObjects == false || string.IsNullOrEmpty(CurrentRouteFile) || (reloadCheckTimer += timeElapsed) < 0.1) + { + return; + } + reloadCheckTimer = 0.0; + + try + { + DateTime time = System.IO.File.GetLastWriteTimeUtc(CurrentRouteFile); + if ((DateTime.UtcNow - time).TotalSeconds < 0.05 || time.Year == 1601) + { + return; + } + if (time > LastReloadTime) + { + if (CurrentRouteFile.EndsWith(".csv", StringComparison.InvariantCultureIgnoreCase) || CurrentRouteFile.EndsWith(".rw", StringComparison.InvariantCultureIgnoreCase)) + { + try + { + string[] currentLines = System.IO.File.ReadAllLines(CurrentRouteFile, TextEncoding.GetSystemEncodingFromFile(CurrentRouteFile)); + if (lastRouteFileLines != null && currentLines.Length > 0) + { + int firstChangedLine = -1; + for (int i = 0; i < Math.Min(currentLines.Length, lastRouteFileLines.Length); i++) + { + if (currentLines[i] != lastRouteFileLines[i]) + { + firstChangedLine = i; + break; + } + } + + // find the first line that actually changed + if (firstChangedLine == -1 && currentLines.Length != lastRouteFileLines.Length) + { + firstChangedLine = Math.Min(currentLines.Length, lastRouteFileLines.Length); + } + + if (firstChangedLine != -1) + { + // find the track position at or before the change in both versions. + // we take the minimum of both to avoid "ghost" objects if a track marker was moved forward. + double startPosNew = 0; + for (int i = firstChangedLine; i >= 0; i--) + { + string line = currentLines[i]; + int commentIdx = line.IndexOf(';'); + if (commentIdx != -1) line = line.Substring(0, commentIdx); + string[] parts = line.Split(','); + if (parts.Length > 0 && double.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double p)) + { + startPosNew = p; + break; + } + } + double startPosOld = 0; + for (int i = Math.Min(firstChangedLine, lastRouteFileLines.Length - 1); i >= 0; i--) + { + string line = lastRouteFileLines[i]; + int commentIdx = line.IndexOf(';'); + if (commentIdx != -1) line = line.Substring(0, commentIdx); + string[] parts = line.Split(','); + if (parts.Length > 0 && double.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double p)) + { + startPosOld = p; + break; + } + } + double startPos = Math.Min(startPosNew, startPosOld); + lastRouteFileLines = currentLines; + ReloadRoute(true, startPos); + return; + } + } + lastRouteFileLines = currentLines; + } + catch + { + // fallback + } + } + ReloadRoute(true); + } + } + catch + { + // ignored + } + } + + // jump to station private static void JumpToStation(int Direction) { if (CurrentRoute.Tracks[0].Direction == TrackDirection.Reverse) @@ -298,7 +467,7 @@ internal static void UpdateGraphicsSettings() Renderer.RenderScene(0.0); Renderer.GameWindow.SwapBuffers(); CameraAlignment a = Renderer.Camera.Alignment; - if (LoadRoute()) + if (LoadRoute(null, false)) { Renderer.Camera.Alignment = a; Renderer.CameraTrackFollower.UpdateAbsolute(-1.0, true, false); @@ -447,60 +616,7 @@ internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e) JumpToPositionEnabled = false; JumpToPositionValue = string.Empty; } - byte[] textureBytes = {}; - if (CurrentRouteFile != null && CurrentlyLoading == false) - { - CurrentlyLoading = true; - lock (Renderer.VisibilityUpdateLock) - { - Renderer.OptionInterface = false; - UpdateCaption(); - if (!Interface.CurrentOptions.LoadingBackground) - { - Renderer.RenderScene(0.0); - Renderer.GameWindow.SwapBuffers(); - textureBytes = new byte[Renderer.Screen.Width * Renderer.Screen.Height * 4]; - GL.ReadPixels(0, 0, Renderer.Screen.Width, Renderer.Screen.Height, PixelFormat.Rgba, PixelType.UnsignedByte, textureBytes); - // GL.ReadPixels is reversed for what it wants as a texture, so we've got to flip it - byte[] tmp = new byte[Renderer.Screen.Width * 4]; // temp row - int currentLine = 0; - while (currentLine < Renderer.Screen.Height / 2) - { - int start = currentLine * Renderer.Screen.Width * 4; - int flipStart = (Renderer.Screen.Height - currentLine - 1) * Renderer.Screen.Width * 4; - Buffer.BlockCopy(textureBytes, start, tmp, 0, Renderer.Screen.Width * 4); - Buffer.BlockCopy(textureBytes, flipStart, textureBytes, start, Renderer.Screen.Width * 4); - Buffer.BlockCopy(tmp, 0, textureBytes, flipStart, Renderer.Screen.Width * 4); - currentLine++; - } - } - - Renderer.Reset(); - CameraAlignment a = Renderer.Camera.Alignment; - - if (LoadRoute(textureBytes)) - { - Renderer.Camera.Alignment = a; - Renderer.CameraTrackFollower.UpdateAbsolute(-1.0, true, false); - Renderer.CameraTrackFollower.UpdateAbsolute(a.TrackPosition, true, false); - ObjectManager.UpdateAnimatedWorldObjects(0.0, true); - } - else - { - Renderer.Camera.Reset(CurrentRoute.Tracks[0].Direction == TrackDirection.Reverse); - Renderer.UpdateViewport(ViewportChangeMode.NoChange); - World.UpdateAbsoluteCamera(0.0); - Renderer.UpdateViewingDistances(CurrentRoute.CurrentBackground.BackgroundImageDistance); - } - } - - CurrentlyLoading = false; - Renderer.OptionInterface = true; - GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; - GC.Collect(); - UpdateCaption(); - - } + ReloadRoute(); break; case Key.F7: if (CurrentHost.Platform == HostPlatform.AppleOSX && IntPtr.Size != 4) @@ -541,7 +657,7 @@ internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e) lock (Renderer.VisibilityUpdateLock) { - if (canLoad && LoadRoute()) + if (canLoad && LoadRoute(null, false)) { ObjectManager.UpdateAnimatedWorldObjects(0.0, true); Renderer.Camera.Reset(CurrentRoute.Tracks[0].Direction == TrackDirection.Reverse); diff --git a/source/RouteViewer/System/Gamewindow.cs b/source/RouteViewer/System/Gamewindow.cs index 98cf96270..be0d67d6e 100644 --- a/source/RouteViewer/System/Gamewindow.cs +++ b/source/RouteViewer/System/Gamewindow.cs @@ -1,4 +1,4 @@ -using LibRender2.Viewports; +using LibRender2.Viewports; using OpenBveApi; using OpenBveApi.Hosts; using OpenBveApi.Math; @@ -79,6 +79,7 @@ protected override void OnRenderFrame(FrameEventArgs e) ObjectManager.UpdateAnimatedWorldObjects(TimeElapsed, false); World.UpdateAbsoluteCamera(TimeElapsed); Program.Sounds.Update(TimeElapsed, SoundModels.Linear); + Program.CheckFileChanges(TimeElapsed); } Program.Renderer.Lighting.UpdateLighting(Program.CurrentRoute.SecondsSinceMidnight, Program.CurrentRoute.LightDefinitions); Program.Renderer.RenderScene(TimeElapsed); diff --git a/source/RouteViewer/formOptions.Designer.cs b/source/RouteViewer/formOptions.Designer.cs index 3c5b080dc..456bfbd78 100644 --- a/source/RouteViewer/formOptions.Designer.cs +++ b/source/RouteViewer/formOptions.Designer.cs @@ -57,6 +57,8 @@ private void InitializeComponent() this.comboBoxNewXParser = new System.Windows.Forms.ComboBox(); this.label15 = new System.Windows.Forms.Label(); this.numericUpDownViewingDistance = new System.Windows.Forms.NumericUpDown(); + this.labelAutoReload = new System.Windows.Forms.Label(); + this.checkBoxAutoReload = new System.Windows.Forms.CheckBox(); ((System.ComponentModel.ISupportInitialize)(this.width)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.height)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.AnsiotropicLevel)).BeginInit(); @@ -139,7 +141,7 @@ private void InitializeComponent() // // button1 // - this.button1.Location = new System.Drawing.Point(211, 447); + this.button1.Location = new System.Drawing.Point(211, 470); this.button1.Name = "button1"; this.button1.Size = new System.Drawing.Size(75, 23); this.button1.TabIndex = 9; @@ -307,11 +309,29 @@ private void InitializeComponent() this.checkBoxProgressBar.TabIndex = 23; this.checkBoxProgressBar.UseVisualStyleBackColor = true; // + // labelAutoReload + // + this.labelAutoReload.AutoSize = true; + this.labelAutoReload.Location = new System.Drawing.Point(15, 326); + this.labelAutoReload.Name = "labelAutoReload"; + this.labelAutoReload.Size = new System.Drawing.Size(100, 13); + this.labelAutoReload.TabIndex = 44; + this.labelAutoReload.Text = "Auto Reload Route:"; + // + // checkBoxAutoReload + // + this.checkBoxAutoReload.AutoSize = true; + this.checkBoxAutoReload.Location = new System.Drawing.Point(248, 326); + this.checkBoxAutoReload.Name = "checkBoxAutoReload"; + this.checkBoxAutoReload.Size = new System.Drawing.Size(15, 14); + this.checkBoxAutoReload.TabIndex = 45; + this.checkBoxAutoReload.UseVisualStyleBackColor = true; + // // label13 // this.label13.AutoSize = true; this.label13.Font = new System.Drawing.Font("Microsoft Sans Serif", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label13.Location = new System.Drawing.Point(99, 333); + this.label13.Location = new System.Drawing.Point(99, 356); this.label13.Name = "label13"; this.label13.Size = new System.Drawing.Size(42, 15); this.label13.TabIndex = 26; @@ -320,7 +340,7 @@ private void InitializeComponent() // label14 // this.label14.AutoSize = true; - this.label14.Location = new System.Drawing.Point(16, 377); + this.label14.Location = new System.Drawing.Point(16, 400); this.label14.Name = "label14"; this.label14.Size = new System.Drawing.Size(106, 13); this.label14.TabIndex = 28; @@ -332,7 +352,7 @@ private void InitializeComponent() this.comboBoxNewObjParser.Items.AddRange(new object[] { "OriginalObjParser", "AssimpObjParser"}); - this.comboBoxNewObjParser.Location = new System.Drawing.Point(166, 377); + this.comboBoxNewObjParser.Location = new System.Drawing.Point(166, 400); this.comboBoxNewObjParser.Name = "comboBoxNewObjParser"; this.comboBoxNewObjParser.Size = new System.Drawing.Size(121, 21); this.comboBoxNewObjParser.TabIndex = 27; @@ -340,7 +360,7 @@ private void InitializeComponent() // label12 // this.label12.AutoSize = true; - this.label12.Location = new System.Drawing.Point(16, 348); + this.label12.Location = new System.Drawing.Point(16, 371); this.label12.Name = "label12"; this.label12.Size = new System.Drawing.Size(97, 13); this.label12.TabIndex = 32; @@ -353,7 +373,7 @@ private void InitializeComponent() "OriginalXParser", "NewXParser", "AssimpXParser"}); - this.comboBoxNewXParser.Location = new System.Drawing.Point(166, 348); + this.comboBoxNewXParser.Location = new System.Drawing.Point(166, 371); this.comboBoxNewXParser.Name = "comboBoxNewXParser"; this.comboBoxNewXParser.Size = new System.Drawing.Size(121, 21); this.comboBoxNewXParser.TabIndex = 31; @@ -361,7 +381,7 @@ private void InitializeComponent() // label15 // this.label15.AutoSize = true; - this.label15.Location = new System.Drawing.Point(12, 412); + this.label15.Location = new System.Drawing.Point(12, 435); this.label15.Name = "label15"; this.label15.Size = new System.Drawing.Size(109, 13); this.label15.TabIndex = 34; @@ -369,7 +389,7 @@ private void InitializeComponent() // // numericUpDownViewingDistance // - this.numericUpDownViewingDistance.Location = new System.Drawing.Point(166, 410); + this.numericUpDownViewingDistance.Location = new System.Drawing.Point(166, 433); this.numericUpDownViewingDistance.Maximum = new decimal(new int[] { 4096, 0, @@ -393,7 +413,9 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(311, 482); + this.ClientSize = new System.Drawing.Size(311, 505); + this.Controls.Add(this.labelAutoReload); + this.Controls.Add(this.checkBoxAutoReload); this.Controls.Add(this.label15); this.Controls.Add(this.numericUpDownViewingDistance); this.Controls.Add(this.label12); @@ -469,5 +491,7 @@ private void InitializeComponent() private System.Windows.Forms.ComboBox comboBoxNewXParser; private System.Windows.Forms.Label label15; private System.Windows.Forms.NumericUpDown numericUpDownViewingDistance; + private System.Windows.Forms.Label labelAutoReload; + private System.Windows.Forms.CheckBox checkBoxAutoReload; } } \ No newline at end of file diff --git a/source/RouteViewer/formOptions.cs b/source/RouteViewer/formOptions.cs index f275c31cc..090f554ca 100644 --- a/source/RouteViewer/formOptions.cs +++ b/source/RouteViewer/formOptions.cs @@ -26,6 +26,7 @@ public FormOptions() comboBoxNewXParser.SelectedIndex = (int) Interface.CurrentOptions.CurrentXParser; comboBoxNewObjParser.SelectedIndex = (int) Interface.CurrentOptions.CurrentObjParser; numericUpDownViewingDistance.Value = Math.Min(Interface.CurrentOptions.ViewingDistance, numericUpDownViewingDistance.Minimum); + checkBoxAutoReload.Checked = Interface.CurrentOptions.AutoReloadObjects; } internal static DialogResult ShowOptions() @@ -126,6 +127,7 @@ private void button1_Click(object sender, EventArgs e) } } Interface.CurrentOptions.ViewingDistance = (int)numericUpDownViewingDistance.Value; + Interface.CurrentOptions.AutoReloadObjects = checkBoxAutoReload.Checked; Interface.CurrentOptions.QuadTreeLeafSize = Math.Max(50, (int)Math.Ceiling(Interface.CurrentOptions.ViewingDistance / 10.0d) * 10); // quad tree size set to 10% of viewing distance to the nearest 10 Interface.CurrentOptions.Save(Path.CombineFile(Program.FileSystem.SettingsFolder, "1.5.0/options_rv.cfg")); for (int i = 0; i < Program.CurrentHost.Plugins.Length; i++)