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