acdream/src/AcDream.App/Rendering/GameWindow.cs
Erik 6a100ef6e7 refactor(app): harden shutdown per final review
Addresses final code review of phase-1 branch (Important I-1, I-3):

- Move plugin Enable() loop inside the same try block as GameWindow.Run,
  and wrap each Enable() in per-plugin try/catch mirroring the Disable
  loop. Previously, a plugin Enable() throwing would skip the finally
  block entirely: plugins that had already enabled would never get
  disabled, Serilog would never flush, and the exception would escape
  ungracefully. Now Enable failures are logged and contained, and
  shutdown always runs.
- Add a comment at the Get<LandBlock> call in GameWindow.OnLoad explaining
  why TryGet was avoided (the [MaybeNullWhen(false)] nullable-generic
  analysis trips TreatWarningsAsErrors).

I-2 (camera aspect doesn't update on window resize) deferred to Phase 2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:19 +02:00

138 lines
4.1 KiB
C#

using AcDream.Core.Terrain;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Silk.NET.Input;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace AcDream.App.Rendering;
public sealed class GameWindow : IDisposable
{
private readonly string _datDir;
private IWindow? _window;
private GL? _gl;
private IInputContext? _input;
private TerrainRenderer? _terrain;
private Shader? _shader;
private OrbitCamera? _camera;
private DatCollection? _dats;
private float _lastMouseX;
private float _lastMouseY;
public GameWindow(string datDir) => _datDir = datDir;
public void Run()
{
var options = WindowOptions.Default with
{
Size = new Vector2D<int>(1280, 720),
Title = "acdream — phase 1",
API = new GraphicsAPI(
ContextAPI.OpenGL,
ContextProfile.Core,
ContextFlags.ForwardCompatible,
new APIVersion(4, 3)),
VSync = true,
};
_window = Window.Create(options);
_window.Load += OnLoad;
_window.Render += OnRender;
_window.Closing += OnClosing;
_window.Run();
}
private void OnLoad()
{
_gl = GL.GetApi(_window!);
_input = _window!.CreateInput();
foreach (var kb in _input.Keyboards)
kb.KeyDown += (_, key, _) =>
{
if (key == Key.Escape)
_window!.Close();
};
foreach (var mouse in _input.Mice)
{
mouse.MouseMove += (m, pos) =>
{
if (m.IsButtonPressed(MouseButton.Left))
{
_camera!.Yaw -= (pos.X - _lastMouseX) * 0.005f;
_camera!.Pitch = Math.Clamp(
_camera.Pitch + (pos.Y - _lastMouseY) * 0.005f,
0.1f, 1.5f);
}
_lastMouseX = pos.X;
_lastMouseY = pos.Y;
};
mouse.Scroll += (_, scroll) =>
_camera!.Distance = Math.Clamp(_camera.Distance - scroll.Y * 20f, 50f, 2000f);
}
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
_gl.Enable(EnableCap.DepthTest);
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
_shader = new Shader(_gl,
Path.Combine(shadersDir, "terrain.vert"),
Path.Combine(shadersDir, "terrain.frag"));
_camera = new OrbitCamera
{
Aspect = _window!.Size.X / (float)_window.Size.Y,
};
_dats = new DatCollection(_datDir, DatAccessType.Read);
// Find ANY landblock ending in 0xFFFF. Holtburg 0xA9B4FFFF is a
// good default; fall back to the first one we find. Using Get<T>
// (returns null on miss) rather than TryGet to sidestep
// [MaybeNullWhen(false)] nullable-generic analysis under
// TreatWarningsAsErrors.
uint landblockId = 0xA9B4FFFFu;
var block = _dats.Get<LandBlock>(landblockId);
if (block is null)
{
foreach (var file in _dats.Cell.Tree)
{
if ((file.Id & 0xFFFFu) == 0xFFFFu)
{
landblockId = file.Id;
block = _dats.Get<LandBlock>(landblockId);
break;
}
}
}
if (block is null)
throw new InvalidOperationException("no landblock found in cell dat");
Console.WriteLine($"loaded landblock 0x{landblockId:X8}");
var meshData = LandblockMesh.Build(block);
_terrain = new TerrainRenderer(_gl, meshData, _shader);
}
private void OnRender(double deltaSeconds)
{
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_terrain?.Draw(_camera!);
}
private void OnClosing()
{
_terrain?.Dispose();
_shader?.Dispose();
_dats?.Dispose();
_input?.Dispose();
_gl?.Dispose();
}
public void Dispose() => _window?.Dispose();
}