From 6773a0ef7620c17d84a1415747750415e38498cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Wed, 23 Jul 2025 20:42:33 -0300 Subject: [PATCH 01/25] more debuggame vibe coding vibecoding vibe coding2 . . .. vibe coding maxx --- .../Graphics/Assets/CoreModels.cs | 111 ++- .../Graphics/DebugFieldRenderer.cs | 764 ++++++++++++++++-- .../Graphics/DebugFieldWindow.cs | 150 +++- .../Graphics/DebugGraphicsContext.cs | 459 +++++++++-- .../Graphics/ImGuiController.cs | 14 +- .../Graphics/Resources/Texture.cs | 7 +- .../Maple2.Server.DebugGame.csproj | 5 + Maple2.Server.DebugGame/Program.cs | 9 +- .../Shaders/wireframePixel.hlsl | 8 + .../Shaders/wireframeVertex.hlsl | 33 + 10 files changed, 1397 insertions(+), 163 deletions(-) create mode 100644 Maple2.Server.DebugGame/Shaders/wireframePixel.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/wireframeVertex.hlsl diff --git a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs index c786d89a3..bb002e43f 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() { @@ -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 = new(); + List wireIndices = new(); + List solidIndices = new(); + + // 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/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 7d7d83efb..6f0b2f95c 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -4,14 +4,31 @@ using ImGuiNET; using Maple2.Model.Enum; using Maple2.Model.Game; +using Maple2.Model.Metadata.FieldEntity; using Maple2.Server.Game.Manager; using Maple2.Server.Game.Model; +using Maple2.Tools.VectorMath; +using Silk.NET.Maths; namespace Maple2.Server.DebugGame.Graphics; public class DebugFieldRenderer : IFieldRenderer { public DebugGraphicsContext Context { get; init; } public FieldManager Field { get; init; } + + // Coordinate system transformation - flip Y/Z axes but keep original scale + private static readonly Matrix4x4 MapRotation = Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)); + + // Transform position - flip Y/Z axes but keep original scale + private static Vector3 TransformMapPosition(Vector3 mapPosition) { + return Vector3.Transform(mapPosition, MapRotation); + } + + // Transform size - no rotation, keep original scale + private static Vector3 TransformMapSize(Vector3 mapSize) { + return mapSize; // Keep original size without scaling + } + public bool IsActive { get { activeMutex.WaitOne(); @@ -23,7 +40,17 @@ public bool IsActive { private readonly HashSet activeWindows = []; private readonly Mutex activeMutex = new(); - private IActor? selectedActor = null; + private IActor? selectedActor; + + // 3D visualization settings + private bool showBoxColliders = true; // Box colliders enabled by default (main map blocks) + private bool showMeshColliders = false; // Mesh colliders disabled by default + private bool showSpawnPoints = false; // Spawn points disabled by default + private bool showVibrateObjects = false; // Vibrate objects disabled by default + private bool showActors = true; // Actors enabled by default + + // Entity caching for performance + private FieldEntity[]? cachedStaticEntities = null; // Cache static entities (spawn points, triggers, etc.) public DebugFieldRenderer(DebugGraphicsContext context, FieldManager field) { Context = context; @@ -40,8 +67,26 @@ 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 + if (Context is { IsFollowingPlayer: false, HasManuallyStopped: false }) { + FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); + if (firstPlayer != null && (!hasTriedAutoFollow || Context.FollowedPlayerId != firstPlayer.Value.Character.Id)) { + Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + hasTriedAutoFollow = true; + } + } + + // Update camera follow if active + UpdateCameraFollow(); + + // We'll use ImGui for text rendering instead of 3D billboards + // ImGui is much simpler and more reliable for debug text } + private bool hasTriedAutoFollow = false; + public void Render(double delta) { if (!IsActive) { return; @@ -67,12 +112,25 @@ public void Render(double delta) { } ImGui.End(); + // Render visualization controls + RenderVisualizationControls(); + // Show actor details panel if an actor is selected if (selectedActor != null) { RenderActorDetailsPanel(fieldWindowPos, fieldWindowSize); } } + // New method specifically for field window 3D rendering + public void RenderFieldWindow3D(double delta) { + if (!IsActive) { + return; + } + + // Render 3D field visualization using the DirectX pipeline + RenderField3DVisualization(delta); + } + private void RenderFieldBasicInfo() { ImGui.Text($"Map ID: {Field.MapId}"); ImGui.Text($"Room ID: {Field.RoomId}"); @@ -103,128 +161,132 @@ private void RenderActorList() { return; } + // Actor selection controls bool showActorDetailsDisabled = selectedActor == null; - if (showActorDetailsDisabled) { ImGui.BeginDisabled(); } - if (ImGui.Button("Show Actor Details") && !showActorDetailsDisabled) { // Keep the selected actor to show details panel } - if (showActorDetailsDisabled) { ImGui.EndDisabled(); } ImGui.SameLine(); - bool clearSelectionDisabled = selectedActor == null; - if (clearSelectionDisabled) { ImGui.BeginDisabled(); } - if (ImGui.Button("Clear Selection") && !clearSelectionDisabled) { selectedActor = null; } - if (clearSelectionDisabled) { ImGui.EndDisabled(); } - if (ImGui.BeginTable("Active Actors", 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 index = 0; - - // Render Players - foreach ((int objectId, FieldPlayer player) in Field.Players) { - ImGui.TableNextRow(); - - bool selected = player == selectedActor; - bool nextSelected = false; - + // Players section (collapsed by default) + if (ImGui.CollapsingHeader($"Players ({Field.Players.Count})", ImGuiTreeNodeFlags.None)) { + if (ImGui.BeginTable("Players Table", 4)) { + ImGui.TableNextRow(ImGuiTableRowFlags.Headers); ImGui.TableSetColumnIndex(0); - nextSelected |= ImGui.Selectable($"Player##Actor {index} 0", selected); + ImGui.Text("Name"); ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{player.Value.Character.Name}##Actor {index} 1", selected); + ImGui.Text("Level"); ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{player.Value.Character.Level}##Actor {index} 2", selected); + ImGui.Text("Object ID"); 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); - - if (nextSelected) { - selectedActor = player; + ImGui.Text("Status"); + + int playerIndex = 0; + foreach ((int objectId, FieldPlayer player) in Field.Players) { + ImGui.TableNextRow(); + bool selected = player == selectedActor; + bool nextSelected = false; + + ImGui.TableSetColumnIndex(0); + nextSelected |= ImGui.Selectable($"{player.Value.Character.Name}##Player {playerIndex} 0", selected); + ImGui.TableSetColumnIndex(1); + nextSelected |= ImGui.Selectable($"{player.Value.Character.Level}##Player {playerIndex} 1", selected); + ImGui.TableSetColumnIndex(2); + nextSelected |= ImGui.Selectable($"{objectId}##Player {playerIndex} 2", selected); + ImGui.TableSetColumnIndex(3); + nextSelected |= ImGui.Selectable($"{(player.IsDead ? "Dead" : "Alive")}##Player {playerIndex} 3", selected); + + if (nextSelected) { + selectedActor = player; + } + ++playerIndex; } - - ++index; + ImGui.EndTable(); } + } - // Render NPCs - foreach ((int objectId, FieldNpc npc) in Field.Npcs) { - ImGui.TableNextRow(); - - bool selected = npc == selectedActor; - bool nextSelected = false; - + // NPCs section (collapsed by default, includes both NPCs and Mobs) + 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); - nextSelected |= ImGui.Selectable($"NPC##Actor {index} 0", selected); + ImGui.Text("Type"); ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Name}##Actor {index} 1", selected); + ImGui.Text("Name"); ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Basic.Level}##Actor {index} 2", selected); + ImGui.Text("Level"); ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{objectId}##Actor {index} 3", selected); + ImGui.Text("Object ID"); ImGui.TableSetColumnIndex(4); - nextSelected |= ImGui.Selectable($"{(npc.IsDead ? "Dead" : "Alive")}##Actor {index} 4", selected); - - if (nextSelected) { - selectedActor = npc; + ImGui.Text("Status"); + + int npcIndex = 0; + + // 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##NPC {npcIndex} 0", selected); + ImGui.TableSetColumnIndex(1); + nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Name}##NPC {npcIndex} 1", selected); + ImGui.TableSetColumnIndex(2); + nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Basic.Level}##NPC {npcIndex} 2", selected); + ImGui.TableSetColumnIndex(3); + nextSelected |= ImGui.Selectable($"{objectId}##NPC {npcIndex} 3", selected); + ImGui.TableSetColumnIndex(4); + nextSelected |= ImGui.Selectable($"{(npc.IsDead ? "Dead" : "Alive")}##NPC {npcIndex} 4", selected); + + if (nextSelected) { + selectedActor = npc; + } + ++npcIndex; } - ++index; - } - - // 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; + // 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##Mob {npcIndex} 0", selected); + ImGui.TableSetColumnIndex(1); + nextSelected |= ImGui.Selectable($"{mob.Value.Metadata.Name}##Mob {npcIndex} 1", selected); + ImGui.TableSetColumnIndex(2); + nextSelected |= ImGui.Selectable($"{mob.Value.Metadata.Basic.Level}##Mob {npcIndex} 2", selected); + ImGui.TableSetColumnIndex(3); + nextSelected |= ImGui.Selectable($"{objectId}##Mob {npcIndex} 3", selected); + ImGui.TableSetColumnIndex(4); + nextSelected |= ImGui.Selectable($"{(mob.IsDead ? "Dead" : "Alive")}##Mob {npcIndex} 4", selected); + + if (nextSelected) { + selectedActor = mob; + } + ++npcIndex; } - - ++index; + ImGui.EndTable(); } - - ImGui.EndTable(); } } @@ -346,7 +408,7 @@ private void RenderActorStatsInfo() { ImGui.Text("Stats:"); ImGui.Indent(); - var stats = selectedActor.Stats; + StatsManager stats = selectedActor.Stats; ImGui.Text($"Health: {stats.Values[BasicAttribute.Health].Current}/{stats.Values[BasicAttribute.Health].Total}"); // Show additional stats for players @@ -382,7 +444,9 @@ private void RenderActorAdditionalInfo() { ImGui.Unindent(); } - public void CleanUp() { } + public void CleanUp() { + // No cleanup needed for ImGui text rendering + } public void AttachWindow(DebugFieldWindow window) { activeMutex.WaitOne(); @@ -390,6 +454,18 @@ public void AttachWindow(DebugFieldWindow window) { activeWindows.Add(window); activeMutex.ReleaseMutex(); + + // Set default camera to field overview when window is first attached + SetFieldOverviewCamera(); + + // Auto-follow first player when field window is opened + // But only if user hasn't manually stopped following + if (!Context.IsFollowingPlayer && !Context.HasManuallyStopped) { + var firstPlayer = Field.Players.Values.FirstOrDefault(); + if (firstPlayer != null) { + Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + } + } } public void DetachWindow(DebugFieldWindow window) { @@ -399,4 +475,520 @@ public void DetachWindow(DebugFieldWindow window) { activeMutex.ReleaseMutex(); } + + private void RenderField3DVisualization(double delta) { + if (Field.AccelerationStructure == null) { + return; + } + + // Set up 3D rendering matrices + Context.UpdateViewMatrix(); + Context.UpdateProjectionMatrix(); + + // Enable wireframe mode for field visualization + bool originalWireframeMode = Context.WireframeMode; + Context.WireframeMode = true; + Context.SetWireframeRasterizer(); + + // Always use wireframe shaders for field visualization (they don't need texture samplers) + Context.WireframeVertexShader?.Bind(); + Context.WireframePixelShader?.Bind(); + + // Render field entities using the DirectX 11 pipeline + RenderFieldEntities3D(); + + // Restore original wireframe mode + Context.WireframeMode = originalWireframeMode; + Context.SetSolidRasterizer(); + + // Also render the entity details window + RenderEntityDetailsWindow(); + } + + private void RenderFieldEntities3D() { + if (Field.AccelerationStructure == null) { + return; + } + + // Render box colliders + if (showBoxColliders) { + RenderBoxColliders(); + } + + // Render mesh colliders + if (showMeshColliders) { + RenderMeshColliders(); + } + + // Render spawn points + if (showSpawnPoints) { + RenderSpawnPoints(); + } + + // Render vibrate objects + if (showVibrateObjects) { + RenderVibrateObjects(); + } + + // Render actors (players, NPCs, mobs) + if (showActors) { + RenderActors(); + + // Render text labels above actors using ImGui + RenderActorTextLabels(); + } + } + + private void RenderBoxColliders() { + // Set white color for box colliders + Context.SetColor(1, 1, 1, 1); // White for box colliders + + List allBoxColliders = GetAllEntities().OfType().ToList(); + + foreach (FieldBoxColliderEntity boxCollider in allBoxColliders) { + Transform transform = new() { + Position = boxCollider.Position, + RotationAnglesDegrees = boxCollider.Rotation, + }; + + transform.Transformation *= MapRotation; + + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(boxCollider.Size) * + Matrix4x4.CreateFromQuaternion(transform.Quaternion) * + Matrix4x4.CreateTranslation(boxCollider.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (white) + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderMeshColliders() { + IEnumerable allMeshColliders = GetAllEntities().OfType(); + + 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; + + // Convert Euler angles (degrees) to quaternion + Vector3 rotationRadians = meshCollider.Rotation * (MathF.PI / 180.0f); + Quaternion rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + + // Apply coordinate system transformation (Y↔Z flip) + Quaternion coordinateTransform = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)(-Math.PI / 2)); + rotation = coordinateTransform * rotation; + + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * + Matrix4x4.CreateFromQuaternion(rotation) * + Matrix4x4.CreateTranslation(center); + + Context.UpdateConstantBuffer(worldMatrix); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderSpawnPoints() { + IEnumerable allSpawnPoints = GetAllEntities().OfType(); + + foreach (FieldSpawnTile spawnPoint in allSpawnPoints) { + // Convert Euler angles (degrees) to quaternion + Vector3 rotationRadians = spawnPoint.Rotation * (MathF.PI / 180.0f); + Quaternion rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 50.0f) * + Matrix4x4.CreateFromQuaternion(rotation) * + Matrix4x4.CreateTranslation(spawnPoint.Position); + + Context.UpdateConstantBuffer(worldMatrix); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderVibrateObjects() { + foreach (FieldVibrateEntity vibrateEntity in Field.AccelerationStructure!.VibrateEntities) { + // Convert Euler angles (degrees) to quaternion + Vector3 rotationRadians = vibrateEntity.Rotation * (MathF.PI / 180.0f); + Quaternion rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 100.0f) * + Matrix4x4.CreateFromQuaternion(rotation) * + Matrix4x4.CreateTranslation(vibrateEntity.Position); + + Context.UpdateConstantBuffer(worldMatrix); + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderActors() { + // Agent parameters from Navigation.cs: radius=0.3f (30cm), height=1.4f (140cm) + const float agentRadius = 0.3f * 100.0f; // Convert to game units (30cm) + const float agentHeight = 1.4f * 100.0f; // Convert to game units (140cm) + + // Render players using cylinders (like Recast agents) - Cyan color + Context.SetColor(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) + Matrix4x4 rotation = Matrix4x4.CreateRotationX((float)(Math.PI / 2)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(player.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (cyan) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for players (like Recast agents) + } + + // Render NPCs using cylinders (like Recast agents) - Green for alive, Gray for dead + foreach ((int _, FieldNpc npc) in Field.Npcs) { + // Set color based on dead status + if (npc.IsDead) { + Context.SetColor(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead NPCs + } else { + Context.SetColor(0, 1, 0, 1); // Green for alive NPCs + } + + // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) + Matrix4x4 rotation = Matrix4x4.CreateRotationX((float)(Math.PI / 2)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(npc.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (green/gray) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs (like Recast agents) + } + + // Render Mobs using cylinders (like Recast agents) - Red for aggressive, Yellow for wandering, Gray for dead + foreach ((int _, FieldNpc mob) in Field.Mobs) { + // Set color based on dead status and battle state + if (mob.IsDead) { + Context.SetColor(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) { + Context.SetColor(1, 0, 0, 1); // Red for aggressive mobs + } else { + Context.SetColor(1, 1, 0, 1); // Yellow for wandering mobs + } + } + + // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) + Matrix4x4 rotation = Matrix4x4.CreateRotationX((float)(Math.PI / 2)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(mob.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (red/yellow/gray) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for mobs (like Recast agents) + } + } + + private void RenderVisualizationControls() { + if (ImGui.Begin("3D Visualization Controls")) { + ImGui.Text("Field Visualization Settings"); + ImGui.Separator(); + + ImGui.Checkbox("Show Box Colliders", ref showBoxColliders); + ImGui.Checkbox("Show Mesh Colliders", ref showMeshColliders); + ImGui.Checkbox("Show Spawn Points", ref showSpawnPoints); + ImGui.Checkbox("Show Vibrate Objects", ref showVibrateObjects); + ImGui.Checkbox("Show Actors", ref showActors); + + ImGui.Separator(); + + // Camera controls + ImGui.Text("Camera Controls:"); + if (ImGui.Button("Reset Camera")) { + // Reset quaternion rotation to default values + Context.CameraRotation = new Quaternion(-0.140f, 0.332f, 0.364f, 0.859f); + Context.UpdateViewMatrix(); + } + + ImGui.SameLine(); + if (ImGui.Button("Toggle Wireframe")) { + Context.ToggleWireframeMode(); + } + + if (ImGui.Button("View Field Overview")) { + SetFieldOverviewCamera(); + } + + // Player follow controls + ImGui.Separator(); + ImGui.Text("Player Follow:"); + + if (Context.IsFollowingPlayer) { + ImGui.Text($"Following Player ID: {Context.FollowedPlayerId}"); + if (ImGui.Button("Stop Following")) { + Context.StopFollowingPlayer(); + } + } else { + if (ImGui.Button("Follow First Player")) { + FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); + if (firstPlayer != null) { + Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + } + } + } + + // Camera position display + ImGui.Text($"Camera Position: {Context.CameraPosition}"); + ImGui.Text($"Camera Target: {Context.CameraTarget}"); + + // Camera rotation display + Quaternion q = Context.CameraRotation; + ImGui.Text($"Camera Rotation: X={q.X:F3}, Y={q.Y:F3}, Z={q.Z:F3}, W={q.W:F3}"); + + ImGui.Text($"Wireframe Mode: {(Context.WireframeMode ? "ON" : "OFF")}"); + } + ImGui.End(); + } + + private void RenderEntityDetailsWindow() { + if (Field.AccelerationStructure == null) { + return; + } + + // Create entity details window + if (ImGui.Begin("Field Entity Details")) { + ImGui.Text("Field Entity Information"); + ImGui.Separator(); + + // Basic field info + ImGui.Text($"Grid Size: {Field.AccelerationStructure.GridSize}"); + ImGui.Text($"Min Index: {Field.AccelerationStructure.MinIndex}"); + ImGui.Text($"Max Index: {Field.AccelerationStructure.MaxIndex}"); + + ImGui.Separator(); + + // Entity counts by type + FieldEntity[] allEntities = GetAllEntities().ToArray(); + Dictionary entityCounts = allEntities.GroupBy(e => e.GetType().Name).ToDictionary(g => g.Key, g => g.Count()); + + ImGui.Text("Entity Types:"); + foreach (KeyValuePair kvp in entityCounts) { + ImGui.Text($" {kvp.Key}: {kvp.Value}"); + } + + ImGui.Separator(); + } + ImGui.End(); + } + + private void SetFieldOverviewCamera() { + var 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 (var collider in allColliders) { + min = Vector3.Min(min, collider.Position - collider.Size * 0.5f); + max = Vector3.Max(max, collider.Position + collider.Size * 0.5f); + } + + // Transform to visualization coordinates + Vector3 centerMap = (min + max) * 0.5f; + Vector3 sizeMap = max - min; + + Vector3 center = TransformMapPosition(centerMap); + Vector3 size = TransformMapSize(sizeMap); + float distance = Math.Max(size.X, Math.Max(size.Y, size.Z)) * 1.5f; + + Context.SetCameraTarget(center); + Context.SetCameraPosition(center + new Vector3(0, -distance * 0.7f, distance * 0.7f)); + } else { + // Fallback if no colliders found + Context.SetCameraPosition(new Vector3(0, -1000, 1000)); + Context.SetCameraTarget(Vector3.Zero); + } + } + + private void UpdateCameraFollow() { + if (!Context.IsFollowingPlayer || Context.FollowedPlayerId == null) { + return; + } + + // Find the player being followed + FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == Context.FollowedPlayerId); + if (followedPlayer == null) { + // Player not found, stop following + Context.StopFollowingPlayer(); + return; + } + + // Update camera to follow player position + Vector3 playerPosition = followedPlayer.Position; + Vector3 offset = new Vector3(-900, -1200, 2000); // Much further behind and above the player for better overview + + Context.SetCameraTarget(playerPosition); + Context.SetCameraPosition(playerPosition + offset); + } + + private Vector2D GetFieldWindowSize() { + // Get the size from the first active field window, or use default + activeMutex.WaitOne(); + try { + var firstWindow = activeWindows.FirstOrDefault(); + if (firstWindow?.DebuggerWindow != null) { + return firstWindow.DebuggerWindow.FramebufferSize; + } + } finally { + activeMutex.ReleaseMutex(); + } + + // Fallback to default field window size + return DebugGraphicsContext.DefaultFieldWindowSize; + } + + private Vector2? WorldToScreen(Vector3 worldPos) { + // Transform world position to screen coordinates + Matrix4x4 viewMatrix = Context.ViewMatrix; + Matrix4x4 projMatrix = Context.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 null; + + // 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 null; + + // Get the actual viewport size from the field window (not main window) + var 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 + + return new Vector2(screenX, screenY); + } + + private void RenderActorTextLabels() { + var drawList = ImGui.GetBackgroundDrawList(); + + // Render player names using ImGui + foreach ((int _, FieldPlayer player) in Field.Players) { + Vector3 textPos = player.Position + new Vector3(0, 0, 200); // Above the cylinder (higher) + Vector2? screenPos = WorldToScreen(textPos); + if (screenPos.HasValue) { + string playerName = player.Value.Character.Name; + Vector2 textSize = ImGui.CalcTextSize(playerName); + Vector4 textColor = new Vector4(0, 1, 1, 1); // Cyan + + // Draw semi-transparent background + Vector2 bgMin = screenPos.Value - new Vector2(4, 2); + Vector2 bgMax = screenPos.Value + textSize + new Vector2(4, 2); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); // Dark background + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); // Border + + // Draw text on top + drawList.AddText(screenPos.Value, ImGui.ColorConvertFloat4ToU32(textColor), playerName); + } + } + + // Render NPC names ImGui + foreach ((int _, FieldNpc npc) in Field.Npcs) { + Vector3 textPos = npc.Position + new Vector3(0, 0, 200); // Above the cylinder (higher) + Vector2? screenPos = WorldToScreen(textPos); + if (screenPos.HasValue) { + string npcName = npc.Value.Metadata.Name ?? $"NPC {npc.Value.Metadata.Id}"; + + Vector4 textColor = new Vector4(0, 1, 0, 1); + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + // Draw background for NPC name + Vector2 nameSize = ImGui.CalcTextSize(npcName); + Vector2 nameBgMin = screenPos.Value - new Vector2(4, 2); + Vector2 nameBgMax = screenPos.Value + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + + drawList.AddText(screenPos.Value, color, npcName); + } + } + + // Render Mob names and HP using ImGui + foreach ((int _, FieldNpc mob) in Field.Mobs) { + Vector3 textPos = mob.Position + new Vector3(0, 0, 200); // Above the cylinder (higher) + Vector2? screenPos = WorldToScreen(textPos); + if (screenPos.HasValue) { + string mobName = mob.Value.Metadata.Name ?? $"Mob {mob.Value.Metadata.Id}"; + + // 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; + + // Choose color based on status + Vector4 textColor; + 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 + } + + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + // Draw background for mob name + Vector2 nameSize = ImGui.CalcTextSize(mobName); + Vector2 nameBgMin = screenPos.Value - new Vector2(4, 2); + Vector2 nameBgMax = screenPos.Value + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + + drawList.AddText(screenPos.Value, color, mobName); + + // Position HP text below the name + Vector3 hpTextPos = mob.Position + new Vector3(0, 0, 180); // Slightly below name + Vector2? hpScreenPos = WorldToScreen(hpTextPos); + if (hpScreenPos.HasValue) { + string hpText = $"HP: {hpPercent:F1}%"; + Vector2 hpSize = ImGui.CalcTextSize(hpText); + Vector2 hpBgMin = hpScreenPos.Value - new Vector2(4, 2); + Vector2 hpBgMax = hpScreenPos.Value + hpSize + new Vector2(4, 2); + drawList.AddRectFilled(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + drawList.AddText(hpScreenPos.Value, color, hpText); + } + } + } + } + + private FieldEntity[] GetAllEntities() { + // Cache static entities since they never change during field runtime + if (cachedStaticEntities != null) { + return cachedStaticEntities; + } + + if (Field.AccelerationStructure == null) { + cachedStaticEntities = []; + return cachedStaticEntities; + } + + 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; + } } diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index 448ac3af1..652f6d187 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -1,4 +1,5 @@ -using Serilog; +using System.Numerics; +using Serilog; using Silk.NET.Core.Native; using Silk.NET.Direct3D11; using Silk.NET.DXGI; @@ -9,18 +10,21 @@ 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; } - private static int _windowIdCounter = 0; + private static int _windowIdCounter; + + // Camera input tracking + private Vector2 lastMousePosition; public DebugFieldWindow(DebugGraphicsContext context) { Context = context; @@ -50,7 +54,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; @@ -135,7 +139,7 @@ private void OnLoad() { ImGuiController.Initialize(DebuggerWindow); } - public unsafe void OnUpdate(double delta) { + public void OnUpdate(double delta) { if (IsClosing) { return; } @@ -144,6 +148,9 @@ public unsafe void OnUpdate(double delta) { ActiveRenderer.Update(); DebuggerWindow!.Title = $"Maple2 - {WindowName}: {ActiveRenderer.Field.Metadata.Name} [{ActiveRenderer.Field.MapId}] ({ActiveRenderer.Field.RoomId})"; } + + // Handle camera input for this field window + HandleCameraInput((float)delta); } public unsafe void OnRender(double delta) { @@ -170,11 +177,12 @@ 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(); + // Render 3D field content first (before ImGui) + if (ActiveRenderer is not null) { + ActiveRenderer.RenderFieldWindow3D(delta); + } + // Then render ImGui content if (ActiveRenderer is not null) { ActiveRenderer.Render(delta); } @@ -191,7 +199,7 @@ public unsafe void OnRender(double delta) { } - 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)); } @@ -207,4 +215,116 @@ private void UnloadField(DebugFieldRenderer renderer) { private void LoadField(DebugFieldRenderer renderer) { } + + private void HandleCameraInput(float deltaTime) { + if (Input == null) return; + + IKeyboard keyboard = Input.Keyboards[0]; + IMouse mouse = Input.Mice[0]; + + float moveSpeed = 500.0f * deltaTime; // Units per second + float rotateSpeed = 2.0f * deltaTime; // Radians per second + + // Faster movement when holding Shift + if (keyboard.IsKeyPressed(Key.ShiftLeft) || keyboard.IsKeyPressed(Key.ShiftRight)) { + moveSpeed *= 5.0f; // 5x faster with Shift + } + + // Calculate camera directions from quaternion rotation + Vector3 forward = Vector3.Transform(Vector3.UnitX, Context.CameraRotation); // X is forward in our coordinate system + Vector3 right = Vector3.Normalize(Vector3.Cross(forward, Vector3.UnitZ)); // Use Z as world up for transformed coords + Vector3 up = Vector3.UnitZ; // Use Z as up direction for Q/E movement + + // WASD movement + Vector3 movement = Vector3.Zero; + if (keyboard.IsKeyPressed(Key.W)) movement += forward; + if (keyboard.IsKeyPressed(Key.S)) movement -= forward; + if (keyboard.IsKeyPressed(Key.A)) movement -= right; + if (keyboard.IsKeyPressed(Key.D)) movement += right; + if (keyboard.IsKeyPressed(Key.Q)) movement += up; + if (keyboard.IsKeyPressed(Key.E)) movement -= up; + + // Arrow keys for camera rotation (quaternion-based) + bool rotated = false; + Quaternion rotationDelta = Quaternion.Identity; + + if (keyboard.IsKeyPressed(Key.Left)) { + rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitZ, rotateSpeed); // Yaw left + rotated = true; + } + if (keyboard.IsKeyPressed(Key.Right)) { + rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitZ, -rotateSpeed); // Yaw right + rotated = true; + } + if (keyboard.IsKeyPressed(Key.Up)) { + rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, -rotateSpeed); // Pitch up (inverted) + rotated = true; + } + if (keyboard.IsKeyPressed(Key.Down)) { + rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, rotateSpeed); // Pitch down (inverted) + rotated = true; + } + + if (rotated) { + // Apply rotation delta to camera + Context.CameraRotation = rotationDelta * Context.CameraRotation; + Context.CameraRotation = Quaternion.Normalize(Context.CameraRotation); + Context.UpdateViewMatrix(); + } + + if (movement != Vector3.Zero) { + movement = Vector3.Normalize(movement) * moveSpeed; + // Move camera position directly (free camera style) + Context.CameraPosition += movement; + Context.UpdateViewMatrix(); + + // Unlock camera follow when manually moving + if (Context.IsFollowingPlayer && movement.LengthSquared() > 0.01f) { + Context.StopFollowingPlayer(); + } + } + + // Mouse look (only when right mouse button is held) + if (mouse.IsButtonPressed(MouseButton.Right)) { + Vector2 mouseDelta = mouse.Position - lastMousePosition; + + if (mouseDelta.X != 0 || mouseDelta.Y != 0) { + float sensitivity = 0.002f; // Mouse sensitivity + + // Create rotation quaternions for yaw and pitch + Quaternion yawRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, -mouseDelta.X * sensitivity); // Yaw around Z-axis + Quaternion pitchRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, mouseDelta.Y * sensitivity); // Pitch around Y-axis (inverted) + + // Apply rotations: first yaw (world space), then pitch (local space) + Context.CameraRotation = yawRotation * Context.CameraRotation * pitchRotation; + + // Normalize to prevent drift + Context.CameraRotation = Quaternion.Normalize(Context.CameraRotation); + + Context.UpdateViewMatrix(); + } + } + + // Mouse wheel movement (with same modifiers as WASD) + if (mouse is { ScrollWheels.Count: > 0 }) { + float scroll = mouse.ScrollWheels[0].Y; + if (scroll != 0) { + // Use same speed modifiers as WASD movement + float wheelMoveSpeed = moveSpeed * scroll; + + // Move forward/backward along camera's forward direction (like W/S) + Vector3 wheelMovement = forward * wheelMoveSpeed; + + Context.CameraPosition += wheelMovement; + Context.UpdateViewMatrix(); + + // Unlock camera follow when manually moving + if (Context.IsFollowingPlayer && wheelMovement.LengthSquared() > 0.01f) { + Context.StopFollowingPlayer(); + } + } + + lastMousePosition = mouse.Position; + } + } } diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 62435ce4b..350040512 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -10,39 +10,63 @@ using Silk.NET.Maths; using Silk.NET.Windowing; using Maple2.Server.DebugGame.Graphics.Assets; -using Maple2.Server.Game.Model; using Maple2.Tools.Extensions; +using System.Numerics; 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(); 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; } + + // Enhanced shader support + private VertexShader? VertexShader { get; set; } + private PixelShader? PixelShader { get; set; } + public VertexShader? WireframeVertexShader { get; private set; } + public PixelShader? WireframePixelShader { get; private set; } + + // 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 = ""; + // 3D rendering state + public Matrix4x4 ViewMatrix { get; set; } = Matrix4x4.Identity; + public Matrix4x4 ProjectionMatrix { get; set; } = Matrix4x4.Identity; + public Vector3 CameraPosition { get; set; } = new Vector3(0, -1000, 1000); // Default overview position + public Vector3 CameraTarget { get; private set; } = Vector3.Zero; + private Vector3 CameraUp { get; set; } = Vector3.UnitY; + + // Quaternion-based camera rotation (eliminates gimbal lock) + public Quaternion CameraRotation { get; set; } = new Quaternion(-0.140f, 0.332f, 0.364f, 0.859f); // Back to standard Y up + public bool WireframeMode { get; set; } = true; // Start in wireframe mode + private readonly List fieldRenderers = []; private readonly Mutex fieldRendererMutex = new(); public DebugFieldRenderer[] FieldRenderers { @@ -58,13 +82,21 @@ 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; + + // Camera follow system + public bool IsFollowingPlayer { get; private set; } + public long? FollowedPlayerId { get; private set; } + public bool HasManuallyStopped { get; private set; } 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 void RunDebugger() { DebuggerWindow!.Initialize(); @@ -73,10 +105,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 +138,6 @@ public void RunDebugger() { } } } - } public bool UpdateWindow(IView window, bool shouldClose) { @@ -240,23 +277,33 @@ private void OnLoad() { DxSwapChain = swapChain; } - if (VertexShader is null) { - VertexShader = new VertexShader(this); - } + // Create shaders + VertexShader ??= new VertexShader(this); - if (PixelShader is null) { - PixelShader = new PixelShader(this); - } + PixelShader ??= new PixelShader(this); + + WireframeVertexShader ??= new VertexShader(this); + + WireframePixelShader ??= new PixelShader(this); VertexShader.Load("screenVertex.hlsl", "vs_main"); PixelShader.Load("screenPixel.hlsl", "ps_main"); + WireframeVertexShader.Load("wireframeVertex.hlsl", "vs_main"); + WireframePixelShader.Load("wireframePixel.hlsl", "ps_main"); + + // Create 3D rendering resources + Create3DRenderingResources(); 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("sample_derp_wave.png"); + + // Initialize 3D matrices + UpdateProjectionMatrix(); + UpdateViewMatrix(); ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); @@ -273,11 +320,192 @@ private string GetResourceRootPath(string rootPath) { return Path.GetFullPath(Path.Combine(resourceRootPath, rootPath)); } + 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; + + // Create constant buffer for matrices and color + var constantBufferDesc = new BufferDesc { + ByteWidth = 256, // 3 * 64 bytes for matrices + 16 bytes for color (rounded up to 256 for alignment) + Usage = Usage.Dynamic, + BindFlags = (uint)BindFlag.ConstantBuffer, + CPUAccessFlags = (uint)CpuAccessFlag.Write, + MiscFlags = 0, + }; + + ID3D11Buffer* constantBuffer = null; + SilkMarshal.ThrowHResult(DxDevice.CreateBuffer(&constantBufferDesc, (SubresourceData*)null, &constantBuffer)); + ConstantBuffer = constantBuffer; + } + + public void UpdateProjectionMatrix() { + Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; + float aspectRatio = (float)windowSize.X / windowSize.Y; + ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( + MathF.PI / 4.0f, // 45 degree field of view + aspectRatio, + 1.0f, // near plane + 50000.0f // far plane - much larger for big fields + ); + } + + public void UpdateViewMatrix() { + // Calculate forward direction from quaternion rotation + Vector3 forward = Vector3.Transform(Vector3.UnitX, CameraRotation); // X is forward in our coordinate system + + // Calculate target as position + forward direction + CameraTarget = CameraPosition + forward; + + // Calculate up vector from quaternion rotation + Vector3 up = Vector3.Transform(Vector3.UnitZ, CameraRotation); // Z is up in our coordinate system + + ViewMatrix = Matrix4x4.CreateLookAt(CameraPosition, CameraTarget, up); + } + + public void UpdateConstantBuffer(Matrix4x4 worldMatrix) { + UpdateConstantBuffer(worldMatrix, new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Default white color + } + + // Current color for rendering + private Vector4 currentColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); // Default white + + public void SetColor(float r, float g, float b, float a) { + currentColor = new Vector4(r, g, b, a); + } + + public void UpdateConstantBuffer(Matrix4x4 worldMatrix, bool useCurrentColor) { + UpdateConstantBuffer(worldMatrix, useCurrentColor ? currentColor : new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); + } + + public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { + if (ConstantBuffer.Handle == null) return; + + MappedSubresource mappedResource; + SilkMarshal.ThrowHResult(DxDeviceContext.Map(ConstantBuffer, 0, Map.WriteDiscard, 0, &mappedResource)); + + // Write matrices and color to constant buffer (world, view, projection, color) + float* data = (float*)mappedResource.PData; + + // Copy world matrix (transposed) + Matrix4x4 worldTransposed = Matrix4x4.Transpose(worldMatrix); + data[0] = worldTransposed.M11; data[1] = worldTransposed.M12; data[2] = worldTransposed.M13; data[3] = worldTransposed.M14; + data[4] = worldTransposed.M21; data[5] = worldTransposed.M22; data[6] = worldTransposed.M23; data[7] = worldTransposed.M24; + data[8] = worldTransposed.M31; data[9] = worldTransposed.M32; data[10] = worldTransposed.M33; data[11] = worldTransposed.M34; + data[12] = worldTransposed.M41; data[13] = worldTransposed.M42; data[14] = worldTransposed.M43; data[15] = worldTransposed.M44; + + // Copy view matrix (transposed) + Matrix4x4 viewTransposed = Matrix4x4.Transpose(ViewMatrix); + data[16] = viewTransposed.M11; data[17] = viewTransposed.M12; data[18] = viewTransposed.M13; data[19] = viewTransposed.M14; + data[20] = viewTransposed.M21; data[21] = viewTransposed.M22; data[22] = viewTransposed.M23; data[23] = viewTransposed.M24; + data[24] = viewTransposed.M31; data[25] = viewTransposed.M32; data[26] = viewTransposed.M33; data[27] = viewTransposed.M34; + data[28] = viewTransposed.M41; data[29] = viewTransposed.M42; data[30] = viewTransposed.M43; data[31] = viewTransposed.M44; + + // Copy projection matrix (transposed) + Matrix4x4 projTransposed = Matrix4x4.Transpose(ProjectionMatrix); + data[32] = projTransposed.M11; data[33] = projTransposed.M12; data[34] = projTransposed.M13; data[35] = projTransposed.M14; + data[36] = projTransposed.M21; data[37] = projTransposed.M22; data[38] = projTransposed.M23; data[39] = projTransposed.M24; + data[40] = projTransposed.M31; data[41] = projTransposed.M32; data[42] = projTransposed.M33; data[43] = projTransposed.M34; + data[44] = projTransposed.M41; data[45] = projTransposed.M42; data[46] = projTransposed.M43; data[47] = projTransposed.M44; + + // Copy color + data[48] = color.X; + data[49] = color.Y; + data[50] = color.Z; + data[51] = color.W; + + DxDeviceContext.Unmap(ConstantBuffer, 0); + + // Bind constant buffer to vertex shader + ID3D11Buffer* constantBuffers = ConstantBuffer; + DxDeviceContext.VSSetConstantBuffers(0, 1, &constantBuffers); + } + public void CleanUp() { + if (isCleanedUp) return; // Prevent multiple cleanup calls + isCleanedUp = true; + unsafe { if (DxDevice.Handle is not null) { VertexShader?.CleanUp(); PixelShader?.CleanUp(); + WireframeVertexShader?.CleanUp(); + WireframePixelShader?.CleanUp(); + + // Clean up 3D resources + DepthStencilBuffer.Dispose(); + DepthStencilView.Dispose(); + DepthStencilState.Dispose(); + SolidRasterizerState.Dispose(); + WireframeRasterizerState.Dispose(); + ConstantBuffer.Dispose(); + DxDevice.Dispose(); DxDeviceContext.Dispose(); DxSwapChain.Dispose(); @@ -285,21 +513,31 @@ public void CleanUp() { VertexShader = null; PixelShader = null; + WireframeVertexShader = null; + WireframePixelShader = null; DxDevice = default; DxDeviceContext = default; DxSwapChain = default; + DepthStencilBuffer = default; + DepthStencilView = default; + DepthStencilState = default; + SolidRasterizerState = default; + WireframeRasterizerState = default; + ConstantBuffer = 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 +567,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 +606,51 @@ 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; + } + + // Update projection matrix for new aspect ratio + UpdateProjectionMatrix(); } public bool HasFieldUpdated(FieldManager field) { @@ -440,31 +721,23 @@ 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); + // Set render targets with depth buffer + DxDeviceContext.OMSetRenderTargets(1, in renderTargetView.Handle, DepthStencilView); - ImGuiController!.BeginFrame((float) delta); + // Set depth stencil state + DxDeviceContext.OMSetDepthStencilState(DepthStencilState, 1); - #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 - - // logic - //FieldListWindow(); - //RendererListWindow(); - - // 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); @@ -472,4 +745,98 @@ private unsafe void OnRender(double delta) { renderTargetView.Dispose(); framebuffer.Dispose(); } + + // Camera control methods + public void SetCameraPosition(Vector3 position) { + CameraPosition = position; + UpdateViewMatrix(); + } + + public void SetCameraTarget(Vector3 target) { + CameraTarget = target; + UpdateViewMatrix(); + } + + public void RotateCamera(float yawDelta, float pitchDelta) { + // Rotate camera around target + Vector3 direction = CameraPosition - 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); + + SetCameraPosition(CameraTarget + new Vector3(x, y, z)); + } + + public void SetCameraOrientationForMapData() { + // Set camera orientation that works well with the transformed map coordinate system + // Based on the working camera position you provided, set up a similar view + Vector3 offset = new Vector3(0, -5, 5); // Look up from below and in front (inverted from typical) + SetCameraPosition(CameraTarget + offset); + } + + public void FlipCameraUpVector() { + // Flip the camera's up vector to fix upside-down view + CameraUp = -CameraUp; + UpdateViewMatrix(); + } + + public void MoveCameraRelative(Vector3 offset) { + CameraPosition += offset; + CameraTarget += offset; + UpdateViewMatrix(); + + // Unlock camera follow when manually moving + if (IsFollowingPlayer && offset.LengthSquared() > 0.01f) { + UnlockCameraFollow(); + } + } + + public void StartFollowingPlayer(long playerId) { + IsFollowingPlayer = true; + FollowedPlayerId = playerId; + HasManuallyStopped = false; // Reset manual stop flag when starting to follow + } + + public void StopFollowingPlayer() { + IsFollowingPlayer = false; + FollowedPlayerId = null; + HasManuallyStopped = true; // Remember that user manually stopped + } + + private void UnlockCameraFollow() { + if (IsFollowingPlayer) { + IsFollowingPlayer = false; + Logger.Information("Camera follow unlocked - manual movement detected"); + } + } + + public void ToggleWireframeMode() { + WireframeMode = !WireframeMode; + Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); + } + + 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 + } } diff --git a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs index e5c9751ac..e19422bb2 100644 --- a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs +++ b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs @@ -18,12 +18,12 @@ public enum ImGuiWindowType { 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; + 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 +60,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"); diff --git a/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs b/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs index 56e6c999a..7c26b4d24 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; @@ -44,8 +45,8 @@ public unsafe void Load(string path) { 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), diff --git a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj index 075d80051..3f815153a 100644 --- a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj +++ b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj @@ -37,4 +37,9 @@ + + + + + diff --git a/Maple2.Server.DebugGame/Program.cs b/Maple2.Server.DebugGame/Program.cs index a4a8148aa..7b2a03249 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; @@ -132,11 +131,11 @@ // ChatCommand Handlers autofac.RegisterType(); - autofac.RegisterAssemblyTypes(typeof(CommandRouter).Assembly) + autofac.RegisterAssemblyTypes(Assembly.LoadFrom("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/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; +} From 02a7e0964445d74115c5bafdc8d11b460dd0c59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 25 Jul 2025 23:37:15 -0300 Subject: [PATCH 02/25] suggestions --- .../Graphics/DebugFieldRenderer.cs | 226 +++++++++--------- 1 file changed, 109 insertions(+), 117 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 6f0b2f95c..ca772a060 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -19,16 +19,6 @@ public class DebugFieldRenderer : IFieldRenderer { // Coordinate system transformation - flip Y/Z axes but keep original scale private static readonly Matrix4x4 MapRotation = Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)); - // Transform position - flip Y/Z axes but keep original scale - private static Vector3 TransformMapPosition(Vector3 mapPosition) { - return Vector3.Transform(mapPosition, MapRotation); - } - - // Transform size - no rotation, keep original scale - private static Vector3 TransformMapSize(Vector3 mapSize) { - return mapSize; // Keep original size without scaling - } - public bool IsActive { get { activeMutex.WaitOne(); @@ -43,14 +33,20 @@ public bool IsActive { private IActor? selectedActor; // 3D visualization settings - private bool showBoxColliders = true; // Box colliders enabled by default (main map blocks) - private bool showMeshColliders = false; // Mesh colliders disabled by default - private bool showSpawnPoints = false; // Spawn points disabled by default - private bool showVibrateObjects = false; // Vibrate objects disabled by default - private bool showActors = true; // Actors enabled by default + private bool showBoxColliders = true; // Box colliders enabled by default (main map blocks) + private bool showMeshColliders; // Mesh colliders disabled by default + private bool showSpawnPoints; // Spawn points disabled by default + private bool showVibrateObjects; // Vibrate objects disabled by default + private bool showActors = true; // Actors enabled by default // Entity caching for performance - private FieldEntity[]? cachedStaticEntities = null; // Cache static entities (spawn points, triggers, etc.) + 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 static readonly Vector3 CameraFollowOffset = new Vector3(-900, -1200, 2000); public DebugFieldRenderer(DebugGraphicsContext context, FieldManager field) { Context = context; @@ -460,8 +456,8 @@ public void AttachWindow(DebugFieldWindow window) { // Auto-follow first player when field window is opened // But only if user hasn't manually stopped following - if (!Context.IsFollowingPlayer && !Context.HasManuallyStopped) { - var firstPlayer = Field.Players.Values.FirstOrDefault(); + if (Context is { IsFollowingPlayer: false, HasManuallyStopped: false }) { + FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); } @@ -572,10 +568,10 @@ private void RenderMeshColliders() { // Convert Euler angles (degrees) to quaternion Vector3 rotationRadians = meshCollider.Rotation * (MathF.PI / 180.0f); - Quaternion rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); // Apply coordinate system transformation (Y↔Z flip) - Quaternion coordinateTransform = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)(-Math.PI / 2)); + var coordinateTransform = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)); rotation = coordinateTransform * rotation; Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * @@ -593,7 +589,7 @@ private void RenderSpawnPoints() { foreach (FieldSpawnTile spawnPoint in allSpawnPoints) { // Convert Euler angles (degrees) to quaternion Vector3 rotationRadians = spawnPoint.Rotation * (MathF.PI / 180.0f); - Quaternion rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 50.0f) * Matrix4x4.CreateFromQuaternion(rotation) * @@ -608,7 +604,7 @@ private void RenderVibrateObjects() { foreach (FieldVibrateEntity vibrateEntity in Field.AccelerationStructure!.VibrateEntities) { // Convert Euler angles (degrees) to quaternion Vector3 rotationRadians = vibrateEntity.Rotation * (MathF.PI / 180.0f); - Quaternion rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 100.0f) * Matrix4x4.CreateFromQuaternion(rotation) * @@ -620,15 +616,14 @@ private void RenderVibrateObjects() { } private void RenderActors() { - // Agent parameters from Navigation.cs: radius=0.3f (30cm), height=1.4f (140cm) - const float agentRadius = 0.3f * 100.0f; // Convert to game units (30cm) - const float agentHeight = 1.4f * 100.0f; // Convert to game units (140cm) + const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; + const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; // Render players using cylinders (like Recast agents) - Cyan color Context.SetColor(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) - Matrix4x4 rotation = Matrix4x4.CreateRotationX((float)(Math.PI / 2)); + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * rotation * Matrix4x4.CreateTranslation(player.Position); @@ -647,7 +642,7 @@ private void RenderActors() { } // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) - Matrix4x4 rotation = Matrix4x4.CreateRotationX((float)(Math.PI / 2)); + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * rotation * Matrix4x4.CreateTranslation(npc.Position); @@ -671,7 +666,7 @@ private void RenderActors() { } // Rotate cylinder 90 degrees around X axis to make it stand upright (Y axis becomes Z axis) - Matrix4x4 rotation = Matrix4x4.CreateRotationX((float)(Math.PI / 2)); + var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * rotation * Matrix4x4.CreateTranslation(mob.Position); @@ -774,13 +769,13 @@ private void RenderEntityDetailsWindow() { } private void SetFieldOverviewCamera() { - var allColliders = GetAllEntities().OfType().ToList(); + 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 (var collider in allColliders) { + 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); } @@ -789,9 +784,8 @@ private void SetFieldOverviewCamera() { Vector3 centerMap = (min + max) * 0.5f; Vector3 sizeMap = max - min; - Vector3 center = TransformMapPosition(centerMap); - Vector3 size = TransformMapSize(sizeMap); - float distance = Math.Max(size.X, Math.Max(size.Y, size.Z)) * 1.5f; + Vector3 center = Vector3.Transform(centerMap, MapRotation); + float distance = Math.Max(sizeMap.X, Math.Max(sizeMap.Y, sizeMap.Z)) * 1.5f; Context.SetCameraTarget(center); Context.SetCameraPosition(center + new Vector3(0, -distance * 0.7f, distance * 0.7f)); @@ -817,17 +811,16 @@ private void UpdateCameraFollow() { // Update camera to follow player position Vector3 playerPosition = followedPlayer.Position; - Vector3 offset = new Vector3(-900, -1200, 2000); // Much further behind and above the player for better overview Context.SetCameraTarget(playerPosition); - Context.SetCameraPosition(playerPosition + offset); + Context.SetCameraPosition(playerPosition + CameraFollowOffset); } private Vector2D GetFieldWindowSize() { // Get the size from the first active field window, or use default activeMutex.WaitOne(); try { - var firstWindow = activeWindows.FirstOrDefault(); + DebugFieldWindow? firstWindow = activeWindows.FirstOrDefault(); if (firstWindow?.DebuggerWindow != null) { return firstWindow.DebuggerWindow.FramebufferSize; } @@ -839,7 +832,9 @@ private Vector2D GetFieldWindowSize() { return DebugGraphicsContext.DefaultFieldWindowSize; } - private Vector2? WorldToScreen(Vector3 worldPos) { + private bool TryWorldToScreen(Vector3 worldPos, out Vector2 screenPos) { + screenPos = Vector2.Zero; + // Transform world position to screen coordinates Matrix4x4 viewMatrix = Context.ViewMatrix; Matrix4x4 projMatrix = Context.ProjectionMatrix; @@ -849,16 +844,16 @@ private Vector2D GetFieldWindowSize() { Vector4 clipPos = Vector4.Transform(new Vector4(worldPos, 1.0f), viewProjMatrix); // Check if behind camera - if (clipPos.W <= 0) return null; + 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 null; + 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) - var windowSize = GetFieldWindowSize(); + Vector2D windowSize = GetFieldWindowSize(); // Convert NDC to screen coordinates // NDC: (-1, -1) = bottom-left, (1, 1) = top-right @@ -866,99 +861,96 @@ private Vector2D GetFieldWindowSize() { 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 - return new Vector2(screenX, screenY); + screenPos = new Vector2(screenX, screenY); + return true; } private void RenderActorTextLabels() { - var drawList = ImGui.GetBackgroundDrawList(); + ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); // Render player names using ImGui foreach ((int _, FieldPlayer player) in Field.Players) { - Vector3 textPos = player.Position + new Vector3(0, 0, 200); // Above the cylinder (higher) - Vector2? screenPos = WorldToScreen(textPos); - if (screenPos.HasValue) { - string playerName = player.Value.Character.Name; - Vector2 textSize = ImGui.CalcTextSize(playerName); - Vector4 textColor = new Vector4(0, 1, 1, 1); // Cyan - - // Draw semi-transparent background - Vector2 bgMin = screenPos.Value - new Vector2(4, 2); - Vector2 bgMax = screenPos.Value + textSize + new Vector2(4, 2); - drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); // Dark background - drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); // Border - - // Draw text on top - drawList.AddText(screenPos.Value, ImGui.ColorConvertFloat4ToU32(textColor), playerName); - } + 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; + Vector2 textSize = ImGui.CalcTextSize(playerName); + var textColor = new Vector4(0, 1, 1, 1); // Cyan + + // Draw semi-transparent background + Vector2 bgMin = screenPos - new Vector2(4, 2); + Vector2 bgMax = screenPos + textSize + new Vector2(4, 2); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); // Dark background + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); // Border + + // Draw text on top + drawList.AddText(screenPos, ImGui.ColorConvertFloat4ToU32(textColor), playerName); } // Render NPC names ImGui foreach ((int _, FieldNpc npc) in Field.Npcs) { - Vector3 textPos = npc.Position + new Vector3(0, 0, 200); // Above the cylinder (higher) - Vector2? screenPos = WorldToScreen(textPos); - if (screenPos.HasValue) { - string npcName = npc.Value.Metadata.Name ?? $"NPC {npc.Value.Metadata.Id}"; - - Vector4 textColor = new Vector4(0, 1, 0, 1); - uint color = ImGui.ColorConvertFloat4ToU32(textColor); - - // Draw background for NPC name - Vector2 nameSize = ImGui.CalcTextSize(npcName); - Vector2 nameBgMin = screenPos.Value - new Vector2(4, 2); - Vector2 nameBgMax = screenPos.Value + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - - drawList.AddText(screenPos.Value, color, npcName); - } + 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}"; + + var textColor = new Vector4(0, 1, 0, 1); + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + // Draw background for NPC name + Vector2 nameSize = ImGui.CalcTextSize(npcName); + Vector2 nameBgMin = screenPos - new Vector2(4, 2); + Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + + drawList.AddText(screenPos, color, npcName); } // Render Mob names and HP using ImGui foreach ((int _, FieldNpc mob) in Field.Mobs) { - Vector3 textPos = mob.Position + new Vector3(0, 0, 200); // Above the cylinder (higher) - Vector2? screenPos = WorldToScreen(textPos); - if (screenPos.HasValue) { - string mobName = mob.Value.Metadata.Name ?? $"Mob {mob.Value.Metadata.Id}"; - - // 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; - - // Choose color based on status - Vector4 textColor; - 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 - } + Vector3 textPos = mob.Position + new Vector3(0, 0, 200); // Above the cylinder + if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; - uint color = ImGui.ColorConvertFloat4ToU32(textColor); - - // Draw background for mob name - Vector2 nameSize = ImGui.CalcTextSize(mobName); - Vector2 nameBgMin = screenPos.Value - new Vector2(4, 2); - Vector2 nameBgMax = screenPos.Value + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - - drawList.AddText(screenPos.Value, color, mobName); - - // Position HP text below the name - Vector3 hpTextPos = mob.Position + new Vector3(0, 0, 180); // Slightly below name - Vector2? hpScreenPos = WorldToScreen(hpTextPos); - if (hpScreenPos.HasValue) { - string hpText = $"HP: {hpPercent:F1}%"; - Vector2 hpSize = ImGui.CalcTextSize(hpText); - Vector2 hpBgMin = hpScreenPos.Value - new Vector2(4, 2); - Vector2 hpBgMax = hpScreenPos.Value + hpSize + new Vector2(4, 2); - drawList.AddRectFilled(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - drawList.AddText(hpScreenPos.Value, color, hpText); - } + string mobName = mob.Value.Metadata.Name ?? $"Mob {mob.Value.Metadata.Id}"; + + // 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; + + // Choose color based on status + Vector4 textColor; + 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 } + + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + // Draw background for mob name + Vector2 nameSize = ImGui.CalcTextSize(mobName); + Vector2 nameBgMin = screenPos - new Vector2(4, 2); + Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + + drawList.AddText(screenPos, color, mobName); + + // Position HP text below the name + Vector3 hpTextPos = mob.Position + new Vector3(0, 0, 180); // Slightly below name + if (!TryWorldToScreen(hpTextPos, out Vector2 hpScreenPos)) continue; + + string hpText = $"HP: {hpPercent:F1}%"; + Vector2 hpSize = ImGui.CalcTextSize(hpText); + Vector2 hpBgMin = hpScreenPos - new Vector2(4, 2); + Vector2 hpBgMax = hpScreenPos + hpSize + new Vector2(4, 2); + drawList.AddRectFilled(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + drawList.AddText(hpScreenPos, color, hpText); } } From 21642aa7ff1bedbe2c68e41ca833593c22f1bb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 25 Jul 2025 23:45:27 -0300 Subject: [PATCH 03/25] fixes --- .../Graphics/Assets/CoreModels.cs | 12 +++++------ .../Maple2.Server.DebugGame.csproj | 20 +++++++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs index bb002e43f..d0495adc5 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs @@ -125,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); @@ -160,9 +160,9 @@ private Mesh CreateQuad() { PrimitiveCount = 2, }; - List vertices = new(); - List wireIndices = new(); - List solidIndices = new(); + List vertices = []; + List wireIndices = []; + List solidIndices = []; // Create vertices for bottom circle (y = 0) for (int i = 0; i < segments; i++) { diff --git a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj index 3f815153a..556926ff5 100644 --- a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj +++ b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj @@ -38,8 +38,24 @@ - - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + From ca61b2873603317648130c88acda8c47f053b6b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 26 Jul 2025 00:02:34 -0300 Subject: [PATCH 04/25] merge files from mettaur --- .../Extensions/XmlExtensions.cs | 18 + .../Graphics/Assets/Asset.cs | 5 + .../Graphics/Assets/AssetManager.cs | 13 + .../Graphics/Assets/ShaderPipelines.cs | 364 ++++++++++++++++++ .../Graphics/Data/InstanceBuffer.cs | 8 + .../Graphics/Data/SceneViewBuffer.cs | 8 + .../Graphics/DebugGraphicsContext.cs | 2 +- .../Graphics/Enum/ShaderStageFlags.cs | 9 + .../Graphics/ImGuiController.cs | 2 +- .../Graphics/Resources/ConstantBuffer.cs | 139 +++++++ .../Graphics/Resources/Mesh.cs | 2 +- .../Graphics/Resources/Shader.cs | 179 ++++++++- .../Graphics/Resources/ShaderPipeline.cs | 22 ++ .../Graphics/Resources/Texture.cs | 6 +- .../Graphics/Scene/FreeCameraController.cs | 81 ++++ .../Graphics/Scene/SceneState.cs | 120 ++++++ .../RenderPipeline/DebugScene.xml | 10 + .../RenderPipeline/Screen.xml | 10 + 18 files changed, 982 insertions(+), 16 deletions(-) create mode 100644 Maple2.Server.DebugGame/Extensions/XmlExtensions.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Assets/Asset.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Assets/AssetManager.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Data/SceneViewBuffer.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Enum/ShaderStageFlags.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Resources/ConstantBuffer.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Resources/ShaderPipeline.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs create mode 100644 Maple2.Server.DebugGame/RenderPipeline/DebugScene.xml create mode 100644 Maple2.Server.DebugGame/RenderPipeline/Screen.xml 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..77458f095 --- /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/ShaderPipelines.cs b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs new file mode 100644 index 000000000..4491af421 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs @@ -0,0 +1,364 @@ +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, element.Name); + + return true; + } + + if (element.Name != expected) { + shaderErrors = true; + + DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{1}', expected '{2}'", currentDocument, 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); + + foreach (FileInfo file in directory.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}}]: {e.Message}", currentDocument, stage); + } + } + + 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..9eedf31fd --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs @@ -0,0 +1,8 @@ +using System.Numerics; + +namespace Maple2.Server.DebugGame.Graphics.Data; + +public struct InstanceBuffer { + public Matrix4x4 Transformation; + public Matrix4x4 InverseTransformation; +} 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/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 350040512..ed8d8a7ed 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -20,7 +20,7 @@ public class DebugGraphicsContext : IGraphicsContext { 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 = []; 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 e19422bb2..19d1cc818 100644 --- a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs +++ b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs @@ -111,7 +111,7 @@ public void CleanUp() { ImGuiNative.igImGui_ImplDX11_Shutdown(); ImGui.DestroyContext(ImGuiContext); - ImGuiContext = default; + ImGuiContext = 0; Input.ConnectionChanged -= OnConnectionChanged; 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..05813a5bd 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs @@ -4,34 +4,181 @@ 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; + } + + 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 = []; + } - public Shader() { + 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 +186,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 +236,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 +260,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 +281,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 +359,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 +386,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 7c26b4d24..8af3c1f94 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs @@ -53,7 +53,7 @@ public unsafe void Load(string path) { 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, @@ -75,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, @@ -98,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/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs new file mode 100644 index 000000000..4fb8a7c3d --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -0,0 +1,81 @@ +using Silk.NET.Input; +using System.Numerics; + +namespace Maple2.Server.DebugGame.Graphics.Scene; + +public class FreeCameraController { + const float MAX_PITCH = (float.Pi / 180) * 89; + + // public InputState? InputState; // wtf is input state? + public dynamic? InputState; + public Camera? Camera; + + 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; + } + + public void Update(float delta) { + if (InputState is null || Camera is null || !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; + } + + Camera.Transform.Position += delta * FlySpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); + + if (!InputState.MouseRight.IsDown) { + return; + } + + float currentPitch = -float.Asin(Camera.Transform.FrontAxis.Z); + float currentYaw = -float.Atan2(-Camera.Transform.FrontAxis.X, -Camera.Transform.FrontAxis.Y); + + Vector3 mouseDelta = InputState.MousePosition.Delta; + + 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; + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs b/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs new file mode 100644 index 000000000..c33e02929 --- /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 Type*[] GetPointerArray(List> list) where Type : unmanaged, IComVtbl { + Type*[] array = new Type*[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/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 @@ + + + + + + + + + + From cacb9a3dff8775888a8462719a456f3dca18ef84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 26 Jul 2025 00:04:22 -0300 Subject: [PATCH 05/25] format --- .../Graphics/Assets/Asset.cs | 2 +- .../Graphics/Assets/CoreModels.cs | 40 +++++++++---------- .../Graphics/DebugFieldWindow.cs | 2 +- .../Graphics/DebugGraphicsContext.cs | 30 +++++++------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs b/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs index 77458f095..510e77f6f 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/Asset.cs @@ -1,5 +1,5 @@ namespace Maple2.Server.DebugGame.Graphics.Assets; public class Asset { - + } diff --git a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs index d0495adc5..77f9bf05a 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs @@ -166,7 +166,7 @@ private Mesh CreateQuad() { // Create vertices for bottom circle (y = 0) for (int i = 0; i < segments; i++) { - float angle = (float)(2 * Math.PI * i / segments); + 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))); @@ -174,7 +174,7 @@ private Mesh CreateQuad() { // Create vertices for top circle (y = height) for (int i = 0; i < segments; i++) { - float angle = (float)(2 * Math.PI * i / segments); + 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))); @@ -190,35 +190,35 @@ private Mesh CreateQuad() { // Create wire frame indices // Bottom circle for (int i = 0; i < segments; i++) { - wireIndices.Add((uint)i); - wireIndices.Add((uint)((i + 1) % segments)); + 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)); + 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)); + 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); + 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)); + solidIndices.Add((uint) topCenter); + solidIndices.Add((uint) (segments + i)); + solidIndices.Add((uint) (segments + (i + 1) % segments)); } // Side triangles @@ -226,14 +226,14 @@ private Mesh CreateQuad() { int next = (i + 1) % segments; // First triangle - solidIndices.Add((uint)i); - solidIndices.Add((uint)(segments + i)); - solidIndices.Add((uint)next); + 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)); + solidIndices.Add((uint) next); + solidIndices.Add((uint) (segments + i)); + solidIndices.Add((uint) (segments + next)); } // Create wire mesh diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index 652f6d187..ce722ccc9 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -150,7 +150,7 @@ public void OnUpdate(double delta) { } // Handle camera input for this field window - HandleCameraInput((float)delta); + HandleCameraInput((float) delta); } public unsafe void OnRender(double delta) { diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index ed8d8a7ed..0f7c02215 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -325,20 +325,20 @@ private unsafe void Create3DRenderingResources() { // Create depth stencil buffer var depthBufferDesc = new Texture2DDesc { - Width = (uint)windowSize.X, - Height = (uint)windowSize.Y, + 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, + BindFlags = (uint) BindFlag.DepthStencil, CPUAccessFlags = 0, MiscFlags = 0, }; ID3D11Texture2D* depthStencilBuffer = null; - SilkMarshal.ThrowHResult(DxDevice.CreateTexture2D(&depthBufferDesc, (SubresourceData*)null, &depthStencilBuffer)); + SilkMarshal.ThrowHResult(DxDevice.CreateTexture2D(&depthBufferDesc, (SubresourceData*) null, &depthStencilBuffer)); DepthStencilBuffer = depthStencilBuffer; // Create depth stencil view @@ -350,7 +350,7 @@ private unsafe void Create3DRenderingResources() { depthStencilViewDesc.Anonymous.Texture2D.MipSlice = 0; ID3D11DepthStencilView* depthStencilView = null; - SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*)DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); + SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*) DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); DepthStencilView = depthStencilView; // Create depth stencil state @@ -395,19 +395,19 @@ private unsafe void Create3DRenderingResources() { var constantBufferDesc = new BufferDesc { ByteWidth = 256, // 3 * 64 bytes for matrices + 16 bytes for color (rounded up to 256 for alignment) Usage = Usage.Dynamic, - BindFlags = (uint)BindFlag.ConstantBuffer, - CPUAccessFlags = (uint)CpuAccessFlag.Write, + BindFlags = (uint) BindFlag.ConstantBuffer, + CPUAccessFlags = (uint) CpuAccessFlag.Write, MiscFlags = 0, }; ID3D11Buffer* constantBuffer = null; - SilkMarshal.ThrowHResult(DxDevice.CreateBuffer(&constantBufferDesc, (SubresourceData*)null, &constantBuffer)); + SilkMarshal.ThrowHResult(DxDevice.CreateBuffer(&constantBufferDesc, (SubresourceData*) null, &constantBuffer)); ConstantBuffer = constantBuffer; } public void UpdateProjectionMatrix() { Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - float aspectRatio = (float)windowSize.X / windowSize.Y; + float aspectRatio = (float) windowSize.X / windowSize.Y; ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( MathF.PI / 4.0f, // 45 degree field of view aspectRatio, @@ -451,7 +451,7 @@ public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { SilkMarshal.ThrowHResult(DxDeviceContext.Map(ConstantBuffer, 0, Map.WriteDiscard, 0, &mappedResource)); // Write matrices and color to constant buffer (world, view, projection, color) - float* data = (float*)mappedResource.PData; + float* data = (float*) mappedResource.PData; // Copy world matrix (transposed) Matrix4x4 worldTransposed = Matrix4x4.Transpose(worldMatrix); @@ -621,20 +621,20 @@ private unsafe void OnFramebufferResize(Vector2D newSize) { // Recreate depth buffer with new size var depthBufferDesc = new Texture2DDesc { - Width = (uint)newSize.X, - Height = (uint)newSize.Y, + 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, + BindFlags = (uint) BindFlag.DepthStencil, CPUAccessFlags = 0, MiscFlags = 0, }; ID3D11Texture2D* depthStencilBuffer = null; - SilkMarshal.ThrowHResult(DxDevice.CreateTexture2D(&depthBufferDesc, (SubresourceData*)null, &depthStencilBuffer)); + SilkMarshal.ThrowHResult(DxDevice.CreateTexture2D(&depthBufferDesc, (SubresourceData*) null, &depthStencilBuffer)); DepthStencilBuffer = depthStencilBuffer; var depthStencilViewDesc = new DepthStencilViewDesc { @@ -645,7 +645,7 @@ private unsafe void OnFramebufferResize(Vector2D newSize) { depthStencilViewDesc.Anonymous.Texture2D.MipSlice = 0; ID3D11DepthStencilView* depthStencilView = null; - SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*)DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); + SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*) DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); DepthStencilView = depthStencilView; } From 72b581b08e8ed1921880979c69253750c397ec97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 26 Jul 2025 00:14:19 -0300 Subject: [PATCH 06/25] . --- .../Graphics/ImGuiController.cs | 33 +++++++++++++++++++ .../Graphics/Scene/FreeCameraController.cs | 3 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs index 19d1cc818..80f3e62e9 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,6 +16,38 @@ 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(); diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 4fb8a7c3d..8b6a3fa4b 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -6,8 +6,7 @@ namespace Maple2.Server.DebugGame.Graphics.Scene; public class FreeCameraController { const float MAX_PITCH = (float.Pi / 180) * 89; - // public InputState? InputState; // wtf is input state? - public dynamic? InputState; + public InputState? InputState; public Camera? Camera; private const Key MoveForward = Key.W; From 6ea637de8e398f10fca2f3eb46158d65bb5082ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 26 Jul 2025 22:23:22 -0300 Subject: [PATCH 07/25] fixes --- .../Graphics/DebugGraphicsContext.cs | 89 +++++++++++++------ .../Maple2.Server.DebugGame.csproj | 11 +-- Maple2.Server.DebugGame/root_path.txt | 1 - 3 files changed, 64 insertions(+), 37 deletions(-) delete mode 100644 Maple2.Server.DebugGame/root_path.txt diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 0f7c02215..da148d5c6 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -10,6 +10,8 @@ using Silk.NET.Maths; using Silk.NET.Windowing; using Maple2.Server.DebugGame.Graphics.Assets; +using Maple2.Server.DebugGame.Graphics.Scene; +using Maple2.Tools; using Maple2.Tools.Extensions; using System.Numerics; @@ -54,7 +56,6 @@ public class DebugGraphicsContext : IGraphicsContext { private Texture? sampleTexture; private ImGuiController? ImGuiController { get; set; } - private string resourceRootPath = ""; // 3D rendering state public Matrix4x4 ViewMatrix { get; set; } = Matrix4x4.Identity; @@ -224,8 +225,12 @@ private void OnLoad() { D3d11 = D3D11.GetApi(DebuggerWindow); Compiler = D3DCompiler.GetApi(); - Shader.ShaderRootPath = GetResourceRootPath("Shaders"); - Texture.TextureRootPath = GetResourceRootPath("Textures"); + // Use application base directory where files are copied during build + Shader.ShaderRootPath = Path.Combine(AppContext.BaseDirectory, "Shaders"); + Texture.TextureRootPath = Path.Combine(AppContext.BaseDirectory, "Textures"); + + // Set AssetRootPath for shader include system + ShaderPipelines.AssetRootPath = AppContext.BaseDirectory; unsafe { ComPtr device = default; @@ -286,10 +291,10 @@ private void OnLoad() { WireframePixelShader ??= new PixelShader(this); - VertexShader.Load("screenVertex.hlsl", "vs_main"); - PixelShader.Load("screenPixel.hlsl", "ps_main"); - WireframeVertexShader.Load("wireframeVertex.hlsl", "vs_main"); - WireframePixelShader.Load("wireframePixel.hlsl", "ps_main"); + VertexShader.Load(Path.Combine(Shader.ShaderRootPath, "screenVertex.hlsl"), "vs_main"); + PixelShader.Load(Path.Combine(Shader.ShaderRootPath, "screenPixel.hlsl"), "ps_main"); + WireframeVertexShader.Load(Path.Combine(Shader.ShaderRootPath, "wireframeVertex.hlsl"), "vs_main"); + WireframePixelShader.Load(Path.Combine(Shader.ShaderRootPath, "wireframePixel.hlsl"), "ps_main"); // Create 3D rendering resources Create3DRenderingResources(); @@ -299,7 +304,7 @@ private void OnLoad() { Logger.Information("Graphics context initialized"); sampleTexture = new Texture(this); - sampleTexture.Load("sample_derp_wave.png"); + sampleTexture.Load(Path.Combine(Texture.TextureRootPath, "sample_derp_wave.png")); // Initialize 3D matrices UpdateProjectionMatrix(); @@ -310,15 +315,7 @@ private void OnLoad() { ImGuiController.Initialize(DebuggerWindow); } - 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)); - } private unsafe void Create3DRenderingResources() { Vector2D windowSize = DebuggerWindow!.FramebufferSize; @@ -455,24 +452,60 @@ public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { // Copy world matrix (transposed) Matrix4x4 worldTransposed = Matrix4x4.Transpose(worldMatrix); - data[0] = worldTransposed.M11; data[1] = worldTransposed.M12; data[2] = worldTransposed.M13; data[3] = worldTransposed.M14; - data[4] = worldTransposed.M21; data[5] = worldTransposed.M22; data[6] = worldTransposed.M23; data[7] = worldTransposed.M24; - data[8] = worldTransposed.M31; data[9] = worldTransposed.M32; data[10] = worldTransposed.M33; data[11] = worldTransposed.M34; - data[12] = worldTransposed.M41; data[13] = worldTransposed.M42; data[14] = worldTransposed.M43; data[15] = worldTransposed.M44; + data[0] = worldTransposed.M11; + data[1] = worldTransposed.M12; + data[2] = worldTransposed.M13; + data[3] = worldTransposed.M14; + data[4] = worldTransposed.M21; + data[5] = worldTransposed.M22; + data[6] = worldTransposed.M23; + data[7] = worldTransposed.M24; + data[8] = worldTransposed.M31; + data[9] = worldTransposed.M32; + data[10] = worldTransposed.M33; + data[11] = worldTransposed.M34; + data[12] = worldTransposed.M41; + data[13] = worldTransposed.M42; + data[14] = worldTransposed.M43; + data[15] = worldTransposed.M44; // Copy view matrix (transposed) Matrix4x4 viewTransposed = Matrix4x4.Transpose(ViewMatrix); - data[16] = viewTransposed.M11; data[17] = viewTransposed.M12; data[18] = viewTransposed.M13; data[19] = viewTransposed.M14; - data[20] = viewTransposed.M21; data[21] = viewTransposed.M22; data[22] = viewTransposed.M23; data[23] = viewTransposed.M24; - data[24] = viewTransposed.M31; data[25] = viewTransposed.M32; data[26] = viewTransposed.M33; data[27] = viewTransposed.M34; - data[28] = viewTransposed.M41; data[29] = viewTransposed.M42; data[30] = viewTransposed.M43; data[31] = viewTransposed.M44; + data[16] = viewTransposed.M11; + data[17] = viewTransposed.M12; + data[18] = viewTransposed.M13; + data[19] = viewTransposed.M14; + data[20] = viewTransposed.M21; + data[21] = viewTransposed.M22; + data[22] = viewTransposed.M23; + data[23] = viewTransposed.M24; + data[24] = viewTransposed.M31; + data[25] = viewTransposed.M32; + data[26] = viewTransposed.M33; + data[27] = viewTransposed.M34; + data[28] = viewTransposed.M41; + data[29] = viewTransposed.M42; + data[30] = viewTransposed.M43; + data[31] = viewTransposed.M44; // Copy projection matrix (transposed) Matrix4x4 projTransposed = Matrix4x4.Transpose(ProjectionMatrix); - data[32] = projTransposed.M11; data[33] = projTransposed.M12; data[34] = projTransposed.M13; data[35] = projTransposed.M14; - data[36] = projTransposed.M21; data[37] = projTransposed.M22; data[38] = projTransposed.M23; data[39] = projTransposed.M24; - data[40] = projTransposed.M31; data[41] = projTransposed.M32; data[42] = projTransposed.M33; data[43] = projTransposed.M34; - data[44] = projTransposed.M41; data[45] = projTransposed.M42; data[46] = projTransposed.M43; data[47] = projTransposed.M44; + data[32] = projTransposed.M11; + data[33] = projTransposed.M12; + data[34] = projTransposed.M13; + data[35] = projTransposed.M14; + data[36] = projTransposed.M21; + data[37] = projTransposed.M22; + data[38] = projTransposed.M23; + data[39] = projTransposed.M24; + data[40] = projTransposed.M31; + data[41] = projTransposed.M32; + data[42] = projTransposed.M33; + data[43] = projTransposed.M34; + data[44] = projTransposed.M41; + data[45] = projTransposed.M42; + data[46] = projTransposed.M43; + data[47] = projTransposed.M44; // Copy color data[48] = color.X; diff --git a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj index 556926ff5..cb2da472d 100644 --- a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj +++ b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj @@ -8,15 +8,7 @@ True - - - - - - PreserveNewest - - @@ -56,6 +48,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Maple2.Server.DebugGame/root_path.txt b/Maple2.Server.DebugGame/root_path.txt deleted file mode 100644 index 8f12dd0a5..000000000 --- a/Maple2.Server.DebugGame/root_path.txt +++ /dev/null @@ -1 +0,0 @@ -../../.. From c03df72255aa13796f47baa307d58a206809e310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 26 Jul 2025 23:34:34 -0300 Subject: [PATCH 08/25] good --- .../Graphics/DebugFieldRenderer.cs | 67 +++-- .../Graphics/DebugFieldWindow.cs | 150 +++------- .../Graphics/DebugGraphicsContext.cs | 133 +-------- .../Graphics/Scene/FreeCameraController.cs | 268 +++++++++++++++++- 4 files changed, 351 insertions(+), 267 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index ca772a060..f609a1078 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -46,8 +46,6 @@ public bool IsActive { private const float AgentHeightMeters = 1.4f; private const float GameUnitsPerMeter = 100.0f; - private static readonly Vector3 CameraFollowOffset = new Vector3(-900, -1200, 2000); - public DebugFieldRenderer(DebugGraphicsContext context, FieldManager field) { Context = context; Field = field; @@ -66,10 +64,10 @@ public void 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 - if (Context is { IsFollowingPlayer: false, HasManuallyStopped: false }) { + if (!Context.CameraController.IsFollowingPlayer && !Context.CameraController.HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); - if (firstPlayer != null && (!hasTriedAutoFollow || Context.FollowedPlayerId != firstPlayer.Value.Character.Id)) { - Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + if (firstPlayer != null && (!hasTriedAutoFollow || Context.CameraController.FollowedPlayerId != firstPlayer.Value.Character.Id)) { + Context.CameraController.StartFollowingPlayer(firstPlayer.Value.Character.Id); hasTriedAutoFollow = true; } } @@ -456,10 +454,10 @@ public void AttachWindow(DebugFieldWindow window) { // Auto-follow first player when field window is opened // But only if user hasn't manually stopped following - if (Context is { IsFollowingPlayer: false, HasManuallyStopped: false }) { + if (!Context.CameraController.IsFollowingPlayer && !Context.CameraController.HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.CameraController.StartFollowingPlayer(firstPlayer.Value.Character.Id); } } } @@ -482,8 +480,8 @@ private void RenderField3DVisualization(double delta) { Context.UpdateProjectionMatrix(); // Enable wireframe mode for field visualization - bool originalWireframeMode = Context.WireframeMode; - Context.WireframeMode = true; + bool originalWireframeMode = Context.CameraController.WireframeMode; + Context.CameraController.WireframeMode = true; Context.SetWireframeRasterizer(); // Always use wireframe shaders for field visualization (they don't need texture samplers) @@ -494,7 +492,7 @@ private void RenderField3DVisualization(double delta) { RenderFieldEntities3D(); // Restore original wireframe mode - Context.WireframeMode = originalWireframeMode; + Context.CameraController.WireframeMode = originalWireframeMode; Context.SetSolidRasterizer(); // Also render the entity details window @@ -692,14 +690,13 @@ private void RenderVisualizationControls() { // Camera controls ImGui.Text("Camera Controls:"); if (ImGui.Button("Reset Camera")) { - // Reset quaternion rotation to default values - Context.CameraRotation = new Quaternion(-0.140f, 0.332f, 0.364f, 0.859f); - Context.UpdateViewMatrix(); + // Reset camera to default rotation for MapleStory 2 + Context.CameraController.SetDefaultRotation(); } ImGui.SameLine(); if (ImGui.Button("Toggle Wireframe")) { - Context.ToggleWireframeMode(); + Context.CameraController.ToggleWireframeMode(); } if (ImGui.Button("View Field Overview")) { @@ -710,29 +707,29 @@ private void RenderVisualizationControls() { ImGui.Separator(); ImGui.Text("Player Follow:"); - if (Context.IsFollowingPlayer) { - ImGui.Text($"Following Player ID: {Context.FollowedPlayerId}"); + if (Context.CameraController.IsFollowingPlayer) { + ImGui.Text($"Following Player ID: {Context.CameraController.FollowedPlayerId}"); if (ImGui.Button("Stop Following")) { - Context.StopFollowingPlayer(); + Context.CameraController.StopFollowingPlayer(); } } else { if (ImGui.Button("Follow First Player")) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.CameraController.StartFollowingPlayer(firstPlayer.Value.Character.Id); } } } // Camera position display - ImGui.Text($"Camera Position: {Context.CameraPosition}"); - ImGui.Text($"Camera Target: {Context.CameraTarget}"); + ImGui.Text($"Camera Position: {Context.CameraController.CameraPosition}"); + ImGui.Text($"Camera Target: {Context.CameraController.CameraTarget}"); // Camera rotation display - Quaternion q = Context.CameraRotation; + Quaternion q = Context.CameraController.CameraRotation; ImGui.Text($"Camera Rotation: X={q.X:F3}, Y={q.Y:F3}, Z={q.Z:F3}, W={q.W:F3}"); - ImGui.Text($"Wireframe Mode: {(Context.WireframeMode ? "ON" : "OFF")}"); + ImGui.Text($"Wireframe Mode: {(Context.CameraController.WireframeMode ? "ON" : "OFF")}"); } ImGui.End(); } @@ -787,33 +784,33 @@ private void SetFieldOverviewCamera() { Vector3 center = Vector3.Transform(centerMap, MapRotation); float distance = Math.Max(sizeMap.X, Math.Max(sizeMap.Y, sizeMap.Z)) * 1.5f; - Context.SetCameraTarget(center); - Context.SetCameraPosition(center + new Vector3(0, -distance * 0.7f, distance * 0.7f)); + Context.CameraController.SetCameraTarget(center); + Context.CameraController.SetCameraPosition(center + new Vector3(0, -distance * 0.7f, distance * 0.7f)); + Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } else { // Fallback if no colliders found - Context.SetCameraPosition(new Vector3(0, -1000, 1000)); - Context.SetCameraTarget(Vector3.Zero); + Context.CameraController.SetCameraPosition(new Vector3(0, -1000, 1000)); + Context.CameraController.SetCameraTarget(Vector3.Zero); + Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } } private void UpdateCameraFollow() { - if (!Context.IsFollowingPlayer || Context.FollowedPlayerId == null) { + if (!Context.CameraController.IsFollowingPlayer || Context.CameraController.FollowedPlayerId == null) { return; } // Find the player being followed - FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == Context.FollowedPlayerId); + FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == Context.CameraController.FollowedPlayerId); if (followedPlayer == null) { // Player not found, stop following - Context.StopFollowingPlayer(); + Context.CameraController.StopFollowingPlayer(); return; } - // Update camera to follow player position + // Update camera to follow player position using the camera controller Vector3 playerPosition = followedPlayer.Position; - - Context.SetCameraTarget(playerPosition); - Context.SetCameraPosition(playerPosition + CameraFollowOffset); + Context.CameraController.UpdatePlayerFollow(playerPosition); } private Vector2D GetFieldWindowSize() { @@ -836,8 +833,8 @@ private bool TryWorldToScreen(Vector3 worldPos, out Vector2 screenPos) { screenPos = Vector2.Zero; // Transform world position to screen coordinates - Matrix4x4 viewMatrix = Context.ViewMatrix; - Matrix4x4 projMatrix = Context.ProjectionMatrix; + Matrix4x4 viewMatrix = Context.CameraController.ViewMatrix; + Matrix4x4 projMatrix = Context.CameraController.ProjectionMatrix; Matrix4x4 viewProjMatrix = viewMatrix * projMatrix; // Transform to clip space diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index ce722ccc9..12f88a63e 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -23,9 +23,6 @@ public class DebugFieldWindow { private static int _windowIdCounter; - // Camera input tracking - private Vector2 lastMousePosition; - public DebugFieldWindow(DebugGraphicsContext context) { Context = context; WindowId = _windowIdCounter++; @@ -101,9 +98,7 @@ public void CleanUp() { } - private void OnClose() { - - } + private void OnClose() { } private void OnLoad() { Input = DebuggerWindow!.CreateInput(); @@ -149,8 +144,9 @@ public void OnUpdate(double delta) { DebuggerWindow!.Title = $"Maple2 - {WindowName}: {ActiveRenderer.Field.Metadata.Name} [{ActiveRenderer.Field.MapId}] ({ActiveRenderer.Field.RoomId})"; } - // Handle camera input for this field window - HandleCameraInput((float) delta); + // Update camera input state and let FreeCameraController handle input + UpdateCameraInputState(); + Context.CameraController.Update((float) delta); } public unsafe void OnRender(double delta) { @@ -208,123 +204,59 @@ public void Close() { IsClosing = true; } - private void UnloadField(DebugFieldRenderer renderer) { + private void UnloadField(DebugFieldRenderer renderer) { } - } - - private void LoadField(DebugFieldRenderer renderer) { - - } + private void LoadField(DebugFieldRenderer renderer) { } - private void HandleCameraInput(float deltaTime) { + private void UpdateCameraInputState() { if (Input == null) return; - IKeyboard keyboard = Input.Keyboards[0]; - IMouse mouse = Input.Mice[0]; - - float moveSpeed = 500.0f * deltaTime; // Units per second - float rotateSpeed = 2.0f * deltaTime; // Radians per second - - // Faster movement when holding Shift - if (keyboard.IsKeyPressed(Key.ShiftLeft) || keyboard.IsKeyPressed(Key.ShiftRight)) { - moveSpeed *= 5.0f; // 5x faster with Shift - } - - // Calculate camera directions from quaternion rotation - Vector3 forward = Vector3.Transform(Vector3.UnitX, Context.CameraRotation); // X is forward in our coordinate system - Vector3 right = Vector3.Normalize(Vector3.Cross(forward, Vector3.UnitZ)); // Use Z as world up for transformed coords - Vector3 up = Vector3.UnitZ; // Use Z as up direction for Q/E movement - - // WASD movement - Vector3 movement = Vector3.Zero; - if (keyboard.IsKeyPressed(Key.W)) movement += forward; - if (keyboard.IsKeyPressed(Key.S)) movement -= forward; - if (keyboard.IsKeyPressed(Key.A)) movement -= right; - if (keyboard.IsKeyPressed(Key.D)) movement += right; - if (keyboard.IsKeyPressed(Key.Q)) movement += up; - if (keyboard.IsKeyPressed(Key.E)) movement -= up; - - // Arrow keys for camera rotation (quaternion-based) - bool rotated = false; - Quaternion rotationDelta = Quaternion.Identity; - - if (keyboard.IsKeyPressed(Key.Left)) { - rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitZ, rotateSpeed); // Yaw left - rotated = true; - } - if (keyboard.IsKeyPressed(Key.Right)) { - rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitZ, -rotateSpeed); // Yaw right - rotated = true; - } - if (keyboard.IsKeyPressed(Key.Up)) { - rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, -rotateSpeed); // Pitch up (inverted) - rotated = true; - } - if (keyboard.IsKeyPressed(Key.Down)) { - rotationDelta *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, rotateSpeed); // Pitch down (inverted) - rotated = true; - } - - if (rotated) { - // Apply rotation delta to camera - Context.CameraRotation = rotationDelta * Context.CameraRotation; - Context.CameraRotation = Quaternion.Normalize(Context.CameraRotation); - Context.UpdateViewMatrix(); - } - - if (movement != Vector3.Zero) { - movement = Vector3.Normalize(movement) * moveSpeed; - // Move camera position directly (free camera style) - Context.CameraPosition += movement; - Context.UpdateViewMatrix(); - - // Unlock camera follow when manually moving - if (Context.IsFollowingPlayer && movement.LengthSquared() > 0.01f) { - Context.StopFollowingPlayer(); - } + // Initialize InputState if not already done + if (Context.CameraController.InputState == null) { + Context.CameraController.InputState = new InputState(); } - // Mouse look (only when right mouse button is held) - if (mouse.IsButtonPressed(MouseButton.Right)) { - Vector2 mouseDelta = mouse.Position - lastMousePosition; - - if (mouseDelta.X != 0 || mouseDelta.Y != 0) { - float sensitivity = 0.002f; // Mouse sensitivity - - // Create rotation quaternions for yaw and pitch - Quaternion yawRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, -mouseDelta.X * sensitivity); // Yaw around Z-axis - Quaternion pitchRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, mouseDelta.Y * sensitivity); // Pitch around Y-axis (inverted) + InputState inputState = Context.CameraController.InputState; + IKeyboard keyboard = Input.Keyboards[0]; + IMouse mouse = Input.Mice[0]; - // Apply rotations: first yaw (world space), then pitch (local space) - Context.CameraRotation = yawRotation * Context.CameraRotation * pitchRotation; + // Update InputFocused - camera should be active when window is focused + inputState.InputFocused = true; - // Normalize to prevent drift - Context.CameraRotation = Quaternion.Normalize(Context.CameraRotation); + // 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 + }; - Context.UpdateViewMatrix(); + 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); } } - // Mouse wheel movement (with same modifiers as WASD) - if (mouse is { ScrollWheels.Count: > 0 }) { - float scroll = mouse.ScrollWheels[0].Y; - if (scroll != 0) { - // Use same speed modifiers as WASD movement - float wheelMoveSpeed = moveSpeed * scroll; + // Update mouse button states + inputState.MouseLeft.LastInput = inputState.MouseLeft.IsDown; + inputState.MouseLeft.IsDown = mouse.IsButtonPressed(MouseButton.Left); - // Move forward/backward along camera's forward direction (like W/S) - Vector3 wheelMovement = forward * wheelMoveSpeed; + inputState.MouseMiddle.LastInput = inputState.MouseMiddle.IsDown; + inputState.MouseMiddle.IsDown = mouse.IsButtonPressed(MouseButton.Middle); - Context.CameraPosition += wheelMovement; - Context.UpdateViewMatrix(); + inputState.MouseRight.LastInput = inputState.MouseRight.IsDown; + inputState.MouseRight.IsDown = mouse.IsButtonPressed(MouseButton.Right); - // Unlock camera follow when manually moving - if (Context.IsFollowingPlayer && wheelMovement.LengthSquared() > 0.01f) { - Context.StopFollowingPlayer(); - } - } + // Update mouse position + inputState.MousePosition.LastPosition = inputState.MousePosition.Position; + inputState.MousePosition.Position = new Vector3(mouse.Position.X, mouse.Position.Y, 0); - lastMousePosition = mouse.Position; + // 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 da148d5c6..1ffc91026 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -56,17 +56,8 @@ public class DebugGraphicsContext : IGraphicsContext { private Texture? sampleTexture; private ImGuiController? ImGuiController { get; set; } - - // 3D rendering state - public Matrix4x4 ViewMatrix { get; set; } = Matrix4x4.Identity; - public Matrix4x4 ProjectionMatrix { get; set; } = Matrix4x4.Identity; - public Vector3 CameraPosition { get; set; } = new Vector3(0, -1000, 1000); // Default overview position - public Vector3 CameraTarget { get; private set; } = Vector3.Zero; - private Vector3 CameraUp { get; set; } = Vector3.UnitY; - - // Quaternion-based camera rotation (eliminates gimbal lock) - public Quaternion CameraRotation { get; set; } = new Quaternion(-0.140f, 0.332f, 0.364f, 0.859f); // Back to standard Y up - public bool WireframeMode { get; set; } = true; // Start in wireframe mode + // Camera controller for all camera functionality + public FreeCameraController CameraController { get; private set; } = new FreeCameraController(); private readonly List fieldRenderers = []; private readonly Mutex fieldRendererMutex = new(); @@ -90,10 +81,7 @@ public DebugFieldRenderer[] FieldRenderers { // Cleanup tracking private bool isCleanedUp; - // Camera follow system - public bool IsFollowingPlayer { get; private set; } - public long? FollowedPlayerId { get; private set; } - public bool HasManuallyStopped { get; private set; } + // Performance tracking public int DeltaMin { get; private set; } public int DeltaMax { get; private set; } private DateTime lastTime = DateTime.Now; @@ -306,9 +294,11 @@ private void OnLoad() { sampleTexture = new Texture(this); sampleTexture.Load(Path.Combine(Texture.TextureRootPath, "sample_derp_wave.png")); - // Initialize 3D matrices - UpdateProjectionMatrix(); - UpdateViewMatrix(); + // Initialize camera controller + CameraController.Camera = new Camera(); + Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; + CameraController.UpdateProjectionMatrix(windowSize.X, windowSize.Y); + CameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); @@ -404,26 +394,11 @@ private unsafe void Create3DRenderingResources() { public void UpdateProjectionMatrix() { Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - float aspectRatio = (float) windowSize.X / windowSize.Y; - ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( - MathF.PI / 4.0f, // 45 degree field of view - aspectRatio, - 1.0f, // near plane - 50000.0f // far plane - much larger for big fields - ); + CameraController.UpdateProjectionMatrix(windowSize.X, windowSize.Y); } public void UpdateViewMatrix() { - // Calculate forward direction from quaternion rotation - Vector3 forward = Vector3.Transform(Vector3.UnitX, CameraRotation); // X is forward in our coordinate system - - // Calculate target as position + forward direction - CameraTarget = CameraPosition + forward; - - // Calculate up vector from quaternion rotation - Vector3 up = Vector3.Transform(Vector3.UnitZ, CameraRotation); // Z is up in our coordinate system - - ViewMatrix = Matrix4x4.CreateLookAt(CameraPosition, CameraTarget, up); + CameraController.UpdateViewMatrix(); } public void UpdateConstantBuffer(Matrix4x4 worldMatrix) { @@ -470,7 +445,7 @@ public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { data[15] = worldTransposed.M44; // Copy view matrix (transposed) - Matrix4x4 viewTransposed = Matrix4x4.Transpose(ViewMatrix); + Matrix4x4 viewTransposed = Matrix4x4.Transpose(CameraController.ViewMatrix); data[16] = viewTransposed.M11; data[17] = viewTransposed.M12; data[18] = viewTransposed.M13; @@ -489,7 +464,7 @@ public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { data[31] = viewTransposed.M44; // Copy projection matrix (transposed) - Matrix4x4 projTransposed = Matrix4x4.Transpose(ProjectionMatrix); + Matrix4x4 projTransposed = Matrix4x4.Transpose(CameraController.ProjectionMatrix); data[32] = projTransposed.M11; data[33] = projTransposed.M12; data[34] = projTransposed.M13; @@ -768,7 +743,7 @@ private unsafe void OnRender(double delta) { DxDeviceContext.OMSetDepthStencilState(DepthStencilState, 1); // Set rasterizer state based on wireframe mode - DxDeviceContext.RSSetState(WireframeMode ? WireframeRasterizerState : SolidRasterizerState); + DxDeviceContext.RSSetState(CameraController.WireframeMode ? WireframeRasterizerState : SolidRasterizerState); ImGuiController!.BeginFrame((float) delta); ImGuiController!.EndFrame(); @@ -779,88 +754,6 @@ private unsafe void OnRender(double delta) { framebuffer.Dispose(); } - // Camera control methods - public void SetCameraPosition(Vector3 position) { - CameraPosition = position; - UpdateViewMatrix(); - } - - public void SetCameraTarget(Vector3 target) { - CameraTarget = target; - UpdateViewMatrix(); - } - - public void RotateCamera(float yawDelta, float pitchDelta) { - // Rotate camera around target - Vector3 direction = CameraPosition - 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); - - SetCameraPosition(CameraTarget + new Vector3(x, y, z)); - } - - public void SetCameraOrientationForMapData() { - // Set camera orientation that works well with the transformed map coordinate system - // Based on the working camera position you provided, set up a similar view - Vector3 offset = new Vector3(0, -5, 5); // Look up from below and in front (inverted from typical) - SetCameraPosition(CameraTarget + offset); - } - - public void FlipCameraUpVector() { - // Flip the camera's up vector to fix upside-down view - CameraUp = -CameraUp; - UpdateViewMatrix(); - } - - public void MoveCameraRelative(Vector3 offset) { - CameraPosition += offset; - CameraTarget += offset; - UpdateViewMatrix(); - - // Unlock camera follow when manually moving - if (IsFollowingPlayer && offset.LengthSquared() > 0.01f) { - UnlockCameraFollow(); - } - } - - public void StartFollowingPlayer(long playerId) { - IsFollowingPlayer = true; - FollowedPlayerId = playerId; - HasManuallyStopped = false; // Reset manual stop flag when starting to follow - } - - public void StopFollowingPlayer() { - IsFollowingPlayer = false; - FollowedPlayerId = null; - HasManuallyStopped = true; // Remember that user manually stopped - } - - private void UnlockCameraFollow() { - if (IsFollowingPlayer) { - IsFollowingPlayer = false; - Logger.Information("Camera follow unlocked - manual movement detected"); - } - } - - public void ToggleWireframeMode() { - WireframeMode = !WireframeMode; - Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); - } - public void SetWireframeRasterizer() { // Set wireframe rasterizer state // This would typically involve setting D3D11 rasterizer state diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 8b6a3fa4b..8959d716f 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -1,14 +1,34 @@ using Silk.NET.Input; using System.Numerics; +using Serilog; namespace Maple2.Server.DebugGame.Graphics.Scene; public class FreeCameraController { + private static readonly ILogger Logger = Log.Logger.ForContext(); + const float MAX_PITCH = (float.Pi / 180) * 89; public InputState? InputState; public Camera? Camera; + // 3D rendering matrices + public Matrix4x4 ViewMatrix { get; private set; } = Matrix4x4.Identity; + public Matrix4x4 ProjectionMatrix { get; private set; } = Matrix4x4.Identity; + + // Rendering modes + public bool WireframeMode { get; set; } = true; // Start in wireframe mode + + // Camera follow system + public bool IsFollowingPlayer { get; private set; } + public long? FollowedPlayerId { get; private set; } + public bool HasManuallyStopped { get; private set; } + + // Camera properties that delegate to Camera.Transform + public Vector3 CameraPosition => Camera?.Transform.Position ?? Vector3.Zero; + public Vector3 CameraTarget { get; private set; } = Vector3.Zero; + public Quaternion CameraRotation => Camera?.Transform.Quaternion ?? Quaternion.Identity; + private const Key MoveForward = Key.W; private const Key MoveBackward = Key.S; private const Key MoveLeft = Key.A; @@ -24,48 +44,109 @@ public float RotationSpeedDegrees { set => RotationSpeed = (float.Pi / 180.0f) * value; } + // Default camera follow offset + private static readonly Vector3 CameraFollowOffset = new Vector3(-800, -1000, 1200); + + // Default camera rotation for MapleStory 2 coordinate system + private static readonly Quaternion DefaultCameraRotation = new Quaternion(0.130f, 0.325f, 0.870f, 0.350f); + + // Field overview camera rotation + private static readonly Quaternion FieldOverviewRotation = new Quaternion(0.000f, 0.450f, 0.900f, 0.000f); + + public FreeCameraController() { + UpdateViewMatrix(); + } + + /// + /// Sets the camera to the default rotation for MapleStory 2 + /// + public void SetDefaultRotation() { + if (Camera != null) { + Camera.Transform.Quaternion = DefaultCameraRotation; + UpdateViewMatrix(); + } + } + + /// + /// Sets the camera to the field overview rotation + /// + public void SetFieldOverviewRotation() { + if (Camera != null) { + Camera.Transform.Quaternion = FieldOverviewRotation; + UpdateViewMatrix(); + } + } + public void Update(float delta) { if (InputState is null || Camera is null || !InputState.InputFocused) { return; } Vector3 moveDirection = new(); + bool hasMovementInput = false; if (InputState.GetState(MoveForward).IsDown) { moveDirection.Y += 1; + hasMovementInput = true; } if (InputState.GetState(MoveBackward).IsDown) { moveDirection.Y -= 1; + hasMovementInput = true; } if (InputState.GetState(MoveRight).IsDown) { moveDirection.X += 1; + hasMovementInput = true; } if (InputState.GetState(MoveLeft).IsDown) { moveDirection.X -= 1; + hasMovementInput = true; } if (InputState.GetState(MoveUp).IsDown) { moveDirection.Z += 1; + hasMovementInput = true; } if (InputState.GetState(MoveDown).IsDown) { moveDirection.Z -= 1; + hasMovementInput = true; + } + + // 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 * FlySpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); + if (hasMovementInput) { + Camera.Transform.Position += delta * currentSpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); + + // Disable follow when manual movement is detected + if (IsFollowingPlayer) { + StopFollowingPlayer(); + } + } if (!InputState.MouseRight.IsDown) { return; } + Vector3 mouseDelta = InputState.MousePosition.Delta; + + // Check if there's actual mouse movement + if (mouseDelta.X != 0 || mouseDelta.Y != 0) { + // Disable follow when mouse rotation is detected + if (IsFollowingPlayer) { + StopFollowingPlayer(); + } + } + float currentPitch = -float.Asin(Camera.Transform.FrontAxis.Z); float currentYaw = -float.Atan2(-Camera.Transform.FrontAxis.X, -Camera.Transform.FrontAxis.Y); - Vector3 mouseDelta = InputState.MousePosition.Delta; - currentPitch = float.Max(-MAX_PITCH, float.Min(MAX_PITCH, currentPitch + RotationSpeed * BaseRotationSpeed * mouseDelta.Y)); currentYaw -= RotationSpeed * BaseRotationSpeed * mouseDelta.X; @@ -76,5 +157,186 @@ public void Update(float delta) { Camera.Transform.Position = position; InputState.MousePosition.Lock = true; + + // Update matrices after camera transform changes + UpdateViewMatrix(); + } + + /// + /// Updates the projection matrix based on window size + /// + public void UpdateProjectionMatrix(int windowWidth, int windowHeight) { + float aspectRatio = (float) windowWidth / windowHeight; + ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( + MathF.PI / 4.0f, // 45 degree field of view + aspectRatio, + 1.0f, // near plane + 50000.0f // far plane - much larger for big fields + ); + } + + /// + /// Updates the view matrix based on current camera transform + /// + public void UpdateViewMatrix() { + if (Camera == null) return; + + // Use Camera.Transform to calculate view matrix + Vector3 position = Camera.Transform.Position; + Vector3 forward = Camera.Transform.FrontAxis; + Vector3 up = Camera.Transform.UpAxis; + + CameraTarget = position + forward; + ViewMatrix = Matrix4x4.CreateLookAt(position, CameraTarget, up); + } + + /// + /// Sets the camera position using Camera.Transform + /// + public void SetCameraPosition(Vector3 position) { + if (Camera == null) return; + Camera.Transform.Position = position; + UpdateViewMatrix(); + } + + /// + /// Sets the camera target and updates camera orientation + /// + public void SetCameraTarget(Vector3 target) { + if (Camera == null) return; + + CameraTarget = target; + Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); + + // Calculate rotation to look at target + Vector3 up = Vector3.UnitZ; // Z is up in our coordinate system + Vector3 right = Vector3.Normalize(Vector3.Cross(direction, up)); + up = Vector3.Cross(right, direction); + + Matrix4x4 lookAtMatrix = new Matrix4x4( + right.X, up.X, -direction.X, 0, + right.Y, up.Y, -direction.Y, 0, + right.Z, up.Z, -direction.Z, 0, + 0, 0, 0, 1 + ); + + Camera.Transform.Transformation = lookAtMatrix; + UpdateViewMatrix(); + } + + /// + /// Rotates the camera around the target point + /// + public void RotateCamera(float yawDelta, float pitchDelta) { + if (Camera == null) return; + + // 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); + + SetCameraPosition(CameraTarget + new Vector3(x, y, z)); + } + + /// + /// Moves the camera relative to its current position + /// + public void MoveCameraRelative(Vector3 offset) { + if (Camera == null) return; + + Camera.Transform.Position += offset; + CameraTarget += offset; + UpdateViewMatrix(); + + // Unlock camera follow when manually moving + if (IsFollowingPlayer && offset.LengthSquared() > 0.01f) { + UnlockCameraFollow(); + } + } + + /// + /// Starts following a specific player + /// + public void StartFollowingPlayer(long playerId) { + IsFollowingPlayer = true; + FollowedPlayerId = playerId; + HasManuallyStopped = false; // Reset manual stop flag when starting to follow + SetDefaultRotation(); // Set default rotation when starting to follow + Logger.Information("Started following player {PlayerId}", playerId); + } + + /// + /// Stops following the current player + /// + public void StopFollowingPlayer() { + IsFollowingPlayer = false; + FollowedPlayerId = null; + HasManuallyStopped = true; // Remember that user manually stopped + Logger.Information("Stopped following player"); + } + + /// + /// Unlocks camera follow when manual movement is detected + /// + private void UnlockCameraFollow() { + if (IsFollowingPlayer) { + IsFollowingPlayer = false; + Logger.Information("Camera follow unlocked - manual movement detected"); + } + } + + /// + /// Updates camera to follow a player at the specified position + /// + public void UpdatePlayerFollow(Vector3 playerPosition) { + if (!IsFollowingPlayer) return; + + SetCameraTarget(playerPosition); + SetCameraPosition(playerPosition + CameraFollowOffset); + SetDefaultRotation(); // Always use default rotation when following player + } + + /// + /// Toggles wireframe rendering mode + /// + public void ToggleWireframeMode() { + WireframeMode = !WireframeMode; + Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); + } + + /// + /// 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) + SetCameraPosition(CameraTarget + offset); + } + + /// + /// Flips the camera's up vector to fix upside-down view + /// + public void FlipCameraUpVector() { + if (Camera == null) return; + + // Flip the up axis in the transform + Matrix4x4 currentTransform = Camera.Transform.Transformation; + Matrix4x4 flipMatrix = Matrix4x4.CreateScale(1, 1, -1); + Camera.Transform.Transformation = currentTransform * flipMatrix; + UpdateViewMatrix(); } } From d7bea61352db4d4e7e03a757c8a8b2cd1aa59ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sun, 27 Jul 2025 00:14:26 -0300 Subject: [PATCH 09/25] portals + format + fixes --- .../Graphics/DebugFieldRenderer.cs | 147 +++++++++++++++++- .../Graphics/DebugFieldWindow.cs | 2 +- .../Graphics/DebugGraphicsContext.cs | 6 +- .../Graphics/Resources/Texture.cs | 2 +- 4 files changed, 148 insertions(+), 9 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index f609a1078..c3d0bbcb1 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -37,6 +37,7 @@ public bool IsActive { private bool showMeshColliders; // Mesh colliders disabled by default private bool showSpawnPoints; // Spawn points disabled by default private bool showVibrateObjects; // Vibrate objects disabled by default + private bool showPortals = true; // Portals enabled by default private bool showActors = true; // Actors enabled by default // Entity caching for performance @@ -524,6 +525,15 @@ private void RenderFieldEntities3D() { RenderVibrateObjects(); } + // Render portals + if (showPortals) { + RenderPortals(); + RenderPortalConnectionLines(); + + // Render portal text labels + RenderPortalTextLabels(); + } + // Render actors (players, NPCs, mobs) if (showActors) { RenderActors(); @@ -599,6 +609,9 @@ private void RenderSpawnPoints() { } private void RenderVibrateObjects() { + // Set pink color for vibrate objects + Context.SetColor(1, 0.75f, 0.8f, 1); // Pink for vibrate objects + foreach (FieldVibrateEntity vibrateEntity in Field.AccelerationStructure!.VibrateEntities) { // Convert Euler angles (degrees) to quaternion Vector3 rotationRadians = vibrateEntity.Rotation * (MathF.PI / 180.0f); @@ -608,11 +621,87 @@ private void RenderVibrateObjects() { Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(vibrateEntity.Position); - Context.UpdateConstantBuffer(worldMatrix); + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (pink) + Context.CoreModels!.WireCube.Draw(); + } + } + + private void RenderPortals() { + // Set blue color for portals + Context.SetColor(0, 0.5f, 1, 1); // Blue for portals + + foreach (FieldPortal portal in Field.GetPortals()) { + // Convert Euler angles (degrees) to quaternion + Vector3 rotationRadians = portal.Rotation * (MathF.PI / 180.0f); + var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + + // Use portal dimensions if available, otherwise use default size + Vector3 size = portal.Value.Dimension != Vector3.Zero ? portal.Value.Dimension : Vector3.One * 100.0f; + + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * + Matrix4x4.CreateFromQuaternion(rotation) * + Matrix4x4.CreateTranslation(portal.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (blue) Context.CoreModels!.WireCube.Draw(); } } + private void RenderPortalConnectionLines() { + // Set cyan color for portal connection lines + Context.SetColor(0, 1, 1, 0.7f); // Cyan with some transparency + + 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(sourcePortal.Position, targetPortal.Position); + } + } + } + } + + private void RenderLine(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; + Quaternion rotation; + + // 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) + rotation = dot > 0 ? Quaternion.Identity : Quaternion.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)); + rotation = Quaternion.CreateFromAxisAngle(axis, angle); + } + + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(10.0f, distance / 2.0f, 10.0f)) * + Matrix4x4.CreateFromQuaternion(rotation) * + Matrix4x4.CreateTranslation(center); + + Context.UpdateConstantBuffer(worldMatrix, true); + Context.CoreModels!.Cylinder.Draw(); + } + private void RenderActors() { const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; @@ -683,6 +772,7 @@ private void RenderVisualizationControls() { ImGui.Checkbox("Show Mesh Colliders", ref showMeshColliders); ImGui.Checkbox("Show Spawn Points", ref showSpawnPoints); ImGui.Checkbox("Show Vibrate Objects", ref showVibrateObjects); + ImGui.Checkbox("Show Portals", ref showPortals); ImGui.Checkbox("Show Actors", ref showActors); ImGui.Separator(); @@ -937,9 +1027,9 @@ private void RenderActorTextLabels() { drawList.AddText(screenPos, color, mobName); - // Position HP text below the name - Vector3 hpTextPos = mob.Position + new Vector3(0, 0, 180); // Slightly below name - if (!TryWorldToScreen(hpTextPos, out Vector2 hpScreenPos)) continue; + // Position HP text below the name using screen-space offset to avoid overlap when zoomed out + // Calculate HP text position in screen space (below the name) with consistent pixel spacing + Vector2 hpScreenPos = new Vector2(screenPos.X, screenPos.Y + nameSize.Y + 4); // 4 pixels below name string hpText = $"HP: {hpPercent:F1}%"; Vector2 hpSize = ImGui.CalcTextSize(hpText); @@ -951,6 +1041,55 @@ private void RenderActorTextLabels() { } } + private void RenderPortalTextLabels() { + ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); + + // Render portal information using ImGui + 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; + + // Build portal info text + string enabledText = portal.Enabled ? "Enabled" : "Disabled"; + 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}"; + + // Choose color based on enabled status + Vector4 textColor = portal.Enabled ? new Vector4(0, 0.8f, 1, 1) : new Vector4(0.6f, 0.6f, 0.6f, 1); // Blue if enabled, gray if disabled + uint color = ImGui.ColorConvertFloat4ToU32(textColor); + + // Calculate total text size for background + 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; // 8 pixels spacing between lines + + // Draw background + Vector2 bgMin = screenPos - new Vector2(4, 2); + Vector2 bgMax = screenPos + new Vector2(maxWidth + 8, totalHeight + 4); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.8f))); // Dark background + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.3f, 0.3f, 0.3f, 0.9f))); // Border + + // Draw text lines + Vector2 currentPos = screenPos; + 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); + } + } + private FieldEntity[] GetAllEntities() { // Cache static entities since they never change during field runtime if (cachedStaticEntities != null) { diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index 12f88a63e..09fb10944 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -232,7 +232,7 @@ private void UpdateCameraInputState() { }; foreach (Key key in keysToTrack) { - int keyIndex = (int)key; + 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); diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 1ffc91026..07086bd7a 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -302,11 +302,11 @@ private void OnLoad() { ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); - ImGuiController.Initialize(DebuggerWindow); + if (DebuggerWindow is not null) { + ImGuiController.Initialize(DebuggerWindow); + } } - - private unsafe void Create3DRenderingResources() { Vector2D windowSize = DebuggerWindow!.FramebufferSize; diff --git a/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs b/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs index 8af3c1f94..d5561f28a 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Texture.cs @@ -40,7 +40,7 @@ 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, }; From 32cf84426dd106c07275ce5b21015f004e90b4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 29 Jul 2025 17:43:32 -0300 Subject: [PATCH 10/25] refactor(camera): unify camera matrix handling and update projection logic --- .../Graphics/DebugFieldRenderer.cs | 8 +- .../Graphics/DebugFieldWindow.cs | 5 -- .../Graphics/DebugGraphicsContext.cs | 11 ++- .../Graphics/Scene/Camera.cs | 40 ++++++++- .../Graphics/Scene/FreeCameraController.cs | 85 +++---------------- 5 files changed, 57 insertions(+), 92 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index c3d0bbcb1..a354b7446 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -875,11 +875,11 @@ private void SetFieldOverviewCamera() { float distance = Math.Max(sizeMap.X, Math.Max(sizeMap.Y, sizeMap.Z)) * 1.5f; Context.CameraController.SetCameraTarget(center); - Context.CameraController.SetCameraPosition(center + new Vector3(0, -distance * 0.7f, distance * 0.7f)); + Context.CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } else { // Fallback if no colliders found - Context.CameraController.SetCameraPosition(new Vector3(0, -1000, 1000)); + Context.CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); Context.CameraController.SetCameraTarget(Vector3.Zero); Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } @@ -923,8 +923,8 @@ private bool TryWorldToScreen(Vector3 worldPos, out Vector2 screenPos) { screenPos = Vector2.Zero; // Transform world position to screen coordinates - Matrix4x4 viewMatrix = Context.CameraController.ViewMatrix; - Matrix4x4 projMatrix = Context.CameraController.ProjectionMatrix; + Matrix4x4 viewMatrix = Context.CameraController.Camera.ViewMatrix; + Matrix4x4 projMatrix = Context.CameraController.Camera.ProjectionMatrix; Matrix4x4 viewProjMatrix = viewMatrix * projMatrix; // Transform to clip space diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index 09fb10944..11771c4cd 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -211,11 +211,6 @@ private void LoadField(DebugFieldRenderer renderer) { } private void UpdateCameraInputState() { if (Input == null) return; - // Initialize InputState if not already done - if (Context.CameraController.InputState == null) { - Context.CameraController.InputState = new InputState(); - } - InputState inputState = Context.CameraController.InputState; IKeyboard keyboard = Input.Keyboards[0]; IMouse mouse = Input.Mice[0]; diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 07086bd7a..7b77fac15 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -295,9 +295,8 @@ private void OnLoad() { sampleTexture.Load(Path.Combine(Texture.TextureRootPath, "sample_derp_wave.png")); // Initialize camera controller - CameraController.Camera = new Camera(); Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - CameraController.UpdateProjectionMatrix(windowSize.X, windowSize.Y); + CameraController.Camera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); CameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); @@ -394,11 +393,11 @@ private unsafe void Create3DRenderingResources() { public void UpdateProjectionMatrix() { Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - CameraController.UpdateProjectionMatrix(windowSize.X, windowSize.Y); + CameraController.Camera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); } public void UpdateViewMatrix() { - CameraController.UpdateViewMatrix(); + CameraController.Camera.UpdateViewMatrix(); } public void UpdateConstantBuffer(Matrix4x4 worldMatrix) { @@ -445,7 +444,7 @@ public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { data[15] = worldTransposed.M44; // Copy view matrix (transposed) - Matrix4x4 viewTransposed = Matrix4x4.Transpose(CameraController.ViewMatrix); + Matrix4x4 viewTransposed = Matrix4x4.Transpose(CameraController.Camera.ViewMatrix); data[16] = viewTransposed.M11; data[17] = viewTransposed.M12; data[18] = viewTransposed.M13; @@ -464,7 +463,7 @@ public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { data[31] = viewTransposed.M44; // Copy projection matrix (transposed) - Matrix4x4 projTransposed = Matrix4x4.Transpose(CameraController.ProjectionMatrix); + Matrix4x4 projTransposed = Matrix4x4.Transpose(CameraController.Camera.ProjectionMatrix); data[32] = projTransposed.M11; data[33] = projTransposed.M12; data[34] = projTransposed.M13; diff --git a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs index ffd0d4f44..1e323cb07 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs @@ -5,19 +5,55 @@ namespace Maple2.Server.DebugGame.Graphics.Scene; public class Camera { public Transform Transform { get; init; } = new Transform(); - public Matrix4x4 ProjectionMatrix { get; private set; } + public Matrix4x4 ProjectionMatrix { get; private set; } = Matrix4x4.Identity; + public Matrix4x4 ViewMatrix { get; private set; } = Matrix4x4.Identity; public float AspectRatio { get; private set; } public float NearPlane { get; private set; } public float FarPlane { get; private set; } public float FieldOfView { get; private set; } + /// + /// Updates the projection matrix based on window size + /// + public void UpdateProjectionMatrix(int windowWidth, int windowHeight) { + float aspectRatio = (float) windowWidth / windowHeight; + ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( + MathF.PI / 4.0f, // 45 degree field of view + aspectRatio, + 1.0f, // near plane + 50000.0f // far plane - much larger for big fields + ); + AspectRatio = aspectRatio; + FieldOfView = MathF.PI / 4.0f; + NearPlane = 1.0f; + FarPlane = 50000.0f; + } + + /// + /// Updates the view matrix based on current camera transform + /// + public void UpdateViewMatrix() { + // Use Transform to calculate view matrix + Vector3 position = Transform.Position; + Vector3 forward = Transform.FrontAxis; + Vector3 up = Transform.UpAxis; + + ViewMatrix = Matrix4x4.CreateLookAt(position, position + forward, up); + } + public void SetProperties(float fieldOfView, float aspectRatio, float nearPlane, float farPlane) { + FieldOfView = fieldOfView; + AspectRatio = aspectRatio; + NearPlane = nearPlane; + FarPlane = farPlane; + ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlane, 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/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 8959d716f..6eaf74156 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -9,12 +9,8 @@ public class FreeCameraController { const float MAX_PITCH = (float.Pi / 180) * 89; - public InputState? InputState; - public Camera? Camera; - - // 3D rendering matrices - public Matrix4x4 ViewMatrix { get; private set; } = Matrix4x4.Identity; - public Matrix4x4 ProjectionMatrix { get; private set; } = Matrix4x4.Identity; + public InputState InputState = new(); + public Camera Camera = new(); // Rendering modes public bool WireframeMode { get; set; } = true; // Start in wireframe mode @@ -25,9 +21,9 @@ public class FreeCameraController { public bool HasManuallyStopped { get; private set; } // Camera properties that delegate to Camera.Transform - public Vector3 CameraPosition => Camera?.Transform.Position ?? Vector3.Zero; + public Vector3 CameraPosition => Camera.Transform.Position; public Vector3 CameraTarget { get; private set; } = Vector3.Zero; - public Quaternion CameraRotation => Camera?.Transform.Quaternion ?? Quaternion.Identity; + public Quaternion CameraRotation => Camera.Transform.Quaternion; private const Key MoveForward = Key.W; private const Key MoveBackward = Key.S; @@ -53,32 +49,22 @@ public float RotationSpeedDegrees { // Field overview camera rotation private static readonly Quaternion FieldOverviewRotation = new Quaternion(0.000f, 0.450f, 0.900f, 0.000f); - public FreeCameraController() { - UpdateViewMatrix(); - } - /// /// Sets the camera to the default rotation for MapleStory 2 /// public void SetDefaultRotation() { - if (Camera != null) { - Camera.Transform.Quaternion = DefaultCameraRotation; - UpdateViewMatrix(); - } + Camera.Transform.Quaternion = DefaultCameraRotation; } /// /// Sets the camera to the field overview rotation /// public void SetFieldOverviewRotation() { - if (Camera != null) { - Camera.Transform.Quaternion = FieldOverviewRotation; - UpdateViewMatrix(); - } + Camera.Transform.Quaternion = FieldOverviewRotation; } public void Update(float delta) { - if (InputState is null || Camera is null || !InputState.InputFocused) { + if (!InputState.InputFocused) { return; } @@ -157,54 +143,12 @@ public void Update(float delta) { Camera.Transform.Position = position; InputState.MousePosition.Lock = true; - - // Update matrices after camera transform changes - UpdateViewMatrix(); - } - - /// - /// Updates the projection matrix based on window size - /// - public void UpdateProjectionMatrix(int windowWidth, int windowHeight) { - float aspectRatio = (float) windowWidth / windowHeight; - ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( - MathF.PI / 4.0f, // 45 degree field of view - aspectRatio, - 1.0f, // near plane - 50000.0f // far plane - much larger for big fields - ); - } - - /// - /// Updates the view matrix based on current camera transform - /// - public void UpdateViewMatrix() { - if (Camera == null) return; - - // Use Camera.Transform to calculate view matrix - Vector3 position = Camera.Transform.Position; - Vector3 forward = Camera.Transform.FrontAxis; - Vector3 up = Camera.Transform.UpAxis; - - CameraTarget = position + forward; - ViewMatrix = Matrix4x4.CreateLookAt(position, CameraTarget, up); - } - - /// - /// Sets the camera position using Camera.Transform - /// - public void SetCameraPosition(Vector3 position) { - if (Camera == null) return; - Camera.Transform.Position = position; - UpdateViewMatrix(); } /// /// Sets the camera target and updates camera orientation /// public void SetCameraTarget(Vector3 target) { - if (Camera == null) return; - CameraTarget = target; Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); @@ -221,15 +165,12 @@ public void SetCameraTarget(Vector3 target) { ); Camera.Transform.Transformation = lookAtMatrix; - UpdateViewMatrix(); } /// /// Rotates the camera around the target point /// public void RotateCamera(float yawDelta, float pitchDelta) { - if (Camera == null) return; - // Rotate camera around target Vector3 direction = Camera.Transform.Position - CameraTarget; float distance = direction.Length(); @@ -249,18 +190,15 @@ public void RotateCamera(float yawDelta, float pitchDelta) { float y = distance * MathF.Sin(currentPitch); float z = distance * MathF.Cos(currentPitch) * MathF.Sin(currentYaw); - SetCameraPosition(CameraTarget + new Vector3(x, y, z)); + Camera.Transform.Position = CameraTarget + new Vector3(x, y, z); } /// /// Moves the camera relative to its current position /// public void MoveCameraRelative(Vector3 offset) { - if (Camera == null) return; - Camera.Transform.Position += offset; CameraTarget += offset; - UpdateViewMatrix(); // Unlock camera follow when manually moving if (IsFollowingPlayer && offset.LengthSquared() > 0.01f) { @@ -306,7 +244,7 @@ public void UpdatePlayerFollow(Vector3 playerPosition) { if (!IsFollowingPlayer) return; SetCameraTarget(playerPosition); - SetCameraPosition(playerPosition + CameraFollowOffset); + Camera.Transform.Position = playerPosition + CameraFollowOffset; SetDefaultRotation(); // Always use default rotation when following player } @@ -324,19 +262,16 @@ public void ToggleWireframeMode() { 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) - SetCameraPosition(CameraTarget + offset); + Camera.Transform.Position = CameraTarget + offset; } /// /// Flips the camera's up vector to fix upside-down view /// public void FlipCameraUpVector() { - if (Camera == null) return; - // Flip the up axis in the transform Matrix4x4 currentTransform = Camera.Transform.Transformation; Matrix4x4 flipMatrix = Matrix4x4.CreateScale(1, 1, -1); Camera.Transform.Transformation = currentTransform * flipMatrix; - UpdateViewMatrix(); } } From 629f778b074b7db26d96eb09b022742b3dd51447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 29 Jul 2025 18:28:28 -0300 Subject: [PATCH 11/25] feat(camera): add ICameraController interface and implement in FreeCameraController --- .../Graphics/DebugFieldRenderer.cs | 21 +- .../Graphics/DebugGraphicsContext.cs | 96 +++++++++- .../Graphics/Scene/FollowCameraController.cs | 180 ++++++++++++++++++ .../Graphics/Scene/FreeCameraController.cs | 107 ++++------- .../Graphics/Scene/ICameraController.cs | 120 ++++++++++++ 5 files changed, 444 insertions(+), 80 deletions(-) create mode 100644 Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index a354b7446..12feeed90 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -65,10 +65,10 @@ public void 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 - if (!Context.CameraController.IsFollowingPlayer && !Context.CameraController.HasManuallyStopped) { + if (!Context.CameraController.IsFollowingPlayer && !Context.HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null && (!hasTriedAutoFollow || Context.CameraController.FollowedPlayerId != firstPlayer.Value.Character.Id)) { - Context.CameraController.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); hasTriedAutoFollow = true; } } @@ -455,10 +455,10 @@ public void AttachWindow(DebugFieldWindow window) { // Auto-follow first player when field window is opened // But only if user hasn't manually stopped following - if (!Context.CameraController.IsFollowingPlayer && !Context.CameraController.HasManuallyStopped) { + if (!Context.CameraController.IsFollowingPlayer && !Context.HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.CameraController.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); } } } @@ -800,13 +800,13 @@ private void RenderVisualizationControls() { if (Context.CameraController.IsFollowingPlayer) { ImGui.Text($"Following Player ID: {Context.CameraController.FollowedPlayerId}"); if (ImGui.Button("Stop Following")) { - Context.CameraController.StopFollowingPlayer(); + Context.StopFollowingPlayer(); } } else { if (ImGui.Button("Follow First Player")) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.CameraController.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); } } } @@ -856,6 +856,11 @@ private void RenderEntityDetailsWindow() { } private void SetFieldOverviewCamera() { + // Stop following player when switching to field overview + if (Context.CameraController.IsFollowingPlayer) { + Context.StopFollowingPlayer(); + } + List allColliders = GetAllEntities().OfType().ToList(); if (allColliders.Count > 0) { // Calculate field bounds in map coordinates @@ -894,13 +899,13 @@ private void UpdateCameraFollow() { FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == Context.CameraController.FollowedPlayerId); if (followedPlayer == null) { // Player not found, stop following - Context.CameraController.StopFollowingPlayer(); + Context.StopFollowingPlayer(); return; } // Update camera to follow player position using the camera controller Vector3 playerPosition = followedPlayer.Position; - Context.CameraController.UpdatePlayerFollow(playerPosition); + Context.UpdatePlayerFollow(playerPosition); } private Vector2D GetFieldWindowSize() { diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 7b77fac15..decb1e279 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -56,8 +56,18 @@ public class DebugGraphicsContext : IGraphicsContext { private Texture? sampleTexture; private ImGuiController? ImGuiController { get; set; } - // Camera controller for all camera functionality - public FreeCameraController CameraController { get; private set; } = new FreeCameraController(); + // Camera controller interface for all camera functionality + public ICameraController CameraController { get; private set; } = null!; + + // Available controller implementations + private FreeCameraController freeCameraController = null!; + private FollowCameraController followCameraController = null!; + + // Shared camera instance + private readonly Camera sharedCamera = new(); + + // Global follow state (independent of active controller) + private bool hasManuallyStoppedFollowing = false; private readonly List fieldRenderers = []; private readonly Mutex fieldRendererMutex = new(); @@ -294,9 +304,15 @@ private void OnLoad() { sampleTexture = new Texture(this); sampleTexture.Load(Path.Combine(Texture.TextureRootPath, "sample_derp_wave.png")); - // Initialize camera controller + // Initialize camera controllers + freeCameraController = new FreeCameraController(sharedCamera); + followCameraController = new FollowCameraController(sharedCamera); + + // Start with free camera controller as default + CameraController = freeCameraController; + Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - CameraController.Camera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); + sharedCamera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); CameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); @@ -400,6 +416,78 @@ public void UpdateViewMatrix() { CameraController.Camera.UpdateViewMatrix(); } + /// + /// Switches to the free camera controller + /// + private void SwitchToFreeCameraController() { + if (CameraController != freeCameraController) { + Logger.Information("Switching from {OldController} to FreeCameraController", GetCurrentControllerType()); + CameraController = freeCameraController; + Logger.Information("Switched to free camera controller - IsFollowing={IsFollowing}", CameraController.IsFollowingPlayer); + } 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" + }; + } + + /// + /// Whether the user has manually stopped following (global state) + /// + public bool HasManuallyStopped => hasManuallyStoppedFollowing; + + /// + /// Starts following a player and automatically switches to follow camera controller + /// + public void StartFollowingPlayer(long playerId) { + hasManuallyStoppedFollowing = false; // Reset manual stop flag when starting to follow + SwitchToFollowCameraController(); + followCameraController.StartFollowingPlayer(playerId); + } + + /// + /// Stops following a player and automatically switches to free camera controller + /// + public void StopFollowingPlayer() { + Logger.Information("StopFollowingPlayer called - before: IsFollowing={IsFollowing}, Controller={Controller}", + CameraController.IsFollowingPlayer, GetCurrentControllerType()); + + hasManuallyStoppedFollowing = true; // Remember that user manually stopped + followCameraController.StopFollowingPlayer(); + SwitchToFreeCameraController(); + + Logger.Information("StopFollowingPlayer called - after: IsFollowing={IsFollowing}, Controller={Controller}, HasManuallyStopped={HasManuallyStopped}", + CameraController.IsFollowingPlayer, GetCurrentControllerType(), HasManuallyStopped); + } + + /// + /// Updates player follow position (only works when follow controller is active) + /// + public void UpdatePlayerFollow(Vector3 playerPosition) { + if (CameraController == followCameraController) { + followCameraController.UpdatePlayerFollow(playerPosition); + } + } + public void UpdateConstantBuffer(Matrix4x4 worldMatrix) { UpdateConstantBuffer(worldMatrix, new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Default white color } diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs new file mode 100644 index 000000000..0e8791743 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs @@ -0,0 +1,180 @@ +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(); + + // Rendering modes + public bool WireframeMode { get; set; } = true; + + // Camera follow system + public bool IsFollowingPlayer { get; private set; } + public long? FollowedPlayerId { get; private set; } + public bool HasManuallyStopped { get; private set; } + + // Default camera follow offset + private static readonly Vector3 CameraFollowOffset = new Vector3(-800, -1000, 1200); + + // Default camera rotation for MapleStory 2 coordinate system + private static readonly Quaternion DefaultCameraRotation = new Quaternion(0.130f, 0.325f, 0.870f, 0.350f); + + // Field overview camera rotation + private static readonly Quaternion FieldOverviewRotation = new Quaternion(0.000f, 0.450f, 0.900f, 0.000f); + + public Vector3 CameraTarget { get; private set; } = Vector3.Zero; + + // Camera properties that delegate to Camera.Transform + public Vector3 CameraPosition => Camera.Transform.Position; + public Quaternion CameraRotation => Camera.Transform.Quaternion; + + public FollowCameraController(Camera camera) { + Camera = camera; + } + + /// + /// Starts following a specific player + /// + public void StartFollowingPlayer(long playerId) { + IsFollowingPlayer = true; + FollowedPlayerId = playerId; + HasManuallyStopped = false; // Reset manual stop flag when starting to follow + SetDefaultRotation(); // Set default rotation when starting to follow + Logger.Information("Started following player {PlayerId}", playerId); + } + + /// + /// Stops following the current player + /// + public void StopFollowingPlayer() { + IsFollowingPlayer = false; + FollowedPlayerId = null; + HasManuallyStopped = true; // Remember that user manually stopped + Logger.Information("Stopped following player"); + } + + /// + /// Unlocks camera follow when manual movement is detected + /// + public void UnlockCameraFollow() { + if (IsFollowingPlayer) { + IsFollowingPlayer = false; + Logger.Information("Camera follow unlocked - manual movement detected"); + } + } + + /// + /// Updates camera to follow a player at the specified position + /// + public void UpdatePlayerFollow(Vector3 playerPosition) { + if (!IsFollowingPlayer) return; + + SetCameraTarget(playerPosition); + Camera.Transform.Position = playerPosition + CameraFollowOffset; + SetDefaultRotation(); // Always use default rotation when following player + } + + /// + /// Sets the camera target and updates camera orientation + /// + public void SetCameraTarget(Vector3 target) { + CameraTarget = target; + Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); + + // Calculate rotation to look at target + Vector3 up = Vector3.UnitZ; // Z is up in our coordinate system + Vector3 right = Vector3.Normalize(Vector3.Cross(direction, up)); + up = Vector3.Cross(right, direction); + + Matrix4x4 lookAtMatrix = new Matrix4x4( + right.X, up.X, -direction.X, 0, + right.Y, up.Y, -direction.Y, 0, + right.Z, up.Z, -direction.Z, 0, + 0, 0, 0, 1 + ); + + Camera.Transform.Transformation = lookAtMatrix; + } + + /// + /// Sets the camera to the default rotation for MapleStory 2 + /// + public void SetDefaultRotation() { + Camera.Transform.Quaternion = DefaultCameraRotation; + } + + /// + /// Moves the camera target relative to its current position + /// + public void MoveCameraTargetRelative(Vector3 offset) { + CameraTarget += offset; + } + + // ICameraController interface methods that are not follow-specific + // These provide minimal implementations since this controller focuses on following + + /// + /// 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 + } + + /// + /// Sets the camera to the field overview rotation + /// + public void SetFieldOverviewRotation() { + Camera.Transform.Quaternion = FieldOverviewRotation; + } + + /// + /// Rotates the camera around the target point - minimal implementation for follow controller + /// + public void RotateCamera(float yawDelta, float pitchDelta) { + // Follow controller maintains fixed camera orientation relative to player + // Manual rotation would break following, so this is intentionally minimal + } + + /// + /// Moves the camera relative to its current position - minimal implementation for follow controller + /// + public void MoveCameraRelative(Vector3 offset) { + // Follow controller maintains fixed camera position relative to player + // Manual movement would break following, so this is intentionally minimal + } + + /// + /// Toggles wireframe rendering mode + /// + public void ToggleWireframeMode() { + WireframeMode = !WireframeMode; + } + + /// + /// 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; + SetDefaultRotation(); + } + + /// + /// 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; + + // Create a flip matrix that inverts the Y axis + Matrix4x4 flipMatrix = Matrix4x4.CreateScale(1, -1, 1); + + Camera.Transform.Transformation = currentTransform * flipMatrix; + } +} diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 6eaf74156..907d257d9 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -4,27 +4,27 @@ namespace Maple2.Server.DebugGame.Graphics.Scene; -public class FreeCameraController { +public class FreeCameraController : ICameraController { private static readonly ILogger Logger = Log.Logger.ForContext(); const float MAX_PITCH = (float.Pi / 180) * 89; - public InputState InputState = new(); - public Camera Camera = new(); + public InputState InputState { get; } = new(); + public Camera Camera { get; } // Rendering modes public bool WireframeMode { get; set; } = true; // Start in wireframe mode - // Camera follow system - public bool IsFollowingPlayer { get; private set; } - public long? FollowedPlayerId { get; private set; } - public bool HasManuallyStopped { get; private set; } - // Camera properties that delegate to Camera.Transform public Vector3 CameraPosition => Camera.Transform.Position; - public Vector3 CameraTarget { get; private set; } = Vector3.Zero; public Quaternion CameraRotation => Camera.Transform.Quaternion; + // Free camera controller doesn't support following - these return default values + public bool IsFollowingPlayer => false; + public long? FollowedPlayerId => null; + public bool HasManuallyStopped => false; + 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; @@ -40,15 +40,16 @@ public float RotationSpeedDegrees { set => RotationSpeed = (float.Pi / 180.0f) * value; } - // Default camera follow offset - private static readonly Vector3 CameraFollowOffset = new Vector3(-800, -1000, 1200); - // Default camera rotation for MapleStory 2 coordinate system private static readonly Quaternion DefaultCameraRotation = new Quaternion(0.130f, 0.325f, 0.870f, 0.350f); // Field overview camera rotation private static readonly Quaternion FieldOverviewRotation = new Quaternion(0.000f, 0.450f, 0.900f, 0.000f); + public FreeCameraController(Camera camera) { + Camera = camera; + } + /// /// Sets the camera to the default rotation for MapleStory 2 /// @@ -110,10 +111,7 @@ public void Update(float delta) { if (hasMovementInput) { Camera.Transform.Position += delta * currentSpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); - // Disable follow when manual movement is detected - if (IsFollowingPlayer) { - StopFollowingPlayer(); - } + // Free camera controller doesn't support following } if (!InputState.MouseRight.IsDown) { @@ -124,10 +122,7 @@ public void Update(float delta) { // Check if there's actual mouse movement if (mouseDelta.X != 0 || mouseDelta.Y != 0) { - // Disable follow when mouse rotation is detected - if (IsFollowingPlayer) { - StopFollowingPlayer(); - } + // Free camera controller doesn't support following } float currentPitch = -float.Asin(Camera.Transform.FrontAxis.Z); @@ -145,27 +140,7 @@ public void Update(float delta) { InputState.MousePosition.Lock = true; } - /// - /// Sets the camera target and updates camera orientation - /// - public void SetCameraTarget(Vector3 target) { - CameraTarget = target; - Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); - - // Calculate rotation to look at target - Vector3 up = Vector3.UnitZ; // Z is up in our coordinate system - Vector3 right = Vector3.Normalize(Vector3.Cross(direction, up)); - up = Vector3.Cross(right, direction); - Matrix4x4 lookAtMatrix = new Matrix4x4( - right.X, up.X, -direction.X, 0, - right.Y, up.Y, -direction.Y, 0, - right.Z, up.Z, -direction.Z, 0, - 0, 0, 0, 1 - ); - - Camera.Transform.Transformation = lookAtMatrix; - } /// /// Rotates the camera around the target point @@ -199,53 +174,49 @@ public void RotateCamera(float yawDelta, float pitchDelta) { public void MoveCameraRelative(Vector3 offset) { Camera.Transform.Position += offset; CameraTarget += offset; - - // Unlock camera follow when manually moving - if (IsFollowingPlayer && offset.LengthSquared() > 0.01f) { - UnlockCameraFollow(); - } } /// - /// Starts following a specific player + /// Starts following a specific player - not supported by free camera controller /// public void StartFollowingPlayer(long playerId) { - IsFollowingPlayer = true; - FollowedPlayerId = playerId; - HasManuallyStopped = false; // Reset manual stop flag when starting to follow - SetDefaultRotation(); // Set default rotation when starting to follow - Logger.Information("Started following player {PlayerId}", playerId); + // Free camera controller doesn't support following } /// - /// Stops following the current player + /// Stops following the current player - not supported by free camera controller /// public void StopFollowingPlayer() { - IsFollowingPlayer = false; - FollowedPlayerId = null; - HasManuallyStopped = true; // Remember that user manually stopped - Logger.Information("Stopped following player"); + // Free camera controller doesn't support following } /// - /// Unlocks camera follow when manual movement is detected + /// Updates camera to follow a player at the specified position - not supported by free camera controller /// - private void UnlockCameraFollow() { - if (IsFollowingPlayer) { - IsFollowingPlayer = false; - Logger.Information("Camera follow unlocked - manual movement detected"); - } + public void UpdatePlayerFollow(Vector3 playerPosition) { + // Free camera controller doesn't support following } /// - /// Updates camera to follow a player at the specified position + /// Sets the camera target /// - public void UpdatePlayerFollow(Vector3 playerPosition) { - if (!IsFollowingPlayer) return; + public void SetCameraTarget(Vector3 target) { + CameraTarget = target; + Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); - SetCameraTarget(playerPosition); - Camera.Transform.Position = playerPosition + CameraFollowOffset; - SetDefaultRotation(); // Always use default rotation when following player + // Calculate rotation to look at target + Vector3 up = Vector3.UnitZ; // Z is up in our coordinate system + Vector3 right = Vector3.Normalize(Vector3.Cross(direction, up)); + up = Vector3.Cross(right, direction); + + Matrix4x4 lookAtMatrix = new Matrix4x4( + right.X, up.X, -direction.X, 0, + right.Y, up.Y, -direction.Y, 0, + right.Z, up.Z, -direction.Z, 0, + 0, 0, 0, 1 + ); + + Camera.Transform.Transformation = lookAtMatrix; } /// diff --git a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs new file mode 100644 index 000000000..44c667422 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs @@ -0,0 +1,120 @@ +using System.Numerics; + +namespace Maple2.Server.DebugGame.Graphics.Scene; + +/// +/// Interface for camera controllers that can manipulate a camera +/// +public interface ICameraController { + /// + /// The camera being controlled + /// + Camera Camera { get; } + + /// + /// Input state for handling user input + /// + InputState InputState { get; } + + /// + /// Whether wireframe rendering mode is enabled + /// + bool WireframeMode { get; set; } + + /// + /// Current camera position + /// + Vector3 CameraPosition { get; } + + /// + /// Current camera rotation + /// + Quaternion CameraRotation { get; } + + /// + /// Current camera target position + /// + Vector3 CameraTarget { get; } + + /// + /// Whether the controller is currently following a player + /// + bool IsFollowingPlayer { get; } + + /// + /// ID of the player being followed (if any) + /// + long? FollowedPlayerId { get; } + + /// + /// Whether the user has manually stopped following + /// + bool HasManuallyStopped { get; } + + /// + /// Updates the controller with the given delta time + /// + /// Time elapsed since last update + void Update(float delta); + + /// + /// Sets the camera to the default rotation for MapleStory 2 + /// + void SetDefaultRotation(); + + /// + /// Sets the camera to the field overview rotation + /// + void SetFieldOverviewRotation(); + + /// + /// Sets the camera target position + /// + /// Target position + void SetCameraTarget(Vector3 target); + + /// + /// Rotates the camera around the target point + /// + /// Yaw rotation delta + /// Pitch rotation delta + void RotateCamera(float yawDelta, float pitchDelta); + + /// + /// Moves the camera relative to its current position + /// + /// Position offset + void MoveCameraRelative(Vector3 offset); + + /// + /// Starts following a specific player + /// + /// ID of the player to follow + void StartFollowingPlayer(long playerId); + + /// + /// Stops following the current player + /// + void StopFollowingPlayer(); + + /// + /// Updates camera to follow a player at the specified position + /// + /// Player position + void UpdatePlayerFollow(Vector3 playerPosition); + + /// + /// Toggles wireframe rendering mode + /// + void ToggleWireframeMode(); + + /// + /// Sets camera orientation that works well with MapleStory 2's coordinate system + /// + void SetCameraOrientationForMapData(); + + /// + /// Flips the camera's up vector to fix upside-down view + /// + void FlipCameraUpVector(); +} From 1cca122373862d3f31ad976f15040d3faaed3da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 29 Jul 2025 18:35:46 -0300 Subject: [PATCH 12/25] refactor: centralize wireframe mode in DebugGraphicsContext for consistency --- .../Graphics/DebugFieldRenderer.cs | 10 +++++----- .../Graphics/DebugGraphicsContext.cs | 13 ++++++++++++- .../Graphics/Scene/FollowCameraController.cs | 10 ++-------- .../Graphics/Scene/FreeCameraController.cs | 11 ++--------- .../Graphics/Scene/ICameraController.cs | 10 ++-------- 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 12feeed90..6389b4cd8 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -481,8 +481,8 @@ private void RenderField3DVisualization(double delta) { Context.UpdateProjectionMatrix(); // Enable wireframe mode for field visualization - bool originalWireframeMode = Context.CameraController.WireframeMode; - Context.CameraController.WireframeMode = true; + bool originalWireframeMode = Context.WireframeMode; + Context.WireframeMode = true; Context.SetWireframeRasterizer(); // Always use wireframe shaders for field visualization (they don't need texture samplers) @@ -493,7 +493,7 @@ private void RenderField3DVisualization(double delta) { RenderFieldEntities3D(); // Restore original wireframe mode - Context.CameraController.WireframeMode = originalWireframeMode; + Context.WireframeMode = originalWireframeMode; Context.SetSolidRasterizer(); // Also render the entity details window @@ -786,7 +786,7 @@ private void RenderVisualizationControls() { ImGui.SameLine(); if (ImGui.Button("Toggle Wireframe")) { - Context.CameraController.ToggleWireframeMode(); + Context.ToggleWireframeMode(); } if (ImGui.Button("View Field Overview")) { @@ -819,7 +819,7 @@ private void RenderVisualizationControls() { Quaternion q = Context.CameraController.CameraRotation; ImGui.Text($"Camera Rotation: X={q.X:F3}, Y={q.Y:F3}, Z={q.Z:F3}, W={q.W:F3}"); - ImGui.Text($"Wireframe Mode: {(Context.CameraController.WireframeMode ? "ON" : "OFF")}"); + ImGui.Text($"Wireframe Mode: {(Context.WireframeMode ? "ON" : "OFF")}"); } ImGui.End(); } diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index decb1e279..a28d48b9b 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -69,6 +69,9 @@ public class DebugGraphicsContext : IGraphicsContext { // Global follow state (independent of active controller) private bool hasManuallyStoppedFollowing = false; + // Rendering state + public bool WireframeMode { get; set; } = true; // Start in wireframe mode + private readonly List fieldRenderers = []; private readonly Mutex fieldRendererMutex = new(); public DebugFieldRenderer[] FieldRenderers { @@ -488,6 +491,14 @@ public void UpdatePlayerFollow(Vector3 playerPosition) { } } + /// + /// Toggles wireframe rendering mode + /// + public void ToggleWireframeMode() { + WireframeMode = !WireframeMode; + Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); + } + public void UpdateConstantBuffer(Matrix4x4 worldMatrix) { UpdateConstantBuffer(worldMatrix, new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Default white color } @@ -830,7 +841,7 @@ private unsafe void OnRender(double delta) { DxDeviceContext.OMSetDepthStencilState(DepthStencilState, 1); // Set rasterizer state based on wireframe mode - DxDeviceContext.RSSetState(CameraController.WireframeMode ? WireframeRasterizerState : SolidRasterizerState); + DxDeviceContext.RSSetState(WireframeMode ? WireframeRasterizerState : SolidRasterizerState); ImGuiController!.BeginFrame((float) delta); ImGuiController!.EndFrame(); diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs index 0e8791743..54765fe96 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs @@ -9,8 +9,7 @@ public class FollowCameraController : ICameraController { public Camera Camera { get; } public InputState InputState { get; } = new(); - // Rendering modes - public bool WireframeMode { get; set; } = true; + // Camera follow system public bool IsFollowingPlayer { get; private set; } @@ -148,12 +147,7 @@ public void MoveCameraRelative(Vector3 offset) { // Manual movement would break following, so this is intentionally minimal } - /// - /// Toggles wireframe rendering mode - /// - public void ToggleWireframeMode() { - WireframeMode = !WireframeMode; - } + /// /// Sets camera orientation that works well with MapleStory 2's coordinate system diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 907d257d9..06ec339ff 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -12,8 +12,7 @@ public class FreeCameraController : ICameraController { public InputState InputState { get; } = new(); public Camera Camera { get; } - // Rendering modes - public bool WireframeMode { get; set; } = true; // Start in wireframe mode + // Camera properties that delegate to Camera.Transform public Vector3 CameraPosition => Camera.Transform.Position; @@ -219,13 +218,7 @@ public void SetCameraTarget(Vector3 target) { Camera.Transform.Transformation = lookAtMatrix; } - /// - /// Toggles wireframe rendering mode - /// - public void ToggleWireframeMode() { - WireframeMode = !WireframeMode; - Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); - } + /// /// Sets camera orientation that works well with MapleStory 2's coordinate system diff --git a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs index 44c667422..53ce7451f 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs @@ -16,10 +16,7 @@ public interface ICameraController { /// InputState InputState { get; } - /// - /// Whether wireframe rendering mode is enabled - /// - bool WireframeMode { get; set; } + /// /// Current camera position @@ -103,10 +100,7 @@ public interface ICameraController { /// Player position void UpdatePlayerFollow(Vector3 playerPosition); - /// - /// Toggles wireframe rendering mode - /// - void ToggleWireframeMode(); + /// /// Sets camera orientation that works well with MapleStory 2's coordinate system From b8778f4c632273698616fc2cfc121a1b7ca468ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 29 Jul 2025 18:42:10 -0300 Subject: [PATCH 13/25] refactor: remove redundant movement input flag in FreeCameraController --- .../Graphics/Scene/FreeCameraController.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 06ec339ff..998aedc92 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -69,36 +69,29 @@ public void Update(float delta) { } Vector3 moveDirection = new(); - bool hasMovementInput = false; if (InputState.GetState(MoveForward).IsDown) { moveDirection.Y += 1; - hasMovementInput = true; } if (InputState.GetState(MoveBackward).IsDown) { moveDirection.Y -= 1; - hasMovementInput = true; } if (InputState.GetState(MoveRight).IsDown) { moveDirection.X += 1; - hasMovementInput = true; } if (InputState.GetState(MoveLeft).IsDown) { moveDirection.X -= 1; - hasMovementInput = true; } if (InputState.GetState(MoveUp).IsDown) { moveDirection.Z += 1; - hasMovementInput = true; } if (InputState.GetState(MoveDown).IsDown) { moveDirection.Z -= 1; - hasMovementInput = true; } // Apply speed modifier with Shift key @@ -107,7 +100,7 @@ public void Update(float delta) { currentSpeed *= 5.0f; // 5x speed with Shift } - if (hasMovementInput) { + if (moveDirection.LengthSquared() > 0) { Camera.Transform.Position += delta * currentSpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); // Free camera controller doesn't support following From 52aaa878f17b09d8037d189b33f5d404f698d2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 2 Aug 2025 19:11:19 -0300 Subject: [PATCH 14/25] fix: improve player follow UI with dynamic status and position display --- Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 6389b4cd8..928eb1e05 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -795,10 +795,9 @@ private void RenderVisualizationControls() { // Player follow controls ImGui.Separator(); - ImGui.Text("Player Follow:"); + ImGui.Text($"Player Follow: {(Context.CameraController.IsFollowingPlayer ? $"Player ID: {Context.CameraController.FollowedPlayerId}" : string.Empty)}"); if (Context.CameraController.IsFollowingPlayer) { - ImGui.Text($"Following Player ID: {Context.CameraController.FollowedPlayerId}"); if (ImGui.Button("Stop Following")) { Context.StopFollowingPlayer(); } @@ -811,9 +810,11 @@ private void RenderVisualizationControls() { } } + if (Context.CameraController.IsFollowingPlayer) { + ImGui.Text($"Player Following Position: {Context.CameraController.CameraTarget}"); + } // Camera position display ImGui.Text($"Camera Position: {Context.CameraController.CameraPosition}"); - ImGui.Text($"Camera Target: {Context.CameraController.CameraTarget}"); // Camera rotation display Quaternion q = Context.CameraController.CameraRotation; From 2678490af0cf703e27238dd8126a8456bcfdaab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 2 Aug 2025 19:33:03 -0300 Subject: [PATCH 15/25] feat(debug): add detailed field entity info and toggle options for portals and actors --- .../Graphics/DebugFieldRenderer.cs | 336 +++++++++--------- 1 file changed, 177 insertions(+), 159 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 928eb1e05..3aa8787e9 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -38,7 +38,12 @@ public bool IsActive { private bool showSpawnPoints; // Spawn points disabled by default private bool showVibrateObjects; // Vibrate objects disabled by default private bool showPortals = true; // Portals enabled by default + private bool showPortalInformation = true; // Portal text information enabled by default + private bool showPortalConnections = true; // Portal connection lines enabled by default private bool showActors = true; // Actors enabled by default + private bool showPlayers = true; // Players enabled by default + private bool showNpcs = true; // NPCs enabled by default + private bool showMobs = true; // Mobs enabled by default // Entity caching for performance private FieldEntity[]? cachedStaticEntities; // Cache static entities (spawn points, triggers, etc.) @@ -103,7 +108,7 @@ public void Render(double delta) { ImGui.Separator(); RenderActorList(); ImGui.Separator(); - RenderFieldProperties(); + RenderFieldEntityDetails(); } ImGui.End(); @@ -131,6 +136,10 @@ private void RenderFieldBasicInfo() { 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}"); @@ -285,20 +294,28 @@ private void RenderActorList() { } } - private void RenderFieldProperties() { - ImGui.Text("Field Status:"); - ImGui.Indent(); - - if (Field.RoomTimer != null) { - ImGui.Text($"Room Timer Active: {Field.RoomTimer.Duration}"); + private void RenderFieldEntityDetails() { + if (Field.AccelerationStructure == null) { + return; } + ImGui.Text("Field Entity Information:"); + ImGui.Separator(); - if (Field.AccelerationStructure != null) { - ImGui.Text("Acceleration Structure: Active"); - } + // Basic field info + ImGui.Text($"Grid Size: {Field.AccelerationStructure.GridSize}"); + ImGui.Text($"Min Index: {Field.AccelerationStructure.MinIndex}"); + ImGui.Text($"Max Index: {Field.AccelerationStructure.MaxIndex}"); - ImGui.Text($"Field Instance Type: {Field.FieldInstance.Type}"); - ImGui.Unindent(); + ImGui.Separator(); + + // Entity counts by type + FieldEntity[] allEntities = GetAllEntities().ToArray(); + Dictionary entityCounts = allEntities.GroupBy(e => e.GetType().Name).ToDictionary(g => g.Key, g => g.Count()); + + ImGui.Text("Entity Types:"); + foreach (KeyValuePair kvp in entityCounts) { + ImGui.Text($" {kvp.Key}: {kvp.Value}"); + } } private void RenderActorDetailsPanel(Vector2 fieldWindowPos, Vector2 fieldWindowSize) { @@ -495,9 +512,6 @@ private void RenderField3DVisualization(double delta) { // Restore original wireframe mode Context.WireframeMode = originalWireframeMode; Context.SetSolidRasterizer(); - - // Also render the entity details window - RenderEntityDetailsWindow(); } private void RenderFieldEntities3D() { @@ -528,10 +542,15 @@ private void RenderFieldEntities3D() { // Render portals if (showPortals) { RenderPortals(); - RenderPortalConnectionLines(); + + if (showPortalConnections) { + RenderPortalConnectionLines(); + } // Render portal text labels - RenderPortalTextLabels(); + if (showPortalInformation) { + RenderPortalTextLabels(); + } } // Render actors (players, NPCs, mobs) @@ -707,59 +726,65 @@ private void RenderActors() { const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; // Render players using cylinders (like Recast agents) - Cyan color - Context.SetColor(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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * - rotation * - Matrix4x4.CreateTranslation(player.Position); - - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (cyan) - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for players (like Recast agents) + if (showPlayers) { + Context.SetColor(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)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(player.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (cyan) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for players (like Recast agents) + } } // Render NPCs using cylinders (like Recast agents) - Green for alive, Gray for dead - foreach ((int _, FieldNpc npc) in Field.Npcs) { - // Set color based on dead status - if (npc.IsDead) { - Context.SetColor(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead NPCs - } else { - Context.SetColor(0, 1, 0, 1); // Green for alive NPCs - } + if (showNpcs) { + foreach ((int _, FieldNpc npc) in Field.Npcs) { + // Set color based on dead status + if (npc.IsDead) { + Context.SetColor(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead NPCs + } else { + Context.SetColor(0, 1, 0, 1); // Green for alive NPCs + } - // 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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * - rotation * - Matrix4x4.CreateTranslation(npc.Position); + // 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)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(npc.Position); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (green/gray) - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs (like Recast agents) + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (green/gray) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs (like Recast agents) + } } // Render Mobs using cylinders (like Recast agents) - Red for aggressive, Yellow for wandering, Gray for dead - foreach ((int _, FieldNpc mob) in Field.Mobs) { - // Set color based on dead status and battle state - if (mob.IsDead) { - Context.SetColor(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) { - Context.SetColor(1, 0, 0, 1); // Red for aggressive mobs + if (showMobs) { + foreach ((int _, FieldNpc mob) in Field.Mobs) { + // Set color based on dead status and battle state + if (mob.IsDead) { + Context.SetColor(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead mobs } else { - Context.SetColor(1, 1, 0, 1); // Yellow for wandering mobs + // Check if mob is aggressive (in battle) or wandering + if (mob.BattleState.InBattle) { + Context.SetColor(1, 0, 0, 1); // Red for aggressive mobs + } else { + Context.SetColor(1, 1, 0, 1); // Yellow for wandering mobs + } } - } - // 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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * - rotation * - Matrix4x4.CreateTranslation(mob.Position); + // 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)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(mob.Position); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (red/yellow/gray) - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for mobs (like Recast agents) + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (red/yellow/gray) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for mobs (like Recast agents) + } } } @@ -769,12 +794,30 @@ private void RenderVisualizationControls() { ImGui.Separator(); ImGui.Checkbox("Show Box Colliders", ref showBoxColliders); - ImGui.Checkbox("Show Mesh Colliders", ref showMeshColliders); - ImGui.Checkbox("Show Spawn Points", ref showSpawnPoints); + // ImGui.Checkbox("Show Mesh Colliders", ref showMeshColliders); + // ImGui.Checkbox("Show Spawn Points", ref showSpawnPoints); ImGui.Checkbox("Show Vibrate Objects", ref showVibrateObjects); ImGui.Checkbox("Show Portals", ref showPortals); + + // Portal sub-options (only show when portals are enabled) + if (showPortals) { + ImGui.Indent(); + ImGui.Checkbox("Show Portal Information", ref showPortalInformation); + ImGui.Checkbox("Show Portal Connections", ref showPortalConnections); + ImGui.Unindent(); + } + ImGui.Checkbox("Show Actors", ref showActors); + // Actor sub-options (only show when actors are enabled) + if (showActors) { + ImGui.Indent(); + ImGui.Checkbox("Show Players", ref showPlayers); + ImGui.Checkbox("Show NPCs", ref showNpcs); + ImGui.Checkbox("Show Mobs", ref showMobs); + ImGui.Unindent(); + } + ImGui.Separator(); // Camera controls @@ -825,37 +868,6 @@ private void RenderVisualizationControls() { ImGui.End(); } - private void RenderEntityDetailsWindow() { - if (Field.AccelerationStructure == null) { - return; - } - - // Create entity details window - if (ImGui.Begin("Field Entity Details")) { - ImGui.Text("Field Entity Information"); - ImGui.Separator(); - - // Basic field info - ImGui.Text($"Grid Size: {Field.AccelerationStructure.GridSize}"); - ImGui.Text($"Min Index: {Field.AccelerationStructure.MinIndex}"); - ImGui.Text($"Max Index: {Field.AccelerationStructure.MaxIndex}"); - - ImGui.Separator(); - - // Entity counts by type - FieldEntity[] allEntities = GetAllEntities().ToArray(); - Dictionary entityCounts = allEntities.GroupBy(e => e.GetType().Name).ToDictionary(g => g.Key, g => g.Count()); - - ImGui.Text("Entity Types:"); - foreach (KeyValuePair kvp in entityCounts) { - ImGui.Text($" {kvp.Key}: {kvp.Value}"); - } - - ImGui.Separator(); - } - ImGui.End(); - } - private void SetFieldOverviewCamera() { // Stop following player when switching to field overview if (Context.CameraController.IsFollowingPlayer) { @@ -962,88 +974,94 @@ private void RenderActorTextLabels() { ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); // Render player names using ImGui - 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; - Vector2 textSize = ImGui.CalcTextSize(playerName); - var textColor = new Vector4(0, 1, 1, 1); // Cyan - - // Draw semi-transparent background - Vector2 bgMin = screenPos - new Vector2(4, 2); - Vector2 bgMax = screenPos + textSize + new Vector2(4, 2); - drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); // Dark background - drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); // Border - - // Draw text on top - drawList.AddText(screenPos, ImGui.ColorConvertFloat4ToU32(textColor), playerName); + 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; + Vector2 textSize = ImGui.CalcTextSize(playerName); + var textColor = new Vector4(0, 1, 1, 1); // Cyan + + // Draw semi-transparent background + Vector2 bgMin = screenPos - new Vector2(4, 2); + Vector2 bgMax = screenPos + textSize + new Vector2(4, 2); + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); // Dark background + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); // Border + + // Draw text on top + drawList.AddText(screenPos, ImGui.ColorConvertFloat4ToU32(textColor), playerName); + } } // Render NPC names ImGui - 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; + 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}"; + string npcName = npc.Value.Metadata.Name ?? $"NPC {npc.Value.Metadata.Id}"; - var textColor = new Vector4(0, 1, 0, 1); - uint color = ImGui.ColorConvertFloat4ToU32(textColor); + var textColor = new Vector4(0, 1, 0, 1); + uint color = ImGui.ColorConvertFloat4ToU32(textColor); - // Draw background for NPC name - Vector2 nameSize = ImGui.CalcTextSize(npcName); - Vector2 nameBgMin = screenPos - new Vector2(4, 2); - Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + // Draw background for NPC name + Vector2 nameSize = ImGui.CalcTextSize(npcName); + Vector2 nameBgMin = screenPos - new Vector2(4, 2); + Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - drawList.AddText(screenPos, color, npcName); + drawList.AddText(screenPos, color, npcName); + } } // Render Mob names and HP using ImGui - 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; + 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}"; + + // 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; + + // Choose color based on status + Vector4 textColor; + 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 + } - string mobName = mob.Value.Metadata.Name ?? $"Mob {mob.Value.Metadata.Id}"; + uint color = ImGui.ColorConvertFloat4ToU32(textColor); - // 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; + // Draw background for mob name + Vector2 nameSize = ImGui.CalcTextSize(mobName); + Vector2 nameBgMin = screenPos - new Vector2(4, 2); + Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - // Choose color based on status - Vector4 textColor; - 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 - } + drawList.AddText(screenPos, color, mobName); - uint color = ImGui.ColorConvertFloat4ToU32(textColor); + // Position HP text below the name using screen-space offset to avoid overlap when zoomed out + // Calculate HP text position in screen space (below the name) with consistent pixel spacing + Vector2 hpScreenPos = new Vector2(screenPos.X, screenPos.Y + nameSize.Y + 4); // 4 pixels below name - // Draw background for mob name - Vector2 nameSize = ImGui.CalcTextSize(mobName); - Vector2 nameBgMin = screenPos - new Vector2(4, 2); - Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - - drawList.AddText(screenPos, color, mobName); - - // Position HP text below the name using screen-space offset to avoid overlap when zoomed out - // Calculate HP text position in screen space (below the name) with consistent pixel spacing - Vector2 hpScreenPos = new Vector2(screenPos.X, screenPos.Y + nameSize.Y + 4); // 4 pixels below name - - string hpText = $"HP: {hpPercent:F1}%"; - Vector2 hpSize = ImGui.CalcTextSize(hpText); - Vector2 hpBgMin = hpScreenPos - new Vector2(4, 2); - Vector2 hpBgMax = hpScreenPos + hpSize + new Vector2(4, 2); - drawList.AddRectFilled(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - drawList.AddText(hpScreenPos, color, hpText); + string hpText = $"HP: {hpPercent:F1}%"; + Vector2 hpSize = ImGui.CalcTextSize(hpText); + Vector2 hpBgMin = hpScreenPos - new Vector2(4, 2); + Vector2 hpBgMax = hpScreenPos + hpSize + new Vector2(4, 2); + drawList.AddRectFilled(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); + drawList.AddRect(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + drawList.AddText(hpScreenPos, color, hpText); + } } } @@ -1056,7 +1074,7 @@ private void RenderPortalTextLabels() { if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; // Build portal info text - string enabledText = portal.Enabled ? "Enabled" : "Disabled"; + 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}"; From df9c149194c9bc30c5a710ca5ee26629dbe2ac85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 2 Aug 2025 19:44:17 -0300 Subject: [PATCH 16/25] feat(debug): add actor selection and highlight in debug field renderer --- .../Graphics/DebugFieldRenderer.cs | 228 ++++++++++++++++-- 1 file changed, 213 insertions(+), 15 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 3aa8787e9..00f536602 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -8,6 +8,7 @@ using Maple2.Server.Game.Manager; using Maple2.Server.Game.Model; using Maple2.Tools.VectorMath; +using Maple2.Tools.Collision; using Silk.NET.Maths; namespace Maple2.Server.DebugGame.Graphics; @@ -127,10 +128,26 @@ public void RenderFieldWindow3D(double delta) { return; } + // Handle mouse input for actor selection + HandleMouseInput(); + // Render 3D field visualization using the DirectX pipeline RenderField3DVisualization(delta); } + private void HandleMouseInput() { + // Check if mouse is clicked and we're hovering over the field window + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { + // Get mouse position in screen coordinates + Vector2 mousePos = ImGui.GetMousePos(); + + // For now, assume we're clicking in the field window if any are active + // The mouse position is already in the correct coordinate space for the field window + // since ImGui handles the coordinate transformation automatically + TrySelectActorAtScreenPosition(mousePos); + } + } + private void RenderFieldBasicInfo() { ImGui.Text($"Map ID: {Field.MapId}"); ImGui.Text($"Room ID: {Field.RoomId}"); @@ -557,6 +574,11 @@ private void RenderFieldEntities3D() { if (showActors) { RenderActors(); + // Render selection highlight for selected actor + if (selectedActor != null) { + RenderActorSelectionHighlight(); + } + // Render text labels above actors using ImGui RenderActorTextLabels(); } @@ -788,6 +810,46 @@ private void RenderActors() { } } + private void RenderActorSelectionHighlight() { + if (selectedActor == null) return; + + const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; + const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; + + // Set bright highlight color (white with high intensity) + Context.SetColor(1, 1, 1, 1); // Bright white for selection highlight + + // Render a slightly larger cylinder as highlight outline + float highlightRadius = agentRadius * 1.2f; // 20% larger radius + float highlightHeight = agentHeight * 1.1f; // 10% larger height + + // 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)); + Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(highlightRadius, highlightHeight, highlightRadius)) * + rotation * + Matrix4x4.CreateTranslation(selectedActor.Position); + + Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (bright white) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for highlight + + // Optional: Add a pulsing effect by varying the highlight intensity + // You could use time-based sine wave to make it pulse + float time = (float)(Environment.TickCount64 % 2000) / 2000.0f; // 2 second cycle + float pulseIntensity = 0.7f + 0.3f * MathF.Sin(time * MathF.PI * 2); // Pulse between 0.7 and 1.0 + + // Render a second, even larger outline with pulsing effect + Context.SetColor(1, 1, 0, pulseIntensity); // Yellow with pulsing alpha + float pulseRadius = agentRadius * 1.4f; + float pulseHeight = agentHeight * 1.2f; + + Matrix4x4 pulseWorldMatrix = Matrix4x4.CreateScale(new Vector3(pulseRadius, pulseHeight, pulseRadius)) * + rotation * + Matrix4x4.CreateTranslation(selectedActor.Position); + + Context.UpdateConstantBuffer(pulseWorldMatrix, true); + Context.CoreModels!.Cylinder.Draw(); + } + private void RenderVisualizationControls() { if (ImGui.Begin("3D Visualization Controls")) { ImGui.Text("Field Visualization Settings"); @@ -970,6 +1032,122 @@ private bool TryWorldToScreen(Vector3 worldPos, out Vector2 screenPos) { return true; } + private bool TryScreenToWorldRay(Vector2 screenPos, out Vector3 rayOrigin, out Vector3 rayDirection) { + rayOrigin = Vector3.Zero; + rayDirection = Vector3.Zero; + + // Get the actual viewport size from the field window + Vector2D windowSize = GetFieldWindowSize(); + + // 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 + + // Get camera matrices + Matrix4x4 viewMatrix = Context.CameraController.Camera.ViewMatrix; + Matrix4x4 projMatrix = Context.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 + } + + // 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 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 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 RenderActorTextLabels() { ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); @@ -980,14 +1158,18 @@ private void RenderActorTextLabels() { if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; string playerName = player.Value.Character.Name; + bool isSelected = selectedActor == player; + Vector2 textSize = ImGui.CalcTextSize(playerName); - var textColor = new Vector4(0, 1, 1, 1); // Cyan + 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 semi-transparent background + // Draw background Vector2 bgMin = screenPos - new Vector2(4, 2); Vector2 bgMax = screenPos + textSize + new Vector2(4, 2); - drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); // Dark background - drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); // Border + drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); // Draw text on top drawList.AddText(screenPos, ImGui.ColorConvertFloat4ToU32(textColor), playerName); @@ -1001,16 +1183,19 @@ private void RenderActorTextLabels() { if (!TryWorldToScreen(textPos, out Vector2 screenPos)) continue; string npcName = npc.Value.Metadata.Name ?? $"NPC {npc.Value.Metadata.Id}"; + bool isSelected = selectedActor == npc; - var textColor = new Vector4(0, 1, 0, 1); + 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); // Draw background for NPC name Vector2 nameSize = ImGui.CalcTextSize(npcName); Vector2 nameBgMin = screenPos - new Vector2(4, 2); Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); drawList.AddText(screenPos, color, npcName); } @@ -1023,20 +1208,33 @@ private void RenderActorTextLabels() { 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; - // Choose color based on status + // Choose color based on status, but override with yellow if selected Vector4 textColor; - 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 + 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 { - textColor = new Vector4(1, 1, 0, 1); // Yellow for wandering + // 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 } uint color = ImGui.ColorConvertFloat4ToU32(textColor); @@ -1045,8 +1243,8 @@ private void RenderActorTextLabels() { Vector2 nameSize = ImGui.CalcTextSize(mobName); Vector2 nameBgMin = screenPos - new Vector2(4, 2); Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); + drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); drawList.AddText(screenPos, color, mobName); From 496a34ac1b719cd8bab2aa8be3c9eff489c0d05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 2 Aug 2025 20:45:32 -0300 Subject: [PATCH 17/25] feat(debug): add player move mode with optional force move in debug renderer --- .../Graphics/DebugFieldRenderer.cs | 160 +++++++++++++----- 1 file changed, 121 insertions(+), 39 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 00f536602..0323903de 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -7,6 +7,7 @@ using Maple2.Model.Metadata.FieldEntity; using Maple2.Server.Game.Manager; using Maple2.Server.Game.Model; +using Maple2.Server.Game.Packets; using Maple2.Tools.VectorMath; using Maple2.Tools.Collision; using Silk.NET.Maths; @@ -46,6 +47,10 @@ public bool IsActive { private bool showNpcs = true; // NPCs enabled by default private bool showMobs = true; // Mobs enabled by default + // Player movement mode + private bool playerMoveMode = false; // Player move mode disabled by default + private bool forceMove = false; // Force move (bypass validation) disabled by default + // Entity caching for performance private FieldEntity[]? cachedStaticEntities; // Cache static entities (spawn points, triggers, etc.) @@ -138,13 +143,21 @@ public void RenderFieldWindow3D(double delta) { 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; + } + // Get mouse position in screen coordinates Vector2 mousePos = ImGui.GetMousePos(); - // For now, assume we're clicking in the field window if any are active - // The mouse position is already in the correct coordinate space for the field window - // since ImGui handles the coordinate transformation automatically - TrySelectActorAtScreenPosition(mousePos); + 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); + } } } @@ -452,12 +465,22 @@ private void RenderActorStatsInfo() { private void RenderActorAdditionalInfo() { if (selectedActor == null) return; - ImGui.Text("Additional Information:"); - ImGui.Indent(); + ImGui.Text("Additional Information"); switch (selectedActor) { case FieldPlayer player: ImGui.Text($"Admin Permissions: {player.AdminPermissions}"); + + // Player movement controls + ImGui.Separator(); + ImGui.Text("Movement Controls:"); + ImGui.Checkbox("Move Player Mode", ref playerMoveMode); + if (playerMoveMode) { + ImGui.Indent(); + ImGui.Checkbox("Force Move (bypass validation)", ref forceMove); + 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")}"); @@ -470,7 +493,6 @@ private void RenderActorAdditionalInfo() { break; } - ImGui.Unindent(); } public void CleanUp() { @@ -597,9 +619,7 @@ private void RenderBoxColliders() { }; transform.Transformation *= MapRotation; - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(boxCollider.Size) * - Matrix4x4.CreateFromQuaternion(transform.Quaternion) * Matrix4x4.CreateTranslation(boxCollider.Position); Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (white) @@ -624,7 +644,6 @@ private void RenderMeshColliders() { rotation = coordinateTransform * rotation; Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * - Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(center); Context.UpdateConstantBuffer(worldMatrix); @@ -641,7 +660,6 @@ private void RenderSpawnPoints() { var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 50.0f) * - Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(spawnPoint.Position); Context.UpdateConstantBuffer(worldMatrix); @@ -659,7 +677,6 @@ private void RenderVibrateObjects() { var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 100.0f) * - Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(vibrateEntity.Position); Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (pink) @@ -672,15 +689,10 @@ private void RenderPortals() { Context.SetColor(0, 0.5f, 1, 1); // Blue for portals foreach (FieldPortal portal in Field.GetPortals()) { - // Convert Euler angles (degrees) to quaternion - Vector3 rotationRadians = portal.Rotation * (MathF.PI / 180.0f); - var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); - // Use portal dimensions if available, otherwise use default size Vector3 size = portal.Value.Dimension != Vector3.Zero ? portal.Value.Dimension : Vector3.One * 100.0f; Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * - Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(portal.Position); Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (blue) @@ -816,29 +828,11 @@ private void RenderActorSelectionHighlight() { const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; - // Set bright highlight color (white with high intensity) - Context.SetColor(1, 1, 1, 1); // Bright white for selection highlight - - // Render a slightly larger cylinder as highlight outline - float highlightRadius = agentRadius * 1.2f; // 20% larger radius - float highlightHeight = agentHeight * 1.1f; // 10% larger height - // 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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(highlightRadius, highlightHeight, highlightRadius)) * - rotation * - Matrix4x4.CreateTranslation(selectedActor.Position); - - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (bright white) - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for highlight - - // Optional: Add a pulsing effect by varying the highlight intensity - // You could use time-based sine wave to make it pulse - float time = (float)(Environment.TickCount64 % 2000) / 2000.0f; // 2 second cycle - float pulseIntensity = 0.7f + 0.3f * MathF.Sin(time * MathF.PI * 2); // Pulse between 0.7 and 1.0 // Render a second, even larger outline with pulsing effect - Context.SetColor(1, 1, 0, pulseIntensity); // Yellow with pulsing alpha + Context.SetColor(1, 1, 0, 1); // Yellow with pulsing alpha float pulseRadius = agentRadius * 1.4f; float pulseHeight = agentHeight * 1.2f; @@ -956,13 +950,12 @@ private void SetFieldOverviewCamera() { Context.CameraController.SetCameraTarget(center); Context.CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); - Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } else { // Fallback if no colliders found Context.CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); Context.CameraController.SetCameraTarget(Vector3.Zero); - Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } + Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } private void UpdateCameraFollow() { @@ -1057,7 +1050,7 @@ private bool TryScreenToWorldRay(Vector2 screenPos, out Vector3 rayOrigin, out V // 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 + Vector4 farPoint = new Vector4(ndcX, ndcY, 1.0f, 1.0f); // Far plane in NDC // Transform to world space Vector4 nearWorld = Vector4.Transform(nearPoint, invViewProjMatrix); @@ -1148,6 +1141,95 @@ private bool TryRayPrismIntersection(Vector3 rayOrigin, Vector3 rayDirection, IP return false; } + 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); + + if (targetPosition.HasValue) { + Vector3 newPosition = targetPosition.Value; + Vector3 currentRotation = player.Rotation; // Keep current rotation + + 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 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); + + 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 + } + // 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); + } + + 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); + } + } + + // Fallback: project ray forward a reasonable distance + Vector3 projected = rayOrigin + rayDirection * 1000.0f; + return new Vector2(projected.X, projected.Y); + } + + private float? FindTopmostBlockAt(Vector2 position) { + if (Field.AccelerationStructure == null) { + return null; + } + + float? highestZ = null; + + // Query box colliders (main map blocks) at this position + var boxColliders = GetAllEntities().OfType(); + + 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); + + if (!highestZ.HasValue || colliderTop > highestZ.Value) { + highestZ = colliderTop; + } + } + } + + return highestZ; + } + + 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); + + return position.X >= min.X && position.X <= max.X && + position.Y >= min.Y && position.Y <= max.Y; + } + private void RenderActorTextLabels() { ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); From 267dd3fc674ee3a10b624c0a3e891c62e1a471a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sun, 3 Aug 2025 16:46:58 -0300 Subject: [PATCH 18/25] . --- .../Graphics/DebugFieldRenderer.cs | 72 +++++++---- .../Graphics/DebugGraphicsContext.cs | 20 +-- .../Graphics/Scene/Camera.cs | 43 ++++--- .../Graphics/Scene/FollowCameraController.cs | 116 ++---------------- .../Graphics/Scene/FreeCameraController.cs | 79 +++--------- .../Graphics/Scene/ICameraController.cs | 93 -------------- 6 files changed, 98 insertions(+), 325 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 0323903de..531655013 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -11,6 +11,7 @@ using Maple2.Tools.VectorMath; using Maple2.Tools.Collision; using Silk.NET.Maths; +using Maple2.Server.DebugGame.Graphics.Scene; namespace Maple2.Server.DebugGame.Graphics; @@ -76,10 +77,12 @@ public void 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 - if (!Context.CameraController.IsFollowingPlayer && !Context.HasManuallyStopped) { + bool isFollowing = Context.CameraController is FollowCameraController { IsFollowingPlayer: true }; + if (!isFollowing && !Context.HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); - if (firstPlayer != null && (!hasTriedAutoFollow || Context.CameraController.FollowedPlayerId != firstPlayer.Value.Character.Id)) { - Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + long? currentFollowedId = Context.CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; + if (firstPlayer != null && (!hasTriedAutoFollow || currentFollowedId != firstPlayer.Value.Character.Id)) { + Context.StartFollowingPlayer(firstPlayer); hasTriedAutoFollow = true; } } @@ -511,10 +514,11 @@ public void AttachWindow(DebugFieldWindow window) { // Auto-follow first player when field window is opened // But only if user hasn't manually stopped following - if (!Context.CameraController.IsFollowingPlayer && !Context.HasManuallyStopped) { + bool isFollowing = Context.CameraController is FollowCameraController { IsFollowingPlayer: true }; + if (!isFollowing && !Context.HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.StartFollowingPlayer(firstPlayer); } } } @@ -533,7 +537,6 @@ private void RenderField3DVisualization(double delta) { } // Set up 3D rendering matrices - Context.UpdateViewMatrix(); Context.UpdateProjectionMatrix(); // Enable wireframe mode for field visualization @@ -879,8 +882,13 @@ private void RenderVisualizationControls() { // Camera controls ImGui.Text("Camera Controls:"); if (ImGui.Button("Reset Camera")) { - // Reset camera to default rotation for MapleStory 2 - Context.CameraController.SetDefaultRotation(); + // Reset camera to default rotation + if (Context.CameraController is FreeCameraController freeCam) { + freeCam.SetDefaultRotation(); + } + // else if (Context.CameraController is FollowCameraController followCam) { + // followCam.SetDefaultRotation(); + // } } ImGui.SameLine(); @@ -894,9 +902,11 @@ private void RenderVisualizationControls() { // Player follow controls ImGui.Separator(); - ImGui.Text($"Player Follow: {(Context.CameraController.IsFollowingPlayer ? $"Player ID: {Context.CameraController.FollowedPlayerId}" : string.Empty)}"); + bool isFollowing = Context.CameraController is FollowCameraController { IsFollowingPlayer: true }; + long? followedId = Context.CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; + ImGui.Text($"Player Follow: {(isFollowing ? $"Player ID: {followedId}" : string.Empty)}"); - if (Context.CameraController.IsFollowingPlayer) { + if (isFollowing) { if (ImGui.Button("Stop Following")) { Context.StopFollowingPlayer(); } @@ -904,20 +914,24 @@ private void RenderVisualizationControls() { if (ImGui.Button("Follow First Player")) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.StartFollowingPlayer(firstPlayer.Value.Character.Id); + Context.StartFollowingPlayer(firstPlayer); } } } - if (Context.CameraController.IsFollowingPlayer) { - ImGui.Text($"Player Following Position: {Context.CameraController.CameraTarget}"); + if (isFollowing) { + // Get target from concrete controller if available + Vector3 target = Context.CameraController switch { + FreeCameraController freeCam => freeCam.CameraTarget, + FollowCameraController followCam => followCam.CameraTarget, + _ => Vector3.Zero, + }; + ImGui.Text($"Player Following Position: {target}"); } - // Camera position display - ImGui.Text($"Camera Position: {Context.CameraController.CameraPosition}"); + ImGui.Text($"Camera Position: {Context.CameraController.Camera.Transform.Position}"); - // Camera rotation display - Quaternion q = Context.CameraController.CameraRotation; - ImGui.Text($"Camera Rotation: X={q.X:F3}, Y={q.Y:F3}, Z={q.Z:F3}, W={q.W:F3}"); + Vector3 forward = Context.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")}"); } @@ -926,7 +940,7 @@ private void RenderVisualizationControls() { private void SetFieldOverviewCamera() { // Stop following player when switching to field overview - if (Context.CameraController.IsFollowingPlayer) { + if (Context.CameraController is FollowCameraController { IsFollowingPlayer: true }) { Context.StopFollowingPlayer(); } @@ -948,23 +962,29 @@ private void SetFieldOverviewCamera() { Vector3 center = Vector3.Transform(centerMap, MapRotation); float distance = Math.Max(sizeMap.X, Math.Max(sizeMap.Y, sizeMap.Z)) * 1.5f; - Context.CameraController.SetCameraTarget(center); - Context.CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); + // Set camera target and position for field overview + if (Context.CameraController is FreeCameraController freeCam) { + freeCam.SetCameraTarget(center); + freeCam.SetFieldOverviewRotation(); + Context.CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); + } } else { // Fallback if no colliders found - Context.CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); - Context.CameraController.SetCameraTarget(Vector3.Zero); + if (Context.CameraController is FreeCameraController freeCam) { + freeCam.SetCameraTarget(Vector3.Zero); + freeCam.SetFieldOverviewRotation(); + Context.CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); + } } - Context.CameraController.SetFieldOverviewRotation(); // Use field overview rotation } private void UpdateCameraFollow() { - if (!Context.CameraController.IsFollowingPlayer || Context.CameraController.FollowedPlayerId == null) { + if (Context.CameraController is not FollowCameraController { IsFollowingPlayer: true } followCam || followCam.FollowedPlayerId == null) { return; } // Find the player being followed - FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == Context.CameraController.FollowedPlayerId); + FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == followCam.FollowedPlayerId); if (followedPlayer == null) { // Player not found, stop following Context.StopFollowingPlayer(); diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index a28d48b9b..08bbe50b1 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -14,6 +14,7 @@ using Maple2.Tools; using Maple2.Tools.Extensions; using System.Numerics; +using Maple2.Server.Game.Model; namespace Maple2.Server.DebugGame.Graphics; @@ -316,7 +317,7 @@ private void OnLoad() { Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; sharedCamera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); - CameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 + freeCameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); @@ -415,18 +416,13 @@ public void UpdateProjectionMatrix() { CameraController.Camera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); } - public void UpdateViewMatrix() { - CameraController.Camera.UpdateViewMatrix(); - } - /// /// Switches to the free camera controller /// private void SwitchToFreeCameraController() { if (CameraController != freeCameraController) { - Logger.Information("Switching from {OldController} to FreeCameraController", GetCurrentControllerType()); CameraController = freeCameraController; - Logger.Information("Switched to free camera controller - IsFollowing={IsFollowing}", CameraController.IsFollowingPlayer); + Logger.Information("Switched to free camera controller"); } else { Logger.Information("Already using FreeCameraController - no switch needed"); } @@ -461,25 +457,19 @@ public string GetCurrentControllerType() { /// /// Starts following a player and automatically switches to follow camera controller /// - public void StartFollowingPlayer(long playerId) { + public void StartFollowingPlayer(FieldPlayer player) { hasManuallyStoppedFollowing = false; // Reset manual stop flag when starting to follow SwitchToFollowCameraController(); - followCameraController.StartFollowingPlayer(playerId); + followCameraController.StartFollowingPlayer(player.Value.Character.Id, player.Position); } /// /// Stops following a player and automatically switches to free camera controller /// public void StopFollowingPlayer() { - Logger.Information("StopFollowingPlayer called - before: IsFollowing={IsFollowing}, Controller={Controller}", - CameraController.IsFollowingPlayer, GetCurrentControllerType()); - hasManuallyStoppedFollowing = true; // Remember that user manually stopped followCameraController.StopFollowingPlayer(); SwitchToFreeCameraController(); - - Logger.Information("StopFollowingPlayer called - after: IsFollowing={IsFollowing}, Controller={Controller}, HasManuallyStopped={HasManuallyStopped}", - CameraController.IsFollowingPlayer, GetCurrentControllerType(), HasManuallyStopped); } /// diff --git a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs index 1e323cb07..429e59c12 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs @@ -5,50 +5,49 @@ namespace Maple2.Server.DebugGame.Graphics.Scene; public class Camera { public Transform Transform { get; init; } = new Transform(); - public Matrix4x4 ProjectionMatrix { get; private set; } = Matrix4x4.Identity; - public Matrix4x4 ViewMatrix { get; private set; } = Matrix4x4.Identity; public float AspectRatio { get; private set; } public float NearPlane { get; private set; } public float FarPlane { get; private set; } public float FieldOfView { get; private set; } + /// + /// 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); + } + } + /// /// Updates the projection matrix based on window size /// public void UpdateProjectionMatrix(int windowWidth, int windowHeight) { float aspectRatio = (float) windowWidth / windowHeight; - ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView( - MathF.PI / 4.0f, // 45 degree field of view - aspectRatio, - 1.0f, // near plane - 50000.0f // far plane - much larger for big fields - ); AspectRatio = aspectRatio; FieldOfView = MathF.PI / 4.0f; NearPlane = 1.0f; FarPlane = 50000.0f; } - /// - /// Updates the view matrix based on current camera transform - /// - public void UpdateViewMatrix() { - // Use Transform to calculate view matrix - Vector3 position = Transform.Position; - Vector3 forward = Transform.FrontAxis; - Vector3 up = Transform.UpAxis; - - ViewMatrix = Matrix4x4.CreateLookAt(position, position + forward, up); - } - public void SetProperties(float fieldOfView, float aspectRatio, float nearPlane, float farPlane) { FieldOfView = fieldOfView; AspectRatio = aspectRatio; NearPlane = nearPlane; FarPlane = farPlane; - - ProjectionMatrix = Matrix4x4.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlane, farPlane); } public void SetProperties(float width, float height, float projectionPlane, float nearPlane, float farPlane) { diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs index 54765fe96..c828d3751 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FollowCameraController.cs @@ -9,28 +9,15 @@ public class FollowCameraController : ICameraController { public Camera Camera { get; } public InputState InputState { get; } = new(); - - // Camera follow system public bool IsFollowingPlayer { get; private set; } public long? FollowedPlayerId { get; private set; } - public bool HasManuallyStopped { get; private set; } // Default camera follow offset private static readonly Vector3 CameraFollowOffset = new Vector3(-800, -1000, 1200); - // Default camera rotation for MapleStory 2 coordinate system - private static readonly Quaternion DefaultCameraRotation = new Quaternion(0.130f, 0.325f, 0.870f, 0.350f); - - // Field overview camera rotation - private static readonly Quaternion FieldOverviewRotation = new Quaternion(0.000f, 0.450f, 0.900f, 0.000f); - public Vector3 CameraTarget { get; private set; } = Vector3.Zero; - // Camera properties that delegate to Camera.Transform - public Vector3 CameraPosition => Camera.Transform.Position; - public Quaternion CameraRotation => Camera.Transform.Quaternion; - public FollowCameraController(Camera camera) { Camera = camera; } @@ -38,11 +25,14 @@ public FollowCameraController(Camera camera) { /// /// Starts following a specific player /// - public void StartFollowingPlayer(long playerId) { + public void StartFollowingPlayer(long playerId, Vector3 playerPosition) { IsFollowingPlayer = true; FollowedPlayerId = playerId; - HasManuallyStopped = false; // Reset manual stop flag when starting to follow - SetDefaultRotation(); // Set default rotation when starting to follow + + // Set initial camera position and look at player only once + Camera.Transform.Position = playerPosition + CameraFollowOffset; + SetCameraTarget(playerPosition); + Logger.Information("Started following player {PlayerId}", playerId); } @@ -52,29 +42,17 @@ public void StartFollowingPlayer(long playerId) { public void StopFollowingPlayer() { IsFollowingPlayer = false; FollowedPlayerId = null; - HasManuallyStopped = true; // Remember that user manually stopped Logger.Information("Stopped following player"); } - /// - /// Unlocks camera follow when manual movement is detected - /// - public void UnlockCameraFollow() { - if (IsFollowingPlayer) { - IsFollowingPlayer = false; - Logger.Information("Camera follow unlocked - manual movement detected"); - } - } - /// /// Updates camera to follow a player at the specified position /// public void UpdatePlayerFollow(Vector3 playerPosition) { if (!IsFollowingPlayer) return; - SetCameraTarget(playerPosition); + // Only update camera position, keep rotation fixed from initial setup Camera.Transform.Position = playerPosition + CameraFollowOffset; - SetDefaultRotation(); // Always use default rotation when following player } /// @@ -84,38 +62,10 @@ public void SetCameraTarget(Vector3 target) { CameraTarget = target; Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); - // Calculate rotation to look at target - Vector3 up = Vector3.UnitZ; // Z is up in our coordinate system - Vector3 right = Vector3.Normalize(Vector3.Cross(direction, up)); - up = Vector3.Cross(right, direction); - - Matrix4x4 lookAtMatrix = new Matrix4x4( - right.X, up.X, -direction.X, 0, - right.Y, up.Y, -direction.Y, 0, - right.Z, up.Z, -direction.Z, 0, - 0, 0, 0, 1 - ); - - Camera.Transform.Transformation = lookAtMatrix; - } - - /// - /// Sets the camera to the default rotation for MapleStory 2 - /// - public void SetDefaultRotation() { - Camera.Transform.Quaternion = DefaultCameraRotation; - } - - /// - /// Moves the camera target relative to its current position - /// - public void MoveCameraTargetRelative(Vector3 offset) { - CameraTarget += offset; + // Use Transform's LookTo method instead of manual matrix calculation + Camera.Transform.LookTo(direction, Vector3.UnitZ, snapToGroundPlane: false); } - // ICameraController interface methods that are not follow-specific - // These provide minimal implementations since this controller focuses on following - /// /// Updates the controller - follow controllers don't need delta-based updates /// @@ -123,52 +73,4 @@ public void Update(float delta) { // Follow controller doesn't need frame-based updates // Following is updated via UpdatePlayerFollow when player position changes } - - /// - /// Sets the camera to the field overview rotation - /// - public void SetFieldOverviewRotation() { - Camera.Transform.Quaternion = FieldOverviewRotation; - } - - /// - /// Rotates the camera around the target point - minimal implementation for follow controller - /// - public void RotateCamera(float yawDelta, float pitchDelta) { - // Follow controller maintains fixed camera orientation relative to player - // Manual rotation would break following, so this is intentionally minimal - } - - /// - /// Moves the camera relative to its current position - minimal implementation for follow controller - /// - public void MoveCameraRelative(Vector3 offset) { - // Follow controller maintains fixed camera position relative to player - // Manual movement would break following, so this is intentionally minimal - } - - - - /// - /// 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; - SetDefaultRotation(); - } - - /// - /// 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; - - // Create a flip matrix that inverts the Y axis - Matrix4x4 flipMatrix = Matrix4x4.CreateScale(1, -1, 1); - - Camera.Transform.Transformation = currentTransform * flipMatrix; - } } diff --git a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs index 998aedc92..baef90090 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/FreeCameraController.cs @@ -12,16 +12,6 @@ public class FreeCameraController : ICameraController { public InputState InputState { get; } = new(); public Camera Camera { get; } - - - // Camera properties that delegate to Camera.Transform - public Vector3 CameraPosition => Camera.Transform.Position; - public Quaternion CameraRotation => Camera.Transform.Quaternion; - - // Free camera controller doesn't support following - these return default values - public bool IsFollowingPlayer => false; - public long? FollowedPlayerId => null; - public bool HasManuallyStopped => false; public Vector3 CameraTarget { get; private set; } = Vector3.Zero; private const Key MoveForward = Key.W; @@ -40,10 +30,20 @@ public float RotationSpeedDegrees { } // Default camera rotation for MapleStory 2 coordinate system - private static readonly Quaternion DefaultCameraRotation = new Quaternion(0.130f, 0.325f, 0.870f, 0.350f); + 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 Quaternion FieldOverviewRotation = new Quaternion(0.000f, 0.450f, 0.900f, 0.000f); + 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; @@ -53,14 +53,14 @@ public FreeCameraController(Camera camera) { /// Sets the camera to the default rotation for MapleStory 2 /// public void SetDefaultRotation() { - Camera.Transform.Quaternion = DefaultCameraRotation; + Camera.Transform.RotationMatrix = DefaultCameraRotation; } /// /// Sets the camera to the field overview rotation /// public void SetFieldOverviewRotation() { - Camera.Transform.Quaternion = FieldOverviewRotation; + Camera.Transform.RotationMatrix = FieldOverviewRotation; } public void Update(float delta) { @@ -100,11 +100,7 @@ public void Update(float delta) { currentSpeed *= 5.0f; // 5x speed with Shift } - if (moveDirection.LengthSquared() > 0) { - Camera.Transform.Position += delta * currentSpeed * (moveDirection.X * Camera.Transform.RightAxis + moveDirection.Y * Camera.Transform.FrontAxis + moveDirection.Z * Camera.Transform.UpAxis); - - // Free camera controller doesn't support following - } + 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; @@ -112,11 +108,6 @@ public void Update(float delta) { Vector3 mouseDelta = InputState.MousePosition.Delta; - // Check if there's actual mouse movement - if (mouseDelta.X != 0 || mouseDelta.Y != 0) { - // Free camera controller doesn't support following - } - float currentPitch = -float.Asin(Camera.Transform.FrontAxis.Z); float currentYaw = -float.Atan2(-Camera.Transform.FrontAxis.X, -Camera.Transform.FrontAxis.Y); @@ -132,8 +123,6 @@ public void Update(float delta) { InputState.MousePosition.Lock = true; } - - /// /// Rotates the camera around the target point /// @@ -168,27 +157,6 @@ public void MoveCameraRelative(Vector3 offset) { CameraTarget += offset; } - /// - /// Starts following a specific player - not supported by free camera controller - /// - public void StartFollowingPlayer(long playerId) { - // Free camera controller doesn't support following - } - - /// - /// Stops following the current player - not supported by free camera controller - /// - public void StopFollowingPlayer() { - // Free camera controller doesn't support following - } - - /// - /// Updates camera to follow a player at the specified position - not supported by free camera controller - /// - public void UpdatePlayerFollow(Vector3 playerPosition) { - // Free camera controller doesn't support following - } - /// /// Sets the camera target /// @@ -196,23 +164,10 @@ public void SetCameraTarget(Vector3 target) { CameraTarget = target; Vector3 direction = Vector3.Normalize(target - Camera.Transform.Position); - // Calculate rotation to look at target - Vector3 up = Vector3.UnitZ; // Z is up in our coordinate system - Vector3 right = Vector3.Normalize(Vector3.Cross(direction, up)); - up = Vector3.Cross(right, direction); - - Matrix4x4 lookAtMatrix = new Matrix4x4( - right.X, up.X, -direction.X, 0, - right.Y, up.Y, -direction.Y, 0, - right.Z, up.Z, -direction.Z, 0, - 0, 0, 0, 1 - ); - - Camera.Transform.Transformation = lookAtMatrix; + // 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 /// diff --git a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs index 53ce7451f..418b21f9a 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs @@ -2,9 +2,6 @@ namespace Maple2.Server.DebugGame.Graphics.Scene; -/// -/// Interface for camera controllers that can manipulate a camera -/// public interface ICameraController { /// /// The camera being controlled @@ -16,99 +13,9 @@ public interface ICameraController { /// InputState InputState { get; } - - - /// - /// Current camera position - /// - Vector3 CameraPosition { get; } - - /// - /// Current camera rotation - /// - Quaternion CameraRotation { get; } - - /// - /// Current camera target position - /// - Vector3 CameraTarget { get; } - - /// - /// Whether the controller is currently following a player - /// - bool IsFollowingPlayer { get; } - - /// - /// ID of the player being followed (if any) - /// - long? FollowedPlayerId { get; } - - /// - /// Whether the user has manually stopped following - /// - bool HasManuallyStopped { get; } - /// /// Updates the controller with the given delta time /// /// Time elapsed since last update void Update(float delta); - - /// - /// Sets the camera to the default rotation for MapleStory 2 - /// - void SetDefaultRotation(); - - /// - /// Sets the camera to the field overview rotation - /// - void SetFieldOverviewRotation(); - - /// - /// Sets the camera target position - /// - /// Target position - void SetCameraTarget(Vector3 target); - - /// - /// Rotates the camera around the target point - /// - /// Yaw rotation delta - /// Pitch rotation delta - void RotateCamera(float yawDelta, float pitchDelta); - - /// - /// Moves the camera relative to its current position - /// - /// Position offset - void MoveCameraRelative(Vector3 offset); - - /// - /// Starts following a specific player - /// - /// ID of the player to follow - void StartFollowingPlayer(long playerId); - - /// - /// Stops following the current player - /// - void StopFollowingPlayer(); - - /// - /// Updates camera to follow a player at the specified position - /// - /// Player position - void UpdatePlayerFollow(Vector3 playerPosition); - - - - /// - /// Sets camera orientation that works well with MapleStory 2's coordinate system - /// - void SetCameraOrientationForMapData(); - - /// - /// Flips the camera's up vector to fix upside-down view - /// - void FlipCameraUpVector(); } From 4c4a73a51bfa43a3475154bde4f989c0f0859450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 9 Aug 2025 01:53:37 -0300 Subject: [PATCH 19/25] Refactor --- .../Graphics/Assets/ShaderPipelines.cs | 3 +- .../Graphics/Data/InstanceBuffer.cs | 1 + .../Graphics/DebugFieldRenderer.cs | 328 ++++++++++++------ .../Graphics/DebugFieldWindow.cs | 68 +++- .../Graphics/DebugGraphicsContext.cs | 322 +++++------------ .../Graphics/Resources/Shader.cs | 2 - .../Maple2.Server.DebugGame.csproj | 149 +++++--- Maple2.Server.DebugGame/Program.cs | 4 +- .../RenderPipeline/Wireframe.xml | 10 + .../Shaders/Common/instance.hlsl | 6 + .../Shaders/Common/pixelInput.hlsl | 8 + .../Shaders/Common/sceneInput.hlsl | 4 + .../Shaders/Common/sceneView.hlsl | 5 + .../Shaders/Common/vertexAttributes.hlsl | 44 +++ .../Shaders/Pipelines/DebugScene/pixel.hlsl | 10 + .../Shaders/Pipelines/DebugScene/vertex.hlsl | 18 + .../Shaders/Pipelines/Screen/pixel.hlsl | 10 + .../Shaders/Pipelines/Screen/vertex.hlsl | 15 + .../Shaders/Pipelines/Wireframe/pixel.hlsl | 8 + .../Shaders/Pipelines/Wireframe/vertex.hlsl | 29 ++ Maple2.Server.DebugGame/root_path.txt | 1 + 21 files changed, 635 insertions(+), 410 deletions(-) create mode 100644 Maple2.Server.DebugGame/RenderPipeline/Wireframe.xml create mode 100644 Maple2.Server.DebugGame/Shaders/Common/instance.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Common/pixelInput.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Common/sceneInput.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Common/sceneView.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Common/vertexAttributes.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/pixel.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Pipelines/DebugScene/vertex.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Pipelines/Screen/pixel.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Pipelines/Screen/vertex.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/pixel.hlsl create mode 100644 Maple2.Server.DebugGame/Shaders/Pipelines/Wireframe/vertex.hlsl create mode 100644 Maple2.Server.DebugGame/root_path.txt diff --git a/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs index 4491af421..dfe8afec8 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs @@ -148,7 +148,8 @@ public void LoadPipelines() { InitializeFileWatchers(pipelineRoot, shaderRoot); - foreach (FileInfo file in directory.EnumerateFiles()) { + IEnumerable enumerateFiles = directory.EnumerateFiles(); + foreach (FileInfo file in enumerateFiles) { string relpath = Path.GetRelativePath(pipelineRoot, file.FullName); currentDocument = relpath; diff --git a/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs b/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs index 9eedf31fd..be6b9305c 100644 --- a/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs +++ b/Maple2.Server.DebugGame/Graphics/Data/InstanceBuffer.cs @@ -5,4 +5,5 @@ 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/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 531655013..542d46171 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -5,6 +5,7 @@ using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata.FieldEntity; +using Maple2.Server.DebugGame.Graphics.Data; using Maple2.Server.Game.Manager; using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; @@ -12,6 +13,7 @@ using Maple2.Tools.Collision; using Silk.NET.Maths; using Maple2.Server.DebugGame.Graphics.Scene; +using Serilog; namespace Maple2.Server.DebugGame.Graphics; @@ -21,6 +23,8 @@ public class DebugFieldRenderer : IFieldRenderer { // 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 { @@ -59,9 +63,37 @@ public bool IsActive { 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) + private bool hasManuallyStoppedFollowing = false; + private bool hasTriedAutoFollow = false; + 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() { @@ -77,24 +109,20 @@ public void 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 = Context.CameraController is FollowCameraController { IsFollowingPlayer: true }; - if (!isFollowing && !Context.HasManuallyStopped) { + bool isFollowing = CameraController is FollowCameraController { IsFollowingPlayer: true }; + if (!isFollowing && !HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); - long? currentFollowedId = Context.CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; + long? currentFollowedId = CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; if (firstPlayer != null && (!hasTriedAutoFollow || currentFollowedId != firstPlayer.Value.Character.Id)) { - Context.StartFollowingPlayer(firstPlayer); + StartFollowingPlayer(firstPlayer); hasTriedAutoFollow = true; } } // Update camera follow if active UpdateCameraFollow(); - - // We'll use ImGui for text rendering instead of 3D billboards - // ImGui is much simpler and more reliable for debug text } - private bool hasTriedAutoFollow = false; public void Render(double delta) { if (!IsActive) { @@ -131,7 +159,7 @@ public void Render(double delta) { } // New method specifically for field window 3D rendering - public void RenderFieldWindow3D(double delta) { + public void RenderFieldWindow3D(DebugFieldWindow window, double delta) { if (!IsActive) { return; } @@ -140,7 +168,7 @@ public void RenderFieldWindow3D(double delta) { HandleMouseInput(); // Render 3D field visualization using the DirectX pipeline - RenderField3DVisualization(delta); + RenderField3DVisualization(window, delta); } private void HandleMouseInput() { @@ -514,11 +542,11 @@ public void AttachWindow(DebugFieldWindow window) { // Auto-follow first player when field window is opened // But only if user hasn't manually stopped following - bool isFollowing = Context.CameraController is FollowCameraController { IsFollowingPlayer: true }; - if (!isFollowing && !Context.HasManuallyStopped) { + bool isFollowing = CameraController is FollowCameraController { IsFollowingPlayer: true }; + if (!isFollowing && !HasManuallyStopped) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.StartFollowingPlayer(firstPlayer); + StartFollowingPlayer(firstPlayer); } } } @@ -531,62 +559,61 @@ public void DetachWindow(DebugFieldWindow window) { activeMutex.ReleaseMutex(); } - private void RenderField3DVisualization(double delta) { + private void RenderField3DVisualization(DebugFieldWindow window, double delta) { if (Field.AccelerationStructure == null) { return; } // Set up 3D rendering matrices - Context.UpdateProjectionMatrix(); + UpdateProjectionMatrix(window); // Enable wireframe mode for field visualization bool originalWireframeMode = Context.WireframeMode; Context.WireframeMode = true; Context.SetWireframeRasterizer(); - // Always use wireframe shaders for field visualization (they don't need texture samplers) - Context.WireframeVertexShader?.Bind(); - Context.WireframePixelShader?.Bind(); + Context.WireframePipeline!.RenderPass!.VertexShader!.Bind(); + Context.WireframePipeline.RenderPass.PixelShader!.Bind(); // Render field entities using the DirectX 11 pipeline - RenderFieldEntities3D(); + RenderFieldEntities3D(window); // Restore original wireframe mode Context.WireframeMode = originalWireframeMode; Context.SetSolidRasterizer(); } - private void RenderFieldEntities3D() { + private void RenderFieldEntities3D(DebugFieldWindow window) { if (Field.AccelerationStructure == null) { return; } // Render box colliders if (showBoxColliders) { - RenderBoxColliders(); + RenderBoxColliders(window); } // Render mesh colliders if (showMeshColliders) { - RenderMeshColliders(); + RenderMeshColliders(window); } // Render spawn points if (showSpawnPoints) { - RenderSpawnPoints(); + RenderSpawnPoints(window); } // Render vibrate objects if (showVibrateObjects) { - RenderVibrateObjects(); + RenderVibrateObjects(window); } // Render portals if (showPortals) { - RenderPortals(); + RenderPortals(window); if (showPortalConnections) { - RenderPortalConnectionLines(); + RenderPortalConnectionLines(window); } // Render portal text labels @@ -597,11 +624,11 @@ private void RenderFieldEntities3D() { // Render actors (players, NPCs, mobs) if (showActors) { - RenderActors(); + RenderActors(window); // Render selection highlight for selected actor if (selectedActor != null) { - RenderActorSelectionHighlight(); + RenderActorSelectionHighlight(window); } // Render text labels above actors using ImGui @@ -609,9 +636,9 @@ private void RenderFieldEntities3D() { } } - private void RenderBoxColliders() { + private void RenderBoxColliders(DebugFieldWindow window) { // Set white color for box colliders - Context.SetColor(1, 1, 1, 1); // White for box colliders + instanceBuffer.Color = new Vector4(1, 1, 1, 1); // White for box colliders List allBoxColliders = GetAllEntities().OfType().ToList(); @@ -622,15 +649,15 @@ private void RenderBoxColliders() { }; transform.Transformation *= MapRotation; - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(boxCollider.Size) * - Matrix4x4.CreateTranslation(boxCollider.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(boxCollider.Size) * + Matrix4x4.CreateTranslation(boxCollider.Position)); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (white) + UpdateWireframeInstance(window); Context.CoreModels!.WireCube.Draw(); } } - private void RenderMeshColliders() { + private void RenderMeshColliders(DebugFieldWindow window) { IEnumerable allMeshColliders = GetAllEntities().OfType(); foreach (FieldMeshColliderEntity meshCollider in allMeshColliders) { @@ -646,66 +673,66 @@ private void RenderMeshColliders() { var coordinateTransform = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)); rotation = coordinateTransform * rotation; - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * - Matrix4x4.CreateTranslation(center); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(size) * + Matrix4x4.CreateTranslation(center)); - Context.UpdateConstantBuffer(worldMatrix); + UpdateWireframeInstance(window); Context.CoreModels!.WireCube.Draw(); } } - private void RenderSpawnPoints() { + private void RenderSpawnPoints(DebugFieldWindow window) { IEnumerable allSpawnPoints = GetAllEntities().OfType(); foreach (FieldSpawnTile spawnPoint in allSpawnPoints) { // Convert Euler angles (degrees) to quaternion - Vector3 rotationRadians = spawnPoint.Rotation * (MathF.PI / 180.0f); - var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); + // Vector3 rotationRadians = spawnPoint.Rotation * (MathF.PI / 180.0f); + // var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 50.0f) * - Matrix4x4.CreateTranslation(spawnPoint.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(Vector3.One * 50.0f) * + Matrix4x4.CreateTranslation(spawnPoint.Position)); - Context.UpdateConstantBuffer(worldMatrix); + UpdateWireframeInstance(window); Context.CoreModels!.WireCube.Draw(); } } - private void RenderVibrateObjects() { + private void RenderVibrateObjects(DebugFieldWindow window) { // Set pink color for vibrate objects - Context.SetColor(1, 0.75f, 0.8f, 1); // Pink for vibrate objects + instanceBuffer.Color = new Vector4(1, 0.75f, 0.8f, 1); // Pink for vibrate objects foreach (FieldVibrateEntity vibrateEntity in Field.AccelerationStructure!.VibrateEntities) { // Convert Euler angles (degrees) to quaternion Vector3 rotationRadians = vibrateEntity.Rotation * (MathF.PI / 180.0f); var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(Vector3.One * 100.0f) * - Matrix4x4.CreateTranslation(vibrateEntity.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(Vector3.One * 100.0f) * + Matrix4x4.CreateTranslation(vibrateEntity.Position)); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (pink) + UpdateWireframeInstance(window); Context.CoreModels!.WireCube.Draw(); } } - private void RenderPortals() { + private void RenderPortals(DebugFieldWindow window) { // Set blue color for portals - Context.SetColor(0, 0.5f, 1, 1); // Blue for portals + instanceBuffer.Color = new Vector4(0, 0.5f, 1, 1); // Blue for portals foreach (FieldPortal portal in Field.GetPortals()) { // Use portal dimensions if available, otherwise use default size Vector3 size = portal.Value.Dimension != Vector3.Zero ? portal.Value.Dimension : Vector3.One * 100.0f; - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(size) * - Matrix4x4.CreateTranslation(portal.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(size) * + Matrix4x4.CreateTranslation(portal.Position)); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (blue) + UpdateWireframeInstance(window); Context.CoreModels!.WireCube.Draw(); } } - private void RenderPortalConnectionLines() { + private void RenderPortalConnectionLines(DebugFieldWindow window) { // Set cyan color for portal connection lines - Context.SetColor(0, 1, 1, 0.7f); // Cyan with some transparency + instanceBuffer.Color = new Vector4(0, 1, 1, 0.7f); // Cyan with some transparency List portals = Field.GetPortals().ToList(); @@ -717,13 +744,13 @@ private void RenderPortalConnectionLines() { if (targetPortal != null) { // Draw a line between the two portals - RenderLine(sourcePortal.Position, targetPortal.Position); + RenderLine(window, sourcePortal.Position, targetPortal.Position); } } } } - private void RenderLine(Vector3 start, Vector3 end) { + 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(); @@ -750,29 +777,29 @@ private void RenderLine(Vector3 start, Vector3 end) { rotation = Quaternion.CreateFromAxisAngle(axis, angle); } - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(10.0f, distance / 2.0f, 10.0f)) * - Matrix4x4.CreateFromQuaternion(rotation) * - Matrix4x4.CreateTranslation(center); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(10.0f, distance / 2.0f, 10.0f)) * + Matrix4x4.CreateFromQuaternion(rotation) * + Matrix4x4.CreateTranslation(center)); - Context.UpdateConstantBuffer(worldMatrix, true); + UpdateWireframeInstance(window); Context.CoreModels!.Cylinder.Draw(); } - private void RenderActors() { + private void RenderActors(DebugFieldWindow window) { const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; // Render players using cylinders (like Recast agents) - Cyan color if (showPlayers) { - Context.SetColor(0, 1, 1, 1); // Cyan for players + 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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * - rotation * - Matrix4x4.CreateTranslation(player.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(player.Position)); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (cyan) + UpdateWireframeInstance(window); Context.CoreModels!.Cylinder.Draw(); // Use cylinder for players (like Recast agents) } } @@ -782,50 +809,51 @@ private void RenderActors() { foreach ((int _, FieldNpc npc) in Field.Npcs) { // Set color based on dead status if (npc.IsDead) { - Context.SetColor(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead NPCs + instanceBuffer.Color = new Vector4(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead NPCs } else { - Context.SetColor(0, 1, 0, 1); // Green for alive NPCs + instanceBuffer.Color = new Vector4(0, 1, 0, 1); // Green for alive NPCs } // 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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * - rotation * - Matrix4x4.CreateTranslation(npc.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(npc.Position)); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (green/gray) + UpdateWireframeInstance(window); Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs (like Recast agents) } } + // Render Mobs using cylinders (like Recast agents) - Red for aggressive, Yellow for wandering, Gray for dead if (showMobs) { foreach ((int _, FieldNpc mob) in Field.Mobs) { // Set color based on dead status and battle state if (mob.IsDead) { - Context.SetColor(0.5f, 0.5f, 0.5f, 0.3f); // Gray and semi-transparent for dead mobs + 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) { - Context.SetColor(1, 0, 0, 1); // Red for aggressive mobs + instanceBuffer.Color = new Vector4(1, 0, 0, 1); // Red for aggressive mobs } else { - Context.SetColor(1, 1, 0, 1); // Yellow for wandering mobs + instanceBuffer.Color = new Vector4(1, 1, 0, 1); // Yellow for wandering mobs } } // 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)); - Matrix4x4 worldMatrix = Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * - rotation * - Matrix4x4.CreateTranslation(mob.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(agentRadius, agentHeight, agentRadius)) * + rotation * + Matrix4x4.CreateTranslation(mob.Position)); - Context.UpdateConstantBuffer(worldMatrix, true); // Use current color (red/yellow/gray) + UpdateWireframeInstance(window); Context.CoreModels!.Cylinder.Draw(); // Use cylinder for mobs (like Recast agents) } } } - private void RenderActorSelectionHighlight() { + private void RenderActorSelectionHighlight(DebugFieldWindow window) { if (selectedActor == null) return; const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; @@ -835,15 +863,15 @@ private void RenderActorSelectionHighlight() { var rotation = Matrix4x4.CreateRotationX((float) (Math.PI / 2)); // Render a second, even larger outline with pulsing effect - Context.SetColor(1, 1, 0, 1); // Yellow with pulsing alpha + instanceBuffer.Color = new Vector4(1, 1, 0, 1); // Yellow with pulsing alpha float pulseRadius = agentRadius * 1.4f; float pulseHeight = agentHeight * 1.2f; - Matrix4x4 pulseWorldMatrix = Matrix4x4.CreateScale(new Vector3(pulseRadius, pulseHeight, pulseRadius)) * - rotation * - Matrix4x4.CreateTranslation(selectedActor.Position); + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(pulseRadius, pulseHeight, pulseRadius)) * + rotation * + Matrix4x4.CreateTranslation(selectedActor.Position)); - Context.UpdateConstantBuffer(pulseWorldMatrix, true); + UpdateWireframeInstance(window); Context.CoreModels!.Cylinder.Draw(); } @@ -883,10 +911,10 @@ private void RenderVisualizationControls() { ImGui.Text("Camera Controls:"); if (ImGui.Button("Reset Camera")) { // Reset camera to default rotation - if (Context.CameraController is FreeCameraController freeCam) { + if (CameraController is FreeCameraController freeCam) { freeCam.SetDefaultRotation(); } - // else if (Context.CameraController is FollowCameraController followCam) { + // else if (CameraController is FollowCameraController followCam) { // followCam.SetDefaultRotation(); // } } @@ -902,35 +930,35 @@ private void RenderVisualizationControls() { // Player follow controls ImGui.Separator(); - bool isFollowing = Context.CameraController is FollowCameraController { IsFollowingPlayer: true }; - long? followedId = Context.CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; + bool isFollowing = CameraController is FollowCameraController { IsFollowingPlayer: true }; + long? followedId = CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; ImGui.Text($"Player Follow: {(isFollowing ? $"Player ID: {followedId}" : string.Empty)}"); if (isFollowing) { if (ImGui.Button("Stop Following")) { - Context.StopFollowingPlayer(); + StopFollowingPlayer(); } } else { if (ImGui.Button("Follow First Player")) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { - Context.StartFollowingPlayer(firstPlayer); + StartFollowingPlayer(firstPlayer); } } } if (isFollowing) { // Get target from concrete controller if available - Vector3 target = Context.CameraController switch { + Vector3 target = CameraController switch { FreeCameraController freeCam => freeCam.CameraTarget, FollowCameraController followCam => followCam.CameraTarget, _ => Vector3.Zero, }; ImGui.Text($"Player Following Position: {target}"); } - ImGui.Text($"Camera Position: {Context.CameraController.Camera.Transform.Position}"); + ImGui.Text($"Camera Position: {CameraController.Camera.Transform.Position}"); - Vector3 forward = Context.CameraController.Camera.Transform.FrontAxis; + Vector3 forward = 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")}"); @@ -940,8 +968,8 @@ private void RenderVisualizationControls() { private void SetFieldOverviewCamera() { // Stop following player when switching to field overview - if (Context.CameraController is FollowCameraController { IsFollowingPlayer: true }) { - Context.StopFollowingPlayer(); + if (CameraController is FollowCameraController { IsFollowingPlayer: true }) { + StopFollowingPlayer(); } List allColliders = GetAllEntities().OfType().ToList(); @@ -963,23 +991,23 @@ private void SetFieldOverviewCamera() { float distance = Math.Max(sizeMap.X, Math.Max(sizeMap.Y, sizeMap.Z)) * 1.5f; // Set camera target and position for field overview - if (Context.CameraController is FreeCameraController freeCam) { + if (CameraController is FreeCameraController freeCam) { freeCam.SetCameraTarget(center); freeCam.SetFieldOverviewRotation(); - Context.CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); + CameraController.Camera.Transform.Position = center + new Vector3(0, -distance * 0.7f, distance * 0.7f); } } else { // Fallback if no colliders found - if (Context.CameraController is FreeCameraController freeCam) { + if (CameraController is FreeCameraController freeCam) { freeCam.SetCameraTarget(Vector3.Zero); freeCam.SetFieldOverviewRotation(); - Context.CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); + CameraController.Camera.Transform.Position = new Vector3(0, -1000, 1000); } } } private void UpdateCameraFollow() { - if (Context.CameraController is not FollowCameraController { IsFollowingPlayer: true } followCam || followCam.FollowedPlayerId == null) { + if (CameraController is not FollowCameraController { IsFollowingPlayer: true } followCam || followCam.FollowedPlayerId == null) { return; } @@ -987,13 +1015,13 @@ private void UpdateCameraFollow() { FieldPlayer? followedPlayer = Field.Players.Values.FirstOrDefault(p => p.Value.Character.Id == followCam.FollowedPlayerId); if (followedPlayer == null) { // Player not found, stop following - Context.StopFollowingPlayer(); + StopFollowingPlayer(); return; } // Update camera to follow player position using the camera controller Vector3 playerPosition = followedPlayer.Position; - Context.UpdatePlayerFollow(playerPosition); + UpdatePlayerFollow(playerPosition); } private Vector2D GetFieldWindowSize() { @@ -1016,8 +1044,8 @@ private bool TryWorldToScreen(Vector3 worldPos, out Vector2 screenPos) { screenPos = Vector2.Zero; // Transform world position to screen coordinates - Matrix4x4 viewMatrix = Context.CameraController.Camera.ViewMatrix; - Matrix4x4 projMatrix = Context.CameraController.Camera.ProjectionMatrix; + Matrix4x4 viewMatrix = CameraController.Camera.ViewMatrix; + Matrix4x4 projMatrix = CameraController.Camera.ProjectionMatrix; Matrix4x4 viewProjMatrix = viewMatrix * projMatrix; // Transform to clip space @@ -1059,8 +1087,8 @@ private bool TryScreenToWorldRay(Vector2 screenPos, out Vector3 rayOrigin, out V float ndcY = 1.0f - (screenPos.Y / windowSize.Y) * 2.0f; // Flip Y axis // Get camera matrices - Matrix4x4 viewMatrix = Context.CameraController.Camera.ViewMatrix; - Matrix4x4 projMatrix = Context.CameraController.Camera.ProjectionMatrix; + Matrix4x4 viewMatrix = CameraController.Camera.ViewMatrix; + Matrix4x4 projMatrix = CameraController.Camera.ProjectionMatrix; // Calculate inverse view-projection matrix Matrix4x4 viewProjMatrix = viewMatrix * projMatrix; @@ -1443,4 +1471,88 @@ private FieldEntity[] GetAllEntities() { cachedStaticEntities = allEntities.ToArray(); return cachedStaticEntities; } + + public void UpdateProjectionMatrix(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" + }; + } + + /// + /// Whether the user has manually stopped following (global state) + /// + public bool HasManuallyStopped => hasManuallyStoppedFollowing; + + /// + /// 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 11771c4cd..f4221a551 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -1,4 +1,7 @@ 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; @@ -20,6 +23,10 @@ public class DebugFieldWindow { public string WindowName { get; } public bool IsInitialized { get; private set; } public bool IsClosing { get; private set; } + public SceneState SceneState { get; init; } + + public ConstantBuffer SceneViewConstantBuffer { get; init; } + public ConstantBuffer InstanceConstantBuffer { get; init; } private static int _windowIdCounter; @@ -27,6 +34,11 @@ 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) { @@ -70,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(); } @@ -142,11 +162,11 @@ public 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(); - Context.CameraController.Update((float) delta); + // Update camera input state and let FreeCameraController handle input + UpdateCameraInputState(); + ActiveRenderer.CameraController.Update((float) delta); + } } public unsafe void OnRender(double delta) { @@ -154,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); @@ -173,9 +199,15 @@ public unsafe void OnRender(double delta) { #region Render code // Begin region for render code + // SceneState.BindTexture(Context.SampleTexture!); + + SceneState.BindConstantBuffer(SceneViewConstantBuffer, 0, Enum.ShaderStageFlags.Vertex); + SceneState.BindConstantBuffer(InstanceConstantBuffer, 1, Enum.ShaderStageFlags.Vertex); + SceneState.UpdateBindings(); + // Render 3D field content first (before ImGui) if (ActiveRenderer is not null) { - ActiveRenderer.RenderFieldWindow3D(delta); + ActiveRenderer.RenderFieldWindow3D(this, delta); } // Then render ImGui content @@ -190,6 +222,8 @@ public unsafe void OnRender(double delta) { DxSwapChain.Present(1, 0); + Context.ShaderPipelines?.EndedFrame(); + renderTargetView.Dispose(); framebuffer.Dispose(); @@ -211,7 +245,7 @@ private void LoadField(DebugFieldRenderer renderer) { } private void UpdateCameraInputState() { if (Input == null) return; - InputState inputState = Context.CameraController.InputState; + InputState inputState = ActiveRenderer!.CameraController.InputState; IKeyboard keyboard = Input.Keyboards[0]; IMouse mouse = Input.Mice[0]; @@ -220,10 +254,24 @@ private void UpdateCameraInputState() { // 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 + 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) { diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 08bbe50b1..4d7a09787 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -39,11 +39,13 @@ public class DebugGraphicsContext : IGraphicsContext { public ComPtr DxFactory { get; private set; } private ComPtr DxSwapChain { get; set; } - // Enhanced shader support - private VertexShader? VertexShader { get; set; } - private PixelShader? PixelShader { get; set; } - public VertexShader? WireframeVertexShader { get; private set; } - public PixelShader? WireframePixelShader { get; private 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; } @@ -51,25 +53,12 @@ public class DebugGraphicsContext : IGraphicsContext { private ComPtr DepthStencilState { get; set; } private ComPtr SolidRasterizerState { get; set; } private ComPtr WireframeRasterizerState { get; set; } - private ComPtr ConstantBuffer { get; set; } + // private ComPtr ConstantBuffer { get; set; } public CoreModels? CoreModels { get; private set; } private Texture? sampleTexture; private ImGuiController? ImGuiController { get; set; } - // Camera controller interface for all camera functionality - public ICameraController CameraController { get; private set; } = null!; - - // Available controller implementations - private FreeCameraController freeCameraController = null!; - private FollowCameraController followCameraController = null!; - - // Shared camera instance - private readonly Camera sharedCamera = new(); - - // Global follow state (independent of active controller) - private bool hasManuallyStoppedFollowing = false; - // Rendering state public bool WireframeMode { get; set; } = true; // Start in wireframe mode @@ -101,6 +90,10 @@ public DebugFieldRenderer[] FieldRenderers { private DateTime lastTime = DateTime.Now; private bool IsClosing { get; set; } + public DebugGraphicsContext() { + SceneState = new(this); + } + public void RunDebugger() { DebuggerWindow!.Initialize(); @@ -227,12 +220,8 @@ private void OnLoad() { D3d11 = D3D11.GetApi(DebuggerWindow); Compiler = D3DCompiler.GetApi(); - // Use application base directory where files are copied during build - Shader.ShaderRootPath = Path.Combine(AppContext.BaseDirectory, "Shaders"); - Texture.TextureRootPath = Path.Combine(AppContext.BaseDirectory, "Textures"); - - // Set AssetRootPath for shader include system - ShaderPipelines.AssetRootPath = AppContext.BaseDirectory; + Texture.TextureRootPath = GetResourceRootPath("Textures"); + ShaderPipelines.AssetRootPath = GetResourceRootPath(""); unsafe { ComPtr device = default; @@ -284,23 +273,13 @@ private void OnLoad() { DxSwapChain = swapChain; } - // Create shaders - VertexShader ??= new VertexShader(this); - - PixelShader ??= new PixelShader(this); - - WireframeVertexShader ??= new VertexShader(this); - - WireframePixelShader ??= new PixelShader(this); - - VertexShader.Load(Path.Combine(Shader.ShaderRootPath, "screenVertex.hlsl"), "vs_main"); - PixelShader.Load(Path.Combine(Shader.ShaderRootPath, "screenPixel.hlsl"), "ps_main"); - WireframeVertexShader.Load(Path.Combine(Shader.ShaderRootPath, "wireframeVertex.hlsl"), "vs_main"); - WireframePixelShader.Load(Path.Combine(Shader.ShaderRootPath, "wireframePixel.hlsl"), "ps_main"); - // Create 3D rendering resources Create3DRenderingResources(); + ShaderPipelines ??= new(this); + + LoadPipelines(); + CoreModels = new CoreModels(this); Logger.Information("Graphics context initialized"); @@ -308,17 +287,6 @@ private void OnLoad() { sampleTexture = new Texture(this); sampleTexture.Load(Path.Combine(Texture.TextureRootPath, "sample_derp_wave.png")); - // Initialize camera controllers - freeCameraController = new FreeCameraController(sharedCamera); - followCameraController = new FollowCameraController(sharedCamera); - - // Start with free camera controller as default - CameraController = freeCameraController; - - Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - sharedCamera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); - freeCameraController.SetDefaultRotation(); // Set default rotation for MapleStory 2 - ImGuiController = new ImGuiController(this, Input, ImGuiWindowType.Main); if (DebuggerWindow is not null) { @@ -326,6 +294,32 @@ private void OnLoad() { } } + private void LoadPipelines() { + ShaderPipelines!.LoadPipelines(); + + if (!ShaderPipelines!.Pipelines.TryGetValue("Screen", out ShaderPipeline? screenPipeline)) { + ShaderPipelines!.ExternalError(); + + Logger.Error("No 'Screen' shader pipeline defined"); + } + + 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; @@ -398,88 +392,19 @@ private unsafe void Create3DRenderingResources() { WireframeRasterizerState = wireframeRasterizerState; // Create constant buffer for matrices and color - var constantBufferDesc = new BufferDesc { - ByteWidth = 256, // 3 * 64 bytes for matrices + 16 bytes for color (rounded up to 256 for alignment) - Usage = Usage.Dynamic, - BindFlags = (uint) BindFlag.ConstantBuffer, - CPUAccessFlags = (uint) CpuAccessFlag.Write, - MiscFlags = 0, - }; - - ID3D11Buffer* constantBuffer = null; - SilkMarshal.ThrowHResult(DxDevice.CreateBuffer(&constantBufferDesc, (SubresourceData*) null, &constantBuffer)); - ConstantBuffer = constantBuffer; - } - - public void UpdateProjectionMatrix() { - Vector2D windowSize = DebuggerWindow?.FramebufferSize ?? DefaultWindowSize; - CameraController.Camera.UpdateProjectionMatrix(windowSize.X, windowSize.Y); - } - - /// - /// 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" - }; + // var constantBufferDesc = new BufferDesc { + // ByteWidth = 256, // 3 * 64 bytes for matrices + 16 bytes for color (rounded up to 256 for alignment) + // Usage = Usage.Dynamic, + // BindFlags = (uint) BindFlag.ConstantBuffer, + // CPUAccessFlags = (uint) CpuAccessFlag.Write, + // MiscFlags = 0, + // }; + // + // ID3D11Buffer* constantBuffer = null; + // SilkMarshal.ThrowHResult(DxDevice.CreateBuffer(&constantBufferDesc, (SubresourceData*) null, &constantBuffer)); + // ConstantBuffer = constantBuffer; } - /// - /// Whether the user has manually stopped following (global state) - /// - public bool HasManuallyStopped => hasManuallyStoppedFollowing; - - /// - /// 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); - } - } /// /// Toggles wireframe rendering mode @@ -489,110 +414,13 @@ public void ToggleWireframeMode() { Logger.Information("Wireframe mode: {Mode}", WireframeMode ? "ON" : "OFF"); } - public void UpdateConstantBuffer(Matrix4x4 worldMatrix) { - UpdateConstantBuffer(worldMatrix, new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); // Default white color - } - - // Current color for rendering - private Vector4 currentColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); // Default white - - public void SetColor(float r, float g, float b, float a) { - currentColor = new Vector4(r, g, b, a); - } - - public void UpdateConstantBuffer(Matrix4x4 worldMatrix, bool useCurrentColor) { - UpdateConstantBuffer(worldMatrix, useCurrentColor ? currentColor : new Vector4(1.0f, 1.0f, 1.0f, 1.0f)); - } - - public unsafe void UpdateConstantBuffer(Matrix4x4 worldMatrix, Vector4 color) { - if (ConstantBuffer.Handle == null) return; - - MappedSubresource mappedResource; - SilkMarshal.ThrowHResult(DxDeviceContext.Map(ConstantBuffer, 0, Map.WriteDiscard, 0, &mappedResource)); - - // Write matrices and color to constant buffer (world, view, projection, color) - float* data = (float*) mappedResource.PData; - - // Copy world matrix (transposed) - Matrix4x4 worldTransposed = Matrix4x4.Transpose(worldMatrix); - data[0] = worldTransposed.M11; - data[1] = worldTransposed.M12; - data[2] = worldTransposed.M13; - data[3] = worldTransposed.M14; - data[4] = worldTransposed.M21; - data[5] = worldTransposed.M22; - data[6] = worldTransposed.M23; - data[7] = worldTransposed.M24; - data[8] = worldTransposed.M31; - data[9] = worldTransposed.M32; - data[10] = worldTransposed.M33; - data[11] = worldTransposed.M34; - data[12] = worldTransposed.M41; - data[13] = worldTransposed.M42; - data[14] = worldTransposed.M43; - data[15] = worldTransposed.M44; - - // Copy view matrix (transposed) - Matrix4x4 viewTransposed = Matrix4x4.Transpose(CameraController.Camera.ViewMatrix); - data[16] = viewTransposed.M11; - data[17] = viewTransposed.M12; - data[18] = viewTransposed.M13; - data[19] = viewTransposed.M14; - data[20] = viewTransposed.M21; - data[21] = viewTransposed.M22; - data[22] = viewTransposed.M23; - data[23] = viewTransposed.M24; - data[24] = viewTransposed.M31; - data[25] = viewTransposed.M32; - data[26] = viewTransposed.M33; - data[27] = viewTransposed.M34; - data[28] = viewTransposed.M41; - data[29] = viewTransposed.M42; - data[30] = viewTransposed.M43; - data[31] = viewTransposed.M44; - - // Copy projection matrix (transposed) - Matrix4x4 projTransposed = Matrix4x4.Transpose(CameraController.Camera.ProjectionMatrix); - data[32] = projTransposed.M11; - data[33] = projTransposed.M12; - data[34] = projTransposed.M13; - data[35] = projTransposed.M14; - data[36] = projTransposed.M21; - data[37] = projTransposed.M22; - data[38] = projTransposed.M23; - data[39] = projTransposed.M24; - data[40] = projTransposed.M31; - data[41] = projTransposed.M32; - data[42] = projTransposed.M33; - data[43] = projTransposed.M34; - data[44] = projTransposed.M41; - data[45] = projTransposed.M42; - data[46] = projTransposed.M43; - data[47] = projTransposed.M44; - - // Copy color - data[48] = color.X; - data[49] = color.Y; - data[50] = color.Z; - data[51] = color.W; - - DxDeviceContext.Unmap(ConstantBuffer, 0); - - // Bind constant buffer to vertex shader - ID3D11Buffer* constantBuffers = ConstantBuffer; - DxDeviceContext.VSSetConstantBuffers(0, 1, &constantBuffers); - } - public void CleanUp() { if (isCleanedUp) return; // Prevent multiple cleanup calls isCleanedUp = true; unsafe { if (DxDevice.Handle is not null) { - VertexShader?.CleanUp(); - PixelShader?.CleanUp(); - WireframeVertexShader?.CleanUp(); - WireframePixelShader?.CleanUp(); + ShaderPipelines?.CleanUp(); // Clean up 3D resources DepthStencilBuffer.Dispose(); @@ -600,17 +428,14 @@ public void CleanUp() { DepthStencilState.Dispose(); SolidRasterizerState.Dispose(); WireframeRasterizerState.Dispose(); - ConstantBuffer.Dispose(); + // ConstantBuffer.Dispose(); DxDevice.Dispose(); DxDeviceContext.Dispose(); DxSwapChain.Dispose(); Input?.Dispose(); - VertexShader = null; - PixelShader = null; - WireframeVertexShader = null; - WireframePixelShader = null; + ShaderPipelines = null; DxDevice = default; DxDeviceContext = default; DxSwapChain = default; @@ -619,7 +444,7 @@ public void CleanUp() { DepthStencilState = default; SolidRasterizerState = default; WireframeRasterizerState = default; - ConstantBuffer = default; + // ConstantBuffer = default; Input = null; Logger.Information("Graphics context cleaning up"); @@ -746,7 +571,7 @@ private unsafe void OnFramebufferResize(Vector2D newSize) { } // Update projection matrix for new aspect ratio - UpdateProjectionMatrix(); + // UpdateProjectionMatrix(); } public bool HasFieldUpdated(FieldManager field) { @@ -765,6 +590,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() { @@ -808,6 +643,11 @@ private void UpdateDeltaTracker() { } private unsafe void OnRender(double delta) { + if (!(ShaderPipelines?.CanRender ?? false)) { + return; + } + + ShaderPipelines?.StartedFrame(); UpdateDeltaTracker(); DebuggerWindow!.MakeCurrent(); @@ -838,6 +678,8 @@ private unsafe void OnRender(double delta) { DxSwapChain.Present(1, 0); + ShaderPipelines?.EndedFrame(); + renderTargetView.Dispose(); framebuffer.Dispose(); } @@ -853,4 +695,14 @@ public void SetSolidRasterizer() { // 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/Resources/Shader.cs b/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs index 05813a5bd..6c9317ebb 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Shader.cs @@ -18,8 +18,6 @@ internal class ShaderCompilationJob { } 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); diff --git a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj index cb2da472d..2897398ac 100644 --- a/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj +++ b/Maple2.Server.DebugGame/Maple2.Server.DebugGame.csproj @@ -1,56 +1,101 @@  - - Exe - net8.0 - enable - enable - True - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - 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 7b2a03249..43bb08a0d 100644 --- a/Maple2.Server.DebugGame/Program.cs +++ b/Maple2.Server.DebugGame/Program.cs @@ -122,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() @@ -131,7 +131,7 @@ // ChatCommand Handlers autofac.RegisterType(); - autofac.RegisterAssemblyTypes(Assembly.LoadFrom("Maple2.Server.Game.dll")) + autofac.RegisterAssemblyTypes(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "Maple2.Server.Game.dll"))) .PublicOnly() .WithAttributeFiltering() .Where(type => typeof(GameCommand).IsAssignableFrom(type)) 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/root_path.txt b/Maple2.Server.DebugGame/root_path.txt new file mode 100644 index 000000000..8f12dd0a5 --- /dev/null +++ b/Maple2.Server.DebugGame/root_path.txt @@ -0,0 +1 @@ +../../.. From 9e955e06377b8c743d6816e27be623ac3f7cbaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 9 Aug 2025 15:36:43 -0300 Subject: [PATCH 20/25] imgui refactor + plots --- .../Graphics/DebugFieldRenderer.cs | 822 +++++------------- .../Graphics/DebugFieldWindow.cs | 18 +- .../Graphics/DebugGraphicsContext.cs | 22 - .../Graphics/ImGuiController.cs | 40 +- .../Graphics/Scene/ICameraController.cs | 4 +- .../Graphics/Ui/FieldPropertiesWindow.cs | 1 + .../Graphics/Ui/Windows/ActorDetailsWindow.cs | 113 +++ .../Graphics/Ui/Windows/FieldInfoWindow.cs | 158 ++++ .../Graphics/Ui/Windows/FieldListWindow.cs | 1 + .../Graphics/Ui/Windows/IUiWindow.cs | 5 +- .../Ui/Windows/VisualizationControlsWindow.cs | 83 ++ .../Graphics/Ui/Windows/WindowListWindow.cs | 1 + 12 files changed, 622 insertions(+), 646 deletions(-) create mode 100644 Maple2.Server.DebugGame/Graphics/Ui/Windows/ActorDetailsWindow.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldInfoWindow.cs create mode 100644 Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 542d46171..c8bcafbb6 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -3,10 +3,8 @@ using Maple2.Server.Game.Manager.Field; using ImGuiNET; using Maple2.Model.Enum; -using Maple2.Model.Game; using Maple2.Model.Metadata.FieldEntity; using Maple2.Server.DebugGame.Graphics.Data; -using Maple2.Server.Game.Manager; using Maple2.Server.Game.Model; using Maple2.Server.Game.Packets; using Maple2.Tools.VectorMath; @@ -14,12 +12,13 @@ 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)); @@ -37,24 +36,24 @@ public bool IsActive { private readonly HashSet activeWindows = []; private readonly Mutex activeMutex = new(); - private IActor? selectedActor; - - // 3D visualization settings - private bool showBoxColliders = true; // Box colliders enabled by default (main map blocks) - private bool showMeshColliders; // Mesh colliders disabled by default - private bool showSpawnPoints; // Spawn points disabled by default - private bool showVibrateObjects; // Vibrate objects disabled by default - private bool showPortals = true; // Portals enabled by default - private bool showPortalInformation = true; // Portal text information enabled by default - private bool showPortalConnections = true; // Portal connection lines enabled by default - private bool showActors = true; // Actors enabled by default - private bool showPlayers = true; // Players enabled by default - private bool showNpcs = true; // NPCs enabled by default - private bool showMobs = true; // Mobs enabled by default - - // Player movement mode - private bool playerMoveMode = false; // Player move mode disabled by default - private bool forceMove = false; // Force move (bypass validation) disabled by default + + // 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.) @@ -77,8 +76,8 @@ public bool IsActive { private readonly Camera sharedCamera = new(); // Global follow state (independent of active controller) - private bool hasManuallyStoppedFollowing = false; - private bool hasTriedAutoFollow = false; + public bool HasManuallyStoppedFollowing; + private bool hasTriedAutoFollow; public DebugFieldRenderer(DebugGraphicsContext context, FieldManager field) { Context = context; @@ -110,7 +109,7 @@ public void 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 && !HasManuallyStopped) { + 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)) { @@ -123,40 +122,7 @@ public void Update() { UpdateCameraFollow(); } - - public void Render(double delta) { - if (!IsActive) { - return; - } - - // Store field window position and size for positioning player details panel - Vector2 fieldWindowPos = Vector2.Zero; - Vector2 fieldWindowSize = Vector2.Zero; - - // 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(); - - RenderFieldBasicInfo(); - ImGui.Separator(); - RenderEntityCounts(); - ImGui.Separator(); - RenderActorList(); - ImGui.Separator(); - RenderFieldEntityDetails(); - } - ImGui.End(); - - // Render visualization controls - RenderVisualizationControls(); - - // Show actor details panel if an actor is selected - if (selectedActor != null) { - RenderActorDetailsPanel(fieldWindowPos, fieldWindowSize); - } - } + public void Render(double delta) { } // New method specifically for field window 3D rendering public void RenderFieldWindow3D(DebugFieldWindow window, double delta) { @@ -182,7 +148,7 @@ private void HandleMouseInput() { // Get mouse position in screen coordinates Vector2 mousePos = ImGui.GetMousePos(); - if (playerMoveMode && selectedActor is FieldPlayer selectedPlayer) { + if (PlayerMoveMode && SelectedActor is FieldPlayer selectedPlayer) { // Move mode: try to move the selected player to clicked position TryMovePlayerToScreenPosition(selectedPlayer, mousePos); } else { @@ -192,343 +158,10 @@ private void HandleMouseInput() { } } - 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}"); - 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}"); - } - } - - 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(); - } - - private void RenderActorList() { - ImGui.Text("Active Actors:"); - - int totalActors = Field.Players.Count + Field.Npcs.Count + Field.Mobs.Count; - if (totalActors == 0) { - ImGui.Text("No active actors"); - return; - } - - // Actor selection controls - bool showActorDetailsDisabled = selectedActor == null; - if (showActorDetailsDisabled) { - ImGui.BeginDisabled(); - } - if (ImGui.Button("Show Actor Details") && !showActorDetailsDisabled) { - // Keep the selected actor to show details panel - } - if (showActorDetailsDisabled) { - ImGui.EndDisabled(); - } - - ImGui.SameLine(); - bool clearSelectionDisabled = selectedActor == null; - if (clearSelectionDisabled) { - ImGui.BeginDisabled(); - } - if (ImGui.Button("Clear Selection") && !clearSelectionDisabled) { - selectedActor = null; - } - if (clearSelectionDisabled) { - ImGui.EndDisabled(); - } - - // Players section (collapsed by default) - 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 playerIndex = 0; - foreach ((int objectId, FieldPlayer player) in Field.Players) { - ImGui.TableNextRow(); - bool selected = player == selectedActor; - bool nextSelected = false; - - ImGui.TableSetColumnIndex(0); - nextSelected |= ImGui.Selectable($"{player.Value.Character.Name}##Player {playerIndex} 0", selected); - ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{player.Value.Character.Level}##Player {playerIndex} 1", selected); - ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{objectId}##Player {playerIndex} 2", selected); - ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{(player.IsDead ? "Dead" : "Alive")}##Player {playerIndex} 3", selected); - - if (nextSelected) { - selectedActor = player; - } - ++playerIndex; - } - ImGui.EndTable(); - } - } - - // NPCs section (collapsed by default, includes both NPCs and Mobs) - 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 npcIndex = 0; - - // 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##NPC {npcIndex} 0", selected); - ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Name}##NPC {npcIndex} 1", selected); - ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{npc.Value.Metadata.Basic.Level}##NPC {npcIndex} 2", selected); - ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{objectId}##NPC {npcIndex} 3", selected); - ImGui.TableSetColumnIndex(4); - nextSelected |= ImGui.Selectable($"{(npc.IsDead ? "Dead" : "Alive")}##NPC {npcIndex} 4", selected); - - if (nextSelected) { - selectedActor = npc; - } - ++npcIndex; - } - - // 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##Mob {npcIndex} 0", selected); - ImGui.TableSetColumnIndex(1); - nextSelected |= ImGui.Selectable($"{mob.Value.Metadata.Name}##Mob {npcIndex} 1", selected); - ImGui.TableSetColumnIndex(2); - nextSelected |= ImGui.Selectable($"{mob.Value.Metadata.Basic.Level}##Mob {npcIndex} 2", selected); - ImGui.TableSetColumnIndex(3); - nextSelected |= ImGui.Selectable($"{objectId}##Mob {npcIndex} 3", selected); - ImGui.TableSetColumnIndex(4); - nextSelected |= ImGui.Selectable($"{(mob.IsDead ? "Dead" : "Alive")}##Mob {npcIndex} 4", selected); - - if (nextSelected) { - selectedActor = mob; - } - ++npcIndex; - } - ImGui.EndTable(); - } - } - } - - private void RenderFieldEntityDetails() { - if (Field.AccelerationStructure == null) { - return; - } - ImGui.Text("Field Entity Information:"); - ImGui.Separator(); - - // Basic field info - ImGui.Text($"Grid Size: {Field.AccelerationStructure.GridSize}"); - ImGui.Text($"Min Index: {Field.AccelerationStructure.MinIndex}"); - ImGui.Text($"Max Index: {Field.AccelerationStructure.MaxIndex}"); - - ImGui.Separator(); - - // Entity counts by type - FieldEntity[] allEntities = GetAllEntities().ToArray(); - Dictionary entityCounts = allEntities.GroupBy(e => e.GetType().Name).ToDictionary(g => g.Key, g => g.Count()); - - ImGui.Text("Entity Types:"); - foreach (KeyValuePair kvp in entityCounts) { - ImGui.Text($" {kvp.Key}: {kvp.Value}"); - } - } - - private void RenderActorDetailsPanel(Vector2 fieldWindowPos, Vector2 fieldWindowSize) { - if (selectedActor == null) return; - - // 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 - )); - - // Set a reasonable size for the actor details panel - ImGui.SetNextWindowSize(new Vector2(350, 500), ImGuiCond.FirstUseEver); - - // Get actor name for window title - string actorName = GetActorName(selectedActor); - string actorType = GetActorType(selectedActor); - - // Create a separate window for actor details - if (ImGui.Begin($"{actorType} Details: {actorName}##ActorDetails")) { - RenderActorBasicInfo(); - ImGui.Separator(); - RenderActorPositionInfo(); - ImGui.Separator(); - RenderActorStatsInfo(); - ImGui.Separator(); - RenderActorAdditionalInfo(); - } - ImGui.End(); - } - - private string GetActorName(IActor actor) { - return actor switch { - FieldPlayer player => player.Value.Character.Name, - FieldNpc npc => npc.Value.Metadata.Name ?? "Unknown", - _ => "Unknown", - }; - } - - private string GetActorType(IActor actor) { - return actor switch { - FieldPlayer => "Player", - FieldNpc => "NPC", - _ => "Unknown", - }; - } - - private void RenderActorBasicInfo() { - if (selectedActor == null) return; - - ImGui.Text($"Type: {GetActorType(selectedActor)}"); - ImGui.Text($"Name: {GetActorName(selectedActor)}"); - ImGui.Text($"Object ID: {selectedActor.ObjectId}"); - ImGui.Text($"Is Dead: {selectedActor.IsDead}"); - - 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; - } - } - - private void RenderActorPositionInfo() { - if (selectedActor == null) return; - - 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}"); - } - ImGui.Text($"Playing Sequence: {selectedActor.Animation.PlayingSequence?.Name ?? "None"}"); - - if (selectedActor is FieldPlayer player) { - ImGui.Text($"Last Ground Position: {player.LastGroundPosition}"); - ImGui.Text($"In Battle: {player.InBattle}"); - } - - ImGui.Unindent(); - } - - private void RenderActorStatsInfo() { - if (selectedActor == null) return; - - ImGui.Text("Stats:"); - ImGui.Indent(); - - StatsManager stats = selectedActor.Stats; - ImGui.Text($"Health: {stats.Values[BasicAttribute.Health].Current}/{stats.Values[BasicAttribute.Health].Total}"); - - // 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}"); - } - - ImGui.Unindent(); - } - - private void RenderActorAdditionalInfo() { - if (selectedActor == null) return; - - ImGui.Text("Additional Information"); - - switch (selectedActor) { - case FieldPlayer player: - ImGui.Text($"Admin Permissions: {player.AdminPermissions}"); + 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" }; - // Player movement controls - ImGui.Separator(); - ImGui.Text("Movement Controls:"); - ImGui.Checkbox("Move Player Mode", ref playerMoveMode); - if (playerMoveMode) { - ImGui.Indent(); - ImGui.Checkbox("Force Move (bypass validation)", ref forceMove); - 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; - } - - } - - public void CleanUp() { - // No cleanup needed for ImGui text rendering - } + public void CleanUp() { } public void AttachWindow(DebugFieldWindow window) { activeMutex.WaitOne(); @@ -543,7 +176,7 @@ public void AttachWindow(DebugFieldWindow window) { // 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 && !HasManuallyStopped) { + if (!isFollowing && !HasManuallyStoppedFollowing) { FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); if (firstPlayer != null) { StartFollowingPlayer(firstPlayer); @@ -564,8 +197,7 @@ private void RenderField3DVisualization(DebugFieldWindow window, double delta) { return; } - // Set up 3D rendering matrices - UpdateProjectionMatrix(window); + UpdateCameraProjection(window); // Enable wireframe mode for field visualization bool originalWireframeMode = Context.WireframeMode; @@ -589,45 +221,53 @@ private void RenderFieldEntities3D(DebugFieldWindow window) { } // Render box colliders - if (showBoxColliders) { + if (ShowBoxColliders) { RenderBoxColliders(window); } // Render mesh colliders - if (showMeshColliders) { + if (ShowMeshColliders) { RenderMeshColliders(window); } // Render spawn points - if (showSpawnPoints) { + if (ShowSpawnPoints) { RenderSpawnPoints(window); } // Render vibrate objects - if (showVibrateObjects) { + if (ShowVibrateObjects) { RenderVibrateObjects(window); } // Render portals - if (showPortals) { + if (ShowPortals) { RenderPortals(window); - if (showPortalConnections) { + if (ShowPortalConnections) { RenderPortalConnectionLines(window); } // Render portal text labels - if (showPortalInformation) { + if (ShowPortalInformation) { RenderPortalTextLabels(); } } + // Render sellable tiles (player housing plots / sellable land tiles) + if (ShowSellableTiles) { + RenderSellableTiles(window); + if (ShowPlotLabels) { + RenderPlotTextLabels(); + } + } + // Render actors (players, NPCs, mobs) - if (showActors) { + if (ShowActors) { RenderActors(window); // Render selection highlight for selected actor - if (selectedActor != null) { + if (SelectedActor != null) { RenderActorSelectionHighlight(window); } @@ -638,7 +278,7 @@ private void RenderFieldEntities3D(DebugFieldWindow window) { private void RenderBoxColliders(DebugFieldWindow window) { // Set white color for box colliders - instanceBuffer.Color = new Vector4(1, 1, 1, 1); // White for box colliders + instanceBuffer.Color = new Vector4(1, 1, 1, 1); List allBoxColliders = GetAllEntities().OfType().ToList(); @@ -665,14 +305,6 @@ private void RenderMeshColliders(DebugFieldWindow window) { Vector3 size = meshCollider.Bounds.Max - meshCollider.Bounds.Min; Vector3 center = (meshCollider.Bounds.Max + meshCollider.Bounds.Min) * 0.5f; - // Convert Euler angles (degrees) to quaternion - Vector3 rotationRadians = meshCollider.Rotation * (MathF.PI / 180.0f); - var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); - - // Apply coordinate system transformation (Y↔Z flip) - var coordinateTransform = Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)); - rotation = coordinateTransform * rotation; - instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(size) * Matrix4x4.CreateTranslation(center)); @@ -685,10 +317,6 @@ private void RenderSpawnPoints(DebugFieldWindow window) { IEnumerable allSpawnPoints = GetAllEntities().OfType(); foreach (FieldSpawnTile spawnPoint in allSpawnPoints) { - // Convert Euler angles (degrees) to quaternion - // Vector3 rotationRadians = spawnPoint.Rotation * (MathF.PI / 180.0f); - // var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); - instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(Vector3.One * 50.0f) * Matrix4x4.CreateTranslation(spawnPoint.Position)); @@ -699,13 +327,9 @@ private void RenderSpawnPoints(DebugFieldWindow window) { private void RenderVibrateObjects(DebugFieldWindow window) { // Set pink color for vibrate objects - instanceBuffer.Color = new Vector4(1, 0.75f, 0.8f, 1); // Pink for vibrate objects + instanceBuffer.Color = new Vector4(1, 0.75f, 0.8f, 1); foreach (FieldVibrateEntity vibrateEntity in Field.AccelerationStructure!.VibrateEntities) { - // Convert Euler angles (degrees) to quaternion - Vector3 rotationRadians = vibrateEntity.Rotation * (MathF.PI / 180.0f); - var rotation = Quaternion.CreateFromYawPitchRoll(rotationRadians.Y, rotationRadians.X, rotationRadians.Z); - instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(Vector3.One * 100.0f) * Matrix4x4.CreateTranslation(vibrateEntity.Position)); @@ -716,14 +340,41 @@ private void RenderVibrateObjects(DebugFieldWindow window) { private void RenderPortals(DebugFieldWindow window) { // Set blue color for portals - instanceBuffer.Color = new Vector4(0, 0.5f, 1, 1); // Blue for portals - + instanceBuffer.Color = new Vector4(0, 0.5f, 1, 1); foreach (FieldPortal portal in Field.GetPortals()) { - // Use portal dimensions if available, otherwise use default size 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(); @@ -732,7 +383,7 @@ private void RenderPortals(DebugFieldWindow window) { private void RenderPortalConnectionLines(DebugFieldWindow window) { // Set cyan color for portal connection lines - instanceBuffer.Color = new Vector4(0, 1, 1, 0.7f); // Cyan with some transparency + instanceBuffer.Color = new Vector4(0, 1, 1, 0.7f); List portals = Field.GetPortals().ToList(); @@ -789,8 +440,7 @@ private void RenderActors(DebugFieldWindow window) { const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; - // Render players using cylinders (like Recast agents) - Cyan color - if (showPlayers) { + 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) @@ -800,12 +450,11 @@ private void RenderActors(DebugFieldWindow window) { Matrix4x4.CreateTranslation(player.Position)); UpdateWireframeInstance(window); - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for players (like Recast agents) + Context.CoreModels!.Cylinder.Draw(); } } - // Render NPCs using cylinders (like Recast agents) - Green for alive, Gray for dead - if (showNpcs) { + if (ShowNpcs) { foreach ((int _, FieldNpc npc) in Field.Npcs) { // Set color based on dead status if (npc.IsDead) { @@ -821,13 +470,12 @@ private void RenderActors(DebugFieldWindow window) { Matrix4x4.CreateTranslation(npc.Position)); UpdateWireframeInstance(window); - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs (like Recast agents) + Context.CoreModels!.Cylinder.Draw(); // Use cylinder for NPCs } } - // Render Mobs using cylinders (like Recast agents) - Red for aggressive, Yellow for wandering, Gray for dead - if (showMobs) { + if (ShowMobs) { foreach ((int _, FieldNpc mob) in Field.Mobs) { // Set color based on dead status and battle state if (mob.IsDead) { @@ -848,125 +496,32 @@ private void RenderActors(DebugFieldWindow window) { Matrix4x4.CreateTranslation(mob.Position)); UpdateWireframeInstance(window); - Context.CoreModels!.Cylinder.Draw(); // Use cylinder for mobs (like Recast agents) + Context.CoreModels!.Cylinder.Draw(); } } } private void RenderActorSelectionHighlight(DebugFieldWindow window) { - if (selectedActor == null) return; + if (SelectedActor == null) return; - const float agentRadius = AgentRadiusMeters * GameUnitsPerMeter; - const float agentHeight = AgentHeightMeters * GameUnitsPerMeter; + 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 a second, even larger outline with pulsing effect - instanceBuffer.Color = new Vector4(1, 1, 0, 1); // Yellow with pulsing alpha - float pulseRadius = agentRadius * 1.4f; - float pulseHeight = agentHeight * 1.2f; + // Render larger outline + instanceBuffer.Color = new Vector4(1, 1, 0, 1); // Yellow - instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(pulseRadius, pulseHeight, pulseRadius)) * + instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(highlightRadius, highlightHeight, highlightRadius)) * rotation * - Matrix4x4.CreateTranslation(selectedActor.Position)); + Matrix4x4.CreateTranslation(SelectedActor.Position)); UpdateWireframeInstance(window); Context.CoreModels!.Cylinder.Draw(); } - private void RenderVisualizationControls() { - if (ImGui.Begin("3D Visualization Controls")) { - ImGui.Text("Field Visualization Settings"); - ImGui.Separator(); - - ImGui.Checkbox("Show Box Colliders", ref showBoxColliders); - // ImGui.Checkbox("Show Mesh Colliders", ref showMeshColliders); - // ImGui.Checkbox("Show Spawn Points", ref showSpawnPoints); - ImGui.Checkbox("Show Vibrate Objects", ref showVibrateObjects); - ImGui.Checkbox("Show Portals", ref showPortals); - - // Portal sub-options (only show when portals are enabled) - if (showPortals) { - ImGui.Indent(); - ImGui.Checkbox("Show Portal Information", ref showPortalInformation); - ImGui.Checkbox("Show Portal Connections", ref showPortalConnections); - ImGui.Unindent(); - } - - ImGui.Checkbox("Show Actors", ref showActors); - - // Actor sub-options (only show when actors are enabled) - if (showActors) { - ImGui.Indent(); - ImGui.Checkbox("Show Players", ref showPlayers); - ImGui.Checkbox("Show NPCs", ref showNpcs); - ImGui.Checkbox("Show Mobs", ref showMobs); - ImGui.Unindent(); - } - - ImGui.Separator(); - - // Camera controls - ImGui.Text("Camera Controls:"); - if (ImGui.Button("Reset Camera")) { - // Reset camera to default rotation - if (CameraController is FreeCameraController freeCam) { - freeCam.SetDefaultRotation(); - } - // else if (CameraController is FollowCameraController followCam) { - // followCam.SetDefaultRotation(); - // } - } - - ImGui.SameLine(); - if (ImGui.Button("Toggle Wireframe")) { - Context.ToggleWireframeMode(); - } - - if (ImGui.Button("View Field Overview")) { - SetFieldOverviewCamera(); - } - - // Player follow controls - ImGui.Separator(); - bool isFollowing = CameraController is FollowCameraController { IsFollowingPlayer: true }; - long? followedId = CameraController is FollowCameraController fc ? fc.FollowedPlayerId : null; - ImGui.Text($"Player Follow: {(isFollowing ? $"Player ID: {followedId}" : string.Empty)}"); - - if (isFollowing) { - if (ImGui.Button("Stop Following")) { - StopFollowingPlayer(); - } - } else { - if (ImGui.Button("Follow First Player")) { - FieldPlayer? firstPlayer = Field.Players.Values.FirstOrDefault(); - if (firstPlayer != null) { - StartFollowingPlayer(firstPlayer); - } - } - } - - if (isFollowing) { - // Get target from concrete controller if available - Vector3 target = CameraController switch { - FreeCameraController freeCam => freeCam.CameraTarget, - FollowCameraController followCam => followCam.CameraTarget, - _ => Vector3.Zero, - }; - ImGui.Text($"Player Following Position: {target}"); - } - ImGui.Text($"Camera Position: {CameraController.Camera.Transform.Position}"); - - Vector3 forward = 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")}"); - } - ImGui.End(); - } - - private void SetFieldOverviewCamera() { + public void SetFieldOverviewCamera() { // Stop following player when switching to field overview if (CameraController is FollowCameraController { IsFollowingPlayer: true }) { StopFollowingPlayer(); @@ -1126,7 +681,7 @@ private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { float closestDistance = float.MaxValue; // Test players - if (showPlayers) { + if (ShowPlayers) { foreach ((int _, FieldPlayer player) in Field.Players) { if (TryRayPrismIntersection(rayOrigin, rayDirection, player.Shape, out float distance)) { if (distance < closestDistance) { @@ -1138,7 +693,7 @@ private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { } // Test NPCs - if (showNpcs) { + if (ShowNpcs) { foreach ((int _, FieldNpc npc) in Field.Npcs) { if (TryRayPrismIntersection(rayOrigin, rayDirection, npc.Shape, out float distance)) { if (distance < closestDistance) { @@ -1150,7 +705,7 @@ private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { } // Test Mobs - if (showMobs) { + if (ShowMobs) { foreach ((int _, FieldNpc mob) in Field.Mobs) { if (TryRayPrismIntersection(rayOrigin, rayDirection, mob.Shape, out float distance)) { if (distance < closestDistance) { @@ -1162,7 +717,7 @@ private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { } if (closestActor != null) { - selectedActor = closestActor; + SelectedActor = closestActor; return true; } @@ -1201,7 +756,7 @@ private void TryMovePlayerToScreenPosition(FieldPlayer player, Vector2 screenPos Vector3 newPosition = targetPosition.Value; Vector3 currentRotation = player.Rotation; // Keep current rotation - if (forceMove) { + if (ForceMove) { // Force move: bypass validation and send packet directly player.Session.Send(PortalPacket.MoveByPortal(player, newPosition, currentRotation)); } else { @@ -1229,7 +784,7 @@ private void TryMovePlayerToScreenPosition(FieldPlayer player, Vector2 screenPos 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; + float targetZ = SelectedActor?.Position.Z ?? 0.0f; if (Math.Abs(rayDirection.Z) > 0.001f) { float t = (targetZ - rayOrigin.Z) / rayDirection.Z; @@ -1281,69 +836,72 @@ private bool IsPositionInBoxCollider(Vector2 position, FieldBoxColliderEntity co private void RenderActorTextLabels() { ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); - // Render player names using ImGui - if (showPlayers) { + // 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; + 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 - Vector2 bgMin = screenPos - new Vector2(4, 2); - Vector2 bgMax = screenPos + textSize + new Vector2(4, 2); + // 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(screenPos, ImGui.ColorConvertFloat4ToU32(textColor), playerName); + drawList.AddText(textTopLeft, ImGui.ColorConvertFloat4ToU32(textColor), playerName); } } - // Render NPC names ImGui - if (showNpcs) { + // 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; + 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); - // Draw background for NPC name Vector2 nameSize = ImGui.CalcTextSize(npcName); - Vector2 nameBgMin = screenPos - new Vector2(4, 2); - Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); + 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(screenPos, color, npcName); + drawList.AddText(nameTopLeft, color, npcName); } } - // Render Mob names and HP using ImGui - if (showMobs) { + // 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; + 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; @@ -1369,26 +927,21 @@ private void RenderActorTextLabels() { uint color = ImGui.ColorConvertFloat4ToU32(textColor); - // Draw background for mob name Vector2 nameSize = ImGui.CalcTextSize(mobName); - Vector2 nameBgMin = screenPos - new Vector2(4, 2); - Vector2 nameBgMax = screenPos + nameSize + new Vector2(4, 2); - drawList.AddRectFilled(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(bgColor)); - drawList.AddRect(nameBgMin, nameBgMax, ImGui.ColorConvertFloat4ToU32(borderColor)); - - drawList.AddText(screenPos, color, mobName); - - // Position HP text below the name using screen-space offset to avoid overlap when zoomed out - // Calculate HP text position in screen space (below the name) with consistent pixel spacing - Vector2 hpScreenPos = new Vector2(screenPos.X, screenPos.Y + nameSize.Y + 4); // 4 pixels below name - - string hpText = $"HP: {hpPercent:F1}%"; Vector2 hpSize = ImGui.CalcTextSize(hpText); - Vector2 hpBgMin = hpScreenPos - new Vector2(4, 2); - Vector2 hpBgMax = hpScreenPos + hpSize + new Vector2(4, 2); - drawList.AddRectFilled(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.7f))); - drawList.AddRect(hpBgMin, hpBgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f))); - drawList.AddText(hpScreenPos, color, 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); } } } @@ -1396,23 +949,20 @@ private void RenderActorTextLabels() { private void RenderPortalTextLabels() { ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); - // Render portal information using ImGui + // 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; - // Build portal info text 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}"; - // Choose color based on enabled status - Vector4 textColor = portal.Enabled ? new Vector4(0, 0.8f, 1, 1) : new Vector4(0.6f, 0.6f, 0.6f, 1); // Blue if enabled, gray if disabled + 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); - // Calculate total text size for background Vector2 enabledSize = ImGui.CalcTextSize(enabledText); Vector2 idSize = ImGui.CalcTextSize(idText); Vector2 targetSize = ImGui.CalcTextSize(targetText); @@ -1420,16 +970,15 @@ private void RenderPortalTextLabels() { 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; // 8 pixels spacing between lines + float totalHeight = enabledSize.Y + idSize.Y + targetSize.Y + targetPortalSize.Y + typeSize.Y + 8; + float left = screenPos.X - (maxWidth * 0.5f); - // Draw background - Vector2 bgMin = screenPos - new Vector2(4, 2); - Vector2 bgMax = screenPos + new Vector2(maxWidth + 8, totalHeight + 4); - drawList.AddRectFilled(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0, 0, 0, 0.8f))); // Dark background - drawList.AddRect(bgMin, bgMax, ImGui.ColorConvertFloat4ToU32(new Vector4(0.3f, 0.3f, 0.3f, 0.9f))); // Border + 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))); - // Draw text lines - Vector2 currentPos = screenPos; + Vector2 currentPos = new(left, screenPos.Y); drawList.AddText(currentPos, color, enabledText); currentPos.Y += enabledSize.Y + 1; drawList.AddText(currentPos, color, idText); @@ -1442,6 +991,76 @@ private void RenderPortalTextLabels() { } } + 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); + } + } + + if (groupInfo.Count == 0) return; + + ImDrawListPtr drawList = ImGui.GetBackgroundDrawList(); + + // 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); + } + } + private FieldEntity[] GetAllEntities() { // Cache static entities since they never change during field runtime if (cachedStaticEntities != null) { @@ -1472,7 +1091,7 @@ private FieldEntity[] GetAllEntities() { return cachedStaticEntities; } - public void UpdateProjectionMatrix(DebugFieldWindow window) { + 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); @@ -1517,16 +1136,11 @@ public string GetCurrentControllerType() { }; } - /// - /// Whether the user has manually stopped following (global state) - /// - public bool HasManuallyStopped => hasManuallyStoppedFollowing; - /// /// 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 + HasManuallyStoppedFollowing = false; // Reset manual stop flag when starting to follow SwitchToFollowCameraController(); followCameraController.StartFollowingPlayer(player.Value.Character.Id, player.Position); } @@ -1535,7 +1149,7 @@ public void StartFollowingPlayer(FieldPlayer player) { /// Stops following a player and automatically switches to free camera controller /// public void StopFollowingPlayer() { - hasManuallyStoppedFollowing = true; // Remember that user manually stopped + HasManuallyStoppedFollowing = true; // Remember that user manually stopped followCameraController.StopFollowingPlayer(); SwitchToFreeCameraController(); } diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs index f4221a551..1d238bd71 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldWindow.cs @@ -205,15 +205,8 @@ public unsafe void OnRender(double delta) { SceneState.BindConstantBuffer(InstanceConstantBuffer, 1, Enum.ShaderStageFlags.Vertex); SceneState.UpdateBindings(); - // Render 3D field content first (before ImGui) - if (ActiveRenderer is not null) { - ActiveRenderer.RenderFieldWindow3D(this, delta); - } - - // Then render ImGui content - if (ActiveRenderer is not null) { - ActiveRenderer.Render(delta); - } + // Render 3D field + ActiveRenderer?.RenderFieldWindow3D(this, delta); // End region for render code #endregion @@ -226,7 +219,6 @@ public unsafe void OnRender(double delta) { renderTargetView.Dispose(); framebuffer.Dispose(); - } private void OnFramebufferResize(Vector2D newSize) { @@ -253,7 +245,7 @@ private void UpdateCameraInputState() { inputState.InputFocused = true; // Update keyboard state for specific keys we care about - Key[] keysToTrack = { + Key[] keysToTrack = [ Key.W, Key.A, Key.S, @@ -271,8 +263,8 @@ private void UpdateCameraInputState() { Key.Up, Key.Down, Key.Left, - Key.Right - }; + Key.Right, + ]; foreach (Key key in keysToTrack) { int keyIndex = (int) key; diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 4d7a09787..ddfa3c50a 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -11,10 +11,7 @@ using Silk.NET.Windowing; using Maple2.Server.DebugGame.Graphics.Assets; using Maple2.Server.DebugGame.Graphics.Scene; -using Maple2.Tools; using Maple2.Tools.Extensions; -using System.Numerics; -using Maple2.Server.Game.Model; namespace Maple2.Server.DebugGame.Graphics; @@ -390,22 +387,8 @@ private unsafe void Create3DRenderingResources() { ID3D11RasterizerState* wireframeRasterizerState = null; SilkMarshal.ThrowHResult(DxDevice.CreateRasterizerState(&wireframeRasterizerDesc, &wireframeRasterizerState)); WireframeRasterizerState = wireframeRasterizerState; - - // Create constant buffer for matrices and color - // var constantBufferDesc = new BufferDesc { - // ByteWidth = 256, // 3 * 64 bytes for matrices + 16 bytes for color (rounded up to 256 for alignment) - // Usage = Usage.Dynamic, - // BindFlags = (uint) BindFlag.ConstantBuffer, - // CPUAccessFlags = (uint) CpuAccessFlag.Write, - // MiscFlags = 0, - // }; - // - // ID3D11Buffer* constantBuffer = null; - // SilkMarshal.ThrowHResult(DxDevice.CreateBuffer(&constantBufferDesc, (SubresourceData*) null, &constantBuffer)); - // ConstantBuffer = constantBuffer; } - /// /// Toggles wireframe rendering mode /// @@ -428,7 +411,6 @@ public void CleanUp() { DepthStencilState.Dispose(); SolidRasterizerState.Dispose(); WireframeRasterizerState.Dispose(); - // ConstantBuffer.Dispose(); DxDevice.Dispose(); DxDeviceContext.Dispose(); @@ -444,7 +426,6 @@ public void CleanUp() { DepthStencilState = default; SolidRasterizerState = default; WireframeRasterizerState = default; - // ConstantBuffer = default; Input = null; Logger.Information("Graphics context cleaning up"); @@ -569,9 +550,6 @@ private unsafe void OnFramebufferResize(Vector2D newSize) { SilkMarshal.ThrowHResult(DxDevice.CreateDepthStencilView((ID3D11Resource*) DepthStencilBuffer.Handle, &depthStencilViewDesc, &depthStencilView)); DepthStencilView = depthStencilView; } - - // Update projection matrix for new aspect ratio - // UpdateProjectionMatrix(); } public bool HasFieldUpdated(FieldManager field) { diff --git a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs index 80f3e62e9..bc91d5975 100644 --- a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs +++ b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs @@ -229,7 +229,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) { @@ -244,7 +244,6 @@ public void RenderWindows() { ImGui.End(); - foreach (IUiWindow window in uiWindows) { if (!window.Enabled) { continue; @@ -254,6 +253,43 @@ 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 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 + float newX = pos.X; + float newY = pos.Y; + if (newX < min.X) newX = min.X; + if (newY < min.Y) newY = min.Y; + if (newX + size.X > max.X) newX = max.X - size.X; + if (newY + size.Y > max.Y) newY = max.Y - size.Y; + + if (Math.Abs(newX - pos.X) > 0.5f || Math.Abs(newY - pos.Y) > 0.5f) { + ImGui.SetWindowPos(new Vector2(newX, newY), ImGuiCond.Always); + } + } + private void RegisterKeyboard(IKeyboard keyboard) { keyboard.KeyUp += OnKeyUp; keyboard.KeyDown += OnKeyDown; diff --git a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs index 418b21f9a..1a65f21d8 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/ICameraController.cs @@ -1,6 +1,4 @@ -using System.Numerics; - -namespace Maple2.Server.DebugGame.Graphics.Scene; +namespace Maple2.Server.DebugGame.Graphics.Scene; public interface ICameraController { /// 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..c14a8e277 --- /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(); } } From 9346473d7d40e9a0aeb261370f0d68c0ef8e08a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 9 Aug 2025 15:39:17 -0300 Subject: [PATCH 21/25] format --- .../Ui/Windows/VisualizationControlsWindow.cs | 142 +++++++++--------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs index c14a8e277..c12f92c21 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/VisualizationControlsWindow.cs @@ -6,78 +6,78 @@ 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 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(); + public void Initialize(DebugGraphicsContext context, ImGuiController controller, DebugFieldWindow? fieldWindow) { + Context = context; + ImGuiController = controller; + FieldWindow = fieldWindow; } - 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}"); + + 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(); } - 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(); - } } From 2af890256eccd242c6b548d713de73ff7f76429f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 9 Aug 2025 16:15:56 -0300 Subject: [PATCH 22/25] small refactors --- .../Graphics/Assets/ShaderPipelines.cs | 6 +- .../Graphics/DebugFieldRenderer.cs | 18 +- .../Graphics/Scene/SceneState.cs | 4 +- .../Collision/PrismRaycastExtensions.cs | 193 ++++++++++++++++++ 4 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 Maple2.Tools/Collision/PrismRaycastExtensions.cs diff --git a/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs index dfe8afec8..765dfb4eb 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/ShaderPipelines.cs @@ -66,7 +66,7 @@ private bool NodeTypeError(XmlElement element, string expected = "", string labe if (expected == "") { shaderErrors = true; - DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{1}'", currentDocument, element.Name); + DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{1}'", currentDocument, label, element.Name); return true; } @@ -74,7 +74,7 @@ private bool NodeTypeError(XmlElement element, string expected = "", string labe if (element.Name != expected) { shaderErrors = true; - DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{1}', expected '{2}'", currentDocument, element.Name, expected); + DebugGraphicsContext.Logger.Error("[{0}]: Unrecognized {label}node type '{2}', expected '{3}'", currentDocument, label, element.Name, expected); return true; } @@ -289,7 +289,7 @@ public void LoadPipelines() { } catch (Exception e) { shaderErrors = true; - DebugGraphicsContext.Logger.Error($"[{{0}}, {{1}}]: {e.Message}", currentDocument, stage); + DebugGraphicsContext.Logger.Error("[{0}, {1}]: {Message}", currentDocument, stage, e.Message); } } diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index c8bcafbb6..61b7bf1c3 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -725,23 +725,7 @@ private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { } 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; + return prism.Raycast(rayOrigin, rayDirection, out distance); } private void TryMovePlayerToScreenPosition(FieldPlayer player, Vector2 screenPos) { diff --git a/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs b/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs index c33e02929..56dc1e991 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/SceneState.cs @@ -58,8 +58,8 @@ public void BindConstantBuffer(ConstantBuffer buffer, int slot, ShaderStageFlags } } - private static unsafe Type*[] GetPointerArray(List> list) where Type : unmanaged, IComVtbl { - Type*[] array = new Type*[list.Count]; + 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(); diff --git a/Maple2.Tools/Collision/PrismRaycastExtensions.cs b/Maple2.Tools/Collision/PrismRaycastExtensions.cs new file mode 100644 index 000000000..aef633c08 --- /dev/null +++ b/Maple2.Tools/Collision/PrismRaycastExtensions.cs @@ -0,0 +1,193 @@ +using System; +using System.Numerics; + +namespace Maple2.Tools.Collision; + +/// +/// Provides analytic (allocation-free) ray intersection tests for IPrism shapes. +/// Supports PointPrism, circular Prism (treated as cylinder) and generic convex Polygon prism. +/// +public static class PrismRaycastExtensions { + /// + /// Perform analytic ray intersection with a prism. Direction need not be normalized. + /// Returns true if the ray intersects, providing distance along the ray in world units to first hit (>=0). + /// + public static bool Raycast(this IPrism prism, in Vector3 origin, in Vector3 direction, out float distance) { + distance = 0f; + float dirLen = direction.Length(); + if (dirLen < 1e-6f) return false; + Vector3 d = direction / dirLen; + Vector3 o = origin; + + if (prism.Contains(o)) return true; // already inside + + // PointPrism: solve param t for a single point; fall back to Contains verification. + if (prism is PointPrism) { + // We cannot access internal point directly; test with three axes selection. + int axis = 0; + float absX = MathF.Abs(d.X); + float absY = MathF.Abs(d.Y); + float absZ = MathF.Abs(d.Z); + if (absY > absX && absY >= absZ) axis = 1; + else if (absZ > absX && absZ > absY) axis = 2; + // Choose a target coordinate via polygon/height; pattern-match for Circle/Polygon + float targetCoord; + float originCoord; + float dirCoord; + switch (axis) { + case 1: + targetCoord = prism.Polygon switch { Circle c => c.Origin.Y, Polygon ppoly => ppoly.Points[0].Y, _ => o.Y }; + originCoord = o.Y; + dirCoord = d.Y; + break; + case 2: + targetCoord = prism.Height.Min; + originCoord = o.Z; + dirCoord = d.Z; + break; // Height.Min == Height.Max + default: + targetCoord = prism.Polygon switch { Circle c => c.Origin.X, Polygon ppoly => ppoly.Points[0].X, _ => o.X }; + originCoord = o.X; + dirCoord = d.X; + break; + } + if (MathF.Abs(dirCoord) < 1e-6f) return false; + float t = (targetCoord - originCoord) / dirCoord; + if (t < 0) return false; + Vector3 p = o + d * t; + if (prism.Contains(p)) { + distance = t * dirLen; + return true; + } + return false; + } + + if (prism is Prism prismStruct && prismStruct.Polygon is Circle circle) { + // Cylinder intersection (circle extruded along Z) + float zMin = prism.Height.Min; + float zMax = prism.Height.Max; + float ox = o.X - circle.Origin.X; + float oy = o.Y - circle.Origin.Y; + float dx = d.X; + float dy = d.Y; + float a = dx * dx + dy * dy; + float b = 2f * (dx * ox + dy * oy); + float c = ox * ox + oy * oy - circle.Radius * circle.Radius; + float tCylEnter, tCylExit; + if (a < 1e-8f) { // Parallel to cylinder axis + if (c > 0f) return false; // Outside + tCylEnter = 0f; + tCylExit = float.PositiveInfinity; + } else { + float disc = b * b - 4f * a * c; + if (disc < 0f) return false; + float sqrt = MathF.Sqrt(disc); + float inv2a = 0.5f / a; + float t0 = (-b - sqrt) * inv2a; + float t1 = (-b + sqrt) * inv2a; + if (t0 > t1) (t0, t1) = (t1, t0); + tCylEnter = t0; + tCylExit = t1; + } + // Z slab + float tZEnter, tZExit; + if (MathF.Abs(d.Z) < 1e-8f) { + if (o.Z < zMin || o.Z > zMax) return false; + tZEnter = 0f; + tZExit = float.PositiveInfinity; + } else { + float tz0 = (zMin - o.Z) / d.Z; + float tz1 = (zMax - o.Z) / d.Z; + if (tz0 > tz1) (tz0, tz1) = (tz1, tz0); + tZEnter = tz0; + tZExit = tz1; + } + float tEnter = MathF.Max(MathF.Max(tCylEnter, tZEnter), 0f); + float tExit = MathF.Min(tCylExit, tZExit); + if (tEnter <= tExit && tExit >= 0f) { + distance = tEnter * dirLen; + return true; + } + return false; + } + + if (prism is Prism genericPrism && genericPrism.Polygon is Polygon poly) { + float zMin = prism.Height.Min; + float zMax = prism.Height.Max; + // Z interval + float tZEnter, tZExit; + if (MathF.Abs(d.Z) < 1e-8f) { + if (o.Z < zMin || o.Z > zMax) return false; + tZEnter = 0f; + tZExit = float.PositiveInfinity; + } else { + float tz0 = (zMin - o.Z) / d.Z; + float tz1 = (zMax - o.Z) / d.Z; + if (tz0 > tz1) (tz0, tz1) = (tz1, tz0); + tZEnter = tz0; + tZExit = tz1; + } + Vector2[] points = poly.Points; + int count = points.Length; + if (count < 3) return false; + // Orientation via twice-area + float area2 = 0f; + for (int i = 0; i < count; i++) { + Vector2 p0 = points[i]; + Vector2 p1 = points[(i + 1) % count]; + area2 += p0.X * p1.Y - p1.X * p0.Y; + } + bool ccw = area2 >= 0f; + float tEnterXY = 0f; + float tExitXY = float.PositiveInfinity; + Vector2 dxy = new(d.X, d.Y); + if (dxy.LengthSquared() < 1e-10f) { // vertical ray + if (!poly.Contains(new Vector2(o.X, o.Y))) return false; + } else { + for (int i = 0; i < count; i++) { + Vector2 v0 = points[i]; + Vector2 v1 = points[(i + 1) % count]; + Vector2 e = v1 - v0; + Vector2 o0 = new(o.X - v0.X, o.Y - v0.Y); + float A = e.X * d.Y - e.Y * d.X; // cross(e,d) + float B = e.X * o0.Y - e.Y * o0.X; // cross(e,o-v0) + if (!ccw) { + A = -A; + B = -B; + } + if (MathF.Abs(A) < 1e-9f) { + if (B < 0f) return false; + continue; + } + float tEdge = -B / A; + if (A > 0f) { + if (tEdge > tEnterXY) tEnterXY = tEdge; + } else { + if (tEdge < tExitXY) tExitXY = tEdge; + } + if (tEnterXY - tExitXY > 1e-6f) return false; + } + } + float tEnter = MathF.Max(MathF.Max(tEnterXY, tZEnter), 0f); + float tExit = MathF.Min(tExitXY, tZExit); + if (tEnter <= tExit && tExit >= 0f) { + distance = tEnter * dirLen; + return true; + } + return false; + } + + // Fallback for unknown prism implementation: sparse exponential sampling. + const float fallbackMax = 10000f; + float tS = 0f; + while (tS < fallbackMax) { + Vector3 p = o + d * tS; + if (prism.Contains(p)) { + distance = tS * dirLen; + return true; + } + tS = tS == 0f ? 10f : tS * 1.5f; + } + return false; + } +} From 623732b04e2066d912ee5e61361e05e67e9d419e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 9 Aug 2025 16:17:23 -0300 Subject: [PATCH 23/25] format --- Maple2.Tools/Collision/PrismRaycastExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Tools/Collision/PrismRaycastExtensions.cs b/Maple2.Tools/Collision/PrismRaycastExtensions.cs index aef633c08..74bcc5080 100644 --- a/Maple2.Tools/Collision/PrismRaycastExtensions.cs +++ b/Maple2.Tools/Collision/PrismRaycastExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; namespace Maple2.Tools.Collision; From 03ce1170959c1a64cc353d96b57111a12046c5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 9 Aug 2025 16:22:49 -0300 Subject: [PATCH 24/25] revert --- .../Graphics/DebugFieldRenderer.cs | 18 +- .../Collision/PrismRaycastExtensions.cs | 193 ------------------ 2 files changed, 17 insertions(+), 194 deletions(-) delete mode 100644 Maple2.Tools/Collision/PrismRaycastExtensions.cs diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index 61b7bf1c3..c8bcafbb6 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -725,7 +725,23 @@ private bool TrySelectActorAtScreenPosition(Vector2 screenPos) { } private bool TryRayPrismIntersection(Vector3 rayOrigin, Vector3 rayDirection, IPrism prism, out float distance) { - return prism.Raycast(rayOrigin, rayDirection, out 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 TryMovePlayerToScreenPosition(FieldPlayer player, Vector2 screenPos) { diff --git a/Maple2.Tools/Collision/PrismRaycastExtensions.cs b/Maple2.Tools/Collision/PrismRaycastExtensions.cs deleted file mode 100644 index 74bcc5080..000000000 --- a/Maple2.Tools/Collision/PrismRaycastExtensions.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Numerics; - -namespace Maple2.Tools.Collision; - -/// -/// Provides analytic (allocation-free) ray intersection tests for IPrism shapes. -/// Supports PointPrism, circular Prism (treated as cylinder) and generic convex Polygon prism. -/// -public static class PrismRaycastExtensions { - /// - /// Perform analytic ray intersection with a prism. Direction need not be normalized. - /// Returns true if the ray intersects, providing distance along the ray in world units to first hit (>=0). - /// - public static bool Raycast(this IPrism prism, in Vector3 origin, in Vector3 direction, out float distance) { - distance = 0f; - float dirLen = direction.Length(); - if (dirLen < 1e-6f) return false; - Vector3 d = direction / dirLen; - Vector3 o = origin; - - if (prism.Contains(o)) return true; // already inside - - // PointPrism: solve param t for a single point; fall back to Contains verification. - if (prism is PointPrism) { - // We cannot access internal point directly; test with three axes selection. - int axis = 0; - float absX = MathF.Abs(d.X); - float absY = MathF.Abs(d.Y); - float absZ = MathF.Abs(d.Z); - if (absY > absX && absY >= absZ) axis = 1; - else if (absZ > absX && absZ > absY) axis = 2; - // Choose a target coordinate via polygon/height; pattern-match for Circle/Polygon - float targetCoord; - float originCoord; - float dirCoord; - switch (axis) { - case 1: - targetCoord = prism.Polygon switch { Circle c => c.Origin.Y, Polygon ppoly => ppoly.Points[0].Y, _ => o.Y }; - originCoord = o.Y; - dirCoord = d.Y; - break; - case 2: - targetCoord = prism.Height.Min; - originCoord = o.Z; - dirCoord = d.Z; - break; // Height.Min == Height.Max - default: - targetCoord = prism.Polygon switch { Circle c => c.Origin.X, Polygon ppoly => ppoly.Points[0].X, _ => o.X }; - originCoord = o.X; - dirCoord = d.X; - break; - } - if (MathF.Abs(dirCoord) < 1e-6f) return false; - float t = (targetCoord - originCoord) / dirCoord; - if (t < 0) return false; - Vector3 p = o + d * t; - if (prism.Contains(p)) { - distance = t * dirLen; - return true; - } - return false; - } - - if (prism is Prism prismStruct && prismStruct.Polygon is Circle circle) { - // Cylinder intersection (circle extruded along Z) - float zMin = prism.Height.Min; - float zMax = prism.Height.Max; - float ox = o.X - circle.Origin.X; - float oy = o.Y - circle.Origin.Y; - float dx = d.X; - float dy = d.Y; - float a = dx * dx + dy * dy; - float b = 2f * (dx * ox + dy * oy); - float c = ox * ox + oy * oy - circle.Radius * circle.Radius; - float tCylEnter, tCylExit; - if (a < 1e-8f) { // Parallel to cylinder axis - if (c > 0f) return false; // Outside - tCylEnter = 0f; - tCylExit = float.PositiveInfinity; - } else { - float disc = b * b - 4f * a * c; - if (disc < 0f) return false; - float sqrt = MathF.Sqrt(disc); - float inv2a = 0.5f / a; - float t0 = (-b - sqrt) * inv2a; - float t1 = (-b + sqrt) * inv2a; - if (t0 > t1) (t0, t1) = (t1, t0); - tCylEnter = t0; - tCylExit = t1; - } - // Z slab - float tZEnter, tZExit; - if (MathF.Abs(d.Z) < 1e-8f) { - if (o.Z < zMin || o.Z > zMax) return false; - tZEnter = 0f; - tZExit = float.PositiveInfinity; - } else { - float tz0 = (zMin - o.Z) / d.Z; - float tz1 = (zMax - o.Z) / d.Z; - if (tz0 > tz1) (tz0, tz1) = (tz1, tz0); - tZEnter = tz0; - tZExit = tz1; - } - float tEnter = MathF.Max(MathF.Max(tCylEnter, tZEnter), 0f); - float tExit = MathF.Min(tCylExit, tZExit); - if (tEnter <= tExit && tExit >= 0f) { - distance = tEnter * dirLen; - return true; - } - return false; - } - - if (prism is Prism genericPrism && genericPrism.Polygon is Polygon poly) { - float zMin = prism.Height.Min; - float zMax = prism.Height.Max; - // Z interval - float tZEnter, tZExit; - if (MathF.Abs(d.Z) < 1e-8f) { - if (o.Z < zMin || o.Z > zMax) return false; - tZEnter = 0f; - tZExit = float.PositiveInfinity; - } else { - float tz0 = (zMin - o.Z) / d.Z; - float tz1 = (zMax - o.Z) / d.Z; - if (tz0 > tz1) (tz0, tz1) = (tz1, tz0); - tZEnter = tz0; - tZExit = tz1; - } - Vector2[] points = poly.Points; - int count = points.Length; - if (count < 3) return false; - // Orientation via twice-area - float area2 = 0f; - for (int i = 0; i < count; i++) { - Vector2 p0 = points[i]; - Vector2 p1 = points[(i + 1) % count]; - area2 += p0.X * p1.Y - p1.X * p0.Y; - } - bool ccw = area2 >= 0f; - float tEnterXY = 0f; - float tExitXY = float.PositiveInfinity; - Vector2 dxy = new(d.X, d.Y); - if (dxy.LengthSquared() < 1e-10f) { // vertical ray - if (!poly.Contains(new Vector2(o.X, o.Y))) return false; - } else { - for (int i = 0; i < count; i++) { - Vector2 v0 = points[i]; - Vector2 v1 = points[(i + 1) % count]; - Vector2 e = v1 - v0; - Vector2 o0 = new(o.X - v0.X, o.Y - v0.Y); - float A = e.X * d.Y - e.Y * d.X; // cross(e,d) - float B = e.X * o0.Y - e.Y * o0.X; // cross(e,o-v0) - if (!ccw) { - A = -A; - B = -B; - } - if (MathF.Abs(A) < 1e-9f) { - if (B < 0f) return false; - continue; - } - float tEdge = -B / A; - if (A > 0f) { - if (tEdge > tEnterXY) tEnterXY = tEdge; - } else { - if (tEdge < tExitXY) tExitXY = tEdge; - } - if (tEnterXY - tExitXY > 1e-6f) return false; - } - } - float tEnter = MathF.Max(MathF.Max(tEnterXY, tZEnter), 0f); - float tExit = MathF.Min(tExitXY, tZExit); - if (tEnter <= tExit && tExit >= 0f) { - distance = tEnter * dirLen; - return true; - } - return false; - } - - // Fallback for unknown prism implementation: sparse exponential sampling. - const float fallbackMax = 10000f; - float tS = 0f; - while (tS < fallbackMax) { - Vector3 p = o + d * tS; - if (prism.Contains(p)) { - distance = tS * dirLen; - return true; - } - tS = tS == 0f ? 10f : tS * 1.5f; - } - return false; - } -} From 904482fffaa93ef80fce58c1285c7763fd39bafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Wed, 20 Aug 2025 01:25:24 -0300 Subject: [PATCH 25/25] Suggestions --- .../Graphics/DebugFieldRenderer.cs | 8 +-- .../Graphics/ImGuiController.cs | 56 ++++++++++++++++--- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs index c8bcafbb6..04b1edccc 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugFieldRenderer.cs @@ -414,22 +414,22 @@ private void RenderLine(DebugFieldWindow window, Vector3 start, Vector3 end) { // Calculate rotation to align cylinder with the line direction // The cylinder model is oriented along the Y axis by default Vector3 defaultUp = Vector3.UnitY; - Quaternion rotation; + 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) - rotation = dot > 0 ? Quaternion.Identity : Quaternion.CreateFromAxisAngle(Vector3.UnitX, MathF.PI); + 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)); - rotation = Quaternion.CreateFromAxisAngle(axis, angle); + rotationMatrix = Matrix4x4.CreateFromAxisAngle(axis, angle); } instanceBuffer.Transformation = Matrix4x4.Transpose(Matrix4x4.CreateScale(new Vector3(10.0f, distance / 2.0f, 10.0f)) * - Matrix4x4.CreateFromQuaternion(rotation) * + rotationMatrix * Matrix4x4.CreateTranslation(center)); UpdateWireframeInstance(window); diff --git a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs index bc91d5975..be6f2c17d 100644 --- a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs +++ b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs @@ -51,6 +51,10 @@ public class InputState { public class ImGuiController { public static readonly ILogger Logger = Log.Logger.ForContext(); + // 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; } @@ -240,6 +244,15 @@ 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(); @@ -268,6 +281,31 @@ public static void ClampWindowToViewport(float padding = 4f) { 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; } @@ -277,16 +315,16 @@ public static void ClampWindowToViewport(float padding = 4f) { // Re-fetch possibly updated size size = ImGui.GetWindowSize(); - // Clamp position - float newX = pos.X; - float newY = pos.Y; - if (newX < min.X) newX = min.X; - if (newY < min.Y) newY = min.Y; - if (newX + size.X > max.X) newX = max.X - size.X; - if (newY + size.Y > max.Y) newY = max.Y - size.Y; + // 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(newX - pos.X) > 0.5f || Math.Abs(newY - pos.Y) > 0.5f) { - ImGui.SetWindowPos(new Vector2(newX, newY), ImGuiCond.Always); + if (Math.Abs(newBodyX - pos.X) > 0.5f || Math.Abs(newBodyY - pos.Y) > 0.5f) { + ImGui.SetWindowPos(new Vector2(newBodyX, newBodyY), ImGuiCond.Always); } }