├── .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