diff --git a/Maple2.Server.DebugGame/Extensions/XmlExtensions.cs b/Maple2.Server.DebugGame/Extensions/XmlExtensions.cs new file mode 100644 index 000000000..990510ed8 --- /dev/null +++ b/Maple2.Server.DebugGame/Extensions/XmlExtensions.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; +using System.Xml; + +namespace Maple2.Server.DebugGame.Extensions; + +public static class XmlExtensions { + public static bool TryGetAttribute(this XmlElement element, string name, [NotNullWhen(returnValue: true)] out string? value) { + value = null; + + if (!element.HasAttribute(name)) { + return false; + } + + value = element.GetAttribute(name); + + return true; + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs b/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs new file mode 100644 index 000000000..510e77f6f --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs @@ -0,0 +1,5 @@ +namespace Maple2.Server.DebugGame.Graphics.Assets; + +public class Asset { + +} diff --git a/Maple2.Server.DebugGame/Graphics/Assets/AssetManager.cs b/Maple2.Server.DebugGame/Graphics/Assets/AssetManager.cs new file mode 100644 index 000000000..2ee2a5ea1 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Assets/AssetManager.cs @@ -0,0 +1,13 @@ +namespace Maple2.Server.DebugGame.Graphics.Assets; + +//public enum AssetType + +public class AssetManager { + public AssetManager? Parent { get; init; } + + public AssetManager(AssetManager? parent = null) { + Parent = parent; + } + + +} diff --git a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs index c786d89a3..77f9bf05a 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs @@ -9,15 +9,20 @@ public class CoreModels { public Mesh Quad { get; init; } public Mesh Cube { get; init; } public Mesh WireCube { get; init; } + public Mesh Cylinder { get; init; } + public Mesh WireCylinder { get; init; } public CoreModels(DebugGraphicsContext context) { Context = context; Quad = CreateQuad(); - var cubes = CreateCubes(); + (Mesh solid, Mesh wire) cubes = CreateCubes(); + (Mesh solid, Mesh wire) cylinders = CreateCylinders(); Cube = cubes.solid; WireCube = cubes.wire; + Cylinder = cylinders.solid; + WireCylinder = cylinders.wire; } private Mesh CreateQuad() { @@ -120,9 +125,9 @@ private Mesh CreateQuad() { List attributeBinding = []; for (int i = 0; i < cubeSolidIndices.Length; i += 3) { - PositionBinding vertA = cubeVertices[cubeSolidIndices[i + 0]]; - PositionBinding vertB = cubeVertices[cubeSolidIndices[i + 1]]; - PositionBinding vertC = cubeVertices[cubeSolidIndices[i + 2]]; + PositionBinding vertA = cubeVertices[cubeSolidIndices[i] + 0]; + PositionBinding vertB = cubeVertices[cubeSolidIndices[i] + 1]; + PositionBinding vertC = cubeVertices[cubeSolidIndices[i] + 2]; positionBinding.Add(vertA); positionBinding.Add(vertB); @@ -145,5 +150,109 @@ private Mesh CreateQuad() { return (cubeSolidMesh, cubeWireMesh); } + + private (Mesh solid, Mesh wire) CreateCylinders() { + const int segments = 16; // Number of segments around the cylinder + const float radius = 0.5f; + const float height = 1.0f; + + Ms2MeshData meshData = new Ms2MeshData { + PrimitiveCount = 2, + }; + + List vertices = []; + List wireIndices = []; + List solidIndices = []; + + // Create vertices for bottom circle (y = 0) + for (int i = 0; i < segments; i++) { + float angle = (float) (2 * Math.PI * i / segments); + float x = radius * MathF.Cos(angle); + float z = radius * MathF.Sin(angle); + vertices.Add(new PositionBinding(new Vector3(x, 0, z))); + } + + // Create vertices for top circle (y = height) + for (int i = 0; i < segments; i++) { + float angle = (float) (2 * Math.PI * i / segments); + float x = radius * MathF.Cos(angle); + float z = radius * MathF.Sin(angle); + vertices.Add(new PositionBinding(new Vector3(x, height, z))); + } + + // Add center vertices for caps + vertices.Add(new PositionBinding(new Vector3(0, 0, 0))); // Bottom center + vertices.Add(new PositionBinding(new Vector3(0, height, 0))); // Top center + + int bottomCenter = segments * 2; + int topCenter = segments * 2 + 1; + + // Create wire frame indices + // Bottom circle + for (int i = 0; i < segments; i++) { + wireIndices.Add((uint) i); + wireIndices.Add((uint) ((i + 1) % segments)); + } + + // Top circle + for (int i = 0; i < segments; i++) { + wireIndices.Add((uint) (segments + i)); + wireIndices.Add((uint) (segments + (i + 1) % segments)); + } + + // Vertical lines + for (int i = 0; i < segments; i++) { + wireIndices.Add((uint) i); + wireIndices.Add((uint) (segments + i)); + } + + // Create solid indices + // Bottom cap triangles + for (int i = 0; i < segments; i++) { + solidIndices.Add((uint) bottomCenter); + solidIndices.Add((uint) ((i + 1) % segments)); + solidIndices.Add((uint) i); + } + + // Top cap triangles + for (int i = 0; i < segments; i++) { + solidIndices.Add((uint) topCenter); + solidIndices.Add((uint) (segments + i)); + solidIndices.Add((uint) (segments + (i + 1) % segments)); + } + + // Side triangles + for (int i = 0; i < segments; i++) { + int next = (i + 1) % segments; + + // First triangle + solidIndices.Add((uint) i); + solidIndices.Add((uint) (segments + i)); + solidIndices.Add((uint) next); + + // Second triangle + solidIndices.Add((uint) next); + solidIndices.Add((uint) (segments + i)); + solidIndices.Add((uint) (segments + next)); + } + + // Create wire mesh + meshData.SetPositionBinding(vertices.ToArray()); + meshData.SetAttributeBinding(vertices.Select(_ => new AttributeBinding(Vector3.UnitY, 0xFFFFFFFF, Vector2.Zero)).ToArray()); + meshData.IsTriangleMesh = false; + meshData.SetIndexBuffer(wireIndices.ToArray()); + + Mesh cylinderWireMesh = new Mesh(Context); + cylinderWireMesh.UploadData(meshData); + + // Create solid mesh + meshData.IsTriangleMesh = true; + meshData.SetIndexBuffer(solidIndices.ToArray()); + + Mesh cylinderSolidMesh = new Mesh(Context); + cylinderSolidMesh.UploadData(meshData); + + return (cylinderSolidMesh, cylinderWireMesh); + } } diff --git a/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs new file mode 100644 index 000000000..765dfb4eb --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs @@ -0,0 +1,365 @@ +using Maple2.Server.DebugGame.Graphics.Resources; +using System.Diagnostics.CodeAnalysis; +using System.Xml; +using Maple2.Server.DebugGame.Extensions; + +namespace Maple2.Server.DebugGame.Graphics.Assets; + +public class ShaderPipelines { + public static string AssetRootPath = string.Empty; + + public DebugGraphicsContext Context { get; init; } + public IReadOnlyDictionary Pipelines { get => pipelines; } + + private string currentDocument = string.Empty; + private Dictionary pipelines = new(); + public bool CanRender => !CompilingShaders && !shaderErrors; + public bool CompilingShaders { + get { + compilingShadersMutex.WaitOne(); + bool value = compilingShaders; + compilingShadersMutex.ReleaseMutex(); + + return value; + } + private set { + compilingShadersMutex.WaitOne(); + compilingShaders = value; + compilingShadersMutex.ReleaseMutex(); + } + } + public int RenderingFrames { + get { + renderingFramesMutex.WaitOne(); + int value = renderingFrames; + renderingFramesMutex.ReleaseMutex(); + + return value; + } + } + private bool compilingShaders; + private int renderingFrames; + private bool shaderErrors; + private Mutex compilingShadersMutex = new(); + private Mutex renderingFramesMutex = new(); + private HashSet filesToWatch = new(); + private FileSystemWatcher? shaderWatcher; + private FileSystemWatcher? pipelineWatcher; + + public ShaderPipelines(DebugGraphicsContext context) { + Context = context; + } + + public void StartedFrame() { + renderingFramesMutex.WaitOne(); + ++renderingFrames; + renderingFramesMutex.ReleaseMutex(); + } + + public void EndedFrame() { + renderingFramesMutex.WaitOne(); + --renderingFrames; + renderingFramesMutex.ReleaseMutex(); + } + + private bool NodeTypeError(XmlElement element, string expected = "", string label = "") { + if (expected == "") { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{1}'", currentDocument, label, element.Name); + + return true; + } + + if (element.Name != expected) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{2}', expected '{3}'", currentDocument, label, element.Name, expected); + + return true; + } + + return false; + } + + private bool GetAttribute(XmlElement element, string attributeName, [NotNullWhen(returnValue: true)] out string? value) { + if (!element.TryGetAttribute(attributeName, out value)) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Expected attribute '{1}' on node type '{2}' not found", currentDocument, attributeName, element.Name); + + return false; + } + + return true; + } + + private void OnChanged(object sender, FileSystemEventArgs e) { + // TODO: optimize to reduce recompilations later? + + CompilingShaders = true; + } + + private void InitializeFileWatchers(string pipelineRoot, string shaderRoot) { + if (shaderWatcher is not null && pipelineWatcher is not null) { + return; + } + + NotifyFilters filters = default; + filters |= NotifyFilters.CreationTime; + filters |= NotifyFilters.DirectoryName; + filters |= NotifyFilters.FileName; + filters |= NotifyFilters.LastWrite; + filters |= NotifyFilters.Security; + filters |= NotifyFilters.Size; + + shaderWatcher = new FileSystemWatcher(shaderRoot); + + shaderWatcher.NotifyFilter = filters; + shaderWatcher.Changed += OnChanged; + shaderWatcher.Created += OnChanged; + shaderWatcher.Deleted += OnChanged; + shaderWatcher.Renamed += OnChanged; + shaderWatcher.IncludeSubdirectories = true; + shaderWatcher.EnableRaisingEvents = true; + + pipelineWatcher = new FileSystemWatcher(pipelineRoot); + + pipelineWatcher.NotifyFilter = filters; + pipelineWatcher.Changed += OnChanged; + pipelineWatcher.Created += OnChanged; + pipelineWatcher.Deleted += OnChanged; + pipelineWatcher.Renamed += OnChanged; + pipelineWatcher.IncludeSubdirectories = true; + pipelineWatcher.EnableRaisingEvents = true; + } + + public void ExternalError() { + shaderErrors = true; + } + + public void LoadPipelines() { + CompilingShaders = true; + shaderErrors = false; + + string pipelineRoot = Path.Combine(AssetRootPath, "RenderPipeline"); + string shaderRoot = Path.Combine(AssetRootPath, "Shaders"); + DirectoryInfo directory = new DirectoryInfo(pipelineRoot); + + InitializeFileWatchers(pipelineRoot, shaderRoot); + + IEnumerable enumerateFiles = directory.EnumerateFiles(); + foreach (FileInfo file in enumerateFiles) { + string relpath = Path.GetRelativePath(pipelineRoot, file.FullName); + currentDocument = relpath; + + if (file.Extension.ToLower() != ".xml") { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Attempt to load non xml file in RenderPipeline/", relpath); + + continue; + } + + XmlDocument document = new XmlDocument(); + FileStream fileStream = file.Open(FileMode.Open, FileAccess.Read); + + document.Load(fileStream); + + XmlElement? root = document.DocumentElement; + + if (root is null) { + shaderErrors = true; + fileStream.Close(); + + DebugGraphicsContext.Logger.Error("[{0}]: Missing root 'pipeline' element", relpath); + + continue; + } + + if (NodeTypeError(root, "pipeline", "root ")) { + fileStream.Close(); + + continue; + } + + if (!GetAttribute(root, "name", out string? pipelineName)) { + fileStream.Close(); + + continue; + } + + if (pipelineName == "") { + shaderErrors = true; + fileStream.Close(); + + DebugGraphicsContext.Logger.Error("[{0}]: Pipeline name is empty", relpath); + + continue; + } + + DebugGraphicsContext.Logger.Information("[{0}]: Initializing pipeline '{1}'", relpath, pipelineName); + + ShaderPipeline pipeline = new() { + Name = pipelineName + }; + + foreach (XmlElement pass in root.ChildNodes) { + if (NodeTypeError(pass, "pass")) { + continue; + } + + DebugGraphicsContext.Logger.Information("[{0}]: Adding render pass", relpath); + + RenderPass renderPass = new(); + + foreach (XmlElement child in pass.ChildNodes) { + if (child.Name == "output") { + if (!GetAttribute(child, "name", out string? value)) { + continue; + } + + renderPass.TargetName = value; + + DebugGraphicsContext.Logger.Information("[{0}]: Registered render pass target '{1}'", relpath, value); + + continue; + } + + if (child.Name == "program") { + foreach (XmlElement shader in child.ChildNodes) { + if (NodeTypeError(shader, "shader")) { + continue; + } + + if (!GetAttribute(shader, "path", out string? path)) { + continue; + } + + if (!GetAttribute(shader, "stage", out string? stage)) { + continue; + } + + if (!GetAttribute(shader, "entry", out string? entry)) { + continue; + } + + string shaderPath = path; + path = Path.Combine(shaderRoot, path); + + try { + switch (stage!) { + case "vertex": + if (renderPass.VertexShader is not null) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Found unexpected second vertex shader", currentDocument); + + break; + } + + renderPass.VertexShader = new VertexShader(Context); + renderPass.VertexShader.Load(path, entry); + + DebugGraphicsContext.Logger.Information("[{0}]: Loaded vertex shader '{1}' with entry '{2}'", relpath, shaderPath, entry); + + break; + case "pixel": + if (renderPass.PixelShader is not null) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Found unexpected second pixel shader", currentDocument); + + break; + } + + renderPass.PixelShader = new PixelShader(Context); + renderPass.PixelShader.Load(path, entry); + + DebugGraphicsContext.Logger.Information("[{0}]: Loaded pixel shader '{1}' with entry '{2}'", relpath, shaderPath, entry); + + break; + default: + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Unexpected shader stage: '{1}'", currentDocument, stage); + + break; + } + } catch (Exception e) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}, {1}]: {Message}", currentDocument, stage, e.Message); + } + } + + continue; + } + + NodeTypeError(child); + } + + bool error = false; + + if (renderPass.TargetName != "swapchain") { + DebugGraphicsContext.Logger.Error("[{0}]: Unexpected render pass target: '{1}'", currentDocument, renderPass.TargetName); + + error = true; + } + + if (renderPass.VertexShader is null) { + DebugGraphicsContext.Logger.Error("[{0}]: Render pass missing vertex shader", currentDocument); + + error = true; + } + + if (renderPass.PixelShader is null) { + DebugGraphicsContext.Logger.Error("[{0}]: Render pass missing pixel shader", currentDocument); + + error = true; + } + + if (error) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Failed to initialize render pass in pipeline '{1}'", currentDocument, pipelineName); + + continue; + } + + pipeline.RenderPass = renderPass; + + DebugGraphicsContext.Logger.Information("[{0}]: Finished render pass", relpath); + } + + if (pipeline.RenderPass is null) { + shaderErrors = true; + fileStream.Close(); + + DebugGraphicsContext.Logger.Error("[{0}]: Failed to initialize pipeline '{1}'", currentDocument, pipelineName); + + continue; + } + + pipelines[pipelineName] = pipeline; + + DebugGraphicsContext.Logger.Information("[{0}]: Registered pipeline '{1}'", relpath, pipelineName); + + fileStream.Close(); + } + + if (shaderErrors) { + DebugGraphicsContext.Logger.Error("Shader pipeline errors detected when initializing shaders"); + } + + CompilingShaders = false; + } + + public void CleanUp() { + foreach ((_, ShaderPipeline pipeline) in pipelines) { + pipeline.CleanUp(); + } + + pipelines.Clear(); + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs b/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs new file mode 100644 index 000000000..be6b9305c --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace Maple2.Server.DebugGame.Graphics.Data; + +public struct InstanceBuffer { + public Matrix4x4 Transformation; + public Matrix4x4 InverseTransformation; + public Vector4 Color; +} diff --git a/Maple2.Server.DebugGame/Graphics/Data/SceneViewBuffer.cs b/Maple2.Server.DebugGame/Graphics/Data/SceneViewBuffer.cs new file mode 100644 index 000000000..6ba727e0e --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Data/SceneViewBuffer.cs @@ -0,0 +1,8 @@ +using System.Numerics; + +namespace Maple2.Server.DebugGame.Graphics.Data; + +public struct SceneViewBuffer { + public Matrix4x4 ViewMatrix; + public Matrix4x4 ProjectionMatrix; +} diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 7d7d83efb..04b1edccc 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -3,15 +3,28 @@ using Maple2.Server.Game.Manager.Field; using ImGuiNET; using Maple2.Model.Enum; -using Maple2.Model.Game; -using Maple2.Server.Game.Manager; +using Maple2.Model.Metadata.FieldEntity; +using Maple2.Server.DebugGame.Graphics.Data; using Maple2.Server.Game.Model; +using Maple2.Server.Game.Packets; +using Maple2.Tools.VectorMath; +using Maple2.Tools.Collision; +using Silk.NET.Maths; +using Maple2.Server.DebugGame.Graphics.Scene; +using Serilog; +using Maple2.Model.Game; namespace Maple2.Server.DebugGame.Graphics; public class DebugFieldRenderer : IFieldRenderer { - public DebugGraphicsContext Context { get; init; } - public FieldManager Field { get; init; } + public DebugGraphicsContext Context { get; } + public FieldManager Field { get; } + + // Coordinate system transformation - flip Y/Z axes but keep original scale + private static readonly Matrix4x4 MapRotation = Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)); + private static readonly ILogger Logger = Log.Logger.ForContext(); + + public bool IsActive { get { activeMutex.WaitOne(); @@ -23,11 +36,63 @@ public bool IsActive { private readonly HashSet activeWindows = []; private readonly Mutex activeMutex = new(); - private IActor? selectedActor = null; + + // Public auto-properties replacing previous private fields + wrappers + public IActor? SelectedActor; + public bool ShowBoxColliders = true; + public bool ShowMeshColliders; + public bool ShowSpawnPoints; + public bool ShowVibrateObjects; + public bool ShowPortals = true; + public bool ShowPortalInformation = true; + public bool ShowPortalConnections = true; + public bool ShowActors = true; + public bool ShowPlayers = true; + public bool ShowNpcs = true; + public bool ShowMobs = true; + public bool ShowSellableTiles; // toggle for rendering sellable plot tiles + public bool ShowPlotLabels = true; // toggle for showing floating plot labels (status/owner/etc.) + public bool PlayerMoveMode; + public bool ForceMove; + + // Entity caching for performance + private FieldEntity[]? cachedStaticEntities; // Cache static entities (spawn points, triggers, etc.) + + private const float AgentRadiusMeters = 0.3f; + private const float AgentHeightMeters = 1.4f; + private const float GameUnitsPerMeter = 100.0f; + + private InstanceBuffer instanceBuffer; + private SceneViewBuffer sceneViewBuffer; + + // Camera controller interface for all camera functionality + public ICameraController CameraController { get; private set; } + + // Available controller implementations + private readonly FreeCameraController freeCameraController; + private readonly FollowCameraController followCameraController; + + // Shared camera instance + private readonly Camera sharedCamera = new(); + + // Global follow state (independent of active controller) + public bool HasManuallyStoppedFollowing; + private bool hasTriedAutoFollow; public DebugFieldRenderer(DebugGraphicsContext context, FieldManager field) { Context = context; Field = field; + + // Initialize camera controllers + freeCameraController = new FreeCameraController(sharedCamera); + followCameraController = new FollowCameraController(sharedCamera); + + // Start with free camera controller as default + CameraController = freeCameraController; + + Vector2D windowSize = Context.DebuggerWindow?.FramebufferSize ?? DebugGraphicsContext.DefaultWindowSize; + sharedCamera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); + freeCameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 } public void Update() { @@ -40,363 +105,1068 @@ public void Update() { Field.Update(); } + + // Auto-follow first player when field window is first opened or when a new player joins + // But only if user hasn't manually stopped following + bool isFollowing = CameraController is FollowCameraController { IsFollowingPlayer: true }; + if (!isFollowing && !HasManuallyStoppedFollowing) { + FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); + long? currentFollowedId = CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; + if (firstPlayer != null && (!hasTriedAutoFollow || currentFollowedId != firstPlayer.Value.Character.Id)) { + StartFollowingPlayer(firstPlayer); + hasTriedAutoFollow = true; + } + } + + // Update camera follow if active + UpdateCameraFollow(); } - public void Render(double delta) { + public void Render(double delta) { } + + // New method specifically for field window 3D rendering + public void RenderFieldWindow3D(DebugFieldWindow window, double delta) { if (!IsActive) { return; } - // Store field window position and size for positioning player details panel - Vector2 fieldWindowPos = Vector2.Zero; - Vector2 fieldWindowSize = Vector2.Zero; + // Handle mouse input for actor selection + HandleMouseInput(); - // Create a field information window - if (ImGui.Begin("Field Information")) { - // Get window position and size while the window is active - fieldWindowPos = ImGui.GetWindowPos(); - fieldWindowSize = ImGui.GetWindowSize(); + // Render 3D field visualization using the DirectX pipeline + RenderField3DVisualization(window, delta); + } - RenderFieldBasicInfo(); - ImGui.Separator(); - RenderEntityCounts(); - ImGui.Separator(); - RenderActorList(); - ImGui.Separator(); - RenderFieldProperties(); - } - ImGui.End(); + private void HandleMouseInput() { + // Check if mouse is clicked and we're hovering over the field window + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { + // Don't process clicks if ImGui is handling them (mouse over UI elements) + if (ImGui.GetIO().WantCaptureMouse) { + return; + } - // Show actor details panel if an actor is selected - if (selectedActor != null) { - RenderActorDetailsPanel(fieldWindowPos, fieldWindowSize); + // Get mouse position in screen coordinates + Vector2 mousePos = ImGui.GetMousePos(); + + if (PlayerMoveMode && SelectedActor is FieldPlayer selectedPlayer) { + // Move mode: try to move the selected player to clicked position + TryMovePlayerToScreenPosition(selectedPlayer, mousePos); + } else { + // Selection mode: try to select an actor at clicked position + TrySelectActorAtScreenPosition(mousePos); + } } } - private void RenderFieldBasicInfo() { - ImGui.Text($"Map ID: {Field.MapId}"); - ImGui.Text($"Room ID: {Field.RoomId}"); - ImGui.Text($"Map Name: {Field.Metadata.Name}"); - ImGui.Text($"Field Type: {Field.FieldType}"); + public string GetActorName(IActor actor) => actor switch { FieldPlayer player => player.Value.Character.Name, FieldNpc npc => npc.Value.Metadata.Name ?? "Unknown", _ => "Unknown" }; + public string GetActorType(IActor actor) => actor switch { FieldPlayer => "Player", FieldNpc => "NPC", _ => "Unknown" }; + + public void CleanUp() { } + + public void AttachWindow(DebugFieldWindow window) { + activeMutex.WaitOne(); + + activeWindows.Add(window); + + activeMutex.ReleaseMutex(); + + // Set default camera to field overview when window is first attached + SetFieldOverviewCamera(); - if (Field.DungeonId > 0) { - ImGui.Text($"Dungeon ID: {Field.DungeonId}"); + // Auto-follow first player when field window is opened + // But only if user hasn't manually stopped following + bool isFollowing = CameraController is FollowCameraController { IsFollowingPlayer: true }; + if (!isFollowing && !HasManuallyStoppedFollowing) { + FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); + if (firstPlayer != null) { + StartFollowingPlayer(firstPlayer); + } } } - private void RenderEntityCounts() { - ImGui.Text("Entity Counts:"); - ImGui.Indent(); - ImGui.Text($"Players: {Field.Players.Count}"); - ImGui.Text($"NPCs: {Field.Npcs.Count}"); - ImGui.Text($"Mobs: {Field.Mobs.Count}"); - ImGui.Text($"Pets: {Field.Pets.Count}"); - ImGui.Unindent(); + public void DetachWindow(DebugFieldWindow window) { + activeMutex.WaitOne(); + + activeWindows.Remove(window); + + activeMutex.ReleaseMutex(); } - private void RenderActorList() { - ImGui.Text("Active Actors:"); + private void RenderField3DVisualization(DebugFieldWindow window, double delta) { + if (Field.AccelerationStructure == null) { + return; + } + + UpdateCameraProjection(window); + + // Enable wireframe mode for field visualization + bool originalWireframeMode = Context.WireframeMode; + Context.WireframeMode = true; + Context.SetWireframeRasterizer(); + + Context.WireframePipeline!.RenderPass!.VertexShader!.Bind(); + Context.WireframePipeline.RenderPass.PixelShader!.Bind(); + + // Render field entities using the DirectX 11 pipeline + RenderFieldEntities3D(window); + + // Restore original wireframe mode + Context.WireframeMode = originalWireframeMode; + Context.SetSolidRasterizer(); + } - int totalActors = Field.Players.Count + Field.Npcs.Count + Field.Mobs.Count; - if (totalActors == 0) { - ImGui.Text("No active actors"); + private void RenderFieldEntities3D(DebugFieldWindow window) { + if (Field.AccelerationStructure == null) { return; } - bool showActorDetailsDisabled = selectedActor == null; + // Render box colliders + if (ShowBoxColliders) { + RenderBoxColliders(window); + } - if (showActorDetailsDisabled) { - ImGui.BeginDisabled(); + // Render mesh colliders + if (ShowMeshColliders) { + RenderMeshColliders(window); } - if (ImGui.Button("Show Actor Details") && !showActorDetailsDisabled) { - // Keep the selected actor to show details panel + // Render spawn points + if (ShowSpawnPoints) { + RenderSpawnPoints(window); } - if (showActorDetailsDisabled) { - ImGui.EndDisabled(); + // Render vibrate objects + if (ShowVibrateObjects) { + RenderVibrateObjects(window); } - ImGui.SameLine(); + // Render portals + if (ShowPortals) { + RenderPortals(window); - bool clearSelectionDisabled = selectedActor == null; + if (ShowPortalConnections) { + RenderPortalConnectionLines(window); + } - if (clearSelectionDisabled) { - ImGui.BeginDisabled(); + // Render portal text labels + if (ShowPortalInformation) { + RenderPortalTextLabels(); + } } - if (ImGui.Button("Clear Selection") && !clearSelectionDisabled) { - selectedActor = null; + // Render sellable tiles (player housing plots / sellable land tiles) + if (ShowSellableTiles) { + RenderSellableTiles(window); + if (ShowPlotLabels) { + RenderPlotTextLabels(); + } } - if (clearSelectionDisabled) { - ImGui.EndDisabled(); + // Render actors (players, NPCs, mobs) + if (ShowActors) { + RenderActors(window); + + // Render selection highlight for selected actor + if (SelectedActor != null) { + RenderActorSelectionHighlight(window); + } + + // Render text labels above actors using ImGui + RenderActorTextLabels(); } + } + + private void RenderBoxColliders(DebugFieldWindow window) { + // Set white color for box colliders + instanceBuffer.Color = new Vector4(1, 1, 1, 1); - if (ImGui.BeginTable("Active Actors", 5)) { - ImGui.TableNextRow(ImGuiTableRowFlags.Headers); + List allBoxColliders = GetAllEntities().OfType().ToList(); - ImGui.TableSetColumnIndex(0); - ImGui.Text("Type"); - ImGui.TableSetColumnIndex(1); - ImGui.Text("Name"); - ImGui.TableSetColumnIndex(2); - ImGui.Text("Level"); - ImGui.TableSetColumnIndex(3); - ImGui.Text("Object ID"); - ImGui.TableSetColumnIndex(4); - ImGui.Text("Status"); + foreach (FieldBoxColliderEntity boxCollider in allBoxColliders) { + Transform transform = new() { + Position = boxCollider.Position, + RotationAnglesDegrees = boxCollider.Rotation, + }; - int index = 0; + transform.Transformation *= MapRotation; + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(boxCollider.Size) * + Matrix4x4.CreateTranslation(boxCollider.Position)); - // Render Players - foreach ((int objectId, FieldPlayer player) in Field.Players) { - ImGui.TableNextRow(); + UpdateWireframeInstance(window); + Context.CoreModels!.WireCube.Draw(); + } + } - bool selected = player == selectedActor; - bool nextSelected = false; + private void RenderMeshColliders(DebugFieldWindow window) { + IEnumerable allMeshColliders = GetAllEntities().OfType(); - ImGui.TableSetColumnIndex(0); - nextSelected |= ImGui.Selectable($"Player##Actor {index} 0", selected); - ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{player.Value.Character.Name}##Actor {index} 1", selected); - ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{player.Value.Character.Level}##Actor {index} 2", selected); - ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{objectId}##Actor {index} 3", selected); - ImGui.TableSetColumnIndex(4); - nextSelected |= ImGui.Selectable($"{(player.IsDead ? "Dead" : "Alive")}##Actor {index} 4", selected); + foreach (FieldMeshColliderEntity meshCollider in allMeshColliders) { + // For mesh colliders, render a simple bounding box for now + Vector3 size = meshCollider.Bounds.Max - meshCollider.Bounds.Min; + Vector3 center = (meshCollider.Bounds.Max + meshCollider.Bounds.Min) * 0.5f; - if (nextSelected) { - selectedActor = player; + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(size) * + Matrix4x4.CreateTranslation(center)); + + UpdateWireframeInstance(window); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderSpawnPoints(DebugFieldWindow window) { + IEnumerable allSpawnPoints = GetAllEntities().OfType(); + + foreach (FieldSpawnTile spawnPoint in allSpawnPoints) { + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(Vector3.One * 50.0f) * + Matrix4x4.CreateTranslation(spawnPoint.Position)); + + UpdateWireframeInstance(window); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderVibrateObjects(DebugFieldWindow window) { + // Set pink color for vibrate objects + instanceBuffer.Color = new Vector4(1, 0.75f, 0.8f, 1); + + foreach (FieldVibrateEntity vibrateEntity in Field.AccelerationStructure!.VibrateEntities) { + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(Vector3.One * 100.0f) * + Matrix4x4.CreateTranslation(vibrateEntity.Position)); + + UpdateWireframeInstance(window); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderPortals(DebugFieldWindow window) { + // Set blue color for portals + instanceBuffer.Color = new Vector4(0, 0.5f, 1, 1); + foreach (FieldPortal portal in Field.GetPortals()) { + Vector3 size = portal.Value.Dimension != Vector3.Zero ? portal.Value.Dimension : Vector3.One * 100.0f; + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(size) * + Matrix4x4.CreateTranslation(portal.Position)); + UpdateWireframeInstance(window); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderSellableTiles(DebugFieldWindow window) { + if (Field.AccelerationStructure == null) return; + IEnumerable tiles = GetAllEntities().OfType(); + + // Simple deterministic color per group (hash -> color) + Vector4 ColorForGroup(int group) { + unchecked { + int hash = (int) (group * 2654435761u); // Knuth multiplicative hash (32-bit wrap) + // extract bytes + float r = ((hash >> 16) & 0xFF) / 255f; + float g = ((hash >> 8) & 0xFF) / 255f; + float b = (hash & 0xFF) / 255f; + // brighten a bit + float scale = 0.6f + 0.4f * ((hash >> 24) & 0xFF) / 255f; + return new Vector4(r * scale, g * scale, b * scale, 0.45f); // semi-transparent + } + } + + foreach (FieldSellableTile tile in tiles) { + // Color by group + instanceBuffer.Color = ColorForGroup(tile.SellableGroup); + Vector3 size = tile.Bounds.Max - tile.Bounds.Min; + Vector3 center = (tile.Bounds.Max + tile.Bounds.Min) * 0.5f; + center.Z -= size.Z * 0.5f; // shift down half-height + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(size) * Matrix4x4.CreateTranslation(center)); + + UpdateWireframeInstance(window); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderPortalConnectionLines(DebugFieldWindow window) { + // Set cyan color for portal connection lines + instanceBuffer.Color = new Vector4(0, 1, 1, 0.7f); + + List portals = Field.GetPortals().ToList(); + + foreach (FieldPortal sourcePortal in portals) { + // Only draw lines for portals that target the same field and have a specific target portal + if (sourcePortal.Value.TargetMapId == Field.MapId && sourcePortal.Value.TargetPortalId > 0) { + // Find the target portal in the same field + FieldPortal? targetPortal = portals.FirstOrDefault(p => p.Value.Id == sourcePortal.Value.TargetPortalId); + + if (targetPortal != null) { + // Draw a line between the two portals + RenderLine(window, sourcePortal.Position, targetPortal.Position); } + } + } + } + + private void RenderLine(DebugFieldWindow window, Vector3 start, Vector3 end) { + // Create a line by drawing a thin cylinder between two points + Vector3 direction = end - start; + float distance = direction.Length(); + + if (distance < 0.1f) return; // Skip very short lines + + Vector3 center = (start + end) * 0.5f; + Vector3 normalizedDirection = Vector3.Normalize(direction); + + // Calculate rotation to align cylinder with the line direction + // The cylinder model is oriented along the Y axis by default + Vector3 defaultUp = Vector3.UnitY; + Matrix4x4 rotationMatrix; + + // Check if direction is parallel to Y axis + float dot = Vector3.Dot(defaultUp, normalizedDirection); + if (Math.Abs(dot) > 0.999f) { + // Direction is parallel to Y axis, no rotation needed (or 180 degrees) + rotationMatrix = dot > 0 ? Matrix4x4.Identity : Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, MathF.PI); + } else { + // Calculate rotation between Y axis and direction + Vector3 axis = Vector3.Normalize(Vector3.Cross(defaultUp, normalizedDirection)); + float angle = MathF.Acos(Math.Clamp(dot, -1.0f, 1.0f)); + rotationMatrix = Matrix4x4.CreateFromAxisAngle(axis, angle); + } - ++index; + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(10.0f, distance / 2.0f, 10.0f)) * + rotationMatrix * + Matrix4x4.CreateTranslation(center)); + + UpdateWireframeInstance(window); + Context.CoreModels!.Cylinder.Draw(); + } + + private void RenderActors(DebugFieldWindow window) { + const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; + const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; + + if (ShowPlayers) { + instanceBuffer.Color = new Vector4(0, 1, 1, 1); // Cyan for players + foreach ((int _, FieldPlayer player) in Field.Players) { + // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(player.Position)); + + UpdateWireframeInstance(window); + Context.CoreModels!.Cylinder.Draw(); } + } - // Render NPCs - foreach ((int objectId, FieldNpc npc) in Field.Npcs) { - ImGui.TableNextRow(); - - bool selected = npc == selectedActor; - bool nextSelected = false; - - ImGui.TableSetColumnIndex(0); - nextSelected |= ImGui.Selectable($"NPC##Actor {index} 0", selected); - ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Name}##Actor {index} 1", selected); - ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Basic.Level}##Actor {index} 2", selected); - ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{objectId}##Actor {index} 3", selected); - ImGui.TableSetColumnIndex(4); - nextSelected |= ImGui.Selectable($"{(npc.IsDead ? "Dead" : "Alive")}##Actor {index} 4", selected); - - if (nextSelected) { - selectedActor = npc; + if (ShowNpcs) { + foreach ((int _, FieldNpc npc) in Field.Npcs) { + // Set color based on dead status + if (npc.IsDead) { + instanceBuffer.Color = new Vector4(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead NPCs + } else { + instanceBuffer.Color = new Vector4(0, 1, 0, 1); // Green for alive NPCs } - ++index; + // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(npc.Position)); + + UpdateWireframeInstance(window); + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs } + } + - // Render Mobs - foreach ((int objectId, FieldNpc mob) in Field.Mobs) { - ImGui.TableNextRow(); - - bool selected = mob == selectedActor; - bool nextSelected = false; - - ImGui.TableSetColumnIndex(0); - nextSelected |= ImGui.Selectable($"Mob##Actor {index} 0", selected); - ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{mob.Value.Metadata.Name}##Actor {index} 1", selected); - ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{mob.Value.Metadata.Basic.Level}##Actor {index} 2", selected); - ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{objectId}##Actor {index} 3", selected); - ImGui.TableSetColumnIndex(4); - nextSelected |= ImGui.Selectable($"{(mob.IsDead ? "Dead" : "Alive")}##Actor {index} 4", selected); - - if (nextSelected) { - selectedActor = mob; + if (ShowMobs) { + foreach ((int _, FieldNpc mob) in Field.Mobs) { + // Set color based on dead status and battle state + if (mob.IsDead) { + instanceBuffer.Color = new Vector4(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead mobs + } else { + // Check if mob is aggressive (in battle) or wandering + if (mob.BattleState.InBattle) { + instanceBuffer.Color = new Vector4(1, 0, 0, 1); // Red for aggressive mobs + } else { + instanceBuffer.Color = new Vector4(1, 1, 0, 1); // Yellow for wandering mobs + } } - ++index; + // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(mob.Position)); + + UpdateWireframeInstance(window); + Context.CoreModels!.Cylinder.Draw(); + } + } + } + + private void RenderActorSelectionHighlight(DebugFieldWindow window) { + if (SelectedActor == null) return; + + const float highlightRadius = AgentRadiusMeters * GameUnitsPerMeter * 1.4f; + const float highlightHeight = AgentHeightMeters * GameUnitsPerMeter * 1.2f; + + // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); + + // Render larger outline + instanceBuffer.Color = new Vector4(1, 1, 0, 1); // Yellow + + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(highlightRadius, highlightHeight, highlightRadius)) * + rotation * + Matrix4x4.CreateTranslation(SelectedActor.Position)); + + UpdateWireframeInstance(window); + Context.CoreModels!.Cylinder.Draw(); + } + + public void SetFieldOverviewCamera() { + // Stop following player when switching to field overview + if (CameraController is FollowCameraController { IsFollowingPlayer: true }) { + StopFollowingPlayer(); + } + + List allColliders = GetAllEntities().OfType().ToList(); + if (allColliders.Count > 0) { + // Calculate field bounds in map coordinates + Vector3 min = allColliders[0].Position; + Vector3 max = allColliders[0].Position; + + foreach (FieldBoxColliderEntity collider in allColliders) { + min = Vector3.Min(min, collider.Position - collider.Size * 0.5f); + max = Vector3.Max(max, collider.Position + collider.Size * 0.5f); } - ImGui.EndTable(); + // Transform to visualization coordinates + Vector3 centerMap = (min + max) * 0.5f; + Vector3 sizeMap = max - min; + + Vector3 center = Vector3.Transform(centerMap, MapRotation); + float distance = Math.Max(sizeMap.X, Math.Max(sizeMap.Y, sizeMap.Z)) * 1.5f; + + // Set camera target and position for field overview + if (CameraController is FreeCameraController freeCam) { + freeCam.SetCameraTarget(center); + freeCam.SetFieldOverviewRotation(); + CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); + } + } else { + // Fallback if no colliders found + if (CameraController is FreeCameraController freeCam) { + freeCam.SetCameraTarget(Vector3.Zero); + freeCam.SetFieldOverviewRotation(); + CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); + } } } - private void RenderFieldProperties() { - ImGui.Text("Field Status:"); - ImGui.Indent(); + private void UpdateCameraFollow() { + if (CameraController is not FollowCameraController { IsFollowingPlayer: true } followCam || followCam.FollowedPlayerId == null) { + return; + } - if (Field.RoomTimer != null) { - ImGui.Text($"Room Timer Active: {Field.RoomTimer.Duration}"); + // Find the player being followed + FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == followCam.FollowedPlayerId); + if (followedPlayer == null) { + // Player not found, stop following + StopFollowingPlayer(); + return; } - if (Field.AccelerationStructure != null) { - ImGui.Text("Acceleration Structure: Active"); + // Update camera to follow player position using the camera controller + Vector3 playerPosition = followedPlayer.Position; + UpdatePlayerFollow(playerPosition); + } + + private Vector2D GetFieldWindowSize() { + // Get the size from the first active field window, or use default + activeMutex.WaitOne(); + try { + DebugFieldWindow? firstWindow = activeWindows.FirstOrDefault(); + if (firstWindow?.DebuggerWindow != null) { + return firstWindow.DebuggerWindow.FramebufferSize; + } + } finally { + activeMutex.ReleaseMutex(); } - ImGui.Text($"Field Instance Type: {Field.FieldInstance.Type}"); - ImGui.Unindent(); + // Fallback to default field window size + return DebugGraphicsContext.DefaultFieldWindowSize; } - private void RenderActorDetailsPanel(Vector2 fieldWindowPos, Vector2 fieldWindowSize) { - if (selectedActor == null) return; + private bool TryWorldToScreen(Vector3 worldPos, out Vector2 screenPos) { + screenPos = Vector2.Zero; + + // Transform world position to screen coordinates + Matrix4x4 viewMatrix = CameraController.Camera.ViewMatrix; + Matrix4x4 projMatrix = CameraController.Camera.ProjectionMatrix; + Matrix4x4 viewProjMatrix = viewMatrix * projMatrix; + + // Transform to clip space + Vector4 clipPos = Vector4.Transform(new Vector4(worldPos, 1.0f), viewProjMatrix); + + // Check if behind camera + if (clipPos.W <= 0) return false; + + // Perspective divide to get NDC coordinates + Vector3 ndcPos = new Vector3(clipPos.X, clipPos.Y, clipPos.Z) / clipPos.W; + + // Check if outside screen bounds (with some tolerance) + if (ndcPos.X < -1.2f || ndcPos.X > 1.2f || ndcPos.Y < -1.2f || ndcPos.Y > 1.2f) return false; + + // Get the actual viewport size from the field window (not main window) + Vector2D windowSize = GetFieldWindowSize(); + + // Convert NDC to screen coordinates + // NDC: (-1, -1) = bottom-left, (1, 1) = top-right + // Screen: (0, 0) = top-left, (width, height) = bottom-right + float screenX = (ndcPos.X + 1.0f) * 0.5f * windowSize.X; + float screenY = (1.0f - ndcPos.Y) * 0.5f * windowSize.Y; // Flip Y axis for screen coordinates + + screenPos = new Vector2(screenX, screenY); + return true; + } - // Position the actor details panel to the right of the field information panel - ImGui.SetNextWindowPos(new Vector2( - fieldWindowPos.X + fieldWindowSize.X + 10, // 10px gap - fieldWindowPos.Y - )); + private bool TryScreenToWorldRay(Vector2 screenPos, out Vector3 rayOrigin, out Vector3 rayDirection) { + rayOrigin = Vector3.Zero; + rayDirection = Vector3.Zero; - // Set a reasonable size for the actor details panel - ImGui.SetNextWindowSize(new Vector2(350, 500), ImGuiCond.FirstUseEver); + // Get the actual viewport size from the field window + Vector2D windowSize = GetFieldWindowSize(); - // Get actor name for window title - string actorName = GetActorName(selectedActor); - string actorType = GetActorType(selectedActor); + // Convert screen coordinates to NDC coordinates + // Screen: (0, 0) = top-left, (width, height) = bottom-right + // NDC: (-1, -1) = bottom-left, (1, 1) = top-right + float ndcX = (screenPos.X / windowSize.X) * 2.0f - 1.0f; + float ndcY = 1.0f - (screenPos.Y / windowSize.Y) * 2.0f; // Flip Y axis - // Create a separate window for actor details - if (ImGui.Begin($"{actorType} Details: {actorName}##ActorDetails")) { - RenderActorBasicInfo(); - ImGui.Separator(); - RenderActorPositionInfo(); - ImGui.Separator(); - RenderActorStatsInfo(); - ImGui.Separator(); - RenderActorAdditionalInfo(); + // Get camera matrices + Matrix4x4 viewMatrix = CameraController.Camera.ViewMatrix; + Matrix4x4 projMatrix = CameraController.Camera.ProjectionMatrix; + + // Calculate inverse view-projection matrix + Matrix4x4 viewProjMatrix = viewMatrix * projMatrix; + if (!Matrix4x4.Invert(viewProjMatrix, out Matrix4x4 invViewProjMatrix)) { + return false; // Matrix is not invertible } - ImGui.End(); + + // Create two points: one on near plane, one on far plane + Vector4 nearPoint = new Vector4(ndcX, ndcY, -1.0f, 1.0f); // Near plane in NDC + Vector4 farPoint = new Vector4(ndcX, ndcY, 1.0f, 1.0f); // Far plane in NDC + + // Transform to world space + Vector4 nearWorld = Vector4.Transform(nearPoint, invViewProjMatrix); + Vector4 farWorld = Vector4.Transform(farPoint, invViewProjMatrix); + + // Perspective divide + if (nearWorld.W == 0 || farWorld.W == 0) return false; + nearWorld /= nearWorld.W; + farWorld /= farWorld.W; + + // Calculate ray + rayOrigin = new Vector3(nearWorld.X, nearWorld.Y, nearWorld.Z); + Vector3 farWorldPos = new Vector3(farWorld.X, farWorld.Y, farWorld.Z); + rayDirection = Vector3.Normalize(farWorldPos - rayOrigin); + + return true; } - private string GetActorName(IActor actor) { - return actor switch { - FieldPlayer player => player.Value.Character.Name, - FieldNpc npc => npc.Value.Metadata.Name ?? "Unknown", - _ => "Unknown", - }; + private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { + if (!TryScreenToWorldRay(screenPos, out Vector3 rayOrigin, out Vector3 rayDirection)) { + return false; + } + + IActor? closestActor = null; + float closestDistance = float.MaxValue; + + // Test players + if (ShowPlayers) { + foreach ((int _, FieldPlayer player) in Field.Players) { + if (TryRayPrismIntersection(rayOrigin, rayDirection, player.Shape, out float distance)) { + if (distance < closestDistance) { + closestDistance = distance; + closestActor = player; + } + } + } + } + + // Test NPCs + if (ShowNpcs) { + foreach ((int _, FieldNpc npc) in Field.Npcs) { + if (TryRayPrismIntersection(rayOrigin, rayDirection, npc.Shape, out float distance)) { + if (distance < closestDistance) { + closestDistance = distance; + closestActor = npc; + } + } + } + } + + // Test Mobs + if (ShowMobs) { + foreach ((int _, FieldNpc mob) in Field.Mobs) { + if (TryRayPrismIntersection(rayOrigin, rayDirection, mob.Shape, out float distance)) { + if (distance < closestDistance) { + closestDistance = distance; + closestActor = mob; + } + } + } + } + + if (closestActor != null) { + SelectedActor = closestActor; + return true; + } + + return false; } - private string GetActorType(IActor actor) { - return actor switch { - FieldPlayer => "Player", - FieldNpc => "NPC", - _ => "Unknown", - }; + private bool TryRayPrismIntersection(Vector3 rayOrigin, Vector3 rayDirection, IPrism prism, out float distance) { + distance = 0; + + // Sample points along the ray to test for intersection with the prism + // This is a simple approach - for more accuracy, you could implement proper ray-prism intersection + const float stepSize = 10.0f; // Step size in world units + const float maxDistance = 10000.0f; // Maximum ray distance to test + + for (float t = 0; t < maxDistance; t += stepSize) { + Vector3 testPoint = rayOrigin + t * rayDirection; + + if (prism.Contains(testPoint)) { + distance = t; + return true; + } + } + + return false; } - private void RenderActorBasicInfo() { - if (selectedActor == null) return; + private void TryMovePlayerToScreenPosition(FieldPlayer player, Vector2 screenPos) { + if (!TryScreenToWorldRay(screenPos, out Vector3 rayOrigin, out Vector3 rayDirection)) { + return; + } + + // Cast ray down to find ground intersection + Vector3? targetPosition = FindGroundIntersection(rayOrigin, rayDirection); - ImGui.Text($"Type: {GetActorType(selectedActor)}"); - ImGui.Text($"Name: {GetActorName(selectedActor)}"); - ImGui.Text($"Object ID: {selectedActor.ObjectId}"); - ImGui.Text($"Is Dead: {selectedActor.IsDead}"); + if (targetPosition.HasValue) { + Vector3 newPosition = targetPosition.Value; + Vector3 currentRotation = player.Rotation; // Keep current rotation - switch (selectedActor) { - case FieldPlayer player: - ImGui.Text($"State: {player.State}"); - ImGui.Text($"Sub State: {player.SubState}"); - Character character = player.Value.Character; - ImGui.Text($"Level: {character.Level}"); - ImGui.Text($"Job: {character.Job}"); - ImGui.Text($"Gender: {character.Gender}"); - ImGui.Text($"Account ID: {player.Value.Account.Id}"); - ImGui.Text($"Character ID: {character.Id}"); - break; - case FieldNpc npc: - ImGui.Text($"State: {npc.State.State}"); - ImGui.Text($"Sub State: {npc.State.SubState}"); - ImGui.Text($"Level: {npc.Value.Metadata.Basic.Level}"); - ImGui.Text($"NPC ID: {npc.Value.Metadata.Id}"); - ImGui.Text($"Model name: {npc.Value.Metadata.Model.Name}"); - ImGui.Text($"Animation Speed: {npc.Value.Metadata.Model.AniSpeed}"); - ImGui.Text($"Is Boss: {npc.Value.IsBoss}"); - break; + if (ForceMove) { + // Force move: bypass validation and send packet directly + player.Session.Send(PortalPacket.MoveByPortal(player, newPosition, currentRotation)); + } else { + // Normal move: use the built-in function with validation + player.MoveToPosition(newPosition, currentRotation); + } } } - private void RenderActorPositionInfo() { - if (selectedActor == null) return; + private Vector3? FindGroundIntersection(Vector3 rayOrigin, Vector3 rayDirection) { + // First, get the X,Y position where the ray would hit a horizontal plane + Vector2 targetXy = GetRayGroundProjection(rayOrigin, rayDirection); + + // Find the topmost block at this X,Y position + float? topBlockZ = FindTopmostBlockAt(targetXy); - ImGui.Text("Position & Movement:"); - ImGui.Indent(); - ImGui.Text($"Position: {selectedActor.Position}"); - ImGui.Text($"Rotation: {selectedActor.Rotation}"); - if (selectedActor is FieldNpc npc) { - ImGui.Text($"Velocity: {npc.MovementState.Velocity}"); + if (topBlockZ.HasValue) { + // Place player on top of the highest block (add some height for the player) + return new Vector3(targetXy.X, targetXy.Y, topBlockZ.Value + 100.0f); // 50 units above block } - ImGui.Text($"Playing Sequence: {selectedActor.Animation.PlayingSequence?.Name ?? "None"}"); + // No blocks found - drop player from way above for natural falling + const float dropHeight = 2000.0f; // Drop from 2000 units above + return new Vector3(targetXy.X, targetXy.Y, dropHeight); + } - if (selectedActor is FieldPlayer player) { - ImGui.Text($"Last Ground Position: {player.LastGroundPosition}"); - ImGui.Text($"In Battle: {player.InBattle}"); + private Vector2 GetRayGroundProjection(Vector3 rayOrigin, Vector3 rayDirection) { + // Project the ray to a reasonable ground level to get X,Y coordinates + float targetZ = SelectedActor?.Position.Z ?? 0.0f; + + if (Math.Abs(rayDirection.Z) > 0.001f) { + float t = (targetZ - rayOrigin.Z) / rayDirection.Z; + if (t > 0) { + Vector3 intersection = rayOrigin + t * rayDirection; + return new Vector2(intersection.X, intersection.Y); + } } - ImGui.Unindent(); + // Fallback: project ray forward a reasonable distance + Vector3 projected = rayOrigin + rayDirection * 1000.0f; + return new Vector2(projected.X, projected.Y); } - private void RenderActorStatsInfo() { - if (selectedActor == null) return; + private float? FindTopmostBlockAt(Vector2 position) { + if (Field.AccelerationStructure == null) { + return null; + } + + float? highestZ = null; - ImGui.Text("Stats:"); - ImGui.Indent(); + // Query box colliders (main map blocks) at this position + var boxColliders = GetAllEntities().OfType(); - var stats = selectedActor.Stats; - ImGui.Text($"Health: {stats.Values[BasicAttribute.Health].Current}/{stats.Values[BasicAttribute.Health].Total}"); + foreach (FieldBoxColliderEntity collider in boxColliders) { + // Check if this collider contains the X,Y position + if (IsPositionInBoxCollider(position, collider)) { + // Calculate the top of this collider + float colliderTop = collider.Position.Z + (collider.Size.Z * 0.5f); - // Show additional stats for players - if (selectedActor is FieldPlayer) { - ImGui.Text($"Spirit: {stats.Values[BasicAttribute.Spirit].Current}/{stats.Values[BasicAttribute.Spirit].Total}"); - ImGui.Text($"Stamina: {stats.Values[BasicAttribute.Stamina].Current}/{stats.Values[BasicAttribute.Stamina].Total}"); + if (!highestZ.HasValue || colliderTop > highestZ.Value) { + highestZ = colliderTop; + } + } } - ImGui.Unindent(); + return highestZ; } - private void RenderActorAdditionalInfo() { - if (selectedActor == null) return; + private bool IsPositionInBoxCollider(Vector2 position, FieldBoxColliderEntity collider) { + // Simple AABB check for now - could be enhanced for rotated boxes + Vector3 min = collider.Position - (collider.Size * 0.5f); + Vector3 max = collider.Position + (collider.Size * 0.5f); - ImGui.Text("Additional Information:"); - ImGui.Indent(); + return position.X >= min.X && position.X <= max.X && + position.Y >= min.Y && position.Y <= max.Y; + } - switch (selectedActor) { - case FieldPlayer player: - ImGui.Text($"Admin Permissions: {player.AdminPermissions}"); - break; - case FieldNpc npc: - ImGui.Text($"NPC Type: {(npc.Value.Metadata.Basic.Kind == 0 ? "Friendly" : "Hostile")}"); - if (npc.Owner != null) { - ImGui.Text($"Spawn Point ID: {npc.SpawnPointId}"); + private void RenderActorTextLabels() { + ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); + + // Render player names + if (ShowPlayers) { + foreach ((int _, FieldPlayer player) in Field.Players) { + Vector3 textPos = player.Position + new Vector3(0, 0, 200); // Above the cylinder + if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; + + string playerName = player.Value.Character.Name; + bool isSelected = SelectedActor == player; + + Vector2 textSize = ImGui.CalcTextSize(playerName); + float left = screenPos.X - (textSize.X * 0.5f); + Vector2 textTopLeft = new(left, screenPos.Y); + Vector4 textColor = isSelected ? new Vector4(1, 1, 0, 1) : new Vector4(0, 1, 1, 1); // Yellow if selected, cyan otherwise + Vector4 bgColor = isSelected ? new Vector4(0.2f, 0.2f, 0, 0.9f) : new Vector4(0, 0, 0, 0.7f); // Darker yellow bg if selected + Vector4 borderColor = isSelected ? new Vector4(1, 1, 0, 1) : new Vector4(0.2f, 0.2f, 0.2f, 0.8f); // Yellow border if selected + + // Draw background centered + Vector2 bgMin = textTopLeft - new Vector2(4, 2); + Vector2 bgMax = textTopLeft + textSize + new Vector2(4, 2); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); + + // Draw text on top + drawList.AddText(textTopLeft, ImGui.ColorConvertFloat4ToU32(textColor), playerName); + } + } + + // Render NPC names + if (ShowNpcs) { + foreach ((int _, FieldNpc npc) in Field.Npcs) { + Vector3 textPos = npc.Position + new Vector3(0, 0, 200); // Above the cylinder + if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; + + string npcName = npc.Value.Metadata.Name ?? $"NPC {npc.Value.Metadata.Id}"; + bool isSelected = SelectedActor == npc; + + Vector4 textColor = isSelected ? new Vector4(1, 1, 0, 1) : new Vector4(0, 1, 0, 1); // Yellow if selected, green otherwise + Vector4 bgColor = isSelected ? new Vector4(0.2f, 0.2f, 0, 0.9f) : new Vector4(0, 0, 0, 0.7f); // Darker yellow bg if selected + Vector4 borderColor = isSelected ? new Vector4(1, 1, 0, 1) : new Vector4(0.2f, 0.2f, 0.2f, 0.8f); // Yellow border if selected + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + Vector2 nameSize = ImGui.CalcTextSize(npcName); + float left = screenPos.X - (nameSize.X * 0.5f); + Vector2 nameTopLeft = new(left, screenPos.Y); + Vector2 nameBgMin = nameTopLeft - new Vector2(4, 2); + Vector2 nameBgMax = nameTopLeft + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); + drawList.AddText(nameTopLeft, color, npcName); + } + } + + // Render Mob names and HP + if (ShowMobs) { + foreach ((int _, FieldNpc mob) in Field.Mobs) { + Vector3 textPos = mob.Position + new Vector3(0, 0, 200); // Above the cylinder + if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; + + string mobName = mob.Value.Metadata.Name ?? $"Mob {mob.Value.Metadata.Id}"; + bool isSelected = SelectedActor == mob; + + // Calculate HP percentage + long currentHp = mob.Stats.Values[BasicAttribute.Health].Current; + long maxHp = mob.Stats.Values[BasicAttribute.Health].Total; + float hpPercent = maxHp > 0 ? (float) currentHp / maxHp * 100f : 0f; + string hpText = $"HP: {hpPercent:F1}%"; + + // Choose color based on status, but override with yellow if selected + Vector4 textColor; + Vector4 bgColor; + Vector4 borderColor; + + if (isSelected) { + textColor = new Vector4(1, 1, 0, 1); // Yellow for selected + bgColor = new Vector4(0.2f, 0.2f, 0, 0.9f); // Darker yellow bg + borderColor = new Vector4(1, 1, 0, 1); // Yellow border + } else { + // Normal colors based on mob status + if (mob.IsDead) { + textColor = new Vector4(0.5f, 0.5f, 0.5f, 0.7f); // Gray for dead + } else if (mob.BattleState.InBattle) { + textColor = new Vector4(1, 0, 0, 1); // Red for aggressive + } else { + textColor = new Vector4(1, 1, 0, 1); // Yellow for wandering + } + bgColor = new Vector4(0, 0, 0, 0.7f); // Normal dark background + borderColor = new Vector4(0.2f, 0.2f, 0.2f, 0.8f); // Normal border } - break; - default: - ImGui.Text("No additional information available."); - break; + + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + Vector2 nameSize = ImGui.CalcTextSize(mobName); + Vector2 hpSize = ImGui.CalcTextSize(hpText); + float maxWidth = MathF.Max(nameSize.X, hpSize.X); + float left = screenPos.X - (maxWidth * 0.5f); + Vector2 namePos = new(left + (maxWidth - nameSize.X) * 0.5f, screenPos.Y); + Vector2 hpPos = new(left + (maxWidth - hpSize.X) * 0.5f, namePos.Y + nameSize.Y + 4); + + // Background covering both lines + Vector2 bgMin = new(left - 4, screenPos.Y - 2); + Vector2 bgMax = new(left + maxWidth + 4, hpPos.Y + hpSize.Y + 2); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); + + drawList.AddText(namePos, color, mobName); + drawList.AddText(hpPos, color, hpText); + } } + } - ImGui.Unindent(); + private void RenderPortalTextLabels() { + ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); + + // Render portal information + foreach (FieldPortal portal in Field.GetPortals()) { + Vector3 textPos = portal.Position + new Vector3(0, 0, 150); // Above the portal cube + if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; + + string enabledText = $"{(portal.Enabled ? "Enabled" : "Disabled")} | {(portal.Visible ? "Visible" : "Hidden")}"; + string idText = $"ID: {portal.Value.Id}"; + string targetText = $"Target: {portal.Value.TargetMapId}"; + string targetPortalText = $"Target Portal: {portal.Value.TargetPortalId}"; + string typeText = $"Type: {portal.Value.Type}"; + + Vector4 textColor = portal.Enabled ? new Vector4(0, 0.8f, 1, 1) : new Vector4(0.6f, 0.6f, 0.6f, 1); + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + Vector2 enabledSize = ImGui.CalcTextSize(enabledText); + Vector2 idSize = ImGui.CalcTextSize(idText); + Vector2 targetSize = ImGui.CalcTextSize(targetText); + Vector2 targetPortalSize = ImGui.CalcTextSize(targetPortalText); + Vector2 typeSize = ImGui.CalcTextSize(typeText); + + float maxWidth = Math.Max(Math.Max(Math.Max(enabledSize.X, idSize.X), Math.Max(targetSize.X, targetPortalSize.X)), typeSize.X); + float totalHeight = enabledSize.Y + idSize.Y + targetSize.Y + targetPortalSize.Y + typeSize.Y + 8; + float left = screenPos.X - (maxWidth * 0.5f); + + Vector2 bgMin = new(left - 4, screenPos.Y - 2); + Vector2 bgMax = new(left + maxWidth + 4, screenPos.Y + totalHeight + 4); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.8f))); + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.3f, 0.3f, 0.3f, 0.9f))); + + Vector2 currentPos = new(left, screenPos.Y); + drawList.AddText(currentPos, color, enabledText); + currentPos.Y += enabledSize.Y + 1; + drawList.AddText(currentPos, color, idText); + currentPos.Y += idSize.Y + 1; + drawList.AddText(currentPos, color, targetText); + currentPos.Y += targetSize.Y + 1; + drawList.AddText(currentPos, color, targetPortalText); + currentPos.Y += targetPortalSize.Y + 1; + drawList.AddText(currentPos, color, typeText); + } } - public void CleanUp() { } + private void RenderPlotTextLabels() { + if (!ShowSellableTiles || !ShowPlotLabels) return; + if (Field.AccelerationStructure == null) return; + + // We'll aggregate one label per SellableGroup using the first tile's center. + // Map group -> representative tile center & vertical extent + var groupInfo = new Dictionary(); + foreach (FieldEntity entity in GetAllEntities()) { + if (entity is not FieldSellableTile tile) continue; + Vector3 size = tile.Bounds.Max - tile.Bounds.Min; + Vector3 center = (tile.Bounds.Max + tile.Bounds.Min) * 0.5f; + center.Z -= size.Z * 0.5f; // match visual cube shift + if (groupInfo.TryGetValue(tile.SellableGroup, out var info)) { + // Average centers and track max Z for label stacking + Vector3 avg = (info.center + center) * 0.5f; + groupInfo[tile.SellableGroup] = (avg, MathF.Max(info.maxZ, center.Z + size.Z)); + } else { + groupInfo[tile.SellableGroup] = (center, center.Z + size.Z); + } + } - public void AttachWindow(DebugFieldWindow window) { - activeMutex.WaitOne(); + if (groupInfo.Count == 0) return; - activeWindows.Add(window); + ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); - activeMutex.ReleaseMutex(); + // Attempt to look up plot metadata via Field.Plots (number == SellableGroup) + foreach ((int group, (Vector3 baseCenter, float topZ)) in groupInfo) { + Vector3 labelWorld = new(baseCenter.X, baseCenter.Y, topZ + 120f); + if (!TryWorldToScreen(labelWorld, out Vector2 screenPos)) continue; + + Plot? plot = null; + if (Field.Plots.ContainsKey(group)) { + plot = Field.Plots[group]; + } + string state = plot?.State.ToString() ?? "Unknown"; + long ownerId = plot?.OwnerId ?? 0; + long expiry = plot?.ExpiryTime ?? 0; + string ownerText = ownerId == 0 ? "Unowned" : ownerId.ToString(); + string expiryText = expiry <= 0 ? "-" : DateTimeOffset.FromUnixTimeSeconds(expiry).ToLocalTime().ToString("yyyy-MM-dd HH:mm"); + string name = plot?.Name ?? string.Empty; + string header = string.IsNullOrEmpty(name) ? $"Plot {group}" : name; + string line2 = $"State: {state} | Owner: {ownerText}"; + string line3 = $"Expiry: {expiryText}"; + + Vector2 headerSize = ImGui.CalcTextSize(header); + Vector2 l2Size = ImGui.CalcTextSize(line2); + Vector2 l3Size = ImGui.CalcTextSize(line3); + float maxWidth = MathF.Max(headerSize.X, MathF.Max(l2Size.X, l3Size.X)); + float totalHeight = headerSize.Y + l2Size.Y + l3Size.Y + 8; + float left = screenPos.X - (maxWidth * 0.5f); + + Vector4 boxColor = new(0, 0, 0, 0.65f); + Vector4 borderColor = new(0.3f, 0.3f, 0.3f, 0.9f); + Vector4 headerColor = new(1f, 0.85f, 0.3f, 1f); + Vector4 textColor = new(0.9f, 0.9f, 0.9f, 1f); + + Vector2 bgMin = new(left - 4, screenPos.Y - 2); + Vector2 bgMax = new(left + maxWidth + 4, screenPos.Y + totalHeight + 4); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(boxColor)); + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); + + Vector2 cursor = new(left, screenPos.Y); + drawList.AddText(cursor, ImGui.ColorConvertFloat4ToU32(headerColor), header); + cursor.Y += headerSize.Y + 1; + drawList.AddText(cursor, ImGui.ColorConvertFloat4ToU32(textColor), line2); + cursor.Y += l2Size.Y + 1; + drawList.AddText(cursor, ImGui.ColorConvertFloat4ToU32(textColor), line3); + } } - public void DetachWindow(DebugFieldWindow window) { - activeMutex.WaitOne(); + private FieldEntity[] GetAllEntities() { + // Cache static entities since they never change during field runtime + if (cachedStaticEntities != null) { + return cachedStaticEntities; + } - activeWindows.Remove(window); + if (Field.AccelerationStructure == null) { + cachedStaticEntities = []; + return cachedStaticEntities; + } - activeMutex.ReleaseMutex(); + var allEntities = new List(); + + // Add all aligned entities + allEntities.AddRange(Field.AccelerationStructure.AlignedEntities.ToArray()); + + // Add all aligned trimmed entities + allEntities.AddRange(Field.AccelerationStructure.AlignedTrimmedEntities.ToArray()); + + // Add all unaligned entities + allEntities.AddRange(Field.AccelerationStructure.UnalignedEntities.ToArray()); + + // Add vibrate entities (they are a special type) + allEntities.AddRange(Field.AccelerationStructure.VibrateEntities.ToArray()); + + // Cache the result for future calls + cachedStaticEntities = allEntities.ToArray(); + return cachedStaticEntities; + } + + private void UpdateCameraProjection(DebugFieldWindow window) { + Vector2D windowSize = Context.DebuggerWindow?.FramebufferSize ?? DebugGraphicsContext.DefaultWindowSize; + CameraController.Camera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); + sceneViewBuffer.ViewMatrix = Matrix4x4.Transpose(CameraController.Camera.ViewMatrix); + sceneViewBuffer.ProjectionMatrix = Matrix4x4.Transpose(CameraController.Camera.ProjectionMatrix); + + window.SceneViewConstantBuffer.Update(in sceneViewBuffer); + + window.SceneState.BindConstantBuffer(window.SceneViewConstantBuffer, 0, Enum.ShaderStageFlags.Vertex); + window.SceneState.UpdateBindings(); + } + + /// + /// Switches to the free camera controller + /// + private void SwitchToFreeCameraController() { + if (CameraController != freeCameraController) { + CameraController = freeCameraController; + Logger.Information("Switched to free camera controller"); + } else { + Logger.Information("Already using FreeCameraController - no switch needed"); + } + } + + /// + /// Switches to the follow camera controller + /// + private void SwitchToFollowCameraController() { + if (CameraController != followCameraController) { + CameraController = followCameraController; + Logger.Information("Switched to follow camera controller"); + } + } + + /// + /// Gets the current controller type name for UI display + /// + public string GetCurrentControllerType() { + return CameraController switch { + FreeCameraController => "Free Camera", + FollowCameraController => "Follow Camera", + _ => "Unknown" + }; + } + + /// + /// Starts following a player and automatically switches to follow camera controller + /// + public void StartFollowingPlayer(FieldPlayer player) { + HasManuallyStoppedFollowing = false; // Reset manual stop flag when starting to follow + SwitchToFollowCameraController(); + followCameraController.StartFollowingPlayer(player.Value.Character.Id, player.Position); + } + + /// + /// Stops following a player and automatically switches to free camera controller + /// + public void StopFollowingPlayer() { + HasManuallyStoppedFollowing = true; // Remember that user manually stopped + followCameraController.StopFollowingPlayer(); + SwitchToFreeCameraController(); + } + + /// + /// Updates player follow position (only works when follow controller is active) + /// + public void UpdatePlayerFollow(Vector3 playerPosition) { + if (CameraController == followCameraController) { + followCameraController.UpdatePlayerFollow(playerPosition); + } + } + + private void UpdateWireframeInstance(DebugFieldWindow window) { + window.InstanceConstantBuffer.Update(in instanceBuffer); + + window.SceneState.BindConstantBuffer(window.InstanceConstantBuffer, 1, Enum.ShaderStageFlags.Vertex); + window.SceneState.UpdateBindings(); } } diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index 448ac3af1..1d238bd71 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -1,4 +1,8 @@ -using Serilog; +using System.Numerics; +using Maple2.Server.DebugGame.Graphics.Data; +using Maple2.Server.DebugGame.Graphics.Resources; +using Maple2.Server.DebugGame.Graphics.Scene; +using Serilog; using Silk.NET.Core.Native; using Silk.NET.Direct3D11; using Silk.NET.DXGI; @@ -9,23 +13,32 @@ namespace Maple2.Server.DebugGame.Graphics; public class DebugFieldWindow { - public DebugGraphicsContext Context { get; init; } + private DebugGraphicsContext Context { get; } public DebugFieldRenderer? ActiveRenderer { get; private set; } public IWindow? DebuggerWindow { get; private set; } - public IInputContext? Input { get; private set; } - public ComPtr DxSwapChain { get; private set; } - public ImGuiController? ImGuiController { get; private set; } - public int WindowId { get; init; } - public string WindowName { get; init; } + private IInputContext? Input { get; set; } + private ComPtr DxSwapChain { get; set; } + private ImGuiController? ImGuiController { get; set; } + private int WindowId { get; } + public string WindowName { get; } public bool IsInitialized { get; private set; } public bool IsClosing { get; private set; } + public SceneState SceneState { get; init; } - private static int _windowIdCounter = 0; + public ConstantBuffer SceneViewConstantBuffer { get; init; } + public ConstantBuffer InstanceConstantBuffer { get; init; } + + private static int _windowIdCounter; public DebugFieldWindow(DebugGraphicsContext context) { Context = context; WindowId = _windowIdCounter++; WindowName = $"Window {WindowId}"; + + SceneState = new SceneState(Context); + + SceneViewConstantBuffer = new ConstantBuffer(Context); + InstanceConstantBuffer = new ConstantBuffer(Context); } public void SetActiveRenderer(DebugFieldRenderer? renderer) { @@ -50,7 +63,7 @@ public void Initialize() { IsInitialized = true; var windowOptions = WindowOptions.Default; - windowOptions.Size = DebugGraphicsContext.DefaultWindowSize; + windowOptions.Size = DebugGraphicsContext.DefaultFieldWindowSize; windowOptions.Title = $"Maple2 - {WindowName}"; windowOptions.API = GraphicsAPI.None; windowOptions.FramesPerSecond = 60; @@ -69,6 +82,14 @@ public void Initialize() { Log.Information("Creating field renderer window"); + SceneViewBuffer sceneViewBuffer = new(); + + SceneViewConstantBuffer.Initialize(in sceneViewBuffer); + + InstanceBuffer instanceBuffer = new(); + + InstanceConstantBuffer.Initialize(in instanceBuffer); + DebuggerWindow.Initialize(); } @@ -97,9 +118,7 @@ public void CleanUp() { } - private void OnClose() { - - } + private void OnClose() { } private void OnLoad() { Input = DebuggerWindow!.CreateInput(); @@ -135,7 +154,7 @@ private void OnLoad() { ImGuiController.Initialize(DebuggerWindow); } - public unsafe void OnUpdate(double delta) { + public void OnUpdate(double delta) { if (IsClosing) { return; } @@ -143,6 +162,10 @@ public unsafe void OnUpdate(double delta) { if (ActiveRenderer is not null) { ActiveRenderer.Update(); DebuggerWindow!.Title = $"Maple2 - {WindowName}: {ActiveRenderer.Field.Metadata.Name} [{ActiveRenderer.Field.MapId}] ({ActiveRenderer.Field.RoomId})"; + + // Update camera input state and let FreeCameraController handle input + UpdateCameraInputState(); + ActiveRenderer.CameraController.Update((float) delta); } } @@ -151,6 +174,12 @@ public unsafe void OnRender(double delta) { return; } + if (!(Context.ShaderPipelines?.CanRender ?? false)) { + return; + } + + Context.ShaderPipelines?.StartedFrame(); + DebuggerWindow!.MakeCurrent(); ComPtr framebuffer = DxSwapChain.GetBuffer(0); @@ -170,14 +199,14 @@ public unsafe void OnRender(double delta) { #region Render code // Begin region for render code - Context.VertexShader!.Bind(); - Context.PixelShader!.Bind(); - Context.SampleTexture!.Bind(); - Context.CoreModels!.Quad.Draw(); + // SceneState.BindTexture(Context.SampleTexture!); - if (ActiveRenderer is not null) { - ActiveRenderer.Render(delta); - } + SceneState.BindConstantBuffer(SceneViewConstantBuffer, 0, Enum.ShaderStageFlags.Vertex); + SceneState.BindConstantBuffer(InstanceConstantBuffer, 1, Enum.ShaderStageFlags.Vertex); + SceneState.UpdateBindings(); + + // Render 3D field + ActiveRenderer?.RenderFieldWindow3D(this, delta); // End region for render code #endregion @@ -186,12 +215,13 @@ public unsafe void OnRender(double delta) { DxSwapChain.Present(1, 0); + Context.ShaderPipelines?.EndedFrame(); + renderTargetView.Dispose(); framebuffer.Dispose(); - } - private unsafe void OnFramebufferResize(Vector2D newSize) { + private void OnFramebufferResize(Vector2D newSize) { // there is currently a bug with resizing where the framebuffer positioning doesn't take into account title bar size SilkMarshal.ThrowHResult(DxSwapChain.ResizeBuffers(0, (uint) newSize.X, (uint) newSize.Y, Format.FormatB8G8R8A8Unorm, 0)); } @@ -200,11 +230,68 @@ public void Close() { IsClosing = true; } - private void UnloadField(DebugFieldRenderer renderer) { + private void UnloadField(DebugFieldRenderer renderer) { } + + private void LoadField(DebugFieldRenderer renderer) { } + + private void UpdateCameraInputState() { + if (Input == null) return; + + InputState inputState = ActiveRenderer!.CameraController.InputState; + IKeyboard keyboard = Input.Keyboards[0]; + IMouse mouse = Input.Mice[0]; + + // Update InputFocused - camera should be active when window is focused + inputState.InputFocused = true; + + // Update keyboard state for specific keys we care about + Key[] keysToTrack = [ + Key.W, + Key.A, + Key.S, + Key.D, + Key.Q, + Key.E, + Key.ShiftLeft, + Key.ShiftRight, + Key.ControlLeft, + Key.ControlRight, + Key.Space, + Key.Escape, + Key.Tab, + Key.Enter, + Key.Up, + Key.Down, + Key.Left, + Key.Right, + ]; + + foreach (Key key in keysToTrack) { + int keyIndex = (int) key; + if (keyIndex >= 0 && keyIndex < inputState.KeyStates.Length) { + inputState.KeyStates[keyIndex].LastInput = inputState.KeyStates[keyIndex].IsDown; + inputState.KeyStates[keyIndex].IsDown = keyboard.IsKeyPressed(key); + } + } - } + // Update mouse button states + inputState.MouseLeft.LastInput = inputState.MouseLeft.IsDown; + inputState.MouseLeft.IsDown = mouse.IsButtonPressed(MouseButton.Left); - private void LoadField(DebugFieldRenderer renderer) { + inputState.MouseMiddle.LastInput = inputState.MouseMiddle.IsDown; + inputState.MouseMiddle.IsDown = mouse.IsButtonPressed(MouseButton.Middle); + inputState.MouseRight.LastInput = inputState.MouseRight.IsDown; + inputState.MouseRight.IsDown = mouse.IsButtonPressed(MouseButton.Right); + + // Update mouse position + inputState.MousePosition.LastPosition = inputState.MousePosition.Position; + inputState.MousePosition.Position = new Vector3(mouse.Position.X, mouse.Position.Y, 0); + + // Update mouse wheel + inputState.MouseWheel.LastPosition = inputState.MouseWheel.Position; + if (mouse.ScrollWheels.Count > 0) { + inputState.MouseWheel.Position = new Vector3(mouse.ScrollWheels[0].X, mouse.ScrollWheels[0].Y, 0); + } } } diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 62435ce4b..ddfa3c50a 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -10,7 +10,7 @@ using Silk.NET.Maths; using Silk.NET.Windowing; using Maple2.Server.DebugGame.Graphics.Assets; -using Maple2.Server.Game.Model; +using Maple2.Server.DebugGame.Graphics.Scene; using Maple2.Tools.Extensions; namespace Maple2.Server.DebugGame.Graphics; @@ -18,30 +18,46 @@ namespace Maple2.Server.DebugGame.Graphics; public class DebugGraphicsContext : IGraphicsContext { private const bool ForceDxvk = false; public static readonly Vector2D DefaultWindowSize = new Vector2D(800, 600); + public static readonly Vector2D DefaultFieldWindowSize = new Vector2D(1920, 1080); public static readonly float[] WindowClearColor = [0.0f, 0.0f, 0.0f, 1.0f]; - private static readonly ILogger Logger = Log.Logger.ForContext(); + public static readonly ILogger Logger = Log.Logger.ForContext(); private readonly Dictionary fields = []; - private IWindow? DebuggerWindow { get; set; } + public IWindow? DebuggerWindow { get; set; } private IInputContext? Input { get; set; } - public D3D11? D3d11 { get; private set; } - public DXGI? Dxgi { get; private set; } + private D3D11? D3d11 { get; set; } + private DXGI? Dxgi { get; set; } public D3DCompiler? Compiler { get; private set; } public ComPtr DxDevice { get; private set; } public ComPtr DxDeviceContext { get; private set; } public ComPtr DxFactory { get; private set; } - public ComPtr DxSwapChain { get; private set; } - public VertexShader? VertexShader { get; private set; } - public PixelShader? PixelShader { get; private set; } + private ComPtr DxSwapChain { get; set; } + + private string resourceRootPath = ""; + + public ShaderPipeline? ScreenPipeline { get; private set; } + public ShaderPipeline? DebugScenePipeline { get; private set; } + public ShaderPipeline? WireframePipeline { get; private set; } + public ShaderPipelines? ShaderPipelines { get; private set; } + public SceneState SceneState { get; init; } + + // 3D rendering resources + private ComPtr DepthStencilBuffer { get; set; } + private ComPtr DepthStencilView { get; set; } + private ComPtr DepthStencilState { get; set; } + private ComPtr SolidRasterizerState { get; set; } + private ComPtr WireframeRasterizerState { get; set; } + // private ComPtr ConstantBuffer { get; set; } public CoreModels? CoreModels { get; private set; } - public Texture? SampleTexture; - public ImGuiController? ImGuiController { get; private set; } + private Texture? sampleTexture; + private ImGuiController? ImGuiController { get; set; } - private string resourceRootPath = ""; + // Rendering state + public bool WireframeMode { get; set; } = true; // Start in wireframe mode private readonly List fieldRenderers = []; private readonly Mutex fieldRendererMutex = new(); @@ -58,13 +74,22 @@ public DebugFieldRenderer[] FieldRenderers { private readonly List fieldWindows = []; private readonly HashSet updatedFields = []; private readonly object updatedFieldsLock = new(); - private int deltaIndex = 0; + private int deltaIndex; private readonly List deltaTimes = []; public int DeltaAverage { get; private set; } + + // Cleanup tracking + private bool isCleanedUp; + + // Performance tracking public int DeltaMin { get; private set; } public int DeltaMax { get; private set; } private DateTime lastTime = DateTime.Now; - public bool IsClosing { get; private set; } + private bool IsClosing { get; set; } + + public DebugGraphicsContext() { + SceneState = new(this); + } public void RunDebugger() { DebuggerWindow!.Initialize(); @@ -73,10 +98,16 @@ public void RunDebugger() { while (!(DebuggerWindow?.IsClosing ?? true) || subWindowsUpdating) { if (DebuggerWindow is not null && !DebuggerWindow.IsClosing) { - UpdateWindow(DebuggerWindow, IsClosing); + try { + UpdateWindow(DebuggerWindow, IsClosing); - if (DebuggerWindow.IsClosing) { + if (DebuggerWindow is not null && DebuggerWindow.IsClosing) { + CleanUp(); + } + } catch (Exception ex) { + Logger.Warning(ex, "Exception during window update"); CleanUp(); + break; } } @@ -100,7 +131,6 @@ public void RunDebugger() { } } } - } public bool UpdateWindow(IView window, bool shouldClose) { @@ -187,8 +217,8 @@ private void OnLoad() { D3d11 = D3D11.GetApi(DebuggerWindow); Compiler = D3DCompiler.GetApi(); - Shader.ShaderRootPath = GetResourceRootPath("Shaders"); Texture.TextureRootPath = GetResourceRootPath("Textures"); + ShaderPipelines.AssetRootPath = GetResourceRootPath(""); unsafe { ComPtr device = default; @@ -240,66 +270,176 @@ private void OnLoad() { DxSwapChain = swapChain; } - if (VertexShader is null) { - VertexShader = new VertexShader(this); - } + // Create 3D rendering resources + Create3DRenderingResources(); - if (PixelShader is null) { - PixelShader = new PixelShader(this); - } + ShaderPipelines ??= new(this); - VertexShader.Load("screenVertex.hlsl", "vs_main"); - PixelShader.Load("screenPixel.hlsl", "ps_main"); + LoadPipelines(); CoreModels = new CoreModels(this); Logger.Information("Graphics context initialized"); - SampleTexture = new Texture(this); - SampleTexture.Load("sample_derp_wave.png"); + sampleTexture = new Texture(this); + sampleTexture.Load(Path.Combine(Texture.TextureRootPath, "sample_derp_wave.png")); ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); - ImGuiController.Initialize(DebuggerWindow); + if (DebuggerWindow is not null) { + ImGuiController.Initialize(DebuggerWindow); + } } - private string GetResourceRootPath(string rootPath) { - resourceRootPath = Environment.CurrentDirectory; + private void LoadPipelines() { + ShaderPipelines!.LoadPipelines(); - if (File.Exists("root_path.txt")) { - resourceRootPath = Path.Combine(resourceRootPath, File.ReadLines("root_path.txt").First()); + if (!ShaderPipelines!.Pipelines.TryGetValue("Screen", out ShaderPipeline? screenPipeline)) { + ShaderPipelines!.ExternalError(); + + Logger.Error("No 'Screen' shader pipeline defined"); } - return Path.GetFullPath(Path.Combine(resourceRootPath, rootPath)); + if (!ShaderPipelines!.Pipelines.TryGetValue("DebugScene", out ShaderPipeline? debugScenePipeline)) { + ShaderPipelines!.ExternalError(); + + Logger.Error("No 'DebugScene' shader pipeline defined"); + } + + if (!ShaderPipelines!.Pipelines.TryGetValue("Wireframe", out ShaderPipeline? wireframePipeline)) { + ShaderPipelines!.ExternalError(); + + Logger.Error("No 'DebugScene' shader pipeline defined"); + } + + ScreenPipeline = screenPipeline; + DebugScenePipeline = debugScenePipeline; + WireframePipeline = wireframePipeline; + } + + private unsafe void Create3DRenderingResources() { + Vector2D windowSize = DebuggerWindow!.FramebufferSize; + + // Create depth stencil buffer + var depthBufferDesc = new Texture2DDesc { + Width = (uint) windowSize.X, + Height = (uint) windowSize.Y, + MipLevels = 1, + ArraySize = 1, + Format = Format.FormatD24UnormS8Uint, + SampleDesc = new SampleDesc(1, 0), + Usage = Usage.Default, + BindFlags = (uint) BindFlag.DepthStencil, + CPUAccessFlags = 0, + MiscFlags = 0, + }; + + ID3D11Texture2D* depthStencilBuffer = null; + SilkMarshal.ThrowHResult(DxDevice.CreateTexture2D(&depthBufferDesc, (SubresourceData*) null, &depthStencilBuffer)); + DepthStencilBuffer = depthStencilBuffer; + + // Create depth stencil view + var depthStencilViewDesc = new DepthStencilViewDesc { + Format = Format.FormatD24UnormS8Uint, + ViewDimension = DsvDimension.Texture2D, + Flags = 0, + }; + depthStencilViewDesc.Anonymous.Texture2D.MipSlice = 0; + + ID3D11DepthStencilView* depthStencilView = null; + SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*) DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); + DepthStencilView = depthStencilView; + + // Create depth stencil state + var depthStencilDesc = new DepthStencilDesc { + DepthEnable = true, + DepthWriteMask = DepthWriteMask.All, + DepthFunc = ComparisonFunc.Less, + StencilEnable = false, + }; + + ID3D11DepthStencilState* depthStencilState = null; + SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilState(&depthStencilDesc, &depthStencilState)); + DepthStencilState = depthStencilState; + + // Create rasterizer states + var solidRasterizerDesc = new RasterizerDesc { + FillMode = FillMode.Solid, + CullMode = CullMode.Back, + FrontCounterClockwise = false, + DepthBias = 0, + DepthBiasClamp = 0.0f, + SlopeScaledDepthBias = 0.0f, + DepthClipEnable = true, + ScissorEnable = false, + MultisampleEnable = false, + AntialiasedLineEnable = false, + }; + + ID3D11RasterizerState* solidRasterizerState = null; + SilkMarshal.ThrowHResult(DxDevice.CreateRasterizerState(&solidRasterizerDesc, &solidRasterizerState)); + SolidRasterizerState = solidRasterizerState; + + RasterizerDesc wireframeRasterizerDesc = solidRasterizerDesc; + wireframeRasterizerDesc.FillMode = FillMode.Wireframe; + wireframeRasterizerDesc.CullMode = CullMode.None; + + ID3D11RasterizerState* wireframeRasterizerState = null; + SilkMarshal.ThrowHResult(DxDevice.CreateRasterizerState(&wireframeRasterizerDesc, &wireframeRasterizerState)); + WireframeRasterizerState = wireframeRasterizerState; + } + + /// + /// Toggles wireframe rendering mode + /// + public void ToggleWireframeMode() { + WireframeMode = !WireframeMode; + Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); } public void CleanUp() { + if (isCleanedUp) return; // Prevent multiple cleanup calls + isCleanedUp = true; + unsafe { if (DxDevice.Handle is not null) { - VertexShader?.CleanUp(); - PixelShader?.CleanUp(); + ShaderPipelines?.CleanUp(); + + // Clean up 3D resources + DepthStencilBuffer.Dispose(); + DepthStencilView.Dispose(); + DepthStencilState.Dispose(); + SolidRasterizerState.Dispose(); + WireframeRasterizerState.Dispose(); + DxDevice.Dispose(); DxDeviceContext.Dispose(); DxSwapChain.Dispose(); Input?.Dispose(); - VertexShader = null; - PixelShader = null; + ShaderPipelines = null; DxDevice = default; DxDeviceContext = default; DxSwapChain = default; + DepthStencilBuffer = default; + DepthStencilView = default; + DepthStencilState = default; + SolidRasterizerState = default; + WireframeRasterizerState = default; Input = null; Logger.Information("Graphics context cleaning up"); } } - if (DebuggerWindow is not null) { - DebuggerWindow.Dispose(); - - DebuggerWindow = null; - - Logger.Information("Window cleaning up"); + try { + if (DebuggerWindow is not null) { + DebuggerWindow.Dispose(); + DebuggerWindow = null; + Logger.Information("Window cleaning up"); + } + } catch (Exception ex) { + Logger.Warning(ex, "Exception during window cleanup"); } } @@ -329,7 +469,7 @@ public IFieldRenderer FieldAdded(FieldManager field) { DebugFieldRenderer renderer = new DebugFieldRenderer(this, field); fieldRendererMutex.WaitOne(); - int index = fieldRenderers.AddSorted(renderer, Comparer.Create(CompareRenderers)); + fieldRenderers.AddSorted(renderer, Comparer.Create(CompareRenderers)); fieldRendererMutex.ReleaseMutex(); fields[field] = renderer; @@ -368,8 +508,48 @@ public void FieldWindowClosed(DebugFieldWindow window) { } private unsafe void OnFramebufferResize(Vector2D newSize) { + // Validate size to prevent errors with invalid dimensions + if (newSize.X <= 0 || newSize.Y <= 0) { + return; // Skip resize if dimensions are invalid + } + // there is currently a bug with resizing where the framebuffer positioning doesn't take into account title bar size SilkMarshal.ThrowHResult(DxSwapChain.ResizeBuffers(0, (uint) newSize.X, (uint) newSize.Y, Format.FormatB8G8R8A8Unorm, 0)); + + // Recreate depth buffer for new size + if (DepthStencilBuffer.Handle != null) { + DepthStencilBuffer.Dispose(); + DepthStencilView.Dispose(); + + // Recreate depth buffer with new size + var depthBufferDesc = new Texture2DDesc { + Width = (uint) newSize.X, + Height = (uint) newSize.Y, + MipLevels = 1, + ArraySize = 1, + Format = Format.FormatD24UnormS8Uint, + SampleDesc = new SampleDesc(1, 0), + Usage = Usage.Default, + BindFlags = (uint) BindFlag.DepthStencil, + CPUAccessFlags = 0, + MiscFlags = 0, + }; + + ID3D11Texture2D* depthStencilBuffer = null; + SilkMarshal.ThrowHResult(DxDevice.CreateTexture2D(&depthBufferDesc, (SubresourceData*) null, &depthStencilBuffer)); + DepthStencilBuffer = depthStencilBuffer; + + var depthStencilViewDesc = new DepthStencilViewDesc { + Format = Format.FormatD24UnormS8Uint, + ViewDimension = DsvDimension.Texture2D, + Flags = 0, + }; + depthStencilViewDesc.Anonymous.Texture2D.MipSlice = 0; + + ID3D11DepthStencilView* depthStencilView = null; + SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*) DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); + DepthStencilView = depthStencilView; + } } public bool HasFieldUpdated(FieldManager field) { @@ -388,6 +568,16 @@ private void OnUpdate(double delta) { lock (updatedFieldsLock) { updatedFields.Clear(); } + + if (ShaderPipelines is null) { + return; + } + + if (ShaderPipelines.CompilingShaders && ShaderPipelines.RenderingFrames == 0) { + ShaderPipelines.CleanUp(); + + LoadPipelines(); + } } private void UpdateDeltaTracker() { @@ -431,6 +621,11 @@ private void UpdateDeltaTracker() { } private unsafe void OnRender(double delta) { + if (!(ShaderPipelines?.CanRender ?? false)) { + return; + } + + ShaderPipelines?.StartedFrame(); UpdateDeltaTracker(); DebuggerWindow!.MakeCurrent(); @@ -440,36 +635,52 @@ private unsafe void OnRender(double delta) { ComPtr renderTargetView = default; SilkMarshal.ThrowHResult(DxDevice.CreateRenderTargetView(framebuffer, null, ref renderTargetView)); + // Clear both color and depth buffers DxDeviceContext.ClearRenderTargetView(renderTargetView, WindowClearColor); + DxDeviceContext.ClearDepthStencilView(DepthStencilView, (uint) (ClearFlag.Depth | ClearFlag.Stencil), 1.0f, 0); Viewport viewport = new Viewport(0, 0, DebuggerWindow!.FramebufferSize.X, DebuggerWindow!.FramebufferSize.Y, 0, 1); DxDeviceContext.RSSetViewports(1, in viewport); - DxDeviceContext.OMSetRenderTargets(1, in renderTargetView.Handle, (ID3D11DepthStencilView*) null); - - ImGuiController!.BeginFrame((float) delta); - - #region Render code - // Begin region for render code - - // A vertex + pixel shader required to draw meshes - VertexShader!.Bind(); - PixelShader!.Bind(); - SampleTexture!.Bind(); // bind textures to active GPU texture samplers for access in shaders - CoreModels!.Quad.Draw(); // draw a full screen quad/rectangle + // Set render targets with depth buffer + DxDeviceContext.OMSetRenderTargets(1, in renderTargetView.Handle, DepthStencilView); - // logic - //FieldListWindow(); - //RendererListWindow(); + // Set depth stencil state + DxDeviceContext.OMSetDepthStencilState(DepthStencilState, 1); - // End region for render code - #endregion + // Set rasterizer state based on wireframe mode + DxDeviceContext.RSSetState(WireframeMode ? WireframeRasterizerState : SolidRasterizerState); + ImGuiController!.BeginFrame((float) delta); ImGuiController!.EndFrame(); DxSwapChain.Present(1, 0); + ShaderPipelines?.EndedFrame(); + renderTargetView.Dispose(); framebuffer.Dispose(); } + + public void SetWireframeRasterizer() { + // Set wireframe rasterizer state + // This would typically involve setting D3D11 rasterizer state + // For now, we'll rely on the WireframeMode flag + } + + public void SetSolidRasterizer() { + // Set solid rasterizer state + // This would typically involve setting D3D11 rasterizer state + // For now, we'll rely on the WireframeMode flag + } + + private string GetResourceRootPath(string rootPath) { + resourceRootPath = Environment.CurrentDirectory; + + if (File.Exists("root_path.txt")) { + resourceRootPath = Path.Combine(resourceRootPath, File.ReadLines("root_path.txt").First()); + } + + return Path.GetFullPath(Path.Combine(resourceRootPath, rootPath)); + } } diff --git a/Maple2.Server.DebugGame/Graphics/Enum/ShaderStageFlags.cs b/Maple2.Server.DebugGame/Graphics/Enum/ShaderStageFlags.cs new file mode 100644 index 000000000..af878a6cf --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Enum/ShaderStageFlags.cs @@ -0,0 +1,9 @@ +namespace Maple2.Server.DebugGame.Graphics.Enum; + +[Flags] +public enum ShaderStageFlags { + None = 0, + + Vertex = (1 << 0), + Pixel = (1 << 1), +} diff --git a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs index e5c9751ac..be6f2c17d 100644 --- a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs +++ b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs @@ -6,6 +6,7 @@ using Silk.NET.Windowing; using System.Drawing; using System.Numerics; +using Maple2.Tools.Extensions; namespace Maple2.Server.DebugGame.Graphics; @@ -15,15 +16,51 @@ public enum ImGuiWindowType { Field, } +public struct KeyState { + public bool IsDown; + public bool LastInput; + public bool Changed => LastInput != IsDown; + public bool IsPressed => IsDown && Changed; + public bool IsReleased => !IsDown && Changed; + public bool IsHeld => IsDown && !Changed; + public bool IsIdle => !IsDown && !Changed; +} + +public struct AnalogInput { + public Vector3 Position; + public Vector3 LastPosition; + public Vector3 Delta => Position - LastPosition; + public bool Moved => !Position.IsNearlyEqual(LastPosition); + public bool Lock; +} + +public class InputState { + public bool CapturedKeyboard; + public bool CapturedMouse; + public bool InputFocused; + public KeyState[] KeyStates { get; init; } = new KeyState[(int) Key.Menu + 1]; + public KeyState MouseLeft = new(); + public KeyState MouseMiddle = new(); + public KeyState MouseRight = new(); + public AnalogInput MousePosition = new(); + public AnalogInput MouseWheel = new(); + + public KeyState GetState(Key key) => KeyStates[(int) key]; +} + public class ImGuiController { public static readonly ILogger Logger = Log.Logger.ForContext(); - public DebugGraphicsContext Context { get; init; } - public IWindow? ParentWindow { get; private set; } - public IInputContext Input { get; init; } - public IntPtr ImGuiContext { get; private set; } - public ImGuiWindowType WindowType { get; init; } - private List pressedCharacters; + // Global config: when true, ClampWindowToViewport will keep only the title bar inside the viewport + // if callers don't explicitly provide the override flag. + public static bool ClampTitleBarOnlyDefault = false; + + private DebugGraphicsContext Context { get; init; } + private IWindow? ParentWindow { get; set; } + private IInputContext Input { get; init; } + private IntPtr ImGuiContext { get; set; } + private ImGuiWindowType WindowType { get; init; } + private readonly List pressedCharacters; private List uiWindows = []; private Dictionary uiWindowMap = new(); @@ -60,7 +97,7 @@ private void InitializeWindows(DebugFieldWindow? fieldWindow) { } foreach (Type type in _uiWindowTypes) { - var windowObject = Activator.CreateInstance(type); + object? windowObject = Activator.CreateInstance(type); if (windowObject is not IUiWindow window) { throw new InvalidCastException($"Type {type.Name} doesn't implement IUiWindow"); @@ -111,7 +148,7 @@ public void CleanUp() { ImGuiNative.igImGui_ImplDX11_Shutdown(); ImGui.DestroyContext(ImGuiContext); - ImGuiContext = default; + ImGuiContext = 0; Input.ConnectionChanged -= OnConnectionChanged; @@ -196,7 +233,7 @@ public void RenderWindows() { comboFlags |= ImGuiComboFlags.WidthFitPreview; if (ImGui.BeginCombo("##TOOLBAR COMBO", "View", comboFlags)) { - foreach (IUiWindow window in uiWindows) { + foreach (IUiWindow window in uiWindows.Where(x => !x.HideFromMenuBar)) { bool toggle = ImGui.Selectable(window.TypeName, window.Enabled); if (toggle) { @@ -207,11 +244,19 @@ public void RenderWindows() { ImGui.EndCombo(); } + // Place Settings combo on the same line as View + ImGui.SameLine(); + if (ImGui.BeginCombo("##TOOLBAR SETTINGS", "Settings", comboFlags)) { + // Global options + ImGui.Checkbox("Clamp title bar only", ref ClampTitleBarOnlyDefault); + + ImGui.EndCombo(); + } + ImGui.PopStyleVar(2); // WindowBorderSize, WindowPadding ImGui.End(); - foreach (IUiWindow window in uiWindows) { if (!window.Enabled) { continue; @@ -221,6 +266,68 @@ public void RenderWindows() { } } + /// + /// Clamp current ImGui window position & size so it stays fully visible inside the main viewport work area. + /// Call at end of each window's Render() after ImGui.Begin but before ImGui.End. + /// + /// Optional padding inside the viewport bounds. + public static void ClampWindowToViewport(float padding = 4f) { + ImGuiViewportPtr vp = ImGui.GetMainViewport(); + Vector2 workPos = vp.WorkPos; // top-left + Vector2 workSize = vp.WorkSize; // available size + Vector2 min = new(workPos.X + padding, workPos.Y + padding); + Vector2 max = new(workPos.X + workSize.X - padding, workPos.Y + workSize.Y - padding); + + Vector2 pos = ImGui.GetWindowPos(); + Vector2 size = ImGui.GetWindowSize(); + + if (ClampTitleBarOnlyDefault) { + // Estimate title bar height using current style + ImGuiStylePtr style = ImGui.GetStyle(); + float titleBarHeight = ImGui.GetFontSize() + style.FramePadding.Y * 2f; + + float newX = pos.X; + float newY = pos.Y; + + // Compute allowed ranges for top-left of title bar + float minXAllowed = min.X; + float maxXAllowed = max.X - size.X; + float minYAllowed = min.Y; + float maxYAllowed = max.Y - titleBarHeight; + + // If the window/titlebar is wider/taller than work area, prefer keeping the left/top aligned to min + if (maxXAllowed < minXAllowed) newX = minXAllowed; else newX = Math.Clamp(newX, minXAllowed, maxXAllowed); + if (maxYAllowed < minYAllowed) newY = minYAllowed; else newY = Math.Clamp(newY, minYAllowed, maxYAllowed); + + if (Math.Abs(newX - pos.X) > 0.5f || Math.Abs(newY - pos.Y) > 0.5f) { + ImGui.SetWindowPos(new Vector2(newX, newY), ImGuiCond.Always); + } + + return; + } + + // If window size exceeds work area, shrink it first + bool resized = false; + if (size.X > max.X - min.X) { size.X = MathF.Max(50f, max.X - min.X); resized = true; } + if (size.Y > max.Y - min.Y) { size.Y = MathF.Max(50f, max.Y - min.Y); resized = true; } + if (resized) ImGui.SetWindowSize(size, ImGuiCond.Always); + + // Re-fetch possibly updated size + size = ImGui.GetWindowSize(); + + // Clamp position for full body + float newBodyX = pos.X; + float newBodyY = pos.Y; + if (newBodyX < min.X) newBodyX = min.X; + if (newBodyY < min.Y) newBodyY = min.Y; + if (newBodyX + size.X > max.X) newBodyX = max.X - size.X; + if (newBodyY + size.Y > max.Y) newBodyY = max.Y - size.Y; + + if (Math.Abs(newBodyX - pos.X) > 0.5f || Math.Abs(newBodyY - pos.Y) > 0.5f) { + ImGui.SetWindowPos(new Vector2(newBodyX, newBodyY), ImGuiCond.Always); + } + } + private void RegisterKeyboard(IKeyboard keyboard) { keyboard.KeyUp += OnKeyUp; keyboard.KeyDown += OnKeyDown; diff --git a/Maple2.Server.DebugGame/Graphics/Resources/ConstantBuffer.cs b/Maple2.Server.DebugGame/Graphics/Resources/ConstantBuffer.cs new file mode 100644 index 000000000..531b4519a --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Resources/ConstantBuffer.cs @@ -0,0 +1,139 @@ +using Silk.NET.Core.Native; +using Silk.NET.Direct3D11; + +namespace Maple2.Server.DebugGame.Graphics.Resources; + +public class ConstantBuffer { + public DebugGraphicsContext Context { get; init; } + public ComPtr Buffer { get; private set; } + public int ResourceSize { get; private set; } + public int ResourceCount { get; private set; } + + public ConstantBuffer(DebugGraphicsContext context) { + Context = context; + } + +#pragma warning disable CS8500 + public unsafe void Initialize(in T data, Usage usage = Usage.Default, CpuAccessFlag cpuAccess = CpuAccessFlag.None) where T : notnull { + ResourceSize = sizeof(T); + ResourceCount = 1; + + fixed (T* dataPointer = &data) { + BufferDesc bufferDesc = new BufferDesc() { + ByteWidth = (uint) ResourceSize, + Usage = usage, + BindFlags = (uint) (BindFlag.ConstantBuffer), + CPUAccessFlags = (uint) cpuAccess, + MiscFlags = 0, + StructureByteStride = (uint) ResourceSize, + }; + + SubresourceData initialData = new SubresourceData() { + PSysMem = dataPointer, + SysMemPitch = 0, + SysMemSlicePitch = 0, + }; + + ID3D11Buffer* buffer = null; + + SilkMarshal.ThrowHResult(Context.DxDevice.CreateBuffer( + pDesc: ref bufferDesc, + pInitialData: ref initialData, + ppBuffer: ref buffer)); + + Buffer = buffer; + } + } + + public unsafe void Initialize(T[] data, Usage usage = Usage.Default, CpuAccessFlag cpuAccess = CpuAccessFlag.None) where T : notnull { + ResourceSize = sizeof(T); + ResourceCount = data.Length; + + fixed (T* dataPointer = &data[0]) { + BufferDesc bufferDesc = new BufferDesc() { + ByteWidth = (uint) (ResourceSize * ResourceCount), + Usage = usage, + BindFlags = (uint) (BindFlag.ConstantBuffer), + CPUAccessFlags = (uint) cpuAccess, + MiscFlags = 0, + StructureByteStride = (uint) ResourceSize, + }; + + SubresourceData initialData = new SubresourceData() { + PSysMem = dataPointer, + SysMemPitch = 0, + SysMemSlicePitch = 0, + }; + + ID3D11Buffer* buffer = null; + + SilkMarshal.ThrowHResult(Context.DxDevice.CreateBuffer( + pDesc: ref bufferDesc, + pInitialData: ref initialData, + ppBuffer: ref buffer)); + + Buffer = buffer; + } + } + + public unsafe void Update(in T data) { + int resourceSize = sizeof(T); + int resourceCount = 1; + + if (Buffer.Handle is null) { + throw new InvalidOperationException($"Attempt to upload {resourceCount} objects of size {resourceSize} bytes to uninitialized constant buffer"); + } + + if (resourceSize != ResourceSize || resourceCount != ResourceCount) { + throw new ArgumentException($"Constant buffer initialized with {ResourceCount} objects of size {ResourceSize} bytes, attempt to update with {resourceCount} objects of size {resourceSize} bytes"); + } + + ID3D11Resource* bufferPointer = (ID3D11Resource*) (ID3D11Buffer*) Buffer; + + fixed (T* dataPointer = &data) { + Box* box = null; + Context.DxDeviceContext.UpdateSubresource( + pDstResource: bufferPointer, + DstSubresource: 0u, + pDstBox: box, + pSrcData: dataPointer, + SrcRowPitch: 0u, + SrcDepthPitch: 0u); + } + } + + public unsafe void Update(T[] data) { + int resourceSize = sizeof(T); + int resourceCount = data.Length; + + if (Buffer.Handle is null) { + throw new InvalidOperationException($"Attempt to upload {resourceCount} objects of size {resourceSize} bytes to uninitialized constant buffer"); + } + + if (resourceSize != ResourceSize || resourceCount != ResourceCount) { + throw new ArgumentException($"Constant buffer initialized with {ResourceCount} objects of size {ResourceSize} bytes, attempt to update with {resourceCount} objects of size {resourceSize} bytes"); + } + + ID3D11Resource* bufferPointer = (ID3D11Resource*) (ID3D11Buffer*) Buffer; + + fixed (T* dataPointer = &data[0]) { + Box* box = null; + Context.DxDeviceContext.UpdateSubresource( + pDstResource: bufferPointer, + DstSubresource: 0u, + pDstBox: box, + pSrcData: dataPointer, + SrcRowPitch: 0u, + SrcDepthPitch: 0u); + } + } +#pragma warning restore CS8500 + + public unsafe void CleanUp() { + if (Buffer.Handle is null) { + return; + } + + Buffer.Dispose(); + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs b/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs index 97d9976d3..1d29bd275 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs @@ -73,7 +73,7 @@ private unsafe void UploadBuffer(ReadOnlySpan data, ref PSysMem = bindingData, }; - ID3D11Buffer* bufferHandle = default; + ID3D11Buffer* bufferHandle = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreateBuffer(ref bufferDescription, ref subresourceData, ref bufferHandle)); buffer = bufferHandle; } diff --git a/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs b/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs index bd8c67e10..6c9317ebb 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs @@ -4,34 +4,179 @@ using System.Numerics; using Silk.NET.DXGI; using System.Runtime.CompilerServices; +using System.Collections.Concurrent; +using Maple2.Server.DebugGame.Graphics.Assets; +using System.Runtime.InteropServices; namespace Maple2.Server.DebugGame.Graphics.Resources; +internal class ShaderCompilationJob { + public string ShaderPath = string.Empty; + public byte[] IncludeSource = []; + public GCHandle IncludeSourceHandle; + public List IncludedFiles = []; +} + public abstract class Shader { - public static string ShaderRootPath = ""; + // HRESULT values, mandatory for some DX11 features + public const int S_OK = 0; + public const int E_FAIL = unchecked((int) 0x80004005); protected ComPtr Bytecode; protected ComPtr CompileErrors; public bool Loaded { get; private set; } + private static unsafe ConcurrentDictionary _compilingShaders = new(); + public string FilePath { get; private set; } = string.Empty; + public IReadOnlyList IncludedFiles => includedFiles; + private List includedFiles = []; + + public Shader() { } + + static unsafe int IncludeOpen(ID3DInclude* include, D3DIncludeType includeType, byte* pFileName, void* pParentData, void** ppData, uint* pBytes) { + string filePath = string.Empty; + string shaderPath = string.Empty; + string fileName = new((sbyte*) pFileName); + ShaderCompilationJob? job = null; + + string message = includeType switch { + D3DIncludeType.D3DIncludeLocal => $"Shader included path \"{fileName}\"", + D3DIncludeType.D3DIncludeSystem => $"Shader included path <{fileName}>", + _ => $"Shader included path '{fileName}'", + }; + + Log.Information(message); + + try { + if (!_compilingShaders.TryGetValue((IntPtr) include, out ShaderCompilationJob? value)) { + Log.Error("[{ShaderPath}]: Failed to find shader path for compiling shader", shaderPath); + + return E_FAIL; + } + + job = value; + shaderPath = Path.GetRelativePath(ShaderPipelines.AssetRootPath, job.ShaderPath); + + if (job.IncludeSource.Length > 0) { + Log.Error("[{ShaderPath}]: Old shader compilation job lingering, failed to clean up previous job", shaderPath); + + return E_FAIL; + } + + switch (includeType) { + case D3DIncludeType.D3DIncludeLocal: + filePath = job.ShaderPath; + + DirectoryInfo? shaderParent = Directory.GetParent(filePath); + + if (shaderParent is null) { + Log.Error("[{ShaderPath}]: Failed to find shader parent directory path for compiling shader", shaderPath); + + return E_FAIL; + } + + filePath = Path.Combine(shaderParent.FullName, fileName); + + break; + case D3DIncludeType.D3DIncludeSystem: + string shaderRoot = Path.Combine(ShaderPipelines.AssetRootPath, "Shaders"); + filePath = Path.Combine(shaderRoot, fileName); + + break; + default: + throw new InvalidDataException($"Unexpected include type: {includeType}"); + } + + if (!File.Exists(filePath)) { + Log.Error("[{ShaderPath}]: Failed to find included file path '{S}'", shaderPath, filePath); + + return E_FAIL; + } + + FileStream file = File.Open(filePath, FileMode.Open, FileAccess.Read); + + if (file.Length > 0) { + job.IncludeSource = new byte[file.Length]; + + file.Read(job.IncludeSource); + + job.IncludeSourceHandle = GCHandle.Alloc(job.IncludeSource, GCHandleType.Pinned); + + *ppData = job.IncludeSourceHandle.AddrOfPinnedObject().ToPointer(); + *pBytes = (uint) file.Length; + } else { + *ppData = null; + *pBytes = 0; + } - public Shader() { + job.IncludedFiles.Add(filePath); + + file.Close(); + + return S_OK; + } catch (Exception exception) { + if (job is not null && job.IncludeSource.Length > 0) { + job.IncludeSourceHandle.Free(); + job.IncludeSource = []; + } + + Log.Error("[{ShaderPath}]: {ExceptionMessage}", shaderPath, exception.Message); + + return E_FAIL; + } + } + + static unsafe int IncludeClose(ID3DInclude* include, void* pData) { + if (!_compilingShaders.TryGetValue((IntPtr) include, out ShaderCompilationJob? job)) { + Log.Error($"Failed to find shader path for compiling shader"); + + return E_FAIL; + } + + if (job.IncludeSource.Length > 0) { + job.IncludeSourceHandle.Free(); + job.IncludeSource = []; + } + + return S_OK; } protected unsafe void LoadBytecode(DebugGraphicsContext context, string path, string entry, string target) { - path = Path.GetFullPath(Path.Combine(ShaderRootPath, path)); + FilePath = path; string source = File.ReadAllText(path); ID3DInclude* defaultInclude = (ID3DInclude*) 1; // enable simple include behavior - ID3D10Blob* bytecode = default; - ID3D10Blob* compileErrors = default; + ID3D10Blob* bytecode = null; + ID3D10Blob* compileErrors = null; + + #region pInclude + // Ugly hack: C# doesn't support struct inheritance, so need to implement a vtable by hand to pass to pInclude + var lpVtable = stackalloc void*[2]; + + delegate* open = &IncludeOpen; + delegate* close = &IncludeClose; + + lpVtable[0] = open; + lpVtable[1] = close; + + var include = new ID3DInclude(lpVtable); + #endregion + + ID3DInclude* pInclude = &include; + ShaderCompilationJob job = new() { + ShaderPath = path, + }; + + if (!_compilingShaders.TryAdd((IntPtr) pInclude, job)) { + Log.Error("[{Path}]: Failed to add shader path to ID3DInclude compilation job dictionary", path); + } HResult result = context.Compiler!.Compile( pSrcData: Marshal(source), SrcDataSize: (nuint) source.Length, pSourceName: Marshal(path), pDefines: null, - pInclude: defaultInclude, + pInclude: ref include, pEntrypoint: Marshal(entry), pTarget: Marshal(target), Flags1: 0, @@ -39,6 +184,18 @@ protected unsafe void LoadBytecode(DebugGraphicsContext context, string path, st ppCode: ref bytecode, ppErrorMsgs: ref compileErrors); + + if (job.IncludeSource.Length > 0) { + job.IncludeSourceHandle.Free(); + job.IncludeSource = []; + } + + if (!_compilingShaders.TryRemove((IntPtr) pInclude, out _)) { + Log.Error("[{Path}]: Failed to remove shader path from ID3DInclude compilation job dictionary", path); + } + + includedFiles = job.IncludedFiles; + Bytecode = bytecode; CompileErrors = compileErrors; @@ -77,7 +234,7 @@ public void Load(string path, string entry) { unsafe { LoadBytecode(Context, path, entry, "ps_5_0"); - ID3D11PixelShader* shader = default; + ID3D11PixelShader* shader = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreatePixelShader( pShaderBytecode: Bytecode.GetBufferPointer(), @@ -101,7 +258,7 @@ public void CleanUp() { public unsafe void Bind() { ID3D11ClassInstance* classInstances = null; - Context.DxDeviceContext.PSSetShader(Shader, ref classInstances, 0); + Context.DxDeviceContext.PSSetShader(Shader, in classInstances, 0); } } @@ -122,7 +279,7 @@ public void Load(string path, string entry) { LoadBytecode(Context, path, entry, "vs_5_0"); unsafe { - ID3D11VertexShader* shader = default; + ID3D11VertexShader* shader = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreateVertexShader( pShaderBytecode: Bytecode.GetBufferPointer(), BytecodeLength: Bytecode.GetBufferSize(), @@ -200,7 +357,7 @@ private unsafe void CreateInputLayout() { } fixed (InputElementDesc* inputElements = vertexAttributes) { - ID3D11InputLayout* layout = default; + ID3D11InputLayout* layout = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreateInputLayout( pInputElementDescs: inputElements, NumElements: 9, @@ -227,7 +384,7 @@ public void CleanUp() { public unsafe void Bind() { ID3D11ClassInstance* classInstances = null; - Context.DxDeviceContext.VSSetShader(Shader, ref classInstances, 0); + Context.DxDeviceContext.VSSetShader(Shader, in classInstances, 0); Context.DxDeviceContext.IASetInputLayout(InputLayout); } } diff --git a/Maple2.Server.DebugGame/Graphics/Resources/ShaderPipeline.cs b/Maple2.Server.DebugGame/Graphics/Resources/ShaderPipeline.cs new file mode 100644 index 000000000..7d20af932 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Resources/ShaderPipeline.cs @@ -0,0 +1,22 @@ +namespace Maple2.Server.DebugGame.Graphics.Resources; + +public class RenderPass { + public string TargetName = string.Empty; + + public VertexShader? VertexShader = null; + public PixelShader? PixelShader = null; + + public void CleanUp() { + VertexShader?.CleanUp(); + PixelShader?.CleanUp(); + } +} + +public class ShaderPipeline { + public string Name = string.Empty; + public RenderPass? RenderPass = null; + + public void CleanUp() { + RenderPass?.CleanUp(); + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs b/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs index 56e6c999a..d5561f28a 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs @@ -1,4 +1,5 @@ -using Silk.NET.Core.Native; +using System.Buffers; +using Silk.NET.Core.Native; using Silk.NET.Direct3D11; using Silk.NET.DXGI; using SixLabors.ImageSharp; @@ -39,20 +40,20 @@ public unsafe void Load(string path) { BindFlags = (uint) BindFlag.ShaderResource, Usage = Usage.Default, CPUAccessFlags = 0, - MiscFlags = (uint) ResourceMiscFlag.None, + MiscFlags = 0, SampleDesc = new SampleDesc(1, 0), ArraySize = 1, }; - if (image.DangerousTryGetSinglePixelMemory(out var imageData)) { - using (var pixelData = imageData.Pin()) { + if (image.DangerousTryGetSinglePixelMemory(out Memory imageData)) { + using (MemoryHandle pixelData = imageData.Pin()) { SubresourceData subresourceData = new SubresourceData { PSysMem = pixelData.Pointer, SysMemPitch = (uint) image.Width * sizeof(int), SysMemSlicePitch = (uint) (image.Width * sizeof(int) * image.Height), }; - ID3D11Texture2D* texture = default; + ID3D11Texture2D* texture = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreateTexture2D( pDesc: in textureDesc, pInitialData: in subresourceData, @@ -74,7 +75,7 @@ public unsafe void Load(string path) { }, }; - ID3D11ShaderResourceView* resourceView = default; + ID3D11ShaderResourceView* resourceView = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreateShaderResourceView( pResource: Texture2D, pDesc: ref resourceViewDesc, @@ -97,7 +98,7 @@ public unsafe void Load(string path) { samplerDesc.BorderColor[2] = 0.0f; samplerDesc.BorderColor[3] = 1.0f; - ID3D11SamplerState* samplerState = default; + ID3D11SamplerState* samplerState = null; SilkMarshal.ThrowHResult(Context.DxDevice.CreateSamplerState( pSamplerDesc: in samplerDesc, ppSamplerState: ref samplerState)); diff --git a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs index ffd0d4f44..429e59c12 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs @@ -5,19 +5,54 @@ namespace Maple2.Server.DebugGame.Graphics.Scene; public class Camera { public Transform Transform { get; init; } = new Transform(); - public Matrix4x4 ProjectionMatrix { get; private set; } public float AspectRatio { get; private set; } public float NearPlane { get; private set; } public float FarPlane { get; private set; } public float FieldOfView { get; private set; } - public void SetProperties(float fieldOfView, float aspectRatio, float nearPlane, float farPlane) { + /// + /// Gets the current view matrix based on camera transform + /// + public Matrix4x4 ViewMatrix { + get { + Vector3 position = Transform.Position; + Vector3 forward = Transform.FrontAxis; + Vector3 up = Transform.UpAxis; + return Matrix4x4.CreateLookAt(position, position + forward, up); + } + } + /// + /// Gets the current projection matrix based on camera properties + /// + public Matrix4x4 ProjectionMatrix { + get { + return Matrix4x4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane); + } } - public void SetProperties(float width, float height, float projectionPlane, float nearPlane, float farPlane) { + /// + /// Updates the projection matrix based on window size + /// + public void UpdateProjectionMatrix(int windowWidth, int windowHeight) { + float aspectRatio = (float) windowWidth / windowHeight; + AspectRatio = aspectRatio; + FieldOfView = MathF.PI / 4.0f; + NearPlane = 1.0f; + FarPlane = 50000.0f; + } + public void SetProperties(float fieldOfView, float aspectRatio, float nearPlane, float farPlane) { + FieldOfView = fieldOfView; + AspectRatio = aspectRatio; + NearPlane = nearPlane; + FarPlane = farPlane; + } + + public void SetProperties(float width, float height, float projectionPlane, float nearPlane, float farPlane) { + float aspectRatio = width / height; + SetProperties(projectionPlane, aspectRatio, nearPlane, farPlane); } } diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs new file mode 100644 index 000000000..c828d3751 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs @@ -0,0 +1,76 @@ +using System.Numerics; +using Serilog; + +namespace Maple2.Server.DebugGame.Graphics.Scene; + +public class FollowCameraController : ICameraController { + private static readonly ILogger Logger = Log.Logger.ForContext(); + + public Camera Camera { get; } + public InputState InputState { get; } = new(); + + // Camera follow system + public bool IsFollowingPlayer { get; private set; } + public long? FollowedPlayerId { get; private set; } + + // Default camera follow offset + private static readonly Vector3 CameraFollowOffset = new Vector3(-800, -1000, 1200); + + public Vector3 CameraTarget { get; private set; } = Vector3.Zero; + + public FollowCameraController(Camera camera) { + Camera = camera; + } + + /// + /// Starts following a specific player + /// + public void StartFollowingPlayer(long playerId, Vector3 playerPosition) { + IsFollowingPlayer = true; + FollowedPlayerId = playerId; + + // Set initial camera position and look at player only once + Camera.Transform.Position = playerPosition + CameraFollowOffset; + SetCameraTarget(playerPosition); + + Logger.Information("Started following player {PlayerId}", playerId); + } + + /// + /// Stops following the current player + /// + public void StopFollowingPlayer() { + IsFollowingPlayer = false; + FollowedPlayerId = null; + Logger.Information("Stopped following player"); + } + + /// + /// Updates camera to follow a player at the specified position + /// + public void UpdatePlayerFollow(Vector3 playerPosition) { + if (!IsFollowingPlayer) return; + + // Only update camera position, keep rotation fixed from initial setup + Camera.Transform.Position = playerPosition + CameraFollowOffset; + } + + /// + /// Sets the camera target and updates camera orientation + /// + public void SetCameraTarget(Vector3 target) { + CameraTarget = target; + Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); + + // Use Transform's LookTo method instead of manual matrix calculation + Camera.Transform.LookTo(direction, Vector3.UnitZ, snapToGroundPlane: false); + } + + /// + /// Updates the controller - follow controllers don't need delta-based updates + /// + public void Update(float delta) { + // Follow controller doesn't need frame-based updates + // Following is updated via UpdatePlayerFollow when player position changes + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs new file mode 100644 index 000000000..baef90090 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -0,0 +1,189 @@ +using Silk.NET.Input; +using System.Numerics; +using Serilog; + +namespace Maple2.Server.DebugGame.Graphics.Scene; + +public class FreeCameraController : ICameraController { + private static readonly ILogger Logger = Log.Logger.ForContext(); + + const float MAX_PITCH = (float.Pi / 180) * 89; + + public InputState InputState { get; } = new(); + public Camera Camera { get; } + + public Vector3 CameraTarget { get; private set; } = Vector3.Zero; + + private const Key MoveForward = Key.W; + private const Key MoveBackward = Key.S; + private const Key MoveLeft = Key.A; + private const Key MoveRight = Key.D; + private const Key MoveUp = Key.E; + private const Key MoveDown = Key.Q; + + public readonly float FlySpeed = 500; + public readonly float BaseRotationSpeed = (1 / 1000.0f) * float.Pi; // 500 pixels for 1/4 of a rotation + public float RotationSpeed = 1; + public float RotationSpeedDegrees { + get => (180.0f / float.Pi) * RotationSpeed; + set => RotationSpeed = (float.Pi / 180.0f) * value; + } + + // Default camera rotation for MapleStory 2 coordinate system + private static readonly Matrix4x4 DefaultCameraRotation = new Matrix4x4( + -0.5347f, 0.8451f, 0.0000f, 0.0000f, + -0.6053f, -0.3830f, 0.6978f, 0.0000f, + 0.5897f, 0.3731f, 0.7163f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 1.0000f + ); + + // Field overview camera rotation + private static readonly Matrix4x4 FieldOverviewRotation = new Matrix4x4( + -1.0000f, 0.0018f, 0.0000f, 0.0000f, + -0.0013f, -0.6994f, 0.7147f, 0.0000f, + 0.0013f, 0.7147f, 0.6994f, 0.0000f, + 0.0000f, 0.0000f, 0.0000f, 1.0000f + ); + + public FreeCameraController(Camera camera) { + Camera = camera; + } + + /// + /// Sets the camera to the default rotation for MapleStory 2 + /// + public void SetDefaultRotation() { + Camera.Transform.RotationMatrix = DefaultCameraRotation; + } + + /// + /// Sets the camera to the field overview rotation + /// + public void SetFieldOverviewRotation() { + Camera.Transform.RotationMatrix = FieldOverviewRotation; + } + + public void Update(float delta) { + if (!InputState.InputFocused) { + return; + } + + Vector3 moveDirection = new(); + + if (InputState.GetState(MoveForward).IsDown) { + moveDirection.Y += 1; + } + + if (InputState.GetState(MoveBackward).IsDown) { + moveDirection.Y -= 1; + } + + if (InputState.GetState(MoveRight).IsDown) { + moveDirection.X += 1; + } + + if (InputState.GetState(MoveLeft).IsDown) { + moveDirection.X -= 1; + } + + if (InputState.GetState(MoveUp).IsDown) { + moveDirection.Z += 1; + } + + if (InputState.GetState(MoveDown).IsDown) { + moveDirection.Z -= 1; + } + + // Apply speed modifier with Shift key + float currentSpeed = FlySpeed; + if (InputState.GetState(Key.ShiftLeft).IsDown || InputState.GetState(Key.ShiftRight).IsDown) { + currentSpeed *= 5.0f; // 5x speed with Shift + } + + Camera.Transform.Position += delta * currentSpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); + + if (!InputState.MouseRight.IsDown) { + return; + } + + Vector3 mouseDelta = InputState.MousePosition.Delta; + + float currentPitch = -float.Asin(Camera.Transform.FrontAxis.Z); + float currentYaw = -float.Atan2(-Camera.Transform.FrontAxis.X, -Camera.Transform.FrontAxis.Y); + + currentPitch = float.Max(-MAX_PITCH, float.Min(MAX_PITCH, currentPitch + RotationSpeed * BaseRotationSpeed * mouseDelta.Y)); + currentYaw -= RotationSpeed * BaseRotationSpeed * mouseDelta.X; + + Matrix4x4 rotation = Matrix4x4.Multiply(Matrix4x4.CreateRotationX(currentPitch), Matrix4x4.CreateRotationZ(currentYaw)); + Vector3 position = Camera.Transform.Position; + + Camera.Transform.Transformation = rotation; + Camera.Transform.Position = position; + + InputState.MousePosition.Lock = true; + } + + /// + /// Rotates the camera around the target point + /// + public void RotateCamera(float yawDelta, float pitchDelta) { + // Rotate camera around target + Vector3 direction = Camera.Transform.Position - CameraTarget; + float distance = direction.Length(); + + if (distance < 0.01f) return; // Avoid division by zero + + // Convert to spherical coordinates + float currentYaw = MathF.Atan2(direction.Z, direction.X); + float currentPitch = MathF.Asin(Math.Clamp(direction.Y / distance, -1.0f, 1.0f)); + + // Apply rotation + currentYaw += yawDelta; + currentPitch = Math.Clamp(currentPitch + pitchDelta, -MathF.PI * 0.4f, MathF.PI * 0.4f); + + // Convert back to cartesian + float x = distance * MathF.Cos(currentPitch) * MathF.Cos(currentYaw); + float y = distance * MathF.Sin(currentPitch); + float z = distance * MathF.Cos(currentPitch) * MathF.Sin(currentYaw); + + Camera.Transform.Position = CameraTarget + new Vector3(x, y, z); + } + + /// + /// Moves the camera relative to its current position + /// + public void MoveCameraRelative(Vector3 offset) { + Camera.Transform.Position += offset; + CameraTarget += offset; + } + + /// + /// Sets the camera target + /// + public void SetCameraTarget(Vector3 target) { + CameraTarget = target; + Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); + + // Use Transform's LookTo method instead of manual matrix calculation + Camera.Transform.LookTo(direction, Vector3.UnitZ, snapToGroundPlane: false); + } + + /// + /// Sets camera orientation that works well with MapleStory 2's coordinate system + /// + public void SetCameraOrientationForMapData() { + // Set camera orientation that works well with the transformed map coordinate system + Vector3 offset = new Vector3(0, -5, 5); // Look up from below and in front (inverted from typical) + Camera.Transform.Position = CameraTarget + offset; + } + + /// + /// Flips the camera's up vector to fix upside-down view + /// + public void FlipCameraUpVector() { + // Flip the up axis in the transform + Matrix4x4 currentTransform = Camera.Transform.Transformation; + Matrix4x4 flipMatrix = Matrix4x4.CreateScale(1, 1, -1); + Camera.Transform.Transformation = currentTransform * flipMatrix; + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs new file mode 100644 index 000000000..1a65f21d8 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs @@ -0,0 +1,19 @@ +namespace Maple2.Server.DebugGame.Graphics.Scene; + +public interface ICameraController { + /// + /// The camera being controlled + /// + Camera Camera { get; } + + /// + /// Input state for handling user input + /// + InputState InputState { get; } + + /// + /// Updates the controller with the given delta time + /// + /// Time elapsed since last update + void Update(float delta); +} diff --git a/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs b/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs new file mode 100644 index 000000000..56dc1e991 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs @@ -0,0 +1,120 @@ +using Silk.NET.Core.Native; +using Silk.NET.Direct3D11; +using Maple2.Server.DebugGame.Graphics.Resources; +using Maple2.Server.DebugGame.Graphics.Enum; + +namespace Maple2.Server.DebugGame.Graphics.Scene; + +public class SceneState { + public DebugGraphicsContext Context { get; init; } + private readonly List> samplerStates = []; + private readonly List> resourceViews = []; + private readonly List> vsConstantBuffers = []; + private readonly List> psConstantBuffers = []; + private bool samplerStatesChanged; + private bool resourceViewsChanged; + private bool vsConstantBuffersChanged; + private bool psConstantBuffersChanged; + + public SceneState(DebugGraphicsContext context) { + Context = context; + } + + private static void Grow(List list, int size) { + for (int i = list.Count; i < size; ++i) { + list.Add(default); + } + } + + private static void AddSlot(List list, int slot, T value) { + Grow(list, slot + 1); + + list[slot] = value; + } + + public void BindTexture(Texture? texture, int resourceSlot = 0, int samplerSlot = -1) { + if (samplerSlot == -1) { + samplerSlot = resourceSlot; + } + + AddSlot(samplerStates, samplerSlot, texture?.SamplerState ?? null); + AddSlot(resourceViews, resourceSlot, texture?.ResourceView ?? null); + + samplerStatesChanged = true; + resourceViewsChanged = true; + } + + public void BindConstantBuffer(ConstantBuffer buffer, int slot, ShaderStageFlags stages) { + if ((stages & ShaderStageFlags.Vertex) != ShaderStageFlags.None) { + AddSlot(vsConstantBuffers, slot, buffer.Buffer); + + vsConstantBuffersChanged = true; + } + + if ((stages & ShaderStageFlags.Pixel) != ShaderStageFlags.None) { + AddSlot(psConstantBuffers, slot, buffer.Buffer); + + psConstantBuffersChanged = true; + } + } + + private static unsafe T*[] GetPointerArray(List> list) where T : unmanaged, IComVtbl { + T*[] array = new T*[list.Count]; + + for (int i = 0; i < list.Count; i++) { + array[i] = list[i].GetPinnableReference(); + } + + return array; + } + + public unsafe void UpdateBindings() { + if (samplerStatesChanged) { + samplerStatesChanged = false; + + ID3D11SamplerState*[] samplerStateArray = GetPointerArray(samplerStates); + + if (samplerStates.Count > 0) { + Context.DxDeviceContext.PSSetSamplers(0, (uint) samplerStates.Count, ref samplerStateArray[0]); + } else { + Context.DxDeviceContext.PSSetSamplers(0, 0, null); + } + } + + if (resourceViewsChanged) { + resourceViewsChanged = false; + + ID3D11ShaderResourceView*[] resourceViewArray = GetPointerArray(resourceViews); + + if (resourceViews.Count > 0) { + Context.DxDeviceContext.PSSetShaderResources(0, (uint) resourceViews.Count, ref resourceViewArray[0]); + } else { + Context.DxDeviceContext.PSSetShaderResources(0, 0, null); + } + } + + if (vsConstantBuffersChanged) { + vsConstantBuffersChanged = false; + + ID3D11Buffer*[] vsConstBufferArray = GetPointerArray(vsConstantBuffers); + + if (vsConstantBuffers.Count > 0) { + Context.DxDeviceContext.VSSetConstantBuffers(0, (uint) vsConstantBuffers.Count, ref vsConstBufferArray[0]); + } else { + Context.DxDeviceContext.VSSetConstantBuffers(0, 0, null); + } + } + + if (psConstantBuffersChanged) { + psConstantBuffersChanged = false; + + ID3D11Buffer*[] psConstBufferArray = GetPointerArray(psConstantBuffers); + + if (psConstantBuffers.Count > 0) { + Context.DxDeviceContext.PSSetConstantBuffers(0, (uint) psConstantBuffers.Count, ref psConstBufferArray[0]); + } else { + Context.DxDeviceContext.PSSetConstantBuffers(0, 0, null); + } + } + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs index 0607f6f63..5ec27373c 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs @@ -100,6 +100,7 @@ public void Render() { ImGui.EndTable(); } + ImGuiController.ClampWindowToViewport(); ImGui.End(); } } diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/ActorDetailsWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/ActorDetailsWindow.cs new file mode 100644 index 000000000..3f94ed7a9 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/ActorDetailsWindow.cs @@ -0,0 +1,113 @@ +using ImGuiNET; +using Maple2.Server.Game.Model; +using Maple2.Model.Enum; +using Maple2.Model.Game; +using Maple2.Server.Game.Manager; + +namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; + +public class ActorDetailsWindow : IUiWindow { + public bool AllowMainWindow => false; + public bool AllowFieldWindow => true; + public bool Enabled { get; set; } = true; + public bool HideFromMenuBar => true; // Hide from main menu bar, only show when an actor is selected + public string TypeName => "Actor Details"; + public DebugGraphicsContext? Context { get; set; } + public ImGuiController? ImGuiController { get; set; } + public DebugFieldWindow? FieldWindow { get; set; } + + public void Initialize(DebugGraphicsContext context, ImGuiController controller, DebugFieldWindow? fieldWindow) { + Context = context; + ImGuiController = controller; + FieldWindow = fieldWindow; + } + + public void Render() { + DebugFieldRenderer? activeRenderer = FieldWindow?.ActiveRenderer; + if (activeRenderer?.SelectedActor == null) { + // Hide window when no actor selected + return; + } + + string baseTitle = $"{activeRenderer.GetActorType(activeRenderer.SelectedActor)} Details: {activeRenderer.GetActorName(activeRenderer.SelectedActor)}###Actor Details"; // stable ID after ### for docking + + if (ImGui.Begin(baseTitle)) { + IActor actor = activeRenderer.SelectedActor!; + ImGui.Text($"Type: {activeRenderer.GetActorType(actor)}"); + ImGui.Text($"Name: {activeRenderer.GetActorName(actor)}"); + ImGui.Text($"Object ID: {actor.ObjectId}"); + ImGui.Text($"Is Dead: {actor.IsDead}"); + switch (actor) { + case FieldPlayer player: + ImGui.Text($"State: {player.State}"); + ImGui.Text($"Sub State: {player.SubState}"); + Character character = player.Value.Character; + ImGui.Text($"Level: {character.Level}"); + ImGui.Text($"Job: {character.Job}"); + ImGui.Text($"Gender: {character.Gender}"); + ImGui.Text($"Account ID: {player.Value.Account.Id}"); + ImGui.Text($"Character ID: {character.Id}"); + break; + case FieldNpc npc: + ImGui.Text($"State: {npc.State.State}"); + ImGui.Text($"Sub State: {npc.State.SubState}"); + ImGui.Text($"Level: {npc.Value.Metadata.Basic.Level}"); + ImGui.Text($"NPC ID: {npc.Value.Metadata.Id}"); + ImGui.Text($"Model name: {npc.Value.Metadata.Model.Name}"); + ImGui.Text($"Animation Speed: {npc.Value.Metadata.Model.AniSpeed}"); + ImGui.Text($"Is Boss: {npc.Value.IsBoss}"); + break; + } + ImGui.Separator(); + ImGui.Text("Position & Movement:"); + ImGui.Indent(); + ImGui.Text($"Position: {actor.Position}"); + ImGui.Text($"Rotation: {actor.Rotation}"); + if (actor is FieldNpc npc2) ImGui.Text($"Velocity: {npc2.MovementState.Velocity}"); + ImGui.Text($"Playing Sequence: {actor.Animation.PlayingSequence?.Name ?? "None"}"); + if (actor is FieldPlayer pl2) { + ImGui.Text($"Last Ground Position: {pl2.LastGroundPosition}"); + ImGui.Text($"In Battle: {pl2.InBattle}"); + } + ImGui.Unindent(); + ImGui.Separator(); + ImGui.Text("Stats:"); + ImGui.Indent(); + StatsManager stats = actor.Stats; + ImGui.Text($"Health: {stats.Values[BasicAttribute.Health].Current}/{stats.Values[BasicAttribute.Health].Total}"); + if (actor is FieldPlayer) { + ImGui.Text($"Spirit: {stats.Values[BasicAttribute.Spirit].Current}/{stats.Values[BasicAttribute.Spirit].Total}"); + ImGui.Text($"Stamina: {stats.Values[BasicAttribute.Stamina].Current}/{stats.Values[BasicAttribute.Stamina].Total}"); + } + ImGui.Unindent(); + ImGui.Separator(); + ImGui.Text("Additional Information"); + switch (actor) { + case FieldPlayer player: + ImGui.Text($"Admin Permissions: {player.AdminPermissions}"); + ImGui.Separator(); + ImGui.Text("Movement Controls:"); + bool moveMode = activeRenderer.PlayerMoveMode; + if (ImGui.Checkbox("Move Player Mode", ref moveMode)) activeRenderer.PlayerMoveMode = moveMode; + if (activeRenderer.PlayerMoveMode) { + ImGui.Indent(); + bool force = activeRenderer.ForceMove; + if (ImGui.Checkbox("Force Move (bypass validation)", ref force)) activeRenderer.ForceMove = force; + ImGui.Text("Click on the map to move this player"); + ImGui.Unindent(); + } + break; + case FieldNpc npc: + ImGui.Text($"NPC Type: {(npc.Value.Metadata.Basic.Kind == 0 ? "Friendly" : "Hostile")}"); + if (npc.Owner != null) ImGui.Text($"Spawn Point ID: {npc.SpawnPointId}"); + break; + default: + ImGui.Text("No additional information available."); + break; + } + + } + ImGuiController.ClampWindowToViewport(); + ImGui.End(); + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldInfoWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldInfoWindow.cs new file mode 100644 index 000000000..141cc65dc --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldInfoWindow.cs @@ -0,0 +1,158 @@ +using ImGuiNET; +using Maple2.Server.Game.Model; +using Maple2.Server.Game.Manager.Field; + +namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; + +public class FieldInfoWindow : IUiWindow { + public bool AllowMainWindow => false; + public bool AllowFieldWindow => true; + public bool Enabled { get; set; } = true; + public string TypeName => "Field Information"; + public DebugGraphicsContext? Context { get; set; } + public ImGuiController? ImGuiController { get; set; } + public DebugFieldWindow? FieldWindow { get; set; } + + public void Initialize(DebugGraphicsContext context, ImGuiController controller, DebugFieldWindow? fieldWindow) { + Context = context; + ImGuiController = controller; + FieldWindow = fieldWindow; + } + + public void Render() { + if (FieldWindow?.ActiveRenderer == null) return; + DebugFieldRenderer? r = FieldWindow.ActiveRenderer; + FieldManager field = r.Field; + ImGui.Begin(TypeName); + // Field Info Section + + ImGui.Text($"Map ID: {field.MapId}"); + ImGui.Text($"Room ID: {field.RoomId}"); + ImGui.Text($"Map Name: {field.Metadata.Name}"); + ImGui.Text($"Field Type: {field.FieldType}"); + ImGui.Text($"Field Instance Type: {field.FieldInstance.Type}"); + if (field.RoomTimer != null) ImGui.Text($"Room Timer Active: {field.RoomTimer.Duration}"); + if (field.DungeonId > 0) ImGui.Text($"Dungeon ID: {field.DungeonId}"); + + if (field.AccelerationStructure != null) { + ImGui.Separator(); + ImGui.Text("Field Entity Information:"); + ImGui.Text($"Grid Size: {field.AccelerationStructure.GridSize}"); + ImGui.Text($"Min Index: {field.AccelerationStructure.MinIndex}"); + ImGui.Text($"Max Index: {field.AccelerationStructure.MaxIndex}"); + ImGui.Separator(); + ImGui.Text("Entity Collection Sizes:"); + ImGui.Text($"Aligned: {field.AccelerationStructure.AlignedEntities.Length}"); + ImGui.Text($"Aligned Trimmed: {field.AccelerationStructure.AlignedTrimmedEntities.Length}"); + ImGui.Text($"Unaligned: {field.AccelerationStructure.UnalignedEntities.Length}"); + ImGui.Text($"Vibrate: {field.AccelerationStructure.VibrateEntities.Length}"); + } + + ImGui.Separator(); + ImGui.Text("Entity Counts:"); + ImGui.Indent(); + ImGui.Text($"Players: {field.Players.Count}"); + ImGui.Text($"NPCs: {field.Npcs.Count}"); + ImGui.Text($"Mobs: {field.Mobs.Count}"); + ImGui.Text($"Pets: {field.Pets.Count}"); + ImGui.Unindent(); + // Actor List Section (inside Field Info window) + + int totalActors = field.Players.Count + field.Npcs.Count + field.Mobs.Count; + if (totalActors == 0) { + ImGui.Text("No active actors"); + } else { + bool clearSelectionDisabled = r.SelectedActor == null; + if (clearSelectionDisabled) ImGui.BeginDisabled(); + if (ImGui.Button("Clear Selection") && !clearSelectionDisabled) { + r.SelectedActor = null; + } + if (clearSelectionDisabled) ImGui.EndDisabled(); + if (ImGui.CollapsingHeader($"Players ({field.Players.Count})", ImGuiTreeNodeFlags.None)) { + if (ImGui.BeginTable("Players Table", 4)) { + ImGui.TableNextRow(ImGuiTableRowFlags.Headers); + ImGui.TableSetColumnIndex(0); + ImGui.Text("Name"); + ImGui.TableSetColumnIndex(1); + ImGui.Text("Level"); + ImGui.TableSetColumnIndex(2); + ImGui.Text("Object ID"); + ImGui.TableSetColumnIndex(3); + ImGui.Text("Status"); + int idx = 0; + foreach ((int objectId, FieldPlayer player) in field.Players) { + ImGui.TableNextRow(); + bool selected = player == r.SelectedActor; + bool nextSel = false; + ImGui.TableSetColumnIndex(0); + nextSel |= ImGui.Selectable($"{player.Value.Character.Name}##P{idx}0", selected); + ImGui.TableSetColumnIndex(1); + nextSel |= ImGui.Selectable($"{player.Value.Character.Level}##P{idx}1", selected); + ImGui.TableSetColumnIndex(2); + nextSel |= ImGui.Selectable($"{objectId}##P{idx}2", selected); + ImGui.TableSetColumnIndex(3); + nextSel |= ImGui.Selectable($"{(player.IsDead ? "Dead" : "Alive")}##P{idx}3", selected); + if (nextSel) r.SelectedActor = player; + idx++; + } + ImGui.EndTable(); + } + } + int totalNpcs = field.Npcs.Count + field.Mobs.Count; + if (ImGui.CollapsingHeader($"NPCs ({totalNpcs})", ImGuiTreeNodeFlags.None)) { + if (ImGui.BeginTable("NPCs Table", 5)) { + ImGui.TableNextRow(ImGuiTableRowFlags.Headers); + ImGui.TableSetColumnIndex(0); + ImGui.Text("Type"); + ImGui.TableSetColumnIndex(1); + ImGui.Text("Name"); + ImGui.TableSetColumnIndex(2); + ImGui.Text("Level"); + ImGui.TableSetColumnIndex(3); + ImGui.Text("Object ID"); + ImGui.TableSetColumnIndex(4); + ImGui.Text("Status"); + int idx = 0; + foreach ((int objectId, FieldNpc npc) in field.Npcs) { + ImGui.TableNextRow(); + bool selected = npc == r.SelectedActor; + bool nextSel = false; + ImGui.TableSetColumnIndex(0); + nextSel |= ImGui.Selectable($"NPC##N{idx}0", selected); + ImGui.TableSetColumnIndex(1); + nextSel |= ImGui.Selectable($"{npc.Value.Metadata.Name}##N{idx}1", selected); + ImGui.TableSetColumnIndex(2); + nextSel |= ImGui.Selectable($"{npc.Value.Metadata.Basic.Level}##N{idx}2", selected); + ImGui.TableSetColumnIndex(3); + nextSel |= ImGui.Selectable($"{objectId}##N{idx}3", selected); + ImGui.TableSetColumnIndex(4); + nextSel |= ImGui.Selectable($"{(npc.IsDead ? "Dead" : "Alive")}##N{idx}4", selected); + if (nextSel) r.SelectedActor = npc; + idx++; + } + foreach ((int objectId, FieldNpc mob) in field.Mobs) { + ImGui.TableNextRow(); + bool selected = mob == r.SelectedActor; + bool nextSel = false; + ImGui.TableSetColumnIndex(0); + nextSel |= ImGui.Selectable($"Mob##M{idx}0", selected); + ImGui.TableSetColumnIndex(1); + nextSel |= ImGui.Selectable($"{mob.Value.Metadata.Name}##M{idx}1", selected); + ImGui.TableSetColumnIndex(2); + nextSel |= ImGui.Selectable($"{mob.Value.Metadata.Basic.Level}##M{idx}2", selected); + ImGui.TableSetColumnIndex(3); + nextSel |= ImGui.Selectable($"{objectId}##M{idx}3", selected); + ImGui.TableSetColumnIndex(4); + nextSel |= ImGui.Selectable($"{(mob.IsDead ? "Dead" : "Alive")}##M{idx}4", selected); + if (nextSel) r.SelectedActor = mob; + idx++; + } + ImGui.EndTable(); + } + } + } + + ImGuiController.ClampWindowToViewport(); + ImGui.End(); + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs index c81ba9c7a..5db671574 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs @@ -117,6 +117,7 @@ public void Render() { ImGui.EndTable(); } + ImGuiController.ClampWindowToViewport(); ImGui.End(); } diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs index f4813694b..1a9be904a 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs @@ -1,9 +1,10 @@ namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; public interface IUiWindow { - public bool AllowMainWindow { get => false; } - public bool AllowFieldWindow { get => false; } + public bool AllowMainWindow => false; + public bool AllowFieldWindow => false; public bool Enabled { get; set; } + public bool HideFromMenuBar => false; public string TypeName { get; } public DebugGraphicsContext? Context { get; protected set; } public ImGuiController? ImGuiController { get; protected set; } diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs new file mode 100644 index 000000000..c12f92c21 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs @@ -0,0 +1,83 @@ +using ImGuiNET; +using Maple2.Server.Game.Model; +using System.Numerics; +using Maple2.Server.DebugGame.Graphics.Scene; + +namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; + +public class VisualizationControlsWindow : IUiWindow { + public bool AllowMainWindow => false; + public bool AllowFieldWindow => true; + public bool Enabled { get; set; } = true; + public string TypeName => "Visualization"; + public DebugGraphicsContext? Context { get; set; } + public ImGuiController? ImGuiController { get; set; } + public DebugFieldWindow? FieldWindow { get; set; } + + public void Initialize(DebugGraphicsContext context, ImGuiController controller, DebugFieldWindow? fieldWindow) { + Context = context; + ImGuiController = controller; + FieldWindow = fieldWindow; + } + + public void Render() { + if (FieldWindow?.ActiveRenderer == null) return; + DebugFieldRenderer renderer = FieldWindow.ActiveRenderer; + ImGui.Begin("3D Visualization Controls"); + ImGui.Text("Field Visualization Settings"); + ImGui.Separator(); + ImGui.Checkbox("Show Box Colliders", ref renderer.ShowBoxColliders); + ImGui.Checkbox("Show Vibrate Objects", ref renderer.ShowVibrateObjects); + ImGui.Checkbox("Show Portals", ref renderer.ShowPortals); + if (renderer.ShowPortals) { + ImGui.Indent(); + ImGui.Checkbox("Show Portal Information", ref renderer.ShowPortalInformation); + ImGui.Checkbox("Show Portal Connections", ref renderer.ShowPortalConnections); + ImGui.Unindent(); + } + ImGui.Checkbox("Show Sellable Tiles (Plots)", ref renderer.ShowSellableTiles); + if (renderer.ShowSellableTiles) { + ImGui.Indent(); + ImGui.Checkbox("Show Plot Labels", ref renderer.ShowPlotLabels); + ImGui.Unindent(); + } + ImGui.Checkbox("Show Actors", ref renderer.ShowActors); + if (renderer.ShowActors) { + ImGui.Indent(); + ImGui.Checkbox("Show Players", ref renderer.ShowPlayers); + ImGui.Checkbox("Show NPCs", ref renderer.ShowNpcs); + ImGui.Checkbox("Show Mobs", ref renderer.ShowMobs); + ImGui.Unindent(); + } + ImGui.Separator(); + ImGui.Text("Camera Controls:"); + if (ImGui.Button("Reset Camera")) { + if (renderer.CameraController is FreeCameraController freeCam) freeCam.SetDefaultRotation(); + } + ImGui.SameLine(); + if (ImGui.Button("Toggle Wireframe")) { Context!.ToggleWireframeMode(); } + if (ImGui.Button("View Field Overview")) { renderer.SetFieldOverviewCamera(); } + ImGui.Separator(); + bool isFollowing = renderer.CameraController is FollowCameraController { IsFollowingPlayer: true }; + long? followedId = renderer.CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; + ImGui.Text($"Player Follow: {(isFollowing ? $"Player ID: {followedId}" : string.Empty)}"); + if (isFollowing) { + if (ImGui.Button("Stop Following")) { renderer.StopFollowingPlayer(); } + } else { + if (ImGui.Button("Follow First Player")) { + FieldPlayer? firstPlayer = renderer.Field.Players.Values.FirstOrDefault(); + if (firstPlayer != null) renderer.StartFollowingPlayer(firstPlayer); + } + } + if (isFollowing) { + Vector3 target = renderer.CameraController switch { FreeCameraController freeCam => freeCam.CameraTarget, FollowCameraController followCam => followCam.CameraTarget, _ => Vector3.Zero }; + ImGui.Text($"Player Following Position: {target}"); + } + ImGui.Text($"Camera Position: {renderer.CameraController.Camera.Transform.Position}"); + Vector3 forward = renderer.CameraController.Camera.Transform.FrontAxis; + ImGui.Text($"Camera Forward: X={forward.X:F3}, Y={forward.Y:F3}, Z={forward.Z:F3}"); + ImGui.Text($"Wireframe Mode: {(Context!.WireframeMode ? "ON" : "OFF")}"); + ImGuiController.ClampWindowToViewport(); + ImGui.End(); + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs index 95ae01d7a..dfd7d03dd 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs @@ -123,6 +123,7 @@ public void Render() { ImGui.EndTable(); } + ImGuiController.ClampWindowToViewport(); ImGui.End(); } } diff --git a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj index 075d80051..2897398ac 100644 --- a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj +++ b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj @@ -1,40 +1,101 @@  - - Exe - net8.0 - enable - enable - True - - - - - - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - + + Exe + net8.0 + enable + enable + True + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Maple2.Server.DebugGame/Program.cs b/Maple2.Server.DebugGame/Program.cs index a4a8148aa..43bb08a0d 100644 --- a/Maple2.Server.DebugGame/Program.cs +++ b/Maple2.Server.DebugGame/Program.cs @@ -1,5 +1,4 @@ -using System.CommandLine; -using System.Globalization; +using System.Globalization; using System.Net; using System.Reflection; using Autofac; @@ -123,7 +122,7 @@ autofac.RegisterModule(); // Make all packet handlers available to PacketRouter - autofac.RegisterAssemblyTypes(Assembly.LoadFrom("Maple2.Server.Game.dll")) + autofac.RegisterAssemblyTypes(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "Maple2.Server.Game.dll"))) .Where(type => typeof(PacketHandler).IsAssignableFrom(type)) .As>() .PropertiesAutowired() @@ -132,11 +131,11 @@ // ChatCommand Handlers autofac.RegisterType(); - autofac.RegisterAssemblyTypes(typeof(CommandRouter).Assembly) + autofac.RegisterAssemblyTypes(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "Maple2.Server.Game.dll"))) .PublicOnly() .WithAttributeFiltering() - .Where(type => typeof(Command).IsAssignableFrom(type)) - .As() + .Where(type => typeof(GameCommand).IsAssignableFrom(type)) + .As() .PropertiesAutowired(); // Managers diff --git a/Maple2.Server.DebugGame/RenderPipeline/DebugScene.xml b/Maple2.Server.DebugGame/RenderPipeline/DebugScene.xml new file mode 100644 index 000000000..7c3b13737 --- /dev/null +++ b/Maple2.Server.DebugGame/RenderPipeline/DebugScene.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Maple2.Server.DebugGame/RenderPipeline/Screen.xml b/Maple2.Server.DebugGame/RenderPipeline/Screen.xml new file mode 100644 index 000000000..756649417 --- /dev/null +++ b/Maple2.Server.DebugGame/RenderPipeline/Screen.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Maple2.Server.DebugGame/RenderPipeline/Wireframe.xml b/Maple2.Server.DebugGame/RenderPipeline/Wireframe.xml new file mode 100644 index 000000000..366804288 --- /dev/null +++ b/Maple2.Server.DebugGame/RenderPipeline/Wireframe.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Maple2.Server.DebugGame/Shaders/Common/instance.hlsl b/Maple2.Server.DebugGame/Shaders/Common/instance.hlsl new file mode 100644 index 000000000..660474e00 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Common/instance.hlsl @@ -0,0 +1,6 @@ +cbuffer InstanceBuffer : register(b1) +{ + matrix Transformation; + matrix TransformationInverse; + float4 color; +}; diff --git a/Maple2.Server.DebugGame/Shaders/Common/pixelInput.hlsl b/Maple2.Server.DebugGame/Shaders/Common/pixelInput.hlsl new file mode 100644 index 000000000..0dc3a7d32 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Common/pixelInput.hlsl @@ -0,0 +1,8 @@ +struct vs_out { + float4 position : SV_POSITION; + float4 color : COLOR; + float3 normal : NORMAL; + float2 texcoord : TEXCOORD; + float3 tangent : TANGENT; + float3 binormal : BINORMAL; +}; diff --git a/Maple2.Server.DebugGame/Shaders/Common/sceneInput.hlsl b/Maple2.Server.DebugGame/Shaders/Common/sceneInput.hlsl new file mode 100644 index 000000000..3bba629c9 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Common/sceneInput.hlsl @@ -0,0 +1,4 @@ +struct SceneProperties { + float4x4 viewMatrix; + float4x4 projectionMatrix; +}; \ No newline at end of file diff --git a/Maple2.Server.DebugGame/Shaders/Common/sceneView.hlsl b/Maple2.Server.DebugGame/Shaders/Common/sceneView.hlsl new file mode 100644 index 000000000..faea7f340 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Common/sceneView.hlsl @@ -0,0 +1,5 @@ +cbuffer SceneViewBuffer : register(b0) +{ + matrix ViewMatrix; + matrix ProjectionMatrix; +}; \ No newline at end of file diff --git a/Maple2.Server.DebugGame/Shaders/Common/vertexAttributes.hlsl b/Maple2.Server.DebugGame/Shaders/Common/vertexAttributes.hlsl new file mode 100644 index 000000000..5eb15642e --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Common/vertexAttributes.hlsl @@ -0,0 +1,44 @@ +struct vs_PositionBinding { + float3 position : POSITION; +}; + +struct vs_AttributeBinding { + float3 normal : NORMAL; + uint color : COLOR; + float2 texcoord : TEXCOORD; +}; + +struct vs_OrientationBinding { + float3 tangent : TANGENT; + float3 binormal : BINORMAL; +}; + +struct vs_MorphBinding { + float3 morphPos1 : MORPH_POSITION; +}; + +struct vs_BlendBinding { + float3 blendWeight : BLENDWEIGHT; + uint blendIndices : BLENDINDICES; +}; + +float4 intToVector(uint value) { + float4 result; + result.x = (float)((value >> (3 * 16)) & 0xFF); + result.y = (float)((value >> (2 * 16)) & 0xFF); + result.z = (float)((value >> (1 * 16)) & 0xFF); + result.w = (float)(value & 0xFF); + + return result; +} + +uint vectorToInt(float4 value) { + value = saturate(value); // clamp from 0 to 1 + + uint result = (uint)(value.w * 255); + result += (uint)(value.x * 255) << (3 * 16); + result += (uint)(value.y * 255) << (2 * 16); + result += (uint)(value.z * 255) << (1 * 16); + + return result; +} diff --git a/Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/pixel.hlsl b/Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/pixel.hlsl new file mode 100644 index 000000000..1502bf8e8 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/pixel.hlsl @@ -0,0 +1,10 @@ +#include + +Texture2D sampleTexture: register(t0); + +SamplerState sampleSampler: register(s0); + +float4 ps_main(vs_out input) : SV_TARGET{ + return input.color; + //return sampleTexture.Sample(sampleSampler, input.texcoord); +} diff --git a/Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/vertex.hlsl b/Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/vertex.hlsl new file mode 100644 index 000000000..83c48e703 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/vertex.hlsl @@ -0,0 +1,18 @@ +#include +#include +#include +#include +#include + +vs_out vs_main(vs_PositionBinding pos, vs_AttributeBinding attrib, vs_OrientationBinding orientation, vs_MorphBinding morph, vs_BlendBinding blend) { + vs_out output; + + output.position = mul(ProjectionMatrix, mul(ViewMatrix, mul(Transformation, float4(pos.position, 1)))); + output.color = intToVector(attrib.color); + output.normal = attrib.normal; + output.texcoord = attrib.texcoord; + output.tangent = orientation.tangent; + output.binormal = orientation.binormal; + + return output; +} diff --git a/Maple2.Server.DebugGame/Shaders/Pipelines/Screen/pixel.hlsl b/Maple2.Server.DebugGame/Shaders/Pipelines/Screen/pixel.hlsl new file mode 100644 index 000000000..a5ed592ce --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Pipelines/Screen/pixel.hlsl @@ -0,0 +1,10 @@ +#include + +Texture2D sampleTexture: register(t0); + +SamplerState sampleSampler: register(s0); + +float4 ps_main(vs_out input) : SV_TARGET{ + //return float4(input.texcoord, 0, 1); + return sampleTexture.Sample(sampleSampler, input.texcoord) * float4(1, 1, 1, 1); +} diff --git a/Maple2.Server.DebugGame/Shaders/Pipelines/Screen/vertex.hlsl b/Maple2.Server.DebugGame/Shaders/Pipelines/Screen/vertex.hlsl new file mode 100644 index 000000000..80ba4bcb7 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Pipelines/Screen/vertex.hlsl @@ -0,0 +1,15 @@ +#include +#include + +vs_out vs_main(vs_PositionBinding pos, vs_AttributeBinding attrib, vs_OrientationBinding orientation, vs_MorphBinding morph, vs_BlendBinding blend) { + vs_out output; + + output.position = float4(pos.position, 1); + output.color = intToVector(attrib.color); + output.normal = attrib.normal; + output.texcoord = attrib.texcoord; + output.tangent = orientation.tangent; + output.binormal = orientation.binormal; + + return output; +} diff --git a/Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/pixel.hlsl b/Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/pixel.hlsl new file mode 100644 index 000000000..cb4968769 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/pixel.hlsl @@ -0,0 +1,8 @@ +struct PixelInput { + float4 position : SV_POSITION; + float4 color : COLOR; +}; + +float4 ps_main(PixelInput input) : SV_TARGET { + return input.color; +} diff --git a/Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/vertex.hlsl b/Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/vertex.hlsl new file mode 100644 index 000000000..31ec09336 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/vertex.hlsl @@ -0,0 +1,29 @@ +#include +#include + +struct VertexInput { + float3 position : POSITION; +}; + +struct VertexOutput { + float4 position : SV_POSITION; + float4 color : COLOR; +}; + +VertexOutput vs_main(VertexInput input) { + VertexOutput output; + + // Transform vertex position to world space + float4 worldPos = mul(float4(input.position, 1.0f), Transformation); + + // Transform to view space + float4 viewPos = mul(worldPos, ViewMatrix); + + // Transform to projection space + output.position = mul(viewPos, ProjectionMatrix); + + // Pass through color + output.color = color; + + return output; +} diff --git a/Maple2.Server.DebugGame/Shaders/wireframePixel.hlsl b/Maple2.Server.DebugGame/Shaders/wireframePixel.hlsl new file mode 100644 index 000000000..cb4968769 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/wireframePixel.hlsl @@ -0,0 +1,8 @@ +struct PixelInput { + float4 position : SV_POSITION; + float4 color : COLOR; +}; + +float4 ps_main(PixelInput input) : SV_TARGET { + return input.color; +} diff --git a/Maple2.Server.DebugGame/Shaders/wireframeVertex.hlsl b/Maple2.Server.DebugGame/Shaders/wireframeVertex.hlsl new file mode 100644 index 000000000..520f96d92 --- /dev/null +++ b/Maple2.Server.DebugGame/Shaders/wireframeVertex.hlsl @@ -0,0 +1,33 @@ +cbuffer ConstantBuffer : register(b0) { + matrix worldMatrix; + matrix viewMatrix; + matrix projectionMatrix; + float4 color; +}; + +struct VertexInput { + float3 position : POSITION; +}; + +struct VertexOutput { + float4 position : SV_POSITION; + float4 color : COLOR; +}; + +VertexOutput vs_main(VertexInput input) { + VertexOutput output; + + // Transform vertex position to world space + float4 worldPos = mul(float4(input.position, 1.0f), worldMatrix); + + // Transform to view space + float4 viewPos = mul(worldPos, viewMatrix); + + // Transform to projection space + output.position = mul(viewPos, projectionMatrix); + + // Pass through color + output.color = color; + + return output; +}