using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; using TrueCraft.Client.Interface; using System.IO; using System.Net; using TrueCraft.API; using TrueCraft.Client.Rendering; using System.Linq; using System.ComponentModel; using TrueCraft.Core.Networking.Packets; using TrueCraft.API.World; using System.Collections.Concurrent; using TrueCraft.Client.Input; using TrueCraft.Core; using MonoGame.Utilities.Png; using System.Diagnostics; using TrueCraft.Core.Logic.Blocks; namespace TrueCraft.Client { public class TrueCraftGame : Game { private MultiplayerClient Client { get; set; } private GraphicsDeviceManager Graphics { get; set; } private List Interfaces { get; set; } private FontRenderer Pixel { get; set; } private SpriteBatch SpriteBatch { get; set; } private IPEndPoint EndPoint { get; set; } private ChunkRenderer ChunkConverter { get; set; } private DateTime LastPhysicsUpdate { get; set; } private DateTime NextPhysicsUpdate { get; set; } private List ChunkMeshes { get; set; } public ConcurrentBag PendingMainThreadActions { get; set; } private ConcurrentBag IncomingChunks { get; set; } public ChatInterface ChatInterface { get; set; } public DebugInterface DebugInterface { get; set; } private RenderTarget2D RenderTarget; private BoundingFrustum CameraView; private Camera Camera; private bool MouseCaptured; private KeyboardComponent KeyboardComponent { get; set; } private MouseComponent MouseComponent { get; set; } private GameTime GameTime { get; set; } private Microsoft.Xna.Framework.Vector3 Delta { get; set; } private TextureMapper TextureMapper { get; set; } private double Bobbing { get; set; } private Coordinates3D HighlightedBlock { get; set; } private Mesh HighlightMesh { get; set; } private Texture2D HighlightTexture { get; set; } private BasicEffect OpaqueEffect, HighlightEffect; private AlphaTestEffect TransparentEffect; public static readonly int Reach = 5; public TrueCraftGame(MultiplayerClient client, IPEndPoint endPoint) { Window.Title = "TrueCraft"; Content.RootDirectory = "Content"; Graphics = new GraphicsDeviceManager(this); Graphics.SynchronizeWithVerticalRetrace = false; Graphics.IsFullScreen = UserSettings.Local.IsFullscreen; Graphics.PreferredBackBufferWidth = UserSettings.Local.WindowResolution.Width; Graphics.PreferredBackBufferHeight = UserSettings.Local.WindowResolution.Height; Client = client; EndPoint = endPoint; LastPhysicsUpdate = DateTime.MinValue; NextPhysicsUpdate = DateTime.MinValue; ChunkMeshes = new List(); IncomingChunks = new ConcurrentBag(); PendingMainThreadActions = new ConcurrentBag(); MouseCaptured = true; Bobbing = 0; var keyboardComponent = new KeyboardComponent(this); KeyboardComponent = keyboardComponent; Components.Add(keyboardComponent); var mouseComponent = new MouseComponent(this); MouseComponent = mouseComponent; Components.Add(mouseComponent); } protected override void Initialize() { Interfaces = new List(); SpriteBatch = new SpriteBatch(GraphicsDevice); base.Initialize(); // (calls LoadContent) ChunkConverter = new ChunkRenderer(Client.World, this, Client.World.World.BlockRepository); Client.ChunkLoaded += (sender, e) => ChunkConverter.Enqueue(e.Chunk); //Client.ChunkModified += (sender, e) => ChunkConverter.Enqueue(e.Chunk, true); ChunkConverter.MeshCompleted += ChunkConverter_MeshGenerated; ChunkConverter.Start(); Client.PropertyChanged += HandleClientPropertyChanged; Client.Connect(EndPoint); var centerX = GraphicsDevice.Viewport.Width / 2; var centerY = GraphicsDevice.Viewport.Height / 2; Mouse.SetPosition(centerX, centerY); Camera = new Camera(GraphicsDevice.Viewport.AspectRatio, 70.0f, 0.1f, 1000.0f); UpdateCamera(); Window.ClientSizeChanged += (sender, e) => CreateRenderTarget(); MouseComponent.Move += OnMouseComponentMove; KeyboardComponent.KeyDown += OnKeyboardKeyDown; KeyboardComponent.KeyUp += OnKeyboardKeyUp; CreateRenderTarget(); } private void InitializeHighlightedBlock() { const int size = 64; HighlightedBlock = -Coordinates3D.One; HighlightTexture = new Texture2D(GraphicsDevice, size, size); var colors = new Color[size * size]; for (int i = 0; i < colors.Length; i++) colors[i] = Color.Transparent; for (int x = 0; x < size; x++) colors[x] = Color.Black; // Top for (int x = 0; x < size; x++) colors[x + (size - 1) * size] = Color.Black; // Bottom for (int y = 0; y < size; y++) colors[y * size] = Color.Black; // Left for (int y = 0; y < size; y++) colors[y * size + (size - 1)] = Color.Black; // Right HighlightTexture.SetData(colors); var texcoords = new[] { Vector2.UnitX + Vector2.UnitY, Vector2.UnitY, Vector2.Zero, Vector2.UnitX }; int[] indicies; var verticies = BlockRenderer.CreateUniformCube(Microsoft.Xna.Framework.Vector3.Zero, texcoords, VisibleFaces.All, 0, out indicies, Color.White); HighlightMesh = new Mesh(this, verticies, indicies); } private void CreateRenderTarget() { RenderTarget = new RenderTarget2D(GraphicsDevice, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, false, GraphicsDevice.PresentationParameters.BackBufferFormat, DepthFormat.Depth24); } void ChunkConverter_MeshGenerated(object sender, RendererEventArgs e) { IncomingChunks.Add(e.Result); } void HandleClientPropertyChanged(object sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { case "Position": UpdateCamera(); var sorter = new ChunkRenderer.ChunkSorter(new Coordinates3D( (int)Client.Position.X, 0, (int)Client.Position.Z)); PendingMainThreadActions.Add(() => ChunkMeshes.Sort(sorter)); break; } } protected override void LoadContent() { // Ensure we have default textures loaded. TextureMapper.LoadDefaults(GraphicsDevice); // Load any custom textures if needed. TextureMapper = new TextureMapper(GraphicsDevice); if (UserSettings.Local.SelectedTexturePack != TexturePack.Default.Name) TextureMapper.AddTexturePack(TexturePack.FromArchive(Path.Combine(TexturePack.TexturePackPath, UserSettings.Local.SelectedTexturePack))); Pixel = new FontRenderer( new Font(Content, "Fonts/Pixel", FontStyle.Regular), new Font(Content, "Fonts/Pixel", FontStyle.Bold), null, // No support for underlined or strikethrough yet. The FontRenderer will revert to using the regular font style. null, // (I don't think BMFont has those options?) new Font(Content, "Fonts/Pixel", FontStyle.Italic)); Interfaces.Add(ChatInterface = new ChatInterface(Client, KeyboardComponent, Pixel)); Interfaces.Add(DebugInterface = new DebugInterface(Client, Pixel)); ChatInterface.IsVisible = true; DebugInterface.IsVisible = true; OpaqueEffect = new BasicEffect(GraphicsDevice); OpaqueEffect.EnableDefaultLighting(); OpaqueEffect.DirectionalLight0.SpecularColor = Color.Black.ToVector3(); OpaqueEffect.DirectionalLight1.SpecularColor = Color.Black.ToVector3(); OpaqueEffect.DirectionalLight2.SpecularColor = Color.Black.ToVector3(); OpaqueEffect.TextureEnabled = true; OpaqueEffect.Texture = TextureMapper.GetTexture("terrain.png"); OpaqueEffect.FogEnabled = true; OpaqueEffect.FogStart = 512f; OpaqueEffect.FogEnd = 1000f; OpaqueEffect.FogColor = Color.CornflowerBlue.ToVector3(); OpaqueEffect.VertexColorEnabled = true; TransparentEffect = new AlphaTestEffect(GraphicsDevice); TransparentEffect.AlphaFunction = CompareFunction.Greater; TransparentEffect.ReferenceAlpha = 127; TransparentEffect.Texture = TextureMapper.GetTexture("terrain.png"); TransparentEffect.VertexColorEnabled = true; InitializeHighlightedBlock(); HighlightEffect = new BasicEffect(GraphicsDevice); HighlightEffect.EnableDefaultLighting(); HighlightEffect.DirectionalLight0.SpecularColor = Color.Black.ToVector3(); HighlightEffect.DirectionalLight1.SpecularColor = Color.Black.ToVector3(); HighlightEffect.DirectionalLight2.SpecularColor = Color.Black.ToVector3(); HighlightEffect.TextureEnabled = true; HighlightEffect.Texture = HighlightTexture; HighlightEffect.VertexColorEnabled = true; //HighlightEffect = new AlphaTestEffect(GraphicsDevice); //HighlightEffect.AlphaFunction = CompareFunction.Greater; //HighlightEffect.ReferenceAlpha = 127; //HighlightEffect.Texture = TextureMapper.GetTexture("terrain.png"); //HighlightEffect.VertexColorEnabled = true; base.LoadContent(); } protected override void OnExiting(object sender, EventArgs args) { ChunkConverter.Stop(); base.OnExiting(sender, args); } private void OnKeyboardKeyDown(object sender, KeyboardKeyEventArgs e) { // TODO: Rebindable keys // TODO: Horizontal terrain collisions switch (e.Key) { // Close game (or chat). case Keys.Escape: if (ChatInterface.HasFocus) ChatInterface.HasFocus = false; else Process.GetCurrentProcess().Kill(); break; // Open chat window. case Keys.T: if (!ChatInterface.HasFocus && ChatInterface.IsVisible) ChatInterface.HasFocus = true; break; // Open chat window. case Keys.OemQuestion: if (!ChatInterface.HasFocus && ChatInterface.IsVisible) ChatInterface.HasFocus = true; break; // Close chat window. case Keys.Enter: if (ChatInterface.HasFocus) ChatInterface.HasFocus = false; break; // Take a screenshot. case Keys.F2: TakeScreenshot(); break; // Toggle debug view. case Keys.F3: DebugInterface.IsVisible = !DebugInterface.IsVisible; break; // Change interface scale. case Keys.F4: foreach (var item in Interfaces) { item.Scale = (InterfaceScale)(item.Scale + 1); if ((int)item.Scale > 2) item.Scale = InterfaceScale.Small; } break; // Move to the left. case Keys.A: case Keys.Left: if (ChatInterface.HasFocus) break; Delta += Microsoft.Xna.Framework.Vector3.Left; break; // Move to the right. case Keys.D: case Keys.Right: if (ChatInterface.HasFocus) break; Delta += Microsoft.Xna.Framework.Vector3.Right; break; // Move forwards. case Keys.W: case Keys.Up: if (ChatInterface.HasFocus) break; Delta += Microsoft.Xna.Framework.Vector3.Forward; break; // Move backwards. case Keys.S: case Keys.Down: if (ChatInterface.HasFocus) break; Delta += Microsoft.Xna.Framework.Vector3.Backward; break; case Keys.Space: if (ChatInterface.HasFocus) break; if (Math.Floor(Client.Position.Y) == Client.Position.Y) // Crappy onground substitute Client.Velocity += TrueCraft.API.Vector3.Up * 0.3; break; // Toggle mouse capture. case Keys.Tab: MouseCaptured = !MouseCaptured; break; } } private void OnKeyboardKeyUp(object sender, KeyboardKeyEventArgs e) { switch (e.Key) { // Stop moving to the left. case Keys.A: case Keys.Left: if (ChatInterface.HasFocus) break; Delta -= Microsoft.Xna.Framework.Vector3.Left; break; // Stop moving to the right. case Keys.D: case Keys.Right: if (ChatInterface.HasFocus) break; Delta -= Microsoft.Xna.Framework.Vector3.Right; break; // Stop moving forwards. case Keys.W: case Keys.Up: if (ChatInterface.HasFocus) break; Delta -= Microsoft.Xna.Framework.Vector3.Forward; break; // Stop moving backwards. case Keys.S: case Keys.Down: if (ChatInterface.HasFocus) break; Delta -= Microsoft.Xna.Framework.Vector3.Backward; break; } } private void OnMouseComponentMove(object sender, MouseMoveEventArgs e) { if (MouseCaptured && IsActive) { var centerX = GraphicsDevice.Viewport.Width / 2; var centerY = GraphicsDevice.Viewport.Height / 2; Mouse.SetPosition(centerX, centerY); var look = new Vector2((centerX - e.X), (centerY - e.Y)) * (float)(GameTime.ElapsedGameTime.TotalSeconds * 30); Client.Yaw += look.X; Client.Pitch += look.Y; Client.Yaw %= 360; Client.Pitch = Microsoft.Xna.Framework.MathHelper.Clamp(Client.Pitch, -89.9f, 89.9f); if (look != Vector2.Zero) UpdateCamera(); } } private void TakeScreenshot() { var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".truecraft", "screenshots", DateTime.Now.ToString("yyyy-MM-dd_H.mm.ss") + ".png"); if (!Directory.Exists(Path.GetDirectoryName(path))) Directory.CreateDirectory(Path.GetDirectoryName(path)); using (var stream = File.OpenWrite(path)) new PngWriter().Write(RenderTarget, stream); ChatInterface.AddMessage(string.Format("Screenshot saved as {0}", Path.GetFileName(path))); } protected override void Update(GameTime gameTime) { GameTime = gameTime; foreach (var i in Interfaces) { i.Update(gameTime); } Mesh mesh; while (IncomingChunks.TryTake(out mesh)) { ChunkMeshes.Add(mesh); } Action action; if (PendingMainThreadActions.TryTake(out action)) action(); IChunk chunk; var adjusted = Client.World.World.FindBlockPosition(new Coordinates3D((int)Client.Position.X, 0, (int)Client.Position.Z), out chunk); if (chunk != null && Client.LoggedIn) { if (chunk.GetHeight((byte)adjusted.X, (byte)adjusted.Z) != 0) Client.Physics.Update(gameTime.ElapsedGameTime); } if (NextPhysicsUpdate < DateTime.UtcNow && Client.LoggedIn) { // NOTE: This is to make the vanilla server send us chunk packets // We should eventually make some means of detecing that we're on a vanilla server to enable this // It's a waste of bandwidth to do it on a TrueCraft server Client.QueuePacket(new PlayerGroundedPacket { OnGround = true }); Client.QueuePacket(new PlayerPositionAndLookPacket(Client.Position.X, Client.Position.Y, Client.Position.Y + MultiplayerClient.Height, Client.Position.Z, Client.Yaw, Client.Pitch, false)); NextPhysicsUpdate = DateTime.UtcNow.AddMilliseconds(1000 / 20); } if (Delta != Microsoft.Xna.Framework.Vector3.Zero) { var lookAt = Microsoft.Xna.Framework.Vector3.Transform( Delta, Matrix.CreateRotationY(Microsoft.Xna.Framework.MathHelper.ToRadians(Client.Yaw))); lookAt.X *= (float)(gameTime.ElapsedGameTime.TotalSeconds * 4.3717); lookAt.Z *= (float)(gameTime.ElapsedGameTime.TotalSeconds * 4.3717); Bobbing += Math.Max(Math.Abs(lookAt.X), Math.Abs(lookAt.Z)); Client.Velocity = new TrueCraft.API.Vector3(lookAt.X, Client.Velocity.Y, lookAt.Z); } else Client.Velocity *= new TrueCraft.API.Vector3(0, 1, 0); UpdateCamera(); base.Update(gameTime); } private void UpdateCamera() { const double bobbingMultiplier = 0.015; var bobbing = Bobbing * 1.5; Camera.Position = new TrueCraft.API.Vector3( Client.Position.X + Math.Cos(bobbing + Math.PI / 2) * bobbingMultiplier - (Client.Size.Width / 2), Client.Position.Y + (Client.Size.Height - 0.5) + Math.Sin(Math.PI / 2 - (2 * bobbing)) * bobbingMultiplier, Client.Position.Z - (Client.Size.Depth / 2)); Camera.Pitch = Client.Pitch; Camera.Yaw = Client.Yaw; CameraView = Camera.GetFrustum(); Camera.ApplyTo(HighlightEffect); Camera.ApplyTo(OpaqueEffect); Camera.ApplyTo(TransparentEffect); } private static readonly BlendState ColorWriteDisable = new BlendState() { ColorWriteChannels = ColorWriteChannels.None }; protected override void Draw(GameTime gameTime) { GraphicsDevice.SetRenderTarget(RenderTarget); GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise; Graphics.GraphicsDevice.Clear(Color.CornflowerBlue); GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp; GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp; int verticies = 0, chunks = 0; GraphicsDevice.DepthStencilState = DepthStencilState.Default; for (int i = 0; i < ChunkMeshes.Count; i++) { if (CameraView.Intersects(ChunkMeshes[i].BoundingBox)) { verticies += ChunkMeshes[i].GetTotalVertices(); chunks++; ChunkMeshes[i].Draw(OpaqueEffect, 0); } } HighlightMesh.Draw(HighlightEffect); GraphicsDevice.BlendState = ColorWriteDisable; for (int i = 0; i < ChunkMeshes.Count; i++) { if (CameraView.Intersects(ChunkMeshes[i].BoundingBox)) { if (!ChunkMeshes[i].IsDisposed) verticies += ChunkMeshes[i].GetTotalVertices(); ChunkMeshes[i].Draw(TransparentEffect, 1); } } GraphicsDevice.BlendState = BlendState.NonPremultiplied; for (int i = 0; i < ChunkMeshes.Count; i++) { if (CameraView.Intersects(ChunkMeshes[i].BoundingBox)) { if (!ChunkMeshes[i].IsDisposed) verticies += ChunkMeshes[i].GetTotalVertices(); ChunkMeshes[i].Draw(TransparentEffect, 1); } } DebugInterface.Vertices = verticies; DebugInterface.Chunks = chunks; HighlightEffect.World = Matrix.CreateScale(1.02f) * Matrix.CreateTranslation(new Microsoft.Xna.Framework.Vector3(0, 43, 0)); HighlightMesh.Draw(HighlightEffect); SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, null); for (int i = 0; i < Interfaces.Count; i++) Interfaces[i].DrawSprites(gameTime, SpriteBatch); SpriteBatch.End(); GraphicsDevice.SetRenderTarget(null); GraphicsDevice.Clear(Color.CornflowerBlue); SpriteBatch.Begin(); SpriteBatch.Draw(RenderTarget, new Vector2(0)); SpriteBatch.End(); base.Draw(gameTime); } protected override void Dispose(bool disposing) { if (disposing) { ChunkConverter.Dispose(); KeyboardComponent.Dispose(); MouseComponent.Dispose(); } base.Dispose(disposing); } } }