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..fba309e62
--- /dev/null
+++ b/source/Plugins/Route.Loksim/Rails/L3DRail.cs
@@ -0,0 +1,354 @@
+//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;
+using OpenBveApi.Math;
+using OpenBveApi.Objects;
+using OpenBveApi.Textures;
+
+namespace LokSimRouteParser
+{
+ internal class L3DRail
+ {
+ internal double Gauge;
+
+ internal double RailTopWidth;
+
+ internal double RailTopHeight;
+
+ internal double BallastWidth;
+
+ internal bool UsesTransparentColor;
+
+ internal List BallastPoints = new List();
+
+ internal StaticObject Object;
+
+ internal Vector2[] railTopTextureCoordinates = new Vector2[2];
+
+ internal Vector2[] railSideTextureCoordinates = new Vector2[2];
+
+ internal Vector2 textureSize;
+
+ internal string textureFile;
+
+ internal L3DRail(string fileName)
+ {
+ // https://eu07.pl/forum/showthread.php?tid=6244&page=2
+ // rail is generated in 10m lengths
+ // follows that the texture co-ordinates must be relative to said 10m
+ // unsure as yet as to how switches are generated
+ // must do some twiddling to sort for radius
+ // It might be easier if we just generate a ~0.5m segment and use this??
+
+ 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;
+ 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;
+ }
+ }
+ }
+ 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
+ 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
+ // (ballast must be a single face)
+ break;
+ }
+ }
+ }
+
+ Object = new StaticObject(Plugin.CurrentHost);
+
+ MeshBuilder builder = new MeshBuilder(Plugin.CurrentHost);
+ // create material to be used
+ Material material = new Material(textureFile);
+ builder.Materials[0] = material;
+
+ // build rail models
+ // NOTE: This can definitely be optimised, but not doing that at the minute
+
+ // rail side L
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y)));
+
+ // rail top L
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge - RailTopWidth, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(-Gauge, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y)));
+ MeshFace face = new MeshFace(new[] { 0, 1, 2, 3 });
+ face.Flags |= FaceFlags.Face2Mask;
+ builder.Faces.Add(face);
+ face = new MeshFace(new[] { 4, 5, 6, 7 });
+ face.Flags |= FaceFlags.Face2Mask;
+ builder.Faces.Add(face);
+ builder.Apply(ref Object);
+ builder = new MeshBuilder(Plugin.CurrentHost);
+ builder.Materials[0] = material;
+ // rail side R
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 0), new Vector2(railSideTextureCoordinates[0].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 10), new Vector2(railSideTextureCoordinates[1].X / textureSize.X, railSideTextureCoordinates[1].Y / textureSize.Y)));
+ // rail top R
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 0), new Vector2(railTopTextureCoordinates[0].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge + RailTopWidth, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[0].Y / textureSize.Y)));
+ builder.Vertices.Add(new Vertex(new Vector3(Gauge, 0.2 + RailTopHeight, 10), new Vector2(railTopTextureCoordinates[1].X / textureSize.X, railTopTextureCoordinates[1].Y / textureSize.Y)));
+ face = new MeshFace(new[] { 0, 1, 2, 3 });
+ face.Flags |= FaceFlags.Face2Mask;
+ builder.Faces.Add(face);
+ face = new MeshFace(new[] { 4, 5, 6, 7 });
+ face.Flags |= FaceFlags.Face2Mask;
+ builder.Faces.Add(face);
+
+ builder.Apply(ref Object);
+ builder = new MeshBuilder(Plugin.CurrentHost);
+ builder.Materials[0] = material;
+
+ // build ballast model:
+ // points are those of *half* an end profile in clockwise direction
+ // e.g.
+ // *-----*
+ // \
+ // \
+ // *
+ // |
+ // |
+ // *
+ // we then negate the X co-ordinate to build the other side
+ // TexBettung appears to be the rectangle across the whole cross-section
+ // current assumption is that our X-coords are proportional to the distance in X axis
+
+ List ballast = new List();
+ int[] sleeperBase = { 0,1,0,0 };
+ 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);
+ }
+ }
+}
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