From 1b452263cbecc0e9b08887847704f031b6bc3407 Mon Sep 17 00:00:00 2001 From: Christopher Lees Date: Thu, 16 Mar 2023 20:18:57 +0000 Subject: [PATCH 1/3] Start of Loksim3D route parser --- OpenBVE.sln | 22 + source/LibRender2/Objects/ObjectLibrary.cs | 2 +- .../OpenBVE/UserInterface/formMain.Start.cs | 21 + .../OpenBveApi/Objects/Helpers/MeshBuilder.cs | 3 +- .../Routes/QuadTree/QuadNode.Leaf.cs | 4 + source/OpenBveApi/System/Path.Loksim3D.cs | 1 + source/Plugins/Route.Loksim/Common/Node.cs | 28 ++ .../Route.Loksim/Common/PropsAttribute.cs | 65 +++ .../Plugins/Route.Loksim/Fahrplan/Fahrplan.cs | 200 ++++++++ .../Plugins/Route.Loksim/Fahrplan/Station.cs | 147 ++++++ .../Kursbuchstrecken/Connection.cs | 85 ++++ .../Kursbuchstrecken/Kursbuchstrecken.cs | 196 ++++++++ source/Plugins/Route.Loksim/Plugin.cs | 103 ++++ .../Route.Loksim/Properties/AssemblyInfo.cs | 35 ++ source/Plugins/Route.Loksim/Rails/L3DRail.cs | 186 ++++++++ .../Plugins/Route.Loksim/Route.LokSim.csproj | 83 ++++ source/Plugins/Route.Loksim/Strecken/Curve.cs | 116 +++++ .../Strecken/Landschaftsobjekt.cs | 228 +++++++++ .../Route.Loksim/Strecken/ObjectGroup.cs | 444 ++++++++++++++++++ source/Plugins/Route.Loksim/Strecken/Pitch.cs | 100 ++++ .../Route.Loksim/Strecken/StartPunkt.cs | 77 +++ .../Plugins/Route.Loksim/Strecken/Strecke.cs | 146 ++++++ .../Route.Loksim/Strecken/Track.TrackType.cs | 36 ++ source/Plugins/Route.Loksim/Strecken/Track.cs | 437 +++++++++++++++++ .../Route.Loksim/Strecken/TrackElement.cs | 40 ++ .../Plugins/Route.Loksim/Strecken/Weiche.cs | 71 +++ source/RouteViewer/ProgramR.cs | 2 +- 27 files changed, 2875 insertions(+), 3 deletions(-) create mode 100644 source/Plugins/Route.Loksim/Common/Node.cs create mode 100644 source/Plugins/Route.Loksim/Common/PropsAttribute.cs create mode 100644 source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs create mode 100644 source/Plugins/Route.Loksim/Fahrplan/Station.cs create mode 100644 source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.cs create mode 100644 source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs create mode 100644 source/Plugins/Route.Loksim/Plugin.cs create mode 100644 source/Plugins/Route.Loksim/Properties/AssemblyInfo.cs create mode 100644 source/Plugins/Route.Loksim/Rails/L3DRail.cs create mode 100644 source/Plugins/Route.Loksim/Route.LokSim.csproj create mode 100644 source/Plugins/Route.Loksim/Strecken/Curve.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/Landschaftsobjekt.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/ObjectGroup.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/Pitch.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/StartPunkt.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/Strecke.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/Track.TrackType.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/Track.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/TrackElement.cs create mode 100644 source/Plugins/Route.Loksim/Strecken/Weiche.cs 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/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/Helpers/MeshBuilder.cs b/source/OpenBveApi/Objects/Helpers/MeshBuilder.cs index 404c1be8b..ff6cd19d5 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 55b4ac1a6..6a9bdd422 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/Route.Loksim/Common/Node.cs b/source/Plugins/Route.Loksim/Common/Node.cs new file mode 100644 index 000000000..bb9dc9c61 --- /dev/null +++ b/source/Plugins/Route.Loksim/Common/Node.cs @@ -0,0 +1,28 @@ +namespace LokSimRouteParser +{ + internal enum LoksimNode + { + // Common + Props, + Random, + // Fahrplan + PneuBremse, + MgBremse, + Haltestellen, + // Kursbuchstrecken + Strecken, + Verbindung, + StartPunkt, + // Objecte + Objecte, + Object, + Eintrag, + Eigenschaften, + Lightning, + // L3DRail + Hoehe, + TexSchieneOben, + TexSchieneSeite, + TexBettung + } +} diff --git a/source/Plugins/Route.Loksim/Common/PropsAttribute.cs b/source/Plugins/Route.Loksim/Common/PropsAttribute.cs new file mode 100644 index 000000000..81bc9e59d --- /dev/null +++ b/source/Plugins/Route.Loksim/Common/PropsAttribute.cs @@ -0,0 +1,65 @@ +namespace LokSimRouteParser +{ + internal enum PropsAttribute + { + // Common attributes + FileAuthor, + FileInfo, + FilePicture, + FileDoku, + Name, + // 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, + + + } +} diff --git a/source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs b/source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs new file mode 100644 index 000000000..48e45ba47 --- /dev/null +++ b/source/Plugins/Route.Loksim/Fahrplan/Fahrplan.cs @@ -0,0 +1,200 @@ +//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.Xml; +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; + } + + if (currentXML.DocumentElement != null) + { + XmlNodeList DocumentNodes = currentXML.DocumentElement.SelectNodes("/FPL"); + if (DocumentNodes == null || DocumentNodes.Count == 0) + { + throw new Exception("No Fahrplan nodes in file " + fileName); + } + + if (DocumentNodes.Count > 1) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Only the first Fahrplan is supported in " + fileName); + } + for (int i = 0; i < DocumentNodes[0].ChildNodes.Count; i++) + { + if (!Enum.TryParse(DocumentNodes[0].ChildNodes[i].Name, out LoksimNode parsedNode)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised node " + DocumentNodes[0].ChildNodes[i].Name + " in LokSim3D Fahrplan " + fileName); + continue; + } + switch (parsedNode) + { + case LoksimNode.Props: + // Basic properties + if (DocumentNodes[0].ChildNodes[i].Attributes != null) + { + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].Attributes.Count; j++) + { + if (!Enum.TryParse(DocumentNodes[0].ChildNodes[i].Attributes[j].Name, true, out PropsAttribute props)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised Props attribute " + DocumentNodes[0].ChildNodes[i].Attributes[j].Name + " in LokSim3D Fahrplan " + fileName); + continue; + } + switch (props) + { + case PropsAttribute.FileAuthor: + FileAuthor = DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText; + break; + case PropsAttribute.FileInfo: + FileInfo = DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText; + break; + case PropsAttribute.FilePicture: + // Path relative to the current Fahrplan + FilePicture = DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText; + break; + case PropsAttribute.KBS: + // Path relative to the LokSim3D content folder + RouteFile = Path.CombineFile(Plugin.FileSystem.LoksimDataDirectory, DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText.TrimStart('\\')); + if (!System.IO.File.Exists(RouteFile)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Kurbuchstrecken file " + DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText + " was not found in Fahrplan " + fileName); + RouteFile = string.Empty; + } + break; + /* + * Brake positions are a letter, which relates somehow to the internal workings of the PZB system + * + * KNOWN POSITION VALUES: + * ---------------------- + * R + * O + * G + * P + */ + case PropsAttribute.BremsArt: + // brake type - PZB related (?) + break; + case PropsAttribute.BremsStellung: + // brake position - Presumably at the start of the game (?) + break; + case PropsAttribute.Zeiten: + // List of times + break; + case PropsAttribute.ZugArt: + // train type + break; + case PropsAttribute.ZugGewicht: + // total weight of train + break; + case PropsAttribute.ZugLaenge: + // total length of train + break; + case PropsAttribute.ZugLimit: + // speed limit for the train + break; + } + } + } + break; + case LoksimNode.PneuBremse: + case LoksimNode.MgBremse: + // Unsupported brake properties + break; + case LoksimNode.Haltestellen: + // List of stations + if (DocumentNodes[0].ChildNodes[i].HasChildNodes) + { + ParseStationList(DocumentNodes[0].ChildNodes[i]); + } + else + { + throw new Exception("Loksim3D: Empty station list in Fahrplan " + fileName); + } + break; + } + } + } + + Kursbuchstrecken = new Kursbuchstrecken(RouteFile); + Kursbuchstrecken.CreateWorld(); + } + + internal void ParseStationList(XmlNode node) + { + Stations = new List(); + for (int i = 0; i < node.ChildNodes.Count; i++) + { + switch (node.ChildNodes[i].Name) + { + case "Halt": + if (node.ChildNodes[i].HasChildNodes) + { + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + Stations.Add(new Station(node.ChildNodes[i].ChildNodes[j])); + break; + } + } + } + break; + default: + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unexpected node " + node.ChildNodes[i].Name + " in Fahrplan station list."); + break; + } + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Fahrplan/Station.cs b/source/Plugins/Route.Loksim/Fahrplan/Station.cs new file mode 100644 index 000000000..65e88f9b8 --- /dev/null +++ b/source/Plugins/Route.Loksim/Fahrplan/Station.cs @@ -0,0 +1,147 @@ +//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.Xml; +using OpenBveApi.Interface; + +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(XmlNode node) + { + if (node.Attributes != null) + { + for (int i = 0; i < node.Attributes.Count; i++) + { + if (!Enum.TryParse(node.Attributes[i].Name, true, out PropsAttribute props)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised Props attribute " +node.Attributes[i].Name + " in station"); + continue; + } + switch (props) + { + case PropsAttribute.Abfahrt: + Arrival = int.Parse(node.Attributes[i].InnerText); + break; + case PropsAttribute.Ankunft: + Departure = int.Parse(node.Attributes[i].InnerText); + break; + case PropsAttribute.Haltepunkt: + if (node.InnerText.ToLowerInvariant() == "true") + { + ShouldStop = true; + } + break; + case PropsAttribute.Bedarfshalt: + if (node.InnerText.ToLowerInvariant() == "true") + { + RequestStop = true; + } + break; + case PropsAttribute.Betriebshalt: + if (node.InnerText.ToLowerInvariant() == "true") + { + NormalStop = true; + } + break; + case PropsAttribute.SignalVorAusfahrt: + if (node.InnerText.ToLowerInvariant() == "true") + { + ForcedRedSignal = true; + } + break; + case PropsAttribute.Haltdauer: + StopTime = int.Parse(node.Attributes[i].InnerText); + break; + case PropsAttribute.Name: + Name = node.Attributes[i].InnerText; + break; + case PropsAttribute.DistanzSoundVorBedarfsHalt: + StopAlarmDistance = int.Parse(node.Attributes[i].InnerText); + break; + case PropsAttribute.SoundAnkunft: + ArrivalSound = node.Attributes[i].InnerText; + break; + case PropsAttribute.SoundAbfahrt: + DepartureSound = node.Attributes[i].InnerText; + break; + case PropsAttribute.SoundAnsage: + // announcement + break; + case PropsAttribute.SoundHalt: + // stop + break; + case PropsAttribute.SoundVorBedarfsHalt: + // stop alarm sound + break; + case PropsAttribute.Tueren: + if (node.InnerText.ToLowerInvariant() == "true") + { + OpenDoors = true; + } + break; + case PropsAttribute.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. + */ + break; + } + } + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.cs b/source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.cs new file mode 100644 index 000000000..6ba680377 --- /dev/null +++ b/source/Plugins/Route.Loksim/Kursbuchstrecken/Connection.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 System; +using System.Xml; +using OpenBveApi.Interface; + +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(XmlNode node, Kursbuchstrecken kursbuchstrecken) + { + if (node.Attributes != null) + { + for (int k = 0; k < node.Attributes.Count; k++) + { + if (!Enum.TryParse(node.Attributes[k].Name, out PropsAttribute props)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unknown attribute " + node.Attributes[k].Name + " encountered in Verbindung."); + } + switch (props) + { + case PropsAttribute.Ende1: + End1 = node.Attributes[k].InnerText.ToLowerInvariant() == "true"; + break; + case PropsAttribute.Ende2: + End2 = node.Attributes[k].InnerText.ToLowerInvariant() == "true"; + break; + case PropsAttribute.Gleis1: + Platform1 = node.Attributes[k].InnerText; + break; + case PropsAttribute.Gleis2: + Platform2 = node.Attributes[k].InnerText; + break; + case PropsAttribute.Strecke1: + if (!kursbuchstrecken.Strecken.ContainsKey(node.Attributes[k].InnerText)) + { + throw new Exception("Loksim3D: Strecke1 was missing from the Kursbuchstecken"); + } + RouteFile1 = node.Attributes[k].InnerText; + break; + case PropsAttribute.Strecke2: + if (!kursbuchstrecken.Strecken.ContainsKey(node.Attributes[k].InnerText)) + { + throw new Exception("Loksim3D: Strecke2 was missing from the Kursbuchstecken"); + } + RouteFile2 = node.Attributes[k].InnerText; + break; + default: + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unexpected attribute " + node.Attributes[k].Name + " encountered in Verbindung."); + break; + } + } + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs b/source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs new file mode 100644 index 000000000..ba5aca46e --- /dev/null +++ b/source/Plugins/Route.Loksim/Kursbuchstrecken/Kursbuchstrecken.cs @@ -0,0 +1,196 @@ +//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.Xml; +using OpenBveApi.Interface; +using OpenBveApi.Math; +using Path = OpenBveApi.Path; + +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) + { + XmlDocument currentXML = new XmlDocument(); + try + { + currentXML.Load(fileName); + } + catch + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Failed to load Kursbuchstrecken " + fileName); + throw; + } + + if (currentXML.DocumentElement != null) + { + XmlNodeList DocumentNodes = currentXML.DocumentElement.SelectNodes("/KBS"); + if (DocumentNodes == null || DocumentNodes.Count == 0) + { + throw new Exception("Loksim3D: No Kursbuchstrecken nodes in file " + fileName); + } + if (DocumentNodes.Count > 1) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Only the first Kursbuchstrecken is supported in " + fileName); + } + + for (int i = 0; i < DocumentNodes[0].ChildNodes.Count; i++) + { + if (!Enum.TryParse(DocumentNodes[0].ChildNodes[i].Name, true, out LoksimNode parsedNode)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised node " + DocumentNodes[0].ChildNodes[i].Name + " in Kursbuchstrecken " + fileName); + continue; + } + switch (parsedNode) + { + case LoksimNode.Props: + // These are present, but appear to contain nothing useful + break; + case LoksimNode.Strecken: + // List of child nodes, each containing a single Props node + if (DocumentNodes[0].ChildNodes[i].HasChildNodes) + { + ParseStrecken(DocumentNodes[0].ChildNodes[i]); + } + else + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Empty list of Strecken in Kursbuchstrecken " + fileName); + } + break; + case LoksimNode.Verbindung: + // List of child nodes, each containing a single Props node + if (DocumentNodes[0].ChildNodes[i].HasChildNodes) + { + ParseVerbindung(DocumentNodes[0].ChildNodes[i]); + } + else + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Empty list of Strecken in Kursbuchstrecken " + fileName); + } + break; + case LoksimNode.StartPunkt: + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].ChildNodes.Count; j++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + StartPoint = new StartPunkt(DocumentNodes[0].ChildNodes[i].ChildNodes[j]); + break; + } + } + break; + } + } + } + } + + private void ParseStrecken(XmlNode node) + { + Strecken = new Dictionary(); + for (int i = 0; i < node.ChildNodes.Count; i++) + { + if (!node.ChildNodes[i].HasChildNodes || node.ChildNodes[i].Name != "Strecke") + { + continue; + } + + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + if (!Enum.TryParse(node.ChildNodes[i].ChildNodes[j].Name, true, out LoksimNode parsedNode)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised node " + node.ChildNodes[i].ChildNodes[j].Name + " in Strecken"); + continue; + } + switch (parsedNode) + { + case LoksimNode.Props: + if (node.ChildNodes[i].ChildNodes[j].Attributes != null) + { + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (node.ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "Name": + string streckeFile = Path.CombineFile(Plugin.FileSystem.LoksimDataDirectory, node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText.TrimStart('\\')); + if (!Strecken.ContainsKey(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText) && File.Exists(streckeFile)) + { + Strecke strecke = new Strecke(streckeFile); + Strecken.Add(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, strecke); + } + break; + } + } + } + break; + default: + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Unexpected node " + node.ChildNodes[i].ChildNodes[j].Name + " encountered in LokSim3D Strecken list."); + break; + } + } + } + } + + private void ParseVerbindung(XmlNode node) + { + Connections = new List(); + for (int i = 0; i < node.ChildNodes.Count; i++) + { + if (!node.ChildNodes[i].HasChildNodes || node.ChildNodes[i].Name != "Name") + { + continue; + } + + for (int j = 0; j < node.ChildNodes[j].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + Connections.Add(new Connection(node.ChildNodes[i].ChildNodes[j], this)); + break; + } + } + } + } + + 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..0d3058158 --- /dev/null +++ b/source/Plugins/Route.Loksim/Rails/L3DRail.cs @@ -0,0 +1,186 @@ +//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 OpenBveApi.Math; +using OpenBveApi.Objects; + +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 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?? + + XmlDocument currentXML = new XmlDocument(); + try + { + currentXML.Load(fileName); + } + catch + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Failed to load L3DRail " + fileName); + throw; + } + + if (currentXML.DocumentElement != null) + { + XmlNodeList DocumentNodes = currentXML.DocumentElement.SelectNodes("/RAIL"); + if (DocumentNodes == null || DocumentNodes.Count == 0) + { + throw new Exception("Loksim3D: No L3DRail nodes in file " + fileName); + } + + if (DocumentNodes.Count > 1) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Only the first L3DRail is supported in " + fileName); + } + + for (int i = 0; i < DocumentNodes[0].ChildNodes.Count; i++) + { + if (!Enum.TryParse(DocumentNodes[0].ChildNodes[i].Name, true, out LoksimNode parsedNode)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised node " + DocumentNodes[0].ChildNodes[i].Name + " in L3DRail " + fileName); + continue; + } + + switch (parsedNode) + { + case LoksimNode.Props: + if (DocumentNodes[0].ChildNodes[i].Attributes != null) + { + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].Attributes.Count; j++) + { + switch (DocumentNodes[0].ChildNodes[i].Attributes[j].Name) + { + case "Transparent": + // appears that pure white is a fixed transparent color if this is set + UsesTransparentColor = DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText == "TRUE"; + break; + case "Spurbreite": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText, out Gauge); + Gauge /= 2; + break; + case "Schienenbreite": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText, out RailTopWidth); + break; + case "Schienenhoehe": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText, out RailTopHeight); + break; + case "Bettungsbreite": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText, out BallastWidth); + break; + } + } + } + break; + case LoksimNode.Hoehe: + // ballast face points, each contains another props Hoehe with Vector3 + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].ChildNodes.Count; j++) + { + for (int k = 0; k < DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "Hoehe": + Vector3.TryParse(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, ';', out Vector3 v); + BallastPoints.Add(v); + break; + } + } + + } + break; + case LoksimNode.TexSchieneOben: + // texture co-ordinates for each segment of the rail top + break; + case LoksimNode.TexSchieneSeite: + // texture co-ordinates for each segment of the rail side + break; + case LoksimNode.TexBettung: + // texture co-ordinates for each segment of the ballast + // (ballast must be a single face) + break; + } + } + } + + Object = new StaticObject(Plugin.CurrentHost); + + MeshBuilder builder = new MeshBuilder(Plugin.CurrentHost); + // rail L + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 0), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 10), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 10), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 0), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 0), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 10), Vector2.Null)); + MeshFace face = new MeshFace(new[] { 0, 1, 2, 3 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + face = new MeshFace(new[] { 2, 3, 4, 5 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + builder.Apply(ref Object); + builder = new MeshBuilder(Plugin.CurrentHost); + // rail R + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 0), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 10), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 10), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 0), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 0), Vector2.Null)); + builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 10), Vector2.Null)); + face = new MeshFace(new[] { 0, 1, 2, 3 }); + face.Flags |= FaceFlags.Face2Mask; + builder.Faces.Add(face); + face = new MeshFace(new[] { 2, 3, 4, 5 }); + 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..0f24132dd --- /dev/null +++ b/source/Plugins/Route.Loksim/Route.LokSim.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {3655732E-5A4F-4D88-B7C0-25BC3043BF8E} + Library + Properties + LokSimRouteParser + Route.Loksim + v4.6.1 + 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 + + + + + + + + + + + \ 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..efebdf55c --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Curve.cs @@ -0,0 +1,116 @@ +//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.Xml; +using OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Curve : TrackElement + { + /// The angle of turn of the curve + internal double Angle; + + internal Curve(XmlNode node) + { + if (node.Attributes == null) + { + return; + } + + for (int i = 0; i < node.Attributes.Count; i++) + { + switch (node.Attributes[i].Name) + { + case "Start": + StartingDistance = double.Parse(node.Attributes[i].InnerText); + break; + case "Ende": + EndingDistance = double.Parse(node.Attributes[i].InnerText); + break; + case "Winkel": + Angle = double.Parse(node.Attributes[i].InnerText); + break; + case "SanfterWinkel": + SoftTransistion = node.Attributes[i].InnerText.ToLowerInvariant() == "true"; + break; + } + } + + // 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..0495dd6c6 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Landschaftsobjekt.cs @@ -0,0 +1,228 @@ +//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.Textures; +using OpenBveApi.World; +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using OpenBveApi.Colors; +using Path = OpenBveApi.Path; + +namespace LokSimRouteParser +{ + internal class LandschaftsObjekt + { + internal StaticObject internalObject; + + internal double TrackPosition; + + internal bool Polygon; + + internal double Scale; + + internal double Distance; + + internal double Length; + + internal LandschaftsObjekt(XmlNode node) + { + // A Landschaftsobjekt is a world ground object + internalObject = new StaticObject(Plugin.CurrentHost); + + string textureFile = string.Empty; + bool shouldTexture = true; + List coordinateList = new List(); + + for (int i = 0; i < node.ChildNodes.Count; i++) + { + switch (node.ChildNodes[i].Name) + { + case "Props": + if (node.ChildNodes[i].Attributes != null) + { + for (int j = 0; j < node.ChildNodes[i].Attributes.Count; j++) + { + switch (node.ChildNodes[i].Attributes[j].Name) + { + case "Abstand": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].Attributes[j].InnerText, out Distance); + break; + case "Position": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].Attributes[j].InnerText, out TrackPosition); + break; + case "Polygon": + Polygon = string.Equals(node.ChildNodes[i].Attributes[j].InnerText, "TRUE", StringComparison.InvariantCultureIgnoreCase); + break; + case "Scale": + // scale controls texture co-ordinates + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].Attributes[j].InnerText, out Scale); + break; + case "TextureFile": + if (string.IsNullOrEmpty(node.ChildNodes[i].Attributes[j].InnerText)) + { + shouldTexture = false; + break; + } + textureFile = Path.CombineFile(Plugin.FileSystem.LoksimDataDirectory, node.ChildNodes[i].Attributes[j].InnerText.TrimStart('\\')); + if (!File.Exists(textureFile)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, true, "LokSim3D: LandschaftsObjekt texture " + node.ChildNodes[i].Attributes[j].InnerText + " was not found."); + // NOT YET RIGHT: SEE NOTES BELOW! + return; + } + + break; + case "Texture": + shouldTexture = string.Equals(node.ChildNodes[i].Attributes[j].InnerText, "TRUE", StringComparison.InvariantCultureIgnoreCase); + break; + case "Zwischenpunkte": + /* + * Intermediate points: + * Curves the object + */ + break; + } + } + } + + break; + case "Landschaftsobjekt": + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + Vector3 coordinates = new Vector3(); + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (node.ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "Hoehe": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out coordinates.Y); + break; + case "Position": + // position of the point relative to the landscape object, e.g. Z + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out coordinates.Z); + Length = Math.Max(coordinates.Z, Length); + break; + case "Verschiebung": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out coordinates.X); + break; + case "Hoehefrei": + // pops the point to the top of the Y stack at Hoehe + break; + } + } + coordinateList.Add(coordinates); + break; + } + + } + + break; + } + } + + /* + * 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) + */ + + 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; + + if (shouldTexture && !string.IsNullOrEmpty(textureFile)) + { + Plugin.CurrentHost.RegisterTexture(textureFile, new TextureParameters(null, null), out material.DaytimeTexture); + } + internalObject.Mesh.Materials = new[] + { + material + }; + + } + + internal void Create(TrackFollower t) + { + t.UpdateAbsolute(TrackPosition, true, false); + Vector3 fPos = t.WorldPosition; + t.UpdateRelative(Length, true, false); + Vector3 rPos = t.WorldPosition; + Vector3 d = fPos == rPos ? fPos : new Vector3(rPos - fPos); + 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); + Transformation tr = new Transformation(d, u, s); + // + internalObject.CreateObject(fPos, tr, 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..5d9dde510 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/ObjectGroup.cs @@ -0,0 +1,444 @@ +//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; +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 System.Xml; + +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(XmlNode node) + { + // An ObjectGroup [Object in the XML] is somewhere roughly analogous to a world tile + Objects = new List(); + for (int i = 0; i < node.ChildNodes.Count; i++) + { + switch (node.ChildNodes[i].Name) + { + case "Props": + if (node.ChildNodes[i].Attributes != null) + { + for (int j = 0; j < node.ChildNodes[i].Attributes.Count; j++) + { + switch (node.ChildNodes[i].Attributes[j].Name) + { + case "Position": + TrackPosition = double.Parse(node.ChildNodes[i].Attributes[j].InnerText); + break; + case "Offset": + // We get an initial position to looking ahead on the transformed track + // Then add an offset to get our final WPos + Vector3.TryParse(node.ChildNodes[i].Attributes[j].InnerText, ';', out OffsetPosition); + break; + case "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) + */ + break; + case "RollMaterial": + // Set to true if this ObjectGroup is another train + break; + case "Name": + Name = node.ChildNodes[i].Attributes[j].InnerText; + break; + case "DynamicVisibility": + break; + case "FixedDynamicVisibility": + break; + case "StreckenAbhaengigkeit": + /* + * Route Dependency: + * ------------------ + * 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 + */ + break; + } + } + } + break; + case "Eintrag": + Objects.Add(new Object(node.ChildNodes[i])); + 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 + { + internal double Distance; + internal int NumberOfObjects; + internal bool FollowsTrackHeight; + internal bool FollowsLandHeight; + internal UnifiedObject UnifiedObject; + internal Vector3 Offset; + internal double Position; + internal Vector3 Rotation; + internal bool FarVisible; + internal LightStates Lighting; + internal Dictionary Properties; + internal LoksimRandom Random; + + + internal Object(XmlNode node) + { + for (int i = 0; i < node.ChildNodes.Count; i++) + { + if (!Enum.TryParse(node.ChildNodes[i].Name, out LoksimNode parsedNode)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised node " + node.ChildNodes[i].Name + " in LokSim3D Eintrag"); + } + switch (parsedNode) + { + case LoksimNode.Props: + if (node.ChildNodes[i].Attributes == null) + { + break; + } + + for (int j = 0; j < node.ChildNodes[i].Attributes.Count; j++) + { + if (!Enum.TryParse(node.ChildNodes[i].Attributes[j].Name, out PropsAttribute props)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised Props attribute " + node.ChildNodes[i].Attributes[j].Name + " in LokSim3D Eintrag"); + continue; + } + switch (props) + { + case PropsAttribute.Abstand: + // when repetition is used, distance to next object + Distance = double.Parse(node.ChildNodes[i].Attributes[j].InnerText); + break; + case PropsAttribute.Anzahl: + // number of objects to place + NumberOfObjects = int.Parse(node.ChildNodes[i].Attributes[j].InnerText); + break; + case PropsAttribute.Datei: + // path to the .l3dgrp or .l3dobj + if (Plugin.previewOnly) + { + break; + } + // Path relative to the LokSim3D content folder + if (string.IsNullOrEmpty(node.ChildNodes[i].Attributes[j].InnerText)) + { + break; + } + string obj = Path.CombineFile(Plugin.FileSystem.LoksimDataDirectory, node.ChildNodes[i].Attributes[j].InnerText.Trim('\\')); + if (Plugin.ObjectCache.TryGetValue(obj, out UnifiedObject)) + { + break; + } + + if (System.IO.File.Exists(obj)) + { + Plugin.CurrentHost.LoadObject(obj, Encoding.UTF8, out UnifiedObject); + Plugin.ObjectCache.Add(obj, UnifiedObject); + } + else + { + Plugin.CurrentHost.AddMessage(MessageType.Error, true, "LokSim3D: Object " + node.ChildNodes[i].Attributes[j].InnerText + " was not found."); + } + break; + case PropsAttribute.GleisHoeheFolgen: + // + if (node.ChildNodes[i].Attributes[j].InnerText == "TRUE") + { + FollowsTrackHeight = true; + } + break; + case PropsAttribute.HoeheLand: + if (node.ChildNodes[i].Attributes[j].InnerText == "TRUE") + { + FollowsLandHeight = true; + } + break; + case PropsAttribute.Position: + // Z position of the object + Position = double.Parse(node.ChildNodes[i].Attributes[j].InnerText); + break; + case PropsAttribute.Verschiebung: + // X,Y,Z position from position + Vector3.TryParse(node.ChildNodes[i].Attributes[j].InnerText, ';', out Offset); + break; + case PropsAttribute.WeitSichtbar: + // used by the Loksim renderer + if (node.ChildNodes[i].Attributes[j].InnerText == "TRUE") + { + FarVisible = true; + } + break; + case PropsAttribute.Winkel: + // X,Y,Z rotation + Vector3.TryParse(node.ChildNodes[i].Attributes[j].InnerText, ';', out Rotation); + break; + } + } + break; + case LoksimNode.Eigenschaften: + /* + * Characteristics: + * This contains a single Props node + * + * The name of the each attribute within this Props node is passed into the .l3dgrp and subsequent .l3dobj + * files, and the value is passed as a variable to be returned using for use in functions etc. + * via the GroupProperty attribute. + * + */ + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + Properties = new Dictionary(); + + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + /* + * DEFAULT PROPERTIES + * ------------------ + * STRECKENHEKTOMETER => Route 100m distance + * STRECKENMETER => Route distance + * WEICHENSTELLUNG => Setting of the associated weiche + */ + Properties.Add(node.ChildNodes[i].ChildNodes[j].Attributes[k].Name, node.ChildNodes[i].ChildNodes[j].Attributes[k].Value); + } + break; + } + } + break; + case LoksimNode.Lightning: + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + Lighting = new LightStates(node.ChildNodes[i].ChildNodes[j]); + break; + } + } + break; + case LoksimNode.Random: + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + Random = new LoksimRandom(node.ChildNodes[i].ChildNodes[j]); + break; + } + } + break; + } + } + } + + internal void Create(TrackFollower t, 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(t.WorldDirection, t.WorldUp, t.WorldSide); + Vector3 objectWpos = t.WorldPosition + o; + Transformation tr = new Transformation(t.WorldDirection, t.WorldUp, t.WorldSide); + UnifiedObject.CreateObject(objectWpos, tr, new Transformation(Rotation.X.ToRadians(), Rotation.Y.ToRadians(), Rotation.Z.ToRadians()), new ObjectCreationParameters(t.TrackPosition, t.TrackPosition, t.TrackPosition + Distance)); + } + else + { + for (int i = 0; i < NumberOfObjects; i++) + { + + Vector3 objectWpos; + Vector3 repetitionPosition = Vector3.Zero; + if (i != 0) + { + t.UpdateRelative(Distance, true, false); + } + + Vector3 oC = new Vector3(o); + oC.Rotate(t.WorldDirection, t.WorldUp, t.WorldSide); + objectWpos = t.WorldPosition + oC; + + //Vector3 objectWpos = t.WorldPosition + o; + //Vector3 repetitionPosition = new Vector3(0, 0, i * Distance); + if (Random != null) + { + repetitionPosition += Random.GetPosition(); + } + repetitionPosition.Rotate(t.WorldDirection, t.WorldUp, t.WorldSide); + Transformation tr = new Transformation(t.WorldDirection, t.WorldUp, t.WorldSide); + UnifiedObject.CreateObject(objectWpos + repetitionPosition, tr, new Transformation(Rotation.X.ToRadians(), Rotation.Y.ToRadians(), Rotation.Z.ToRadians()), new ObjectCreationParameters(t.TrackPosition + i * Distance, t.TrackPosition + i * Distance, t.TrackPosition + i * Distance)); + } + + } + + } + } + } + + internal class LoksimRandom + { + 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(XmlNode node) + { + if (node.Attributes != null) + { + for (int i = 0; i < node.Attributes.Count; i++) + { + switch (node.Attributes[i].Name) + { + case "SRAND": + // random seed + NumberFormats.TryParseIntVb6(node.Attributes[i].InnerText, out Seed); + break; + case "XValue": + // plus / minus X axis + NumberFormats.TryParseDoubleVb6(node.Attributes[i].InnerText, out XValue); + break; + case "YValue": + // plus / minus Y axis + NumberFormats.TryParseDoubleVb6(node.Attributes[i].InnerText, out YValue); + break; + case "ZValue": + // plus / minus Z axis + NumberFormats.TryParseDoubleVb6(node.Attributes[i].InnerText, out ZValue); + break; + } + } + } + random = new Random(Seed); + + } + + internal Vector3 GetPosition() + { + double randomResult = random.NextDouble(); + // random result returns zero plus / minus the value + Vector3 v = new Vector3(XValue - randomResult * XValue * 2, YValue - randomResult * YValue * 2, ZValue - randomResult * ZValue * 2); + return v; + } + + + } + + internal class LightStates + { + internal double Day; + internal double Night; + internal bool TrainLit; + internal LightStates(XmlNode node) + { + if (node.Attributes == null) + { + return; + } + + for (int i = 0; i < node.Attributes.Count; i++) + { + switch (node.Attributes[i].Name) + { + case "DayLight": + // Going to be roughly analogous to the BVE DNB values + Day = double.Parse(node.Attributes[i].InnerText); + break; + case "NightLight": + // Going to be roughly analogous to the BVE DNB values + Night = double.Parse(node.Attributes[i].InnerText); + break; + case "LokLight": + // Whether object is lit by light emitted by trains + if (node.Attributes[i].InnerText == "TRUE") + { + TrainLit = true; + } + break; + } + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Pitch.cs b/source/Plugins/Route.Loksim/Strecken/Pitch.cs new file mode 100644 index 000000000..8ba124cac --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Pitch.cs @@ -0,0 +1,100 @@ +//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 OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Pitch : TrackElement + { + internal double PerThousand; + + internal Pitch(XmlNode node) + { + if (node.Attributes == null) + { + return; + } + + for (int i = 0; i < node.Attributes.Count; i++) + { + switch (node.Attributes[i].Name) + { + case "Start": + StartingDistance = double.Parse(node.Attributes[i].InnerText); + break; + case "Ende": + EndingDistance = double.Parse(node.Attributes[i].InnerText); + break; + case "Promille": + PerThousand = double.Parse(node.Attributes[i].InnerText); + break; + case "SanfterWinkel": + SoftTransistion = node.Attributes[i].InnerText.ToLowerInvariant() == "true"; + break; + } + } + } + + 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..efe43f597 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/StartPunkt.cs @@ -0,0 +1,77 @@ +//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.Xml; +using OpenBveApi.Interface; + +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(XmlNode node) + { + if (node.Attributes == null) + { + return; + } + + for (int i = 0; i < node.Attributes.Count; i++) + { + if (!Enum.TryParse(node.Attributes[i].Name, true, out PropsAttribute props)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised Props attribute " +node.Attributes[i].Name + " in StartPunkt"); + continue; + } + switch (props) + { + case PropsAttribute.Gleis: + Track = node.Attributes[i].InnerText; + break; + case PropsAttribute.Pos: + Position = double.Parse(node.Attributes[i].InnerText); + break; + case PropsAttribute.Richtung: + if (node.Attributes[i].InnerText == "TRUE") + { + Reverse = true; + } + break; + case PropsAttribute.Strecke: + RouteFile = node.Attributes[i].InnerText; + break; + } + } + } + } +} diff --git a/source/Plugins/Route.Loksim/Strecken/Strecke.cs b/source/Plugins/Route.Loksim/Strecken/Strecke.cs new file mode 100644 index 000000000..1c3e4f795 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Strecke.cs @@ -0,0 +1,146 @@ +//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 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) + { + 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 + string f = File.ReadAllText(fileName); + f = f.Replace("Fahrt Gegenrichtung", "FahrtGegenrichtung"); + f = f.Replace("Fahrt Richtung", "FahrtRichtung"); + currentXML.LoadXml(f); + } + catch + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Failed to load Strecke " + fileName); + throw; + } + + if (currentXML.DocumentElement != null) + { + XmlNodeList DocumentNodes = currentXML.DocumentElement.SelectNodes("/STRECKE"); + if (DocumentNodes == null || DocumentNodes.Count == 0) + { + throw new Exception("Loksim3D: No Strecke nodes in file " + fileName); + } + + if (DocumentNodes.Count > 1) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Only the first Strecke is supported in " + fileName); + } + + for (int i = 0; i < DocumentNodes[0].ChildNodes.Count; i++) + { + switch (DocumentNodes[0].ChildNodes[i].Name) + { + case "Props": + // These are present, but the info in here isn't useful at present + break; + case "DefaulRail": + /* + * The default rail appearance + * From the files, LokSim seems to auto-generate the rail object somehow... + */ + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].ChildNodes.Count; j++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + for (int k = 0; k < DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "File": + string railFile = Path.CombineFile(Plugin.FileSystem.LoksimDataDirectory, DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText.TrimStart('\\')); + L3DRail rail = new L3DRail(railFile); + DefaultRail = rail.Object; + break; + } + } + break; + } + } + break; + case "StdTexture": + // Unknown - Empty in demo routes + break; + case "Himmelsrichtung": + // The direction of travel (construction) for this routefile + if (!string.IsNullOrEmpty(DocumentNodes[0].ChildNodes[i].InnerText)) + { + Direction = int.Parse(DocumentNodes[0].ChildNodes[i].InnerText); + } + break; + case "GLEIS": + // Creates a track + // NOTE: Names are unique per Strecke, but a route can contain multiple Strecke with the same track keys + Track newTrack = new Track(DocumentNodes[0].ChildNodes[i], this); + Tracks.Add(newTrack.Name, newTrack); + break; + } + } + } + } + + 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..fed9b5de3 --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Track.cs @@ -0,0 +1,437 @@ +//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.Configuration; +using System.Xml; +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 StartPos; + + internal Strecke Strecke; + + internal Guid Guid; + + internal Track(XmlNode node, Strecke strecke) + { + if (!node.HasChildNodes) + { + Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "Loksim3D: Empty Gleis encountered in Strecke "); + return; + } + + 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(); + + for (int i = 0; i < node.ChildNodes.Count; i++) + { + switch (node.ChildNodes[i].Name) + { + case "Props": + if (node.ChildNodes[i].Attributes == null) + { + break; + } + + for (int j = 0; j < node.ChildNodes[i].Attributes.Count; j++) + { + if (!Enum.TryParse(node.ChildNodes[i].Attributes[j].Name, true, out PropsAttribute props)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: Unrecognised Props attribute " + node.ChildNodes[i].Attributes[j].Name + " in StartPunkt"); + continue; + } + switch (props) + { + case PropsAttribute.Name: + Name = node.ChildNodes[i].Attributes[j].InnerText; + break; + } + } + break; + case "TOPOLOGIE": + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Erstellung": + // Creation parameters + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].ChildNodes.Count; k++) + { + switch (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Name) + { + case "Props": + if (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes == null) + { + break; + } + for (int l = 0; l < node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes.Count; l++) + { + switch (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].Name) + { + case "EndPunkt": + // end point (M) + break; + case "ParallelGleis": + // track to parallel + ParallelTrack = node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText; + if (!string.IsNullOrEmpty(ParallelTrack) && !Strecke.Tracks.ContainsKey(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: The track to parallel with key " + ParallelTrack + " was not found."); + } + + break; + case "ParallelStart": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText, out ParallelStart); + break; + case "ParallelEnd": + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText, out ParallelEnd); + break; + case "ParallelAbstand": + // if TYP is set to parallel then distance from ParallelGleis + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText, out ParallelOffset); + break; + case "StartPos": + // starting position (M) + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText, out StartPos); + break; + case "StartPunkt": + // starting point on world tile + Vector3.TryParse(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText.Split(';'), out StartPoint); + break; + case "Typ": + if (!Enum.TryParse(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText, true, out Type)) + { + Plugin.CurrentHost.AddMessage(MessageType.Warning, false, "Loksim3D: Unknown track type " + node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText); + } + break; + case "WeicheEnd": + // switch track ON END (?) + break; + case "WeicheGleis": + // switch to track key + WeicheTrack = node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText; + if (!string.IsNullOrEmpty(WeicheTrack) && !Strecke.Tracks.ContainsKey(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText)) + { + Plugin.CurrentHost.AddMessage(MessageType.Error, false, "Loksim3D: The switch track with key " + WeicheTrack + " was not found."); + } + break; + case "WeicheGleisPos": + // switch to track defined by WeicheGleis at position (M) + NumberFormats.TryParseDoubleVb6(node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes[l].InnerText, out WeichePos); + break; + case "WeicheGleisRichtung": + // switch direction (? display on HUD, switch generation ?) + break; + case "WeicheName": + // switch name + break; + case "WeicheStart": + break; + case "WeichenWinkel": + break; + case "Winkel": + break; + + } + } + break; + } + } + break; + case "KURVE": + case "STEIGUNG": + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].ChildNodes.Count; k++) + { + switch (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Name) + { + case "Props": + if (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes == null) + { + break; + } + + if (node.ChildNodes[i].ChildNodes[j].Name == "STEIGUNG") + { + Elements.Add(new Pitch(node.ChildNodes[i].ChildNodes[j].ChildNodes[k])); + } + else + { + Elements.Add(new Curve(node.ChildNodes[i].ChildNodes[j].ChildNodes[k])); + } + break; + } + } + break; + case "Weiche": + for (int k = 0; k < node.ChildNodes[i].ChildNodes[j].ChildNodes.Count; k++) + { + switch (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Name) + { + case "Props": + if (node.ChildNodes[i].ChildNodes[j].ChildNodes[k].Attributes == null) + { + break; + } + + if (node.ChildNodes[i].ChildNodes[j].Name == "Weiche") + { + Elements.Add(new Weiche(node.ChildNodes[i].ChildNodes[j].ChildNodes[k])); + } + break; + } + } + break; + case "Ausweiche": + // avoidance? + break; + } + } + break; + case "Objecte": + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Object": + // objects + Objects.Add(new ObjectGroup(node.ChildNodes[i].ChildNodes[j])); + break; + } + } + break; + case "Landschaft": + for (int j = 0; j < node.ChildNodes[i].ChildNodes.Count; j++) + { + switch (node.ChildNodes[i].ChildNodes[j].Name) + { + case "Landschaftsobjekt": + // ground objects + Landschaftsobjekt.Add(new LandschaftsObjekt(node.ChildNodes[i].ChildNodes[j])); + break; + } + } + 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); + + string groundFile = Path.CombineFile(Plugin.FileSystem.GetDataFolder(new[] { "Compatibility" }),"Loksim\\Grass.csv"); + + Plugin.CurrentHost.LoadObject(groundFile, System.Text.Encoding.UTF8, out UnifiedObject ground); + + 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/Weiche.cs b/source/Plugins/Route.Loksim/Strecken/Weiche.cs new file mode 100644 index 000000000..19146a79b --- /dev/null +++ b/source/Plugins/Route.Loksim/Strecken/Weiche.cs @@ -0,0 +1,71 @@ +//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 OpenBveApi.Math; + +namespace LokSimRouteParser +{ + internal class Weiche : TrackElement + { + internal Weiche(XmlNode node) + { + if (node.Attributes == null) + { + return; + } + + for (int i = 0; i < node.Attributes.Count; i++) + { + switch (node.Attributes[i].Name) + { + case "Position": + // single position, minor hack + StartingDistance = double.Parse(node.Attributes[i].InnerText); + break; + } + } + } + + 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 b718bd574..5eb560113 100644 --- a/source/RouteViewer/ProgramR.cs +++ b/source/RouteViewer/ProgramR.cs @@ -524,7 +524,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(); From 62d13983211040f668484f9fa4703e9b41f6dd44 Mon Sep 17 00:00:00 2001 From: Christopher Lees Date: Wed, 24 Jun 2026 16:01:44 +0100 Subject: [PATCH 2/3] Build rails --- source/Plugins/Route.Loksim/Rails/L3DRail.cs | 202 +++++++++++++++++-- 1 file changed, 185 insertions(+), 17 deletions(-) diff --git a/source/Plugins/Route.Loksim/Rails/L3DRail.cs b/source/Plugins/Route.Loksim/Rails/L3DRail.cs index 0d3058158..fba309e62 100644 --- a/source/Plugins/Route.Loksim/Rails/L3DRail.cs +++ b/source/Plugins/Route.Loksim/Rails/L3DRail.cs @@ -26,8 +26,10 @@ using System; using System.Collections.Generic; using System.Xml; +using OpenBveApi; using OpenBveApi.Math; using OpenBveApi.Objects; +using OpenBveApi.Textures; namespace LokSimRouteParser { @@ -47,6 +49,14 @@ internal class L3DRail internal StaticObject Object; + internal Vector2[] railTopTextureCoordinates = new Vector2[2]; + + internal Vector2[] railSideTextureCoordinates = new Vector2[2]; + + internal Vector2 textureSize; + + internal string textureFile; + internal L3DRail(string fileName) { // https://eu07.pl/forum/showthread.php?tid=6244&page=2 @@ -114,6 +124,12 @@ internal L3DRail(string fileName) case "Bettungsbreite": NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText, out BallastWidth); break; + case "Texture": + textureFile = Path.Loksim3D.CombineFile(Path.GetDirectoryName(fileName), DocumentNodes[0].ChildNodes[i].Attributes[j].InnerText, string.Empty); + Plugin.CurrentHost.QueryTextureDimensions(textureFile, out int tx, out int ty); + textureSize.X = tx; + textureSize.Y = ty; + break; } } } @@ -137,9 +153,67 @@ internal L3DRail(string fileName) break; case LoksimNode.TexSchieneOben: // texture co-ordinates for each segment of the rail top + if (DocumentNodes[0].ChildNodes[i].HasChildNodes) + { + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].ChildNodes.Count; j++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + for (int k = 0; k < DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "x1": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railTopTextureCoordinates[0].X); + break; + case "x2": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railTopTextureCoordinates[1].X); + break; + case "y1": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railTopTextureCoordinates[0].Y); + break; + case "y2": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railTopTextureCoordinates[1].Y); + break; + } + } + break; + } + } + } break; case LoksimNode.TexSchieneSeite: // texture co-ordinates for each segment of the rail side + if (DocumentNodes[0].ChildNodes[i].HasChildNodes) + { + for (int j = 0; j < DocumentNodes[0].ChildNodes[i].ChildNodes.Count; j++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Name) + { + case "Props": + for (int k = 0; k < DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes.Count; k++) + { + switch (DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].Name) + { + case "x1": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railSideTextureCoordinates[0].X); + break; + case "x2": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railSideTextureCoordinates[1].X); + break; + case "y1": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railSideTextureCoordinates[0].Y); + break; + case "y2": + NumberFormats.TryParseDoubleVb6(DocumentNodes[0].ChildNodes[i].ChildNodes[j].Attributes[k].InnerText, out railSideTextureCoordinates[1].Y); + break; + } + } + break; + } + } + } break; case LoksimNode.TexBettung: // texture co-ordinates for each segment of the ballast @@ -150,36 +224,130 @@ internal L3DRail(string fileName) } Object = new StaticObject(Plugin.CurrentHost); - + MeshBuilder builder = new MeshBuilder(Plugin.CurrentHost); - // rail L - builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 0), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 10), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 10), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 0), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 0), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 10), Vector2.Null)); + // 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[] { 2, 3, 4, 5 }); + 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); - // rail R - builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 0), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 10), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 10), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 0), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 0), Vector2.Null)); - builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 10), Vector2.Null)); + 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[] { 2, 3, 4, 5 }); + 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 }; + for (int i = 0; i < BallastPoints.Count; i++) + { + Vector3 point = BallastPoints[i]; + if (i % 2 != 0) + { + point.Z += 10; + builder.Vertices.Add(new Vertex(point, Vector2.Null)); + ballast.Add(builder.Vertices.Count - 1); + point.Z -= 10; + } + else + { + builder.Vertices.Add(new Vertex(point, Vector2.Null)); + ballast.Add(builder.Vertices.Count - 1); + point.Z += 10; + } + + 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]; + point.X = -point.X; + if (i % 2 != 0) + { + + point.Z += 10; + builder.Vertices.Add(new Vertex(point, Vector2.Null)); + ballast.Add(builder.Vertices.Count - 1); + point.Z -= 10; + } + else + { + builder.Vertices.Add(new Vertex(point, Vector2.Null)); + ballast.Add(builder.Vertices.Count - 1); + point.Z += 10; + } + + 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); + + face = new MeshFace(sleeperBase); face.Flags |= FaceFlags.Face2Mask; builder.Faces.Add(face); + builder.Apply(ref Object); } } From db7d06c6e6d62d04078e8723073117ac5c33f98b Mon Sep 17 00:00:00 2001 From: Christopher Lees Date: Thu, 25 Jun 2026 11:47:53 +0100 Subject: [PATCH 3/3] Case fix --- .../Strecken/{Landschaftsobjekt.cs => LandschaftsObjekt.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename source/Plugins/Route.Loksim/Strecken/{Landschaftsobjekt.cs => LandschaftsObjekt.cs} (100%) diff --git a/source/Plugins/Route.Loksim/Strecken/Landschaftsobjekt.cs b/source/Plugins/Route.Loksim/Strecken/LandschaftsObjekt.cs similarity index 100% rename from source/Plugins/Route.Loksim/Strecken/Landschaftsobjekt.cs rename to source/Plugins/Route.Loksim/Strecken/LandschaftsObjekt.cs