Exemple #1
0
        /// <summary>Parses a track following object</summary>
        /// <param name="FileName">The XML file to parse</param>
        internal static TrainManager.TrackFollowingObject ParseTrackFollowingObject(string FileName, string ObjectPath)
        {
            // The current XML file to load
            XDocument CurrentXML = XDocument.Load(FileName, LoadOptions.SetLineInfo);
            string    Path       = System.IO.Path.GetDirectoryName(FileName);

            // Check for null
            if (CurrentXML.Root == null)
            {
                // We couldn't find any valid XML, so return false
                throw new System.IO.InvalidDataException();
            }

            IEnumerable <XElement> DocumentElements = CurrentXML.Root.Elements("TrackFollowingObject");

            // Check this file actually contains OpenBVE other train definition elements
            if (DocumentElements == null || !DocumentElements.Any())
            {
                // We couldn't find any valid XML, so return false
                throw new System.IO.InvalidDataException();
            }

            TrainManager.TrackFollowingObject Train = new TrainManager.TrackFollowingObject(TrainState.Pending);

            foreach (XElement Element in DocumentElements)
            {
                ParseTrackFollowingObjectNode(Element, FileName, Path, ObjectPath, Train);
            }

            return(Train);
        }
Exemple #2
0
        /// <summary>Parses a track following object</summary>
        /// <param name="ObjectPath">Absolute path to the object folder of route data</param>
        /// <param name="FileName">The XML file to parse</param>
        internal static TrainManager.TrackFollowingObject ParseTrackFollowingObject(string ObjectPath, string FileName)
        {
            // The current XML file to load
            XDocument       CurrentXML  = XDocument.Load(FileName, LoadOptions.SetLineInfo);
            List <XElement> TfoElements = CurrentXML.XPathSelectElements("/openBVE/TrackFollowingObject").ToList();

            // Check this file actually contains OpenBVE other train definition elements
            if (!TfoElements.Any())
            {
                // We couldn't find any valid XML, so return false
                throw new InvalidDataException();
            }

            TrainManager.TrackFollowingObject Train = new TrainManager.TrackFollowingObject(TrainState.Pending);

            foreach (XElement Element in TfoElements)
            {
                ParseTrackFollowingObjectNode(ObjectPath, FileName, Element, Train);
            }

            return(Train);
        }
Exemple #3
0
 internal TrackFollowingObjectAI(TrainManager.TrackFollowingObject train, TravelData[] data)
 {
     Train             = train;
     Data              = data;
     TimeLastProcessed = 0.0;
 }
