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>
138 lines
4.1 KiB
C#
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();
|
|
}
|