├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── src ├── Minecraftonia.WaveFunctionCollapse │ ├── GlobalUsings.Core.cs │ ├── Architecture │ │ └── ArchitectureModuleType.cs │ ├── Minecraftonia.WaveFunctionCollapse.csproj │ └── WaveFunctionCollapseGenerator3D.cs ├── Minecraftonia.Core │ ├── TerrainGenerationMode.cs │ ├── Minecraftonia.Core.csproj │ ├── BlockType.cs │ └── MinecraftoniaWorldConfig.cs ├── Minecraftonia.Rendering.Avalonia │ ├── Presenters │ │ ├── FramePresentationMode.cs │ │ ├── IVoxelFramePresenterFactory.cs │ │ ├── IVoxelFramePresenter.cs │ │ ├── DefaultVoxelFramePresenterFactory.cs │ │ ├── WritableBitmapFramePresenter.cs │ │ └── SkiaTextureFramePresenter.cs │ ├── RenderingConfiguration.cs │ ├── Minecraftonia.Rendering.Avalonia.csproj │ ├── Controls │ │ ├── VoxelProjector.cs │ │ ├── VoxelOverlayRenderer.cs │ │ └── VoxelRenderControl.cs │ └── Input │ │ └── MouseCursorUtils.cs ├── Minecraftonia.Rendering.Pipelines │ ├── IVoxelRendererFactory.cs │ ├── IVoxelRenderResult.cs │ ├── IVoxelMaterialProvider.cs │ ├── IVoxelRenderer.cs │ ├── VoxelRenderResult.cs │ ├── VoxelRendererOptions.cs │ ├── Minecraftonia.Rendering.Pipelines.csproj │ ├── VoxelRayTracerFactory.cs │ ├── RenderSamplePattern.cs │ ├── GlobalIlluminationSamples.cs │ ├── VoxelRaycaster.cs │ ├── FxaaSharpenFilter.cs │ └── VoxelDdaWalker.cs ├── Minecraftonia.Game │ ├── IGameSaveService.cs │ ├── Rendering │ │ ├── IGameRenderer.cs │ │ ├── GameRenderResult.cs │ │ └── DefaultGameRenderer.cs │ ├── GameInputConfiguration.cs │ ├── Minecraftonia.Game.csproj │ ├── GameSaveData.cs │ ├── GameControlConfiguration.cs │ └── FileGameSaveService.cs ├── Minecraftonia.Hosting.Avalonia │ ├── IKeyboardInputSource.cs │ ├── IPointerInputSource.cs │ ├── Minecraftonia.Hosting.Avalonia.csproj │ ├── KeyboardInputSource.cs │ └── PointerInputSource.cs ├── Minecraftonia.Hosting │ ├── IRenderPipeline.cs │ ├── GameTime.cs │ ├── IGameSession.cs │ ├── Minecraftonia.Hosting.csproj │ └── GameHost.cs ├── Minecraftonia.Rendering.Core │ ├── VoxelMaterialSample.cs │ ├── IVoxelFrameBuffer.cs │ ├── VoxelCamera.cs │ ├── Minecraftonia.Rendering.Core.csproj │ ├── VoxelSize.cs │ ├── GlobalIlluminationSettings.cs │ ├── VoxelLightingMath.cs │ └── VoxelFrameBuffer.cs ├── Minecraftonia │ ├── App.axaml │ ├── App.axaml.cs │ ├── Program.cs │ ├── app.manifest │ ├── Minecraftonia.csproj │ └── MainWindow.axaml.cs ├── Minecraftonia.VoxelEngine │ ├── Minecraftonia.VoxelEngine.csproj │ ├── VoxelRaycastHit.cs │ ├── Int3.cs │ ├── ChunkDimensions.cs │ ├── BlockFace.cs │ ├── VoxelBlockAccessCache.cs │ ├── ChunkCoordinate.cs │ ├── IVoxelWorld.cs │ ├── Player.cs │ ├── VoxelChunk.cs │ └── VoxelWorld.cs ├── Minecraftonia.MarkovJunior │ ├── Minecraftonia.MarkovJunior.csproj │ ├── MarkovRule.cs │ ├── MarkovJuniorEngine.cs │ ├── MarkovSymbol.cs │ ├── MarkovLayer.cs │ ├── Rules │ │ ├── NoiseFillRule.cs │ │ ├── PatternStampRule.cs │ │ └── AdjacencyConstraintRule.cs │ └── MarkovJuniorState.cs ├── Minecraftonia.OpenStreetMap │ └── Minecraftonia.OpenStreetMap.csproj ├── Minecraftonia.MarkovJunior.Architecture │ ├── Minecraftonia.MarkovJunior.Architecture.csproj │ ├── ArchitectureSymbolSet.cs │ ├── ArchitectureClusterContext.cs │ └── ArchitectureDebugExporter.cs └── Minecraftonia.Content │ └── Minecraftonia.Content.csproj ├── samples ├── Minecraftonia.Sample.Doom.Core │ ├── Class1.cs │ ├── Minecraftonia.Sample.Doom.Core.csproj │ └── DoomVoxelWorld.cs ├── Minecraftonia.Sample.Doom.Avalonia │ ├── MainWindow.axaml.cs │ ├── MainWindow.axaml │ ├── App.axaml │ ├── App.axaml.cs │ ├── Program.cs │ ├── app.manifest │ └── Minecraftonia.Sample.Doom.Avalonia.csproj ├── Minecraftonia.Sample.BasicBlock │ ├── RenderingConfigurationFactory.cs │ ├── Program.cs │ ├── Minecraftonia.Sample.BasicBlock.csproj │ └── SampleWorld.cs └── Minecraftonia.Sample.Doom │ ├── Minecraftonia.Sample.Doom.csproj │ └── Program.cs ├── global.json ├── Directory.Build.props ├── NuGet.Config ├── .gitattributes ├── docs ├── smoke-tests.md ├── hosting-plan.md └── refactor-plan.md ├── LICENSE ├── Directory.Build.targets ├── .gitignore └── .editorconfig /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [wieslawsoltes] 2 | -------------------------------------------------------------------------------- /src/Minecraftonia.WaveFunctionCollapse/GlobalUsings.Core.cs: -------------------------------------------------------------------------------- 1 | global using Minecraftonia.Core; 2 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Core/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Sample.Doom.Core; 2 | 3 | public class Class1 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.301", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Minecraftonia.Core/TerrainGenerationMode.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Core; 2 | 3 | public enum TerrainGenerationMode 4 | { 5 | Legacy = 0, 6 | WaveFunctionCollapse = 1 7 | } 8 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Presenters/FramePresentationMode.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Rendering.Avalonia.Presenters; 2 | 3 | public enum FramePresentationMode 4 | { 5 | WritableBitmap, 6 | SkiaTexture 7 | } 8 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.1.0 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/IVoxelRendererFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Rendering.Pipelines; 2 | 3 | public interface IVoxelRendererFactory 4 | { 5 | IVoxelRenderer Create(VoxelRendererOptions options); 6 | } 7 | -------------------------------------------------------------------------------- /src/Minecraftonia.WaveFunctionCollapse/Architecture/ArchitectureModuleType.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.WaveFunctionCollapse.Architecture; 2 | 3 | public enum ArchitectureModuleType 4 | { 5 | Housing = 0, 6 | Market = 1, 7 | Temple = 2 8 | } 9 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/IGameSaveService.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Game; 2 | 3 | public interface IGameSaveService 4 | { 5 | string GetSavePath(string saveName); 6 | GameSaveData Load(string path); 7 | void Save(GameSaveData data, string path); 8 | } 9 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Presenters/IVoxelFramePresenterFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Rendering.Avalonia.Presenters; 2 | 3 | public interface IVoxelFramePresenterFactory 4 | { 5 | IVoxelFramePresenter Create(FramePresentationMode mode); 6 | } 7 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/Rendering/IGameRenderer.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | 3 | namespace Minecraftonia.Game.Rendering; 4 | 5 | public interface IGameRenderer 6 | { 7 | GameRenderResult Render(MinecraftoniaGame game, IVoxelFrameBuffer? framebuffer); 8 | } 9 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace Minecraftonia.Sample.Doom.Avalonia; 4 | 5 | public partial class MainWindow : Window 6 | { 7 | public MainWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/IVoxelRenderResult.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | 3 | namespace Minecraftonia.Rendering.Pipelines; 4 | 5 | public interface IVoxelRenderResult 6 | { 7 | IVoxelFrameBuffer Framebuffer { get; } 8 | VoxelCamera Camera { get; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting.Avalonia/IKeyboardInputSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Input; 3 | 4 | namespace Minecraftonia.Hosting.Avalonia; 5 | 6 | public interface IKeyboardInputSource : IDisposable 7 | { 8 | bool IsDown(Key key); 9 | bool WasPressed(Key key); 10 | void NextFrame(); 11 | } 12 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/IVoxelMaterialProvider.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | using Minecraftonia.VoxelEngine; 3 | 4 | namespace Minecraftonia.Rendering.Pipelines; 5 | 6 | public interface IVoxelMaterialProvider 7 | { 8 | VoxelMaterialSample Sample(TBlock block, BlockFace face, float u, float v); 9 | } 10 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/GameInputConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Minecraftonia.Hosting.Avalonia; 4 | 5 | namespace Minecraftonia.Game; 6 | 7 | public sealed record GameInputConfiguration( 8 | Func CreateKeyboard, 9 | Func CreatePointer); 10 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting/IRenderPipeline.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | using Minecraftonia.Rendering.Pipelines; 3 | 4 | namespace Minecraftonia.Hosting; 5 | 6 | public interface IRenderPipeline 7 | where TBlock : struct 8 | { 9 | IVoxelRenderResult Render(IGameSession session, IVoxelFrameBuffer? framebuffer = null); 10 | } 11 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting/GameTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Hosting; 4 | 5 | /// 6 | /// Represents timing information for a simulation tick. 7 | /// 8 | public readonly record struct GameTime(TimeSpan Total, TimeSpan Elapsed) 9 | { 10 | public static GameTime FromElapsed(TimeSpan elapsed, TimeSpan total) => new(total, elapsed); 11 | } 12 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Presenters/IVoxelFramePresenter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Media; 4 | using Minecraftonia.Rendering.Core; 5 | 6 | namespace Minecraftonia.Rendering.Avalonia.Presenters; 7 | 8 | public interface IVoxelFramePresenter : IDisposable 9 | { 10 | void Render(DrawingContext context, IVoxelFrameBuffer framebuffer, Rect destination); 11 | } 12 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/VoxelMaterialSample.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Minecraftonia.Rendering.Core; 4 | 5 | public readonly struct VoxelMaterialSample 6 | { 7 | public VoxelMaterialSample(Vector3 color, float opacity) 8 | { 9 | Color = color; 10 | Opacity = opacity; 11 | } 12 | 13 | public Vector3 Color { get; } 14 | public float Opacity { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting.Avalonia/IPointerInputSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Hosting.Avalonia; 4 | 5 | public interface IPointerInputSource : IDisposable 6 | { 7 | float DeltaX { get; } 8 | float DeltaY { get; } 9 | bool IsMouseLookEnabled { get; } 10 | 11 | void EnableMouseLook(); 12 | void DisableMouseLook(); 13 | void QueueWarpToCenter(); 14 | void NextFrame(); 15 | } 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/IVoxelFrameBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Rendering.Core; 4 | 5 | public interface IVoxelFrameBuffer : IDisposable 6 | { 7 | VoxelSize Size { get; } 8 | int Stride { get; } 9 | int Length { get; } 10 | bool IsDisposed { get; } 11 | Span Span { get; } 12 | ReadOnlySpan ReadOnlySpan { get; } 13 | byte[] Pixels { get; } 14 | void Resize(VoxelSize size); 15 | } 16 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/IVoxelRenderer.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | using Minecraftonia.VoxelEngine; 3 | 4 | namespace Minecraftonia.Rendering.Pipelines; 5 | 6 | public interface IVoxelRenderer 7 | { 8 | IVoxelRenderResult Render( 9 | IVoxelWorld world, 10 | Player player, 11 | IVoxelMaterialProvider materials, 12 | IVoxelFrameBuffer? framebuffer = null); 13 | } 14 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/Rendering/GameRenderResult.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | 3 | namespace Minecraftonia.Game.Rendering; 4 | 5 | public readonly struct GameRenderResult 6 | { 7 | public GameRenderResult(IVoxelFrameBuffer framebuffer, VoxelCamera camera) 8 | { 9 | Framebuffer = framebuffer; 10 | Camera = camera; 11 | } 12 | 13 | public IVoxelFrameBuffer Framebuffer { get; } 14 | public VoxelCamera Camera { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Minecraftonia/App.axaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Minecraftonia.Core/Minecraftonia.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Core domain abstractions and utilities shared across the Minecraftonia libraries. 8 | true 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/Minecraftonia.VoxelEngine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | High performance voxel engine primitives powering Minecraftonia worlds. 8 | true 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/smoke-tests.md: -------------------------------------------------------------------------------- 1 | # Smoke Test Notes 2 | 3 | ## Desktop Game 4 | - `dotnet build Minecraftonia.sln` 5 | - `dotnet run --project Minecraftonia` and exercise menu navigation, pointer capture toggling, block placement/breaking (F1/F5 smoke). 6 | 7 | ## Sample: Basic Block 8 | - `dotnet run --project samples/Minecraftonia.Sample.BasicBlock` 9 | - Click or press `Tab` to capture the pointer, then verify WASD/Space/Shift fly controls and pointer look updates render smoothly around the sample column. 10 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/Minecraftonia.MarkovJunior.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Markov Junior based procedural content generation utilities tailored for Minecraftonia. 8 | true 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/RenderingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Rendering.Core; 2 | using Minecraftonia.Rendering.Pipelines; 3 | using Minecraftonia.Rendering.Avalonia.Presenters; 4 | 5 | namespace Minecraftonia.Rendering.Avalonia; 6 | 7 | public sealed record RenderingConfiguration( 8 | IVoxelRendererFactory RendererFactory, 9 | IVoxelFramePresenterFactory PresenterFactory, 10 | IVoxelMaterialProvider Materials) 11 | where TBlock : struct; 12 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/MarkovRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.MarkovJunior; 4 | 5 | /// 6 | /// Base type for MarkovJunior-inspired rules. Each rule may mutate the grid and returns true when a change occurs. 7 | /// 8 | public abstract class MarkovRule 9 | { 10 | protected MarkovRule(string name) 11 | { 12 | Name = name; 13 | } 14 | 15 | public string Name { get; } 16 | 17 | public abstract bool Apply(MarkovJuniorState state, Random random); 18 | } 19 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Core/Minecraftonia.Sample.Doom.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/VoxelRenderResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minecraftonia.Rendering.Core; 3 | 4 | namespace Minecraftonia.Rendering.Pipelines; 5 | 6 | public readonly struct VoxelRenderResult : IVoxelRenderResult 7 | { 8 | public VoxelRenderResult(IVoxelFrameBuffer framebuffer, VoxelCamera camera) 9 | { 10 | Framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer)); 11 | Camera = camera; 12 | } 13 | 14 | public IVoxelFrameBuffer Framebuffer { get; } 15 | public VoxelCamera Camera { get; } 16 | } 17 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Presenters/DefaultVoxelFramePresenterFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Rendering.Avalonia.Presenters; 2 | 3 | public sealed class DefaultVoxelFramePresenterFactory : IVoxelFramePresenterFactory 4 | { 5 | public IVoxelFramePresenter Create(FramePresentationMode mode) 6 | { 7 | return mode switch 8 | { 9 | FramePresentationMode.WritableBitmap => new WritableBitmapFramePresenter(), 10 | FramePresentationMode.SkiaTexture => new SkiaTextureFramePresenter(), 11 | _ => new SkiaTextureFramePresenter() 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/VoxelCamera.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Minecraftonia.Rendering.Core; 4 | 5 | public readonly struct VoxelCamera 6 | { 7 | public VoxelCamera(Vector3 forward, Vector3 right, Vector3 up, float tanHalfFov, float aspect) 8 | { 9 | Forward = forward; 10 | Right = right; 11 | Up = up; 12 | TanHalfFov = tanHalfFov; 13 | Aspect = aspect; 14 | } 15 | 16 | public Vector3 Forward { get; } 17 | public Vector3 Right { get; } 18 | public Vector3 Up { get; } 19 | public float TanHalfFov { get; } 20 | public float Aspect { get; } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.WaveFunctionCollapse/Minecraftonia.WaveFunctionCollapse.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Wave Function Collapse algorithms adapted for Minecraftonia structure generation. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/Minecraftonia.Rendering.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Rendering abstractions, materials, and camera systems for Minecraftonia voxel worlds. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Minecraftonia/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace Minecraftonia; 6 | 7 | public partial class App : Application 8 | { 9 | public override void Initialize() 10 | { 11 | AvaloniaXamlLoader.Load(this); 12 | } 13 | 14 | public override void OnFrameworkInitializationCompleted() 15 | { 16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 17 | { 18 | desktop.MainWindow = new MainWindow(); 19 | } 20 | 21 | base.OnFrameworkInitializationCompleted(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/VoxelRaycastHit.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Minecraftonia.VoxelEngine; 4 | 5 | public readonly struct VoxelRaycastHit 6 | { 7 | public Int3 Block { get; } 8 | public BlockFace Face { get; } 9 | public TBlock BlockType { get; } 10 | public Vector3 Point { get; } 11 | public float Distance { get; } 12 | 13 | public VoxelRaycastHit(Int3 block, BlockFace face, TBlock blockType, Vector3 point, float distance) 14 | { 15 | Block = block; 16 | Face = face; 17 | BlockType = blockType; 18 | Point = point; 19 | Distance = distance; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.OpenStreetMap/Minecraftonia.OpenStreetMap.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | OpenStreetMap integration helpers for importing real world data into Minecraftonia worlds. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace Minecraftonia.Sample.Doom.Avalonia; 6 | 7 | public partial class App : Application 8 | { 9 | public override void Initialize() 10 | { 11 | AvaloniaXamlLoader.Load(this); 12 | } 13 | 14 | public override void OnFrameworkInitializationCompleted() 15 | { 16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 17 | { 18 | desktop.MainWindow = new MainWindow(); 19 | } 20 | 21 | base.OnFrameworkInitializationCompleted(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/Int3.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Minecraftonia.VoxelEngine; 4 | 5 | public readonly struct Int3 6 | { 7 | public readonly int X; 8 | public readonly int Y; 9 | public readonly int Z; 10 | 11 | public Int3(int x, int y, int z) 12 | { 13 | X = x; 14 | Y = y; 15 | Z = z; 16 | } 17 | 18 | public static readonly Int3 Zero = new(0, 0, 0); 19 | 20 | public static Int3 operator +(Int3 a, Int3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); 21 | public static Int3 operator -(Int3 a, Int3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); 22 | 23 | public Vector3 ToVector3() => new(X, Y, Z); 24 | } 25 | -------------------------------------------------------------------------------- /docs/hosting-plan.md: -------------------------------------------------------------------------------- 1 | # Hosting Refactor Plan 2 | 3 | 1. [x] Create `Minecraftonia.Hosting` library with reusable render loop infrastructure – includes `GameTime`, `IGameSession`, `IRenderPipeline`, and `GameHost` wrappers that coordinate updates and rendering. 4 | 2. [x] Extract shared input and camera controllers into `Minecraftonia.Hosting.Avalonia`, reusing logic from `GameControl`. 5 | 3. [x] Refactor `GameControl` to compose the hosting services instead of owning render/input loops directly. 6 | 4. [x] Update `samples/Minecraftonia.Sample.BasicBlock` to rely on the hosting package for game-like behaviour. 7 | 5. [x] Refresh docs and CI samples to point to the new hosting primitives, and verify builds/tests. 8 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/ChunkDimensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.VoxelEngine; 4 | 5 | public readonly struct ChunkDimensions 6 | { 7 | public ChunkDimensions(int sizeX, int sizeY, int sizeZ) 8 | { 9 | if (sizeX <= 0) throw new ArgumentOutOfRangeException(nameof(sizeX)); 10 | if (sizeY <= 0) throw new ArgumentOutOfRangeException(nameof(sizeY)); 11 | if (sizeZ <= 0) throw new ArgumentOutOfRangeException(nameof(sizeZ)); 12 | 13 | SizeX = sizeX; 14 | SizeY = sizeY; 15 | SizeZ = sizeZ; 16 | } 17 | 18 | public int SizeX { get; } 19 | public int SizeY { get; } 20 | public int SizeZ { get; } 21 | 22 | public int Volume => SizeX * SizeY * SizeZ; 23 | } 24 | -------------------------------------------------------------------------------- /src/Minecraftonia/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace Minecraftonia; 5 | 6 | class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | [STAThread] 12 | public static void Main(string[] args) => BuildAvaloniaApp() 13 | .StartWithClassicDesktopLifetime(args); 14 | 15 | // Avalonia configuration, don't remove; also used by visual designer. 16 | public static AppBuilder BuildAvaloniaApp() 17 | => AppBuilder.Configure() 18 | .UsePlatformDetect() 19 | .WithInterFont() 20 | .LogToTrace(); 21 | } 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/VoxelRendererOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using Minecraftonia.Rendering.Core; 4 | 5 | namespace Minecraftonia.Rendering.Pipelines; 6 | 7 | public sealed record VoxelRendererOptions( 8 | VoxelSize RenderSize, 9 | float FieldOfViewDegrees, 10 | Func IsSolid, 11 | Func IsEmpty, 12 | int SamplesPerPixel = 1, 13 | bool EnableFxaa = true, 14 | float FxaaContrastThreshold = 0.0312f, 15 | float FxaaRelativeThreshold = 0.125f, 16 | bool EnableSharpen = true, 17 | float SharpenAmount = 0.18f, 18 | float FogStart = 45f, 19 | float FogEnd = 90f, 20 | Vector3? FogColor = null, 21 | GlobalIlluminationSettings? GlobalIllumination = null); 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/Minecraftonia.Rendering.Pipelines.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | true 8 | Configurable rendering pipelines and effects tailored for Minecraftonia visuals. 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace Minecraftonia.Sample.Doom.Avalonia; 5 | 6 | class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | [STAThread] 12 | public static void Main(string[] args) => BuildAvaloniaApp() 13 | .StartWithClassicDesktopLifetime(args); 14 | 15 | // Avalonia configuration, don't remove; also used by visual designer. 16 | public static AppBuilder BuildAvaloniaApp() 17 | => AppBuilder.Configure() 18 | .UsePlatformDetect() 19 | .WithInterFont() 20 | .LogToTrace(); 21 | } 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/BlockFace.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.VoxelEngine; 2 | 3 | public enum BlockFace 4 | { 5 | NegativeX, 6 | PositiveX, 7 | NegativeY, 8 | PositiveY, 9 | NegativeZ, 10 | PositiveZ 11 | } 12 | 13 | public static class BlockFaceExtensions 14 | { 15 | public static Int3 ToOffset(this BlockFace face) 16 | { 17 | return face switch 18 | { 19 | BlockFace.NegativeX => new Int3(-1, 0, 0), 20 | BlockFace.PositiveX => new Int3(1, 0, 0), 21 | BlockFace.NegativeY => new Int3(0, -1, 0), 22 | BlockFace.PositiveY => new Int3(0, 1, 0), 23 | BlockFace.NegativeZ => new Int3(0, 0, -1), 24 | BlockFace.PositiveZ => new Int3(0, 0, 1), 25 | _ => Int3.Zero 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting/IGameSession.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Core; 2 | using Minecraftonia.Rendering.Pipelines; 3 | using Minecraftonia.VoxelEngine; 4 | 5 | namespace Minecraftonia.Hosting; 6 | 7 | /// 8 | /// Represents a minimal game session that can be driven by the shared hosting infrastructure. 9 | /// 10 | /// Block type used by the voxel world. 11 | public interface IGameSession 12 | where TBlock : struct 13 | { 14 | IVoxelWorld World { get; } 15 | Player Player { get; } 16 | IVoxelMaterialProvider Materials { get; } 17 | 18 | /// 19 | /// Allows the session to advance its simulation. 20 | /// 21 | /// Timing information for the tick. 22 | void Update(GameTime time); 23 | } 24 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/VoxelBlockAccessCache.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.VoxelEngine; 2 | 3 | public struct VoxelBlockAccessCache 4 | { 5 | private ChunkCoordinate _coordinate; 6 | private VoxelChunk? _chunk; 7 | private TBlock[]? _blocks; 8 | 9 | internal ChunkCoordinate Coordinate => _coordinate; 10 | internal VoxelChunk? Chunk => _chunk; 11 | internal TBlock[]? Blocks => _blocks; 12 | 13 | public bool IsValid => _chunk is not null; 14 | 15 | internal void SetChunk(VoxelChunk chunk) 16 | { 17 | _chunk = chunk; 18 | _coordinate = chunk.Coordinate; 19 | _blocks = chunk.RawBlocks; 20 | } 21 | 22 | internal bool Matches(ChunkCoordinate coordinate) 23 | { 24 | return _chunk is not null && coordinate == _coordinate; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.BasicBlock/RenderingConfigurationFactory.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Content; 2 | using Minecraftonia.Core; 3 | using Minecraftonia.Hosting; 4 | using Minecraftonia.Rendering.Avalonia; 5 | using Minecraftonia.Rendering.Avalonia.Presenters; 6 | using Minecraftonia.Rendering.Core; 7 | using Minecraftonia.Rendering.Pipelines; 8 | 9 | namespace Minecraftonia.Sample.BasicBlock; 10 | 11 | internal static class RenderingConfigurationFactory 12 | { 13 | public static RenderingConfiguration Create() 14 | { 15 | var rendererFactory = new VoxelRayTracerFactory(); 16 | var presenterFactory = new DefaultVoxelFramePresenterFactory(); 17 | var materials = new BlockTextures(); 18 | 19 | return new RenderingConfiguration(rendererFactory, presenterFactory, materials); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior.Architecture/Minecraftonia.MarkovJunior.Architecture.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Building blocks for procedural architecture generation powered by Markov Junior. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Minecraftonia.Core/BlockType.cs: -------------------------------------------------------------------------------- 1 | namespace Minecraftonia.Core; 2 | 3 | public enum BlockType 4 | { 5 | Air = 0, 6 | Grass, 7 | Dirt, 8 | Stone, 9 | Sand, 10 | Water, 11 | Wood, 12 | Leaves 13 | } 14 | 15 | public static class BlockTypeExtensions 16 | { 17 | public static bool IsSolid(this BlockType type) 18 | { 19 | return type switch 20 | { 21 | BlockType.Air => false, 22 | BlockType.Water => false, 23 | BlockType.Leaves => false, 24 | _ => true 25 | }; 26 | } 27 | 28 | public static bool IsTransparent(this BlockType type) 29 | { 30 | return type switch 31 | { 32 | BlockType.Air => true, 33 | BlockType.Water => true, 34 | BlockType.Leaves => true, 35 | _ => false 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Minecraftonia.Content/Minecraftonia.Content.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Prebuilt voxel assets, textures, and definitions for Minecraftonia based applications. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting/Minecraftonia.Hosting.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Hosting infrastructure, service registration, and lifecycle helpers for Minecraftonia applications. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/MarkovJuniorEngine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Minecraftonia.MarkovJunior; 5 | 6 | public sealed class MarkovJuniorEngine 7 | { 8 | private readonly List _layers = new(); 9 | private readonly Random _random; 10 | 11 | public MarkovJuniorEngine(int seed) 12 | { 13 | _random = new Random(seed); 14 | } 15 | 16 | public MarkovJuniorEngine AddLayer(MarkovLayer layer) 17 | { 18 | _layers.Add(layer ?? throw new ArgumentNullException(nameof(layer))); 19 | return this; 20 | } 21 | 22 | public bool Execute(MarkovJuniorState state) 23 | { 24 | bool changed = false; 25 | foreach (var layer in _layers) 26 | { 27 | if (layer.Execute(state, _random)) 28 | { 29 | changed = true; 30 | } 31 | } 32 | 33 | return changed; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Minecraftonia/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom/Minecraftonia.Sample.Doom.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting.Avalonia/Minecraftonia.Hosting.Avalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Avalonia UI specific hosting adapters and bootstrapping routines for Minecraftonia. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/VoxelRayTracerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Rendering.Pipelines; 4 | 5 | public sealed class VoxelRayTracerFactory : IVoxelRendererFactory 6 | { 7 | public IVoxelRenderer Create(VoxelRendererOptions options) 8 | { 9 | ArgumentNullException.ThrowIfNull(options); 10 | ArgumentNullException.ThrowIfNull(options.IsSolid); 11 | ArgumentNullException.ThrowIfNull(options.IsEmpty); 12 | 13 | return new VoxelRayTracer( 14 | options.RenderSize, 15 | options.FieldOfViewDegrees, 16 | options.IsSolid, 17 | options.IsEmpty, 18 | options.SamplesPerPixel, 19 | options.EnableFxaa, 20 | options.FxaaContrastThreshold, 21 | options.FxaaRelativeThreshold, 22 | options.EnableSharpen, 23 | options.SharpenAmount, 24 | options.FogStart, 25 | options.FogEnd, 26 | options.FogColor, 27 | options.GlobalIllumination); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Wiesław Šoltés 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/ChunkCoordinate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.VoxelEngine; 4 | 5 | public readonly struct ChunkCoordinate : IEquatable 6 | { 7 | public ChunkCoordinate(int x, int y, int z) 8 | { 9 | X = x; 10 | Y = y; 11 | Z = z; 12 | } 13 | 14 | public int X { get; } 15 | public int Y { get; } 16 | public int Z { get; } 17 | 18 | public bool Equals(ChunkCoordinate other) 19 | { 20 | return X == other.X && Y == other.Y && Z == other.Z; 21 | } 22 | 23 | public override bool Equals(object? obj) 24 | { 25 | return obj is ChunkCoordinate other && Equals(other); 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | return HashCode.Combine(X, Y, Z); 31 | } 32 | 33 | public override string ToString() 34 | { 35 | return $"({X}, {Y}, {Z})"; 36 | } 37 | 38 | public static bool operator ==(ChunkCoordinate left, ChunkCoordinate right) => left.Equals(right); 39 | public static bool operator !=(ChunkCoordinate left, ChunkCoordinate right) => !left.Equals(right); 40 | } 41 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Minecraftonia.Rendering.Avalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | true 8 | Avalonia rendering surfaces and UI bindings for the Minecraftonia rendering pipeline. 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/VoxelSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Rendering.Core; 4 | 5 | public readonly struct VoxelSize : IEquatable 6 | { 7 | public VoxelSize(int width, int height) 8 | { 9 | if (width <= 0) 10 | { 11 | throw new ArgumentOutOfRangeException(nameof(width), "Width must be positive."); 12 | } 13 | 14 | if (height <= 0) 15 | { 16 | throw new ArgumentOutOfRangeException(nameof(height), "Height must be positive."); 17 | } 18 | 19 | Width = width; 20 | Height = height; 21 | } 22 | 23 | public int Width { get; } 24 | public int Height { get; } 25 | 26 | public bool Equals(VoxelSize other) => Width == other.Width && Height == other.Height; 27 | public override bool Equals(object? obj) => obj is VoxelSize other && Equals(other); 28 | public override int GetHashCode() => HashCode.Combine(Width, Height); 29 | 30 | public static bool operator ==(VoxelSize left, VoxelSize right) => left.Equals(right); 31 | public static bool operator !=(VoxelSize left, VoxelSize right) => !left.Equals(right); 32 | } 33 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/Rendering/DefaultGameRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minecraftonia.Core; 3 | using Minecraftonia.Rendering.Core; 4 | using Minecraftonia.Rendering.Pipelines; 5 | 6 | namespace Minecraftonia.Game.Rendering; 7 | 8 | public sealed class DefaultGameRenderer : IGameRenderer 9 | { 10 | private readonly IVoxelRenderer _voxelRenderer; 11 | private readonly IVoxelMaterialProvider _materials; 12 | 13 | public DefaultGameRenderer(IVoxelRenderer voxelRenderer, IVoxelMaterialProvider materials) 14 | { 15 | _voxelRenderer = voxelRenderer ?? throw new ArgumentNullException(nameof(voxelRenderer)); 16 | _materials = materials ?? throw new ArgumentNullException(nameof(materials)); 17 | } 18 | 19 | public GameRenderResult Render(MinecraftoniaGame game, IVoxelFrameBuffer? framebuffer) 20 | { 21 | if (game is null) 22 | { 23 | throw new ArgumentNullException(nameof(game)); 24 | } 25 | 26 | var result = _voxelRenderer.Render(game.World, game.Player, _materials, framebuffer); 27 | return new GameRenderResult(result.Framebuffer, result.Camera); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/IVoxelWorld.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | 4 | namespace Minecraftonia.VoxelEngine; 5 | 6 | public interface IVoxelWorld 7 | { 8 | ChunkDimensions ChunkSize { get; } 9 | int ChunkCountX { get; } 10 | int ChunkCountY { get; } 11 | int ChunkCountZ { get; } 12 | int Width { get; } 13 | int Height { get; } 14 | int Depth { get; } 15 | 16 | IReadOnlyDictionary> LoadedChunks { get; } 17 | 18 | bool InBounds(int x, int y, int z); 19 | bool InBounds(Vector3 position); 20 | 21 | TBlock GetBlock(int x, int y, int z); 22 | TBlock GetBlock(Int3 position); 23 | TBlock GetBlock(int x, int y, int z, ref VoxelBlockAccessCache cache); 24 | TBlock GetBlockFast( 25 | int chunkX, 26 | int chunkY, 27 | int chunkZ, 28 | int localX, 29 | int localY, 30 | int localZ, 31 | ref VoxelBlockAccessCache cache); 32 | 33 | ChunkCoordinate GetChunkCoordinate(int x, int y, int z); 34 | ChunkCoordinate GetChunkCoordinate(Vector3 position); 35 | 36 | IEnumerable EnumerateLoadedChunks(); 37 | bool TryGetLoadedChunk(ChunkCoordinate coordinate, out VoxelChunk chunk); 38 | } 39 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/RenderSamplePattern.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace Minecraftonia.Rendering.Pipelines; 5 | 6 | public static class RenderSamplePattern 7 | { 8 | public static Vector2[] CreateStratified(int samples) 9 | { 10 | var offsets = new Vector2[samples]; 11 | offsets[0] = new Vector2(0.5f, 0.5f); 12 | 13 | if (samples == 1) 14 | { 15 | return offsets; 16 | } 17 | 18 | int grid = (int)Math.Ceiling(MathF.Sqrt(samples)); 19 | float step = 1f / grid; 20 | float half = step / 2f; 21 | 22 | int index = 1; 23 | for (int gy = 0; gy < grid && index < samples; gy++) 24 | { 25 | for (int gx = 0; gx < grid && index < samples; gx++) 26 | { 27 | float ox = half + gx * step; 28 | float oy = half + gy * step; 29 | if (Math.Abs(ox - 0.5f) < 0.001f && Math.Abs(oy - 0.5f) < 0.001f) 30 | { 31 | continue; 32 | } 33 | 34 | offsets[index++] = new Vector2(ox, oy); 35 | } 36 | } 37 | 38 | while (index < samples) 39 | { 40 | offsets[index] = new Vector2(0.5f, 0.5f); 41 | index++; 42 | } 43 | 44 | return offsets; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | Wieslaw Soltes 4 | Wieslaw Soltes 5 | https://github.com/wieslawsoltes/Minecraftonia 6 | https://github.com/wieslawsoltes/Minecraftonia 7 | git 8 | MIT 9 | minecraftonia;minecraft;voxel;avalonia;procedural 10 | false 11 | README.md 12 | true 13 | snupkg 14 | true 15 | true 16 | $(MSBuildProjectName) library for the Minecraftonia ecosystem. 17 | $(PackageDescription) 18 | 19 | 20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/MarkovSymbol.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Minecraftonia.MarkovJunior; 5 | 6 | /// 7 | /// Represents a symbol used by the MarkovJunior-inspired rule system. 8 | /// Symbols carry tags so rules can reason about semantics (e.g. street, wall, canopy). 9 | /// 10 | public sealed class MarkovSymbol 11 | { 12 | private readonly HashSet _tags = new(StringComparer.OrdinalIgnoreCase); 13 | 14 | public MarkovSymbol(string id, int paletteIndex) 15 | { 16 | if (string.IsNullOrWhiteSpace(id)) 17 | { 18 | throw new ArgumentException("Symbol id cannot be empty.", nameof(id)); 19 | } 20 | 21 | Id = id; 22 | PaletteIndex = paletteIndex; 23 | } 24 | 25 | public string Id { get; } 26 | public int PaletteIndex { get; } 27 | 28 | public IReadOnlyCollection Tags => _tags; 29 | 30 | public MarkovSymbol WithTags(params string[] tags) 31 | { 32 | if (tags is null) 33 | { 34 | return this; 35 | } 36 | 37 | foreach (var tag in tags) 38 | { 39 | if (!string.IsNullOrWhiteSpace(tag)) 40 | { 41 | _tags.Add(tag.ToLowerInvariant()); 42 | } 43 | } 44 | 45 | return this; 46 | } 47 | 48 | public bool HasTag(string tag) => _tags.Contains(tag); 49 | } 50 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.BasicBlock/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Controls; 4 | using Avalonia.Controls.ApplicationLifetimes; 5 | using Avalonia.Layout; 6 | 7 | namespace Minecraftonia.Sample.BasicBlock; 8 | 9 | internal sealed class App : Application 10 | { 11 | public override void OnFrameworkInitializationCompleted() 12 | { 13 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 14 | { 15 | desktop.MainWindow = new MainWindow(); 16 | } 17 | 18 | base.OnFrameworkInitializationCompleted(); 19 | } 20 | } 21 | 22 | internal sealed class MainWindow : Window 23 | { 24 | public MainWindow() 25 | { 26 | Title = "Minecraftonia Sample – Basic Block"; 27 | Width = 960; 28 | Height = 600; 29 | 30 | Content = new SampleGameControl(RenderingConfigurationFactory.Create()) 31 | { 32 | HorizontalAlignment = HorizontalAlignment.Stretch, 33 | VerticalAlignment = VerticalAlignment.Stretch 34 | }; 35 | } 36 | } 37 | 38 | internal static class Program 39 | { 40 | [STAThread] 41 | public static void Main(string[] args) 42 | { 43 | BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); 44 | } 45 | 46 | public static AppBuilder BuildAvaloniaApp() => 47 | AppBuilder.Configure() 48 | .UsePlatformDetect() 49 | .LogToTrace(); 50 | } 51 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting.Avalonia/KeyboardInputSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Avalonia.Input; 4 | using Avalonia.Controls; 5 | 6 | namespace Minecraftonia.Hosting.Avalonia; 7 | 8 | /// 9 | /// Tracks keyboard state for a TopLevel and exposes per-frame key information. 10 | /// 11 | public sealed class KeyboardInputSource : IKeyboardInputSource 12 | { 13 | private readonly HashSet _keysDown = new(); 14 | private readonly HashSet _keysPressed = new(); 15 | private readonly TopLevel _topLevel; 16 | 17 | public KeyboardInputSource(TopLevel topLevel) 18 | { 19 | _topLevel = topLevel ?? throw new ArgumentNullException(nameof(topLevel)); 20 | _topLevel.KeyDown += OnKeyDown; 21 | _topLevel.KeyUp += OnKeyUp; 22 | } 23 | 24 | private void OnKeyDown(object? sender, KeyEventArgs e) 25 | { 26 | if (_keysDown.Add(e.Key)) 27 | { 28 | _keysPressed.Add(e.Key); 29 | } 30 | } 31 | 32 | private void OnKeyUp(object? sender, KeyEventArgs e) 33 | { 34 | _keysDown.Remove(e.Key); 35 | } 36 | 37 | public bool IsDown(Key key) => _keysDown.Contains(key); 38 | 39 | public bool WasPressed(Key key) => _keysPressed.Contains(key); 40 | 41 | public void NextFrame() => _keysPressed.Clear(); 42 | 43 | public void Dispose() 44 | { 45 | _topLevel.KeyDown -= OnKeyDown; 46 | _topLevel.KeyUp -= OnKeyUp; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.BasicBlock/Minecraftonia.Sample.BasicBlock.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/GlobalIlluminationSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Minecraftonia.Rendering.Core; 4 | 5 | public readonly record struct GlobalIlluminationSettings( 6 | bool Enabled = true, 7 | int DiffuseSampleCount = 6, 8 | int BounceCount = 1, 9 | float MaxDistance = 18f, 10 | float Strength = 1.1f, 11 | float SkyContribution = 0.9f, 12 | float OcclusionStrength = 1.0f, 13 | float DistanceAttenuation = 0.24f, 14 | float ShadowBias = 0.00075f, 15 | float SunShadowSoftness = 0.65f, 16 | float SunMaxDistance = 70f, 17 | Vector3 SunDirection = default, 18 | Vector3 SunColor = default, 19 | float SunIntensity = 1.45f, 20 | Vector3 AmbientLight = default, 21 | bool UseBentNormalForAmbient = true, 22 | int MaxSecondarySteps = 96) 23 | { 24 | public static GlobalIlluminationSettings Default => new( 25 | Enabled: true, 26 | DiffuseSampleCount: 6, 27 | BounceCount: 1, 28 | MaxDistance: 18f, 29 | Strength: 1.1f, 30 | SkyContribution: 0.9f, 31 | OcclusionStrength: 1.0f, 32 | DistanceAttenuation: 0.24f, 33 | ShadowBias: 0.00075f, 34 | SunShadowSoftness: 0.65f, 35 | SunMaxDistance: 70f, 36 | SunDirection: Vector3.Normalize(new Vector3(-0.35f, 0.88f, 0.25f)), 37 | SunColor: new Vector3(1.08f, 1.0f, 0.86f), 38 | SunIntensity: 1.45f, 39 | AmbientLight: new Vector3(0.16f, 0.19f, 0.24f), 40 | UseBentNormalForAmbient: true, 41 | MaxSecondarySteps: 96); 42 | } 43 | -------------------------------------------------------------------------------- /src/Minecraftonia.Hosting/GameHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minecraftonia.Rendering.Core; 3 | using Minecraftonia.Rendering.Pipelines; 4 | 5 | namespace Minecraftonia.Hosting; 6 | 7 | /// 8 | /// Coordinates simulation ticks and rendering using an and . 9 | /// 10 | public sealed class GameHost 11 | where TBlock : struct 12 | { 13 | private readonly IGameSession _session; 14 | private readonly IRenderPipeline _pipeline; 15 | private GameTime _time; 16 | private IVoxelFrameBuffer? _frameBuffer; 17 | 18 | public GameHost(IGameSession session, IRenderPipeline pipeline) 19 | { 20 | _session = session ?? throw new ArgumentNullException(nameof(session)); 21 | _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); 22 | _time = new GameTime(TimeSpan.Zero, TimeSpan.Zero); 23 | } 24 | 25 | /// 26 | /// Advances the session and renders a frame. 27 | /// 28 | /// Elapsed time since the last step. 29 | public IVoxelRenderResult Step(TimeSpan elapsed) 30 | { 31 | _time = new GameTime(_time.Total + elapsed, elapsed); 32 | _session.Update(_time); 33 | var result = _pipeline.Render(_session, _frameBuffer); 34 | _frameBuffer = result.Framebuffer; 35 | LastResult = result; 36 | return result; 37 | } 38 | 39 | public IVoxelRenderResult? LastResult { get; private set; } 40 | } 41 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/MarkovLayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Minecraftonia.MarkovJunior; 5 | 6 | /// 7 | /// Groups rules into passes that execute in sequence, similar to MarkovJunior schedules. 8 | /// 9 | public sealed class MarkovLayer 10 | { 11 | private readonly List _rules = new(); 12 | 13 | public MarkovLayer(string name, int maxIterations = 50) 14 | { 15 | if (maxIterations <= 0) 16 | { 17 | throw new ArgumentOutOfRangeException(nameof(maxIterations)); 18 | } 19 | 20 | Name = name; 21 | MaxIterations = maxIterations; 22 | } 23 | 24 | public string Name { get; } 25 | public int MaxIterations { get; } 26 | 27 | public MarkovLayer AddRule(MarkovRule rule) 28 | { 29 | _rules.Add(rule ?? throw new ArgumentNullException(nameof(rule))); 30 | return this; 31 | } 32 | 33 | public bool Execute(MarkovJuniorState state, Random random) 34 | { 35 | bool anyChange = false; 36 | for (int iteration = 0; iteration < MaxIterations; iteration++) 37 | { 38 | bool iterationChange = false; 39 | foreach (var rule in _rules) 40 | { 41 | if (rule.Apply(state, random)) 42 | { 43 | iterationChange = true; 44 | anyChange = true; 45 | } 46 | } 47 | 48 | if (!iterationChange) 49 | { 50 | break; 51 | } 52 | } 53 | 54 | return anyChange; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/Player.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace Minecraftonia.VoxelEngine; 5 | 6 | public sealed class Player 7 | { 8 | public Vector3 Position; 9 | public Vector3 Velocity; 10 | public float Yaw; 11 | public float Pitch; 12 | public bool IsOnGround; 13 | 14 | public float EyeHeight { get; set; } = 1.62f; 15 | 16 | public Vector3 EyePosition => Position + new Vector3(0f, EyeHeight, 0f); 17 | 18 | public Vector3 Forward 19 | { 20 | get 21 | { 22 | float yawRad = Yaw * (MathF.PI / 180f); 23 | float pitchRad = Pitch * (MathF.PI / 180f); 24 | float cosPitch = MathF.Cos(pitchRad); 25 | return Vector3.Normalize(new Vector3( 26 | MathF.Sin(yawRad) * cosPitch, 27 | MathF.Sin(pitchRad), 28 | MathF.Cos(yawRad) * cosPitch 29 | )); 30 | } 31 | } 32 | 33 | public Vector3 Right 34 | { 35 | get 36 | { 37 | Vector3 forward = Forward; 38 | Vector3 horizontalForward = new(forward.X, 0f, forward.Z); 39 | if (horizontalForward.LengthSquared() < 0.0001f) 40 | { 41 | return Vector3.UnitX; 42 | } 43 | 44 | horizontalForward = Vector3.Normalize(horizontalForward); 45 | Vector3 right = new Vector3(horizontalForward.Z, 0f, -horizontalForward.X); 46 | if (right.LengthSquared() < 0.0001f) 47 | { 48 | return Vector3.UnitX; 49 | } 50 | 51 | return Vector3.Normalize(right); 52 | } 53 | } 54 | 55 | public Vector3 Up => Vector3.UnitY; 56 | } 57 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.BasicBlock/SampleWorld.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minecraftonia.Core; 3 | using Minecraftonia.VoxelEngine; 4 | 5 | namespace Minecraftonia.Sample.BasicBlock; 6 | 7 | internal sealed class SampleWorld : VoxelWorld 8 | { 9 | private readonly BlockType[] _blocks; 10 | 11 | public SampleWorld() 12 | : base(new ChunkDimensions(16, 16, 16), 1, 1, 1) 13 | { 14 | _blocks = new BlockType[ChunkSize.Volume]; 15 | PopulateArray(); 16 | } 17 | 18 | private void PopulateArray() 19 | { 20 | Array.Fill(_blocks, BlockType.Air); 21 | 22 | for (int z = 0; z < ChunkSize.SizeZ; z++) 23 | { 24 | for (int x = 0; x < ChunkSize.SizeX; x++) 25 | { 26 | _blocks[Index(x, 0, z)] = BlockType.Stone; 27 | } 28 | } 29 | 30 | int pillarX = ChunkSize.SizeX / 2; 31 | int pillarZ = ChunkSize.SizeZ / 2; 32 | 33 | _blocks[Index(pillarX, 1, pillarZ)] = BlockType.Wood; 34 | _blocks[Index(pillarX, 2, pillarZ)] = BlockType.Leaves; 35 | } 36 | 37 | protected override void PopulateChunk(VoxelChunk chunk) 38 | { 39 | var data = chunk.DataSpan; 40 | for (int y = 0; y < ChunkSize.SizeY; y++) 41 | { 42 | for (int z = 0; z < ChunkSize.SizeZ; z++) 43 | { 44 | for (int x = 0; x < ChunkSize.SizeX; x++) 45 | { 46 | data[(y * ChunkSize.SizeZ + z) * ChunkSize.SizeX + x] = _blocks[Index(x, y, z)]; 47 | } 48 | } 49 | } 50 | } 51 | 52 | private int Index(int x, int y, int z) 53 | { 54 | return (y * ChunkSize.SizeZ + z) * ChunkSize.SizeX + x; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/Rules/NoiseFillRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.MarkovJunior.Rules; 4 | 5 | public sealed class NoiseFillRule : MarkovRule 6 | { 7 | private readonly MarkovSymbol _symbol; 8 | private readonly double _threshold; 9 | private readonly int _salt; 10 | 11 | public NoiseFillRule(string name, MarkovSymbol symbol, double threshold, int salt) 12 | : base(name) 13 | { 14 | _symbol = symbol ?? throw new ArgumentNullException(nameof(symbol)); 15 | _threshold = threshold; 16 | _salt = salt; 17 | } 18 | 19 | public override bool Apply(MarkovJuniorState state, Random random) 20 | { 21 | bool changed = false; 22 | for (int x = 0; x < state.SizeX; x++) 23 | { 24 | for (int y = 0; y < state.SizeY; y++) 25 | { 26 | for (int z = 0; z < state.SizeZ; z++) 27 | { 28 | if (state.GetSymbol(x, y, z) != state.EmptySymbol) 29 | { 30 | continue; 31 | } 32 | 33 | double noise = HashToUnit(x, y, z); 34 | if (noise < _threshold) 35 | { 36 | state.SetSymbol(x, y, z, _symbol); 37 | changed = true; 38 | } 39 | } 40 | } 41 | } 42 | 43 | return changed; 44 | } 45 | 46 | private double HashToUnit(int x, int y, int z) 47 | { 48 | int h = _salt; 49 | h = unchecked(h * 73856093) ^ x; 50 | h = unchecked(h * 19349663) ^ y; 51 | h = unchecked(h * 83492791) ^ z; 52 | h ^= h >> 13; 53 | h ^= h << 7; 54 | h &= int.MaxValue; 55 | return h / (double)int.MaxValue; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/GlobalIlluminationSamples.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace Minecraftonia.Rendering.Pipelines; 5 | 6 | public static class GlobalIlluminationSamples 7 | { 8 | public static ReadOnlySpan HemisphereSamples128 => _hemisphereSamples128; 9 | 10 | private static readonly Vector3[] _hemisphereSamples128 = CreateHemisphereSamples(128); 11 | 12 | private static Vector3[] CreateHemisphereSamples(int count) 13 | { 14 | var samples = new Vector3[count]; 15 | 16 | for (int i = 0; i < count; i++) 17 | { 18 | Vector2 xi = Hammersley(i, count); 19 | float phi = xi.Y * MathF.PI * 2f; 20 | float cosTheta = MathF.Sqrt(1f - xi.X); 21 | float sinTheta = MathF.Sqrt(MathF.Max(0f, 1f - cosTheta * cosTheta)); 22 | 23 | float x = MathF.Cos(phi) * sinTheta; 24 | float y = cosTheta; 25 | float z = MathF.Sin(phi) * sinTheta; 26 | samples[i] = new Vector3(x, y, z); 27 | } 28 | 29 | return samples; 30 | } 31 | 32 | private static Vector2 Hammersley(int index, int count) 33 | { 34 | float e1 = index / (float)count; 35 | float e2 = RadicalInverseVdC(index); 36 | return new Vector2(e1, e2); 37 | } 38 | 39 | private static float RadicalInverseVdC(int index) 40 | { 41 | uint bits = (uint)index; 42 | bits = (bits << 16) | (bits >> 16); 43 | bits = ((bits & 0x55555555u) << 1) | ((bits & 0xAAAAAAAau) >> 1); 44 | bits = ((bits & 0x33333333u) << 2) | ((bits & 0xCCCCCCCCu) >> 2); 45 | bits = ((bits & 0x0F0F0F0Fu) << 4) | ((bits & 0xF0F0F0F0u) >> 4); 46 | bits = ((bits & 0x00FF00FFu) << 8) | ((bits & 0xFF00FF00u) >> 8); 47 | return bits * 2.3283064365386963e-10f; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Controls/VoxelProjector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using Avalonia; 4 | using Minecraftonia.Rendering.Core; 5 | 6 | namespace Minecraftonia.Rendering.Avalonia.Controls; 7 | 8 | public readonly struct VoxelProjector 9 | { 10 | public VoxelProjector(VoxelCamera camera, Vector3 eyePosition, Size viewportSize) 11 | { 12 | Camera = camera; 13 | EyePosition = eyePosition; 14 | ViewportSize = viewportSize; 15 | } 16 | 17 | public VoxelCamera Camera { get; } 18 | public Vector3 EyePosition { get; } 19 | public Size ViewportSize { get; } 20 | 21 | public bool TryProject(Vector3 worldPoint, out Point projected) 22 | { 23 | projected = default; 24 | 25 | if (ViewportSize.Width <= 0 || ViewportSize.Height <= 0) 26 | { 27 | return false; 28 | } 29 | 30 | if (Camera.TanHalfFov <= float.Epsilon || Camera.Aspect <= float.Epsilon) 31 | { 32 | return false; 33 | } 34 | 35 | Vector3 toPoint = worldPoint - EyePosition; 36 | 37 | float x = Vector3.Dot(toPoint, Camera.Right); 38 | float y = Vector3.Dot(toPoint, Camera.Up); 39 | float z = Vector3.Dot(toPoint, Camera.Forward); 40 | 41 | if (z <= 0.05f) 42 | { 43 | return false; 44 | } 45 | 46 | float ndcX = x / (z * Camera.TanHalfFov * Camera.Aspect); 47 | float ndcY = y / (z * Camera.TanHalfFov); 48 | 49 | double screenX = (ndcX + 1d) * 0.5d * ViewportSize.Width; 50 | double screenY = (1d - ndcY) * 0.5d * ViewportSize.Height; 51 | 52 | if (!double.IsFinite(screenX) || !double.IsFinite(screenY)) 53 | { 54 | return false; 55 | } 56 | 57 | projected = new Point(screenX, screenY); 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Presenters/WritableBitmapFramePresenter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Avalonia; 4 | using Avalonia.Media; 5 | using Avalonia.Media.Imaging; 6 | using Avalonia.Platform; 7 | using Minecraftonia.Rendering.Core; 8 | 9 | namespace Minecraftonia.Rendering.Avalonia.Presenters; 10 | 11 | public sealed class WritableBitmapFramePresenter : IVoxelFramePresenter 12 | { 13 | private WriteableBitmap? _bitmap; 14 | private VoxelSize _size; 15 | 16 | public void Render(DrawingContext context, IVoxelFrameBuffer framebuffer, Rect destination) 17 | { 18 | if (destination.Width <= 0 || destination.Height <= 0) 19 | { 20 | return; 21 | } 22 | 23 | EnsureBitmap(framebuffer.Size); 24 | if (_bitmap is null) 25 | { 26 | return; 27 | } 28 | 29 | using (var lockResult = _bitmap.Lock()) 30 | { 31 | Marshal.Copy(framebuffer.Pixels, 0, lockResult.Address, framebuffer.Length); 32 | } 33 | 34 | var sourceRect = new Rect(0, 0, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height); 35 | context.DrawImage(_bitmap, sourceRect, destination); 36 | } 37 | 38 | public void Dispose() 39 | { 40 | _bitmap?.Dispose(); 41 | _bitmap = null; 42 | _size = default; 43 | } 44 | 45 | private void EnsureBitmap(VoxelSize size) 46 | { 47 | if (_bitmap is { } existing && existing.PixelSize.Width == size.Width && existing.PixelSize.Height == size.Height) 48 | { 49 | return; 50 | } 51 | 52 | _bitmap?.Dispose(); 53 | var pixelSize = new PixelSize(size.Width, size.Height); 54 | _bitmap = new WriteableBitmap(pixelSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul); 55 | _size = size; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/Minecraftonia.Game.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | Shared gameplay systems, simulation services, and scene orchestration for Minecraftonia titles. 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/GameSaveData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Minecraftonia.Core; 4 | 5 | namespace Minecraftonia.Game; 6 | 7 | public sealed class GameSaveData 8 | { 9 | public const int CurrentVersion = 2; 10 | 11 | public int Version { get; set; } = CurrentVersion; 12 | public int Width { get; set; } 13 | public int Height { get; set; } 14 | public int Depth { get; set; } 15 | public int WaterLevel { get; set; } 16 | public int Seed { get; set; } 17 | public byte[] Blocks { get; set; } = Array.Empty(); 18 | public PlayerSaveData Player { get; set; } = new(); 19 | public int SelectedPaletteIndex { get; set; } 20 | public TerrainGenerationMode GenerationMode { get; set; } = TerrainGenerationMode.Legacy; 21 | public bool UseOpenStreetMap { get; set; } 22 | public bool RequireOpenStreetMap { get; set; } = true; 23 | public int ChunkSizeX { get; set; } 24 | public int ChunkSizeY { get; set; } 25 | public int ChunkSizeZ { get; set; } 26 | public int ChunkCountX { get; set; } 27 | public int ChunkCountY { get; set; } 28 | public int ChunkCountZ { get; set; } 29 | public int ChunkStreamingRadius { get; set; } = 2; 30 | public List Chunks { get; set; } = new(); 31 | } 32 | 33 | public sealed class PlayerSaveData 34 | { 35 | public float X { get; set; } 36 | public float Y { get; set; } 37 | public float Z { get; set; } 38 | public float VelocityX { get; set; } 39 | public float VelocityY { get; set; } 40 | public float VelocityZ { get; set; } 41 | public float Yaw { get; set; } 42 | public float Pitch { get; set; } 43 | public bool IsOnGround { get; set; } 44 | public float EyeHeight { get; set; } = 1.62f; 45 | } 46 | 47 | public sealed class ChunkSaveData 48 | { 49 | public int X { get; set; } 50 | public int Y { get; set; } 51 | public int Z { get; set; } 52 | public byte[] Blocks { get; set; } = Array.Empty(); 53 | } 54 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/VoxelLightingMath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using Minecraftonia.VoxelEngine; 4 | 5 | namespace Minecraftonia.Rendering.Core; 6 | 7 | public static class VoxelLightingMath 8 | { 9 | public static Vector3 FaceToNormal(BlockFace face) 10 | { 11 | return face switch 12 | { 13 | BlockFace.PositiveX => Vector3.UnitX, 14 | BlockFace.NegativeX => -Vector3.UnitX, 15 | BlockFace.PositiveY => Vector3.UnitY, 16 | BlockFace.NegativeY => -Vector3.UnitY, 17 | BlockFace.PositiveZ => Vector3.UnitZ, 18 | BlockFace.NegativeZ => -Vector3.UnitZ, 19 | _ => Vector3.UnitY 20 | }; 21 | } 22 | 23 | public static float GetFaceLight(BlockFace face) 24 | { 25 | return face switch 26 | { 27 | BlockFace.PositiveY => 1.0f, 28 | BlockFace.NegativeY => 0.55f, 29 | BlockFace.PositiveX => 0.9f, 30 | BlockFace.NegativeX => 0.75f, 31 | BlockFace.PositiveZ => 0.85f, 32 | BlockFace.NegativeZ => 0.7f, 33 | _ => 1f 34 | }; 35 | } 36 | 37 | public static Vector2 ComputeFaceUv(BlockFace face, Vector3 local) 38 | { 39 | local = new Vector3( 40 | Math.Clamp(local.X, 0f, 0.999f), 41 | Math.Clamp(local.Y, 0f, 0.999f), 42 | Math.Clamp(local.Z, 0f, 0.999f)); 43 | 44 | return face switch 45 | { 46 | BlockFace.PositiveX => new Vector2(1f - local.Z, 1f - local.Y), 47 | BlockFace.NegativeX => new Vector2(local.Z, 1f - local.Y), 48 | BlockFace.PositiveZ => new Vector2(local.X, 1f - local.Y), 49 | BlockFace.NegativeZ => new Vector2(1f - local.X, 1f - local.Y), 50 | BlockFace.PositiveY => new Vector2(local.X, local.Z), 51 | BlockFace.NegativeY => new Vector2(local.X, 1f - local.Z), 52 | _ => new Vector2(local.X, local.Y) 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Avalonia/Minecraftonia.Sample.Doom.Avalonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net9.0 5 | enable 6 | true 7 | app.manifest 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | None 20 | All 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior.Architecture/ArchitectureSymbolSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Minecraftonia.MarkovJunior; 3 | 4 | namespace Minecraftonia.MarkovJunior.Architecture; 5 | 6 | public static class ArchitectureSymbolSet 7 | { 8 | public static MarkovSymbol Floor { get; } = new MarkovSymbol("floor", paletteIndex: 1).WithTags("structure", "walkable"); 9 | public static MarkovSymbol Wall { get; } = new MarkovSymbol("wall", paletteIndex: 2).WithTags("structure", "wall"); 10 | public static MarkovSymbol Pillar { get; } = new MarkovSymbol("pillar", paletteIndex: 3).WithTags("structure", "support"); 11 | public static MarkovSymbol Doorway { get; } = new MarkovSymbol("doorway", paletteIndex: 4).WithTags("structure", "opening"); 12 | public static MarkovSymbol Window { get; } = new MarkovSymbol("window", paletteIndex: 5).WithTags("structure", "opening", "window"); 13 | public static MarkovSymbol Street { get; } = new MarkovSymbol("street", paletteIndex: 6).WithTags("street", "walkable"); 14 | public static MarkovSymbol Plaza { get; } = new MarkovSymbol("plaza", paletteIndex: 7).WithTags("street", "plaza"); 15 | public static MarkovSymbol Garden { get; } = new MarkovSymbol("garden", paletteIndex: 8).WithTags("vegetation", "decor"); 16 | public static MarkovSymbol Stair { get; } = new MarkovSymbol("stair", paletteIndex: 9).WithTags("structure", "stairs", "walkable"); 17 | public static MarkovSymbol Altar { get; } = new MarkovSymbol("altar", paletteIndex: 10).WithTags("structure", "sacred"); 18 | public static MarkovSymbol Roof { get; } = new MarkovSymbol("roof", paletteIndex: 11).WithTags("structure", "roof"); 19 | public static MarkovSymbol Stall { get; } = new MarkovSymbol("stall", paletteIndex: 12).WithTags("market", "structure"); 20 | public static MarkovSymbol Empty { get; } = new MarkovSymbol("empty", paletteIndex: 0); 21 | 22 | public static IReadOnlyList All { get; } = new[] 23 | { 24 | Floor, 25 | Wall, 26 | Pillar, 27 | Doorway, 28 | Window, 29 | Street, 30 | Plaza, 31 | Garden, 32 | Stair, 33 | Altar, 34 | Roof, 35 | Stall, 36 | Empty 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/GameControlConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Minecraftonia.Content; 4 | using Minecraftonia.Core; 5 | using Minecraftonia.Hosting.Avalonia; 6 | using Minecraftonia.Rendering.Avalonia; 7 | using Minecraftonia.Rendering.Avalonia.Presenters; 8 | using Minecraftonia.Rendering.Core; 9 | using Minecraftonia.Rendering.Pipelines; 10 | 11 | namespace Minecraftonia.Game; 12 | 13 | public sealed record GameControlConfiguration( 14 | RenderingConfiguration Rendering, 15 | BlockTextures Textures, 16 | MinecraftoniaWorldConfig WorldConfig, 17 | GlobalIlluminationSettings GlobalIllumination, 18 | VoxelSize RenderSize, 19 | FramePresentationMode InitialPresentationMode, 20 | GameInputConfiguration Input, 21 | IGameSaveService SaveService) 22 | { 23 | public static GameControlConfiguration CreateDefault() 24 | { 25 | var textures = new BlockTextures(); 26 | var worldConfig = MinecraftoniaWorldConfig.FromDimensions( 27 | 96, 28 | 48, 29 | 96, 30 | waterLevel: 8, 31 | seed: 1337); 32 | 33 | var rendering = new RenderingConfiguration( 34 | new VoxelRayTracerFactory(), 35 | new DefaultVoxelFramePresenterFactory(), 36 | textures); 37 | 38 | var globalIllumination = GlobalIlluminationSettings.Default with 39 | { 40 | DiffuseSampleCount = 5, 41 | MaxDistance = 22f, 42 | Strength = 1.05f, 43 | AmbientLight = new System.Numerics.Vector3(0.18f, 0.21f, 0.26f), 44 | SunShadowSoftness = 0.58f, 45 | Enabled = false 46 | }; 47 | 48 | var input = new GameInputConfiguration( 49 | topLevel => new KeyboardInputSource(topLevel), 50 | (topLevel, control) => new PointerInputSource(topLevel, control)); 51 | 52 | var saveService = new FileGameSaveService(); 53 | 54 | return new GameControlConfiguration( 55 | rendering, 56 | textures, 57 | worldConfig, 58 | globalIllumination, 59 | new VoxelSize(360, 202), 60 | FramePresentationMode.SkiaTexture, 61 | input, 62 | saveService); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Minecraftonia/Minecraftonia.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net9.0 5 | enable 6 | true 7 | app.manifest 8 | true 9 | true 10 | false 11 | 12 | 13 | 14 | true 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | None 26 | All 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Core/VoxelFrameBuffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Rendering.Core; 4 | 5 | public sealed class VoxelFrameBuffer : IVoxelFrameBuffer 6 | { 7 | private byte[] _pixels; 8 | private bool _disposed; 9 | 10 | public VoxelFrameBuffer(VoxelSize size) 11 | { 12 | EnsureValid(size); 13 | Size = size; 14 | Stride = size.Width * 4; 15 | _pixels = new byte[Stride * size.Height]; 16 | } 17 | 18 | public VoxelSize Size { get; private set; } 19 | 20 | public int Stride { get; private set; } 21 | 22 | public int Length => Stride * Size.Height; 23 | 24 | public bool IsDisposed => _disposed; 25 | 26 | public Span Span 27 | { 28 | get 29 | { 30 | ThrowIfDisposed(); 31 | return _pixels.AsSpan(0, Length); 32 | } 33 | } 34 | 35 | public ReadOnlySpan ReadOnlySpan 36 | { 37 | get 38 | { 39 | ThrowIfDisposed(); 40 | return _pixels.AsSpan(0, Length); 41 | } 42 | } 43 | 44 | public byte[] Pixels 45 | { 46 | get 47 | { 48 | ThrowIfDisposed(); 49 | return _pixels; 50 | } 51 | } 52 | 53 | public void Resize(VoxelSize size) 54 | { 55 | ThrowIfDisposed(); 56 | 57 | EnsureValid(size); 58 | Size = size; 59 | Stride = size.Width * 4; 60 | int required = Stride * size.Height; 61 | if (_pixels.Length < required) 62 | { 63 | _pixels = new byte[required]; 64 | } 65 | } 66 | 67 | public void Dispose() 68 | { 69 | if (_disposed) 70 | { 71 | return; 72 | } 73 | 74 | _pixels = Array.Empty(); 75 | Size = default; 76 | Stride = 0; 77 | _disposed = true; 78 | } 79 | 80 | private static void EnsureValid(VoxelSize size) 81 | { 82 | if (size.Width <= 0 || size.Height <= 0) 83 | { 84 | throw new ArgumentOutOfRangeException(nameof(size), "Dimensions must be positive."); 85 | } 86 | } 87 | 88 | private void ThrowIfDisposed() 89 | { 90 | if (_disposed) 91 | { 92 | throw new ObjectDisposedException(nameof(VoxelFrameBuffer)); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Minecraftonia.Game/FileGameSaveService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | 5 | namespace Minecraftonia.Game; 6 | 7 | public sealed class FileGameSaveService : IGameSaveService 8 | { 9 | private const string AppFolderName = "Minecraftonia"; 10 | private const string SavesFolderName = "Saves"; 11 | private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.General) 12 | { 13 | WriteIndented = true 14 | }; 15 | 16 | public string GetSavePath(string saveName) 17 | { 18 | if (string.IsNullOrWhiteSpace(saveName)) 19 | { 20 | throw new ArgumentException("Save name must not be empty.", nameof(saveName)); 21 | } 22 | 23 | foreach (char c in Path.GetInvalidFileNameChars()) 24 | { 25 | saveName = saveName.Replace(c, '_'); 26 | } 27 | 28 | return Path.Combine(GetSavesDirectory(), saveName + ".json"); 29 | } 30 | 31 | public void Save(GameSaveData saveData, string path) 32 | { 33 | if (saveData is null) throw new ArgumentNullException(nameof(saveData)); 34 | if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path must not be empty.", nameof(path)); 35 | 36 | Directory.CreateDirectory(Path.GetDirectoryName(path)!); 37 | using FileStream stream = File.Create(path); 38 | JsonSerializer.Serialize(stream, saveData, _jsonOptions); 39 | } 40 | 41 | public GameSaveData Load(string path) 42 | { 43 | if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path must not be empty.", nameof(path)); 44 | if (!File.Exists(path)) 45 | { 46 | throw new FileNotFoundException("Save file not found.", path); 47 | } 48 | 49 | using FileStream stream = File.OpenRead(path); 50 | var save = JsonSerializer.Deserialize(stream, _jsonOptions); 51 | if (save is null) 52 | { 53 | throw new InvalidOperationException("Failed to load save file."); 54 | } 55 | 56 | return save; 57 | } 58 | 59 | private static string GetSavesDirectory() 60 | { 61 | string basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 62 | string path = Path.Combine(basePath, AppFolderName, SavesFolderName); 63 | Directory.CreateDirectory(path); 64 | return path; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/MarkovJuniorState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Minecraftonia.MarkovJunior; 5 | 6 | /// 7 | /// Maintains MarkovJunior symbols, metadata, and utility helpers for rule execution. 8 | /// 9 | public sealed class MarkovJuniorState 10 | { 11 | private readonly MarkovSymbol[,,] _grid; 12 | private readonly HashSet[,,] _cellTags; 13 | 14 | public MarkovJuniorState(int sizeX, int sizeY, int sizeZ, MarkovSymbol emptySymbol) 15 | { 16 | if (sizeX <= 0 || sizeY <= 0 || sizeZ <= 0) 17 | { 18 | throw new ArgumentOutOfRangeException(nameof(sizeX), "Grid dimensions must be positive."); 19 | } 20 | 21 | EmptySymbol = emptySymbol ?? throw new ArgumentNullException(nameof(emptySymbol)); 22 | SizeX = sizeX; 23 | SizeY = sizeY; 24 | SizeZ = sizeZ; 25 | _grid = new MarkovSymbol[sizeX, sizeY, sizeZ]; 26 | _cellTags = new HashSet[sizeX, sizeY, sizeZ]; 27 | 28 | for (int x = 0; x < sizeX; x++) 29 | { 30 | for (int y = 0; y < sizeY; y++) 31 | { 32 | for (int z = 0; z < sizeZ; z++) 33 | { 34 | _grid[x, y, z] = EmptySymbol; 35 | _cellTags[x, y, z] = new HashSet(StringComparer.OrdinalIgnoreCase); 36 | } 37 | } 38 | } 39 | } 40 | 41 | public int SizeX { get; } 42 | public int SizeY { get; } 43 | public int SizeZ { get; } 44 | public MarkovSymbol EmptySymbol { get; } 45 | 46 | public MarkovSymbol GetSymbol(int x, int y, int z) => _grid[x, y, z]; 47 | 48 | public void SetSymbol(int x, int y, int z, MarkovSymbol symbol) 49 | { 50 | _grid[x, y, z] = symbol ?? EmptySymbol; 51 | } 52 | 53 | public IReadOnlyCollection GetCellTags(int x, int y, int z) => _cellTags[x, y, z]; 54 | 55 | public void AddCellTag(int x, int y, int z, string tag) 56 | { 57 | if (!string.IsNullOrWhiteSpace(tag)) 58 | { 59 | _cellTags[x, y, z].Add(tag.ToLowerInvariant()); 60 | } 61 | } 62 | 63 | public bool ContainsCellTag(int x, int y, int z, string tag) => _cellTags[x, y, z].Contains(tag); 64 | 65 | public bool InBounds(int x, int y, int z) 66 | { 67 | return x >= 0 && x < SizeX 68 | && y >= 0 && y < SizeY 69 | && z >= 0 && z < SizeZ; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Minecraftonia.Core/MinecraftoniaWorldConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.Core; 4 | 5 | public sealed class MinecraftoniaWorldConfig 6 | { 7 | public int ChunkSizeX { get; init; } = 16; 8 | public int ChunkSizeY { get; init; } = 16; 9 | public int ChunkSizeZ { get; init; } = 16; 10 | 11 | public int ChunkCountX { get; init; } = 6; 12 | public int ChunkCountY { get; init; } = 3; 13 | public int ChunkCountZ { get; init; } = 6; 14 | 15 | public int WaterLevel { get; init; } = 8; 16 | public int Seed { get; init; } = 1337; 17 | public TerrainGenerationMode GenerationMode { get; init; } = TerrainGenerationMode.Legacy; 18 | public bool UseOpenStreetMap { get; init; } = true; 19 | public bool RequireOpenStreetMap { get; init; } = true; 20 | 21 | public int Width => ChunkSizeX * ChunkCountX; 22 | public int Height => ChunkSizeY * ChunkCountY; 23 | public int Depth => ChunkSizeZ * ChunkCountZ; 24 | 25 | public static MinecraftoniaWorldConfig FromDimensions( 26 | int width, 27 | int height, 28 | int depth, 29 | int waterLevel, 30 | int seed, 31 | int chunkSizeX = 16, 32 | int chunkSizeY = 16, 33 | int chunkSizeZ = 16, 34 | bool useOpenStreetMap = true, 35 | bool requireOpenStreetMap = true) 36 | { 37 | if (width % chunkSizeX != 0) 38 | { 39 | throw new ArgumentException($"Width {width} must be divisible by chunk size X {chunkSizeX}.", nameof(width)); 40 | } 41 | 42 | if (height % chunkSizeY != 0) 43 | { 44 | throw new ArgumentException($"Height {height} must be divisible by chunk size Y {chunkSizeY}.", nameof(height)); 45 | } 46 | 47 | if (depth % chunkSizeZ != 0) 48 | { 49 | throw new ArgumentException($"Depth {depth} must be divisible by chunk size Z {chunkSizeZ}.", nameof(depth)); 50 | } 51 | 52 | return new MinecraftoniaWorldConfig 53 | { 54 | ChunkSizeX = chunkSizeX, 55 | ChunkSizeY = chunkSizeY, 56 | ChunkSizeZ = chunkSizeZ, 57 | ChunkCountX = width / chunkSizeX, 58 | ChunkCountY = height / chunkSizeY, 59 | ChunkCountZ = depth / chunkSizeZ, 60 | WaterLevel = waterLevel, 61 | Seed = seed, 62 | GenerationMode = TerrainGenerationMode.Legacy, 63 | UseOpenStreetMap = useOpenStreetMap, 64 | RequireOpenStreetMap = requireOpenStreetMap 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Input/MouseCursorUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Avalonia; 4 | 5 | namespace Minecraftonia.Rendering.Avalonia.Input; 6 | 7 | public static partial class MouseCursorUtils 8 | { 9 | public static bool TryWarpPointer(PixelPoint screenPoint) 10 | { 11 | if (OperatingSystem.IsWindows()) 12 | { 13 | return SetCursorPos(screenPoint.X, screenPoint.Y); 14 | } 15 | 16 | if (OperatingSystem.IsMacOS()) 17 | { 18 | return TryWarpMac(screenPoint); 19 | } 20 | 21 | return false; 22 | } 23 | 24 | [LibraryImport("user32.dll", SetLastError = true)] 25 | [return: MarshalAs(UnmanagedType.Bool)] 26 | private static partial bool SetCursorPos(int x, int y); 27 | 28 | private static bool TryWarpMac(PixelPoint screenPoint) 29 | { 30 | var displayId = CGMainDisplayID(); 31 | if (displayId == IntPtr.Zero) 32 | { 33 | return false; 34 | } 35 | 36 | var bounds = CGDisplayBounds(displayId); 37 | double targetX = screenPoint.X; 38 | double targetY = bounds.Size.Height - screenPoint.Y; 39 | var target = new CGPoint { X = targetX, Y = targetY }; 40 | 41 | CGWarpMouseCursorPosition(target); 42 | CGAssociateMouseAndMouseCursorPosition(true); 43 | return true; 44 | } 45 | 46 | [LibraryImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] 47 | private static partial void CGWarpMouseCursorPosition(CGPoint position); 48 | 49 | [LibraryImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] 50 | private static partial void CGAssociateMouseAndMouseCursorPosition([MarshalAs(UnmanagedType.Bool)] bool connected); 51 | 52 | [LibraryImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] 53 | private static partial nint CGMainDisplayID(); 54 | 55 | [LibraryImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")] 56 | private static partial CGRect CGDisplayBounds(nint display); 57 | 58 | [StructLayout(LayoutKind.Sequential)] 59 | private struct CGPoint 60 | { 61 | public double X; 62 | public double Y; 63 | } 64 | 65 | [StructLayout(LayoutKind.Sequential)] 66 | private struct CGSize 67 | { 68 | public double Width; 69 | public double Height; 70 | } 71 | 72 | [StructLayout(LayoutKind.Sequential)] 73 | private struct CGRect 74 | { 75 | public CGPoint Origin; 76 | public CGSize Size; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior.Architecture/ArchitectureClusterContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Minecraftonia.WaveFunctionCollapse; 3 | 4 | namespace Minecraftonia.MarkovJunior.Architecture; 5 | 6 | public readonly struct ArchitectureClusterContext 7 | { 8 | public ArchitectureClusterContext(SettlementCluster cluster, int tileSizeX, int tileSizeZ) 9 | { 10 | Cluster = cluster ?? throw new ArgumentNullException(nameof(cluster)); 11 | TileSizeX = tileSizeX; 12 | TileSizeZ = tileSizeZ; 13 | TileCountX = cluster.Width; 14 | TileCountZ = cluster.Depth; 15 | 16 | if (Cluster.Area == 0 || TileCountX == 0 || TileCountZ == 0) 17 | { 18 | LayoutWidth = 0; 19 | LayoutDepth = 0; 20 | OriginGridX = Cluster.MinX; 21 | OriginGridZ = Cluster.MinZ; 22 | TileMask = new bool[0, 0]; 23 | return; 24 | } 25 | 26 | LayoutWidth = TileCountX * TileSizeX; 27 | LayoutDepth = TileCountZ * TileSizeZ; 28 | OriginGridX = Cluster.MinX; 29 | OriginGridZ = Cluster.MinZ; 30 | TileMask = new bool[TileCountX, TileCountZ]; 31 | 32 | foreach (var (x, z) in cluster.Cells) 33 | { 34 | int localX = x - cluster.MinX; 35 | int localZ = z - cluster.MinZ; 36 | if (localX < 0 || localZ < 0 || localX >= TileCountX || localZ >= TileCountZ) 37 | { 38 | continue; 39 | } 40 | 41 | TileMask[localX, localZ] = true; 42 | } 43 | } 44 | 45 | public SettlementCluster Cluster { get; } 46 | public int TileSizeX { get; } 47 | public int TileSizeZ { get; } 48 | public int TileCountX { get; } 49 | public int TileCountZ { get; } 50 | public int LayoutWidth { get; } 51 | public int LayoutDepth { get; } 52 | public int OriginGridX { get; } 53 | public int OriginGridZ { get; } 54 | public bool[,] TileMask { get; } 55 | 56 | public bool IsTileOccupied(int tileX, int tileZ) 57 | { 58 | if (TileMask.GetLength(0) == 0 || TileMask.GetLength(1) == 0) 59 | { 60 | return false; 61 | } 62 | 63 | if (tileX < 0 || tileZ < 0 || tileX >= TileMask.GetLength(0) || tileZ >= TileMask.GetLength(1)) 64 | { 65 | return false; 66 | } 67 | 68 | return TileMask[tileX, tileZ]; 69 | } 70 | 71 | public bool IsInsideCluster(int localX, int localZ) 72 | { 73 | if (LayoutWidth == 0 || LayoutDepth == 0) 74 | { 75 | return false; 76 | } 77 | 78 | if (localX < 0 || localZ < 0 || localX >= LayoutWidth || localZ >= LayoutDepth) 79 | { 80 | return false; 81 | } 82 | 83 | int tileX = localX / TileSizeX; 84 | int tileZ = localZ / TileSizeZ; 85 | return IsTileOccupied(tileX, tileZ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/Rules/PatternStampRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.MarkovJunior.Rules; 4 | 5 | /// 6 | /// Stamps a rectangular prism of symbols when a predicate matches the anchor cell. 7 | /// 8 | public sealed class PatternStampRule : MarkovRule 9 | { 10 | private readonly MarkovSymbol[,,] _pattern; 11 | private readonly Func _predicate; 12 | private readonly int _offsetY; 13 | 14 | public PatternStampRule( 15 | string name, 16 | MarkovSymbol[,,] pattern, 17 | Func predicate, 18 | int offsetY = 0) 19 | : base(name) 20 | { 21 | _pattern = pattern ?? throw new ArgumentNullException(nameof(pattern)); 22 | _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); 23 | _offsetY = offsetY; 24 | } 25 | 26 | public override bool Apply(MarkovJuniorState state, Random random) 27 | { 28 | bool changed = false; 29 | int sizeX = _pattern.GetLength(0); 30 | int sizeY = _pattern.GetLength(1); 31 | int sizeZ = _pattern.GetLength(2); 32 | 33 | for (int x = 0; x < state.SizeX - sizeX + 1; x++) 34 | { 35 | for (int z = 0; z < state.SizeZ - sizeZ + 1; z++) 36 | { 37 | int anchorY = Math.Clamp(_offsetY, 0, state.SizeY - sizeY); 38 | if (!_predicate(state, x, anchorY, z)) 39 | { 40 | continue; 41 | } 42 | 43 | for (int px = 0; px < sizeX; px++) 44 | { 45 | for (int py = 0; py < sizeY; py++) 46 | { 47 | for (int pz = 0; pz < sizeZ; pz++) 48 | { 49 | var symbol = _pattern[px, py, pz]; 50 | if (symbol == null) 51 | { 52 | continue; 53 | } 54 | 55 | int worldX = x + px; 56 | int worldY = anchorY + py; 57 | int worldZ = z + pz; 58 | 59 | if (!state.InBounds(worldX, worldY, worldZ)) 60 | { 61 | continue; 62 | } 63 | 64 | if (symbol == state.EmptySymbol) 65 | { 66 | continue; 67 | } 68 | 69 | if (state.GetSymbol(worldX, worldY, worldZ) == state.EmptySymbol) 70 | { 71 | state.SetSymbol(worldX, worldY, worldZ, symbol); 72 | changed = true; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | return changed; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior.Architecture/ArchitectureDebugExporter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Minecraftonia.MarkovJunior; 5 | using Minecraftonia.WaveFunctionCollapse; 6 | 7 | namespace Minecraftonia.MarkovJunior.Architecture; 8 | 9 | internal static class ArchitectureDebugExporter 10 | { 11 | public static void ExportCluster( 12 | MacroBlueprint blueprint, 13 | MarkovJuniorState state, 14 | SettlementCluster cluster, 15 | ArchitectureClusterContext context, 16 | int originX, 17 | int originZ, 18 | string? source = null) 19 | { 20 | var flag = Environment.GetEnvironmentVariable("MINECRAFTONIA_ARCH_DEBUG"); 21 | if (!string.Equals(flag, "1", StringComparison.OrdinalIgnoreCase)) 22 | { 23 | return; 24 | } 25 | 26 | Directory.CreateDirectory(Path.Combine("docs", "debug")); 27 | var path = Path.Combine("docs", "debug", "architecture.txt"); 28 | using var writer = new StreamWriter(path, append: true, Encoding.UTF8); 29 | writer.WriteLine($"# Cluster {cluster.Id} ({cluster.ModuleType}) origin=({originX},{originZ}) tiles={cluster.Area}"); 30 | if (!string.IsNullOrWhiteSpace(source)) 31 | { 32 | writer.WriteLine($"# Source: {source}"); 33 | } 34 | writer.WriteLine($"# Bounds grid=({cluster.MinX},{cluster.MinZ})-({cluster.MaxX},{cluster.MaxZ}) layout={state.SizeX}x{state.SizeZ}"); 35 | 36 | writer.WriteLine("Cluster Mask:"); 37 | for (int tileZ = 0; tileZ < context.TileCountZ; tileZ++) 38 | { 39 | var line = new StringBuilder(); 40 | for (int tileX = 0; tileX < context.TileCountX; tileX++) 41 | { 42 | line.Append(context.IsTileOccupied(tileX, tileZ) ? '#' : '.'); 43 | } 44 | writer.WriteLine(line.ToString()); 45 | } 46 | 47 | writer.WriteLine("Layout Symbols:"); 48 | for (int z = 0; z < state.SizeZ; z++) 49 | { 50 | var line = new StringBuilder(); 51 | for (int x = 0; x < state.SizeX; x++) 52 | { 53 | var symbol = state.GetSymbol(x, 0, z); 54 | bool multiLevel = state.ContainsCellTag(x, 0, z, ArchitectureRuleSet.MultiLevelTag); 55 | bool canopy = state.ContainsCellTag(x, 0, z, "market_canopy"); 56 | 57 | char c = symbol.Id switch 58 | { 59 | "street" => '=', 60 | "plaza" => 'P', 61 | "garden" => 'g', 62 | "floor" => multiLevel ? 'H' : 'F', 63 | "doorway" => 'D', 64 | "window" => 'W', 65 | "pillar" => '+', 66 | "stair" => 'S', 67 | "stall" => canopy ? 'c' : 'm', 68 | "roof" => 'R', 69 | "altar" => 'A', 70 | _ => '.' 71 | }; 72 | line.Append(c); 73 | } 74 | writer.WriteLine(line.ToString()); 75 | } 76 | 77 | writer.WriteLine(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Minecraftonia.Content; 4 | using Minecraftonia.Core; 5 | using Minecraftonia.Rendering.Core; 6 | using Minecraftonia.Rendering.Pipelines; 7 | using Minecraftonia.VoxelEngine; 8 | using Minecraftonia.Sample.Doom.Core; 9 | 10 | namespace Minecraftonia.Sample.Doom; 11 | 12 | internal static class Program 13 | { 14 | private const string OutputFileName = "doom-frame.ppm"; 15 | 16 | public static int Main() 17 | { 18 | var textures = new BlockTextures(); 19 | var world = new DoomVoxelWorld(); 20 | world.PreloadAllChunks(); 21 | 22 | var player = new Player 23 | { 24 | Position = new System.Numerics.Vector3(DoomVoxelWorld.MapWidth / 2f, 1.0f, 5.5f), 25 | EyeHeight = 1.6f, 26 | Yaw = 0f, 27 | Pitch = -4f 28 | }; 29 | 30 | var renderOptions = new VoxelRendererOptions( 31 | new VoxelSize(320, 200), 32 | 65f, 33 | block => block.IsSolid(), 34 | block => block == BlockType.Air, 35 | SamplesPerPixel: 1, 36 | EnableFxaa: true, 37 | EnableSharpen: true, 38 | GlobalIllumination: GlobalIlluminationSettings.Default with { Enabled = false }); 39 | 40 | var renderer = new VoxelRayTracerFactory().Create(renderOptions); 41 | var result = renderer.Render(world, player, textures); 42 | 43 | try 44 | { 45 | WriteFrameAsPpm(result.Framebuffer, OutputFileName); 46 | } 47 | finally 48 | { 49 | result.Framebuffer.Dispose(); 50 | } 51 | 52 | Console.WriteLine($"Rendered Doom-inspired voxel hall to {Path.GetFullPath(OutputFileName)}"); 53 | return 0; 54 | } 55 | 56 | private static void WriteFrameAsPpm(IVoxelFrameBuffer framebuffer, string path) 57 | { 58 | var directory = Path.GetDirectoryName(Path.GetFullPath(path)); 59 | if (!string.IsNullOrEmpty(directory)) 60 | { 61 | Directory.CreateDirectory(directory); 62 | } 63 | 64 | using var stream = File.Create(path); 65 | using var writer = new BinaryWriter(stream); 66 | int width = framebuffer.Size.Width; 67 | int height = framebuffer.Size.Height; 68 | writer.Write(System.Text.Encoding.ASCII.GetBytes($"P6\n{width} {height}\n255\n")); 69 | 70 | var span = framebuffer.ReadOnlySpan; 71 | for (int i = 0; i < span.Length; i += 4) 72 | { 73 | byte b = span[i]; 74 | byte g = span[i + 1]; 75 | byte r = span[i + 2]; 76 | byte a = span[i + 3]; 77 | 78 | if (a > 0 && a < 255) 79 | { 80 | float alpha = a / 255f; 81 | if (alpha > 0f) 82 | { 83 | r = (byte)Math.Clamp((int)MathF.Round(r / alpha), 0, 255); 84 | g = (byte)Math.Clamp((int)MathF.Round(g / alpha), 0, 255); 85 | b = (byte)Math.Clamp((int)MathF.Round(b / alpha), 0, 255); 86 | } 87 | } 88 | 89 | writer.Write(r); 90 | writer.Write(g); 91 | writer.Write(b); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Minecraftonia.VoxelEngine/VoxelChunk.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Minecraftonia.VoxelEngine; 6 | 7 | public sealed class VoxelChunk 8 | { 9 | private readonly TBlock[] _blocks; 10 | private int _solidCount; 11 | private static readonly EqualityComparer Comparer = EqualityComparer.Default; 12 | 13 | public VoxelChunk(ChunkCoordinate coordinate, ChunkDimensions dimensions) 14 | { 15 | Coordinate = coordinate; 16 | Dimensions = dimensions; 17 | _blocks = new TBlock[dimensions.Volume]; 18 | _solidCount = 0; 19 | } 20 | 21 | public ChunkCoordinate Coordinate { get; } 22 | public ChunkDimensions Dimensions { get; } 23 | 24 | public bool IsPopulated { get; private set; } 25 | public bool IsDirty { get; private set; } 26 | public bool IsEmpty => _solidCount == 0; 27 | 28 | public Span DataSpan => _blocks.AsSpan(); 29 | public ReadOnlySpan DataReadOnlySpan => _blocks.AsSpan(); 30 | internal TBlock[] RawBlocks => _blocks; 31 | 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | private int Index(int x, int y, int z) 34 | { 35 | return (y * Dimensions.SizeZ + z) * Dimensions.SizeX + x; 36 | } 37 | 38 | public TBlock GetBlock(int x, int y, int z) 39 | { 40 | return _blocks[Index(x, y, z)]; 41 | } 42 | 43 | public void SetBlock(int x, int y, int z, TBlock value, bool markDirty) 44 | { 45 | int index = Index(x, y, z); 46 | var previous = _blocks[index]; 47 | _blocks[index] = value; 48 | UpdateSolidCount(previous, value); 49 | if (markDirty) 50 | { 51 | IsDirty = true; 52 | } 53 | } 54 | 55 | public void MarkDirty() 56 | { 57 | IsDirty = true; 58 | } 59 | 60 | public void ClearDirty() 61 | { 62 | IsDirty = false; 63 | } 64 | 65 | public void MarkPopulated() 66 | { 67 | IsPopulated = true; 68 | } 69 | 70 | public void CopyTo(Span destination) 71 | { 72 | if (destination.Length != _blocks.Length) 73 | { 74 | throw new ArgumentException($"Destination length {destination.Length} does not match chunk volume {_blocks.Length}.", nameof(destination)); 75 | } 76 | 77 | _blocks.AsSpan().CopyTo(destination); 78 | } 79 | 80 | internal void RecalculateOccupancy() 81 | { 82 | _solidCount = 0; 83 | foreach (var block in _blocks) 84 | { 85 | if (!IsEmptyValue(block)) 86 | { 87 | _solidCount++; 88 | } 89 | } 90 | } 91 | 92 | private static bool IsEmptyValue(TBlock value) 93 | { 94 | return Comparer.Equals(value, default!); 95 | } 96 | 97 | private void UpdateSolidCount(TBlock previous, TBlock current) 98 | { 99 | bool wasSolid = !IsEmptyValue(previous); 100 | bool isSolid = !IsEmptyValue(current); 101 | if (wasSolid == isSolid) 102 | { 103 | return; 104 | } 105 | 106 | if (isSolid) 107 | { 108 | _solidCount++; 109 | } 110 | else 111 | { 112 | _solidCount--; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Minecraftonia.MarkovJunior/Rules/AdjacencyConstraintRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Minecraftonia.MarkovJunior.Rules; 4 | 5 | /// 6 | /// Ensures that cells annotated with a tag are surrounded by specified neighbour tags (soft constraint). 7 | /// 8 | public sealed class AdjacencyConstraintRule : MarkovRule 9 | { 10 | private readonly string _subjectTag; 11 | private readonly string[] _requiredNeighborTags; 12 | private readonly int _maxAttemptsPerCell; 13 | 14 | public AdjacencyConstraintRule(string name, string subjectTag, string[] requiredNeighborTags, int maxAttemptsPerCell = 3) 15 | : base(name) 16 | { 17 | _subjectTag = subjectTag ?? throw new ArgumentNullException(nameof(subjectTag)); 18 | _requiredNeighborTags = requiredNeighborTags ?? Array.Empty(); 19 | _maxAttemptsPerCell = Math.Max(1, maxAttemptsPerCell); 20 | } 21 | 22 | public override bool Apply(MarkovJuniorState state, Random random) 23 | { 24 | bool changed = false; 25 | 26 | for (int x = 0; x < state.SizeX; x++) 27 | { 28 | for (int y = 0; y < state.SizeY; y++) 29 | { 30 | for (int z = 0; z < state.SizeZ; z++) 31 | { 32 | if (!state.ContainsCellTag(x, y, z, _subjectTag)) 33 | { 34 | continue; 35 | } 36 | 37 | if (SatisfiesNeighbors(state, x, y, z)) 38 | { 39 | continue; 40 | } 41 | 42 | for (int attempt = 0; attempt < _maxAttemptsPerCell; attempt++) 43 | { 44 | int nx = x + random.Next(-1, 2); 45 | int nz = z + random.Next(-1, 2); 46 | int ny = y; 47 | if (!state.InBounds(nx, ny, nz)) 48 | { 49 | continue; 50 | } 51 | 52 | if (state.GetSymbol(nx, ny, nz) == state.EmptySymbol) 53 | { 54 | state.SetSymbol(nx, ny, nz, state.GetSymbol(x, y, z)); 55 | changed = true; 56 | break; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | return changed; 64 | } 65 | 66 | private bool SatisfiesNeighbors(MarkovJuniorState state, int x, int y, int z) 67 | { 68 | foreach (var tag in _requiredNeighborTags) 69 | { 70 | bool found = false; 71 | for (int dx = -1; dx <= 1 && !found; dx++) 72 | { 73 | for (int dz = -1; dz <= 1 && !found; dz++) 74 | { 75 | if (dx == 0 && dz == 0) 76 | { 77 | continue; 78 | } 79 | 80 | int nx = x + dx; 81 | int nz = z + dz; 82 | if (!state.InBounds(nx, y, nz)) 83 | { 84 | continue; 85 | } 86 | 87 | var symbol = state.GetSymbol(nx, y, nz); 88 | if (symbol.HasTag(tag) || state.ContainsCellTag(nx, y, nz, tag)) 89 | { 90 | found = true; 91 | } 92 | } 93 | } 94 | 95 | if (!found) 96 | { 97 | return false; 98 | } 99 | } 100 | 101 | return true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Pipelines/VoxelRaycaster.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using Minecraftonia.VoxelEngine; 4 | 5 | namespace Minecraftonia.Rendering.Pipelines; 6 | 7 | public static class VoxelRaycaster 8 | { 9 | public static bool TryPick( 10 | IVoxelWorld world, 11 | Vector3 origin, 12 | Vector3 direction, 13 | float maxDistance, 14 | Func isEmpty, 15 | out VoxelRaycastHit hit) 16 | { 17 | if (world is null) throw new ArgumentNullException(nameof(world)); 18 | if (isEmpty is null) throw new ArgumentNullException(nameof(isEmpty)); 19 | 20 | origin += direction * 0.0005f; 21 | 22 | int mapX = (int)MathF.Floor(origin.X); 23 | int mapY = (int)MathF.Floor(origin.Y); 24 | int mapZ = (int)MathF.Floor(origin.Z); 25 | 26 | float rayDirX = direction.X; 27 | float rayDirY = direction.Y; 28 | float rayDirZ = direction.Z; 29 | 30 | int stepX = rayDirX < 0 ? -1 : 1; 31 | int stepY = rayDirY < 0 ? -1 : 1; 32 | int stepZ = rayDirZ < 0 ? -1 : 1; 33 | 34 | float deltaDistX = rayDirX == 0 ? float.MaxValue : MathF.Abs(1f / rayDirX); 35 | float deltaDistY = rayDirY == 0 ? float.MaxValue : MathF.Abs(1f / rayDirY); 36 | float deltaDistZ = rayDirZ == 0 ? float.MaxValue : MathF.Abs(1f / rayDirZ); 37 | 38 | float sideDistX = rayDirX < 0 39 | ? (origin.X - mapX) * deltaDistX 40 | : (mapX + 1f - origin.X) * deltaDistX; 41 | 42 | float sideDistY = rayDirY < 0 43 | ? (origin.Y - mapY) * deltaDistY 44 | : (mapY + 1f - origin.Y) * deltaDistY; 45 | 46 | float sideDistZ = rayDirZ < 0 47 | ? (origin.Z - mapZ) * deltaDistZ 48 | : (mapZ + 1f - origin.Z) * deltaDistZ; 49 | 50 | const int maxSteps = 256; 51 | 52 | for (int step = 0; step < maxSteps; step++) 53 | { 54 | BlockFace face; 55 | float traveled; 56 | 57 | if (sideDistX < sideDistY) 58 | { 59 | if (sideDistX < sideDistZ) 60 | { 61 | mapX += stepX; 62 | traveled = sideDistX; 63 | sideDistX += deltaDistX; 64 | face = stepX > 0 ? BlockFace.NegativeX : BlockFace.PositiveX; 65 | } 66 | else 67 | { 68 | mapZ += stepZ; 69 | traveled = sideDistZ; 70 | sideDistZ += deltaDistZ; 71 | face = stepZ > 0 ? BlockFace.NegativeZ : BlockFace.PositiveZ; 72 | } 73 | } 74 | else 75 | { 76 | if (sideDistY < sideDistZ) 77 | { 78 | mapY += stepY; 79 | traveled = sideDistY; 80 | sideDistY += deltaDistY; 81 | face = stepY > 0 ? BlockFace.NegativeY : BlockFace.PositiveY; 82 | } 83 | else 84 | { 85 | mapZ += stepZ; 86 | traveled = sideDistZ; 87 | sideDistZ += deltaDistZ; 88 | face = stepZ > 0 ? BlockFace.NegativeZ : BlockFace.PositiveZ; 89 | } 90 | } 91 | 92 | if (traveled >= maxDistance) 93 | { 94 | break; 95 | } 96 | 97 | if (!world.InBounds(mapX, mapY, mapZ)) 98 | { 99 | continue; 100 | } 101 | 102 | TBlock block = world.GetBlock(mapX, mapY, mapZ); 103 | if (isEmpty(block)) 104 | { 105 | continue; 106 | } 107 | 108 | Vector3 hitPoint = origin + direction * traveled; 109 | hit = new VoxelRaycastHit(new Int3(mapX, mapY, mapZ), face, block, hitPoint, traveled); 110 | return true; 111 | } 112 | 113 | hit = default; 114 | return false; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /samples/Minecraftonia.Sample.Doom.Core/DoomVoxelWorld.cs: -------------------------------------------------------------------------------- 1 | using Minecraftonia.Core; 2 | using Minecraftonia.VoxelEngine; 3 | 4 | namespace Minecraftonia.Sample.Doom.Core; 5 | 6 | public sealed class DoomVoxelWorld : VoxelWorld 7 | { 8 | public const int MapWidth = 32; 9 | public const int MapDepth = 32; 10 | 11 | private const int LocalChunkSize = 16; 12 | private const int ChunkCountXConst = MapWidth / LocalChunkSize; 13 | private const int ChunkCountZConst = MapDepth / LocalChunkSize; 14 | private const int ChunkCountYConst = 1; 15 | private const int MapHeight = 16; 16 | 17 | private const int WallHeight = 5; 18 | private const int ColumnHeight = 4; 19 | 20 | public DoomVoxelWorld() 21 | : base(new ChunkDimensions(LocalChunkSize, MapHeight, LocalChunkSize), ChunkCountXConst, ChunkCountYConst, ChunkCountZConst) 22 | { 23 | } 24 | 25 | public void PreloadAllChunks() 26 | { 27 | var center = new ChunkCoordinate(ChunkCountX / 2, 0, ChunkCountZ / 2); 28 | int radius = Math.Max(Math.Max(ChunkCountX, ChunkCountZ), 1); 29 | EnsureChunksInRange(center, radius); 30 | } 31 | 32 | protected override void PopulateChunk(VoxelChunk chunk) 33 | { 34 | var dims = chunk.Dimensions; 35 | var coordinate = chunk.Coordinate; 36 | 37 | for (int y = 0; y < dims.SizeY; y++) 38 | { 39 | for (int z = 0; z < dims.SizeZ; z++) 40 | { 41 | for (int x = 0; x < dims.SizeX; x++) 42 | { 43 | int globalX = coordinate.X * dims.SizeX + x; 44 | int globalY = coordinate.Y * dims.SizeY + y; 45 | int globalZ = coordinate.Z * dims.SizeZ + z; 46 | 47 | var block = SampleBlock(globalX, globalY, globalZ); 48 | chunk.SetBlock(x, y, z, block, markDirty: false); 49 | } 50 | } 51 | } 52 | } 53 | 54 | private static BlockType SampleBlock(int x, int y, int z) 55 | { 56 | char cell = GetCell(x, z); 57 | 58 | if (y == 0) 59 | { 60 | return cell switch 61 | { 62 | '~' => BlockType.Sand, 63 | '.' => BlockType.Grass, 64 | 'C' => BlockType.Grass, 65 | _ => BlockType.Stone 66 | }; 67 | } 68 | 69 | if (cell == '~') 70 | { 71 | return y == 1 ? BlockType.Water : BlockType.Air; 72 | } 73 | 74 | if (cell == '#') 75 | { 76 | return y <= WallHeight ? BlockType.Stone : BlockType.Air; 77 | } 78 | 79 | if (cell == 'C') 80 | { 81 | return y <= ColumnHeight ? BlockType.Wood : BlockType.Air; 82 | } 83 | 84 | if (y == 1) 85 | { 86 | // Add a subtle trim along the floor to evoke Doom's industrial vibe. 87 | if ((x + z) % 8 == 0) 88 | { 89 | return BlockType.Wood; 90 | } 91 | } 92 | 93 | return BlockType.Air; 94 | } 95 | 96 | private static char GetCell(int x, int z) 97 | { 98 | if (x < 0 || x >= MapWidth || z < 0 || z >= MapDepth) 99 | { 100 | return '#'; 101 | } 102 | 103 | if (x <= 1 || z <= 1 || x >= MapWidth - 2 || z >= MapDepth - 2) 104 | { 105 | return '#'; 106 | } 107 | 108 | // Entry corridor leading into the hangar. 109 | if (z <= 6 && x >= MapWidth / 2 - 3 && x <= MapWidth / 2 + 3) 110 | { 111 | return '.'; 112 | } 113 | 114 | // Main hangar bounds. 115 | if (x >= 4 && x <= MapWidth - 5 && z >= 6 && z <= MapDepth - 6) 116 | { 117 | // Acid pit inspired by the classic central slime pool. 118 | if (z >= MapDepth / 2 + 2 && z <= MapDepth / 2 + 5 && x >= 10 && x <= MapWidth - 11) 119 | { 120 | return '~'; 121 | } 122 | 123 | // Support columns. 124 | if ((x % 6 == 0) && (z % 6 == 0)) 125 | { 126 | return 'C'; 127 | } 128 | 129 | // Inner wall ring. 130 | if ((x == 6 || x == MapWidth - 7) && z >= 10 && z <= MapDepth - 10) 131 | { 132 | return '#'; 133 | } 134 | 135 | return '.'; 136 | } 137 | 138 | // Side corridors. 139 | if (x >= MapWidth / 2 - 2 && x <= MapWidth / 2 + 2) 140 | { 141 | return '.'; 142 | } 143 | 144 | return '#'; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test-pack: 16 | name: Test & Pack 17 | runs-on: ubuntu-latest 18 | env: 19 | PACK_OUTPUT: artifacts/packages 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup .NET SDK 25 | uses: actions/setup-dotnet@v4 26 | with: 27 | dotnet-version: 9.0.x 28 | 29 | - name: Restore 30 | run: dotnet restore Minecraftonia.sln 31 | 32 | - name: Build 33 | run: dotnet build Minecraftonia.sln -c Release --no-restore 34 | 35 | - name: Test 36 | shell: bash 37 | run: | 38 | if dotnet sln Minecraftonia.sln list | grep -qi test; then 39 | dotnet test Minecraftonia.sln -c Release --no-build 40 | else 41 | echo "No test projects found; skipping dotnet test." 42 | fi 43 | 44 | - name: Pack libraries 45 | run: dotnet pack Minecraftonia.sln -c Release --no-build -p:ContinuousIntegrationBuild=true --output $PACK_OUTPUT 46 | 47 | - name: Upload NuGet packages 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: nuget-packages 51 | path: ${{ env.PACK_OUTPUT }} 52 | if-no-files-found: error 53 | 54 | build: 55 | name: Publish ${{ matrix.rid }} 56 | needs: test-pack 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | include: 62 | - os: ubuntu-latest 63 | rid: linux-x64 64 | zip_name: Minecraftonia-linux-x64.zip 65 | - os: macos-latest 66 | rid: osx-arm64 67 | zip_name: Minecraftonia-macos-arm64.zip 68 | - os: windows-latest 69 | rid: win-x64 70 | zip_name: Minecraftonia-windows-x64.zip 71 | env: 72 | APP_NAME: Minecraftonia 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v4 76 | 77 | - name: Setup .NET SDK 78 | uses: actions/setup-dotnet@v4 79 | with: 80 | dotnet-version: 9.0.x 81 | 82 | - name: Restore 83 | run: dotnet restore Minecraftonia.sln 84 | 85 | - name: Build solution 86 | run: dotnet build Minecraftonia.sln -c Release --no-restore 87 | 88 | - name: Build sample app 89 | run: dotnet build samples/Minecraftonia.Sample.BasicBlock/Minecraftonia.Sample.BasicBlock.csproj -c Release --no-restore 90 | 91 | - name: Publish 92 | run: dotnet publish src/Minecraftonia/Minecraftonia.csproj -c Release -r ${{ matrix.rid }} --self-contained true -o publish/${{ matrix.rid }} 93 | 94 | - name: Remove debug artifacts 95 | shell: bash 96 | run: | 97 | find publish/${{ matrix.rid }} -name '*.dSYM' -prune -exec rm -rf {} + 98 | find publish/${{ matrix.rid }} -type f \( -name '*.pdb' -o -name '*.pdf' -o -name '*.dsym' -o -name '*.dbg' \) -delete 99 | 100 | - name: Strip binaries (macOS) 101 | if: runner.os == 'macOS' 102 | run: | 103 | bin_dir="publish/${{ matrix.rid }}" 104 | if [ -f "$bin_dir/${{ env.APP_NAME }}" ]; then 105 | strip -x "$bin_dir/${{ env.APP_NAME }}" 106 | fi 107 | while IFS= read -r lib; do 108 | strip -x "$lib" 109 | done < <(find "$bin_dir" -type f -name '*.dylib') 110 | 111 | - name: Strip binaries (Linux) 112 | if: runner.os == 'Linux' 113 | run: | 114 | bin_dir="publish/${{ matrix.rid }}" 115 | if [ -f "$bin_dir/${{ env.APP_NAME }}" ]; then 116 | strip --strip-unneeded "$bin_dir/${{ env.APP_NAME }}" 117 | fi 118 | while IFS= read -r lib; do 119 | strip --strip-unneeded "$lib" 120 | done < <(find "$bin_dir" -type f -name '*.so') 121 | 122 | - name: Archive build 123 | if: runner.os != 'Windows' 124 | run: | 125 | cd publish/${{ matrix.rid }} 126 | zip -r ../${{ matrix.zip_name }} . 127 | 128 | - name: Archive build (Windows) 129 | if: runner.os == 'Windows' 130 | run: | 131 | cd publish/${{ matrix.rid }} 132 | Compress-Archive -Path * -DestinationPath ../${{ matrix.zip_name }} 133 | shell: pwsh 134 | 135 | - name: Upload artifact 136 | uses: actions/upload-artifact@v4 137 | with: 138 | name: ${{ matrix.rid }} 139 | path: publish/${{ matrix.zip_name }} 140 | if-no-files-found: error 141 | -------------------------------------------------------------------------------- /docs/refactor-plan.md: -------------------------------------------------------------------------------- 1 | # Reusable Rendering Refactor Plan 2 | 3 | ## Key Findings 4 | 5 | - `src/Minecraftonia.Game/GameControl.cs:19` currently owns input, camera, ray tracing, and frame-presenter logic, making it difficult to reuse the renderer without the rest of the game. 6 | - `src/Minecraftonia.Game/WritableBitmapFramePresenter.cs:11` and `src/Minecraftonia.Game/SkiaTextureFramePresenter.cs:11` implement generic bitmap/Skia presentation inside the game project instead of a shared UI bridge. 7 | - `src/Minecraftonia.Rendering.Core/VoxelFrameBuffer.cs:6` and related renderer types depend on `Avalonia.PixelSize`, preventing consumption from other UI stacks. 8 | - `src/Minecraftonia.Game/MinecraftoniaGame.cs:33` and `src/Minecraftonia.Game/BlockTextures.cs:10` bundle palette, materials, saves, and world config together; meanwhile `src/Minecraftonia.WaveFunctionCollapse/BlockType.cs:3` defines `BlockType`, pulling in optional systems for any consumer. 9 | 10 | ## Target Modularization 11 | 12 | - Establish `Minecraftonia.Core` for shared primitives such as block enums, world configuration, and math helpers, with no UI dependencies. 13 | - Keep `Minecraftonia.VoxelEngine` focused on simulation while depending only on the new core; expose a slim `IVoxelWorld` API for renderers and tooling. 14 | - Refactor `Minecraftonia.VoxelRendering` into a UI-agnostic library that works with framework-neutral buffer and size abstractions. 15 | - Introduce `Minecraftonia.Rendering.Avalonia` to host writable-bitmap and Skia presenters along with an Avalonia `VoxelRenderControl`. 16 | - Move reusable material and camera helpers into a dedicated shared library (e.g. `Minecraftonia.Content`), leaving `Minecraftonia.Game` with game-specific orchestration, menus, saves, and optional WFC/OSM modules. 17 | 18 | ## Phased Work Plan 19 | 20 | 1. Baseline and Core Extraction 21 | Build the current solution, then add `Minecraftonia.Core` and migrate `BlockType`, extensions, and `MinecraftoniaWorldConfig` into it with minimal disruption. 22 | 2. Renderer Decoupling 23 | Remove Avalonia-specific structs from `Minecraftonia.VoxelRendering`, replacing them with neutral abstractions and introducing renderer interfaces. 24 | 3. UI Bridges 25 | Create `Minecraftonia.Rendering.Avalonia`, move the frame presenters there, and refactor `GameControl` to compose injected renderer services. 26 | 4. Gameplay Library Split 27 | Relocate shared content helpers into the new shared library and slim `Minecraftonia.Game` to orchestrate gameplay features. 28 | 5. Cleanup and Documentation 29 | Update solution/project files, CI pipelines, and documentation; perform smoke tests for both the main game and new sample app. 30 | 31 | ## Sample App Outline 32 | 33 | - Create `samples/Minecraftonia.Sample.BasicBlock` referencing the shared core, engine, rendering, and Avalonia bridge projects. 34 | - Demonstrate writable-bitmap rendering of a single block with simple camera orbit controls and an optional toggle for the Skia pathway. 35 | - Document how the sample wires abstractions together so developers can plug in their own voxel content. 36 | 37 | ## Task Checklist 38 | 39 | 1. [x] Verify the current solution builds and the existing game runs to capture a baseline before code moves. 40 | 2. [x] Introduce a `Minecraftonia.Core` project that hosts shared primitives (block enums, world config, math helpers) with zero Avalonia dependencies. 41 | 3. [x] Relocate `BlockType`, extension helpers, and `MinecraftoniaWorldConfig` into `Minecraftonia.Core`, then update all projects to consume the new types. 42 | 4. [x] Review `Minecraftonia.VoxelEngine` so it depends only on `Minecraftonia.Core`; extract an `IVoxelWorld` surface for downstream consumers. 43 | 5. [x] Refactor `Minecraftonia.VoxelRendering` to remove Avalonia-specific structs, relying on framework-neutral buffer and size abstractions. 44 | 6. [x] Add renderer interfaces (`IVoxelFrameBuffer`, `IVoxelRenderer`, material samplers) that separate simulation data from presentation. 45 | 7. [x] Create a `Minecraftonia.Rendering.Avalonia` project containing writable-bitmap and Skia presenters plus an Avalonia `VoxelRenderControl`. 46 | 8. [x] Update `GameControl` to compose the new Avalonia presenter, delegating engine, input, and rendering responsibilities to injectable collaborators. 47 | 9. [x] Move reusable content helpers (`BlockTextures`, camera utilities) into a `Minecraftonia.Content` (or similarly named) shared library. 48 | 10. [x] Slim `Minecraftonia.Game` to game-specific orchestration (menus, saves, WFC/OSM opt-in modules) while consuming the shared libraries. 49 | 11. [x] Add a `samples/Minecraftonia.Sample.BasicBlock` project showcasing writable-bitmap rendering of a single block with simple camera controls. 50 | 12. [x] Refresh documentation and CI scripts to include the new projects, and record smoke-test notes for both the main game and sample app. 51 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Controls/VoxelOverlayRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using Avalonia; 4 | using Avalonia.Media; 5 | using Minecraftonia.VoxelEngine; 6 | 7 | namespace Minecraftonia.Rendering.Avalonia.Controls; 8 | 9 | public static class VoxelOverlayRenderer 10 | { 11 | public static void DrawCrosshair(DrawingContext context, Size viewport, Pen? pen = null, double length = 9d) 12 | { 13 | if (viewport.Width <= 0 || viewport.Height <= 0) 14 | { 15 | return; 16 | } 17 | 18 | pen ??= new Pen(Brushes.White, 1); 19 | var center = new Point(viewport.Width / 2d, viewport.Height / 2d); 20 | context.DrawLine(pen, center + new global::Avalonia.Vector(-length, 0), center + new global::Avalonia.Vector(length, 0)); 21 | context.DrawLine(pen, center + new global::Avalonia.Vector(0, -length), center + new global::Avalonia.Vector(0, length)); 22 | } 23 | 24 | public static void DrawSelection(DrawingContext context, VoxelProjector projector, VoxelRaycastHit hit) 25 | { 26 | Span corners = stackalloc Vector3[4]; 27 | GetFaceCorners(hit.Block, hit.Face, corners); 28 | 29 | var projectedPoints = new Point[4]; 30 | for (int i = 0; i < corners.Length; i++) 31 | { 32 | if (!projector.TryProject(corners[i], out var screenPoint)) 33 | { 34 | return; 35 | } 36 | 37 | projectedPoints[i] = screenPoint; 38 | } 39 | 40 | var geometry = new StreamGeometry(); 41 | using (var ctx = geometry.Open()) 42 | { 43 | ctx.BeginFigure(projectedPoints[0], true); 44 | ctx.LineTo(projectedPoints[1]); 45 | ctx.LineTo(projectedPoints[2]); 46 | ctx.LineTo(projectedPoints[3]); 47 | ctx.EndFigure(true); 48 | } 49 | 50 | double thickness = Math.Max(1.5, projector.ViewportSize.Width * 0.002); 51 | var fill = new SolidColorBrush(Color.FromArgb(40, 255, 255, 255)); 52 | var pen = new Pen(new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)), thickness); 53 | context.DrawGeometry(fill, pen, geometry); 54 | } 55 | 56 | private static void GetFaceCorners(Int3 block, BlockFace face, Span destination) 57 | { 58 | Vector3 min = block.ToVector3(); 59 | Vector3 max = min + Vector3.One; 60 | 61 | switch (face) 62 | { 63 | case BlockFace.PositiveX: 64 | destination[0] = new Vector3(max.X, min.Y, min.Z); 65 | destination[1] = new Vector3(max.X, max.Y, min.Z); 66 | destination[2] = new Vector3(max.X, max.Y, max.Z); 67 | destination[3] = new Vector3(max.X, min.Y, max.Z); 68 | break; 69 | case BlockFace.NegativeX: 70 | destination[0] = new Vector3(min.X, min.Y, min.Z); 71 | destination[1] = new Vector3(min.X, min.Y, max.Z); 72 | destination[2] = new Vector3(min.X, max.Y, max.Z); 73 | destination[3] = new Vector3(min.X, max.Y, min.Z); 74 | break; 75 | case BlockFace.PositiveY: 76 | destination[0] = new Vector3(min.X, max.Y, min.Z); 77 | destination[1] = new Vector3(max.X, max.Y, min.Z); 78 | destination[2] = new Vector3(max.X, max.Y, max.Z); 79 | destination[3] = new Vector3(min.X, max.Y, max.Z); 80 | break; 81 | case BlockFace.NegativeY: 82 | destination[0] = new Vector3(min.X, min.Y, min.Z); 83 | destination[1] = new Vector3(min.X, min.Y, max.Z); 84 | destination[2] = new Vector3(max.X, min.Y, max.Z); 85 | destination[3] = new Vector3(max.X, min.Y, min.Z); 86 | break; 87 | case BlockFace.PositiveZ: 88 | destination[0] = new Vector3(min.X, min.Y, max.Z); 89 | destination[1] = new Vector3(min.X, max.Y, max.Z); 90 | destination[2] = new Vector3(max.X, max.Y, max.Z); 91 | destination[3] = new Vector3(max.X, min.Y, max.Z); 92 | break; 93 | case BlockFace.NegativeZ: 94 | destination[0] = new Vector3(min.X, min.Y, min.Z); 95 | destination[1] = new Vector3(max.X, min.Y, min.Z); 96 | destination[2] = new Vector3(max.X, max.Y, min.Z); 97 | destination[3] = new Vector3(min.X, max.Y, min.Z); 98 | break; 99 | default: 100 | destination[0] = min; 101 | destination[1] = min; 102 | destination[2] = min; 103 | destination[3] = min; 104 | break; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Controls/VoxelRenderControl.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Input; 4 | using Avalonia.Media; 5 | using Avalonia.Threading; 6 | using Minecraftonia.Rendering.Avalonia.Presenters; 7 | using Minecraftonia.Rendering.Core; 8 | using Minecraftonia.Rendering.Pipelines; 9 | using Minecraftonia.VoxelEngine; 10 | 11 | namespace Minecraftonia.Rendering.Avalonia.Controls; 12 | 13 | public sealed class VoxelRenderControl : Control where TBlock : struct 14 | { 15 | private IVoxelFrameBuffer? _framebuffer; 16 | private VoxelCamera _camera; 17 | private bool _renderQueued; 18 | 19 | private IVoxelRenderer? _renderer; 20 | private IVoxelMaterialProvider? _materials; 21 | private IVoxelFramePresenter _framePresenter = new WritableBitmapFramePresenter(); 22 | private IVoxelWorld? _world; 23 | private Player? _player; 24 | private bool _renderContinuously = true; 25 | 26 | public VoxelRenderControl() 27 | { 28 | Focusable = true; 29 | } 30 | 31 | public IVoxelRenderer? Renderer 32 | { 33 | get => _renderer; 34 | set 35 | { 36 | if (!ReferenceEquals(_renderer, value)) 37 | { 38 | _renderer = value; 39 | QueueRender(); 40 | } 41 | } 42 | } 43 | 44 | public IVoxelMaterialProvider? Materials 45 | { 46 | get => _materials; 47 | set 48 | { 49 | if (!ReferenceEquals(_materials, value)) 50 | { 51 | _materials = value; 52 | QueueRender(); 53 | } 54 | } 55 | } 56 | 57 | public IVoxelFramePresenter FramePresenter 58 | { 59 | get => _framePresenter; 60 | set 61 | { 62 | if (!ReferenceEquals(_framePresenter, value)) 63 | { 64 | _framePresenter.Dispose(); 65 | _framePresenter = value ?? throw new ArgumentNullException(nameof(value)); 66 | QueueRender(); 67 | } 68 | } 69 | } 70 | 71 | public IVoxelWorld? World 72 | { 73 | get => _world; 74 | set 75 | { 76 | if (!ReferenceEquals(_world, value)) 77 | { 78 | _world = value; 79 | QueueRender(); 80 | } 81 | } 82 | } 83 | 84 | public Player? Player 85 | { 86 | get => _player; 87 | set 88 | { 89 | _player = value; 90 | QueueRender(); 91 | } 92 | } 93 | 94 | public bool RenderContinuously 95 | { 96 | get => _renderContinuously; 97 | set 98 | { 99 | _renderContinuously = value; 100 | if (value) 101 | { 102 | QueueRender(); 103 | } 104 | } 105 | } 106 | 107 | protected override Size ArrangeOverride(Size finalSize) 108 | { 109 | QueueRender(); 110 | return base.ArrangeOverride(finalSize); 111 | } 112 | 113 | private void QueueRender() 114 | { 115 | if (_renderQueued) 116 | { 117 | return; 118 | } 119 | 120 | _renderQueued = true; 121 | Dispatcher.UIThread.Post(RenderFrame, DispatcherPriority.Render); 122 | } 123 | 124 | private void RenderFrame() 125 | { 126 | _renderQueued = false; 127 | 128 | var renderer = _renderer; 129 | var world = _world; 130 | var materials = _materials; 131 | var player = _player; 132 | 133 | if (renderer is null || world is null || materials is null || player is null) 134 | { 135 | return; 136 | } 137 | 138 | var result = renderer.Render(world, player, materials, _framebuffer); 139 | _framebuffer = result.Framebuffer; 140 | _camera = result.Camera; 141 | 142 | InvalidateVisual(); 143 | 144 | if (RenderContinuously) 145 | { 146 | QueueRender(); 147 | } 148 | } 149 | 150 | public override void Render(DrawingContext context) 151 | { 152 | base.Render(context); 153 | 154 | if (_framebuffer is { IsDisposed: false } framebuffer) 155 | { 156 | var destRect = new Rect(Bounds.Size); 157 | if (destRect.Width <= 0 || destRect.Height <= 0) 158 | { 159 | return; 160 | } 161 | 162 | _framePresenter.Render(context, framebuffer, destRect); 163 | } 164 | } 165 | 166 | protected override void OnPointerPressed(PointerPressedEventArgs e) 167 | { 168 | base.OnPointerPressed(e); 169 | Focus(); 170 | } 171 | 172 | protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) 173 | { 174 | base.OnDetachedFromVisualTree(e); 175 | _framebuffer?.Dispose(); 176 | _framebuffer = null; 177 | _framePresenter.Dispose(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Minecraftonia.Rendering.Avalonia/Presenters/SkiaTextureFramePresenter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Media; 4 | using Avalonia.Platform; 5 | using Avalonia.Rendering.SceneGraph; 6 | using Avalonia.Skia; 7 | using Minecraftonia.Rendering.Core; 8 | using SkiaSharp; 9 | 10 | namespace Minecraftonia.Rendering.Avalonia.Presenters; 11 | 12 | public sealed class SkiaTextureFramePresenter : IVoxelFramePresenter 13 | { 14 | private GRContext? _grContext; 15 | private SKSurface? _surface; 16 | private VoxelSize _size; 17 | private readonly SKPaint _paint = new() { FilterQuality = SKFilterQuality.None, IsAntialias = false }; 18 | 19 | public void Render(DrawingContext context, IVoxelFrameBuffer framebuffer, Rect destination) 20 | { 21 | if (destination.Width <= 0 || destination.Height <= 0) 22 | { 23 | return; 24 | } 25 | 26 | context.Custom(new SkiaDrawOperation(destination, framebuffer, this)); 27 | } 28 | 29 | public void Dispose() 30 | { 31 | ReleaseSurface(); 32 | _paint.Dispose(); 33 | } 34 | 35 | private void RenderToCanvas(SKCanvas canvas, GRContext? grContext, IVoxelFrameBuffer frame, Rect destination) 36 | { 37 | var info = new SKImageInfo(frame.Size.Width, frame.Size.Height, SKColorType.Bgra8888, SKAlphaType.Premul); 38 | var target = ToSkRect(destination); 39 | 40 | unsafe 41 | { 42 | fixed (byte* src = frame.Pixels) 43 | { 44 | if (grContext is not null && EnsureSurface(grContext, frame.Size, info)) 45 | { 46 | var surface = _surface!; 47 | using var bitmap = new SKBitmap(); 48 | if (!bitmap.InstallPixels(info, (IntPtr)src, frame.Stride)) 49 | { 50 | ReleaseSurface(); 51 | } 52 | else 53 | { 54 | surface.Canvas.Clear(SKColors.Transparent); 55 | surface.Canvas.DrawBitmap(bitmap, SKRect.Create(info.Width, info.Height)); 56 | surface.Canvas.Flush(); 57 | 58 | using var image = surface.Snapshot(); 59 | canvas.DrawImage(image, target, _paint); 60 | return; 61 | } 62 | } 63 | 64 | ReleaseSurface(); 65 | using var fallback = SKImage.FromPixels(info, (IntPtr)src, frame.Stride); 66 | canvas.DrawImage(fallback, target, _paint); 67 | } 68 | } 69 | } 70 | 71 | private bool EnsureSurface(GRContext context, VoxelSize size, SKImageInfo info) 72 | { 73 | if (_surface != null) 74 | { 75 | bool contextChanged = !ReferenceEquals(_grContext, context); 76 | bool sizeChanged = _size != size; 77 | if (contextChanged || sizeChanged) 78 | { 79 | _surface.Dispose(); 80 | _surface = null; 81 | _grContext = null; 82 | } 83 | } 84 | 85 | if (_surface == null) 86 | { 87 | _surface = SKSurface.Create(context, true, info); 88 | if (_surface == null) 89 | { 90 | return false; 91 | } 92 | 93 | _grContext = context; 94 | _size = size; 95 | } 96 | 97 | return true; 98 | } 99 | 100 | private void ReleaseSurface() 101 | { 102 | _surface?.Dispose(); 103 | _surface = null; 104 | _grContext = null; 105 | _size = default; 106 | } 107 | 108 | private static SKRect ToSkRect(Rect rect) 109 | { 110 | return new SKRect( 111 | (float)rect.X, 112 | (float)rect.Y, 113 | (float)(rect.X + rect.Width), 114 | (float)(rect.Y + rect.Height)); 115 | } 116 | 117 | private sealed class SkiaDrawOperation : ICustomDrawOperation 118 | { 119 | private readonly Rect _destination; 120 | private readonly IVoxelFrameBuffer _frame; 121 | private readonly SkiaTextureFramePresenter _presenter; 122 | 123 | public SkiaDrawOperation(Rect destination, IVoxelFrameBuffer frame, SkiaTextureFramePresenter presenter) 124 | { 125 | _destination = destination; 126 | _frame = frame; 127 | _presenter = presenter; 128 | } 129 | 130 | public Rect Bounds => _destination; 131 | 132 | public void Dispose() 133 | { 134 | } 135 | 136 | public bool HitTest(Point p) => _destination.Contains(p); 137 | 138 | public void Render(ImmediateDrawingContext context) 139 | { 140 | if (_frame.IsDisposed) 141 | { 142 | return; 143 | } 144 | 145 | if (context.TryGetFeature() is not { } leaseFeature) 146 | { 147 | return; 148 | } 149 | 150 | using var lease = leaseFeature.Lease(); 151 | var canvas = lease.SkCanvas; 152 | if (canvas is null) 153 | { 154 | return; 155 | } 156 | 157 | _presenter.RenderToCanvas(canvas, lease.GrContext, _frame, _destination); 158 | } 159 | 160 | public bool Equals(ICustomDrawOperation? other) => ReferenceEquals(this, other); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Minecraftonia/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Avalonia.Controls; 4 | using Avalonia.Interactivity; 5 | using Avalonia.Threading; 6 | using Minecraftonia.Game; 7 | 8 | namespace Minecraftonia; 9 | 10 | public partial class MainWindow : Window 11 | { 12 | private const string DefaultSaveSlot = "latest"; 13 | 14 | private GameControl _gameControl = default!; 15 | private Border _mainMenuOverlay = default!; 16 | private Border _pauseMenuOverlay = default!; 17 | private TextBlock _mainMenuStatus = default!; 18 | private TextBlock _pauseMenuStatus = default!; 19 | private Button _loadButton = default!; 20 | private readonly IGameSaveService _saveService; 21 | 22 | public MainWindow() 23 | { 24 | InitializeComponent(); 25 | 26 | _gameControl = this.FindControl("GameView") ?? throw new InvalidOperationException("Game view not found."); 27 | _mainMenuOverlay = this.FindControl("MainMenuOverlay") ?? throw new InvalidOperationException("Main menu overlay not found."); 28 | _pauseMenuOverlay = this.FindControl("PauseMenuOverlay") ?? throw new InvalidOperationException("Pause menu overlay not found."); 29 | _mainMenuStatus = this.FindControl("MainMenuStatus") ?? throw new InvalidOperationException("Main menu status not found."); 30 | _pauseMenuStatus = this.FindControl("PauseMenuStatus") ?? throw new InvalidOperationException("Pause menu status not found."); 31 | _loadButton = this.FindControl