diff --git a/OpenBVE.sln b/OpenBVE.sln index 6d033bc23..e3fb2cacc 100644 --- a/OpenBVE.sln +++ b/OpenBVE.sln @@ -48,6 +48,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RouteViewer", "source\Route ProjectSection(ProjectDependencies) = postProject {06D89847-9C7A-47D5-8C7A-95AEBFFF5F1E} = {06D89847-9C7A-47D5-8C7A-95AEBFFF5F1E} {081F5739-33DA-421A-B177-7B548D96646F} = {081F5739-33DA-421A-B177-7B548D96646F} + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E} = {3655732E-5A4F-4D88-B7C0-25BC3043BF8E} {4B775819-3574-443E-95AD-B40BC6EA6469} = {4B775819-3574-443E-95AD-B40BC6EA6469} {67418D38-1E2E-4944-A1B0-09E00FC2D055} = {67418D38-1E2E-4944-A1B0-09E00FC2D055} {678031C7-6D87-4F5F-9C4A-12F86B76342C} = {678031C7-6D87-4F5F-9C4A-12F86B76342C} @@ -187,6 +188,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KatoInput", "source\InputDe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZuikiInput", "source\InputDevicePlugins\ZuikiInput\ZuikiInput.csproj", "{73974327-132E-45CC-A278-7030DADB6242}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Route.LokSim", "source\Plugins\Route.Loksim\Route.LokSim.csproj", "{3655732E-5A4F-4D88-B7C0-25BC3043BF8E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -956,6 +959,24 @@ Global {73974327-132E-45CC-A278-7030DADB6242}.Release|Mixed Platforms.Build.0 = Release|Any CPU {73974327-132E-45CC-A278-7030DADB6242}.Release|x86.ActiveCfg = Release|Any CPU {73974327-132E-45CC-A278-7030DADB6242}.Release|x86.Build.0 = Release|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Debug|x86.Build.0 = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.GLMenu|Any CPU.ActiveCfg = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.GLMenu|Any CPU.Build.0 = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.GLMenu|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.GLMenu|Mixed Platforms.Build.0 = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.GLMenu|x86.ActiveCfg = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.GLMenu|x86.Build.0 = Debug|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Release|Any CPU.Build.0 = Release|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Release|x86.ActiveCfg = Release|Any CPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -995,6 +1016,7 @@ Global {C4BE7A1F-9CCD-4E78-8341-741ABDA8E026} = {D75531B7-000E-432E-A168-51846256A9D1} {7ED7B285-FAE6-4B34-ACC5-87399F27C8BC} = {16553295-E70F-4596-AA78-848EEA576C4A} {BA9949E2-47BE-4308-AD66-1314853FDC49} = {851ADF1E-4500-4A14-B4AA-C1087A6C537E} + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E} = {D75531B7-000E-432E-A168-51846256A9D1} {73974327-132E-45CC-A278-7030DADB6242} = {851ADF1E-4500-4A14-B4AA-C1087A6C537E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/assets/Compatibility/Loksim/Grass.csv b/assets/Compatibility/Loksim/Grass.csv new file mode 100644 index 000000000..7b858509f --- /dev/null +++ b/assets/Compatibility/Loksim/Grass.csv @@ -0,0 +1,13 @@ +CreateMeshBuilder,,, +AddVertex,-450,0,10 +AddVertex,-450,-0.01,0 +AddVertex,450,-0.01,0 +AddVertex,450,-0.01,10 +AddFace,3,2,1,0 + +LoadTexture,Grass.png,, +SetTextureCoordinates,0,0,0 +SetTextureCoordinates,1,1,0 +SetTextureCoordinates,2,1,30 +SetTextureCoordinates,3,0,30 + diff --git a/assets/Compatibility/Loksim/grass.png b/assets/Compatibility/Loksim/grass.png new file mode 100644 index 000000000..e1ac5abf5 Binary files /dev/null and b/assets/Compatibility/Loksim/grass.png differ diff --git a/source/LibRender2/BaseRenderer.cs b/source/LibRender2/BaseRenderer.cs index 3a4c19f93..9040541d8 100644 --- a/source/LibRender2/BaseRenderer.cs +++ b/source/LibRender2/BaseRenderer.cs @@ -1378,6 +1378,11 @@ public void RenderFace(Shader shader, ObjectState state, MeshFace face, bool deb break; } + if ((face.Flags & FaceFlags.DisableDepthWrite) != 0) + { + GL.DepthMask(false); + } + // blend factor float distanceFactor; if (material.GlowAttenuationData != 0) @@ -1516,6 +1521,7 @@ public void RenderFace(Shader shader, ObjectState state, MeshFace face, bool deb GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill); } lastObjectState = state; + GL.DepthMask(true); } /// Sets the current MouseCursor diff --git a/source/LibRender2/Objects/ObjectLibrary.cs b/source/LibRender2/Objects/ObjectLibrary.cs index 7c3b389c6..7358bbfbf 100644 --- a/source/LibRender2/Objects/ObjectLibrary.cs +++ b/source/LibRender2/Objects/ObjectLibrary.cs @@ -58,7 +58,7 @@ private void RemoveObject(ObjectState state) { lock (LockObject) { - if (Objects.TryRemove(state, out _)) + if (state != null && Objects.TryRemove(state, out _)) { myOpaqueFaces.RemoveAll(x => x.Object == state); myAlphaFaces.RemoveAll(x => x.Object == state); diff --git a/source/OpenBVE/UserInterface/formMain.Start.cs b/source/OpenBVE/UserInterface/formMain.Start.cs index 8d9aa06c8..103cf3f9f 100644 --- a/source/OpenBVE/UserInterface/formMain.Start.cs +++ b/source/OpenBVE/UserInterface/formMain.Start.cs @@ -310,6 +310,27 @@ private void PopulateRouteList(string routeFolder, ListView listView, bool packa } } break; + case ".l3dfpl": + fileName = System.IO.Path.GetFileName(Files[i]); + if (fileName == null) + { + continue; + } + + if (!Program.CurrentHost.LoadPlugins(Program.FileSystem, Interface.CurrentOptions, out _, Program.TrainManager, Program.Renderer)) + { + throw new Exception("Unable to load the required plugins- Please reinstall OpenBVE"); + } + for (int j = 0; j < Program.CurrentHost.Plugins.Length; j++) + { + if (Program.CurrentHost.Plugins[j].Route != null && Program.CurrentHost.Plugins[j].Route.CanLoadRoute(Files[i])) + { + Item = listView.Items.Add(fileName); + Item.ImageKey = @"loksim"; + Item.Tag = Files[i]; + } + } + break; } } } diff --git a/source/OpenBveApi/Objects/FaceType.cs b/source/OpenBveApi/Objects/FaceType.cs index d1da1040e..5d55ed811 100644 --- a/source/OpenBveApi/Objects/FaceType.cs +++ b/source/OpenBveApi/Objects/FaceType.cs @@ -22,6 +22,7 @@ public enum FaceFlags : byte /// The mask used for unidirectional Face commands FaceTypeMask = 7, /// The mask used for bidirectional Face2 commands - Face2Mask = 8 + Face2Mask = 8, + DisableDepthWrite = 16, } } diff --git a/source/OpenBveApi/Objects/Helpers/MeshBuilder.cs b/source/OpenBveApi/Objects/Helpers/MeshBuilder.cs index af5878624..061e358f3 100644 --- a/source/OpenBveApi/Objects/Helpers/MeshBuilder.cs +++ b/source/OpenBveApi/Objects/Helpers/MeshBuilder.cs @@ -95,7 +95,8 @@ public void Apply(ref StaticObject Object, bool EnableHacks = false, bool Ignore if (Materials[i].TransparencyTexture != null) { currentHost.LoadTexture(Materials[i].TransparencyTexture, new TextureParameters(null, null), out transparency); - currentHost.LoadTexture(ref transparency, OpenGlTextureWrapMode.ClampClamp); + // FIXME: CRASHES SOMETIMES + //currentHost.LoadTexture(ref transparency, OpenGlTextureWrapMode.ClampClamp); } TextureParameters parameters; diff --git a/source/OpenBveApi/Routes/QuadTree/QuadNode.Leaf.cs b/source/OpenBveApi/Routes/QuadTree/QuadNode.Leaf.cs index 3d412f7cd..ba12c58ba 100644 --- a/source/OpenBveApi/Routes/QuadTree/QuadNode.Leaf.cs +++ b/source/OpenBveApi/Routes/QuadTree/QuadNode.Leaf.cs @@ -227,6 +227,10 @@ internal void UpdateBoundingRectangle(ObjectState objectState, Vector3 quadPosit Vector3 absolutePosition = quadCenter + quadPosition; for (int i = 0; i < objectState.Prototype.Mesh.Vertices.Length; i++) { + if (objectState.Prototype.Mesh.Vertices[i] == null) + { + return; + } Vector3 vector = absolutePosition + Vector3.Rotate(objectState.Prototype.Mesh.Vertices[i].Coordinates, quadOrientation); if (vector.X < BoundingRectangle.Left) { diff --git a/source/OpenBveApi/System/Path.Loksim3D.cs b/source/OpenBveApi/System/Path.Loksim3D.cs index 9f79ada60..76cb7ad68 100644 --- a/source/OpenBveApi/System/Path.Loksim3D.cs +++ b/source/OpenBveApi/System/Path.Loksim3D.cs @@ -18,6 +18,7 @@ public static class Loksim3D /// public static string CombineFile(string absolute, string relative, string packageDirectory) { + relative = relative.TrimStart('\\'); try { string file = Path.CombineFile(absolute, relative); diff --git a/source/Plugins/Formats.OpenBve/Block.cs b/source/Plugins/Formats.OpenBve/Block.cs index 377b635c5..c9bc0f18c 100644 --- a/source/Plugins/Formats.OpenBve/Block.cs +++ b/source/Plugins/Formats.OpenBve/Block.cs @@ -750,6 +750,20 @@ public virtual bool GetEnumValue(T2 key, out T3 enumValue, out int index, ou return false; } + /// Unconditionally gets the next value as a string + public virtual void GetNextValue(out T2 valueType, out string value) + { + if (keyValuePairs.Count > 0) + { + valueType = keyValuePairs.Keys.First(); + + keyValuePairs.TryRemove(valueType, out KeyValuePair v); + value = v.Value; + } + valueType = default; + value = string.Empty; + } + /// Reads the specified Enum value from the block public virtual bool GetEnumValue(T2 key, out T3 enumValue, out Color32 color) where T3 : struct, Enum { diff --git a/source/Plugins/Formats.OpenBve/Enums/Loksim/LoksimAttribute.cs b/source/Plugins/Formats.OpenBve/Enums/Loksim/LoksimAttribute.cs new file mode 100644 index 000000000..92861af7f --- /dev/null +++ b/source/Plugins/Formats.OpenBve/Enums/Loksim/LoksimAttribute.cs @@ -0,0 +1,115 @@ +namespace Formats.OpenBve +{ + public enum LoksimAttribute + { + Unknown = 0, + // Common attributes + FileAuthor, + FileInfo, + FilePicture, + FileDoku, + Name, + File, + Value, + // Kurbuchstrecken only + KBS, + BremsArt, + BremsStellung, + WetterDatei, + Zeiten, + ZugArt, + ZugGewicht, + ZugLaenge, + ZugLimit, + // StartPunkt only + Gleis, + Pos, + Richtung, + Strecke, + // Station only + Abfahrt, + Ankunft, + Bedarfshalt, + Betriebshalt, + Haltdauer, + Haltepunkt, + DistanzSoundVorBedarfsHalt, + DistanzSoundVorHalt, + SignalVorAusfahrt, + SignalVorEinfahrt, + SoundAbfahrt, + SoundAnkunft, + SoundAnsage, + SoundHalt, + SoundVorBedarfsHalt, + SoundVorHalt, + Tueren, + Zugfolgestelle, + // Connection + Ende1, + Ende2, + Gleis1, + Gleis2, + Strecke1, + Strecke2, + // Eintrag + Abstand, + Anzahl, + Datei, + GleisHoeheFolgen, + HoeheLand, + Position, + Verschiebung, + WeitSichtbar, + Winkel, + // Strecke + StartPunkt, + EndPunkt, + ParallelGleis, + ParallelStart, + ParallelEnd, + ParallelAbstand, + StartPos, + Typ, + WeicheEnd, + WeicheGleis, + WeicheGleisPos, + Offset, + Qualitaet, + RollMaterial, + DynamicVisibility, + FixedDynamicVisibility, + StreckenAbhaengigkeit, + KILOMETER, + DayLight, + NightLight, + LokLight, + SRAND, + XValue, + YValue, + ZValue, + Polygon, + Scale, + TextureFile, + Texture, + Zwischenpunkte, + Hoehe, + Hoehefrei, + // KURVE + Start, + Ende, + SanfterWinkel, + // STEIGUNG + Promille, + //L3DRail + Transparent, + Spurbreite, + Schienenbreite, + Schienenhoehe, + Bettungsbreite, + x1, + x2, + y1, + y2, + } +} diff --git a/source/Plugins/Formats.OpenBve/Enums/Loksim/LoksimNode.cs b/source/Plugins/Formats.OpenBve/Enums/Loksim/LoksimNode.cs new file mode 100644 index 000000000..0b2646ac4 --- /dev/null +++ b/source/Plugins/Formats.OpenBve/Enums/Loksim/LoksimNode.cs @@ -0,0 +1,46 @@ +namespace Formats.OpenBve +{ + public enum LoksimNode + { + // Common + Props, + Random, + Name, + // Fahrplan + PneuBremse, + MgBremse, + Haltestellen, + Halt, + // Kursbuchstrecken + Strecken, + Strecke, + Verbindung, + StartPunkt, + WeichenStellung, + // Strecke + DefaulRail, + StdTexture, + Himmelsrichtung, + GLEIS, + TOPOLOGIE, + Erstellung, + KURVE, + STEIGUNG, + Weiche, + Kreuzung, + Ausweiche, + Landschaft, + Landschaftsobjekt, + // Objecte + Objecte, + Object, + Eintrag, + Eigenschaften, + Lightning, + // L3DRail + Hoehe, + TexSchieneOben, + TexSchieneSeite, + TexBettung + } +} diff --git a/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj b/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj index 84cdf01bc..f7bd7df72 100644 --- a/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj +++ b/source/Plugins/Formats.OpenBve/Formats.OpenBve.csproj @@ -64,6 +64,8 @@ + + @@ -88,6 +90,7 @@ + diff --git a/source/Plugins/Formats.OpenBve/XML/XMLFileAttributes.cs b/source/Plugins/Formats.OpenBve/XML/XMLFileAttributes.cs new file mode 100644 index 000000000..146d404ef --- /dev/null +++ b/source/Plugins/Formats.OpenBve/XML/XMLFileAttributes.cs @@ -0,0 +1,238 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2025, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using OpenBveApi.Hosts; +using OpenBveApi.Interface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; +using Path = OpenBveApi.Path; + + +namespace Formats.OpenBve +{ + /// Root block for an XML file using attributes to contain values + public class AttributedXMLFile : Block where T1 : struct, Enum where T2 : struct, Enum + { + public AttributedXMLFile(string fileName, string rootPath, HostInterface currentHost) : this(XDocument.Load(fileName, LoadOptions.SetLineInfo), fileName, rootPath, currentHost) + { + } + + public AttributedXMLFile(string text, string fileName, string rootPath, HostInterface currentHost) : this(XDocument.Parse(text), fileName, rootPath, currentHost) + { + } + + public AttributedXMLFile(XDocument currentXML, string fileName, string rootPath, HostInterface currentHost) : + base(-1, default, fileName, currentHost) + { + IEnumerable DocumentNodes = currentXML.XPathSelectElements(rootPath).ToList(); + if (DocumentNodes.Any()) + { + foreach (XElement rootNode in DocumentNodes) + { + foreach (XElement element in rootNode.Elements()) + { + if (element.Name.LocalName.Equals("Props", StringComparison.InvariantCultureIgnoreCase)) + { + foreach (XAttribute attribute in element.Attributes()) + { + if (Enum.TryParse(attribute.Name.LocalName, true, out T2 at)) + { + keyValuePairs.TryAdd(at, new KeyValuePair(-1, attribute.Value)); + } + } + continue; + } + if (Enum.TryParse(element.Name.LocalName, true, out T1 key)) + { + subBlocks.Add(new AttributedXMLSection(fileName, element, key, currentHost)); + } + + } + } + } + } + + public override bool GetPath(T2 key, string absolutePath, out string finalPath) + { + if (keyValuePairs.TryRemove(key, out var value)) + { + if (!Path.ContainsInvalidChars(value.Value)) + { + + string relativePath = value.Value; + if (string.IsNullOrEmpty(relativePath)) + { + finalPath = string.Empty; + return false; + } + + if (Path.IsAbsolutePath(relativePath)) + { + relativePath = relativePath.TrimStart('/', '\\'); + } + + try + { + finalPath = Path.CombineFile(absolutePath, relativePath); + } + catch + { + finalPath = string.Empty; + } + + if (File.Exists(finalPath)) + { + return true; + } + + try + { + finalPath = Path.CombineFile(absolutePath, relativePath); + } + catch + { + finalPath = string.Empty; + return false; + } + + if (File.Exists(finalPath)) + { + return true; + } + + currentHost.AddMessage(MessageType.Warning, false, "File " + value.Value + " was not found in Key " + key + " in Section " + Key + " at line " + value.Key); + finalPath = string.Empty; + return false; + + } + + currentHost.AddMessage(MessageType.Warning, false, "Path contains invalid characters for " + key + " in Section " + Key + " at line " + value.Key); + } + finalPath = string.Empty; + return false; + } + } + + public class AttributedXMLSection : Block where T1 : struct, Enum where T2 : struct, Enum + { + public AttributedXMLSection(string fileName, XElement element, T1 myKey, HostInterface host) : base(-1, myKey, fileName, host) + { + if (element.HasAttributes) + { + foreach (XAttribute attribute in element.Attributes()) + { + if (Enum.TryParse(attribute.Name.LocalName, true, out T2 at)) + { + keyValuePairs.TryAdd(at, new KeyValuePair(-1, attribute.Value)); + } + } + } + else if (element.HasElements) + { + foreach (XElement childElement in element.Elements()) + { + if (childElement.Name.LocalName.Equals("Props", StringComparison.InvariantCultureIgnoreCase)) + { + foreach (XAttribute attribute in childElement.Attributes()) + { + if (Enum.TryParse(attribute.Name.LocalName, true, out T2 at)) + { + keyValuePairs.TryAdd(at, new KeyValuePair(-1, attribute.Value)); + } + } + continue; + } + if (Enum.TryParse(childElement.Name.LocalName, true, out T1 key)) + { + subBlocks.Add(new AttributedXMLSection(fileName, childElement, key, host)); + } + } + } + } + + public override bool GetPath(T2 key, string absolutePath, out string finalPath) + { + if (keyValuePairs.TryRemove(key, out var value)) + { + if (!Path.ContainsInvalidChars(value.Value)) + { + + string relativePath = value.Value; + if (string.IsNullOrEmpty(relativePath)) + { + finalPath = string.Empty; + return false; + } + + if (Path.IsAbsolutePath(relativePath)) + { + relativePath = relativePath.TrimStart('/', '\\'); + } + + try + { + finalPath = Path.CombineFile(absolutePath, relativePath); + } + catch + { + finalPath = string.Empty; + } + + if (File.Exists(finalPath)) + { + return true; + } + + try + { + finalPath = Path.CombineFile(absolutePath, relativePath); + } + catch + { + finalPath = string.Empty; + return false; + } + + if (File.Exists(finalPath)) + { + return true; + } + + currentHost.AddMessage(MessageType.Warning, false, "File " + value.Value + " was not found in Key " + key + " in Section " + Key + " at line " + value.Key); + finalPath = string.Empty; + return false; + + } + + currentHost.AddMessage(MessageType.Warning, false, "Path contains invalid characters for " + key + " in Section " + Key + " at line " + value.Key); + } + finalPath = string.Empty; + return false; + } + } +} diff --git a/source/Plugins/Object.LokSim/Plugin.cs b/source/Plugins/Object.LokSim/Plugin.cs index 8983449a7..a3dd49367 100644 --- a/source/Plugins/Object.LokSim/Plugin.cs +++ b/source/Plugins/Object.LokSim/Plugin.cs @@ -1,4 +1,4 @@ -//Simplified BSD License (BSD-2-Clause) +//Simplified BSD License (BSD-2-Clause) // //Copyright (c) 2020, Christopher Lees, The OpenBVE Project // @@ -79,8 +79,8 @@ public override bool LoadObject(string path, Encoding textEncoding, out UnifiedO new AnimatedObject(Plugin.currentHost, (StaticObject)unifiedObject) }, }; - aoc.Objects[0].RotateXFunction = new FunctionScript(currentHost, "billboardx", true); - aoc.Objects[0].RotateXDirection = Vector3.Down; + aoc.Objects[0].RotateYFunction = new FunctionScript(currentHost, "billboardy * 0.5", true); + aoc.Objects[0].RotateYDirection = Vector3.Down; unifiedObject = aoc; } } diff --git a/source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs b/source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs new file mode 100644 index 000000000..2c14ad86e --- /dev/null +++ b/source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs @@ -0,0 +1,88 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.Collections.Generic; +using System.Xml; +using Formats.OpenBve; +using OpenBveApi; +using OpenBveApi.Interface; + +namespace LokSimRouteParser +{ + internal class Fahrplan + { + /// The author of this file + internal string FileAuthor; + /// The info text to display + internal string FileInfo; + /// The image file for this Fahrplan + internal string FilePicture; + /// The route data file for this Fahrplan + internal string RouteFile; + /// The stations list for this Fahrplan + internal List Stations; + /// The actual route data for this Fahrplan + private Kursbuchstrecken Kursbuchstrecken; + + internal Fahrplan(string fileName) + { + XmlDocument currentXML = new XmlDocument(); + try + { + currentXML.Load(fileName); + } + catch + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Failed to load Fahrplan " + fileName); + throw; + } + + AttributedXMLFile fahrplanFile = new AttributedXMLFile(fileName, "/FPL", Plugin.CurrentHost); + fahrplanFile.GetValue(LoksimAttribute.FileAuthor, out FileAuthor); + fahrplanFile.GetValue(LoksimAttribute.FileInfo, out FileInfo); + fahrplanFile.GetPath(LoksimAttribute.FilePicture, Path.GetDirectoryName(fileName), out FilePicture); + fahrplanFile.GetPath(LoksimAttribute.KBS, Plugin.FileSystem.LoksimDataDirectory, out RouteFile); + // BremsArt : brake type, PZB related (?) + // BremsStellung : brake position, presumably at start of game + // Zeiten : list of times + // ZugArt : train type + // ZugGewicht : weight of train + // ZugLaenge : length of train + // ZugLimit : max speed for train + + // PneuBremse and MgBremse blocks : unsupported brake properties at the minute + + Stations = new List(); + fahrplanFile.ReadBlock(LoksimNode.Haltestellen, out Block haltestellenBlock); + while (haltestellenBlock.RemainingSubBlocks > 0) + { + Block stationBlock = haltestellenBlock.ReadNextBlock(); + Stations.Add(new Station(stationBlock)); + } + + Kursbuchstrecken = new Kursbuchstrecken(RouteFile); + Kursbuchstrecken.CreateWorld(); + } + } +} diff --git a/source/Plugins/Route.Loksim/Fahrplan/Station.cs b/source/Plugins/Route.Loksim/Fahrplan/Station.cs new file mode 100644 index 000000000..c51ca9349 --- /dev/null +++ b/source/Plugins/Route.Loksim/Fahrplan/Station.cs @@ -0,0 +1,85 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using Formats.OpenBve; + +namespace LokSimRouteParser +{ + internal class Station + { + /// The arrival time + internal int Arrival; + /// The departure time + internal int Departure; + /// Whether this is a stop point + internal bool ShouldStop; + /// Whether this is a normal stop + internal bool NormalStop; + /// Whether this is a request stop + internal bool RequestStop; + /// The minimum time stopped + internal int StopTime; + /// The station name + internal string Name; + /// Whether the station forces a red signal until departure + internal bool ForcedRedSignal; + /// The distance at which the pre-station stop alarm is sounded + internal int StopAlarmDistance; + /// The departure sound + internal string DepartureSound; + /// The arrival sound + internal string ArrivalSound; + /// Whether the doors are to open + internal bool OpenDoors; + + internal Station(Block stationBlock) + { + stationBlock.GetValue(LoksimAttribute.Abfahrt, out Arrival); + stationBlock.GetValue(LoksimAttribute.Ankunft, out Departure); + stationBlock.GetValue(LoksimAttribute.Haltepunkt, out ShouldStop); + stationBlock.GetValue(LoksimAttribute.Bedarfshalt, out RequestStop); + stationBlock.GetValue(LoksimAttribute.Betriebshalt, out NormalStop); + stationBlock.GetValue(LoksimAttribute.SignalVorAusfahrt, out ForcedRedSignal); + stationBlock.GetValue(LoksimAttribute.Haltdauer, out StopTime); + stationBlock.GetValue(LoksimAttribute.Name, out Name); + stationBlock.GetValue(LoksimAttribute.DistanzSoundVorBedarfsHalt, out StopAlarmDistance); + stationBlock.GetPath(LoksimAttribute.SoundAnkunft, Plugin.FileSystem.LoksimDataDirectory, out ArrivalSound); + stationBlock.GetPath(LoksimAttribute.SoundAbfahrt, Plugin.FileSystem.LoksimDataDirectory, out DepartureSound); + /* + * SoundAnsage : announcement + * SoundHalt : stop + * SoundVorBedarfsHalt : stop alarm sound + * Zugfolgestelle: + * Train Following Point: + * ---------------------- + * + * Section of track ahead may only be released if it is free of vehicles and not in use by train + * in opposite direction + * + * PZB related, probably interacts with the forced red signal. + */ + } + } +} + diff --git a/source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.cs b/source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.cs new file mode 100644 index 000000000..e9b9f374b --- /dev/null +++ b/source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.cs @@ -0,0 +1,55 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using Formats.OpenBve; + +namespace LokSimRouteParser +{ + internal class Connection + { + internal bool End1; + internal bool End2; + internal string Platform1; + internal string Platform2; + internal string RouteFile1; + internal string RouteFile2; + internal Connection(Block connectionBlock, Kursbuchstrecken kursbuchstrecken) + { + connectionBlock.GetValue(LoksimAttribute.Ende1, out End1); + connectionBlock.GetValue(LoksimAttribute.Ende2, out End2); + connectionBlock.GetValue(LoksimAttribute.Gleis1, out Platform1); + connectionBlock.GetValue(LoksimAttribute.Gleis2, out Platform2); + if (!connectionBlock.GetValue(LoksimAttribute.Strecke1, out RouteFile1) || !kursbuchstrecken.Strecken.ContainsKey(RouteFile1)) + { + throw new Exception("Loksim3D: Strecke1 was missing from the Kursbuchstecken"); + } + + if (!connectionBlock.GetValue(LoksimAttribute.Strecke2, out RouteFile2) || !kursbuchstrecken.Strecken.ContainsKey(RouteFile2)) + { + throw new Exception("Loksim3D: Strecke2 was missing from the Kursbuchstecken"); + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs b/source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs new file mode 100644 index 000000000..ebefa6d5e --- /dev/null +++ b/source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs @@ -0,0 +1,105 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.Collections.Generic; +using Formats.OpenBve; +using OpenBveApi.Interface; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Kursbuchstrecken + { + /// The list of connections between routefiles + internal List Connections; + /// The list of routefiles + internal Dictionary Strecken; + /// The starting point + internal StartPunkt StartPoint; + + internal Kursbuchstrecken(string fileName) + { + AttributedXMLFile kursbuchstreckenFile = new AttributedXMLFile(fileName, "/KBS", Plugin.CurrentHost); + Strecken = new Dictionary(); + while (kursbuchstreckenFile.RemainingSubBlocks > 0) + { + Block subBlock = kursbuchstreckenFile.ReadNextBlock(); + switch (subBlock.Key) + { + case LoksimNode.Props: + // Present, but appear to contain nothing useful to us at the moment + break; + case LoksimNode.Verbindung: + case LoksimNode.Strecken: + if (subBlock.RemainingSubBlocks == 0) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Empty list of " + subBlock.Key + " in Kursbuchstrecken " + fileName); + break; + } + + while (subBlock.RemainingSubBlocks > 0) + { + Block nextBlock = subBlock.ReadNextBlock(); + if (nextBlock.Key == LoksimNode.Strecke) + { + ParseStrecken(nextBlock); + } + else + { + ParseVerbindung(nextBlock); + } + } + break; + case LoksimNode.StartPunkt: + StartPoint = new StartPunkt(subBlock); + break; + } + } + } + + private void ParseStrecken(Block streckenBlock) + { + if (streckenBlock.GetPath(LoksimAttribute.Name, Plugin.FileSystem.LoksimDataDirectory, out string streckeFile)) + { + Strecke strecke = new Strecke(streckeFile); + Strecken.Add(streckeFile.Replace(Plugin.FileSystem.LoksimDataDirectory, string.Empty), strecke); + } + } + + private void ParseVerbindung(Block verbindungBlock) + { + Connections = new List(); + Connections.Add(new Connection(verbindungBlock, this)); + } + + internal void CreateWorld() + { + // For the minute, just create the first strecken and see what explodes horribly + Strecke startingStrecke = Strecken[StartPoint.RouteFile]; + + Vector3 startingWpos = Vector3.Zero; + startingStrecke.Create(ref startingWpos); // vector starting point of world tile + } + } +} diff --git a/source/Plugins/Route.Loksim/Plugin.cs b/source/Plugins/Route.Loksim/Plugin.cs new file mode 100644 index 000000000..3f24b8c38 --- /dev/null +++ b/source/Plugins/Route.Loksim/Plugin.cs @@ -0,0 +1,103 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Collections.Generic; +using System.Text; +using OpenBveApi; +using OpenBveApi.FileSystem; +using OpenBveApi.Hosts; +using OpenBveApi.Interface; +using OpenBveApi.Objects; +using OpenBveApi.Routes; +using RouteManager2; +using RouteManager2.Stations; + +namespace LokSimRouteParser +{ + public class Plugin : RouteInterface + { + internal static HostInterface CurrentHost; + + internal static CurrentRoute CurrentRoute; + + internal static FileSystem FileSystem; + + internal static Dictionary ObjectCache; + + internal static BaseOptions BaseOptions; + + internal static bool previewOnly; + + internal static Dictionary TrackKeys; + + /// Called when the plugin is loaded. + /// The host that loaded the plugin. + /// + /// + /// + public override void Load(HostInterface host, FileSystem fileSystem, BaseOptions Options, object rendererReference) + { + CurrentHost = host; + FileSystem = fileSystem; + ObjectCache = new Dictionary(); + BaseOptions = Options; + } + + public override bool CanLoadRoute(string path) + { + return path.ToLowerInvariant().EndsWith(".l3dfpl"); + } + + public override bool LoadRoute(string path, Encoding Encoding, string trainPath, string objectPath, string soundPath, bool PreviewOnly, ref object route) + { + LastException = null; + Cancel = false; + CurrentProgress = 0.0; + IsLoading = true; + FileSystem.AppendToLogFile("Loading route file: " + path); + CurrentRoute = (CurrentRoute)route; + CurrentRoute.Stations = new RouteStation[] { }; + FileSystem.AppendToLogFile("Route file format is: LokSim3D"); + previewOnly = PreviewOnly; + BaseOptions.ObjectDisposalMode = ObjectDisposalMode.QuadTree; + BaseOptions.ViewingDistance = 10000; + TrackKeys = new Dictionary(); + try + { + Fahrplan Fahrplan = new Fahrplan(path); + IsLoading = false; + return true; + } + catch(Exception ex) + { + route = null; + CurrentHost.AddMessage(MessageType.Error, false, "An unexpected error occured whilst attempting to load the following LokSim3D routefile: " + path); + IsLoading = false; + LastException = ex; + return false; + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Properties/AssemblyInfo.cs b/source/Plugins/Route.Loksim/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..629fb729a --- /dev/null +++ b/source/Plugins/Route.Loksim/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Route.Loksim")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Route.Loksim")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3655732e-5a4f-4d88-b7c0-25bc3043bf8e")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/source/Plugins/Route.Loksim/Rails/L3DRail.cs b/source/Plugins/Route.Loksim/Rails/L3DRail.cs new file mode 100644 index 000000000..2a83f8140 --- /dev/null +++ b/source/Plugins/Route.Loksim/Rails/L3DRail.cs @@ -0,0 +1,260 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using OpenBveApi.Interface; +using System; +using System.Collections.Generic; +using System.Xml; +using Formats.OpenBve; +using OpenBveApi; +using OpenBveApi.Math; +using OpenBveApi.Objects; +using OpenBveApi.Textures; + +namespace LokSimRouteParser +{ + internal class L3DRail + { + internal double Gauge; + + internal double RailTopWidth; + + internal double RailTopHeight; + + internal double BallastWidth; + + internal bool UsesTransparentColor; + + internal List BallastPoints = new List(); + + internal StaticObject Object; + + internal Vector2[] railTopTextureCoordinates = new Vector2[2]; + + internal Vector2[] railSideTextureCoordinates = new Vector2[2]; + + internal Vector2[] ballastTextureCoordinates = new Vector2[2]; + + internal Vector2 textureSize; + + internal string textureFile; + + internal L3DRail(string fileName) + { + // https://eu07.pl/forum/showthread.php?tid=6244&page=2 + // rail is generated in 10m lengths + // follows that the texture co-ordinates must be relative to said 10m + // unsure as yet as to how switches are generated + // must do some twiddling to sort for radius + // It might be easier if we just generate a ~0.5m segment and use this?? + + + AttributedXMLFile railFile = new AttributedXMLFile(fileName, "/RAIL", Plugin.CurrentHost); + + railFile.GetValue(LoksimAttribute.Transparent, out UsesTransparentColor); + railFile.GetValue(LoksimAttribute.Spurbreite, out Gauge); + Gauge /= 2; + railFile.GetValue(LoksimAttribute.Schienenbreite, out RailTopWidth); + railFile.GetValue(LoksimAttribute.Schienenhoehe, out RailTopHeight); + railFile.GetValue(LoksimAttribute.Bettungsbreite, out BallastWidth); + if (railFile.GetPath(LoksimAttribute.Texture, Path.GetDirectoryName(fileName), out textureFile)) + { + Plugin.CurrentHost.QueryTextureDimensions(textureFile, out int tx, out int ty); + textureSize.X = tx; + textureSize.Y = ty; + } + + while (railFile.RemainingSubBlocks > 0) + { + Block subBlock = railFile.ReadNextBlock(); + switch (subBlock.Key) + { + case LoksimNode.Hoehe: + subBlock.GetVector3(LoksimAttribute.Hoehe, ';', out Vector3 v); + BallastPoints.Add(v); + break; + case LoksimNode.TexSchieneOben: + subBlock.GetValue(LoksimAttribute.x1, out railTopTextureCoordinates[0].X); + subBlock.GetValue(LoksimAttribute.x2, out railTopTextureCoordinates[1].X); + subBlock.GetValue(LoksimAttribute.y1, out railTopTextureCoordinates[0].Y); + subBlock.GetValue(LoksimAttribute.y2, out railTopTextureCoordinates[1].Y); + break; + case LoksimNode.TexSchieneSeite: + subBlock.GetValue(LoksimAttribute.x1, out railSideTextureCoordinates[0].X); + subBlock.GetValue(LoksimAttribute.x2, out railSideTextureCoordinates[1].X); + subBlock.GetValue(LoksimAttribute.y1, out railSideTextureCoordinates[0].Y); + subBlock.GetValue(LoksimAttribute.y2, out railSideTextureCoordinates[1].Y); + break; + case LoksimNode.TexBettung: + subBlock.GetValue(LoksimAttribute.x1, out ballastTextureCoordinates[0].X); + subBlock.GetValue(LoksimAttribute.x2, out ballastTextureCoordinates[1].X); + subBlock.GetValue(LoksimAttribute.y1, out ballastTextureCoordinates[0].Y); + subBlock.GetValue(LoksimAttribute.y2, out ballastTextureCoordinates[1].Y); + break; + } + } + + Object = new StaticObject(Plugin.CurrentHost); + + MeshBuilder builder = new MeshBuilder(Plugin.CurrentHost); + // create material to be used + Material material = new Material(textureFile); + builder.Materials[0] = material; + + // build rail models + // NOTE: This can definitely be optimised, but not doing that at the minute + + // rail side L + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y))); + + // rail top L + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y))); + MeshFace face = new MeshFace(new[] { 0, 1, 2, 3 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + face = new MeshFace(new[] { 4, 5, 6, 7 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + builder.Apply(ref Object); + builder = new MeshBuilder(Plugin.CurrentHost); + builder.Materials[0] = material; + // rail side R + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y))); + // rail top R + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y))); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y))); + face = new MeshFace(new[] { 0, 1, 2, 3 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + face = new MeshFace(new[] { 4, 5, 6, 7 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + + builder.Apply(ref Object); + builder = new MeshBuilder(Plugin.CurrentHost); + builder.Materials[0] = material; + + // build ballast model: + // points are those of *half* an end profile in clockwise direction + // e.g. + // *-----* + // \ + // \ + // * + // | + // | + // * + // we then negate the X co-ordinate to build the other side + // TexBettung appears to be the rectangle across the whole cross-section + // current assumption is that our X-coords are proportional to the distance in X axis + + List ballast = new List(); + int[] sleeperBase = { 0,1,0,0 }; + + ballastTextureCoordinates[0] /= textureSize; + ballastTextureCoordinates[1] /= textureSize; + + for (int i = 0; i < BallastPoints.Count; i++) + { + Vector3 point = BallastPoints[i]; + Vector2 textureCoordinates = new Vector2(ballastTextureCoordinates[1]); + textureCoordinates.X = ballastTextureCoordinates[1].X - ((ballastTextureCoordinates[1].X - ballastTextureCoordinates[0].X) * ((point.X - BallastPoints[0].X) / (BallastPoints[BallastPoints.Count - 1].X - BallastPoints[0].X))); + if (i % 2 != 0) + { + // far + point.Z += 10; + builder.Vertices.Add(new Vertex(point, textureCoordinates)); + ballast.Add(builder.Vertices.Count - 1); + point.Z -= 10; + } + else + { + // near + textureCoordinates.Y = ballastTextureCoordinates[0].Y; + builder.Vertices.Add(new Vertex(point, textureCoordinates)); + ballast.Add(builder.Vertices.Count - 1); + point.Z += 10; + } + + textureCoordinates.Y = ballastTextureCoordinates[0].Y; + builder.Vertices.Add(new Vertex(new Vector3(point), Vector2.Null)); + ballast.Add(builder.Vertices.Count - 1); + } + face = new MeshFace(ballast.ToArray()); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + + sleeperBase[2] = builder.Vertices.Count + 1; + sleeperBase[3] = builder.Vertices.Count; + + ballast = new List(); + for (int i = 0; i < BallastPoints.Count; i++) + { + Vector3 point = BallastPoints[i]; + Vector2 textureCoordinates = new Vector2(ballastTextureCoordinates[1]); + textureCoordinates.X = ballastTextureCoordinates[1].X - ((ballastTextureCoordinates[1].X - ballastTextureCoordinates[0].X) * ((point.X - BallastPoints[0].X) / (BallastPoints[BallastPoints.Count - 1].X - BallastPoints[0].X))); + point.X = -point.X; + if (i % 2 != 0) + { + // far + point.Z += 10; + builder.Vertices.Add(new Vertex(point, textureCoordinates)); + ballast.Add(builder.Vertices.Count - 1); + point.Z -= 10; + } + else + { + // near + builder.Vertices.Add(new Vertex(point, textureCoordinates)); + ballast.Add(builder.Vertices.Count - 1); + point.Z += 10; + } + + textureCoordinates.Y = ballastTextureCoordinates[0].Y; + builder.Vertices.Add(new Vertex(new Vector3(point), textureCoordinates)); + ballast.Add(builder.Vertices.Count - 1); + } + face = new MeshFace(ballast.ToArray()); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + + face = new MeshFace(sleeperBase); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + + builder.Apply(ref Object); + } + } +} diff --git a/source/Plugins/Route.Loksim/Route.LokSim.csproj b/source/Plugins/Route.Loksim/Route.LokSim.csproj new file mode 100644 index 000000000..5ab999220 --- /dev/null +++ b/source/Plugins/Route.Loksim/Route.LokSim.csproj @@ -0,0 +1,90 @@ + + + + + 9.0 + + + Debug + AnyCPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E} + Library + Properties + LokSimRouteParser + Route.Loksim + v4.8 + 512 + true + + + + true + full + false + ..\..\..\bin_debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\..\..\bin_release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {27134980-4415-4375-A564-40A9014DFA5F} + OpenBveApi + + + {4EF680D7-DF17-4978-9872-133EDC169B4B} + RouteManager2 + + + {7ed7b285-fae6-4b34-acc5-87399f27c8bc} + Formats.OpenBve + + + + + + + + + + + \ No newline at end of file diff --git a/source/Plugins/Route.Loksim/Strecken/Curve.cs b/source/Plugins/Route.Loksim/Strecken/Curve.cs new file mode 100644 index 000000000..b7ab1dbf3 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Curve.cs @@ -0,0 +1,97 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Collections.Generic; +using Formats.OpenBve; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Curve : TrackElement + { + /// The angle of turn of the curve + internal double Angle; + + internal Curve(Block curveBlock) + { + curveBlock.GetValue(LoksimAttribute.Start, out StartingDistance); + curveBlock.GetValue(LoksimAttribute.Ende, out EndingDistance); + curveBlock.GetValue(LoksimAttribute.Winkel, out Angle); + curveBlock.GetValue(LoksimAttribute.SanfterWinkel, out SoftTransistion); + + // from Loksim documentation: + // ((𝐸nde – 𝑆tart) ∗ 180) / (𝜋𝜋 ∗ 𝑅adius) = 𝑊inkel + // Convert this to curve radius by rearranging equation + Angle = 180 * (EndingDistance - StartingDistance) / (2 * Math.PI * Angle); + } + + internal override void Create(ref List track, ref Vector3 Position, ref Vector2 Direction) + { + OpenBveApi.Routes.TrackElement lastElement = track[track.Count - 1]; + double currentDistance = lastElement.StartingTrackPosition; + + // for convenience add all 'missing' 1m track elements before the first + while (currentDistance < StartingDistance) + { + OpenBveApi.Routes.TrackElement tS = new OpenBveApi.Routes.TrackElement(currentDistance) + { + CurveRadius = 0, + WorldPosition = Position, + WorldDirection = Vector3.GetVector3(Direction, 0), + WorldSide = new Vector3(Direction.Y, 0.0, -Direction.X), + }; + + tS.WorldUp = Vector3.Cross(tS.WorldDirection, tS.WorldSide); + track.Add(tS); + currentDistance += 1; + Position.X += Direction.X; + Position.Z += Direction.Y; + } + + currentDistance = StartingDistance; + while (currentDistance < EndingDistance) + { + OpenBveApi.Routes.TrackElement tS = new OpenBveApi.Routes.TrackElement(currentDistance) + { + CurveRadius = Angle, + WorldPosition = Position, + WorldDirection = Vector3.GetVector3(Direction, 0), + WorldSide = new Vector3(Direction.Y, 0.0, - Direction.X), + }; + + tS.WorldUp = Vector3.Cross(tS.WorldDirection, tS.WorldSide); + track.Add(tS); + currentDistance += 1; + Position.X += Direction.X; + Position.Z += Direction.Y; + + double r = Angle; + double b = 1.0 / Math.Abs(r); + double a = 0.5 * Math.Sign(r) * b; + Direction.Rotate(Math.Cos(-a), Math.Sin(-a)); + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/LandschaftsObjekt.cs b/source/Plugins/Route.Loksim/Strecken/LandschaftsObjekt.cs new file mode 100644 index 000000000..c185613dc --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/LandschaftsObjekt.cs @@ -0,0 +1,198 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using OpenBveApi.Math; +using OpenBveApi.Objects; +using OpenBveApi.Routes; +using OpenBveApi.Textures; +using OpenBveApi.World; +using System; +using System.Collections.Generic; +using Formats.OpenBve; +using OpenBveApi.Colors; + +namespace LokSimRouteParser +{ + internal class LandschaftsObjekt + { + internal StaticObject internalObject; + + internal double TrackPosition; + + internal bool Polygon; + + internal double Scale; + + internal double Distance; + + internal double Length; + + internal LightStates Lighting; + + internal bool IntermediatePoints; + + internal bool Valid; + + internal string TextureFile; + + private List coordinateList; + + internal LandschaftsObjekt(Block landschaftBlock) + { + // A Landschaftsobjekt is a world ground object + internalObject = new StaticObject(Plugin.CurrentHost); + + bool shouldTexture = false; + + landschaftBlock.GetValue(LoksimAttribute.Abstand, out Distance); + landschaftBlock.GetValue(LoksimAttribute.Position, out TrackPosition); + landschaftBlock.GetValue(LoksimAttribute.Polygon, out Polygon); + landschaftBlock.GetValue(LoksimAttribute.Scale, out Scale); + landschaftBlock.GetValue(LoksimAttribute.Zwischenpunkte, out IntermediatePoints); + if (landschaftBlock.GetPath(LoksimAttribute.TextureFile, Plugin.FileSystem.LoksimDataDirectory, out TextureFile)) + { + Valid = true; + landschaftBlock.GetValue(LoksimAttribute.Texture, out shouldTexture); + } + + if (!shouldTexture) + { + Valid = false; + return; + } + + // may contain multiple Landschaftsobjekt blocks + coordinateList = new List(); + while (landschaftBlock.RemainingSubBlocks > 0) + { + Block subBlock = landschaftBlock.ReadNextBlock(); + switch (subBlock.Key) + { + case LoksimNode.Landschaftsobjekt: + { + Vector3 coordinates = new Vector3(); + subBlock.GetValue(LoksimAttribute.Hoehe, out coordinates.Y); + subBlock.GetValue(LoksimAttribute.Position, out coordinates.Z); + Length = Math.Max(coordinates.Z, Length); + subBlock.GetValue(LoksimAttribute.Verschiebung, out coordinates.X); + // Hoehefrei : pops the point to the top of the Z stack + coordinateList.Add(coordinates); + break; + } + case LoksimNode.Lightning: + Lighting = new LightStates(subBlock); + break; + } + } + } + + internal void Create(TrackFollower t) + { + if (Valid == false) + { + // polygon with no texture creates a 'hole' in the ground + // as the experiment is to render ground behind everything, discard + return; + } + + /* + * Need to figure out how to deal with the handling of these properly. + * + * This works as follows: + * -------------------------------- + * If should texture is not set (by an empty / missing texture file) + * there is actually a 'hole' in the Loksim generated ground, + * corresponding to the bounds of our LandschaftsObjeckt + * + * Need to dig into this some more, but I *suspect* that any area + * covered by a LandschaftsObjeckt actually doesn't generate a ground at all + * (most of this then masked by the badly limited camera view ability) + * + * EXPERIMENT: + * Trying a new flag on objects so that the ground is rendered without depth + * writes (e.g. behind everything) + * Can possible merge this with the background code... + * + * ------------------------------------------------ + * Zwischenpunkt + * ------------- + * Essentially, this adds a *new* vertex every 10m Z, but transformed by the X-pos of the rail + * yucky... + */ + + internalObject.Mesh.Vertices = new VertexTemplate[coordinateList.Count]; + List vertexWinding = new List(); + for (int i = 0; i < coordinateList.Count; i++) + { + switch (i % 4) + { + case 0: + internalObject.Mesh.Vertices[i] = new Vertex(coordinateList[i], new Vector2(0, 0)); + break; + case 1: + internalObject.Mesh.Vertices[i] = new Vertex(coordinateList[i], new Vector2(1, 0)); + break; + case 2: + internalObject.Mesh.Vertices[i] = new Vertex(coordinateList[i], new Vector2(1, 1)); + break; + case 3: + internalObject.Mesh.Vertices[i] = new Vertex(coordinateList[i], new Vector2(0, 1)); + break; + } + vertexWinding.Add(i); + } + + internalObject.Mesh.Faces = new[] + { + new MeshFace(vertexWinding.ToArray(), 0, FaceFlags.Face2Mask) + }; + internalObject.OptimizeObject(true, 0, true); + + MeshMaterial material = new MeshMaterial(); + material.Color = Color32.White; + + Plugin.CurrentHost.RegisterTexture(TextureFile, new TextureParameters(null, null), out material.DaytimeTexture); + internalObject.Mesh.Materials = new[] + { + material + }; + + + t.UpdateAbsolute(TrackPosition, true, false); + Vector3 wPos = t.WorldPosition; + t.UpdateRelative(Length, true, false); + + Vector3 d = t.WorldPosition == wPos ? t.WorldPosition : new Vector3(t.WorldPosition - wPos); + double tt = d.Magnitude(); + d *= tt; + tt = 1.0 / Math.Sqrt(d.X * d.X + d.Z * d.Z); + double ex = d.X * tt; + double ez = d.Z * tt; + Vector3 s = new Vector3(ez, 0.0, -ex); + Vector3 u = Vector3.Cross(d, s); + + internalObject.CreateObject(wPos, new Transformation(d,u,s), new ObjectCreationParameters(TrackPosition, TrackPosition -500, TrackPosition + 500)); + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/ObjectGroup.cs b/source/Plugins/Route.Loksim/Strecken/ObjectGroup.cs new file mode 100644 index 000000000..42f240c50 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/ObjectGroup.cs @@ -0,0 +1,295 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using OpenBveApi.Interface; +using OpenBveApi.Math; +using OpenBveApi.Objects; +using OpenBveApi.Routes; +using OpenBveApi.World; +using System; +using System.Collections.Generic; +using System.Text; +using Formats.OpenBve; + +namespace LokSimRouteParser +{ + internal class ObjectGroup + { + /// The position of this on the rail + internal double TrackPosition; + /// The offset from the position + internal Vector3 OffsetPosition; + + internal string Name; + + internal List Objects; + internal ObjectGroup(Block objectGroupBlock) + { + // An ObjectGroup [Object in the XML] is somewhere roughly analogous to a world tile + Objects = new List(); + objectGroupBlock.GetValue(LoksimAttribute.Position, out TrackPosition); + // We get an initial position to looking ahead on the transformed track + // Then add an offset to get our final WPos + objectGroupBlock.GetVector3(LoksimAttribute.Offset, ';', out OffsetPosition); + + /* Qualitaet, + * --------- + * Used by the renderer to determine visibility + * Our renderer can probably ignore this + * ----------------------------------------------- + * 0 - Absolutely necessary (e.g. signals, tracks) + * 1 - Very important (e.g. railway embankments) + * 2 - Important (e.g. tunnels, bridges) + * 3 - Normal object (e.g houses) + * 4 - Unimportant object (e.g. trees) + * 5 - Completely unimportant (e.g. overpass- presumably road traffic) + */ + // RollMaterial : set to true if this ObjectGroup is another train + // DynamicVisibility + // FixedDynamicVisibility + /* StreckenAbhaengigkeit + * ------------------ + * 0 - Display independent of the train (default) + * 1 - Displayed only if no train is on this track + * 2 - Displayed only if train is on this track + */ + + while (objectGroupBlock.RemainingSubBlocks > 0) + { + Block subBlock = objectGroupBlock.ReadNextBlock(); + switch (subBlock.Key) + { + case LoksimNode.Eintrag: + Objects.Add(new Object(subBlock)); + break; + } + } + } + + internal void Create(TrackFollower t) + { + + for (int i = 0; i < Objects.Count; i++) + { + // reset the track follower position, as we're using it for repetitions + t.UpdateAbsolute(TrackPosition, true, false); + Objects[i].Create(t, OffsetPosition); + } + } + } + + internal class Object + { + /// When repetition is used, distance to next object + internal double Distance; + /// Number of copies of object to place + internal int NumberOfObjects; + internal bool FollowsTrackHeight; + internal bool FollowsLandHeight; + internal UnifiedObject UnifiedObject; + /// Offset vector of the object from its rail position + internal Vector3 Offset; + /// Z-position of the object on the rail + internal double Position; + /// Rotation in degrees for each axis + internal Vector3 Rotation; + internal bool FarVisible; + internal LightStates Lighting; + internal Dictionary Properties; + internal LoksimRandom Random; + + + internal Object(Block objectBlock) + { + objectBlock.GetValue(LoksimAttribute.Abstand, out Distance); + objectBlock.GetValue(LoksimAttribute.Anzahl, out NumberOfObjects); + if (!Plugin.previewOnly && objectBlock.GetPath(LoksimAttribute.Datei, Plugin.FileSystem.LoksimDataDirectory, out string objectPath)) + { + if (!Plugin.ObjectCache.TryGetValue(objectPath, out UnifiedObject)) + { + if (System.IO.File.Exists(objectPath)) + { + Plugin.CurrentHost.LoadObject(objectPath, Encoding.UTF8, out UnifiedObject); + Plugin.ObjectCache.Add(objectPath, UnifiedObject); + } + else + { + Plugin.CurrentHost.AddMessage(MessageType.Error, true, "LokSim3D: Object " + objectPath.Replace(Plugin.FileSystem.LoksimDataDirectory, string.Empty) + " was not found."); + } + } + } + + objectBlock.GetValue(LoksimAttribute.GleisHoeheFolgen, out FollowsTrackHeight); + objectBlock.GetValue(LoksimAttribute.HoeheLand, out FollowsLandHeight); + objectBlock.GetValue(LoksimAttribute.Position, out Position); + objectBlock.GetVector3(LoksimAttribute.Verschiebung, ';', out Offset); + objectBlock.GetValue(LoksimAttribute.WeitSichtbar, out FarVisible); + objectBlock.GetVector3(LoksimAttribute.Winkel, ';', out Rotation); + + if (objectBlock.ReadBlock(LoksimNode.Eigenschaften, out Block eigenschaftenBlock)) + { + Properties = new Dictionary(); + while (eigenschaftenBlock.RemainingDataValues > 0) + { + eigenschaftenBlock.GetNextValue(out LoksimAttribute valueType, out string value); + if (valueType != LoksimAttribute.Unknown && !Properties.ContainsKey(valueType)) + { + Properties.Add(valueType, value); + } + } + } + + if (objectBlock.ReadBlock(LoksimNode.Lightning, out Block lightingBlock)) + { + Lighting = new LightStates(lightingBlock); + } + + if (objectBlock.ReadBlock(LoksimNode.Random, out Block randomBlock)) + { + Random = new LoksimRandom(randomBlock); + } + } + + internal void Create(TrackFollower tf, Vector3 offsetPosition) + { + if (UnifiedObject != null) + { + Vector3 o = new Vector3(Offset); + o.Z += Position; + o += offsetPosition; + + if (NumberOfObjects == 1) + { + + if (Random != null) + { + Vector3 v = Random.GetPosition(); + o += v; + } + o.Rotate(tf.WorldDirection, tf.WorldUp, tf.WorldSide); + Vector3 objectWpos = tf.WorldPosition + o; + Transformation tr = new Transformation(tf.WorldDirection, tf.WorldUp, tf.WorldSide); + UnifiedObject.CreateObject(objectWpos, tr, new Transformation(Rotation.X.ToRadians(), Rotation.Y.ToRadians(), Rotation.Z.ToRadians()), new ObjectCreationParameters(tf.TrackPosition, tf.TrackPosition, tf.TrackPosition + 500)); + } + else + { + Vector3 lastWpos = tf.WorldPosition; + for (int i = 0; i < NumberOfObjects; i++) + { + + Vector3 objectWpos; + Vector3 repetitionPosition = Vector3.Zero; + if (i != 0) + { + tf.UpdateRelative(Distance, true, false); + } + + + Vector3 d = tf.WorldPosition == lastWpos ? tf.WorldPosition : new Vector3(tf.WorldPosition - lastWpos); + double t = d.Magnitude(); + d *= t; + t = 1.0 / Math.Sqrt(d.X * d.X + d.Z * d.Z); + double ex = d.X * t; + double ez = d.Z * t; + Vector3 s = new Vector3(ez, 0.0, -ex); + Vector3 u = Vector3.Cross(d, s); + + + Vector3 oC = new Vector3(o); + oC.Rotate(tf.WorldDirection, tf.WorldUp, tf.WorldSide); + objectWpos = tf.WorldPosition + oC; + + if (Random != null) + { + repetitionPosition += Random.GetPosition(); + } + repetitionPosition.Rotate(d,u,s); + Transformation tr = new Transformation(d,u,s); + UnifiedObject.CreateObject(objectWpos + repetitionPosition, tr, new Transformation(Rotation.X.ToRadians(), Rotation.Y.ToRadians(), Rotation.Z.ToRadians()), new ObjectCreationParameters(tf.TrackPosition, tf.TrackPosition, tf.TrackPosition + 500)); + + + lastWpos = tf.WorldPosition; + } + + + } + + } + } + } + + internal class LoksimRandom + { + /// The random seed + internal readonly int Seed; + + internal readonly double XValue; + + internal readonly double YValue; + + internal readonly double ZValue; + + internal readonly double XRotation; + + internal readonly double YRotation; + + internal readonly double ZRotation; + + + private readonly Random random; + + internal LoksimRandom(Block randomBlock) + { + randomBlock.GetValue(LoksimAttribute.SRAND, out Seed); + randomBlock.GetValue(LoksimAttribute.XValue, out XValue); + randomBlock.GetValue(LoksimAttribute.YValue, out YValue); + randomBlock.GetValue(LoksimAttribute.ZValue, out ZValue); + random = new Random(Seed); + + } + + internal Vector3 GetPosition() + { + double randomResult = random.NextDouble(); + // random result returns zero plus / minus the value + return new Vector3(XValue - randomResult * XValue * 2, YValue - randomResult * YValue * 2, ZValue - randomResult * ZValue * 2); + } + + + } + + internal class LightStates + { + internal double Day; + internal double Night; + internal bool TrainLit; + internal LightStates(Block lightingBlock) + { + // Going to be roughly analogous to the BVE DNB values + lightingBlock.GetValue(LoksimAttribute.DayLight, out Day); + lightingBlock.GetValue(LoksimAttribute.NightLight, out Night); + lightingBlock.GetValue(LoksimAttribute.LokLight, out TrainLit); + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Pitch.cs b/source/Plugins/Route.Loksim/Strecken/Pitch.cs new file mode 100644 index 000000000..db1c382f2 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Pitch.cs @@ -0,0 +1,81 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.Collections.Generic; +using Formats.OpenBve; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Pitch : TrackElement + { + internal double PerThousand; + + internal Pitch(Block pitchBlock) + { + pitchBlock.GetValue(LoksimAttribute.Start, out StartingDistance); + pitchBlock.GetValue(LoksimAttribute.Ende, out EndingDistance); + pitchBlock.GetValue(LoksimAttribute.Promille, out PerThousand); + pitchBlock.GetValue(LoksimAttribute.SanfterWinkel, out SoftTransistion); + } + + internal override void Create(ref List track, ref Vector3 Position, ref Vector2 Direction) + { + return; + /* + if (!track.ContainsKey(StartingDistance)) + { + OpenBveApi.Routes.TrackElement tS = new OpenBveApi.Routes.TrackElement(StartingDistance) + { + Pitch = PerThousand, + WorldPosition = new Vector3(StartingDistance, 0, 0) + }; + track.Add(StartingDistance, tS); + } + else + { + OpenBveApi.Routes.TrackElement tS = track[StartingDistance]; + tS.Pitch = PerThousand; + track[StartingDistance] = tS; + } + + if (!track.ContainsKey(EndingDistance)) + { + OpenBveApi.Routes.TrackElement tE = new OpenBveApi.Routes.TrackElement(EndingDistance) + { + Pitch = PerThousand, + WorldPosition = new Vector3(EndingDistance, 0,0) + }; + track.Add(EndingDistance, tE); + } + else + { + OpenBveApi.Routes.TrackElement tE = track[EndingDistance]; + tE.Pitch = 0; + track[EndingDistance] = tE; + } + */ + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/StartPunkt.cs b/source/Plugins/Route.Loksim/Strecken/StartPunkt.cs new file mode 100644 index 000000000..6645f872a --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/StartPunkt.cs @@ -0,0 +1,48 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using Formats.OpenBve; + +namespace LokSimRouteParser +{ + internal class StartPunkt + { + /// The rail to start on + internal string Track; + /// The track position on that rail + internal double Position; + /// Whether to start in the reverse direction + internal bool Reverse; + /// The actual routefile to start in + internal string RouteFile; + + internal StartPunkt(Block startPunktBlock) + { + startPunktBlock.GetValue(LoksimAttribute.Gleis, out Track); + startPunktBlock.GetValue(LoksimAttribute.Pos, out Position); + startPunktBlock.GetValue(LoksimAttribute.Richtung, out Reverse); + startPunktBlock.GetValue(LoksimAttribute.Strecke, out RouteFile); + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Strecke.cs b/source/Plugins/Route.Loksim/Strecken/Strecke.cs new file mode 100644 index 000000000..96f7b9c01 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Strecke.cs @@ -0,0 +1,136 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using Formats.OpenBve; +using OpenBveApi.Interface; +using OpenBveApi.Math; +using OpenBveApi.Objects; +using Path = OpenBveApi.Path; + +namespace LokSimRouteParser +{ + internal class Strecke + { + /// The default rail object used + internal StaticObject DefaultRail; + + internal string StdTexture; + + internal int Direction; + + internal Dictionary Tracks = new Dictionary(); + + + internal Strecke(string fileName) + { + string f = File.ReadAllText(fileName); + f = f.Replace("Fahrt Gegenrichtung", "FahrtGegenrichtung"); + f = f.Replace("Fahrt Richtung", "FahrtRichtung"); + AttributedXMLFile streckeFile = new AttributedXMLFile(f, fileName, "/STRECKE", Plugin.CurrentHost); + + while (streckeFile.RemainingSubBlocks > 0) + { + Block subBlock = streckeFile.ReadNextBlock(); + switch (subBlock.Key) + { + case LoksimNode.Props: + // These are present, but the info in here isn't useful at present + break; + case LoksimNode.DefaulRail: + if (subBlock.GetPath(LoksimAttribute.File, Plugin.FileSystem.LoksimDataDirectory, out string railFile)) + { + L3DRail rail = new L3DRail(railFile); + DefaultRail = rail.Object; + } + break; + case LoksimNode.StdTexture: + // Unknown - Empty in demo routes + break; + case LoksimNode.Himmelsrichtung: + // Direction of travel (construction) for this routefile + subBlock.GetValue(LoksimAttribute.Value, out Direction); + break; + case LoksimNode.GLEIS: + if (subBlock.GetValue(LoksimAttribute.Name, out string trackName) && subBlock.ReadBlock(LoksimNode.TOPOLOGIE, out Block topologyBlock)) + { + // NOTE: Names are unique per Strecke, but a route can contain multiple Strecke with the same track keys + Track newTrack = new Track(topologyBlock, trackName, this); + + + if (subBlock.ReadBlock(LoksimNode.Objecte, out Block objectsBlock)) + { + while (objectsBlock.RemainingSubBlocks > 0) + { + Block objectBlock = objectsBlock.ReadNextBlock(); + newTrack.Objects.Add(new ObjectGroup(objectBlock)); + } + } + + if (subBlock.ReadBlock(LoksimNode.Landschaft, out Block landschaftBlock)) + { + while (landschaftBlock.RemainingSubBlocks > 0) + { + newTrack.Landschaftsobjekt.Add(new LandschaftsObjekt(landschaftBlock.ReadNextBlock())); + } + + } + Tracks.Add(trackName, newTrack); + } + break; + } + } + XmlDocument currentXML = new XmlDocument(); + try + { + // The XML parser LokSim uses seems to allow space in XML attribute names, + // despite the fact this is *not* technically valid + + currentXML.LoadXml(f); + } + catch + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Failed to load Strecke " + fileName); + throw; + } + } + + internal void Create(ref Vector3 wpos) + { + for (int i = 0; i < Tracks.Count; i++) + { + string trackKey = Tracks.ElementAt(i).Key; + if (!Plugin.TrackKeys.ContainsKey(Tracks[trackKey].Guid)) + { + Plugin.TrackKeys.Add(Tracks[trackKey].Guid, i); + } + Tracks[trackKey].Create(wpos, i, DefaultRail); + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Track.TrackType.cs b/source/Plugins/Route.Loksim/Strecken/Track.TrackType.cs new file mode 100644 index 000000000..c61d07818 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Track.TrackType.cs @@ -0,0 +1,36 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +namespace LokSimRouteParser +{ + internal enum TrackType + { + /// Normal track + Normal, + /// Track runs parallel to another track + Parallel, + /// Track is a switch + Weiche + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Track.cs b/source/Plugins/Route.Loksim/Strecken/Track.cs new file mode 100644 index 000000000..da88aab5b --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Track.cs @@ -0,0 +1,314 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Collections.Generic; +using Formats.OpenBve; +using OpenBveApi; +using OpenBveApi.Interface; +using OpenBveApi.Math; +using OpenBveApi.Objects; +using OpenBveApi.Routes; +using OpenBveApi.World; +using RouteManager2.Events; +using RouteManager2.Stations; + +namespace LokSimRouteParser +{ + internal class Track + { + internal readonly string Name; + + internal List Elements; + + internal List Objects; + + internal List Rails; + + internal List Landschaftsobjekt; + + internal TrackType Type; + + internal string ParallelTrack; + + internal string WeicheTrack; + + internal double WeichePos; + + internal double ParallelOffset; + + internal double ParallelStart; + + internal double ParallelEnd; + + internal Vector3 StartPoint; + + internal double EndPoint; + + internal double StartPos; + + internal Strecke Strecke; + + internal Guid Guid; + + internal Track(Block topologyBlock, string trackName, Strecke strecke) + { + Strecke = strecke; + // track names can be re-used between different strecke so generate a GUID for unique identification + Guid = Guid.NewGuid(); + + Elements = new List(); + Objects = new List(); + Landschaftsobjekt = new List(); + StartPoint = new Vector3(); + + while (topologyBlock.RemainingSubBlocks > 0) + { + Block subBlock = topologyBlock.ReadNextBlock(); + switch (subBlock.Key) + { + case LoksimNode.Erstellung: + // End point (m) + subBlock.GetValue(LoksimAttribute.EndPunkt, out EndPoint); + if (subBlock.GetValue(LoksimAttribute.ParallelGleis, out ParallelTrack)) + { + if (!Strecke.Tracks.ContainsKey(ParallelTrack)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: The track to parallel with key " + ParallelTrack + " was not found."); + } + else + { + subBlock.GetValue(LoksimAttribute.ParallelStart, out ParallelStart); + subBlock.GetValue(LoksimAttribute.ParallelEnd, out ParallelEnd); + subBlock.GetValue(LoksimAttribute.ParallelAbstand, out ParallelOffset); + } + } + + subBlock.GetValue(LoksimAttribute.StartPos, out StartPos); + subBlock.GetVector3(LoksimAttribute.StartPunkt, ';', out StartPoint); + subBlock.GetEnumValue(LoksimAttribute.Typ, out Type); + if (subBlock.GetValue(LoksimAttribute.WeicheGleis, out WeicheTrack)) + { + if(!Strecke.Tracks.ContainsKey(WeicheTrack)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: The switch track with key " + WeicheTrack + " was not found."); + } + else + { + // WeicheStart : ? + // WeicheEnd : ? + // WeichenWinkel : radius of switching track + + // WeicheGleisRichtung : switch direction (? for display on HUD, or for object direction ?) + // WeicheName : switch name (hud) + + subBlock.GetValue(LoksimAttribute.WeicheGleisPos, out WeichePos); + } + } + // Winkel : probably used in conjunction with Weiche above + break; + case LoksimNode.KURVE: + case LoksimNode.STEIGUNG: + Elements.Add(subBlock.Key == LoksimNode.KURVE ? new Curve(subBlock) : new Pitch(subBlock)); + break; + case LoksimNode.Weiche: + Elements.Add(new Weiche(subBlock)); + break; + case LoksimNode.Ausweiche: + // avoidance? + break; + } + } + } + + internal void Create(Vector3 wpos, int newTrackKey, StaticObject defaultRail) + { + wpos += StartPoint; // ?? + if (ParallelOffset != 0) + { + wpos.X += ParallelOffset; + } + List newElements = new List(); + + + /* + * Track with Typ == Weiche + * + * Begins with a switch. + * Select Track defined by WeicheGleis + * WeicheGleisPos is the *relative* position on WeicheGleis of Element 0 + */ + + Vector2 direction = Vector2.Down; + TrackFollower tf = new TrackFollower(Plugin.CurrentHost); + Vector3 directionVector = Vector3.Left; + switch (Type) + { + case TrackType.Parallel: + tf.TrackIndex = Plugin.TrackKeys[Strecke.Tracks[ParallelTrack].Guid]; + tf.UpdateAbsolute(ParallelStart, true, false); + directionVector.Rotate(tf.WorldDirection, tf.WorldUp, tf.WorldSide); + directionVector *= ParallelOffset; + wpos = tf.WorldPosition + directionVector; + direction = new Vector2(tf.WorldDirection.X, tf.WorldDirection.Y); + break; + case TrackType.Weiche: + tf.TrackIndex = Plugin.TrackKeys[Strecke.Tracks[WeicheTrack].Guid]; + tf.UpdateAbsolute(WeichePos, true, false); + directionVector.Rotate(tf.WorldDirection, tf.WorldUp, tf.WorldSide); + directionVector *= ParallelOffset; + wpos = tf.WorldPosition + directionVector; + direction = new Vector2(tf.WorldDirection.X, tf.WorldDirection.Y); + break; + } + + OpenBveApi.Routes.TrackElement te = new OpenBveApi.Routes.TrackElement(StartPos); + te.WorldPosition = wpos; + newElements.Add(te); + + + for (int i = 0; i < Elements.Count; i++) + { + if (Type == TrackType.Parallel) + { + double sPos = StartPos; + while (tf.TrackPosition < Elements[i].StartingDistance) + { + tf.UpdateRelative(1, true, false); + Vector3 elementDirection = Vector3.Left; + elementDirection.Rotate(tf.WorldDirection, tf.WorldUp, tf.WorldSide); + elementDirection *= ParallelOffset; + OpenBveApi.Routes.TrackElement ae = new OpenBveApi.Routes.TrackElement(sPos); + ae.WorldPosition = tf.WorldPosition + elementDirection; + newElements.Add(ae); + StartPos++; + } + } + Elements[i].Create(ref newElements, ref wpos, ref direction); + } + + if (newTrackKey != 0) + { + Plugin.CurrentRoute.Tracks.Add(newTrackKey, new OpenBveApi.Routes.Track(Name)); + } + + Plugin.CurrentRoute.Tracks[newTrackKey].Elements = new OpenBveApi.Routes.TrackElement[newElements.Count]; + newElements.CopyTo(Plugin.CurrentRoute.Tracks[newTrackKey].Elements, 0); + + + if (newTrackKey == 0) + { + /* + * TEMP HACKS TO ALLOW VIEWING + * REMOVE ONCE STATIONS ARE IMPLEMENTED + */ + int eL = Plugin.CurrentRoute.Tracks[0].Elements.Length - 1; + Plugin.CurrentRoute.Stations = new[] + { + new RouteStation + { + Name = "Start", + Stops = new[] + { + new StationStop + { + TrackPosition = Plugin.CurrentRoute.Tracks[newTrackKey].Elements[0].StartingTrackPosition + } + } + }, + new RouteStation + { + Name = "End", + Stops = new[] + { + new StationStop + { + TrackPosition = Plugin.CurrentRoute.Tracks[newTrackKey].Elements[eL].StartingTrackPosition + } + } + } + }; + Plugin.CurrentRoute.Tracks[newTrackKey].Elements[0].Events = new List() + { + new StationStartEvent(Plugin.CurrentRoute, 0, 0) + }; + Plugin.CurrentRoute.Tracks[newTrackKey].Elements[1].Events = new List() + { + new StationEndEvent(Plugin.CurrentHost, Plugin.CurrentRoute, 0, 0) + }; + Plugin.CurrentRoute.Tracks[newTrackKey].Elements[eL].Events = new List() + { + new StationStartEvent(Plugin.CurrentRoute, 0, 1) + }; + } + + TrackFollower t = new TrackFollower(Plugin.CurrentHost); + t.TrackIndex = newTrackKey; + for (int i = 0; i < Objects.Count; i++) + { + Objects[i].Create(t); + } + + for (int i = 0; i < Landschaftsobjekt.Count; i++) + { + Landschaftsobjekt[i].Create(t); + } + + double start = Plugin.CurrentHost.Tracks[0].Elements[0].StartingTrackPosition; + double end = Plugin.CurrentHost.Tracks[0].Elements[Plugin.CurrentHost.Tracks[0].Elements.Length -1].StartingTrackPosition; + + t.UpdateAbsolute(start, true, false); + + /* + * HACK: + * Loksim has an overall ground plane + * This can be overridden by placing a LandschaftObject + * + * We don't exactly do that, but if we render a ground plane + * with depth writes *disabled* anything (including + * LandschaftObject which go below the track datum) will + * overlay it. + * + * TODO: + * Might be possible to merge this with backgrounds + */ + + string groundFile = Path.CombineFile(Plugin.FileSystem.GetDataFolder(new[] { "Compatibility" }),"Loksim\\Grass.csv"); + Plugin.CurrentHost.LoadObject(groundFile, System.Text.Encoding.UTF8, out UnifiedObject ground); + StaticObject so = ground as StaticObject; + so.Mesh.Faces[0].Flags |= FaceFlags.DisableDepthWrite; + + while (t.TrackPosition < end) + { + t.UpdateRelative(10, true, false); + Plugin.CurrentHost.CreateStaticObject((StaticObject)ground, t.WorldPosition, new ObjectCreationParameters(t.TrackPosition, t.TrackPosition + 25), Transformation.NullTransformation); + if (defaultRail != null) + { + Plugin.CurrentHost.CreateStaticObject(defaultRail, t.WorldPosition, new ObjectCreationParameters(t.TrackPosition, t.TrackPosition + 25), new Transformation(t.WorldDirection, t.WorldUp, t.WorldSide)); + } + + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/TrackElement.cs b/source/Plugins/Route.Loksim/Strecken/TrackElement.cs new file mode 100644 index 000000000..0e17b3f73 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/TrackElement.cs @@ -0,0 +1,40 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.Collections.Generic; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal abstract class TrackElement + { + /// The distance at which this element starts + internal double StartingDistance; + /// The distance at which the element ends + internal double EndingDistance; + internal bool SoftTransistion; + + internal abstract void Create(ref List track, ref Vector3 Position, ref Vector2 Direction); + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Weather.cs b/source/Plugins/Route.Loksim/Strecken/Weather.cs new file mode 100644 index 000000000..e6968c0dd --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Weather.cs @@ -0,0 +1,101 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.Xml; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + /// Class describing a Loksim3D weather / skybox file + internal class Weather + { + + internal double DaytimeBrightness; + + internal double NightTimeBrightness; + + internal double DawnBegin; + + internal double DawnEnd; + + internal double DuskBegin; + + internal double DuskEnd; + + internal Weather(XmlNode node) + { + if (node.Attributes == null) + { + return; + } + + for (int i = 0; i < node.ChildNodes.Count; i++) + { + switch (node.ChildNodes[i].Name) + { + case "AmbientIllumination": + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (node.ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "BrightnessDay": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out DaytimeBrightness); + break; + case "BrightnessNight": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out NightTimeBrightness); + break; + case "DawnBegin": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out DawnBegin); + break; + case "DawnEnd": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out DawnEnd); + break; + case "DuskBegin": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out DuskBegin); + break; + case "DuskEnd": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out DuskEnd); + break; + } + } + break; + } + } + break; + case "WeatherSet": + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + + } + break; + } + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Weiche.cs b/source/Plugins/Route.Loksim/Strecken/Weiche.cs new file mode 100644 index 000000000..214effd91 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Weiche.cs @@ -0,0 +1,57 @@ +//Simplified BSD License (BSD-2-Clause) +// +//Copyright (c) 2026, Christopher Lees, The OpenBVE Project +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +//LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +//ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.Collections.Generic; +using Formats.OpenBve; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Weiche : TrackElement + { + internal Weiche(Block propsBlock) + { + propsBlock.GetValue(LoksimAttribute.Position, out StartingDistance); + } + + internal override void Create(ref List track, ref Vector3 Position, ref Vector2 Direction) + { + /* + if (!track.ContainsKey(StartingDistance)) + { + OpenBveApi.Routes.TrackElement tS = new OpenBveApi.Routes.TrackElement(StartingDistance) + { + WorldPosition = new Vector3(StartingDistance, 0, 0) + }; + track.Add(StartingDistance, tS); + } + else + { + OpenBveApi.Routes.TrackElement tS = track[StartingDistance]; + track[StartingDistance] = tS; + } + */ + } + } +} diff --git a/source/RouteViewer/ProgramR.cs b/source/RouteViewer/ProgramR.cs index 73dfeb6d0..457550401 100644 --- a/source/RouteViewer/ProgramR.cs +++ b/source/RouteViewer/ProgramR.cs @@ -526,7 +526,7 @@ internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e) string previousRoute = CurrentRouteFile; OpenFileDialog Dialog = new OpenFileDialog(); Dialog.CheckFileExists = true; - Dialog.Filter = @"All Supported Routes|*.csv;*.rw;*.dat;*.txt|CSV/RW files|*.csv;*.rw|Mechanik Routes|*.dat|BVE5 Routes|*.txt|All files|*"; + Dialog.Filter = @"All Supported Routes|*.csv;*.rw;*.dat;*.txt;*.l3dfpl|CSV/RW files|*.csv;*.rw|Mechanik Routes|*.dat|BVE5 Routes|*.txt|LokSim3D Routes|*.l3dfpl|All files|*"; if (Dialog.ShowDialog() == DialogResult.OK) { Application.DoEvents();