Repository: jeff-1amstudios/OpenNFS1 Branch: master Commit: 357fe6c3314a Files: 126 Total size: 472.1 KB Directory structure: gitextract_3ncehpj2/ ├── .editorconfig ├── .gitignore ├── Engine/ │ ├── AverageValueVector3.cs │ ├── ChaseCamera.cs │ ├── Engine.cs │ ├── FPSCamera.cs │ ├── FPSCounter.cs │ ├── FixedChaseCamera.cs │ ├── GameConsole.cs │ ├── GameEngine.csproj │ ├── GameObject.cs │ ├── GraphicsUtilities.cs │ ├── ICamera.cs │ ├── IDrawableObject.cs │ ├── IGameScreen.cs │ ├── IWorld.cs │ ├── InputProvider.cs │ ├── ParticleSystem/ │ │ ├── ParticleEmitter.cs │ │ ├── ParticleSettings.cs │ │ ├── ParticleSystem.cs │ │ └── ParticleVertex.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── ScreenEffects.cs │ ├── SimpleCamera.cs │ ├── SkyBox.cs │ ├── SoundEngine2.cs │ └── Utility.cs ├── Installer/ │ ├── OpenNFS1.nsi │ ├── build-installer.bat │ └── readme.txt ├── NFSSpecs.txt ├── OpenNFS1/ │ ├── Audio/ │ │ ├── BnkVehicleAudioProvider.cs │ │ ├── EnvironmentAudioProvider.cs │ │ └── VehicleAudioProvider.cs │ ├── AverageValue.cs │ ├── Content/ │ │ ├── ArialBlack-Italic.spritefont │ │ ├── ArialBlack.spritefont │ │ ├── ParticleEffect.fx │ │ ├── common.fxh │ │ ├── macros.fxh │ │ └── structures.fxh │ ├── Dashboards/ │ │ ├── Dashboard.cs │ │ ├── DashboardDescription.cs │ │ └── GearboxAnimation.cs │ ├── Game1.cs │ ├── GameConfig.cs │ ├── Mesh.cs │ ├── ObjectShadow.cs │ ├── OpenNFS1.csproj │ ├── Parsers/ │ │ ├── Audio/ │ │ │ ├── BnkFile.cs │ │ │ └── WavWriter.cs │ │ ├── BaseChunk.cs │ │ ├── BitmapChunk.cs │ │ ├── BitmapLoader.cs │ │ ├── CfmFile.cs │ │ ├── FshFile.cs │ │ ├── HeaderChunk.cs │ │ ├── MeshChunk.cs │ │ ├── OpenRoadTrackfamFile.cs │ │ ├── QfsFile.cs │ │ ├── TrackfamFile.cs │ │ └── TriFile.cs │ ├── Physics/ │ │ ├── AutoGearbox.cs │ │ ├── BaseGearbox.cs │ │ ├── DrivableVehicle.cs │ │ ├── ManualGearbox.cs │ │ ├── Motor.cs │ │ ├── Spring.cs │ │ ├── Vector3Helper.cs │ │ ├── VehicleFenceCollision.cs │ │ └── VehicleWheel.cs │ ├── PlayerUI.cs │ ├── Polygon.cs │ ├── Program.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Race/ │ │ ├── PlayerRaceStats.cs │ │ ├── Race.cs │ │ └── RaceUI.cs │ ├── Tracks/ │ │ ├── SceneryObject.cs │ │ ├── TerrainRow.cs │ │ ├── TerrainSegment.cs │ │ ├── Track.cs │ │ ├── TrackAssembler.cs │ │ ├── TrackBillboardModel.cs │ │ ├── TrackDescription.cs │ │ ├── TrackNode.cs │ │ ├── TrackObjectDescriptor.cs │ │ └── TrackSkybox.cs │ ├── UI/ │ │ └── Screens/ │ │ ├── BaseUIScreen.cs │ │ ├── ChooseDataDownloadScreen.cs │ │ ├── DataDownloadScreen.cs │ │ ├── DoRaceScreen.cs │ │ ├── HomeScreen.cs │ │ ├── LoadRaceScreen.cs │ │ ├── OpenNFS1SplashScreen.cs │ │ ├── RaceFinishedScreen.cs │ │ ├── RaceOptionsScreen.cs │ │ └── RacePausedScreen.cs │ ├── UIController.cs │ ├── VehicleController.cs │ ├── Vehicles/ │ │ ├── AI/ │ │ │ ├── AIDriver.cs │ │ │ ├── IDriver.cs │ │ │ └── TrafficDriver.cs │ │ ├── CarMesh.cs │ │ ├── CarModelCache.cs │ │ ├── PlayerDriver.cs │ │ ├── Traffic/ │ │ │ └── TrafficController.cs │ │ ├── TyreSmokeParticleSystem.cs │ │ ├── Vehicle.cs │ │ ├── VehicleDescription.cs │ │ └── WheelModel.cs │ ├── Views/ │ │ ├── BaseExternalView.cs │ │ ├── BumperView.cs │ │ ├── ChaseView.cs │ │ ├── DashboardView.cs │ │ ├── DebugView.cs │ │ ├── DropCameraView.cs │ │ └── IView.cs │ └── gameconfig.json ├── OpenNFS1.sln ├── PreBuilt_Content/ │ ├── ArialBlack-Italic.xnb │ ├── ArialBlack.xnb │ └── ParticleEffect.xnb ├── TnfsSeSpex.txt ├── readme.md └── reverse_engineering.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_size = 4 tab_width = 4 indent_style = tab ================================================ FILE: .gitignore ================================================ *.suo *.user _ReSharper.* bin obj deploy ================================================ FILE: Engine/AverageValueVector3.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace GameEngine { public class AverageValueVector3 { int _nbrValues; List _values = new List(); public AverageValueVector3(int nbrVaues) { _nbrValues = nbrVaues; } public void Reset(int nbrValues) { _nbrValues = nbrValues; _values.Clear(); } public void AddValue(Vector3 value) { _values.Add(value); if (_values.Count > _nbrValues) _values.RemoveAt(0); } public Vector3 GetAveragedValue() { Vector3 average = new Vector3(); foreach (Vector3 value in _values) { average += value; } return average / _values.Count; } } } ================================================ FILE: Engine/ChaseCamera.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; using System.Diagnostics; namespace GameEngine { public class ChaseCamera : ICamera { private Vector3 _chasePosition; private Vector3 _chaseDirection = new Vector3(0, 0, -1); private Vector3 _up = Vector3.Up; private Vector3 _desiredPositionOffset = new Vector3(0, 2.0f, 2.0f); private Vector3 _desiredPosition; private Vector3 _lookAtOffset = new Vector3(0, 2.8f, 0); private Vector3 _lookAt; private float _stiffness = 1800.0f; private float _zstiffness = 2.0f; private float _damping = 600.0f; private float _mass = 50.0f; private Vector3 _position; private Vector3 _velocity; private float _fieldOfView = MathHelper.ToRadians(45.0f); private float _nearPlaneDistance = 1.0f; private float _farPlaneDistance = 15000.0f; private Matrix _view; private Matrix _projection; /// /// Position of object being chased. /// public Vector3 ChasePosition { get { return _chasePosition; } set { _chasePosition = value; } } public void FollowObject(GameObject obj) { _chasePosition = obj.Position; _chaseDirection = obj.Orientation; } /// /// Direction the chased object is facing. /// public Vector3 ChaseDirection { get { return _chaseDirection; } set { _chaseDirection = value; } } /// /// Chased object's Up vector. /// public Vector3 Up { get { return _up; } set { _up = value; } } /// /// Desired camera position in the chased object's coordinate system. /// public Vector3 DesiredPositionOffset { get { return _desiredPositionOffset; } set { _desiredPositionOffset = value; } } /// /// Desired camera position in world space. /// public Vector3 DesiredPosition { get { // Ensure correct value even if update has not been called this frame UpdateWorldPositions(); return _desiredPosition; } } /// /// Look at point in the chased object's coordinate system. /// public Vector3 LookAtOffset { get { return _lookAtOffset; } set { _lookAtOffset = value; } } /// /// Look at point in world space. /// public Vector3 LookAt { get { // Ensure correct value even if update has not been called this frame UpdateWorldPositions(); return _lookAt; } } /// /// Physics coefficient which controls the influence of the camera's position /// over the spring force. The stiffer the spring, the closer it will stay /// the chased object. /// public float Stiffness { get { return _stiffness; } set { _stiffness = value; } } /// /// Controls how hard the camera tries to keep up with the chased object /// public float ZStiffness { get { return _zstiffness; } set { _zstiffness = value; } } /// /// Physics coefficient which approximates internal friction of the spring. /// Sufficient damping will prevent the spring from oscillating infinitely. /// public float Damping { get { return _damping; } set { _damping = value; } } /// /// Mass of the camera body. Heaver objects require stiffer springs with less /// damping to move at the same rate as lighter objects. /// public float Mass { get { return _mass; } set { _mass = value; } } /// /// Position of camera in world space. /// public Vector3 Position { get { return _position; } } /// /// Velocity of camera. /// public Vector3 Velocity { get { return _velocity; } } #region Perspective properties /// /// Perspective field of view. /// public float FieldOfView { get { return _fieldOfView; } set { _fieldOfView = value; } } /// /// Distance to the near clipping plane. /// public float NearPlaneDistance { get { return _nearPlaneDistance; } set { _nearPlaneDistance = value; } } /// /// Distance to the far clipping plane. /// public float FarPlaneDistance { get { return _farPlaneDistance; } set { _farPlaneDistance = value; } } #endregion /// /// View transform matrix. /// public Matrix View { get { return _view; } } /// /// Projecton transform matrix. /// public Matrix Projection { get { return _projection; } } /// /// Rebuilds object space values in world space. Invoke before publicly /// returning or privately accessing world space values. /// private void UpdateWorldPositions() { Matrix transform = Matrix.Identity; transform.Forward = ChaseDirection; transform.Up = Up; transform.Right = Vector3.Cross(Up, ChaseDirection); // Calculate desired camera properties in world space _desiredPosition = ChasePosition + Vector3.TransformNormal(DesiredPositionOffset, transform); _lookAt = ChasePosition + Vector3.TransformNormal(LookAtOffset, transform); } /// /// Rebuilds camera's view and projection matricies. /// private void UpdateMatrices() { _view = Matrix.CreateLookAt(this.Position, this.LookAt, this.Up); _projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView, Engine.Instance.AspectRatio, NearPlaneDistance, FarPlaneDistance); } /// /// Forces camera to be at desired position and to stop moving. The is useful /// when the chased object is first created or after it has been teleported. /// Failing to call this after a large change to the chased object's position /// will result in the camera quickly flying across the world. /// public void Reset() { UpdateWorldPositions(); // Stop motion _velocity = Vector3.Zero; // Force desired position _position = _desiredPosition; UpdateMatrices(); } /// /// Animates the camera from its current position towards the desired offset /// behind the chased object. The camera's animation is controlled by a simple /// physical spring attached to the camera and anchored to the desired position. /// public void Update(GameTime gameTime) { UpdateWorldPositions(); float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; // Calculate spring force Vector3 stretch = (_position - _desiredPosition); Vector3 force = -_stiffness * stretch - _damping * _velocity; // Apply acceleration Vector3 acceleration = force / _mass; _velocity += acceleration * elapsed; // Apply velocity _position += _velocity * elapsed; // Keep up with chased object //if (Vector3.Distance(_chasePosition, _position) > 50) //{ //_position += _chaseDirection * elapsed * (Vector3.Distance(_chasePosition, _position)) * _zstiffness; //} UpdateMatrices(); } public void SetPosition(Vector3 position) { _position = position; } } } ================================================ FILE: Engine/Engine.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using System.Diagnostics; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using GameEngine; namespace GameEngine { public class Engine : DrawableGameComponent { private static Engine _instance; private ContentManager _contentManager; private ICamera _camera; private InputProvider _inputProvider; private GraphicsUtilities _graphicsUtils; private IWorld _world; public GraphicsDeviceManager _graphics; private SpriteBatch _spriteBatch; public Vector2 ScreenSize; public float FrameTime; public static Engine Instance { get { return _instance; } } public static void Create(Game game, GraphicsDeviceManager graphics) { Debug.Assert(_instance == null); _instance = new Engine(game); _instance.EngineStartup(graphics); } private Engine(Game game) : base(game) { } private void EngineStartup(GraphicsDeviceManager graphics) { _graphics = graphics; _contentManager = new ContentManager(base.Game.Services); //Game bits _inputProvider = new InputProvider(base.Game); var defaultFont = Engine.Instance.ContentManager.Load("Content\\ArialBlack"); _graphicsUtils = new GraphicsUtilities(defaultFont); _spriteBatch = new SpriteBatch(Device); base.Game.Components.Add(this); } public float AspectRatio { get { return (float)Device.Viewport.Width / (float)Device.Viewport.Height; } } public override void Update(GameTime gameTime) { base.Update(gameTime); FrameTime = (float)gameTime.ElapsedGameTime.TotalSeconds; GameConsole.Clear(); _inputProvider.Update(gameTime); SoundEngine2.Instance.Update(gameTime); Screen.Update(gameTime); ScreenEffects.Instance.Update(gameTime); _graphicsUtils.Update(gameTime); } public override void Draw(GameTime gameTime) { Screen.Draw(); _graphicsUtils.Draw(); ScreenEffects.Instance.Draw(); _graphicsUtils.DrawText(); } public ContentManager ContentManager { get { return _contentManager; } } public GraphicsDevice Device { get { return _graphics.GraphicsDevice; } } //public BasicEffect CurrentEffect //{ // get { return _currentEffect; } // set { _currentEffect = value; } //} public GraphicsUtilities GraphicsUtils { get { return _graphicsUtils; } } public IWorld World { get { return _world; } set { _world = value; } } public ICamera Camera { get { return _camera; } set { _camera = value; } } public InputProvider Input { get { return _inputProvider; } set { _inputProvider = value; } } public IGameScreen Screen {get; set; } public SpriteBatch SpriteBatch { get { return _spriteBatch; } } Random _random = new Random(); public Random Random { get { return _random; } } //public bool EnableBloom //{ // set // { // if (value) // { // _game.Components.Add(new BloomComponent(_game)); // } // else // { // foreach (IGameComponent component in _game.Components) // { // if (component is BloomComponent) // { // _game.Components.Remove(component); // break; // } // } // } // } //} } } ================================================ FILE: Engine/FPSCamera.cs ================================================ //----------------------------------------------------------------------------- // Copyright (c) 2007 dhpoware. All Rights Reserved. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using GameEngine; namespace OneAmEngine { /// /// The FirstPersonCamera class implements the logic for a first person /// style 3D camera. This class also handles player input that is used /// to control the camera. To use this class, create an instance of the /// FirstPersonCamera class and then call the Update() method once a /// frame from your game's main loop. The FirstPersonCamera's Update() /// method will process mouse and keyboard input used to manipulate the /// camera. To change the default movement key bindings call the /// MapActionToKey() method. Most of the code in this class is used to /// simulate camera view bobbing, crouching, and jumping. /// public class FPSCamera : ICamera { public const float DEFAULT_FOVX = 60.0f; public const float DEFAULT_ROTATION_SPEED = 0.25f; public const float DEFAULT_ZNEAR = 0.1f; private const float GRAVITY = -9.8f; private const float DECELERATION = -0.5f; private const float STRAFE_SPEED_MULTIPLIER = 15.5f; private const float VelocityInversionMultiplier = 20.0f; private const float Acceleration = 5.0f; private const float Deceleration = -5.0f; private const float JumpVelocity = 0.23f; private const float MaxSpeed = 1.5f; private float _strafeDelta, _forwardDelta, _velocity; private Vector3 _orientation, _position; public Vector3 Position { get { return _position; } set { _position = value; } } public Vector3 Orientation { get { return _orientation; } set { _orientation = value; } } public float DrawDistance { get; set; } public Matrix View { get; private set; } public Matrix Projection { get; private set; } public FPSCamera() { Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(DEFAULT_FOVX), Engine.Instance.AspectRatio, DEFAULT_ZNEAR, 15000); View = Matrix.Identity; } public void Update(GameTime gt) { InputProvider input = Engine.Instance.Input; float elapsedTime = Engine.Instance.FrameTime; _forwardDelta = input.MoveForward * elapsedTime * Acceleration; _strafeDelta = input.Strafe * elapsedTime * Acceleration; float speed = 0.5f; if (input.IsKeyDown(Keys.Home)) { _orientation.Y -= speed * elapsedTime; } if (input.IsKeyDown(Keys.End)) { _orientation.Y += speed * elapsedTime; } if (input.IsKeyDown(Keys.Delete)) { _orientation.X += speed * elapsedTime; } if (input.IsKeyDown(Keys.PageDown)) { _orientation.X -= speed * elapsedTime; } UpdateVelocity(); MoveForward(); _position.X += (float)(Math.Cos(_orientation.X) * input.Strafe); _position.Z -= (float)(Math.Sin(_orientation.X) * input.Strafe); Matrix view = Matrix.CreateTranslation(-Position); view *= Matrix.CreateRotationY(-_orientation.X); view *= Matrix.CreateRotationX(_orientation.Y); view *= Matrix.CreateRotationZ(_orientation.Z); View = view; Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(DEFAULT_FOVX), Engine.Instance.AspectRatio, DEFAULT_ZNEAR, 15000); } private void UpdateVelocity() { float elapsedTimeSec = Engine.Instance.FrameTime; // Accelerate or decelerate as camera is moved forward or backward. float acceleration = Acceleration; if (_forwardDelta != 0.0f) { // Speed up the transition from moving backwards to moving // forwards and vice versa. Otherwise there will be too much // of a delay as the camera slows down and then accelerates. if ((_forwardDelta > 0.0f && _velocity < 0.0f) || (_forwardDelta < 0.0f && _velocity > 0.0f)) { acceleration *= VelocityInversionMultiplier; } _velocity += _forwardDelta * acceleration; } else { if (_velocity > 0.0f) { _velocity += Deceleration * elapsedTimeSec; if (_velocity < 0.0f) _velocity = 0.0f; } else if (_velocity < 0.0f) { _velocity -= Deceleration * elapsedTimeSec; if (_velocity > 0.0f) _velocity = 0.0f; } } if (_velocity > MaxSpeed) { _velocity = MaxSpeed; acceleration = 0; } if (_velocity < -MaxSpeed) { _velocity = -MaxSpeed; acceleration = 0; } } public void MoveForward() { _position.X -= (float)((Math.Sin(_orientation.X) * Math.Cos(_orientation.Y)) * _velocity); _position.Z -= (float)((Math.Cos(_orientation.X) * Math.Cos(_orientation.Y)) * _velocity); _position.Y -= _orientation.Y * _velocity; } public void SetPosition(Vector3 pos) { } public void FollowObject(GameObject obj) { } } } ================================================ FILE: Engine/FPSCounter.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; namespace GameEngine { public class FrameRateCounter : DrawableGameComponent { int frameRate = 0; int frameCounter = 0; TimeSpan elapsedTime = TimeSpan.Zero; public FrameRateCounter() : base(Engine.Instance.Game) { } public override void Update(GameTime gameTime) { elapsedTime += gameTime.ElapsedGameTime; if (elapsedTime > TimeSpan.FromSeconds(1)) { elapsedTime -= TimeSpan.FromSeconds(1); frameRate = frameCounter; frameCounter = 0; } } public override void Draw(GameTime gameTime) { frameCounter++; string fps = string.Format("fps: {0}", frameRate); GameConsole.WriteLine(fps); } } } ================================================ FILE: Engine/FixedChaseCamera.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; namespace GameEngine { /// /// Camera that stays a fixed distance behind an object but swings freely /// public class FixedChaseCamera : ICamera { public FixedChaseCamera() { } public Vector3 RightVec = Vector3.Right; public Vector3 UpVector = Vector3.Up; AverageValueVector3 _lookAt = new AverageValueVector3(40); /// /// Position of camera in world space. /// public Vector3 Position { get { return _position; } set { _position = value; } } private Vector3 _position; public Vector3 ChaseDirection { set { //_lookAt.AddValue(value); _chaseDirection = value; } } private Vector3 _chaseDirection; public float ChaseOffset { get; set; } /// /// Perspective field of view. /// public float FieldOfView { get { return fieldOfView; } set { fieldOfView = value; } } private float fieldOfView = MathHelper.ToRadians(45.0f); /// /// Distance to the near clipping plane. /// public float NearPlaneDistance { get { return nearPlaneDistance; } set { nearPlaneDistance = value; } } private float nearPlaneDistance = 1.0f; /// /// Distance to the far clipping plane. /// public float FarPlaneDistance { get { return farPlaneDistance; } set { farPlaneDistance = value; } } private float farPlaneDistance = 15000.0f; /// /// View transform matrix. /// public Matrix View { get { return _view; } } private Matrix _view; /// /// Projecton transform matrix. /// public Matrix Projection { get { return _projection; } } private Matrix _projection; public void Update(GameTime gameTime) { _lookAt.AddValue(new Vector3(ChaseOffset, ChaseHeight, 0) + (-_chaseDirection * new Vector3(ChaseDistance, ChaseDistance, ChaseDistance))); Vector3 avgLookAt = _lookAt.GetAveragedValue(); Vector3 cameraPosition = _position +avgLookAt; _view = Matrix.CreateLookAt(cameraPosition, cameraPosition - avgLookAt + new Vector3(0,13,0), Vector3.Up); _projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView, Engine.Instance.AspectRatio, NearPlaneDistance, FarPlaneDistance); } public void SetPosition(Vector3 position) { _position = position; } public void FollowObject(GameObject obj) { } public float ChaseDistance { get; set; } public float ChaseHeight { get; set; } } } ================================================ FILE: Engine/GameConsole.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace GameEngine { public static class GameConsole { static int _row = 0; public static void Clear() { _row = 0; } public static void WriteLine(object o) { Engine.Instance.GraphicsUtils.AddText(new Vector2(21, _row * 18 + 101), o.ToString(), Justify.MIDDLE_LEFT, Color.Black); Engine.Instance.GraphicsUtils.AddText(new Vector2(20, _row * 18 + 100), o.ToString(), Justify.MIDDLE_LEFT, Color.White); _row++; } public static void WriteLine(Vector3 vec) { vec.X = (float)Math.Round(vec.X, 3); vec.Y = (float)Math.Round(vec.Y, 3); vec.Z = (float)Math.Round(vec.Z, 3); WriteLine(vec.ToString()); } } } ================================================ FILE: Engine/GameEngine.csproj ================================================  Debug x86 8.0.30703 2.0 {F66B2F9A-AF38-40F9-A094-522C823D04EE} Library Properties GameEngine GameEngine 512 x86 true full false bin\WindowsGL\Debug\ DEBUG;TRACE;WINDOWS prompt 4 x86 pdbonly true bin\WindowsGL\Release\ TRACE;WINDOWS prompt 4 Icon.ico False ..\lib\MonoGame.Framework.dll ================================================ FILE: Engine/GameObject.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace GameEngine { public abstract class GameObject { protected Vector3 _position, _lastPosition, _size, _orientation; protected float _velocity; protected bool _visible; protected bool _lockToGround; public GameObject() { _visible = true; } public Vector3 Position { get { return _position; } set { _position = value; } } public Vector3 Orientation { get { return _orientation; } set { _orientation = value; } } public Vector3 Size { get { return _size; } set { _size = value; } } public float Velocity { get { return _velocity; } set { _velocity = value; } } public bool Visible { get { return _visible; } set { _visible = value; } } public bool LockToGround { get { return _lockToGround; } set { _lockToGround = value; } } public void SetRotation(float rotation) { _orientation.X = rotation; } public void MoveForward() { _lastPosition = _position; _position.X -= (float)((Math.Sin(_orientation.X) * Math.Cos(_orientation.Y)) * _velocity); _position.Z -= (float)((Math.Cos(_orientation.X) * Math.Cos(_orientation.Y)) * _velocity); if (!_lockToGround) _position.Y -= _orientation.Y * _velocity; } public Vector3 GetLookAt(float distance) { Vector3 lookAt = _position; lookAt.X -= (float)((Math.Sin(_orientation.X) * Math.Cos(_orientation.Y)) * distance); lookAt.Z -= (float)((Math.Cos(_orientation.X) * Math.Cos(_orientation.Y)) * distance); lookAt.Y -= _orientation.Y * distance; return lookAt; } public Vector3 GetLookAt(Vector3 orientation, float distance) { Vector3 lookAt = _position; lookAt.X -= (float)((Math.Sin(orientation.X) * Math.Cos(orientation.Y)) * distance); lookAt.Z -= (float)((Math.Cos(orientation.X) * Math.Cos(orientation.Y)) * distance); lookAt.Y -= orientation.Y * distance; return lookAt; } public void Strafe(float amount) { _position.X += (float)(Math.Cos(_orientation.X) * amount); _position.Z -= (float)(Math.Sin(_orientation.X) * amount); } public Matrix WorldTransform { get { Matrix world = Matrix.CreateFromYawPitchRoll(_orientation.X, -_orientation.Y, _orientation.Z); world *= Matrix.CreateScale(_size); world *= Matrix.CreateTranslation(_position); return world; } } /// /// Moves the camera. The dx, dy, and dz parameters determine how /// far to move the camera forwards, upwards, and sideways. /// /// Sideways movement amount. /// Upwards movement amount. /// Forwads movement amount. public void Move(float dx, float dy, float dz) { _position.X += dx; _position.Y += dy; _position.Z += dz; } /// /// Moves the camera along the given direction. /// /// The direction to move. /// How far to move along direction. public void Move(Vector3 direction, float amount) { _position += direction * amount; } public void Rotate(float amount) { _orientation.X += amount; } public void Pitch(float amount) { _orientation.Y += amount; } public virtual Vector3 GetCameraPosition() { return _position; } public BoundingSphere BoundingSphere { get { return new BoundingSphere(_position, _size.X); } } public abstract void Update(GameTime gameTime); public abstract void Render(); public virtual void OnPlayerSelect() { } } } ================================================ FILE: Engine/GraphicsUtilities.cs ================================================ #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; #endregion namespace GameEngine { /// /// Used for text justification. /// public enum Justify { TOP_LEFT, TOP_CENTER, TOP_RIGHT, MIDDLE_LEFT, MIDDLE_CENTER, MIDDLE_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT } /// /// Type of shape to draw. /// public enum ShapeType { Cube } /// /// GraphicsUtilities /// DrawableGameComponent for debug-graphics functionality. /// Currently supports 3D lines, text, and basic solid shapes. /// Registers self as service provider - IGraphicsUtilitiesService. /// /// To use: /// Create an instance of GraphicsUtilities /// Add it to the list of components /// Set its view/projection matrices every frame /// Add lines/text/shapes every frame /// /// public class GraphicsUtilities : IDrawableObject { #region Creation / Initialization public GraphicsUtilities(SpriteFont font) { CreateLineEffect(); CreateShapeEffect(); CreateCube(); mFont1 = font; mSpriteBatch = new SpriteBatch(Engine.Instance.Device); } public void Update(GameTime gameTime) { if (Engine.Instance.Camera == null) return; SetViewMatrix(Engine.Instance.Camera.View); SetProjectionMatrix(Engine.Instance.Camera.Projection); } /// /// Draw utility graphics waiting to be rendered this pass. /// public void Draw() { Engine.Instance.Device.DepthStencilState = DepthStencilState.Default; // Draw shapes if (sShapeList.Count > 0) { int nbrPrimitives = 0; foreach (ShapeData shapeData in sShapeList) { switch (shapeData.mType) { case ShapeType.Cube: Engine.Instance.Device.SetVertexBuffer(mCubeVertexBuffer); nbrPrimitives = 12; Engine.Instance.Device.RasterizerState = RasterizerState.CullClockwise; break; } mBasicShapeEffect.DiffuseColor = shapeData.mColor.ToVector3() * 0.5f; mBasicShapeEffect.SpecularColor = shapeData.mColor.ToVector3(); mBasicShapeEffect.TextureEnabled = false; mBasicShapeEffect.World = shapeData.mWorldMatrix; mBasicShapeEffect.View = mViewMatrix; mBasicShapeEffect.Projection = mProjectionMatrix; foreach (EffectPass pass in mBasicShapeEffect.CurrentTechnique.Passes) { pass.Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleList, 0, nbrPrimitives); } } } ClearShapes(); // Draw lines if (sLinesList.Count > 0) { mLineVertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionColor), sLinesList.Count, BufferUsage.WriteOnly); mLineVertexBuffer.SetData(sLinesList.ToArray()); Engine.Instance.Device.SetVertexBuffer(mLineVertexBuffer); mBasicLineEffect.View = mViewMatrix; mBasicLineEffect.Projection = mProjectionMatrix; foreach (EffectPass pass in mBasicLineEffect.CurrentTechnique.Passes) { pass.Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.LineList, 0, sLinesList.Count / 2); } } ClearLines(); } public void DrawText() { // Draw text if (sTextList.Count > 0) { mSpriteBatch.Begin(); foreach (TextData textData in sTextList) { Vector2 screenPos = new Vector2(textData.mPos.X, textData.mPos.Y); if (!textData.mIsTransformed) { // If text was specified in 3D, transform it to 2D coordinates Vector3 transformed = Engine.Instance.Device.Viewport.Project(textData.mPos, mProjectionMatrix, mViewMatrix, Matrix.Identity); // Don't draw text for positions behind the camera if (transformed.Z < 0.0f) { continue; } screenPos.X = transformed.X; screenPos.Y = transformed.Y; } // Draw each string JustifyText(mFont1, textData.mText, textData.mJustify, screenPos, out screenPos); mSpriteBatch.DrawString(mFont1, textData.mText, screenPos, textData.mColor); } mSpriteBatch.End(); } ClearText(); } #endregion #region Utilities (Line/Text/Object drawing) public void AddCube(Matrix worldTransform, Color color) { if (sShapeList.Count >= MAX_SHAPES) { return; } ShapeData shapeData = new ShapeData(); shapeData.mType = ShapeType.Cube; shapeData.mWorldMatrix = worldTransform; shapeData.mColor = color; shapeData.mTexture = null; sShapeList.Add(shapeData); } /// /// Add 3D line. /// /// 3D world-space start position /// 3D world-space end position /// Color of line public void AddLine(Vector3 startPos, Vector3 endPos, Color color) { if (sLinesList.Count >= MAX_LINES * 2) { return; } VertexPositionColor lineVert = new VertexPositionColor(); lineVert.Position = startPos; lineVert.Color = color; sLinesList.Add(lineVert); lineVert.Position = endPos; lineVert.Color = color; sLinesList.Add(lineVert); } /// /// Add text at 2D position. /// /// XY screen coordinates (pixels) /// Text to draw /// Color of text public void AddText(Vector2 pos, String text, Justify justify, Color color) { if (sTextList.Count >= MAX_TEXT_LINES) { return; } TextData textData = new TextData(); textData.mPos.X = pos.X; textData.mPos.Y = pos.Y; textData.mPos.Z = 0.0f; textData.mText = text; textData.mColor = color; textData.mJustify = justify; textData.mIsTransformed = true; sTextList.Add(textData); } /// /// Add text at 3D position. /// /// 3D world-space position for text /// Text to draw /// Color of text public void AddText(Vector3 worldPos, String text, Justify justify, Color color) { if (sTextList.Count >= MAX_TEXT_LINES) { return; } TextData textData = new TextData(); textData.mPos = worldPos; textData.mText = text; textData.mColor = color; textData.mJustify = justify; textData.mIsTransformed = false; sTextList.Add(textData); } /// /// Add coordinate axis using the specified transformation. /// /// World transformation matrix /// Scale on drawn lines (1.0 units by default). public void AddAxis(Matrix worldTransform, float scale) { AddLine(worldTransform.Translation, worldTransform.Translation + worldTransform.Forward, Color.Red); AddLine(worldTransform.Translation, worldTransform.Translation + worldTransform.Left, Color.Green); AddLine(worldTransform.Translation, worldTransform.Translation + worldTransform.Up, Color.Blue); } /// /// Add a cube using lines. /// /// World transformation matrix, specifies the center of the cube. /// Color public void AddWireframeCube(Matrix worldTransform, Color color) { Vector3 forwardVector = worldTransform.Forward / 2.0f; Vector3 leftVector = worldTransform.Left / 2.0f; Vector3 upVector = worldTransform.Up / 2.0f; Vector3 centerPosition = worldTransform.Translation; Vector3 forwardLeftUp = centerPosition + forwardVector + leftVector + upVector; Vector3 forwardRightUp = centerPosition + forwardVector - leftVector + upVector; Vector3 backwardLeftUp = centerPosition - forwardVector + leftVector + upVector; Vector3 backwardRightUp = centerPosition - forwardVector - leftVector + upVector; Vector3 forwardLeftDown = centerPosition + forwardVector + leftVector - upVector; Vector3 forwardRightDown = centerPosition + forwardVector - leftVector - upVector; Vector3 backwardLeftDown = centerPosition - forwardVector + leftVector - upVector; Vector3 backwardRightDown = centerPosition - forwardVector - leftVector - upVector; // Draw top AddLine(forwardLeftUp, forwardRightUp, color); AddLine(forwardRightUp, backwardRightUp, color); AddLine(backwardRightUp, backwardLeftUp, color); AddLine(backwardLeftUp, forwardLeftUp, color); // Draw bottom AddLine(forwardLeftDown, forwardRightDown, color); AddLine(forwardRightDown, backwardRightDown, color); AddLine(backwardRightDown, backwardLeftDown, color); AddLine(backwardLeftDown, forwardLeftDown, color); // Draw sides AddLine(forwardLeftUp, forwardLeftDown, color); AddLine(forwardRightUp, forwardRightDown, color); AddLine(backwardRightUp, backwardRightDown, color); AddLine(backwardLeftUp, backwardLeftDown, color); } /// /// Add a square grid using lines. /// /// World transformation matrix, specifies the center of the grid. /// Number of rows (and columns). /// Color. public void AddSquareGrid(Matrix worldTransform, int numRows, Color color) { if (0 < numRows) { float scale = worldTransform.Forward.Length(); Vector3 forwardVector = worldTransform.Forward / 2.0f; Vector3 leftVector = worldTransform.Left / 2.0f; Vector3 backwardsNormalizedVector = -forwardVector / forwardVector.Length(); Vector3 rightNormalizedVector = -leftVector / leftVector.Length(); Vector3 centerPosition = worldTransform.Translation; Vector3 forwardLeft = centerPosition + forwardVector + leftVector; Vector3 forwardRight = centerPosition + forwardVector - leftVector; Vector3 backwardLeft = centerPosition - forwardVector + leftVector; Vector3 backwardRight = centerPosition - forwardVector - leftVector; // Draw outline of the grid AddLine(forwardLeft, forwardRight, color); AddLine(forwardRight, backwardRight, color); AddLine(backwardRight, backwardLeft, color); AddLine(backwardLeft, forwardLeft, color); // Draw interior grid lines float stepSize = 1.0f / (float)(numRows); for (int ii = 1; ii < numRows; ++ii) { float percentageAcross = (float)(ii) * stepSize; // Front-to-back line AddLine(forwardLeft + (rightNormalizedVector * percentageAcross * scale), backwardLeft + (rightNormalizedVector * percentageAcross * scale), color); // Left-to-right line AddLine(forwardLeft + (backwardsNormalizedVector * percentageAcross * scale), forwardRight + (backwardsNormalizedVector * percentageAcross * scale), color); } } } #endregion #region Data Access (used to set view/projection matrices) public void SetViewMatrix(Matrix view) { mViewMatrix = view; } public void SetProjectionMatrix(Matrix proj) { mProjectionMatrix = proj; } /// /// Clear list of shapes waiting to be rendered. /// public void ClearShapes() { sShapeList.Clear(); } /// /// Clear list of lines waiting to be rendered. /// public void ClearLines() { sLinesList.Clear(); } /// /// Clear list of text waiting to be rendered. /// public void ClearText() { sTextList.Clear(); } #endregion #region Private Data & Methods /// /// Justify text based on enumerated value. /// private void JustifyText(SpriteFont font, String text, Justify justify, Vector2 inputPos, out Vector2 resultPos) { Vector2 textSize = font.MeasureString(text); // Default text to upper-left resultPos = inputPos; switch (justify) { case Justify.TOP_LEFT: break; case Justify.TOP_CENTER: resultPos.X -= (textSize.X / 2); break; case Justify.TOP_RIGHT: resultPos.X -= textSize.X; break; case Justify.MIDDLE_LEFT: resultPos.Y -= (textSize.Y / 2); break; case Justify.MIDDLE_CENTER: resultPos.X -= (textSize.X / 2); resultPos.Y -= (textSize.Y / 2); break; case Justify.MIDDLE_RIGHT: resultPos.X -= textSize.X; resultPos.Y -= (textSize.Y / 2); break; case Justify.BOTTOM_LEFT: resultPos.Y -= textSize.Y; break; case Justify.BOTTOM_CENTER: resultPos.X -= (textSize.X / 2); resultPos.Y -= textSize.Y; break; case Justify.BOTTOM_RIGHT: resultPos.X -= textSize.X; resultPos.Y -= textSize.Y; break; } } /// /// Create the BasicEffect to be used by Lines. /// private void CreateLineEffect() { mBasicLineEffect = new BasicEffect(Engine.Instance.Device); mBasicLineEffect.VertexColorEnabled = true; } /// /// Create the BasicEffect to be used by shapes. /// private void CreateShapeEffect() { mBasicShapeEffect = new BasicEffect(Engine.Instance.Device); mBasicShapeEffect.Alpha = 1.0f; mBasicShapeEffect.DiffuseColor = new Vector3(0.5f, 0.5f, 0.5f); mBasicShapeEffect.SpecularColor = new Vector3(1.0f, 1.0f, 1.0f); mBasicShapeEffect.SpecularPower = 3.0f; mBasicShapeEffect.AmbientLightColor = new Vector3(0.75f, 0.75f, 0.75f); mBasicShapeEffect.DirectionalLight0.Enabled = true; mBasicShapeEffect.DirectionalLight0.DiffuseColor = Vector3.One; mBasicShapeEffect.DirectionalLight0.Direction = Vector3.Normalize(new Vector3(1.0f, -1.0f, -1.0f)); mBasicShapeEffect.DirectionalLight0.SpecularColor = Vector3.One; mBasicShapeEffect.DirectionalLight1.Enabled = true; mBasicShapeEffect.DirectionalLight1.DiffuseColor = new Vector3(0.5f, 0.5f, 0.5f); mBasicShapeEffect.DirectionalLight1.Direction = Vector3.Normalize(new Vector3(-1.0f, -1.0f, 1.0f)); mBasicShapeEffect.DirectionalLight1.SpecularColor = new Vector3(0.5f, 0.5f, 0.5f); mBasicShapeEffect.LightingEnabled = true; } /// /// Create vertices and vertex buffer for drawing a solid cube. /// private void CreateCube() { mCubeVertices = new VertexPositionNormalTexture[36]; Vector3 topLeftFront = new Vector3(-0.5f, 0.5f, 0.5f); Vector3 bottomLeftFront = new Vector3(-0.5f, -0.5f, 0.5f); Vector3 topRightFront = new Vector3(0.5f, 0.5f, 0.5f); Vector3 bottomRightFront = new Vector3(0.5f, -0.5f, 0.5f); Vector3 topLeftBack = new Vector3(-0.5f, 0.5f, -0.5f); Vector3 topRightBack = new Vector3(0.5f, 0.5f, -0.5f); Vector3 bottomLeftBack = new Vector3(-0.5f, -0.5f, -0.5f); Vector3 bottomRightBack = new Vector3(0.5f, -0.5f, -0.5f); Vector2 textureTopLeft = new Vector2(0.0f, 0.0f); Vector2 textureTopRight = new Vector2(1.0f, 0.0f); Vector2 textureBottomLeft = new Vector2(0.0f, 1.0f); Vector2 textureBottomRight = new Vector2(1.0f, 1.0f); Vector3 frontNormal = new Vector3(0.0f, 0.0f, 1.0f); Vector3 backNormal = new Vector3(0.0f, 0.0f, -1.0f); Vector3 topNormal = new Vector3(0.0f, 1.0f, 0.0f); Vector3 bottomNormal = new Vector3(0.0f, -1.0f, 0.0f); Vector3 leftNormal = new Vector3(-1.0f, 0.0f, 0.0f); Vector3 rightNormal = new Vector3(1.0f, 0.0f, 0.0f); // Front face. mCubeVertices[0] = new VertexPositionNormalTexture(topLeftFront, frontNormal, textureTopLeft); mCubeVertices[1] = new VertexPositionNormalTexture(bottomLeftFront, frontNormal, textureBottomLeft); mCubeVertices[2] = new VertexPositionNormalTexture(topRightFront, frontNormal, textureTopRight); mCubeVertices[3] = new VertexPositionNormalTexture(bottomLeftFront, frontNormal, textureBottomLeft); mCubeVertices[4] = new VertexPositionNormalTexture(bottomRightFront, frontNormal, textureBottomRight); mCubeVertices[5] = new VertexPositionNormalTexture(topRightFront, frontNormal, textureTopRight); // Back face. mCubeVertices[6] = new VertexPositionNormalTexture(topLeftBack, backNormal, textureTopRight); mCubeVertices[7] = new VertexPositionNormalTexture(topRightBack, backNormal, textureTopLeft); mCubeVertices[8] = new VertexPositionNormalTexture(bottomLeftBack, backNormal, textureBottomRight); mCubeVertices[9] = new VertexPositionNormalTexture(bottomLeftBack, backNormal, textureBottomRight); mCubeVertices[10] = new VertexPositionNormalTexture(topRightBack, backNormal, textureTopLeft); mCubeVertices[11] = new VertexPositionNormalTexture(bottomRightBack, backNormal, textureBottomLeft); // Top face. mCubeVertices[12] = new VertexPositionNormalTexture(topLeftFront, topNormal, textureBottomLeft); mCubeVertices[13] = new VertexPositionNormalTexture(topRightBack, topNormal, textureTopRight); mCubeVertices[14] = new VertexPositionNormalTexture(topLeftBack, topNormal, textureTopLeft); mCubeVertices[15] = new VertexPositionNormalTexture(topLeftFront, topNormal, textureBottomLeft); mCubeVertices[16] = new VertexPositionNormalTexture(topRightFront, topNormal, textureBottomRight); mCubeVertices[17] = new VertexPositionNormalTexture(topRightBack, topNormal, textureTopRight); // Bottom face. mCubeVertices[18] = new VertexPositionNormalTexture(bottomLeftFront, bottomNormal, textureTopLeft); mCubeVertices[19] = new VertexPositionNormalTexture(bottomLeftBack, bottomNormal, textureBottomLeft); mCubeVertices[20] = new VertexPositionNormalTexture(bottomRightBack, bottomNormal, textureBottomRight); mCubeVertices[21] = new VertexPositionNormalTexture(bottomLeftFront, bottomNormal, textureTopLeft); mCubeVertices[22] = new VertexPositionNormalTexture(bottomRightBack, bottomNormal, textureBottomRight); mCubeVertices[23] = new VertexPositionNormalTexture(bottomRightFront, bottomNormal, textureTopRight); // Left face. mCubeVertices[24] = new VertexPositionNormalTexture(topLeftFront, leftNormal, textureTopRight); mCubeVertices[25] = new VertexPositionNormalTexture(bottomLeftBack, leftNormal, textureBottomLeft); mCubeVertices[26] = new VertexPositionNormalTexture(bottomLeftFront, leftNormal, textureBottomRight); mCubeVertices[27] = new VertexPositionNormalTexture(topLeftBack, leftNormal, textureTopLeft); mCubeVertices[28] = new VertexPositionNormalTexture(bottomLeftBack, leftNormal, textureBottomLeft); mCubeVertices[29] = new VertexPositionNormalTexture(topLeftFront, leftNormal, textureTopRight); // Right face. mCubeVertices[30] = new VertexPositionNormalTexture(topRightFront, rightNormal, textureTopLeft); mCubeVertices[31] = new VertexPositionNormalTexture(bottomRightFront, rightNormal, textureBottomLeft); mCubeVertices[32] = new VertexPositionNormalTexture(bottomRightBack, rightNormal, textureBottomRight); mCubeVertices[33] = new VertexPositionNormalTexture(topRightBack, rightNormal, textureTopRight); mCubeVertices[34] = new VertexPositionNormalTexture(topRightFront, rightNormal, textureTopLeft); mCubeVertices[35] = new VertexPositionNormalTexture(bottomRightBack, rightNormal, textureBottomRight); mCubeVertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionNormalTexture), mCubeVertices.Length, BufferUsage.WriteOnly); mCubeVertexBuffer.SetData(mCubeVertices); } // Internal data types struct TextData { public Vector3 mPos; public String mText; public Color mColor; public Justify mJustify; public bool mIsTransformed; } struct ShapeData { public ShapeType mType; public Matrix mWorldMatrix; public Color mColor; public Texture2D mTexture; } Matrix mViewMatrix = Matrix.Identity; Matrix mProjectionMatrix = Matrix.CreateLookAt(Vector3.One, Vector3.Zero, Vector3.Up); // Lists of data private List sLinesList = new List(); private List sTextList = new List(); private List sShapeList = new List(); // Shape-drawing data BasicEffect mBasicShapeEffect; VertexPositionNormalTexture[] mCubeVertices; VertexBuffer mCubeVertexBuffer; const int MAX_SHAPES = 200; // Line-drawing data VertexBuffer mLineVertexBuffer; BasicEffect mBasicLineEffect; const int MAX_LINES = 1024; // Text-drawing data SpriteBatch mSpriteBatch; SpriteFont mFont1; const int MAX_TEXT_LINES = 50; #endregion } } ================================================ FILE: Engine/ICamera.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; namespace GameEngine { public interface ICamera { /// /// Returns the camera's current view matrix. /// Matrix View { get; } /// /// Returns the camera's current perspective projection matrix. /// Matrix Projection { get; } void FollowObject(GameObject obj); void Update(GameTime time); Vector3 Position { get;} void SetPosition(Vector3 position); } } ================================================ FILE: Engine/IDrawableObject.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace GameEngine { public interface IDrawableObject { void Update(GameTime gameTime); void Draw(); } } ================================================ FILE: Engine/IGameScreen.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace GameEngine { public interface IGameScreen : IDrawableObject { } } ================================================ FILE: Engine/IWorld.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace GameEngine { public interface IWorld : IDrawableObject { float GetHeightAtPoint(Vector3 position); void Reset(); } } ================================================ FILE: Engine/InputProvider.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using System.Diagnostics; namespace GameEngine { public enum Actions { MoveForwardsPrimary, MoveForwardsAlternate, MoveBackwardsPrimary, MoveBackwardsAlternate, StrafeRightPrimary, StrafeRightAlternate, StrafeLeftPrimary, StrafeLeftAlternate, RunPrimary, RunAlternate, CrouchPrimary, CrouchAlternate, JumpPrimary, JumpAlternate }; public enum MouseInputMode { FPS, FreeMouse } public class InputProvider : GameComponent { private GamePadState _gamePadState, _previousGamePadState; private KeyboardState _keyboardState, _previousKeyboardState; private const float SENSITIVITY = 1.0f; private float _perFrameMultiplier; public InputProvider(Game game) : base(game) { } public GamePadState GamePadState { get { return _gamePadState; } } public override void Update(GameTime gameTime) { float frameTime = (float)gameTime.ElapsedGameTime.TotalSeconds; _perFrameMultiplier = frameTime * SENSITIVITY; _previousKeyboardState = _keyboardState; _previousGamePadState = _gamePadState; _keyboardState = Keyboard.GetState(); _gamePadState = GamePad.GetState(PlayerIndex.One); if (_previousKeyboardState == null) _previousKeyboardState = _keyboardState; base.Update(gameTime); } private Vector2 GetScreenCenter() { GameWindow window = Engine.Instance.Game.Window; return new Vector2(window.ClientBounds.Width / 2, window.ClientBounds.Height / 2); } public float MoveForward { get { if (_gamePadState.ThumbSticks.Left.Y != 0) return _gamePadState.ThumbSticks.Left.Y * _perFrameMultiplier; else if (_keyboardState.IsKeyDown(Keys.W)) return 1.0f * _perFrameMultiplier; else if (_keyboardState.IsKeyDown(Keys.S)) return -1.0f * _perFrameMultiplier; else return 0.0f; } } public float Strafe { get { if (_gamePadState.ThumbSticks.Left.X != 0) return _gamePadState.ThumbSticks.Left.X * _perFrameMultiplier; else if (_keyboardState.IsKeyDown(Keys.A)) return -1.0f * _perFrameMultiplier; else if (_keyboardState.IsKeyDown(Keys.D)) return 1.0f * _perFrameMultiplier; else return 0.0f; } } public bool WasPressed(Keys key) { return _previousKeyboardState.IsKeyDown(key) && !_keyboardState.IsKeyDown(key); } public bool WasPressed(Buttons button) { return _previousGamePadState.IsButtonDown(button) && !_gamePadState.IsButtonDown(button); } public bool IsKeyDown(Keys key) { return _keyboardState.IsKeyDown(key); } } } ================================================ FILE: Engine/ParticleSystem/ParticleEmitter.cs ================================================ #region File Description //----------------------------------------------------------------------------- // ParticleEmitter.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using Microsoft.Xna.Framework; using System.Collections.Generic; #endregion namespace GameEngine { /// /// Helper for objects that want to leave particles behind them as they /// move around the world. This emitter implementation solves two related /// problems: /// /// If an object wants to create particles very slowly, less than once per /// frame, it can be a pain to keep track of which updates ought to create /// a new particle versus which should not. /// /// If an object is moving quickly and is creating many particles per frame, /// it will look ugly if these particles are all bunched up together. Much /// better if they can be spread out along a line between where the object /// is now and where it was on the previous frame. This is particularly /// important for leaving trails behind fast moving objects such as rockets. /// /// This emitter class keeps track of a moving object, remembering its /// previous position so it can calculate the velocity of the object. It /// works out the perfect locations for creating particles at any frequency /// you specify, regardless of whether this is faster or slower than the /// game update rate. /// public class ParticleEmitter { #region Fields public ParticleSystem ParticleSystem { get; set; } float _timeBetweenParticles; Vector3 _previousPosition; float _timeLeftOver; public float ParticlesPerSecond; public float DumpsPerSecond = 0.2f; #endregion public float LastDumpTime; public bool Enabled = true; public List ParticleSystems = new List(); public static List AllEmitters = new List(); /// /// Constructs a new particle emitter object. /// public ParticleEmitter(ParticleSystem particleSystem, float particlesPerSecond, Vector3 initialPosition) { ParticleSystem = particleSystem; _timeBetweenParticles = 1.0f / particlesPerSecond; ParticlesPerSecond = particlesPerSecond; _previousPosition = initialPosition; AllEmitters.Add(this); } public void Update(Vector3 newPosition) { Update(newPosition, ParticleSystem); } /// /// Updates the emitter, creating the appropriate number of particles /// in the appropriate positions. /// public void Update(Vector3 newPosition, ParticleSystem particleSystem) { float elapsedSeconds = Engine.Instance.FrameTime; // Work out how much time has passed since the previous update. if (elapsedSeconds > 0 && Enabled) { // Work out how fast we are moving. Vector3 velocity = (newPosition - _previousPosition) / elapsedSeconds; // If we had any time left over that we didn't use during the // previous update, add that to the current elapsed time. float timeToSpend = _timeLeftOver + elapsedSeconds; // Counter for looping over the time interval. float currentTime = -_timeLeftOver; // Create particles as long as we have a big enough time interval. while (timeToSpend > _timeBetweenParticles) { currentTime += _timeBetweenParticles; timeToSpend -= _timeBetweenParticles; // Work out the optimal position for this particle. This will produce // evenly spaced particles regardless of the object speed, particle // creation frequency, or game update rate. float mu = currentTime / elapsedSeconds; Vector3 position = Vector3.Lerp(_previousPosition, newPosition, mu); // Create the particle. particleSystem.AddParticle(position, velocity); } // Store any time we didn't use, so it can be part of the next update. _timeLeftOver = timeToSpend; } _previousPosition = newPosition; } } } ================================================ FILE: Engine/ParticleSystem/ParticleSettings.cs ================================================ #region File Description //----------------------------------------------------------------------------- // ParticleSettings.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; #endregion namespace GameEngine { /// /// Settings class describes all the tweakable options used /// to control the appearance of a particle system. /// public class ParticleSettings { // Name of the texture used by this particle system. public Texture2D Texture = null; // Maximum number of particles that can be displayed at one time. public int MaxParticles = 100; // How long these particles will last. public TimeSpan Duration = TimeSpan.FromSeconds(1); // If greater than zero, some particles will last a shorter time than others. public float DurationRandomness = 0; // Controls how much particles are influenced by the velocity of the object // which created them. You can see this in action with the explosion effect, // where the flames continue to move in the same direction as the source // projectile. The projectile trail particles, on the other hand, set this // value very low so they are less affected by the velocity of the projectile. public float EmitterVelocitySensitivity = 1; // Range of values controlling how much X and Z axis velocity to give each // particle. Values for individual particles are randomly chosen from somewhere // between these limits. public float MinHorizontalVelocity = 0; public float MaxHorizontalVelocity = 0; // Range of values controlling how much Y axis velocity to give each particle. // Values for individual particles are randomly chosen from somewhere between // these limits. public float MinVerticalVelocity = 0; public float MaxVerticalVelocity = 0; // Direction and strength of the gravity effect. Note that this can point in any // direction, not just down! The fire effect points it upward to make the flames // rise, and the smoke plume points it sideways to simulate wind. public Vector3 Gravity = Vector3.Zero; // Controls how the particle velocity will change over their lifetime. If set // to 1, particles will keep going at the same speed as when they were created. // If set to 0, particles will come to a complete stop right before they die. // Values greater than 1 make the particles speed up over time. public float EndVelocity = 1; // Range of values controlling the particle color and alpha. Values for // individual particles are randomly chosen from somewhere between these limits. public Color MinColor = Color.White; public Color MaxColor = Color.White; // Range of values controlling how fast the particles rotate. Values for // individual particles are randomly chosen from somewhere between these // limits. If both these values are set to 0, the particle system will // automatically switch to an alternative shader technique that does not // support rotation, and thus requires significantly less GPU power. This // means if you don't need the rotation effect, you may get a performance // boost from leaving these values at 0. public float MinRotateSpeed = 0; public float MaxRotateSpeed = 0; // Range of values controlling how big the particles are when first created. // Values for individual particles are randomly chosen from somewhere between // these limits. public float MinStartSize = 100; public float MaxStartSize = 100; // Range of values controlling how big particles become at the end of their // life. Values for individual particles are randomly chosen from somewhere // between these limits. public float MinEndSize = 100; public float MaxEndSize = 100; // Alpha blending settings. public Blend SourceBlend = Blend.SourceAlpha; public Blend DestinationBlend = Blend.InverseSourceAlpha; } } ================================================ FILE: Engine/ParticleSystem/ParticleSystem.cs ================================================ #region File Description //----------------------------------------------------------------------------- // ParticleSystem.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics.PackedVector; using System.Collections.Generic; using System.Diagnostics; using System.IO; #endregion namespace GameEngine { /// /// The main component in charge of displaying particles. /// public abstract class ParticleSystem { #region Fields // Settings class controls the appearance and animation of this particle system. ParticleSettings settings = new ParticleSettings(); // Custom effect for drawing particles. This computes the particle // animation entirely in the vertex shader: no per-particle CPU work required! Effect particleEffect; // Shortcuts for accessing frequently changed effect parameters. EffectParameter effectViewParameter; EffectParameter effectProjectionParameter; EffectParameter effectViewportScaleParameter; EffectParameter effectTimeParameter; // An array of particles, treated as a circular queue. ParticleVertex[] particles; // A vertex buffer holding our particles. This contains the same data as // the particles array, but copied across to where the GPU can access it. DynamicVertexBuffer vertexBuffer; // Index buffer turns sets of four vertices into particle quads (pairs of triangles). IndexBuffer indexBuffer; // The particles array and vertex buffer are treated as a circular queue. // Initially, the entire contents of the array are free, because no particles // are in use. When a new particle is created, this is allocated from the // beginning of the array. If more than one particle is created, these will // always be stored in a consecutive block of array elements. Because all // particles last for the same amount of time, old particles will always be // removed in order from the start of this active particle region, so the // active and free regions will never be intermingled. Because the queue is // circular, there can be times when the active particle region wraps from the // end of the array back to the start. The queue uses modulo arithmetic to // handle these cases. For instance with a four entry queue we could have: // // 0 // 1 - first active particle // 2 // 3 - first free particle // // In this case, particles 1 and 2 are active, while 3 and 4 are free. // Using modulo arithmetic we could also have: // // 0 // 1 - first free particle // 2 // 3 - first active particle // // Here, 3 and 0 are active, while 1 and 2 are free. // // But wait! The full story is even more complex. // // When we create a new particle, we add them to our managed particles array. // We also need to copy this new data into the GPU vertex buffer, but we don't // want to do that straight away, because setting new data into a vertex buffer // can be an expensive operation. If we are going to be adding several particles // in a single frame, it is faster to initially just store them in our managed // array, and then later upload them all to the GPU in one single call. So our // queue also needs a region for storing new particles that have been added to // the managed array but not yet uploaded to the vertex buffer. // // Another issue occurs when old particles are retired. The CPU and GPU run // asynchronously, so the GPU will often still be busy drawing the previous // frame while the CPU is working on the next frame. This can cause a // synchronization problem if an old particle is retired, and then immediately // overwritten by a new one, because the CPU might try to change the contents // of the vertex buffer while the GPU is still busy drawing the old data from // it. Normally the graphics driver will take care of this by waiting until // the GPU has finished drawing inside the VertexBuffer.SetData call, but we // don't want to waste time waiting around every time we try to add a new // particle! To avoid this delay, we can specify the SetDataOptions.NoOverwrite // flag when we write to the vertex buffer. This basically means "I promise I // will never try to overwrite any data that the GPU might still be using, so // you can just go ahead and update the buffer straight away". To keep this // promise, we must avoid reusing vertices immediately after they are drawn. // // So in total, our queue contains four different regions: // // Vertices between firstActiveParticle and firstNewParticle are actively // being drawn, and exist in both the managed particles array and the GPU // vertex buffer. // // Vertices between firstNewParticle and firstFreeParticle are newly created, // and exist only in the managed particles array. These need to be uploaded // to the GPU at the start of the next draw call. // // Vertices between firstFreeParticle and firstRetiredParticle are free and // waiting to be allocated. // // Vertices between firstRetiredParticle and firstActiveParticle are no longer // being drawn, but were drawn recently enough that the GPU could still be // using them. These need to be kept around for a few more frames before they // can be reallocated. int firstActiveParticle; int firstNewParticle; int firstFreeParticle; int firstRetiredParticle; // Store the current time, in seconds. float currentTime; // Count how many times Draw has been called. This is used to know // when it is safe to retire old particles back into the free list. int drawCounter; // Shared random number generator. static Random random = new Random(); #endregion public static List AllParticleSystems = new List(); #region Initialization /// /// Initializes the component. /// public void InitializeSystem() { InitializeSettings(settings); // Allocate the particle array, and fill in the corner fields (which never change). particles = new ParticleVertex[settings.MaxParticles * 4]; for (int i = 0; i < settings.MaxParticles; i++) { particles[i * 4 + 0].Corner = new Short2(-1, -1); particles[i * 4 + 1].Corner = new Short2(1, -1); particles[i * 4 + 2].Corner = new Short2(1, 1); particles[i * 4 + 3].Corner = new Short2(-1, 1); } LoadContent(); AllParticleSystems.Add(this); } /// /// Derived particle system classes should override this method /// and use it to initalize their tweakable settings. /// protected abstract void InitializeSettings(ParticleSettings settings); /// /// Loads graphics for the particle system. /// protected void LoadContent() { LoadParticleEffect(); vertexBuffer = new DynamicVertexBuffer(Engine.Instance.Device, ParticleVertex.VertexDeclaration, settings.MaxParticles * 4, BufferUsage.WriteOnly); // Create and populate the index buffer. ushort[] indices = new ushort[settings.MaxParticles * 6]; for (int i = 0; i < settings.MaxParticles; i++) { indices[i * 6 + 0] = (ushort)(i * 4 + 0); indices[i * 6 + 1] = (ushort)(i * 4 + 1); indices[i * 6 + 2] = (ushort)(i * 4 + 2); indices[i * 6 + 3] = (ushort)(i * 4 + 0); indices[i * 6 + 4] = (ushort)(i * 4 + 2); indices[i * 6 + 5] = (ushort)(i * 4 + 3); } indexBuffer = new IndexBuffer(Engine.Instance.Device, typeof(ushort), indices.Length, BufferUsage.WriteOnly); indexBuffer.SetData(indices); } /// /// Helper for loading and initializing the particle effect. /// void LoadParticleEffect() { //Effect effect = Engine.Instance.ContentManager.Load("Content/ParticleEffect"); // If we have several particle systems, the content manager will return // a single shared effect instance to them all. But we want to preconfigure // the effect with parameters that are specific to this particular // particle system. By cloning the effect, we prevent one particle system // from stomping over the parameter settings of another. //particleEffect = new Effect(Engine.Instance.Device, File.ReadAllBytes("Content\\ParticleEffect.mgfx")); particleEffect = Engine.Instance.ContentManager.Load("Content/ParticleEffect"); EffectParameterCollection parameters = particleEffect.Parameters; // Look up shortcuts for parameters that change every frame. effectViewParameter = parameters["View"]; effectProjectionParameter = parameters["Projection"]; effectViewportScaleParameter = parameters["ViewportScale"]; effectTimeParameter = parameters["CurrentTime"]; // Set the values of parameters that do not change. parameters["Duration"].SetValue((float)settings.Duration.TotalSeconds); parameters["DurationRandomness"].SetValue(settings.DurationRandomness); parameters["Gravity"].SetValue(settings.Gravity); parameters["EndVelocity"].SetValue(settings.EndVelocity); parameters["MinColor"].SetValue(settings.MinColor.ToVector4()); parameters["MaxColor"].SetValue(settings.MaxColor.ToVector4()); parameters["RotateSpeed"].SetValue( new Vector2(settings.MinRotateSpeed, settings.MaxRotateSpeed)); parameters["StartSize"].SetValue( new Vector2(settings.MinStartSize, settings.MaxStartSize)); parameters["EndSize"].SetValue( new Vector2(settings.MinEndSize, settings.MaxEndSize)); parameters["Texture"].SetValue(settings.Texture); } #endregion #region Update and Draw /// /// Updates the particle system. /// public void Update() { currentTime += Engine.Instance.FrameTime; RetireActiveParticles(); FreeRetiredParticles(); // If we let our timer go on increasing for ever, it would eventually // run out of floating point precision, at which point the particles // would render incorrectly. An easy way to prevent this is to notice // that the time value doesn't matter when no particles are being drawn, // so we can reset it back to zero any time the active queue is empty. if (firstActiveParticle == firstFreeParticle) currentTime = 0; if (firstRetiredParticle == firstActiveParticle) drawCounter = 0; } /// /// Helper for checking when active particles have reached the end of /// their life. It moves old particles from the active area of the queue /// to the retired section. /// void RetireActiveParticles() { float particleDuration = (float)settings.Duration.TotalSeconds; while (firstActiveParticle != firstNewParticle) { // Is this particle old enough to retire? // We multiply the active particle index by four, because each // particle consists of a quad that is made up of four vertices. float particleAge = currentTime - particles[firstActiveParticle * 4].Time; if (particleAge < particleDuration) break; // Remember the time at which we retired this particle. particles[firstActiveParticle * 4].Time = drawCounter; // Move the particle from the active to the retired queue. firstActiveParticle++; if (firstActiveParticle >= settings.MaxParticles) firstActiveParticle = 0; } } /// /// Helper for checking when retired particles have been kept around long /// enough that we can be sure the GPU is no longer using them. It moves /// old particles from the retired area of the queue to the free section. /// void FreeRetiredParticles() { while (firstRetiredParticle != firstActiveParticle) { // Has this particle been unused long enough that // the GPU is sure to be finished with it? // We multiply the retired particle index by four, because each // particle consists of a quad that is made up of four vertices. int age = drawCounter - (int)particles[firstRetiredParticle * 4].Time; // The GPU is never supposed to get more than 2 frames behind the CPU. // We add 1 to that, just to be safe in case of buggy drivers that // might bend the rules and let the GPU get further behind. if (age < 3) break; // Move the particle from the retired to the free queue. firstRetiredParticle++; if (firstRetiredParticle >= settings.MaxParticles) firstRetiredParticle = 0; } } /// /// Draws the particle system. /// public void Render() { GraphicsDevice device = Engine.Instance.Device; ICamera camera = Engine.Instance.Camera; effectViewParameter.SetValue(camera.View); effectProjectionParameter.SetValue(camera.Projection); // Restore the vertex buffer contents if the graphics device was lost. if (vertexBuffer.IsContentLost) { vertexBuffer.SetData(particles); } // If there are any particles waiting in the newly added queue, // we'd better upload them to the GPU ready for drawing. if (firstNewParticle != firstFreeParticle) { AddNewParticlesToVertexBuffer(); } // If there are any active particles, draw them now! if (firstActiveParticle != firstFreeParticle) { SetParticleRenderStates(); // Set an effect parameter describing the viewport size. This is // needed to convert particle sizes into screen space point sizes. effectViewportScaleParameter.SetValue(new Vector2(0.5f / device.Viewport.AspectRatio, -0.5f)); // Set an effect parameter describing the current time. All the vertex // shader particle animation is keyed off this value. effectTimeParameter.SetValue(currentTime); // Set the particle vertex and index buffer. device.SetVertexBuffer(vertexBuffer); device.Indices = indexBuffer; // Activate the particle effect. foreach (EffectPass pass in particleEffect.CurrentTechnique.Passes) { pass.Apply(); if (firstActiveParticle < firstFreeParticle) { // If the active particles are all in one consecutive range, // we can draw them all in a single call. device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, /*firstActiveParticle * 4*/ 0, (firstFreeParticle), firstActiveParticle * 6, (firstFreeParticle - firstActiveParticle) * 2); } else { // If the active particle range wraps past the end of the queue // back to the start, we must split them over two draw calls. device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, /*firstActiveParticle * 4*/ 0, (settings.MaxParticles), firstActiveParticle * 6, (settings.MaxParticles - firstActiveParticle) * 2); if (firstFreeParticle > 0) { device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, firstFreeParticle * 4, 0, firstFreeParticle * 2); } } } // Reset some of the renderstates that we changed, // so as not to mess up any other subsequent drawing. device.DepthStencilState = DepthStencilState.Default; device.BlendState = BlendState.Opaque; } drawCounter++; } /// /// Helper for uploading new particles from our managed /// array to the GPU vertex buffer. /// void AddNewParticlesToVertexBuffer() { int stride = ParticleVertex.SizeInBytes; if (firstNewParticle < firstFreeParticle) { // If the new particles are all in one consecutive range, // we can upload them all in a single call. vertexBuffer.SetData(firstNewParticle * stride * 4, particles, firstNewParticle * 4, (firstFreeParticle - firstNewParticle) * 4, stride, SetDataOptions.NoOverwrite); } else { // If the new particle range wraps past the end of the queue // back to the start, we must split them over two upload calls. vertexBuffer.SetData(firstNewParticle * stride * 4, particles, firstNewParticle * 4, (settings.MaxParticles - firstNewParticle) * 4, stride, SetDataOptions.NoOverwrite); if (firstFreeParticle > 0) { vertexBuffer.SetData(0, particles, 0, firstFreeParticle * 4, stride, SetDataOptions.NoOverwrite); } } // Move the particles we just uploaded from the new to the active queue. firstNewParticle = firstFreeParticle; } #endregion #region Public Methods /// /// Adds a new particle to the system. /// public void AddParticle(Vector3 position, Vector3 velocity) { // Figure out where in the circular queue to allocate the new particle. int nextFreeParticle = firstFreeParticle + 1; if (nextFreeParticle >= settings.MaxParticles) nextFreeParticle = 0; // If there are no free particles, we just have to give up. if (nextFreeParticle == firstRetiredParticle) { return; } // Adjust the input velocity based on how much // this particle system wants to be affected by it. velocity *= settings.EmitterVelocitySensitivity; // Add in some random amount of horizontal velocity. float horizontalVelocity = MathHelper.Lerp(settings.MinHorizontalVelocity, settings.MaxHorizontalVelocity, (float)random.NextDouble()); double horizontalAngle = random.NextDouble() * MathHelper.TwoPi; velocity.X += horizontalVelocity * (float)Math.Cos(horizontalAngle); velocity.Z += horizontalVelocity * (float)Math.Sin(horizontalAngle); // Add in some random amount of vertical velocity. velocity.Y += MathHelper.Lerp(settings.MinVerticalVelocity, settings.MaxVerticalVelocity, (float)random.NextDouble()); // Choose four random control values. These will be used by the vertex // shader to give each particle a different size, rotation, and color. Color randomValues = new Color((byte)random.Next(255), (byte)random.Next(255), (byte)random.Next(255), (byte)random.Next(255)); // Fill in the particle vertex structure. for (int i = 0; i < 4; i++) { particles[firstFreeParticle * 4 + i].Position = position; particles[firstFreeParticle * 4 + i].Velocity = velocity; particles[firstFreeParticle * 4 + i].Random = randomValues; particles[firstFreeParticle * 4 + i].Time = currentTime; } firstFreeParticle = nextFreeParticle; } #endregion void SetParticleRenderStates() { Engine.Instance.Device.BlendState = BlendState.NonPremultiplied; // Set the alpha blend mode. //renderState.AlphaBlendEnable = true; //renderState.AlphaBlendOperation = BlendFunction.Add; //renderState.SourceBlend = settings.SourceBlend; //renderState.DestinationBlend = settings.DestinationBlend; // Set the alpha test mode. //renderState.AlphaTestEnable = true; //renderState.AlphaFunction = CompareFunction.Greater; //renderState.ReferenceAlpha = 0; // Enable the depth buffer (so particles will not be visible through // solid objects like the ground plane), but disable depth writes // (so particles will not obscure other particles). Engine.Instance.Device.DepthStencilState = DepthStencilState.DepthRead; } public void Clear() { firstActiveParticle = 0; firstNewParticle = 0; firstFreeParticle = 0; firstRetiredParticle = 0; } /// /// Sets the camera view and projection matrices /// that will be used to draw this particle system. /// public void SetCamera(ICamera camera) { effectViewParameter.SetValue(camera.View); effectProjectionParameter.SetValue(camera.Projection); } } } ================================================ FILE: Engine/ParticleSystem/ParticleVertex.cs ================================================ #region File Description //----------------------------------------------------------------------------- // ParticleVertex.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics.PackedVector; #endregion namespace GameEngine { /// /// Custom vertex structure for drawing point sprite particles. /// struct ParticleVertex { // Stores which corner of the particle quad this vertex represents. public Short2 Corner; // Stores the starting position of the particle. public Vector3 Position; // Stores the starting velocity of the particle. public Vector3 Velocity; // Four random values, used to make each particle look slightly different. public Color Random; // The time (in seconds) at which this particle was created. public float Time; // Describe the layout of this vertex structure. public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration ( new VertexElement(0, VertexElementFormat.Short2, VertexElementUsage.Position, 0), new VertexElement(4, VertexElementFormat.Vector3, VertexElementUsage.Position, 1), new VertexElement(16, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0), new VertexElement(28, VertexElementFormat.Color, VertexElementUsage.Color, 0), new VertexElement(32, VertexElementFormat.Single, VertexElementUsage.TextureCoordinate, 0) ); // Describe the size of this vertex structure. public const int SizeInBytes = 36; } } ================================================ FILE: Engine/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; 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("Engine")] [assembly: AssemblyProduct("Engine")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyDescription("")] [assembly: AssemblyCompany("")] [assembly: AssemblyCopyright("Copyright © 2013")] [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("45cbce85-4b7b-4ec4-b30b-8489311f3329")] // 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")] ================================================ FILE: Engine/ScreenEffects.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace GameEngine { public class ScreenEffects : IDrawableObject { public event EventHandler FadeCompleted; private enum FadeDirection { None, FadeIn, FadeOut } private static ScreenEffects _instance; public static ScreenEffects Instance { get { if (_instance == null) _instance = new ScreenEffects(); return _instance; } } private float _alpha; private FadeDirection _fadeDirection; private Texture2D _fadeTexture; public float FadeSpeed = 350; private ScreenEffects() { _fadeDirection = FadeDirection.None; _fadeTexture = new Texture2D(Engine.Instance.Device, 1, 1); _fadeTexture.SetData(new Color[] { Color.Black }); } /// /// Helper draws a translucent black fullscreen sprite, used for fading /// screens in and out, and for darkening the background behind popups. /// public void FadeScreen() { _alpha = 0; _fadeDirection = FadeDirection.FadeOut; } public void UnFadeScreen() { _alpha = 255; _fadeDirection = FadeDirection.FadeIn; } #region IDrawableObject Members public void Update(GameTime gameTime) { if (_fadeDirection == FadeDirection.FadeOut) { _alpha += FadeSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds; if (_alpha >= 255) CompleteFade(); } else if (_fadeDirection == FadeDirection.FadeIn) { _alpha -= FadeSpeed * 0.5f * (float)gameTime.ElapsedGameTime.TotalSeconds; if (_alpha <= 0) CompleteFade(); } } public void Draw() { if (_fadeDirection != FadeDirection.None) { Viewport viewport = Engine.Instance.Device.Viewport; Engine.Instance.SpriteBatch.Begin(); Engine.Instance.SpriteBatch.Draw(_fadeTexture, new Rectangle(0, 0, viewport.Width, viewport.Height), new Color(255, 255, 255, (byte)_alpha)); Engine.Instance.SpriteBatch.End(); } } private void CompleteFade() { _fadeDirection = FadeDirection.None; if (FadeCompleted != null) { FadeCompleted(this, null); } } #endregion public static Texture2D TakeScreenshot() { int w = Engine.Instance.Device.PresentationParameters.BackBufferWidth; int h = Engine.Instance.Device.PresentationParameters.BackBufferHeight; int[] backBuffer = new int[w * h]; //copy into a texture Texture2D texture = new Texture2D(Engine.Instance.Device, w, h, false, Engine.Instance.Device.PresentationParameters.BackBufferFormat); texture.SetData(backBuffer); return texture; } } } ================================================ FILE: Engine/SimpleCamera.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; namespace GameEngine { public class SimpleCamera : ICamera { public SimpleCamera() { } public Vector3 RightVec = Vector3.Right; public Vector3 UpVector = Vector3.Up; /// /// Look at point in world space. /// public Vector3 LookAt { get { return lookAt; } set { lookAt = value; } } private Vector3 lookAt; /// /// Position of camera in world space. /// public Vector3 Position { get { return _position; } set { _position = value; } } private Vector3 _position; /// /// Perspective field of view. /// public float FieldOfView { get { return fieldOfView; } set { fieldOfView = value; } } private float fieldOfView = MathHelper.ToRadians(45.0f); /// /// Distance to the near clipping plane. /// public float NearPlaneDistance { get { return nearPlaneDistance; } set { nearPlaneDistance = value; } } private float nearPlaneDistance = 1.0f; /// /// Distance to the far clipping plane. /// public float FarPlaneDistance { get { return farPlaneDistance; } set { farPlaneDistance = value; } } private float farPlaneDistance = 15000.0f; /// /// View transform matrix. /// public Matrix View { get { return view; } } private Matrix view; /// /// Projecton transform matrix. /// public Matrix Projection { get { return projection; } } private Matrix projection; /// /// Animates the camera from its current position towards the desired offset /// behind the chased object. The camera's animation is controlled by a simple /// physical spring attached to the camera and anchored to the desired position. /// public void Update(GameTime gameTime) { view = Matrix.CreateLookAt(this.Position, this.LookAt, UpVector); projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView, Engine.Instance.AspectRatio, NearPlaneDistance, FarPlaneDistance); } public void SetPosition(Vector3 position) { _position = position; } public void FollowObject(GameObject obj) { } } } ================================================ FILE: Engine/SkyBox.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using GameEngine; using System.IO; namespace GameEngine { public class SkyBox : IDrawableObject { Texture2D[] textures = new Texture2D[6]; public Texture2D[] Textures { get { return textures; } set { textures = value; } } AlphaTestEffect _effect; VertexBuffer vertices; IndexBuffer indices; float YOffset = 15f; public SkyBox() { LoadResources(); } public void LoadResources() { _effect = new AlphaTestEffect(Engine.Instance.Device); vertices = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), 4 * 6, BufferUsage.WriteOnly); VertexPositionTexture[] data = new VertexPositionTexture[4 * 6]; float y = 0.3f; #region Define Vertexes Vector3 vExtents = new Vector3(1400, 450, 800); //back data[0].Position = new Vector3(vExtents.X, -vExtents.Y * y, -vExtents.Z); data[0].TextureCoordinate.X = 1.5f; data[0].TextureCoordinate.Y = 1.0f; data[1].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z); data[1].TextureCoordinate.X = 1.5f; data[1].TextureCoordinate.Y = 0.0f; data[2].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z); data[2].TextureCoordinate.X = 0.5f; data[2].TextureCoordinate.Y = 0.0f; data[3].Position = new Vector3(-vExtents.X, -vExtents.Y * y, -vExtents.Z); data[3].TextureCoordinate.X = 0.5f; data[3].TextureCoordinate.Y = 1.0f; //front data[4].Position = new Vector3(-vExtents.X, -vExtents.Y * y, vExtents.Z); data[4].TextureCoordinate.X = 1.5f; data[4].TextureCoordinate.Y = 1.0f; data[5].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z); data[5].TextureCoordinate.X = 1.5f; data[5].TextureCoordinate.Y = 0.0f; data[6].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z); data[6].TextureCoordinate.X = 0.5f; data[6].TextureCoordinate.Y = 0.0f; data[7].Position = new Vector3(vExtents.X, -vExtents.Y * y, vExtents.Z); data[7].TextureCoordinate.X = 0.5f; data[7].TextureCoordinate.Y = 1.0f; //bottom data[8].Position = new Vector3(-vExtents.X, -vExtents.Y * y, -vExtents.Z); data[8].TextureCoordinate.X = 1.5f; data[8].TextureCoordinate.Y = 0.0f; data[9].Position = new Vector3(-vExtents.X, -vExtents.Y * y, vExtents.Z); data[9].TextureCoordinate.X = 1.5f; data[9].TextureCoordinate.Y = 1.0f; data[10].Position = new Vector3(vExtents.X, -vExtents.Y * y, vExtents.Z); data[10].TextureCoordinate.X = 0.5f; data[10].TextureCoordinate.Y = 1.0f; data[11].Position = new Vector3(vExtents.X, -vExtents.Y * y, -vExtents.Z); data[11].TextureCoordinate.X = 0.5f; data[11].TextureCoordinate.Y = 0.0f; //top data[12].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z); data[12].TextureCoordinate.X = 0.5f; data[12].TextureCoordinate.Y = 0.0f; data[13].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z); data[13].TextureCoordinate.X = 0.5f; data[13].TextureCoordinate.Y = 1.0f; data[14].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z); data[14].TextureCoordinate.X = 1.5f; data[14].TextureCoordinate.Y = 1.0f; data[15].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z); data[15].TextureCoordinate.X = 1.5f; data[15].TextureCoordinate.Y = 0.0f; //left data[16].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z); data[16].TextureCoordinate.X = 1.5f; data[16].TextureCoordinate.Y = 0.0f; data[17].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z); data[17].TextureCoordinate.X = 0.5f; data[17].TextureCoordinate.Y = 0.0f; data[18].Position = new Vector3(-vExtents.X, -vExtents.Y * y, vExtents.Z); data[18].TextureCoordinate.X = 0.5f; data[18].TextureCoordinate.Y = 1.0f; data[19].Position = new Vector3(-vExtents.X, -vExtents.Y * y, -vExtents.Z); data[19].TextureCoordinate.X = 1.5f; data[19].TextureCoordinate.Y = 1.0f; //right data[20].Position = new Vector3(vExtents.X, -vExtents.Y * y, -vExtents.Z); data[20].TextureCoordinate.X = 0.5f; data[20].TextureCoordinate.Y = 1.0f; data[21].Position = new Vector3(vExtents.X, -vExtents.Y * y, vExtents.Z); data[21].TextureCoordinate.X = 1.5f; data[21].TextureCoordinate.Y = 1.0f; data[22].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z); data[22].TextureCoordinate.X = 1.5f; data[22].TextureCoordinate.Y = 0.0f; data[23].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z); data[23].TextureCoordinate.X = 0.5f; data[23].TextureCoordinate.Y = 0.0f; vertices.SetData(data); indices = new IndexBuffer(Engine.Instance.Device, typeof(short), 6 * 6, BufferUsage.WriteOnly); short[] ib = new short[6 * 6]; for (int x = 0; x < 6; x++) { ib[x * 6 + 0] = (short)(x * 4 + 0); ib[x * 6 + 2] = (short)(x * 4 + 1); ib[x * 6 + 1] = (short)(x * 4 + 2); ib[x * 6 + 3] = (short)(x * 4 + 2); ib[x * 6 + 5] = (short)(x * 4 + 3); ib[x * 6 + 4] = (short)(x * 4 + 0); } indices.SetData(ib); #endregion } public void Update(GameTime gameTime) { var pos = Engine.Instance.Camera.Position; pos.Y += YOffset; _effect.World = Matrix.CreateTranslation(pos); _effect.Projection = Engine.Instance.Camera.Projection; _effect.View = Engine.Instance.Camera.View; } public void Draw() { if (vertices == null) return; GraphicsDevice device = Engine.Instance.Device; var ds = DepthStencilState.None; device.DepthStencilState = ds; device.SamplerStates[0] = SamplerState.LinearWrap; device.RasterizerState = RasterizerState.CullCounterClockwise; device.SetVertexBuffer(vertices); device.Indices = indices; _effect.CurrentTechnique.Passes[0].Apply(); for (int x = 0; x < 6; x++) { if (textures[x] == null) continue; _effect.Texture = textures[x]; _effect.Techniques[0].Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.VertexCount, x * 6, 2); } device.DepthStencilState = DepthStencilState.Default; } } } ================================================ FILE: Engine/SoundEngine2.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework; namespace GameEngine { class SoundEffectDescriptor { public SoundEffectInstance Effect; public float RemainingDuration; } public class SoundEngine2 { private static SoundEngine2 _instance; public static SoundEngine2 Instance { get { if (_instance == null) _instance = new SoundEngine2(); return _instance; } } private List _effects = new List(); private SoundEngine2() { } public void PlayEffect(SoundEffectInstance effect, float duration) { SoundEffectDescriptor sd = new SoundEffectDescriptor(); sd.Effect = effect; sd.RemainingDuration = duration; _effects.Add(sd); } public void Update(GameTime gameTime) { for (int i = _effects.Count - 1; i >= 0; i--) { _effects[i].RemainingDuration -= (float)gameTime.ElapsedGameTime.TotalSeconds; if (_effects[i].RemainingDuration <= 0) { _effects[i].Effect.Stop(); _effects.RemoveAt(i); } } } } } ================================================ FILE: Engine/Utility.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace GameEngine { public class Triangle { public Vector3 V1, V2, V3; public Triangle(Vector3 v1, Vector3 v2, Vector3 v3) { V1 = v1; V2 = v2; V3 = v3; } } public class Utility { public static float Epsilon = 1E-5f; //for numerical imprecision public static Random RandomGenerator = new Random(); public static bool FindRayTriangleIntersection(ref Vector3 rayOrigin, Vector3 rayDirection, float maximumLength, ref Vector3 a, ref Vector3 b, ref Vector3 c, out Vector3 hitLocation, out float t) { hitLocation = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity); t = float.NegativeInfinity; Vector3 vector = b - a; Vector3 vector2 = c - a; Vector3 vector4 = Vector3.Cross(rayDirection, vector2); float num = Vector3.Dot(vector, vector4); if ((num > -1E-07f) && (num < 1E-07f)) { return false; } float num2 = 1f / num; Vector3 vector3 = rayOrigin - a; float num3 = Vector3.Dot(vector3, vector4) * num2; if ((num3 < -0.01f) || (num3 > 1.01f)) { return false; } Vector3 vector5 = Vector3.Cross(vector3, vector); float num4 = Vector3.Dot(rayDirection, vector5) * num2; if ((num4 < -0.01f) || ((num3 + num4) > 1.01f)) { return false; } t = Vector3.Dot(vector2, vector5) * num2; if ((t > maximumLength) || (t < 0f)) { t = float.NegativeInfinity; return false; } hitLocation = rayOrigin + (t * rayDirection); return true; } public static Vector3 RotatePoint(Vector2 point, float degrees) { float cDegrees = ((float)Math.PI * degrees) / 180.0f; //Radians float cosDegrees = (float)Math.Cos(cDegrees); float sinDegrees = (float)Math.Sin(cDegrees); float x = (point.X * cosDegrees) + (point.Y * sinDegrees); float z = (point.X * -sinDegrees) + (point.Y * cosDegrees); return new Vector3(x, 0, -z); } public static bool IsLeftOfLine(Vector3 line1, Vector3 line2, Vector3 pos) { return ((line2.X - line1.X) * (pos.Z - line1.Z) - (line2.Z - line1.Z) * (pos.X - line1.X)) > 0; } public static Vector3 GetClosestPointOnLine(Vector3 line1, Vector3 line2, Vector3 pos) { Vector3 lineDir = line2 - line1; lineDir.Normalize(); float d = Vector3.Distance(line1, line2); Vector3 v1 = pos - line1; float t = Vector3.Dot(lineDir, v1); if (t <= 0) return line1; if (t >= d) return line2; return line1 + lineDir * t; } public static float GetSignedAngleBetweenVectors(Vector3 from, Vector3 to, bool ignoreY) { if (ignoreY) from.Y = to.Y = 0; from.Normalize(); to.Normalize(); Vector3 toRight = Vector3.Cross(to, Vector3.Up); toRight.Normalize(); float forwardDot = Vector3.Dot(from, to); float rightDot = Vector3.Dot(from, toRight); // Keep dot in range to prevent rounding errors forwardDot = MathHelper.Clamp(forwardDot, -1.0f, 1.0f); double angleBetween = Math.Acos(forwardDot); if (rightDot < 0.0f) angleBetween *= -1.0f; return (float)angleBetween; } } } ================================================ FILE: Installer/OpenNFS1.nsi ================================================ !include LogicLib.nsh ; The name of the installer Name "OpenNFS1" ; The file to write OutFile "OpenNFS1_Install-v1.2.exe" ; The default installation directory InstallDir $PROGRAMFILES\OpenNFS1 ; Registry key to check for directory (so if you install again, it will ; overwrite the old one automatically) InstallDirRegKey HKLM "Software\OpenNFS1" "Install_Dir" ; Request application privileges for Windows Vista RequestExecutionLevel admin ;-------------------------------- ; Pages Page components Page directory Page instfiles UninstPage uninstConfirm UninstPage instfiles ;-------------------------------- ; The stuff to install Section "OpenNFS1 (required)" SectionIn RO ; Set output path to the installation directory. SetOutPath $INSTDIR ; Include all files in the deploy folder File /r "deploy\*.*" File /r "3rdparty" File "readme.txt" Call CheckAndInstallDotNet Call InstallOAL ; Write the installation path into the registry WriteRegStr HKLM SOFTWARE\OpenNFS1 "Install_Dir" "$INSTDIR" ; Write the uninstall keys for Windows WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenNFS1" "DisplayName" "OpenNFS1" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenNFS1" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenNFS1" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenNFS1" "NoRepair" 1 WriteUninstaller "uninstall.exe" SectionEnd ; Optional section (can be disabled by the user) Section "Start Menu Shortcuts" CreateDirectory "$SMPROGRAMS\OpenNFS1" CreateShortcut "$SMPROGRAMS\OpenNFS1\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 CreateShortcut "$SMPROGRAMS\OpenNFS1\OpenNFS1.lnk" "$INSTDIR\OpenNFS1.exe" "" "$INSTDIR\OpenNFS1.exe" 0 SectionEnd ;-------------------------------- ; Uninstaller Section "Uninstall" ; Remove registry keys DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenNFS1" DeleteRegKey HKLM SOFTWARE\OpenNFS1 ; Remove shortcuts Delete "$SMPROGRAMS\OpenNFS1\*.*" ; Remove directories used RMDir "$SMPROGRAMS\OpenNFS1" RMDir /R "$INSTDIR" SectionEnd Function CheckAndInstallDotNet ; Installer dotNetFx45_Full_setup.exe avalible from http://msdn.microsoft.com/en-us/library/5a4x27ek.aspx ; Magic numbers from http://msdn.microsoft.com/en-us/library/ee942965.aspx ClearErrors ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" "Release" IfErrors NotDetected ${If} $0 >= 978389 DetailPrint "Microsoft .NET Framework 4.5 is installed ($0)" ${Else} NotDetected: MessageBox MB_YESNO|MB_ICONQUESTION ".NET Framework 4.5+ is required, \ do you want to launch the web installer? This requires a valid internet connection." IDYES InstallDotNet IDNO Cancel Cancel: MessageBox MB_ICONEXCLAMATION "To install, Microsoft's .NET Framework v4.5 \ (or higher) must be installed. Cannot proceed with the installation!" # ${OpenURL} "${WWW_MS_DOTNET4_5}" RMDir /r "$INSTDIR" SetOutPath "$PROGRAMFILES" RMDir "$INSTDIR" Abort ; Install .NET4.5. InstallDotNet: DetailPrint "Installing Microsoft .NET Framework 4.5" SetDetailsPrint listonly ExecWait '"$INSTDIR\3rdparty\dotNetFx45_Full_setup.exe" /passive /norestart' $0 ${If} $0 == 3010 ${OrIf} $0 == 1641 DetailPrint "Microsoft .NET Framework 4.5 installer requested reboot." SetRebootFlag true ${EndIf} SetDetailsPrint lastused DetailPrint "Microsoft .NET Framework 4.5 installer returned $0" ${EndIf} FunctionEnd Function InstallOAL ExecWait '"$INSTDIR\3rdparty\oalinst.exe" /s' $0 FunctionEnd ================================================ FILE: Installer/build-installer.bat ================================================ rmdir /S /Q deploy mkdir deploy set src=..\OpenNFS1\bin\WindowsGL\Debug set dest=.\deploy xcopy %src%\Content %dest%\Content\ /s /e copy %src%\gameconfig.json %dest% copy %src%\OpenNFS1.exe %dest% copy %src%\*.dll %dest% copy %src%\*.pdb %dest% del %dest%\IgnoreMe.dll "C:\Program Files (x86)\NSIS\makensis" /V1 OpenNFS1.nsi ================================================ FILE: Installer/readme.txt ================================================ OpenNFS1 v1.2 ---------------- Web: http://www.1amstudios.com/projects/opennfs1 Src: https://github.com/jeff-1amstudios/OpenNFS1 Email: jeff@1amstudios.com OpenNFS1 is a ground-up remake of the original EA Need for Speed 1. The code is all written from scratch without reverse engineering executables, and it uses the original data files that were on the CD back in 1995! The format of the various binary data files was worked out by Ian Brown, Denis Auroux and myself. Main features: Written in C# and XNA. (Now converted to Monogame) 16 tracks with scenery, animations, tunnels 9 drivable cars with animated dashboards Can drive tracks backwards (not allowed in the original) Can drive past the finish signs on open road stages to see the real end of the track (probably never seen by anyone except by the original developers!) Keys: C - Change camera Up - Accelerate Down - Brake Left / Right - Steer Space - Handbrake A / Z - Gear up down in manual gearbox mode F1 - Toggle debug mode. This enables an FPS camera which can be moved independantly of the car Models, textures, tracks, cars by Pioneer Productions / EA Seattle (C) 1995. OpenNFS1 is not affiated in any way with EA or Pioneer Productions ================================================ FILE: NFSSpecs.txt ================================================ ====================================================================== THE UNOFFICIAL NEED FOR SPEED FILE FORMAT SPECIFICATIONS - Version 0.2 Copyright (c) 1995-96, Denis AUROUX (MXK) - auroux@clipper.ens.fr ====================================================================== Last updated : February 13, 1996 This file is available from the following URL : http://www.eleves.ens.fr:8080/home/auroux/nfsspecs.txt Disclaimer : NO WARRANTY of any kind comes with this file. Added in version 0.2 : - TRI files (section B.6) : all you need to write a track editor. A - GAMEDATA directory ================== A.1 GAMEDATA\CONFIG\PATHS.DAT ------------------------- This file contains 18 null-terminated strings of 80 characters each (total 1440 bytes), describing the paths where NFS looks for its files. For minimum install, here are the contents (with hex offset): 000 gamedata/config/ 2D0 F:\simdata\misc\ 050 gamedata/savegame/ 320 F:\simdata\trackfam\ 0A0 F:\frontend\speech\ 370 F:\simdata\slides\ 0F0 F:\simdata\soundbnk\ 3C0 F:\simdata\carFams\ 140 F:\frontend\music\ 410 F:\simdata\soundbnk\ 190 F:\frontend\art\ 460 F:\simdata\carspecs\ 1E0 gamedata/modem/ 4B0 F:\simdata\dash\ 230 F:\frontend\movielow\ 500 F:\simdata\misc\ 280 gamedata/replay/ 550 F:\frontend\show\ Note that simdata\soundbnk and simdata\misc are present twice. Also note that in the CD-ROM, directories whose name starts with a 'G' correspond to the German version of the game. Don't know why the car dashboards had to be translated, though :) Modify these entries if you want to copy some of the CD files to your hard disk, for example if you want to modify them. Remember that modifying the files can be hazardous, and that because the file formats are not fully known it is better to modify an existing file than to create one from scratch. A.2 GAMEDATA\SAVEGAME\*.SAV ----------------------- These are the tournament save files (840 bytes each). - Offset 0 : your name (null-terminated string followed by zeros) - Offset 28 : 'Opponent' (") - Offset EC : 'Player 2' (") - Offset 114 : 'Opponent 2' (") - Offset 1D8 : name of the tournament (") - Offset 20A : 'REPLAY.RPL' (") - Offsets 23C-26F : ??? - Offset 270 : name of the tournament (") - Offsets 30E-337 : ??? - Offsets 338, 33A, 33C, 33E, 340, 342, 344 : these contain 01 if the corresponding race has been won, otherwise 00. The first three bytes are for open-road races, while the other correspond to closed tracks. For instance, in order to access the bonus track, save a tournament, then set all these bytes to 01 except 33E to 00 (Rusty Springs), then reload the tournament and win Rusty Springs. The odd offsets 339 to 345 are zeros. - Offsets 346-347 : ??? The other offsets contains zeros. A.3 GAMEDATA\CONFIG\CONFIG.DAT -------------------------- This file (25552 bytes) contains all the relevant configuration data (current car, track, display & sound options, ...), as well as the current scores (best times, top speeds, laps, ...). It also describes whether you have access to the bonus track and the bonus car, etc... Anybody got anything more detailed to put here ? B - SIMDATA directory ================= B.1 SIMDATA\CARSPECS\*.PBS ---------------------- These files (one per player car) describe the car performance. Usually, installing these files to the hard disk (by editing PATHS.DAT) makes Need for Speed inoperable (no acceleration is possible) because the game modifies SIMDATA\CARSPECS\BY_R&T. However this can be prevented by making the check file read-only (run ATTRIB +R BY_R&T). Note that copying one of these files on top of another one will affect the car performance and handling, but not the engine sound. So if the max rpm's of the two engines are not the same, the sound can get badly altered because the correct sound sample is not available. These files are somehow compressed, because by modifying a single byte in the file one can reach various results such as : an instant system crash ; a car that materializes under the road and cannot move, with 11 gears (!) ; a car without front wheels, that keeps spinning at its initial position... B.2 SIMDATA\DASH\*.FMM ------------------ These small files (two per player car, corresponding to the hi-res and low-res dashboards) describe the shape of the car's windshield in each display resolution. The files are slightly different for 320x200 and 640x480. a) Low-res .FMMs (*DL.FMM) : The file structure is the following : first a 24-byte header, then four chunks. The header format is the following : offset len data ------ --- ---- 00 4 'wwww' 04 4 4 (number of chunks) 08 4 18h (offset of the first chunk) 0C 4 offset of the second chunk 10 4 offset of the third chunk 14 4 offset of the fourth chunk The chunks in low-res .FMMs have the following header (22 bytes) : offset len data ------ --- ---- 00 4 'BAMF' 04 2 length of the chunk in bytes, minus two 06 4 ? (usually equals 10000h or 8000h : maybe an image size) 0A 2 maximum "length" value in the chunk's records 0C 4 ? (usually equals 10000h or 8000h : maybe an image size) 10 4 ? (usually equals 10000h or 8000h : maybe an image size) 14 2 number of records in the chunk (length minus 18h, div 8) This header is in each chunk immediately followed by the 8-byte records. Each record has the following structure : offset len data ------ --- ---- 00 4 baddr 04 2 vaddr 06 2 length The last record is followed by a 16-bit value (purpose unknown). The vaddr and length values are interpreted as follows : starting from address "vaddr" in video memory (i.e. of the form 320y+x for pixel (x,y)), "length" pixels do not belong to the dashboard, and must instead be drawn because they correspond to screen portions where the player must see the road through the windshield. The baddr field corresponds to an internal buffer offset, describing where the data to be displayed at "vaddr" are to be found in the buffer where NFS draws the view. - The first chunk corresponds to high detail : vaddr and baddr are equal, and each line corresponds to 320 bytes in the buffer (every pixel is stored). - The second chunk corresponds to medium detail : each line corresponds to 320 bytes in the buffer, but only half of the pixels are stored, thus only the first 160 bytes contain relevant data, the following 160 being unused. To vaddr=320y+x corresponds baddr=320y+(x>>1) : the horizontal resolution is decreased. - The third chunk corresponds to low detail : two consecutive lines have the same baddr values, and these two lines correspond to 320 bytes in the buffer. As before, only half of these 320 bytes are used. Only a quarter of the pixels are stored. vaddr=320y+x corresponds to baddr=320.(y>>1)+(x>>1) : both horizontal and vertical resolutions are decreased. - The fourth chunk corresponds to the interlaced mode : only half of the screen lines are referenced ; as in low detail, two screen lines correspond to 320 bytes, half of which are unused. vaddr=320y+x still corresponds to baddr=320.(y>>1)+(x>>1), but only the even-numbered lines are present. b) Hi-res .FMMs (*DH.FMM) : The chunk format is slightly different because the video memory is now segmented in several 64K pages. (Note that the values at offsets 06, 0C and 10 in the chunk header are now larger, e.g. 40000h and 10000h) Offset 14h in the chunk now contains, instead of the total number of records in the chunk, only the number of records that correspond to the first page of video memory (i.e. the first 64K). It is followed by the corresponding records. After these records, another 16-bit value describes the number of records that are located in the second page of video memory (the following 64K); the records for the second page follow (note that the 32-bit baddr values at last become useful ; only the 16 last bits of the vaddr are stored, since it is known to be located in the second page). The chunk continues with the following pages, until the number of records for a given page is zero. This zero value is followed by a 16-bit value (purpose unknown) as in the low-res files. The four chunks correspond respectively to high, medium, low and interlaced detail levels, as described above (but the lines are now each 640 pixels long ; when not in high resolution, only 320 of the 640 bytes are used). B.3 SIMDATA\DASH\*.FSH ------------------ These store the different bitmaps that make up the dashboard. The 16-byte header format is the following : offset len data ------ --- ---- 00 4 'SHPI' 04 4 length of the file in bytes 08 4 number of objects in the directory 0C 4 directory identifier string The directory identifier is 'GIMX' in .FSH dashboard files. This header is followed by the directory entries, each consisting of a 4-byte identifier string, and a 4-byte offset inside the file pointing to the beginning of the corresponding data. Each entry in the directory represents a piece of the dashboard. There are gaps between the directory and the first bitmap, and between consecutive bitmaps (significance unknown). Each directory entry points to a bitmap block with the following structure : offset len data ------ --- ---- 00 1 7Bh 01 3 size of the block (= width x length + 10h) 04 2 width of the bitmap in pixels 06 2 heigth of the bitmap in pixels 08 4 ? 0C 2 x position to display the bitmap 0E 2 y position to display the bitmap 10 w.h bitmap data : 1 byte per pixel Note that the object called 'dash' in the directory takes the whole screen (320x200 or 640x480, at position x=0, y=0)." The various objects, depending on their 4-letter identifier, represent : the dashboard itself, the steering wheel in various positions, the radar detector lights, the gauges, and also pieces of the steering wheel to redraw over the gauges when necessary. Note that value FF in the bitmaps stands for the background : this is useful when a bitmap is drawn on top of another one. Also note that some SHPI bitmap directories contain entries that actually describe the palette to be used with the bitmaps. Typically, entries with names like '!PAL', and with bitmap dimension 256x3, correspond to palettes. The palette data consists of 256 3-byte records, each record containing the red, green and blue components of the corresponding color (1 byte each). B.4 SIMDATA\CARFAMS\*.CFM --------------------- These files describe the exterior look of the cars during the race. There seem to be two different kinds of files : - full CFMs that can be used to describe any car - restricted CFMs that can only describe traffic or opponents - note that yourself and your head-to-head opponent must have full CFMs, whereas "single race" opponents can be either full or restricted CFMs. a) Full CFMs on the CD : Standard cars : ANSX.CFM, CZR1.CFM, DVIPER.CFM, F512TR.CFM, LDIABL.CFM, MRX7.CFM, P911.CFM, TSUPRA.CFM, TRAFFC.CFM (Warrior) More or less bugged previous versions of these cars : F512M.CFM, P911T.CFM, WARIOR.CFM, WARRIOR.CFM b) Restricted CFMs on the CD : Vans/jeeps/pickups : JEEP.CFM, PICKUP.CFM, RODEO.CFM, VANDURA.CFM Station-wagons : AXXESS.CFM, WAGON.CFM Usual cars : BMW.CFM, JETTA.CFM, LEMANS.CFM, SUNBIRD.CFM Sports cars : CRX.CFM, PROBE.CFM The cop's car : COPMUST.CFM Restricted versions of the player cars (loaded for single race opponents): MANSX.CFM, MCZR1.CFM, MDVIPER.CFM, MF512TR.CFM, MLDIABL.CFM, MMRX7.CFM, MP911.CFM, MTSUPRA.CFM, MTRAFFC.CFM Bugged versions of the player cars : LDIABLO.CFM, MF512M.CFM, MP911T.CFM, TESTA.CFM These files are 'wwww' files containing four chunks. The header is as in .FMM files (see B.2). The first and second chunk correspond to high-detail car structure, while the third and fourth chunk correspond to low-detail. The first and third chunks have header 'ORIP', and describe how the car is to be drawn (position and orientation of each car element). The second and fourth chunks are 'SHPI' bitmap directories (see B.3), with directory identifier 'WRAP'. They provide the bitmaps referenced by the ORIP chunks. Note that the offsets specified in the bitmap directories are relative to the 'SHPI' header. An 'ORIP' chunk consists of a header followed by nine blocks. The header has length 112 (70h) and the following structure : offset len data ------ --- ---- 00 4 'ORIP' 04 4 chunk length in bytes 08 4 2BCh (=700) 0C 4 0 10 4 size of block 8 in 12-byte records 14 4 ? the previous minus 10h 18 4 offset of block 8 in the ORIP (in bytes) 1C 4 size of block 2 in 8-byte records 20 4 offset of block 2 24 4 size of block 1 in 12-byte records 28 4 offset of block 1 (=70h) 2C 12 identifier string 38 4 size of block 3 in 20-byte records 3C 4 offset of block 3 40 4 size of block 4 in 20-byte records 44 4 offset of block 4 48 4 size of block 5 in 28-byte records 4C 4 offset of block 5 50 4 offset of block 9 54 4 size of block 6 in 12-byte records 58 4 offset of block 6 5C 4 size of block 7 in 12-byte records 60 4 offset of block 7 64 4 0 68 4 70h 6C 4 0 The first block consists of 12-byte records and describes the polygons that are used to draw the car: offset len data ------ --- ---- 00 1 83h for triangle, 84h for quadrangle, 8Ch when quadrangle/null 01 1 ? 02 1 texture number 03 1 ? 04 4 polygon offset for pol1 08 4 polygon offset for pol2 The texture number corresponds to a record position in block 3, which is converted to one of the textures in the SHPI chunk that follows the ORIP. The polygon offsets correspond to positions in the last block, where the polygons are listed consecutively (first pol2 and then pol1, for each record). The first polygon has pol2 starting at 0. (The polygon offsets are given in 4-byte units). In the last block, vertices belonging to a pol1 are expanded to 3D points by lookup in block 8, while vertices belonging to a pol2 are expanded to 2D points by lookup in block 2. The portion of the relevant texture corresponding to pol2 is mapped onto the 3D polygon pol1. When the first byte is 83h, pol1=pol2+3 and next polygon's pol2 equals pol1+3. When it is 84h, pol1=pol2+4 and next pol2 equals pol1+4. When it is 8Ch, pol1=pol2 and next pol2 equals pol1+4. The second block is made of 8-byte records, which describe the (x,y) coordinates inside the textures referenced by pol2. offset len data ------ --- ---- 00 4 x coordinate 04 4 y coordinate Third block (20-byte records): offset len data ------ --- ---- 00 8 identifier string 1 08 4 identifier string 2 0C 8 ? 00 00 00 81 F5 00 00 00 This block describes the link between the texture numbers of block 1 and the textures stored in the SHPI chunk. Identifier string 2, when non-void, corresponds to one of the SHPI directory entries. Fourth block (20-byte records): offset len data ------ --- ---- 00 8 identifier string ('left_tur', 'right_tu') 08 4 number of vertices 0C 4 polygon offset (in block 9) 10 4 ? Fifth block (28-byte records): offset len data ------ --- ---- 00 12 identifier string ('NON-SORT', 'inside', 'surface', 'outside') 0C 4 ? 10 4 polygon offset (in block 9) 14 4 number of vertices 18 4 0 Sixth and seventh blocks (12-byte records): offset len data ------ --- ---- 00 8 identifier string 08 4 ? The eighth block consists of 12-byte records which describe the position in space of each vertex (using signed long ints, centered at (0,0,0)). offset len data ------ --- ---- 00 4 x coord. (x axis is lateral) 04 4 z coord. (z axis is vertical, z increases when going up) 08 4 y coord. (y axis is longitudinal, increases when going forward) The last block is a succession of long integers, each representing a vertex number. This block describes the vertices each polygon consists of, and is referenced by the index tables of the first, fourth and fifth blocks. Each vertex number referenced in a pol1 polygon is converted to a point in space using block 8, while each vertex number referenced in a pol2 polygon is converted to a position in a texture bitmap using block 2. B.5 SIMDATA\MISC\*.QFS, SIMDATA\SLIDES\*.QFS, ... --------------------------------------------- This seems to be EAC's favorite bitmap format... Basically, it is similar to the .FSH files described in B.3, however the data are now compressed using some kind of Huffman algorithm. Outside of this compression thing, the file still contains a 'SHPI' header, and a directory. The directory itself is much shorter than in the dashboard FSH's : usually one entry (the bitmap itself), sometimes two (when a palette is specified). The big trouble is in finding the details of the decompression algorithm. The directory header is 'LN32', indicating 32768-color mode. (see C.2). B.6 SIMDATA\MISC\*.TRI ------------------ Don't know why these are in the MISC directory... they are quite important indeed ! They describe the track itself, including the shape of the road and the position of the scenery items. The objects are referenced by numbers which correspond to entries in the corresponding file in SIMDATA\TRACKFAM. Their structure is fairly complex, so I recommend you play with the files in order to get acquainted with their subtleties. The information contained in this section was used to develop the first NFS track editor, called TRACKED, and available at the following URL : http://www.eleves.ens.fr:8080/home/auroux/nfs/tracked.zip In order to understand the structure of TRI files you have to know that a track is the superposition of three structures : - first, a 'virtual road' : this is a sequence of points in space, which will be called 'nodes'. These points correspond to successive positions along the track, and all the cars have to pass near each of these positions. During the game, the virtual road is invisible, but you have to stay close to it. - second, the scenery : this is a collection of points in space, which will be called 'vertices', together with textures which are mapped onto the polygons defined by consecutive vertices. These textures are used to draw the road, the roadside and part of the landscape. Thus it is of course preferable that the scenery remain close to the virtual road ! - and last, the objects : points in space together with bitmaps, which are used for road signs, buildings, trees, etc... Some of them are plain 2D bitmaps, but others have a more sophisticated polygonal 3D structure. The 3D coordinates x,y,z will always be used with the following meaning : - x is an axis which is transversal to the starting line. Positive x values correspond to points which are on the right of the starting position. (i.e. if you start looking to the north, x points to the east) - y is an axis which is parallel to the starting line. Positive y values correspond to points which are ahead of the starting position. (i.e. y points to the north) - z is a vertical axis. Positive z values correspond to points higher than the starting position. a) TRI files begin with 12F8h bytes of headers and index tables. These are as follows : offset len data ------ --- ---- 00 4 ? 04 8 ? 0C 4 x coordinate of the first node 10 4 z coordinate of the first node 14 4 y coordinate of the first node 18 4 ? 1C 4 ? 20 4 ? 24 4 length of the scenery data (in bytes : 554h per record) 28 4 ? 2C 960h first index table 98C 960h second index table The first index table is a succession of 32-bit offsets. It follows an arithmetic progression by 548h as a general rule. This means the first value is 0, followed by 548h, A90h, etc... On closed tracks, when the end is reached, the offsets start again with 0, 548h, A90h, etc... (so that the end of a lap is connected with the beginning of the following one !). The table is filled to 960h bytes (600 entries) with zero values. This table is probably only used as a lookup table in memory during the game, since there are no 548h-byte structures in the file. The second index table is also a succession of 32-bit offsets, but these offsets are inside the TRI file. This one follows an arithmetic progression by 554h. It corresponds to the offsets of the successive records for scenery data, which starts at offset 1B000h in the scenery file. So this table starts with 1B000h, 1B554h, 1BAA8h, etc... until the end of the TRI file is reached. It is filled to 600 entries with zero values. b) The virtual road data follows, and is constituted of 36-byte records (one for each node). The first record is at offset 12F8h, and the last allowed record is at offset 16468h (this leaves room for 2400 records, and since a scenery block corresponds to four nodes, both capacities are equal). The unused records (after the last node) are filled with zero values. The structure of each record is the following : offset len data ------ --- ---- 00 1 a0 01 1 a1 02 1 a2 03 1 a3 04 1 b0 05 1 b1 06 1 b2 07 1 b3 08 4 x coordinate 0C 4 z coordinate 10 4 y coordinate 14 2 slope 16 2 slant-A 18 2 orientation 1A 2 0 1C 2 y-orientation 1E 2 slant-B 20 2 x-orientation 22 2 0 - a0,a1,a2,a3 are 8-bit values whose purpose is unknown : possibly the width of the road on each side ? - b0,b1,b2,b3 are 8-bit values of unknown purpose. - x, z and y coordinates are signed long values. - slope is a value indicating the slope at the current node, i.e. the difference between the z coordinates of two consecutive nodes. A good approximation is : slope(i) = (z(i+1) - z(i))/152. However, to complicate things, it is stored as a signed 14-bit value, complemented to 4000h. This means -1 is stored as 3FFFh, -2 as 3FFEh, ... So in fact you must perform a logical and with 3FFFh before storing ! - slant-A is a value indicating how the road is slanted to the left or to the right (as in the turns in Autumn Valley or Lost Vegas). It is a signed 14-bit value, like slope. The value is positive if the road is slanted to the right, negative if it is slanted to the left. - slant-B has the same purpose, but is a standard signed 16-bit value. Its value is positive for the left, negative for the right. The approximative relation between slant-A and slant-B is slant-B = -12.3 slant-A (remember that slant-A is 14-bit, though) - orientation is a 14-bit value, and is equal to 0 for north (increasing y), 1000h for east (increasing x), 2000h for south (decreasing y), 3000h for west (decreasing x), and back to 3FFFh for north. - y-orientation is a signed 16-bit value, which is proportional to the y coordinate variation. Meanwhile, x-orientation is proportional to the *opposite* of the x coordinate variation. This means that the couple (-xorientation,yorientation) gives the orientation of the track. The norm (square root of xorient^2+yorient^2) is usually around 32000 (a little less than 8000h, to avoid numeric overflows), but can fluctuate with the only condition that (-xor,yor) gives the correct orientation. c) The objects data comes next. There are first several distinct zones, many of which seem to be unused (?) : offset len data ------ --- ---- 16478 708h 3-byte records (there are 600... as many as scenery blocks ?) 16B80 4 40h (?) 16B84 4 3E8h = 1000 (size of the main block in records) 16B88 4 'OBJS' 16B8C 4 428Ch (total length of the remaining blocks) 16B90 400h 16-byte records (unknown purpose) 16F90 4 ? 16F94 3E80h object data : 1000 records of 16 bytes (one per object) 1AE14 1ECh 0 The object data itself consists of a 16-byte record per object. The record structure is the following : offset len data ------ --- ---- 00 4 reference node 04 1 bitmap number 05 1 flip 06 4 flags (unknown purpose) 0A 2 relative x coordinate 0C 2 relative z coordinate 0E 2 relative y coordinate Each object is related to a reference node in the virtual road. The x,z,y coordinates are then expressed as signed 16-bit values relative to the coordinates of the reference node. Beware that the axes are simply translated but not rotated ! (i.e. the x and y axes are still pointing east and north) The objects are sorted in the order of increasing reference nodes. A reference node value of -1 indicates that the record is unused (i.e. after the end of the used records). Also note that the coordinates are not expressed in the same unit as the 32-bit absolute coordinates seen above (the units are much larger, so that the value fits in 16 bits). The 8-bit flip value is equal to 0 for an object that is perfectly perpendi- cular to the track (e.g. a road sign), larger values for objects that are slightly turned, until 64 for an object that is mapped along the track (e.g. an ad on the side of the road), then up to 128 which is the perfectly reversed position (since the objects have no "back", this is the common way of reversing a road sign for a turn in the other direction), then up to 192 which is again longitudinal mapping (the other way) and until 255 which is back to the normal position. The bitmap number corresponds to a texture in the corresponding .FAM file (see B.8). The relevant bitmaps are in the second chunk of the .FAM file. Two cases can occur : - closed tracks : the second chunk is a 'wwww' structure containing a single subchunk which is in turn a SHPI directory where the entry corres- ponding to bitmap #n is called "nn00" where nn is n written in decimal. (e.g. bitmap #18 is "1800"). Furthermore the object called "!pal" or "!PAL", when it exists, is the corresponding palette (256 3-byte entries) ; FFh is transparent. - open roads : the second chunk contains a subchunk per bitmap, and each subchunk is a SHPI containing at least the object "0000" (the bitmap), and possibly a palette ("!pal" or "!PAL"). Object #n is then the bitmap "0000" in the subchunk #n (the first subchunk is #0). One must add to these 2D objects (plain bitmaps) the 3D objects described in the fourth chunk of the .FAM file. They usually correspond to numbers above the last 2D object ; however it happens, in closed tracks, that some of the 3D objects are given numbers inside the range used by 2D objects. In that case, the numbers describing 2D objects are shifted upwards. (i.e. the bitmap "4400" corresponds to object #45 or #46). This phenomenon apparently does not occur for open roads, where the 3D objects always follow the 2D objects. Furthermore, certain consecutive bitmaps represent successive states of an animated object. In that case, the game will display successively the relevant bitmaps. Note that if the second bitmap is given instead of the first, the animation does not occur. d) The scenery data starts at offset 1B000h. It is made up of records of size 554h, each corresponding to four nodes in the virtual road. The records are consecutive and the last record ends the .TRI file. Each record has the following structure : offset len data ------ --- ---- 000 4 'TRKD' 004 4 548h = length of the record contents 008 4 number of this record (0 for the first, then 1, etc...) 00C 2 ? 00E 10 textures 018 12 reference point 024 12 ? 030 12 point A0 03C 12 point A1 ... .. ........ 09C 12 point A9 0A8 12 point A10 0B4 12 ? 0C0 12 point B0 0CC 12 point B1 ... .. ........ 12C 12 point B9 138 12 point B10 144 12 ? 150 12 point C0 15C 12 point C1 ... .. ........ 1BC 12 point C9 1C8 12 point C10 1D4 12 ? 1E0 12 point D0 1EC 12 point D1 ... .. ........ 24C 12 point D9 258 12 point D10 264 12 ? 270 12 ? 27C 12 point E0 288 12 point E1 ... .. ........ 2E8 12 point E9 2F4 12 point E10 300 516 ? unused (room for 43 points) 504 80 ? unused Each point is given by three signed 32-bit absolute coordinates (x,z,y as usual). The coordinates are the same as in the virtual road data. The reference point is unused in the game and usually equal to point A0. The points A0,...,A10 in record #n (starting with 0) correspond to the node #4n (starting with 0) in the virtual road data. B0,...,B10 correspond to node #4n+1, C0...C10 to node #4n+2, D0...D10 to node #4n+3 and E0...E10 to node #4n+4. Thus the points E0...E10 are identical to the points A0...A10 of the following record. The eleven point series (0 to 10) are arranged as follows : A0-E0 are near the middle of the road, and thus close to the corresponding nodes. A1-E1 are a little to the right, A2-E2 further right, ... until A5-E5. A6-E6 are a little to the left, A7-E7 further left, ... until A10-E10. (In tunnels, the points A5-E5 and A10-E10 get back to the center, consti- tuting the ceiling). To each record correspond ten textures (coded at the beginning), each given by a 8-bit value, T1,T2,...,T10. T1 is used between A0-E0 and A1-E1, T2 between A1-E1 and A2-E2, ..., T5 between A4-E4 and A5-E5 ; meanwhile, T6 is used between A0-E0 and A6-E6, T7 between A6-E6 and A7-E7, ..., T10 between A9-E9 and A10-E10. This is summarized on the following diagram : E10---E9---E8---E7---E6---E0---E1---E2---E3---E4---E5 node 4n+4 | | | | | || | | | | | | | | | | || | | | | | D10 D9 D8 D7 D6 D0 D1 D2 D3 D4 D5 node 4n+3 | T | T | T | T | T || T | T | T | T | T | | | | | | || | | | | | C10 C9 C8 C7 C6 C0 C1 C2 C3 C4 C5 node 4n+2 | 10 | 9 | 8 | 7 | 6 || 1 | 2 | 3 | 4 | 5 | | | | | | || | | | | | B10 B9 B8 B7 B6 B0 B1 B2 B3 B4 B5 node 4n+1 | | | | | || | | | | | | | | | | || | | | | | A10---A9---A8---A7---A6---A0---A1---A2---A3---A4---A5 node 4n ^ the nodes are here | The texture numbers are converted to bitmaps in the first chunk of the .FAM file (see B.8). There are two different cases : - closed tracks : the first chunk is a 'wwww' structure which contains a single subchunk which is in turn a SHPI bitmap directory, possibly with a palette '!PAL' or '!pal'. There is also often a bitmap called 'ga00' or 'GA00' (unknown interpretation). The names have the structure "xxls", where xx is a decimal value indicating the texture group, l is 'A', 'B' or 'C', and s indicates a scale ('0' is the largest, while '3'&'4' are very small). The various scales are here to speed up the texture-mapping algorithm, anyway the only texture that is always present is with s=0. The xx and l values correspond to a texture number n in the following way : n=3xx if l='A', 3xx+1 if l='B' and 3xx+2 if l='C'. Note that there are holes in the numbering : many numbers do not have a bitmap. Examples : bitmap "03C0" corresponds to texture #11 (3x3+2) at the largest scale; bitmap "14A1" corresponds to texture #42 (3x14) at the second scale available. - open roads : the first chunk contains a subchunk per texture group (i.e. the xx value is now the number of the subchunk, starting with 0). Each subchunk is a SHPI directory containing potentially a palette, and bitmaps labelled "l00s", where l is 'A','B' or 'C' and s is the scale. As before, n=3xx if l='A', 3xx+1 if l='B', 3xx+2 if l='C', and there are holes in the numbering. Examples : texture #11 at scale '0' is now the bitmap "C000" in subchunk #3. Texture #42 at scale '1' is now the bitmap called "A001" in subchunk #14. B.7 SIMDATA\SOUNDBNK\*.BNK ---------------------- These file store the sound samples for the engines, the crashes, etc... The filenames ending with 'W' correspond to 16-bit sound, while 'B' stands for 8-bit sound. Logically, 'S' should stand for stereo and 'M' for mono, but it seems that, at least for cars, the file that gets loaded in 8-bit mono mode is the one whose name ends with 'SB'. I don't know why. I'm beginning to think that EAC recruited Microsoft programmers ;-) Also remember that the 8 bit sound samples are signed data. If you plan to use WAV files as samples, don't forget to change this. The file format is the following : - the first 512 bytes contain 128 long integers, which are either zero if the corresponding sample is not present, or the offset of a sample header. Car files contain four samples with header offsets 200h, 248h, 290h and 2D8h, declared at offsets 04, 08, 0C and 80h respectively. The two first samples are the engine sound, the third is the car horn, the fourth is the sound that gets produced when you change gears. - each sample header consists of 72 bytes (48h). The format is the following: offset len data ------ --- ---- 00 4 ? flags (for 8-bit car sounds, these are 1, 2, 4000h and 10h) 04 4 header's offset + 28h (i.e. offset of the 'EACS' marker) 08 4 ? (0 for cars) 0C 4 ? (0 for cars except gear change) 10 4 ? (0 for cars) 14 1 ? 15 1 ? (80h for cars) 16 1 ? (0 for cars) 17 1 ? 18 4 ? (7F40h for cars) 1C 4 ? (0 for cars) 20 4 ? (0 for cars) 24 4 ? (0 for cars) 28 4 'EACS' 2C 4 3E80h (sampling rate : 16 kHz) 30 1 8/16 bit flag : 1 for 8-bit, 2 for 16-bit 31 1 mono/stereo flag : 1 for mono, 2 for stereo 32 1 ? (0 for cars) 33 1 ? (FFh for cars) 34 4 beginning of the repeat loop (in samples : 1, 2 or 4 bytes) 38 4 length of the repeat loop (in samples) 3C 4 length of the sound (in samples) 40 4 offset of the raw sound data in the .BNK file 44 4 ? (0 for cars) B.8 SIMDATA\TRACKFAM\*.FAM ---------------------- These files contain the bitmaps that are used for displaying the correspon- ding track or track segment. They make use of the 'wwww' file format, using the usual header (see B.2) : first 'wwww', then the number of chunks (as a long integer), then the offsets of each chunk header (long integers). The .FAM file itself is a 'wwww' file containing four chunks. The first chunk corresponds to the background bitmaps, making up the terrain and the road, while the second chunk is devoted to foreground bitmaps (road signs, etc...) Each of these two chunks is itself in 'wwww' format, containing sub-chunks. Note that the offsets of the sub-chunks listed in the headers are relative to the beginning of the chunks. In the open road .FAM's, the chunks are very subdivided and contain many small subchunks, while in the closed tracks, there is only one big subchunk. Now the subchunks in each of the first two chunks are themselves 'SHPI' bitmap directories (see B.3), whose elements are a palette and several bitmaps. Keep in mind that the offsets in the directories are relative to the beginning of the subchunk (i.e. the 'SHPI' header). The third chunk of the .FAM file is a SHPI file describing the horizon : the SHPI directory contains two entries, one for the palette and one for the bitmap itself. The fourth chunk describes the three-dimensional objects in the scenery. It is a 'wwww' structure, containing a subchunk per object. Now each of these subchunks is quite similar to a .CFM file (see B.4) : it is itself a 'wwww' structure containing two subsubchunks ! The first subsubchunk is an 'ORIP' structure describing the shape of the object, while the second is a 'SHPI' structure containing the bitmaps referenced by the 'ORIP' part. If you didn't catch everything, don't worry... look at NFSVIEW.PAS (see D.1) C - FRONTEND directory ================== C.1 FRONTEND\ART\*.QFS, FRONTEND\SHOW\*.QFS : see B.5 --------------------------------------- C.2 FRONTEND\ART\*.INV ------------------ These files describe how to reduce the palette from hi-color mode to 256 colors. The corresponding .QFS files (compressed SHPI bitmaps) have directory identifier 'LN32', indicating the bitmaps are in 32768-color mode (each red, blue and green component is coded on 5 bits (0 to 31), and the 15 bits describing the color are stored in a 16-bit word). Since many video cards do not support these modes, information is provided for palette reduction to 256-color modes. This information is partly stored as a palette object in the QFS file, describing the RGB components of the 256 colors. In order to help Need for Speed with the palette conversion, the .INV file (length 32768 bytes) stores, for each 15-bit value, which palette entry is relevant. Displaying the 'LN32' bitmaps in 256-color is thus done by first expanding the .QFS file, then setting the specified palette, and converting each 15-bit pixel in the bitmap to the 8-bit value located at the corresponding offset in the .INV file. C.3 FRONTEND\MUSIC\*.ASF -------------------- These files contain the music for the front end. The 40-byte header is as follows (note a consistency with the EACS structures described in B.7) : offset len data ------ --- ---- 00 4 '1SNh' 04 4 ? 08 4 'EACS' 0C 4 3E80h (sampling rate 16 kHz) 10 1 2 (16-bit mode flag) 11 1 2 (stereo mode flag) 12 1 ? (0 usually) 13 1 ? (0 usually) 14 4 length of the wave data (expressed in 4-byte samples) 18 4 beginning of the repeat loop (in samples) 1C 4 length of the repeat loop (in samples) 20 4 0 (the start offset is not specified : should be 28h) 24 4 ? Signed 16-bit stereo wave data follows. C.4 FRONTEND\SPEECH\*.EAS --------------------- These file contain the speeches, as an EACS structure (see B.7 and C.3). The 32-byte header is now as follows : offset len data ------ --- ---- 00 4 'EACS' 04 4 3E80h (sampling rate 16 kHz) 08 1 1 (8-bit mode flag) 09 1 1 (mono mode flag) 0A 1 ? (0 usually) 0B 1 ? (FFh usually) 0C 4 length of wave data (expressed in 1-byte units) 10 4 FFFFFFFFh (no repeat loop) 14 4 0 (no repeat length) 18 4 20h (offset of the wave data) 1C 4 ? Signed 8-bit mono wave data follows. D - Appendix ======== D.1 NFSVIEW.PAS ----------- {NFS file viewer : recurse into 'wwww' structures and view 'SHPI' bitmaps} uses crt; var f:file; scr:array[0..199,0..319] of byte absolute $A000:0; pal:array[0..767] of byte; s:string[5]; procedure setpalette; assembler; asm mov dx,3c8h xor cl,cl mov si,offset pal cld @bcl: mov al,cl out dx,al inc dx lodsb out dx,al lodsb out dx,al lodsb out dx,al dec dx inc cl jnz @bcl end; function st(x:longint):string; var s:string[20]; begin str(x,s); st:=s; end; procedure viewshpi(pre:string;off:longint); {process a SHPI} var ni,i:integer; w,h,xp,yp,y:word; l:longint; foundpal:boolean; begin seek(f,off+8); blockread(f,ni,2); writeln(pre,'SHPI ',ni,' bitmaps'#10#13'Looking for a palette...'); foundpal:=false; for i:=0 to ni-1 do begin {look for a palette} seek(f,off+20+8*i); blockread(f,l,4); seek(f,off+l+4); blockread(f,w,2); blockread(f,h,2); if (w=256) and (h=3) then begin blockread(f,pal,8); blockread(f,pal,768); foundpal:=true; end; end; if foundpal then writeln('Found !') else writeln('No palette.'); readkey; asm mov ax,13h int 10h end; if foundpal then setpalette; for i:=0 to ni-1 do begin {display the bitmaps} fillchar(scr,64000,0); seek(f,off+20+8*i); blockread(f,l,4); seek(f,off+l+4); blockread(f,w,2); blockread(f,h,2); blockread(f,l,4); blockread(f,xp,2); blockread(f,yp,2); if (w<>256) or (h<>3) then begin if (xp+w>320) or (yp+h>200) then begin yp:=0; xp:=0; end; for y:=yp to yp+h-1 do blockread(f,scr[y,xp],w); readkey; end; end; asm mov ax,3 int 10h end; end; procedure viewwwww(pre:string;off:longint); {process a wwww block} var n,i:integer; l:longint; s:string[5]; begin seek(f,off+4); blockread(f,n,2); for i:=1 to n do begin {process each chunk} seek(f,off+4+4*i); blockread(f,l,4); s[0]:=#4; seek(f,off+l); blockread(f,s[1],4); {read 4-char id} if s='wwww' then viewwwww(pre+'chunk '+st(i)+'/'+st(n)+' sub',off+l) else if s='SHPI' then viewshpi(pre+'chunk '+st(i)+'/'+st(n)+' ',off+l) else begin writeln(pre,'chunk ',i,'/',n,' unrecognized header ',s); readkey; end; end; end; begin if paramcount=0 then begin writeln('Need For Speed wwww/SHPI Viewer 1.0 (c) Denis Auroux 1995'); writeln('Syntax : NFSVIEW filename'#10#13); exit; end; clrscr; assign(f,paramstr(1)); reset(f,1); s[0]:=#4; blockread(f,s[1],4); {read 4-char id} if s='wwww' then viewwwww('',0) else if s='SHPI' then viewshpi('',0) else writeln('Unrecognized header ',s,#10#13); close(f); end. ============================================================================ For any comments and additions to this file, mail to auroux@clipper.ens.fr. ============================================================================ ================================================ FILE: OpenNFS1/Audio/BnkVehicleAudioProvider.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Audio; using GameEngine; using Microsoft.Xna.Framework; using OpenNFS1.Physics; using System.Diagnostics; using OpenNFS1.Audio; using OpenNFS1.Parsers.Audio; namespace OpenNFS1 { class VehicleAudioProvider2 { const int GRASS_SLIDE_INDEX = 5; DrivableVehicle _car; SoundEffect _engineOnEffect, _engineOffEffect, _grassSlide; SoundEffectInstance _engineOn, _engineOff, _skidInstance, _grassSlideInstance; SoundEffect _gearChange; List _skids = new List(); bool _isActive = false; public VehicleAudioProvider2(DrivableVehicle car) { _car = car; } public void Initialize() { if (_engineOn != null) { return; } _isActive = true; BnkFile bnk = new BnkFile(_car.Descriptor.SoundBnkFile); var sample = bnk.Samples[0]; _engineOnEffect = new SoundEffect(sample.PCMData, sample.SampleRate, sample.NbrChannels == 2 ? AudioChannels.Stereo : AudioChannels.Mono); _engineOn = _engineOnEffect.CreateInstance(); sample = bnk.Samples[1]; _engineOffEffect = new SoundEffect(sample.PCMData, sample.SampleRate, sample.NbrChannels == 2 ? AudioChannels.Stereo : AudioChannels.Mono); _engineOff = _engineOffEffect.CreateInstance(); sample = bnk.Samples[3]; _gearChange = new SoundEffect(sample.PCMData, sample.SampleRate, sample.NbrChannels == 2 ? AudioChannels.Stereo : AudioChannels.Mono); //temp = Engine.Instance.ContentManager.Load(String.Format("Content/Audio/Vehicles/{0}/engine-on-low", _car.Descriptor.Name)); //_engineOnLow = temp.CreateInstance(); ////.Play(0.3f, 0, 0); //temp = Engine.Instance.ContentManager.Load(String.Format("Content/Audio/Vehicles/{0}/engine-on-high", _car.Descriptor.Name)); //_engineOn = temp.CreateInstance(); // temp.Play(0.3f, 0, 0); //temp = Engine.Instance.ContentManager.Load(String.Format("Content/Audio/Vehicles/{0}/engine-off-low", _car.Descriptor.Name)); //_engineOffLow = temp.CreateInstance(); //temp.Play(0.3f, 0, 0); //temp = Engine.Instance.ContentManager.Load(String.Format("Content/Audio/Vehicles/{0}/engine-off-high", _car.Descriptor.Name)); //_engineOff = temp.CreateInstance(); //temp.Play(0.3f, 0, 0); BnkFile envBnk = new BnkFile("COLL_SW.BNK"); sample = envBnk.Samples[GRASS_SLIDE_INDEX]; _grassSlide = new SoundEffect(sample.PCMData, sample.SampleRate, sample.NbrChannels == 2 ? AudioChannels.Stereo : AudioChannels.Mono); _grassSlideInstance = _grassSlide.CreateInstance(); _skids.Add(Engine.Instance.ContentManager.Load("Content/Audio/Vehicles/common/skid1")); _skids.Add(Engine.Instance.ContentManager.Load("Content/Audio/Vehicles/common/skid2")); _skids.Add(Engine.Instance.ContentManager.Load("Content/Audio/Vehicles/common/skid3")); } public void UpdateEngine() { if (!_isActive) return; float engineRpmFactor = ((_car.Motor.Rpm - 0.8f) / _car.Motor.RedlineRpm) - 0.5f; if (_car.Motor.Throttle == 0 && !_car.Motor.AtRedline) { _engineOn.Pause(); _engineOff.Resume(); _engineOff.Pitch = engineRpmFactor; } else { _engineOn.Resume(); _engineOff.Pause(); _engineOn.Pitch = engineRpmFactor; } } public void PlaySkid(bool play) { if (!_isActive) return; if (play) { if (_skidInstance != null && _skidInstance.State == SoundState.Playing) return; if (_skidInstance != null && _skidInstance.State != SoundState.Playing) { _skidInstance.Resume(); } else { _skidInstance = _skids[Engine.Instance.Random.Next(_skids.Count)].CreateInstance(); //.Play(0.3f, 0, 0); } } else { if (_skidInstance != null && _skidInstance.State == SoundState.Playing) { _skidInstance.Stop(); _skidInstance = null; } } } public void ChangeGear() { if (!_isActive) return; _gearChange.Play(); } public void HitGround() { if (!_isActive) return; EnvironmentAudioProvider.Instance.PlayCollision(2); SoundEngine2.Instance.PlayEffect(_skids[2].CreateInstance(), 0.2f); } public void PlayOffRoad(bool play) { if (!_isActive) return; if (play) { _grassSlideInstance.Volume = 0.4f; if (_grassSlideInstance.State == SoundState.Playing) return; else _grassSlideInstance.Resume(); } else { _grassSlideInstance.Pause(); } } public void StopAll() { if (!_isActive) return; if (_skidInstance != null) _skidInstance.Stop(); _engineOff.Stop(); _engineOn.Stop(); } } } ================================================ FILE: OpenNFS1/Audio/EnvironmentAudioProvider.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Audio; using GameEngine; namespace OpenNFS1.Audio { class EnvironmentAudioProvider { private static EnvironmentAudioProvider _instance; public static EnvironmentAudioProvider Instance { get { if (_instance == null) { _instance = new EnvironmentAudioProvider(); } return _instance; } } List _collisions; SoundEffectInstance _collisionInstance; public string BasePath { get { return GameConfig.CdDataPath + "/ConvertedAudio"; } } private EnvironmentAudioProvider() { _collisions = new List(); _collisions.Add(Engine.Instance.ContentManager.Load(BasePath + "/Environment/collision1")); _collisions.Add(Engine.Instance.ContentManager.Load(BasePath + "/Environment/collision2")); _collisions.Add(Engine.Instance.ContentManager.Load(BasePath + "/Environment/collision3")); } public void PlayVehicleFenceCollision() { if (_collisionInstance != null && _collisionInstance.State == SoundState.Playing) { return; } _collisionInstance = _collisions[Utility.RandomGenerator.Next(_collisions.Count)].CreateInstance(); _collisionInstance.Play(); } public void PlayCollision(int index) { _collisions[index].Play(0.5f, 0, 0); } } } ================================================ FILE: OpenNFS1/Audio/VehicleAudioProvider.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Audio; using GameEngine; using Microsoft.Xna.Framework; using OpenNFS1.Physics; using System.Diagnostics; using OpenNFS1.Audio; namespace OpenNFS1 { class VehicleAudioProvider { DrivableVehicle _car; SoundEffectInstance _engineOnLow, _engineOnHigh, _engineOffLow, _engineOffHigh, _skidInstance, _offRoadInstance; SoundEffect _gearChange; List _skids = new List(); bool _isActive = false; public VehicleAudioProvider(DrivableVehicle car) { _car = car; } public string BasePath { get { return GameConfig.CdDataPath + "/ConvertedAudio"; } } public void Initialize() { if (_engineOnLow != null) { return; } _isActive = true; SoundEffect temp; temp = Engine.Instance.ContentManager.Load(String.Format(BasePath + "/Vehicles/{0}/engine-on-low", _car.Descriptor.Name)); _engineOnLow = temp.CreateInstance(); //.Play(0.3f, 0, 0); temp = Engine.Instance.ContentManager.Load(String.Format(BasePath + "/Vehicles/{0}/engine-on-high", _car.Descriptor.Name)); _engineOnHigh = temp.CreateInstance(); // temp.Play(0.3f, 0, 0); temp = Engine.Instance.ContentManager.Load(String.Format(BasePath + "/Vehicles/{0}/engine-off-low", _car.Descriptor.Name)); _engineOffLow = temp.CreateInstance(); //temp.Play(0.3f, 0, 0); temp = Engine.Instance.ContentManager.Load(String.Format(BasePath + "/Vehicles/{0}/engine-off-high", _car.Descriptor.Name)); _engineOffHigh = temp.CreateInstance(); //temp.Play(0.3f, 0, 0); temp = Engine.Instance.ContentManager.Load(BasePath + "/Vehicles/common/grass_slide"); _offRoadInstance = temp.CreateInstance(); //temp.Play(0.3f, 0, 0, true); _offRoadInstance.Pause(); _skids.Add(Engine.Instance.ContentManager.Load(BasePath + "/Vehicles/common/skid1")); _skids.Add(Engine.Instance.ContentManager.Load(BasePath + "/Vehicles/common/skid2")); _skids.Add(Engine.Instance.ContentManager.Load(BasePath + "/Vehicles/common/skid3")); _engineOnLow.Pause(); _engineOnHigh.Pause(); _engineOffLow.Pause(); _engineOffHigh.Pause(); _gearChange = Engine.Instance.ContentManager.Load(String.Format(BasePath + "/Vehicles/{0}/gear-change", _car.Descriptor.Name)); } public void UpdateEngine() { if (!_isActive) return; float engineRpmFactor = (_car.Motor.Rpm - 0.8f) / _car.Motor.RedlineRpm; SoundEffectInstance low, high; if (_car.Motor.Throttle == 0 && !_car.Motor.AtRedline) { _engineOnHigh.Pause(); _engineOnLow.Pause(); _engineOffHigh.Resume(); _engineOffLow.Resume(); low = _engineOffLow; high = _engineOffHigh; } else { _engineOnHigh.Resume(); _engineOnLow.Resume(); _engineOffHigh.Pause(); _engineOffLow.Pause(); low = _engineOnLow; high = _engineOnHigh; } low.Volume = engineRpmFactor > 0.55f ? 0 : 0.3f; high.Volume = engineRpmFactor < 0.45f ? 0 : 0.3f; if (engineRpmFactor < 0.1f) { low.Volume = MathHelper.Lerp(0.1f, 0.3f, (engineRpmFactor) * 10); } if (engineRpmFactor > 0.45f && engineRpmFactor < 0.55f) { low.Volume = MathHelper.Lerp(0.3f, 0f, (engineRpmFactor - 0.45f) * 10); high.Volume = MathHelper.Lerp(0, 0.3f, (engineRpmFactor - 0.45f) * 10); } low.Pitch = engineRpmFactor - 0.1f; high.Pitch = engineRpmFactor - 0.5f; } public void PlaySkid(bool play) { if (!_isActive) return; if (play) { if (_skidInstance != null && _skidInstance.State == SoundState.Playing) return; if (_skidInstance != null && _skidInstance.State != SoundState.Playing) { _skidInstance.Resume(); } else { _skidInstance = _skids[Engine.Instance.Random.Next(_skids.Count)].CreateInstance(); //.Play(0.3f, 0, 0); } } else { if (_skidInstance != null && _skidInstance.State == SoundState.Playing) { _skidInstance.Stop(); _skidInstance = null; } } } public void ChangeGear() { if (!_isActive) return; _gearChange.Play(); } public void HitGround() { if (!_isActive) return; EnvironmentAudioProvider.Instance.PlayCollision(2); SoundEngine2.Instance.PlayEffect(_skids[2].CreateInstance(), 0.2f); } public void PlayOffRoad(bool play) { if (!_isActive) return; if (play) { _offRoadInstance.Volume = 0.4f; if (_offRoadInstance.State == SoundState.Playing) return; else _offRoadInstance.Resume(); } else { _offRoadInstance.Pause(); } } public void StopAll() { if (!_isActive) return; if (_skidInstance != null) _skidInstance.Stop(); _engineOffHigh.Stop(); _engineOffLow.Stop(); _engineOnHigh.Stop(); _engineOnLow.Stop(); } } } ================================================ FILE: OpenNFS1/AverageValue.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace OpenNFS1 { class AverageValue { int _nbrValues; List _values = new List(); public AverageValue(int nbrVaues) { _nbrValues = nbrVaues; } public void AddValue(float value) { _values.Add(value); if (_values.Count > _nbrValues) _values.RemoveAt(0); } public float GetAveragedValue() { float average = 0; foreach (float value in _values) { average += value; } return average / _values.Count; } public void Clear() { _values.Clear(); } } } ================================================ FILE: OpenNFS1/Content/ArialBlack-Italic.spritefont ================================================  Arial Black 26 0 true ~ ================================================ FILE: OpenNFS1/Content/ArialBlack.spritefont ================================================  Arial Black 26 0 true ~ ================================================ FILE: OpenNFS1/Content/ParticleEffect.fx ================================================ //----------------------------------------------------------------------------- // ParticleEffect.fx // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- // Camera parameters. float4x4 View; float4x4 Projection; float2 ViewportScale; // The current time, in seconds. float CurrentTime; // Parameters describing how the particles animate. float Duration; float DurationRandomness; float3 Gravity; float EndVelocity; float4 MinColor; float4 MaxColor; // These float2 parameters describe the min and max of a range. // The actual value is chosen differently for each particle, // interpolating between x and y by some random amount. float2 RotateSpeed; float2 StartSize; float2 EndSize; // Particle texture and sampler. texture Texture; sampler Sampler2 = sampler_state { Texture = ; MinFilter = Linear; MagFilter = Linear; MipFilter = Point; AddressU = Clamp; AddressV = Clamp; }; // Vertex shader input structure describes the start position and // velocity of the particle, and the time at which it was created, // along with some random values that affect its size and rotation. struct VertexShaderInput { float2 Corner : POSITION0; float3 Position : POSITION1; float3 Velocity : NORMAL0; float4 Random : COLOR0; float Time : TEXCOORD0; }; // Vertex shader output structure specifies the position and color of the particle. struct VertexShaderOutput { float4 Position : POSITION0; float4 Color : COLOR0; float2 TextureCoordinate : COLOR1; }; // Vertex shader helper for computing the position of a particle. float4 ComputeParticlePosition(float3 position, float3 velocity, float age, float normalizedAge) { float startVelocity = length(velocity); // Work out how fast the particle should be moving at the end of its life, // by applying a constant scaling factor to its starting velocity. float endVelocity = startVelocity * EndVelocity; // Our particles have constant acceleration, so given a starting velocity // S and ending velocity E, at time T their velocity should be S + (E-S)*T. // The particle position is the sum of this velocity over the range 0 to T. // To compute the position directly, we must integrate the velocity // equation. Integrating S + (E-S)*T for T produces S*T + (E-S)*T*T/2. float velocityIntegral = startVelocity * normalizedAge + (endVelocity - startVelocity) * normalizedAge * normalizedAge / 2; position += normalize(velocity) * velocityIntegral * Duration; // Apply the gravitational force. position += Gravity * age * normalizedAge; // Apply the camera view and projection transforms. return mul(mul(float4(position, 1), View), Projection); } // Vertex shader helper for computing the size of a particle. float ComputeParticleSize(float randomValue, float normalizedAge) { // Apply a random factor to make each particle a slightly different size. float startSize = lerp(StartSize.x, StartSize.y, randomValue); float endSize = lerp(EndSize.x, EndSize.y, randomValue); // Compute the actual size based on the age of the particle. float size = lerp(startSize, endSize, normalizedAge); // Project the size into screen coordinates. return size * Projection._m11; } // Vertex shader helper for computing the color of a particle. float4 ComputeParticleColor(float4 projectedPosition, float randomValue, float normalizedAge) { // Apply a random factor to make each particle a slightly different color. float4 color = lerp(MinColor, MaxColor, randomValue); // Fade the alpha based on the age of the particle. This curve is hard coded // to make the particle fade in fairly quickly, then fade out more slowly: // plot x*(1-x)*(1-x) for x=0:1 in a graphing program if you want to see what // this looks like. The 6.7 scaling factor normalizes the curve so the alpha // will reach all the way up to fully solid. color.a *= normalizedAge * (1 - normalizedAge) * (1 - normalizedAge) * 6.7; return color; } // Vertex shader helper for computing the rotation of a particle. float2x2 ComputeParticleRotation(float randomValue, float age) { // Apply a random factor to make each particle rotate at a different speed. float rotateSpeed = lerp(RotateSpeed.x, RotateSpeed.y, randomValue); float rotation = rotateSpeed * age; // Compute a 2x2 rotation matrix. float c = cos(rotation); float s = sin(rotation); return float2x2(c, -s, s, c); } // Custom vertex shader animates particles entirely on the GPU. VertexShaderOutput ParticleVertexShader(VertexShaderInput input) { VertexShaderOutput output; // Compute the age of the particle. float age = CurrentTime - input.Time; // Apply a random factor to make different particles age at different rates. age *= 1 + input.Random.x * DurationRandomness; // Normalize the age into the range zero to one. float normalizedAge = saturate(age / Duration); // Compute the particle position, size, color, and rotation. output.Position = ComputeParticlePosition(input.Position, input.Velocity, age, normalizedAge); float size = ComputeParticleSize(input.Random.y, normalizedAge); float2x2 rotation = ComputeParticleRotation(input.Random.w, age); output.Position.xy += mul(input.Corner, rotation) * size * ViewportScale; output.Color = ComputeParticleColor(output.Position, input.Random.z, normalizedAge); output.TextureCoordinate = (input.Corner + 1) / 2; return output; } // Pixel shader for drawing particles. float4 ParticlePixelShader(VertexShaderOutput input) : COLOR0 { float2 tex = input.TextureCoordinate; float4 v = tex2D(Sampler2, tex); return v * input.Color; } // Effect technique for drawing particles. technique Particles { pass P0 { VertexShader = compile vs_2_0 ParticleVertexShader(); PixelShader = compile ps_2_0 ParticlePixelShader(); } } ================================================ FILE: OpenNFS1/Content/common.fxh ================================================ //----------------------------------------------------------------------------- // Common.fxh // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- float ComputeFogFactor(float4 position) { return saturate(dot(position, FogVector)); } void ApplyFog(inout float4 color, float fogFactor) { color.rgb = lerp(color.rgb, FogColor * color.a, fogFactor); } void AddSpecular(inout float4 color, float3 specular) { color.rgb += specular * color.a; } struct CommonVSOutput { float4 Pos_ps; float4 Diffuse; float3 Specular; float FogFactor; }; CommonVSOutput ComputeCommonVSOutput(float4 position) { CommonVSOutput vout; vout.Pos_ps = mul(position, WorldViewProj); vout.Diffuse = DiffuseColor; vout.Specular = 0; vout.FogFactor = ComputeFogFactor(position); return vout; } #define SetCommonVSOutputParams \ vout.PositionPS = cout.Pos_ps; \ vout.Diffuse = cout.Diffuse; \ vout.Specular = float4(cout.Specular, cout.FogFactor); #define SetCommonVSOutputParamsNoFog \ vout.PositionPS = cout.Pos_ps; \ vout.Diffuse = cout.Diffuse; ================================================ FILE: OpenNFS1/Content/macros.fxh ================================================ //----------------------------------------------------------------------------- // Macros.fxh // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #ifdef SM4 // Macros for targetting shader model 4.0 (DX11) #define TECHNIQUE(name, vsname, psname ) \ technique name { pass { VertexShader = compile vs_4_0_level_9_1 vsname (); PixelShader = compile ps_4_0_level_9_1 psname(); } } #define BEGIN_CONSTANTS cbuffer Parameters : register(b0) { #define MATRIX_CONSTANTS #define END_CONSTANTS }; #define _vs(r) #define _ps(r) #define _cb(r) #define DECLARE_TEXTURE(Name, index) \ Texture2D Name : register(t##index); \ sampler Name##Sampler : register(s##index) #define DECLARE_CUBEMAP(Name, index) \ TextureCube Name : register(t##index); \ sampler Name##Sampler : register(s##index) #define SAMPLE_TEXTURE(Name, texCoord) Name.Sample(Name##Sampler, texCoord) #define SAMPLE_CUBEMAP(Name, texCoord) Name.Sample(Name##Sampler, texCoord) #else // Macros for targetting shader model 2.0 (DX9) #define TECHNIQUE(name, vsname, psname ) \ technique name { pass { VertexShader = compile vs_2_0 vsname (); PixelShader = compile ps_2_0 psname(); } } #define BEGIN_CONSTANTS #define MATRIX_CONSTANTS #define END_CONSTANTS #define _vs(r) : register(vs, r) #define _ps(r) : register(ps, r) #define _cb(r) #define DECLARE_TEXTURE(Name, index) \ sampler2D Name : register(s##index); #define DECLARE_CUBEMAP(Name, index) \ samplerCUBE Name : register(s##index); #define SAMPLE_TEXTURE(Name, texCoord) tex2D(Name, texCoord) #define SAMPLE_CUBEMAP(Name, texCoord) texCUBE(Name, texCoord) #endif ================================================ FILE: OpenNFS1/Content/structures.fxh ================================================ //----------------------------------------------------------------------------- // Structurs.fxh // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- // Vertex shader input structures. struct VSInput { float4 Position : SV_Position; }; struct VSInputVc { float4 Position : SV_Position; float4 Color : COLOR; }; struct VSInputTx { float4 Position : SV_Position; float2 TexCoord : TEXCOORD0; }; struct VSInputTxVc { float4 Position : SV_Position; float2 TexCoord : TEXCOORD0; float4 Color : COLOR; }; struct VSInputNm { float4 Position : SV_Position; float3 Normal : NORMAL; }; struct VSInputNmVc { float4 Position : SV_Position; float3 Normal : NORMAL; float4 Color : COLOR; }; struct VSInputNmTx { float4 Position : SV_Position; float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; }; struct VSInputNmTxVc { float4 Position : SV_Position; float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; float4 Color : COLOR; }; struct VSInputTx2 { float4 Position : SV_Position; float2 TexCoord : TEXCOORD0; float2 TexCoord2 : TEXCOORD1; }; struct VSInputTx2Vc { float4 Position : SV_Position; float2 TexCoord : TEXCOORD0; float2 TexCoord2 : TEXCOORD1; float4 Color : COLOR; }; struct VSInputNmTxWeights { float4 Position : SV_Position; float3 Normal : NORMAL; float2 TexCoord : TEXCOORD0; int4 Indices : BLENDINDICES0; float4 Weights : BLENDWEIGHT0; }; // Vertex shader output structures. struct VSOutput { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; float4 Specular : COLOR1; }; struct VSOutputNoFog { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; }; struct VSOutputTx { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; float4 Specular : COLOR1; float2 TexCoord : TEXCOORD0; }; struct VSOutputTxNoFog { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; float2 TexCoord : TEXCOORD0; }; struct VSOutputPixelLighting { float4 PositionPS : SV_Position; float4 PositionWS : TEXCOORD0; float3 NormalWS : TEXCOORD1; float4 Diffuse : COLOR0; }; struct VSOutputPixelLightingTx { float4 PositionPS : SV_Position; float2 TexCoord : TEXCOORD0; float4 PositionWS : TEXCOORD1; float3 NormalWS : TEXCOORD2; float4 Diffuse : COLOR0; }; struct VSOutputTx2 { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; float4 Specular : COLOR1; float2 TexCoord : TEXCOORD0; float2 TexCoord2 : TEXCOORD1; }; struct VSOutputTx2NoFog { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; float2 TexCoord : TEXCOORD0; float2 TexCoord2 : TEXCOORD1; }; struct VSOutputTxEnvMap { float4 PositionPS : SV_Position; float4 Diffuse : COLOR0; float4 Specular : COLOR1; float2 TexCoord : TEXCOORD0; float3 EnvCoord : TEXCOORD1; }; ================================================ FILE: OpenNFS1/Dashboards/Dashboard.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using OpenNFS1.Parsers; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Physics; using System.Diagnostics; using Microsoft.Xna.Framework.Input; using OpenNFS1.Vehicles; namespace OpenNFS1.Dashboards { class Dashboard { protected DrivableVehicle _car; //protected Texture2D _instrumentLine; GearboxAnimation _gearBoxAnimation; public bool IsVisible { get; set; } DashboardDescription _descriptor; BitmapEntry Dash, GearGate, GearKnob, Wstr, Wl06, Wl14, Wl22, Wl32, Wl45, Wr06, Wr14, Wr22, Wr32, Wr45; BitmapEntry Leather1, Leather2, Leather3; static Texture2D _tachLineTexture; public Dashboard(DrivableVehicle car, DashboardDescription descriptor) { _car = car; _descriptor = descriptor; _car.Motor.Gearbox.GearChangeStarted += new EventHandler(Gearbox_GearChangeStarted); _gearBoxAnimation = new GearboxAnimation(); //_instrumentLine = Engine.Instance.ContentManager.Load("Content\\SpeedoLine"); if (_tachLineTexture == null) { _tachLineTexture = new Texture2D(Engine.Instance.Device, (int)3, 25); Color[] pixels = new Color[_tachLineTexture.Width * _tachLineTexture.Height]; for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.Red; _tachLineTexture.SetData(pixels); } FshFile fsh = new FshFile(Path.Combine(@"SIMDATA\DASH", descriptor.Filename)); var bitmaps = fsh.Header; Dash = bitmaps.FindByName("dash"); GearGate = bitmaps.FindByName("gate"); GearKnob = bitmaps.FindByName("nob1"); Leather1 = bitmaps.FindByName("lth1"); Leather2 = bitmaps.FindByName("lth2"); Leather3 = bitmaps.FindByName("lth3"); //steering wheel images, from straight to left, then to the right. Not all cars have all steering angles Wstr = bitmaps.FindByName("wstr"); Wl06 = bitmaps.FindByName("wl06"); Wl14 = bitmaps.FindByName("wl14"); Wl22 = bitmaps.FindByName("wl22"); Wl32 = bitmaps.FindByName("wl32"); if (Wl32 == null) Wl32 = Wl22; Wl45 = bitmaps.FindByName("wl45"); if (Wl45 == null) Wl45 = Wl32; Wr06 = bitmaps.FindByName("wr06"); Wr14 = bitmaps.FindByName("wr14"); Wr22 = bitmaps.FindByName("wr22"); Wr32 = bitmaps.FindByName("wr32"); if (Wr32 == null) Wr32 = Wr22; Wr45 = bitmaps.FindByName("wr45"); if (Wr45 == null) Wr45 = Wr32; } void Gearbox_GearChangeStarted(object sender, EventArgs e) { if (IsVisible) { _gearBoxAnimation.Current = _car.Motor.Gearbox.CurrentGear + 1; _gearBoxAnimation.Next = _car.Motor.Gearbox.NextGear + 1; } } public void Update(GameTime gameTime) { _gearBoxAnimation.Update(gameTime); if (Engine.Instance.Input.IsKeyDown(Keys.I)) _descriptor.TachPosition.Y += Engine.Instance.FrameTime * 20; if (Engine.Instance.Input.IsKeyDown(Keys.K)) _descriptor.TachPosition.Y -= Engine.Instance.FrameTime * 20; if (Engine.Instance.Input.IsKeyDown(Keys.J)) _descriptor.TachPosition.X -= Engine.Instance.FrameTime * 20; if (Engine.Instance.Input.IsKeyDown(Keys.L)) _descriptor.TachPosition.X += Engine.Instance.FrameTime * 20; if (Engine.Instance.Input.IsKeyDown(Keys.O)) _descriptor.TachNeedleLength -= Engine.Instance.FrameTime; Debug.WriteLine((int)_descriptor.TachPosition.X + ", " + (int)_descriptor.TachPosition.Y + ", " + _descriptor.TachNeedleLength); } public void Render() { Engine.Instance.SpriteBatch.Draw(Dash.Texture, Dash.GetDisplayAt(), Color.White); if (_gearBoxAnimation.IsAnimating) { RenderGearstick(); } Color color = new Color(165, 0, 0, 255); float rpmFactor = _car.Motor.Rpm / _car.Motor.RedlineRpm; Vector2 revCounterPosition = _descriptor.TachPosition; float rotation = (float)(rpmFactor * Math.PI * _descriptor.TachRpmMultiplier) - _descriptor.TachIdlePosition; RenderSteeringWheel(); Engine.Instance.SpriteBatch.Draw(_tachLineTexture, revCounterPosition, null, color, rotation, new Vector2(1.5f, 25), new Vector2(0.8f, _descriptor.TachNeedleLength), SpriteEffects.None, 0); } public void RenderGearstick() { Vector2 gatePos = GearGate.GetDisplayAt(); Vector2 gateCenter = gatePos + new Vector2(GearGate.Texture.Width, GearGate.Texture.Height) / 2; Engine.Instance.SpriteBatch.Draw(GearGate.Texture, gatePos, Color.White); var gearAnim = _gearBoxAnimation.CurrentPosition; Vector2 offset; // leather gearstick boot if (Leather1 != null) { offset = new Vector2(Leather1.Texture.Width, Leather1.Texture.Height) / 2; Engine.Instance.SpriteBatch.Draw(Leather1.Texture, gateCenter - offset + gearAnim * 0.1f, Color.White); offset = new Vector2(Leather2.Texture.Width, Leather2.Texture.Height) / 2; Engine.Instance.SpriteBatch.Draw(Leather2.Texture, gateCenter - offset + gearAnim * 0.4f, Color.White); offset = new Vector2(Leather3.Texture.Width, Leather3.Texture.Height) / 2; Engine.Instance.SpriteBatch.Draw(Leather3.Texture, gateCenter - offset + gearAnim * 0.7f, Color.White); } offset = new Vector2(GearKnob.Texture.Width, GearKnob.Texture.Height) / 2; Engine.Instance.SpriteBatch.Draw(GearKnob.Texture, gateCenter - offset + gearAnim, Color.White); } public void RenderSteeringWheel() { float steeringFactor = _car._steeringWheel / Vehicle.MaxSteeringLock; if (steeringFactor < -0.8f) Engine.Instance.SpriteBatch.Draw(Wl45.Texture, Wl45.GetDisplayAt(), Color.White); else if (steeringFactor < -0.64f) Engine.Instance.SpriteBatch.Draw(Wl32.Texture, Wl32.GetDisplayAt(), Color.White); else if (steeringFactor < -0.48f) Engine.Instance.SpriteBatch.Draw(Wl22.Texture, Wl22.GetDisplayAt(), Color.White); else if (steeringFactor < -0.32f) Engine.Instance.SpriteBatch.Draw(Wl14.Texture, Wl14.GetDisplayAt(), Color.White); else if (steeringFactor < -0.16f) Engine.Instance.SpriteBatch.Draw(Wl06.Texture, Wl06.GetDisplayAt(), Color.White); else if (steeringFactor > 0.8f) Engine.Instance.SpriteBatch.Draw(Wr45.Texture, Wr45.GetDisplayAt(), Color.White); else if (steeringFactor > 0.64f) Engine.Instance.SpriteBatch.Draw(Wr32.Texture, Wr32.GetDisplayAt(), Color.White); else if (steeringFactor > 0.48f) Engine.Instance.SpriteBatch.Draw(Wr22.Texture, Wr22.GetDisplayAt(), Color.White); else if (steeringFactor > 0.32f) Engine.Instance.SpriteBatch.Draw(Wr14.Texture, Wr14.GetDisplayAt(), Color.White); else if (steeringFactor > 0.16f) Engine.Instance.SpriteBatch.Draw(Wr06.Texture, Wr06.GetDisplayAt(), Color.White); else Engine.Instance.SpriteBatch.Draw(Wstr.Texture, Wstr.GetDisplayAt(), Color.White); } } } ================================================ FILE: OpenNFS1/Dashboards/DashboardDescription.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; namespace OpenNFS1.Dashboards { class DashboardDescription { public string Filename; public Vector2 TachPosition; public float TachRpmMultiplier; public float TachIdlePosition; public float TachNeedleLength; public static List Descriptions = new List(); static DashboardDescription() { //ZR1 Descriptions.Add(new DashboardDescription { Filename = "czr1dh.fsh", TachPosition = new Vector2(277, 398), TachRpmMultiplier = 0.54f, TachIdlePosition = 1.9f, TachNeedleLength = 1.7f }); //Diablo Descriptions.Add(new DashboardDescription { Filename = "ldiabldh.fsh", TachPosition = new Vector2(277, 398), TachRpmMultiplier = 1.25f, TachIdlePosition = 2.42f, TachNeedleLength = 1.8f }); //Viper Descriptions.Add(new DashboardDescription { Filename = "dviperdh.fsh", TachPosition = new Vector2(417, 364), TachRpmMultiplier = 1f, TachIdlePosition = 2.62f, TachNeedleLength = 1.26f }); //F512 Descriptions.Add(new DashboardDescription { Filename = "f512trdh.fsh", TachPosition = new Vector2(382, 363), TachRpmMultiplier = 1.3f, TachIdlePosition = 2.52f, TachNeedleLength = 0.9f }); //NSX Descriptions.Add(new DashboardDescription { Filename = "ansxdh.fsh", TachPosition = new Vector2(288, 362), TachRpmMultiplier = 1.3f, TachIdlePosition = 2.6f, TachNeedleLength = 1.1f }); //911 Descriptions.Add(new DashboardDescription { Filename = "p911dh.fsh", TachPosition = new Vector2(321, 361), TachRpmMultiplier = 1.37f, TachIdlePosition = 2.61f, TachNeedleLength = 1f }); //RX7 Descriptions.Add(new DashboardDescription { Filename = "mrx7dh.fsh", TachPosition = new Vector2(317, 361), TachRpmMultiplier = 1.5f, TachIdlePosition = 2.62f, TachNeedleLength = 0.95f }); //Supra Descriptions.Add(new DashboardDescription { Filename = "tsupradh.fsh", TachPosition = new Vector2(324, 354), TachRpmMultiplier = 1.28f, TachIdlePosition = 2.4f, TachNeedleLength = 1f }); //Warrior Descriptions.Add(new DashboardDescription { Filename = "traffcdh.fsh", TachPosition = new Vector2(319, 376), TachRpmMultiplier = 0.63f, TachIdlePosition = 1.8f, TachNeedleLength = 1.35f }); } } } ================================================ FILE: OpenNFS1/Dashboards/GearboxAnimation.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace OpenNFS1.Dashboards { enum AnimationStatus { ToNeutral, ToX, ToY } class GearboxAnimation { List _gearPositions; public int Current, Next; Vector2 _lastPos, _currentPosition; float _amount = 0; AnimationStatus _status; float _waitAtEndOfAnimationTime; public GearboxAnimation() { float boundX = 20, boundY = 30; _gearPositions = new List(); _gearPositions.Add(new Vector2(boundX, boundY)); _gearPositions.Add(new Vector2(0, 0)); _gearPositions.Add(new Vector2(-boundX, -boundY)); _gearPositions.Add(new Vector2(-boundX, boundY)); _gearPositions.Add(new Vector2(0, -boundY)); _gearPositions.Add(new Vector2(0, boundY)); _gearPositions.Add(new Vector2(boundX, -boundY)); _gearPositions.Add(new Vector2(boundX, boundY)); _currentPosition = _gearPositions[2]; Current = 2; Next = 2; } public Vector2 CurrentPosition { get { return _currentPosition; } set { _currentPosition = value; } } public bool IsAnimating { get { return Current != Next || _waitAtEndOfAnimationTime > 0; } } public void Update(GameTime gameTime) { if (Current != Next) { if (_currentPosition.Y != 0 && _status == AnimationStatus.ToNeutral) { _currentPosition.Y = MathHelper.Lerp(_gearPositions[Current].Y, 0, _amount); if (_amount >= 1) { _currentPosition.Y = 0; _amount = 0; _lastPos = _currentPosition; } } else if (_currentPosition.X != _gearPositions[Next].X) { _currentPosition.X = MathHelper.Lerp(_lastPos.X, _gearPositions[Next].X, _amount); if (_amount >= 1) { _currentPosition.X = _gearPositions[Next].X; _amount = 0; _lastPos = _currentPosition; } } else if (_currentPosition.Y != _gearPositions[Next].Y) { _status = AnimationStatus.ToY; _currentPosition.Y = MathHelper.Lerp(_lastPos.Y, _gearPositions[Next].Y, _amount); if (_amount >= 1) { _currentPosition.Y = _gearPositions[Next].Y; _amount = 0; } } else if (_currentPosition.X == _gearPositions[Next].X && _currentPosition.Y == _gearPositions[Next].Y) { _status = AnimationStatus.ToNeutral; Current = Next; _waitAtEndOfAnimationTime = 0.5f; } _amount += (float)gameTime.ElapsedGameTime.TotalSeconds * (_gearPositions[Current].X != _gearPositions[Next].X ? 7.5f : 5.5f); } if (_waitAtEndOfAnimationTime >= 0) _waitAtEndOfAnimationTime -= (float)gameTime.ElapsedGameTime.TotalSeconds; } } } ================================================ FILE: OpenNFS1/Game1.cs ================================================ using System; using System.IO; using System.Runtime.InteropServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GameEngine; using OpenNFS1.UI.Screens; namespace OpenNFS1 { /// /// This is the main type for your game /// class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager _graphics; RenderTarget2D _renderTarget; [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndIntertAfter, int X, int Y, int cx, int cy, int uFlags); public Game1() { _graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; GameConfig.Load(); if (GameConfig.FullScreen) { _graphics.PreferredBackBufferWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width; _graphics.PreferredBackBufferHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height; } else { _graphics.PreferredBackBufferWidth = 800; _graphics.PreferredBackBufferHeight = 600; } _graphics.PreferredDepthStencilFormat = DepthFormat.Depth24; _graphics.PreferMultiSampling = true; _graphics.IsFullScreen = false; } /// /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// protected override void Initialize() { base.Initialize(); if (GameConfig.FullScreen) { // hack to move window to top-left.. Type type = typeof(OpenTKGameWindow); System.Reflection.FieldInfo field = type.GetField("window", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); OpenTK.GameWindow window = (OpenTK.GameWindow)field.GetValue(this.Window); this.Window.IsBorderless = true; window.X = 0; window.Y = 0; } } /// /// LoadContent will be called once per game and is the place to load /// all of your content. /// protected override void LoadContent() { Engine.Create(this, _graphics); _renderTarget = new RenderTarget2D(Engine.Instance.Device, 640, 480, false, _graphics.GraphicsDevice.DisplayMode.Format, DepthFormat.Depth24, 1, RenderTargetUsage.DiscardContents); Engine.Instance.Device.SetRenderTarget(_renderTarget); Engine.Instance.ScreenSize = new Vector2(GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height); Engine.Instance.Screen = new OpenNFS1SplashScreen(); } /// /// UnloadContent will be called once per game and is the place to unload /// all content. /// protected override void UnloadContent() { Engine.Instance.ContentManager.Unload(); } /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// /// Provides a snapshot of timing values. protected override void Update(GameTime gameTime) { base.Update(gameTime); } /// /// This is called when the game should draw itself. /// /// Provides a snapshot of timing values. protected override void Draw(GameTime gameTime) { Engine.Instance.Device.SetRenderTarget(_renderTarget); Color c = new Color(0.15f, 0.15f, 0.15f); _graphics.GraphicsDevice.Clear(c); base.Draw(gameTime); Engine.Instance.Device.SetRenderTarget(null); using (SpriteBatch sprite = new SpriteBatch(Engine.Instance.Device)) { Rectangle r = Window.ClientBounds; if (GameConfig.FullScreen) { // retain original 4:3 aspect ratio int originalWith = r.Width; int w = (int)((4f / 3f) * r.Height); r.Width = w; r.X = originalWith / 2 - w / 2; } sprite.Begin(); sprite.Draw(_renderTarget, r, Color.White); sprite.End(); } } } } ================================================ FILE: OpenNFS1/GameConfig.cs ================================================ using System; using System.Collections.Generic; using System.Text; using OpenNFS1.Physics; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.IO; using Microsoft.Xna.Framework; using OpenNFS1.Vehicles; using OpenNFS1.Parsers.Track; using Microsoft.Xna.Framework.Graphics; namespace OpenNFS1 { static class GameConfig { // constants, shouldnt be here really public const float MeshScale = 0.040f; public const float TerrainScale = 0.000080f; public static readonly float FOV = MathHelper.ToRadians(65); public const float MaxSegmentRenderCount = 50; public static readonly SamplerState WrapSampler = SamplerState.AnisotropicWrap; // Set while navigating through menus public static VehicleDescription SelectedVehicle; public static TrackDescription SelectedTrackDescription; public static Track CurrentTrack; public static bool ManualGearbox { get; set; } public static bool AlternativeTimeOfDay { get; set; } // Loaded from config file public static bool FullScreen { get; set; } public static string CdDataPath { get; set; } public static bool Render2dScenery = true, Render3dScenery = true; public static bool RenderOnlyPhysicalTrack = true; public static int DrawDistance { get; set; } public static bool RespectOpenRoadCheckpoints { get; set; } public static bool DrawDebugInfo { get; set; } static GameConfig() { } public static void Load() { JObject o1 = JObject.Parse(File.ReadAllText(@"gameconfig.json")); FullScreen = o1.Value("fullScreen"); CdDataPath = o1.Value("cdDataPath"); DrawDistance = o1.Value("drawDistance"); RespectOpenRoadCheckpoints = o1.Value("respectOpenRoadCheckpoints"); DrawDebugInfo = o1.Value("drawDebugInfo"); } } } ================================================ FILE: OpenNFS1/Mesh.cs ================================================ using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Parsers; using GameEngine; using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; namespace OpenNFS1 { public static class MeshCache { } class Mesh { protected List _polys { get; set; } protected VertexBuffer _vertexBuffer; public string Identifier { get; private set; } public BoundingBox BoundingBox {get; private set;} public Mesh(MeshChunk meshChunk, BitmapChunk bmpChunk) { _polys = meshChunk.Polygons; Identifier = meshChunk.Identifier; Resolve(bmpChunk); BoundingBox = GetBoundingBox(); } public void Resolve(BitmapChunk bitmapChunk) { if (_vertexBuffer != null) return; //already resolved int vertCount = 0; List allVerts = new List(); foreach (Polygon poly in _polys) { if (poly.TextureName != null) { poly.ResolveTexture(bitmapChunk.FindByName(poly.TextureName)); } poly.VertexBufferIndex = vertCount; vertCount += poly.VertexCount; allVerts.AddRange(poly.GetVertices()); } _vertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), vertCount, BufferUsage.WriteOnly); _vertexBuffer.SetData(allVerts.ToArray()); } public virtual void Render(Effect effect) { Engine.Instance.Device.SetVertexBuffer(_vertexBuffer); effect.CurrentTechnique.Passes[0].Apply(); foreach (Polygon poly in _polys) { if (poly.VertexBufferIndex < 0) continue; Engine.Instance.Device.Textures[0] = poly.Texture; Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleList, poly.VertexBufferIndex, poly.VertexCount / 3); } } private BoundingBox GetBoundingBox() { BoundingBox bb = new BoundingBox(); bb.Min = new Microsoft.Xna.Framework.Vector3(float.MaxValue); bb.Max = new Microsoft.Xna.Framework.Vector3(-float.MaxValue); foreach (var poly in _polys) { foreach (var vert in poly.Vertices) { if (vert.X < bb.Min.X) bb.Min.X = vert.X; if (vert.Y < bb.Min.Y) bb.Min.Y = vert.Y; if (vert.Z < bb.Min.Z) bb.Min.Z = vert.Z; if (vert.X > bb.Max.X) bb.Max.X = vert.X; if (vert.Y > bb.Max.Y) bb.Max.Y = vert.Y; if (vert.Z > bb.Max.Z) bb.Max.Z = vert.Z; } } return bb; } public void Dispose() { _vertexBuffer.Dispose(); } } } ================================================ FILE: OpenNFS1/ObjectShadow.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace OpenNFS1 { class ObjectShadow { static BasicEffect _effect; static VertexPositionColor[] _verts = new VertexPositionColor[4]; static ObjectShadow() { _effect = new BasicEffect(Engine.Instance.Device); _effect.TextureEnabled = false; _effect.VertexColorEnabled = true; Color shadowColor = new Color(10, 10, 10, 150); for (int i = 0; i < 4; i++) { _verts[i] = new VertexPositionColor(Vector3.Zero, shadowColor); } } public static void Render(Vector3[] points, bool ignoreDepthBuffer) { for (int i = 0; i < 4; i++) { _verts[i].Position = points[i]; } GraphicsDevice device = Engine.Instance.Device; _effect.World = Matrix.Identity; _effect.View = Engine.Instance.Camera.View; _effect.Projection = Engine.Instance.Camera.Projection; device.DepthStencilState = ignoreDepthBuffer ? DepthStencilState.None : DepthStencilState.Default; device.BlendState = BlendState.AlphaBlend; device.RasterizerState = RasterizerState.CullCounterClockwise; _effect.CurrentTechnique.Passes[0].Apply(); device.DrawUserPrimitives(PrimitiveType.TriangleStrip, _verts, 0, 2); //device.RenderState.AlphaBlendEnable = false; device.BlendState = BlendState.Opaque; device.DepthStencilState = DepthStencilState.Default; device.RasterizerState = RasterizerState.CullNone; } } } ================================================ FILE: OpenNFS1/OpenNFS1.csproj ================================================  Debug x86 8.0.30703 2.0 {2BBFF57F-1C9D-430E-8343-D2662437114D} WinExe Properties OpenNFS1 OpenNFS1 512 x86 true full false bin\WindowsGL\Debug\ DEBUG;TRACE;WINDOWS prompt 4 x86 pdbonly true bin\WindowsGL\Release\ TRACE;WINDOWS prompt 4 Icon.ico ..\lib\Ionic.Zip.Reduced.dll False ..\lib\MonoGame.Framework.dll ..\lib\Newtonsoft.Json.dll False ..\lib\OpenTK.dll SDL.dll PreserveNewest PreserveNewest PreserveNewest PreserveNewest {F66B2F9A-AF38-40F9-A094-522C823D04EE} GameEngine copy /Y $(SolutionDir)PreBuilt_Content\*.xnb $(TargetDir)Content ================================================ FILE: OpenNFS1/Parsers/Audio/BnkFile.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Diagnostics; namespace OpenNFS1.Parsers.Audio { class BnkSample { public int NbrSamples; public Int16 NbrChannels; public byte[] PCMData; public int SampleRate; public int LoopStart; } // Bnk files (in SOUNDBNK folder) contain audio samples class BnkFile { string _filename; public bool ForceMono = true; public List Samples { get; private set; } public BnkFile(string filename) { _filename = filename; Samples = new List(); filename = Path.Combine(GameConfig.CdDataPath, @"Simdata\Soundbnk\" + filename); BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open)); Parse(reader); reader.Close(); } private void Parse(BinaryReader reader) { List sampleOffsets = new List(); for (int i = 0; i < 128; i++) { int sampleOffset = reader.ReadInt32(); if (sampleOffset != 0) sampleOffsets.Add(sampleOffset); } int count = 0; foreach (int sampleOffset in sampleOffsets) { reader.BaseStream.Position = sampleOffset; ReadSample(reader, count++); } } private void ReadSample(BinaryReader reader, int sampleIndex) { BnkSample sample = new BnkSample(); reader.BaseStream.Position += 0x28; string id = new string(reader.ReadChars(4)); sample.SampleRate = reader.ReadInt32(); byte bytesPerSample = reader.ReadByte(); sample.NbrChannels = reader.ReadByte(); byte compression = reader.ReadByte(); byte type = reader.ReadByte(); int totalLength = reader.ReadInt32(); sample.LoopStart = reader.ReadInt32(); int loopLength = reader.ReadInt32(); int dataOffset = reader.ReadInt32(); int unk3 = reader.ReadInt32(); // HACKS to avoid popping sounds.. Not sure why we need to do this.. maybe NFS finds values near // loop start/end which are the best match //if (_filename == "SUPRA_SW.bnk" && sampleIndex == 0) // loopLength += 10; //Debug.WriteLine(String.Format("{0}: sampleRate:{5} bps: {1} loopStart: {2} loopLen: {3} totalLen: {4}, offset: {6}, channels: {7}", // headerIndex, bytesPerSample, sample.LoopStart, loopLength, sample.NbrSamples, sample.SampleRate, dataOffset, sample.NbrChannels)); reader.BaseStream.Position = dataOffset + (bytesPerSample * sample.NbrChannels * sample.LoopStart); sample.NbrSamples = loopLength == 0 ? totalLength : loopLength; byte[] soundData = reader.ReadBytes(bytesPerSample * sample.NbrChannels * sample.NbrSamples); if (bytesPerSample == 1) //convert 8 bit signed PCM data to unsigned { for (int i = 0; i < soundData.Length; i++) soundData[i] = (byte)(soundData[i] + 0x80); } if (ForceMono && sample.NbrChannels == 2) { MemoryStream ms = new MemoryStream(); for (int i = 0; i < soundData.Length; i += bytesPerSample * 2) { ms.Write(soundData, i, bytesPerSample); } soundData = ms.ToArray(); sample.NbrChannels = 1; } /* if (sample.NbrChannels == 2) { MemoryStream ms = new MemoryStream(); for (int i = 0; i < soundData.Length - 4; i += 4) { ms.Write(soundData, i, 2); } soundData = ms.ToArray(); sample.NbrChannels = 1; short[] sdata = new short[soundData.Length / 2]; Buffer.BlockCopy(soundData, 0, sdata, 0, soundData.Length); soundData = new byte[sdata.Length * 2]; Buffer.BlockCopy(sdata, 0, soundData, 0, loopLength * 2); } if (sample.NbrChannels == 3) { short lowestValue = short.MaxValue; int lowestValueSample = 0; short[] sdata = new short[soundData.Length / 2]; Buffer.BlockCopy(soundData, 0, sdata, 0, soundData.Length); for (int i = loopLength; i < loopLength + 20; i++) { if (Math.Abs(sdata[i]) < Math.Abs(lowestValue)) { lowestValue = sdata[i]; lowestValueSample = i; } Debug.WriteLine(sdata[i]); } Debug.WriteLine("was " + loopLength + ", now " + lowestValueSample); loopLength = lowestValueSample; soundData = new byte[lowestValueSample * 2]; Buffer.BlockCopy(sdata, 0, soundData, 0, lowestValueSample * 2); }*/ sample.PCMData = soundData; Samples.Add(sample); //WavWriter.Write(String.Format("c:\\temp\\test{0}.wav", headerIndex), // nbrSamples, channels, soundData); } } } ================================================ FILE: OpenNFS1/Parsers/Audio/WavWriter.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; namespace OpenNFS1.Parsers.Audio { class WavWriter { public WavWriter() { } public static void Write(string filename, int samples, short channels, byte[] soundData) { FileStream stream = new FileStream(filename, FileMode.Create); BinaryWriter writer = new BinaryWriter(stream); int RIFF = 0x46464952; int WAVE = 0x45564157; int DATA = 0x61746164; int formatChunkSize = 16; int headerSize = 8; int format = 0x20746D66; short formatType = 1; int samplesPerSecond = 16000; // 44100; short bitsPerSample = 16; short frameSize = (short)(channels * ((bitsPerSample + 7) / 8)); int bytesPerSecond = samplesPerSecond * frameSize; int waveSize = 4; int dataChunkSize = samples * frameSize; int fileSize = waveSize + headerSize + formatChunkSize + headerSize + dataChunkSize; writer.Write(RIFF); writer.Write(fileSize); writer.Write(WAVE); writer.Write(format); writer.Write(formatChunkSize); writer.Write(formatType); writer.Write(channels); writer.Write(samplesPerSecond); writer.Write(bytesPerSecond); writer.Write(frameSize); writer.Write(bitsPerSample); writer.Write(DATA); writer.Write(dataChunkSize); writer.Write(soundData); writer.Close(); } } } ================================================ FILE: OpenNFS1/Parsers/BaseChunk.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; namespace OpenNFS1.Parsers { class BaseChunk { private long _offset; public long Offset { get { return _offset; } set { _offset = value; } } protected int _length; public void Load(BinaryReader reader) { reader.BaseStream.Position = _offset; Read(reader); } public virtual void Read(BinaryReader reader) { _offset = reader.BaseStream.Position - 4; _length = reader.ReadInt32(); } public void SkipHeader(BinaryReader reader) { reader.BaseStream.Position += 4; } } } ================================================ FILE: OpenNFS1/Parsers/BitmapChunk.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Xna.Framework.Graphics; using GameEngine; using System.Runtime.InteropServices; using System.Diagnostics; using Microsoft.Xna.Framework; namespace OpenNFS1.Parsers { enum BitmapEntryType { Unknown = 0, Texture = 0x7b, DosPalette = 0x22, Palette = 0x24, TextureTrailer = 0x7c } class BitmapEntry { public string Id; public int Offset; public int Length; public BitmapEntryType Type; public Texture2D Texture; public short[] Misc = new short[4]; //different purposes for different types of bitmaps public override string ToString() { return Id; } // these misc fields are often used by UI bitmaps to define where they should be displayed public Vector2 GetDisplayAt() { return new Vector2(Misc[2], Misc[3]); } } class BitmapChunk : BaseChunk { const int EntryHeaderLength = 16; List _bitmaps = new List(); internal List Bitmaps { get { return _bitmaps; } } public int Index { get; set; } public bool EnableTextureAttachments { get; set; } //fsh files use an extension to store detail alongside bitmaps, such as palettes etc. public delegate void TextureGeneratedHandler(BitmapEntry entry, byte[] palette, byte[] pixelData); public event TextureGeneratedHandler TextureGenerated; public static byte[] _palette; private static byte[] _lastPalette; //some files don't have their own palettes, in that case, use the last palette we loaded public override void Read(BinaryReader reader) { base.Read(reader); int itemCount = reader.ReadInt32(); string directoryName = new string(reader.ReadChars(4)); Debug.WriteLine(">> Loading bitmap chunk " + directoryName); for (int i = 0; i < itemCount; i++) { BitmapEntry entry = new BitmapEntry(); entry.Id = new string(reader.ReadChars(4)); entry.Offset = reader.ReadInt32(); _bitmaps.Add(entry); } //Load global palette first foreach (BitmapEntry entry in _bitmaps) { if (entry.Id.ToUpper() == "!PAL") { reader.BaseStream.Position = Offset + entry.Offset; ReadEntry(reader, entry); _palette = _lastPalette; Debug.WriteLine("\tLoaded global palette"); break; } } for (int i = 0; i < _bitmaps.Count; i++) { BitmapEntry entry = _bitmaps[i]; if (entry.Type == BitmapEntryType.Unknown) { reader.BaseStream.Position = Offset + entry.Offset; ReadEntry(reader, entry); Debug.WriteLine("\tLoaded bitmap " + entry.Id); } } } private void ReadEntry(BinaryReader reader, BitmapEntry entry) { int iCode = reader.ReadInt32(); entry.Type = (BitmapEntryType)(iCode & 0x7F); entry.Length = iCode >> 8; int width = reader.ReadInt16(); int height = reader.ReadInt16(); if (entry.Type == BitmapEntryType.TextureTrailer) return; entry.Misc[0] = reader.ReadInt16(); entry.Misc[1] = reader.ReadInt16(); entry.Misc[2] = reader.ReadInt16(); entry.Misc[3] = reader.ReadInt16(); switch (entry.Type) { case BitmapEntryType.DosPalette: case BitmapEntryType.Palette: _lastPalette = LoadPalette(reader, entry.Type); entry.Type = BitmapEntryType.Palette; break; case BitmapEntryType.Texture: ReadTexture(reader, entry, width, height); break; } } private void ReadTexture(BinaryReader reader, BitmapEntry entry, int width, int height) { byte[] pixelData = reader.ReadBytes(width * height); if (EnableTextureAttachments) { while (entry.Length > 0) { var childEntry = new BitmapEntry(); ReadEntry(reader, childEntry); if (childEntry.Type == BitmapEntryType.DosPalette || childEntry.Type == BitmapEntryType.Palette) { _palette = _lastPalette; } if (childEntry.Length == 0) break; } } if (_palette == null) { _palette = _lastPalette; } TextureGenerator tg = new TextureGenerator(_palette); entry.Texture = tg.Generate(pixelData, width, height); entry.Type = BitmapEntryType.Texture; if (TextureGenerated != null) TextureGenerated(entry, _palette, pixelData); } byte[] LoadPalette(BinaryReader reader, BitmapEntryType type) { byte[] pal = reader.ReadBytes(3 * 256); switch (type) { case BitmapEntryType.DosPalette: // 0x22 palettes are DOS-style. r,g,b values in range 0..64 for (int i = 0; i < 768; i++) { pal[i] = (byte)(pal[i] << 2); } return pal; case BitmapEntryType.Palette: // 0x24 palettes have r,g,b in range 0..255. Nothing to do return pal; default: //throw new NotImplementedException(); return null; } } public BitmapEntry FindByName(string name) { return _bitmaps.Find(a => a.Id.Equals(name, StringComparison.InvariantCultureIgnoreCase)); } } } ================================================ FILE: OpenNFS1/Parsers/BitmapLoader.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using System.IO; namespace OpenNFS1.Parsers { class TextureGenerator { byte[] _palette; public TextureGenerator(byte[] palette) { _palette = palette; } public Texture2D Generate(byte[] pixelData, int width, int height) { Texture2D newTexture = new Texture2D(Engine.Instance.Device, width, height); newTexture.SetData(GenerateImageData(pixelData, width, height)); return newTexture; } private byte[] GenerateImageData(byte[] pixelData, int width, int height) { Vector3 mostUsed = Vector3.Zero; int overhang = 0; // (4 - ((width * 4) % 4)); int stride = (width * 4) + overhang; byte[] imgData = new byte[stride * height]; int curPosition = 0; for (int i = 0; i < height; i++) { for (int x = 0; x < width; x++) { byte pixel = pixelData[width * i + x]; if (pixel == 0xFF) { //byte[] rgb = GetRGBForPixel(mostUsed); imgData[curPosition] = (byte)mostUsed.Z; // rgb[2]; imgData[curPosition + 1] = (byte)mostUsed.Y; // rgb[1]; imgData[curPosition + 2] = (byte)mostUsed.X; // rgb[0]; imgData[curPosition + 3] = 0; } else { byte[] rgb = GetRGBForPixel(pixel); imgData[curPosition] = rgb[0]; imgData[curPosition + 1] = rgb[1]; imgData[curPosition + 2] = rgb[2]; imgData[curPosition + 3] = 0xFF; } curPosition += 4; } curPosition += overhang; } return imgData; } private Vector3 GetMostUsedColour(string id, byte[] pixels) { if (_palette == null) return new Vector3(255, 0, 255); if (id.StartsWith("tyr")) { return Vector3.Zero; } byte[] coloursUsed = new byte[256]; foreach (byte pixel in pixels) { if (pixel != 0xFF) coloursUsed[pixel]++; } int coloursUsedCount = 1; Vector3 avgColour = Vector3.Zero; for (int i = 0; i < 255; i++) { if (coloursUsed[i] > 0) { avgColour += new Vector3(_palette[i * 3], _palette[i * 3 + 1], (int)_palette[i * 3 + 2]); coloursUsedCount++; } } return avgColour / coloursUsedCount; } private byte[] GetRGBForPixel(int pixel) { if (_palette == null) return new byte[] { 255, 0, 255 }; byte[] rgb = new byte[3]; rgb[0] = _palette[pixel * 3]; rgb[1] = _palette[pixel * 3 + 1]; rgb[2] = _palette[pixel * 3 + 2]; return rgb; } } } ================================================ FILE: OpenNFS1/Parsers/CfmFile.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Xna.Framework.Graphics; using GameEngine; using Microsoft.Xna.Framework; using OpenNFS1.Vehicles; namespace OpenNFS1.Parsers { // .CFM files contain the vertices and textures for cars. class CfmFile { public CarMesh Mesh { get; private set; } private Color _brakeColor; // A CFM file can contain either a 'full' model (drivable, has wheel definitions etc), or a traffic model which is // only a static model and not drivable public CfmFile(string filename) { Parse(filename); } private void Parse(string filename) { string carFile = Path.Combine(GameConfig.CdDataPath, filename); BinaryReader br = new BinaryReader(File.Open(carFile, FileMode.Open)); HeaderChunk rootChunk = new HeaderChunk(); rootChunk.Read(br, true); // Cfm files contain a high-res model + bitmaps at index 0, and a low-res model + bitmaps at index 1. We only use the high-res resources. rootChunk.MeshChunks[0].Load(br); rootChunk.BitmapChunks[0].TextureGenerated += CfmFile_TextureGenerated; rootChunk.BitmapChunks[0].Load(br); br.Close(); Mesh = new CarMesh(rootChunk.MeshChunks[0], rootChunk.BitmapChunks[0], _brakeColor); } // The brakes are painted with palette color #254. Remember what color that maps to now so we can generate brake on/off textures later void CfmFile_TextureGenerated(BitmapEntry entry, byte[] palette, byte[] pixelData) { if (entry.Id == "rsid") { _brakeColor = new Color(palette[254 * 3], palette[254 * 3 + 1], palette[254 * 3 + 2]); } } } } ================================================ FILE: OpenNFS1/Parsers/FshFile.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using OpenNFS1; using OpenNFS1.Parsers; namespace OpenNFS1.Parsers { // A FSH file holds 1 or many bitmap entries class FshFile { public BitmapChunk Header { get; private set; } public FshFile(string filename) { filename = Path.Combine(GameConfig.CdDataPath, filename); Parse(File.Open(filename, FileMode.Open)); } public FshFile(byte[] contents) { Parse(new MemoryStream(contents)); } private void Parse(Stream contents) { BinaryReader br = new BinaryReader(contents); Header = new BitmapChunk(); Header.EnableTextureAttachments = true; Header.SkipHeader(br); Header.Read(br); br.Close(); } } } ================================================ FILE: OpenNFS1/Parsers/HeaderChunk.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Xna.Framework.Graphics; using System.Diagnostics; namespace OpenNFS1.Parsers { class HeaderChunk : BaseChunk { private List _meshChunks = new List(); private List _bitmapChunks = new List(); private List _headerChunks = new List(); private static int _index; public override void Read(BinaryReader reader) { Read(reader, false, false); } public void Read(BinaryReader reader, bool readHeadersOnly) { Read(reader, true, readHeadersOnly); } public void Read(BinaryReader reader, bool readIdentifier, bool readHeadersOnly) { Debug.WriteLine(">> Reading header chunk"); if (readIdentifier) { char[] identifier = reader.ReadChars(4); } long offset = reader.BaseStream.Position - 4; //-4 for identifier int chunkCount = reader.ReadInt32(); List chunkOffsets = new List(); for (int i = 0; i < chunkCount; i++) { chunkOffsets.Add(reader.ReadInt32()); } for (int i = 0; i < chunkCount; i++) { reader.BaseStream.Position = offset + chunkOffsets[i]; ReadChunk(reader, readHeadersOnly); } } private void ReadChunk(BinaryReader reader, bool readHeaderOnly) { string identifier = new string(reader.ReadChars(4)); if (identifier == "ORIP") { MeshChunk meshChunk = new MeshChunk(); if (readHeaderOnly) meshChunk.Offset = reader.BaseStream.Position; else { meshChunk.Read(reader); } _meshChunks.Add(meshChunk); } else if (identifier == "SHPI") { BitmapChunk bitmapChunk = new BitmapChunk(); bitmapChunk.Index = _index++; if (readHeaderOnly) bitmapChunk.Offset = reader.BaseStream.Position; else bitmapChunk.Read(reader); _bitmapChunks.Add(bitmapChunk); } else if (identifier == "wwww") { HeaderChunk header = new HeaderChunk(); if (readHeaderOnly) header.Offset = reader.BaseStream.Position; else header.Read(reader, false, false); _headerChunks.Add(header); } else { throw new NotImplementedException(); } } internal List MeshChunks { get { return _meshChunks; } } internal List BitmapChunks { get { return _bitmapChunks; } } internal List HeaderChunks { get { return _headerChunks; } } } } ================================================ FILE: OpenNFS1/Parsers/MeshChunk.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GameEngine; using System.Diagnostics; using OpenNFS1; namespace OpenNFS1.Parsers { class MeshChunk : BaseChunk { List _vertices = new List(); List _vertexTextureMap = new List(); List _polygons = new List(); private List _textureNames = new List(); public string Identifier; public List Polygons { get { return _polygons; } } public override void Read(BinaryReader reader) { base.Read(reader); Debug.WriteLine(">> Loading mesh chunk"); int unk = reader.ReadInt32(); unk = reader.ReadInt32(); int vertexCount = reader.ReadInt32(); unk = reader.ReadInt32(); int vertexBlockOffset = reader.ReadInt32(); int texturePointsCount = reader.ReadInt32(); int texturePointsBlockOffset = reader.ReadInt32(); int polygonCount = reader.ReadInt32(); int polygonBlockOffset = reader.ReadInt32(); string identifer = new string(reader.ReadChars(12)); Identifier = identifer.Substring(0, identifer.IndexOf('\0')); Debug.WriteLine("Loading mesh " + Identifier); int textureNameCount = reader.ReadInt32(); int textureNameBlockOffset = reader.ReadInt32(); reader.ReadBytes(16); //section 4,5 int polygonVertexMapBlockOffset = reader.ReadInt32(); reader.ReadBytes(8); //section 6 int labelCount = reader.ReadInt32(); int polygonLabelBlockOffset = reader.ReadInt32(); reader.BaseStream.Position = Offset + polygonVertexMapBlockOffset; MemoryStream ms = new MemoryStream(reader.ReadBytes(_length - polygonVertexMapBlockOffset)); BinaryReader polygonVertexMapReader = new BinaryReader(ms); reader.BaseStream.Position = Offset + vertexBlockOffset; ReadVertexBlock(reader, vertexCount); reader.BaseStream.Position = Offset + texturePointsBlockOffset; ReadTextureMapBlock(reader, texturePointsCount); reader.BaseStream.Position = Offset + textureNameBlockOffset; ReadTextureNameBlock(reader, textureNameCount); reader.BaseStream.Position = Offset + polygonBlockOffset; ReadPolygonBlock(reader, polygonCount, polygonVertexMapReader); polygonVertexMapReader.Close(); reader.BaseStream.Position = Offset + polygonLabelBlockOffset; ReadPolygonLabelBlock(reader, labelCount); } private void ReadVertexBlock(BinaryReader reader, int vertexCount) { for (int i = 0; i < vertexCount; i++) { float x = reader.ReadInt32(); float y = reader.ReadInt32(); float z = reader.ReadInt32(); Vector3 vertex = new Vector3(x, y, -z) * GameConfig.MeshScale; _vertices.Add(vertex); } } private void ReadTextureMapBlock(BinaryReader reader, int texturePointsCount) { for (int i = 0; i < texturePointsCount; i++) { int tU = reader.ReadInt32(); int tV = reader.ReadInt32(); _vertexTextureMap.Add(new Vector2(tU, tV)); } } private void ReadTextureNameBlock(BinaryReader reader, int textureNameCount) { for (int i = 0; i < textureNameCount; i++) { reader.ReadBytes(8); string id2 = new string(reader.ReadChars(4)); if (id2 == "\0\0\0\0") id2 = null; _textureNames.Add(id2); reader.ReadBytes(8); } } private void ReadPolygonBlock(BinaryReader reader, int polygonCount, BinaryReader polygonVertexMap) { for (int i = 0; i < polygonCount; i++) { byte typeFlag = reader.ReadByte(); int shapeId = typeFlag & (0xff >> 5); // type = 3 or 4. Held in the first 3 bits //bool computeTextureUVs = (typeFlag & (0x1 << 3)) != 0; // bit 3 is set if there are *no* texture co-ords (and we should infer them?) byte b1 = reader.ReadByte(); byte textureNumber = reader.ReadByte(); byte b2 = reader.ReadByte(); int verticesIndex = reader.ReadInt32(); int textureCoordsIndex = reader.ReadInt32(); if (shapeId != 3 && shapeId != 4) { // something in Burnt Sienna has a value of 2. Haven't investigated yet. continue; } // if these 2 indexes are the same, there are no texture coords bool computeTextureUVs = verticesIndex == textureCoordsIndex; Polygon polygon = new Polygon((PolygonShape)shapeId, computeTextureUVs); polygon.TextureName = _textureNames[textureNumber]; if (polygon.TextureName == "dkfr") { } //Vertices for polygon polygonVertexMap.BaseStream.Position = verticesIndex * sizeof(int); int v1 = polygonVertexMap.ReadInt32(); int v2 = polygonVertexMap.ReadInt32(); int v3 = polygonVertexMap.ReadInt32(); int v4 = polygonVertexMap.ReadInt32(); //Debug.WriteLine(String.Format("Poly {8} {0} {9} ({7}): {1},{2},{3},{4}, / {5} {6} computeUvs: {10}", i, v1, v2, v3, v4, b1, b2, polygon.TextureName, polygon.Shape, typeFlag.ToString("X"), computeTextureUVs)); //Texture co-ords for vertices if (!computeTextureUVs) { polygonVertexMap.BaseStream.Position = textureCoordsIndex * sizeof(int); int t1 = polygonVertexMap.ReadInt32(); int t2 = polygonVertexMap.ReadInt32(); int t3 = polygonVertexMap.ReadInt32(); int t4 = polygonVertexMap.ReadInt32(); polygon.Vertices[0] = _vertices[v1]; polygon.Vertices[1] = _vertices[v2]; polygon.Vertices[2] = _vertices[v3]; polygon.TextureUVs[0] = _vertexTextureMap[t1]; polygon.TextureUVs[1] = _vertexTextureMap[t2]; polygon.TextureUVs[2] = _vertexTextureMap[t3]; if (polygon.Shape == PolygonShape.Quad) { polygon.Vertices[3] = _vertices[v1]; polygon.Vertices[4] = _vertices[v3]; polygon.Vertices[5] = _vertices[v4]; polygon.TextureUVs[3] = _vertexTextureMap[t1]; polygon.TextureUVs[4] = _vertexTextureMap[t3]; polygon.TextureUVs[5] = _vertexTextureMap[t4]; } } else { if (polygon.Shape == PolygonShape.Quad) { polygon.Vertices[0] = _vertices[v1]; polygon.Vertices[1] = _vertices[v2]; polygon.Vertices[2] = _vertices[v3]; polygon.Vertices[3] = _vertices[v1]; polygon.Vertices[4] = _vertices[v3]; polygon.Vertices[5] = _vertices[v4]; } else { throw new NotImplementedException(); } } _polygons.Add(polygon); } } private void ReadPolygonLabelBlock(BinaryReader reader, int labelCount) { for (int i = 0; i < labelCount; i++) { string label = new string(reader.ReadChars(8)); label = label.Substring(0, label.IndexOf("\0")); int polyIndex = reader.ReadInt32(); _polygons[polyIndex].Label = label; } } } } ================================================ FILE: OpenNFS1/Parsers/OpenRoadTrackfamFile.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using Microsoft.Xna.Framework.Graphics; namespace OpenNFS1.Parsers { class OpenRoadTrackfamFile : TrackfamFile { public OpenRoadTrackfamFile(string trackFile, bool alternateTimeOfDay) : base(trackFile, alternateTimeOfDay) { } public override Texture2D GetGroundTexture(int textureNbr) { int groupId = textureNbr / 3; int remainder = textureNbr % 3; string id = null; if (remainder == 0) id = "A000"; else if (remainder == 1) id = "B000"; else if (remainder == 2) id = "C000"; else throw new NotImplementedException(); BitmapEntry found = _root.HeaderChunks[TERRAIN_CHUNK].BitmapChunks[groupId].Bitmaps.Find(a => a.Id == id); if (found == null) { Debug.WriteLine("Warning: Ground texture not found: " + textureNbr); return null; } else { return found.Texture; } } public override Texture2D GetSceneryTexture(int textureNbr) { textureNbr /= 4; BitmapEntry found = _root.HeaderChunks[SCENERY_CHUNK].BitmapChunks[textureNbr].Bitmaps.Find(a => a.Id == "0000"); if (found == null) { Debug.WriteLine("Warning: Scenery texture not found: " + textureNbr); return null; } else { return found.Texture; } } public override Texture2D GetFenceTexture(int textureNbr) { var tex = GetGroundTexture(textureNbr); if (tex == null) { //nasty hack: handle Alpine 1's initial fence textureIds which don't seem to match any other tracks... tex = GetGroundTexture(textureNbr * 3); } return tex; } } } ================================================ FILE: OpenNFS1/Parsers/QfsFile.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using OpenNFS1; namespace OpenNFS1.Parsers { // A QFS file is a compressed FSH file. A FSH file holds bitmaps class QfsFile { public FshFile Content { get; private set; } public QfsFile(string filename) { filename = Path.Combine(GameConfig.CdDataPath, filename); byte[] decompressedData = Decompress(filename); Content = new FshFile(decompressedData); } // Apparently this LZ77 compression variant was figured out by an 'Ian Brown' (can't find any contact info for him). // This method was ported from Dennis Auroux's fshtool.c source. (http://www-math.mit.edu/~auroux/software/index.html) private byte[] Decompress(string filename) { byte[] inbuf = File.ReadAllBytes(filename); int buflen = inbuf.Length; byte[] outbuf; byte packcode; int a,b,c,len,offset; int inlen,outlen,inpos,outpos; /* length of data */ inlen=buflen; outlen=(inbuf[2]<<16)+(inbuf[3]<<8)+inbuf[4]; outbuf= new byte[outlen]; /* position in file */ if ((inbuf[0] & 0x01) != 0) inpos=8; else inpos=5; outpos=0; /* main decoding loop */ while ((inpos>2)+3; offset=((packcode>>5)<<8)+a+1; mmemcpy(outbuf,outpos,outbuf,outpos-offset,len); outpos+=len; } else if ((packcode&0x40) == 0) { len=(a>>6)&3; mmemcpy(outbuf,outpos,inbuf,inpos+3,len); inpos+=len+3; outpos+=len; len=(packcode&0x3f)+4; offset=(a&0x3f)*256+b+1; mmemcpy(outbuf,outpos,outbuf,outpos-offset,len); outpos+=len; } else if ((packcode&0x20) == 0) { c=inbuf[inpos+3]; len=packcode&3; mmemcpy(outbuf,outpos,inbuf,inpos+4,len); inpos+=len+4; outpos+=len; len=((packcode>>2)&3)*256+c+5; offset=((packcode&0x10)<<12)+256*a+b+1; mmemcpy(outbuf,outpos,outbuf,outpos-offset,len); outpos+=len; } else { len=(packcode&0x1f)*4+4; mmemcpy(outbuf,outpos,inbuf,inpos+1,len); inpos+=len+1; outpos+=len; } } /* trailing bytes */ if ((inpos a.Id == id); if (found == null) { Debug.WriteLine("Warning: Ground texture not found: " + textureNbr); return null; } else return found.Texture; } public virtual Texture2D GetSceneryTexture(int textureNbr) { textureNbr /= 4; string texture = textureNbr.ToString("00") + "00"; BitmapEntry found = _root.HeaderChunks[SCENERY_CHUNK].BitmapChunks[0].Bitmaps.Find(a => a.Id == texture); if (found == null) { Debug.WriteLine("Warning: Scenery texture not found: " + textureNbr); return null; } else { return found.Texture; } } public Texture2D HorizonTexture { get { return _root.BitmapChunks[0].FindByName("horz").Texture; } } public virtual Texture2D GetFenceTexture(int textureNbr) { return _root.HeaderChunks[TERRAIN_CHUNK].BitmapChunks[0].FindByName("ga00").Texture; } // Mesh data (vertices + textures) are stored in the 3rd chunk of a FAM file public Mesh GetMesh(int index) { if (_meshCache[index] == null) { var meshChunk = _root.HeaderChunks[MESH_CHUNK].HeaderChunks[index].MeshChunks[0]; var bmpChunk = _root.HeaderChunks[MESH_CHUNK].HeaderChunks[index].BitmapChunks[0]; _meshCache[index] = new Mesh(meshChunk, bmpChunk); } return _meshCache[index]; } public void Dispose() { foreach (var bm in _root.HeaderChunks[TERRAIN_CHUNK].BitmapChunks[0].Bitmaps) { if (bm.Texture != null) bm.Texture.Dispose(); } foreach (var bm in _root.HeaderChunks[SCENERY_CHUNK].BitmapChunks[0].Bitmaps) { if (bm.Texture != null) bm.Texture.Dispose(); } foreach (var bm in _root.HeaderChunks[MESH_CHUNK].BitmapChunks) { foreach (var bm2 in bm.Bitmaps) if (bm2.Texture != null) bm2.Texture.Dispose(); } foreach (var mesh in _meshCache) { if (mesh != null) mesh.Dispose(); } } } } ================================================ FILE: OpenNFS1/Parsers/TriFile.cs ================================================ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1; using OpenNFS1.Parsers.Track; using OpenNFS1.Tracks; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; namespace OpenNFS1.Parsers { enum SceneryType { Model = 1, Bitmap = 4, TwoSidedBitmap = 6 } [Flags] enum SceneryFlags { None = 0, Unk1 = 1, Animated = 4 } class SceneryObjectDescriptor { public int Id; public float Width, Height; public SceneryFlags Flags; public SceneryType Type; public int ResourceId, Resource2Id; public int AnimationFrameCount; } class SceneryObject { public SceneryObjectDescriptor Descriptor; public int ReferenceNode; public float Orientation; public Vector3 RelativePosition; } /// /// Tri files contain the track vertices, physical road data, scenery item placement /// class TriFile { public const int NbrTerrainPointsPerSide = 6; //includes the middle-point (duplicated on both sides) public const int NbrRowsPerSegment = 4; public List Nodes { get; private set; } public List Scenery { get; private set; } public List ObjectDescriptors { get; private set; } public List Segments { get; private set; } public bool IsOpenRoad { get; private set; } public string FileName { get; private set; } public int FenceTextureId { get; private set; } public TriFile(string filename) { FileName = Path.GetFileName(filename); filename = Path.Combine(GameConfig.CdDataPath, filename); BinaryReader reader = new BinaryReader(File.Open(filename, FileMode.Open)); byte trackVersion = reader.ReadByte(); if (trackVersion != 0x11) { throw new Exception("Tri file version not supported: " + trackVersion); } Nodes = new List(); ObjectDescriptors = new List(); Scenery = new List(); Segments = new List(); ParseTrackNodesBlock(reader); ParseSceneryObjectsBlock(reader); ParseTerrainBlock(reader); ComputeAbsoluteTerrainPoints(); reader.Close(); } void ParseTrackNodesBlock(BinaryReader reader) { reader.BaseStream.Position = 2444; //start of node list TrackNode prevNode = null; for (int i = 0; i < 2400; i++) { var node = new TrackNode(); node.Number = Nodes.Count; float vergeScale = 8000; node.DistanceToLeftVerge = reader.ReadByte(); node.DistanceToLeftVerge *= GameConfig.TerrainScale * vergeScale; node.DistanceToRightVerge = reader.ReadByte(); node.DistanceToRightVerge *= GameConfig.TerrainScale * vergeScale; node.DistanceToLeftBarrier = reader.ReadByte(); node.DistanceToLeftBarrier *= GameConfig.TerrainScale * vergeScale; node.DistanceToRightBarrier = reader.ReadByte(); node.DistanceToRightBarrier *= GameConfig.TerrainScale * vergeScale; node.Flag1 = reader.ReadByte(); node.Flag2 = reader.ReadByte(); node.Flag3 = reader.ReadByte(); node.NodeProperty = reader.ReadByte(); // unused trackNodes are filled with zeroes, so stop when we have a node with a zero position if (node.DistanceToLeftVerge == 0 && node.DistanceToRightVerge == 0 && node.DistanceToLeftBarrier == 0 && node.DistanceToRightBarrier == 0) { break; } //Debug.WriteLine("{0},{1},{2},{3}", node.b[0], node.b[1], node.b[2], node.b[3]); node.Position = new Vector3(reader.ReadInt32(), reader.ReadInt32(), -reader.ReadInt32()) * GameConfig.TerrainScale; // Slope is stored as a 2's complement value. Convert it back to signed value Int16 slope = reader.ReadInt16(); //bool msbSet = (slope & (0x1 << 13)) != 0; //if (msbSet) //{ // slope = (short)~slope; // slope++; // slope *= -1; //} if (slope > 0x2000) { slope -= 0x3FFF; } node.Slope = slope; node.Slant = reader.ReadInt16(); //weird slant-A float orientation = (float)reader.ReadInt16(); //convert to signed degrees //0 = forwards, 0x1000 = right, 0x2000 = back, 0x3000 = left, 0x3FFF back to forwards if (orientation > 0x2000) { orientation -= 0x3FFF; } node.Orientation = ((orientation / 0x3FFF) * -360); node.unk1 = reader.ReadBytes(2); node.ZOrientation = reader.ReadInt16(); node.Slant = reader.ReadInt16(); // slant-B node.XOrientation = reader.ReadInt16(); node.unk2 = reader.ReadBytes(2); if (prevNode != null) { prevNode.Next = node; node.Prev = prevNode; } prevNode = node; Nodes.Add(node); } // If this is a circuit track, hook the last node up to the first if (Vector3.Distance(Nodes[0].Position, Nodes[Nodes.Count - 1].Position) > 100) { IsOpenRoad = true; } else { prevNode.Next = Nodes[0]; Nodes[0].Prev = prevNode; } for (int i = 0; i < Nodes.Count - 1; i++) { var node = Nodes[i]; var normal = Vector3.Cross(node.GetRightBoundary() - node.GetLeftBoundary(), node.Next.Position - node.GetLeftBoundary()); node.Up = Vector3.Normalize(normal); } } private void ParseSceneryObjectsBlock(BinaryReader reader) { reader.BaseStream.Position = 0x15B0C; reader.BaseStream.Position += 1800; //jump over unknown index int objectDescriptorCount = reader.ReadInt32(); int objectCount = reader.ReadInt32(); reader.ReadChars(4); //'OBJS' reader.BaseStream.Position += 8; for (int i = 0; i < objectDescriptorCount; i++) { SceneryObjectDescriptor sd = new SceneryObjectDescriptor(); byte[] bytes = reader.ReadBytes(16); sd.Flags = (SceneryFlags)bytes[0]; sd.Type = (SceneryType)bytes[1]; sd.Width = BitConverter.ToInt16(bytes, 6); sd.Width *= GameConfig.TerrainScale * 65000; sd.Height = BitConverter.ToInt16(bytes, 14); sd.Height *= GameConfig.TerrainScale * 65000; sd.ResourceId = bytes[2]; if (sd.Type == SceneryType.TwoSidedBitmap) { sd.Resource2Id = bytes[3]; } if ((sd.Flags & SceneryFlags.Animated) == SceneryFlags.Animated) { sd.AnimationFrameCount = bytes[8]; } sd.Id = ObjectDescriptors.Count; ObjectDescriptors.Add(sd); } long currentPos = reader.BaseStream.Position; for (int i = 0; i < objectCount; i++) { int referenceNode = reader.ReadInt32(); if (referenceNode == -1) { break; } SceneryObject obj = new SceneryObject(); obj.ReferenceNode = referenceNode % Nodes.Count; //why do some tracks reference nodes above the node count? int descriptorRef = reader.ReadByte(); obj.Descriptor = ObjectDescriptors[descriptorRef]; obj.Orientation = (float)reader.ReadByte(); byte[] flags = reader.ReadBytes(4); obj.RelativePosition = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); Scenery.Add(obj); } reader.BaseStream.Position = currentPos + 16000; //jump to end of billboards reader.BaseStream.Position += 492; //blank space } private void ParseTerrainBlock(BinaryReader reader) { reader.BaseStream.Position = 0x1A4A8; long fileSize = reader.BaseStream.Length; long position = reader.BaseStream.Position; TerrainSegment last = null; while (true) { char[] trkd = reader.ReadChars(4); //"TRKD" if (trkd[0] == 'C') //Strange format un-used AL22.TRI { return; } int blockLength = reader.ReadInt32(); int blockNumber = reader.ReadInt32(); byte unk1 = reader.ReadByte(); byte fenceType = reader.ReadByte(); TerrainSegment terrainSegment = new TerrainSegment(); terrainSegment.Number = Segments.Count; terrainSegment.TextureIds = reader.ReadBytes(10); terrainSegment.Rows[0] = ReadTerrainRow(reader); terrainSegment.Rows[1] = ReadTerrainRow(reader); terrainSegment.Rows[2] = ReadTerrainRow(reader); terrainSegment.Rows[3] = ReadTerrainRow(reader); //fenceType stores the sides of the road the fence lives, and the textureId to use for it. // If the top bit is set, fence on the left exists, if next bit is set, fence is on the right. Both can also be set. //The other 6 bits seem to the texture number if (fenceType != 0) { bool bit7 = (fenceType & (0x1 << 7)) != 0; bool bit6 = (fenceType & (0x1 << 6)) != 0; terrainSegment.HasLeftFence = bit7; terrainSegment.HasRightFence = bit6; // Ignore the top 2 bits to find the texture to use terrainSegment.FenceTextureId = fenceType & (0xff >> 2); //Debug.WriteLine("fence: " + fenceType + ", texture: " + terrainSegment.FenceTextureId + ", " + terrainSegment.HasLeftFence + ", " + terrainSegment.HasRightFence); } //Debug.WriteLine("TRKD: " + i + ", " + terrainSegment.Rows[0].RightPoints[5] + ", fence: " + terrainSegment.FenceTextureId + " , " + terrainSegment.HasLeftFence + ", " + terrainSegment.HasRightFence); if (last != null) { last.Next = terrainSegment; terrainSegment.Prev = last; } last = terrainSegment; Segments.Add(terrainSegment); //skip to end of block (+12 to include block header) position += blockLength + 12; reader.BaseStream.Position = position; if (reader.BaseStream.Position >= fileSize) { break; } } // If this is a circuit track, hook the last segment up to the first if (!IsOpenRoad) { last.Next = Segments[0]; Segments[0].Prev = last; } } private TerrainRow ReadTerrainRow(BinaryReader reader) { TerrainRow row = new TerrainRow(); row.MiddlePoint = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.RightPoints[0] = row.MiddlePoint; row.RightPoints[1] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.RightPoints[2] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.RightPoints[3] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.RightPoints[4] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.RightPoints[5] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.LeftPoints[0] = row.MiddlePoint; row.LeftPoints[1] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.LeftPoints[2] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.LeftPoints[3] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.LeftPoints[4] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.LeftPoints[5] = new Vector3(reader.ReadInt16(), reader.ReadInt16(), -reader.ReadInt16()); row.MiddlePoint *= GameConfig.TerrainScale; for (int i = 0; i < NbrTerrainPointsPerSide; i++) row.RightPoints[i] *= GameConfig.TerrainScale; for (int i = 0; i < NbrTerrainPointsPerSide; i++) row.LeftPoints[i] *= GameConfig.TerrainScale; // Each point is relative to the previous point for (int i = 1; i < NbrTerrainPointsPerSide; i++) { row.RightPoints[i] += row.RightPoints[i - 1]; } for (int i = 1; i < NbrTerrainPointsPerSide; i++) { row.LeftPoints[i] += row.LeftPoints[i - 1]; } return row; } private void ComputeAbsoluteTerrainPoints() { var segmentIndex = 0; var nodeIndex = 0; foreach (var segment in Segments) { if (segmentIndex == 48) { } for (int i = 0; i < NbrRowsPerSegment; i++) { var relativePoint = Nodes[nodeIndex].Position; segment.Rows[i].RelativizeTo(relativePoint); nodeIndex++; } segmentIndex++; } } } } ================================================ FILE: OpenNFS1/Physics/AutoGearbox.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; using Microsoft.Xna.Framework.Input; namespace OpenNFS1.Physics { class AutoGearbox : BaseGearbox { private const float ChangeUpPoint = 0.94f; private const float ChangeDownPoint = 0.65f; public AutoGearbox(List ratios, float changeTime) : base(ratios, changeTime) { } public override void Update(float motorRpmPercent, GearboxAction action) { if (_motor.Rpm < 2 && (_currentGear == GEAR_NEUTRAL || _currentGear == GEAR_1) && action == GearboxAction.GearDown) { GearDown(); } if ((_currentGear == GEAR_REVERSE || _currentGear == GEAR_NEUTRAL) && action == GearboxAction.GearUp) { GearUp(); } if (!_motor.WheelsSpinning) { if (_currentGear == GEAR_REVERSE || _currentGear == GEAR_NEUTRAL) { } else { if (motorRpmPercent > ChangeUpPoint && _currentGear < Ratios.Count - 1) GearUp(); if (motorRpmPercent < ChangeDownPoint && CurrentGear > 1 && GearEngaged && !_motor.IsAccelerating && _motor.CanChangeDown) GearDown(); } } base.Update(motorRpmPercent, action); } } } ================================================ FILE: OpenNFS1/Physics/BaseGearbox.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; using Microsoft.Xna.Framework; using GameEngine; namespace OpenNFS1.Physics { class GearboxGearChange { public int Change; public float TimeTillEngaged; } enum GearboxAction { None, GearUp, GearDown } abstract class BaseGearbox { public const int GEAR_REVERSE = 0; public const int GEAR_NEUTRAL = 1; public const int GEAR_1 = 2; public event EventHandler GearChangeStarted; public event EventHandler GearChangeCompleted; private List _ratios; protected float _changeTime; protected int _currentGear; protected GearboxGearChange _gearChange; protected Motor _motor; private float _clutch; public float Clutch { get { return _clutch; } set { _clutch = value; } } public List Ratios { get { return _ratios; } } public int CurrentGear { get { return _currentGear - 1; } set { _currentGear = value + 1; } } public float CurrentRatio { get { return _ratios[_currentGear]; } } public float NextRatio { get { return _ratios[_currentGear + _gearChange.Change]; } } public int NextGear { get { return CurrentGear + _gearChange.Change; } } public bool GearEngaged { get { return _gearChange == null; } } public Motor Motor { set { _motor = value; } } public BaseGearbox(List ratios, float changeTime) { ratios.Insert(0, -ratios[0] * 1.5f); //insert reverse ratios.Insert(1, 0); //insert neutral _ratios = ratios; _changeTime = changeTime; _currentGear = GEAR_NEUTRAL; } public void GearUp() { if (_gearChange == null) { _gearChange = new GearboxGearChange(); _gearChange.Change = 1; _gearChange.TimeTillEngaged = _changeTime; _clutch = 0.0f; GearChangeStarted(this, null); } } public void GearDown() { if (_gearChange == null) { _gearChange = new GearboxGearChange(); _gearChange.Change = -1; _gearChange.TimeTillEngaged = _changeTime; _clutch = 0.0f; GearChangeStarted(this, null); } } /// /// /// /// 0..1 where 1 is max rpms /// public virtual void Update(float motorRpmPercent, GearboxAction action) { if (_gearChange != null) { _gearChange.TimeTillEngaged -= Engine.Instance.FrameTime; if (_gearChange.TimeTillEngaged <= 0) { if (_gearChange.Change > 0) { if (_currentGear < _ratios.Count - 1) _currentGear++; } else { if (_currentGear > -1) _currentGear--; } _clutch = 1.0f; _gearChange = null; if (GearChangeCompleted != null) GearChangeCompleted(this, null); } else { _clutch = (_changeTime - _gearChange.TimeTillEngaged) / _changeTime; } } } public static BaseGearbox Create(bool manual, List ratios, float changeTime) { if (manual) return new ManualGearbox(ratios, changeTime); else return new AutoGearbox(ratios, changeTime); } } } ================================================ FILE: OpenNFS1/Physics/DrivableVehicle.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GameEngine; using OpenNFS1.Parsers.Track; using OpenNFS1.Tracks; using OpenNFS1.Vehicles; namespace OpenNFS1.Physics { class DrivableVehicle : Vehicle { const float CarFrictionOnRoad = 14; const float AirFrictionPerSpeed = 0.07f; const float MaxAirFriction = AirFrictionPerSpeed * 100.0f; // for reflecting away from walls public float MaxRotationPerSec = 5f; public float RearSlipFactorSpeed = 1.6f; public float RearSlipFactorMax = 0.5f; public float FrontSlipMultiplier = 0.0000017f; public VehicleDescription Descriptor { get; private set; } public float BrakePower = 70; public float HandbrakePower = 40; public Motor Motor { get; private set; } public Vector3 RenderDirection; public Spring BodyPitch { get; private set; } public Spring BodyRoll { get; private set; } private float _rotateAfterCollision; public float RotateCarAfterCollision { set { _rotateAfterCollision = value; _rotationChange = 0; } } public VehicleWheel[] Wheels { get; private set; } float _frontSlipFactor, _rearSlipFactor; Vector3 _force; VehicleAudioProvider _audioProvider; int _nbrWheelsOffRoad; float _traction = 520; // inputs public float ThrottlePedalInput, BrakePedalInput; public bool GearUpInput, GearDownInput, HandbrakeInput; public bool AutoDrift; //used for ai racers public DrivableVehicle(VehicleDescription desc) : base(desc.ModelFile) { Descriptor = desc; float offset = VehicleWheel.Width / 2 - 0.1f; Wheels = new VehicleWheel[4]; Wheels[0] = new VehicleWheel(this, _model.LeftFrontWheelPos, _model.FrontWheelSize, _model.WheelTexture, offset); Wheels[1] = new VehicleWheel(this, _model.RightFrontWheelPos, _model.FrontWheelSize, _model.WheelTexture, -offset); Wheels[2] = new VehicleWheel(this, _model.LeftRearWheelPos, _model.RearWheelSize, _model.WheelTexture, offset); Wheels[3] = new VehicleWheel(this, _model.RightRearWheelPos, _model.RearWheelSize, _model.WheelTexture, -offset); List power = new List(new float[] { 0.2f, 0.3f, 0.4f, 0.7f, 0.8f, 1.0f, 0.8f, 0.8f, 0.8f, 0.3f }); List ratios = new List(new float[] { 3.827f, 2.360f, 1.685f, 1.312f, 1.000f, 0.793f }); BaseGearbox gearbox = BaseGearbox.Create(GameConfig.ManualGearbox, ratios, 0.2f); Motor = new Motor(power, Descriptor.Horsepower, Descriptor.Redline, gearbox); Motor.Gearbox.GearChangeStarted += new EventHandler(Gearbox_GearChanged); _traction = (Motor.GetPowerAtRpmForGear(Motor.RedlineRpm, 2) * 30) - 30; BodyPitch = new Spring(1200, 1.5f, 200, 0, 1.4f); BodyRoll = new Spring(1200, 1.5f, 180, 0, 3); _audioProvider = new VehicleAudioProvider(this); } bool _audioEnabled; public bool AudioEnabled { get { return _audioEnabled; } set { if (value) _audioProvider.Initialize(); else _audioProvider.StopAll(); _audioEnabled = value; } } private void UpdateWheels() { //front wheels for (int i = 0; i < 2; i++) Wheels[i].Rotation -= Speed * Engine.Instance.FrameTime; //back wheels if (!HandbrakeInput) { for (int i = 2; i < 4; i++) { Wheels[i].Rotation -= Engine.Instance.FrameTime * (Motor.WheelsSpinning ? 50 : Speed); } } Wheels[0].Steer(_steeringWheel); Wheels[1].Steer(_steeringWheel); if (_isOnGround && BrakePedalInput > 0.5f && Math.Abs(Speed) > 10) { if (Speed < 80) { Wheels[0].IsSkidding = Wheels[1].IsSkidding = Wheels[2].IsSkidding = Wheels[3].IsSkidding = true; } _audioProvider.PlaySkid(true); } else if (_isOnGround && Math.Abs(_rearSlipFactor) > 0) { Wheels[2].IsSkidding = Wheels[3].IsSkidding = true; _audioProvider.PlaySkid(true); } else if (_isOnGround && Math.Abs(_steeringWheel) > 0.25f && _frontSlipFactor > 0.43f) { _audioProvider.PlaySkid(true); } else { _audioProvider.PlaySkid(false); } foreach (VehicleWheel wheel in Wheels) wheel.Update(); } private void UpdateDrag() { float elapsedSeconds = Engine.Instance.FrameTime; float airFriction = AirFrictionPerSpeed * Math.Abs(Speed); if (airFriction > MaxAirFriction) airFriction = MaxAirFriction; // Don't use ground friction if we are not on the ground. float groundFriction = CarFrictionOnRoad; if (_isOnGround == false) groundFriction = 0; _force *= 1.0f - (groundFriction + airFriction) * 0.06f * elapsedSeconds; Speed *= 1.0f - (groundFriction + airFriction) * 0.0015f * elapsedSeconds; if (_isOnGround) { float drag = BrakePower * BrakePedalInput; if (HandbrakeInput) drag += HandbrakePower; // as we're turning, add drag drag += Math.Abs(_steeringWheel) * 10f; if (Math.Abs(Speed) > 30) { drag += _nbrWheelsOffRoad * 5f; } drag += Motor.CurrentFriction; if (Math.Abs(Speed) < 1 || drag < 0) drag = 0; if (Speed > 0) { Speed -= drag * elapsedSeconds; if (Speed < 0) Speed = 0; //avoid braking so hard we go backwards } else if (Speed < 0) { Speed += drag * elapsedSeconds; } } } private void UpdateEngineForce() { _previousSpeed = Speed; float newAccelerationForce = 0.0f; Motor.Throttle = ThrottlePedalInput; newAccelerationForce += Motor.CurrentPowerOutput * 0.4f; if (Motor.Gearbox.GearEngaged && Motor.Gearbox.CurrentGear > 0) { float tractionFactor = Math.Min(1, (_traction + Speed) / newAccelerationForce); Motor.WheelsSpinning = tractionFactor < 1 || (Motor.Rpm > 0.7f && Speed < 10 && Motor.Throttle > 0); if (Motor.WheelsSpinning) { _audioProvider.PlaySkid(true); Wheels[2].IsSkidding = Wheels[3].IsSkidding = true; } else if (!_isOnGround) { Motor.WheelsSpinning = true; } } GearboxAction action = GearboxAction.None; if (GearUpInput) action = GearboxAction.GearUp; else if (GearDownInput) action = GearboxAction.GearDown; Motor.Update(Speed, action); if (Motor.AtRedline && !Motor.WheelsSpinning) { _force *= 0.2f; } if (Motor.Throttle == 0 && Math.Abs(Speed) < 1) { Speed = 0; } if (_isOnGround) _force += Direction * newAccelerationForce * Engine.Instance.FrameTime * 2.5f; } public override void Update() { if (CurrentNode.Next == null) return; float elapsedSeconds = Engine.Instance.FrameTime; UpdateRearSlip(); UpdateEngineForce(); UpdateDrag(); Vector3 speedChangeVector = _force / Descriptor.Mass; if (_isOnGround && speedChangeVector.Length() > 0) { float speedApplyFactor = Vector3.Dot(Vector3.Normalize(speedChangeVector), Direction); if (speedApplyFactor > 1) speedApplyFactor = 1; Speed += speedChangeVector.Length() * speedApplyFactor; } UpdateWheels(); CheckForCollisions(); // Calculate pitch depending on the force float speedChange = Speed - _previousSpeed; BodyPitch.ChangePosition(speedChange * 0.6f); BodyRoll.ChangePosition(_steeringWheel * -0.05f * Math.Min(1, Math.Abs(Speed) / 30)); BodyPitch.Simulate(Engine.Instance.FrameTime * 2.5f); BodyRoll.Simulate(Engine.Instance.FrameTime * 2.5f); _audioProvider.UpdateEngine(); base.Update(); } public override void HandleExtraSteeringPhysics() { float maxRot = MaxRotationPerSec * Engine.Instance.FrameTime; // Handle car rotation after collision if (_rotateAfterCollision != 0) { _audioProvider.PlaySkid(true); if (_rotateAfterCollision > maxRot) { _rotationChange += maxRot; _rotateAfterCollision -= maxRot; } else if (_rotateAfterCollision < -maxRot) { _rotationChange -= maxRot; _rotateAfterCollision += maxRot; } else { _rotationChange += _rotateAfterCollision; RotateCarAfterCollision = 0; } } else { _frontSlipFactor = 0; if (_rotationChange != 0) { _frontSlipFactor = Math.Min(0.91f, Descriptor.Mass * Math.Abs(Speed) * FrontSlipMultiplier); _rotationChange *= 1 - _frontSlipFactor; } } } void UpdateRearSlip() { if (_isOnGround) { if (((AutoDrift && Speed > 100) || HandbrakeInput || (SteeringInput != 0 && _rearSlipFactor != 0)) && Speed > 10) { if (SteeringInput < 0) { float prevSlipFactor = _rearSlipFactor; _rearSlipFactor = Math.Min(RearSlipFactorMax, _rearSlipFactor + RearSlipFactorSpeed * Engine.Instance.FrameTime); if (prevSlipFactor < 0 && _rearSlipFactor > 0) _rearSlipFactor = 0; } else if (SteeringInput > 0) { float prevSlipFactor = _rearSlipFactor; _rearSlipFactor = Math.Max(-RearSlipFactorMax, _rearSlipFactor - RearSlipFactorSpeed * Engine.Instance.FrameTime); if (prevSlipFactor > 0 && _rearSlipFactor < 0) _rearSlipFactor = 0; } } if (SteeringInput == 0 || Speed < 10) { if (Math.Abs(_rearSlipFactor) > 0.03f) _rearSlipFactor += 0.7f * (_rearSlipFactor > 0 ? -RearSlipFactorSpeed : RearSlipFactorSpeed) * Engine.Instance.FrameTime; else _rearSlipFactor = 0; } } RenderDirection = Vector3.TransformNormal(Direction, Matrix.CreateFromAxisAngle(Up, _rearSlipFactor)); } void CheckForCollisions() { _nbrWheelsOffRoad = VehicleFenceCollision.GetWheelsOutsideRoadVerge(this); if (Speed > 3 && _nbrWheelsOffRoad > 0) { _audioProvider.PlayOffRoad(true); Wheels[2].IsSkidding = Wheels[3].IsSkidding = true; } else { _audioProvider.PlayOffRoad(false); } VehicleFenceCollision.Handle(this); } public void RenderShadow(bool isPlayer) { // Shadow Vector3[] points = new Vector3[4]; float y = -Wheels[0].Size / 2 + 0.4f; float xoffset = 0.1f; points[0] = Wheels[0].GetOffsetPosition(new Vector3(-xoffset, y, -2)); points[1] = Wheels[1].GetOffsetPosition(new Vector3(xoffset, y, -2)); points[2] = Wheels[2].GetOffsetPosition(new Vector3(-xoffset, y, 3.5f)); points[3] = Wheels[3].GetOffsetPosition(new Vector3(xoffset, y, 3.5f)); if (!_isOnGround) { points[0].Y = _currentHeightOfTrack; points[1].Y = _currentHeightOfTrack; points[2].Y = _currentHeightOfTrack; points[3].Y = _currentHeightOfTrack; } ObjectShadow.Render(points, isPlayer); } public override void Render() { _effect.View = Engine.Instance.Camera.View; _effect.Projection = Engine.Instance.Camera.Projection; _effect.World = GetRenderMatrix(); _effect.CurrentTechnique.Passes[0].Apply(); _model.Render(_effect, BrakePedalInput > 0); WheelModel.BeginBatch(); foreach (VehicleWheel wheel in Wheels) { wheel.Render(); } } public override Matrix GetRenderMatrix() { Matrix orientation = Matrix.Identity; orientation.Right = Vector3.Cross(RenderDirection, Up); orientation.Up = Up; orientation.Forward = RenderDirection; return Matrix.CreateRotationX(BodyPitch.Position / 60) * Matrix.CreateRotationZ(-BodyRoll.Position * 0.21f) * orientation * Matrix.CreateTranslation(Position); } protected void Gearbox_GearChanged(object sender, EventArgs e) { _audioProvider.ChangeGear(); } public override void OnGroundHit() { _audioProvider.HitGround(); } } } ================================================ FILE: OpenNFS1/Physics/ManualGearbox.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework; namespace OpenNFS1.Physics { class ManualGearbox : BaseGearbox { public ManualGearbox(List ratios, float changeTime) : base(ratios, changeTime) { } public override void Update(float motorRpmPercent, GearboxAction action) { if (action == GearboxAction.GearUp && _currentGear < Ratios.Count - 1) GearUp(); if (action == GearboxAction.GearDown && CurrentGear > -1 && _motor.CanChangeDown) GearDown(); base.Update(motorRpmPercent, action); } } } ================================================ FILE: OpenNFS1/Physics/Motor.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using System.Diagnostics; using GameEngine; namespace OpenNFS1.Physics { class Motor { private const float DRIVETRAIN_MULTIPLIER = 34; private List _powerCurve; private float _maxPower; private float _redlineRpm; private BaseGearbox _gearbox; private float _rpm, _prevRpm, _prevEngagedRpm; private float _throttle; private float _currentPowerOutput; private float _rpmLimiter; private float _lastCarSpeed; public float CurrentPowerOutput { get { if (_gearbox.GearEngaged) return _currentPowerOutput * _throttle * _gearbox.CurrentRatio; else return 0; } } public float CurrentFriction { get { if (_gearbox.GearEngaged && _throttle == 0) return Math.Abs(_gearbox.CurrentRatio) * 4; else return 0; } } public float MaxPower { get { return _maxPower; } set { _maxPower = value; } } public bool AtRedline { get { return _rpm >= _redlineRpm; } } public float Throttle { get { return _throttle; } set { _throttle = value; } } public float Rpm { get { return _rpm; } } public float RedlineRpm { get { return _redlineRpm; } } public bool IsAccelerating { get { return _rpm > _prevRpm; } } internal BaseGearbox Gearbox { get { return _gearbox; } } public Motor(List powerCurve, float maxPower, float redline, BaseGearbox gearbox) { _powerCurve = powerCurve; _maxPower = maxPower; _redlineRpm = redline; _gearbox = gearbox; _gearbox.Motor = this; } public void Update(float carSpeed, GearboxAction action) { _prevRpm = _rpm; _lastCarSpeed = carSpeed; if (_rpm >= _redlineRpm) { _rpm = _redlineRpm; } if (_rpmLimiter > 0) { if (!WheelsSpinning) _currentPowerOutput = 0; //_throttle = 0; _rpmLimiter -= Engine.Instance.FrameTime; } else _currentPowerOutput = _maxPower * MathHelper.Lerp(_powerCurve[(int)_rpm], _powerCurve[(int)_rpm + 1], _rpm - (int)_rpm); if (_gearbox.GearEngaged) { if (_gearbox.CurrentGear == 0 || WheelsSpinning) { HandleRpmNoLoad(); } else { _rpm = carSpeed * _gearbox.CurrentRatio / DRIVETRAIN_MULTIPLIER; if (_rpm < 0.8f) _rpm = 0.8f; //idle speed } _prevEngagedRpm = _rpm; } else { _rpm = MathHelper.Lerp(_prevEngagedRpm /*Math.Abs(carSpeed) * _gearbox.CurrentRatio / DRIVETRAIN_MULTIPLIER*/, carSpeed * _gearbox.NextRatio / DRIVETRAIN_MULTIPLIER, _gearbox.Clutch); } if (_rpm < 0.8f) _rpm = 0.8f; if (_rpm >= _redlineRpm) { _rpmLimiter = 0.2f; _rpm = _redlineRpm; } _gearbox.Update(_rpm / _redlineRpm, action); } public float GetPowerAtRpmForGear(float rpm, int gear) { float power = _maxPower * MathHelper.Lerp(_powerCurve[(int)_redlineRpm], _powerCurve[(int)_redlineRpm + 1], _redlineRpm - (int)_redlineRpm); power *= _gearbox.Ratios[gear]; return power; } public void Idle() { _rpm = 0.8f; _gearbox.CurrentGear = 1; } public float GetRpmForGear(int gear) { return _lastCarSpeed * _gearbox.Ratios[gear] / DRIVETRAIN_MULTIPLIER; } private void HandleRpmNoLoad() { if (_throttle == 0.0f || _rpmLimiter > 0) { _rpm -= Engine.Instance.FrameTime * 4.4f; if (_rpm < 0.8f) _rpm = 0.8f; } else { _rpm += Engine.Instance.FrameTime *_throttle * 8f; } } public bool WheelsSpinning { get; set; } public bool CanChangeDown { get { return GetRpmForGear(_gearbox.CurrentGear) / RedlineRpm < 0.9f; } } } } ================================================ FILE: OpenNFS1/Physics/Spring.cs ================================================ using System; using System.Collections; using System.Text; namespace OpenNFS1.Physics { /// /// Simple 1D spring /// class Spring { private float _mass; private float _friction; private float _springConstant; private float _position = 0.0f; private float _velocity = 0.0f; private float _force = 0.0f; private float _maxValue; public float Position { get { return _position; } } /// /// Create simple 1D spring with specified values. /// public Spring(float mass, float friction, float springConstant, float initialPosition, float maxValue) { _mass = mass; _friction = friction; _springConstant = springConstant; _position = initialPosition; _force = 0; _velocity = 0; _maxValue = maxValue; } public void Simulate(float timeChange) { // Calculate force again _force += -_position * _springConstant; // Calculate velocity _velocity = _force / _mass; // And apply it to the current position _position += timeChange * _velocity; // Apply friction _force *= 1.0f - (timeChange * _friction); } public void ChangePosition(float change) { _position += change; if (_position < 0 && _position < -_maxValue) _position = -_maxValue; else if (_position > 0 && _position > _maxValue) _position = _maxValue; } } } ================================================ FILE: OpenNFS1/Physics/Vector3Helper.cs ================================================ // Project: RacingGame, File: Vector3Helper.cs // Namespace: RacingGame.Helpers, Class: Vector3Helper // Path: C:\code\RacingGame\Helpers, Author: Abi // Code lines: 62, Size of file: 2,14 KB // Creation date: 13.10.2006 23:14 // Last modified: 20.10.2006 08:29 // Generated with Commenter by abi.exDream.com using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Text; using System.IO; namespace OpenNFS1.Physics { /// /// Vector 3 helper /// class Vector3Helper { #region Constructor /// /// Private constructor to prevent instantiation. /// private Vector3Helper() { } // Vector3Helper() #endregion #region GetAngleBetweenVectors /// /// Return angle between two vectors. Used for visbility testing and /// for checking angles between vectors for the road sign generation. /// /// Vector 1 /// Vector 2 /// Float public static float GetAngleBetweenVectors(Vector3 vec1, Vector3 vec2) { // See http://en.wikipedia.org/wiki/Vector_(spatial) // for help and check out the Dot Product section ^^ // Both vectors are normalized so we can save deviding through the // lengths. return (float)Math.Acos(Vector3.Dot(vec1, vec2)); } // GetAngleBetweenVectors(vec1, vec2) #endregion #region DistanceToLine /// /// Distance from our point to the line described by linePos1 and linePos2. /// /// Point /// Line position 1 /// Line position 2 /// Float public static float DistanceToLine(Vector3 point, Vector3 linePos1, Vector3 linePos2) { // For help check out this article: // http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html Vector3 lineVec = linePos2 - linePos1; Vector3 pointVec = linePos1 - point; return Vector3.Cross(lineVec, pointVec).Length() / lineVec.Length(); } #endregion #region SignedDistanceToPlane /// /// Signed distance to plane /// /// Point /// Plane position /// Plane normal /// Float public static float SignedDistanceToPlane(Vector3 point, Vector3 planePosition, Vector3 planeNormal) { Vector3 pointVec = planePosition - point; return Vector3.Dot(Vector3.Normalize(planeNormal), pointVec); } #endregion } } ================================================ FILE: OpenNFS1/Physics/VehicleFenceCollision.cs ================================================ using System; using System.Diagnostics; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Audio; namespace OpenNFS1.Physics { class VehicleFenceCollision { public static void Handle(DrivableVehicle car) { var leftBound1 = car.CurrentNode.GetLeftBoundary(); var leftBound2 = car.CurrentNode.Next.GetLeftBoundary(); var rightBound1 = car.CurrentNode.GetRightBoundary(); var rightBound2 = car.CurrentNode.Next.GetRightBoundary(); for (int wheelNumber = 0; wheelNumber < 4; wheelNumber++) { VehicleWheel wheel = car.Wheels[wheelNumber]; Vector3 fenceNormal = Vector3.Zero; bool collision = false; float collisionAngle = 0; float direction = 0; if (Utility.IsLeftOfLine(leftBound2, leftBound1, wheel.WorldPosition)) { fenceNormal = Vector3.Cross(Vector3.Normalize(leftBound2 - leftBound1), car.CurrentNode.Up); collisionAngle = Vector3Helper.GetAngleBetweenVectors(car.Right, fenceNormal); collision = true; direction = -1; } else if (!Utility.IsLeftOfLine(rightBound2, rightBound1, wheel.WorldPosition)) { fenceNormal = Vector3.Cross(car.CurrentNode.Up, Vector3.Normalize(rightBound2 - rightBound1)); collisionAngle = Vector3Helper.GetAngleBetweenVectors(-car.Right, fenceNormal); collision = true; direction = 1; } if (!collision) continue; Vector3 collisionDir = Vector3.Reflect(car.Direction, fenceNormal); // Force car back on the road, for that calculate impulse and collision direction // Flip at 180 degrees (if driving in wrong direction) if (collisionAngle > MathHelper.Pi / 2) collisionAngle -= MathHelper.Pi; // Just correct rotation if we hit the fence at a shallow angle if (Math.Abs(collisionAngle) < MathHelper.ToRadians(45)) { SlideAlongFence(car, wheelNumber, collisionAngle, direction); } // If 90-45 degrees (in either direction), make frontal crash // + stop car + wobble camera else if (Math.Abs(collisionAngle) < MathHelper.Pi * 3.0f / 4.0f) { HandleHeadOnCrash(car, wheelNumber, collisionAngle); } // move away from the way slightly. We should be a bit smarter and along more of a slide along the wall.. car.Position += fenceNormal * 1.5f; break; } } public static int GetWheelsOutsideRoadVerge(DrivableVehicle car) { var leftVerge1 = car.CurrentNode.GetLeftVerge(); var leftVerge2 = car.CurrentNode.Next.GetLeftVerge(); var rightVerge1 = car.CurrentNode.GetRightVerge(); var rightVerge2 = car.CurrentNode.Next.GetRightVerge(); int wheelsOutsideVerge = 0; for (int wheelNumber = 0; wheelNumber < 4; wheelNumber++) { VehicleWheel wheel = car.Wheels[wheelNumber]; if (Utility.IsLeftOfLine(leftVerge2, leftVerge1, wheel.WorldPosition)) { wheelsOutsideVerge++; } else if (!Utility.IsLeftOfLine(rightVerge2, rightVerge1, wheel.WorldPosition)) { wheelsOutsideVerge++; } } return wheelsOutsideVerge; } private static void SlideAlongFence(DrivableVehicle car, int wheel, float collisionAngle, float direction) { if (car.AudioEnabled && Math.Abs(collisionAngle) > MathHelper.ToRadians(30)) { EnvironmentAudioProvider.Instance.PlayVehicleFenceCollision(); } // if we hit the front wheels, change the direction to almost match the fence direction. If we hit the back wheels this looks weird so // only rotate 50% of the way if (wheel < 2) { car.RotateCarAfterCollision = direction * collisionAngle * 1.1f; car.Speed *= 0.80f; } else { car.RotateCarAfterCollision = direction * collisionAngle *0.5f; car.Speed *= 0.9f; } } private static void HandleHeadOnCrash(DrivableVehicle car, int wheel, float collisionAngle) { if (car.AudioEnabled) { EnvironmentAudioProvider.Instance.PlayVehicleFenceCollision(); } // Also rotate car if less than 60 degrees if (Math.Abs(collisionAngle) < MathHelper.Pi / 3.0f) car.RotateCarAfterCollision = +collisionAngle / 3.0f; //bounce back a little car.Speed *= -0.15f; } } } ================================================ FILE: OpenNFS1/Physics/VehicleWheel.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using GameEngine; using Microsoft.Xna.Framework.Graphics; namespace OpenNFS1.Physics { class VehicleWheel { public const float Width = 1.5f; Vector3 _axlePoint; DrivableVehicle _car; float _steeringAngle; float _size; float _rotation; Texture2D _texture; ParticleEmitter _smokeEmitter; Vector3 _renderOffset; //we need to offset the wheel so that the front of the tire matches the original vertex position regardless of the tire width Matrix _wheelMatrix; public float Rotation { get { return _rotation; } set { _rotation = value; } } public bool IsSkidding { get; set; } public VehicleWheel(DrivableVehicle car, Vector3 axlePoint, float size, Texture2D texture, float renderXOffset) { _car = car; _axlePoint = axlePoint; _size = size; _texture = texture; _renderOffset = new Vector3(renderXOffset, 0, 0); _wheelMatrix = Matrix.CreateScale(new Vector3(_size, Width, _size)) * Matrix.CreateRotationZ(MathHelper.ToRadians(-90)); //cylinder geometry faces upwards _smokeEmitter = new ParticleEmitter(TyreSmokeParticleSystem.Instance, 20, WorldPosition); } public Vector3 WorldPosition { get { var m = Matrix.Identity; m.Right = Vector3.Cross(_car.RenderDirection, _car.Up); m.Up = _car.Up; m.Forward = _car.RenderDirection; return Vector3.Transform(_axlePoint, m * Matrix.CreateTranslation(_car.Position)); } } public Vector3 BottomPosition { get { var pos = WorldPosition; pos.Y -= Size / 2; return pos; } } public Vector3 GetOffsetPosition(Vector3 offset) { var m = Matrix.Identity; m.Right = Vector3.Cross(_car.RenderDirection, _car.Up); m.Up = _car.Up; m.Forward = _car.RenderDirection; var pos = _axlePoint + offset; return Vector3.Transform(pos, m * Matrix.CreateTranslation(_car.Position)); } public float Size { get { return _size; } } public void Steer(float angle) { _steeringAngle = -angle; } public void Update() { _smokeEmitter.Enabled = IsSkidding; _smokeEmitter.Update(BottomPosition); IsSkidding = false; } public void Render() { Matrix carOrientation = Matrix.Identity; carOrientation.Forward = _car.RenderDirection; carOrientation.Up = _car.Up; carOrientation.Right = Vector3.Cross(_car.RenderDirection, _car.Up); WheelModel.Render( _wheelMatrix * Matrix.CreateRotationX(_rotation / _size * 2f) * Matrix.CreateRotationY(_steeringAngle * 1.3f) * carOrientation * Matrix.CreateTranslation(GetOffsetPosition(_renderOffset)), _texture); } } } ================================================ FILE: OpenNFS1/PlayerUI.cs ================================================ using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using GameEngine; using OpenNFS1.Physics; using OpenNFS1.Tracks; using OpenNFS1.Vehicles; using OpenNFS1.Views; namespace OpenNFS1 { class PlayerUI { const int DebugViewIndex = 0; DrivableVehicle _vehicle; List _views = new List(); int _currentView = 1; public PlayerUI(DrivableVehicle vehicle) { _vehicle = vehicle; _views.Add(new DebugView(_vehicle)); _views.Add(new ChaseView(_vehicle, 32, 14, 0)); _views.Add(new DashboardView(_vehicle)); _views.Add(new BumperView(_vehicle)); _views.Add(new DropCameraView(_vehicle)); _views[_currentView].Activate(); } public bool ShouldRenderCar { get { return _views[_currentView].ShouldRenderPlayer; } } public TrackNode CurrentNode { get { return _vehicle.CurrentNode; } } public void Update(GameTime gameTime) { if (VehicleController.ChangeView) { _views[_currentView].Deactivate(); while (true) { _currentView++; _currentView %= _views.Count; if (_views[_currentView].Selectable) break; } _views[_currentView].Activate(); } if (Engine.Instance.Input.WasPressed(Keys.F1)) //toggle debug FPS view { if (_currentView == DebugViewIndex) _currentView = 1; else _currentView = DebugViewIndex; _views[_currentView].Activate(); } _views[_currentView].Update(gameTime); } public void Render() { _views[_currentView].Render(); } } } ================================================ FILE: OpenNFS1/Polygon.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace OpenNFS1.Parsers { enum PolygonShape { Triangle = 3, Quad = 4, } class Polygon { public PolygonShape Shape { get; set; } public string Label { get; set; } public string TextureName { get; set; } public Vector3[] Vertices {get ;set; } public Vector2[] TextureUVs {get; set;} public Texture2D Texture {get; set;} public int VertexCount { get { if (Shape == PolygonShape.Triangle) return 3; else return 6; } } public int VertexBufferIndex { get; set; } bool _computeUvs; public Polygon(PolygonShape type, bool computeUVs) { Shape = type; _computeUvs = computeUVs; Vertices = new Vector3[VertexCount]; TextureUVs = new Vector2[VertexCount]; if (_computeUvs) { TextureUVs[0] = new Vector2(0, 0); TextureUVs[1] = new Vector2(1, 0); TextureUVs[2] = new Vector2(1, 1); TextureUVs[3] = new Vector2(0, 0); TextureUVs[4] = new Vector2(1, 1); TextureUVs[5] = new Vector2(0, 1); } } public void ResolveTexture(BitmapEntry bmpEntry) { if (bmpEntry == null) { return; } Texture = bmpEntry.Texture; if (_computeUvs) //don't need to scale our uvs based on the texture size return; // otherwise, because the uvs are in the range 0,0,tex_width,tex_height we need to scale them to the 0,1 range for (int i =0; i < VertexCount; i++) { Vector2 coord = TextureUVs[i]; coord.X /= bmpEntry.Texture.Width; coord.Y /= bmpEntry.Texture.Height; TextureUVs[i] = coord; } } public VertexPositionTexture[] GetVertices() { VertexPositionTexture[] verts = new VertexPositionTexture[VertexCount]; for (int i = 0; i < VertexCount; i++ ) { verts[i] = new VertexPositionTexture(Vertices[i], TextureUVs[i]); } return verts; } } } ================================================ FILE: OpenNFS1/Program.cs ================================================ #region Using Statements using System; using System.Collections.Generic; using System.IO; using System.Linq; #endregion namespace OpenNFS1 { /// /// The main class. /// public static class Program { /// /// The main entry point for the application. /// [STAThread] static void Main() { AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; using (var game = new Game1()) game.Run(); } static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { File.WriteAllText("exception.txt", e.ExceptionObject.ToString()); } } } ================================================ FILE: OpenNFS1/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; 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("OpenNFS1")] [assembly: AssemblyProduct("OpenNFS1")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyDescription("")] [assembly: AssemblyCompany("")] [assembly: AssemblyCopyright("Copyright © 2013")] [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("169d9959-2864-49ca-acb8-d5adb00ff627")] // 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.2.0.0")] [assembly: AssemblyFileVersion("1.2.0.0")] ================================================ FILE: OpenNFS1/Race/PlayerRaceStats.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenNFS1 { class PlayerRaceStats { public int CurrentLap { get; private set; } DateTime _currentLapStartTime = DateTime.MinValue; public List LapTimes = new List(); public bool HasPassedLapHalfwayPoint; public int Position { get; set; } public void OnLapStarted() { if (_currentLapStartTime != DateTime.MinValue) { LapTimes.Add((int)new TimeSpan(DateTime.Now.Ticks - _currentLapStartTime.Ticks).TotalSeconds); } CurrentLap++; _currentLapStartTime = DateTime.Now; HasPassedLapHalfwayPoint = false; } public TimeSpan CurrentLapTime { get { return new TimeSpan(DateTime.Now.Ticks - _currentLapStartTime.Ticks); } } } } ================================================ FILE: OpenNFS1/Race/Race.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenNFS1.Physics; using GameEngine; using OpenNFS1.UI.Screens; using OpenNFS1.Parsers.Track; using OpenNFS1.Tracks; using OpenNFS1.Vehicles.AI; using OpenNFS1.Vehicles; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace OpenNFS1 { class Race { int _nbrLaps; DateTime _countdownStartTime, _raceStartTime; bool _started; public bool Finished { get; private set; } public PlayerDriver Player { get; private set; } TrafficController _trafficController; public List Drivers { get; private set; } public PlayerRaceStats PlayerStats { get; private set; } public Track Track { get; private set; } public Race(int nbrLaps, Track track, PlayerDriver player) { _nbrLaps = nbrLaps; Player = player; Track = track; PlayerStats = new PlayerRaceStats(); Drivers = new List(); AddDriver(player); if (track.Description.IsOpenRoad) { _trafficController = new TrafficController(this); } } public int NbrLaps { get { return _nbrLaps; } } public TimeSpan RaceTime { get { return new TimeSpan(DateTime.Now.Ticks - _raceStartTime.Ticks); } } public int SecondsTillStart { get { return 3 - (int)new TimeSpan(DateTime.Now.Ticks - _countdownStartTime.Ticks).TotalSeconds; } } public void StartCountdown() { VehicleController.ForceBrake = false; _countdownStartTime = DateTime.Now; } public void AddDriver(IDriver d) { AddDriver(d, Track.RoadNodes[1]); } // add a driver to the race, placing him in a starting grid public void AddDriver(IDriver d, TrackNode startNode) { d.Vehicle.PlaceOnTrack(Track, startNode); if (d is TrafficDriver) { } else if (d is RacingAIDriver || d is PlayerDriver) { // place on starting grid int lane = (Drivers.Count % 2 == 0 ? AIDriver.MaxVirtualLanes - 1 : 1); if (d is RacingAIDriver) { ((RacingAIDriver)d).VirtualLane = lane; } Vector3 pos = d.Vehicle.CurrentNode.Position; pos.Z -= Drivers.Count * 30; pos.X = Vector3.Lerp(d.Vehicle.CurrentNode.GetLeftVerge2(), d.Vehicle.CurrentNode.GetRightVerge2(), (float)lane / (AIDriver.MaxVirtualLanes)).X; d.Vehicle.Position = pos; } Drivers.Add(d); } public void Update() { foreach (var driver in Drivers) driver.Update(Drivers); Track.Update(); /* so you cant reverse over the line and get 0.1sec laps */ var node = Player.Vehicle.CurrentNode; if (node.Number == Track.CheckpointNode && PlayerStats.HasPassedLapHalfwayPoint) { PlayerStats.OnLapStarted(); } if (node.Number == 100) //just pick some arbitrary node thats farish away from the start { PlayerStats.HasPassedLapHalfwayPoint = true; } var racingDrivers = new List(Drivers.Where(a => a is RacingAIDriver || a is PlayerDriver)); racingDrivers.Sort((a1, a2) => a2.Vehicle.TrackPosition.CompareTo(a1.Vehicle.TrackPosition)); PlayerStats.Position = racingDrivers.FindIndex(a => a == Player); if (SecondsTillStart <= 0 && !_started) { foreach (var d in racingDrivers) { ((DrivableVehicle)d.Vehicle).Motor.Gearbox.CurrentGear = 1; } _raceStartTime = DateTime.Now; PlayerStats.OnLapStarted(); _started = true; } if (PlayerStats.CurrentLap > _nbrLaps) { if (Player.Vehicle.Speed > 0) { VehicleController.ForceBrake = true; } else Finished = true; } if (_trafficController != null) _trafficController.Update(); } public void Render(bool renderPlayerVehicle) { Engine.Instance.Device.BlendState = BlendState.Opaque; Engine.Instance.Device.DepthStencilState = DepthStencilState.Default; Engine.Instance.Device.SamplerStates[0] = GameConfig.WrapSampler; Track.Render(Engine.Instance.Camera.Position, Player.Vehicle.CurrentNode); var frustum = new BoundingFrustum(Engine.Instance.Camera.View * Engine.Instance.Camera.Projection); foreach (var driver in Drivers) { bool isPlayer = driver == Player; if (isPlayer && !renderPlayerVehicle) continue; if (!frustum.Intersects(driver.Vehicle.BoundingSphere)) continue; if (driver.Vehicle is DrivableVehicle) { ((DrivableVehicle)driver.Vehicle).RenderShadow(isPlayer); } if (driver is AIDriver && ((AIDriver)driver).AtEndOfTrack) continue; driver.Vehicle.Render(); } } } } ================================================ FILE: OpenNFS1/Race/RaceUI.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using GameEngine; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Vehicles.AI; using OpenNFS1.Parsers; using OpenNFS1.Physics; namespace OpenNFS1.UI { class RaceUI { Race _race; Rectangle _backgroundRectangle; Texture2D _backgroundTexture; SpriteFont _font; public RaceUI(Race race) { _race = race; int height = Engine.Instance.Device.Viewport.Height; int width = Engine.Instance.Device.Viewport.Width; _backgroundRectangle = new Rectangle(0, 0, width, 30); Color[] pixel = new Color[1] { Color.Black }; _backgroundTexture = new Texture2D(Engine.Instance.Device, 1, 1); _backgroundTexture.SetData(pixel); _font = Engine.Instance.ContentManager.Load("Content\\ArialBlack-Italic"); } public void Render() { Engine.Instance.SpriteBatch.Begin(); int secondsTillStart = _race.SecondsTillStart; if (secondsTillStart > 0) { Engine.Instance.SpriteBatch.DrawString(_font, _race.SecondsTillStart.ToString(), new Vector2(300, 50), Color.Yellow, 0, Vector2.Zero, 1.5f, SpriteEffects.None, 0); } else if (secondsTillStart == 0) { Engine.Instance.SpriteBatch.DrawString(_font, "Go!", new Vector2(300, 50), Color.Yellow, 0, Vector2.Zero, 1.5f, SpriteEffects.None, 0); } Engine.Instance.SpriteBatch.Draw(_backgroundTexture, _backgroundRectangle, Color.White); string msg = String.Format("{0}:{1}", _race.PlayerStats.CurrentLapTime.Minutes.ToString("00"), _race.PlayerStats.CurrentLapTime.Seconds.ToString("00")); Engine.Instance.SpriteBatch.DrawString(_font, msg, new Vector2(15, 0), Color.GreenYellow, 0, Vector2.Zero, 0.6f, SpriteEffects.None, 0); msg = String.Format("{0} kph", Math.Abs((int)_race.Player.Vehicle.Speed)); Engine.Instance.SpriteBatch.DrawString(_font, msg, new Vector2(270, -5), Color.WhiteSmoke, 0, Vector2.Zero, 0.8f, SpriteEffects.None, 0); Engine.Instance.SpriteBatch.DrawString(_font, msg, new Vector2(271, -4), Color.Red, 0, Vector2.Zero, 0.8f, SpriteEffects.None, 0); msg = String.Format("G:{0}", GearToString(((DrivableVehicle)_race.Player.Vehicle).Motor.Gearbox.CurrentGear)); Engine.Instance.SpriteBatch.DrawString(_font, msg, new Vector2(410, 0), Color.GreenYellow, 0, Vector2.Zero, 0.6f, SpriteEffects.None, 0); if (!GameConfig.SelectedTrackDescription.IsOpenRoad) { msg = String.Format("L:{0}/{1}", Math.Min(_race.PlayerStats.CurrentLap, _race.NbrLaps), _race.NbrLaps); Engine.Instance.SpriteBatch.DrawString(_font, msg, new Vector2(480, 0), Color.GreenYellow, 0, Vector2.Zero, 0.6f, SpriteEffects.None, 0); } msg = String.Format("P:{0}/{1}", _race.PlayerStats.Position + 1, _race.Drivers.Count(a => a is RacingAIDriver || a is PlayerDriver)); Engine.Instance.SpriteBatch.DrawString(_font, msg, new Vector2(550, 0), Color.GreenYellow, 0, Vector2.Zero, 0.6f, SpriteEffects.None, 0); Engine.Instance.SpriteBatch.End(); } private string GearToString(int gear) { if (gear == -1) return "R"; else if (gear == 0) return "N"; else return gear.ToString(); } } } ================================================ FILE: OpenNFS1/Tracks/SceneryObject.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using OpenNFS1.Loaders; using GameEngine; using OpenNFS1.Parsers; using OpenNFS1; namespace OpenNFS1.Parsers.Track { abstract class SceneryItem { public int SegmentRef; public Vector3 Position; public float Orientation; public Vector2 Size; public abstract void Initialize(); public virtual void Update() { } public abstract void Render(AlphaTestEffect effect); } class BillboardSceneryItem : SceneryItem { protected Matrix _matrix; protected Texture2D _texture; public BillboardSceneryItem() { } public BillboardSceneryItem(Texture2D texture) { _texture = texture; } public override void Initialize() { if (Size.X == 0) { float aspect = (float)_texture.Width / _texture.Height; Size.X = Size.Y * aspect * GameConfig.TerrainScale * 10000; } _matrix = Matrix.CreateScale(Size.X, Size.Y, 1) * Matrix.CreateRotationY(Orientation) * Matrix.CreateTranslation(Position); } public override void Render(AlphaTestEffect effect) { effect.World = _matrix; effect.Texture = _texture; effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2); } } class AnimatedBillboardSceneryItem : BillboardSceneryItem { List _textures; int _currentTexture; double _textureChangeTime; public AnimatedBillboardSceneryItem(List textures) { _textures = textures; _textureChangeTime = Engine.Instance.Random.NextDouble(); } public override void Initialize() { if (Size.X == 0) Size.X = _textures[0].Width * GameConfig.TerrainScale * 10000; if (Size.Y == 0) Size.Y = _textures[0].Height * GameConfig.TerrainScale * 10000; _matrix = Matrix.CreateScale(Size.X, Size.Y, 1) * Matrix.CreateRotationY(Orientation) * Matrix.CreateTranslation(Position); } public override void Update() { _textureChangeTime -= Engine.Instance.FrameTime; if (_textureChangeTime < 0.0f) { _currentTexture++; _currentTexture %= _textures.Count; _texture = _textures[_currentTexture]; _textureChangeTime = 0.2f; } } } class TwoSidedBillboardSceneryItem : SceneryItem { Texture2D _texture1, _texture2; Matrix _matrix1, _matrix2; public TwoSidedBillboardSceneryItem(Texture2D texture1, Texture2D texture2) { _texture1 = texture1; _texture2 = texture2; } public override void Initialize() { _matrix1 = Matrix.CreateScale(Size.X, Size.Y, 1) * Matrix.CreateRotationY(Orientation) * Matrix.CreateTranslation(Position); _matrix2 = Matrix.CreateScale(Size.X, Size.Y, 1) * Matrix.CreateTranslation(Size.X / 2, 0, Size.X / 2) * Matrix.CreateRotationY(Orientation + MathHelper.ToRadians(90)) * Matrix.CreateTranslation(Position); } public override void Render(AlphaTestEffect effect) { //side 1 effect.World = _matrix1; effect.Texture = _texture1; effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2); //side 2 effect.World = _matrix2; effect.Texture = _texture2; effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2); } } class ModelSceneryItem : SceneryItem { Mesh _mesh; Matrix _matrix; public ModelSceneryItem(Mesh mesh) { _mesh = mesh; } public override void Initialize() { _matrix = Matrix.CreateScale(7.5f) * Matrix.CreateRotationY(Orientation) * Matrix.CreateTranslation(Position); } public override void Render(AlphaTestEffect effect) { effect.World = _matrix; _mesh.Render(effect); } } } ================================================ FILE: OpenNFS1/Tracks/TerrainRow.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using OpenNFS1.Parsers; namespace OpenNFS1.Tracks { // A terrain row maps to a TrackNode. TerrainRow defines the vertex positions for that bit of track, the TrackNode defines the physical properties // that the vehicles follow (slope, slant, width...) class TerrainRow { const int TerrainPositionScale = 500; public Vector3 MiddlePoint; public Vector3[] LeftPoints = new Vector3[TriFile.NbrTerrainPointsPerSide]; public Vector3[] RightPoints = new Vector3[TriFile.NbrTerrainPointsPerSide]; public Vector3 GetPoint(int pointIndex) { if (pointIndex >= TriFile.NbrTerrainPointsPerSide) return LeftPoints[(pointIndex - TriFile.NbrTerrainPointsPerSide)]; else return RightPoints[pointIndex]; } public void RelativizeTo(Vector3 position) { MiddlePoint = position + MiddlePoint * TerrainPositionScale; for (int i = 0; i < LeftPoints.Length; i++) { LeftPoints[i] = position + LeftPoints[i] * TerrainPositionScale; } for (int i = 0; i < RightPoints.Length; i++) { RightPoints[i] = position + RightPoints[i] * TerrainPositionScale; } } } } ================================================ FILE: OpenNFS1/Tracks/TerrainSegment.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Parsers; using OpenNFS1.Parsers.Track; namespace OpenNFS1.Tracks { // A terrainSegment holds a group of 4 TerrainRow's and defines properties common to those rows (fence, textures etc) class TerrainSegment { public int Number; public Texture2D[] Textures = new Texture2D[10]; public bool HasLeftFence, HasRightFence; public int FenceTextureId; public Texture2D FenceTexture; public byte[] TextureIds; public TerrainRow[] Rows = new TerrainRow[4]; public int FenceBufferIndex; public int TerrainBufferIndex; public TerrainSegment Next, Prev; // used for frustum culling calculation public BoundingBox BoundingBox; } } ================================================ FILE: OpenNFS1/Tracks/Track.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Xna.Framework; using System.Diagnostics; using GameEngine; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Loaders; using OpenNFS1.Tracks; using Microsoft.Xna.Framework.Input; using OpenNFS1.Vehicles; using OpenNFS1.Vehicles.AI; using OpenNFS1.Physics; namespace OpenNFS1.Parsers.Track { class Track { AlphaTestEffect _effect; TrackSkyBox _skybox; BasicEffect _physicalRoadEffect; public TrackDescription Description { get; set; } public List SceneryItems { get; set; } public List RoadNodes { get; set; } public List TerrainSegments { get; set; } public VertexBuffer TerrainVertexBuffer { get; set; } public VertexBuffer FenceVertexBuffer { get; set; } public TrackfamFile TrackFam { get; set; } public int CheckpointNode { get; set; } public bool IsOpenRoad { get; set; } public Track() { _effect = new AlphaTestEffect(Engine.Instance.Device); _effect.ReferenceAlpha = 100; _effect.AlphaFunction = CompareFunction.Greater; _effect.VertexColorEnabled = false; _effect.FogEnabled = false; _physicalRoadEffect = new BasicEffect(Engine.Instance.Device); } public void Initialize() { _skybox = new TrackSkyBox(TrackFam.HorizonTexture); } public void Update() { _skybox.Update(); foreach (var item in SceneryItems) { item.Update(); } } public void Render(Vector3 cameraPosition, TrackNode currentNode) { _skybox.Render(); _effect.View = Engine.Instance.Camera.View; _effect.Projection = Engine.Instance.Camera.Projection; _effect.World = Matrix.Identity; int segmentIndex = currentNode.Number / 4; var startSegment = TerrainSegments[segmentIndex]; var renderedSegments = new List(); Engine.Instance.Device.SetVertexBuffer(TerrainVertexBuffer); _effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.RasterizerState = RasterizerState.CullNone; Engine.Instance.Device.SamplerStates[0] = GameConfig.WrapSampler; var frustum = new BoundingFrustum(Engine.Instance.Camera.View * Engine.Instance.Camera.Projection); // draw segments from the player vehicle forwards. Stop when a segment is out of view var segment = startSegment; for (int i = 0; i < GameConfig.MaxSegmentRenderCount; i++) { if (segment == null) break; if (frustum.Intersects(segment.BoundingBox)) { RenderSegment(segment); renderedSegments.Add(segment); } else { break; } segment = segment.Next; } // draw segments from the player vehicle backwards. Stop when a segment is out of view segment = startSegment.Prev; for (int i = 0; i < GameConfig.MaxSegmentRenderCount; i++) { if (segment == null) break; if (frustum.Intersects(segment.BoundingBox)) { RenderSegment(segment); renderedSegments.Add(segment); } segment = segment.Prev; } DrawScenery(renderedSegments); if (FenceVertexBuffer != null) { Engine.Instance.Device.SetVertexBuffer(FenceVertexBuffer); _effect.World = Matrix.Identity; _effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.SamplerStates[0] = GameConfig.WrapSampler; foreach (var renderedSegment in renderedSegments) { DrawFenceStrips(renderedSegment); } } if (GameConfig.DrawDebugInfo) { var node = currentNode; for (int i = 0; i < GameConfig.MaxSegmentRenderCount; i++) { Engine.Instance.GraphicsUtils.AddCube(Matrix.CreateTranslation(node.GetLeftBoundary()), Color.Red); Engine.Instance.GraphicsUtils.AddCube(Matrix.CreateTranslation(node.GetRightBoundary()), Color.Red); Engine.Instance.GraphicsUtils.AddCube(Matrix.CreateTranslation(node.GetLeftVerge()), Color.Blue); Engine.Instance.GraphicsUtils.AddCube(Matrix.CreateTranslation(node.GetRightVerge()), Color.Blue); Engine.Instance.GraphicsUtils.AddCube(Matrix.CreateTranslation(node.Position), Color.Yellow); if (node.Number % TriFile.NbrRowsPerSegment == 0) { Engine.Instance.GraphicsUtils.AddLine(node.GetLeftBoundary(), node.GetRightBoundary(), Color.White); } node = node.Next; if (node == null) break; } GameConsole.WriteLine(String.Format("Position node: {0}, segment: {1}", currentNode.Number, (int)(currentNode.Number / TriFile.NbrRowsPerSegment))); GameConsole.WriteLine(String.Format("Node property: {0}, flags: {1}, {2}, {3}", currentNode.NodeProperty, currentNode.Flag1, currentNode.Flag2, currentNode.Flag3)); } } private void RenderSegment(TerrainSegment segment) { int vertexIndex = segment.TerrainBufferIndex; DrawTerrainStrip(ref vertexIndex, 0, segment.Textures[0]); DrawTerrainStrip(ref vertexIndex, 1, segment.Textures[1]); DrawTerrainStrip(ref vertexIndex, 2, segment.Textures[2]); DrawTerrainStrip(ref vertexIndex, 3, segment.Textures[3]); DrawTerrainStrip(ref vertexIndex, 4, segment.Textures[4]); DrawTerrainStrip(ref vertexIndex, 5, segment.Textures[5]); DrawTerrainStrip(ref vertexIndex, 6, segment.Textures[6]); DrawTerrainStrip(ref vertexIndex, 7, segment.Textures[7]); DrawTerrainStrip(ref vertexIndex, 8, segment.Textures[8]); DrawTerrainStrip(ref vertexIndex, 9, segment.Textures[9]); } private void DrawTerrainStrip(ref int vertexIndex, int stripNumber, Texture2D texture) { if (texture != null) { Engine.Instance.Device.Textures[0] = texture; Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, vertexIndex, TrackAssembler.NbrTrianglesPerTerrainStrip); } vertexIndex += TrackAssembler.NbrVerticesPerTerrainStrip; } private void DrawFenceStrips(TerrainSegment segment) { int offset = segment.FenceBufferIndex; if (segment.HasLeftFence) { Engine.Instance.Device.Textures[0] = segment.FenceTexture; Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, offset, 4); offset += 6; } if (segment.HasRightFence) { Engine.Instance.Device.Textures[0] = segment.FenceTexture; Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, offset, 4); } } private void DrawScenery(List renderedSegments) { Engine.Instance.Device.RasterizerState = RasterizerState.CullNone; Engine.Instance.Device.SamplerStates[0] = SamplerState.PointClamp; if (GameConfig.Render2dScenery) { TrackBillboardModel.BeginBatch(); foreach (SceneryItem scenery in SceneryItems) { if (!renderedSegments.Exists(a=> a.Number == scenery.SegmentRef)) continue; if (scenery is BillboardSceneryItem || scenery is TwoSidedBillboardSceneryItem) { scenery.Render(_effect); } } } if (GameConfig.Render3dScenery) { foreach (SceneryItem model in SceneryItems) { if (model is ModelSceneryItem) { if (!renderedSegments.Exists(a => a.Number == model.SegmentRef)) continue; model.Render(_effect); } } } } public float GetHeightAtPoint(TrackNode node, Vector3 point) { Vector3 a = node.GetLeftBoundary(); Vector3 b = node.GetRightBoundary(); Vector3 c = node.Next.GetRightBoundary(); Vector3 d = node.Next.GetLeftBoundary(); Vector3 hitLoc; float t; Vector3 pos = point; pos.Y += 100; if (Utility.FindRayTriangleIntersection(ref pos, Vector3.Down, 1000f, ref a, ref b, ref c, out hitLoc, out t)) { return hitLoc.Y; } else if (Utility.FindRayTriangleIntersection(ref pos, Vector3.Down, 1000f, ref a, ref c, ref d, out hitLoc, out t)) { return hitLoc.Y; } else { return -9999; } } public void Dispose() { _effect.Dispose(); TerrainVertexBuffer.Dispose(); if (FenceVertexBuffer != null) FenceVertexBuffer.Dispose(); TrackFam.Dispose(); } } } ================================================ FILE: OpenNFS1/Tracks/TrackAssembler.cs ================================================ using System; using System.Collections.Generic; using System.Text; using OpenNFS1.Parsers.Track; using System.IO; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using System.Diagnostics; using GameEngine; using OpenNFS1.Parsers; using OpenNFS1.Tracks; using System.Linq; namespace OpenNFS1.Loaders { class TrackAssembler { //public static readonly int TRIANGLES_PER_ROW = 12; public static readonly int TRIANGLES_PER_SEGMENT = 2; //48; const int OPEN_ROAD_CHECKPOINT_SCENERYITEM_ID = 124; public const int NbrTrianglesPerTerrainStrip = 8; public const int NbrVerticesPerTerrainStrip = 10; public const int NbrVerticesPerSegment = 10 * NbrVerticesPerTerrainStrip; public const int NbrNodesPerSegment = 4; private TrackfamFile _trackFam; private TriFile _tri; public string ProgressMessage; private void AddProgress(string progress) { ProgressMessage += "\r\n" + progress; } public Track Assemble(TriFile tri) { _tri = tri; AddProgress("Reading track scenery file"); _trackFam = tri.IsOpenRoad ? new OpenRoadTrackfamFile(tri.FileName, GameConfig.AlternativeTimeOfDay) : new TrackfamFile(tri.FileName, GameConfig.AlternativeTimeOfDay); AssembleTrackSegments(); Track track = new Track(); track.RoadNodes = _tri.Nodes; track.SceneryItems = AssembleSceneryItems(); track.TerrainSegments = _tri.Segments; track.TerrainVertexBuffer = AssembleTerrainVertices(); track.FenceVertexBuffer = AssembleFenceVertices(); track.TrackFam = _trackFam; track.IsOpenRoad = _tri.IsOpenRoad; // Find checkpoint scenery object and use it to mark the end of the track. // Or ignore it and mark the end of the track as the last node to allow us to drive past the checkpoint! if (tri.IsOpenRoad && GameConfig.RespectOpenRoadCheckpoints) { foreach (var obj in _tri.Scenery) { if (obj.Descriptor.ResourceId == OPEN_ROAD_CHECKPOINT_SCENERYITEM_ID) { track.CheckpointNode = obj.ReferenceNode; break; } } } else { track.CheckpointNode = _tri.Nodes.Count - 2; } track.Initialize(); return track; } private List AssembleSceneryItems() { AddProgress("Assembling scenery items"); List renderObjects = new List(); foreach (var obj in _tri.Scenery) { SceneryItem renderObject; int bitmapId; switch (obj.Descriptor.Type) { case SceneryType.Bitmap: if ((obj.Descriptor.Flags & SceneryFlags.Animated) == SceneryFlags.Animated) { // Successive bitmaps are referenced by incrementing the scenery descriptor List textures = new List(); for (int i = 0; i < obj.Descriptor.AnimationFrameCount; i++) { var nextBitmap = _tri.ObjectDescriptors[obj.Descriptor.Id + i]; var texture = _trackFam.GetSceneryTexture(nextBitmap.ResourceId); textures.Add(texture); } renderObject = new AnimatedBillboardSceneryItem(textures); } else { bitmapId = obj.Descriptor.ResourceId; renderObject = new BillboardSceneryItem(_trackFam.GetSceneryTexture(bitmapId)); } break; case SceneryType.TwoSidedBitmap: bitmapId = obj.Descriptor.ResourceId; int bitmap2Id = obj.Descriptor.Resource2Id; renderObject = new TwoSidedBillboardSceneryItem(_trackFam.GetSceneryTexture(bitmapId), _trackFam.GetSceneryTexture(bitmap2Id)); break; case SceneryType.Model: int modelId = obj.Descriptor.ResourceId; renderObject = new ModelSceneryItem(_trackFam.GetMesh(modelId)); break; default: throw new NotImplementedException(); } renderObject.Size = new Vector2(obj.Descriptor.Width, obj.Descriptor.Height); renderObject.SegmentRef = obj.ReferenceNode / 4; renderObject.Position = _tri.Nodes[obj.ReferenceNode].Position + ((obj.RelativePosition * GameConfig.TerrainScale) * 240); renderObject.Orientation = MathHelper.ToRadians((_tri.Nodes[obj.ReferenceNode].Orientation) - ((obj.Orientation / 256) * 360)); renderObject.Initialize(); renderObjects.Add(renderObject); } return renderObjects; } private VertexBuffer AssembleTerrainVertices() { AddProgress("Assembling terrain vertices"); List vertices = new List(); for (int segmentIndex = 0; segmentIndex < _tri.Segments.Count; segmentIndex++) { var segment = _tri.Segments[segmentIndex]; segment.TerrainBufferIndex = vertices.Count; //Ground textures are rotated 90 degrees, so we swap u,v coordinates around float tU = 0.0f; //Right side of road for (int j = 1; j < TriFile.NbrTerrainPointsPerSide; j++) { tU = 0.0f; int index1, index2; GetPointIndicesForRightSide(segment, j, out index1, out index2); for (int row = 0; row < TriFile.NbrRowsPerSegment; row++) { vertices.Add(new VertexPositionTexture(segment.Rows[row].GetPoint(index1), new Vector2(tU, 0.0f))); vertices.Add(new VertexPositionTexture(segment.Rows[row].GetPoint(index2), new Vector2(tU, 1.0f))); tU += 0.5f; } //attach the end of this segment to the start of the next if its not the last if (segment.Next != null) { vertices.Add(new VertexPositionTexture(segment.Next.Rows[0].GetPoint(index1), new Vector2(tU, 0.0f))); vertices.Add(new VertexPositionTexture(segment.Next.Rows[0].GetPoint(index2), new Vector2(tU, 1.0f))); } else { vertices.Add(new VertexPositionTexture(segment.Rows[TriFile.NbrRowsPerSegment - 1].GetPoint(index1), new Vector2(tU, 0.0f))); vertices.Add(new VertexPositionTexture(segment.Rows[TriFile.NbrRowsPerSegment - 1].GetPoint(index2), new Vector2(tU, 1.0f))); } } //Left side of road for (int j = 1; j < TriFile.NbrTerrainPointsPerSide; j++) { tU = 0.0f; int index1, index2; GetPointIndicesForLeftSide(segment, j, out index1, out index2); for (int row = 0; row < TriFile.NbrRowsPerSegment; row++) { vertices.Add(new VertexPositionTexture(segment.Rows[row].GetPoint(index1), new Vector2(tU, 1.0f))); vertices.Add(new VertexPositionTexture(segment.Rows[row].GetPoint(index2), new Vector2(tU, 0.0f))); tU += 0.5f; } //attach the end of this segment to the start of the next if its not the last if (segment.Next != null) { vertices.Add(new VertexPositionTexture(segment.Next.Rows[0].GetPoint(index1), new Vector2(tU, 1.0f))); vertices.Add(new VertexPositionTexture(segment.Next.Rows[0].GetPoint(index2), new Vector2(tU, 0.0f))); } else { vertices.Add(new VertexPositionTexture(segment.Rows[TriFile.NbrRowsPerSegment - 1].GetPoint(index1), new Vector2(tU, 1.0f))); vertices.Add(new VertexPositionTexture(segment.Rows[TriFile.NbrRowsPerSegment - 1].GetPoint(index2), new Vector2(tU, 0.0f))); } } } var vertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), vertices.Count, BufferUsage.WriteOnly); vertexBuffer.SetData(vertices.ToArray()); return vertexBuffer; } void GetPointIndicesForRightSide(TerrainSegment segment, int terrainPointIndex, out int index1, out int index2) { // only care if this is the last terrain strip if (terrainPointIndex == TriFile.NbrTerrainPointsPerSide - 1) { var node = _tri.Nodes[segment.Number * TriFile.NbrRowsPerSegment]; if (node.NodeProperty == TrackNodeProperty.RIGHT_TUNNEL_A2_A9) { index1 = 2; index2 = 10; return; } } index1 = terrainPointIndex; index2 = terrainPointIndex - 1; } void GetPointIndicesForLeftSide(TerrainSegment segment, int terrainPointIndex, out int index1, out int index2) { // only care if this is the last terrain strip if (terrainPointIndex == TriFile.NbrTerrainPointsPerSide - 1) { var node = _tri.Nodes[segment.Number * TriFile.NbrRowsPerSegment]; if (node.NodeProperty == TrackNodeProperty.LEFT_TUNNEL_A9_A4) { index1 = 9; index2 = 4; return; } if (node.NodeProperty == TrackNodeProperty.LEFT_TUNNEL_A9_A5) { index1 = 9; index2 = 5; return; } } index1 = TriFile.NbrTerrainPointsPerSide + terrainPointIndex - 1; index2 = TriFile.NbrTerrainPointsPerSide + terrainPointIndex; } // Fences are defined by the TerrainSegment. If fence is enabled, we draw a fence from row0 to row2, then row2 to row4. VertexBuffer AssembleFenceVertices() { AddProgress("Assembling fence vertices"); Vector3 fenceHeight = new Vector3(0, GameConfig.TerrainScale * 50000, 0); List vertices = new List(); for (int i = 0; i < _tri.Segments.Count; i++) { var segment = _tri.Segments[i]; if (!(segment.HasLeftFence | segment.HasRightFence)) continue; segment.FenceBufferIndex = vertices.Count; segment.FenceTexture = _trackFam.GetFenceTexture(segment.FenceTextureId); var node0 = _tri.Nodes[i * NbrNodesPerSegment]; var node2 = node0.Next.Next; var node4 = node2.Next.Next; if (segment.HasLeftFence) { vertices.Add(new VertexPositionTexture(node0.GetLeftBoundary(), new Vector2(0, 1))); vertices.Add(new VertexPositionTexture(node0.GetLeftBoundary() + fenceHeight, new Vector2(0, 0))); vertices.Add(new VertexPositionTexture(node2.GetLeftBoundary(), new Vector2(1.5f, 1))); vertices.Add(new VertexPositionTexture(node2.GetLeftBoundary() + fenceHeight, new Vector2(1.5f, 0))); vertices.Add(new VertexPositionTexture(node4.GetLeftBoundary(), new Vector2(3, 1))); vertices.Add(new VertexPositionTexture(node4.GetLeftBoundary() + fenceHeight, new Vector2(3, 0))); } if (segment.HasRightFence) { vertices.Add(new VertexPositionTexture(node0.GetRightBoundary(), new Vector2(0, 1))); vertices.Add(new VertexPositionTexture(node0.GetRightBoundary() + fenceHeight, new Vector2(0, 0))); vertices.Add(new VertexPositionTexture(node2.GetRightBoundary(), new Vector2(1.5f, 1))); vertices.Add(new VertexPositionTexture(node2.GetRightBoundary() + fenceHeight, new Vector2(1.5f, 0))); vertices.Add(new VertexPositionTexture(node4.GetRightBoundary(), new Vector2(3, 1))); vertices.Add(new VertexPositionTexture(node4.GetRightBoundary() + fenceHeight, new Vector2(3, 0))); } } if (vertices.Count == 0) return null; var vertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), vertices.Count, BufferUsage.WriteOnly); vertexBuffer.SetData(vertices.ToArray()); return vertexBuffer; } void AssembleTrackSegments() { AddProgress("Assembling track segments"); const int textureCount = 10; foreach (var segment in _tri.Segments) { segment.Textures = new Texture2D[textureCount]; for (int i = 0; i < textureCount; i++) { segment.Textures[i] = _trackFam.GetGroundTexture(segment.TextureIds[i]); } // calculate bounding size for this segment var firstRow = segment.Rows[0]; var lastRow = segment.Next != null ? segment.Next.Rows[0] : segment.Rows[TriFile.NbrRowsPerSegment - 1]; Vector3 min = new Vector3(float.MaxValue), max = new Vector3(); List allPoints = new List(); allPoints.AddRange(firstRow.LeftPoints); allPoints.AddRange(firstRow.RightPoints); allPoints.AddRange(lastRow.LeftPoints); allPoints.AddRange(lastRow.RightPoints); foreach (var pos in allPoints) { if (pos.X < min.X) min.X = pos.X; if (pos.X > max.X) max.X = pos.X; if (pos.Y < min.Y) min.Y = pos.Y; if (pos.Y > max.Y) max.Y = pos.Y; if (pos.Z < min.Z) min.Z = pos.Z; if (pos.Z > max.Z) max.Z = pos.Z; } segment.BoundingBox = new BoundingBox(min, max); } } public List GeneratePhysicalVertices(List nodes) { List verts = new List(); List roadVerts = new List(); TrackNode nextNode; //Vector3 prevLeft = GetRoadOffsetPosition(nodes[nodes.Count - 1], -nodes[nodes.Count - 1].DistanceToLeftBarrier); // nodes[0].Position + Utility.RotatePoint(new Vector2(-nodes[0].DistanceToLeftBarrier, 0), nodes[0].Orientation); //Vector3 prevRight = GetRoadOffsetPosition(nodes[nodes.Count - 1], nodes[nodes.Count - 1].DistanceToRightBarrier); // nodes[0].Position + Utility.RotatePoint(new Vector2(nodes[0].DistanceToRightBarrier, 0), nodes[0].Orientation); for (int i = 0; i < nodes.Count; i++) { TrackNode node = nodes[i]; if (i + 1 < nodes.Count) nextNode = nodes[i + 1]; else nextNode = nodes[0]; //join up to start line float zPos = node.Position.Z - nextNode.Position.Z; Vector3 prevLeft = GetRoadOffsetPosition(node, -node.DistanceToLeftBarrier); Vector3 prevRight = GetRoadOffsetPosition(node, node.DistanceToRightBarrier); Vector3 currentLeft = nextNode.Position + Utility.RotatePoint(new Vector2(-node.DistanceToLeftBarrier, 0), -node.Orientation); Vector3 currentRight = nextNode.Position + Utility.RotatePoint(new Vector2(node.DistanceToRightBarrier, 0), -node.Orientation); var t = new Triangle(nextNode.GetLeftBoundary(), node.GetLeftBoundary(), node.GetRightBoundary()); var t2 = new Triangle(nextNode.GetLeftBoundary(), node.GetRightBoundary(), nextNode.GetRightBoundary()); roadVerts.Add(t); roadVerts.Add(t2); var normal = Vector3.Normalize(Vector3.Cross(t.V2 - t.V1, t.V3 - t.V1)); var normal2 = Vector3.Normalize(Vector3.Cross(t2.V2 - t2.V1, t2.V3 - t2.V1)); if (Math.Round(normal.X, 2) != Math.Round(normal2.X, 2) || Math.Round(normal.Y, 2) != Math.Round(normal2.Y, 2) || Math.Round(normal.Z, 2) != Math.Round(normal2.Z, 2)) { } //roadVerts.Add(prevLeft); //roadVerts.Add(currentLeft); //roadVerts.Add(prevRight); //roadVerts.Add(currentLeft); //roadVerts.Add(prevRight); //roadVerts.Add(currentRight); prevLeft = currentLeft; prevRight = currentRight; } //_vertexBuffer = new VertexBuffer(Engine.Instance.Device, VertexPositionColor.SizeInBytes * verts.Count, BufferUsage.WriteOnly); //_vertexBuffer.SetData(verts.ToArray()); return roadVerts; } public static Vector3 GetRoadOffsetPosition(TrackNode roadNode, float offset) { Vector3 position = roadNode.Position + Utility.RotatePoint(new Vector2(offset, 0), -roadNode.Orientation); return position; } } } ================================================ FILE: OpenNFS1/Tracks/TrackBillboardModel.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using GameEngine; namespace OpenNFS1 { static class TrackBillboardModel { static VertexPositionTexture[] _vertices; static VertexBuffer _vertexBuffer; static TrackBillboardModel() { CreateGeometry(); //_renderEffect = new AlphaTestEffect(Engine.Instance.Device); //_renderEffect.AlphaFunction = CompareFunction.Greater; //_renderEffect.ReferenceAlpha = 5; } public static void BeginBatch() { Engine.Instance.Device.SetVertexBuffer(_vertexBuffer); //_renderEffect.View = Engine.Instance.Camera.View; //_renderEffect.Projection = Engine.Instance.Camera.Projection; //_renderEffect.CurrentTechnique.Passes[0].Apply(); } //public static void Render(AlphaTestEffect effect, Texture2D texture) //{ // _renderEffect.World = world; // _renderEffect.Texture = texture; // _renderEffect.CurrentTechnique.Passes[0].Apply(); // Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 2); //} private static void CreateGeometry() { Vector3 topLeftFront = new Vector3(-0.5f, 1.0f, 0.5f); Vector3 bottomLeftFront = new Vector3(-0.5f, 0.0f, 0.5f); Vector3 topRightFront = new Vector3(0.5f, 1.0f, 0.5f); Vector3 bottomRightFront = new Vector3(0.5f, 0.0f, 0.5f); Vector2 textureTopLeft = new Vector2(0.0f, 0.0f); Vector2 textureTopRight = new Vector2(1.0f, 0.0f); Vector2 textureBottomLeft = new Vector2(0.0f, 1.0f); Vector2 textureBottomRight = new Vector2(1.0f, 1.0f); _vertices = new VertexPositionTexture[4]; _vertices[0] = new VertexPositionTexture(topLeftFront, textureTopLeft); _vertices[1] = new VertexPositionTexture(bottomLeftFront, textureBottomLeft); _vertices[2] = new VertexPositionTexture(topRightFront, textureTopRight); _vertices[3] = new VertexPositionTexture(bottomRightFront, textureBottomRight); _vertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), _vertices.Length, BufferUsage.WriteOnly); _vertexBuffer.SetData(_vertices); } } } ================================================ FILE: OpenNFS1/Tracks/TrackDescription.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.IO; namespace OpenNFS1 { class TrackDescription { static List _trackDescriptions; public static List Descriptions { get { return TrackDescription._trackDescriptions; } } public string Name; public string FileName; public bool IsOpenRoad; public string ImageFile; public bool HideFromMenu; public string AlternativeTimeOfDay; static TrackDescription() { _trackDescriptions = new List(); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\al1.tri", Name = "Alpine", IsOpenRoad = true, ImageFile = "alpine.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\cl1.tri", Name = "Coastal", IsOpenRoad = true, ImageFile = "coast.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\cy1.tri", Name = "City", IsOpenRoad = true, ImageFile = "city.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr1.tri", Name = "Rusty Springs", IsOpenRoad = false, ImageFile = "springs.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr2.tri", Name = "Autumn Valley", IsOpenRoad = false, ImageFile = "avalley.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr3.tri", Name = "Vertigo Ridge", IsOpenRoad = false, ImageFile = "vridge.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr5.tri", Name = "Oasis Springs (hidden track)", IsOpenRoad = false, ImageFile = "springsr.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr6.tri", Name = "Burnt Sienna", IsOpenRoad = false, ImageFile = "bsienna.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr7.tri", Name = "Transpolis", IsOpenRoad = false, ImageFile = "tpolis.qfs", AlternativeTimeOfDay = "Morning" }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\tr4.tri", Name = "Lost Vegas", IsOpenRoad = false, ImageFile = "lvegas.qfs", }); //Open road stages _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\al2.tri", Name = "Alpine 2", IsOpenRoad = true, ImageFile = "alpine.qfs", HideFromMenu = true }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\al3.tri", Name = "Alpine 3", IsOpenRoad = true, ImageFile = "alpine.qfs", HideFromMenu = true }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\cy2.tri", Name = "City 2", IsOpenRoad = true, ImageFile = "city.qfs", HideFromMenu = true }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\cy3.tri", Name = "City 3", IsOpenRoad = true, ImageFile = "city.qfs", HideFromMenu = true }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\cl2.tri", Name = "Coastal 2", IsOpenRoad = true, ImageFile = "coast.qfs", HideFromMenu = true }); _trackDescriptions.Add(new TrackDescription() { FileName = "SIMDATA\\MISC\\cl3.tri", Name = "Coastal 3", IsOpenRoad = true, ImageFile = "coast.qfs", HideFromMenu = true }); } /// /// Quick and very dirty way of getting the next stage in an open road race /// /// /// public static TrackDescription GetNextOpenRoadStage(TrackDescription openRoadTrack) { string lookForNumber = Path.GetFileNameWithoutExtension(openRoadTrack.FileName).Substring(2); lookForNumber = (int.Parse(lookForNumber)+1).ToString(); return _trackDescriptions.Find(delegate(TrackDescription track) { if (Path.GetFileName(track.FileName).StartsWith(Path.GetFileName(openRoadTrack.FileName).Substring(0, 2))) { if (Path.GetFileNameWithoutExtension(track.FileName).EndsWith(lookForNumber)) return true; } return false; }); } } } ================================================ FILE: OpenNFS1/Tracks/TrackNode.cs ================================================ using Microsoft.Xna.Framework; using OpenNFS1; using GameEngine; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenNFS1.Tracks { class TrackNodeProperty { // Unimplemented: public const byte LANE_SPLIT = 0; // Used in Alpine, Coastal tracks when the road widens. vertices should be moved to the right. public const byte LANE_REJOIN = 2; //Used in Alpine, Coastal tracks when the road narrows. vertices should be moved to the right public const byte TUNNEL = 4; //plays tunnel sound public const byte COBBLED_ROAD = 5; //plays different road noise (Last Vegas) public const byte WATERFALL_AUDIO_LEFT_CHANNEL = 14; //plays waterfall audio in left channel public const byte WATERFALL_AUDIO_RIGHT_CHANNEL = 15; //plays waterfall audio in right channel public const byte WATER_AUDIO_LEFT_CHANNEL = 18; //plays water sound public const byte WATER_AUDIO_RIGHT_CHANNEL = 18; //plays water sound // Implemented: // The next 3 properties are interesting! If set, either the last terrain strip on the right or left is detached and // placed between the specified points. This is how the tunnels in Vertigo Ridge and Coastal (and Alpine 3) are constructed. // For the meaning of A2,A9 etc, refer to /NFSSpecs.txt public const byte RIGHT_TUNNEL_A2_A9 = 7; public const byte LEFT_TUNNEL_A9_A4 = 12; public const byte LEFT_TUNNEL_A9_A5 = 13; } class TrackNode { public int Number; public float DistanceToLeftVerge, DistanceToLeftBarrier; public float DistanceToRightVerge, DistanceToRightBarrier; public byte Flag1; public byte Flag2; public byte Flag3; public byte NodeProperty; public Vector3 Position; public float Slope; public Int32 Slant, ZOrientation, XOrientation; public float Orientation; public Vector3 Up; public byte[] unk1, unk2; public TrackNode Next, Prev; const float SlantMultiplier = 1.028f; const float RoadPadding = 7; public Vector3 GetLeftBoundary() { return GetLeftOffset(DistanceToLeftBarrier); } public Vector3 GetLeftVerge() { return GetLeftOffset(DistanceToLeftVerge); } public Vector3 GetRightBoundary() { return GetRightOffset(DistanceToRightBarrier); } public Vector3 GetRightVerge() { return GetRightOffset(DistanceToRightVerge); } public Vector3 GetLeftVerge2() { return GetLeftOffset(DistanceToLeftVerge - RoadPadding); } public Vector3 GetRightVerge2() { return GetRightOffset(DistanceToRightVerge - RoadPadding); } public Vector3 GetMiddlePoint() { return (GetLeftVerge() + GetRightVerge()) / 2; } private Vector3 GetLeftOffset(float offset) { Vector3 position = Position + Utility.RotatePoint(new Vector2(-offset, 0), -Orientation); float slantAngle = (((float)Slant / short.MaxValue)); var pos3 = Vector3.Transform(new Vector3(-offset, 0, 0), Matrix.CreateRotationZ(slantAngle * SlantMultiplier) * Matrix.CreateRotationY(MathHelper.ToRadians(Orientation))); return Position + pos3; } private Vector3 GetRightOffset(float offset) { float slantAngle = (((float)Slant / short.MaxValue)); // 32000f)); var pos3 = Vector3.Transform(new Vector3(offset, 0, 0), Matrix.CreateRotationZ(slantAngle * SlantMultiplier) * Matrix.CreateRotationY(MathHelper.ToRadians(Orientation))); return Position + pos3; } } } ================================================ FILE: OpenNFS1/Tracks/TrackObjectDescriptor.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenNFS1.Tracks { class TrackObjectDescriptor { } } ================================================ FILE: OpenNFS1/Tracks/TrackSkybox.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using GameEngine; using System.IO; namespace OpenNFS1.Tracks { public class TrackSkyBox { Texture2D[] textures = new Texture2D[6]; public Texture2D[] Textures { get { return textures; } set { textures = value; } } BasicEffect _effect; VertexBuffer vertices; IndexBuffer indices; bool _hasGeneratedTopBottomTextures; public float YOffset; public TrackSkyBox(Texture2D horizon) { // This is silly... for some reason after loading the track on a separate thread, we have to wait until // we get called in the update() loop to be able to pull the pixel data to generate the top & bottom textures _hasGeneratedTopBottomTextures = false; Textures[0] = horizon; Textures[1] = horizon; //Textures[2] = bottomTexture; //Textures[3] = topTexture; Textures[4] = horizon; Textures[5] = horizon; YOffset = 60; _effect = new BasicEffect(Engine.Instance.Device); _effect.TextureEnabled = true; vertices = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), 4 * 6, BufferUsage.WriteOnly); VertexPositionTexture[] data = new VertexPositionTexture[4 * 6]; float y = 0.3f; #region Define Vertexes Vector3 vExtents = new Vector3(1400, 450, 800); //back data[0].Position = new Vector3(vExtents.X, -vExtents.Y * y, -vExtents.Z); data[0].TextureCoordinate.X = 1f; data[0].TextureCoordinate.Y = 1.0f; data[1].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z); data[1].TextureCoordinate.X = 1f; data[1].TextureCoordinate.Y = 0.0f; data[2].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z); data[2].TextureCoordinate.X = 0f; data[2].TextureCoordinate.Y = 0.0f; data[3].Position = new Vector3(-vExtents.X, -vExtents.Y * y, -vExtents.Z); data[3].TextureCoordinate.X = 0f; data[3].TextureCoordinate.Y = 1.0f; //front data[4].Position = new Vector3(-vExtents.X, -vExtents.Y * y, vExtents.Z); data[4].TextureCoordinate.X = 1f; data[4].TextureCoordinate.Y = 1.0f; data[5].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z); data[5].TextureCoordinate.X = 1f; data[5].TextureCoordinate.Y = 0.0f; data[6].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z); data[6].TextureCoordinate.X = 0f; data[6].TextureCoordinate.Y = 0.0f; data[7].Position = new Vector3(vExtents.X, -vExtents.Y * y, vExtents.Z); data[7].TextureCoordinate.X = 0f; data[7].TextureCoordinate.Y = 1.0f; //bottom data[8].Position = new Vector3(-vExtents.X, -vExtents.Y * y, -vExtents.Z); data[8].TextureCoordinate.X = 1f; data[8].TextureCoordinate.Y = 0.0f; data[9].Position = new Vector3(-vExtents.X, -vExtents.Y * y, vExtents.Z); data[9].TextureCoordinate.X = 1f; data[9].TextureCoordinate.Y = 1.0f; data[10].Position = new Vector3(vExtents.X, -vExtents.Y * y, vExtents.Z); data[10].TextureCoordinate.X = 0f; data[10].TextureCoordinate.Y = 1.0f; data[11].Position = new Vector3(vExtents.X, -vExtents.Y * y, -vExtents.Z); data[11].TextureCoordinate.X = 0f; data[11].TextureCoordinate.Y = 0.0f; //top data[12].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z); data[12].TextureCoordinate.X = 0f; data[12].TextureCoordinate.Y = 0.0f; data[13].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z); data[13].TextureCoordinate.X = 0f; data[13].TextureCoordinate.Y = 1.0f; data[14].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z); data[14].TextureCoordinate.X = 1f; data[14].TextureCoordinate.Y = 1.0f; data[15].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z); data[15].TextureCoordinate.X = 1f; data[15].TextureCoordinate.Y = 0.0f; //left data[16].Position = new Vector3(-vExtents.X, vExtents.Y, -vExtents.Z); data[16].TextureCoordinate.X = 0f; data[16].TextureCoordinate.Y = 0.0f; data[17].Position = new Vector3(-vExtents.X, vExtents.Y, vExtents.Z); data[17].TextureCoordinate.X = 1f; data[17].TextureCoordinate.Y = 0.0f; data[18].Position = new Vector3(-vExtents.X, -vExtents.Y * y, vExtents.Z); data[18].TextureCoordinate.X = 1f; data[18].TextureCoordinate.Y = 1.0f; data[19].Position = new Vector3(-vExtents.X, -vExtents.Y * y, -vExtents.Z); data[19].TextureCoordinate.X = 0f; data[19].TextureCoordinate.Y = 1.0f; //right data[20].Position = new Vector3(vExtents.X, -vExtents.Y * y, -vExtents.Z); data[20].TextureCoordinate.X = 1f; data[20].TextureCoordinate.Y = 1.0f; data[21].Position = new Vector3(vExtents.X, -vExtents.Y * y, vExtents.Z); data[21].TextureCoordinate.X = 0f; data[21].TextureCoordinate.Y = 1.0f; data[22].Position = new Vector3(vExtents.X, vExtents.Y, vExtents.Z); data[22].TextureCoordinate.X = 0f; data[22].TextureCoordinate.Y = 0.0f; data[23].Position = new Vector3(vExtents.X, vExtents.Y, -vExtents.Z); data[23].TextureCoordinate.X = 1f; data[23].TextureCoordinate.Y = 0.0f; vertices.SetData(data); indices = new IndexBuffer(Engine.Instance.Device, typeof(short), 6 * 6, BufferUsage.WriteOnly); short[] ib = new short[6 * 6]; for (int x = 0; x < 6; x++) { ib[x * 6 + 0] = (short)(x * 4 + 0); ib[x * 6 + 2] = (short)(x * 4 + 1); ib[x * 6 + 1] = (short)(x * 4 + 2); ib[x * 6 + 3] = (short)(x * 4 + 2); ib[x * 6 + 5] = (short)(x * 4 + 3); ib[x * 6 + 4] = (short)(x * 4 + 0); } indices.SetData(ib); #endregion } public void Update() { var pos = Engine.Instance.Camera.Position; pos.Y += YOffset; _effect.World = Matrix.CreateTranslation(pos); _effect.Projection = Engine.Instance.Camera.Projection; _effect.View = Engine.Instance.Camera.View; // Skybox only has the side textures, generate a single-color top and bottom texture if (!_hasGeneratedTopBottomTextures) { Color[] pixel = new Color[1]; textures[0].GetData(0, new Rectangle(0, 0, 1, 1), pixel, 0, 1); //top left pixel var topTexture = new Texture2D(Engine.Instance.Device, 1, 1); topTexture.SetData(pixel); var bottomTexture = new Texture2D(Engine.Instance.Device, 1, 1); textures[0].GetData(0, new Rectangle(0, textures[0].Height - 1, 1, 1), pixel, 0, 1); //bottom left pixel bottomTexture.SetData(pixel); Color[] px = new Color[textures[0].Width * textures[1].Height]; textures[0].GetData(px); Textures[2] = bottomTexture; Textures[3] = topTexture; _hasGeneratedTopBottomTextures = true; } } public void Render() { if (vertices == null) return; GraphicsDevice device = Engine.Instance.Device; device.DepthStencilState = DepthStencilState.None; device.SamplerStates[0] = SamplerState.LinearWrap; device.RasterizerState = RasterizerState.CullCounterClockwise; device.SetVertexBuffer(vertices); device.Indices = indices; for (int x = 0; x < 6; x++) { if (textures[x] == null) continue; _effect.Texture = textures[x]; _effect.CurrentTechnique.Passes[0].Apply(); device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.VertexCount, x * 6, 2); } device.DepthStencilState = DepthStencilState.Default; } } } ================================================ FILE: OpenNFS1/UI/Screens/BaseUIScreen.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Media; using GameEngine; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using OpenNFS1.Parsers; using System.IO; namespace OpenNFS1.UI.Screens { class BaseUIScreen { protected SpriteFont Font { get; private set; } static Texture2D _background; int _currentLine; protected float TextSize = 0.5f; protected float TitleSize = 1; protected float SectionSize = 0.8f; protected Color TextColor = Color.Orange; public BaseUIScreen() { Font = Engine.Instance.ContentManager.Load("Content\\ArialBlack-Italic"); _background = Engine.Instance.ContentManager.Load("Content\\splash-screen.jpg"); } public virtual void Draw() { Engine.Instance.SpriteBatch.Begin(); if (_background != null) { Engine.Instance.SpriteBatch.Draw(_background, Vector2.Zero, Color.FromNonPremultiplied(50, 50, 50, 255)); } _currentLine = 5; } public void WriteLine(string text) { WriteLine(text, Color.White, _currentLine, 30, TextSize); } public void WriteLine(string text, Color c) { WriteLine(text, c, _currentLine, 30, TextSize); } public void WriteLine(string text, bool selected, int lineOffset, int column, float size) { WriteLine(text, selected ? Color.Yellow : Color.LightGray, lineOffset, column, selected ? size * 1.1f : size); } public void WriteLine(string text, Color c, int lineOffset, int column, float size) { if (String.IsNullOrEmpty(text)) return; _currentLine += lineOffset; string[] lines = text.Split(new string[] { "\r\n" }, StringSplitOptions.None); for (int i = 0; i 0) { int ratio = (int)(((double)_dataDownloaded / (double)_dataContentLength) * 50); string progress = new string('=', ratio); WriteLine(progress, TextColor, 80, 30, TextSize); long downloadedMb = _dataDownloaded / 1024 / 1024; long contentLengthMb = _dataContentLength / 1024 / 1024; WriteLine(String.Format("Downloaded {0}mb / {1}mb", downloadedMb, contentLengthMb), TextColor, 20, 30, TextSize); } if (_unpacking) { WriteLine("Unpacking into CD_Data folder...", TextColor, 30, 30, TextSize); } if (_completed) { WriteLine("Unpacking complete! Hit enter to continue.", TextColor, 30, 30, TextSize); } if (_downloadError) { WriteLine("An error occured while downloading - please check\r\nexception.txt for details.\r\n\r\nHit enter to quit.", TextColor, 60, 30, TextSize); } Engine.Instance.SpriteBatch.End(); } private void DownloadDataThreadProc() { try { string url = "http://www.1amstudios.com/download/NFSSE_cd_data.zip"; WebRequest request = WebRequest.Create(url); var response = request.GetResponse(); _dataContentLength = long.Parse(response.Headers["Content-Length"]); byte[] buffer = new byte[4096]; string tempFileName = Path.GetTempFileName(); Stream fileStream = File.Open(tempFileName, FileMode.Create); using (Stream s = response.GetResponseStream()) { while (true) { int read = s.Read(buffer, 0, 4096); _dataDownloaded += read; if (read == 0) break; fileStream.Write(buffer, 0, read); } } fileStream.Close(); _unpacking = true; var zipFile = ZipFile.Read(tempFileName); Directory.CreateDirectory("CD_Data"); zipFile.ExtractAll("CD_Data"); zipFile.Dispose(); File.Delete(tempFileName); _completed = true; } catch (Exception ex) { File.WriteAllText("exception.txt", ex.ToString()); _downloadError = true; } } } } ================================================ FILE: OpenNFS1/UI/Screens/DoRaceScreen.cs ================================================ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using GameEngine; using OpenNFS1.Parsers.Track; using OpenNFS1.Physics; using OpenNFS1.UI; using OpenNFS1.UI.Screens; using OpenNFS1.Vehicles; using OpenNFS1.Vehicles.AI; namespace OpenNFS1 { class DoRaceScreen : IGameScreen { Track _track; DrivableVehicle _car; Race _race; RaceUI _raceUI; PlayerUI _playerUI; Viewport _raceViewport, _uiViewport; PlayerDriver _playerDriver; public DoRaceScreen(Track track) { _track = track; _car = new DrivableVehicle(GameConfig.SelectedVehicle); _playerDriver = new PlayerDriver(_car); _car.AudioEnabled = true; _race = new Race(_track.IsOpenRoad ? 1 : 3, _track, _playerDriver); for (int i = 0; i < 10; i++) { int j = Engine.Instance.Random.Next(VehicleDescription.Descriptions.Count); _race.AddDriver(new RacingAIDriver(VehicleDescription.Descriptions[j])); } //_race.AddDriver(new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "Viper"))); //_race.AddDriver(new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "Viper"))); //_race.AddDriver(new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "Viper"))); //_race.AddDriver(new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "Viper"))); _playerUI = new PlayerUI(_car); /* d = new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "911")); _aiDrivers.Add(d); _track.AddDriver(d); d = new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "Viper")); _aiDrivers.Add(d); _track.AddDriver(d); d = new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "Diablo")); _aiDrivers.Add(d); _track.AddDriver(d); d = new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "F512")); _aiDrivers.Add(d); _track.AddDriver(d); d = new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "ZR1")); _aiDrivers.Add(d); _track.AddDriver(d); d = new AIDriver(VehicleDescription.Descriptions.Find(a => a.Name == "NSX")); _aiDrivers.Add(d); _track.AddDriver(d); */ _raceUI = new RaceUI(_race); _race.StartCountdown(); _raceViewport = new Viewport(0, 0, 640, 400); _uiViewport = new Viewport(0, 0, 640, 480); } #region IDrawableObject Members public void Update(GameTime gameTime) { Engine.Instance.Device.Viewport = _raceViewport; _race.Update(); TyreSmokeParticleSystem.Instance.Update(); _playerUI.Update(gameTime); if (_race.Finished) { _car.AudioEnabled = false; Engine.Instance.Screen = new RaceFinishedScreen(_race, _track); return; } if (UIController.Pause) { Pause(); return; } if (Engine.Instance.Input.WasPressed(Keys.R)) { _car.Reset(); } TyreSmokeParticleSystem.Instance.SetCamera(Engine.Instance.Camera); Engine.Instance.Camera.Update(gameTime); } public void Pause() { _car.AudioEnabled = false; Engine.Instance.Screen = new RacePausedScreen(this); } public void Resume() { _car.AudioEnabled = true; } public void Draw() { Engine.Instance.Device.Viewport = _raceViewport; _race.Render(_playerUI.ShouldRenderCar); TyreSmokeParticleSystem.Instance.Render(); Engine.Instance.Device.Viewport = _uiViewport; _raceUI.Render(); _playerUI.Render(); Engine.Instance.Device.Viewport = _raceViewport; } #endregion } } ================================================ FILE: OpenNFS1/UI/Screens/HomeScreen.cs ================================================ using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using GameEngine; using OpenNFS1.Parsers; using OpenNFS1.Parsers.Audio; using OpenNFS1.Vehicles; namespace OpenNFS1.UI.Screens { class VehicleUIControl { public BitmapEntry Bitmap { get; private set; } public VehicleDescription Descriptor { get; private set; } public VehicleUIControl(VehicleDescription desc) { Descriptor = desc; QfsFile qfs = new QfsFile(@"Frontend\Art\Control\" + desc.UIImageFile); Bitmap = qfs.Content.Header.Bitmaps.Find(a => a.Id == "0000"); } } class TrackUIControl { public BitmapEntry Bitmap { get; private set; } public TrackDescription Descriptor { get; private set; } public TrackUIControl(TrackDescription desc) { Descriptor = desc; QfsFile qfs = new QfsFile(@"Frontend\Art\Control\" + desc.ImageFile); Bitmap = qfs.Content.Header.Bitmaps.Find(a => a.Id == "0000"); } } enum SelectedControlType { Vehicle, Track } class HomeScreen : IGameScreen { BitmapEntry _background, _vehicleSelection, _trackSelection, _raceButtonSelection; const int VehicleSelected = 0; const int TrackSelected = 1; const int RaceButtonSelected = 2; List _vehicles = new List(); List _track = new List(); int _currentVehicle = 2; int _currentTrack = 4; int _selectedControl = RaceButtonSelected; public HomeScreen() : base() { QfsFile qfs = new QfsFile(@"FRONTEND\ART\control\central.qfs"); _background = qfs.Content.Header.FindByName("bgnd"); _vehicleSelection = qfs.Content.Header.FindByName("Tlb1"); _trackSelection = qfs.Content.Header.FindByName("Brb4"); _raceButtonSelection = qfs.Content.Header.FindByName("Ra1l"); foreach (var vehicle in VehicleDescription.Descriptions) { _vehicles.Add(new VehicleUIControl(vehicle)); } foreach (var track in TrackDescription.Descriptions) { if (!track.HideFromMenu) { _track.Add(new TrackUIControl(track)); } } if (GameConfig.SelectedTrackDescription != null) _currentTrack = _track.FindIndex(a => a.Descriptor == GameConfig.SelectedTrackDescription); if (GameConfig.SelectedVehicle != null) _currentVehicle = _vehicles.FindIndex(a => a.Descriptor == GameConfig.SelectedVehicle); if (_currentTrack == -1) _currentTrack = 0; } public void Update(GameTime gameTime) { switch (_selectedControl) { case VehicleSelected: if (Engine.Instance.Input.WasPressed(Keys.Left)) _currentVehicle--; if (_currentVehicle < 0) _currentVehicle = _vehicles.Count-1; else if (Engine.Instance.Input.WasPressed(Keys.Right)) _currentVehicle++; _currentVehicle %= _vehicles.Count; break; case TrackSelected: if (Engine.Instance.Input.WasPressed(Keys.Left)) _currentTrack--; if (_currentTrack < 0) _currentTrack = _track.Count - 1; else if (Engine.Instance.Input.WasPressed(Keys.Right)) _currentTrack = (_currentTrack + 1) % _track.Count; break; } if (Engine.Instance.Input.WasPressed(Keys.Down)) { _selectedControl++; _selectedControl %= 3; } else if (Engine.Instance.Input.WasPressed(Keys.Up)) { _selectedControl--; if (_selectedControl < 0) _selectedControl = 2; } else if (Engine.Instance.Input.WasPressed(Keys.Enter) && _selectedControl == RaceButtonSelected) { GameConfig.SelectedVehicle = _vehicles[_currentVehicle].Descriptor; GameConfig.SelectedTrackDescription = _track[_currentTrack].Descriptor; Engine.Instance.Screen = new RaceOptionsScreen(); } else if (Engine.Instance.Input.WasPressed(Keys.Escape)) { Engine.Instance.Game.Exit(); } } public void Draw() { Engine.Instance.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); Engine.Instance.SpriteBatch.Draw(_background.Texture, Vector2.Zero, Color.White); Engine.Instance.SpriteBatch.Draw(_vehicles[_currentVehicle].Bitmap.Texture, _vehicles[_currentVehicle].Bitmap.GetDisplayAt(), Color.White); Engine.Instance.SpriteBatch.Draw(_track[_currentTrack].Bitmap.Texture, _track[_currentTrack].Bitmap.GetDisplayAt(), Color.White); switch (_selectedControl) { case VehicleSelected: Engine.Instance.SpriteBatch.Draw(_vehicleSelection.Texture, _vehicleSelection.GetDisplayAt(), Color.White); break; case TrackSelected: Engine.Instance.SpriteBatch.Draw(_trackSelection.Texture, _trackSelection.GetDisplayAt(), Color.White); break; case RaceButtonSelected: Engine.Instance.SpriteBatch.Draw(_raceButtonSelection.Texture, _raceButtonSelection.GetDisplayAt(), Color.White); break; } Engine.Instance.SpriteBatch.End(); } } } ================================================ FILE: OpenNFS1/UI/Screens/LoadRaceScreen.cs ================================================ using System; using System.Threading; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GameEngine; using OpenNFS1.Loaders; using OpenNFS1.Parsers; using OpenNFS1.Parsers.Track; using OpenNFS1.UI.Screens; namespace OpenNFS1 { class LoadRaceScreen : BaseUIScreen, IGameScreen { float _loadingTime; int _nbrDots = 0; TriFile _tri; TrackAssembler _assembler; public LoadRaceScreen() : base() { if (GameConfig.CurrentTrack != null) { GameConfig.CurrentTrack.Dispose(); GameConfig.CurrentTrack = null; } new Thread(LoadTrack).Start(); } #region IDrawableObject Members public void Update(GameTime gameTime) { if (GameConfig.CurrentTrack != null) { Engine.Instance.Screen = new DoRaceScreen(GameConfig.CurrentTrack); } _loadingTime += Engine.Instance.FrameTime; if (_loadingTime > 0.1f) { _nbrDots++; _loadingTime = 0; } } public override void Draw() { base.Draw(); WriteLine(GameConfig.SelectedTrackDescription.Name, Color.White, 20, 30, TitleSize); WriteLine(String.Format("Loading {0}", new string('.', _nbrDots)), TextColor, 50, 30, TextSize); WriteLine("Reading track file " + GameConfig.SelectedTrackDescription.FileName, TextColor, 20, 30, TextSize); if (_assembler != null) WriteLine(_assembler.ProgressMessage, TextColor, 20, 30, TextSize); Engine.Instance.SpriteBatch.End(); } #endregion private void LoadTrack() { _tri = new TriFile(GameConfig.SelectedTrackDescription.FileName); _assembler = new TrackAssembler(); GameConfig.CurrentTrack = _assembler.Assemble(_tri); GameConfig.CurrentTrack.Description = GameConfig.SelectedTrackDescription; } } } ================================================ FILE: OpenNFS1/UI/Screens/OpenNFS1SplashScreen.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using GameEngine; namespace OpenNFS1.UI.Screens { class OpenNFS1SplashScreen : BaseUIScreen, IGameScreen { float _elapsedTime; public void Update(GameTime gameTime) { _elapsedTime += Engine.Instance.FrameTime; if (Engine.Instance.Input.WasPressed(Keys.Enter)) { if (!Directory.Exists(GameConfig.CdDataPath) || Directory.GetDirectories(GameConfig.CdDataPath).Length == 0) Engine.Instance.Screen = new ChooseDataDownloadScreen(); else { Engine.Instance.Screen = new HomeScreen(); } } } public override void Draw() { base.Draw(); var version = Assembly.GetExecutingAssembly().GetName().Version; WriteLine("OpenNFS1 v" + version.ToString(2), Color.Red, 0, 30, TextSize); WriteLine( "OpenNFS1 is a remake of the original EA Need for Speed 1.\r\n\r\n" + "OpenNFS1 code was written from scratch without reverse\r\n" + "engineering the NFS executable, and it uses the original data\r\n" + "files that were on the CD back in 1995!", TextColor, 50, 30, TextSize); WriteLine("By Jeff H - www.1amstudios.com", TextColor, 40, 30, TextSize); WriteLine( "Need for Speed 1 was produced by Pioneer Productions\r\nand EA Seattle in 1995.\r\n" + "1amStudios and OpenNFS1 are not affiliated in any way\r\nwith Pioneer Productions or EA Seattle.", Color.LightGray, 190, 30, TextSize); Engine.Instance.SpriteBatch.End(); } } } ================================================ FILE: OpenNFS1/UI/Screens/RaceFinishedScreen.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Parsers.Track; namespace OpenNFS1.UI.Screens { class RaceFinishedScreen : BaseUIScreen, IGameScreen { Race _race; Texture2D _background; Track _raceTrack; int _selectedOption = 0; public RaceFinishedScreen(Race race, Track raceTrack) : base() { _race = race; _raceTrack = raceTrack; _background = ScreenEffects.TakeScreenshot(); GC.Collect(); //force some memory freeness here. } #region IDrawableObject Members public void Update(GameTime gameTime) { if (UIController.Back) { Engine.Instance.Screen = new HomeScreen(); } if (UIController.Up && _selectedOption > 0) _selectedOption--; else if (UIController.Down && _selectedOption < 1) _selectedOption++; if (UIController.Ok) { if (_selectedOption == 1) Engine.Instance.Screen = new HomeScreen(); else { if (!GameConfig.SelectedTrackDescription.IsOpenRoad) Engine.Instance.Screen = new DoRaceScreen(_raceTrack); else { GameConfig.SelectedTrackDescription = TrackDescription.GetNextOpenRoadStage(GameConfig.SelectedTrackDescription); if (GameConfig.SelectedTrackDescription == null) Engine.Instance.Screen = new HomeScreen(); else Engine.Instance.Screen = new LoadRaceScreen(); } } } } public override void Draw() { base.Draw(); Engine.Instance.SpriteBatch.Draw(_background, Vector2.Zero, new Color(255, 255, 255, 100)); if (GameConfig.SelectedTrackDescription.IsOpenRoad) DrawOpenRoadResult(); else DrawCircuitResult(); Engine.Instance.SpriteBatch.End(); } #endregion private void DrawCircuitResult() { WriteLine(GameConfig.SelectedTrackDescription.Name + "- Race completed", Color.Gray, 20, 30, TitleSize); int totalSeconds = 0; for (int i = 0; i < _race.PlayerStats.LapTimes.Count; i++) { WriteLine("Lap " + (i + 1) + ": " + TimeSpan.FromSeconds(_race.PlayerStats.LapTimes[i]).ToString()); totalSeconds += _race.PlayerStats.LapTimes[i]; } WriteLine("Total time: " + TimeSpan.FromSeconds(totalSeconds)); WriteLine(" Race again", _selectedOption == 0, 60, 30, SectionSize); WriteLine(" Main menu", _selectedOption == 1, 30, 30, SectionSize); } private void DrawOpenRoadResult() { WriteLine(GameConfig.SelectedTrackDescription.Name + " - Stage completed", Color.Gray, 20, 30, TitleSize); WriteLine("Time: " + TimeSpan.FromSeconds(_race.PlayerStats.LapTimes[0])); if (TrackDescription.GetNextOpenRoadStage(GameConfig.SelectedTrackDescription) != null) { WriteLine("Continue to next stage", _selectedOption == 0, 60, 30, SectionSize); } WriteLine(" Main menu", _selectedOption == 1, 30, 30, SectionSize); } } } ================================================ FILE: OpenNFS1/UI/Screens/RaceOptionsScreen.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using GameEngine; using OpenNFS1.Parsers; namespace OpenNFS1.UI.Screens { class RaceOptionsScreen: BaseUIScreen, IGameScreen { int _selectedOption, _gearboxOption, _timeOfDayOption; public RaceOptionsScreen() : base() { _selectedOption = 2; _gearboxOption = GameConfig.ManualGearbox ? 1 : 0; _timeOfDayOption = GameConfig.AlternativeTimeOfDay ? 1 : 0; } public void Update(GameTime gameTime) { if (Engine.Instance.Input.WasPressed(Keys.Down)) { _selectedOption = Math.Min(_selectedOption + 1, 2); } else if (Engine.Instance.Input.WasPressed(Keys.Up)) { _selectedOption = Math.Max(_selectedOption - 1, 0); } else if (Engine.Instance.Input.WasPressed(Keys.Left)) { if (_selectedOption == 0) _gearboxOption = 0; else _timeOfDayOption = 0; } else if (Engine.Instance.Input.WasPressed(Keys.Right)) { if (_selectedOption == 0) _gearboxOption = 1; else _timeOfDayOption = 1; } else if (Engine.Instance.Input.WasPressed(Keys.Enter)) { if (_selectedOption == 2) { GameConfig.ManualGearbox = _gearboxOption == 1; GameConfig.AlternativeTimeOfDay = _timeOfDayOption == 1; Engine.Instance.Screen = new LoadRaceScreen(); } } } public override void Draw() { base.Draw(); WriteLine("Race settings", Color.White, 20, 30, TitleSize); WriteLine("Gearbox", _selectedOption == 0, 60, 30, SectionSize); WriteLine("Auto", _gearboxOption == 0, 40, 40, TextSize); WriteLine("Manual", _gearboxOption == 1, 0, 120, TextSize); WriteLine("Time of day", _selectedOption == 1, 40, 30, SectionSize); WriteLine("Midday", _timeOfDayOption == 0, 40, 40, TextSize); if (GameConfig.SelectedTrackDescription.AlternativeTimeOfDay != null) { WriteLine(GameConfig.SelectedTrackDescription.AlternativeTimeOfDay, _timeOfDayOption == 1, 0, 120, TextSize); } WriteLine("Go!", _selectedOption == 2, 40, 30, SectionSize); Engine.Instance.SpriteBatch.End(); } } } ================================================ FILE: OpenNFS1/UI/Screens/RacePausedScreen.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Physics; namespace OpenNFS1.UI.Screens { class RacePausedScreen : BaseUIScreen, IGameScreen { DoRaceScreen _currentRace; int _selectedOption = 0; public RacePausedScreen(DoRaceScreen currentRace) : base() { _currentRace = currentRace; } #region IDrawableObject Members public void Update(GameTime gameTime) { //Engine.Instance.Device.Viewport = FullViewport; if (UIController.Up && _selectedOption > 0) _selectedOption--; else if (UIController.Down && _selectedOption < 1) _selectedOption++; if (UIController.Back) { Engine.Instance.Screen = _currentRace; return; } if (UIController.Ok) { if (_selectedOption == 0) { Engine.Instance.Screen = _currentRace; _currentRace.Resume(); } else { Engine.Instance.Screen = new HomeScreen(); } } } public override void Draw() { base.Draw(); WriteLine("Race paused", Color.White, 20, 30, TitleSize); WriteLine("Continue", _selectedOption == 0, 60, 30, SectionSize); WriteLine("Main menu", _selectedOption == 1, 40, 30, SectionSize); Engine.Instance.SpriteBatch.End(); } #endregion } } ================================================ FILE: OpenNFS1/UIController.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework.Input; namespace OpenNFS1 { static class UIController { public static bool Left { get { if (Engine.Instance.Input.WasPressed(Keys.Left)) return true; if (Engine.Instance.Input.WasPressed(Buttons.DPadLeft)) return true; return false; } } public static bool Right { get { if (Engine.Instance.Input.WasPressed(Keys.Right)) return true; if (Engine.Instance.Input.WasPressed(Buttons.DPadRight)) return true; return false; } } public static bool Up { get { if (Engine.Instance.Input.WasPressed(Keys.Up)) return true; if (Engine.Instance.Input.WasPressed(Buttons.DPadUp)) return true; return false; } } public static bool Down { get { if (Engine.Instance.Input.WasPressed(Keys.Down)) return true; if (Engine.Instance.Input.WasPressed(Buttons.DPadDown)) return true; return false; } } public static bool Back { get { if (Engine.Instance.Input.WasPressed(Keys.Escape)) return true; if (Engine.Instance.Input.WasPressed(Buttons.B)) return true; return false; } } public static bool Ok { get { if (Engine.Instance.Input.WasPressed(Keys.Enter)) return true; if (Engine.Instance.Input.WasPressed(Buttons.A)) return true; return false; } } public static bool Pause { get { if (Engine.Instance.Input.WasPressed(Keys.P) || Engine.Instance.Input.WasPressed(Keys.Escape)) return true; if (Engine.Instance.Input.WasPressed(Buttons.Start)) return true; return false; } } } } ================================================ FILE: OpenNFS1/VehicleController.cs ================================================ using System; using System.Collections.Generic; using System.Text; using GameEngine; using Microsoft.Xna.Framework.Input; namespace OpenNFS1 { static class VehicleController { public static bool ForceBrake { get; set; } public static float Acceleration { get { if (ForceBrake) return 0; if (Engine.Instance.Input.IsKeyDown(Keys.Up)) return 1.0f; return Engine.Instance.Input.GamePadState.Triggers.Right; } } public static float Brake { get { if (ForceBrake) return 1.0f; if (Engine.Instance.Input.IsKeyDown(Keys.Down)) return 1.0f; return Engine.Instance.Input.GamePadState.Triggers.Left; } } public static float Turn { get { if (Engine.Instance.Input.IsKeyDown(Keys.Left)) return -1; else if (Engine.Instance.Input.IsKeyDown(Keys.Right)) return 1; return Engine.Instance.Input.GamePadState.ThumbSticks.Left.X; } } public static bool ChangeView { get { if (Engine.Instance.Input.WasPressed(Keys.C)) return true; if (Engine.Instance.Input.WasPressed(Buttons.RightShoulder)) return true; return false; } } public static bool GearUp { get { if (Engine.Instance.Input.WasPressed(Keys.A)) return true; if (Engine.Instance.Input.WasPressed(Buttons.B)) return true; return false; } } public static bool GearDown { get { if (Engine.Instance.Input.WasPressed(Keys.Z)) return true; if (Engine.Instance.Input.WasPressed(Buttons.X)) return true; return false; } } public static bool Handbrake { get { if (Engine.Instance.Input.IsKeyDown(Keys.Space)) return true; return false; } } } } ================================================ FILE: OpenNFS1/Vehicles/AI/AIDriver.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Parsers.Track; using OpenNFS1.Physics; using OpenNFS1.Tracks; namespace OpenNFS1.Vehicles.AI { abstract class AIDriver : IDriver { public int VirtualLane { get; set; } public Vehicle Vehicle { get; protected set; } public Boolean AtEndOfTrack { get; set; } public const int MaxVirtualLanes = 4; public virtual Vector3 GetNextTarget() { return Vector3.Lerp(Vehicle.CurrentNode.Next.Next.GetLeftVerge2(), Vehicle.CurrentNode.Next.Next.GetRightVerge2(), (float)VirtualLane / (MaxVirtualLanes)); } protected virtual void FollowTrack() { var target = GetNextTarget(); if (GameConfig.DrawDebugInfo) { Engine.Instance.GraphicsUtils.AddCube(Matrix.CreateTranslation(target), Color.Yellow); } float angle = Utility.GetSignedAngleBetweenVectors(Vehicle.Direction, target - Vehicle.Position, true); if (angle < -0.1f) { Vehicle.SteeringInput = 0.5f; } else if (angle > 0.1f) { Vehicle.SteeringInput = -0.5f; } else { Vehicle.SteeringInput = 0; } } public abstract void Update(List otherDrivers); } class RacingAIDriver : AIDriver { private float _firstLaneChangeAllowed; //avoid all racers changing lanes immediately private DrivableVehicle _vehicle; public bool AtEndOfTrack; public RacingAIDriver(VehicleDescription vehicleDescriptor) { _vehicle = new DrivableVehicle(vehicleDescriptor); _vehicle.SteeringSpeed = 6; _vehicle.AutoDrift = false; Vehicle = _vehicle; _firstLaneChangeAllowed = Engine.Instance.Random.Next(5, 40); } public override void Update(List otherDrivers) { _vehicle.ThrottlePedalInput = 0.9f; _vehicle.BrakePedalInput = 0; var node = _vehicle.CurrentNode; var pos = _vehicle.Position; if (node.Next == null || node.Next.Next == null) { _vehicle.ThrottlePedalInput = 0; _vehicle.BrakePedalInput = 1; AtEndOfTrack = true; return; } FollowTrack(); foreach (var otherDriver in otherDrivers) { if (otherDriver == this) continue; //if (!(otherDriver is AIDriver)) continue; // if I am not in the same lane, ignore if (otherDriver is AIDriver && ((AIDriver)otherDriver).VirtualLane != VirtualLane) continue; if (otherDriver is PlayerDriver) continue; //ignore player for now // if I am going slower than the other driver, ignore if (Vehicle.Speed < otherDriver.Vehicle.Speed) continue; var progressDist = otherDriver.Vehicle.TrackPosition - _vehicle.TrackPosition; // if we are only slightly behind another driver (less than 2 nodes back) then consider them a possible danger if (progressDist > 0 && progressDist < 2f) { // pick a new lane if we're far enough from the start of the race. if (Vehicle.CurrentNode.Number > _firstLaneChangeAllowed) { if (Engine.Instance.Random.Next() % 2 == 0) VirtualLane = Math.Max(0, VirtualLane - 1); else VirtualLane = Math.Min(MaxVirtualLanes, VirtualLane + 1); } else { Vehicle.Speed = Math.Max(0, otherDriver.Vehicle.Speed * 0.8f); } } } _vehicle.Update(); } public void Render() { } } } ================================================ FILE: OpenNFS1/Vehicles/AI/IDriver.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenNFS1.Physics; namespace OpenNFS1.Vehicles.AI { interface IDriver { Vehicle Vehicle { get; } void Update(List otherDrivers); } } ================================================ FILE: OpenNFS1/Vehicles/AI/TrafficDriver.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Parsers.Track; using OpenNFS1.Physics; using OpenNFS1.Tracks; namespace OpenNFS1.Vehicles.AI { enum TrafficDriverDirection { Forward, Backward } class TrafficDriver : AIDriver { TrafficDriverDirection _direction; public TrafficDriver(string cfmFile, TrafficDriverDirection direction) { Vehicle = new Vehicle(cfmFile); //Vehicle.SteeringSpeed = 7; _direction = direction; if (direction == TrafficDriverDirection.Forward) { VirtualLane = MaxVirtualLanes - 1; } else { VirtualLane = 1; } } public override Vector3 GetNextTarget() { if (_direction == TrafficDriverDirection.Forward) return Vector3.Lerp(Vehicle.CurrentNode.Next.Next.GetLeftVerge2(), Vehicle.CurrentNode.Next.Next.GetRightVerge2(), (float)VirtualLane / (MaxVirtualLanes)); else return Vector3.Lerp(Vehicle.CurrentNode.Prev.Prev.GetLeftVerge2(), Vehicle.CurrentNode.Prev.Prev.GetRightVerge2(), (float)VirtualLane / (MaxVirtualLanes)); } protected override void FollowTrack() { float angle = Utility.GetSignedAngleBetweenVectors(Vehicle.Direction, GetNextTarget() - Vehicle.Position, true); // if we're more than 90 degrees off course, set immediately. (where we've just placed a traffic car on the track) if (Math.Abs(angle) > MathHelper.PiOver2) { Vector3 newDir = Vector3.Normalize(GetNextTarget() - Vehicle.Position); newDir.Y = 0; Vehicle.Direction = Vector3.Normalize(newDir); return; } base.FollowTrack(); } public override void Update(List otherDrivers) { Vehicle.Speed = 50; var node = Vehicle.CurrentNode; if (node.Next == null || node.Next.Next == null || node.Prev == null || node.Prev.Prev == null) { return; } if (_direction == TrafficDriverDirection.Backward) { } FollowTrack(); foreach (var otherDriver in otherDrivers) { if (otherDriver == this) continue; // if I am not in the same lane, ignore if (otherDriver is AIDriver && ((AIDriver)otherDriver).VirtualLane != VirtualLane) continue; // if we are going slower than the other driver, ignore if (Vehicle.Speed < otherDriver.Vehicle.Speed) continue; var progressDist = otherDriver.Vehicle.TrackPosition - Vehicle.TrackPosition; if (_direction == TrafficDriverDirection.Backward) progressDist = -progressDist; // if we are only slightly behind another driver (less than 2 nodes back) then consider them a possible danger if (progressDist > 0 && progressDist < 2f) { // slow down immediately Vehicle.Speed = Math.Max(0, otherDriver.Vehicle.Speed * 0.9f); } } Vehicle.Update(); } } } ================================================ FILE: OpenNFS1/Vehicles/CarMesh.cs ================================================ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Parsers; using GameEngine; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenNFS1.Vehicles { class CarMesh : Mesh { Polygon _rightRearWheel, _leftRearWheel, _rightFrontWheel, _leftFrontWheel; Polygon _leftBrakeLight, _rightBrakeLight; Texture2D _wheelTexture, _brakeOnTexture, _brakeOffTexture; static Color BrakeOffColor = new Color(88, 10, 5); //color of brake-off light color... static Color BrakeOnColor = new Color(158, 110, 6); //color of brake-off light color... public CarMesh(MeshChunk meshChunk, BitmapChunk bmpChunk, Color brakeColor) : base(meshChunk, bmpChunk) { foreach (var poly in _polys) { switch (poly.Label) { case "rt_rear": _rightRearWheel = poly; break; case "lt_rear": _leftRearWheel = poly; break; case "rt_frnt": _rightFrontWheel = poly; break; case "lt_frnt": _leftFrontWheel = poly; break; case "bkll": _leftBrakeLight = poly; break; case "bklr": _rightBrakeLight = poly; break; } } var tyreEntry = bmpChunk.FindByName("tyr1"); if (tyreEntry != null) _wheelTexture = tyreEntry.Texture; // This seems like it should be done in a shader but I couldn't get it to work well enough // (dealing with original paletted colors doesn't work so well in a texture stretched over a polygon) var rsidPoly = _polys.FirstOrDefault(a => a.TextureName == "rsid"); if (rsidPoly != null) { // Generate a new texture for brake lights on. Color[] pixels = new Color[rsidPoly.Texture.Width * rsidPoly.Texture.Height]; rsidPoly.Texture.GetData(pixels); for (int i = 0; i < pixels.Length; i++) { if (pixels[i] == brakeColor) pixels[i] = BrakeOnColor; } _brakeOnTexture = new Texture2D(Engine.Instance.Device, rsidPoly.Texture.Width, rsidPoly.Texture.Height); _brakeOnTexture.SetData(pixels); // Generate a new texture for brake lights off. for (int i = 0; i < pixels.Length; i++) { if (pixels[i] == BrakeOnColor) pixels[i] = BrakeOffColor; } _brakeOffTexture = new Texture2D(Engine.Instance.Device, _leftBrakeLight.Texture.Width, _leftBrakeLight.Texture.Height); _brakeOffTexture.SetData(pixels); } } public Vector3 LeftFrontWheelPos { get { return GetWheelAxlePoint(_leftFrontWheel); } } public Vector3 RightFrontWheelPos { get { return GetWheelAxlePoint(_rightFrontWheel); } } public Vector3 LeftRearWheelPos { get { return GetWheelAxlePoint(_leftRearWheel); } } public Vector3 RightRearWheelPos { get { return GetWheelAxlePoint(_rightRearWheel); } } private Vector3 GetWheelAxlePoint(Polygon wheelPoly) { float y = (wheelPoly.Vertices.Max(a => a.Y) + wheelPoly.Vertices.Min(a => a.Y)) / 2; float z = (wheelPoly.Vertices.Max(a => a.Z) + wheelPoly.Vertices.Min(a => a.Z)) / 2; // X value is always the same return new Vector3(wheelPoly.Vertices[0].X, y, z); } public float RearWheelSize { get { return _leftRearWheel.Vertices.Max(a => a.Y) - _leftRearWheel.Vertices.Min(a => a.Y); } } public float FrontWheelSize { get { return _leftFrontWheel.Vertices.Max(a => a.Y) - _leftFrontWheel.Vertices.Min(a => a.Y); } } public Texture2D WheelTexture { get { return _wheelTexture; } } public void Render(Effect effect, bool enableBrakeLights) { Engine.Instance.Device.SetVertexBuffer(_vertexBuffer); effect.CurrentTechnique.Passes[0].Apply(); foreach (Polygon poly in _polys) { if (poly == _rightFrontWheel || poly == _leftFrontWheel || poly == _leftRearWheel || poly == _rightRearWheel) { continue; } else if (_brakeOnTexture != null && poly.TextureName == "rsid") { Engine.Instance.Device.Textures[0] = enableBrakeLights ? _brakeOnTexture : _brakeOffTexture; } else { Engine.Instance.Device.Textures[0] = poly.Texture; } Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleList, poly.VertexBufferIndex, poly.VertexCount / 3); } } } } ================================================ FILE: OpenNFS1/Vehicles/CarModelCache.cs ================================================ using System; using System.Collections.Generic; using System.Text; using OpenNFS1.Parsers; namespace OpenNFS1.Vehicles { static class CarModelCache { static Dictionary _cache = new Dictionary(); public static CarMesh GetCfm(string filename) { if (!_cache.ContainsKey(filename)) { CfmFile cfm = new CfmFile(filename); _cache.Add(filename, cfm.Mesh); return cfm.Mesh; } else { return _cache[filename]; } } } } ================================================ FILE: OpenNFS1/Vehicles/PlayerDriver.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using OpenNFS1.Physics; namespace OpenNFS1.Vehicles.AI { class PlayerDriver : IDriver { DrivableVehicle _vehicle; public Vehicle Vehicle { get { return _vehicle; } } public PlayerDriver(DrivableVehicle vehicle) { _vehicle = vehicle; } public void Update(List otherDrivers) { _vehicle.ThrottlePedalInput = VehicleController.Acceleration; _vehicle.BrakePedalInput = VehicleController.Brake; _vehicle.SteeringInput= VehicleController.Turn; _vehicle.GearDownInput = VehicleController.GearDown; _vehicle.GearUpInput = VehicleController.GearUp; _vehicle.HandbrakeInput = VehicleController.Handbrake; _vehicle.Update(); } public void Render() { } } } ================================================ FILE: OpenNFS1/Vehicles/Traffic/TrafficController.cs ================================================ using System; using System.Collections.Generic; using System.Text; using OpenNFS1.Parsers.Track; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Physics; using OpenNFS1.Tracks; using Microsoft.Xna.Framework.Graphics; using OpenNFS1.Vehicles.AI; namespace OpenNFS1.Vehicles { class TrafficController { Race _race; List _traffic = new List(); static string[] _trafficModels; static TrafficController() { _trafficModels = new string[] { @"SIMDATA\CARFAMS\axxess.CFM", @"SIMDATA\CARFAMS\vandura.CFM", @"SIMDATA\CARFAMS\bmw.CFM", @"SIMDATA\CARFAMS\copmust.CFM", @"SIMDATA\CARFAMS\crx.CFM", @"SIMDATA\CARFAMS\jeep.CFM", @"SIMDATA\CARFAMS\jetta.CFM", @"SIMDATA\CARFAMS\lemans.CFM", @"SIMDATA\CARFAMS\pickup.CFM", @"SIMDATA\CARFAMS\probe.CFM", @"SIMDATA\CARFAMS\rodeo.CFM", @"SIMDATA\CARFAMS\sunbird.CFM", @"SIMDATA\CARFAMS\wagon.CFM" }; } public TrafficController(Race race) { _race = race; } public void Update() { var player = _race.Player; for (int i = _traffic.Count - 1; i >= 0; i--) { var driver = _traffic[i]; // too far from player if (driver.Vehicle.CurrentNode.Number < player.Vehicle.CurrentNode.Number - 30 || driver.Vehicle.CurrentNode.Number > player.Vehicle.CurrentNode.Number + 140) { _race.Drivers.Remove(driver); _traffic.Remove(driver); continue; } // end of track, just stop if (driver.Vehicle.CurrentNode.Next == null || driver.Vehicle.CurrentNode.Next.Next == null) { driver.Vehicle.Speed = 0; driver.AtEndOfTrack = true; } // start of track just stop if (driver.Vehicle.CurrentNode.Prev == null || driver.Vehicle.CurrentNode.Prev.Prev == null) { driver.Vehicle.Speed = 0; //_race.Drivers.Remove(driver); //_traffic.Remove(driver); //continue; } } // if player is close to the start or end of track, don't spawn new traffic if (_race.Player.Vehicle.CurrentNode.Number < 20 || _race.Player.Vehicle.CurrentNode.Number > _race.Track.RoadNodes.Count - 20) return; while (_traffic.Count < 5) { int cfmIndex = Engine.Instance.Random.Next(_trafficModels.Length); // about 1/3rd of cars should go backwards var direction = Engine.Instance.Random.Next() % 3 == 0 ? TrafficDriverDirection.Backward : TrafficDriverDirection.Forward; var driver = new TrafficDriver(_trafficModels[cfmIndex], direction); int distanceFromPlayer; //if (direction == TrafficDriverDirection.Forward) distanceFromPlayer = Engine.Instance.Random.Next(40, 200); //else // distanceFromPlayer = Engine.Instance.Random.Next(-100, -30); int nodeIndex = (_race.Player.Vehicle.CurrentNode.Number + distanceFromPlayer) % _race.Track.RoadNodes.Count - 1; nodeIndex = Math.Max(1, nodeIndex); _race.AddDriver(driver, _race.Track.RoadNodes[nodeIndex]); _traffic.Add(driver); } } } } ================================================ FILE: OpenNFS1/Vehicles/TyreSmokeParticleSystem.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using OpenNFS1.Physics; using GameEngine; using Microsoft.Xna.Framework.Graphics; namespace OpenNFS1 { class TyreSmokeParticleSystem : ParticleSystem { static TyreSmokeParticleSystem _instance; public static TyreSmokeParticleSystem Instance { get { if (_instance == null) { _instance = new TyreSmokeParticleSystem(); _instance.InitializeSystem(); } return _instance; } } protected override void InitializeSettings(ParticleSettings settings) { settings.Texture = Engine.Instance.ContentManager.Load("Content/smoke"); settings.MaxParticles = 800; settings.Duration = TimeSpan.FromSeconds(0.7f); settings.DurationRandomness = 1f; settings.MinHorizontalVelocity = 0; settings.MaxHorizontalVelocity = 5; settings.EmitterVelocitySensitivity = 0.6f; settings.MinVerticalVelocity = 3; settings.MaxVerticalVelocity = 7; settings.Gravity = new Vector3(0, -2, 0); settings.EndVelocity = 0.75f; //settings.MinRotateSpeed = -1; //settings.MaxRotateSpeed = 1; settings.MinStartSize = 4; settings.MaxStartSize = 4; settings.MinEndSize = 10; settings.MaxEndSize = 30; } } } ================================================ FILE: OpenNFS1/Vehicles/Vehicle.cs ================================================ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using OpenNFS1; using OpenNFS1.Dashboards; using OpenNFS1.Physics; using OpenNFS1.Vehicles; using GameEngine; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using OpenNFS1.Parsers.Track; using OpenNFS1.Tracks; using System.Diagnostics; namespace OpenNFS1.Vehicles { class Vehicle { public const float MaxSteeringLock = 0.3f; const float Gravity = 9.81f; public float SteeringSpeed = 2.1f; public Track Track { get; set; } public Vector3 Position { get; set; } public Vector3 Direction { get; set; } public Vector3 Up { get; set; } public float Speed { get; set; } public TrackNode CurrentNode { get; private set; } public float TrackPosition { get; set; } protected CarMesh _model; protected AlphaTestEffect _effect; public float _steeringWheel; // inputs public float SteeringInput; protected float _previousSpeed; protected float _currentHeightOfTrack; protected float _rotationChange = 0.0f; protected bool _isOnGround = true; float _upVelocity = 0; float _timeInAir = 0; public Vector3 Right { get { return Vector3.Cross(Direction, Up); } } public BoundingSphere BoundingSphere { get { return new BoundingSphere(Position, _model.BoundingBox.Max.Z - _model.BoundingBox.Min.Z); } } public Vehicle(string cfmFile) { _model = CarModelCache.GetCfm(cfmFile); _effect = new AlphaTestEffect(Engine.Instance.Device); Direction = Vector3.Forward; Up = Vector3.Up; } public void PlaceOnTrack(Track t, TrackNode startNode) { Track = t; CurrentNode = startNode; Position = Vector3.Lerp(startNode.GetLeftVerge(), startNode.GetRightVerge2(), 0.5f); //center of node } public virtual void Update() { if (CurrentNode.Next == null || CurrentNode.Prev == null) return; float elapsedSeconds = Engine.Instance.FrameTime; Direction = Vector3.TransformNormal(Direction, Matrix.CreateFromAxisAngle(Up, _rotationChange)); Position += Speed * Direction * Engine.Instance.FrameTime * 2.5f; UpdateSteering(); UpdateTrackNode(); if (CurrentNode.Next == null || CurrentNode.Prev == null) return; TrackNode node = null, nextNode = null; if (Distance2d(Position, CurrentNode.Next.Position) < Distance2d(Position, CurrentNode.Prev.Position)) { node = CurrentNode; nextNode = CurrentNode.Next; } else { node = CurrentNode.Prev; nextNode = CurrentNode; } FollowTrackOrientation(node, nextNode); ApplyGravity(); } private void UpdateTrackNode() { var nextNode = CurrentNode.Next; var prevNode = CurrentNode.Prev; if (!Utility.IsLeftOfLine(nextNode.GetLeftBoundary(), nextNode.GetRightBoundary(), Position)) { CurrentNode = CurrentNode.Next; //Debug.WriteLine("passed node - new node " + CurrentNode.Number); } else if (prevNode != null && Utility.IsLeftOfLine(prevNode.GetLeftBoundary(), prevNode.GetRightBoundary(), Position)) { CurrentNode = prevNode; //Debug.WriteLine("passed node (back) - new node " + CurrentNode.Number); } } private void FollowTrackOrientation(TrackNode node, TrackNode nextNode) { var closestPoint1 = Utility.GetClosestPointOnLine(node.GetLeftBoundary(), node.GetRightBoundary(), Position); var closestPoint2 = Utility.GetClosestPointOnLine(nextNode.GetLeftBoundary(), nextNode.GetRightBoundary(), Position); var dist = Distance2d(closestPoint1, closestPoint2); var carDist = Distance2d(closestPoint1, Position); float ratio = Math.Min(carDist / dist, 1.0f); TrackPosition = CurrentNode.Number + ratio; // if the road is sloping downwards and we have enough speed, unstick from ground if (node.Slope - nextNode.Slope > 50 && Speed > 100 && _isOnGround) { _isOnGround = false; _upVelocity = -0.4f; } if (_isOnGround) { Up = Vector3.Lerp(node.Up, nextNode.Up, ratio); Up = Vector3.Normalize(Up); Direction = Vector3.Cross(Up, Right); Direction.Normalize(); } _currentHeightOfTrack = MathHelper.Lerp(closestPoint1.Y, closestPoint2.Y, ratio); if (_currentHeightOfTrack == -9999) { throw new Exception(); } if (_isOnGround) { var newPosition = Position; newPosition.Y = _currentHeightOfTrack; Position = newPosition; } //GameConsole.WriteLine("height: " + _position.Y, 0); //GameConsole.WriteLine("ratio: " + ratio, 1); } private void UpdateSteering() { float elapsedSeconds = Engine.Instance.FrameTime; if (SteeringInput < 0) { _steeringWheel += SteeringSpeed * elapsedSeconds * SteeringInput; _steeringWheel = Math.Max(_steeringWheel, -MaxSteeringLock); } else if (SteeringInput > 0) { _steeringWheel += SteeringSpeed * elapsedSeconds * SteeringInput; _steeringWheel = Math.Min(_steeringWheel, MaxSteeringLock); } else { if (_steeringWheel > 0.01f) _steeringWheel -= SteeringSpeed * elapsedSeconds * 0.9f; else if (_steeringWheel < -0.01f) _steeringWheel += SteeringSpeed * elapsedSeconds * 0.9f; else _steeringWheel = 0; } if (_isOnGround) { _rotationChange = _steeringWheel * 0.05f; if (Math.Abs(Speed) < 2) _rotationChange = 0; if (Speed > 0) _rotationChange *= -1; } HandleExtraSteeringPhysics(); } public virtual void HandleExtraSteeringPhysics() { } private void ApplyGravity() { if (_isOnGround) return; bool wasOnGround = _isOnGround; _isOnGround = Position.Y < _currentHeightOfTrack; if (!_isOnGround) { var newPosition = Position; newPosition.Y -= Gravity * 10f * _timeInAir * Engine.Instance.FrameTime; Position = newPosition; // slowly pitch the nose of the car downwards - helps to flatten out the jump and looks better if (_timeInAir > 0.3f && Direction.Y > -0.3f) { var newDirection = Direction; newDirection.Y -= _timeInAir * 0.006f; Direction = Vector3.Normalize(newDirection); } } if (_isOnGround && !wasOnGround) { if (_timeInAir > 0.2f) { OnGroundHit(); } } if (_isOnGround) _timeInAir = 0; else { _timeInAir += Engine.Instance.FrameTime; _upVelocity -= Engine.Instance.FrameTime * 100; } } public virtual void OnGroundHit() { } public void Reset() { Position = CurrentNode.Position; Direction = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(MathHelper.ToRadians(CurrentNode.Orientation))); Speed = 0; } public virtual void Render() { _effect.View = Engine.Instance.Camera.View; _effect.Projection = Engine.Instance.Camera.Projection; _effect.World = GetRenderMatrix(); Engine.Instance.Device.RasterizerState = RasterizerState.CullNone; Engine.Instance.Device.BlendState = BlendState.Opaque; _effect.CurrentTechnique.Passes[0].Apply(); _model.Render(_effect); } public virtual Matrix GetRenderMatrix() { Matrix orientation = Matrix.Identity; orientation.Right = Right; orientation.Up = Up; orientation.Forward = Direction; return orientation * Matrix.CreateTranslation(Position); } public static float Distance2d(Vector3 pos1, Vector3 pos2) { pos1.Y = pos2.Y = 0; return Vector3.Distance(pos1, pos2); } } } ================================================ FILE: OpenNFS1/Vehicles/VehicleDescription.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenNFS1.Vehicles { class VehicleDescription { public string Name; public string UIImageFile; public string ModelFile; public string SoundBnkFile; public int Horsepower; public float Redline; public int Mass; public static List Descriptions; static VehicleDescription() { Descriptions = new List(); Descriptions.Add(new VehicleDescription { Name = "RX7", UIImageFile = "rx71.qfs", ModelFile = @"SIMDATA\CARFAMS\mrx7.cfm", SoundBnkFile = "RX7_SW.bnk", Horsepower = 255, //4, Mass = 1280, Redline = 8f }); Descriptions.Add(new VehicleDescription { Name = "NSX", UIImageFile = "nsx1.qfs", ModelFile = @"SIMDATA\CARFAMS\ansx.cfm", SoundBnkFile = "NSX_SW.bnk", Horsepower = 270, Mass = 1380, Redline = 7.5f }); Descriptions.Add(new VehicleDescription { Name = "Supra", UIImageFile = "sup1.qfs", ModelFile = @"SIMDATA\CARFAMS\tsupra.cfm", SoundBnkFile = "SUPRA_SW.bnk", Horsepower = 320, // was 6 (x46) Mass = 1580, Redline = 7f }); Descriptions.Add(new VehicleDescription { Name = "911", UIImageFile = "9111.qfs", ModelFile = @"SIMDATA\CARFAMS\p911.cfm", SoundBnkFile = "911_SW.bnk", Horsepower = 270, Mass = 1380, Redline = 6.6f }); Descriptions.Add(new VehicleDescription { Name = "ZR1", UIImageFile = "vet1.qfs", ModelFile = @"SIMDATA\CARFAMS\czr1.cfm", SoundBnkFile = "ZR1_SW.bnk", Horsepower = 405, Mass = 1380, Redline = 6.5f }); Descriptions.Add(new VehicleDescription { Name = "Viper", UIImageFile = "vip1.qfs", ModelFile = @"SIMDATA\CARFAMS\dviper.cfm", SoundBnkFile = "VIPERSW.bnk", Horsepower = 400, Mass = 1380, Redline = 6f }); Descriptions.Add(new VehicleDescription { Name = "F512", UIImageFile = "5121.qfs", ModelFile = @"SIMDATA\CARFAMS\f512tr.cfm", SoundBnkFile = "TR512_SW.bnk", Horsepower = 421, Mass = 1380, Redline = 8f }); Descriptions.Add(new VehicleDescription { Name = "Diablo", UIImageFile = "dia1.qfs", ModelFile = @"SIMDATA\CARFAMS\ldiabl.cfm", SoundBnkFile = "DIABLOSW.bnk", Horsepower = 490, Mass = 1380, Redline = 7.5f }); Descriptions.Add(new VehicleDescription { Name = "Warrior", UIImageFile = "war1.qfs", ModelFile = @"SIMDATA\CARFAMS\traffc.cfm", SoundBnkFile = "TRAFFC.bnk", Horsepower = 700, Mass = 1380, Redline = 7f }); } } } ================================================ FILE: OpenNFS1/Vehicles/WheelModel.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Parsers; namespace OpenNFS1 { static class WheelModel { static VertexPositionTexture[] _cylinderVertices; static VertexBuffer _cylinderVertexBuffer; static AlphaTestEffect _effect; static Texture2D _rubberTexture; static WheelModel() { CreateGeometry(); _effect = new AlphaTestEffect(Engine.Instance.Device); _effect.VertexColorEnabled = false; _rubberTexture = new Texture2D(Engine.Instance.Device, 1, 1); _rubberTexture.SetData(new Color[] { new Color(0.1f, 0.1f, 0.1f) }); } public static void BeginBatch() { Engine.Instance.Device.SetVertexBuffer(_cylinderVertexBuffer); _effect.View = Engine.Instance.Camera.View; _effect.Projection = Engine.Instance.Camera.Projection; } public static void Render(Matrix world, Texture2D texture) { // render rubber tire part _effect.World = world; _effect.Texture = _rubberTexture; _effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleList, 0, 288 / 3); // render the sides (hubs) _effect.Texture = texture; _effect.VertexColorEnabled = false; _effect.CurrentTechnique.Passes[0].Apply(); Engine.Instance.Device.DrawPrimitives(PrimitiveType.TriangleList, 288, 4); } private static void CreateGeometry() { int numSides = 32; Vector3 bottomCenter = new Vector3(0, -.5f, 0); Vector3 topCenter = new Vector3(0, .5f, 0); Vector3 currentVector; Vector3 nextVector; float xPos1, xPos2, zPos1, zPos2; float radius = 0.51f; _cylinderVertices = new VertexPositionTexture[numSides * 9 + 12]; float angleChange = (float)Math.PI / (numSides / 2); float angle; Color c = new Color(0.1f, 0.1f, 0.1f); //rubber color for (int k = 0; k < numSides; k++) { angle = k * angleChange; xPos1 = (float)Math.Cos(angle); xPos2 = (float)Math.Cos(angle + angleChange); zPos1 = (float)Math.Sin(angle); zPos2 = (float)Math.Sin(angle + angleChange); currentVector = new Vector3(xPos1, 0, zPos1); nextVector = new Vector3(xPos2, 0, zPos2); _cylinderVertices[k * 9 + 0] = new VertexPositionTexture(topCenter + currentVector * radius, Vector2.Zero); _cylinderVertices[k * 9 + 1] = new VertexPositionTexture(bottomCenter + currentVector * radius, Vector2.Zero); _cylinderVertices[k * 9 + 2] = new VertexPositionTexture(bottomCenter + nextVector * radius, Vector2.Zero); _cylinderVertices[k * 9 + 3] = new VertexPositionTexture(bottomCenter + nextVector * radius, Vector2.Zero); _cylinderVertices[k * 9 + 5] = new VertexPositionTexture(topCenter + currentVector * radius, Vector2.Zero); _cylinderVertices[k * 9 + 4] = new VertexPositionTexture(topCenter + nextVector * radius, Vector2.Zero); } // Add the sides of the wheel to the cylinder float y = 0.505f; Vector3 bottomLeftFront = new Vector3(-0.5f, y, 0.5f); Vector3 bottomRightFront = new Vector3(0.5f, y, 0.5f); Vector3 bottomLeftBack = new Vector3(-0.5f, y, -0.5f); Vector3 bottomRightBack = new Vector3(0.5f, y, -0.5f); c = Color.White; _cylinderVertices[288] = new VertexPositionTexture(bottomLeftFront, new Vector2(0.0f, 0.0f)); _cylinderVertices[289] = new VertexPositionTexture(bottomLeftBack, new Vector2(0.0f, 1.0f)); _cylinderVertices[290] = new VertexPositionTexture(bottomRightBack, new Vector2(1.0f, 1.0f)); _cylinderVertices[291] = new VertexPositionTexture(bottomLeftFront, new Vector2(0.0f, 0.0f)); _cylinderVertices[292] = new VertexPositionTexture(bottomRightBack, new Vector2(1.0f, 1.0f)); _cylinderVertices[293] = new VertexPositionTexture(bottomRightFront, new Vector2(1.0f, 0.0f)); _cylinderVertices[294] = new VertexPositionTexture(new Vector3(-0.5f, -y, 0.5f), new Vector2(0.0f, 1.0f)); _cylinderVertices[295] = new VertexPositionTexture(new Vector3(0.5f, -y, -0.5f), new Vector2(1.0f, 0.0f)); _cylinderVertices[296] = new VertexPositionTexture(new Vector3(-0.5f, -y, -0.5f), new Vector2(0.0f, 0.0f)); _cylinderVertices[297] = new VertexPositionTexture(new Vector3(-0.5f, -y, 0.5f), new Vector2(0.0f, 1.0f)); _cylinderVertices[298] = new VertexPositionTexture(new Vector3(0.5f, -y, 0.5f), new Vector2(1.0f, 1.0f)); _cylinderVertices[299] = new VertexPositionTexture(new Vector3(0.5f, -y, -0.5f), new Vector2(1.0f, 0.0f)); _cylinderVertexBuffer = new VertexBuffer(Engine.Instance.Device, typeof(VertexPositionTexture), _cylinderVertices.Length, BufferUsage.WriteOnly); _cylinderVertexBuffer.SetData(_cylinderVertices); } } } ================================================ FILE: OpenNFS1/Views/BaseExternalView.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GameEngine; using OpenNFS1.Parsers; using OpenNFS1.Physics; namespace OpenNFS1.Views { class BaseExternalView { static BitmapEntry _bottomBar, _bottomFill, _tacho, _map; static Texture2D _tachLineTexture; const int _size = 160; const float _needleLength = 2.5f; const float _needleWidth = 3f; public BaseExternalView() { if (_bottomBar == null) { var fsh = new FshFile(@"Simdata\Misc\MaskHi.fsh"); _bottomBar = fsh.Header.Bitmaps.Find(a => a.Id == "b00b"); _bottomFill = fsh.Header.Bitmaps.Find(a => a.Id == "0002"); _tacho = fsh.Header.Bitmaps.Find(a => a.Id == "tach"); _map = fsh.Header.Bitmaps.Find(a => a.Id == "mpbd"); _tachLineTexture = new Texture2D(Engine.Instance.Device, (int)_needleWidth, 25); Color[] pixels = new Color[_tachLineTexture.Width * _tachLineTexture.Height]; for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.Red; _tachLineTexture.SetData(pixels); } } public void RenderBackground(DrivableVehicle car) { float carRpm = car.Motor.Rpm / car.Motor.RedlineRpm; Engine.Instance.SpriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, RasterizerState.CullNone); // Draw tacho Color color = new Color(255, 255, 255, 200); Engine.Instance.SpriteBatch.Draw(_tacho.Texture, new Rectangle(_tacho.Misc[2], _tacho.Misc[3], _size, _size), Color.White); float rotation = (float)(carRpm * Math.PI * 1.4f) - 2.56f; Engine.Instance.SpriteBatch.Draw(_tachLineTexture, new Vector2(_tacho.Misc[2] + _size / 2, _tacho.Misc[3] + _size / 2), null, color, rotation, new Vector2(_needleWidth/2, 25), new Vector2(1f, _needleLength), SpriteEffects.None, 0); // mini-map overlay //Engine.Instance.SpriteBatch.Draw(_map.Texture, _map.GetDisplayAt(), Color.White); // Draw bottom fill const int barHeight = 17; Engine.Instance.SpriteBatch.Draw(_bottomBar.Texture, new Vector2(0, 400), new Rectangle(0, 0, 640, barHeight), Color.White); Engine.Instance.SpriteBatch.Draw(_bottomFill.Texture, new Vector2(0, 400 + barHeight), new Rectangle(0, 0, 640, 60), Color.White); Engine.Instance.SpriteBatch.End(); } } } ================================================ FILE: OpenNFS1/Views/BumperView.cs ================================================ using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Physics; namespace OpenNFS1.Views { class BumperView : BaseExternalView, IView { DrivableVehicle _car; SimpleCamera _camera; public BumperView(DrivableVehicle car) { _car = car; _camera = new SimpleCamera(); _camera.FieldOfView = GameConfig.FOV; _camera.FarPlaneDistance = GameConfig.DrawDistance; } #region IView Members public bool Selectable { get { return true; } } public bool ShouldRenderPlayer { get { return false; } } public void Activate() { Engine.Instance.Camera = _camera; } public void Deactivate() { } public void Update(GameTime gameTime) { _camera.Position = _car.Position + _car.Direction * 2.8f + new Vector3(0, 5, 0); _camera.LookAt = _camera.Position + _car.Direction * 60f + new Vector3(0, _car.BodyPitch.Position, 0); _camera.UpVector = _car.Up; // +new Vector3(_car.Roll.Position * 0.2f, 0, 0); //float f = 1.8f; // _car.Roll.Position; //_camera.Up = _car.Up + _car.Direction * new Vector3(f, f, f); } public void Render() { RenderBackground(_car); } #endregion } } ================================================ FILE: OpenNFS1/Views/ChaseView.cs ================================================ using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Physics; namespace OpenNFS1.Views { class ChaseView : BaseExternalView, IView { DrivableVehicle _car; FixedChaseCamera _camera; public bool ShouldRenderPlayer { get { return true; } } public ChaseView(DrivableVehicle car, int distance, int height, int offset) { _car = car; _camera = new FixedChaseCamera(); _camera.FieldOfView = GameConfig.FOV; _camera.FarPlaneDistance = GameConfig.DrawDistance; _camera.ChaseDistance = distance; _camera.ChaseHeight = height; _camera.ChaseOffset = offset; } #region IView Members public bool Selectable { get { return true; } } public void Activate() { Engine.Instance.Camera = _camera; _camera.Position = _car.Position; _camera.ChaseDirection = _car.Direction; } public void Deactivate() { } public void Update(GameTime gameTime) { _camera.Position = _car.Position; _camera.ChaseDirection = _car.Direction; _camera.UpVector = _car.Up; } public void Render() { RenderBackground(_car); } #endregion } } ================================================ FILE: OpenNFS1/Views/DashboardView.cs ================================================ using System.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using GameEngine; using OpenNFS1.Dashboards; using OpenNFS1.Physics; using OpenNFS1.Vehicles; using OpenNFS1.Views; namespace OpenNFS1 { class DashboardView : IView { DrivableVehicle _car; SimpleCamera _camera; private Dashboard _dashboard; public DashboardView(DrivableVehicle car) { _car = car; _camera = new SimpleCamera(); _camera.FieldOfView = GameConfig.FOV; _camera.FarPlaneDistance = GameConfig.DrawDistance; var dashfile = Path.GetFileNameWithoutExtension(car.Descriptor.ModelFile) + "dh.fsh"; var dashDescription = DashboardDescription.Descriptions.Find(a => a.Filename == dashfile); _dashboard = new Dashboard(car, dashDescription); } #region IView Members public bool Selectable { get { return true; } } public bool ShouldRenderPlayer { get { return false; } } public void Update(GameTime gameTime) { _camera.Position = _car.Position + new Vector3(0, 5, 0); _camera.LookAt = _camera.Position + _car.RenderDirection * 60f + new Vector3(0, _car.BodyPitch.Position, 0); _camera.UpVector = _car.Up; // +new Vector3(_car.Roll.Position * 0.2f, 0, 0); _dashboard.Update(gameTime); } public void Render() { Engine.Instance.SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); _dashboard.Render(); Engine.Instance.SpriteBatch.End(); } public void Activate() { Engine.Instance.Camera = _camera; _dashboard.IsVisible = true; } public void Deactivate() { _dashboard.IsVisible = false; } #endregion } } ================================================ FILE: OpenNFS1/Views/DebugView.cs ================================================ using Microsoft.Xna.Framework; using GameEngine; using OneAmEngine; using OpenNFS1.Physics; using OpenNFS1.Views; namespace OpenNFS1 { class DebugView : IView { DrivableVehicle _car; FPSCamera _camera; public DebugView(DrivableVehicle car) { _car = car; _camera = new FPSCamera(); } #region IView Members public bool Selectable { get { return false; } } public bool ShouldRenderPlayer { get { return true; } } public void Update(GameTime gameTime) { _camera.Update(gameTime); } public void Render() { } public void Activate() { Engine.Instance.Camera = _camera; _camera.Position = _car.Position; } public void Deactivate() { } #endregion } } ================================================ FILE: OpenNFS1/Views/DropCameraView.cs ================================================ using System; using Microsoft.Xna.Framework; using GameEngine; using OpenNFS1.Physics; using OpenNFS1.Tracks; namespace OpenNFS1.Views { class DropCameraView : BaseExternalView, IView { const int MaxCameraDistance = 15; DrivableVehicle _car; SimpleCamera _camera; TrackNode _cameraNode; public DropCameraView(DrivableVehicle car) { _car = car; _camera = new SimpleCamera(); _camera.FieldOfView = GameConfig.FOV; _camera.FarPlaneDistance = GameConfig.DrawDistance; PositionCameraAtNode(car.Track.RoadNodes[10]); } #region IView Members public bool Selectable { get { return true; } } public bool ShouldRenderPlayer { get { return true; } } public void Activate() { Engine.Instance.Camera = _camera; } public void Deactivate() { } public void Update(GameTime gameTime) { if (Math.Abs(_car.CurrentNode.Number - _cameraNode.Number) > MaxCameraDistance) { int nextNode = (_car.CurrentNode.Number + MaxCameraDistance) % _car.Track.RoadNodes.Count; PositionCameraAtNode(_car.Track.RoadNodes[nextNode]); } _camera.LookAt = _car.Position; } private void PositionCameraAtNode(TrackNode node) { _cameraNode = node; _camera.Position = Engine.Instance.Random.Next() % 2 == 0 ? _cameraNode.GetLeftBoundary() : _cameraNode.GetRightBoundary(); _camera.Position = _camera.Position + new Vector3(0, Engine.Instance.Random.Next(15, 50), 0); } public void Render() { RenderBackground(_car); } #endregion } } ================================================ FILE: OpenNFS1/Views/IView.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; namespace OpenNFS1.Views { interface IView { bool Selectable { get; } bool ShouldRenderPlayer { get; } void Update(GameTime gameTime); void Render(); void Activate(); void Deactivate(); } } ================================================ FILE: OpenNFS1/gameconfig.json ================================================ { "fullScreen": false, "cdDataPath": "CD_Data", "drawDistance": 5000, "respectOpenRoadCheckpoints": true, "drawDebugInfo": false } ================================================ FILE: OpenNFS1.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 VisualStudioVersion = 12.0.31101.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNFS1", "OpenNFS1\OpenNFS1.csproj", "{2BBFF57F-1C9D-430E-8343-D2662437114D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameEngine", "Engine\GameEngine.csproj", "{F66B2F9A-AF38-40F9-A094-522C823D04EE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Android|Any CPU = Android|Any CPU Android|Mixed Platforms = Android|Mixed Platforms Android|x86 = Android|x86 Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms Debug|x86 = Debug|x86 iOS|Any CPU = iOS|Any CPU iOS|Mixed Platforms = iOS|Mixed Platforms iOS|x86 = iOS|x86 Linux|Any CPU = Linux|Any CPU Linux|Mixed Platforms = Linux|Mixed Platforms Linux|x86 = Linux|x86 OSX|Any CPU = OSX|Any CPU OSX|Mixed Platforms = OSX|Mixed Platforms OSX|x86 = OSX|x86 PSM|Any CPU = PSM|Any CPU PSM|Mixed Platforms = PSM|Mixed Platforms PSM|x86 = PSM|x86 Release|Any CPU = Release|Any CPU Release|Mixed Platforms = Release|Mixed Platforms Release|x86 = Release|x86 Windows|Any CPU = Windows|Any CPU Windows|Mixed Platforms = Windows|Mixed Platforms Windows|x86 = Windows|x86 Windows8|Any CPU = Windows8|Any CPU Windows8|Mixed Platforms = Windows8|Mixed Platforms Windows8|x86 = Windows8|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2BBFF57F-1C9D-430E-8343-D2662437114D}.Android|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Android|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Android|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Android|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Android|x86.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Debug|Any CPU.ActiveCfg = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Debug|Mixed Platforms.Build.0 = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Debug|x86.ActiveCfg = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Debug|x86.Build.0 = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.iOS|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.iOS|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.iOS|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.iOS|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.iOS|x86.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Linux|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Linux|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Linux|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Linux|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Linux|x86.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.OSX|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.OSX|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.OSX|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.OSX|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.OSX|x86.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.PSM|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.PSM|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.PSM|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.PSM|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.PSM|x86.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Release|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Release|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Release|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Release|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Release|x86.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows|Mixed Platforms.ActiveCfg = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows|Mixed Platforms.Build.0 = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows|x86.ActiveCfg = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows|x86.Build.0 = Debug|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows8|Any CPU.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows8|Mixed Platforms.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows8|Mixed Platforms.Build.0 = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows8|x86.ActiveCfg = Release|x86 {2BBFF57F-1C9D-430E-8343-D2662437114D}.Windows8|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Android|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Android|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Android|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Android|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Android|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Debug|Any CPU.ActiveCfg = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Debug|Mixed Platforms.Build.0 = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Debug|x86.ActiveCfg = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Debug|x86.Build.0 = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.iOS|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.iOS|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.iOS|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.iOS|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.iOS|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Linux|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Linux|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Linux|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Linux|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Linux|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.OSX|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.OSX|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.OSX|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.OSX|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.OSX|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.PSM|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.PSM|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.PSM|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.PSM|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.PSM|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Release|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Release|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Release|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Release|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Release|x86.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows|Mixed Platforms.ActiveCfg = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows|Mixed Platforms.Build.0 = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows|x86.ActiveCfg = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows|x86.Build.0 = Debug|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows8|Any CPU.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows8|Mixed Platforms.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows8|Mixed Platforms.Build.0 = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows8|x86.ActiveCfg = Release|x86 {F66B2F9A-AF38-40F9-A094-522C823D04EE}.Windows8|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal ================================================ FILE: TnfsSeSpex.txt ================================================ ============================================================================ THE UNOFFICIAL NEED FOR SPEED - SE FILE FORMAT SPECIFICATIONS - Version 0.2 Copyright(c)1996, Ian Brown - 101735.527@compuserve.com Portions Copyright(c)1995-1996, Dennis Auroux (MXK) - auroux@clipper.ens.fr ============================================================================ This file forms an addendum to the original NFS File Format specifications, available from the following URL: http://www.eleves.ens.fr:8080/home/auroux/nfsspecs.txt The most recent version of this file is available from: http://ourworld.compuserve.com/homepages/ianbrown/nfsse_sp.txt E - APPENDIX -- NFS-SE changes =========================== E.1 GAMEDATA\CONFIG\PATHS.DAT ------------------------- This file is essentially the same as for TNFS except for one addition. The Track FAM files in NFSSE now come in 2 varieties, a normal file in \simdata\ntrackfam and an internationalised file \simdata\etrackfam (English) or \simdata\gtrackfam (German). The pattern of entries, then, is now as follows: 000 gamedata/config/ 2D0 F:\simdata\misc\ 050 gamedata/savegame/ 320 F:\simdata\etrackfam\ 0A0 F:\frontend\speech\ 370 F:\simdata\gtrackfam\ 0F0 F:\simdata\soundbnk\ 3C0 F:\simdata\slides\ 140 F:\frontend\music\ 410 F:\simdata\carfams\ 190 F:\frontend\art\ 460 F:\simdata\soundbnk\ 1E0 gamedata/modem/ 4B0 F:\simdata\carspecs\ 230 F:\frontend\movielow\ 500 F:\simdata\dash\ 280 gamedata/replay/ 550 F:\frontend\misc\ 5A0 F:\frontend\show\ E.2 SIMDATA\MISC\*.TRI ------------------ These files describe the track itself, including the shape of the road and the position of the scenery items. The objects are referenced by numbers which correspond to entries in the corresponding files in SIMDATA\ETRACKFAM, and SIMDATA\NTRACKFAM. In order to understand the structure of TRI files you have to know that a track is the superposition of three structures : - first, a 'virtual road' : this is a sequence of points in space, which will be called 'nodes'. These points correspond to successive positions along the track, and all the cars have to pass near each of these positions. During the game, the virtual road is invisible, but you have to stay close to it. - second, the scenery : this is a collection of points in space, which will be called 'vertices', together with textures which are mapped onto the polygons defined by consecutive vertices. These textures are used to draw the road, the roadside and part of the landscape. Thus it is of course preferable that the scenery remain close to the virtual road ! - and last, the objects : points in space together with bitmaps, which are used for road signs, buildings, trees, etc... Some of them are plain 2D bitmaps, but others have a more sophisticated polygonal 3D structure. The 3D coordinates x,y,z will always be used with the following meaning : - x is an axis which is transversal to the starting line. Positive x values correspond to points which are on the right of the starting position. (i.e. if you start looking to the north, x points to the east) - y is an axis which is parallel to the starting line. Positive y values correspond to points which are ahead of the starting position. (i.e. y points to the north) - z is a vertical axis. Positive z values correspond to points higher than the starting position. a) TRI files begin with 98Ch bytes of headers and index tables. These are as follows : offset len data ------ --- ---- 00 4 ? 04 8 ? 0C 4 x coordinate of the first node 10 4 z coordinate of the first node 14 4 y coordinate of the first node 18 4 ? 1C 4 ? 20 4 ? 24 4 length of the scenery data (in bytes : 120h per record) 28 4 ? 2C 960h first index table The first index table is a succession of 32-bit offsets. It follows an arithmetic progression by 120h as a general rule. This means the first value is 0, followed by 548h, A90h, etc... On closed tracks, when the end is reached, the offsets start again with 0, 548h, A90h, etc... (so that the end of a lap is connected with the beginning of the following one !). The table is filled to 960h bytes (600 entries) with zero values. These offsets are added to the base address of the scenery data to get an absolute offset into the file. b) The virtual road data follows, and is constituted of 36-byte records (one for each node). The first record is at offset 98Ch, and the last allowed record is at offset 15B0Ch (this leaves room for 2400 records, and since a scenery block corresponds to four nodes, both capacities are equal). The unused records (after the last node) are filled with zero values. The structure of each record is the following : offset len data ------ --- ---- 00 1 a0 01 1 a1 02 1 a2 03 1 a3 04 1 b0 05 1 b1 06 1 b2 07 1 b3 08 4 x coordinate 0C 4 z coordinate 10 4 y coordinate 14 2 slope 16 2 slant-A 18 2 orientation 1A 2 0 1C 2 y-orientation 1E 2 slant-B 20 2 x-orientation 22 2 0 - a0,a1,a2,a3 are 8-bit values which specify the width of the main road, and the width of the finging area. a0 = distance from the virtual road to the left hand verge a1 = distance from the virtual road to the right hand verge a2 = distance from virtual road to the absolute left hand edge a3 = distanec from virtual road to the absolute right hand edge - b0,b1,b2,b3 are 8-bit values of unknown purpose. - x, z and y coordinates are signed long values. - slope is a value indicating the slope at the current node, i.e. the difference between the z coordinates of two consecutive nodes. A good approximation is : slope(i) = (z(i+1) - z(i))/152. However, to complicate things, it is stored as a signed 14-bit value, complemented to 4000h. This means -1 is stored as 3FFFh, -2 as 3FFEh, ... So in fact you must perform a logical and with 3FFFh before storing ! - slant-A is a value indicating how the road is slanted to the left or to the right (as in the turns in Autumn Valley or Lost Vegas). It is a signed 14-bit value, like slope. The value is positive if the road is slanted to the right, negative if it is slanted to the left. - slant-B has the same purpose, but is a standard signed 16-bit value. Its value is positive for the left, negative for the right. The approximative relation between slant-A and slant-B is slant-B = -12.3 slant-A (remember that slant-A is 14-bit, though) - orientation is a 14-bit value, and is equal to 0 for north (increasing y), 1000h for east (increasing x), 2000h for south (decreasing y), 3000h for west (decreasing x), and back to 3FFFh for north. - y-orientation is a signed 16-bit value, which is proportional to the y coordinate variation. Meanwhile, x-orientation is proportional to the *opposite* of the x coordinate variation. This means that the couple (-xorientation,yorientation) gives the orientation of the track. The norm (square root of xorient^2+yorient^2) is usually around 32000 (a little less than 8000h, to avoid numeric overflows), but can fluctuate with the only condition that (-xor,yor) gives the correct orientation. c) The objects data comes next. There are first several distinct zones, many of which seem to be unused (?) : offset len data ------ --- ---- 15B0C 708h 3-byte records (there are 600... as many as scenery blocks ?) 16214 4 40h (?) 16218 4 3E8h = 1000 (size of the main block in records) 1621C 4 'SJOB' 16220 4 428Ch (total length of the remaining blocks) 16224 400h 16-byte records (unknown purpose) 16624 4 ? 16628 3E80h object data : 1000 records of 16 bytes (one per object) The object data itself consists of a 16-byte record per object. The record structure is the following : offset len data ------ --- ---- 00 4 reference node 04 1 bitmap number 05 1 flip 06 4 flags (unknown purpose) 0A 2 relative x coordinate 0C 2 relative z coordinate 0E 2 relative y coordinate Each object is related to a reference node in the virtual road. The x,z,y coordinates are then expressed as signed 16-bit values relative to the coordinates of the reference node. Beware that the axes are simply translated but not rotated ! (i.e. the x and y axes are still pointing east and north) The objects are sorted in the order of increasing reference nodes. A reference node value of -1 indicates that the record is unused (i.e. after the end of the used records). Also note that the coordinates are not expressed in the same unit as the 32-bit absolute coordinates seen above (the units are much larger, so that the value fits in 16 bits). The 8-bit flip value is equal to 0 for an object that is perfectly perpendi- cular to the track (e.g. a road sign), larger values for objects that are slightly turned, until 64 for an object that is mapped along the track (e.g. an ad on the side of the road), then up to 128 which is the perfectly reversed position (since the objects have no "back", this is the common way of reversing a road sign for a turn in the other direction), then up to 192 which is again longitudinal mapping (the other way) and until 255 which is back to the normal position. The bitmap number corresponds to a texture in the corresponding .FAM file (see B.8). The relevant bitmaps are in the second chunk of the .FAM file. Two cases can occur : - closed tracks : the second chunk is a 'wwww' structure containing a single subchunk which is in turn a SHPI directory where the entry corres- ponding to bitmap #n is called "nn00" where nn is n written in decimal. (e.g. bitmap #18 is "1800"). Furthermore the object called "!pal" or "!PAL", when it exists, is the corresponding palette (256 3-byte entries) ; FFh is transparent. - open roads : the second chunk contains a subchunk per bitmap, and each subchunk is a SHPI containing at least the object "0000" (the bitmap), and possibly a palette ("!pal" or "!PAL"). Object #n is then the bitmap "0000" in the subchunk #n (the first subchunk is #0). One must add to these 2D objects (plain bitmaps) the 3D objects described in the fourth chunk of the .FAM file. They usually correspond to numbers above the last 2D object ; however it happens, in closed tracks, that some of the 3D objects are given numbers inside the range used by 2D objects. In that case, the numbers describing 2D objects are shifted upwards. (i.e. the bitmap "4400" corresponds to object #45 or #46). This phenomenon apparently does not occur for open roads, where the 3D objects always follow the 2D objects. Furthermore, certain consecutive bitmaps represent successive states of an animated object. In that case, the game will display successively the relevant bitmaps. Note that if the second bitmap is given instead of the first, the animation does not occur. d) The scenery data starts at offset 1A4A8h. It is made up of records of size 120h, each corresponding to four nodes in the virtual road. The records are consecutive and the last record ends the .TRI file. Each record has the following structure : offset len data ------ --- ---- 000 4 'TRKD' 004 4 114h = length of the record contents 008 4 00000000h 00C 2 ? 00E 10 textures 018 12 reference point 024 6 point A0 02A 6 point A1 ... .. ........ 05C 6 point A9 060 6 point A10 066 6 point B0 06C 6 point B1 ... .. ........ 09E 6 point B9 0A2 6 point B10 0A8 6 point C0 0AE 6 point C1 ... .. ........ 0DE 6 point C9 0E4 6 point C10 0EA 6 point D0 0F0 6 point D1 ... .. ........ 120 6 point D9 126 6 point D10 12C 6 point E0 130 6 point E1 ... .. ........ 162 6 point E9 168 6 point E10 Each point is given by three signed 16-bit relative coordinates (x,z,y as usual). The coordinates are in the same reference frame as in the virtual road data. The coordinates are relative to the virtual track point as given in the virtual track data. The points A0,...,A10 in record #n (starting with 0) correspond to the node #4n (starting with 0) in the virtual road data. B0,...,B10 correspond to node #4n+1, C0...C10 to node #4n+2, D0...D10 to node #4n+3 and E0...E10 to node #4n+4. Thus the points E0...E10 are identical to the points A0...A10 of the following record. The eleven point series (0 to 10) are arranged as follows : A0-E0 are near the middle of the road, and thus close to the corresponding nodes. A1-E1 are a little to the right, A2-E2 further right, ... until A5-E5. A6-E6 are a little to the left, A7-E7 further left, ... until A10-E10. (In tunnels, the points A5-E5 and A10-E10 get back to the center, consti- tuting the ceiling). To each record correspond ten textures (coded at the beginning), each given by a 8-bit value, T1,T2,...,T10. T1 is used between A0-E0 and A1-E1, T2 between A1-E1 and A2-E2, ..., T5 between A4-E4 and A5-E5 ; meanwhile, T6 is used between A0-E0 and A6-E6, T7 between A6-E6 and A7-E7, ..., T10 between A9-E9 and A10-E10. This is summarized on the following diagram : E10---E9---E8---E7---E6---E0---E1---E2---E3---E4---E5 node 4n+4 | | | | | || | | | | | | | | | | || | | | | | D10 D9 D8 D7 D6 D0 D1 D2 D3 D4 D5 node 4n+3 | T | T | T | T | T || T | T | T | T | T | | | | | | || | | | | | C10 C9 C8 C7 C6 C0 C1 C2 C3 C4 C5 node 4n+2 | 10 | 9 | 8 | 7 | 6 || 1 | 2 | 3 | 4 | 5 | | | | | | || | | | | | B10 B9 B8 B7 B6 B0 B1 B2 B3 B4 B5 node 4n+1 | | | | | || | | | | | | | | | | || | | | | | A10---A9---A8---A7---A6---A0---A1---A2---A3---A4---A5 node 4n ^ the nodes are here | The texture numbers are converted to bitmaps in the first chunk of the .FAM file (see B.8). There are two different cases : - closed tracks : the first chunk is a 'wwww' structure which contains a single subchunk which is in turn a SHPI bitmap directory, possibly with a palette '!PAL' or '!pal'. There is also often a bitmap called 'ga00' or 'GA00' (unknown interpretation). The names have the structure "xxls", where xx is a decimal value indicating the texture group, l is 'A', 'B' or 'C', and s indicates a scale ('0' is the largest, while '3'&'4' are very small). The various scales are here to speed up the texture-mapping algorithm, anyway the only texture that is always present is with s=0. The xx and l values correspond to a texture number n in the following way : n=3xx if l='A', 3xx+1 if l='B' and 3xx+2 if l='C'. Note that there are holes in the numbering : many numbers do not have a bitmap. Examples : bitmap "03C0" corresponds to texture #11 (3x3+2) at the largest scale; bitmap "14A1" corresponds to texture #42 (3x14) at the second scale available. - open roads : the first chunk contains a subchunk per texture group (i.e. the xx value is now the number of the subchunk, starting with 0). Each subchunk is a SHPI directory containing potentially a palette, and bitmaps labelled "l00s", where l is 'A','B' or 'C' and s is the scale. As before, n=3xx if l='A', 3xx+1 if l='B', 3xx+2 if l='C', and there are holes in the numbering. Examples : texture #11 at scale '0' is now the bitmap "C000" in subchunk #3. Texture #42 at scale '1' is now the bitmap called "A001" in subchunk #14. F - APPENDIX -- Compression Formats ================================ Compressed data files start with a 5 byte header. offset len data ------ --- ---- 000 1 Pack Code Hi byte 001 1 Pack Code Lo byte (== FBh or 32h) 002 1 Expanded length Hi byte 003 1 Expanded length Mid byte 004 1 Expanded length Lo byte If bit 0 of the pack code Hi byte (ie Offset 0 & 0x01) is set, then there are now 3 padding bytes to allow the data to begin on a 32 bit boundary. Otherwise, the data starts at offset 5. The interpretation of the data is different for different pack codes. In the description that follows, it is assumed that bit zero of the pack code Hi byte has been cleared. F.1 Pack code == 10FBh, or 1032h ---------------------------- This is the pack code used by the .QFS files, and it indicates LZ77 compression has been used. To decode, we read and decode chunks as per the following C code: /************************************************************************* Function: ReadPackFile (CFile&) Purpose: Reads in and unpacks the specified file Returns: A pointer to the unpacked data if successful. Comments: A null pointer is returned on error *************************************************************************/ #include "stdafx.h" #include "pack.h" // Note that we rely here on the characteristics of an // overlapping 'copy up' of bytes. Hence we cannot use // the memcpy library function. unsigned char* ReadPackFile(CFile& file) { int filesize = file.GetLength(); unsigned char *pSourceData = (unsigned char *)malloc(filesize); if (NULL == pSourceData) return(NULL); int bytesread = file.Read(pSourceData, filesize); if (bytesread != filesize){ free(pSourceData); return(NULL); } int PackCode = (pSourceData[0]&0xfe)*256 + pSourceData[1]; if (PackCode != 0x10fb){ free(pSourceData); return(NULL); // Invalid pack code } int ExpandedLength = (pSourceData[2] << 16) + (pSourceData[3] << 8) + pSourceData[4]; int filepos = 5; int TargetOffset = 0; if (pSourceData[0] & 0x01) filepos = 8; // align if necessary. unsigned char *pExpandedData = (unsigned char *)malloc(ExpandedLength); if (pExpandedData){ while(filepos < filesize && pSourceData[filepos] < 0xfc){ unsigned char pack_byte = pSourceData[filepos]; int a = pSourceData[filepos+1]; int b = pSourceData[filepos+2]; if (!(pack_byte & 0x80)){ int len = pack_byte&0x03; unsigned char *pDest = pExpandedData+TargetOffset; unsigned char *pSrc = pSourceData+filepos+2; TargetOffset += len; filepos += (len+2); while (len--) *pDest++ = *pSrc++; len = ((pack_byte & 0x1c)>>2) + 3; int offset = (pack_byte >> 5) + a + 1; pDest = pExpandedData + TargetOffset; pSrc = pDest-offset; TargetOffset += len; while (len--) *pDest++ = *pSrc++; } else if (!(pack_byte & 0x40)){ int len = (a >> 6) &0x03; unsigned char *pDest = pExpandedData + TargetOffset; unsigned char *pSrc = pSourceData+filepos+3; filepos += (len+3); TargetOffset += len; while (len--) *pDest++ = *pSrc++; int offset = (a&0x3f)*256 + b + 1; pDest = pExpandedData + TargetOffset; pSrc = pDest-offset; len = (pack_byte & 0x3f)+4; TargetOffset += len; while (len--) *pDest++ = *pSrc++; } else if (!(pack_byte & 0x20)){ int c = pSourceData[filepos+3]; int len = (pack_byte & 0x03); unsigned char *pDest = pExpandedData + TargetOffset; unsigned char *pSrc = pSourceData+filepos+4; filepos += (len+4); TargetOffset += len; while (len--) *pDest++ = *pSrc++; int offset = ((pack_byte & 0x10)<<0x0c) + 256*a + b + 1; pDest = pExpandedData + TargetOffset; pSrc = pDest-offset; len = ((pack_byte >> 2)&0x03)*256+c+5; TargetOffset += len; while (len--) *pDest++ = *pSrc++; } else { int len = (pack_byte&0x1f)*4+4; unsigned char *pDest = pExpandedData + TargetOffset; unsigned char *pSrc = pSourceData+filepos+1; filepos += (len+1); TargetOffset += len; while (len--) *pDest++ = *pSrc++; } } if (filepos < filesize && TargetOffset < ExpandedLength){ unsigned char *pDest = pExpandedData + TargetOffset; unsigned char *pSrc = pSourceData+filepos+1; int len = pSourceData[filepos]&0x03; while (len--) *pDest++ = *pSrc++; } } free(pSourceData); return(pExpandedData); } ================================================ FILE: readme.md ================================================ ## OpenNFS1 OpenNFS1 is a ground-up remake of the original EA Need for Speed 1. The code is all written from scratch without reverse engineering executables, and it uses the original data files that were on the CD back in 1995! The format of the various binary data files was worked out by Ian Brown, Denis Auroux and myself. ## Main features * Written in C# and XNA. (Now converted to Monogame) * 16 tracks with scenery, animations, tunnels * 9 drivable cars with animated dashboards * Can drive tracks backwards (not allowed in the original) * Can drive past the finish signs on open road stages to see the real end of the track (probably never seen by anyone except by the original developers!) ## Requires * OpenAL * Monogame >= 3.2.0 * .NET 4 ### Build from source ``` git clone https://github.com/jeff-1amstudios/OpenNFS1.git ``` Open OpenNFS1.sln in Visual Studio ### Installer You can also install the last stable binary from the [Releases page](https://github.com/jeff-1amstudios/OpenNFS1/releases) ## Legal: Models, textures, tracks, cars by Pioneer Productions / EA Seattle (C) 1995. OpenNFS1 is not affiated in any way with EA or Pioneer Productions ================================================ FILE: reverse_engineering.txt ================================================ 2 left lane / 1 nothing 0 left lane \ 3 nothing (default) Lost Vegas: Inside (ambient noise): 17,17,0,4 Normal: 17,0,0,3 Cobbled road: 17,0,0,5 Twisty section: 17,1,0,3 Alpine: Normal: 17,17,34,3 Some bits: 17,1,34,3 Lane change: 17,17,34,0 Wide lane: 18,17,34,1 ... some 18,17,34,1 Uphill wide lane: 18,17,34,16 In tunnel: 17,0,0,4 Coastal 1 node 288 - 18,17,2,1 x,x,0,0 move A6-A8 verts 1 to the right if the associated terrainRow is #1 in its segment x,x,0,1 no effect x,x,0,2 move A6-A8 verts 1 to the right if the associated terrainRow is last in its segment x,x,0,3 no effect x,x,0,4 play tunnel sound effect 5 cobbled road sound effect (Lost Vegas) 6 no effect 7 tunnel take last right strip and put it between A2-A9 x,x,0,8 .. left wall appeared to move across the track - snapped back as we got closer x,x,0,9 tunnel (vertigo track) take last left strip and put it between A7-A4 x,x,0,10 no effect x,x,0,11 no effect x,x,0,12 tunnel (vertigo track) take last left strip and put it between A9-A4 x,x,0,13 tunnel - take last left strip and put it between A9-A5 14 waterfall (autumn valley) left channel 15 waterfall (autumn valley) right channel x,x,0,16 no effect 17 water left channel 18 water right channel x,x,0,32 crash 20,x,x,x - distance cars should drive from boundary? 10 - cars going forwards all in center of road, no cars going backwards 5 - same as 10 Tunnels: Not based on vertex locations Not based on textureId Appears to join the last / 2nd last left to second-last right A8 - A4