Exemple #4
0
        /// <summary>Parses a base track following object node</summary>
        /// <param name="Element">The XElement to parse</param>
        /// <param name="FileName">The filename of the containing XML file</param>
        /// <param name="Path">The path of the containing XML file</param>
        /// <param name="Train">The track following object to parse this node into</param>
        private static void ParseTrackFollowingObjectNode(XElement Element, string FileName, string Path, string ObjectPath, TrainManager.TrackFollowingObject Train)
        {
            System.Globalization.CultureInfo Culture = System.Globalization.CultureInfo.InvariantCulture;
            string TrainDirectory       = string.Empty;
            bool   consistReversed      = false;
            List <Game.TravelData> Data = new List <Game.TravelData>();

            foreach (XElement SectionElement in Element.Elements())
            {
                string Section = SectionElement.Name.LocalName;

                switch (SectionElement.Name.LocalName.ToLowerInvariant())
                {
                case "stops":
                    ParseStopNode(SectionElement, FileName, Data);
                    break;

                case "train":
                    foreach (XElement KeyNode in SectionElement.Elements())
                    {
                        string Key        = KeyNode.Name.LocalName;
                        string Value      = KeyNode.Value;
                        int    LineNumber = ((IXmlLineInfo)KeyNode).LineNumber;

                        switch (Key.ToLowerInvariant())
                        {
                        case "directory":
                        {
                            string trainDirectory = OpenBveApi.Path.CombineDirectory(Path, Value);
                            if (!System.IO.Directory.Exists(trainDirectory))
                            {
                                trainDirectory = OpenBveApi.Path.CombineFile(Program.FileSystem.InitialTrainFolder, Value);
                            }
                            if (!System.IO.Directory.Exists(trainDirectory))
                            {
                                trainDirectory = OpenBveApi.Path.CombineFile(Program.FileSystem.TrainInstallationDirectory, Value);
                            }
                            if (!System.IO.Directory.Exists(trainDirectory))
                            {
                                trainDirectory = OpenBveApi.Path.CombineFile(ObjectPath, Value);
                            }

                            if (!System.IO.Directory.Exists(trainDirectory))
                            {
                                Interface.AddMessage(MessageType.Error, false, "Directory was not found in " + Key + " in " + Section + " at line " + LineNumber.ToString(Culture) + " in " + FileName);
                            }
                            else
                            {
                                TrainDirectory = trainDirectory;
                            }
                        }
                        break;

                        case "reversed":
                            int n;
                            NumberFormats.TryParseIntVb6(Value, out n);
                            if (n == 1 || Value.ToLowerInvariant() == "true")
                            {
                                consistReversed = true;
                            }
                            break;
                        }
                    }
                    break;

                case "definition":
                    foreach (XElement KeyNode in SectionElement.Elements())
                    {
                        string Key        = KeyNode.Name.LocalName;
                        string Value      = KeyNode.Value;
                        int    LineNumber = ((IXmlLineInfo)KeyNode).LineNumber;

                        switch (Key.ToLowerInvariant())
                        {
                        case "appearancetime":
                            if (Value.Length != 0 && !Interface.TryParseTime(Value, out Train.AppearanceTime))
                            {
                                Interface.AddMessage(MessageType.Error, false, "Value is invalid in " + Key + " in " + Section + " at line " + LineNumber.ToString(Culture) + " in " + FileName);
                            }
                            break;

                        case "appearancestartposition":
                            if (Value.Length != 0 && !NumberFormats.TryParseDoubleVb6(Value, out Train.AppearanceStartPosition))
                            {
                                Interface.AddMessage(MessageType.Error, false, "Value is invalid in " + Key + " in " + Section + " at line " + LineNumber.ToString(Culture) + " in " + FileName);
                            }
                            break;

                        case "appearanceendposition":
                            if (Value.Length != 0 && !NumberFormats.TryParseDoubleVb6(Value, out Train.AppearanceEndPosition))
                            {
                                Interface.AddMessage(MessageType.Error, false, "Value is invalid in " + Key + " in " + Section + " at line " + LineNumber.ToString(Culture) + " in " + FileName);
                            }
                            break;

                        case "leavetime":
                            if (Value.Length != 0 && !Interface.TryParseTime(Value, out Train.LeaveTime))
                            {
                                Interface.AddMessage(MessageType.Error, false, "Value is invalid in " + Key + " in " + Section + " at line " + LineNumber.ToString(Culture) + " in " + FileName);
                            }
                            break;
                        }
                    }
                    break;
                }
            }

            if (!Data.Any() || string.IsNullOrEmpty(TrainDirectory))
            {
                return;
            }

            /*
             * First check for a train.ai file- Functionally identical, but allows for differently configured AI
             * trains not to show up as driveable
             */
            string TrainData = OpenBveApi.Path.CombineFile(TrainDirectory, "train.ai");

            if (!System.IO.File.Exists(TrainData))
            {
                // Check for the standard driveable train.dat
                TrainData = OpenBveApi.Path.CombineFile(TrainDirectory, "train.dat");
            }
            string ExteriorFile = OpenBveApi.Path.CombineFile(TrainDirectory, "extensions.cfg");

            if (!System.IO.File.Exists(TrainData) || !System.IO.File.Exists(ExteriorFile))
            {
                Interface.AddMessage(MessageType.Error, true, "The supplied train folder in TrackFollowingObject " + FileName + " did not contain a complete set of data.");
                return;
            }
            TrainDatParser.ParseTrainData(TrainData, TextEncoding.GetSystemEncodingFromFile(TrainData), Train);
            SoundCfgParser.ParseSoundConfig(TrainDirectory, Train);
            Train.AI = new Game.TrackFollowingObjectAI(Train, Data);

            UnifiedObject[] CarObjects     = new UnifiedObject[Train.Cars.Length];
            UnifiedObject[] BogieObjects   = new UnifiedObject[Train.Cars.Length * 2];
            UnifiedObject[] CouplerObjects = new UnifiedObject[Train.Cars.Length - 1];
            ExtensionsCfgParser.ParseExtensionsConfig(System.IO.Path.GetDirectoryName(ExteriorFile), TextEncoding.GetSystemEncodingFromFile(ExteriorFile), ref CarObjects, ref BogieObjects, ref CouplerObjects, Train, true);

            int currentBogieObject = 0;

            for (int i = 0; i < Train.Cars.Length; i++)
            {
                if (CarObjects[i] == null)
                {
                    // load default exterior object
                    string       file = OpenBveApi.Path.CombineFile(Program.FileSystem.GetDataFolder("Compatibility"), "exterior.csv");
                    StaticObject so   = ObjectManager.LoadStaticObject(file, System.Text.Encoding.UTF8, false);
                    if (so == null)
                    {
                        CarObjects[i] = null;
                    }
                    else
                    {
                        double sx = Train.Cars[i].Width;
                        double sy = Train.Cars[i].Height;
                        double sz = Train.Cars[i].Length;
                        so.ApplyScale(sx, sy, sz);
                        CarObjects[i] = so;
                    }
                }
                if (CarObjects[i] != null)
                {
                    // add object
                    Train.Cars[i].LoadCarSections(CarObjects[i]);
                }

                //Load bogie objects
                if (BogieObjects[currentBogieObject] != null)
                {
                    Train.Cars[i].FrontBogie.LoadCarSections(BogieObjects[currentBogieObject]);
                }
                currentBogieObject++;
                if (BogieObjects[currentBogieObject] != null)
                {
                    Train.Cars[i].RearBogie.LoadCarSections(BogieObjects[currentBogieObject]);
                }
                currentBogieObject++;
            }

            // door open/close speed
            foreach (var Car in Train.Cars)
            {
                if (Car.Specs.DoorOpenFrequency <= 0.0)
                {
                    if (Car.Doors[0].OpenSound.Buffer != null & Car.Doors[1].OpenSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].OpenSound.Buffer);
                        Program.Sounds.LoadBuffer(Car.Doors[1].OpenSound.Buffer);
                        double a = Car.Doors[0].OpenSound.Buffer.Duration;
                        double b = Car.Doors[1].OpenSound.Buffer.Duration;
                        Car.Specs.DoorOpenFrequency = a + b > 0.0 ? 2.0 / (a + b) : 0.8;
                    }
                    else if (Car.Doors[0].OpenSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].OpenSound.Buffer);
                        double a = Car.Doors[0].OpenSound.Buffer.Duration;
                        Car.Specs.DoorOpenFrequency = a > 0.0 ? 1.0 / a : 0.8;
                    }
                    else if (Car.Doors[1].OpenSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].OpenSound.Buffer);
                        double b = Car.Doors[1].OpenSound.Buffer.Duration;
                        Car.Specs.DoorOpenFrequency = b > 0.0 ? 1.0 / b : 0.8;
                    }
                    else
                    {
                        Car.Specs.DoorOpenFrequency = 0.8;
                    }
                }
                if (Car.Specs.DoorCloseFrequency <= 0.0)
                {
                    if (Car.Doors[0].CloseSound.Buffer != null & Car.Doors[1].CloseSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].CloseSound.Buffer);
                        Program.Sounds.LoadBuffer(Car.Doors[1].CloseSound.Buffer);
                        double a = Car.Doors[0].CloseSound.Buffer.Duration;
                        double b = Car.Doors[1].CloseSound.Buffer.Duration;
                        Car.Specs.DoorCloseFrequency = a + b > 0.0 ? 2.0 / (a + b) : 0.8;
                    }
                    else if (Car.Doors[0].CloseSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].CloseSound.Buffer);
                        double a = Car.Doors[0].CloseSound.Buffer.Duration;
                        Car.Specs.DoorCloseFrequency = a > 0.0 ? 1.0 / a : 0.8;
                    }
                    else if (Car.Doors[1].CloseSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].CloseSound.Buffer);
                        double b = Car.Doors[1].CloseSound.Buffer.Duration;
                        Car.Specs.DoorCloseFrequency = b > 0.0 ? 1.0 / b : 0.8;
                    }
                    else
                    {
                        Car.Specs.DoorCloseFrequency = 0.8;
                    }
                }
                const double f = 0.015;
                const double g = 2.75;
                Car.Specs.DoorOpenPitch       = Math.Exp(f * Math.Tan(g * (Program.RandomNumberGenerator.NextDouble() - 0.5)));
                Car.Specs.DoorClosePitch      = Math.Exp(f * Math.Tan(g * (Program.RandomNumberGenerator.NextDouble() - 0.5)));
                Car.Specs.DoorOpenFrequency  /= Car.Specs.DoorOpenPitch;
                Car.Specs.DoorCloseFrequency /= Car.Specs.DoorClosePitch;

                /*
                 * Remove the following two lines, then the pitch at which doors play
                 * takes their randomized opening and closing times into account.
                 * */
                Car.Specs.DoorOpenPitch  = 1.0;
                Car.Specs.DoorClosePitch = 1.0;
            }

            foreach (var Car in Train.Cars)
            {
                Car.FrontAxle.Follower.TrackIndex            = Data[0].RailIndex;
                Car.RearAxle.Follower.TrackIndex             = Data[0].RailIndex;
                Car.FrontBogie.FrontAxle.Follower.TrackIndex = Data[0].RailIndex;
                Car.FrontBogie.RearAxle.Follower.TrackIndex  = Data[0].RailIndex;
                Car.RearBogie.FrontAxle.Follower.TrackIndex  = Data[0].RailIndex;
                Car.RearBogie.RearAxle.Follower.TrackIndex   = Data[0].RailIndex;
            }

            if (consistReversed)
            {
                Train.Reverse();
            }
            Train.PlaceCars(Data[0].StopPosition);
        }
