ClassiCube/ClassicalSharp/Game/Game.cs

458 lines
No EOL
14 KiB
C#

// Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Net;
using System.Threading;
using ClassicalSharp.Audio;
using ClassicalSharp.Commands;
using ClassicalSharp.Entities;
using ClassicalSharp.Events;
using ClassicalSharp.GraphicsAPI;
using ClassicalSharp.Gui.Screens;
using ClassicalSharp.Map;
using ClassicalSharp.Model;
using ClassicalSharp.Network;
using ClassicalSharp.Particles;
using ClassicalSharp.Renderers;
using ClassicalSharp.Selections;
using ClassicalSharp.Textures;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input;
using OpenTK.Platform;
#if ANDROID
using Android.Graphics;
#endif
using PathIO = System.IO.Path; // Android.Graphics.Path clash otherwise
using BlockID = System.UInt16;
namespace ClassicalSharp {
public partial class Game : IDisposable {
public Game(string username, string mppass, string skinServer, int width, int height) {
string title = Program.AppName + " (" + username + ")";
window = Factory.CreateWindow(width, height, title, GraphicsMode.Default, DisplayDevice.Default);
window.Visible = true;
Username = username;
Mppass = mppass;
this.skinServer = skinServer;
}
public bool ChangeTerrainAtlas(Bitmap atlas) {
if (!ValidateBitmap("terrain.png", atlas)) return false;
if (atlas.Width != atlas.Height && (atlas.Width * 2 != atlas.Height)) {
Chat.Add("&cUnable to use terrain.png from the texture pack.");
Chat.Add("&c Its width is not the same as its height.");
return false;
}
if (Graphics.LostContext) return false;
Atlas1D.Dispose();
Atlas2D.Dispose();
Atlas2D.UpdateState(atlas);
Atlas1D.UpdateState();
Events.RaiseTerrainAtlasChanged();
return true;
}
Stopwatch render_watch = new Stopwatch();
public void Run() {
window.Closed += OnClosed;
window.Resize += OnResize;
OnLoad();
OnResize(null, null);
Utils.LogDebug("Entering main loop.");
render_watch.Start();
while (true) {
window.ProcessEvents();
if (window.Exists && !isExiting) {
// Cap the maximum time drift to 1 second (e.g. when the process is suspended).
double time = render_watch.Elapsed.TotalSeconds;
if (time > 1.0) time = 1.0;
if (time <= 0) continue;
render_watch.Reset();
render_watch.Start();
RenderFrame(time);
} else { return; }
}
}
bool isExiting;
public void Exit() {
isExiting = true;
// TODO: is isExiting right
window.Close();
}
public void SetViewDistance(int distance, bool userDist) {
if (userDist) {
UserViewDistance = distance;
Options.Set(OptionsKey.ViewDist, distance);
}
distance = Math.Min(distance, MaxViewDistance);
if (distance == ViewDistance) return;
ViewDistance = distance;
Events.RaiseViewDistanceChanged();
UpdateProjection();
}
public ScheduledTask AddScheduledTask(double interval, ScheduledTaskCallback callback) {
ScheduledTask task = new ScheduledTask();
task.Interval = interval; task.Callback = callback;
Tasks.Add(task);
return task;
}
public void UpdateProjection() {
DefaultFov = Options.GetInt(OptionsKey.FieldOfView, 1, 150, 70);
Camera.GetProjection(out Graphics.Projection);
Graphics.SetMatrixMode(MatrixType.Projection);
Graphics.LoadMatrix(ref Graphics.Projection);
Graphics.SetMatrixMode(MatrixType.Modelview);
Events.RaiseProjectionChanged();
}
public void Disconnect(string title, string reason) {
World.Reset();
WorldEvents.RaiseOnNewMap();
Gui.SetNewScreen(new DisconnectScreen(this, title, reason));
IDrawer2D.InitCols();
BlockInfo.Reset();
TexturePack.ExtractDefault(this);
for (int i = 0; i < Components.Count; i++) {
Components[i].Reset(this);
}
GC.Collect();
}
public void CycleCamera() {
if (ClassicMode) return;
int i = Cameras.IndexOf(Camera);
i = (i + 1) % Cameras.Count;
Camera = Cameras[i];
Camera.ResetRotOffset();
if (!LocalPlayer.Hacks.CanUseThirdPersonCamera || !LocalPlayer.Hacks.Enabled)
Camera = Cameras[0];
UpdateProjection();
}
public void UpdateBlock(int x, int y, int z, BlockID block) {
BlockID oldBlock = World.GetBlock(x, y, z);
World.SetBlock(x, y, z, block);
WeatherRenderer weather = WeatherRenderer;
if (weather.heightmap != null)
weather.OnBlockChanged(x, y, z, oldBlock, block);
Lighting.OnBlockChanged(x, y, z, oldBlock, block);
// Refresh the chunk the block was located in.
int cx = x >> 4, cy = y >> 4, cz = z >> 4;
MapRenderer.GetChunk(cx, cy, cz).AllAir &= BlockInfo.Draw[block] == DrawType.Gas;
MapRenderer.RefreshChunk(cx, cy, cz);
}
public bool IsKeyDown(Key key) { return Input.IsKeyDown(key); }
public bool IsKeyDown(KeyBind binding) { return Input.IsKeyDown(binding); }
public bool IsMousePressed(MouseButton button) { return Input.IsMousePressed(button); }
public Key Mapping(KeyBind mapping) { return Input.Keys[mapping]; }
public bool CanPick(BlockID block) {
if (BlockInfo.Draw[block] == DrawType.Gas) return false;
if (BlockInfo.Draw[block] == DrawType.Sprite) return true;
if (BlockInfo.Collide[block] != CollideType.Liquid) return true;
return BreakableLiquids && BlockInfo.CanPlace[block] && BlockInfo.CanDelete[block];
}
/// <summary> Reads a bitmap from the stream (converting it to 32 bits per pixel if necessary),
/// and updates the native texture for it. </summary>
public bool UpdateTexture(ref int texId, string file, byte[] data, bool setSkinType) {
using (Bitmap bmp = Platform.ReadBmp(Drawer2D, data)) {
if (!ValidateBitmap(file, bmp)) return false;
Graphics.DeleteTexture(ref texId);
if (setSkinType) SetDefaultSkinType(bmp);
texId = Graphics.CreateTexture(bmp, true, false);
return true;
}
}
void SetDefaultSkinType(Bitmap bmp) {
DefaultPlayerSkinType = Utils.GetSkinType(bmp);
if (DefaultPlayerSkinType == SkinType.Invalid)
throw new NotSupportedException("char.png has invalid dimensions");
Entity[] entities = Entities.List;
for (int i = 0; i < EntityList.MaxCount; i++) {
if (entities[i] == null || entities[i].TextureId != 0) continue;
entities[i].SkinType = DefaultPlayerSkinType;
}
}
public bool ValidateBitmap(string file, Bitmap bmp) {
if (bmp == null) {
Chat.Add("&cError loading " + file + " from the texture pack.");
return false;
}
int maxSize = Graphics.MaxTextureDimensions;
if (bmp.Width > maxSize || bmp.Height > maxSize) {
Chat.Add("&cUnable to use " + file + " from the texture pack.");
Chat.Add("&c Its size is (" + bmp.Width + "," + bmp.Height
+ "), your GPU supports (" + maxSize + "," + maxSize + ") at most.");
return false;
}
if (!Utils.IsPowerOf2(bmp.Width) || !Utils.IsPowerOf2(bmp.Height)) {
Chat.Add("&cUnable to use " + file + " from the texture pack.");
Chat.Add("&c Its size is (" + bmp.Width + "," + bmp.Height
+ "), which is not power of two dimensions.");
return false;
}
return true;
}
public int CalcRenderType(string type) {
if (Utils.CaselessEq(type, "legacyfast")) {
return 0x03;
} else if (Utils.CaselessEq(type, "legacy")) {
return 0x01;
} else if (Utils.CaselessEq(type, "normal")) {
return 0x00;
} else if (Utils.CaselessEq(type, "normalfast")) {
return 0x02;
}
return -1;
}
void UpdateClientSize() {
Size size = window.ClientSize;
Width = Math.Max(size.Width, 1);
Height = Math.Max(size.Height, 1);
}
void OnResize(object sender, EventArgs e) {
UpdateClientSize();
Graphics.OnWindowResize(this);
UpdateProjection();
Gui.OnResize();
}
void OnClosed(object sender, EventArgs e) { isExiting = true; }
void OnNewMapCore(object sender, EventArgs e) {
for (int i = 0; i < Components.Count; i++)
Components[i].OnNewMap(this);
}
void OnNewMapLoadedCore(object sender, EventArgs e) {
for (int i = 0; i < Components.Count; i++)
Components[i].OnNewMapLoaded(this);
}
void TextureChangedCore(object sender, TextureEventArgs e) {
if (e.Name == "terrain.png") {
Bitmap atlas = Platform.ReadBmp(Drawer2D, e.Data);
if (ChangeTerrainAtlas(atlas)) return;
atlas.Dispose();
} else if (e.Name == "default.png") {
Bitmap bmp = Platform.ReadBmp(Drawer2D, e.Data);
Drawer2D.SetFontBitmap(bmp);
Events.RaiseChatFontChanged();
}
}
Stopwatch frameTimer = new Stopwatch();
float limitMilliseconds;
public void SetFpsLimitMethod(FpsLimitMethod method) {
FpsLimit = method;
limitMilliseconds = 0;
Graphics.SetVSync(this, method == FpsLimitMethod.LimitVSync);
if (method == FpsLimitMethod.Limit120FPS)
limitMilliseconds = 1000f / 120;
if (method == FpsLimitMethod.Limit60FPS)
limitMilliseconds = 1000f / 60;
if (method == FpsLimitMethod.Limit30FPS)
limitMilliseconds = 1000f / 30;
}
void LimitFPS() {
if (FpsLimit == FpsLimitMethod.LimitVSync) return;
double elapsed = frameTimer.Elapsed.TotalMilliseconds;
double leftOver = limitMilliseconds - elapsed;
if (leftOver > 0.001) // going faster than limit
Thread.Sleep((int)Math.Round(leftOver, MidpointRounding.AwayFromZero));
}
internal void RenderFrame(double delta) {
frameTimer.Reset();
frameTimer.Start();
Graphics.BeginFrame(this);
Graphics.BindIb(Graphics.defaultIb);
accumulator += delta;
Vertices = 0;
Mode.BeginFrame(delta);
Camera.UpdateMouse();
if (!Focused && !Gui.ActiveScreen.HandlesAllInput) {
Gui.SetNewScreen(new PauseScreen(this));
}
bool allowZoom = Gui.activeScreen == null && !Gui.hudScreen.HandlesAllInput;
if (allowZoom && IsKeyDown(KeyBind.ZoomScrolling)) {
Input.SetFOV(ZoomFov, false);
}
DoScheduledTasks(delta);
float t = (float)(entTask.Accumulator / entTask.Interval);
LocalPlayer.SetInterpPosition(t);
if (!SkipClear) Graphics.Clear();
CurrentCameraPos = Camera.GetPosition(t);
UpdateViewMatrix();
bool visible = Gui.activeScreen == null || !Gui.activeScreen.BlocksWorld;
if (!World.HasBlocks) visible = false;
if (visible) {
Render3D(delta, t);
} else {
SelectedPos.SetAsInvalid();
}
Gui.Render(delta);
if (screenshotRequested) TakeScreenshot();
Mode.EndFrame(delta);
Graphics.EndFrame(this);
LimitFPS();
}
void UpdateViewMatrix() {
Graphics.SetMatrixMode(MatrixType.Modelview);
Camera.GetView(out Graphics.View);
Graphics.LoadMatrix(ref Graphics.View);
Culling.CalcFrustumEquations(ref Graphics.Projection, ref Graphics.View);
}
void Render3D(double delta, float t) {
if (SkyboxRenderer.ShouldRender)
SkyboxRenderer.Render(delta);
AxisLinesRenderer.Render(delta);
Entities.RenderModels(Graphics, delta, t);
Entities.RenderNames(Graphics, delta);
ParticleManager.Render(delta, t);
Camera.GetPickedBlock(SelectedPos); // TODO: only pick when necessary
EnvRenderer.Render(delta);
ChunkUpdater.Update(delta);
MapRenderer.RenderNormal(delta);
MapBordersRenderer.RenderSides(delta);
Entities.DrawShadows();
if (SelectedPos.Valid && !HideGui) {
Picking.Update(SelectedPos);
Picking.Render(delta);
}
// Render water over translucent blocks when underwater for proper alpha blending
Vector3 pos = LocalPlayer.Position;
if (CurrentCameraPos.Y < World.Env.EdgeHeight
&& (pos.X < 0 || pos.Z < 0 || pos.X > World.Width || pos.Z > World.Length)) {
MapRenderer.RenderTranslucent(delta);
MapBordersRenderer.RenderEdges(delta);
} else {
MapBordersRenderer.RenderEdges(delta);
MapRenderer.RenderTranslucent(delta);
}
// Need to render again over top of translucent block, as the selection outline
// is drawn without writing to the depth buffer
if (SelectedPos.Valid && !HideGui && BlockInfo.Draw[SelectedPos.Block] == DrawType.Translucent) {
Picking.Render(delta);
}
SelectionManager.Render(delta);
Entities.RenderHoveredNames(Graphics, delta);
bool left = IsMousePressed(MouseButton.Left);
bool middle = IsMousePressed(MouseButton.Middle);
bool right = IsMousePressed(MouseButton.Right);
Input.PickBlocks(true, left, middle, right);
if (!HideGui) HeldBlockRenderer.Render(delta);
}
void DoScheduledTasks(double time) {
for (int i = 0; i < Tasks.Count; i++) {
ScheduledTask task = Tasks[i];
task.Accumulator += time;
while (task.Accumulator >= task.Interval) {
task.Callback(task);
task.Accumulator -= task.Interval;
}
}
}
void TakeScreenshot() {
if (!Platform.DirectoryExists("screenshots")) {
Platform.DirectoryCreate("screenshots");
}
string timestamp = Utils.LocalNow().ToString("dd-MM-yyyy-HH-mm-ss");
string file = "screenshot_" + timestamp + ".png";
string path = PathIO.Combine("screenshots", file);
using (Stream fs = Platform.FileCreate(path)) {
Graphics.TakeScreenshot(fs, Width, Height);
}
screenshotRequested = false;
Chat.Add("&eTaken screenshot as: " + file);
}
public void Dispose() {
ChunkUpdater.Dispose();
Atlas2D.Dispose();
Atlas1D.Dispose();
ModelCache.Dispose();
Entities.Dispose();
WorldEvents.OnNewMap -= OnNewMapCore;
WorldEvents.OnNewMapLoaded -= OnNewMapLoadedCore;
Events.TextureChanged -= TextureChangedCore;
for (int i = 0; i < Components.Count; i++)
Components[i].Dispose();
Drawer2D.DisposeInstance();
Graphics.Dispose();
// TODO: is this needed
//window.Dispose();
if (!Options.HasChanged()) return;
Options.Load();
Options.Save();
}
}
}