From 11efd1bccedd2ab371708d25ef9a384e748436ff Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Sun, 29 May 2022 18:59:24 +0200 Subject: [PATCH 01/13] fix compile --- src/WorldGenerator.Tweaking/WorldGenerator.Tweaking.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WorldGenerator.Tweaking/WorldGenerator.Tweaking.csproj b/src/WorldGenerator.Tweaking/WorldGenerator.Tweaking.csproj index 8b92158..0b7c573 100644 --- a/src/WorldGenerator.Tweaking/WorldGenerator.Tweaking.csproj +++ b/src/WorldGenerator.Tweaking/WorldGenerator.Tweaking.csproj @@ -3,6 +3,7 @@ Exe net6.0 + 10 From 65e6a48030b9b1e3b36a9340cfc00320ecdc2e62 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Sun, 29 May 2022 18:59:59 +0200 Subject: [PATCH 02/13] Add new worldgen viewer based from MiMap --- .../Components/GuiMapViewer.cs | 318 ++++++++++++++ .../Content/Content.mgcb | 0 src/MiMap.Viewer.DesktopGL/Globals.cs | 62 +++ .../Graphics/CachedRegionMesh.cs | 49 +++ .../Graphics/DrawVertDeclaration.cs | 30 ++ .../Graphics/GraphicsContext.cs | 263 ++++++++++++ .../Graphics/ImGuiRenderer.cs | 387 ++++++++++++++++++ .../Graphics/RegionMeshManager.cs | 47 +++ src/MiMap.Viewer.DesktopGL/Icon.bmp | Bin 0 -> 262282 bytes src/MiMap.Viewer.DesktopGL/Icon.ico | Bin 0 -> 33897 bytes .../MiMap.Viewer.DesktopGL.csproj | 54 +++ src/MiMap.Viewer.DesktopGL/MiMapViewer.cs | 161 ++++++++ src/MiMap.Viewer.DesktopGL/Models/Map.cs | 158 +++++++ src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs | 101 +++++ src/MiMap.Viewer.DesktopGL/NLog.config | 36 ++ src/MiMap.Viewer.DesktopGL/Program.cs | 55 +++ src/MiMap.Viewer.DesktopGL/app.manifest | 43 ++ src/WorldGenerator.sln | 6 + 18 files changed, 1770 insertions(+) create mode 100644 src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Content/Content.mgcb create mode 100644 src/MiMap.Viewer.DesktopGL/Globals.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/DrawVertDeclaration.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/GraphicsContext.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/ImGuiRenderer.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Icon.bmp create mode 100644 src/MiMap.Viewer.DesktopGL/Icon.ico create mode 100644 src/MiMap.Viewer.DesktopGL/MiMap.Viewer.DesktopGL.csproj create mode 100644 src/MiMap.Viewer.DesktopGL/MiMapViewer.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Models/Map.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs create mode 100644 src/MiMap.Viewer.DesktopGL/NLog.config create mode 100644 src/MiMap.Viewer.DesktopGL/Program.cs create mode 100644 src/MiMap.Viewer.DesktopGL/app.manifest diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs new file mode 100644 index 0000000..a7dfc39 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -0,0 +1,318 @@ +// /* +// * MiMap.Viewer.DesktopGL +// * +// * Copyright (c) 2020 Dan Spiteri +// * +// * / + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MiMap.Viewer.DesktopGL.Graphics; +using NLog; +using static ImGuiNET.ImGui; + +namespace MiMap.Viewer.DesktopGL.Components +{ + public class GuiMapViewer : DrawableGameComponent + { + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + private Point _position = Point.Zero; + private Rectangle _mapBounds; + private float _scale = 1f; + private readonly object _chunkSync = new object(); + private List _regions; + private List _chunks; + private bool _chunksDirty = false; + private Map _map; + private Point _mapPosition; + private Matrix _transform; + private int _drawOrder = 1; + private bool _visible = true; + private IRegionMeshManager _regionMeshManager; + private BasicEffect _effect; + private VertexBuffer _vertexBuffer; + private IndexBuffer _indexBuffer; + private ImGuiRenderer _gui; + private readonly RasterizerState _rasterizerState; + private Rectangle _bounds; + private MouseState _mouseState; + + public Rectangle Bounds + { + get => _bounds; + set + { + if (_bounds == value) return; + _bounds = value; + RecalculateTransform(); + } + } + + public Point MapPosition + { + get => _mapPosition; + set + { + if (_mapPosition == value) return; + _mapPosition = value; + RecalculateTransform(); + } + } + public Rectangle MapBounds + { + get => _mapBounds; + private set + { + if (_mapBounds == value) + return; + + _mapBounds = value; + OnMapBoundsChanged(); + } + } + public Map Map + { + get => _map; + set + { + if (_map != default) + { + _map.RegionGenerated -= OnRegionGenerated; + } + + _map = value; + + if (_map != default) + { + _map.RegionGenerated += OnRegionGenerated; + } + } + } + public Matrix Transform + { + get => _transform; + private set + { + _transform = value; + InverseTransform = Matrix.Invert(value); + } + } + private Matrix InverseTransform { get; set; } + + public GuiMapViewer(MiMapViewer game, Map map) : base(game) + { + Map = map; + _regions = new List(); + _chunks = new List(); + _regionMeshManager = game.RegionMeshManager; + _gui = game.ImGuiRenderer; + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + FillMode = FillMode.Solid, + ScissorTestEnable = true, + DepthClipEnable = false, + MultiSampleAntiAlias = true + }; + } + + public override void Initialize() + { + base.Initialize(); + + _effect = new BasicEffect(GraphicsDevice) + { + LightingEnabled = false, + VertexColorEnabled = false, + TextureEnabled = true, + FogEnabled = false + }; + + Game.GraphicsDevice.DeviceReset += (s, o) => UpdateBounds(); + Game.Activated += (s, o) => UpdateBounds(); + Game.Window.ClientSizeChanged += (s, o) => UpdateBounds(); + UpdateBounds(); + + RecalculateTransform(); + _vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), 4, BufferUsage.WriteOnly); + _indexBuffer = new IndexBuffer(GraphicsDevice, typeof(ushort), 6, BufferUsage.WriteOnly); + _vertexBuffer.SetData(new[] + { + new VertexPositionTexture(new Vector3(0f, 0f, 0f), new Vector2(0f, 0f)), + new VertexPositionTexture(new Vector3(512f, 0f, 0f), new Vector2(0f, 1f)), + new VertexPositionTexture(new Vector3(0f, 512f, 0f), new Vector2(1f, 0f)), + new VertexPositionTexture(new Vector3(512f, 512f, 0f), new Vector2(1f, 1f)), + }); + _indexBuffer.SetData(new ushort[] + { + 0, 1, 2, + 2, 1, 3 + }); + } + + public override void Update(GameTime gameTime) + { + UpdateMouseInput(); + if (Map == default) return; + + if (_chunksDirty) + { + var regions = Map.GetRegions(_mapBounds); + lock (_chunkSync) + { + _regions.Clear(); + _regions.AddRange(regions.Where(r => r.IsComplete).Select(r => _regionMeshManager.CacheRegion(r))); + } + + _chunksDirty = false; + } + } + + #region Drawing + + public override void Draw(GameTime gameTime) + { + base.Draw(gameTime); + + if (Map != default) + { + DrawMap(gameTime); + } + + _gui.BeforeLayout(gameTime); + + DrawImGui(); + + _gui.AfterLayout(); + } + + private void DrawImGui() + { + if (Begin("Map Viewer")) + { + if (BeginTable("mapviewtable", 2)) + { + // PushID("mapviewtable_Scale"); + TableNextRow(); + TableNextColumn(); + Text("Scale"); + TableNextColumn(); + // TableSetColumnIndex(1); + // SetNextItemWidth(-float.MinValue); + DragFloat("##value", ref _scale, 0.01f, 8f); + // PopID(); + EndTable(); + } + + End(); + } + } + private void DrawMap(GameTime gameTime) + { + using (var cxt = GraphicsContext.CreateContext(GraphicsDevice, BlendState.AlphaBlend, DepthStencilState.None, _rasterizerState, SamplerState.PointClamp)) + { + cxt.ScissorRectangle = Bounds; + var scaleMatrix = Matrix.CreateScale(_scale, _scale, 1f); + foreach (var region in _regions) + { + cxt.GraphicsDevice.SetVertexBuffer(_vertexBuffer); + cxt.GraphicsDevice.Indices = _indexBuffer; + + _effect.World = region.World * scaleMatrix; + _effect.Texture = region.Texture; + + foreach (var p in _effect.CurrentTechnique.Passes) + { + p.Apply(); + cxt.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 2); + } + } + } + } + + #endregion + + private void UpdateBounds() + { + Bounds = new Rectangle(Point.Zero, Game.Window.ClientBounds.Size); + } + + private void RecalculateTransform() + { + var p = MapPosition; + Transform = Matrix.Identity + * Matrix.CreateTranslation(p.X, p.Y, 0) + ; + + RecalculateMapBounds(); + } + private void RecalculateMapBounds() + { + var screenBounds = Bounds; + var screenSize = new Vector2(screenBounds.Width, screenBounds.Height); + + var blockBoundsMin = Vector2.Transform(Vector2.Zero, Transform); + var blockBoundsSize = screenSize / _scale; + + var bbSize = new Point((int)Math.Ceiling(blockBoundsSize.X), (int)Math.Ceiling(blockBoundsSize.Y)); + var bbMin = new Point((int)Math.Floor(blockBoundsMin.X - (bbSize.X / 2f)), (int)Math.Floor(blockBoundsMin.Y - (bbSize.Y / 2f))); + + MapBounds = new Rectangle(bbMin, bbSize); + + if (_effect != default) + { + _effect.Projection = Matrix.CreateOrthographic(screenBounds.Width, screenBounds.Height, 0.1f, 100f); + + var p = Vector3.Transform(Vector3.Zero, Transform); + + _effect.View = Matrix.CreateLookAt(new Vector3(_mapPosition.X, _mapPosition.Y, 10), new Vector3(_mapPosition.X, _mapPosition.Y, 0), Vector3.Up); + _effect.World = Matrix.CreateWorld(p + (Vector3.Forward * 10), Vector3.Forward, Vector3.Up); + } + } + private void OnMapBoundsChanged() + { + _chunksDirty = true; + Log.Info($"Map bounds changed: {_mapBounds.X:000}, {_mapBounds.Y:000} => {_mapBounds.Width:0000} x {_mapBounds.Height:0000}"); + } + private void OnRegionGenerated(object sender, Point regionPosition) + { + //_chunksDirty = true; + var region = Map.GetRegion(regionPosition); + if (region != null) + { + _regions.Add(_regionMeshManager.CacheRegion(region)); + Log.Info($"Region generated: {regionPosition.X:000}, {regionPosition.Y:000}"); + } + } + + #region Mouse Events + + private void UpdateMouseInput() + { + var newState = Mouse.GetState(); + + if (_mouseState.Position != newState.Position) + { + OnCursorMove(newState.Position, _mouseState.Position, _mouseState.LeftButton == ButtonState.Pressed); + } + + _mouseState = newState; + } + private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bool isCursorDown) + { + if (isCursorDown) + { + var p = (cursorPosition - previousCursorPosition); + MapPosition += new Point(-p.X, p.Y); + } + } + + #endregion + + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Content/Content.mgcb b/src/MiMap.Viewer.DesktopGL/Content/Content.mgcb new file mode 100644 index 0000000..e69de29 diff --git a/src/MiMap.Viewer.DesktopGL/Globals.cs b/src/MiMap.Viewer.DesktopGL/Globals.cs new file mode 100644 index 0000000..5bd7f41 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Globals.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MiMap.Viewer.DesktopGL +{ + public static class Globals + { + public static readonly Color[] BiomeColors; + + static Globals() + { + BiomeColors = new Color[byte.MaxValue]; + + byte i = 0; + BiomeColors[i++] = new Color(0xFF700000); + BiomeColors[i++] = new Color(0xFF60B38D); + BiomeColors[i++] = new Color(0xFF1894FA); + BiomeColors[i++] = new Color(0xFF606060); + BiomeColors[i++] = new Color(0xFF216605); + BiomeColors[i++] = new Color(0xFF59660B); + BiomeColors[i++] = new Color(0xFFB2F907); + BiomeColors[i++] = new Color(0xFFFF0000); + BiomeColors[i++] = new Color(0xFF0000FF); + BiomeColors[i++] = new Color(0xFFFF8080); + BiomeColors[i++] = new Color(0xFFA09090); + BiomeColors[i++] = new Color(0xFFFFA0A0); + BiomeColors[i++] = new Color(0xFFFFFFFF); + BiomeColors[i++] = new Color(0xFFA0A0A0); + BiomeColors[i++] = new Color(0xFFFF00FF); + BiomeColors[i++] = new Color(0xFFFF00A0); + BiomeColors[i++] = new Color(0xFF55DEFA); + BiomeColors[i++] = new Color(0xFF125FD2); + BiomeColors[i++] = new Color(0xFF1C5522); + BiomeColors[i++] = new Color(0xFF333916); + BiomeColors[i++] = new Color(0xFF9A7872); + BiomeColors[i++] = new Color(0xFF097B53); + BiomeColors[i++] = new Color(0xFF05422C); + BiomeColors[i++] = new Color(0xFF178B62); + BiomeColors[i++] = new Color(0xFF300000); + BiomeColors[i++] = new Color(0xFF84A2A2); + BiomeColors[i++] = new Color(0xFFC0F0FA); + BiomeColors[i++] = new Color(0xFF447430); + BiomeColors[i++] = new Color(0xFF325F1F); + BiomeColors[i++] = new Color(0xFF1A5140); + BiomeColors[i++] = new Color(0xFF4A5531); + BiomeColors[i++] = new Color(0xFF363F24); + BiomeColors[i++] = new Color(0xFF516659); + BiomeColors[i++] = new Color(0xFF3E5F54); + BiomeColors[i++] = new Color(0xFF507050); + BiomeColors[i++] = new Color(0xFF5FB2BD); + BiomeColors[i++] = new Color(0xFF649DA7); + BiomeColors[i++] = new Color(0xFF1545D9); + BiomeColors[i++] = new Color(0xFF6597B0); + BiomeColors[i++] = new Color(0xFF658CCA); + + for (; i < BiomeColors.Length; i++) + { + BiomeColors[i] = Color.Transparent; + } + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs new file mode 100644 index 0000000..071f552 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + public class CachedRegionMesh : IDisposable + { + // FaceGroupOptimizer + // FaceGroupUtil + public Texture2D Texture { get; private set; } + + public Matrix World { get; private set; } + + public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) + { + var t = new Texture2D(graphics, 1 << 9, 1 << 9); + var d = new Color[(1 << 9) * (1 << 9)]; + + int x, y, z; + byte b; + Color c; + foreach (var chunk in region.Chunks) + { + for (int cx = 0; cx < 16; cx++) + for (int cz = 0; cz < 16; cz++) + { + x = (chunk.X << 4) + cx; + z = (chunk.Z << 4) + cz; + b = chunk.GetBiome(cx, cz); + y = chunk.GetHeight(cx, cz); + c = chunk.GetColor(cx, cz); + + d[(((((chunk.X & 31) << 4)) + cx) * (1 << 9)) + (((chunk.Z & 31) << 4) + cz)] = c; + } + } + + t.SetData(d); + Texture = t; + World = Matrix.Identity + * Matrix.CreateTranslation((region.X << 9), (region.Z << 9), 0f); + } + + public void Dispose() + { + Texture?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/DrawVertDeclaration.cs b/src/MiMap.Viewer.DesktopGL/Graphics/DrawVertDeclaration.cs new file mode 100644 index 0000000..61e1841 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/DrawVertDeclaration.cs @@ -0,0 +1,30 @@ +using ImGuiNET; +using Microsoft.Xna.Framework.Graphics; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/GraphicsContext.cs b/src/MiMap.Viewer.DesktopGL/Graphics/GraphicsContext.cs new file mode 100644 index 0000000..59df02f --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/GraphicsContext.cs @@ -0,0 +1,263 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + public class GraphicsContext : IDisposable + { + public EventHandler Disposed; + + public GraphicsDevice GraphicsDevice { get; } + + #region Properties + + private readonly PropertyState _viewportProperty; + private readonly PropertyState _blendStateProperty; + private readonly PropertyState _depthStencilStateProperty; + private readonly PropertyState _rasterizerStateProperty; + private readonly PropertyState _samplerStateProperty; + private readonly PropertyState _scissorRectangleProperty; + private readonly PropertyState _blendFactorProperty; + + public Viewport Viewport + { + get => _viewportProperty; + set => _viewportProperty.Set(value); + } + public BlendState BlendState + { + get => _blendStateProperty; + set => _blendStateProperty.Set(value); + } + public DepthStencilState DepthStencilState + { + get => _depthStencilStateProperty; + set => _depthStencilStateProperty.Set(value); + } + public RasterizerState RasterizerState + { + get => _rasterizerStateProperty; + set => _rasterizerStateProperty.Set(value); + } + public SamplerState SamplerState + { + get => _samplerStateProperty; + set => _samplerStateProperty.Set(value); + } + public Rectangle ScissorRectangle + { + get => _scissorRectangleProperty; + set => _scissorRectangleProperty.Set(value); + } + public Color BlendFactor + { + get => _blendFactorProperty; + set => _blendFactorProperty.Set(value); + } + + #endregion + + public GraphicsContext(GraphicsDevice graphicsDevice) + { + GraphicsDevice = graphicsDevice; + + _viewportProperty = CreateState(g => g.Viewport, (g, v) => g.Viewport = v); + _blendStateProperty = CreateState(g => g.BlendState, (g, v) => g.BlendState = v); + _depthStencilStateProperty = CreateState(g => g.DepthStencilState, (g, v) => g.DepthStencilState = v); + _rasterizerStateProperty = CreateState(g => g.RasterizerState, (g, v) => g.RasterizerState = v); + _samplerStateProperty = CreateState(g => g.SamplerStates[0], (g, v) => g.SamplerStates[0] = v); + _scissorRectangleProperty = CreateState(g => g.ScissorRectangle, (g, v) => g.ScissorRectangle = v); + _blendFactorProperty = CreateState(g => g.BlendFactor, (g, v) => g.BlendFactor = v); + } + + public GraphicsContext CreateContext(BlendState blendState = null, DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, SamplerState samplerState = null) + { + return CreateContext(GraphicsDevice, blendState, depthStencilState, rasterizerState, samplerState); + } + + public void RestoreState() + { + ForEachProperty(p => p.RestoreInitialValue()); + } + + private void ForEachProperty(Action> action) + { + if (action == null) return; + + action.Invoke(_viewportProperty); + action.Invoke(_blendStateProperty); + action.Invoke(_depthStencilStateProperty); + action.Invoke(_rasterizerStateProperty); + action.Invoke(_samplerStateProperty); + action.Invoke(_scissorRectangleProperty); + action.Invoke(_blendFactorProperty); + } + + public static GraphicsContext CreateContext(GraphicsDevice graphicsDevice, + BlendState blendState = null, + DepthStencilState depthStencilState = null, + RasterizerState rasterizerState = null, + SamplerState samplerState = null) + { + var context = new GraphicsContext(graphicsDevice); + + if (blendState != null) + { + context.BlendState = blendState; + } + + if (depthStencilState != null) + { + context.DepthStencilState = depthStencilState; + } + + if (rasterizerState != null) + { + context.RasterizerState = rasterizerState; + } + + if (samplerState != null) + { + context.SamplerState = samplerState; + } + + return context; + } + + #region Dispose + + private bool _isDisposed = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + ForEachProperty(p => p.Dispose()); + + Disposed?.Invoke(this, null); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + #endregion + + + private PropertyState CreateState(Func getProperty, Action setProperty) + { + return new PropertyState(GraphicsDevice, getProperty(GraphicsDevice), setProperty); + } + + public interface IPropertyState : IDisposable + { + void RestoreInitialValue(); + } + + public class PropertyState : IPropertyState + { + public TPropertyType InitialValue { get; } + public TPropertyType Value + { + get => _currentValue; + set => Set(value); + } + + private bool _dirty; + private TPropertyType _currentValue; + private readonly TPropertyOwner _owner; + + private Func _getValueFunc; + private readonly Action _setValueFunc; + + + public PropertyState(TPropertyOwner owner, TPropertyType initialValue, Action setValueFunc) + { + _owner = owner; + _setValueFunc = setValueFunc; + + InitialValue = initialValue; + _currentValue = initialValue; + } + + public TPropertyType Set(TPropertyType newValue) + { + _dirty = true; + _currentValue = newValue; + + _setValueFunc(_owner, _currentValue); + + return Value; + } + + public void RestoreInitialValue() + { + if (_dirty) + { + _setValueFunc(_owner, InitialValue); + } + } + + public void Dispose() + { + RestoreInitialValue(); + } + + public static implicit operator TPropertyType(PropertyState propertyState) + { + return propertyState.Value; + } + } + + + } + + public static class GraphicsContextExtensions + { + public static IDisposable PushRenderTarget(this GraphicsDevice graphicsDevice, RenderTarget2D renderTarget, Viewport viewport) + { + var currentRenderTargets = graphicsDevice.GetRenderTargets(); + graphicsDevice.SetRenderTarget(renderTarget); + + var currentViewport = graphicsDevice.Viewport; + graphicsDevice.Viewport = viewport; + + return new ContextDisposable(() => + { + graphicsDevice.SetRenderTargets(currentRenderTargets); + graphicsDevice.Viewport = currentViewport; + }); + } + + public static IDisposable PushRenderTarget(this GraphicsDevice graphicsDevice, RenderTarget2D renderTarget) + { + var current = graphicsDevice.GetRenderTargets(); + graphicsDevice.SetRenderTarget(renderTarget); + return new ContextDisposable(() => graphicsDevice.SetRenderTargets(current)); + } + } + + + internal class ContextDisposable : IDisposable + { + private readonly Action _onDisposeAction; + + public ContextDisposable(Action onDisposeAction) + { + _onDisposeAction = onDisposeAction; + } + + public void Dispose() + { + _onDisposeAction?.Invoke(); + } + } +} diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/ImGuiRenderer.cs b/src/MiMap.Viewer.DesktopGL/Graphics/ImGuiRenderer.cs new file mode 100644 index 0000000..d5b41e2 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/ImGuiRenderer.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + + private List _keys = new List(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Maps ImGui keys to XNA keys. We use this later on to tell ImGui what keys were pressed + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + _keys.Add(io.KeyMap[(int)ImGuiKey.Tab] = (int)Keys.Tab); + _keys.Add(io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left); + _keys.Add(io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right); + _keys.Add(io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up); + _keys.Add(io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down); + _keys.Add(io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp); + _keys.Add(io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown); + _keys.Add(io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home); + _keys.Add(io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End); + _keys.Add(io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete); + _keys.Add(io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Back); + _keys.Add(io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter); + _keys.Add(io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape); + _keys.Add(io.KeyMap[(int)ImGuiKey.Space] = (int)Keys.Space); + _keys.Add(io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A); + _keys.Add(io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C); + _keys.Add(io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V); + _keys.Add(io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X); + _keys.Add(io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y); + _keys.Add(io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + + io.AddInputCharacter(a.Character); + }; + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + + ImGui.GetIO().Fonts.AddFontDefault(); + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + + for (int i = 0; i < _keys.Count; i++) + { + io.KeysDown[_keys[i]] = keyboard.IsKeyDown((Keys)_keys[i]); + } + + io.KeyShift = keyboard.IsKeyDown(Keys.LeftShift) || keyboard.IsKeyDown(Keys.RightShift); + io.KeyCtrl = keyboard.IsKeyDown(Keys.LeftControl) || keyboard.IsKeyDown(Keys.RightControl); + io.KeyAlt = keyboard.IsKeyDown(Keys.LeftAlt) || keyboard.IsKeyDown(Keys.RightAlt); + io.KeySuper = keyboard.IsKeyDown(Keys.LeftWindows) || keyboard.IsKeyDown(Keys.RightWindows); + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + + io.MousePos = new System.Numerics.Vector2(mouse.X, mouse.Y); + + io.MouseDown[0] = mouse.LeftButton == ButtonState.Pressed; + io.MouseDown[1] = mouse.RightButton == ButtonState.Pressed; + io.MouseDown[2] = mouse.MiddleButton == ButtonState.Pressed; + + var scrollDelta = mouse.ScrollWheelValue - _scrollWheelValue; + io.MouseWheel = scrollDelta > 0 ? 1 : scrollDelta < 0 ? -1 : 0; + _scrollWheelValue = mouse.ScrollWheelValue; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdListsRange[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdListsRange[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs b/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs new file mode 100644 index 0000000..1c4509f --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + public interface IRegionMeshManager : IDisposable + { + CachedRegionMesh CacheRegion(MapRegion region); + } + + public class RegionMeshManager : IRegionMeshManager + { + private GraphicsDevice _graphics; + + private IDictionary _cache = new Dictionary(); + + public RegionMeshManager(GraphicsDevice graphics) + { + _graphics = graphics; + } + + public CachedRegionMesh CacheRegion(MapRegion region) + { + var i = new Point(region.X, region.Z); + CachedRegionMesh cached; + if (_cache.TryGetValue(i, out cached)) return cached; + cached = new CachedRegionMesh(_graphics, region); + _cache[i] = cached; + return cached; + } + + public void Dispose() + { + var points = _cache.Keys.ToArray(); + foreach (var p in points) + { + if (_cache.Remove(p, out var c)) + { + c.Dispose(); + } + } + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Icon.bmp b/src/MiMap.Viewer.DesktopGL/Icon.bmp new file mode 100644 index 0000000000000000000000000000000000000000..4af43af485eab987b09faeebef717188ce27464a GIT binary patch literal 262282 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 z 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 Bh&=!R literal 0 HcmV?d00001 diff --git a/src/MiMap.Viewer.DesktopGL/Icon.ico b/src/MiMap.Viewer.DesktopGL/Icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2b3de014c4c2c62a9848f238f0ff3b6959a411a7 GIT binary patch literal 33897 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 + + + Exe + net6.0 + false + false + latest + Release;Debug + AnyCPU;x64;x86 + true + + + app.manifest + Icon.ico + + + + + + + + Always + + + + + + + + + + + false + + + + + + + %(Filename).cs + + + + + + + + + + diff --git a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs new file mode 100644 index 0000000..7032ba3 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs @@ -0,0 +1,161 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MiMap.Viewer.DesktopGL.Components; +using MiMap.Viewer.DesktopGL.Graphics; +using MiNET.Worlds; +using OpenAPI.WorldGenerator.Generators; + +namespace MiMap.Viewer.DesktopGL +{ + public class MiMapViewer : Game + { + public static MiMapViewer Instance { get; private set; } + + private readonly GraphicsDeviceManager _graphics; + public ImGuiRenderer ImGuiRenderer { get; private set; } + private Texture2D _xnaTexture; + private IntPtr _imGuiTexture; + + public IWorldGenerator WorldGenerator { get; private set; } + public Map Map { get; private set; } + public IRegionMeshManager RegionMeshManager { get; private set; } + private GuiMapViewer _mapViewer; + + public MiMapViewer() : base() + { + Instance = this; + _graphics = new GraphicsDeviceManager(this) + { + PreferredBackBufferHeight = 720, + PreferredBackBufferWidth = 1280, + PreferMultiSampling = true, + GraphicsProfile = GraphicsProfile.HiDef + }; + _graphics.PreparingDeviceSettings += (sender, args) => + { + //_graphics.PreferredBackBufferFormat = SurfaceFormat.Color; + _graphics.PreferMultiSampling = true; + _graphics.PreferredBackBufferHeight = 720; + _graphics.PreferredBackBufferWidth = 1280; + _graphics.GraphicsProfile = GraphicsProfile.HiDef; + }; + + Content.RootDirectory = "Content"; + IsMouseVisible = true; + Window.AllowUserResizing = true; + + WorldGenerator = new OverworldGeneratorV2(); + Map = new Map(WorldGenerator); + } + + protected override void Initialize() + { + // _graphics.GraphicsProfile = GraphicsProfile.HiDef; + // _graphics.SynchronizeWithVerticalRetrace = false; +// _graphics.PreferMultiSampling = true; + IsFixedTimeStep = false; + Window.AllowUserResizing = true; + + UpdateViewport(); + Window.ClientSizeChanged += (s, o) => UpdateViewport(); + + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + RegionMeshManager = new RegionMeshManager(GraphicsDevice); + _mapViewer = new GuiMapViewer(this, Map); + _mapViewer.Initialize(); + Components.Add(_mapViewer); + + } + + private void UpdateViewport(bool apply = true) + { + var bounds = Window.ClientBounds; + + if ( + _graphics.PreferredBackBufferWidth != bounds.Width + || + _graphics.PreferredBackBufferHeight != bounds.Height) + { + if (_graphics.IsFullScreen) + { + _graphics.PreferredBackBufferWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width; + _graphics.PreferredBackBufferHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height; + } + else + { + _graphics.PreferredBackBufferWidth = bounds.Width; + _graphics.PreferredBackBufferHeight = bounds.Height; + } + + if (apply) + _graphics.ApplyChanges(); + } + + ImGuiRenderer?.RebuildFontAtlas(); + //_graphics.GraphicsDevice.Viewport = new Viewport(bounds); + } + + protected override void LoadContent() + { + // First, load the texture as a Texture2D (can also be done using the XNA/FNA content pipeline) + _xnaTexture = CreateTexture(GraphicsDevice, 300, 150, pixel => + { + var red = (pixel % 300) / 2; + return new Color(red, 1, 1); + }); + + // Then, bind it to an ImGui-friendly pointer, that we can use during regular ImGui.** calls (see below) + _imGuiTexture = ImGuiRenderer.BindTexture(_xnaTexture); + + base.LoadContent(); + + UpdateViewport(); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || + Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _mapViewer?.Dispose(); + RegionMeshManager?.Dispose(); + Map?.Dispose(); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.HotPink); + base.Draw(gameTime); + } + + private static Texture2D CreateTexture(GraphicsDevice device, int width, int height, Func paint) + { + //initialize a texture + var texture = new Texture2D(device, width, height); + + //the array holds the color for each pixel in the texture + Color[] data = new Color[width * height]; + for(var pixel = 0; pixel < data.Length; pixel++) + { + //the function applies the color according to the specified pixel + data[pixel] = paint( pixel ); + } + + //set the color + texture.SetData( data ); + + return texture; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Models/Map.cs b/src/MiMap.Viewer.DesktopGL/Models/Map.cs new file mode 100644 index 0000000..5f40aaa --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Models/Map.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Xna.Framework; +using MiNET.Utils.Vectors; +using MiNET.Worlds; +using NLog; + +namespace MiMap.Viewer.DesktopGL +{ + public class Map : IDisposable + { + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + private readonly IWorldGenerator _worldGenerator; + public Dictionary Regions { get; } + + public event EventHandler RegionGenerated; + public event EventHandler ChunkGenerated; + + private Thread _thread; + private ConcurrentQueue _regionsToGenerate; + private AutoResetEvent _trigger; + private bool _running; + + public Map(IWorldGenerator worldGenerator) + { + _worldGenerator = worldGenerator; + Regions = new Dictionary(); + _regionsToGenerate = new ConcurrentQueue(); + _trigger = new AutoResetEvent(false); + _thread = new Thread(Run) + { + Name = "WorldGenerator", + IsBackground = false + }; + _thread.Start(); + } + + public void Run() + { + _running = true; + + while (_running) + { + if (_trigger.WaitOne(10000)) + { + while (_regionsToGenerate.TryPeek(out var r)) + { + if (Regions.ContainsKey(r)) + { + _regionsToGenerate.TryDequeue(out _); + continue; + } + + Log.Info($"Generating Region: {r.X:000}, {r.Y:000}"); + var sw = Stopwatch.StartNew(); + Regions[r] = new MapRegion(r.X, r.Y); + _regionsToGenerate.TryDequeue(out _); + Parallel.For(0, 32 * 32, new ParallelOptions() + { + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + TaskScheduler = TaskScheduler.Default + }, + (i) => + { + var cx = (int)Math.Floor(i / 32f); + var cz = i % 32; + var chunkPosition = new ChunkCoordinates((r.X << 5) + cx, (r.Y << 5) + cz); + var csw = Stopwatch.StartNew(); + using var chunk = _worldGenerator.GenerateChunkColumn(chunkPosition); + csw.Stop(); + var t1 = csw.ElapsedMilliseconds; + csw.Restart(); + Regions[r][cx, cz] = ExtractChunkData(chunk); + csw.Stop(); + //Log.Info($"Generated Chunk: {chunkPosition.X:000}, {chunkPosition.Z:000} in {t1:F2}ms (extract data in {csw.ElapsedMilliseconds:F2}ms)"); + ChunkGenerated?.Invoke(this, new Point(chunkPosition.X, chunkPosition.Z)); + }); + sw.Stop(); + Log.Info($"Generated Region: {r.X:000}, {r.Y:000} in {sw.ElapsedMilliseconds:F2}ms"); + Regions[r].IsComplete = true; + RegionGenerated?.Invoke(this, r); + } + } + } + } + + private void EnqueueRegion(Point regionCoords) + { + if (Regions.ContainsKey(regionCoords) || _regionsToGenerate.Contains(regionCoords)) + return; + + Log.Info($"Enqueue Region: {regionCoords.X:000}, {regionCoords.Y:000}"); + _regionsToGenerate.Enqueue(regionCoords); + _trigger.Set(); + } + + private MapChunk ExtractChunkData(ChunkColumn chunk) + { + var mapChunk = new MapChunk(chunk.X, chunk.Z); + for (int x = 0; x < 16; x++) + for (int z = 0; z < 16; z++) + { + mapChunk.SetHeight(x, z, chunk.GetHeight(x, z)); + mapChunk.SetBiome(x, z, chunk.GetBiome(x, z)); + } + + return mapChunk; + } + + public MapRegion GetRegion(Point regionPosition) + { + return Regions[regionPosition]; + } + + public IEnumerable GetRegions() + { + var v = Regions.Values.ToArray(); + foreach (var region in v) + { + yield return region; + } + } + + public IEnumerable GetRegions(Rectangle blockBounds) + { + var regionMin = new Point((blockBounds.X >> 9) - 1, (blockBounds.Y >> 9) - 1); + var regionMax = new Point(((blockBounds.X + blockBounds.Width) >> 9) + 1, ((blockBounds.Y + blockBounds.Height) >> 9) + 1); + + for (int x = regionMin.X; x <= regionMax.X; x++) + for (int z = regionMin.Y; z <= regionMax.Y; z++) + { + var p = new Point(x, z); + if (Regions.TryGetValue(p, out var region)) + { + yield return region; + } + else + { + EnqueueRegion(p); + } + } + } + + public void Dispose() + { + _running = false; + _trigger.Set(); + _thread.Join(); + _trigger?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs new file mode 100644 index 0000000..9731bd7 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MiMap.Viewer.DesktopGL +{ + public class MapRegion + { + public readonly int X; + public readonly int Z; + + public readonly MapChunk[] Chunks; + public bool IsComplete { get; internal set; } + + public MapRegion(int x, int z) + { + X = x; + Z = z; + Chunks = new MapChunk[32 * 32]; + } + + public void SetChunk(int cx, int cz, MapChunk chunk) + { + Chunks[GetIndex(cx & 31, cz & 31)] = chunk; + } + + public MapChunk this[int cx, int cz] + { + get => Chunks[GetIndex(cx & 31, cz & 31)]; + set => Chunks[GetIndex(cx & 31, cz & 31)] = value; + } + + private int GetIndex(int x, int z) + { + return (x * 32) + z; + } + } + + public class MapChunk + { + public readonly int X; + public readonly int Z; + + public readonly int[] Heights; + public readonly byte[] Biomes; + public readonly Color[] Colors; + + public MapChunk(int x, int z) + { + X = x; + Z = z; + Heights = new int[16 * 16]; + Biomes = new byte[16 * 16]; + Colors = new Color[16 * 16]; + } + + public int GetHeight(int x, int z) + { + return Heights[GetIndex(x & 15, z & 15)]; + } + + public byte GetBiome(int x, int z) + { + return Biomes[GetIndex(x & 15, z & 15)]; + } + + public Color GetColor(int x, int z) + { + return Colors[GetIndex(x & 15, z & 15)]; + } + + public void SetHeight(int x, int z, int height) + { + var i = GetIndex(x & 15, z & 15); + Heights[i] = height; + UpdateColor(i); + } + + public void SetBiome(int x, int z, byte biome) + { + var i = GetIndex(x & 15, z & 15); + Biomes[i] = biome; + UpdateColor(i); + } + + private void UpdateColor(int i) + { + var biome = Biomes[i]; + var height = Heights[i]; + var hIntensity = MathHelper.Clamp((height % (255f / 25f)) / 25f, 0f, 1f) / 2; + + var c1 = Globals.BiomeColors[biome]; + var c2 = Color.Black; + Colors[i] = Color.Lerp(c1, c2, hIntensity); + } + + private int GetIndex(int x, int z) + { + return (x * 16) + z; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/NLog.config b/src/MiMap.Viewer.DesktopGL/NLog.config new file mode 100644 index 0000000..7e95444 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/NLog.config @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Program.cs b/src/MiMap.Viewer.DesktopGL/Program.cs new file mode 100644 index 0000000..f10fe98 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Program.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework; +using NLog; + +namespace MiMap.Viewer.DesktopGL +{ + public static class Program + { + [STAThread] + static void Main(string[] args) + { + ConfigureNLog(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))); + + var logger = LogManager.GetCurrentClassLogger(); + try + { + logger.Info($"Starting {Assembly.GetExecutingAssembly().GetName().Name}"); + + using (var game = new MiMapViewer()) + { + game.Run(GameRunBehavior.Synchronous); + } + } + catch (Exception ex) + { + // NLog: catch any exception and log it. + logger.Error(ex, "Stopped program because of exception"); + throw; + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + LogManager.Shutdown(); + } + } + + private static void ConfigureNLog(string baseDir) + { + string loggerConfigFile = Path.Combine(baseDir, "NLog.config"); + + string logsDir = Path.Combine(baseDir, "logs"); + if (!Directory.Exists(logsDir)) + { + Directory.CreateDirectory(logsDir); + } + + LogManager.ThrowConfigExceptions = false; + LogManager.LoadConfiguration(loggerConfigFile); +// LogManager.Configuration = new XmlLoggingConfiguration(loggerConfigFile); + LogManager.Configuration.Variables["basedir"] = baseDir; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/app.manifest b/src/MiMap.Viewer.DesktopGL/app.manifest new file mode 100644 index 0000000..b77e67a --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/src/WorldGenerator.sln b/src/WorldGenerator.sln index 64d5ad0..aaf85e9 100644 --- a/src/WorldGenerator.sln +++ b/src/WorldGenerator.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAPI.WorldGenerator", "O EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldGenerator.Tweaking", "WorldGenerator.Tweaking\WorldGenerator.Tweaking.csproj", "{2AB15C83-8F60-4D20-A8E0-2158B1C01E25}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiMap.Viewer.DesktopGL", "MiMap.Viewer.DesktopGL\MiMap.Viewer.DesktopGL.csproj", "{EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {2AB15C83-8F60-4D20-A8E0-2158B1C01E25}.Debug|Any CPU.Build.0 = Debug|Any CPU {2AB15C83-8F60-4D20-A8E0-2158B1C01E25}.Release|Any CPU.ActiveCfg = Release|Any CPU {2AB15C83-8F60-4D20-A8E0-2158B1C01E25}.Release|Any CPU.Build.0 = Release|Any CPU + {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal From 76c89966aa99d9febff2c459787e04cac1466cf5 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 01:10:38 +0200 Subject: [PATCH 03/13] improve things + break things --- .../Components/GuiMapViewer.cs | 230 +++++++++++++++--- .../Graphics/CachedRegionMesh.cs | 42 +++- .../MiMap.Viewer.DesktopGL.csproj | 3 + src/MiMap.Viewer.DesktopGL/MiMapViewer.cs | 21 ++ .../Models/BiomeColors.cs | 64 +++++ src/MiMap.Viewer.DesktopGL/Models/Map.cs | 7 +- src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs | 42 +--- .../Models/MapRegion.cs | 34 +++ src/MiMap.Viewer.DesktopGL/biomeColors.json | 83 +++++++ 9 files changed, 449 insertions(+), 77 deletions(-) create mode 100644 src/MiMap.Viewer.DesktopGL/Models/BiomeColors.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs create mode 100644 src/MiMap.Viewer.DesktopGL/biomeColors.json diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index a7dfc39..6b9d138 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -8,11 +8,14 @@ using System; using System.Collections.Generic; using System.Linq; +using ImGuiNET; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MiMap.Viewer.DesktopGL.Graphics; using NLog; +using OpenAPI.WorldGenerator.Generators.Biomes; +using OpenAPI.WorldGenerator.Utils; using static ImGuiNET.ImGui; namespace MiMap.Viewer.DesktopGL.Components @@ -41,7 +44,12 @@ public class GuiMapViewer : DrawableGameComponent private readonly RasterizerState _rasterizerState; private Rectangle _bounds; private MouseState _mouseState; - + private int[] _cursorBlock = new int[3]; + private BiomeBase _cursorBlockBiome; + private int[] _cursorChunk = new int[2]; + private int[] _cursorRegion = new int[2]; + private int _cursorBlockBiomeId; + public Rectangle Bounds { get => _bounds; @@ -63,6 +71,7 @@ public Point MapPosition RecalculateTransform(); } } + public Rectangle MapBounds { get => _mapBounds; @@ -75,6 +84,7 @@ private set OnMapBoundsChanged(); } } + public Map Map { get => _map; @@ -93,6 +103,7 @@ public Map Map } } } + public Matrix Transform { get => _transform; @@ -102,6 +113,7 @@ private set InverseTransform = Matrix.Invert(value); } } + private Matrix InverseTransform { get; set; } public GuiMapViewer(MiMapViewer game, Map map) : base(game) @@ -124,7 +136,7 @@ public GuiMapViewer(MiMapViewer game, Map map) : base(game) public override void Initialize() { base.Initialize(); - + _effect = new BasicEffect(GraphicsDevice) { LightingEnabled = false, @@ -137,15 +149,15 @@ public override void Initialize() Game.Activated += (s, o) => UpdateBounds(); Game.Window.ClientSizeChanged += (s, o) => UpdateBounds(); UpdateBounds(); - + RecalculateTransform(); _vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), 4, BufferUsage.WriteOnly); _indexBuffer = new IndexBuffer(GraphicsDevice, typeof(ushort), 6, BufferUsage.WriteOnly); _vertexBuffer.SetData(new[] { new VertexPositionTexture(new Vector3(0f, 0f, 0f), new Vector2(0f, 0f)), - new VertexPositionTexture(new Vector3(512f, 0f, 0f), new Vector2(0f, 1f)), - new VertexPositionTexture(new Vector3(0f, 512f, 0f), new Vector2(1f, 0f)), + new VertexPositionTexture(new Vector3(512f, 0f, 0f), new Vector2(1f, 0f)), + new VertexPositionTexture(new Vector3(0f, 512f, 0f), new Vector2(0f, 1f)), new VertexPositionTexture(new Vector3(512f, 512f, 0f), new Vector2(1f, 1f)), }); _indexBuffer.SetData(new ushort[] @@ -187,45 +199,168 @@ public override void Draw(GameTime gameTime) _gui.BeforeLayout(gameTime); DrawImGui(); - + _gui.AfterLayout(); } private void DrawImGui() { - if (Begin("Map Viewer")) + try { - if (BeginTable("mapviewtable", 2)) + if (Begin("Map Viewer")) { - // PushID("mapviewtable_Scale"); - TableNextRow(); - TableNextColumn(); - Text("Scale"); - TableNextColumn(); - // TableSetColumnIndex(1); - // SetNextItemWidth(-float.MinValue); - DragFloat("##value", ref _scale, 0.01f, 8f); - // PopID(); - EndTable(); + if (BeginTable("mapviewtable", 2)) + { + // PushID("mapviewtable_Scale"); + TableNextRow(); + TableNextColumn(); + Text("Scale"); + TableNextColumn(); + // TableSetColumnIndex(1); + // SetNextItemWidth(-float.MinValue); + SliderFloat("##value", ref _scale, 0.01f, 8f); + + TableNextRow(); + + // PopID(); + EndTable(); + } + + End(); } - End(); + if (Begin("Info")) + { + Text("At Cursor"); + InputInt3("Block", ref _cursorBlock[0], ImGuiInputTextFlags.ReadOnly); + InputInt2("Chunk", ref _cursorChunk[0], ImGuiInputTextFlags.ReadOnly); + InputInt2("Region", ref _cursorRegion[0], ImGuiInputTextFlags.ReadOnly); + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNode("Biome Info")) + { + var biome = _cursorBlockBiome; + + var biomeId = biome?.Id ?? _cursorBlockBiomeId; + var biomeName = biome?.Name ?? string.Empty; + var biomeDefinitionName = biome?.DefinitionName ?? string.Empty; + var biomeMinHeight = biome?.MinHeight ?? 0; + var biomeMaxHeight = biome?.MaxHeight ?? 0; + var biomeTemperature = biome?.Temperature ?? 0; + var biomeDownfall = biome?.Downfall ?? 0; + + InputInt("ID", ref biomeId, 0, 0, ImGuiInputTextFlags.ReadOnly); + InputText("Name", ref biomeName, 0, ImGuiInputTextFlags.ReadOnly); + InputText("Definition Name", ref biomeDefinitionName, 0, ImGuiInputTextFlags.ReadOnly); + InputFloat("Min Height", ref biomeMinHeight, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + InputFloat("Max Height", ref biomeMaxHeight, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + InputFloat("Temperature", ref biomeTemperature, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + InputFloat("Downfall", ref biomeDownfall, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNode("Config")) + { + var cfg = biome?.Config; + BeginDisabled(); + + var cfgIsEdgeBiome = cfg?.IsEdgeBiome ?? false; + var cfgAllowRivers = cfg?.AllowRivers ?? false; + var cfgAllowScenicLakes = cfg?.AllowScenicLakes ?? false; + var cfgSurfaceBlendIn = cfg?.SurfaceBlendIn ?? false; + var cfgSurfaceBlendOut = cfg?.SurfaceBlendOut ?? false; + var cfgWeightMultiplier = cfg?.WeightMultiplier ?? 0; + + Checkbox("Is Edge Biome", ref cfgIsEdgeBiome); + Checkbox("Allow Rivers", ref cfgAllowRivers); + Checkbox("Allow Scenic Lakes", ref cfgAllowScenicLakes); + Checkbox("Surface Blend In", ref cfgSurfaceBlendIn); + Checkbox("Surface Blend Out", ref cfgSurfaceBlendOut); + InputFloat("Weight Multiplier", ref cfgWeightMultiplier); + + EndDisabled(); + + TreePop(); + } + + + TreePop(); + } + + End(); + } + + if (Begin("Biome Colors")) + { + if (BeginTable("biomeclr", 3)) + { + foreach (var c in Map.BiomeProvider.Biomes) + { + TableNextRow(); + TableNextColumn(); + Text(c.Id.ToString()); + TableNextColumn(); + Text(c.Name); + TableNextColumn(); + TableSetBgColor(ImGuiTableBgTarget.CellBg, GetColor(c.Color ?? System.Drawing.Color.Transparent)); + Text(" "); + } + + EndTable(); + } + + End(); + } + } + catch (Exception ex) + { + Log.Error(ex, $"Drawing exception."); } } + + private static uint GetColor(System.Drawing.Color color) + { +// return 0xFFFF0000; + return (uint)( + 0xFF << 24 + | ((color.B & 0xFF) << 16) + | ((color.G & 0xFF) << 8) + | ((color.R & 0xFF) << 0) + ); + // return (uint)( + // ((color.G & 0xFF) << 24) + // | ((color.B & 0xFF) << 16) + // | ((color.R & 0xFF) << 8) + // | 0xFF + // ); + } + private void DrawMap(GameTime gameTime) { using (var cxt = GraphicsContext.CreateContext(GraphicsDevice, BlendState.AlphaBlend, DepthStencilState.None, _rasterizerState, SamplerState.PointClamp)) { cxt.ScissorRectangle = Bounds; - var scaleMatrix = Matrix.CreateScale(_scale, _scale, 1f); + var scaleMatrix = + Matrix.Identity + * Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f) + * Matrix.CreateScale(_scale, _scale, 1f) + ; foreach (var region in _regions) { cxt.GraphicsDevice.SetVertexBuffer(_vertexBuffer); cxt.GraphicsDevice.Indices = _indexBuffer; - _effect.World = region.World * scaleMatrix; + _effect.World = region.World * Transform; _effect.Texture = region.Texture; + if (_cursorRegion[0] == region.X && _cursorRegion[1] == region.Z) + { + _effect.DiffuseColor = Color.SteelBlue.ToVector3(); + } + else + { + _effect.DiffuseColor = Color.White.ToVector3(); + } + foreach (var p in _effect.CurrentTechnique.Passes) { p.Apply(); @@ -246,21 +381,23 @@ private void RecalculateTransform() { var p = MapPosition; Transform = Matrix.Identity - * Matrix.CreateTranslation(p.X, p.Y, 0) + * Matrix.CreateTranslation(-p.X, -p.Y, 0) + * Matrix.CreateScale(_scale, _scale, 1f) ; RecalculateMapBounds(); } + private void RecalculateMapBounds() { var screenBounds = Bounds; var screenSize = new Vector2(screenBounds.Width, screenBounds.Height); - var blockBoundsMin = Vector2.Transform(Vector2.Zero, Transform); - var blockBoundsSize = screenSize / _scale; + var blockBoundsMin = Unproject(Vector2.Zero); + var blockBoundsSize = Unproject(screenSize) - blockBoundsMin; var bbSize = new Point((int)Math.Ceiling(blockBoundsSize.X), (int)Math.Ceiling(blockBoundsSize.Y)); - var bbMin = new Point((int)Math.Floor(blockBoundsMin.X - (bbSize.X / 2f)), (int)Math.Floor(blockBoundsMin.Y - (bbSize.Y / 2f))); + var bbMin = new Point((int)Math.Floor(blockBoundsMin.X), (int)Math.Floor(blockBoundsMin.Y)); MapBounds = new Rectangle(bbMin, bbSize); @@ -268,17 +405,19 @@ private void RecalculateMapBounds() { _effect.Projection = Matrix.CreateOrthographic(screenBounds.Width, screenBounds.Height, 0.1f, 100f); - var p = Vector3.Transform(Vector3.Zero, Transform); + var p = Vector3.Transform(new Vector3(0f, 0f, 0f), Transform); - _effect.View = Matrix.CreateLookAt(new Vector3(_mapPosition.X, _mapPosition.Y, 10), new Vector3(_mapPosition.X, _mapPosition.Y, 0), Vector3.Up); - _effect.World = Matrix.CreateWorld(p + (Vector3.Forward * 10), Vector3.Forward, Vector3.Up); + _effect.View = Matrix.CreateLookAt(new Vector3(0, 0, 10), new Vector3(0, 0, 0), Vector3.Up); + _effect.World = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up); } } + private void OnMapBoundsChanged() { _chunksDirty = true; Log.Info($"Map bounds changed: {_mapBounds.X:000}, {_mapBounds.Y:000} => {_mapBounds.Width:0000} x {_mapBounds.Height:0000}"); } + private void OnRegionGenerated(object sender, Point regionPosition) { //_chunksDirty = true; @@ -289,22 +428,52 @@ private void OnRegionGenerated(object sender, Point regionPosition) Log.Info($"Region generated: {regionPosition.X:000}, {regionPosition.Y:000}"); } } - + #region Mouse Events private void UpdateMouseInput() { + if (GetIO().WantCaptureMouse) + return; + var newState = Mouse.GetState(); if (_mouseState.Position != newState.Position) { OnCursorMove(newState.Position, _mouseState.Position, _mouseState.LeftButton == ButtonState.Pressed); } - + _mouseState = newState; } + + private Vector3 Unproject(Vector2 cursor) + { + // return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f)); + return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Transform); + } + private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bool isCursorDown) { + var cursorBlockPos = Unproject(cursorPosition.ToVector2()); + // var cursorBlockPos = Vector3.Transform(new Vector3(cursorPosition.X, cursorPosition.Y, 0f), Transform*_effect.View); + _cursorBlock[0] = (int)cursorBlockPos.X; + _cursorBlock[2] = (int)cursorBlockPos.Y; + _cursorChunk[0] = _cursorBlock[0] >> 5; + _cursorChunk[1] = _cursorBlock[2] >> 5; + _cursorRegion[0] = _cursorBlock[0] >> 9; + _cursorRegion[1] = _cursorBlock[2] >> 9; + + var cursorBlockRegion = Map.GetRegion(new Point(_cursorRegion[0], _cursorRegion[1])); + if (cursorBlockRegion?.IsComplete ?? false) + { + var cursorBlockChunk = cursorBlockRegion[_cursorChunk[0] & 31, _cursorChunk[1] & 31]; + var x = _cursorBlock[0] & 16; + var z = _cursorBlock[2] & 16; + _cursorBlock[1] = (int)cursorBlockChunk.GetHeight(x, z); + _cursorBlockBiomeId = (int)cursorBlockChunk.GetBiome(x, z); + _cursorBlockBiome = Map.BiomeProvider.GetBiome(_cursorBlockBiomeId); + } + if (isCursorDown) { var p = (cursorPosition - previousCursorPosition); @@ -313,6 +482,5 @@ private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bo } #endregion - } } \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs index 071f552..01b67d9 100644 --- a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs +++ b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs @@ -8,42 +8,62 @@ public class CachedRegionMesh : IDisposable { // FaceGroupOptimizer // FaceGroupUtil + + public int X { get; } + public int Z { get; } public Texture2D Texture { get; private set; } public Matrix World { get; private set; } public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) { + X = region.X; + Z = region.Z; var t = new Texture2D(graphics, 1 << 9, 1 << 9); var d = new Color[(1 << 9) * (1 << 9)]; int x, y, z; byte b; Color c; - foreach (var chunk in region.Chunks) + + for (int cx = 0; cx < 32; cx++) + for (int cz = 0; cz < 32; cz++) { - for (int cx = 0; cx < 16; cx++) - for (int cz = 0; cz < 16; cz++) + var chunk = region[cx, cz]; + for (int bx = 0; bx < 16; bx++) + for (int bz = 0; bz < 16; bz++) { - x = (chunk.X << 4) + cx; - z = (chunk.Z << 4) + cz; - b = chunk.GetBiome(cx, cz); - y = chunk.GetHeight(cx, cz); - c = chunk.GetColor(cx, cz); + x = (cx << 4) + bx; + z = (cz << 4) + bz; + b = chunk.GetBiome(bx, bz); + y = chunk.GetHeight(bx, bz); + c = chunk.GetColor(bx, bz); - d[(((((chunk.X & 31) << 4)) + cx) * (1 << 9)) + (((chunk.Z & 31) << 4) + cz)] = c; +// d[(((((chunk.Z & 31) << 4)) + cz) * (1 << 9)) + (((chunk.X & 31) << 4) + cx)] = c; + SetData(d, x, z, c); } } + + for (int i = 0; i < 512; i++) + for (int j = 0; j < 5; j++) + { + SetData(d, i, j, Color.Red); + SetData(d, j, i, Color.Blue); + } t.SetData(d); Texture = t; - World = Matrix.Identity - * Matrix.CreateTranslation((region.X << 9), (region.Z << 9), 0f); + World = Matrix.CreateWorld(new Vector3((region.X << 9), (region.Z << 9), 0f), Vector3.Forward, Vector3.Up); } public void Dispose() { Texture?.Dispose(); } + + private static void SetData(Color[] data, int blockX, int blockZ, Color color) + { + data[(blockZ * 512) + blockX] = color; + } } } \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/MiMap.Viewer.DesktopGL.csproj b/src/MiMap.Viewer.DesktopGL/MiMap.Viewer.DesktopGL.csproj index 3cdab73..4171eaf 100644 --- a/src/MiMap.Viewer.DesktopGL/MiMap.Viewer.DesktopGL.csproj +++ b/src/MiMap.Viewer.DesktopGL/MiMap.Viewer.DesktopGL.csproj @@ -22,6 +22,9 @@ Always + + Always + diff --git a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs index 7032ba3..fc8e574 100644 --- a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MiMap.Viewer.DesktopGL.Components; using MiMap.Viewer.DesktopGL.Graphics; +using MiMap.Viewer.DesktopGL.Models; using MiNET.Worlds; using OpenAPI.WorldGenerator.Generators; @@ -68,7 +70,26 @@ protected override void Initialize() _mapViewer = new GuiMapViewer(this, Map); _mapViewer.Initialize(); Components.Add(_mapViewer); + + + // Initialize biome colors + InitializeBiomeColors(); + + } + + private void InitializeBiomeColors() + { + var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "biomeColors.json")); + var map = BiomeColors.FromJson(json); + foreach (var mapping in map.ColorMap) + { + var biome = Map.BiomeProvider.GetBiome(mapping.BiomeId); + if (biome != default) + { + biome.Color = System.Drawing.Color.FromArgb(mapping.JColor.R, mapping.JColor.G, mapping.JColor.B); + } + } } private void UpdateViewport(bool apply = true) diff --git a/src/MiMap.Viewer.DesktopGL/Models/BiomeColors.cs b/src/MiMap.Viewer.DesktopGL/Models/BiomeColors.cs new file mode 100644 index 0000000..2aa934e --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Models/BiomeColors.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace MiMap.Viewer.DesktopGL.Models +{ + public partial class BiomeColors + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("colorMap")] + public BiomeColorMapping[] ColorMap { get; set; } + + public static BiomeColors FromJson(string json) => JsonConvert.DeserializeObject(json); + public string ToJson(BiomeColors self) => JsonConvert.SerializeObject(self); + } + + [JsonConverter(typeof(BiomeColorMappingConverter))] + public partial class BiomeColorMapping + { + public byte BiomeId { get; set; } + + public JColor JColor { get; set; } + } + + public partial class JColor + { + [JsonProperty("r")] + public int R { get; set; } + + [JsonProperty("g")] + public int G { get; set; } + + [JsonProperty("b")] + public int B { get; set; } + } + + public class BiomeColorMappingConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, BiomeColorMapping value, JsonSerializer serializer) + { + writer.WriteStartArray(); + writer.WriteValue(value.BiomeId); + serializer.Serialize(writer, value.JColor); + writer.WriteEndArray(); + } + + public override BiomeColorMapping ReadJson(JsonReader reader, Type objectType, BiomeColorMapping existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartArray) + return null; + + var v = existingValue ?? new BiomeColorMapping(); + v.BiomeId = (byte)(reader.ReadAsInt32() ?? 0); + reader.Read(); + v.JColor = serializer.Deserialize(reader); + + reader.Read(); // end array + return v; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Models/Map.cs b/src/MiMap.Viewer.DesktopGL/Models/Map.cs index 5f40aaa..357d026 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/Map.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/Map.cs @@ -9,6 +9,7 @@ using MiNET.Utils.Vectors; using MiNET.Worlds; using NLog; +using OpenAPI.WorldGenerator.Generators.Biomes; namespace MiMap.Viewer.DesktopGL { @@ -27,6 +28,8 @@ public class Map : IDisposable private AutoResetEvent _trigger; private bool _running; + public readonly BiomeProvider BiomeProvider = new BiomeProvider(); + public Map(IWorldGenerator worldGenerator) { _worldGenerator = worldGenerator; @@ -115,7 +118,9 @@ private MapChunk ExtractChunkData(ChunkColumn chunk) public MapRegion GetRegion(Point regionPosition) { - return Regions[regionPosition]; + if (Regions.TryGetValue(regionPosition, out var region)) + return region; + return null; } public IEnumerable GetRegions() diff --git a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs index 9731bd7..e356ef1 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs @@ -1,40 +1,9 @@ using System; using Microsoft.Xna.Framework; +using OpenAPI.WorldGenerator.Utils; namespace MiMap.Viewer.DesktopGL { - public class MapRegion - { - public readonly int X; - public readonly int Z; - - public readonly MapChunk[] Chunks; - public bool IsComplete { get; internal set; } - - public MapRegion(int x, int z) - { - X = x; - Z = z; - Chunks = new MapChunk[32 * 32]; - } - - public void SetChunk(int cx, int cz, MapChunk chunk) - { - Chunks[GetIndex(cx & 31, cz & 31)] = chunk; - } - - public MapChunk this[int cx, int cz] - { - get => Chunks[GetIndex(cx & 31, cz & 31)]; - set => Chunks[GetIndex(cx & 31, cz & 31)] = value; - } - - private int GetIndex(int x, int z) - { - return (x * 32) + z; - } - } - public class MapChunk { public readonly int X; @@ -88,9 +57,14 @@ private void UpdateColor(int i) var height = Heights[i]; var hIntensity = MathHelper.Clamp((height % (255f / 25f)) / 25f, 0f, 1f) / 2; - var c1 = Globals.BiomeColors[biome]; + // var c1 = Globals.BiomeColors[biome]; + var c1d = MiMapViewer.Instance.Map.BiomeProvider.GetBiome(biome)?.Color; + var c1 = c1d.HasValue + ? new Color(c1d.Value.R, c1d.Value.G, c1d.Value.B, c1d.Value.A) + : Color.HotPink; var c2 = Color.Black; - Colors[i] = Color.Lerp(c1, c2, hIntensity); + // Colors[i] = Color.Lerp(c1, c2, hIntensity); + Colors[i] = c1; } private int GetIndex(int x, int z) diff --git a/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs b/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs new file mode 100644 index 0000000..e7e6eff --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs @@ -0,0 +1,34 @@ +namespace MiMap.Viewer.DesktopGL +{ + public class MapRegion + { + public readonly int X; + public readonly int Z; + + public readonly MapChunk[] Chunks; + public bool IsComplete { get; internal set; } + + public MapRegion(int x, int z) + { + X = x; + Z = z; + Chunks = new MapChunk[32 * 32]; + } + + public void SetChunk(int cx, int cz, MapChunk chunk) + { + Chunks[GetIndex(cx & 31, cz & 31)] = chunk; + } + + public MapChunk this[int cx, int cz] + { + get => Chunks[GetIndex(cx & 31, cz & 31)]; + set => Chunks[GetIndex(cx & 31, cz & 31)] = value; + } + + private int GetIndex(int x, int z) + { + return (x * 32) + z; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/biomeColors.json b/src/MiMap.Viewer.DesktopGL/biomeColors.json new file mode 100644 index 0000000..2ab037e --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/biomeColors.json @@ -0,0 +1,83 @@ +{ + "name":"test", + "colorMap":[ + [ 0, { "r":0, "g":0, "b":112 } ], + [ 1, { "r":141, "g":179, "b":96 } ], + [ 2, { "r":250, "g":148, "b":24 } ], + [ 3, { "r":96, "g":96, "b":96 } ], + [ 4, { "r":5, "g":102, "b":33 } ], + [ 5, { "r":11, "g":2, "b":89 } ], + [ 6, { "r":7, "g":249, "b":178 } ], + [ 7, { "r":0, "g":0, "b":255 } ], + [ 8, { "r":255, "g":0, "b":0 } ], + [ 9, { "r":128, "g":128, "b":255 } ], + [ 10, { "r":112, "g":112, "b":214 } ], + [ 11, { "r":160, "g":160, "b":255 } ], + [ 12, { "r":255, "g":255, "b":255 } ], + [ 13, { "r":160, "g":160, "b":160 } ], + [ 14, { "r":255, "g":0, "b":255 } ], + [ 15, { "r":160, "g":0, "b":255 } ], + [ 16, { "r":250, "g":222, "b":85 } ], + [ 17, { "r":210, "g":95, "b":18 } ], + [ 18, { "r":34, "g":85, "b":28 } ], + [ 19, { "r":22, "g":57, "b":51 } ], + [ 20, { "r":114, "g":120, "b":154 } ], + [ 21, { "r":83, "g":123, "b":9 } ], + [ 22, { "r":44, "g":66, "b":5 } ], + [ 23, { "r":98, "g":139, "b":23 } ], + [ 24, { "r":0, "g":0, "b":48 } ], + [ 25, { "r":162, "g":162, "b":132 } ], + [ 26, { "r":250, "g":240, "b":192 } ], + [ 27, { "r":48, "g":116, "b":68 } ], + [ 28, { "r":31, "g":5, "b":50 } ], + [ 29, { "r":64, "g":81, "b":26 } ], + [ 30, { "r":49, "g":85, "b":74 } ], + [ 31, { "r":36, "g":63, "b":54 } ], + [ 32, { "r":89, "g":102, "b":81 } ], + [ 33, { "r":69, "g":7, "b":62 } ], + [ 34, { "r":80, "g":112, "b":80 } ], + [ 35, { "r":189, "g":18, "b":95 } ], + [ 36, { "r":167, "g":157, "b":100 } ], + [ 37, { "r":217, "g":69, "b":21 } ], + [ 38, { "r":17, "g":151, "b":101 } ], + [ 39, { "r":202, "g":140, "b":101 } ], + [ 40, { "r":128, "g":128, "b":255 } ], + [ 41, { "r":128, "g":128, "b":255 } ], + [ 42, { "r":128, "g":128, "b":255 } ], + [ 43, { "r":128, "g":128, "b":255 } ], + [ 44, { "r":0, "g":0, "b":172 } ], + [ 45, { "r":0, "g":0, "b":144 } ], + [ 46, { "r":32, "g":32, "b":112 } ], + [ 47, { "r":0, "g":0, "b":80 } ], + [ 48, { "r":0, "g":0, "b":64 } ], + [ 49, { "r":32, "g":32, "b":56 } ], + [ 50, { "r":64, "g":64, "b":144 } ], + [ 127, { "r":0, "g":0, "b":0 } ], + [ 129, { "r":181, "g":219, "b":136 } ], + [ 130, { "r":255, "g":188, "b":64 } ], + [ 131, { "r":136, "g":136, "b":136 } ], + [ 132, { "r":45, "g":142, "b":73 } ], + [ 133, { "r":51, "g":142, "b":19 } ], + [ 134, { "r":47, "g":255, "b":18 } ], + [ 140, { "r":180, "g":20, "b":220 } ], + [ 149, { "r":123, "g":13, "b":49 } ], + [ 151, { "r":138, "g":179, "b":63 } ], + [ 155, { "r":88, "g":156, "b":108 } ], + [ 156, { "r":71, "g":15, "b":90 } ], + [ 157, { "r":104, "g":121, "b":66 } ], + [ 158, { "r":89, "g":125, "b":114 } ], + [ 160, { "r":129, "g":142, "b":121 } ], + [ 161, { "r":109, "g":119, "b":102 } ], + [ 162, { "r":120, "g":52, "b":120 } ], + [ 163, { "r":229, "g":218, "b":135 } ], + [ 164, { "r":207, "g":197, "b":140 } ], + [ 165, { "r":255, "g":109, "b":61 } ], + [ 166, { "r":216, "g":191, "b":141 } ], + [ 167, { "r":242, "g":180, "b":141 } ], + [ 168, { "r":118, "g":142, "b":20 } ], + [ 169, { "r":59, "g":71, "b":10 } ], + [ 170, { "r":82, "g":41, "b":33 } ], + [ 171, { "r":221, "g":8, "b":8 } ], + [ 172, { "r":73, "g":144, "b":123 } ] + ] +} \ No newline at end of file From c8ba1fd5e06165f1d659d8b1cf738a3a5f5aae8c Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 10:14:22 +0200 Subject: [PATCH 04/13] replace BiomeProvider with BiomeRegistry + fix file-scoped namespaces --- .../Components/GuiMapViewer.cs | 8 +++---- src/MiMap.Viewer.DesktopGL/MiMapViewer.cs | 2 +- src/MiMap.Viewer.DesktopGL/Models/Map.cs | 2 +- src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs | 4 ++-- .../Vanilla/Forest/BirchForestHillsMBiome.cs | 5 ++-- .../Vanilla/Forest/BirchForestMBiome.cs | 5 ++-- .../Vanilla/Forest/FlowerForestBiome.cs | 5 ++-- .../Terrain/BirchForestHillsMTerrain.cs | 5 ++-- .../Generators/Terrain/BirchForestMTerrain.cs | 5 ++-- .../Terrain/VanillaMesaPlateauTerrain.cs | 5 ++-- .../Utils/ColorUtils.cs | 23 ++++++++++--------- .../Utils/Noise/Primitives/NoiseQuality.cs | 5 ++-- 12 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index 6b9d138..792da0f 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -268,14 +268,14 @@ private void DrawImGui() var cfgAllowScenicLakes = cfg?.AllowScenicLakes ?? false; var cfgSurfaceBlendIn = cfg?.SurfaceBlendIn ?? false; var cfgSurfaceBlendOut = cfg?.SurfaceBlendOut ?? false; - var cfgWeightMultiplier = cfg?.WeightMultiplier ?? 0; + var cfgWeight = cfg?.Weight ?? 0; Checkbox("Is Edge Biome", ref cfgIsEdgeBiome); Checkbox("Allow Rivers", ref cfgAllowRivers); Checkbox("Allow Scenic Lakes", ref cfgAllowScenicLakes); Checkbox("Surface Blend In", ref cfgSurfaceBlendIn); Checkbox("Surface Blend Out", ref cfgSurfaceBlendOut); - InputFloat("Weight Multiplier", ref cfgWeightMultiplier); + InputInt("Weight", ref cfgWeight); EndDisabled(); @@ -293,7 +293,7 @@ private void DrawImGui() { if (BeginTable("biomeclr", 3)) { - foreach (var c in Map.BiomeProvider.Biomes) + foreach (var c in Map.BiomeRegistry.Biomes) { TableNextRow(); TableNextColumn(); @@ -471,7 +471,7 @@ private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bo var z = _cursorBlock[2] & 16; _cursorBlock[1] = (int)cursorBlockChunk.GetHeight(x, z); _cursorBlockBiomeId = (int)cursorBlockChunk.GetBiome(x, z); - _cursorBlockBiome = Map.BiomeProvider.GetBiome(_cursorBlockBiomeId); + _cursorBlockBiome = Map.BiomeRegistry.GetBiome(_cursorBlockBiomeId); } if (isCursorDown) diff --git a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs index fc8e574..285772b 100644 --- a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs @@ -84,7 +84,7 @@ private void InitializeBiomeColors() foreach (var mapping in map.ColorMap) { - var biome = Map.BiomeProvider.GetBiome(mapping.BiomeId); + var biome = Map.BiomeRegistry.GetBiome(mapping.BiomeId); if (biome != default) { biome.Color = System.Drawing.Color.FromArgb(mapping.JColor.R, mapping.JColor.G, mapping.JColor.B); diff --git a/src/MiMap.Viewer.DesktopGL/Models/Map.cs b/src/MiMap.Viewer.DesktopGL/Models/Map.cs index 357d026..a5eda16 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/Map.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/Map.cs @@ -28,7 +28,7 @@ public class Map : IDisposable private AutoResetEvent _trigger; private bool _running; - public readonly BiomeProvider BiomeProvider = new BiomeProvider(); + public readonly BiomeRegistry BiomeRegistry = new BiomeRegistry(); public Map(IWorldGenerator worldGenerator) { diff --git a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs index e356ef1..87dcc32 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs @@ -58,9 +58,9 @@ private void UpdateColor(int i) var hIntensity = MathHelper.Clamp((height % (255f / 25f)) / 25f, 0f, 1f) / 2; // var c1 = Globals.BiomeColors[biome]; - var c1d = MiMapViewer.Instance.Map.BiomeProvider.GetBiome(biome)?.Color; + var c1d = MiMapViewer.Instance.Map.BiomeRegistry.GetBiome(biome)?.Color; var c1 = c1d.HasValue - ? new Color(c1d.Value.R, c1d.Value.G, c1d.Value.B, c1d.Value.A) + ? new Color((byte)c1d.Value.R, c1d.Value.G, c1d.Value.B, c1d.Value.A) : Color.HotPink; var c2 = Color.Black; // Colors[i] = Color.Lerp(c1, c2, hIntensity); diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs index 8382cc8..44820b7 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs @@ -2,8 +2,8 @@ using OpenAPI.WorldGenerator.Generators.Terrain; using OpenAPI.WorldGenerator.Utils; -namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest; - +namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest +{ public class BirchForestHillsMBiome : BiomeBase { public BirchForestHillsMBiome() @@ -19,4 +19,5 @@ public BirchForestHillsMBiome() Color = ColorUtils.FromHtml("#47875A"); } +} } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestMBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestMBiome.cs index f344cdf..6f6de2a 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestMBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestMBiome.cs @@ -2,8 +2,8 @@ using OpenAPI.WorldGenerator.Generators.Terrain; using OpenAPI.WorldGenerator.Utils; -namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest; - +namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest +{ public class BirchForestMBiome : BiomeBase { public BirchForestMBiome() @@ -19,4 +19,5 @@ public BirchForestMBiome() Color = ColorUtils.FromHtml("#589C6C"); } +} } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/FlowerForestBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/FlowerForestBiome.cs index 584f2cb..767eb12 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/FlowerForestBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/FlowerForestBiome.cs @@ -2,8 +2,8 @@ using OpenAPI.WorldGenerator.Generators.Terrain; using OpenAPI.WorldGenerator.Utils; -namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest; - +namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest +{ public class FlowerForestBiome : BiomeBase { public FlowerForestBiome() @@ -20,4 +20,5 @@ public FlowerForestBiome() Color = ColorUtils.FromHtml("#2D8E49"); } +} } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs index 7ce19ea..85e6686 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs @@ -1,5 +1,5 @@ -namespace OpenAPI.WorldGenerator.Generators.Terrain; - +namespace OpenAPI.WorldGenerator.Generators.Terrain +{ public class BirchForestHillsMTerrain : TerrainBase { private float HillStrength { get; set; } = 70f; @@ -17,4 +17,5 @@ public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y { return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight /*- generator.Preset.SeaLevel*/); } +} } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs index 0c37ebd..80989b2 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs @@ -1,7 +1,7 @@ using OpenAPI.WorldGenerator.Generators.Effects; -namespace OpenAPI.WorldGenerator.Generators.Terrain; - +namespace OpenAPI.WorldGenerator.Generators.Terrain +{ public class BirchForestMTerrain : TerrainBase { private GroundEffect GroundEffect { get; set; }= new GroundEffect(4f); @@ -11,4 +11,5 @@ public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, 80f, 65f); return TerrainForest(generator, x, y, river, generator.Preset.SeaLevel + 3 + GroundEffect.Added(generator, x, y)); } +} } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/VanillaMesaPlateauTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/VanillaMesaPlateauTerrain.cs index b41de55..5657ec5 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/VanillaMesaPlateauTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/VanillaMesaPlateauTerrain.cs @@ -1,5 +1,5 @@ -namespace OpenAPI.WorldGenerator.Generators.Terrain; - +namespace OpenAPI.WorldGenerator.Generators.Terrain +{ public class VanillaMesaPlateauTerrain : TerrainBase { private readonly float[] _height; @@ -23,4 +23,5 @@ public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y { return TerrainPlateau(generator, x, y, river, _height, border, _strength, _heightLength, 100f, false); } +} } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Utils/ColorUtils.cs b/src/OpenAPI.WorldGenerator/Utils/ColorUtils.cs index c94d27f..7be867d 100644 --- a/src/OpenAPI.WorldGenerator/Utils/ColorUtils.cs +++ b/src/OpenAPI.WorldGenerator/Utils/ColorUtils.cs @@ -1,20 +1,21 @@ using System; using System.Drawing; -namespace OpenAPI.WorldGenerator.Utils; - -public static class ColorUtils +namespace OpenAPI.WorldGenerator.Utils { - public static Color FromHtml(string hex) + public static class ColorUtils { - if (hex.StartsWith("#")) - hex = hex.Substring(1); + public static Color FromHtml(string hex) + { + if (hex.StartsWith("#")) + hex = hex.Substring(1); - if (hex.Length != 6) throw new Exception("Color not valid"); + if (hex.Length != 6) throw new Exception("Color not valid"); - return Color.FromArgb( - int.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber), - int.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber), - int.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber)); + return Color.FromArgb( + int.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber), + int.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber), + int.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber)); + } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/NoiseQuality.cs b/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/NoiseQuality.cs index f660fc4..fda5947 100644 --- a/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/NoiseQuality.cs +++ b/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/NoiseQuality.cs @@ -1,8 +1,9 @@ -namespace OpenAPI.WorldGenerator.Utils.Noise.Primitives; - +namespace OpenAPI.WorldGenerator.Utils.Noise.Primitives +{ public enum NoiseQuality : byte { Fast, Standard, Best, +} } \ No newline at end of file From 4a10c1eb9232b3d57bb371525547bce0f936348d Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 14:42:14 +0200 Subject: [PATCH 05/13] Working viewer! --- .../Components/GuiMapViewer.cs | 328 +++++++++++++++--- .../Graphics/CachedChunkMesh.cs | 68 ++++ .../Graphics/CachedRegionMesh.cs | 19 +- .../Graphics/RegionMeshManager.cs | 30 +- .../Graphics/SpriteBatchExtensions.cs | 113 ++++++ src/MiMap.Viewer.DesktopGL/MiMapViewer.cs | 2 + src/MiMap.Viewer.DesktopGL/Models/Map.cs | 61 +++- src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs | 12 +- .../Models/MapRegion.cs | 8 +- src/MiMap.Viewer.DesktopGL/NLog.config | 53 +-- src/MiMap.Viewer.DesktopGL/Program.cs | 13 +- 11 files changed, 612 insertions(+), 95 deletions(-) create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/CachedChunkMesh.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index 792da0f..9842d3c 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -6,6 +6,7 @@ // * / using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using ImGuiNET; @@ -20,16 +21,24 @@ namespace MiMap.Viewer.DesktopGL.Components { + public enum MapViewMode + { + Region, + Chunk + } + public class GuiMapViewer : DrawableGameComponent { private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); private Point _position = Point.Zero; + private Rectangle _visibleChunkBounds; private Rectangle _mapBounds; private float _scale = 1f; private readonly object _chunkSync = new object(); private List _regions; - private List _chunks; + private List _chunks; + private ConcurrentQueue _pendingChunks; private bool _chunksDirty = false; private Map _map; private Point _mapPosition; @@ -38,18 +47,24 @@ public class GuiMapViewer : DrawableGameComponent private bool _visible = true; private IRegionMeshManager _regionMeshManager; private BasicEffect _effect; + private BasicEffect _spriteBatchEffect; private VertexBuffer _vertexBuffer; private IndexBuffer _indexBuffer; private ImGuiRenderer _gui; private readonly RasterizerState _rasterizerState; private Rectangle _bounds; private MouseState _mouseState; + private Point _cursorPosition; private int[] _cursorBlock = new int[3]; private BiomeBase _cursorBlockBiome; private int[] _cursorChunk = new int[2]; private int[] _cursorRegion = new int[2]; private int _cursorBlockBiomeId; + private SpriteBatch _spriteBatch; + + private MapViewMode _mode = MapViewMode.Chunk; + public Rectangle Bounds { get => _bounds; @@ -57,7 +72,7 @@ public Rectangle Bounds { if (_bounds == value) return; _bounds = value; - RecalculateTransform(); + RecalculateMapBounds(); } } @@ -93,6 +108,7 @@ public Map Map if (_map != default) { _map.RegionGenerated -= OnRegionGenerated; + _map.ChunkGenerated -= OnChunkGenerated; } _map = value; @@ -100,10 +116,12 @@ public Map Map if (_map != default) { _map.RegionGenerated += OnRegionGenerated; + _map.ChunkGenerated += OnChunkGenerated; } } } + public Matrix Transform { get => _transform; @@ -120,7 +138,8 @@ public GuiMapViewer(MiMapViewer game, Map map) : base(game) { Map = map; _regions = new List(); - _chunks = new List(); + _chunks = new List(); + _pendingChunks = new ConcurrentQueue(); _regionMeshManager = game.RegionMeshManager; _gui = game.ImGuiRenderer; _rasterizerState = new RasterizerState() @@ -137,6 +156,7 @@ public override void Initialize() { base.Initialize(); + _spriteBatch = new SpriteBatch(GraphicsDevice); _effect = new BasicEffect(GraphicsDevice) { LightingEnabled = false, @@ -144,6 +164,11 @@ public override void Initialize() TextureEnabled = true, FogEnabled = false }; + _spriteBatchEffect = new BasicEffect(GraphicsDevice) + { + TextureEnabled = true, + VertexColorEnabled = true + }; Game.GraphicsDevice.DeviceReset += (s, o) => UpdateBounds(); Game.Activated += (s, o) => UpdateBounds(); @@ -169,20 +194,41 @@ public override void Initialize() public override void Update(GameTime gameTime) { + UpdateBounds(); UpdateMouseInput(); + if (Map == default) return; if (_chunksDirty) { - var regions = Map.GetRegions(_mapBounds); - lock (_chunkSync) + if (_mode == MapViewMode.Region) { - _regions.Clear(); - _regions.AddRange(regions.Where(r => r.IsComplete).Select(r => _regionMeshManager.CacheRegion(r))); + var regions = Map.GetRegions(_mapBounds); + lock (_chunkSync) + { + _regions.Clear(); + _regions.AddRange(regions.Where(r => r.IsComplete).Select(r => _regionMeshManager.CacheRegion(r))); + } + } + else if (_mode == MapViewMode.Chunk) + { + Map.EnqueueChunks(_mapBounds); // generate the visible chunks } _chunksDirty = false; } + + if (_mode == MapViewMode.Chunk) + { + if (!_pendingChunks.IsEmpty) + { + MapChunk c; + while (_pendingChunks.TryDequeue(out c)) + { + _chunks.Add(_regionMeshManager.CacheChunk(c)); + } + } + } } #region Drawing @@ -209,16 +255,38 @@ private void DrawImGui() { if (Begin("Map Viewer")) { - if (BeginTable("mapviewtable", 2)) + if (BeginTable("mapviewtable", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) { - // PushID("mapviewtable_Scale"); + _mapPositions[0] = _mapPosition.X; + _mapPositions[1] = _mapPosition.Y; + TableNextRow(); + TableNextColumn(); + Text("Map Position"); + TableNextColumn(); + InputInt2("##value", ref _mapPositions[0]); + if (IsItemEdited()) + { + MapPosition = new Point(_mapPositions[0], _mapPositions[1]); + } + + var bounds = MapBounds; + var boundsValues = new int[] { bounds.X, bounds.Y, bounds.Width, bounds.Height }; + + TableNextRow(); + TableNextColumn(); + Text("Map Bounds"); + TableNextColumn(); + InputInt4("##value", ref boundsValues[0], ImGuiInputTextFlags.ReadOnly); + TableNextRow(); TableNextColumn(); Text("Scale"); TableNextColumn(); - // TableSetColumnIndex(1); - // SetNextItemWidth(-float.MinValue); SliderFloat("##value", ref _scale, 0.01f, 8f); + if (IsItemEdited()) + { + RecalculateTransform(); + } TableNextRow(); @@ -226,6 +294,96 @@ private void DrawImGui() EndTable(); } + Spacing(); + + if (BeginTable("mapviewtable", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) + { + var cursorPositionValues = new int[] + { + _cursorPosition.X, + _cursorPosition.Y + }; + TableNextRow(); + TableNextColumn(); + Text("Cursor Position"); + TableNextColumn(); + InputInt2("##value", ref cursorPositionValues[0], ImGuiInputTextFlags.ReadOnly); + + + EndTable(); + } + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNodeEx("Graphics")) + { + if (BeginTable("graphics", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit)) + { + TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); + TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); + + var v = GraphicsDevice.Viewport; + var viewportValues = new int[] + { + v.X, + v.Y, + v.Width, + v.Height + }; + + TableNextRow(); + TableNextColumn(); + Text("Viewport"); + TableNextColumn(); + InputInt4("##value", ref viewportValues[0], ImGuiInputTextFlags.ReadOnly); + + EndTable(); + } + + TreePop(); + } + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNodeEx("Window")) + { + if (BeginTable("window", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit)) + { + TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); + TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); + + var p = Game.Window.Position; + var windowPositionValues = new int[] + { + p.X, + p.Y + }; + TableNextRow(); + TableNextColumn(); + Text("Position"); + TableNextColumn(); + InputInt2("##value", ref windowPositionValues[0], ImGuiInputTextFlags.ReadOnly); + + + var c = Game.Window.ClientBounds; + var windowClientBoundsValues = new int[] + { + c.X, + c.Y, + c.Width, + c.Height + }; + TableNextRow(); + TableNextColumn(); + Text("Client Bounds"); + TableNextColumn(); + InputInt4("##value", ref windowClientBoundsValues[0], ImGuiInputTextFlags.ReadOnly); + + + EndTable(); + } + + TreePop(); + } + End(); } @@ -291,7 +449,7 @@ private void DrawImGui() if (Begin("Biome Colors")) { - if (BeginTable("biomeclr", 3)) + if (BeginTable("biomeclr", 3, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) { foreach (var c in Map.BiomeRegistry.Biomes) { @@ -304,10 +462,10 @@ private void DrawImGui() TableSetBgColor(ImGuiTableBgTarget.CellBg, GetColor(c.Color ?? System.Drawing.Color.Transparent)); Text(" "); } - + EndTable(); } - + End(); } } @@ -333,8 +491,37 @@ private static uint GetColor(System.Drawing.Color color) // | 0xFF // ); } - + private void DrawMap(GameTime gameTime) + { + if (_mode == MapViewMode.Region) + DrawMap_Region(gameTime); + else if (_mode == MapViewMode.Chunk) + DrawMap_Chunk(gameTime); + } + + private static readonly Point RegionSize = new Point(512, 512); + private static readonly Rectangle ChunkSize = new Rectangle(0, 0, 16, 16); + private int[] _mapPositions = new int[2]; + + private void DrawMap_Chunk(GameTime gameTime) + { + _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone, _spriteBatchEffect, Transform); + + foreach (var chunk in _chunks) + { + _spriteBatch.Draw(chunk.Texture, chunk.Position.ToVector2(), Color.White); + // _spriteBatch.Draw(chunk.Texture, chunk.Position.ToVector2(), ChunkSize, Color.White, 0, Vector2.Zero, _scale,SpriteEffects.FlipVertically, 0); + } + + _spriteBatch.DrawRectangle(new Rectangle(_cursorBlock[0], _cursorBlock[2], 1, 1), Color.White); + _spriteBatch.DrawRectangle(new Rectangle(_cursorChunk[0] << 4, _cursorChunk[1] << 4, 16, 16), Color.Yellow); + _spriteBatch.DrawRectangle(new Rectangle(_cursorRegion[0] << 9, _cursorRegion[1] << 9, 512, 512), Color.PaleGreen); + + _spriteBatch.End(); + } + + private void DrawMap_Region(GameTime gameTime) { using (var cxt = GraphicsContext.CreateContext(GraphicsDevice, BlendState.AlphaBlend, DepthStencilState.None, _rasterizerState, SamplerState.PointClamp)) { @@ -372,17 +559,25 @@ private void DrawMap(GameTime gameTime) #endregion + private Rectangle _previousClientBounds = Rectangle.Empty; + private void UpdateBounds() { - Bounds = new Rectangle(Point.Zero, Game.Window.ClientBounds.Size); + var clientBounds = Game.Window.ClientBounds; + if (clientBounds != _previousClientBounds) + { + Bounds = new Rectangle(Point.Zero, Game.Window.ClientBounds.Size); + RecalculateMapBounds(); + } } private void RecalculateTransform() { var p = MapPosition; Transform = Matrix.Identity - * Matrix.CreateTranslation(-p.X, -p.Y, 0) + // * Matrix.CreateRotationX(MathHelper.Pi) * Matrix.CreateScale(_scale, _scale, 1f) + * Matrix.CreateTranslation(-p.X, -p.Y, 0) ; RecalculateMapBounds(); @@ -390,42 +585,69 @@ private void RecalculateTransform() private void RecalculateMapBounds() { - var screenBounds = Bounds; - var screenSize = new Vector2(screenBounds.Width, screenBounds.Height); - - var blockBoundsMin = Unproject(Vector2.Zero); - var blockBoundsSize = Unproject(screenSize) - blockBoundsMin; - - var bbSize = new Point((int)Math.Ceiling(blockBoundsSize.X), (int)Math.Ceiling(blockBoundsSize.Y)); - var bbMin = new Point((int)Math.Floor(blockBoundsMin.X), (int)Math.Floor(blockBoundsMin.Y)); - + var screenBounds = _bounds; + + // var bbSize = (_bounds.Size.ToVector2()) / _scale; + // var bbCenter = (_mapPosition.ToVector2() + _bounds.Center.ToVector2()) / _scale; + // var bbMin = _mapPosition.ToVector2() / _scale; + var bbMin = Unproject(Point.Zero); + var bbSize = Unproject(_bounds.Size) - bbMin; + MapBounds = new Rectangle(bbMin, bbSize); - + if (_effect != default) { _effect.Projection = Matrix.CreateOrthographic(screenBounds.Width, screenBounds.Height, 0.1f, 100f); - - var p = Vector3.Transform(new Vector3(0f, 0f, 0f), Transform); - + _effect.View = Matrix.CreateLookAt(new Vector3(0, 0, 10), new Vector3(0, 0, 0), Vector3.Up); _effect.World = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up); } + + if (_spriteBatchEffect != default) + { + _spriteBatchEffect.World = Transform; + _spriteBatchEffect.View = Matrix.CreateLookAt(Vector3.Backward, Vector3.Zero, Vector3.Up); + // _spriteBatchEffect.Projection = Matrix.CreateOrthographicOffCenter(0, screenSize.X / _scale, 0, screenSize.Y / _scale, 0.1f, 100f); + _spriteBatchEffect.Projection = Matrix.CreateOrthographicOffCenter(0, screenBounds.Width, screenBounds.Height, 0, 0f, 1f); + } } private void OnMapBoundsChanged() { - _chunksDirty = true; + //_chunksDirty = true; + var b = _mapBounds; + var visibleChunkBounds = new Rectangle(b.X >> 4, b.Y >> 4, b.Width >> 4, b.Height >> 4); + if (visibleChunkBounds != _visibleChunkBounds) + { + _chunksDirty = true; + } + + _visibleChunkBounds = visibleChunkBounds; + Log.Info($"Map bounds changed: {_mapBounds.X:000}, {_mapBounds.Y:000} => {_mapBounds.Width:0000} x {_mapBounds.Height:0000}"); } private void OnRegionGenerated(object sender, Point regionPosition) { //_chunksDirty = true; - var region = Map.GetRegion(regionPosition); - if (region != null) + // var region = Map.GetRegion(regionPosition); + // if (region != null) + // { + // _regions.Add(_regionMeshManager.CacheRegion(region)); + // + // Log.Info($"Region generated: {regionPosition.X:000}, {regionPosition.Y:000}"); + // } + } + + private void OnChunkGenerated(object sender, Point chunkPosition) + { + // _chunksDirty = true; + var chunk = Map.GetChunk(chunkPosition); + if (chunk != null) { - _regions.Add(_regionMeshManager.CacheRegion(region)); - Log.Info($"Region generated: {regionPosition.X:000}, {regionPosition.Y:000}"); + _pendingChunks.Enqueue(chunk); + + Log.Debug($"Chunk generated: {chunkPosition.X:000}, {chunkPosition.Y:000}"); } } @@ -440,35 +662,47 @@ private void UpdateMouseInput() if (_mouseState.Position != newState.Position) { - OnCursorMove(newState.Position, _mouseState.Position, _mouseState.LeftButton == ButtonState.Pressed); + var bounds = Game.Window.ClientBounds; + // var currPos = new Point(newState.X - bounds.X, bounds.Height - (newState.Y - bounds.Y)); + // var prevPos = new Point(_mouseState.X - bounds.X, bounds.Height - (_mouseState.Y - bounds.Y)); + //var currPos = new Point(newState.X - bounds.X, newState.Y - bounds.Y); + //var prevPos = new Point(_mouseState.X - bounds.X, _mouseState.Y - bounds.Y); + + var currPos = new Point(newState.X, newState.Y); + var prevPos = new Point(_mouseState.X, _mouseState.Y); + OnCursorMove(currPos, prevPos, _mouseState.LeftButton == ButtonState.Pressed); + _cursorPosition = currPos; } _mouseState = newState; } - private Vector3 Unproject(Vector2 cursor) + private Point Unproject(Point cursor) { + return _mapPosition + (cursor.ToVector2() * _scale).ToPoint(); + +// return Vector2.Transform(cursor, InverseTransform); // return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f)); - return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Transform); + //return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Transform); } private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bool isCursorDown) { - var cursorBlockPos = Unproject(cursorPosition.ToVector2()); + var cursorBlockPos = Unproject(cursorPosition); // var cursorBlockPos = Vector3.Transform(new Vector3(cursorPosition.X, cursorPosition.Y, 0f), Transform*_effect.View); - _cursorBlock[0] = (int)cursorBlockPos.X; - _cursorBlock[2] = (int)cursorBlockPos.Y; - _cursorChunk[0] = _cursorBlock[0] >> 5; - _cursorChunk[1] = _cursorBlock[2] >> 5; + _cursorBlock[0] = cursorBlockPos.X; + _cursorBlock[2] = cursorBlockPos.Y; + _cursorChunk[0] = _cursorBlock[0] >> 4; + _cursorChunk[1] = _cursorBlock[2] >> 4; _cursorRegion[0] = _cursorBlock[0] >> 9; _cursorRegion[1] = _cursorBlock[2] >> 9; var cursorBlockRegion = Map.GetRegion(new Point(_cursorRegion[0], _cursorRegion[1])); if (cursorBlockRegion?.IsComplete ?? false) { - var cursorBlockChunk = cursorBlockRegion[_cursorChunk[0] & 31, _cursorChunk[1] & 31]; - var x = _cursorBlock[0] & 16; - var z = _cursorBlock[2] & 16; + var cursorBlockChunk = cursorBlockRegion[_cursorChunk[0] % 32, _cursorChunk[1] % 32]; + var x = _cursorBlock[0] % 16; + var z = _cursorBlock[2] % 16; _cursorBlock[1] = (int)cursorBlockChunk.GetHeight(x, z); _cursorBlockBiomeId = (int)cursorBlockChunk.GetBiome(x, z); _cursorBlockBiome = Map.BiomeRegistry.GetBiome(_cursorBlockBiomeId); @@ -477,7 +711,7 @@ private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bo if (isCursorDown) { var p = (cursorPosition - previousCursorPosition); - MapPosition += new Point(-p.X, p.Y); + MapPosition += new Point(-p.X, -p.Y); } } diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/CachedChunkMesh.cs b/src/MiMap.Viewer.DesktopGL/Graphics/CachedChunkMesh.cs new file mode 100644 index 0000000..06bdb41 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/CachedChunkMesh.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + public class CachedChunkMesh : ITile, IDisposable + { + // FaceGroupOptimizer + // FaceGroupUtil + + public int X { get; } + public int Z { get; } + public Point Position { get; } + public Texture2D Texture { get; private set; } + + public Matrix World { get; private set; } + + public CachedChunkMesh(GraphicsDevice graphics, MapChunk chunk) + { + X = chunk.X; + Z = chunk.Z; + Position = new Point(X << 4, Z << 4); + + var t = new Texture2D(graphics, 1 << 4, 1 << 4); + var d = new Color[(1 << 4) * (1 << 4)]; + + int x, y, z; + byte b; + Color c; + + for (int bx = 0; bx < 16; bx++) + for (int bz = 0; bz < 16; bz++) + { + b = chunk.GetBiome(bx, bz); + y = chunk.GetHeight(bx, bz); + c = chunk.GetColor(bx, bz); + + SetData(d, bx, bz, c); + } + + + // for (int i = 0; i < 16; i++) + // for (int j = 0; j < 1; j++) + // { + // SetData(d, i, j, Color.Red * 0.5f); + // SetData(d, j, i, Color.Blue * 0.5f); + // } + + t.SetData(d); + Texture = t; + World = Matrix.CreateWorld(new Vector3((chunk.X << 4), (chunk.Z << 4), 0f), Vector3.Forward, Vector3.Up); + + } + + public void Dispose() + { + Texture?.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetData(Color[] data, int blockX, int blockZ, Color color) + { + data[(blockZ * 16) + blockX] = color; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs index 01b67d9..8b320c0 100644 --- a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs +++ b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs @@ -1,24 +1,35 @@ using System; +using System.Runtime.CompilerServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace MiMap.Viewer.DesktopGL.Graphics { - public class CachedRegionMesh : IDisposable + public interface ITile + { + int X { get; } + int Z { get; } + Point Position { get; } + Texture2D Texture { get; } + Matrix World { get; } + } + public class CachedRegionMesh : ITile, IDisposable { // FaceGroupOptimizer // FaceGroupUtil public int X { get; } public int Z { get; } - public Texture2D Texture { get; private set; } + public Point Position { get; } + public Texture2D Texture { get; } - public Matrix World { get; private set; } + public Matrix World { get; } public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) { X = region.X; Z = region.Z; + Position = new Point(X << 9, Z << 9); var t = new Texture2D(graphics, 1 << 9, 1 << 9); var d = new Color[(1 << 9) * (1 << 9)]; @@ -39,7 +50,6 @@ public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) y = chunk.GetHeight(bx, bz); c = chunk.GetColor(bx, bz); -// d[(((((chunk.Z & 31) << 4)) + cz) * (1 << 9)) + (((chunk.X & 31) << 4) + cx)] = c; SetData(d, x, z, c); } } @@ -61,6 +71,7 @@ public void Dispose() Texture?.Dispose(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetData(Color[] data, int blockX, int blockZ, Color color) { data[(blockZ * 512) + blockX] = color; diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs b/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs index 1c4509f..52eacb0 100644 --- a/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs +++ b/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs @@ -9,13 +9,15 @@ namespace MiMap.Viewer.DesktopGL.Graphics public interface IRegionMeshManager : IDisposable { CachedRegionMesh CacheRegion(MapRegion region); + CachedChunkMesh CacheChunk(MapChunk chunk); } public class RegionMeshManager : IRegionMeshManager { private GraphicsDevice _graphics; - private IDictionary _cache = new Dictionary(); + private IDictionary _regionCache = new Dictionary(); + private IDictionary _chunkCache = new Dictionary(); public RegionMeshManager(GraphicsDevice graphics) { @@ -26,18 +28,36 @@ public CachedRegionMesh CacheRegion(MapRegion region) { var i = new Point(region.X, region.Z); CachedRegionMesh cached; - if (_cache.TryGetValue(i, out cached)) return cached; + if (_regionCache.TryGetValue(i, out cached)) return cached; cached = new CachedRegionMesh(_graphics, region); - _cache[i] = cached; + _regionCache[i] = cached; + return cached; + } + + public CachedChunkMesh CacheChunk(MapChunk chunk) + { + var i = new Point(chunk.X, chunk.Z); + CachedChunkMesh cached; + if (_chunkCache.TryGetValue(i, out cached)) return cached; + cached = new CachedChunkMesh(_graphics, chunk); + _chunkCache[i] = cached; return cached; } public void Dispose() { - var points = _cache.Keys.ToArray(); + var points = _regionCache.Keys.ToArray(); + foreach (var p in points) + { + if (_regionCache.Remove(p, out var c)) + { + c.Dispose(); + } + } + points = _chunkCache.Keys.ToArray(); foreach (var p in points) { - if (_cache.Remove(p, out var c)) + if (_chunkCache.Remove(p, out var c)) { c.Dispose(); } diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs b/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs new file mode 100644 index 0000000..aba9199 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs @@ -0,0 +1,113 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MiMap.Viewer.DesktopGL.Graphics +{ + public static class SpriteBatchExtensions + { + private static Texture2D WhiteTexture { get; set; } + + + public static void Init(GraphicsDevice gd) + { + // TODO + WhiteTexture = new Texture2D(MiMapViewer.Instance.GraphicsDevice, 1, 1); + WhiteTexture.SetData(new Color[] {Color.White}); + } + + public static void Dispose() + { + WhiteTexture?.Dispose(); + } + + + /// + /// Draw a line between the two supplied points. + /// + /// Starting point. + /// End point. + /// The draw color. + public static void DrawLine(this SpriteBatch sb, float thickness, Vector2 start, Vector2 end, Color color, Vector2 scale, float layerdepth) + { + float length = (end - start).Length(); + float rotation = (float)Math.Atan2(end.Y - start.Y, end.X - start.X); + sb.Draw(WhiteTexture, start, null, color, rotation, Vector2.Zero, new Vector2(scale.X * length, scale.Y * (thickness)), SpriteEffects.None, layerdepth); + } + + /// + /// Draw a rectangle. + /// + /// The rectangle to draw. + /// The draw color. + public static void DrawRectangle(this SpriteBatch sb, Rectangle rectangle, Color color) + { + sb.Draw(WhiteTexture, new Rectangle(rectangle.Left, rectangle.Top, rectangle.Width, 1), color); + sb.Draw(WhiteTexture, new Rectangle(rectangle.Left, rectangle.Bottom, rectangle.Width, 1), color); + sb.Draw(WhiteTexture, new Rectangle(rectangle.Left, rectangle.Top, 1, rectangle.Height), color); + sb.Draw(WhiteTexture, new Rectangle(rectangle.Right, rectangle.Top, 1, rectangle.Height + 1), color); + } + + /// + /// Fill a rectangle. + /// + /// The rectangle to fill. + /// The fill color. + public static void FillRectangle(this SpriteBatch sb, Rectangle rectangle, Color color) + { + sb.Draw(WhiteTexture, rectangle, color); + } + + public static void FillRectangle(this SpriteBatch sb, Rectangle rectangle, Color color, float layerDepth) + { + sb.Draw(WhiteTexture, rectangle, null, color, 0f, Vector2.Zero, SpriteEffects.None, layerDepth); + } + + public static string GetBytesReadable(long i) + { + // Get absolute value + long absoluteI = (i < 0 ? -i : i); + // Determine the suffix and readable value + string suffix; + double readable; + if (absoluteI >= 0x1000000000000000) // Exabyte + { + suffix = "EB"; + readable = (i >> 50); + } + else if (absoluteI >= 0x4000000000000) // Petabyte + { + suffix = "PB"; + readable = (i >> 40); + } + else if (absoluteI >= 0x10000000000) // Terabyte + { + suffix = "TB"; + readable = (i >> 30); + } + else if (absoluteI >= 0x40000000) // Gigabyte + { + suffix = "GB"; + readable = (i >> 20); + } + else if (absoluteI >= 0x100000) // Megabyte + { + suffix = "MB"; + readable = (i >> 10); + } + else if (absoluteI >= 0x400) // Kilobyte + { + suffix = "KB"; + readable = i; + } + else + { + return i.ToString("0 B"); // Byte + } + // Divide by 1024 to get fractional value + readable = (readable / 1024); + // Return formatted number with suffix + return readable.ToString("0.### ") + suffix; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs index 285772b..a2ee3e8 100644 --- a/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/MiMapViewer.cs @@ -63,6 +63,8 @@ protected override void Initialize() UpdateViewport(); Window.ClientSizeChanged += (s, o) => UpdateViewport(); + SpriteBatchExtensions.Init(GraphicsDevice); + ImGuiRenderer = new ImGuiRenderer(this); ImGuiRenderer.RebuildFontAtlas(); diff --git a/src/MiMap.Viewer.DesktopGL/Models/Map.cs b/src/MiMap.Viewer.DesktopGL/Models/Map.cs index a5eda16..81af1af 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/Map.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/Map.cs @@ -123,6 +123,14 @@ public MapRegion GetRegion(Point regionPosition) return null; } + public MapChunk GetChunk(Point chunkPosition) + { + var regionPosition = new Point(chunkPosition.X >> 5, chunkPosition.Y >> 5); + if (Regions.TryGetValue(regionPosition, out var region)) + return region[chunkPosition.X % 32,chunkPosition.Y % 32]; + return null; + } + public IEnumerable GetRegions() { var v = Regions.Values.ToArray(); @@ -131,7 +139,7 @@ public IEnumerable GetRegions() yield return region; } } - + public IEnumerable GetRegions(Rectangle blockBounds) { var regionMin = new Point((blockBounds.X >> 9) - 1, (blockBounds.Y >> 9) - 1); @@ -152,6 +160,57 @@ public IEnumerable GetRegions(Rectangle blockBounds) } } + public void EnqueueChunks(Rectangle blockBounds) + { + var regionMin = new Point(blockBounds.X >> 9, blockBounds.Y >> 9 ); + var regionMax = new Point((blockBounds.X + blockBounds.Width) >> 9, (blockBounds.Y + blockBounds.Height) >> 9); + + for (int rx = regionMin.X; rx <= regionMax.X; rx++) + for (int rz = regionMin.Y; rz <= regionMax.Y; rz++) + { + var p = new Point(rx, rz); + if (!Regions.ContainsKey(p)) + { + EnqueueRegion(p); + } + } + } + + public IEnumerable GetChunks(Rectangle blockBounds) + { + var regionMin = new Point((blockBounds.X >> 9), (blockBounds.Y >> 9) ); + var regionMax = new Point(((blockBounds.X + blockBounds.Width) >> 9), ((blockBounds.Y + blockBounds.Height) >> 9)); + + var chunkMin = new Point((blockBounds.X >> 4), (blockBounds.Y >> 4)); + var chunkMax = new Point(((blockBounds.X + blockBounds.Width) >> 4), ((blockBounds.Y + blockBounds.Height) >> 4)); + + for (int rx = regionMin.X; rx <= regionMax.X; rx++) + for (int rz = regionMin.Y; rz <= regionMax.Y; rz++) + { + var p = new Point(rx, rz); + if (Regions.TryGetValue(p, out var region)) + { + for (int cx = 0; cx < 32; cx++) + for (int cz = 0; cz < 32; cz++) + { + var x = (rx << 5) + cx; + var z = (rz << 5) + cz; + if (x >= chunkMin.X && x <= chunkMax.X && + z >= chunkMin.Y && z <= chunkMax.Y) + { + var chunk = region[cx, cz]; + if (chunk != default) + yield return chunk; + } + } + } + else + { + EnqueueRegion(p); + } + } + } + public void Dispose() { _running = false; diff --git a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs index 87dcc32..e591910 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/MapChunk.cs @@ -24,29 +24,29 @@ public MapChunk(int x, int z) public int GetHeight(int x, int z) { - return Heights[GetIndex(x & 15, z & 15)]; + return Heights[GetIndex(x, z)]; } public byte GetBiome(int x, int z) { - return Biomes[GetIndex(x & 15, z & 15)]; + return Biomes[GetIndex(x, z)]; } public Color GetColor(int x, int z) { - return Colors[GetIndex(x & 15, z & 15)]; + return Colors[GetIndex(x, z)]; } public void SetHeight(int x, int z, int height) { - var i = GetIndex(x & 15, z & 15); + var i = GetIndex(x, z); Heights[i] = height; UpdateColor(i); } public void SetBiome(int x, int z, byte biome) { - var i = GetIndex(x & 15, z & 15); + var i = GetIndex(x, z); Biomes[i] = biome; UpdateColor(i); } @@ -69,7 +69,7 @@ private void UpdateColor(int i) private int GetIndex(int x, int z) { - return (x * 16) + z; + return ((x & 0x0F) * 16) + (z & 0x0F); } } } \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs b/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs index e7e6eff..94baac4 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/MapRegion.cs @@ -17,18 +17,18 @@ public MapRegion(int x, int z) public void SetChunk(int cx, int cz, MapChunk chunk) { - Chunks[GetIndex(cx & 31, cz & 31)] = chunk; + Chunks[GetIndex(cx, cz)] = chunk; } public MapChunk this[int cx, int cz] { - get => Chunks[GetIndex(cx & 31, cz & 31)]; - set => Chunks[GetIndex(cx & 31, cz & 31)] = value; + get => Chunks[GetIndex(cx, cz)]; + set => Chunks[GetIndex(cx, cz)] = value; } private int GetIndex(int x, int z) { - return (x * 32) + z; + return ((x & 0x1F) * 32) + (z & 0x1F); } } } \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/NLog.config b/src/MiMap.Viewer.DesktopGL/NLog.config index 7e95444..cbe4ef3 100644 --- a/src/MiMap.Viewer.DesktopGL/NLog.config +++ b/src/MiMap.Viewer.DesktopGL/NLog.config @@ -1,36 +1,39 @@  - - - - - - + xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd" + autoReload="true" + throwExceptions="false" + internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log"> + - - - - - - + + + + + + + + + + layout="${longdate} ${pad:padding=5:inner=${level:uppercase=true}} | ${logger}| ${callsite:className=true:includeSourcePath=false:methodName=true:includeNamespace=true}| ${message:withexception=true}" + fileName="${basedir}/logs/info-${cached:cached=true:inner=${date:format=yyyy-MM-dd HH-mm-ss}}.log" keepFileOpen="true"/> + layout="${longdate} ${pad:padding=5:inner=${level:uppercase=true}} | ${logger}| ${callsite:className=true:includeSourcePath=false:methodName=true:includeNamespace=true}| ${message:withexception=true} ${stacktrace}" + fileName="${basedir}/logs/error-${cached:cached=true:inner=${date:format=yyyy-MM-dd HH-mm-ss}}.log" keepFileOpen="true"/> - + - - + + - \ No newline at end of file + + diff --git a/src/MiMap.Viewer.DesktopGL/Program.cs b/src/MiMap.Viewer.DesktopGL/Program.cs index f10fe98..f574251 100644 --- a/src/MiMap.Viewer.DesktopGL/Program.cs +++ b/src/MiMap.Viewer.DesktopGL/Program.cs @@ -2,21 +2,22 @@ using System.IO; using System.Reflection; using Microsoft.Xna.Framework; +using MiMap.Viewer.DesktopGL.Graphics; using NLog; namespace MiMap.Viewer.DesktopGL { public static class Program { + private static ILogger Log; [STAThread] static void Main(string[] args) { ConfigureNLog(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))); - var logger = LogManager.GetCurrentClassLogger(); try { - logger.Info($"Starting {Assembly.GetExecutingAssembly().GetName().Name}"); + Log.Info($"Starting {Assembly.GetExecutingAssembly().GetName().Name}"); using (var game = new MiMapViewer()) { @@ -26,11 +27,12 @@ static void Main(string[] args) catch (Exception ex) { // NLog: catch any exception and log it. - logger.Error(ex, "Stopped program because of exception"); + Log.Error(ex, "Stopped program because of exception"); throw; } finally { + SpriteBatchExtensions.Dispose(); // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) LogManager.Shutdown(); } @@ -50,6 +52,11 @@ private static void ConfigureNLog(string baseDir) LogManager.LoadConfiguration(loggerConfigFile); // LogManager.Configuration = new XmlLoggingConfiguration(loggerConfigFile); LogManager.Configuration.Variables["basedir"] = baseDir; + + Log = LogManager.GetCurrentClassLogger(); + + AppDomain.CurrentDomain.FirstChanceException += (sender, args) => Log.Error(args.Exception, "FirstChanceException"); + AppDomain.CurrentDomain.UnhandledException += (sender, args) => Log.Error(args.ExceptionObject as Exception, "Unhandled exception"); } } } \ No newline at end of file From 3dfbba5edd6b2550d2e18eaf1c95ebca1d9ce8b8 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 14:46:33 +0200 Subject: [PATCH 06/13] fix cursor position when scale isn't 1 --- src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index 9842d3c..7444be3 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -679,7 +679,7 @@ private void UpdateMouseInput() private Point Unproject(Point cursor) { - return _mapPosition + (cursor.ToVector2() * _scale).ToPoint(); + return _mapPosition + (cursor.ToVector2() / _scale).ToPoint(); // return Vector2.Transform(cursor, InverseTransform); // return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f)); From a0f7efcfe2ffef1771241646def8e21b8748eeba Mon Sep 17 00:00:00 2001 From: Kenny van Vulpen Date: Mon, 30 May 2022 14:46:59 +0200 Subject: [PATCH 07/13] Commit --- .../DebugWorldProvider.cs | 14 +- .../Generators/Biomes/BiomeBase.cs | 66 +--- .../Generators/Biomes/BiomeRegistry.cs | 76 ++-- .../Generators/Biomes/Config/BiomeConfig.cs | 2 + .../Biomes/Vanilla/Desert/DesertHillsBiome.cs | 8 +- .../Biomes/Vanilla/Forest/BirchForestBiome.cs | 2 +- .../Vanilla/Forest/BirchForestHillsMBiome.cs | 4 + ...orestHillsBiome.cs => WoodedHillsBiome.cs} | 8 +- .../Biomes/Vanilla/Jungle/JungleBiome.cs | 7 +- .../Biomes/Vanilla/Jungle/JungleEdgeBiome.cs | 2 + .../Biomes/Vanilla/Jungle/JungleHillsBiome.cs | 9 +- .../Vanilla/Mushroom/MushroomIslandBiome.cs | 12 +- .../Mushroom/MushroomIslandShoreBiome.cs | 10 + .../Biomes/Vanilla/Plains/IcePlainsBiome.cs | 4 +- .../Biomes/Vanilla/Taiga/TaigaHillsBiome.cs | 6 +- .../Generators/Decorators/FoliageDecorator.cs | 24 +- .../Generators/Effects/GroundEffect.cs | 9 +- .../Generators/Effects/HeightEffect.cs | 31 +- .../Generators/Effects/JitterEffect.cs | 14 +- .../Generators/Effects/RaiseEffect.cs | 2 - .../Effects/SpikeEverywhereEffect.cs | 20 +- .../Generators/Effects/SummedHeightEffect.cs | 18 + .../Generators/Effects/VoronoiBorderEffect.cs | 12 +- .../Effects/VoronoiPlateauEffect.cs | 6 +- .../Generators/OverworldGeneratorV2.cs | 362 +++++------------- .../Generators/Preset.cs | 8 +- .../Generators/Structures/AcaciaTree.cs | 14 +- .../Generators/Structures/BirchTree.cs | 16 +- .../Generators/Structures/CactusStructure.cs | 29 +- .../Generators/Structures/LargeJungleTree.cs | 47 +-- .../Generators/Structures/OakTree.cs | 13 +- .../Generators/Structures/PineTree.cs | 28 +- .../Generators/Structures/SmallJungleTree.cs | 36 +- .../Generators/Structures/SpruceTree.cs | 21 - .../Generators/Structures/StructureClass.cs | 112 +++++- .../Generators/Structures/TreeStructure.cs | 58 +-- .../Surfaces/Mushroom/MushroomSurface.cs | 117 ++++++ .../Surfaces/Plains/IcePlainsSurface.cs | 74 ++++ .../Generators/Surfaces/SurfaceBase.cs | 11 +- .../Terrain/BirchForestHillsMTerrain.cs | 2 +- .../Terrain/BirchForestHillsTerrain.cs | 2 +- .../Generators/Terrain/BirchForestMTerrain.cs | 11 +- .../Generators/Terrain/BirchForestTerrain.cs | 8 +- .../Generators/Terrain/DesertHillsTerrain.cs | 14 +- .../Generators/Terrain/ForestHillsTerrains.cs | 2 +- .../Generators/Terrain/ForestTerrain.cs | 8 - .../Generators/Terrain/IcePlainsTerrain.cs | 14 + .../Generators/Terrain/JungleEdgeTerrain.cs | 7 +- .../Generators/Terrain/JungleHillsTerrain.cs | 2 +- .../Generators/Terrain/JungleTerrain.cs | 6 +- .../Generators/Terrain/MesaPlateauTerrain.cs | 2 +- .../Generators/Terrain/MesaTerrain.cs | 2 - .../Terrain/MushroomIslandShoreTerrain.cs | 14 + .../Terrain/MushroomIslandTerrain.cs | 10 + .../Generators/Terrain/PlainsTerrain.cs | 15 +- .../Generators/Terrain/SavannaTerrain.cs | 7 +- .../Generators/Terrain/TaigaHillsTerrain.cs | 11 +- .../Generators/Terrain/TerrainBase.cs | 28 +- .../Utils/Noise/Api/ISimplexData2D.cs | 2 +- .../Utils/Noise/Modules/FilterNoiseModule.cs | 2 +- .../Utils/Noise/Modules/VoronoiNoseModule.cs | 42 +- .../Utils/Noise/Primitives/SimplexNoise.cs | 3 +- .../Utils/Noise/SimplexData2D.cs | 115 +----- src/WorldGenerator.Tweaking/Program.cs | 156 +++++--- src/WorldGenerator.Tweaking/TestGame.cs | 56 ++- 65 files changed, 956 insertions(+), 897 deletions(-) rename src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/{ForestHillsBiome.cs => WoodedHillsBiome.cs} (75%) create mode 100644 src/OpenAPI.WorldGenerator/Generators/Effects/SummedHeightEffect.cs delete mode 100644 src/OpenAPI.WorldGenerator/Generators/Structures/SpruceTree.cs create mode 100644 src/OpenAPI.WorldGenerator/Generators/Surfaces/Mushroom/MushroomSurface.cs create mode 100644 src/OpenAPI.WorldGenerator/Generators/Surfaces/Plains/IcePlainsSurface.cs create mode 100644 src/OpenAPI.WorldGenerator/Generators/Terrain/IcePlainsTerrain.cs create mode 100644 src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandShoreTerrain.cs create mode 100644 src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandTerrain.cs diff --git a/src/OpenAPI.WorldGenerator/DebugWorldProvider.cs b/src/OpenAPI.WorldGenerator/DebugWorldProvider.cs index ba0a338..486d13f 100644 --- a/src/OpenAPI.WorldGenerator/DebugWorldProvider.cs +++ b/src/OpenAPI.WorldGenerator/DebugWorldProvider.cs @@ -12,14 +12,14 @@ namespace OpenAPI.WorldGenerator { public class DebugWorldProvider : IWorldProvider, ICachingWorldProvider { - private readonly ConcurrentDictionary _chunkCache = new ConcurrentDictionary(); - public bool IsCaching { get; private set; } + private readonly ConcurrentDictionary _chunkCache = new ConcurrentDictionary(); + public bool IsCaching => true; public IWorldGenerator Generator { get; } public DebugWorldProvider(IWorldGenerator worldGenerator) { Generator = worldGenerator; - IsCaching = true; + // IsCaching = true; } public void Initialize() @@ -61,7 +61,7 @@ public long GetDayTime() public string GetName() { - return "Cool world"; + return "OpenMiNET.WorldGenerator World Provider"; } public int SaveChunks() @@ -93,7 +93,7 @@ public int UnloadChunks(MiNET.Player[] players, ChunkCoordinates spawn, double m { int removed = 0; - lock (_chunkCache) + // lock (_chunkCache) { List coords = new List {spawn}; @@ -109,13 +109,13 @@ public int UnloadChunks(MiNET.Player[] players, ChunkCoordinates spawn, double m if (!keep) { _chunkCache.TryRemove(chunkColumn.Key, out ChunkColumn waste); - if (waste != null) { foreach (var chunk in waste) { - chunk.PutPool(); + chunk.Dispose(); } + waste.Dispose(); } Interlocked.Increment(ref removed); diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeBase.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeBase.cs index 4460c7f..3a7c3e1 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeBase.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeBase.cs @@ -59,65 +59,32 @@ public float TerrainNoise(OverworldGeneratorV2 generator, int x, int y, float bo } river = 1f - (1f - borderForRiver) * (1f - river); - return Terrain.GenerateNoise(generator, x, y, border, river); + return Terrain.GenerateNoise(generator, x, y, border, 1f); } float lakePressure = LakePressure(generator, x, y, border, generator.LakeFrequency, OverworldGeneratorV2.LakeBendSizeLarge, OverworldGeneratorV2.LakeBendSizeMedium, OverworldGeneratorV2.LakeBendSizeSmall); float lakeFlattening = LakeFlattening(lakePressure, generator.LakeShoreLevel, generator.LakeDepressionLevel); - /*if (!this.Config.AllowScenicLakes) - { - return Terrain.GenerateNoise(generator, x, y, border, river); - }*/ - // combine rivers and lakes - if ((river < 1) && (lakeFlattening < 1)) - { + if ((river < 1) && (lakeFlattening < 1)) { river = (1f - river) / river + (1f - lakeFlattening) / lakeFlattening; river = (1f / (river + 1f)); } - else if (lakeFlattening < river) - { + else if (lakeFlattening < river) { river = lakeFlattening; } // smooth the edges on the top river = 1f - river; - river *= (river / (river + 0.05f) * (1.05f)); + river = river * (river / (river + 0.05f) * (1.05f)); river = 1f - river; // make the water areas flat for water features float riverFlattening = river * (1f + OverworldGeneratorV2.RiverFlatteningAddend) - OverworldGeneratorV2.RiverFlatteningAddend; - if (riverFlattening < 0) - { + if (riverFlattening < 0) { riverFlattening = 0; } - - /*float riverFlattening = river*1.25f-0.25f; - if (riverFlattening <0) riverFlattening = 0; - if ((river<1)&&(lakeFlattening<1)) - { - riverFlattening = (1f-riverFlattening)/riverFlattening+(1f-lakeFlattening)/lakeFlattening; - riverFlattening = (1f/(riverFlattening+1f)); - } - else if (lakeFlattening < riverFlattening) - { - riverFlattening = lakeFlattening; - } - - // the lakes have to have a little less flattening to avoid the rocky edges - lakeFlattening = LakeFlattening(lakePressure, generator.LakeWaterLevel, generator.LakeDepressionLevel); - - if ((river<1)&&(lakeFlattening<1)) - { - river = (1f-river)/river+(1f-lakeFlattening)/lakeFlattening; - river = (1f/(river+1f)); - } - else if (lakeFlattening < river) - { - river = lakeFlattening; - }*/ // flatten terrain to set up for the water features float terrainNoise = Terrain.GenerateNoise(generator, x, y, border, riverFlattening); @@ -153,7 +120,7 @@ public float ErodedNoise(OverworldGeneratorV2 generator, int x, int y, float riv // river of actualRiverProportions now maps to 1; float riverFlattening = 1f - river; - riverFlattening = riverFlattening - (1f - OverworldGeneratorV2.ActualRiverProportion); + riverFlattening -= (1f - OverworldGeneratorV2.ActualRiverProportion); // return biomeHeight if no river effect if (riverFlattening < 0) { @@ -186,7 +153,7 @@ public float LakePressure(OverworldGeneratorV2 generator, int x, int y, float bo float pX = x; float pY = y; - ISimplexData2D jitterData = SimplexData2D.NewDisk(); + ISimplexData2D jitterData = new SimplexData2D(); generator.SimplexInstance(1).GetValue(x / 240.0d, y / 240.0d, jitterData); pX += jitterData.GetDeltaX() * largeBendSize; @@ -206,8 +173,6 @@ public float LakePressure(OverworldGeneratorV2 generator, int x, int y, float bo public float LakeFlattening(float pressure, float shoreLevel, float topLevel) { - if (!this.Config.AllowScenicLakes) - return 1f; // adjusts the lake pressure to the river numbers. The lake shoreLevel is mapped // to become equivalent to actualRiverProportion if (pressure > topLevel) @@ -227,21 +192,10 @@ public float LakeFlattening(float pressure, float shoreLevel, float topLevel) return 1; } - /*public void Replace(ChunkColumn primer, BlockCoordinates blockPos, int x, int y, int depth, OverworldGeneratorV2 generator, - float[] noise, float river, BiomeBase[] biomes) - { - Replace(primer, blockPos.X, blockPos.Z, x, y, depth, generator, noise, river, biomes); - }*/ - - public void Replace(ChunkColumn primer, int blockX, int blockZ, int x, int z, int depth, OverworldGeneratorV2 generator, + public virtual void Replace(ChunkColumn primer, int blockX, int blockZ, int x, int z, int depth, OverworldGeneratorV2 generator, float[] noise, float river, BiomeBase[] biomes) { - /* if (RTG.surfacesDisabled() || this.getConfig().DISABLE_RTG_SURFACES.get()) - { - return; - } -*/ - if (this.Surface == null) + if (this.Surface == null) return; float riverRegion = !this.Config.AllowRivers ? 0f : river; @@ -263,7 +217,7 @@ protected void ReplaceWithRiver(ChunkColumn primer, int i, int j, int x, int y, float riverRegion = !this.Config.AllowRivers ? 0f : river; this.Surface.PaintTerrain(primer, i, j, x, y, depth, generator, noise, riverRegion, biomes); - //this.surfaceRiver.paintTerrain(primer, i, j, x, y, depth, generator, noise, riverRegion, biomes); + /* if (RTGConfig.lushRiverbanksInDesert()) { this.surfaceRiver.paintTerrain(primer, i, j, x, y, depth, generator, noise, riverRegion, biomes); diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeRegistry.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeRegistry.cs index dad2c24..9bf8a88 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeRegistry.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/BiomeRegistry.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; @@ -53,7 +54,7 @@ public BiomeRegistry() new ExtremeHillsPlusMBiome(),*/ new FlowerForestBiome(), new ForestBiome(), - new ForestHillsBiome(), + new WoodedHillsBiome(), new FrozenOceanBiome(), // new IceMountainsBiome(), new IcePlainsBiome(), @@ -164,7 +165,7 @@ public BiomeRegistry() byId.TryAdd(biome.Id, biome); } - foreach (var group in Biomes.Where(x => !x.Config.IsEdgeBiome).GroupBy(x => x.Temperature * x.Downfall * x.Config.Weight)) + foreach (var group in Biomes.Where(x => !x.Config.IsEdgeBiome).GroupBy(x => (1f + x.Temperature) * (1f + x.Downfall) * x.Config.Weight)) { if (group.Count() > 1) { @@ -196,57 +197,54 @@ public BiomeBase[] GetBiomes() public int GetBiome(float temperature, float downfall, float selector) { var biomes = GetBiomes(); - // selector = Math.Clamp(selector, 0, 1f); + var weights = ArrayPool.Shared.Rent(biomes.Length); - - float[] weights = new float[biomes.Length]; - - float minDifference = float.MaxValue; - float sum = 0f; - - for (int i = 0; i < biomes.Length; i++) + try { - var temperatureDifference = MathF.Abs((biomes[i].Temperature - temperature)); - var humidityDifference = MathF.Abs(biomes[i].Downfall - downfall); + float sum = 0f; - // temperatureDifference *= 5.5f; - // humidityDifference *= 1.5f; + for (int i = 0; i < biomes.Length; i++) + { + var temperatureDifference = (biomes[i].Temperature - temperature); + var humidityDifference = (biomes[i].Downfall - downfall); - var difference = - (temperatureDifference * temperatureDifference + humidityDifference * humidityDifference) / 3f; + //temperatureDifference *= 7.5f; + //humidityDifference *= 2.5f; - var w = (float)biomes[i].Config.Weight; - /*if (biomes[i].MaxHeight < height || biomes[i].MinHeight > height) - { - var mdifference = MathF.Min( - MathF.Abs(biomes[i].MaxHeight - height), MathF.Abs(biomes[i].MinHeight - height)) * 4f; - - w -= mdifference; - }*/ - weights[i] = w * (1f + difference); + var weight = (float) biomes[i].Config.Weight * MathF.Abs( + (temperatureDifference * temperatureDifference + humidityDifference * humidityDifference)); - sum += weights[i]; - } + if (weight > 0f) + { + weights[i] = weight; - selector *= sum; + sum += weight; + } + } - int result = 0; + selector *= sum; - float currentWeightIndex = 0; - for (int i = 0; i < weights.Length; i++) - { - var value = weights[i]; + float currentWeightIndex = 0; - if (value > 0f) + for (int i = 0; i < biomes.Length; i++) { - currentWeightIndex += value; + var value = weights[i]; + + if (value > 0f) + { + currentWeightIndex += value; - if (currentWeightIndex >= selector) - return biomes[i].Id; + if (currentWeightIndex >= selector) + return biomes[i].Id; + } } - } - return biomes[result].Id; + return biomes[0].Id; + } + finally + { + ArrayPool.Shared.Return(weights, true); + } } public BiomeBase GetBiome(int id) diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Config/BiomeConfig.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Config/BiomeConfig.cs index 02620e1..fb26d32 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Config/BiomeConfig.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Config/BiomeConfig.cs @@ -11,6 +11,8 @@ public class BiomeConfig public bool IsEdgeBiome { get; set; } = false; public int Weight { get; set; } = Weights.Common; + + public float TreeDensity = 0.6f; public BiomeConfig() { diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Desert/DesertHillsBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Desert/DesertHillsBiome.cs index 456f8f3..c0c2449 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Desert/DesertHillsBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Desert/DesertHillsBiome.cs @@ -15,13 +15,13 @@ public DesertHillsBiome() Downfall = 0.0f; MinHeight = 0.2f; MaxHeight = 0.7f; - + + Config.AllowRivers = false; + Config.AllowScenicLakes = false; + Terrain = new DesertHillsTerrain(10f, 80f, 68f, 200f); Surface = new SurfaceBase(Config, new Sand(), new Sandstone()); - Config.AllowScenicLakes = false; - Config.AllowRivers = false; - Color = ColorUtils.FromHtml("#D25F12"); } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestBiome.cs index 1b4f52f..966296a 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestBiome.cs @@ -15,7 +15,7 @@ public BirchForestBiome() MinHeight = 0.1f; MaxHeight = 0.2f; Terrain = new BirchForestTerrain(); - Config.Weight = Weights.Common; + Config.Weight = Weights.Common - 1; Color = ColorUtils.FromHtml("#307444"); } diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs index 8382cc8..b5ea611 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/BirchForestHillsMBiome.cs @@ -14,6 +14,10 @@ public BirchForestHillsMBiome() Downfall = 0.6f; MinHeight = 0.35f; MaxHeight = 0.45f; + + Config.AllowRivers = false; + Config.AllowScenicLakes = false; + Terrain = new BirchForestHillsMTerrain(); Config.Weight = Weights.Rare; diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/ForestHillsBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/WoodedHillsBiome.cs similarity index 75% rename from src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/ForestHillsBiome.cs rename to src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/WoodedHillsBiome.cs index 97c1d54..172d12d 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/ForestHillsBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Forest/WoodedHillsBiome.cs @@ -4,9 +4,9 @@ namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Forest { - public class ForestHillsBiome : BiomeBase + public class WoodedHillsBiome : BiomeBase { - public ForestHillsBiome() + public WoodedHillsBiome() { Id = 18; Name = "Wooded Hills"; @@ -14,6 +14,10 @@ public ForestHillsBiome() Downfall = 0.8f; MinHeight = 0.2f; MaxHeight = 0.6f; + + Config.AllowRivers = false; + Config.AllowScenicLakes = false; + Terrain = new ForestHillsTerrain(); Config.Weight = Weights.Common; diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleBiome.cs index 7b3508d..b586e05 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleBiome.cs @@ -15,10 +15,13 @@ public JungleBiome() Downfall = 0.9f; MinHeight = 0.1f; MaxHeight = 0.4f; - + + Config.Weight = Weights.Common; + Config.TreeDensity = 0.4f; + Terrain = new JungleTerrain(); Surface = new JungleSurface(Config, new Grass(), new Dirt(), 0f, 1.5f, 60f, 65f, 1.5f, new Podzol(), 0.09f); - Config.Weight = Weights.Common; + Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#537B09"); // Config.WeightMultiplier = 1.f; diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleEdgeBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleEdgeBiome.cs index 1c3d057..f1e58ca 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleEdgeBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleEdgeBiome.cs @@ -13,6 +13,8 @@ public JungleEdgeBiome() MinHeight = 0.1f; MaxHeight = 0.2f; + Config.TreeDensity = 0.65f; + Terrain = new JungleEdgeTerrain(); Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#628B17"); } diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleHillsBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleHillsBiome.cs index 5924700..c126b1e 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleHillsBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Jungle/JungleHillsBiome.cs @@ -13,10 +13,15 @@ public JungleHillsBiome() Downfall = 0.9f; MinHeight = 0.2f; MaxHeight = 1.8f; - - Terrain = new JungleHillsTerrain(72f, 40f); + + Config.AllowRivers = false; + Config.AllowScenicLakes = false; + Config.Weight = Weights.Uncommon; + Config.Weight = Weights.Common; + Config.TreeDensity = 0.4f; + Terrain = new JungleHillsTerrain(72f, 40f); Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#2C4205"); } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandBiome.cs index ed6ae36..a266c20 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandBiome.cs @@ -1,3 +1,8 @@ +using MiNET.Blocks; +using OpenAPI.WorldGenerator.Generators.Biomes.Config; +using OpenAPI.WorldGenerator.Generators.Surfaces.Mushroom; +using OpenAPI.WorldGenerator.Generators.Terrain; + namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Mushroom { public class MushroomIslandBiome : BiomeBase @@ -10,8 +15,13 @@ public MushroomIslandBiome() Downfall = 1.0f; MinHeight = 0.2f; MaxHeight = 1f; - + Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#FF00FF"); + Config.AllowScenicLakes = false; + Config.Weight = Weights.SuperRare; + + Surface = new MushroomSurface(Config, new Mycelium(), new Stone(), 0f); + Terrain = new MushroomIslandTerrain(); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandShoreBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandShoreBiome.cs index 1cb6f9e..66c6160 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandShoreBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Mushroom/MushroomIslandShoreBiome.cs @@ -1,3 +1,8 @@ +using MiNET.Blocks; +using OpenAPI.WorldGenerator.Generators.Biomes.Config; +using OpenAPI.WorldGenerator.Generators.Surfaces.Mushroom; +using OpenAPI.WorldGenerator.Generators.Terrain; + namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Mushroom { public class MushroomIslandShoreBiome : BiomeBase @@ -12,6 +17,11 @@ public MushroomIslandShoreBiome() MaxHeight = 0.1f; Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#A000FF"); + Config.AllowScenicLakes = false; + Config.Weight = Weights.SuperRare; + + Surface = new MushroomSurface(Config, new Mycelium(), new Stone(), 0f); + Terrain = new MushroomIslandShoreTerrain(); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Plains/IcePlainsBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Plains/IcePlainsBiome.cs index 1a195a8..30be056 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Plains/IcePlainsBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Plains/IcePlainsBiome.cs @@ -1,3 +1,5 @@ +using MiNET.Blocks; +using OpenAPI.WorldGenerator.Generators.Surfaces.Plains; using OpenAPI.WorldGenerator.Generators.Terrain; namespace OpenAPI.WorldGenerator.Generators.Biomes.Vanilla.Plains @@ -14,7 +16,7 @@ public IcePlainsBiome() MaxHeight = 0.5f; Terrain = new IcePlainsTerrain(); - + Surface = new IcePlainsSurface(Config, new Snow(), new Dirt(), new Snow(), new Dirt()); Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#FFFFFF"); } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Taiga/TaigaHillsBiome.cs b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Taiga/TaigaHillsBiome.cs index 6a4e594..195a5bf 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Taiga/TaigaHillsBiome.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Biomes/Vanilla/Taiga/TaigaHillsBiome.cs @@ -15,10 +15,12 @@ public TaigaHillsBiome() MinHeight = 0.2f; MaxHeight = 0.7f; + Config.AllowRivers = false; + Config.AllowScenicLakes = false; + Terrain = new TaigaHillsTerrain(); Surface = new TaigaSurface(Config, new Grass(), new Dirt()); - - Config.AllowRivers = false; + Color = OpenAPI.WorldGenerator.Utils.ColorUtils.FromHtml("#163933"); } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Decorators/FoliageDecorator.cs b/src/OpenAPI.WorldGenerator/Generators/Decorators/FoliageDecorator.cs index a8debd0..1c6c173 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Decorators/FoliageDecorator.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Decorators/FoliageDecorator.cs @@ -5,6 +5,7 @@ using OpenAPI.WorldGenerator.Generators.Biomes; using OpenAPI.WorldGenerator.Generators.Structures; using OpenAPI.WorldGenerator.Utils.Noise; +using OpenAPI.WorldGenerator.Utils.Noise.Modules; using OpenAPI.WorldGenerator.Utils.Noise.Primitives; using Biome = OpenAPI.WorldGenerator.Generators.Biomes.Biome; using Structure = OpenAPI.WorldGenerator.Generators.Structures.Structure; @@ -53,25 +54,20 @@ public override void Decorate(ChunkColumn column, if (surface && y >= Preset.SeaLevel) { var m = Math.Min(biome.Downfall * 0.32f, 0.03f); - var noise = Simplex.GetValue(rx * m, rz * m); + var noise = MathF.Abs( Math.Clamp(Simplex.GetValue(rx * m, rz * m), 0f, 1f)); if (x >= 3 && x <= 13 && z >= 3 && z <= 13) { Structure tree = null; - //if (biome.Config.) - if (biome.Downfall <= 0f && biome.Temperature >= 2f) + if (biome.Downfall >= 0 && (noise > ((biome.Config.TreeDensity) + (y / 512f)))) { - if (GetRandom(64) == 16) + if (currentTemperature >= 2f && biome.Downfall <= 0f) { var randValue = GetRandom(3); tree = new CactusStructure(randValue); } - } - - if (tree == null && biome.Downfall > 0 && (noise > (0.5f + (y / 512f)))) - { - if (currentTemperature >= 1f && biome.Downfall >= 0.4f) + else if (currentTemperature >= 1f && biome.Downfall >= 0.4f) { if (GetRandom(8) == 4) { @@ -111,13 +107,14 @@ public override void Decorate(ChunkColumn column, { if (y + 1 < 254) { - if (tree.CanCreate(column, x, y, z)) + StructurePlan plan = new StructurePlan(); + tree.Create(plan, x, y + 1, z); + + if (plan.TryExecute(column)) { - tree.Create(column, x, y + 1, z); + generated = true; } } - - generated = true; } } @@ -243,6 +240,7 @@ private int GetRandom(int max) protected override void InitSeed(int seed) { Simplex = new SimplexPerlin(seed, NoiseQuality.Fast); + Simplex = new ScaledNoiseModule(Simplex) {ScaleX = 16f, ScaleY = 1f, ScaleZ = 16f}; // Simplex.SetScale(1.5); Rnd = new FastRandom(seed); diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/GroundEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/GroundEffect.cs index fa66bd0..97f7d1c 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/GroundEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/GroundEffect.cs @@ -7,19 +7,16 @@ namespace OpenAPI.WorldGenerator.Generators.Effects */ public class GroundEffect : HeightEffect { - - // the standard ground effect - private float amplitude; + private float Amplitude { get; } public GroundEffect(float amplitude) { - this.amplitude = amplitude; + this.Amplitude = amplitude; } public override float Added(OverworldGeneratorV2 generator, float x, float y) { - return TerrainBase.GetGroundNoise(generator, x, y, amplitude); + return TerrainBase.GetGroundNoise(generator, x, y, Amplitude); } - } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/HeightEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/HeightEffect.cs index a48fccc..10505e7 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/HeightEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/HeightEffect.cs @@ -1,35 +1,12 @@ namespace OpenAPI.WorldGenerator.Generators.Effects { - /** - * @author Zeno410 - */ - public abstract class HeightEffect { - - public abstract float Added(OverworldGeneratorV2 generator, float x, float y); - - public HeightEffect Plus(HeightEffect added) { - - return new SummedHeightEffect(this, added); - } - } - - public class SummedHeightEffect : HeightEffect + public abstract class HeightEffect { + public abstract float Added(OverworldGeneratorV2 generator, float x, float y); - private HeightEffect _one; - private HeightEffect _two; - - public SummedHeightEffect(HeightEffect one, HeightEffect two) - { - - this._one = one; - this._two = two; - } - - public override float Added(OverworldGeneratorV2 generator, float x, float y) + public HeightEffect Plus(HeightEffect added) { - return _one.Added(generator, x, y) + _two.Added(generator, x, y); + return new SummedHeightEffect(this, added); } } - } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/JitterEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/JitterEffect.cs index a40fda1..a3f16ef 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/JitterEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/JitterEffect.cs @@ -6,24 +6,24 @@ namespace OpenAPI.WorldGenerator.Generators.Effects { public class JitterEffect : HeightEffect { - public float Amplitude = int.MaxValue; - public float Wavelength = 0; - public HeightEffect Jittered; + public float Amplitude { get; set; } = int.MaxValue; + public float Wavelength { get; set; } = 0; + public HeightEffect Jittered { get; set; } public JitterEffect() { } - public JitterEffect(float amplitude, float wavelength, HeightEffect toJitter) { - + public JitterEffect(float amplitude, float wavelength, HeightEffect toJitter) + { this.Amplitude = amplitude; this.Wavelength = wavelength; this.Jittered = toJitter; } - + public override float Added(OverworldGeneratorV2 generator, float x, float y) { - ISimplexData2D jitterData = SimplexData2D.NewDisk(); + ISimplexData2D jitterData = new SimplexData2D(); generator.SimplexInstance(1).GetValue(x / Wavelength, y / Wavelength, jitterData); int pX = (int) Math.Round(x + jitterData.GetDeltaX() * Amplitude); int pY = (int) Math.Round(y + jitterData.GetDeltaY() * Amplitude); diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/RaiseEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/RaiseEffect.cs index cd17444..70552ad 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/RaiseEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/RaiseEffect.cs @@ -7,7 +7,6 @@ namespace OpenAPI.WorldGenerator.Generators.Effects */ public class RaiseEffect : HeightEffect { - // just adds a number public readonly float Height; @@ -18,7 +17,6 @@ public RaiseEffect(float height) public override float Added(OverworldGeneratorV2 generator, float x, float y) { - return Height; } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/SpikeEverywhereEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/SpikeEverywhereEffect.cs index 2e2508c..4209a40 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/SpikeEverywhereEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/SpikeEverywhereEffect.cs @@ -3,30 +3,26 @@ namespace OpenAPI.WorldGenerator.Generators.Effects { - /** - * This creates a spiky multiplier going from 0 to 1 - * - * @author Zeno410 - */ + /// + /// Creates a spiky multiplier going from 0 to 1 + /// public class SpikeEverywhereEffect : HeightEffect { - // not going to bother to set up a creator shell to make sure everything is set // set defaults to absurd values to crash if they're not set // a trio of parameters frequently used together - public float Wavelength = 0; + public float Wavelength { get; set; } = 0; - public float MinimumSimplex = int.MaxValue; // normal range is -1 to 1; + public float MinimumSimplex { get; set; } = int.MaxValue; // normal range is -1 to 1; //usually numbers above 0 are often preferred to avoid dead basins - public int Octave; - public float Power = 1.6f; // usually a range of 1 to 2 - public HeightEffect Spiked; + public int Octave { get; set; } + public float Power { get; set; } = 1.6f; // usually a range of 1 to 2 + public HeightEffect Spiked { get; set; } public override float Added(OverworldGeneratorV2 generator, float x, float y) { - float noise = generator.SimplexInstance(Octave).GetValue(x / Wavelength, y / Wavelength); noise = MathF.Abs(noise); noise = TerrainBase.BlendedHillHeight(noise, MinimumSimplex); diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/SummedHeightEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/SummedHeightEffect.cs new file mode 100644 index 0000000..c200bb9 --- /dev/null +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/SummedHeightEffect.cs @@ -0,0 +1,18 @@ +namespace OpenAPI.WorldGenerator.Generators.Effects; + +public class SummedHeightEffect : HeightEffect +{ + private HeightEffect _one; + private HeightEffect _two; + + public SummedHeightEffect(HeightEffect one, HeightEffect two) + { + this._one = one; + this._two = two; + } + + public override float Added(OverworldGeneratorV2 generator, float x, float y) + { + return _one.Added(generator, x, y) + _two.Added(generator, x, y); + } +} \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiBorderEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiBorderEffect.cs index 3a4ac79..bb10bbd 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiBorderEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiBorderEffect.cs @@ -4,14 +4,16 @@ namespace OpenAPI.WorldGenerator.Generators.Effects { public class VoronoiBorderEffect : HeightEffect { - public float PointWavelength = 0; - public float Floor = float.MaxValue; - public float MinimumDivisor = 0;//low divisors can produce excessive rates of change - - public override float Added(OverworldGeneratorV2 generator, float x, float y) { + public float PointWavelength { get; set; } = 0; + public float Floor { get; set; } = float.MaxValue; + public float MinimumDivisor { get; set; } = 0;//low divisors can produce excessive rates of change + + public override float Added(OverworldGeneratorV2 generator, float x, float y) + { VoronoiResult points = generator.CellularInstance(1).Eval2D(x / PointWavelength, y / PointWavelength); float raise = (float) (points.InteriorValue); raise = 1.0f - raise; + //raise = TerrainBase.blendedHillHeight(raise, floor); return raise; } diff --git a/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiPlateauEffect.cs b/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiPlateauEffect.cs index 1a4fab5..a8a5c98 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiPlateauEffect.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Effects/VoronoiPlateauEffect.cs @@ -6,9 +6,9 @@ namespace OpenAPI.WorldGenerator.Generators.Effects { public class VoronoiPlateauEffect : HeightEffect { - public float PointWavelength = 200; - public float MinimumDivisor = 0;//low divisors can produce excessive rates of change - public float AdjustmentRadius = 3f; + public float PointWavelength { get; set; } = 200; + public float MinimumDivisor { get; set; } = 0;//low divisors can produce excessive rates of change + public float AdjustmentRadius { get; set; } = 3f; public override float Added(OverworldGeneratorV2 generator, float x, float y) diff --git a/src/OpenAPI.WorldGenerator/Generators/OverworldGeneratorV2.cs b/src/OpenAPI.WorldGenerator/Generators/OverworldGeneratorV2.cs index 4bc347a..41cc01a 100644 --- a/src/OpenAPI.WorldGenerator/Generators/OverworldGeneratorV2.cs +++ b/src/OpenAPI.WorldGenerator/Generators/OverworldGeneratorV2.cs @@ -64,7 +64,7 @@ public OverworldGeneratorV2() // Preset = JsonConvert.DeserializeObject( // "{\"useCaves\":true,\"useStrongholds\":true,\"useVillages\":true,\"useMineShafts\":true,\"useTemples\":true,\"useRavines\":true,\"useMonuments\":true,\"useMansions\":true,\"useLavaOceans\":false,\"useWaterLakes\":true,\"useLavaLakes\":true,\"useDungeons\":true,\"fixedBiome\":-3,\"biomeSize\":4,\"seaLevel\":63,\"riverSize\":4,\"waterLakeChance\":4,\"lavaLakeChance\":80,\"dungeonChance\":8,\"dirtSize\":33,\"dirtCount\":10,\"dirtMinHeight\":0,\"dirtMaxHeight\":255,\"gravelSize\":33,\"gravelCount\":8,\"gravelMinHeight\":0,\"gravelMaxHeight\":255,\"graniteSize\":33,\"graniteCount\":10,\"graniteMinHeight\":0,\"graniteMaxHeight\":80,\"dioriteSize\":33,\"dioriteCount\":10,\"dioriteMinHeight\":0,\"dioriteMaxHeight\":80,\"andesiteSize\":33,\"andesiteCount\":10,\"andesiteMinHeight\":0,\"andesiteMaxHeight\":80,\"coalSize\":17,\"coalCount\":20,\"coalMinHeight\":0,\"coalMaxHeight\":128,\"ironSize\":9,\"ironCount\":20,\"ironMinHeight\":0,\"ironMaxHeight\":64,\"goldSize\":9,\"goldCount\":2,\"goldMinHeight\":0,\"goldMaxHeight\":32,\"redstoneSize\":8,\"redstoneCount\":8,\"redstoneMinHeight\":0,\"redstoneMaxHeight\":16,\"diamondSize\":8,\"diamondCount\":1,\"diamondMinHeight\":0,\"diamondMaxHeight\":16,\"lapisSize\":7,\"lapisCount\":1,\"lapisMinHeight\":0,\"lapisMaxHeight\":32,\"coordinateScale\":684,\"heightScale\":684,\"mainNoiseScaleX\":80,\"mainNoiseScaleY\":160,\"mainNoiseScaleZ\":80,\"depthNoiseScaleX\":200,\"depthNoiseScaleZ\":200,\"depthNoiseScaleExponent\":0.5,\"biomeDepthWeight\":1,\"biomeDepthOffset\":0,\"biomeScaleWeight\":1,\"biomeScaleOffset\":1,\"lowerLimitScale\":512,\"upperLimitScale\":512,\"baseSize\":8.5,\"stretchY\":12,\"lapisCenterHeight\":16,\"lapisSpread\":16}");; - int seed = 3566635; + int seed = 356556635; _seed = seed; InitBiomeProviders(seed); @@ -76,8 +76,6 @@ public OverworldGeneratorV2() this._cellularNoiseInstances[i] = new SpacedCellularNoise(seed * i); } - // BiomeList = new BiomeBase[256]; - for (int i = 0; i < _weightings.Length; i++) { _weightings[i] = new float[256]; @@ -109,15 +107,13 @@ public OverworldGeneratorV2() BeachBiome = new bool[256]; LandBiomes = new bool[256]; WaterBiomes= new bool[256]; + foreach (var biome in totalBiomes) { BeachBiome[biome.Id] = ((biome.Type & BiomeType.Beach) != 0); LandBiomes[biome.Id] = (biome.Type & BiomeType.Land) != 0; WaterBiomes[biome.Id] = (biome.Type & BiomeType.Ocean) != 0 || (biome.Type & BiomeType.River) != 0; } - // BeachBiome = totalBiomes.Select(x => (x.Type.HasFlag(BiomeType.Beach))).ToArray(); - // LandBiome = totalBiomes.Select(x => (x.Type.HasFlag(BiomeType.Land))).ToArray(); - // OceanBiome = totalBiomes.Select(x => (x.Type.HasFlag(BiomeType.Ocean))).ToArray(); } public float RiverSeperation => RiverSeparationBase / Preset.RiverFrequency; @@ -150,98 +146,49 @@ public SpacedCellularNoise CellularInstance(int index) { private ChunkDecorator[] Decorators { get; set; } private void InitBiomeProviders(int seed) { + var distortNoise = new MultiFractalNoiseModule(new SimplexPerlin(seed ^ 32, NoiseQuality.Fast)); + + BiomeRegistry = new BiomeRegistry(); var biomeScale = 16f * Preset.BiomeSize; - INoiseModule temperaturePrimitive = new SimplexPerlin(seed ^ 3, NoiseQuality.Fast); - INoiseModule temperatureNoise = temperaturePrimitive; - - temperatureNoise = new VoronoiNoseModule() + INoiseModule temperatureNoise = new VoronoiNoseModule(new SimplexPerlin(seed ^ 3, NoiseQuality.Fast)) { - Primitive = temperatureNoise, - Distance = false, - Frequency = 0.0525644f, - OctaveCount = 6, - // Size = 8, - - Displacement = 2f, - Gain = 1.3f, - Lacunarity = 1.2f, - SpectralExponent = 0.6f + Distance = false, Frequency = 0.0325644f, Displacement = 2f }; - temperatureNoise = new TurbulenceNoiseModule(temperatureNoise, - new SimplexPerlin(seed ^ 6, NoiseQuality.Fast), - new SimplexPerlin(seed ^ 12, NoiseQuality.Fast), - new SimplexPerlin(seed ^ 18, NoiseQuality.Fast), 2f); + temperatureNoise = new TurbulenceNoiseModule(temperatureNoise, distortNoise, distortNoise, distortNoise, 3.5f); - temperatureNoise = new ScaledNoiseModule(temperatureNoise) + TemperatureNoise = new ScaledNoiseModule(temperatureNoise) { ScaleX = 1f / biomeScale, ScaleY = 1f / biomeScale, ScaleZ = 1f / biomeScale }; - INoiseModule rainNoise = new SimplexPerlin(seed * seed ^ 2, NoiseQuality.Fast); - rainNoise = new VoronoiNoseModule() + RainfallNoise = new ScaledNoiseModule(new VoronoiNoseModule(new SimplexPerlin(seed * seed ^ 2, NoiseQuality.Fast)) { - Primitive = rainNoise, - Distance = true, - Frequency = 0.0122776f, - OctaveCount = 6, - - Displacement = 1f, - Gain = 1.3f, - Lacunarity = 1.1f, - SpectralExponent = 0.8f - }; - - rainNoise = new TurbulenceNoiseModule(rainNoise, - new SimplexPerlin((seed * seed) ^ 6, NoiseQuality.Fast), - new SimplexPerlin((seed * seed) ^ 12, NoiseQuality.Fast), - new SimplexPerlin((seed * seed) ^ 18, NoiseQuality.Fast), 2f); - - rainNoise = new ScaledNoiseModule(rainNoise) + Distance = false, + Frequency = 0.022776f, + Displacement = 1f + }) { ScaleX = 1f / biomeScale, ScaleY = 1f / biomeScale, ScaleZ = 1f / biomeScale }; - // rainNoise = new TurbulenceNoiseModule(rainNoise, distortionY, distortionX, distortionX, 16f); - - BiomeRegistry = new BiomeRegistry(); - - - INoiseModule selectorNoise = new SimplexPerlin(seed, NoiseQuality.Fast); - - - selectorNoise = new VoronoiNoseModule() + INoiseModule selectorNoise = new VoronoiNoseModule(new SimplexPerlin(seed * 69, NoiseQuality.Fast)) { - Primitive = selectorNoise, - Distance = false, - Frequency = 0.1245776f, - OctaveCount = 3, - - Displacement = 1f, - Gain = 2.3f, - Lacunarity = 1.3f, - SpectralExponent = 0.8f - };// new SimplexPerlin(seed * seed ^ 2, NoiseQuality.Fast);} - - selectorNoise = new TurbulenceNoiseModule(selectorNoise, - new SimplexPerlin(seed * seed ^ 32, NoiseQuality.Fast), - new SimplexPerlin(seed * seed ^ 64, NoiseQuality.Fast), - new SimplexPerlin(seed * seed ^ 128, NoiseQuality.Fast), 1.5f); + Distance = false, Frequency = 0.245776f, Displacement = 1f + }; - selectorNoise = new ScaledNoiseModule(selectorNoise) + selectorNoise = new TurbulenceNoiseModule(selectorNoise, distortNoise, distortNoise, distortNoise, 4f); + + SelectorNoise = new ScaledNoiseModule( + selectorNoise) { - ScaleX = 1f / 128f, - ScaleY = 1f / 128f, - ScaleZ = 1f / 128f + ScaleX = 1f / Preset.MainNoiseScaleX, ScaleY = 1f / Preset.MainNoiseScaleY, ScaleZ = 1f / Preset.MainNoiseScaleZ }; var d = new FoliageDecorator(Preset); d.SetSeed(seed); - - RainfallNoise = rainNoise; - TemperatureNoise = temperatureNoise; - SelectorNoise = selectorNoise; + Decorators = new ChunkDecorator[] {d}; } @@ -260,73 +207,17 @@ public ChunkColumn GenerateChunkColumn(ChunkCoordinates chunkCoordinates) ChunkColumn column = new ChunkColumn() {X = chunkCoordinates.X, Z = chunkCoordinates.Z}; ChunkLandscape landscape = new ChunkLandscape(); - var biomes = CalculateBiomes(chunkCoordinates, landscape); - + CalculateBiomes(chunkCoordinates, landscape); + GenerateTerrain(column, landscape.Noise); - FixBiomes(column, landscape, biomes); + BlendBiomes(column, landscape); - SetBiomeBlocks(column, landscape); + // SetBiomeBlocks(column, landscape); return column; } - private float RiverAdjusted(float top, float river) { - if (river >= 1) { - return top; - } - float erodedRiver = river / ActualRiverProportion; - if (erodedRiver <= 1f) { - top = top * (1 - erodedRiver) + (Preset.SeaLevel - 1f) * erodedRiver; - } - top = top * (1 - river) + (Preset.SeaLevel - 1f) * river; - return top; - } - - private void SetBiomeBlocks(ChunkColumn chunk, ChunkLandscape landscape) - { - int worldX = chunk.X * 16; - int worldZ = chunk.Z * 16; - - var coords = new BlockCoordinates(worldX, 0, worldZ); - - for (int x = 0; x < 16; x++) - { - coords.X = worldX + x; - for (int z = 0; z < 16; z++) - { - coords.Z = worldZ + z; - - var index = NoiseMap.GetIndex(x, z); - var biome = landscape.Biome[index]; - var river = landscape.River[index]; - int depth = -1; - - biome.Replace(chunk, coords.X, coords.Z, x, z, depth, this, landscape.Noise, river, landscape.Biome); - chunk.biomeId[index] = (byte) biome.Id; - } - } - - for (int x = 0; x < 16; x++) - { - for (int z = 0; z < 16; z++) - { - var index = NoiseMap.GetIndex(x, z); - - var biome = landscape.Biome[index]; - var river = landscape.River[index]; - biome.Decorate(chunk, landscape.Noise[index], river); - - foreach (var decorator in Decorators) - { - decorator.Decorate( - chunk, chunk.X, chunk.Z, BiomeRegistry.GetBiome(chunk.biomeId[index]), new float[0], x, - chunk.height[index], z, true, false); - } - } - } - } - #region Biome Selection private long _totalLookups = 0; @@ -342,19 +233,14 @@ private void SetBiomeBlocks(ChunkColumn chunk, ChunkLandscape landscape) public float MaxHeight = float.MinValue; public float MinHeight = float.MaxValue; - // private ThreadSafeList _uniqueBiomes = new ThreadSafeList(); - private int[] CalculateBiomes(ChunkCoordinates coordinates, ChunkLandscape landscape) + private void CalculateBiomes(ChunkCoordinates coordinates, ChunkLandscape landscape) { - // var biomes = BiomeProvider.GetBiomes().ToArray(); - int worldX = coordinates.X << 4; int worldZ = coordinates.Z << 4; float[] weightedBiomes = new float[256]; int[] biomes = new int[SampleArraySize * SampleArraySize]; - - var availableBiomes = BiomeRegistry.GetBiomes(); for (int x = -SampleSize; x < SampleSize + 5; x++) { for (int z = -SampleSize; z < SampleSize + 5; z++) @@ -369,49 +255,42 @@ private int[] CalculateBiomes(ChunkCoordinates coordinates, ChunkLandscape lands //We do this by getting the absolute value (0 to 1) and multiplying it by 2. // temp = MathF.Abs(temp) * 2f; + /* if (temp > 2f) { var remainder = temp - 2f; temp = 2f - remainder; - } + }*/ MaxTemp = MathF.Max(temp, MaxTemp); MinTemp = MathF.Min(temp, MinTemp); + /* if (rain > 1f) { var remainder = rain - 1f; rain = 1f - remainder; - } + }*/ MaxRain = MathF.Max(rain, MaxRain); MinRain = MathF.Min(rain, MinRain); _totalLookups++; - var selector =MathF.Abs(SelectorNoise.GetValue(xx, zz)); - if (selector > 1f) + var selector = MathF.Abs(SelectorNoise.GetValue(xx, zz)); + /*if (selector > 1f) { var remainder = selector - 1f; selector = 1f - remainder; - } + }*/ MaxSelector = MathF.Max(selector, MaxSelector); MinSelector = MathF.Min(selector, MinSelector); - /*var height = MathF.Abs(HeightNoise.GetValue(xx, zz)); - height -= 1f; - - MaxHeight = MathF.Max(height, MaxHeight); - MinHeight = MathF.Min(height, MinHeight); - - height = Math.Clamp(height, -1.8f, 1.8f);*/ biomes[(x + SampleSize) * SampleArraySize + (z + SampleSize)] = (BiomeRegistry.GetBiome( (float) temp, (float) rain, selector)); } } - // var availableBiomes = BiomeProvider.GetBiomes(); - for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { @@ -461,169 +340,116 @@ private int[] CalculateBiomes(ChunkCoordinates coordinates, ChunkLandscape lands } } } - - //landscape.Biome[index] = weightedBiomes.; - - // landscape.Biome[index] = BiomeProvider.GetBiome(i); - //landscape.Biome[index] = BiomeProvider.GetBiome(); } } - - return biomes; } public const int MAX_BIOMES = 256; - private void FixBiomes(ChunkColumn column, ChunkLandscape landscape, int[] neighboring) + private void BlendBiomes(ChunkColumn column, ChunkLandscape landscape) { - // return; - // landscape.Biome - - ISimplexData2D jitterData = SimplexData2D.NewDisk(); + /* + ISimplexData2D jitterData = new SimplexData2D(); BiomeBase[] jitteredBiomes = new BiomeBase[256]; - var riverStrength = landscape.River; - var noise = landscape.Noise; + BiomeBase jitterbiome, actualbiome; - for (int i = 0; i < 16; i++) + for (int x = 0; x < 16; x++) { - for (int j = 0; j < 16; j++) + for (int z = 0; z < 16; z++) { - int x = (column.X * 16) + i; - int z = (column.Z * 16) + j; - this.SimplexInstance(0).GetValue(x, z, jitterData); - int pX = (int) Math.Round(x + jitterData.GetDeltaX() * BlendRadius); - int pZ = (int) Math.Round(z + jitterData.GetDeltaY() * BlendRadius); - actualbiome = landscape.Biome[(x & 15) * 16 + (z & 15)]; + int realX = (column.X * 16) + x; + int realZ = (column.Z * 16) + z; + this.SimplexInstance(0).GetValue(realX, realZ, jitterData); + int pX = (int) Math.Round(realX + jitterData.GetDeltaX() * BlendRadius); + int pZ = (int) Math.Round(realZ + jitterData.GetDeltaY() * BlendRadius); + actualbiome = landscape.Biome[(realX & 15) * 16 + (realZ & 15)]; jitterbiome = landscape.Biome[(pX & 15) * 16 + (pZ & 15)]; - jitteredBiomes[i * 16 + j] = + jitteredBiomes[x * 16 + z] = (actualbiome.Config.SurfaceBlendIn && jitterbiome.Config.SurfaceBlendOut) ? jitterbiome : actualbiome; - - //riverStrength[i * 16 + j] = RiverAdjusted(Preset.SeaLevel, riverStrength[i * 16 + j]); } } + */ + SetBiomeBlocks(column, landscape, landscape.Biome); + } + + private void SetBiomeBlocks(ChunkColumn chunk, ChunkLandscape landscape, BiomeBase[] jitteredBiomes) + { + int worldX = chunk.X * 16; + int worldZ = chunk.Z * 16; - BiomeBase realisticBiome; - - // currently just stuffs the genLayer into the jitter; - for (int i = 0; i < MAX_BIOMES; i++) + for (int x = 0; x < 16; x++) { - realisticBiome = landscape.Biome[i]; - - var targetHeight = noise[i]; + var cx = worldX + x; + for (int z = 0; z < 16; z++) + { + var cz = worldZ + z; - bool canBeRiver = riverStrength[i] > 0.7 /* && targetHeight >= (Preset.SeaLevel * 0.5f) - && targetHeight <= Preset.SeaLevel + 1*/; + var index = NoiseMap.GetIndex(x, z); + var localRiver = landscape.River[index]; + int depth = -1; - if (targetHeight > Preset.SeaLevel) - { - // replace - jitteredBiomes[i] = realisticBiome; - } - else - { - // check for river - if (canBeRiver && (realisticBiome.Type & BiomeType.Ocean) == 0) - { - // make river - jitteredBiomes[i] = realisticBiome.GetRiverBiome(); - } - else if (targetHeight < Preset.SeaLevel) - { - jitteredBiomes[i] = realisticBiome; - } + jitteredBiomes[index].Replace(chunk, cx, cz, x, z, depth, this, landscape.Noise, localRiver, landscape.Biome); } } - - BiomeSearch waterSearch = new BiomeSearch(WaterBiomes); - BiomeSearch landSearch = new BiomeSearch(LandBiomes); - - waterSearch.SetNotHunted(); - waterSearch.SetAbsent(); - - landSearch.SetNotHunted(); - landSearch.SetAbsent(); - // beachSearch.Hunt(neighboring); - // landSearch.Hunt(neighboring); - - float beachTop = Preset.SeaLevel + 1.5f; // 64.5f; - float beachBottom = Preset.SeaLevel - 1.5f; - var b = jitteredBiomes.Select(x => x.Id).ToArray(); - for (int i = 0; i < MAX_BIOMES; i++) + + + for (int x = 0; x < 16; x++) { - if (noise[i] < beachBottom || noise[i] > beachTop) - { - continue; // this block isn't beach level - } - - if ((jitteredBiomes[i].Type & BiomeType.Swamp) != 0 || ((jitteredBiomes[i].Type & BiomeType.Ocean) == 0 && (jitteredBiomes[i].Type & BiomeType.River) == 0)) + var cx = worldX + x; + for (int z = 0; z < 16; z++) { - continue; - } - - - // if (waterSearch.IsNotHunted()) - // { - // waterSearch.Hunt(neighboring); - // } - - // int foundWaterBiome = waterSearch.Biomes[i]; + var cz = worldZ + z; + var index = NoiseMap.GetIndex(x, z); - // if (foundWaterBiome != -1) - { - if (landSearch.IsNotHunted()) + var biome = landscape.Biome[index]; + var river = landscape.River[index]; + + if (river > 0.9f && biome.Config.AllowRivers) { - landSearch.Hunt(neighboring); + chunk.biomeId[index] = (byte) biome.GetRiverBiome().Id; + biome.GetRiverBiome().Decorate(chunk, landscape.Noise[index], river); } - - // var nearestWaterBiome = BiomeProvider.GetBiome(foundWaterBiome); - int nearestLandBiome = landSearch.Biomes[i]; - - if (nearestLandBiome > -1) + else { - var foundBiome = BiomeRegistry.GetBiome(nearestLandBiome).GetBeachBiome(); + chunk.biomeId[index] = (byte) landscape.Biome[index].Id; + biome.Decorate(chunk, landscape.Noise[index], river); + } - if (foundBiome != null) - { - jitteredBiomes[i] = foundBiome; - } + foreach (var decorator in Decorators) + { + decorator.Decorate( + chunk, chunk.X, chunk.Z, biome, Array.Empty(), x, + chunk.height[index], z, true, false); } } } - - for (int i = 0; i < jitteredBiomes.Length; i++) - { - landscape.Biome[i] = jitteredBiomes[i]; - } } #endregion + private static int _stoneId = new Stone().GetRuntimeId(); + private static int _waterId = new Water().GetRuntimeId(); private void GenerateTerrain(ChunkColumn column, float[] noiseMap) { - int stoneId = new Stone().GetRuntimeId(); - var waterId = new Water().GetRuntimeId(); for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { var height = (int) noiseMap[NoiseMap.GetIndex(x, z)]; - for (int y = 0; (y <= height || y <= Preset.SeaLevel) && y < 255; y++) + for (int y = 0; (y <= Math.Max(height, Preset.SeaLevel)) && y < 255; y++) { - if (y > height) + if (y <= height) { - if (y <= Preset.SeaLevel) - { - column.SetBlockByRuntimeId(x, y, z, waterId); - } + column.SetBlockByRuntimeId(x, y, z, _stoneId); } - else + else if (y < Preset.SeaLevel) { - column.SetBlockByRuntimeId(x, y, z, stoneId); + column.SetBlockByRuntimeId(x, y, z, _waterId); } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Preset.cs b/src/OpenAPI.WorldGenerator/Generators/Preset.cs index 4ca32e6..85f1da4 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Preset.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Preset.cs @@ -215,10 +215,12 @@ public class WorldGeneratorPreset [JsonProperty("lapisSpread")] public float LapisSpread { get; set; } - - public float RiverFrequency { get; set; } = 1.0f; - public float RiverBendMult { get; set; } = 1.0f; + public float RiverSizeMult { get; set; } = 1.0f; + public float RiverFrequency { get; set; } = 0.5f; + public float RiverBendMult { get; set; } = 1.0f; + public float RiverCutOffAmpl { get; set; } = 0.5f; + public float RiverCutOffScale { get; set; } = 350.0f; public float RTGlakeSizeMult { get; set; } = 1f; // RTG public float RTGlakeFreqMult { get; set; } = 1f; // RTG diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/AcaciaTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/AcaciaTree.cs index 40b199e..2964041 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/AcaciaTree.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/AcaciaTree.cs @@ -13,11 +13,6 @@ public override string Name get { return "AcaciaTree"; } } - public override int MaxHeight - { - get { return 8; } - } - private static readonly int WoodId = new Wood() { WoodType = "acacia" @@ -29,15 +24,16 @@ public override int MaxHeight }.GetRuntimeId(); private readonly int _leafRadius = 2; - public override void Create(ChunkColumn column, int x, int y, int z) + public override void Create(StructurePlan plan, int x, int y, int z) { var location = new Vector3(x, y, z); - if (!ValidLocation(location, _leafRadius)) return; + CheckFloor(plan, x, y - 1, z); + // if (!ValidLocation(location, _leafRadius)) return; int height = Rnd.Next(5, 6); - GenerateColumn(column, location, height, WoodId); - GenerateVanillaLeaves(column, location + new Vector3(0, height, 0), _leafRadius, LeafId); + GenerateColumn(plan, location, height, WoodId); + GenerateVanillaLeaves(plan, location + new Vector3(0, height, 0), _leafRadius, LeafId); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/BirchTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/BirchTree.cs index 877eb9f..7394f4c 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/BirchTree.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/BirchTree.cs @@ -11,11 +11,6 @@ public override string Name get { return "BirchTree"; } } - public override int MaxHeight - { - get { return 7; } - } - private const int LeafRadius = 2; private static readonly int WoodId = new Wood() { @@ -27,18 +22,17 @@ public override int MaxHeight OldLeafType = "birch" }.GetRuntimeId(); - public override void Create(ChunkColumn column, int x, int y, int z) + public override void Create(StructurePlan plan, int x, int y, int z) { // var block = blocks[OverworldGenerator.GetIndex(x, y - 1, z)]; // if (block != 2 && block != 3) return; - + CheckFloor(plan, x, y - 1, z); var location = new Vector3(x, y, z); - if (!ValidLocation(location, LeafRadius)) return; - int height = Rnd.Next(4, MaxHeight); - GenerateColumn(column, location, height, WoodId); + int height = Rnd.Next(4, 7); + GenerateColumn(plan, location, height, WoodId); Vector3 leafLocation = location + new Vector3(0, height, 0); - GenerateVanillaLeaves(column, leafLocation, LeafRadius, LeafId); + GenerateVanillaLeaves(plan, leafLocation, LeafRadius, LeafId); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/CactusStructure.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/CactusStructure.cs index f936290..23ed05f 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/CactusStructure.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/CactusStructure.cs @@ -20,40 +20,21 @@ public CactusStructure(int height) } private static readonly int CactusBlockId = new Cactus().GetRuntimeId(); - public override void Create(ChunkColumn chunk, int x, int y, int z) + public override void Create(StructurePlan plan, int x, int y, int z) { + plan.RequireBlock(x,y - 1,z, new Sand()); //if (blocks[OverworldGenerator.GetIndex(x, y - 1, z)] != 12) return; //Not sand, do not generate. - var growth = Rnd.Next(0x1, 0x15); + //var growth = Rnd.Next(0x1, 0x15); for (int modifiedY = y; modifiedY < y + _height; modifiedY++) { - if (!CheckSafe(chunk, x, modifiedY, z)) break; + //if (!CheckSafe(plan, x, modifiedY, z)) break; //blocks[OverworldGenerator.GetIndex(x, modifiedY, z)] = 81; //metadata[OverworldGenerator.GetIndex(x, modifiedY, z)] = (byte) growth; - chunk.SetBlockByRuntimeId(x, modifiedY, z, CactusBlockId); //Cactus block + plan.PlaceBlock(x, modifiedY, z, CactusBlockId); //Cactus block //chunk.SetMetadata(x, modifiedY, z, (byte)growth); } } - - private bool CheckSafe(ChunkColumn chunk, int x, int y, int z) - { - if (!chunk.IsAir(x - 1, y, z)) return false; - if (!chunk.IsAir(x + 1, y, z)) return false; - if (!chunk.IsAir(x, y, z - 1)) return false; - if (!chunk.IsAir(x, y, z + 1)) return false; - - return true; - } - - private bool CheckSafe(Level level, int x, int y, int z) - { - if (level.IsAir(new BlockCoordinates(x - 1, y, z))) return false; - if (level.IsAir(new BlockCoordinates(x + 1, y, z))) return false; - if (level.IsAir(new BlockCoordinates(x, y, z - 1))) return false; - if (level.IsAir(new BlockCoordinates(x, y, z + 1))) return false; - - return true; - } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/LargeJungleTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/LargeJungleTree.cs index 81d29b9..cafa02f 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/LargeJungleTree.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/LargeJungleTree.cs @@ -13,10 +13,6 @@ public override string Name get { return "JungleTree"; } } - public override int MaxHeight - { - get { return 30; } - } private int BaseSize = 9; private int Roots = 3; @@ -35,51 +31,46 @@ public override int MaxHeight { OldLeafType = "jungle" }.GetRuntimeId(); - - public override void Create(ChunkColumn chunk, int x, int y, int z) - { - if (x < 6 || x > 10 || z < 6 || z > 10) return; - - //var block = blocks[OverworldGenerator.GetIndex(x, y - 1, z)]; - // if (block != 2 && block != 3) return; - - BaseSize = 9 + Rnd.Next(5); + public override void Create(StructurePlan plan, int x, int y, int z) + { + CheckFloor(plan, x, y - 1, z); + var baseSize = 9 + Rnd.Next(5); if (Roots > 0f) { for (int k = 0; k < 3; k++) { - GenerateBranch(chunk, x, y + Roots, z, (120 * k) - 40 + Rnd.Next(80), 1.6f + (float)Rnd.NextDouble() * 0.1f, Roots * 1.7f, 1f, - WoodId); + var horRootDir = (120 * k) - 40 + Rnd.Next(80); + var verRootDir = 1.6f + (float) Rnd.NextDouble() * 0.1f; + + GenerateBranch(plan, x, y + Roots, z, horRootDir, verRootDir, Roots * 1.7f, 1f, WoodId); } } - for (int i = y + Roots; i < y + BaseSize; i++) + for (int i = y + Roots; i < y + baseSize; i++) { - chunk.SetBlockByRuntimeId(x, i, z, WoodId); + plan.PlaceBlock(x, i, z, WoodId); } - + float horDir, verDir; int eX, eY, eZ; + for (int j = 0; j < Branches; j++) { horDir = (120 * j) - 60 + Rnd.Next(120); - verDir = VerticalStart + (float)Rnd.NextDouble() * VerticalRand; + verDir = VerticalStart + (float) Rnd.NextDouble() * VerticalRand; - eX = x + (int)(Math.Cos(horDir * Math.PI / 180D) * verDir * BranchLength); - eZ = z + (int)(Math.Sin(horDir * Math.PI / 180D) * verDir * BranchLength); - eY = y + BaseSize + (int)((1f - verDir) * BranchLength); + eX = x + (int) (Math.Cos(horDir * Math.PI / 180D) * verDir * BranchLength); + eZ = z + (int) (Math.Sin(horDir * Math.PI / 180D) * verDir * BranchLength); + eY = y + baseSize + (int) ((1f - verDir) * BranchLength); - if (CanGenerateBranch(x, y + BaseSize, z, horDir, verDir, BranchLength, 1f, 4f, 1.5f) && CanGenerateLeaves(eX, eY, eZ, 4f, 1.5f)) + //if (CanGenerateBranch(x, y + BaseSize, z, horDir, verDir, BranchLength, 1f/*, 4f, 1.5f*/) && CanGenerateLeaves(eX, eY, eZ, 4f, 1.5f)) { - GenerateBranch(chunk, x, y + BaseSize, z, horDir, verDir, BranchLength, 1f, - WoodId); + GenerateBranch(plan, x, y + baseSize, z, horDir, verDir, BranchLength, 1f, WoodId); for (int m = 0; m < 1; m++) { - GenerateLeaves(chunk, eX, eY, eZ, 4f, 1.5f, - LeavesId, - WoodId); + GenerateLeaves(plan, eX, eY, eZ, 4f, 1.5f, LeavesId, WoodId); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/OakTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/OakTree.cs index 199606d..95037b2 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/OakTree.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/OakTree.cs @@ -17,11 +17,6 @@ public override string Name get { return "OakTree"; } } - public override int MaxHeight - { - get { return 7; } - } - private static readonly int WoodId = new Wood() { WoodType = "oak" @@ -33,15 +28,15 @@ public override int MaxHeight }.GetRuntimeId(); private readonly int _leafRadius = 2; - public override void Create(ChunkColumn chunk, int x, int y, int z) + public override void Create(StructurePlan plan, int x, int y, int z) { + CheckFloor(plan, x, y - 1, z); var location = new Vector3(x, y, z); - if (!ValidLocation(location, _leafRadius)) return; int height = Rnd.Next(4, 5); - GenerateColumn(chunk, location, height, WoodId); + GenerateColumn(plan, location, height, WoodId); Vector3 leafLocation = location + new Vector3(0, height, 0); - GenerateVanillaLeaves(chunk, leafLocation, _leafRadius, LeafId); + GenerateVanillaLeaves(plan, leafLocation, _leafRadius, LeafId); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/PineTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/PineTree.cs index bfd7a01..9b381cc 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/PineTree.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/PineTree.cs @@ -15,11 +15,6 @@ public override string Name get { return "PineTree"; } } - public override int MaxHeight - { - get { return 10; } - } - private static readonly int WoodId = new Wood() { WoodType = "oak" @@ -30,43 +25,40 @@ public override int MaxHeight OldLeafType = "oak" }.GetRuntimeId(); - public override void Create(ChunkColumn column, int x, int y, int z) + public override void Create(StructurePlan plan, int x, int y, int z) { - //var block = blocks[OverworldGenerator.GetIndex(x, y - 1, z)]; - //if (block != 2 && block != 3) return; - + CheckFloor(plan, x, y - 1, z); var location = new Vector3(x, y, z); - if (!ValidLocation(location, _leafRadius)) return; var height = Rnd.Next(7, 8); - GenerateColumn(column, location, height, WoodId); + GenerateColumn(plan, location, height, WoodId); for (var Y = 1; Y < height; Y++) { if (Y % 2 == 0) { - GenerateVanillaCircle(column, location + new Vector3(0, Y + 1, 0), _leafRadius - 1, LeafId, 1); + GenerateVanillaCircle(plan, location + new Vector3(0, Y + 1, 0), _leafRadius - 1, LeafId, 1); continue; } - GenerateVanillaCircle(column, location + new Vector3(0, Y + 1, 0), _leafRadius, LeafId, 1); + GenerateVanillaCircle(plan, location + new Vector3(0, Y + 1, 0), _leafRadius, LeafId, 1); } - GenerateTopper(column, location + new Vector3(0, height, 0), 0x1); + GenerateTopper(plan, location + new Vector3(0, height, 0), 0x1); } - protected void GenerateTopper(ChunkColumn chunk, Vector3 location, byte type = 0x0) + protected void GenerateTopper(StructurePlan plan, Vector3 location, byte type = 0x0) { var sectionRadius = 1; - GenerateCircle(chunk, location, sectionRadius, LeafId); + GenerateCircle(plan, location, sectionRadius, LeafId); var top = location + new Vector3(0, 1, 0); var x = (int)location.X; var y = (int)location.Y + 1; var z = (int)location.Z; - chunk.SetBlockByRuntimeId(x, y, z, LeafId); + plan.PlaceBlock(x, y, z, LeafId); //chunk.SetMetadata(x, y, z, 1); if (type == 0x1 && y < 256) - GenerateVanillaCircle(chunk, new Vector3(x, y, z), sectionRadius, LeafId); + GenerateVanillaCircle(plan, new Vector3(x, y, z), sectionRadius, LeafId); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/SmallJungleTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/SmallJungleTree.cs index 6841a39..c17accc 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/SmallJungleTree.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/SmallJungleTree.cs @@ -13,11 +13,6 @@ public override string Name get { return "JungleTree"; } } - public override int MaxHeight - { - get { return 6; } - } - private int BaseSize = 5; private int Roots = 0; private float BranchLength = 5f; @@ -36,28 +31,26 @@ public override int MaxHeight OldLeafType = "jungle" }.GetRuntimeId(); - public override void Create(ChunkColumn chunk, int x, int y, int z) + public override void Create(StructurePlan plan, int x, int y, int z) { + CheckFloor(plan, x, y - 1, z); //var block = blocks[OverworldGenerator.GetIndex(x, y - 1, z)]; //if (block != 2 && block != 3) return; + BaseSize = 3 + Rnd.Next(2); - BaseSize = 3 + Rnd.Next(2); if (Roots > 0f) { for (int k = 0; k < 3; k++) { - GenerateBranch(chunk, x, y + Roots, z, (120 * k) - 40 + Rnd.Next(80), 1.6f + (float)Rnd.NextDouble() * 0.1f, Roots * 1.7f, 1f, WoodId); + GenerateBranch(plan, x, y + Roots, z, (120 * k) - 40 + Rnd.Next(80), 1.6f + (float)Rnd.NextDouble() * 0.1f, Roots * 1.7f, 1f, WoodId); } } for (int i = y + Roots; i < y + BaseSize; i++) { - //blocks[OverworldGenerator.GetIndex(x, i, z)] = 17; - //metadata[OverworldGenerator.GetIndex(x, i, z)] = 3; - chunk.SetBlockByRuntimeId(x, i, z, WoodId); - // chunk.SetMetadata(x, i, z, 3); + plan.PlaceBlock(x, i, z, WoodId); } - + float horDir, verDir; int eX, eY, eZ; for (int j = 0; j < Branches; j++) @@ -69,27 +62,16 @@ public override void Create(ChunkColumn chunk, int x, int y, int z) eZ = z + (int)(Math.Sin(horDir * Math.PI / 180D) * verDir * BranchLength); eY = y + BaseSize + (int)((1f - verDir) * BranchLength); - if (CanGenerateBranch(x, y + BaseSize, z, horDir, verDir, BranchLength, 1f, 4f, 1.5f) && CanGenerateLeaves(eX, eY, eZ, 4f, 1.5f)) + //if (CanGenerateBranch(x, y + BaseSize, z, horDir, verDir, BranchLength, 1f) && CanGenerateLeaves(eX, eY, eZ, 4f, 1.5f)) { - GenerateBranch(chunk, x, y + BaseSize, z, horDir, verDir, BranchLength, 1f, WoodId); + GenerateBranch(plan, x, y + BaseSize, z, horDir, verDir, BranchLength, 1f, WoodId); for (int m = 0; m < 1; m++) { - GenerateLeaves(chunk, eX, eY, eZ, 4f, 1.5f, LeavesId, WoodId); + GenerateLeaves(plan, eX, eY, eZ, 4f, 1.5f, LeavesId, WoodId); } } } - - /*var location = new Vector3(x, y, z); - if (!ValidLocation(location, 2)) return; - - int height = Math.Max(4, Rnd.Next(MaxHeight)); - - GenerateColumn(chunk, location, height, 17, 3); - Vector3 leafLocation = location + new Vector3(0, height, 0); - GenerateVanillaLeaves(chunk, leafLocation, 2, 18, 3); - */ - // base.Create(chunk, x, y, z); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/SpruceTree.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/SpruceTree.cs deleted file mode 100644 index cc80be9..0000000 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/SpruceTree.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MiNET.Blocks; -using MiNET.Utils; -using MiNET.Utils.Vectors; -using MiNET.Worlds; - -namespace OpenAPI.WorldGenerator.Generators.Structures -{ - public class SpruceTree : TreeStructure - { - public override string Name - { - get { return "SpruceTree"; } - } - - public override int MaxHeight - { - get { return 8; } - } - } -} - diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/StructureClass.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/StructureClass.cs index cb5c264..1dce8ce 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/StructureClass.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/StructureClass.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using MiNET.Blocks; using MiNET.Utils; using MiNET.Utils.Vectors; @@ -14,25 +16,115 @@ public virtual string Name get { return null; } } - public virtual int MaxHeight { get { return 0; } } + public virtual void Create(StructurePlan plan, int x, int y, int z) + { + + } + } - public virtual void Create(ChunkColumn column, int x, int y, int z) + public class StructurePlan + { + private LinkedList Actions { get; } + + public StructurePlan() { - + Actions = new LinkedList(); + } + + public void AddAction(StructureAction action) + { + Actions.AddLast(action); + } + + public void PlaceBlock(int x, int y, int z, int runtimeId) + { + AddAction(new PlaceBlockAction(x, y, z, runtimeId)); + } + + public void RequireBlock(int x, int y, int z, params Block[] validBlocks) + { + AddAction(new RequireBlockAction(x, y, z, validBlocks)); } - public virtual bool CanCreate(ChunkColumn column, int x, int y, int z) + public bool TryExecute(ChunkColumn column) { + foreach (var action in Actions) + { + if (!action.Validate(column)) + return false; + } + + foreach (var action in Actions) + { + action.Execute(column); + } + return true; } - protected Block GetBlockObject(int runtimeId) + public class RequireBlockAction : StructureAction + { + public int X { get; } + public int Y { get; } + public int Z { get; } + public Type[] RuntimeIds { get; } + + public RequireBlockAction(int x, int y, int z, params Block[] runtimeIds) + { + X = x; + Y = y; + Z = z; + RuntimeIds = runtimeIds.Select(x => x.GetType()).ToArray(); + } + + /// + public override bool Validate(ChunkColumn column) + { + var blockAtPosition = column.GetBlockObject(X, Y, Z); + var blockType = blockAtPosition.GetType(); + + return RuntimeIds.Any(x => x.IsAssignableFrom(blockType)); + } + + /// + public override void Execute(ChunkColumn column) + { + + } + } + + public class PlaceBlockAction : StructureAction + { + public int X { get; } + public int Y { get; } + public int Z { get; } + public int RuntimeId { get; } + + public PlaceBlockAction(int x, int y, int z, int runtimeId) + { + X = x; + Y = y; + Z = z; + RuntimeId = runtimeId; + } + + /// + public override bool Validate(ChunkColumn column) + { + return X >= 0 && X <= 15 && Z >= 0 && Z <= 15; + } + + /// + public override void Execute(ChunkColumn column) + { + column.SetBlockByRuntimeId(X, Y, Z, RuntimeId); + } + } + + public abstract class StructureAction { - BlockStateContainer blockStateContainer = BlockFactory.BlockPalette[runtimeId]; - Block blockById = BlockFactory.GetBlockById(blockStateContainer.Id); - blockById.SetState(blockStateContainer.States); - blockById.Metadata = (byte) blockStateContainer.Data; - return blockById; + public abstract bool Validate(ChunkColumn column); + public abstract void Execute(ChunkColumn column); } } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Structures/TreeStructure.cs b/src/OpenAPI.WorldGenerator/Generators/Structures/TreeStructure.cs index 30b6509..9ff3f18 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Structures/TreeStructure.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Structures/TreeStructure.cs @@ -12,22 +12,31 @@ namespace OpenAPI.WorldGenerator.Generators.Structures //And from https://github.com/SirCmpwn/TrueCraft public class TreeStructure : Structure { - protected void GenerateVanillaLeaves(ChunkColumn chunk, Vector3 location, int radius, int blockRuntimeId) + private static readonly Block[] ValidBlocks = new Block[] + { + new Grass(), new Dirt(), new Farmland() + }; + + protected void CheckFloor(StructurePlan plan, int x, int y, int z) + { + plan.RequireBlock(x,y,z, ValidBlocks); + } + protected void GenerateVanillaLeaves(StructurePlan plan, Vector3 location, int radius, int blockRuntimeId) { - if (location.X > 16 || location.X < 0 || location.Z > 16 || location.Z < 0) return; var radiusOffset = radius; for (var yOffset = -radius; yOffset <= radius; yOffset = (yOffset + 1)) { var y = location.Y + yOffset; if (y > 255) continue; - GenerateVanillaCircle(chunk, new Vector3(location.X, y, location.Z), radiusOffset, blockRuntimeId); + + GenerateVanillaCircle(plan, new Vector3(location.X, y, location.Z), radiusOffset, blockRuntimeId); if (yOffset != -radius && yOffset % 2 == 0) radiusOffset--; } } - protected void GenerateVanillaCircle(ChunkColumn chunk, Vector3 location, int radius, int blockRuntimeId, + protected void GenerateVanillaCircle(StructurePlan plan, Vector3 location, int radius, int blockRuntimeId, double corner = 0) { for (var I = -radius; I <= radius; I = (I + 1)) @@ -48,13 +57,13 @@ protected void GenerateVanillaCircle(ChunkColumn chunk, Vector3 location, int ra if (x < 0 || z > 16) continue; if (z < 0 || z > 16) continue; - chunk.SetBlockByRuntimeId((int)x, (int)location.Y, (int)z, blockRuntimeId); + plan.PlaceBlock((int)x, (int)location.Y, (int)z, blockRuntimeId); } } } } - public static void GenerateColumn(ChunkColumn chunk, Vector3 location, int height, int blockRuntimeId) + public static void GenerateColumn(StructurePlan plan, Vector3 location, int height, int blockRuntimeId) { for (var o = 0; o < height; o++) { @@ -62,11 +71,11 @@ public static void GenerateColumn(ChunkColumn chunk, Vector3 location, int heigh var y = (int)location.Y + o; var z = (int)location.Z; - chunk.SetBlockByRuntimeId(x, y, z, blockRuntimeId); + plan.PlaceBlock(x, y, z, blockRuntimeId); } } - protected void GenerateCircle(ChunkColumn chunk, Vector3 location, int radius, int blockRuntimeId) + protected void GenerateCircle(StructurePlan plan, Vector3 location, int radius, int blockRuntimeId) { for (var I = -radius; I <= radius; I = (I + 1)) { @@ -85,13 +94,13 @@ protected void GenerateCircle(ChunkColumn chunk, Vector3 location, int radius, i var y = (int)location.Y; var z = (int)Z; - chunk.SetBlockByRuntimeId(x, y, z, blockRuntimeId); + plan.PlaceBlock(x, y, z, blockRuntimeId); } } } } - protected bool CanGenerateBranch(float x, float y, float z, float horDir, float verDir, float branchLength, float speed, float size, float width) + /*protected bool CanGenerateBranch(float x, float y, float z, float horDir, float verDir, float branchLength, float speed/*, float size, float width#1#) { if (verDir < 0f) { @@ -120,7 +129,7 @@ protected bool CanGenerateBranch(float x, float y, float z, float horDir, float if (x < 0 || x >= 16 || z < 0 || z >= 16) return false; } - int i, j, k, s = (int)(size - 1f), w = (int)((size - 1f) * width); + /*int i, j, k, s = (int)(size - 1f), w = (int)((size - 1f) * width); for (i = -w; i <= w; i++) { for (j = -s; j <= s; j++) @@ -130,12 +139,12 @@ protected bool CanGenerateBranch(float x, float y, float z, float horDir, float if (x + i < 0 || x + i >= 16 || z + k < 0 || z + k >= 16) return false; } } - } + }#1# return true; - } + }*/ - protected void GenerateBranch(ChunkColumn chunk, float x, float y, float z, double horDir, float verDir, float length, float speed, int blockRuntimeId) + protected void GenerateBranch(StructurePlan plan, float x, float y, float z, double horDir, float verDir, float length, float speed, int blockRuntimeId) { if (verDir < 0f) { @@ -155,7 +164,8 @@ protected void GenerateBranch(ChunkColumn chunk, float x, float y, float z, doub while (c < length) { - chunk.SetBlockByRuntimeId((int)x, (int)y, (int)z, blockRuntimeId); + plan.PlaceBlock((int)x, (int)y, (int)z, blockRuntimeId); + //chunk.SetBlockByRuntimeId((int)x, (int)y, (int)z, blockRuntimeId); x += velX; y += velY; @@ -165,7 +175,7 @@ protected void GenerateBranch(ChunkColumn chunk, float x, float y, float z, doub } } - protected bool CanGenerateLeaves(int x, int y, int z, float size, float width) + /*protected bool CanGenerateLeaves(int x, int y, int z, float size, float width) { float dist; int i, j, k, s = (int)(size - 1f), w = (int)((size - 1f) * width); @@ -187,9 +197,9 @@ protected bool CanGenerateLeaves(int x, int y, int z, float size, float width) } } return true; - } + }*/ - protected void GenerateLeaves(ChunkColumn chunk, int x, int y, int z, float size, float width, int leafBlockRuntimeId, int woodRuntimeId) + protected void GenerateLeaves(StructurePlan plan, int x, int y, int z, float size, float width, int leafBlockRuntimeId, int woodRuntimeId) { float dist; int i, j, k, s = (int)(size - 1f), w = (int)((size - 1f) * width); @@ -204,27 +214,27 @@ protected void GenerateLeaves(ChunkColumn chunk, int x, int y, int z, float size { if (dist < 0.6f) { - chunk.SetBlockByRuntimeId(x + i, y + j, z + k, woodRuntimeId); + plan.PlaceBlock(x + i, y + j, z + k, woodRuntimeId); } - chunk.SetBlockByRuntimeId(x + i, y + j, z + k, leafBlockRuntimeId); + plan.PlaceBlock(x + i, y + j, z + k, leafBlockRuntimeId); } } } } } - public bool ValidLocation(Vector3 location, int leafRadius) + /*public bool ValidLocation(Vector3 location, int leafRadius) { return !(location.X - leafRadius < 0) && !(location.X + leafRadius >= 16) && !(location.Z - leafRadius < 0) && !(location.Z + leafRadius >= 16); - } + }*/ private static int[] ValidBlockRuntimeIds = new int[] { new Grass().GetRuntimeId(), new Dirt().GetRuntimeId(), new Farmland().GetRuntimeId() }; - /// + /*/// public override bool CanCreate(ChunkColumn column, int x, int y, int z) { var block = column.GetBlockObject(x, y, z); @@ -235,6 +245,6 @@ public override bool CanCreate(ChunkColumn column, int x, int y, int z) return false; return base.CanCreate(column, x, y, z); - } + }*/ } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Surfaces/Mushroom/MushroomSurface.cs b/src/OpenAPI.WorldGenerator/Generators/Surfaces/Mushroom/MushroomSurface.cs new file mode 100644 index 0000000..3bdccf6 --- /dev/null +++ b/src/OpenAPI.WorldGenerator/Generators/Surfaces/Mushroom/MushroomSurface.cs @@ -0,0 +1,117 @@ +using MiNET.Blocks; +using MiNET.Worlds; +using OpenAPI.WorldGenerator.Generators.Biomes; +using OpenAPI.WorldGenerator.Generators.Biomes.Config; +using OpenAPI.WorldGenerator.Generators.Terrain; + +namespace OpenAPI.WorldGenerator.Generators.Surfaces.Mushroom; + +public class MushroomSurface : SurfaceBase +{ + private float min; + + private float sCliff = 1.5f; + private float sHeight = 60f; + private float sStrength = 65f; + private float cCliff = 1.5f; + + /// + public MushroomSurface(BiomeConfig config, Block top, Block filler, float minCliff, float stoneCliff, float stoneHeight, float stoneStrength, float clayCliff) : this(config, top, filler, minCliff) + { + sCliff = stoneCliff; + sHeight = stoneHeight; + sStrength = stoneStrength; + cCliff = clayCliff; + } + + /// + public MushroomSurface(BiomeConfig config, Block top, Block filler, float minCliff) : base(config, top, filler) + { + min = minCliff; + } + + /// + public override void PaintTerrain(ChunkColumn column, + int blockX, + int blockZ, + int x, + int z, + int depth, + OverworldGeneratorV2 generator, + float[] noise, + float river, + BiomeBase[] biomes) + { + var rand = Rnd; + var simplex = generator.SimplexInstance(0); + float c = TerrainBase.CalcCliff(x, z, noise); + int cliff = 0; + + int b; + + for (int k = 255; k > -1; k--) + { + b = column.GetBlockId(x, k, z); + + if (b == AirId) + { + depth = -1; + } + else if (b == StoneId) + { + depth++; + + if (depth == 0) + { + float p = simplex.GetValue(x / 8f, z / 8f, k / 8f) * 0.5f; + + if (c > min && c > sCliff - ((k - sHeight) / sStrength) + p) + { + cliff = 1; + } + + if (c > cCliff) + { + cliff = 2; + } + + if (cliff == 1) + { + if (rand.Next(3) == 0) + { + column.SetBlockByRuntimeId(x, k, z, CliffCobbleBlock); + } + else + { + + column.SetBlockByRuntimeId(x, k, z, CliffStoneBlock); + } + } + else if (cliff == 2) + { + column.SetBlockByRuntimeId(x, k, z, ShadowStoneBlock); + } + else + { + column.SetBlockByRuntimeId(x, k, z, TopBlock); + } + } + else if (depth < 6) + { + if (cliff == 1) + { + column.SetBlockByRuntimeId(x, k, z, CliffStoneBlock); + } + else if (cliff == 2) + { + column.SetBlockByRuntimeId(x, k, z, ShadowStoneBlock); + } + else + { + column.SetBlockByRuntimeId(x, k, z, FillerBlock); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Surfaces/Plains/IcePlainsSurface.cs b/src/OpenAPI.WorldGenerator/Generators/Surfaces/Plains/IcePlainsSurface.cs new file mode 100644 index 0000000..df04e1b --- /dev/null +++ b/src/OpenAPI.WorldGenerator/Generators/Surfaces/Plains/IcePlainsSurface.cs @@ -0,0 +1,74 @@ +using MiNET.Blocks; +using MiNET.Worlds; +using OpenAPI.WorldGenerator.Generators.Biomes; +using OpenAPI.WorldGenerator.Generators.Biomes.Config; +using OpenAPI.WorldGenerator.Generators.Terrain; + +namespace OpenAPI.WorldGenerator.Generators.Surfaces.Plains; + +public class IcePlainsSurface : SurfaceBase +{ + private int CliffBlock1 { get; } + private int CliffBlock2 { get; } + /// + public IcePlainsSurface(BiomeConfig config, Block top, Block filler, Block cliff1, Block cliff2) : base(config, top, filler) + { + CliffBlock1 = cliff1.GetRuntimeId(); + CliffBlock2 = cliff2.GetRuntimeId(); + } + + /// + public override void PaintTerrain(ChunkColumn column, + int blockX, + int blockZ, + int x, + int z, + int depth, + OverworldGeneratorV2 generator, + float[] noise, + float river, + BiomeBase[] biomes) + { + float c = TerrainBase.CalcCliff(x, z, noise); + bool cliff = c > 1.4f; + + int b; + + for (int y = 255; y > -1; y--) + { + b = column.GetBlockId(x, y, z); + + if (b == AirId) + { + depth = -1; + } + else if (b == StoneId) + { + depth++; + + if (cliff) + { + if (depth > -1 && depth < 2) + { + column.SetBlockByRuntimeId(x, y, z, CliffBlock1); + } + else if (depth < 10) + { + column.SetBlockByRuntimeId(x, y, z, CliffBlock2); + } + } + else + { + if (depth == 0 && y > 61) + { + column.SetBlockByRuntimeId(x, y, z, TopBlock); + } + else if (depth < 4) + { + column.SetBlockByRuntimeId(x, y, z, FillerBlock); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Surfaces/SurfaceBase.cs b/src/OpenAPI.WorldGenerator/Generators/Surfaces/SurfaceBase.cs index 551481b..3161eb2 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Surfaces/SurfaceBase.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Surfaces/SurfaceBase.cs @@ -25,6 +25,7 @@ public class SurfaceBase protected static readonly int AirId = new Air().Id; protected static readonly int StoneId = new Stone().Id; protected static readonly int WaterId = new Water().Id; + protected static readonly int SandRuntimeId = new Sand().GetRuntimeId(); public SurfaceBase(BiomeConfig config, Block top, Block filler) { @@ -42,7 +43,7 @@ public SurfaceBase(BiomeConfig config, Block top, Block filler) public virtual void PaintTerrain(ChunkColumn column, int blockX, int blockZ, int x, int z, int depth, OverworldGeneratorV2 generator, float[] noise, float river, BiomeBase[] biomes) { - // float c = TerrainBase.CalcCliff(x, z, noise); + //float c = TerrainBase.CalcCliff(x, z, noise); // bool cliff = c > 1.4f; for (int y = 255; y > -1; y--) { @@ -53,8 +54,12 @@ public virtual void PaintTerrain(ChunkColumn column, int blockX, int blockZ, int } else if (b == StoneId) { depth++; - - if (depth == 0 && y > generator.Preset.SeaLevel - 2) { + + if (depth < 4 && river > 0.7) + { + column.SetBlockByRuntimeId(x,y, z, SandRuntimeId); + } + else if (depth == 0 && y > generator.Preset.SeaLevel - 2) { column.SetBlockByRuntimeId(x, y, z, TopBlock); } else if (depth < 4) { diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs index 7ce19ea..1e92afb 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsMTerrain.cs @@ -15,6 +15,6 @@ public BirchForestHillsMTerrain(float baseHeight, float hillStrength) : base(bas public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight /*- generator.Preset.SeaLevel*/); + return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight - generator.Preset.SeaLevel); } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsTerrain.cs index 115a360..41acf90 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestHillsTerrain.cs @@ -15,7 +15,7 @@ public BirchForestHillsTerrain(float baseHeight, float hillStrength) : base(base public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight /*- generator.Preset.SeaLevel*/); + return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight - generator.Preset.SeaLevel); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs index 0c37ebd..1976ba7 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestMTerrain.cs @@ -4,11 +4,14 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain; public class BirchForestMTerrain : TerrainBase { - private GroundEffect GroundEffect { get; set; }= new GroundEffect(4f); - + public BirchForestMTerrain() + { + HeightEffect = new GroundEffect(4f); + } + public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, 80f, 65f); - return TerrainForest(generator, x, y, river, generator.Preset.SeaLevel + 3 + GroundEffect.Added(generator, x, y)); + return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, 80f, generator.Preset.SeaLevel + 1); + //return TerrainForest(generator, x, y, river, generator.Preset.SeaLevel + 3 + GroundEffect.Added(generator, x, y)); } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestTerrain.cs index 772636e..14e40e4 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/BirchForestTerrain.cs @@ -4,11 +4,15 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class BirchForestTerrain : TerrainBase { - private GroundEffect GroundEffect { get; set; }= new GroundEffect(4f); + public BirchForestTerrain() + { + HeightEffect = new GroundEffect(4f); + } public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainForest(generator, x, y, river, generator.Preset.SeaLevel + 3 + GroundEffect.Added(generator, x, y)); + return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, 80f, 65f); + return TerrainForest(generator, x, y, river, generator.Preset.SeaLevel + 3 + HeightEffect.Added(generator, x, y)); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/DesertHillsTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/DesertHillsTerrain.cs index 836ef0f..f89c596 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/DesertHillsTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/DesertHillsTerrain.cs @@ -2,20 +2,20 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class DesertHillsTerrain : TerrainBase { - private float _start; - private float _height; - private float _width; + private float _hillStart; + private float _landHeight; + private float _hillWidth; public DesertHillsTerrain(float hillStart, float landHeight, float baseHeight, float hillWidth) : base(baseHeight) { - _start = hillStart; - _height = landHeight; - _width = hillWidth; + _hillStart = hillStart; + _landHeight = landHeight; + _hillWidth = hillWidth; } public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainHighland(generator, x, y, river, _start, _width, _height, BaseHeight /*- generator.Preset.SeaLevel*/); + return TerrainHighland(generator, x, y, river, _hillStart, _hillWidth, _landHeight, BaseHeight - generator.Preset.SeaLevel); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestHillsTerrains.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestHillsTerrains.cs index 53a19ee..b0ab638 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestHillsTerrains.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestHillsTerrains.cs @@ -4,7 +4,7 @@ public class ForestHillsTerrain : TerrainBase { public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainHighland(generator, x, y, river, 10f, 68f, 30f, BaseHeight /*- generator.Preset.SeaLevel*/); + return TerrainHighland(generator, x, y, river, 10f, 68f, 30f, BaseHeight - generator.Preset.SeaLevel); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestTerrain.cs index e7fafb2..7259610 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/ForestTerrain.cs @@ -6,14 +6,6 @@ public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y { GroundNoise = GetGroundNoise(generator, x, y, GroundVariation); return TerrainForest(generator, x, y, river, generator.Preset.SeaLevel + 3 + GroundNoise); - // return TerrainForest(generator, x, y, river, ge) - /*GroundNoise = GetGroundNoise(x, y, GroundVariation, generator); - - float m = Hills(x, y, 10f, generator); - - float floNoise = generator.Preset.SeaLevel + 3 + GroundNoise + m; - - return Riverized(generator, floNoise, river);*/ } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/IcePlainsTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/IcePlainsTerrain.cs new file mode 100644 index 0000000..94cbf82 --- /dev/null +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/IcePlainsTerrain.cs @@ -0,0 +1,14 @@ +namespace OpenAPI.WorldGenerator.Generators.Terrain; + +public class IcePlainsTerrain : TerrainBase +{ + public IcePlainsTerrain() : base(65f) + { + + } + + public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) + { + return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, 200f, BaseHeight); + } +} \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleEdgeTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleEdgeTerrain.cs index cddae79..7b00ec0 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleEdgeTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleEdgeTerrain.cs @@ -4,11 +4,14 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class JungleEdgeTerrain : TerrainBase { - private GroundEffect GroundEffect { get; set; } = new GroundEffect(4f); + public JungleEdgeTerrain() + { + HeightEffect = new GroundEffect(4f); + } public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return Riverized(generator, generator.Preset.SeaLevel + 3 + GroundEffect.Added(generator, x, y), river); + return Riverized(generator, generator.Preset.SeaLevel + 3 + HeightEffect.Added(generator, x, y), river); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleHillsTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleHillsTerrain.cs index 6e5a8e1..8271e5a 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleHillsTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleHillsTerrain.cs @@ -11,7 +11,7 @@ public JungleHillsTerrain(float baseHeight, float hillStrength) : base(baseHeigh public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight/* - generator.Preset.SeaLevel*/); + return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight - generator.Preset.SeaLevel); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleTerrain.cs index df7b379..8bb18f1 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/JungleTerrain.cs @@ -2,9 +2,13 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class JungleTerrain : TerrainBase { + public JungleTerrain(float baseHeight = 66f) : base(baseHeight) + { + + } public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainFlatLakes(generator, x, y, river, 66f); + return TerrainFlatLakes(generator, x, y, river, BaseHeight); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaPlateauTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaPlateauTerrain.cs index a3775d4..3fd5712 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaPlateauTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaPlateauTerrain.cs @@ -26,7 +26,7 @@ public MesaPlateauTerrain(float baseHeight) : base(baseHeight) public override float GenerateNoise(OverworldGeneratorV2 generator, int passedX, int passedY, float border, float river) { - var jitterData = SimplexData2D.NewDisk(); + var jitterData = new SimplexData2D(); generator.SimplexInstance(1).GetValue(passedX / _jitterWavelength, passedY / _jitterWavelength, jitterData); float x = (float) (passedX + jitterData.GetDeltaX() * _jitterAmplitude); float y = (float) (passedY + jitterData.GetDeltaY() * _jitterAmplitude); diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaTerrain.cs index ffaa67c..ea169af 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/MesaTerrain.cs @@ -4,8 +4,6 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class MesaTerrain : TerrainBase { - // private GroundEffect GroundEffect { get; set; }= new GroundEffect(4f); - public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { return TerrainMesa(generator, x, y, river, border);// Riverized(generator, BaseHeight + GroundEffect.Added(generator, x, y), river); diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandShoreTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandShoreTerrain.cs new file mode 100644 index 0000000..7990214 --- /dev/null +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandShoreTerrain.cs @@ -0,0 +1,14 @@ +namespace OpenAPI.WorldGenerator.Generators.Terrain; + +public class MushroomIslandShoreTerrain : TerrainBase +{ + public MushroomIslandShoreTerrain() : base(61.5f) + { + + } + /// + public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) + { + return TerrainMarsh(generator, x, y, BaseHeight, river); + } +} \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandTerrain.cs new file mode 100644 index 0000000..91639e1 --- /dev/null +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/MushroomIslandTerrain.cs @@ -0,0 +1,10 @@ +namespace OpenAPI.WorldGenerator.Generators.Terrain; + +public class MushroomIslandTerrain : TerrainBase +{ + /// + public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) + { + return TerrainGrasslandFlats(generator, x, y, river, 40f, BaseHeight); + } +} \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/PlainsTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/PlainsTerrain.cs index b9bd66b..2f7b688 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/PlainsTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/PlainsTerrain.cs @@ -4,20 +4,15 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class PlainsTerrain : TerrainBase { - private GroundEffect _groundEffect = new GroundEffect(4f); - - public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) + public PlainsTerrain() : base(65f) { - return Riverized(generator, 65f + _groundEffect.Added(generator, x, y), river); - - return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, generator.Preset.HeightScale, BaseHeight); + HeightEffect = new GroundEffect(4f); } - } - - public class IcePlainsTerrain : TerrainBase - { + public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { + return Riverized(generator, BaseHeight + HeightEffect.Added(generator, x, y), river); + return TerrainPlains(generator, x, y, river, 160f, 10f, 60f, generator.Preset.HeightScale, BaseHeight); } } diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/SavannaTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/SavannaTerrain.cs index 5f11aa4..ca981cc 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/SavannaTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/SavannaTerrain.cs @@ -4,11 +4,14 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class SavannaTerrain : TerrainBase { - private GroundEffect GroundEffect { get; set; }= new GroundEffect(4f); + public SavannaTerrain() + { + HeightEffect = new GroundEffect(4f); + } public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return Riverized(generator, generator.Preset.SeaLevel + 3 + GroundEffect.Added(generator, x, y), river); + return Riverized(generator, generator.Preset.SeaLevel + 3 + HeightEffect.Added(generator, x, y), river); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/TaigaHillsTerrain.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/TaigaHillsTerrain.cs index 334e3a5..e8435ea 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/TaigaHillsTerrain.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/TaigaHillsTerrain.cs @@ -2,20 +2,15 @@ namespace OpenAPI.WorldGenerator.Generators.Terrain { public class TaigaHillsTerrain : TerrainBase { - private float HillStrength { get; set; } = 30f; - public TaigaHillsTerrain() : this(72f, 30f) - { - - } - - public TaigaHillsTerrain(float baseHeight, float hillStrength) : base(baseHeight) + private float HillStrength { get; set; } + public TaigaHillsTerrain(float baseHeight = 72f, float hillStrength = 30f) : base(baseHeight) { HillStrength = hillStrength; } public override float GenerateNoise(OverworldGeneratorV2 generator, int x, int y, float border, float river) { - return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight /*- generator.Preset.SeaLevel*/); + return TerrainHighland(generator, x, y, river, 10f, 68f, HillStrength, BaseHeight - generator.Preset.SeaLevel); } } } \ No newline at end of file diff --git a/src/OpenAPI.WorldGenerator/Generators/Terrain/TerrainBase.cs b/src/OpenAPI.WorldGenerator/Generators/Terrain/TerrainBase.cs index 3d8b30e..d21a5fe 100644 --- a/src/OpenAPI.WorldGenerator/Generators/Terrain/TerrainBase.cs +++ b/src/OpenAPI.WorldGenerator/Generators/Terrain/TerrainBase.cs @@ -1,6 +1,7 @@ using System; using MiNET.Utils; using MiNET.Utils.Vectors; +using OpenAPI.WorldGenerator.Generators.Effects; using OpenAPI.WorldGenerator.Utils.Noise; namespace OpenAPI.WorldGenerator.Generators.Terrain @@ -19,6 +20,8 @@ private static float protected float BaseHeight; // added as most terrains have this; protected float GroundNoise; + protected HeightEffect HeightEffect { get; set; } + public TerrainBase(float baseHeight) { @@ -96,14 +99,13 @@ public static float Hills(OverworldGeneratorV2 generator, float x, float y, floa return m * hillStrength; } - public static float GetTerrainBase(OverworldGeneratorV2 generator, float river) { - + public static float GetTerrainBase(OverworldGeneratorV2 generator, float river) + { return generator.Preset.GetTerrainBase() * river; } public static float GetGroundNoise(OverworldGeneratorV2 generator, int x, int y, float amplitude) { - float h = BlendedHillHeight(generator.SimplexInstance(0).GetValue(x / 49f, y / 49f), 0.2f) * amplitude; h += BlendedHillHeight(generator.SimplexInstance(1).GetValue(x / 23f, y / 23f), 0.2f) * amplitude / 2f; h += BlendedHillHeight(generator.SimplexInstance(2).GetValue(x / 11f, y / 11f), 0.2f) * amplitude / 4f; @@ -112,7 +114,6 @@ public static float GetGroundNoise(OverworldGeneratorV2 generator, int x, int y, public static float GetGroundNoise(OverworldGeneratorV2 generator, float x, float y, float amplitude) { - float h = BlendedHillHeight(generator.SimplexInstance(0).GetValue(x / 49f, y / 49f), 0.2f) * amplitude; h += BlendedHillHeight(generator.SimplexInstance(1).GetValue(x / 23f, y / 23f), 0.2f) * amplitude / 2f; h += BlendedHillHeight(generator.SimplexInstance(2).GetValue(x / 11f, y / 11f), 0.2f) * amplitude / 4f; @@ -137,7 +138,7 @@ public static float MountainCap(float m) public static float Riverized(OverworldGeneratorV2 generator, float height, float river) { - var baseH = generator.Preset.SeaLevel + 0.45f; + var baseH = generator.Preset.SeaLevel - 0.55f; if (height < baseH) { return height; @@ -257,12 +258,11 @@ public static float TerrainGrasslandMountains(OverworldGeneratorV2 generator, in return Riverized(generator, baseHeight + h + m, river); } - public static float TerrainHighland(OverworldGeneratorV2 generator, float x, float y, float river, float start, - float width, float height, float baseAdjust) + public static float TerrainHighland(OverworldGeneratorV2 generator, float x, float y, float river, float hillStart, + float hillWidth, float hillHeight, float baseAdjust) { - - float h = generator.SimplexInstance(0).GetValue(x / width, y / width) * height * river; //-140 to 140 - h = h < start ? start + ((h - start) / 4.5f) : h; + float h = generator.SimplexInstance(0).GetValue(x / hillWidth, y / hillWidth) * hillHeight * river; //-140 to 140 + h = h < hillStart ? hillStart + ((h - hillStart) / 4.5f) : h; if (h < 0f) { @@ -273,7 +273,7 @@ public static float TerrainHighland(OverworldGeneratorV2 generator, float x, flo { float st = h * 1.5f > 15f ? 15f : h * 1.5f; // 0 to 15 h += generator.SimplexInstance(4).GetValue(x / 70f, y / 70f, 1f) * st; // 0 to 155 - h = h * river; + h *= river; } h += BlendedHillHeight(generator.SimplexInstance(0).GetValue(x / 20f, y / 20f), 0f) * 4f; @@ -282,7 +282,7 @@ public static float TerrainHighland(OverworldGeneratorV2 generator, float x, flo if (h < 0) { - h = h / 2f; + h /= 2f; } if (h < -3) @@ -290,7 +290,7 @@ public static float TerrainHighland(OverworldGeneratorV2 generator, float x, flo h = (h + 3f) / 2f - 3f; } - return (GetTerrainBase(generator, river)) + (h + baseAdjust) * river; + return (GetTerrainBase(generator, river)) + ((h + baseAdjust) * river); } public static float TerrainLonelyMountain(OverworldGeneratorV2 generator, int x, int y, float river, @@ -618,7 +618,7 @@ public static float GetRiverStrength(OverworldGeneratorV2 generator, int x, int int worldZ = z; float pX = worldX; float pZ = worldZ; - var jitterData = SimplexData2D.NewDisk(); + var jitterData = new SimplexData2D(); //New river curve function. No longer creates worldwide curve correlations along cardinal axes. generator.SimplexInstance(1).GetValue((float) worldX / 240.0f, (float) worldZ / 240.0f, jitterData); pX += jitterData.GetDeltaX() * generator.RiverLargeBendSize; diff --git a/src/OpenAPI.WorldGenerator/Utils/Noise/Api/ISimplexData2D.cs b/src/OpenAPI.WorldGenerator/Utils/Noise/Api/ISimplexData2D.cs index 96071b5..c47d2c4 100644 --- a/src/OpenAPI.WorldGenerator/Utils/Noise/Api/ISimplexData2D.cs +++ b/src/OpenAPI.WorldGenerator/Utils/Noise/Api/ISimplexData2D.cs @@ -16,7 +16,7 @@ public interface ISimplexData2D { void Clear(); - IDataRequest Request(); + void Request(float attn, float extrapolation, float gx, float gy, int giSph2, float dx, float dy); public interface IDataRequest { diff --git a/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/FilterNoiseModule.cs b/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/FilterNoiseModule.cs index 9931a9b..691deea 100644 --- a/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/FilterNoiseModule.cs +++ b/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/FilterNoiseModule.cs @@ -33,7 +33,7 @@ public float Frequency get { return this._frequency; } set { this._frequency = value; } } - + /// /// A multiplier that determines how quickly the frequency increases for each successive octave in a Perlin-noise function. /// diff --git a/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/VoronoiNoseModule.cs b/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/VoronoiNoseModule.cs index b9b0f95..a725aab 100644 --- a/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/VoronoiNoseModule.cs +++ b/src/OpenAPI.WorldGenerator/Utils/Noise/Modules/VoronoiNoseModule.cs @@ -4,19 +4,21 @@ namespace OpenAPI.WorldGenerator.Utils.Noise.Modules { - public class VoronoiNoseModule : FilterNoise, INoiseModule + public class VoronoiNoseModule : INoiseModule { public const float DefaultDisplacement = 1.0f; - public VoronoiNoseModule() + private INoiseModule Source { get; } + + public VoronoiNoseModule(INoiseModule source) { - + Source = source; } private float _displacement = DefaultDisplacement; private bool _distance; - + protected float _frequency = 1f; /// /// This noise module assigns each Voronoi cell with a random constant @@ -43,8 +45,17 @@ public bool Distance set { _distance = value; } } + /// + /// The number of cycles per unit length that a specific coherent-noise function outputs. + /// + [Modifier] + public float Frequency + { + get { return this._frequency; } + set { this._frequency = value; } + } + public int Size { get; set; } = 2; - public float GetValue(float x, float y) { x *= _frequency; @@ -66,10 +77,11 @@ public float GetValue(float x, float y) { // Calculate the position and distance to the seed point inside of // this unit cube. - var off = _source.GetValue(xCur, yCur); - float xPos = xCur + off;//_source2D.GetValue(xCur, yCur); - float yPos = yCur + off;//_source2D.GetValue(xCur, yCur); - + var off = Source.GetValue(xCur, yCur); + + float xPos = xCur + off; + float yPos = yCur + off; + float xDist = xPos - x; float yDist = yPos - y; float dist = xDist * xDist + yDist * yDist; @@ -99,7 +111,7 @@ public float GetValue(float x, float y) value = 0.0f; // Return the calculated distance with the displacement value applied. - return value + (_displacement * _source.GetValue( + return value + (_displacement * Source.GetValue( MathF.Floor(xCandidate), MathF.Floor(yCandidate)) ); @@ -139,9 +151,11 @@ public float GetValue(float x, float y, float z) { // Calculate the position and distance to the seed point inside of // this unit cube. - float xPos = xCur + _source.GetValue(xCur, yCur, zCur); - float yPos = yCur + _source.GetValue(xCur, yCur, zCur); - float zPos = zCur + _source.GetValue(xCur, yCur, zCur); + var off = Source.GetValue(xCur, yCur, zCur); + + float xPos = xCur + off; + float yPos = yCur + off; + float zPos = zCur + off; float xDist = xPos - x; float yDist = yPos - y; @@ -176,7 +190,7 @@ public float GetValue(float x, float y, float z) value = 0.0f; // Return the calculated distance with the displacement value applied. - return value + (_displacement*_source.GetValue( + return value + (_displacement*Source.GetValue( (MathF.Floor(xCandidate)), (MathF.Floor(yCandidate)), (MathF.Floor(zCandidate))) diff --git a/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/SimplexNoise.cs b/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/SimplexNoise.cs index ff32744..5538802 100644 --- a/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/SimplexNoise.cs +++ b/src/OpenAPI.WorldGenerator/Utils/Noise/Primitives/SimplexNoise.cs @@ -771,7 +771,8 @@ public virtual void GetValue(double x, double y, ISimplexData2D data) double gy = Gradients2D[gi + 1]; double extrp = gx * dx + gy * dy; int giSph2 = _perm2DSph2[giP]; - data.Request().Apply((float) attn, (float) extrp, (float) gx, (float) gy, giSph2, (float) dx, (float) dy); + + data.Request((float) attn, (float) extrp, (float) gx, (float) gy, giSph2, (float) dx, (float) dy); } } diff --git a/src/OpenAPI.WorldGenerator/Utils/Noise/SimplexData2D.cs b/src/OpenAPI.WorldGenerator/Utils/Noise/SimplexData2D.cs index 705e98a..71c7f16 100644 --- a/src/OpenAPI.WorldGenerator/Utils/Noise/SimplexData2D.cs +++ b/src/OpenAPI.WorldGenerator/Utils/Noise/SimplexData2D.cs @@ -4,67 +4,44 @@ namespace OpenAPI.WorldGenerator.Utils.Noise { - public abstract class SimplexData2D : ISimplexData2D + public class SimplexData2D : ISimplexData2D { + private float _deltaX; + private float _deltaY; - private float deltaX; - private float deltaY; - - private SimplexData2D() + public SimplexData2D() { this.Clear(); } - /** - * Gets a new {@link SimplexData2D.Disk} multi-evaluation data object for use in generating jitter effects. - * - * @return a new instance of SimplexData2D.Disk - * @since 1.0.0 - */ - public static ISimplexData2D NewDisk() - { - return new Disk(); - } - - /** - * Gets a new {@link SimplexData2D.Derivative} multi-evaluation data object for use in generating jitter effects. - * - * @return a new instance of SimplexData2D.Derivative - * @since 1.0.0 - */ - public static ISimplexData2D NewDerivative() - { - return new Derivative(); - } - public float GetDeltaX() { - return this.deltaX; + return this._deltaX; } public void SetDeltaX(float deltaX) { - this.deltaX = deltaX; + this._deltaX = deltaX; } public float GetDeltaY() { - return this.deltaY; + return this._deltaY; } public void SetDeltaY(float deltaY) { - this.deltaY = deltaY; + this._deltaY = deltaY; } public void AddToDeltaX(float val) { - this.deltaX += val; + this._deltaX += val; } public void AddToDeltaY(float val) { - this.deltaY += val; + this._deltaY += val; } public void Clear() @@ -73,74 +50,12 @@ public void Clear() this.SetDeltaY(0.0f); } - public virtual ISimplexData2D.IDataRequest Request() + public virtual void Request(float attn, float extrapolation, float gx, float gy, int giSph2, float dx, float dy) { - return new Disk.DiskDataRequest(this); - } - - public class Disk : SimplexData2D - { - - public Disk() : base() - { - - } - - public override ISimplexData2D.IDataRequest Request() - { - return new DiskDataRequest(this); - } - - public class DiskDataRequest : ISimplexData2D.IDataRequest - { - private SimplexData2D Data { get; } - - public DiskDataRequest(SimplexData2D data2D) - { - Data = data2D; - } - - public void Apply(float attn, float extrapolation, float gx, float gy, int gi_sph2, float dx, - float dy) - { - float attnSq = attn * attn; - float extrap = attnSq * attnSq * extrapolation; - Data.AddToDeltaX((float) (extrap * SimplexPerlin.GradientsSph2[gi_sph2])); - Data.AddToDeltaY((float) (extrap * SimplexPerlin.GradientsSph2[gi_sph2 + 1])); - } - } - } - - public class Derivative : SimplexData2D - { - - public Derivative() - { - - } - - public override ISimplexData2D.IDataRequest Request() - { - return new DerivativeDataRequest(this); - } - - public class DerivativeDataRequest : ISimplexData2D.IDataRequest - { - private SimplexData2D Data { get; } - - public DerivativeDataRequest(SimplexData2D data2D) - { - Data = data2D; - } - - public void Apply(float attn, float extrapolation, float gx, float gy, int gi_sph2, float dx, - float dy) - { - double attnSq = attn * attn; - Data.AddToDeltaX((float) ((gx * attn - 8f * dx * extrapolation) * attnSq * attn)); - Data.AddToDeltaY((float) ((gy * attn - 8f * dy * extrapolation) * attnSq * attn)); - } - } + float attnSq = attn * attn; + float extrap = attnSq * attnSq * extrapolation; + AddToDeltaX((float) (extrap * SimplexPerlin.GradientsSph2[giSph2])); + AddToDeltaY((float) (extrap * SimplexPerlin.GradientsSph2[giSph2 + 1])); } } } \ No newline at end of file diff --git a/src/WorldGenerator.Tweaking/Program.cs b/src/WorldGenerator.Tweaking/Program.cs index dfae97f..2589c09 100644 --- a/src/WorldGenerator.Tweaking/Program.cs +++ b/src/WorldGenerator.Tweaking/Program.cs @@ -3,83 +3,143 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.Xna.Framework; using MiNET.Utils.Vectors; using OpenAPI.WorldGenerator.Generators; +using OpenAPI.WorldGenerator.Utils.Noise; +using OpenAPI.WorldGenerator.Utils.Noise.Modules; +using OpenAPI.WorldGenerator.Utils.Noise.Primitives; +using OpenAPI.WorldGenerator.Utils.Noise.Transformers; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace WorldGenerator.Tweaking { class Program { - private static TestGame Game { get; set; } - public const int Radius = 372; + private static Game Game { get; set; } + public const int Radius = 512; public const int Resolution = 1; - //private static ConcurrentQueue Finished = new ConcurrentQueue(); + static void Main(string[] args) { WorldGen = new OverworldGeneratorV2(); - Game = new TestGame(WorldGen, Radius, Resolution); - // gen.ApplyBlocks = true; + // RunNoiseTest(); + RunPreviewer(); + //Console.WriteLine($"Min Height: {gen.MinHeight} Max Height: {gen.MaxHeight}"); + } + + private static void RunNoiseTest() + { + var seed = 124; + var biomeScale = 4 * 16f; + + INoiseModule temperatureNoise = new VoronoiNoseModule(new AverageSelectorModule(new SimplexPerlin(seed ^ 3, NoiseQuality.Fast), new SimplexPerlin(seed ^ 64, NoiseQuality.Fast))) + { + Distance = true, + Frequency = 0.0325644f, + Displacement = 2f + }; + + temperatureNoise = new ScaledNoiseModule(temperatureNoise) + { + ScaleX = 1f / biomeScale, ScaleY = 1f / biomeScale, ScaleZ = 1f / biomeScale + }; + + Image output = new Image(Radius, Radius); + for (int x = 0; x < Radius; x++) + { + for (int z = 0; z < Radius; z++) + { + var temp = MathF.Abs(temperatureNoise.GetValue(x * 16f, z* 16f)); + // var rain = WorldGen.RainfallNoise.GetValue(x* 16f, z* 16f); + + output[x, z] = new Rgba32((1f / 2f) * temp, 0f, 0f); + } + } + output.SaveAsPng("test.png"); + + return; + } + + private static void RunPreviewer() + { + var game = new TestGame(WorldGen, Radius, Resolution); + Game = game; bool done = false; - // ChunkColumn[] generatedChunks = new ChunkColumn[chunks * chunks]; + long average = 0; long min = long.MaxValue; long max = long.MinValue; - int chunskGenerated = 0; - // threads[0] = new Thread(() => { GenerateBiomeMap(chunks); }); + int chunskGenerated = 0; List gen = new List(); - for (int z = 0; z < Radius; z+= Resolution) + + for (int z = 0; z < Radius; z++) { - for(int x= 0; x < Radius; x+= Resolution) + for (int x = 0; x < Radius; x++) { - gen.Add(new ChunkCoordinates(x, z)); + var cc = new ChunkCoordinates((x), (z)); + gen.Add(cc); } } + var cancellationToken = new CancellationTokenSource(); - new Thread(() => - { - Stopwatch timer = Stopwatch.StartNew(); - Parallel.ForEach( - gen, new ParallelOptions() - { - CancellationToken = cancellationToken.Token - }, coords => - { - Stopwatch timing = new Stopwatch(); - timing.Restart(); - - Game.Add(WorldGen.GenerateChunkColumn(coords)); - - chunskGenerated++; - - timing.Stop(); - - average += timing.ElapsedMilliseconds; - - if (timing.ElapsedMilliseconds < min) - min = timing.ElapsedMilliseconds; - - if (timing.ElapsedMilliseconds > max) - max = timing.ElapsedMilliseconds; - }); - - timer.Stop(); - - Console.Clear(); - - Console.WriteLine($"Generating {Radius * Radius} chunks took: {timer.Elapsed}"); - }).Start(); - + + new Thread( + () => + { + int total = gen.Count; + Stopwatch timer = Stopwatch.StartNew(); + + Parallel.ForEach( + gen, new ParallelOptions() {CancellationToken = cancellationToken.Token}, coords => + { + try + { + Stopwatch timing = new Stopwatch(); + timing.Restart(); + + game.Add(WorldGen.GenerateChunkColumn(coords)); + + timing.Stop(); + + average += timing.ElapsedMilliseconds; + + if (timing.ElapsedMilliseconds < min) + min = timing.ElapsedMilliseconds; + + if (timing.ElapsedMilliseconds > max) + max = timing.ElapsedMilliseconds; + + if (Interlocked.Increment(ref chunskGenerated) % 500 == 0) + { + float progress = (1f / total) * chunskGenerated; + Console.WriteLine($"[{progress:P000}] Generated {chunskGenerated} of {total} chunks. Average={(average / chunskGenerated):F2}ms"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Ehhh: {ex}"); + } + }); + + timer.Stop(); + + Console.Clear(); + + Console.WriteLine($"Generating {chunskGenerated} chunks took: {timer.Elapsed}"); + }).Start(); + Game.Run(); - + cancellationToken.Cancel(); - //Console.WriteLine($"Min Height: {gen.MinHeight} Max Height: {gen.MaxHeight}"); } - + public static OverworldGeneratorV2 WorldGen { get; set; } } } \ No newline at end of file diff --git a/src/WorldGenerator.Tweaking/TestGame.cs b/src/WorldGenerator.Tweaking/TestGame.cs index 2eeea48..9c8e01a 100644 --- a/src/WorldGenerator.Tweaking/TestGame.cs +++ b/src/WorldGenerator.Tweaking/TestGame.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; +using MiNET.Utils.Vectors; using MiNET.Worlds; using Newtonsoft.Json; using OpenAPI.WorldGenerator.Generators; @@ -21,16 +22,12 @@ public class TestGame : Game private GraphicsDeviceManager _graphics; private OverworldGeneratorV2 _worldGen; - private Dictionary _blockColors; public TestGame(OverworldGeneratorV2 worldGen, int chunks, int resolution) { _worldGen = worldGen; _chunks = chunks; _resolution = resolution; _graphics = new GraphicsDeviceManager(this); - - _blockColors = JsonConvert.DeserializeObject>( - File.ReadAllText(Path.Combine("Resources", "blockColors.json"))); } /// @@ -79,6 +76,7 @@ public void Add(ChunkColumn chunk) private MouseState _mouseState = new MouseState(); + private int CursorHeight { get; set; } = 0; private BiomeBase ClickedBiome { get; set; } = null; private Point CursorPos { get; set; } = Point.Zero; @@ -107,6 +105,7 @@ protected override void Update(GameTime gameTime) (float) stageData.Texture.Width / width, (float) stageData.Texture.Height / height); pos = new Point((int) (pos.X * scale.X), (int) (pos.Y * scale.Y)); + //var chunkPos = new ChunkCoordinates(pos.X >> 4, pos.Y >> 4); var color = stageData.GetColorAt(pos.X, pos.Y); var biome = _worldGen.BiomeRegistry.Biomes.FirstOrDefault(x => { @@ -114,11 +113,26 @@ protected override void Update(GameTime gameTime) return c.R == color.R && c.B == color.B && c.G == color.G; }); + ClickedBiome = biome; CursorPos = pos; } } + else if (pos.X >= width && pos.X <= width * 2f && pos.Y >= height && pos.Y <= height * 2f) + { + pos.X -= width; + pos.Y -= height; + if (_stageDatas.TryGetValue(RenderStage.Height, out var stageData)) + { + var scale = new Vector2( + (float) stageData.Texture.Width / width, (float) stageData.Texture.Height / height); + + pos = new Point((int) (pos.X * scale.X), (int) (pos.Y * scale.Y)); + var color = stageData.GetColorAt(pos.X, pos.Y); + CursorHeight = color.R; + } + } } _biomeInfoSw.Restart(); @@ -146,7 +160,12 @@ protected override void Draw(GameTime gameTime) stage.Value.AddChunk(chunk); } - chunk?.Dispose(); + foreach (var subChunk in chunk) + { + subChunk.Dispose(); + } + + chunk.Dispose(); updates++; } @@ -163,11 +182,13 @@ protected override void Draw(GameTime gameTime) var size = _spriteFont.MeasureString(text); _spriteBatch.DrawString(_spriteFont, text, new Vector2(width - size.X, 10), Color.White); } + + DrawHeight(width * 2f, height + 10f); var updateText = $"Updates: {updates:000}\n" + $"Temp: min={_worldGen.MinTemp:F3} max={_worldGen.MaxTemp:F3} range={(_worldGen.MaxTemp - _worldGen.MinTemp):F3}\n" + $"Rain: min={_worldGen.MinRain:F3} max={_worldGen.MaxRain:F3} range={(_worldGen.MaxRain - _worldGen.MinRain):F3}\n" - + $"Selector: min={_worldGen.MinSelector:F3} max={_worldGen.MaxSelector:F3} range={(_worldGen.MaxSelector - _worldGen.MinSelector):F3/*}\n"/* + + $"Selector: min={_worldGen.MinSelector:F3} max={_worldGen.MaxSelector:F3} range={(_worldGen.MaxSelector - _worldGen.MinSelector):F3}\n"/* + $"Height: min={_worldGen.MinHeight:F3} max={_worldGen.MaxHeight:F3} range={(_worldGen.MaxHeight - _worldGen.MinHeight):F3}\n"*/; var updateTextSize = _spriteFont.MeasureString(updateText); _spriteBatch.DrawString(_spriteFont, updateText, new Vector2(GraphicsDevice.Viewport.Width - (updateTextSize.X + 5), 5), Color.White); @@ -177,6 +198,13 @@ protected override void Draw(GameTime gameTime) base.Draw(gameTime); } + private void DrawHeight(float x, float y) + { + string text = $"Height: {CursorHeight:000}"; + var size = _spriteFont.MeasureString(text); + _spriteBatch.DrawString(_spriteFont, text, new Vector2(x - size.X + 10, y), Color.White); + } + private void DrawBorder(Rectangle rectangleToDraw, int thicknessOfBorder, Color borderColor) { // Draw top line @@ -236,11 +264,10 @@ public enum RenderStage { Temperature, Height, - Biomes, - Blocks + Biomes } - private class StageData + private class StageData { private readonly RenderStage _stage; private readonly GraphicsDevice _device; @@ -380,17 +407,6 @@ private void DrawChunk(ChunkColumn data) var c = biome.Color.GetValueOrDefault(); _spriteBatch.Draw(_pixel, pixelPosition, new Color(c.R, c.G, c.B)); } break; - - case RenderStage.Blocks: - { - var block = data.GetBlockObject(x, data.GetHeight(x,y), y); - - if (_parent._blockColors.TryGetValue(block.Name, out uint color)) - { - int height = data.GetHeight(x, y); - _spriteBatch.Draw(_pixel, pixelPosition, ChangeColorBrightness(new Color(color), -(1f / 255f) * height)); - } - } break; } } } From c37286afd8b872528e506a5ce340af494850fdda Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 14:57:31 +0200 Subject: [PATCH 08/13] fix cursor position when map isn't at 0,0 --- src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index 7444be3..4115cae 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -576,8 +576,8 @@ private void RecalculateTransform() var p = MapPosition; Transform = Matrix.Identity // * Matrix.CreateRotationX(MathHelper.Pi) - * Matrix.CreateScale(_scale, _scale, 1f) * Matrix.CreateTranslation(-p.X, -p.Y, 0) + * Matrix.CreateScale(_scale, _scale, 1f) ; RecalculateMapBounds(); @@ -679,7 +679,7 @@ private void UpdateMouseInput() private Point Unproject(Point cursor) { - return _mapPosition + (cursor.ToVector2() / _scale).ToPoint(); + return (cursor.ToVector2() / _scale).ToPoint() + _mapPosition; // return Vector2.Transform(cursor, InverseTransform); // return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f)); From 746262a764bc7f11853d12ed9c79969eb510fc04 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 15:26:02 +0200 Subject: [PATCH 09/13] lang version 10 --- src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj b/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj index 21d87ba..618e93c 100644 --- a/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj +++ b/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj @@ -2,6 +2,7 @@ net6.0 + 10 From 6a023463c17c37309eb1add452ba67c30f5ded57 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 18:33:48 +0200 Subject: [PATCH 10/13] performance improvements for mapviewer (render texture per region, but still update per chunk) --- .../Components/GuiMapViewer.ImGui.cs | 446 ++++++++++++++++++ .../Components/GuiMapViewer.cs | 443 +++-------------- .../Graphics/CachedRegionMesh.cs | 65 ++- .../Graphics/RegionMeshManager.cs | 21 +- .../Graphics/SpriteBatchExtensions.cs | 9 + src/MiMap.Viewer.DesktopGL/Models/Map.cs | 16 +- .../Utilities/Spiral.cs | 49 ++ 7 files changed, 655 insertions(+), 394 deletions(-) create mode 100644 src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.ImGui.cs create mode 100644 src/MiMap.Viewer.DesktopGL/Utilities/Spiral.cs diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.ImGui.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.ImGui.cs new file mode 100644 index 0000000..fe7c4f2 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.ImGui.cs @@ -0,0 +1,446 @@ +using System; +using System.Numerics; +using Microsoft.Xna.Framework; +using ImGuiNET; +using OpenAPI.WorldGenerator.Generators.Biomes; +using static ImGuiNET.ImGui; + +namespace MiMap.Viewer.DesktopGL.Components +{ + public partial class GuiMapViewer + { + private Point _cursorPosition; + private int[] _cursorBlock = new int[3]; + private BiomeBase _cursorBlockBiome; + private int[] _cursorChunk = new int[2]; + private int[] _cursorRegion = new int[2]; + private int _cursorBlockBiomeId; + + private void DrawImGui_StyleEditor() + { + if (Begin("ImGui Style Editor")) + { + if (BeginTableEx("styleeditor", 2)) + { + var io = GetIO(); + io.ConfigWindowsResizeFromEdges = true; + var style = GetStyle(); + + TableRowEditor(nameof(style.Alpha), ref style.Alpha); + TableRowEditor(nameof(style.AntiAliasedFill), ref style.AntiAliasedFill); + TableRowEditor(nameof(style.AntiAliasedLines), ref style.AntiAliasedLines); + TableRowEditor(nameof(style.AntiAliasedLinesUseTex), ref style.AntiAliasedLinesUseTex); + TableRowEditor(nameof(style.ButtonTextAlign), ref style.ButtonTextAlign); + TableRowEditor(nameof(style.CellPadding), ref style.CellPadding); + TableRowEditor(nameof(style.ChildBorderSize), ref style.ChildBorderSize); + TableRowEditor(nameof(style.ChildRounding), ref style.ChildRounding); + TableRowEditor(nameof(style.CircleTessellationMaxError), ref style.CircleTessellationMaxError); + // TableRowEditor(nameof(style.ColorButtonPosition), ref style.ColorButtonPosition); + // TableRowEditor(nameof(style.Colors), ref style.Colors); + TableRowEditor(nameof(style.ColumnsMinSpacing), ref style.ColumnsMinSpacing); + TableRowEditor(nameof(style.CurveTessellationTol), ref style.CurveTessellationTol); + TableRowEditor(nameof(style.DisabledAlpha), ref style.DisabledAlpha); + TableRowEditor(nameof(style.DisplaySafeAreaPadding), ref style.DisplaySafeAreaPadding); + TableRowEditor(nameof(style.FrameBorderSize), ref style.FrameBorderSize); + TableRowEditor(nameof(style.FramePadding), ref style.FramePadding); + TableRowEditor(nameof(style.FrameRounding), ref style.FrameRounding); + TableRowEditor(nameof(style.GrabMinSize), ref style.GrabMinSize); + TableRowEditor(nameof(style.GrabRounding), ref style.GrabRounding); + TableRowEditor(nameof(style.IndentSpacing), ref style.IndentSpacing); + TableRowEditor(nameof(style.ItemInnerSpacing), ref style.ItemInnerSpacing); + TableRowEditor(nameof(style.ItemSpacing), ref style.ItemSpacing); + TableRowEditor(nameof(style.LogSliderDeadzone), ref style.LogSliderDeadzone); + TableRowEditor(nameof(style.MouseCursorScale), ref style.MouseCursorScale); + TableRowEditor(nameof(style.PopupBorderSize), ref style.PopupBorderSize); + TableRowEditor(nameof(style.PopupRounding), ref style.PopupRounding); + TableRowEditor(nameof(style.ScrollbarRounding), ref style.ScrollbarRounding); + TableRowEditor(nameof(style.ScrollbarSize), ref style.ScrollbarSize); + TableRowEditor(nameof(style.SelectableTextAlign), ref style.SelectableTextAlign); + TableRowEditor(nameof(style.TabBorderSize), ref style.TabBorderSize); + TableRowEditor(nameof(style.TabMinWidthForCloseButton), ref style.TabMinWidthForCloseButton); + TableRowEditor(nameof(style.TabRounding), ref style.TabRounding); + TableRowEditor(nameof(style.TouchExtraPadding), ref style.TouchExtraPadding); + TableRowEditor(nameof(style.WindowBorderSize), ref style.WindowBorderSize); + TableRowEditor(nameof(style.WindowPadding), ref style.WindowPadding); + TableRowEditor(nameof(style.WindowRounding), ref style.WindowRounding); + TableRowEditor(nameof(style.WindowMinSize), ref style.WindowMinSize); + TableRowEditor(nameof(style.WindowTitleAlign), ref style.WindowTitleAlign); + TableRowEditor(nameof(style.DisplayWindowPadding), ref style.DisplayWindowPadding); + + EndTable(); + } + + End(); + } + } + + private bool BeginTableEx(string name, int columns, ImGuiTableFlags flags = ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit) + { + if (BeginTable(name, columns, flags)) + { + TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); + for (int i = 0; i < columns; i++) + { + TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); + } + + return true; + } + + return false; + } + + private void TableRowEditor(string label, ref string value) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + TableNextColumn(); + InputText(null, ref value, 0); + } + + private void TableRowEditor(string label, ref int value) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + TableNextColumn(); + InputInt(null, ref value); + } + + private void TableRowEditor(string label, ref float value) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + TableNextColumn(); + DragFloat(null, ref value); + } + + private void TableRowEditor(string label, ref bool value) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + TableNextColumn(); + Checkbox(null, ref value); + } + + private void TableRowEditor(string label, ref System.Numerics.Vector2 value) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + TableNextColumn(); + DragFloat2(null, ref value); + } + + private void TableRowEditor(string label, ref System.Numerics.Vector3 value) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + TableNextColumn(); + DragFloat3(null, ref value); + } + + private void TableRowEx(string label, params Action[] columns) + { + TableNextRow(); + TableNextColumn(); + Text(label); + + for (int i = 0; i < columns.Length; i++) + { + TableNextColumn(); + columns[i].Invoke(); + } + } + + private void TableRowEx(params Action[] columns) + { + TableNextRow(); + for (int i = 0; i < columns.Length; i++) + { + TableNextColumn(); + columns[i].Invoke(); + } + } + + private void DrawImGui() + { + try + { + var io = GetIO(); + io.ConfigFlags |= ImGuiConfigFlags.DockingEnable; + io.ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + io.ConfigFlags |= ImGuiConfigFlags.DpiEnableScaleViewports; + io.ConfigFlags |= ImGuiConfigFlags.DpiEnableScaleFonts; + io.ConfigWindowsResizeFromEdges = true; + + //DrawImGui_StyleEditor(); + + DockSpaceOverViewport(GetMainViewport(), ImGuiDockNodeFlags.NoDockingInCentralNode | ImGuiDockNodeFlags.PassthruCentralNode); + + if (Begin("Map Viewer", ImGuiWindowFlags.DockNodeHost)) + { + if (BeginTable("mapviewtable", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) + { + _mapPositions[0] = _mapPosition.X; + _mapPositions[1] = _mapPosition.Y; + TableNextRow(); + TableNextColumn(); + Text("Map Position"); + TableNextColumn(); + InputInt2("##value", ref _mapPositions[0]); + if (IsItemEdited()) + { + MapPosition = new Point(_mapPositions[0], _mapPositions[1]); + } + + var bounds = MapBounds; + var boundsValues = new int[] { bounds.X, bounds.Y, bounds.Width, bounds.Height }; + + TableNextRow(); + TableNextColumn(); + Text("Map Bounds"); + TableNextColumn(); + InputInt4("##value", ref boundsValues[0], ImGuiInputTextFlags.ReadOnly); + + TableNextRow(); + TableNextColumn(); + Text("Scale"); + TableNextColumn(); + SliderFloat("##value", ref _scale, MinScale, MaxScale); + if (IsItemEdited()) + { + RecalculateTransform(); + } + + TableNextRow(); + + // PopID(); + EndTable(); + } + + Spacing(); + + if (BeginTable("mapviewtable", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) + { + var cursorPositionValues = new int[] + { + _cursorPosition.X, + _cursorPosition.Y + }; + TableNextRow(); + TableNextColumn(); + Text("Cursor Position"); + TableNextColumn(); + InputInt2("##value", ref cursorPositionValues[0], ImGuiInputTextFlags.ReadOnly); + + + EndTable(); + } + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNodeEx("Graphics")) + { + if (BeginTable("graphics", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit)) + { + TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); + TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); + + var v = GraphicsDevice.Viewport; + var viewportValues = new int[] + { + v.X, + v.Y, + v.Width, + v.Height + }; + + TableNextRow(); + TableNextColumn(); + Text("Viewport"); + TableNextColumn(); + InputInt4("##value", ref viewportValues[0], ImGuiInputTextFlags.ReadOnly); + + EndTable(); + } + + TreePop(); + } + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNodeEx("Window")) + { + if (BeginTable("window", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit)) + { + TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); + TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); + + var p = Game.Window.Position; + var windowPositionValues = new int[] + { + p.X, + p.Y + }; + TableNextRow(); + TableNextColumn(); + Text("Position"); + TableNextColumn(); + InputInt2("##value", ref windowPositionValues[0], ImGuiInputTextFlags.ReadOnly); + + + var c = Game.Window.ClientBounds; + var windowClientBoundsValues = new int[] + { + c.X, + c.Y, + c.Width, + c.Height + }; + TableNextRow(); + TableNextColumn(); + Text("Client Bounds"); + TableNextColumn(); + InputInt4("##value", ref windowClientBoundsValues[0], ImGuiInputTextFlags.ReadOnly); + + + EndTable(); + } + + TreePop(); + } + + End(); + } + + if (Begin("Info")) + { + Text("At Cursor"); + InputInt3("Block", ref _cursorBlock[0], ImGuiInputTextFlags.ReadOnly); + InputInt2("Chunk", ref _cursorChunk[0], ImGuiInputTextFlags.ReadOnly); + InputInt2("Region", ref _cursorRegion[0], ImGuiInputTextFlags.ReadOnly); + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNode("Biome Info")) + { + var biome = _cursorBlockBiome; + + var biomeId = biome?.Id ?? _cursorBlockBiomeId; + var biomeName = biome?.Name ?? string.Empty; + var biomeDefinitionName = biome?.DefinitionName ?? string.Empty; + var biomeMinHeight = biome?.MinHeight ?? 0; + var biomeMaxHeight = biome?.MaxHeight ?? 0; + var biomeTemperature = biome?.Temperature ?? 0; + var biomeDownfall = biome?.Downfall ?? 0; + + InputInt("ID", ref biomeId, 0, 0, ImGuiInputTextFlags.ReadOnly); + InputText("Name", ref biomeName, 0, ImGuiInputTextFlags.ReadOnly); + InputText("Definition Name", ref biomeDefinitionName, 0, ImGuiInputTextFlags.ReadOnly); + InputFloat("Min Height", ref biomeMinHeight, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + InputFloat("Max Height", ref biomeMaxHeight, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + InputFloat("Temperature", ref biomeTemperature, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + InputFloat("Downfall", ref biomeDownfall, 0, 0, null, ImGuiInputTextFlags.ReadOnly); + + SetNextItemOpen(true, ImGuiCond.FirstUseEver); + if (TreeNode("Config")) + { + var cfg = biome?.Config; + BeginDisabled(); + + var cfgIsEdgeBiome = cfg?.IsEdgeBiome ?? false; + var cfgAllowRivers = cfg?.AllowRivers ?? false; + var cfgAllowScenicLakes = cfg?.AllowScenicLakes ?? false; + var cfgSurfaceBlendIn = cfg?.SurfaceBlendIn ?? false; + var cfgSurfaceBlendOut = cfg?.SurfaceBlendOut ?? false; + var cfgWeight = cfg?.Weight ?? 0; + + Checkbox("Is Edge Biome", ref cfgIsEdgeBiome); + Checkbox("Allow Rivers", ref cfgAllowRivers); + Checkbox("Allow Scenic Lakes", ref cfgAllowScenicLakes); + Checkbox("Surface Blend In", ref cfgSurfaceBlendIn); + Checkbox("Surface Blend Out", ref cfgSurfaceBlendOut); + InputInt("Weight", ref cfgWeight); + + EndDisabled(); + + TreePop(); + } + + + TreePop(); + } + + End(); + } + + if (Begin("Biome Colors")) + { + if (BeginTable("biomeclr", 3, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) + { + foreach (var c in Map.BiomeRegistry.Biomes) + { + TableNextRow(); + TableNextColumn(); + Text(c.Id.ToString()); + TableNextColumn(); + Text(c.Name); + TableNextColumn(); + TableSetBgColor(ImGuiTableBgTarget.CellBg, GetColor(c.Color ?? System.Drawing.Color.Transparent)); + Text(" "); + } + + EndTable(); + } + + End(); + } + } + catch (Exception ex) + { + Log.Error(ex, $"Drawing exception."); + } + } + + private void OnCursorMove_ImGui(Point cursorPosition, Point previousCursorPosition, bool isCursorDown) + { + var cursorBlockPos = Unproject(cursorPosition); + // var cursorBlockPos = Vector3.Transform(new Vector3(cursorPosition.X, cursorPosition.Y, 0f), Transform*_effect.View); + _cursorBlock[0] = cursorBlockPos.X; + _cursorBlock[2] = cursorBlockPos.Y; + _cursorChunk[0] = _cursorBlock[0] >> 4; + _cursorChunk[1] = _cursorBlock[2] >> 4; + _cursorRegion[0] = _cursorBlock[0] >> 9; + _cursorRegion[1] = _cursorBlock[2] >> 9; + + var cursorBlockRegion = Map.GetRegion(new Point(_cursorRegion[0], _cursorRegion[1])); + if (cursorBlockRegion?.IsComplete ?? false) + { + var cursorBlockChunk = cursorBlockRegion[_cursorChunk[0] % 32, _cursorChunk[1] % 32]; + var x = _cursorBlock[0] % 16; + var z = _cursorBlock[2] % 16; + _cursorBlock[1] = (int)cursorBlockChunk.GetHeight(x, z); + _cursorBlockBiomeId = (int)cursorBlockChunk.GetBiome(x, z); + _cursorBlockBiome = Map.BiomeRegistry.GetBiome(_cursorBlockBiomeId); + } + } + + private static uint GetColor(System.Drawing.Color color) + { + return (uint)( + 0xFF << 24 + | ((color.B & 0xFF) << 16) + | ((color.G & 0xFF) << 8) + | ((color.R & 0xFF) << 0) + ); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index 4115cae..0b36822 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using ImGuiNET; using Microsoft.Xna.Framework; @@ -17,27 +18,19 @@ using NLog; using OpenAPI.WorldGenerator.Generators.Biomes; using OpenAPI.WorldGenerator.Utils; -using static ImGuiNET.ImGui; namespace MiMap.Viewer.DesktopGL.Components { - public enum MapViewMode - { - Region, - Chunk - } - - public class GuiMapViewer : DrawableGameComponent + public partial class GuiMapViewer : DrawableGameComponent { + public const float MinScale = 0.1f; + public const float MaxScale = 8f; private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - private Point _position = Point.Zero; private Rectangle _visibleChunkBounds; private Rectangle _mapBounds; - private float _scale = 1f; - private readonly object _chunkSync = new object(); - private List _regions; - private List _chunks; + private float _scale = 0.5f; + private ConcurrentQueue _pendingRegions; private ConcurrentQueue _pendingChunks; private bool _chunksDirty = false; private Map _map; @@ -51,20 +44,11 @@ public class GuiMapViewer : DrawableGameComponent private VertexBuffer _vertexBuffer; private IndexBuffer _indexBuffer; private ImGuiRenderer _gui; - private readonly RasterizerState _rasterizerState; private Rectangle _bounds; private MouseState _mouseState; - private Point _cursorPosition; - private int[] _cursorBlock = new int[3]; - private BiomeBase _cursorBlockBiome; - private int[] _cursorChunk = new int[2]; - private int[] _cursorRegion = new int[2]; - private int _cursorBlockBiomeId; private SpriteBatch _spriteBatch; - - private MapViewMode _mode = MapViewMode.Chunk; - + public Rectangle Bounds { get => _bounds; @@ -137,19 +121,10 @@ private set public GuiMapViewer(MiMapViewer game, Map map) : base(game) { Map = map; - _regions = new List(); - _chunks = new List(); + _pendingRegions = new ConcurrentQueue(); _pendingChunks = new ConcurrentQueue(); _regionMeshManager = game.RegionMeshManager; _gui = game.ImGuiRenderer; - _rasterizerState = new RasterizerState() - { - CullMode = CullMode.None, - FillMode = FillMode.Solid, - ScissorTestEnable = true, - DepthClipEnable = false, - MultiSampleAntiAlias = true - }; } public override void Initialize() @@ -157,17 +132,15 @@ public override void Initialize() base.Initialize(); _spriteBatch = new SpriteBatch(GraphicsDevice); - _effect = new BasicEffect(GraphicsDevice) + _spriteBatchEffect = new BasicEffect(GraphicsDevice) { - LightingEnabled = false, - VertexColorEnabled = false, TextureEnabled = true, - FogEnabled = false + VertexColorEnabled = true }; - _spriteBatchEffect = new BasicEffect(GraphicsDevice) + _effect = new BasicEffect(GraphicsDevice) { TextureEnabled = true, - VertexColorEnabled = true + VertexColorEnabled = false }; Game.GraphicsDevice.DeviceReset += (s, o) => UpdateBounds(); @@ -188,7 +161,7 @@ public override void Initialize() _indexBuffer.SetData(new ushort[] { 0, 1, 2, - 2, 1, 3 + 1, 3, 2 }); } @@ -201,32 +174,25 @@ public override void Update(GameTime gameTime) if (_chunksDirty) { - if (_mode == MapViewMode.Region) - { - var regions = Map.GetRegions(_mapBounds); - lock (_chunkSync) - { - _regions.Clear(); - _regions.AddRange(regions.Where(r => r.IsComplete).Select(r => _regionMeshManager.CacheRegion(r))); - } - } - else if (_mode == MapViewMode.Chunk) - { - Map.EnqueueChunks(_mapBounds); // generate the visible chunks - } + Map.EnqueueChunks(_mapBounds); // generate the visible chunks _chunksDirty = false; } - if (_mode == MapViewMode.Chunk) + if (!_pendingChunks.IsEmpty || !_pendingRegions.IsEmpty) { - if (!_pendingChunks.IsEmpty) + var sw = Stopwatch.StartNew(); + MapRegion r; + while (_pendingRegions.TryDequeue(out r) && sw.ElapsedMilliseconds < 36f) { - MapChunk c; - while (_pendingChunks.TryDequeue(out c)) - { - _chunks.Add(_regionMeshManager.CacheChunk(c)); - } + //_regions.Add(_regionMeshManager.CacheRegion(r)); + } + + MapChunk c; + while (_pendingChunks.TryDequeue(out c) && sw.ElapsedMilliseconds < 50f) + { + _regionMeshManager.CacheRegionChunk(c); +// _chunks.Add(_regionMeshManager.CacheChunk(c)); } } } @@ -249,305 +215,44 @@ public override void Draw(GameTime gameTime) _gui.AfterLayout(); } - private void DrawImGui() - { - try - { - if (Begin("Map Viewer")) - { - if (BeginTable("mapviewtable", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) - { - _mapPositions[0] = _mapPosition.X; - _mapPositions[1] = _mapPosition.Y; - TableNextRow(); - TableNextColumn(); - Text("Map Position"); - TableNextColumn(); - InputInt2("##value", ref _mapPositions[0]); - if (IsItemEdited()) - { - MapPosition = new Point(_mapPositions[0], _mapPositions[1]); - } - - var bounds = MapBounds; - var boundsValues = new int[] { bounds.X, bounds.Y, bounds.Width, bounds.Height }; - - TableNextRow(); - TableNextColumn(); - Text("Map Bounds"); - TableNextColumn(); - InputInt4("##value", ref boundsValues[0], ImGuiInputTextFlags.ReadOnly); - - TableNextRow(); - TableNextColumn(); - Text("Scale"); - TableNextColumn(); - SliderFloat("##value", ref _scale, 0.01f, 8f); - if (IsItemEdited()) - { - RecalculateTransform(); - } - - TableNextRow(); - - // PopID(); - EndTable(); - } - - Spacing(); - - if (BeginTable("mapviewtable", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) - { - var cursorPositionValues = new int[] - { - _cursorPosition.X, - _cursorPosition.Y - }; - TableNextRow(); - TableNextColumn(); - Text("Cursor Position"); - TableNextColumn(); - InputInt2("##value", ref cursorPositionValues[0], ImGuiInputTextFlags.ReadOnly); - - - EndTable(); - } - - SetNextItemOpen(true, ImGuiCond.FirstUseEver); - if (TreeNodeEx("Graphics")) - { - if (BeginTable("graphics", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit)) - { - TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); - TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); - - var v = GraphicsDevice.Viewport; - var viewportValues = new int[] - { - v.X, - v.Y, - v.Width, - v.Height - }; - - TableNextRow(); - TableNextColumn(); - Text("Viewport"); - TableNextColumn(); - InputInt4("##value", ref viewportValues[0], ImGuiInputTextFlags.ReadOnly); - - EndTable(); - } - - TreePop(); - } - - SetNextItemOpen(true, ImGuiCond.FirstUseEver); - if (TreeNodeEx("Window")) - { - if (BeginTable("window", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit)) - { - TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed); - TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); - - var p = Game.Window.Position; - var windowPositionValues = new int[] - { - p.X, - p.Y - }; - TableNextRow(); - TableNextColumn(); - Text("Position"); - TableNextColumn(); - InputInt2("##value", ref windowPositionValues[0], ImGuiInputTextFlags.ReadOnly); - - - var c = Game.Window.ClientBounds; - var windowClientBoundsValues = new int[] - { - c.X, - c.Y, - c.Width, - c.Height - }; - TableNextRow(); - TableNextColumn(); - Text("Client Bounds"); - TableNextColumn(); - InputInt4("##value", ref windowClientBoundsValues[0], ImGuiInputTextFlags.ReadOnly); - - - EndTable(); - } - - TreePop(); - } - - End(); - } - - if (Begin("Info")) - { - Text("At Cursor"); - InputInt3("Block", ref _cursorBlock[0], ImGuiInputTextFlags.ReadOnly); - InputInt2("Chunk", ref _cursorChunk[0], ImGuiInputTextFlags.ReadOnly); - InputInt2("Region", ref _cursorRegion[0], ImGuiInputTextFlags.ReadOnly); - - SetNextItemOpen(true, ImGuiCond.FirstUseEver); - if (TreeNode("Biome Info")) - { - var biome = _cursorBlockBiome; - - var biomeId = biome?.Id ?? _cursorBlockBiomeId; - var biomeName = biome?.Name ?? string.Empty; - var biomeDefinitionName = biome?.DefinitionName ?? string.Empty; - var biomeMinHeight = biome?.MinHeight ?? 0; - var biomeMaxHeight = biome?.MaxHeight ?? 0; - var biomeTemperature = biome?.Temperature ?? 0; - var biomeDownfall = biome?.Downfall ?? 0; - - InputInt("ID", ref biomeId, 0, 0, ImGuiInputTextFlags.ReadOnly); - InputText("Name", ref biomeName, 0, ImGuiInputTextFlags.ReadOnly); - InputText("Definition Name", ref biomeDefinitionName, 0, ImGuiInputTextFlags.ReadOnly); - InputFloat("Min Height", ref biomeMinHeight, 0, 0, null, ImGuiInputTextFlags.ReadOnly); - InputFloat("Max Height", ref biomeMaxHeight, 0, 0, null, ImGuiInputTextFlags.ReadOnly); - InputFloat("Temperature", ref biomeTemperature, 0, 0, null, ImGuiInputTextFlags.ReadOnly); - InputFloat("Downfall", ref biomeDownfall, 0, 0, null, ImGuiInputTextFlags.ReadOnly); - - SetNextItemOpen(true, ImGuiCond.FirstUseEver); - if (TreeNode("Config")) - { - var cfg = biome?.Config; - BeginDisabled(); - - var cfgIsEdgeBiome = cfg?.IsEdgeBiome ?? false; - var cfgAllowRivers = cfg?.AllowRivers ?? false; - var cfgAllowScenicLakes = cfg?.AllowScenicLakes ?? false; - var cfgSurfaceBlendIn = cfg?.SurfaceBlendIn ?? false; - var cfgSurfaceBlendOut = cfg?.SurfaceBlendOut ?? false; - var cfgWeight = cfg?.Weight ?? 0; - - Checkbox("Is Edge Biome", ref cfgIsEdgeBiome); - Checkbox("Allow Rivers", ref cfgAllowRivers); - Checkbox("Allow Scenic Lakes", ref cfgAllowScenicLakes); - Checkbox("Surface Blend In", ref cfgSurfaceBlendIn); - Checkbox("Surface Blend Out", ref cfgSurfaceBlendOut); - InputInt("Weight", ref cfgWeight); - - EndDisabled(); - - TreePop(); - } - - - TreePop(); - } - - End(); - } - - if (Begin("Biome Colors")) - { - if (BeginTable("biomeclr", 3, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingStretchProp)) - { - foreach (var c in Map.BiomeRegistry.Biomes) - { - TableNextRow(); - TableNextColumn(); - Text(c.Id.ToString()); - TableNextColumn(); - Text(c.Name); - TableNextColumn(); - TableSetBgColor(ImGuiTableBgTarget.CellBg, GetColor(c.Color ?? System.Drawing.Color.Transparent)); - Text(" "); - } - - EndTable(); - } - - End(); - } - } - catch (Exception ex) - { - Log.Error(ex, $"Drawing exception."); - } - } - - private static uint GetColor(System.Drawing.Color color) - { -// return 0xFFFF0000; - return (uint)( - 0xFF << 24 - | ((color.B & 0xFF) << 16) - | ((color.G & 0xFF) << 8) - | ((color.R & 0xFF) << 0) - ); - // return (uint)( - // ((color.G & 0xFF) << 24) - // | ((color.B & 0xFF) << 16) - // | ((color.R & 0xFF) << 8) - // | 0xFF - // ); - } - private void DrawMap(GameTime gameTime) { - if (_mode == MapViewMode.Region) DrawMap_Region(gameTime); - else if (_mode == MapViewMode.Chunk) - DrawMap_Chunk(gameTime); } private static readonly Point RegionSize = new Point(512, 512); private static readonly Rectangle ChunkSize = new Rectangle(0, 0, 16, 16); private int[] _mapPositions = new int[2]; - private void DrawMap_Chunk(GameTime gameTime) - { - _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone, _spriteBatchEffect, Transform); - - foreach (var chunk in _chunks) - { - _spriteBatch.Draw(chunk.Texture, chunk.Position.ToVector2(), Color.White); - // _spriteBatch.Draw(chunk.Texture, chunk.Position.ToVector2(), ChunkSize, Color.White, 0, Vector2.Zero, _scale,SpriteEffects.FlipVertically, 0); - } - - _spriteBatch.DrawRectangle(new Rectangle(_cursorBlock[0], _cursorBlock[2], 1, 1), Color.White); - _spriteBatch.DrawRectangle(new Rectangle(_cursorChunk[0] << 4, _cursorChunk[1] << 4, 16, 16), Color.Yellow); - _spriteBatch.DrawRectangle(new Rectangle(_cursorRegion[0] << 9, _cursorRegion[1] << 9, 512, 512), Color.PaleGreen); - - _spriteBatch.End(); - } + // private void DrawMap_Chunk(GameTime gameTime) + // { + // _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone, _spriteBatchEffect, Transform); + // + // foreach (var chunk in _chunks) + // { + // _spriteBatch.Draw(chunk.Texture, chunk.Position.ToVector2(), Color.White); + // // _spriteBatch.Draw(chunk.Texture, chunk.Position.ToVector2(), ChunkSize, Color.White, 0, Vector2.Zero, _scale,SpriteEffects.FlipVertically, 0); + // } + // + // _spriteBatch.DrawRectangle(new Rectangle(_cursorBlock[0], _cursorBlock[2], 1, 1), Color.White); + // _spriteBatch.DrawRectangle(new Rectangle(_cursorChunk[0] << 4, _cursorChunk[1] << 4, 16, 16), Color.Yellow); + // _spriteBatch.DrawRectangle(new Rectangle(_cursorRegion[0] << 9, _cursorRegion[1] << 9, 512, 512), Color.PaleGreen); + // + // _spriteBatch.End(); + // } private void DrawMap_Region(GameTime gameTime) { - using (var cxt = GraphicsContext.CreateContext(GraphicsDevice, BlendState.AlphaBlend, DepthStencilState.None, _rasterizerState, SamplerState.PointClamp)) + using (var cxt = GraphicsContext.CreateContext(GraphicsDevice, BlendState.AlphaBlend, DepthStencilState.None, RasterizerState.CullNone, SamplerState.LinearClamp)) { - cxt.ScissorRectangle = Bounds; - var scaleMatrix = - Matrix.Identity - * Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f) - * Matrix.CreateScale(_scale, _scale, 1f) - ; - foreach (var region in _regions) + foreach (var region in _regionMeshManager.Regions) { cxt.GraphicsDevice.SetVertexBuffer(_vertexBuffer); cxt.GraphicsDevice.Indices = _indexBuffer; _effect.World = region.World * Transform; _effect.Texture = region.Texture; - - if (_cursorRegion[0] == region.X && _cursorRegion[1] == region.Z) - { - _effect.DiffuseColor = Color.SteelBlue.ToVector3(); - } - else - { - _effect.DiffuseColor = Color.White.ToVector3(); - } - + foreach (var p in _effect.CurrentTechnique.Passes) { p.Apply(); @@ -555,6 +260,17 @@ private void DrawMap_Region(GameTime gameTime) } } } + + _spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone, _spriteBatchEffect, Transform); + + _spriteBatch.DrawRectangle(new Rectangle(_cursorBlock[0], _cursorBlock[2], 1, 1), Color.White); + _spriteBatch.DrawRectangle(new Rectangle(_cursorChunk[0] << 4, _cursorChunk[1] << 4, 16, 16), Color.Yellow); + _spriteBatch.DrawRectangle(new Rectangle(_cursorRegion[0] << 9, _cursorRegion[1] << 9, 512, 512), Color.PaleGreen); + + _spriteBatch.DrawLine(1f, new Vector2(_mapBounds.X, _cursorBlock[2]), new Vector2(_mapBounds.X + _mapBounds.Width, _cursorBlock[2]), Color.DarkSlateGray * 0.65f); + _spriteBatch.DrawLine(1f, new Vector2(_cursorBlock[0], _mapBounds.Y), new Vector2(_cursorBlock[0], _mapBounds.Y + _mapBounds.Height), Color.DarkSlateGray * 0.65f); + + _spriteBatch.End(); } #endregion @@ -573,6 +289,9 @@ private void UpdateBounds() private void RecalculateTransform() { + if (_scale < MinScale) _scale = MinScale; + if (_scale > MaxScale) _scale = MaxScale; + var p = MapPosition; Transform = Matrix.Identity // * Matrix.CreateRotationX(MathHelper.Pi) @@ -594,20 +313,18 @@ private void RecalculateMapBounds() var bbSize = Unproject(_bounds.Size) - bbMin; MapBounds = new Rectangle(bbMin, bbSize); - + if (_effect != default) { - _effect.Projection = Matrix.CreateOrthographic(screenBounds.Width, screenBounds.Height, 0.1f, 100f); - - _effect.View = Matrix.CreateLookAt(new Vector3(0, 0, 10), new Vector3(0, 0, 0), Vector3.Up); - _effect.World = Matrix.CreateWorld(Vector3.Zero, Vector3.Forward, Vector3.Up); + _effect.World = Transform; + _effect.View = Matrix.CreateLookAt(Vector3.Backward, Vector3.Zero, Vector3.Up); + _effect.Projection = Matrix.CreateOrthographicOffCenter(0, screenBounds.Width, screenBounds.Height, 0, 0f, 1f); } - + if (_spriteBatchEffect != default) { _spriteBatchEffect.World = Transform; _spriteBatchEffect.View = Matrix.CreateLookAt(Vector3.Backward, Vector3.Zero, Vector3.Up); - // _spriteBatchEffect.Projection = Matrix.CreateOrthographicOffCenter(0, screenSize.X / _scale, 0, screenSize.Y / _scale, 0.1f, 100f); _spriteBatchEffect.Projection = Matrix.CreateOrthographicOffCenter(0, screenBounds.Width, screenBounds.Height, 0, 0f, 1f); } } @@ -655,7 +372,7 @@ private void OnChunkGenerated(object sender, Point chunkPosition) private void UpdateMouseInput() { - if (GetIO().WantCaptureMouse) + if (ImGui.GetIO().WantCaptureMouse) return; var newState = Mouse.GetState(); @@ -670,7 +387,10 @@ private void UpdateMouseInput() var currPos = new Point(newState.X, newState.Y); var prevPos = new Point(_mouseState.X, _mouseState.Y); - OnCursorMove(currPos, prevPos, _mouseState.LeftButton == ButtonState.Pressed); + var isPressed = _mouseState.LeftButton == ButtonState.Pressed; + + OnCursorMove_ImGui(currPos, prevPos, isPressed); + OnCursorMove(currPos, prevPos, isPressed); _cursorPosition = currPos; } @@ -680,37 +400,14 @@ private void UpdateMouseInput() private Point Unproject(Point cursor) { return (cursor.ToVector2() / _scale).ToPoint() + _mapPosition; - -// return Vector2.Transform(cursor, InverseTransform); - // return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Matrix.CreateTranslation(-_mapPosition.X, -_mapPosition.Y, 0f)); - //return GraphicsDevice.Viewport.Unproject(new Vector3(cursor.X, cursor.Y, 0f), _effect.Projection, _effect.View, Transform); } private void OnCursorMove(Point cursorPosition, Point previousCursorPosition, bool isCursorDown) { - var cursorBlockPos = Unproject(cursorPosition); - // var cursorBlockPos = Vector3.Transform(new Vector3(cursorPosition.X, cursorPosition.Y, 0f), Transform*_effect.View); - _cursorBlock[0] = cursorBlockPos.X; - _cursorBlock[2] = cursorBlockPos.Y; - _cursorChunk[0] = _cursorBlock[0] >> 4; - _cursorChunk[1] = _cursorBlock[2] >> 4; - _cursorRegion[0] = _cursorBlock[0] >> 9; - _cursorRegion[1] = _cursorBlock[2] >> 9; - - var cursorBlockRegion = Map.GetRegion(new Point(_cursorRegion[0], _cursorRegion[1])); - if (cursorBlockRegion?.IsComplete ?? false) - { - var cursorBlockChunk = cursorBlockRegion[_cursorChunk[0] % 32, _cursorChunk[1] % 32]; - var x = _cursorBlock[0] % 16; - var z = _cursorBlock[2] % 16; - _cursorBlock[1] = (int)cursorBlockChunk.GetHeight(x, z); - _cursorBlockBiomeId = (int)cursorBlockChunk.GetBiome(x, z); - _cursorBlockBiome = Map.BiomeRegistry.GetBiome(_cursorBlockBiomeId); - } if (isCursorDown) { - var p = (cursorPosition - previousCursorPosition); + var p = ((cursorPosition - previousCursorPosition).ToVector2() / _scale).ToPoint(); MapPosition += new Point(-p.X, -p.Y); } } diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs index 8b320c0..254e830 100644 --- a/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs +++ b/src/MiMap.Viewer.DesktopGL/Graphics/CachedRegionMesh.cs @@ -24,13 +24,20 @@ public class CachedRegionMesh : ITile, IDisposable public Texture2D Texture { get; } public Matrix World { get; } - - public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) + + public CachedRegionMesh(GraphicsDevice graphics, int x, int z) { - X = region.X; - Z = region.Z; + X = x; + Z = z; Position = new Point(X << 9, Z << 9); var t = new Texture2D(graphics, 1 << 9, 1 << 9); + + Texture = t; + World = Matrix.CreateWorld(new Vector3(Position.X, Position.Y, 0f), Vector3.Forward, Vector3.Up); + } + + public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) : this(graphics, region.X, region.Z) + { var d = new Color[(1 << 9) * (1 << 9)]; int x, y, z; @@ -41,16 +48,19 @@ public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) for (int cz = 0; cz < 32; cz++) { var chunk = region[cx, cz]; - for (int bx = 0; bx < 16; bx++) - for (int bz = 0; bz < 16; bz++) + if (chunk != default) { - x = (cx << 4) + bx; - z = (cz << 4) + bz; - b = chunk.GetBiome(bx, bz); - y = chunk.GetHeight(bx, bz); - c = chunk.GetColor(bx, bz); + for (int bx = 0; bx < 16; bx++) + for (int bz = 0; bz < 16; bz++) + { + x = (cx << 4) + bx; + z = (cz << 4) + bz; + b = chunk.GetBiome(bx, bz); + y = chunk.GetHeight(bx, bz); + c = chunk.GetColor(bx, bz); - SetData(d, x, z, c); + SetData(d, x, z, c); + } } } @@ -61,9 +71,34 @@ public CachedRegionMesh(GraphicsDevice graphics, MapRegion region) SetData(d, j, i, Color.Blue); } - t.SetData(d); - Texture = t; - World = Matrix.CreateWorld(new Vector3((region.X << 9), (region.Z << 9), 0f), Vector3.Forward, Vector3.Up); + Texture.SetData(d); + } + + public void UpdateChunk(MapChunk chunk) + { + if (chunk == default) + return; + + var cx = chunk.X & 0x1F; + var cz = chunk.Z & 0x1F; + + int x, y, z; + byte b; + Color c; + var d = new Color[(1 << 4) * (1 << 4)]; + + for (int bx = 0; bx < 16; bx++) + for (int bz = 0; bz < 16; bz++) + { + b = chunk.GetBiome(bx, bz); + y = chunk.GetHeight(bx, bz); + c = chunk.GetColor(bx, bz); + + d[(bz * 16) + bx] = c; + } + + var r = new Rectangle(cx << 4, cz << 4, 16, 16); + Texture.SetData(0, r, d, 0, 16*16); } public void Dispose() diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs b/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs index 52eacb0..99d2cf9 100644 --- a/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs +++ b/src/MiMap.Viewer.DesktopGL/Graphics/RegionMeshManager.cs @@ -8,7 +8,9 @@ namespace MiMap.Viewer.DesktopGL.Graphics { public interface IRegionMeshManager : IDisposable { + ICollection Regions { get; } CachedRegionMesh CacheRegion(MapRegion region); + void CacheRegionChunk(MapChunk chunk); CachedChunkMesh CacheChunk(MapChunk chunk); } @@ -19,6 +21,8 @@ public class RegionMeshManager : IRegionMeshManager private IDictionary _regionCache = new Dictionary(); private IDictionary _chunkCache = new Dictionary(); + public ICollection Regions => _regionCache.Values; + public RegionMeshManager(GraphicsDevice graphics) { _graphics = graphics; @@ -33,12 +37,27 @@ public CachedRegionMesh CacheRegion(MapRegion region) _regionCache[i] = cached; return cached; } + + public void CacheRegionChunk(MapChunk chunk) + { + var i = new Point(chunk.X >> 5, chunk.Z >> 5); + + CachedRegionMesh cachedRegion; + if (!_regionCache.TryGetValue(i, out cachedRegion)) + { + cachedRegion = new CachedRegionMesh(_graphics, i.X, i.Y); + _regionCache[i] = cachedRegion; + } + + cachedRegion.UpdateChunk(chunk); + } public CachedChunkMesh CacheChunk(MapChunk chunk) { var i = new Point(chunk.X, chunk.Z); CachedChunkMesh cached; - if (_chunkCache.TryGetValue(i, out cached)) return cached; + if (_chunkCache.TryGetValue(i, out cached)) + return cached; cached = new CachedChunkMesh(_graphics, chunk); _chunkCache[i] = cached; return cached; diff --git a/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs b/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs index aba9199..ce7bf81 100644 --- a/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs +++ b/src/MiMap.Viewer.DesktopGL/Graphics/SpriteBatchExtensions.cs @@ -23,6 +23,15 @@ public static void Dispose() /// + /// Draw a line between the two supplied points. + /// + /// Starting point. + /// End point. + /// The draw color. + public static void DrawLine(this SpriteBatch sb, float thickness, Vector2 start, Vector2 end, Color color) + => DrawLine(sb, thickness, start, end, color, Vector2.One, 0); + + /// /// Draw a line between the two supplied points. /// /// Starting point. diff --git a/src/MiMap.Viewer.DesktopGL/Models/Map.cs b/src/MiMap.Viewer.DesktopGL/Models/Map.cs index 81af1af..42ceb2f 100644 --- a/src/MiMap.Viewer.DesktopGL/Models/Map.cs +++ b/src/MiMap.Viewer.DesktopGL/Models/Map.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; +using MiMap.Viewer.DesktopGL.Utilities; using MiNET.Utils.Vectors; using MiNET.Worlds; using NLog; @@ -64,6 +65,7 @@ public void Run() var sw = Stopwatch.StartNew(); Regions[r] = new MapRegion(r.X, r.Y); _regionsToGenerate.TryDequeue(out _); + var chunkGenTimes = new float[32 * 32]; Parallel.For(0, 32 * 32, new ParallelOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount / 2, @@ -81,11 +83,15 @@ public void Run() csw.Restart(); Regions[r][cx, cz] = ExtractChunkData(chunk); csw.Stop(); - //Log.Info($"Generated Chunk: {chunkPosition.X:000}, {chunkPosition.Z:000} in {t1:F2}ms (extract data in {csw.ElapsedMilliseconds:F2}ms)"); + chunkGenTimes[(cx * 32) + cz] = t1; + Log.Debug($"Completed Chunk {chunkPosition.X:000}, {chunkPosition.Z:000} in {t1:N3} ms (generation: {t1:N3} ms, dataExtraction: {csw.ElapsedMilliseconds:N3} ms)"); ChunkGenerated?.Invoke(this, new Point(chunkPosition.X, chunkPosition.Z)); }); sw.Stop(); - Log.Info($"Generated Region: {r.X:000}, {r.Y:000} in {sw.ElapsedMilliseconds:F2}ms"); + var ctMin = chunkGenTimes.Min(); + var ctMax = chunkGenTimes.Max(); + var ctAvg = chunkGenTimes.Average(); + Log.Info($"Generated Region: {r.X:000}, {r.Y:000} in {sw.ElapsedMilliseconds:N3} ms (ChunkGen: min = {ctMin:N3}, max = {ctMax:N3}, avg = {ctAvg:N3})"); Regions[r].IsComplete = true; RegionGenerated?.Invoke(this, r); } @@ -162,13 +168,13 @@ public IEnumerable GetRegions(Rectangle blockBounds) public void EnqueueChunks(Rectangle blockBounds) { + _regionsToGenerate.Clear(); + var regionMin = new Point(blockBounds.X >> 9, blockBounds.Y >> 9 ); var regionMax = new Point((blockBounds.X + blockBounds.Width) >> 9, (blockBounds.Y + blockBounds.Height) >> 9); - for (int rx = regionMin.X; rx <= regionMax.X; rx++) - for (int rz = regionMin.Y; rz <= regionMax.Y; rz++) + foreach(var p in Spiral.FillRegionFromCenter(new Rectangle(regionMin - new Point(1,1), (regionMax - regionMin) + new Point(2,2)))) { - var p = new Point(rx, rz); if (!Regions.ContainsKey(p)) { EnqueueRegion(p); diff --git a/src/MiMap.Viewer.DesktopGL/Utilities/Spiral.cs b/src/MiMap.Viewer.DesktopGL/Utilities/Spiral.cs new file mode 100644 index 0000000..2b18ee4 --- /dev/null +++ b/src/MiMap.Viewer.DesktopGL/Utilities/Spiral.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MiMap.Viewer.DesktopGL.Utilities +{ + public static class Spiral + { + + public static IEnumerable FillRegionFromCenter(Rectangle region) + { + var cX = region.Center.X; + var cY = region.Center.Y; + var r = (int)Math.Max(region.Width, region.Height); + + var x = 0; + var y = 0; + var t = r; + var dx = 0; + var dy = -1; + var p = Point.Zero; + + for (var i = 0; i < (r * r); i++) + { + if ((-r / 2 <= x) && (x <= r / 2) && (-r / 2 <= y) && (y <= r / 2)) + { + p = new Point(cX + x, cY + y); + if (region.RegionContainsInclusive(p)) + yield return p; + } + + if ((x == y) || ((x < 0) && (x == -y)) || ((x > 0) && (x == 1 - y))) + { + t = dx; + dx = -dy; + dy = t; + } + + x += dx; + y += dy; + } + } + + private static bool RegionContainsInclusive(this Rectangle region, Point point) + => point.X >= region.X && point.X <= (region.X + region.Width) && + point.Y >= region.Y && point.Y <= (region.Y + region.Height); + + } +} \ No newline at end of file From ab6c13ee49c32831af04c0e5d76cc95431481726 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 18:34:56 +0200 Subject: [PATCH 11/13] always optimize worldgen for max performance --- src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj b/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj index 618e93c..e1acddb 100644 --- a/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj +++ b/src/OpenAPI.WorldGenerator/OpenAPI.WorldGenerator.csproj @@ -5,6 +5,11 @@ 10 + + + true + + From f61ed39e20208a9484e0ce6cb246b990249ee8a0 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Mon, 30 May 2022 20:30:56 +0200 Subject: [PATCH 12/13] increase minscale --- src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs index 0b36822..11ca130 100644 --- a/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs +++ b/src/MiMap.Viewer.DesktopGL/Components/GuiMapViewer.cs @@ -23,7 +23,7 @@ namespace MiMap.Viewer.DesktopGL.Components { public partial class GuiMapViewer : DrawableGameComponent { - public const float MinScale = 0.1f; + public const float MinScale = 0.2f; public const float MaxScale = 8f; private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); From 31d8bb4468f7e32347f34a3c90cf8840993f9536 Mon Sep 17 00:00:00 2001 From: Dan Spiteri Date: Wed, 1 Jun 2022 01:56:48 +0200 Subject: [PATCH 13/13] Port viewer to veldrid (using ElementEngine) --- .gitmodules | 3 + src/MiMap.Viewer.Element/Game.cs | 57 ++++ .../MiMap.Viewer.Element.csproj | 18 ++ .../MiMapTiles/MiMapTilesRenderer.cs | 38 +++ .../MiMapTiles/MiMapTilesRendererChunk.cs | 47 ++++ .../MiMapTiles/MiMapTilesWorld.cs | 255 ++++++++++++++++++ .../MiMapTiles/MiMapTilesWorldChunk.cs | 51 ++++ .../MiMapTiles/MiMapTilesWorldChunkData.cs | 11 + .../MiMapTiles/MiMapTilesWorldData.cs | 16 ++ .../MiMapTiles/MiMapTilesWorldLayer.cs | 44 +++ .../MiMapTiles/MiMapTilesWorldLayerData.cs | 8 + src/MiMap.Viewer.Element/Program.cs | 11 + .../Utilities/MathUtil.cs | 41 +++ .../Utilities/RectangleExtensions.cs | 12 + src/MiMap.Viewer.Element/Utilities/Spiral.cs | 53 ++++ src/WorldGenerator.sln | 19 ++ src/submodules/ElementEngine | 1 + 17 files changed, 685 insertions(+) create mode 100644 .gitmodules create mode 100644 src/MiMap.Viewer.Element/Game.cs create mode 100644 src/MiMap.Viewer.Element/MiMap.Viewer.Element.csproj create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRenderer.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRendererChunk.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorld.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunk.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunkData.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldData.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayer.cs create mode 100644 src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayerData.cs create mode 100644 src/MiMap.Viewer.Element/Program.cs create mode 100644 src/MiMap.Viewer.Element/Utilities/MathUtil.cs create mode 100644 src/MiMap.Viewer.Element/Utilities/RectangleExtensions.cs create mode 100644 src/MiMap.Viewer.Element/Utilities/Spiral.cs create mode 160000 src/submodules/ElementEngine diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f6abc42 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/submodules/ElementEngine"] + path = src/submodules/ElementEngine + url = https://github.com/pandepic/ElementEngine.git diff --git a/src/MiMap.Viewer.Element/Game.cs b/src/MiMap.Viewer.Element/Game.cs new file mode 100644 index 0000000..000523c --- /dev/null +++ b/src/MiMap.Viewer.Element/Game.cs @@ -0,0 +1,57 @@ +using ElementEngine; +using ElementEngine.EndlessTiles; +using ElementEngine.Tiled; +using MiMap.Viewer.Element.MiMapTiles; +using MiNET.Worlds; +using OpenAPI.WorldGenerator.Generators; +using Veldrid; + +namespace MiMap.Viewer.Element +{ + public class Game : BaseGame + { + public IWorldGenerator WorldGenerator; + public MiMapTilesWorld TilesWorld; + public Texture2D TilesTexture; + public MiMapTilesRenderer TilesRenderer; + public Camera2D BackgroundCamera; + + public override void Load() + { + SettingsManager.LoadFromPath("Settings.xml"); + + var windowRect = new ElementEngine.Rectangle() + { + X = SettingsManager.GetSetting("Window", "X"), + Y = SettingsManager.GetSetting("Window", "Y"), + Width = SettingsManager.GetSetting("Window", "Width"), + Height = SettingsManager.GetSetting("Window", "Height") + }; + + var graphicsBackend = GraphicsBackend.Vulkan; + +#if OPENGL + graphicsBackend = GraphicsBackend.OpenGL; +#endif + + SetupWindow(windowRect, "Captain Shostakovich", graphicsBackend); + SetupAssets(); + + ClearColor = RgbaFloat.Black; + + InputManager.LoadGameControls(); + + WorldGenerator = new OverworldGeneratorV2(); + + TilesWorld = new MiMapTilesWorld(WorldGenerator); + + //todo: TilesTexture + TilesRenderer = new MiMapTilesRenderer(TilesWorld, TilesTexture); + + BackgroundCamera = new Camera2D(new ElementEngine.Rectangle(0, 0, ElementGlobals.TargetResolutionWidth, ElementGlobals.TargetResolutionHeight)) + { + Zoom = 3 + }; + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMap.Viewer.Element.csproj b/src/MiMap.Viewer.Element/MiMap.Viewer.Element.csproj new file mode 100644 index 0000000..12b8f2a --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMap.Viewer.Element.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + + + + + + + + + + + + diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRenderer.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRenderer.cs new file mode 100644 index 0000000..c3cd504 --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRenderer.cs @@ -0,0 +1,38 @@ +using ElementEngine; + +namespace MiMap.Viewer.Element.MiMapTiles +{ + // MiMapTilesRendererChunk + + public class MiMapTilesRenderer + { + public MiMapTilesWorld World { get; set; } + public Texture2D Tilesheet { get; set; } + public Dictionary Chunks { get; set; } = new Dictionary(); + + public MiMapTilesRenderer(MiMapTilesWorld world, Texture2D tilesheet) + { + World = world; + Tilesheet = tilesheet; + + foreach (var (_, chunk) in world.Chunks) + Chunks.Add(chunk.Position, new MiMapTilesRendererChunk(chunk, tilesheet)); + } + + public void Update(GameTimer gameTimer) + { + foreach (var (_, chunk) in Chunks) + chunk.TileBatch.Update(gameTimer); + } + + public void DrawLayers(int start, int end, Camera2D camera) + { + foreach (var (pos, chunk) in Chunks) + { + if (camera.ScaledView.Intersects(chunk.ChunkRect)) + chunk.TileBatch.DrawLayers(start, end, camera, chunk.ChunkRect.LocationF); + } + } // DrawLayers + + } // MiMapTilesRenderer +} diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRendererChunk.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRendererChunk.cs new file mode 100644 index 0000000..1dbda45 --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesRendererChunk.cs @@ -0,0 +1,47 @@ +using ElementEngine; + +namespace MiMap.Viewer.Element.MiMapTiles +{ + public class MiMapTilesRendererChunk + { + public MiMapTilesWorldChunk ChunkData { get; set; } + public Rectangle ChunkRect; + public TileBatch2D TileBatch { get; set; } + + public MiMapTilesRendererChunk(MiMapTilesWorldChunk chunkData, Texture2D tilesheet) + { + ChunkData = chunkData; + ChunkRect = new Rectangle( + ChunkData.Position * (ChunkData.World.TileSize * ChunkData.World.ChunkSize), + ChunkData.World.TileSize * ChunkData.World.ChunkSize); + + TileBatch = new TileBatch2D(ChunkData.World.ChunkSize.X, ChunkData.World.ChunkSize.Y, ChunkData.World.TileSize.X, ChunkData.World.TileSize.Y, tilesheet, TileBatch2DWrapMode.None, ChunkData.World.TileAnimations); + + TileBatch.BeginBuild(); + + foreach (var layer in ChunkData.Layers) + BuildTilebatchLayer(layer); + + TileBatch.EndBuild(); + } + + public void BuildTilebatchLayer(MiMapTilesWorldLayer layer) + { + for (int y = 0; y < ChunkData.World.ChunkSize.Y; y++) + { + for (int x = 0; x < ChunkData.World.ChunkSize.X; x++) + { + var tileID = layer.Tiles[x + ChunkData.World.ChunkSize.X * y]; + + if (tileID == 0) + continue; + + TileBatch.SetTileAtPosition(x, y, tileID); + } + } + + TileBatch.EndLayer(); + layer.ClearTiles(); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorld.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorld.cs new file mode 100644 index 0000000..97ae9c6 --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorld.cs @@ -0,0 +1,255 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using ElementEngine; +using MiNET.Utils.Vectors; +using MiNET.Worlds; +using NLog; + +namespace MiMap.Viewer.Element.MiMapTiles +{ + public class ConcurrentWorkItemQueue : IDisposable + { + public event EventHandler ItemCompleted; + public event EventHandler ItemStarted; + + private readonly int _threadCount; + private readonly Action _processWorkItem; + private object _chunksSync = new object(); + private Queue _pending; + private HashSet _inProgress; + private HashSet _completed; + + private Thread[] _threads; + private AutoResetEvent _trigger; + private bool _running; + + public ConcurrentWorkItemQueue(Action processWorkItem) : this(Environment.ProcessorCount, processWorkItem) + { + } + + public ConcurrentWorkItemQueue(int threadCount, Action processWorkItem) + { + _threadCount = threadCount; + _processWorkItem = processWorkItem; + _pending = new Queue(); + _inProgress = new HashSet(); + _completed = new HashSet(); + _trigger = new AutoResetEvent(false); + _threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) + { + _threads[i] = new Thread(WorkItemThreadRun) + { + Name = "WorldGenerator", + IsBackground = false + }; + _threads[i].Start(); + } + } + + public void Start() + { + _running = true; + for (int i = 0; i < _threadCount; i++) + { + _threads[i].Start(); + } + } + + public void Stop() + { + _running = false; + _trigger.Set(); + for (int i = 0; i < _threads.Length; i++) + { + _threads[i].Join(); + } + _trigger?.Dispose(); + } + + public bool TryEnqueue(T item) + { + lock (_chunksSync) + { + if (_pending.Contains(item) || _inProgress.Contains(item) || _completed.Contains(item)) + return false; + + _pending.Enqueue(item); + return true; + } + } + + private bool TryDequeue(int timeout, out T item) + { + item = default; + if (!Monitor.TryEnter(_chunksSync, timeout)) + return false; + try + { + if (_pending.TryDequeue(out item)) + { + _inProgress.Add(item); + return true; + } + + return false; + } + finally + { + Monitor.Exit(_chunksSync); + } + } + + private void MarkComplete(T item) + { + lock (_chunksSync) + { + _inProgress.Remove(item); + _completed.Add(item); + } + } + + public void WorkItemThreadRun() + { + while (_running) + { + _trigger.WaitOne(1000); + + while (_running && TryDequeue(50, out var c)) + { + ItemStarted?.Invoke(this, c); + + _processWorkItem.Invoke(c); + + MarkComplete(c); + ItemCompleted?.Invoke(this, c); + } + + } + } + + public void ClearQueue() + { + lock (_chunksSync) + { + _pending.Clear(); + } + } + + public void Reset() + { + lock (_chunksSync) + { + _pending.Clear(); + _inProgress.Clear(); + _completed.Clear(); + } + } + + public void Dispose() + { + Reset(); + Stop(); + _trigger?.Dispose(); + } + } + + public class MiMapTilesWorld : IDisposable + { + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + public IWorldGenerator WorldGenerator { get; } + public const int BLANK_TILE = -1; + + public Vector2I TileSize { get; set; } + public Vector2I ChunkSize { get; set; } + public Dictionary Chunks { get; set; } = new Dictionary(); + public Dictionary TileAnimations { get; set; } = new Dictionary(); + + private ConcurrentWorkItemQueue _workItemQueue; + private ConcurrentBag _newChunks; + + public MiMapTilesWorld(IWorldGenerator worldGenerator) + { + WorldGenerator = worldGenerator; + ChunkSize = new Vector2I(512, 512); + TileSize = new Vector2I(16, 16); + _workItemQueue = new ConcurrentWorkItemQueue(GenerateChunk); + _newChunks = new ConcurrentBag(); + } + + public void EnqueueChunk(Vector2I chunkCoords) + { + if (!_workItemQueue.TryEnqueue(chunkCoords)) + return; + + Log.Info($"Enqueue Chunk: {chunkCoords.X:N2}, {chunkCoords.Y:N2}"); + } + + private void GenerateChunk(Vector2I chunkCoords) + { + Log.Info($"Generating Chunk: {chunkCoords.X:N2}, {chunkCoords.Y:N2}"); + var csw = Stopwatch.StartNew(); + var chunkPosition = new ChunkCoordinates(chunkCoords.X, chunkCoords.Y); + using var chunk = WorldGenerator.GenerateChunkColumn(chunkPosition); + csw.Stop(); + var t1 = csw.ElapsedMilliseconds; + csw.Restart(); + var worldChunk = ExtractWorldChunk(chunkCoords, chunk); + _newChunks.Add(worldChunk); + csw.Stop(); + Log.Debug($"Completed Chunk {chunkPosition.X:N2}, {chunkPosition.Z:N2} in {t1:N3} ms (generation: {t1:N3} ms, dataExtraction: {csw.ElapsedMilliseconds:N3} ms)"); + } + + private MiMapTilesWorldChunk ExtractWorldChunk(Vector2I coords, ChunkColumn chunkColumn) + { + var chunk = new MiMapTilesWorldChunk(coords, this); + + var biomeTiles = new int[chunk.TotalTiles]; + var heightTiles = new int[chunk.TotalTiles]; + // var temperatureTiles = new int[chunk.TotalTiles]; + // var humidityTiles = new int[chunk.TotalTiles]; + + int i = 0; + for (int x = 0; x < 16; x++) + for (int z = 0; z < 16; z++) + { + i = (z << 4) + x; + + biomeTiles[i] = (int)chunkColumn.biomeId[i]; + heightTiles[i] = chunkColumn.height[i]; + } + + chunk.UpdateLayerTiles(MiMapTilesWorldLayerType.Biome, biomeTiles); + chunk.UpdateLayerTiles(MiMapTilesWorldLayerType.Height, heightTiles); + chunk.UpdateLayerTiles(MiMapTilesWorldLayerType.Humidity, biomeTiles); + chunk.UpdateLayerTiles(MiMapTilesWorldLayerType.Temperature, biomeTiles); + + return chunk; + } + + public void Update() + { + while (_newChunks.TryTake(out var chunk)) + { + Chunks.Add(chunk.Position, chunk); + } + } + + public void ClearTiles() + { + foreach (var (_, chunk) in Chunks) + chunk.ClearTiles(); + } + + public void ResetTiles() + { + foreach (var (_, chunk) in Chunks) + chunk.ResetTiles(); + } + + public void Dispose() + { + _workItemQueue.Dispose(); + } + } // EndlessTilesWorld +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunk.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunk.cs new file mode 100644 index 0000000..3bef2d3 --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunk.cs @@ -0,0 +1,51 @@ +using ElementEngine; + +namespace MiMap.Viewer.Element.MiMapTiles +{ + public enum MiMapTilesWorldLayerType : int + { + Biome = 0, + Height = 1, + Temperature = 2, + Humidity = 3 + } + + public class MiMapTilesWorldChunk + { + private static readonly int MiMapTilesWorldLayerTypeCount = Enum.GetNames(typeof(MiMapTilesWorldLayerType)).Length; + + public MiMapTilesWorld World { get; set; } + public Vector2I Position { get; set; } + public MiMapTilesWorldLayer[] Layers { get; } + + public int TotalTiles => World.ChunkSize.X * World.ChunkSize.Y; + + public MiMapTilesWorldChunk(Vector2I position, MiMapTilesWorld world) + { + Position = position; + World = world; + Layers = new MiMapTilesWorldLayer[MiMapTilesWorldLayerTypeCount]; + for (int i = 0; i < Layers.Length; i++) + { + Layers[i] = new MiMapTilesWorldLayer((MiMapTilesWorldLayerType)i, this); + } + } + + public void UpdateLayerTiles(MiMapTilesWorldLayerType type, int[] tiles) + { + Layers[(int)type].ResetTiles(tiles); + } + + public void ClearTiles() + { + foreach (var layer in Layers) + layer.ClearTiles(); + } + + public void ResetTiles() + { + foreach (var layer in Layers) + layer.ResetTiles(); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunkData.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunkData.cs new file mode 100644 index 0000000..6b8d141 --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldChunkData.cs @@ -0,0 +1,11 @@ +using ElementEngine; + +namespace MiMap.Viewer.Element.MiMapTiles +{ + public class MiMapTilesWorldChunkData + { + public Vector2I Position { get; set; } + public List Layers { get; set; } + + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldData.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldData.cs new file mode 100644 index 0000000..f5f5e6e --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldData.cs @@ -0,0 +1,16 @@ +using ElementEngine; + +namespace MiMap.Viewer.Element.MiMapTiles +{ + public class MiMapTilesWorldData + { + public string Name { get; set; } + public string TilesheetPath { get; set; } + public string TilesheetName { get; set; } + public Vector2I TileSize { get; set; } + public Vector2I ChunkSize { get; set; } + + public Dictionary SavedChunks { get; set; } + public Dictionary TileAnimations { get; set; } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayer.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayer.cs new file mode 100644 index 0000000..2c1bd88 --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayer.cs @@ -0,0 +1,44 @@ +namespace MiMap.Viewer.Element.MiMapTiles +{ + public class MiMapTilesWorldLayer + { + public MiMapTilesWorldChunk Chunk { get; } + + public MiMapTilesWorldLayerType Type { get; } + public int[] Tiles { get; set; } + + public bool TilesLoaded => Tiles != null; + + public MiMapTilesWorldLayer(MiMapTilesWorldLayerType type, MiMapTilesWorldChunk chunk) + { + Type = type; + Tiles = null; + Chunk = chunk; + } + public MiMapTilesWorldLayer(MiMapTilesWorldLayerType type, int[] tiles, MiMapTilesWorldChunk chunk) : this(type, chunk) + { + Tiles = tiles; + } + + public void ClearTiles() + { + Tiles = null; + } + + public void ResetTiles(int[] newTiles) + { + if (newTiles.Length != Chunk.TotalTiles) + throw new IndexOutOfRangeException(nameof(newTiles)); + + Tiles = newTiles; + } + + public void ResetTiles() + { + Tiles = new int[Chunk.TotalTiles]; + for (var i = 0; i < Chunk.TotalTiles; i++) + Tiles[i] = MiMapTilesWorld.BLANK_TILE; + } + + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayerData.cs b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayerData.cs new file mode 100644 index 0000000..75c866d --- /dev/null +++ b/src/MiMap.Viewer.Element/MiMapTiles/MiMapTilesWorldLayerData.cs @@ -0,0 +1,8 @@ +namespace MiMap.Viewer.Element.MiMapTiles +{ + public class MiMapTilesWorldLayerData + { + public int Index { get; set; } + public string CompressedTiles { get; set; } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/Program.cs b/src/MiMap.Viewer.Element/Program.cs new file mode 100644 index 0000000..5275247 --- /dev/null +++ b/src/MiMap.Viewer.Element/Program.cs @@ -0,0 +1,11 @@ +namespace MiMap.Viewer.Element +{ + class Program + { + static void Main(string[] args) + { + using (var game = new Game()) + game.Run(); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/Utilities/MathUtil.cs b/src/MiMap.Viewer.Element/Utilities/MathUtil.cs new file mode 100644 index 0000000..cfb3d0f --- /dev/null +++ b/src/MiMap.Viewer.Element/Utilities/MathUtil.cs @@ -0,0 +1,41 @@ +using System.Runtime.CompilerServices; + +namespace MiMap.Viewer.Core.Utilities +{ + public static class MathUtil + { + #region Clamp + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp(this float v, float min, float max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp(this double v, double min, double max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte Clamp(this sbyte v, sbyte min, sbyte max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Clamp(this byte v, byte min, byte max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Clamp(this short v, short min, short max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Clamp(this ushort v, ushort min, ushort max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp(this int v, int min, int max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Clamp(this uint v, uint min, uint max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Clamp(this long v, long min, long max) => v < min ? min : v > max ? max : v; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Clamp(this ulong v, ulong min, ulong max) => v < min ? min : v > max ? max : v; + + #endregion + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/Utilities/RectangleExtensions.cs b/src/MiMap.Viewer.Element/Utilities/RectangleExtensions.cs new file mode 100644 index 0000000..e67423e --- /dev/null +++ b/src/MiMap.Viewer.Element/Utilities/RectangleExtensions.cs @@ -0,0 +1,12 @@ +using Veldrid; + +namespace MiMap.Viewer.Core.Utilities +{ + public static class RectangleExtensions + { + public static Point Center(this Rectangle rectangle) + { + return new Point(rectangle.X + (rectangle.Width / 2), rectangle.Y + (rectangle.Height / 2)); + } + } +} \ No newline at end of file diff --git a/src/MiMap.Viewer.Element/Utilities/Spiral.cs b/src/MiMap.Viewer.Element/Utilities/Spiral.cs new file mode 100644 index 0000000..b53b913 --- /dev/null +++ b/src/MiMap.Viewer.Element/Utilities/Spiral.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using MiMap.Viewer.Core.Utilities; +using Veldrid; + +namespace MiMap.Viewer.DesktopGL.Utilities +{ + public static class Spiral + { + + public static IEnumerable FillRegionFromCenter(Rectangle region) + { + var c = region.Center(); + var cX = c.X; + var cY = c.Y; + var r = (int)Math.Max(region.Width, region.Height); + + var x = 0; + var y = 0; + var t = r; + var dx = 0; + var dy = -1; + var p = new Point(); + + for (var i = 0; i < (r * r); i++) + { + if ((-r / 2 <= x) && (x <= r / 2) && (-r / 2 <= y) && (y <= r / 2)) + { + p = new Point(cX + x, cY + y); + if (region.RegionContainsInclusive(p)) + yield return p; + } + + if ((x == y) || ((x < 0) && (x == -y)) || ((x > 0) && (x == 1 - y))) + { + t = dx; + dx = -dy; + dy = t; + } + + x += dx; + y += dy; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool RegionContainsInclusive(this Rectangle region, Point point) + => point.X >= region.X && point.X <= (region.X + region.Width) && + point.Y >= region.Y && point.Y <= (region.Y + region.Height); + + } +} \ No newline at end of file diff --git a/src/WorldGenerator.sln b/src/WorldGenerator.sln index aaf85e9..cdab117 100644 --- a/src/WorldGenerator.sln +++ b/src/WorldGenerator.sln @@ -6,6 +6,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldGenerator.Tweaking", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiMap.Viewer.DesktopGL", "MiMap.Viewer.DesktopGL\MiMap.Viewer.DesktopGL.csproj", "{EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MiMap.Viewer", "MiMap.Viewer", "{DB562C1B-F240-4832-8355-F6B3AA19608D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiMap.Viewer.Element", "MiMap.Viewer.Element\MiMap.Viewer.Element.csproj", "{6568E523-7999-4751-926D-DAF38F3EA62D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElementEngine", "submodules\ElementEngine\ElementEngine.csproj", "{7EC00713-86EC-4E58-8AC7-3904B830557F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +30,18 @@ Global {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163}.Release|Any CPU.Build.0 = Release|Any CPU + {6568E523-7999-4751-926D-DAF38F3EA62D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568E523-7999-4751-926D-DAF38F3EA62D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568E523-7999-4751-926D-DAF38F3EA62D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568E523-7999-4751-926D-DAF38F3EA62D}.Release|Any CPU.Build.0 = Release|Any CPU + {7EC00713-86EC-4E58-8AC7-3904B830557F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EC00713-86EC-4E58-8AC7-3904B830557F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EC00713-86EC-4E58-8AC7-3904B830557F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EC00713-86EC-4E58-8AC7-3904B830557F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EE6B11E1-77BF-4CD6-B98B-4B01F2B68163} = {DB562C1B-F240-4832-8355-F6B3AA19608D} + {6568E523-7999-4751-926D-DAF38F3EA62D} = {DB562C1B-F240-4832-8355-F6B3AA19608D} + {7EC00713-86EC-4E58-8AC7-3904B830557F} = {DB562C1B-F240-4832-8355-F6B3AA19608D} EndGlobalSection EndGlobal diff --git a/src/submodules/ElementEngine b/src/submodules/ElementEngine new file mode 160000 index 0000000..67b1866 --- /dev/null +++ b/src/submodules/ElementEngine @@ -0,0 +1 @@ +Subproject commit 67b1866f32815f7b8b822c9a2e932a302f6b5a57