Exemple #5
0
        /// <summary>Parses a base track following object node</summary>
        /// <param name="ObjectPath">Absolute path to the object folder of route data</param>
        /// <param name="FileName">The filename of the containing XML file</param>
        /// <param name="SectionElement">The XElement to parse</param>
        /// <param name="Train">The track following object to parse this node into</param>
        private static void ParseTrackFollowingObjectNode(string ObjectPath, string FileName, XElement SectionElement, TrainManager.TrackFollowingObject Train)
        {
            string Section = SectionElement.Name.LocalName;

            string TrainDirectory       = string.Empty;
            bool   ConsistReversed      = false;
            List <Game.TravelData> Data = new List <Game.TravelData>();

            foreach (XElement KeyNode in SectionElement.Elements())
            {
                string Key        = KeyNode.Name.LocalName;
                int    LineNumber = ((IXmlLineInfo)KeyNode).LineNumber;

                switch (Key.ToLowerInvariant())
                {
                case "definition":
                    ParseDefinitionNode(FileName, KeyNode, Train);
                    break;

                case "train":
                    ParseTrainNode(ObjectPath, FileName, KeyNode, ref TrainDirectory, ref ConsistReversed);
                    break;

                case "points":
                case "stops":
                    ParseTravelDataNodes(FileName, KeyNode, Data);
                    break;

                default:
                    Interface.AddMessage(MessageType.Warning, false, $"Unsupported key {Key} encountered in {Section} at line {LineNumber.ToString(culture)} in {FileName}");
                    break;
                }
            }

            if (Data.Count < 2)
            {
                Interface.AddMessage(MessageType.Error, false, $"There must be at least two points to go through in {FileName}");
                return;
            }

            if (!(Data.First() is Game.TravelStopData) || !(Data.Last() is Game.TravelStopData))
            {
                Interface.AddMessage(MessageType.Error, false, $"The first and the last point to go through must be the \"Stop\" node in {FileName}");
                return;
            }

            if (string.IsNullOrEmpty(TrainDirectory))
            {
                Interface.AddMessage(MessageType.Error, false, $"No train has been specified in {FileName}");
                return;
            }

            /*
             * First check for a train.ai file- Functionally identical, but allows for differently configured AI
             * trains not to show up as drivable
             */
            string TrainData = OpenBveApi.Path.CombineFile(TrainDirectory, "train.ai");

            if (!File.Exists(TrainData))
            {
                // Check for the standard drivable train.dat
                TrainData = OpenBveApi.Path.CombineFile(TrainDirectory, "train.dat");
            }
            string ExteriorFile = OpenBveApi.Path.CombineFile(TrainDirectory, "extensions.cfg");

            if (!File.Exists(TrainData) || !File.Exists(ExteriorFile))
            {
                Interface.AddMessage(MessageType.Error, true, $"The supplied train folder in TrackFollowingObject {FileName} did not contain a complete set of data.");
                return;
            }
            TrainDatParser.ParseTrainData(TrainData, TextEncoding.GetSystemEncodingFromFile(TrainData), Train);
            SoundCfgParser.ParseSoundConfig(TrainDirectory, Train);
            Train.AI = new Game.TrackFollowingObjectAI(Train, Data.ToArray());

            UnifiedObject[] CarObjects          = new UnifiedObject[Train.Cars.Length];
            UnifiedObject[] BogieObjects        = new UnifiedObject[Train.Cars.Length * 2];
            UnifiedObject[] CouplerObjects      = new UnifiedObject[Train.Cars.Length - 1];
            bool[]          VisibleFromInterior = new bool[Train.Cars.Length];
            ExtensionsCfgParser.ParseExtensionsConfig(System.IO.Path.GetDirectoryName(ExteriorFile), TextEncoding.GetSystemEncodingFromFile(ExteriorFile), ref CarObjects, ref BogieObjects, ref CouplerObjects, ref VisibleFromInterior, Train, true);

            int currentBogieObject = 0;

            for (int i = 0; i < Train.Cars.Length; i++)
            {
                if (CarObjects[i] == null)
                {
                    // load default exterior object
                    string       file = OpenBveApi.Path.CombineFile(Program.FileSystem.GetDataFolder("Compatibility"), "exterior.csv");
                    StaticObject so;
                    Program.CurrentHost.LoadStaticObject(file, Encoding.UTF8, false, out so);
                    if (so == null)
                    {
                        CarObjects[i] = null;
                    }
                    else
                    {
                        so.ApplyScale(Train.Cars[i].Width, Train.Cars[i].Height, Train.Cars[i].Length);
                        CarObjects[i] = so;
                    }
                }
                if (CarObjects[i] != null)
                {
                    // add object
                    Train.Cars[i].LoadCarSections(CarObjects[i], false);
                }

                //Load bogie objects
                if (BogieObjects[currentBogieObject] != null)
                {
                    Train.Cars[i].FrontBogie.LoadCarSections(BogieObjects[currentBogieObject], false);
                }
                currentBogieObject++;
                if (BogieObjects[currentBogieObject] != null)
                {
                    Train.Cars[i].RearBogie.LoadCarSections(BogieObjects[currentBogieObject], false);
                }
                currentBogieObject++;
            }

            // door open/close speed
            foreach (var Car in Train.Cars)
            {
                if (Car.Specs.DoorOpenFrequency <= 0.0)
                {
                    if (Car.Doors[0].OpenSound.Buffer != null & Car.Doors[1].OpenSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].OpenSound.Buffer);
                        Program.Sounds.LoadBuffer(Car.Doors[1].OpenSound.Buffer);
                        double a = Car.Doors[0].OpenSound.Buffer.Duration;
                        double b = Car.Doors[1].OpenSound.Buffer.Duration;
                        Car.Specs.DoorOpenFrequency = a + b > 0.0 ? 2.0 / (a + b) : 0.8;
                    }
                    else if (Car.Doors[0].OpenSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].OpenSound.Buffer);
                        double a = Car.Doors[0].OpenSound.Buffer.Duration;
                        Car.Specs.DoorOpenFrequency = a > 0.0 ? 1.0 / a : 0.8;
                    }
                    else if (Car.Doors[1].OpenSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].OpenSound.Buffer);
                        double b = Car.Doors[1].OpenSound.Buffer.Duration;
                        Car.Specs.DoorOpenFrequency = b > 0.0 ? 1.0 / b : 0.8;
                    }
                    else
                    {
                        Car.Specs.DoorOpenFrequency = 0.8;
                    }
                }
                if (Car.Specs.DoorCloseFrequency <= 0.0)
                {
                    if (Car.Doors[0].CloseSound.Buffer != null & Car.Doors[1].CloseSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].CloseSound.Buffer);
                        Program.Sounds.LoadBuffer(Car.Doors[1].CloseSound.Buffer);
                        double a = Car.Doors[0].CloseSound.Buffer.Duration;
                        double b = Car.Doors[1].CloseSound.Buffer.Duration;
                        Car.Specs.DoorCloseFrequency = a + b > 0.0 ? 2.0 / (a + b) : 0.8;
                    }
                    else if (Car.Doors[0].CloseSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].CloseSound.Buffer);
                        double a = Car.Doors[0].CloseSound.Buffer.Duration;
                        Car.Specs.DoorCloseFrequency = a > 0.0 ? 1.0 / a : 0.8;
                    }
                    else if (Car.Doors[1].CloseSound.Buffer != null)
                    {
                        Program.Sounds.LoadBuffer(Car.Doors[0].CloseSound.Buffer);
                        double b = Car.Doors[1].CloseSound.Buffer.Duration;
                        Car.Specs.DoorCloseFrequency = b > 0.0 ? 1.0 / b : 0.8;
                    }
                    else
                    {
                        Car.Specs.DoorCloseFrequency = 0.8;
                    }
                }
                const double f = 0.015;
                const double g = 2.75;
                Car.Specs.DoorOpenPitch       = Math.Exp(f * Math.Tan(g * (Program.RandomNumberGenerator.NextDouble() - 0.5)));
                Car.Specs.DoorClosePitch      = Math.Exp(f * Math.Tan(g * (Program.RandomNumberGenerator.NextDouble() - 0.5)));
                Car.Specs.DoorOpenFrequency  /= Car.Specs.DoorOpenPitch;
                Car.Specs.DoorCloseFrequency /= Car.Specs.DoorClosePitch;

                /*
                 * Remove the following two lines, then the pitch at which doors play
                 * takes their randomized opening and closing times into account.
                 * */
                Car.Specs.DoorOpenPitch  = 1.0;
                Car.Specs.DoorClosePitch = 1.0;
            }

            foreach (var Car in Train.Cars)
            {
                Car.FrontAxle.Follower.TrackIndex            = Data[0].RailIndex;
                Car.RearAxle.Follower.TrackIndex             = Data[0].RailIndex;
                Car.FrontBogie.FrontAxle.Follower.TrackIndex = Data[0].RailIndex;
                Car.FrontBogie.RearAxle.Follower.TrackIndex  = Data[0].RailIndex;
                Car.RearBogie.FrontAxle.Follower.TrackIndex  = Data[0].RailIndex;
                Car.RearBogie.RearAxle.Follower.TrackIndex   = Data[0].RailIndex;
            }

            if (ConsistReversed)
            {
                Train.Reverse();
            }
            Train.PlaceCars(Data[0].Position);
        }
Exemple #6
0
        /// <summary>
        /// Function to parse TFO definition
        /// </summary>
        /// <param name="FileName">The filename of the containing XML file</param>
        /// <param name="SectionElement">The XElement to parse</param>
        /// <param name="Train">The track following object to parse this node into</param>
        private static void ParseDefinitionNode(string FileName, XElement SectionElement, TrainManager.TrackFollowingObject Train)
        {
            string Section = SectionElement.Name.LocalName;

            foreach (XElement KeyNode in SectionElement.Elements())
            {
                string Key        = KeyNode.Name.LocalName;
                string Value      = KeyNode.Value;
                int    LineNumber = ((IXmlLineInfo)KeyNode).LineNumber;

                switch (Key.ToLowerInvariant())
                {
                case "appearancetime":
                    if (Value.Any() && !Interface.TryParseTime(Value, out Train.AppearanceTime))
                    {
                        Interface.AddMessage(MessageType.Error, false, $"Value is invalid in {Key} in {Section} at line {LineNumber.ToString(culture)} in {FileName}");
                    }
                    break;

                case "appearancestartposition":
                    if (Value.Any() && !NumberFormats.TryParseDoubleVb6(Value, out Train.AppearanceStartPosition))
                    {
                        Interface.AddMessage(MessageType.Error, false, $"Value is invalid in {Key} in {Section} at line {LineNumber.ToString(culture)} in {FileName}");
                    }
                    break;

                case "appearanceendposition":
                    if (Value.Any() && !NumberFormats.TryParseDoubleVb6(Value, out Train.AppearanceEndPosition))
                    {
                        Interface.AddMessage(MessageType.Error, false, $"Value is invalid in {Key} in {Section} at line {LineNumber.ToString(culture)} in {FileName}");
                    }
                    break;

                case "leavetime":
                    if (Value.Any() && !Interface.TryParseTime(Value, out Train.LeaveTime))
                    {
                        Interface.AddMessage(MessageType.Error, false, $"Value is invalid in {Key} in {Section} at line {LineNumber.ToString(culture)} in {FileName}");
                    }
                    break;

                default:
                    Interface.AddMessage(MessageType.Warning, false, $"Unsupported key {Key} encountered in {Section} at line {LineNumber.ToString(culture)} in {FileName}");
                    break;
                }
            }
        }